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
package/src/eval/kl.ts CHANGED
@@ -22,14 +22,15 @@
22
22
 
23
23
  import { existsSync } from "node:fs";
24
24
  import { MlxArray } from "../mlx/array";
25
- import { Dtype } from "../mlx/ffi";
25
+ import { clearCache, Dtype } from "../mlx/ffi";
26
26
  import * as ops from "../mlx/ops";
27
27
  import { Registry } from "../registry";
28
28
  import { Weights } from "../weights";
29
29
  import { loadModelConfig, type ModelConfig, type TurboQuantScheme } from "../config";
30
30
  import { createModel, type RuntimeModel } from "../model/factory";
31
31
  import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
32
- import { maybeQuantizeKv } from "../generate";
32
+ import { evalCacheState, maybeQuantizeKv } from "../generate";
33
+ import { ReferenceLogitsDump, type ReferenceRecords } from "./reference-logits";
33
34
  import {
34
35
  configureRuntime,
35
36
  runtimeKey,
@@ -310,7 +311,7 @@ export async function evaluateKlServingDecode(opts: {
310
311
  /** KV-scheme A/B on the teacher-forced serving-decode path: bf16 baseline
311
312
  * (no kvOverride) vs an explicit KV scheme (e.g. TurboQuant), same model /
312
313
  * same prompts / same weight load — the quality-vs-bpw curve gate
313
- * (scripts/eval-turboquant-curve.ts). Unlike evaluateKlServingDecode (env-flag
314
+ * (scripts/turboquant/eval-turboquant-curve.ts). Unlike evaluateKlServingDecode (env-flag
314
315
  * self-flip), the two arms differ by an actual GenerateOptions kv scheme
315
316
  * object, so this is the seam for schemes that aren't env-lever-shaped. */
316
317
  export async function evaluateKlKvArm(opts: {
@@ -355,3 +356,235 @@ export async function evaluateKlKvArm(opts: {
355
356
  : `bf16 vs kv-scheme (${decodeSteps} steps)`,
356
357
  };
357
358
  }
359
+
360
+ // --- saved-teacher KL (--reference-logits) --------------------------------
361
+ // The two-model mode above needs both arms resident; a bf16 teacher never
362
+ // fits beside a candidate on 32 GB. So the teacher is dumped ONCE from
363
+ // Python (top-k logits + full-vocab logsumexp per position; format and
364
+ // rationale in reference-logits.ts) and every candidate is scored against
365
+ // the dump through OUR engine.
366
+ //
367
+ // Per position, with the teacher's top-k slice j ∈ K:
368
+ // p_j = exp(ref_logit_j − ref_logsumexp) (full-vocab softmax)
369
+ // log q_j = cand_logit_j − logsumexp(cand, FULL vocab)
370
+ // KL = Σ_j p_j · (log p_j − log q_j)
371
+ // The sum is truncated to K, so it under-counts by the tail p carries
372
+ // outside the top-k — `meanCapturedMass` (Σ_j p_j) reports exactly how much
373
+ // of p the number covers, and a run with low captured mass is not a
374
+ // comparable KL. Everything is float32; the candidate's logsumexp is over
375
+ // its FULL vocab (never the top-k slice), so log q is a real log-probability.
376
+ //
377
+ // The forward is the SERVING path (model.makeCache + chunked forwardHidden +
378
+ // logitsFromHidden), NOT trainForward: trainForward's cache stub has no
379
+ // DeltaNet SSMCache.advance, which is why `mlx-bun perplexity` cannot score
380
+ // qwen3_5 at all (docs/design/turboquant.md, "Known engine gaps"). KV stays
381
+ // bf16 — this is a weights-only instrument.
382
+
383
+ export interface RefLogitsKLResult extends KLResult {
384
+ /** Positions actually scored (Σ over sequences). */
385
+ nPositions: number;
386
+ /** Fraction of positions where argmax(ref) == argmax(cand) over full vocab. */
387
+ top1Agreement: number;
388
+ /** Mean Σ_{j∈topK} p_j — how much of the teacher's mass the KL sum covers. */
389
+ meanCapturedMass: number;
390
+ topK: number;
391
+ /** manifest.model — the teacher these numbers are against. */
392
+ refModel: string;
393
+ }
394
+
395
+ /** Teacher-forced candidate logits for one sequence, emitted in
396
+ * position-ordered chunks: `onChunk(startPos, logits[1, C, V])`. The callee
397
+ * owns the array and disposes it after `onChunk` returns. This is the seam
398
+ * the model-free test replaces (no weights, no registry). */
399
+ export type TeacherForcedLogits = (
400
+ tokens: number[],
401
+ onChunk: (startPos: number, logits: MlxArray) => void,
402
+ ) => void;
403
+
404
+ /** The real engine forward: one cache for the whole sequence, prefilled in
405
+ * chunks, lm_head applied per chunk so full-vocab logits for a 4k context
406
+ * never exist at once (4096 × 151k × 4 B ≈ 2.5 GB). */
407
+ export function engineTeacherForcedLogits(
408
+ model: RuntimeModel,
409
+ chunkSize: number,
410
+ ): TeacherForcedLogits {
411
+ return (tokens, onChunk) => {
412
+ const cache = model.makeCache();
413
+ try {
414
+ for (let pos = 0; pos < tokens.length; pos += chunkSize) {
415
+ const end = Math.min(pos + chunkSize, tokens.length);
416
+ const ids = ops.fromInt32(tokens.slice(pos, end), [1, end - pos]);
417
+ let h: MlxArray;
418
+ try {
419
+ h = model.forwardHidden(ids, cache);
420
+ } finally {
421
+ ids.dispose();
422
+ }
423
+ let logits: MlxArray;
424
+ try {
425
+ logits = model.logitsFromHidden(h);
426
+ } finally {
427
+ h.dispose();
428
+ }
429
+ try {
430
+ onChunk(pos, logits);
431
+ } finally {
432
+ logits.dispose();
433
+ }
434
+ evalCacheState(cache);
435
+ clearCache(); // mlx-lm _prefill cadence: don't let chunk transients pile up
436
+ }
437
+ } finally {
438
+ for (const c of cache) c.dispose();
439
+ }
440
+ };
441
+ }
442
+
443
+ interface ChunkScore {
444
+ kl: Float32Array; // [rows]
445
+ mass: Float32Array; // [rows]
446
+ top1: Uint8Array; // [rows] 1 = candidate argmax == teacher argmax
447
+ }
448
+
449
+ /** Score `rows` candidate positions against the matching teacher records.
450
+ * `logits` is [1, C, V]; rows [rowLo, rowHi) of it line up with
451
+ * `records` rows [0, count). */
452
+ function scoreChunkAgainstRecords(
453
+ logits: MlxArray, rowLo: number, rowHi: number, records: ReferenceRecords,
454
+ ): ChunkScore {
455
+ const rows = rowHi - rowLo;
456
+ const K = records.topK;
457
+ const V = logits.shape[2]!;
458
+ if (records.count !== rows)
459
+ throw new Error(`reference-logits: ${records.count} records for ${rows} candidate rows`);
460
+
461
+ // Teacher indices must exist in the candidate's vocabulary.
462
+ let maxIdx = -1;
463
+ for (const v of records.indices) if (v > maxIdx) maxIdx = v;
464
+ if (maxIdx >= V || records.indices.some((v) => v < 0))
465
+ throw new Error(
466
+ `reference-logits: teacher index ${maxIdx} out of range for candidate vocab ${V} ` +
467
+ `— the dump was made with a different tokenizer`,
468
+ );
469
+
470
+ const owned: MlxArray[] = [];
471
+ try {
472
+ const sliced = logits.slice([0, rowLo, 0], [1, rowHi, V]);
473
+ owned.push(sliced);
474
+ const f32 = sliced.dtype === Dtype.float32 ? sliced : sliced.astype(Dtype.float32);
475
+ if (f32 !== sliced) owned.push(f32);
476
+
477
+ const lse = ops.logsumexpAxis(f32, -1, false); // [1, rows], full vocab
478
+ owned.push(lse);
479
+ const argmax = ops.argmaxAxis(f32, -1); // [1, rows], full vocab
480
+ owned.push(argmax);
481
+ const idx = MlxArray.fromInt32(records.indices, [1, rows, K]);
482
+ owned.push(idx);
483
+ const gathered = ops.takeAlongAxis(f32, idx, 2); // [1, rows, K]
484
+ owned.push(gathered);
485
+
486
+ const candTop = gathered.toFloat32();
487
+ const candLse = lse.toFloat32();
488
+ const candArgmax = argmax.toIntTokens();
489
+
490
+ const kl = new Float32Array(rows);
491
+ const mass = new Float32Array(rows);
492
+ const top1 = new Uint8Array(rows);
493
+ for (let r = 0; r < rows; r++) {
494
+ const refLse = records.logsumexp[r]!;
495
+ const qLse = candLse[r]!;
496
+ const base = r * K;
497
+ let acc = 0;
498
+ let m = 0;
499
+ for (let j = 0; j < K; j++) {
500
+ const logP = records.logits[base + j]! - refLse;
501
+ const p = Math.exp(logP);
502
+ if (p === 0) continue;
503
+ m += p;
504
+ acc += p * (logP - (candTop[base + j]! - qLse));
505
+ }
506
+ kl[r] = acc;
507
+ mass[r] = m;
508
+ top1[r] = candArgmax[r] === records.indices[base]! ? 1 : 0;
509
+ }
510
+ return { kl, mass, top1 };
511
+ } finally {
512
+ for (const a of owned) a.dispose();
513
+ }
514
+ }
515
+
516
+ /** KL(saved bf16 teacher ‖ candidate) at EVERY teacher-forced position. */
517
+ export async function evaluateKlVsReferenceLogits(opts: {
518
+ candidate: string;
519
+ /** Dump directory (runs/kl-teacher/<tag>). */
520
+ referenceDir: string;
521
+ /** Cap on sequences scored (default: all in the manifest). */
522
+ nSeqs?: number;
523
+ /** Positions per candidate forward (default 512 — see the memory note). */
524
+ chunkSize?: number;
525
+ /** Test seam: use this forward instead of loading `candidate`. */
526
+ forward?: TeacherForcedLogits;
527
+ }): Promise<RefLogitsKLResult> {
528
+ const t0 = Date.now();
529
+ const dump = ReferenceLogitsDump.open(opts.referenceDir);
530
+ const chunkSize = Math.max(1, opts.chunkSize ?? 512);
531
+ const nSeqs = Math.max(1, Math.min(opts.nSeqs ?? dump.nSeqs, dump.nSeqs));
532
+
533
+ const forward = opts.forward
534
+ ?? engineTeacherForcedLogits((await loadRunnable(opts.candidate)).model, chunkSize);
535
+
536
+ try {
537
+ const perSeq: Float32Array[] = [];
538
+ let nPositions = 0;
539
+ let top1Hits = 0;
540
+ let massSum = 0;
541
+
542
+ for (let s = 0; s < nSeqs; s++) {
543
+ const tokens = dump.tokens(s);
544
+ const scored = Math.min(dump.positionsPerSeq, tokens.length - 1);
545
+ const kls = new Float32Array(scored);
546
+ let filled = 0;
547
+
548
+ forward(tokens, (startPos, logits) => {
549
+ const chunkLen = logits.shape[1]!;
550
+ const lo = startPos;
551
+ const hi = Math.min(startPos + chunkLen, scored);
552
+ if (hi <= lo) return; // trailing positions have no teacher record
553
+ const records = dump.readRecords(s, lo, hi);
554
+ const sc = scoreChunkAgainstRecords(logits, lo - startPos, hi - startPos, records);
555
+ for (let r = 0; r < sc.kl.length; r++) {
556
+ kls[lo + r] = sc.kl[r]!;
557
+ massSum += sc.mass[r]!;
558
+ top1Hits += sc.top1[r]!;
559
+ }
560
+ filled += hi - lo;
561
+ });
562
+
563
+ if (filled !== scored)
564
+ throw new Error(
565
+ `reference-logits: candidate forward covered ${filled} of ${scored} positions ` +
566
+ `on sequence ${s}`,
567
+ );
568
+ perSeq.push(kls);
569
+ nPositions += scored;
570
+ }
571
+
572
+ const agg = aggregate(perSeq);
573
+ return {
574
+ nPrompts: perSeq.length,
575
+ seqLen: dump.ctxLen,
576
+ meanKl: agg.mean,
577
+ medianKl: agg.median,
578
+ p95Kl: agg.p95,
579
+ elapsedSec: (Date.now() - t0) / 1000,
580
+ refLabel: `ref-logits:${dump.manifest.model} top${dump.topK} (${opts.referenceDir})`,
581
+ nPositions,
582
+ top1Agreement: nPositions ? top1Hits / nPositions : 0,
583
+ meanCapturedMass: nPositions ? massSum / nPositions : 0,
584
+ topK: dump.topK,
585
+ refModel: dump.manifest.model,
586
+ };
587
+ } finally {
588
+ dump.close();
589
+ }
590
+ }
@@ -0,0 +1,207 @@
1
+ // Reader for a saved bf16 TEACHER-LOGITS dump (format_version 1).
2
+ //
3
+ // Why the format exists: a bf16 teacher can never be resident on a 32 GB box
4
+ // alongside the candidate, so the teacher's distributions are computed ONCE
5
+ // (Python side, one model at a time) and written to disk as a top-k slice
6
+ // plus the full-vocab logsumexp. Any candidate loaded through OUR engine can
7
+ // then be scored against it (src/eval/kl.ts `evaluateKlVsReferenceLogits`)
8
+ // without the teacher ever being co-resident.
9
+ //
10
+ // On-disk contract (fixed — this module reads it, it does not negotiate it):
11
+ //
12
+ // <dir>/manifest.json { format_version: 1, model, corpus_sha256, ctx_len,
13
+ // n_seqs, top_k, positions_per_seq, created, notes }
14
+ // <dir>/tokens.bin int32 LE [n_seqs][ctx_len] — teacher-forced ids
15
+ // <dir>/seq-<i>.bin one record per r = 0..ctx_len-2; record r is the
16
+ // distribution over the token at index r+1, i.e. the
17
+ // distribution predicted from tokens[0..r] — so record
18
+ // r lines up with candidate logits at POSITION r:
19
+ // int32[top_k] indices, descending by logit
20
+ // float16[top_k] reference logits at those indices
21
+ // float32 logsumexp over the FULL vocab
22
+ //
23
+ // Records are fixed-width (6·top_k + 4 bytes), so a range is one pread — the
24
+ // scorer streams a chunk of records at a time and never holds a whole
25
+ // sequence file (top_k 2048 × ctx 4096 ≈ 50 MB) in the JS heap.
26
+
27
+ import { closeSync, existsSync, openSync, readFileSync, readSync, statSync } from "node:fs";
28
+
29
+ export interface ReferenceLogitsManifest {
30
+ format_version: number;
31
+ model: string;
32
+ corpus_sha256: string;
33
+ ctx_len: number;
34
+ n_seqs: number;
35
+ top_k: number;
36
+ positions_per_seq: number;
37
+ created?: string;
38
+ notes?: string;
39
+ }
40
+
41
+ /** A contiguous run of records, flattened: row i of `indices`/`logits` is
42
+ * `[i*topK, (i+1)*topK)`. Logits are decoded from float16 to float32. */
43
+ export interface ReferenceRecords {
44
+ /** Absolute record index of row 0. */
45
+ start: number;
46
+ count: number;
47
+ topK: number;
48
+ indices: Int32Array; // [count · topK]
49
+ logits: Float32Array; // [count · topK]
50
+ logsumexp: Float32Array; // [count] — over the FULL teacher vocab
51
+ }
52
+
53
+ const SUPPORTED_FORMAT_VERSION = 1;
54
+
55
+ /** float16 bits → float32 (host-side; same expansion as MlxArray.toFloat32Host,
56
+ * kept local so reading a dump never touches the GPU). */
57
+ function decodeFloat16(bits: number): number {
58
+ const sign = bits & 0x8000;
59
+ const exponent = (bits >>> 10) & 0x1f;
60
+ const fraction = bits & 0x03ff;
61
+ let value: number;
62
+ if (exponent === 0) value = fraction === 0 ? 0 : fraction * 2 ** -24;
63
+ else if (exponent === 0x1f) value = fraction === 0 ? Infinity : NaN;
64
+ else value = (1 + fraction / 1024) * 2 ** (exponent - 15);
65
+ return sign ? -value : value;
66
+ }
67
+
68
+ export class ReferenceLogitsDump {
69
+ readonly dir: string;
70
+ readonly manifest: ReferenceLogitsManifest;
71
+ readonly topK: number;
72
+ readonly ctxLen: number;
73
+ readonly nSeqs: number;
74
+ /** Records per sequence file — derived from the file, cross-checked
75
+ * against the manifest. */
76
+ readonly positionsPerSeq: number;
77
+ readonly recordBytes: number;
78
+
79
+ #tokens: Int32Array;
80
+ #fd: number | null = null;
81
+ #fdSeq = -1;
82
+
83
+ private constructor(dir: string, manifest: ReferenceLogitsManifest, tokens: Int32Array, positionsPerSeq: number) {
84
+ this.dir = dir;
85
+ this.manifest = manifest;
86
+ this.topK = manifest.top_k;
87
+ this.ctxLen = manifest.ctx_len;
88
+ this.nSeqs = manifest.n_seqs;
89
+ this.positionsPerSeq = positionsPerSeq;
90
+ this.recordBytes = manifest.top_k * 6 + 4;
91
+ this.#tokens = tokens;
92
+ }
93
+
94
+ static open(dir: string): ReferenceLogitsDump {
95
+ const manifestPath = `${dir}/manifest.json`;
96
+ if (!existsSync(manifestPath))
97
+ throw new Error(`reference-logits: no manifest.json in ${dir}`);
98
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as ReferenceLogitsManifest;
99
+ if (manifest.format_version !== SUPPORTED_FORMAT_VERSION)
100
+ throw new Error(
101
+ `reference-logits: format_version ${manifest.format_version} unsupported ` +
102
+ `(this reader implements ${SUPPORTED_FORMAT_VERSION})`,
103
+ );
104
+ for (const k of ["ctx_len", "n_seqs", "top_k"] as const)
105
+ if (!Number.isInteger(manifest[k]) || manifest[k] <= 0)
106
+ throw new Error(`reference-logits: manifest.${k} must be a positive integer (got ${manifest[k]})`);
107
+
108
+ const tokensBytes = readFileSync(`${dir}/tokens.bin`);
109
+ const wantTokenBytes = manifest.n_seqs * manifest.ctx_len * 4;
110
+ if (tokensBytes.byteLength !== wantTokenBytes)
111
+ throw new Error(
112
+ `reference-logits: tokens.bin is ${tokensBytes.byteLength} B, expected ` +
113
+ `${wantTokenBytes} B (n_seqs ${manifest.n_seqs} × ctx_len ${manifest.ctx_len} × int32)`,
114
+ );
115
+ // Copy: the Buffer's byteOffset need not be int32-aligned for a view.
116
+ const tokens = new Int32Array(
117
+ tokensBytes.buffer.slice(tokensBytes.byteOffset, tokensBytes.byteOffset + tokensBytes.byteLength),
118
+ );
119
+
120
+ const recordBytes = manifest.top_k * 6 + 4;
121
+ const seq0 = `${dir}/seq-0.bin`;
122
+ if (!existsSync(seq0)) throw new Error(`reference-logits: missing ${seq0}`);
123
+ const size = statSync(seq0).size;
124
+ if (size % recordBytes !== 0)
125
+ throw new Error(
126
+ `reference-logits: seq-0.bin is ${size} B, not a multiple of the ` +
127
+ `${recordBytes} B record (top_k ${manifest.top_k})`,
128
+ );
129
+ const positions = size / recordBytes;
130
+ if (positions > manifest.ctx_len - 1)
131
+ throw new Error(
132
+ `reference-logits: seq-0.bin holds ${positions} records but ctx_len ` +
133
+ `${manifest.ctx_len} allows at most ${manifest.ctx_len - 1}`,
134
+ );
135
+ if (Number.isInteger(manifest.positions_per_seq) && manifest.positions_per_seq !== positions)
136
+ console.warn(
137
+ `[reference-logits] manifest.positions_per_seq=${manifest.positions_per_seq} but ` +
138
+ `seq-0.bin holds ${positions} records — using the file`,
139
+ );
140
+ return new ReferenceLogitsDump(dir, manifest, tokens, positions);
141
+ }
142
+
143
+ /** Teacher-forced token ids of sequence `seq` (length ctx_len). */
144
+ tokens(seq: number): number[] {
145
+ this.#assertSeq(seq);
146
+ const off = seq * this.ctxLen;
147
+ return Array.from(this.#tokens.subarray(off, off + this.ctxLen));
148
+ }
149
+
150
+ /** Records [lo, hi) of sequence `seq` — one pread, decoded to float32. */
151
+ readRecords(seq: number, lo: number, hi: number): ReferenceRecords {
152
+ this.#assertSeq(seq);
153
+ if (lo < 0 || hi > this.positionsPerSeq || hi <= lo)
154
+ throw new Error(
155
+ `reference-logits: record range [${lo},${hi}) out of bounds for ` +
156
+ `${this.positionsPerSeq} records`,
157
+ );
158
+ const count = hi - lo;
159
+ const K = this.topK;
160
+ const buf = Buffer.allocUnsafe(count * this.recordBytes);
161
+ const fd = this.#open(seq);
162
+ const read = readSync(fd, buf, 0, buf.byteLength, lo * this.recordBytes);
163
+ if (read !== buf.byteLength)
164
+ throw new Error(`reference-logits: short read on seq-${seq}.bin (${read}/${buf.byteLength} B)`);
165
+
166
+ const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
167
+ const indices = new Int32Array(count * K);
168
+ const logits = new Float32Array(count * K);
169
+ const logsumexp = new Float32Array(count);
170
+ for (let r = 0; r < count; r++) {
171
+ const base = r * this.recordBytes;
172
+ const row = r * K;
173
+ for (let j = 0; j < K; j++) indices[row + j] = dv.getInt32(base + j * 4, true);
174
+ const lbase = base + K * 4;
175
+ for (let j = 0; j < K; j++) logits[row + j] = decodeFloat16(dv.getUint16(lbase + j * 2, true));
176
+ logsumexp[r] = dv.getFloat32(lbase + K * 2, true);
177
+ }
178
+ return { start: lo, count, topK: K, indices, logits, logsumexp };
179
+ }
180
+
181
+ close(): void {
182
+ if (this.#fd !== null) closeSync(this.#fd);
183
+ this.#fd = null;
184
+ this.#fdSeq = -1;
185
+ }
186
+
187
+ #open(seq: number): number {
188
+ if (this.#fdSeq === seq && this.#fd !== null) return this.#fd;
189
+ if (this.#fd !== null) closeSync(this.#fd);
190
+ const path = `${this.dir}/seq-${seq}.bin`;
191
+ if (!existsSync(path)) throw new Error(`reference-logits: missing ${path}`);
192
+ const size = statSync(path).size;
193
+ if (size < this.positionsPerSeq * this.recordBytes)
194
+ throw new Error(
195
+ `reference-logits: seq-${seq}.bin is ${size} B, short of the ` +
196
+ `${this.positionsPerSeq * this.recordBytes} B seq-0.bin defines`,
197
+ );
198
+ this.#fd = openSync(path, "r");
199
+ this.#fdSeq = seq;
200
+ return this.#fd;
201
+ }
202
+
203
+ #assertSeq(seq: number): void {
204
+ if (!Number.isInteger(seq) || seq < 0 || seq >= this.nSeqs)
205
+ throw new Error(`reference-logits: sequence ${seq} out of range (n_seqs ${this.nSeqs})`);
206
+ }
207
+ }
@@ -1,3 +1,4 @@
1
+ import type { GenerateOptions } from "../generate";
1
2
  // Task-eval runner: load a model and greedily complete prompts through the
2
3
  // REAL serving path. Unlike the M0 KL (compat forward() + plain caches),
3
4
  // this drives generate() with the model's kv_config, so generation runs the
@@ -5,9 +6,6 @@
5
6
 
6
7
  import { homedir } from "node:os";
7
8
  import { readFileSync } from "node:fs";
8
- import { generate, type GenerateOptions } from "../generate";
9
- import { clearCache } from "../mlx/ffi";
10
- import * as ops from "../mlx/ops";
11
9
  import { ChatTemplate } from "../chat-template";
12
10
  import { loadModelConfig, type ModelConfig } from "../config";
13
11
  import { runtimeValue } from "../runtime-config";
@@ -17,13 +15,12 @@ import {
17
15
  type RuntimeModel,
18
16
  } from "../model/factory";
19
17
  import { isGlm52Config } from "../model/support";
20
- import { DiffusionGemmaModel } from "../model/diffusion-gemma";
21
18
  import { loadTokenizer, type LoadedTokenizer } from "../tokenizer";
22
19
  import { resolveModelDir } from "./kl";
23
20
 
24
21
  export const EVAL_DATA_DIR = runtimeValue("MLX_BUN_EVAL_DATA") ?? `${homedir()}/.cache/mlx-bun/eval-data`;
25
22
 
26
- /** Read a jsonl dataset exported by scripts/eval/export-datasets.py. */
23
+ /** Read a jsonl dataset exported by scripts/oracle/export-datasets.py. */
27
24
  export function loadJsonl<T = Record<string, unknown>>(name: string): T[] {
28
25
  const path = `${EVAL_DATA_DIR}/${name}.jsonl`;
29
26
  const out: T[] = [];
@@ -55,6 +52,7 @@ export function sampleIndices(total: number, n: number, seed = 42): number[] {
55
52
  type SamplerArm = Partial<Pick<GenerateOptions, "temperature" | "topP" | "topK" | "seed" | "hlg" | "curve">>;
56
53
 
57
54
  export interface TaskModel {
55
+ completionClient?: import("../contracts/completion").CompletionClient<{ body: string; options?: GenOpts }, string>;
58
56
  model: RuntimeModel;
59
57
  tokenizer: LoadedTokenizer;
60
58
  template: ChatTemplate | null;
@@ -117,96 +115,10 @@ export interface GenOpts {
117
115
  kvScheme?: Pick<GenerateOptions, "kvBits" | "kvConfig" | "quantizedKvStart" | "turboQuant">;
118
116
  }
119
117
 
120
- /** Bit-exact greedy decode: raw `model.forward` + argmax loop, which matches mlx-lm
121
- * token-for-token (gen8k: 8000/8000). The product `generate()` decode wrapper
122
- * (forwardHidden/logitsFromHidden + pipelined sampler) diverges from the raw forward
123
- * on near-ties past ~32 tokens, so the EVAL decodes bit-exactly itself rather than
124
- * alter the serving path. Greedy + full-precision KV only. */
125
- export function greedyDecodeBitExact(tm: TaskModel, ids: number[], maxTokens: number): string {
126
- if (tm.activeAdapters) tm.model.loraState.active = tm.activeAdapters; // apply the mounted adapter
127
- const cache = tm.model.makeCache();
128
- try {
129
- // Chunked, LM-head-free prefill. The old single-shot `forward(prompt)`
130
- // materialized [1, L, vocab] logits just to discard them — at a 248k
131
- // vocab that's ~1 GB per 1k prompt tokens, and with no allocator-cache
132
- // clears the Metal buffer cache retained every giant freed tensor
133
- // (the 27B capability-suite 73 GB swap-thrash). Prefill only needs the
134
- // cache state, which forwardHidden advances identically (the LM head
135
- // never touches the cache) — bit-exact by construction. Chunk size and
136
- // clear cadence mirror mlx-lm generate_step / our generate().
137
- const PREFILL_CHUNK = 2048;
138
- const prompt = ids.slice(0, -1);
139
- for (let s = 0; s < prompt.length; s += PREFILL_CHUNK) {
140
- const chunk = prompt.slice(s, s + PREFILL_CHUNK);
141
- const cIds = ops.fromInt32(chunk, [1, chunk.length]);
142
- tm.model.forwardHidden(cIds, cache).dispose();
143
- cIds.dispose();
144
- clearCache();
145
- }
146
- let last = ids[ids.length - 1]!;
147
- const eos = new Set(tm.config.eosTokenIds);
148
- const out: number[] = [];
149
- for (let i = 0; i < maxTokens; i++) {
150
- const lg = tm.model.forward([last], cache);
151
- const am = ops.argmaxAxis(lg, -1); // GPU argmax (mlx-lm greedy: lowest-index tie-break)
152
- lg.dispose();
153
- const best = ops.itemUint32(am);
154
- am.dispose();
155
- if (i % 256 === 0) clearCache(); // mlx-lm decode cadence
156
- if (eos.has(best)) break; // mlx-lm halts on EOS and excludes it from the output
157
- out.push(best);
158
- last = best;
159
- }
160
- return tm.tokenizer.decode(out, true);
161
- } finally {
162
- for (const c of cache) c.dispose();
163
- }
164
- }
165
-
166
- /** Complete `body` via the model's real quantized-KV path. Greedy by default;
167
- * pass `opts.sampler` to drive temperature/HLG/etc. */
168
- export async function generateText(tm: TaskModel, body: string, opts: GenOpts = {}): Promise<string> {
169
- const maxTokens = opts.maxTokens ?? 256;
170
- const enableThinking = opts.enableThinking ?? runtimeValue("MLX_BUN_EVAL_THINK") === "1";
171
- const templated = opts.useChat !== false && tm.template !== null;
172
- const text = templated
173
- ? tm.template!.render([{ role: "user", content: body }], { addGenerationPrompt: true, enableThinking })
174
- : body;
175
- // The chat template ALREADY emits the BOS; encoding it with add_special_tokens on
176
- // would prepend a SECOND BOS (<s><s>…), corrupting generation. Raw bodies (no
177
- // template) get the BOS added as normal.
178
- const ids = tm.tokenizer.encode(text, /* addSpecialTokens */ !templated);
179
-
180
- // optiq's PUBLISHED eval generates with a FULL-PRECISION KV cache — its eval calls
181
- // plain mlx_lm.generate (kv-quant lives in the serving runtime, not the eval). So
182
- // for like-for-like parity we default the eval to UNQUANTIZED KV; MMLU's argmax
183
- // path was already unquantized, which is why it matched while generation didn't.
184
- // MLX_BUN_EVAL_KV_QUANT=1 generates through the model's quantized KV (serving
185
- // config); an explicit opts.kvScheme (cli generate's resolved tier route) wins.
186
- const envKv: GenOpts["kvScheme"] | undefined =
187
- (runtimeValue("MLX_BUN_EVAL_KV_QUANT") === "1" && tm.config.kvQuant?.length)
188
- ? { kvConfig: tm.config.kvQuant, quantizedKvStart: 0 } : undefined;
189
- const kvScheme = opts.kvScheme ?? envKv;
190
- const kvActive = !!(kvScheme && (kvScheme.kvBits || kvScheme.kvConfig?.length || kvScheme.turboQuant));
191
-
192
- // Parity default — greedy + full-precision KV + no sampler arm: decode bit-exactly
193
- // via the raw forward (matches mlx-lm token-for-token). Sampler arms / kv-quant fall
194
- // through to the product generate(). DiffusionGemma is non-autoregressive (no AR
195
- // forward) — always go through generate(), which routes it to the denoising engine.
196
- const isDiffusion = tm.model instanceof DiffusionGemmaModel;
197
- if (!isDiffusion && !kvActive && !opts.sampler && !tm.samplerOverride) {
198
- return greedyDecodeBitExact(tm, ids, maxTokens);
199
- }
200
- const gen = generate(tm.model, ids, {
201
- maxTokens,
202
- temperature: 0, // greedy default — deterministic head-to-head arms
203
- adapters: tm.activeAdapters ?? [], // per-request activation of the mounted eval-adapter
204
- ...(kvActive ? kvScheme : {}),
205
- ...(opts.sampler ?? {}), // overrides temperature when provided
206
- ...(tm.samplerOverride ?? {}), // arm override wins over the task's own sampler
207
- });
118
+ export { greedyDecodeBitExact } from "../backends/mlx/evaluation";
119
+ import { createEvalCompletionClient } from "../backends/mlx/evaluation";
208
120
 
209
- const out: number[] = [];
210
- for await (const { token } of gen) out.push(token);
211
- return tm.tokenizer.decode(out, true);
121
+ /** Application entry point; selection and model math belong to its client. */
122
+ export function generateText(tm: TaskModel, body: string, opts: GenOpts = {}): Promise<string> {
123
+ return (tm.completionClient ?? createEvalCompletionClient(tm)).complete({ body, options: opts });
212
124
  }