mlx-bun 0.0.13 → 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 (224) hide show
  1. package/README.md +31 -21
  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/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
  8. package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
  9. package/docs/design/colibri-glm52-port.md +1 -1
  10. package/docs/design/compat-cli-surface-design.md +1 -1
  11. package/docs/design/decode-speed-program.md +1 -1
  12. package/docs/design/docs-reorg-plan.md +1 -1
  13. package/docs/design/dspark-serving-program.md +1 -1
  14. package/docs/design/dspark-speculative-decoding.md +53 -3
  15. package/docs/design/grammar-spec-batching-integration.md +1 -1
  16. package/docs/design/mlx-lm-serving-execution-seam.md +81 -0
  17. package/docs/design/orpo-dynamic-lambda.md +1 -1
  18. package/docs/design/orpo-future-enhancements.md +1 -1
  19. package/docs/design/orpo-training.md +2 -2
  20. package/docs/design/parity-tier-dag.md +1 -1
  21. package/docs/design/repo-cleanup-plan.md +46 -4
  22. package/docs/design/segmented-backward-training.md +2 -2
  23. package/docs/design/ssd-kv-cold-tier.md +127 -0
  24. package/docs/design/turboquant-weights.md +467 -0
  25. package/docs/design/w4a16-compute-precision-spike.md +1 -1
  26. package/docs/design/web-chat-redesign.md +5 -5
  27. package/docs/planning/chunk-task-roadmap.md +1 -1
  28. package/docs/planning/curve-sampler-research-plan.md +4 -4
  29. package/docs/planning/memory-docs-and-dag-plan.md +1 -1
  30. package/docs/planning/release-notes-v0.2.0.md +49 -0
  31. package/docs/planning/video-series.md +421 -0
  32. package/docs/reference/cli.md +15 -4
  33. package/docs/reference/distribution.md +1 -0
  34. package/docs/reference/features-matrix.md +3 -2
  35. package/docs/reference/library-api.md +41 -1
  36. package/docs/reference/models.md +33 -0
  37. package/docs/reference/server-api.md +69 -14
  38. package/docs/reference/server-config.md +57 -33
  39. package/package.json +2 -2
  40. package/src/anthropic.ts +92 -3
  41. package/src/cli.ts +131 -36
  42. package/src/eval/kl.ts +27 -20
  43. package/src/eval/runner.ts +23 -4
  44. package/src/eval/tasks/bfcl.ts +2 -1
  45. package/src/eval/tasks/gsm8k.ts +2 -1
  46. package/src/eval/tasks/hashhop.ts +2 -1
  47. package/src/eval/tasks/humaneval.ts +5 -4
  48. package/src/eval/tasks/ifeval.ts +2 -1
  49. package/src/eval/tasks/mmlu.ts +2 -1
  50. package/src/expert-io.ts +3 -1
  51. package/src/expert-offload.ts +3 -1
  52. package/src/expert-trace.ts +3 -1
  53. package/src/fit.ts +24 -43
  54. package/src/flags.ts +7 -9
  55. package/src/generate.ts +107 -92
  56. package/src/grammar.ts +2 -1
  57. package/src/harness-pi.ts +1 -1
  58. package/src/index.ts +20 -0
  59. package/src/jobs/job-entry.ts +3 -2
  60. package/src/kv-scheme.ts +199 -0
  61. package/src/kv-store.ts +370 -205
  62. package/src/media-fetch.ts +19 -3
  63. package/src/memory/model.ts +2 -1
  64. package/src/memory/vault.ts +2 -1
  65. package/src/memory-plan.ts +18 -0
  66. package/src/mlx/ffi.ts +8 -11
  67. package/src/mlx/ops.ts +31 -6
  68. package/src/model/batched-mask.ts +1 -1
  69. package/src/model/batched-quant.ts +1 -1
  70. package/src/model/batched-rotating-quant.ts +43 -97
  71. package/src/model/batched-rotating-state.ts +73 -0
  72. package/src/model/batched-rotating.ts +56 -105
  73. package/src/model/batched-row-storage.ts +127 -0
  74. package/src/model/compiled-decode.ts +3 -2
  75. package/src/model/factory.ts +44 -49
  76. package/src/model/gemma4-base.ts +163 -10
  77. package/src/model/generated/gemma4-12b.ts +50 -146
  78. package/src/model/generated/gemma4-26b.ts +32 -92
  79. package/src/model/generated/gemma4-e4b.ts +26 -74
  80. package/src/model/glm52-cache.ts +19 -9
  81. package/src/model/glm52-memory.ts +28 -1
  82. package/src/model/minicpm5.ts +2 -1
  83. package/src/model/profile.ts +433 -0
  84. package/src/model/qwen3-delta.ts +98 -2
  85. package/src/model/qwen3-mrope.ts +252 -0
  86. package/src/model/qwen3_5.ts +217 -25
  87. package/src/native/frame_extract.swift +140 -0
  88. package/src/native-pack.ts +13 -6
  89. package/src/pi-web.ts +6 -5
  90. package/src/prompt-cache.ts +2 -2
  91. package/src/quantize/atomic-output.ts +38 -0
  92. package/src/quantize/calibration.ts +2 -1
  93. package/src/quantize/config-writer.ts +6 -0
  94. package/src/quantize/index.ts +28 -1
  95. package/src/quantize/job.ts +11 -0
  96. package/src/quantize/quantizer.ts +265 -100
  97. package/src/quantize/rotate.ts +603 -0
  98. package/src/quantize/safetensors-writer.ts +79 -1
  99. package/src/quantize/sensitivity.ts +41 -59
  100. package/src/quantize/weight-transform.ts +292 -0
  101. package/src/responses.ts +102 -2
  102. package/src/runtime-config.ts +67 -0
  103. package/src/sampler.ts +272 -0
  104. package/src/serve/admin-routes.ts +184 -0
  105. package/src/serve/aux-routes.ts +132 -0
  106. package/src/serve/batch-scheduler.ts +200 -87
  107. package/src/serve/completion-executor.ts +346 -0
  108. package/src/serve/completion-sink.ts +157 -0
  109. package/src/serve/discovery-routes.ts +205 -0
  110. package/src/serve/generation-gateway.ts +116 -109
  111. package/src/serve/kv-budget.ts +14 -0
  112. package/src/serve/lab-routes.ts +338 -0
  113. package/src/serve/model-admin-routes.ts +153 -0
  114. package/src/serve/prompt-response-trace.ts +147 -0
  115. package/src/serve/request-plan.ts +158 -0
  116. package/src/serve/static-routes.ts +101 -0
  117. package/src/server.ts +704 -1400
  118. package/src/spec/dflash-source.ts +2 -1
  119. package/src/spec/dspark/module-dflash.ts +1 -1
  120. package/src/spec/glm52-mtp-source.ts +8 -0
  121. package/src/spec/qwen-mtp-source.ts +431 -0
  122. package/src/spec/serve-loop.ts +36 -68
  123. package/src/ssd-cache.ts +17 -14
  124. package/src/ssd-durability.ts +195 -0
  125. package/src/train/flash-cce.ts +12 -11
  126. package/src/train/forward.ts +32 -1
  127. package/src/train/loss.ts +2 -1
  128. package/src/train/segmented.ts +4 -3
  129. package/src/train/trainer.ts +3 -2
  130. package/src/vision/preprocess.ts +1 -1
  131. package/src/vision/prompt.ts +47 -1
  132. package/src/vision/qwen3vl-preprocess.ts +409 -0
  133. package/src/vision/qwen3vl-prompt.ts +196 -0
  134. package/src/vision/qwen3vl-tower.ts +513 -0
  135. package/src/vision/video-frames.ts +148 -0
  136. package/src/web/app.js +10 -3
  137. package/src/web/src/chat.ts +12 -5
  138. package/src/weights.ts +81 -26
  139. /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
  140. /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
  141. /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
  142. /package/docs/{investigations → archive/investigations}/curve-runs/bisector-route.json +0 -0
  143. /package/docs/{investigations → archive/investigations}/curve-runs/bisector-trace.json +0 -0
  144. /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.json +0 -0
  145. /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.svg +0 -0
  146. /package/docs/{investigations → archive/investigations}/curve-terrain.html +0 -0
  147. /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
  148. /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
  149. /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
  150. /package/docs/{investigations → archive/investigations}/hlg-ab.html +0 -0
  151. /package/docs/{investigations → archive/investigations}/hlg-explorer.html +0 -0
  152. /package/docs/{investigations → archive/investigations}/hlg-figs/cliff.svg +0 -0
  153. /package/docs/{investigations → archive/investigations}/hlg-figs/cross.svg +0 -0
  154. /package/docs/{investigations → archive/investigations}/hlg-figs/edges.svg +0 -0
  155. /package/docs/{investigations → archive/investigations}/hlg-figs/k20-grids.svg +0 -0
  156. /package/docs/{investigations → archive/investigations}/hlg-figs/k20-peaks.svg +0 -0
  157. /package/docs/{investigations → archive/investigations}/hlg-figs/lw-curve.svg +0 -0
  158. /package/docs/{investigations → archive/investigations}/hlg-figs/terrain.svg +0 -0
  159. /package/docs/{investigations → archive/investigations}/hlg-report.html +0 -0
  160. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-ab.json +0 -0
  161. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind-prompts.md +0 -0
  162. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind.json +0 -0
  163. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cliff.json +0 -0
  164. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cross.json +0 -0
  165. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-edge.json +0 -0
  166. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-eval1.json +0 -0
  167. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-frontier.json +0 -0
  168. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid.json +0 -0
  169. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid2.json +0 -0
  170. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-lw-coarse.json +0 -0
  171. /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-map.json +0 -0
  172. /package/docs/{investigations → archive/investigations}/hlg-runs/run0-original.log +0 -0
  173. /package/docs/{investigations → archive/investigations}/hlg-runs/runA-recommended.log +0 -0
  174. /package/docs/{investigations → archive/investigations}/hlg-runs/runAA-cliff-cpm5.log +0 -0
  175. /package/docs/{investigations → archive/investigations}/hlg-runs/runAB-grid2-e4b.log +0 -0
  176. /package/docs/{investigations → archive/investigations}/hlg-runs/runAC-peak-e4b-partial.log +0 -0
  177. /package/docs/{investigations → archive/investigations}/hlg-runs/runAD-ab-samples.log +0 -0
  178. /package/docs/{investigations → archive/investigations}/hlg-runs/runAE-edge-e4b.log +0 -0
  179. /package/docs/{investigations → archive/investigations}/hlg-runs/runAF-logit-calibration.log +0 -0
  180. /package/docs/{investigations → archive/investigations}/hlg-runs/runAG-eval-config2.log +0 -0
  181. /package/docs/{investigations → archive/investigations}/hlg-runs/runB-gain-variants.log +0 -0
  182. /package/docs/{investigations → archive/investigations}/hlg-runs/runC-pivots.log +0 -0
  183. /package/docs/{investigations → archive/investigations}/hlg-runs/runD-oetf.log +0 -0
  184. /package/docs/{investigations → archive/investigations}/hlg-runs/runE-eotf.log +0 -0
  185. /package/docs/{investigations → archive/investigations}/hlg-runs/runF-shaper-working.log +0 -0
  186. /package/docs/{investigations → archive/investigations}/hlg-runs/runG-shaper-sweep.log +0 -0
  187. /package/docs/{investigations → archive/investigations}/hlg-runs/runH-wsweep.log +0 -0
  188. /package/docs/{investigations → archive/investigations}/hlg-runs/runI-asweep.log +0 -0
  189. /package/docs/{investigations → archive/investigations}/hlg-runs/runJ-knobmap.log +0 -0
  190. /package/docs/{investigations → archive/investigations}/hlg-runs/runK-frontier.log +0 -0
  191. /package/docs/{investigations → archive/investigations}/hlg-runs/runL-widemap.log +0 -0
  192. /package/docs/{investigations → archive/investigations}/hlg-runs/runM-frontier-emb.log +0 -0
  193. /package/docs/{investigations → archive/investigations}/hlg-runs/runN-verify.log +0 -0
  194. /package/docs/{investigations → archive/investigations}/hlg-runs/runO-frontier-seed5000.log +0 -0
  195. /package/docs/{investigations → archive/investigations}/hlg-runs/runP-grid-AxSm.log +0 -0
  196. /package/docs/{investigations → archive/investigations}/hlg-runs/runQ-grid-tgW.log +0 -0
  197. /package/docs/{investigations → archive/investigations}/hlg-runs/runR-blind-transcript.log +0 -0
  198. /package/docs/{investigations → archive/investigations}/hlg-runs/runS-cross-coherence.log +0 -0
  199. /package/docs/{investigations → archive/investigations}/hlg-runs/runT-cross-cpm5.log +0 -0
  200. /package/docs/{investigations → archive/investigations}/hlg-runs/runU-cross-12b.log +0 -0
  201. /package/docs/{investigations → archive/investigations}/hlg-runs/runV-lw-e4b.log +0 -0
  202. /package/docs/{investigations → archive/investigations}/hlg-runs/runW-lw-12b.log +0 -0
  203. /package/docs/{investigations → archive/investigations}/hlg-runs/runX-lw-cpm5.log +0 -0
  204. /package/docs/{investigations → archive/investigations}/hlg-runs/runY-cliff-e4b.log +0 -0
  205. /package/docs/{investigations → archive/investigations}/hlg-runs/runZ-cliff-12b.log +0 -0
  206. /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
  207. /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
  208. /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
  209. /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
  210. /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
  211. /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
  212. /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
  213. /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
  214. /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
  215. /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
  216. /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
  217. /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
  218. /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
  219. /package/docs/{planning → archive/planning}/journal.md +0 -0
  220. /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
  221. /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
  222. /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
  223. /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
  224. /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
@@ -30,6 +30,10 @@ import {
30
30
  type Mask,
31
31
  } from "./gemma4-base";
32
32
  import { gatedDeltaUpdate, SSMCache } from "./qwen3-delta";
33
+ import {
34
+ activeMrope, applyInterleavedRope, buildMropePositions, mropeInvFreq,
35
+ setActiveMrope, type MropeRequestState,
36
+ } from "./qwen3-mrope";
33
37
 
34
38
  const PREFIX = "language_model";
35
39
 
@@ -146,30 +150,58 @@ export class GatedDeltaNet {
146
150
  const [B, S] = x.shape as [number, number, number];
147
151
  const convDim = this.keyDim * 2 + this.valueDim;
148
152
  const nKeep = this.convKernel - 1;
153
+ const prof = (globalThis as Record<string, unknown>).__deltaProf as
154
+ Record<string, number>
155
+ | undefined;
156
+
157
+ // Armed speculative verify round (serve loop, serial lane): this forward
158
+ // must be rewindable. Snapshot = hand the REPLACED state slots to the
159
+ // round instead of disposing (free — arrays are immutable); record the
160
+ // position-local kernel inputs (qkv/a/b) and install the prefix replay.
161
+ const spec = cache.specRound;
162
+ if (spec) {
163
+ if (!spec.armed)
164
+ throw new Error("GatedDeltaNet: spec round already recorded this round");
165
+ spec.armed = false;
166
+ spec.S = S;
167
+ spec.replay = (c, keep) => this.#replaySpecPrefix(c, keep);
168
+ }
149
169
 
170
+ const t0 = prof ? performance.now() : 0;
150
171
  const qkv = this.inProjQkv.forward(x); // [B,S,convDim]
151
172
  let z = this.inProjZ.forward(x);
152
173
  z = disposing(z, ops.reshape(z, [B, S, this.numVHeads, this.headVDim]));
153
174
  const b = this.inProjB.forward(x); // [B,S,numVHeads]
154
- const a = this.inProjA.forward(x); // [B,S,numVHeads]
175
+ const a = this.inProjA.forward(x);
176
+ if (prof) { ops.evalAll([qkv, z, b, a]); prof.proj = (prof.proj ?? 0) + performance.now() - t0; }
177
+ const tc = prof ? performance.now() : 0; // [B,S,numVHeads]
155
178
 
156
179
  // Causal depthwise conv with the conv-state prefix (B=1: no ssm mask).
157
180
  const convState =
158
181
  cache.conv ?? ops.zeros([B, nKeep, convDim], x.dtype);
159
182
  const convInput = ops.concatAxis([convState, qkv], 1); // [B,S+nKeep,convDim]
160
183
  if (!cache.conv) convState.dispose();
161
- qkv.dispose();
162
- // New conv state = last nKeep rows (contiguous, the array is sliced).
163
- const newConv = ops.contiguous(
164
- convInput.slice([0, S, 0], [B, S + nKeep, convDim]),
165
- );
166
- cache.conv?.dispose();
184
+ if (spec) spec.qkv = qkv;
185
+ else qkv.dispose();
186
+ // New conv state = last nKeep rows. MUST be a TRUE copy (mx.copy):
187
+ // this tail slice is already row-contiguous at B=1, so ops.contiguous
188
+ // is a no-op VIEW sharing convInput's buffer — caching it pinned the
189
+ // whole [B, S+nKeep, convDim] chunk buffer (~42 MB/layer/chunk, 48
190
+ // layers = the measured 2.0 GB-per-2048-chunk prefill leak; 46 GB
191
+ // active at 32k, async-GPU-OOM on 24 GB — M4 2026-08-20).
192
+ const newConvView = convInput.slice([0, S, 0], [B, S + nKeep, convDim]);
193
+ const newConv = ops.copyOf(newConvView);
194
+ newConvView.dispose();
195
+ if (spec) spec.prevConv = cache.conv;
196
+ else cache.conv?.dispose();
167
197
  cache.conv = newConv;
168
198
 
169
199
  const conv = ops.conv1d(convInput, this.convWeight, 1, 0, 1, convDim);
170
200
  convInput.dispose();
171
201
  const convOut = compiledSilu(conv); // nn.silu (mx.compile), matches mlx-lm
172
202
  conv.dispose();
203
+ if (prof) { ops.evalAll([convOut]); prof.conv = (prof.conv ?? 0) + performance.now() - tc; }
204
+ const tn = prof ? performance.now() : 0;
173
205
 
174
206
  const [qFlat, kFlat, vFlat] = ops.split(
175
207
  convOut, [this.keyDim, 2 * this.keyDim], -1,
@@ -187,16 +219,26 @@ export class GatedDeltaNet {
187
219
  q = disposing(q, ops.mulScalar(q, invScale * invScale));
188
220
  k = disposing(k, ops.rmsNorm(k, null, 1e-6));
189
221
  k = disposing(k, ops.mulScalar(k, invScale));
222
+ if (prof) { ops.evalAll([q, k]); prof.norms = (prof.norms ?? 0) + performance.now() - tn; }
223
+ const tk = prof ? performance.now() : 0;
190
224
 
191
225
  const [out, newState] = gatedDeltaUpdate(
192
226
  q, k, v, a, b, this.aLog, this.dtBias, cache.recurrent,
193
227
  );
228
+ if (prof) { ops.evalAll([out]); prof.kernel = (prof.kernel ?? 0) + performance.now() - tk; }
229
+ const to = prof ? performance.now() : 0;
194
230
  q.dispose();
195
231
  k.dispose();
196
232
  v.dispose();
197
- a.dispose();
198
- b.dispose();
199
- cache.recurrent?.dispose();
233
+ if (spec) {
234
+ spec.a = a;
235
+ spec.b = b;
236
+ spec.prevRecurrent = cache.recurrent;
237
+ } else {
238
+ a.dispose();
239
+ b.dispose();
240
+ cache.recurrent?.dispose();
241
+ }
200
242
  cache.recurrent = newState;
201
243
  cache.advance(S);
202
244
 
@@ -208,9 +250,80 @@ export class GatedDeltaNet {
208
250
  gated.dispose();
209
251
  const result = this.outProj.forward(merged);
210
252
  merged.dispose();
253
+ if (prof) { ops.evalAll([result]); prof.out = (prof.out ?? 0) + performance.now() - to; }
211
254
  return result;
212
255
  }
213
256
 
257
+ /** Speculative rollback replay: with the pre-round snapshot RESTORED onto
258
+ * `cache`, re-advance the first `keep` window tokens from the recorded
259
+ * position-local inputs. Every op mirrors forward() on the same values —
260
+ * conv windows, silu, per-position norms, and the kernel's serial prefix
261
+ * are all independent of the rejected tail — so the resulting conv +
262
+ * recurrent state is BIT-EXACTLY what a forward over only the accepted
263
+ * prefix would have produced. Output projections (z / out_proj) are state-
264
+ * free and skipped: only the states matter here. */
265
+ #replaySpecPrefix(cache: SSMCache, keep: number): void {
266
+ const r = cache.specRound;
267
+ if (!r || !r.qkv || !r.a || !r.b) throw new Error("GatedDeltaNet replay without recorded round inputs");
268
+ const B = r.qkv.shape[0]!;
269
+ const convDim = this.keyDim * 2 + this.valueDim;
270
+ const nKeep = this.convKernel - 1;
271
+
272
+ const qkvPfxView = r.qkv.slice([0, 0, 0], [B, keep, convDim]);
273
+ const qkvPfx = ops.contiguous(qkvPfxView);
274
+ qkvPfxView.dispose();
275
+ const convState = cache.conv ?? ops.zeros([B, nKeep, convDim], qkvPfx.dtype);
276
+ const convInput = ops.concatAxis([convState, qkvPfx], 1);
277
+ if (!cache.conv) convState.dispose();
278
+ qkvPfx.dispose();
279
+ // TRUE copy, same reason as forward(): the contiguous tail slice would
280
+ // otherwise be a view pinning the whole replay convInput buffer.
281
+ const rConvView = convInput.slice([0, keep, 0], [B, keep + nKeep, convDim]);
282
+ const newConv = ops.copyOf(rConvView);
283
+ rConvView.dispose();
284
+ cache.conv?.dispose();
285
+ cache.conv = newConv;
286
+
287
+ const conv = ops.conv1d(convInput, this.convWeight, 1, 0, 1, convDim);
288
+ convInput.dispose();
289
+ const convOut = compiledSilu(conv);
290
+ conv.dispose();
291
+ const [qFlat, kFlat, vFlat] = ops.split(
292
+ convOut, [this.keyDim, 2 * this.keyDim], -1,
293
+ ) as [MlxArray, MlxArray, MlxArray];
294
+ convOut.dispose();
295
+ let q = ops.reshape(qFlat, [B, keep, this.numKHeads, this.headKDim]);
296
+ qFlat.dispose();
297
+ let k = ops.reshape(kFlat, [B, keep, this.numKHeads, this.headKDim]);
298
+ kFlat.dispose();
299
+ const v = disposing(vFlat, ops.reshape(vFlat, [B, keep, this.numVHeads, this.headVDim]));
300
+ const invScale = Math.pow(this.headKDim, -0.5);
301
+ q = disposing(q, ops.rmsNorm(q, null, 1e-6));
302
+ q = disposing(q, ops.mulScalar(q, invScale * invScale));
303
+ k = disposing(k, ops.rmsNorm(k, null, 1e-6));
304
+ k = disposing(k, ops.mulScalar(k, invScale));
305
+
306
+ const aPfxView = r.a.slice([0, 0, 0], [B, keep, this.numVHeads]);
307
+ const aPfx = ops.contiguous(aPfxView);
308
+ aPfxView.dispose();
309
+ const bPfxView = r.b.slice([0, 0, 0], [B, keep, this.numVHeads]);
310
+ const bPfx = ops.contiguous(bPfxView);
311
+ bPfxView.dispose();
312
+
313
+ const [y, newState] = gatedDeltaUpdate(
314
+ q, k, v, aPfx, bPfx, this.aLog, this.dtBias, cache.recurrent,
315
+ );
316
+ q.dispose();
317
+ k.dispose();
318
+ v.dispose();
319
+ aPfx.dispose();
320
+ bPfx.dispose();
321
+ y.dispose(); // only the state advance matters on the rollback path
322
+ cache.recurrent?.dispose();
323
+ cache.recurrent = newState;
324
+ cache.advance(keep);
325
+ }
326
+
214
327
  private rmsNormGated(hidden: MlxArray, gate: MlxArray): MlxArray {
215
328
  const xn = ops.rmsNorm(hidden, this.normWeight, this.eps); // bf16
216
329
  // Oracle: _precise_swiglu(hidden, gate, xn) — ALWAYS one @mx.compile kernel
@@ -279,13 +392,23 @@ export class Qwen3Attention {
279
392
  // position as ropeOffsetArr (rows have different prompt lengths); the
280
393
  // dynamic-offset kernel is the same fast::rope, bit-exact vs the static
281
394
  // form (tests/compile.test.ts). Serial lane: scalar offset, unchanged.
395
+ // Vision requests (serial lane only) install activeMrope for the current
396
+ // forward: 3D interleaved positions via the manual apply — the reference
397
+ // never uses the fused fast-rope kernel when position_ids are supplied,
398
+ // so this IS the oracle's own arithmetic. Text-only stays on ops.rope.
399
+ const mr = activeMrope;
282
400
  const offArr = (cache as { ropeOffsetArr?: MlxArray }).ropeOffsetArr;
283
- q = disposing(q, offArr
284
- ? ops.ropeDynamic(q, this.ropeDims, this.ropeBase, offArr, null)
285
- : ops.rope(q, this.ropeDims, this.ropeBase, cache.offset, null));
286
- k = disposing(k, offArr
287
- ? ops.ropeDynamic(k, this.ropeDims, this.ropeBase, offArr, null)
288
- : ops.rope(k, this.ropeDims, this.ropeBase, cache.offset, null));
401
+ if (mr) {
402
+ q = disposing(q, applyInterleavedRope(q, mr));
403
+ k = disposing(k, applyInterleavedRope(k, mr));
404
+ } else {
405
+ q = disposing(q, offArr
406
+ ? ops.ropeDynamic(q, this.ropeDims, this.ropeBase, offArr, null)
407
+ : ops.rope(q, this.ropeDims, this.ropeBase, cache.offset, null));
408
+ k = disposing(k, offArr
409
+ ? ops.ropeDynamic(k, this.ropeDims, this.ropeBase, offArr, null)
410
+ : ops.rope(k, this.ropeDims, this.ropeBase, cache.offset, null));
411
+ }
289
412
 
290
413
  let attn: MlxArray;
291
414
  if (cache instanceof QuantizedKVCache) {
@@ -448,8 +571,42 @@ export class Qwen35Model {
448
571
  return this.forwardLayers(h, cache);
449
572
  }
450
573
 
451
- forwardEmbeddings(_embeds: MlxArray, _cache: Cache[], _bidir: MlxArray | null): MlxArray {
452
- throw new Error("qwen3_5 vision/input-embedding path is not supported");
574
+ /** Active vision mRoPE request state (serial lane; set by the generation
575
+ * gateway around a vision request's run, null for text-only — which keeps
576
+ * the bit-exact fast-rope path). While set, forwardLayers installs the
577
+ * per-forward interleaved cos/sin consumed by every full-attn layer. */
578
+ mrope: MropeRequestState | null = null;
579
+ #mropeInvFreq: MlxArray | null = null;
580
+
581
+ /** Vision prefill: spliced input embeddings [1, L, H] (image features
582
+ * overwriting image-token rows — the caller builds them). Caller keeps
583
+ * ownership of `embeds`. bidir/ids/multimodal are the Gemma-shaped extras
584
+ * and are unused: Qwen3.5 vision attends fully causally. */
585
+ forwardEmbeddings(
586
+ embeds: MlxArray, cache: Cache[], _bidir: MlxArray | null,
587
+ _ids: MlxArray | null = null, _multimodal: MlxArray | null = null,
588
+ ): MlxArray {
589
+ const h = ops.contiguous(embeds); // fresh handle — the layer loop consumes it
590
+ return this.forwardLayers(h, cache);
591
+ }
592
+
593
+ /** Layer-output tap (same contract as Gemma4Model.hiddenTap): the serve
594
+ * loop's forwardMaybeTap sets it around prefill/verify forwards so
595
+ * KV-borrowing draft sources (native Qwen MTP) can read PRE-final-norm
596
+ * hiddens — layer 63's output stream, what mlx-vlm captures with
597
+ * skip_final_norm=True. No-op (and graph-identical) when null. */
598
+ hiddenTap: { layers: Set<number>; pos?: number; captured: Map<number, MlxArray> } | null = null;
599
+
600
+ /** Store layer `i`'s residual stream ([1,L,H]) if the tap requests it.
601
+ * Copies so the loop's h.dispose() can't free the capture. */
602
+ protected captureLayer(i: number, h: MlxArray): void {
603
+ const tap = this.hiddenTap;
604
+ if (!tap || !tap.layers.has(i)) return;
605
+ const H = h.shape[2]!;
606
+ const src = tap.pos !== undefined ? h.slice([0, tap.pos, 0], [1, tap.pos + 1, H]) : h;
607
+ const copy = ops.contiguous(src);
608
+ if (src !== h) src.dispose();
609
+ tap.captured.set(i, copy);
453
610
  }
454
611
 
455
612
  protected forwardLayers(h0: MlxArray, cache: Cache[]): MlxArray {
@@ -457,14 +614,49 @@ export class Qwen35Model {
457
614
  // One full-attention mask shared by all full layers (same offset); linear
458
615
  // layers see no ssm mask at B=1.
459
616
  const faMask = cache[this.faIdx]!.makeMask(L, null);
460
- let h = h0;
461
- for (let i = 0; i < this.layers.length; i++) {
462
- const next = this.layers[i]!.forward(h, faMask, cache[i]!);
463
- h.dispose();
464
- h = next;
617
+ // Vision requests: one interleaved-mRoPE cos/sin table per forward,
618
+ // shared by all 12 full-attention layers (positions are layer-invariant).
619
+ let mropeFwd: ReturnType<typeof buildMropePositions> | null = null;
620
+ if (this.mrope) {
621
+ const t = this.config.text;
622
+ const ropeDims = Math.trunc(t.headDim * t.partialRotaryFactor);
623
+ const base = t.ropeParameters.full_attention?.ropeTheta ?? 10000;
624
+ this.#mropeInvFreq ??= mropeInvFreq(ropeDims, base);
625
+ mropeFwd = buildMropePositions(
626
+ this.mrope, cache[this.faIdx]!.offset, L, this.#mropeInvFreq, ropeDims,
627
+ );
628
+ setActiveMrope(mropeFwd);
629
+ }
630
+ let h: MlxArray | null = h0;
631
+ const prof = (globalThis as Record<string, unknown>).__deltaProf as
632
+ Record<string, number>
633
+ | undefined;
634
+ try {
635
+ for (let i = 0; i < this.layers.length; i++) {
636
+ const tl = prof ? performance.now() : 0;
637
+ const next = this.layers[i]!.forward(h, faMask, cache[i]!);
638
+ h.dispose();
639
+ h = next;
640
+ if (prof) {
641
+ ops.evalAll([h]);
642
+ const key = this.layers[i]!.isLinear ? "layerLin" : "layerFull";
643
+ prof[key] = (prof[key] ?? 0) + performance.now() - tl;
644
+ }
645
+ this.captureLayer(i, h); // native-MTP pre-final-norm tap (no-op unless set)
646
+ }
647
+ const out = disposing(h, this.finalNorm.forward(h));
648
+ h = null; // consumed — the finally must not double-free
649
+ return out;
650
+ } finally {
651
+ if (mropeFwd) {
652
+ setActiveMrope(null);
653
+ mropeFwd.posIds.dispose();
654
+ }
655
+ // A mid-loop layer throw must not strand the mask array or the
656
+ // in-flight [1,L,H] residual (2026-08-18 review).
657
+ faMask.arr?.dispose();
658
+ h?.dispose();
465
659
  }
466
- faMask.arr?.dispose();
467
- return disposing(h, this.finalNorm.forward(h));
468
660
  }
469
661
 
470
662
  logitsFromHidden(h: MlxArray): MlxArray {
@@ -0,0 +1,140 @@
1
+ // AVFoundation frame-extraction sidecar probe (PLAN 14w) — the video
2
+ // equivalent of the audio path's afconvert doctrine: macOS-native codecs,
3
+ // zero vendored dependencies.
4
+ //
5
+ // frame-extract <video> <outDir> [fps=2] [maxFrames=768]
6
+ // → writes frame-%04d.png (RGB) + prints "N <count> W <w> H <h>"
7
+ // frame-extract --make-fixture <out.mov>
8
+ // → writes the deterministic 6-frame 320x240 gradient test clip
9
+ //
10
+ // Sampling mirrors mlx-vlm's do_sample_frames: timestamps k/fps for
11
+ // k = 0..min(ceil(duration*fps), maxFrames)-1, precise tolerance.
12
+
13
+ import AVFoundation
14
+ import Foundation
15
+ import ImageIO
16
+ import UniformTypeIdentifiers
17
+
18
+ func die(_ msg: String) -> Never {
19
+ FileHandle.standardError.write((msg + "\n").data(using: .utf8)!)
20
+ exit(1)
21
+ }
22
+
23
+ func makeFixture(_ path: String) {
24
+ let url = URL(fileURLWithPath: path)
25
+ try? FileManager.default.removeItem(at: url)
26
+ guard let writer = try? AVAssetWriter(outputURL: url, fileType: .mov) else {
27
+ die("AVAssetWriter init failed")
28
+ }
29
+ let W = 320, H = 240, FRAMES = 6
30
+ let settings: [String: Any] = [
31
+ AVVideoCodecKey: AVVideoCodecType.h264,
32
+ AVVideoWidthKey: W,
33
+ AVVideoHeightKey: H,
34
+ ]
35
+ let input = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
36
+ let adaptor = AVAssetWriterInputPixelBufferAdaptor(
37
+ assetWriterInput: input,
38
+ sourcePixelBufferAttributes: [
39
+ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB,
40
+ kCVPixelBufferWidthKey as String: W,
41
+ kCVPixelBufferHeightKey as String: H,
42
+ ])
43
+ writer.add(input)
44
+ writer.startWriting()
45
+ writer.startSession(atSourceTime: .zero)
46
+ for f in 0..<FRAMES {
47
+ while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 0.01) }
48
+ var pb: CVPixelBuffer?
49
+ CVPixelBufferCreate(nil, W, H, kCVPixelFormatType_32ARGB, nil, &pb)
50
+ guard let pixelBuffer = pb else { die("CVPixelBufferCreate failed") }
51
+ CVPixelBufferLockBaseAddress(pixelBuffer, [])
52
+ let base = CVPixelBufferGetBaseAddress(pixelBuffer)!
53
+ let bpr = CVPixelBufferGetBytesPerRow(pixelBuffer)
54
+ let p = base.assumingMemoryBound(to: UInt8.self)
55
+ for y in 0..<H {
56
+ for x in 0..<W {
57
+ let o = y * bpr + x * 4
58
+ p[o] = 255
59
+ p[o + 1] = UInt8((x * 255 / W + f * 20) % 256)
60
+ p[o + 2] = UInt8((y * 255 / H + f * 10) % 256)
61
+ p[o + 3] = UInt8(((x + y) * 255 / (W + H) + f * 30) % 256)
62
+ }
63
+ }
64
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
65
+ adaptor.append(pixelBuffer, withPresentationTime: CMTime(value: CMTimeValue(f), timescale: 2))
66
+ }
67
+ input.markAsFinished()
68
+ let sem = DispatchSemaphore(value: 0)
69
+ writer.finishWriting { sem.signal() }
70
+ sem.wait()
71
+ guard writer.status == .completed else { die("finishWriting status \(writer.status.rawValue)") }
72
+ print("fixture \(path)")
73
+ }
74
+
75
+ let args = CommandLine.arguments
76
+ if args.count >= 3 && args[1] == "--make-fixture" {
77
+ makeFixture(args[2])
78
+ exit(0)
79
+ }
80
+ guard args.count >= 3 else { die("usage: frame-extract <video> <outDir> [fps] [maxFrames]") }
81
+ let videoPath = args[1]
82
+ let outDir = args[2]
83
+ let fps = args.count > 3 ? Double(args[3]) ?? 2.0 : 2.0
84
+ let maxFrames = args.count > 4 ? Int(args[4]) ?? 768 : 768
85
+
86
+ let asset = AVURLAsset(url: URL(fileURLWithPath: videoPath))
87
+ let sem = DispatchSemaphore(value: 0)
88
+ var duration = CMTime.zero
89
+ var loadError: Error?
90
+ Task {
91
+ do { duration = try await asset.load(.duration) } catch { loadError = error }
92
+ sem.signal()
93
+ }
94
+ sem.wait()
95
+ if let e = loadError { die("asset load failed: \(e.localizedDescription)") }
96
+ // Fragmented/malformed containers can load with an indefinite duration —
97
+ // CMTimeGetSeconds yields NaN and Int(NaN) is a Swift runtime TRAP. Fall
98
+ // back to the frame cap; the extraction loop below already stops at the
99
+ // first undecodable timestamp.
100
+ let seconds = CMTimeGetSeconds(duration)
101
+ let count = seconds.isFinite
102
+ ? max(1, min(Int((seconds * fps).rounded(.up)), maxFrames))
103
+ : maxFrames
104
+
105
+ let gen = AVAssetImageGenerator(asset: asset)
106
+ gen.appliesPreferredTrackTransform = true
107
+ gen.requestedTimeToleranceBefore = .zero
108
+ gen.requestedTimeToleranceAfter = .zero
109
+ // Bound DECODED memory at the source: the language pipeline's video pixel
110
+ // budget always downscales frames far below 1024px anyway (max_pixels
111
+ // 32·32·768 across t·h·w), so capping the longest edge here loses nothing
112
+ // it would have kept while keeping a 4K clip from materializing ~24 MB per
113
+ // frame (768 frames ≈ 17.8 GiB) before preprocessing.
114
+ gen.maximumSize = CGSize(width: 1024, height: 1024)
115
+
116
+ try? FileManager.default.createDirectory(
117
+ atPath: outDir, withIntermediateDirectories: true)
118
+ var written = 0
119
+ var lastW = 0, lastH = 0
120
+ for k in 0..<count {
121
+ let t = CMTime(seconds: Double(k) / fps, preferredTimescale: 600)
122
+ let img: CGImage
123
+ do {
124
+ img = try gen.copyCGImage(at: t, actualTime: nil)
125
+ } catch {
126
+ // Past-end sampling on short clips: stop at the last decodable frame.
127
+ break
128
+ }
129
+ lastW = img.width
130
+ lastH = img.height
131
+ let out = URL(fileURLWithPath: String(format: "%@/frame-%04d.png", outDir, k))
132
+ guard let dest = CGImageDestinationCreateWithURL(
133
+ out as CFURL, UTType.png.identifier as CFString, 1, nil)
134
+ else { die("png destination failed") }
135
+ CGImageDestinationAddImage(dest, img, nil)
136
+ guard CGImageDestinationFinalize(dest) else { die("png write failed") }
137
+ written += 1
138
+ }
139
+ guard written > 0 else { die("no frames decodable from \(videoPath)") }
140
+ print("N \(written) W \(lastW) H \(lastH)")
@@ -15,18 +15,24 @@ import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
15
15
  import { homedir, tmpdir } from "node:os";
16
16
  import { dirname, join } from "node:path";
17
17
  import { downloadOne } from "./download";
18
+ import { runtimeValue } from "./runtime-config";
18
19
 
19
- export const NATIVE_PACK_VERSION = "0.2.0";
20
+ export const NATIVE_PACK_VERSION = "0.3.0";
20
21
  export const NATIVE_PACK_FILES = [
21
22
  "libmlxc.dylib", "libmlx.dylib", "libjaccl.dylib", "mlx.metallib",
22
- "libmlx_bun_expert_io.dylib",
23
+ "libmlx_bun_expert_io.dylib", "mlx-bun-frame-extract",
23
24
  ] as const;
24
25
 
26
+ // RELEASE-TIME BAKE: scripts/build-native-pack.sh prints the sha256/size for
27
+ // the freshly built tarball — publish `native-v0.3.0` with both assets and
28
+ // paste the values here BEFORE tagging the package release (the download
29
+ // 404s until the GitHub release exists; dev trees resolve via homebrew/env
30
+ // and never hit this path).
25
31
  const SHA256: Record<string, string> = {
26
- arm64: "9bd3795c5ea8f52b18413501f2d68c32c264a20751302300a08dc04cd67df97c",
32
+ arm64: "b485fed858971ea6f072f349a0234c0e2d5030d3ef2ddfa942c8975eca2d3b2e",
27
33
  };
28
34
  const SIZE: Record<string, number> = {
29
- arm64: 52_307_647,
35
+ arm64: 52299265,
30
36
  };
31
37
 
32
38
  export function nativePackName(arch = process.arch): string {
@@ -37,7 +43,7 @@ export function nativePackName(arch = process.arch): string {
37
43
  * the repo is private the download needs a token (GITHUB_TOKEN or
38
44
  * `gh auth token`); once public it works anonymously. */
39
45
  export function nativePackUrl(arch = process.arch): string {
40
- return process.env.MLX_BUN_NATIVE_PACK_URL ??
46
+ return runtimeValue("MLX_BUN_NATIVE_PACK_URL") ??
41
47
  `https://github.com/joshuarossi/mlx-bun/releases/download/native-v${NATIVE_PACK_VERSION}/${nativePackName(arch)}`;
42
48
  }
43
49
 
@@ -48,7 +54,8 @@ export function nativePackDir(arch = process.arch): string {
48
54
  /** Where the native runtime currently resolves from, or null if absent
49
55
  * everywhere (= a fresh machine that needs the pack). */
50
56
  export function nativeRuntimeDir(): string | null {
51
- if (process.env.MLX_BUN_LIBMLXC) return dirname(process.env.MLX_BUN_LIBMLXC);
57
+ const explicit = runtimeValue("MLX_BUN_LIBMLXC");
58
+ if (explicit) return dirname(explicit);
52
59
  const candidates = [
53
60
  dirname(process.execPath),
54
61
  nativePackDir(),
package/src/pi-web.ts CHANGED
@@ -27,6 +27,7 @@ import { createHash } from "node:crypto";
27
27
  import { mkdirSync, rmSync } from "node:fs";
28
28
  import { homedir } from "node:os";
29
29
  import { join, resolve } from "node:path";
30
+ import { runtimeValue } from "./runtime-config";
30
31
  import type { ServerWebSocket, WebSocketHandler } from "bun";
31
32
  import {
32
33
  createAgentSessionFromServices,
@@ -1394,7 +1395,7 @@ class PiWebSession {
1394
1395
  (d) => d.state === "active" && d.repoId !== this.opts.modelId,
1395
1396
  )?.repoId ?? null,
1396
1397
  }, { hasTools: WELCOME_TOOLS.length > 0 }) + surface.memoryHint;
1397
- if (process.env.MLX_BUN_PI_DEBUG) {
1398
+ if (runtimeValue("MLX_BUN_PI_DEBUG")) {
1398
1399
  console.error(`[pi-web] prompt ${WEB_CHAT_PROMPT_VERSION} sha=${webChatPromptFingerprint(webPrompt)} memory=${surface.memoryEnabled ? "on" : "off"} codingTools=${this.codingToolsActive ? "on" : "off"}`);
1399
1400
  }
1400
1401
  const services = await createAgentSessionServices({
@@ -1826,7 +1827,7 @@ class PiWebSession {
1826
1827
 
1827
1828
  pi.on("tool_call", (event: ToolCallEvent) => {
1828
1829
  const block = decideBeforeToolCall(this.loopHygiene, event.toolName, event.input);
1829
- if (block && process.env.MLX_BUN_PI_DEBUG) {
1830
+ if (block && runtimeValue("MLX_BUN_PI_DEBUG")) {
1830
1831
  console.error(`[pi-web] loop-hygiene blocked ${event.toolName}: ${block.reason.split("\n")[0]}`);
1831
1832
  }
1832
1833
  return block ? { block: true, reason: block.reason } : undefined;
@@ -1852,7 +1853,7 @@ class PiWebSession {
1852
1853
  private installApprovalGate(pi: ExtensionAPI): void {
1853
1854
  pi.on("tool_call", async (event: ToolCallEvent) => {
1854
1855
  const tool = event.toolName;
1855
- if (process.env.MLX_BUN_PI_DEBUG) {
1856
+ if (runtimeValue("MLX_BUN_PI_DEBUG")) {
1856
1857
  console.error(`[pi-web] tool_call ${tool} args=${JSON.stringify(event.input)}`);
1857
1858
  }
1858
1859
 
@@ -1945,7 +1946,7 @@ class PiWebSession {
1945
1946
  }
1946
1947
 
1947
1948
  private onSessionEvent(event: AgentSessionEvent): void {
1948
- if (process.env.MLX_BUN_PI_DEBUG) {
1949
+ if (runtimeValue("MLX_BUN_PI_DEBUG")) {
1949
1950
  const extra = event.type === "message_update"
1950
1951
  ? `/${(event as { assistantMessageEvent?: { type?: string } }).assistantMessageEvent?.type}` : "";
1951
1952
  console.error(`[pi-event] ${event.type}${extra}`);
@@ -2025,7 +2026,7 @@ class PiWebSession {
2025
2026
  // isStreaming atomically inside prompt(), so this read can't strand a
2026
2027
  // message at the turn boundary.
2027
2028
  case "prompt": {
2028
- if (process.env.MLX_BUN_PI_DEBUG) {
2029
+ if (runtimeValue("MLX_BUN_PI_DEBUG")) {
2029
2030
  console.error(`[pi-web] prompt text=${JSON.stringify(msg.text.slice(0, 500))} images=${msg.images?.length ?? 0} streaming=${session.isStreaming} adapter=${this.selectedAdapter ?? "base"}`);
2030
2031
  }
2031
2032
  const images = toPiImages(msg.images);
@@ -24,7 +24,7 @@
24
24
  // reachable); onPut lets the server schedule its debounced write-behind
25
25
  // snapshot for both lanes.
26
26
 
27
- import { TurboQuantKVCache, type Cache } from "./model/gemma4";
27
+ import type { Cache } from "./model/gemma4";
28
28
  import { cloneKvCaches } from "./kv-store";
29
29
 
30
30
  /** Reference-counted release: wraps an entry's `retain` (e.g. an mmap
@@ -112,7 +112,7 @@ export function cacheBytes(caches: Cache[]): number {
112
112
  // views per call (kv-store.ts snapshotCache/cloneKvCaches contract) —
113
113
  // those must be disposed here or they leak (see generate.ts
114
114
  // evalCacheState for the same hazard on the prefill path).
115
- if (c instanceof TurboQuantKVCache) for (const a of state) a.dispose();
115
+ if (c.stateNeedsDispose) for (const a of state) a.dispose();
116
116
  }
117
117
  return total;
118
118
  }
@@ -0,0 +1,38 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ readdirSync,
6
+ renameSync,
7
+ rmdirSync,
8
+ rmSync,
9
+ statSync,
10
+ } from "node:fs";
11
+ import { basename, dirname, join, resolve } from "node:path";
12
+
13
+ /** Build a complete directory beside its destination, then publish it with a
14
+ * single same-filesystem rename. Failed writers leave no partial destination. */
15
+ export async function writeAtomicDirectory<T>(
16
+ outDir: string,
17
+ write: (stagingDir: string) => Promise<T>,
18
+ ): Promise<T> {
19
+ const destination = resolve(outDir);
20
+ const replaceEmptyDestination = existsSync(destination);
21
+ if (
22
+ replaceEmptyDestination &&
23
+ (!statSync(destination).isDirectory() || readdirSync(destination).length !== 0)
24
+ ) throw new Error(`output directory already exists: ${outDir}`);
25
+
26
+ const parent = dirname(destination);
27
+ mkdirSync(parent, { recursive: true });
28
+ const staging = mkdtempSync(join(parent, `.${basename(destination)}.tmp-`));
29
+ try {
30
+ const result = await write(staging);
31
+ if (replaceEmptyDestination) rmdirSync(destination);
32
+ renameSync(staging, destination);
33
+ return result;
34
+ } catch (error) {
35
+ rmSync(staging, { recursive: true, force: true });
36
+ throw error;
37
+ }
38
+ }
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { existsSync } from "node:fs";
22
22
  import type { LoadedTokenizer } from "../tokenizer";
23
+ import { runtimeValue } from "../runtime-config";
23
24
 
24
25
  /** One JSONL calibration sample (subset of the OptIQ schema we can consume). */
25
26
  interface CalibrationSample {
@@ -34,7 +35,7 @@ function bundledMixCandidates(): string[] {
34
35
  const home = process.env.HOME ?? "";
35
36
  const rel = "lib/python3.14/site-packages/optiq/calibration/data/optiq.jsonl";
36
37
  return [
37
- process.env.MLX_BUN_CALIBRATION_JSONL,
38
+ runtimeValue("MLX_BUN_CALIBRATION_JSONL"),
38
39
  `${home}/Code/mlx-lm-example/.venv/${rel}`,
39
40
  `${home}/Code/mlx-lm/.venv/${rel}`,
40
41
  ].filter((p): p is string => !!p);
@@ -76,6 +76,12 @@ export interface OptiqMetadata {
76
76
  group_size: number;
77
77
  achieved_bpw: number;
78
78
  per_layer_count: number;
79
+ weight_transforms?: Array<{
80
+ id: string;
81
+ seed: number;
82
+ family: string;
83
+ deviations: string[];
84
+ }>;
79
85
  }
80
86
 
81
87
  /**