mlx-bun 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -32
- package/docs/README.md +58 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +1535 -50
- package/docs/design/decode-speed-program.md +394 -1171
- package/docs/design/generic-model-support.md +195 -3
- package/docs/design/kv-cache.md +538 -225
- package/docs/design/speculative-decoding.md +552 -50
- package/docs/design/turboquant.md +140 -17
- package/docs/design/unified-engine-frontier-plan.md +259 -150
- package/docs/planning/PRODUCT_ROADMAP.md +21 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3537 -1112
- package/docs/reference/cli.md +102 -2
- package/docs/reference/distribution.md +45 -13
- package/docs/reference/environment.md +159 -13
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +28 -7
- package/docs/reference/server-api.md +257 -33
- package/docs/reference/server-config.md +372 -151
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +2 -2
- package/src/audio/audiotoolbox.ts +112 -0
- package/src/audio/conformer.ts +8 -0
- package/src/audio/mic-capture.ts +98 -0
- package/src/audio/silero-vad.ts +379 -0
- package/src/audio/whisper-decode.ts +774 -0
- package/src/audio/whisper-fast.ts +732 -0
- package/src/audio/whisper-format.ts +73 -0
- package/src/audio/whisper-mel.ts +185 -0
- package/src/audio/whisper-timing.ts +274 -0
- package/src/audio/whisper-tokenizer.ts +239 -0
- package/src/audio/whisper-transcribe.ts +410 -0
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +464 -236
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +107 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- package/src/backends/mlx/encoder-cache.ts +25 -0
- package/src/backends/mlx/fill-append.ts +21 -8
- package/src/backends/mlx/fill-group.ts +497 -0
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +122 -19
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +161 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/mixed-iteration.ts +62 -0
- package/src/backends/mlx/model-prompt.ts +46 -32
- package/src/backends/mlx/model-serving.ts +5 -4
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +100 -0
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +244 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/prompt-input.ts +29 -0
- package/src/backends/mlx/qwen-prompt-input.ts +40 -0
- package/src/backends/mlx/request-state-policy.ts +54 -0
- package/src/backends/mlx/rollback.ts +45 -4
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +51 -73
- package/src/backends/mlx/speculative-group.ts +438 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +21 -1
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
- package/src/cli.ts +333 -36
- package/src/contracts/execution.ts +21 -0
- package/src/contracts/object-cache.ts +8 -0
- package/src/contracts/pi-provider.ts +1 -1
- package/src/contracts/pi.ts +3 -1
- package/src/contracts/prefix-cache.ts +33 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/engine/execution-plan.ts +38 -25
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +32 -7
- package/src/fill/fill-session.ts +14 -3
- package/src/fit.ts +29 -15
- package/src/generate.ts +105 -195
- package/src/grammar.ts +13 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/prefill.ts +6 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +92 -12
- package/src/kv-store.ts +183 -93
- package/src/lab/README.md +5 -1
- package/src/lab/paged-kv/paged-attention.ts +117 -0
- package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
- package/src/lab/paged-kv/paged-kv.ts +126 -46
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +39 -1
- package/src/mlx/ffi.ts +19 -0
- package/src/mlx/normalized-argmax.ts +116 -0
- package/src/mlx/ops.ts +97 -10
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +12 -4
- package/src/model/batched-quant.ts +8 -2
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +76 -15
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +64 -22
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +94 -0
- package/src/model/compiled-decode.ts +7 -1
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +57 -0
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +183 -82
- package/src/model/gemma4.ts +116 -28
- package/src/model/generated/gemma4-12b.ts +4 -1
- package/src/model/generated/gemma4-26b.ts +4 -1
- package/src/model/generated/gemma4-e4b.ts +4 -1
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/kv-attention-view.ts +70 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +177 -75
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +4 -4
- package/src/model/turboquant-codec.ts +89 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/model/whisper.ts +435 -0
- package/src/native/mic_capture.swift +92 -0
- package/src/native-pack.ts +6 -5
- package/src/pi-web.ts +9 -1
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +306 -86
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +37 -25
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +19 -14
- package/src/serve/completion-executor.ts +9 -1
- package/src/serve/discovery-routes.ts +17 -1
- package/src/serve/generated-token-history.ts +57 -0
- package/src/serve/generation-gateway.ts +88 -38
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/model-host.ts +3 -4
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +6 -3
- package/src/serve/request-prep.ts +40 -25
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +185 -261
- package/src/spec/assistant-rows.ts +93 -0
- package/src/spec/assistant-source.ts +28 -18
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +12 -10
- package/src/spec/dflash-source.ts +28 -4
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/deepspec-module.ts +95 -120
- package/src/spec/dspark/module-dflash.ts +126 -170
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +115 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +230 -0
- package/src/spec/qwen-mtp-source.ts +84 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +73 -69
- package/src/spec/source.ts +93 -15
- package/src/spec/two-model-rows.ts +62 -0
- package/src/spec/two-model.ts +45 -103
- package/src/ssd-cache.ts +107 -36
- package/src/ssd-durability.ts +29 -27
- package/src/storage/host-buffer.ts +24 -0
- package/src/storage/kv-writer.ts +83 -0
- package/src/storage/kv-writer.worker.js +264 -0
- package/src/storage/retention-policy.ts +37 -0
- package/src/tiered-prompt-cache.ts +45 -0
- package/src/vision/embedder.ts +8 -0
- package/src/vision/prompt.ts +101 -65
- package/src/vision/qwen3vl-prompt.ts +101 -64
- package/src/vision/qwen3vl-tower.ts +11 -0
- package/src/vision/siglip.ts +8 -0
- package/src/web/app.html +12 -0
- package/src/web/app.js +217 -57
- package/src/web/src/chat.ts +4 -1
- package/src/web/src/composer.ts +3 -0
- package/src/web/src/voice.ts +141 -0
- package/src/web/sw.js +1 -1
- package/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { decodedKvDonorAttention } from "./decoded-kv-donor";
|
|
2
|
+
import type { MlxArray } from "../mlx/array";
|
|
3
|
+
import { KvTensorRows, type KvTensorRowView } from "../backends/mlx/kv-tensor-rows";
|
|
4
|
+
import { disposeResources } from "../engine/resources";
|
|
5
|
+
import { TurboQuantKVCache, type BatchableCache, type Cache, type Mask, type RotatedValueAttentionState, type PaddedPrefillCache, type PrefillPadding } from "./gemma4-base";
|
|
6
|
+
import { TurboQuantCodec, turboQuantFusedDecode, disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
|
|
7
|
+
|
|
8
|
+
const fields = ["kIdx", "kScales", "kZeros", "vPacked", "vScales"] as const;
|
|
9
|
+
function encoded(planes: readonly MlxArray[]): TurboQuantTensor {
|
|
10
|
+
return { kIdx: planes[0]!, kScales: planes[1]!, kZeros: planes[2]!, vPacked: planes[3]!, vScales: planes[4]! };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The existing TurboQuant codec over shared tensor-row storage. Row changes
|
|
14
|
+
* copy encoded bytes without decoding, rotating or requantizing their values. */
|
|
15
|
+
export class BatchedTurboQuantKVCache implements BatchableCache, PaddedPrefillCache {
|
|
16
|
+
readonly #storage = new KvTensorRows();
|
|
17
|
+
readonly #codec: TurboQuantCodec;
|
|
18
|
+
#reuseOffsets: number[] = [];
|
|
19
|
+
#headDim: number | null = null;
|
|
20
|
+
constructor(readonly kBits: number, readonly vBits: number,
|
|
21
|
+
readonly fusedDecode = turboQuantFusedDecode()) {
|
|
22
|
+
this.#codec = new TurboQuantCodec(kBits, vBits, fusedDecode);
|
|
23
|
+
}
|
|
24
|
+
restorePrefillEnds(ends: readonly number[] | undefined): void { this.#storage.restorePrefillEnds(ends); }
|
|
25
|
+
preparePrefill(padding: PrefillPadding): void {
|
|
26
|
+
this.#storage.preparePrefill(padding);
|
|
27
|
+
if (!this.#reuseOffsets.length) this.#reuseOffsets = padding.lengths.map(() => 0);
|
|
28
|
+
}
|
|
29
|
+
finalizePrefill(): void { this.#storage.finalizePrefill(); }
|
|
30
|
+
captureDonorRows(): import("./gemma4-base").KvDonorRows {
|
|
31
|
+
if (!this.#storage.planes.length || this.#headDim === null) throw new Error("cache is empty");
|
|
32
|
+
const [keys, values] = this.#codec.decode(encoded(this.#storage.planes), this.offset, this.#headDim, false);
|
|
33
|
+
return { keys, values, ...this.#storage.captureDonorValidity() };
|
|
34
|
+
}
|
|
35
|
+
captureDonorAttention(): import("./gemma4-base").KvDonorAttention {
|
|
36
|
+
return decodedKvDonorAttention(this.captureDonorRows());
|
|
37
|
+
}
|
|
38
|
+
get rotatedValueAttention(): RotatedValueAttentionState { return this; }
|
|
39
|
+
signature(): string { return `kv:batched-turboquant:${this.kBits}:${this.vBits}`; }
|
|
40
|
+
get minimumReusableOffset(): number { return Math.max(0, ...this.#reuseOffsets); }
|
|
41
|
+
get headDim(): number | null { return this.#headDim; }
|
|
42
|
+
get rowOffsets(): readonly number[] { return this.#storage.rowOffsets; }
|
|
43
|
+
get leftPad(): readonly number[] { return this.#storage.leftPad; }
|
|
44
|
+
get batchSize(): number | null { return this.#storage.batchSize; }
|
|
45
|
+
get offset(): number { return this.#storage.offset; }
|
|
46
|
+
get ropeOffsetArr(): MlxArray | undefined { return this.#storage.ropeOffsetArr; }
|
|
47
|
+
makeEmptyBatch(): BatchedTurboQuantKVCache { return new BatchedTurboQuantKVCache(this.kBits, this.vBits, this.fusedDecode); }
|
|
48
|
+
bytesPerToken(): number { return this.#storage.bytesPerToken(); }
|
|
49
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * tokens; }
|
|
50
|
+
makeMask(tokens: number, window: number | null): Mask { return this.#storage.makeMask(tokens, window); }
|
|
51
|
+
isTrimmable(): boolean { return true; }
|
|
52
|
+
trim(count: number): void { this.#storage.trim(count); }
|
|
53
|
+
specRoundBegin(): void { this.#storage.specRoundBegin(); }
|
|
54
|
+
specRoundCommit(): void { this.#storage.specRoundCommit(); }
|
|
55
|
+
specRoundRollback(keep: number | readonly number[]): void { this.#storage.specRoundRollback(keep); }
|
|
56
|
+
#append(k: MlxArray, v: MlxArray, defer: boolean): [MlxArray, MlxArray] {
|
|
57
|
+
const packed = this.#codec.encode(k, v);
|
|
58
|
+
try {
|
|
59
|
+
this.#storage.append(fields.map(field => packed[field]));
|
|
60
|
+
this.#headDim = k.shape[3]!;
|
|
61
|
+
return this.#codec.decode(encoded(this.#storage.planes), this.offset, this.#headDim, defer);
|
|
62
|
+
} finally { disposeTurboQuant(packed); }
|
|
63
|
+
}
|
|
64
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] { return this.#append(k, v, false); }
|
|
65
|
+
updateAndFetchDeferredV(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] { return this.#append(k, v, true); }
|
|
66
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
67
|
+
const views: KvTensorRowView[] = [], held: MlxArray[] = [];
|
|
68
|
+
const reuseOffsets: number[] = [];
|
|
69
|
+
let dim = this.#headDim;
|
|
70
|
+
try {
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
if (row instanceof BatchedTurboQuantKVCache) {
|
|
73
|
+
views.push(row.#storage); dim ??= row.headDim; reuseOffsets.push(...row.#reuseOffsets);
|
|
74
|
+
} else if (row instanceof TurboQuantKVCache) {
|
|
75
|
+
const planes = row.state(); held.push(...planes); reuseOffsets.push(row.minimumReusableOffset);
|
|
76
|
+
views.push({ planes, rowOffsets: [row.offset], leftPad: [0] }); dim ??= row.headDim;
|
|
77
|
+
} else throw new Error(`TurboQuant row layout cannot merge ${row.signature()}`);
|
|
78
|
+
}
|
|
79
|
+
this.#storage.mergeRows(views); this.#headDim = dim; this.#reuseOffsets = reuseOffsets;
|
|
80
|
+
} finally { disposeResources(held); }
|
|
81
|
+
}
|
|
82
|
+
alignRows(leftPad: readonly number[]): void { this.#storage.alignRows(leftPad); }
|
|
83
|
+
filterRows(keep: readonly number[]): void {
|
|
84
|
+
this.#storage.filterRows(keep); this.#reuseOffsets = keep.map(row => this.#reuseOffsets[row]!);
|
|
85
|
+
}
|
|
86
|
+
state(): MlxArray[] { return this.#storage.planes; }
|
|
87
|
+
extractRow(row: number): TurboQuantKVCache {
|
|
88
|
+
const result = new TurboQuantKVCache(this.kBits, this.vBits, this.fusedDecode), count = this.rowOffsets[row]!;
|
|
89
|
+
if (count) result.restoreState(encoded(this.#storage.extractRow(row)), count, this.#headDim!);
|
|
90
|
+
result.minimumReusableOffset = this.#reuseOffsets[row] ?? 0;
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
dispose(): void { this.#storage.dispose(); this.#headDim = null; this.#reuseOffsets = []; }
|
|
94
|
+
}
|
|
@@ -277,7 +277,10 @@ function decodePhase(c: AnyCache): "concat" | "ring" {
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
function disposeShared(s: SharedKv): void {
|
|
280
|
-
if (s.kind === "
|
|
280
|
+
if (s.kind === "view") {
|
|
281
|
+
s.attention.dispose();
|
|
282
|
+
s.offsetArr?.dispose();
|
|
283
|
+
} else if (s.kind === "plain") {
|
|
281
284
|
s.keys.dispose();
|
|
282
285
|
s.values.dispose();
|
|
283
286
|
} else {
|
|
@@ -381,6 +384,9 @@ export class CompiledDecode {
|
|
|
381
384
|
readonly #segmented: boolean;
|
|
382
385
|
|
|
383
386
|
private constructor(readonly model: Gemma4Model) {
|
|
387
|
+
// A fresh resume has no prefill to evaluate lazy FP32 constants. Capture
|
|
388
|
+
// their values, not their construction graph, in every compiled closure.
|
|
389
|
+
model.materializeGraphConstants();
|
|
384
390
|
const t = model.config.text;
|
|
385
391
|
this.#segmented =
|
|
386
392
|
t.numKvSharedLayers === 0 && !t.enableMoeBlock && model.perLayerWidth === 0;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as ops from "../mlx/ops";
|
|
2
|
+
import type { KvDonorAttention, KvDonorRows } from "./gemma4-base";
|
|
3
|
+
|
|
4
|
+
/** Takes ownership of decoded donor planes. Assistant attention uses decoded
|
|
5
|
+
* values, preserving its SDPA arithmetic rather than moving inverse rotation. */
|
|
6
|
+
export function decodedKvDonorAttention(view: KvDonorRows): KvDonorAttention {
|
|
7
|
+
const { keys, values, offsets, starts, ends } = view;
|
|
8
|
+
return { width: keys.shape[2]!, dtype: keys.dtype, offsets, starts, ends,
|
|
9
|
+
attend: (q, scale, mask) => ops.sdpa(q, keys, values, scale, mask.mode, mask.arr),
|
|
10
|
+
dispose() { keys.dispose(); values.dispose(); },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { captureFullKvDonorAttention } from "../backends/mlx/full-kv-row-donor";
|
|
2
|
+
import { appendFullKvRows } from "../backends/mlx/full-kv-row-append";
|
|
3
|
+
import { fullRowPadding, fullRowPhysicalLength } from "./full-prefill-row";
|
|
4
|
+
import { captureKvAttention } from "./kv-attention-view";
|
|
5
|
+
import type { MlxArray } from "../mlx/array";
|
|
6
|
+
import * as ops from "../mlx/ops";
|
|
7
|
+
import { FullTransitioningKvRows } from "../backends/mlx/full-transitioning-kv-rows";
|
|
8
|
+
import { BatchedQuantizedKVCache } from "./batched-quantized-kv";
|
|
9
|
+
import { QuantizedKVCache, type Cache, type Mask, type KvAttentionState, type KvAttentionView } from "./gemma4-base";
|
|
10
|
+
|
|
11
|
+
/** Affine storage keeps its native quantized-attention arithmetic while rows
|
|
12
|
+
* cross the conversion boundary independently. Once all rows convert, their
|
|
13
|
+
* packed planes use the existing batched attention implementation. */
|
|
14
|
+
export class DelayedQuantizedKVCache extends FullTransitioningKvRows<BatchedQuantizedKVCache> implements KvAttentionState {
|
|
15
|
+
constructor(readonly groupSize: number, readonly bits: number, readonly start: number,
|
|
16
|
+
readonly maintain: (rows: Cache[]) => void, row?: Cache) {
|
|
17
|
+
super({ signature: `kv:delayed-quant:${bits}:${groupSize}:${start}`, conversionOffset: start, maintain,
|
|
18
|
+
converted: row => row instanceof QuantizedKVCache,
|
|
19
|
+
makeLayout: () => new BatchedQuantizedKVCache(groupSize, bits) }, row);
|
|
20
|
+
}
|
|
21
|
+
get attentionState(): KvAttentionState { return this; }
|
|
22
|
+
captureDonorAttention() {
|
|
23
|
+
return this.packed?.captureDonorAttention() ?? captureFullKvDonorAttention(this.rows, this.leftPad, this.offset);
|
|
24
|
+
}
|
|
25
|
+
makeEmptyBatch(): DelayedQuantizedKVCache { return new DelayedQuantizedKVCache(this.groupSize, this.bits, this.start, this.maintain); }
|
|
26
|
+
updateAndFetch(): [MlxArray, MlxArray] { throw new Error("mixed precision rows use their attention state"); }
|
|
27
|
+
appendAndFetch(k: MlxArray, v: MlxArray): KvAttentionView {
|
|
28
|
+
this.advance();
|
|
29
|
+
if (this.packed) return captureKvAttention(this.packed, k, v);
|
|
30
|
+
if (this.rows.every(row => !row.quantizedAttention)) {
|
|
31
|
+
const [keys, values] = appendFullKvRows(this.rows, this.leftPad, this.offset + k.shape[2]!, k, v);
|
|
32
|
+
try { this.syncPositions(true); } catch (error) { keys.dispose(); values.dispose(); throw error; }
|
|
33
|
+
return { attend: (q, scale, mask) => ops.sdpa(q, keys, values, scale, mask.mode, mask.arr),
|
|
34
|
+
dispose() { keys.dispose(); values.dispose(); } };
|
|
35
|
+
}
|
|
36
|
+
const views: KvAttentionView[] = [], pads = this.rows.map((row, index) => this.leftPad[index]! - fullRowPadding(row));
|
|
37
|
+
const lengths = this.rows.map(cache => fullRowPhysicalLength(cache) + k.shape[2]!);
|
|
38
|
+
try {
|
|
39
|
+
for (const [row, cache] of this.rows.entries()) {
|
|
40
|
+
const slice = (a: MlxArray) => a.slice([row, 0, 0, 0], [row + 1, a.shape[1]!, a.shape[2]!, a.shape[3]!]);
|
|
41
|
+
using kr = slice(k), vr = slice(v);
|
|
42
|
+
views.push(captureKvAttention(cache, kr, vr));
|
|
43
|
+
}
|
|
44
|
+
this.syncPositions(true);
|
|
45
|
+
} catch (error) { for (const view of views) view.dispose(); throw error; }
|
|
46
|
+
return {
|
|
47
|
+
attend(q, scale, mask, independentPositions) {
|
|
48
|
+
const outputs: MlxArray[] = [];
|
|
49
|
+
try {
|
|
50
|
+
for (const [row, view] of views.entries()) {
|
|
51
|
+
using qr = q.slice([row, 0, 0, 0], [row + 1, q.shape[1]!, q.shape[2]!, q.shape[3]!]);
|
|
52
|
+
// Capture alignment with the KV tensors, never consult live rows.
|
|
53
|
+
const start = mask.arr?.shape.map(() => 0), end = mask.arr ? [...mask.arr.shape] : undefined;
|
|
54
|
+
if (start && end) {
|
|
55
|
+
if (end.length === 4 && end[0]! > 1) { start[0] = row; end[0] = row + 1; }
|
|
56
|
+
start[start.length - 1] = pads[row]!;
|
|
57
|
+
end[end.length - 1] = pads[row]! + lengths[row]!;
|
|
58
|
+
}
|
|
59
|
+
using arr = mask.arr ? mask.arr.slice(start!, end!) : null;
|
|
60
|
+
outputs.push(view.attend(qr, scale, { mode: mask.mode, arr }, independentPositions));
|
|
61
|
+
}
|
|
62
|
+
return outputs.length === 1 ? ops.contiguous(outputs[0]!) : ops.concatAxis(outputs, 0);
|
|
63
|
+
} finally { for (const output of outputs) output.dispose(); }
|
|
64
|
+
},
|
|
65
|
+
dispose() { for (const view of views) view.dispose(); },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
updateAndAttend(q: MlxArray, k: MlxArray, v: MlxArray, scale: number, mask: Mask): MlxArray {
|
|
69
|
+
const view = this.appendAndFetch(k, v);
|
|
70
|
+
try { return view.attend(q, scale, mask); } finally { view.dispose(); }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import { RotatingKVCache, type Cache, type KvAttentionState, type KvAttentionView, type PaddedPrefillCache, type PrefillPadding } from "./gemma4-base";
|
|
4
|
+
import { SpeculativeTransitioningKvRows } from "../backends/mlx/transitioning-kv-rows";
|
|
5
|
+
import { AlignedRotatingCache, alignRotatingRows, RotatingAffineLayout, SpeculativeRotatingAffineLayout, RotatingKvPositions } from "./rotating-kv-layout";
|
|
6
|
+
import { BatchedRotatingQuantCache } from "./batched-rotating-quant";
|
|
7
|
+
import { captureKvAttention, combineKvDonorAttention } from "./kv-attention-view";
|
|
8
|
+
|
|
9
|
+
/** Precision changes preserve each row's physical columns. The model owns
|
|
10
|
+
* queries and scale; the shared lifecycle owns membership and conversion. */
|
|
11
|
+
export class DelayedRotatingQuantizedKVCache extends SpeculativeTransitioningKvRows<RotatingAffineLayout> implements KvAttentionState, PaddedPrefillCache {
|
|
12
|
+
constructor(readonly maxSize: number, readonly groupSize: number, readonly bits: number, readonly start: number,
|
|
13
|
+
readonly maintain: (rows: Cache[]) => void, row?: Cache, readonly speculative = false) {
|
|
14
|
+
super({ signature: `kv:delayed-rotating-quant:${maxSize}:${bits}:${groupSize}:${start}`, conversionOffset: start, maintain,
|
|
15
|
+
converted: row => row instanceof AlignedRotatingCache && row.inner instanceof BatchedRotatingQuantCache,
|
|
16
|
+
makeLayout: () => speculative ? new SpeculativeRotatingAffineLayout(maxSize, groupSize, bits) : new RotatingAffineLayout(maxSize, groupSize, bits), prepareRows: alignRotatingRows,
|
|
17
|
+
packRows: (layout, rows) => layout.adoptAlignedRows(rows as readonly AlignedRotatingCache[]),
|
|
18
|
+
extractRow: row => (row as AlignedRotatingCache).extract(speculative ? maxSize : undefined),
|
|
19
|
+
rollbackRow: (row, before, keep, preserve) => (row as AlignedRotatingCache).rollback(before, keep, preserve) }, row, new RotatingKvPositions(maxSize));
|
|
20
|
+
}
|
|
21
|
+
get attentionState(): KvAttentionState { return this; }
|
|
22
|
+
captureDonorAttention(): import("./gemma4-base").KvDonorAttention {
|
|
23
|
+
if (this.packed) return this.packed.captureDonorAttention();
|
|
24
|
+
const views: import("./gemma4-base").KvDonorAttention[] = [];
|
|
25
|
+
try {
|
|
26
|
+
for (const row of this.rows) views.push((row as AlignedRotatingCache).captureDonorAttention());
|
|
27
|
+
return combineKvDonorAttention(views);
|
|
28
|
+
} catch (error) { for (const view of views) view.dispose(); throw error; }
|
|
29
|
+
}
|
|
30
|
+
override makeMask(tokens: number, window: number | null) {
|
|
31
|
+
if (this.speculative && this.leftPad.every(pad => pad === 0) &&
|
|
32
|
+
this.offset + tokens <= Math.min(this.maxSize, window ?? this.maxSize))
|
|
33
|
+
return { mode: tokens === 1 ? "" as const : "causal" as const, arr: null };
|
|
34
|
+
return super.makeMask(tokens, window);
|
|
35
|
+
}
|
|
36
|
+
preparePrefill(padding: PrefillPadding): void {
|
|
37
|
+
if (!this.batchSize) {
|
|
38
|
+
const rows = padding.lengths.map(() => new RotatingKVCache(this.maxSize));
|
|
39
|
+
try { this.mergeRows(rows); } finally { for (const row of rows) row.dispose(); }
|
|
40
|
+
}
|
|
41
|
+
if (this.packed) { this.packed.preparePrefill(padding); return; }
|
|
42
|
+
// Every physical row follows the same block geometry, even when only a
|
|
43
|
+
// sibling has right padding. The token lengths still belong to each row.
|
|
44
|
+
const rightPaddedBatch = padding.rightPadding?.some(pad => pad > 0);
|
|
45
|
+
for (const [row, cache] of this.rows.entries()) (cache as AlignedRotatingCache).inner.preparePrefill({
|
|
46
|
+
lengths: [padding.lengths[row]!],
|
|
47
|
+
...(padding.leftPadding ? { leftPadding: [padding.leftPadding[row]!] } : {}),
|
|
48
|
+
...(padding.rightPadding ? { rightPadding: [padding.rightPadding[row]!] } : {}),
|
|
49
|
+
}, rightPaddedBatch);
|
|
50
|
+
this.syncPositions(true);
|
|
51
|
+
}
|
|
52
|
+
finalizePrefill(): void {
|
|
53
|
+
if (this.packed) { this.packed.finalizePrefill(); return; }
|
|
54
|
+
for (const cache of this.rows) (cache as AlignedRotatingCache).inner.finalizePrefill();
|
|
55
|
+
this.syncPositions(true);
|
|
56
|
+
}
|
|
57
|
+
get maxTokens(): number { return this.maxSize; }
|
|
58
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * Math.min(tokens, this.maxSize); }
|
|
59
|
+
makeEmptyBatch(): DelayedRotatingQuantizedKVCache {
|
|
60
|
+
return new DelayedRotatingQuantizedKVCache(this.maxSize, this.groupSize, this.bits, this.start, this.maintain, undefined, this.speculative);
|
|
61
|
+
}
|
|
62
|
+
updateAndFetch(): [MlxArray, MlxArray] { throw new Error("mixed precision rows use their attention state"); }
|
|
63
|
+
releaseRopeArr(): void { this.packed?.releaseRopeArr(); }
|
|
64
|
+
appendAndFetch(k: MlxArray, v: MlxArray): KvAttentionView {
|
|
65
|
+
this.advance();
|
|
66
|
+
if (this.packed) return captureKvAttention(this.packed, k, v);
|
|
67
|
+
const views: KvAttentionView[] = [];
|
|
68
|
+
try {
|
|
69
|
+
for (const [row, cache] of this.rows.entries()) {
|
|
70
|
+
const slice = (a: MlxArray) => a.slice([row, 0, 0, 0], [row + 1, a.shape[1]!, a.shape[2]!, a.shape[3]!]);
|
|
71
|
+
using kr = slice(k), vr = slice(v);
|
|
72
|
+
views.push(captureKvAttention(cache, kr, vr));
|
|
73
|
+
}
|
|
74
|
+
this.syncPositions(true);
|
|
75
|
+
} catch (error) { for (const view of views) view.dispose(); throw error; }
|
|
76
|
+
return {
|
|
77
|
+
attend(q, scale, mask) {
|
|
78
|
+
const outputs: MlxArray[] = [];
|
|
79
|
+
try {
|
|
80
|
+
for (const [row, view] of views.entries()) {
|
|
81
|
+
using qr = q.slice([row, 0, 0, 0], [row + 1, q.shape[1]!, q.shape[2]!, q.shape[3]!]);
|
|
82
|
+
const start = mask.arr?.shape.map(() => 0), end = mask.arr ? [...mask.arr.shape] : undefined;
|
|
83
|
+
if (start && end && end.length === 4 && end[0]! > 1) { start[0] = row; end[0] = row + 1; }
|
|
84
|
+
using arr = mask.arr ? mask.arr.slice(start!, end!) : null;
|
|
85
|
+
outputs.push(view.attend(qr, scale, { mode: mask.mode, arr }));
|
|
86
|
+
}
|
|
87
|
+
return outputs.length === 1 ? ops.contiguous(outputs[0]!) : ops.concatAxis(outputs, 0);
|
|
88
|
+
} finally { for (const output of outputs) output.dispose(); }
|
|
89
|
+
},
|
|
90
|
+
dispose() { for (const view of views) view.dispose(); },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { turboQuantFusedDecode } from "./turboquant-codec";
|
|
2
|
+
import { captureFullKvDonorRows } from "../backends/mlx/full-kv-row-donor";
|
|
3
|
+
import { decodedKvDonorAttention } from "./decoded-kv-donor";
|
|
4
|
+
import { appendFullKvRows } from "../backends/mlx/full-kv-row-append";
|
|
5
|
+
import type { MlxArray } from "../mlx/array";
|
|
6
|
+
import { Dtype } from "../mlx/ffi";
|
|
7
|
+
import * as ops from "../mlx/ops";
|
|
8
|
+
import { unrotateValues } from "../mlx/turboquant-ops";
|
|
9
|
+
import { type Cache, type RotatedValueAttentionState, TurboQuantKVCache } from "./gemma4-base";
|
|
10
|
+
import { BatchedTurboQuantKVCache } from "./batched-turboquant-kv";
|
|
11
|
+
import { FullTransitioningKvRows } from "../backends/mlx/full-transitioning-kv-rows";
|
|
12
|
+
|
|
13
|
+
/** TurboQuant tensor access and value-domain handling over the shared
|
|
14
|
+
* row-transition lifecycle. Model projections and attention remain batched. */
|
|
15
|
+
export class DelayedTurboQuantKVCache extends FullTransitioningKvRows<BatchedTurboQuantKVCache> implements RotatedValueAttentionState {
|
|
16
|
+
#rotated: boolean[] = [];
|
|
17
|
+
constructor(readonly kBits: number, readonly vBits: number, readonly start: number,
|
|
18
|
+
readonly maintain: (rows: Cache[]) => void, row?: Cache,
|
|
19
|
+
readonly fusedDecode = turboQuantFusedDecode()) {
|
|
20
|
+
super({ signature: `kv:delayed-turboquant:${kBits}:${vBits}:${start}`, conversionOffset: start, maintain,
|
|
21
|
+
converted: row => row instanceof TurboQuantKVCache,
|
|
22
|
+
makeLayout: () => new BatchedTurboQuantKVCache(kBits, vBits, fusedDecode) }, row);
|
|
23
|
+
}
|
|
24
|
+
captureDonorRows(): import("./gemma4-base").KvDonorRows {
|
|
25
|
+
return this.packed?.captureDonorRows() ?? captureFullKvDonorRows(this.rows, this.leftPad, this.offset);
|
|
26
|
+
}
|
|
27
|
+
captureDonorAttention(): import("./gemma4-base").KvDonorAttention {
|
|
28
|
+
return decodedKvDonorAttention(this.captureDonorRows());
|
|
29
|
+
}
|
|
30
|
+
get rotatedValueAttention(): RotatedValueAttentionState { return this; }
|
|
31
|
+
#append(k: MlxArray, v: MlxArray, deferred: boolean): [MlxArray, MlxArray] {
|
|
32
|
+
this.advance();
|
|
33
|
+
if (this.packed) {
|
|
34
|
+
this.#rotated = Array.from({ length: this.batchSize! }, () => true);
|
|
35
|
+
return deferred ? this.packed.updateAndFetchDeferredV(k, v) : this.packed.updateAndFetch(k, v);
|
|
36
|
+
}
|
|
37
|
+
this.#rotated = this.rows.map(row => !!row.rotatedValueAttention);
|
|
38
|
+
const result = appendFullKvRows(this.rows, this.leftPad, this.offset + k.shape[2]!, k, v, deferred);
|
|
39
|
+
try { this.syncPositions(true); } catch (error) { result[0].dispose(); result[1].dispose(); throw error; }
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] { return this.#append(k, v, false); }
|
|
44
|
+
updateAndFetchDeferredV(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] { return this.#append(k, v, true); }
|
|
45
|
+
captureValueTransform(): (output: MlxArray) => MlxArray {
|
|
46
|
+
const rotated = [...this.#rotated];
|
|
47
|
+
if (rotated.every(value => value)) return unrotateValues;
|
|
48
|
+
if (rotated.every(value => !value)) return output => ops.contiguous(output);
|
|
49
|
+
return output => {
|
|
50
|
+
using restored = unrotateValues(output);
|
|
51
|
+
using flags = ops.fromInt32(rotated.map(value => Number(value)), [rotated.length, 1, 1, 1]);
|
|
52
|
+
using mask = flags.astype(Dtype.bool);
|
|
53
|
+
return ops.where(mask, restored, output);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
makeEmptyBatch(): DelayedTurboQuantKVCache { return new DelayedTurboQuantKVCache(this.kBits, this.vBits, this.start, this.maintain, undefined, this.fusedDecode); }
|
|
57
|
+
}
|
package/src/model/factory.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type Glm52MemoryPlan,
|
|
24
24
|
} from "./glm52-memory";
|
|
25
25
|
import { UniversalDenseModel } from "./universal/dense";
|
|
26
|
+
import { WhisperModel } from "./whisper";
|
|
26
27
|
import { genericArgsFor } from "./universal/archs";
|
|
27
28
|
import {
|
|
28
29
|
ModelImplementationRegistry,
|
|
@@ -134,6 +135,26 @@ export async function openGlm52RuntimeModel(
|
|
|
134
135
|
return { model, plan };
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
/** Open a Whisper checkpoint (encoder-decoder speech model) through the same
|
|
139
|
+
* profile resolution as text models. Whisper never enters the chat loop, so
|
|
140
|
+
* it is not a RuntimeModel: the transcription engine (src/audio/whisper-*)
|
|
141
|
+
* owns it. */
|
|
142
|
+
export async function openWhisperModel(
|
|
143
|
+
modelDir: string, options: { readonly profiles?: ResolveModelProfileOptions } = {},
|
|
144
|
+
): Promise<{ model: WhisperModel; profile: ResolvedModelProfile; config: ModelConfig }> {
|
|
145
|
+
const config = await loadModelConfig(modelDir);
|
|
146
|
+
const profile = resolveModelProfile(config, options.profiles);
|
|
147
|
+
if (profile.profile.execution.graph !== "whisper")
|
|
148
|
+
throw new Error(`${modelDir} is not a Whisper checkpoint (profile ${profile.profile.id})`);
|
|
149
|
+
const weights = await Weights.open(modelDir);
|
|
150
|
+
try {
|
|
151
|
+
return { model: new WhisperModel(weights, config), profile, config };
|
|
152
|
+
} catch (error) {
|
|
153
|
+
weights.dispose();
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
137
158
|
/**
|
|
138
159
|
* Artifact-aware construction. GLM-5.2's Colibri snapshot has no ordinary
|
|
139
160
|
* model.safetensors.index.json, so it must bypass Weights.open and use its
|
|
@@ -155,6 +176,11 @@ export async function openModel<Model>(
|
|
|
155
176
|
throw new Error("custom Colibri implementations require a streamed loader binding; refusing to fall back");
|
|
156
177
|
return (await openGlm52RuntimeModel(modelDir, options)).model;
|
|
157
178
|
}
|
|
179
|
+
if (profile.profile.execution.loop === "encoder-decoder")
|
|
180
|
+
throw new Error(
|
|
181
|
+
`${modelDir} is a speech model (${profile.profile.id}); open it with openWhisperModel ` +
|
|
182
|
+
"or serve it as a transcription model — it has no chat/completion loop",
|
|
183
|
+
);
|
|
158
184
|
// Resolve before opening weights; missing or incompatible code allocates nothing.
|
|
159
185
|
const implementation = (options.implementations ?? MLX_MODEL_IMPLEMENTATIONS).select(config, profile);
|
|
160
186
|
const weights = await Weights.open(modelDir);
|
package/src/model/fingerprint.ts
CHANGED
|
@@ -31,3 +31,15 @@ export function configFingerprint(config: ModelConfig): string {
|
|
|
31
31
|
h.update(payload);
|
|
32
32
|
return h.digest("hex").slice(0, 16);
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/** Exact immutable model-component identity, calculated once while its owner
|
|
36
|
+
* holds the native execution lease. Names/shapes/dtypes disambiguate layouts. */
|
|
37
|
+
export function tensorFingerprint(entries: Iterable<readonly [string,
|
|
38
|
+
Pick<import("../mlx/array").MlxArray, "dtype" | "shape" | "rawBytesView">]>): string {
|
|
39
|
+
const hash = new Bun.CryptoHasher("sha256");
|
|
40
|
+
for (const [name, tensor] of [...entries].sort(([a], [b]) => a.localeCompare(b))) {
|
|
41
|
+
hash.update(JSON.stringify([name, tensor.dtype, tensor.shape]));
|
|
42
|
+
hash.update(tensor.rawBytesView());
|
|
43
|
+
}
|
|
44
|
+
return hash.digest("hex");
|
|
45
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import type { PrefillPadding } from "./gemma4-base";
|
|
4
|
+
import { rowRollIndices } from "./batched-row-storage";
|
|
5
|
+
import { disposeResources } from "../engine/resources";
|
|
6
|
+
|
|
7
|
+
interface Positions { rowOffsets: number[]; leftPad: number[] }
|
|
8
|
+
|
|
9
|
+
/** mlx-lm BatchKVCache.prepare/finalize (MIT), over arbitrary stored planes.
|
|
10
|
+
* Padding changes token positions; the representation's codec is uninvolved. */
|
|
11
|
+
export class FullPrefillPadding {
|
|
12
|
+
constructor(readonly tokenAxis = 2) {}
|
|
13
|
+
#ends?: number[];
|
|
14
|
+
get endOffsets(): readonly number[] | undefined { return this.#ends; }
|
|
15
|
+
restoreEnds(ends: readonly number[] | undefined): void { this.#ends = ends ? [...ends] : undefined; }
|
|
16
|
+
validOffset(offset: number, row: number): number { return Math.min(offset, this.#ends?.[row] ?? offset); }
|
|
17
|
+
|
|
18
|
+
prepare(positions: Positions, padding: PrefillPadding): void {
|
|
19
|
+
if (!positions.rowOffsets.length) {
|
|
20
|
+
positions.rowOffsets = padding.lengths.map(() => 0);
|
|
21
|
+
positions.leftPad = [...positions.rowOffsets];
|
|
22
|
+
}
|
|
23
|
+
if (padding.leftPadding) {
|
|
24
|
+
positions.leftPad = positions.leftPad.map((pad, row) => pad + padding.leftPadding![row]!);
|
|
25
|
+
positions.rowOffsets = positions.rowOffsets.map((offset, row) => offset - padding.leftPadding![row]!);
|
|
26
|
+
}
|
|
27
|
+
// Like rotating storage, retain absolute valid ends. A cohort can finish
|
|
28
|
+
// before its planned width when a longer request leaves during prefill.
|
|
29
|
+
this.#ends = padding.rightPadding?.some(pad => pad > 0)
|
|
30
|
+
? padding.lengths.map((length, row) => positions.rowOffsets[row]! + length) : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Borrow planes; return owned replacements only when padding needs moving.
|
|
34
|
+
* Stage every plane before committing positions, including on FFI failure. */
|
|
35
|
+
finalize(planes: readonly MlxArray[], positions: Positions): MlxArray[] | undefined {
|
|
36
|
+
if (!this.#ends) return;
|
|
37
|
+
const padding = this.#ends.map((end, row) => Math.max(0, positions.rowOffsets[row]! - end));
|
|
38
|
+
if (!padding.some(pad => pad > 0)) { this.#ends = undefined; return; }
|
|
39
|
+
const next: MlxArray[] = [];
|
|
40
|
+
try {
|
|
41
|
+
if (planes.length) {
|
|
42
|
+
const width = planes[0]!.shape[this.tokenAxis]!;
|
|
43
|
+
using indices = rowRollIndices(width, padding);
|
|
44
|
+
using compressed = this.tokenAxis === 1 ? ops.reshape(indices, [padding.length, width, 1]) : null;
|
|
45
|
+
for (const plane of planes) next.push(ops.takeAlongAxis(plane, compressed ?? indices, this.tokenAxis));
|
|
46
|
+
}
|
|
47
|
+
} catch (error) { disposeResources(next); throw error; }
|
|
48
|
+
positions.rowOffsets = positions.rowOffsets.map((offset, row) => offset - padding[row]!);
|
|
49
|
+
positions.leftPad = positions.leftPad.map((pad, row) => pad + padding[row]!);
|
|
50
|
+
this.#ends = undefined;
|
|
51
|
+
return next;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
filter(keep: readonly number[]): void { if (this.#ends) this.#ends = keep.map(row => this.#ends![row]!); }
|
|
55
|
+
clear(): void { this.#ends = undefined; }
|
|
56
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import { materializeCopy } from "../mlx/materialize";
|
|
3
|
+
import { withResource, disposeResources } from "../engine/resources";
|
|
4
|
+
import { leaseCacheState } from "../backends/mlx/state-views";
|
|
5
|
+
import { FullPrefillPadding } from "./full-prefill-padding";
|
|
6
|
+
import { KVCache, QuantizedKVCache, TurboQuantKVCache, type Cache, type Mask, type PrefillPadding } from "./gemma4-base";
|
|
7
|
+
|
|
8
|
+
type FullCache = KVCache | QuantizedKVCache | TurboQuantKVCache;
|
|
9
|
+
|
|
10
|
+
/** A physical full-attention row with separate valid-token coverage. Codecs
|
|
11
|
+
* consume the same physical tensors; padding never advances their threshold. */
|
|
12
|
+
export class FullPrefillRow implements Cache {
|
|
13
|
+
readonly padding = new FullPrefillPadding();
|
|
14
|
+
leftPadding = 0;
|
|
15
|
+
constructor(public inner: FullCache) {}
|
|
16
|
+
get minimumReusableOffset(): number { return (this.inner as Cache).minimumReusableOffset ?? 0; }
|
|
17
|
+
set minimumReusableOffset(value: number) { (this.inner as Cache).minimumReusableOffset = value; }
|
|
18
|
+
get positionOffset(): number { return this.inner.offset - this.leftPadding; }
|
|
19
|
+
get physicalLength(): number { return this.inner.offset; }
|
|
20
|
+
get offset(): number { return Math.max(0, this.padding.validOffset(this.positionOffset, 0)); }
|
|
21
|
+
get stateNeedsDispose(): boolean { return (this.inner as Cache).stateNeedsDispose ?? false; }
|
|
22
|
+
get affineConversion(): FullPrefillRow | undefined { return this.inner instanceof KVCache ? this : undefined; }
|
|
23
|
+
get turboConversion(): FullPrefillRow | undefined { return this.inner instanceof KVCache ? this : undefined; }
|
|
24
|
+
get quantizedAttention() { return (this.inner as Cache).quantizedAttention; }
|
|
25
|
+
get rotatedValueAttention() { return (this.inner as Cache).rotatedValueAttention; }
|
|
26
|
+
signature(): string { return this.inner.signature(); }
|
|
27
|
+
state(): MlxArray[] { return this.inner.state(); }
|
|
28
|
+
makeMask(tokens: number, window: number | null): Mask { return this.inner.makeMask(tokens, window); }
|
|
29
|
+
isTrimmable(): boolean { return true; }
|
|
30
|
+
trim(count: number): void { this.inner.trim(count); }
|
|
31
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] { return this.inner.updateAndFetch(k, v); }
|
|
32
|
+
bytesPerToken(): number { return (this.inner as Cache).bytesPerToken?.() ?? 0; }
|
|
33
|
+
preparePrefill(padding: PrefillPadding): void {
|
|
34
|
+
const positions = { rowOffsets: [this.positionOffset], leftPad: [this.leftPadding] };
|
|
35
|
+
this.padding.prepare(positions, padding);
|
|
36
|
+
this.leftPadding = positions.leftPad[0]!;
|
|
37
|
+
}
|
|
38
|
+
finalizePrefill(): void {
|
|
39
|
+
const positions = { rowOffsets: [this.positionOffset], leftPad: [this.leftPadding] };
|
|
40
|
+
const next = withResource(leaseCacheState(this.inner), state => this.padding.finalize(state, positions));
|
|
41
|
+
if (next) this.replace(next, this.inner.offset);
|
|
42
|
+
this.leftPadding = positions.leftPad[0]!;
|
|
43
|
+
}
|
|
44
|
+
removeLeftPadding(count: number): void {
|
|
45
|
+
if (!count) return;
|
|
46
|
+
const next: MlxArray[] = [];
|
|
47
|
+
try {
|
|
48
|
+
withResource(leaseCacheState(this.inner), state => {
|
|
49
|
+
for (const plane of state) next.push(plane.slice([0, 0, count, 0], [...plane.shape]));
|
|
50
|
+
});
|
|
51
|
+
this.replace(next, this.inner.offset - count); next.length = 0;
|
|
52
|
+
this.leftPadding -= count;
|
|
53
|
+
} finally { disposeResources(next); }
|
|
54
|
+
}
|
|
55
|
+
private restored(planes: MlxArray[], offset: number): FullCache {
|
|
56
|
+
const source = this.inner;
|
|
57
|
+
if (source instanceof KVCache) {
|
|
58
|
+
const result = new KVCache();
|
|
59
|
+
if (planes.length) result.restoreState(planes[0]!, planes[1]!, offset);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
if (source instanceof QuantizedKVCache) {
|
|
63
|
+
const result = new QuantizedKVCache(source.groupSize, source.bits);
|
|
64
|
+
if (planes.length) result.restoreState({ packed: planes[0]!, scales: planes[1]!, biases: planes[2]! },
|
|
65
|
+
{ packed: planes[3]!, scales: planes[4]!, biases: planes[5]! }, offset);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
const result = new TurboQuantKVCache(source.kBits, source.vBits, source.fusedDecode);
|
|
69
|
+
if (planes.length) result.restoreState({ kIdx: planes[0]!, kScales: planes[1]!, kZeros: planes[2]!,
|
|
70
|
+
vPacked: planes[3]!, vScales: planes[4]! }, offset, source.headDim!);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
private replace(planes: MlxArray[], offset: number): void {
|
|
74
|
+
const next = this.restored(planes, offset), previous = this.inner;
|
|
75
|
+
(next as Cache).minimumReusableOffset = this.minimumReusableOffset;
|
|
76
|
+
this.inner = next; previous.dispose();
|
|
77
|
+
}
|
|
78
|
+
extract(): Cache {
|
|
79
|
+
const count = this.offset, planes: MlxArray[] = [];
|
|
80
|
+
try {
|
|
81
|
+
if (count) withResource(leaseCacheState(this.inner), state => {
|
|
82
|
+
for (const plane of state) {
|
|
83
|
+
using view = plane.slice([0, 0, this.leftPadding, 0], [1, plane.shape[1]!, this.leftPadding + count, plane.shape[3]!]);
|
|
84
|
+
planes.push(materializeCopy(view));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const result = this.restored(planes, count); planes.length = 0;
|
|
88
|
+
(result as Cache).minimumReusableOffset = this.minimumReusableOffset;
|
|
89
|
+
return result;
|
|
90
|
+
} finally { disposeResources(planes); }
|
|
91
|
+
}
|
|
92
|
+
toQuantized(groupSize: number, bits: number): FullPrefillRow {
|
|
93
|
+
this.inner = (this.inner as KVCache).toQuantized(groupSize, bits);
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
toTurboQuantized(kBits: number, vBits: number): FullPrefillRow {
|
|
97
|
+
this.inner = TurboQuantKVCache.fromKVCache(this.inner as KVCache, kBits, vBits);
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
dispose(): void { this.inner.dispose(); this.padding.clear(); }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function fullRowInner(row: Cache): Cache { return row instanceof FullPrefillRow ? row.inner : row; }
|
|
104
|
+
export function fullRowPadding(row: Cache): number { return row instanceof FullPrefillRow ? row.leftPadding : 0; }
|
|
105
|
+
export function fullRowPhysicalLength(row: Cache): number { return row instanceof FullPrefillRow ? row.physicalLength : row.offset; }
|