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
@@ -26,7 +26,10 @@ import { cpuStream, MlxArray } from "../mlx/array";
26
26
  import * as ops from "../mlx/ops";
27
27
  import { trainForward } from "../train/forward";
28
28
  import type { RuntimeModel } from "../model/factory";
29
- import type { QuantizedLinear } from "../model/gemma4-base";
29
+ import type {
30
+ QuantizedLinear,
31
+ QuantizedLinearState,
32
+ } from "../model/gemma4-base";
30
33
 
31
34
  /** Per-layer KL sensitivity at each candidate bit-width (port of
32
35
  * optiq SensitivityResult). */
@@ -43,46 +46,23 @@ export interface AnalyzeSensitivityOptions {
43
46
  }
44
47
 
45
48
  // --------------------------------------------------------------------------
46
- // Minimal in-place mutation of a QuantizedLinear.
47
- //
48
- // gemma4-base.ts declares QuantizedLinear's w/scales/biases/spec as `readonly`
49
- // (they never change at inference time). The sensitivity sweep needs to swap a
50
- // layer's quantized weight transiently. Rather than edit the model file, we
51
- // poke the public fields here via a typed cast — a quantize-only mutation
52
- // helper. The fields are public, so this is a write to public state, not a
53
- // reach into private internals. Flagged in the port report.
49
+ // Scoped state exchange for sensitivity probes. QuantizedLinear owns the only
50
+ // mutation capability; inference consumers retain a read-only getter surface.
54
51
  // --------------------------------------------------------------------------
55
52
 
56
- /** Mutable view of the public QuantizedLinear fields we swap. */
57
- type MutableQuantizedLinear = {
58
- w: MlxArray;
59
- scales: MlxArray;
60
- biases: MlxArray | null;
61
- spec: ops.QuantSpec;
62
- };
63
-
64
53
  /** Snapshot of a QuantizedLinear's quantized state (for restore). */
65
- interface LayerState {
66
- w: MlxArray;
67
- scales: MlxArray;
68
- biases: MlxArray | null;
69
- spec: ops.QuantSpec;
70
- }
54
+ type LayerState = QuantizedLinearState;
71
55
 
72
56
  function captureState(layer: QuantizedLinear): LayerState {
73
57
  return { w: layer.w, scales: layer.scales, biases: layer.biases, spec: layer.spec };
74
58
  }
75
59
 
76
60
  function restoreState(layer: QuantizedLinear, state: LayerState): void {
77
- const m = layer as unknown as MutableQuantizedLinear;
61
+ const transient = layer.exchangeQuantizedState(state);
78
62
  // Dispose the transient swapped-in tensors before restoring the originals.
79
- if (m.w !== state.w) m.w.dispose();
80
- if (m.scales !== state.scales) m.scales.dispose();
81
- if (m.biases && m.biases !== state.biases) m.biases.dispose();
82
- m.w = state.w;
83
- m.scales = state.scales;
84
- m.biases = state.biases;
85
- m.spec = state.spec;
63
+ if (transient.w !== state.w) transient.w.dispose();
64
+ if (transient.scales !== state.scales) transient.scales.dispose();
65
+ if (transient.biases && transient.biases !== state.biases) transient.biases.dispose();
86
66
  }
87
67
 
88
68
  /** Quantize `bf16Weight` at `newBits`/`groupSize` and swap it into `layer`
@@ -97,15 +77,16 @@ function mutateLayerToBits(
97
77
  original: LayerState,
98
78
  ): void {
99
79
  const q = ops.quantize(bf16Weight, groupSize, newBits, "affine", cpuStream);
100
- const m = layer as unknown as MutableQuantizedLinear;
80
+ const previous = layer.exchangeQuantizedState({
81
+ w: q.packed,
82
+ scales: q.scales,
83
+ biases: q.biases,
84
+ spec: { bits: newBits, groupSize, mode: "affine" },
85
+ });
101
86
  // Free any previously swapped-in transient tensors (not the original).
102
- if (m.w !== original.w) m.w.dispose();
103
- if (m.scales !== original.scales) m.scales.dispose();
104
- if (m.biases && m.biases !== original.biases) m.biases.dispose();
105
- m.w = q.packed;
106
- m.scales = q.scales;
107
- m.biases = q.biases;
108
- m.spec = { bits: newBits, groupSize, mode: "affine" };
87
+ if (previous.w !== original.w) previous.w.dispose();
88
+ if (previous.scales !== original.scales) previous.scales.dispose();
89
+ if (previous.biases && previous.biases !== original.biases) previous.biases.dispose();
109
90
  ops.evalAll([q.packed, q.scales, q.biases]);
110
91
  }
111
92
 
@@ -193,29 +174,30 @@ export function analyzeSensitivityExact(
193
174
  ops.evalAll([bf16Source]);
194
175
 
195
176
  const sensitivities: Record<number, number> = {};
196
-
197
- for (const bits of candidateBits) {
198
- if (bits === baselineBits) {
199
- // KL is 0 by construction (re-quantizing to the baseline bit-width
200
- // reproduces the running model). Skip the forwards.
201
- sensitivities[bits] = 0.0;
202
- continue;
177
+ try {
178
+ for (const bits of candidateBits) {
179
+ if (bits === baselineBits) {
180
+ // KL is 0 by construction (re-quantizing to the baseline bit-width
181
+ // reproduces the running model). Skip the forwards.
182
+ sensitivities[bits] = 0.0;
183
+ continue;
184
+ }
185
+ mutateLayerToBits(layer, bf16Source, bits, groupSize, original);
186
+
187
+ let klSum = 0;
188
+ for (let i = 0; i < calArrays.length; i++) {
189
+ const cur = trainForward(model, calArrays[i]!);
190
+ klSum += klFromRef(cur, refLogits[i]!);
191
+ cur.dispose();
192
+ }
193
+ sensitivities[bits] = klSum / Math.max(calArrays.length, 1);
203
194
  }
204
- mutateLayerToBits(layer, bf16Source, bits, groupSize, original);
205
-
206
- let klSum = 0;
207
- for (let i = 0; i < calArrays.length; i++) {
208
- const cur = trainForward(model, calArrays[i]!);
209
- klSum += klFromRef(cur, refLogits[i]!);
210
- cur.dispose();
211
- }
212
- sensitivities[bits] = klSum / Math.max(calArrays.length, 1);
195
+ } finally {
196
+ // Restore even when quantization or a calibration forward throws.
197
+ restoreState(layer, original);
198
+ bf16Source.dispose();
213
199
  }
214
200
 
215
- // Restore the original quantized weight; release the bf16 source.
216
- restoreState(layer, original);
217
- bf16Source.dispose();
218
-
219
201
  // param count = product of the dequantized (full) weight shape.
220
202
  const fullShape = [layer.outFeatures, layer.inFeatures];
221
203
  const paramCount = fullShape.reduce((a, b) => a * b, 1);
@@ -0,0 +1,292 @@
1
+ // Quantizer-facing offline weight-transform seam. Recipe math lives in
2
+ // rotate.ts; this module owns planning, family adaptation, and tensor ownership
3
+ // so the artifact writer has one contract for every fold implementation.
4
+
5
+ import type { ModelConfig } from "../config";
6
+ import type { MlxArray } from "../mlx/array";
7
+ import type { Weights } from "../weights";
8
+ import {
9
+ foldLlamaWeights,
10
+ planQwen35Fold,
11
+ planQwenMtpFold,
12
+ QwenFoldContext,
13
+ type FoldGeometry,
14
+ type FoldOptions,
15
+ type QwenFoldOptions,
16
+ type QwenFoldPlan,
17
+ } from "./rotate";
18
+
19
+ /** Serialized alongside a quantized artifact so a folded basis is a property
20
+ * of the artifact, not an out-of-band experiment-script convention. */
21
+ export interface WeightTransformMetadata {
22
+ id: string;
23
+ seed: number;
24
+ family: string;
25
+ deviations: string[];
26
+ }
27
+
28
+ /** Pure output plan. `sourceByOutput` makes cloning/untie operations explicit:
29
+ * most outputs map to themselves; a synthesized head maps to its embedding. */
30
+ export interface WeightTransformPlan {
31
+ readonly id: string;
32
+ readonly outputNames: readonly string[];
33
+ readonly sourceByOutput: ReadonlyMap<string, string>;
34
+ readonly untieWordEmbeddings: boolean;
35
+ readonly metadata: WeightTransformMetadata;
36
+ }
37
+
38
+ /** Per-run executor. `apply` returns a lazy, owned array and never disposes
39
+ * `source`; the quantizer owns the result and the Weights object owns source. */
40
+ export interface WeightTransformContext {
41
+ apply(outputName: string, source: MlxArray): MlxArray;
42
+ dispose(): void;
43
+ }
44
+
45
+ /** Common seam for the Llama, Qwen3.5 trunk, and Qwen MTP fold recipes. */
46
+ export interface WeightTransform {
47
+ readonly id: string;
48
+ /** Name/config analysis only: no mlx arrays, model load, or device work. */
49
+ plan(names: readonly string[], config: ModelConfig): WeightTransformPlan;
50
+ createContext(weights: Weights, plan: WeightTransformPlan): WeightTransformContext;
51
+ }
52
+
53
+ export interface LlamaWeightTransformPlan extends WeightTransformPlan {
54
+ readonly kind: "llama";
55
+ readonly geometry: FoldGeometry;
56
+ readonly options: FoldOptions;
57
+ }
58
+
59
+ export interface QwenWeightTransformPlan extends WeightTransformPlan {
60
+ readonly kind: "qwen3_5" | "qwen3_5_mtp";
61
+ readonly hiddenSize: number;
62
+ readonly seed: number;
63
+ readonly fold: QwenFoldPlan;
64
+ }
65
+
66
+ function sourceMap(names: readonly string[]): Map<string, string> {
67
+ return new Map(names.map((name) => [name, name]));
68
+ }
69
+
70
+ function assertPow2(n: number, what: string): void {
71
+ if (n < 2 || (n & (n - 1)) !== 0)
72
+ throw new Error(
73
+ `rotation fold: ${what}=${n} is not a power of two — the Kronecker ` +
74
+ "Hadamard path is not implemented",
75
+ );
76
+ }
77
+
78
+ /** Pure Llama fold plan. Array work remains inside createContext/apply. */
79
+ export function planLlamaWeightTransform(
80
+ names: readonly string[],
81
+ config: ModelConfig,
82
+ options: FoldOptions,
83
+ ): LlamaWeightTransformPlan {
84
+ const has = new Set(names);
85
+ for (const required of ["model.embed_tokens.weight", "model.norm.weight"])
86
+ if (!has.has(required)) throw new Error(`rotation fold: missing tensor ${required}`);
87
+ if (has.has("lm_head.weight"))
88
+ throw new Error("rotation fold: llama adapter currently requires tied embeddings");
89
+
90
+ for (let i = 0; i < config.text.numHiddenLayers; i++) {
91
+ const layer = `model.layers.${i}`;
92
+ for (const required of [
93
+ `${layer}.input_layernorm.weight`,
94
+ `${layer}.post_attention_layernorm.weight`,
95
+ `${layer}.self_attn.q_proj.weight`,
96
+ `${layer}.self_attn.k_proj.weight`,
97
+ `${layer}.self_attn.v_proj.weight`,
98
+ `${layer}.self_attn.o_proj.weight`,
99
+ `${layer}.mlp.gate_proj.weight`,
100
+ `${layer}.mlp.up_proj.weight`,
101
+ `${layer}.mlp.down_proj.weight`,
102
+ ]) {
103
+ if (!has.has(required)) throw new Error(`rotation fold: missing tensor ${required}`);
104
+ }
105
+ }
106
+
107
+ const geometry: FoldGeometry = {
108
+ numLayers: config.text.numHiddenLayers,
109
+ hiddenSize: config.text.hiddenSize,
110
+ numHeads: config.text.numAttentionHeads,
111
+ numKvHeads: config.text.numKeyValueHeads,
112
+ headDim: config.text.headDim,
113
+ };
114
+ if (options.r1 ?? true) assertPow2(geometry.hiddenSize, "hidden_size");
115
+ if (options.r2 ?? true) assertPow2(geometry.headDim, "head_dim");
116
+
117
+ const outputNames = [...names, "lm_head.weight"];
118
+ const sourceByOutput = sourceMap(names);
119
+ sourceByOutput.set("lm_head.weight", "model.embed_tokens.weight");
120
+ return {
121
+ kind: "llama",
122
+ id: "rotation.llama",
123
+ outputNames,
124
+ sourceByOutput,
125
+ untieWordEmbeddings: true,
126
+ geometry,
127
+ options: { ...options },
128
+ metadata: {
129
+ id: "rotation.llama",
130
+ seed: options.seed,
131
+ family: "llama",
132
+ deviations: [
133
+ "no-embedding-mean-centering",
134
+ "no-R4-downproj-input-fold",
135
+ "no-R3",
136
+ "gamma-kept-in-module-as-ones (eps preserved)",
137
+ "fold-precision-f32",
138
+ ],
139
+ },
140
+ };
141
+ }
142
+
143
+ function createLlamaContext(
144
+ weights: Weights,
145
+ plan: LlamaWeightTransformPlan,
146
+ ): WeightTransformContext {
147
+ const folded = foldLlamaWeights(weights, plan.geometry, plan.options);
148
+ const pending = new Map(folded.tensors.map((tensor) => [tensor.name, tensor.array]));
149
+ return {
150
+ apply(outputName, source) {
151
+ const transformed = pending.get(outputName);
152
+ if (transformed) {
153
+ pending.delete(outputName);
154
+ return transformed;
155
+ }
156
+ // Preserve family-specific tensors outside the fold corridor.
157
+ return source.astype(source.dtype);
158
+ },
159
+ dispose() {
160
+ for (const array of pending.values()) array.dispose();
161
+ pending.clear();
162
+ },
163
+ };
164
+ }
165
+
166
+ function qwenTransformPlan(
167
+ names: readonly string[],
168
+ config: ModelConfig,
169
+ seed: number,
170
+ companion: boolean,
171
+ prefix?: string,
172
+ ): QwenWeightTransformPlan {
173
+ const fold = companion
174
+ ? planQwenMtpFold(names)
175
+ : planQwen35Fold(
176
+ names,
177
+ prefix ?? (names.includes("language_model.model.norm.weight") ? "language_model." : ""),
178
+ );
179
+ const outputNames = [...names];
180
+ const sourceByOutput = sourceMap(names);
181
+ if (fold.extraHead) {
182
+ outputNames.push(fold.extraHead.name);
183
+ sourceByOutput.set(fold.extraHead.name, fold.extraHead.from);
184
+ }
185
+ const family = companion ? "qwen3_5_mtp" : "qwen3_5";
186
+ return {
187
+ kind: family,
188
+ id: `rotation.${family}`,
189
+ outputNames,
190
+ sourceByOutput,
191
+ untieWordEmbeddings: fold.extraHead !== null,
192
+ hiddenSize: config.text.hiddenSize,
193
+ seed,
194
+ fold,
195
+ metadata: {
196
+ id: `rotation.${family}`,
197
+ seed,
198
+ family,
199
+ deviations: [...fold.deviations],
200
+ },
201
+ };
202
+ }
203
+
204
+ function createQwenContext(
205
+ weights: Weights,
206
+ plan: QwenWeightTransformPlan,
207
+ ): WeightTransformContext {
208
+ // Lazy mode composes into the quantizer's existing writer. The experiment
209
+ // script keeps QwenFoldContext's eager default because it releases shards.
210
+ const context = new QwenFoldContext(
211
+ weights,
212
+ plan.hiddenSize,
213
+ plan.seed,
214
+ plan.fold.gammaNames,
215
+ false,
216
+ );
217
+ return {
218
+ apply(outputName, source) {
219
+ const extra = plan.fold.extraHead;
220
+ if (extra && outputName === extra.name)
221
+ return context.apply({ kind: "input", gamma: extra.gamma }, source);
222
+ const op = plan.fold.ops.get(outputName);
223
+ if (!op) throw new Error(`weight transform ${plan.id}: unplanned output ${outputName}`);
224
+ return context.apply(op, source);
225
+ },
226
+ dispose: () => context.dispose(),
227
+ };
228
+ }
229
+
230
+ /** Llama-family γ+R1+R2 adapter. */
231
+ export function llamaWeightTransform(options: FoldOptions): WeightTransform {
232
+ return {
233
+ id: "rotation.llama",
234
+ plan: (names, config) => planLlamaWeightTransform(names, config, options),
235
+ createContext(weights, plan) {
236
+ if (!("kind" in plan) || plan.kind !== "llama")
237
+ throw new Error(`weight transform rotation.llama received plan ${plan.id}`);
238
+ return createLlamaContext(weights, plan as LlamaWeightTransformPlan);
239
+ },
240
+ };
241
+ }
242
+
243
+ /** Qwen3.5 trunk/VL adapter (R1 only because of the attention output gate). */
244
+ export function qwen35WeightTransform(options: QwenFoldOptions): WeightTransform {
245
+ return {
246
+ id: "rotation.qwen3_5",
247
+ plan: (names, config) =>
248
+ qwenTransformPlan(names, config, options.seed, false, options.prefix),
249
+ createContext(weights, plan) {
250
+ if (!("kind" in plan) || plan.kind !== "qwen3_5")
251
+ throw new Error(`weight transform rotation.qwen3_5 received plan ${plan.id}`);
252
+ return createQwenContext(weights, plan as QwenWeightTransformPlan);
253
+ },
254
+ };
255
+ }
256
+
257
+ /** Qwen3.5 MTP companion adapter. Must use the same seed as its trunk. */
258
+ export function qwenMtpWeightTransform(seed: number): WeightTransform {
259
+ return {
260
+ id: "rotation.qwen3_5_mtp",
261
+ plan: (names, config) => qwenTransformPlan(names, config, seed, true),
262
+ createContext(weights, plan) {
263
+ if (!("kind" in plan) || plan.kind !== "qwen3_5_mtp")
264
+ throw new Error(`weight transform rotation.qwen3_5_mtp received plan ${plan.id}`);
265
+ return createQwenContext(weights, plan as QwenWeightTransformPlan);
266
+ },
267
+ };
268
+ }
269
+
270
+ /** Select one of the three adapters from the source model and tensor schema. */
271
+ export function automaticRotationWeightTransform(
272
+ options: FoldOptions & { prefix?: string },
273
+ ): WeightTransform {
274
+ return {
275
+ id: "rotation.auto",
276
+ plan(names, config) {
277
+ if (names.includes("fc.weight") && names.includes("pre_fc_norm_hidden.weight"))
278
+ return qwenTransformPlan(names, config, options.seed, true);
279
+ if (config.modelType === "qwen3_5")
280
+ return qwenTransformPlan(names, config, options.seed, false, options.prefix);
281
+ if (config.modelType === "llama")
282
+ return planLlamaWeightTransform(names, config, options);
283
+ throw new Error(`rotation transform: unsupported model_type ${config.modelType}`);
284
+ },
285
+ createContext(weights, plan) {
286
+ if (!("kind" in plan)) throw new Error(`rotation transform: invalid plan ${plan.id}`);
287
+ return plan.kind === "llama"
288
+ ? createLlamaContext(weights, plan as LlamaWeightTransformPlan)
289
+ : createQwenContext(weights, plan as QwenWeightTransformPlan);
290
+ },
291
+ };
292
+ }
package/src/responses.ts CHANGED
@@ -17,6 +17,11 @@
17
17
  // - previous_response_id is echoed in the response when the request
18
18
  // carried one (oracle always emits null).
19
19
 
20
+ import type {
21
+ CompletionEvent,
22
+ CompletionStreamProtocol,
23
+ } from "./serve/completion-sink";
24
+
20
25
  // ---------------------------------------------------------------------
21
26
  // Response store (previous_response_id resumption)
22
27
  // ---------------------------------------------------------------------
@@ -167,6 +172,55 @@ function coerceText(content: unknown): string {
167
172
  return parts.join("");
168
173
  }
169
174
 
175
+ /** Convert a Responses message content array to chat-shaped content: text
176
+ * flattens, media TRANSLATES (`input_image` → `image_url`, incl. the
177
+ * Responses-standard string `image_url` field; video symmetrically), and
178
+ * unknown non-text parts REJECT loudly — a silent media drop answers
179
+ * confidently about content the model never saw (the anthropic.ts audio
180
+ * doctrine). Text-only arrays keep the legacy flattened-string shape. */
181
+ function coerceContent(content: unknown): string | Record<string, unknown>[] {
182
+ if (content == null) return "";
183
+ if (typeof content === "string") return content;
184
+ if (!Array.isArray(content)) return String(content);
185
+ const parts: Record<string, unknown>[] = [];
186
+ let hasMedia = false;
187
+ for (const part of content) {
188
+ if (typeof part === "string") {
189
+ parts.push({ type: "text", text: part });
190
+ continue;
191
+ }
192
+ if (!part || typeof part !== "object") continue;
193
+ const p = part as Record<string, unknown>;
194
+ if (p.type === "input_image" || p.type === "image_url") {
195
+ const url = typeof p.image_url === "string"
196
+ ? p.image_url
197
+ : (p.image_url as { url?: string } | undefined)?.url;
198
+ if (typeof url !== "string")
199
+ throw new Error("input_image part missing image_url");
200
+ parts.push({ type: "image_url", image_url: { url } });
201
+ hasMedia = true;
202
+ } else if (p.type === "input_video" || p.type === "video_url") {
203
+ const url = typeof p.video_url === "string"
204
+ ? p.video_url
205
+ : (p.video_url as { url?: string } | undefined)?.url;
206
+ if (typeof url !== "string")
207
+ throw new Error("input_video part missing video_url");
208
+ parts.push({ type: "video_url", video_url: { url } });
209
+ hasMedia = true;
210
+ } else if (p.text != null) {
211
+ parts.push({ type: "text", text: String(p.text) });
212
+ } else {
213
+ throw new Error(
214
+ `unsupported Responses content part type "${String(p.type)}" — ` +
215
+ `use input_text, input_image, or input_video`,
216
+ );
217
+ }
218
+ }
219
+ if (!hasMedia)
220
+ return parts.map((p) => String((p as { text?: unknown }).text ?? "")).join("");
221
+ return parts;
222
+ }
223
+
170
224
  /** Convert a prior response's output array into input-shaped items for
171
225
  * previous_response_id resumption (reasoning items dropped — replaying
172
226
  * them invites the model to repeat itself). */
@@ -232,11 +286,14 @@ export function responsesToChatBody(body: ResponsesRequest): Record<string, unkn
232
286
  }
233
287
  // regular message item (explicit type or implicit by having role)
234
288
  let role = String(item.role ?? "user");
235
- const content = coerceText(item.content);
236
289
  if (role === "system" || role === "developer") {
237
- if (content) systemParts.push(content);
290
+ // System prompts are text by contract — flatten (media in a system
291
+ // item has no defined semantics on any surface we mirror).
292
+ const text = coerceText(item.content);
293
+ if (text) systemParts.push(text);
238
294
  continue;
239
295
  }
296
+ const content = coerceContent(item.content);
240
297
  if (!["user", "assistant", "tool"].includes(role)) role = "user";
241
298
  messages.push({ role, content });
242
299
  }
@@ -722,6 +779,49 @@ export class ResponsesStreamTranslator {
722
779
  }
723
780
  }
724
781
 
782
+ /** Direct semantic-event adapter used by the server. `onComplete` receives
783
+ * the same response.completed payload stored for previous_response_id. */
784
+ export function createResponsesStreamProtocol(
785
+ model: string,
786
+ previousResponseId: string | null,
787
+ onComplete: (final: Record<string, unknown>) => void,
788
+ ): CompletionStreamProtocol {
789
+ const translator = new ResponsesStreamTranslator(model, previousResponseId);
790
+ const chunk = (delta: Record<string, unknown>) => ({ choices: [{ delta }] });
791
+ return {
792
+ start: () => translator.addChunk(chunk({ role: "assistant", content: "" })),
793
+ addEvents(events: CompletionEvent[]) {
794
+ return events.flatMap((event) => {
795
+ if (event.type === "reasoning") {
796
+ return translator.addChunk(chunk({ reasoning: event.text }));
797
+ }
798
+ if (event.type === "content") {
799
+ return translator.addChunk(chunk({ content: event.text }));
800
+ }
801
+ return translator.addChunk(chunk({
802
+ tool_calls: event.calls.map((call, index) => ({ index, ...call })),
803
+ }));
804
+ });
805
+ },
806
+ finish(reason, usage) {
807
+ const frames = [
808
+ ...translator.addChunk({ choices: [{ delta: {}, finish_reason: reason }], usage }),
809
+ ...translator.finalize(),
810
+ ];
811
+ onComplete(translator.finalResponse());
812
+ return frames;
813
+ },
814
+ error(message) {
815
+ return [sse("error", {
816
+ type: "error",
817
+ code: "server_error",
818
+ message,
819
+ param: null,
820
+ })];
821
+ },
822
+ };
823
+ }
824
+
725
825
  /** Wrap our OpenAI SSE byte stream as a Responses SSE byte stream.
726
826
  * onComplete fires after response.completed with the final payload so
727
827
  * the route can store it for previous_response_id resumption. */
@@ -0,0 +1,67 @@
1
+ export type RuntimeKey = `MLX_BUN_${string}`;
2
+ export type RuntimeOverrides = Readonly<Partial<Record<RuntimeKey, string | undefined>>>;
3
+
4
+ export interface RuntimeConfig {
5
+ readonly values: Readonly<Record<string, string | undefined>>;
6
+ value(name: RuntimeKey): string | undefined;
7
+ flag(name: RuntimeKey, defaultOn: boolean): boolean;
8
+ number(name: RuntimeKey, fallback: number): number;
9
+ }
10
+
11
+ /** Build one immutable runtime snapshot. Only mlx-bun-owned keys cross this
12
+ * boundary; feature code never reads or mutates process.env. */
13
+ export function createRuntimeConfig(
14
+ source: Readonly<Record<string, string | undefined>>,
15
+ ): RuntimeConfig {
16
+ const values = Object.freeze(Object.fromEntries(
17
+ Object.entries(source).filter(([key, value]) =>
18
+ key.startsWith("MLX_BUN_") && value !== undefined),
19
+ ));
20
+ return Object.freeze({
21
+ values,
22
+ value: (name: RuntimeKey) => values[name],
23
+ flag: (name: RuntimeKey, defaultOn: boolean) => {
24
+ const value = values[name];
25
+ return value === "1" ? true : value === "0" ? false : defaultOn;
26
+ },
27
+ number: (name: RuntimeKey, fallback: number) => {
28
+ const value = values[name];
29
+ if (value === undefined) return fallback;
30
+ const parsed = Number(value);
31
+ return Number.isFinite(parsed) ? parsed : fallback;
32
+ },
33
+ });
34
+ }
35
+
36
+ let active = createRuntimeConfig(process.env);
37
+
38
+ export function runtimeConfig(): RuntimeConfig {
39
+ return active;
40
+ }
41
+
42
+ export function runtimeValue(name: RuntimeKey): string | undefined {
43
+ return active.value(name);
44
+ }
45
+
46
+ export function runtimeFlag(name: RuntimeKey, defaultOn: boolean): boolean {
47
+ return active.flag(name, defaultOn);
48
+ }
49
+
50
+ export function runtimeNumber(name: RuntimeKey, fallback: number): number {
51
+ return active.number(name, fallback);
52
+ }
53
+
54
+ export function runtimeKey(name: string): RuntimeKey {
55
+ if (!name.startsWith("MLX_BUN_"))
56
+ throw new Error(`runtime flag must start with MLX_BUN_ (got ${name})`);
57
+ return name as RuntimeKey;
58
+ }
59
+
60
+ /** Replace selected values by installing a new frozen snapshot. The CLI uses
61
+ * this once while resolving flags; tests may use the returned restore hook.
62
+ * No environment mutation is involved. */
63
+ export function configureRuntime(overrides: RuntimeOverrides): () => void {
64
+ const previous = active;
65
+ active = createRuntimeConfig({ ...previous.values, ...overrides });
66
+ return () => { active = previous; };
67
+ }