@mlx-bun/inference 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +1475 -2
- package/THIRD_PARTY_NOTICES.md +3404 -0
- package/dist/native/libmlx_bun_ane.dylib +0 -0
- package/dist/native/libmlx_bun_expert_io.dylib +0 -0
- package/dist/native/mlx-bun-frame-extract +0 -0
- package/examples/fit-model.ts +25 -0
- package/examples/qwen3-forward.ts +42 -0
- package/examples/qwen3-generate.ts +28 -0
- package/examples/trellis-expand.ts +23 -0
- package/package.json +129 -4
- package/src/adapters/available.ts +110 -0
- package/src/adapters/index.ts +3 -0
- package/src/adapters/manager.ts +316 -0
- package/src/artifacts/auxiliary-files.ts +45 -0
- package/src/artifacts/config-dialects.ts +94 -0
- package/src/artifacts/config.ts +341 -0
- package/src/artifacts/draft-vocabulary.ts +30 -0
- package/src/artifacts/expert-offload-build.ts +75 -0
- package/src/artifacts/expert-offload.ts +89 -0
- package/src/artifacts/experts/index.ts +3 -0
- package/src/artifacts/experts/io.ts +386 -0
- package/src/artifacts/experts/residency.ts +1134 -0
- package/src/artifacts/experts/usage.ts +415 -0
- package/src/artifacts/fingerprint.ts +44 -0
- package/src/artifacts/glm52/container.ts +305 -0
- package/src/artifacts/glm52/expert-layout.ts +249 -0
- package/src/artifacts/glm52/index.ts +7 -0
- package/src/artifacts/glm52/kernel-layout.ts +124 -0
- package/src/artifacts/glm52/layout.ts +152 -0
- package/src/artifacts/glm52/memory.ts +500 -0
- package/src/artifacts/glm52/resident-weights.ts +135 -0
- package/src/artifacts/glm52/weights.ts +503 -0
- package/src/artifacts/glm52-config.ts +262 -0
- package/src/artifacts/identity.ts +94 -0
- package/src/artifacts/index.ts +15 -0
- package/src/artifacts/mmap.ts +90 -0
- package/src/artifacts/safetensors-writer.ts +249 -0
- package/src/artifacts/safetensors.ts +164 -0
- package/src/artifacts/turboquant-fold.ts +42 -0
- package/src/artifacts/weight-audit.ts +43 -0
- package/src/artifacts/weights.ts +255 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/mlx/attention.ts +8 -0
- package/src/contracts/mlx/cache.ts +510 -0
- package/src/contracts/mlx/checkpoint.ts +10 -0
- package/src/contracts/mlx/draft-checkpoint.ts +14 -0
- package/src/contracts/mlx/draft-projection.ts +27 -0
- package/src/contracts/mlx/draft-target.ts +84 -0
- package/src/contracts/mlx/drafter.ts +176 -0
- package/src/contracts/mlx/forward-work.ts +23 -0
- package/src/contracts/mlx/glm52-weights.ts +25 -0
- package/src/contracts/mlx/graph.ts +309 -0
- package/src/contracts/mlx/head.ts +6 -0
- package/src/contracts/mlx/index.ts +15 -0
- package/src/contracts/mlx/kv-codec.ts +9 -0
- package/src/contracts/mlx/media.ts +113 -0
- package/src/contracts/mlx/positions.ts +16 -0
- package/src/contracts/mlx/row-table.ts +2 -0
- package/src/contracts/mlx/trainable.ts +103 -0
- package/src/contracts/mlx/whisper.ts +96 -0
- package/src/contracts/portable/composition.ts +106 -0
- package/src/contracts/portable/denoising.ts +17 -0
- package/src/contracts/portable/execution.ts +79 -0
- package/src/contracts/portable/generation.ts +103 -0
- package/src/contracts/portable/graph.ts +80 -0
- package/src/contracts/portable/index.ts +15 -0
- package/src/contracts/portable/memory-plan.ts +26 -0
- package/src/contracts/portable/object-cache.ts +8 -0
- package/src/contracts/portable/prefill.ts +63 -0
- package/src/contracts/portable/prefix-cache.ts +33 -0
- package/src/contracts/portable/prompt.ts +2 -0
- package/src/contracts/portable/resources.ts +25 -0
- package/src/contracts/portable/rollback.ts +7 -0
- package/src/contracts/portable/sampling.ts +44 -0
- package/src/contracts/portable/scheduling.ts +54 -0
- package/src/contracts/portable/token-delivery.ts +4 -0
- package/src/contracts/portable/tsconfig.json +14 -0
- package/src/embeddings/index.ts +1 -0
- package/src/embeddings/text.ts +85 -0
- package/src/execution/admission.ts +59 -0
- package/src/execution/batch-group.ts +1240 -0
- package/src/execution/batch-types.ts +225 -0
- package/src/execution/cancellation.ts +38 -0
- package/src/execution/continuation-request.ts +45 -0
- package/src/execution/continuation-types.ts +10 -0
- package/src/execution/coordinator.ts +70 -0
- package/src/execution/denoising-group.ts +180 -0
- package/src/execution/engine.ts +56 -0
- package/src/execution/fill-group.ts +505 -0
- package/src/execution/fit.ts +184 -0
- package/src/execution/gateway-binding.ts +178 -0
- package/src/execution/grammar-group.ts +224 -0
- package/src/execution/index.ts +18 -0
- package/src/execution/methods.ts +75 -0
- package/src/execution/mixed-iteration.ts +96 -0
- package/src/execution/plan.ts +65 -0
- package/src/execution/prefill-cohort.ts +100 -0
- package/src/execution/prefill-rows.ts +263 -0
- package/src/execution/request-policy.ts +19 -0
- package/src/execution/row-sampling.ts +49 -0
- package/src/execution/scheduler.ts +92 -0
- package/src/execution/session.ts +247 -0
- package/src/execution/speculative-group.ts +797 -0
- package/src/execution/tasks.ts +44 -0
- package/src/generation/autoregressive.ts +840 -0
- package/src/generation/bindings/autoregressive.ts +104 -0
- package/src/generation/bindings/denoising.ts +60 -0
- package/src/generation/bindings/prefill-policy.ts +36 -0
- package/src/generation/checkpoint-identity.ts +60 -0
- package/src/generation/denoising.ts +85 -0
- package/src/generation/diffusion.ts +583 -0
- package/src/generation/fill/echo-index.ts +188 -0
- package/src/generation/fill/index.ts +3 -0
- package/src/generation/fill/proposal.ts +69 -0
- package/src/generation/fill/schema-rows.ts +506 -0
- package/src/generation/fill/session.ts +464 -0
- package/src/generation/fill/tool-boundary.ts +103 -0
- package/src/generation/fill-append.ts +58 -0
- package/src/generation/generate.ts +44 -0
- package/src/generation/grammar-step.ts +39 -0
- package/src/generation/index.ts +14 -0
- package/src/generation/phase-dispatch.ts +115 -0
- package/src/generation/prefill.ts +33 -0
- package/src/generation/result.ts +49 -0
- package/src/generation/scopes.ts +157 -0
- package/src/generation/speculative/acceptance.ts +41 -0
- package/src/generation/speculative/adaptive-draft-gate.ts +166 -0
- package/src/generation/speculative/bindings/assistant-rows.ts +102 -0
- package/src/generation/speculative/bindings/autoregressive-rows.ts +124 -0
- package/src/generation/speculative/bindings/binding.ts +106 -0
- package/src/generation/speculative/bindings/deepspec-rows.ts +33 -0
- package/src/generation/speculative/bindings/draft-target.ts +10 -0
- package/src/generation/speculative/bindings/glm52-mtp-rows.ts +128 -0
- package/src/generation/speculative/bindings/projected-context-rows.ts +78 -0
- package/src/generation/speculative/bindings/projected-draft-rows.ts +95 -0
- package/src/generation/speculative/bindings/qwen-mtp-rows.ts +336 -0
- package/src/generation/speculative/bindings/two-model-rows.ts +63 -0
- package/src/generation/speculative/cache-identity.ts +16 -0
- package/src/generation/speculative/draft-kind.ts +149 -0
- package/src/generation/speculative/draft-policy.ts +41 -0
- package/src/generation/speculative/draft-registry.ts +118 -0
- package/src/generation/speculative/draft-vocab.ts +110 -0
- package/src/generation/speculative/index.ts +17 -0
- package/src/generation/speculative/output.ts +49 -0
- package/src/generation/speculative/round.ts +307 -0
- package/src/generation/speculative/source.ts +144 -0
- package/src/generation/speculative/sources/assistant-source.ts +64 -0
- package/src/generation/speculative/sources/deepspec-source.ts +70 -0
- package/src/generation/speculative/sources/dflash-source.ts +84 -0
- package/src/generation/speculative/sources/dflash2-source.ts +86 -0
- package/src/generation/speculative/sources/glm52-mtp-source.ts +66 -0
- package/src/generation/speculative/sources/ngram-source.ts +165 -0
- package/src/generation/speculative/sources/qwen-mtp-source.ts +154 -0
- package/src/generation/speculative/sources/two-model.ts +75 -0
- package/src/generation/types.ts +194 -0
- package/src/index.ts +20 -0
- package/src/input/audio/audiotoolbox.ts +112 -0
- package/src/input/audio/decode.ts +133 -0
- package/src/input/audio/features.ts +202 -0
- package/src/input/audio/index.ts +6 -0
- package/src/input/audio/transcode.ts +67 -0
- package/src/input/audio/whisper-mel.ts +185 -0
- package/src/input/audio/whisper-tokenizer.ts +238 -0
- package/src/input/chat-template.ts +389 -0
- package/src/input/index.ts +6 -0
- package/src/input/media-fetch.ts +439 -0
- package/src/input/packed-mixed.ts +43 -0
- package/src/input/special-tokens.ts +75 -0
- package/src/input/token-groups.ts +49 -0
- package/src/input/tokenizer.ts +336 -0
- package/src/input/tool-call.ts +689 -0
- package/src/input/vision/index.ts +3 -0
- package/src/input/vision/preprocess.ts +247 -0
- package/src/input/vision/prompt.ts +398 -0
- package/src/input/vision/qwen3vl-preprocess.ts +409 -0
- package/src/input/vision/qwen3vl-prompt.ts +237 -0
- package/src/input/vision/video-frames.ts +100 -0
- package/src/kernels/ane/fill.ts +101 -0
- package/src/kernels/ane/linear.ts +416 -0
- package/src/kernels/attention/flash.ts +694 -0
- package/src/kernels/attention/kvq4-decode.ts +155 -0
- package/src/kernels/attention/masks.ts +40 -0
- package/src/kernels/attention/multi-query.ts +193 -0
- package/src/kernels/attention/paged.ts +130 -0
- package/src/kernels/delta/gated.ts +306 -0
- package/src/kernels/delta/index.ts +3 -0
- package/src/kernels/delta/silu-rms-heads.ts +69 -0
- package/src/kernels/delta/state.ts +74 -0
- package/src/kernels/gated-rms-norm.ts +64 -0
- package/src/kernels/glm52/layout.ts +37 -0
- package/src/kernels/glm52/streamed-experts.ts +360 -0
- package/src/kernels/logits.ts +38 -0
- package/src/kernels/quantization/affine-row.ts +154 -0
- package/src/kernels/quantization/affine-rows.ts +36 -0
- package/src/kernels/quantization/affine-split-mma.ts +144 -0
- package/src/kernels/quantization/affine3-mma.ts +117 -0
- package/src/kernels/quantization/affine3-rows.ts +103 -0
- package/src/kernels/quantization/affine4-mma.ts +113 -0
- package/src/kernels/quantization/affine4-rows.ts +102 -0
- package/src/kernels/residual-rms-norm.ts +65 -0
- package/src/kernels/sampling/index.ts +2 -0
- package/src/kernels/sampling/normalized-argmax.ts +116 -0
- package/src/kernels/sampling/token-bitmask.ts +52 -0
- package/src/kernels/softcap.ts +35 -0
- package/src/kernels/trellis/codebook.ts +123 -0
- package/src/kernels/trellis/down-codebook.ts +184 -0
- package/src/kernels/trellis/down-factored.ts +180 -0
- package/src/kernels/trellis/down-k3-interleaved-factored.ts +146 -0
- package/src/kernels/trellis/down-mma-wide.ts +195 -0
- package/src/kernels/trellis/down-mma.ts +145 -0
- package/src/kernels/trellis/expand.ts +42 -0
- package/src/kernels/trellis/gate-up-codebook-mma.ts +141 -0
- package/src/kernels/trellis/gate-up-codebook.ts +137 -0
- package/src/kernels/trellis/gate-up-factored.ts +197 -0
- package/src/kernels/trellis/gate-up-mma-wide.ts +147 -0
- package/src/kernels/trellis/gate-up-mma.ts +148 -0
- package/src/kernels/trellis/gate-up.ts +248 -0
- package/src/kernels/trellis/geometry.ts +22 -0
- package/src/kernels/trellis/index.ts +103 -0
- package/src/kernels/trellis/launch.ts +12 -0
- package/src/kernels/trellis/mixed-gate-up-factored.ts +119 -0
- package/src/kernels/trellis/mixed-gate-up.ts +146 -0
- package/src/kernels/trellis/qualified.ts +34 -0
- package/src/kernels/trellis/reduce.ts +150 -0
- package/src/kernels/trellis/scatter.ts +407 -0
- package/src/kernels/trellis/splitk-prefill.ts +152 -0
- package/src/kernels/trellis/tiled-prefill.ts +131 -0
- package/src/kernels/trellis/vector-expand.ts +58 -0
- package/src/kernels/trellis/wide-prefill.ts +89 -0
- package/src/kernels/turboquant/decode.ts +82 -0
- package/src/kernels/turboquant/index.ts +5 -0
- package/src/kernels/turboquant/inverse.ts +122 -0
- package/src/kernels/turboquant/ops.ts +860 -0
- package/src/kernels/turboquant/signs.ts +130 -0
- package/src/kernels/turboquant/tables.ts +124 -0
- package/src/layers/affine-verify-linear.ts +75 -0
- package/src/layers/ane-prefill-split.ts +283 -0
- package/src/layers/compiled-normalization.ts +41 -0
- package/src/layers/delta-heads.ts +23 -0
- package/src/layers/dense-embedding.ts +29 -0
- package/src/layers/dense-linear.ts +32 -0
- package/src/layers/dequantized-linear.ts +72 -0
- package/src/layers/folded-attention.ts +42 -0
- package/src/layers/geglu.ts +130 -0
- package/src/layers/helpers.ts +7 -0
- package/src/layers/index.ts +36 -0
- package/src/layers/kv4-decode-attention.ts +15 -0
- package/src/layers/layer-norm.ts +24 -0
- package/src/layers/loading.ts +92 -0
- package/src/layers/lora.ts +150 -0
- package/src/layers/normalization.ts +9 -0
- package/src/layers/prefix-layout.ts +33 -0
- package/src/layers/quantized-append-attention.ts +32 -0
- package/src/layers/quantized-attention.ts +353 -0
- package/src/layers/quantized-embedding.ts +39 -0
- package/src/layers/quantized-linear.ts +137 -0
- package/src/layers/quantized-switch-linear.ts +37 -0
- package/src/layers/qwen-append.ts +12 -0
- package/src/layers/qwen-mrope.ts +238 -0
- package/src/layers/rope-2d.ts +110 -0
- package/src/layers/rope.ts +389 -0
- package/src/layers/swiglu.ts +23 -0
- package/src/layers/trellis-codebook-down.ts +52 -0
- package/src/layers/trellis-codebook-gate-up.ts +42 -0
- package/src/layers/trellis-down.ts +117 -0
- package/src/layers/trellis-gate-up.ts +71 -0
- package/src/layers/trellis-linear.ts +211 -0
- package/src/layers/trellis-prompt.ts +102 -0
- package/src/layers/trellis-reduce-gate-up.ts +73 -0
- package/src/layers/trellis-scatter-down.ts +98 -0
- package/src/models/ane-install.ts +74 -0
- package/src/models/audio/conformer.ts +656 -0
- package/src/models/cache-composition.ts +44 -0
- package/src/models/capabilities.ts +89 -0
- package/src/models/chat-template.ts +10 -0
- package/src/models/composition.ts +107 -0
- package/src/models/diffusion-gemma/experts.ts +59 -0
- package/src/models/diffusion-gemma/family.ts +21 -0
- package/src/models/diffusion-gemma/model.ts +900 -0
- package/src/models/diffusion-gemma/vision.ts +549 -0
- package/src/models/drafter-loaders.ts +49 -0
- package/src/models/drafters.ts +39 -0
- package/src/models/factory.ts +351 -0
- package/src/models/families.ts +70 -0
- package/src/models/family.ts +125 -0
- package/src/models/gemma4/assistant-target.ts +46 -0
- package/src/models/gemma4/assistant.ts +327 -0
- package/src/models/gemma4/blocks.ts +755 -0
- package/src/models/gemma4/compiled-decode.ts +452 -0
- package/src/models/gemma4/family.ts +39 -0
- package/src/models/gemma4/gemma4-12b.ts +677 -0
- package/src/models/gemma4/gemma4-26b.ts +544 -0
- package/src/models/gemma4/gemma4-e4b.ts +599 -0
- package/src/models/gemma4/graphs.ts +147 -0
- package/src/models/gemma4/media.ts +88 -0
- package/src/models/gemma4/model.ts +860 -0
- package/src/models/gemma4/serving-blocks.ts +72 -0
- package/src/models/gemma4/trainable.ts +186 -0
- package/src/models/gemma4/written-blocks.ts +232 -0
- package/src/models/gemma4/written-forward.ts +159 -0
- package/src/models/gemma4/written-input.ts +29 -0
- package/src/models/glm52/atlas.ts +507 -0
- package/src/models/glm52/coupling.ts +388 -0
- package/src/models/glm52/dsa.ts +601 -0
- package/src/models/glm52/family.ts +20 -0
- package/src/models/glm52/mla.ts +812 -0
- package/src/models/glm52/model.ts +1127 -0
- package/src/models/glm52/moe.ts +223 -0
- package/src/models/glm52/mtp.ts +60 -0
- package/src/models/glm52/pilot.ts +537 -0
- package/src/models/glm52/reference.ts +445 -0
- package/src/models/glm52/residency.ts +557 -0
- package/src/models/glm52/streamed-experts.ts +447 -0
- package/src/models/graph.ts +59 -0
- package/src/models/implementation.ts +230 -0
- package/src/models/index.ts +15 -0
- package/src/models/media-input.ts +15 -0
- package/src/models/memory-plan.ts +39 -0
- package/src/models/minicpm5/family.ts +29 -0
- package/src/models/minicpm5/model.ts +394 -0
- package/src/models/minicpm5/trainable.ts +90 -0
- package/src/models/profile.ts +337 -0
- package/src/models/qwen/blocks.ts +658 -0
- package/src/models/qwen/checkpoint.ts +190 -0
- package/src/models/qwen/families.ts +57 -0
- package/src/models/qwen/media-input.ts +161 -0
- package/src/models/qwen/mtp.ts +101 -0
- package/src/models/qwen/qwen3-moe.ts +355 -0
- package/src/models/qwen/qwen3.ts +323 -0
- package/src/models/qwen/qwen38-27b-trellis-m1max.ts +1532 -0
- package/src/models/qwen/qwen38-27b-trellis-m4pro.ts +1092 -0
- package/src/models/qwen/qwen3_5.ts +433 -0
- package/src/models/qwen/written-layers.ts +285 -0
- package/src/models/row-table.ts +1 -0
- package/src/models/runtime.ts +73 -0
- package/src/models/speculative/deepspec-artifact.ts +10 -0
- package/src/models/speculative/deepspec.ts +744 -0
- package/src/models/speculative/dflash.ts +553 -0
- package/src/models/speculative/dflash2-artifact.ts +9 -0
- package/src/models/speculative/dflash2-operations.ts +1 -0
- package/src/models/speculative/dflash2.ts +576 -0
- package/src/models/speculative/loader.ts +29 -0
- package/src/models/support.ts +79 -0
- package/src/models/universal/archs.ts +365 -0
- package/src/models/universal/blocks.ts +479 -0
- package/src/models/universal/dense.ts +255 -0
- package/src/models/universal/family.ts +17 -0
- package/src/models/vision/qwen3vl.ts +566 -0
- package/src/models/vision/siglip.ts +520 -0
- package/src/models/vision/unified.ts +176 -0
- package/src/models/whisper/family.ts +19 -0
- package/src/models/whisper/fast.ts +734 -0
- package/src/models/whisper/model.ts +430 -0
- package/src/runtime/compiled-trace.ts +16 -0
- package/src/runtime/config.ts +77 -0
- package/src/runtime/expert-trace.ts +71 -0
- package/src/runtime/graph-forward.ts +65 -0
- package/src/runtime/integers.ts +6 -0
- package/src/runtime/native.ts +16 -0
- package/src/runtime/resources.ts +53 -0
- package/src/runtime/row-operations.ts +42 -0
- package/src/runtime/trace.ts +161 -0
- package/src/sampling/curve.ts +122 -0
- package/src/sampling/draft.ts +55 -0
- package/src/sampling/extras.ts +45 -0
- package/src/sampling/filters.ts +134 -0
- package/src/sampling/grammar.ts +630 -0
- package/src/sampling/hlg.ts +290 -0
- package/src/sampling/index.ts +9 -0
- package/src/sampling/processors.ts +142 -0
- package/src/sampling/sampler.ts +120 -0
- package/src/sampling/step.ts +343 -0
- package/src/sampling/types.ts +161 -0
- package/src/sampling/window-filters.ts +46 -0
- package/src/scoring/decisions.ts +96 -0
- package/src/scoring/full-sequence.ts +103 -0
- package/src/scoring/index.ts +9 -0
- package/src/scoring/kl.ts +29 -0
- package/src/scoring/perplexity.ts +165 -0
- package/src/scoring/response-logps.ts +65 -0
- package/src/state/affine-attention.ts +180 -0
- package/src/state/attachment.ts +68 -0
- package/src/state/attention-read.ts +215 -0
- package/src/state/batched-kv.ts +294 -0
- package/src/state/batched-mask.ts +445 -0
- package/src/state/batched-quant.ts +389 -0
- package/src/state/batched-quantized-kv.ts +209 -0
- package/src/state/batched-rotating-quant.ts +457 -0
- package/src/state/batched-rotating-state.ts +149 -0
- package/src/state/batched-rotating.ts +430 -0
- package/src/state/batched-row-storage.ts +179 -0
- package/src/state/batched-ssm.ts +54 -0
- package/src/state/batched-turboquant-kv.ts +144 -0
- package/src/state/bf16-first-kv.ts +663 -0
- package/src/state/cache-service.ts +592 -0
- package/src/state/capabilities.ts +65 -0
- package/src/state/checkpoint.ts +46 -0
- package/src/state/continuation-persistence.ts +126 -0
- package/src/state/decode-step-inputs.ts +31 -0
- package/src/state/decoded-kv-donor.ts +12 -0
- package/src/state/delayed-quantized-kv.ts +141 -0
- package/src/state/delayed-rotating-quantized-kv.ts +178 -0
- package/src/state/delayed-turboquant-kv.ts +123 -0
- package/src/state/durability.ts +214 -0
- package/src/state/encoder-cache.ts +26 -0
- package/src/state/full-kv-row-append.ts +33 -0
- package/src/state/full-kv-row-donor.ts +54 -0
- package/src/state/full-prefill-padding.ts +56 -0
- package/src/state/full-prefill-row.ts +112 -0
- package/src/state/full-transitioning-kv-rows.ts +70 -0
- package/src/state/glm52-cache.ts +783 -0
- package/src/state/grouped-heads-kv4-cache.ts +26 -0
- package/src/state/index.ts +38 -0
- package/src/state/kv-attention-view.ts +77 -0
- package/src/state/kv-budget.ts +14 -0
- package/src/state/kv-scheme.ts +315 -0
- package/src/state/kv-storage.ts +57 -0
- package/src/state/kv-tensor-rows.ts +212 -0
- package/src/state/kv.ts +324 -0
- package/src/state/kv4-head256-cache.ts +46 -0
- package/src/state/latent-legos.ts +895 -0
- package/src/state/layout.ts +277 -0
- package/src/state/leases.ts +26 -0
- package/src/state/paged/cache.ts +689 -0
- package/src/state/paged/codec.ts +36 -0
- package/src/state/paged/rows.ts +223 -0
- package/src/state/persistence-types.ts +173 -0
- package/src/state/persistence-worker.ts +83 -0
- package/src/state/persistence.ts +1036 -0
- package/src/state/persistence.worker.d.ts +3 -0
- package/src/state/persistence.worker.js +264 -0
- package/src/state/prefix-cache.ts +685 -0
- package/src/state/quantized-kv.ts +369 -0
- package/src/state/quantized-tensor.ts +26 -0
- package/src/state/request-policy.ts +48 -0
- package/src/state/retention-policy.ts +37 -0
- package/src/state/rollback.ts +131 -0
- package/src/state/rotating-kv-donor.ts +35 -0
- package/src/state/rotating-kv-layout.ts +341 -0
- package/src/state/rotating-kv.ts +529 -0
- package/src/state/rotating-quantized-kv.ts +616 -0
- package/src/state/rotating-row-transaction.ts +35 -0
- package/src/state/rows.ts +104 -0
- package/src/state/softcap-attention.ts +118 -0
- package/src/state/speculative/glm52-mtp-state.ts +38 -0
- package/src/state/speculative/qwen-mtp-state.ts +34 -0
- package/src/state/speculative-rotating-kv.ts +113 -0
- package/src/state/ssd-cache.ts +434 -0
- package/src/state/ssm-prefill-padding.ts +39 -0
- package/src/state/ssm.ts +422 -0
- package/src/state/target-layout.ts +7 -0
- package/src/state/tiered-prefix-cache.ts +45 -0
- package/src/state/training-cache.ts +300 -0
- package/src/state/transitioning-kv-rows.ts +267 -0
- package/src/state/turboquant-attention.ts +97 -0
- package/src/state/turboquant-codec.ts +90 -0
- package/src/state/turboquant-kv.ts +442 -0
- package/src/state/views.ts +26 -0
- package/src/transcription/index.ts +3 -0
- package/src/transcription/silero-vad.ts +379 -0
- package/src/transcription/whisper/decode.ts +773 -0
- package/src/transcription/whisper/format.ts +74 -0
- package/src/transcription/whisper/index.ts +4 -0
- package/src/transcription/whisper/timing.ts +274 -0
- package/src/transcription/whisper/transcribe.ts +394 -0
- package/src/transcription/whisper/types.ts +22 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
// Grammar-constrained decoding via @mlc-ai/web-xgrammar (WASM).
|
|
2
|
+
//
|
|
3
|
+
// Serves the structured-output API surface: response_format (json_object /
|
|
4
|
+
// json_schema), guided_grammar (EBNF/LARK), guided_regex, guided_choice,
|
|
5
|
+
// and the structured_outputs alias — full parity with oMLX/vLLM. From our
|
|
6
|
+
// side it's all one shape: compile something → GrammarMatcher → per-step
|
|
7
|
+
// bitmask → mask invalid logits to -inf before the sampler. The only
|
|
8
|
+
// difference between the modes is which xgrammar compile method we call.
|
|
9
|
+
//
|
|
10
|
+
// Fidelity tier: L2-class (oMLX is the oracle — same model+prompt+schema+seed
|
|
11
|
+
// must produce the same token stream). mlx-lm has no grammar support, so this
|
|
12
|
+
// is purely an oMLX-parity feature, not an L1 contract. The bitmask does not
|
|
13
|
+
// change the numerics of VALID tokens (it only sets invalid ones to -inf), so
|
|
14
|
+
// it touches neither L1 nor L2 decode numerics.
|
|
15
|
+
//
|
|
16
|
+
// Integration: a GrammarController is a stateful logits masker. Because
|
|
17
|
+
// xgrammar's getNextTokenBitmask() is async (WASM) and acceptToken() needs
|
|
18
|
+
// the token id as a JS number, the controller does NOT ride the sync
|
|
19
|
+
// LogitsProcessor array (which operates on device-side token arrays with no
|
|
20
|
+
// readback). Instead generate() drives it explicitly: after each token is
|
|
21
|
+
// sampled, call accept(tokenId) (sync matcher advance + fires async mask
|
|
22
|
+
// precompute that overlaps the next GPU forward), then await ready() before
|
|
23
|
+
// the next sampleStep; applyMask() applies the ready bitmask synchronously.
|
|
24
|
+
// See the grammar branch in generate()'s decode loop.
|
|
25
|
+
|
|
26
|
+
import xgrammar, {
|
|
27
|
+
type TokenizerInfo as XTokenizerInfo,
|
|
28
|
+
type GrammarCompiler as XGrammarCompiler,
|
|
29
|
+
type CompiledGrammar as XCompiledGrammar,
|
|
30
|
+
type GrammarMatcher as XGrammarMatcher,
|
|
31
|
+
type Grammar as XGrammar,
|
|
32
|
+
} from "@mlc-ai/web-xgrammar";
|
|
33
|
+
import { readFileSync } from "node:fs";
|
|
34
|
+
import { MlxArray } from "@mlx-bun/mlx/array";
|
|
35
|
+
import * as ops from "@mlx-bun/mlx/ops";
|
|
36
|
+
import { applyTokenBitmask } from "../kernels/sampling/token-bitmask";
|
|
37
|
+
import { runtimeValue } from "../runtime/config";
|
|
38
|
+
import type { LoadedTokenizer } from "../input/tokenizer";
|
|
39
|
+
|
|
40
|
+
/** The request shape — the union of every structured-output field name across
|
|
41
|
+
* the OpenAI/oMLX/vLLM API surfaces. BOTH spellings are accepted: the wire
|
|
42
|
+
* protocol is snake_case (the server passes the raw request body — this was
|
|
43
|
+
* a live bug until 2026-07-03: only camelCase was read, so response_format
|
|
44
|
+
* over HTTP silently no-op'd; caught by the feature-matrix benchmark's
|
|
45
|
+
* conformance gate); library callers use camelCase. */
|
|
46
|
+
export interface GrammarRequest {
|
|
47
|
+
/** OpenAI response_format. {type:"json_object"} | {type:"json_schema",
|
|
48
|
+
* json_schema:{name,schema,strict?}} | {type:"text"} (text = no-op). */
|
|
49
|
+
responseFormat?: unknown;
|
|
50
|
+
response_format?: unknown;
|
|
51
|
+
/** vLLM/oMLX: raw EBNF/LARK grammar string. */
|
|
52
|
+
guidedGrammar?: string;
|
|
53
|
+
guided_grammar?: string;
|
|
54
|
+
/** vLLM/oMLX: regex. Compiled via `root ::= <regex>` EBNF embedding, so
|
|
55
|
+
* only the regex∩EBNF subset works (the F5 gap — real regex→EBNF or
|
|
56
|
+
* structural-tag RegexFormat is the tracked fix, `02d723a:docs/reference/server-api.md`). */
|
|
57
|
+
guidedRegex?: string;
|
|
58
|
+
guided_regex?: string;
|
|
59
|
+
/** vLLM/oMLX: restrict output to one of these strings (enum). */
|
|
60
|
+
guidedChoice?: string[];
|
|
61
|
+
guided_choice?: string[];
|
|
62
|
+
/** vLLM/oMLX: a JSON schema object directly (alias for response_format's
|
|
63
|
+
* json_schema.schema). */
|
|
64
|
+
structuredOutputs?: unknown;
|
|
65
|
+
structured_outputs?: unknown;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A compiled grammar ready to mask logits. Null when the request asks for no
|
|
69
|
+
* constraint (text, or none of the fields set). `warning` is set when the
|
|
70
|
+
* requested grammar could not be compiled and the caller should degrade to
|
|
71
|
+
* prompt injection (mirrors oMLX's _compile_grammar_for_request returning None
|
|
72
|
+
* + a Warning header). */
|
|
73
|
+
export interface CompiledGrammarResult {
|
|
74
|
+
controller: GrammarController;
|
|
75
|
+
/** Schema/grammar for the degrade-path system-prompt injection (oMLX parity). */
|
|
76
|
+
degradeHint: string | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** xgrammar TokenizerInfo is expensive to build (it decodes every vocab id).
|
|
80
|
+
* Cache one per tokenizer.json path — it's vocab-structural, not per-request. */
|
|
81
|
+
/** Module-level serializer for ALL xgrammar WASM calls. The WASM instance is
|
|
82
|
+
* single-threaded; CONCURRENT async calls corrupt emscripten's binding
|
|
83
|
+
* layer (BindingError: Expected null or instance of VectorInt, got an
|
|
84
|
+
* instance of VectorInt — reproduced in `02d723a:tests/parity/grammar.test.ts` B1 test, which
|
|
85
|
+
* fires 4 overlapping compiles + fills). Under `--batch N` the scheduler
|
|
86
|
+
* fires N accept()s (each kicks an async fill) then awaits ready() on all;
|
|
87
|
+
* and the server compiles grammar for concurrent requests. Without
|
|
88
|
+
* serialization the overlapping WASM calls crash. This queue keeps the
|
|
89
|
+
* async-overlap API (fills still overlap GPU compute — the queue only
|
|
90
|
+
* serializes the CPU-side WASM calls, each 0.004–0.19 ms) while ensuring
|
|
91
|
+
* only ONE xgrammar call is in flight at a time. Both the compile path
|
|
92
|
+
* (compileGrammarRequest) and the fill path (prime/accept) ride it. */
|
|
93
|
+
let wasmChain: Promise<unknown> = Promise.resolve();
|
|
94
|
+
function wasmQueue<T>(fn: () => Promise<T>): Promise<T> {
|
|
95
|
+
const run = wasmChain.then(fn, fn); // run regardless of prior success/failure
|
|
96
|
+
wasmChain = run.then(
|
|
97
|
+
() => undefined,
|
|
98
|
+
() => undefined,
|
|
99
|
+
);
|
|
100
|
+
return run;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** F4: ONE TokenizerInfo + ONE compiler per tokenizer.json path, cached for
|
|
104
|
+
* the process lifetime. xgrammar's GrammarCompiler carries an internal
|
|
105
|
+
* compilation cache (cacheEnabled defaults true), so agentic clients
|
|
106
|
+
* replaying the same JSON schema hit a cached grammar instead of paying a
|
|
107
|
+
* full schema→grammar compile per request. Both are vocab-structural.
|
|
108
|
+
* Controllers get ownsCompiler=false and never dispose it.
|
|
109
|
+
* The entry holds a PROMISE, installed synchronously before any await —
|
|
110
|
+
* concurrent compiles after a path change share ONE build (single-flight)
|
|
111
|
+
* instead of racing to rebuild and double-dispose the previous pair. */
|
|
112
|
+
let cachedTokenizerInfo: {
|
|
113
|
+
path: string;
|
|
114
|
+
pair: Promise<{ info: XTokenizerInfo; compiler: XGrammarCompiler }>;
|
|
115
|
+
} | null = null;
|
|
116
|
+
|
|
117
|
+
/** Extract the ordered vocab array + detect xgrammar vocab type from
|
|
118
|
+
* tokenizer.json (the same data packages/inference/src/input/tokenizer.ts already loads).
|
|
119
|
+
* - decoder.type == "ByteLevel" → byte_level (Llama3, Qwen)
|
|
120
|
+
* - decoder is Sequence w/ ByteFallback → byte_fallback (Gemma/SP)
|
|
121
|
+
* - otherwise → raw
|
|
122
|
+
* prependSpace is true for byte_level (ByteLevel pretokenizers add a leading
|
|
123
|
+
* space). Validated in xgrammar-spike.ts (deleted 2026-08-23; git history) across Gemma
|
|
124
|
+
* (byte_fallback, 256k), Llama-3 / Qwen2.5 (byte_level, 128k/151k). */
|
|
125
|
+
function loadVocab(tokenizerJsonPath: string): {
|
|
126
|
+
vocab: string[];
|
|
127
|
+
vocabType: string;
|
|
128
|
+
prependSpace: boolean;
|
|
129
|
+
} {
|
|
130
|
+
const t = JSON.parse(readFileSync(tokenizerJsonPath, "utf8"));
|
|
131
|
+
const v = t.model?.vocab;
|
|
132
|
+
if (!v || typeof v !== "object")
|
|
133
|
+
throw new Error("tokenizer.json has no model.vocab");
|
|
134
|
+
const entries = Object.entries(v as Record<string, number>);
|
|
135
|
+
const size = entries.length;
|
|
136
|
+
const vocab = new Array<string>(size);
|
|
137
|
+
for (const [tok, id] of entries) vocab[id] = tok;
|
|
138
|
+
|
|
139
|
+
let vocabType = "raw";
|
|
140
|
+
const dec = t.decoder;
|
|
141
|
+
if (dec?.type === "ByteLevel") vocabType = "byte_level";
|
|
142
|
+
else if (dec?.type === "Sequence" && Array.isArray(dec.decoders)) {
|
|
143
|
+
if (dec.decoders.some((d: { type?: string }) => d?.type === "ByteFallback"))
|
|
144
|
+
vocabType = "byte_fallback";
|
|
145
|
+
} else if (dec?.type === "ByteFallback") {
|
|
146
|
+
vocabType = "byte_fallback";
|
|
147
|
+
}
|
|
148
|
+
return { vocab, vocabType, prependSpace: vocabType === "byte_level" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Get (cached) xgrammar TokenizerInfo + its process-lifetime compiler for a
|
|
152
|
+
* tokenizer.json path. On a path change (model switch) the previous pair is
|
|
153
|
+
* disposed exactly once, by the single flight that replaced it. */
|
|
154
|
+
function getTokenizerInfo(
|
|
155
|
+
tokenizerJsonPath: string,
|
|
156
|
+
): Promise<{ info: XTokenizerInfo; compiler: XGrammarCompiler }> {
|
|
157
|
+
if (cachedTokenizerInfo?.path === tokenizerJsonPath) return cachedTokenizerInfo.pair;
|
|
158
|
+
const prev = cachedTokenizerInfo;
|
|
159
|
+
const pair = (async () => {
|
|
160
|
+
const { vocab, vocabType, prependSpace } = loadVocab(tokenizerJsonPath);
|
|
161
|
+
const info = await wasmQueue(() =>
|
|
162
|
+
xgrammar.TokenizerInfo.createTokenizerInfo(vocab, vocabType, prependSpace, vocab.length),
|
|
163
|
+
);
|
|
164
|
+
const compiler = await wasmQueue(() => xgrammar.GrammarCompiler.createGrammarCompiler(info));
|
|
165
|
+
if (prev)
|
|
166
|
+
prev.pair.then(
|
|
167
|
+
(p) => {
|
|
168
|
+
p.compiler.dispose();
|
|
169
|
+
p.info.dispose();
|
|
170
|
+
},
|
|
171
|
+
() => undefined,
|
|
172
|
+
);
|
|
173
|
+
return { info, compiler };
|
|
174
|
+
})();
|
|
175
|
+
// Install synchronously — concurrent callers join this flight. A failed
|
|
176
|
+
// flight uncaches itself (callers still see the rejection) so a transient
|
|
177
|
+
// error doesn't poison the path forever.
|
|
178
|
+
cachedTokenizerInfo = { path: tokenizerJsonPath, pair };
|
|
179
|
+
pair.catch(() => {
|
|
180
|
+
if (cachedTokenizerInfo?.pair === pair) cachedTokenizerInfo = null;
|
|
181
|
+
});
|
|
182
|
+
return pair;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The model's logit width: config.vocab_size, which may exceed the ids the
|
|
186
|
+
* matcher knows (embedding padding, added tokens outside model.vocab).
|
|
187
|
+
* Falls back to the tokenizer length. */
|
|
188
|
+
function logitWidth(tokenizer: LoadedTokenizer, configVocabSize?: number): number {
|
|
189
|
+
return configVocabSize ?? tokenizer.vocabSize ?? 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Keep XGrammar's verdict for every id its matcher knows and reject every
|
|
193
|
+
* other id. The matcher's bitmask has ceil(known / 32) words, and XGrammar can
|
|
194
|
+
* set the unused high bits of the final word when `known` is not a multiple
|
|
195
|
+
* of 32: a 4-id vocabulary with guided_choice yes/no allows ids 4-31, and
|
|
196
|
+
* MiniCPM5's 130072 model.vocab ids allow added tokens 130072-130079. Ids
|
|
197
|
+
* past the matcher also include config padding and added tokens it was not
|
|
198
|
+
* built with. The result spans the logit width with those bits clear; both
|
|
199
|
+
* mask paths also reject ids past the words they are given. */
|
|
200
|
+
function grammarOwnedBits(bits: Int32Array, known: number, logits: number): Int32Array {
|
|
201
|
+
const owned = new Int32Array(Math.ceil(logits / 32));
|
|
202
|
+
const whole = known >>> 5, tail = known & 31;
|
|
203
|
+
owned.set(bits.subarray(0, Math.min(whole, owned.length)));
|
|
204
|
+
if (tail !== 0 && whole < owned.length) owned[whole] = (bits[whole] ?? 0) & ~(-1 << tail);
|
|
205
|
+
return owned;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Stateful grammar masker. Driven explicitly by generate()'s decode loop
|
|
209
|
+
* (see module doc): accept(tokenId) advances the matcher + fires async mask
|
|
210
|
+
* precompute; ready() awaits it; applyMask(logits) sets invalid ids to -inf. */
|
|
211
|
+
export class GrammarController {
|
|
212
|
+
private readonly matcher: XGrammarMatcher;
|
|
213
|
+
private readonly compiled: XCompiledGrammar;
|
|
214
|
+
private readonly compiler: XGrammarCompiler;
|
|
215
|
+
private readonly ownsCompiler: boolean;
|
|
216
|
+
/** `matcher`: ids XGrammar's TokenizerInfo was built with, the only ids the
|
|
217
|
+
* grammar decides. `logits`: the model's logit width. */
|
|
218
|
+
private readonly vocab: { readonly matcher: number; readonly logits: number };
|
|
219
|
+
/** Raw-encode hook for jump-forward (encode WITHOUT special tokens); null
|
|
220
|
+
* disables jumpForward(). */
|
|
221
|
+
private readonly encodeRaw: ((text: string) => number[]) | null;
|
|
222
|
+
/** Grammar-owned bits (grammarOwnedBits), read by both mask paths. */
|
|
223
|
+
private readyMask: Int32Array;
|
|
224
|
+
private pending: Promise<void> | null;
|
|
225
|
+
private terminated = false;
|
|
226
|
+
private disposed = false;
|
|
227
|
+
private readonly metalMask = runtimeValue("MLX_BUN_TOKEN_MASK") === "metal";
|
|
228
|
+
/** Tokens emitted via jumpForward() this generation — telemetry/test hook. */
|
|
229
|
+
jumpedTokens = 0;
|
|
230
|
+
|
|
231
|
+
constructor(
|
|
232
|
+
matcher: XGrammarMatcher,
|
|
233
|
+
compiled: XCompiledGrammar,
|
|
234
|
+
compiler: XGrammarCompiler,
|
|
235
|
+
ownsCompiler: boolean,
|
|
236
|
+
vocab: { readonly matcher: number; readonly logits: number },
|
|
237
|
+
encodeRaw?: (text: string) => number[],
|
|
238
|
+
) {
|
|
239
|
+
this.matcher = matcher;
|
|
240
|
+
this.compiled = compiled;
|
|
241
|
+
this.compiler = compiler;
|
|
242
|
+
this.ownsCompiler = ownsCompiler;
|
|
243
|
+
this.vocab = vocab;
|
|
244
|
+
this.encodeRaw = encodeRaw ?? null;
|
|
245
|
+
const all = new Int32Array(Math.ceil(vocab.matcher / 32)).fill(-1);
|
|
246
|
+
this.readyMask = grammarOwnedBits(all, vocab.matcher, vocab.logits);
|
|
247
|
+
this.pending = null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Normalize each XGrammar mask once, before either mask path reads it. */
|
|
251
|
+
private setReadyMask(bits: Int32Array): void {
|
|
252
|
+
this.readyMask = grammarOwnedBits(bits, this.vocab.matcher, this.vocab.logits);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Precompute the step-0 mask. Must be awaited before the first applyMask(). */
|
|
256
|
+
async prime(): Promise<void> {
|
|
257
|
+
this.setReadyMask(await wasmQueue(() => this.matcher.getNextTokenBitmask()));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Apply the ready bitmask to logits [1, V]. Invalid token ids → -inf.
|
|
261
|
+
* Sync — the mask is already materialized. Returns a NEW array (caller
|
|
262
|
+
* disposes the input). Mirrors oMLX's apply_token_bitmask_mlx. Both paths
|
|
263
|
+
* read the grammar-owned readyMask, so ids the matcher does not know are
|
|
264
|
+
* never valid. */
|
|
265
|
+
applyMask(logits: MlxArray): MlxArray {
|
|
266
|
+
if (this.metalMask) return applyTokenBitmask(logits, this.readyMask);
|
|
267
|
+
// Build a -inf/0 additive float mask on device from the int32 bitmask.
|
|
268
|
+
// Each int32 bit b (little-endian within the word) gates token
|
|
269
|
+
// word*32 + b. Set = valid (xgrammar convention). Adding -inf → -inf,
|
|
270
|
+
// adding 0 → noop.
|
|
271
|
+
const V = logits.shape[logits.shape.length - 1]!;
|
|
272
|
+
const maskArr = new Float32Array(V);
|
|
273
|
+
for (let id = 0; id < V; id++) {
|
|
274
|
+
const word = id >>> 5;
|
|
275
|
+
const bit = id & 31;
|
|
276
|
+
// F3: readyMask already clears ids the matcher does not know, up to
|
|
277
|
+
// the configured logit width. Logits wider than that are never valid
|
|
278
|
+
// either; mask them explicitly rather than relying on undefined>>>bit
|
|
279
|
+
// coercing to 0.
|
|
280
|
+
const w = word < this.readyMask.length ? this.readyMask[word]! : 0;
|
|
281
|
+
const valid = (w >>> bit) & 1;
|
|
282
|
+
maskArr[id] = valid ? 0 : -Infinity;
|
|
283
|
+
}
|
|
284
|
+
const raw = MlxArray.fromFloat32(maskArr, [1, V]);
|
|
285
|
+
const mask = raw.astype(logits.dtype);
|
|
286
|
+
raw.dispose();
|
|
287
|
+
const out = ops.add(logits, mask);
|
|
288
|
+
mask.dispose();
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Advance the matcher by the just-sampled token and fire async mask
|
|
293
|
+
* precompute for the NEXT step (overlaps the GPU forward). Call AFTER the
|
|
294
|
+
* token is read back to JS. No-op once terminated. */
|
|
295
|
+
accept(tokenId: number): void {
|
|
296
|
+
if (this.terminated) return;
|
|
297
|
+
const ok = this.matcher.acceptToken(tokenId);
|
|
298
|
+
if (!ok) {
|
|
299
|
+
// The sampler only ever picks valid tokens (we masked the rest), so an
|
|
300
|
+
// accept failure means the vocab/mask drifted from the model's actual
|
|
301
|
+
// tokenization — not a logic bug in the walk. Log + keep going; the
|
|
302
|
+
// output may be malformed but generation continues rather than dying.
|
|
303
|
+
console.warn(`grammar: matcher rejected token ${tokenId}`);
|
|
304
|
+
}
|
|
305
|
+
if (this.matcher.isTerminated()) {
|
|
306
|
+
this.terminated = true;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
this.pending = this.fireFill();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Queue the async bitmask fill. The closure re-checks `disposed` at RUN
|
|
313
|
+
* time: a controller disposed while its fill is still queued (an exception
|
|
314
|
+
* between accept/jumpForward and ready()) must not call into the deleted
|
|
315
|
+
* WASM matcher — that BindingError poisons the module-wide wasmChain for
|
|
316
|
+
* every later grammar request (found by `02d723a:tests/parity/grammar-jump.test.ts`). */
|
|
317
|
+
private fireFill(): Promise<void> {
|
|
318
|
+
return wasmQueue(() =>
|
|
319
|
+
this.disposed
|
|
320
|
+
? Promise.resolve(null)
|
|
321
|
+
: this.matcher.getNextTokenBitmask(),
|
|
322
|
+
).then((m) => {
|
|
323
|
+
if (m) this.setReadyMask(m);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Await the mask precompute fired by accept(). Should be called before the
|
|
328
|
+
* next applyMask(); usually resolves immediately (overlapped the forward). */
|
|
329
|
+
async ready(): Promise<void> {
|
|
330
|
+
if (this.pending) {
|
|
331
|
+
await this.pending;
|
|
332
|
+
this.pending = null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
get isTerminated(): boolean {
|
|
337
|
+
return this.terminated;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Propose the forced string without advancing the matcher. Tokenization
|
|
341
|
+
* can differ at its first boundary; verification and the normal grammar
|
|
342
|
+
* sampler accept or reject the candidates before anything is committed. */
|
|
343
|
+
async proposeTokens(maxTokens: number): Promise<number[]> {
|
|
344
|
+
await this.ready();
|
|
345
|
+
if (this.terminated || !this.encodeRaw) return [];
|
|
346
|
+
return wasmQueue(async () => {
|
|
347
|
+
if (this.disposed) return [];
|
|
348
|
+
const text = this.matcher.findJumpForwardString();
|
|
349
|
+
return text ? this.encodeRaw!(text).slice(0, maxTokens) : [];
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Jump-forward decoding (SGLang's technique via xgrammar's
|
|
354
|
+
* findJumpForwardString; opt-in — the composition's grammarJump): when the grammar
|
|
355
|
+
* admits exactly one continuation string (JSON keys/punctuation, enum
|
|
356
|
+
* bodies — longest with `any_whitespace:false` compact schemas), return its
|
|
357
|
+
* token ids so the decode loop can carry them into the KV with ONE
|
|
358
|
+
* multi-token forward instead of one masked forward per token.
|
|
359
|
+
*
|
|
360
|
+
* Contract: MUTATES the matcher for every id it returns (and only those) —
|
|
361
|
+
* the caller MUST emit exactly the returned ids, then await ready() before
|
|
362
|
+
* the next applyMask. Call only after ready() has resolved (no WASM fill in
|
|
363
|
+
* flight — the matcher calls here are sync). Returns null (matcher
|
|
364
|
+
* untouched) when: no hook, terminated, the jump retokenizes to <2 ids
|
|
365
|
+
* (a 1-token jump saves no forward), maxIds < 2, or the first id is
|
|
366
|
+
* rejected. If a LATER id is rejected the accepted prefix is returned —
|
|
367
|
+
* those ids already advanced the matcher, so they must be emitted; the
|
|
368
|
+
* loop resumes masked decoding after them (no rollback needed by
|
|
369
|
+
* construction).
|
|
370
|
+
*
|
|
371
|
+
* Fidelity note (why opt-in, no oracle): encode(jumpStr) standalone can
|
|
372
|
+
* tokenize the forced span differently than the model would have sampled
|
|
373
|
+
* it. The emitted STRING is identical (token strings concatenate to the
|
|
374
|
+
* jump string) and grammar validity is preserved (every id is
|
|
375
|
+
* matcher-accepted), but the token STREAM — and, conditioning on it,
|
|
376
|
+
* content generated AFTER the forced span — may differ from an unjumped
|
|
377
|
+
* run. SentencePiece-family tokenizers whose raw encode prepends a space
|
|
378
|
+
* simply never jump (the first id gets rejected) — the guard degrades to
|
|
379
|
+
* normal decoding, never to invalid output. */
|
|
380
|
+
jumpForward(maxIds: number): number[] | null {
|
|
381
|
+
if (this.terminated || !this.encodeRaw || maxIds < 2) return null;
|
|
382
|
+
const str = this.matcher.findJumpForwardString();
|
|
383
|
+
if (!str || str.length < 2) return null;
|
|
384
|
+
const ids = this.encodeRaw(str);
|
|
385
|
+
if (ids.length < 2) return null;
|
|
386
|
+
const take = Math.min(ids.length, maxIds);
|
|
387
|
+
if (take < 2) return null;
|
|
388
|
+
const accepted: number[] = [];
|
|
389
|
+
for (let i = 0; i < take; i++) {
|
|
390
|
+
const id = ids[i]!;
|
|
391
|
+
if (!this.matcher.acceptToken(id)) break; // keep the accepted prefix
|
|
392
|
+
accepted.push(id);
|
|
393
|
+
if (this.matcher.isTerminated()) {
|
|
394
|
+
this.terminated = true;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (accepted.length === 0) return null; // matcher untouched
|
|
399
|
+
this.jumpedTokens += accepted.length;
|
|
400
|
+
if (!this.terminated) this.pending = this.fireFill();
|
|
401
|
+
return accepted;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
dispose(): void {
|
|
405
|
+
// Idempotent: generate()/the gateway dispose in finally, and the server's
|
|
406
|
+
// early-reject paths dispose pre-run — a throw inside run can reach both.
|
|
407
|
+
if (this.disposed) return;
|
|
408
|
+
this.disposed = true; // queued fills become no-ops (see fireFill)
|
|
409
|
+
this.matcher.dispose();
|
|
410
|
+
this.compiled.dispose();
|
|
411
|
+
if (this.ownsCompiler) this.compiler.dispose();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Normalize the request fields + compile. Returns null when no constraint is
|
|
416
|
+
* requested (text / none set). On compile failure returns
|
|
417
|
+
* `{ controller: null, degradeHint }` — callers inject a "respond in valid
|
|
418
|
+
* JSON" system prompt + emit a Warning header (oMLX parity, never 500). */
|
|
419
|
+
export async function compileGrammarRequest(
|
|
420
|
+
req: GrammarRequest,
|
|
421
|
+
tokenizer: LoadedTokenizer,
|
|
422
|
+
configVocabSize?: number,
|
|
423
|
+
): Promise<{ controller: GrammarController | null; degradeHint: string | null } | null> {
|
|
424
|
+
const tokenizerJsonPath = tokenizer.tokenizerJsonPath;
|
|
425
|
+
if (!tokenizerJsonPath)
|
|
426
|
+
throw new Error("grammar: tokenizer.tokenizerJsonPath is required (set by loadTokenizer)");
|
|
427
|
+
|
|
428
|
+
const resolved = resolveGrammarRequest(req);
|
|
429
|
+
if (!resolved) return null;
|
|
430
|
+
|
|
431
|
+
// Each xgrammar WASM call rides wasmQueue individually (NOT a body-level
|
|
432
|
+
// wrap — prime() calls wasmQueue internally, so a body-level wrap would
|
|
433
|
+
// deadlock reentrantly). Individual calls serialize without nesting.
|
|
434
|
+
const { info, compiler } = await getTokenizerInfo(tokenizerJsonPath);
|
|
435
|
+
let compiled: XCompiledGrammar | null = null;
|
|
436
|
+
let degradeHint: string | null = null;
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
switch (resolved.kind) {
|
|
440
|
+
case "json_object":
|
|
441
|
+
compiled = await wasmQueue(() => compiler.compileBuiltinJSONGrammar());
|
|
442
|
+
break;
|
|
443
|
+
case "json_schema": {
|
|
444
|
+
const s = resolved.schema;
|
|
445
|
+
compiled = await wasmQueue(() =>
|
|
446
|
+
compiler.compileJSONSchema(
|
|
447
|
+
typeof s === "string" ? s : JSON.stringify(s),
|
|
448
|
+
resolved.anyWhitespace ?? true,
|
|
449
|
+
resolved.indent,
|
|
450
|
+
resolved.separators,
|
|
451
|
+
resolved.strict ?? true,
|
|
452
|
+
),
|
|
453
|
+
);
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
case "grammar": {
|
|
457
|
+
const g = await wasmQueue(() => xgrammar.Grammar.fromEBNF(resolved.ebnf, resolved.rootRule));
|
|
458
|
+
compiled = await wasmQueue(() => compiler.compileGrammar(g));
|
|
459
|
+
g.dispose();
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
case "regex": {
|
|
463
|
+
const ebnf = `root ::= ${resolved.regex}`;
|
|
464
|
+
const g = await wasmQueue(() => xgrammar.Grammar.fromEBNF(ebnf));
|
|
465
|
+
compiled = await wasmQueue(() => compiler.compileGrammar(g));
|
|
466
|
+
g.dispose();
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
case "choice": {
|
|
470
|
+
// F6: escape everything an EBNF string literal can't hold bare.
|
|
471
|
+
// \n\r\t have named escapes; any other control char has no EBNF
|
|
472
|
+
// spelling — throw into the degrade path (prompt injection +
|
|
473
|
+
// Warning header), never a 500.
|
|
474
|
+
const esc = (c: string) => {
|
|
475
|
+
const body = c
|
|
476
|
+
.replace(/\\/g, "\\\\")
|
|
477
|
+
.replace(/"/g, '\\"')
|
|
478
|
+
.replace(/\n/g, "\\n")
|
|
479
|
+
.replace(/\r/g, "\\r")
|
|
480
|
+
.replace(/\t/g, "\\t");
|
|
481
|
+
// eslint-disable-next-line no-control-regex
|
|
482
|
+
if (/[\x00-\x1f]/.test(body))
|
|
483
|
+
throw new Error("guided_choice: unsupported control character in choice");
|
|
484
|
+
return `"${body}"`;
|
|
485
|
+
};
|
|
486
|
+
const opts = resolved.choices.map(esc).join(" | ");
|
|
487
|
+
const g = await wasmQueue(() => xgrammar.Grammar.fromEBNF(`root ::= ${opts}`));
|
|
488
|
+
compiled = await wasmQueue(() => compiler.compileGrammar(g));
|
|
489
|
+
g.dispose();
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
} catch (e) {
|
|
494
|
+
degradeHint = resolved.degradeDescription ?? String(e);
|
|
495
|
+
compiled = null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!compiled) {
|
|
499
|
+
if (!degradeHint) degradeHint = resolved.degradeDescription ?? "grammar compile failed";
|
|
500
|
+
// The compiler is the cached per-TokenizerInfo instance (F4) — a failed
|
|
501
|
+
// compile must NOT dispose it; the WASM abort is catchable and the
|
|
502
|
+
// compiler state survives (verified in `02d723a:tests/parity/grammar.test.ts`).
|
|
503
|
+
// Return the hint (NOT bare null): it drives the degrade path's
|
|
504
|
+
// system-prompt injection + Warning header. Dropping it here served
|
|
505
|
+
// unconstrained output with no Warning at all (found 2026-07-07 sweep).
|
|
506
|
+
return { controller: null, degradeHint };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// terminateWithoutStopToken=true: the matcher terminates as soon as the
|
|
510
|
+
// grammar is SATISFIED (e.g. the closing `}` of a complete JSON), not only
|
|
511
|
+
// when the model emits a stop token. This is what oMLX does — generation
|
|
512
|
+
// halts once the structured output is complete, so the sampler never faces
|
|
513
|
+
// an all-rejected (all--inf) mask after the grammar closes (which would
|
|
514
|
+
// make greedy argmax return a garbage token id like 0 that the matcher
|
|
515
|
+
// then rejects, looping until max_tokens).
|
|
516
|
+
const matcher = await wasmQueue(() =>
|
|
517
|
+
xgrammar.GrammarMatcher.createGrammarMatcher(compiled!, undefined, true),
|
|
518
|
+
);
|
|
519
|
+
// The matcher decides only the ids its TokenizerInfo was built with
|
|
520
|
+
// (model.vocab); the logits can be wider.
|
|
521
|
+
const known = info.getVocabSize();
|
|
522
|
+
const controller = new GrammarController(
|
|
523
|
+
matcher, compiled, compiler, false,
|
|
524
|
+
{ matcher: known, logits: logitWidth(tokenizer, configVocabSize) || known },
|
|
525
|
+
// Raw encode (no BOS/specials) for jump-forward retokenization.
|
|
526
|
+
(s) => tokenizer.encode(s, false),
|
|
527
|
+
);
|
|
528
|
+
await controller.prime();
|
|
529
|
+
return { controller, degradeHint };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Internal: normalize the union of request fields into one compile target. */
|
|
533
|
+
function resolveGrammarRequest(req: GrammarRequest):
|
|
534
|
+
| (
|
|
535
|
+
| { kind: "json_object"; degradeDescription: string }
|
|
536
|
+
| {
|
|
537
|
+
kind: "json_schema";
|
|
538
|
+
schema: unknown;
|
|
539
|
+
strict?: boolean;
|
|
540
|
+
anyWhitespace?: boolean;
|
|
541
|
+
indent?: number;
|
|
542
|
+
separators?: [string, string];
|
|
543
|
+
degradeDescription: string;
|
|
544
|
+
}
|
|
545
|
+
| { kind: "grammar"; ebnf: string; rootRule?: string; degradeDescription: string }
|
|
546
|
+
| { kind: "regex"; regex: string; degradeDescription: string }
|
|
547
|
+
| { kind: "choice"; choices: string[]; degradeDescription: string }
|
|
548
|
+
)
|
|
549
|
+
| null {
|
|
550
|
+
// Normalize the two spellings first (wire = snake_case, library = camel).
|
|
551
|
+
const guidedGrammar = req.guidedGrammar ?? req.guided_grammar;
|
|
552
|
+
const guidedRegex = req.guidedRegex ?? req.guided_regex;
|
|
553
|
+
const guidedChoice = req.guidedChoice ?? req.guided_choice;
|
|
554
|
+
const structuredOutputs = req.structuredOutputs ?? req.structured_outputs;
|
|
555
|
+
const responseFormat = req.responseFormat ?? req.response_format;
|
|
556
|
+
|
|
557
|
+
// Precedence mirrors oMLX's _effective_guided_grammar / _compile_grammar:
|
|
558
|
+
// explicit grammar > response_format json_schema > json_object > regex >
|
|
559
|
+
// choice > structured_outputs. text / unset → null (no constraint).
|
|
560
|
+
if (guidedGrammar) {
|
|
561
|
+
return {
|
|
562
|
+
kind: "grammar",
|
|
563
|
+
ebnf: guidedGrammar,
|
|
564
|
+
degradeDescription: "guided_grammar",
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const rf = responseFormat;
|
|
569
|
+
if (rf && typeof rf === "object") {
|
|
570
|
+
const type = (rf as { type?: string }).type;
|
|
571
|
+
if (type === "json_schema") {
|
|
572
|
+
const js = (rf as {
|
|
573
|
+
json_schema?: {
|
|
574
|
+
schema?: unknown;
|
|
575
|
+
strict?: boolean;
|
|
576
|
+
any_whitespace?: boolean;
|
|
577
|
+
anyWhitespace?: boolean;
|
|
578
|
+
};
|
|
579
|
+
}).json_schema;
|
|
580
|
+
const schema = js?.schema ?? structuredOutputs;
|
|
581
|
+
if (schema) {
|
|
582
|
+
return {
|
|
583
|
+
kind: "json_schema",
|
|
584
|
+
schema,
|
|
585
|
+
strict: js?.strict,
|
|
586
|
+
// Whitespace-stall escape hatch (`02d723a:docs/reference/server-api.md` known gaps):
|
|
587
|
+
// any_whitespace=false compiles the schema with xgrammar's compact
|
|
588
|
+
// separators — no whitespace choice points, so a whitespace-
|
|
589
|
+
// degenerate base model can't greedily tab-loop to max_tokens.
|
|
590
|
+
// Default stays true (oMLX parity: model-chosen formatting).
|
|
591
|
+
anyWhitespace: js?.any_whitespace ?? js?.anyWhitespace,
|
|
592
|
+
degradeDescription: "response_format json_schema",
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
} else if (type === "json_object") {
|
|
596
|
+
return { kind: "json_object", degradeDescription: "response_format json_object" };
|
|
597
|
+
}
|
|
598
|
+
// type === "text" → no constraint (OpenAI: text = free-form)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (structuredOutputs && typeof structuredOutputs === "object") {
|
|
602
|
+
// structured_outputs as a bare schema object (oMLX alias)
|
|
603
|
+
return {
|
|
604
|
+
kind: "json_schema",
|
|
605
|
+
schema: structuredOutputs,
|
|
606
|
+
degradeDescription: "structured_outputs",
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (guidedRegex) {
|
|
611
|
+
return { kind: "regex", regex: guidedRegex, degradeDescription: "guided_regex" };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (guidedChoice && guidedChoice.length > 0) {
|
|
615
|
+
return {
|
|
616
|
+
kind: "choice",
|
|
617
|
+
choices: guidedChoice,
|
|
618
|
+
degradeDescription: "guided_choice",
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Kill switch: MLX_BUN_GRAMMAR=0 disables grammar compilation entirely
|
|
626
|
+
* (requests degrade to prompt injection). Matches the project's envelope-gated
|
|
627
|
+
* kill-switch discipline. */
|
|
628
|
+
export function grammarEnabled(): boolean {
|
|
629
|
+
return runtimeValue("MLX_BUN_GRAMMAR") !== "0";
|
|
630
|
+
}
|