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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { MlxArray } from "../mlx/array";
|
|
2
|
+
import { Dtype } from "../mlx/ffi";
|
|
3
|
+
import * as ops from "../mlx/ops";
|
|
4
|
+
import { materializeCopy } from "../mlx/materialize";
|
|
5
|
+
import { KVCache, isPlainKvCache, type BatchableCache, type Cache, type Mask, type PaddedPrefillCache, type PrefillPadding } from "./gemma4-base";
|
|
6
|
+
import { FullPrefillPadding } from "./full-prefill-padding";
|
|
7
|
+
|
|
8
|
+
/** Full-attention storage with independently advancing row positions.
|
|
9
|
+
* Speculative rollback changes coverage, without moving the retained KV.
|
|
10
|
+
* Subsequent appends overwrite each row's rejected suffix in place when MLX
|
|
11
|
+
* can donate the buffer. Queue membership is outside this layout. */
|
|
12
|
+
export class BatchedKVCache implements BatchableCache, PaddedPrefillCache {
|
|
13
|
+
keys: MlxArray | null = null;
|
|
14
|
+
values: MlxArray | null = null;
|
|
15
|
+
rowOffsets: number[] = [];
|
|
16
|
+
leftPad: number[] = [];
|
|
17
|
+
#rope: MlxArray | undefined;
|
|
18
|
+
#beforeRound: number[] | undefined;
|
|
19
|
+
readonly #padding = new FullPrefillPadding();
|
|
20
|
+
|
|
21
|
+
restorePrefillEnds(ends: readonly number[] | undefined): void { this.#padding.restoreEnds(ends); }
|
|
22
|
+
preparePrefill(padding: PrefillPadding): void {
|
|
23
|
+
this.#padding.prepare(this, padding); this.#positionsChanged();
|
|
24
|
+
}
|
|
25
|
+
finalizePrefill(): void {
|
|
26
|
+
const next = this.#padding.finalize(this.state(), this);
|
|
27
|
+
if (!next) return;
|
|
28
|
+
this.keys?.dispose(); this.values?.dispose();
|
|
29
|
+
this.keys = next[0] ?? null; this.values = next[1] ?? null;
|
|
30
|
+
this.#positionsChanged();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
captureDonorRows(): import("./gemma4-base").KvDonorRows {
|
|
34
|
+
const B = this.rowOffsets.length, width = this.offset;
|
|
35
|
+
const keys = this.keys!.slice([0,0,0,0], [B,this.keys!.shape[1]!,width,this.keys!.shape[3]!]);
|
|
36
|
+
let values: MlxArray;
|
|
37
|
+
try { values = this.values!.slice([0,0,0,0], [B,this.values!.shape[1]!,width,this.values!.shape[3]!]); }
|
|
38
|
+
catch (error) { keys.dispose(); throw error; }
|
|
39
|
+
return { keys, values, offsets: [...this.rowOffsets], starts: [...this.leftPad], ends: this.#ends() };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
signature(): string { return "kv:batched-positions"; }
|
|
43
|
+
get batchSize(): number | null { return this.rowOffsets.length || null; }
|
|
44
|
+
get offset(): number { return Math.max(0, ...this.#ends()); }
|
|
45
|
+
#ends(): number[] { return this.rowOffsets.map((offset, row) => offset + this.leftPad[row]!); }
|
|
46
|
+
get ropeOffsetArr(): MlxArray | undefined {
|
|
47
|
+
// A common unpadded position uses the existing scalar RoPE operation.
|
|
48
|
+
if (this.leftPad.every(pad => pad === 0) && this.rowOffsets.every(offset => offset === this.rowOffsets[0])) return undefined;
|
|
49
|
+
return this.#rope ??= ops.fromInt32(this.rowOffsets, [this.rowOffsets.length]);
|
|
50
|
+
}
|
|
51
|
+
#positionsChanged(): void { this.#rope?.dispose(); this.#rope = undefined; }
|
|
52
|
+
makeEmptyBatch(): BatchedKVCache { return new BatchedKVCache(); }
|
|
53
|
+
bytesPerToken(): number {
|
|
54
|
+
if (!this.keys || !this.values) return 0;
|
|
55
|
+
return (this.keys.nbytes + this.values.nbytes) / (this.rowOffsets.length * this.keys.shape[2]!);
|
|
56
|
+
}
|
|
57
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * tokens; }
|
|
58
|
+
state(): MlxArray[] { return this.keys && this.values ? [this.keys, this.values] : []; }
|
|
59
|
+
isTrimmable(): boolean { return true; }
|
|
60
|
+
trim(count: number): void {
|
|
61
|
+
this.rowOffsets = this.rowOffsets.map(offset => offset - count);
|
|
62
|
+
this.#positionsChanged();
|
|
63
|
+
}
|
|
64
|
+
specRoundBegin(): void { this.#beforeRound = [...this.rowOffsets]; }
|
|
65
|
+
specRoundCommit(): void { this.#beforeRound = undefined; }
|
|
66
|
+
specRoundRollback(keep: number | readonly number[]): void {
|
|
67
|
+
this.rowOffsets = this.#beforeRound!.map((offset, row) => offset + (typeof keep === "number" ? keep : keep[row]!));
|
|
68
|
+
this.#beforeRound = undefined;
|
|
69
|
+
this.#positionsChanged();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
makeMask(N: number, window: number | null): Mask {
|
|
73
|
+
const B = this.rowOffsets.length, ends = this.#ends(), S = this.offset + N;
|
|
74
|
+
if (window === null && this.leftPad.every(pad => pad === 0) && ends.every(end => end === ends[0]))
|
|
75
|
+
return { mode: N === 1 ? "" : "causal", arr: null };
|
|
76
|
+
using starts = ops.fromInt32(ends, [B, 1, 1, 1]);
|
|
77
|
+
using relative = ops.arange(0, N, 1, Dtype.int32);
|
|
78
|
+
using queries = ops.reshape(relative, [1, 1, N, 1]);
|
|
79
|
+
using position = ops.add(starts, queries);
|
|
80
|
+
using indices = ops.arange(0, S, 1, Dtype.int32);
|
|
81
|
+
using keys = ops.reshape(indices, [1, 1, 1, S]);
|
|
82
|
+
using pads = ops.fromInt32(this.leftPad, [B, 1, 1, 1]);
|
|
83
|
+
using causal = ops.lessEqual(keys, position);
|
|
84
|
+
using valid = ops.greaterEqual(keys, pads);
|
|
85
|
+
const mask = ops.logicalAnd(causal, valid);
|
|
86
|
+
if (window === null) return { mode: "array", arr: mask };
|
|
87
|
+
using windowSize = ops.fromInt32([window], []);
|
|
88
|
+
using limit = ops.add(keys, windowSize);
|
|
89
|
+
using within = ops.less(position, limit);
|
|
90
|
+
try { return { mode: "array", arr: ops.logicalAnd(mask, within) }; }
|
|
91
|
+
finally { mask.dispose(); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
95
|
+
const B = this.rowOffsets.length, N = k.shape[2]!, ends = this.#ends();
|
|
96
|
+
const needed = Math.max(...ends) + N;
|
|
97
|
+
const grow = (array: MlxArray) => {
|
|
98
|
+
const [rows, heads, capacity, width] = array.shape as [number, number, number, number];
|
|
99
|
+
if (capacity >= needed) return array;
|
|
100
|
+
using padding = ops.zeros([rows, heads, Math.ceil(needed / 256) * 256 - capacity, width], array.dtype);
|
|
101
|
+
const grown = ops.concatAxis([array, padding], 2);
|
|
102
|
+
array.dispose();
|
|
103
|
+
return grown;
|
|
104
|
+
};
|
|
105
|
+
const allocate = (input: MlxArray) => ops.zeros(
|
|
106
|
+
[B, input.shape[1]!, Math.ceil(needed / 256) * 256, input.shape[3]!], input.dtype);
|
|
107
|
+
this.keys = this.keys ? grow(this.keys) : allocate(k);
|
|
108
|
+
this.values = this.values ? grow(this.values) : allocate(v);
|
|
109
|
+
const common = ends.every(end => end === ends[0]);
|
|
110
|
+
using positions = common ? null : ops.fromInt32(ends.flatMap(end =>
|
|
111
|
+
Array.from({ length: N }, (_, position) => end + position)), [B, 1, N, 1]);
|
|
112
|
+
const append = (array: MlxArray, values: MlxArray) => common
|
|
113
|
+
? ops.sliceUpdate(array, values, [0, 0, ends[0]!, 0], [B, array.shape[1]!, ends[0]! + N, array.shape[3]!])
|
|
114
|
+
: ops.putAlongAxis(array, positions!, values, 2);
|
|
115
|
+
const keys = append(this.keys, k);
|
|
116
|
+
let values: MlxArray;
|
|
117
|
+
try { values = append(this.values, v); }
|
|
118
|
+
catch (error) { keys.dispose(); throw error; }
|
|
119
|
+
this.keys.dispose(); this.values.dispose();
|
|
120
|
+
this.keys = keys; this.values = values;
|
|
121
|
+
this.rowOffsets = this.rowOffsets.map(offset => offset + N);
|
|
122
|
+
this.#positionsChanged();
|
|
123
|
+
return [keys.slice([0, 0, 0, 0], [B, keys.shape[1]!, needed, keys.shape[3]!]),
|
|
124
|
+
values.slice([0, 0, 0, 0], [B, values.shape[1]!, needed, values.shape[3]!])];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
128
|
+
const only = rows.length === 1 ? rows[0] : undefined;
|
|
129
|
+
if (only && (only instanceof BatchedKVCache || isPlainKvCache(only))) {
|
|
130
|
+
// Preserve allocated capacity when a request starts alone. Immutable
|
|
131
|
+
// aliases transfer ownership without copying its prefill storage.
|
|
132
|
+
const keys = only.keys ? ops.copyOf(only.keys) : null;
|
|
133
|
+
let values: MlxArray | null;
|
|
134
|
+
try { values = only.values ? ops.copyOf(only.values) : null; }
|
|
135
|
+
catch (error) { keys?.dispose(); throw error; }
|
|
136
|
+
const offsets = only instanceof BatchedKVCache ? [...only.rowOffsets] : [only.offset];
|
|
137
|
+
const pads = only instanceof BatchedKVCache ? [...only.leftPad] : [0];
|
|
138
|
+
this.dispose(); this.keys = keys; this.values = values;
|
|
139
|
+
this.rowOffsets = offsets; this.leftPad = pads;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const chunks: Array<{ keys: MlxArray | null; values: MlxArray | null; offsets: number[]; pads: number[]; width: number }> = [];
|
|
143
|
+
const held: MlxArray[] = [];
|
|
144
|
+
try {
|
|
145
|
+
for (const cache of rows) {
|
|
146
|
+
let keys: MlxArray | null, values: MlxArray | null, offsets: number[], pads: number[];
|
|
147
|
+
if (cache instanceof BatchedKVCache && cache.offset === 0) {
|
|
148
|
+
keys = values = null; offsets = [...cache.rowOffsets]; pads = [...cache.leftPad];
|
|
149
|
+
} else if (cache instanceof BatchedKVCache) {
|
|
150
|
+
const B = cache.rowOffsets.length, width = cache.offset;
|
|
151
|
+
keys = cache.keys!.slice([0, 0, 0, 0], [B, cache.keys!.shape[1]!, width, cache.keys!.shape[3]!]);
|
|
152
|
+
held.push(keys);
|
|
153
|
+
values = cache.values!.slice([0, 0, 0, 0], [B, cache.values!.shape[1]!, width, cache.values!.shape[3]!]);
|
|
154
|
+
held.push(values);
|
|
155
|
+
offsets = [...cache.rowOffsets]; pads = [...cache.leftPad];
|
|
156
|
+
} else if (isPlainKvCache(cache)) {
|
|
157
|
+
if (cache.offset === 0) keys = values = null;
|
|
158
|
+
else { [keys, values] = cache.temporalView(); held.push(keys, values); }
|
|
159
|
+
offsets = [cache.offset]; pads = [0];
|
|
160
|
+
} else throw new Error(`full-attention batch cannot merge ${cache.signature()}`);
|
|
161
|
+
chunks.push({ keys, values, offsets, pads, width: keys?.shape[2] ?? 0 });
|
|
162
|
+
}
|
|
163
|
+
const width = Math.max(0, ...chunks.map(chunk => chunk.width));
|
|
164
|
+
const padded = (array: MlxArray, count: number) => {
|
|
165
|
+
if (!count) return array;
|
|
166
|
+
using padding = ops.zeros([array.shape[0]!, array.shape[1]!, count, array.shape[3]!], array.dtype);
|
|
167
|
+
const result = ops.concatAxis([padding, array], 2); held.push(result); return result;
|
|
168
|
+
};
|
|
169
|
+
const merge = (field: "keys" | "values") => {
|
|
170
|
+
if (!width) return null; // Geometry arrives with the first append.
|
|
171
|
+
const prototype = chunks.find(chunk => chunk[field])![field]!;
|
|
172
|
+
return ops.concatAxis(chunks.map(chunk => {
|
|
173
|
+
const array = chunk[field];
|
|
174
|
+
if (array) return padded(array, width - chunk.width);
|
|
175
|
+
const empty = ops.zeros([chunk.offsets.length, prototype.shape[1]!, width, prototype.shape[3]!], prototype.dtype);
|
|
176
|
+
held.push(empty); return empty;
|
|
177
|
+
}), 0);
|
|
178
|
+
};
|
|
179
|
+
const keys = merge("keys");
|
|
180
|
+
let values: MlxArray | null;
|
|
181
|
+
try { values = merge("values"); }
|
|
182
|
+
catch (error) { keys?.dispose(); throw error; }
|
|
183
|
+
this.dispose();
|
|
184
|
+
this.keys = keys; this.values = values;
|
|
185
|
+
this.rowOffsets = chunks.flatMap(chunk => chunk.offsets);
|
|
186
|
+
this.leftPad = chunks.flatMap(chunk => chunk.pads.map(pad => pad + width - chunk.width));
|
|
187
|
+
} finally { for (const array of held) array.dispose(); }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
filterRows(rows: readonly number[]): void {
|
|
191
|
+
if (!rows.length) { this.dispose(); return; }
|
|
192
|
+
const pads = rows.map(row => this.leftPad[row]!);
|
|
193
|
+
const sharedPadding = Math.min(...pads);
|
|
194
|
+
using indices = this.keys ? ops.fromInt32([...rows], [rows.length]) : null;
|
|
195
|
+
const select = (array: MlxArray | null): MlxArray | null => {
|
|
196
|
+
if (!array) return null;
|
|
197
|
+
const selected = ops.takeAxis(array, indices!, 0);
|
|
198
|
+
if (!sharedPadding) return selected;
|
|
199
|
+
// mlx-lm BatchKVCache.filter removes padding common to all survivors.
|
|
200
|
+
// Keeping it changes attention reduction geometry after retirement.
|
|
201
|
+
try { return selected.slice([0, 0, sharedPadding, 0], [...selected.shape]); }
|
|
202
|
+
finally { selected.dispose(); }
|
|
203
|
+
};
|
|
204
|
+
const keys = select(this.keys);
|
|
205
|
+
let values: MlxArray | null;
|
|
206
|
+
try { values = select(this.values); }
|
|
207
|
+
catch (error) { keys?.dispose(); throw error; }
|
|
208
|
+
this.keys?.dispose(); this.values?.dispose();
|
|
209
|
+
this.keys = keys; this.values = values;
|
|
210
|
+
this.rowOffsets = rows.map(row => this.rowOffsets[row]!);
|
|
211
|
+
this.leftPad = pads.map(pad => pad - sharedPadding);
|
|
212
|
+
this.#padding.filter(rows);
|
|
213
|
+
this.#positionsChanged();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
extractRow(row: number): KVCache {
|
|
217
|
+
const start = this.leftPad[row]!, count = this.rowOffsets[row]!;
|
|
218
|
+
if (count === 0) return new KVCache();
|
|
219
|
+
const copy = (array: MlxArray) => {
|
|
220
|
+
using view = array.slice([row, 0, start, 0], [row + 1, array.shape[1]!, start + count, array.shape[3]!]);
|
|
221
|
+
return materializeCopy(view);
|
|
222
|
+
};
|
|
223
|
+
const keys = copy(this.keys!);
|
|
224
|
+
let values: MlxArray;
|
|
225
|
+
try { values = copy(this.values!); }
|
|
226
|
+
catch (error) { keys.dispose(); throw error; }
|
|
227
|
+
const cache = new KVCache(); cache.restoreState(keys, values, count); return cache;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
dispose(): void {
|
|
231
|
+
this.keys?.dispose(); this.values?.dispose(); this.#positionsChanged();
|
|
232
|
+
this.keys = this.values = null;
|
|
233
|
+
this.rowOffsets = []; this.leftPad = []; this.#beforeRound = undefined; this.#padding.clear();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -259,13 +259,21 @@ export function extractKVRow(cache: KVCache, leftPad: number, i: number): KVCach
|
|
|
259
259
|
|
|
260
260
|
/** mlx-lm `filter`: keep only `keep` (sorted row indices) along the batch
|
|
261
261
|
* axis — eviction of finished sequences from a batched [B,H,S,D] buffer.
|
|
262
|
-
*
|
|
262
|
+
* `trimLeft` removes padding shared by all survivors (the caller updates
|
|
263
|
+
* offsets and per-row padding). Caller owns the result; inputs stay owned. */
|
|
263
264
|
export function filterKVRows(
|
|
264
|
-
keys: MlxArray, values: MlxArray, keep: number[],
|
|
265
|
+
keys: MlxArray, values: MlxArray, keep: number[], trimLeft = 0,
|
|
265
266
|
): { keys: MlxArray; values: MlxArray } {
|
|
266
267
|
const idx = MlxArray.fromInt32(Int32Array.from(keep), [keep.length]);
|
|
267
|
-
const
|
|
268
|
-
|
|
268
|
+
const take = (a: MlxArray) => {
|
|
269
|
+
const rows = ops.takeAxis(a, idx, 0);
|
|
270
|
+
if (trimLeft === 0) return rows;
|
|
271
|
+
const trimmed = rows.slice([0, 0, trimLeft, 0], rows.shape);
|
|
272
|
+
rows.dispose();
|
|
273
|
+
return trimmed;
|
|
274
|
+
};
|
|
275
|
+
const k = take(keys);
|
|
276
|
+
const v = take(values);
|
|
269
277
|
idx.dispose();
|
|
270
278
|
return { keys: k, values: v };
|
|
271
279
|
}
|
|
@@ -134,11 +134,17 @@ export function extractQuantRow(
|
|
|
134
134
|
/** Quantized twin of filterKVRows: keep `keep` rows along the batch axis.
|
|
135
135
|
* Caller owns the result; inputs are not disposed. */
|
|
136
136
|
export function filterQuantRows(
|
|
137
|
-
keys: ops.QuantizedTensor, values: ops.QuantizedTensor, keep: number[],
|
|
137
|
+
keys: ops.QuantizedTensor, values: ops.QuantizedTensor, keep: number[], trimLeft = 0,
|
|
138
138
|
): { keys: ops.QuantizedTensor; values: ops.QuantizedTensor } {
|
|
139
139
|
const idx = MlxArray.fromInt32(Int32Array.from(keep), [keep.length]);
|
|
140
140
|
const take = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
|
|
141
|
-
tripleMap(t, (a) =>
|
|
141
|
+
tripleMap(t, (a) => {
|
|
142
|
+
const rows = ops.takeAxis(a, idx, 0);
|
|
143
|
+
if (trimLeft === 0) return rows;
|
|
144
|
+
const trimmed = rows.slice([0, 0, trimLeft, 0], rows.shape);
|
|
145
|
+
rows.dispose();
|
|
146
|
+
return trimmed;
|
|
147
|
+
});
|
|
142
148
|
const k = take(keys);
|
|
143
149
|
const v = take(values);
|
|
144
150
|
idx.dispose();
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import { cleanupFailure, disposeResources } from "../engine/resources";
|
|
4
|
+
import { BatchedKVCache } from "./batched-kv";
|
|
5
|
+
import { KVCache, QuantizedKVCache, isQuantizedKvCache,
|
|
6
|
+
type BatchableCache, type Cache, type Mask, type QuantizedAttentionState, type PaddedPrefillCache, type PrefillPadding } from "./gemma4-base";
|
|
7
|
+
|
|
8
|
+
import { KvTensorRows } from "../backends/mlx/kv-tensor-rows";
|
|
9
|
+
import { quantizedDonorAttention } from "./kv-attention-view";
|
|
10
|
+
|
|
11
|
+
const FIELDS = ["packed", "scales", "biases"] as const;
|
|
12
|
+
|
|
13
|
+
/** Affine KV uses the same row-position storage operations for its packed
|
|
14
|
+
* values, scales and biases. No dequantization or requantization is needed
|
|
15
|
+
* for merge, rollback, retirement or extraction. Inputs share this codec. */
|
|
16
|
+
export class BatchedQuantizedKVCache implements BatchableCache, QuantizedAttentionState, PaddedPrefillCache {
|
|
17
|
+
#planes = FIELDS.map(() => new BatchedKVCache());
|
|
18
|
+
#minimum: number[] = [];
|
|
19
|
+
get minimumReusableOffset(): number { return Math.max(0, ...this.#minimum); }
|
|
20
|
+
constructor(readonly groupSize: number, readonly bits: number) {}
|
|
21
|
+
restorePrefillEnds(ends: readonly number[] | undefined): void { for (const plane of this.#planes) plane.restorePrefillEnds(ends); }
|
|
22
|
+
preparePrefill(padding: PrefillPadding): void {
|
|
23
|
+
for (const plane of this.#planes) plane.preparePrefill(padding);
|
|
24
|
+
if (!this.#minimum.length) this.#minimum = padding.lengths.map(() => 0);
|
|
25
|
+
}
|
|
26
|
+
finalizePrefill(): void { for (const plane of this.#planes) plane.finalizePrefill(); }
|
|
27
|
+
get quantizedAttention(): QuantizedAttentionState { return this; }
|
|
28
|
+
captureDonorAttention(): import("./gemma4-base").KvDonorAttention {
|
|
29
|
+
const views: import("./gemma4-base").KvDonorRows[] = [];
|
|
30
|
+
try {
|
|
31
|
+
for (const plane of this.#planes) views.push(plane.captureDonorRows());
|
|
32
|
+
const field = (name: "keys" | "values") => ({
|
|
33
|
+
packed: views[0]![name], scales: views[1]![name], biases: views[2]![name],
|
|
34
|
+
});
|
|
35
|
+
const { offsets, starts, ends } = views[0]!;
|
|
36
|
+
return quantizedDonorAttention(field("keys"), field("values"), { offsets, starts, ends }, this.groupSize, this.bits);
|
|
37
|
+
} catch (error) { disposeResources(views.flatMap(view => [view.keys, view.values])); throw error; }
|
|
38
|
+
}
|
|
39
|
+
signature(): string { return `kv:batched-quant:${this.bits}:${this.groupSize}`; }
|
|
40
|
+
get rowOffsets(): readonly number[] { return this.#planes[0]!.rowOffsets; }
|
|
41
|
+
get leftPad(): readonly number[] { return this.#planes[0]!.leftPad; }
|
|
42
|
+
get batchSize(): number | null { return this.#planes[0]!.batchSize; }
|
|
43
|
+
get offset(): number { return this.#planes[0]!.offset; }
|
|
44
|
+
get ropeOffsetArr(): MlxArray | undefined { return this.#planes[0]!.ropeOffsetArr; }
|
|
45
|
+
makeEmptyBatch(): BatchedQuantizedKVCache { return new BatchedQuantizedKVCache(this.groupSize, this.bits); }
|
|
46
|
+
bytesPerToken(): number { return this.#planes.reduce((n, plane) => n + plane.bytesPerToken(), 0); }
|
|
47
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * tokens; }
|
|
48
|
+
makeMask(tokens: number, window: number | null): Mask { return this.#planes[0]!.makeMask(tokens, window); }
|
|
49
|
+
isTrimmable(): boolean { return true; }
|
|
50
|
+
trim(count: number): void { for (const plane of this.#planes) plane.trim(count); }
|
|
51
|
+
specRoundBegin(): void { for (const plane of this.#planes) plane.specRoundBegin(); }
|
|
52
|
+
specRoundCommit(): void { for (const plane of this.#planes) plane.specRoundCommit(); }
|
|
53
|
+
specRoundRollback(keep: number | readonly number[]): void {
|
|
54
|
+
for (const plane of this.#planes) plane.specRoundRollback(keep);
|
|
55
|
+
}
|
|
56
|
+
updateAndFetch(): [MlxArray, MlxArray] { throw new Error("quantized attention uses updateAndFetchQuantized"); }
|
|
57
|
+
|
|
58
|
+
updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
59
|
+
using inputs = new DisposableStack();
|
|
60
|
+
const key = ops.quantize(k, this.groupSize, this.bits);
|
|
61
|
+
for (const field of FIELDS) inputs.use(key[field]);
|
|
62
|
+
const value = ops.quantize(v, this.groupSize, this.bits);
|
|
63
|
+
for (const field of FIELDS) inputs.use(value[field]);
|
|
64
|
+
const held: MlxArray[] = [], keys: Partial<ops.QuantizedTensor> = {}, values: Partial<ops.QuantizedTensor> = {};
|
|
65
|
+
try {
|
|
66
|
+
for (const [index, field] of FIELDS.entries()) {
|
|
67
|
+
const [kk, vv] = this.#planes[index]!.updateAndFetch(key[field], value[field]);
|
|
68
|
+
held.push(kk, vv); keys[field] = kk; values[field] = vv;
|
|
69
|
+
}
|
|
70
|
+
return [keys as ops.QuantizedTensor, values as ops.QuantizedTensor];
|
|
71
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(held)); }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Borrow prepared request state. All planes stage replacements before
|
|
75
|
+
* releasing the current group, preserving donor ownership on failure. */
|
|
76
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
77
|
+
const next: BatchedKVCache[] = [];
|
|
78
|
+
try {
|
|
79
|
+
for (const [index, field] of FIELDS.entries()) {
|
|
80
|
+
const held: KVCache[] = [], inputs: Cache[] = [];
|
|
81
|
+
try {
|
|
82
|
+
for (const row of rows) {
|
|
83
|
+
if (row instanceof BatchedQuantizedKVCache) inputs.push(row.#planes[index]!);
|
|
84
|
+
else if (isQuantizedKvCache(row)) {
|
|
85
|
+
const view = new KVCache(); held.push(view);
|
|
86
|
+
if (row.offset > 0) {
|
|
87
|
+
const key = ops.copyOf(row.keys![field]);
|
|
88
|
+
let value: MlxArray;
|
|
89
|
+
try { value = ops.copyOf(row.values![field]); }
|
|
90
|
+
catch (error) { key.dispose(); throw error; }
|
|
91
|
+
view.restoreState(key, value, row.offset);
|
|
92
|
+
}
|
|
93
|
+
inputs.push(view);
|
|
94
|
+
} else throw new Error(`quantized row layout cannot merge ${row.signature()}`);
|
|
95
|
+
}
|
|
96
|
+
const plane = new BatchedKVCache(); next.push(plane); plane.mergeRows(inputs);
|
|
97
|
+
} finally { disposeResources(held); }
|
|
98
|
+
}
|
|
99
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources(next)); }
|
|
100
|
+
const previous = this.#planes; this.#planes = next; disposeResources(previous);
|
|
101
|
+
this.#minimum = rows.flatMap(row => row instanceof BatchedQuantizedKVCache ? row.#minimum : [row.minimumReusableOffset ?? 0]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
alignRows(leftPad: readonly number[]): void {
|
|
105
|
+
if (leftPad.every((pad, row) => pad === this.leftPad[row])) return;
|
|
106
|
+
const aligned = new KvTensorRows();
|
|
107
|
+
try {
|
|
108
|
+
aligned.mergeRows([{ planes: this.#planes.flatMap(plane => plane.keys ? [plane.keys, plane.values!] : []),
|
|
109
|
+
rowOffsets: this.rowOffsets, leftPad: this.leftPad }]);
|
|
110
|
+
aligned.alignRows(leftPad);
|
|
111
|
+
for (const plane of this.#planes) {
|
|
112
|
+
plane.keys?.dispose(); plane.values?.dispose();
|
|
113
|
+
plane.keys = aligned.planes.shift() ?? null; plane.values = aligned.planes.shift() ?? null;
|
|
114
|
+
plane.leftPad = [...leftPad];
|
|
115
|
+
}
|
|
116
|
+
} finally { aligned.dispose(); }
|
|
117
|
+
}
|
|
118
|
+
filterRows(keep: readonly number[]): void {
|
|
119
|
+
for (const plane of this.#planes) plane.filterRows(keep);
|
|
120
|
+
this.#minimum = keep.map(row => this.#minimum[row]!);
|
|
121
|
+
}
|
|
122
|
+
state(): MlxArray[] {
|
|
123
|
+
if (!this.#planes[0]!.keys) return [];
|
|
124
|
+
return [...this.#planes.map(plane => plane.keys!), ...this.#planes.map(plane => plane.values!)];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Return the existing persisted representation, owning compact arrays. */
|
|
128
|
+
extractRow(row: number): QuantizedKVCache {
|
|
129
|
+
const offset = this.rowOffsets[row]!, result = new QuantizedKVCache(this.groupSize, this.bits);
|
|
130
|
+
if (this.#minimum[row]) result.minimumReusableOffset = this.#minimum[row];
|
|
131
|
+
if (offset === 0) return result;
|
|
132
|
+
const held: KVCache[] = [];
|
|
133
|
+
try {
|
|
134
|
+
for (const plane of this.#planes) held.push(plane.extractRow(row));
|
|
135
|
+
result.restoreState(
|
|
136
|
+
{ packed: held[0]!.keys!, scales: held[1]!.keys!, biases: held[2]!.keys! },
|
|
137
|
+
{ packed: held[0]!.values!, scales: held[1]!.values!, biases: held[2]!.values! }, offset);
|
|
138
|
+
for (const cache of held) cache.keys = cache.values = null;
|
|
139
|
+
return result;
|
|
140
|
+
} finally { disposeResources(held); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
dispose(): void { disposeResources(this.#planes); this.#minimum = []; }
|
|
144
|
+
}
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
// untouched. PERSISTENT (like BatchedRotatingCache, unlike the ephemeral
|
|
21
21
|
// per-step BatchedQuantDecodeMaskCache): the scheduler passes it through
|
|
22
22
|
// every step; it self-tracks per-row offset/leftPad and the scalar ring
|
|
23
|
-
// state.
|
|
23
|
+
// state. Multi-token blocks use the same chronological storage operation as
|
|
24
|
+
// the bf16 layout; one-token updates keep the existing in-place ring kernel.
|
|
24
25
|
//
|
|
25
26
|
// Gates: tests/unit/batched-rotating-quant.test.ts (model-free byte-identity vs
|
|
26
27
|
// the serial oracle per row, through ring wrap, B=1 and B=2) + the gemma
|
|
@@ -29,11 +30,13 @@
|
|
|
29
30
|
import { MlxArray } from "../mlx/array";
|
|
30
31
|
import * as ops from "../mlx/ops";
|
|
31
32
|
import { Dtype } from "../mlx/ffi";
|
|
32
|
-
import { RotatingQuantizedKVCache, type Mask } from "./gemma4-base";
|
|
33
|
+
import { RotatingQuantizedKVCache, type Mask, type PaddedPrefillCache, type PrefillPadding } from "./gemma4-base";
|
|
33
34
|
import { buildBatchedRotatingMask } from "./batched-rotating";
|
|
34
35
|
import type { QuantRow } from "./batched-quant";
|
|
35
|
-
import { BatchedRotatingState } from "./batched-rotating-state";
|
|
36
|
+
import { BatchedRotatingState, type RotatingPositionSnapshot } from "./batched-rotating-state";
|
|
36
37
|
import {
|
|
38
|
+
appendRotatingStorage,
|
|
39
|
+
rowRollIndices,
|
|
37
40
|
mergeStorageRows,
|
|
38
41
|
quantizedRowStorage,
|
|
39
42
|
temporalStorageView,
|
|
@@ -54,7 +57,7 @@ const mapTriple = (
|
|
|
54
57
|
* mechanics (see batched-rotating.ts) with RotatingQuantizedKVCache
|
|
55
58
|
* storage. Scalar ring state (`ringIdx`/`offset` reuse the base fields);
|
|
56
59
|
* per-row `offsetArr` (absolute positions → RoPE) and `leftPad`. */
|
|
57
|
-
export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
60
|
+
export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache implements PaddedPrefillCache {
|
|
58
61
|
readonly #rows: BatchedRotatingState;
|
|
59
62
|
/** Per-row RoPE positions. STABLE ACROSS A STEP — refreshed only at
|
|
60
63
|
* releaseRopeArr() (the scheduler's post-dispatch hook), never inside
|
|
@@ -72,7 +75,34 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
72
75
|
) {
|
|
73
76
|
super(maxSize, groupSize, bits);
|
|
74
77
|
this.#rows = new BatchedRotatingState(maxSize, leftPad, offsets);
|
|
75
|
-
this.ropeOffsetArr = MlxArray.fromInt32(Int32Array.from(offsets), [offsets.length]);
|
|
78
|
+
this.ropeOffsetArr = offsets.length ? MlxArray.fromInt32(Int32Array.from(offsets), [offsets.length]) : ops.zeros([0], Dtype.int32);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static empty(maxSize: number, groupSize: number, bits: number, leftPad: readonly number[]): BatchedRotatingQuantCache {
|
|
82
|
+
return new BatchedRotatingQuantCache(maxSize, groupSize, bits, [...leftPad], leftPad.map(pad => -pad));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get positionSnapshot(): RotatingPositionSnapshot { return this.#rows.snapshot(); }
|
|
86
|
+
validOffset(row: number): number { return this.#rows.validOffset(row); }
|
|
87
|
+
preparePrefill(padding: PrefillPadding, rightPaddedBatch?: boolean): void { this.#rows.preparePrefill(padding, rightPaddedBatch); this.releaseRopeArr(); }
|
|
88
|
+
finalizePrefill(): void {
|
|
89
|
+
const shifts = this.#rows.prefillRoll();
|
|
90
|
+
if (shifts?.some(shift => shift !== 0) && this.keys && this.values) {
|
|
91
|
+
using indices = rowRollIndices(this.keys.packed.shape[2]!, shifts);
|
|
92
|
+
const keys = quantizedRowStorage.rollRows(this.keys, indices);
|
|
93
|
+
let values: ops.QuantizedTensor;
|
|
94
|
+
try { values = quantizedRowStorage.rollRows(this.values, indices); }
|
|
95
|
+
catch (error) { disposeTriple(keys); throw error; }
|
|
96
|
+
disposeTriple(this.keys); disposeTriple(this.values); this.keys = keys; this.values = values;
|
|
97
|
+
}
|
|
98
|
+
this.#rows.finalizePrefill(); this.releaseRopeArr();
|
|
99
|
+
}
|
|
100
|
+
/** Adopt owned tensors without changing their physical ring columns. */
|
|
101
|
+
static adoptPhysical(keys: ops.QuantizedTensor | null, values: ops.QuantizedTensor | null, groupSize: number, bits: number, position: RotatingPositionSnapshot): BatchedRotatingQuantCache {
|
|
102
|
+
const cache = new BatchedRotatingQuantCache(position.maxSize, groupSize, bits, [...position.leftPad], [...position.offsets]);
|
|
103
|
+
cache.#rows.restore(position); cache.keys = keys; cache.values = values;
|
|
104
|
+
cache.offset = position.totalOffset; cache.ringIdx = position.ringIndex;
|
|
105
|
+
return cache;
|
|
76
106
|
}
|
|
77
107
|
|
|
78
108
|
get offsetArr(): number[] { return this.#rows.offsets; }
|
|
@@ -93,7 +123,7 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
93
123
|
* old handle after this). */
|
|
94
124
|
releaseRopeArr(): void {
|
|
95
125
|
this.ropeOffsetArr.dispose();
|
|
96
|
-
this.ropeOffsetArr = MlxArray.fromInt32(Int32Array.from(this.offsetArr), [this.#B]);
|
|
126
|
+
this.ropeOffsetArr = this.#B ? MlxArray.fromInt32(Int32Array.from(this.offsetArr), [this.#B]) : ops.zeros([0], Dtype.int32);
|
|
97
127
|
}
|
|
98
128
|
|
|
99
129
|
#allocTriple(B: number, H: number, T: number, dim: number, dtype: Dtype): ops.QuantizedTensor {
|
|
@@ -111,7 +141,7 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
111
141
|
mode: "array",
|
|
112
142
|
arr: buildBatchedRotatingMask(
|
|
113
143
|
this.#B, N, this.leftPad, this.maxSize, window,
|
|
114
|
-
this.#rows.ringIndex, this.#rows.totalOffset, this.#rows.rotated,
|
|
144
|
+
this.#rows.ringIndex, this.#rows.totalOffset, this.#rows.rotated, N > 1 || this.#rows.hasPendingPadding,
|
|
115
145
|
),
|
|
116
146
|
};
|
|
117
147
|
}
|
|
@@ -122,8 +152,7 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
122
152
|
override updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
123
153
|
const [B, H, S, D] = k.shape as [number, number, number, number];
|
|
124
154
|
const vD = v.shape[3]!;
|
|
125
|
-
if (S !== 1)
|
|
126
|
-
throw new Error("BatchedRotatingQuantCache supports N=1 decode updates only (solo-prefill then merge)");
|
|
155
|
+
if (S !== 1 || this.#rows.hasPendingPadding) return this.#updateConcat(k, v);
|
|
127
156
|
const prev = this.#rows.totalOffset;
|
|
128
157
|
|
|
129
158
|
// Grow the buffer (in STEP chunks) until it reaches maxSize.
|
|
@@ -202,6 +231,29 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
202
231
|
return [cut(this.keys!), cut(this.values!)];
|
|
203
232
|
}
|
|
204
233
|
|
|
234
|
+
#updateConcat(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
235
|
+
const kq = ops.quantize(k, this.groupSize, this.bits);
|
|
236
|
+
const vq = ops.quantize(v, this.groupSize, this.bits);
|
|
237
|
+
let keys: ops.QuantizedTensor | undefined;
|
|
238
|
+
let values: ops.QuantizedTensor;
|
|
239
|
+
try {
|
|
240
|
+
keys = appendRotatingStorage(quantizedRowStorage, this.keys, kq, this.#rows);
|
|
241
|
+
values = appendRotatingStorage(quantizedRowStorage, this.values, vq, this.#rows);
|
|
242
|
+
} catch (error) { if (keys) disposeTriple(keys); throw error; }
|
|
243
|
+
finally { disposeTriple(kq); disposeTriple(vq); }
|
|
244
|
+
const history = this.keys ? this.#rows.activeLength : 0;
|
|
245
|
+
if (this.keys) disposeTriple(this.keys);
|
|
246
|
+
if (this.values) disposeTriple(this.values);
|
|
247
|
+
this.keys = keys; this.values = values;
|
|
248
|
+
this.#rows.commitConcat(k.shape[2]!, history);
|
|
249
|
+
this.offset = this.#rows.totalOffset;
|
|
250
|
+
this.ringIdx = this.#rows.ringIndex;
|
|
251
|
+
return [
|
|
252
|
+
quantizedRowStorage.slice(keys, 0, this.#B, 0, this.ringIdx),
|
|
253
|
+
quantizedRowStorage.slice(values, 0, this.#B, 0, this.ringIdx),
|
|
254
|
+
];
|
|
255
|
+
}
|
|
256
|
+
|
|
205
257
|
#assign2(dst: MlxArray, src: MlxArray, S: number): MlxArray {
|
|
206
258
|
const [B, H, , D] = dst.shape as [number, number, number, number];
|
|
207
259
|
const out = ops.sliceUpdate(dst, src, [0, 0, this.ringIdx, 0], [B, H, this.ringIdx + S, D]);
|
|
@@ -214,8 +266,12 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
214
266
|
override temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
215
267
|
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
216
268
|
return [
|
|
217
|
-
temporalStorageView(quantizedRowStorage, this.keys, this.#rows
|
|
218
|
-
|
|
269
|
+
temporalStorageView(quantizedRowStorage, this.keys, this.#rows, {
|
|
270
|
+
from: Math.max(0, this.#rows.activeLength - this.maxSize), to: this.#rows.activeLength,
|
|
271
|
+
}),
|
|
272
|
+
temporalStorageView(quantizedRowStorage, this.values, this.#rows, {
|
|
273
|
+
from: Math.max(0, this.#rows.activeLength - this.maxSize), to: this.#rows.activeLength,
|
|
274
|
+
}),
|
|
219
275
|
];
|
|
220
276
|
}
|
|
221
277
|
|
|
@@ -228,15 +284,15 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
228
284
|
* is byte-safe (packing along HEAD_DIM — file header); per-row byte
|
|
229
285
|
* identity vs the serial oracle is the class invariant
|
|
230
286
|
* (tests/batched-rotating-quant), extraction is a pure slice+copy. */
|
|
231
|
-
extractRow(i: number): RotatingQuantizedKVCache | null {
|
|
287
|
+
extractRow(i: number, limit = Infinity): RotatingQuantizedKVCache | null {
|
|
232
288
|
if (!this.keys || !this.values) return null;
|
|
233
|
-
const pad = Math.max(0, this.leftPad[i]
|
|
289
|
+
const pad = Math.max(0, this.leftPad[i]!, this.#rows.activeLength - limit);
|
|
234
290
|
const c = new RotatingQuantizedKVCache(this.maxSize, this.groupSize, this.bits);
|
|
235
291
|
const k = temporalStorageView(quantizedRowStorage, this.keys, this.#rows, {
|
|
236
|
-
row: i, from: pad, copy: true,
|
|
292
|
+
row: i, from: pad, to: this.#rows.activeLength, copy: true,
|
|
237
293
|
});
|
|
238
294
|
const v = temporalStorageView(quantizedRowStorage, this.values, this.#rows, {
|
|
239
|
-
row: i, from: pad, copy: true,
|
|
295
|
+
row: i, from: pad, to: this.#rows.activeLength, copy: true,
|
|
240
296
|
});
|
|
241
297
|
c.restoreState(k, v, this.offsetArr[i]!, k.packed.shape[2]!);
|
|
242
298
|
return c;
|
|
@@ -258,6 +314,11 @@ export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
|
258
314
|
|
|
259
315
|
filterRows(keep: readonly number[]): void { this.filter([...keep]); }
|
|
260
316
|
|
|
317
|
+
override trim(count: number): void {
|
|
318
|
+
this.#rows.trim(count); this.offset = this.#rows.totalOffset; this.ringIdx = this.#rows.ringIndex;
|
|
319
|
+
this.releaseRopeArr();
|
|
320
|
+
}
|
|
321
|
+
|
|
261
322
|
override isTrimmable(): boolean {
|
|
262
323
|
return false; // batched rows never re-enter the prompt cache directly
|
|
263
324
|
}
|