@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,1240 @@
|
|
|
1
|
+
import { pagedDecodeCompiles } from "../state/layout";
|
|
2
|
+
import { type MlxForwardWork,type MlxPreparationWork } from "../contracts/mlx/forward-work";
|
|
3
|
+
import type { DisposableResource } from "../contracts/portable/resources";
|
|
4
|
+
import type { ExecutionGroup } from "../contracts/portable/scheduling";
|
|
5
|
+
import { cleanupFailure,disposeResources,withResource } from "../runtime/resources";
|
|
6
|
+
import { KvCacheService, type RequestStateCache } from "../state/cache-service";
|
|
7
|
+
import { AdmissionRejected } from "./admission";
|
|
8
|
+
import { runMixedTokenIteration, dispatchMixedGroups } from "./mixed-iteration";
|
|
9
|
+
import { MlxPrefillCohort,type PrefillState } from "./prefill-cohort";
|
|
10
|
+
import type { MlxPrefillForward } from "./prefill-rows";
|
|
11
|
+
import { MlxPhaseDispatch } from "../generation/phase-dispatch";
|
|
12
|
+
import { driveExecutionGroup } from "./scheduler";
|
|
13
|
+
// Continuous-batching scheduler for `--batch N` serving (the engine loop).
|
|
14
|
+
// Owns ONE running batch and drives it forward one decode step at a
|
|
15
|
+
// time, admitting waiting requests and evicting finished ones between steps —
|
|
16
|
+
// iteration-level (continuous) scheduling, not static batching. See
|
|
17
|
+
// `02d723a:docs/design/batching.md`.
|
|
18
|
+
//
|
|
19
|
+
// The numerically-hard parts are verified primitives, owned by state/:
|
|
20
|
+
// - the batched FORWARD (per-row RoPE/mask) is bit-parity with mlx-lm B=N
|
|
21
|
+
// across all 4 models (`02d723a:tests/parity/batched-decode-parity.test.ts`);
|
|
22
|
+
// - the dynamic-B FULL-attention ops mergeKVRows / filterKVRows match mlx-lm
|
|
23
|
+
// BatchKVCache (same test), and the SLIDING-window
|
|
24
|
+
// BatchedRotatingCache (merge/filter/decode/make_mask incl. ring-wrap)
|
|
25
|
+
// matches mlx-lm BatchRotatingKVCache (`02d723a:tests/unit/batched-rotating.test.ts`).
|
|
26
|
+
// This module is the ORCHESTRATION on top: admission, the step loop, per-row
|
|
27
|
+
// sampling + token accounting, eviction. It never names a storage family:
|
|
28
|
+
// each layer's state joins, filters and extracts through the layer's own row
|
|
29
|
+
// layout (state/layout `ownedCacheLayout`, the BatchableCache port), which
|
|
30
|
+
// also supplies the mask and per-row RoPE positions its rows need. Those
|
|
31
|
+
// layouts are padded full-attention rows, per-row-position sliding rings (plain
|
|
32
|
+
// and quantized), recurrent state (no temporal axis, no padding), and the
|
|
33
|
+
// layouts that own their tensors (paged, delayed affine, TurboQuant, GLM).
|
|
34
|
+
//
|
|
35
|
+
// Engine mechanics (the serial decode loop's hygiene, transplanted —
|
|
36
|
+
// batching-v2-plan step 3):
|
|
37
|
+
// - PIPELINED decode: each step builds the NEXT step's graph from the
|
|
38
|
+
// still-unread sampled-token array (asyncEval), then reads the PREVIOUS
|
|
39
|
+
// step's tokens while the new step computes — mlx-lm GenerationBatch._step.
|
|
40
|
+
// The pipeline is flushed (read + emit) before a join merges, so admission
|
|
41
|
+
// never has to reconcile an in-flight token array with a new row.
|
|
42
|
+
// - clearCache every 256 steps (serial's cadence; mlx-lm batched uses 512),
|
|
43
|
+
// not every step — per-step clears trashed the buffer pool each token.
|
|
44
|
+
// - CHUNKED, INTERLEAVED admission: a joiner prefills prefillChunkSize
|
|
45
|
+
// tokens per loop iteration with one batch decode step run in between, so
|
|
46
|
+
// running rows stall at most one chunk per joiner (mlx-lm interleaves the
|
|
47
|
+
// same way), and the prefill transient stays bounded.
|
|
48
|
+
// - Failure containment: one row's onToken throwing evicts THAT row (its
|
|
49
|
+
// promise rejects); siblings keep decoding (mlx-lm `remove` semantics). A
|
|
50
|
+
// forward/sampling error still drops the whole batch (can't be attributed
|
|
51
|
+
// to a row).
|
|
52
|
+
//
|
|
53
|
+
// Bun-async, NO threads: a single detached driver loop owns the GPU for batched
|
|
54
|
+
// mode (an ExclusiveLock keeps the serial fallback off the GPU concurrently).
|
|
55
|
+
// When `admissionHeld` reports a waiting serial-lane request, the loop stops
|
|
56
|
+
// admitting, finishes the running rows, and releases the lock so the serial
|
|
57
|
+
// request runs (mlx-lm's drain_batch) — resumed via kick().
|
|
58
|
+
// Joins keep the running batch: a full-attention layout appends the new row in
|
|
59
|
+
// one pad + concat; rotating layers re-merge. MLX_BUN_BATCH_EXTEND=0 forces the
|
|
60
|
+
// full-attention re-merge.
|
|
61
|
+
|
|
62
|
+
import { MlxArray } from "@mlx-bun/mlx/array";
|
|
63
|
+
import { activeMemory,cacheMemory,clearCache,Dtype,peakMemory } from "@mlx-bun/mlx/ffi";
|
|
64
|
+
import * as ops from "@mlx-bun/mlx/ops";
|
|
65
|
+
import { type Cache } from "../contracts/mlx/cache";
|
|
66
|
+
import type { PrefillPolicy } from "../contracts/portable/prefill";
|
|
67
|
+
import { type MlxCompiledDecodeStep, type MlxMixedRange, type MlxSeparateMixed } from "../contracts/mlx/graph";
|
|
68
|
+
import { BF16_KV,COMPOSITION_DEFAULTS } from "../contracts/portable/composition";
|
|
69
|
+
import { resolveMlxPrefillPolicy } from "../generation/bindings/prefill-policy";
|
|
70
|
+
import { acquireModelWiredLimit } from "../generation/index";
|
|
71
|
+
import { compiledDecodeStepOf, tokenPhasesOf } from "../models/capabilities";
|
|
72
|
+
import type { MlxTokenGraph } from "../models/graph";
|
|
73
|
+
import { runtimeConfig,withRuntimeConfig,type RuntimeConfig } from "../runtime/config";
|
|
74
|
+
import { independentGreedySampling } from "../sampling/index";
|
|
75
|
+
import { createKvMaintenance,type KvMaintenance } from "../state/layout";
|
|
76
|
+
import { extractSoleRow,filterLayoutRows,ownedCacheLayout,publishRow } from "../state/layout";
|
|
77
|
+
import { leaseCacheStates } from "../state/leases";
|
|
78
|
+
import { cloneSingleRowState } from "../state/views";
|
|
79
|
+
import { CancellationSource,GenerationCancelled } from "./cancellation";
|
|
80
|
+
import { ExecutionTasks } from "./tasks";
|
|
81
|
+
|
|
82
|
+
/** Per-step phase timing (MLX_BUN_BATCH_STEP_TRACE=1, debug-only): where a
|
|
83
|
+
* decode step's wall time goes — graph BUILD+dispatch (host), the pipelined
|
|
84
|
+
* READ of the previous step's tokens (GPU wait), row EMIT (onToken/SSE), and
|
|
85
|
+
* the GAP between consecutive steps (drive-loop + everything else). Sums
|
|
86
|
+
* print via `stepTraceReport()` (the b1 profile experiment calls it). */
|
|
87
|
+
const STEP_T = { t0: 0, lastEnd: 0, build: 0, read: 0, emit: 0, gap: 0, n: 0 };
|
|
88
|
+
export function stepTraceReport(): string {
|
|
89
|
+
const per = (x: number) => (STEP_T.n ? (x / STEP_T.n).toFixed(3) : "0");
|
|
90
|
+
const s = `steps=${STEP_T.n} build=${per(STEP_T.build)}ms read=${per(STEP_T.read)}ms emit=${per(STEP_T.emit)}ms gap=${per(STEP_T.gap)}ms (per step)`;
|
|
91
|
+
STEP_T.t0 = STEP_T.lastEnd = STEP_T.build = STEP_T.read = STEP_T.emit = STEP_T.gap = STEP_T.n = 0;
|
|
92
|
+
return s;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let nextMixedWorkId = 0;
|
|
96
|
+
|
|
97
|
+
export class MlxBatchExecutionGroup {
|
|
98
|
+
readonly #tasks = new ExecutionTasks();
|
|
99
|
+
readonly #runtime: RuntimeConfig;
|
|
100
|
+
/** Decode-pipeline kill switch (MLX_BUN_BATCH_NO_PIPELINE=1, read once at
|
|
101
|
+
* construction): read each step's tokens synchronously. */
|
|
102
|
+
readonly #noPipeline: boolean;
|
|
103
|
+
readonly #stepTrace: boolean;
|
|
104
|
+
readonly #maxQueued: number;
|
|
105
|
+
#running: Row[] = [];
|
|
106
|
+
/** Per-layer state of the running rows; null when empty. A lone row that
|
|
107
|
+
* never merged keeps the serial caches its prefill built; every merge
|
|
108
|
+
* replaces them with each layer's own row layout (state/layout). */
|
|
109
|
+
#inners: Cache[] | null = null;
|
|
110
|
+
#pending: Row[] = [];
|
|
111
|
+
#prefill: MlxGroupPreparation | null = null; // the (single) joiner mid-prefill
|
|
112
|
+
#preparationPublishedOutput = false;
|
|
113
|
+
/** Sampled-but-unread token array [B], aligned with #running — the decode
|
|
114
|
+
* pipeline register. Filtered/disposed alongside the batched KV. */
|
|
115
|
+
#pendingToks: MlxArray | null = null;
|
|
116
|
+
/** Per-slot flags for #pendingToks: true = a REAL sampled token of that
|
|
117
|
+
* row's stream; false = a doomed/terminated row's placeholder (fed-token
|
|
118
|
+
* accounting must skip it — see Row.fed/fedTainted). Filtered/cleared in
|
|
119
|
+
* lockstep with the register. */
|
|
120
|
+
#pendingReal: boolean[] | null = null;
|
|
121
|
+
#method: MlxGroupedMethod | undefined;
|
|
122
|
+
#methodKey: string | undefined;
|
|
123
|
+
#decodeStateKey: string | undefined;
|
|
124
|
+
#steps = 0; // decode-step counter (clearCache cadence)
|
|
125
|
+
#cacheMaintenanceSteps = 0;
|
|
126
|
+
#looping = false;
|
|
127
|
+
#closed = false;
|
|
128
|
+
#driver: Promise<void> | null = null;
|
|
129
|
+
#wake: (() => void) | null = null;
|
|
130
|
+
readonly #maxBatch: number;
|
|
131
|
+
readonly #lock: ExclusiveLock | undefined;
|
|
132
|
+
readonly #admissionHeld: (() => boolean) | undefined;
|
|
133
|
+
readonly #prefillChunkSize: number;
|
|
134
|
+
readonly #prefillPolicy: PrefillPolicy;
|
|
135
|
+
readonly #prefillBatchTokenLimit: number;
|
|
136
|
+
readonly #prefillTailSplit: boolean;
|
|
137
|
+
/** The composition's cache service: admission under its KV budget, every row's state in and out. */
|
|
138
|
+
readonly #cache: RequestStateCache;
|
|
139
|
+
/** Whether the running rows' prefixes reuse and keep state; a row that differs waits for its own group. */
|
|
140
|
+
#reusesPrefix: boolean | undefined;
|
|
141
|
+
/** Retain hook of the currently-ADOPTED row's cache entry (at most one:
|
|
142
|
+
* only a lone adopted row holds un-copied entry caches). Runs after the
|
|
143
|
+
* adopted caches are disposed, or transfers back on put(). */
|
|
144
|
+
#adoptedRetain: (() => void) | null = null;
|
|
145
|
+
#contextKey: string | undefined;
|
|
146
|
+
#releaseContext: (() => void) | undefined;
|
|
147
|
+
/** Where a row's composed caches join row layouts under the composition's KV scheme. */
|
|
148
|
+
readonly #maintainKv: KvMaintenance;
|
|
149
|
+
/** The graph's compiled decode step, composed at bind when the graph declares
|
|
150
|
+
* one, its caches can be driven by it and the composition's `compiledDecode` is on.
|
|
151
|
+
* It runs a lone row whose caches are the graph's own (`#compiledRow`), never
|
|
152
|
+
* falling back to the ordinary forward: a failed step fails the batch. */
|
|
153
|
+
readonly #compiled: MlxCompiledDecodeStep | null;
|
|
154
|
+
/** Whether the lone running row decodes through `#compiled`: decided when the
|
|
155
|
+
* row is adopted (its request allows replay and every cache it holds is one
|
|
156
|
+
* the step drives), cleared when a second row merges. */
|
|
157
|
+
#compiledRow = false;
|
|
158
|
+
/** The graph's phase operations, indexed by the phase and rows of each forward. */
|
|
159
|
+
readonly #dispatch: MlxPhaseDispatch;
|
|
160
|
+
/** The composition's vectorized greedy sampling: one argmax over the batch when every live row is plain greedy. */
|
|
161
|
+
readonly #vectorGreedy: boolean;
|
|
162
|
+
/** The mixed iteration: the graph's mixed-rows forward when it declares one and the composition's
|
|
163
|
+
* `mixedPrefill` is on (default off), bound once; null otherwise. */
|
|
164
|
+
readonly #mixed: readonly MlxMixedRange[] | null;
|
|
165
|
+
readonly #mixedSeparate: MlxSeparateMixed | undefined;
|
|
166
|
+
/** The composition's `mixedTokenBudget`: prompt tokens one mixed forward carries. */
|
|
167
|
+
readonly #mixedTokenBudget: number;
|
|
168
|
+
|
|
169
|
+
constructor(private readonly model: MlxTokenGraph, opts: MlxBatchExecutionGroupOptions) {
|
|
170
|
+
// The composition facts this group runs, read once: the graph's record, else the defaults.
|
|
171
|
+
const facts = model.composition ?? COMPOSITION_DEFAULTS;
|
|
172
|
+
this.#runtime = opts.runtime ?? runtimeConfig();
|
|
173
|
+
this.#noPipeline = this.#runtime.value("MLX_BUN_BATCH_NO_PIPELINE") === "1";
|
|
174
|
+
this.#stepTrace = this.#runtime.value("MLX_BUN_BATCH_STEP_TRACE") === "1";
|
|
175
|
+
this.#maxQueued = opts.maxQueued ?? 64;
|
|
176
|
+
if (!Number.isSafeInteger(this.#maxQueued) || this.#maxQueued < 1) throw new Error("invalid batch queue limit");
|
|
177
|
+
this.#maxBatch = Math.max(1, Math.floor(opts.maxBatch));
|
|
178
|
+
this.#lock = opts.lock;
|
|
179
|
+
this.#admissionHeld = opts.admissionHeld;
|
|
180
|
+
this.#prefillPolicy = resolveMlxPrefillPolicy(model, this.#runtime, opts.prefillChunkSize);
|
|
181
|
+
this.#prefillChunkSize = this.#prefillPolicy.chunkSize(0);
|
|
182
|
+
this.#prefillBatchTokenLimit = opts.prefillBatchTokenLimit ?? 2048;
|
|
183
|
+
this.#prefillTailSplit = facts.prefillTailSplit;
|
|
184
|
+
this.#cache = opts.cache ?? new KvCacheService(model, {}, { runtime: this.#runtime });
|
|
185
|
+
const kv = model.composition?.kv ?? BF16_KV;
|
|
186
|
+
this.#maintainKv = createKvMaintenance(kv);
|
|
187
|
+
const proto = withRuntimeConfig(this.#runtime, () => model.makeCache()); // fresh caches hold no buffers
|
|
188
|
+
for (const c of proto) c.dispose();
|
|
189
|
+
// The composition decided at load whether its storage has a compiled form.
|
|
190
|
+
this.#compiled = (!model.composition?.pagedKv || pagedDecodeCompiles(model.composition.pagedKv, model.composition.kv))
|
|
191
|
+
? withRuntimeConfig(this.#runtime, () => compiledDecodeStepOf(model))
|
|
192
|
+
: null;
|
|
193
|
+
this.#dispatch = new MlxPhaseDispatch(tokenPhasesOf(model));
|
|
194
|
+
this.#vectorGreedy = facts.vectorGreedySampling;
|
|
195
|
+
this.#mixed = facts.mixedPrefill && (model.mixed || model.mixedSeparate) ? model.mixed ?? [] : null;
|
|
196
|
+
this.#mixedSeparate = model.mixedSeparate;
|
|
197
|
+
this.#mixedTokenBudget = facts.mixedTokenBudget;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get activeRows(): number {
|
|
201
|
+
return this.#running.length;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
get pendingRows(): number {
|
|
205
|
+
return this.#pending.length + (this.#prefill?.rows.length ?? 0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Admission of the queue head by the cache service's KV budget. True =
|
|
209
|
+
* admitted now. A candidate that can never be admitted (past the caches'
|
|
210
|
+
* position limit, or over the budget alone) is rejected here, so it never
|
|
211
|
+
* deadlocks the queue; one that fits alone but not beside the admitted rows
|
|
212
|
+
* waits. */
|
|
213
|
+
#kvAdmits(candidate: Row): boolean {
|
|
214
|
+
try { return this.#cache.admit(candidate.req); }
|
|
215
|
+
catch (error) {
|
|
216
|
+
this.#pending.shift();
|
|
217
|
+
candidate.reject(error);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Submit a request; resolves when its row finishes (EOS, stop, or length). */
|
|
223
|
+
submit(req: BatchRequest): Promise<BatchStats> {
|
|
224
|
+
if (this.#closed) return Promise.reject(new Error("scheduler closed"));
|
|
225
|
+
if (req.signal?.aborted) return Promise.reject(req.signal.reason);
|
|
226
|
+
if (this.#pending.length >= this.#maxQueued) return Promise.reject(new AdmissionRejected());
|
|
227
|
+
const request = req = { ...req, prefillChunkSize: req.prefillChunkSize ?? this.#prefillPolicy.chunkSize(req.promptIds.length) };
|
|
228
|
+
return new Promise<BatchStats>((resolve, reject) => {
|
|
229
|
+
let abortListener: (() => void) | null = null;
|
|
230
|
+
// A row settles once: its state lease with the cache ends there, completed or not.
|
|
231
|
+
const cleanup = (completed: boolean) => {
|
|
232
|
+
if (abortListener) request.signal?.removeEventListener("abort", abortListener);
|
|
233
|
+
abortListener = null;
|
|
234
|
+
this.#cache.release(request, completed);
|
|
235
|
+
};
|
|
236
|
+
this.#pending.push({
|
|
237
|
+
req: request,
|
|
238
|
+
resolve: (stats) => { cleanup(true); resolve(stats); },
|
|
239
|
+
reject: (error) => { cleanup(false); reject(error); },
|
|
240
|
+
current: 0, generated: 0, sampled: 0, promptTokens: req.promptIds.length,
|
|
241
|
+
admittedAt: 0, firstTokenAt: 0,
|
|
242
|
+
cachedTokens: 0, fed: [], fedTainted: false, merged: false,
|
|
243
|
+
});
|
|
244
|
+
if (req.signal) {
|
|
245
|
+
abortListener = () => this.kick();
|
|
246
|
+
req.signal.addEventListener("abort", abortListener, { once: true });
|
|
247
|
+
}
|
|
248
|
+
this.#ensureLoop();
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Wake the driver loop (e.g. after admissionHeld flips back to false). */
|
|
253
|
+
kick(): void {
|
|
254
|
+
this.#ensureLoop();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Read-only native preparation borrows a boundary of the live execution
|
|
258
|
+
* group. Model mutation still requires the gateway's exclusive drain. */
|
|
259
|
+
runPreparation<T>(work: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
260
|
+
if (this.#closed) return Promise.reject(new Error("scheduler closed"));
|
|
261
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
262
|
+
const cancellation = signal ? new CancellationSource() : undefined;
|
|
263
|
+
const abort = () => cancellation?.cancel("requested");
|
|
264
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
265
|
+
const result = this.#tasks.enqueue(work, cancellation).catch(error => {
|
|
266
|
+
throw error instanceof GenerationCancelled && signal?.aborted ? signal.reason : error;
|
|
267
|
+
}).finally(() => signal?.removeEventListener("abort", abort));
|
|
268
|
+
this.#ensureLoop();
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#ensureLoop(): void {
|
|
273
|
+
if (this.#wake) { this.#wake(); return; }
|
|
274
|
+
if (this.#looping || this.#closed) return;
|
|
275
|
+
this.#looping = true;
|
|
276
|
+
this.#driver = withRuntimeConfig(this.#runtime, () => this.#drive());
|
|
277
|
+
void this.#driver.catch((error) => console.error(`batch scheduler cleanup failed: ${error}`));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Stop at the next safe work boundary and release all queued/active runs. */
|
|
281
|
+
async close(): Promise<void> {
|
|
282
|
+
this.#closed = true;
|
|
283
|
+
this.#wake?.();
|
|
284
|
+
await this.#driver;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async #drive(): Promise<void> {
|
|
288
|
+
const scheduler = this;
|
|
289
|
+
const group: ExecutionGroup = {
|
|
290
|
+
get pendingTasks() { return scheduler.#tasks.pending; },
|
|
291
|
+
advanceTask: () => this.#tasks.advance(),
|
|
292
|
+
get active() { return scheduler.#running.length; },
|
|
293
|
+
get queued() { return scheduler.#pending.length; },
|
|
294
|
+
get preparing() { return scheduler.#prefill !== null; },
|
|
295
|
+
get preparingRows() { return scheduler.#prefill?.rows.length ?? 0; },
|
|
296
|
+
get canPrepareMore() { return scheduler.#prefill?.canAdmit === true; },
|
|
297
|
+
get preparingTokens() { return scheduler.#prefill?.tokenWeight ?? 0; },
|
|
298
|
+
// Cache restoration belongs to preparation. A queued request's prompt
|
|
299
|
+
// length is an upper bound until its cache owner resolves the prefix.
|
|
300
|
+
get nextPreparationTokens() { return scheduler.#pending[0]?.promptTokens ?? 0; },
|
|
301
|
+
get maxPreparationTokens() { return scheduler.#prefillBatchTokenLimit; },
|
|
302
|
+
get preparationPublishedOutput() { return scheduler.#preparationPublishedOutput; },
|
|
303
|
+
get maxActive() { return scheduler.#maxBatch; },
|
|
304
|
+
get admissionHeld() { return scheduler.#admissionHeld?.() === true; },
|
|
305
|
+
get closed() { return scheduler.#closed; },
|
|
306
|
+
pruneCancelled() {
|
|
307
|
+
for (let i = scheduler.#pending.length - 1; i >= 0; i--) {
|
|
308
|
+
const row = scheduler.#pending[i]!;
|
|
309
|
+
if (!row.req.signal?.aborted) continue;
|
|
310
|
+
scheduler.#pending.splice(i, 1);
|
|
311
|
+
row.reject(row.req.signal.reason);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
admitNext: () => this.#admitNext(),
|
|
315
|
+
canBurst: () => this.#contextCompatible(this.#pending[0]!) && this.#cache.fits(this.#pending[0]!.req),
|
|
316
|
+
get mixedPreparation() {
|
|
317
|
+
return scheduler.#mixed && scheduler.#decodeStateKey === undefined && scheduler.#prefill?.supportsMixedWork !== false &&
|
|
318
|
+
(!scheduler.#method || scheduler.#method.runningTokens !== undefined)
|
|
319
|
+
? { runningTokens: scheduler.#method?.runningTokens ?? scheduler.#running.length,
|
|
320
|
+
minimumPreparationTokens: scheduler.#prefill?.rows.length ?? 0 }
|
|
321
|
+
: undefined;
|
|
322
|
+
},
|
|
323
|
+
get maxIterationTokens() { return scheduler.#mixedTokenBudget; },
|
|
324
|
+
advanceMixed: async tokenBudget => {
|
|
325
|
+
this.#cache.boundary();
|
|
326
|
+
const mixed = this.#mixed!;
|
|
327
|
+
const advanced = await runMixedTokenIteration({
|
|
328
|
+
prepare: forward => this.#advancePreparation({ forward,
|
|
329
|
+
maxTokens: tokenBudget - (this.#method?.runningTokens ?? this.#running.length) }),
|
|
330
|
+
decode: forward => this.#method ? this.#method.advance(forward) : this.#step(forward),
|
|
331
|
+
mixed: (groups, rows) => {
|
|
332
|
+
const workId = `mixed:${++nextMixedWorkId}`;
|
|
333
|
+
const tokens = groups.map(group => group.ids.shape[0]! * group.ids.shape[1]!);
|
|
334
|
+
const closes = [...this.#running, ...(this.#prefill?.rows ?? [])].map(row =>
|
|
335
|
+
row.req.trace?.begin("engine.mixed_forward", { workId,
|
|
336
|
+
decodeTokens: tokens[0]!, prefillTokens: tokens[1]!, packedTokens: tokens[0]! + tokens[1]! }));
|
|
337
|
+
try { return dispatchMixedGroups(groups, rows, mixed, this.#mixedSeparate); }
|
|
338
|
+
finally { for (const close of closes) close?.(); }
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
if (!advanced && this.#running.length) {
|
|
342
|
+
if (this.#method) await this.#method.advance(); else await this.#step();
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
advancePreparation: () => {
|
|
346
|
+
this.#cache.boundary();
|
|
347
|
+
return this.#advancePreparation();
|
|
348
|
+
},
|
|
349
|
+
advance: () => {
|
|
350
|
+
if (this.#cacheMaintenanceSteps++ % 256 === 0) this.#cache.boundary();
|
|
351
|
+
return this.#method ? this.#method.advance() : this.#step();
|
|
352
|
+
},
|
|
353
|
+
failActive: (error) => {
|
|
354
|
+
for (const row of this.#running) row.reject(error);
|
|
355
|
+
this.#applyFilter([], true); // failed state never enters the prefix store
|
|
356
|
+
},
|
|
357
|
+
failAll: (error) => this.#failAll(error),
|
|
358
|
+
reserveResidency: () => {
|
|
359
|
+
const release = acquireModelWiredLimit(this.model);
|
|
360
|
+
return () => disposeResources([
|
|
361
|
+
{ dispose: () => this.#leaveContext() }, { dispose: release },
|
|
362
|
+
]);
|
|
363
|
+
},
|
|
364
|
+
...(this.#lock ? { acquireExecution: () => this.#lock!.acquire() } : {}),
|
|
365
|
+
waitForWork: async () => {
|
|
366
|
+
await new Promise<void>((resolve) => { this.#wake = resolve; });
|
|
367
|
+
this.#wake = null;
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
try {
|
|
371
|
+
await driveExecutionGroup(group, {
|
|
372
|
+
now: () => performance.now(),
|
|
373
|
+
yield: () => new Promise<void>((resolve) => setImmediate(resolve)),
|
|
374
|
+
}, this.#runtime.flag("MLX_BUN_EARLY_FIRST_TOKEN", true));
|
|
375
|
+
} finally { this.#looping = false; }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#contextCompatible(row: Row): boolean {
|
|
379
|
+
return (!this.#running.length && !this.#prefill) || ((row.req.context?.key ?? "") === this.#contextKey && row.req.method?.key === this.#methodKey && (row.req.reusesPrefix !== false) === this.#reusesPrefix && row.req.promptInput?.decodeState?.key === this.#decodeStateKey);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#leaveContext(): void {
|
|
383
|
+
const release = this.#releaseContext;
|
|
384
|
+
this.#releaseContext = undefined;
|
|
385
|
+
this.#contextKey = undefined;
|
|
386
|
+
release?.();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#admitNext(): boolean {
|
|
390
|
+
if (!this.#contextCompatible(this.#pending[0]!) || !this.#kvAdmits(this.#pending[0]!)) return false;
|
|
391
|
+
const row = this.#pending.shift()!;
|
|
392
|
+
try {
|
|
393
|
+
this.#reusesPrefix = row.req.reusesPrefix !== false;
|
|
394
|
+
this.#decodeStateKey = row.req.promptInput?.decodeState?.key;
|
|
395
|
+
row.cacheNamespace = typeof row.req.cacheNamespace === "function"
|
|
396
|
+
? row.req.cacheNamespace() : row.req.cacheNamespace;
|
|
397
|
+
const key = row.req.context?.key ?? "";
|
|
398
|
+
if (key !== this.#contextKey) {
|
|
399
|
+
this.#leaveContext();
|
|
400
|
+
this.#releaseContext = row.req.context?.enter();
|
|
401
|
+
this.#contextKey = key;
|
|
402
|
+
}
|
|
403
|
+
row.req.onAdmitted?.();
|
|
404
|
+
row.admittedAt = performance.now();
|
|
405
|
+
if (row.req.method?.key !== this.#methodKey) {
|
|
406
|
+
this.#method?.dispose(); this.#method = undefined; this.#methodKey = undefined;
|
|
407
|
+
if (row.req.method) {
|
|
408
|
+
const executor = this;
|
|
409
|
+
this.#method = row.req.method.open({
|
|
410
|
+
get rows() { return executor.#running; }, runtime: this.#runtime,
|
|
411
|
+
prefillChunkSize: this.#prefillChunkSize, prefillTailSplit: this.#prefillTailSplit,
|
|
412
|
+
cache: this.#cache, kvMaintenance: this.#maintainKv,
|
|
413
|
+
join: row => { this.#running.push(row); },
|
|
414
|
+
filterRows: keep => this.#applyFilter(keep),
|
|
415
|
+
publish: async (row, token, logprobs) => {
|
|
416
|
+
row.req.signal?.throwIfAborted();
|
|
417
|
+
this.#firstOutput(row);
|
|
418
|
+
return row.req.onToken(token, logprobs);
|
|
419
|
+
},
|
|
420
|
+
finish: (row, reason) => this.#finish(row, reason),
|
|
421
|
+
});
|
|
422
|
+
this.#methodKey = row.req.method.key;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (this.#prefill) {
|
|
426
|
+
this.#prefill.admit!(row);
|
|
427
|
+
} else if (this.#method) {
|
|
428
|
+
this.#prefill = this.#method.prepare(row);
|
|
429
|
+
} else {
|
|
430
|
+
const preparation = new MlxPrefillCohort({
|
|
431
|
+
chunkSize: this.#prefillChunkSize, tailSplit: this.#prefillTailSplit,
|
|
432
|
+
cache: this.#cache, maintain: this.#maintainKv,
|
|
433
|
+
forward: async (ids, caches, step) => this.#dispatch.prompt(step.phase, step.sequences, step.tokens)(ids, caches, null),
|
|
434
|
+
project: (hidden, caches, completed, step) => this.#projectPrefill(hidden, caches, completed, step),
|
|
435
|
+
complete: (state, logits) => this.#completePrefill(state, logits),
|
|
436
|
+
resume: state => this.#completePrefill(state, null),
|
|
437
|
+
reject: (failed, error) => this.#rejectPreparingRows([failed], error),
|
|
438
|
+
});
|
|
439
|
+
preparation.admit(row); this.#prefill = preparation;
|
|
440
|
+
}
|
|
441
|
+
} catch (error) { row.reject(error); }
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#rejectPreparingRows(rows: readonly Row[], error: unknown): void {
|
|
446
|
+
const retiring = new Set(rows);
|
|
447
|
+
const keep = this.#running.flatMap((row, index) => retiring.has(row) ? [] : [index]);
|
|
448
|
+
if (keep.length !== this.#running.length) this.#applyFilter(keep, true);
|
|
449
|
+
for (const row of rows) row.reject(error);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async #advancePreparation(work?: MlxPreparationWork): Promise<void> {
|
|
453
|
+
const p = this.#prefill!, rows = p.rows;
|
|
454
|
+
this.#preparationPublishedOutput = false;
|
|
455
|
+
try {
|
|
456
|
+
if (await p.advance(work)) this.#prefill = null;
|
|
457
|
+
} catch (error) {
|
|
458
|
+
this.#prefill = null;
|
|
459
|
+
let failure = error;
|
|
460
|
+
try { p.dispose(); }
|
|
461
|
+
catch (cleanupError) { failure = new AggregateError([error, cleanupError], "batch prefill and cleanup failed"); }
|
|
462
|
+
try { this.#rejectPreparingRows(rows, failure); }
|
|
463
|
+
catch (cleanupError) {
|
|
464
|
+
failure = new AggregateError([failure, cleanupError], "batch admission retirement failed");
|
|
465
|
+
for (const row of rows) row.reject(failure);
|
|
466
|
+
this.#failAll(failure);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#failAll(error: unknown): void {
|
|
472
|
+
this.#tasks.rejectAll(error);
|
|
473
|
+
const p = this.#prefill;
|
|
474
|
+
const rows = new Set([...this.#running, ...this.#pending, ...(p?.rows ?? [])]);
|
|
475
|
+
const retain = this.#adoptedRetain;
|
|
476
|
+
const resources = [p, this.#method, ...(this.#inners ?? []), this.#pendingToks,
|
|
477
|
+
{ dispose: () => retain?.() }]
|
|
478
|
+
.filter((resource): resource is DisposableResource => resource != null);
|
|
479
|
+
this.#pending = []; this.#running = []; this.#prefill = null;
|
|
480
|
+
this.#method = undefined; this.#methodKey = undefined;
|
|
481
|
+
this.#inners = null; this.#pendingToks = null; this.#pendingReal = null; this.#compiledRow = false;
|
|
482
|
+
this.#adoptedRetain = null;
|
|
483
|
+
for (const row of rows) row.reject(error);
|
|
484
|
+
disposeResources(resources);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** The target owns projection geometry; samplers receive individual rows. */
|
|
488
|
+
#projectPrefill(hidden: MlxArray, caches: Cache[], completed: readonly PrefillState[], step: MlxPrefillForward): MlxArray {
|
|
489
|
+
for (const p of completed) {
|
|
490
|
+
p.closePrefill?.(); p.closePrefill = undefined;
|
|
491
|
+
p.row.closeTokenZero = p.row.req.trace?.begin("token_zero.total", { mechanism: "continuous" });
|
|
492
|
+
}
|
|
493
|
+
const attributed = this.#runtime.value("MLX_BUN_P2R_SYNC") === "1"
|
|
494
|
+
? completed.flatMap(p => p.row.req.trace ? [p.row.req.trace] : []) : [];
|
|
495
|
+
const begin = (phase: "token_zero.forward" | "token_zero.head") => attributed.map(trace => trace.begin(phase, {
|
|
496
|
+
mechanism: "continuous", activeBytes: activeMemory(), cacheBytes: cacheMemory(), peakBytes: peakMemory(),
|
|
497
|
+
}));
|
|
498
|
+
if (attributed.length) {
|
|
499
|
+
const closes = begin("token_zero.forward");
|
|
500
|
+
try { withResource(leaseCacheStates(caches), state => ops.evalAll([hidden, ...state])); }
|
|
501
|
+
finally { for (const close of closes) close(); }
|
|
502
|
+
}
|
|
503
|
+
// Project before selecting rows: M4 quantized projection can use different
|
|
504
|
+
// arithmetic at M=1. Tail-split preparation reaches this with N=1.
|
|
505
|
+
const logits = this.#dispatch.logits(step.sequences * step.tokens)(hidden);
|
|
506
|
+
try {
|
|
507
|
+
if (attributed.length) {
|
|
508
|
+
const closes = begin("token_zero.head");
|
|
509
|
+
try { ops.evalAll([logits]); } finally { for (const close of closes) close(); }
|
|
510
|
+
}
|
|
511
|
+
return logits;
|
|
512
|
+
} catch (error) { logits.dispose(); throw error; }
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Sample borrowed logits from the shared target projection. */
|
|
516
|
+
async #completePrefill(p: PrefillState, logits: MlxArray | null): Promise<void> {
|
|
517
|
+
if (p.continuation) {
|
|
518
|
+
const saved = p.continuation, row = p.row;
|
|
519
|
+
row.req.continuation!.resumeSampling(saved);
|
|
520
|
+
const interval = row.req.continuation!.interval;
|
|
521
|
+
this.#checkpointAt.set(row, (Math.floor(saved.generatedTokens / interval) + 1) * interval);
|
|
522
|
+
row.firstTokenAt = performance.now();
|
|
523
|
+
row.closeTokenZero?.(); row.closeTokenZero = undefined;
|
|
524
|
+
this.#preparationPublishedOutput = true;
|
|
525
|
+
for (const token of saved.cacheTokens.slice(row.req.promptIds.length)) {
|
|
526
|
+
row.req.signal?.throwIfAborted();
|
|
527
|
+
if (await row.req.onToken(token) === false)
|
|
528
|
+
throw new Error("saved generation prefix triggered a terminal stop while replaying");
|
|
529
|
+
}
|
|
530
|
+
row.fed = saved.cacheTokens.slice(row.req.promptIds.length);
|
|
531
|
+
row.generated = saved.generatedTokens + 1;
|
|
532
|
+
row.sampled = saved.generatedTokens + 1;
|
|
533
|
+
row.cachedTokens = Math.min(saved.cacheTokens.length, row.promptTokens);
|
|
534
|
+
const stop = await this.#emit(row, saved.pendingToken);
|
|
535
|
+
if (stop !== "continue") {
|
|
536
|
+
this.#putOrDispose(row, p.solo, saved.cacheTokens, p.retain);
|
|
537
|
+
p.solo = [];
|
|
538
|
+
this.#finish(row, stop);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
await this.#mergeJoiner(p);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (!logits) throw new Error("ordinary prefill completed without logits");
|
|
545
|
+
const forceAttribution = !!p.row.req.trace && this.#runtime.value("MLX_BUN_P2R_SYNC") === "1";
|
|
546
|
+
const V = logits.shape[2]!;
|
|
547
|
+
const last2 = ops.reshape(logits, [1, V]);
|
|
548
|
+
const closeSample = forceAttribution
|
|
549
|
+
? p.row.req.trace!.begin("token_zero.sample", { mechanism: "continuous" })
|
|
550
|
+
: undefined;
|
|
551
|
+
let sampled: MlxArray;
|
|
552
|
+
try { sampled = p.row.req.sample!(last2, 0); } finally { last2.dispose(); }
|
|
553
|
+
// The first admission can seed the existing device-token register. Its
|
|
554
|
+
// first decode forward then overlaps token-zero readback, just like every
|
|
555
|
+
// later pipeline step. Grammar and explicit eager-output modes retain
|
|
556
|
+
// their read-before-advance order.
|
|
557
|
+
if (!this.#inners && !p.row.req.grammar && !this.#noPipeline &&
|
|
558
|
+
!this.#runtime.flag("MLX_BUN_EARLY_FIRST_TOKEN", true) && !forceAttribution) {
|
|
559
|
+
let owned: MlxArray | null = sampled;
|
|
560
|
+
try {
|
|
561
|
+
ops.asyncEvalAll([sampled]);
|
|
562
|
+
p.row.sampled = 1;
|
|
563
|
+
await this.#mergeJoiner(p);
|
|
564
|
+
this.#pendingToks = sampled;
|
|
565
|
+
this.#pendingReal = [true];
|
|
566
|
+
owned = null;
|
|
567
|
+
return;
|
|
568
|
+
} finally { owned?.dispose(); }
|
|
569
|
+
}
|
|
570
|
+
const tok = this.#readToken(sampled);
|
|
571
|
+
closeSample?.();
|
|
572
|
+
p.row.sampled = 1;
|
|
573
|
+
p.row.generated = 1;
|
|
574
|
+
|
|
575
|
+
// Grammar (B1): the mask0 was applied inside the sample closure (the
|
|
576
|
+
// controller is primed at compile). After reading token 0, advance the
|
|
577
|
+
// matcher — fires the async fill for the NEXT step, overlapping the merge
|
|
578
|
+
// below. If the grammar is already satisfied at token 0 (a 1-token grammar,
|
|
579
|
+
// e.g. guided_choice landing on a single-token option), emit + finish("stop")
|
|
580
|
+
// WITHOUT merging into the batch — the row never joins #running.
|
|
581
|
+
if (p.row.req.grammar) {
|
|
582
|
+
p.row.req.grammar.accept(tok);
|
|
583
|
+
if (p.row.req.grammar.isTerminated) {
|
|
584
|
+
const stop = await this.#emit(p.row, tok);
|
|
585
|
+
// Token 0 was sampled but never fed — the caches cover exactly the
|
|
586
|
+
// prompt, a clean prompt-only entry (put-or-dispose).
|
|
587
|
+
this.#putOrDispose(p.row, p.solo, p.row.req.promptIds, p.retain);
|
|
588
|
+
this.#finish(p.row, stop === "continue" ? "stop" : stop);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const stop = await this.#emit(p.row, tok);
|
|
594
|
+
if (stop !== "continue") {
|
|
595
|
+
this.#putOrDispose(p.row, p.solo, p.row.req.promptIds, p.retain);
|
|
596
|
+
this.#finish(p.row, stop);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
// The composed caches converted inside their own appends (oracle
|
|
600
|
+
// composition: prefill ids[:-1] → convert → L=1 step-0 under the default
|
|
601
|
+
// tail split; with the composition's prefillTailSplit off, token 0 sampled from the
|
|
602
|
+
// final chunk's bf16 read, then the conversion, before decode step 1).
|
|
603
|
+
await this.#mergeJoiner(p);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Merge a fully-prefilled joiner with the running batch, layer by layer,
|
|
608
|
+
* each layer's own row layout doing the work (state/layout). Flushes the
|
|
609
|
+
* decode pipeline first so the row set is settled and the next step starts
|
|
610
|
+
* cold. */
|
|
611
|
+
async #mergeJoiner(p: PrefillState): Promise<void> {
|
|
612
|
+
await this.#flushPipeline();
|
|
613
|
+
this.#maintainKv.prepareBatch?.(p.solo);
|
|
614
|
+
|
|
615
|
+
// ADOPT, don't copy: a row joining an EMPTY batch keeps its solo caches as
|
|
616
|
+
// the batch inners — a pointer handoff, zero bytes moved. The copy happens
|
|
617
|
+
// only when a SECOND row joins and a genuinely new layout must exist. The
|
|
618
|
+
// prize beyond the saved copy: the lone row's caches stay the graph's own
|
|
619
|
+
// serial caches, so the B=1 step is literally the direct graph, and compiled
|
|
620
|
+
// decode and prompt-cache take/put become possible for it. Each layout's
|
|
621
|
+
// mergeRows takes such an adopted row as its first row.
|
|
622
|
+
if (!this.#inners) {
|
|
623
|
+
// The adopted row decodes through the compiled step when its request
|
|
624
|
+
// allows replay. The composition has the step only when every serial
|
|
625
|
+
// cache a row holds is one it drives (a stored prefix comes back as the
|
|
626
|
+
// graph's composed caches, a cohort row as its layout's serial form).
|
|
627
|
+
this.#compiledRow = this.#compiled !== null && p.row.req.compiledDecode !== false;
|
|
628
|
+
this.#inners = p.solo; p.solo = [];
|
|
629
|
+
this.#adoptedRetain = p.retain ?? null; p.retain = undefined;
|
|
630
|
+
this.#running.push(p.row);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
this.#compiledRow = false;
|
|
634
|
+
|
|
635
|
+
const prev = this.#inners;
|
|
636
|
+
const merged: Cache[] = [];
|
|
637
|
+
try {
|
|
638
|
+
for (let layer = 0; layer < p.solo.length; layer++) {
|
|
639
|
+
const solo = p.solo[layer]!;
|
|
640
|
+
const layout = ownedCacheLayout(solo);
|
|
641
|
+
if (!layout) throw new Error(`cache layer ${layer} (${solo.signature()}) has no batch layout`);
|
|
642
|
+
merged.push(layout);
|
|
643
|
+
layout.mergeRows([prev[layer]!, solo]);
|
|
644
|
+
}
|
|
645
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(merged)); }
|
|
646
|
+
for (const c of prev) c.dispose();
|
|
647
|
+
// An adopted row's entry-backed arrays are gone after the prev dispose;
|
|
648
|
+
// run its retain now. The joiner's likewise after its solo dispose.
|
|
649
|
+
this.#adoptedRetain?.();
|
|
650
|
+
this.#adoptedRetain = null;
|
|
651
|
+
const completed = p.solo; p.solo = [];
|
|
652
|
+
const release = p.retain; p.retain = undefined;
|
|
653
|
+
disposeResources([...completed, { dispose: () => release?.() }]);
|
|
654
|
+
// Every row in a REAL merge has its KV interleaved in batched buffers —
|
|
655
|
+
// no longer prompt-cache put() candidates.
|
|
656
|
+
for (const r of this.#running) r.merged = true;
|
|
657
|
+
p.row.merged = true;
|
|
658
|
+
this.#inners = merged;
|
|
659
|
+
this.#running.push(p.row);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** One PIPELINED batched decode step (mlx-lm GenerationBatch._step):
|
|
663
|
+
* 1. forward all rows from the UNREAD pending token array (or, pipeline
|
|
664
|
+
* cold, from the rows' last emitted tokens), sample each live row's next
|
|
665
|
+
* token on its [1,V] slice, asyncEval the new [B] token array;
|
|
666
|
+
* 2. THEN sync-read the previous step's tokens (overlapping the readback
|
|
667
|
+
* with this step's compute), emit them, and evict finished rows.
|
|
668
|
+
* Rows that finish get one extra harmless KV write from the already-built
|
|
669
|
+
* step; filter drops the row (mlx-lm behaves identically). Length-finished
|
|
670
|
+
* rows are known in advance and are NOT sampled (placeholder slot). */
|
|
671
|
+
/** One decode forward of the `B` running rows: the media rows' positioned decode or the graph's decode for
|
|
672
|
+
* their width, through the mixed iteration's hook when it runs one. */
|
|
673
|
+
#decodeForward(ids: MlxArray, caches: Cache[], B: number, work?: MlxForwardWork): Promise<MlxArray> {
|
|
674
|
+
const width = (B), decodeState = this.#running[0]?.req.promptInput?.decodeState;
|
|
675
|
+
const states = decodeState ? this.#running.map(row => row.req.promptInput!.decodeState!) : null;
|
|
676
|
+
const own = async () => decodeState ? decodeState.decode(ids, caches, states!, width) : this.#dispatch.decode(B)(ids, caches, null);
|
|
677
|
+
return work ? work(ids, caches, { phase: "decode", rows: B, capture: null, own }) : own();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async #step(forward?: MlxForwardWork): Promise<void> {
|
|
681
|
+
if (this.#stepTrace) {
|
|
682
|
+
const now = performance.now();
|
|
683
|
+
if (STEP_T.lastEnd) STEP_T.gap += now - STEP_T.lastEnd;
|
|
684
|
+
STEP_T.t0 = now;
|
|
685
|
+
}
|
|
686
|
+
const rows = this.#running;
|
|
687
|
+
const B = rows.length;
|
|
688
|
+
const inners = this.#inners!;
|
|
689
|
+
const decodeState = rows[0]?.req.promptInput?.decodeState;
|
|
690
|
+
|
|
691
|
+
// A row is live if it still needs tokens sampled; a row whose pending
|
|
692
|
+
// unread token is its last (sampled == maxTokens) only awaits emission.
|
|
693
|
+
const anyLive = rows.some((r) => r.sampled < r.req.maxTokens);
|
|
694
|
+
// Grammar (B1): if any row has a LIVE grammar controller (not terminated,
|
|
695
|
+
// still sampling), take the read-before-build shape — the matcher's
|
|
696
|
+
// acceptToken needs the token id as a JS number, so the previous step's
|
|
697
|
+
// [B] token array is read back NOW (before building the next graph),
|
|
698
|
+
// accept()ed per row (firing async fills that overlap the forward), then
|
|
699
|
+
// ready() is awaited before this step's sample. This is the serial loop's
|
|
700
|
+
// grammar resolution transplanted to the batch. Batches with NO live
|
|
701
|
+
// grammar row keep the pipelined path byte-identical (zero cost when
|
|
702
|
+
// unused). The trade: while a grammar row is live the batch runs
|
|
703
|
+
// effectively NO_PIPELINE (readback no longer overlaps GPU compute) —
|
|
704
|
+
// bounded by the readback (~0.1 ms) + fills (0.004–0.19 ms/row, overlapped
|
|
705
|
+
// with the graph build). Serial grammar pays the identical trade today.
|
|
706
|
+
const hasLiveGrammar = anyLive && rows.some(
|
|
707
|
+
(r) => r.req.grammar && !r.req.grammar.isTerminated,
|
|
708
|
+
);
|
|
709
|
+
if (hasLiveGrammar) return this.#stepGrammar(forward);
|
|
710
|
+
|
|
711
|
+
let nextToks: MlxArray | null = null;
|
|
712
|
+
let nextReal: boolean[] | null = null;
|
|
713
|
+
if (anyLive) {
|
|
714
|
+
// Fed-token accounting (prompt-cache put): a COLD step feeds each
|
|
715
|
+
// row's `current` (values known now, always a real emitted token); a
|
|
716
|
+
// pipelined step feeds the pending array, whose values are pushed at
|
|
717
|
+
// the read below gated on the per-slot real flags.
|
|
718
|
+
if (!this.#pendingToks) for (const r of rows) r.fed.push(r.current);
|
|
719
|
+
// Each layer's state is its own row layout (state/layout), which supplies
|
|
720
|
+
// the mask and per-row RoPE positions its rows need — and none when no
|
|
721
|
+
// row is padded: with every leftPad 0 a layout is exactly the serial cache
|
|
722
|
+
// (the empty N=1 mask, the shared scalar offset), so the B=1 case above
|
|
723
|
+
// all dispatches the SAME per-step graph a direct generation builds,
|
|
724
|
+
// without a host mask build + ~8 device nodes PER FULL LAYER PER TOKEN
|
|
725
|
+
// (the constant ~4–6 ms/step host tax at B=1).
|
|
726
|
+
// Compiled decode at B=1: after adopt-don't-copy, a lone row's caches
|
|
727
|
+
// are the caches the graph itself made, so a graph that declares a
|
|
728
|
+
// compiled step replays its recorded graph here — closing the batch
|
|
729
|
+
// lane's last B=1 host-tax gap (e4b's ~7%). The step was composed at
|
|
730
|
+
// bind and the row chosen for it when it was adopted (`#compiledRow`);
|
|
731
|
+
// a per-row int32 sampler register keeps the decode operation (the
|
|
732
|
+
// trace signature is uint32). A failed step fails the batch: there is
|
|
733
|
+
// no second path to fall back to.
|
|
734
|
+
// Grammar batches use #stepGrammar and stay on the decode operation.
|
|
735
|
+
let lg: MlxArray | null = null;
|
|
736
|
+
let evalWith: MlxArray[] = [];
|
|
737
|
+
if (!forward && !decodeState && this.#compiledRow && (!this.#pendingToks || this.#pendingToks.dtype === Dtype.uint32)) {
|
|
738
|
+
let cur = this.#pendingToks;
|
|
739
|
+
let owned = false;
|
|
740
|
+
if (!cur) {
|
|
741
|
+
const i = ops.fromInt32([rows[0]!.current], [1]);
|
|
742
|
+
cur = i.astype(Dtype.uint32);
|
|
743
|
+
i.dispose();
|
|
744
|
+
owned = true;
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
const r = this.#compiled!.step(cur, inners as Cache[]);
|
|
748
|
+
lg = r.logits; // [1,1,V]
|
|
749
|
+
evalWith = r.evalWith;
|
|
750
|
+
} finally {
|
|
751
|
+
if (owned) cur.dispose();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
let fwd: Cache[] | null = null;
|
|
755
|
+
try {
|
|
756
|
+
if (!lg) {
|
|
757
|
+
fwd = inners;
|
|
758
|
+
const ids = this.#pendingToks
|
|
759
|
+
? ops.reshape(this.#pendingToks, [B, 1]) // feed the unread tokens
|
|
760
|
+
: ops.fromInt32(rows.map((r) => r.current), [B, 1]); // pipeline cold
|
|
761
|
+
const h = await this.#decodeForward(ids, fwd, B, forward);
|
|
762
|
+
ids.dispose();
|
|
763
|
+
lg = this.#dispatch.logits(B)(h); // [B,1,V]
|
|
764
|
+
h.dispose();
|
|
765
|
+
}
|
|
766
|
+
const V = lg.shape[2]!;
|
|
767
|
+
// Vectorized homogeneous sampling (batching-perf-path P0): when every
|
|
768
|
+
// LIVE row is plain greedy, one log-softmax+argmax over [B,V] replaces
|
|
769
|
+
// B slice/sample/concat graphs. Per-row identical math (row-independent
|
|
770
|
+
// ops, same per-row shapes — argmax over log-softmax mirrors the
|
|
771
|
+
// closure's toLogprobs→argmax exactly, tie behavior included). Doomed
|
|
772
|
+
// rows get a real argmax instead of the placeholder 0 — equally
|
|
773
|
+
// harmless (the slot is filtered before it is ever emitted; its only
|
|
774
|
+
// use is one KV write on the row's own, about-to-evict row).
|
|
775
|
+
const vecOk =
|
|
776
|
+
this.#vectorGreedy &&
|
|
777
|
+
rows.every((r) => r.sampled >= r.req.maxTokens || r.req.plainGreedy);
|
|
778
|
+
// Doomed slots hold placeholders (vec path: a real argmax value,
|
|
779
|
+
// equally not part of the row's stream) — flag them so the fed
|
|
780
|
+
// accounting at the read stays per-row exact.
|
|
781
|
+
nextReal = rows.map((r) => r.sampled < r.req.maxTokens);
|
|
782
|
+
if (vecOk) {
|
|
783
|
+
const flat = ops.reshape(lg, [B, V]);
|
|
784
|
+
lg.dispose();
|
|
785
|
+
try { nextToks = independentGreedySampling.sample(flat); }
|
|
786
|
+
finally { flat.dispose(); }
|
|
787
|
+
for (const row of rows) if (row.sampled < row.req.maxTokens) row.sampled++;
|
|
788
|
+
} else {
|
|
789
|
+
const sampled: MlxArray[] = [];
|
|
790
|
+
for (let b = 0; b < B; b++) {
|
|
791
|
+
const row = rows[b]!;
|
|
792
|
+
if (row.sampled >= row.req.maxTokens) {
|
|
793
|
+
// Length-doomed row: evicted right after the emission below ever
|
|
794
|
+
// uses this slot as input — placeholder keeps the [B] alignment.
|
|
795
|
+
sampled.push(ops.fromInt32([0], [1]));
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const rl = lg.slice([b, 0, 0], [b + 1, 1, V]);
|
|
799
|
+
const rl2 = ops.reshape(rl, [1, V]);
|
|
800
|
+
rl.dispose();
|
|
801
|
+
sampled.push(row.req.sample!(rl2, row.sampled));
|
|
802
|
+
row.sampled++;
|
|
803
|
+
rl2.dispose();
|
|
804
|
+
}
|
|
805
|
+
lg.dispose();
|
|
806
|
+
nextToks = ops.concatAxis(sampled, 0); // [B]
|
|
807
|
+
for (const t of sampled) t.dispose();
|
|
808
|
+
}
|
|
809
|
+
// dispatch; read NEXT iteration. evalWith: the compiled step's
|
|
810
|
+
// cache-update nodes must ride the same async_eval (generate.ts).
|
|
811
|
+
ops.asyncEvalAll([nextToks, ...evalWith]);
|
|
812
|
+
} finally {
|
|
813
|
+
// Free the step's RoPE arrays; the layouts persist across steps.
|
|
814
|
+
if (fwd) for (const c of fwd) (c as { releaseRopeArr?: () => void }).releaseRopeArr?.();
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
this.#steps++;
|
|
818
|
+
if (this.#steps % 256 === 0) clearCache(); // serial's cadence, not per-step
|
|
819
|
+
if (this.#stepTrace) STEP_T.build += performance.now() - STEP_T.t0;
|
|
820
|
+
|
|
821
|
+
// Read + emit the PREVIOUS step's tokens while the new step computes.
|
|
822
|
+
const prev = this.#pendingToks;
|
|
823
|
+
const prevReal = this.#pendingReal;
|
|
824
|
+
this.#pendingToks = nextToks;
|
|
825
|
+
this.#pendingReal = nextReal;
|
|
826
|
+
// Kill switch / A-B lever (house style):
|
|
827
|
+
// MLX_BUN_BATCH_NO_PIPELINE=1 reads THIS step's tokens synchronously —
|
|
828
|
+
// set from process start `prev` is always null, so the flush below IS the
|
|
829
|
+
// whole phase 2. Same math either way (pipelining is scheduling).
|
|
830
|
+
if (this.#noPipeline) {
|
|
831
|
+
if (this.#running.some(row => row.req.continuation)) this.#captureContinuations();
|
|
832
|
+
await this.#flushPipeline();
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (prev) {
|
|
836
|
+
const tRead = this.#stepTrace ? performance.now() : 0;
|
|
837
|
+
const toks = prev.toIntTokens();
|
|
838
|
+
prev.dispose();
|
|
839
|
+
// These values were the step's forward input (fed) iff a forward ran.
|
|
840
|
+
// Placeholder slots are excluded from `fed` — and taint their row if
|
|
841
|
+
// they ever fed (see Row.fedTainted; structurally unreachable today).
|
|
842
|
+
if (anyLive)
|
|
843
|
+
for (let b = 0; b < B; b++) {
|
|
844
|
+
if (!prevReal || prevReal[b]) rows[b]!.fed.push(toks[b]!);
|
|
845
|
+
else rows[b]!.fedTainted = true;
|
|
846
|
+
}
|
|
847
|
+
if (this.#stepTrace) STEP_T.read += performance.now() - tRead;
|
|
848
|
+
const tEmit = this.#stepTrace ? performance.now() : 0;
|
|
849
|
+
await this.#emitRows(toks); // also filters #pendingToks on eviction
|
|
850
|
+
if (this.#stepTrace) { STEP_T.emit += performance.now() - tEmit; STEP_T.n++; }
|
|
851
|
+
}
|
|
852
|
+
if (this.#running.some(row => row.req.continuation)) this.#captureContinuations();
|
|
853
|
+
if (this.#stepTrace) STEP_T.lastEnd = performance.now();
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
readonly #checkpointAt = new WeakMap<Row, number>();
|
|
857
|
+
/** Cache extraction belongs to this existing ordinary numerical driver; the
|
|
858
|
+
* request's continuation names the cadence and the cache service persists. */
|
|
859
|
+
#captureContinuations(): void {
|
|
860
|
+
if (!this.#pendingToks || !this.#running.some(row => row.req.continuation)) return;
|
|
861
|
+
let pending: number[] | undefined;
|
|
862
|
+
const failed = new Set<number>();
|
|
863
|
+
for (let index = 0; index < this.#running.length; index++) {
|
|
864
|
+
const row = this.#running[index]!, policy = row.req.continuation;
|
|
865
|
+
if (!policy || row.fedTainted || row.fed.length !== row.generated ||
|
|
866
|
+
row.generated < (this.#checkpointAt.get(row) ?? policy.interval) ||
|
|
867
|
+
this.#pendingReal?.[index] === false) continue;
|
|
868
|
+
pending ??= this.#pendingToks.toIntTokens();
|
|
869
|
+
const tokens = [...row.req.promptIds, ...row.fed];
|
|
870
|
+
const caches = this.#running.length === 1 && !row.merged
|
|
871
|
+
? cloneSingleRowState(this.#inners! as Cache[], this.#cache.codecs)
|
|
872
|
+
: this.#extractRowCaches(index, tokens.length);
|
|
873
|
+
if (!caches) continue;
|
|
874
|
+
try {
|
|
875
|
+
this.#cache.checkpoint(row.req, { caches, cacheTokens: tokens, generatedTokens: row.generated,
|
|
876
|
+
pendingToken: pending[index]! });
|
|
877
|
+
this.#checkpointAt.set(row, (Math.floor(row.generated / policy.interval) + 1) * policy.interval);
|
|
878
|
+
} catch (error) { row.reject(error); failed.add(index); }
|
|
879
|
+
}
|
|
880
|
+
if (failed.size) this.#applyFilter(this.#running.flatMap((_, index) => failed.has(index) ? [] : [index]), true);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** Read out the pipeline register (if any): emit its tokens and evict
|
|
884
|
+
* finished rows, leaving the pipeline cold. Called before a join merges. */
|
|
885
|
+
async #flushPipeline(): Promise<void> {
|
|
886
|
+
const prev = this.#pendingToks;
|
|
887
|
+
if (!prev) return;
|
|
888
|
+
this.#pendingToks = null;
|
|
889
|
+
this.#pendingReal = null; // flushed values never fed — nothing to account
|
|
890
|
+
const toks = prev.toIntTokens();
|
|
891
|
+
prev.dispose();
|
|
892
|
+
// Grammar rows: the flushed tokens are EMITTED below, so their matchers
|
|
893
|
+
// must advance here — #stepGrammar's accept only covers tokens it reads
|
|
894
|
+
// from a live pending array, and after this flush it cold-starts (no
|
|
895
|
+
// accept). Skipping this left the matcher one token behind its stream on
|
|
896
|
+
// every mid-decode join → one-step-stale masks → invalid output (found
|
|
897
|
+
// by the feature-matrix conformance gate, 2026-07-03; the fill fired
|
|
898
|
+
// here is awaited by the next #stepGrammar's ready()).
|
|
899
|
+
for (let b = 0; b < this.#running.length && b < toks.length; b++) {
|
|
900
|
+
const g = this.#running[b]!.req.grammar;
|
|
901
|
+
if (g && !g.isTerminated) g.accept(toks[b]!);
|
|
902
|
+
}
|
|
903
|
+
await this.#emitRows(toks);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/** The read-before-build decode step for batches with ≥1 live grammar row
|
|
907
|
+
* (B1). Mirrors the serial loop's grammar resolution: read the previous
|
|
908
|
+
* step's [B] tokens NOW (acceptToken needs JS numbers), accept() per row
|
|
909
|
+
* (fires async fills), build the forward graph (fills overlap), await
|
|
910
|
+
* ready(), sample per live row (the closure applies applyMask), then emit
|
|
911
|
+
* the values read in step 1 — no second readback. Terminated grammar rows
|
|
912
|
+
* keep their [B] slot through the forward (placeholder, not sampled) and
|
|
913
|
+
* finish("stop") in #emitRows. On a cold start (first step after prefill)
|
|
914
|
+
* there is no pending array to read; the prefill already accepted token 0
|
|
915
|
+
* and fired the fill, so we just await ready() + sample. */
|
|
916
|
+
async #stepGrammar(forward?: MlxForwardWork): Promise<void> {
|
|
917
|
+
const rows = this.#running;
|
|
918
|
+
const B = rows.length;
|
|
919
|
+
const inners = this.#inners!;
|
|
920
|
+
|
|
921
|
+
// (1) Read the pending [B] token array (host copy for accept). The device
|
|
922
|
+
// array is kept for the forward input below; disposed after sampling.
|
|
923
|
+
// Cold start: pendingToks null → prevVals empty (prefill handled tok0).
|
|
924
|
+
const prev = this.#pendingToks;
|
|
925
|
+
const prevVals: number[] =
|
|
926
|
+
prev ? prev.toIntTokens() : [];
|
|
927
|
+
if (this.#runtime.value("MLX_BUN_GRAMMAR_DEBUG") === "1")
|
|
928
|
+
console.log(`[sg] B=${B} prevVals=${JSON.stringify(prevVals)} current=${JSON.stringify(rows.map((r) => r.current))} sampled=${JSON.stringify(rows.map((r) => r.sampled))}`);
|
|
929
|
+
|
|
930
|
+
// (2) accept() per live grammar row — fires that row's async bitmask fill.
|
|
931
|
+
for (let b = 0; b < B && prevVals.length; b++) {
|
|
932
|
+
const g = rows[b]!.req.grammar;
|
|
933
|
+
if (g && !g.isTerminated) g.accept(prevVals[b]!);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
let nextToks: MlxArray | null = null;
|
|
937
|
+
let nextReal: boolean[] | null = null;
|
|
938
|
+
const anyLive = rows.some((r) => r.sampled < r.req.maxTokens);
|
|
939
|
+
if (anyLive) {
|
|
940
|
+
// Fed-token accounting (prompt-cache put) — mirror of #step's read:
|
|
941
|
+
// real slots push, a placeholder that fed taints its row.
|
|
942
|
+
const prevReal = this.#pendingReal;
|
|
943
|
+
if (prev)
|
|
944
|
+
for (let b = 0; b < B; b++) {
|
|
945
|
+
if (!prevReal || prevReal[b]) rows[b]!.fed.push(prevVals[b]!);
|
|
946
|
+
else rows[b]!.fedTainted = true;
|
|
947
|
+
}
|
|
948
|
+
else for (const r of rows) r.fed.push(r.current);
|
|
949
|
+
const fwd: Cache[] = inners;
|
|
950
|
+
try {
|
|
951
|
+
// (3) Build the forward graph (host-side; the fills overlap it).
|
|
952
|
+
const ids = prev
|
|
953
|
+
? ops.reshape(prev, [B, 1]) // feed the unread tokens (device array)
|
|
954
|
+
: ops.fromInt32(rows.map((r) => r.current), [B, 1]); // pipeline cold
|
|
955
|
+
const h = await this.#decodeForward(ids, fwd, B, forward);
|
|
956
|
+
ids.dispose();
|
|
957
|
+
const lg = this.#dispatch.logits(B)(h); // [B,1,V]
|
|
958
|
+
h.dispose();
|
|
959
|
+
const V = lg.shape[2]!;
|
|
960
|
+
|
|
961
|
+
// (4) await ready() on every live grammar row — the fills fired in (2)
|
|
962
|
+
// overlapped the graph build above. Usually already resolved.
|
|
963
|
+
const liveGrammar = rows.filter(
|
|
964
|
+
(r) => r.req.grammar && !r.req.grammar.isTerminated,
|
|
965
|
+
);
|
|
966
|
+
if (liveGrammar.length)
|
|
967
|
+
await Promise.all(liveGrammar.map((r) => r.req.grammar!.ready()));
|
|
968
|
+
|
|
969
|
+
// (5) Sample per live row (the closure applies applyMask after the
|
|
970
|
+
// logits processors). Terminated grammar rows + length-doomed rows
|
|
971
|
+
// take a placeholder (one harmless KV write, then evicted).
|
|
972
|
+
const sampled: MlxArray[] = [];
|
|
973
|
+
nextReal = rows.map(
|
|
974
|
+
(r) => r.sampled < r.req.maxTokens && !r.req.grammar?.isTerminated,
|
|
975
|
+
);
|
|
976
|
+
for (let b = 0; b < B; b++) {
|
|
977
|
+
const row = rows[b]!;
|
|
978
|
+
if (
|
|
979
|
+
row.sampled >= row.req.maxTokens ||
|
|
980
|
+
row.req.grammar?.isTerminated
|
|
981
|
+
) {
|
|
982
|
+
sampled.push(ops.fromInt32([0], [1]));
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
const rl = lg.slice([b, 0, 0], [b + 1, 1, V]);
|
|
986
|
+
const rl2 = ops.reshape(rl, [1, V]);
|
|
987
|
+
rl.dispose();
|
|
988
|
+
sampled.push(row.req.sample!(rl2, row.sampled));
|
|
989
|
+
row.sampled++;
|
|
990
|
+
rl2.dispose();
|
|
991
|
+
}
|
|
992
|
+
lg.dispose();
|
|
993
|
+
nextToks = ops.concatAxis(sampled, 0); // [B]
|
|
994
|
+
for (const t of sampled) t.dispose();
|
|
995
|
+
ops.asyncEvalAll([nextToks]);
|
|
996
|
+
} finally {
|
|
997
|
+
for (const c of fwd)
|
|
998
|
+
(c as { releaseRopeArr?: () => void }).releaseRopeArr?.();
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
this.#steps++;
|
|
1002
|
+
if (this.#steps % 256 === 0) clearCache();
|
|
1003
|
+
|
|
1004
|
+
// The device array `prev` has now fed the forward + been read for accept;
|
|
1005
|
+
// dispose it and install the new pending array. On cold start prev is null.
|
|
1006
|
+
prev?.dispose();
|
|
1007
|
+
this.#pendingToks = nextToks;
|
|
1008
|
+
this.#pendingReal = nextReal;
|
|
1009
|
+
|
|
1010
|
+
// (6) Emit the values read in (1) — no second readback. Terminated grammar
|
|
1011
|
+
// rows finish("stop") via #emit's isTerminated check; the filter evicts.
|
|
1012
|
+
// Cold start: prevVals empty (prefill emitted tok0) → nothing to emit.
|
|
1013
|
+
if (prevVals.length) await this.#emitRows(prevVals);
|
|
1014
|
+
if (this.#running.some(row => row.req.continuation)) this.#captureContinuations();
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** Emit one read-back token per running row; evict finished rows. A row's
|
|
1018
|
+
* onToken throwing rejects THAT row and evicts it — siblings continue. */
|
|
1019
|
+
async #emitRows(toks: number[]): Promise<void> {
|
|
1020
|
+
const rows = this.#running;
|
|
1021
|
+
const B = rows.length;
|
|
1022
|
+
const keep: number[] = [];
|
|
1023
|
+
const done: { b: number; row: Row }[] = []; // clean finishes (stop/length)
|
|
1024
|
+
for (let b = 0; b < B; b++) {
|
|
1025
|
+
const row = rows[b]!;
|
|
1026
|
+
row.generated++;
|
|
1027
|
+
let disp: "continue" | "stop" | "length";
|
|
1028
|
+
try {
|
|
1029
|
+
disp = await this.#emit(row, toks[b]!);
|
|
1030
|
+
} catch (e) {
|
|
1031
|
+
row.reject(e); // containment: this row only (mlx-lm `remove`)
|
|
1032
|
+
row.merged = true; // poison: a rejected row is never put() back
|
|
1033
|
+
row.fedTainted = true;
|
|
1034
|
+
continue; // not kept → evicted by the filter below
|
|
1035
|
+
}
|
|
1036
|
+
if (disp === "continue") keep.push(b);
|
|
1037
|
+
else {
|
|
1038
|
+
done.push({ b, row });
|
|
1039
|
+
this.#finish(row, disp);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (keep.length < B) {
|
|
1043
|
+
// Per-row prompt-cache extraction (mlx-lm server.py:864-880 →
|
|
1044
|
+
// BatchGenerator.extract_cache): a MERGED row's KV lives interleaved
|
|
1045
|
+
// in the batched inners, so it is pulled out into fresh serial caches
|
|
1046
|
+
// BEFORE filter() mutates them. Never-merged lone rows skip this —
|
|
1047
|
+
// #applyFilter's keep=[] path put()s their adopted caches zero-copy.
|
|
1048
|
+
// Rejected rows aren't in `done`; the whole-batch error path calls
|
|
1049
|
+
// #applyFilter(dropOnly) directly and never reaches here.
|
|
1050
|
+
for (const { b, row } of done) this.#extractAndPut(b, row);
|
|
1051
|
+
this.#applyFilter(keep);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/** Account one sampled token for a row. Mirrors generate(): EOS terminates
|
|
1056
|
+
* WITHOUT an onToken call; otherwise onToken(token) runs and `false` halts;
|
|
1057
|
+
* reaching maxTokens ends with "length". Advances row.current on continue. */
|
|
1058
|
+
async #emit(row: Row, token: number, logprobs?: import("../generation/index").TokenLogprobs): Promise<"continue" | "stop" | "length"> {
|
|
1059
|
+
row.req.signal?.throwIfAborted();
|
|
1060
|
+
this.#firstOutput(row);
|
|
1061
|
+
if (row.generated === 1) clearCache();
|
|
1062
|
+
if (row.req.eosTokenIds.includes(token)) return "stop";
|
|
1063
|
+
const cont = await row.req.onToken(token, logprobs);
|
|
1064
|
+
if (cont === false) return "stop";
|
|
1065
|
+
// Grammar termination: the matcher is satisfied (e.g. closing `}` accepted).
|
|
1066
|
+
// The final token has been delivered via onToken above; halt with "stop" so
|
|
1067
|
+
// the row finishes + evicts rather than sampling into an all--inf mask.
|
|
1068
|
+
if (row.req.grammar?.isTerminated) return "stop";
|
|
1069
|
+
if (row.generated >= row.req.maxTokens) return "length";
|
|
1070
|
+
row.current = token;
|
|
1071
|
+
return "continue";
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
#firstOutput(row: Row): void {
|
|
1075
|
+
if (row.generated === 1) {
|
|
1076
|
+
if (this.#prefill?.rows.includes(row)) this.#preparationPublishedOutput = true;
|
|
1077
|
+
row.firstTokenAt = performance.now();
|
|
1078
|
+
row.closeTokenZero?.();
|
|
1079
|
+
row.closeTokenZero = undefined;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/** A row completes: resolving it ends its state lease as completed, which
|
|
1084
|
+
* removes a replayable generation's checkpoints. */
|
|
1085
|
+
#finish(row: Row, reason: "stop" | "length"): void {
|
|
1086
|
+
const span = row.decodeSpan;
|
|
1087
|
+
const now = span?.end ?? performance.now();
|
|
1088
|
+
const first = span?.start ?? (row.firstTokenAt || now);
|
|
1089
|
+
row.resolve({
|
|
1090
|
+
...(row.spec ? { spec: row.spec } : {}),
|
|
1091
|
+
...(row.fill ? { fill: row.fill } : {}),
|
|
1092
|
+
promptTokens: row.promptTokens,
|
|
1093
|
+
generatedTokens: row.generated,
|
|
1094
|
+
cachedTokens: row.cachedTokens,
|
|
1095
|
+
finishReason: reason,
|
|
1096
|
+
prefillMs: row.admittedAt ? first - row.admittedAt : 0,
|
|
1097
|
+
decodeMs: span || row.firstTokenAt ? now - first : 0,
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/** Finish-time disposition of `row`'s serial-class caches covering exactly
|
|
1102
|
+
* `tokens`: published to the cache service when it keeps the row's state
|
|
1103
|
+
* and the offset lines up (defensive — a mismatch means the accounting is
|
|
1104
|
+
* wrong and the entry would corrupt future hits), else disposed. `retain`
|
|
1105
|
+
* rides along with the state (runs after dispose). */
|
|
1106
|
+
#putOrDispose(row: Row, caches: Cache[], tokens: number[], retain?: () => void): void {
|
|
1107
|
+
if (this.#cache.keeps(row.req)) {
|
|
1108
|
+
// A never-merged row can still own a row layout. Persistence receives
|
|
1109
|
+
// its existing serial representation, just like merged-row retirement.
|
|
1110
|
+
const extracted: Cache[] = [], replaced: Cache[] = [];
|
|
1111
|
+
let persistent: Cache[];
|
|
1112
|
+
try {
|
|
1113
|
+
persistent = caches.map(cache => {
|
|
1114
|
+
const sole = extractSoleRow(cache);
|
|
1115
|
+
if (!sole) return cache;
|
|
1116
|
+
extracted.push(sole); replaced.push(cache);
|
|
1117
|
+
return sole;
|
|
1118
|
+
});
|
|
1119
|
+
} catch (error) { disposeResources(extracted); throw error; }
|
|
1120
|
+
try { disposeResources(replaced); }
|
|
1121
|
+
catch (error) { disposeResources(extracted); throw error; }
|
|
1122
|
+
caches = persistent;
|
|
1123
|
+
const withOff = caches.find(
|
|
1124
|
+
(c) => typeof (c as { offset?: unknown }).offset === "number",
|
|
1125
|
+
) as { offset: number } | undefined;
|
|
1126
|
+
if (withOff && withOff.offset === tokens.length) {
|
|
1127
|
+
this.#cache.publish(row.req, row.cacheNamespace ?? "", tokens, { caches, retain });
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
for (const c of caches) c.dispose();
|
|
1132
|
+
retain?.();
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/** Extract a finishing MERGED row's KV into fresh serial caches and publish
|
|
1136
|
+
* them keyed by [promptIds + fed] — the batch-lane mirror of mlx-lm
|
|
1137
|
+
* server.py:872 (extract_cache → prompt_cache.insert_cache). Gates: the
|
|
1138
|
+
* cache keeps the row's state, promptTokens >= 256 or an explicit cache
|
|
1139
|
+
* session (the extraction copies), plus an exact coverage key
|
|
1140
|
+
* (!fedTainted). Refusal (#extractRowCaches null) just disposes-by-omission
|
|
1141
|
+
* — the row's KV dies with the filter, exactly the pre-extraction behavior. */
|
|
1142
|
+
#extractAndPut(b: number, row: Row): void {
|
|
1143
|
+
if (!this.#cache.keeps(row.req) || !row.merged || row.fedTainted) return;
|
|
1144
|
+
if (row.promptTokens < 256 && !row.req.cacheSessionId) return;
|
|
1145
|
+
const tokens = [...row.req.promptIds, ...row.fed];
|
|
1146
|
+
const caches = this.#extractRowCaches(b, tokens.length);
|
|
1147
|
+
if (!caches) return;
|
|
1148
|
+
// Materialize the owned copies without stalling the in-flight step (the
|
|
1149
|
+
// slices depend on this step's KV writes): async — the batched source
|
|
1150
|
+
// buffers free once the copies land, instead of being pinned by a lazy
|
|
1151
|
+
// graph inside an idle cache entry.
|
|
1152
|
+
withResource(leaseCacheStates(caches), state => ops.asyncEvalAll([...state]));
|
|
1153
|
+
this.#putOrDispose(row, caches, tokens);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/** Row `b` of every layer as OWNED serial-class caches, or null when a
|
|
1157
|
+
* layer cannot publish the row (then the caller drops the row's KV as
|
|
1158
|
+
* before). Bit-exactness: merge/extend/filter/decode are byte-preserving
|
|
1159
|
+
* per row (`02d723a:tests/parity/batched-decode-parity.test.ts`,
|
|
1160
|
+
* `02d723a:tests/unit/batched-rotating.test.ts`,
|
|
1161
|
+
* packages/inference/tests/state/batched-rotating-quant.test.ts) and each
|
|
1162
|
+
* extract is a pure slice+copy of those bytes
|
|
1163
|
+
* (`02d723a:tests/unit/batched-extract.test.ts`), so an extracted row's bytes ==
|
|
1164
|
+
* the solo run's. A layer that cannot be trimmed (recurrent state) refuses a
|
|
1165
|
+
* row whose own count is not the key's `expectTokens` exactly, defensively:
|
|
1166
|
+
* a mismatched entry would silently corrupt every future exact hit. */
|
|
1167
|
+
#extractRowCaches(b: number, expectTokens: number): Cache[] | null {
|
|
1168
|
+
const out: Cache[] = [];
|
|
1169
|
+
for (const inner of this.#inners!) {
|
|
1170
|
+
// Adopted serial state never coexists with a merged row (defensive).
|
|
1171
|
+
const c = publishRow(inner, b, expectTokens);
|
|
1172
|
+
if (!c) {
|
|
1173
|
+
for (const d of out) d.dispose();
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
out.push(c);
|
|
1177
|
+
}
|
|
1178
|
+
return out;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/** Evict rows not in `keep` (sorted ascending) from the batched KV and the
|
|
1182
|
+
* pipeline register. */
|
|
1183
|
+
#applyFilter(keep: number[], dropOnly = false): void {
|
|
1184
|
+
if (this.#method) {
|
|
1185
|
+
this.#method.filterRows(keep, dropOnly);
|
|
1186
|
+
this.#running = keep.map(row => this.#running[row]!);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
const inners = this.#inners!;
|
|
1190
|
+
if (keep.length === 0) {
|
|
1191
|
+
// Publication: a lone NEVER-MERGED row's inners are
|
|
1192
|
+
// its adopted serial-class caches, covering exactly prompt+fed — hand
|
|
1193
|
+
// them back to the cache instead of disposing. dropOnly (the batch-
|
|
1194
|
+
// drop error path) and poisoned rows dispose as before; MERGED rows'
|
|
1195
|
+
// KV was already extracted per row in #emitRows (owned copies), so
|
|
1196
|
+
// disposing the batched inners here is safe either way.
|
|
1197
|
+
const solo =
|
|
1198
|
+
!dropOnly && this.#running.length === 1 && !this.#running[0]!.merged
|
|
1199
|
+
? this.#running[0]!
|
|
1200
|
+
: null;
|
|
1201
|
+
if (solo) {
|
|
1202
|
+
this.#putOrDispose(solo, inners, [...solo.req.promptIds, ...solo.fed], this.#adoptedRetain ?? undefined);
|
|
1203
|
+
} else {
|
|
1204
|
+
for (const c of inners) c.dispose();
|
|
1205
|
+
this.#adoptedRetain?.();
|
|
1206
|
+
}
|
|
1207
|
+
this.#adoptedRetain = null;
|
|
1208
|
+
this.#inners = null; this.#compiledRow = false;
|
|
1209
|
+
this.#running = [];
|
|
1210
|
+
this.#pendingToks?.dispose();
|
|
1211
|
+
this.#pendingToks = null;
|
|
1212
|
+
this.#pendingReal = null;
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
// Each layout evicts the rows and whatever padding the survivors share
|
|
1216
|
+
// (mlx-lm BatchKVCache.filter); each row's absolute RoPE position stays
|
|
1217
|
+
// unchanged.
|
|
1218
|
+
// Adopted lone-row state exists only at B=1, where the only filter is the
|
|
1219
|
+
// keep=[] dispose-all handled above: filtering serial state is unreachable.
|
|
1220
|
+
for (const inner of inners) filterLayoutRows(inner, keep);
|
|
1221
|
+
this.#running = keep.map((i) => this.#running[i]!);
|
|
1222
|
+
if (this.#pendingToks) {
|
|
1223
|
+
const idx = ops.fromInt32(keep, [keep.length]);
|
|
1224
|
+
const next = ops.takeAxis(this.#pendingToks, idx, 0);
|
|
1225
|
+
idx.dispose();
|
|
1226
|
+
this.#pendingToks.dispose();
|
|
1227
|
+
this.#pendingToks = next;
|
|
1228
|
+
}
|
|
1229
|
+
if (this.#pendingReal) this.#pendingReal = keep.map((i) => this.#pendingReal![i]!);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
#readToken(t: MlxArray): number {
|
|
1233
|
+
const v = t.toIntTokens()[0]!;
|
|
1234
|
+
t.dispose();
|
|
1235
|
+
return v;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
import { BatchRequest,BatchStats,ExclusiveLock,MlxBatchExecutionGroupOptions,MlxGroupedMethod,MlxGroupPreparation,Row } from "./batch-types";
|
|
1240
|
+
export { type BatchRequest,type BatchRequestFields,type BatchStats,type ExclusiveLock,type MlxBatchExecutionGroupOptions,type MlxGroupedMethod,type MlxGroupMethodHost,type MlxGroupMethodRequest,type MlxGroupPreparation,type Row,type RowSampler } from "./batch-types";
|