mlx-bun 0.0.13 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -578
- package/bin/mlx-bun.mjs +1 -1
- package/docs/README.md +91 -0
- package/docs/{design → archive}/hlg-sampling.md +3 -3
- package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
- package/docs/{design → archive/investigations}/colibri-glm52-port.md +1 -1
- package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
- package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
- package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
- package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
- package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
- package/docs/{planning → archive/planning}/chunk-task-roadmap.md +1 -1
- package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +4 -4
- package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +1 -1
- package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
- package/docs/archive/planning/release-notes-v0.2.0.md +49 -0
- package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
- package/docs/design/batching.md +569 -0
- package/docs/design/decode-speed-program.md +3178 -169
- package/docs/design/dreaming-nightly-pipeline.md +675 -282
- package/docs/design/generic-model-support.md +579 -334
- package/docs/design/kv-cache.md +680 -0
- package/docs/design/orpo-training.md +673 -1239
- package/docs/design/speculative-decoding.md +1627 -0
- package/docs/design/turboquant.md +2102 -0
- package/docs/design/unified-engine-frontier-plan.md +1831 -727
- package/docs/design/web-chat-redesign.md +666 -1083
- package/docs/planning/PRODUCT_ROADMAP.md +34 -1
- package/docs/planning/release-notes-v0.3.0.md +35 -0
- package/docs/planning/video-series.md +421 -0
- package/docs/reference/benchmarks.md +1162 -0
- package/docs/reference/cli.md +395 -149
- package/docs/reference/distribution.md +321 -181
- package/docs/reference/environment.md +348 -0
- package/docs/reference/glossary.md +39 -0
- package/docs/reference/library-api.md +580 -142
- package/docs/reference/memory.md +227 -64
- package/docs/reference/models.md +137 -6
- package/docs/reference/server-api.md +938 -422
- package/docs/reference/server-config.md +652 -320
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +166 -0
- package/package.json +10 -5
- package/src/admission-plan.ts +18 -0
- package/src/anthropic.ts +92 -3
- package/src/audio/decode.ts +2 -2
- package/src/audio/features.ts +2 -2
- package/src/backends/mlx/autoregressive.ts +108 -0
- package/src/backends/mlx/batch-group.ts +1646 -0
- package/src/backends/mlx/diffusion.ts +39 -0
- package/src/backends/mlx/draft-target.ts +47 -0
- package/src/backends/mlx/evaluation.ts +105 -0
- package/src/backends/mlx/fill-append.ts +45 -0
- package/src/backends/mlx/gateway-binding.ts +71 -0
- package/src/backends/mlx/graph.ts +38 -0
- package/src/backends/mlx/memory-client.ts +189 -0
- package/src/backends/mlx/memory-guard.ts +40 -0
- package/src/backends/mlx/methods.ts +90 -0
- package/src/backends/mlx/model-prompt.ts +164 -0
- package/src/backends/mlx/model-serving.ts +79 -0
- package/src/backends/mlx/prefill.ts +33 -0
- package/src/backends/mlx/request-policy.ts +20 -0
- package/src/backends/mlx/rollback.ts +51 -0
- package/src/backends/mlx/serial-executor.ts +273 -0
- package/src/backends/mlx/speculative.ts +114 -0
- package/src/backends/mlx/state-views.ts +26 -0
- package/src/backends/mlx/text-engine.ts +73 -0
- package/src/cli.ts +189 -61
- package/src/client.ts +66 -0
- package/src/config.ts +21 -4
- package/src/contracts/completion.ts +16 -0
- package/src/contracts/execution.ts +49 -0
- package/src/contracts/generation.ts +103 -0
- package/src/contracts/host.ts +7 -0
- package/src/contracts/jobs.ts +66 -0
- package/src/contracts/pi-provider.ts +30 -0
- package/src/contracts/pi.ts +259 -0
- package/src/contracts/resources.ts +18 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/contracts/tsconfig.json +14 -0
- package/src/diffusion/diffusion-generate.ts +218 -153
- package/src/embed.ts +1 -1
- package/src/engine/admission.ts +59 -0
- package/src/engine/cancellation.ts +38 -0
- package/src/engine/engine.ts +56 -0
- package/src/engine/execution-coordinator.ts +70 -0
- package/src/engine/execution-plan.ts +51 -0
- package/src/engine/resources.ts +42 -0
- package/src/engine/scheduler.ts +67 -0
- package/src/engine/session.ts +247 -0
- package/src/eval/README.md +44 -1
- package/src/eval/kl.ts +263 -23
- package/src/eval/reference-logits.ts +207 -0
- package/src/eval/runner.ts +10 -79
- package/src/eval/tasks/bfcl.ts +2 -1
- package/src/eval/tasks/gsm8k.ts +2 -1
- package/src/eval/tasks/hashhop.ts +2 -1
- package/src/eval/tasks/humaneval.ts +5 -4
- package/src/eval/tasks/ifeval.ts +2 -1
- package/src/eval/tasks/mmlu.ts +2 -1
- package/src/expert-io.ts +3 -1
- package/src/expert-offload.ts +3 -1
- package/src/fill/echo-index.ts +188 -0
- package/src/fill/fill-session.ts +452 -0
- package/src/fill/proposal.ts +68 -0
- package/src/fill/schema-rows.ts +506 -0
- package/src/fill/tool-boundary.ts +103 -0
- package/src/fit.ts +24 -43
- package/src/generate.ts +740 -337
- package/src/grammar.ts +14 -8
- package/src/harness-pi.ts +15 -34
- package/src/hub-rest.ts +4 -4
- package/src/index.ts +27 -0
- package/src/inference/denoising.ts +17 -0
- package/src/inference/graph.ts +33 -0
- package/src/inference/prefill.ts +39 -0
- package/src/inference/rollback.ts +7 -0
- package/src/jobs/db.ts +4 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-entry.ts +15 -14
- package/src/jobs/runner.ts +98 -13
- package/src/jobs/task-client.ts +19 -0
- package/src/jobs/types.ts +2 -58
- package/src/kv-scheme.ts +199 -0
- package/src/kv-store.ts +459 -234
- package/src/lab/README.md +18 -0
- package/src/{assets → lab/curve}/curve-designer.html +2 -2
- package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
- package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +7 -5
- package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
- package/src/library.ts +40 -0
- package/src/lora.ts +18 -0
- package/src/media-fetch.ts +20 -4
- package/src/memory/article.ts +1 -1
- package/src/memory/chunk.ts +1 -1
- package/src/memory/model.ts +32 -170
- package/src/memory/prompts.ts +1 -1
- package/src/memory/rest.ts +1 -1
- package/src/memory/schedule.ts +1 -1
- package/src/memory/stages.ts +4 -1
- package/src/memory/vault.ts +3 -4
- package/src/mlx/array.ts +9 -10
- package/src/mlx/compile.ts +12 -2
- package/src/mlx/ffi.ts +66 -22
- package/src/mlx/materialize.ts +23 -0
- package/src/mlx/metal-kernel.ts +10 -11
- package/src/mlx/ops.ts +64 -19
- package/src/mlx/quantized-rows.ts +36 -0
- package/src/mlx/token-bitmask.ts +52 -0
- package/src/mlx/turboquant-kv-decode.ts +82 -0
- package/src/mlx/turboquant-kv-inverse.ts +122 -0
- package/src/mlx/turboquant-ops.ts +33 -3
- package/src/mlx/turboquant-tables.ts +2 -2
- package/src/model/batched-mask.ts +9 -3
- package/src/model/batched-quant.ts +2 -2
- package/src/model/batched-rotating-quant.ts +45 -99
- package/src/model/batched-rotating-state.ts +73 -0
- package/src/model/batched-rotating.ts +57 -106
- package/src/model/batched-row-storage.ts +127 -0
- package/src/model/compiled-decode.ts +4 -3
- package/src/model/diffusion-gemma.ts +8 -8
- package/src/model/factory.ts +87 -49
- package/src/model/fingerprint.ts +1 -1
- package/src/model/flash-attention.ts +3 -3
- package/src/model/gemma4-base.ts +187 -17
- package/src/model/gemma4.ts +6 -6
- package/src/model/generated/gemma4-12b.ts +52 -148
- package/src/model/generated/gemma4-26b.ts +34 -94
- package/src/model/generated/gemma4-e4b.ts +28 -76
- package/src/model/glm52-cache.ts +19 -9
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +28 -1
- package/src/model/implementation.ts +69 -0
- package/src/model/minicpm5.ts +7 -8
- package/src/model/profile.ts +443 -0
- package/src/model/qwen-append.ts +12 -0
- package/src/model/qwen-conv.ts +52 -0
- package/src/model/qwen3-delta.ts +98 -2
- package/src/model/qwen3-mrope.ts +252 -0
- package/src/model/qwen3_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +336 -60
- package/src/model/support.ts +1 -1
- package/src/model/trellis-balanced-scatter.ts +51 -0
- package/src/model/trellis-linear.ts +672 -0
- package/src/model/trellis-shared-m.ts +151 -0
- package/src/model/trellis-shared-scatter.ts +144 -0
- package/src/model/trellis-splitk-prefill.ts +158 -0
- package/src/model/trellis-tiled-prefill.ts +136 -0
- package/src/model/trellis-vector-expand.ts +62 -0
- package/src/model/trellis-wide-prefill.ts +94 -0
- package/src/model/universal/dense.ts +1 -1
- package/src/model/universal/modules.ts +8 -1
- package/src/native/frame_extract.swift +140 -0
- package/src/native-pack.ts +15 -7
- package/src/pi-provider.ts +6 -64
- package/src/pi-web.ts +14 -320
- package/src/prompt-cache.ts +97 -54
- package/src/quantize/allocator.ts +1 -1
- package/src/quantize/atomic-output.ts +38 -0
- package/src/quantize/calibration.ts +2 -1
- package/src/quantize/config-writer.ts +16 -3
- package/src/quantize/index.ts +28 -1
- package/src/quantize/job.ts +11 -0
- package/src/quantize/quantizer.ts +265 -100
- package/src/quantize/rotate.ts +603 -0
- package/src/quantize/safetensors-writer.ts +79 -1
- package/src/quantize/sensitivity.ts +41 -59
- package/src/quantize/trellis.ts +586 -0
- package/src/quantize/weight-transform.ts +292 -0
- package/src/responses.ts +130 -3
- package/src/runtime-config.ts +81 -0
- package/src/sampler.ts +276 -4
- package/src/serve/admin-routes.ts +184 -0
- package/src/serve/aux-routes.ts +132 -0
- package/src/serve/batch-scheduler.ts +8 -1558
- package/src/serve/chat-request.ts +391 -0
- package/src/serve/chat-stage.ts +195 -0
- package/src/serve/checkpoint-identity.ts +64 -0
- package/src/serve/completion-executor.ts +354 -0
- package/src/serve/completion-sink.ts +157 -0
- package/src/serve/discovery-routes.ts +204 -0
- package/src/serve/generation-gateway.ts +211 -243
- package/src/serve/http.ts +159 -0
- package/src/serve/inference-request.ts +79 -0
- package/src/serve/isolate.ts +297 -105
- package/src/serve/kv-budget.ts +14 -0
- package/src/serve/lab-routes.ts +341 -0
- package/src/serve/lane-registry.ts +2 -1
- package/src/serve/model-admin-routes.ts +155 -0
- package/src/serve/model-binding.ts +43 -0
- package/src/serve/model-host.ts +512 -0
- package/src/serve/openai-wire.ts +103 -0
- package/src/serve/parent-application.ts +56 -0
- package/src/serve/pipeline.ts +33 -0
- package/src/serve/preparation.ts +33 -0
- package/src/serve/prompt-response-trace.ts +147 -0
- package/src/serve/request-plan.ts +162 -0
- package/src/serve/request-prep.ts +346 -0
- package/src/serve/response-lease.ts +24 -0
- package/src/serve/responses-client.ts +69 -0
- package/src/serve/session-completion-engine.ts +110 -0
- package/src/serve/session-search.ts +1 -1
- package/src/serve/static-routes.ts +101 -0
- package/src/serve/text-completion-stage.ts +104 -0
- package/src/serve/token-streams.ts +519 -0
- package/src/server.ts +523 -3551
- package/src/spec/assistant-source.ts +10 -44
- package/src/spec/deepspec-source.ts +6 -6
- package/src/spec/dflash-source.ts +9 -8
- package/src/spec/dspark/ab-stats.ts +3 -3
- package/src/spec/dspark/calibration.ts +2 -2
- package/src/spec/dspark/deepspec-module.ts +2 -2
- package/src/spec/dspark/generate-dflash.ts +2 -2
- package/src/spec/dspark/generate.ts +2 -2
- package/src/spec/dspark/loader.ts +1 -1
- package/src/spec/dspark/module-dflash.ts +5 -4
- package/src/spec/dspark/module.ts +3 -3
- package/src/spec/dspark/quantize-drafter.ts +1 -1
- package/src/spec/generate.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +9 -1
- package/src/spec/ngram-source.ts +1 -1
- package/src/spec/prefix-state.ts +45 -0
- package/src/spec/qwen-mtp-source.ts +528 -0
- package/src/spec/serve-loop.ts +179 -213
- package/src/spec/source.ts +42 -10
- package/src/spec/two-model.ts +1 -1
- package/src/ssd-cache.ts +95 -24
- package/src/ssd-durability.ts +207 -0
- package/src/tool-call.ts +51 -14
- package/src/train/flash-cce.ts +14 -13
- package/src/train/forward.ts +38 -1
- package/src/train/loss.ts +3 -2
- package/src/train/prefix-shared.ts +5 -1
- package/src/train/segmented.ts +9 -6
- package/src/train/trainer.ts +9 -8
- package/src/vision/preprocess.ts +1 -1
- package/src/vision/prompt.ts +48 -2
- package/src/vision/qwen3vl-preprocess.ts +409 -0
- package/src/vision/qwen3vl-prompt.ts +196 -0
- package/src/vision/qwen3vl-tower.ts +555 -0
- package/src/vision/video-frames.ts +148 -0
- package/src/web/app.html +1 -1
- package/src/web/app.js +15 -6
- package/src/web/src/adapters-panel.ts +3 -3
- package/src/web/src/assistant.ts +2 -36
- package/src/web/src/chat.ts +16 -9
- package/src/web/src/composer.ts +4 -4
- package/src/web/src/hub.ts +1 -1
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +1 -1
- package/src/web/src/model-picker.ts +1 -1
- package/src/web/src/palette.ts +1 -1
- package/src/web/src/protocol.ts +3 -39
- package/src/web/src/sessions.ts +1 -1
- package/src/web/src/ui-catalog.ts +4 -18
- package/src/web-assets.ts +47 -0
- package/src/weights.ts +134 -33
- package/docs/archive/mlx-bun-lab-report.html +0 -275
- package/docs/archive/wwdc-mlx-bun.html +0 -848
- package/docs/design/adapters-end-to-end.md +0 -205
- package/docs/design/batching-perf-path.md +0 -255
- package/docs/design/batching-v2-plan.md +0 -529
- package/docs/design/bucketing-stage.md +0 -197
- package/docs/design/compat-cli-surface-design.md +0 -230
- package/docs/design/docs-reorg-plan.md +0 -165
- package/docs/design/dspark-serving-program.md +0 -371
- package/docs/design/dspark-speculative-decoding.md +0 -268
- package/docs/design/grammar-spec-batching-integration.md +0 -301
- package/docs/design/memory-inference-path.md +0 -422
- package/docs/design/memory-synthesis.md +0 -287
- package/docs/design/memory-system.md +0 -237
- package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
- package/docs/design/omlx-adoption-map.md +0 -98
- package/docs/design/orpo-dynamic-lambda.md +0 -220
- package/docs/design/orpo-future-enhancements.md +0 -340
- package/docs/design/paged-kv-cache.md +0 -214
- package/docs/design/parallel-slots.md +0 -567
- package/docs/design/parity-tier-dag.md +0 -161
- package/docs/design/repo-cleanup-plan.md +0 -189
- package/docs/design/runtime-isolation.md +0 -263
- package/docs/design/segmented-backward-training.md +0 -543
- package/docs/design/spec-decode-larger-targets.md +0 -229
- package/docs/design/ssd-kv-cold-tier.md +0 -288
- package/docs/design/structured-output.md +0 -484
- package/docs/design/tauri-desktop-app.md +0 -127
- package/docs/design/the-dreaming-master-plan.md +0 -445
- package/docs/design/turboquant-kv.md +0 -235
- package/docs/design/w4a16-compute-precision-spike.md +0 -232
- package/docs/design/web-chat-beat-matrix.md +0 -315
- package/docs/design/write-pipeline-entity.md +0 -468
- package/docs/investigations/curve-runs/bisector-route.json +0 -98
- package/docs/investigations/curve-runs/bisector-trace.json +0 -1393
- package/docs/investigations/curve-runs/curve-terrain.json +0 -1179
- package/docs/investigations/curve-runs/curve-terrain.svg +0 -42
- package/docs/investigations/hlg-ab.html +0 -34
- package/docs/investigations/hlg-explorer.html +0 -238
- package/docs/investigations/hlg-figs/cliff.svg +0 -1
- package/docs/investigations/hlg-figs/cross.svg +0 -1
- package/docs/investigations/hlg-figs/edges.svg +0 -1
- package/docs/investigations/hlg-figs/k20-grids.svg +0 -1
- package/docs/investigations/hlg-figs/k20-peaks.svg +0 -1
- package/docs/investigations/hlg-figs/lw-curve.svg +0 -1
- package/docs/investigations/hlg-figs/terrain.svg +0 -1
- package/docs/investigations/hlg-report.html +0 -262
- package/docs/investigations/hlg-runs/hlg-ab.json +0 -149
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
- package/docs/investigations/hlg-runs/hlg-blind.json +0 -107
- package/docs/investigations/hlg-runs/hlg-cliff.json +0 -197
- package/docs/investigations/hlg-runs/hlg-cross.json +0 -505
- package/docs/investigations/hlg-runs/hlg-edge.json +0 -388
- package/docs/investigations/hlg-runs/hlg-eval1.json +0 -25
- package/docs/investigations/hlg-runs/hlg-frontier.json +0 -86
- package/docs/investigations/hlg-runs/hlg-grid.json +0 -242
- package/docs/investigations/hlg-runs/hlg-grid2.json +0 -343
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
- package/docs/investigations/hlg-runs/hlg-map.json +0 -383
- package/docs/investigations/hlg-runs/run0-original.log +0 -159
- package/docs/investigations/hlg-runs/runA-recommended.log +0 -131
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +0 -8
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +0 -16
- package/docs/investigations/hlg-runs/runB-gain-variants.log +0 -192
- package/docs/investigations/hlg-runs/runC-pivots.log +0 -162
- package/docs/investigations/hlg-runs/runD-oetf.log +0 -132
- package/docs/investigations/hlg-runs/runE-eotf.log +0 -162
- package/docs/investigations/hlg-runs/runF-shaper-working.log +0 -86
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
- package/docs/investigations/hlg-runs/runH-wsweep.log +0 -176
- package/docs/investigations/hlg-runs/runI-asweep.log +0 -176
- package/docs/investigations/hlg-runs/runJ-knobmap.log +0 -49
- package/docs/investigations/hlg-runs/runK-frontier.log +0 -19
- package/docs/investigations/hlg-runs/runL-widemap.log +0 -65
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +0 -15
- package/docs/investigations/hlg-runs/runN-verify.log +0 -116
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +0 -79
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +0 -33
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
- package/docs/investigations/hlg-runs/runU-cross-12b.log +0 -33
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +0 -22
- package/docs/investigations/hlg-runs/runW-lw-12b.log +0 -22
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
- package/docs/planning/IDEAS.md +0 -69
- package/docs/reference/embedding.md +0 -197
- package/docs/reference/features-matrix.md +0 -100
- package/docs/reference/orpo-quickstart.md +0 -135
- package/src/flags.ts +0 -13
- /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
- /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
- /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
- /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
- /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
- /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
- /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
- /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
- /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
- /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
- /package/docs/{planning → archive/planning}/journal.md +0 -0
- /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
- /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
- /package/{docs/investigations → src/serve/assets}/curve-terrain.html +0 -0
- /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.
|