mlx-bun 0.2.0 → 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 (364) hide show
  1. package/README.md +71 -588
  2. package/docs/README.md +91 -0
  3. package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
  4. package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
  5. package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
  6. package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
  7. package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
  8. package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
  9. package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
  10. package/docs/design/batching.md +569 -0
  11. package/docs/design/decode-speed-program.md +3178 -169
  12. package/docs/design/dreaming-nightly-pipeline.md +675 -282
  13. package/docs/design/generic-model-support.md +579 -334
  14. package/docs/design/kv-cache.md +680 -0
  15. package/docs/design/orpo-training.md +673 -1239
  16. package/docs/design/speculative-decoding.md +1627 -0
  17. package/docs/design/turboquant.md +2102 -0
  18. package/docs/design/unified-engine-frontier-plan.md +1831 -727
  19. package/docs/design/web-chat-redesign.md +666 -1083
  20. package/docs/planning/PRODUCT_ROADMAP.md +34 -1
  21. package/docs/planning/release-notes-v0.3.0.md +35 -0
  22. package/docs/planning/video-series.md +2 -2
  23. package/docs/reference/benchmarks.md +1162 -0
  24. package/docs/reference/cli.md +394 -159
  25. package/docs/reference/distribution.md +321 -182
  26. package/docs/reference/environment.md +348 -0
  27. package/docs/reference/glossary.md +39 -0
  28. package/docs/reference/library-api.md +566 -168
  29. package/docs/reference/memory.md +227 -64
  30. package/docs/reference/models.md +104 -6
  31. package/docs/reference/server-api.md +886 -425
  32. package/docs/reference/server-config.md +638 -330
  33. package/docs/reference/training.md +393 -254
  34. package/docs/reference/troubleshooting.md +166 -0
  35. package/package.json +9 -4
  36. package/src/audio/decode.ts +2 -2
  37. package/src/audio/features.ts +2 -2
  38. package/src/backends/mlx/autoregressive.ts +108 -0
  39. package/src/backends/mlx/batch-group.ts +1646 -0
  40. package/src/backends/mlx/diffusion.ts +39 -0
  41. package/src/backends/mlx/draft-target.ts +47 -0
  42. package/src/backends/mlx/evaluation.ts +105 -0
  43. package/src/backends/mlx/fill-append.ts +45 -0
  44. package/src/backends/mlx/gateway-binding.ts +71 -0
  45. package/src/backends/mlx/graph.ts +38 -0
  46. package/src/backends/mlx/memory-client.ts +189 -0
  47. package/src/backends/mlx/memory-guard.ts +40 -0
  48. package/src/backends/mlx/methods.ts +90 -0
  49. package/src/backends/mlx/model-prompt.ts +164 -0
  50. package/src/backends/mlx/model-serving.ts +79 -0
  51. package/src/backends/mlx/prefill.ts +33 -0
  52. package/src/backends/mlx/request-policy.ts +20 -0
  53. package/src/backends/mlx/rollback.ts +51 -0
  54. package/src/backends/mlx/serial-executor.ts +273 -0
  55. package/src/backends/mlx/speculative.ts +114 -0
  56. package/src/backends/mlx/state-views.ts +26 -0
  57. package/src/backends/mlx/text-engine.ts +73 -0
  58. package/src/cli.ts +58 -25
  59. package/src/client.ts +66 -0
  60. package/src/config.ts +21 -4
  61. package/src/contracts/completion.ts +16 -0
  62. package/src/contracts/execution.ts +49 -0
  63. package/src/contracts/generation.ts +103 -0
  64. package/src/contracts/host.ts +7 -0
  65. package/src/contracts/jobs.ts +66 -0
  66. package/src/contracts/pi-provider.ts +30 -0
  67. package/src/contracts/pi.ts +259 -0
  68. package/src/contracts/resources.ts +18 -0
  69. package/src/contracts/scheduling.ts +27 -0
  70. package/src/contracts/tsconfig.json +14 -0
  71. package/src/diffusion/diffusion-generate.ts +218 -153
  72. package/src/embed.ts +1 -1
  73. package/src/engine/admission.ts +59 -0
  74. package/src/engine/cancellation.ts +38 -0
  75. package/src/engine/engine.ts +56 -0
  76. package/src/engine/execution-coordinator.ts +70 -0
  77. package/src/engine/execution-plan.ts +51 -0
  78. package/src/engine/resources.ts +42 -0
  79. package/src/engine/scheduler.ts +67 -0
  80. package/src/engine/session.ts +247 -0
  81. package/src/eval/README.md +44 -1
  82. package/src/eval/kl.ts +236 -3
  83. package/src/eval/reference-logits.ts +207 -0
  84. package/src/eval/runner.ts +8 -96
  85. package/src/fill/echo-index.ts +188 -0
  86. package/src/fill/fill-session.ts +452 -0
  87. package/src/fill/proposal.ts +68 -0
  88. package/src/fill/schema-rows.ts +506 -0
  89. package/src/fill/tool-boundary.ts +103 -0
  90. package/src/fit.ts +1 -1
  91. package/src/generate.ts +656 -268
  92. package/src/grammar.ts +12 -7
  93. package/src/harness-pi.ts +14 -33
  94. package/src/hub-rest.ts +4 -4
  95. package/src/index.ts +7 -0
  96. package/src/inference/denoising.ts +17 -0
  97. package/src/inference/graph.ts +33 -0
  98. package/src/inference/prefill.ts +39 -0
  99. package/src/inference/rollback.ts +7 -0
  100. package/src/jobs/db.ts +4 -0
  101. package/src/jobs/index.ts +2 -0
  102. package/src/jobs/job-entry.ts +12 -12
  103. package/src/jobs/runner.ts +98 -13
  104. package/src/jobs/task-client.ts +19 -0
  105. package/src/jobs/types.ts +2 -58
  106. package/src/kv-store.ts +109 -49
  107. package/src/lab/README.md +18 -0
  108. package/src/{assets → lab/curve}/curve-designer.html +2 -2
  109. package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
  110. package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +5 -5
  111. package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
  112. package/src/library.ts +40 -0
  113. package/src/lora.ts +18 -0
  114. package/src/media-fetch.ts +1 -1
  115. package/src/memory/article.ts +1 -1
  116. package/src/memory/chunk.ts +1 -1
  117. package/src/memory/model.ts +30 -169
  118. package/src/memory/prompts.ts +1 -1
  119. package/src/memory/rest.ts +1 -1
  120. package/src/memory/schedule.ts +1 -1
  121. package/src/memory/stages.ts +4 -1
  122. package/src/memory/vault.ts +1 -3
  123. package/src/mlx/array.ts +9 -10
  124. package/src/mlx/compile.ts +12 -2
  125. package/src/mlx/ffi.ts +58 -11
  126. package/src/mlx/materialize.ts +23 -0
  127. package/src/mlx/metal-kernel.ts +10 -11
  128. package/src/mlx/ops.ts +33 -13
  129. package/src/mlx/quantized-rows.ts +36 -0
  130. package/src/mlx/token-bitmask.ts +52 -0
  131. package/src/mlx/turboquant-kv-decode.ts +82 -0
  132. package/src/mlx/turboquant-kv-inverse.ts +122 -0
  133. package/src/mlx/turboquant-ops.ts +33 -3
  134. package/src/mlx/turboquant-tables.ts +2 -2
  135. package/src/model/batched-mask.ts +8 -2
  136. package/src/model/batched-quant.ts +1 -1
  137. package/src/model/batched-rotating-quant.ts +2 -2
  138. package/src/model/batched-rotating.ts +1 -1
  139. package/src/model/compiled-decode.ts +1 -1
  140. package/src/model/diffusion-gemma.ts +8 -8
  141. package/src/model/factory.ts +76 -33
  142. package/src/model/fingerprint.ts +1 -1
  143. package/src/model/flash-attention.ts +3 -3
  144. package/src/model/gemma4-base.ts +26 -9
  145. package/src/model/gemma4.ts +6 -6
  146. package/src/model/generated/gemma4-12b.ts +2 -2
  147. package/src/model/generated/gemma4-26b.ts +2 -2
  148. package/src/model/generated/gemma4-e4b.ts +2 -2
  149. package/src/model/glm52-dsa.ts +13 -9
  150. package/src/model/glm52-memory.ts +1 -1
  151. package/src/model/implementation.ts +69 -0
  152. package/src/model/minicpm5.ts +6 -8
  153. package/src/model/profile.ts +12 -2
  154. package/src/model/qwen-append.ts +12 -0
  155. package/src/model/qwen-conv.ts +52 -0
  156. package/src/model/qwen3_5-checkpoint.ts +164 -0
  157. package/src/model/qwen3_5.ts +140 -56
  158. package/src/model/support.ts +1 -1
  159. package/src/model/trellis-balanced-scatter.ts +51 -0
  160. package/src/model/trellis-linear.ts +672 -0
  161. package/src/model/trellis-shared-m.ts +151 -0
  162. package/src/model/trellis-shared-scatter.ts +144 -0
  163. package/src/model/trellis-splitk-prefill.ts +158 -0
  164. package/src/model/trellis-tiled-prefill.ts +136 -0
  165. package/src/model/trellis-vector-expand.ts +62 -0
  166. package/src/model/trellis-wide-prefill.ts +94 -0
  167. package/src/model/universal/dense.ts +1 -1
  168. package/src/model/universal/modules.ts +8 -1
  169. package/src/native-pack.ts +6 -5
  170. package/src/pi-provider.ts +6 -64
  171. package/src/pi-web.ts +8 -315
  172. package/src/prompt-cache.ts +96 -53
  173. package/src/quantize/allocator.ts +1 -1
  174. package/src/quantize/config-writer.ts +10 -3
  175. package/src/quantize/rotate.ts +4 -4
  176. package/src/quantize/trellis.ts +586 -0
  177. package/src/responses.ts +28 -1
  178. package/src/runtime-config.ts +18 -4
  179. package/src/sampler.ts +4 -4
  180. package/src/serve/batch-scheduler.ts +8 -1671
  181. package/src/serve/chat-request.ts +391 -0
  182. package/src/serve/chat-stage.ts +195 -0
  183. package/src/serve/checkpoint-identity.ts +64 -0
  184. package/src/serve/completion-executor.ts +15 -7
  185. package/src/serve/discovery-routes.ts +12 -13
  186. package/src/serve/generation-gateway.ts +154 -193
  187. package/src/serve/http.ts +159 -0
  188. package/src/serve/inference-request.ts +79 -0
  189. package/src/serve/isolate.ts +297 -105
  190. package/src/serve/lab-routes.ts +3 -0
  191. package/src/serve/lane-registry.ts +2 -1
  192. package/src/serve/model-admin-routes.ts +7 -5
  193. package/src/serve/model-binding.ts +43 -0
  194. package/src/serve/model-host.ts +512 -0
  195. package/src/serve/openai-wire.ts +103 -0
  196. package/src/serve/parent-application.ts +56 -0
  197. package/src/serve/pipeline.ts +33 -0
  198. package/src/serve/preparation.ts +33 -0
  199. package/src/serve/request-plan.ts +19 -15
  200. package/src/serve/request-prep.ts +346 -0
  201. package/src/serve/response-lease.ts +24 -0
  202. package/src/serve/responses-client.ts +69 -0
  203. package/src/serve/session-completion-engine.ts +110 -0
  204. package/src/serve/session-search.ts +1 -1
  205. package/src/serve/static-routes.ts +5 -5
  206. package/src/serve/text-completion-stage.ts +104 -0
  207. package/src/serve/token-streams.ts +519 -0
  208. package/src/server.ts +317 -2649
  209. package/src/spec/assistant-source.ts +10 -44
  210. package/src/spec/deepspec-source.ts +6 -6
  211. package/src/spec/dflash-source.ts +8 -8
  212. package/src/spec/dspark/ab-stats.ts +3 -3
  213. package/src/spec/dspark/calibration.ts +2 -2
  214. package/src/spec/dspark/deepspec-module.ts +2 -2
  215. package/src/spec/dspark/generate-dflash.ts +2 -2
  216. package/src/spec/dspark/generate.ts +2 -2
  217. package/src/spec/dspark/loader.ts +1 -1
  218. package/src/spec/dspark/module-dflash.ts +4 -3
  219. package/src/spec/dspark/module.ts +3 -3
  220. package/src/spec/dspark/quantize-drafter.ts +1 -1
  221. package/src/spec/generate.ts +1 -1
  222. package/src/spec/glm52-mtp-source.ts +1 -1
  223. package/src/spec/ngram-source.ts +1 -1
  224. package/src/spec/prefix-state.ts +45 -0
  225. package/src/spec/qwen-mtp-source.ts +166 -69
  226. package/src/spec/serve-loop.ts +154 -156
  227. package/src/spec/source.ts +42 -10
  228. package/src/spec/two-model.ts +1 -1
  229. package/src/ssd-cache.ts +80 -12
  230. package/src/ssd-durability.ts +25 -13
  231. package/src/tool-call.ts +51 -14
  232. package/src/train/flash-cce.ts +2 -2
  233. package/src/train/forward.ts +6 -0
  234. package/src/train/loss.ts +1 -1
  235. package/src/train/prefix-shared.ts +5 -1
  236. package/src/train/segmented.ts +5 -3
  237. package/src/train/trainer.ts +6 -6
  238. package/src/vision/prompt.ts +1 -1
  239. package/src/vision/qwen3vl-tower.ts +52 -10
  240. package/src/web/app.html +1 -1
  241. package/src/web/app.js +5 -3
  242. package/src/web/src/adapters-panel.ts +3 -3
  243. package/src/web/src/assistant.ts +2 -36
  244. package/src/web/src/chat.ts +4 -4
  245. package/src/web/src/composer.ts +4 -4
  246. package/src/web/src/hub.ts +1 -1
  247. package/src/web/src/markdown.ts +0 -0
  248. package/src/web/src/memory-panel.ts +1 -1
  249. package/src/web/src/model-picker.ts +1 -1
  250. package/src/web/src/palette.ts +1 -1
  251. package/src/web/src/protocol.ts +3 -39
  252. package/src/web/src/sessions.ts +1 -1
  253. package/src/web/src/ui-catalog.ts +4 -18
  254. package/src/web-assets.ts +47 -0
  255. package/src/weights.ts +55 -9
  256. package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
  257. package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
  258. package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
  259. package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
  260. package/docs/archive/investigations/hlg-ab.html +0 -34
  261. package/docs/archive/investigations/hlg-explorer.html +0 -238
  262. package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
  263. package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
  264. package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
  265. package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
  266. package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
  267. package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
  268. package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
  269. package/docs/archive/investigations/hlg-report.html +0 -262
  270. package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
  271. package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
  272. package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
  273. package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
  274. package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
  275. package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
  276. package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
  277. package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
  278. package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
  279. package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
  280. package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
  281. package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
  282. package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
  283. package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
  284. package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
  285. package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
  286. package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
  287. package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
  288. package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
  289. package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
  290. package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
  291. package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
  292. package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
  293. package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
  294. package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
  295. package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
  296. package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
  297. package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
  298. package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
  299. package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
  300. package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
  301. package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
  302. package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
  303. package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
  304. package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
  305. package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
  306. package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
  307. package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
  308. package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
  309. package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
  310. package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
  311. package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
  312. package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
  313. package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
  314. package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
  315. package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
  316. package/docs/archive/mlx-bun-lab-report.html +0 -275
  317. package/docs/archive/wwdc-mlx-bun.html +0 -848
  318. package/docs/design/adapters-end-to-end.md +0 -205
  319. package/docs/design/batching-perf-path.md +0 -255
  320. package/docs/design/batching-v2-plan.md +0 -529
  321. package/docs/design/bucketing-stage.md +0 -197
  322. package/docs/design/compat-cli-surface-design.md +0 -230
  323. package/docs/design/docs-reorg-plan.md +0 -165
  324. package/docs/design/dspark-serving-program.md +0 -371
  325. package/docs/design/dspark-speculative-decoding.md +0 -318
  326. package/docs/design/grammar-spec-batching-integration.md +0 -301
  327. package/docs/design/memory-inference-path.md +0 -422
  328. package/docs/design/memory-synthesis.md +0 -287
  329. package/docs/design/memory-system.md +0 -237
  330. package/docs/design/mlx-lm-serving-execution-seam.md +0 -81
  331. package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
  332. package/docs/design/omlx-adoption-map.md +0 -98
  333. package/docs/design/orpo-dynamic-lambda.md +0 -220
  334. package/docs/design/orpo-future-enhancements.md +0 -340
  335. package/docs/design/paged-kv-cache.md +0 -214
  336. package/docs/design/parallel-slots.md +0 -567
  337. package/docs/design/parity-tier-dag.md +0 -161
  338. package/docs/design/repo-cleanup-plan.md +0 -231
  339. package/docs/design/runtime-isolation.md +0 -263
  340. package/docs/design/segmented-backward-training.md +0 -543
  341. package/docs/design/spec-decode-larger-targets.md +0 -229
  342. package/docs/design/ssd-kv-cold-tier.md +0 -415
  343. package/docs/design/structured-output.md +0 -484
  344. package/docs/design/tauri-desktop-app.md +0 -127
  345. package/docs/design/the-dreaming-master-plan.md +0 -445
  346. package/docs/design/turboquant-kv.md +0 -235
  347. package/docs/design/turboquant-weights.md +0 -467
  348. package/docs/design/w4a16-compute-precision-spike.md +0 -232
  349. package/docs/design/web-chat-beat-matrix.md +0 -315
  350. package/docs/design/write-pipeline-entity.md +0 -468
  351. package/docs/planning/IDEAS.md +0 -69
  352. package/docs/reference/embedding.md +0 -197
  353. package/docs/reference/features-matrix.md +0 -101
  354. package/docs/reference/orpo-quickstart.md +0 -135
  355. package/src/flags.ts +0 -11
  356. /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
  357. /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
  358. /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
  359. /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
  360. /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
  361. /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
  362. /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
  363. /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
  364. /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
@@ -0,0 +1,39 @@
1
+ import type { DenoisingGraph } from "../../inference/denoising";
2
+ import type { MlxArray } from "../../mlx/array";
3
+ import type { Cache } from "../../model/gemma4-base";
4
+ import type { DiffusionGemmaModel } from "../../model/diffusion-gemma";
5
+ import type { MlxModelMemory } from "./autoregressive";
6
+ import { disposeResources } from "../../engine/resources";
7
+ import { runtimeConfig, type RuntimeConfig } from "../../runtime-config";
8
+
9
+ export interface MlxDenoisingBinding<State = Cache[]> {
10
+ readonly runtime?: RuntimeConfig;
11
+ readonly graph: DenoisingGraph<MlxArray, State>;
12
+ readonly memory: MlxModelMemory;
13
+ readonly adapters?: { active: string[] };
14
+ }
15
+
16
+ export function bindLegacyDenoisingModel(model: DiffusionGemmaModel): MlxDenoisingBinding {
17
+ return {
18
+ runtime: runtimeConfig(),
19
+ memory: model, adapters: model.loraState,
20
+ graph: {
21
+ descriptor: Object.freeze({ id: "legacy-diffusion-gemma", backend: "mlx",
22
+ graphAbi: "mlx-denoising-v1", stateAbi: "legacy-cache-array-v1", artifact: "legacy-resident-model" }),
23
+ vocabSize: model.config.text.vocabSize, canvasLength: model.canvasLength, embedScale: model.embedScale,
24
+ prefill: (ids, vision) => vision ? model.prefillVision(ids, vision) : model.prefill(ids),
25
+ extendPrefill: model.extendPrefill.bind(model),
26
+ decoderLogits: model.decoderLogits.bind(model),
27
+ dequantEmbedWeight: model.dequantEmbedWeight.bind(model),
28
+ softEmbeddings: model.softEmbeddings.bind(model),
29
+ closeState: disposeResources,
30
+ },
31
+ };
32
+ }
33
+
34
+ export function assertMlxDenoisingGraph<State>(graph: DenoisingGraph<MlxArray, State>): void {
35
+ const descriptor = graph.descriptor;
36
+ if (descriptor.backend !== "mlx" || descriptor.graphAbi !== "mlx-denoising-v1" ||
37
+ !descriptor.stateAbi)
38
+ throw new Error(`denoising graph ${descriptor.id} has an incompatible backend, graph, or state ABI`);
39
+ }
@@ -0,0 +1,47 @@
1
+ import type { RuntimeModel } from "../../model/factory";
2
+ import { Gemma4Model, type Cache } from "../../model/gemma4";
3
+ import { Qwen35Model } from "../../model/qwen3_5";
4
+ import type { MlxArray } from "../../mlx/array";
5
+ import * as ops from "../../mlx/ops";
6
+ import type { TargetView, AssistantTarget } from "../../spec/source";
7
+
8
+ /** Class checks and live cache layout stay at the compatibility boundary.
9
+ * A replacement graph supplies the same ports without becoming these classes. */
10
+ export function bindLegacyDraftTarget(model: RuntimeModel, caches: Cache[]): TargetView {
11
+ if (model instanceof Gemma4Model) {
12
+ let sliding = -1, full = -1;
13
+ for (let i = 0; i < model.numDonors; i++) {
14
+ if (model.layers[i]!.layerType === "sliding_attention") sliding = i;
15
+ else full = i;
16
+ }
17
+ const view = (index: number): [MlxArray, MlxArray] => {
18
+ const cache = caches[index];
19
+ if (!cache || !("temporalView" in cache) || typeof cache.temporalView !== "function")
20
+ throw new Error("assistant target has no plain donor view");
21
+ return cache.temporalView();
22
+ };
23
+ const assistant: AssistantTarget | undefined = sliding >= 0 && full >= 0 ? {
24
+ position: () => caches[0]!.offset - 1,
25
+ embedScaled(token) {
26
+ const ids = ops.fromInt32([token], [1, 1]);
27
+ let embedded: MlxArray | undefined;
28
+ try { embedded = model.embed.encode(ids); return ops.mulScalar(embedded, model.embedScale); }
29
+ finally { ids.dispose(); embedded?.dispose(); }
30
+ },
31
+ readDonors() {
32
+ const slidingView = view(sliding);
33
+ try { return { sliding: slidingView, full: view(full) }; }
34
+ catch (error) { for (const array of slidingView) array.dispose(); throw error; }
35
+ },
36
+ } : undefined;
37
+ return Object.freeze({ identity: model, assistant, gemmaTaps: Object.freeze({
38
+ layerCount: model.layers.length,
39
+ projection: Object.freeze({ embed: model.embed, logitsFromHidden: model.logitsFromHidden.bind(model) }),
40
+ }) });
41
+ }
42
+ if (model instanceof Qwen35Model) return Object.freeze({ identity: model, qwenMtp: Object.freeze({
43
+ hiddenSize: model.config.text.hiddenSize, layerCount: model.config.text.numHiddenLayers,
44
+ embed: model.embed.encode.bind(model.embed), logitsFromHidden: model.logitsFromHidden.bind(model),
45
+ }) });
46
+ return Object.freeze({ identity: model });
47
+ }
@@ -0,0 +1,105 @@
1
+ import type { TaskModel, GenOpts } from "../../eval/runner";
2
+ import type { CompletionClient } from "../../contracts/completion";
3
+ import { generate } from "../../generate";
4
+ import { clearCache } from "../../mlx/ffi";
5
+ import * as ops from "../../mlx/ops";
6
+ import { runtimeValue } from "../../runtime-config";
7
+ import { DiffusionGemmaModel } from "../../model/diffusion-gemma";
8
+
9
+ /** Bit-exact greedy decode: raw `model.forward` + argmax loop, which matches mlx-lm
10
+ * token-for-token (gen8k: 8000/8000). The product `generate()` decode wrapper
11
+ * (forwardHidden/logitsFromHidden + pipelined sampler) diverges from the raw forward
12
+ * on near-ties past ~32 tokens, so the EVAL decodes bit-exactly itself rather than
13
+ * alter the serving path. Greedy + full-precision KV only. */
14
+ export function greedyDecodeBitExact(tm: TaskModel, ids: number[], maxTokens: number): string {
15
+ if (tm.activeAdapters) tm.model.loraState.active = tm.activeAdapters; // apply the mounted adapter
16
+ const cache = tm.model.makeCache();
17
+ try {
18
+ // Chunked, LM-head-free prefill. The old single-shot `forward(prompt)`
19
+ // materialized [1, L, vocab] logits just to discard them — at a 248k
20
+ // vocab that's ~1 GB per 1k prompt tokens, and with no allocator-cache
21
+ // clears the Metal buffer cache retained every giant freed tensor
22
+ // (the 27B capability-suite 73 GB swap-thrash). Prefill only needs the
23
+ // cache state, which forwardHidden advances identically (the LM head
24
+ // never touches the cache) — bit-exact by construction. Chunk size and
25
+ // clear cadence mirror mlx-lm generate_step / our generate().
26
+ const PREFILL_CHUNK = 2048;
27
+ const prompt = ids.slice(0, -1);
28
+ for (let s = 0; s < prompt.length; s += PREFILL_CHUNK) {
29
+ const chunk = prompt.slice(s, s + PREFILL_CHUNK);
30
+ const cIds = ops.fromInt32(chunk, [1, chunk.length]);
31
+ tm.model.forwardHidden(cIds, cache).dispose();
32
+ cIds.dispose();
33
+ clearCache();
34
+ }
35
+ let last = ids[ids.length - 1]!;
36
+ const eos = new Set(tm.config.eosTokenIds);
37
+ const out: number[] = [];
38
+ for (let i = 0; i < maxTokens; i++) {
39
+ const lg = tm.model.forward([last], cache);
40
+ const am = ops.argmaxAxis(lg, -1); // GPU argmax (mlx-lm greedy: lowest-index tie-break)
41
+ lg.dispose();
42
+ const best = ops.itemUint32(am);
43
+ am.dispose();
44
+ if (i % 256 === 0) clearCache(); // mlx-lm decode cadence
45
+ if (eos.has(best)) break; // mlx-lm halts on EOS and excludes it from the output
46
+ out.push(best);
47
+ last = best;
48
+ }
49
+ return tm.tokenizer.decode(out, true);
50
+ } finally {
51
+ for (const c of cache) c.dispose();
52
+ }
53
+ }
54
+
55
+ /** Complete `body` via the model's real quantized-KV path. Greedy by default;
56
+ * pass `opts.sampler` to drive temperature/HLG/etc. */
57
+ async function generateTextNative(tm: TaskModel, body: string, opts: GenOpts = {}): Promise<string> {
58
+ const maxTokens = opts.maxTokens ?? 256;
59
+ const enableThinking = opts.enableThinking ?? runtimeValue("MLX_BUN_EVAL_THINK") === "1";
60
+ const templated = opts.useChat !== false && tm.template !== null;
61
+ const text = templated
62
+ ? tm.template!.render([{ role: "user", content: body }], { addGenerationPrompt: true, enableThinking })
63
+ : body;
64
+ // The chat template ALREADY emits the BOS; encoding it with add_special_tokens on
65
+ // would prepend a SECOND BOS (<s><s>…), corrupting generation. Raw bodies (no
66
+ // template) get the BOS added as normal.
67
+ const ids = tm.tokenizer.encode(text, /* addSpecialTokens */ !templated);
68
+
69
+ // optiq's PUBLISHED eval generates with a FULL-PRECISION KV cache — its eval calls
70
+ // plain mlx_lm.generate (kv-quant lives in the serving runtime, not the eval). So
71
+ // for like-for-like parity we default the eval to UNQUANTIZED KV; MMLU's argmax
72
+ // path was already unquantized, which is why it matched while generation didn't.
73
+ // MLX_BUN_EVAL_KV_QUANT=1 generates through the model's quantized KV (serving
74
+ // config); an explicit opts.kvScheme (cli generate's resolved tier route) wins.
75
+ const envKv: GenOpts["kvScheme"] | undefined =
76
+ (runtimeValue("MLX_BUN_EVAL_KV_QUANT") === "1" && tm.config.kvQuant?.length)
77
+ ? { kvConfig: tm.config.kvQuant, quantizedKvStart: 0 } : undefined;
78
+ const kvScheme = opts.kvScheme ?? envKv;
79
+ const kvActive = !!(kvScheme && (kvScheme.kvBits || kvScheme.kvConfig?.length || kvScheme.turboQuant));
80
+
81
+ // Parity default — greedy + full-precision KV + no sampler arm: decode bit-exactly
82
+ // via the raw forward (matches mlx-lm token-for-token). Sampler arms / kv-quant fall
83
+ // through to the product generate(). DiffusionGemma is non-autoregressive (no AR
84
+ // forward) — always go through generate(), which routes it to the denoising engine.
85
+ const isDiffusion = tm.model instanceof DiffusionGemmaModel;
86
+ if (!isDiffusion && !kvActive && !opts.sampler && !tm.samplerOverride) {
87
+ return greedyDecodeBitExact(tm, ids, maxTokens);
88
+ }
89
+ const gen = generate(tm.model, ids, {
90
+ maxTokens,
91
+ temperature: 0, // greedy default — deterministic head-to-head arms
92
+ adapters: tm.activeAdapters ?? [], // per-request activation of the mounted eval-adapter
93
+ ...(kvActive ? kvScheme : {}),
94
+ ...(opts.sampler ?? {}), // overrides temperature when provided
95
+ ...(tm.samplerOverride ?? {}), // arm override wins over the task's own sampler
96
+ });
97
+
98
+ const out: number[] = [];
99
+ for await (const { token } of gen) out.push(token);
100
+ return tm.tokenizer.decode(out, true);
101
+ }
102
+
103
+ export function createEvalCompletionClient(tm: TaskModel): CompletionClient<{ body: string; options?: GenOpts }, string> {
104
+ return { complete: ({ body, options }) => generateTextNative(tm, body, options) };
105
+ }
@@ -0,0 +1,45 @@
1
+ import type { MlxArray } from "../../mlx/array";
2
+ import * as ops from "../../mlx/ops";
3
+ import type { Cache } from "../../model/gemma4";
4
+ import { withResource } from "../../engine/resources";
5
+ import { leaseCacheStates } from "./state-views";
6
+
7
+ /** Append an already accepted span without intermediate vocabulary heads.
8
+ * Chunking changes the graph shape, so callers must qualify its numerics for
9
+ * their model. A verify round must use chunkSize=0: recurrent rollback records
10
+ * one forward per round. The complete append precedes emission/cancellation
11
+ * boundaries, preserving the cache-covered prefix on a consumer break.
12
+ */
13
+ export async function appendFillHidden(
14
+ forward: (ids: MlxArray, cache: Cache[]) => MlxArray | Promise<MlxArray>,
15
+ cache: Cache[],
16
+ ids: number[],
17
+ chunkSize: (cache: readonly Cache[]) => number,
18
+ ): Promise<MlxArray> {
19
+ const input = ops.fromInt32(ids, [1, ids.length]);
20
+ const hidden: MlxArray[] = [];
21
+ try {
22
+ for (let start = 0; start < ids.length;) {
23
+ const limit = chunkSize(cache);
24
+ if (!Number.isSafeInteger(limit) || limit < 1)
25
+ throw new Error("Model append chunk limit must be a positive integer");
26
+ const end = Math.min(ids.length, start + limit);
27
+ if (start === 0 && end === ids.length) return await forward(input, cache);
28
+ const part = input.slice([0, start], [1, end]);
29
+ try {
30
+ const h = await forward(part, cache);
31
+ hidden.push(h);
32
+ // Bound transient graphs while retaining all positions for an optional
33
+ // diagnostic trace. Normal generation projects only the last position.
34
+ withResource(leaseCacheStates(cache), (state) => ops.evalAll([h, ...state]));
35
+ } finally {
36
+ part.dispose();
37
+ }
38
+ start = end;
39
+ }
40
+ return ops.concatAxis(hidden, 1);
41
+ } finally {
42
+ for (const h of hidden) h.dispose();
43
+ input.dispose();
44
+ }
45
+ }
@@ -0,0 +1,71 @@
1
+ import type { ModelConfig } from "../../config";
2
+ import type { KvScheme } from "../../kv-scheme";
3
+ import type { RuntimeModel } from "../../model/factory";
4
+ import { DiffusionGemmaModel } from "../../model/diffusion-gemma";
5
+ import { UniversalDenseModel } from "../../model/universal/dense";
6
+ import { KVCache, RotatingKVCache, isBatchableCache, isPlainKvCache, isRotatingPlainCache } from "../../model/gemma4-base";
7
+ import { SSMCache } from "../../model/qwen3-delta";
8
+ import { Qwen35Model } from "../../model/qwen3_5";
9
+ import { runtimeConfig, type RuntimeConfig } from "../../runtime-config";
10
+ import { disposeResources } from "../../engine/resources";
11
+ import { legacyCompiledDecodeAvailable } from "./autoregressive";
12
+ import { MlxBatchExecutionGroup, type MlxBatchExecutionGroupOptions } from "./batch-group";
13
+ import type { GenerateOptions } from "../../generate";
14
+ import type { ExecutionRequirements, ResolvedExecution } from "../../contracts/execution";
15
+ import { resolveExecution } from "../../engine/execution-plan";
16
+
17
+ export interface MlxBatchGroup extends Pick<MlxBatchExecutionGroup,
18
+ "activeRows" | "pendingRows" | "projectedKvBytes" | "kvBudgetBytes" | "submit" | "kick" | "close"> {}
19
+
20
+ /** A model implementation owns capability checks and the execution group.
21
+ * Scheduling never inspects concrete model/cache classes. */
22
+ export interface MlxGatewayBinding {
23
+ readonly config: ModelConfig;
24
+ readonly runtime: RuntimeConfig;
25
+ plan(request: ExecutionRequirements, options: GenerateOptions,
26
+ scheduling: { continuous: boolean; quantizedBatch: boolean; checkpoints: boolean }): ResolvedExecution;
27
+ cachesBatchable(): boolean;
28
+ kvBatchable(scheme: KvScheme): boolean;
29
+ createBatchGroup(options: MlxBatchExecutionGroupOptions): MlxBatchGroup;
30
+ }
31
+
32
+ export function bindMlxGateway(model: RuntimeModel): MlxGatewayBinding {
33
+ const runtime = runtimeConfig();
34
+ return {
35
+ config: model.config, runtime,
36
+ plan(request, options, scheduling) {
37
+ return resolveExecution(request, {
38
+ ...scheduling,
39
+ method: model instanceof DiffusionGemmaModel ? "denoising" : "autoregressive",
40
+ compiledDecode: legacyCompiledDecodeAvailable(model),
41
+ grammarBatch: runtime.value("MLX_BUN_GRAMMAR_BATCH") !== "0",
42
+ speculativeKvQuant: model instanceof Qwen35Model && options.kvBits === 4 &&
43
+ !options.kvConfig?.length && options.quantizedKvStart === 0 &&
44
+ runtime.flag("MLX_BUN_QWEN_SPEC_KV4", false),
45
+ }, {
46
+ pagedKv: !!options.pagedKv, fill: !!options.fill,
47
+ compiledDecode: runtime.flag("MLX_BUN_COMPILED_DECODE", true),
48
+ grammarJump: runtime.flag("MLX_BUN_GRAMMAR_JUMP", false),
49
+ });
50
+ },
51
+ cachesBatchable() {
52
+ if (model instanceof UniversalDenseModel)
53
+ return !model.args.maskArray && !model.args.layerTypes?.includes("sliding_attention");
54
+ const caches = model.makeCache();
55
+ try {
56
+ const ssm = runtime.value("MLX_BUN_BATCH_SSM") !== "0";
57
+ return caches.every((cache) => cache instanceof KVCache || cache instanceof RotatingKVCache ||
58
+ isBatchableCache(cache) || (ssm && cache instanceof SSMCache));
59
+ } finally { disposeResources(caches); }
60
+ },
61
+ kvBatchable(scheme) {
62
+ if (scheme.kind !== "affine-config") return false;
63
+ const caches = model.makeCache();
64
+ try {
65
+ return scheme.batchable(model.config,
66
+ (layer) => isPlainKvCache(caches[layer]) || isRotatingPlainCache(caches[layer]));
67
+ } finally { disposeResources(caches); }
68
+ },
69
+ createBatchGroup: (options) => new MlxBatchExecutionGroup(model, options),
70
+ };
71
+ }
@@ -0,0 +1,38 @@
1
+ import type { AutoregressiveGraph, GraphDescriptor, LogitSelection } from "../../inference/graph";
2
+ import type { MlxArray } from "../../mlx/array";
3
+
4
+ /** Structural input: generated and hand-written graphs need no union membership. */
5
+ export interface MlxGraphOperations<State> {
6
+ forwardHidden(ids: MlxArray, state: State): MlxArray;
7
+ forwardHiddenAsync?: (ids: MlxArray, state: State) => Promise<MlxArray>;
8
+ logitsFromHidden(hidden: MlxArray): MlxArray;
9
+ }
10
+
11
+ /** Bind once. No array wrapper, await, readback, or synchronization is added
12
+ * to synchronous forward calls. Existing quant-specific kernels stay inside
13
+ * the supplied operations. This adapter does not take ownership of weights. */
14
+ export function bindMlxGraph<State>(
15
+ operations: MlxGraphOperations<State>,
16
+ descriptor: Omit<GraphDescriptor, "backend" | "graphAbi">,
17
+ ): AutoregressiveGraph<MlxArray, State, MlxArray> {
18
+ const forward = operations.forwardHiddenAsync
19
+ ? operations.forwardHiddenAsync.bind(operations)
20
+ : operations.forwardHidden.bind(operations);
21
+ const project = operations.logitsFromHidden.bind(operations);
22
+ return {
23
+ descriptor: Object.freeze({ ...descriptor, backend: "mlx", graphAbi: "mlx-hidden-bsh-v1" }),
24
+ forwardHidden: forward,
25
+ projectLogits(hidden: MlxArray, selection: LogitSelection) {
26
+ if (selection.type === "all") return project(hidden);
27
+ const shape = hidden.shape;
28
+ if (shape.length !== 3) throw new Error("MLX hidden state must have shape [batch, positions, width]");
29
+ const [batch, positions, width] = shape as [number, number, number];
30
+ const start = selection.type === "last" ? positions - 1 : selection.start;
31
+ const end = selection.type === "last" ? positions : selection.end;
32
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end <= start || end > positions)
33
+ throw new Error("invalid logits position selection");
34
+ const selected = hidden.slice([0, start, 0], [batch, end, width]);
35
+ try { return project(selected); } finally { selected.dispose(); }
36
+ },
37
+ };
38
+ }
@@ -0,0 +1,189 @@
1
+ import { existsSync } from "node:fs";
2
+ import { loadTaskModel, greedyDecodeBitExact, type TaskModel } from "../../eval/runner";
3
+ import type { ChatTemplate } from "../../chat-template";
4
+ import { MlxBatchExecutionGroup as BatchScheduler, type RowSampler } from "./batch-group";
5
+ import * as ops from "../../mlx/ops";
6
+ import type { MlxArray } from "../../mlx/array";
7
+ import { adapterDirFor, memoryBatchSize, memoryPromptIds, MODEL_ID,
8
+ type LocalInput, type MemoryCompletionClient } from "../../memory/model";
9
+
10
+ class AsyncMutex {
11
+ #tail: Promise<void> = Promise.resolve();
12
+ acquire(): Promise<() => void> {
13
+ let release!: () => void;
14
+ const gate = new Promise<void>((r) => { release = r; });
15
+ const wait = this.#tail;
16
+ this.#tail = this.#tail.then(() => gate);
17
+ return wait.then(() => release);
18
+ }
19
+ }
20
+
21
+ interface MemoryRuntime {
22
+ tm: TaskModel;
23
+ template: ChatTemplate;
24
+ scheduler: BatchScheduler;
25
+ /** True once the `memory-chunk` adapter is mounted on the shared model. */
26
+ hasChunkAdapter: boolean;
27
+ }
28
+
29
+
30
+ function requireModel(): void {
31
+ if (!existsSync(`${MODEL_ID}/config.json`))
32
+ throw new Error(`memory: Gemma-4-e4b is not downloaded (looked under ${MODEL_ID}). Fetch it first: HF_HUB_DISABLE_XET=1 hf download mlx-community/gemma-4-e4b-it-OptiQ-4bit`);
33
+ }
34
+
35
+ let runtimePromise: Promise<MemoryRuntime> | null = null;
36
+ const opLock = new AsyncMutex(); // serializes whole memory ops (loraState ownership)
37
+ const schedLock = new AsyncMutex(); // the scheduler's exclusive-GPU lock
38
+
39
+ /** Lazily load the ONE shared model + mount memory-chunk + build the scheduler.
40
+ * Never auto-downloads; throws (with the fetch hint) until the snapshot lands. */
41
+ async function getRuntime(): Promise<MemoryRuntime> {
42
+ if (!runtimePromise) {
43
+ runtimePromise = (async () => {
44
+ requireModel();
45
+ const tm = await loadTaskModel(MODEL_ID); // base e4b, no adapter
46
+ if (!tm.template) throw new Error(`memory: ${MODEL_ID} has no chat template`);
47
+ let hasChunkAdapter = false;
48
+ const chunkDir = adapterDirFor("chunk");
49
+ if (chunkDir) {
50
+ // Mount ONCE; activation is per-call via loraState.active (see decodeGreedy
51
+ // / batchDecode). Mounting alone runs base — it must be activated.
52
+ const { AdapterManager } = await import("../../lora");
53
+ await new AdapterManager(tm.model).mount("memory-chunk", chunkDir);
54
+ hasChunkAdapter = true;
55
+ }
56
+ const scheduler = new BatchScheduler(tm.model, {
57
+ maxBatch: memoryBatchSize(),
58
+ lock: { acquire: () => schedLock.acquire() },
59
+ });
60
+ return { tm, template: tm.template, scheduler, hasChunkAdapter };
61
+ })();
62
+ }
63
+ return runtimePromise;
64
+ }
65
+
66
+ /** The adapters to activate for a stage. Only `chunk` is adapter-bound (and only
67
+ * when `memory-chunk` is mounted); every base stage runs with `active = []`. */
68
+ function activeFor(stage: string, rt: MemoryRuntime): string[] {
69
+ return stage === "chunk" && rt.hasChunkAdapter ? ["memory-chunk"] : [];
70
+ }
71
+
72
+ /** One bit-exact greedy decode for a stage (raw-forward argmax, runner parity).
73
+ * Sets the stage's adapter for the call and resets `active` to [] after. */
74
+ function decodeGreedy(rt: MemoryRuntime, stage: string, input: LocalInput, maxTokens: number): string {
75
+ const active = activeFor(stage, rt);
76
+ rt.tm.model.loraState.active = active;
77
+ const ids = memoryPromptIds(stage, input, rt.tm.tokenizer, rt.template);
78
+ try {
79
+ return greedyDecodeBitExact(
80
+ { ...rt.tm, activeAdapters: active.length ? active : undefined },
81
+ ids,
82
+ maxTokens,
83
+ );
84
+ } finally {
85
+ rt.tm.model.loraState.active = [];
86
+ }
87
+ }
88
+
89
+ /** Batched decode for N independent inputs of ONE stage. Sets the stage adapter
90
+ * globally for the whole batch (the scheduler reads `loraState.active` per
91
+ * forward, so all rows get it uniformly — a batch is necessarily single-adapter),
92
+ * submits every row to the shared scheduler with a greedy per-row sampler, and
93
+ * resets `active` after all rows finish. Order-preserving: out[i] ↔ inputs[i].
94
+ *
95
+ * Decode-path caveat: this samples via forwardHidden/logitsFromHidden (the
96
+ * scheduler), which can diverge from the runner's raw-forward greedy on near-ties
97
+ * past ~32 tokens. For the JSON/binary memory stages (validated by
98
+ * well-formedness) this is acceptable; the single-call fallback stays bit-exact. */
99
+ async function batchDecode(
100
+ rt: MemoryRuntime,
101
+ stage: string,
102
+ inputs: LocalInput[],
103
+ maxTokens: number,
104
+ ): Promise<string[]> {
105
+ const active = activeFor(stage, rt);
106
+ rt.tm.model.loraState.active = active;
107
+ const eos = rt.tm.config.eosTokenIds;
108
+ const greedySample: RowSampler = (logits1V: MlxArray) => ops.argmaxAxis(logits1V, -1);
109
+ try {
110
+ return await Promise.all(
111
+ inputs.map((inp) => {
112
+ const ids = memoryPromptIds(stage, inp, rt.tm.tokenizer, rt.template);
113
+ const toks: number[] = [];
114
+ return rt.scheduler
115
+ .submit({
116
+ promptIds: ids,
117
+ maxTokens,
118
+ eosTokenIds: eos,
119
+ sample: greedySample,
120
+ onToken: (t) => { toks.push(t); },
121
+ })
122
+ .then(() => rt.tm.tokenizer.decode(toks, true));
123
+ }),
124
+ );
125
+ } finally {
126
+ rt.tm.model.loraState.active = [];
127
+ }
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Public seams
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /** Call the local model for a pipeline `stage` with a {system?, user} input.
135
+ * Renders through the shared chat-template path, runs the MODEL precheck, and
136
+ * decodes ONE completion on the bit-exact greedy path (the safe fallback used by
137
+ * every single-call site). NEVER auto-downloads. */
138
+ export async function callLocal(
139
+ stage: string,
140
+ input: LocalInput,
141
+ opts?: { maxTokens?: number },
142
+ ): Promise<string> {
143
+ const rt = await getRuntime();
144
+ const release = await opLock.acquire();
145
+ try {
146
+ return decodeGreedy(rt, stage, input, opts?.maxTokens ?? 256);
147
+ } finally {
148
+ release();
149
+ }
150
+ }
151
+
152
+ /** N independent calls for ONE stage (homogeneous adapter by construction).
153
+ * Routed through the shared in-process BatchScheduler when batch>1 AND N>1, else
154
+ * looped on the bit-exact greedy fallback. Order-preserving: out[i] ↔ inputs[i]. */
155
+ export async function callLocalBatch(
156
+ stage: string,
157
+ inputs: LocalInput[],
158
+ opts?: { maxTokens?: number },
159
+ ): Promise<string[]> {
160
+ if (inputs.length === 0) return [];
161
+ const rt = await getRuntime();
162
+ const maxTokens = opts?.maxTokens ?? 256;
163
+ const release = await opLock.acquire();
164
+ try {
165
+ if (inputs.length === 1 || memoryBatchSize() <= 1) {
166
+ const out: string[] = [];
167
+ for (const inp of inputs) out.push(decodeGreedy(rt, stage, inp, maxTokens));
168
+ return out;
169
+ }
170
+ return await batchDecode(rt, stage, inputs, maxTokens);
171
+ } finally {
172
+ release();
173
+ }
174
+ }
175
+
176
+ export const memoryCompletionClient: MemoryCompletionClient = {
177
+ complete: ({ stage, input, maxTokens }) => callLocal(stage, input, { maxTokens }),
178
+ completeBatch(requests) {
179
+ if (!requests.length) return Promise.resolve([]);
180
+ const first = requests[0]!;
181
+ if (requests.some((request) => request.stage !== first.stage || request.maxTokens !== first.maxTokens))
182
+ return (async () => {
183
+ const results: string[] = [];
184
+ for (const request of requests) results.push(await this.complete(request));
185
+ return results;
186
+ })();
187
+ return callLocalBatch(first.stage, requests.map((request) => request.input), { maxTokens: first.maxTokens });
188
+ },
189
+ };
@@ -0,0 +1,40 @@
1
+ import { DEFAULT_CHUNK, TRANSIENT_PER_TOKEN } from "../../fit";
2
+ import { kvBytesAt, type KvSchemeOptions } from "../../kv-scheme";
3
+ import { activeMemory, clearCache, maxRecommendedWorkingSetSize } from "../../mlx/ffi";
4
+ import type { RuntimeModel } from "../../model/factory";
5
+ import { Qwen35Model } from "../../model/qwen3_5";
6
+ import type { PromptCache } from "../../prompt-cache";
7
+ import { runtimeValue } from "../../runtime-config";
8
+
9
+ /** Admission's ceiling and the cache that can release memory under the GPU lease. */
10
+ export interface MlxMemoryBudget {
11
+ readonly usableBytes: number;
12
+ readonly kvOptions: KvSchemeOptions;
13
+ readonly promptCache: Pick<PromptCache, "relievePressure" | "totalBytes">;
14
+ }
15
+
16
+ /** Both execution paths enter after acquiring the request's reusable prefix.
17
+ * The caller owns the GPU lease and must close the scope on every exit. */
18
+ export function enterMlxMemoryGuard(model: RuntimeModel, budget: MlxMemoryBudget,
19
+ promptTokens: number, prefillChunkSize?: number): { check(): void; close(): void } {
20
+ const reserve = (prefillChunkSize ?? DEFAULT_CHUNK) * TRANSIENT_PER_TOKEN +
21
+ kvBytesAt(model.config, promptTokens, budget.kvOptions);
22
+ const ceiling = Math.min(budget.usableBytes, maxRecommendedWorkingSetSize());
23
+ const check = () => {
24
+ if (activeMemory() + reserve <= ceiling) return;
25
+ clearCache();
26
+ if (runtimeValue("MLX_BUN_PREFILL_MEM_LOG") === "1")
27
+ console.error(`[cache-memory] active=${activeMemory()} prompt=${budget.promptCache.totalBytes} reserve=${reserve} ceiling=${ceiling}`);
28
+ const count = budget.promptCache.relievePressure(() => {
29
+ clearCache();
30
+ return activeMemory() + reserve > ceiling;
31
+ });
32
+ if (count) console.log(`[prompt-cache] demoted ${count} entries for GPU headroom`);
33
+ if (activeMemory() + reserve > ceiling)
34
+ throw new Error("insufficient GPU headroom after prompt-cache demotion; saved generation checkpoints are retained");
35
+ };
36
+ const qwen = model instanceof Qwen35Model ? model : undefined;
37
+ const previous = qwen?.prefillMemoryGuard;
38
+ if (qwen) qwen.prefillMemoryGuard = check;
39
+ return { check, close() { if (qwen) qwen.prefillMemoryGuard = previous ?? null; } };
40
+ }
@@ -0,0 +1,90 @@
1
+ import { runtimeConfig, withRuntimeConfig, type RuntimeConfig } from "../../runtime-config";
2
+ import { snapshotGenerationPolicy } from "./request-policy";
3
+ import type { GenerationOutput, InferenceMethod, MethodResult } from "../../contracts/generation";
4
+ import { throwIfCancelled } from "../../engine/cancellation";
5
+ import { generateAutoregressive, generateDenoising, type GenerateOptions, type GenerateStats } from "../../generate";
6
+ import { specRun } from "../../spec/serve-loop";
7
+ import type { MlxAutoregressiveBinding } from "./autoregressive";
8
+ import type { MlxSpeculativeBinding } from "./speculative";
9
+ import type { MlxDenoisingBinding } from "./diffusion";
10
+
11
+ /** The native algorithm owns cleanup in its finally. This host adapter owns
12
+ * cancellation subscriptions and waits for the algorithm before closing. */
13
+ function nativeMethod<Metrics>(
14
+ id: string,
15
+ execute: (output: GenerationOutput, signal: AbortSignal) => Promise<MethodResult<Metrics>>,
16
+ runtime: RuntimeConfig,
17
+ ): InferenceMethod<Metrics> {
18
+ return {
19
+ id,
20
+ async createRun(cancellation) {
21
+ throwIfCancelled(cancellation);
22
+ const abort = new AbortController();
23
+ const unsubscribe = cancellation.subscribe((reason) => abort.abort(
24
+ new DOMException(`generation cancelled: ${reason}`, "AbortError"),
25
+ ));
26
+ let work: Promise<MethodResult<Metrics>> | undefined;
27
+ let closing: Promise<void> | undefined;
28
+ return {
29
+ execute(output) {
30
+ if (work || closing) return Promise.reject(new Error("method run can execute only once"));
31
+ work = withRuntimeConfig(runtime, () => execute(output, abort.signal));
32
+ return work;
33
+ },
34
+ close() {
35
+ return closing ??= (async () => {
36
+ abort.abort(new DOMException("method closed", "AbortError"));
37
+ try { await work; } catch { /* execute owns the algorithm's error */ }
38
+ finally { unsubscribe(); }
39
+ })();
40
+ },
41
+ };
42
+ },
43
+ };
44
+ }
45
+
46
+ /** Each plan supplies its prepared per-run resources. Caches/media in options
47
+ * retain the same ownership contract as generateAutoregressive. */
48
+ export function createAutoregressiveMethod(
49
+ binding: MlxAutoregressiveBinding, promptIds: readonly number[], options: GenerateOptions,
50
+ ): InferenceMethod<GenerateStats> {
51
+ const prompt = [...promptIds];
52
+ options = snapshotGenerationPolicy(options);
53
+ return nativeMethod("mlx-autoregressive", async (output, signal) => {
54
+ const generation = generateAutoregressive(binding, prompt, { ...options, signal });
55
+ for await (const item of generation)
56
+ if (await output.commit([item.token], item.logprobs ? [item.logprobs] : undefined) === false) break;
57
+ const metrics = generation.stats;
58
+ if (!metrics) throw new Error("AR method did not settle its metrics");
59
+ return { finishReason: metrics.finishReason ?? (metrics.generatedTokens >= (options.maxTokens ?? 512) ? "length" : "stop"), metrics };
60
+ }, binding.runtime ?? runtimeConfig());
61
+ }
62
+
63
+ export function createSpeculativeMethod(
64
+ binding: MlxSpeculativeBinding, numDraftTokens: number,
65
+ promptIds: readonly number[], options: GenerateOptions,
66
+ ): InferenceMethod<GenerateStats> {
67
+ const prompt = [...promptIds];
68
+ options = snapshotGenerationPolicy(options);
69
+ return nativeMethod("mlx-speculative", async (output, signal) => {
70
+ const metrics = await specRun(binding, numDraftTokens, prompt, { ...options, signal },
71
+ (token) => output.commit([token]));
72
+ return { finishReason: metrics.finishReason ?? (metrics.generatedTokens >= (options.maxTokens ?? 512) ? "length" : "stop"), metrics };
73
+ }, binding.runtime ?? runtimeConfig());
74
+ }
75
+
76
+ /** The canvas stays private. This method publishes only the finished result. */
77
+ export function createDenoisingMethod<State>(
78
+ binding: MlxDenoisingBinding<State>, promptIds: readonly number[], options: GenerateOptions,
79
+ ): InferenceMethod<GenerateStats> {
80
+ const prompt = [...promptIds];
81
+ options = snapshotGenerationPolicy(options);
82
+ return nativeMethod("mlx-denoising", async (output, signal) => {
83
+ const generation = generateDenoising(binding, prompt, { ...options, signal });
84
+ for await (const item of generation)
85
+ if (await output.commit([item.token]) === false) break;
86
+ const metrics = generation.stats;
87
+ if (!metrics) throw new Error("denoising method did not settle its metrics");
88
+ return { finishReason: metrics.generatedTokens >= (options.maxTokens ?? 256) ? "length" : "stop", metrics };
89
+ }, binding.runtime ?? runtimeConfig());
90
+ }