mlx-bun 0.0.13 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -578
- package/bin/mlx-bun.mjs +1 -1
- package/docs/README.md +91 -0
- package/docs/{design → archive}/hlg-sampling.md +3 -3
- package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
- package/docs/{design → archive/investigations}/colibri-glm52-port.md +1 -1
- package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
- package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
- package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
- package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
- package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
- package/docs/{planning → archive/planning}/chunk-task-roadmap.md +1 -1
- package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +4 -4
- package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +1 -1
- package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
- package/docs/archive/planning/release-notes-v0.2.0.md +49 -0
- package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
- package/docs/design/batching.md +569 -0
- package/docs/design/decode-speed-program.md +3178 -169
- package/docs/design/dreaming-nightly-pipeline.md +675 -282
- package/docs/design/generic-model-support.md +579 -334
- package/docs/design/kv-cache.md +680 -0
- package/docs/design/orpo-training.md +673 -1239
- package/docs/design/speculative-decoding.md +1627 -0
- package/docs/design/turboquant.md +2102 -0
- package/docs/design/unified-engine-frontier-plan.md +1831 -727
- package/docs/design/web-chat-redesign.md +666 -1083
- package/docs/planning/PRODUCT_ROADMAP.md +34 -1
- package/docs/planning/release-notes-v0.3.0.md +35 -0
- package/docs/planning/video-series.md +421 -0
- package/docs/reference/benchmarks.md +1162 -0
- package/docs/reference/cli.md +395 -149
- package/docs/reference/distribution.md +321 -181
- package/docs/reference/environment.md +348 -0
- package/docs/reference/glossary.md +39 -0
- package/docs/reference/library-api.md +580 -142
- package/docs/reference/memory.md +227 -64
- package/docs/reference/models.md +137 -6
- package/docs/reference/server-api.md +938 -422
- package/docs/reference/server-config.md +652 -320
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +166 -0
- package/package.json +10 -5
- package/src/admission-plan.ts +18 -0
- package/src/anthropic.ts +92 -3
- package/src/audio/decode.ts +2 -2
- package/src/audio/features.ts +2 -2
- package/src/backends/mlx/autoregressive.ts +108 -0
- package/src/backends/mlx/batch-group.ts +1646 -0
- package/src/backends/mlx/diffusion.ts +39 -0
- package/src/backends/mlx/draft-target.ts +47 -0
- package/src/backends/mlx/evaluation.ts +105 -0
- package/src/backends/mlx/fill-append.ts +45 -0
- package/src/backends/mlx/gateway-binding.ts +71 -0
- package/src/backends/mlx/graph.ts +38 -0
- package/src/backends/mlx/memory-client.ts +189 -0
- package/src/backends/mlx/memory-guard.ts +40 -0
- package/src/backends/mlx/methods.ts +90 -0
- package/src/backends/mlx/model-prompt.ts +164 -0
- package/src/backends/mlx/model-serving.ts +79 -0
- package/src/backends/mlx/prefill.ts +33 -0
- package/src/backends/mlx/request-policy.ts +20 -0
- package/src/backends/mlx/rollback.ts +51 -0
- package/src/backends/mlx/serial-executor.ts +273 -0
- package/src/backends/mlx/speculative.ts +114 -0
- package/src/backends/mlx/state-views.ts +26 -0
- package/src/backends/mlx/text-engine.ts +73 -0
- package/src/cli.ts +189 -61
- package/src/client.ts +66 -0
- package/src/config.ts +21 -4
- package/src/contracts/completion.ts +16 -0
- package/src/contracts/execution.ts +49 -0
- package/src/contracts/generation.ts +103 -0
- package/src/contracts/host.ts +7 -0
- package/src/contracts/jobs.ts +66 -0
- package/src/contracts/pi-provider.ts +30 -0
- package/src/contracts/pi.ts +259 -0
- package/src/contracts/resources.ts +18 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/contracts/tsconfig.json +14 -0
- package/src/diffusion/diffusion-generate.ts +218 -153
- package/src/embed.ts +1 -1
- package/src/engine/admission.ts +59 -0
- package/src/engine/cancellation.ts +38 -0
- package/src/engine/engine.ts +56 -0
- package/src/engine/execution-coordinator.ts +70 -0
- package/src/engine/execution-plan.ts +51 -0
- package/src/engine/resources.ts +42 -0
- package/src/engine/scheduler.ts +67 -0
- package/src/engine/session.ts +247 -0
- package/src/eval/README.md +44 -1
- package/src/eval/kl.ts +263 -23
- package/src/eval/reference-logits.ts +207 -0
- package/src/eval/runner.ts +10 -79
- package/src/eval/tasks/bfcl.ts +2 -1
- package/src/eval/tasks/gsm8k.ts +2 -1
- package/src/eval/tasks/hashhop.ts +2 -1
- package/src/eval/tasks/humaneval.ts +5 -4
- package/src/eval/tasks/ifeval.ts +2 -1
- package/src/eval/tasks/mmlu.ts +2 -1
- package/src/expert-io.ts +3 -1
- package/src/expert-offload.ts +3 -1
- package/src/fill/echo-index.ts +188 -0
- package/src/fill/fill-session.ts +452 -0
- package/src/fill/proposal.ts +68 -0
- package/src/fill/schema-rows.ts +506 -0
- package/src/fill/tool-boundary.ts +103 -0
- package/src/fit.ts +24 -43
- package/src/generate.ts +740 -337
- package/src/grammar.ts +14 -8
- package/src/harness-pi.ts +15 -34
- package/src/hub-rest.ts +4 -4
- package/src/index.ts +27 -0
- package/src/inference/denoising.ts +17 -0
- package/src/inference/graph.ts +33 -0
- package/src/inference/prefill.ts +39 -0
- package/src/inference/rollback.ts +7 -0
- package/src/jobs/db.ts +4 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-entry.ts +15 -14
- package/src/jobs/runner.ts +98 -13
- package/src/jobs/task-client.ts +19 -0
- package/src/jobs/types.ts +2 -58
- package/src/kv-scheme.ts +199 -0
- package/src/kv-store.ts +459 -234
- package/src/lab/README.md +18 -0
- package/src/{assets → lab/curve}/curve-designer.html +2 -2
- package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
- package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +7 -5
- package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
- package/src/library.ts +40 -0
- package/src/lora.ts +18 -0
- package/src/media-fetch.ts +20 -4
- package/src/memory/article.ts +1 -1
- package/src/memory/chunk.ts +1 -1
- package/src/memory/model.ts +32 -170
- package/src/memory/prompts.ts +1 -1
- package/src/memory/rest.ts +1 -1
- package/src/memory/schedule.ts +1 -1
- package/src/memory/stages.ts +4 -1
- package/src/memory/vault.ts +3 -4
- package/src/mlx/array.ts +9 -10
- package/src/mlx/compile.ts +12 -2
- package/src/mlx/ffi.ts +66 -22
- package/src/mlx/materialize.ts +23 -0
- package/src/mlx/metal-kernel.ts +10 -11
- package/src/mlx/ops.ts +64 -19
- package/src/mlx/quantized-rows.ts +36 -0
- package/src/mlx/token-bitmask.ts +52 -0
- package/src/mlx/turboquant-kv-decode.ts +82 -0
- package/src/mlx/turboquant-kv-inverse.ts +122 -0
- package/src/mlx/turboquant-ops.ts +33 -3
- package/src/mlx/turboquant-tables.ts +2 -2
- package/src/model/batched-mask.ts +9 -3
- package/src/model/batched-quant.ts +2 -2
- package/src/model/batched-rotating-quant.ts +45 -99
- package/src/model/batched-rotating-state.ts +73 -0
- package/src/model/batched-rotating.ts +57 -106
- package/src/model/batched-row-storage.ts +127 -0
- package/src/model/compiled-decode.ts +4 -3
- package/src/model/diffusion-gemma.ts +8 -8
- package/src/model/factory.ts +87 -49
- package/src/model/fingerprint.ts +1 -1
- package/src/model/flash-attention.ts +3 -3
- package/src/model/gemma4-base.ts +187 -17
- package/src/model/gemma4.ts +6 -6
- package/src/model/generated/gemma4-12b.ts +52 -148
- package/src/model/generated/gemma4-26b.ts +34 -94
- package/src/model/generated/gemma4-e4b.ts +28 -76
- package/src/model/glm52-cache.ts +19 -9
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +28 -1
- package/src/model/implementation.ts +69 -0
- package/src/model/minicpm5.ts +7 -8
- package/src/model/profile.ts +443 -0
- package/src/model/qwen-append.ts +12 -0
- package/src/model/qwen-conv.ts +52 -0
- package/src/model/qwen3-delta.ts +98 -2
- package/src/model/qwen3-mrope.ts +252 -0
- package/src/model/qwen3_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +336 -60
- package/src/model/support.ts +1 -1
- package/src/model/trellis-balanced-scatter.ts +51 -0
- package/src/model/trellis-linear.ts +672 -0
- package/src/model/trellis-shared-m.ts +151 -0
- package/src/model/trellis-shared-scatter.ts +144 -0
- package/src/model/trellis-splitk-prefill.ts +158 -0
- package/src/model/trellis-tiled-prefill.ts +136 -0
- package/src/model/trellis-vector-expand.ts +62 -0
- package/src/model/trellis-wide-prefill.ts +94 -0
- package/src/model/universal/dense.ts +1 -1
- package/src/model/universal/modules.ts +8 -1
- package/src/native/frame_extract.swift +140 -0
- package/src/native-pack.ts +15 -7
- package/src/pi-provider.ts +6 -64
- package/src/pi-web.ts +14 -320
- package/src/prompt-cache.ts +97 -54
- package/src/quantize/allocator.ts +1 -1
- package/src/quantize/atomic-output.ts +38 -0
- package/src/quantize/calibration.ts +2 -1
- package/src/quantize/config-writer.ts +16 -3
- package/src/quantize/index.ts +28 -1
- package/src/quantize/job.ts +11 -0
- package/src/quantize/quantizer.ts +265 -100
- package/src/quantize/rotate.ts +603 -0
- package/src/quantize/safetensors-writer.ts +79 -1
- package/src/quantize/sensitivity.ts +41 -59
- package/src/quantize/trellis.ts +586 -0
- package/src/quantize/weight-transform.ts +292 -0
- package/src/responses.ts +130 -3
- package/src/runtime-config.ts +81 -0
- package/src/sampler.ts +276 -4
- package/src/serve/admin-routes.ts +184 -0
- package/src/serve/aux-routes.ts +132 -0
- package/src/serve/batch-scheduler.ts +8 -1558
- package/src/serve/chat-request.ts +391 -0
- package/src/serve/chat-stage.ts +195 -0
- package/src/serve/checkpoint-identity.ts +64 -0
- package/src/serve/completion-executor.ts +354 -0
- package/src/serve/completion-sink.ts +157 -0
- package/src/serve/discovery-routes.ts +204 -0
- package/src/serve/generation-gateway.ts +211 -243
- package/src/serve/http.ts +159 -0
- package/src/serve/inference-request.ts +79 -0
- package/src/serve/isolate.ts +297 -105
- package/src/serve/kv-budget.ts +14 -0
- package/src/serve/lab-routes.ts +341 -0
- package/src/serve/lane-registry.ts +2 -1
- package/src/serve/model-admin-routes.ts +155 -0
- package/src/serve/model-binding.ts +43 -0
- package/src/serve/model-host.ts +512 -0
- package/src/serve/openai-wire.ts +103 -0
- package/src/serve/parent-application.ts +56 -0
- package/src/serve/pipeline.ts +33 -0
- package/src/serve/preparation.ts +33 -0
- package/src/serve/prompt-response-trace.ts +147 -0
- package/src/serve/request-plan.ts +162 -0
- package/src/serve/request-prep.ts +346 -0
- package/src/serve/response-lease.ts +24 -0
- package/src/serve/responses-client.ts +69 -0
- package/src/serve/session-completion-engine.ts +110 -0
- package/src/serve/session-search.ts +1 -1
- package/src/serve/static-routes.ts +101 -0
- package/src/serve/text-completion-stage.ts +104 -0
- package/src/serve/token-streams.ts +519 -0
- package/src/server.ts +523 -3551
- package/src/spec/assistant-source.ts +10 -44
- package/src/spec/deepspec-source.ts +6 -6
- package/src/spec/dflash-source.ts +9 -8
- package/src/spec/dspark/ab-stats.ts +3 -3
- package/src/spec/dspark/calibration.ts +2 -2
- package/src/spec/dspark/deepspec-module.ts +2 -2
- package/src/spec/dspark/generate-dflash.ts +2 -2
- package/src/spec/dspark/generate.ts +2 -2
- package/src/spec/dspark/loader.ts +1 -1
- package/src/spec/dspark/module-dflash.ts +5 -4
- package/src/spec/dspark/module.ts +3 -3
- package/src/spec/dspark/quantize-drafter.ts +1 -1
- package/src/spec/generate.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +9 -1
- package/src/spec/ngram-source.ts +1 -1
- package/src/spec/prefix-state.ts +45 -0
- package/src/spec/qwen-mtp-source.ts +528 -0
- package/src/spec/serve-loop.ts +179 -213
- package/src/spec/source.ts +42 -10
- package/src/spec/two-model.ts +1 -1
- package/src/ssd-cache.ts +95 -24
- package/src/ssd-durability.ts +207 -0
- package/src/tool-call.ts +51 -14
- package/src/train/flash-cce.ts +14 -13
- package/src/train/forward.ts +38 -1
- package/src/train/loss.ts +3 -2
- package/src/train/prefix-shared.ts +5 -1
- package/src/train/segmented.ts +9 -6
- package/src/train/trainer.ts +9 -8
- package/src/vision/preprocess.ts +1 -1
- package/src/vision/prompt.ts +48 -2
- package/src/vision/qwen3vl-preprocess.ts +409 -0
- package/src/vision/qwen3vl-prompt.ts +196 -0
- package/src/vision/qwen3vl-tower.ts +555 -0
- package/src/vision/video-frames.ts +148 -0
- package/src/web/app.html +1 -1
- package/src/web/app.js +15 -6
- package/src/web/src/adapters-panel.ts +3 -3
- package/src/web/src/assistant.ts +2 -36
- package/src/web/src/chat.ts +16 -9
- package/src/web/src/composer.ts +4 -4
- package/src/web/src/hub.ts +1 -1
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +1 -1
- package/src/web/src/model-picker.ts +1 -1
- package/src/web/src/palette.ts +1 -1
- package/src/web/src/protocol.ts +3 -39
- package/src/web/src/sessions.ts +1 -1
- package/src/web/src/ui-catalog.ts +4 -18
- package/src/web-assets.ts +47 -0
- package/src/weights.ts +134 -33
- package/docs/archive/mlx-bun-lab-report.html +0 -275
- package/docs/archive/wwdc-mlx-bun.html +0 -848
- package/docs/design/adapters-end-to-end.md +0 -205
- package/docs/design/batching-perf-path.md +0 -255
- package/docs/design/batching-v2-plan.md +0 -529
- package/docs/design/bucketing-stage.md +0 -197
- package/docs/design/compat-cli-surface-design.md +0 -230
- package/docs/design/docs-reorg-plan.md +0 -165
- package/docs/design/dspark-serving-program.md +0 -371
- package/docs/design/dspark-speculative-decoding.md +0 -268
- package/docs/design/grammar-spec-batching-integration.md +0 -301
- package/docs/design/memory-inference-path.md +0 -422
- package/docs/design/memory-synthesis.md +0 -287
- package/docs/design/memory-system.md +0 -237
- package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
- package/docs/design/omlx-adoption-map.md +0 -98
- package/docs/design/orpo-dynamic-lambda.md +0 -220
- package/docs/design/orpo-future-enhancements.md +0 -340
- package/docs/design/paged-kv-cache.md +0 -214
- package/docs/design/parallel-slots.md +0 -567
- package/docs/design/parity-tier-dag.md +0 -161
- package/docs/design/repo-cleanup-plan.md +0 -189
- package/docs/design/runtime-isolation.md +0 -263
- package/docs/design/segmented-backward-training.md +0 -543
- package/docs/design/spec-decode-larger-targets.md +0 -229
- package/docs/design/ssd-kv-cold-tier.md +0 -288
- package/docs/design/structured-output.md +0 -484
- package/docs/design/tauri-desktop-app.md +0 -127
- package/docs/design/the-dreaming-master-plan.md +0 -445
- package/docs/design/turboquant-kv.md +0 -235
- package/docs/design/w4a16-compute-precision-spike.md +0 -232
- package/docs/design/web-chat-beat-matrix.md +0 -315
- package/docs/design/write-pipeline-entity.md +0 -468
- package/docs/investigations/curve-runs/bisector-route.json +0 -98
- package/docs/investigations/curve-runs/bisector-trace.json +0 -1393
- package/docs/investigations/curve-runs/curve-terrain.json +0 -1179
- package/docs/investigations/curve-runs/curve-terrain.svg +0 -42
- package/docs/investigations/hlg-ab.html +0 -34
- package/docs/investigations/hlg-explorer.html +0 -238
- package/docs/investigations/hlg-figs/cliff.svg +0 -1
- package/docs/investigations/hlg-figs/cross.svg +0 -1
- package/docs/investigations/hlg-figs/edges.svg +0 -1
- package/docs/investigations/hlg-figs/k20-grids.svg +0 -1
- package/docs/investigations/hlg-figs/k20-peaks.svg +0 -1
- package/docs/investigations/hlg-figs/lw-curve.svg +0 -1
- package/docs/investigations/hlg-figs/terrain.svg +0 -1
- package/docs/investigations/hlg-report.html +0 -262
- package/docs/investigations/hlg-runs/hlg-ab.json +0 -149
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
- package/docs/investigations/hlg-runs/hlg-blind.json +0 -107
- package/docs/investigations/hlg-runs/hlg-cliff.json +0 -197
- package/docs/investigations/hlg-runs/hlg-cross.json +0 -505
- package/docs/investigations/hlg-runs/hlg-edge.json +0 -388
- package/docs/investigations/hlg-runs/hlg-eval1.json +0 -25
- package/docs/investigations/hlg-runs/hlg-frontier.json +0 -86
- package/docs/investigations/hlg-runs/hlg-grid.json +0 -242
- package/docs/investigations/hlg-runs/hlg-grid2.json +0 -343
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
- package/docs/investigations/hlg-runs/hlg-map.json +0 -383
- package/docs/investigations/hlg-runs/run0-original.log +0 -159
- package/docs/investigations/hlg-runs/runA-recommended.log +0 -131
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +0 -8
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +0 -16
- package/docs/investigations/hlg-runs/runB-gain-variants.log +0 -192
- package/docs/investigations/hlg-runs/runC-pivots.log +0 -162
- package/docs/investigations/hlg-runs/runD-oetf.log +0 -132
- package/docs/investigations/hlg-runs/runE-eotf.log +0 -162
- package/docs/investigations/hlg-runs/runF-shaper-working.log +0 -86
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
- package/docs/investigations/hlg-runs/runH-wsweep.log +0 -176
- package/docs/investigations/hlg-runs/runI-asweep.log +0 -176
- package/docs/investigations/hlg-runs/runJ-knobmap.log +0 -49
- package/docs/investigations/hlg-runs/runK-frontier.log +0 -19
- package/docs/investigations/hlg-runs/runL-widemap.log +0 -65
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +0 -15
- package/docs/investigations/hlg-runs/runN-verify.log +0 -116
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +0 -79
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +0 -33
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
- package/docs/investigations/hlg-runs/runU-cross-12b.log +0 -33
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +0 -22
- package/docs/investigations/hlg-runs/runW-lw-12b.log +0 -22
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
- package/docs/planning/IDEAS.md +0 -69
- package/docs/reference/embedding.md +0 -197
- package/docs/reference/features-matrix.md +0 -100
- package/docs/reference/orpo-quickstart.md +0 -135
- package/src/flags.ts +0 -13
- /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
- /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
- /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
- /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
- /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
- /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
- /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
- /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
- /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
- /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
- /package/docs/{planning → archive/planning}/journal.md +0 -0
- /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
- /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
- /package/{docs/investigations → src/serve/assets}/curve-terrain.html +0 -0
- /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
package/src/generate.ts
CHANGED
|
@@ -4,34 +4,74 @@
|
|
|
4
4
|
// - decode pipelining via mx.async_eval: step n+1's graph is built and
|
|
5
5
|
// dispatched before step n's token is read back, so the GPU never idles
|
|
6
6
|
// on the JS round-trip
|
|
7
|
+
// - opt-in early token-zero yield reduces latency before that pipeline starts
|
|
7
8
|
// - sampling stays on-device; only the chosen token id crosses to JS
|
|
8
9
|
|
|
10
|
+
import { appendFileSync } from "node:fs";
|
|
9
11
|
import { MlxArray, gpuStream } from "./mlx/array";
|
|
10
12
|
import {
|
|
13
|
+
activeMemory,
|
|
11
14
|
clearCache,
|
|
12
15
|
maxRecommendedWorkingSetSize,
|
|
16
|
+
peakMemory,
|
|
13
17
|
setWiredLimit,
|
|
14
18
|
synchronize,
|
|
15
19
|
} from "./mlx/ffi";
|
|
16
20
|
import * as ops from "./mlx/ops";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { PagedKVCache } from "./model/paged-kv";
|
|
21
|
+
import { KVCache, RotatingKVCache, TurboQuantKVCache, type Cache } from "./model/gemma4";
|
|
22
|
+
import { PagedKVCache } from "./lab/paged-kv/paged-kv";
|
|
20
23
|
import { DiffusionGemmaModel } from "./model/diffusion-gemma";
|
|
21
|
-
import {
|
|
24
|
+
import { denoiseAsync } from "./diffusion/diffusion-generate";
|
|
25
|
+
import { bindLegacyDenoisingModel, type MlxDenoisingBinding } from "./backends/mlx/diffusion";
|
|
22
26
|
import type { RuntimeModel } from "./model/factory";
|
|
27
|
+
import {
|
|
28
|
+
assertMlxAutoregressiveBinding, bindLegacyAutoregressiveModel,
|
|
29
|
+
type MlxAutoregressiveBinding, type MlxModelMemory, type MlxDecodeStep, type MlxTokenAppend,
|
|
30
|
+
} from "./backends/mlx/autoregressive";
|
|
31
|
+
import { nextPrefillStep } from "./inference/prefill";
|
|
32
|
+
import { evalCacheState, executeMlxPrefillStep } from "./backends/mlx/prefill";
|
|
33
|
+
import { appendFillHidden } from "./backends/mlx/fill-append";
|
|
34
|
+
export { evalCacheState } from "./backends/mlx/prefill";
|
|
35
|
+
import type { TokenLogprobs } from "./contracts/generation";
|
|
36
|
+
export type { TokenLogprobs } from "./contracts/generation";
|
|
37
|
+
import type { PromptResponseTrace } from "./serve/prompt-response-trace";
|
|
23
38
|
import type { KvQuantSpec, TurboQuantScheme } from "./config";
|
|
24
|
-
import {
|
|
39
|
+
import { runtimeConfig, runtimeValue, withRuntimeConfig, type RuntimeConfig } from "./runtime-config";
|
|
25
40
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
41
|
+
disposeStepExtras,
|
|
42
|
+
makeStepSampler,
|
|
43
|
+
stepExtrasArrays,
|
|
44
|
+
type LogitsProcessorOptions,
|
|
45
|
+
type SamplerOptions,
|
|
46
|
+
type StepExtras,
|
|
28
47
|
} from "./sampler";
|
|
29
48
|
import type { GrammarController } from "./grammar";
|
|
49
|
+
import {
|
|
50
|
+
fillTraceEnabled,
|
|
51
|
+
fillTracePath,
|
|
52
|
+
type FillTraceRecord,
|
|
53
|
+
resolveFillMode,
|
|
54
|
+
type FillSession,
|
|
55
|
+
type FillStats,
|
|
56
|
+
type Proposal,
|
|
57
|
+
} from "./fill/fill-session";
|
|
30
58
|
|
|
31
59
|
export interface GenerateOptions extends SamplerOptions, LogitsProcessorOptions {
|
|
60
|
+
/** RAM budget for a model-owned paired target/draft prompt snapshot. */
|
|
61
|
+
speculativeCacheBytes?: number;
|
|
62
|
+
/** Resolved host policy. Direct compatibility calls resolve from their
|
|
63
|
+
* captured binding when this is absent. */
|
|
64
|
+
decodePolicy?: Readonly<Pick<import("./contracts/execution").ResolvedExecution, "compiledDecode" | "grammarJump">>;
|
|
65
|
+
/** Cooperatively cancel at prefill/decode boundaries; pending native work
|
|
66
|
+
* finishes before owned resources are released. */
|
|
67
|
+
signal?: AbortSignal;
|
|
32
68
|
maxTokens?: number;
|
|
33
69
|
eosTokenIds?: number[];
|
|
34
70
|
prefillChunkSize?: number;
|
|
71
|
+
/** Whether the request supplied its seed explicitly. Durable generation
|
|
72
|
+
* checkpoints use this to distinguish a reproducible user policy from the
|
|
73
|
+
* server-generated seed that must be recovered from checkpoint metadata. */
|
|
74
|
+
seedWasExplicit?: boolean;
|
|
35
75
|
/** Fired once during prefill, at `snapshotAt` tokens (default: the full
|
|
36
76
|
* prompt) and BEFORE any further KV is written — the one moment the
|
|
37
77
|
* caches hold exactly that prefix (post-wrap rings can't be rewound
|
|
@@ -43,6 +83,25 @@ export interface GenerateOptions extends SamplerOptions, LogitsProcessorOptions
|
|
|
43
83
|
* there would make the entry untrimmable-and-divergent). Prefill is
|
|
44
84
|
* split at this index when it falls mid-prompt. */
|
|
45
85
|
snapshotAt?: number;
|
|
86
|
+
/** Resume an interrupted autoregressive generation from a durable KV
|
|
87
|
+
* checkpoint. `cache` must cover every token in `promptTokens` exactly;
|
|
88
|
+
* `initialPendingToken` is the already-sampled next token, so no cache
|
|
89
|
+
* rewind is required (important for recurrent/untrimmable layers). */
|
|
90
|
+
initialPendingToken?: number;
|
|
91
|
+
/** Number of completion tokens already emitted before this resumed run. */
|
|
92
|
+
initialGeneratedTokens?: number;
|
|
93
|
+
/** Original request prompt length used for protocol usage accounting when
|
|
94
|
+
* `promptTokens` also contains replayed completion tokens. */
|
|
95
|
+
originalPromptTokens?: number;
|
|
96
|
+
/** Periodic, safe decode-boundary checkpoint hook. The cache covers
|
|
97
|
+
* `cacheTokens`; `pendingToken` has been sampled but not emitted. */
|
|
98
|
+
checkpointEveryTokens?: number;
|
|
99
|
+
onDecodeCheckpoint?: (state: {
|
|
100
|
+
cacheTokens: number[];
|
|
101
|
+
caches: Cache[];
|
|
102
|
+
generatedTokens: number;
|
|
103
|
+
pendingToken: number;
|
|
104
|
+
}) => void | Promise<void>;
|
|
46
105
|
/** Pre-warmed KV caches (e.g. from the prompt cache). cache[0].offset
|
|
47
106
|
* prompt tokens are treated as already prefilled; only the suffix is
|
|
48
107
|
* forwarded. Caller keeps ownership — generate() will not dispose. */
|
|
@@ -53,7 +112,7 @@ export interface GenerateOptions extends SamplerOptions, LogitsProcessorOptions
|
|
|
53
112
|
promptEmbeddings?: MlxArray;
|
|
54
113
|
/** bool [L] marking image tokens (bidirectional attention among them).
|
|
55
114
|
* MUST be unset when the prompt contains any audio — audio prompts run
|
|
56
|
-
* fully causal (
|
|
115
|
+
* fully causal (docs/design/generic-model-support.md §3.3 Q1). */
|
|
57
116
|
imageMask?: MlxArray;
|
|
58
117
|
/** bool [L] marking ALL multimodal soft tokens (image | audio) for
|
|
59
118
|
* per-layer-input id zeroing (e2b/e4b), decoupled from imageMask so
|
|
@@ -74,7 +133,7 @@ export interface GenerateOptions extends SamplerOptions, LogitsProcessorOptions
|
|
|
74
133
|
/** Convert once a cache's offset reaches this (uniform-kvBits default
|
|
75
134
|
* 5000 = mlx-lm; kvConfig default 0 = optiq serve). */
|
|
76
135
|
quantizedKvStart?: number;
|
|
77
|
-
/** TurboQuant scheme (docs/design/turboquant
|
|
136
|
+
/** TurboQuant scheme (docs/design/turboquant.md): rotation-based KV
|
|
78
137
|
* quantization, a CLI-only runtime lever in the same class as uniform
|
|
79
138
|
* kvBits (mutually exclusive with kvBits/kvConfig — maybeQuantizeKv
|
|
80
139
|
* checks kvBits/kvConfig first, so set at most one). Full-attention
|
|
@@ -82,7 +141,7 @@ export interface GenerateOptions extends SamplerOptions, LogitsProcessorOptions
|
|
|
82
141
|
* RotatingKVCache (sliding-window) layers stay bf16 in v1 — a one-time
|
|
83
142
|
* warning names the limitation, never a throw. */
|
|
84
143
|
turboQuant?: TurboQuantScheme;
|
|
85
|
-
/** OPTIONAL paged KV storage (docs/design/
|
|
144
|
+
/** OPTIONAL paged KV storage (docs/design/kv-cache.md): fresh
|
|
86
145
|
* full-attention KVCache layers are replaced with PagedKVCache (block
|
|
87
146
|
* pool + gather-to-contiguous) before prefill. v1 scope: serial batch=1
|
|
88
147
|
* Gemma4-family, bf16 — mutually exclusive with kvBits/kvConfig/
|
|
@@ -115,45 +174,60 @@ export interface GenerateOptions extends SamplerOptions, LogitsProcessorOptions
|
|
|
115
174
|
* advances the matcher, and awaits the async mask precompute (which overlaps
|
|
116
175
|
* the GPU forward). Non-grammar requests keep the fast pipelined loop. */
|
|
117
176
|
grammar?: GrammarController;
|
|
177
|
+
/** Token fast-forwarding (src/fill/, docs/design/speculative-decoding.md
|
|
178
|
+
* "Token fast-forwarding"): a per-request table of DETERMINED token spans
|
|
179
|
+
* (compiled from the request's `tools` + the chat template). When the
|
|
180
|
+
* stream enters one, the engine appends the whole span itself with ONE
|
|
181
|
+
* multi-token forward and resumes sampling after it.
|
|
182
|
+
*
|
|
183
|
+
* This is context extension, NOT speculation: nothing is drafted, verified,
|
|
184
|
+
* or rolled back, and no comparison is made against what the model would
|
|
185
|
+
* have produced. Injection bypasses the sampler, which is a documented
|
|
186
|
+
* behavior-policy deviation at temperature > 0 — hence opt-in
|
|
187
|
+
* (MLX_BUN_FILL=strict) and the composition refusals in shouldUseFill.
|
|
188
|
+
* Serial lane only: the batch scheduler and the spec loop never read it. */
|
|
189
|
+
fill?: FillSession;
|
|
118
190
|
}
|
|
119
191
|
|
|
120
192
|
export function shouldUseGrammarJump(
|
|
121
193
|
options: Pick<GenerateOptions, "grammar" | "logprobs" | "topLogprobs">,
|
|
194
|
+
runtime: RuntimeConfig = runtimeConfig(),
|
|
122
195
|
): boolean {
|
|
123
196
|
return options.grammar !== undefined &&
|
|
124
|
-
|
|
197
|
+
runtime.flag("MLX_BUN_GRAMMAR_JUMP", false) &&
|
|
125
198
|
!options.logprobs &&
|
|
126
199
|
!(options.topLogprobs && options.topLogprobs > 0);
|
|
127
200
|
}
|
|
128
201
|
|
|
129
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
*
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
202
|
+
/** Composition gate for token fast-forwarding, enforced inside the engine
|
|
203
|
+
* (the serve layer refuses more shapes it alone can see — a user-fixed seed,
|
|
204
|
+
* a mounted draft model, continuous placement: src/serve/request-prep.ts,
|
|
205
|
+
* src/serve/chat-stage.ts). Throws when MLX_BUN_FILL names the unbuilt echo
|
|
206
|
+
* tier — an operator who asked for K3c must not get K3b silently.
|
|
207
|
+
* - grammar: forced-token production is the grammar's job; two producers in
|
|
208
|
+
* one iteration is forbidden (asserted in the loop).
|
|
209
|
+
* - logprobs/top_logprobs: injected tokens are never sampled, so they have
|
|
210
|
+
* no distribution row (same rule as shouldUseGrammarJump).
|
|
211
|
+
* - promptEmbeddings: vision/audio prompts (and mRoPE) — untested shape.
|
|
212
|
+
* - kvBits/kvConfig/turboQuant: post-conversion multi-token append is
|
|
213
|
+
* L-generic but unvalidated; refuse in v1 rather than pay it silently. */
|
|
214
|
+
export function shouldUseFill(
|
|
215
|
+
options: Pick<
|
|
216
|
+
GenerateOptions,
|
|
217
|
+
"fill" | "grammar" | "logprobs" | "topLogprobs" | "promptEmbeddings"
|
|
218
|
+
| "kvBits" | "kvConfig" | "turboQuant"
|
|
219
|
+
>,
|
|
220
|
+
runtime: RuntimeConfig = runtimeConfig(),
|
|
221
|
+
): boolean {
|
|
222
|
+
if (!options.fill) return false;
|
|
223
|
+
if (resolveFillMode(runtime.value("MLX_BUN_FILL") ?? "off") === "off") return false;
|
|
224
|
+
return options.grammar === undefined &&
|
|
225
|
+
!options.logprobs &&
|
|
226
|
+
!(options.topLogprobs && options.topLogprobs > 0) &&
|
|
227
|
+
options.promptEmbeddings === undefined &&
|
|
228
|
+
!options.kvBits &&
|
|
229
|
+
!options.kvConfig?.length &&
|
|
230
|
+
options.turboQuant === undefined;
|
|
157
231
|
}
|
|
158
232
|
|
|
159
233
|
/** Port of mlx-lm maybe_quantize_kv_cache + BOTH halves of optiq serve's
|
|
@@ -172,7 +246,7 @@ export function evalCacheState(cache: Cache[]): void {
|
|
|
172
246
|
* layers' bf16 K/V as graph inputs alongside ALL quantized outputs —
|
|
173
247
|
* the exact transient optiq's fix kills (16.35 → 7.60 GB at 32k on a
|
|
174
248
|
* 24 GB Mac). Numerics untouched: same quantize math, only the eval
|
|
175
|
-
* ordering is forced (tests/kv-quant.test.ts, tests/rotating-kvq.test.ts). */
|
|
249
|
+
* ordering is forced (tests/parity/kv-quant.test.ts, tests/parity/rotating-kvq.test.ts). */
|
|
176
250
|
export function maybeQuantizeKv(cache: Cache[], options: GenerateOptions): void {
|
|
177
251
|
const { kvBits, kvConfig, turboQuant } = options;
|
|
178
252
|
if (turboQuant) {
|
|
@@ -207,7 +281,7 @@ export function maybeQuantizeKv(cache: Cache[], options: GenerateOptions): void
|
|
|
207
281
|
}
|
|
208
282
|
}
|
|
209
283
|
|
|
210
|
-
/** Paged-KV conversion (docs/design/
|
|
284
|
+
/** Paged-KV conversion (docs/design/kv-cache.md): swap each FRESH
|
|
211
285
|
* plain full-attention KVCache for a PagedKVCache sized to hold
|
|
212
286
|
* `capacityTokens` (prompt + maxTokens — known exactly at generate()
|
|
213
287
|
* setup, so pool exhaustion is unreachable absent an accounting bug).
|
|
@@ -233,11 +307,29 @@ export function maybePageKv(
|
|
|
233
307
|
}
|
|
234
308
|
|
|
235
309
|
/** Emitted once per process: RotatingKVCache (sliding-window) layers are a
|
|
236
|
-
* documented v1 non-goal (docs/design/turboquant
|
|
310
|
+
* documented v1 non-goal (docs/design/turboquant.md) — they stay bf16
|
|
237
311
|
* rather than throwing, so mixed full-attention/sliding-window models (e.g.
|
|
238
312
|
* Gemma) still serve correctly under --kv-quant turbo. */
|
|
239
313
|
let warnedTurboRotating = false;
|
|
240
314
|
|
|
315
|
+
/** Emitted once per process: token fast-forwarding skips models with
|
|
316
|
+
* sliding-window layers in v1 (see the gate in generateInner). */
|
|
317
|
+
let warnedFillRotating = false;
|
|
318
|
+
|
|
319
|
+
/** The rewind surface a verify-policy fill needs. `Cache` declares the
|
|
320
|
+
* spec-round trio as optional members, but the cache list is a UNION with
|
|
321
|
+
* GLM's MLACache (which declares none of them), so the union has no such
|
|
322
|
+
* property to read. One narrowing helper keeps the call sites honest — every
|
|
323
|
+
* use is still optional-chained. */
|
|
324
|
+
type RewindableCache = {
|
|
325
|
+
isTrimmable(): boolean;
|
|
326
|
+
trim(n: number, bypass?: boolean): void;
|
|
327
|
+
specRoundBegin?(): void;
|
|
328
|
+
specRoundCommit?(): void;
|
|
329
|
+
specRoundRollback?(keep: number): void;
|
|
330
|
+
};
|
|
331
|
+
const rewindable = (c: unknown): RewindableCache => c as RewindableCache;
|
|
332
|
+
|
|
241
333
|
/** TurboQuant conversion chokepoint (mirrors the uniform/config branch
|
|
242
334
|
* above): only plain full-attention KVCache instances convert, via
|
|
243
335
|
* TurboQuantKVCache.fromKVCache — RotatingKVCache stays bf16 (warn once,
|
|
@@ -250,7 +342,7 @@ function maybeTurboQuantizeKv(cache: Cache[], scheme: TurboQuantScheme, start: n
|
|
|
250
342
|
warnedTurboRotating = true;
|
|
251
343
|
console.warn(
|
|
252
344
|
"[turbo-quant] sliding-window (RotatingKVCache) layers stay bf16 in v1 " +
|
|
253
|
-
"(full-attention only) — docs/design/turboquant
|
|
345
|
+
"(full-attention only) — docs/design/turboquant.md.",
|
|
254
346
|
);
|
|
255
347
|
}
|
|
256
348
|
continue;
|
|
@@ -273,6 +365,8 @@ function maybeTurboQuantizeKv(cache: Cache[], scheme: TurboQuantScheme, start: n
|
|
|
273
365
|
|
|
274
366
|
export interface GenerateStats {
|
|
275
367
|
promptTokens: number;
|
|
368
|
+
/** An observed terminal cause takes precedence over the token-count limit. */
|
|
369
|
+
finishReason?: "stop" | "length";
|
|
276
370
|
/** Prompt tokens skipped via a pre-warmed cache. */
|
|
277
371
|
cachedTokens: number;
|
|
278
372
|
generatedTokens: number;
|
|
@@ -294,6 +388,14 @@ export interface GenerateStats {
|
|
|
294
388
|
rejected?: number; rounds?: number; acceptanceLengths?: number[];
|
|
295
389
|
tokensPerForward?: number; forwardsSaved?: number;
|
|
296
390
|
};
|
|
391
|
+
/** Token fast-forwarding telemetry (serial lane, MLX_BUN_FILL=strict).
|
|
392
|
+
* Present only when a fill table was actually armed for this generation. */
|
|
393
|
+
fill?: FillStats;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export interface GenerateDiagnostics {
|
|
397
|
+
trace?: PromptResponseTrace;
|
|
398
|
+
mechanism?: "serial" | "continuous";
|
|
297
399
|
}
|
|
298
400
|
|
|
299
401
|
export interface GeneratedToken {
|
|
@@ -305,16 +407,23 @@ export interface GeneratedToken {
|
|
|
305
407
|
|
|
306
408
|
export class Generation implements AsyncIterable<GeneratedToken> {
|
|
307
409
|
stats: GenerateStats | null = null;
|
|
308
|
-
readonly #
|
|
410
|
+
readonly #next: () => Promise<IteratorResult<GeneratedToken, GenerateStats>>;
|
|
411
|
+
readonly #return: () => Promise<IteratorResult<GeneratedToken, GenerateStats>>;
|
|
309
412
|
|
|
310
|
-
constructor(iter: AsyncGenerator<GeneratedToken, GenerateStats
|
|
311
|
-
|
|
413
|
+
constructor(iter: AsyncGenerator<GeneratedToken, GenerateStats>, runtime = runtimeConfig()) {
|
|
414
|
+
// Async generators resume in the caller's context on every next/return.
|
|
415
|
+
// Bind both operations so consumer awaits and early close cannot change
|
|
416
|
+
// the runtime snapshot seen by kernels or their cleanup.
|
|
417
|
+
const next = iter.next.bind(iter);
|
|
418
|
+
const close = () => iter.return(undefined as unknown as GenerateStats);
|
|
419
|
+
this.#next = () => withRuntimeConfig(runtime, next);
|
|
420
|
+
this.#return = () => withRuntimeConfig(runtime, close);
|
|
312
421
|
}
|
|
313
422
|
|
|
314
423
|
async *[Symbol.asyncIterator](): AsyncGenerator<GeneratedToken> {
|
|
315
424
|
try {
|
|
316
425
|
while (true) {
|
|
317
|
-
const r = await this.#
|
|
426
|
+
const r = await this.#next();
|
|
318
427
|
if (r.done) {
|
|
319
428
|
this.stats = r.value;
|
|
320
429
|
return;
|
|
@@ -327,7 +436,7 @@ export class Generation implements AsyncIterable<GeneratedToken> {
|
|
|
327
436
|
// disposal, wired/adapter scopes) and capture the stats its
|
|
328
437
|
// early-return path still reports.
|
|
329
438
|
if (this.stats === null) {
|
|
330
|
-
const r = await this.#
|
|
439
|
+
const r = await this.#return();
|
|
331
440
|
if (r.done && r.value) this.stats = r.value;
|
|
332
441
|
}
|
|
333
442
|
}
|
|
@@ -346,18 +455,15 @@ export class Generation implements AsyncIterable<GeneratedToken> {
|
|
|
346
455
|
// Scope semantics match the reference: set → generate → synchronize →
|
|
347
456
|
// restore; nothing stays pinned between generations. Re-entrant: only
|
|
348
457
|
// the outermost wiring scope touches the limit.
|
|
349
|
-
|
|
458
|
+
// macOS 26.6 reports a 24.96 GiB recommended set on this 24 GB machine.
|
|
459
|
+
// The old 0.75 fraction therefore stopped wiring the 13-16 GiB Qwen/GLM
|
|
460
|
+
// models even though they page heavily without an explicit wired limit.
|
|
461
|
+
// Keep smaller 8-9 GiB models unwired while covering the large-model class.
|
|
462
|
+
const WIRE_THRESHOLD = 0.5;
|
|
350
463
|
let wiredScopeDepth = 0;
|
|
351
464
|
let wiredOldLimit = 0;
|
|
352
465
|
|
|
353
|
-
type WiredModelMemory =
|
|
354
|
-
readonly weightsBytes: number;
|
|
355
|
-
readonly expertRuntime?: {
|
|
356
|
-
readonly plan: { readonly plannedBytes: number };
|
|
357
|
-
flushUsage?: () => void;
|
|
358
|
-
finishUsage?: () => Promise<void>;
|
|
359
|
-
} | null;
|
|
360
|
-
};
|
|
466
|
+
type WiredModelMemory = MlxModelMemory;
|
|
361
467
|
|
|
362
468
|
/** Bytes whose MLX graph must stay resident while a model executes. */
|
|
363
469
|
export function wiredWorkingSetBytes(model: WiredModelMemory): number {
|
|
@@ -370,7 +476,7 @@ export function wiredWorkingSetBytes(model: WiredModelMemory): number {
|
|
|
370
476
|
export function modelNeedsWiredLimit(
|
|
371
477
|
model: WiredModelMemory,
|
|
372
478
|
recommendedBytes = maxRecommendedWorkingSetSize(),
|
|
373
|
-
force =
|
|
479
|
+
force = runtimeValue("MLX_BUN_FORCE_WIRE") === "1",
|
|
374
480
|
): boolean {
|
|
375
481
|
return force ||
|
|
376
482
|
wiredWorkingSetBytes(model) > WIRE_THRESHOLD * recommendedBytes;
|
|
@@ -387,24 +493,77 @@ function exitWiredScope(): void {
|
|
|
387
493
|
}
|
|
388
494
|
}
|
|
389
495
|
|
|
496
|
+
/** Hold the process wired limit while one model owns GPU execution. */
|
|
497
|
+
export function acquireModelWiredLimit(model: WiredModelMemory): () => void {
|
|
498
|
+
if (!modelNeedsWiredLimit(model)) return () => {};
|
|
499
|
+
enterWiredScope();
|
|
500
|
+
let released = false;
|
|
501
|
+
return () => {
|
|
502
|
+
if (released) return;
|
|
503
|
+
released = true;
|
|
504
|
+
exitWiredScope();
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
390
508
|
export function generate(
|
|
391
509
|
model: RuntimeModel,
|
|
392
510
|
promptTokens: number[],
|
|
393
511
|
options: GenerateOptions = {},
|
|
512
|
+
diagnostics: GenerateDiagnostics = {},
|
|
394
513
|
): Generation {
|
|
514
|
+
return bindGeneration(model)(promptTokens, options, diagnostics);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Resolve the compatibility model once at host construction. Every request
|
|
518
|
+
* borrows the same binding and immutable runtime snapshot. */
|
|
519
|
+
export function bindGeneration(model: RuntimeModel): (
|
|
520
|
+
promptTokens: number[], options?: GenerateOptions, diagnostics?: GenerateDiagnostics,
|
|
521
|
+
) => Generation {
|
|
395
522
|
// DiffusionGemma is non-autoregressive: route to the denoising engine instead
|
|
396
523
|
// of the AR decode loop. Same Generation/GenerateStats contract so the CLI and
|
|
397
524
|
// server stream it through the existing token machinery.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
525
|
+
if (!(model instanceof DiffusionGemmaModel)) {
|
|
526
|
+
const binding = bindLegacyAutoregressiveModel(model);
|
|
527
|
+
return (prompt, options, diagnostics) => generateAutoregressive(binding, prompt, options, diagnostics);
|
|
528
|
+
}
|
|
529
|
+
const binding = bindLegacyDenoisingModel(model);
|
|
530
|
+
return (prompt, options) => generateDenoising(binding, prompt, options);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Execute a supplied backend binding through the same AR loop and resource
|
|
534
|
+
* scopes as generate(). Callers serialize shared model/adapter access. Replacing
|
|
535
|
+
* this binding replaces graph, cache construction, media, and optional decode
|
|
536
|
+
* together; it never borrows another model's compiled execution implicitly. */
|
|
537
|
+
export function generateAutoregressive(
|
|
538
|
+
binding: MlxAutoregressiveBinding,
|
|
539
|
+
promptTokens: number[],
|
|
540
|
+
options: GenerateOptions = {},
|
|
541
|
+
diagnostics: GenerateDiagnostics = {},
|
|
542
|
+
): Generation {
|
|
543
|
+
const runtime = binding.runtime ?? runtimeConfig();
|
|
544
|
+
let inner = generateInner(binding, promptTokens, options, diagnostics, runtime);
|
|
545
|
+
if (options.adapters?.length && binding.adapters) {
|
|
546
|
+
inner = adapterScoped({ loraState: binding.adapters }, options.adapters, inner);
|
|
547
|
+
}
|
|
548
|
+
if (binding.memory.expertRuntime?.finishUsage || binding.memory.expertRuntime?.flushUsage)
|
|
549
|
+
inner = usageScoped(binding.memory, inner);
|
|
550
|
+
return new Generation(modelNeedsWiredLimit(binding.memory, undefined,
|
|
551
|
+
runtime.value("MLX_BUN_FORCE_WIRE") === "1") ? wiredScoped(inner) : inner, runtime);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Denoising bindings use their own state and feedback graph. Callers hold
|
|
555
|
+
* the native runtime's exclusive generation lease, including its global RNG. */
|
|
556
|
+
export function generateDenoising<State>(
|
|
557
|
+
binding: MlxDenoisingBinding<State>, promptTokens: number[], options: GenerateOptions = {},
|
|
558
|
+
): Generation {
|
|
559
|
+
let inner = generateDiffusionInner(binding, promptTokens, options);
|
|
560
|
+
if (options.adapters?.length && binding.adapters)
|
|
561
|
+
inner = adapterScoped({ loraState: binding.adapters }, options.adapters, inner);
|
|
562
|
+
if (binding.memory.expertRuntime?.finishUsage || binding.memory.expertRuntime?.flushUsage)
|
|
563
|
+
inner = usageScoped(binding.memory, inner);
|
|
564
|
+
const runtime = binding.runtime ?? runtimeConfig();
|
|
565
|
+
return new Generation(modelNeedsWiredLimit(binding.memory, undefined,
|
|
566
|
+
runtime.value("MLX_BUN_FORCE_WIRE") === "1") ? wiredScoped(inner) : inner, runtime);
|
|
408
567
|
}
|
|
409
568
|
|
|
410
569
|
/** Non-autoregressive diffusion generation, adapted to the AR Generation
|
|
@@ -412,11 +571,12 @@ export function generate(
|
|
|
412
571
|
* streams the emitted tokens. v1: greedy (temperature 0, confidence-threshold
|
|
413
572
|
* sampler — the OptiQ default); per-block intra-stream + temperature>0
|
|
414
573
|
* (categorical) are follow-ups. */
|
|
415
|
-
async function* generateDiffusionInner(
|
|
416
|
-
|
|
574
|
+
async function* generateDiffusionInner<State>(
|
|
575
|
+
binding: MlxDenoisingBinding<State>,
|
|
417
576
|
promptTokens: number[],
|
|
418
577
|
options: GenerateOptions,
|
|
419
578
|
): AsyncGenerator<GeneratedToken, GenerateStats> {
|
|
579
|
+
options.signal?.throwIfAborted();
|
|
420
580
|
const t0 = performance.now();
|
|
421
581
|
const maxTokens = options.maxTokens ?? 256;
|
|
422
582
|
// A fresh random canvas seed per request unless the caller pins one.
|
|
@@ -426,40 +586,52 @@ async function* generateDiffusionInner(
|
|
|
426
586
|
: BigInt(Math.floor(Math.random() * 0x7fffffff));
|
|
427
587
|
// The shipped checkpoint's tokenizer stops on {1, 106}; union any caller eos.
|
|
428
588
|
const eos = [...new Set([1, 106, ...(options.eosTokenIds ?? [])])];
|
|
429
|
-
const result =
|
|
589
|
+
const result = await denoiseAsync(binding.graph, promptTokens, {
|
|
430
590
|
maxTokens,
|
|
431
591
|
sampler: "confidence-threshold",
|
|
432
592
|
temperature: 0,
|
|
433
593
|
eosTokenIds: eos,
|
|
434
594
|
seed,
|
|
435
595
|
visionPixels: options.visionPixels,
|
|
436
|
-
});
|
|
596
|
+
}, options.signal);
|
|
437
597
|
const decodeMs = performance.now() - t0;
|
|
438
598
|
let index = 0;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
599
|
+
let failure: { error: unknown } | undefined;
|
|
600
|
+
try {
|
|
601
|
+
for (const token of result.tokens) {
|
|
602
|
+
options.signal?.throwIfAborted();
|
|
603
|
+
yield { token, index: index++ };
|
|
604
|
+
}
|
|
605
|
+
} catch (error) { failure = { error }; }
|
|
606
|
+
finally {
|
|
607
|
+
// A stop parser may close delivery before the finished canvas is exhausted.
|
|
608
|
+
// Settle the emitted count on return(), without suppressing cancellation/errors.
|
|
609
|
+
if (failure) throw failure.error;
|
|
610
|
+
return {
|
|
611
|
+
promptTokens: promptTokens.length,
|
|
612
|
+
cachedTokens: 0,
|
|
613
|
+
generatedTokens: index,
|
|
614
|
+
prefillTps: 0,
|
|
615
|
+
decodeTps: index / Math.max(decodeMs / 1000, 1e-9),
|
|
616
|
+
prefillMs: 0,
|
|
617
|
+
decodeMs,
|
|
618
|
+
cacheTokens: [],
|
|
619
|
+
};
|
|
620
|
+
}
|
|
450
621
|
}
|
|
451
622
|
|
|
452
623
|
/** Hold the model's active-adapter list for exactly this generation. */
|
|
453
624
|
async function* adapterScoped(
|
|
454
|
-
model:
|
|
625
|
+
model: { loraState: { active: string[] } },
|
|
455
626
|
adapters: string[],
|
|
456
627
|
inner: AsyncGenerator<GeneratedToken, GenerateStats>,
|
|
457
628
|
): AsyncGenerator<GeneratedToken, GenerateStats> {
|
|
629
|
+
const previous = model.loraState.active;
|
|
458
630
|
model.loraState.active = adapters;
|
|
459
631
|
try {
|
|
460
632
|
return yield* inner;
|
|
461
633
|
} finally {
|
|
462
|
-
model.loraState.active =
|
|
634
|
+
model.loraState.active = previous;
|
|
463
635
|
}
|
|
464
636
|
}
|
|
465
637
|
|
|
@@ -496,12 +668,11 @@ export async function withModelWiredLimit<T>(
|
|
|
496
668
|
model: WiredModelMemory,
|
|
497
669
|
run: () => Promise<T>,
|
|
498
670
|
): Promise<T> {
|
|
499
|
-
|
|
500
|
-
enterWiredScope();
|
|
671
|
+
const release = acquireModelWiredLimit(model);
|
|
501
672
|
try {
|
|
502
673
|
return await run();
|
|
503
674
|
} finally {
|
|
504
|
-
|
|
675
|
+
release();
|
|
505
676
|
}
|
|
506
677
|
}
|
|
507
678
|
|
|
@@ -520,37 +691,34 @@ export async function withModelUsageFlush<T>(
|
|
|
520
691
|
}
|
|
521
692
|
}
|
|
522
693
|
|
|
523
|
-
/** Forward through either the ordinary synchronous model surface or a
|
|
524
|
-
* streamed-expert model whose layer execution must cross async I/O. Keeping
|
|
525
|
-
* this choice inside the shared generator preserves the complete generation
|
|
526
|
-
* contract (sampling, grammar, logprobs, prompt cache, and cancellation) for
|
|
527
|
-
* both execution styles. */
|
|
528
|
-
async function forwardHiddenForGeneration(
|
|
529
|
-
model: RuntimeModel,
|
|
530
|
-
ids: MlxArray,
|
|
531
|
-
cache: Cache[],
|
|
532
|
-
): Promise<MlxArray> {
|
|
533
|
-
const asyncModel = model as RuntimeModel & {
|
|
534
|
-
forwardHiddenAsync?: (ids: MlxArray, cache: Cache[]) => Promise<MlxArray>;
|
|
535
|
-
};
|
|
536
|
-
return typeof asyncModel.forwardHiddenAsync === "function"
|
|
537
|
-
? await asyncModel.forwardHiddenAsync(ids, cache)
|
|
538
|
-
: model.forwardHidden(ids, cache);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
694
|
async function* generateInner(
|
|
542
|
-
|
|
695
|
+
binding: MlxAutoregressiveBinding,
|
|
543
696
|
promptTokens: number[],
|
|
544
697
|
options: GenerateOptions,
|
|
698
|
+
diagnostics: GenerateDiagnostics,
|
|
699
|
+
runtime: RuntimeConfig,
|
|
545
700
|
): AsyncGenerator<GeneratedToken, GenerateStats> {
|
|
701
|
+
if (options.signal?.aborted) {
|
|
702
|
+
options.grammar?.dispose();
|
|
703
|
+
options.signal.throwIfAborted();
|
|
704
|
+
}
|
|
546
705
|
const {
|
|
547
706
|
maxTokens = 512,
|
|
548
|
-
eosTokenIds =
|
|
707
|
+
eosTokenIds = binding.eosTokenIds,
|
|
549
708
|
prefillChunkSize = 2048,
|
|
550
709
|
} = options;
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
assertMlxAutoregressiveBinding(binding);
|
|
713
|
+
if (options.promptEmbeddings && !binding.forwardEmbeddings)
|
|
714
|
+
throw new Error("AR binding does not support prompt embeddings");
|
|
715
|
+
if (options.adapters?.length && !binding.adapters)
|
|
716
|
+
throw new Error("AR binding does not support adapters");
|
|
717
|
+
} catch (error) {
|
|
718
|
+
options.grammar?.dispose();
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
721
|
+
const graph = binding.graph;
|
|
554
722
|
|
|
555
723
|
// logprobs capture (mlx_lm.server semantics — see GenerateOptions.logprobs).
|
|
556
724
|
// Everything below is gated: when neither flag is set, no extra ops, evals,
|
|
@@ -558,36 +726,80 @@ async function* generateInner(
|
|
|
558
726
|
const captureSelLp = options.logprobs === true;
|
|
559
727
|
const captureTopK =
|
|
560
728
|
options.topLogprobs && options.topLogprobs > 0 ? options.topLogprobs : 0;
|
|
561
|
-
const
|
|
729
|
+
const stepSampler = makeStepSampler(options, {
|
|
730
|
+
tokenRepresentation: "device",
|
|
731
|
+
grammarWait: "external",
|
|
732
|
+
historyUpdate: "manual",
|
|
733
|
+
captureSelectedLogprob: captureSelLp,
|
|
734
|
+
captureTopLogprobs: captureTopK,
|
|
735
|
+
});
|
|
736
|
+
const needsTokenHistory = stepSampler.needsHistory;
|
|
562
737
|
|
|
738
|
+
const closePrefill = diagnostics.trace?.begin("prefill.total", {
|
|
739
|
+
mechanism: diagnostics.mechanism ?? "serial",
|
|
740
|
+
promptTokens: promptTokens.length,
|
|
741
|
+
cachedTokens: options.cache?.[0]?.offset ?? 0,
|
|
742
|
+
});
|
|
743
|
+
const closeBatchSetup = diagnostics.trace?.begin("prefill.batch_setup", {
|
|
744
|
+
mechanism: diagnostics.mechanism ?? "serial",
|
|
745
|
+
});
|
|
563
746
|
const ownsCache = !options.cache;
|
|
564
|
-
const
|
|
747
|
+
const resuming = options.initialPendingToken !== undefined;
|
|
748
|
+
let cache: Cache[] = options.cache ?? [];
|
|
749
|
+
try {
|
|
750
|
+
if (ownsCache) cache = binding.makeCache();
|
|
751
|
+
if (!cache.length) throw new Error("AR binding returned an empty cache");
|
|
752
|
+
const cached = cache[0]!.offset;
|
|
753
|
+
if ((!resuming && cached >= promptTokens.length) ||
|
|
754
|
+
(resuming && cached !== promptTokens.length))
|
|
755
|
+
throw new Error(
|
|
756
|
+
resuming
|
|
757
|
+
? `resumed cache (${cached} tokens) must exactly cover the resume prefix (${promptTokens.length})`
|
|
758
|
+
: `pre-warmed cache (${cached} tokens) must be a strict prefix of the prompt (${promptTokens.length})`,
|
|
759
|
+
);
|
|
760
|
+
// Replace fresh full-attention caches before any forward. The deepest
|
|
761
|
+
// write is prompt + maxTokens - 1, so capacity covers every decode step.
|
|
762
|
+
maybePageKv(cache, options, promptTokens.length + maxTokens);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
if (ownsCache) for (const state of cache) state.dispose();
|
|
765
|
+
stepSampler.dispose();
|
|
766
|
+
options.grammar?.dispose();
|
|
767
|
+
closeBatchSetup?.();
|
|
768
|
+
closePrefill?.();
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
565
771
|
const cachedTokens = cache[0]!.offset;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
772
|
+
// Token fast-forwarding gate (see shouldUseFill). RotatingKVCache (sliding
|
|
773
|
+
// window) layers append multi-token writes through #updateConcat — O(window)
|
|
774
|
+
// per append rather than the O(L) a plain ring pays — so v1 warns and skips
|
|
775
|
+
// the whole feature for those models rather than paying it silently.
|
|
776
|
+
let fillOn = shouldUseFill(options, runtime);
|
|
777
|
+
if (fillOn && cache.some((c) => c instanceof RotatingKVCache)) {
|
|
778
|
+
fillOn = false;
|
|
779
|
+
if (!warnedFillRotating) {
|
|
780
|
+
warnedFillRotating = true;
|
|
781
|
+
console.warn(
|
|
782
|
+
"[fill] sliding-window (RotatingKVCache) layers skip token " +
|
|
783
|
+
"fast-forwarding in v1 (multi-token append is O(window) there) — " +
|
|
784
|
+
"docs/design/speculative-decoding.md.",
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const fillTrace = fillOn && fillTraceEnabled();
|
|
789
|
+
const fillTraceFile = fillOn ? fillTracePath() : null;
|
|
790
|
+
// Verify-policy proposals (echo tier) need the rejected tail rewound. That
|
|
791
|
+
// is the SAME contract the spec lane's rounds use: trimmable caches drop the
|
|
792
|
+
// tail; recurrent caches (SSMCache — gated-DeltaNet state, untrimmable)
|
|
793
|
+
// restore a pre-round snapshot and bit-exactly replay the accepted prefix.
|
|
794
|
+
// A model whose caches can do neither still gets assert-policy fills; verify
|
|
795
|
+
// proposals are dropped and counted (stats.verifyUnsupported).
|
|
796
|
+
const verifyCapable = fillOn && cache.every(
|
|
797
|
+
(c) => c.isTrimmable() || typeof rewindable(c).specRoundRollback === "function",
|
|
798
|
+
);
|
|
799
|
+
closeBatchSetup?.();
|
|
578
800
|
/** Device-side logprob capture for one step, computed from the SAME lp the
|
|
579
801
|
* sampler saw (post-processors, pre-truncation) — read back lazily with the
|
|
580
802
|
* token so decode pipelining is preserved. */
|
|
581
|
-
interface StepExtras {
|
|
582
|
-
sel: MlxArray | null; // [1,1] emitted token's logprob
|
|
583
|
-
topIdx: MlxArray | null; // [1,k] top-k token ids
|
|
584
|
-
topVals: MlxArray | null; // [1,k] their logprobs
|
|
585
|
-
}
|
|
586
|
-
const extrasArrays = (e: StepExtras | null): MlxArray[] =>
|
|
587
|
-
e ? [e.sel, e.topIdx, e.topVals].filter((a): a is MlxArray => a !== null) : [];
|
|
588
|
-
const disposeExtras = (e: StepExtras | null): void => {
|
|
589
|
-
if (e) for (const a of [e.sel, e.topIdx, e.topVals]) a?.dispose();
|
|
590
|
-
};
|
|
591
803
|
/** Read extras back to JS (forces eval — they were async-dispatched with the
|
|
592
804
|
* token) without appending casts behind the next decode step. */
|
|
593
805
|
const readExtras = (e: StepExtras | null): TokenLogprobs | undefined => {
|
|
@@ -601,7 +813,7 @@ async function* generateInner(
|
|
|
601
813
|
(a, b) => b.logprob - a.logprob,
|
|
602
814
|
);
|
|
603
815
|
}
|
|
604
|
-
|
|
816
|
+
disposeStepExtras(e);
|
|
605
817
|
return out;
|
|
606
818
|
};
|
|
607
819
|
|
|
@@ -611,52 +823,11 @@ async function* generateInner(
|
|
|
611
823
|
logits3d: MlxArray,
|
|
612
824
|
step: number,
|
|
613
825
|
): { tok: MlxArray; extras: StepExtras | null } => {
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
for (const p of processors) logits = disposing(logits, p(history, logits));
|
|
617
|
-
// Grammar mask (src/grammar.ts): apply AFTER the standard processors so the
|
|
618
|
-
// grammar has the final say on validity (a -inf'd token stays -inf through
|
|
619
|
-
// log-softmax). Off (no grammar) = zero cost — the branch is unset.
|
|
620
|
-
if (options.grammar) logits = disposing(logits, options.grammar.applyMask(logits));
|
|
621
|
-
const lp = toLogprobs(logits);
|
|
622
|
-
logits.dispose();
|
|
623
|
-
const tok = sampler(lp, step);
|
|
624
|
-
let extras: StepExtras | null = null;
|
|
625
|
-
if (capture) {
|
|
626
|
-
let sel: MlxArray | null = null;
|
|
627
|
-
let topIdx: MlxArray | null = null;
|
|
628
|
-
let topVals: MlxArray | null = null;
|
|
629
|
-
if (captureSelLp) {
|
|
630
|
-
const idx = ops.reshape(tok, [1, 1]);
|
|
631
|
-
sel = ops.takeAlongAxis(lp, idx, -1);
|
|
632
|
-
idx.dispose();
|
|
633
|
-
}
|
|
634
|
-
if (captureTopK > 0) {
|
|
635
|
-
// mlx-lm _format_top_logprobs: argpartition(-logprobs, kth=k-1)[:k]
|
|
636
|
-
const k = Math.min(captureTopK, V);
|
|
637
|
-
const negLp = ops.neg(lp);
|
|
638
|
-
const part = ops.argpartitionAxis(negLp, k - 1, -1);
|
|
639
|
-
const idxView = part.slice([0, 0], [1, k]);
|
|
640
|
-
topIdx = ops.contiguous(idxView); // slices are strided views; raw readback needs contiguous
|
|
641
|
-
topVals = ops.takeAlongAxis(lp, topIdx, -1);
|
|
642
|
-
for (const a of [negLp, part, idxView]) a.dispose();
|
|
643
|
-
}
|
|
644
|
-
extras = { sel, topIdx, topVals };
|
|
645
|
-
}
|
|
646
|
-
lp.dispose();
|
|
647
|
-
return { tok, extras };
|
|
826
|
+
const result = stepSampler.sample(logits3d, step);
|
|
827
|
+
return { tok: result.token, extras: result.extras };
|
|
648
828
|
};
|
|
649
829
|
|
|
650
|
-
const pushHistory = (tok: MlxArray) =>
|
|
651
|
-
if (!needsTokenHistory) return;
|
|
652
|
-
if (!history) {
|
|
653
|
-
history = ops.reshape(tok, [1]);
|
|
654
|
-
} else {
|
|
655
|
-
const prev = history;
|
|
656
|
-
history = ops.concatAxis([prev, tok], 0);
|
|
657
|
-
prev.dispose();
|
|
658
|
-
}
|
|
659
|
-
};
|
|
830
|
+
const pushHistory = (tok: MlxArray): void => stepSampler.commitDevice(tok);
|
|
660
831
|
|
|
661
832
|
// Decode-loop state lives at function scope so the finally can still
|
|
662
833
|
// report stats and dispose in-flight arrays when the consumer
|
|
@@ -665,127 +836,301 @@ async function* generateInner(
|
|
|
665
836
|
let prefillMs = 0;
|
|
666
837
|
let tDecode = 0;
|
|
667
838
|
let decodeMs = 0;
|
|
668
|
-
let generated = 0;
|
|
839
|
+
let generated = options.initialGeneratedTokens ?? 0;
|
|
840
|
+
let finishReason: GenerateStats["finishReason"];
|
|
669
841
|
const forwarded: number[] = [];
|
|
670
842
|
let pending: MlxArray | null = null;
|
|
671
843
|
let nextPending: MlxArray | null = null;
|
|
672
844
|
let pendingExtras: StepExtras | null = null;
|
|
673
845
|
let nextExtras: StepExtras | null = null;
|
|
846
|
+
let decoder: MlxDecodeStep | null = null;
|
|
847
|
+
let appender: MlxTokenAppend | null = null;
|
|
674
848
|
let finished = false;
|
|
675
849
|
let threw = false;
|
|
850
|
+
let executionError: unknown;
|
|
851
|
+
let closeTokenZero: (() => void) | undefined;
|
|
676
852
|
const makeStats = (): GenerateStats => ({
|
|
677
|
-
promptTokens: promptTokens.length,
|
|
678
|
-
cachedTokens,
|
|
853
|
+
promptTokens: options.originalPromptTokens ?? promptTokens.length,
|
|
854
|
+
cachedTokens: Math.min(cachedTokens, options.originalPromptTokens ?? promptTokens.length),
|
|
679
855
|
generatedTokens: generated,
|
|
856
|
+
...(finishReason ? { finishReason } : {}),
|
|
680
857
|
prefillMs,
|
|
681
858
|
decodeMs,
|
|
682
|
-
prefillTps:
|
|
859
|
+
prefillTps: prefillMs > 0
|
|
860
|
+
? ((promptTokens.length - cachedTokens) / prefillMs) * 1000
|
|
861
|
+
: 0,
|
|
683
862
|
decodeTps: (generated / decodeMs) * 1000,
|
|
684
863
|
cacheTokens: [...promptTokens, ...forwarded],
|
|
864
|
+
...(fillOn ? { fill: options.fill!.stats } : {}),
|
|
685
865
|
});
|
|
686
866
|
|
|
867
|
+
/** The one invariant that makes fill safe: the caches hold exactly
|
|
868
|
+
* prompt + everything `forwarded` records, so a fill append must forward
|
|
869
|
+
* ONLY the injected ids — the normal step already consumed `cur` and wrote
|
|
870
|
+
* its KV. Forwarding [token, ...ids] would duplicate a position and
|
|
871
|
+
* silently corrupt both the KV and PromptCache.put's key. Checked under
|
|
872
|
+
* MLX_BUN_FILL_TRACE=1 on both sides of the append. */
|
|
873
|
+
const assertFillAlignment = (where: string): void => {
|
|
874
|
+
const offset = cache[0]!.offset;
|
|
875
|
+
const want = promptTokens.length + forwarded.length;
|
|
876
|
+
if (offset !== want)
|
|
877
|
+
throw new Error(
|
|
878
|
+
`[fill] cache misalignment ${where}: cache offset ${offset} != ` +
|
|
879
|
+
`prompt ${promptTokens.length} + forwarded ${forwarded.length}`,
|
|
880
|
+
);
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
/** THE apply primitive for token fast-forwarding — ONE chunked forward
|
|
884
|
+
* carries a proposal into the KV (and the recurrent state), and BOTH
|
|
885
|
+
* policies ride it (src/fill/proposal.ts):
|
|
886
|
+
*
|
|
887
|
+
* - assert: the span is determined (a template scaffold). Append and move
|
|
888
|
+
* on. No readback, no checkpoint, no rewind. The in-flight sample for
|
|
889
|
+
* the position after `token` is dropped UNEXAMINED — a discarded
|
|
890
|
+
* pipeline dispatch, not a rejected draft.
|
|
891
|
+
* - verify: the span is likely (a copy from earlier in the session).
|
|
892
|
+
* Position 0 is checked BEFORE the forward against the in-flight sample
|
|
893
|
+
* (free, and a mismatch costs nothing — nothing has been written).
|
|
894
|
+
* Positions 1..m-1 are checked against the argmax already sitting in
|
|
895
|
+
* THIS forward's logits, so verification adds no pass over the weights.
|
|
896
|
+
* The rejected tail is rewound through the same cache contract the spec
|
|
897
|
+
* lane's rounds use, and decode resumes at the first disagreement.
|
|
898
|
+
*
|
|
899
|
+
* Returns the ids the engine must now emit (empty = no fill happened).
|
|
900
|
+
* Mutates nextPending/nextExtras/forwarded, exactly like the normal step. */
|
|
901
|
+
/** One trace record: the proposal next to the model's own token at every
|
|
902
|
+
* span position (`actual[0]` = in-flight sample, `actual[j]` = argmax after
|
|
903
|
+
* ids[j-1]). Appended as JSONL to MLX_BUN_FILL_TRACE=<file>. */
|
|
904
|
+
const traceProposal = (
|
|
905
|
+
proposal: Proposal, generatedNow: number, accepted: number, actual: number[],
|
|
906
|
+
): void => {
|
|
907
|
+
const ids = proposal.ids;
|
|
908
|
+
let firstMismatch = -1;
|
|
909
|
+
for (let j = 0; j < ids.length && j < actual.length; j++)
|
|
910
|
+
if (actual[j] !== ids[j]) { firstMismatch = j; break; }
|
|
911
|
+
const dec = options.fill!.decode;
|
|
912
|
+
const rec: FillTraceRecord = {
|
|
913
|
+
ts: new Date().toISOString(), origin: proposal.origin, policy: proposal.policy,
|
|
914
|
+
generated: generatedNow, proposedLen: ids.length, accepted, firstMismatch,
|
|
915
|
+
proposed: ids, actual,
|
|
916
|
+
...(dec ? { proposedText: dec(ids), actualText: dec(actual) } : {}),
|
|
917
|
+
};
|
|
918
|
+
try { appendFileSync(fillTraceFile!, JSON.stringify(rec) + "\n"); } catch { /* trace is best-effort */ }
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
const applyProposal = async (
|
|
922
|
+
proposal: Proposal, generatedNow: number,
|
|
923
|
+
): Promise<number[]> => {
|
|
924
|
+
const fill = options.fill!;
|
|
925
|
+
const ids = proposal.ids;
|
|
926
|
+
const verify = proposal.policy === "verify";
|
|
927
|
+
if (verify && !verifyCapable) {
|
|
928
|
+
fill.noteVerifyUnsupported();
|
|
929
|
+
fill.commit(proposal, 0);
|
|
930
|
+
return [];
|
|
931
|
+
}
|
|
932
|
+
// The in-flight sample IS the model's own choice for position 0 (read for
|
|
933
|
+
// the trace under both policies; the served assert path never checks it).
|
|
934
|
+
const inFlight = verify || fillTraceFile ? ops.itemUint32(nextPending!) : -1;
|
|
935
|
+
if (verify) {
|
|
936
|
+
if (inFlight !== ids[0]) {
|
|
937
|
+
if (fillTraceFile) traceProposal(proposal, generatedNow, 0, [inFlight]);
|
|
938
|
+
fill.commit(proposal, 0);
|
|
939
|
+
return []; // nextPending survives; this becomes an ordinary step
|
|
940
|
+
}
|
|
941
|
+
} else {
|
|
942
|
+
fill.noteWastedSample();
|
|
943
|
+
}
|
|
944
|
+
if (fillTrace) assertFillAlignment("at fill entry");
|
|
945
|
+
nextPending!.dispose();
|
|
946
|
+
nextPending = null;
|
|
947
|
+
disposeStepExtras(nextExtras);
|
|
948
|
+
nextExtras = null;
|
|
949
|
+
// No-op under the fill exclusions (kv quant refuses fill); kept so the
|
|
950
|
+
// boundary stays correct if those exclusions ever loosen.
|
|
951
|
+
maybeQuantizeKv(cache, options);
|
|
952
|
+
|
|
953
|
+
let checkpointMs = 0;
|
|
954
|
+
let roundOpen = false;
|
|
955
|
+
if (verify) {
|
|
956
|
+
const t0 = performance.now();
|
|
957
|
+
for (const c of cache) rewindable(c).specRoundBegin?.();
|
|
958
|
+
roundOpen = true;
|
|
959
|
+
checkpointMs += performance.now() - t0;
|
|
960
|
+
}
|
|
961
|
+
let accepted = ids.length;
|
|
962
|
+
let hf: MlxArray | null = null;
|
|
963
|
+
let logitsAll: MlxArray | null = null;
|
|
964
|
+
try {
|
|
965
|
+
const chunkSize = (state: readonly Cache[]) => {
|
|
966
|
+
const maxChunk = appender?.maxChunkSize(state) ?? 1;
|
|
967
|
+
return fill.appendChunkSize > 0 ? Math.min(fill.appendChunkSize, maxChunk) : maxChunk;
|
|
968
|
+
};
|
|
969
|
+
hf = await appendFillHidden(
|
|
970
|
+
!verify && appender ? appender.forwardHidden.bind(appender) : graph.forwardHidden.bind(graph),
|
|
971
|
+
cache, ids, verify ? () => ids.length : chunkSize,
|
|
972
|
+
);
|
|
973
|
+
const [, Lf, Hf] = hf.shape as [number, number, number];
|
|
974
|
+
let pred: number[] | null = null;
|
|
975
|
+
if (verify || fillTraceFile) {
|
|
976
|
+
// Free logits: this forward already computed every span position's
|
|
977
|
+
// hidden state. argmax at j is the model's continuation after ids[j],
|
|
978
|
+
// so it verifies ids[j+1]. (Under assert this readback is trace-only.)
|
|
979
|
+
logitsAll = graph.projectLogits(hf, { type: "all" });
|
|
980
|
+
const argmax = ops.argmaxAxis(logitsAll, -1);
|
|
981
|
+
pred = argmax.toIntTokens();
|
|
982
|
+
argmax.dispose();
|
|
983
|
+
}
|
|
984
|
+
if (verify && pred) {
|
|
985
|
+
for (let j = 0; j + 1 < ids.length; j++) {
|
|
986
|
+
if (pred[j] !== ids[j + 1]) { accepted = j + 1; break; }
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (fillTraceFile && pred)
|
|
990
|
+
traceProposal(proposal, generatedNow, accepted, [inFlight, ...pred.slice(0, ids.length - 1)]);
|
|
991
|
+
const t1 = performance.now();
|
|
992
|
+
if (accepted < ids.length) {
|
|
993
|
+
// Trimmable caches drop the rejected tail; recurrent caches restore
|
|
994
|
+
// their pre-round snapshot and bit-exactly replay the accepted prefix
|
|
995
|
+
// (Cache.specRoundRollback — the same primitive src/spec/serve-loop.ts
|
|
996
|
+
// uses after its accept walk).
|
|
997
|
+
for (const c of cache) {
|
|
998
|
+
const rc = rewindable(c);
|
|
999
|
+
if (rc.specRoundRollback) rc.specRoundRollback(accepted);
|
|
1000
|
+
else rc.trim(ids.length - accepted);
|
|
1001
|
+
}
|
|
1002
|
+
roundOpen = false;
|
|
1003
|
+
} else if (roundOpen) {
|
|
1004
|
+
for (const c of cache) rewindable(c).specRoundCommit?.();
|
|
1005
|
+
roundOpen = false;
|
|
1006
|
+
}
|
|
1007
|
+
if (verify) checkpointMs += performance.now() - t1;
|
|
1008
|
+
forwarded.push(...ids.slice(0, accepted));
|
|
1009
|
+
if (fillTrace) assertFillAlignment("after fill append");
|
|
1010
|
+
// The sampler's token history gets the ACCEPTED ids only, before the
|
|
1011
|
+
// resume sample — so logits processors (repetition/presence/frequency)
|
|
1012
|
+
// see exactly the sequence that was emitted.
|
|
1013
|
+
stepSampler.commitNumbers(ids.slice(0, accepted));
|
|
1014
|
+
// Resume from the LAST ACCEPTED position's logits: the bonus token on a
|
|
1015
|
+
// full accept, the correction on a partial one. Same sampler and step
|
|
1016
|
+
// index as an ordinary step — the correction's KV is written by the next
|
|
1017
|
+
// iteration's forward, like any sampled token.
|
|
1018
|
+
const willGen = generatedNow + accepted;
|
|
1019
|
+
if (willGen < maxTokens) {
|
|
1020
|
+
let logits: MlxArray;
|
|
1021
|
+
if (logitsAll) {
|
|
1022
|
+
const V = logitsAll.shape[2]!;
|
|
1023
|
+
logits = logitsAll.slice([0, accepted - 1, 0], [1, accepted, V]);
|
|
1024
|
+
} else {
|
|
1025
|
+
const hLast = hf.slice([0, Lf - 1, 0], [1, Lf, Hf]);
|
|
1026
|
+
logits = graph.projectLogits(hLast, { type: "all" });
|
|
1027
|
+
hLast.dispose();
|
|
1028
|
+
}
|
|
1029
|
+
const sn = sampleStep(logits, willGen);
|
|
1030
|
+
nextPending = sn.tok;
|
|
1031
|
+
nextExtras = sn.extras;
|
|
1032
|
+
logits.dispose();
|
|
1033
|
+
ops.asyncEvalAll([nextPending, ...stepExtrasArrays(nextExtras)]);
|
|
1034
|
+
}
|
|
1035
|
+
} finally {
|
|
1036
|
+
// A throw mid-round would otherwise leave the recurrent caches armed
|
|
1037
|
+
// (their next forward raises "spec round already recorded").
|
|
1038
|
+
if (roundOpen) for (const c of cache) rewindable(c).specRoundCommit?.();
|
|
1039
|
+
logitsAll?.dispose();
|
|
1040
|
+
hf?.dispose();
|
|
1041
|
+
}
|
|
1042
|
+
if (verify) fill.noteVerifyEvent(checkpointMs);
|
|
1043
|
+
fill.commit(proposal, accepted);
|
|
1044
|
+
if (fillTrace)
|
|
1045
|
+
console.error(
|
|
1046
|
+
`[fill] ${proposal.origin}/${proposal.policy} ${accepted}/${ids.length} ` +
|
|
1047
|
+
`after ${generatedNow} generated (events ${fill.stats.events}, ` +
|
|
1048
|
+
`injected ${fill.stats.injected}, checkpoint ${checkpointMs.toFixed(2)}ms)`,
|
|
1049
|
+
);
|
|
1050
|
+
return ids.slice(0, accepted);
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
let nextCheckpoint = options.checkpointEveryTokens && options.checkpointEveryTokens > 0
|
|
1054
|
+
? (Math.floor(generated / options.checkpointEveryTokens) + 1) * options.checkpointEveryTokens
|
|
1055
|
+
: Number.POSITIVE_INFINITY;
|
|
1056
|
+
let tPrefill = performance.now();
|
|
1057
|
+
|
|
687
1058
|
try {
|
|
1059
|
+
if (resuming) {
|
|
1060
|
+
// The checkpoint cache already covers the complete replay prefix. Its
|
|
1061
|
+
// pending token was sampled from that exact state before the snapshot,
|
|
1062
|
+
// so resume starts directly at the decode loop without another forward.
|
|
1063
|
+
if (needsTokenHistory) stepSampler.seedHistory(promptTokens);
|
|
1064
|
+
pending = ops.fromInt32([options.initialPendingToken!], [1]);
|
|
1065
|
+
ops.asyncEvalAll([pending]);
|
|
1066
|
+
prefillMs = 0;
|
|
1067
|
+
closePrefill?.();
|
|
1068
|
+
tDecode = performance.now();
|
|
1069
|
+
} else {
|
|
688
1070
|
// ---- prefill ----
|
|
1071
|
+
const closeInitialKv = diagnostics.trace?.begin("prefill.kv_maintenance", {
|
|
1072
|
+
mechanism: diagnostics.mechanism ?? "serial",
|
|
1073
|
+
boundary: "initial",
|
|
1074
|
+
});
|
|
689
1075
|
maybeQuantizeKv(cache, options);
|
|
690
|
-
|
|
1076
|
+
closeInitialKv?.();
|
|
1077
|
+
tPrefill = performance.now();
|
|
691
1078
|
let h0: MlxArray;
|
|
692
1079
|
if (options.promptEmbeddings) {
|
|
693
1080
|
if (cachedTokens !== 0)
|
|
694
1081
|
throw new Error("promptEmbeddings cannot be combined with a pre-warmed cache");
|
|
695
1082
|
if (needsTokenHistory)
|
|
696
|
-
|
|
1083
|
+
stepSampler.seedHistory(promptTokens);
|
|
697
1084
|
// e2b/e4b need the spliced token ids to build per-layer inputs
|
|
698
1085
|
// (multimodal soft-token positions zeroed inside forwardEmbeddings).
|
|
699
1086
|
const embedIds = ops.fromInt32(promptTokens, [1, promptTokens.length]);
|
|
700
|
-
h0 =
|
|
1087
|
+
h0 = binding.forwardEmbeddings!(
|
|
701
1088
|
options.promptEmbeddings, cache, options.imageMask ?? null, embedIds,
|
|
702
1089
|
options.multimodalMask ?? null,
|
|
703
1090
|
);
|
|
704
1091
|
embedIds.dispose();
|
|
705
1092
|
} else {
|
|
706
1093
|
let pos = cachedTokens;
|
|
707
|
-
|
|
708
|
-
// chunks, no logits), fire the hook while the caches hold exactly
|
|
709
|
-
// that prefix, then continue with the remainder below. Splitting here
|
|
710
|
-
// changes chunk shapes only for requests that opted in — the same
|
|
711
|
-
// numerics class as a prompt-cache-hit continuation prefill.
|
|
712
|
-
const snapAt = options.onPrefillDone && options.snapshotAt !== undefined
|
|
1094
|
+
const snapshotAt = options.onPrefillDone && options.snapshotAt !== undefined
|
|
713
1095
|
? Math.min(Math.max(options.snapshotAt, cachedTokens), promptTokens.length)
|
|
714
|
-
:
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
// a SEPARATE L=1 forward of the LAST prompt token (its server's
|
|
737
|
-
// batched engine shares the convention: insert_segments' forced final
|
|
738
|
-
// 1-token segment + GenerationBatch._step, generate.py:1645/1327).
|
|
739
|
-
// Forwarding the last token as the tail of an L=n GEMM instead is
|
|
740
|
-
// ulp-different in bf16 (qmm-vs-qmv + L-dependent SDPA dispatch) in
|
|
741
|
-
// BOTH the step-0 logits and that token's stored KV — near-tie greedy
|
|
742
|
-
// streams flip (2026-07-07 12B completion-probe ✗: first token flip at
|
|
743
|
-
// step 24). MLX_BUN_PREFILL_TAIL_SPLIT=0 restores the old
|
|
744
|
-
// full-final-chunk convention (A/B lever + kill switch).
|
|
745
|
-
const tailSplit = flagOn("MLX_BUN_PREFILL_TAIL_SPLIT", true);
|
|
746
|
-
const drainGate = tailSplit ? 1 : prefillChunkSize;
|
|
747
|
-
while (promptTokens.length - pos > drainGate) {
|
|
748
|
-
const n = tailSplit
|
|
749
|
-
? Math.min(prefillChunkSize, promptTokens.length - pos - 1)
|
|
750
|
-
: prefillChunkSize;
|
|
751
|
-
const chunk = promptTokens.slice(pos, pos + n);
|
|
752
|
-
const ids = ops.fromInt32(chunk, [1, chunk.length]);
|
|
753
|
-
const h = await forwardHiddenForGeneration(model, ids, cache);
|
|
754
|
-
ids.dispose();
|
|
755
|
-
h.dispose(); // logits never computed for non-final chunks
|
|
756
|
-
evalCacheState(cache);
|
|
757
|
-
maybeQuantizeKv(cache, options);
|
|
758
|
-
// mlx-lm _prefill clears the allocator cache after every chunk;
|
|
759
|
-
// without this, prefill transients pile up in the buffer cache
|
|
760
|
-
// and the first decode step pays a one-shot reclaim stall that
|
|
761
|
-
// scales with prompt length (~800 ms after an 8k prefill —
|
|
762
|
-
// measured, scripts/decode-split.ts; the context-scaling decode
|
|
763
|
-
// gap's main term).
|
|
764
|
-
clearCache();
|
|
765
|
-
pos += n;
|
|
766
|
-
// Macrotask yield between chunks (runtime-isolation.md Phase 1).
|
|
767
|
-
await new Promise<void>((r) => setImmediate(r));
|
|
768
|
-
}
|
|
769
|
-
if (needsTokenHistory) {
|
|
770
|
-
history = ops.fromInt32(promptTokens, [promptTokens.length]);
|
|
1096
|
+
: undefined;
|
|
1097
|
+
const tailSplit = runtime.flag("MLX_BUN_PREFILL_TAIL_SPLIT", true);
|
|
1098
|
+
const forward = graph.forwardHidden.bind(graph);
|
|
1099
|
+
const maintain = () => maybeQuantizeKv(cache, options);
|
|
1100
|
+
while (true) {
|
|
1101
|
+
options.signal?.throwIfAborted();
|
|
1102
|
+
const step = nextPrefillStep({ length: promptTokens.length, position: pos,
|
|
1103
|
+
chunkSize: prefillChunkSize, tailSplit, snapshotAt });
|
|
1104
|
+
if (step.kind === "final" && needsTokenHistory) stepSampler.seedHistory(promptTokens);
|
|
1105
|
+
const closeChunk = diagnostics.trace?.begin("prefill.chunk", {
|
|
1106
|
+
mechanism: diagnostics.mechanism ?? "serial", startToken: step.start,
|
|
1107
|
+
tokens: step.end - step.start, ...(step.kind === "final" ? { final: true } : {}),
|
|
1108
|
+
});
|
|
1109
|
+
let hidden: MlxArray | null;
|
|
1110
|
+
try { hidden = await executeMlxPrefillStep(forward, cache, promptTokens, step, maintain); }
|
|
1111
|
+
finally { closeChunk?.(); }
|
|
1112
|
+
pos = step.end;
|
|
1113
|
+
if (hidden) { h0 = hidden; break; }
|
|
1114
|
+
if (runtime.value("MLX_BUN_PREFILL_MEM_LOG") === "1")
|
|
1115
|
+
console.error(`[prefill-mem] ${pos} active ${(activeMemory() / 2 ** 30).toFixed(2)} peak ${(peakMemory() / 2 ** 30).toFixed(2)}`);
|
|
1116
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
1117
|
+
if (step.snapshot) options.onPrefillDone?.();
|
|
771
1118
|
}
|
|
772
|
-
// Under tailSplit this is exactly [promptTokens[len-1]] — the L=1
|
|
773
|
-
// step-0 forward (uncompiled L=1 is bit-exact with compiled decode,
|
|
774
|
-
// tests/compiled-decode.test.ts). The last prompt token's KV enters
|
|
775
|
-
// the cache HERE, so downstream bookkeeping (forwarded[], cacheTokens,
|
|
776
|
-
// PromptCache.put alignment) is unchanged: after step 0 the caches
|
|
777
|
-
// cover exactly the prompt, as before.
|
|
778
|
-
const lastChunk = promptTokens.slice(pos);
|
|
779
|
-
const ids0 = ops.fromInt32(lastChunk, [1, lastChunk.length]);
|
|
780
|
-
h0 = await forwardHiddenForGeneration(model, ids0, cache);
|
|
781
|
-
ids0.dispose();
|
|
782
1119
|
}
|
|
783
1120
|
if (options.snapshotAt === undefined || options.snapshotAt >= promptTokens.length)
|
|
784
1121
|
options.onPrefillDone?.();
|
|
1122
|
+
// The trace's prefill span is the uncached model-forward portion only.
|
|
1123
|
+
// Token-0 lm-head, sampling, and readback are a separate additive stage.
|
|
1124
|
+
if (diagnostics.trace && runtime.value("MLX_BUN_P2R_SYNC") === "1")
|
|
1125
|
+
synchronize(gpuStream);
|
|
1126
|
+
closePrefill?.();
|
|
1127
|
+
closeTokenZero = diagnostics.trace?.begin("token_zero.total", {
|
|
1128
|
+
mechanism: diagnostics.mechanism ?? "serial",
|
|
1129
|
+
});
|
|
785
1130
|
const [, L0, H] = h0.shape as [number, number, number];
|
|
786
1131
|
const hLast = h0.slice([0, L0 - 1, 0], [1, L0, H]);
|
|
787
1132
|
h0.dispose();
|
|
788
|
-
const logits0 =
|
|
1133
|
+
const logits0 = graph.projectLogits(hLast, { type: "all" });
|
|
789
1134
|
hLast.dispose();
|
|
790
1135
|
const s0 = sampleStep(logits0, 0); // token array [1] (+ optional extras)
|
|
791
1136
|
pending = s0.tok;
|
|
@@ -799,31 +1144,17 @@ async function* generateInner(
|
|
|
799
1144
|
// to prompt_time, not decode — replicated so cross-stack decode
|
|
800
1145
|
// tok/s measure the same quantity. The boundary cost is real and
|
|
801
1146
|
// scales with prompt length; it belongs to "having prefilled".
|
|
802
|
-
ops.asyncEvalAll([pending, ...
|
|
1147
|
+
ops.asyncEvalAll([pending, ...stepExtrasArrays(pendingExtras)]);
|
|
1148
|
+
}
|
|
803
1149
|
|
|
804
1150
|
// ---- decode (pipelined) ----
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
// MoE models stay uncompiled: GatherQMM lacks output_shapes in mlx
|
|
813
|
-
// 0.6.0, and shapeless replay re-infers the whole tape whenever the
|
|
814
|
-
// growing attention windows change shape (= every step). Remove this
|
|
815
|
-
// when upstream implements GatherQMM::output_shapes.
|
|
816
|
-
let compiled =
|
|
817
|
-
flagOn("MLX_BUN_COMPILED_DECODE", true) &&
|
|
818
|
-
!options.adapters?.length &&
|
|
819
|
-
// Paged caches can't compile (data-dependent block-list length —
|
|
820
|
-
// the shapeless-replay hazard; CompiledDecode.supports() also
|
|
821
|
-
// excludes them per step, this just skips the setup).
|
|
822
|
-
!options.pagedKv &&
|
|
823
|
-
model.config.modelType.startsWith("gemma4") &&
|
|
824
|
-
!model.config.text.enableMoeBlock
|
|
825
|
-
? CompiledDecode.for(model as Gemma4Model)
|
|
826
|
-
: null;
|
|
1151
|
+
// Selection and transactional fallback belong to the graph's binding.
|
|
1152
|
+
decoder = options.decodePolicy?.compiledDecode === false ? null : binding.createDecode?.({
|
|
1153
|
+
hasAdapters: !!options.adapters?.length, pagedKv: !!options.pagedKv,
|
|
1154
|
+
}) ?? null;
|
|
1155
|
+
appender = fillOn ? binding.createAppend?.({
|
|
1156
|
+
hasAdapters: !!options.adapters?.length, pagedKv: !!options.pagedKv,
|
|
1157
|
+
}) ?? null : null;
|
|
827
1158
|
let stop = false;
|
|
828
1159
|
/** Token id read eagerly at the top of the loop for grammar (reused for
|
|
829
1160
|
* the yield, avoiding a second readback). -1 when grammar is off — the
|
|
@@ -836,11 +1167,38 @@ async function* generateInner(
|
|
|
836
1167
|
// (see GrammarController.jumpForward for the contract + the fidelity
|
|
837
1168
|
// note on why this is opt-in). Excluded when logprobs are requested
|
|
838
1169
|
// (jumped tokens are never sampled, so they'd have no logprobs rows).
|
|
839
|
-
const grammarJump = shouldUseGrammarJump(options);
|
|
1170
|
+
const grammarJump = options.decodePolicy?.grammarJump ?? shouldUseGrammarJump(options, runtime);
|
|
1171
|
+
// Yield token zero before its own decode forward. The captured runtime
|
|
1172
|
+
// keeps this scheduling choice stable across consumer awaits.
|
|
1173
|
+
const earlyFirstToken = runtime.value("MLX_BUN_EARLY_FIRST_TOKEN") === "1" &&
|
|
1174
|
+
maxTokens > 1 && !resuming && !fillOn && !options.grammar;
|
|
840
1175
|
while (!stop) {
|
|
1176
|
+
options.signal?.throwIfAborted();
|
|
841
1177
|
const cur = pending!;
|
|
842
|
-
const
|
|
843
|
-
|
|
1178
|
+
const stepIndex = generated;
|
|
1179
|
+
let curExtras = pendingExtras;
|
|
1180
|
+
let yieldedEarly = false;
|
|
1181
|
+
if (earlyFirstToken && stepIndex === 0) {
|
|
1182
|
+
const firstToken = ops.itemUint32(cur);
|
|
1183
|
+
options.signal?.throwIfAborted();
|
|
1184
|
+
if (!eosTokenIds.includes(firstToken)) {
|
|
1185
|
+
prefillMs = performance.now() - tPrefill;
|
|
1186
|
+
closeTokenZero?.();
|
|
1187
|
+
closeTokenZero = undefined;
|
|
1188
|
+
tDecode = performance.now();
|
|
1189
|
+
const logprobs = readExtras(curExtras);
|
|
1190
|
+
curExtras = null;
|
|
1191
|
+
pendingExtras = null;
|
|
1192
|
+
// Count before yielding so an early return reports this token. Its
|
|
1193
|
+
// KV is still absent; forwarded describes the prompt-only cache.
|
|
1194
|
+
generated++;
|
|
1195
|
+
yieldedEarly = true;
|
|
1196
|
+
yield { token: firstToken, index: stepIndex, ...(logprobs ? { logprobs } : {}) };
|
|
1197
|
+
options.signal?.throwIfAborted();
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
// Keep current extras owned by the run until readback succeeds. A
|
|
1201
|
+
// forward/grammar failure must leave them reachable by finally.
|
|
844
1202
|
// Grammar advance (src/grammar.ts): acceptToken needs the token id as a
|
|
845
1203
|
// JS number, which the pipelined loop defers (it operates on device
|
|
846
1204
|
// arrays). So grammar requests eager-read cur here, advance the matcher,
|
|
@@ -861,11 +1219,11 @@ async function* generateInner(
|
|
|
861
1219
|
let jumpEmit: number[] | null = null;
|
|
862
1220
|
if (options.grammar) {
|
|
863
1221
|
grammarTok = ops.itemUint32(cur);
|
|
864
|
-
if (
|
|
1222
|
+
if (stepIndex + 1 < maxTokens) {
|
|
865
1223
|
options.grammar.accept(grammarTok);
|
|
866
1224
|
await options.grammar.ready();
|
|
867
1225
|
if (grammarJump && !options.grammar.isTerminated) {
|
|
868
|
-
jumpEmit = options.grammar.jumpForward(maxTokens - (
|
|
1226
|
+
jumpEmit = options.grammar.jumpForward(maxTokens - (stepIndex + 1));
|
|
869
1227
|
// jumpForward advanced the matcher and fired the post-jump mask
|
|
870
1228
|
// fill; it must be ready before this iteration's sampleStep.
|
|
871
1229
|
if (jumpEmit) await options.grammar.ready();
|
|
@@ -886,98 +1244,113 @@ async function* generateInner(
|
|
|
886
1244
|
// following iteration (supports() re-checks the grown caches).
|
|
887
1245
|
maybeQuantizeKv(cache, options);
|
|
888
1246
|
pushHistory(cur);
|
|
889
|
-
|
|
890
|
-
const jt = ops.fromInt32(jumpEmit, [jumpEmit.length]);
|
|
891
|
-
const prev = history!;
|
|
892
|
-
history = ops.concatAxis([prev, jt], 0);
|
|
893
|
-
prev.dispose();
|
|
894
|
-
jt.dispose();
|
|
895
|
-
}
|
|
1247
|
+
stepSampler.commitNumbers(jumpEmit);
|
|
896
1248
|
const chunk = [grammarTok, ...jumpEmit];
|
|
897
1249
|
const ids = ops.fromInt32(chunk, [1, chunk.length]);
|
|
898
|
-
const h = await
|
|
1250
|
+
const h = await graph.forwardHidden(ids, cache);
|
|
899
1251
|
ids.dispose();
|
|
900
1252
|
// Every chunk token's KV is in the cache regardless of what follows.
|
|
901
1253
|
forwarded.push(...chunk);
|
|
902
|
-
const willGen =
|
|
1254
|
+
const willGen = stepIndex + 1 + jumpEmit.length;
|
|
903
1255
|
if (willGen < maxTokens && !options.grammar!.isTerminated) {
|
|
904
1256
|
const [, Lj, Hj] = h.shape as [number, number, number];
|
|
905
1257
|
const hLast = h.slice([0, Lj - 1, 0], [1, Lj, Hj]);
|
|
906
1258
|
h.dispose();
|
|
907
|
-
const logits =
|
|
1259
|
+
const logits = graph.projectLogits(hLast, { type: "all" });
|
|
908
1260
|
hLast.dispose();
|
|
909
1261
|
const sn = sampleStep(logits, willGen);
|
|
910
1262
|
nextPending = sn.tok;
|
|
911
1263
|
nextExtras = sn.extras;
|
|
912
1264
|
logits.dispose();
|
|
913
|
-
ops.asyncEvalAll([nextPending, ...
|
|
1265
|
+
ops.asyncEvalAll([nextPending, ...stepExtrasArrays(nextExtras)]);
|
|
914
1266
|
} else {
|
|
915
1267
|
h.dispose(); // burst ends the generation (max_tokens or grammar done)
|
|
916
1268
|
}
|
|
917
|
-
} else if (
|
|
1269
|
+
} else if (stepIndex + 1 < maxTokens && !options.grammar?.isTerminated) {
|
|
918
1270
|
maybeQuantizeKv(cache, options);
|
|
919
1271
|
pushHistory(cur);
|
|
920
1272
|
let logits: MlxArray | null = null;
|
|
921
1273
|
let evalWith: MlxArray[] = [];
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
evalWith = r.evalWith;
|
|
927
|
-
} catch (e) {
|
|
928
|
-
// Safe to re-forward the SAME token below: a failed step is
|
|
929
|
-
// transactional — segmented mode stages ring adopts and rolls
|
|
930
|
-
// back committed js-layer writes on throw (see #stepSegmented),
|
|
931
|
-
// and buffer growth done by a partial prepare is benign for the
|
|
932
|
-
// uncompiled path (updateAndFetch re-checks capacity).
|
|
933
|
-
compiled = null;
|
|
934
|
-
console.warn(`compiled decode disabled for this generation: ${e}`);
|
|
935
|
-
}
|
|
1274
|
+
const decoded = decoder?.tryStep(cur, cache);
|
|
1275
|
+
if (decoded) {
|
|
1276
|
+
logits = decoded.logits;
|
|
1277
|
+
evalWith = decoded.evalWith;
|
|
936
1278
|
}
|
|
937
1279
|
if (!logits) {
|
|
938
1280
|
const ids = ops.reshape(cur, [1, 1]);
|
|
939
|
-
const h = await
|
|
1281
|
+
const h = await graph.forwardHidden(ids, cache);
|
|
940
1282
|
ids.dispose();
|
|
941
|
-
logits =
|
|
1283
|
+
logits = graph.projectLogits(h, { type: "all" });
|
|
942
1284
|
h.dispose();
|
|
943
1285
|
}
|
|
944
|
-
const sn = sampleStep(logits,
|
|
1286
|
+
const sn = sampleStep(logits, stepIndex + 1);
|
|
945
1287
|
nextPending = sn.tok;
|
|
946
1288
|
nextExtras = sn.extras;
|
|
947
1289
|
logits.dispose();
|
|
948
|
-
ops.asyncEvalAll([nextPending, ...
|
|
1290
|
+
ops.asyncEvalAll([nextPending, ...stepExtrasArrays(nextExtras), ...evalWith]);
|
|
949
1291
|
}
|
|
950
1292
|
|
|
951
1293
|
// sync-read step n's token while n+1 computes
|
|
952
1294
|
// sync-read step n's token while n+1 computes (grammar already read it
|
|
953
1295
|
// eagerly above — reuse to avoid a second GPU sync)
|
|
954
1296
|
const token = options.grammar ? grammarTok : ops.itemUint32(cur);
|
|
1297
|
+
options.signal?.throwIfAborted();
|
|
955
1298
|
if (generated === 0) {
|
|
956
1299
|
// first token arrived: prompt clock stops, decode clock starts
|
|
957
1300
|
// (mlx-lm stream_generate's n==0 clock swap; the first token is
|
|
958
1301
|
// "free" on the decode clock there too)
|
|
959
1302
|
prefillMs = performance.now() - tPrefill;
|
|
1303
|
+
closeTokenZero?.();
|
|
1304
|
+
closeTokenZero = undefined;
|
|
960
1305
|
tDecode = performance.now();
|
|
961
1306
|
}
|
|
962
1307
|
cur.dispose();
|
|
963
1308
|
pending = null;
|
|
964
|
-
generated++;
|
|
1309
|
+
if (!yieldedEarly) generated++;
|
|
965
1310
|
// if a next-step graph was built, this token's KV entered the cache
|
|
966
1311
|
// (jump iterations pushed the whole chunk already)
|
|
967
1312
|
if (nextPending !== null && !jumpEmit) forwarded.push(token);
|
|
968
1313
|
|
|
969
1314
|
if (eosTokenIds.includes(token)) {
|
|
970
|
-
|
|
1315
|
+
finishReason = "stop";
|
|
1316
|
+
disposeStepExtras(curExtras);
|
|
1317
|
+
pendingExtras = null;
|
|
971
1318
|
nextPending?.dispose();
|
|
972
1319
|
nextPending = null;
|
|
973
|
-
|
|
1320
|
+
disposeStepExtras(nextExtras);
|
|
974
1321
|
nextExtras = null;
|
|
975
1322
|
stop = true;
|
|
976
1323
|
} else {
|
|
1324
|
+
// ---- token fast-forwarding (K3): APPEND, then yield ----------------
|
|
1325
|
+
// The engine already knows the next m tokens, so it writes them into
|
|
1326
|
+
// the KV itself with an append and resumes sampling after them. No
|
|
1327
|
+
// draft, no verify, no rollback: an injected token is indistinguishable
|
|
1328
|
+
// to the model from one it sampled. The append happens BEFORE any of
|
|
1329
|
+
// the burst's yields, which is what makes a consumer break mid-burst
|
|
1330
|
+
// safe — `forwarded` already describes the cache exactly.
|
|
1331
|
+
let fillEmit: number[] | null = null;
|
|
1332
|
+
if (fillOn) {
|
|
1333
|
+
if (jumpEmit)
|
|
1334
|
+
throw new Error("fill and grammar jump-forward cannot share an iteration");
|
|
1335
|
+
// A fill needs an in-flight step to ride; with nextPending null this
|
|
1336
|
+
// token's KV was never written and the generation ends here. push()
|
|
1337
|
+
// still runs so the session's history (and echo index) stay exact.
|
|
1338
|
+
const proposal = options.fill!.push(
|
|
1339
|
+
token, nextPending !== null ? maxTokens - generated : 0,
|
|
1340
|
+
);
|
|
1341
|
+
if (proposal && nextPending !== null) {
|
|
1342
|
+
const emitted = await applyProposal(proposal, generated);
|
|
1343
|
+
if (emitted.length) fillEmit = emitted;
|
|
1344
|
+
} else if (proposal) {
|
|
1345
|
+
options.fill!.commit(proposal, 0); // unreachable at budget 0; belt
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
977
1348
|
// readExtras before the yield: if the consumer breaks at this yield,
|
|
978
1349
|
// the extras are already read and disposed.
|
|
979
1350
|
const logprobs = readExtras(curExtras);
|
|
980
|
-
|
|
1351
|
+
pendingExtras = null;
|
|
1352
|
+
if (!yieldedEarly) yield { token, index: generated - 1, ...(logprobs ? { logprobs } : {}) };
|
|
1353
|
+
options.signal?.throwIfAborted();
|
|
981
1354
|
// mlx-lm generate_step: clear_cache after token 0 (drops the
|
|
982
1355
|
// remaining prefill transients) and every 256 tokens after
|
|
983
1356
|
if ((generated - 1) % 256 === 0) clearCache();
|
|
@@ -988,11 +1361,24 @@ async function* generateInner(
|
|
|
988
1361
|
// safe — `forwarded` already reflects the cache exactly.
|
|
989
1362
|
if (jumpEmit) {
|
|
990
1363
|
for (const jt of jumpEmit) {
|
|
1364
|
+
options.signal?.throwIfAborted();
|
|
991
1365
|
generated++;
|
|
992
1366
|
yield { token: jt, index: generated - 1 };
|
|
993
1367
|
if ((generated - 1) % 256 === 0) clearCache();
|
|
994
1368
|
}
|
|
995
1369
|
}
|
|
1370
|
+
// Fill burst: same one-yield-per-token shape as the jump burst, so
|
|
1371
|
+
// CompletionSink / StopMatcher / StreamDecoder see an ordinary stream
|
|
1372
|
+
// (a stop sequence inside an injected span fires exactly where it
|
|
1373
|
+
// would have in an unfilled run).
|
|
1374
|
+
if (fillEmit) {
|
|
1375
|
+
for (const ft of fillEmit) {
|
|
1376
|
+
options.signal?.throwIfAborted();
|
|
1377
|
+
generated++;
|
|
1378
|
+
yield { token: ft, index: generated - 1 };
|
|
1379
|
+
if ((generated - 1) % 256 === 0) clearCache();
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
996
1382
|
if (nextPending === null) {
|
|
997
1383
|
stop = true;
|
|
998
1384
|
} else {
|
|
@@ -1001,6 +1387,21 @@ async function* generateInner(
|
|
|
1001
1387
|
pendingExtras = nextExtras;
|
|
1002
1388
|
nextExtras = null;
|
|
1003
1389
|
}
|
|
1390
|
+
if (
|
|
1391
|
+
pending !== null &&
|
|
1392
|
+
options.onDecodeCheckpoint &&
|
|
1393
|
+
generated >= nextCheckpoint
|
|
1394
|
+
) {
|
|
1395
|
+
const pendingToken = ops.itemUint32(pending);
|
|
1396
|
+
await options.onDecodeCheckpoint({
|
|
1397
|
+
cacheTokens: [...promptTokens, ...forwarded],
|
|
1398
|
+
caches: cache,
|
|
1399
|
+
generatedTokens: generated,
|
|
1400
|
+
pendingToken,
|
|
1401
|
+
});
|
|
1402
|
+
const every = options.checkpointEveryTokens!;
|
|
1403
|
+
while (nextCheckpoint <= generated) nextCheckpoint += every;
|
|
1404
|
+
}
|
|
1004
1405
|
}
|
|
1005
1406
|
}
|
|
1006
1407
|
decodeMs = performance.now() - tDecode;
|
|
@@ -1008,21 +1409,28 @@ async function* generateInner(
|
|
|
1008
1409
|
return makeStats();
|
|
1009
1410
|
} catch (e) {
|
|
1010
1411
|
threw = true;
|
|
1412
|
+
executionError = e;
|
|
1011
1413
|
throw e;
|
|
1012
1414
|
} finally {
|
|
1013
1415
|
if (!finished) {
|
|
1014
1416
|
pending?.dispose();
|
|
1015
1417
|
nextPending?.dispose();
|
|
1016
|
-
|
|
1017
|
-
|
|
1418
|
+
disposeStepExtras(pendingExtras);
|
|
1419
|
+
disposeStepExtras(nextExtras);
|
|
1420
|
+
}
|
|
1421
|
+
try {
|
|
1422
|
+
const closing = decoder?.close();
|
|
1423
|
+
if (closing) await closing;
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
if (threw) throw new AggregateError([executionError, error],
|
|
1426
|
+
"AR execution and decoder cleanup failed", { cause: executionError });
|
|
1427
|
+
throw error;
|
|
1428
|
+
} finally {
|
|
1429
|
+
if (ownsCache) for (const c of cache) c.dispose();
|
|
1430
|
+
// TokenizerInfo is process-cached; this request owns the matcher.
|
|
1431
|
+
options.grammar?.dispose();
|
|
1432
|
+
stepSampler.dispose();
|
|
1018
1433
|
}
|
|
1019
|
-
if (ownsCache) for (const c of cache) c.dispose();
|
|
1020
|
-
// The grammar controller owns native WASM state (GrammarMatcher +
|
|
1021
|
-
// TokenizerInfo cache) — dispose on every exit path (normal, early break,
|
|
1022
|
-
// throw). TokenizerInfo itself is process-cached (vocab-structural), so
|
|
1023
|
-
// only the per-request matcher/compiled are freed here.
|
|
1024
|
-
options.grammar?.dispose();
|
|
1025
|
-
history?.dispose();
|
|
1026
1434
|
if (!finished && !threw) {
|
|
1027
1435
|
// forced early return (consumer break at a yield): still report
|
|
1028
1436
|
// stats — `forwarded` only lists tokens whose KV actually entered
|
|
@@ -1032,8 +1440,3 @@ async function* generateInner(
|
|
|
1032
1440
|
}
|
|
1033
1441
|
}
|
|
1034
1442
|
}
|
|
1035
|
-
|
|
1036
|
-
function disposing(old: MlxArray, next: MlxArray): MlxArray {
|
|
1037
|
-
old.dispose();
|
|
1038
|
-
return next;
|
|
1039
|
-
}
|