@mlx-bun/inference 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1475 -2
- package/THIRD_PARTY_NOTICES.md +3404 -0
- package/dist/native/libmlx_bun_ane.dylib +0 -0
- package/dist/native/libmlx_bun_expert_io.dylib +0 -0
- package/dist/native/mlx-bun-frame-extract +0 -0
- package/examples/fit-model.ts +25 -0
- package/examples/qwen3-forward.ts +42 -0
- package/examples/qwen3-generate.ts +28 -0
- package/examples/trellis-expand.ts +23 -0
- package/package.json +129 -4
- package/src/adapters/available.ts +110 -0
- package/src/adapters/index.ts +3 -0
- package/src/adapters/manager.ts +316 -0
- package/src/artifacts/auxiliary-files.ts +45 -0
- package/src/artifacts/config-dialects.ts +94 -0
- package/src/artifacts/config.ts +341 -0
- package/src/artifacts/draft-vocabulary.ts +30 -0
- package/src/artifacts/expert-offload-build.ts +75 -0
- package/src/artifacts/expert-offload.ts +89 -0
- package/src/artifacts/experts/index.ts +3 -0
- package/src/artifacts/experts/io.ts +386 -0
- package/src/artifacts/experts/residency.ts +1134 -0
- package/src/artifacts/experts/usage.ts +415 -0
- package/src/artifacts/fingerprint.ts +44 -0
- package/src/artifacts/glm52/container.ts +305 -0
- package/src/artifacts/glm52/expert-layout.ts +249 -0
- package/src/artifacts/glm52/index.ts +7 -0
- package/src/artifacts/glm52/kernel-layout.ts +124 -0
- package/src/artifacts/glm52/layout.ts +152 -0
- package/src/artifacts/glm52/memory.ts +500 -0
- package/src/artifacts/glm52/resident-weights.ts +135 -0
- package/src/artifacts/glm52/weights.ts +503 -0
- package/src/artifacts/glm52-config.ts +262 -0
- package/src/artifacts/identity.ts +94 -0
- package/src/artifacts/index.ts +15 -0
- package/src/artifacts/mmap.ts +90 -0
- package/src/artifacts/safetensors-writer.ts +249 -0
- package/src/artifacts/safetensors.ts +164 -0
- package/src/artifacts/turboquant-fold.ts +42 -0
- package/src/artifacts/weight-audit.ts +43 -0
- package/src/artifacts/weights.ts +255 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/mlx/attention.ts +8 -0
- package/src/contracts/mlx/cache.ts +510 -0
- package/src/contracts/mlx/checkpoint.ts +10 -0
- package/src/contracts/mlx/draft-checkpoint.ts +14 -0
- package/src/contracts/mlx/draft-projection.ts +27 -0
- package/src/contracts/mlx/draft-target.ts +84 -0
- package/src/contracts/mlx/drafter.ts +176 -0
- package/src/contracts/mlx/forward-work.ts +23 -0
- package/src/contracts/mlx/glm52-weights.ts +25 -0
- package/src/contracts/mlx/graph.ts +309 -0
- package/src/contracts/mlx/head.ts +6 -0
- package/src/contracts/mlx/index.ts +15 -0
- package/src/contracts/mlx/kv-codec.ts +9 -0
- package/src/contracts/mlx/media.ts +113 -0
- package/src/contracts/mlx/positions.ts +16 -0
- package/src/contracts/mlx/row-table.ts +2 -0
- package/src/contracts/mlx/trainable.ts +103 -0
- package/src/contracts/mlx/whisper.ts +96 -0
- package/src/contracts/portable/composition.ts +106 -0
- package/src/contracts/portable/denoising.ts +17 -0
- package/src/contracts/portable/execution.ts +79 -0
- package/src/contracts/portable/generation.ts +103 -0
- package/src/contracts/portable/graph.ts +80 -0
- package/src/contracts/portable/index.ts +15 -0
- package/src/contracts/portable/memory-plan.ts +26 -0
- package/src/contracts/portable/object-cache.ts +8 -0
- package/src/contracts/portable/prefill.ts +63 -0
- package/src/contracts/portable/prefix-cache.ts +33 -0
- package/src/contracts/portable/prompt.ts +2 -0
- package/src/contracts/portable/resources.ts +25 -0
- package/src/contracts/portable/rollback.ts +7 -0
- package/src/contracts/portable/sampling.ts +44 -0
- package/src/contracts/portable/scheduling.ts +54 -0
- package/src/contracts/portable/token-delivery.ts +4 -0
- package/src/contracts/portable/tsconfig.json +14 -0
- package/src/embeddings/index.ts +1 -0
- package/src/embeddings/text.ts +85 -0
- package/src/execution/admission.ts +59 -0
- package/src/execution/batch-group.ts +1240 -0
- package/src/execution/batch-types.ts +225 -0
- package/src/execution/cancellation.ts +38 -0
- package/src/execution/continuation-request.ts +45 -0
- package/src/execution/continuation-types.ts +10 -0
- package/src/execution/coordinator.ts +70 -0
- package/src/execution/denoising-group.ts +180 -0
- package/src/execution/engine.ts +56 -0
- package/src/execution/fill-group.ts +505 -0
- package/src/execution/fit.ts +184 -0
- package/src/execution/gateway-binding.ts +178 -0
- package/src/execution/grammar-group.ts +224 -0
- package/src/execution/index.ts +18 -0
- package/src/execution/methods.ts +75 -0
- package/src/execution/mixed-iteration.ts +96 -0
- package/src/execution/plan.ts +65 -0
- package/src/execution/prefill-cohort.ts +100 -0
- package/src/execution/prefill-rows.ts +263 -0
- package/src/execution/request-policy.ts +19 -0
- package/src/execution/row-sampling.ts +49 -0
- package/src/execution/scheduler.ts +92 -0
- package/src/execution/session.ts +247 -0
- package/src/execution/speculative-group.ts +797 -0
- package/src/execution/tasks.ts +44 -0
- package/src/generation/autoregressive.ts +840 -0
- package/src/generation/bindings/autoregressive.ts +104 -0
- package/src/generation/bindings/denoising.ts +60 -0
- package/src/generation/bindings/prefill-policy.ts +36 -0
- package/src/generation/checkpoint-identity.ts +60 -0
- package/src/generation/denoising.ts +85 -0
- package/src/generation/diffusion.ts +583 -0
- package/src/generation/fill/echo-index.ts +188 -0
- package/src/generation/fill/index.ts +3 -0
- package/src/generation/fill/proposal.ts +69 -0
- package/src/generation/fill/schema-rows.ts +506 -0
- package/src/generation/fill/session.ts +464 -0
- package/src/generation/fill/tool-boundary.ts +103 -0
- package/src/generation/fill-append.ts +58 -0
- package/src/generation/generate.ts +44 -0
- package/src/generation/grammar-step.ts +39 -0
- package/src/generation/index.ts +14 -0
- package/src/generation/phase-dispatch.ts +115 -0
- package/src/generation/prefill.ts +33 -0
- package/src/generation/result.ts +49 -0
- package/src/generation/scopes.ts +157 -0
- package/src/generation/speculative/acceptance.ts +41 -0
- package/src/generation/speculative/adaptive-draft-gate.ts +166 -0
- package/src/generation/speculative/bindings/assistant-rows.ts +102 -0
- package/src/generation/speculative/bindings/autoregressive-rows.ts +124 -0
- package/src/generation/speculative/bindings/binding.ts +106 -0
- package/src/generation/speculative/bindings/deepspec-rows.ts +33 -0
- package/src/generation/speculative/bindings/draft-target.ts +10 -0
- package/src/generation/speculative/bindings/glm52-mtp-rows.ts +128 -0
- package/src/generation/speculative/bindings/projected-context-rows.ts +78 -0
- package/src/generation/speculative/bindings/projected-draft-rows.ts +95 -0
- package/src/generation/speculative/bindings/qwen-mtp-rows.ts +336 -0
- package/src/generation/speculative/bindings/two-model-rows.ts +63 -0
- package/src/generation/speculative/cache-identity.ts +16 -0
- package/src/generation/speculative/draft-kind.ts +149 -0
- package/src/generation/speculative/draft-policy.ts +41 -0
- package/src/generation/speculative/draft-registry.ts +118 -0
- package/src/generation/speculative/draft-vocab.ts +110 -0
- package/src/generation/speculative/index.ts +17 -0
- package/src/generation/speculative/output.ts +49 -0
- package/src/generation/speculative/round.ts +307 -0
- package/src/generation/speculative/source.ts +144 -0
- package/src/generation/speculative/sources/assistant-source.ts +64 -0
- package/src/generation/speculative/sources/deepspec-source.ts +70 -0
- package/src/generation/speculative/sources/dflash-source.ts +84 -0
- package/src/generation/speculative/sources/dflash2-source.ts +86 -0
- package/src/generation/speculative/sources/glm52-mtp-source.ts +66 -0
- package/src/generation/speculative/sources/ngram-source.ts +165 -0
- package/src/generation/speculative/sources/qwen-mtp-source.ts +154 -0
- package/src/generation/speculative/sources/two-model.ts +75 -0
- package/src/generation/types.ts +194 -0
- package/src/index.ts +20 -0
- package/src/input/audio/audiotoolbox.ts +112 -0
- package/src/input/audio/decode.ts +133 -0
- package/src/input/audio/features.ts +202 -0
- package/src/input/audio/index.ts +6 -0
- package/src/input/audio/transcode.ts +67 -0
- package/src/input/audio/whisper-mel.ts +185 -0
- package/src/input/audio/whisper-tokenizer.ts +238 -0
- package/src/input/chat-template.ts +389 -0
- package/src/input/index.ts +6 -0
- package/src/input/media-fetch.ts +439 -0
- package/src/input/packed-mixed.ts +43 -0
- package/src/input/special-tokens.ts +75 -0
- package/src/input/token-groups.ts +49 -0
- package/src/input/tokenizer.ts +336 -0
- package/src/input/tool-call.ts +689 -0
- package/src/input/vision/index.ts +3 -0
- package/src/input/vision/preprocess.ts +247 -0
- package/src/input/vision/prompt.ts +398 -0
- package/src/input/vision/qwen3vl-preprocess.ts +409 -0
- package/src/input/vision/qwen3vl-prompt.ts +237 -0
- package/src/input/vision/video-frames.ts +100 -0
- package/src/kernels/ane/fill.ts +101 -0
- package/src/kernels/ane/linear.ts +416 -0
- package/src/kernels/attention/flash.ts +694 -0
- package/src/kernels/attention/kvq4-decode.ts +155 -0
- package/src/kernels/attention/masks.ts +40 -0
- package/src/kernels/attention/multi-query.ts +193 -0
- package/src/kernels/attention/paged.ts +130 -0
- package/src/kernels/delta/gated.ts +306 -0
- package/src/kernels/delta/index.ts +3 -0
- package/src/kernels/delta/silu-rms-heads.ts +69 -0
- package/src/kernels/delta/state.ts +74 -0
- package/src/kernels/gated-rms-norm.ts +64 -0
- package/src/kernels/glm52/layout.ts +37 -0
- package/src/kernels/glm52/streamed-experts.ts +360 -0
- package/src/kernels/logits.ts +38 -0
- package/src/kernels/quantization/affine-row.ts +154 -0
- package/src/kernels/quantization/affine-rows.ts +36 -0
- package/src/kernels/quantization/affine-split-mma.ts +144 -0
- package/src/kernels/quantization/affine3-mma.ts +117 -0
- package/src/kernels/quantization/affine3-rows.ts +103 -0
- package/src/kernels/quantization/affine4-mma.ts +113 -0
- package/src/kernels/quantization/affine4-rows.ts +102 -0
- package/src/kernels/residual-rms-norm.ts +65 -0
- package/src/kernels/sampling/index.ts +2 -0
- package/src/kernels/sampling/normalized-argmax.ts +116 -0
- package/src/kernels/sampling/token-bitmask.ts +52 -0
- package/src/kernels/softcap.ts +35 -0
- package/src/kernels/trellis/codebook.ts +123 -0
- package/src/kernels/trellis/down-codebook.ts +184 -0
- package/src/kernels/trellis/down-factored.ts +180 -0
- package/src/kernels/trellis/down-k3-interleaved-factored.ts +146 -0
- package/src/kernels/trellis/down-mma-wide.ts +195 -0
- package/src/kernels/trellis/down-mma.ts +145 -0
- package/src/kernels/trellis/expand.ts +42 -0
- package/src/kernels/trellis/gate-up-codebook-mma.ts +141 -0
- package/src/kernels/trellis/gate-up-codebook.ts +137 -0
- package/src/kernels/trellis/gate-up-factored.ts +197 -0
- package/src/kernels/trellis/gate-up-mma-wide.ts +147 -0
- package/src/kernels/trellis/gate-up-mma.ts +148 -0
- package/src/kernels/trellis/gate-up.ts +248 -0
- package/src/kernels/trellis/geometry.ts +22 -0
- package/src/kernels/trellis/index.ts +103 -0
- package/src/kernels/trellis/launch.ts +12 -0
- package/src/kernels/trellis/mixed-gate-up-factored.ts +119 -0
- package/src/kernels/trellis/mixed-gate-up.ts +146 -0
- package/src/kernels/trellis/qualified.ts +34 -0
- package/src/kernels/trellis/reduce.ts +150 -0
- package/src/kernels/trellis/scatter.ts +407 -0
- package/src/kernels/trellis/splitk-prefill.ts +152 -0
- package/src/kernels/trellis/tiled-prefill.ts +131 -0
- package/src/kernels/trellis/vector-expand.ts +58 -0
- package/src/kernels/trellis/wide-prefill.ts +89 -0
- package/src/kernels/turboquant/decode.ts +82 -0
- package/src/kernels/turboquant/index.ts +5 -0
- package/src/kernels/turboquant/inverse.ts +122 -0
- package/src/kernels/turboquant/ops.ts +860 -0
- package/src/kernels/turboquant/signs.ts +130 -0
- package/src/kernels/turboquant/tables.ts +124 -0
- package/src/layers/affine-verify-linear.ts +75 -0
- package/src/layers/ane-prefill-split.ts +283 -0
- package/src/layers/compiled-normalization.ts +41 -0
- package/src/layers/delta-heads.ts +23 -0
- package/src/layers/dense-embedding.ts +29 -0
- package/src/layers/dense-linear.ts +32 -0
- package/src/layers/dequantized-linear.ts +72 -0
- package/src/layers/folded-attention.ts +42 -0
- package/src/layers/geglu.ts +130 -0
- package/src/layers/helpers.ts +7 -0
- package/src/layers/index.ts +36 -0
- package/src/layers/kv4-decode-attention.ts +15 -0
- package/src/layers/layer-norm.ts +24 -0
- package/src/layers/loading.ts +92 -0
- package/src/layers/lora.ts +150 -0
- package/src/layers/normalization.ts +9 -0
- package/src/layers/prefix-layout.ts +33 -0
- package/src/layers/quantized-append-attention.ts +32 -0
- package/src/layers/quantized-attention.ts +353 -0
- package/src/layers/quantized-embedding.ts +39 -0
- package/src/layers/quantized-linear.ts +137 -0
- package/src/layers/quantized-switch-linear.ts +37 -0
- package/src/layers/qwen-append.ts +12 -0
- package/src/layers/qwen-mrope.ts +238 -0
- package/src/layers/rope-2d.ts +110 -0
- package/src/layers/rope.ts +389 -0
- package/src/layers/swiglu.ts +23 -0
- package/src/layers/trellis-codebook-down.ts +52 -0
- package/src/layers/trellis-codebook-gate-up.ts +42 -0
- package/src/layers/trellis-down.ts +117 -0
- package/src/layers/trellis-gate-up.ts +71 -0
- package/src/layers/trellis-linear.ts +211 -0
- package/src/layers/trellis-prompt.ts +102 -0
- package/src/layers/trellis-reduce-gate-up.ts +73 -0
- package/src/layers/trellis-scatter-down.ts +98 -0
- package/src/models/ane-install.ts +74 -0
- package/src/models/audio/conformer.ts +656 -0
- package/src/models/cache-composition.ts +44 -0
- package/src/models/capabilities.ts +89 -0
- package/src/models/chat-template.ts +10 -0
- package/src/models/composition.ts +107 -0
- package/src/models/diffusion-gemma/experts.ts +59 -0
- package/src/models/diffusion-gemma/family.ts +21 -0
- package/src/models/diffusion-gemma/model.ts +900 -0
- package/src/models/diffusion-gemma/vision.ts +549 -0
- package/src/models/drafter-loaders.ts +49 -0
- package/src/models/drafters.ts +39 -0
- package/src/models/factory.ts +351 -0
- package/src/models/families.ts +70 -0
- package/src/models/family.ts +125 -0
- package/src/models/gemma4/assistant-target.ts +46 -0
- package/src/models/gemma4/assistant.ts +327 -0
- package/src/models/gemma4/blocks.ts +755 -0
- package/src/models/gemma4/compiled-decode.ts +452 -0
- package/src/models/gemma4/family.ts +39 -0
- package/src/models/gemma4/gemma4-12b.ts +677 -0
- package/src/models/gemma4/gemma4-26b.ts +544 -0
- package/src/models/gemma4/gemma4-e4b.ts +599 -0
- package/src/models/gemma4/graphs.ts +147 -0
- package/src/models/gemma4/media.ts +88 -0
- package/src/models/gemma4/model.ts +860 -0
- package/src/models/gemma4/serving-blocks.ts +72 -0
- package/src/models/gemma4/trainable.ts +186 -0
- package/src/models/gemma4/written-blocks.ts +232 -0
- package/src/models/gemma4/written-forward.ts +159 -0
- package/src/models/gemma4/written-input.ts +29 -0
- package/src/models/glm52/atlas.ts +507 -0
- package/src/models/glm52/coupling.ts +388 -0
- package/src/models/glm52/dsa.ts +601 -0
- package/src/models/glm52/family.ts +20 -0
- package/src/models/glm52/mla.ts +812 -0
- package/src/models/glm52/model.ts +1127 -0
- package/src/models/glm52/moe.ts +223 -0
- package/src/models/glm52/mtp.ts +60 -0
- package/src/models/glm52/pilot.ts +537 -0
- package/src/models/glm52/reference.ts +445 -0
- package/src/models/glm52/residency.ts +557 -0
- package/src/models/glm52/streamed-experts.ts +447 -0
- package/src/models/graph.ts +59 -0
- package/src/models/implementation.ts +230 -0
- package/src/models/index.ts +15 -0
- package/src/models/media-input.ts +15 -0
- package/src/models/memory-plan.ts +39 -0
- package/src/models/minicpm5/family.ts +29 -0
- package/src/models/minicpm5/model.ts +394 -0
- package/src/models/minicpm5/trainable.ts +90 -0
- package/src/models/profile.ts +337 -0
- package/src/models/qwen/blocks.ts +658 -0
- package/src/models/qwen/checkpoint.ts +190 -0
- package/src/models/qwen/families.ts +57 -0
- package/src/models/qwen/media-input.ts +161 -0
- package/src/models/qwen/mtp.ts +101 -0
- package/src/models/qwen/qwen3-moe.ts +355 -0
- package/src/models/qwen/qwen3.ts +323 -0
- package/src/models/qwen/qwen38-27b-trellis-m1max.ts +1532 -0
- package/src/models/qwen/qwen38-27b-trellis-m4pro.ts +1092 -0
- package/src/models/qwen/qwen3_5.ts +433 -0
- package/src/models/qwen/written-layers.ts +285 -0
- package/src/models/row-table.ts +1 -0
- package/src/models/runtime.ts +73 -0
- package/src/models/speculative/deepspec-artifact.ts +10 -0
- package/src/models/speculative/deepspec.ts +744 -0
- package/src/models/speculative/dflash.ts +553 -0
- package/src/models/speculative/dflash2-artifact.ts +9 -0
- package/src/models/speculative/dflash2-operations.ts +1 -0
- package/src/models/speculative/dflash2.ts +576 -0
- package/src/models/speculative/loader.ts +29 -0
- package/src/models/support.ts +79 -0
- package/src/models/universal/archs.ts +365 -0
- package/src/models/universal/blocks.ts +479 -0
- package/src/models/universal/dense.ts +255 -0
- package/src/models/universal/family.ts +17 -0
- package/src/models/vision/qwen3vl.ts +566 -0
- package/src/models/vision/siglip.ts +520 -0
- package/src/models/vision/unified.ts +176 -0
- package/src/models/whisper/family.ts +19 -0
- package/src/models/whisper/fast.ts +734 -0
- package/src/models/whisper/model.ts +430 -0
- package/src/runtime/compiled-trace.ts +16 -0
- package/src/runtime/config.ts +77 -0
- package/src/runtime/expert-trace.ts +71 -0
- package/src/runtime/graph-forward.ts +65 -0
- package/src/runtime/integers.ts +6 -0
- package/src/runtime/native.ts +16 -0
- package/src/runtime/resources.ts +53 -0
- package/src/runtime/row-operations.ts +42 -0
- package/src/runtime/trace.ts +161 -0
- package/src/sampling/curve.ts +122 -0
- package/src/sampling/draft.ts +55 -0
- package/src/sampling/extras.ts +45 -0
- package/src/sampling/filters.ts +134 -0
- package/src/sampling/grammar.ts +630 -0
- package/src/sampling/hlg.ts +290 -0
- package/src/sampling/index.ts +9 -0
- package/src/sampling/processors.ts +142 -0
- package/src/sampling/sampler.ts +120 -0
- package/src/sampling/step.ts +343 -0
- package/src/sampling/types.ts +161 -0
- package/src/sampling/window-filters.ts +46 -0
- package/src/scoring/decisions.ts +96 -0
- package/src/scoring/full-sequence.ts +103 -0
- package/src/scoring/index.ts +9 -0
- package/src/scoring/kl.ts +29 -0
- package/src/scoring/perplexity.ts +165 -0
- package/src/scoring/response-logps.ts +65 -0
- package/src/state/affine-attention.ts +180 -0
- package/src/state/attachment.ts +68 -0
- package/src/state/attention-read.ts +215 -0
- package/src/state/batched-kv.ts +294 -0
- package/src/state/batched-mask.ts +445 -0
- package/src/state/batched-quant.ts +389 -0
- package/src/state/batched-quantized-kv.ts +209 -0
- package/src/state/batched-rotating-quant.ts +457 -0
- package/src/state/batched-rotating-state.ts +149 -0
- package/src/state/batched-rotating.ts +430 -0
- package/src/state/batched-row-storage.ts +179 -0
- package/src/state/batched-ssm.ts +54 -0
- package/src/state/batched-turboquant-kv.ts +144 -0
- package/src/state/bf16-first-kv.ts +663 -0
- package/src/state/cache-service.ts +592 -0
- package/src/state/capabilities.ts +65 -0
- package/src/state/checkpoint.ts +46 -0
- package/src/state/continuation-persistence.ts +126 -0
- package/src/state/decode-step-inputs.ts +31 -0
- package/src/state/decoded-kv-donor.ts +12 -0
- package/src/state/delayed-quantized-kv.ts +141 -0
- package/src/state/delayed-rotating-quantized-kv.ts +178 -0
- package/src/state/delayed-turboquant-kv.ts +123 -0
- package/src/state/durability.ts +214 -0
- package/src/state/encoder-cache.ts +26 -0
- package/src/state/full-kv-row-append.ts +33 -0
- package/src/state/full-kv-row-donor.ts +54 -0
- package/src/state/full-prefill-padding.ts +56 -0
- package/src/state/full-prefill-row.ts +112 -0
- package/src/state/full-transitioning-kv-rows.ts +70 -0
- package/src/state/glm52-cache.ts +783 -0
- package/src/state/grouped-heads-kv4-cache.ts +26 -0
- package/src/state/index.ts +38 -0
- package/src/state/kv-attention-view.ts +77 -0
- package/src/state/kv-budget.ts +14 -0
- package/src/state/kv-scheme.ts +315 -0
- package/src/state/kv-storage.ts +57 -0
- package/src/state/kv-tensor-rows.ts +212 -0
- package/src/state/kv.ts +324 -0
- package/src/state/kv4-head256-cache.ts +46 -0
- package/src/state/latent-legos.ts +895 -0
- package/src/state/layout.ts +277 -0
- package/src/state/leases.ts +26 -0
- package/src/state/paged/cache.ts +689 -0
- package/src/state/paged/codec.ts +36 -0
- package/src/state/paged/rows.ts +223 -0
- package/src/state/persistence-types.ts +173 -0
- package/src/state/persistence-worker.ts +83 -0
- package/src/state/persistence.ts +1036 -0
- package/src/state/persistence.worker.d.ts +3 -0
- package/src/state/persistence.worker.js +264 -0
- package/src/state/prefix-cache.ts +685 -0
- package/src/state/quantized-kv.ts +369 -0
- package/src/state/quantized-tensor.ts +26 -0
- package/src/state/request-policy.ts +48 -0
- package/src/state/retention-policy.ts +37 -0
- package/src/state/rollback.ts +131 -0
- package/src/state/rotating-kv-donor.ts +35 -0
- package/src/state/rotating-kv-layout.ts +341 -0
- package/src/state/rotating-kv.ts +529 -0
- package/src/state/rotating-quantized-kv.ts +616 -0
- package/src/state/rotating-row-transaction.ts +35 -0
- package/src/state/rows.ts +104 -0
- package/src/state/softcap-attention.ts +118 -0
- package/src/state/speculative/glm52-mtp-state.ts +38 -0
- package/src/state/speculative/qwen-mtp-state.ts +34 -0
- package/src/state/speculative-rotating-kv.ts +113 -0
- package/src/state/ssd-cache.ts +434 -0
- package/src/state/ssm-prefill-padding.ts +39 -0
- package/src/state/ssm.ts +422 -0
- package/src/state/target-layout.ts +7 -0
- package/src/state/tiered-prefix-cache.ts +45 -0
- package/src/state/training-cache.ts +300 -0
- package/src/state/transitioning-kv-rows.ts +267 -0
- package/src/state/turboquant-attention.ts +97 -0
- package/src/state/turboquant-codec.ts +90 -0
- package/src/state/turboquant-kv.ts +442 -0
- package/src/state/views.ts +26 -0
- package/src/transcription/index.ts +3 -0
- package/src/transcription/silero-vad.ts +379 -0
- package/src/transcription/whisper/decode.ts +773 -0
- package/src/transcription/whisper/format.ts +74 -0
- package/src/transcription/whisper/index.ts +4 -0
- package/src/transcription/whisper/timing.ts +274 -0
- package/src/transcription/whisper/transcribe.ts +394 -0
- package/src/transcription/whisper/types.ts +22 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import { type CheckpointAttachment } from "../contracts/mlx/checkpoint";
|
|
2
|
+
import { materializeCheckpoint, attachmentBytes, cloneAttachments, disposeAttachments } from "./checkpoint";
|
|
3
|
+
import { leaseCacheState } from "./leases";
|
|
4
|
+
import { minimumReusableOffset } from "./views";
|
|
5
|
+
import { withResource, cleanupFailure, disposeResources, ownResource } from "../runtime/resources";
|
|
6
|
+
// Byte-capped LRU prompt cache — the RAM tier of the Layer-0 KV store.
|
|
7
|
+
//
|
|
8
|
+
// The mlx-lm lesson: a count-capped cache of multi-GB KV
|
|
9
|
+
// entries is an OOM footgun. Ours accounts bytes (sum of KV array bytes
|
|
10
|
+
// per entry) and evicts least-recently-used until under the cap.
|
|
11
|
+
//
|
|
12
|
+
// Usage pattern (single generation queue, so take/put is race-free):
|
|
13
|
+
// const hit = cache.take(promptIds); // longest-prefix match
|
|
14
|
+
// generate(model, promptIds, { cache: hit?.caches ?? fresh })
|
|
15
|
+
// cache.put([...promptIds, ...generated], caches, ns, hit?.retain);
|
|
16
|
+
// take() lends zero-copy views and leaves every RAM donor in place, including
|
|
17
|
+
// recurrent and method state. SSD persistence runs independently of RAM reuse.
|
|
18
|
+
// put() supersedes same-ns prefix-ancestors when the new entry is trimmable.
|
|
19
|
+
//
|
|
20
|
+
// TIERING (Layer 0, unified-engine plan): when a ColdTier is attached,
|
|
21
|
+
// take() itself runs the two-tier dance — RAM peek vs cold find, restore
|
|
22
|
+
// (streamed copy) + trim when the cold tier holds a strictly longer
|
|
23
|
+
// prefix — so every consumer (the cache service the scheduler acquires
|
|
24
|
+
// through, direct generation) gets SSD restores through the same take()/put()
|
|
25
|
+
// it already calls. Eviction spills to the tier (the #spill hook); idle
|
|
26
|
+
// entries demote to it when policy permits. TieredPromptCache owns queued
|
|
27
|
+
// persistence and promotes restored prefixes into RAM.
|
|
28
|
+
|
|
29
|
+
import type { Cache } from "../contracts/mlx/cache";
|
|
30
|
+
import type { PrefixCache, PrefixCacheHit } from "../contracts/portable/prefix-cache";
|
|
31
|
+
import { LruRetention, type RetentionPolicy, type RetentionCandidate } from "./retention-policy";
|
|
32
|
+
import { cloneKvCaches } from "./persistence";
|
|
33
|
+
|
|
34
|
+
/** Reference-counted release: wraps an entry's `retain` (e.g. an mmap
|
|
35
|
+
* unmap) so DONOR + CLONES can each hold a share — the underlying hook
|
|
36
|
+
* runs once, after the LAST holder releases. Prefix sharing hands out
|
|
37
|
+
* zero-copy VIEWS of a donor's buffers; if the donor's unmap ran while a
|
|
38
|
+
* clone still referenced the mapped pages, the clone would read freed
|
|
39
|
+
* memory. */
|
|
40
|
+
function makeSharedRetain(retain: (() => void) | undefined): { acquire(): () => void } {
|
|
41
|
+
let count = 0;
|
|
42
|
+
let released = false;
|
|
43
|
+
return {
|
|
44
|
+
acquire() {
|
|
45
|
+
count++;
|
|
46
|
+
let mine = false;
|
|
47
|
+
return () => {
|
|
48
|
+
if (mine) return; // idempotent per share
|
|
49
|
+
mine = true;
|
|
50
|
+
count--;
|
|
51
|
+
if (count === 0 && !released) {
|
|
52
|
+
released = true;
|
|
53
|
+
retain?.();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The cold (SSD) tier the RAM cache tiers over. Structural — this module
|
|
61
|
+
* never imports ssd-cache (which imports us); the server binds
|
|
62
|
+
* SsdCacheStore + the model into this shape
|
|
63
|
+
* (apps/mlx-bun/src/engine/cache-services.ts). Lookup and
|
|
64
|
+
* restore failures return null; store returns false on failure. Operations
|
|
65
|
+
* that require persistence retain their donor if storing fails. */
|
|
66
|
+
export interface ColdTier {
|
|
67
|
+
/** Longest stored usable prefix for prompt/ns — index-only, no I/O.
|
|
68
|
+
* `handle` is the tier's opaque entry token, passed back to restore. */
|
|
69
|
+
find(prompt: number[], ns: string): { prefixLen: number; handle: unknown } | null;
|
|
70
|
+
/** Restore GPU-visible state. The default SSD backend copies into owned
|
|
71
|
+
* arrays. An optional backing release runs after consumers dispose their
|
|
72
|
+
* arrays. Null on any failure. */
|
|
73
|
+
restore(handle: unknown): { tokens: number[]; caches: Cache[]; attachments?: CheckpointAttachment[]; retain: () => void } | null;
|
|
74
|
+
findExact?(tokens: number[], ns: string): { prefixLen: number; handle: unknown } | null;
|
|
75
|
+
restoreAsync?(handle: unknown): Promise<ReturnType<ColdTier["restore"]>>;
|
|
76
|
+
/** Tensor objects have no target layers; restore with their own schema. */
|
|
77
|
+
restoreObjectAsync?(handle: unknown): Promise<ReturnType<ColdTier["restore"]>>;
|
|
78
|
+
/** Borrow live caches and finish persistence before returning. A false
|
|
79
|
+
* result prevents a required demotion. */
|
|
80
|
+
store(tokens: number[], caches: Cache[], ns: string, attachments?: CheckpointAttachment[]): boolean | void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Spill sink for eviction/demotion (write-behind, 2026-07-06).
|
|
84
|
+
*
|
|
85
|
+
* `spillOwned` (preferred, NON-BLOCKING): the cache hands the sink an
|
|
86
|
+
* entry it OWNS — zero-copy CLONES of the caches (made via the same
|
|
87
|
+
* injectable cloner take() uses, BEFORE the donor is disposed; entries
|
|
88
|
+
* are immutable so the clones stay consistent forever) plus a copy of
|
|
89
|
+
* the tokens. The sink must dispose entry.caches when done, on BOTH
|
|
90
|
+
* fulfill and reject paths (the server chains storeAsync -> dispose on
|
|
91
|
+
* ssdWriteChain). A sink that throws SYNCHRONOUSLY has not taken
|
|
92
|
+
* ownership — the cache disposes the clones and the spill degrades to
|
|
93
|
+
* a no-op; eviction/demotion never unwinds.
|
|
94
|
+
*
|
|
95
|
+
* `spillSync` (fallback): called with the LIVE entry about to be
|
|
96
|
+
* disposed; must finish reading before returning. This is the old
|
|
97
|
+
* contract — a full synchronous serialize+write under the generation
|
|
98
|
+
* lock — kept for tests and simple embedders. */
|
|
99
|
+
export interface SpillSink {
|
|
100
|
+
spillOwned?: (entry: PromptCacheEntry) => void;
|
|
101
|
+
spillSync?: (entry: PromptCacheEntry) => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface PromptCacheEntry extends PrefixCacheHit<Cache[], CheckpointAttachment[]> {
|
|
105
|
+
/** Namespace key — adapter spec for LoRA requests ("" = base model).
|
|
106
|
+
* KV computed under one adapter must never seed another's prefill. */
|
|
107
|
+
ns: string;
|
|
108
|
+
/** Release hook for entries restored from the SSD cold tier: unmaps the
|
|
109
|
+
* backing COW file mapping. MUST run only after `caches` are disposed
|
|
110
|
+
* (they may alias file pages); every dispose path here honors that, and
|
|
111
|
+
* callers who take() an entry carry the thunk with it. */
|
|
112
|
+
retain?: () => void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function cacheBytes(caches: Cache[]): number {
|
|
116
|
+
let total = 0;
|
|
117
|
+
for (const c of caches) {
|
|
118
|
+
total += withResource(leaseCacheState(c), (state) => state.reduce((bytes, a) => bytes + a.nbytes, 0));
|
|
119
|
+
}
|
|
120
|
+
return total;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Length of the longest common prefix of two token sequences. */
|
|
124
|
+
export function commonPrefixLength(a: number[], b: number[]): number {
|
|
125
|
+
const n = Math.min(a.length, b.length);
|
|
126
|
+
let i = 0;
|
|
127
|
+
while (i < n && a[i] === b[i]) i++;
|
|
128
|
+
return i;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface EntryRecord extends RetentionCandidate {
|
|
132
|
+
entry: PromptCacheEntry;
|
|
133
|
+
bytes: number;
|
|
134
|
+
lastUsed: number;
|
|
135
|
+
lastUsedMs: number;
|
|
136
|
+
/** Ref-counted retain: the donor holds one share (entry.retain); every
|
|
137
|
+
* clone handed out by take() holds another. */
|
|
138
|
+
share: { acquire(): () => void };
|
|
139
|
+
resident: boolean;
|
|
140
|
+
sessions: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class PromptCache implements PrefixCache<Cache[], CheckpointAttachment[]> {
|
|
144
|
+
/** Exact tensor objects share this store's byte budget, eviction and writer.
|
|
145
|
+
* Empty token history denotes no conversation; the private namespace is the
|
|
146
|
+
* complete object identity. No fabricated language tokens enter the index. */
|
|
147
|
+
readonly #objects: import("../contracts/portable/object-cache").ObjectCache<CheckpointAttachment[]> = {
|
|
148
|
+
take: key => this.#takeObject(`object:${key}`),
|
|
149
|
+
put: (key, value) => this.put([], [], `object:${key}`, undefined, value),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
get objects(): import("../contracts/portable/object-cache").ObjectCache<CheckpointAttachment[]> | undefined {
|
|
153
|
+
return this.maxBytes > 0 || this.#cold ? this.#objects : undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async #takeObject(ns: string): Promise<{ value: CheckpointAttachment[]; dispose(): void } | null> {
|
|
157
|
+
let entry = this.findExact([], ns);
|
|
158
|
+
if (entry) this.objectHits++;
|
|
159
|
+
else this.objectMisses++;
|
|
160
|
+
if (!entry) {
|
|
161
|
+
const cold = this.#cold;
|
|
162
|
+
const hit = cold?.findExact?.([], ns);
|
|
163
|
+
if (!hit || !cold?.restoreObjectAsync) return null;
|
|
164
|
+
const epoch = this.#epoch;
|
|
165
|
+
const loaded = await cold.restoreObjectAsync(hit.handle);
|
|
166
|
+
if (!loaded) return null;
|
|
167
|
+
this.objectRestores++;
|
|
168
|
+
if (epoch !== this.#epoch) { this.#disposeEntry({ ...loaded, ns }, false); return null; }
|
|
169
|
+
// Another request may have published this immutable object during IO.
|
|
170
|
+
entry = this.findExact([], ns);
|
|
171
|
+
if (entry) this.#disposeEntry({ ...loaded, ns }, false);
|
|
172
|
+
else {
|
|
173
|
+
this.#publishingPrefetch = true;
|
|
174
|
+
try { this.put([], loaded.caches, ns, loaded.retain, loaded.attachments); }
|
|
175
|
+
catch (error) { this.#disposeEntry({ ...loaded, ns }, false); throw error; }
|
|
176
|
+
finally { this.#publishingPrefetch = false; }
|
|
177
|
+
entry = this.findExact([], ns);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (!entry) return null;
|
|
181
|
+
const borrowed = this.#borrow(this.#records.get(entry)!, 0, ns);
|
|
182
|
+
try { this.evictToBudget(); this.reclaim(); }
|
|
183
|
+
catch (error) { this.#disposeEntry(borrowed, false); throw error; }
|
|
184
|
+
let released = false;
|
|
185
|
+
return { value: borrowed.attachments ?? [], dispose: () => {
|
|
186
|
+
if (released) return; released = true;
|
|
187
|
+
this.#disposeEntry(borrowed, false);
|
|
188
|
+
} };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
readonly maxBytes: number;
|
|
192
|
+
#entries: EntryRecord[] = [];
|
|
193
|
+
#clock = 0;
|
|
194
|
+
#sessions = new Map<string, Map<string, EntryRecord>>();
|
|
195
|
+
#records = new WeakMap<PromptCacheEntry, EntryRecord>();
|
|
196
|
+
sessionHits = 0;
|
|
197
|
+
sessionMisses = 0;
|
|
198
|
+
prefixScans = 0;
|
|
199
|
+
objectHits = 0;
|
|
200
|
+
objectMisses = 0;
|
|
201
|
+
objectRestores = 0;
|
|
202
|
+
|
|
203
|
+
closeSession(sessionId: string): void {
|
|
204
|
+
for (const record of this.#sessions.get(sessionId)?.values() ?? []) record.sessions--;
|
|
205
|
+
this.#sessions.delete(sessionId);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#associate(sessionId: string | undefined, ns: string, record: EntryRecord): void {
|
|
209
|
+
if (!sessionId) return;
|
|
210
|
+
let entries = this.#sessions.get(sessionId);
|
|
211
|
+
if (!entries) this.#sessions.set(sessionId, entries = new Map());
|
|
212
|
+
const old = entries.get(ns);
|
|
213
|
+
if (old === record) return;
|
|
214
|
+
if (old) old.sessions--;
|
|
215
|
+
entries.set(ns, record); record.sessions++;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#sessionRecord(prompt: number[], ns: string, sessionId?: string): EntryRecord | undefined {
|
|
219
|
+
const record = sessionId ? this.#sessions.get(sessionId)?.get(ns) : undefined;
|
|
220
|
+
// A session labels affinity. Full-history callers may edit or branch;
|
|
221
|
+
// incompatible history uses ordinary lookup, never a request refusal.
|
|
222
|
+
return record && record.entry.tokens.length < prompt.length &&
|
|
223
|
+
commonPrefixLength(record.entry.tokens, prompt) === record.entry.tokens.length ? record : undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#coldHit(prompt: number[], ns: string, sessionId?: string) {
|
|
227
|
+
const known = this.#sessionRecord(prompt, ns, sessionId);
|
|
228
|
+
return (known && this.#cold?.findExact?.(known.entry.tokens, ns)) || this.#cold?.find(prompt, ns) || null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
hits = 0;
|
|
232
|
+
misses = 0;
|
|
233
|
+
demotions = 0;
|
|
234
|
+
/** Cold-tier spill sinks (see SpillSink): spillOwned gets clones and
|
|
235
|
+
* runs the write off-lock; spillSync gets the live entry inline.
|
|
236
|
+
* Failures are the sink's problem; eviction proceeds regardless. */
|
|
237
|
+
readonly #spillOwned: ((entry: PromptCacheEntry) => void) | null;
|
|
238
|
+
readonly #spillSync: ((entry: PromptCacheEntry) => void) | null;
|
|
239
|
+
/** Optional cold tier: take() tiers over it, demoteIdle() spills into it. */
|
|
240
|
+
readonly #cold: ColdTier | null;
|
|
241
|
+
/** Observer after publication, independent of persistence policy. */
|
|
242
|
+
onPut: ((tokens: number[], ns: string) => void) | null = null;
|
|
243
|
+
/** The backend measures pressure; this store chooses which optional
|
|
244
|
+
* snapshots to release. No storage I/O runs at a reclaim boundary. */
|
|
245
|
+
pressure: { overBudget(): boolean; releasePending?(): void } | null = null;
|
|
246
|
+
/** Tier policy: a pending write is not a lower-tier copy. This controls
|
|
247
|
+
* eviction only; persistence completion does not itself remove an entry. */
|
|
248
|
+
canEvict: ((entry: PromptCacheEntry) => boolean) | null = null;
|
|
249
|
+
protected promoteRestores = false;
|
|
250
|
+
retention: RetentionPolicy = new LruRetention();
|
|
251
|
+
#prefetches = new Map<unknown, Promise<PromptCacheEntry | null>>();
|
|
252
|
+
#pins = new Map<PromptCacheEntry, number>();
|
|
253
|
+
#publishingPrefetch = false;
|
|
254
|
+
#epoch = 0;
|
|
255
|
+
|
|
256
|
+
/** Start cold reads during request preparation, coalescing identical IO.
|
|
257
|
+
* Active request interest keeps a just-restored donor resident until the
|
|
258
|
+
* caller has consumed it. Cancellation releases interest through the lease. */
|
|
259
|
+
async prefetch(prompt: number[], ns = "", sessionId?: string): Promise<() => void> {
|
|
260
|
+
const known = this.#sessionRecord(prompt, ns, sessionId);
|
|
261
|
+
if (known?.resident) return this.#pin(known.entry);
|
|
262
|
+
const cold = this.#cold;
|
|
263
|
+
const hit = cold?.restoreAsync ? this.#coldHit(prompt, ns, sessionId) : null;
|
|
264
|
+
if (!cold?.restoreAsync || !hit || (!known && hit.prefixLen <= this.peekPrefixLen(prompt, ns))) return () => {};
|
|
265
|
+
let pending = this.#prefetches.get(hit.handle);
|
|
266
|
+
if (!pending) {
|
|
267
|
+
const epoch = this.#epoch;
|
|
268
|
+
pending = (async () => {
|
|
269
|
+
const loaded = await cold.restoreAsync!(hit.handle);
|
|
270
|
+
if (!loaded) return null;
|
|
271
|
+
if (epoch !== this.#epoch) { this.#disposeEntry({ ...loaded, ns }, false); return null; }
|
|
272
|
+
this.#publishingPrefetch = true;
|
|
273
|
+
try { this.put(loaded.tokens, loaded.caches, ns, loaded.retain, loaded.attachments); }
|
|
274
|
+
catch (error) { this.#disposeEntry({ ...loaded, ns }, false); throw error; }
|
|
275
|
+
finally { this.#publishingPrefetch = false; }
|
|
276
|
+
return this.findExact(loaded.tokens, ns);
|
|
277
|
+
})();
|
|
278
|
+
this.#prefetches.set(hit.handle, pending);
|
|
279
|
+
}
|
|
280
|
+
let entry: PromptCacheEntry | null;
|
|
281
|
+
try { entry = await pending; }
|
|
282
|
+
finally { if (this.#prefetches.get(hit.handle) === pending) this.#prefetches.delete(hit.handle); }
|
|
283
|
+
if (!entry) return () => {};
|
|
284
|
+
this.#associate(sessionId, ns, this.#records.get(entry)!);
|
|
285
|
+
return this.#pin(entry);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#pin(entry: PromptCacheEntry): () => void {
|
|
289
|
+
this.#pins.set(entry, (this.#pins.get(entry) ?? 0) + 1);
|
|
290
|
+
let released = false;
|
|
291
|
+
return () => {
|
|
292
|
+
if (released) return; released = true;
|
|
293
|
+
const count = this.#pins.get(entry!) ?? 1;
|
|
294
|
+
if (count === 1) this.#pins.delete(entry!); else this.#pins.set(entry!, count - 1);
|
|
295
|
+
this.evictToBudget(); this.reclaim();
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#victim(): EntryRecord | undefined {
|
|
300
|
+
const candidates = this.#entries.filter(record => !this.#pins.has(record.entry));
|
|
301
|
+
const unrelated = candidates.filter(record => record.sessions === 0);
|
|
302
|
+
return candidates.length ? this.retention.victim(unrelated.length ? unrelated : candidates) : undefined;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
evictToBudget(): void {
|
|
306
|
+
if (this.#publishingPrefetch) return;
|
|
307
|
+
while (this.totalBytes > this.maxBytes) {
|
|
308
|
+
if (!this.#entries.length) break;
|
|
309
|
+
const oldest = this.#victim();
|
|
310
|
+
if (!oldest) break;
|
|
311
|
+
if (this.canEvict && !this.canEvict(oldest.entry)) break;
|
|
312
|
+
this.retention.evicted(oldest);
|
|
313
|
+
this.#entries = this.#entries.filter(r => r !== oldest);
|
|
314
|
+
try { this.#disposeEntry(oldest.entry, !this.canEvict); }
|
|
315
|
+
catch (error) { console.warn(`[prompt-cache] entry cleanup failed: ${error}`); }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
reclaim(): void {
|
|
320
|
+
const pressure = this.pressure;
|
|
321
|
+
if (!pressure?.overBudget()) return;
|
|
322
|
+
pressure.releasePending?.();
|
|
323
|
+
while (this.#entries.length && pressure.overBudget()) {
|
|
324
|
+
const oldest = this.#victim();
|
|
325
|
+
if (!oldest) break;
|
|
326
|
+
if (this.canEvict && !this.canEvict(oldest.entry)) break;
|
|
327
|
+
this.retention.evicted(oldest);
|
|
328
|
+
this.#entries = this.#entries.filter(r => r !== oldest);
|
|
329
|
+
this.#disposeEntry(oldest.entry, false);
|
|
330
|
+
this.demotions++;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Zero-copy view cloner — injectable so model-free tests can stub it. */
|
|
335
|
+
readonly #clone: (caches: Cache[]) => Cache[];
|
|
336
|
+
|
|
337
|
+
constructor(
|
|
338
|
+
maxBytes: number,
|
|
339
|
+
// Bare function = legacy sync spill (tests, simple embedders).
|
|
340
|
+
spill: SpillSink | ((entry: PromptCacheEntry) => void) | null = null,
|
|
341
|
+
cold: ColdTier | null = null,
|
|
342
|
+
clone: (caches: Cache[]) => Cache[] = cloneKvCaches,
|
|
343
|
+
private readonly materialize: (caches: Cache[], attachments?: CheckpointAttachment[]) => void = materializeCheckpoint,
|
|
344
|
+
) {
|
|
345
|
+
this.maxBytes = maxBytes;
|
|
346
|
+
this.#spillOwned = typeof spill === "function" ? null : spill?.spillOwned ?? null;
|
|
347
|
+
this.#spillSync = typeof spill === "function" ? spill : spill?.spillSync ?? null;
|
|
348
|
+
this.#cold = cold;
|
|
349
|
+
this.#clone = clone;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#disposeEntry(entry: PromptCacheEntry, spillFirst: boolean): void {
|
|
353
|
+
const record = this.#records.get(entry);
|
|
354
|
+
if (record) record.resident = false;
|
|
355
|
+
if (spillFirst) {
|
|
356
|
+
if (this.#spillOwned) {
|
|
357
|
+
// NON-BLOCKING spill: clone BEFORE disposing (clones are zero-copy
|
|
358
|
+
// views of the live arrays) and hand ownership to the sink, which
|
|
359
|
+
// flushes off the generation lock. The underlying GPU buffers stay
|
|
360
|
+
// alive until the sink disposes the clones — for demoteIdle that
|
|
361
|
+
// means the memory is freed one bounded write-chain hop later, not
|
|
362
|
+
// instantly; acceptable because the chain is serial and each write
|
|
363
|
+
// disposes its clones on settle (fulfill AND reject).
|
|
364
|
+
// Retain contract: entry.retain still runs below, possibly while
|
|
365
|
+
// the clones are in flight — safe because cold-restored entries
|
|
366
|
+
// OWN their bytes (streamed copy-restore, 2026-07-07): there is
|
|
367
|
+
// no backing mmap for retain to unmap, so nothing the clones
|
|
368
|
+
// could alias goes away.
|
|
369
|
+
let clones: Cache[] | null = null;
|
|
370
|
+
let attachments: CheckpointAttachment[] | undefined;
|
|
371
|
+
try {
|
|
372
|
+
clones = this.#clone(entry.caches);
|
|
373
|
+
attachments = cloneAttachments(entry.attachments);
|
|
374
|
+
this.#spillOwned({ tokens: [...entry.tokens], caches: clones, ns: entry.ns, attachments });
|
|
375
|
+
} catch {
|
|
376
|
+
// Failure degrades to no-spill; eviction/demotion never unwinds.
|
|
377
|
+
if (clones) for (const c of clones) try { c.dispose(); } catch {}
|
|
378
|
+
try { disposeAttachments(attachments); } catch {}
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
const spill =
|
|
382
|
+
this.#spillSync ??
|
|
383
|
+
(this.#cold
|
|
384
|
+
? (e: PromptCacheEntry) => this.#cold!.store(e.tokens, e.caches, e.ns, e.attachments)
|
|
385
|
+
: null);
|
|
386
|
+
if (spill) try { spill(entry); } catch {}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
disposeResources([...entry.caches, { dispose: () => disposeAttachments(entry.attachments) }]);
|
|
390
|
+
entry.retain?.(); // backing release requires successful cache disposal
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
get totalBytes(): number {
|
|
394
|
+
return this.#entries.reduce((a, e) => a + e.bytes, 0);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
get size(): number {
|
|
398
|
+
return this.#entries.length;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Release LRU entries until the caller's live-memory check is satisfied.
|
|
402
|
+
* Unlike background eviction, pressure relief must finish the SSD write
|
|
403
|
+
* and release the buffers now, not enqueue clones that keep them alive.
|
|
404
|
+
* Call under the engine lock, after taking the request's reusable prefix. */
|
|
405
|
+
relievePressure(overBudget: () => boolean): number {
|
|
406
|
+
let count = 0;
|
|
407
|
+
while (this.#entries.length && overBudget()) {
|
|
408
|
+
const victim = this.#victim();
|
|
409
|
+
if (!victim) break;
|
|
410
|
+
const oldest = this.#entries.indexOf(victim);
|
|
411
|
+
const { entry } = victim;
|
|
412
|
+
// Keep the entry resident if persistence throws. A failed SSD write
|
|
413
|
+
// must not be presented as a successful demotion.
|
|
414
|
+
if (this.#cold) {
|
|
415
|
+
if (this.#cold.store(entry.tokens, entry.caches, entry.ns, entry.attachments) === false) break;
|
|
416
|
+
}
|
|
417
|
+
else this.#spillSync?.(entry);
|
|
418
|
+
this.retention.evicted(victim);
|
|
419
|
+
this.#entries.splice(oldest, 1);
|
|
420
|
+
this.#disposeEntry(entry, false);
|
|
421
|
+
this.demotions++;
|
|
422
|
+
count++;
|
|
423
|
+
}
|
|
424
|
+
return count;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Find the entry with the longest usable common prefix of `prompt`
|
|
428
|
+
* across BOTH tiers. Every RAM entry lends zero-copy views and retains its
|
|
429
|
+
* donor, including exact-boundary recurrent state and method attachments.
|
|
430
|
+
* The caller owns the returned views —
|
|
431
|
+
* dispose them or put() an extended entry, honoring `retain` either way.
|
|
432
|
+
*
|
|
433
|
+
* Usable prefix = common prefix capped at prompt.length - 1 (at least
|
|
434
|
+
* one token must be forwarded to produce logits). Entries longer than
|
|
435
|
+
* the prefix need cache.trim(); ring caches lose trimability once
|
|
436
|
+
* wrapped — those entries only match in full.
|
|
437
|
+
*
|
|
438
|
+
* Tier order: the cold tier wins only with a STRICTLY longer usable
|
|
439
|
+
* prefix (restore reads storage and materializes native arrays). A
|
|
440
|
+
* cold entry with an untrimmable divergent tail (ring post-wrap, SSM)
|
|
441
|
+
* is dropped and the RAM candidate (or a fresh prefill) serves.
|
|
442
|
+
* hits/misses count RAM candidacy only (cold restores are counted by
|
|
443
|
+
* the tier itself), preserving the /stats meaning. */
|
|
444
|
+
take(prompt: number[], ns = "", sessionId?: string): PromptCacheEntry | null {
|
|
445
|
+
const hit = this.#take(prompt, ns, sessionId);
|
|
446
|
+
// Keep the chosen state's owned views before releasing optional donors.
|
|
447
|
+
// This also covers a cold restore materializing its bytes at admission.
|
|
448
|
+
try { this.reclaim(); return hit; }
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (hit) this.#disposeEntry(hit, false);
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
#take(prompt: number[], ns: string, sessionId?: string): PromptCacheEntry | null {
|
|
456
|
+
const known = this.#sessionRecord(prompt, ns, sessionId);
|
|
457
|
+
if (known?.resident) {
|
|
458
|
+
this.sessionHits++; this.hits++;
|
|
459
|
+
return this.#borrow(known, known.entry.tokens.length, ns);
|
|
460
|
+
}
|
|
461
|
+
if (sessionId) this.sessionMisses++;
|
|
462
|
+
this.prefixScans++;
|
|
463
|
+
let bestIdx = -1;
|
|
464
|
+
let bestLen = 0;
|
|
465
|
+
for (let i = 0; i < this.#entries.length; i++) {
|
|
466
|
+
const e = this.#entries[i]!.entry;
|
|
467
|
+
if (e.ns !== ns) continue;
|
|
468
|
+
const p = Math.min(commonPrefixLength(e.tokens, prompt), prompt.length - 1);
|
|
469
|
+
if (p <= bestLen || p < minimumReusableOffset(e.caches)) continue;
|
|
470
|
+
const trimNeeded = e.tokens.length - p;
|
|
471
|
+
if (trimNeeded > 0 && (!!e.attachments?.length || !e.caches.every((c) => c.isTrimmable()))) continue;
|
|
472
|
+
bestLen = p;
|
|
473
|
+
bestIdx = i;
|
|
474
|
+
}
|
|
475
|
+
if (bestIdx === -1) this.misses++;
|
|
476
|
+
else this.hits++;
|
|
477
|
+
|
|
478
|
+
// Cold tier: strictly longer usable prefix ⇒ restore + trim; the RAM
|
|
479
|
+
// candidate stays put UNTOUCHED (the old serial-lane flow trimmed it
|
|
480
|
+
// before comparing — a needless entry degradation, gone now).
|
|
481
|
+
if (this.#cold) {
|
|
482
|
+
const hit = this.#coldHit(prompt, ns, sessionId);
|
|
483
|
+
if (hit && hit.prefixLen > bestLen) {
|
|
484
|
+
const loaded = this.#cold.restore(hit.handle);
|
|
485
|
+
if (loaded) {
|
|
486
|
+
const owner = ownResource(loaded, (entry) =>
|
|
487
|
+
this.#disposeEntry({ ...entry, ns }, false));
|
|
488
|
+
const result = withResource(owner, (entry) => {
|
|
489
|
+
const trimNeeded = entry.tokens.length - hit.prefixLen;
|
|
490
|
+
if (hit.prefixLen < minimumReusableOffset(entry.caches)) return null;
|
|
491
|
+
if (trimNeeded > 0 && (!!entry.attachments?.length || !entry.caches.every((cache) => cache.isTrimmable()))) {
|
|
492
|
+
console.warn(
|
|
493
|
+
`[prompt-cache] cold entry unusable: ${entry.tokens.length} tokens, ` +
|
|
494
|
+
`needs trim ${trimNeeded} but untrimmable (wrapped ring/SSM) — re-prefilling`,
|
|
495
|
+
);
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
if (trimNeeded > 0) for (const cache of entry.caches) cache.trim(trimNeeded);
|
|
499
|
+
const tokens = entry.tokens.slice(0, hit.prefixLen);
|
|
500
|
+
return { ...owner.transfer(), tokens, ns };
|
|
501
|
+
});
|
|
502
|
+
if (result) {
|
|
503
|
+
if (this.promoteRestores) {
|
|
504
|
+
// Cache the restored prefix immediately. A second request can
|
|
505
|
+
// reuse RAM even before this borrower finishes its generation.
|
|
506
|
+
const share = makeSharedRetain(result.retain);
|
|
507
|
+
result.retain = share.acquire();
|
|
508
|
+
const retained = share.acquire();
|
|
509
|
+
let clones: Cache[] = [];
|
|
510
|
+
let attachments: CheckpointAttachment[] | undefined;
|
|
511
|
+
try {
|
|
512
|
+
clones = this.#clone(result.caches);
|
|
513
|
+
attachments = cloneAttachments(result.attachments);
|
|
514
|
+
this.put([...result.tokens], clones, ns, retained, attachments, sessionId);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
cleanupFailure(error, () => disposeResources([...clones,
|
|
517
|
+
{ dispose: () => disposeAttachments(attachments) },
|
|
518
|
+
{ dispose: retained }, { dispose: () => this.#disposeEntry(result, false) }]));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (bestIdx === -1) return null;
|
|
528
|
+
// PREFIX SHARING (non-consuming serve): the caller gets zero-copy
|
|
529
|
+
// CLONES of the donor's caches, trimmed to the matched prefix; the
|
|
530
|
+
// donor stays in the cache untouched, ready to serve the next agent /
|
|
531
|
+
// session with the same prefix. Safe because mlx cache updates are
|
|
532
|
+
// FUNCTIONAL (updateAndFetch reassigns fresh arrays; buffer donation
|
|
533
|
+
// only fires at refcount 1, and the donor always holds a ref), so a
|
|
534
|
+
// clone extending itself can never mutate the donor's bytes. The old
|
|
535
|
+
// consume-and-trim semantics CANNIBALIZED donors: agent B borrowing
|
|
536
|
+
// agent A's 2k system prompt destroyed A's 10k entry to do it.
|
|
537
|
+
const rec = this.#entries[bestIdx]!;
|
|
538
|
+
this.#associate(sessionId, ns, rec);
|
|
539
|
+
return this.#borrow(rec, bestLen, ns);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
#borrow(rec: EntryRecord, bestLen: number, ns: string): PromptCacheEntry {
|
|
543
|
+
rec.lastUsed = ++this.#clock;
|
|
544
|
+
rec.lastUsedMs = Date.now();
|
|
545
|
+
rec.uses++; rec.cost = bestLen; this.retention.accessed(rec);
|
|
546
|
+
// Lend immutable views for every backend state. RAM retention and SSD
|
|
547
|
+
// durability remain cache responsibilities while the caller extends its
|
|
548
|
+
// own views. This keeps synchronous persistence out of a RAM hit.
|
|
549
|
+
const clones = this.#clone(rec.entry.caches);
|
|
550
|
+
let attachments: CheckpointAttachment[] | undefined;
|
|
551
|
+
const trimNeeded = rec.entry.tokens.length - bestLen;
|
|
552
|
+
try {
|
|
553
|
+
attachments = cloneAttachments(rec.entry.attachments);
|
|
554
|
+
if (trimNeeded > 0) for (const c of clones) c.trim(trimNeeded);
|
|
555
|
+
} catch (error) {
|
|
556
|
+
cleanupFailure(error, () => disposeResources([...clones, { dispose: () => disposeAttachments(attachments) }]));
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
tokens: rec.entry.tokens.slice(0, bestLen),
|
|
560
|
+
caches: clones,
|
|
561
|
+
attachments,
|
|
562
|
+
ns,
|
|
563
|
+
retain: rec.share.acquire(),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Longest usable prefix length available for `prompt` WITHOUT taking
|
|
568
|
+
* (the SSD tier compares this against its own best before restoring). */
|
|
569
|
+
peekPrefixLen(prompt: number[], ns = ""): number {
|
|
570
|
+
let best = 0;
|
|
571
|
+
for (const { entry: e } of this.#entries) {
|
|
572
|
+
if (e.ns !== ns) continue;
|
|
573
|
+
const p = Math.min(commonPrefixLength(e.tokens, prompt), prompt.length - 1);
|
|
574
|
+
if (p <= best || p < minimumReusableOffset(e.caches)) continue;
|
|
575
|
+
const trimNeeded = e.tokens.length - p; // same usability rule as take()
|
|
576
|
+
if (trimNeeded > 0 && (!!e.attachments?.length || !e.caches.every((c) => c.isTrimmable()))) continue;
|
|
577
|
+
best = p;
|
|
578
|
+
}
|
|
579
|
+
return best;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** Read-only lookup of an immutable, cache-owned checkpoint. */
|
|
583
|
+
findExact(tokens: number[], ns = ""): PromptCacheEntry | null {
|
|
584
|
+
for (const { entry: e } of this.#entries) {
|
|
585
|
+
if (e.ns !== ns || e.tokens.length !== tokens.length) continue;
|
|
586
|
+
if (commonPrefixLength(e.tokens, tokens) === tokens.length) return e;
|
|
587
|
+
}
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Insert (or reinsert) an entry; evicts LRU entries over the byte cap
|
|
592
|
+
* (spilling each to the cold tier first, when one is attached). If the
|
|
593
|
+
* entry itself exceeds the cap it is spilled + disposed, not stored. */
|
|
594
|
+
put(tokens: number[], caches: Cache[], ns = "", retain?: () => void, attachments?: CheckpointAttachment[], sessionId?: string): void {
|
|
595
|
+
// All cache-provided validation runs before ownership is adopted.
|
|
596
|
+
const bytes = cacheBytes(caches) + attachmentBytes(attachments);
|
|
597
|
+
const trimmable = !attachments?.length && caches.every((cache) => cache.isTrimmable());
|
|
598
|
+
// Storage takes resolved snapshots, independently of their producer.
|
|
599
|
+
this.materialize(caches, attachments);
|
|
600
|
+
const discard = (entry: PromptCacheEntry, spill: boolean) => {
|
|
601
|
+
try { this.#disposeEntry(entry, spill); }
|
|
602
|
+
catch (error) {
|
|
603
|
+
// The new entry is already owned here. Throwing back to its previous
|
|
604
|
+
// owner would invite disposal of state still held by this store.
|
|
605
|
+
console.warn(`[prompt-cache] entry cleanup failed: ${error}`);
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
const entry: PromptCacheEntry = { tokens, caches, ns, retain, attachments };
|
|
609
|
+
if (bytes > this.maxBytes && !this.canEvict) {
|
|
610
|
+
discard(entry, true);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
// The donor's retain becomes ref-counted so take()'s clones can hold
|
|
614
|
+
// shares; the entry's own retain is the donor's share.
|
|
615
|
+
const share = makeSharedRetain(retain);
|
|
616
|
+
entry.retain = share.acquire();
|
|
617
|
+
const rec: EntryRecord = { entry, bytes, cost: tokens.length, uses: 1, priority: 0, lastUsed: ++this.#clock, lastUsedMs: Date.now(), share, resident: true, sessions: 0 };
|
|
618
|
+
this.retention.accessed(rec);
|
|
619
|
+
this.#entries.push(rec);
|
|
620
|
+
this.#records.set(entry, rec);
|
|
621
|
+
this.#associate(sessionId, ns, rec);
|
|
622
|
+
// Exact duplicates (equal tokens) are redundant REGARDLESS of
|
|
623
|
+
// trimmability — the new entry serves exactly the matches the old one
|
|
624
|
+
// did (mlx-lm's trie replaces in place). Without this, every gemma ctx
|
|
625
|
+
// repeat left another full-size wrapped-ring entry behind (measured
|
|
626
|
+
// ~0.4 GB retained per repeat on the 12B bench).
|
|
627
|
+
for (const old of [...this.#entries]) {
|
|
628
|
+
if (old === rec || old.entry.ns !== ns) continue;
|
|
629
|
+
if (old.entry.tokens.length !== tokens.length) continue;
|
|
630
|
+
if (commonPrefixLength(old.entry.tokens, tokens) !== tokens.length) continue;
|
|
631
|
+
// Equivalent publication replaces the cache object, not the checkpoint
|
|
632
|
+
// identity. Every session sharing it follows the replacement.
|
|
633
|
+
if (old.sessions) for (const sessions of this.#sessions.values()) {
|
|
634
|
+
if (sessions.get(ns) !== old) continue;
|
|
635
|
+
sessions.set(ns, rec); old.sessions--; rec.sessions++;
|
|
636
|
+
}
|
|
637
|
+
this.#entries = this.#entries.filter((r) => r !== old);
|
|
638
|
+
discard(old.entry, false);
|
|
639
|
+
}
|
|
640
|
+
// SUPERSEDE strict prefix-ancestors (the SSD store's rule, brought to
|
|
641
|
+
// RAM — prefix sharing would otherwise grow an entry per turn): same-ns
|
|
642
|
+
// entries whose tokens are a prefix of the new entry are redundant
|
|
643
|
+
// and disposed WITHOUT spill (the new entry serves their prefixes and
|
|
644
|
+
// its own write-behind persists them) — but ONLY when the new entry's
|
|
645
|
+
// caches are all trimmable. An untrimmable new entry (wrapped ring)
|
|
646
|
+
// can only serve exact-length matches, so shorter ancestors — e.g.
|
|
647
|
+
// the prompt-boundary snapshot that makes drifted multi-turn agents
|
|
648
|
+
// hit — must survive.
|
|
649
|
+
if (trimmable) {
|
|
650
|
+
for (const old of [...this.#entries]) {
|
|
651
|
+
if (old === rec || old.entry.ns !== ns) continue;
|
|
652
|
+
if (old.entry.tokens.length >= tokens.length || old.entry.tokens.length < minimumReusableOffset(caches)) continue;
|
|
653
|
+
if (commonPrefixLength(old.entry.tokens, tokens) !== old.entry.tokens.length) continue;
|
|
654
|
+
this.#entries = this.#entries.filter((r) => r !== old);
|
|
655
|
+
discard(old.entry, false);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
this.evictToBudget();
|
|
659
|
+
try { this.onPut?.(tokens, ns); } catch {}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Evict entries older than idleMs. Tiered residency policy first ensures
|
|
663
|
+
* SSD coverage. No-op without a cold tier. Returns entries removed from RAM. */
|
|
664
|
+
demoteIdle(idleMs: number, now = Date.now()): number {
|
|
665
|
+
if (!this.#cold) return 0;
|
|
666
|
+
let n = 0;
|
|
667
|
+
for (const rec of [...this.#entries]) {
|
|
668
|
+
if (now - rec.lastUsedMs < idleMs || this.#pins.has(rec.entry)) continue;
|
|
669
|
+
if (this.canEvict && !this.canEvict(rec.entry)) continue;
|
|
670
|
+
this.#entries = this.#entries.filter((r) => r !== rec);
|
|
671
|
+
this.#disposeEntry(rec.entry, !this.canEvict); // persisted entries need no duplicate write
|
|
672
|
+
this.demotions++;
|
|
673
|
+
n++;
|
|
674
|
+
}
|
|
675
|
+
return n;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
clear(): void {
|
|
679
|
+
this.#sessions.clear();
|
|
680
|
+
this.#epoch++;
|
|
681
|
+
const entries = this.#entries;
|
|
682
|
+
this.#entries = [];
|
|
683
|
+
disposeResources(entries.map(({ entry }) => ({ dispose: () => this.#disposeEntry(entry, false) })));
|
|
684
|
+
}
|
|
685
|
+
}
|