mlx-bun 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +43 -13
  2. package/THIRD_PARTY_LICENSES.md +9 -0
  3. package/docs/design/colibri-glm52-port.md +1479 -0
  4. package/docs/design/pre-colibri-stabilization.md +329 -0
  5. package/docs/investigations/colibri-oracle-pin.md +1284 -0
  6. package/docs/investigations/indexshare-performance-spike.md +357 -0
  7. package/docs/planning/release-notes-v0.0.12.md +80 -0
  8. package/docs/reference/cli.md +28 -2
  9. package/docs/reference/distribution.md +25 -1
  10. package/docs/reference/embedding.md +5 -2
  11. package/docs/reference/features-matrix.md +4 -2
  12. package/docs/reference/library-api.md +41 -13
  13. package/docs/reference/models.md +49 -2
  14. package/docs/reference/server-api.md +90 -9
  15. package/docs/reference/server-config.md +21 -14
  16. package/docs/reference/training.md +10 -1
  17. package/package.json +1 -1
  18. package/src/anthropic.ts +54 -44
  19. package/src/chat-template.ts +155 -10
  20. package/src/cli.ts +127 -8
  21. package/src/config.ts +71 -26
  22. package/src/diffusion/diffusion-generate.ts +14 -8
  23. package/src/download.ts +103 -12
  24. package/src/eval/ifeval.ts +63 -120
  25. package/src/eval/runner.ts +14 -5
  26. package/src/eval/tasks/ifeval.ts +167 -31
  27. package/src/expert-io.ts +391 -0
  28. package/src/expert-residency.ts +1134 -0
  29. package/src/expert-usage.ts +415 -0
  30. package/src/generate.ts +114 -17
  31. package/src/hf-push.ts +1 -1
  32. package/src/index.ts +14 -2
  33. package/src/jobs/runner.ts +37 -18
  34. package/src/kv-store.ts +147 -8
  35. package/src/lora.ts +77 -14
  36. package/src/media-fetch.ts +198 -56
  37. package/src/mlx/array.ts +43 -0
  38. package/src/mlx/autograd.ts +51 -14
  39. package/src/mlx/checkpoint.ts +46 -13
  40. package/src/mlx/custom-vjp.ts +83 -35
  41. package/src/model/factory.ts +96 -5
  42. package/src/model/gemma4-base.ts +32 -0
  43. package/src/model/glm52-atlas.ts +511 -0
  44. package/src/model/glm52-cache.ts +706 -0
  45. package/src/model/glm52-config.ts +262 -0
  46. package/src/model/glm52-container.ts +305 -0
  47. package/src/model/glm52-coupling.ts +388 -0
  48. package/src/model/glm52-dsa.ts +602 -0
  49. package/src/model/glm52-expert-layout.ts +253 -0
  50. package/src/model/glm52-layout.ts +152 -0
  51. package/src/model/glm52-memory.ts +473 -0
  52. package/src/model/glm52-mla.ts +786 -0
  53. package/src/model/glm52-moe.ts +223 -0
  54. package/src/model/glm52-pilot.ts +537 -0
  55. package/src/model/glm52-reference.ts +445 -0
  56. package/src/model/glm52-residency.ts +557 -0
  57. package/src/model/glm52-resident-weights.ts +135 -0
  58. package/src/model/glm52-streamed-experts.ts +453 -0
  59. package/src/model/glm52-streamed-metal.ts +610 -0
  60. package/src/model/glm52-weights.ts +503 -0
  61. package/src/model/glm52.ts +1153 -0
  62. package/src/model/support.ts +8 -1
  63. package/src/native/expert_io.c +523 -0
  64. package/src/native-pack.ts +6 -4
  65. package/src/responses.ts +62 -49
  66. package/src/serve/batch-scheduler.ts +95 -16
  67. package/src/serve/generation-gateway.ts +41 -16
  68. package/src/server.ts +362 -81
  69. package/src/spec/glm52-mtp-source.ts +392 -0
  70. package/src/spec/serve-loop.ts +99 -18
  71. package/src/spec/source.ts +31 -6
  72. package/src/ssd-cache.ts +6 -2
  73. package/src/tool-call.ts +30 -2
  74. package/src/train/job.ts +0 -10
  75. package/src/train/lora-params.ts +17 -6
  76. package/src/train/trainer.ts +59 -26
  77. package/src/web/app.html +13 -2
  78. package/src/web/app.js +30 -3
  79. package/src/web/src/status.ts +51 -3
  80. package/src/web-tools.ts +54 -2
  81. package/src/weights.ts +82 -19
@@ -15,6 +15,24 @@ import { JSCallback, ptr, read } from "bun:ffi";
15
15
  import { C, takeMlxError } from "./ffi";
16
16
  import { MlxArray } from "./array";
17
17
 
18
+ type CallbackHandle = Pick<JSCallback, "ptr" | "close">;
19
+ type NativeCall<F extends (...args: any[]) => any> =
20
+ (...args: Parameters<F>) => ReturnType<F>;
21
+
22
+ /** Constructor-only dependency seam for deterministic native failure tests. */
23
+ export interface CustomVjpConstructorHooks {
24
+ callback?: (
25
+ callback: ConstructorParameters<typeof JSCallback>[0],
26
+ definition: ConstructorParameters<typeof JSCallback>[1],
27
+ ) => CallbackHandle;
28
+ forwardClosureNew?: NativeCall<typeof C.mlx_closure_new_func_payload>;
29
+ vjpClosureNew?: NativeCall<typeof C.mlx_closure_custom_new_func_payload>;
30
+ transformedNew?: NativeCall<typeof C.mlx_closure_new>;
31
+ transform?: NativeCall<typeof C.mlx_custom_vjp>;
32
+ closureFree?: NativeCall<typeof C.mlx_closure_free>;
33
+ customClosureFree?: NativeCall<typeof C.mlx_closure_custom_free>;
34
+ }
35
+
18
36
  function readVec(vec: bigint): MlxArray[] {
19
37
  const n = Number(C.mlx_vector_array_size(vec));
20
38
  const out: MlxArray[] = [];
@@ -41,19 +59,30 @@ function setVec(outPtr: number, arrays: MlxArray[]): void {
41
59
  * pure functions of their arguments — the vjp re-reads everything from the
42
60
  * graph during the enclosing backward. */
43
61
  export class CustomVjp {
44
- readonly #fwdCb: JSCallback;
45
- readonly #vjpCb: JSCallback;
62
+ readonly #fwdCb: CallbackHandle;
63
+ readonly #vjpCb: CallbackHandle;
46
64
  readonly #fwdClosure: bigint;
47
65
  readonly #vjpClosure: bigint;
48
66
  readonly #combined: bigint;
67
+ readonly #freeClosure: NativeCall<typeof C.mlx_closure_free>;
68
+ readonly #freeCustomClosure: NativeCall<typeof C.mlx_closure_custom_free>;
49
69
  #error: string | null = null;
50
70
  #disposed = false;
51
71
 
52
72
  constructor(
53
73
  fwd: (inputs: MlxArray[]) => MlxArray[],
54
74
  vjp: (primals: MlxArray[], cotangents: MlxArray[], outputs: MlxArray[]) => MlxArray[],
75
+ hooks: CustomVjpConstructorHooks = {},
55
76
  ) {
56
- this.#fwdCb = new JSCallback(
77
+ const makeCallback = hooks.callback ?? ((callback, definition) => new JSCallback(callback, definition));
78
+ const forwardClosureNew = hooks.forwardClosureNew ?? C.mlx_closure_new_func_payload;
79
+ const vjpClosureNew = hooks.vjpClosureNew ?? C.mlx_closure_custom_new_func_payload;
80
+ const transformedNew = hooks.transformedNew ?? C.mlx_closure_new;
81
+ const transform = hooks.transform ?? C.mlx_custom_vjp;
82
+ this.#freeClosure = hooks.closureFree ?? C.mlx_closure_free;
83
+ this.#freeCustomClosure = hooks.customClosureFree ?? C.mlx_closure_custom_free;
84
+
85
+ const fwdCb = makeCallback(
57
86
  (outPtr: number, inVec: bigint, _payload: number): number => {
58
87
  try {
59
88
  const inputs = readVec(inVec);
@@ -70,37 +99,56 @@ export class CustomVjp {
70
99
  { args: ["ptr", "u64", "ptr"], returns: "i32" },
71
100
  );
72
101
 
73
- this.#vjpCb = new JSCallback(
74
- (resPtr: number, primalsVec: bigint, cotsVec: bigint, outsVec: bigint, _payload: number): number => {
75
- try {
76
- const primals = readVec(primalsVec);
77
- const cots = readVec(cotsVec);
78
- const outs = readVec(outsVec);
79
- const grads = vjp(primals, cots, outs);
80
- setVec(resPtr, grads);
81
- for (const a of [...primals, ...cots, ...outs]) a.dispose();
82
- for (const g of grads) g.dispose();
83
- return 0;
84
- } catch (e) {
85
- this.#error = e instanceof Error ? e.message : String(e);
86
- return 1;
87
- }
88
- },
89
- { args: ["ptr", "u64", "u64", "u64", "ptr"], returns: "i32" },
90
- );
91
-
92
- this.#fwdClosure = C.mlx_closure_new_func_payload(this.#fwdCb.ptr as never, null, null);
93
- this.#vjpClosure = C.mlx_closure_custom_new_func_payload(this.#vjpCb.ptr as never, null, null);
102
+ let vjpCb: CallbackHandle;
103
+ try {
104
+ vjpCb = makeCallback(
105
+ (resPtr: number, primalsVec: bigint, cotsVec: bigint, outsVec: bigint, _payload: number): number => {
106
+ try {
107
+ const primals = readVec(primalsVec);
108
+ const cots = readVec(cotsVec);
109
+ const outs = readVec(outsVec);
110
+ const grads = vjp(primals, cots, outs);
111
+ setVec(resPtr, grads);
112
+ for (const a of [...primals, ...cots, ...outs]) a.dispose();
113
+ for (const g of grads) g.dispose();
114
+ return 0;
115
+ } catch (e) {
116
+ this.#error = e instanceof Error ? e.message : String(e);
117
+ return 1;
118
+ }
119
+ },
120
+ { args: ["ptr", "u64", "u64", "u64", "ptr"], returns: "i32" },
121
+ );
122
+ } catch (e) {
123
+ // Callback construction is itself fallible. The native-closure guard
124
+ // below starts only after both callbacks exist, so this partial state
125
+ // must release the already-created forward callback here.
126
+ fwdCb.close();
127
+ throw e;
128
+ }
94
129
 
95
- const slot = new BigUint64Array([C.mlx_closure_new()]);
96
- if (C.mlx_custom_vjp(ptr(slot), this.#fwdClosure, this.#vjpClosure) !== 0) {
97
- C.mlx_closure_free(this.#fwdClosure);
98
- C.mlx_closure_custom_free(this.#vjpClosure);
99
- this.#fwdCb.close();
100
- this.#vjpCb.close();
101
- throw new Error(`mlx_custom_vjp failed: ${takeMlxError() ?? ""}`);
130
+ let fwdClosure: bigint | null = null;
131
+ let vjpClosure: bigint | null = null;
132
+ let slot: BigUint64Array | null = null;
133
+ try {
134
+ fwdClosure = forwardClosureNew(fwdCb.ptr as never, null, null);
135
+ vjpClosure = vjpClosureNew(vjpCb.ptr as never, null, null);
136
+ slot = new BigUint64Array([transformedNew()]);
137
+ if (transform(ptr(slot), fwdClosure, vjpClosure) !== 0)
138
+ throw new Error(`mlx_custom_vjp failed: ${takeMlxError() ?? ""}`);
139
+ this.#fwdCb = fwdCb;
140
+ this.#vjpCb = vjpCb;
141
+ this.#fwdClosure = fwdClosure;
142
+ this.#vjpClosure = vjpClosure;
143
+ this.#combined = read.u64(ptr(slot), 0);
144
+ } catch (e) {
145
+ if (slot) this.#freeClosure(read.u64(ptr(slot), 0));
146
+ if (fwdClosure !== null) this.#freeClosure(fwdClosure);
147
+ if (vjpClosure !== null) this.#freeCustomClosure(vjpClosure);
148
+ fwdCb.close();
149
+ vjpCb.close();
150
+ throw e;
102
151
  }
103
- this.#combined = read.u64(ptr(slot), 0);
104
152
  }
105
153
 
106
154
  apply(inputs: MlxArray[]): MlxArray[] {
@@ -127,9 +175,9 @@ export class CustomVjp {
127
175
  dispose(): void {
128
176
  if (this.#disposed) return;
129
177
  this.#disposed = true;
130
- C.mlx_closure_free(this.#combined);
131
- C.mlx_closure_free(this.#fwdClosure);
132
- C.mlx_closure_custom_free(this.#vjpClosure);
178
+ this.#freeClosure(this.#combined);
179
+ this.#freeClosure(this.#fwdClosure);
180
+ this.#freeCustomClosure(this.#vjpClosure);
133
181
  this.#fwdCb.close();
134
182
  this.#vjpCb.close();
135
183
  }
@@ -4,8 +4,9 @@
4
4
  // Gemma4Model and only override forwardLayers (with their own
5
5
  // cache-signature guard), so the choice is always safe.
6
6
 
7
- import type { ModelConfig } from "../config";
8
- import type { Weights } from "../weights";
7
+ import { loadModelConfig, type ModelConfig } from "../config";
8
+ import { totalmem } from "node:os";
9
+ import { Weights } from "../weights";
9
10
  import { Gemma4Model } from "./gemma4";
10
11
  import { configFingerprint } from "./fingerprint";
11
12
  import { GENERATED } from "./generated";
@@ -14,19 +15,109 @@ import { Qwen35Model } from "./qwen3_5";
14
15
  import { Qwen3Model } from "./qwen3";
15
16
  import { Qwen3MoeModel } from "./qwen3-moe";
16
17
  import { DiffusionGemmaModel } from "./diffusion-gemma";
18
+ import { Glm52Model } from "./glm52";
19
+ import {
20
+ GLM52_G5_DEFAULT_CONTEXT_TOKENS,
21
+ GLM52_G5_DEFAULT_MAX_GENERATION_TOKENS,
22
+ GLM52_G5_DEFAULT_PROCESS_LIMIT_BYTES,
23
+ planGlm52MemoryForArtifact,
24
+ type Glm52MemoryPlan,
25
+ } from "./glm52-memory";
17
26
  import { UniversalDenseModel } from "./universal/dense";
18
27
  import { genericArgsFor, GENERIC_MODEL_TYPES, remapModelType } from "./universal/archs";
19
- import { isDiffusionGemmaConfig, isMiniCPM5Config, isQwen35Config, isQwen3Config, isQwen3MoeConfig } from "./support";
28
+ import { isDiffusionGemmaConfig, isGlm52Config, isMiniCPM5Config, isQwen35Config, isQwen3Config, isQwen3MoeConfig } from "./support";
20
29
 
21
30
  export type RuntimeModel =
22
31
  | Gemma4Model | MiniCPM5Model | Qwen35Model | Qwen3Model | Qwen3MoeModel
23
- | DiffusionGemmaModel | UniversalDenseModel;
32
+ | DiffusionGemmaModel | Glm52Model | UniversalDenseModel;
33
+
34
+ export interface Glm52RuntimeOpenOptions {
35
+ /** Whole-process ceiling. Defaults to the smaller of the validated 25 GiB
36
+ * preset and physical RAM. */
37
+ memoryBudgetBytes?: number;
38
+ /** Context reserved by the exact GLM resource equation. */
39
+ contextTokens?: number;
40
+ /** Generated-token allowance within contextTokens. */
41
+ maxGenerationTokens?: number;
42
+ /** Maximum ordinary continuous-batch rows. */
43
+ batchSize?: number;
44
+ /** Native checkpoint MTP row; on by default. */
45
+ enableMtp?: boolean;
46
+ mtpDraftTokens?: number;
47
+ /** Test/diagnostic override; normal callers use physical RAM. */
48
+ machineBytes?: number;
49
+ /** Native expert-I/O dylib override. Packaged/dev defaults are automatic. */
50
+ libraryPath?: string;
51
+ }
52
+
53
+ /** Open the direct Colibri artifact through the validated bounded expert
54
+ * runtime. The header-only plan runs before any resident tensor or expert slab
55
+ * is opened, so an impossible configuration fails without committing memory. */
56
+ export async function openGlm52RuntimeModel(
57
+ modelDir: string,
58
+ options: Glm52RuntimeOpenOptions = {},
59
+ ): Promise<{ model: Glm52Model; plan: Glm52MemoryPlan }> {
60
+ const machineBytes = options.machineBytes ?? totalmem();
61
+ const processLimitBytes = options.memoryBudgetBytes ??
62
+ Math.min(GLM52_G5_DEFAULT_PROCESS_LIMIT_BYTES, machineBytes);
63
+ const plan = await planGlm52MemoryForArtifact(modelDir, {
64
+ machineBytes,
65
+ processLimitBytes,
66
+ contextTokens: options.contextTokens ?? GLM52_G5_DEFAULT_CONTEXT_TOKENS,
67
+ maxGenerationTokens:
68
+ options.maxGenerationTokens ?? GLM52_G5_DEFAULT_MAX_GENERATION_TOKENS,
69
+ batchSize: options.batchSize ?? 1,
70
+ enableMtp: options.enableMtp !== false,
71
+ mtpDraftTokens: options.mtpDraftTokens,
72
+ });
73
+ const model = await Glm52Model.openStreamed(modelDir, {
74
+ budgetBytes: plan.processLimitBytes,
75
+ reserveBytes: plan.runtimeReserveBytes,
76
+ workingSlots: plan.mainWorkingSlots,
77
+ maxSlotsPerLayer: 1,
78
+ workers: 2,
79
+ libraryPath: options.libraryPath,
80
+ decodeKernel: "metal",
81
+ enableMtp: plan.enableMtp,
82
+ mtpDraftTokens: plan.mtpDraftTokens,
83
+ });
84
+ if (model.expertRuntime?.plan.plannedBytes !== plan.plannedProcessBytes) {
85
+ const actual = model.expertRuntime?.plan.plannedBytes ?? 0;
86
+ model.dispose();
87
+ throw new Error(
88
+ `GLM runtime plan ${actual} != preflight resource equation ` +
89
+ `${plan.plannedProcessBytes}`,
90
+ );
91
+ }
92
+ return { model, plan };
93
+ }
94
+
95
+ /**
96
+ * Artifact-aware construction. GLM-5.2's Colibri snapshot has no ordinary
97
+ * model.safetensors.index.json, so it must bypass Weights.open and use its
98
+ * dedicated header catalog/tensor source.
99
+ */
100
+ export async function openModel(
101
+ modelDir: string,
102
+ options: Glm52RuntimeOpenOptions = {},
103
+ ): Promise<RuntimeModel> {
104
+ const config = await loadModelConfig(modelDir);
105
+ if (isGlm52Config(config))
106
+ return (await openGlm52RuntimeModel(modelDir, options)).model;
107
+ return createModel(await Weights.open(modelDir), config);
108
+ }
24
109
 
25
110
  /** Dispatch ladder (docs/design/generic-model-support.md §3.3):
26
111
  * dedicated → generated/monolith (gemma4*) → generic (Tier-0 universal)
27
112
  * → reject with a helpful error. Generic never shadows a dedicated port. */
28
113
  export function createModel(weights: Weights, config: ModelConfig): RuntimeModel {
29
114
  // 1. Dedicated classes.
115
+ if (isGlm52Config(config)) {
116
+ throw new Error(
117
+ "glm_moe_dsa uses the direct Colibri container; construct it with " +
118
+ "openModel(modelDir) or Glm52Model.open(modelDir)",
119
+ );
120
+ }
30
121
  // DiffusionGemma is non-autoregressive — generate() detects it and routes to
31
122
  // the denoising engine instead of the AR decode loop. It exposes the shared
32
123
  // RuntimeModel surface (config/weightsBytes/loraState/makeCache) with the
@@ -54,7 +145,7 @@ export function createModel(weights: Weights, config: ModelConfig): RuntimeModel
54
145
  throw new Error(
55
146
  `unsupported model_type "${config.modelType}"` +
56
147
  (arch !== config.modelType ? ` (mlx-lm remaps it to "${arch}")` : "") +
57
- ` — targeted: gemma4*, diffusion_gemma, qwen3_5, qwen3, qwen3_moe, MiniCPM5;` +
148
+ ` — targeted: gemma4*, diffusion_gemma, glm_moe_dsa, qwen3_5, qwen3, qwen3_moe, MiniCPM5;` +
58
149
  ` generic (Tier-0): ${[...GENERIC_MODEL_TYPES].sort().join(", ")}`,
59
150
  );
60
151
  }
@@ -234,6 +234,38 @@ export interface Cache {
234
234
  dispose(): void;
235
235
  }
236
236
 
237
+ /**
238
+ * Capability contract for cache layouts that own their dynamic-row batching.
239
+ * The scheduler uses this for non-K/V state families (for example GLM's
240
+ * compressed MLA/DSA state) instead of teaching the scheduler their tensor
241
+ * layout. `mergeRows` writes into an empty cache and does not consume inputs;
242
+ * extracted rows are independent owned caches.
243
+ */
244
+ export interface BatchableCache extends Cache {
245
+ readonly batchSize: number | null;
246
+ readonly rowOffsets: readonly number[];
247
+ readonly leftPad: readonly number[];
248
+ readonly maxTokens?: number;
249
+ makeEmptyBatch(): BatchableCache;
250
+ mergeRows(rows: readonly Cache[]): void;
251
+ extractRow(row: number): Cache;
252
+ filterRows(keep: readonly number[]): void;
253
+ projectedBytes(tokens: number): number;
254
+ }
255
+
256
+ export function isBatchableCache(cache: Cache): cache is BatchableCache {
257
+ const candidate = cache as Partial<BatchableCache>;
258
+ return (
259
+ typeof candidate.makeEmptyBatch === "function" &&
260
+ typeof candidate.mergeRows === "function" &&
261
+ typeof candidate.extractRow === "function" &&
262
+ typeof candidate.filterRows === "function" &&
263
+ typeof candidate.projectedBytes === "function" &&
264
+ Array.isArray(candidate.rowOffsets) &&
265
+ Array.isArray(candidate.leftPad)
266
+ );
267
+ }
268
+
237
269
  /** How one decode step interacts with a cache under compiled decode
238
270
  * (returned by prepareDecodeStep, consumed by compiled-decode.ts):
239
271
  * - "concat": the compiled graph fetches concat(active prefix, new kv)