mlx-bun 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/README.md +4 -1
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/design/batching.md +1321 -49
- package/docs/design/decode-speed-program.md +331 -1163
- package/docs/design/generic-model-support.md +79 -3
- package/docs/design/kv-cache.md +198 -54
- package/docs/design/speculative-decoding.md +387 -24
- package/docs/design/turboquant.md +123 -13
- package/docs/design/unified-engine-frontier-plan.md +144 -142
- package/docs/planning/PRODUCT_ROADMAP.md +16 -0
- package/docs/planning/release-notes-v0.4.0.md +45 -0
- package/docs/reference/benchmarks.md +2457 -6
- package/docs/reference/cli.md +41 -2
- package/docs/reference/distribution.md +16 -9
- package/docs/reference/environment.md +55 -5
- package/docs/reference/models.md +13 -4
- package/docs/reference/server-api.md +79 -25
- package/docs/reference/server-config.md +178 -84
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +1 -1
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/batch-group.ts +368 -224
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +106 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- 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 +77 -18
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +133 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/model-serving.ts +1 -1
- package/src/backends/mlx/prefill-cohort.ts +89 -0
- package/src/backends/mlx/prefill-rows.ts +200 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/request-state-policy.ts +41 -0
- package/src/backends/mlx/rollback.ts +41 -3
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +35 -66
- package/src/backends/mlx/speculative-group.ts +359 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- 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 +165 -0
- package/src/cli.ts +11 -36
- package/src/contracts/execution.ts +11 -0
- package/src/contracts/prefix-cache.ts +23 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +18 -0
- package/src/engine/execution-plan.ts +18 -16
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +10 -5
- package/src/fit.ts +29 -15
- package/src/generate.ts +77 -173
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +70 -12
- package/src/kv-store.ts +70 -16
- package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
- package/src/lab/paged-kv/paged-kv.ts +25 -11
- package/src/mlx/array.ts +2 -1
- 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 +93 -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 +55 -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 +162 -80
- package/src/model/gemma4.ts +34 -26
- 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 +67 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3_5.ts +82 -42
- 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/trellis-linear.ts +2 -2
- package/src/model/turboquant-codec.ts +83 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +42 -44
- package/src/sampler.ts +36 -25
- package/src/serve/chat-stage.ts +2 -2
- package/src/serve/generation-gateway.ts +39 -30
- package/src/serve/model-host.ts +3 -4
- package/src/serve/request-plan.ts +4 -3
- package/src/serve/request-prep.ts +2 -4
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/server.ts +64 -108
- package/src/spec/assistant-rows.ts +92 -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 +53 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +92 -20
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +219 -0
- package/src/spec/qwen-mtp-source.ts +81 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +70 -68
- package/src/spec/source.ts +80 -15
- package/src/spec/two-model-rows.ts +61 -0
- package/src/spec/two-model.ts +43 -101
- package/src/ssd-cache.ts +33 -21
- package/src/ssd-durability.ts +11 -7
- package/src/web/app.js +57 -57
- 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
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
import type { PrefillPadding } from "./gemma4-base";
|
|
2
|
+
|
|
3
|
+
export interface RotatingPositionSnapshot {
|
|
4
|
+
readonly maxSize: number;
|
|
5
|
+
readonly offsets: readonly number[];
|
|
6
|
+
readonly leftPad: readonly number[];
|
|
7
|
+
readonly ringIndex: number;
|
|
8
|
+
readonly totalOffset: number;
|
|
9
|
+
readonly rotated: boolean;
|
|
10
|
+
readonly prefillEnds?: readonly number[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Acceptance changes row positions, independently of plain or encoded planes. */
|
|
14
|
+
export function rotatingRollbackPlan(snapshot: RotatingPositionSnapshot, before: readonly number[],
|
|
15
|
+
keep: number | readonly number[], preserveColumns = false): { trim: number } | { shifts: number[]; position: RotatingPositionSnapshot } {
|
|
16
|
+
const counts = before.map((_, row) => typeof keep === "number" ? keep : keep[row]!);
|
|
17
|
+
if (!preserveColumns && before.length === 1 && snapshot.totalOffset < snapshot.maxSize)
|
|
18
|
+
return { trim: snapshot.offsets[0]! - before[0]! - counts[0]! };
|
|
19
|
+
const offsets = before.map((offset, row) => offset + counts[row]!);
|
|
20
|
+
const shifts = snapshot.offsets.map((offset, row) => offset - offsets[row]!);
|
|
21
|
+
return { shifts, position: { ...snapshot, offsets,
|
|
22
|
+
leftPad: snapshot.leftPad.map((pad, row) => pad + shifts[row]!) } };
|
|
23
|
+
}
|
|
24
|
+
|
|
1
25
|
/** Host-side row and ring bookkeeping shared by plain and quantized storage. */
|
|
2
26
|
export class BatchedRotatingState {
|
|
3
27
|
readonly maxSize: number;
|
|
@@ -6,6 +30,7 @@ export class BatchedRotatingState {
|
|
|
6
30
|
ringIndex = 0;
|
|
7
31
|
totalOffset = 0;
|
|
8
32
|
rotated = false;
|
|
33
|
+
#prefillEnds?: number[];
|
|
9
34
|
|
|
10
35
|
constructor(maxSize: number, leftPad: readonly number[], offsets?: readonly number[]) {
|
|
11
36
|
this.maxSize = maxSize;
|
|
@@ -13,8 +38,45 @@ export class BatchedRotatingState {
|
|
|
13
38
|
this.offsets = offsets ? [...offsets] : leftPad.map((pad) => -pad);
|
|
14
39
|
}
|
|
15
40
|
|
|
41
|
+
snapshot(): RotatingPositionSnapshot {
|
|
42
|
+
return { maxSize: this.maxSize, offsets: [...this.offsets], leftPad: [...this.leftPad],
|
|
43
|
+
ringIndex: this.ringIndex, totalOffset: this.totalOffset, rotated: this.rotated,
|
|
44
|
+
...(this.#prefillEnds ? { prefillEnds: [...this.#prefillEnds] } : {}) };
|
|
45
|
+
}
|
|
46
|
+
restore(snapshot: RotatingPositionSnapshot): void {
|
|
47
|
+
this.offsets = [...snapshot.offsets]; this.leftPad = [...snapshot.leftPad];
|
|
48
|
+
this.ringIndex = snapshot.ringIndex; this.totalOffset = snapshot.totalOffset; this.rotated = snapshot.rotated;
|
|
49
|
+
this.#prefillEnds = snapshot.prefillEnds ? [...snapshot.prefillEnds] : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
preparePrefill(padding: PrefillPadding, rightPaddedBatch = padding.rightPadding?.some(pad => pad > 0)): void {
|
|
53
|
+
if (!this.batchSize) { this.leftPad = padding.lengths.map(() => 0); this.offsets = [...this.leftPad]; }
|
|
54
|
+
if (padding.leftPadding) {
|
|
55
|
+
this.leftPad = this.leftPad.map((pad, row) => pad + padding.leftPadding![row]!);
|
|
56
|
+
this.offsets = this.offsets.map((offset, row) => offset - padding.leftPadding![row]!);
|
|
57
|
+
}
|
|
58
|
+
this.#prefillEnds = rightPaddedBatch
|
|
59
|
+
? padding.lengths.map((length, row) => length + this.offsets[row]!) : undefined;
|
|
60
|
+
}
|
|
61
|
+
get hasPendingPadding(): boolean { return this.#prefillEnds !== undefined; }
|
|
62
|
+
validOffset(row: number): number {
|
|
63
|
+
return Math.min(this.offsets[row]!, this.#prefillEnds?.[row] ?? this.offsets[row]!);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
prefillRoll(): number[] | undefined {
|
|
67
|
+
return this.#prefillEnds?.map((end, row) => Math.max(0, this.offsets[row]! - end));
|
|
68
|
+
}
|
|
69
|
+
#rollPadding(): void {
|
|
70
|
+
const shifts = this.prefillRoll();
|
|
71
|
+
if (!shifts) return;
|
|
72
|
+
this.leftPad = this.leftPad.map((pad, row) => pad + shifts[row]!);
|
|
73
|
+
this.offsets = this.offsets.map((offset, row) => offset - shifts[row]!);
|
|
74
|
+
}
|
|
75
|
+
finalizePrefill(): void { this.#rollPadding(); this.#prefillEnds = undefined; }
|
|
76
|
+
|
|
16
77
|
get batchSize(): number { return this.leftPad.length; }
|
|
17
78
|
get validLength(): number { return Math.min(this.totalOffset, this.maxSize); }
|
|
79
|
+
get activeLength(): number { return this.rotated ? this.maxSize : this.ringIndex; }
|
|
18
80
|
get trimmable(): boolean { return this.totalOffset < this.maxSize; }
|
|
19
81
|
|
|
20
82
|
markGrown(previousOffset: number): void {
|
|
@@ -43,9 +105,22 @@ export class BatchedRotatingState {
|
|
|
43
105
|
this.ringIndex += tokens;
|
|
44
106
|
}
|
|
45
107
|
|
|
108
|
+
/** mlx-lm BatchRotatingKVCache._update_concat: the block keeps up to
|
|
109
|
+
* maxSize - 1 historical columns so each query has a complete window. */
|
|
110
|
+
commitConcat(tokens: number, historicalLength: number): void {
|
|
111
|
+
if (historicalLength) this.#rollPadding();
|
|
112
|
+
const trim = Math.max(0, historicalLength - this.maxSize + 1);
|
|
113
|
+
this.leftPad = this.leftPad.map(pad => pad - trim);
|
|
114
|
+
this.totalOffset += tokens;
|
|
115
|
+
this.offsets = this.offsets.map(offset => offset + tokens);
|
|
116
|
+
this.ringIndex = historicalLength - trim + tokens;
|
|
117
|
+
this.rotated = false;
|
|
118
|
+
}
|
|
119
|
+
|
|
46
120
|
filter(keep: readonly number[]): void {
|
|
47
121
|
this.offsets = keep.map((row) => this.offsets[row]!);
|
|
48
122
|
this.leftPad = keep.map((row) => this.leftPad[row]!);
|
|
123
|
+
if (this.#prefillEnds) this.#prefillEnds = keep.map(row => this.#prefillEnds![row]!);
|
|
49
124
|
}
|
|
50
125
|
|
|
51
126
|
trim(tokens: number): number {
|
|
@@ -61,6 +136,7 @@ export class BatchedRotatingState {
|
|
|
61
136
|
this.ringIndex = width;
|
|
62
137
|
this.offsets = [...offsets];
|
|
63
138
|
this.rotated = false;
|
|
139
|
+
this.#prefillEnds = undefined;
|
|
64
140
|
}
|
|
65
141
|
|
|
66
142
|
/** Physical ranges that expose a ring buffer in temporal order. */
|
|
@@ -6,12 +6,10 @@
|
|
|
6
6
|
// column is the same for every row), with PER-ROW `offset` (absolute position,
|
|
7
7
|
// drives RoPE) and `leftPad` (padding columns in the buffer, masked out).
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// (
|
|
13
|
-
// solo-prefills each request (single-stream RotatingKVCache) then merges, so
|
|
14
|
-
// this cache only ever sees N=1 updates. Positions (offset/leftPad/_idx/_offset/
|
|
9
|
+
// Includes merge, one-token writes, block prefill, padding finalization, masks
|
|
10
|
+
// and row extraction. Prefill preparation is a storage capability; the serving
|
|
11
|
+
// scheduler's use of whole prompt cohorts is a separate concern.
|
|
12
|
+
// Positions (offset/leftPad/_idx/_offset/
|
|
15
13
|
// rotated) are tracked on the HOST (small deterministic ints) so make_mask is
|
|
16
14
|
// built in JS like buildBatchedDecodeMask; only K/V live on device.
|
|
17
15
|
//
|
|
@@ -24,12 +22,14 @@
|
|
|
24
22
|
|
|
25
23
|
import { MlxArray } from "../mlx/array";
|
|
26
24
|
import * as ops from "../mlx/ops";
|
|
27
|
-
import { RotatingKVCache, type Cache, type Mask } from "./gemma4-base";
|
|
28
|
-
import { BatchedRotatingState } from "./batched-rotating-state";
|
|
25
|
+
import { RotatingKVCache, type Cache, type Mask, type PaddedPrefillCache, type PrefillPadding } from "./gemma4-base";
|
|
26
|
+
import { BatchedRotatingState, type RotatingPositionSnapshot } from "./batched-rotating-state";
|
|
29
27
|
import {
|
|
28
|
+
appendRotatingStorage,
|
|
30
29
|
mergeStorageRows,
|
|
31
30
|
plainRowStorage,
|
|
32
31
|
temporalStorageView,
|
|
32
|
+
rowRollIndices,
|
|
33
33
|
} from "./batched-row-storage";
|
|
34
34
|
|
|
35
35
|
const STEP = 256;
|
|
@@ -45,14 +45,14 @@ const STEP = 256;
|
|
|
45
45
|
* update's decrement is the persistent one, applied separately by the cache). */
|
|
46
46
|
export function buildBatchedRotatingMask(
|
|
47
47
|
B: number, N: number, leftPad: number[],
|
|
48
|
-
maxSize: number, window: number, idx: number, offsetScalar: number, rotated: boolean,
|
|
48
|
+
maxSize: number, window: number, idx: number, offsetScalar: number, rotated: boolean, block = N > 1,
|
|
49
49
|
): MlxArray {
|
|
50
50
|
const off = Math.min(maxSize - 1, offsetScalar);
|
|
51
51
|
const S = off + N;
|
|
52
52
|
|
|
53
53
|
// Local (non-persisted) leftPad for the mask: trim + rotation shrink it.
|
|
54
|
-
const trimSize = idx - maxSize + (
|
|
55
|
-
const isRot =
|
|
54
|
+
const trimSize = idx - maxSize + (block ? 1 : 0);
|
|
55
|
+
const isRot = !block && (rotated || idx >= maxSize);
|
|
56
56
|
const lp = leftPad.map((x) => x - (trimSize > 0 ? trimSize : 0) - (isRot ? 1 : 0));
|
|
57
57
|
|
|
58
58
|
// roll(shift): physical column = (temporal column + shift) mod S.
|
|
@@ -80,8 +80,9 @@ export function buildBatchedRotatingMask(
|
|
|
80
80
|
return mask;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
83
|
+
/** Port of mlx-lm BatchRotatingKVCache; padded singleton chunks use its
|
|
84
|
+
* concat operation with the matching block mask until finalization. */
|
|
85
|
+
export class BatchedRotatingCache implements Cache, PaddedPrefillCache {
|
|
85
86
|
keys: MlxArray | null = null;
|
|
86
87
|
values: MlxArray | null = null;
|
|
87
88
|
readonly #rows: BatchedRotatingState;
|
|
@@ -103,6 +104,29 @@ export class BatchedRotatingCache implements Cache {
|
|
|
103
104
|
* misroutes a batched cache into a serial-only path. */
|
|
104
105
|
signature(): string { return "kv:rotating-plain"; }
|
|
105
106
|
|
|
107
|
+
get positionSnapshot(): RotatingPositionSnapshot { return this.#rows.snapshot(); }
|
|
108
|
+
validOffset(row: number): number { return this.#rows.validOffset(row); }
|
|
109
|
+
preparePrefill(padding: PrefillPadding, rightPaddedBatch?: boolean): void { this.#rows.preparePrefill(padding, rightPaddedBatch); this.releaseRopeArr(); }
|
|
110
|
+
finalizePrefill(): void {
|
|
111
|
+
const shifts = this.#rows.prefillRoll();
|
|
112
|
+
if (shifts?.some(shift => shift !== 0) && this.keys && this.values) {
|
|
113
|
+
using indices = rowRollIndices(this.keys.shape[2]!, shifts);
|
|
114
|
+
const keys = plainRowStorage.rollRows(this.keys, indices);
|
|
115
|
+
let values: MlxArray;
|
|
116
|
+
try { values = plainRowStorage.rollRows(this.values, indices); }
|
|
117
|
+
catch (error) { keys.dispose(); throw error; }
|
|
118
|
+
this.keys.dispose(); this.values.dispose(); this.keys = keys; this.values = values;
|
|
119
|
+
}
|
|
120
|
+
this.#rows.finalizePrefill(); this.releaseRopeArr();
|
|
121
|
+
}
|
|
122
|
+
/** Adopt owned tensors without changing their physical ring columns. */
|
|
123
|
+
static adoptPhysical(keys: MlxArray | null, values: MlxArray | null, position: RotatingPositionSnapshot): BatchedRotatingCache {
|
|
124
|
+
const cache = new BatchedRotatingCache(position.maxSize, [...position.leftPad]);
|
|
125
|
+
cache.#rows.restore(position); cache.keys = keys; cache.values = values;
|
|
126
|
+
|
|
127
|
+
return cache;
|
|
128
|
+
}
|
|
129
|
+
|
|
106
130
|
get offsetArr(): number[] { return this.#rows.offsets; }
|
|
107
131
|
get leftPad(): number[] { return this.#rows.leftPad; }
|
|
108
132
|
get batchSize(): number { return this.#rows.batchSize; }
|
|
@@ -133,7 +157,7 @@ export class BatchedRotatingCache implements Cache {
|
|
|
133
157
|
mode: "array",
|
|
134
158
|
arr: buildBatchedRotatingMask(
|
|
135
159
|
this.#B, N, this.leftPad, this.maxSize, window,
|
|
136
|
-
this.#rows.ringIndex, this.offset, this.#rows.rotated,
|
|
160
|
+
this.#rows.ringIndex, this.offset, this.#rows.rotated, N > 1 || this.#rows.hasPendingPadding,
|
|
137
161
|
),
|
|
138
162
|
};
|
|
139
163
|
}
|
|
@@ -142,8 +166,7 @@ export class BatchedRotatingCache implements Cache {
|
|
|
142
166
|
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
143
167
|
const [B, H, S, D] = k.shape as [number, number, number, number];
|
|
144
168
|
const vD = v.shape[3]!;
|
|
145
|
-
if (S !== 1)
|
|
146
|
-
throw new Error("BatchedRotatingCache supports N=1 decode updates only (solo-prefill then merge)");
|
|
169
|
+
if (S !== 1 || this.#rows.hasPendingPadding) return this.#updateConcat(k, v);
|
|
147
170
|
const prev = this.offset;
|
|
148
171
|
|
|
149
172
|
// Grow the buffer (in STEP chunks) until it reaches maxSize.
|
|
@@ -202,6 +225,21 @@ export class BatchedRotatingCache implements Cache {
|
|
|
202
225
|
];
|
|
203
226
|
}
|
|
204
227
|
|
|
228
|
+
#updateConcat(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
229
|
+
const keys = appendRotatingStorage(plainRowStorage, this.keys, k, this.#rows);
|
|
230
|
+
let values: MlxArray;
|
|
231
|
+
try { values = appendRotatingStorage(plainRowStorage, this.values, v, this.#rows); }
|
|
232
|
+
catch (error) { keys.dispose(); throw error; }
|
|
233
|
+
const history = this.keys ? this.#rows.activeLength : 0;
|
|
234
|
+
this.keys?.dispose(); this.values?.dispose();
|
|
235
|
+
this.keys = keys; this.values = values;
|
|
236
|
+
this.#rows.commitConcat(k.shape[2]!, history);
|
|
237
|
+
return [
|
|
238
|
+
plainRowStorage.slice(keys, 0, this.#B, 0, this.#rows.ringIndex),
|
|
239
|
+
plainRowStorage.slice(values, 0, this.#B, 0, this.#rows.ringIndex),
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
|
|
205
243
|
/** Free the per-step RoPE array without disposing KV (wrapper-rebuild path). */
|
|
206
244
|
releaseRopeArr(): void {
|
|
207
245
|
this.#ropeArr?.dispose();
|
|
@@ -213,8 +251,12 @@ export class BatchedRotatingCache implements Cache {
|
|
|
213
251
|
temporalView(): [MlxArray, MlxArray] {
|
|
214
252
|
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
215
253
|
return [
|
|
216
|
-
temporalStorageView(plainRowStorage, this.keys, this.#rows
|
|
217
|
-
|
|
254
|
+
temporalStorageView(plainRowStorage, this.keys, this.#rows, {
|
|
255
|
+
from: Math.max(0, this.#rows.activeLength - this.maxSize), to: this.#rows.activeLength,
|
|
256
|
+
}),
|
|
257
|
+
temporalStorageView(plainRowStorage, this.values, this.#rows, {
|
|
258
|
+
from: Math.max(0, this.#rows.activeLength - this.maxSize), to: this.#rows.activeLength,
|
|
259
|
+
}),
|
|
218
260
|
];
|
|
219
261
|
}
|
|
220
262
|
|
|
@@ -228,15 +270,15 @@ export class BatchedRotatingCache implements Cache {
|
|
|
228
270
|
* oracle's `cache._idx = cache.keys.shape[2]`. Bit-exact vs a solo run:
|
|
229
271
|
* merge/decode/filter keep each row's ring bytes identical to the serial
|
|
230
272
|
* cache's (tests/batched-rotating) and this is a pure slice+copy. */
|
|
231
|
-
extractRow(i: number): RotatingKVCache | null {
|
|
273
|
+
extractRow(i: number, limit?: number): RotatingKVCache | null {
|
|
232
274
|
if (!this.keys || !this.values) return null;
|
|
233
|
-
const pad = Math.max(0, this.leftPad[i]
|
|
275
|
+
const pad = Math.max(0, this.leftPad[i]!, limit === undefined ? 0 : this.#rows.activeLength - limit);
|
|
234
276
|
const c = new RotatingKVCache(this.maxSize);
|
|
235
277
|
const k = temporalStorageView(plainRowStorage, this.keys, this.#rows, {
|
|
236
|
-
row: i, from: pad, copy: true,
|
|
278
|
+
row: i, from: pad, to: this.#rows.activeLength, copy: true,
|
|
237
279
|
});
|
|
238
280
|
const v = temporalStorageView(plainRowStorage, this.values, this.#rows, {
|
|
239
|
-
row: i, from: pad, copy: true,
|
|
281
|
+
row: i, from: pad, to: this.#rows.activeLength, copy: true,
|
|
240
282
|
});
|
|
241
283
|
c.restoreState(k, v, this.offsetArr[i]!, k.shape[2]!);
|
|
242
284
|
return c;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MlxArray } from "../mlx/array";
|
|
2
2
|
import * as ops from "../mlx/ops";
|
|
3
3
|
import { BatchedRotatingState } from "./batched-rotating-state";
|
|
4
|
+
import { Dtype } from "../mlx/ffi";
|
|
4
5
|
|
|
5
6
|
export interface RowStorage<T> {
|
|
6
7
|
shape(value: T): readonly [number, number, number, number];
|
|
@@ -10,6 +11,7 @@ export interface RowStorage<T> {
|
|
|
10
11
|
padLeft(value: T, tokens: number): T;
|
|
11
12
|
takeRows(value: T, keep: readonly number[]): T;
|
|
12
13
|
copy(value: T): T;
|
|
14
|
+
rollRows(value: T, indices: MlxArray): T;
|
|
13
15
|
dispose(value: T): void;
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -36,6 +38,7 @@ export const plainRowStorage: RowStorage<MlxArray> = {
|
|
|
36
38
|
return out;
|
|
37
39
|
},
|
|
38
40
|
copy: (value) => ops.copyOf(value),
|
|
41
|
+
rollRows: (value, indices) => ops.takeAlongAxis(value, indices, 2),
|
|
39
42
|
dispose: (value) => value.dispose(),
|
|
40
43
|
};
|
|
41
44
|
|
|
@@ -77,6 +80,14 @@ export const quantizedRowStorage: RowStorage<ops.QuantizedTensor> = {
|
|
|
77
80
|
biases: ops.copyOf(value.biases),
|
|
78
81
|
};
|
|
79
82
|
},
|
|
83
|
+
rollRows(value, indices) {
|
|
84
|
+
const packed = ops.takeAlongAxis(value.packed, indices, 2);
|
|
85
|
+
let scales: MlxArray | undefined;
|
|
86
|
+
try {
|
|
87
|
+
scales = ops.takeAlongAxis(value.scales, indices, 2);
|
|
88
|
+
return { packed, scales, biases: ops.takeAlongAxis(value.biases, indices, 2) };
|
|
89
|
+
} catch (error) { packed.dispose(); scales?.dispose(); throw error; }
|
|
90
|
+
},
|
|
80
91
|
dispose(value) {
|
|
81
92
|
value.packed.dispose();
|
|
82
93
|
value.scales.dispose();
|
|
@@ -84,6 +95,19 @@ export const quantizedRowStorage: RowStorage<ops.QuantizedTensor> = {
|
|
|
84
95
|
},
|
|
85
96
|
};
|
|
86
97
|
|
|
98
|
+
/** Per-row cyclic shift, shared by plain and encoded storage planes.
|
|
99
|
+
* This is mlx-lm's dynamic_roll indexing; only positions cross the FFI. */
|
|
100
|
+
export function rowRollIndices(length: number, shifts: readonly number[]): MlxArray {
|
|
101
|
+
using columns = ops.arange(0, length, 1, Dtype.int32);
|
|
102
|
+
using positions = ops.reshape(columns, [1, 1, length, 1]);
|
|
103
|
+
using starts = ops.fromInt32(shifts.map(shift => ((-shift % length) + length) % length), [shifts.length, 1, 1, 1]);
|
|
104
|
+
using raw = ops.add(positions, starts);
|
|
105
|
+
using width = ops.fromInt32([length], []);
|
|
106
|
+
using wrapped = ops.sub(raw, width);
|
|
107
|
+
using over = ops.greaterEqual(raw, width);
|
|
108
|
+
return ops.where(over, wrapped, raw);
|
|
109
|
+
}
|
|
110
|
+
|
|
87
111
|
export function mergeStorageRows<T>(
|
|
88
112
|
storage: RowStorage<T>,
|
|
89
113
|
rows: readonly T[],
|
|
@@ -125,3 +149,31 @@ export function temporalStorageView<T>(
|
|
|
125
149
|
storage.dispose(cut);
|
|
126
150
|
return owned;
|
|
127
151
|
}
|
|
152
|
+
|
|
153
|
+
/** Borrow storage, return an owned chronological block without decoding the
|
|
154
|
+
* representation. Port of mlx-lm's BatchRotatingKVCache._update_concat
|
|
155
|
+
* (MIT): discard spare capacity, retain the full window for the first query. */
|
|
156
|
+
export function appendRotatingStorage<T>(
|
|
157
|
+
storage: RowStorage<T>, previous: T | null, incoming: T, state: BatchedRotatingState,
|
|
158
|
+
): T {
|
|
159
|
+
if (previous === null) return storage.slice(incoming, 0, storage.shape(incoming)[0], 0, storage.shape(incoming)[2]);
|
|
160
|
+
const active = state.activeLength;
|
|
161
|
+
const shifts = state.prefillRoll();
|
|
162
|
+
if (shifts?.some(shift => shift !== 0)) {
|
|
163
|
+
const chronological = temporalStorageView(storage, previous, state, { to: active });
|
|
164
|
+
let rolled: T | undefined, history: T | undefined;
|
|
165
|
+
try {
|
|
166
|
+
using indices = rowRollIndices(active, shifts);
|
|
167
|
+
rolled = storage.rollRows(chronological, indices);
|
|
168
|
+
history = storage.slice(rolled, 0, state.batchSize, Math.max(0, active - state.maxSize + 1), active);
|
|
169
|
+
return storage.concatTokens([history, incoming]);
|
|
170
|
+
} finally {
|
|
171
|
+
storage.dispose(chronological); if (rolled) storage.dispose(rolled); if (history) storage.dispose(history);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const history = temporalStorageView(storage, previous, state, {
|
|
175
|
+
from: Math.max(0, active - state.maxSize + 1), to: active,
|
|
176
|
+
});
|
|
177
|
+
try { return storage.concatTokens([history, incoming]); }
|
|
178
|
+
finally { storage.dispose(history); }
|
|
179
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SsmPrefillPadding } from "./ssm-prefill-padding";
|
|
2
|
+
import type { PaddedPrefillCache, PrefillPadding } from "./gemma4-base";
|
|
3
|
+
import * as ops from "../mlx/ops";
|
|
4
|
+
import type { MlxArray } from "../mlx/array";
|
|
5
|
+
import type { BatchableCache, Cache } from "./gemma4-base";
|
|
6
|
+
import { SSMCache } from "./qwen3-delta";
|
|
7
|
+
|
|
8
|
+
/** Recurrent storage behind the same row-layout port as attention KV.
|
|
9
|
+
* Inputs to merge are borrowed; request membership is owned by the caller. */
|
|
10
|
+
export class BatchedSSMCache extends SSMCache implements BatchableCache, PaddedPrefillCache {
|
|
11
|
+
constructor() { super(); this.offsets = []; }
|
|
12
|
+
preparePrefill(padding: PrefillPadding): void {
|
|
13
|
+
if (!this.offsets!.length) this.offsets = padding.lengths.map(() => 0);
|
|
14
|
+
const padded = padding.leftPadding?.some(n => n > 0) || padding.rightPadding?.some(n => n > 0);
|
|
15
|
+
this.prefillPadding = padded ? new SsmPrefillPadding(padding) : null;
|
|
16
|
+
}
|
|
17
|
+
finalizePrefill(): void { this.prefillPadding = null; }
|
|
18
|
+
get rowOffsets(): readonly number[] { return this.offsets!; }
|
|
19
|
+
get leftPad(): readonly number[] { return this.rowOffsets.map(() => 0); }
|
|
20
|
+
makeEmptyBatch(): BatchedSSMCache { return new BatchedSSMCache(); }
|
|
21
|
+
projectedBytes(_tokens: number): number {
|
|
22
|
+
return this.batchSize ? this.state().reduce((bytes, array) => bytes + array.nbytes, 0) / this.batchSize : 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
26
|
+
if (!rows.length) { this.dispose(); return; }
|
|
27
|
+
const sources = rows as readonly SSMCache[];
|
|
28
|
+
const offsets = sources.flatMap(source => source.offsets ?? [source.offset]);
|
|
29
|
+
const merge = (field: "conv" | "recurrent"): MlxArray | null => {
|
|
30
|
+
const prototype = sources.find(source => source[field])?.[field];
|
|
31
|
+
if (!prototype) return null;
|
|
32
|
+
if (sources.length === 1) return ops.copyOf(prototype);
|
|
33
|
+
const empty: MlxArray[] = [];
|
|
34
|
+
try {
|
|
35
|
+
return ops.concatAxis(sources.map(source => {
|
|
36
|
+
if (source[field]) return source[field]!;
|
|
37
|
+
const zeros = ops.zeros([source.offsets?.length ?? 1, ...prototype.shape.slice(1)], prototype.dtype);
|
|
38
|
+
empty.push(zeros); return zeros;
|
|
39
|
+
}), 0);
|
|
40
|
+
} finally { for (const array of empty) array.dispose(); }
|
|
41
|
+
};
|
|
42
|
+
const conv = merge("conv");
|
|
43
|
+
let recurrent;
|
|
44
|
+
try { recurrent = merge("recurrent"); }
|
|
45
|
+
catch (error) { conv?.dispose(); throw error; }
|
|
46
|
+
this.dispose();
|
|
47
|
+
this.conv = conv; this.recurrent = recurrent;
|
|
48
|
+
this.offsets = offsets; this.offset = Math.max(0, ...offsets);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override dispose(): void { super.dispose(); this.offset = 0; this.offsets = []; }
|
|
52
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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, 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
|
+
this.#codec = new TurboQuantCodec(kBits, vBits, process.env.MLX_BUN_TURBOQUANT_FUSED_DECODE === "1");
|
|
22
|
+
}
|
|
23
|
+
restorePrefillEnds(ends: readonly number[] | undefined): void { this.#storage.restorePrefillEnds(ends); }
|
|
24
|
+
preparePrefill(padding: PrefillPadding): void {
|
|
25
|
+
this.#storage.preparePrefill(padding);
|
|
26
|
+
if (!this.#reuseOffsets.length) this.#reuseOffsets = padding.lengths.map(() => 0);
|
|
27
|
+
}
|
|
28
|
+
finalizePrefill(): void { this.#storage.finalizePrefill(); }
|
|
29
|
+
captureDonorRows(): import("./gemma4-base").KvDonorRows {
|
|
30
|
+
if (!this.#storage.planes.length || this.#headDim === null) throw new Error("cache is empty");
|
|
31
|
+
const [keys, values] = this.#codec.decode(encoded(this.#storage.planes), this.offset, this.#headDim, false);
|
|
32
|
+
return { keys, values, ...this.#storage.captureDonorValidity() };
|
|
33
|
+
}
|
|
34
|
+
captureDonorAttention(): import("./gemma4-base").KvDonorAttention {
|
|
35
|
+
return decodedKvDonorAttention(this.captureDonorRows());
|
|
36
|
+
}
|
|
37
|
+
get rotatedValueAttention(): RotatedValueAttentionState { return this; }
|
|
38
|
+
signature(): string { return `kv:batched-turboquant:${this.kBits}:${this.vBits}`; }
|
|
39
|
+
get minimumReusableOffset(): number { return Math.max(0, ...this.#reuseOffsets); }
|
|
40
|
+
get headDim(): number | null { return this.#headDim; }
|
|
41
|
+
get rowOffsets(): readonly number[] { return this.#storage.rowOffsets; }
|
|
42
|
+
get leftPad(): readonly number[] { return this.#storage.leftPad; }
|
|
43
|
+
get batchSize(): number | null { return this.#storage.batchSize; }
|
|
44
|
+
get offset(): number { return this.#storage.offset; }
|
|
45
|
+
get ropeOffsetArr(): MlxArray | undefined { return this.#storage.ropeOffsetArr; }
|
|
46
|
+
makeEmptyBatch(): BatchedTurboQuantKVCache { return new BatchedTurboQuantKVCache(this.kBits, this.vBits); }
|
|
47
|
+
bytesPerToken(): number { return this.#storage.bytesPerToken(); }
|
|
48
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * tokens; }
|
|
49
|
+
makeMask(tokens: number, window: number | null): Mask { return this.#storage.makeMask(tokens, window); }
|
|
50
|
+
isTrimmable(): boolean { return true; }
|
|
51
|
+
trim(count: number): void { this.#storage.trim(count); }
|
|
52
|
+
specRoundBegin(): void { this.#storage.specRoundBegin(); }
|
|
53
|
+
specRoundCommit(): void { this.#storage.specRoundCommit(); }
|
|
54
|
+
specRoundRollback(keep: number | readonly number[]): void { this.#storage.specRoundRollback(keep); }
|
|
55
|
+
#append(k: MlxArray, v: MlxArray, defer: boolean): [MlxArray, MlxArray] {
|
|
56
|
+
const packed = this.#codec.encode(k, v);
|
|
57
|
+
try {
|
|
58
|
+
this.#storage.append(fields.map(field => packed[field]));
|
|
59
|
+
this.#headDim = k.shape[3]!;
|
|
60
|
+
return this.#codec.decode(encoded(this.#storage.planes), this.offset, this.#headDim, defer);
|
|
61
|
+
} finally { disposeTurboQuant(packed); }
|
|
62
|
+
}
|
|
63
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] { return this.#append(k, v, false); }
|
|
64
|
+
updateAndFetchDeferredV(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] { return this.#append(k, v, true); }
|
|
65
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
66
|
+
const views: KvTensorRowView[] = [], held: MlxArray[] = [];
|
|
67
|
+
const reuseOffsets: number[] = [];
|
|
68
|
+
let dim = this.#headDim;
|
|
69
|
+
try {
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
if (row instanceof BatchedTurboQuantKVCache) {
|
|
72
|
+
views.push(row.#storage); dim ??= row.headDim; reuseOffsets.push(...row.#reuseOffsets);
|
|
73
|
+
} else if (row instanceof TurboQuantKVCache) {
|
|
74
|
+
const planes = row.state(); held.push(...planes); reuseOffsets.push(row.minimumReusableOffset);
|
|
75
|
+
views.push({ planes, rowOffsets: [row.offset], leftPad: [0] }); dim ??= row.headDim;
|
|
76
|
+
} else throw new Error(`TurboQuant row layout cannot merge ${row.signature()}`);
|
|
77
|
+
}
|
|
78
|
+
this.#storage.mergeRows(views); this.#headDim = dim; this.#reuseOffsets = reuseOffsets;
|
|
79
|
+
} finally { disposeResources(held); }
|
|
80
|
+
}
|
|
81
|
+
alignRows(leftPad: readonly number[]): void { this.#storage.alignRows(leftPad); }
|
|
82
|
+
filterRows(keep: readonly number[]): void {
|
|
83
|
+
this.#storage.filterRows(keep); this.#reuseOffsets = keep.map(row => this.#reuseOffsets[row]!);
|
|
84
|
+
}
|
|
85
|
+
state(): MlxArray[] { return this.#storage.planes; }
|
|
86
|
+
extractRow(row: number): TurboQuantKVCache {
|
|
87
|
+
const result = new TurboQuantKVCache(this.kBits, this.vBits), count = this.rowOffsets[row]!;
|
|
88
|
+
if (count) result.restoreState(encoded(this.#storage.extractRow(row)), count, this.#headDim!);
|
|
89
|
+
result.minimumReusableOffset = this.#reuseOffsets[row] ?? 0;
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
dispose(): void { this.#storage.dispose(); this.#headDim = null; this.#reuseOffsets = []; }
|
|
93
|
+
}
|
|
@@ -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}`, 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) {
|
|
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 }));
|
|
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
|
+
}
|