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/expert-io.ts CHANGED
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import { nativePackDir } from "./native-pack";
5
5
  import type { MlxArray } from "./mlx/array";
6
6
  import type { MlxHandle } from "./mlx/ffi";
7
+ import { runtimeValue } from "./runtime-config";
7
8
 
8
9
  const { cstring, i32, ptr: pointer, u32, u64 } = FFIType;
9
10
  const EBUSY = 16;
@@ -69,7 +70,8 @@ export interface ExpertIOHintSnapshot {
69
70
 
70
71
  function resolveLibrary(explicit?: string): string {
71
72
  if (explicit) return explicit;
72
- if (process.env.MLX_BUN_EXPERT_IO_DYLIB) return process.env.MLX_BUN_EXPERT_IO_DYLIB;
73
+ const configured = runtimeValue("MLX_BUN_EXPERT_IO_DYLIB");
74
+ if (configured) return configured;
73
75
  const candidates = [
74
76
  join(dirname(process.execPath), "libmlx_bun_expert_io.dylib"),
75
77
  join(nativePackDir(), "libmlx_bun_expert_io.dylib"),
@@ -16,6 +16,7 @@ import { readFileSync } from "node:fs";
16
16
  import { MmapFile } from "./mmap";
17
17
  import { MlxArray, SAFETENSORS_TO_MLX } from "./mlx/array";
18
18
  import type { SafetensorsDtype } from "./safetensors";
19
+ import { runtimeValue } from "./runtime-config";
19
20
 
20
21
  export interface OffloadEntry {
21
22
  name: string;
@@ -71,4 +72,5 @@ export function expertOffloadArray(name: string): MlxArray | null {
71
72
  return MlxArray.fromPointer(mm.pointer(e.offset), e.shape, SAFETENSORS_TO_MLX[e.dtype]);
72
73
  }
73
74
 
74
- if (process.env.MLX_BUN_EXPERT_OFFLOAD) activateExpertOffload(process.env.MLX_BUN_EXPERT_OFFLOAD);
75
+ const expertOffloadPath = runtimeValue("MLX_BUN_EXPERT_OFFLOAD");
76
+ if (expertOffloadPath) activateExpertOffload(expertOffloadPath);
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { Dtype } from "./mlx/ffi";
21
21
  import type { MlxArray } from "./mlx/array";
22
+ import { runtimeValue } from "./runtime-config";
22
23
 
23
24
  let sink: Bun.FileSink | null = null;
24
25
  let seq = 0;
@@ -61,7 +62,8 @@ export function recordRouting(layer: number, indices: MlxArray): void {
61
62
  if (++written % 2000 === 0) sink.flush();
62
63
  }
63
64
 
64
- if (process.env.MLX_BUN_EXPERT_TRACE) beginExpertTrace(process.env.MLX_BUN_EXPERT_TRACE);
65
+ const expertTracePath = runtimeValue("MLX_BUN_EXPERT_TRACE");
66
+ if (expertTracePath) beginExpertTrace(expertTracePath);
65
67
 
66
68
  process.on("exit", () => { try { endExpertTrace(); } catch { /* best-effort */ } });
67
69
  process.on("SIGINT", () => { endExpertTrace(); process.exit(0); });
package/src/fit.ts CHANGED
@@ -10,6 +10,16 @@
10
10
 
11
11
  import { totalmem } from "node:os";
12
12
  import type { ModelConfig } from "./config";
13
+ import type { MemoryPlan } from "./memory-plan";
14
+ import {
15
+ kvBytesAt,
16
+ kvGeometry,
17
+ kvQuantBytesPerElement,
18
+ type KvSchemeOptions,
19
+ } from "./kv-scheme";
20
+
21
+ export { kvBytesAt, kvQuantBytesPerElement } from "./kv-scheme";
22
+ export type FitKvScheme = KvSchemeOptions;
13
23
 
14
24
  /** Decode-efficiency vs theoretical bandwidth ceiling, measured on the
15
25
  * reference machine (24.9 tok/s vs 30.3 ceiling @600 ctx). */
@@ -26,8 +36,6 @@ export const TRANSIENT_PER_TOKEN = 0.55e6;
26
36
  * recommendedMaxWorkingSetSize is ~75% on consumer SKUs). */
27
37
  export const WIRED_FRACTION = 0.75;
28
38
  export const DEFAULT_CHUNK = 2048;
29
- /** KV cache element size (bf16; quantized KV lands in Phase 6). */
30
- const KV_BYTES = 2;
31
39
 
32
40
  export interface MachineSpec {
33
41
  name: string;
@@ -130,43 +138,9 @@ export function chooseAutoModel<T extends AutoPickCandidate>(
130
138
  return bySizeDesc.find(fitsCoexistBudget) ?? bySizeDesc.find(fitsFullBudget);
131
139
  }
132
140
 
133
- export interface FitReport {
134
- fits: boolean;
135
- contextTokens: number;
136
- weightsBytes: number;
137
- kvBytes: number;
138
- transientBytes: number;
139
- totalBytes: number;
140
- usableBytes: number;
141
- maxSafeContext: number;
142
- predictedDecodeTps: number;
143
- }
144
-
145
- interface KvGeometry {
146
- fullLayers: number;
147
- slidingLayers: number;
148
- fullBytesPerToken: number;
149
- slidingBytesPerToken: number;
150
- window: number;
151
- }
152
-
153
- function kvGeometry(config: ModelConfig): KvGeometry {
154
- const t = config.text;
155
- const slidingLayers = t.layerTypes.filter((l) => l === "sliding_attention").length;
156
- const fullLayers = t.numHiddenLayers - slidingLayers;
157
- const kEqVFactor = t.attentionKEqV ? 2 : 2; // k and v stored separately either way
158
- return {
159
- fullLayers,
160
- slidingLayers,
161
- fullBytesPerToken: fullLayers * kEqVFactor * t.numGlobalKeyValueHeads * t.globalHeadDim * KV_BYTES,
162
- slidingBytesPerToken: slidingLayers * 2 * t.numKeyValueHeads * t.headDim * KV_BYTES,
163
- window: t.slidingWindow,
164
- };
165
- }
166
-
167
- export function kvBytesAt(config: ModelConfig, ctx: number): number {
168
- const g = kvGeometry(config);
169
- return g.fullBytesPerToken * ctx + g.slidingBytesPerToken * Math.min(ctx, g.window);
141
+ export interface FitReport extends MemoryPlan {
142
+ readonly strategy: "generic-kv";
143
+ readonly predictedDecodeTps: number;
170
144
  }
171
145
 
172
146
  export function fit(
@@ -182,17 +156,20 @@ export function fit(
182
156
  * replaces the machine-derived usable ceiling (ram × WIRED_FRACTION)
183
157
  * outright — the budget IS the usable envelope. */
184
158
  usableBytes?: number,
159
+ /** Active KV-quant scheme; a quantized cache holds more context in the
160
+ * same budget, so the solved ceiling (and admission) must bill it. */
161
+ kvScheme?: FitKvScheme,
185
162
  ): FitReport {
186
163
  const usable = usableBytes ?? machine.ramBytes * WIRED_FRACTION;
187
164
  const transient = Math.min(chunk, ctx) * TRANSIENT_PER_TOKEN;
188
- const kv = kvBytesAt(config, ctx);
165
+ const kv = kvBytesAt(config, ctx, kvScheme);
189
166
  const total = weightsBytes + kv + transient;
190
167
 
191
168
  // solve max context: weights + kv(ctx) + transient ≤ usable.
192
169
  // Below the window both KV terms are linear in ctx; above it the
193
170
  // sliding term saturates and only full-attention layers keep growing.
194
- const g = kvGeometry(config);
195
- const fixed = weightsBytes + chunk * TRANSIENT_PER_TOKEN;
171
+ const g = kvGeometry(config, kvScheme);
172
+ const fixed = weightsBytes + chunk * TRANSIENT_PER_TOKEN + g.linearStateBytes;
196
173
  let maxCtx = 0;
197
174
  if (usable > fixed) {
198
175
  const budget = usable - fixed;
@@ -216,11 +193,14 @@ export function fit(
216
193
  (isMoe ? MOE_DECODE_EFFICIENCY : DECODE_EFFICIENCY);
217
194
 
218
195
  return {
196
+ schemaVersion: 1,
197
+ strategy: "generic-kv",
219
198
  fits: total <= usable,
220
199
  contextTokens: ctx,
221
200
  weightsBytes,
222
201
  kvBytes: kv,
223
202
  transientBytes: transient,
203
+ reserveBytes: 0,
224
204
  totalBytes: total,
225
205
  usableBytes: usable,
226
206
  maxSafeContext: maxCtx,
@@ -231,6 +211,7 @@ export function fit(
231
211
  /** The SKU matrix: which Apple Silicon configs run this model at `ctx`. */
232
212
  export function skuMatrix(
233
213
  config: ModelConfig, weightsBytes: number, ctx: number, expertsBytes = 0,
214
+ kvScheme?: FitKvScheme,
234
215
  ): { sku: string; ramGB: number; fits: boolean; maxContext: number; decodeTps: number }[] {
235
216
  const rows: ReturnType<typeof skuMatrix> = [];
236
217
  for (const sku of APPLE_SKUS) {
@@ -240,7 +221,7 @@ export function skuMatrix(
240
221
  ramBytes: ram * 2 ** 30,
241
222
  bandwidthGBs: sku.bandwidthGBs,
242
223
  };
243
- const r = fit(config, weightsBytes, ctx, m, DEFAULT_CHUNK, expertsBytes);
224
+ const r = fit(config, weightsBytes, ctx, m, DEFAULT_CHUNK, expertsBytes, undefined, kvScheme);
244
225
  rows.push({
245
226
  sku: sku.chip,
246
227
  ramGB: ram,
package/src/flags.ts CHANGED
@@ -1,13 +1,11 @@
1
- // Generic on/off resolver for the per-fork decode flags (--compiled-decode,
2
- // --compiled-activations, --fused-sdpa, …). An explicit "1"/"0" in the env
3
- // always wins; otherwise the caller's `defaultOn` applies. The tier aliases
4
- // (--l1/--l2, applyDecodeRoute in cli.ts) set the env vars; model code
5
- // reads them through this helper so a hand-set flag and a tier preset resolve
6
- // identically.
1
+ import { runtimeFlag, type RuntimeKey } from "./runtime-config";
2
+
3
+ // Generic on/off resolver for per-fork decode flags. Process environment is
4
+ // captured once by runtime-config; CLI tier aliases install an explicit
5
+ // immutable override before model modules are loaded.
7
6
 
8
7
  /** Resolve an on/off env flag. An explicit "1"/"0" always wins; otherwise the
9
8
  * flag takes `defaultOn`. */
10
- export function flagOn(name: string, defaultOn: boolean): boolean {
11
- const v = process.env[name];
12
- return v === "1" ? true : v === "0" ? false : defaultOn;
9
+ export function flagOn(name: RuntimeKey, defaultOn: boolean): boolean {
10
+ return runtimeFlag(name, defaultOn);
13
11
  }
package/src/generate.ts CHANGED
@@ -8,8 +8,10 @@
8
8
 
9
9
  import { MlxArray, gpuStream } from "./mlx/array";
10
10
  import {
11
+ activeMemory,
11
12
  clearCache,
12
13
  maxRecommendedWorkingSetSize,
14
+ peakMemory,
13
15
  setWiredLimit,
14
16
  synchronize,
15
17
  } from "./mlx/ffi";
@@ -20,11 +22,17 @@ import { PagedKVCache } from "./model/paged-kv";
20
22
  import { DiffusionGemmaModel } from "./model/diffusion-gemma";
21
23
  import { diffusionGenerate } from "./diffusion/diffusion-generate";
22
24
  import type { RuntimeModel } from "./model/factory";
25
+ import type { PromptResponseTrace } from "./serve/prompt-response-trace";
23
26
  import type { KvQuantSpec, TurboQuantScheme } from "./config";
24
27
  import { flagOn } from "./flags";
28
+ import { runtimeValue } from "./runtime-config";
25
29
  import {
26
- makeLogitsProcessors, makeSampler,
27
- type LogitsProcessorOptions, type SamplerOptions, toLogprobs,
30
+ disposeStepExtras,
31
+ makeStepSampler,
32
+ stepExtrasArrays,
33
+ type LogitsProcessorOptions,
34
+ type SamplerOptions,
35
+ type StepExtras,
28
36
  } from "./sampler";
29
37
  import type { GrammarController } from "./grammar";
30
38
 
@@ -296,6 +304,11 @@ export interface GenerateStats {
296
304
  };
297
305
  }
298
306
 
307
+ export interface GenerateDiagnostics {
308
+ trace?: PromptResponseTrace;
309
+ mechanism?: "serial" | "continuous";
310
+ }
311
+
299
312
  export interface GeneratedToken {
300
313
  token: number;
301
314
  index: number;
@@ -346,7 +359,11 @@ export class Generation implements AsyncIterable<GeneratedToken> {
346
359
  // Scope semantics match the reference: set → generate → synchronize →
347
360
  // restore; nothing stays pinned between generations. Re-entrant: only
348
361
  // the outermost wiring scope touches the limit.
349
- const WIRE_THRESHOLD = 0.75;
362
+ // macOS 26.6 reports a 24.96 GiB recommended set on this 24 GB machine.
363
+ // The old 0.75 fraction therefore stopped wiring the 13-16 GiB Qwen/GLM
364
+ // models even though they page heavily without an explicit wired limit.
365
+ // Keep smaller 8-9 GiB models unwired while covering the large-model class.
366
+ const WIRE_THRESHOLD = 0.5;
350
367
  let wiredScopeDepth = 0;
351
368
  let wiredOldLimit = 0;
352
369
 
@@ -370,7 +387,7 @@ export function wiredWorkingSetBytes(model: WiredModelMemory): number {
370
387
  export function modelNeedsWiredLimit(
371
388
  model: WiredModelMemory,
372
389
  recommendedBytes = maxRecommendedWorkingSetSize(),
373
- force = process.env.MLX_BUN_FORCE_WIRE === "1",
390
+ force = runtimeValue("MLX_BUN_FORCE_WIRE") === "1",
374
391
  ): boolean {
375
392
  return force ||
376
393
  wiredWorkingSetBytes(model) > WIRE_THRESHOLD * recommendedBytes;
@@ -387,10 +404,23 @@ function exitWiredScope(): void {
387
404
  }
388
405
  }
389
406
 
407
+ /** Hold the process wired limit while one model owns GPU execution. */
408
+ export function acquireModelWiredLimit(model: WiredModelMemory): () => void {
409
+ if (!modelNeedsWiredLimit(model)) return () => {};
410
+ enterWiredScope();
411
+ let released = false;
412
+ return () => {
413
+ if (released) return;
414
+ released = true;
415
+ exitWiredScope();
416
+ };
417
+ }
418
+
390
419
  export function generate(
391
420
  model: RuntimeModel,
392
421
  promptTokens: number[],
393
422
  options: GenerateOptions = {},
423
+ diagnostics: GenerateDiagnostics = {},
394
424
  ): Generation {
395
425
  // DiffusionGemma is non-autoregressive: route to the denoising engine instead
396
426
  // of the AR decode loop. Same Generation/GenerateStats contract so the CLI and
@@ -398,7 +428,7 @@ export function generate(
398
428
  let inner =
399
429
  model instanceof DiffusionGemmaModel
400
430
  ? generateDiffusionInner(model, promptTokens, options)
401
- : generateInner(model, promptTokens, options);
431
+ : generateInner(model, promptTokens, options, diagnostics);
402
432
  if (options.adapters?.length) inner = adapterScoped(model, options.adapters, inner);
403
433
  const memoryModel: WiredModelMemory = model;
404
434
  if (memoryModel.expertRuntime?.flushUsage)
@@ -496,12 +526,11 @@ export async function withModelWiredLimit<T>(
496
526
  model: WiredModelMemory,
497
527
  run: () => Promise<T>,
498
528
  ): Promise<T> {
499
- if (!modelNeedsWiredLimit(model)) return run();
500
- enterWiredScope();
529
+ const release = acquireModelWiredLimit(model);
501
530
  try {
502
531
  return await run();
503
532
  } finally {
504
- exitWiredScope();
533
+ release();
505
534
  }
506
535
  }
507
536
 
@@ -542,15 +571,13 @@ async function* generateInner(
542
571
  model: RuntimeModel,
543
572
  promptTokens: number[],
544
573
  options: GenerateOptions,
574
+ diagnostics: GenerateDiagnostics,
545
575
  ): AsyncGenerator<GeneratedToken, GenerateStats> {
546
576
  const {
547
577
  maxTokens = 512,
548
578
  eosTokenIds = model.config.eosTokenIds,
549
579
  prefillChunkSize = 2048,
550
580
  } = options;
551
- const sampler = makeSampler(options);
552
- const processors = makeLogitsProcessors(options);
553
- const needsTokenHistory = processors.length > 0;
554
581
 
555
582
  // logprobs capture (mlx_lm.server semantics — see GenerateOptions.logprobs).
556
583
  // Everything below is gated: when neither flag is set, no extra ops, evals,
@@ -558,8 +585,23 @@ async function* generateInner(
558
585
  const captureSelLp = options.logprobs === true;
559
586
  const captureTopK =
560
587
  options.topLogprobs && options.topLogprobs > 0 ? options.topLogprobs : 0;
561
- const capture = captureSelLp || captureTopK > 0;
588
+ const stepSampler = makeStepSampler(options, {
589
+ tokenRepresentation: "device",
590
+ grammarWait: "external",
591
+ historyUpdate: "manual",
592
+ captureSelectedLogprob: captureSelLp,
593
+ captureTopLogprobs: captureTopK,
594
+ });
595
+ const needsTokenHistory = stepSampler.needsHistory;
562
596
 
597
+ const closePrefill = diagnostics.trace?.begin("prefill.total", {
598
+ mechanism: diagnostics.mechanism ?? "serial",
599
+ promptTokens: promptTokens.length,
600
+ cachedTokens: options.cache?.[0]?.offset ?? 0,
601
+ });
602
+ const closeBatchSetup = diagnostics.trace?.begin("prefill.batch_setup", {
603
+ mechanism: diagnostics.mechanism ?? "serial",
604
+ });
563
605
  const ownsCache = !options.cache;
564
606
  const cache = options.cache ?? model.makeCache();
565
607
  const cachedTokens = cache[0]!.offset;
@@ -572,22 +614,10 @@ async function* generateInner(
572
614
  // prompt + maxTokens − 1 (step maxTokens−1's forward), so this bound
573
615
  // makes pool exhaustion an accounting-bug tripwire, not a request limit.
574
616
  maybePageKv(cache, options, promptTokens.length + maxTokens);
575
- /** device-side token history (only maintained when processors need it) */
576
- let history: MlxArray | null = null;
577
-
617
+ closeBatchSetup?.();
578
618
  /** Device-side logprob capture for one step, computed from the SAME lp the
579
619
  * sampler saw (post-processors, pre-truncation) — read back lazily with the
580
620
  * token so decode pipelining is preserved. */
581
- interface StepExtras {
582
- sel: MlxArray | null; // [1,1] emitted token's logprob
583
- topIdx: MlxArray | null; // [1,k] top-k token ids
584
- topVals: MlxArray | null; // [1,k] their logprobs
585
- }
586
- const extrasArrays = (e: StepExtras | null): MlxArray[] =>
587
- e ? [e.sel, e.topIdx, e.topVals].filter((a): a is MlxArray => a !== null) : [];
588
- const disposeExtras = (e: StepExtras | null): void => {
589
- if (e) for (const a of [e.sel, e.topIdx, e.topVals]) a?.dispose();
590
- };
591
621
  /** Read extras back to JS (forces eval — they were async-dispatched with the
592
622
  * token) without appending casts behind the next decode step. */
593
623
  const readExtras = (e: StepExtras | null): TokenLogprobs | undefined => {
@@ -601,7 +631,7 @@ async function* generateInner(
601
631
  (a, b) => b.logprob - a.logprob,
602
632
  );
603
633
  }
604
- disposeExtras(e);
634
+ disposeStepExtras(e);
605
635
  return out;
606
636
  };
607
637
 
@@ -611,52 +641,11 @@ async function* generateInner(
611
641
  logits3d: MlxArray,
612
642
  step: number,
613
643
  ): { tok: MlxArray; extras: StepExtras | null } => {
614
- const V = logits3d.shape[2]!;
615
- let logits = ops.reshape(logits3d, [1, V]);
616
- for (const p of processors) logits = disposing(logits, p(history, logits));
617
- // Grammar mask (src/grammar.ts): apply AFTER the standard processors so the
618
- // grammar has the final say on validity (a -inf'd token stays -inf through
619
- // log-softmax). Off (no grammar) = zero cost — the branch is unset.
620
- if (options.grammar) logits = disposing(logits, options.grammar.applyMask(logits));
621
- const lp = toLogprobs(logits);
622
- logits.dispose();
623
- const tok = sampler(lp, step);
624
- let extras: StepExtras | null = null;
625
- if (capture) {
626
- let sel: MlxArray | null = null;
627
- let topIdx: MlxArray | null = null;
628
- let topVals: MlxArray | null = null;
629
- if (captureSelLp) {
630
- const idx = ops.reshape(tok, [1, 1]);
631
- sel = ops.takeAlongAxis(lp, idx, -1);
632
- idx.dispose();
633
- }
634
- if (captureTopK > 0) {
635
- // mlx-lm _format_top_logprobs: argpartition(-logprobs, kth=k-1)[:k]
636
- const k = Math.min(captureTopK, V);
637
- const negLp = ops.neg(lp);
638
- const part = ops.argpartitionAxis(negLp, k - 1, -1);
639
- const idxView = part.slice([0, 0], [1, k]);
640
- topIdx = ops.contiguous(idxView); // slices are strided views; raw readback needs contiguous
641
- topVals = ops.takeAlongAxis(lp, topIdx, -1);
642
- for (const a of [negLp, part, idxView]) a.dispose();
643
- }
644
- extras = { sel, topIdx, topVals };
645
- }
646
- lp.dispose();
647
- return { tok, extras };
644
+ const result = stepSampler.sample(logits3d, step);
645
+ return { tok: result.token, extras: result.extras };
648
646
  };
649
647
 
650
- const pushHistory = (tok: MlxArray) => {
651
- if (!needsTokenHistory) return;
652
- if (!history) {
653
- history = ops.reshape(tok, [1]);
654
- } else {
655
- const prev = history;
656
- history = ops.concatAxis([prev, tok], 0);
657
- prev.dispose();
658
- }
659
- };
648
+ const pushHistory = (tok: MlxArray): void => stepSampler.commitDevice(tok);
660
649
 
661
650
  // Decode-loop state lives at function scope so the finally can still
662
651
  // report stats and dispose in-flight arrays when the consumer
@@ -673,6 +662,7 @@ async function* generateInner(
673
662
  let nextExtras: StepExtras | null = null;
674
663
  let finished = false;
675
664
  let threw = false;
665
+ let closeTokenZero: (() => void) | undefined;
676
666
  const makeStats = (): GenerateStats => ({
677
667
  promptTokens: promptTokens.length,
678
668
  cachedTokens,
@@ -686,14 +676,19 @@ async function* generateInner(
686
676
 
687
677
  try {
688
678
  // ---- prefill ----
679
+ const closeInitialKv = diagnostics.trace?.begin("prefill.kv_maintenance", {
680
+ mechanism: diagnostics.mechanism ?? "serial",
681
+ boundary: "initial",
682
+ });
689
683
  maybeQuantizeKv(cache, options);
684
+ closeInitialKv?.();
690
685
  const tPrefill = performance.now();
691
686
  let h0: MlxArray;
692
687
  if (options.promptEmbeddings) {
693
688
  if (cachedTokens !== 0)
694
689
  throw new Error("promptEmbeddings cannot be combined with a pre-warmed cache");
695
690
  if (needsTokenHistory)
696
- history = ops.fromInt32(promptTokens, [promptTokens.length]);
691
+ stepSampler.seedHistory(promptTokens);
697
692
  // e2b/e4b need the spliced token ids to build per-layer inputs
698
693
  // (multimodal soft-token positions zeroed inside forwardEmbeddings).
699
694
  const embedIds = ops.fromInt32(promptTokens, [1, promptTokens.length]);
@@ -715,6 +710,11 @@ async function* generateInner(
715
710
  if (snapAt > pos && snapAt < promptTokens.length) {
716
711
  while (pos < snapAt) {
717
712
  const chunk = promptTokens.slice(pos, Math.min(pos + prefillChunkSize, snapAt));
713
+ const closeChunk = diagnostics.trace?.begin("prefill.chunk", {
714
+ mechanism: diagnostics.mechanism ?? "serial",
715
+ startToken: pos,
716
+ tokens: chunk.length,
717
+ });
718
718
  const ids = ops.fromInt32(chunk, [1, chunk.length]);
719
719
  const h = await forwardHiddenForGeneration(model, ids, cache);
720
720
  ids.dispose();
@@ -722,6 +722,7 @@ async function* generateInner(
722
722
  evalCacheState(cache);
723
723
  maybeQuantizeKv(cache, options);
724
724
  clearCache();
725
+ closeChunk?.();
725
726
  pos += chunk.length;
726
727
  // Macrotask yield between chunks (runtime-isolation.md Phase 1):
727
728
  // each chunk is a synchronous multi-hundred-ms FFI eval; without
@@ -749,6 +750,11 @@ async function* generateInner(
749
750
  ? Math.min(prefillChunkSize, promptTokens.length - pos - 1)
750
751
  : prefillChunkSize;
751
752
  const chunk = promptTokens.slice(pos, pos + n);
753
+ const closeChunk = diagnostics.trace?.begin("prefill.chunk", {
754
+ mechanism: diagnostics.mechanism ?? "serial",
755
+ startToken: pos,
756
+ tokens: chunk.length,
757
+ });
752
758
  const ids = ops.fromInt32(chunk, [1, chunk.length]);
753
759
  const h = await forwardHiddenForGeneration(model, ids, cache);
754
760
  ids.dispose();
@@ -762,12 +768,15 @@ async function* generateInner(
762
768
  // measured, scripts/decode-split.ts; the context-scaling decode
763
769
  // gap's main term).
764
770
  clearCache();
771
+ closeChunk?.();
765
772
  pos += n;
773
+ if (runtimeValue("MLX_BUN_PREFILL_MEM_LOG") === "1")
774
+ console.error(`[prefill-mem] ${pos} active ${(activeMemory() / 2 ** 30).toFixed(2)} peak ${(peakMemory() / 2 ** 30).toFixed(2)}`);
766
775
  // Macrotask yield between chunks (runtime-isolation.md Phase 1).
767
776
  await new Promise<void>((r) => setImmediate(r));
768
777
  }
769
778
  if (needsTokenHistory) {
770
- history = ops.fromInt32(promptTokens, [promptTokens.length]);
779
+ stepSampler.seedHistory(promptTokens);
771
780
  }
772
781
  // Under tailSplit this is exactly [promptTokens[len-1]] — the L=1
773
782
  // step-0 forward (uncompiled L=1 is bit-exact with compiled decode,
@@ -776,12 +785,27 @@ async function* generateInner(
776
785
  // PromptCache.put alignment) is unchanged: after step 0 the caches
777
786
  // cover exactly the prompt, as before.
778
787
  const lastChunk = promptTokens.slice(pos);
788
+ const closeChunk = diagnostics.trace?.begin("prefill.chunk", {
789
+ mechanism: diagnostics.mechanism ?? "serial",
790
+ startToken: pos,
791
+ tokens: lastChunk.length,
792
+ final: true,
793
+ });
779
794
  const ids0 = ops.fromInt32(lastChunk, [1, lastChunk.length]);
780
795
  h0 = await forwardHiddenForGeneration(model, ids0, cache);
781
796
  ids0.dispose();
797
+ closeChunk?.();
782
798
  }
783
799
  if (options.snapshotAt === undefined || options.snapshotAt >= promptTokens.length)
784
800
  options.onPrefillDone?.();
801
+ // The trace's prefill span is the uncached model-forward portion only.
802
+ // Token-0 lm-head, sampling, and readback are a separate additive stage.
803
+ if (diagnostics.trace && runtimeValue("MLX_BUN_P2R_SYNC") === "1")
804
+ synchronize(gpuStream);
805
+ closePrefill?.();
806
+ closeTokenZero = diagnostics.trace?.begin("token_zero.total", {
807
+ mechanism: diagnostics.mechanism ?? "serial",
808
+ });
785
809
  const [, L0, H] = h0.shape as [number, number, number];
786
810
  const hLast = h0.slice([0, L0 - 1, 0], [1, L0, H]);
787
811
  h0.dispose();
@@ -799,7 +823,7 @@ async function* generateInner(
799
823
  // to prompt_time, not decode — replicated so cross-stack decode
800
824
  // tok/s measure the same quantity. The boundary cost is real and
801
825
  // scales with prompt length; it belongs to "having prefilled".
802
- ops.asyncEvalAll([pending, ...extrasArrays(pendingExtras)]);
826
+ ops.asyncEvalAll([pending, ...stepExtrasArrays(pendingExtras)]);
803
827
 
804
828
  // ---- decode (pipelined) ----
805
829
  // Compiled decode (docs/design/optimization_plan.md Phase A): replay the per-step
@@ -886,13 +910,7 @@ async function* generateInner(
886
910
  // following iteration (supports() re-checks the grown caches).
887
911
  maybeQuantizeKv(cache, options);
888
912
  pushHistory(cur);
889
- if (needsTokenHistory) {
890
- const jt = ops.fromInt32(jumpEmit, [jumpEmit.length]);
891
- const prev = history!;
892
- history = ops.concatAxis([prev, jt], 0);
893
- prev.dispose();
894
- jt.dispose();
895
- }
913
+ stepSampler.commitNumbers(jumpEmit);
896
914
  const chunk = [grammarTok, ...jumpEmit];
897
915
  const ids = ops.fromInt32(chunk, [1, chunk.length]);
898
916
  const h = await forwardHiddenForGeneration(model, ids, cache);
@@ -910,7 +928,7 @@ async function* generateInner(
910
928
  nextPending = sn.tok;
911
929
  nextExtras = sn.extras;
912
930
  logits.dispose();
913
- ops.asyncEvalAll([nextPending, ...extrasArrays(nextExtras)]);
931
+ ops.asyncEvalAll([nextPending, ...stepExtrasArrays(nextExtras)]);
914
932
  } else {
915
933
  h.dispose(); // burst ends the generation (max_tokens or grammar done)
916
934
  }
@@ -945,7 +963,7 @@ async function* generateInner(
945
963
  nextPending = sn.tok;
946
964
  nextExtras = sn.extras;
947
965
  logits.dispose();
948
- ops.asyncEvalAll([nextPending, ...extrasArrays(nextExtras), ...evalWith]);
966
+ ops.asyncEvalAll([nextPending, ...stepExtrasArrays(nextExtras), ...evalWith]);
949
967
  }
950
968
 
951
969
  // sync-read step n's token while n+1 computes
@@ -957,6 +975,8 @@ async function* generateInner(
957
975
  // (mlx-lm stream_generate's n==0 clock swap; the first token is
958
976
  // "free" on the decode clock there too)
959
977
  prefillMs = performance.now() - tPrefill;
978
+ closeTokenZero?.();
979
+ closeTokenZero = undefined;
960
980
  tDecode = performance.now();
961
981
  }
962
982
  cur.dispose();
@@ -967,10 +987,10 @@ async function* generateInner(
967
987
  if (nextPending !== null && !jumpEmit) forwarded.push(token);
968
988
 
969
989
  if (eosTokenIds.includes(token)) {
970
- disposeExtras(curExtras);
990
+ disposeStepExtras(curExtras);
971
991
  nextPending?.dispose();
972
992
  nextPending = null;
973
- disposeExtras(nextExtras);
993
+ disposeStepExtras(nextExtras);
974
994
  nextExtras = null;
975
995
  stop = true;
976
996
  } else {
@@ -1013,8 +1033,8 @@ async function* generateInner(
1013
1033
  if (!finished) {
1014
1034
  pending?.dispose();
1015
1035
  nextPending?.dispose();
1016
- disposeExtras(pendingExtras);
1017
- disposeExtras(nextExtras);
1036
+ disposeStepExtras(pendingExtras);
1037
+ disposeStepExtras(nextExtras);
1018
1038
  }
1019
1039
  if (ownsCache) for (const c of cache) c.dispose();
1020
1040
  // The grammar controller owns native WASM state (GrammarMatcher +
@@ -1022,7 +1042,7 @@ async function* generateInner(
1022
1042
  // throw). TokenizerInfo itself is process-cached (vocab-structural), so
1023
1043
  // only the per-request matcher/compiled are freed here.
1024
1044
  options.grammar?.dispose();
1025
- history?.dispose();
1045
+ stepSampler.dispose();
1026
1046
  if (!finished && !threw) {
1027
1047
  // forced early return (consumer break at a yield): still report
1028
1048
  // stats — `forwarded` only lists tokens whose KV actually entered
@@ -1032,8 +1052,3 @@ async function* generateInner(
1032
1052
  }
1033
1053
  }
1034
1054
  }
1035
-
1036
- function disposing(old: MlxArray, next: MlxArray): MlxArray {
1037
- old.dispose();
1038
- return next;
1039
- }
package/src/grammar.ts CHANGED
@@ -33,6 +33,7 @@ import xgrammar, {
33
33
  import { readFileSync } from "node:fs";
34
34
  import { MlxArray } from "./mlx/array";
35
35
  import * as ops from "./mlx/ops";
36
+ import { runtimeValue } from "./runtime-config";
36
37
  import type { LoadedTokenizer } from "./tokenizer";
37
38
 
38
39
  /** The request shape — the union of every structured-output field name across
@@ -581,5 +582,5 @@ function resolveGrammarRequest(req: GrammarRequest):
581
582
  * (requests degrade to prompt injection). Matches the project's envelope-gated
582
583
  * kill-switch discipline (MLX_BUN_COMPILED_DECODE, MLX_BUN_GRAMMAR_BATCH, …). */
583
584
  export function grammarEnabled(): boolean {
584
- return process.env.MLX_BUN_GRAMMAR !== "0";
585
+ return runtimeValue("MLX_BUN_GRAMMAR") !== "0";
585
586
  }
package/src/harness-pi.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // mlx-bun harness pi — register mlx-bun as a provider in the user's pi
2
- // install (Phase 16 P1, docs/investigations/pi-builtin-investigation.md).
2
+ // install (Phase 16 P1, docs/archive/investigations/pi-builtin-investigation.md).
3
3
  //
4
4
  // Mechanism: a generated extension file in ~/.pi/agent/extensions/ that
5
5
  // calls pi.registerProvider() with dynamic discovery — it fetches