@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,860 @@
|
|
|
1
|
+
import type { MlxCacheLayer, MlxMixedRange, MlxSequenceRange } from "../../contracts/mlx/graph";
|
|
2
|
+
// Gemma 4 text model — line-for-line port of mlx-lm's gemma4_text.py
|
|
3
|
+
// (oracle venv site-packages), covering the paths our target models
|
|
4
|
+
// exercise: 12B (dense), e4b (per-layer-input embeddings + KV-shared
|
|
5
|
+
// layers), 26B-A4B (MoE block: router + gather_qmm experts).
|
|
6
|
+
//
|
|
7
|
+
// Parity notes:
|
|
8
|
+
// - SDPA scale is 1.0 (Gemma4 normalizes q/k instead).
|
|
9
|
+
// - Full-attention layers: global_head_dim 512, 1 global KV head,
|
|
10
|
+
// attention_k_eq_v (V = same projection as K, with un-scaled RMS norm);
|
|
11
|
+
// ProportionalRoPE rotates only partial_rotary_factor·dims dims.
|
|
12
|
+
// - Python-float scalars promote weakly to the array dtype.
|
|
13
|
+
// - Replicate mlx python helper implementations exactly (x**3 is
|
|
14
|
+
// mx.power, not x·x·x — they round differently in bf16).
|
|
15
|
+
//
|
|
16
|
+
// Reads: every attention block reads its layer's cache through the read the
|
|
17
|
+
// caller's phase names (contracts/mlx/cache.ts AttentionCache), so the cache
|
|
18
|
+
// builds the mask (sliding layers compose RotatingKVCache, whose reads own the
|
|
19
|
+
// window) and attends with the kernel it was composed with. The graph never
|
|
20
|
+
// inspects the query count, builds a mask or chooses a kernel, the training
|
|
21
|
+
// forward (gradient checkpointing, prefix-shared and segmented training)
|
|
22
|
+
// included: it reads its stateless training caches the same way. The blocks
|
|
23
|
+
// (attention, feed-forward, per-layer inputs, KV sharing, decoder layer) live in
|
|
24
|
+
// blocks.ts, which the written graph files compose too.
|
|
25
|
+
|
|
26
|
+
import { disposeResources } from "../../runtime/resources";
|
|
27
|
+
import type { MlxArray } from "@mlx-bun/mlx/array";
|
|
28
|
+
import * as ops from "@mlx-bun/mlx/ops";
|
|
29
|
+
import type { ModelConfig } from "../../artifacts/config";
|
|
30
|
+
import type { Weights } from "../../artifacts/weights";
|
|
31
|
+
import { Checkpoint } from "@mlx-bun/mlx/checkpoint";
|
|
32
|
+
import { LoraState } from "../../layers/lora";
|
|
33
|
+
import type { GraphDeclaration } from "../../contracts/portable/graph";
|
|
34
|
+
import type { MlxCapturingForward, MlxRowTable, MlxCompiledDecodeStep, MlxDeclaredGraph, MlxHeadForward, MlxLayerCapture, MlxMixedForward, MlxPhaseForward, MlxKnownOperations, MlxPromptRange, MlxTokenPhases, MlxVerifyForward } from "../../contracts/mlx/graph";
|
|
35
|
+
import { CompiledDecode } from "./compiled-decode";
|
|
36
|
+
import type { TargetView } from "../../contracts/mlx/draft-target";
|
|
37
|
+
import type { TrainableGraph } from "../../contracts/mlx/trainable";
|
|
38
|
+
import { gemma4Trainable, type GradCheckpointCtx, type LayerLoras } from "./trainable";
|
|
39
|
+
import type { MediaEncoders, MediaSidecarProbes, MlxPromptInput, Vision } from "../../contracts/mlx/media";
|
|
40
|
+
import { declareGraph } from "../capabilities";
|
|
41
|
+
import { DEFAULT_PHASE_ROWS, rowTable } from "../graph";
|
|
42
|
+
import { bindGemma4MediaInput, gemma4DraftTarget, gemma4MediaEncoders } from "./media";
|
|
43
|
+
import type { AttentionRead, Cache, SharedKv, TrainingAttentionCache } from "../../contracts/mlx/cache";
|
|
44
|
+
import { CompiledGegluMLP, PlainGegluMLP, CompiledGeglu } from "../../layers/geglu";
|
|
45
|
+
import { ServingDenseLayer, ServingMoeLayer } from "./serving-blocks";
|
|
46
|
+
import { FixedExperts, SortedSwitchGLU, UnsortedSwitchGLU } from "./blocks";
|
|
47
|
+
import type { MlxEmbeddingsRange } from "../../contracts/mlx/media";
|
|
48
|
+
import type { MlxSeparateMixed, MlxPhaseGroup } from "../../contracts/mlx/graph";
|
|
49
|
+
import { mapPackedTokens } from "../../input/token-groups";
|
|
50
|
+
import { disposing } from "../../layers/helpers";
|
|
51
|
+
import { RMSNorm } from "../../layers/normalization";
|
|
52
|
+
import { QuantizedEmbedding } from "../../layers/quantized-embedding";
|
|
53
|
+
import type { QuantizedLinear } from "../../layers/quantized-linear";
|
|
54
|
+
import { logitSoftcap } from "../../kernels/logits";
|
|
55
|
+
import { compiledLogitSoftcap } from "../../kernels/softcap";
|
|
56
|
+
import { AttentionMasks } from "../../state/attention-read";
|
|
57
|
+
import { composedFullAttentionCache, composedSlidingAttentionCache } from "../../state/bf16-first-kv";
|
|
58
|
+
import { deferredTurboQuantKernels } from "../../state/turboquant-attention";
|
|
59
|
+
import {
|
|
60
|
+
DecoderLayer, kvDonors, PerLayerInputs, type Attention, type DonorRead, type Gemma4Phase,
|
|
61
|
+
} from "./blocks";
|
|
62
|
+
|
|
63
|
+
/** A donor layer's keys and values in the training forward: the keys (RoPE
|
|
64
|
+
* applied) and values it appended to its training cache, which its sharers read
|
|
65
|
+
* through that cache and which cross checkpoint and segment boundaries as
|
|
66
|
+
* tensors. */
|
|
67
|
+
type DecoderOperation = Pick<DecoderLayer,"forward"|"forwardAttn"|"forwardMlp">;
|
|
68
|
+
|
|
69
|
+
type PlainSharedKv = Extract<SharedKv, { kind: "plain" }>;
|
|
70
|
+
|
|
71
|
+
/** The training forward's attention (`Gemma4Model.forwardTrainingHidden`,
|
|
72
|
+
* gradient checkpointing and the segmented backward) over `attn`'s
|
|
73
|
+
* projections, roped block-wise under a prefix-shared layout. `cache` is the
|
|
74
|
+
* training cache of this layer, or of its donor for a KV-sharing layer; it
|
|
75
|
+
* stores nothing (`TrainingCache`, `PrefixSharedCache`), so its window read
|
|
76
|
+
* of the keys and values handed to it owns the mask (causal, windowed, padded
|
|
77
|
+
* or block-sparse) and the kernel the run composed, and a sharer reads its
|
|
78
|
+
* donor's keys and values (`sharedIn`) by appending them to the donor's cache
|
|
79
|
+
* again. Returns the attention output and the donor's keys and values (a
|
|
80
|
+
* sharer returns `sharedIn`); the caller owns those of a donor and disposes
|
|
81
|
+
* them after the pass. */
|
|
82
|
+
function attentionTraining(
|
|
83
|
+
attn: Attention, x: MlxArray, cache: TrainingAttentionCache, sharedIn: PlainSharedKv | null,
|
|
84
|
+
): { out: MlxArray; shared: PlainSharedKv } {
|
|
85
|
+
const [B, L] = x.shape as [number, number, number];
|
|
86
|
+
|
|
87
|
+
let q = attn.qProj.forward(x);
|
|
88
|
+
q = disposing(q, ops.reshape(q, [B, L, attn.nHeads, attn.headDim]));
|
|
89
|
+
q = disposing(q, attn.qNorm.forward(q));
|
|
90
|
+
|
|
91
|
+
let shared: PlainSharedKv;
|
|
92
|
+
let read: AttentionRead;
|
|
93
|
+
if (!attn.hasKv) {
|
|
94
|
+
if (!sharedIn) throw new Error("KV-shared layer received no shared KV");
|
|
95
|
+
shared = sharedIn;
|
|
96
|
+
read = cache.appendWindow(shared.keys, shared.values);
|
|
97
|
+
} else {
|
|
98
|
+
let k = attn.kProj!.forward(x);
|
|
99
|
+
k = disposing(k, ops.reshape(k, [B, L, attn.nKvHeads, attn.headDim]));
|
|
100
|
+
let v: MlxArray;
|
|
101
|
+
if (attn.vProj) {
|
|
102
|
+
v = attn.vProj.forward(x);
|
|
103
|
+
v = disposing(v, ops.reshape(v, [B, L, attn.nKvHeads, attn.headDim]));
|
|
104
|
+
} else {
|
|
105
|
+
v = k; // attention_k_eq_v: shared projection; norms differ below
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const kNormed = attn.kNorm!.forward(k);
|
|
109
|
+
const kT = ops.transposeAxes(kNormed, [0, 2, 1, 3]);
|
|
110
|
+
kNormed.dispose();
|
|
111
|
+
const kRoped = cache.ropePositions(kT, (block, offset) => attn.rope(block, offset));
|
|
112
|
+
kT.dispose();
|
|
113
|
+
|
|
114
|
+
const vNormed = attn.vNorm!.forward(v);
|
|
115
|
+
const vT = ops.transposeAxes(vNormed, [0, 2, 1, 3]);
|
|
116
|
+
vNormed.dispose();
|
|
117
|
+
if (v !== k) v.dispose();
|
|
118
|
+
k.dispose();
|
|
119
|
+
|
|
120
|
+
try { read = cache.appendWindow(kRoped, vT); }
|
|
121
|
+
catch (error) { kRoped.dispose(); vT.dispose(); throw error; }
|
|
122
|
+
shared = { kind: "plain", keys: kRoped, values: vT, offset: cache.offset };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let out: MlxArray;
|
|
126
|
+
try {
|
|
127
|
+
q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
|
|
128
|
+
q = disposing(q, cache.ropePositions(q, (block, offset) => attn.rope(block, offset)));
|
|
129
|
+
out = read.attend(q, 1.0);
|
|
130
|
+
} finally { read.dispose(); }
|
|
131
|
+
q.dispose();
|
|
132
|
+
const attnT = ops.transposeAxes(out, [0, 2, 1, 3]);
|
|
133
|
+
out.dispose();
|
|
134
|
+
const merged = ops.reshape(attnT, [B, L, -1]);
|
|
135
|
+
attnT.dispose();
|
|
136
|
+
return { out: disposing(merged, attn.oProj.forward(merged)), shared };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The training forward's attention sub-block of `layer` (see
|
|
140
|
+
* `DecoderLayer.forwardAttn`): inputNorm → `attentionTraining` → postAttnNorm →
|
|
141
|
+
* residual add. */
|
|
142
|
+
function layerAttnTraining(
|
|
143
|
+
layer: DecoderLayer, x: MlxArray, cache: TrainingAttentionCache, sharedIn: PlainSharedKv | null,
|
|
144
|
+
): { h: MlxArray; shared: PlainSharedKv } {
|
|
145
|
+
let h = layer.inputNorm.forward(x);
|
|
146
|
+
const { out, shared } = attentionTraining(layer.attn, h, cache, sharedIn);
|
|
147
|
+
h.dispose();
|
|
148
|
+
h = out;
|
|
149
|
+
h = disposing(h, layer.postAttnNorm.forward(h));
|
|
150
|
+
h = disposing(h, ops.add(x, h));
|
|
151
|
+
return { h, shared };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The training forward's layer (see `attentionTraining`). The split into two
|
|
155
|
+
* sub-blocks lets the checkpoint path rematerialize attention and MLP
|
|
156
|
+
* separately (peak attn+MLP → max(attn,MLP)+hMid). */
|
|
157
|
+
function layerTraining(
|
|
158
|
+
layer: DecoderLayer, x: MlxArray, cache: TrainingAttentionCache,
|
|
159
|
+
sharedIn: PlainSharedKv | null, perLayerInput: MlxArray | null,
|
|
160
|
+
): { h: MlxArray; shared: PlainSharedKv } {
|
|
161
|
+
const { h: hMid, shared } = layerAttnTraining(layer, x, cache, sharedIn);
|
|
162
|
+
const h = layer.forwardMlp(hMid, perLayerInput);
|
|
163
|
+
hMid.dispose();
|
|
164
|
+
return { h, shared };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export class Gemma4Model implements MlxDeclaredGraph, MlxTokenPhases {
|
|
168
|
+
#disposed = false;
|
|
169
|
+
readonly config: ModelConfig;
|
|
170
|
+
/** Total weight-shard bytes (for the conditional wired-limit scope). */
|
|
171
|
+
readonly weightsBytes: number;
|
|
172
|
+
/** Weight-name prefix ("language_model.model" or "model"). */
|
|
173
|
+
readonly prefixBase: string;
|
|
174
|
+
/** Active LoRA adapters for the current generation (see LoraState). */
|
|
175
|
+
readonly loraState = new LoraState();
|
|
176
|
+
/** Prepared image/audio embeddings, layer taps for drafts, and a compiled
|
|
177
|
+
* decode step, except through experts: shapeless replay
|
|
178
|
+
* retraces their growing windows. */
|
|
179
|
+
get graphCapabilities(): GraphDeclaration {
|
|
180
|
+
return declareGraph({
|
|
181
|
+
media: { input: "embeddings", video: false }, hiddenLayerTaps: true,
|
|
182
|
+
compiledDecode: this.config?.modelType?.startsWith("gemma4") === true && !this.config.text.enableMoeBlock,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
#trainable: TrainableGraph | undefined;
|
|
186
|
+
/** Segmented backward with KV-shared donor threading, prefix-shared ORPO,
|
|
187
|
+
* gradient checkpointing, and the quantized tied head. Flash training
|
|
188
|
+
* attention is refused: e4b crashes natively on it at long sequences. */
|
|
189
|
+
get trainable(): TrainableGraph { return this.#trainable ??= gemma4Trainable(this); }
|
|
190
|
+
bindMediaInput(input: Vision): MlxPromptInput { return bindGemma4MediaInput(this, input); }
|
|
191
|
+
mediaEncoders(modelDir: string, probes: MediaSidecarProbes): Promise<MediaEncoders> {
|
|
192
|
+
return gemma4MediaEncoders(this, modelDir, probes);
|
|
193
|
+
}
|
|
194
|
+
draftTarget(caches: Cache[]): TargetView { return gemma4DraftTarget(this, caches); }
|
|
195
|
+
/** The compiled decode step declared above: the step records this graph's own
|
|
196
|
+
* forward, so it shares the graph's weights and constants. */
|
|
197
|
+
compiledDecodeStep(): MlxCompiledDecodeStep { return CompiledDecode.for(this); }
|
|
198
|
+
releaseCompiledDecode(): void { CompiledDecode.release(this); }
|
|
199
|
+
readonly embed: QuantizedEmbedding;
|
|
200
|
+
readonly layers: DecoderLayer[];
|
|
201
|
+
readonly finalNorm: RMSNorm;
|
|
202
|
+
readonly embedScale: number;
|
|
203
|
+
readonly windowSize: number;
|
|
204
|
+
/** Donor count: layers [0, numDonors) own caches; the rest share. */
|
|
205
|
+
readonly numDonors: number;
|
|
206
|
+
/** layer idx → donor layer idx whose fetched KV it consumes (self for donors). */
|
|
207
|
+
readonly previousKvs: number[];
|
|
208
|
+
/** layer idx → cache index (donors only; -1 for sharers). */
|
|
209
|
+
readonly cacheIndex: number[];
|
|
210
|
+
/** Set by the trainer around its training forwards to checkpoint each layer
|
|
211
|
+
* (null = off). While set, the scoring and training forward
|
|
212
|
+
* (`sequenceHidden`) runs the training forward (`forwardTrainingHidden`'s),
|
|
213
|
+
* whose donors hand their sharers keys and values as checkpoint tensors. */
|
|
214
|
+
gradCkpt: GradCheckpointCtx | null = null;
|
|
215
|
+
// e2b/e4b per-layer input machinery (the block and its parts)
|
|
216
|
+
readonly perLayerInputs: PerLayerInputs | null;
|
|
217
|
+
readonly perLayerEmbed: QuantizedEmbedding | null;
|
|
218
|
+
readonly perLayerModelProjection: QuantizedLinear | null;
|
|
219
|
+
readonly perLayerProjectionNorm: RMSNorm | null;
|
|
220
|
+
readonly perLayerWidth: number;
|
|
221
|
+
readonly #servingLayers: readonly ServingDenseLayer[];
|
|
222
|
+
readonly #sortedLayers: readonly ServingDenseLayer[];
|
|
223
|
+
readonly traceLayers: readonly ServingDenseLayer[];
|
|
224
|
+
readonly traceDecode: MlxCapturingForward;
|
|
225
|
+
readonly #ordinarySlice: PerLayerInputs["ordinaryLayer"] | undefined;
|
|
226
|
+
readonly #traceSlice: PerLayerInputs["traceLayer"] | undefined;
|
|
227
|
+
readonly traceLogits = (hidden: MlxArray) => this.#head(hidden, logitSoftcap);
|
|
228
|
+
readonly mediaPrefill: readonly MlxEmbeddingsRange[];
|
|
229
|
+
readonly embeddingPrompt: readonly MlxEmbeddingsRange[];
|
|
230
|
+
readonly mixed: readonly MlxMixedRange[];
|
|
231
|
+
readonly mixedSeparate: MlxSeparateMixed;
|
|
232
|
+
constructor(weights: Weights, config: ModelConfig, maxPhaseRows = DEFAULT_PHASE_ROWS) {
|
|
233
|
+
const t = config.text;
|
|
234
|
+
if (t.hiddenSize === 0 || !config.text.tieWordEmbeddings)
|
|
235
|
+
throw new Error("only tied-embedding text configs are supported");
|
|
236
|
+
|
|
237
|
+
this.config = config;
|
|
238
|
+
this.weightsBytes = [...weights.shards.files.values()]
|
|
239
|
+
.reduce((a, f) => a + f.mmap.size, 0);
|
|
240
|
+
// gemma4_unified prefixes weights with language_model.; gemma4_text doesn't
|
|
241
|
+
const prefixBase = weights.has("language_model.model.embed_tokens.weight")
|
|
242
|
+
? "language_model.model" : "model";
|
|
243
|
+
this.prefixBase = prefixBase;
|
|
244
|
+
this.embed = QuantizedEmbedding.load(weights, `${prefixBase}.embed_tokens`, config);
|
|
245
|
+
|
|
246
|
+
this.numDonors = t.numHiddenLayers - t.numKvSharedLayers;
|
|
247
|
+
this.layers = Array.from(
|
|
248
|
+
{ length: t.numHiddenLayers },
|
|
249
|
+
(_, i) => new DecoderLayer(weights, config, prefixBase, i, i < this.numDonors, true),
|
|
250
|
+
);
|
|
251
|
+
// previous_kvs (reference Gemma4TextModel): sharer j uses the LAST
|
|
252
|
+
// donor of its own layer type
|
|
253
|
+
this.previousKvs = kvDonors(this.layers.map(layer => layer.layerType), this.numDonors);
|
|
254
|
+
this.cacheIndex = this.layers.map((_, i) => (i < this.numDonors ? i : -1));
|
|
255
|
+
|
|
256
|
+
this.finalNorm = new RMSNorm(
|
|
257
|
+
weights.tensor(`${prefixBase}.norm.weight`),
|
|
258
|
+
t.rmsNormEps,
|
|
259
|
+
);
|
|
260
|
+
this.embedScale = Math.sqrt(t.hiddenSize);
|
|
261
|
+
this.windowSize = t.slidingWindow;
|
|
262
|
+
|
|
263
|
+
this.perLayerInputs = t.hiddenSizePerLayerInput > 0 ? new PerLayerInputs(weights, config, prefixBase) : null;
|
|
264
|
+
this.perLayerEmbed = this.perLayerInputs?.embed ?? null;
|
|
265
|
+
this.perLayerModelProjection = this.perLayerInputs?.modelProjection ?? null;
|
|
266
|
+
this.perLayerProjectionNorm = this.perLayerInputs?.projectionNorm ?? null;
|
|
267
|
+
this.perLayerWidth = this.perLayerInputs?.width ?? 0;
|
|
268
|
+
this.#servingLayers = this.layers.map(layer => layer.experts
|
|
269
|
+
? new ServingMoeLayer(layer, new CompiledGegluMLP(layer.mlp), new FixedExperts(new UnsortedSwitchGLU(layer.experts.switchGlu, new CompiledGeglu())))
|
|
270
|
+
: new ServingDenseLayer(layer, new CompiledGegluMLP(layer.mlp)));
|
|
271
|
+
this.#sortedLayers = this.layers.map(layer => layer.experts
|
|
272
|
+
? new ServingMoeLayer(layer, new CompiledGegluMLP(layer.mlp), new FixedExperts(new SortedSwitchGLU(layer.experts.switchGlu, new CompiledGeglu())))
|
|
273
|
+
: new ServingDenseLayer(layer, new CompiledGegluMLP(layer.mlp)));
|
|
274
|
+
this.#ordinarySlice = this.perLayerInputs?.ordinaryLayer.bind(this.perLayerInputs);
|
|
275
|
+
this.#traceSlice = this.perLayerInputs?.traceLayer.bind(this.perLayerInputs);
|
|
276
|
+
this.traceLayers = this.layers.map(layer => new ServingDenseLayer(layer, new PlainGegluMLP(layer.mlp)));
|
|
277
|
+
this.traceDecode = (ids,cache,capture) => {
|
|
278
|
+
const h = this.forwardDecoder(this.embedTokens(ids),cache,"decode",ids,capture,this.traceLayers,this.#traceSlice);
|
|
279
|
+
capture?.(this.layers.length,h); return h;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Artifact top-k determines the reference gather-sort boundary. Each
|
|
283
|
+
// declared operation already owns its concrete expert and activation form.
|
|
284
|
+
const boundary = t.enableMoeBlock ? Math.ceil(64 / t.topKExperts) : Infinity;
|
|
285
|
+
const views = [{from:1,to:boundary-1,layers:this.#servingLayers}, {from:boundary,to:Infinity,layers:this.#sortedLayers}].filter(view=>Number.isFinite(view.from) && view.from<=view.to);
|
|
286
|
+
this.prompt = views.map(view => ({from:view.from,to:view.to,operation:(ids,cache,capture)=>this.#window(ids,cache,capture,view.layers)}));
|
|
287
|
+
this.promptChunk = this.prompt;
|
|
288
|
+
this.multiSequencePrompt = this.prompt;
|
|
289
|
+
this.multiSequencePromptChunk = this.multiSequencePrompt;
|
|
290
|
+
const decode: Record<number,MlxCapturingForward> = {}, verify: Record<number,MlxCapturingForward> = {};
|
|
291
|
+
for (let rows=1; rows<=maxPhaseRows; rows++) {
|
|
292
|
+
const layers = rows < boundary ? this.#servingLayers : this.#sortedLayers;
|
|
293
|
+
decode[rows] = (ids,cache,capture) => {
|
|
294
|
+
const hidden = this.forwardDecoder(this.embedTokens(ids),cache,"decode",ids,capture,layers);
|
|
295
|
+
capture?.(this.layers.length,hidden); return hidden;
|
|
296
|
+
};
|
|
297
|
+
verify[rows] = (ids,cache,capture) => this.#window(ids,cache,capture,layers);
|
|
298
|
+
}
|
|
299
|
+
this.decode = decode; this.verify = verify; this.appendKnown = views.map(view=>({from:view.from,to:view.to,operation:(ids,cache)=>this.#window(ids,cache,null,view.layers)}));
|
|
300
|
+
this.logits = rowTable<MlxHeadForward>(hidden=>this.logitsFromHidden(hidden),maxPhaseRows);
|
|
301
|
+
this.sequence = views.map(view=>({from:view.from,to:view.to,operation:(ids,cache)=>this.gradCkpt
|
|
302
|
+
? this.#forwardTraining(this.embedTokens(ids),cache,ids)
|
|
303
|
+
: this.forwardDecoder(this.embedTokens(ids),cache,"window",ids,null,view.layers)}));
|
|
304
|
+
this.mediaPrefill = views.map(view=>({from:view.from,to:view.to,operation:(embeddings,cache,imageMask,ids,multimodalMask)=>
|
|
305
|
+
this.#prefillEmbeddings(embeddings,cache,imageMask,ids,multimodalMask,view.layers)}));
|
|
306
|
+
this.embeddingPrompt = this.mediaPrefill;
|
|
307
|
+
this.mixed = views.map(view=>({from:view.from,to:view.to,operation:groups=>this.#mixed(groups,view.layers)}));
|
|
308
|
+
const separate = views.map(view=>({from:view.from,to:view.to,operation:(group:MlxPhaseGroup)=>{
|
|
309
|
+
const hidden = this.forwardDecoder(this.embedTokens(group.ids),group.state,group.phase === "decode" ? "decode" : "window",group.ids,group.capture,view.layers);
|
|
310
|
+
group.capture?.(this.layers.length,hidden); return hidden;
|
|
311
|
+
}}));
|
|
312
|
+
this.mixedSeparate = {decode:separate,window:separate,verify:separate};
|
|
313
|
+
|
|
314
|
+
this.cacheLayers = this.layers.slice(0,this.numDonors).map(layer=>layer.layerType === "sliding_attention" ? {kind:"sliding" as const,window:this.windowSize}:{kind:"full" as const,turboQuant:"deferred" as const});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** LoRA-mountable linears, keyed by weight-file module path: optiq
|
|
318
|
+
* mount.py's 7 target suffixes PLUS the e2b/e4b per-layer-input
|
|
319
|
+
* projections — mlx-lm's trainer targets those on e4b and optiq's
|
|
320
|
+
* mount silently drops their trained weights (we apply every adapter
|
|
321
|
+
* weight we can map).
|
|
322
|
+
* Expert pools stay non-targets (LoRASwitchLinear is future work). */
|
|
323
|
+
loraTargets(): Map<string, QuantizedLinear> {
|
|
324
|
+
const out = new Map<string, QuantizedLinear>();
|
|
325
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
326
|
+
const l = this.layers[i]!;
|
|
327
|
+
const p = `${this.prefixBase}.layers.${i}`;
|
|
328
|
+
const add = (suffix: string, lin: QuantizedLinear | null) => {
|
|
329
|
+
if (lin) out.set(`${p}.${suffix}`, lin);
|
|
330
|
+
};
|
|
331
|
+
add("self_attn.q_proj", l.attn.qProj);
|
|
332
|
+
add("self_attn.k_proj", l.attn.kProj);
|
|
333
|
+
add("self_attn.v_proj", l.attn.vProj);
|
|
334
|
+
add("self_attn.o_proj", l.attn.oProj);
|
|
335
|
+
add("mlp.gate_proj", l.mlp.gate);
|
|
336
|
+
add("mlp.up_proj", l.mlp.up);
|
|
337
|
+
add("mlp.down_proj", l.mlp.down);
|
|
338
|
+
add("per_layer_input_gate", l.perLayerGate);
|
|
339
|
+
add("per_layer_projection", l.perLayerProjection);
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Caches for donor layers only (sharers attend their donors' reads), all
|
|
345
|
+
* sharing one mask memo, so each forward builds each mask once. Sliding
|
|
346
|
+
* layers compose a ring of the sliding window, whose reads own the window
|
|
347
|
+
* mask, and stay bf16 under TurboQuant. The full-attention caches' TurboQuant
|
|
348
|
+
* read is deferred (`deferredTurboQuantKernels`): values stay rotated through
|
|
349
|
+
* the fused SDPA and the read un-rotates the output. */
|
|
350
|
+
readonly cacheLayers: readonly MlxCacheLayer[];
|
|
351
|
+
readonly sequence: readonly MlxSequenceRange[];
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
/** Release model-owned graph constants after all executions and compiled
|
|
355
|
+
* runners have stopped. Weight tensors and hidden taps are borrowed. */
|
|
356
|
+
dispose(): void {
|
|
357
|
+
if (this.#disposed) return;
|
|
358
|
+
this.#disposed = true;
|
|
359
|
+
disposeResources(this.layers.flatMap(layer => layer.attn.ropeFreqs ? [layer.attn.ropeFreqs] : []));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Computed model constants must retain their constructor arithmetic when
|
|
363
|
+
* a restored request enters compiled decode without a prefill first. */
|
|
364
|
+
materializeGraphConstants(): void {
|
|
365
|
+
const constants = this.layers.flatMap(layer => layer.attn.ropeFreqs ? [layer.attn.ropeFreqs] : []);
|
|
366
|
+
if (constants.length) ops.evalAll(constants);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** The scaled token embeddings [B, L, hidden] of `ids` (owned). */
|
|
370
|
+
protected embedTokens(ids: MlxArray): MlxArray {
|
|
371
|
+
const h = this.embed.encode(ids);
|
|
372
|
+
return disposing(h, ops.mulScalar(h, this.embedScale));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** One whole chunk of a prompt: tokens per sequence equal the composition's
|
|
376
|
+
* prefill chunk size; rows are sequences times that size. Token ids only: a
|
|
377
|
+
* prompt with prepared media enters through the `MlxPromptInput` that
|
|
378
|
+
* `bindMediaInput` returns. The GPU is this graph's one unit. */
|
|
379
|
+
readonly prompt: readonly MlxPromptRange[];
|
|
380
|
+
readonly promptChunk: typeof this.prompt;
|
|
381
|
+
readonly multiSequencePromptChunk: typeof this.multiSequencePrompt;
|
|
382
|
+
readonly multiSequencePrompt: readonly MlxPromptRange[];
|
|
383
|
+
|
|
384
|
+
/** A prompt's positions that are not a whole chunk: the window read at every width. */
|
|
385
|
+
|
|
386
|
+
/** Known tokens: this graph declares no committed spans, so they read as a window. */
|
|
387
|
+
readonly appendKnown: MlxKnownOperations;
|
|
388
|
+
|
|
389
|
+
/** A causal window of token ids over every layer, `capture` seeing every layer and then the final hidden. */
|
|
390
|
+
#window(ids: MlxArray, cache: Cache[], capture: MlxLayerCapture | null, layers = this.#servingLayers): MlxArray {
|
|
391
|
+
const hidden = this.forwardDecoder(this.embedTokens(ids), cache, "window", ids, capture, layers);
|
|
392
|
+
capture?.(this.layers.length, hidden);
|
|
393
|
+
return hidden;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** The one output head at every width. */
|
|
397
|
+
readonly logits: MlxRowTable<MlxHeadForward>;
|
|
398
|
+
|
|
399
|
+
/** One new token per sequence: `ids` [sequences, 1], so rows are sequences. */
|
|
400
|
+
readonly decode: MlxRowTable<MlxCapturingForward>;
|
|
401
|
+
|
|
402
|
+
/** A speculative window: each sequence's last accepted token and its drafted
|
|
403
|
+
* tokens, right-padded by the caller to the round's depth, `ids`
|
|
404
|
+
* [sequences, depth + 1], so rows are sequences times (depth + 1). The graph
|
|
405
|
+
* appends all depth + 1 positions; dropping the rejected suffix is the
|
|
406
|
+
* caller's speculative transaction, begun before this call. */
|
|
407
|
+
readonly verify: MlxRowTable<MlxCapturingForward>;
|
|
408
|
+
|
|
409
|
+
/** Per-layer inputs (reference _get_per_layer_inputs +
|
|
410
|
+
* _project_per_layer_inputs): [B, L, nLayers, perLayerWidth] (`PerLayerInputs.compute`). */
|
|
411
|
+
protected computePerLayerInputs(ids: MlxArray, hScaled: MlxArray): MlxArray {
|
|
412
|
+
return this.perLayerInputs!.compute(ids, hScaled);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** The vision prefill phase: pre-merged (unscaled) input embeddings → hidden states. Used by the
|
|
416
|
+
* multimodal (vision/audio) path; `bidir` (bool [L]) marks image tokens,
|
|
417
|
+
* which attend bidirectionally among themselves (use_bidirectional_attention:
|
|
418
|
+
* "vision" — text stays causal; audio prompts pass NO bidir mask, and a
|
|
419
|
+
* mixed image+audio prompt drops the image overlay too,
|
|
420
|
+
* `02d723a:docs/design/generic-model-support.md` §6.6). `ids` ([1, L], the spliced token ids)
|
|
421
|
+
* is required for per-layer-input models (e2b/e4b): multimodal soft-token
|
|
422
|
+
* positions get token 0's per-layer embedding, and their tower content
|
|
423
|
+
* enters only through the projection term (the merged hidden). Matches
|
|
424
|
+
* optiq's frontend: `zeroed = where(text_mask, input_ids, 0)`.
|
|
425
|
+
*
|
|
426
|
+
* `multimodal` (bool [L]) is the per-layer-input ZEROING mask — the UNION
|
|
427
|
+
* of multimodal soft tokens (image | audio, §3.3 Q2), decoupled from the
|
|
428
|
+
* attention overlay: an audio-only prompt has no bidir mask but its audio
|
|
429
|
+
* positions must still be zeroed. When null (the legacy vision call
|
|
430
|
+
* shape), the zeroing mask falls back to `bidir` — vision passes the same
|
|
431
|
+
* image mask for both roles, so that path is unchanged. */
|
|
432
|
+
#prefillEmbeddings(
|
|
433
|
+
embeds: MlxArray, cache: Cache[], bidir: MlxArray | null,
|
|
434
|
+
ids: MlxArray | null = null, multimodal: MlxArray | null = null,
|
|
435
|
+
layers: readonly DecoderOperation[] = this.#servingLayers,
|
|
436
|
+
): MlxArray {
|
|
437
|
+
const h = ops.mulScalar(embeds, this.embedScale);
|
|
438
|
+
const phase: Gemma4Phase = bidir ? { bidirectional: bidir } : "window";
|
|
439
|
+
if (this.perLayerWidth === 0) return this.forwardDecoder(h, cache, phase, null, null, layers);
|
|
440
|
+
|
|
441
|
+
if (!ids)
|
|
442
|
+
throw new Error("per-layer-input multimodal models require token ids");
|
|
443
|
+
const zeroing = multimodal ?? bidir;
|
|
444
|
+
let plIds = ids;
|
|
445
|
+
if (zeroing) {
|
|
446
|
+
const cond = ops.reshape(zeroing, ids.shape);
|
|
447
|
+
const zero = ops.zeros(ids.shape, ids.dtype);
|
|
448
|
+
plIds = ops.where(cond, zero, ids);
|
|
449
|
+
cond.dispose();
|
|
450
|
+
zero.dispose();
|
|
451
|
+
}
|
|
452
|
+
const out = this.forwardDecoder(h, cache, phase, plIds, null, layers);
|
|
453
|
+
if (plIds !== ids) plIds.dispose();
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** The decoder over `h0` [B, L, hidden] (consumed), every donor layer's
|
|
458
|
+
* attention reading its cache for `phase` and every KV-sharing layer
|
|
459
|
+
* attending its donor's read. `ids` [B, L] feed the per-layer inputs
|
|
460
|
+
* (e2b/e4b). `capture` receives each layer's residual output after the layer
|
|
461
|
+
* runs (see `MlxLayerCapture`); the caller hands it the final hidden. */
|
|
462
|
+
protected forwardDecoder(
|
|
463
|
+
h0: MlxArray, cache: Cache[], phase: Gemma4Phase, ids: MlxArray | null,
|
|
464
|
+
capture: MlxLayerCapture | null = null, layers: readonly DecoderOperation[] = this.#servingLayers, slice = this.#ordinarySlice,
|
|
465
|
+
): MlxArray {
|
|
466
|
+
const B = h0.shape[0]!;
|
|
467
|
+
const L = h0.shape[1]!;
|
|
468
|
+
let h: MlxArray | null = h0;
|
|
469
|
+
let perLayer: MlxArray | null = null;
|
|
470
|
+
const donors: (DonorRead | null)[] = Array(this.numDonors).fill(null);
|
|
471
|
+
try {
|
|
472
|
+
// per-layer inputs (e2b/e4b)
|
|
473
|
+
if (this.perLayerWidth > 0) perLayer = this.computePerLayerInputs(ids!, h);
|
|
474
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
475
|
+
const ci = this.cacheIndex[i]!;
|
|
476
|
+
const pls = perLayer ? slice!(perLayer,i,B,L) : null;
|
|
477
|
+
let next: { h: MlxArray; donor: DonorRead | null };
|
|
478
|
+
try {
|
|
479
|
+
next = layers[i]!.forward(h, ci === -1 ? null : cache[ci]!, phase,
|
|
480
|
+
ci === -1 ? donors[this.previousKvs[i]!]! : null, pls);
|
|
481
|
+
} finally { pls?.dispose(); }
|
|
482
|
+
h.dispose();
|
|
483
|
+
h = next.h;
|
|
484
|
+
if (ci !== -1) donors[ci] = next.donor;
|
|
485
|
+
capture?.(i, h);
|
|
486
|
+
}
|
|
487
|
+
const out = this.finalNorm.forward(h);
|
|
488
|
+
h.dispose();
|
|
489
|
+
h = null;
|
|
490
|
+
return out;
|
|
491
|
+
} finally {
|
|
492
|
+
h?.dispose();
|
|
493
|
+
perLayer?.dispose();
|
|
494
|
+
// each donor's read exactly once, after its last sharer
|
|
495
|
+
for (const donor of donors) donor?.dispose();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Layer `i`'s per-layer input [B, L, width] (owned) from the model's
|
|
500
|
+
* [B, L, nLayers, width] tensor (`PerLayerInputs.layer`). */
|
|
501
|
+
#perLayerInput(perLayer: MlxArray, i: number, B: number, L: number): MlxArray {
|
|
502
|
+
return this.perLayerInputs!.layer(perLayer, i, B, L);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** The training forward over stateless training caches (`TrainingCache`,
|
|
506
|
+
* `PrefixSharedCache`), one per donor layer: ids [1, T] to the final-norm
|
|
507
|
+
* hidden [1, T, hidden]. Every attention reads through its training cache's
|
|
508
|
+
* window read (a KV-sharing layer through its donor's, with the donor's keys
|
|
509
|
+
* and values), and a prefix-shared layout ropes block-wise; under `gradCkpt`
|
|
510
|
+
* each layer is checkpointed with its donor keys and values as checkpoint
|
|
511
|
+
* tensors. */
|
|
512
|
+
forwardTrainingHidden(ids: MlxArray, cache: Cache[]): MlxArray {
|
|
513
|
+
return this.#forwardTraining(this.embedTokens(ids), cache, ids);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** The training cache layer `i` reads: its own, or its donor's for a
|
|
517
|
+
* KV-sharing layer (`cache` holds the donors' training caches). */
|
|
518
|
+
#trainingCache(cache: Cache[], i: number): TrainingAttentionCache {
|
|
519
|
+
const ci = this.cacheIndex[i]!;
|
|
520
|
+
return cache[ci === -1 ? this.cacheIndex[this.previousKvs[i]!]! : ci] as TrainingAttentionCache;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** The training forward's decoder over `h0` (consumed). */
|
|
524
|
+
#forwardTraining(h0: MlxArray, cache: Cache[], ids: MlxArray | null): MlxArray {
|
|
525
|
+
const B = h0.shape[0]!;
|
|
526
|
+
const L = h0.shape[1]!;
|
|
527
|
+
let h = h0;
|
|
528
|
+
|
|
529
|
+
// per-layer inputs (e2b/e4b)
|
|
530
|
+
let perLayer: MlxArray | null = null;
|
|
531
|
+
if (this.perLayerWidth > 0) {
|
|
532
|
+
if (!ids) throw new Error("per-layer-input models require token ids");
|
|
533
|
+
perLayer = this.computePerLayerInputs(ids, h);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const intermediates: (PlainSharedKv | null)[] = Array(this.layers.length).fill(null);
|
|
537
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
538
|
+
const layer = this.layers[i]!;
|
|
539
|
+
const ci = this.cacheIndex[i]!;
|
|
540
|
+
const sharedIn = ci === -1 ? (intermediates[this.previousKvs[i]!] ?? null) : null;
|
|
541
|
+
const pls = perLayer ? this.#perLayerInput(perLayer, i, B, L) : null;
|
|
542
|
+
const layerCache = this.#trainingCache(cache, i);
|
|
543
|
+
const { h: next, shared } = this.gradCkpt
|
|
544
|
+
? this.runCheckpointedLayer(i, layer, h, layerCache, sharedIn, pls)
|
|
545
|
+
: layerTraining(layer, h, layerCache, sharedIn, pls);
|
|
546
|
+
pls?.dispose();
|
|
547
|
+
h.dispose();
|
|
548
|
+
h = next;
|
|
549
|
+
intermediates[i] = shared;
|
|
550
|
+
}
|
|
551
|
+
perLayer?.dispose();
|
|
552
|
+
// dispose donor KV (each donor's shared entry exactly once)
|
|
553
|
+
for (let i = 0; i < this.numDonors; i++) {
|
|
554
|
+
const s = intermediates[i];
|
|
555
|
+
if (!s) continue;
|
|
556
|
+
Gemma4Model.disposeSharedKv(s);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
h = disposing(h, this.finalNorm.forward(h));
|
|
560
|
+
return h;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Mixed rows in one forward: each group's attention reads its own caches,
|
|
564
|
+
* positions and SDPA shape in its own phase (a verify group reads a window);
|
|
565
|
+
* feed-forward work packs only real tokens, when the composition packs mixed
|
|
566
|
+
* MLP and no group is a verify window. Kernel selection for packed matmuls
|
|
567
|
+
* can differ from a solo GEMV, so this is an explicit Lab port until its
|
|
568
|
+
* matching packed oracle and serving comparison are accepted. */
|
|
569
|
+
#mixed(groups: Parameters<MlxMixedForward>[0], layers: readonly ServingDenseLayer[]): MlxArray[] {
|
|
570
|
+
const runs: Array<{ h: MlxArray; perLayer: MlxArray | null; donors: (DonorRead | null)[] }> = [];
|
|
571
|
+
const results: MlxArray[] = [];
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
for (const group of groups) {
|
|
575
|
+
const run = { h: this.embedTokens(group.ids), perLayer: null as MlxArray | null,
|
|
576
|
+
donors: Array<DonorRead | null>(this.numDonors).fill(null) };
|
|
577
|
+
runs.push(run);
|
|
578
|
+
if (this.perLayerWidth > 0) run.perLayer = this.computePerLayerInputs(group.ids, run.h);
|
|
579
|
+
}
|
|
580
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
581
|
+
const layer = layers[i]!, ci = this.cacheIndex[i]!;
|
|
582
|
+
const mids: MlxArray[] = [], inputs: MlxArray[] = [];
|
|
583
|
+
try {
|
|
584
|
+
for (const [row, group] of groups.entries()) {
|
|
585
|
+
const run = runs[row]!;
|
|
586
|
+
const { h, donor } = layer.forwardAttn(run.h, ci === -1 ? null : group.state[ci]!,
|
|
587
|
+
group.phase === "decode" ? "decode" : "window", ci === -1 ? run.donors[this.previousKvs[i]!]! : null);
|
|
588
|
+
mids.push(h);
|
|
589
|
+
if (ci !== -1) run.donors[ci] = donor;
|
|
590
|
+
if (run.perLayer) {
|
|
591
|
+
const [B, L] = group.ids.shape;
|
|
592
|
+
using part = run.perLayer.slice([0, 0, i, 0], [B!, L!, i + 1, this.perLayerWidth]);
|
|
593
|
+
inputs.push(ops.reshape(part, [B!, L!, this.perLayerWidth]));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// Per-layer inputs use the same token order as the hidden states.
|
|
597
|
+
let packedInput: MlxArray | null = null;
|
|
598
|
+
if (inputs.length) {
|
|
599
|
+
const flat = inputs.map(input => ops.reshape(input, [1, -1, this.perLayerWidth]));
|
|
600
|
+
try { packedInput = ops.concatAxis(flat, 1); }
|
|
601
|
+
finally { for (const input of flat) input.dispose(); }
|
|
602
|
+
}
|
|
603
|
+
using pli = packedInput;
|
|
604
|
+
const outputs: MlxArray[] = [];
|
|
605
|
+
try {
|
|
606
|
+
outputs.push(...mapPackedTokens(mids, packed => layer.forwardMlp(packed, pli)));
|
|
607
|
+
} catch (error) { for (const output of outputs) output.dispose(); throw error; }
|
|
608
|
+
for (const [row, run] of runs.entries()) {
|
|
609
|
+
run.h.dispose(); run.h = outputs[row]!;
|
|
610
|
+
}
|
|
611
|
+
for (const [row, group] of groups.entries()) group.capture?.(i, runs[row]!.h);
|
|
612
|
+
} finally {
|
|
613
|
+
for (const mid of mids) mid.dispose();
|
|
614
|
+
for (const input of inputs) input.dispose();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
for (const [row, group] of groups.entries()) {
|
|
618
|
+
const hidden = this.finalNorm.forward(runs[row]!.h); results.push(hidden);
|
|
619
|
+
group.capture?.(this.layers.length, hidden);
|
|
620
|
+
}
|
|
621
|
+
return results;
|
|
622
|
+
} catch (error) { for (const result of results) result.dispose(); throw error; }
|
|
623
|
+
finally {
|
|
624
|
+
for (const run of runs) {
|
|
625
|
+
run.h.dispose(); run.perLayer?.dispose();
|
|
626
|
+
for (const donor of run.donors) donor?.dispose();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// --- Segmented-backward support (`02d723a:docs/design/orpo-training.md`
|
|
632
|
+
// §5.3). These run the training forward's layers (`layerTraining`). The
|
|
633
|
+
// segmented driver builds the training caches + per-layer inputs ONCE, then
|
|
634
|
+
// drives runLayerRange per segment, threading the KV-shared donor K/V across
|
|
635
|
+
// segment boundaries.
|
|
636
|
+
|
|
637
|
+
/** Donor layer indices that KV-sharing sharers reuse (e4b: {22,23} — the last
|
|
638
|
+
* donor of each attention type; empty for dense models). Their K/V must
|
|
639
|
+
* survive across segment boundaries (saved as detached boundaries). */
|
|
640
|
+
get reusedDonors(): Set<number> {
|
|
641
|
+
return new Set(this.previousKvs.filter((d, i) => d !== i));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** e2b/e4b training entry: the SCALED input embedding (the first segment
|
|
645
|
+
* boundary) plus the per-layer-input tensor [B,L,nLayers,width] (a detached
|
|
646
|
+
* boundary, sliced per layer inside runLayerRange). perLayer is null for dense
|
|
647
|
+
* models. Caller owns both. */
|
|
648
|
+
embedForSegmented(ids: MlxArray): { hScaled: MlxArray; perLayer: MlxArray | null } {
|
|
649
|
+
let h = this.embed.encode(ids);
|
|
650
|
+
h = disposing(h, ops.mulScalar(h, this.embedScale));
|
|
651
|
+
const perLayer = this.perLayerWidth > 0 ? this.computePerLayerInputs(ids, h) : null;
|
|
652
|
+
return { hScaled: h, perLayer };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** Dispose a training donor's K/V. */
|
|
656
|
+
private static disposeSharedKv(s: PlainSharedKv): void {
|
|
657
|
+
s.offsetArr?.dispose();
|
|
658
|
+
s.keys.dispose();
|
|
659
|
+
s.values.dispose();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Run decoder layers `[aIdx, bIdx)` on hidden `h`, returning the residual
|
|
663
|
+
* stream after the last layer in the range (NO finalNorm) plus the K/V of
|
|
664
|
+
* any reused-donor layers in the range (`donorKvOut`, for later segments).
|
|
665
|
+
* The input `h` is NEVER disposed (the caller / vjp owns the boundary leaf);
|
|
666
|
+
* intra-range hidden intermediates and per-layer slices ARE disposed.
|
|
667
|
+
* Non-reused donors' K/V are disposed; reused donors' K/V are returned
|
|
668
|
+
* (caller threads + disposes). NOT gradient-checkpointed — the segmentation
|
|
669
|
+
* IS the memory strategy.
|
|
670
|
+
*
|
|
671
|
+
* @param cache the pass's training caches, one per donor layer.
|
|
672
|
+
* @param perLayer the [B,L,nLayers,width] tensor (caller-owned) or null.
|
|
673
|
+
* @param donorKvIn K/V of reused donors from EARLIER segments, keyed by donor
|
|
674
|
+
* layer index (caller-owned; not disposed here). */
|
|
675
|
+
runLayerRange(
|
|
676
|
+
h: MlxArray, aIdx: number, bIdx: number, cache: Cache[],
|
|
677
|
+
perLayer: MlxArray | null, donorKvIn: Map<number, SharedKv>,
|
|
678
|
+
): { h: MlxArray; donorKvOut: Map<number, SharedKv> } {
|
|
679
|
+
const donorKvOut = new Map<number, SharedKv>();
|
|
680
|
+
if (aIdx >= bIdx) return { h, donorKvOut };
|
|
681
|
+
const B = h.shape[0]!;
|
|
682
|
+
const L = h.shape[1]!;
|
|
683
|
+
const reused = this.reusedDonors;
|
|
684
|
+
let cur = h;
|
|
685
|
+
for (let i = aIdx; i < bIdx; i++) {
|
|
686
|
+
const layer = this.layers[i]!;
|
|
687
|
+
const ci = this.cacheIndex[i]!;
|
|
688
|
+
const isDonor = ci !== -1;
|
|
689
|
+
let sharedIn: PlainSharedKv | null = null;
|
|
690
|
+
if (!isDonor) {
|
|
691
|
+
const donor = this.previousKvs[i]!;
|
|
692
|
+
// The segment donors hand on the plain keys and values they appended.
|
|
693
|
+
sharedIn = (donorKvOut.get(donor) ?? donorKvIn.get(donor) ?? null) as PlainSharedKv | null;
|
|
694
|
+
if (!sharedIn) throw new Error(`runLayerRange: donor ${donor} K/V not available for sharer ${i}`);
|
|
695
|
+
}
|
|
696
|
+
let pls: MlxArray | null = null;
|
|
697
|
+
if (perLayer) {
|
|
698
|
+
const sl = perLayer.slice([0, 0, i, 0], [B, L, i + 1, this.perLayerWidth]);
|
|
699
|
+
pls = ops.reshape(sl, [B, L, this.perLayerWidth]);
|
|
700
|
+
sl.dispose();
|
|
701
|
+
}
|
|
702
|
+
const { h: next, shared } = layerTraining(layer, cur, this.#trainingCache(cache, i), sharedIn, pls);
|
|
703
|
+
pls?.dispose();
|
|
704
|
+
if (i > aIdx) cur.dispose(); // keep the input boundary leaf; drop interiors
|
|
705
|
+
cur = next;
|
|
706
|
+
if (isDonor) {
|
|
707
|
+
if (reused.has(i)) donorKvOut.set(i, shared);
|
|
708
|
+
else Gemma4Model.disposeSharedKv(shared);
|
|
709
|
+
}
|
|
710
|
+
// sharer: `shared` === sharedIn (owned by donorKvIn/Out) — do not dispose.
|
|
711
|
+
}
|
|
712
|
+
return { h: cur, donorKvOut };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** Run one layer under gradient checkpointing: its interior activations are
|
|
716
|
+
* dropped and recomputed in the backward pass. The layer's LoRA params, h,
|
|
717
|
+
* pls, and a sharer's donor KV are threaded as explicit checkpoint inputs
|
|
718
|
+
* (the closure must be pure and self-contained — it re-runs during the
|
|
719
|
+
* backward, and captured autograd primals would be stale); the training
|
|
720
|
+
* cache it reads stores nothing, so the recompute reads exactly as the
|
|
721
|
+
* forward did. A donor layer also emits its K/V as checkpoint outputs so the
|
|
722
|
+
* (retained, small) tensors reach the sharer layers downstream. */
|
|
723
|
+
private runCheckpointedLayer(
|
|
724
|
+
i: number, layer: DecoderLayer, h: MlxArray,
|
|
725
|
+
layerCache: TrainingAttentionCache, sharedIn: PlainSharedKv | null, pls: MlxArray | null,
|
|
726
|
+
): { h: MlxArray; shared: PlainSharedKv | null } {
|
|
727
|
+
const ctx = this.gradCkpt!;
|
|
728
|
+
const ll = ctx.byLayer.get(i) ?? { attn: [], mlp: [] };
|
|
729
|
+
return ctx.splitMlp
|
|
730
|
+
? this.runSplitCheckpointedLayer(layer, h, layerCache, sharedIn, pls, ll, ctx)
|
|
731
|
+
: this.runSingleCheckpointedLayer(layer, h, layerCache, sharedIn, pls, ll, ctx);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** One Checkpoint over the whole layer (the original path). */
|
|
735
|
+
private runSingleCheckpointedLayer(
|
|
736
|
+
layer: DecoderLayer, h: MlxArray,
|
|
737
|
+
layerCache: TrainingAttentionCache, sharedIn: PlainSharedKv | null, pls: MlxArray | null,
|
|
738
|
+
ll: LayerLoras, ctx: GradCheckpointCtx,
|
|
739
|
+
): { h: MlxArray; shared: PlainSharedKv | null } {
|
|
740
|
+
const lws = [...ll.attn, ...ll.mlp];
|
|
741
|
+
const isSharer = !layer.attn.hasKv;
|
|
742
|
+
const sIn = isSharer && sharedIn && sharedIn.kind === "plain" ? sharedIn : null;
|
|
743
|
+
const hasPls = pls !== null;
|
|
744
|
+
|
|
745
|
+
// Input order: [h, pls?, sharedInK?, sharedInV?, (lora_a, lora_b)*]
|
|
746
|
+
const inputs: MlxArray[] = [h];
|
|
747
|
+
if (hasPls) inputs.push(pls!);
|
|
748
|
+
if (sIn) inputs.push(sIn.keys, sIn.values);
|
|
749
|
+
for (const lw of lws) inputs.push(lw.a, lw.b);
|
|
750
|
+
|
|
751
|
+
const ck = new Checkpoint((ins) => {
|
|
752
|
+
let k = 0;
|
|
753
|
+
const hIn = ins[k++]!;
|
|
754
|
+
const plsIn = hasPls ? ins[k++]! : null;
|
|
755
|
+
const sharedInRebuilt: PlainSharedKv | null = sIn
|
|
756
|
+
? { kind: "plain", keys: ins[k++]!, values: ins[k++]!, offset: 0 }
|
|
757
|
+
: null;
|
|
758
|
+
const saved = lws.map((lw) => [lw.a, lw.b] as [MlxArray, MlxArray]);
|
|
759
|
+
for (const lw of lws) { lw.a = ins[k++]!; lw.b = ins[k++]!; }
|
|
760
|
+
try {
|
|
761
|
+
const out = layerTraining(layer, hIn, layerCache, sharedInRebuilt, plsIn);
|
|
762
|
+
if (!isSharer && out.shared && out.shared.kind === "plain")
|
|
763
|
+
return [out.h, out.shared.keys, out.shared.values];
|
|
764
|
+
return [out.h];
|
|
765
|
+
} finally {
|
|
766
|
+
lws.forEach((lw, j) => { lw.a = saved[j]![0]; lw.b = saved[j]![1]; });
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
ctx.keepAlive.push(ck);
|
|
770
|
+
|
|
771
|
+
const outs = ck.apply(inputs);
|
|
772
|
+
const shared: PlainSharedKv | null =
|
|
773
|
+
!isSharer && outs.length === 3
|
|
774
|
+
? { kind: "plain", keys: outs[1]!, values: outs[2]!, offset: 0 }
|
|
775
|
+
: null;
|
|
776
|
+
return { h: outs[0]!, shared };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** Intra-layer MLP split (lever 4): TWO checkpoints — the attention sub-block
|
|
780
|
+
* (forwardAttn: inputNorm → attn → residual add, threading the donor K/V) and
|
|
781
|
+
* the MLP sub-block (forwardMlp: FFN + per-layer gate) — with the post-attn
|
|
782
|
+
* residual `hMid` as the boundary. The backward recompute holds only one
|
|
783
|
+
* sub-block's activations at a time, dropping per-layer peak from `attn+MLP`
|
|
784
|
+
* to `max(attn,MLP)+hMid`. Attn LoRA / pls / donor-KV go to the attn ck; MLP
|
|
785
|
+
* LoRA to the MLP ck. */
|
|
786
|
+
private runSplitCheckpointedLayer(
|
|
787
|
+
layer: DecoderLayer, h: MlxArray,
|
|
788
|
+
layerCache: TrainingAttentionCache, sharedIn: PlainSharedKv | null, pls: MlxArray | null,
|
|
789
|
+
ll: LayerLoras, ctx: GradCheckpointCtx,
|
|
790
|
+
): { h: MlxArray; shared: PlainSharedKv | null } {
|
|
791
|
+
const isSharer = !layer.attn.hasKv;
|
|
792
|
+
const sIn = isSharer && sharedIn && sharedIn.kind === "plain" ? sharedIn : null;
|
|
793
|
+
const hasPls = pls !== null;
|
|
794
|
+
const attnLws = ll.attn;
|
|
795
|
+
const mlpLws = ll.mlp;
|
|
796
|
+
|
|
797
|
+
// --- attn sub-block checkpoint: [h, sharedInK?, sharedInV?, attnLora*]
|
|
798
|
+
const attnInputs: MlxArray[] = [h];
|
|
799
|
+
if (sIn) attnInputs.push(sIn.keys, sIn.values);
|
|
800
|
+
for (const lw of attnLws) attnInputs.push(lw.a, lw.b);
|
|
801
|
+
|
|
802
|
+
const attnCk = new Checkpoint((ins) => {
|
|
803
|
+
let k = 0;
|
|
804
|
+
const hIn = ins[k++]!;
|
|
805
|
+
const sharedInRebuilt: PlainSharedKv | null = sIn
|
|
806
|
+
? { kind: "plain", keys: ins[k++]!, values: ins[k++]!, offset: 0 }
|
|
807
|
+
: null;
|
|
808
|
+
const saved = attnLws.map((lw) => [lw.a, lw.b] as [MlxArray, MlxArray]);
|
|
809
|
+
for (const lw of attnLws) { lw.a = ins[k++]!; lw.b = ins[k++]!; }
|
|
810
|
+
try {
|
|
811
|
+
const out = layerAttnTraining(layer, hIn, layerCache, sharedInRebuilt);
|
|
812
|
+
if (!isSharer && out.shared && out.shared.kind === "plain")
|
|
813
|
+
return [out.h, out.shared.keys, out.shared.values];
|
|
814
|
+
return [out.h];
|
|
815
|
+
} finally {
|
|
816
|
+
attnLws.forEach((lw, j) => { lw.a = saved[j]![0]; lw.b = saved[j]![1]; });
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
ctx.keepAlive.push(attnCk);
|
|
820
|
+
const attnOuts = attnCk.apply(attnInputs);
|
|
821
|
+
const hMid = attnOuts[0]!;
|
|
822
|
+
const shared: PlainSharedKv | null =
|
|
823
|
+
!isSharer && attnOuts.length === 3
|
|
824
|
+
? { kind: "plain", keys: attnOuts[1]!, values: attnOuts[2]!, offset: 0 }
|
|
825
|
+
: null;
|
|
826
|
+
|
|
827
|
+
// --- MLP sub-block checkpoint: [hMid, pls?, mlpLora*] -> [hOut]
|
|
828
|
+
const mlpInputs: MlxArray[] = [hMid];
|
|
829
|
+
if (hasPls) mlpInputs.push(pls!);
|
|
830
|
+
for (const lw of mlpLws) mlpInputs.push(lw.a, lw.b);
|
|
831
|
+
|
|
832
|
+
const mlpCk = new Checkpoint((ins) => {
|
|
833
|
+
let k = 0;
|
|
834
|
+
const hMidIn = ins[k++]!;
|
|
835
|
+
const plsIn = hasPls ? ins[k++]! : null;
|
|
836
|
+
const saved = mlpLws.map((lw) => [lw.a, lw.b] as [MlxArray, MlxArray]);
|
|
837
|
+
for (const lw of mlpLws) { lw.a = ins[k++]!; lw.b = ins[k++]!; }
|
|
838
|
+
try {
|
|
839
|
+
return [layer.forwardMlp(hMidIn, plsIn)];
|
|
840
|
+
} finally {
|
|
841
|
+
mlpLws.forEach((lw, j) => { lw.a = saved[j]![0]; lw.b = saved[j]![1]; });
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
ctx.keepAlive.push(mlpCk);
|
|
845
|
+
const mlpOuts = mlpCk.apply(mlpInputs);
|
|
846
|
+
hMid.dispose(); // boundary leaf consumed by the MLP ck (mlx retains it in-graph)
|
|
847
|
+
|
|
848
|
+
return { h: mlpOuts[0]!, shared };
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** hidden [1, L, hidden] → softcapped logits [1, L, vocab]. */
|
|
852
|
+
logitsFromHidden(h: MlxArray): MlxArray { return this.#head(h, compiledLogitSoftcap); }
|
|
853
|
+
|
|
854
|
+
#head(h: MlxArray, softcapOperation: typeof logitSoftcap): MlxArray {
|
|
855
|
+
let logits = this.embed.asLinear(h);
|
|
856
|
+
const softcap = this.config.text.finalLogitSoftcapping;
|
|
857
|
+
if (softcap !== null) logits = disposing(logits, softcapOperation(logits, softcap));
|
|
858
|
+
return logits;
|
|
859
|
+
}
|
|
860
|
+
}
|