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
@@ -194,10 +194,37 @@ export function gatedDeltaUpdate(
194
194
  * and writes these slots directly; `advance(N)` only tracks token count for
195
195
  * B=1 single-stream (lengths / left_padding are batched-decode concerns).
196
196
  * Not a KVCache, so maybeQuantizeKv skips it. */
197
+ /** One armed speculative verify round on an SSMCache: the pre-round state
198
+ * snapshot (retained refs — MLX arrays are immutable, so this is free) plus
199
+ * the layer's recorded position-local kernel inputs, which make a partial
200
+ * reject bit-exactly replayable: state after the first `keep` window tokens
201
+ * is the same arithmetic prefix whether or not the rejected tail was ever
202
+ * processed. The replay is installed by the OWNING layer (it needs the conv
203
+ * weight / A_log / dt_bias); the cache only stores and frees. */
204
+ export interface SsmSpecRound {
205
+ /** True from specRoundBegin() until the layer's forward records. */
206
+ armed: boolean;
207
+ prevConv: MlxArray | null;
208
+ prevRecurrent: MlxArray | null;
209
+ prevOffset: number;
210
+ /** Pre-conv in_proj output [B,S,convDim] — position-local. */
211
+ qkv: MlxArray | null;
212
+ /** Gate/beta projections [B,S,Hv] — position-local. */
213
+ a: MlxArray | null;
214
+ b: MlxArray | null;
215
+ /** Window length S of the recorded verify forward. */
216
+ S: number;
217
+ /** Layer-bound prefix replay: called AFTER the snapshot is restored onto
218
+ * the cache; advances conv/recurrent/offset by `keep` tokens. */
219
+ replay: ((cache: SSMCache, keep: number) => void) | null;
220
+ }
221
+
197
222
  export class SSMCache implements Cache {
198
223
  conv: MlxArray | null = null;
199
224
  recurrent: MlxArray | null = null;
200
225
  offset = 0;
226
+ /** Live speculative verify round (serial spec lane only; null otherwise). */
227
+ specRound: SsmSpecRound | null = null;
201
228
  /** Per-row token coverage (batch lane only; null on serial B=1 caches,
202
229
  * where `offset` IS the row's count). mlx-lm's ArraysCache tracks no
203
230
  * offset at all (cache.py:594-722) — per-sequence coverage is the
@@ -207,6 +234,10 @@ export class SSMCache implements Cache {
207
234
  * lockstep by advance(), row-filtered by filter(). */
208
235
  offsets: number[] | null = null;
209
236
 
237
+ signature(): string { return "ssm"; }
238
+ bytesPerToken(): number { return 0; }
239
+ get batchSize(): number { return this.offsets?.length ?? 1; }
240
+
210
241
  /** Row `i`'s own token coverage (per-row when batched, `offset` serial). */
211
242
  rowOffset(i: number): number {
212
243
  return this.offsets ? this.offsets[i]! : this.offset;
@@ -241,14 +272,77 @@ export class SSMCache implements Cache {
241
272
  }
242
273
 
243
274
  isTrimmable(): boolean {
244
- return false; // recurrent state can't be rolled back per token
275
+ return false; // recurrent state can't be trimmed per token — see specRound*
245
276
  }
246
277
 
247
278
  trim(_n: number): void {
248
279
  throw new Error("SSMCache is not trimmable");
249
280
  }
250
281
 
282
+ // --- speculative verify-round support (serial spec lane, B=1) ------------
283
+ // The serve loop arms a round before the verify forward and resolves it
284
+ // after the accept walk. The layer's forward, seeing an armed round, hands
285
+ // its replaced state slots to the round instead of disposing them, records
286
+ // qkv/a/b, and installs the replay. Cache/interface contract:
287
+ // rollback(keep) restores the snapshot then replays `keep` window tokens
288
+ // bit-exactly; commit frees the snapshot + recordings.
289
+
290
+ specRoundBegin(): void {
291
+ this.#dropSpecRound(); // defensive: a mid-round throw left a stale round
292
+ this.specRound = {
293
+ armed: true,
294
+ prevConv: null,
295
+ prevRecurrent: null,
296
+ prevOffset: this.offset,
297
+ qkv: null,
298
+ a: null,
299
+ b: null,
300
+ S: 0,
301
+ replay: null,
302
+ };
303
+ }
304
+
305
+ specRoundCommit(): void {
306
+ this.#dropSpecRound();
307
+ }
308
+
309
+ specRoundRollback(keep: number): void {
310
+ const r = this.specRound;
311
+ if (!r) throw new Error("SSMCache.specRoundRollback without an armed round");
312
+ if (r.armed)
313
+ throw new Error("SSMCache.specRoundRollback before the verify forward recorded");
314
+ if (keep < 0 || keep > r.S)
315
+ throw new Error(`SSMCache.specRoundRollback keep=${keep} outside window S=${r.S}`);
316
+ // Restore the pre-round snapshot (ownership moves back to the cache).
317
+ this.conv?.dispose();
318
+ this.recurrent?.dispose();
319
+ this.conv = r.prevConv;
320
+ this.recurrent = r.prevRecurrent;
321
+ r.prevConv = null;
322
+ r.prevRecurrent = null;
323
+ this.offset = r.prevOffset;
324
+ if (this.offsets)
325
+ throw new Error("SSMCache.specRoundRollback on a batched cache (serial lane only)");
326
+ if (keep > 0) {
327
+ if (!r.replay) throw new Error("SSMCache.specRoundRollback with no recorded replay");
328
+ r.replay(this, keep); // advances conv/recurrent/offset by `keep`
329
+ }
330
+ this.#dropSpecRound();
331
+ }
332
+
333
+ #dropSpecRound(): void {
334
+ const r = this.specRound;
335
+ if (!r) return;
336
+ r.prevConv?.dispose();
337
+ r.prevRecurrent?.dispose();
338
+ r.qkv?.dispose();
339
+ r.a?.dispose();
340
+ r.b?.dispose();
341
+ this.specRound = null;
342
+ }
343
+
251
344
  dispose(): void {
345
+ this.#dropSpecRound();
252
346
  this.conv?.dispose();
253
347
  this.recurrent?.dispose();
254
348
  this.conv = null;
@@ -304,6 +398,8 @@ export class SSMCache implements Cache {
304
398
  if (this.offsets) this.offsets = keep.map((i) => this.offsets![i]!);
305
399
  }
306
400
 
401
+ filterRows(keep: readonly number[]): void { this.filter([...keep]); }
402
+
307
403
  /** Row `i` as a fresh SERIAL cache — port of mlx-lm ArraysCache.extract
308
404
  * (cache.py:673-676: `cache.cache = [c[idx : idx + 1] for c in self.cache]`,
309
405
  * a B-axis slice of every state slot). Ours are OWNED contiguous copies
@@ -319,7 +415,7 @@ export class SSMCache implements Cache {
319
415
  const hi = [...a.shape];
320
416
  hi[0] = i + 1;
321
417
  const view = a.slice(lo, hi);
322
- const own = ops.contiguous(view);
418
+ const own = ops.copyOf(view); // TRUE copy: contiguous(view) is a no-op VIEW when already contiguous — pins the source buffer (2026-08-20 DeltaNet conv leak class)
323
419
  view.dispose();
324
420
  return own;
325
421
  };
@@ -0,0 +1,252 @@
1
+ // Qwen3.5/3.8 multimodal RoPE (mRoPE) — the language-side position machinery
2
+ // for vision requests (PLAN 14v), port of mlx-vlm's qwen3_5 language rope:
3
+ // MRoPERotaryEmbedding (style "interleaved", mrope_section [11,11,10]) +
4
+ // get_rope_index + the rope_deltas decode continuation.
5
+ //
6
+ // TEXT-ONLY EQUIVALENCE: with all three position streams equal, interleaved
7
+ // selection collapses to plain partial RoPE — which is why the text path's
8
+ // ops.rope stays bit-exact untouched. This module only activates for
9
+ // requests whose prompt contains vision spans; while active, EVERY
10
+ // full-attention rope in the request (prefill AND decode) goes through the
11
+ // manual interleaved path, mirroring the reference (which never uses the
12
+ // fused fast-rope kernel when position_ids are supplied).
13
+ //
14
+ // Position semantics (get_rope_index, B=1 serial): text tokens advance all
15
+ // three streams together; an image span of grid (t, h/merge, w/merge) gets
16
+ // t/h/w grid positions offset by the current base; text after an image
17
+ // resumes at max+1. delta = (max position + 1) - sequence length; decode
18
+ // positions are offset + delta on all three streams.
19
+
20
+ import { MlxArray } from "../mlx/array";
21
+ import { Dtype } from "../mlx/ffi";
22
+ import * as ops from "../mlx/ops";
23
+ import { MetalKernel } from "../mlx/metal-kernel";
24
+
25
+ export interface MropeRequestState {
26
+ /** Full-grid positions for the prompt, one Int32Array per t/h/w stream. */
27
+ positions: [Int32Array, Int32Array, Int32Array];
28
+ /** (max position + 1) - promptLength; decode positions = offset + delta. */
29
+ delta: number;
30
+ }
31
+
32
+ export interface MropeForwardState {
33
+ /** Effective positions [3, 1, L] int32 for the CURRENT forward window. */
34
+ posIds: MlxArray;
35
+ /** Borrowed model-owned f32 inv_freq (mropeInvFreq) — not disposed here. */
36
+ invFreq: MlxArray;
37
+ rotaryDims: number;
38
+ }
39
+
40
+ /** Active per-forward mRoPE cos/sin, read by Qwen3Attention.forward. Null on
41
+ * the text-only path (serial lane only — vision requests never batch). */
42
+ export let activeMrope: MropeForwardState | null = null;
43
+ export function setActiveMrope(s: MropeForwardState | null): void {
44
+ activeMrope = s;
45
+ }
46
+
47
+ const MROPE_SECTION = [11, 11, 10] as const;
48
+
49
+ /** Interleaved frequency→stream selector (rope_utils
50
+ * _interleaved_position_selector): default t(0); indices 1,4,7,… take
51
+ * h(1) while < section[1]*3; indices 2,5,8,… take w(2) while < section[2]*3. */
52
+ export function interleavedSelector(freqDim: number): Int32Array {
53
+ const sel = new Int32Array(freqDim);
54
+ for (let dim = 1; dim <= 2; dim++) {
55
+ const offset = dim; // h starts at 1, w at 2
56
+ for (let i = offset; i < Math.min(MROPE_SECTION[dim]! * 3, freqDim); i += 3)
57
+ sel[i] = dim;
58
+ }
59
+ return sel;
60
+ }
61
+
62
+ /** inv_freq for the language rope — computed on-device exactly like
63
+ * compute_inv_freq (Metal powf is not correctly rounded; a JS emulation
64
+ * drifts by 1 ulp on some entries). Returned as a live f32 device array
65
+ * (the apply kernel reads it directly). */
66
+ export function mropeInvFreq(rotaryDims: number, base: number): MlxArray {
67
+ const ar = ops.arange(0, rotaryDims, 2, Dtype.float32);
68
+ const ex = ops.mulScalar(ar, 1 / rotaryDims);
69
+ ar.dispose();
70
+ const theta = MlxArray.fromFloat32(Float32Array.from([base]), [1]);
71
+ const p = ops.pow(theta, ex);
72
+ theta.dispose();
73
+ ex.dispose();
74
+ const one = MlxArray.fromFloat32(Float32Array.from([1]), [1]);
75
+ const invA = ops.div(one, p);
76
+ one.dispose();
77
+ p.dispose();
78
+ return invA;
79
+ }
80
+
81
+ /** Effective positions [3,1,L] int32 for window [offset, offset+L) —
82
+ * prompt-grid positions while inside the prompt, offset+delta beyond it
83
+ * (the reference's rope_deltas continuation). */
84
+ export function buildMropePositions(
85
+ state: MropeRequestState, offset: number, L: number,
86
+ invFreq: MlxArray, rotaryDims: number,
87
+ ): MropeForwardState {
88
+ const P = state.positions;
89
+ const promptLen = P[0].length;
90
+ const joint = new Int32Array(3 * L);
91
+ for (let axis = 0; axis < 3; axis++) {
92
+ for (let l = 0; l < L; l++) {
93
+ const idx = offset + l;
94
+ joint[axis * L + l] = idx < promptLen ? P[axis]![idx]! : idx + state.delta;
95
+ }
96
+ }
97
+ return { posIds: MlxArray.fromInt32(joint, [3, 1, L]), invFreq, rotaryDims };
98
+ }
99
+
100
+ // The reference language-side apply is a CUSTOM METAL KERNEL (rope_utils
101
+ // _mrope_apply_kernel, pairing "half_split", position_ndim 3): per rotated
102
+ // pair it computes angle = pos * inv_freq in f32, cos/sin in-kernel, and
103
+ // writes x*c − x_pair*s / x_pair*c + x*s with ONE rounding to the tensor
104
+ // dtype. A bf16 cos/sin manual path is ~0.4% off per rotation and moved
105
+ // step-0 logits by ~1.0 — this verbatim port is the oracle's arithmetic.
106
+ const kernelCache = new Map<number, MetalKernel>();
107
+ function mropeKernel(rotaryDims: number): MetalKernel {
108
+ let k = kernelCache.get(rotaryDims);
109
+ if (k) return k;
110
+ const half = rotaryDims / 2;
111
+ const source = `
112
+ uint elem = thread_position_in_grid.x;
113
+
114
+ const int half_dim = ${half};
115
+ const int q_bsz = x_shape[0];
116
+ const int q_heads = x_shape[1];
117
+ const int q_len = x_shape[2];
118
+ const int q_dim = x_shape[3];
119
+ const int slots = half_dim + q_dim - ${rotaryDims};
120
+ const int work_size = q_bsz * q_heads * q_len * slots;
121
+
122
+ if (elem >= uint(work_size)) {
123
+ return;
124
+ }
125
+
126
+ int local = int(elem);
127
+ int slot = local % slots;
128
+ int tmp = local / slots;
129
+ int t = tmp % q_len;
130
+ tmp = tmp / q_len;
131
+ int h = tmp % q_heads;
132
+ int b = tmp / q_heads;
133
+ int base = ((b * q_heads + h) * q_len + t) * q_dim;
134
+
135
+ if (slot >= half_dim) {
136
+ int pass_d = ${rotaryDims} + slot - half_dim;
137
+ int pass_idx = base + pass_d;
138
+ x_out[pass_idx] = x[pass_idx];
139
+ return;
140
+ }
141
+
142
+ int freq_idx = slot;
143
+ int d = freq_idx;
144
+ int pair_d = d + half_dim;
145
+ int axis = int(position_selector[freq_idx]);
146
+ float pos = static_cast<float>(position_ids[(axis * q_bsz + b) * q_len + t]);
147
+ float angle = pos * static_cast<float>(inv_freq[freq_idx]);
148
+ float c = metal::cos(angle);
149
+ float s = metal::sin(angle);
150
+
151
+ int idx = base + d;
152
+ float xv = static_cast<float>(x[idx]);
153
+ float xp = static_cast<float>(x[base + pair_d]);
154
+ x_out[idx] = static_cast<T>(xv * c - xp * s);
155
+ x_out[base + pair_d] = static_cast<T>(xp * c + xv * s);
156
+ `;
157
+ k = new MetalKernel({
158
+ name: `mrope_apply_half_split_${rotaryDims}_3d`,
159
+ inputNames: ["x", "position_ids", "inv_freq", "position_selector"],
160
+ outputNames: ["x_out"],
161
+ source,
162
+ ensureRowContiguous: true,
163
+ });
164
+ kernelCache.set(rotaryDims, k);
165
+ return k;
166
+ }
167
+
168
+ const selectorCache = new Map<number, MlxArray>();
169
+ function selectorArr(half: number): MlxArray {
170
+ let s = selectorCache.get(half);
171
+ if (!s) {
172
+ s = MlxArray.fromInt32(interleavedSelector(half), [half]);
173
+ selectorCache.set(half, s);
174
+ }
175
+ return s;
176
+ }
177
+
178
+ /** Apply interleaved mRoPE to one [B, heads, L, headDim] tensor via the
179
+ * reference's exact Metal kernel. `invFreq` is the f32 device array from
180
+ * mropeInvFreq. */
181
+ export function applyInterleavedRope(
182
+ x: MlxArray, fwd: MropeForwardState,
183
+ ): MlxArray {
184
+ const rotaryDims = fwd.rotaryDims;
185
+ const [B, H, L, D] = x.shape as [number, number, number, number];
186
+ const half = rotaryDims / 2;
187
+ const slots = half + D - rotaryDims;
188
+ const work = B * H * L * slots;
189
+ const [out] = mropeKernel(rotaryDims).apply(
190
+ [x, fwd.posIds, fwd.invFreq, selectorArr(half)],
191
+ {
192
+ outputs: [{ shape: [B, H, L, D], dtype: x.dtype }],
193
+ grid: [work, 1, 1],
194
+ threadGroup: [256, 1, 1],
195
+ templateDtypes: { T: x.dtype },
196
+ },
197
+ );
198
+ return out!;
199
+ }
200
+
201
+ /** get_rope_index port (B=1, no attention mask): positions per stream + the
202
+ * decode delta for a prompt with vision spans. `grids` are the per-image
203
+ * (t, h, w) grids in PATCH units and must appear in prompt order. */
204
+ export function qwenRopeIndex(
205
+ ids: readonly number[],
206
+ grids: readonly [number, number, number][],
207
+ mergeSize: number,
208
+ imageTokenId: number,
209
+ videoTokenId: number,
210
+ ): MropeRequestState {
211
+ const L = ids.length;
212
+ const P: [Int32Array, Int32Array, Int32Array] =
213
+ [new Int32Array(L), new Int32Array(L), new Int32Array(L)];
214
+ let write = 0; // next output index (== st in the reference walk)
215
+ let base = 0; // st_idx: next position after everything placed so far
216
+ let gi = 0;
217
+ let i = 0;
218
+ while (i < L) {
219
+ const tok = ids[i]!;
220
+ if ((tok === imageTokenId || tok === videoTokenId) && gi < grids.length) {
221
+ const [t, h, w] = grids[gi]!;
222
+ gi++;
223
+ const gh = Math.trunc(h / mergeSize);
224
+ const gw = Math.trunc(w / mergeSize);
225
+ const span = t * gh * gw;
226
+ for (let k = 0; k < span; k++) {
227
+ const tt = Math.trunc(k / (gh * gw));
228
+ const rem = k % (gh * gw);
229
+ const hh = Math.trunc(rem / gw);
230
+ const ww = rem % gw;
231
+ P[0][write + k] = base + tt;
232
+ P[1][write + k] = base + hh;
233
+ P[2][write + k] = base + ww;
234
+ }
235
+ // Reference: next base = max position in the span + 1 = base +
236
+ // max(t, gh, gw) … computed as llm_pos_ids.max()+1.
237
+ base = base + Math.max(t, gh, gw);
238
+ write += span;
239
+ i += span;
240
+ } else {
241
+ P[0][write] = base;
242
+ P[1][write] = base;
243
+ P[2][write] = base;
244
+ base++;
245
+ write++;
246
+ i++;
247
+ }
248
+ }
249
+ if (write !== L)
250
+ throw new Error(`qwenRopeIndex covered ${write} of ${L} tokens (grids misaligned?)`);
251
+ return { positions: P, delta: base - L };
252
+ }