mlx-bun 0.0.12 → 0.2.0

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 (226) hide show
  1. package/README.md +40 -24
  2. package/bin/mlx-bun.mjs +1 -1
  3. package/docs/{design → archive}/hlg-sampling.md +3 -3
  4. package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
  5. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
  6. package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
  7. package/docs/archive/planning/release-notes-v0.0.13.md +77 -0
  8. package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
  9. package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
  10. package/docs/design/colibri-glm52-port.md +8 -1
  11. package/docs/design/compat-cli-surface-design.md +1 -1
  12. package/docs/design/decode-speed-program.md +1 -1
  13. package/docs/design/docs-reorg-plan.md +1 -1
  14. package/docs/design/dspark-serving-program.md +1 -1
  15. package/docs/design/dspark-speculative-decoding.md +53 -3
  16. package/docs/design/grammar-spec-batching-integration.md +1 -1
  17. package/docs/design/mlx-lm-serving-execution-seam.md +81 -0
  18. package/docs/design/orpo-dynamic-lambda.md +1 -1
  19. package/docs/design/orpo-future-enhancements.md +1 -1
  20. package/docs/design/orpo-training.md +2 -2
  21. package/docs/design/parity-tier-dag.md +1 -1
  22. package/docs/design/repo-cleanup-plan.md +46 -4
  23. package/docs/design/segmented-backward-training.md +2 -2
  24. package/docs/design/ssd-kv-cold-tier.md +127 -0
  25. package/docs/design/turboquant-weights.md +467 -0
  26. package/docs/design/w4a16-compute-precision-spike.md +1 -1
  27. package/docs/design/web-chat-redesign.md +5 -5
  28. package/docs/planning/chunk-task-roadmap.md +1 -1
  29. package/docs/planning/curve-sampler-research-plan.md +4 -4
  30. package/docs/planning/memory-docs-and-dag-plan.md +1 -1
  31. package/docs/planning/release-notes-v0.2.0.md +49 -0
  32. package/docs/planning/video-series.md +421 -0
  33. package/docs/reference/cli.md +15 -4
  34. package/docs/reference/distribution.md +1 -0
  35. package/docs/reference/features-matrix.md +3 -1
  36. package/docs/reference/library-api.md +41 -1
  37. package/docs/reference/models.md +33 -0
  38. package/docs/reference/server-api.md +86 -19
  39. package/docs/reference/server-config.md +57 -33
  40. package/package.json +2 -2
  41. package/src/anthropic.ts +92 -3
  42. package/src/chat-template.ts +34 -1
  43. package/src/cli.ts +131 -36
  44. package/src/config.ts +10 -0
  45. package/src/eval/kl.ts +27 -20
  46. package/src/eval/runner.ts +23 -4
  47. package/src/eval/tasks/bfcl.ts +2 -1
  48. package/src/eval/tasks/gsm8k.ts +2 -1
  49. package/src/eval/tasks/hashhop.ts +2 -1
  50. package/src/eval/tasks/humaneval.ts +5 -4
  51. package/src/eval/tasks/ifeval.ts +2 -1
  52. package/src/eval/tasks/mmlu.ts +2 -1
  53. package/src/expert-io.ts +3 -1
  54. package/src/expert-offload.ts +3 -1
  55. package/src/expert-trace.ts +3 -1
  56. package/src/fit.ts +24 -43
  57. package/src/flags.ts +7 -9
  58. package/src/generate.ts +107 -92
  59. package/src/grammar.ts +2 -1
  60. package/src/harness-pi.ts +1 -1
  61. package/src/index.ts +20 -0
  62. package/src/jobs/job-entry.ts +3 -2
  63. package/src/kv-scheme.ts +199 -0
  64. package/src/kv-store.ts +370 -205
  65. package/src/media-fetch.ts +19 -3
  66. package/src/memory/model.ts +2 -1
  67. package/src/memory/vault.ts +2 -1
  68. package/src/memory-plan.ts +18 -0
  69. package/src/mlx/ffi.ts +8 -11
  70. package/src/mlx/ops.ts +31 -6
  71. package/src/model/batched-mask.ts +1 -1
  72. package/src/model/batched-quant.ts +1 -1
  73. package/src/model/batched-rotating-quant.ts +43 -97
  74. package/src/model/batched-rotating-state.ts +73 -0
  75. package/src/model/batched-rotating.ts +56 -105
  76. package/src/model/batched-row-storage.ts +127 -0
  77. package/src/model/compiled-decode.ts +3 -2
  78. package/src/model/factory.ts +44 -49
  79. package/src/model/gemma4-base.ts +163 -10
  80. package/src/model/generated/gemma4-12b.ts +50 -146
  81. package/src/model/generated/gemma4-26b.ts +32 -92
  82. package/src/model/generated/gemma4-e4b.ts +26 -74
  83. package/src/model/glm52-cache.ts +19 -9
  84. package/src/model/glm52-memory.ts +28 -1
  85. package/src/model/minicpm5.ts +2 -1
  86. package/src/model/profile.ts +433 -0
  87. package/src/model/qwen3-delta.ts +98 -2
  88. package/src/model/qwen3-mrope.ts +252 -0
  89. package/src/model/qwen3_5.ts +217 -25
  90. package/src/native/frame_extract.swift +140 -0
  91. package/src/native-pack.ts +13 -6
  92. package/src/pi-web.ts +6 -5
  93. package/src/prompt-cache.ts +2 -2
  94. package/src/quantize/atomic-output.ts +38 -0
  95. package/src/quantize/calibration.ts +2 -1
  96. package/src/quantize/config-writer.ts +6 -0
  97. package/src/quantize/index.ts +28 -1
  98. package/src/quantize/job.ts +11 -0
  99. package/src/quantize/quantizer.ts +265 -100
  100. package/src/quantize/rotate.ts +603 -0
  101. package/src/quantize/safetensors-writer.ts +79 -1
  102. package/src/quantize/sensitivity.ts +41 -59
  103. package/src/quantize/weight-transform.ts +292 -0
  104. package/src/responses.ts +102 -2
  105. package/src/runtime-config.ts +67 -0
  106. package/src/sampler.ts +272 -0
  107. package/src/serve/admin-routes.ts +184 -0
  108. package/src/serve/aux-routes.ts +132 -0
  109. package/src/serve/batch-scheduler.ts +200 -87
  110. package/src/serve/completion-executor.ts +346 -0
  111. package/src/serve/completion-sink.ts +157 -0
  112. package/src/serve/discovery-routes.ts +205 -0
  113. package/src/serve/generation-gateway.ts +116 -109
  114. package/src/serve/kv-budget.ts +14 -0
  115. package/src/serve/lab-routes.ts +338 -0
  116. package/src/serve/model-admin-routes.ts +153 -0
  117. package/src/serve/prompt-response-trace.ts +147 -0
  118. package/src/serve/request-plan.ts +158 -0
  119. package/src/serve/static-routes.ts +101 -0
  120. package/src/server.ts +768 -1367
  121. package/src/spec/dflash-source.ts +2 -1
  122. package/src/spec/dspark/module-dflash.ts +1 -1
  123. package/src/spec/glm52-mtp-source.ts +8 -0
  124. package/src/spec/qwen-mtp-source.ts +431 -0
  125. package/src/spec/serve-loop.ts +36 -68
  126. package/src/ssd-cache.ts +17 -14
  127. package/src/ssd-durability.ts +195 -0
  128. package/src/train/flash-cce.ts +12 -11
  129. package/src/train/forward.ts +32 -1
  130. package/src/train/loss.ts +2 -1
  131. package/src/train/segmented.ts +4 -3
  132. package/src/train/trainer.ts +3 -2
  133. package/src/vision/preprocess.ts +1 -1
  134. package/src/vision/prompt.ts +47 -1
  135. package/src/vision/qwen3vl-preprocess.ts +409 -0
  136. package/src/vision/qwen3vl-prompt.ts +196 -0
  137. package/src/vision/qwen3vl-tower.ts +513 -0
  138. package/src/vision/video-frames.ts +148 -0
  139. package/src/web/app.js +10 -3
  140. package/src/web/src/chat.ts +12 -5
  141. package/src/weights.ts +81 -26
  142. /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
  143. /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
  144. /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
  145. /package/docs/{investigations → archive/investigations}/curve-runs/bisector-route.json +0 -0
  146. /package/docs/{investigations → archive/investigations}/curve-runs/bisector-trace.json +0 -0
  147. /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.json +0 -0
  148. /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.svg +0 -0
  149. /package/docs/{investigations → archive/investigations}/curve-terrain.html +0 -0
  150. /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
  151. /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
  152. /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
  153. /package/docs/{investigations → archive/investigations}/hlg-ab.html +0 -0
  154. /package/docs/{investigations → archive/investigations}/hlg-explorer.html +0 -0
  155. /package/docs/{investigations → archive/investigations}/hlg-figs/cliff.svg +0 -0
  156. /package/docs/{investigations → archive/investigations}/hlg-figs/cross.svg +0 -0
  157. /package/docs/{investigations → archive/investigations}/hlg-figs/edges.svg +0 -0
  158. /package/docs/{investigations → archive/investigations}/hlg-figs/k20-grids.svg +0 -0
  159. /package/docs/{investigations → archive/investigations}/hlg-figs/k20-peaks.svg +0 -0
  160. /package/docs/{investigations → archive/investigations}/hlg-figs/lw-curve.svg +0 -0
  161. /package/docs/{investigations → archive/investigations}/hlg-figs/terrain.svg +0 -0
  162. /package/docs/{investigations → archive/investigations}/hlg-report.html +0 -0
  163. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-ab.json +0 -0
  164. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind-prompts.md +0 -0
  165. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind.json +0 -0
  166. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cliff.json +0 -0
  167. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cross.json +0 -0
  168. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-edge.json +0 -0
  169. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-eval1.json +0 -0
  170. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-frontier.json +0 -0
  171. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid.json +0 -0
  172. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid2.json +0 -0
  173. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-lw-coarse.json +0 -0
  174. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-map.json +0 -0
  175. /package/docs/{investigations → archive/investigations}/hlg-runs/run0-original.log +0 -0
  176. /package/docs/{investigations → archive/investigations}/hlg-runs/runA-recommended.log +0 -0
  177. /package/docs/{investigations → archive/investigations}/hlg-runs/runAA-cliff-cpm5.log +0 -0
  178. /package/docs/{investigations → archive/investigations}/hlg-runs/runAB-grid2-e4b.log +0 -0
  179. /package/docs/{investigations → archive/investigations}/hlg-runs/runAC-peak-e4b-partial.log +0 -0
  180. /package/docs/{investigations → archive/investigations}/hlg-runs/runAD-ab-samples.log +0 -0
  181. /package/docs/{investigations → archive/investigations}/hlg-runs/runAE-edge-e4b.log +0 -0
  182. /package/docs/{investigations → archive/investigations}/hlg-runs/runAF-logit-calibration.log +0 -0
  183. /package/docs/{investigations → archive/investigations}/hlg-runs/runAG-eval-config2.log +0 -0
  184. /package/docs/{investigations → archive/investigations}/hlg-runs/runB-gain-variants.log +0 -0
  185. /package/docs/{investigations → archive/investigations}/hlg-runs/runC-pivots.log +0 -0
  186. /package/docs/{investigations → archive/investigations}/hlg-runs/runD-oetf.log +0 -0
  187. /package/docs/{investigations → archive/investigations}/hlg-runs/runE-eotf.log +0 -0
  188. /package/docs/{investigations → archive/investigations}/hlg-runs/runF-shaper-working.log +0 -0
  189. /package/docs/{investigations → archive/investigations}/hlg-runs/runG-shaper-sweep.log +0 -0
  190. /package/docs/{investigations → archive/investigations}/hlg-runs/runH-wsweep.log +0 -0
  191. /package/docs/{investigations → archive/investigations}/hlg-runs/runI-asweep.log +0 -0
  192. /package/docs/{investigations → archive/investigations}/hlg-runs/runJ-knobmap.log +0 -0
  193. /package/docs/{investigations → archive/investigations}/hlg-runs/runK-frontier.log +0 -0
  194. /package/docs/{investigations → archive/investigations}/hlg-runs/runL-widemap.log +0 -0
  195. /package/docs/{investigations → archive/investigations}/hlg-runs/runM-frontier-emb.log +0 -0
  196. /package/docs/{investigations → archive/investigations}/hlg-runs/runN-verify.log +0 -0
  197. /package/docs/{investigations → archive/investigations}/hlg-runs/runO-frontier-seed5000.log +0 -0
  198. /package/docs/{investigations → archive/investigations}/hlg-runs/runP-grid-AxSm.log +0 -0
  199. /package/docs/{investigations → archive/investigations}/hlg-runs/runQ-grid-tgW.log +0 -0
  200. /package/docs/{investigations → archive/investigations}/hlg-runs/runR-blind-transcript.log +0 -0
  201. /package/docs/{investigations → archive/investigations}/hlg-runs/runS-cross-coherence.log +0 -0
  202. /package/docs/{investigations → archive/investigations}/hlg-runs/runT-cross-cpm5.log +0 -0
  203. /package/docs/{investigations → archive/investigations}/hlg-runs/runU-cross-12b.log +0 -0
  204. /package/docs/{investigations → archive/investigations}/hlg-runs/runV-lw-e4b.log +0 -0
  205. /package/docs/{investigations → archive/investigations}/hlg-runs/runW-lw-12b.log +0 -0
  206. /package/docs/{investigations → archive/investigations}/hlg-runs/runX-lw-cpm5.log +0 -0
  207. /package/docs/{investigations → archive/investigations}/hlg-runs/runY-cliff-e4b.log +0 -0
  208. /package/docs/{investigations → archive/investigations}/hlg-runs/runZ-cliff-12b.log +0 -0
  209. /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
  210. /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
  211. /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
  212. /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
  213. /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
  214. /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
  215. /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
  216. /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
  217. /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
  218. /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
  219. /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
  220. /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
  221. /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
  222. /package/docs/{planning → archive/planning}/journal.md +0 -0
  223. /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
  224. /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
  225. /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
  226. /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
@@ -25,6 +25,12 @@
25
25
  import { MlxArray } from "../mlx/array";
26
26
  import * as ops from "../mlx/ops";
27
27
  import { RotatingKVCache, type Cache, type Mask } from "./gemma4-base";
28
+ import { BatchedRotatingState } from "./batched-rotating-state";
29
+ import {
30
+ mergeStorageRows,
31
+ plainRowStorage,
32
+ temporalStorageView,
33
+ } from "./batched-row-storage";
28
34
 
29
35
  const STEP = 256;
30
36
 
@@ -78,39 +84,46 @@ export function buildBatchedRotatingMask(
78
84
  export class BatchedRotatingCache implements Cache {
79
85
  keys: MlxArray | null = null;
80
86
  values: MlxArray | null = null;
81
- /** Per-row absolute position (drives RoPE) and buffer padding. */
82
- offsetArr: number[];
83
- leftPad: number[];
84
- #idx = 0;
85
- #offset = 0; // scalar total tokens processed (mlx-lm _offset)
86
- #rotated = false;
87
+ readonly #rows: BatchedRotatingState;
87
88
  #ropeArr: MlxArray | null = null;
88
89
  #ropeForOffset = -1;
89
90
  readonly maxSize: number;
90
91
 
91
92
  constructor(maxSize: number, leftPad: number[]) {
92
93
  this.maxSize = maxSize;
93
- this.leftPad = [...leftPad];
94
- this.offsetArr = leftPad.map((l) => -l);
94
+ this.#rows = new BatchedRotatingState(maxSize, leftPad);
95
95
  }
96
96
 
97
+ /** Same signature as the serial RotatingKVCache — the scheduler's merge
98
+ * guards recognize a batched ring as "rotating-plain + RowBatchCache"
99
+ * (batch-scheduler #mergeJoiner prevRot branch); without this the next
100
+ * join sees "unknown", skips the prevRot rows, and replaces the batch's
101
+ * sliding-layer KV with a one-row cache (B≥3 collapse). Guard order
102
+ * everywhere is isRowBatchCache FIRST, so the shared string never
103
+ * misroutes a batched cache into a serial-only path. */
104
+ signature(): string { return "kv:rotating-plain"; }
105
+
106
+ get offsetArr(): number[] { return this.#rows.offsets; }
107
+ get leftPad(): number[] { return this.#rows.leftPad; }
108
+ get batchSize(): number { return this.#rows.batchSize; }
109
+
97
110
  /** Current batch size — tracks filter() (which shrinks the per-row arrays). */
98
111
  get #B(): number {
99
- return this.leftPad.length;
112
+ return this.#rows.batchSize;
100
113
  }
101
114
 
102
115
  /** mlx-lm uses `cache.offset` (the per-row array) for the scalar interface
103
116
  * too; we expose the scalar total as `offset` and the per-row positions via
104
117
  * `ropeOffsetArr` (the model's per-row RoPE path). */
105
118
  get offset(): number {
106
- return this.#offset;
119
+ return this.#rows.totalOffset;
107
120
  }
108
121
 
109
122
  get ropeOffsetArr(): MlxArray {
110
- if (this.#ropeArr && this.#ropeForOffset === this.#offset) return this.#ropeArr;
123
+ if (this.#ropeArr && this.#ropeForOffset === this.offset) return this.#ropeArr;
111
124
  this.#ropeArr?.dispose();
112
125
  this.#ropeArr = MlxArray.fromInt32(Int32Array.from(this.offsetArr), [this.#B]);
113
- this.#ropeForOffset = this.#offset;
126
+ this.#ropeForOffset = this.offset;
114
127
  return this.#ropeArr;
115
128
  }
116
129
 
@@ -120,7 +133,7 @@ export class BatchedRotatingCache implements Cache {
120
133
  mode: "array",
121
134
  arr: buildBatchedRotatingMask(
122
135
  this.#B, N, this.leftPad, this.maxSize, window,
123
- this.#idx, this.#offset, this.#rotated,
136
+ this.#rows.ringIndex, this.offset, this.#rows.rotated,
124
137
  ),
125
138
  };
126
139
  }
@@ -131,7 +144,7 @@ export class BatchedRotatingCache implements Cache {
131
144
  const vD = v.shape[3]!;
132
145
  if (S !== 1)
133
146
  throw new Error("BatchedRotatingCache supports N=1 decode updates only (solo-prefill then merge)");
134
- const prev = this.#offset;
147
+ const prev = this.offset;
135
148
 
136
149
  // Grow the buffer (in STEP chunks) until it reaches maxSize.
137
150
  if (!this.keys || (prev >= this.keys.shape[2]! && this.keys.shape[2]! < this.maxSize)) {
@@ -148,7 +161,7 @@ export class BatchedRotatingCache implements Cache {
148
161
  this.keys = newK;
149
162
  this.values = newV;
150
163
  }
151
- this.#idx = prev;
164
+ this.#rows.markGrown(prev);
152
165
  }
153
166
 
154
167
  // Trim any overshoot past maxSize (decrements left padding persistently).
@@ -160,34 +173,27 @@ export class BatchedRotatingCache implements Cache {
160
173
  this.values!.dispose();
161
174
  this.keys = tk;
162
175
  this.values = tv;
163
- this.#idx = this.maxSize;
164
- this.leftPad = this.leftPad.map((x) => x - trimSize);
176
+ this.#rows.trimOvershoot(trimSize);
165
177
  }
166
178
 
167
179
  // Rotate when the write head reaches the end of the ring.
168
- if (this.#idx === this.maxSize) {
169
- this.#rotated = true;
170
- this.#idx = 0;
171
- }
172
- if (this.#rotated) this.leftPad = this.leftPad.map((x) => x - S);
180
+ const writeIndex = this.#rows.beginWrite(S);
173
181
 
174
182
  // Write the new K/V at the ring head.
175
183
  const [, , SK, DK] = this.keys!.shape as [number, number, number, number];
176
- const k2 = ops.sliceUpdate(this.keys!, k, [0, 0, this.#idx, 0], [B, H, this.#idx + S, DK]);
177
- const v2 = ops.sliceUpdate(this.values!, v, [0, 0, this.#idx, 0], [B, H, this.#idx + S, vD]);
184
+ const k2 = ops.sliceUpdate(this.keys!, k, [0, 0, writeIndex, 0], [B, H, writeIndex + S, DK]);
185
+ const v2 = ops.sliceUpdate(this.values!, v, [0, 0, writeIndex, 0], [B, H, writeIndex + S, vD]);
178
186
  this.keys!.dispose();
179
187
  this.values!.dispose();
180
188
  this.keys = k2;
181
189
  this.values = v2;
182
- this.#offset += S;
183
- this.offsetArr = this.offsetArr.map((x) => x + S);
184
- this.#idx += S;
190
+ this.#rows.commitWrite(S);
185
191
 
186
192
  // Return the populated prefix (ring not yet full) or the whole buffer.
187
- if (this.#offset < this.maxSize) {
193
+ if (this.offset < this.maxSize) {
188
194
  return [
189
- this.keys.slice([0, 0, 0, 0], [B, H, this.#offset, DK]),
190
- this.values.slice([0, 0, 0, 0], [B, H, this.#offset, vD]),
195
+ this.keys.slice([0, 0, 0, 0], [B, H, this.offset, DK]),
196
+ this.values.slice([0, 0, 0, 0], [B, H, this.offset, vD]),
191
197
  ];
192
198
  }
193
199
  return [
@@ -206,27 +212,10 @@ export class BatchedRotatingCache implements Cache {
206
212
  /** Ring contents in temporal order, cut to the valid length (extract). */
207
213
  temporalView(): [MlxArray, MlxArray] {
208
214
  if (!this.keys || !this.values) throw new Error("cache is empty");
209
- const valid = Math.min(this.#offset, this.maxSize);
210
- const order = (a: MlxArray): MlxArray => {
211
- const [B, H, Sbuf, D] = a.shape as [number, number, number, number];
212
- let t: MlxArray;
213
- if (this.#idx === Sbuf) {
214
- t = a.slice([0, 0, 0, 0], [B, H, Sbuf, D]);
215
- } else if (this.#idx < this.#offset) {
216
- const tail = a.slice([0, 0, this.#idx, 0], [B, H, Sbuf, D]);
217
- const head = a.slice([0, 0, 0, 0], [B, H, this.#idx, D]);
218
- t = ops.concatAxis([tail, head], 2);
219
- tail.dispose();
220
- head.dispose();
221
- } else {
222
- t = a.slice([0, 0, 0, 0], [B, H, this.#idx, D]);
223
- }
224
- const [, , , Dd] = t.shape as [number, number, number, number];
225
- const cut = t.slice([0, 0, 0, 0], [B, H, valid, Dd]);
226
- t.dispose();
227
- return cut;
228
- };
229
- return [order(this.keys), order(this.values)];
215
+ return [
216
+ temporalStorageView(plainRowStorage, this.keys, this.#rows),
217
+ temporalStorageView(plainRowStorage, this.values, this.#rows),
218
+ ];
230
219
  }
231
220
 
232
221
  /** mlx-lm `BatchRotatingKVCache.extract` (models/cache.py:1417): row `i`
@@ -242,34 +231,13 @@ export class BatchedRotatingCache implements Cache {
242
231
  extractRow(i: number): RotatingKVCache | null {
243
232
  if (!this.keys || !this.values) return null;
244
233
  const pad = Math.max(0, this.leftPad[i]!);
245
- const valid = Math.min(this.#offset, this.maxSize);
246
- const cut = (a: MlxArray): MlxArray => {
247
- const [, H, Sbuf, D] = a.shape as [number, number, number, number];
248
- const row = a.slice([i, 0, 0, 0], [i + 1, H, Sbuf, D]);
249
- // temporal order — same three cases as temporalView, single row.
250
- let t: MlxArray;
251
- if (this.#idx === Sbuf) {
252
- t = row;
253
- } else if (this.#idx < this.#offset) {
254
- const tail = row.slice([0, 0, this.#idx, 0], [1, H, Sbuf, D]);
255
- const head = row.slice([0, 0, 0, 0], [1, H, this.#idx, D]);
256
- t = ops.concatAxis([tail, head], 2);
257
- tail.dispose();
258
- head.dispose();
259
- row.dispose();
260
- } else {
261
- t = row.slice([0, 0, 0, 0], [1, H, this.#idx, D]);
262
- row.dispose();
263
- }
264
- const cutV = t.slice([0, 0, pad, 0], [1, H, valid, D]);
265
- t.dispose();
266
- const own = ops.contiguous(cutV);
267
- cutV.dispose();
268
- return own;
269
- };
270
234
  const c = new RotatingKVCache(this.maxSize);
271
- const k = cut(this.keys);
272
- const v = cut(this.values);
235
+ const k = temporalStorageView(plainRowStorage, this.keys, this.#rows, {
236
+ row: i, from: pad, copy: true,
237
+ });
238
+ const v = temporalStorageView(plainRowStorage, this.values, this.#rows, {
239
+ row: i, from: pad, copy: true,
240
+ });
273
241
  c.restoreState(k, v, this.offsetArr[i]!, k.shape[2]!);
274
242
  return c;
275
243
  }
@@ -277,31 +245,27 @@ export class BatchedRotatingCache implements Cache {
277
245
  /** Keep only `keep` rows along the batch axis (eviction). */
278
246
  filter(keep: number[]): void {
279
247
  if (this.keys && this.values) {
280
- const idxArr = MlxArray.fromInt32(Int32Array.from(keep), [keep.length]);
281
- const k = ops.takeAxis(this.keys, idxArr, 0);
282
- const v = ops.takeAxis(this.values, idxArr, 0);
283
- idxArr.dispose();
248
+ const k = plainRowStorage.takeRows(this.keys, keep);
249
+ const v = plainRowStorage.takeRows(this.values, keep);
284
250
  this.keys.dispose();
285
251
  this.values.dispose();
286
252
  this.keys = k;
287
253
  this.values = v;
288
254
  }
289
- this.offsetArr = keep.map((i) => this.offsetArr[i]!);
290
- this.leftPad = keep.map((i) => this.leftPad[i]!);
255
+ this.#rows.filter(keep);
291
256
  this.releaseRopeArr();
292
257
  }
293
258
 
259
+ filterRows(keep: readonly number[]): void { this.filter([...keep]); }
260
+
294
261
  state(): MlxArray[] {
295
262
  return this.keys && this.values ? [this.keys, this.values] : [];
296
263
  }
297
264
  isTrimmable(): boolean {
298
- return this.#offset < this.maxSize;
265
+ return this.#rows.trimmable;
299
266
  }
300
267
  trim(n: number): void {
301
- const k = Math.min(this.#offset, n);
302
- this.#offset -= k;
303
- this.#idx -= k;
304
- this.offsetArr = this.offsetArr.map((x) => x - k);
268
+ this.#rows.trim(n);
305
269
  }
306
270
  dispose(): void {
307
271
  this.keys?.dispose();
@@ -322,24 +286,11 @@ export class BatchedRotatingCache implements Cache {
322
286
  const width = Math.max(...lens, 0);
323
287
  const leftPad = lens.map((l) => width - l);
324
288
  const cache = new BatchedRotatingCache(maxSize, leftPad);
325
- cache.offsetArr = [...offsets];
289
+ cache.#rows.restoreMerged(width, offsets);
326
290
  if (width === 0) return cache;
327
291
 
328
- const padTo = (a: MlxArray, pad: number): MlxArray => {
329
- if (pad === 0) return a.slice([0, 0, 0, 0], a.shape as number[]);
330
- const [B, H, , D] = a.shape as [number, number, number, number];
331
- const z = ops.zeros([B, H, pad, D], a.dtype);
332
- const out = ops.concatAxis([z, a], 2);
333
- z.dispose();
334
- return out;
335
- };
336
- const ks = rows.map((r, i) => padTo(r.keys, leftPad[i]!));
337
- const vs = rows.map((r, i) => padTo(r.values, leftPad[i]!));
338
- cache.keys = ops.concatAxis(ks, 0);
339
- cache.values = ops.concatAxis(vs, 0);
340
- for (const a of [...ks, ...vs]) a.dispose();
341
- cache.#idx = width;
342
- cache.#offset = width;
292
+ cache.keys = mergeStorageRows(plainRowStorage, rows.map((row) => row.keys), leftPad);
293
+ cache.values = mergeStorageRows(plainRowStorage, rows.map((row) => row.values), leftPad);
343
294
  return cache;
344
295
  }
345
296
  }
@@ -0,0 +1,127 @@
1
+ import { MlxArray } from "../mlx/array";
2
+ import * as ops from "../mlx/ops";
3
+ import { BatchedRotatingState } from "./batched-rotating-state";
4
+
5
+ export interface RowStorage<T> {
6
+ shape(value: T): readonly [number, number, number, number];
7
+ slice(value: T, batchFrom: number, batchTo: number, tokenFrom: number, tokenTo: number): T;
8
+ concatTokens(values: readonly T[]): T;
9
+ concatRows(values: readonly T[]): T;
10
+ padLeft(value: T, tokens: number): T;
11
+ takeRows(value: T, keep: readonly number[]): T;
12
+ copy(value: T): T;
13
+ dispose(value: T): void;
14
+ }
15
+
16
+ export const plainRowStorage: RowStorage<MlxArray> = {
17
+ shape: (value) => value.shape as [number, number, number, number],
18
+ slice(value, batchFrom, batchTo, tokenFrom, tokenTo) {
19
+ const [, H, , D] = value.shape as [number, number, number, number];
20
+ return value.slice([batchFrom, 0, tokenFrom, 0], [batchTo, H, tokenTo, D]);
21
+ },
22
+ concatTokens: (values) => ops.concatAxis([...values], 2),
23
+ concatRows: (values) => ops.concatAxis([...values], 0),
24
+ padLeft(value, tokens) {
25
+ if (tokens === 0) return value.slice([0, 0, 0, 0], value.shape as number[]);
26
+ const [B, H, , D] = value.shape as [number, number, number, number];
27
+ const zeros = ops.zeros([B, H, tokens, D], value.dtype);
28
+ const out = ops.concatAxis([zeros, value], 2);
29
+ zeros.dispose();
30
+ return out;
31
+ },
32
+ takeRows(value, keep) {
33
+ const indices = MlxArray.fromInt32(Int32Array.from(keep), [keep.length]);
34
+ const out = ops.takeAxis(value, indices, 0);
35
+ indices.dispose();
36
+ return out;
37
+ },
38
+ copy: (value) => ops.copyOf(value),
39
+ dispose: (value) => value.dispose(),
40
+ };
41
+
42
+ export const quantizedRowStorage: RowStorage<ops.QuantizedTensor> = {
43
+ shape: (value) => value.packed.shape as [number, number, number, number],
44
+ slice(value, batchFrom, batchTo, tokenFrom, tokenTo) {
45
+ const cut = (array: MlxArray): MlxArray => {
46
+ const [, H, , D] = array.shape as [number, number, number, number];
47
+ return array.slice([batchFrom, 0, tokenFrom, 0], [batchTo, H, tokenTo, D]);
48
+ };
49
+ return { packed: cut(value.packed), scales: cut(value.scales), biases: cut(value.biases) };
50
+ },
51
+ concatTokens(values) {
52
+ return {
53
+ packed: ops.concatAxis(values.map((value) => value.packed), 2),
54
+ scales: ops.concatAxis(values.map((value) => value.scales), 2),
55
+ biases: ops.concatAxis(values.map((value) => value.biases), 2),
56
+ };
57
+ },
58
+ concatRows(values) {
59
+ return {
60
+ packed: ops.concatAxis(values.map((value) => value.packed), 0),
61
+ scales: ops.concatAxis(values.map((value) => value.scales), 0),
62
+ biases: ops.concatAxis(values.map((value) => value.biases), 0),
63
+ };
64
+ },
65
+ padLeft(value, tokens) {
66
+ const pad = (array: MlxArray): MlxArray => plainRowStorage.padLeft(array, tokens);
67
+ return { packed: pad(value.packed), scales: pad(value.scales), biases: pad(value.biases) };
68
+ },
69
+ takeRows(value, keep) {
70
+ const take = (array: MlxArray): MlxArray => plainRowStorage.takeRows(array, keep);
71
+ return { packed: take(value.packed), scales: take(value.scales), biases: take(value.biases) };
72
+ },
73
+ copy(value) {
74
+ return {
75
+ packed: ops.copyOf(value.packed),
76
+ scales: ops.copyOf(value.scales),
77
+ biases: ops.copyOf(value.biases),
78
+ };
79
+ },
80
+ dispose(value) {
81
+ value.packed.dispose();
82
+ value.scales.dispose();
83
+ value.biases.dispose();
84
+ },
85
+ };
86
+
87
+ export function mergeStorageRows<T>(
88
+ storage: RowStorage<T>,
89
+ rows: readonly T[],
90
+ leftPad: readonly number[],
91
+ ): T {
92
+ const padded = rows.map((row, index) => storage.padLeft(row, leftPad[index]!));
93
+ try {
94
+ return storage.concatRows(padded);
95
+ } finally {
96
+ for (const row of padded) storage.dispose(row);
97
+ }
98
+ }
99
+
100
+ /** De-roll storage into temporal order, then cut one row or the whole batch. */
101
+ export function temporalStorageView<T>(
102
+ storage: RowStorage<T>,
103
+ value: T,
104
+ state: BatchedRotatingState,
105
+ options: { row?: number; from?: number; to?: number; copy?: boolean } = {},
106
+ ): T {
107
+ const [batch, , tokens] = storage.shape(value);
108
+ const batchFrom = options.row ?? 0;
109
+ const batchTo = options.row === undefined ? batch : options.row + 1;
110
+ const parts = state.temporalRanges(tokens).map(([from, to]) =>
111
+ storage.slice(value, batchFrom, batchTo, from, to)
112
+ );
113
+ const ordered = parts.length === 1 ? parts[0]! : storage.concatTokens(parts);
114
+ if (parts.length > 1) for (const part of parts) storage.dispose(part);
115
+ const cut = storage.slice(
116
+ ordered,
117
+ 0,
118
+ batchTo - batchFrom,
119
+ options.from ?? 0,
120
+ options.to ?? state.validLength,
121
+ );
122
+ storage.dispose(ordered);
123
+ if (!options.copy) return cut;
124
+ const owned = storage.copy(cut);
125
+ storage.dispose(cut);
126
+ return owned;
127
+ }
@@ -33,6 +33,7 @@
33
33
  import { MlxArray } from "../mlx/array";
34
34
  import { CompiledFunction } from "../mlx/compile";
35
35
  import * as ops from "../mlx/ops";
36
+ import { runtimeValue } from "../runtime-config";
36
37
  import {
37
38
  Gemma4Model,
38
39
  KVCache,
@@ -207,7 +208,7 @@ function slotDesc(c: AnyCache, plan: DecodeStepPlan): SlotDesc {
207
208
  * inside quantizedSdpa's dispatch). Offsets/capacities don't — they're
208
209
  * array values or shapeless dims. */
209
210
  function closureKey(descs: SlotDesc[]): string {
210
- const flags = `nf=${process.env.MLX_BUN_NO_FUSED_SDPA === "1" ? 1 : 0}`;
211
+ const flags = `nf=${runtimeValue("MLX_BUN_NO_FUSED_SDPA") === "1" ? 1 : 0}`;
211
212
  return descs.map((d) => `${d.kind}:${d.groupSize}:${d.bits}`).join(",") + "|" + flags;
212
213
  }
213
214
 
@@ -555,7 +556,7 @@ export class CompiledDecode {
555
556
  anyCaches
556
557
  .map((c, i) => (phases[i] === "ring" ? `${slotDesc(c, plans[i]!).kind}:${slotDesc(c, plans[i]!).groupSize}:${slotDesc(c, plans[i]!).bits}` : "js"))
557
558
  .join(",") +
558
- `|nf=${process.env.MLX_BUN_NO_FUSED_SDPA === "1" ? 1 : 0}`;
559
+ `|nf=${runtimeValue("MLX_BUN_NO_FUSED_SDPA") === "1" ? 1 : 0}`;
559
560
  if (this.#broken.has(key)) throw new Error(`compiled decode: known-broken closure ${key}`);
560
561
 
561
562
  let closures = this.#segClosures.get(key);
@@ -1,8 +1,7 @@
1
- // Model construction with generated-specialization dispatch
2
- // (docs/design/optimization_plan.md Phase C): pick the generated class whose config
3
- // fingerprint matches, else the monolith. Generated classes subclass
4
- // Gemma4Model and only override forwardLayers (with their own
5
- // cache-signature guard), so the choice is always safe.
1
+ // Model construction consumes one declared profile. Exact artifact profiles
2
+ // outrank family profiles; dedicated/generated graphs outrank the universal
3
+ // fallback. Request-level methods are resolved elsewhere and are never changed
4
+ // by this module.
6
5
 
7
6
  import { loadModelConfig, type ModelConfig } from "../config";
8
7
  import { totalmem } from "node:os";
@@ -24,8 +23,12 @@ import {
24
23
  type Glm52MemoryPlan,
25
24
  } from "./glm52-memory";
26
25
  import { UniversalDenseModel } from "./universal/dense";
27
- import { genericArgsFor, GENERIC_MODEL_TYPES, remapModelType } from "./universal/archs";
28
- import { isDiffusionGemmaConfig, isGlm52Config, isMiniCPM5Config, isQwen35Config, isQwen3Config, isQwen3MoeConfig } from "./support";
26
+ import { genericArgsFor } from "./universal/archs";
27
+ import {
28
+ assertResolvedModelProfile,
29
+ resolveModelProfile,
30
+ type ResolvedModelProfile,
31
+ } from "./profile";
29
32
 
30
33
  export type RuntimeModel =
31
34
  | Gemma4Model | MiniCPM5Model | Qwen35Model | Qwen3Model | Qwen3MoeModel
@@ -102,50 +105,42 @@ export async function openModel(
102
105
  options: Glm52RuntimeOpenOptions = {},
103
106
  ): Promise<RuntimeModel> {
104
107
  const config = await loadModelConfig(modelDir);
105
- if (isGlm52Config(config))
108
+ const profile = resolveModelProfile(config);
109
+ if (profile.profile.execution.loader === "colibri")
106
110
  return (await openGlm52RuntimeModel(modelDir, options)).model;
107
- return createModel(await Weights.open(modelDir), config);
111
+ return createModel(await Weights.open(modelDir), config, profile);
108
112
  }
109
113
 
110
- /** Dispatch ladder (docs/design/generic-model-support.md §3.3):
111
- * dedicated → generated/monolith (gemma4*) → generic (Tier-0 universal)
112
- * → reject with a helpful error. Generic never shadows a dedicated port. */
113
- export function createModel(weights: Weights, config: ModelConfig): RuntimeModel {
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
- }
121
- // DiffusionGemma is non-autoregressive — generate() detects it and routes to
122
- // the denoising engine instead of the AR decode loop. It exposes the shared
123
- // RuntimeModel surface (config/weightsBytes/loraState/makeCache) with the
124
- // AR-only methods as throwing stubs (never called on this model).
125
- if (isDiffusionGemmaConfig(config)) return new DiffusionGemmaModel(weights, config);
126
- if (isMiniCPM5Config(config)) return new MiniCPM5Model(weights, config);
127
- if (isQwen35Config(config)) return new Qwen35Model(weights, config);
128
- if (isQwen3MoeConfig(config)) return new Qwen3MoeModel(weights, config);
129
- if (isQwen3Config(config)) return new Qwen3Model(weights, config);
130
- // 2. gemma4*: fingerprint-matched generated specialization, else monolith.
131
- // Always prefer the model-specific generated file when its config
132
- // fingerprint matches — the parity regime is selected by the OUTPUT-changing
133
- // flags (kv-quant), which the generated forward already gates, NOT by
134
- // detouring an optimized model through the generic monolith.
135
- if (config.modelType.startsWith("gemma4")) {
136
- const cls = GENERATED.get(configFingerprint(config)) ?? Gemma4Model;
137
- return new cls(weights, config);
114
+ /** Construct the graph named by a previously validated profile. */
115
+ export function createModel(
116
+ weights: Weights,
117
+ config: ModelConfig,
118
+ resolved: ResolvedModelProfile = resolveModelProfile(config),
119
+ ): RuntimeModel {
120
+ assertResolvedModelProfile(config, resolved);
121
+ const execution = resolved.profile.execution;
122
+ switch (execution.graph) {
123
+ case "glm5.2":
124
+ throw new Error(
125
+ "glm_moe_dsa uses the direct Colibri container; construct it with " +
126
+ "openModel(modelDir) or Glm52Model.open(modelDir)",
127
+ );
128
+ // DiffusionGemma is non-autoregressive. The profile pairs this graph with
129
+ // the denoising loop; generate() recognizes the constructed model surface.
130
+ case "diffusion-gemma": return new DiffusionGemmaModel(weights, config);
131
+ case "minicpm5": return new MiniCPM5Model(weights, config);
132
+ case "qwen3.5": return new Qwen35Model(weights, config);
133
+ case "qwen3-moe": return new Qwen3MoeModel(weights, config);
134
+ case "qwen3": return new Qwen3Model(weights, config);
135
+ case "gemma4": {
136
+ // Generated specializations remain exact config matches. Output-changing
137
+ // request methods stay independently resolved and cannot be fallbacks.
138
+ const cls = execution.specialization === "generated"
139
+ ? GENERATED.get(configFingerprint(config))!
140
+ : Gemma4Model;
141
+ return new cls(weights, config);
142
+ }
143
+ case "universal-dense":
144
+ return new UniversalDenseModel(weights, config, genericArgsFor(config)!);
138
145
  }
139
- // 3. Generic Tier-0 fallback: the universal-dense descriptor table
140
- // (replaces the old hard "llama = MiniCPM5 only" / unknown-type throws).
141
- const generic = genericArgsFor(config);
142
- if (generic) return new UniversalDenseModel(weights, config, generic);
143
- // 4. Reject, naming the arch (post-remap) and the declared surface.
144
- const arch = remapModelType(config.modelType);
145
- throw new Error(
146
- `unsupported model_type "${config.modelType}"` +
147
- (arch !== config.modelType ? ` (mlx-lm remaps it to "${arch}")` : "") +
148
- ` — targeted: gemma4*, diffusion_gemma, glm_moe_dsa, qwen3_5, qwen3, qwen3_moe, MiniCPM5;` +
149
- ` generic (Tier-0): ${[...GENERIC_MODEL_TYPES].sort().join(", ")}`,
150
- );
151
146
  }