mlx-bun 0.0.13 → 0.3.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 (427) hide show
  1. package/README.md +71 -578
  2. package/bin/mlx-bun.mjs +1 -1
  3. package/docs/README.md +91 -0
  4. package/docs/{design → archive}/hlg-sampling.md +3 -3
  5. package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
  6. package/docs/{design → archive/investigations}/colibri-glm52-port.md +1 -1
  7. package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
  8. package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
  9. package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
  10. package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
  11. package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
  12. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
  13. package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
  14. package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
  15. package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
  16. package/docs/{planning → archive/planning}/chunk-task-roadmap.md +1 -1
  17. package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +4 -4
  18. package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +1 -1
  19. package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
  20. package/docs/archive/planning/release-notes-v0.2.0.md +49 -0
  21. package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
  22. package/docs/design/batching.md +569 -0
  23. package/docs/design/decode-speed-program.md +3178 -169
  24. package/docs/design/dreaming-nightly-pipeline.md +675 -282
  25. package/docs/design/generic-model-support.md +579 -334
  26. package/docs/design/kv-cache.md +680 -0
  27. package/docs/design/orpo-training.md +673 -1239
  28. package/docs/design/speculative-decoding.md +1627 -0
  29. package/docs/design/turboquant.md +2102 -0
  30. package/docs/design/unified-engine-frontier-plan.md +1831 -727
  31. package/docs/design/web-chat-redesign.md +666 -1083
  32. package/docs/planning/PRODUCT_ROADMAP.md +34 -1
  33. package/docs/planning/release-notes-v0.3.0.md +35 -0
  34. package/docs/planning/video-series.md +421 -0
  35. package/docs/reference/benchmarks.md +1162 -0
  36. package/docs/reference/cli.md +395 -149
  37. package/docs/reference/distribution.md +321 -181
  38. package/docs/reference/environment.md +348 -0
  39. package/docs/reference/glossary.md +39 -0
  40. package/docs/reference/library-api.md +580 -142
  41. package/docs/reference/memory.md +227 -64
  42. package/docs/reference/models.md +137 -6
  43. package/docs/reference/server-api.md +938 -422
  44. package/docs/reference/server-config.md +652 -320
  45. package/docs/reference/training.md +393 -254
  46. package/docs/reference/troubleshooting.md +166 -0
  47. package/package.json +10 -5
  48. package/src/admission-plan.ts +18 -0
  49. package/src/anthropic.ts +92 -3
  50. package/src/audio/decode.ts +2 -2
  51. package/src/audio/features.ts +2 -2
  52. package/src/backends/mlx/autoregressive.ts +108 -0
  53. package/src/backends/mlx/batch-group.ts +1646 -0
  54. package/src/backends/mlx/diffusion.ts +39 -0
  55. package/src/backends/mlx/draft-target.ts +47 -0
  56. package/src/backends/mlx/evaluation.ts +105 -0
  57. package/src/backends/mlx/fill-append.ts +45 -0
  58. package/src/backends/mlx/gateway-binding.ts +71 -0
  59. package/src/backends/mlx/graph.ts +38 -0
  60. package/src/backends/mlx/memory-client.ts +189 -0
  61. package/src/backends/mlx/memory-guard.ts +40 -0
  62. package/src/backends/mlx/methods.ts +90 -0
  63. package/src/backends/mlx/model-prompt.ts +164 -0
  64. package/src/backends/mlx/model-serving.ts +79 -0
  65. package/src/backends/mlx/prefill.ts +33 -0
  66. package/src/backends/mlx/request-policy.ts +20 -0
  67. package/src/backends/mlx/rollback.ts +51 -0
  68. package/src/backends/mlx/serial-executor.ts +273 -0
  69. package/src/backends/mlx/speculative.ts +114 -0
  70. package/src/backends/mlx/state-views.ts +26 -0
  71. package/src/backends/mlx/text-engine.ts +73 -0
  72. package/src/cli.ts +189 -61
  73. package/src/client.ts +66 -0
  74. package/src/config.ts +21 -4
  75. package/src/contracts/completion.ts +16 -0
  76. package/src/contracts/execution.ts +49 -0
  77. package/src/contracts/generation.ts +103 -0
  78. package/src/contracts/host.ts +7 -0
  79. package/src/contracts/jobs.ts +66 -0
  80. package/src/contracts/pi-provider.ts +30 -0
  81. package/src/contracts/pi.ts +259 -0
  82. package/src/contracts/resources.ts +18 -0
  83. package/src/contracts/scheduling.ts +27 -0
  84. package/src/contracts/tsconfig.json +14 -0
  85. package/src/diffusion/diffusion-generate.ts +218 -153
  86. package/src/embed.ts +1 -1
  87. package/src/engine/admission.ts +59 -0
  88. package/src/engine/cancellation.ts +38 -0
  89. package/src/engine/engine.ts +56 -0
  90. package/src/engine/execution-coordinator.ts +70 -0
  91. package/src/engine/execution-plan.ts +51 -0
  92. package/src/engine/resources.ts +42 -0
  93. package/src/engine/scheduler.ts +67 -0
  94. package/src/engine/session.ts +247 -0
  95. package/src/eval/README.md +44 -1
  96. package/src/eval/kl.ts +263 -23
  97. package/src/eval/reference-logits.ts +207 -0
  98. package/src/eval/runner.ts +10 -79
  99. package/src/eval/tasks/bfcl.ts +2 -1
  100. package/src/eval/tasks/gsm8k.ts +2 -1
  101. package/src/eval/tasks/hashhop.ts +2 -1
  102. package/src/eval/tasks/humaneval.ts +5 -4
  103. package/src/eval/tasks/ifeval.ts +2 -1
  104. package/src/eval/tasks/mmlu.ts +2 -1
  105. package/src/expert-io.ts +3 -1
  106. package/src/expert-offload.ts +3 -1
  107. package/src/fill/echo-index.ts +188 -0
  108. package/src/fill/fill-session.ts +452 -0
  109. package/src/fill/proposal.ts +68 -0
  110. package/src/fill/schema-rows.ts +506 -0
  111. package/src/fill/tool-boundary.ts +103 -0
  112. package/src/fit.ts +24 -43
  113. package/src/generate.ts +740 -337
  114. package/src/grammar.ts +14 -8
  115. package/src/harness-pi.ts +15 -34
  116. package/src/hub-rest.ts +4 -4
  117. package/src/index.ts +27 -0
  118. package/src/inference/denoising.ts +17 -0
  119. package/src/inference/graph.ts +33 -0
  120. package/src/inference/prefill.ts +39 -0
  121. package/src/inference/rollback.ts +7 -0
  122. package/src/jobs/db.ts +4 -0
  123. package/src/jobs/index.ts +2 -0
  124. package/src/jobs/job-entry.ts +15 -14
  125. package/src/jobs/runner.ts +98 -13
  126. package/src/jobs/task-client.ts +19 -0
  127. package/src/jobs/types.ts +2 -58
  128. package/src/kv-scheme.ts +199 -0
  129. package/src/kv-store.ts +459 -234
  130. package/src/lab/README.md +18 -0
  131. package/src/{assets → lab/curve}/curve-designer.html +2 -2
  132. package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
  133. package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +7 -5
  134. package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
  135. package/src/library.ts +40 -0
  136. package/src/lora.ts +18 -0
  137. package/src/media-fetch.ts +20 -4
  138. package/src/memory/article.ts +1 -1
  139. package/src/memory/chunk.ts +1 -1
  140. package/src/memory/model.ts +32 -170
  141. package/src/memory/prompts.ts +1 -1
  142. package/src/memory/rest.ts +1 -1
  143. package/src/memory/schedule.ts +1 -1
  144. package/src/memory/stages.ts +4 -1
  145. package/src/memory/vault.ts +3 -4
  146. package/src/mlx/array.ts +9 -10
  147. package/src/mlx/compile.ts +12 -2
  148. package/src/mlx/ffi.ts +66 -22
  149. package/src/mlx/materialize.ts +23 -0
  150. package/src/mlx/metal-kernel.ts +10 -11
  151. package/src/mlx/ops.ts +64 -19
  152. package/src/mlx/quantized-rows.ts +36 -0
  153. package/src/mlx/token-bitmask.ts +52 -0
  154. package/src/mlx/turboquant-kv-decode.ts +82 -0
  155. package/src/mlx/turboquant-kv-inverse.ts +122 -0
  156. package/src/mlx/turboquant-ops.ts +33 -3
  157. package/src/mlx/turboquant-tables.ts +2 -2
  158. package/src/model/batched-mask.ts +9 -3
  159. package/src/model/batched-quant.ts +2 -2
  160. package/src/model/batched-rotating-quant.ts +45 -99
  161. package/src/model/batched-rotating-state.ts +73 -0
  162. package/src/model/batched-rotating.ts +57 -106
  163. package/src/model/batched-row-storage.ts +127 -0
  164. package/src/model/compiled-decode.ts +4 -3
  165. package/src/model/diffusion-gemma.ts +8 -8
  166. package/src/model/factory.ts +87 -49
  167. package/src/model/fingerprint.ts +1 -1
  168. package/src/model/flash-attention.ts +3 -3
  169. package/src/model/gemma4-base.ts +187 -17
  170. package/src/model/gemma4.ts +6 -6
  171. package/src/model/generated/gemma4-12b.ts +52 -148
  172. package/src/model/generated/gemma4-26b.ts +34 -94
  173. package/src/model/generated/gemma4-e4b.ts +28 -76
  174. package/src/model/glm52-cache.ts +19 -9
  175. package/src/model/glm52-dsa.ts +13 -9
  176. package/src/model/glm52-memory.ts +28 -1
  177. package/src/model/implementation.ts +69 -0
  178. package/src/model/minicpm5.ts +7 -8
  179. package/src/model/profile.ts +443 -0
  180. package/src/model/qwen-append.ts +12 -0
  181. package/src/model/qwen-conv.ts +52 -0
  182. package/src/model/qwen3-delta.ts +98 -2
  183. package/src/model/qwen3-mrope.ts +252 -0
  184. package/src/model/qwen3_5-checkpoint.ts +164 -0
  185. package/src/model/qwen3_5.ts +336 -60
  186. package/src/model/support.ts +1 -1
  187. package/src/model/trellis-balanced-scatter.ts +51 -0
  188. package/src/model/trellis-linear.ts +672 -0
  189. package/src/model/trellis-shared-m.ts +151 -0
  190. package/src/model/trellis-shared-scatter.ts +144 -0
  191. package/src/model/trellis-splitk-prefill.ts +158 -0
  192. package/src/model/trellis-tiled-prefill.ts +136 -0
  193. package/src/model/trellis-vector-expand.ts +62 -0
  194. package/src/model/trellis-wide-prefill.ts +94 -0
  195. package/src/model/universal/dense.ts +1 -1
  196. package/src/model/universal/modules.ts +8 -1
  197. package/src/native/frame_extract.swift +140 -0
  198. package/src/native-pack.ts +15 -7
  199. package/src/pi-provider.ts +6 -64
  200. package/src/pi-web.ts +14 -320
  201. package/src/prompt-cache.ts +97 -54
  202. package/src/quantize/allocator.ts +1 -1
  203. package/src/quantize/atomic-output.ts +38 -0
  204. package/src/quantize/calibration.ts +2 -1
  205. package/src/quantize/config-writer.ts +16 -3
  206. package/src/quantize/index.ts +28 -1
  207. package/src/quantize/job.ts +11 -0
  208. package/src/quantize/quantizer.ts +265 -100
  209. package/src/quantize/rotate.ts +603 -0
  210. package/src/quantize/safetensors-writer.ts +79 -1
  211. package/src/quantize/sensitivity.ts +41 -59
  212. package/src/quantize/trellis.ts +586 -0
  213. package/src/quantize/weight-transform.ts +292 -0
  214. package/src/responses.ts +130 -3
  215. package/src/runtime-config.ts +81 -0
  216. package/src/sampler.ts +276 -4
  217. package/src/serve/admin-routes.ts +184 -0
  218. package/src/serve/aux-routes.ts +132 -0
  219. package/src/serve/batch-scheduler.ts +8 -1558
  220. package/src/serve/chat-request.ts +391 -0
  221. package/src/serve/chat-stage.ts +195 -0
  222. package/src/serve/checkpoint-identity.ts +64 -0
  223. package/src/serve/completion-executor.ts +354 -0
  224. package/src/serve/completion-sink.ts +157 -0
  225. package/src/serve/discovery-routes.ts +204 -0
  226. package/src/serve/generation-gateway.ts +211 -243
  227. package/src/serve/http.ts +159 -0
  228. package/src/serve/inference-request.ts +79 -0
  229. package/src/serve/isolate.ts +297 -105
  230. package/src/serve/kv-budget.ts +14 -0
  231. package/src/serve/lab-routes.ts +341 -0
  232. package/src/serve/lane-registry.ts +2 -1
  233. package/src/serve/model-admin-routes.ts +155 -0
  234. package/src/serve/model-binding.ts +43 -0
  235. package/src/serve/model-host.ts +512 -0
  236. package/src/serve/openai-wire.ts +103 -0
  237. package/src/serve/parent-application.ts +56 -0
  238. package/src/serve/pipeline.ts +33 -0
  239. package/src/serve/preparation.ts +33 -0
  240. package/src/serve/prompt-response-trace.ts +147 -0
  241. package/src/serve/request-plan.ts +162 -0
  242. package/src/serve/request-prep.ts +346 -0
  243. package/src/serve/response-lease.ts +24 -0
  244. package/src/serve/responses-client.ts +69 -0
  245. package/src/serve/session-completion-engine.ts +110 -0
  246. package/src/serve/session-search.ts +1 -1
  247. package/src/serve/static-routes.ts +101 -0
  248. package/src/serve/text-completion-stage.ts +104 -0
  249. package/src/serve/token-streams.ts +519 -0
  250. package/src/server.ts +523 -3551
  251. package/src/spec/assistant-source.ts +10 -44
  252. package/src/spec/deepspec-source.ts +6 -6
  253. package/src/spec/dflash-source.ts +9 -8
  254. package/src/spec/dspark/ab-stats.ts +3 -3
  255. package/src/spec/dspark/calibration.ts +2 -2
  256. package/src/spec/dspark/deepspec-module.ts +2 -2
  257. package/src/spec/dspark/generate-dflash.ts +2 -2
  258. package/src/spec/dspark/generate.ts +2 -2
  259. package/src/spec/dspark/loader.ts +1 -1
  260. package/src/spec/dspark/module-dflash.ts +5 -4
  261. package/src/spec/dspark/module.ts +3 -3
  262. package/src/spec/dspark/quantize-drafter.ts +1 -1
  263. package/src/spec/generate.ts +1 -1
  264. package/src/spec/glm52-mtp-source.ts +9 -1
  265. package/src/spec/ngram-source.ts +1 -1
  266. package/src/spec/prefix-state.ts +45 -0
  267. package/src/spec/qwen-mtp-source.ts +528 -0
  268. package/src/spec/serve-loop.ts +179 -213
  269. package/src/spec/source.ts +42 -10
  270. package/src/spec/two-model.ts +1 -1
  271. package/src/ssd-cache.ts +95 -24
  272. package/src/ssd-durability.ts +207 -0
  273. package/src/tool-call.ts +51 -14
  274. package/src/train/flash-cce.ts +14 -13
  275. package/src/train/forward.ts +38 -1
  276. package/src/train/loss.ts +3 -2
  277. package/src/train/prefix-shared.ts +5 -1
  278. package/src/train/segmented.ts +9 -6
  279. package/src/train/trainer.ts +9 -8
  280. package/src/vision/preprocess.ts +1 -1
  281. package/src/vision/prompt.ts +48 -2
  282. package/src/vision/qwen3vl-preprocess.ts +409 -0
  283. package/src/vision/qwen3vl-prompt.ts +196 -0
  284. package/src/vision/qwen3vl-tower.ts +555 -0
  285. package/src/vision/video-frames.ts +148 -0
  286. package/src/web/app.html +1 -1
  287. package/src/web/app.js +15 -6
  288. package/src/web/src/adapters-panel.ts +3 -3
  289. package/src/web/src/assistant.ts +2 -36
  290. package/src/web/src/chat.ts +16 -9
  291. package/src/web/src/composer.ts +4 -4
  292. package/src/web/src/hub.ts +1 -1
  293. package/src/web/src/markdown.ts +0 -0
  294. package/src/web/src/memory-panel.ts +1 -1
  295. package/src/web/src/model-picker.ts +1 -1
  296. package/src/web/src/palette.ts +1 -1
  297. package/src/web/src/protocol.ts +3 -39
  298. package/src/web/src/sessions.ts +1 -1
  299. package/src/web/src/ui-catalog.ts +4 -18
  300. package/src/web-assets.ts +47 -0
  301. package/src/weights.ts +134 -33
  302. package/docs/archive/mlx-bun-lab-report.html +0 -275
  303. package/docs/archive/wwdc-mlx-bun.html +0 -848
  304. package/docs/design/adapters-end-to-end.md +0 -205
  305. package/docs/design/batching-perf-path.md +0 -255
  306. package/docs/design/batching-v2-plan.md +0 -529
  307. package/docs/design/bucketing-stage.md +0 -197
  308. package/docs/design/compat-cli-surface-design.md +0 -230
  309. package/docs/design/docs-reorg-plan.md +0 -165
  310. package/docs/design/dspark-serving-program.md +0 -371
  311. package/docs/design/dspark-speculative-decoding.md +0 -268
  312. package/docs/design/grammar-spec-batching-integration.md +0 -301
  313. package/docs/design/memory-inference-path.md +0 -422
  314. package/docs/design/memory-synthesis.md +0 -287
  315. package/docs/design/memory-system.md +0 -237
  316. package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
  317. package/docs/design/omlx-adoption-map.md +0 -98
  318. package/docs/design/orpo-dynamic-lambda.md +0 -220
  319. package/docs/design/orpo-future-enhancements.md +0 -340
  320. package/docs/design/paged-kv-cache.md +0 -214
  321. package/docs/design/parallel-slots.md +0 -567
  322. package/docs/design/parity-tier-dag.md +0 -161
  323. package/docs/design/repo-cleanup-plan.md +0 -189
  324. package/docs/design/runtime-isolation.md +0 -263
  325. package/docs/design/segmented-backward-training.md +0 -543
  326. package/docs/design/spec-decode-larger-targets.md +0 -229
  327. package/docs/design/ssd-kv-cold-tier.md +0 -288
  328. package/docs/design/structured-output.md +0 -484
  329. package/docs/design/tauri-desktop-app.md +0 -127
  330. package/docs/design/the-dreaming-master-plan.md +0 -445
  331. package/docs/design/turboquant-kv.md +0 -235
  332. package/docs/design/w4a16-compute-precision-spike.md +0 -232
  333. package/docs/design/web-chat-beat-matrix.md +0 -315
  334. package/docs/design/write-pipeline-entity.md +0 -468
  335. package/docs/investigations/curve-runs/bisector-route.json +0 -98
  336. package/docs/investigations/curve-runs/bisector-trace.json +0 -1393
  337. package/docs/investigations/curve-runs/curve-terrain.json +0 -1179
  338. package/docs/investigations/curve-runs/curve-terrain.svg +0 -42
  339. package/docs/investigations/hlg-ab.html +0 -34
  340. package/docs/investigations/hlg-explorer.html +0 -238
  341. package/docs/investigations/hlg-figs/cliff.svg +0 -1
  342. package/docs/investigations/hlg-figs/cross.svg +0 -1
  343. package/docs/investigations/hlg-figs/edges.svg +0 -1
  344. package/docs/investigations/hlg-figs/k20-grids.svg +0 -1
  345. package/docs/investigations/hlg-figs/k20-peaks.svg +0 -1
  346. package/docs/investigations/hlg-figs/lw-curve.svg +0 -1
  347. package/docs/investigations/hlg-figs/terrain.svg +0 -1
  348. package/docs/investigations/hlg-report.html +0 -262
  349. package/docs/investigations/hlg-runs/hlg-ab.json +0 -149
  350. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
  351. package/docs/investigations/hlg-runs/hlg-blind.json +0 -107
  352. package/docs/investigations/hlg-runs/hlg-cliff.json +0 -197
  353. package/docs/investigations/hlg-runs/hlg-cross.json +0 -505
  354. package/docs/investigations/hlg-runs/hlg-edge.json +0 -388
  355. package/docs/investigations/hlg-runs/hlg-eval1.json +0 -25
  356. package/docs/investigations/hlg-runs/hlg-frontier.json +0 -86
  357. package/docs/investigations/hlg-runs/hlg-grid.json +0 -242
  358. package/docs/investigations/hlg-runs/hlg-grid2.json +0 -343
  359. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
  360. package/docs/investigations/hlg-runs/hlg-map.json +0 -383
  361. package/docs/investigations/hlg-runs/run0-original.log +0 -159
  362. package/docs/investigations/hlg-runs/runA-recommended.log +0 -131
  363. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
  364. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
  365. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
  366. package/docs/investigations/hlg-runs/runAD-ab-samples.log +0 -8
  367. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
  368. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
  369. package/docs/investigations/hlg-runs/runAG-eval-config2.log +0 -16
  370. package/docs/investigations/hlg-runs/runB-gain-variants.log +0 -192
  371. package/docs/investigations/hlg-runs/runC-pivots.log +0 -162
  372. package/docs/investigations/hlg-runs/runD-oetf.log +0 -132
  373. package/docs/investigations/hlg-runs/runE-eotf.log +0 -162
  374. package/docs/investigations/hlg-runs/runF-shaper-working.log +0 -86
  375. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
  376. package/docs/investigations/hlg-runs/runH-wsweep.log +0 -176
  377. package/docs/investigations/hlg-runs/runI-asweep.log +0 -176
  378. package/docs/investigations/hlg-runs/runJ-knobmap.log +0 -49
  379. package/docs/investigations/hlg-runs/runK-frontier.log +0 -19
  380. package/docs/investigations/hlg-runs/runL-widemap.log +0 -65
  381. package/docs/investigations/hlg-runs/runM-frontier-emb.log +0 -15
  382. package/docs/investigations/hlg-runs/runN-verify.log +0 -116
  383. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
  384. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
  385. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
  386. package/docs/investigations/hlg-runs/runR-blind-transcript.log +0 -79
  387. package/docs/investigations/hlg-runs/runS-cross-coherence.log +0 -33
  388. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
  389. package/docs/investigations/hlg-runs/runU-cross-12b.log +0 -33
  390. package/docs/investigations/hlg-runs/runV-lw-e4b.log +0 -22
  391. package/docs/investigations/hlg-runs/runW-lw-12b.log +0 -22
  392. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
  393. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
  394. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
  395. package/docs/planning/IDEAS.md +0 -69
  396. package/docs/reference/embedding.md +0 -197
  397. package/docs/reference/features-matrix.md +0 -100
  398. package/docs/reference/orpo-quickstart.md +0 -135
  399. package/src/flags.ts +0 -13
  400. /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
  401. /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
  402. /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
  403. /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
  404. /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
  405. /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
  406. /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
  407. /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
  408. /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
  409. /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
  410. /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
  411. /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
  412. /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
  413. /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
  414. /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
  415. /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
  416. /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
  417. /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
  418. /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
  419. /package/docs/{planning → archive/planning}/journal.md +0 -0
  420. /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
  421. /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
  422. /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
  423. /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
  424. /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
  425. /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
  426. /package/{docs/investigations → src/serve/assets}/curve-terrain.html +0 -0
  427. /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
@@ -1,178 +1,141 @@
1
- # Unified engine + the frontier program — the reorganization plan
2
-
3
- Status: **PLAN** (2026-07-05, from the flag-architecture discussion with Josh
4
- after the naked-=-L1 decision). This is the design that makes the library
5
- "make sense": one engine, one honest flag surface, per-scheme oracles, and a
6
- Lab where optimization experiments live until they earn defaults.
7
-
8
- ## 0. The goals (Josh, 2026-07-05 — the two-part product)
9
-
10
- 1. **A drop-in replacement for mlx-lm: support EVERYTHING they do, with
11
- bit-exact parity.** This is the default. Parity is only DEFINED over
12
- outputs mlx-lm can produce — you cannot ask for "mlx-lm's bits under
13
- mixed-precision KV" because mlx-lm cannot output anything under it.
14
- 2. **Beyond mlx-lm — features you opt into by deliberately trading the
15
- parity guarantee for something you value MORE**, each answering to its
16
- own oracle (whoever already ships it):
17
- - **multi-model switching** (oracle: optiq per-request switching / oMLX
18
- EnginePool) — ❌ missing today; the newest backlog item (layer 4).
19
- - **mixed-precision KV cache** (oracle: optiq) — ✅ serial, bit-exact;
20
- ✅ **under batching for all-full-attention configs (LANDED 2026-07-05
21
- — Phase 3.1 P1: cpm5 end-to-end, per-row bit-exact vs the optiq
22
- composition; no other stack ships this)**; rotating-layer configs
23
- (gemma) = milestone 2.
24
- - **prefix sharing** (oracle: vLLM/SGLang) — ⚠️ whole-prefix reuse only;
25
- true cross-request sharing (one system prompt's KV, N rows) missing.
26
- - **prompt caching** (oracle: mlx-lm's LRUPromptCache) — ✅ byte-capped
27
- + SSD cold tier beyond anyone; ❌ batched rows → Phase 3.2.
28
- - **structured outputs** (oracle: oMLX/xgrammar) — ✅ serial + batched,
29
- byte-identical vs oMLX; composes with spec decode.
30
-
31
- Supersedes: batching-v2-plan.md's standing decision that `--batch N` is a
32
- mode switch (reversal documented in §4, with the new rationale — the old
33
- rejection's determinism argument is answered, not ignored). Builds on:
34
- faithful-l1-consolidation.md (the L1 baseline, now the naked default),
35
- parity-tier-dag.md (per-scheme oracles), omlx-adoption-map.md (survey),
36
- batching-perf-path.md (the B=1 gap numbers this plan must close).
37
-
1
+ ---
2
+ status: active
3
+ axis: ON
4
+ canonical-for: engine-architecture
5
+ plan-anchor: "Interface-based engine refactor `[~]`"
6
+ last-verified: 2026-08-23
38
7
  ---
39
8
 
40
- ## 1. North star: beyond the Pareto frontier
9
+ # Engine architecture — four layers, per-scheme oracles, one request path
10
+
11
+ This is the canonical architecture doc for mlx-bun's engine. It owns:
12
+
13
+ - the **four layers** (L1 graph / L2 KV+quant schemes / L3 engine / L4 surface)
14
+ and the rule that features decompose along them;
15
+ - the **fidelity tiers** — L1 = mlx-lm bit-exact, L2 = mlx-optiq bit-exact,
16
+ Lab = no-oracle experiments gated by KL/eval and a paired A/B before any
17
+ default — and the decision procedure that places every optimization;
18
+ - the **naked default IS L1** decision (2026-07-05);
19
+ - the **engine** decision that concurrency is the batch size;
20
+ - the **request path** as it exists in `src/` today, the single-path rule, and
21
+ the known deviations from it;
22
+ - the **flag end-state** and how it maps onto the layers.
23
+
24
+ The v2 refactor is in [§12](#12-interface-based-engine-refactor).
25
+ Josh requested replaceable implementations behind interfaces on 2026-09-04.
26
+ That section defines the target and migration, with implementation status below.
27
+ The portable session and native implementation ports are documented in the
28
+ library API; existing model-class exports remain supported.
29
+ Earlier sections retain the current architecture and numerical contracts.
30
+ `last-verified` above dates the earlier inventory, not a whole-document reaudit.
31
+
32
+ Folded in (2026-08-23): `docs/design/unified-engine-frontier-plan.md` (tier framing, two axes, flag
33
+ classification) and `docs/archive/investigations/faithful-l1-consolidation.md` (the 2026-07-04 faithful→L1
34
+ consolidation and the 2026-07-05 deletion pass, now the History section).
35
+ Status and changelog prose lives in PLAN.md — see "Decision: naked default =
36
+ --l1; levers must beat the baseline (2026-07-05, Josh)" for the decision
37
+ record and "Serving architecture consolidation" for the live S0–S4 work. Every
38
+ `src/` claim below was re-read against the tree on 2026-08-23; where an older
39
+ draft of this doc disagreed with the code, the code wins and the note says so.
40
+
41
+ Companions: `docs/design/batching.md` (B=1 gap numbers), `docs/design/batching.md` and
42
+ `docs/design/batching.md` (scheduler mechanics — the mode-switch decision there is
43
+ superseded by §5), `docs/design/unified-engine-frontier-plan.md` (the S0–S3 seam),
44
+ `decode-speed-program.md` (the only doc that ranks speed levers; it also
45
+ carries the oMLX port ledger that used to be `docs/design/decode-speed-program.md`).
41
46
 
42
- The claim we are building toward: **"the best way to run local AI on a Mac."**
43
- Formally: every (model, workload) point we ship should sit on or beyond the
44
- current local-inference Pareto frontier in this space:
47
+ ---
45
48
 
46
- | axis | direction | measured by |
47
- |---|---|---|
48
- | memory | less is better | generation peak (Metal), fit-model predictions |
49
- | decode tok/s | more | bench-h2h (stable cells only) |
50
- | prefill / TTFT | faster | bench-h2h, server TTFT |
51
- | throughput | higher | batched aggregate tok/s (bench-serving-load) |
52
- | intelligence | higher | frozen eval suite (MMLU/GSM8K/IFEval/HumanEval/BFCL/HashHop, evals.sqlite) |
53
-
54
- **THE CONTRACT (Josh, 2026-07-05):** two rules, and they compose:
55
-
56
- 1. **The drop-in promise: we must produce the SAME BITS as mlx-lm** — the
57
- original oracle. How the bits are produced (storage, kernels,
58
- scheduling, engine, language) is the space we compete in.
59
- 2. **The oracle for ANYTHING is whoever already does that thing.** Want
60
- mlx-lm's decode → mlx-lm is the oracle. Want optiq's mixed KV → optiq.
61
- Want vLLM's paged attention → **vLLM is the oracle**: read their
62
- implementation, copy what they did, verify against them, then optimize.
63
- This is the standing house method (FaithfulMiniCPM5 = copy mlx-lm;
64
- mixed KV = copy optiq; batching = copy BatchGenerator; SSD tier = copy
65
- oMLX). Never frame a capability someone ships as oracle-less. "No
66
- oracle" means NOBODY does the thing — genuine research — and that, and
67
- only that, is what the Lab is for.
68
-
69
- Verification form follows the oracle's framework: same-framework oracles
70
- (mlx-lm, optiq — same libmlx) admit bit-exact gates; cross-framework
71
- oracles (vLLM — CUDA/PyTorch) anchor the ALGORITHM and behavior (block
72
- semantics, sharing correctness, utilization), while rule 1 still binds the
73
- final output bits in the default regime. "Faithful" kernel-set identity is
74
- the easiest route to same-bits, not the contract; the one constraint
75
- bit-exactness puts on implementations is math/reduction order (the perf
76
- kernel died exactly there).
49
+ ## 1. The two-part product and the contract
50
+
51
+ 1. **A drop-in replacement for mlx-lm: everything they do, bit-exact.** This is
52
+ the default. Parity is only *defined* over outputs mlx-lm can produce — you
53
+ cannot ask for "mlx-lm's bits under mixed-precision KV" because mlx-lm
54
+ cannot output anything under it.
55
+ 2. **Beyond mlx-lm — features you opt into by deliberately trading the parity
56
+ guarantee for something you value more**, each answering to its own oracle:
57
+ mixed-precision KV (optiq), structured output (oMLX/xgrammar), speculative
58
+ decoding (mlx-lm's `speculative_generate_step`; optiq for the KV-borrowing
59
+ assistant drafter), prompt caching (mlx-lm's `LRUPromptCache`), prefix
60
+ sharing and paged KV (vLLM/SGLang), multi-model switching (optiq
61
+ per-request switching / oMLX EnginePool).
62
+
63
+ **THE CONTRACT (Josh, 2026-07-05) — two rules that compose:**
64
+
65
+ 1. **The drop-in promise: the same BITS as mlx-lm.** How the bits are produced
66
+ (storage, kernels, scheduling, engine, language) is the space we compete in.
67
+ 2. **The oracle for anything is whoever already does that thing.** Want
68
+ mlx-lm's decode → mlx-lm. optiq's mixed KV → optiq. vLLM's paged attention
69
+ → vLLM: read their implementation, copy what they did, verify against them,
70
+ then optimize. "No oracle" means *nobody* does the thing — genuine research
71
+ — and that, and only that, is what the Lab is for.
72
+
73
+ Verification form follows the oracle's framework: same-libmlx oracles (mlx-lm,
74
+ optiq) admit bit-exact gates; cross-framework oracles (vLLM, CUDA/PyTorch)
75
+ anchor the algorithm and behavior while rule 1 still binds the output bits in
76
+ the default regime. "Faithful" kernel-set identity is the easiest route to
77
+ same-bits, not the contract; the one constraint bit-exactness puts on an
78
+ implementation is math/reduction order.
77
79
 
78
80
  Two consequences drive everything below:
79
81
 
80
- - **The baseline must be even and proven before optimizing.** We now have it:
81
- the L1 faithful path is bit-exact with mlx-lm at 1.00× speed on every
82
- model. Anything that claims to beat it proves it with a paired A/B on a
83
- stable pass — otherwise it is noise or a regression (2026-07-05 evidence:
84
- every output-changing lever failed this bar).
85
- - **Frontier-shifting work is usually a *quality-per-byte* play, not a raw
86
- kernel play.** Mixed-precision quantization (weights AND KV) moves memory
87
- down while holding or raising intelligence — a genuine beyond-the-frontier
88
- move, unlike micro-kernel wins that trade along it. This is why quantized
89
- KV stays first-class even though it lost the speed A/B: it buys context
90
- headroom (12B@16k: −1.3 GB, growing linearly) and mixed schemes buy
91
- intelligence-per-byte.
92
-
93
- ## 2. Survey — what the other engines got right (and wrong for us)
94
-
95
- - **mlx-lm** (`server.py`/`generate.py`): the original oracle and THE
96
- drop-in target — and the project most similar to ours: it spans layers
97
- 1+2+3(+4), shipping the reference graphs, native bf16 + uniform 4/8-bit
98
- quantized KV (`QuantizedKVCache`/`--kv-bits` — layer 2 is theirs too),
99
- the batched engine, and the server. Its server is **concurrency-driven
100
- by default** — it auto-creates a `BatchGenerator` (decode-concurrency
101
- cap 32, prefill concurrency, chunked `prefill_step_size`) and routes any
102
- batchable request into it; only non-batchable requests (draft models
103
- etc.) serve sequentially. There is no "batching mode" flag — the cap is
104
- the only knob. Count-capped prompt cache (the OOM footgun we already
105
- fixed by byte-capping); no quantization on the batched path.
106
- → **Adopt: the concurrency-driven default is our drop-in-parity behavior.**
107
- - **vLLM**: continuous batching + PagedAttention — the KV cache is block-paged
108
- with a block table per sequence, which is what makes batching, long context,
109
- and prefix sharing *compose* instead of conflict. No batch flag; `max_num_seqs`
110
- caps. Chunked prefill interleaves with decode so TTFT doesn't starve running
111
- streams. → **Adopt (long-term): block-paged KV as the structure that unifies
112
- batch + quantized KV + prompt cache + SSD tier. Adopt (near-term): the
113
- no-mode-flag scheduler and prefill/decode interleaving policy.**
114
- - **llama.cpp** (server): slot-based (`-np` = slot count), one shared KV
115
- buffer partitioned by sequence — concurrency-driven, no mode flag. KV cache
116
- default is **f16; quantized KV (q8_0/q4_0) is opt-in** — independent
117
- confirmation of our bf16-default decision. Its imatrix (importance-matrix)
118
- quantization is the ecosystem's calibration-driven intelligence-per-byte
119
- play — the same family as oQ and our knapsack `--target-bpw`.
120
- → **Adopt: nothing structural we don't have; validates KV defaults; imatrix
121
- is prior art for the §7 quant program.**
122
- - **oMLX**: scheduler on mlx-lm's BatchGenerator + product appliances. Already
123
- systematically mapped (omlx-adoption-map.md): batching parity + SSD tier
124
- ported and won; burst decode ported and REFUTED (Python-GIL medicine, our
125
- disease doesn't exist); oQ quant + DFlash queued. → **Keep mining the queue;
126
- its engine architecture is not ahead of ours.**
127
- - **mlx-optiq**: the L2 oracle — per-layer mixed KV (`kv_config.json`), fused
128
- quantized SDPA, vision sidecar. Not a batching engine. → **Its role is the
129
- oracle for mixed-precision schemes (§5) and the source of the next quant
130
- ideas (TurboQuant per Josh).**
131
-
132
- ## 3. The architecture (four layers, each with one job)
82
+ - **The baseline must be even and proven before optimizing.** The L1 path is
83
+ bit-exact with mlx-lm and at decode parity on every model (PLAN.md, the
84
+ 2026-07-05 decision). Anything claiming to beat it proves it with a paired
85
+ A/B on a stable pass; otherwise it is noise or a regression.
86
+ - **Frontier-shifting work is usually a quality-per-byte play, not a raw kernel
87
+ play.** Mixed-precision quantization (weights and KV) moves memory down while
88
+ holding or raising intelligence. Quantized KV stays first-class even though
89
+ it lost the speed A/B: it buys context headroom.
90
+
91
+ The frontier axes we measure: memory (generation peak, fit-model), decode
92
+ tok/s (paired h2h, stable cells), prefill/TTFT, batched aggregate throughput,
93
+ and intelligence (the frozen eval suite in evals.sqlite). Curated numbers live
94
+ in `docs/reference/benchmarks.md`.
95
+
96
+ ## 2. The four layers
133
97
 
134
98
  ```mermaid
135
99
  flowchart TB
136
100
  subgraph clients["CLIENTS"]
137
101
  direction LR
138
102
  PI["Pi agent<br/>chat UI + terminal"]
139
- CLI["mlx-bun CLI<br/>generate · eval · train"]
140
- API["any OpenAI client"]
103
+ CLI["mlx-bun CLI"]
104
+ API["any OpenAI / Anthropic / Responses client"]
141
105
  LIB["TS/Bun library import"]
142
106
  end
143
- subgraph surface["SERVING SURFACE — src/server.ts"]
144
- V1["OpenAI /v1 API"]
145
- ADMIT["admission control<br/>--memory-budget · --kv-budget"]
107
+ subgraph surface["L4 SERVING SURFACE — src/server.ts + src/serve/*"]
108
+ V1["/v1 endpoints → prepareCompletion → CompletionExecutor"]
109
+ ADMIT["admission: --memory-budget · --kv-budget"]
146
110
  end
147
- subgraph engine["ENGINE — one loop (Phase 2: concurrency IS the batch size)"]
148
- SCHED["continuous-batching scheduler<br/>--batch N = cap (default 32 after B=1 gate)"]
149
- B1["B=1 fast path<br/>pipelined + compiled step (today's serial loop)"]
150
- BN["B>=2 batched step<br/>extend-join · bit-exact vs mlx-lm at same composition"]
151
- FEAT["shared, implemented ONCE: prompt cache ·<br/>SSD KV tier · spec decode · grammar"]
111
+ subgraph engine["L3 ENGINE — GenerationGateway.place() → serial | continuous"]
112
+ SCHED["BatchScheduler (--batch N cap, default 8)<br/>B=1 adopted-cache fast path · B≥2 step"]
113
+ SERIAL["strict serial executor (generate.ts)<br/>--batch 1 and not-yet-batchable compositions"]
114
+ FEAT["shared: prompt cache · SSD tier · grammar · samplers"]
152
115
  end
153
- subgraph models["MODEL LAYER — fingerprint-routed, NEVER flag-routed"]
154
- OPT["optimized-verified graphs<br/>gemma4 12B/e4b/26B · MiniCPM5 · Qwen3/3.5"]
155
- UNI["universal graph — any HF model, best effort"]
116
+ subgraph models["L1 MODEL GRAPH — profile-routed, never flag-routed"]
117
+ OPT["dedicated / generated graphs<br/>gemma4 · MiniCPM5 · Qwen3/3.5/3.8 · Qwen3-MoE · GLM-5.2"]
118
+ UNI["universal dense graph — any supported HF arch, best effort"]
156
119
  end
157
- subgraph numerics["NUMERICS / KV SCHEMES — per-scheme oracles"]
120
+ subgraph numerics["L2 KV / QUANT SCHEMES — per-scheme oracles"]
158
121
  BF["bf16 (default) — oracle: mlx-lm"]
159
122
  UKV["uniform kv 4/8 — oracle: mlx-lm --kv-bits"]
160
123
  MKV["mixed kv_config — oracle: mlx-optiq"]
161
- LAB["LAB: TurboQuant · mixed-bpw weights ·<br/>new kernels — KL + eval gates"]
124
+ LAB["Lab: TurboQuant KV · rotation-folded weights · new kernels — KL + eval gates"]
162
125
  end
163
126
  HW["libmlx (Metal) via bun:ffi — the only runtime link"]
164
127
  PI --> V1
165
128
  CLI --> V1
166
129
  API --> V1
167
- LIB --> SCHED
130
+ LIB --> SERIAL
168
131
  V1 --> ADMIT --> SCHED
169
- SCHED -->|1 active| B1
170
- SCHED -->|2+ active| BN
132
+ ADMIT --> SERIAL
171
133
  SCHED -.-> FEAT
172
- B1 --> OPT
173
- BN --> OPT
174
- B1 --> UNI
175
- BN --> UNI
134
+ SERIAL -.-> FEAT
135
+ SCHED --> OPT
136
+ SERIAL --> OPT
137
+ SCHED --> UNI
138
+ SERIAL --> UNI
176
139
  OPT --> BF
177
140
  OPT --> UKV
178
141
  OPT --> MKV
@@ -183,592 +146,1733 @@ flowchart TB
183
146
  MKV --> HW
184
147
  ```
185
148
 
186
- How a model becomes servable, and the promotion path:
187
-
188
- ```mermaid
189
- flowchart LR
190
- Q["mlx-bun serve QUERY"] --> REG["registry (local HF cache)"]
191
- REG -->|not downloaded| GET["mlx-bun get"]
192
- GET --> REG
193
- REG --> FP["config.json fingerprint"]
194
- FP -->|optimized-verified| OPTF["custom graph for THAT model"]
195
- FP -->|anything else| UNIF["universal graph"]
196
- ```
197
-
198
- ```mermaid
199
- flowchart LR
200
- A["runs on universal"] --> B["custom per-model graph<br/>(port the oracle's exact ops)"]
201
- B --> C["bit-exact goldens vs scheme oracle"]
202
- C --> D["h2h bench >= 1.00x mlx-lm, stable pass"]
203
- D --> E["OPTIMIZED-VERIFIED badge"]
204
- E -.-> F["Lab experiments on top<br/>paired A/B + expiry"]
205
- ```
206
-
207
- **The model-layer philosophy (Josh's, verbatim intent):** the universal layer
208
- is the promise that anything from Hugging Face *runs*; the optimized-verified
209
- layer is the promise that the models we care about run *better than anywhere
210
- else*. Promotion from universal → optimized is a defined checklist: custom
211
- graph file, bit-exact parity goldens vs the scheme oracle, h2h bench entry at
212
- ≥1.00× vs mlx-lm, eval-suite scores recorded. "Optimized and verified" is a
213
- badge with teeth, and the badge list is the marketing claim.
214
-
215
- ### Layer 0 — the SSD spill substrate (Josh, 2026-07-05)
216
-
217
- Every stage that tried it found the same result: **it is almost always
218
- faster to cache to SSD and mmap it back than to regenerate, and spilling
219
- lets RAM be used as a cache instead of the only tier.** Three independent
220
- implementations already prove the pattern: the SSD KV cold tier (restart
221
- TTFT 12.1 s → 0.24 s, 0% decode overhead), MoE expert offload (page-aligned
222
- mmap, phys footprint ≈ active params), and the byte-capped prompt cache.
223
- Decision: extract the pattern into ONE generic substrate — content-addressed
224
- keys (model fingerprint + scheme + adapter + producer version), zero-copy
225
- mmap restore, byte-capped LRU eviction, corruption self-quarantine — that
226
- layers 1/2/4 consume. Future clients: vision encoder features (oMLX
227
- adoption-map #6), compiled xgrammar grammars, quantized-conversion results,
228
- TurboQuant artifacts. `src/ssd-cache.ts` is the seed to generalize.
229
-
230
- ### The batching workload (Josh, 2026-07-05)
231
-
232
- The motivating case is THE USER'S AGENT HARNESS: someone points pi / a
233
- Claude-Code-style coding agent at their local server and spins up
234
- SUB-AGENTS to work different parts of a project. Serial, each agent queues
235
- behind all the others — the Nth agent's first token waits for N−1 full
236
- generations and every agent's loop stalls. Batched, 4–8 agents decode
237
- simultaneously: each stream is somewhat slower, but nobody waits, every
238
- agent starts instantly, and the fleet's wall-clock collapses (cpm5: 345
239
- aggregate at B=4 vs 267 solo → four jobs finish in ~3.1×T with zero queue
240
- latency, vs 4×T serial with the last agent idle through three
241
- generations). That is the entire reason a single-person laptop runtime
242
- carries a continuous-batching engine. Consequences: concurrency-driven
243
- batching (sub-agents don't coordinate arrivals); **prefix sharing gains
244
- priority** (sub-agents share long system prompts — one shared KV prefix ×
245
- N rows); per-slot adapters (different agents, different specializations);
246
- and batching × quantized KV first (agent fleets want long contexts).
247
- "Single-user" in this project means single-user, many-agents.
248
-
249
- ### Layer responsibilities (the contract, 2026-07-05)
250
-
251
- 1. **Model graph** — one forward step given hidden + caches; attention
252
- dispatches on the CACHE TYPE it is handed (bf16 → ops.sdpa, quantized →
253
- quantizedSdpa); shape-generic in B; selected by fingerprint, never flags.
254
- *Responsibility: numerics of a step, for any B, for whatever cache it's
255
- given.*
256
- 2. **KV scheme (cache classes)** — the memory format and its semantics:
257
- bf16 / uniform / mixed per-layer; conversion timing (bf16 prefill →
258
- quantize populated rows — the optiq hook semantics); in batched form the
259
- multi-row layout (padding, per-row rope, merge/extract/extend/filter).
260
- *Responsibility: the scheme PER ROW — a row's bytes and math identical
261
- solo or batched. This is where each scheme's oracle binds.*
262
- 3. **Engine (scheduler)** — rows: admission, solo prefill, join/merge, the
149
+ ### Layer responsibilities (the contract)
150
+
151
+ 1. **Model graph (L1)** — one forward step given hidden + caches; attention
152
+ dispatches on the *cache type it is handed* (bf16 → `ops.sdpa`, quantized →
153
+ the quantized SDPA); shape-generic in B; selected by model profile, never by
154
+ flags. *Responsibility: the numerics of a step, for any B, for whatever
155
+ cache it is given.* The contract ENDS AT LOGITS.
156
+ 2. **KV scheme (L2, cache classes)** — the memory format and its semantics:
157
+ bf16 / uniform / mixed per-layer / TurboQuant; conversion timing (bf16
158
+ prefill → quantize populated rows, optiq's hook semantics); in batched form
159
+ the multi-row layout (padding, per-row rope, merge/extract/extend/filter).
160
+ *Responsibility: the scheme PER ROW — a row's bytes and math identical solo
161
+ or batched. This is where each scheme's oracle binds.* In code: `KvScheme`
162
+ (`src/kv-scheme.ts`, kinds `bf16 | affine-uniform | affine-config | turbo`),
163
+ the cache classes in `src/model/gemma4-base.ts`, and the batched variants in
164
+ `src/model/batched-*.ts`.
165
+ 3. **Engine (L3, scheduler)** — rows: admission, solo prefill, join/merge, the
263
166
  step loop, per-row sampling, eviction, pipelining, the concurrency cap.
264
- Never implements scheme math; calls layer-2 operations.
265
- *Responsibility: how many rows and when — never what a row's bytes mean.*
266
- 4. **Serving surface** — endpoints, per-request features (adapters,
267
- grammar, sampling, spec policy), prompt cache (keyed fingerprint +
268
- effective scheme), admission budgets. *Responsibility: requests, not
269
- tensors.*
270
-
271
- **Features decompose ALONG the layers (that's the model working, not
272
- leaking).** Per-request LoRA is the worked example: layer 1 = the math (a
273
- parameterization — `Wx + scale·B(Ax)` — oracle-gated vs mlx-lm's tuner);
274
- layer 3 = the policy (which rows run which adapter; today one active
275
- adapter per batch, per-slot adapters are the matrix item); layer 4 = the
276
- surface (`adapter` field, mount/hot-swap, discovery); plus one sideways
277
- obligation — KV under adapter A ≠ base KV, so the prompt cache keys by
278
- adapter (the SSD tier already does). When placing a feature, name its
279
- component at each layer and each component's gate.
280
-
281
- **Sampling's placement (and the debranching invariant).** The model
282
- graph's contract ENDS AT LOGITS. Sampling is *specified* at layer 4 (the
283
- request's params become a per-row sampler closure) and *executed* at
284
- layer 3 (each step, on each row's [1,V] slice; vectorized when all-greedy)
285
- — it never enters layer 1. This is the load-bearing placement behind the
286
- layer-1 debranching goal (Josh: the generated file pre-decides all ops and
287
- shapes; the path is a serial line with no branchable checks): everything
167
+ Never implements scheme math; calls layer-2 operations. *Responsibility: how
168
+ many rows and when — never what a row's bytes mean.* In code:
169
+ `src/serve/generation-gateway.ts` (placement + the single mutual-exclusion
170
+ domain), `src/serve/batch-scheduler.ts`, `src/generate.ts` (the strict
171
+ serial loop), `src/spec/serve-loop.ts` (the verify loop).
172
+ 4. **Serving surface (L4)** — endpoints, per-request features (adapters,
173
+ grammar, sampling, spec policy), prompt cache (keyed by fingerprint +
174
+ effective scheme + adapter), admission budgets. *Responsibility: requests,
175
+ not tensors.* In code: `src/server.ts`, `src/serve/request-plan.ts`,
176
+ `src/serve/completion-executor.ts`, `src/serve/completion-sink.ts`.
177
+
178
+ ### Model selection is a declared profile, not a flag
179
+
180
+ `resolveModelProfile()` (`src/model/profile.ts`) freezes, before construction,
181
+ the artifact/config identity, the fidelity target, the required engine
182
+ capabilities, and the loader/graph/loop composition. Exact artifact profiles
183
+ outrank family profiles; dedicated/generated graphs outrank the universal dense
184
+ fallback; an exact profile whose config or capabilities mismatch refuses
185
+ instead of downgrading. Profiles declare construction only — they cannot
186
+ rewrite MTP, KV scheme, adapters, grammar, or sampling (those are request
187
+ methods resolved independently). Promotion universal → dedicated is a
188
+ checklist: custom graph file, bit-exact goldens vs the scheme oracle, an h2h
189
+ entry at ≥1.00× vs mlx-lm on a stable pass, eval-suite rows recorded.
190
+
191
+ Note: `profile.ts` keeps `FidelityTier = "l1" | "l2" | "l3"`, where `l3` means
192
+ `oracle: null, claim: "measured"` — the Lab, named by its old letter. There is
193
+ no `--l3` on the CLI (it hard-errors; §7).
194
+
195
+ ### Features decompose ALONG the layers
196
+
197
+ Per-request LoRA is the worked example: L1 = the math (`Wx + scale·B(Ax)`,
198
+ oracle-gated vs mlx-lm's tuner); L3 = the policy (which rows run which
199
+ adapter; today one active adapter per batch, and adapter requests run on the
200
+ serial mechanism); L4 = the surface (`adapter` field, mount/hot-swap,
201
+ discovery); plus one sideways obligation — KV under adapter A ≠ base KV, so the
202
+ prompt cache keys by adapter namespace (`cacheNs` in `runGeneration`). When
203
+ placing a feature, name its component at each layer and each component's gate.
204
+
205
+ ### Sampling's placement and the debranching invariant
206
+
207
+ Sampling is *specified* at L4 (request params → a per-row `StepSampler`,
208
+ `src/sampler.ts`) and *executed* at L3 (each step, on each row's `[1,V]`
209
+ slice; vectorized when all-greedy) — it never enters L1. Everything
288
210
  per-request-variable is either POST-GRAPH (sampling, grammar masks, stop
289
- logic) or DATA the fixed graph consumes (adapter weights, caches). Mixed
290
- adapter/no-adapter batches stay branch-free via vLLM's Punica/SGMV pattern
291
- — every row runs the adapter math, non-adapter rows at scale 0; data, not
292
- control flow (vLLM = the oracle for per-slot LoRA batching). Debranching
293
- backlog: the per-token runtime checks still in generated files (`L > 1`,
294
- `fusedSdpaRuntimeOk`) are per-GENERATION-decidable → hoist them out of the
295
- token path; compiled decode already achieves the fully-debranched line
296
- dynamically (trace once, replay).
297
-
298
- **The option surface IS the four layers.** Users adjust: (1) how the model
299
- graph works — universal vs optimized file, compiled decode, future
300
- unrolled/fused variants; (2) how the quantization works — weights bpw,
301
- KV scheme, future TurboQuant; (3) how the scheduler works — concurrency
302
- cap, KV budget, prefill chunking, spec policy; (4) the serving surface —
303
- endpoints, adapters, structured output, sampling defaults, caches,
304
- multi-model. Flags, help text, and reference docs should be ARRANGED in
305
- these four groups.
306
-
307
- **Composition truth (2026-07-05, source-verified to the line AND
308
- empirically proven live):** "does mixed-precision KV work with batched
309
- requests?" — optiq serve as shipped: **NO** (install_mixed_kv hooks
310
- stream_generate, the serial path; mlx_lm.server 0.31.3 contains zero
311
- quantization in its 1,904 lines and BatchKVCache has no to_quantized —
312
- batchable requests decode bf16). **Live 2×2 proof** (optiq serve, cpm5,
313
- 9,132-token prompt, 128-tok decode, fresh server per arm; seeded requests
314
- route serial per mlx-lm's `_is_batchable`): batched path decode is
315
- IDENTICAL with and without --kv-config (120.5 vs 120.6 tok/s — the config
316
- is inert there), while the serial path drops 148.7 → 32.7 tok/s when the
317
- mixed hook engages (4.5× slower at this context in the shipped python
318
- pipeline). So optiq's composed reality today: batched requests silently
319
- bf16; quantized requests pay a heavy serial penalty. Phase 3.1's batched
320
- mixed-KV at full engine decode speed is an uncontested frontier point. mlx-bun
321
- today: **NO** (willBatch routes shape.kvQuant serial). mlx-bun after Phase
322
- 3.1: **YES — the build**, and a first on this stack. In layer terms the
323
- limitation is purely layer 2: the quantized schemes only have single-row
324
- implementations, so layer 3 has nothing to call — not an engine limitation,
325
- not a model limitation. Phase 3.1 = give layer 2 batched quantized caches
326
- (copy the serial per-row ops that already bit-match optiq), teach layer 3 to
327
- call them at join/evict, hand layer 1 what it already dispatches on, then
328
- delete `shape.kvQuant` from willBatch — that deletion IS the feature.
329
- Bonus finding from the same review: optiq's Mac-safe concurrency default is
330
- **8** (they warn mlx-lm's 32 OOMs unified memory under burst) — adopt 8 as
331
- the `--batch` default, superseding the earlier 32.
332
-
333
- ## 4. The engine: concurrency IS the batch size
211
+ logic) or DATA the fixed graph consumes (adapter weights, caches). This is
212
+ what keeps the generated graph a straight line. Debranching backlog, still
213
+ open: the generated gemma files carry per-token runtime checks (`L > 1 &&
214
+ fusedSdpaRuntimeOk(q, mask)` in `src/model/generated/gemma4-*.ts`) that are
215
+ per-generation-decidable and should be hoisted out of the token path; compiled
216
+ decode already achieves the debranched line dynamically (trace once, replay).
217
+
218
+ ### Layer 0 — the SSD spill substrate
219
+
220
+ It is almost always faster to cache to SSD and mmap it back than to
221
+ regenerate, and spilling lets RAM be a cache instead of the only tier. Three
222
+ implementations prove it: the SSD KV cold tier (`src/ssd-cache.ts`, a
223
+ `ColdTier` inside `PromptCache.take()` so both mechanisms restore prefixes
224
+ from disk at admission), MoE expert offload (page-aligned mmap), and the
225
+ byte-capped prompt cache. Decision: one generic substrate — content-addressed
226
+ keys (fingerprint + scheme + adapter + producer version), zero-copy mmap
227
+ restore, byte-capped LRU, corruption self-quarantine — that layers 1/2/4
228
+ consume. Future clients: vision encoder features, compiled grammars,
229
+ quantization results, TurboQuant artifacts. Economics (Josh): the SSD
230
+ competes with RECOMPUTE, not RAM — a long agent context is minutes of prefill
231
+ vs about a second of NVMe restore, and the ratio grows with context.
232
+
233
+ ### The option surface IS the four layers
234
+
235
+ Users adjust: (1) how the model graph works — profile, compiled decode; (2)
236
+ how quantization works — weight bpw, KV scheme, TurboQuant; (3) how the
237
+ scheduler works — concurrency cap, KV budget, drafting policy; (4) the serving
238
+ surface — endpoints, adapters, structured output, sampling defaults, caches,
239
+ multi-model. Flags, help text, and reference docs should be arranged in these
240
+ four groups (§7).
241
+
242
+ ## 3. Fidelity: per-scheme oracles, two axes, one decision procedure
243
+
244
+ The tier ladder is not a product mode. It is a **map from numeric scheme to
245
+ its verification oracle**:
246
+
247
+ | scheme | oracle | verification |
248
+ |---|---|---|
249
+ | bf16 (default), any batch B | **mlx-lm** at the same composition | bit-exact goldens (serial + B=2) |
250
+ | uniform kv 4/8 | **mlx-lm** `--kv-bits` (+ optiq's rotating cache class where upstream is NYI) | bit-exact (`kv-quant` tests) — `quantizedSdpaUnfused` is op-for-op mlx-lm's `quantized_scaled_dot_product_attention` |
251
+ | mixed per-layer kv (`kv_config.json`) | **mlx-optiq** `install_mixed_kv` | bit-exact (`mixed-kv-parity` test; batched rows per-row vs the serial oracle) |
252
+ | TurboQuant KV, rotation-folded weights, mixed-bpw weights, original kernels, batch-invariant kernels | **none exists** → Lab gates | KL vs our own reference path + frozen eval suite + envelope tests + kill switch |
253
+
254
+ `--l1`/`--l2` therefore name the first three rows' oracles. Mixed-precision
255
+ quantization is NOT demoted by the naked-=-L1 decision — it is the flagship of
256
+ the row that mlx-lm cannot oracle.
257
+
258
+ ### The two axes (parity gates performance)
259
+
260
+ Every optimization has two independent coordinates, and conflating them is
261
+ what made the flag surface unreadable:
262
+
263
+ 1. **Parity** — which reference does it reproduce bit-for-bit? This sets the
264
+ *lowest* tier it may live in: matches mlx-lm → L1; matches optiq but not
265
+ mlx-lm → L2; matches neither → no oracle, Lab-gated.
266
+ 2. **Performance** — is it the fastest correct way? This sets whether it is
267
+ the **default (on)** within its tier or a kept-but-off opt-in.
268
+
269
+ **Parity gates performance.** A faster kernel that breaks mlx-lm parity is not
270
+ "a faster L1" — it bubbles up to whatever tier its parity allows, and the user
271
+ opts in. An *optimization* that still matches the oracle stays low:
272
+ compiled decode is ours but is L1 because it replays the same ops bit-for-bit.
273
+ Only nodes with no oracle are forced into the Lab.
274
+
275
+ ### Decision procedure for any new optimization
276
+
277
+ 1. **Measure its parity** (run both ways, compare — one flag, one measurement
278
+ per lever). The lowest oracle it matches bit-for-bit is its tier ceiling.
279
+ 2. **Measure its performance** vs that tier's current default on a stable
280
+ pass:
281
+ - faster AND holds the guarantee → becomes the **default (on)** for the tier;
282
+ - slower but still correct → a documented **default-off kill switch**
283
+ (useful for A/B; never the default);
284
+ - breaks the guarantee but offers a real trade-off → a **Lab item**
285
+ (env flag, bench script, expiry); earns a default only by beating the L1
286
+ baseline in a paired A/B (+ KL PASS if output-changing).
287
+
288
+ Worked: *5% less memory, still bit-for-bit* → default-on in L1. *2× tok/s at
289
+ KL 0.0015* → Lab item. *Another way, 3% slower, still correct* → kept, off.
290
+
291
+ ### Composition rule: a composition inherits its scheme's oracle
292
+
293
+ Batching, prompt cache, spec decode, adapters — none change a scheme's
294
+ numerics, so composing them with a scheme verifies against THAT scheme's
295
+ oracle, per row. Batched bf16 anchors to mlx-lm B=N goldens; batched mixed-KV
296
+ anchors to optiq's per-row mechanics (our serial `maybeQuantizeKv` is the
297
+ verified op-for-op copy) and gates on per-row bit-exactness of the unpadded
298
+ row plus a calibrated envelope for padded rows (their noise is pre-existing
299
+ bf16 reduction order under grid snapping, measured at the pinned join step).
300
+ Do NOT invent Lab-style KL/teacher-forced gates for a composition of an
301
+ oracle-backed scheme; the Lab is only for schemes with no oracle at all.
302
+
303
+ ### Flag classification (from the tier framing)
304
+
305
+ A flag falls into exactly one of three buckets:
306
+
307
+ | flag selects… | tier effect | what it IS | action |
308
+ |---|---|---|---|
309
+ | the only viable route | — | not a choice | always-on default; no flag |
310
+ | between two routes at the same tier / same oracle | none | a kill switch (A/B, debugging) | default the fast one; keep the slow one documented |
311
+ | between an oracle-backed route and a no-oracle route | tier ↔ Lab | a real parity ⇄ optimization knob | keep, document as such, group by layer |
312
+
313
+ Training flags classify the same way (verified in `src/cli.ts` train help):
314
+ `--grad-clip` is an always-on guard; `--seg N` / `--no-segment` is a
315
+ memory↔compute knob, bit-exact either way; `--grad-accum` and `--lambda` are
316
+ training-quality/hyperparameter knobs; `--no-flash` (flash-CCE head → the
317
+ MLX fused head) and `--no-prefix` (prefix-shared forward → two-forward) are
318
+ the only true parity ⇄ optimization toggles on that surface. The 2026-06-21
319
+ "parity-tier DAG" HTML map that motivated this classification
320
+ (`docs/dag/training-inference-map.html`) no longer exists in the tree; the
321
+ roadmap it proposed — derive the graph from code, CI-enforce "an L1-tagged
322
+ node has a passing bit-exact test", shrink the no-oracle surface, generate the
323
+ flag surface from the graph — remains an idea, not a phase.
324
+
325
+ ### Lab lifecycle rule (the anti-flag-pile law)
326
+
327
+ Every Lab experiment ships with (a) a hypothesis in frontier-axis terms, (b) a
328
+ paired A/B bench script, (c) an expiry review date. Outcomes: promote to
329
+ default (with stable-pass numbers) or delete (with a breadcrumb in the design
330
+ doc). No third state. "Off in every regime with no promotion path" is a
331
+ deletion that hasn't happened yet.
332
+
333
+ ## 4. Decision: the naked default IS L1 (2026-07-05, Josh)
334
+
335
+ The 2026-07-05 h2h pass showed the L1 kernel set at decode parity with mlx-lm
336
+ on every model while every output-changing lever failed to beat it in a paired
337
+ A/B, and quantized KV measured slower than bf16 at ≤16k on both stacks (it
338
+ buys memory headroom only). The record with numbers is PLAN.md "Decision:
339
+ naked default = --l1".
340
+
341
+ - **Naked = `--l1`.** `applyDecodeRoute` (`src/cli.ts`) defaults the tier to
342
+ `l1`: bf16 KV, compiled decode on, compiled activations on, fused SDPA off.
343
+ - **Prior perf-optimization work is untrusted until re-proven** from this
344
+ baseline (the losers were deleted the same day — §11 History).
345
+ - **The bar for any lever to earn a default back:** a paired A/B win vs L1 on
346
+ a stable pass (no `unstable` tag), plus KL PASS if output-changing.
347
+ - **Explicit `--kv-quant` picks its oracle's composition**: `config` → optiq's
348
+ fused prefill SDPA + stock unfused decode (the L2 golden composition);
349
+ uniform `4|8` → unfused (mlx-lm's algorithm, L1-eligible); `turbo[:k.v.]` →
350
+ dequantize-on-fetch through stock `ops.sdpa` (Lab). `--fused-sdpa` still
351
+ overrides either.
352
+ - **`--l1`/`--l2` are pure aliases.** Each expands to a fixed set of per-fork
353
+ values (`TIERS` in `applyDecodeRoute`), installed through
354
+ `configureRuntime()` into the immutable runtime snapshot before model modules
355
+ load; a per-fork flag overrides one value. Two invariants: a user can
356
+ reproduce any tier byte-for-byte from individual flags, and no preset may set
357
+ anything that isn't also an individual flag.
358
+
359
+ ## 5. The engine: concurrency IS the batch size
334
360
 
335
361
  **Decision (Josh, 2026-07-05): batching is determined by how many concurrent
336
- requests are in flight, not by a flag.** One request = batch of 1 at full
337
- serial speed; N requests = continuous batching; requests join/leave running
338
- batches (the extend-join machinery already landed and is token-exact vs
339
- mlx-lm B=2).
340
-
341
- This **reverses** batching-v2-plan.md's standing decision (`--batch N` as a
342
- mode switch, auto-batching rejected for determinism). The old objection was
343
- "an idle-vs-loaded server produces different numerics for the same request."
344
- It gets a real answer, not a shrug:
345
-
346
- 1. **The L1 oracle itself behaves this way.** mlx_lm.server auto-batches by
347
- default; "drop-in for mlx-lm" now *requires* concurrency-driven batching.
348
- Our parity contract becomes: **bit-exact to mlx-lm at the same batch
349
- composition** — already golden-verified at B=1 and B=2.
350
- 2. Anyone who needs load-independent numerics pins the cap to 1 (see below).
351
- 3. **Batch-invariant kernels** (identical numerics regardless of B — the
352
- Thinking-Machines-style program) go to the Lab as a research item; if that
353
- ever lands, determinism-under-load comes back for free and it's an
354
- arXiv-lens result on Apple Silicon.
355
-
356
- **Flag decision (Josh, 2026-07-05): the flag stays `--batch` — no rename.**
357
- Its *semantics* change from mode switch to cap: `--batch N` = maximum
358
- concurrent rows (mlx-lm's `--decode-concurrency` twin), **default 32**, and
359
- `--batch 1` is the force-serial / determinism pin. TIMING: the 32 default
360
- lands **with Phase 2, not before** — flipping it while the batched lane is
361
- still 1.8× slower at B=1 would regress every single-request user ~44%. Until
362
- GATE-B1-SPEED passes, `--batch` keeps today's default (1 = the serial lane).
363
-
364
- **The crux gate — the B=1 gap.** Today the batched lane at B=1 runs cpm5 at
365
- ~149 tok/s vs 267 serial (batching-perf-path.md): the unified engine is
366
- **1.8× off** at the composition that matters most for a single-user runtime.
367
-
368
- **Measured 2026-07-05 (paired in-process A/B, cpm5, M1 Max): mlx-lm's own
369
- `BatchGenerator` at B=1 runs 256.5 tok/s vs 264.6 for its `stream_generate`
370
- — a 3.3% tax on the most host-tax-sensitive model we have.** Two
371
- consequences: (1) the unified design is *proven achievable* — a continuous-
372
- batching loop can serve a lone request at ~0.97× of the best serial loop,
373
- so our lane's 1.8× gap is an implementation artifact, not an inherent cost
374
- of the architecture; (2) our h2h 1.00× parity numbers were measured against
375
- `stream_generate` (mlx-lm's FASTEST single-stream path), so the baseline we
376
- match is not batched-handicapped — and `mlx_lm.server` actually serves a
377
- single request ~3% below the number we already match. The serial lane
378
- cannot be deleted until:
379
-
380
- - `GATE-B1-SPEED`: unified engine at B=1 ≥ 99% of today's serial decode tok/s
381
- and TTFT on cpm5 + e4b + 12B (stable bench pass), and
382
- - `GATE-B1-PARITY`: unified engine at B=1 bit-exact vs the L1 goldens (full
383
- logprobs, not just greedy).
384
-
385
- How to close 1.8×: the serial lane's wins are known and portable — the
386
- pipelined decode loop (async-eval overlap), compiled per-step graph replay,
387
- and no per-token scheduler hops. The likely landing: **the scheduler owns
388
- admission and join/leave; when exactly one sequence is active it executes the
389
- same pipelined/compiled step the serial path runs today** (B=1 specialization
390
- inside one engine — a fast path, not a second lane), falling back to the
391
- general batched step at B≥2. Same public machinery, no gateway routing rules,
392
- no feature×lane matrix.
393
-
394
- **COMPOSITION IS THE PRODUCT (Josh, 2026-07-05).** The goal state, verbatim
395
- intent: *"you start with the server, it serves an optimized version of the
396
- model, you add on top of that mixed-precision quantized KV cache, as well as
397
- LoRA adapters, and you add on top of that DSpark speculative decoding, and
398
- structured output potentially, and then you also can switch the sampling
399
- method and values."* Every serving capability is a STACKABLE layer on one
400
- engine — never a lane-routing condition, never mutually exclusive. The
401
- current gateway is the anti-pattern: `willBatch` routes vision / adapters /
402
- logprobs / seeds / kv-quant / drafts to a different lane, which is exactly
403
- the "batching OR performance" choice being killed.
404
-
405
- The composition matrix the unified engine must satisfy (each row = a layer;
406
- any subset of rows must stack):
407
-
408
- | layer | today | end state |
362
+ requests are in flight, not by a flag.** One request = a lone row at serial
363
+ speed; N requests = continuous batching; rows join and leave running batches.
364
+ This reversed `docs/design/batching.md`'s "`--batch N` is a mode switch" decision.
365
+ The old objection — "an idle vs loaded server produces different numerics for
366
+ the same request" — gets a real answer:
367
+
368
+ 1. **The L1 oracle itself behaves this way.** `mlx_lm.server` auto-batches;
369
+ "drop-in for mlx-lm" *requires* concurrency-driven batching. The parity
370
+ contract is **bit-exact to mlx-lm at the same batch composition**.
371
+ 2. Anyone who needs load-independent numerics pins `--batch 1`.
372
+ 3. **Batch-invariant kernels** (identical numerics regardless of B) are a Lab
373
+ research item; if they land, determinism-under-load returns for free.
374
+
375
+ **The flag stays `--batch`** (no rename); its semantics are the cap on
376
+ concurrent rows (`--decode-concurrency` is accepted as the mlx_lm.server
377
+ alias). **Default 8** (`serverOptions.batch ?? 8` in `src/server.ts` and
378
+ `src/cli.ts`) — optiq's Mac-safe concurrency default and the 4–8-sub-agent
379
+ workload, superseding the earlier 32. `--batch 1` pins the strict serial
380
+ executor. An older draft of this doc and its diagram said 32; the code says 8.
381
+
382
+ **The workload this is for**: a user's agent harness pointing sub-agents at
383
+ one local server. Serial, the Nth agent's first token waits for N−1 full
384
+ generations; batched, every agent starts instantly and the fleet's wall-clock
385
+ collapses. Consequences: concurrency-driven batching (sub-agents don't
386
+ coordinate arrivals), prefix sharing gains priority (shared system prompts),
387
+ per-slot adapters, and batching × quantized KV first (long contexts).
388
+ "Single-user" in this project means single user, many agents.
389
+
390
+ ### The B=1 fast path (the crux gate, met)
391
+
392
+ The unified engine could not replace the serial lane until a lone request
393
+ through the scheduler matched serial decode speed and bits. mlx-lm's own
394
+ `BatchGenerator` at B=1 runs within a few percent of its `stream_generate`
395
+ (paired A/B, PLAN.md), so the design was proven achievable; our gap was an
396
+ implementation artifact, closed in two moves (both are now design contracts,
397
+ §8): readbacks in a pipelined loop must not create ops, and a lone row keeps
398
+ bare serial-class caches so the step dispatches the same graph the serial loop
399
+ builds. Today `BatchScheduler` runs, for `B === 1 && unpadded && supports(inners)`
400
+ with a uint32 pipeline register, the serial engine's `CompiledDecode` step
401
+ (adopt-don't-copy: a row joining an empty batch keeps its solo caches as the
402
+ inners; the merge copy runs only when a second row actually joins). The
403
+ recorded scheduler/serial paired decode ratios are 0.992–0.996 (PLAN.md
404
+ "Serving architecture consolidation"), and `GATE-B1-PARITY` (bit-exact vs the
405
+ L1 goldens through the scheduler) is green on the curated probes.
406
+
407
+ ### Composition is the product
408
+
409
+ Every serving capability is a STACKABLE layer on one engine — never a
410
+ lane-routing condition, never mutually exclusive. The matrix the engine must
411
+ satisfy, with the state of `GenerationGateway.#supportsContinuous` as read on
412
+ 2026-08-23:
413
+
414
+ | layer | today (src) | end state |
409
415
  |---|---|---|
410
- | optimized per-model graph | ✅ both lanes | ✅ (fingerprint-routed, unchanged) |
411
- | mixed-precision quantized KV | serial only | one active scheme per server, applies at any B |
412
- | LoRA adapters (hot-swap, per-request select) | serial only (compiled decode also skips them) | per-slot adapter state in the batched step |
413
- | speculative decoding (two-model + assistant + DSpark, 2026-07-06) | serial only, forces ALL requests serial; the `DraftSource` seam is now KV-borrowing-ready (target donor-KV / anchor-hidden / tapped H_ctx flow through it — the per-slot substrate is in place) | per-slot drafting behind the `DraftSource` seam |
414
- | structured output (grammar) | ✅ both lanes (B1 per-row matchers) | ✅ (already composes; keep the conformance gate) |
415
- | sampling method + values (temp/top-p/top-k/min-p/XTC/penalties/HLG/seed) | per-row samplers batch; seeds force serial | fully per-row, seeds included |
416
- | prompt cache / SSD tier | serial only | prefix reuse for batched rows |
417
-
418
- Ordering (by value): quantized KV under batching first (the
419
- agent-fan-out-with-long-context composition), then prompt cache, then
420
- adapters, then spec decode per slot, then chunked prefill interleaving
421
- (vLLM policy) so a 16k prefill doesn't stall running streams.
422
- `scripts/bench-modes.ts` is already the composition scoreboard (its cell
423
- matrix spans kv × batch × grammar × cache × spec) — every layer added to
424
- the batched engine gets a cell there and a parity gate.
425
-
426
- ## 5. Fidelity: per-SCHEME oracles (fixes "L1 can't oracle mixed")
427
-
428
- The tier ladder stops being a product mode and becomes what it always really
429
- was — a **map from numeric scheme to its verification oracle**:
416
+ | dedicated per-model graph | both mechanisms (profile-routed) | unchanged |
417
+ | mixed-precision KV (`affine-config`) | continuous when every configured layer is a plain or rotating KVCache the scheme can convert (`KvScheme.batchable` + the gateway's cache probe); uniform `kvBits`, SSM-layer configs, and TurboQuant route serial | one active scheme per server, any B |
418
+ | LoRA adapters | serial only (`shape.hasAdapters`) | per-slot adapter state (vLLM Punica/SGMV pattern: every row runs the adapter math, non-adapter rows at scale 0) |
419
+ | speculative decoding (two-model · assistant · dspark · deepspec · native MTP · ngram) | serial only; a mounted draft routes every request serial (`shape.hasDraft`, mlx_lm.server's `is_batchable = draft is None`); GLM-5.2 MTP is on by default and pins the serial verify lane (`--mtp off` restores batching) | per-slot drafting behind the `DraftSource` seam |
420
+ | structured output (grammar) | both (per-row matchers; `MLX_BUN_GRAMMAR_BATCH=0` forces serial) | unchanged; keep the conformance gate |
421
+ | sampling (temp/top-p/top-k/min-p/XTC/penalties/HLG/seed) | per-row `StepSampler` batches, including repetition penalty and logits extras; a user-fixed `seed` forces serial (mlx-lm's `_is_batchable`) | fully per-row, seeds included |
422
+ | logprobs capture | serial only (`shape.wantsLogprobs`) | per-row readback in the scheduler |
423
+ | vision / audio media prompts | serial only (`shape.hasVision`; embeddings-prefill bypasses the prompt cache) | batched media prefill |
424
+ | prompt cache / SSD tier | both: scheduler joiners `take()` the longest usable prefix at admission, never-merged rows `put()` back on finish; the cold tier restores inside `take()` | block-granular sharing across rows |
425
+ | paged KV (`--paged-kv`) | serial only, Gemma4 bf16, refuses `--batch N>1`, `--kv-quant`, `--draft-model`; bypasses the prompt cache | block-level CoW prefix sharing (vLLM oracle) |
426
+
427
+ Ordering of the remaining rows (by value): adapters per slot, then spec per
428
+ slot, then logprobs, then chunked prefill interleaving (vLLM policy) so a long
429
+ prefill doesn't stall running streams. `scripts/bench-matrix.ts modes` is the
430
+ composition scoreboard — every layer added to the scheduler gets a cell and a
431
+ parity gate.
432
+
433
+ ## 6. Request path today
434
+
435
+ Modules in order, as read in `src/` on 2026-08-30:
436
+
437
+ 1. **The request pipeline** (`src/serve/`). A request is data; each stage
438
+ is one program with a declared input and output, composed in order by
439
+ `src/server.ts` (which otherwise only assembles the server — KV scheme,
440
+ prompt cache + SSD tier, gateway, admission — and dispatches routes):
441
+ `new ChatRequest(body)` (`chat-request.ts`; validation at construction,
442
+ mlx-lm's 400 messages) → `ChatStage.run` (`chat-stage.ts`; template render
443
+ + prompt ids, media embeddings by family, grammar, adapters, sampling
444
+ defaults via `request-prep.ts`) → `InferenceRequest` (`inference-request.ts`)
445
+ → `InferenceStage.admit` (memory ceiling → reject or clamp; seals the plan)
446
+ → `InferenceStage.run` → `InferenceResult` → wire. `/v1/completions` is
447
+ `TextCompletionStage` into the same inference stage; `/v1/messages` and
448
+ `/v1/responses` reuse `ChatStage` and format the result themselves. One
449
+ JSON/SSE writer (`http.ts`) serves every surface; the protocol object
450
+ (`openai-wire.ts`, `src/anthropic.ts`, `src/responses.ts`) is the only
451
+ thing that differs.
452
+ 2. **`src/serve/completion-executor.ts` `prepareCompletion()`** → **`src/serve/request-plan.ts` `planRequest()`** — admission (memory budget → reject or clamp `maxTokens`), capture options, adapter selection, and the `RequestShape` are derived ONCE for chat and raw-text, streaming and non-streaming. The result is an opaque, single-use `PreparedCompletion`; adapters cannot inspect or rewrite the plan. Owned resources (media arrays, grammar) dispose on rejection.
453
+ 3. **`CompletionExecutor.execute()`** — owns one completion attempt: asks the engine for placement, records the lane (`src/serve/lane-registry.ts`), builds the `CompletionSink` (semantic events: content / reasoning / tool calls / stop), collects logprobs, transfers resource ownership exactly once, runs, and settles the terminal summary (finish reason, usage, cached tokens, final lane, speculation stats). Adapters only format that summary.
454
+ 4. **`src/serve/generation-gateway.ts` `GenerationGateway.place(shape)`** — freezes one `GenerationPlacement { shape, mechanism: "serial" | "continuous" }`. `#supportsContinuous` is a capability check only (§5 matrix); it never rewrites MTP, KV scheme, TurboQuant, grammar, adapters, or sampling. `run()` rejects a placement made for another shape. One `AsyncMutex` is the single mutual-exclusion domain: a serial run holds it; the scheduler holds it for its whole active period; a waiting serial request drains the batch (mlx-lm's `drain_batch`).
455
+ 5. **`continuous` →** `src/serve/batch-scheduler.ts` — admission under `--kv-budget`, chunked interleaved solo prefill, join/merge/extend/filter over per-layer cache types, the pipelined step loop, per-row `StepSampler`, eviction. B=1 takes the adopted-cache fast path (§5). **`serial` →** `runGeneration` in `src/server.ts` → `src/generate.ts` (prompt-cache take/put, snapshot boundary, the chunked prefill, the pipelined compiled decode loop) or, when a draft is mounted and the request is spec-eligible, `src/spec/serve-loop.ts`.
456
+
457
+ **The single-path rule.** Options are *declarations* carried through
458
+ placement, never forks above the scheduler. A request's resolved settings
459
+ (KV scheme, TurboQuant, drafting, grammar, adapters, sampling) are authoritative
460
+ from `planRequest` onward; `place()` may only answer "does the continuous
461
+ mechanism implement exactly this composition?" — it may not substitute a
462
+ cheaper composition to make a request batchable (the optiq bug class: silently
463
+ serving bf16 under a quantized scheme). The scheduler independently refuses a
464
+ scheme it cannot execute at construction, and `KvScheme` freezes its per-layer
465
+ entries so a caller cannot mutate conversion or accounting after resolution.
466
+
467
+ **Known deviations from the single-path rule (verified 2026-08-23):**
468
+
469
+ 1. **Two prefill loops.** `src/generate.ts` (the chunk loop with the
470
+ `snapshotAt` split, the mlx-lm tail-split convention
471
+ `min(prefillChunkSize, remaining-1)`, per-chunk `clearCache`) and
472
+ `BatchScheduler.#prefillChunk` in `src/serve/batch-scheduler.ts` (the same
473
+ conventions re-implemented over `PrefillState` for interleaved admission).
474
+ Both are gated bit-exact, but a convention change must land in two places.
475
+ 2. **Spec eligibility is decided after placement.** `planRequest` only knows
476
+ `hasDraft = !!ctx.draft` (server-level), which routes every request serial
477
+ while a draft is mounted. The real per-request eligibility — text-only, no
478
+ adapters, no logprobs capture, bf16 KV (no `kvBits`/`kvConfig`/`turboQuant`),
479
+ not paged — is evaluated inside `runGeneration` after `place()`; ineligible
480
+ requests silently fall through to plain serial decode, and the lane label is
481
+ corrected from `serial+spec` to `serial` only when `stats.spec` is absent
482
+ (`finalLane` in the executor). Resolving this means lifting the eligibility
483
+ predicate into `RequestShape` so placement and the lane label are decided
484
+ once.
485
+
486
+ Other seams worth knowing: `runGeneration` also decides the prompt-cache
487
+ bypass (media, paged) and the adapter cache namespace; `--isolate` wraps the
488
+ ENTIRE server as an engine child behind a proxy (`src/serve/isolate.ts`,
489
+ `ModelPool` for `--model-pool` LRU residency and model switching by the
490
+ request's `model` field) — the inter-process API is the /v1 surface itself, no
491
+ second protocol.
492
+
493
+ ## 7. The flag surface (src truth, grouped by layer)
494
+
495
+ Read from `SERVER_FLAGS` and `applyDecodeRoute` in `src/cli.ts`. Every
496
+ `MLX_BUN_*` value is read through the immutable runtime snapshot
497
+ (`src/runtime-config.ts` `runtimeValue/runtimeFlag`, `src/flags.ts` `flagOn`);
498
+ feature code never reads `process.env`, and the CLI installs tier/fork
499
+ overrides with `configureRuntime()` before model modules load.
500
+
501
+ **Parity tier (aliases):** `--l1` (default) · `--l2`. `--l3` hard-errors with a
502
+ pointer to this doc.
503
+
504
+ **L2 — quantization:** `--kv-quant config|off|4|8|turbo[:k<bits>v<bits>]`
505
+ (default off). An older draft of this doc listed only `config|4|8|off`; the
506
+ `turbo` axis is real and mutually exclusive with the affine modes.
507
+
508
+ **L3 — scheduler / decode policy:** `--batch <n>` (default 8;
509
+ `--decode-concurrency` alias) · `--kv-budget <GB>` · `--draft-model <query>` ·
510
+ `--draft-kind two-model|assistant|dspark|deepspec|mtp|ngram` ·
511
+ `--num-draft-tokens` · `--ngram-max` / `--ngram-min` · `--mtp on|off`
512
+ (GLM-5.2 native MTP, on by default) · `--context-length` (GLM-5.2 reservation)
513
+ · `--paged-kv` / `--paged-kv-block-size` (env `MLX_BUN_PAGED_KV=1`).
514
+
515
+ **L4 — serving surface:** `--model` · `--host` · `--port` · `--memory-budget`
516
+ · `--prompt-cache <GB>` · `--ssd-cache <dir>` · `--ssd-cache-max` ·
517
+ `--ssd-demote-idle` · `--ssd-cache-verify` · `--isolate` · `--model-pool` ·
518
+ `--unix` (internal) · `--no-open` · `--allow-private-media` · `--adapter`
519
+ (`--adapter-path` alias) · `--thinking` · `--temperature`/`--temp` · `--top-p`
520
+ · `--top-k` · `--max-tokens` · the HLG sampler family (`--hlg-sampling`,
521
+ `--hlg-width`, `--hlg-shoulder`, `--hlg-toe`, `--hlg-pivot-offset`).
522
+
523
+ **Kill switches (bit-exact A/B levers, each selects a slower same-parity
524
+ path):** `--compiled-decode on|off` (`MLX_BUN_COMPILED_DECODE`) ·
525
+ `--compiled-activations on|off` (`MLX_BUN_COMPILED_GEGLU` +
526
+ `MLX_BUN_COMPILED_SWIGLU`; only gemma geglu and MiniCPM5 swiglu have an
527
+ uncompiled form — qwen3/qwen3.5/universal compile unconditionally) ·
528
+ `--fused-sdpa on|off` (`MLX_BUN_NO_FUSED_SDPA`, inverted; default follows
529
+ `--kv-quant`) · `--force-wire` (`MLX_BUN_FORCE_WIRE`) · `--expert-offload`
530
+ (`MLX_BUN_EXPERT_OFFLOAD`). An older draft said these were env-only; the three
531
+ decode kill switches are CLI flags as well.
532
+
533
+ **Lab / diagnostic env flags (no CLI flag; live with a bench and an expiry):**
534
+ `MLX_BUN_GRAMMAR_JUMP=1` (jump-forward decoding, serial non-spec loop),
535
+ `MLX_BUN_GRAMMAR_BATCH=0` (grammar back to serial), `MLX_BUN_BATCH_SSM=0`
536
+ (SSM caches back to serial), `MLX_BUN_BATCH_NO_PIPELINE`,
537
+ `MLX_BUN_BATCH_STEP_TRACE`, `MLX_BUN_BATCH_VEC_SAMPLE`, `MLX_BUN_BATCH_EXTEND`,
538
+ `MLX_BUN_PREFILL_TAIL_SPLIT`, `MLX_BUN_FLASH_MIN_M`, the `MLX_BUN_CCE_*`
539
+ training-kernel switches, and the memory/trace loggers. Anything
540
+ output-changing is Lab by definition.
541
+
542
+ **Deleted (2026-07-05, §11):** `--fused-decode`, `--fused-gelu`,
543
+ `--perf-kernel`, `--l3`, and their env twins. None exist in `src/`.
544
+
545
+ ## 8. Design invariants proven en route (durable, not changelog)
546
+
547
+ - **Readbacks in a pipelined loop must not create ops.** `toFloat32()` on the
548
+ int token array enqueued a cast BEHIND the next dispatched step, stalling the
549
+ "overlapped" read a full GPU step per token. Use `MlxArray.toIntTokens()`.
550
+ - **A lone row keeps bare serial-class caches.** `KVCache.makeMask(1)` is the
551
+ empty mask and scalar rope; the batched step then dispatches the same graph
552
+ the serial loop builds. The per-layer `BatchedDecodeMaskCache` wrapper is
553
+ only for padded batches.
554
+ - **Rope-array step-stability contract.** A batched cache's `ropeOffsetArr`
555
+ must be stable within a decode step and refresh only at `releaseRopeArr()`;
556
+ re-reading it mid-update ropes K and Q one position apart.
557
+ - **Generated-file guards accept batched subclasses.** The generated forwards'
558
+ per-layer `instanceof` guards pass any batched cache that subclasses a
559
+ serial class, so an all-quant gemma batch decodes through the generated
560
+ fast path — a feature (B=1 proved bit-exact) but a contract: batched caches
561
+ must behave serially under re-reads. Test with FULL configs; one bf16 layer
562
+ anywhere fails the guard and drops to the monolith.
563
+ - **Quantization packs along HEAD_DIM**, so token-axis batch surgery over
564
+ (packed, scales, biases) triples is byte-safe; solo rows convert at serial
565
+ chunk boundaries (bit-exact by construction).
566
+ - **A scheme-less path must refuse, never silently drop quantization.** The
567
+ gateway only threads a `KvScheme` to the scheduler when it is batchable; the
568
+ scheduler refuses an unsupported scheme at construction.
569
+ - **Prefix sharing is non-consuming.** `PromptCache.take()` serves zero-copy
570
+ clones (ref-counted retain so a demoted donor never unmaps pages a clone
571
+ reads); the donor stays put; `put()` supersedes same-namespace prefix
572
+ ancestors when the new entry is trimmable. Kills the cannibalization flaw
573
+ (agent B consuming agent A's entry). v1 shares COMPUTE and durability;
574
+ concurrent rows still hold separate physical KV — one shared physical prefix
575
+ across rows is the block-KV frontier item, and whole-entry duplication in
576
+ the disk tier is the block-granularity revisit trigger.
577
+ - **Prompt-boundary snapshot.** The prompt+generation entry is untrimmable
578
+ past a wrapped ring or under quantized KV, so every substantial request also
579
+ snapshots a strict prompt prefix (cap `len-1`), the mlx-lm
580
+ `insert_segments` invariant; a re-rendered next turn always matches.
581
+ - **Never a JS callback as an mlx buffer destructor** — last-ref `Data` dtors
582
+ run on the Metal completion thread (native `dlsym(free)` dtor, process-pinned
583
+ mmaps).
584
+ - **Isolation proxies the whole server**, not the gateway: grammar WASM,
585
+ vision arrays, and sampler closures don't serialize; the /v1 surface is the
586
+ IPC.
587
+
588
+ ## 9. The frontier program (order matters)
430
589
 
431
- | scheme | oracle | verification |
590
+ Ranked by expected frontier shift (memory ↓ / intelligence ↑ first, then
591
+ speed). Each item is a Lab program with its own doc; `decode-speed-program.md`
592
+ ranks the speed levers and is the only doc that does.
593
+
594
+ 1. **Mixed-precision weights** — knapsack `--target-bpw` (ours; OptiQ-style
595
+ sensitivity port in `src/quantize/sensitivity.ts`) and rotation-folded
596
+ quantization (`docs/design/turboquant.md`, `--rotate-weights`). Gate:
597
+ perplexity + frozen 6-task eval at equal bpw.
598
+ 2. **TurboQuant KV** (`docs/design/turboquant.md`, landed v1) — orthogonal to
599
+ allocation: mixed precision is the ALLOCATION axis, TurboQuant the
600
+ QUANTIZER axis; they compose. Solo-only in v1.
601
+ 3. **Speculative decoding depth** — DFlash/DSpark, native MTP, behind the
602
+ `DraftSource` seam (`docs/design/speculative-decoding.md`).
603
+ 4. **Per-model graph work from the baseline** — unroll a model's flat DAG,
604
+ find fusion the compiler misses, prove with kernel-trace diffs, promote per
605
+ model.
606
+ 5. **Batch-invariant kernels** (research) — restores determinism under load.
607
+
608
+ ## 10. Decisions log and open items
609
+
610
+ 1. **Cap default 8** — flipped 2026-07-05 after the B=1 gates; `--batch 1`
611
+ pins arrival-independent numerics; kv-budget admission keeps 8 safe on
612
+ small boxes.
613
+ 2. **Flag stays `--batch`**; semantics = cap.
614
+ 3. **Perf kernel deleted**; a future flash-decode kernel re-derives from the L1
615
+ baseline in the Lab — no resurrection.
616
+ 4. **`--l1`/`--l2` remain user-facing CLI aliases** (`src/cli.ts` documents
617
+ them under "Parity tier"). The 2026-07-05 intent that they become
618
+ bench/test vocabulary only has not been executed; if it is, `serve --help`,
619
+ `docs/reference/cli.md`, and `server-config.md` change in the same commit.
620
+ 5. **Block-paged KV**: an optional serial-only v1 exists (`--paged-kv`,
621
+ `docs/design/kv-cache.md`); padded-batch waste removal and block CoW prefix
622
+ sharing are its follow-ups, triggered when prefix sharing under batching
623
+ becomes the bottleneck.
624
+ 6. **Prefix sharing scope**: must extend to the disk tier; first-class cases
625
+ are new-session spin-up and server restart — the shared prefix is a durable
626
+ object, not a property of one conversation's entry.
627
+ 7. **Open composition rows** (§5): adapters, spec, logprobs, media on the
628
+ continuous mechanism; chunked prefill interleaving as policy.
629
+ 8. **Open single-path deviations** (§6): unify the prefill loops; move
630
+ prompt/media/grammar construction behind the seam; lift spec eligibility
631
+ into `RequestShape`.
632
+ 9. **Debranching backlog** (§2): hoist the generated files' per-token
633
+ `L > 1 && fusedSdpaRuntimeOk` checks.
634
+
635
+ ## 11. History
636
+
637
+ **2026-07-04 — faithful → L1 consolidation.** An audit found three
638
+ overlapping "match mlx-lm" mechanisms that disagreed: the `--l1/--l2/--l3`
639
+ tier presets, an `MLX_BUN_FAITHFUL` preset that forced the gemma monolith and
640
+ set choices no flag could reach, and a family of unwired `Faithful*` model
641
+ subclasses. The decisive rule that collapsed them: **"compiled" kernels
642
+ (`@mx.compile` geglu/swiglu, compiled decode) go through the same libmlx as
643
+ mlx-lm → bit-exact and faster → L1 defaults; custom "fused" Metal kernels were
644
+ mlx-bun originals with a proven residual → not bit-exact.** Landed in four
645
+ phases: compiled activations default-on everywhere (qwen3, qwen3.5, universal
646
+ dense, gemma geglu via `MLX_BUN_COMPILED_GEGLU`, MiniCPM5 swiglu), verified
647
+ `maxDiff === 0` vs mlx-lm; `--compiled-activations` and `--fused-gelu` added as
648
+ per-fork flags so `--l1` became a pure alias; `MLX_BUN_FAITHFUL`, `faithful.ts`
649
+ (→ `flags.ts`, only `flagOn` kept), and the four unwired subclasses deleted;
650
+ docs + the "cost of removing each faithful kernel" bench matrix. The only
651
+ family that had it right from the start was `qwen3_moe`: the production class
652
+ IS the faithful port. Bit-exact-to-mlx-lm *wants* compiled activations (mlx-lm
653
+ `@mx.compile`s them), so the earlier `--l1` "unfused" help text was backwards.
654
+
655
+ **2026-07-05 — the Phase-1 deletion pass** (same day as the naked-=-L1
656
+ decision; ~50 files touched, 23 deleted, suite green). Josh: "we will end up
657
+ redoing all the work we have currently done given that we now have a different
658
+ starting point." Deleted, one funeral each:
659
+
660
+ - `--fused-decode` / `MLX_BUN_FUSED_DECODE` — 1.00×, forced uncompiled decode,
661
+ a silent-wrongness footgun.
662
+ - `--fused-gelu` / `MLX_BUN_FUSED_GELU` and `MLX_BUN_FUSED_SWIGLU` (+
663
+ `fused-mlp-kernel.ts`, `steel-linear-kernel.ts`, the swiglu/geglu/steel
664
+ one-off scripts) — +0–1%; the compiled closures already own the fusion win
665
+ bit-exactly.
666
+ - `--perf-kernel` / `MLX_BUN_PERF_KERNEL` (the fused online-softmax decode
667
+ SDPA) with its frozen-oracle scaffolding (`perf-kernel-oracle.test.ts`,
668
+ `freeze-perf-oracle.ts`) — regressed e4b in the paired A/B; its one win
669
+ carried a KL WARN. Its earlier placement in the L2 preset (commit `f1bf5cc`)
670
+ had claimed a bit-exact optiq oracle; the golden was frozen from mlx-bun's
671
+ own engine and the gate was argmax agreement, so 2026-07-01 had already
672
+ restored the rule that the bare tier is the guarantee.
673
+ - `MLX_BUN_CPM5_FAITHFUL` + `FaithfulMiniCPM5` (`minicpm5-faithful.ts`) — the
674
+ default IS the faithful path; the op-for-op A/B reference served its purpose
675
+ (it is how the unfused-swiglu dispatch tax was found).
676
+ - `--l3` as a product mode — the Lab replaces it; the flag hard-errors.
677
+ - Training-side flag sanitization (`MLX_BUN_PERF_KERNEL=0` /
678
+ `MLX_BUN_FUSED_GELU=0` in trainers, launchers, recipes) — obsolete.
679
+
680
+ **2026-07-05 — engine unification** (Phases 0–3.2 of the original migration
681
+ plan; numbers in PLAN.md): the B=1 batch-lane gap measured, root-caused to the
682
+ two host bugs in §8, closed; batched mixed-KV for full-attention and rotating
683
+ layers; adopt-don't-copy + compiled decode at B=1 + prompt cache on the batch
684
+ lane; SSD tier moved inside `PromptCache.take()`; non-consuming prefix sharing;
685
+ `--isolate` P1 (proxy) and P2 (`ModelPool`, multi-model switching); `--batch`
686
+ default 8.
687
+
688
+ **2026-08-21 — serving architecture consolidation S0–S3** (live in PLAN.md):
689
+ `PreparedCompletion` + `CompletionExecutor` (S0), immutable
690
+ `GenerationPlacement` (S1), declared model profiles (S2), `serial | continuous`
691
+ as the placement vocabulary with the scheduler's B=1 fast path as the default
692
+ lone-request route (S3). S4 (land + post-merge verify) is the open box.
693
+
694
+ ## 12. Interface-based engine refactor
695
+
696
+ Design requested 2026-09-04. Initial implementation is recorded in §12.13. The R0–R10
697
+ checkboxes live in PLAN.md under "Interface-based engine refactor". This
698
+ section owns the contracts, migration details, and acceptance criteria.
699
+
700
+ ### 12.1 Outcome and scope
701
+
702
+ The product objective is to push the Pareto frontier of **speed, quality, and
703
+ size for local AI on a Mac**. Josh's goal is that a user sees the result and
704
+ asks how that quality, performance, and model size are possible on their
705
+ laptop. Interface consistency is useful when it makes those gains easier to
706
+ build, compare, compose, and ship. It is not an independent reason to accept
707
+ slower execution or a larger working set.
708
+
709
+ The current target is **Josh's designated Qwen3.8-27B quants**. They are on a
710
+ drive unavailable to this session. Artifact access is needed for target-specific
711
+ numerical and performance validation; it does not block the refactor.
712
+ The locally cached published flagship and TQ snapshots are not those targets
713
+ (Josh clarified this on 2026-09-04); availability must not select the product
714
+ baseline. Quant recipes and quality findings remain in [TurboQuant](turboquant.md),
715
+ including rejected variants and raw-EOS regressions. Interfaces must preserve
716
+ exact packing, rotation, per-layer precision, recurrent-state, and kernel
717
+ choices. Other artifacts/families provide compatibility coverage only.
718
+
719
+ A downloaded quant supplies weights, configuration, and tokenizer data. The
720
+ engine owns the hyperoptimized implementations and the rules for identifying
721
+ which quant each one supports. Resolve artifact identity and configuration at
722
+ load time, select registered engine code, then retain that binding for execution.
723
+ The quant does not supply a graph, kernel, method, plugin, or executable manifest.
724
+ An unknown artifact may use a compatible family implementation. An exact
725
+ declaration with missing code or incompatible configuration must fail explicitly.
726
+ Sessions remain independent of the selected implementation.
727
+
728
+ Aggressive specialization for a particular quantization is expected. A
729
+ quant-specific layout, graph, fused kernel, cache scheme, compiled schedule,
730
+ or complete execution method is a legitimate production implementation.
731
+ Several specialized implementations can be worth maintaining when each wins
732
+ in a useful regime. Shared orchestration must not force the fastest numerical
733
+ implementation through an inefficient generic operator path.
734
+ A model implementation may expose several supported execution methods. Loading
735
+ the weights does not permanently choose one method; request planning selects
736
+ among the compatible methods. Their private graphs, state and numerical loops
737
+ can differ while the session contract remains the same.
738
+
739
+ A session must work without knowing the model class, graph implementation,
740
+ decoding algorithm, cache layout, or scheduling mechanism. Every replaceable
741
+ component exposes an interface. Immutable configuration, events, and results
742
+ remain data records; implementations keep private helpers private.
743
+
744
+ There are three distinct replacement operations:
745
+
746
+ | Replacement | Integration change | What must remain compatible |
432
747
  |---|---|---|
433
- | bf16 (default), any batch B | **mlx-lm** at same composition | bit-exact goldens (serial + B=2 landed) |
434
- | uniform kv 4/8 | **mlx-lm** `--kv-bits` (+ optiq's rotating cache class where upstream is NYI) | bit-exact (kv-quant.test) |
435
- | mixed per-layer kv (`kv_config.json`) | **mlx-optiq** `install_mixed_kv` | bit-exact (mixed-kv-parity.test, landed 2026-07-05, maxDiff 0) |
436
- | mixed-precision weights (knapsack/oQ/TurboQuant), original kernels, batch-invariant kernels | **none exists** → Lab gates | KL vs our own reference path + frozen eval suite (intelligence axis) + envelope tests + kill switch |
437
-
438
- So: mixed-precision quantization is NOT demoted by the naked-=-L1 decision —
439
- it is the flagship of the row that mlx-lm cannot oracle, verified against
440
- optiq where optiq reaches (KV) and against the eval suite where no oracle
441
- exists (weights). `--l1/--l2` survive as *bench/test vocabulary* for the
442
- first three rows; they stop being user-facing product modes.
443
-
444
- **Composition rule (Josh, 2026-07-05): a composition inherits its scheme's
445
- oracle.** Batching, prompt cache, spec decode, adapters — none of them
446
- change a scheme's numerics, so composing them with a scheme verifies
447
- against THAT SCHEME'S oracle, per row. Batched bf16 anchored to mlx-lm
448
- (B=N token-exact goldens); batched mixed-KV anchors to the L2 oracle the
449
- same way: copy optiq's per-row quantization mechanics (`install_mixed_kv`'s
450
- per-layer map + streaming `to_quantized` + RotatingQuantizedKVCache — our
451
- serial `maybeQuantizeKv` is already the verified op-for-op copy), then
452
- gate on per-row bit-exactness against it (row cache contents; B=1 through
453
- the engine vs the mixed-KV golden; B>1 trajectories at the standing
454
- long-prefix bar). Do NOT invent Lab-style KL/teacher-forced gates for a
455
- composition of an oracle-backed scheme — the Lab is only for schemes with
456
- no oracle at all (new quant methods, original kernels).
457
-
458
- ## 6. The flag surface (end state — every flag with its reason)
459
-
460
- User-facing serve/generate flags after the deletion pass:
461
-
462
- | flag | why it exists |
748
+ | Dedicated graph → generated, compiled, or quant-specialized graph | Replace one graph registration in a model bundle | Graph ABI, backend, input/output shapes, state ABI, numerical contract |
749
+ | Autoregressive → speculative or diffusion method | Select a method registration and its compatible graph bundle | Session lifecycle, committed-output events, cancellation, accounting |
750
+ | MLX implementation → another runtime | Register a complete backend bundle with its graphs, state, sampling, and resource services | Engine/session contracts; a numerical claim requires separate evidence |
751
+
752
+ The first operation must require no session, scheduler-policy, route, or
753
+ application changes. The second must require no method-specific session or
754
+ protocol branches. A different numerical runtime cannot reuse MLX tensor
755
+ handles merely because the TypeScript interface matches.
756
+
757
+ Preserve the existing APIs and numerical defaults during migration. A new
758
+ internal interface does not implicitly expand supported feature combinations.
759
+ Keep one repository and the single-binary distribution. Remote compute, new
760
+ model families, a universal tensor library, a generic graph compiler, and true
761
+ B-wide prefill are separate projects. Training gets its own task interface;
762
+ it does not pretend to be a text-generation session.
763
+
764
+ This scope boundary does not defer quant-specific optimization. The refactor
765
+ must accommodate the existing quantization campaign and future optimized
766
+ implementations throughout migration. New numerical work has separate paired
767
+ validation, so structural changes cannot conceal a quality or performance
768
+ tradeoff. §12.11 defines the shared frontier scorecard.
769
+
770
+ Future design question (2026-09-05): consider a monorepo with the inference
771
+ engine kept together as one package, and application modules such as memory
772
+ and the web UI in separate packages. CLI/TUI and training boundaries can be
773
+ considered in that discussion. This is an unplanned idea, not part of the
774
+ current optimization campaign. Before choosing boundaries, map dependencies,
775
+ native assets, builds, tests, and releases; preserve in-process execution and
776
+ the single-binary distribution. Package boundaries should follow demonstrated
777
+ ownership and dependency needs without adding abstractions to the decode path.
778
+
779
+ ### 12.2 Dependency structure
780
+
781
+ ```mermaid
782
+ flowchart TD
783
+ Apps["CLI / HTTP / Pi / memory / evaluation"] --> Client["CompletionClient and task interfaces"]
784
+ Client --> Prep["Prompt preparation and request planning"]
785
+ Prep --> Engine["InferenceEngine"]
786
+ Engine --> Session["GenerationSession lifecycle"]
787
+ Engine --> Schedule["Scheduler policy and resource admission"]
788
+ Session --> Method["InferenceMethod"]
789
+ Schedule --> Group["SchedulingAdapter / ExecutionGroup"]
790
+ Group --> Method
791
+ Method --> Graph["Graph interface"]
792
+ Method --> State["State and checkpoint interfaces"]
793
+ Method --> Sample["Sampling / constraints / draft interfaces"]
794
+ Graph --> Backend["Backend runtime and resource leases"]
795
+ State --> Backend
796
+ Sample --> Backend
797
+ Compose["Composition root and implementation registry"] -.-> Engine
798
+ Compose -.-> Method
799
+ Compose -.-> Graph
800
+ Compose -.-> Backend
801
+ ```
802
+
803
+ Interfaces belong to the consumer's contract layer. Composition roots import
804
+ implementations and supply them explicitly. No service locator inside a token
805
+ loop, import-time self-registration, dependency injection framework, or
806
+ reflection-based constructor discovery is needed.
807
+
808
+ Proposed homes, introduced only as their migration phase lands:
809
+
810
+ | Home | Responsibility and permitted dependencies |
463
811
  |---|---|
464
- | `--kv-quant config\|4\|8\|off` | the ONE performance trade-off a user makes: decode speed vs context headroom (+ intelligence-per-byte with mixed). Default off (bf16). Composition (fused vs unfused quantized SDPA) is DERIVED from the scheme's oracle, not chosen. |
465
- | `--batch N` | cap on concurrent rows (mlx-lm's `--decode-concurrency` twin). Default 32 once Phase 2's B=1 gate passes; `--batch 1` = force-serial / determinism pin. Name kept (Josh 2026-07-05) — semantics change from mode to cap. |
466
- | `--draft-model`, `--num-draft-tokens` | speculative decoding (mlx-lm parity) |
467
- | `--adapter`, `--memory-budget`, `--kv-budget`, `--prompt-cache`, `--ssd-cache`, `--expert-offload`, `--force-wire` | capacity/serving features, not numerics levers |
468
- | sampling flags (`--temperature` etc., HLG family) | request-default sampling |
469
-
470
- Everything else becomes either a **kill switch** (env-only, documented for
471
- debugging: `MLX_BUN_COMPILED_DECODE=0`, `MLX_BUN_COMPILED_GEGLU=0`,
472
- `MLX_BUN_NO_FUSED_SDPA=1`) or a **Lab flag** (env-only, lives with its bench
473
- script and expiry — §7). Kill switches are bit-exact by definition (they
474
- select a slower same-parity path); anything output-changing is Lab.
475
-
476
- **Deleted — EXECUTED 2026-07-05** (one funeral each; ~50 files touched, 23
477
- deleted, suite green):
478
- - `--fused-decode` / `MLX_BUN_FUSED_DECODE` — 1.00×, forces uncompiled,
479
- silent-wrongness footgun. Delete kernel path + flag + backstop throw.
480
- - `--fused-gelu` / `MLX_BUN_FUSED_GELU` + `MLX_BUN_FUSED_SWIGLU` — +0–1%;
481
- the compiled closures already own the fusion win bit-exactly. Delete both
482
- custom Metal kernels + flags (git history keeps the source).
483
- - `--perf-kernel` / `MLX_BUN_PERF_KERNEL` — **DELETE** (Josh 2026-07-05:
484
- "we are going to start a full optimization program anyway… we will end up
485
- redoing all the work we have currently done given that we now have a
486
- different starting point"). The kernel, its frozen-oracle test scaffolding
487
- (perf-kernel-oracle.test.ts, freeze-perf-oracle.ts), and the flag all go;
488
- git history + the h2h evidence (+6% 12B@16k, −38% e4b@16k, KL WARN) are
489
- the breadcrumb. If a flash-decode kernel returns, it's re-derived from the
490
- L1 baseline in the Lab under the §7.4 program, not resurrected.
491
- - `MLX_BUN_CPM5_FAITHFUL` + `FaithfulMiniCPM5` — the default IS the faithful
492
- path now; the A/B reference served its purpose.
493
- - `--l3` as a product mode — the Lab replaces it; the flag now hard-errors
494
- with a pointer here. `--l1`/`--l2` stay as documented aliases.
495
- - Also deleted with the fused-swiglu family: `fused-mlp-kernel.ts` and
496
- `steel-linear-kernel.ts` (only reachable through the deleted gates) and
497
- the ~17 `scripts/experiments/swiglu-*`/geglu/steel one-off scripts.
498
- - Training flag sanitization (`MLX_BUN_PERF_KERNEL=0` / `MLX_BUN_FUSED_GELU=0`
499
- in trainers/launchers/recipes) — obsolete; the no-vjp kernels it guarded
500
- against no longer exist.
501
-
502
- **Lab lifecycle rule** (the anti-flag-pile law): every Lab experiment ships
503
- with (a) a hypothesis stated in frontier-axis terms, (b) a paired A/B bench
504
- script, (c) an expiry review date. Outcomes: promote to default (with the
505
- stable-pass numbers) or delete (with a breadcrumb in the design doc). No
506
- third state. "Off in every regime with no promotion path" — the fused-decode
507
- condition — is a deletion that hasn't happened yet.
508
-
509
- ## 7. The frontier program (AFTER stability — the order matters)
510
-
511
- Phase gate: none of this starts until §8 phases 0–2 are done. "Now we
512
- finally have a baseline that is even and proven; investigate THAT."
812
+ | `src/contracts/` | Portable requests, events, results, session/client/task interfaces. No Bun, DOM, MLX, concrete model, or application imports. |
813
+ | `src/engine/` | Session lifecycle, planner, scheduler policy, admission. Depends on contracts and injected ports. |
814
+ | `src/inference/` | Method and graph contracts; reusable AR orchestration, verification, and scheduling adapters. Tensor-facing code is parameterized by a backend binding. |
815
+ | `src/backends/mlx/` | MLX runtime, typed bindings, resource owners, execution groups, state adapters. Wrap existing numerics before relocating files. |
816
+ | `src/model/` | Existing model math behind graph adapters, artifact profiles, loaders. Concrete implementations may use MLX internally. |
817
+ | `src/serve/` | Protocol parsing/framing and transport; calls the completion client. No concrete graph selection or cache surgery. |
818
+ | `src/app/` | Composition roots, runtime bootstrap, server/application assembly. The intentional location for concrete imports. |
819
+ | Existing application directories | Memory, Pi, web, eval, train, quantize consume their declared client/task ports. Move root modules only when their dependencies have been separated. |
820
+
821
+ Use TypeScript projects plus an AST/import-resolution boundary check. The check
822
+ must follow type-only imports, re-exports, aliases, and literal dynamic imports.
823
+ New forbidden edges fail; temporary legacy edges have a named R-phase owner and
824
+ a deletion gate. Browser contracts cannot import declarations out of pi-web.ts.
825
+ No new concrete model checks in engine/session/planner code. Model-specific
826
+ checks remain legitimate inside the model's own adapter.
827
+
828
+ ### 12.3 Interface inventory
829
+
830
+ These are responsibility boundaries, not a mandate for one class per row.
831
+ Small stateless ports may be implemented by functions or object literals.
832
+
833
+ | Interface | Owns | Must not own |
834
+ |---|---|---|
835
+ | `CompletionClient` | Semantic completion request, event stream, terminal result, cancellation | Tensor/state handles, model class selection |
836
+ | `PromptPreparer` | Template/tokenizer policy, token IDs, validated media descriptors, stable-prefix boundary | GPU scheduling or request-global model mutation |
837
+ | `ExecutionPlanner` | Effective immutable policy, implementation selection, capability negotiation, fallback reasons, plan identity | Starting generation or inventing new feature support |
838
+ | `InferenceEngine` | Session creation, runtime coordination, shutdown | Per-family forward math |
839
+ | `GenerationSession` | Lifecycle, committed output, result, cancellation and close | Token-at-a-time assumption, KV offsets, concrete method branches |
840
+ | `InferenceMethod` | AR/speculative/diffusion algorithm and private per-run state | HTTP framing or application sessions |
841
+ | `SchedulingAdapter` / `ExecutionGroup` | Method-specific join/leave and bounded execution work | Queue priority policy or independent memory admission |
842
+ | `Scheduler` | Fairness, active-row cap, queue limits, safe-point dispatch | Cache merge math, sampler math, model-family checks |
843
+ | `ModelBundle` / `GraphFactory` | Artifact construction, graph selection, compatible backend/state services | Rewriting requested sampling or KV policy |
844
+ | `AutoregressiveGraph` | Forward-to-hidden and selective logits projection | Token selection, prefill loop, stopping, persistence |
845
+ | `DenoisingGraph` | Canvas/noise-step operations required by diffusion | Pretend AR cache or fake next-token API |
846
+ | `SpecializedExecutionFactory` | Quant/artifact/device-specific fused execution behind the same method/group contract | Changing quality policy or observable session semantics without a declared plan |
847
+ | `SamplingPolicy` / `SamplingKernel` | Shared processor/mask/logprob/sample/history semantics and backend execution | Transport, independent request defaults |
848
+ | `ConstraintProvider` / `DraftProvider` | Per-run constraint state or proposals under declared capabilities | Mutating target model fields outside a bound context |
849
+ | `StateFactory` and capability facets | Owned state, borrowing, snapshots, batch operations, rollback where supported | Assuming every state is trimmable token KV |
850
+ | `PrefixStore` / `CheckpointStore` | Lookup, retention, integrity, atomic persistence and version checks | Reconstructing arbitrary concrete cache classes itself |
851
+ | `ResourceManager` / `ResourceLease` | Reservations, backing memory lifetime, device completion, release | Model math or hiding synchronization |
852
+ | `ArtifactResolver` / `TokenizerProvider` | Pinned artifact identity, tokenizer/template provenance and loading | Request execution |
853
+ | `TelemetrySink` / `Clock` | Structured observations and time | Scheduling or output decisions |
854
+ | `EngineHost` / `TaskRunner` | Direct or isolated runtime lifecycle; train/quantize task execution | Sending live tensor handles across a process boundary |
855
+
856
+ Existing `CompletionExecutor`, `CompletionSink`, `DraftProvider`, `DraftSource`,
857
+ model profiles, `KvScheme`, and the cache codec table are starting points.
858
+ Adapt or evolve them; do not retain a second permanent wrapper hierarchy.
859
+
860
+ ### 12.4 Session and graph contracts
861
+
862
+ Illustrative signatures below fix the intended separation. Supporting record
863
+ types are specified by the semantics in this section and finalized in R1.
864
+ They are not implemented public exports yet.
865
+
866
+ ```ts
867
+ interface InferenceEngine {
868
+ open(request: GenerationRequest, control: RunControl): Promise<GenerationSession>;
869
+ close(): Promise<void>;
870
+ }
871
+
872
+ interface GenerationSession {
873
+ readonly id: string;
874
+ readonly events: AsyncIterable<GenerationEvent>;
875
+ readonly result: Promise<GenerationOutcome>;
876
+ cancel(reason: CancelReason): Promise<void>;
877
+ close(): Promise<void>;
878
+ }
879
+
880
+ type GenerationEvent =
881
+ | { type: "committed"; sequence: number; tokenIds: readonly number[] }
882
+ | { type: "progress"; usage: UsageSnapshot };
883
+
884
+ interface InferenceMethod<Binding> {
885
+ readonly descriptor: MethodDescriptor;
886
+ createRun(plan: ExecutionPlan, binding: Binding): Promise<MethodRun<Binding>>;
887
+ }
888
+
889
+ // Tensor-facing contracts exist only inside a compatible backend bundle.
890
+ interface AutoregressiveGraph<Tensor, State, Inputs> {
891
+ readonly descriptor: GraphDescriptor;
892
+ forwardHidden(inputs: Inputs, state: State): Tensor;
893
+ projectLogits(hidden: Tensor, selection: LogitSelection): Tensor;
894
+ }
895
+
896
+ interface GraphFactory<Graph, Binding> {
897
+ readonly descriptor: GraphDescriptor;
898
+ open(artifact: ResolvedArtifact, binding: Binding): Promise<Graph>;
899
+ }
900
+ ```
513
901
 
514
- Ranked by expected frontier shift (memory ↓ / intelligence ↑ first, then
515
- speed):
516
-
517
- 1. **Mixed-precision weights** — knapsack `--target-bpw` (ours) vs oQ
518
- (calibration-driven sensitivity, omlx-adoption-map §4) vs llama.cpp
519
- imatrix as prior art. Gate: perplexity + frozen 6-task eval at equal bpw.
520
- The purest beyond-the-frontier play we know: same memory, more
521
- intelligence. (arXiv-lens candidate.)
522
- 2. **TurboQuant KV** (Josh) — ORTHOGONAL to mixed precision and composes
523
- with it: mixed precision is the ALLOCATION axis (how many bits per
524
- transformer layer, optiq's sensitivity map), TurboQuant is the
525
- QUANTIZER axis (how a given bit budget encodes each K/V vector, within
526
- a layer — rotation-based, online, calibration-free). Composition =
527
- sensitivity allocation × better quantizer: more fidelity at the same
528
- KV bytes. Architecture: a layer-2 cache format + a layer-1 attention
529
- kernel keyed by cache type (same shape as optiq's scheme). Oracle: the
530
- paper + its reference implementation anchor the ALGORITHM (read them
531
- first, per the oracle rule); the output is genuinely novel (no serving
532
- stack ships it) → eval-suite gates. Composes with Phase 3.1's batched
533
- quantized buffers.
534
- 3. **Speculative decoding depth** — DFlash/DSpark behind the existing
535
- `DraftSource` seam; decode tok/s at zero quality cost when drafts land.
536
- 4. **Per-model graph work from the baseline** — the optimized-verified
537
- layer's whole point: unroll a model's flat DAG, find fusion the compiler
538
- misses, prove with kernel-trace diffs, promote per model. The perf-kernel
539
- root-cause lives here.
540
- 5. **Batch-invariant kernels** (research) — restores determinism-under-load;
541
- novel on Apple Silicon.
542
-
543
- ## 8. Migration phases (each with a hard gate)
544
-
545
- - **Phase 0 — measure the gap** — **DONE 2026-07-05** (M1 Max, same server
546
- harness both lanes, median-of-5, bf16 KV):
547
-
548
- | model | serial B=1 | batch-lane B=1 | ratio | extra host ms/token | TTFT serial→batch |
549
- |---|---|---|---|---|---|
550
- | cpm5 | 281.4 tok/s | 128.8 | 0.46× | +4.2 ms | 44 → 55 ms |
551
- | e4b | 62.1 | 44.9 | 0.72× | +6.2 ms | 54 → 121 ms |
552
- | 12B | 29.8 | 25.6 | 0.86× | +5.5 ms | 82 → 251 ms |
553
-
554
- The overhead is a roughly CONSTANT ~4–6 ms per decode step, independent of
555
- model size → per-step host tax, not GPU work. cpm5 (which never uses
556
- compiled decode) pays it too, so compiled-decode's absence is not the
557
- story. Suspects (in dispatch-site order): the `setImmediate` macrotask hop
558
- per drive-loop iteration, per-step per-layer `BatchedDecodeMaskCache`
559
- construction + mask materialization at B=1 (serial passes caches bare with
560
- an empty mask), and the per-token emit path. TTFT gap = solo-prefill +
561
- merge + admission hops. Closure worklist = profile these three, in order.
562
- - **Phase 1 — the deletion pass** (§6): dead kernels, flag surface, docs,
563
- Lab scaffolding (move perf-kernel + its bench + oracle tests). Gate: full
564
- suite green; naked defaults byte-identical to today's L1 route.
565
- - **Phase 2 — engine unification** — **DECODE GAP CLOSED 2026-07-05.** The
566
- ~4–6 ms/step host tax decomposed into exactly two bugs, both fixed:
567
- 1. **The pipelined token read enqueued an `astype` kernel** —
568
- `prev.toFloat32()` on the int token array created a NEW cast op that
569
- queued BEHIND the entire just-dispatched next step on the GPU stream,
570
- stalling the "overlapped" read for a FULL step of GPU work every
571
- token (this alone was the bulk of the gap; zero host/GPU overlap,
572
- proven by NO_PIPELINE ≈ pipelined). Fix: `MlxArray.toIntTokens()` —
573
- eval + raw uint32/int32 buffer read, no cast (mirrors serial's
574
- `itemUint32`).
575
- 2. **Per-layer per-step mask/rope wrapper churn**: `#step` rebuilt a
576
- `BatchedDecodeMaskCache` + array mask for every full layer every
577
- token even when no row had left padding. Fix: the unpadded fast path —
578
- bare caches (KVCache.makeMask(1) = the empty mask, scalar rope) →
579
- the batch step dispatches the SAME graph the serial loop builds.
580
-
581
- Measured after (B=1 through the batch lane, same SSE harness as Phase 0):
582
- cpm5 129→**264** (serial 281 SSE / in-process ratio **0.994**), e4b
583
- 45→**57.6** (0.93 — remainder = compiled decode, serial-only today),
584
- 12B 25.6→**29.7** (**1.00**). Batched parity oracles green (11/11
585
- applicable; the CPM extend-join golden failure PRE-EXISTS this work —
586
- proven by stash/rerun — and is logged as an open item).
587
- **Remaining before the `--batch` 32 default flip**: compiled decode
588
- inside the B=1 step (e4b's last 7%), prompt cache for batched rows
589
- (TTFT: serial hits the cache on repeat prompts, batch re-prefills —
590
- e4b 126 vs 54 ms, 12B 246 vs 82 ms), then GATE-B1-SPEED ≥99% on all
591
- three + GATE-B1-PARITY (bit-exact vs L1 goldens) + B=2 goldens intact.
592
- - **Phase 3 — composition**: quantized KV under batching — **P1 LANDED
593
- 2026-07-05**: src/model/batched-quant.ts (merge/extend/filter over
594
- triples + BatchedQuantDecodeMaskCache), scheduler solo-conversion at the
595
- serial chunk boundaries, gateway kv-batchability memo (all-full-attention
596
- kvConfig batches; uniform/rotating stays serial; scheme-less gateway
597
- refuses — never silently drop quantization, the optiq bug class). Gates
598
- green: B=1 through the scheduler BIT-EXACT vs the cpm5 optiq golden
599
- (decode steps, maxDiff 0); B=2 dynamic join — unpadded row BIT-EXACT vs
600
- solo at every step, padded row within the calibrated envelope (5e-2;
601
- bf16 same-harness shows ~9e-3 — the pre-existing reduction-order noise,
602
- amplified by grid snapping; scripts/experiments/batched-quant-kl-profile
603
- .ts). E2E: --batch 2 --kv-quant config on cpm5 = /stats active_rows 2,
604
- 400 tok @ 240 tok/s aggregate, coherent output. NEXT: milestone 2
605
- (batched rotating-quant for gemma configs), prompt-cache reuse for
606
- batched rows, chunked prefill interleaving. Gate per feature: parity +
607
- no aggregate-throughput regression at B=4.
608
- - **Phase 3.2 — lone-request = serial (LANDED 2026-07-05)**, three moves:
609
- 1. **Adopt-don't-copy**: a row joining an EMPTY batch keeps its solo
610
- caches as the inners (pointer handoff; the merge copy now runs only
611
- when a second row actually joins — incl. a new merge branch that
612
- treats an adopted serial RotatingKVCache as the first row). The
613
- lone row's caches stay SERIAL-CLASS, which unlocks the next two.
614
- 2. **Compiled decode at B=1**: the scheduler replays the serial
615
- engine's CompiledDecode step when B=1 ∧ unpadded ∧
616
- supports(inners) ∧ uint32 pipeline register (same runner, same
617
- traces, same MLX_BUN_COMPILED_DECODE kill switch; falls back +
618
- disables on error like generate()). Gate: free-running greedy B=1
619
- == serial generate() token-for-token on 12B with
620
- CompiledDecode.stepsExecuted advancing (tests/batch-scheduler).
621
- 3. **Prompt cache on the batch lane**: joiners take() the longest
622
- usable prefix at admission (suffix-only prefill — multi-turn chat
623
- TTFT); rows finishing NEVER-MERGED put() back (exact prompt+fed
624
- accounting, offset-checked; reject/batch-drop paths poisoned; SSD
625
- retain hooks ride every disposal path). Gate: second identical
626
- request reports cached_tokens=prompt-1, step-0 argmax anchored,
627
- KL≤1e-2 (tests/batch-scheduler "prompt-cache reuse").
628
- **Measured (apple-m1-max/32GB, paired in-process A/B, best-of):
629
- B=1-through-batch-lane / serial = cpm5 0.996, e4b 0.992 (was 0.93 —
630
- compiled closed the last 7%), 12B 0.993 → GATE-B1-SPEED decode met on
631
- all three.** All gated suites green (scheduler CPM+12B, quant gates 1+2,
632
- full per-file model-free suite 0 fail).
633
- Findings: (a) gate 2's padded-row KL is JOIN-STEP dependent (grid-snap
634
- bin flips compound: K=5→4.5e-2, 6→3.5e-2, 7→1.5e-1, 8→1.3e-1; bf16
635
- ≤9e-3 at every K; identical on pre-3.2 main; extend vs re-merge
636
- byte-identical) — the gate now PINS the join step and calibrates there;
637
- padded-row KL is an envelope, the contract is the unpadded row's
638
- bit-exactness + model-free triple-surgery byte-identity. (b) v1 gap:
639
- an entry take()n by a row that later MERGES is consumed, not re-put —
640
- fix is a zero-copy view snapshot at merge (serial's boundary-snapshot
641
- trick); noted for milestone 2. (c) monolithic `bun test` can jetsam on
642
- a busy 32GB box (largestProcess=bun) — per-file loop is the fallback
643
- gate; not a repo defect.
644
- **Remaining before the `--batch` default flip (Josh's call)**:
645
- milestone 2 batched rotating-quant, then flip.
646
- - **Phase 3 milestone 2 — batched ROTATING-quantized KV (LANDED
647
- 2026-07-05)**: src/model/batched-rotating-quant.ts —
648
- BatchedRotatingQuantCache subclasses RotatingQuantizedKVCache (attention
649
- dispatch untouched) and runs the mlx-lm BatchRotatingKVCache ring
650
- mechanics over (packed, scales, biases) triples; RotatingQuantizedKVCache
651
- gains temporalView(); scheduler #quantizeSolo converts rotating layers
652
- (maybeQuantizeKv dispatch) and #mergeJoiner grows a rot-quant twin
653
- branch; gateway #kvBatchable accepts configs naming rotating layers →
654
- **gemma's whole kv_config batches** (every shipped kv_config now does).
655
- Gates: model-free byte-identity per row vs the serial oracle through
656
- ring wrap, B=1+B=2+filter (tests/batched-rotating-quant.test.ts); gemma
657
- 12B B=2 dynamic join through the real scheduler — unpadded row KL-0 at
658
- EVERY step, padded row ≤4e-3 (tests/batched-kv-quant-parity.test.ts).
659
- **Findings (hard-won, 2026-07-05)**: (a) THE ROPE-ARRAY STEP-STABILITY
660
- CONTRACT — a batched cache's ropeOffsetArr must be STABLE within a
661
- decode step and refresh only at releaseRopeArr(): the monolith CAPTURES
662
- it pre-update and ropes Q post-update (in-update dispose = use-after-
663
- dispose), while the GENERATED specializations RE-READ it for Q
664
- post-update (in-update refresh = K and Q roped one position apart →
665
- O(10) KL garbage). (b) GENERATED-FILE GUARDS ACCEPT BATCHED SUBCLASSES:
666
- the generated forwards' per-layer `instanceof` cache guards pass any
667
- batched cache that subclasses a serial class — an all-quant gemma batch
668
- therefore decodes through the GENERATED fast path (a feature — B=1
669
- twin proved BIT-EXACT through it — but a contract: batched caches must
670
- behave serially under re-reads). This is why the bug only appeared when
671
- EVERY layer was configured: one bf16 layer anywhere fails the guard and
672
- drops to the monolith. Diagnosis artifacts:
673
- scripts/experiments/gemma-rotquant-kl-profile.ts (CONFIG_FILTER/
674
- ROT_KEEP/ROT_DROP/ROT_IDX bisection) and gemma-twin-b1-repro.ts
675
- (TWIN_MASK/TWIN_ROPE toggles — rope isolated in two runs once the
676
- B=1 repro existed).
677
- - **Layer 0 — the SSD tier becomes a property of the store (LANDED
678
- 2026-07-05)**: PromptCache gains a structural ColdTier and runs the
679
- two-tier take (RAM peek vs cold find → zero-copy restore + trim)
680
- INSIDE take(), so the batch scheduler restores prefixes from disk at
681
- admission with zero scheduler changes (gated E2E on cpm5: demote →
682
- disk → batch-lane restore, cached=prompt−1, KL 1.5e-3); onPut drives
683
- the write-behind snapshot for both lanes; NEW idle demotion
684
- (--ssd-demote-idle, default 300 s with the tier) spills idle entries
685
- and FREES their GPU memory — RAM drains between agent bursts, every
686
- prefix stays reachable. Also fixes a latent flaw: the old serial flow
687
- trimmed the RAM entry before deciding the SSD copy won. The economics
688
- (Josh): the SSD competes with RECOMPUTE, not RAM — 12B prefill 168
689
- tok/s ⇒ a 30k agent context is ~3 min of prefill vs ~1 s of NVMe
690
- restore, and the ratio grows with context. Follow-ups noted: extract
691
- the generic blob-store mechanics (fingerprint dirs, atomic writes,
692
- scan/recovery, LRU cap) for a second client when one lands (adapters,
693
- expert offload); memory-pressure-triggered demotion (vs idle-only).
694
- - **Prefix sharing v1 (LANDED 2026-07-05)**: PromptCache.take() is
695
- NON-CONSUMING — a hit serves zero-copy CLONES (cloneKvCaches views,
696
- trimmed to the matched prefix, retain ref-counted so a demoted/evicted
697
- donor can never unmap pages a clone still reads) and the donor entry
698
- stays put. Safe by mlx's functional cache updates (updateAndFetch
699
- reassigns fresh arrays; donation needs refcount 1 and the donor holds a
700
- ref), so clones can never mutate donors. put() supersedes same-ns
701
- prefix-ancestors AND exact duplicates when the new entry is trimmable
702
- (untrimmable extensions preserve their prompt-boundary snapshots — the
703
- drift-proof server). This kills the CANNIBALIZATION flaw: agent B
704
- borrowing agent A's 2k system prompt used to consume-and-trim A's 10k
705
- entry to serve it. Gates: 22 unit tests (donor persistence, clone
706
- independence, supersede rules, ref-count ordering) + real-model
707
- scheduler gate: B served from A's entry (cached=|SYS|, KL 1.8e-3), A's
708
- NEXT TURN still hits its full entry. New-session and restart sharing
709
- fall out of the (already non-consuming) SSD tier. **Scope, honestly**:
710
- v1 shares the COMPUTE (one prefill serves N agents) and the durability;
711
- concurrent batch rows still hold separate physical KV copies — one
712
- shared physical prefix across rows is the paged/block-KV frontier item
713
- (§9.4), and per §9.5 the disk tier's whole-entry duplication of shared
714
- prefixes is the block-granularity revisit trigger.
715
- - **Runtime isolation P1 (LANDED 2026-07-05, opt-in `--isolate`)**: the
716
- engine child = the ENTIRE existing server on a unix domain socket; the
717
- parent = a thin reverse proxy (src/serve/isolate.ts) — zero MLX calls,
718
- crash isolation (502 + auto-respawn with crash-loop backoff), abort
719
- propagation, GET-retry across respawn. Decision recorded in
720
- runtime-isolation.md: proxy-the-whole-server instead of the original
721
- gateway-as-IPC-client sketch (grammar WASM/vision arrays/sampler
722
- closures don't serialize; the inter-process API is the /v1 surface
723
- itself — no second protocol). Measured paired (M1 Max, cpm5): proxied
724
- 197.2 vs direct 197.9 tok/s (−0.4%, noise), TTFT +2 ms, SSE per-token
725
- granularity preserved (96 reads/96 tokens), parent 0.6 ms mid-decode
726
- (criterion <50 ms). Gates: tests/isolate-proxy.test.ts (model-free fake
727
- engine: passthrough, granularity, abort, crash→respawn) +
728
- tests/isolate-e2e.test.ts (real cpm5 child). Prefill chunk loops also
729
- gained macrotask yields (the last isolation-Phase-1 item). Not proxied
730
- v1: /ws/chat (501). NEXT: P2 = child-per-model pool — multi-model
731
- switching by SPAWN-OVERLAP (new child loads while the old one serves;
732
- drain → demote-to-SSD → park; route by the request `model` field);
733
- P3 = GPU-lease unification with quantize/finetune jobs.
734
- - **Runtime isolation P2 — CHILD-PER-MODEL POOL (LANDED 2026-07-05; task
735
- #14 multi-model switching)**: ModelPool in src/serve/isolate.ts — LRU
736
- residency over engine children (--model-pool, default 1), request
737
- routing by the JSON `model` field on the generation endpoints (STRICT
738
- exact-/v1/models-id resolution: fuzzy strings like "gpt-4" keep
739
- mlx-lm's ignored-field drop-in semantics), spawn-overlap switching (the
740
- new model health-gates while the old serves), and graceful lossless
741
- eviction: POST /admin/drain (engine-mode-only endpoint: gateway
742
- quiesce + demoteIdle(0) → the whole prompt cache to that model's SSD
743
- fingerprint dir) then exit. engineArgvForModel pins --model per child.
744
- **Measured (M1 Max, cpm5⇄qwen0.8b, pool 1): switch 1.5 s · switch-back
745
- 1.2 s · cached_tokens 103/104 on return** — the conversation's KV
746
- survived full model eviction and restored from disk instead of
747
- re-prefilling (tests/isolate-switch-e2e.test.ts). Model-free pool gates
748
- (routing, drop-in ignore, cap-1 drain→demote→evict→respawn):
749
- tests/isolate-proxy.test.ts. Defaults unaffected: without --isolate no
750
- pool code exists in the request path.
751
- - **Phase 4 — the frontier program opens** (§7 order).
752
-
753
- ## 9. Decisions log (was: open questions)
754
-
755
- 1. **Cap default 8 — FLIPPED 2026-07-05** (Josh; earlier "32" note
756
- superseded by the sub-agents rationale, 4–8 agents + optiq's Mac-safe
757
- 8). Landed after Phase 3 met every gate: lone-request = serial engine
758
- (bits + speed + TTFT + prompt/SSD cache), quantized compositions
759
- batch. `--batch 1` pins strict arrival-independent numerics;
760
- kv-budget admission keeps 8 safe on small boxes.
761
- 2. **Flag stays `--batch`** — no rename; semantics become the cap;
762
- `--batch 1` = force-serial pin.
763
- 3. **Perf kernel: deleted in Phase 1** (§6) — the optimization program
764
- re-derives from the L1 baseline; no resurrection.
765
- 4. Still open: PagedAttention-style block KV — Phase 3 follow-up or its own
766
- design doc when prefix sharing under batching becomes the bottleneck.
767
- 5. **Prefix sharing scope (Josh 2026-07-05)**: sharing must extend to the
768
- DISK tier too — the whole-entry cold store duplicates a shared system
769
- prompt across N agent conversations (blocks would dedupe; that is the
770
- revisit trigger for block granularity). First-class use cases beyond
771
- concurrent rows: SPINNING UP A NEW SESSION (fresh conversation, same
772
- system prompt → instant prefix) and SERVER RESTART (re-attach without
773
- re-prefill) — the shared prefix should be a durable object, not a
774
- property of any one conversation's entry.
902
+ `GenerationSession` is one completion attempt, distinct from a Pi conversation
903
+ or application thread. The engine coordinates independent sessions sharing a
904
+ resident model. The method's private run owns prompt position, sampler history,
905
+ pending device work, and caches/canvas; those do not become session fields.
906
+ `RunControl` uses a portable cancellation port; HTTP adapters translate their
907
+ AbortSignal into it. Contract declarations do not pull in browser/server modules.
908
+
909
+ All methods publish only irrevocably committed token spans. A span can contain
910
+ one token, an accepted speculative group, or a whole completed diffusion
911
+ result. Tentative diffusion canvas revisions and rejected draft tokens are
912
+ private. A non-streaming method may publish nothing until its final output.
913
+ The existing completion sink converts committed tokens to semantic text,
914
+ reasoning, tools, stops, and protocol usage. Tokenizer identity is fixed at
915
+ session open. Task types that cannot provide this token contract use a separate
916
+ task interface rather than fabricated tokens or usage.
917
+
918
+ Lifecycle is `created → preparing → queued → running → settling → terminal`.
919
+ Terminal outcome is exactly one of completed, cancelled, or failed. Each state
920
+ accepts cancellation. Early iterator return initiates cancellation. Only one
921
+ consumer may read events; progress observation cannot steal output. A bounded
922
+ queue propagates backpressure to scheduling between safe work units. Never
923
+ silently drop output or let a slow consumer retain unlimited device state.
924
+
925
+ `result` settles once on every path, including no consumer, worker death,
926
+ preparation failure, and cancellation. It contains a discriminated outcome
927
+ with final usage/error data. Events already emitted remain a partial result
928
+ on failure. GenerationRequest declares streaming or collecting consumption at
929
+ open. Streaming starts on demand and has a configured idle-consumer deadline;
930
+ an abandoned/unread stream cancels when that deadline expires. Collecting runs
931
+ without an event reader, reserves output storage against the admitted maximum,
932
+ and returns collected tokens with the outcome. Insufficient collection capacity
933
+ fails admission; it never silently truncates output. R1 fixes the queue/deadline
934
+ defaults with tests. Completed sessions reject further advancement;
935
+ cancel/close are idempotent. Close cancels live work and awaits safe release.
936
+
937
+ Graph calls remain direct device operations. The interface does not insert
938
+ an await, readback, tensor wrapper, or synchronization for each operator.
939
+ `forwardHidden` plus selective `projectLogits` preserves the existing ability
940
+ to avoid a full prompt-length × vocabulary logits allocation. Compile caches
941
+ belong to the implementation and key on the graph/state/request parameters
942
+ that affect their trace. A graph factory owns and closes resident weights;
943
+ individual runs borrow them through leases.
944
+
945
+ The selective-hidden/logits interface is the reusable AR path. A specialization
946
+ may instead implement a coarse execution contract that fuses graph, head,
947
+ sampling, or state updates and never materializes those intermediate tensors.
948
+ It must preserve the declared sampler/constraint semantics, accounting, safe
949
+ points, and resource ownership, and pass the same conformance suite. Do not
950
+ require a fused kernel to synthesize hidden/logits objects solely to satisfy
951
+ an abstraction. The planner binds this alternative once through the registry;
952
+ the session and scheduler policy remain unchanged.
953
+
954
+ R1 must specify graph ABI dimensions, dtypes, mutation rules, position/mask
955
+ inputs, output ownership, and evaluation semantics. Include media inputs,
956
+ hidden taps, and adapter bindings as typed graph capabilities. Move mRoPE,
957
+ adapter selection, and hidden taps out of mutable model-global request fields
958
+ into bound per-run contexts. While legacy models still mutate those fields,
959
+ their adapter requires the exclusive lease and restores them on every exit.
960
+
961
+ The registration bundle binds graph, tensor type, state factory, sampler,
962
+ device runtime, and optional capabilities together. Use generics and opaque
963
+ handles internally; do not erase them with `any` or public casts. Validate
964
+ backend ID, state ABI, graph ABI, and artifact identity at construction and
965
+ restore. TypeScript compatibility alone does not establish numerical parity.
966
+
967
+ Specialization keys may include exact artifact fingerprint, weight quant/layout
968
+ and packing version, layer/shape geometry, KV scheme, Mac GPU capabilities,
969
+ context regime, and batch regime. Dispatch is planned or bound at a safe shape
970
+ transition, with a validated compatible fallback outside the optimized range.
971
+ It must not become a collection of per-token environment checks. Fused paths
972
+ may retain optional features only when implemented; capability negotiation
973
+ selects another faithful implementation when the requested composition needs it.
974
+
975
+ ### 12.5 Capability negotiation and planning
976
+
977
+ Replace scattered feature booleans with versioned capability descriptors and
978
+ typed capability interfaces. Obtain a complete optional facet once during
979
+ planning, such as rollback or batch join. The core never probes a handful of
980
+ optional methods and hopes they form a complete implementation.
981
+
982
+ Evaluate the whole composition: artifact/profile, graph, method, KV scheme,
983
+ sampler, seed, constraints, adapters, media, checkpointing, and batching.
984
+ Two independently supported features do not prove their combination works.
985
+ Each supported composition links to its conformance/oracle test cell.
986
+
987
+ Within a compatible composition, selection may use a measured execution recipe
988
+ for the artifact/quant/device/workload. A recipe binds the entire implementation
989
+ bundle, its quality contract, and resource model. It can choose a quant-specific
990
+ fused method rather than a generic graph. Record provenance and the evidence
991
+ behind automatic selection. Never optimize by changing the requested artifact,
992
+ quant, or quality tier without an explicitly permitted product policy.
993
+
994
+ Planning returns supported, refused, or an explicit policy-permitted fallback
995
+ with reasons. Distinguish required request behavior from preferred acceleration.
996
+ Retain existing intentional fallback behavior during migration and record it
997
+ in the plan. Never silently change a required scheme, constraint, seed, or
998
+ adapter to gain batching. Preserving existing support also means retaining
999
+ explicit refusals. Fallback ordering is deterministic and tested.
1000
+
1001
+ The plan contains artifact and tokenizer identities, implementation IDs/ABIs,
1002
+ oracle tier, actual algorithm, resolved sampling/stop policy, state layout,
1003
+ cache/checkpoint policy, resource estimates, and allowed execution shapes.
1004
+ Dynamic batch membership is runtime state within those allowed shapes. Record
1005
+ it in traces; do not freeze a future arrival schedule into admission.
1006
+
1007
+ Normalize environment/CLI/request configuration at the boundary. Capture an
1008
+ immutable effective snapshot per engine/run. No runtime configuration lookups
1009
+ inside forward/decode after binding. Experiment commands construct separate
1010
+ bindings with explicit snapshots. Diagnostics may observe a running request
1011
+ but cannot change its numeric policy halfway through execution.
1012
+
1013
+ Admission is two-stage: validate metadata and reserve a conservative preparation
1014
+ budget before compiling grammar or running media towers; then refine the
1015
+ reservation using prepared sizes before execution. Count shared resident
1016
+ weights once, per-run state, draft resources, media/preparation transients,
1017
+ compiled state, pending output, and retained snapshot/write-behind buffers.
1018
+ Reservation ownership transfers across stages and releases on every failure.
1019
+ Keep prompt/context clamping semantics and aggregate KV limits consistent with
1020
+ current policy. A rejected prompt must not first allocate its full GPU payload.
1021
+
1022
+ ### 12.6 State, ownership, and persistence
1023
+
1024
+ Resource rules apply to success, partial construction, eviction, cancellation,
1025
+ rollback, shutdown, and native failure:
1026
+
1027
+ - A resource has one owner, with explicit borrowed views or retained leases.
1028
+ Ownership transfer is a checked operation; a resource cannot be disposed by
1029
+ both the old and new owner. Close is idempotent and attempts every release
1030
+ even if one cleanup fails, preserving the original execution error.
1031
+ - Device handles and host/mmap backing memory have distinct lifetimes. A native
1032
+ completion fence governs final backing release. JS scope exit or array
1033
+ disposal alone is insufficient. Never restore a JS callback destructor on
1034
+ a Metal completion thread. Finalization remains a diagnostic/backstop.
1035
+ - Replace `stateNeedsDispose` with an explicit owned state-view lease. Snapshot
1036
+ creation returns a stable owner; later writes to the donor cannot change it.
1037
+ Prefix lookup returns retained state and its exact logical coverage.
1038
+ - State capability facets include snapshot, prefix reuse, trimming, batch
1039
+ merge/extract/filter, and speculative transaction. Recurrent state supports
1040
+ exact restore/replay where proven; diffusion state can expose a different
1041
+ snapshot without claiming a token prefix or trim operation.
1042
+ - A speculative transaction includes every participating layer and draft-side
1043
+ state. Commit or rollback leaves one consistent accepted prefix. Partial
1044
+ failure invalidates the run; it cannot return uncertain state to PrefixStore.
1045
+ - Storage policy is separate from layout codecs. Provider-owned codecs bind
1046
+ formats to a compatible implementation. Keep streamed tensor writes,
1047
+ corruption checks, atomic publication, byte caps, and bounded spill queues.
1048
+ Do not replace the current copy-restore with unsafe mmap borrowing for style.
1049
+
1050
+ Prefix identity and generation-resume identity are separate records. Prefix
1051
+ identity covers artifact content, tokenizer/template interpretation, adapters
1052
+ and their revisions, effective numeric scheme, graph/state compatibility, and
1053
+ media content when reuse is supported. Generation identity additionally covers
1054
+ the complete normalized request, stop strings, sampling/seed policy, method,
1055
+ constraints, generation limit, and resume schema. Canonicalize map keys while
1056
+ preserving semantically ordered arrays. Store the descriptor as well as its
1057
+ digest, and validate it on restore.
1058
+
1059
+ A resume snapshot includes consumed input coverage, committed completion count,
1060
+ pending token/device work in a restorable form, RNG/history position, and any
1061
+ method/constraint state needed for exact continuation. Physical cache offset,
1062
+ committed output, and bytes delivered to a client are different quantities.
1063
+ Snapshots are legal only at declared safe points. Unsupported exact-resume
1064
+ compositions remain refused or disabled as documented.
1065
+
1066
+ Retries must distinguish a new generation from a resumed attempt. Preserve the
1067
+ current replay contract through its compatibility adapter; explicitly design
1068
+ any future idempotency/resume field before changing the public API. Never claim
1069
+ exactly-once delivery across a broken stream or automatically replay tool side
1070
+ effects. Cold-prefix misses may recompute; an incompatible exact-resume request
1071
+ must report the mismatch rather than invent a continuation. Version the format
1072
+ and define read-old/write-new compatibility or explicit invalidation per change.
1073
+
1074
+ ### 12.7 Scheduling and inference methods
1075
+
1076
+ Keep policy independent from mechanics. Scheduler chooses eligible work,
1077
+ fairness, queue bounds, active-row limits, and resource reservations. A method's
1078
+ scheduling adapter creates execution groups and implements join/leave/advance
1079
+ over its own state. Native batch merge and filter stay inside the MLX adapter.
1080
+ Scheduler must not import KV classes or perform a per-layer tensor loop.
1081
+
1082
+ An advancement is a bounded safe unit, not necessarily a token: one prefill
1083
+ chunk, a pipelined decode step, one verify round, or one denoising iteration.
1084
+ Budgets describe admitted resources and work hints separately. A synchronous
1085
+ kernel cannot be interrupted; cancellation is honored at the next safe point.
1086
+ Keep pending MLX work between advancements to preserve decode overlap. No
1087
+ mandatory GPU synchronize or full logits readback at the scheduling boundary.
1088
+
1089
+ An execution group owns its resident run handles exclusively. Join/leave
1090
+ validates compatibility before state mutation. Group-wide failure either
1091
+ proves unaffected rows safe or fails the entire group; it cannot publish
1092
+ possibly corrupted snapshots. Slow readers stop receiving dispatch while their
1093
+ reservations remain accounted for; cancellation releases them after pending
1094
+ device work is safe. Queued cancellation removes the waiter promptly so a dead
1095
+ serial request cannot keep batch admission in drain mode.
1096
+
1097
+ Migrate these existing algorithms behind the contracts:
1098
+
1099
+ | Method/component | Preserved behavior |
1100
+ |---|---|
1101
+ | AR method | Shared prefill program, existing tail split/snapshot boundaries, sampler order, pending-token pipeline |
1102
+ | Speculative method | Existing verifier and DraftSource variants, actual proposal length, target taps, per-step RNG, recurrent rollback, source-specific prefill convention |
1103
+ | Fill/grammar acceleration | Explicit assert/verify proposal policy and constraint state; preserve current exclusions and fidelity gates |
1104
+ | Diffusion method | Existing denoising behavior with private canvas state and committed output only; no fabricated AR state |
1105
+ | Continuous execution adapter | B=1 adopted-cache path; existing merge/extract/filter operations and per-row sampler history |
1106
+ | Quant-specific execution | Fused or precompiled work units with the same observable semantics and explicit numerical tier; no forced intermediate allocations |
1107
+
1108
+ Shared AR prefill does not imply full-prompt forwarding for every method.
1109
+ Native GLM MTP already declares a different prefill convention. Preserve it as
1110
+ data in the method plan. Likewise, unifying solo prefill code does not establish
1111
+ bit identity with B-wide prefill or broaden the batch support matrix.
1112
+
1113
+ The shared prefill program is the canonical policy for segmentation, snapshots,
1114
+ and cancellation. A measured specialization can execute that program through
1115
+ a different kernel schedule while satisfying its numerical and resource
1116
+ contract. Share the decisions; do not prohibit a faster executor merely because
1117
+ its implementation cannot reuse a helper function.
1118
+
1119
+ ### 12.8 Applications, library, and process boundaries
1120
+
1121
+ Keep the existing token-to-semantic completion pipeline. `CompletionClient`
1122
+ is the application/transport boundary; `GenerationSession` is the worker's
1123
+ method-neutral token boundary. An ordinary OpenAI-compatible remote endpoint
1124
+ can implement CompletionClient without exposing token IDs or local checkpoints.
1125
+ It cannot masquerade as a fully capable local GenerationSession.
1126
+
1127
+ Provide direct and isolated CompletionClient/EngineHost implementations. Evolve
1128
+ the existing `src/serve/isolate.ts` EngineChild, ModelPool, health gating,
1129
+ restart policy, and HTTP-over-Unix-socket proxy. Isolation already exists.
1130
+ Do not build a competing supervisor or change the wire protocol merely to
1131
+ match internal interfaces. Current whole-server isolation remains supported
1132
+ while CPU application services are separated from worker-owned compute.
1133
+
1134
+ Worker requests contain portable policy and media descriptors/bytes. Grammar
1135
+ controllers, media tensors, sampler closures, state handles, and device leases
1136
+ are created in the worker. A crash fails in-flight completion attempts, resets
1137
+ worker-owned residency, and preserves parent health/application state. Existing
1138
+ GET/HEAD retry behavior does not authorize automatic generation POST replay.
1139
+ Cross-process resume requires the explicit checkpoint/replay contract above.
1140
+
1141
+ One resource coordinator covers inference and managed training/quantization
1142
+ jobs that share a runtime host. Preserve the existing jobs subprocess design
1143
+ and persist terminal job state. Do not describe this coordinator as controlling
1144
+ unrelated external Python processes or a separately launched training run.
1145
+
1146
+ The high-level library opens a runtime asynchronously before importing/loading
1147
+ native implementations. Root high-level imports must work without installed
1148
+ MLX. Keep advanced native/model access in explicit low-level exports with clear
1149
+ ownership and bootstrap requirements. Compatibility wrappers preserve current
1150
+ exports until a deliberate versioned removal; they may not maintain duplicate
1151
+ generation loops. Validate the packaged binary and native sidecars, not only
1152
+ source execution, before release.
1153
+
1154
+ ### 12.9 Current-to-target migration map
1155
+
1156
+ | Current code | Target and removal condition |
1157
+ |---|---|
1158
+ | `src/server.ts` runGeneration | Method/cache orchestration moves to inference/engine services; server retains composition and route dispatch |
1159
+ | `src/generate.ts` | Compatibility entry around the AR method/session; one shared prefill and pending-token implementation |
1160
+ | `src/serve/batch-scheduler.ts` | Split queue policy from MLX execution-group mechanics; delete duplicate prefill after parity gates |
1161
+ | `src/serve/generation-gateway.ts` | Planner plus scheduling/resource coordination; remove late feature eligibility and concrete model probes |
1162
+ | `src/serve/{request-plan,completion-executor,completion-sink}.ts` | Evolve into the request/session boundary and semantic adapter; retain tested accounting/framing behavior |
1163
+ | `src/model/{factory,profile}.ts` and concrete graph files | Model bundle registration and graph adapters; replace the central concrete RuntimeModel union in generic consumers |
1164
+ | `src/model/gemma4-base.ts` cache classes | Shared state contracts leave the Gemma module; implementations remain behind MLX state adapters until moved independently |
1165
+ | `src/{prompt-cache,kv-store,ssd-cache}.ts` | Prefix/checkpoint policies use provider codecs and explicit retained snapshots |
1166
+ | `src/{runtime-config,mlx/array,mlx/ffi}.ts` | Bound immutable configuration, explicit runtime opening, native resource owners |
1167
+ | `src/spec/` and `src/fill/` | Typed method/draft/constraint capabilities; existing algorithms and oracle fixtures retained |
1168
+ | `src/pi-web.ts`, Pi provider files, `src/web/src/` | Shared portable protocol declarations, client adapters, one provider-wiring implementation |
1169
+ | `src/memory/`, `src/eval/`, `src/train/`, `src/quantize/`, `src/jobs/` | Completion/task clients and explicit artifact/resource services; preserve task-specific semantics |
1170
+ | `src/serve/isolate.ts`, `src/index.ts`, `src/cli.ts` | Host/client adapters and composition roots; async high-level bootstrap and compatibility exports |
1171
+
1172
+ File moves follow responsibility extraction. Every temporary adapter has a
1173
+ deletion criterion. Do not combine renames, numerical rewrites, and protocol
1174
+ changes in one patch. Preserve failure reproduction fixtures through migration.
1175
+
1176
+ ### 12.10 Implementation stages and dependencies
1177
+
1178
+ Each stage is one or more reviewable PRs. No calendar promise is made before R0
1179
+ establishes the fixture and machine prerequisites. Checkboxes are in PLAN.md.
1180
+
1181
+ **R0. Stabilize and record the baseline.** Pin the compiler and align local/CI
1182
+ typechecks. Reproduce and fix checkpoint stop-policy identity and serial
1183
+ cancellation with targeted tests. Diagnose the recorded Qwen cache-reuse crash
1184
+ before migrating its state implementation. Record exact revisions, existing
1185
+ support/refusal cells, public defaults, ABI/schema versions, and resource
1186
+ ownership. Capture quiet-machine baselines using existing bench tooling.
1187
+ Structural refactoring and model-free validation proceed while target artifacts,
1188
+ quiet hardware, or crash diagnosis are unavailable. Preserve existing numerical
1189
+ and state behavior; changing the affected state implementation or promoting a
1190
+ default still requires its relevant correctness and performance gates.
1191
+ Exit: fixes independently verified, baseline manifest available, missing
1192
+ machine/oracle cells explicitly listed. No unverified previous-review claim
1193
+ becomes a passing gate merely by entering this plan.
1194
+
1195
+ **R1. Land contracts and prove method independence.** Depends on R0's inventory.
1196
+ Create portable interfaces and dependency checks. Implement scripted AR-like
1197
+ and final-output-only fake methods with different private state. Run the exact
1198
+ same session lifecycle suite against both, including cancellation, failure,
1199
+ backpressure, no-consumer behavior, and close. Introduce a legacy adapter around
1200
+ the current executor so existing APIs still run. Exit: engine/session modules
1201
+ import no concrete graph/native module; switching fake methods edits registration
1202
+ only. Finalize the illustrative signatures through these executable contracts.
1203
+
1204
+ **R2. Bind real graphs and backend capabilities.** Depends on R1. Wrap existing
1205
+ models with GraphFactory/ModelBundle bindings, beginning with one dedicated
1206
+ graph and its existing generated counterpart. Bind sampler/state/runtime types,
1207
+ resource estimates, artifact profile, media inputs, taps, and adapter context.
1208
+ Exit: both real graphs run through the same method/session without consumer
1209
+ edits; direct forward logits match their pinned oracle; backend/state mismatch
1210
+ fails before allocation. Keep old models reachable through adapters until their
1211
+ family-specific tests have migrated.
1212
+
1213
+ Also adapt an existing quant-specific path through its bundle and verify that
1214
+ the interface adds no obligatory dequantization, tensor copy, intermediate
1215
+ allocation, or synchronization. Validate the coarse fused-execution contract
1216
+ before standardizing the reusable graph ABI.
1217
+
1218
+ **R3. Establish resource and state ownership.** Depends on R2 and the relevant
1219
+ R0 crash fix. Introduce explicit state views, retained snapshots, device-backed
1220
+ leases, and complete rollback facets. Evolve the codec table into provider-bound
1221
+ codecs. Validate prefix and resume identities, including stop policy and adapter
1222
+ revision. Exit: injected failures release all owners; donor/clone isolation,
1223
+ ring wrap, recurrent rollback, cold restore, and restart fixtures pass. No new
1224
+ GC-dependent lifetime or unsafe mmap release is accepted.
1225
+
1226
+ **R4. Resolve plans and reservations once.** Depends on R1–R3. Move speculation,
1227
+ fill, compilation, cache bypass, and capability selection into one planner.
1228
+ Use exact resolved plan data for execution, metrics, and checkpoint identity.
1229
+ Move native media/constraint allocation behind preparation admission. Capture
1230
+ configuration per binding. Exit: table-driven tests cover every shipped
1231
+ composition and refusal; lane reports agree before/after execution; engine code
1232
+ does not reread mutable global configuration. Existing fallbacks stay explicit.
1233
+
1234
+ **R5. Share AR prefill and session execution.** Depends on R2–R4. Extract a
1235
+ step-able prefill program preserving chunk/eval/quantization/snapshot/final-token
1236
+ order. Route serial execution and current solo batch-prefill through it. Move
1237
+ pending-token, history, and checkpoint-safe-point state into the AR method run.
1238
+ Thread cancellation through queue, preparation, prefill, and decode. Exit:
1239
+ duplicate prefill loops are removed, phase-specific abort tests release the
1240
+ lease, and cold/warm/restored logits plus streamed results match the baseline.
1241
+ This is the Maintainability prefill item; Phase 18 S1a remains separate.
1242
+
1243
+ **R6. Adapt other methods and extensions.** Depends on R3–R5. Move the existing
1244
+ speculative verifier and all retained DraftSource implementations behind typed
1245
+ target views. Adapt fill/grammar acceleration and diffusion without expanding
1246
+ their support. Exit: the same session suite passes for each; accepted/rejected
1247
+ trace, pending-token accounting, stop handling, and recurrent rollback match
1248
+ their method oracle. No session branch identifies AR/spec/diffusion by name.
1249
+
1250
+ **R7. Separate scheduler policy from batch execution.** Depends on R5–R6.
1251
+ Move merge/extract/filter and B=1 adoption into execution groups. Centralize
1252
+ fairness, drain policy, queue bounds, reservations, and cancellation. Exit:
1253
+ dynamic join/leave, uneven rows, slow consumers, group failure, mixed-KV,
1254
+ recurrent state, and ring-wrap cells pass wherever supported; unsupported
1255
+ compositions retain their recorded placement/refusal. Preserve pipeline overlap.
1256
+ Do not merge true B-wide prefill or new concurrency features into this stage.
1257
+
1258
+ **R8. Move applications onto client/task interfaces.** Depends on R1 and R4;
1259
+ runtime cutover follows R5–R7. Separate shared web/Pi protocol declarations,
1260
+ consolidate Pi provider configuration, and route memory/eval through clients.
1261
+ Train/quantize retain task ports and dedicated numerical graphs. Add async
1262
+ high-level library bootstrap and compatibility exports. Exit: CPU-only clients
1263
+ and high-level package imports need no MLX; API/CLI/UI fixtures preserve current
1264
+ behavior; task progress/cancellation and artifact semantics remain intact.
1265
+
1266
+ **R9. Adapt isolation and resource coordination.** Depends on R7–R8. Implement
1267
+ EngineHost over existing isolation/model-pool code, separate parent application
1268
+ state from worker compute, and coordinate managed GPU jobs. Exit: worker startup
1269
+ failure, death mid-stream, bounded restart, model eviction, disconnect, and
1270
+ shutdown tests pass; no automatic POST replay or native object serialization;
1271
+ direct/isolated clients share the semantic contract. Compare transport overhead
1272
+ before any default change. Existing `--isolate` behavior remains available.
1273
+
1274
+ **R10. Cut over, remove duplication, and close.** Depends on R0–R9. Default to
1275
+ the new engine only after the full supported matrix and performance gates pass.
1276
+ Delete legacy executor branches and temporary import exceptions. Review generated
1277
+ twins, diffusion, curve/HLG, expert offload, paged KV, DSpark variants, compiled
1278
+ decode, and Pi wiring using the existing D6 requirement: demonstrated use or
1279
+ measured value, otherwise an explicit deletion decision. Retained experiments
1280
+ register through declared interfaces. Update reference docs and package exports
1281
+ in the same change as any public difference. Exit: replacement exercise below
1282
+ passes, no duplicate engine remains, binary packaging works, and open-phase
1283
+ blocks close without archiving this still-canonical architecture document.
1284
+
1285
+ ### 12.11 Validation and release gates
1286
+
1287
+ **Contract tests on every PR.** Run without native MLX: scripted alternate
1288
+ methods, lifecycle, ownership transfer, capability composition/refusal, stable
1289
+ identity, queue fairness, resource accounting, event ordering/backpressure,
1290
+ partial result/error behavior, and dependency boundaries. Add graph adapter
1291
+ type-tests that reject incompatible backend/state bindings. Existing protocol
1292
+ and UI tests remain their behavioral reference.
1293
+
1294
+ **Numerical and state matrix on available Apple Silicon machines.** Use the
1295
+ pinned oracle and existing machine-layered goldens. Cover dedicated/generated
1296
+ graphs, dense/MoE/recurrent/rotating state, bf16/uniform/mixed/TurboQuant where
1297
+ supported, cold/warm/SSD/resume, B=1 and matching-B concurrent composition,
1298
+ constraints, seeds, logprobs, adapters, media, speculation, and fill. Use targeted
1299
+ pairwise cases plus every known failure reproduction; do not imply every
1300
+ Cartesian combination is supported. Diffusion keeps its distinct oracle gate.
1301
+ Exact cells remain exact. Existing tolerance cells require their recorded
1302
+ justification; a refactor cannot loosen a gate to get green.
1303
+
1304
+ **Lifecycle sequences.** Exercise cancel before preparation, during admission,
1305
+ between prefill chunks, after dispatch but before readback, during verify, during
1306
+ checkpoint write, and on slow/broken streams. Check one terminal outcome and
1307
+ eventual reservation release after device completion. Restore-and-continue must
1308
+ equal uninterrupted execution for the exact supported resume policy. Test
1309
+ incompatible/corrupt checkpoints and failed cleanup. Measure steady memory
1310
+ after synchronization across repeated warm and aborted requests.
1311
+
1312
+ **Frontier scorecard.** Speed, quality, and size are independent axes. Keep a
1313
+ Pareto set rather than collapsing them into one weighted score that hides a
1314
+ regression. Every optimization names its target Mac memory budget, workload,
1315
+ context, and concurrency before measurement.
1316
+
1317
+ | Axis | Required evidence |
1318
+ |---|---|
1319
+ | Speed | Short/long-context TTFT, sustained decode, inter-token latency, concurrent throughput; include startup/compile and cold/warm behavior separately |
1320
+ | Quality | Oracle identity for unchanged numerics; for changed quants/numerics, KL as a screen plus frozen task evaluations, raw-prompt/EOS checks, and representative multi-turn agent/tool outcomes |
1321
+ | Size | Actual artifact bytes and effective bpw including scales/codebooks/sidecars; peak unified-memory footprint at the declared context, including KV, draft model, workspaces and retained snapshots |
1322
+ | Practical experience | Largest useful context that fits, responsiveness during long work, completion correctness, restart behavior, and a reproducible local task with visible user benefit |
1323
+
1324
+ A frontier improvement improves at least one axis without a meaningful loss on
1325
+ the others at the declared measurement precision. A useful tradeoff can also be
1326
+ a new non-dominated point; describe the lost/gained quantities explicitly and
1327
+ apply the intended quality floor. A smaller model that causes an EOS/task
1328
+ failure is not a quality-preserving win because its KL or aggregate benchmark
1329
+ looked similar. Neither faster kernels alone nor smaller on-disk weights alone
1330
+ establish a better product experience.
1331
+
1332
+ Compare to the best relevant measured recipe, including the current shipping
1333
+ bundle and credible same-budget alternatives, not an intentionally weak
1334
+ baseline. Publish artifact provenance, input corpus, evaluator, uncertainty,
1335
+ machine and memory accounting. Keep training/tuning examples separate from the
1336
+ held-out promotion set. Include ordinary laptop use and cold-start experience
1337
+ in product evaluation while keeping quotable paired microbenchmarks quiet.
1338
+
1339
+ Optimization cycle: choose a bottleneck and target budget → propose a quant,
1340
+ layout, graph/kernel or execution recipe → establish its numerical/quality
1341
+ contract → measure the full scorecard → retain the non-dominated recipes →
1342
+ register validated selectors for the appropriate Macs and workloads. Negative
1343
+ findings belong in the topic doc; one-off research scripts are then removed.
1344
+ The existing TurboQuant, decode-speed, KV and speculative programs own their
1345
+ experiments. This architecture supplies the common registration and evaluation
1346
+ contract without duplicating their backlogs.
1347
+
1348
+ Pure refactor PRs may be frontier-neutral. V2 product promotion requires at
1349
+ least one reproducible frontier advance plus a representative local workload
1350
+ that demonstrates the benefit. Until measured, it is an objective, not a claim.
1351
+ Supporting multiple useful quant-specialized implementations is intentional;
1352
+ remove duplicated lifecycle policy, not a demonstrated numerical advantage.
1353
+
1354
+ **Performance regression guard.** Record host/chip/RAM, revisions, artifact, effective plan,
1355
+ concurrency, context, cache warmth and native runtime. Compare paired old/new
1356
+ runs on the same quiet machine with existing tooling. Track startup, short/long
1357
+ TTFT, inter-token latency, decode throughput, concurrent throughput/fairness,
1358
+ peak memory, cancellation delay, and checkpoint stall. Predeclare a 3% median
1359
+ regression investigation trigger for TTFT/decode on each representative cell, with
1360
+ enough repetitions to distinguish it from noise; noisy results are unresolved.
1361
+ Even smaller repeatable losses require a fix or an explicit measured benefit
1362
+ elsewhere. No unexplained growing memory retention or disabled pipeline overlap.
1363
+ The trigger prioritizes investigation; it is not an acceptable abstraction tax,
1364
+ does not redefine a Pareto improvement, and does not permit quality degradation.
1365
+ Report on both dev machines before final cutover; do not compare bits across
1366
+ their different metallibs. Curated numbers go only in benchmarks.md.
1367
+
1368
+ **Replacement exercise.** Before closure, replace a real compatible graph with
1369
+ another registered graph, run a quant-specific coarse/fused implementation
1370
+ without adding session branches, then swap the method for a final-output-only test
1371
+ implementation, then run the client through direct and isolated hosts. Session,
1372
+ scheduler-policy, protocol, and application code must remain unchanged. Changes
1373
+ belong only to the implementation/bundle registration and its tests. This is the
1374
+ acceptance test for Josh's "swap one piece" requirement.
1375
+
1376
+ ### 12.12 Rollout rules and remaining design decisions
1377
+
1378
+ Each stage routes a bounded set of existing configurations through a temporary
1379
+ implementation selector in the composition root. Preserve the old implementation
1380
+ only for paired comparison and rollback while that stage is open. Do not add
1381
+ permanent user modes for intermediate architecture. Rollback selects a coherent
1382
+ bundle and compatible persistence schema; never mix old graph state with a new
1383
+ codec because both happen to implement an interface.
1384
+
1385
+ Decisions still requiring implementation evidence have explicit owners:
1386
+
1387
+ | Decision | Owner / decision gate |
1388
+ |---|---|
1389
+ | Exact event buffering and collect-mode limits | R1 contract tests; no-consumer and slow-consumer behavior must be bounded |
1390
+ | Smallest graph ABI that covers existing taps/media/compiled paths | R2 real graph replacement plus per-family adapters |
1391
+ | Fence/lease implementation and old checkpoint compatibility | R3 native lifetime tests and restore/restart fixtures |
1392
+ | Which batch mechanics can share a method adapter | R7 parity and pipeline-overlap measurements |
1393
+ | Whether finer worker separation should become a default | R9 crash containment, packaging, and transport measurements |
1394
+ | Retirement of each experimental/duplicate implementation | R10 D6 usage or paired measurement; no deletion by interface preference |
1395
+ | Which optimized recipe becomes the automatic choice for a Mac/workload | R10 full frontier scorecard and held-out quality gate; no universal winner assumed |
1396
+
1397
+ R0 fixes are small independent changes. R1–R4 establish and validate the
1398
+ contracts. R5–R7 perform the execution migration. R8–R10 complete applications,
1399
+ isolation, and deletion. Contract drafting must lead to the first real graph
1400
+ replacement in R2; it cannot grow indefinitely without that working proof.
1401
+
1402
+ ### 12.13 Implementation status (2026-09-05)
1403
+
1404
+ Branch: `refactor/interface-engine-v2`. The comparison baseline is
1405
+ `6d45ca1e17e825c48af9b246a37486380117f640` (`origin/main`, fetched again
1406
+ 2026-09-05). Its reference docs and tests freeze the pre-refactor public
1407
+ defaults, supported combinations and refusals; use `git show <revision>:<path>`
1408
+ to inspect that baseline without consulting a moving branch.
1409
+
1410
+ | Baseline dimension | Frozen source / compatibility rule |
1411
+ |---|---|
1412
+ | Flags, defaults and routes | Baseline `docs/reference/{server-config,server-api,cli}.md`; no new architecture mode becomes a default in this phase |
1413
+ | Families, quants and feature support | Baseline `docs/reference/models.md`, model profiles and parity tests; the locally available published Qwen artifacts supply compatibility evidence only |
1414
+ | Native oracle and machine | Baseline `docs/reference/environment.md` plus its machine-layered golden manifests; all branch numerical comparisons reported here use the M1 Max 32 GB and the pinned venv |
1415
+ | Toolchain | Bun 1.4.0 (`34cbb9a40`), TypeScript pinned to 6.0.3; Python MLX/Metal 0.31.2, mlx-lm 0.31.3, mlx-optiq 0.2.15 |
1416
+ | Graph/state representation | Existing MLX hidden `[batch, positions, width]` and legacy cache arrays; newly declared `mlx-hidden-bsh-v1` / `legacy-cache-array-v1` describe those representations without converting them. Denoising uses its distinct `mlx-denoising-v1` graph ABI |
1417
+ | Persistence | SSD prefix format 3 is retained; legacy codec identity remains readable. Generation resume identity advances from the baseline's unversioned policy hash to version 4, including stops, adapter content, implementation and resolved policy |
1418
+ | Ownership | Resident weights remain borrowed; request caches, rollback scratch, native preparation and retained prefix backing release at their existing device-safe boundaries, now expressed through owners and leases |
1419
+ | Performance | Baseline/candidate paired diagnostics exist for available artifacts; quiet-machine preflight did not pass, so none establishes a quotable regression result or frontier advance |
1420
+
1421
+ Outstanding evidence is explicit: Josh's designated quant artifacts, the M4 Pro
1422
+ 24 GB matrix, quiet paired performance, unavailable diffusion/drafter weights,
1423
+ the Qwen 4B B=2/persistence oracle fixtures, and reconciliation of the reported
1424
+ other-machine Metal-memory fix. The crash is diagnosed as a native allocation
1425
+ failure; this branch preserves the affected cache math. These missing cells
1426
+ do not prevent interface work. They keep the full frontier scorecard and
1427
+ production acceptance gate open after the branch implementation is complete.
1428
+
1429
+ R0 fixes pin TypeScript 6.0.3 for
1430
+ local/CI checks, add stop strings and a version to generation-checkpoint
1431
+ identity, remove cancelled serial waiters immediately, and check cancellation
1432
+ between AR/spec target-prefill chunks and committed-token boundaries.
1433
+
1434
+ `src/contracts/generation.ts` defines portable planner, method/run, session,
1435
+ output, result, cancellation, and timer ports. `src/engine/` implements lifecycle
1436
+ and delivery without model or runtime imports. The dedicated TypeScript project
1437
+ has ES2022 only and no ambient runtime types; an AST/resolved-import test also
1438
+ checks type imports, re-exports, and dynamic imports against dependency layers.
1439
+
1440
+ Streaming starts on the first reader demand. Defaults are 32 queued events,
1441
+ 256 queued token IDs, 20 top-logprob entries per token, and a 30-second
1442
+ idle-consumer deadline. Large committed spans split under backpressure.
1443
+ Collecting reserves a Uint32 buffer before method creation, with an aggregate
1444
+ 65,536-token active-collection ceiling; terminal output ownership transfers to
1445
+ the caller. Collection currently returns IDs only; logprobs require streaming.
1446
+ Cancellation and early iterator return wait for run cleanup. Failure outcomes
1447
+ retain the execution error if cleanup also fails. These are internal defaults,
1448
+ not new server flags.
1449
+
1450
+ HTTP completion execution now uses the shared session by default.
1451
+ `createSessionCompletionEngine` delivers tokens directly to the existing
1452
+ stop/tool parser through callback output, avoiding a second queue. The callback
1453
+ is awaited and can request a normal stop; cleanup completes before the result
1454
+ settles. Streaming/collection library consumers retain their bounded delivery.
1455
+ AR, speculative and denoising methods honor callback stops. Denoising now settles
1456
+ its emitted-token count when delivery closes before the completed canvas ends.
1457
+ `src/backends/mlx/text-engine.ts` is the data-only token API. It snapshots requests
1458
+ before demand-start and shares `createCompletionMethod` with HTTP. Native media,
1459
+ adapters and grammar still require prepared inputs; their HTTP requests now run
1460
+ through the same session lifecycle. The temporary text-only legacy engine is gone.
1461
+
1462
+ `ModelServingBinding` owns the gateway binding, serial execution construction,
1463
+ prompt/media preparation, SSD restore, signal evaluation, embedding execution and
1464
+ model diagnostics. The server consumes model metadata and this binding.
1465
+ `MlxGatewayBinding` owns compatibility decisions and batch-group construction;
1466
+ method IDs are implementation-owned strings. A model can expose several methods
1467
+ with unrelated private state. Existing classes enter through the MLX binder;
1468
+ replacement implementations supply the port directly. `loadContext` selects an
1469
+ engine-owned host implementation before either default weight loader runs.
1470
+ The HTTP replacement test selects a synthetic exact artifact registration and
1471
+ runs two custom methods with no concrete model, forward function or cache factory.
1472
+ This proves the construction-to-session path, not quant quality or performance.
1473
+
1474
+ `src/inference/graph.ts` declares the backend-bound graph ABI.
1475
+ `src/backends/mlx/graph.ts` binds synchronous/streamed forwards once and selects
1476
+ hidden positions before vocabulary projection. `src/backends/mlx/autoregressive.ts`
1477
+ defines one `MlxAutoregressiveBinding` for the graph, cache construction, media
1478
+ forwarding, adapter state, memory accounting, and optional compiled/fused decode.
1479
+ `generateAutoregressive()` consumes that interface without requiring a
1480
+ `RuntimeModel`. The existing `generate()` API adapts AR models into it, preserving
1481
+ the serial loop's sampling, prefill chunks, lazy evaluation, and decode pipeline.
1482
+ No tensor wrapper, device readback, or synchronization was introduced.
1483
+
1484
+ Concrete Gemma compiled-decode selection and rollback-based fallback now live
1485
+ in the legacy binding. A replacement supplies its own decoder; the loop never
1486
+ infers one from the model family. A declined step must leave state unchanged.
1487
+ Unrecovered errors propagate without retry. Bindings must declare the MLX
1488
+ hidden-state ABI and legacy `Cache[]` ABI; mismatches fail before cache allocation.
1489
+
1490
+ The host now captures AR/denoising and speculative bindings once. Compiled
1491
+ replay permission and grammar jumping are resolved alongside placement and
1492
+ included in checkpoint identity. Serial execution consumes that policy; B=1
1493
+ batch replay also honors it, with dynamic cache geometry still allowed to
1494
+ decline replay transactionally. A lazily created batch group inherits the
1495
+ gateway's runtime snapshot. Composition and snapshot tests cover later config
1496
+ changes, adapters, paged/media fallback, speculation, denoising and logprobs.
1497
+ Legacy kernel flag reads now resolve through an execution-local runtime snapshot.
1498
+ AR/denoising generator resumes and cleanup, the speculative verifier and its
1499
+ memory scope, and each batch driver retain that snapshot across awaits. A later
1500
+ host configuration change cannot alter a running binding's kernel policy.
1501
+ Nested/concurrent scope tests and native binding tests cover early close,
1502
+ verification callbacks and lazy batch construction. The scope adds no tensor
1503
+ copy, evaluation or device fence; its performance remains part of the paired gate.
1504
+ Rejected initial state releases owned caches while preserving borrowed caches.
1505
+ Decoder cleanup completes before cache disposal, including early iterator return.
1506
+ If execution and decoder cleanup both fail, the aggregate retains both errors.
1507
+ The legacy descriptor is not a persistence identity. Rich media remains a native
1508
+ preparation input; tap capabilities and provider-bound persistence are described
1509
+ below. A different cache ABI requires its own compatible method/backend binding.
1510
+
1511
+ Serial cache lookup, checkpoint replay/persistence and prompt-boundary snapshots
1512
+ now live in `src/backends/mlx/serial-executor.ts`. The HTTP server injects a bound
1513
+ generator/verifier, cache constructor, prefix/checkpoint stores, clone function
1514
+ and adapter namespace service. Native Qwen media context is installed/restored
1515
+ by its compatibility binder. The executor has no concrete model dispatch;
1516
+ the existing gateway still owns scheduling and exclusive execution.
1517
+ Native input ownership starts before cache lookup/construction. A failed lookup,
1518
+ cache constructor or trace hook releases prepared grammar/media and any acquired
1519
+ cache state; one failed destructor does not skip sibling releases or replace
1520
+ the execution error. Checkpoint replay checks cancellation before each saved
1521
+ token and retains its durable checkpoint when interrupted.
1522
+ Failed prefix trimming releases acquired clone/restored state. Prefix insertion
1523
+ validates cache metadata before adopting ownership; eviction cleanup errors are
1524
+ reported after adoption without inviting the caller to dispose the live stored
1525
+ entry. Clear detaches entries and attempts cleanup for every sibling. Backing
1526
+ retention is released only after successful cache disposal. Rejected optional
1527
+ prompt-boundary snapshots release their clones while generation retains its
1528
+ live state. These are failure-path ownership changes, with no cache-math change.
1529
+
1530
+ Model-free binding tests exercise an independent graph, a replacement fused
1531
+ decoder, declined steps, unrecovered errors, and unsupported ABI/media/adapter
1532
+ requests. The native `generated-parity` gate runs dedicated and generated Gemma
1533
+ bindings through the same gateway method and portable session consumer. Tokens
1534
+ and cache coverage match, and its counter verifies the generated path executed.
1535
+ This is a compatibility gate using available weights, not a product speed claim.
1536
+ The binding migration also passes all eight compiled-decode parity/recovery
1537
+ checks and the Qwen3.8-27B TQ smoke below on the M1 Max 32 GB.
1538
+
1539
+ `src/model/implementation.ts` defines the backend-parameterized
1540
+ `ModelImplementation` construction port and immutable registration table.
1541
+ `ModelExecutionComposition.implementation` lets an exact engine-owned artifact
1542
+ profile name its code. Existing identity/config guards perform the matching;
1543
+ the registry does not introduce another artifact matcher. `src/model/factory.ts`
1544
+ now registers the existing resident constructors, including generated Gemma,
1545
+ and selects one before construction. `openModel` checks the binding before
1546
+ opening weights; callers can compose profiles and implementations through its
1547
+ options. `createModel` accepts a registry for already-open weights. No selection
1548
+ is added to the token loop, and no model file format changes.
1549
+ Both factories preserve a supplied registry's return interface; replacement
1550
+ implementations need not join the concrete `RuntimeModel` union. Construction
1551
+ does not select the request's execution method. An implementation can support
1552
+ several methods, including quant-specific paths, while the session consumes
1553
+ the selected method's execution, output and cleanup contract. The shared
1554
+ contract does not require a shared numerical loop.
1555
+
1556
+ Unknown artifacts retain existing family selection. Missing implementations,
1557
+ graph/loader/loop mismatches, duplicate IDs, and inconsistent exact identities
1558
+ fail explicitly. Construction failures propagate without trying another graph.
1559
+ Model-free tests exercise the factory with a synthetic exact quant identity and
1560
+ an alternate constructor; this establishes selection, not numerical or speed
1561
+ claims. No registration pretends to identify Josh's inaccessible quants.
1562
+ This step covers resident graph construction. Colibri retains its existing
1563
+ streamed opener and refuses supplied resident registries or named overrides
1564
+ until its resident loader binding exists. Existing class-based library exports
1565
+ remain available through compatibility binders. The server consumes the
1566
+ model-owned serving port; host implementations can own resident or streamed
1567
+ loading. Specialized inference methods remain.
1568
+
1569
+ Artifact-parameterized native smoke gate:
1570
+ `MLX_BUN_QWEN_QUANT_PATH=/path/to/our/artifact bun test tests/parity/qwen-quant-engine.test.ts`.
1571
+ Run one artifact per process. Compatibility-only checks on the M1 Max 32 GB
1572
+ pass for the locally cached published `mjriii/Qwen3.8-27B` and
1573
+ `mjriii/Qwen3.8-27B-TQ`: direct-versus-bound logits, legacy-versus-session greedy
1574
+ output/cache coverage, and repeated recurrent-prefix borrowing. These are not
1575
+ Josh's current target quants. Target validation awaits access to those artifacts;
1576
+ implementation continues independently. The test is a bounded smoke, not a full model oracle or the
1577
+ recorded turn-8 repro. Gemma compiled-decode checks are secondary coverage too.
1578
+
1579
+ R1 is complete. Incremental and final-only producers run the same lifecycle
1580
+ cases for collection/streaming, blocked-consumer cancellation, partial failure,
1581
+ and no-consumer/pre-cancelled settlement. Additional tests cover preparation
1582
+ cancellation, admission refusal, metadata bounds, and cleanup failures.
1583
+
1584
+ R3 now gives request resources one checked owner. Transfer invalidates the
1585
+ previous owner, close is idempotent even after a destructor failure, and cleanup
1586
+ attempts all releases while preserving the original execution error. MLX state
1587
+ views expose a lease; prefill and prefix-cache accounting close it without
1588
+ knowing whether handles are borrowed or temporary. Only the compatibility
1589
+ adapter reads the legacy ownership marker. No tensor copy, evaluation or device
1590
+ synchronization is added by these leases. Fault-injection tests cover transfer,
1591
+ partial view acquisition and throwing destructors. Existing native fences remain
1592
+ in place; codec binding and persistence identity are described below.
1593
+
1594
+ R4 now resolves method, scheduling mechanism, paged-KV fallback, prompt-cache
1595
+ bypass, fill eligibility and checkpoint eligibility in one portable planner.
1596
+ The gateway passes that immutable decision to serial execution; lane accounting
1597
+ uses the actual method, including AR fallback with a configured draft. Request
1598
+ policy arrays/maps are copied and frozen at admission. AR runtime settings and
1599
+ batch work flags use a binding snapshot. Native grammar/media preparation takes
1600
+ the gateway execution lease before allocation; text rendering remains outside
1601
+ that lease. Cancellation before/during preparation is covered with allocation
1602
+ and cleanup assertions.
1603
+ Native AR/speculative/denoising method creation now uses the same policy snapshot
1604
+ as HTTP admission. Scalar settings and nested policy arrays/maps cannot change
1605
+ between registration and the first reader demand. A binding without an explicit
1606
+ runtime captures the host default at method creation; kernels execute under
1607
+ that snapshot. Native caches, controllers and callbacks retain their declared
1608
+ ownership instead of being cloned with policy data.
1609
+
1610
+ Checkpoint request identity is now canonical SHA-256 over sampling/stop policy,
1611
+ resolved execution and the artifact/implementation/state ABI. Object insertion
1612
+ order is ignored; array order remains meaningful. Version-4 keys include the
1613
+ resolved compiled/grammar policy; version-2/3 generation keys do
1614
+ not resume through this version; ordinary prefix-store format is unchanged.
1615
+ Nine native scheduling gates pass after planner integration, including rotating
1616
+ dynamic joins, row failure containment, serial drain, prefix sharing, SSD restore
1617
+ and compiled B=1. Preparation reservations now remain owned through execution; see below.
1618
+
1619
+ R3 state persistence now uses `CacheCodecProvider`, bound through the server's
1620
+ model context, RAM cloner, batch snapshots and SSD store. A provider supplies
1621
+ clone/snapshot/restore/trimmability together; ambiguous matches refuse. Persisted
1622
+ provider identity is checked before cache allocation or mmap. Earlier v3 files
1623
+ implicitly use the legacy provider; a different provider must identify itself.
1624
+ Restore still copies one tensor at a time and unmaps only after those copies,
1625
+ so this change introduces no new device/backing lifetime assumption. Failed
1626
+ clone and snapshot construction release intermediate owned views.
1627
+
1628
+ Adapter namespaces now hash the weight file plus scale/rank policy once at
1629
+ mount. Remounting the same name with different bytes or scale cannot reuse its
1630
+ old RAM prefix or generation checkpoint. The shared server cache uses that
1631
+ namespace for both lookup and persistence. Codec provider fault/round-trip tests
1632
+ pass; native persistence passes five cells with one unavailable golden skipped,
1633
+ and dedicated/generated Gemma passes six cells, including direct logit identity.
1634
+
1635
+ R5 now shares a portable prefill program between serial AR and solo batch
1636
+ admission. Its MLX executor preserves drain evaluation, KV maintenance, allocator
1637
+ clears, stable-boundary snapshots, and the separate final-token forward. Batch
1638
+ admission retains its short-tail scheduling behavior. Six native scheduler
1639
+ checks pass, including dynamic rows, prompt reuse, SSD restore and compiled B=1;
1640
+ the dedicated/generated same-session test also passes on the M1 Max 32 GB.
1641
+ The native AR method run owns its generator's pending tokens, history and
1642
+ checkpoint boundary. Phase-specific tests now cover cancellation before any
1643
+ cache allocation, after a prefill chunk, after decode dispatch/before emission,
1644
+ and while an asynchronous checkpoint borrows live state. Cancellation waits for
1645
+ the checkpoint to finish before releasing state. Current-token logprob arrays
1646
+ remain owned until readback succeeds, including forward/grammar failure;
1647
+ cancelled execution stops before emission or a subsequent checkpoint write.
1648
+
1649
+ R6 now runs the speculative verifier through `MlxSpeculativeBinding`. Target
1650
+ forwards, projection, tap capture, draft construction and kernel pinning bind
1651
+ outside the verifier. Legacy tap contexts restore their previous value on exit.
1652
+ The complete rollback facet validates recurrent begin/commit/replay together;
1653
+ partial layer failures invalidate the transaction and the run discards its state.
1654
+ Ring-capacity fallback retains its strict pre-write boundary. The verifier owns
1655
+ input/hidden scratch on failure and attempts every final release.
1656
+
1657
+ AR and speculative methods now implement `InferenceMethod` directly. Their shared
1658
+ host adapter translates cancellation and waits for native cleanup. The same
1659
+ session tests exercise collection, streaming, early reader closure and forward
1660
+ failure for both. Four native Qwen3.5-0.8B gates pass, including recurrent
1661
+ rejections and identical speculative acceptance traces through a portable
1662
+ session. This smaller available model supplies compatibility coverage only.
1663
+ Denoising now implements that same method/session contract through a separate
1664
+ `DenoisingGraph<Tensor, State>`. Its test binding uses a position-only state
1665
+ record, with no AR cache or token-prefix API. The existing synchronous diffusion
1666
+ entry and cooperative generation drain one denoising program. Cooperative calls
1667
+ yield between denoising steps and publish only the final output; cancellation
1668
+ releases canvas, feedback, embedding weights and graph state. Generation holds
1669
+ the existing exclusive runtime lease, including the diffusion global RNG.
1670
+
1671
+ Scripted regression tests cover both samplers, RNG-dependent canvases across
1672
+ multiple blocks, sync/async identity, preparation failure, cancellation between
1673
+ steps and repeated-run native memory accounting. The existing soft-embedding
1674
+ lifetime test passes. Diffusion weights are absent on this machine, so its
1675
+ weight/oracle gates remain unrun. Draft sources now consume declared target
1676
+ ports for assistant donor views/scaled embeddings, Gemma hidden taps/output
1677
+ projection and Qwen MTP embeddings/output projection. The target view contains
1678
+ no concrete model or cache array; the MLX compatibility binder owns family
1679
+ checks and donor layout. Native GLM providers compare an opaque target identity
1680
+ before borrowing their own model-owned weights. Both retained DSpark variants
1681
+ use the projection interface. Missing extensions refuse before draft allocation.
1682
+ An independent assistant target fixture proves construction and draft cleanup
1683
+ without a model class; donor acquisition releases partial views on failure.
1684
+ The locally cached published Qwen3.8-27B and its bundled MTP head pass the
1685
+ same-artifact greedy comparison after this migration. This does not establish
1686
+ performance or compatibility for Josh's unavailable exact quants.
1687
+
1688
+ R7 now separates the portable scheduling driver from `MlxBatchExecutionGroup`.
1689
+ The driver sees queue/active counts, admission readiness and bounded work units;
1690
+ it imports no model, cache, tensor or runtime implementation. Native admission,
1691
+ merge/extract/filter, sampling and pipeline state live in `src/backends/mlx/`.
1692
+ A compatibility re-export preserves existing batch-scheduler imports.
1693
+
1694
+ The driver retains short-admission grouping, long-prefill interleaving, serial
1695
+ drain and the 25 ms single-row responsiveness budget. Six native scheduler
1696
+ checks and two native failure-containment/serial-drain checks pass. Policy tests cover those ordering rules, execution failure and
1697
+ lease release; backend fault tests cover a request removed from the queue when
1698
+ admission fails, retained-prefix cleanup, shutdown and submission after close.
1699
+ Closing a group stops at a safe boundary, rejects pending/active requests and
1700
+ releases owned state. Bounded request and preparation queues now reject overflow explicitly. The full
1701
+ mixed-state concurrency/performance matrix remains open.
1702
+
1703
+ R8 now shares Pi/browser messages, history, sampling data and route IDs in
1704
+ `src/contracts/pi.ts`. Compatibility re-exports preserve server-side imports.
1705
+ The browser TypeScript project has no Bun ambient types; Bun UI tests remain
1706
+ covered by the root project. The resolved-import gate prevents browser modules
1707
+ from importing server implementations, including type-only imports.
1708
+
1709
+ R4/R7 admission now uses a portable FIFO `AdmissionPool`. The gateway bounds
1710
+ active requests and queued waiters; cancelled waiters do not release another
1711
+ request's capacity. Media and grammar preparation reserve capacity before native
1712
+ allocation. The request owner retains that reservation after transferring native
1713
+ resources to execution, then releases it on success, failure or cancellation.
1714
+ The direct batch group also bounds pending submissions. Overload is explicit in
1715
+ the existing HTTP/SSE error contract. Nine native scheduling checks pass after
1716
+ this integration, including rotating joins, failure containment, prefix/SSD reuse
1717
+ and compiled B=1. The gateway's shared execution lease covers preparation,
1718
+ generation and managed GPU jobs.
1719
+
1720
+ R8 is complete: completion/batch/task contracts are portable. Memory calls accept an injected
1721
+ client; their existing native implementation moved behind a lazy backend import.
1722
+ Eval generation accepts a replacement completion client and retains its separate
1723
+ bit-exact greedy oracle path in the MLX backend. Training and quantization keep
1724
+ their numerical runners and artifact results; both job execution modes now use
1725
+ the task adapter. Progress delivery is synchronous and unbuffered; cooperative
1726
+ cancellation checks before work and at progress boundaries. Native work already
1727
+ dispatched still completes at its safe boundary.
1728
+
1729
+ Pi's embedded registry and generated extension use one model-definition builder.
1730
+ Job and Pi/browser messages now share portable declarations. New `mlx-bun/engine`
1731
+ and `mlx-bun/client` exports import without MLX; `initializeMlx()` explicitly
1732
+ bootstraps the compatibility API. The existing root import remains compatible.
1733
+ The JSON completion client uses either HTTP or an `EngineHost` without changing
1734
+ callers, preserves abort, and never retries completion POSTs.
1735
+
1736
+ R9 adapts the existing `EngineChild` into `EngineHost`, bounds restart attempts,
1737
+ cancels restart backoff on close, kills failed startup, and checks disconnect
1738
+ while waiting for readiness. Model-pool startup failures and shutdown close their
1739
+ owned children; an evicted default can be resolved again for an absent/unknown
1740
+ model request. The fake-host proxy suite now runs in the model-free CI tier.
1741
+ Real MiniCPM JSON and incremental SSE requests pass through the isolated host.
1742
+
1743
+ Managed GPU subprocess jobs reserve the same gateway lease as inference: the
1744
+ current batch drains before spawn, and the job retains the lease through child
1745
+ exit, including crashes. Host shutdown cancels admission/queued jobs and terminates
1746
+ its active job before releasing the lease. Fault tests cover rejected admission,
1747
+ spawn failure, child death and shutdown. The isolation parent now owns job/settings routes and their store. A portable
1748
+ shared/exclusive coordinator covers every worker's startup, restart, response
1749
+ body and eviction. Before GPU job spawn, live UDS lease responses reserve each
1750
+ worker's gateway after durability flush; disconnect releases the worker lock.
1751
+ Those connections remain owned through subprocess exit, not merely until a
1752
+ client HTTP disconnect. Native MiniCPM checks verify that generation waits for
1753
+ the parent lease and resumes after release. Worker death does not remove parent
1754
+ job records. In-process task cancellation waits for runner cleanup before closing
1755
+ the store. The parent serves the same embedded web assets and owns Responses
1756
+ conversation history; direct and isolated serving share conversation-resolution
1757
+ policy. Workers skip duplicate history retention for parent-owned requests. A
1758
+ real worker replacement preserves previous-response continuation and streamed
1759
+ metadata. WebSocket chat remains an explicit unsupported isolation cell. No
1760
+ process-host default has changed. R9's direct/isolated transport comparison is
1761
+ complete on the M1 Max using the same MiniCPM artifact and CLI/request cells:
1762
+ completion and chat probes each matched 64 greedy tokens; cold, warm, SSD
1763
+ restart and concurrent legs completed. The run is diagnostic because the quiet
1764
+ gate failed. The optional `mlx-bun-isolated` benchmark arm accounts for parent
1765
+ and worker RSS; raw reports stay machine-local. R9 is complete for the retained
1766
+ opt-in host; promotion and the other machine's scorecard remain R10 gates.
1767
+
1768
+ The current model-free suite passes; root, browser and portable typechecks pass.
1769
+ Native state checks cover mixed-KV oracle logits, wrapped-ring conversion,
1770
+ speculative recurrent rollback and persistence. The Qwen B=2 oracle and one
1771
+ Qwen persistence fixture are absent and skip. The binary builds and its CLI/Pi
1772
+ asset smoke passes. Managed jobs now self-exec the packaged binary; literal
1773
+ runner imports retain quantize/train code in the bundle, and a compiled
1774
+ subprocess smoke verifies progress and terminal persistence. These checks do not replace the unavailable artifact/machine
1775
+ cells or the final production acceptance gate.
1776
+
1777
+ The Qwen replay now has a native exception diagnosis: Metal reports insufficient
1778
+ memory from its completion handler. Details remain in the existing turn-8 repro.
1779
+ Josh reports a possible fix on the other machine; reconcile it before changing
1780
+ the affected state path. The 2026-09-05 benchmark preflight found 4 GB of existing swap and high
1781
+ background CPU load. Measurements taken without that gate are diagnostic only.
1782
+ Quiet-machine baselines remain open. Remaining R0–R10
1783
+ work is tracked in PLAN.md; no speed/quality/size improvement is claimed yet.
1784
+
1785
+ The first per-file native matrix at `a75cd39` ran 84 local parity files with
1786
+ batch-decode opt-in: 338 passes, 44 skips and seven failed assertions. Five
1787
+ audio failures were a fixture path left behind by the test-directory move.
1788
+ The two numerical failures reproduced on baseline `6d45ca1`: the local CPM
1789
+ mixed-KV blobs did not match their manifest hashes, and CPM extend-join fell
1790
+ back to the M4 reference because no M1 override existed. The pinned Python
1791
+ oracle regenerated the mixed-KV manifest's exact hashes and an M1 extend-join
1792
+ trajectory identical to the implementation. The corrected batch suites pass
1793
+ 15 tests with one unavailable e4b oracle skip. Tests now check blob hashes;
1794
+ the mixed-KV generator writes blobs and manifests to the same resolved output
1795
+ directory. A second per-file run enables the local train/quantize/LoRA/generated
1796
+ and model-family gates. Two rotating-quantized continuation failures also
1797
+ reproduced on the baseline: the test compared `generate()`'s bf16-head/quantize/
1798
+ tail prefill with a fully quantized single-forward oracle. The generator now
1799
+ records both procedures separately. Existing mechanics, single-forward logits
1800
+ and trajectories regenerated unchanged; the matching serving trajectories pass
1801
+ the original prefix threshold. All six rotating tests pass. Missing new
1802
+ continuation fixtures skip explicitly on other machines until regeneration.
1803
+ A process that exits successfully without registering tests is not a passing
1804
+ matrix cell; absent large-model/drafter fixtures remain open.
1805
+ The production test runner now uses a fresh process for each native test file,
1806
+ preserving model residency and runtime isolation between cells. The complete
1807
+ default runner passed at `2a14021`, including hygiene,
1808
+ all TypeScript projects, the model-free suite and available parity/research
1809
+ tests. Optional cells are reported separately; a green default run does not
1810
+ imply unavailable weights or opt-in cases ran.
1811
+ That run reports 2,344 passes, 98 skips and no failures, with 109 native files
1812
+ each receiving a fresh process.
1813
+ The expanded runner at `ab0db74` enabled the locally applicable model-family,
1814
+ training, quantization, LoRA and batch-decode gates: 2,427 passes, 44 skips and
1815
+ no failures across the model-free tier and 109 native files. Ten native files
1816
+ registered no tests because their configured model/drafter fixtures were absent;
1817
+ those are unavailable cells, not passes. At `b9b56aa`, the model-free suite
1818
+ passed 1,718 tests with 10 skips. All TypeScript projects, hygiene and compiled
1819
+ CLI/Pi/assets/job smokes passed. The subsequent focused native run passed 44
1820
+ tests covering batch lifecycle, SSD state, generated graphs, ngram verification,
1821
+ both available published Qwen graph/session bindings and the published Qwen MTP
1822
+ target/draft pair. Explicit MTP paths supplied one previously unavailable cell.
1823
+ These results do not close the target-artifact, second-machine or default-cutover
1824
+ gates.
1825
+
1826
+ The sequential Gemma e4b repeat compares baseline `6d45ca1` with `5e3b92e`
1827
+ using default and serial HTTP arms, the same artifact, a 4K context and 96
1828
+ requested tokens. Both arms preserve the completion/chat greedy probes and
1829
+ durable restart state. The earlier long-context prefill slowdown and default-arm
1830
+ RSS increase did not reproduce; warm-cache latency still varies across runs.
1831
+ Background load and existing swap keep these reports diagnostic. This repeat
1832
+ does not establish a speed win or satisfy the quiet-machine frontier gate.
1833
+
1834
+ R10's implementation-retention review currently records these decisions. None
1835
+ of the open measurement cells changes an existing numerical default:
1836
+
1837
+ | Implementation | Current disposition and remaining evidence |
1838
+ |---|---|
1839
+ | Pi provider wiring | Consolidated into one builder; generated-extension and binary asset tests pass. Duplicate policy is removed |
1840
+ | Dedicated/generated Gemma | Both register through the engine and pass the same session/oracle gates. Keep both during paired comparison; numerical equivalence alone does not choose a winner |
1841
+ | Compiled decode | Retained behind the declared decoder capability and resolved eligibility. Existing recovery/parity gates pass; final paired regression measurements remain required |
1842
+ | Diffusion | One denoising program and method interface replace duplicate orchestration. Real model/oracle fixtures remain unavailable; no promotion or deletion decision is implied by scripted tests |
1843
+ | Assistant/DSpark/DeepSpec/MTP | Typed draft target ports replace concrete target dispatch. Available Qwen MTP and ngram verification pass; source-specific drafter validation and the DSpark wall-clock decision remain open in the speculative program |
1844
+ | Curve/HLG and paged KV | Existing opt-in placement and refusals remain. Their experiment-specific quality/performance gates remain open; this refactor does not make them defaults |
1845
+ | Expert offload | Existing streamed GLM loading/task semantics remain behind native bindings; model-free streamed/provider tests and packaged task entry pass. The larger-model value/footprint decision remains in its own program |
1846
+ The final server cutover is implemented at `015f56d`. The model-free suite passes
1847
+ 1,732 tests with 10 skips. Fresh native processes run all 109 parity/research
1848
+ files with the locally applicable opt-ins, including the published Qwen target
1849
+ and MTP paths: 720 passes, 33 skips, no failures. Nine fixture-dependent files
1850
+ register no tests and remain unavailable cells. The second published Qwen TQ
1851
+ artifact also passes the graph/session test. The public replacement and import/
1852
+ docs boundary checks pass after the final exports. Commit `d1abd60` also puts
1853
+ embeddings under the shared GPU lease. Its new ownership test, the public
1854
+ replacement test and both native embedding-route tests pass. All three
1855
+ TypeScript projects pass, as does hygiene. The standalone binary built at
1856
+ `d1abd60` passes CLI, Pi asset and managed-job subprocess smoke checks.
1857
+
1858
+ The final HTTP comparison uses the frozen `6d45ca1` baseline and this branch on
1859
+ the M1 Max 32 GB, with MiniCPM5 and Gemma e4b, default and serial arms, a 4k
1860
+ context and 96 generated tokens. The first candidate run overlapped a repository
1861
+ hygiene scan and showed slower e4b long prefill and SSD restart. A sequential
1862
+ e4b repeat without other checks removed the long-prefill gap, but restart
1863
+ latency still varied. A further candidate-then-baseline default-arm pair removed
1864
+ that restart gap too. Completion and chat parity probes match between default
1865
+ and serial arms; every restart restores a durable prefix with no failed or
1866
+ dropped spills. These loaded-machine diagnostics do not establish a quiet
1867
+ performance acceptance result. Raw reports remain local in
1868
+ `/tmp/mlx-bun-cutover-{baseline,candidate}-report.md`,
1869
+ `/tmp/mlx-bun-cutover-repeat-{baseline,candidate}-report.md` and
1870
+ `/tmp/mlx-bun-cutover-reverse-{baseline,candidate}-report.md`.
1871
+
1872
+ R1–R9 implementation and R10 code migration are complete. Existing model-class
1873
+ exports use backend compatibility binders; there is one default session lifecycle
1874
+ and no alternate legacy HTTP executor. R10's retained implementations keep their
1875
+ existing placement and numerical defaults. Their independent experiment programs
1876
+ still own promotion/deletion decisions. Quiet performance, exact target quants,
1877
+ unavailable oracle/drafter fixtures and the second machine remain acceptance
1878
+ work, not missing interface code. No frontier advance is claimed by this refactor.