mlx-bun 0.2.0 → 0.4.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 -588
- package/docs/README.md +94 -0
- package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
- package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
- package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
- package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/archive/planning/release-notes-v0.3.0.md +35 -0
- package/docs/design/batching.md +1841 -0
- package/docs/design/decode-speed-program.md +2346 -169
- package/docs/design/dreaming-nightly-pipeline.md +675 -282
- package/docs/design/generic-model-support.md +655 -334
- package/docs/design/kv-cache.md +824 -0
- package/docs/design/orpo-training.md +673 -1239
- package/docs/design/speculative-decoding.md +1990 -0
- package/docs/design/turboquant.md +2212 -0
- package/docs/design/unified-engine-frontier-plan.md +1833 -727
- package/docs/design/web-chat-redesign.md +666 -1083
- package/docs/planning/PRODUCT_ROADMAP.md +50 -1
- package/docs/planning/release-notes-v0.4.0.md +45 -0
- package/docs/planning/video-series.md +2 -2
- package/docs/reference/benchmarks.md +3613 -0
- package/docs/reference/cli.md +434 -160
- package/docs/reference/distribution.md +328 -182
- package/docs/reference/environment.md +398 -0
- package/docs/reference/glossary.md +39 -0
- package/docs/reference/library-api.md +566 -168
- package/docs/reference/memory.md +227 -64
- package/docs/reference/models.md +113 -6
- package/docs/reference/server-api.md +944 -429
- package/docs/reference/server-config.md +740 -338
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +158 -0
- package/package.json +9 -4
- package/src/audio/decode.ts +2 -2
- package/src/audio/features.ts +2 -2
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +108 -0
- package/src/backends/mlx/batch-group.ts +1790 -0
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +106 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/diffusion.ts +39 -0
- package/src/backends/mlx/draft-target.ts +32 -0
- package/src/backends/mlx/evaluation.ts +105 -0
- package/src/backends/mlx/fill-append.ts +45 -0
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +130 -0
- package/src/backends/mlx/graph.ts +38 -0
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +133 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/memory-client.ts +189 -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-cohort.ts +89 -0
- package/src/backends/mlx/prefill-rows.ts +200 -0
- package/src/backends/mlx/prefill.ts +33 -0
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/request-policy.ts +20 -0
- package/src/backends/mlx/request-state-policy.ts +41 -0
- package/src/backends/mlx/rollback.ts +89 -0
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +242 -0
- package/src/backends/mlx/speculative-group.ts +359 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +114 -0
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +46 -0
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/text-engine.ts +73 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +165 -0
- package/src/cli.ts +66 -58
- package/src/client.ts +66 -0
- package/src/config.ts +21 -4
- package/src/contracts/completion.ts +16 -0
- package/src/contracts/execution.ts +60 -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/prefix-cache.ts +23 -0
- package/src/contracts/resources.ts +25 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +45 -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 +53 -0
- package/src/engine/resources.ts +53 -0
- package/src/engine/scheduler.ts +72 -0
- package/src/engine/session.ts +247 -0
- package/src/eval/README.md +44 -1
- package/src/eval/kl.ts +236 -3
- package/src/eval/reference-logits.ts +207 -0
- package/src/eval/runner.ts +8 -96
- 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 +30 -16
- package/src/generate.ts +709 -417
- package/src/grammar.ts +12 -7
- package/src/harness-pi.ts +14 -33
- package/src/hub-rest.ts +4 -4
- package/src/index.ts +7 -0
- package/src/inference/denoising.ts +17 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -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 +12 -12
- 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 +70 -12
- package/src/kv-store.ts +177 -63
- 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} +5 -5
- package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
- package/src/{model → lab/paged-kv}/paged-kv.ts +32 -15
- package/src/library.ts +40 -0
- package/src/lora.ts +18 -0
- package/src/media-fetch.ts +1 -1
- package/src/memory/article.ts +1 -1
- package/src/memory/chunk.ts +1 -1
- package/src/memory/model.ts +30 -169
- 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 +1 -3
- package/src/mlx/array.ts +11 -11
- package/src/mlx/compile.ts +12 -2
- package/src/mlx/ffi.ts +58 -11
- package/src/mlx/materialize.ts +23 -0
- package/src/mlx/metal-kernel.ts +10 -11
- package/src/mlx/ops.ts +33 -13
- 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/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +20 -6
- package/src/model/batched-quant.ts +9 -3
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +78 -17
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +65 -23
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +93 -0
- package/src/model/compiled-decode.ts +8 -2
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +55 -0
- package/src/model/diffusion-gemma.ts +8 -8
- package/src/model/factory.ts +76 -33
- package/src/model/fingerprint.ts +1 -1
- package/src/model/flash-attention.ts +3 -3
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +179 -80
- package/src/model/gemma4.ts +40 -32
- package/src/model/generated/gemma4-12b.ts +6 -3
- package/src/model/generated/gemma4-26b.ts +6 -3
- package/src/model/generated/gemma4-e4b.ts +6 -3
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +1 -1
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/implementation.ts +69 -0
- package/src/model/kv-attention-view.ts +67 -0
- package/src/model/minicpm5.ts +17 -15
- package/src/model/profile.ts +12 -2
- package/src/model/qwen-append.ts +12 -0
- package/src/model/qwen-conv.ts +52 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +202 -78
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- 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/turboquant-codec.ts +83 -0
- package/src/model/universal/dense.ts +15 -14
- package/src/model/universal/modules.ts +8 -1
- package/src/native-pack.ts +6 -5
- package/src/pi-provider.ts +6 -64
- package/src/pi-web.ts +8 -315
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +110 -69
- package/src/quantize/allocator.ts +1 -1
- package/src/quantize/config-writer.ts +10 -3
- package/src/quantize/rotate.ts +4 -4
- package/src/quantize/trellis.ts +586 -0
- package/src/responses.ts +28 -1
- package/src/runtime-config.ts +18 -4
- package/src/sampler.ts +40 -29
- package/src/serve/batch-scheduler.ts +8 -1671
- 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 +15 -7
- package/src/serve/discovery-routes.ts +12 -13
- package/src/serve/generation-gateway.ts +183 -213
- 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/lab-routes.ts +3 -0
- package/src/serve/lane-registry.ts +2 -1
- package/src/serve/model-admin-routes.ts +7 -5
- package/src/serve/model-binding.ts +43 -0
- package/src/serve/model-host.ts +511 -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/request-plan.ts +23 -18
- package/src/serve/request-prep.ts +344 -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 +5 -5
- package/src/serve/text-completion-stage.ts +104 -0
- package/src/serve/token-streams.ts +519 -0
- package/src/server.ts +335 -2711
- package/src/spec/assistant-rows.ts +92 -0
- package/src/spec/assistant-source.ts +31 -55
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +16 -14
- package/src/spec/dflash-source.ts +36 -12
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/ab-stats.ts +3 -3
- package/src/spec/dspark/calibration.ts +2 -2
- package/src/spec/dspark/deepspec-module.ts +97 -122
- 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 +129 -172
- 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-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -137
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +93 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +219 -0
- package/src/spec/qwen-mtp-source.ts +147 -195
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +201 -201
- package/src/spec/source.ts +110 -13
- package/src/spec/two-model-rows.ts +61 -0
- package/src/spec/two-model.ts +44 -102
- package/src/ssd-cache.ts +99 -19
- package/src/ssd-durability.ts +36 -20
- package/src/tool-call.ts +51 -14
- package/src/train/flash-cce.ts +2 -2
- package/src/train/forward.ts +6 -0
- package/src/train/loss.ts +1 -1
- package/src/train/prefix-shared.ts +5 -1
- package/src/train/segmented.ts +5 -3
- package/src/train/trainer.ts +6 -6
- package/src/vision/prompt.ts +1 -1
- package/src/vision/qwen3vl-tower.ts +52 -10
- package/src/web/app.html +1 -1
- package/src/web/app.js +62 -60
- package/src/web/src/adapters-panel.ts +3 -3
- package/src/web/src/assistant.ts +2 -36
- package/src/web/src/chat.ts +4 -4
- 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 +55 -9
- package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
- package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
- package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
- package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
- package/docs/archive/investigations/hlg-ab.html +0 -34
- package/docs/archive/investigations/hlg-explorer.html +0 -238
- package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
- package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
- package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
- package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
- package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
- package/docs/archive/investigations/hlg-report.html +0 -262
- package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
- package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
- package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
- package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
- package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
- package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
- package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
- package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
- package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
- package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
- package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
- package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
- package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
- package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
- package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
- package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
- package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
- package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
- package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
- package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
- package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
- package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
- package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
- package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
- package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
- package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
- package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
- package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
- package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
- package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
- package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
- package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
- package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
- package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
- package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
- package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
- package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
- package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
- package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
- package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
- package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
- package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
- 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 -318
- 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-serving-execution-seam.md +0 -81
- 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 -231
- 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 -415
- 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/turboquant-weights.md +0 -467
- 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/planning/IDEAS.md +0 -69
- package/docs/reference/embedding.md +0 -197
- package/docs/reference/features-matrix.md +0 -101
- package/docs/reference/orpo-quickstart.md +0 -135
- package/src/flags.ts +0 -11
- /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
- /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
- /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
- /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
- /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
- /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
- /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
- /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// TurboQuant KV-cache codec —
|
|
1
|
+
// TurboQuant KV-cache codec — reference operations ported op-for-op
|
|
2
2
|
// from the vendored vllm-metal reference
|
|
3
3
|
// (lab/repro/vllm-metal-turboquant/turboquant_reference.py). See
|
|
4
|
-
// docs/design/turboquant
|
|
5
|
-
// goldens/turboquant.json (via tests/turboquant-ops.test.ts) for the
|
|
4
|
+
// docs/design/turboquant.md for the algorithm writeup and
|
|
5
|
+
// goldens/turboquant.json (via tests/research/turboquant-ops.test.ts) for the
|
|
6
6
|
// bit-exactness oracle. Quantization groups run along the LAST axis
|
|
7
7
|
// (head_dim) only — never the token axis, per the standing invariant.
|
|
8
8
|
//
|
|
@@ -27,6 +27,10 @@ import {
|
|
|
27
27
|
type TurboQuantHeadDim, type TurboQuantValueBits,
|
|
28
28
|
} from "./turboquant-tables";
|
|
29
29
|
|
|
30
|
+
import { tryJointDecodePackedKv, type PackedKvArrays } from "./turboquant-kv-decode";
|
|
31
|
+
import { tryDecodePackedKvInverse256 } from "./turboquant-kv-inverse";
|
|
32
|
+
import { isShapelessTracing } from "./compile";
|
|
33
|
+
|
|
30
34
|
type S = MlxHandle;
|
|
31
35
|
|
|
32
36
|
const BLOCK_SIZE = 32;
|
|
@@ -421,6 +425,32 @@ export function decodeValuesRotated(
|
|
|
421
425
|
return outBf16;
|
|
422
426
|
}
|
|
423
427
|
|
|
428
|
+
/** Optional exact packed K/V decode. Inputs stay borrowed; the caller owns
|
|
429
|
+
* the returned pair. The eager path keeps the reference inverse rotation. */
|
|
430
|
+
export function tryDecodePackedKv(
|
|
431
|
+
inputs: PackedKvArrays, kBits: number, vBits: number, headDim: number,
|
|
432
|
+
deferV = false, s: S = gpuStream,
|
|
433
|
+
): [MlxArray, MlxArray] | null {
|
|
434
|
+
// Reject unsupported value widths before requesting their centroid table.
|
|
435
|
+
if (![2, 3, 4, 5, 8].includes(vBits)) return null;
|
|
436
|
+
if (!deferV && headDim === 256 && kBits === 8 && vBits === 3 &&
|
|
437
|
+
s === gpuStream && !isShapelessTracing() && inputs[0].shape[2]! >= 8192) {
|
|
438
|
+
const decoded = tryDecodePackedKvInverse256(
|
|
439
|
+
inputs, centroidsFor(vBits), signArray(256), kBits, vBits, headDim, s,
|
|
440
|
+
);
|
|
441
|
+
if (decoded) return decoded;
|
|
442
|
+
}
|
|
443
|
+
const decoded = tryJointDecodePackedKv(inputs, centroidsFor(vBits), kBits, vBits, headDim, deferV, s);
|
|
444
|
+
if (!decoded || deferV) return decoded;
|
|
445
|
+
const [key, rotated] = decoded;
|
|
446
|
+
try {
|
|
447
|
+
const value = fwht(rotated, false, s);
|
|
448
|
+
try { return [key, value.astype(Dtype.bfloat16, s)]; }
|
|
449
|
+
finally { value.dispose(); }
|
|
450
|
+
} catch (error) { key.dispose(); throw error; }
|
|
451
|
+
finally { rotated.dispose(); }
|
|
452
|
+
}
|
|
453
|
+
|
|
424
454
|
/** Undo the value rotation on an attention output computed against
|
|
425
455
|
* decodeValuesRotated values. f32 through the transform (matching the
|
|
426
456
|
* eager decode path's precision), returned in the input's dtype. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// GENERATED by scripts/regen
|
|
2
|
-
// Regenerate: bun scripts/regen
|
|
1
|
+
// GENERATED by scripts/regen.ts turboquant — DO NOT EDIT.
|
|
2
|
+
// Regenerate: bun scripts/regen.ts turboquant
|
|
3
3
|
//
|
|
4
4
|
// Source: lab/repro/vllm-metal-turboquant/turboquant_reference.py (vendored
|
|
5
5
|
// vllm-metal oracle, see PROVENANCE.md in that directory), executed under
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
/** Identity calculation owns bytes; this interface owns memo persistence. */
|
|
7
|
+
export interface ArtifactIdentityStore {
|
|
8
|
+
get(key: string): Promise<string | undefined>;
|
|
9
|
+
put(key: string, digest: string): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class FileArtifactIdentityStore implements ArtifactIdentityStore {
|
|
13
|
+
readonly #memory = new Map<string, string>();
|
|
14
|
+
readonly #pending = new Set<Promise<void>>();
|
|
15
|
+
constructor(readonly directory: string) {}
|
|
16
|
+
|
|
17
|
+
async get(key: string): Promise<string | undefined> {
|
|
18
|
+
const memory = this.#memory.get(key);
|
|
19
|
+
if (memory) return memory;
|
|
20
|
+
try {
|
|
21
|
+
const digest = await readFile(join(this.directory, key), "utf8");
|
|
22
|
+
if (!/^[0-9a-f]{64}$/.test(digest)) return undefined;
|
|
23
|
+
this.#memory.set(key, digest);
|
|
24
|
+
return digest;
|
|
25
|
+
} catch { return undefined; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
put(key: string, digest: string): void {
|
|
29
|
+
this.#memory.set(key, digest);
|
|
30
|
+
const temporary = join(this.directory, `${key}.${process.pid}.${randomUUID()}.tmp`);
|
|
31
|
+
const write = async () => {
|
|
32
|
+
try {
|
|
33
|
+
await mkdir(this.directory, { recursive: true });
|
|
34
|
+
await writeFile(temporary, digest);
|
|
35
|
+
await rename(temporary, join(this.directory, key));
|
|
36
|
+
} catch { await rm(temporary, { force: true }).catch(() => {}); }
|
|
37
|
+
};
|
|
38
|
+
const pending = write().finally(() => this.#pending.delete(pending));
|
|
39
|
+
this.#pending.add(pending);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async flush(): Promise<void> { await Promise.all(this.#pending); }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const defaultStore = new FileArtifactIdentityStore(join(homedir(), ".cache", "mlx-bun", "artifact-identities"));
|
|
46
|
+
export interface ArtifactIdentityFile { name: string; path: string; }
|
|
47
|
+
|
|
48
|
+
async function revision(files: readonly ArtifactIdentityFile[]): Promise<string> {
|
|
49
|
+
return JSON.stringify(await Promise.all(files.map(async file => {
|
|
50
|
+
const path = await realpath(file.path), info = await stat(path, { bigint: true });
|
|
51
|
+
return [file.name, path, info.dev, info.ino, info.size, info.mtimeNs, info.ctimeNs].map(String);
|
|
52
|
+
})));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Preserve the existing seed/name/byte digest, reusing it while file revisions
|
|
56
|
+
* are unchanged. ctime catches same-size edits even when mtime is restored.
|
|
57
|
+
* A memo miss computes the full identity; persistence never delays its caller. */
|
|
58
|
+
export async function artifactIdentity(seed: string, input: readonly ArtifactIdentityFile[],
|
|
59
|
+
store: ArtifactIdentityStore = defaultStore): Promise<string> {
|
|
60
|
+
const files = [...input].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
61
|
+
const before = await revision(files);
|
|
62
|
+
const key = createHash("sha256").update(JSON.stringify([seed, before])).digest("hex");
|
|
63
|
+
const cached = await store.get(key);
|
|
64
|
+
if (cached) return cached;
|
|
65
|
+
const digest = createHash("sha256").update(seed);
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
digest.update(file.name);
|
|
68
|
+
for await (const chunk of Bun.file(file.path).stream()) digest.update(chunk);
|
|
69
|
+
}
|
|
70
|
+
const value = digest.digest("hex");
|
|
71
|
+
if (before === await revision(files)) store.put(key, value);
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { MlxArray } from "../mlx/array";
|
|
2
|
+
import { Dtype } from "../mlx/ffi";
|
|
3
|
+
import * as ops from "../mlx/ops";
|
|
4
|
+
import { materializeCopy } from "../mlx/materialize";
|
|
5
|
+
import { KVCache, isPlainKvCache, type BatchableCache, type Cache, type Mask, type PaddedPrefillCache, type PrefillPadding } from "./gemma4-base";
|
|
6
|
+
import { FullPrefillPadding } from "./full-prefill-padding";
|
|
7
|
+
|
|
8
|
+
/** Full-attention storage with independently advancing row positions.
|
|
9
|
+
* Speculative rollback changes coverage, without moving the retained KV.
|
|
10
|
+
* Subsequent appends overwrite each row's rejected suffix in place when MLX
|
|
11
|
+
* can donate the buffer. Queue membership is outside this layout. */
|
|
12
|
+
export class BatchedKVCache implements BatchableCache, PaddedPrefillCache {
|
|
13
|
+
keys: MlxArray | null = null;
|
|
14
|
+
values: MlxArray | null = null;
|
|
15
|
+
rowOffsets: number[] = [];
|
|
16
|
+
leftPad: number[] = [];
|
|
17
|
+
#rope: MlxArray | undefined;
|
|
18
|
+
#beforeRound: number[] | undefined;
|
|
19
|
+
readonly #padding = new FullPrefillPadding();
|
|
20
|
+
|
|
21
|
+
restorePrefillEnds(ends: readonly number[] | undefined): void { this.#padding.restoreEnds(ends); }
|
|
22
|
+
preparePrefill(padding: PrefillPadding): void {
|
|
23
|
+
this.#padding.prepare(this, padding); this.#positionsChanged();
|
|
24
|
+
}
|
|
25
|
+
finalizePrefill(): void {
|
|
26
|
+
const next = this.#padding.finalize(this.state(), this);
|
|
27
|
+
if (!next) return;
|
|
28
|
+
this.keys?.dispose(); this.values?.dispose();
|
|
29
|
+
this.keys = next[0] ?? null; this.values = next[1] ?? null;
|
|
30
|
+
this.#positionsChanged();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
captureDonorRows(): import("./gemma4-base").KvDonorRows {
|
|
34
|
+
const B = this.rowOffsets.length, width = this.offset;
|
|
35
|
+
const keys = this.keys!.slice([0,0,0,0], [B,this.keys!.shape[1]!,width,this.keys!.shape[3]!]);
|
|
36
|
+
let values: MlxArray;
|
|
37
|
+
try { values = this.values!.slice([0,0,0,0], [B,this.values!.shape[1]!,width,this.values!.shape[3]!]); }
|
|
38
|
+
catch (error) { keys.dispose(); throw error; }
|
|
39
|
+
return { keys, values, offsets: [...this.rowOffsets], starts: [...this.leftPad], ends: this.#ends() };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
signature(): string { return "kv:batched-positions"; }
|
|
43
|
+
get batchSize(): number | null { return this.rowOffsets.length || null; }
|
|
44
|
+
get offset(): number { return Math.max(0, ...this.#ends()); }
|
|
45
|
+
#ends(): number[] { return this.rowOffsets.map((offset, row) => offset + this.leftPad[row]!); }
|
|
46
|
+
get ropeOffsetArr(): MlxArray | undefined {
|
|
47
|
+
// A common unpadded position uses the existing scalar RoPE operation.
|
|
48
|
+
if (this.leftPad.every(pad => pad === 0) && this.rowOffsets.every(offset => offset === this.rowOffsets[0])) return undefined;
|
|
49
|
+
return this.#rope ??= ops.fromInt32(this.rowOffsets, [this.rowOffsets.length]);
|
|
50
|
+
}
|
|
51
|
+
#positionsChanged(): void { this.#rope?.dispose(); this.#rope = undefined; }
|
|
52
|
+
makeEmptyBatch(): BatchedKVCache { return new BatchedKVCache(); }
|
|
53
|
+
bytesPerToken(): number {
|
|
54
|
+
if (!this.keys || !this.values) return 0;
|
|
55
|
+
return (this.keys.nbytes + this.values.nbytes) / (this.rowOffsets.length * this.keys.shape[2]!);
|
|
56
|
+
}
|
|
57
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * tokens; }
|
|
58
|
+
state(): MlxArray[] { return this.keys && this.values ? [this.keys, this.values] : []; }
|
|
59
|
+
isTrimmable(): boolean { return true; }
|
|
60
|
+
trim(count: number): void {
|
|
61
|
+
this.rowOffsets = this.rowOffsets.map(offset => offset - count);
|
|
62
|
+
this.#positionsChanged();
|
|
63
|
+
}
|
|
64
|
+
specRoundBegin(): void { this.#beforeRound = [...this.rowOffsets]; }
|
|
65
|
+
specRoundCommit(): void { this.#beforeRound = undefined; }
|
|
66
|
+
specRoundRollback(keep: number | readonly number[]): void {
|
|
67
|
+
this.rowOffsets = this.#beforeRound!.map((offset, row) => offset + (typeof keep === "number" ? keep : keep[row]!));
|
|
68
|
+
this.#beforeRound = undefined;
|
|
69
|
+
this.#positionsChanged();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
makeMask(N: number, window: number | null): Mask {
|
|
73
|
+
const B = this.rowOffsets.length, ends = this.#ends(), S = this.offset + N;
|
|
74
|
+
if (window === null && this.leftPad.every(pad => pad === 0) && ends.every(end => end === ends[0]))
|
|
75
|
+
return { mode: N === 1 ? "" : "causal", arr: null };
|
|
76
|
+
using starts = ops.fromInt32(ends, [B, 1, 1, 1]);
|
|
77
|
+
using relative = ops.arange(0, N, 1, Dtype.int32);
|
|
78
|
+
using queries = ops.reshape(relative, [1, 1, N, 1]);
|
|
79
|
+
using position = ops.add(starts, queries);
|
|
80
|
+
using indices = ops.arange(0, S, 1, Dtype.int32);
|
|
81
|
+
using keys = ops.reshape(indices, [1, 1, 1, S]);
|
|
82
|
+
using pads = ops.fromInt32(this.leftPad, [B, 1, 1, 1]);
|
|
83
|
+
using causal = ops.lessEqual(keys, position);
|
|
84
|
+
using valid = ops.greaterEqual(keys, pads);
|
|
85
|
+
const mask = ops.logicalAnd(causal, valid);
|
|
86
|
+
if (window === null) return { mode: "array", arr: mask };
|
|
87
|
+
using windowSize = ops.fromInt32([window], []);
|
|
88
|
+
using limit = ops.add(keys, windowSize);
|
|
89
|
+
using within = ops.less(position, limit);
|
|
90
|
+
try { return { mode: "array", arr: ops.logicalAnd(mask, within) }; }
|
|
91
|
+
finally { mask.dispose(); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
95
|
+
const B = this.rowOffsets.length, N = k.shape[2]!, ends = this.#ends();
|
|
96
|
+
const needed = Math.max(...ends) + N;
|
|
97
|
+
const grow = (array: MlxArray) => {
|
|
98
|
+
const [rows, heads, capacity, width] = array.shape as [number, number, number, number];
|
|
99
|
+
if (capacity >= needed) return array;
|
|
100
|
+
using padding = ops.zeros([rows, heads, Math.ceil(needed / 256) * 256 - capacity, width], array.dtype);
|
|
101
|
+
const grown = ops.concatAxis([array, padding], 2);
|
|
102
|
+
array.dispose();
|
|
103
|
+
return grown;
|
|
104
|
+
};
|
|
105
|
+
const allocate = (input: MlxArray) => ops.zeros(
|
|
106
|
+
[B, input.shape[1]!, Math.ceil(needed / 256) * 256, input.shape[3]!], input.dtype);
|
|
107
|
+
this.keys = this.keys ? grow(this.keys) : allocate(k);
|
|
108
|
+
this.values = this.values ? grow(this.values) : allocate(v);
|
|
109
|
+
const common = ends.every(end => end === ends[0]);
|
|
110
|
+
using positions = common ? null : ops.fromInt32(ends.flatMap(end =>
|
|
111
|
+
Array.from({ length: N }, (_, position) => end + position)), [B, 1, N, 1]);
|
|
112
|
+
const append = (array: MlxArray, values: MlxArray) => common
|
|
113
|
+
? ops.sliceUpdate(array, values, [0, 0, ends[0]!, 0], [B, array.shape[1]!, ends[0]! + N, array.shape[3]!])
|
|
114
|
+
: ops.putAlongAxis(array, positions!, values, 2);
|
|
115
|
+
const keys = append(this.keys, k);
|
|
116
|
+
let values: MlxArray;
|
|
117
|
+
try { values = append(this.values, v); }
|
|
118
|
+
catch (error) { keys.dispose(); throw error; }
|
|
119
|
+
this.keys.dispose(); this.values.dispose();
|
|
120
|
+
this.keys = keys; this.values = values;
|
|
121
|
+
this.rowOffsets = this.rowOffsets.map(offset => offset + N);
|
|
122
|
+
this.#positionsChanged();
|
|
123
|
+
return [keys.slice([0, 0, 0, 0], [B, keys.shape[1]!, needed, keys.shape[3]!]),
|
|
124
|
+
values.slice([0, 0, 0, 0], [B, values.shape[1]!, needed, values.shape[3]!])];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
128
|
+
const only = rows.length === 1 ? rows[0] : undefined;
|
|
129
|
+
if (only && (only instanceof BatchedKVCache || isPlainKvCache(only))) {
|
|
130
|
+
// Preserve allocated capacity when a request starts alone. Immutable
|
|
131
|
+
// aliases transfer ownership without copying its prefill storage.
|
|
132
|
+
const keys = only.keys ? ops.copyOf(only.keys) : null;
|
|
133
|
+
let values: MlxArray | null;
|
|
134
|
+
try { values = only.values ? ops.copyOf(only.values) : null; }
|
|
135
|
+
catch (error) { keys?.dispose(); throw error; }
|
|
136
|
+
const offsets = only instanceof BatchedKVCache ? [...only.rowOffsets] : [only.offset];
|
|
137
|
+
const pads = only instanceof BatchedKVCache ? [...only.leftPad] : [0];
|
|
138
|
+
this.dispose(); this.keys = keys; this.values = values;
|
|
139
|
+
this.rowOffsets = offsets; this.leftPad = pads;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const chunks: Array<{ keys: MlxArray | null; values: MlxArray | null; offsets: number[]; pads: number[]; width: number }> = [];
|
|
143
|
+
const held: MlxArray[] = [];
|
|
144
|
+
try {
|
|
145
|
+
for (const cache of rows) {
|
|
146
|
+
let keys: MlxArray | null, values: MlxArray | null, offsets: number[], pads: number[];
|
|
147
|
+
if (cache instanceof BatchedKVCache && cache.offset === 0) {
|
|
148
|
+
keys = values = null; offsets = [...cache.rowOffsets]; pads = [...cache.leftPad];
|
|
149
|
+
} else if (cache instanceof BatchedKVCache) {
|
|
150
|
+
const B = cache.rowOffsets.length, width = cache.offset;
|
|
151
|
+
keys = cache.keys!.slice([0, 0, 0, 0], [B, cache.keys!.shape[1]!, width, cache.keys!.shape[3]!]);
|
|
152
|
+
held.push(keys);
|
|
153
|
+
values = cache.values!.slice([0, 0, 0, 0], [B, cache.values!.shape[1]!, width, cache.values!.shape[3]!]);
|
|
154
|
+
held.push(values);
|
|
155
|
+
offsets = [...cache.rowOffsets]; pads = [...cache.leftPad];
|
|
156
|
+
} else if (isPlainKvCache(cache)) {
|
|
157
|
+
if (cache.offset === 0) keys = values = null;
|
|
158
|
+
else { [keys, values] = cache.temporalView(); held.push(keys, values); }
|
|
159
|
+
offsets = [cache.offset]; pads = [0];
|
|
160
|
+
} else throw new Error(`full-attention batch cannot merge ${cache.signature()}`);
|
|
161
|
+
chunks.push({ keys, values, offsets, pads, width: keys?.shape[2] ?? 0 });
|
|
162
|
+
}
|
|
163
|
+
const width = Math.max(0, ...chunks.map(chunk => chunk.width));
|
|
164
|
+
const padded = (array: MlxArray, count: number) => {
|
|
165
|
+
if (!count) return array;
|
|
166
|
+
using padding = ops.zeros([array.shape[0]!, array.shape[1]!, count, array.shape[3]!], array.dtype);
|
|
167
|
+
const result = ops.concatAxis([padding, array], 2); held.push(result); return result;
|
|
168
|
+
};
|
|
169
|
+
const merge = (field: "keys" | "values") => {
|
|
170
|
+
if (!width) return null; // Geometry arrives with the first append.
|
|
171
|
+
const prototype = chunks.find(chunk => chunk[field])![field]!;
|
|
172
|
+
return ops.concatAxis(chunks.map(chunk => {
|
|
173
|
+
const array = chunk[field];
|
|
174
|
+
if (array) return padded(array, width - chunk.width);
|
|
175
|
+
const empty = ops.zeros([chunk.offsets.length, prototype.shape[1]!, width, prototype.shape[3]!], prototype.dtype);
|
|
176
|
+
held.push(empty); return empty;
|
|
177
|
+
}), 0);
|
|
178
|
+
};
|
|
179
|
+
const keys = merge("keys");
|
|
180
|
+
let values: MlxArray | null;
|
|
181
|
+
try { values = merge("values"); }
|
|
182
|
+
catch (error) { keys?.dispose(); throw error; }
|
|
183
|
+
this.dispose();
|
|
184
|
+
this.keys = keys; this.values = values;
|
|
185
|
+
this.rowOffsets = chunks.flatMap(chunk => chunk.offsets);
|
|
186
|
+
this.leftPad = chunks.flatMap(chunk => chunk.pads.map(pad => pad + width - chunk.width));
|
|
187
|
+
} finally { for (const array of held) array.dispose(); }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
filterRows(rows: readonly number[]): void {
|
|
191
|
+
if (!rows.length) { this.dispose(); return; }
|
|
192
|
+
const pads = rows.map(row => this.leftPad[row]!);
|
|
193
|
+
const sharedPadding = Math.min(...pads);
|
|
194
|
+
using indices = this.keys ? ops.fromInt32([...rows], [rows.length]) : null;
|
|
195
|
+
const select = (array: MlxArray | null): MlxArray | null => {
|
|
196
|
+
if (!array) return null;
|
|
197
|
+
const selected = ops.takeAxis(array, indices!, 0);
|
|
198
|
+
if (!sharedPadding) return selected;
|
|
199
|
+
// mlx-lm BatchKVCache.filter removes padding common to all survivors.
|
|
200
|
+
// Keeping it changes attention reduction geometry after retirement.
|
|
201
|
+
try { return selected.slice([0, 0, sharedPadding, 0], [...selected.shape]); }
|
|
202
|
+
finally { selected.dispose(); }
|
|
203
|
+
};
|
|
204
|
+
const keys = select(this.keys);
|
|
205
|
+
let values: MlxArray | null;
|
|
206
|
+
try { values = select(this.values); }
|
|
207
|
+
catch (error) { keys?.dispose(); throw error; }
|
|
208
|
+
this.keys?.dispose(); this.values?.dispose();
|
|
209
|
+
this.keys = keys; this.values = values;
|
|
210
|
+
this.rowOffsets = rows.map(row => this.rowOffsets[row]!);
|
|
211
|
+
this.leftPad = pads.map(pad => pad - sharedPadding);
|
|
212
|
+
this.#padding.filter(rows);
|
|
213
|
+
this.#positionsChanged();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
extractRow(row: number): KVCache {
|
|
217
|
+
const start = this.leftPad[row]!, count = this.rowOffsets[row]!;
|
|
218
|
+
if (count === 0) return new KVCache();
|
|
219
|
+
const copy = (array: MlxArray) => {
|
|
220
|
+
using view = array.slice([row, 0, start, 0], [row + 1, array.shape[1]!, start + count, array.shape[3]!]);
|
|
221
|
+
return materializeCopy(view);
|
|
222
|
+
};
|
|
223
|
+
const keys = copy(this.keys!);
|
|
224
|
+
let values: MlxArray;
|
|
225
|
+
try { values = copy(this.values!); }
|
|
226
|
+
catch (error) { keys.dispose(); throw error; }
|
|
227
|
+
const cache = new KVCache(); cache.restoreState(keys, values, count); return cache;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
dispose(): void {
|
|
231
|
+
this.keys?.dispose(); this.values?.dispose(); this.#positionsChanged();
|
|
232
|
+
this.keys = this.values = null;
|
|
233
|
+
this.rowOffsets = []; this.leftPad = []; this.#beforeRound = undefined; this.#padding.clear();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Companion to src/train/forward.ts's buildBatchedPadMask, which builds the
|
|
4
4
|
// PREFILL mask ([B,1,L,L], offset 0, right-padded — the training case, already
|
|
5
|
-
// parity-proven by tests/train-batch-e2e.test.ts). This module covers the
|
|
5
|
+
// parity-proven by tests/parity/train-batch-e2e.test.ts). This module covers the
|
|
6
6
|
// DECODE case for batched serving: left-padded rows sharing one growing
|
|
7
7
|
// [B,H,S,D] KV buffer, where a step forwards N new query tokens at a nonzero
|
|
8
8
|
// offset. (The two builders should be consolidated when batched prefill is
|
|
9
|
-
// wired into the serving path — phase S1a, docs/design/
|
|
9
|
+
// wired into the serving path — phase S1a, docs/design/batching.md.)
|
|
10
10
|
//
|
|
11
11
|
// Why left-padding for decode: with rows right-aligned in the KV buffer, every
|
|
12
12
|
// row's next write lands in the SAME column, so one advancing offset serves
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// thing the mask must encode is (a) causality, (b) each row's left padding,
|
|
16
16
|
// and (c) the sliding window, if any.
|
|
17
17
|
|
|
18
|
+
import { cacheSignature } from "./gemma4-base";
|
|
18
19
|
import { MlxArray } from "../mlx/array";
|
|
19
20
|
import * as ops from "../mlx/ops";
|
|
20
21
|
import { createCausalMask, KVCache, type Cache, type Mask } from "./gemma4-base";
|
|
@@ -92,6 +93,11 @@ export class BatchedDecodeMaskCache implements Cache {
|
|
|
92
93
|
#ropeArr: MlxArray | null = null;
|
|
93
94
|
#ropeForOffset = -1;
|
|
94
95
|
|
|
96
|
+
/** A decorator, not a storage kind: never claims the inner cache's
|
|
97
|
+
* identity (isPlainKvCache(wrapper) must stay false — it is not a
|
|
98
|
+
* KVCache), but records what it wraps for diagnostics/persistence. */
|
|
99
|
+
signature(): string { return `kv:batched-mask:${cacheSignature(this.inner)}`; }
|
|
100
|
+
|
|
95
101
|
constructor(
|
|
96
102
|
private readonly inner: Cache,
|
|
97
103
|
private readonly B: number,
|
|
@@ -253,13 +259,21 @@ export function extractKVRow(cache: KVCache, leftPad: number, i: number): KVCach
|
|
|
253
259
|
|
|
254
260
|
/** mlx-lm `filter`: keep only `keep` (sorted row indices) along the batch
|
|
255
261
|
* axis — eviction of finished sequences from a batched [B,H,S,D] buffer.
|
|
256
|
-
*
|
|
262
|
+
* `trimLeft` removes padding shared by all survivors (the caller updates
|
|
263
|
+
* offsets and per-row padding). Caller owns the result; inputs stay owned. */
|
|
257
264
|
export function filterKVRows(
|
|
258
|
-
keys: MlxArray, values: MlxArray, keep: number[],
|
|
265
|
+
keys: MlxArray, values: MlxArray, keep: number[], trimLeft = 0,
|
|
259
266
|
): { keys: MlxArray; values: MlxArray } {
|
|
260
267
|
const idx = MlxArray.fromInt32(Int32Array.from(keep), [keep.length]);
|
|
261
|
-
const
|
|
262
|
-
|
|
268
|
+
const take = (a: MlxArray) => {
|
|
269
|
+
const rows = ops.takeAxis(a, idx, 0);
|
|
270
|
+
if (trimLeft === 0) return rows;
|
|
271
|
+
const trimmed = rows.slice([0, 0, trimLeft, 0], rows.shape);
|
|
272
|
+
rows.dispose();
|
|
273
|
+
return trimmed;
|
|
274
|
+
};
|
|
275
|
+
const k = take(keys);
|
|
276
|
+
const v = take(values);
|
|
263
277
|
idx.dispose();
|
|
264
278
|
return { keys: k, values: v };
|
|
265
279
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// (maybeQuantizeKv at chunk boundaries — see BatchScheduler.#quantizeSolo),
|
|
18
18
|
// so a row's quantized bytes are bit-exact vs serial `--kv-quant config` by
|
|
19
19
|
// construction; this module only re-arranges those bytes across the batch
|
|
20
|
-
// axis. Gates: tests/batched-kv-quant-parity.test.ts.
|
|
20
|
+
// axis. Gates: tests/parity/batched-kv-quant-parity.test.ts.
|
|
21
21
|
|
|
22
22
|
import { MlxArray } from "../mlx/array";
|
|
23
23
|
import * as ops from "../mlx/ops";
|
|
@@ -134,11 +134,17 @@ export function extractQuantRow(
|
|
|
134
134
|
/** Quantized twin of filterKVRows: keep `keep` rows along the batch axis.
|
|
135
135
|
* Caller owns the result; inputs are not disposed. */
|
|
136
136
|
export function filterQuantRows(
|
|
137
|
-
keys: ops.QuantizedTensor, values: ops.QuantizedTensor, keep: number[],
|
|
137
|
+
keys: ops.QuantizedTensor, values: ops.QuantizedTensor, keep: number[], trimLeft = 0,
|
|
138
138
|
): { keys: ops.QuantizedTensor; values: ops.QuantizedTensor } {
|
|
139
139
|
const idx = MlxArray.fromInt32(Int32Array.from(keep), [keep.length]);
|
|
140
140
|
const take = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
|
|
141
|
-
tripleMap(t, (a) =>
|
|
141
|
+
tripleMap(t, (a) => {
|
|
142
|
+
const rows = ops.takeAxis(a, idx, 0);
|
|
143
|
+
if (trimLeft === 0) return rows;
|
|
144
|
+
const trimmed = rows.slice([0, 0, trimLeft, 0], rows.shape);
|
|
145
|
+
rows.dispose();
|
|
146
|
+
return trimmed;
|
|
147
|
+
});
|
|
142
148
|
const k = take(keys);
|
|
143
149
|
const v = take(values);
|
|
144
150
|
idx.dispose();
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import { cleanupFailure, disposeResources } from "../engine/resources";
|
|
4
|
+
import { BatchedKVCache } from "./batched-kv";
|
|
5
|
+
import { KVCache, QuantizedKVCache, isQuantizedKvCache,
|
|
6
|
+
type BatchableCache, type Cache, type Mask, type QuantizedAttentionState, type PaddedPrefillCache, type PrefillPadding } from "./gemma4-base";
|
|
7
|
+
|
|
8
|
+
import { KvTensorRows } from "../backends/mlx/kv-tensor-rows";
|
|
9
|
+
import { quantizedDonorAttention } from "./kv-attention-view";
|
|
10
|
+
|
|
11
|
+
const FIELDS = ["packed", "scales", "biases"] as const;
|
|
12
|
+
|
|
13
|
+
/** Affine KV uses the same row-position storage operations for its packed
|
|
14
|
+
* values, scales and biases. No dequantization or requantization is needed
|
|
15
|
+
* for merge, rollback, retirement or extraction. Inputs share this codec. */
|
|
16
|
+
export class BatchedQuantizedKVCache implements BatchableCache, QuantizedAttentionState, PaddedPrefillCache {
|
|
17
|
+
#planes = FIELDS.map(() => new BatchedKVCache());
|
|
18
|
+
#minimum: number[] = [];
|
|
19
|
+
get minimumReusableOffset(): number { return Math.max(0, ...this.#minimum); }
|
|
20
|
+
constructor(readonly groupSize: number, readonly bits: number) {}
|
|
21
|
+
restorePrefillEnds(ends: readonly number[] | undefined): void { for (const plane of this.#planes) plane.restorePrefillEnds(ends); }
|
|
22
|
+
preparePrefill(padding: PrefillPadding): void {
|
|
23
|
+
for (const plane of this.#planes) plane.preparePrefill(padding);
|
|
24
|
+
if (!this.#minimum.length) this.#minimum = padding.lengths.map(() => 0);
|
|
25
|
+
}
|
|
26
|
+
finalizePrefill(): void { for (const plane of this.#planes) plane.finalizePrefill(); }
|
|
27
|
+
get quantizedAttention(): QuantizedAttentionState { return this; }
|
|
28
|
+
captureDonorAttention(): import("./gemma4-base").KvDonorAttention {
|
|
29
|
+
const views: import("./gemma4-base").KvDonorRows[] = [];
|
|
30
|
+
try {
|
|
31
|
+
for (const plane of this.#planes) views.push(plane.captureDonorRows());
|
|
32
|
+
const field = (name: "keys" | "values") => ({
|
|
33
|
+
packed: views[0]![name], scales: views[1]![name], biases: views[2]![name],
|
|
34
|
+
});
|
|
35
|
+
const { offsets, starts, ends } = views[0]!;
|
|
36
|
+
return quantizedDonorAttention(field("keys"), field("values"), { offsets, starts, ends }, this.groupSize, this.bits);
|
|
37
|
+
} catch (error) { disposeResources(views.flatMap(view => [view.keys, view.values])); throw error; }
|
|
38
|
+
}
|
|
39
|
+
signature(): string { return `kv:batched-quant:${this.bits}:${this.groupSize}`; }
|
|
40
|
+
get rowOffsets(): readonly number[] { return this.#planes[0]!.rowOffsets; }
|
|
41
|
+
get leftPad(): readonly number[] { return this.#planes[0]!.leftPad; }
|
|
42
|
+
get batchSize(): number | null { return this.#planes[0]!.batchSize; }
|
|
43
|
+
get offset(): number { return this.#planes[0]!.offset; }
|
|
44
|
+
get ropeOffsetArr(): MlxArray | undefined { return this.#planes[0]!.ropeOffsetArr; }
|
|
45
|
+
makeEmptyBatch(): BatchedQuantizedKVCache { return new BatchedQuantizedKVCache(this.groupSize, this.bits); }
|
|
46
|
+
bytesPerToken(): number { return this.#planes.reduce((n, plane) => n + plane.bytesPerToken(), 0); }
|
|
47
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * tokens; }
|
|
48
|
+
makeMask(tokens: number, window: number | null): Mask { return this.#planes[0]!.makeMask(tokens, window); }
|
|
49
|
+
isTrimmable(): boolean { return true; }
|
|
50
|
+
trim(count: number): void { for (const plane of this.#planes) plane.trim(count); }
|
|
51
|
+
specRoundBegin(): void { for (const plane of this.#planes) plane.specRoundBegin(); }
|
|
52
|
+
specRoundCommit(): void { for (const plane of this.#planes) plane.specRoundCommit(); }
|
|
53
|
+
specRoundRollback(keep: number | readonly number[]): void {
|
|
54
|
+
for (const plane of this.#planes) plane.specRoundRollback(keep);
|
|
55
|
+
}
|
|
56
|
+
updateAndFetch(): [MlxArray, MlxArray] { throw new Error("quantized attention uses updateAndFetchQuantized"); }
|
|
57
|
+
|
|
58
|
+
updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
59
|
+
using inputs = new DisposableStack();
|
|
60
|
+
const key = ops.quantize(k, this.groupSize, this.bits);
|
|
61
|
+
for (const field of FIELDS) inputs.use(key[field]);
|
|
62
|
+
const value = ops.quantize(v, this.groupSize, this.bits);
|
|
63
|
+
for (const field of FIELDS) inputs.use(value[field]);
|
|
64
|
+
const held: MlxArray[] = [], keys: Partial<ops.QuantizedTensor> = {}, values: Partial<ops.QuantizedTensor> = {};
|
|
65
|
+
try {
|
|
66
|
+
for (const [index, field] of FIELDS.entries()) {
|
|
67
|
+
const [kk, vv] = this.#planes[index]!.updateAndFetch(key[field], value[field]);
|
|
68
|
+
held.push(kk, vv); keys[field] = kk; values[field] = vv;
|
|
69
|
+
}
|
|
70
|
+
return [keys as ops.QuantizedTensor, values as ops.QuantizedTensor];
|
|
71
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(held)); }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Borrow prepared request state. All planes stage replacements before
|
|
75
|
+
* releasing the current group, preserving donor ownership on failure. */
|
|
76
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
77
|
+
const next: BatchedKVCache[] = [];
|
|
78
|
+
try {
|
|
79
|
+
for (const [index, field] of FIELDS.entries()) {
|
|
80
|
+
const held: KVCache[] = [], inputs: Cache[] = [];
|
|
81
|
+
try {
|
|
82
|
+
for (const row of rows) {
|
|
83
|
+
if (row instanceof BatchedQuantizedKVCache) inputs.push(row.#planes[index]!);
|
|
84
|
+
else if (isQuantizedKvCache(row)) {
|
|
85
|
+
const view = new KVCache(); held.push(view);
|
|
86
|
+
if (row.offset > 0) {
|
|
87
|
+
const key = ops.copyOf(row.keys![field]);
|
|
88
|
+
let value: MlxArray;
|
|
89
|
+
try { value = ops.copyOf(row.values![field]); }
|
|
90
|
+
catch (error) { key.dispose(); throw error; }
|
|
91
|
+
view.restoreState(key, value, row.offset);
|
|
92
|
+
}
|
|
93
|
+
inputs.push(view);
|
|
94
|
+
} else throw new Error(`quantized row layout cannot merge ${row.signature()}`);
|
|
95
|
+
}
|
|
96
|
+
const plane = new BatchedKVCache(); next.push(plane); plane.mergeRows(inputs);
|
|
97
|
+
} finally { disposeResources(held); }
|
|
98
|
+
}
|
|
99
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(next)); }
|
|
100
|
+
const previous = this.#planes; this.#planes = next; disposeResources(previous);
|
|
101
|
+
this.#minimum = rows.flatMap(row => row instanceof BatchedQuantizedKVCache ? row.#minimum : [row.minimumReusableOffset ?? 0]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
alignRows(leftPad: readonly number[]): void {
|
|
105
|
+
if (leftPad.every((pad, row) => pad === this.leftPad[row])) return;
|
|
106
|
+
const aligned = new KvTensorRows();
|
|
107
|
+
try {
|
|
108
|
+
aligned.mergeRows([{ planes: this.#planes.flatMap(plane => plane.keys ? [plane.keys, plane.values!] : []),
|
|
109
|
+
rowOffsets: this.rowOffsets, leftPad: this.leftPad }]);
|
|
110
|
+
aligned.alignRows(leftPad);
|
|
111
|
+
for (const plane of this.#planes) {
|
|
112
|
+
plane.keys?.dispose(); plane.values?.dispose();
|
|
113
|
+
plane.keys = aligned.planes.shift() ?? null; plane.values = aligned.planes.shift() ?? null;
|
|
114
|
+
plane.leftPad = [...leftPad];
|
|
115
|
+
}
|
|
116
|
+
} finally { aligned.dispose(); }
|
|
117
|
+
}
|
|
118
|
+
filterRows(keep: readonly number[]): void {
|
|
119
|
+
for (const plane of this.#planes) plane.filterRows(keep);
|
|
120
|
+
this.#minimum = keep.map(row => this.#minimum[row]!);
|
|
121
|
+
}
|
|
122
|
+
state(): MlxArray[] {
|
|
123
|
+
if (!this.#planes[0]!.keys) return [];
|
|
124
|
+
return [...this.#planes.map(plane => plane.keys!), ...this.#planes.map(plane => plane.values!)];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Return the existing persisted representation, owning compact arrays. */
|
|
128
|
+
extractRow(row: number): QuantizedKVCache {
|
|
129
|
+
const offset = this.rowOffsets[row]!, result = new QuantizedKVCache(this.groupSize, this.bits);
|
|
130
|
+
if (this.#minimum[row]) result.minimumReusableOffset = this.#minimum[row];
|
|
131
|
+
if (offset === 0) return result;
|
|
132
|
+
const held: KVCache[] = [];
|
|
133
|
+
try {
|
|
134
|
+
for (const plane of this.#planes) held.push(plane.extractRow(row));
|
|
135
|
+
result.restoreState(
|
|
136
|
+
{ packed: held[0]!.keys!, scales: held[1]!.keys!, biases: held[2]!.keys! },
|
|
137
|
+
{ packed: held[0]!.values!, scales: held[1]!.values!, biases: held[2]!.values! }, offset);
|
|
138
|
+
for (const cache of held) cache.keys = cache.values = null;
|
|
139
|
+
return result;
|
|
140
|
+
} finally { disposeResources(held); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
dispose(): void { disposeResources(this.#planes); this.#minimum = []; }
|
|
144
|
+
}
|