mlx-bun 0.5.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1449 -109
- package/THIRD_PARTY_NOTICES.md +882 -0
- package/bin/mlx-bun.convert.mjs +4 -0
- package/bin/mlx-bun.fuse.mjs +4 -0
- package/bin/mlx-bun.generate.mjs +4 -0
- package/bin/mlx-bun.lora.mjs +4 -0
- package/bin/mlx-bun.mjs +13 -21
- package/bin/mlx-bun.server.mjs +4 -0
- package/bin/mlx-bun.upload.mjs +4 -0
- package/dist/web/app.js +9584 -0
- package/package.json +50 -53
- package/src/cli/ane-install.ts +76 -0
- package/src/cli/args.ts +179 -0
- package/src/cli/engine-entry.ts +68 -0
- package/src/cli/inference.ts +214 -0
- package/src/cli/job-entry.ts +97 -0
- package/src/cli/library-host.ts +117 -0
- package/src/cli/main.ts +57 -0
- package/src/cli/memory-engine.ts +163 -0
- package/src/cli/memory.ts +36 -0
- package/src/cli/mlx-lm-aliases.ts +309 -0
- package/src/cli/model-choice.ts +33 -0
- package/src/cli/model-selection.ts +84 -0
- package/src/cli/module-host.ts +11 -0
- package/src/cli/module-verbs.ts +50 -0
- package/src/cli/numerical-policy.ts +12 -0
- package/src/cli/perplexity.ts +89 -0
- package/src/cli/serve-host.ts +423 -0
- package/src/cli/serve-isolated.ts +217 -0
- package/src/cli/serve-options.ts +106 -0
- package/src/cli/serve-state.ts +205 -0
- package/src/cli/serve.ts +500 -0
- package/src/cli/served-model-host.ts +102 -0
- package/src/cli/server-entry.ts +148 -0
- package/src/cli/serving-unit.ts +177 -0
- package/src/{tui.ts → cli/terminal.ts} +13 -8
- package/src/cli/worker-adapters.ts +34 -0
- package/src/cli/worker-entry.ts +272 -0
- package/src/cli/worker-unit.ts +179 -0
- package/src/engine/adapter-operation.ts +26 -0
- package/src/engine/cache-services.ts +114 -0
- package/src/engine/completion.ts +39 -0
- package/src/engine/generation-gateway.ts +358 -0
- package/src/engine/index.ts +59 -0
- package/src/engine/kv-budget.ts +120 -0
- package/src/engine/media-preparation.ts +232 -0
- package/src/engine/model-binding.ts +24 -0
- package/src/engine/model-host.ts +513 -0
- package/src/engine/model-serving.ts +89 -0
- package/src/{serve → engine}/preparation.ts +7 -5
- package/src/{serve → engine}/session-completion-engine.ts +6 -6
- package/src/engine/telemetry.ts +109 -0
- package/src/hub/downloads.ts +116 -0
- package/src/jobs/db.ts +16 -7
- package/src/jobs/events.ts +29 -0
- package/src/jobs/executable.ts +5 -0
- package/src/jobs/host.ts +51 -0
- package/src/jobs/in-process.ts +55 -0
- package/src/{contracts/jobs.ts → jobs/protocol.ts} +7 -9
- package/src/jobs/runner.ts +150 -231
- package/src/jobs/service.ts +75 -0
- package/src/jobs/sse.ts +59 -23
- package/src/jobs/worker-memory.ts +46 -0
- package/src/jobs/worker-process.ts +163 -0
- package/src/jobs/worker-supervisor.ts +258 -0
- package/src/modules.ts +58 -0
- package/src/publishing/catalog-hub.ts +18 -0
- package/src/publishing/credentials.ts +31 -0
- package/src/publishing/upload.ts +37 -0
- package/src/residency/leased-adapters.ts +21 -0
- package/src/residency/local-records.ts +16 -0
- package/src/residency/model-residency.ts +405 -0
- package/src/residency/record-index.ts +28 -0
- package/src/residency/resident-estimate.ts +50 -0
- package/src/{anthropic.ts → server/anthropic.ts} +3 -81
- package/src/server/cache-routes.ts +36 -0
- package/src/{serve → server}/chat-request.ts +10 -10
- package/src/{serve → server}/chat-stage.ts +14 -14
- package/src/{client.ts → server/client.ts} +28 -2
- package/src/{serve → server}/completion-executor.ts +33 -41
- package/src/server/decision-body.ts +55 -0
- package/src/server/decision-routes.ts +134 -0
- package/src/server/decision-wire.ts +228 -0
- package/src/{serve → server}/discovery-routes.ts +25 -82
- package/src/{serve → server}/http.ts +25 -13
- package/src/{serve → server}/inference-request.ts +5 -5
- package/src/server/job-routes.ts +20 -0
- package/src/server/media-prompt.ts +63 -0
- package/src/server/memory-completion-client.ts +46 -0
- package/src/server/model-routes.ts +192 -0
- package/src/{serve → server}/openai-wire.ts +3 -2
- package/src/{serve → server}/pipeline.ts +1 -1
- package/src/server/prompt-builder.ts +13 -0
- package/src/server/prompt-contracts.ts +17 -0
- package/src/server/proxy-routes.ts +187 -0
- package/src/server/publishing-routes.ts +41 -0
- package/src/{serve → server}/request-plan.ts +6 -8
- package/src/{serve → server}/request-prep.ts +47 -50
- package/src/{serve → server}/responses-client.ts +30 -13
- package/src/{responses.ts → server/responses.ts} +12 -93
- package/src/server/routes.ts +205 -0
- package/src/server/start.ts +72 -0
- package/src/server/status-routes.ts +108 -0
- package/src/{serve → server}/text-completion-stage.ts +25 -4
- package/src/server/text-prompt.ts +12 -0
- package/src/{serve → server}/token-streams.ts +36 -38
- package/src/server/worker-routes.ts +311 -0
- package/src/storage/paths.ts +61 -0
- package/src/web/assets.ts +37 -0
- package/src/web/{src → browser}/api.ts +5 -23
- package/src/web/browser/chat-host.ts +17 -0
- package/src/web/browser/installed-panels.ts +6 -0
- package/src/web/browser/main.ts +55 -0
- package/src/web/browser/markdown.ts +279 -0
- package/src/web/browser/memory-panel.ts +9 -0
- package/src/web/browser/palette.ts +64 -0
- package/src/web/browser/protocol.ts +13 -0
- package/src/web/browser/shell.ts +467 -0
- package/src/web/{src → browser}/status.ts +32 -36
- package/src/web/build.ts +51 -0
- package/src/web/public/app.html +1054 -0
- package/src/web/{sw.js → public/sw.js} +4 -3
- package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
- package/src/web/{vendor → public/vendor}/hljs.js +2 -2
- package/THIRD_PARTY_LICENSES.md +0 -51
- package/docs/README.md +0 -107
- package/docs/archive/hlg-sampling.md +0 -350
- package/docs/archive/investigations/audio-input-plan.md +0 -356
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +0 -3617
- package/docs/archive/investigations/chunk-finetune-experiment.md +0 -84
- package/docs/archive/investigations/colibri-glm52-port.md +0 -1486
- package/docs/archive/investigations/colibri-oracle-pin.md +0 -1284
- package/docs/archive/investigations/curve-bisector-routing.md +0 -123
- package/docs/archive/investigations/decode-roofline-lookagain.md +0 -348
- package/docs/archive/investigations/diffusion-gemma-port.md +0 -602
- package/docs/archive/investigations/dspark-handoff.md +0 -224
- package/docs/archive/investigations/expert-offload-single-user-moe.md +0 -292
- package/docs/archive/investigations/faithful-l1-consolidation.md +0 -310
- package/docs/archive/investigations/hlg-sampling-investigation.md +0 -970
- package/docs/archive/investigations/indexshare-performance-spike.md +0 -357
- package/docs/archive/investigations/kernel-perf-review-2026-07.md +0 -171
- package/docs/archive/investigations/lab-build-journal.md +0 -157
- package/docs/archive/investigations/minicpm5-decode-megakernel.md +0 -489
- package/docs/archive/investigations/openjarvis-fit.md +0 -198
- package/docs/archive/investigations/openwiki-evaluation.md +0 -249
- package/docs/archive/investigations/optimization_plan.md +0 -346
- package/docs/archive/investigations/orpo-base-uf-experiment-and-directions.md +0 -120
- package/docs/archive/investigations/orpo-flash-cce-pin-leak.md +0 -167
- package/docs/archive/investigations/orpo-uf-testing-handoff.md +0 -126
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +0 -92
- package/docs/archive/investigations/pi-builtin-investigation.md +0 -308
- package/docs/archive/investigations/pre-colibri-stabilization.md +0 -329
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +0 -1092
- package/docs/archive/investigations/segmented-backward-handoff.md +0 -148
- package/docs/archive/investigations/starter-model-port-handoff.md +0 -342
- package/docs/archive/investigations/steel-flash-cce-handoff.md +0 -489
- package/docs/archive/investigations/the-dreaming-handoff.md +0 -141
- package/docs/archive/investigations/trainer-validation-experiment.md +0 -173
- package/docs/archive/planning/chat-ui-vision-memory-first.md +0 -199
- package/docs/archive/planning/chat-ui-vision.md +0 -50
- package/docs/archive/planning/chunk-task-roadmap.md +0 -77
- package/docs/archive/planning/curve-sampler-research-plan.md +0 -331
- package/docs/archive/planning/journal.md +0 -316
- package/docs/archive/planning/memory-docs-and-dag-plan.md +0 -194
- package/docs/archive/planning/pi-integration-review.md +0 -178
- package/docs/archive/planning/release-notes-v0.0.10.md +0 -57
- package/docs/archive/planning/release-notes-v0.0.11.md +0 -239
- package/docs/archive/planning/release-notes-v0.0.12.md +0 -80
- package/docs/archive/planning/release-notes-v0.0.13.md +0 -77
- package/docs/archive/planning/release-notes-v0.0.9.md +0 -122
- package/docs/archive/planning/release-notes-v0.2.0.md +0 -49
- package/docs/archive/planning/release-notes-v0.3.0.md +0 -35
- package/docs/archive/planning/release-notes-v0.4.0.md +0 -45
- package/docs/archive/planning/release-notes-v0.5.0.md +0 -37
- package/docs/archive/planning/web-ui-pass-plan.md +0 -213
- package/docs/archive/planning/website-readme-pass-plan.md +0 -224
- package/docs/design/batching.md +0 -2054
- package/docs/design/decode-speed-program.md +0 -2404
- package/docs/design/dreaming-nightly-pipeline.md +0 -684
- package/docs/design/generic-model-support.md +0 -821
- package/docs/design/kv-cache.md +0 -993
- package/docs/design/orpo-training.md +0 -680
- package/docs/design/speculative-decoding.md +0 -2129
- package/docs/design/turboquant.md +0 -2225
- package/docs/design/unified-engine-frontier-plan.md +0 -1987
- package/docs/design/web-chat-redesign.md +0 -695
- package/docs/planning/PRODUCT_ROADMAP.md +0 -593
- package/docs/planning/ResearchTopics.md +0 -119
- package/docs/planning/public-readiness-handoff.md +0 -244
- package/docs/planning/public-readiness.md +0 -149
- package/docs/planning/video-series.md +0 -421
- package/docs/reference/benchmarks.md +0 -3587
- package/docs/reference/cli.md +0 -646
- package/docs/reference/distribution.md +0 -380
- package/docs/reference/environment.md +0 -494
- package/docs/reference/glossary.md +0 -39
- package/docs/reference/library-api.md +0 -688
- package/docs/reference/memory.md +0 -260
- package/docs/reference/models.md +0 -346
- package/docs/reference/server-api.md +0 -1945
- package/docs/reference/server-config.md +0 -973
- package/docs/reference/training.md +0 -462
- package/docs/reference/troubleshooting.md +0 -158
- package/src/admission-plan.ts +0 -18
- package/src/audio/audiotoolbox.ts +0 -112
- package/src/audio/conformer.ts +0 -655
- package/src/audio/decode.ts +0 -133
- package/src/audio/features.ts +0 -202
- package/src/audio/mic-capture.ts +0 -98
- package/src/audio/silero-vad.ts +0 -379
- package/src/audio/transcode.ts +0 -67
- package/src/audio/whisper-decode.ts +0 -774
- package/src/audio/whisper-fast.ts +0 -732
- package/src/audio/whisper-format.ts +0 -73
- package/src/audio/whisper-mel.ts +0 -185
- package/src/audio/whisper-timing.ts +0 -274
- package/src/audio/whisper-tokenizer.ts +0 -239
- package/src/audio/whisper-transcribe.ts +0 -410
- package/src/backends/mlx/assistant-target.ts +0 -46
- package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
- package/src/backends/mlx/autoregressive.ts +0 -129
- package/src/backends/mlx/batch-group.ts +0 -1874
- package/src/backends/mlx/cache-attachment.ts +0 -67
- package/src/backends/mlx/cache-layout.ts +0 -31
- package/src/backends/mlx/checkpoint-state.ts +0 -53
- package/src/backends/mlx/continuation-persistence.ts +0 -107
- package/src/backends/mlx/continuation-request.ts +0 -43
- package/src/backends/mlx/continuation.ts +0 -77
- package/src/backends/mlx/diffusion.ts +0 -39
- package/src/backends/mlx/draft-target.ts +0 -32
- package/src/backends/mlx/encoder-cache.ts +0 -25
- package/src/backends/mlx/evaluation.ts +0 -105
- package/src/backends/mlx/fill-append.ts +0 -58
- package/src/backends/mlx/fill-group.ts +0 -497
- package/src/backends/mlx/full-kv-row-append.ts +0 -32
- package/src/backends/mlx/full-kv-row-donor.ts +0 -54
- package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
- package/src/backends/mlx/gateway-binding.ts +0 -174
- package/src/backends/mlx/graph.ts +0 -38
- package/src/backends/mlx/kv-codec.ts +0 -9
- package/src/backends/mlx/kv-maintenance.ts +0 -161
- package/src/backends/mlx/kv-tensor-rows.ts +0 -212
- package/src/backends/mlx/memory-client.ts +0 -189
- package/src/backends/mlx/methods.ts +0 -90
- package/src/backends/mlx/mixed-iteration.ts +0 -62
- package/src/backends/mlx/model-prompt.ts +0 -178
- package/src/backends/mlx/model-serving.ts +0 -80
- package/src/backends/mlx/paged-cache-codec.ts +0 -27
- package/src/backends/mlx/prefill-cohort.ts +0 -100
- package/src/backends/mlx/prefill-policy.ts +0 -26
- package/src/backends/mlx/prefill-rows.ts +0 -244
- package/src/backends/mlx/prefill.ts +0 -33
- package/src/backends/mlx/projected-context-rows.ts +0 -72
- package/src/backends/mlx/prompt-input.ts +0 -29
- package/src/backends/mlx/qwen-prompt-input.ts +0 -40
- package/src/backends/mlx/request-policy.ts +0 -20
- package/src/backends/mlx/request-state-policy.ts +0 -54
- package/src/backends/mlx/rollback.ts +0 -92
- package/src/backends/mlx/row-sampling.ts +0 -49
- package/src/backends/mlx/serial-executor.ts +0 -251
- package/src/backends/mlx/speculative-group.ts +0 -438
- package/src/backends/mlx/speculative-round.ts +0 -161
- package/src/backends/mlx/speculative.ts +0 -131
- package/src/backends/mlx/state-rows.ts +0 -104
- package/src/backends/mlx/state-views.ts +0 -46
- package/src/backends/mlx/target-layout-capability.ts +0 -35
- package/src/backends/mlx/text-engine.ts +0 -73
- package/src/backends/mlx/transitioning-kv-rows.ts +0 -173
- package/src/chat-template.ts +0 -382
- package/src/cli.ts +0 -3490
- package/src/config.ts +0 -399
- package/src/contracts/completion.ts +0 -16
- package/src/contracts/execution.ts +0 -70
- package/src/contracts/generation.ts +0 -103
- package/src/contracts/host.ts +0 -7
- package/src/contracts/object-cache.ts +0 -8
- package/src/contracts/pi-provider.ts +0 -30
- package/src/contracts/pi.ts +0 -261
- package/src/contracts/prefix-cache.ts +0 -33
- package/src/contracts/resources.ts +0 -25
- package/src/contracts/sampling.ts +0 -23
- package/src/contracts/scheduling.ts +0 -54
- package/src/contracts/tsconfig.json +0 -14
- package/src/dataset/generators.ts +0 -1087
- package/src/dataset/index.ts +0 -12
- package/src/dataset/job.ts +0 -81
- package/src/dataset/llm.ts +0 -71
- package/src/dataset/registry.ts +0 -528
- package/src/diffusion/diffusion-generate.ts +0 -507
- package/src/download.ts +0 -505
- package/src/embed.ts +0 -77
- package/src/engine/admission.ts +0 -59
- package/src/engine/cancellation.ts +0 -38
- package/src/engine/engine.ts +0 -56
- package/src/engine/execution-coordinator.ts +0 -70
- package/src/engine/execution-plan.ts +0 -64
- package/src/engine/execution-tasks.ts +0 -44
- package/src/engine/namespaced-cache.ts +0 -13
- package/src/engine/resources.ts +0 -53
- package/src/engine/scheduler.ts +0 -92
- package/src/engine/session.ts +0 -247
- package/src/eval/README.md +0 -130
- package/src/eval/ifeval.ts +0 -81
- package/src/eval/kl-prompts.ts +0 -29
- package/src/eval/kl.ts +0 -590
- package/src/eval/perplexity.ts +0 -164
- package/src/eval/quality-db.ts +0 -92
- package/src/eval/reference-logits.ts +0 -207
- package/src/eval/runner.ts +0 -124
- package/src/eval/score.ts +0 -49
- package/src/eval/tasks/bfcl.ts +0 -535
- package/src/eval/tasks/gsm8k.ts +0 -98
- package/src/eval/tasks/hashhop.ts +0 -139
- package/src/eval/tasks/humaneval.ts +0 -275
- package/src/eval/tasks/ifeval.ts +0 -525
- package/src/eval/tasks/mmlu.ts +0 -157
- package/src/eval/ultrafeedback.ts +0 -60
- package/src/evaldb.ts +0 -199
- package/src/expert-io.ts +0 -393
- package/src/expert-offload-build.ts +0 -77
- package/src/expert-offload.ts +0 -76
- package/src/expert-residency.ts +0 -1134
- package/src/expert-usage.ts +0 -415
- package/src/fill/echo-index.ts +0 -188
- package/src/fill/fill-session.ts +0 -463
- package/src/fill/proposal.ts +0 -68
- package/src/fill/schema-rows.ts +0 -506
- package/src/fill/tool-boundary.ts +0 -103
- package/src/fit.ts +0 -249
- package/src/generate.ts +0 -1352
- package/src/grammar.ts +0 -604
- package/src/harness-pi.ts +0 -184
- package/src/hf-push.ts +0 -628
- package/src/hub-rest.ts +0 -242
- package/src/index.ts +0 -63
- package/src/inference/denoising.ts +0 -17
- package/src/inference/draft-acceptance.ts +0 -41
- package/src/inference/draft-output.ts +0 -49
- package/src/inference/graph.ts +0 -33
- package/src/inference/prefill.ts +0 -45
- package/src/inference/rollback.ts +0 -7
- package/src/jobs/index.ts +0 -31
- package/src/jobs/job-entry.ts +0 -107
- package/src/jobs/task-client.ts +0 -19
- package/src/jobs/types.ts +0 -2
- package/src/kv-scheme.ts +0 -279
- package/src/kv-store.ts +0 -1135
- package/src/lab/README.md +0 -22
- package/src/lab/curve/curve-designer.html +0 -303
- package/src/lab/curve/curve-sampler.ts +0 -122
- package/src/lab/expert-trace/expert-trace.ts +0 -69
- package/src/lab/paged-kv/paged-attention.ts +0 -117
- package/src/lab/paged-kv/paged-kv-rows.ts +0 -111
- package/src/lab/paged-kv/paged-kv.ts +0 -336
- package/src/library.ts +0 -40
- package/src/lora.ts +0 -408
- package/src/md-modules.d.ts +0 -54
- package/src/media-fetch.ts +0 -442
- package/src/memory/article.ts +0 -558
- package/src/memory/chunk-validation.ts +0 -150
- package/src/memory/chunk.ts +0 -318
- package/src/memory/cluster.ts +0 -351
- package/src/memory/coverage.ts +0 -134
- package/src/memory/crosslink.ts +0 -0
- package/src/memory/db.ts +0 -253
- package/src/memory/entity.ts +0 -203
- package/src/memory/gate.ts +0 -187
- package/src/memory/history.ts +0 -152
- package/src/memory/ingest.ts +0 -335
- package/src/memory/model.ts +0 -183
- package/src/memory/normalize.ts +0 -671
- package/src/memory/parse.ts +0 -22
- package/src/memory/pipeline.ts +0 -275
- package/src/memory/prompts.ts +0 -65
- package/src/memory/query.ts +0 -334
- package/src/memory/reconcile.ts +0 -734
- package/src/memory/redirect.ts +0 -108
- package/src/memory/reindex.ts +0 -325
- package/src/memory/resolve.ts +0 -307
- package/src/memory/rest.ts +0 -245
- package/src/memory/route.ts +0 -272
- package/src/memory/schedule.ts +0 -165
- package/src/memory/stages.ts +0 -777
- package/src/memory/synthesize.ts +0 -1638
- package/src/memory/tools.ts +0 -749
- package/src/memory/vault.ts +0 -1194
- package/src/memory/wikify.ts +0 -1064
- package/src/mlx/array.ts +0 -425
- package/src/mlx/autograd.ts +0 -304
- package/src/mlx/checkpoint.ts +0 -149
- package/src/mlx/compile.ts +0 -168
- package/src/mlx/custom-vjp.ts +0 -188
- package/src/mlx/ffi.ts +0 -531
- package/src/mlx/materialize.ts +0 -23
- package/src/mlx/metal-kernel.ts +0 -159
- package/src/mlx/normalized-argmax.ts +0 -116
- package/src/mlx/ops.ts +0 -1108
- package/src/mlx/quantized-rows.ts +0 -36
- package/src/mlx/token-bitmask.ts +0 -52
- package/src/mlx/turboquant-kv-decode.ts +0 -82
- package/src/mlx/turboquant-kv-inverse.ts +0 -122
- package/src/mlx/turboquant-ops.ts +0 -861
- package/src/mlx/turboquant-tables.ts +0 -237
- package/src/mmap.ts +0 -90
- package/src/model/artifact-identity.ts +0 -73
- package/src/model/batched-kv.ts +0 -235
- package/src/model/batched-mask.ts +0 -279
- package/src/model/batched-quant.ts +0 -212
- package/src/model/batched-quantized-kv.ts +0 -144
- package/src/model/batched-rotating-quant.ts +0 -353
- package/src/model/batched-rotating-state.ts +0 -149
- package/src/model/batched-rotating.ts +0 -338
- package/src/model/batched-row-storage.ts +0 -179
- package/src/model/batched-ssm.ts +0 -52
- package/src/model/batched-turboquant-kv.ts +0 -94
- package/src/model/compiled-decode.ts +0 -712
- package/src/model/decoded-kv-donor.ts +0 -12
- package/src/model/delayed-quantized-kv.ts +0 -72
- package/src/model/delayed-rotating-quantized-kv.ts +0 -93
- package/src/model/delayed-turboquant-kv.ts +0 -57
- package/src/model/diffusion-gemma.ts +0 -1026
- package/src/model/factory.ts +0 -215
- package/src/model/fingerprint.ts +0 -45
- package/src/model/flash-attention.ts +0 -694
- package/src/model/full-prefill-padding.ts +0 -56
- package/src/model/full-prefill-row.ts +0 -105
- package/src/model/gemma4-base.ts +0 -2370
- package/src/model/gemma4.ts +0 -1395
- package/src/model/generated/gemma4-12b.ts +0 -429
- package/src/model/generated/gemma4-26b.ts +0 -448
- package/src/model/generated/gemma4-e4b.ts +0 -717
- package/src/model/generated/index.ts +0 -18
- package/src/model/glm52-atlas.ts +0 -511
- package/src/model/glm52-cache.ts +0 -792
- package/src/model/glm52-config.ts +0 -262
- package/src/model/glm52-container.ts +0 -305
- package/src/model/glm52-coupling.ts +0 -388
- package/src/model/glm52-dsa.ts +0 -606
- package/src/model/glm52-expert-layout.ts +0 -253
- package/src/model/glm52-layout.ts +0 -152
- package/src/model/glm52-memory.ts +0 -500
- package/src/model/glm52-mla.ts +0 -792
- package/src/model/glm52-moe.ts +0 -223
- package/src/model/glm52-pilot.ts +0 -537
- package/src/model/glm52-reference.ts +0 -445
- package/src/model/glm52-residency.ts +0 -557
- package/src/model/glm52-resident-weights.ts +0 -135
- package/src/model/glm52-streamed-experts.ts +0 -453
- package/src/model/glm52-streamed-metal.ts +0 -610
- package/src/model/glm52-weights.ts +0 -503
- package/src/model/glm52.ts +0 -1132
- package/src/model/implementation.ts +0 -69
- package/src/model/kv-attention-view.ts +0 -70
- package/src/model/minicpm5.ts +0 -376
- package/src/model/profile.ts +0 -468
- package/src/model/quantized-append-attention.ts +0 -31
- package/src/model/qwen-append.ts +0 -12
- package/src/model/qwen-conv.ts +0 -52
- package/src/model/qwen3-delta-state.ts +0 -74
- package/src/model/qwen3-delta.ts +0 -451
- package/src/model/qwen3-moe.ts +0 -404
- package/src/model/qwen3-mrope.ts +0 -250
- package/src/model/qwen3.ts +0 -277
- package/src/model/qwen3_5-checkpoint.ts +0 -164
- package/src/model/qwen3_5.ts +0 -881
- package/src/model/rotating-kv-donor.ts +0 -35
- package/src/model/rotating-kv-layout.ts +0 -262
- package/src/model/rotating-row-transaction.ts +0 -35
- package/src/model/speculative-rotating-kv.ts +0 -101
- package/src/model/ssm-prefill-padding.ts +0 -39
- package/src/model/support.ts +0 -110
- package/src/model/token-groups.ts +0 -66
- package/src/model/trellis-balanced-scatter.ts +0 -51
- package/src/model/trellis-linear.ts +0 -672
- package/src/model/trellis-shared-m.ts +0 -151
- package/src/model/trellis-shared-scatter.ts +0 -144
- package/src/model/trellis-splitk-prefill.ts +0 -158
- package/src/model/trellis-tiled-prefill.ts +0 -136
- package/src/model/trellis-vector-expand.ts +0 -62
- package/src/model/trellis-wide-prefill.ts +0 -94
- package/src/model/turboquant-codec.ts +0 -89
- package/src/model/universal/archs.ts +0 -332
- package/src/model/universal/dense.ts +0 -612
- package/src/model/universal/modules.ts +0 -170
- package/src/model/universal/rope.ts +0 -381
- package/src/model/whisper.ts +0 -435
- package/src/native/expert_io.c +0 -523
- package/src/native/frame_extract.swift +0 -140
- package/src/native/mic_capture.swift +0 -92
- package/src/native-pack.ts +0 -163
- package/src/pi-provider.ts +0 -53
- package/src/pi-session.ts +0 -81
- package/src/pi-terminal.ts +0 -274
- package/src/pi-web.ts +0 -1940
- package/src/preflight.ts +0 -158
- package/src/prompt-cache.ts +0 -682
- package/src/quantize/allocator.ts +0 -627
- package/src/quantize/atomic-output.ts +0 -38
- package/src/quantize/calibration.ts +0 -181
- package/src/quantize/config-writer.ts +0 -145
- package/src/quantize/index.ts +0 -84
- package/src/quantize/job.ts +0 -162
- package/src/quantize/quantizer.ts +0 -666
- package/src/quantize/rotate.ts +0 -603
- package/src/quantize/safetensors-writer.ts +0 -249
- package/src/quantize/sensitivity.ts +0 -216
- package/src/quantize/trellis.ts +0 -586
- package/src/quantize/weight-transform.ts +0 -292
- package/src/registry.ts +0 -602
- package/src/runtime-config.ts +0 -77
- package/src/safetensors.ts +0 -165
- package/src/sampler.ts +0 -1012
- package/src/serve/admin-routes.ts +0 -184
- package/src/serve/assets/curve-terrain.html +0 -105
- package/src/serve/assets/training-inference-map.html +0 -575
- package/src/serve/audio-routes.ts +0 -323
- package/src/serve/aux-routes.ts +0 -132
- package/src/serve/batch-scheduler.ts +0 -8
- package/src/serve/checkpoint-identity.ts +0 -64
- package/src/serve/generation-gateway.ts +0 -537
- package/src/serve/isolate.ts +0 -609
- package/src/serve/kv-budget.ts +0 -14
- package/src/serve/lab-routes.ts +0 -341
- package/src/serve/lane-registry.ts +0 -45
- package/src/serve/model-admin-routes.ts +0 -155
- package/src/serve/model-binding.ts +0 -49
- package/src/serve/model-host.ts +0 -511
- package/src/serve/parent-application.ts +0 -56
- package/src/serve/prompt-response-trace.ts +0 -161
- package/src/serve/response-lease.ts +0 -24
- package/src/serve/session-search.ts +0 -326
- package/src/serve/static-routes.ts +0 -101
- package/src/serve/transcription-server.ts +0 -64
- package/src/serve/transcription-service.ts +0 -502
- package/src/server.ts +0 -1282
- package/src/spec/assistant-rows.ts +0 -93
- package/src/spec/assistant-source.ts +0 -126
- package/src/spec/cache-identity.ts +0 -22
- package/src/spec/deepspec-rows.ts +0 -29
- package/src/spec/deepspec-source.ts +0 -145
- package/src/spec/dflash-source.ts +0 -149
- package/src/spec/drafter.ts +0 -304
- package/src/spec/dspark/ab-stats.ts +0 -136
- package/src/spec/dspark/calibration.ts +0 -112
- package/src/spec/dspark/data-dflash.ts +0 -156
- package/src/spec/dspark/data.ts +0 -201
- package/src/spec/dspark/deepspec-module.ts +0 -776
- package/src/spec/dspark/generate-dflash.ts +0 -245
- package/src/spec/dspark/generate.ts +0 -290
- package/src/spec/dspark/loader.ts +0 -23
- package/src/spec/dspark/loss.ts +0 -213
- package/src/spec/dspark/module-dflash.ts +0 -560
- package/src/spec/dspark/module.ts +0 -540
- package/src/spec/dspark/quantize-drafter.ts +0 -91
- package/src/spec/dspark/sample.ts +0 -112
- package/src/spec/generate.ts +0 -276
- package/src/spec/glm52-mtp-graph.ts +0 -29
- package/src/spec/glm52-mtp-rows.ts +0 -132
- package/src/spec/glm52-mtp-source.ts +0 -318
- package/src/spec/glm52-mtp-state.ts +0 -33
- package/src/spec/ngram-source.ts +0 -226
- package/src/spec/projected-draft-rows.ts +0 -73
- package/src/spec/qwen-mtp-module.ts +0 -171
- package/src/spec/qwen-mtp-rows.ts +0 -230
- package/src/spec/qwen-mtp-source.ts +0 -386
- package/src/spec/qwen-mtp-state.ts +0 -30
- package/src/spec/serve-loop.ts +0 -587
- package/src/spec/source.ts +0 -228
- package/src/spec/two-model-rows.ts +0 -62
- package/src/spec/two-model.ts +0 -113
- package/src/ssd-cache.ts +0 -431
- package/src/ssd-durability.ts +0 -209
- package/src/storage/host-buffer.ts +0 -24
- package/src/storage/kv-writer.ts +0 -83
- package/src/storage/kv-writer.worker.js +0 -264
- package/src/storage/retention-policy.ts +0 -37
- package/src/tiered-prompt-cache.ts +0 -45
- package/src/tokenizer.ts +0 -335
- package/src/tool-approvals.ts +0 -131
- package/src/tool-call.ts +0 -695
- package/src/train/dataset.ts +0 -484
- package/src/train/diffusion-lora.ts +0 -216
- package/src/train/export.ts +0 -32
- package/src/train/flash-cce.ts +0 -1234
- package/src/train/forward.ts +0 -266
- package/src/train/fuse.ts +0 -243
- package/src/train/index.ts +0 -46
- package/src/train/job.ts +0 -205
- package/src/train/lora-params.ts +0 -254
- package/src/train/loss.ts +0 -1591
- package/src/train/merge.ts +0 -195
- package/src/train/optimizer.ts +0 -183
- package/src/train/prefix-shared.ts +0 -490
- package/src/train/rank.ts +0 -137
- package/src/train/segmented.ts +0 -1828
- package/src/train/steel-qmm-header.ts +0 -1461
- package/src/train/trainer.ts +0 -1221
- package/src/train/watch.ts +0 -542
- package/src/vision/diffusion-vision.ts +0 -620
- package/src/vision/embedder.ts +0 -176
- package/src/vision/preprocess.ts +0 -247
- package/src/vision/prompt.ts +0 -409
- package/src/vision/qwen3vl-preprocess.ts +0 -409
- package/src/vision/qwen3vl-prompt.ts +0 -233
- package/src/vision/qwen3vl-tower.ts +0 -566
- package/src/vision/siglip.ts +0 -607
- package/src/vision/video-frames.ts +0 -148
- package/src/web/app.html +0 -2155
- package/src/web/app.js +0 -5863
- package/src/web/skills/memory/SKILL.md +0 -120
- package/src/web/skills/web-research/SKILL.md +0 -46
- package/src/web/skills.ts +0 -97
- package/src/web/src/adapters-panel.ts +0 -288
- package/src/web/src/assistant.ts +0 -451
- package/src/web/src/chat.ts +0 -1060
- package/src/web/src/composer.ts +0 -962
- package/src/web/src/dataset.ts +0 -132
- package/src/web/src/finetune.ts +0 -267
- package/src/web/src/hub.ts +0 -428
- package/src/web/src/main.ts +0 -98
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +0 -572
- package/src/web/src/model-picker.ts +0 -159
- package/src/web/src/palette.ts +0 -346
- package/src/web/src/protocol.ts +0 -26
- package/src/web/src/quantize.ts +0 -167
- package/src/web/src/rag.ts +0 -274
- package/src/web/src/sessions.ts +0 -469
- package/src/web/src/shell.ts +0 -804
- package/src/web/src/ui-catalog.ts +0 -78
- package/src/web/src/voice.ts +0 -141
- package/src/web/vendor/README.md +0 -61
- package/src/web-assets.ts +0 -47
- package/src/web-tools.ts +0 -478
- package/src/weights.ts +0 -255
- /package/src/{serve → server}/completion-sink.ts +0 -0
- /package/src/{serve → server}/generated-token-history.ts +0 -0
- /package/src/web/{icon.svg → public/icon.svg} +0 -0
- /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
- /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
package/src/model/gemma4-base.ts
DELETED
|
@@ -1,2370 +0,0 @@
|
|
|
1
|
-
// gemma4-base — the config-INDEPENDENT machinery shared by every
|
|
2
|
-
// architecture variant (docs/archive/investigations/optimization_plan.md Phase B: pure code
|
|
3
|
-
// movement out of the gemma4.ts monolith; every definition here is
|
|
4
|
-
// verbatim from it). Cache classes, quantized-KV SDPA, masks, the
|
|
5
|
-
// quantized primitives + LoRA machinery, and small graph helpers live
|
|
6
|
-
// here; everything that branches on architecture (Attention, MLP, MoE,
|
|
7
|
-
// DecoderLayer, Gemma4Model) stays in gemma4.ts and becomes the
|
|
8
|
-
// per-model generated code in Phase C.
|
|
9
|
-
|
|
10
|
-
import { decodedKvDonorAttention } from "./decoded-kv-donor";
|
|
11
|
-
import type { ModelConfig } from "../config";
|
|
12
|
-
import { quantFor } from "../config";
|
|
13
|
-
import type { Weights } from "../weights";
|
|
14
|
-
import { MlxArray } from "../mlx/array";
|
|
15
|
-
import { Dtype, deviceArchitecture } from "../mlx/ffi";
|
|
16
|
-
import * as ops from "../mlx/ops";
|
|
17
|
-
import { quantizedMatmulRows } from "../mlx/quantized-rows";
|
|
18
|
-
import { expertOffloadArray } from "../expert-offload";
|
|
19
|
-
import * as tq from "../mlx/turboquant-ops";
|
|
20
|
-
import type { KvCodec } from "../backends/mlx/kv-codec";
|
|
21
|
-
import { TurboQuantCodec, turboQuantFusedDecode, disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
|
|
22
|
-
export { disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
|
|
23
|
-
import { runtimeValue } from "../runtime-config";
|
|
24
|
-
|
|
25
|
-
export type MaskMode = "" | "causal";
|
|
26
|
-
export interface Mask {
|
|
27
|
-
mode: MaskMode | "array";
|
|
28
|
-
arr: MlxArray | null;
|
|
29
|
-
/** True when `arr` is exactly the bottom-right-aligned causal matrix
|
|
30
|
-
* (offset continuation, no window) — the case where mlx-lm would
|
|
31
|
-
* have handed the string "causal" instead. Only these array masks
|
|
32
|
-
* are eligible for the fused tiled SDPA: the optiq wrapper falls
|
|
33
|
-
* back to unfused on every array mask, and window/bidir masks must
|
|
34
|
-
* match that to stay scenario-bit-exact (Phase 9 finding). */
|
|
35
|
-
causalEquivalent?: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Per-adapter LoRA weights for one linear (mlx-lm LoRALinear shapes:
|
|
39
|
-
* a [in_features, rank], b [rank, out_features], typically f32). */
|
|
40
|
-
export interface LoraWeights {
|
|
41
|
-
a: MlxArray;
|
|
42
|
-
b: MlxArray;
|
|
43
|
-
scale: number;
|
|
44
|
-
rank: number;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Active-adapter state, shared by every mounted linear of one model.
|
|
48
|
-
* A plain field, NOT a ContextVar port: our generation queue is
|
|
49
|
-
* serialized, so exactly one request's adapters are active at a time
|
|
50
|
-
* (PLAN Phase 8 decision). Set/restored by generate(). */
|
|
51
|
-
export class LoraState {
|
|
52
|
-
active: string[] = [];
|
|
53
|
-
/** Training-only LoRA-input dropout. `rate` is the drop probability; `seed` is
|
|
54
|
-
* set per micro-step by the trainer and is CONSTANT across that step's forward
|
|
55
|
-
* and any recompute (segmented / gradient-checkpoint), so each layer's mask —
|
|
56
|
-
* keyed by (seed, the linear's dropoutId) — is reproduced exactly in the
|
|
57
|
-
* backward. `seed === null` (the default, and for inference) disables it. */
|
|
58
|
-
dropoutRate = 0;
|
|
59
|
-
dropoutSeed: number | null = null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Replaceable quantized payload. Inference sees this as read-only through
|
|
63
|
-
* QuantizedLinear getters; sensitivity analysis exchanges it atomically. */
|
|
64
|
-
export interface QuantizedLinearState {
|
|
65
|
-
w: MlxArray;
|
|
66
|
-
scales: MlxArray;
|
|
67
|
-
biases: MlxArray | null;
|
|
68
|
-
spec: ops.QuantSpec;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Inverted LoRA-input dropout, keyed by (seed, id) so it is deterministic —
|
|
72
|
-
* the same (seed, id, shape) reproduces the mask, which is what makes the
|
|
73
|
-
* segmented / gradient-checkpoint recompute correct. kept ⇒ x/(1-p),
|
|
74
|
-
* dropped ⇒ 0 (preserves the expectation). Caller owns the result. */
|
|
75
|
-
export function loraInputDropout(x: MlxArray, p: number, seed: number, id: number): MlxArray {
|
|
76
|
-
const key = ops.randomKey(BigInt(seed) * 100003n + BigInt(id));
|
|
77
|
-
const u = ops.randomUniform(x.shape, Dtype.float32, 0, 1, key); // [shape] in [0,1)
|
|
78
|
-
key.dispose();
|
|
79
|
-
const pArr = ops.scalarLike(p, u);
|
|
80
|
-
const keep = ops.less(pArr, u); // u > p ⇒ keep (bool [shape])
|
|
81
|
-
const scaleX = ops.scalarLike(1 / (1 - p), x);
|
|
82
|
-
const zeroX = ops.scalarLike(0, x);
|
|
83
|
-
const mask = ops.where(keep, scaleX, zeroX); // x.dtype
|
|
84
|
-
const xd = ops.mul(x, mask);
|
|
85
|
-
for (const a of [u, pArr, keep, scaleX, zeroX, mask]) a.dispose();
|
|
86
|
-
return xd;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export class QuantizedLinear {
|
|
90
|
-
/** Mounted adapters keyed by id (null until first mount — fast path). */
|
|
91
|
-
adapters: Map<string, LoraWeights> | null = null;
|
|
92
|
-
/** Shared per-model active state (wired by AdapterManager.mount). */
|
|
93
|
-
loraState: LoraState | null = null;
|
|
94
|
-
/** Stable per-target index for keying training-only LoRA dropout (set by
|
|
95
|
-
* attachForTraining). Gives each adapted linear an independent dropout mask. */
|
|
96
|
-
dropoutId = 0;
|
|
97
|
-
private _w: MlxArray;
|
|
98
|
-
private _scales: MlxArray;
|
|
99
|
-
private _biases: MlxArray | null;
|
|
100
|
-
private _spec: ops.QuantSpec;
|
|
101
|
-
|
|
102
|
-
constructor(
|
|
103
|
-
w: MlxArray,
|
|
104
|
-
scales: MlxArray,
|
|
105
|
-
biases: MlxArray | null,
|
|
106
|
-
spec: ops.QuantSpec,
|
|
107
|
-
/** ADDITIVE bias term (`.bias`, mlx nn.QuantizedLinear's optional bias —
|
|
108
|
-
* qwen2 qkv, starcoder2, …). Distinct from `biases`, the quantization
|
|
109
|
-
* zero-points. Applied after the matmul, before any LoRA residual,
|
|
110
|
-
* exactly like mlx's QuantizedLinear.__call__. */
|
|
111
|
-
readonly bias: MlxArray | null = null,
|
|
112
|
-
) {
|
|
113
|
-
this._w = w;
|
|
114
|
-
this._scales = scales;
|
|
115
|
-
this._biases = biases;
|
|
116
|
-
this._spec = spec;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
get w(): MlxArray { return this._w; }
|
|
120
|
-
get scales(): MlxArray { return this._scales; }
|
|
121
|
-
get biases(): MlxArray | null { return this._biases; }
|
|
122
|
-
get spec(): ops.QuantSpec { return this._spec; }
|
|
123
|
-
|
|
124
|
-
/** Atomically install a quantized payload and return the previous one.
|
|
125
|
-
* This is the sole mutation capability used by sensitivity sweeps. */
|
|
126
|
-
exchangeQuantizedState(next: QuantizedLinearState): QuantizedLinearState {
|
|
127
|
-
const previous = {
|
|
128
|
-
w: this._w,
|
|
129
|
-
scales: this._scales,
|
|
130
|
-
biases: this._biases,
|
|
131
|
-
spec: this._spec,
|
|
132
|
-
};
|
|
133
|
-
this._w = next.w;
|
|
134
|
-
this._scales = next.scales;
|
|
135
|
-
this._biases = next.biases;
|
|
136
|
-
this._spec = next.spec;
|
|
137
|
-
return previous;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
static load(weights: Weights, path: string, config: ModelConfig): QuantizedLinear {
|
|
141
|
-
if (!weights.has(`${path}.scales`))
|
|
142
|
-
throw new Error(`${path}: expected quantized linear (no .scales tensor)`);
|
|
143
|
-
const spec = quantFor(config.quantization, path);
|
|
144
|
-
if (!spec) throw new Error(`${path}: no quant spec`);
|
|
145
|
-
return new QuantizedLinear(
|
|
146
|
-
weights.tensor(`${path}.weight`),
|
|
147
|
-
weights.tensor(`${path}.scales`),
|
|
148
|
-
weights.has(`${path}.biases`) ? weights.tensor(`${path}.biases`) : null,
|
|
149
|
-
spec,
|
|
150
|
-
weights.has(`${path}.bias`) ? weights.tensor(`${path}.bias`) : null,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** (in_features, out_features) from the quantized tensors (reference
|
|
155
|
-
* _infer_linear_shape: scales are [out, in/group_size]). */
|
|
156
|
-
get inFeatures(): number {
|
|
157
|
-
return this._scales.shape[1]! * this._spec.groupSize;
|
|
158
|
-
}
|
|
159
|
-
get outFeatures(): number {
|
|
160
|
-
return this._scales.shape[0]!;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
forward(x: MlxArray, independentRows = false): MlxArray {
|
|
164
|
-
let out = independentRows ? quantizedMatmulRows(
|
|
165
|
-
x, this._w, this._scales, this._biases, this._spec,
|
|
166
|
-
) : ops.quantizedMatmul(
|
|
167
|
-
x,
|
|
168
|
-
this._w,
|
|
169
|
-
this._scales,
|
|
170
|
-
this._biases,
|
|
171
|
-
this._spec,
|
|
172
|
-
true,
|
|
173
|
-
);
|
|
174
|
-
// Additive bias (mlx QuantizedLinear: `x = x + self["bias"]`), before
|
|
175
|
-
// the LoRA residual (LoRALinear calls the base linear bias-inclusive).
|
|
176
|
-
if (this.bias) out = disposing(out, ops.add(out, this.bias));
|
|
177
|
-
// LoRA residual — composition is mlx-lm LoRALinear / optiq apply.py:
|
|
178
|
-
// y + (scale · ((x @ A) @ B)).astype(x.dtype)
|
|
179
|
-
// (optiq mount.py omits the astype, leaking the f32 residual into the
|
|
180
|
-
// bf16 stream — divergence documented in PLAN Phase 8 findings; the
|
|
181
|
-
// cast form is what the adapters were trained behind.)
|
|
182
|
-
const st = this.loraState;
|
|
183
|
-
if (st && st.active.length > 0 && this.adapters && this.adapters.size > 0) {
|
|
184
|
-
// Training-only LoRA-input dropout (PEFT applies dropout to x before A;
|
|
185
|
-
// the base quantized path is untouched). Keyed by (seed, dropoutId) so the
|
|
186
|
-
// backward recompute reproduces the exact mask.
|
|
187
|
-
let xLora = x;
|
|
188
|
-
let xDrop: MlxArray | null = null;
|
|
189
|
-
if (st.dropoutSeed !== null && st.dropoutRate > 0) {
|
|
190
|
-
xDrop = loraInputDropout(x, st.dropoutRate, st.dropoutSeed, this.dropoutId);
|
|
191
|
-
xLora = xDrop;
|
|
192
|
-
}
|
|
193
|
-
for (const id of st.active) {
|
|
194
|
-
const lw = this.adapters.get(id);
|
|
195
|
-
if (!lw) continue;
|
|
196
|
-
const xa = ops.matmul(xLora, lw.a);
|
|
197
|
-
const z = ops.matmul(xa, lw.b);
|
|
198
|
-
xa.dispose();
|
|
199
|
-
const zs = ops.mulScalar(z, lw.scale);
|
|
200
|
-
z.dispose();
|
|
201
|
-
const zc = zs.astype(x.dtype);
|
|
202
|
-
zs.dispose();
|
|
203
|
-
out = disposing(out, ops.add(out, zc));
|
|
204
|
-
zc.dispose();
|
|
205
|
-
}
|
|
206
|
-
xDrop?.dispose();
|
|
207
|
-
}
|
|
208
|
-
return out;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export class RMSNorm {
|
|
213
|
-
constructor(readonly weight: MlxArray | null, readonly eps: number) {}
|
|
214
|
-
forward(x: MlxArray): MlxArray {
|
|
215
|
-
return ops.rmsNorm(x, this.weight, this.eps);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export class QuantizedEmbedding {
|
|
220
|
-
constructor(
|
|
221
|
-
readonly w: MlxArray,
|
|
222
|
-
readonly scales: MlxArray,
|
|
223
|
-
readonly biases: MlxArray | null,
|
|
224
|
-
readonly spec: ops.QuantSpec,
|
|
225
|
-
) {}
|
|
226
|
-
|
|
227
|
-
static load(weights: Weights, path: string, config: ModelConfig): QuantizedEmbedding {
|
|
228
|
-
const spec = quantFor(config.quantization, path)!;
|
|
229
|
-
return new QuantizedEmbedding(
|
|
230
|
-
weights.tensor(`${path}.weight`),
|
|
231
|
-
weights.tensor(`${path}.scales`),
|
|
232
|
-
weights.has(`${path}.biases`) ? weights.tensor(`${path}.biases`) : null,
|
|
233
|
-
spec,
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** ids [1, L] (int/uint) → embeddings [1, L, hidden]. */
|
|
238
|
-
encode(ids: MlxArray): MlxArray {
|
|
239
|
-
const rows = ops.takeAxis(this.w, ids, 0);
|
|
240
|
-
const scaleRows = ops.takeAxis(this.scales, ids, 0);
|
|
241
|
-
const biasRows = this.biases ? ops.takeAxis(this.biases, ids, 0) : null;
|
|
242
|
-
const out = ops.dequantize(rows, scaleRows, biasRows, this.spec);
|
|
243
|
-
for (const a of [rows, scaleRows]) a.dispose();
|
|
244
|
-
biasRows?.dispose();
|
|
245
|
-
return out;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** Tied output head: h [1, L, hidden] → logits [1, L, vocab]. */
|
|
249
|
-
asLinear(h: MlxArray): MlxArray {
|
|
250
|
-
return ops.quantizedMatmul(h, this.w, this.scales, this.biases, this.spec, true);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** Fetched KV handed from a donor layer to its sharers (and to the
|
|
255
|
-
* speculative drafter). Owned by forwardLayers for the pass.
|
|
256
|
-
* `offsetArr` is an owned pre-write position snapshot. Donor and sharing
|
|
257
|
-
* attention layers keep it until the fetched KV is released, even if the
|
|
258
|
-
* cache advances or replaces its borrowed row-position array. */
|
|
259
|
-
export type SharedKv =
|
|
260
|
-
| { kind: "plain"; keys: MlxArray; values: MlxArray; offset: number;
|
|
261
|
-
offsetArr?: MlxArray;
|
|
262
|
-
/** TurboQuant deferred-V: `values` are still in the rotated (FWHT)
|
|
263
|
-
* domain — every consumer must un-rotate its attention OUTPUT
|
|
264
|
-
* (tq.unrotateValues) after sdpa. Attention is linear in V, so the
|
|
265
|
-
* result is the same decode, just transformed once per query row
|
|
266
|
-
* instead of once per cached token. */
|
|
267
|
-
vRotated?: boolean;
|
|
268
|
-
restoreValues?: (output: MlxArray) => MlxArray }
|
|
269
|
-
| { kind: "quant"; keys: ops.QuantizedTensor; values: ops.QuantizedTensor;
|
|
270
|
-
offset: number; groupSize: number; bits: number; offsetArr?: MlxArray }
|
|
271
|
-
| { kind: "view"; attention: KvAttentionView; offset: number; offsetArr?: MlxArray };
|
|
272
|
-
|
|
273
|
-
/** Retain an independent array handle for positions used across cache writes.
|
|
274
|
-
* The contiguous view preserves the integer values and shares their storage;
|
|
275
|
-
* callers release it with the fetched attention state. */
|
|
276
|
-
export function captureRopeOffsets(cache: Pick<Cache, "ropeOffsetArr">): MlxArray | undefined {
|
|
277
|
-
const offsets = cache.ropeOffsetArr;
|
|
278
|
-
return offsets ? ops.contiguous(offsets) : undefined;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/** Quantized attention consumes a numerical storage port, independent of the
|
|
282
|
-
* concrete layout used for row positions, retention and persistence. */
|
|
283
|
-
export interface QuantizedAttentionState {
|
|
284
|
-
readonly groupSize: number;
|
|
285
|
-
readonly bits: number;
|
|
286
|
-
updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor];
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** Values may stay in the codec's rotated domain until after attention. */
|
|
290
|
-
export interface RotatedValueAttentionState {
|
|
291
|
-
/** Capture a fetched value domain independently of later row changes. */
|
|
292
|
-
captureValueTransform?(): (output: MlxArray) => MlxArray;
|
|
293
|
-
updateAndFetchDeferredV(k: MlxArray, v: MlxArray): [MlxArray, MlxArray];
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/** A captured numerical view supports multiple queries without appending KV
|
|
297
|
-
* again. It owns its tensor handles independently of later cache membership or
|
|
298
|
-
* precision changes. Query/mask inputs are borrowed; outputs are owned. */
|
|
299
|
-
export interface KvAttentionView {
|
|
300
|
-
/** Qualified committed spans retain the single-position reduction order. */
|
|
301
|
-
attend(q: MlxArray, scale: number, mask: Mask, independentPositions?: boolean): MlxArray;
|
|
302
|
-
dispose(): void;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/** Storage appends once and hands every attention consumer the same view. */
|
|
306
|
-
export interface KvAttentionState {
|
|
307
|
-
appendAndFetch(k: MlxArray, v: MlxArray): KvAttentionView;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** Owned read-only planes with validity separate from physical padding. */
|
|
311
|
-
export interface KvDonorRows {
|
|
312
|
-
readonly keys: MlxArray;
|
|
313
|
-
readonly values: MlxArray;
|
|
314
|
-
readonly offsets: readonly number[];
|
|
315
|
-
readonly starts: readonly number[];
|
|
316
|
-
readonly ends: readonly number[];
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** Captured Q-only attention hides plain or encoded donor storage. */
|
|
320
|
-
export interface KvDonorAttention extends KvAttentionView {
|
|
321
|
-
readonly width: number;
|
|
322
|
-
readonly dtype: Dtype;
|
|
323
|
-
readonly offsets: readonly number[];
|
|
324
|
-
readonly starts: readonly number[];
|
|
325
|
-
readonly ends: readonly number[];
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export interface Cache {
|
|
329
|
-
/** Maximum committed positions before this state changes precision. */
|
|
330
|
-
maxAppendTokens?(): number;
|
|
331
|
-
captureDonorRows?(): KvDonorRows;
|
|
332
|
-
captureDonorAttention?(): KvDonorAttention;
|
|
333
|
-
/** A preparation method commits each request's own precision boundary.
|
|
334
|
-
* Splitting a forward for a sibling must not move that boundary. */
|
|
335
|
-
readonly prefillMaintenance?: {
|
|
336
|
-
beginPrefill(): void;
|
|
337
|
-
commitPrefill(rows: readonly number[]): void;
|
|
338
|
-
endPrefill(): void;
|
|
339
|
-
};
|
|
340
|
-
/** Optional representation-owned affine conversion. Logical position can
|
|
341
|
-
* differ from the physical write head in a padded rotating layout. */
|
|
342
|
-
readonly affineConversion?: { readonly offset: number; toQuantized(groupSize: number, bits: number): Cache };
|
|
343
|
-
readonly turboConversion?: { readonly offset: number; toTurboQuantized(kBits: number, vBits: number): Cache };
|
|
344
|
-
readonly attentionState?: KvAttentionState;
|
|
345
|
-
/** Earliest prefix whose retained representation can resume generation.
|
|
346
|
-
* Irreversible transitions may preserve bytes while invalidating older
|
|
347
|
-
* precision boundaries. Reuse policy must not trim below this offset. */
|
|
348
|
-
minimumReusableOffset?: number;
|
|
349
|
-
readonly rotatedValueAttention?: RotatedValueAttentionState;
|
|
350
|
-
readonly quantizedAttention?: QuantizedAttentionState;
|
|
351
|
-
offset: number;
|
|
352
|
-
/** Stable storage identity for compatibility guards and persistence.
|
|
353
|
-
* REQUIRED: every capability predicate (isPlainKvCache, isRotating*,
|
|
354
|
-
* kv-store codecs) is a string compare on this value, so a missing
|
|
355
|
-
* override used to fail OPEN into wrong routing (PRs #42/#43:
|
|
356
|
-
* BatchedRotatingCache shipped without one and batched joins dropped
|
|
357
|
-
* running rows). Wrappers report their own kind plus the inner kind. */
|
|
358
|
-
signature(): string;
|
|
359
|
-
/** Physical cache storage consumed by one additional token for one row.
|
|
360
|
-
* Recurrent caches return 0 because their state does not grow with the
|
|
361
|
-
* sequence. Optional for stateless/training adapters that are never
|
|
362
|
-
* admitted or persisted. */
|
|
363
|
-
bytesPerToken?(): number;
|
|
364
|
-
/** state() returned temporary views that the caller must release. */
|
|
365
|
-
readonly stateNeedsDispose?: boolean;
|
|
366
|
-
/** Compiled-decode trace adapters expose the RoPE offset as an int32
|
|
367
|
-
* array input here; real caches leave it unset (static int path). */
|
|
368
|
-
readonly ropeOffsetArr?: MlxArray;
|
|
369
|
-
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray];
|
|
370
|
-
/** Mask for an N-token step given this cache's state. */
|
|
371
|
-
makeMask(N: number, windowSize: number | null): Mask;
|
|
372
|
-
state(): MlxArray[];
|
|
373
|
-
/** Can `trim(n)` drop the last n tokens? (Ring caches lose trimability
|
|
374
|
-
* once wrapped.) */
|
|
375
|
-
isTrimmable(): boolean;
|
|
376
|
-
/** Drop the last n tokens (future writes overwrite them). `bypass` (default
|
|
377
|
-
* false → unchanged behavior) skips the isTrimmable guard for the
|
|
378
|
-
* speculative-decode rollback case (trim the last n ≤ γ tips right after a
|
|
379
|
-
* >1 concat write); rotating caches then physically shrink the buffer tail. */
|
|
380
|
-
trim(n: number, bypass?: boolean): void;
|
|
381
|
-
/** Speculative verify-round support for NON-trimmable recurrent caches
|
|
382
|
-
* (SSMCache — gated-DeltaNet conv + recurrent state). A cache exposing
|
|
383
|
-
* these is spec-eligible without isTrimmable(): the serve loop arms a
|
|
384
|
-
* round before the verify forward (the layer snapshots pre-round state —
|
|
385
|
-
* free, MLX arrays are immutable — and records its position-local kernel
|
|
386
|
-
* inputs), then resolves it: commit on full accept, or rollback(keep)
|
|
387
|
-
* which restores the snapshot and bit-exactly REPLAYS the first `keep`
|
|
388
|
-
* window tokens through the recurrence (identical arithmetic prefix ⇒
|
|
389
|
-
* identical state). Trimmable caches leave these unset and keep trim(). */
|
|
390
|
-
specRoundBegin?(): void;
|
|
391
|
-
specRoundCommit?(): void;
|
|
392
|
-
specRoundRollback?(keep: number): void;
|
|
393
|
-
dispose(): void;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
export function cacheSignature(cache: Cache | undefined): string {
|
|
397
|
-
return cache ? cache.signature() : "unknown";
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
export interface RowBatchCache extends Cache {
|
|
401
|
-
readonly batchSize: number;
|
|
402
|
-
filterRows(keep: readonly number[]): void;
|
|
403
|
-
extractRow(row: number): Cache | null;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
export function isRowBatchCache(cache: Cache): cache is RowBatchCache {
|
|
407
|
-
const candidate = cache as Partial<RowBatchCache>;
|
|
408
|
-
return typeof candidate.batchSize === "number" &&
|
|
409
|
-
typeof candidate.filterRows === "function" &&
|
|
410
|
-
typeof candidate.extractRow === "function";
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export function isPlainKvCache(cache: Cache | undefined): cache is KVCache {
|
|
414
|
-
return cacheSignature(cache) === "kv:plain";
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
export function isQuantizedKvCache(cache: Cache | undefined): cache is QuantizedKVCache {
|
|
418
|
-
return cacheSignature(cache).startsWith("kv:quant:");
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
export function isRotatingPlainCache(cache: Cache | undefined): cache is RotatingKVCache {
|
|
422
|
-
return cacheSignature(cache) === "kv:rotating-plain";
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
export function isRotatingQuantizedCache(
|
|
426
|
-
cache: Cache | undefined,
|
|
427
|
-
): cache is RotatingQuantizedKVCache {
|
|
428
|
-
return cacheSignature(cache).startsWith("kv:rotating-quant:");
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Capability contract for cache layouts that own their dynamic-row batching.
|
|
433
|
-
* The scheduler uses this for non-K/V state families (for example GLM's
|
|
434
|
-
* compressed MLA/DSA state) instead of teaching the scheduler their tensor
|
|
435
|
-
* layout. `mergeRows` writes into an empty cache and does not consume inputs;
|
|
436
|
-
* extracted rows are independent owned caches.
|
|
437
|
-
*/
|
|
438
|
-
export interface BatchableCache extends Cache {
|
|
439
|
-
readonly batchSize: number | null;
|
|
440
|
-
readonly rowOffsets: readonly number[];
|
|
441
|
-
readonly leftPad: readonly number[];
|
|
442
|
-
readonly maxTokens?: number;
|
|
443
|
-
makeEmptyBatch(): BatchableCache;
|
|
444
|
-
mergeRows(rows: readonly Cache[]): void;
|
|
445
|
-
extractRow(row: number): Cache;
|
|
446
|
-
filterRows(keep: readonly number[]): void;
|
|
447
|
-
projectedBytes(tokens: number): number;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/** Padding is interpreted by cache geometry, independently of scheduling.
|
|
451
|
-
* Left padding initializes an empty group; lengths count input columns before
|
|
452
|
-
* trailing padding. Finalize before ordinary decode or publishing checkpoints. */
|
|
453
|
-
export interface PrefillPadding {
|
|
454
|
-
readonly leftPadding?: readonly number[];
|
|
455
|
-
readonly lengths: readonly number[];
|
|
456
|
-
readonly rightPadding?: readonly number[];
|
|
457
|
-
}
|
|
458
|
-
export interface PaddedPrefillCache extends Cache {
|
|
459
|
-
preparePrefill(padding: PrefillPadding): void;
|
|
460
|
-
finalizePrefill(): void;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
export function isBatchableCache(cache: Cache): cache is BatchableCache {
|
|
464
|
-
const candidate = cache as Partial<BatchableCache>;
|
|
465
|
-
return (
|
|
466
|
-
typeof candidate.makeEmptyBatch === "function" &&
|
|
467
|
-
typeof candidate.mergeRows === "function" &&
|
|
468
|
-
typeof candidate.extractRow === "function" &&
|
|
469
|
-
typeof candidate.filterRows === "function" &&
|
|
470
|
-
typeof candidate.projectedBytes === "function" &&
|
|
471
|
-
Array.isArray(candidate.rowOffsets) &&
|
|
472
|
-
Array.isArray(candidate.leftPad)
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/** How one decode step interacts with a cache under compiled decode
|
|
477
|
-
* (returned by prepareDecodeStep, consumed by compiled-decode.ts):
|
|
478
|
-
* - "concat": the compiled graph fetches concat(active prefix, new kv)
|
|
479
|
-
* — same values as today's write-then-slice — and the WRITE happens
|
|
480
|
-
* outside the graph right after (writeDecodeStep), keeping the buffer
|
|
481
|
-
* single-referenced at its slice_update so mlx donates it in place.
|
|
482
|
-
* - "ring": the write happens IN-graph (slice_update_dynamic at
|
|
483
|
-
* writePos) and the fetch is the full updated buffer — the rotating
|
|
484
|
-
* steady state, where attention reads the whole ring in ring order
|
|
485
|
-
* (a concat would permute KV positions and change summation order).
|
|
486
|
-
* The updated buffers come back as closure outputs; adoptDecodeStep
|
|
487
|
-
* swaps them in. */
|
|
488
|
-
export interface DecodeStepPlan {
|
|
489
|
-
fetch: "concat" | "ring";
|
|
490
|
-
/** Write position on axis 2 (== ring index for rotating caches). */
|
|
491
|
-
writePos: number;
|
|
492
|
-
/** Valid prefix length for "concat" fetches (== offset). */
|
|
493
|
-
activeLen: number;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/** KV cache — port of mlx-lm cache.py KVCache: preallocated in steps of
|
|
497
|
-
* 256 along the sequence axis, updated in place via slice_update. */
|
|
498
|
-
export class KVCache implements Cache {
|
|
499
|
-
static readonly STEP = 256;
|
|
500
|
-
/** Set only by compiled-decode trace adapters (see Cache). */
|
|
501
|
-
readonly ropeOffsetArr?: MlxArray;
|
|
502
|
-
keys: MlxArray | null = null;
|
|
503
|
-
values: MlxArray | null = null;
|
|
504
|
-
offset = 0;
|
|
505
|
-
|
|
506
|
-
signature(): string { return "kv:plain"; }
|
|
507
|
-
|
|
508
|
-
bytesPerToken(): number {
|
|
509
|
-
const steps = this.keys?.shape[2] ?? 0;
|
|
510
|
-
return steps > 0 ? (this.keys!.nbytes + this.values!.nbytes) / steps : 0;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
514
|
-
const prev = this.offset;
|
|
515
|
-
const L = k.shape[2]!;
|
|
516
|
-
if (!this.keys || prev + L > this.keys.shape[2]!) {
|
|
517
|
-
const [B, H, , D] = k.shape as [number, number, number, number];
|
|
518
|
-
const vD = v.shape[3]!;
|
|
519
|
-
const nSteps = Math.floor((KVCache.STEP + L - 1) / KVCache.STEP);
|
|
520
|
-
const newK = ops.zeros([B, H, nSteps * KVCache.STEP, D], k.dtype);
|
|
521
|
-
const newV = ops.zeros([B, H, nSteps * KVCache.STEP, vD], v.dtype);
|
|
522
|
-
if (this.keys && this.values) {
|
|
523
|
-
let oldK = this.keys;
|
|
524
|
-
let oldV = this.values;
|
|
525
|
-
if (prev % KVCache.STEP !== 0) {
|
|
526
|
-
const trimK = oldK.slice([0, 0, 0, 0], [B, H, prev, D]);
|
|
527
|
-
const trimV = oldV.slice([0, 0, 0, 0], [B, H, prev, vD]);
|
|
528
|
-
oldK.dispose();
|
|
529
|
-
oldV.dispose();
|
|
530
|
-
oldK = trimK;
|
|
531
|
-
oldV = trimV;
|
|
532
|
-
}
|
|
533
|
-
this.keys = ops.concatAxis([oldK, newK], 2);
|
|
534
|
-
this.values = ops.concatAxis([oldV, newV], 2);
|
|
535
|
-
for (const a of [oldK, oldV, newK, newV]) a.dispose();
|
|
536
|
-
} else {
|
|
537
|
-
this.keys = newK;
|
|
538
|
-
this.values = newV;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
this.offset += L;
|
|
543
|
-
const [B, H, S, D] = this.keys!.shape as [number, number, number, number];
|
|
544
|
-
const vD = this.values!.shape[3]!;
|
|
545
|
-
const k2 = ops.sliceUpdate(this.keys!, k, [0, 0, prev, 0], [B, H, this.offset, D]);
|
|
546
|
-
const v2 = ops.sliceUpdate(this.values!, v, [0, 0, prev, 0], [B, H, this.offset, vD]);
|
|
547
|
-
this.keys!.dispose();
|
|
548
|
-
this.values!.dispose();
|
|
549
|
-
this.keys = k2;
|
|
550
|
-
this.values = v2;
|
|
551
|
-
return [
|
|
552
|
-
this.keys.slice([0, 0, 0, 0], [B, H, this.offset, D]),
|
|
553
|
-
this.values.slice([0, 0, 0, 0], [B, H, this.offset, vD]),
|
|
554
|
-
];
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
makeMask(N: number, windowSize: number | null): Mask {
|
|
558
|
-
if (N === 1) return { mode: "", arr: null };
|
|
559
|
-
// Windowless multi-token chunks get the STRING "causal" at ANY offset —
|
|
560
|
-
// mlx-lm cache.py:114-125 create_attention_mask does exactly this, and
|
|
561
|
-
// mx.fast SDPA aligns the string bottom-right, i.e. the same values as
|
|
562
|
-
// our materialized matrix. Shipping the array instead (the old
|
|
563
|
-
// offset>0 branch) forced a materialize-and-add mask path: a bool
|
|
564
|
-
// [N, offset+N] built PER CHUNK and read by every full-attention layer
|
|
565
|
-
// — the e4b 16k prefill gap vs mlx-lm (2026-07-06b, hd=512 layers
|
|
566
|
-
// where the fallback dispatch can't block-skip an array mask).
|
|
567
|
-
if (windowSize === null) return { mode: "causal", arr: null };
|
|
568
|
-
if (this.offset === 0 && N <= windowSize) return { mode: "causal", arr: null };
|
|
569
|
-
return { mode: "array", arr: createCausalMask(N, this.offset, windowSize) };
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/** Arrays to eval to materialize cache state (prefill chunk boundary). */
|
|
573
|
-
state(): MlxArray[] {
|
|
574
|
-
return this.keys && this.values ? [this.keys, this.values] : [];
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
isTrimmable(): boolean {
|
|
578
|
-
return true;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
trim(n: number): void {
|
|
582
|
-
this.offset = Math.max(0, this.offset - n);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/** Chronological (K, V) view sliced to offset (drafter donor read). */
|
|
586
|
-
captureDonorRows(): KvDonorRows {
|
|
587
|
-
const [keys, values] = this.temporalView();
|
|
588
|
-
const B = keys.shape[0]!, width = keys.shape[2]!;
|
|
589
|
-
return { keys, values, offsets: Array(B).fill(this.offset),
|
|
590
|
-
starts: Array(B).fill(0), ends: Array(B).fill(width) };
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
temporalView(): [MlxArray, MlxArray] {
|
|
594
|
-
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
595
|
-
const [B, H, , D] = this.keys.shape as [number, number, number, number];
|
|
596
|
-
const vD = this.values.shape[3]!;
|
|
597
|
-
return [
|
|
598
|
-
this.keys.slice([0, 0, 0, 0], [B, H, this.offset, D]),
|
|
599
|
-
this.values.slice([0, 0, 0, 0], [B, H, this.offset, vD]),
|
|
600
|
-
];
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
/** Compiled decode: host-side bookkeeping for a 1-token step — the
|
|
604
|
-
* growth/trim half of updateAndFetch (L=1), without the write. */
|
|
605
|
-
prepareDecodeStep(): DecodeStepPlan {
|
|
606
|
-
const prev = this.offset;
|
|
607
|
-
if (!this.keys || !this.values) throw new Error("compiled decode on an empty cache");
|
|
608
|
-
if (prev + 1 > this.keys.shape[2]!) {
|
|
609
|
-
const [B, H, , D] = this.keys.shape as [number, number, number, number];
|
|
610
|
-
const vD = this.values.shape[3]!;
|
|
611
|
-
let oldK = this.keys;
|
|
612
|
-
let oldV = this.values;
|
|
613
|
-
if (prev % KVCache.STEP !== 0) {
|
|
614
|
-
const trimK = oldK.slice([0, 0, 0, 0], [B, H, prev, D]);
|
|
615
|
-
const trimV = oldV.slice([0, 0, 0, 0], [B, H, prev, vD]);
|
|
616
|
-
oldK.dispose();
|
|
617
|
-
oldV.dispose();
|
|
618
|
-
oldK = trimK;
|
|
619
|
-
oldV = trimV;
|
|
620
|
-
}
|
|
621
|
-
const newK = ops.zeros([B, H, KVCache.STEP, D], oldK.dtype);
|
|
622
|
-
const newV = ops.zeros([B, H, KVCache.STEP, vD], oldV.dtype);
|
|
623
|
-
this.keys = ops.concatAxis([oldK, newK], 2);
|
|
624
|
-
this.values = ops.concatAxis([oldV, newV], 2);
|
|
625
|
-
for (const a of [oldK, oldV, newK, newV]) a.dispose();
|
|
626
|
-
}
|
|
627
|
-
return { fetch: "concat", writePos: prev, activeLen: prev };
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/** Compiled decode: the write half (same sliceUpdate as updateAndFetch).
|
|
631
|
-
* Takes ownership of kNew/vNew; returns the arrays to async-eval with
|
|
632
|
-
* the step (the updated buffers). */
|
|
633
|
-
writeDecodeStep(kNew: MlxArray, vNew: MlxArray): MlxArray[] {
|
|
634
|
-
const prev = this.offset;
|
|
635
|
-
const [B, H, , D] = this.keys!.shape as [number, number, number, number];
|
|
636
|
-
const vD = this.values!.shape[3]!;
|
|
637
|
-
const k2 = ops.sliceUpdate(this.keys!, kNew, [0, 0, prev, 0], [B, H, prev + 1, D]);
|
|
638
|
-
const v2 = ops.sliceUpdate(this.values!, vNew, [0, 0, prev, 0], [B, H, prev + 1, vD]);
|
|
639
|
-
this.keys!.dispose();
|
|
640
|
-
this.values!.dispose();
|
|
641
|
-
kNew.dispose();
|
|
642
|
-
vNew.dispose();
|
|
643
|
-
this.keys = k2;
|
|
644
|
-
this.values = v2;
|
|
645
|
-
this.offset = prev + 1;
|
|
646
|
-
return [k2, v2];
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/** Adopt persisted state (takes ownership of the arrays). */
|
|
650
|
-
restoreState(keys: MlxArray, values: MlxArray, offset: number): void {
|
|
651
|
-
this.dispose();
|
|
652
|
-
this.keys = keys;
|
|
653
|
-
this.values = values;
|
|
654
|
-
this.offset = offset;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/** Port of mlx-lm KVCache.to_quantized: quantize the whole buffer
|
|
658
|
-
* (padding included — it's overwritten before being read). */
|
|
659
|
-
toQuantized(groupSize: number, bits: number): QuantizedKVCache {
|
|
660
|
-
const q = new QuantizedKVCache(groupSize, bits);
|
|
661
|
-
q.offset = this.offset;
|
|
662
|
-
if (this.keys && this.values) {
|
|
663
|
-
q.keys = ops.quantize(this.keys, groupSize, bits);
|
|
664
|
-
q.values = ops.quantize(this.values, groupSize, bits);
|
|
665
|
-
}
|
|
666
|
-
this.dispose();
|
|
667
|
-
return q;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
dispose(): void {
|
|
671
|
-
this.keys?.dispose();
|
|
672
|
-
this.values?.dispose();
|
|
673
|
-
this.keys = this.values = null;
|
|
674
|
-
this.offset = 0;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/** Quantized KV cache — port of mlx-lm QuantizedKVCache: keys/values
|
|
679
|
-
* stored as (packed u32, scales, biases) triples, quantized along
|
|
680
|
-
* head_dim. Only full-attention layers convert (mlx-lm's rotating-cache
|
|
681
|
-
* quantization is NYI upstream; sliding layers are window-capped
|
|
682
|
-
* anyway). Attention dispatches to quantizedSdpa for these. */
|
|
683
|
-
export class QuantizedKVCache implements Cache {
|
|
684
|
-
minimumReusableOffset?: number;
|
|
685
|
-
static readonly STEP = 256;
|
|
686
|
-
/** Set only by compiled-decode trace adapters (see Cache). */
|
|
687
|
-
readonly ropeOffsetArr?: MlxArray;
|
|
688
|
-
keys: ops.QuantizedTensor | null = null;
|
|
689
|
-
values: ops.QuantizedTensor | null = null;
|
|
690
|
-
offset = 0;
|
|
691
|
-
|
|
692
|
-
constructor(readonly groupSize: number, readonly bits: number) {}
|
|
693
|
-
|
|
694
|
-
get quantizedAttention(): QuantizedAttentionState { return this; }
|
|
695
|
-
|
|
696
|
-
signature(): string { return `kv:quant:${this.bits}:${this.groupSize}`; }
|
|
697
|
-
|
|
698
|
-
bytesPerToken(): number {
|
|
699
|
-
const steps = this.keys?.packed.shape[2] ?? 0;
|
|
700
|
-
if (steps === 0 || !this.keys || !this.values) return 0;
|
|
701
|
-
const tensors = [
|
|
702
|
-
this.keys.packed, this.keys.scales, this.keys.biases,
|
|
703
|
-
this.values.packed, this.values.scales, this.values.biases,
|
|
704
|
-
];
|
|
705
|
-
return tensors.reduce((bytes, array) => bytes + array.nbytes, 0) / steps;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
updateAndFetch(): [MlxArray, MlxArray] {
|
|
709
|
-
throw new Error("QuantizedKVCache: use updateAndFetchQuantized");
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
#grow(triple: ops.QuantizedTensor | null, lastDims: number[], B: number, H: number, steps: number, dtype: Dtype): ops.QuantizedTensor {
|
|
713
|
-
const mk = (dim: number, dt: Dtype) => ops.zeros([B, H, steps, dim], dt);
|
|
714
|
-
if (!triple)
|
|
715
|
-
return { packed: mk(lastDims[0]!, Dtype.uint32), scales: mk(lastDims[1]!, dtype), biases: mk(lastDims[2]!, dtype) };
|
|
716
|
-
const ext = (a: MlxArray, dim: number, dt: Dtype): MlxArray => {
|
|
717
|
-
const z = mk(dim, dt);
|
|
718
|
-
const out = ops.concatAxis([a, z], 2);
|
|
719
|
-
a.dispose();
|
|
720
|
-
z.dispose();
|
|
721
|
-
return out;
|
|
722
|
-
};
|
|
723
|
-
return {
|
|
724
|
-
packed: ext(triple.packed, lastDims[0]!, Dtype.uint32),
|
|
725
|
-
scales: ext(triple.scales, lastDims[1]!, dtype),
|
|
726
|
-
biases: ext(triple.biases, lastDims[2]!, dtype),
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/** Quantize incoming k/v and append; returns quantized views to offset. */
|
|
731
|
-
updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
732
|
-
const [B, H, L, kD] = k.shape as [number, number, number, number];
|
|
733
|
-
const vD = v.shape[3]!;
|
|
734
|
-
const prev = this.offset;
|
|
735
|
-
const elPerInt = 32 / this.bits;
|
|
736
|
-
|
|
737
|
-
if (!this.keys || prev + L > this.keys.packed.shape[2]!) {
|
|
738
|
-
const newSteps = Math.floor((QuantizedKVCache.STEP + L - 1) / QuantizedKVCache.STEP) * QuantizedKVCache.STEP;
|
|
739
|
-
if (this.keys && prev % QuantizedKVCache.STEP !== 0) {
|
|
740
|
-
const trimTo = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
741
|
-
const cut = (a: MlxArray): MlxArray => {
|
|
742
|
-
const [b, h, , d] = a.shape as [number, number, number, number];
|
|
743
|
-
const s = a.slice([0, 0, 0, 0], [b, h, prev, d]);
|
|
744
|
-
a.dispose();
|
|
745
|
-
return s;
|
|
746
|
-
};
|
|
747
|
-
return { packed: cut(t.packed), scales: cut(t.scales), biases: cut(t.biases) };
|
|
748
|
-
};
|
|
749
|
-
this.keys = trimTo(this.keys);
|
|
750
|
-
this.values = trimTo(this.values!);
|
|
751
|
-
}
|
|
752
|
-
const dtype = k.dtype;
|
|
753
|
-
this.keys = this.#grow(this.keys, [kD / elPerInt, kD / this.groupSize, kD / this.groupSize], B, H, newSteps, dtype);
|
|
754
|
-
this.values = this.#grow(this.values, [vD / elPerInt, vD / this.groupSize, vD / this.groupSize], B, H, newSteps, dtype);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
this.offset += L;
|
|
758
|
-
const kq = ops.quantize(k, this.groupSize, this.bits);
|
|
759
|
-
const vq = ops.quantize(v, this.groupSize, this.bits);
|
|
760
|
-
|
|
761
|
-
const writeAll = (dst: ops.QuantizedTensor, src: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
762
|
-
const w = (d: MlxArray, s: MlxArray): MlxArray => {
|
|
763
|
-
const [b, h, , dd] = d.shape as [number, number, number, number];
|
|
764
|
-
const u = ops.sliceUpdate(d, s, [0, 0, prev, 0], [b, h, this.offset, dd]);
|
|
765
|
-
d.dispose();
|
|
766
|
-
s.dispose();
|
|
767
|
-
return u;
|
|
768
|
-
};
|
|
769
|
-
return {
|
|
770
|
-
packed: w(dst.packed, src.packed),
|
|
771
|
-
scales: w(dst.scales, src.scales),
|
|
772
|
-
biases: w(dst.biases, src.biases),
|
|
773
|
-
};
|
|
774
|
-
};
|
|
775
|
-
this.keys = writeAll(this.keys!, kq);
|
|
776
|
-
this.values = writeAll(this.values!, vq);
|
|
777
|
-
|
|
778
|
-
const fetch = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
779
|
-
const f = (a: MlxArray): MlxArray => {
|
|
780
|
-
const [b, h, , d] = a.shape as [number, number, number, number];
|
|
781
|
-
return a.slice([0, 0, 0, 0], [b, h, this.offset, d]);
|
|
782
|
-
};
|
|
783
|
-
return { packed: f(t.packed), scales: f(t.scales), biases: f(t.biases) };
|
|
784
|
-
};
|
|
785
|
-
return [fetch(this.keys), fetch(this.values)];
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
makeMask(N: number, windowSize: number | null): Mask {
|
|
789
|
-
if (N === 1) return { mode: "", arr: null };
|
|
790
|
-
// Same rule as KVCache.makeMask (mlx-lm cache.py:114-125): windowless
|
|
791
|
-
// multi-token chunks are the string "causal" at any offset. The old
|
|
792
|
-
// causalEquivalent escape hatch existed precisely because this case
|
|
793
|
-
// used to ship an array; only windowed continuations materialize now.
|
|
794
|
-
if (windowSize === null || (this.offset === 0 && N <= windowSize))
|
|
795
|
-
return { mode: "causal", arr: null };
|
|
796
|
-
return { mode: "array", arr: createCausalMask(N, this.offset, windowSize) };
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
/** Compiled decode: growth half of updateAndFetchQuantized (L=1),
|
|
800
|
-
* without quantize/write (those live in the compiled graph / after). */
|
|
801
|
-
prepareDecodeStep(): DecodeStepPlan {
|
|
802
|
-
const prev = this.offset;
|
|
803
|
-
if (!this.keys || !this.values) throw new Error("compiled decode on an empty cache");
|
|
804
|
-
if (prev + 1 > this.keys.packed.shape[2]!) {
|
|
805
|
-
if (prev % QuantizedKVCache.STEP !== 0) {
|
|
806
|
-
const trimTo = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
807
|
-
const cut = (a: MlxArray): MlxArray => {
|
|
808
|
-
const [b, h, , d] = a.shape as [number, number, number, number];
|
|
809
|
-
const s = a.slice([0, 0, 0, 0], [b, h, prev, d]);
|
|
810
|
-
a.dispose();
|
|
811
|
-
return s;
|
|
812
|
-
};
|
|
813
|
-
return { packed: cut(t.packed), scales: cut(t.scales), biases: cut(t.biases) };
|
|
814
|
-
};
|
|
815
|
-
this.keys = trimTo(this.keys);
|
|
816
|
-
this.values = trimTo(this.values);
|
|
817
|
-
}
|
|
818
|
-
const [B, H] = this.keys.packed.shape as [number, number, number, number];
|
|
819
|
-
const elPerInt = 32 / this.bits;
|
|
820
|
-
const kD = this.keys.packed.shape[3]! * elPerInt;
|
|
821
|
-
const vD = this.values.packed.shape[3]! * elPerInt;
|
|
822
|
-
const dtype = this.keys.scales.dtype;
|
|
823
|
-
this.keys = this.#grow(this.keys, [kD / elPerInt, kD / this.groupSize, kD / this.groupSize], B, H, QuantizedKVCache.STEP, dtype);
|
|
824
|
-
this.values = this.#grow(this.values, [vD / elPerInt, vD / this.groupSize, vD / this.groupSize], B, H, QuantizedKVCache.STEP, dtype);
|
|
825
|
-
}
|
|
826
|
-
return { fetch: "concat", writePos: prev, activeLen: prev };
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
/** Compiled decode: the write half — six sliceUpdates of the already-
|
|
830
|
-
* quantized step row (quantize ran in-graph). Takes ownership of the
|
|
831
|
-
* rows; returns the updated buffers to async-eval with the step. */
|
|
832
|
-
writeDecodeStep(rows: MlxArray[]): MlxArray[] {
|
|
833
|
-
const prev = this.offset;
|
|
834
|
-
const w = (d: MlxArray, srcRow: MlxArray): MlxArray => {
|
|
835
|
-
const [b, h, , dd] = d.shape as [number, number, number, number];
|
|
836
|
-
const u = ops.sliceUpdate(d, srcRow, [0, 0, prev, 0], [b, h, prev + 1, dd]);
|
|
837
|
-
d.dispose();
|
|
838
|
-
srcRow.dispose();
|
|
839
|
-
return u;
|
|
840
|
-
};
|
|
841
|
-
this.keys = {
|
|
842
|
-
packed: w(this.keys!.packed, rows[0]!),
|
|
843
|
-
scales: w(this.keys!.scales, rows[1]!),
|
|
844
|
-
biases: w(this.keys!.biases, rows[2]!),
|
|
845
|
-
};
|
|
846
|
-
this.values = {
|
|
847
|
-
packed: w(this.values!.packed, rows[3]!),
|
|
848
|
-
scales: w(this.values!.scales, rows[4]!),
|
|
849
|
-
biases: w(this.values!.biases, rows[5]!),
|
|
850
|
-
};
|
|
851
|
-
this.offset = prev + 1;
|
|
852
|
-
return this.state();
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
state(): MlxArray[] {
|
|
856
|
-
if (!this.keys || !this.values) return [];
|
|
857
|
-
return [
|
|
858
|
-
this.keys.packed, this.keys.scales, this.keys.biases,
|
|
859
|
-
this.values.packed, this.values.scales, this.values.biases,
|
|
860
|
-
];
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
isTrimmable(): boolean {
|
|
864
|
-
return true;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
trim(n: number): void {
|
|
868
|
-
this.offset = Math.max(0, this.offset - n);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
/** Adopt persisted state (takes ownership of the triples' arrays) —
|
|
872
|
-
* the quantized twin of KVCache.restoreState (kv-store persistence). */
|
|
873
|
-
restoreState(keys: ops.QuantizedTensor, values: ops.QuantizedTensor, offset: number): void {
|
|
874
|
-
this.dispose();
|
|
875
|
-
this.keys = keys;
|
|
876
|
-
this.values = values;
|
|
877
|
-
this.offset = offset;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
/** Chronological (K, V) triples sliced to offset — the quantized twin of
|
|
881
|
-
* KVCache.temporalView (batched merge/extend/filter reads). Caller owns
|
|
882
|
-
* the returned views. */
|
|
883
|
-
captureDonorAttention(): KvDonorAttention {
|
|
884
|
-
const [keys, values] = this.temporalView();
|
|
885
|
-
const B = keys.packed.shape[0]!, width = keys.packed.shape[2]!;
|
|
886
|
-
return { width, dtype: keys.scales.dtype, offsets: Array(B).fill(this.offset),
|
|
887
|
-
starts: Array(B).fill(0), ends: Array(B).fill(width),
|
|
888
|
-
attend: (q, scale, mask) => quantizedSdpa(q, keys, values, scale, mask, this.groupSize, this.bits),
|
|
889
|
-
dispose() { disposeTriple(keys); disposeTriple(values); },
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
894
|
-
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
895
|
-
const cut = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
896
|
-
const f = (a: MlxArray): MlxArray => {
|
|
897
|
-
const [b, h, , d] = a.shape as [number, number, number, number];
|
|
898
|
-
return a.slice([0, 0, 0, 0], [b, h, this.offset, d]);
|
|
899
|
-
};
|
|
900
|
-
return { packed: f(t.packed), scales: f(t.scales), biases: f(t.biases) };
|
|
901
|
-
};
|
|
902
|
-
return [cut(this.keys), cut(this.values)];
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
dispose(): void {
|
|
906
|
-
for (const a of this.state()) a.dispose();
|
|
907
|
-
this.keys = this.values = null;
|
|
908
|
-
this.offset = 0;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/** Rotating (sliding-window) KV cache — port of mlx-lm RotatingKVCache
|
|
913
|
-
* with keep=0 (gemma4's configuration): a ring buffer of max_size
|
|
914
|
-
* entries, so decode attends over at most the window. RoPE offsets use
|
|
915
|
-
* the true position; masks use the buffer-clamped offset. */
|
|
916
|
-
export class RotatingKVCache implements Cache {
|
|
917
|
-
declare minimumReusableOffset?: number;
|
|
918
|
-
static readonly STEP = 256;
|
|
919
|
-
/** Set only by compiled-decode trace adapters (see Cache). */
|
|
920
|
-
readonly ropeOffsetArr?: MlxArray;
|
|
921
|
-
keys: MlxArray | null = null;
|
|
922
|
-
values: MlxArray | null = null;
|
|
923
|
-
offset = 0;
|
|
924
|
-
#idx = 0;
|
|
925
|
-
readonly maxSize: number;
|
|
926
|
-
|
|
927
|
-
constructor(maxSize: number) {
|
|
928
|
-
this.maxSize = maxSize;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
signature(): string { return "kv:rotating-plain"; }
|
|
932
|
-
|
|
933
|
-
bytesPerToken(): number {
|
|
934
|
-
const steps = this.keys?.shape[2] ?? 0;
|
|
935
|
-
return steps > 0 ? (this.keys!.nbytes + this.values!.nbytes) / steps : 0;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
/** v with ring contents rearranged into temporal order (keep=0). */
|
|
939
|
-
#temporalOrder(v: MlxArray): MlxArray {
|
|
940
|
-
const S = v.shape[2]!;
|
|
941
|
-
const [B, H, , D] = v.shape as [number, number, number, number];
|
|
942
|
-
if (this.#idx === S) return v.slice([0, 0, 0, 0], [B, H, S, D]);
|
|
943
|
-
if (this.#idx < this.offset) {
|
|
944
|
-
const tail = v.slice([0, 0, this.#idx, 0], [B, H, S, D]);
|
|
945
|
-
const head = v.slice([0, 0, 0, 0], [B, H, this.#idx, D]);
|
|
946
|
-
const out = ops.concatAxis([tail, head], 2);
|
|
947
|
-
tail.dispose();
|
|
948
|
-
head.dispose();
|
|
949
|
-
return out;
|
|
950
|
-
}
|
|
951
|
-
return v.slice([0, 0, 0, 0], [B, H, this.#idx, D]);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
#trim(trimSize: number, v: MlxArray, append: MlxArray | null): MlxArray {
|
|
955
|
-
const [B, H, S, D] = v.shape as [number, number, number, number];
|
|
956
|
-
let base: MlxArray;
|
|
957
|
-
if (trimSize > 0) {
|
|
958
|
-
base = v.slice([0, 0, trimSize, 0], [B, H, S, D]);
|
|
959
|
-
} else {
|
|
960
|
-
base = v.slice([0, 0, 0, 0], [B, H, S, D]);
|
|
961
|
-
}
|
|
962
|
-
if (!append) return base;
|
|
963
|
-
const out = ops.concatAxis([base, append], 2);
|
|
964
|
-
base.dispose();
|
|
965
|
-
return out;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
#updateConcat(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
969
|
-
const S = k.shape[2]!;
|
|
970
|
-
if (!this.keys || !this.values) {
|
|
971
|
-
// own copies (full-range slice = cheap view node)
|
|
972
|
-
const [B, H, , D] = k.shape as [number, number, number, number];
|
|
973
|
-
const vD = v.shape[3]!;
|
|
974
|
-
this.keys = k.slice([0, 0, 0, 0], [B, H, S, D]);
|
|
975
|
-
this.values = v.slice([0, 0, 0, 0], [B, H, S, vD]);
|
|
976
|
-
} else {
|
|
977
|
-
const tk = this.#temporalOrder(this.keys);
|
|
978
|
-
const tv = this.#temporalOrder(this.values);
|
|
979
|
-
this.keys.dispose();
|
|
980
|
-
this.values.dispose();
|
|
981
|
-
this.#idx = tk.shape[2]!;
|
|
982
|
-
const trimSize = this.#idx - this.maxSize + 1;
|
|
983
|
-
this.keys = this.#trim(trimSize, tk, k);
|
|
984
|
-
this.values = this.#trim(trimSize, tv, v);
|
|
985
|
-
tk.dispose();
|
|
986
|
-
tv.dispose();
|
|
987
|
-
}
|
|
988
|
-
this.offset += S;
|
|
989
|
-
this.#idx = this.keys.shape[2]!;
|
|
990
|
-
return this.#fetchAll();
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
#updateInPlace(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
994
|
-
const [B, H, S, D] = k.shape as [number, number, number, number];
|
|
995
|
-
const vD = v.shape[3]!;
|
|
996
|
-
const prev = this.offset;
|
|
997
|
-
|
|
998
|
-
if (!this.keys || (prev >= this.keys.shape[2]! && this.keys.shape[2]! < this.maxSize)) {
|
|
999
|
-
const newSize = Math.min(RotatingKVCache.STEP, this.maxSize - prev);
|
|
1000
|
-
const newK = ops.zeros([B, H, newSize, D], k.dtype);
|
|
1001
|
-
const newV = ops.zeros([B, H, newSize, vD], v.dtype);
|
|
1002
|
-
if (this.keys && this.values) {
|
|
1003
|
-
const ck = ops.concatAxis([this.keys, newK], 2);
|
|
1004
|
-
const cv = ops.concatAxis([this.values, newV], 2);
|
|
1005
|
-
this.keys.dispose();
|
|
1006
|
-
this.values.dispose();
|
|
1007
|
-
newK.dispose();
|
|
1008
|
-
newV.dispose();
|
|
1009
|
-
this.keys = ck;
|
|
1010
|
-
this.values = cv;
|
|
1011
|
-
} else {
|
|
1012
|
-
this.keys = newK;
|
|
1013
|
-
this.values = newV;
|
|
1014
|
-
}
|
|
1015
|
-
this.#idx = prev;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
const trimSize = this.keys!.shape[2]! - this.maxSize;
|
|
1019
|
-
if (trimSize > 0) {
|
|
1020
|
-
const tk = this.#trim(trimSize, this.keys!, null);
|
|
1021
|
-
const tv = this.#trim(trimSize, this.values!, null);
|
|
1022
|
-
this.keys!.dispose();
|
|
1023
|
-
this.values!.dispose();
|
|
1024
|
-
this.keys = tk;
|
|
1025
|
-
this.values = tv;
|
|
1026
|
-
this.#idx = this.maxSize;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
if (this.#idx === this.maxSize) this.#idx = 0; // rotate (keep=0)
|
|
1030
|
-
|
|
1031
|
-
const [, , SK, DK] = this.keys!.shape as [number, number, number, number];
|
|
1032
|
-
const k2 = ops.sliceUpdate(this.keys!, k, [0, 0, this.#idx, 0], [B, H, this.#idx + S, DK]);
|
|
1033
|
-
const v2 = ops.sliceUpdate(this.values!, v, [0, 0, this.#idx, 0], [B, H, this.#idx + S, vD]);
|
|
1034
|
-
this.keys!.dispose();
|
|
1035
|
-
this.values!.dispose();
|
|
1036
|
-
this.keys = k2;
|
|
1037
|
-
this.values = v2;
|
|
1038
|
-
this.offset += S;
|
|
1039
|
-
this.#idx += S;
|
|
1040
|
-
|
|
1041
|
-
if (this.offset < this.maxSize) {
|
|
1042
|
-
const kOut = this.keys.slice([0, 0, 0, 0], [B, H, this.offset, DK]);
|
|
1043
|
-
const vOut = this.values.slice([0, 0, 0, 0], [B, H, this.offset, vD]);
|
|
1044
|
-
return [kOut, vOut];
|
|
1045
|
-
}
|
|
1046
|
-
return this.#fetchAll();
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
#fetchAll(): [MlxArray, MlxArray] {
|
|
1050
|
-
const [B, H, S, D] = this.keys!.shape as [number, number, number, number];
|
|
1051
|
-
const vD = this.values!.shape[3]!;
|
|
1052
|
-
return [
|
|
1053
|
-
this.keys!.slice([0, 0, 0, 0], [B, H, S, D]),
|
|
1054
|
-
this.values!.slice([0, 0, 0, 0], [B, H, S, vD]),
|
|
1055
|
-
];
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
1059
|
-
return k.shape[2]! === 1 ? this.#updateInPlace(k, v) : this.#updateConcat(k, v);
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
makeMask(N: number, windowSize: number | null): Mask {
|
|
1063
|
-
const window = windowSize ?? this.maxSize;
|
|
1064
|
-
if (N > 1) {
|
|
1065
|
-
const offset = Math.min(this.maxSize - 1, this.offset);
|
|
1066
|
-
if (offset + N > window)
|
|
1067
|
-
return { mode: "array", arr: createCausalMask(N, offset, window) };
|
|
1068
|
-
return { mode: "causal", arr: null };
|
|
1069
|
-
}
|
|
1070
|
-
// N == 1: eviction enforces the window (window === maxSize for gemma4)
|
|
1071
|
-
return { mode: "", arr: null };
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
state(): MlxArray[] {
|
|
1075
|
-
return this.keys && this.values ? [this.keys, this.values] : [];
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
/** Port of mlx-lm RotatingKVCache.is_trimmable/trim: only valid while
|
|
1079
|
-
* the ring has never wrapped (still in temporal order). */
|
|
1080
|
-
isTrimmable(): boolean {
|
|
1081
|
-
return this.offset < this.maxSize;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
trim(n: number, bypass = false): void {
|
|
1085
|
-
const k = Math.min(this.offset, n);
|
|
1086
|
-
if (!bypass) {
|
|
1087
|
-
// mlx-lm semantics: only valid pre-wrap (guarded by isTrimmable upstream).
|
|
1088
|
-
this.offset -= k;
|
|
1089
|
-
this.#idx -= k;
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
// Spec-decode rollback after a >1 #updateConcat write: the buffer is in
|
|
1093
|
-
// temporal order with #idx === bufferLen and the newest tokens are the last
|
|
1094
|
-
// rows, so physically slice the last k off and keep #idx === new length.
|
|
1095
|
-
// (Only sound right after a concat write with k ≤ γ — DSpark's only caller.)
|
|
1096
|
-
this.offset -= k;
|
|
1097
|
-
if (k > 0 && this.keys && this.values) {
|
|
1098
|
-
const [B, H, S, D] = this.keys.shape as [number, number, number, number];
|
|
1099
|
-
const vD = this.values.shape[3]!;
|
|
1100
|
-
const nk = this.keys.slice([0, 0, 0, 0], [B, H, S - k, D]);
|
|
1101
|
-
const nv = this.values.slice([0, 0, 0, 0], [B, H, S - k, vD]);
|
|
1102
|
-
this.keys.dispose();
|
|
1103
|
-
this.values.dispose();
|
|
1104
|
-
this.keys = nk;
|
|
1105
|
-
this.values = nv;
|
|
1106
|
-
this.#idx = nk.shape[2]!;
|
|
1107
|
-
} else {
|
|
1108
|
-
this.#idx -= k;
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
get ringIdx(): number {
|
|
1113
|
-
return this.#idx;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
/** Compiled decode: host-side bookkeeping of #updateInPlace (L=1) —
|
|
1117
|
-
* growth, oversize trim, rotation — without the write. */
|
|
1118
|
-
prepareDecodeStep(): DecodeStepPlan {
|
|
1119
|
-
const prev = this.offset;
|
|
1120
|
-
if (!this.keys || !this.values) throw new Error("compiled decode on an empty cache");
|
|
1121
|
-
if (prev >= this.keys.shape[2]! && this.keys.shape[2]! < this.maxSize) {
|
|
1122
|
-
const [B, H, , D] = this.keys.shape as [number, number, number, number];
|
|
1123
|
-
const vD = this.values.shape[3]!;
|
|
1124
|
-
const newSize = Math.min(RotatingKVCache.STEP, this.maxSize - prev);
|
|
1125
|
-
const newK = ops.zeros([B, H, newSize, D], this.keys.dtype);
|
|
1126
|
-
const newV = ops.zeros([B, H, newSize, vD], this.values.dtype);
|
|
1127
|
-
const ck = ops.concatAxis([this.keys, newK], 2);
|
|
1128
|
-
const cv = ops.concatAxis([this.values, newV], 2);
|
|
1129
|
-
for (const a of [this.keys, this.values, newK, newV]) a.dispose();
|
|
1130
|
-
this.keys = ck;
|
|
1131
|
-
this.values = cv;
|
|
1132
|
-
this.#idx = prev;
|
|
1133
|
-
}
|
|
1134
|
-
const trimSize = this.keys.shape[2]! - this.maxSize;
|
|
1135
|
-
if (trimSize > 0) {
|
|
1136
|
-
const tk = this.#trim(trimSize, this.keys, null);
|
|
1137
|
-
const tv = this.#trim(trimSize, this.values, null);
|
|
1138
|
-
this.keys.dispose();
|
|
1139
|
-
this.values.dispose();
|
|
1140
|
-
this.keys = tk;
|
|
1141
|
-
this.values = tv;
|
|
1142
|
-
this.#idx = this.maxSize;
|
|
1143
|
-
}
|
|
1144
|
-
if (this.#idx === this.maxSize) this.#idx = 0; // rotate (keep=0)
|
|
1145
|
-
// Pre-window-fill the attended set is the [0..offset] prefix plus the
|
|
1146
|
-
// new row (today's slice of the updated buffer); once writes wrap,
|
|
1147
|
-
// it's the whole ring in ring order, which only the in-graph
|
|
1148
|
-
// write-then-read-all form reproduces bit-exactly.
|
|
1149
|
-
const fetch = prev + 1 < this.maxSize && this.#idx === prev ? "concat" : "ring";
|
|
1150
|
-
return { fetch, writePos: this.#idx, activeLen: prev };
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
/** Compiled decode, concat fetch: the write half. Takes ownership of
|
|
1154
|
-
* kNew/vNew; returns the updated buffers to async-eval. */
|
|
1155
|
-
writeDecodeStep(kNew: MlxArray, vNew: MlxArray): MlxArray[] {
|
|
1156
|
-
const [B, H, , D] = this.keys!.shape as [number, number, number, number];
|
|
1157
|
-
const vD = this.values!.shape[3]!;
|
|
1158
|
-
const k2 = ops.sliceUpdate(this.keys!, kNew, [0, 0, this.#idx, 0], [B, H, this.#idx + 1, D]);
|
|
1159
|
-
const v2 = ops.sliceUpdate(this.values!, vNew, [0, 0, this.#idx, 0], [B, H, this.#idx + 1, vD]);
|
|
1160
|
-
this.keys!.dispose();
|
|
1161
|
-
this.values!.dispose();
|
|
1162
|
-
kNew.dispose();
|
|
1163
|
-
vNew.dispose();
|
|
1164
|
-
this.keys = k2;
|
|
1165
|
-
this.values = v2;
|
|
1166
|
-
this.offset += 1;
|
|
1167
|
-
this.#idx += 1;
|
|
1168
|
-
return [k2, v2];
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
/** Compiled decode, ring fetch: adopt the in-graph-updated buffers
|
|
1172
|
-
* (closure outputs; ownership transfers here). */
|
|
1173
|
-
adoptDecodeStep(newKeys: MlxArray, newValues: MlxArray): MlxArray[] {
|
|
1174
|
-
this.keys!.dispose();
|
|
1175
|
-
this.values!.dispose();
|
|
1176
|
-
this.keys = newKeys;
|
|
1177
|
-
this.values = newValues;
|
|
1178
|
-
this.offset += 1;
|
|
1179
|
-
this.#idx += 1;
|
|
1180
|
-
return [newKeys, newValues];
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
/** Chronological (K, V) view, valid length min(offset, maxSize)
|
|
1184
|
-
* (port of optiq kv_view._read_cache_temporal). */
|
|
1185
|
-
captureDonorRows(): KvDonorRows {
|
|
1186
|
-
const [keys, values] = this.temporalView();
|
|
1187
|
-
const B = keys.shape[0]!, width = keys.shape[2]!;
|
|
1188
|
-
return { keys, values, offsets: Array(B).fill(this.offset),
|
|
1189
|
-
starts: Array(B).fill(0), ends: Array(B).fill(width) };
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
temporalView(): [MlxArray, MlxArray] {
|
|
1193
|
-
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
1194
|
-
const tk = this.#temporalOrder(this.keys);
|
|
1195
|
-
const tv = this.#temporalOrder(this.values);
|
|
1196
|
-
const valid = Math.min(this.offset, this.maxSize);
|
|
1197
|
-
const cut = (a: MlxArray): MlxArray => {
|
|
1198
|
-
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1199
|
-
const s = a.slice([0, 0, 0, 0], [B, H, valid, D]);
|
|
1200
|
-
a.dispose();
|
|
1201
|
-
return s;
|
|
1202
|
-
};
|
|
1203
|
-
return [cut(tk), cut(tv)];
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
/** Adopt persisted state (takes ownership of the arrays). */
|
|
1207
|
-
restoreState(keys: MlxArray, values: MlxArray, offset: number, idx: number): void {
|
|
1208
|
-
this.dispose();
|
|
1209
|
-
this.keys = keys;
|
|
1210
|
-
this.values = values;
|
|
1211
|
-
this.offset = offset;
|
|
1212
|
-
this.#idx = idx;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
/** Port of optiq rotating.py _replay_into_quantized: quantize the
|
|
1216
|
-
* whole buffer AS-LAID-OUT (ring order, not temporal order — correct
|
|
1217
|
-
* because ringIdx is preserved with it) into a
|
|
1218
|
-
* RotatingQuantizedKVCache. */
|
|
1219
|
-
toQuantized(groupSize: number, bits: number): RotatingQuantizedKVCache {
|
|
1220
|
-
const q = new RotatingQuantizedKVCache(this.maxSize, groupSize, bits);
|
|
1221
|
-
if (this.keys && this.values) {
|
|
1222
|
-
q.keys = ops.quantize(this.keys, groupSize, bits);
|
|
1223
|
-
q.values = ops.quantize(this.values, groupSize, bits);
|
|
1224
|
-
}
|
|
1225
|
-
q.offset = this.offset;
|
|
1226
|
-
q.ringIdx = this.#idx;
|
|
1227
|
-
this.dispose();
|
|
1228
|
-
return q;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
dispose(): void {
|
|
1232
|
-
this.keys?.dispose();
|
|
1233
|
-
this.values?.dispose();
|
|
1234
|
-
this.keys = this.values = null;
|
|
1235
|
-
this.offset = 0;
|
|
1236
|
-
this.#idx = 0;
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
export const mapTriple = (
|
|
1241
|
-
t: ops.QuantizedTensor, f: (a: MlxArray) => MlxArray,
|
|
1242
|
-
): ops.QuantizedTensor => ({ packed: f(t.packed), scales: f(t.scales), biases: f(t.biases) });
|
|
1243
|
-
|
|
1244
|
-
export const disposeTriple = (t: ops.QuantizedTensor): void => {
|
|
1245
|
-
t.packed.dispose();
|
|
1246
|
-
t.scales.dispose();
|
|
1247
|
-
t.biases.dispose();
|
|
1248
|
-
};
|
|
1249
|
-
|
|
1250
|
-
/** Quantized rotating (sliding-window) KV cache — port of optiq
|
|
1251
|
-
* runtime/kv/rotating.py RotatingQuantizedKVCache with keep=0
|
|
1252
|
-
* (gemma4's configuration): RotatingKVCache's ring mechanics over
|
|
1253
|
-
* (packed, scales, biases) triples, storage convention identical to
|
|
1254
|
-
* QuantizedKVCache. Returns ACTIVE QUANTIZED SLICES — the oracle's
|
|
1255
|
-
* module docstring claims dequantize-on-read, but its code does not
|
|
1256
|
-
* (Phase 9 finding; port follows the code). optiq's producer-registry
|
|
1257
|
-
* + SDPA patches are unnecessary here: our SharedKv carries
|
|
1258
|
-
* groupSize/bits through the donor→sharer plumbing explicitly. */
|
|
1259
|
-
export class RotatingQuantizedKVCache implements Cache {
|
|
1260
|
-
get quantizedAttention(): QuantizedAttentionState { return this; }
|
|
1261
|
-
declare minimumReusableOffset?: number;
|
|
1262
|
-
static readonly STEP = 256;
|
|
1263
|
-
/** Set only by compiled-decode trace adapters (see Cache). */
|
|
1264
|
-
readonly ropeOffsetArr?: MlxArray;
|
|
1265
|
-
keys: ops.QuantizedTensor | null = null;
|
|
1266
|
-
values: ops.QuantizedTensor | null = null;
|
|
1267
|
-
offset = 0;
|
|
1268
|
-
/** Ring write index (oracle `_idx`); public so toQuantized replay
|
|
1269
|
-
* and persistence can carry it. */
|
|
1270
|
-
ringIdx = 0;
|
|
1271
|
-
readonly maxSize: number;
|
|
1272
|
-
|
|
1273
|
-
constructor(maxSize: number, readonly groupSize: number, readonly bits: number) {
|
|
1274
|
-
this.maxSize = maxSize;
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
signature(): string { return `kv:rotating-quant:${this.bits}:${this.groupSize}`; }
|
|
1278
|
-
|
|
1279
|
-
bytesPerToken(): number {
|
|
1280
|
-
const steps = this.keys?.packed.shape[2] ?? 0;
|
|
1281
|
-
if (steps === 0 || !this.keys || !this.values) return 0;
|
|
1282
|
-
const tensors = [
|
|
1283
|
-
this.keys.packed, this.keys.scales, this.keys.biases,
|
|
1284
|
-
this.values.packed, this.values.scales, this.values.biases,
|
|
1285
|
-
];
|
|
1286
|
-
return tensors.reduce((bytes, array) => bytes + array.nbytes, 0) / steps;
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
updateAndFetch(): [MlxArray, MlxArray] {
|
|
1290
|
-
throw new Error("RotatingQuantizedKVCache: use updateAndFetchQuantized");
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
#seqLen(): number {
|
|
1294
|
-
return this.keys ? this.keys.packed.shape[2]! : 0;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
/** Empty (packed, scales, biases) triple of T tokens (oracle _alloc_pair). */
|
|
1298
|
-
#allocPair(B: number, H: number, T: number, dim: number, dtype: Dtype): ops.QuantizedTensor {
|
|
1299
|
-
const elPerInt = 32 / this.bits;
|
|
1300
|
-
return {
|
|
1301
|
-
packed: ops.zeros([B, H, T, dim / elPerInt], Dtype.uint32),
|
|
1302
|
-
scales: ops.zeros([B, H, T, dim / this.groupSize], dtype),
|
|
1303
|
-
biases: ops.zeros([B, H, T, dim / this.groupSize], dtype),
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
/** Ring contents rearranged into temporal order, per component (keep=0). */
|
|
1308
|
-
#temporalOrder(t: ops.QuantizedTensor): ops.QuantizedTensor {
|
|
1309
|
-
const S = t.packed.shape[2]!;
|
|
1310
|
-
const cut = (a: MlxArray, from: number, to: number): MlxArray => {
|
|
1311
|
-
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1312
|
-
return a.slice([0, 0, from, 0], [B, H, to, D]);
|
|
1313
|
-
};
|
|
1314
|
-
if (this.ringIdx === S) return mapTriple(t, (a) => cut(a, 0, S));
|
|
1315
|
-
if (this.ringIdx < this.offset) {
|
|
1316
|
-
return mapTriple(t, (a) => {
|
|
1317
|
-
const tail = cut(a, this.ringIdx, S);
|
|
1318
|
-
const head = cut(a, 0, this.ringIdx);
|
|
1319
|
-
const out = ops.concatAxis([tail, head], 2);
|
|
1320
|
-
tail.dispose();
|
|
1321
|
-
head.dispose();
|
|
1322
|
-
return out;
|
|
1323
|
-
});
|
|
1324
|
-
}
|
|
1325
|
-
return mapTriple(t, (a) => cut(a, 0, this.ringIdx));
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
#trim(trimSize: number, t: ops.QuantizedTensor, append: ops.QuantizedTensor | null): ops.QuantizedTensor {
|
|
1329
|
-
const part = (a: MlxArray, ap: MlxArray | null): MlxArray => {
|
|
1330
|
-
const [B, H, S, D] = a.shape as [number, number, number, number];
|
|
1331
|
-
const base = a.slice([0, 0, trimSize > 0 ? trimSize : 0, 0], [B, H, S, D]);
|
|
1332
|
-
if (!ap) return base;
|
|
1333
|
-
const out = ops.concatAxis([base, ap], 2);
|
|
1334
|
-
base.dispose();
|
|
1335
|
-
return out;
|
|
1336
|
-
};
|
|
1337
|
-
return {
|
|
1338
|
-
packed: part(t.packed, append?.packed ?? null),
|
|
1339
|
-
scales: part(t.scales, append?.scales ?? null),
|
|
1340
|
-
biases: part(t.biases, append?.biases ?? null),
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
#updateConcat(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
1345
|
-
const S = k.shape[2]!;
|
|
1346
|
-
const kq = ops.quantize(k, this.groupSize, this.bits);
|
|
1347
|
-
const vq = ops.quantize(v, this.groupSize, this.bits);
|
|
1348
|
-
if (!this.keys || !this.values) {
|
|
1349
|
-
this.keys = kq;
|
|
1350
|
-
this.values = vq;
|
|
1351
|
-
} else {
|
|
1352
|
-
const tk = this.#temporalOrder(this.keys);
|
|
1353
|
-
const tv = this.#temporalOrder(this.values);
|
|
1354
|
-
disposeTriple(this.keys);
|
|
1355
|
-
disposeTriple(this.values);
|
|
1356
|
-
this.ringIdx = tk.packed.shape[2]!;
|
|
1357
|
-
const trimSize = this.ringIdx - this.maxSize + 1;
|
|
1358
|
-
this.keys = this.#trim(trimSize, tk, kq);
|
|
1359
|
-
this.values = this.#trim(trimSize, tv, vq);
|
|
1360
|
-
for (const t of [tk, tv, kq, vq]) disposeTriple(t);
|
|
1361
|
-
}
|
|
1362
|
-
this.offset += S;
|
|
1363
|
-
this.ringIdx = this.#seqLen();
|
|
1364
|
-
return this.#activeSlices();
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
#updateInPlace(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
1368
|
-
const [B, H, S, D] = k.shape as [number, number, number, number];
|
|
1369
|
-
const vD = v.shape[3]!;
|
|
1370
|
-
const prev = this.offset;
|
|
1371
|
-
|
|
1372
|
-
if (!this.keys || (prev >= this.#seqLen() && this.#seqLen() < this.maxSize)) {
|
|
1373
|
-
const newSize = Math.min(RotatingQuantizedKVCache.STEP, this.maxSize - prev);
|
|
1374
|
-
const newK = this.#allocPair(B, H, newSize, D, k.dtype);
|
|
1375
|
-
const newV = this.#allocPair(B, H, newSize, vD, v.dtype);
|
|
1376
|
-
if (this.keys && this.values) {
|
|
1377
|
-
const grow = (old: ops.QuantizedTensor, add: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
1378
|
-
const cat = (a: MlxArray, b: MlxArray): MlxArray => {
|
|
1379
|
-
const out = ops.concatAxis([a, b], 2);
|
|
1380
|
-
a.dispose();
|
|
1381
|
-
b.dispose();
|
|
1382
|
-
return out;
|
|
1383
|
-
};
|
|
1384
|
-
return {
|
|
1385
|
-
packed: cat(old.packed, add.packed),
|
|
1386
|
-
scales: cat(old.scales, add.scales),
|
|
1387
|
-
biases: cat(old.biases, add.biases),
|
|
1388
|
-
};
|
|
1389
|
-
};
|
|
1390
|
-
this.keys = grow(this.keys, newK);
|
|
1391
|
-
this.values = grow(this.values, newV);
|
|
1392
|
-
} else {
|
|
1393
|
-
this.keys = newK;
|
|
1394
|
-
this.values = newV;
|
|
1395
|
-
}
|
|
1396
|
-
this.ringIdx = prev;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
const trimSize = this.#seqLen() - this.maxSize;
|
|
1400
|
-
if (trimSize > 0) {
|
|
1401
|
-
const tk = this.#trim(trimSize, this.keys!, null);
|
|
1402
|
-
const tv = this.#trim(trimSize, this.values!, null);
|
|
1403
|
-
disposeTriple(this.keys!);
|
|
1404
|
-
disposeTriple(this.values!);
|
|
1405
|
-
this.keys = tk;
|
|
1406
|
-
this.values = tv;
|
|
1407
|
-
this.ringIdx = this.maxSize;
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
if (this.ringIdx === this.maxSize) this.ringIdx = 0; // rotate (keep=0)
|
|
1411
|
-
|
|
1412
|
-
const kq = ops.quantize(k, this.groupSize, this.bits);
|
|
1413
|
-
const vq = ops.quantize(v, this.groupSize, this.bits);
|
|
1414
|
-
const writeAt = (dst: ops.QuantizedTensor, src: ops.QuantizedTensor): ops.QuantizedTensor => ({
|
|
1415
|
-
packed: this.#assign(dst.packed, src.packed, S),
|
|
1416
|
-
scales: this.#assign(dst.scales, src.scales, S),
|
|
1417
|
-
biases: this.#assign(dst.biases, src.biases, S),
|
|
1418
|
-
});
|
|
1419
|
-
this.keys = writeAt(this.keys!, kq);
|
|
1420
|
-
this.values = writeAt(this.values!, vq);
|
|
1421
|
-
disposeTriple(kq);
|
|
1422
|
-
disposeTriple(vq);
|
|
1423
|
-
|
|
1424
|
-
this.offset += S;
|
|
1425
|
-
this.ringIdx += S;
|
|
1426
|
-
return this.#activeSlices();
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
#assign(dst: MlxArray, src: MlxArray, S: number): MlxArray {
|
|
1430
|
-
const [B, H, , D] = dst.shape as [number, number, number, number];
|
|
1431
|
-
const out = ops.sliceUpdate(dst, src, [0, 0, this.ringIdx, 0], [B, H, this.ringIdx + S, D]);
|
|
1432
|
-
dst.dispose();
|
|
1433
|
-
return out;
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
/** Active window as quantized triples (oracle _active_slices; fresh
|
|
1437
|
-
* view handles so callers own what they dispose). */
|
|
1438
|
-
#activeSlices(): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
1439
|
-
const upTo = this.offset < this.maxSize ? this.offset : this.#seqLen();
|
|
1440
|
-
const cut = (a: MlxArray): MlxArray => {
|
|
1441
|
-
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1442
|
-
return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
|
|
1443
|
-
};
|
|
1444
|
-
return [mapTriple(this.keys!, cut), mapTriple(this.values!, cut)];
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
/** Quantize incoming k/v and write into the ring; returns active
|
|
1448
|
-
* quantized triples (S=1 in place, S>1 via temporal-order + concat). */
|
|
1449
|
-
updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
1450
|
-
return k.shape[2]! === 1 ? this.#updateInPlace(k, v) : this.#updateConcat(k, v);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
/** Same mask formula as RotatingKVCache (inherited in the oracle). */
|
|
1454
|
-
makeMask(N: number, windowSize: number | null): Mask {
|
|
1455
|
-
const window = windowSize ?? this.maxSize;
|
|
1456
|
-
if (N > 1) {
|
|
1457
|
-
const offset = Math.min(this.maxSize - 1, this.offset);
|
|
1458
|
-
if (offset + N > window)
|
|
1459
|
-
return { mode: "array", arr: createCausalMask(N, offset, window) };
|
|
1460
|
-
return { mode: "causal", arr: null };
|
|
1461
|
-
}
|
|
1462
|
-
return { mode: "", arr: null };
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
state(): MlxArray[] {
|
|
1466
|
-
if (!this.keys || !this.values) return [];
|
|
1467
|
-
return [
|
|
1468
|
-
this.keys.packed, this.keys.scales, this.keys.biases,
|
|
1469
|
-
this.values.packed, this.values.scales, this.values.biases,
|
|
1470
|
-
];
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
/** Ring rule (inherited semantics): trimmable only before wrap. */
|
|
1474
|
-
isTrimmable(): boolean {
|
|
1475
|
-
return this.offset < this.maxSize;
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
trim(n: number, bypass = false): void {
|
|
1479
|
-
const k = Math.min(this.offset, n);
|
|
1480
|
-
if (!bypass) {
|
|
1481
|
-
this.offset -= k;
|
|
1482
|
-
this.ringIdx -= k;
|
|
1483
|
-
return;
|
|
1484
|
-
}
|
|
1485
|
-
// Spec-decode rollback after a >1 concat write — physically slice the last k
|
|
1486
|
-
// off each (packed, scales, biases) component and keep ringIdx === seqLen.
|
|
1487
|
-
this.offset -= k;
|
|
1488
|
-
if (k > 0 && this.keys && this.values) {
|
|
1489
|
-
const S = this.#seqLen();
|
|
1490
|
-
const cut = (a: MlxArray): MlxArray => {
|
|
1491
|
-
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1492
|
-
return a.slice([0, 0, 0, 0], [B, H, S - k, D]);
|
|
1493
|
-
};
|
|
1494
|
-
const nk = mapTriple(this.keys, cut);
|
|
1495
|
-
const nv = mapTriple(this.values, cut);
|
|
1496
|
-
disposeTriple(this.keys);
|
|
1497
|
-
disposeTriple(this.values);
|
|
1498
|
-
this.keys = nk;
|
|
1499
|
-
this.values = nv;
|
|
1500
|
-
this.ringIdx = this.#seqLen();
|
|
1501
|
-
} else {
|
|
1502
|
-
this.ringIdx -= k;
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
/** Adopt persisted state (takes ownership of the triples' arrays) —
|
|
1507
|
-
* ring order as-laid-out, ringIdx carried with it (kv-store persistence). */
|
|
1508
|
-
restoreState(keys: ops.QuantizedTensor, values: ops.QuantizedTensor, offset: number, idx: number): void {
|
|
1509
|
-
this.dispose();
|
|
1510
|
-
this.keys = keys;
|
|
1511
|
-
this.values = values;
|
|
1512
|
-
this.offset = offset;
|
|
1513
|
-
this.ringIdx = idx;
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
/** Chronological (K, V) triples cut to the valid window — the quantized
|
|
1517
|
-
* twin of RotatingKVCache.temporalView (batched merge reads, Phase 3
|
|
1518
|
-
* milestone 2). Caller owns the returned views. */
|
|
1519
|
-
captureDonorAttention(): KvDonorAttention {
|
|
1520
|
-
const [keys, values] = this.temporalView();
|
|
1521
|
-
const B = keys.packed.shape[0]!, width = keys.packed.shape[2]!;
|
|
1522
|
-
return { width, dtype: keys.scales.dtype, offsets: Array(B).fill(this.offset),
|
|
1523
|
-
starts: Array(B).fill(0), ends: Array(B).fill(width),
|
|
1524
|
-
attend: (q, scale, mask) => quantizedSdpa(q, keys, values, scale, mask, this.groupSize, this.bits),
|
|
1525
|
-
dispose() { disposeTriple(keys); disposeTriple(values); },
|
|
1526
|
-
};
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
1530
|
-
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
1531
|
-
const valid = Math.min(this.offset, this.maxSize);
|
|
1532
|
-
const cutValid = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
|
|
1533
|
-
mapTriple(t, (a) => {
|
|
1534
|
-
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1535
|
-
return a.slice([0, 0, 0, 0], [B, H, valid, D]);
|
|
1536
|
-
});
|
|
1537
|
-
const tk = this.#temporalOrder(this.keys);
|
|
1538
|
-
const tv = this.#temporalOrder(this.values);
|
|
1539
|
-
const out: [ops.QuantizedTensor, ops.QuantizedTensor] = [cutValid(tk), cutValid(tv)];
|
|
1540
|
-
disposeTriple(tk);
|
|
1541
|
-
disposeTriple(tv);
|
|
1542
|
-
return out;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
/** Oracle: to_quantized on an already-quantized rotating cache is
|
|
1546
|
-
* idempotent. */
|
|
1547
|
-
toQuantized(): RotatingQuantizedKVCache {
|
|
1548
|
-
return this;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
/** Compiled decode: host-side bookkeeping of #updateInPlace (L=1) —
|
|
1552
|
-
* growth, oversize trim, rotation — without quantize/write. */
|
|
1553
|
-
prepareDecodeStep(): DecodeStepPlan {
|
|
1554
|
-
const prev = this.offset;
|
|
1555
|
-
if (!this.keys || !this.values) throw new Error("compiled decode on an empty cache");
|
|
1556
|
-
if (prev >= this.#seqLen() && this.#seqLen() < this.maxSize) {
|
|
1557
|
-
const [B, H, , pD] = this.keys.packed.shape as [number, number, number, number];
|
|
1558
|
-
const elPerInt = 32 / this.bits;
|
|
1559
|
-
const kD = pD * elPerInt;
|
|
1560
|
-
const vD = this.values.packed.shape[3]! * elPerInt;
|
|
1561
|
-
const dtype = this.keys.scales.dtype;
|
|
1562
|
-
const newSize = Math.min(RotatingQuantizedKVCache.STEP, this.maxSize - prev);
|
|
1563
|
-
const newK = this.#allocPair(B, H, newSize, kD, dtype);
|
|
1564
|
-
const newV = this.#allocPair(B, H, newSize, vD, dtype);
|
|
1565
|
-
const grow = (old: ops.QuantizedTensor, add: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
1566
|
-
const cat = (a: MlxArray, b: MlxArray): MlxArray => {
|
|
1567
|
-
const out = ops.concatAxis([a, b], 2);
|
|
1568
|
-
a.dispose();
|
|
1569
|
-
b.dispose();
|
|
1570
|
-
return out;
|
|
1571
|
-
};
|
|
1572
|
-
return {
|
|
1573
|
-
packed: cat(old.packed, add.packed),
|
|
1574
|
-
scales: cat(old.scales, add.scales),
|
|
1575
|
-
biases: cat(old.biases, add.biases),
|
|
1576
|
-
};
|
|
1577
|
-
};
|
|
1578
|
-
this.keys = grow(this.keys, newK);
|
|
1579
|
-
this.values = grow(this.values, newV);
|
|
1580
|
-
this.ringIdx = prev;
|
|
1581
|
-
}
|
|
1582
|
-
const trimSize = this.#seqLen() - this.maxSize;
|
|
1583
|
-
if (trimSize > 0) {
|
|
1584
|
-
const tk = this.#trim(trimSize, this.keys, null);
|
|
1585
|
-
const tv = this.#trim(trimSize, this.values, null);
|
|
1586
|
-
disposeTriple(this.keys);
|
|
1587
|
-
disposeTriple(this.values);
|
|
1588
|
-
this.keys = tk;
|
|
1589
|
-
this.values = tv;
|
|
1590
|
-
this.ringIdx = this.maxSize;
|
|
1591
|
-
}
|
|
1592
|
-
if (this.ringIdx === this.maxSize) this.ringIdx = 0; // rotate (keep=0)
|
|
1593
|
-
const fetch = prev + 1 < this.maxSize && this.ringIdx === prev ? "concat" : "ring";
|
|
1594
|
-
return { fetch, writePos: this.ringIdx, activeLen: prev };
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
/** Compiled decode, concat fetch: six sliceUpdates of the in-graph-
|
|
1598
|
-
* quantized step row. Takes ownership; returns updated buffers. */
|
|
1599
|
-
writeDecodeStep(rows: MlxArray[]): MlxArray[] {
|
|
1600
|
-
const w = (d: MlxArray, srcRow: MlxArray): MlxArray => {
|
|
1601
|
-
const [b, h, , dd] = d.shape as [number, number, number, number];
|
|
1602
|
-
const u = ops.sliceUpdate(d, srcRow, [0, 0, this.ringIdx, 0], [b, h, this.ringIdx + 1, dd]);
|
|
1603
|
-
d.dispose();
|
|
1604
|
-
srcRow.dispose();
|
|
1605
|
-
return u;
|
|
1606
|
-
};
|
|
1607
|
-
this.keys = {
|
|
1608
|
-
packed: w(this.keys!.packed, rows[0]!),
|
|
1609
|
-
scales: w(this.keys!.scales, rows[1]!),
|
|
1610
|
-
biases: w(this.keys!.biases, rows[2]!),
|
|
1611
|
-
};
|
|
1612
|
-
this.values = {
|
|
1613
|
-
packed: w(this.values!.packed, rows[3]!),
|
|
1614
|
-
scales: w(this.values!.scales, rows[4]!),
|
|
1615
|
-
biases: w(this.values!.biases, rows[5]!),
|
|
1616
|
-
};
|
|
1617
|
-
this.offset += 1;
|
|
1618
|
-
this.ringIdx += 1;
|
|
1619
|
-
return this.state();
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
/** Compiled decode, ring fetch: adopt the six in-graph-updated buffers
|
|
1623
|
-
* (closure outputs, in state() order; ownership transfers here). */
|
|
1624
|
-
adoptDecodeStep(bufs: MlxArray[]): MlxArray[] {
|
|
1625
|
-
disposeTriple(this.keys!);
|
|
1626
|
-
disposeTriple(this.values!);
|
|
1627
|
-
this.keys = { packed: bufs[0]!, scales: bufs[1]!, biases: bufs[2]! };
|
|
1628
|
-
this.values = { packed: bufs[3]!, scales: bufs[4]!, biases: bufs[5]! };
|
|
1629
|
-
this.offset += 1;
|
|
1630
|
-
this.ringIdx += 1;
|
|
1631
|
-
return bufs;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
dispose(): void {
|
|
1635
|
-
if (this.keys) disposeTriple(this.keys);
|
|
1636
|
-
if (this.values) disposeTriple(this.values);
|
|
1637
|
-
this.keys = this.values = null;
|
|
1638
|
-
this.offset = 0;
|
|
1639
|
-
this.ringIdx = 0;
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
/** TurboQuant KV cache — v1 (docs/design/turboquant.md): dequantize-
|
|
1644
|
-
* on-fetch. Deliberately does NOT subclass KVCache/RotatingKVCache (or
|
|
1645
|
-
* QuantizedKVCache) — a novel class fails every generated-file
|
|
1646
|
-
* `#matches()` guard and every batching `instanceof` allow-list, so it
|
|
1647
|
-
* gets solo-only routing and monolith fallback for free, the same way
|
|
1648
|
-
* QuantizedKVCache does today. Only full-attention (non-rotating)
|
|
1649
|
-
* layers use this in v1; sliding-window layers stay bf16.
|
|
1650
|
-
*
|
|
1651
|
-
* Storage: 5 arrays per the vllm-metal reference's `turbo_quant_encode`
|
|
1652
|
-
* tuple — kIdx (int8 unpacked at kBits=8, else bit-packed uint8),
|
|
1653
|
-
* kScales/kZeros (fp16, one pair per 32-group), vPacked (bit-packed
|
|
1654
|
-
* uint8, no-op at vBits=8), vScales (fp16, one per 32-group).
|
|
1655
|
-
* updateAndFetch quantizes ONLY the newly appended tokens (unlike
|
|
1656
|
-
* QuantizedKVCache's whole-buffer-toQuantized conversion path — the
|
|
1657
|
-
* streaming append is the whole point of a live cache) and returns the
|
|
1658
|
-
* DEQUANTIZED bf16 active window so ops.sdpa runs unmodified — v1 pays
|
|
1659
|
-
* a full-window dequant every step; the deferred-InvFWHT trick is a
|
|
1660
|
-
* documented non-goal until the quality gate passes. */
|
|
1661
|
-
export class TurboQuantKVCache implements Cache {
|
|
1662
|
-
minimumReusableOffset = 0;
|
|
1663
|
-
readonly stateNeedsDispose = true;
|
|
1664
|
-
static readonly STEP = 256;
|
|
1665
|
-
/** Set only by compiled-decode trace adapters (see Cache) — TurboQuant
|
|
1666
|
-
* is not a compiled-decode participant in v1 (novel class, monolith
|
|
1667
|
-
* fallback only), so this is always unset; captured once per step by
|
|
1668
|
-
* construction, never refreshed mid-step (batched-rotating-quant.ts:55-63). */
|
|
1669
|
-
readonly ropeOffsetArr?: MlxArray;
|
|
1670
|
-
#kv: TurboQuantTensor | null = null;
|
|
1671
|
-
offset = 0;
|
|
1672
|
-
/** Validated lazily on first update, once head_dim is known. */
|
|
1673
|
-
#headDim: number | null = null;
|
|
1674
|
-
/** Experimental operation selection, captured once for this cache. */
|
|
1675
|
-
readonly #codec: KvCodec<TurboQuantTensor>;
|
|
1676
|
-
|
|
1677
|
-
constructor(readonly kBits: number, readonly vBits: number,
|
|
1678
|
-
readonly fusedDecode = turboQuantFusedDecode()) {
|
|
1679
|
-
this.#codec = new TurboQuantCodec(kBits, vBits, fusedDecode);
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
captureDonorRows(): KvDonorRows {
|
|
1683
|
-
if (!this.#kv || this.#headDim === null) throw new Error("cache is empty");
|
|
1684
|
-
const [keys, values] = this.#decode(this.#kv, this.offset, this.#headDim, false);
|
|
1685
|
-
const B = keys.shape[0]!;
|
|
1686
|
-
return { keys, values, offsets: Array(B).fill(this.offset),
|
|
1687
|
-
starts: Array(B).fill(0), ends: Array(B).fill(this.offset) };
|
|
1688
|
-
}
|
|
1689
|
-
captureDonorAttention(): KvDonorAttention { return decodedKvDonorAttention(this.captureDonorRows()); }
|
|
1690
|
-
|
|
1691
|
-
get rotatedValueAttention(): RotatedValueAttentionState { return this; }
|
|
1692
|
-
signature(): string { return `kv:turboquant:${this.kBits}:${this.vBits}`; }
|
|
1693
|
-
|
|
1694
|
-
bytesPerToken(): number {
|
|
1695
|
-
const steps = this.#seqLen();
|
|
1696
|
-
if (steps === 0 || !this.#kv) return 0;
|
|
1697
|
-
return [
|
|
1698
|
-
this.#kv.kIdx, this.#kv.kScales, this.#kv.kZeros,
|
|
1699
|
-
this.#kv.vPacked, this.#kv.vScales,
|
|
1700
|
-
].reduce((bytes, array) => bytes + array.nbytes, 0) / steps;
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
get headDim(): number | null {
|
|
1704
|
-
return this.#headDim;
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
#validateHeadDim(dim: number): void {
|
|
1708
|
-
if (this.#headDim !== null) return;
|
|
1709
|
-
const supported = [64, 128, 256, 512];
|
|
1710
|
-
if (dim % 32 !== 0 || !supported.includes(dim)) {
|
|
1711
|
-
throw new Error(
|
|
1712
|
-
`TurboQuantKVCache: head_dim ${dim} must be divisible by 32 and in {${supported.join(",")}}`,
|
|
1713
|
-
);
|
|
1714
|
-
}
|
|
1715
|
-
this.#headDim = dim;
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
#packedKDim(dim: number): number {
|
|
1719
|
-
return tq.packedDim(dim, this.kBits);
|
|
1720
|
-
}
|
|
1721
|
-
#packedVDim(dim: number): number {
|
|
1722
|
-
return tq.packedDim(dim, this.vBits);
|
|
1723
|
-
}
|
|
1724
|
-
#nGroups(dim: number): number {
|
|
1725
|
-
return dim / tq.TURBOQUANT_BLOCK_SIZE;
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
#alloc(B: number, H: number, T: number, dim: number): TurboQuantTensor {
|
|
1729
|
-
const kIdxDtype = this.kBits === 8 ? Dtype.int8 : Dtype.uint8;
|
|
1730
|
-
return {
|
|
1731
|
-
kIdx: ops.zeros([B, H, T, this.#packedKDim(dim)], kIdxDtype),
|
|
1732
|
-
kScales: ops.zeros([B, H, T, this.#nGroups(dim)], Dtype.float16),
|
|
1733
|
-
kZeros: ops.zeros([B, H, T, this.#nGroups(dim)], Dtype.float16),
|
|
1734
|
-
vPacked: ops.zeros([B, H, T, this.#packedVDim(dim)], Dtype.uint8),
|
|
1735
|
-
vScales: ops.zeros([B, H, T, this.#nGroups(dim)], Dtype.float16),
|
|
1736
|
-
};
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
#seqLen(): number {
|
|
1740
|
-
return this.#kv ? this.#kv.kIdx.shape[2]! : 0;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
#encode(k: MlxArray, v: MlxArray): TurboQuantTensor { return this.#codec.encode(k, v); }
|
|
1744
|
-
#decode(t: TurboQuantTensor, upTo: number, headDim: number, deferV = false): [MlxArray, MlxArray] {
|
|
1745
|
-
return this.#codec.decode(t, upTo, headDim, deferV);
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
/** Quantize the newly-appended k/v, append into growable packed
|
|
1749
|
-
* storage (256-step growth, mirroring KVCache), advance offset, then
|
|
1750
|
-
* dequantize the WHOLE active window (v1 semantics) so the standard
|
|
1751
|
-
* ops.sdpa path runs unmodified. */
|
|
1752
|
-
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
1753
|
-
const D = this.#append(k, v);
|
|
1754
|
-
return this.#decode(this.#kv!, this.offset, D);
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
/** Deferred-inverse-FWHT read path: same append, but the returned V
|
|
1758
|
-
* window stays in the ROTATED domain. The caller MUST un-rotate its
|
|
1759
|
-
* attention output with tq.unrotateValues (see SharedKv.vRotated) —
|
|
1760
|
-
* same decode by linearity, O(q·d log d) per step instead of
|
|
1761
|
-
* O(T·d log d). Opt-in per attention site; sites that don't know about
|
|
1762
|
-
* rotation keep calling updateAndFetch and stay correct. */
|
|
1763
|
-
updateAndFetchDeferredV(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
1764
|
-
const D = this.#append(k, v);
|
|
1765
|
-
return this.#decode(this.#kv!, this.offset, D, true);
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
/** Shared append: grow storage, encode the new tokens, slice-write them
|
|
1769
|
-
* at the previous offset, advance offset. Returns head_dim. */
|
|
1770
|
-
#append(k: MlxArray, v: MlxArray): number {
|
|
1771
|
-
const [B, H, L, D] = k.shape as [number, number, number, number];
|
|
1772
|
-
this.#validateHeadDim(D);
|
|
1773
|
-
const prev = this.offset;
|
|
1774
|
-
|
|
1775
|
-
if (!this.#kv || prev + L > this.#seqLen()) {
|
|
1776
|
-
const nSteps = Math.floor((TurboQuantKVCache.STEP + L - 1) / TurboQuantKVCache.STEP);
|
|
1777
|
-
const newSteps = nSteps * TurboQuantKVCache.STEP;
|
|
1778
|
-
if (this.#kv && prev % TurboQuantKVCache.STEP !== 0) {
|
|
1779
|
-
const trimTo = (t: TurboQuantTensor): TurboQuantTensor => {
|
|
1780
|
-
const cut = (a: MlxArray): MlxArray => {
|
|
1781
|
-
const [b, h, , d] = a.shape as [number, number, number, number];
|
|
1782
|
-
const s = a.slice([0, 0, 0, 0], [b, h, prev, d]);
|
|
1783
|
-
a.dispose();
|
|
1784
|
-
return s;
|
|
1785
|
-
};
|
|
1786
|
-
return { kIdx: cut(t.kIdx), kScales: cut(t.kScales), kZeros: cut(t.kZeros),
|
|
1787
|
-
vPacked: cut(t.vPacked), vScales: cut(t.vScales) };
|
|
1788
|
-
};
|
|
1789
|
-
this.#kv = trimTo(this.#kv);
|
|
1790
|
-
}
|
|
1791
|
-
const grown = this.#alloc(B, H, newSteps, D);
|
|
1792
|
-
if (this.#kv) {
|
|
1793
|
-
const cat = (a: MlxArray, b: MlxArray): MlxArray => {
|
|
1794
|
-
const out = ops.concatAxis([a, b], 2);
|
|
1795
|
-
a.dispose();
|
|
1796
|
-
b.dispose();
|
|
1797
|
-
return out;
|
|
1798
|
-
};
|
|
1799
|
-
this.#kv = {
|
|
1800
|
-
kIdx: cat(this.#kv.kIdx, grown.kIdx),
|
|
1801
|
-
kScales: cat(this.#kv.kScales, grown.kScales),
|
|
1802
|
-
kZeros: cat(this.#kv.kZeros, grown.kZeros),
|
|
1803
|
-
vPacked: cat(this.#kv.vPacked, grown.vPacked),
|
|
1804
|
-
vScales: cat(this.#kv.vScales, grown.vScales),
|
|
1805
|
-
};
|
|
1806
|
-
} else {
|
|
1807
|
-
this.#kv = grown;
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
this.offset += L;
|
|
1812
|
-
const enc = this.#encode(k, v);
|
|
1813
|
-
const write = (dst: MlxArray, src: MlxArray): MlxArray => {
|
|
1814
|
-
const [b, h, , d] = dst.shape as [number, number, number, number];
|
|
1815
|
-
const u = ops.sliceUpdate(dst, src, [0, 0, prev, 0], [b, h, this.offset, d]);
|
|
1816
|
-
dst.dispose();
|
|
1817
|
-
src.dispose();
|
|
1818
|
-
return u;
|
|
1819
|
-
};
|
|
1820
|
-
this.#kv = {
|
|
1821
|
-
kIdx: write(this.#kv!.kIdx, enc.kIdx),
|
|
1822
|
-
kScales: write(this.#kv!.kScales, enc.kScales),
|
|
1823
|
-
kZeros: write(this.#kv!.kZeros, enc.kZeros),
|
|
1824
|
-
vPacked: write(this.#kv!.vPacked, enc.vPacked),
|
|
1825
|
-
vScales: write(this.#kv!.vScales, enc.vScales),
|
|
1826
|
-
};
|
|
1827
|
-
|
|
1828
|
-
return D;
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
/** Same rule as KVCache/QuantizedKVCache.makeMask (mlx-lm cache.py:114-125):
|
|
1832
|
-
* windowless multi-token chunks are the string "causal" at any offset —
|
|
1833
|
-
* TurboQuant v1 is full-attention-only, never windowed. */
|
|
1834
|
-
makeMask(N: number, windowSize: number | null): Mask {
|
|
1835
|
-
if (N === 1) return { mode: "", arr: null };
|
|
1836
|
-
if (windowSize === null) return { mode: "causal", arr: null };
|
|
1837
|
-
if (this.offset === 0 && N <= windowSize) return { mode: "causal", arr: null };
|
|
1838
|
-
return { mode: "array", arr: createCausalMask(N, this.offset, windowSize) };
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
/** Trimmed-to-offset arrays in documented order: kIdx, kScales, kZeros,
|
|
1842
|
-
* vPacked, vScales (kv-store.ts tensor-slot ordering for kind
|
|
1843
|
-
* "turboquant"). Empty cache → []. */
|
|
1844
|
-
state(): MlxArray[] {
|
|
1845
|
-
if (!this.#kv) return [];
|
|
1846
|
-
const cut = (a: MlxArray): MlxArray => {
|
|
1847
|
-
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1848
|
-
return a.slice([0, 0, 0, 0], [B, H, this.offset, D]);
|
|
1849
|
-
};
|
|
1850
|
-
return [cut(this.#kv.kIdx), cut(this.#kv.kScales), cut(this.#kv.kZeros),
|
|
1851
|
-
cut(this.#kv.vPacked), cut(this.#kv.vScales)];
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
isTrimmable(): boolean {
|
|
1855
|
-
return true;
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
/** Drop the last n tokens (byte-safe: groups run along head_dim, so a
|
|
1859
|
-
* token-axis slice never splits a group). Same front/back convention
|
|
1860
|
-
* as KVCache.trim: offset shrinks, storage is overwritten on next write. */
|
|
1861
|
-
trim(n: number): void {
|
|
1862
|
-
this.offset = Math.max(0, this.offset - n);
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
/** Adopt persisted/converted state (takes ownership of the arrays). */
|
|
1866
|
-
restoreState(t: TurboQuantTensor, offset: number, headDim: number): void {
|
|
1867
|
-
this.dispose();
|
|
1868
|
-
this.#kv = t;
|
|
1869
|
-
this.offset = offset;
|
|
1870
|
-
this.#headDim = headDim;
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
/** Convert an existing bf16 KVCache/RotatingKVCache's live window in one
|
|
1874
|
-
* shot: quantize the whole [.., :offset, :] region, preserve offset,
|
|
1875
|
-
* dispose the source's arrays. Mirrors KVCache.toQuantized/
|
|
1876
|
-
* RotatingKVCache.toQuantized's contract (source cache is consumed). */
|
|
1877
|
-
static fromKVCache(cache: KVCache | RotatingKVCache, kBits: number, vBits: number): TurboQuantKVCache {
|
|
1878
|
-
const q = new TurboQuantKVCache(kBits, vBits);
|
|
1879
|
-
const [k, v] = cache.keys && cache.values ? cache.temporalView() : [null, null];
|
|
1880
|
-
if (k && v) {
|
|
1881
|
-
const D = k.shape[3]!;
|
|
1882
|
-
q.#validateHeadDim(D);
|
|
1883
|
-
const enc = q.#encode(k, v);
|
|
1884
|
-
k.dispose();
|
|
1885
|
-
v.dispose();
|
|
1886
|
-
q.#kv = enc;
|
|
1887
|
-
q.#headDim = D;
|
|
1888
|
-
}
|
|
1889
|
-
q.offset = cache.offset;
|
|
1890
|
-
q.minimumReusableOffset = (cache as Cache).minimumReusableOffset ?? 0;
|
|
1891
|
-
cache.dispose();
|
|
1892
|
-
return q;
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
dispose(): void {
|
|
1896
|
-
if (this.#kv) disposeTurboQuant(this.#kv);
|
|
1897
|
-
this.#kv = null;
|
|
1898
|
-
this.offset = 0;
|
|
1899
|
-
this.minimumReusableOffset = 0;
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
export const FINFO_MIN: Partial<Record<Dtype, number>> = {
|
|
1904
|
-
[Dtype.bfloat16]: -3.3895313892515355e38,
|
|
1905
|
-
[Dtype.float16]: -65504,
|
|
1906
|
-
[Dtype.float32]: -3.4028234663852886e38,
|
|
1907
|
-
};
|
|
1908
|
-
|
|
1909
|
-
/** Port of base.py quantized_scaled_dot_product_attention (stock,
|
|
1910
|
-
* non-tiled): scores and output via quantized_matmul against the
|
|
1911
|
-
* quantized KV triples; GQA via a 5-d reshape. Exported for the
|
|
1912
|
-
* fused-vs-unfused parity tests. */
|
|
1913
|
-
export function quantizedSdpaUnfused(
|
|
1914
|
-
q: MlxArray, kq: ops.QuantizedTensor, vq: ops.QuantizedTensor,
|
|
1915
|
-
scale: number, mask: Mask, groupSize: number, bits: number,
|
|
1916
|
-
): MlxArray {
|
|
1917
|
-
const [B, H, L, D] = q.shape as [number, number, number, number];
|
|
1918
|
-
const KV = kq.packed.shape[1]!;
|
|
1919
|
-
const nRep = H / KV;
|
|
1920
|
-
const N = kq.packed.shape[2]!;
|
|
1921
|
-
|
|
1922
|
-
// scale is 1.0 for Gemma4 (q/k are RMS-normed) — skip the identity multiply.
|
|
1923
|
-
let queries = q;
|
|
1924
|
-
const owned: MlxArray[] = [];
|
|
1925
|
-
if (scale !== 1.0) { queries = ops.mulScalar(q, scale); owned.push(queries); }
|
|
1926
|
-
|
|
1927
|
-
let kT = kq;
|
|
1928
|
-
let vT = vq;
|
|
1929
|
-
if (nRep > 1) {
|
|
1930
|
-
const qr = ops.reshape(queries, [B, KV, nRep, L, D]);
|
|
1931
|
-
owned.push(qr);
|
|
1932
|
-
queries = qr;
|
|
1933
|
-
const expand = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
1934
|
-
const e = (a: MlxArray): MlxArray => {
|
|
1935
|
-
// expand_dims(axis=-3) like the reference — view-preserving;
|
|
1936
|
-
// reshape would copy the strided slice and change kernel paths
|
|
1937
|
-
const r = ops.expandDims(a, -3);
|
|
1938
|
-
owned.push(r);
|
|
1939
|
-
return r;
|
|
1940
|
-
};
|
|
1941
|
-
return { packed: e(t.packed), scales: e(t.scales), biases: e(t.biases) };
|
|
1942
|
-
};
|
|
1943
|
-
kT = expand(kq);
|
|
1944
|
-
vT = expand(vq);
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
// M4 Pro's native key matvec retains exact arithmetic when three GQA
|
|
1948
|
-
// heads share a batch. Restore score geometry before softmax/value work.
|
|
1949
|
-
const groupHeads = B <= 2 && H === 24 && KV === 4 && L === 3 && D === 256 &&
|
|
1950
|
-
N >= 8192 && groupSize === 64 && bits === 4 &&
|
|
1951
|
-
(q.dtype === Dtype.bfloat16 || q.dtype === Dtype.float32) &&
|
|
1952
|
-
deviceArchitecture() === "applegpu_g16s";
|
|
1953
|
-
let keyQueries = queries;
|
|
1954
|
-
if (groupHeads) {
|
|
1955
|
-
keyQueries = ops.reshape(queries, [B, KV, 2, 9, D]);
|
|
1956
|
-
owned.push(keyQueries);
|
|
1957
|
-
}
|
|
1958
|
-
let scores = ops.quantizedMatmulQT(keyQueries, kT, true, groupSize, bits);
|
|
1959
|
-
owned.push(scores);
|
|
1960
|
-
if (groupHeads) {
|
|
1961
|
-
scores = ops.reshape(scores, [B, KV, nRep, L, N]);
|
|
1962
|
-
owned.push(scores);
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
let maskArr: MlxArray | null = null;
|
|
1966
|
-
let ownsMask = false;
|
|
1967
|
-
if (mask.mode === "causal") {
|
|
1968
|
-
const qIdx = ops.arange(N - L, N, 1, Dtype.int32);
|
|
1969
|
-
const kIdx = ops.arange(0, N, 1, Dtype.int32);
|
|
1970
|
-
const qCol = ops.reshape(qIdx, [L, 1]);
|
|
1971
|
-
const kRow = ops.reshape(kIdx, [1, N]);
|
|
1972
|
-
maskArr = ops.greaterEqual(qCol, kRow);
|
|
1973
|
-
ownsMask = true;
|
|
1974
|
-
for (const a of [qIdx, kIdx, qCol, kRow]) a.dispose();
|
|
1975
|
-
} else if (mask.mode === "array") {
|
|
1976
|
-
maskArr = mask.arr;
|
|
1977
|
-
// Batched padding masks are [B,1,L,S] (buildBatchedDecodeMask). When GQA
|
|
1978
|
-
// reshaped the scores to 5-D [B,KV,nRep,L,S], trailing-dim broadcasting
|
|
1979
|
-
// would misalign the mask's B with KV — insert the unit axis explicitly:
|
|
1980
|
-
// [B,1,1,L,S] broadcasts correctly over (KV, nRep).
|
|
1981
|
-
if (maskArr && maskArr.ndim === 4 && nRep > 1) {
|
|
1982
|
-
maskArr = ops.expandDims(maskArr, 1);
|
|
1983
|
-
ownsMask = true;
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
if (maskArr) {
|
|
1987
|
-
let masked: MlxArray;
|
|
1988
|
-
if (maskArr.dtype === Dtype.bool) {
|
|
1989
|
-
using ninf = ops.scalarLike(FINFO_MIN[scores.dtype] ?? -3.4e38, scores);
|
|
1990
|
-
masked = ops.where(maskArr, scores, ninf);
|
|
1991
|
-
} else masked = ops.add(scores, maskArr);
|
|
1992
|
-
if (ownsMask) maskArr.dispose();
|
|
1993
|
-
owned.push(masked);
|
|
1994
|
-
scores = masked;
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
const probs = ops.softmaxAxis(scores, -1, true);
|
|
1998
|
-
owned.push(probs);
|
|
1999
|
-
let out = ops.quantizedMatmulQT(probs, vT, false, groupSize, bits);
|
|
2000
|
-
if (nRep > 1) {
|
|
2001
|
-
const r = ops.reshape(out, [B, H, L, D]);
|
|
2002
|
-
out.dispose();
|
|
2003
|
-
out = r;
|
|
2004
|
-
}
|
|
2005
|
-
for (const a of owned) a.dispose();
|
|
2006
|
-
return out;
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
/** Tile size for the fused quantized-SDPA prefill path (oracle default). */
|
|
2010
|
-
export const FUSED_N_CHUNK = 512;
|
|
2011
|
-
|
|
2012
|
-
/** Port of optiq fused_quant_sdpa._prefill_flashattn_n_tiled: a
|
|
2013
|
-
* FlashAttention-2 loop over the KV N axis with mx.quantized_matmul as
|
|
2014
|
-
* the inner kernel. Never materializes the full [..., L, N] scores
|
|
2015
|
-
* matrix — the per-tile transient is bounded by FUSED_N_CHUNK, which is
|
|
2016
|
-
* the whole point (stock u4 KV prefill peaks ABOVE fp16 KV at long
|
|
2017
|
-
* context; see the oracle's module docstring). Op composition order
|
|
2018
|
-
* mirrors the oracle exactly: parity is tier a (bit-exact) vs the python
|
|
2019
|
-
* fused path; vs quantizedSdpaUnfused it is tier b by construction
|
|
2020
|
-
* (online softmax ≠ one-shot precise softmax in bf16). */
|
|
2021
|
-
export function quantizedSdpaTiled(
|
|
2022
|
-
q: MlxArray, kq: ops.QuantizedTensor, vq: ops.QuantizedTensor,
|
|
2023
|
-
scale: number, mask: Mask, groupSize: number, bits: number,
|
|
2024
|
-
): MlxArray {
|
|
2025
|
-
const [B, H, L, D] = q.shape as [number, number, number, number];
|
|
2026
|
-
const KV = kq.packed.shape[1]!;
|
|
2027
|
-
const nRep = H / KV;
|
|
2028
|
-
const N = kq.packed.shape[2]!;
|
|
2029
|
-
|
|
2030
|
-
// scale is 1.0 for Gemma4 (q/k are RMS-normed) — skip the identity multiply.
|
|
2031
|
-
let queries = q;
|
|
2032
|
-
let ownsQueries = false;
|
|
2033
|
-
if (scale !== 1.0) { queries = ops.mulScalar(q, scale); ownsQueries = true; }
|
|
2034
|
-
|
|
2035
|
-
let kT = kq;
|
|
2036
|
-
let vT = vq;
|
|
2037
|
-
const expanded: MlxArray[] = [];
|
|
2038
|
-
if (nRep > 1) {
|
|
2039
|
-
const qr = ops.reshape(queries, [B, KV, nRep, L, D]);
|
|
2040
|
-
if (ownsQueries) queries.dispose();
|
|
2041
|
-
queries = qr;
|
|
2042
|
-
ownsQueries = true;
|
|
2043
|
-
const expand = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
2044
|
-
const e = (a: MlxArray): MlxArray => {
|
|
2045
|
-
const r = ops.expandDims(a, -3);
|
|
2046
|
-
expanded.push(r);
|
|
2047
|
-
return r;
|
|
2048
|
-
};
|
|
2049
|
-
return { packed: e(t.packed), scales: e(t.scales), biases: e(t.biases) };
|
|
2050
|
-
};
|
|
2051
|
-
kT = expand(kq);
|
|
2052
|
-
vT = expand(vq);
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
// The oracle builds a bottom-right-aligned [L, N] bool causal matrix
|
|
2056
|
-
// and slices columns per tile; our "array" masks (createCausalMask at
|
|
2057
|
-
// offset > 0) are that same matrix already materialized — both slice
|
|
2058
|
-
// identically below.
|
|
2059
|
-
let maskArr: MlxArray | null = null;
|
|
2060
|
-
let ownsMask = false;
|
|
2061
|
-
if (mask.mode === "causal") {
|
|
2062
|
-
const qIdx = ops.arange(N - L, N, 1, Dtype.int32);
|
|
2063
|
-
const kIdx = ops.arange(0, N, 1, Dtype.int32);
|
|
2064
|
-
const qCol = ops.reshape(qIdx, [L, 1]);
|
|
2065
|
-
const kRow = ops.reshape(kIdx, [1, N]);
|
|
2066
|
-
maskArr = ops.greaterEqual(qCol, kRow);
|
|
2067
|
-
ownsMask = true;
|
|
2068
|
-
for (const a of [qIdx, kIdx, qCol, kRow]) a.dispose();
|
|
2069
|
-
} else if (mask.mode === "array") {
|
|
2070
|
-
maskArr = mask.arr;
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
// Slice [..., n0:n1, :] (KV triples) / [..., n0:n1] (mask chunks).
|
|
2074
|
-
// Under a compiled-decode trace, subrange Slice swaps to DynamicSlice
|
|
2075
|
-
// (identical values; shapeless compile rejects Slice — same pattern as
|
|
2076
|
-
// the per-layer-input and MoE top-k slices).
|
|
2077
|
-
const sliceAxis = (a: MlxArray, axisFromEnd: 1 | 2, n0: number, n1: number): MlxArray => {
|
|
2078
|
-
const dims = a.shape;
|
|
2079
|
-
const axis = dims.length - axisFromEnd;
|
|
2080
|
-
if (isCompiledTrace()) {
|
|
2081
|
-
const start = ops.fromInt32([n0], [1]);
|
|
2082
|
-
const size = [...dims];
|
|
2083
|
-
size[axis] = n1 - n0;
|
|
2084
|
-
const out = ops.sliceDynamic(a, start, [axis], size);
|
|
2085
|
-
start.dispose();
|
|
2086
|
-
return out;
|
|
2087
|
-
}
|
|
2088
|
-
const start = dims.map(() => 0);
|
|
2089
|
-
const stop = [...dims];
|
|
2090
|
-
start[axis] = n0;
|
|
2091
|
-
stop[axis] = n1;
|
|
2092
|
-
return a.slice(start, stop);
|
|
2093
|
-
};
|
|
2094
|
-
|
|
2095
|
-
let oAcc: MlxArray | null = null;
|
|
2096
|
-
let rowMax: MlxArray | null = null;
|
|
2097
|
-
let rowSum: MlxArray | null = null;
|
|
2098
|
-
|
|
2099
|
-
for (let n0 = 0; n0 < N; n0 += FUSED_N_CHUNK) {
|
|
2100
|
-
const n1 = Math.min(n0 + FUSED_N_CHUNK, N);
|
|
2101
|
-
const kChunk: ops.QuantizedTensor = {
|
|
2102
|
-
packed: sliceAxis(kT.packed, 2, n0, n1),
|
|
2103
|
-
scales: sliceAxis(kT.scales, 2, n0, n1),
|
|
2104
|
-
biases: sliceAxis(kT.biases, 2, n0, n1),
|
|
2105
|
-
};
|
|
2106
|
-
const vChunk: ops.QuantizedTensor = {
|
|
2107
|
-
packed: sliceAxis(vT.packed, 2, n0, n1),
|
|
2108
|
-
scales: sliceAxis(vT.scales, 2, n0, n1),
|
|
2109
|
-
biases: sliceAxis(vT.biases, 2, n0, n1),
|
|
2110
|
-
};
|
|
2111
|
-
|
|
2112
|
-
let scores = ops.quantizedMatmulQT(queries, kChunk, true, groupSize, bits);
|
|
2113
|
-
for (const a of [kChunk.packed, kChunk.scales, kChunk.biases]) a.dispose();
|
|
2114
|
-
|
|
2115
|
-
if (maskArr) {
|
|
2116
|
-
const maskChunk = sliceAxis(maskArr, 1, n0, n1);
|
|
2117
|
-
let masked: MlxArray;
|
|
2118
|
-
if (maskChunk.dtype === Dtype.bool) {
|
|
2119
|
-
const ninf = ops.scalarLike(FINFO_MIN[scores.dtype] ?? -3.4e38, scores);
|
|
2120
|
-
masked = ops.where(maskChunk, scores, ninf);
|
|
2121
|
-
ninf.dispose();
|
|
2122
|
-
} else {
|
|
2123
|
-
masked = ops.add(scores, maskChunk);
|
|
2124
|
-
}
|
|
2125
|
-
maskChunk.dispose();
|
|
2126
|
-
scores.dispose();
|
|
2127
|
-
scores = masked;
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
const chunkMax = ops.maxAxis(scores, -1, true);
|
|
2131
|
-
if (oAcc === null) {
|
|
2132
|
-
rowMax = chunkMax;
|
|
2133
|
-
const shifted = ops.sub(scores, rowMax);
|
|
2134
|
-
const exps = ops.exp(shifted);
|
|
2135
|
-
shifted.dispose();
|
|
2136
|
-
rowSum = ops.sumAxis(exps, -1, true);
|
|
2137
|
-
oAcc = ops.quantizedMatmulQT(exps, vChunk, false, groupSize, bits);
|
|
2138
|
-
exps.dispose();
|
|
2139
|
-
} else {
|
|
2140
|
-
const newMax = ops.maximum(rowMax!, chunkMax);
|
|
2141
|
-
chunkMax.dispose();
|
|
2142
|
-
const maxDiff = ops.sub(rowMax!, newMax);
|
|
2143
|
-
const factor = ops.exp(maxDiff);
|
|
2144
|
-
maxDiff.dispose();
|
|
2145
|
-
const shifted = ops.sub(scores, newMax);
|
|
2146
|
-
const exps = ops.exp(shifted);
|
|
2147
|
-
shifted.dispose();
|
|
2148
|
-
// new_sum = factor * row_sum + sum(exps) (association order kept)
|
|
2149
|
-
const carried = ops.mul(factor, rowSum!);
|
|
2150
|
-
const sumExps = ops.sumAxis(exps, -1, true);
|
|
2151
|
-
const newSum = ops.add(carried, sumExps);
|
|
2152
|
-
carried.dispose();
|
|
2153
|
-
sumExps.dispose();
|
|
2154
|
-
const deltaOut = ops.quantizedMatmulQT(exps, vChunk, false, groupSize, bits);
|
|
2155
|
-
exps.dispose();
|
|
2156
|
-
// o_acc = o_acc * factor + delta_out
|
|
2157
|
-
const scaledAcc = ops.mul(oAcc!, factor);
|
|
2158
|
-
factor.dispose();
|
|
2159
|
-
const nextAcc = ops.add(scaledAcc, deltaOut);
|
|
2160
|
-
scaledAcc.dispose();
|
|
2161
|
-
deltaOut.dispose();
|
|
2162
|
-
oAcc!.dispose();
|
|
2163
|
-
oAcc = nextAcc;
|
|
2164
|
-
rowMax!.dispose();
|
|
2165
|
-
rowMax = newMax;
|
|
2166
|
-
rowSum!.dispose();
|
|
2167
|
-
rowSum = newSum;
|
|
2168
|
-
}
|
|
2169
|
-
scores.dispose();
|
|
2170
|
-
for (const a of [vChunk.packed, vChunk.scales, vChunk.biases]) a.dispose();
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
let out = ops.div(oAcc!, rowSum!);
|
|
2174
|
-
oAcc!.dispose();
|
|
2175
|
-
rowMax!.dispose();
|
|
2176
|
-
rowSum!.dispose();
|
|
2177
|
-
if (ownsMask) maskArr!.dispose();
|
|
2178
|
-
for (const a of expanded) a.dispose();
|
|
2179
|
-
if (ownsQueries) queries.dispose();
|
|
2180
|
-
if (nRep > 1) {
|
|
2181
|
-
const r = ops.reshape(out, [B, H, L, D]);
|
|
2182
|
-
out.dispose();
|
|
2183
|
-
out = r;
|
|
2184
|
-
}
|
|
2185
|
-
return out;
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
/** Gate for the tiled path — port of fused_quant_sdpa._supported plus the
|
|
2189
|
-
* wrapper's mask check, with one documented deviation: the oracle wrapper
|
|
2190
|
-
* falls back to unfused on ARRAY masks because mlx-lm always hands it the
|
|
2191
|
-
* "causal" string for windowless continuations (its make_mask returns
|
|
2192
|
-
* "causal" even at offset > 0); our makeMask materializes the equivalent
|
|
2193
|
-
* bool matrix — so masks flagged causalEquivalent tile too (the oracle's
|
|
2194
|
-
* INNER function handles them with the same column slicing we use).
|
|
2195
|
-
* Window/bidir array masks do NOT tile, matching the reference's
|
|
2196
|
-
* scenario-level dispatch exactly (Phase 9: sliding-layer quantized
|
|
2197
|
-
* prefill is unfused in optiq too). */
|
|
2198
|
-
function fusedSdpaSupported(q: MlxArray, mask: Mask, groupSize: number, bits: number): boolean {
|
|
2199
|
-
// Escape hatch mirroring optiq serve's --no-fused-kv: forces the
|
|
2200
|
-
// stock unfused path everywhere. Also the A/B lever for
|
|
2201
|
-
// scripts/bench-levers.ts fused-prefill. Read per call (cheap next to the
|
|
2202
|
-
// FFI work) so tests and paired A/B harnesses can flip it in-process.
|
|
2203
|
-
if (runtimeValue("MLX_BUN_NO_FUSED_SDPA") === "1") return false;
|
|
2204
|
-
if (bits !== 4 && bits !== 8) return false;
|
|
2205
|
-
if (groupSize !== 32 && groupSize !== 64 && groupSize !== 128) return false;
|
|
2206
|
-
if (q.dtype !== Dtype.bfloat16 && q.dtype !== Dtype.float16) return false;
|
|
2207
|
-
if (mask.mode === "causal" || mask.mode === "") return true; // "" = oracle's mask=None
|
|
2208
|
-
if (mask.mode === "array")
|
|
2209
|
-
return mask.causalEquivalent === true && mask.arr !== null &&
|
|
2210
|
-
mask.arr.shape.length === 2 && mask.arr.dtype === Dtype.bool;
|
|
2211
|
-
return false;
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
/** Quantized-cache SDPA dispatch: L > 1 (prefill/continuation) with a
|
|
2215
|
-
* supported config goes through the N-tiled fused path; decode (L = 1)
|
|
2216
|
-
* and unsupported configs stay on the stock unfused port. Exported for
|
|
2217
|
-
* the dispatch-gate tests. */
|
|
2218
|
-
export function quantizedSdpa(
|
|
2219
|
-
q: MlxArray, kq: ops.QuantizedTensor, vq: ops.QuantizedTensor,
|
|
2220
|
-
scale: number, mask: Mask, groupSize: number, bits: number,
|
|
2221
|
-
): MlxArray {
|
|
2222
|
-
const tile = q.shape[2]! > 1;
|
|
2223
|
-
if (tile && fusedSdpaSupported(q, mask, groupSize, bits)) {
|
|
2224
|
-
// The N-tiled loop CANNOT be traced: its tile count and slice extents
|
|
2225
|
-
// bake at trace-time N, so under shapeless replay a growing (concat-
|
|
2226
|
-
// phase) quantized cache's newest rows are silently never attended.
|
|
2227
|
-
// This throw is the backstop for any compiled caller — it lands in the
|
|
2228
|
-
// trace's transactional fallback instead of producing silently wrong
|
|
2229
|
-
// attention.
|
|
2230
|
-
if (isCompiledTrace())
|
|
2231
|
-
throw new Error(
|
|
2232
|
-
"quantizedSdpaTiled inside a compiled trace: the tile loop bakes trace-time N " +
|
|
2233
|
-
"(growing KV rows would be silently dropped) — run this generation uncompiled",
|
|
2234
|
-
);
|
|
2235
|
-
return quantizedSdpaTiled(q, kq, vq, scale, mask, groupSize, bits);
|
|
2236
|
-
}
|
|
2237
|
-
return quantizedSdpaUnfused(q, kq, vq, scale, mask, groupSize, bits);
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
/** The runtime-only half of fusedSdpaSupported (env flag, dtype, mask
|
|
2241
|
-
* kind) — generated models (Phase D) bake the (bits, group_size) half
|
|
2242
|
-
* as a compile-time constant and call this for the rest. The combined
|
|
2243
|
-
* predicate is exactly fusedSdpaSupported. */
|
|
2244
|
-
export function fusedSdpaRuntimeOk(q: MlxArray, mask: Mask): boolean {
|
|
2245
|
-
if (runtimeValue("MLX_BUN_NO_FUSED_SDPA") === "1") return false;
|
|
2246
|
-
if (q.dtype !== Dtype.bfloat16 && q.dtype !== Dtype.float16) return false;
|
|
2247
|
-
if (mask.mode === "causal" || mask.mode === "") return true;
|
|
2248
|
-
if (mask.mode === "array")
|
|
2249
|
-
return mask.causalEquivalent === true && mask.arr !== null &&
|
|
2250
|
-
mask.arr.shape.length === 2 && mask.arr.dtype === Dtype.bool;
|
|
2251
|
-
return false;
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
/** Port of base.py create_causal_mask (bool, [N, offset+N]). */
|
|
2255
|
-
export function createCausalMask(N: number, offset: number, windowSize: number | null): MlxArray {
|
|
2256
|
-
const rinds = ops.arange(0, offset + N, 1, Dtype.int32);
|
|
2257
|
-
const lindsFlat = offset ? ops.arange(offset, offset + N, 1, Dtype.int32) : rinds;
|
|
2258
|
-
const linds = ops.reshape(lindsFlat, [N, 1]);
|
|
2259
|
-
const rindsB = ops.reshape(rinds, [1, offset + N]);
|
|
2260
|
-
let mask = ops.greaterEqual(linds, rindsB);
|
|
2261
|
-
if (windowSize !== null) {
|
|
2262
|
-
const w = ops.fromInt32([windowSize], []);
|
|
2263
|
-
const rPlusW = ops.add(rindsB, w);
|
|
2264
|
-
const inWindow = ops.less(linds, rPlusW);
|
|
2265
|
-
const combined = ops.logicalAnd(mask, inWindow);
|
|
2266
|
-
for (const a of [w, rPlusW, inWindow, mask]) a.dispose();
|
|
2267
|
-
mask = combined;
|
|
2268
|
-
}
|
|
2269
|
-
if (lindsFlat !== rinds) lindsFlat.dispose();
|
|
2270
|
-
rinds.dispose();
|
|
2271
|
-
linds.dispose();
|
|
2272
|
-
rindsB.dispose();
|
|
2273
|
-
return mask;
|
|
2274
|
-
}
|
|
2275
|
-
/** Port of switch_layers.QuantizedSwitchLinear (gather_qmm over stacked
|
|
2276
|
-
* expert weights; rhs_indices selects the expert per row). */
|
|
2277
|
-
export class QuantizedSwitchLinear {
|
|
2278
|
-
constructor(
|
|
2279
|
-
readonly w: MlxArray,
|
|
2280
|
-
readonly scales: MlxArray,
|
|
2281
|
-
readonly biases: MlxArray | null,
|
|
2282
|
-
readonly spec: ops.QuantSpec,
|
|
2283
|
-
) {}
|
|
2284
|
-
|
|
2285
|
-
static load(weights: Weights, path: string, config: ModelConfig): QuantizedSwitchLinear {
|
|
2286
|
-
if (!weights.has(`${path}.scales`))
|
|
2287
|
-
throw new Error(`${path}: expected quantized switch linear (no .scales tensor)`);
|
|
2288
|
-
const spec = quantFor(config.quantization, path);
|
|
2289
|
-
if (!spec) throw new Error(`${path}: no quant spec`);
|
|
2290
|
-
// Expert WEIGHT (the ~94% of expert bytes) comes from the page-aligned
|
|
2291
|
-
// offload mmap when --expert-offload is active (else resident); scales/
|
|
2292
|
-
// biases stay resident (small). Same bytes either way → bit-exact.
|
|
2293
|
-
const wName = `${path}.weight`;
|
|
2294
|
-
return new QuantizedSwitchLinear(
|
|
2295
|
-
expertOffloadArray(wName) ?? weights.tensor(wName),
|
|
2296
|
-
weights.tensor(`${path}.scales`),
|
|
2297
|
-
weights.has(`${path}.biases`) ? weights.tensor(`${path}.biases`) : null,
|
|
2298
|
-
spec,
|
|
2299
|
-
);
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
forward(x: MlxArray, indices: MlxArray, sortedIndices: boolean): MlxArray {
|
|
2303
|
-
return ops.gatherQmm(x, this.w, this.scales, this.biases, indices, this.spec, sortedIndices);
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
/** Dispose `old` and return `next` — for h = disposing(h, op(h)) chains. */
|
|
2307
|
-
export function disposing(old: MlxArray, next: MlxArray): MlxArray {
|
|
2308
|
-
old.dispose();
|
|
2309
|
-
return next;
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
// Compiled-decode trace mode (set by compiled-decode.ts around its trace,
|
|
2313
|
-
// which runs synchronously on the serialized generation queue — same
|
|
2314
|
-
// safety argument as LoraState). Inside a shapeless-compiled graph, mlx's
|
|
2315
|
-
// Slice primitive cannot re-infer output shapes, so the two subrange
|
|
2316
|
-
// slices on the decode path (per-layer-input split, MoE top-k) swap to
|
|
2317
|
-
// DynamicSlice — identical values, shapeless-safe — ONLY while tracing.
|
|
2318
|
-
// The uncompiled path keeps the exact oracle op sequence.
|
|
2319
|
-
let compiledTrace = false;
|
|
2320
|
-
export function setCompiledTrace(v: boolean): void {
|
|
2321
|
-
compiledTrace = v;
|
|
2322
|
-
}
|
|
2323
|
-
export function isCompiledTrace(): boolean {
|
|
2324
|
-
return compiledTrace;
|
|
2325
|
-
}
|
|
2326
|
-
/** Causal(+window) mask OR'd with image×image bidirectional attention. */
|
|
2327
|
-
export function bidirMask(L: number, windowSize: number | null, bidir: MlxArray): Mask {
|
|
2328
|
-
const causal = createCausalMask(L, 0, windowSize);
|
|
2329
|
-
const col = ops.reshape(bidir, [L, 1]);
|
|
2330
|
-
const row = ops.reshape(bidir, [1, L]);
|
|
2331
|
-
const outer = ops.logicalAnd(col, row);
|
|
2332
|
-
col.dispose();
|
|
2333
|
-
row.dispose();
|
|
2334
|
-
const allow = ops.logicalOr(causal, outer);
|
|
2335
|
-
causal.dispose();
|
|
2336
|
-
outer.dispose();
|
|
2337
|
-
return { mode: "array", arr: allow };
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
/** tanh(x / cap) * cap with weak-scalar semantics. */
|
|
2341
|
-
export function logitSoftcap(logits: MlxArray, cap: number): MlxArray {
|
|
2342
|
-
const capArr = ops.scalarLike(cap, logits);
|
|
2343
|
-
const scaled = ops.div(logits, capArr);
|
|
2344
|
-
const t = ops.tanh(scaled);
|
|
2345
|
-
scaled.dispose();
|
|
2346
|
-
const out = ops.mul(t, capArr);
|
|
2347
|
-
t.dispose();
|
|
2348
|
-
capArr.dispose();
|
|
2349
|
-
return out;
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
/** argmax over vocab at the last sequence position of [1, L, V]. */
|
|
2353
|
-
export function argmaxLastPosition(logits: MlxArray): number {
|
|
2354
|
-
const [, L, V] = logits.shape as [number, number, number];
|
|
2355
|
-
const last = logits.slice([0, L - 1, 0], [1, L, V]);
|
|
2356
|
-
const am = ops.argmaxAxis(last, -1);
|
|
2357
|
-
const id = ops.itemUint32(am);
|
|
2358
|
-
last.dispose();
|
|
2359
|
-
am.dispose();
|
|
2360
|
-
return id;
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
/** Last-position logits as f32 (for the parity harness). */
|
|
2364
|
-
export function lastPositionLogits(logits: MlxArray): Float32Array {
|
|
2365
|
-
const [, L, V] = logits.shape as [number, number, number];
|
|
2366
|
-
const last = logits.slice([0, L - 1, 0], [1, L, V]);
|
|
2367
|
-
const f32 = last.toFloat32();
|
|
2368
|
-
last.dispose();
|
|
2369
|
-
return f32;
|
|
2370
|
-
}
|