mlx-bun 0.0.10 → 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 (216) hide show
  1. package/README.md +107 -32
  2. package/THIRD_PARTY_LICENSES.md +10 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +58 -35
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/colibri-glm52-port.md +1479 -0
  10. package/docs/design/compat-cli-surface-design.md +4 -3
  11. package/docs/design/decode-speed-program.md +172 -0
  12. package/docs/design/docs-reorg-plan.md +1 -1
  13. package/docs/design/dspark-serving-program.md +371 -0
  14. package/docs/design/dspark-speculative-decoding.md +167 -5
  15. package/docs/design/faithful-l1-consolidation.md +310 -0
  16. package/docs/design/generic-model-support.md +8 -5
  17. package/docs/design/grammar-spec-batching-integration.md +301 -0
  18. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  19. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  20. package/docs/design/omlx-adoption-map.md +14 -6
  21. package/docs/design/orpo-training.md +34 -30
  22. package/docs/design/paged-kv-cache.md +214 -0
  23. package/docs/design/parallel-slots.md +82 -49
  24. package/docs/design/parity-tier-dag.md +43 -33
  25. package/docs/design/pre-colibri-stabilization.md +329 -0
  26. package/docs/design/repo-cleanup-plan.md +189 -0
  27. package/docs/design/runtime-isolation.md +19 -3
  28. package/docs/design/segmented-backward-training.md +3 -2
  29. package/docs/design/ssd-kv-cold-tier.md +130 -0
  30. package/docs/design/structured-output.md +484 -0
  31. package/docs/design/the-dreaming-master-plan.md +2 -3
  32. package/docs/design/turboquant-kv.md +235 -0
  33. package/docs/design/unified-engine-frontier-plan.md +774 -0
  34. package/docs/design/web-chat-beat-matrix.md +315 -0
  35. package/docs/design/web-chat-redesign.md +1112 -0
  36. package/docs/investigations/colibri-oracle-pin.md +1284 -0
  37. package/docs/investigations/dspark-handoff.md +182 -16
  38. package/docs/investigations/indexshare-performance-spike.md +357 -0
  39. package/docs/investigations/lab-build-journal.md +2 -2
  40. package/docs/investigations/openwiki-evaluation.md +249 -0
  41. package/docs/planning/IDEAS.md +14 -0
  42. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  43. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  44. package/docs/planning/chat-ui-vision.md +50 -0
  45. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  46. package/docs/planning/release-notes-v0.0.11.md +239 -0
  47. package/docs/planning/release-notes-v0.0.12.md +80 -0
  48. package/docs/reference/cli.md +63 -18
  49. package/docs/reference/distribution.md +37 -6
  50. package/docs/reference/embedding.md +40 -16
  51. package/docs/reference/features-matrix.md +99 -0
  52. package/docs/reference/library-api.md +69 -20
  53. package/docs/reference/memory.md +26 -6
  54. package/docs/reference/models.md +60 -3
  55. package/docs/reference/orpo-quickstart.md +3 -4
  56. package/docs/reference/server-api.md +717 -24
  57. package/docs/reference/server-config.md +265 -129
  58. package/docs/reference/training.md +24 -16
  59. package/package.json +5 -3
  60. package/src/anthropic.ts +67 -44
  61. package/src/audio/conformer.ts +647 -0
  62. package/src/audio/decode.ts +133 -0
  63. package/src/audio/features.ts +202 -0
  64. package/src/audio/transcode.ts +67 -0
  65. package/src/chat-template.ts +155 -10
  66. package/src/cli.ts +483 -113
  67. package/src/config.ts +116 -24
  68. package/src/diffusion/diffusion-generate.ts +14 -8
  69. package/src/download.ts +143 -12
  70. package/src/eval/README.md +7 -6
  71. package/src/eval/ifeval.ts +63 -120
  72. package/src/eval/kl.ts +59 -3
  73. package/src/eval/quality-db.ts +2 -2
  74. package/src/eval/runner.ts +17 -8
  75. package/src/eval/tasks/ifeval.ts +167 -31
  76. package/src/expert-io.ts +391 -0
  77. package/src/expert-offload.ts +15 -2
  78. package/src/expert-residency.ts +1134 -0
  79. package/src/expert-usage.ts +415 -0
  80. package/src/flags.ts +13 -0
  81. package/src/generate.ts +438 -35
  82. package/src/grammar.ts +585 -0
  83. package/src/hf-push.ts +1 -1
  84. package/src/hub-rest.ts +242 -0
  85. package/src/index.ts +14 -2
  86. package/src/jobs/runner.ts +37 -18
  87. package/src/kv-store.ts +586 -93
  88. package/src/lora.ts +85 -14
  89. package/src/md-modules.d.ts +42 -0
  90. package/src/media-fetch.ts +426 -0
  91. package/src/memory/rest.ts +245 -0
  92. package/src/memory/vault.ts +74 -0
  93. package/src/mlx/array.ts +149 -19
  94. package/src/mlx/autograd.ts +53 -16
  95. package/src/mlx/checkpoint.ts +46 -13
  96. package/src/mlx/custom-vjp.ts +83 -35
  97. package/src/mlx/ffi.ts +27 -3
  98. package/src/mlx/metal-kernel.ts +7 -4
  99. package/src/mlx/ops.ts +153 -23
  100. package/src/mlx/turboquant-ops.ts +831 -0
  101. package/src/mlx/turboquant-tables.ts +237 -0
  102. package/src/model/batched-mask.ts +62 -1
  103. package/src/model/batched-quant.ts +206 -0
  104. package/src/model/batched-rotating-quant.ts +346 -0
  105. package/src/model/batched-rotating.ts +46 -1
  106. package/src/model/compiled-decode.ts +4 -15
  107. package/src/model/factory.ts +103 -6
  108. package/src/model/gemma4-base.ts +395 -39
  109. package/src/model/gemma4.ts +122 -33
  110. package/src/model/generated/gemma4-12b.ts +9 -13
  111. package/src/model/generated/gemma4-26b.ts +10 -5
  112. package/src/model/generated/gemma4-e4b.ts +14 -9
  113. package/src/model/glm52-atlas.ts +511 -0
  114. package/src/model/glm52-cache.ts +706 -0
  115. package/src/model/glm52-config.ts +262 -0
  116. package/src/model/glm52-container.ts +305 -0
  117. package/src/model/glm52-coupling.ts +388 -0
  118. package/src/model/glm52-dsa.ts +602 -0
  119. package/src/model/glm52-expert-layout.ts +253 -0
  120. package/src/model/glm52-layout.ts +152 -0
  121. package/src/model/glm52-memory.ts +473 -0
  122. package/src/model/glm52-mla.ts +786 -0
  123. package/src/model/glm52-moe.ts +223 -0
  124. package/src/model/glm52-pilot.ts +537 -0
  125. package/src/model/glm52-reference.ts +445 -0
  126. package/src/model/glm52-residency.ts +557 -0
  127. package/src/model/glm52-resident-weights.ts +135 -0
  128. package/src/model/glm52-streamed-experts.ts +453 -0
  129. package/src/model/glm52-streamed-metal.ts +610 -0
  130. package/src/model/glm52-weights.ts +503 -0
  131. package/src/model/glm52.ts +1153 -0
  132. package/src/model/minicpm5.ts +58 -102
  133. package/src/model/paged-kv.ts +253 -0
  134. package/src/model/qwen3-delta.ts +76 -15
  135. package/src/model/qwen3-moe.ts +404 -0
  136. package/src/model/qwen3.ts +9 -8
  137. package/src/model/qwen3_5.ts +88 -23
  138. package/src/model/support.ts +19 -1
  139. package/src/model/universal/dense.ts +40 -5
  140. package/src/model/universal/rope.ts +36 -21
  141. package/src/native/expert_io.c +523 -0
  142. package/src/native-pack.ts +6 -4
  143. package/src/pi-web.ts +1299 -82
  144. package/src/preflight.ts +27 -8
  145. package/src/prompt-cache.ts +293 -33
  146. package/src/quantize/quantizer.ts +16 -1
  147. package/src/registry.ts +57 -2
  148. package/src/responses.ts +62 -49
  149. package/src/serve/batch-scheduler.ts +1050 -78
  150. package/src/serve/generation-gateway.ts +252 -24
  151. package/src/serve/isolate.ts +417 -0
  152. package/src/serve/lane-registry.ts +44 -0
  153. package/src/serve/session-search.ts +326 -0
  154. package/src/server.ts +1701 -175
  155. package/src/spec/assistant-source.ts +150 -0
  156. package/src/spec/deepspec-source.ts +143 -0
  157. package/src/spec/dflash-source.ts +124 -0
  158. package/src/spec/dspark/ab-stats.ts +136 -0
  159. package/src/spec/dspark/calibration.ts +112 -0
  160. package/src/spec/dspark/deepspec-module.ts +801 -0
  161. package/src/spec/dspark/generate-dflash.ts +72 -20
  162. package/src/spec/dspark/loader.ts +23 -0
  163. package/src/spec/dspark/module-dflash.ts +278 -36
  164. package/src/spec/dspark/quantize-drafter.ts +91 -0
  165. package/src/spec/generate.ts +59 -25
  166. package/src/spec/glm52-mtp-source.ts +392 -0
  167. package/src/spec/ngram-source.ts +132 -0
  168. package/src/spec/serve-loop.ts +617 -0
  169. package/src/spec/source.ts +118 -0
  170. package/src/spec/two-model.ts +171 -0
  171. package/src/ssd-cache.ts +111 -35
  172. package/src/tokenizer.ts +288 -2
  173. package/src/tool-approvals.ts +131 -0
  174. package/src/tool-call.ts +438 -22
  175. package/src/train/flash-cce.ts +2 -2
  176. package/src/train/job.ts +0 -12
  177. package/src/train/lora-params.ts +17 -6
  178. package/src/train/trainer.ts +60 -30
  179. package/src/vision/prompt.ts +217 -48
  180. package/src/web/app.html +1197 -1791
  181. package/src/web/app.js +5694 -0
  182. package/src/web/icon.svg +13 -0
  183. package/src/web/manifest.webmanifest +24 -0
  184. package/src/web/src/adapters-panel.ts +288 -0
  185. package/src/web/src/api.ts +57 -0
  186. package/src/web/src/assistant.ts +485 -0
  187. package/src/web/src/chat.ts +1050 -0
  188. package/src/web/src/composer.ts +959 -0
  189. package/src/web/src/dataset.ts +132 -0
  190. package/src/web/src/finetune.ts +267 -0
  191. package/src/web/src/hub.ts +428 -0
  192. package/src/web/src/main.ts +98 -0
  193. package/src/web/src/markdown.ts +0 -0
  194. package/src/web/src/memory-panel.ts +572 -0
  195. package/src/web/src/model-picker.ts +159 -0
  196. package/src/web/src/palette.ts +346 -0
  197. package/src/web/src/protocol.ts +62 -0
  198. package/src/web/src/quantize.ts +167 -0
  199. package/src/web/src/rag.ts +274 -0
  200. package/src/web/src/sessions.ts +469 -0
  201. package/src/web/src/shell.ts +804 -0
  202. package/src/web/src/status.ts +300 -0
  203. package/src/web/src/ui-catalog.ts +92 -0
  204. package/src/web/sw.js +71 -0
  205. package/src/web/vendor/README.md +61 -0
  206. package/src/web/vendor/hljs-LICENSE +29 -0
  207. package/src/web/vendor/hljs-theme.css +59 -0
  208. package/src/web/vendor/hljs.js +26 -0
  209. package/src/web-tools.ts +54 -2
  210. package/src/weights.ts +82 -19
  211. package/src/model/fused-decode-kernel.ts +0 -312
  212. package/src/model/fused-geglu-kernel.ts +0 -198
  213. package/src/model/fused-mlp-kernel.ts +0 -169
  214. package/src/model/fused-swiglu-kernel.ts +0 -240
  215. package/src/model/steel-linear-kernel.ts +0 -96
  216. package/src/status-page.html +0 -497
@@ -0,0 +1,346 @@
1
+ // Batched sliding-window QUANTIZED KV — Phase 3 milestone 2 of
2
+ // docs/design/unified-engine-frontier-plan.md: the composition that lets a
3
+ // per-layer kv_config naming ROTATING layers (gemma) batch. No stack ships
4
+ // this (mlx-lm's batched path is bf16-only; optiq's mixed-KV hook is
5
+ // serial-only), so per the composition rule the oracle is the SERIAL
6
+ // RotatingQuantizedKVCache per row: each joiner's solo prefill converts with
7
+ // the serial ops (BatchScheduler.#quantizeSolo), and this class only
8
+ // re-arranges those bytes across the batch axis + runs the mlx-lm
9
+ // BatchRotatingKVCache ring mechanics (batched-rotating.ts) over
10
+ // (packed, scales, biases) triples instead of bf16 tensors.
11
+ //
12
+ // Same safety argument as batched-quant.ts: quantization packs along
13
+ // HEAD_DIM, so the token axis stays token-granular in all three tensors —
14
+ // left-pad columns are zeros, masked by buildBatchedRotatingMask (token-axis
15
+ // only; shared verbatim with the bf16 twin), never dequantized into anything
16
+ // a real row reads.
17
+ //
18
+ // Subclasses RotatingQuantizedKVCache so Attention.forward's `instanceof`
19
+ // dispatch (gemma4.ts) routes to the quantized path with the L1 model file
20
+ // untouched. PERSISTENT (like BatchedRotatingCache, unlike the ephemeral
21
+ // per-step BatchedQuantDecodeMaskCache): the scheduler passes it through
22
+ // every step; it self-tracks per-row offset/leftPad and the scalar ring
23
+ // state. N=1 decode updates only (rows solo-prefill serially, then merge).
24
+ //
25
+ // Gates: tests/batched-rotating-quant.test.ts (model-free byte-identity vs
26
+ // the serial oracle per row, through ring wrap, B=1 and B=2) + the gemma
27
+ // scheduler gate in tests/batched-kv-quant-parity.test.ts.
28
+
29
+ import { MlxArray } from "../mlx/array";
30
+ import * as ops from "../mlx/ops";
31
+ import { Dtype } from "../mlx/ffi";
32
+ import { RotatingQuantizedKVCache, type Mask } from "./gemma4-base";
33
+ import { buildBatchedRotatingMask } from "./batched-rotating";
34
+ import type { QuantRow } from "./batched-quant";
35
+
36
+ const STEP = 256; // shared with RotatingQuantizedKVCache.STEP and the bf16 twin
37
+
38
+ const disposeTriple = (t: ops.QuantizedTensor): void => {
39
+ t.packed.dispose();
40
+ t.scales.dispose();
41
+ t.biases.dispose();
42
+ };
43
+ const mapTriple = (
44
+ t: ops.QuantizedTensor, f: (a: MlxArray) => MlxArray,
45
+ ): ops.QuantizedTensor => ({ packed: f(t.packed), scales: f(t.scales), biases: f(t.biases) });
46
+
47
+ /** Batched ring over quantized triples — mlx-lm BatchRotatingKVCache
48
+ * mechanics (see batched-rotating.ts) with RotatingQuantizedKVCache
49
+ * storage. Scalar ring state (`ringIdx`/`offset` reuse the base fields);
50
+ * per-row `offsetArr` (absolute positions → RoPE) and `leftPad`. */
51
+ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
52
+ offsetArr: number[];
53
+ leftPad: number[];
54
+ #rotated = false;
55
+ /** Per-row RoPE positions. STABLE ACROSS A STEP — refreshed only at
56
+ * releaseRopeArr() (the scheduler's post-dispatch hook), never inside
57
+ * the update: the monolith CAPTURES it pre-update and ropes Q after
58
+ * (an in-update dispose is a use-after-dispose there), and the
59
+ * GENERATED specializations RE-READ it for Q post-update (an in-update
60
+ * refresh hands K and Q different positions there — found the hard way
61
+ * 2026-07-05: the twin passes the generated files' instanceof cache
62
+ * guards by subclassing, so an all-quant gemma batch decodes through
63
+ * the generated graph). Widens the base's readonly to mutable. */
64
+ override ropeOffsetArr: MlxArray;
65
+
66
+ private constructor(
67
+ maxSize: number, groupSize: number, bits: number, leftPad: number[], offsets: number[],
68
+ ) {
69
+ super(maxSize, groupSize, bits);
70
+ this.leftPad = [...leftPad];
71
+ this.offsetArr = [...offsets];
72
+ this.ropeOffsetArr = MlxArray.fromInt32(Int32Array.from(offsets), [offsets.length]);
73
+ }
74
+
75
+ get #B(): number {
76
+ return this.leftPad.length;
77
+ }
78
+
79
+ #seqLen2(): number {
80
+ return this.keys ? this.keys.packed.shape[2]! : 0;
81
+ }
82
+
83
+ /** Scheduler hook, called on every fwd cache AFTER the step's graph is
84
+ * built — the ONE safe point to swap the rope array for the advanced
85
+ * positions (the step's graph holds its own reference; no JS reads the
86
+ * old handle after this). */
87
+ releaseRopeArr(): void {
88
+ this.ropeOffsetArr.dispose();
89
+ this.ropeOffsetArr = MlxArray.fromInt32(Int32Array.from(this.offsetArr), [this.#B]);
90
+ }
91
+
92
+ #allocTriple(B: number, H: number, T: number, dim: number, dtype: Dtype): ops.QuantizedTensor {
93
+ const elPerInt = 32 / this.bits;
94
+ return {
95
+ packed: ops.zeros([B, H, T, dim / elPerInt], Dtype.uint32),
96
+ scales: ops.zeros([B, H, T, dim / this.groupSize], dtype),
97
+ biases: ops.zeros([B, H, T, dim / this.groupSize], dtype),
98
+ };
99
+ }
100
+
101
+ override makeMask(N: number, windowSize: number | null): Mask {
102
+ const window = windowSize ?? this.maxSize;
103
+ return {
104
+ mode: "array",
105
+ arr: buildBatchedRotatingMask(
106
+ this.#B, N, this.leftPad, this.maxSize, window,
107
+ this.ringIdx, this.offset, this.#rotated,
108
+ ),
109
+ };
110
+ }
111
+
112
+ /** N=1 decode update — the bf16 twin's updateAndFetch (mlx-lm
113
+ * _update_in_place) over triples, with quantize-on-write from the
114
+ * serial oracle. */
115
+ override updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
116
+ const [B, H, S, D] = k.shape as [number, number, number, number];
117
+ const vD = v.shape[3]!;
118
+ if (S !== 1)
119
+ throw new Error("BatchedRotatingQuantCache supports N=1 decode updates only (solo-prefill then merge)");
120
+ const prev = this.offset;
121
+
122
+ // Grow the buffer (in STEP chunks) until it reaches maxSize.
123
+ if (!this.keys || (prev >= this.#seqLen2() && this.#seqLen2() < this.maxSize)) {
124
+ const newSize = Math.min(STEP, this.maxSize - prev);
125
+ const newK = this.#allocTriple(B, H, newSize, D, k.dtype);
126
+ const newV = this.#allocTriple(B, H, newSize, vD, v.dtype);
127
+ if (this.keys && this.values) {
128
+ const grow = (old: ops.QuantizedTensor, add: ops.QuantizedTensor): ops.QuantizedTensor => {
129
+ const cat = (a: MlxArray, b: MlxArray): MlxArray => {
130
+ const out = ops.concatAxis([a, b], 2);
131
+ a.dispose();
132
+ b.dispose();
133
+ return out;
134
+ };
135
+ return {
136
+ packed: cat(old.packed, add.packed),
137
+ scales: cat(old.scales, add.scales),
138
+ biases: cat(old.biases, add.biases),
139
+ };
140
+ };
141
+ this.keys = grow(this.keys, newK);
142
+ this.values = grow(this.values, newV);
143
+ } else {
144
+ this.keys = newK;
145
+ this.values = newV;
146
+ }
147
+ this.ringIdx = prev;
148
+ }
149
+
150
+ // Trim any overshoot past maxSize (decrements left padding persistently).
151
+ const trimSize = this.#seqLen2() - this.maxSize;
152
+ if (trimSize > 0) {
153
+ const cutFrom = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
154
+ mapTriple(t, (a) => {
155
+ const [b2, h2, s2, d2] = a.shape as [number, number, number, number];
156
+ const s = a.slice([0, 0, trimSize, 0], [b2, h2, s2, d2]);
157
+ a.dispose();
158
+ return s;
159
+ });
160
+ this.keys = cutFrom(this.keys!);
161
+ this.values = cutFrom(this.values!);
162
+ this.ringIdx = this.maxSize;
163
+ this.leftPad = this.leftPad.map((x) => x - trimSize);
164
+ }
165
+
166
+ // Rotate when the write head reaches the end of the ring.
167
+ if (this.ringIdx === this.maxSize) {
168
+ this.#rotated = true;
169
+ this.ringIdx = 0;
170
+ }
171
+ if (this.#rotated) this.leftPad = this.leftPad.map((x) => x - S);
172
+
173
+ // Quantize + write the new K/V at the ring head (six sliceUpdates).
174
+ const kq = ops.quantize(k, this.groupSize, this.bits);
175
+ const vq = ops.quantize(v, this.groupSize, this.bits);
176
+ const writeAt = (dst: ops.QuantizedTensor, src: ops.QuantizedTensor): ops.QuantizedTensor =>
177
+ ({
178
+ packed: this.#assign2(dst.packed, src.packed, S),
179
+ scales: this.#assign2(dst.scales, src.scales, S),
180
+ biases: this.#assign2(dst.biases, src.biases, S),
181
+ });
182
+ this.keys = writeAt(this.keys!, kq);
183
+ this.values = writeAt(this.values!, vq);
184
+ disposeTriple(kq);
185
+ disposeTriple(vq);
186
+
187
+ this.offset += S;
188
+ this.offsetArr = this.offsetArr.map((x) => x + S);
189
+ this.ringIdx += S;
190
+
191
+ // Return the populated prefix (ring not yet full) or the whole buffer.
192
+ const upTo = this.offset < this.maxSize ? this.offset : this.#seqLen2();
193
+ const cut = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
194
+ mapTriple(t, (a) => {
195
+ const [b2, h2, , d2] = a.shape as [number, number, number, number];
196
+ return a.slice([0, 0, 0, 0], [b2, h2, upTo, d2]);
197
+ });
198
+ return [cut(this.keys!), cut(this.values!)];
199
+ }
200
+
201
+ #assign2(dst: MlxArray, src: MlxArray, S: number): MlxArray {
202
+ const [B, H, , D] = dst.shape as [number, number, number, number];
203
+ const out = ops.sliceUpdate(dst, src, [0, 0, this.ringIdx, 0], [B, H, this.ringIdx + S, D]);
204
+ dst.dispose();
205
+ return out;
206
+ }
207
+
208
+ /** Ring contents in temporal order, cut to the valid length (extract) —
209
+ * overrides the base to use the BATCHED ring state. */
210
+ override temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
211
+ if (!this.keys || !this.values) throw new Error("cache is empty");
212
+ const valid = Math.min(this.offset, this.maxSize);
213
+ const order = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
214
+ mapTriple(t, (a) => {
215
+ const [B, H, Sbuf, D] = a.shape as [number, number, number, number];
216
+ let tt: MlxArray;
217
+ if (this.ringIdx === Sbuf) {
218
+ tt = a.slice([0, 0, 0, 0], [B, H, Sbuf, D]);
219
+ } else if (this.ringIdx < this.offset) {
220
+ const tail = a.slice([0, 0, this.ringIdx, 0], [B, H, Sbuf, D]);
221
+ const head = a.slice([0, 0, 0, 0], [B, H, this.ringIdx, D]);
222
+ tt = ops.concatAxis([tail, head], 2);
223
+ tail.dispose();
224
+ head.dispose();
225
+ } else {
226
+ tt = a.slice([0, 0, 0, 0], [B, H, this.ringIdx, D]);
227
+ }
228
+ const cut = tt.slice([0, 0, 0, 0], [B, H, valid, D]);
229
+ tt.dispose();
230
+ return cut;
231
+ });
232
+ return [order(this.keys), order(this.values)];
233
+ }
234
+
235
+ /** Quantized twin of BatchedRotatingCache.extractRow (mlx-lm
236
+ * BatchRotatingKVCache.extract, models/cache.py:1417, over triples):
237
+ * row `i` as a fresh SERIAL RotatingQuantizedKVCache — de-rolled to
238
+ * temporal order, left padding stripped, OWNED contiguous copies;
239
+ * offset = the row's absolute position, ringIdx = the new buffer length
240
+ * (the oracle's `cache._idx = cache.keys.shape[2]`). Token-axis slicing
241
+ * is byte-safe (packing along HEAD_DIM — file header); per-row byte
242
+ * identity vs the serial oracle is the class invariant
243
+ * (tests/batched-rotating-quant), extraction is a pure slice+copy. */
244
+ extractRow(i: number): RotatingQuantizedKVCache | null {
245
+ if (!this.keys || !this.values) return null;
246
+ const pad = Math.max(0, this.leftPad[i]!);
247
+ const valid = Math.min(this.offset, this.maxSize);
248
+ const cut = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
249
+ mapTriple(t, (a) => {
250
+ const [, H, Sbuf, D] = a.shape as [number, number, number, number];
251
+ const row = a.slice([i, 0, 0, 0], [i + 1, H, Sbuf, D]);
252
+ let tt: MlxArray;
253
+ if (this.ringIdx === Sbuf) {
254
+ tt = row;
255
+ } else if (this.ringIdx < this.offset) {
256
+ const tail = row.slice([0, 0, this.ringIdx, 0], [1, H, Sbuf, D]);
257
+ const head = row.slice([0, 0, 0, 0], [1, H, this.ringIdx, D]);
258
+ tt = ops.concatAxis([tail, head], 2);
259
+ tail.dispose();
260
+ head.dispose();
261
+ row.dispose();
262
+ } else {
263
+ tt = row.slice([0, 0, 0, 0], [1, H, this.ringIdx, D]);
264
+ row.dispose();
265
+ }
266
+ const cutV = tt.slice([0, 0, pad, 0], [1, H, valid, D]);
267
+ tt.dispose();
268
+ const own = ops.contiguous(cutV);
269
+ cutV.dispose();
270
+ return own;
271
+ });
272
+ const c = new RotatingQuantizedKVCache(this.maxSize, this.groupSize, this.bits);
273
+ const k = cut(this.keys);
274
+ const v = cut(this.values);
275
+ c.restoreState(k, v, this.offsetArr[i]!, k.packed.shape[2]!);
276
+ return c;
277
+ }
278
+
279
+ /** Keep only `keep` rows along the batch axis (eviction), in place. */
280
+ filter(keep: number[]): void {
281
+ if (this.keys && this.values) {
282
+ const idxArr = MlxArray.fromInt32(Int32Array.from(keep), [keep.length]);
283
+ const take = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
284
+ mapTriple(t, (a) => ops.takeAxis(a, idxArr, 0));
285
+ const k = take(this.keys);
286
+ const v = take(this.values);
287
+ idxArr.dispose();
288
+ disposeTriple(this.keys);
289
+ disposeTriple(this.values);
290
+ this.keys = k;
291
+ this.values = v;
292
+ }
293
+ this.offsetArr = keep.map((i) => this.offsetArr[i]!);
294
+ this.leftPad = keep.map((i) => this.leftPad[i]!);
295
+ this.releaseRopeArr(); // safe here: filter runs between steps
296
+ }
297
+
298
+ override isTrimmable(): boolean {
299
+ return false; // batched rows never re-enter the prompt cache directly
300
+ }
301
+
302
+ override dispose(): void {
303
+ super.dispose();
304
+ this.releaseRopeArr();
305
+ this.ropeOffsetArr?.dispose();
306
+ }
307
+
308
+ /** Assemble a batch from per-row temporal quantized slices (the bf16
309
+ * twin's merge over triples). Each row is a solo/adopted/batched cache's
310
+ * temporalView ([1,H,Li,*], Li ≤ maxSize, temporal order) and
311
+ * `offsets[i]` its absolute position. Result is in temporal order
312
+ * (rotated=false, ringIdx=offset=width). */
313
+ static merge(
314
+ rows: QuantRow[], offsets: number[], maxSize: number, groupSize: number, bits: number,
315
+ ): BatchedRotatingQuantCache {
316
+ const lens = rows.map((r) => r.keys.packed.shape[2]!);
317
+ const width = Math.max(...lens, 0);
318
+ const leftPad = lens.map((l) => width - l);
319
+ const cache = new BatchedRotatingQuantCache(maxSize, groupSize, bits, leftPad, offsets);
320
+ if (width === 0) return cache;
321
+
322
+ const padTo = (a: MlxArray, pad: number): MlxArray => {
323
+ if (pad === 0) return a.slice([0, 0, 0, 0], a.shape as number[]);
324
+ const [B, H, , D] = a.shape as [number, number, number, number];
325
+ const z = ops.zeros([B, H, pad, D], a.dtype);
326
+ const out = ops.concatAxis([z, a], 2);
327
+ z.dispose();
328
+ return out;
329
+ };
330
+ const padded = rows.map((r, i) => ({
331
+ keys: mapTriple(r.keys, (a) => padTo(a, leftPad[i]!)),
332
+ values: mapTriple(r.values, (a) => padTo(a, leftPad[i]!)),
333
+ }));
334
+ const cat = (pick: (r: QuantRow) => ops.QuantizedTensor): ops.QuantizedTensor => ({
335
+ packed: ops.concatAxis(padded.map((r) => pick(r).packed), 0),
336
+ scales: ops.concatAxis(padded.map((r) => pick(r).scales), 0),
337
+ biases: ops.concatAxis(padded.map((r) => pick(r).biases), 0),
338
+ });
339
+ cache.keys = cat((r) => r.keys);
340
+ cache.values = cat((r) => r.values);
341
+ for (const r of padded) { disposeTriple(r.keys); disposeTriple(r.values); }
342
+ cache.ringIdx = width;
343
+ cache.offset = width;
344
+ return cache;
345
+ }
346
+ }
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { MlxArray } from "../mlx/array";
26
26
  import * as ops from "../mlx/ops";
27
- import type { Cache, Mask } from "./gemma4-base";
27
+ import { RotatingKVCache, type Cache, type Mask } from "./gemma4-base";
28
28
 
29
29
  const STEP = 256;
30
30
 
@@ -229,6 +229,51 @@ export class BatchedRotatingCache implements Cache {
229
229
  return [order(this.keys), order(this.values)];
230
230
  }
231
231
 
232
+ /** mlx-lm `BatchRotatingKVCache.extract` (models/cache.py:1417): row `i`
233
+ * as a fresh SERIAL RotatingKVCache — de-rolled to temporal order (the
234
+ * oracle's roll(-_idx) when rotated), left padding stripped
235
+ * (`max(0, left_padding[idx])` — post-wrap pads go negative), OWNED
236
+ * contiguous copies. `offset` = the row's absolute position (may exceed
237
+ * the buffer: wrapped rings have evicted tokens); ring idx = the new
238
+ * buffer length — temporal order ⇔ write head at the end, exactly the
239
+ * oracle's `cache._idx = cache.keys.shape[2]`. Bit-exact vs a solo run:
240
+ * merge/decode/filter keep each row's ring bytes identical to the serial
241
+ * cache's (tests/batched-rotating) and this is a pure slice+copy. */
242
+ extractRow(i: number): RotatingKVCache | null {
243
+ if (!this.keys || !this.values) return null;
244
+ 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
+ const c = new RotatingKVCache(this.maxSize);
271
+ const k = cut(this.keys);
272
+ const v = cut(this.values);
273
+ c.restoreState(k, v, this.offsetArr[i]!, k.shape[2]!);
274
+ return c;
275
+ }
276
+
232
277
  /** Keep only `keep` rows along the batch axis (eviction). */
233
278
  filter(keep: number[]): void {
234
279
  if (this.keys && this.values) {
@@ -33,7 +33,6 @@
33
33
  import { MlxArray } from "../mlx/array";
34
34
  import { CompiledFunction } from "../mlx/compile";
35
35
  import * as ops from "../mlx/ops";
36
- import { perfKernelEnabled } from "./fused-decode-kernel";
37
36
  import {
38
37
  Gemma4Model,
39
38
  KVCache,
@@ -208,10 +207,7 @@ function slotDesc(c: AnyCache, plan: DecodeStepPlan): SlotDesc {
208
207
  * inside quantizedSdpa's dispatch). Offsets/capacities don't — they're
209
208
  * array values or shapeless dims. */
210
209
  function closureKey(descs: SlotDesc[]): string {
211
- const flags =
212
- `fd=${process.env.MLX_BUN_FUSED_DECODE === "1" ? 1 : 0}` +
213
- `,nf=${process.env.MLX_BUN_NO_FUSED_SDPA === "1" ? 1 : 0}` +
214
- `,pk=${perfKernelEnabled() ? 1 : 0}`;
210
+ const flags = `nf=${process.env.MLX_BUN_NO_FUSED_SDPA === "1" ? 1 : 0}`;
215
211
  return descs.map((d) => `${d.kind}:${d.groupSize}:${d.bits}`).join(",") + "|" + flags;
216
212
  }
217
213
 
@@ -272,12 +268,8 @@ function makeTraceFn(model: Gemma4Model, descs: SlotDesc[]) {
272
268
  /** Non-mutating phase check (segmented mode decides layer placement
273
269
  * BEFORE any cache bookkeeping runs). Mirrors prepareDecodeStep's
274
270
  * outcome: rotating caches go ring once the next write lands at or past
275
- * the window; everything else grows. Under the perf kernel, quantized
276
- * layers run as JS layers regardless — the CustomKernel primitive has
277
- * no output_shapes and cannot live inside a compiled closure. */
271
+ * the window; everything else grows. */
278
272
  function decodePhase(c: AnyCache): "concat" | "ring" {
279
- if (perfKernelEnabled() && (c instanceof QuantizedKVCache || c instanceof RotatingQuantizedKVCache))
280
- return "concat";
281
273
  if (c instanceof RotatingKVCache || c instanceof RotatingQuantizedKVCache)
282
274
  return c.offset + 1 < c.maxSize ? "concat" : "ring";
283
275
  return "concat";
@@ -563,9 +555,7 @@ export class CompiledDecode {
563
555
  anyCaches
564
556
  .map((c, i) => (phases[i] === "ring" ? `${slotDesc(c, plans[i]!).kind}:${slotDesc(c, plans[i]!).groupSize}:${slotDesc(c, plans[i]!).bits}` : "js"))
565
557
  .join(",") +
566
- `|fd=${process.env.MLX_BUN_FUSED_DECODE === "1" ? 1 : 0}` +
567
- `,nf=${process.env.MLX_BUN_NO_FUSED_SDPA === "1" ? 1 : 0}` +
568
- `,pk=${perfKernelEnabled() ? 1 : 0}`;
558
+ `|nf=${process.env.MLX_BUN_NO_FUSED_SDPA === "1" ? 1 : 0}`;
569
559
  if (this.#broken.has(key)) throw new Error(`compiled decode: known-broken closure ${key}`);
570
560
 
571
561
  let closures = this.#segClosures.get(key);
@@ -608,8 +598,7 @@ export class CompiledDecode {
608
598
  // reverting offset (and ring index) — trim(1) semantics. The written
609
599
  // KV row beyond the reverted offset is dead data the re-forward
610
600
  // overwrites at the same position (updateAndFetch writes at offset/
611
- // ringIdx), so post-recovery state equals a single clean write. This
612
- // holds for the post-wrap quantized-under-perf-kernel case too: the
601
+ // ringIdx), so post-recovery state equals a single clean write; the
613
602
  // in-place S=1 write slot is re-derived from the reverted ringIdx.
614
603
  const stagedAdopts: { c: AnyCache; bufs: MlxArray[] }[] = [];
615
604
  const committedJs: { c: AnyCache; offsetBefore: number }[] = [];
@@ -4,28 +4,120 @@
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";
12
13
  import { MiniCPM5Model } from "./minicpm5";
13
14
  import { Qwen35Model } from "./qwen3_5";
14
15
  import { Qwen3Model } from "./qwen3";
16
+ import { Qwen3MoeModel } from "./qwen3-moe";
15
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";
16
26
  import { UniversalDenseModel } from "./universal/dense";
17
27
  import { genericArgsFor, GENERIC_MODEL_TYPES, remapModelType } from "./universal/archs";
18
- import { isDiffusionGemmaConfig, isMiniCPM5Config, isQwen35Config, isQwen3Config } from "./support";
28
+ import { isDiffusionGemmaConfig, isGlm52Config, isMiniCPM5Config, isQwen35Config, isQwen3Config, isQwen3MoeConfig } from "./support";
19
29
 
20
30
  export type RuntimeModel =
21
- | Gemma4Model | MiniCPM5Model | Qwen35Model | Qwen3Model | DiffusionGemmaModel
22
- | UniversalDenseModel;
31
+ | Gemma4Model | MiniCPM5Model | Qwen35Model | Qwen3Model | Qwen3MoeModel
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
+ }
23
109
 
24
110
  /** Dispatch ladder (docs/design/generic-model-support.md §3.3):
25
111
  * dedicated → generated/monolith (gemma4*) → generic (Tier-0 universal)
26
112
  * → reject with a helpful error. Generic never shadows a dedicated port. */
27
113
  export function createModel(weights: Weights, config: ModelConfig): RuntimeModel {
28
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
+ }
29
121
  // DiffusionGemma is non-autoregressive — generate() detects it and routes to
30
122
  // the denoising engine instead of the AR decode loop. It exposes the shared
31
123
  // RuntimeModel surface (config/weightsBytes/loraState/makeCache) with the
@@ -33,8 +125,13 @@ export function createModel(weights: Weights, config: ModelConfig): RuntimeModel
33
125
  if (isDiffusionGemmaConfig(config)) return new DiffusionGemmaModel(weights, config);
34
126
  if (isMiniCPM5Config(config)) return new MiniCPM5Model(weights, config);
35
127
  if (isQwen35Config(config)) return new Qwen35Model(weights, config);
128
+ if (isQwen3MoeConfig(config)) return new Qwen3MoeModel(weights, config);
36
129
  if (isQwen3Config(config)) return new Qwen3Model(weights, config);
37
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.
38
135
  if (config.modelType.startsWith("gemma4")) {
39
136
  const cls = GENERATED.get(configFingerprint(config)) ?? Gemma4Model;
40
137
  return new cls(weights, config);
@@ -48,7 +145,7 @@ export function createModel(weights: Weights, config: ModelConfig): RuntimeModel
48
145
  throw new Error(
49
146
  `unsupported model_type "${config.modelType}"` +
50
147
  (arch !== config.modelType ? ` (mlx-lm remaps it to "${arch}")` : "") +
51
- ` — targeted: gemma4*, diffusion_gemma, qwen3_5, qwen3, MiniCPM5;` +
148
+ ` — targeted: gemma4*, diffusion_gemma, glm_moe_dsa, qwen3_5, qwen3, qwen3_moe, MiniCPM5;` +
52
149
  ` generic (Tier-0): ${[...GENERIC_MODEL_TYPES].sort().join(", ")}`,
53
150
  );
54
151
  }