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
@@ -1,484 +0,0 @@
1
- # Structured output (grammar-constrained decoding) — design
2
-
3
- Status: LANDED (2026-07-02). Adoption-map item #1
4
- (`docs/design/omlx-adoption-map.md`). L2-class (oMLX is the oracle).
5
-
6
- ## What it is
7
-
8
- OpenAI/oMLX/vLLM-compatible `response_format` + `guided_*` fields on
9
- `/v1/chat/completions` and `/v1/completions`, enforced at the sampler via
10
- **grammar-constrained decoding**: a per-step token bitmask masks invalid
11
- logits to `-inf` before sampling, guaranteeing the generated text is valid
12
- JSON (or matches a JSON schema, regex, EBNF grammar, or enum choice).
13
-
14
- | Field | Compiles to | Example |
15
- | --- | --- | --- |
16
- | `response_format: {type:"json_object"}` | xgrammar builtin JSON grammar | any valid JSON |
17
- | `response_format: {type:"json_schema", json_schema:{name,schema,strict}}` | `compileJSONSchema(schema)` | `{"name":"…","age":N}` |
18
- | `guided_grammar` (EBNF/LARK) | `Grammar.fromEBNF` | `root ::= "\"" [a-z]+ "\""` |
19
- | `guided_regex` | EBNF-wrapped regex | `[A-Z]{3}\d+` |
20
- | `guided_choice` (enum) | `root ::= ("a"\|"b"\|"c")` | `["yes","no","maybe"]` |
21
- | `structured_outputs` | bare schema (alias for json_schema.schema) | — |
22
-
23
- From our side it's **one shape**: compile something → `GrammarMatcher` →
24
- per-step bitmask → mask logits. The only difference between modes is which
25
- xgrammar compile method we call. Precedence mirrors oMLX's
26
- `_effective_guided_grammar`: `guided_grammar > json_schema > json_object >
27
- structured_outputs > guided_regex > guided_choice`.
28
-
29
- ## Engine choice: `@mlc-ai/web-xgrammar` (WASM)
30
-
31
- The **official MLC-ai xgrammar WASM/JS build** (Apache-2.0, same authors as
32
- xgrammar). Same engine oMLX uses, just WASM-packaged. Chosen over a C/ABI
33
- FFI shim because:
34
-
35
- - **No native build.** No `libxgrammar.a` linking, no `extern "C"` shim, no
36
- `build-binary.sh` changes, no signing/notarization concerns. It's an npm
37
- dep that ships WASM, same as any JS dep.
38
- - **Behavioral parity by construction.** Same engine → same bitmask
39
- semantics, same schema→grammar compilation, same structural-tag handling.
40
- - **Fast enough.** Measured per-step `getNextTokenBitmask` on a real JSON
41
- walk (nanosecond timing, state-changing): 0.004–0.19 ms across Gemma 256k
42
- byte_fallback / Llama-3 128k byte_level / Qwen2.5 151k byte_level. <1% of
43
- a GPU decode step. The bitmask is CPU work either way (WASM vs C++ native
44
- would only differ in WASM overhead, which is negligible on M-series), so
45
- the C/ABI fallback was not needed. Evidence: `scripts/experiments/xgrammar-spike.ts`.
46
-
47
- ## Vocab extraction + type detection
48
-
49
- `TokenizerInfo` is fed from data we already have. `loadVocab(tokenizer.json)`:
50
- - `encoded_vocab` = the per-id decoded strings (the `model.vocab` object,
51
- inverted to an ordered array).
52
- - `vocabType`: `decoder.type=="ByteLevel"` → `byte_level` (Llama-3, Qwen);
53
- `decoder` is a `Sequence` containing `ByteFallback` → `byte_fallback`
54
- (Gemma/SentencePiece); else `raw`.
55
- - `prependSpace = (vocabType == "byte_level")` (ByteLevel pretokenizers add
56
- a leading space).
57
-
58
- `TokenizerInfo` is **cached per `tokenizer.json` path** (it decodes every
59
- vocab id once — expensive, but vocab-structural, not per-request).
60
- `LoadedTokenizer.tokenizerJsonPath` threads the path from `loadTokenizer`.
61
-
62
- ## Integration: the pipelining tension
63
-
64
- `GrammarMatcher.acceptToken()` needs the token id as a **JS number**,
65
- which requires the sync GPU readback (`ops.itemUint32`). The decode loop's
66
- pipelining defers that readback to overlap the forward; existing
67
- logits-processors (repetition penalty etc.) operate on device-side token
68
- arrays with no readback, so they ride the sync `LogitsProcessor` array
69
- cleanly. Grammar can't.
70
-
71
- Resolution: grammar requests take a slightly different loop shape in
72
- `generate()` (`src/generate.ts`):
73
-
74
- 1. Eager-read `cur` (the just-sampled token) at the **top** of the loop.
75
- 2. `grammar.accept(tokenId)` — sync matcher advance + fires async
76
- `getNextTokenBitmask()` (overlaps the GPU forward dispatched just below).
77
- 3. `await grammar.ready()` — the mask for the next step is materialized.
78
- 4. `sampleStep` applies `grammar.applyMask(logits)` after the standard
79
- processors, before the sampler.
80
-
81
- Non-grammar requests keep the fast pipelined loop untouched. The cost is
82
- ~0.1 ms/step of GPU idle during the eager readback (grammar is opt-in;
83
- outputs are usually short). The mask fill (CPU WASM) overlaps the forward.
84
-
85
- `terminateWithoutStopToken=true` on the matcher: it terminates when the
86
- **grammar is satisfied** (e.g. the closing `}` of a complete JSON), not
87
- only on an EOS stop token. Without this, the sampler faced an all-`-inf`
88
- mask after the JSON closed (greedy argmax → garbage token 0, matcher
89
- rejected, looped to max_tokens). The loop skips building the next step
90
- once `isTerminated`.
91
-
92
- ## Degrade path (oMLX parity)
93
-
94
- If grammar compile fails (malformed schema/EBNF), the request **degrades**,
95
- never 500:
96
-
97
- - `/v1/chat/completions`: inject a system message — *"You must respond with
98
- valid JSON matching the '<name>' schema. JSON Schema: ```json…```"*
99
- (ported from oMLX `api.tool_calling.build_json_system_prompt`) — and emit
100
- a `Warning` header.
101
- - `/v1/completions`: no chat template to inject into, so emit the `Warning`
102
- header only (documented gap vs oMLX's text-completions behavior).
103
-
104
- xgrammar's EBNF parser calls `abort()` on a parse error, but the WASM trap
105
- surfaces as a catchable `RuntimeError: Aborted()` — the process survives,
106
- WASM state is intact, and a later good grammar still compiles (verified in
107
- `tests/grammar.test.ts`). The `[FATAL]/Aborted()` stderr line is xgrammar's
108
- `LOG(FATAL)` noise, harmless.
109
-
110
- ## Kill switch
111
-
112
- `MLX_BUN_GRAMMAR=0` disables grammar compilation entirely (requests degrade
113
- to prompt injection). Matches the project's envelope-gated kill-switch
114
- discipline (`MLX_BUN_COMPILED_DECODE`, `MLX_BUN_NO_FUSED_SDPA`, …).
115
-
116
- ## Tests
117
-
118
- - `tests/grammar.test.ts` (8 tests, model-free — needs only a tokenizer.json):
119
- step0 mask admits the expected tight set across all five request kinds;
120
- accept/ready advances state; choice + EBNF + json_object + json_schema all
121
- mask correctly; degrade returns null on malformed grammar (abort caught);
122
- text/unset → null; applyMask keeps valid / -infs invalid.
123
- - `scripts/experiments/xgrammar-spike.ts`: phase-0 perf + correctness across
124
- three vocab types.
125
- - `scripts/experiments/xgrammar-parity.ts`: L2 parity vs oMLX through the
126
- real chat template — byte-identical content, both valid JSON.
127
-
128
- ## Known gaps / follow-ups
129
-
130
- - **Whitespace formatting differs from oMLX.** With `anyWhitespace=true`
131
- (the default both use) the grammar allows any whitespace; the model freely
132
- picks indented (ours) vs compact (oMLX). Content is byte-identical; both
133
- are valid JSON. A subtle version-skew (WASM 0.1.27 vs native 0.2.0) /
134
- logit detail at whitespace steps. Not a correctness issue.
135
- - **Whitespace STALL mode** (observed 2026-07-03, B2 bring-up): unlimited
136
- whitespace means a whitespace-degenerate model can greedily emit
137
- whitespace until max_tokens (CPM5-1B base + raw prompt + a boolean
138
- value → tab loop after the colon, 96/96 tokens, no value emitted). Same
139
- exposure as oMLX (same default); chat-tuned models rarely hit it.
140
- The knob LANDED 2026-07-07 (it bit tests/batch-grammar.test.ts on the
141
- M1 Max — 96/96 tabs after a key): `json_schema.any_whitespace: false`
142
- compiles compact separators (no whitespace choice points). Default-off,
143
- oMLX parity stays the default; server-api.md documents the field.
144
- - **B2 batch gates: LANDED 2026-07-03** (`tests/batch-grammar.test.ts`,
145
- `MLX_BUN_TEST_BATCH_DECODE=1`, CPM): all-grammar B=4 four-schemas
146
- cross-bleed gate, mixed-batch sibling byte-match (pinned schedule),
147
- early-termination + churn + joiner, mid-JSON max_tokens truncation,
148
- prefill-terminated row never merges. Bring-up also hardened the F4 cache
149
- (single-flight rebuild on tokenizer switch — concurrent compiles were
150
- double-disposing the previous compiler). Still open from B2: the bench
151
- (all-grammar vs all-free at `--batch 4`) — now part of the Phase E
152
- feature-matrix benchmark (grammar-spec-batching-integration.md).
153
- - **Batched lane.** LANDED (B1, 2026-07-02). The WASM build has no
154
- `BatchGrammarMatcher`; the batched lane (`--batch N`) uses N individual
155
- per-row matchers driven by the scheduler's `#stepGrammar` (read-before-build:
156
- read pending [B] tokens, accept per row, build forward, await ready, sample,
157
- emit). Terminated rows finish + evict before their slot is sampled again
158
- (the all-`-inf` guarantee, per row). `MLX_BUN_GRAMMAR_BATCH=0` forces serial
159
- fallback. A module-level `wasmQueue` serializes ALL xgrammar WASM calls — the
160
- single-threaded WASM instance corrupts under concurrent fills/compiles
161
- (BindingError, caught by the B2 test).
162
- - **Structural tags for thinking models.** `compileStructuralTag` (permissive
163
- during `<think>`, constraining only the output) is supported by xgrammar
164
- but NOT YET WIRED. oMLX wraps the grammar in a structural tag when
165
- `reasoning_parser` is set; the equivalent here needs the thinking-tag
166
- format from `ctx.template`.
167
- - **`/v1/responses` logprobs-style gap.** `response_format` is accepted on
168
- chat + completions; the Responses API shim routes through chat so it
169
- inherits support, but untested there.
170
-
171
- ---
172
-
173
- # Serial review + batched-lane plan (2026-07-02)
174
-
175
- A full read of the serial implementation (`src/grammar.ts`, the grammar
176
- branch in `src/generate.ts`, the server wiring, the spike/parity scripts,
177
- and the batch engine `src/serve/{generation-gateway,batch-scheduler}.ts`)
178
- before wiring grammar into `--batch N`. Findings first, then the plan.
179
-
180
- ## What's solid (verified, keep as-is)
181
-
182
- - **Terminal-state handling.** After the grammar's final character (e.g.
183
- the closing `}`) NO token is valid — the next mask is all-`-inf` and
184
- greedy argmax would return garbage. The serial loop handles this
185
- correctly at both ends: `terminateWithoutStopToken=true` makes the
186
- matcher terminate the moment the grammar is satisfied, and the decode
187
- loop skips building step n+1 when `isTerminated` (generate.ts). The
188
- batched lane must replicate this **per row**: a terminated row must
189
- never be sampled again (plan B1).
190
- - **Perf is a measured non-issue.** Per-step `getNextTokenBitmask` on a
191
- real JSON walk: 0.004–0.19 ms across 128k–256k vocabs (nanosecond
192
- timing, `xgrammar-spike.ts`), ~constant regardless of state tightness;
193
- the CPU-side `applyMask` host loop is inside the <1%-of-decode-step
194
- envelope. At B=4 the aggregate worst case is ~0.8 ms of CPU fill work
195
- that overlaps GPU compute — no device-side masking needed for v1.
196
- - **The pipelining resolution.** Eager-read the sampled token, `accept()`
197
- (fires the async fill), `await ready()` overlapping the next forward —
198
- the right shape; the batch lane reuses it (B1).
199
- - **Degrade path** (never 500, prompt injection + Warning header, WASM
200
- abort is catchable) and **disposal discipline** (generate()'s finally
201
- owns the controller on every exit path).
202
- - **Accept-failure policy**: a matcher reject after masking can only mean
203
- vocab/mask drift — warn + continue, don't kill the generation.
204
-
205
- ## Findings
206
-
207
- **F1 — BUG (serial): stale/garbage final token at the max_tokens
208
- boundary.** In generate.ts, `grammarTok` is only refreshed when
209
- `generated + 1 < maxTokens`, but the token emitted is
210
- `options.grammar ? grammarTok : ops.itemUint32(cur)` unconditionally. When
211
- a grammar generation reaches the max_tokens cap, the LAST iteration skips
212
- the refresh: the emitted token duplicates the previous step's token (or is
213
- `-1` when max_tokens=1). Truncated-JSON responses end on a corrupted
214
- token; `forwarded`/cacheTokens record the wrong id. Fix: always eager-read
215
- `cur` when grammar is on; gate only `accept()`/`ready()` (which exist to
216
- prepare the NEXT step) on "a next step will be built".
217
-
218
- **F2 — CRITICAL (batch): grammar is silently dropped under `--batch N`.**
219
- `RequestShape` has no grammar field, `GenerationGateway.willBatch()` never
220
- checks it, and the batch branch of `gateway.run()` ignores
221
- `options.grammar` entirely. An otherwise-batchable request with
222
- `response_format` is admitted to the batch and generates UNCONSTRAINED —
223
- HTTP 200, no Warning header, schema silently not enforced — and the
224
- compiled `GrammarController` (WASM matcher + compiled grammar + compiler)
225
- is never disposed (generate() never runs), leaking WASM state per request.
226
- The design doc's "serial-lane-only under `--batch`" was intent, not code.
227
- Plan B0 fixes routing immediately; B1 makes batching real.
228
-
229
- **F3 — robustness: mask width vs padded config vocab.** `applyMask`
230
- iterates the logits width V = `config.vocab_size` (padded, can exceed the
231
- tokenizer vocab) while `readyMask` has the MATCHER's width. For padded ids
232
- `this.readyMask[word]!` is `undefined`, and `(undefined >>> bit) & 1`
233
- evaluates to 0 → `-inf`. That is the correct behavior (padding ids are
234
- never valid) but it works by accident under a lying non-null assertion.
235
- Make it explicit (treat out-of-range words as 0) + a comment.
236
-
237
- **F4 — efficiency: fresh `GrammarCompiler` per request.**
238
- `compileGrammarRequest` builds a new compiler every request, discarding
239
- xgrammar's internal compilation cache (`createGrammarCompiler(info,
240
- cacheEnabled?)` — the WASM API exposes it). Agentic clients replay the
241
- same schema hundreds of times; each pays full schema→grammar compile.
242
- Cache ONE compiler per TokenizerInfo (both are vocab-structural); the
243
- `ownsCompiler=false` path in GrammarController already exists for this.
244
-
245
- **F5 — gap: `guided_regex` supports only the regex∩EBNF intersection.**
246
- The WASM build has NO `Grammar.fromRegex` (verified in
247
- `lib/xgrammar.d.ts`: fromEBNF / fromJSONSchema / builtinJSONGrammar /
248
- fromStructuralTag only). Our `root ::= ${regex}` hack accepts regexes that
249
- happen to parse as xgrammar EBNF (char classes, `{m,n}`); real-world
250
- regexes (`\d`, `.`, anchors, groups) hit the degrade path. Follow-up: port
251
- a regex→EBNF translation (native xgrammar has `_regex_to_ebnf`) or
252
- document the subset. Not a batching blocker.
253
-
254
- **F6 — minor: `guided_choice` escaping** covers `\` and `"` only; a choice
255
- containing a newline/control char breaks the EBNF string literal. Escape
256
- `\n\r\t` or 400 on control chars.
257
-
258
- **F7 — known, hotter under batch: thinking models.** The mask constrains
259
- from token 0, so it forbids the `<think>` preamble. Qwen3.5 — a headline
260
- `--batch` model, thinking default-on — makes this visible. The fix is
261
- structural tags (`fromStructuralTag` IS in the WASM API): permissive
262
- during the think span, constrained output after. Separate work item; not
263
- part of the batching plan.
264
-
265
- ## The plan
266
-
267
- ### B0 — correctness stopgaps (land first, small)
268
-
269
- 1. **F1 fix** in generate.ts: under grammar, read `grammarTok =
270
- ops.itemUint32(cur)` unconditionally at the top of the loop; keep
271
- `accept()`/`ready()` gated on `generated + 1 < maxTokens`. Regression
272
- test: grammar + max_tokens smaller than the JSON → truncated output's
273
- final token is the actually-sampled token (and max_tokens=1 emits a
274
- real token, not -1).
275
- 2. **F2 routing fix**: add `hasGrammar` to `RequestShape` (true when a
276
- controller compiled — degrade-path requests have NO controller and stay
277
- batchable, the injection already happened at the prompt level);
278
- `willBatch()` returns false for it. This makes the design doc's claim
279
- true and stops the silent non-enforcement + controller leak TODAY,
280
- independent of B1's schedule.
281
- 3. Lane-debug: `MLX_BUN_LANE_DEBUG=1` line includes `hasGrammar`.
282
-
283
- Exit: `--batch 4` server + `response_format` request → enforced output
284
- (valid schema-conformant JSON), routed serial, controller disposed.
285
-
286
- ### B1 — per-row grammar in the batch scheduler
287
-
288
- No `BatchGrammarMatcher` in the WASM build → N independent per-row
289
- matchers, driven by the scheduler. The design maps the serial loop's three
290
- grammar duties (mask-before-sample, accept-after-read,
291
- terminate-without-stop) onto the batch engine's existing structures:
292
-
293
- - **`BatchRequest.grammar?: GrammarController`** (new field). The gateway
294
- passes `options.grammar` through on the batch lane and **owns disposal**
295
- in a finally around `submit()` (covers resolve, reject, eviction, and
296
- the whole-batch-drop error path — the scheduler uses but never owns).
297
- - **Mask application** goes inside the gateway's existing per-row `sample`
298
- closure: after the logits processors, before `toLogprobs` — same "grammar
299
- has the final say" ordering as serial `sampleStep`. `applyMask` is sync
300
- and reused as-is; the documented precondition is that the scheduler has
301
- awaited `ready()` before invoking `sample` for that row.
302
- - **Prefill (admission)**: token 0 is already sampled through the row's
303
- closure, and the controller is primed at compile time, so the step-0
304
- mask applies with zero scheduler changes. After the existing
305
- `#readToken`: `accept(tok0)`; if the matcher terminated (a 1-token
306
- grammar), emit + finish("stop") WITHOUT merging into the batch;
307
- otherwise the fill is in flight and `ready()` is awaited before that
308
- row's next sample.
309
- - **`#step()` with ≥1 live grammar row** takes the read-before-build shape
310
- (the serial resolution, transplanted):
311
- 1. Read the pending `[B]` token array NOW (one sync readback, before
312
- building the next step's graph).
313
- 2. For each grammar row: `accept(tok_b)` — fires that row's async fill.
314
- Rows whose matcher terminated are marked done: they keep their `[B]`
315
- slot through the forward (one harmless KV write, exactly like today's
316
- length-doomed placeholder rows) but are NOT sampled.
317
- 3. Build the batched forward graph (host-side; the fills overlap it).
318
- 4. `await ready()` on every live grammar row.
319
- 5. Sample per live row (closure applies the mask), asyncEval.
320
- 6. Emit using the values read in (1) — no second readback — via the
321
- existing `#emitRows`; grammar-terminated rows emit their final token
322
- and finish with `finish_reason: "stop"`, then the existing filter
323
- evicts them. **This is the all-`-inf` guarantee, per row: a
324
- terminated row is finished/evicted before its slot is ever sampled
325
- again.**
326
- Batches with NO grammar rows keep the current pipelined path
327
- byte-identical (zero cost when the feature is unused).
328
- - **The trade, stated:** while any grammar row is live the batch runs
329
- effectively NO_PIPELINE (the readback no longer overlaps the next step's
330
- GPU compute) — same math, scheduling only; bounded by the readback
331
- (~0.1 ms) + fills (0.004–0.19 ms/row, overlapped with graph build).
332
- Serial grammar pays the identical trade today. Rejected alternative:
333
- speculative pipelining via the matcher's `rollBack(1)` (sample with a
334
- stale mask, roll back on mismatch) — complexity without evidence; the
335
- WASM API exposes rollBack if a measured need ever appears.
336
- - **EOS / stop sequences / accept-failure**: unchanged semantics. Grammar
337
- termination is an additional per-row stop source; accept-failure warns
338
- and continues (serial parity).
339
- - **Levers**: `MLX_BUN_GRAMMAR=0` still kills the feature (degrade
340
- everywhere). New `MLX_BUN_GRAMMAR_BATCH=0` forces grammar rows back to
341
- the serial lane (B0 behavior) — the A/B + kill switch for the new code,
342
- house style.
343
- - Seeded requests already route serial (`userSeed`), so grammar+seed
344
- reproducibility is untouched.
345
-
346
- ### B2 — gates (tests + numbers)
347
-
348
- - **Scheduler tests** (model-gated, alongside tests/batch-scheduler.test.ts):
349
- - Mixed batch: 1 grammar row + free-running siblings — grammar row emits
350
- valid schema JSON; siblings byte-match a no-grammar-sibling run (mask
351
- isolation).
352
- - All-grammar B=4 with FOUR DIFFERENT schemas — each row conforms to ITS
353
- schema. This is THE bug-class gate: row↔matcher misalignment after
354
- eviction/join would cross-bleed masks.
355
- - Early termination + churn: a grammar row terminates mid-batch,
356
- siblings continue, a joiner admits afterward; controller disposed.
357
- - max_tokens truncation mid-JSON under batch (F1's regression, batch
358
- flavor).
359
- - Prefill-terminated row (grammar satisfied at token 0) never merges.
360
- - **Model-free unit** (tests/grammar.test.ts): N matchers advanced
361
- interleaved with overlapping async fills — the one novel runtime
362
- assumption (concurrent per-row fills on the single WASM instance don't
363
- cross-contaminate) gets cheap insurance.
364
- - **Lane parity**: same greedy request through serial vs batch lane → both
365
- valid + schema-conformant. Byte-equality is NOT the gate (batched
366
- forward is mlx-lm-B=N-parity, not B=1-parity; the scheduler's existing
367
- gate is teacher-forced/KL) — the grammar gate is validity + matcher
368
- never rejects + correct finish_reasons.
369
- - **Bench**: `--batch 4` all-grammar vs all-free, aggregate tok/s + TTFT
370
- (cpm5 + e4b). Every perf claim gets a number on this machine; expected
371
- cost <2% from the pipeline trade.
372
-
373
- ## Addendum (2026-07-02): XGrammar-2 / TVM-FFI / the WASM version gap
374
-
375
- Investigated per Josh: [XGrammar-2](https://blog.mlc.ai/2026/05/04/xgrammar-2-fast-customizable-structured-generation)
376
- (native 0.2.x, May 2026) vs our `@mlc-ai/web-xgrammar` 0.1.27 WASM.
377
-
378
- **What XGrammar-2 adds (native):** Structural Tag v2 (composable JSON DSL:
379
- sequence/tag/any_text/triggered_tags/**regex**/json_schema — uniformly
380
- expresses harmony format, tool calling, reasoning channels), cross-grammar
381
- caching + repetition-state compression (up to 80× compile speedup),
382
- **`BatchGrammarMatcher`** (`batch_fill_next_token_bitmask` /
383
- `batch_accept_token` / `batch_rollback` — a C++ **thread pool** over N
384
- matchers, one call per step), `traverse_draft_tree` for speculative
385
- decoding, and [TVM-FFI](https://github.com/apache/tvm-ffi)-based bindings
386
- (Python/C++/Rust/JS) with **MLX support** (`pip install "xgrammar[metal]"`).
387
-
388
- **The npm package IS behind, but less than the version number implies.**
389
- [npm 0.1.27](https://www.npmjs.com/package/@mlc-ai/web-xgrammar) was
390
- published 2025-12-25 (native 0.2.0 landed 2026-05-01 — that's the
391
- whitespace-parity version skew). BUT the `web/` tree on current main still
392
- says 0.1.27 while its `xgrammar_binding.cc` compiles against the CURRENT
393
- `cpp/` core — a source rebuild today produces an XGrammar-2-core WASM.
394
- The npm version number is just stale packaging.
395
-
396
- **BatchGrammarMatcher is NOT in the WASM binding — and wouldn't help at
397
- our B anyway.** Verified in `web/src/xgrammar_binding.cc` on main: no
398
- batch class is bound. Its win is the C++ thread pool, and the emscripten
399
- build isn't compiled with pthreads — a WASM batch call would be a serial
400
- C++ loop, saving only per-call JS↔WASM overhead on fills already measured
401
- at 0.004–0.19 ms. At B≤4 (≤0.8 ms/step aggregate, overlapped) there is no
402
- prize. **B1's N-per-row-matchers design stands regardless of engine
403
- version.**
404
-
405
- **Two upgrade paths, decoupled from B1:**
406
-
407
- - **U1 — rebuild web-xgrammar from current main** (emsdk + `web/build.sh`,
408
- vendor the artifact). Gets the XGrammar-2 core as WASM: cross-grammar
409
- cache + repetition compression (compile-time wins — F4's pain),
410
- Structural Tag v2 incl. `RegexFormat` (a real F5 fix: compile
411
- `guided_regex` via `fromStructuralTag({format:{type:"regex",pattern}})`
412
- instead of the `root ::= regex` EBNF hack) and the triggered-tags
413
- machinery F7 needs for thinking models. Also re-tests the
414
- whitespace-parity gap (it was 0.1.27-vs-0.2.0 skew). Same API surface,
415
- no native-build/signing implications. NOTE: the INSTALLED 0.1.27
416
- typedefs already declare `RegexFormat`/`fromStructuralTag` — try F5/F7
417
- against the current package first; upgrade only if the Dec-2025 core
418
- predates the semantics we need.
419
- - **U2 — native XGrammar-2 over the TVM-FFI stable C ABI via bun:ffi.**
420
- The original "no native build" rationale is weakened by TVM-FFI (a
421
- stable C ABI is exactly what we bind mlx-c through today), and it would
422
- buy: real threaded batch fill, MLX-side mask application, and
423
- `traverse_draft_tree` — which composes with the DSpark speculative-
424
- decoding path (grammar-masking a draft tree is otherwise awkward).
425
- Costs: a dylib in distribution (build-binary.sh, signing/notarization),
426
- a new FFI surface. Trigger conditions, not calendar: B grows past ~8,
427
- spec decode + grammar needs to compose, or agentic multi-tool schemas
428
- make compile time visible despite the F4 cache. Until one fires, WASM
429
- stays the engine.
430
-
431
- ### B3 — follow-ups (explicitly out of scope for the batch landing)
432
-
433
- F4 compiler cache (biggest real-world win — agentic schema replay), F5
434
- regex via structural tag `RegexFormat` (see addendum — try the installed
435
- package first, else U1), F6 choice escaping, F3 explicit padded-vocab
436
- handling, F7 structural tags for thinking models (batch-relevant:
437
- Qwen3.5; the Structural Tag v2 machinery in the addendum is the tool),
438
- U1/U2 engine upgrades (addendum), and device-side bitmask expansion
439
- (upload the packed `ceil(V/32)` int32 mask ≈ 8 KB instead of host-building
440
- a V-float mask ≈ 1 MB/step/row, expand with `right_shift`+`bitwise_and`
441
- on device) — ONLY if the B2 bench shows the O(B·V) host loop, which the
442
- measured serial numbers say it won't at B≤4.
443
-
444
- ## Addendum (2026-07-07): jump-forward decoding LANDED (opt-in, serial lane)
445
-
446
- `MLX_BUN_GRAMMAR_JUMP=1` enables SGLang's jump-forward technique through
447
- the WASM API we already ship (`GrammarMatcher.findJumpForwardString`,
448
- exposed since 0.1.27 and previously uncalled): after each matcher
449
- advance, if the grammar admits exactly ONE continuation string, the
450
- serial decode loop retokenizes it (`tokenizer.encode(str, false)` — no
451
- specials), advances the matcher per id (partial-accept keeps the
452
- accepted prefix; matcher/emitted lockstep holds by construction, no
453
- rollback machinery), and carries the whole burst into the KV with ONE
454
- `[1, 1+m]` forward instead of m masked single-token forwards.
455
- `GrammarController.jumpForward` owns the contract (src/grammar.ts);
456
- generate()'s jump iteration owns the KV/history/`forwarded`/emission
457
- bookkeeping.
458
-
459
- Why opt-in (Lab, no oracle): the emitted STRING is identical and
460
- validity is preserved, but encode(jumpStr) standalone may tokenize a
461
- forced span differently than the model would have sampled it — the
462
- token stream, and content generated after the span (conditioning), can
463
- legally differ from an unjumped run. oMLX does not jump, so there is
464
- nothing to be bit-exact against. Gates: string-losslessness invariants +
465
- schema validity + jump-fired telemetry (`controller.jumpedTokens`),
466
- tests/grammar-jump.test.ts (contract suite + Llama-3.2-1B e2e, flag
467
- on/off).
468
-
469
- Notes for the next pass:
470
- - `any_whitespace:false` compiles to a FIXED pretty-printed layout
471
- (measured: step-0 jump = `{\n "name": "` on the person schema) —
472
- fixed formatting makes forced spans longer, so compact/fixed schemas
473
- are where the flag pays.
474
- - Excluded for `logprobs` requests (jumped tokens are never sampled).
475
- - SP-family (byte_fallback) tokenizers whose raw encode prepends space
476
- never jump — first id gets matcher-rejected, guard degrades to normal
477
- decode. Structurally safe, documented in server-config.md.
478
- - Batch lane (`#stepGrammar`) does NOT jump yet: per-row bursts make the
479
- batched step ragged — follow-up only if serial measurements justify it.
480
- - Landed alongside: a disposed-guard on the async bitmask fill
481
- (`fireFill`) — disposing a controller with a queued fill used to call
482
- into the deleted WASM matcher and poison the module-wide wasmChain for
483
- every later grammar request (BindingError; found by the jump contract
484
- tests, latent for any exception between accept() and ready()).
@@ -1,127 +0,0 @@
1
- # Design: mlx-bun desktop app (Tauri)
2
-
3
- A native macOS desktop app that wraps the existing mlx-bun chat UI in a
4
- window — double-click an icon, get local AI, no terminal. The app is a
5
- **thin shell**: it spawns `mlx-bun serve` as a sidecar and points a
6
- WKWebView at the chat UI the server already serves. Almost no new code;
7
- the value is packaging + UX, not new functionality.
8
-
9
- Companion docs: [embedding.md](../reference/embedding.md) (the sidecar
10
- layout this builds on), [distribution.md](../reference/distribution.md)
11
- (the sign/notarize pipeline this reuses).
12
-
13
- ## Where it lives — separate repo (recommended)
14
-
15
- Ship the app from its **own repo** (`mlx-bun-desktop`), not inside this
16
- one.
17
-
18
- - **Toolchain isolation.** This repo is Bun/TS. Tauri drags in Rust,
19
- Cargo, and `src-tauri/target/` (heavy build artifacts). Keeping it out
20
- preserves the core repo's clean dependency surface and scope.
21
- - **Different product, cadence, audience.** The app's artifact is a
22
- signed `.dmg` / Homebrew **Cask** for end users; the core repo ships a
23
- CLI tarball + npm package for developers. They version independently.
24
- - **The sidecar is already a published artifact.** The app pulls a
25
- pinned, signed, notarized `mlx-bun` bundle from a core-repo release
26
- (the versionless `mlx-bun-arm64.tar.gz`, or a tagged
27
- `mlx-bun-v<ver>-arm64.tar.gz`) — no source coupling needed.
28
- - **Precedent.** The Homebrew tap is already a separate repo; this fits.
29
-
30
- In-repo alternative (a `desktop/` subdir) is viable if tight version
31
- lockstep ever matters more than toolchain cleanliness — the mechanics
32
- below are identical, only the sidecar source changes (local `dist/`
33
- instead of a downloaded release). Default to separate.
34
-
35
- ## Architecture
36
-
37
- ```
38
- mlx-bun-desktop.app
39
- ├─ src-tauri (Rust shell)
40
- │ on launch → pick a free port, spawn sidecar:
41
- │ mlx-bun serve <model> --port <p> --no-open
42
- │ readiness → poll GET http://127.0.0.1:<p>/v1/models until 200
43
- │ window → WKWebView → http://127.0.0.1:<p>/#/chat
44
- │ on quit → SIGKILL the sidecar (restart→ready is ~0.4s; safe)
45
- └─ Contents/Resources/
46
- mlx-bun + libmlxc.dylib + libmlx.dylib + libjaccl.dylib
47
- + mlx.metallib + pi assets (the bundle build-binary.sh produces)
48
- ```
49
-
50
- The window renders the **same page** `mlx-bun serve` serves today
51
- (`/#/chat`, the embedded pi web chat). No bespoke UI.
52
-
53
- ## Components
54
-
55
- 1. **Sidecar binary.** Tauri `externalBin` (target-triple-suffixed, e.g.
56
- `mlx-bun-aarch64-apple-darwin`); the dylibs + metallib + pi assets go
57
- in `bundle.resources` beside it. mlx-bun resolves its libs via
58
- `dirname(realpath(execPath))`, so the flat Resources layout works
59
- unchanged (verified: Bun realpaths execPath through symlinks).
60
- 2. **Spawn / teardown** (`main.rs`). Use Tauri's shell/sidecar API (or
61
- `std::process::Command`) to launch on `setup`, kill on window close /
62
- app exit. One generation runs at a time server-side, so no client
63
- concurrency to manage.
64
- 3. **Port.** Bind an ephemeral port in Rust, pass it to both the spawn
65
- (`--port`) and the webview URL, to avoid 8080 collisions with a
66
- user's own `mlx-bun serve`.
67
- 4. **Readiness gate.** Show a lightweight "starting…/downloading model…"
68
- splash until `/v1/models` returns 200. Warm start ~0.4s; first-ever
69
- run downloads the starter model (~0.9 GB) — the existing onboarding
70
- page already narrates this, so the splash can hand off to it.
71
-
72
- ## Distribution
73
-
74
- A `.app` is a better Gatekeeper citizen than the flat CLI binary:
75
-
76
- - **Stapleable.** Unlike a bare Mach-O, a `.app`/`.dmg` accepts a stapled
77
- notarization ticket → works **offline**, no online check needed.
78
- - **Channels:** signed `.dmg` direct download, and/or a Homebrew **Cask**
79
- (`brew install --cask mlx-bun`) — a natural 5th install path.
80
- - **Not App Store.** JIT + a spawned child process + the Metal model
81
- don't fit the sandbox. Developer-ID + notarized + DMG/Cask outside the
82
- store is the same flow this project already uses.
83
-
84
- ### Signing / entitlements
85
-
86
- Reuse the core pipeline. The nested sidecar `mlx-bun` still needs the
87
- same entitlements as the standalone build
88
- ([packaging/entitlements.plist](../../packaging/entitlements.plist)):
89
-
90
- - `com.apple.security.cs.allow-jit` + `allow-unsigned-executable-memory`
91
- (JavaScriptCore JIT) — without these the sidecar is killed on launch.
92
- - `com.apple.security.cs.disable-library-validation` — the sidecar
93
- dlopens the bundled MLX dylibs.
94
-
95
- Tauri's bundler signs nested binaries during the `.app` signing pass;
96
- point these entitlements at the sidecar specifically. Notarize the
97
- `.app` (or `.dmg`) with the existing `AC_PROFILE` notary credentials,
98
- then `xcrun stapler staple`.
99
-
100
- ## Milestones
101
-
102
- 1. **Spike** — `tauri init` in a new repo; hardcode port 8080; spawn a
103
- locally-built `mlx-bun serve --no-open`; load `/#/chat` in the window;
104
- `cargo tauri dev`. Proves the wrap end-to-end.
105
- 2. **Robust runtime** — ephemeral port; readiness poll + splash;
106
- clean teardown on quit; surface sidecar stderr to a log.
107
- 3. **Bundle + sidecar** — wire `externalBin` + `bundle.resources` to a
108
- pinned core-repo release tarball (download in a build script, or check
109
- in a fetch step); first-run model-download UX.
110
- 4. **Sign + notarize + package** — Developer-ID sign with nested
111
- entitlements, notarize, staple, emit `.dmg`.
112
- 5. **Cask** — formula in a tap pointing at the `.dmg` release asset;
113
- `brew install --cask`.
114
-
115
- ## Open questions / risks
116
-
117
- - **Tauri sidecar entitlement plumbing** — confirm Tauri applies the
118
- per-binary entitlements to `externalBin` (may need a custom signing
119
- hook in the bundler config). Validate early in milestone 4.
120
- - **Window vs. menubar** — a normal window is the MVP; a menubar/tray app
121
- ("always-on local model") is a possible later mode.
122
- - **Model picker** — MVP serves one model (the starter). Switching models
123
- means restarting the sidecar; defer a UI for that.
124
- - **Version pinning** — decide whether the app tracks `latest` or pins a
125
- specific `mlx-bun` release per app version (pin is safer for repro).
126
- - **Universal binary** — arm64-only today; Tauri can target both, but
127
- MLX is Apple-Silicon-only, so x86 is moot.