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
package/src/cli.ts CHANGED
@@ -13,7 +13,8 @@ import { existsSync } from "node:fs";
13
13
  import { basename, join, resolve } from "node:path";
14
14
  import { Registry } from "./registry";
15
15
  import { loadModelConfig, parseTurboQuantScheme, type TurboQuantScheme } from "./config";
16
- import { fit, skuMatrix, thisMachine } from "./fit";
16
+ import { fit, skuMatrix, thisMachine, type FitKvScheme } from "./fit";
17
+ import { resolveKvScheme } from "./kv-scheme";
17
18
  import { EvalDB } from "./evaldb";
18
19
  import pkg from "../package.json" with { type: "json" };
19
20
  import { renderHelp } from "./tui";
@@ -27,6 +28,11 @@ import {
27
28
  planGlm52MemoryForArtifact,
28
29
  } from "./model/glm52-memory";
29
30
  import type { JobEvent } from "./jobs/types";
31
+ import {
32
+ configureRuntime,
33
+ runtimeValue,
34
+ type RuntimeKey,
35
+ } from "./runtime-config";
30
36
 
31
37
  const argv = process.argv.slice(2);
32
38
  // The appliance path: naked `mlx-bun` (or only options, e.g.
@@ -118,10 +124,10 @@ const SERVER_FLAGS = `Server options:
118
124
  --unix <path> (internal) listen on a unix socket instead of a
119
125
  TCP port — the engine half of --isolate.
120
126
  --batch <n> Max concurrent requests decoded together
121
- [default: 8]. A LONE request runs the exact
122
- serial engine (same bits, same speed); the cap
123
- engages only when concurrent requests arrive
124
- (agent fan-out). 1 pins strict serial.
127
+ [default: 8]. A lone scheduler request uses the
128
+ parity- and latency-gated B=1 fast path; concurrent
129
+ requests can form B=N (agent fan-out). 1 pins the
130
+ preserved strict serial executor.
125
131
  --decode-concurrency is accepted as the
126
132
  mlx_lm.server alias (semantics differ; see
127
133
  docs/reference/server-config.md)
@@ -152,9 +158,13 @@ Model & quality:
152
158
  Serial lane only — with --batch N a mounted draft
153
159
  routes every request serial, like mlx_lm.server.
154
160
  --draft-kind <kind> Override draft-artifact detection:
155
- two-model | assistant | dspark | deepspec | ngram
161
+ two-model | assistant | dspark | deepspec | mtp | ngram
156
162
  (deepspec = DeepSeek's released Gemma4DSparkModel
157
- drafters; ngram = MODEL-FREE prompt lookup — no
163
+ drafters; mtp = a NATIVE multi-token-prediction
164
+ head split from the target's own release, e.g.
165
+ mlx-community/Qwen3.8-27B-MTP-bf16 — auto-detected
166
+ from model_type *_mtp;
167
+ ngram = MODEL-FREE prompt lookup — no
158
168
  --draft-model, drafts copied from the request's
159
169
  own context, lossless) [default: auto]
160
170
  --num-draft-tokens <n> Drafts per verify round [default: 3; ngram: 10;
@@ -343,6 +353,8 @@ Usage: mlx-bun fit <query> [options]
343
353
 
344
354
  Options:
345
355
  --ctx <tokens> Context size to assess [default: 8192]
356
+ --kv-quant <mode> Bill the KV cache quantized: 4 | 8 | config | off
357
+ (mirrors serve --kv-quant; default bf16)
346
358
  --skus Also print the Apple silicon SKU matrix
347
359
 
348
360
  Solves weights + KV growth + prefill transient against wired memory and
@@ -576,6 +588,9 @@ knapsack per-layer bit allocation; implies quantization, no -q needed):
576
588
  --candidate-bits <l> Comma list the knapsack may pick from [default: 4,8]
577
589
  --calibration-mix <m> "optiq" or a JSONL path [default: optiq]
578
590
  --n-calibration <n> Calibration samples [default: 2]
591
+ --rotate-weights Fold the model's offline TurboQuant rotation before
592
+ quantization (auto-detects Llama/Qwen3.5/Qwen MTP)
593
+ --rotation-seed <n> Deterministic rotation seed [default: 42]
579
594
 
580
595
  Not supported (mlx_lm.convert flags we don't implement — the command
581
596
  exits with an error rather than guessing): --dtype, -d/--dequantize,
@@ -805,11 +820,11 @@ function openChatUi(url: string, hostPort: string): void {
805
820
  }
806
821
 
807
822
  /** Server/runtime flags shared by every mode that loads a model
808
- * (serve, pi). Env levers are set here so they're in place before the
823
+ * (serve, pi). Runtime overrides are installed here before the
809
824
  * generate/compiled-decode modules read them. */
810
825
  /** Resolve the decode ROUTE: a tier alias (--l1/--l2) sets the whole route,
811
826
  * and an explicit per-fork flag (--kv-quant/--fused-sdpa/…) overrides the
812
- * alias. Sets the decode env levers and returns the kv-quant mode. See
827
+ * alias. Installs decode runtime options and returns the kv-quant mode. See
813
828
  * docs/design/parity-tier-dag.md + unified-engine-frontier-plan.md.
814
829
  * Each tier is a GUARANTEE about which reference you reproduce bit-for-bit:
815
830
  * --l1 bit-for-bit IDENTICAL to mlx-lm — drop-in replacement for mlx-lm
@@ -863,8 +878,9 @@ function applyDecodeRoute(): { kvQuant?: "off" | "config" | number; turboQuant?:
863
878
  const pick = (name: string, base: boolean | undefined): boolean | null => {
864
879
  const ex = onOff(name); return ex !== null ? ex : (base ?? null); // explicit flag wins, else tier preset
865
880
  };
866
- const set = (env: string, val: boolean | null, invert = false) => {
867
- if (val !== null) process.env[env] = (invert ? !val : val) ? "1" : "0";
881
+ const set = (name: RuntimeKey, val: boolean | null, invert = false) => {
882
+ if (val !== null)
883
+ configureRuntime({ [name]: (invert ? !val : val) ? "1" : "0" });
868
884
  };
869
885
  // Explicit --kv-quant picks the composition its ORACLE uses: config (the
870
886
  // optiq mixed scheme) runs optiq's fused SDPA (the L2 golden composition);
@@ -874,7 +890,7 @@ function applyDecodeRoute(): { kvQuant?: "off" | "config" | number; turboQuant?:
874
890
  set("MLX_BUN_COMPILED_DECODE", pick("compiled-decode", p.compiled));
875
891
  set("MLX_BUN_NO_FUSED_SDPA", pick("fused-sdpa", fusedSdpaBase), true); // inverted env
876
892
  // Compiled activations (the faithful geglu/swiglu — mlx-lm's @mx.compile kernel).
877
- // One fork drives both env vars; qwen3/qwen3.5/universal compile unconditionally,
893
+ // One fork drives both runtime options; qwen3/qwen3.5/universal compile unconditionally,
878
894
  // so this toggles gemma geglu + minicpm5 swiglu (the only unfused-capable sites).
879
895
  set("MLX_BUN_COMPILED_GEGLU", pick("compiled-activations", p.compiledAct));
880
896
  set("MLX_BUN_COMPILED_SWIGLU", pick("compiled-activations", p.compiledAct));
@@ -909,11 +925,12 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
909
925
  process.exit(1);
910
926
  };
911
927
  const route = applyDecodeRoute(); // --l1/--l2 tier alias, with per-fork flags overriding
912
- if (flag("force-wire")) process.env.MLX_BUN_FORCE_WIRE = "1";
928
+ if (flag("force-wire")) configureRuntime({ MLX_BUN_FORCE_WIRE: "1" });
913
929
  // Remote image_url/audio_url media fetches block private/loopback/link-
914
930
  // local destinations by default (SSRF — src/media-fetch.ts); this is the
915
931
  // LAN-hosts escape hatch.
916
- if (flag("allow-private-media")) process.env.MLX_BUN_ALLOW_PRIVATE_MEDIA = "1";
932
+ if (flag("allow-private-media"))
933
+ configureRuntime({ MLX_BUN_ALLOW_PRIVATE_MEDIA: "1" });
917
934
 
918
935
  const serverOptions: import("./server").ServerOptions = {};
919
936
  const budgetGB = Number(opt("memory-budget", "0"));
@@ -945,7 +962,7 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
945
962
  console.warn("--ssd-cache-verify has no effect without --ssd-cache — ignored");
946
963
  }
947
964
  // --batch N: max concurrent requests batched through the mlx-lm-parity
948
- // engine (N=1 = today's serial path). --decode-concurrency is accepted for
965
+ // engine (N=1 pins the strict serial executor). --decode-concurrency is accepted for
949
966
  // drop-in compatibility with mlx_lm.server, but the semantics differ: there
950
967
  // it caps per-BatchGenerator decode parallelism (default 32); in mlx-bun it
951
968
  // caps continuous batching (default 8; `--batch 1` pins the serial path —
@@ -965,7 +982,7 @@ function serverRuntimeFlags(): { port: number; serverOptions: import("./server")
965
982
  // storage, default off (docs/design/paged-kv-cache.md). Serial-only in v1:
966
983
  // with no explicit --batch, pin --batch 1 (the default is 8); an explicit
967
984
  // --batch N>1 is refused loudly by createServer rather than downgraded.
968
- if (flag("paged-kv") || process.env.MLX_BUN_PAGED_KV === "1") {
985
+ if (flag("paged-kv") || runtimeValue("MLX_BUN_PAGED_KV") === "1") {
969
986
  const bsRaw = opt("paged-kv-block-size");
970
987
  serverOptions.pagedKv = bsRaw !== null ? { blockSize: Number(bsRaw) } : {};
971
988
  if (batchRaw === null) {
@@ -1086,7 +1103,7 @@ async function mountStartupAdapter(
1086
1103
  function runtimeSummary(o: import("./server").ServerOptions): string {
1087
1104
  const kv = o.turboQuant ? `turbo-k${o.turboQuant.kBits}v${o.turboQuant.vBits}`
1088
1105
  : o.kvQuant === "off" ? "off" : typeof o.kvQuant === "number" ? `kv${o.kvQuant}` : "config";
1089
- const lever = (env: string, dflt: string) => process.env[env] ?? dflt;
1106
+ const lever = (name: RuntimeKey, dflt: string) => runtimeValue(name) ?? dflt;
1090
1107
  return `kv-quant ${kv} · compiled-decode ${lever("MLX_BUN_COMPILED_DECODE", "1") === "1" ? "on" : "off"}` +
1091
1108
  (lever("MLX_BUN_COMPILED_GEGLU", "1") === "0" ? " · compiled-activations off" : "") +
1092
1109
  (o.batch && o.batch > 1 ? ` · batch ${o.batch}` : "") +
@@ -1376,7 +1393,7 @@ switch (cmd) {
1376
1393
 
1377
1394
  case "fit": {
1378
1395
  const query = positional(0);
1379
- if (!query) { console.error("usage: mlx-bun fit <query> [--ctx N] [--skus]"); process.exit(1); }
1396
+ if (!query) { console.error("usage: mlx-bun fit <query> [--ctx N] [--kv-quant 4|8|config] [--skus]"); process.exit(1); }
1380
1397
  const reg = new Registry();
1381
1398
  if (reg.list().length === 0) await reg.scan();
1382
1399
  const m = reg.resolve(query);
@@ -1443,9 +1460,32 @@ switch (cmd) {
1443
1460
  console.log();
1444
1461
  break;
1445
1462
  }
1446
- const r = fit(config, m.sizeBytes, ctx, thisMachine(), undefined, m.expertsBytes);
1463
+ // --kv-quant mirrors serve: bill the quantized cache's true bytes so
1464
+ // the reported window matches what serving with the same flag admits.
1465
+ const kvQuantOpt = opt("kv-quant");
1466
+ let fitKvScheme: FitKvScheme | undefined;
1467
+ if (kvQuantOpt === "4" || kvQuantOpt === "8") {
1468
+ fitKvScheme = resolveKvScheme({ override: Number(kvQuantOpt) }).fitOptions;
1469
+ } else if (kvQuantOpt === "config") {
1470
+ if (!config.kvQuant?.length) {
1471
+ console.error(`${m.repoId} ships no per-layer kv-quant config — use --kv-quant 4|8`);
1472
+ process.exit(1);
1473
+ }
1474
+ fitKvScheme = resolveKvScheme({
1475
+ override: "config",
1476
+ config: config.kvQuant,
1477
+ missingConfig: "error",
1478
+ }).fitOptions;
1479
+ } else if (kvQuantOpt != null && kvQuantOpt !== "off") {
1480
+ console.error(`--kv-quant ${kvQuantOpt}: expected 4, 8, config, or off`);
1481
+ process.exit(1);
1482
+ }
1483
+ const r = fit(config, m.sizeBytes, ctx, thisMachine(), undefined, m.expertsBytes, undefined, fitKvScheme);
1447
1484
  h1("will it fit?");
1448
- console.log(` ${style.bold(m.repoId)} ${style.dim(`@ ${ctx.toLocaleString()} context · this machine`)}`);
1485
+ const kvNote = fitKvScheme
1486
+ ? style.dim(` · kv-quant ${fitKvScheme.kvBits ? `${fitKvScheme.kvBits}-bit` : "config"}`)
1487
+ : "";
1488
+ console.log(` ${style.bold(m.repoId)} ${style.dim(`@ ${ctx.toLocaleString()} context · this machine`)}${kvNote}`);
1449
1489
  console.log();
1450
1490
  const expertsNote = config.text.enableMoeBlock && m.expertsBytes > 0
1451
1491
  ? style.dim(` (experts ${gb(m.expertsBytes)}; top ${config.text.topKExperts}/${config.text.numExperts} read per token)`)
@@ -1470,7 +1510,7 @@ switch (cmd) {
1470
1510
  { header: "fits", paint: (c) => (c.includes("fits") ? style.green(c) : style.dim(c)) },
1471
1511
  { header: "max context", align: "right" }, { header: "decode", align: "right" },
1472
1512
  ],
1473
- skuMatrix(config, m.sizeBytes, ctx, m.expertsBytes).map((row) => [
1513
+ skuMatrix(config, m.sizeBytes, ctx, m.expertsBytes, fitKvScheme).map((row) => [
1474
1514
  row.sku, `${row.ramGB} GB`, row.fits ? "fits" : "—",
1475
1515
  row.fits ? row.maxContext.toLocaleString() : "—",
1476
1516
  row.fits ? `~${row.decodeTps.toFixed(0)} tok/s` : "—",
@@ -1573,10 +1613,29 @@ switch (cmd) {
1573
1613
  const config = await loadModelConfig(m.path);
1574
1614
  const report = fit(config, m.sizeBytes, 8192, undefined, undefined, m.expertsBytes);
1575
1615
  sFit.done(`${style.bold(m.repoId)} ${style.dim(`· ${gb(m.sizeBytes)} · ~${report.predictedDecodeTps.toFixed(0)} tok/s predicted`)}${picked ? style.dim(" · auto-picked (override: mlx-bun serve <query>)") : ""}`);
1616
+ // Near-ceiling advisory (admission doctrine: advise, never refuse). A
1617
+ // model near the DEFAULT Metal wired ceiling (~75% of RAM) dies with an
1618
+ // UNCATCHABLE async-GPU-OOM on long prefills (M4 24 GB × 17 GB model,
1619
+ // 2026-08-20) — surface the standard remedy up front instead.
1620
+ {
1621
+ const { maxRecommendedWorkingSetSize } = await import("./mlx/ffi");
1622
+ const ceiling = maxRecommendedWorkingSetSize();
1623
+ const sysctlRaw = Bun.spawnSync(["sysctl", "-n", "iogpu.wired_limit_mb"]).stdout.toString().trim();
1624
+ const limitIsDefault = sysctlRaw === "0" || sysctlRaw === "";
1625
+ if (limitIsDefault && ceiling > 0 && m.sizeBytes > 0.8 * ceiling) {
1626
+ const ramGb = Number(Bun.spawnSync(["sysctl", "-n", "hw.memsize"]).stdout.toString().trim()) / 2 ** 30;
1627
+ const suggestMb = Math.floor((ramGb - 2.5) * 1024);
1628
+ console.error(style.dim(
1629
+ ` ! ${gb(m.sizeBytes)} model vs ~${gb(ceiling)} default GPU ceiling — long prompts can\n` +
1630
+ ` hit an uncatchable Metal OOM. If that happens, raise the limit first:\n` +
1631
+ ` sudo sysctl iogpu.wired_limit_mb=${suggestMb} (resets on reboot)`,
1632
+ ));
1633
+ }
1634
+ }
1576
1635
  const sNative = step("native runtime");
1577
1636
  await ensureNative(sNative);
1578
1637
  sNative.done("native runtime ready");
1579
- const { createServer, loadContext } = await import("./server");
1638
+ const { createServer, loadContext, shutdownServer } = await import("./server");
1580
1639
  if (flag("expert-offload")) {
1581
1640
  if (m.expertsBytes === 0) {
1582
1641
  console.error(style.dim("--expert-offload ignored: this model has no experts (dense)"));
@@ -1609,9 +1668,9 @@ switch (cmd) {
1609
1668
  }
1610
1669
  const draftKindRaw = opt("draft-kind");
1611
1670
  const draftKind = (draftKindRaw ?? undefined) as
1612
- | "dspark" | "deepspec" | "assistant" | "two-model" | "ngram" | undefined;
1613
- if (draftKind !== undefined && !["dspark", "deepspec", "assistant", "two-model", "ngram"].includes(draftKind)) {
1614
- console.error(`--draft-kind expects two-model|assistant|dspark|deepspec|ngram (got "${draftKindRaw}")`);
1671
+ | "dspark" | "deepspec" | "assistant" | "two-model" | "ngram" | "mtp" | undefined;
1672
+ if (draftKind !== undefined && !["dspark", "deepspec", "assistant", "two-model", "ngram", "mtp"].includes(draftKind)) {
1673
+ console.error(`--draft-kind expects two-model|assistant|dspark|deepspec|mtp|ngram (got "${draftKindRaw}")`);
1615
1674
  process.exit(1);
1616
1675
  }
1617
1676
  // Prompt-lookup window bounds (ngram kind only; validated here so a typo
@@ -1639,7 +1698,10 @@ switch (cmd) {
1639
1698
  const ctx = await loadContext(m.path, m.repoId, {
1640
1699
  memoryBudgetBytes: rt.serverOptions.memoryBudgetBytes,
1641
1700
  glm: glm52RuntimeFlags(rt.serverOptions),
1642
- ...(draftModelDir || draftKind === "ngram"
1701
+ // ngram is model-free; mtp resolves the bundled <model>/mtp/ companion
1702
+ // in loadContext when no --draft-model is given (cli.md contract —
1703
+ // this gate silently dropped that case until 2026-08-20).
1704
+ ...(draftModelDir || draftKind === "ngram" || draftKind === "mtp"
1643
1705
  ? {
1644
1706
  ...(draftModelDir ? { draftModelDir } : {}),
1645
1707
  numDraftTokens,
@@ -1652,6 +1714,30 @@ switch (cmd) {
1652
1714
  sLoad.done(`weights loaded ${style.dim(`in ${(performance.now() - t0).toFixed(0)} ms`)}${draftModelDir ? style.dim(` · draft: ${draftModelDir.split("/").filter(Boolean).at(-1)}`) : draftKind === "ngram" ? style.dim(" · draft: ngram (prompt lookup)") : ""}`);
1653
1715
  await mountStartupAdapter(ctx, rt.serverOptions);
1654
1716
  const server = createServer(ctx, rt.port, { ...rt.serverOptions, owner: "serve" });
1717
+ let shutdownStarted = false;
1718
+ const onServeSignal = (signal: "SIGINT" | "SIGTERM"): void => {
1719
+ if (shutdownStarted) return;
1720
+ shutdownStarted = true;
1721
+ const rawTimeout = Number(runtimeValue("MLX_BUN_SHUTDOWN_TIMEOUT_MS"));
1722
+ const timeoutMs = Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : 120_000;
1723
+ console.log(`\n[server] ${signal}: flushing prompt-cache durability`);
1724
+ void shutdownServer(server, timeoutMs).then((result) => {
1725
+ if (!result.durability.durable) {
1726
+ const d = result.durability;
1727
+ console.warn(
1728
+ `[server] cache flush incomplete after ${Math.round(d.elapsedMs)} ms: ` +
1729
+ `${d.pendingSnapshots} snapshots, ${d.pendingSpills} spills, ` +
1730
+ `${d.droppedSpills} dropped, ${d.failedSpills} failed`,
1731
+ );
1732
+ }
1733
+ process.exit(0);
1734
+ }).catch((error) => {
1735
+ console.warn(`[server] shutdown flush failed: ${(error as Error).message}`);
1736
+ process.exit(0);
1737
+ });
1738
+ };
1739
+ process.once("SIGINT", () => onServeSignal("SIGINT"));
1740
+ process.once("SIGTERM", () => onServeSignal("SIGTERM"));
1655
1741
  const shownHost = rt.serverOptions.hostname ?? "localhost";
1656
1742
  console.log();
1657
1743
  box([
@@ -1682,7 +1768,7 @@ switch (cmd) {
1682
1768
  // three entry points (raw / OpenAI API / chat UI) differ only in how params
1683
1769
  // are POPULATED; here every param is explicit. Decode-path levers mirror
1684
1770
  // serve so you can pin the route (mlx-lm compat = --l1).
1685
- // --l1/--l2 + per-fork overrides set the decode env levers; the
1771
+ // --l1/--l2 + per-fork overrides set the decode runtime options; the
1686
1772
  // returned KV scheme is applied below so `generate --l2` runs the same
1687
1773
  // quantized-KV route (and produces the same tokens) as `serve --l2`.
1688
1774
  const route = applyDecodeRoute();
@@ -1706,13 +1792,12 @@ switch (cmd) {
1706
1792
  // bf16, same silent fallback as serve), N = uniform bits from decode
1707
1793
  // start. Unset (no tier, no --kv-quant) keeps generate's historical
1708
1794
  // default: bf16 bit-exact greedy (generateText's parity path).
1709
- const kvScheme =
1710
- route.turboQuant ? { turboQuant: route.turboQuant, quantizedKvStart: 0 }
1711
- : route.kvQuant === "off" ? {}
1712
- : route.kvQuant === "config"
1713
- ? (tm.config.kvQuant?.length ? { kvConfig: tm.config.kvQuant } : {})
1714
- : typeof route.kvQuant === "number"
1715
- ? { kvBits: route.kvQuant, quantizedKvStart: 0 }
1795
+ const kvScheme = route.turboQuant || route.kvQuant !== undefined
1796
+ ? resolveKvScheme({
1797
+ override: route.kvQuant,
1798
+ turboQuant: route.turboQuant,
1799
+ config: tm.config.kvQuant,
1800
+ }).generationOptions
1716
1801
  : undefined;
1717
1802
  const num = (n: string): number | undefined => { const v = opt(n); return v == null ? undefined : Number(v); };
1718
1803
  const text = await generateText(tm, prompt, {
@@ -1794,7 +1879,7 @@ switch (cmd) {
1794
1879
  case "benchmark": {
1795
1880
  const { banner, step, box, style } = await import("./tui");
1796
1881
  banner(pkg.version);
1797
- serverRuntimeFlags(); // env levers (--compiled-decode, --fused-sdpa, ...) apply to the run
1882
+ serverRuntimeFlags(); // decode options (--compiled-decode, --fused-sdpa, ...) apply to the run
1798
1883
  const { m, picked } = await resolveModelAuto(opt("model") ?? positional(0) ?? opt("query"));
1799
1884
  const tokens = Number(opt("tokens", "256"));
1800
1885
  const runs = Number(opt("runs", "3"));
@@ -2446,6 +2531,12 @@ switch (cmd) {
2446
2531
  console.error(`--candidate-bits expects a comma list of integers in [2, 8] (got "${opt("candidate-bits")}")`);
2447
2532
  process.exit(1);
2448
2533
  }
2534
+ const rotateWeights = flag("rotate-weights");
2535
+ const rotationSeed = Number(opt("rotation-seed", "42"));
2536
+ if (!Number.isInteger(rotationSeed)) {
2537
+ console.error(`--rotation-seed expects an integer (got "${opt("rotation-seed")}")`);
2538
+ process.exit(1);
2539
+ }
2449
2540
  const mlxPath = opt("mlx-path", "mlx_model")!;
2450
2541
  const { existsSync } = await import("node:fs");
2451
2542
  if (existsSync(mlxPath)) {
@@ -2487,7 +2578,7 @@ switch (cmd) {
2487
2578
  ? `quantizing (mixed, target ${targetBpw} bpw — sensitivity sweep, ~minutes)`
2488
2579
  : `quantizing (${qBits}-bit, group ${qGroup})`,
2489
2580
  );
2490
- const { quantizeModelDir } = await import("./quantize");
2581
+ const { quantizeModelDir, automaticRotationWeightTransform } = await import("./quantize");
2491
2582
  try {
2492
2583
  const r = await quantizeModelDir(
2493
2584
  srcDir,
@@ -2500,6 +2591,9 @@ switch (cmd) {
2500
2591
  ...(candidateBits ? { candidateBits } : {}),
2501
2592
  ...(opt("calibration-mix") ? { calibrationMix: opt("calibration-mix")! } : {}),
2502
2593
  ...(opt("n-calibration") ? { nCalibration: Number(opt("n-calibration")) } : {}),
2594
+ ...(rotateWeights
2595
+ ? { weightTransform: automaticRotationWeightTransform({ seed: rotationSeed }) }
2596
+ : {}),
2503
2597
  },
2504
2598
  (e) => sQ.update(e.message),
2505
2599
  );
@@ -2511,6 +2605,7 @@ switch (cmd) {
2511
2605
  `source ${style.dim(srcDir)}`,
2512
2606
  `model ${style.bold(r.outDir)} ${style.dim(`· ${gb(r.write.totalSize)}`)}`,
2513
2607
  `quant ${style.dim(targetBpw !== undefined ? `mixed ${r.achievedBpw.toFixed(2)} bpw (target ${targetBpw})` : `${qBits}-bit g${qGroup} affine`)}`,
2608
+ ...(rotateWeights ? [`transform ${style.dim(`TurboQuant rotation seed ${rotationSeed}`)}`] : []),
2514
2609
  "",
2515
2610
  `serve it ${style.accent(`mlx-bun serve ${r.outDir}`)}`,
2516
2611
  ]);
package/src/eval/kl.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  //
10
10
  // Two reference modes:
11
11
  // * self-flag (DEFAULT, the kernel-drift gate): one model, one weight
12
- // load; forward each prompt twice with an env lever set to its
12
+ // load; forward each prompt twice with a runtime lever set to its
13
13
  // reference value vs its candidate value. Directly measures the drift
14
14
  // a perf lever introduces. Fits in RAM trivially (no second model).
15
15
  // * two-model (optiq-style absolute quality): a separate reference
@@ -30,6 +30,10 @@ import { loadModelConfig, type ModelConfig, type TurboQuantScheme } from "../con
30
30
  import { createModel, type RuntimeModel } from "../model/factory";
31
31
  import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
32
32
  import { maybeQuantizeKv } from "../generate";
33
+ import {
34
+ configureRuntime,
35
+ runtimeKey,
36
+ } from "../runtime-config";
33
37
 
34
38
  export interface KLResult {
35
39
  nPrompts: number;
@@ -135,23 +139,28 @@ export async function evaluateKlSelfFlag(opts: {
135
139
  const { model, tokenizer } = await loadRunnable(opts.candidate);
136
140
  const tokenized = prepPrompts(opts.prompts, tokenizer, seqLen, n);
137
141
 
138
- const prev = process.env[opts.flag];
142
+ const key = runtimeKey(opts.flag);
139
143
  const perPrompt: Float32Array[] = [];
140
- try {
141
- for (const ids of tokenized) {
142
- process.env[opts.flag] = opts.refValue;
143
- const p = forwardLogits(model, ids);
144
- process.env[opts.flag] = opts.candValue;
145
- const q = forwardLogits(model, ids);
146
- const T = Math.min(p.shape[1]!, q.shape[1]!);
147
- const pT = p.slice([0, 0, 0], [1, T, p.shape[2]!]);
148
- const qT = q.slice([0, 0, 0], [1, T, q.shape[2]!]);
149
- perPrompt.push(klPerToken(pT, qT));
150
- for (const a of [p, q, pT, qT]) a.dispose();
144
+ for (const ids of tokenized) {
145
+ let p: MlxArray;
146
+ const restoreRef = configureRuntime({ [key]: opts.refValue });
147
+ try {
148
+ p = forwardLogits(model, ids);
149
+ } finally {
150
+ restoreRef();
151
151
  }
152
- } finally {
153
- if (prev === undefined) delete process.env[opts.flag];
154
- else process.env[opts.flag] = prev;
152
+ let q: MlxArray;
153
+ const restoreCandidate = configureRuntime({ [key]: opts.candValue });
154
+ try {
155
+ q = forwardLogits(model, ids);
156
+ } finally {
157
+ restoreCandidate();
158
+ }
159
+ const T = Math.min(p.shape[1]!, q.shape[1]!);
160
+ const pT = p.slice([0, 0, 0], [1, T, p.shape[2]!]);
161
+ const qT = q.slice([0, 0, 0], [1, T, q.shape[2]!]);
162
+ perPrompt.push(klPerToken(pT, qT));
163
+ for (const a of [p, q, pT, qT]) a.dispose();
155
164
  }
156
165
 
157
166
  const agg = aggregate(perPrompt);
@@ -232,8 +241,7 @@ function decodeArm(
232
241
  flag: string, value: string,
233
242
  kvOverride?: Parameters<typeof maybeQuantizeKv>[1],
234
243
  ): Float32Array[] {
235
- const prev = process.env[flag];
236
- process.env[flag] = value;
244
+ const restore = configureRuntime({ [runtimeKey(flag)]: value });
237
245
  try {
238
246
  const cache = model.makeCache();
239
247
  try {
@@ -256,8 +264,7 @@ function decodeArm(
256
264
  for (const c of cache) c.dispose();
257
265
  }
258
266
  } finally {
259
- if (prev === undefined) delete process.env[flag];
260
- else process.env[flag] = prev;
267
+ restore();
261
268
  }
262
269
  }
263
270
 
@@ -6,9 +6,11 @@
6
6
  import { homedir } from "node:os";
7
7
  import { readFileSync } from "node:fs";
8
8
  import { generate, type GenerateOptions } from "../generate";
9
+ import { clearCache } from "../mlx/ffi";
9
10
  import * as ops from "../mlx/ops";
10
11
  import { ChatTemplate } from "../chat-template";
11
12
  import { loadModelConfig, type ModelConfig } from "../config";
13
+ import { runtimeValue } from "../runtime-config";
12
14
  import {
13
15
  openModel,
14
16
  type Glm52RuntimeOpenOptions,
@@ -19,7 +21,7 @@ import { DiffusionGemmaModel } from "../model/diffusion-gemma";
19
21
  import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
20
22
  import { resolveModelDir } from "./kl";
21
23
 
22
- export const EVAL_DATA_DIR = process.env.MLX_BUN_EVAL_DATA ?? `${homedir()}/.cache/mlx-bun/eval-data`;
24
+ export const EVAL_DATA_DIR = runtimeValue("MLX_BUN_EVAL_DATA") ?? `${homedir()}/.cache/mlx-bun/eval-data`;
23
25
 
24
26
  /** Read a jsonl dataset exported by scripts/eval/export-datasets.py. */
25
27
  export function loadJsonl<T = Record<string, unknown>>(name: string): T[] {
@@ -124,7 +126,23 @@ export function greedyDecodeBitExact(tm: TaskModel, ids: number[], maxTokens: nu
124
126
  if (tm.activeAdapters) tm.model.loraState.active = tm.activeAdapters; // apply the mounted adapter
125
127
  const cache = tm.model.makeCache();
126
128
  try {
127
- if (ids.length > 1) tm.model.forward(ids.slice(0, -1), cache).dispose(); // prefill
129
+ // Chunked, LM-head-free prefill. The old single-shot `forward(prompt)`
130
+ // materialized [1, L, vocab] logits just to discard them — at a 248k
131
+ // vocab that's ~1 GB per 1k prompt tokens, and with no allocator-cache
132
+ // clears the Metal buffer cache retained every giant freed tensor
133
+ // (the 27B capability-suite 73 GB swap-thrash). Prefill only needs the
134
+ // cache state, which forwardHidden advances identically (the LM head
135
+ // never touches the cache) — bit-exact by construction. Chunk size and
136
+ // clear cadence mirror mlx-lm generate_step / our generate().
137
+ const PREFILL_CHUNK = 2048;
138
+ const prompt = ids.slice(0, -1);
139
+ for (let s = 0; s < prompt.length; s += PREFILL_CHUNK) {
140
+ const chunk = prompt.slice(s, s + PREFILL_CHUNK);
141
+ const cIds = ops.fromInt32(chunk, [1, chunk.length]);
142
+ tm.model.forwardHidden(cIds, cache).dispose();
143
+ cIds.dispose();
144
+ clearCache();
145
+ }
128
146
  let last = ids[ids.length - 1]!;
129
147
  const eos = new Set(tm.config.eosTokenIds);
130
148
  const out: number[] = [];
@@ -134,6 +152,7 @@ export function greedyDecodeBitExact(tm: TaskModel, ids: number[], maxTokens: nu
134
152
  lg.dispose();
135
153
  const best = ops.itemUint32(am);
136
154
  am.dispose();
155
+ if (i % 256 === 0) clearCache(); // mlx-lm decode cadence
137
156
  if (eos.has(best)) break; // mlx-lm halts on EOS and excludes it from the output
138
157
  out.push(best);
139
158
  last = best;
@@ -148,7 +167,7 @@ export function greedyDecodeBitExact(tm: TaskModel, ids: number[], maxTokens: nu
148
167
  * pass `opts.sampler` to drive temperature/HLG/etc. */
149
168
  export async function generateText(tm: TaskModel, body: string, opts: GenOpts = {}): Promise<string> {
150
169
  const maxTokens = opts.maxTokens ?? 256;
151
- const enableThinking = opts.enableThinking ?? process.env.MLX_BUN_EVAL_THINK === "1";
170
+ const enableThinking = opts.enableThinking ?? runtimeValue("MLX_BUN_EVAL_THINK") === "1";
152
171
  const templated = opts.useChat !== false && tm.template !== null;
153
172
  const text = templated
154
173
  ? tm.template!.render([{ role: "user", content: body }], { addGenerationPrompt: true, enableThinking })
@@ -165,7 +184,7 @@ export async function generateText(tm: TaskModel, body: string, opts: GenOpts =
165
184
  // MLX_BUN_EVAL_KV_QUANT=1 generates through the model's quantized KV (serving
166
185
  // config); an explicit opts.kvScheme (cli generate's resolved tier route) wins.
167
186
  const envKv: GenOpts["kvScheme"] | undefined =
168
- (process.env.MLX_BUN_EVAL_KV_QUANT === "1" && tm.config.kvQuant?.length)
187
+ (runtimeValue("MLX_BUN_EVAL_KV_QUANT") === "1" && tm.config.kvQuant?.length)
169
188
  ? { kvConfig: tm.config.kvQuant, quantizedKvStart: 0 } : undefined;
170
189
  const kvScheme = opts.kvScheme ?? envKv;
171
190
  const kvActive = !!(kvScheme && (kvScheme.kvBits || kvScheme.kvConfig?.length || kvScheme.turboQuant));
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
18
18
  import { type ToolDefinition } from "../../chat-template";
19
+ import { runtimeValue } from "../../runtime-config";
19
20
 
20
21
  // ---------------------------------------------------------------------------
21
22
  // Data shapes (the merged jsonl: each line = {query, answer}).
@@ -465,7 +466,7 @@ export async function evaluateBfcl(
465
466
 
466
467
  // Optiq-parity mode (DEFAULT): optiq's EXACT 200 BFCL-simple questions, scored
467
468
  // through OUR pipeline. MLX_BUN_BFCL_FROZEN=0 reverts to our copy + sampling.
468
- const useFrozen = opts.frozen ?? (process.env.MLX_BUN_BFCL_FROZEN !== "0");
469
+ const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_BFCL_FROZEN") !== "0");
469
470
  let items: BfclItem[];
470
471
  if (useFrozen) {
471
472
  items = loadJsonl<BfclFrozenRow>("bfcl_optiq_frozen").map((r) => ({
@@ -2,6 +2,7 @@
2
2
  // 3-shot CoT, greedy decode, extract the numeric answer, compare.
3
3
 
4
4
  import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
5
+ import { runtimeValue } from "../../runtime-config";
5
6
 
6
7
  // 3-shot exemplars (verbatim from gsm8k.py).
7
8
  const FEW_SHOT: { question: string; answer: string }[] = [
@@ -73,7 +74,7 @@ export async function evaluateGsm8k(
73
74
  // his published GSM8K used) but through OUR OWN pipeline — our few-shot builder,
74
75
  // our chat template, our scorer. If our number reproduces his, our runtime is
75
76
  // faithful. MLX_BUN_GSM8K_FROZEN=0 falls back to our own sampling of the full set.
76
- const useFrozen = opts.frozen ?? (process.env.MLX_BUN_GSM8K_FROZEN !== "0");
77
+ const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_GSM8K_FROZEN") !== "0");
77
78
  let rows: Gsm8kRow[];
78
79
  if (useFrozen) {
79
80
  rows = loadJsonl<Gsm8kRow>("gsm8k_optiq_frozen"); // optiq's exact draw; we use {question, answer}
@@ -10,6 +10,7 @@
10
10
  // fused-attention kernel (opportunity A) has the most to prove.
11
11
 
12
12
  import { generateText, loadJsonl, type TaskModel } from "../runner";
13
+ import { runtimeValue } from "../../runtime-config";
13
14
 
14
15
  const INSTR =
15
16
  "You will be given a list of hash assignments and a starting hash. " +
@@ -96,7 +97,7 @@ export async function evaluateHashhop(
96
97
  // RECONSTRUCTION producing DIFFERENT problems, so it cannot reproduce optiq's
97
98
  // number — the frozen problems are the only way to match. MLX_BUN_HASHHOP_FROZEN=0
98
99
  // uses our generator.
99
- const useFrozen = opts.frozen ?? (process.env.MLX_BUN_HASHHOP_FROZEN !== "0");
100
+ const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_HASHHOP_FROZEN") !== "0");
100
101
  if (useFrozen) {
101
102
  const rows = loadJsonl<HashhopFrozen>("hashhop_optiq_frozen");
102
103
  const hopCorrect: Record<number, number> = {};
@@ -16,11 +16,12 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
16
16
  import { tmpdir } from "node:os";
17
17
  import { join } from "node:path";
18
18
  import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
19
+ import { runtimeValue } from "../../runtime-config";
19
20
 
20
21
  // Oracle venv python — the same interpreter the Python reference uses.
21
22
  // Machine-specific: overridable via MLX_BUN_ORACLE_PYTHON (the committed default is
22
23
  // the reference box; other laptops set the env, never re-commit the path).
23
- const ORACLE_PYTHON = process.env.MLX_BUN_ORACLE_PYTHON ?? "/Users/joshrossi/Code/mlx-lm/.venv/bin/python";
24
+ const ORACLE_PYTHON = runtimeValue("MLX_BUN_ORACLE_PYTHON") ?? "/Users/joshrossi/Code/mlx-lm/.venv/bin/python";
24
25
 
25
26
  interface HumanevalRow {
26
27
  task_id: string;
@@ -185,7 +186,7 @@ function runProgram(program: string, timeoutSec: number, memoryMb: number): RunR
185
186
  env,
186
187
  timeout: timeoutSec * 1000, // hard wall-clock kill
187
188
  killSignal: "SIGKILL",
188
- stdio: ["ignore", "ignore", process.env.MLX_BUN_EVAL_DEBUG === "1" ? "pipe" : "ignore"],
189
+ stdio: ["ignore", "ignore", runtimeValue("MLX_BUN_EVAL_DEBUG") === "1" ? "pipe" : "ignore"],
189
190
  encoding: "utf8",
190
191
  });
191
192
 
@@ -219,7 +220,7 @@ export async function evaluateHumaneval(
219
220
  // Optiq-parity mode (DEFAULT): optiq's EXACT 164-problem set (he runs the full
220
221
  // HumanEval test split), scored through OUR pipeline. MLX_BUN_HUMANEVAL_FROZEN=0
221
222
  // reverts to our own copy + optional sampling.
222
- const useFrozen = opts.frozen ?? (process.env.MLX_BUN_HUMANEVAL_FROZEN !== "0");
223
+ const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_HUMANEVAL_FROZEN") !== "0");
223
224
  const rows = loadJsonl<HumanevalRow>(useFrozen ? "humaneval_optiq_frozen" : "humaneval");
224
225
  const idx = useFrozen
225
226
  ? Array.from({ length: rows.length }, (_, i) => i)
@@ -252,7 +253,7 @@ export async function evaluateHumaneval(
252
253
 
253
254
  const rr = runProgram(program, timeoutSec, memoryMb);
254
255
  if (rr.ok) nPass++;
255
- else if (process.env.MLX_BUN_EVAL_DEBUG === "1") {
256
+ else if (runtimeValue("MLX_BUN_EVAL_DEBUG") === "1") {
256
257
  const err = rr.stderr ?? "";
257
258
  const reason = rr.timedOut ? "TIMEOUT"
258
259
  : completion.trim() === "" ? "EMPTY"
@@ -21,6 +21,7 @@
21
21
  // default in both implementations, so behavior is identical.
22
22
 
23
23
  import { generateText, loadJsonl, sampleIndices, type TaskModel } from "../runner";
24
+ import { runtimeValue } from "../../runtime-config";
24
25
 
25
26
  export interface IfevalInstance {
26
27
  prompt: string;
@@ -472,7 +473,7 @@ export async function evaluateIfeval(
472
473
  // Optiq-parity mode (DEFAULT): score optiq's EXACT prompt set (he runs the full
473
474
  // IFEval split — 541 prompts) through OUR verifiers + OUR chat template.
474
475
  // MLX_BUN_IFEVAL_FROZEN=0 reverts to our own copy + optional sampling.
475
- const useFrozen = opts.frozen ?? (process.env.MLX_BUN_IFEVAL_FROZEN !== "0");
476
+ const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_IFEVAL_FROZEN") !== "0");
476
477
  const rows = loadJsonl<IfevalRow>(useFrozen ? "ifeval_optiq_frozen" : "ifeval");
477
478
  const nSamples = opts.nSamples ?? rows.length;
478
479
  const idx = (!useFrozen && nSamples < rows.length)
@@ -10,6 +10,7 @@
10
10
  // table. MLX_BUN_MMLU_FROZEN=0 falls back to our own sampling of the full 14k set.
11
11
 
12
12
  import { loadJsonl, sampleIndices, type TaskModel } from "../runner";
13
+ import { runtimeValue } from "../../runtime-config";
13
14
 
14
15
  interface MmluRow {
15
16
  question: string;
@@ -85,7 +86,7 @@ export async function evaluateMmlu(
85
86
  // parity, not sampling. Set MLX_BUN_MMLU_FROZEN=0 (or frozen:false) to fall back to
86
87
  // our own stratified sampling of the full 14k test set (a DIFFERENT subset: JS RNG
87
88
  // ≠ numpy RandomState, so it does not match optiq's questions even at the same seed).
88
- const useFrozen = opts.frozen ?? (process.env.MLX_BUN_MMLU_FROZEN !== "0");
89
+ const useFrozen = opts.frozen ?? (runtimeValue("MLX_BUN_MMLU_FROZEN") !== "0");
89
90
 
90
91
  const testRows = loadJsonl<MmluRow>(useFrozen ? "mmlu_optiq_frozen" : "mmlu_test");
91
92
  const devRows = loadJsonl<MmluRow>(useFrozen ? "mmlu_optiq_dev" : "mmlu_dev");