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,262 @@
|
|
|
1
|
+
import { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import { cloneKvCaches } from "../kv-store";
|
|
4
|
+
import { type BatchableCache, type Cache, type Mask, type PaddedPrefillCache, type PrefillPadding, RotatingKVCache, RotatingQuantizedKVCache } from "./gemma4-base";
|
|
5
|
+
import { BatchedRotatingCache, buildBatchedRotatingMask } from "./batched-rotating";
|
|
6
|
+
import { BatchedRotatingQuantCache } from "./batched-rotating-quant";
|
|
7
|
+
import { BatchedRotatingState, type RotatingPositionSnapshot } from "./batched-rotating-state";
|
|
8
|
+
import { plainRowStorage, quantizedRowStorage, temporalStorageView } from "./batched-row-storage";
|
|
9
|
+
import type { TransitioningKvPositions, TransitionedKvLayout } from "../backends/mlx/transitioning-kv-rows";
|
|
10
|
+
import { captureRotatingDonorAttention } from "./rotating-kv-donor";
|
|
11
|
+
|
|
12
|
+
import { rollbackRotatingRing } from "./rotating-row-transaction";
|
|
13
|
+
|
|
14
|
+
const fields = ["packed", "scales", "biases"] as const;
|
|
15
|
+
type Ring = BatchedRotatingCache | BatchedRotatingQuantCache;
|
|
16
|
+
|
|
17
|
+
function combinedPosition(rings: readonly Ring[]): RotatingPositionSnapshot {
|
|
18
|
+
const states = rings.map(ring => ring.positionSnapshot);
|
|
19
|
+
return { ...states[0]!, offsets: states.map(state => state.offsets[0]!),
|
|
20
|
+
leftPad: states.map(state => state.leftPad[0]!),
|
|
21
|
+
prefillEnds: states[0]!.prefillEnds
|
|
22
|
+
? states.map(state => state.prefillEnds![0]!) : undefined };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One logical request with the group's physical ring geometry. This keeps
|
|
26
|
+
* masks stable while different requests cross their precision boundary. */
|
|
27
|
+
export class AlignedRotatingCache implements Cache {
|
|
28
|
+
minimumReusableOffset = 0;
|
|
29
|
+
constructor(public inner: Ring) {}
|
|
30
|
+
get offset(): number { return this.inner.validOffset(0); }
|
|
31
|
+
get affineConversion(): AlignedRotatingCache | undefined { return this.inner instanceof BatchedRotatingCache ? this : undefined; }
|
|
32
|
+
get quantizedAttention() { return this.inner instanceof BatchedRotatingQuantCache ? this.inner : undefined; }
|
|
33
|
+
signature(): string { return this.inner.signature(); }
|
|
34
|
+
captureDonorAttention() { return captureRotatingDonorAttention(this.inner); }
|
|
35
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] { return this.inner.updateAndFetch(k, v); }
|
|
36
|
+
makeMask(tokens: number, window: number | null): Mask { return this.inner.makeMask(tokens, window); }
|
|
37
|
+
isTrimmable(): boolean { return this.inner.positionSnapshot.totalOffset < this.inner.maxSize; }
|
|
38
|
+
trim(count: number): void { this.inner.trim(count); }
|
|
39
|
+
rollback(before: number, keep: number, preserveColumns: boolean): void {
|
|
40
|
+
this.inner = rollbackRotatingRing(this.inner, [before], keep, preserveColumns);
|
|
41
|
+
}
|
|
42
|
+
state(): MlxArray[] { return this.inner.state(); }
|
|
43
|
+
bytesPerToken(): number {
|
|
44
|
+
const state = this.state(), tokens = state[0]?.shape[2] ?? 0;
|
|
45
|
+
return tokens ? state.reduce((n, a) => n + a.nbytes, 0) / tokens : 0;
|
|
46
|
+
}
|
|
47
|
+
dispose(): void { this.inner.dispose(); }
|
|
48
|
+
extract(limit?: number): Cache {
|
|
49
|
+
const row = this.inner.extractRow(0, limit) ?? (this.inner instanceof BatchedRotatingQuantCache
|
|
50
|
+
? new RotatingQuantizedKVCache(this.inner.maxSize, this.inner.groupSize, this.inner.bits)
|
|
51
|
+
: new RotatingKVCache(this.inner.maxSize));
|
|
52
|
+
row.minimumReusableOffset = this.minimumReusableOffset;
|
|
53
|
+
return row;
|
|
54
|
+
}
|
|
55
|
+
toQuantized(groupSize: number, bits: number): AlignedRotatingCache {
|
|
56
|
+
const plain = this.inner as BatchedRotatingCache;
|
|
57
|
+
const keys = ops.quantize(plain.keys!, groupSize, bits);
|
|
58
|
+
let values: ops.QuantizedTensor;
|
|
59
|
+
try { values = ops.quantize(plain.values!, groupSize, bits); }
|
|
60
|
+
catch (error) { quantizedRowStorage.dispose(keys); throw error; }
|
|
61
|
+
const next = new AlignedRotatingCache(BatchedRotatingQuantCache.adoptPhysical(keys, values, groupSize, bits, plain.positionSnapshot));
|
|
62
|
+
next.minimumReusableOffset = this.minimumReusableOffset;
|
|
63
|
+
this.dispose();
|
|
64
|
+
return next;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Borrow the newest valid window from a solo cache, including a temporary
|
|
69
|
+
* oversized block. No dequantization or physical-ring normalization in place. */
|
|
70
|
+
export function rotatingSourcePosition(row: RotatingKVCache | RotatingQuantizedKVCache): BatchedRotatingState {
|
|
71
|
+
const state = new BatchedRotatingState(row.maxSize, [0], [row.offset]);
|
|
72
|
+
const length = row instanceof RotatingKVCache ? row.keys?.shape[2] ?? 0 : row.keys?.packed.shape[2] ?? 0;
|
|
73
|
+
state.restore({ maxSize: row.maxSize, offsets: [row.offset], leftPad: [0], totalOffset: row.offset,
|
|
74
|
+
ringIndex: row.ringIdx, rotated: row.ringIdx < row.offset && row.ringIdx < length });
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Take ownership of solo rows and align them once at membership changes. */
|
|
79
|
+
export function alignRotatingRows(rows: Cache[]): AlignedRotatingCache[] {
|
|
80
|
+
const width = Math.max(0, ...rows.map(row => Math.min(row.offset, (row as RotatingKVCache).maxSize)));
|
|
81
|
+
const prototype = rows.find(row => row.state().length > 0) as RotatingKVCache | RotatingQuantizedKVCache | undefined;
|
|
82
|
+
const emptyPlane = (field: "keys" | "values"): MlxArray | null => {
|
|
83
|
+
if (!width || !prototype) return null;
|
|
84
|
+
const tensor = prototype[field]!;
|
|
85
|
+
if (tensor instanceof MlxArray) return ops.zeros([1, tensor.shape[1]!, width, tensor.shape[3]!], tensor.dtype);
|
|
86
|
+
return ops.zeros([1, tensor.packed.shape[1]!, width, tensor.packed.shape[3]! * 32 / (prototype as RotatingQuantizedKVCache).bits], tensor.scales.dtype);
|
|
87
|
+
};
|
|
88
|
+
const result: AlignedRotatingCache[] = [];
|
|
89
|
+
try {
|
|
90
|
+
for (const source of rows) {
|
|
91
|
+
if (!(source instanceof RotatingKVCache || source instanceof RotatingQuantizedKVCache))
|
|
92
|
+
throw new Error(`rotating layout cannot adopt ${source.signature()}`);
|
|
93
|
+
const length = Math.min(source.offset, source.maxSize), pad = width - length;
|
|
94
|
+
const position: RotatingPositionSnapshot = { maxSize: source.maxSize, offsets: [source.offset], leftPad: [pad],
|
|
95
|
+
totalOffset: width, ringIndex: width, rotated: false };
|
|
96
|
+
const state = rotatingSourcePosition(source);
|
|
97
|
+
const options = { from: Math.max(0, state.activeLength - length), to: state.activeLength };
|
|
98
|
+
let inner: Ring;
|
|
99
|
+
if (source instanceof RotatingKVCache) {
|
|
100
|
+
using k = source.keys ? temporalStorageView(plainRowStorage, source.keys, state, options) : null;
|
|
101
|
+
using v = source.values ? temporalStorageView(plainRowStorage, source.values, state, options) : null;
|
|
102
|
+
const keys = k ? plainRowStorage.padLeft(k, pad) : emptyPlane("keys");
|
|
103
|
+
let values: MlxArray | null;
|
|
104
|
+
try { values = v ? plainRowStorage.padLeft(v, pad) : emptyPlane("values"); }
|
|
105
|
+
catch (error) { keys?.dispose(); throw error; }
|
|
106
|
+
inner = BatchedRotatingCache.adoptPhysical(keys, values, position);
|
|
107
|
+
} else {
|
|
108
|
+
const k = source.keys ? temporalStorageView(quantizedRowStorage, source.keys, state, options) : null;
|
|
109
|
+
const v = source.values ? temporalStorageView(quantizedRowStorage, source.values, state, options) : null;
|
|
110
|
+
const empty = (field: "keys" | "values") => {
|
|
111
|
+
using tensor = emptyPlane(field);
|
|
112
|
+
return tensor ? ops.quantize(tensor, source.groupSize, source.bits) : null;
|
|
113
|
+
};
|
|
114
|
+
try {
|
|
115
|
+
const keys = k ? quantizedRowStorage.padLeft(k, pad) : empty("keys");
|
|
116
|
+
let values: ops.QuantizedTensor | null;
|
|
117
|
+
try { values = v ? quantizedRowStorage.padLeft(v, pad) : empty("values"); }
|
|
118
|
+
catch (error) { if (keys) quantizedRowStorage.dispose(keys); throw error; }
|
|
119
|
+
inner = BatchedRotatingQuantCache.adoptPhysical(keys, values, source.groupSize, source.bits, position);
|
|
120
|
+
} finally { if (k) quantizedRowStorage.dispose(k); if (v) quantizedRowStorage.dispose(v); }
|
|
121
|
+
}
|
|
122
|
+
const row = new AlignedRotatingCache(inner); row.minimumReusableOffset = source.minimumReusableOffset ?? 0;
|
|
123
|
+
result.push(row);
|
|
124
|
+
}
|
|
125
|
+
} catch (error) { for (const row of result) row.dispose(); throw error; }
|
|
126
|
+
for (const row of rows) row.dispose();
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Position state has no codec or attention arithmetic. */
|
|
131
|
+
export class RotatingKvPositions implements TransitioningKvPositions {
|
|
132
|
+
#state: BatchedRotatingState;
|
|
133
|
+
#rope?: MlxArray;
|
|
134
|
+
constructor(maxSize: number) { this.#state = new BatchedRotatingState(maxSize, []); }
|
|
135
|
+
get rowOffsets(): readonly number[] { return this.#state.offsets; }
|
|
136
|
+
get leftPad(): readonly number[] { return this.#state.leftPad; }
|
|
137
|
+
get offset(): number { return this.#state.totalOffset; }
|
|
138
|
+
get batchSize(): number | null { return this.#state.batchSize || null; }
|
|
139
|
+
get ropeOffsetArr(): MlxArray | undefined {
|
|
140
|
+
if (!this.batchSize) return undefined;
|
|
141
|
+
return this.#rope ??= ops.fromInt32([...this.rowOffsets], [this.batchSize]);
|
|
142
|
+
}
|
|
143
|
+
sync(rows: readonly Cache[]): void {
|
|
144
|
+
this.#rope?.dispose(); this.#rope = undefined;
|
|
145
|
+
if (!rows.length) { this.#state = new BatchedRotatingState(this.#state.maxSize, []); return; }
|
|
146
|
+
this.#state.restore(combinedPosition(rows.map(row => (row as AlignedRotatingCache).inner)));
|
|
147
|
+
}
|
|
148
|
+
filterRows(keep: readonly number[]): void { this.#state.filter(keep); this.#rope?.dispose(); this.#rope = undefined; }
|
|
149
|
+
makeMask(tokens: number, window: number | null): Mask {
|
|
150
|
+
return { mode: "array", arr: buildBatchedRotatingMask(this.#state.batchSize, tokens, this.#state.leftPad,
|
|
151
|
+
this.#state.maxSize, window ?? this.#state.maxSize, this.#state.ringIndex, this.#state.totalOffset, this.#state.rotated,
|
|
152
|
+
tokens > 1 || this.#state.hasPendingPadding) };
|
|
153
|
+
}
|
|
154
|
+
dispose(): void { this.#rope?.dispose(); this.#rope = undefined; this.#state = new BatchedRotatingState(this.#state.maxSize, []); }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Once all rows use affine storage, concatenate their already-aligned planes
|
|
158
|
+
* without changing ring columns or rebuilding any attention mask. */
|
|
159
|
+
export class RotatingAffineLayout implements TransitionedKvLayout, PaddedPrefillCache {
|
|
160
|
+
#inner?: BatchedRotatingQuantCache;
|
|
161
|
+
#before?: number[];
|
|
162
|
+
#minimum: number[] = [];
|
|
163
|
+
constructor(readonly maxSize: number, readonly groupSize: number, readonly bits: number) {}
|
|
164
|
+
get minimumReusableOffset(): number { return Math.max(0, ...this.#minimum); }
|
|
165
|
+
get quantizedAttention() { return this.#inner; }
|
|
166
|
+
captureDonorAttention(): import("./gemma4-base").KvDonorAttention {
|
|
167
|
+
return captureRotatingDonorAttention(this.#inner!);
|
|
168
|
+
}
|
|
169
|
+
get rowOffsets(): readonly number[] { return this.#inner?.offsetArr ?? []; }
|
|
170
|
+
get leftPad(): readonly number[] { return this.#inner?.leftPad ?? []; }
|
|
171
|
+
get offset(): number { return this.#inner?.offset ?? 0; }
|
|
172
|
+
get batchSize(): number | null { return this.#inner?.batchSize || null; }
|
|
173
|
+
get ropeOffsetArr(): MlxArray | undefined { return this.#inner?.ropeOffsetArr; }
|
|
174
|
+
get maxTokens(): number { return this.maxSize; }
|
|
175
|
+
signature(): string { return `kv:rotating-affine:${this.maxSize}:${this.groupSize}:${this.bits}`; }
|
|
176
|
+
makeEmptyBatch(): RotatingAffineLayout { return new RotatingAffineLayout(this.maxSize, this.groupSize, this.bits); }
|
|
177
|
+
makeMask(tokens: number, window: number | null): Mask { return this.#inner!.makeMask(tokens, window); }
|
|
178
|
+
preparePrefill(padding: PrefillPadding): void { this.#inner!.preparePrefill(padding); }
|
|
179
|
+
finalizePrefill(): void { this.#inner!.finalizePrefill(); }
|
|
180
|
+
bytesPerToken(): number {
|
|
181
|
+
const tokens = this.#inner?.keys?.packed.shape[2] ?? 0;
|
|
182
|
+
return tokens ? this.state().reduce((n, a) => n + a.nbytes, 0) / (tokens * this.#inner!.batchSize) : 0;
|
|
183
|
+
}
|
|
184
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * Math.min(tokens, this.maxSize); }
|
|
185
|
+
isTrimmable(): boolean { return this.#inner!.positionSnapshot.totalOffset < this.maxSize; }
|
|
186
|
+
trim(count: number): void { this.#inner!.trim(count); }
|
|
187
|
+
specRoundBegin(): void { this.#before = [...this.rowOffsets]; }
|
|
188
|
+
specRoundCommit(): void { this.#before = undefined; }
|
|
189
|
+
specRoundRollback(keep: number | readonly number[]): void {
|
|
190
|
+
this.#inner = rollbackRotatingRing(this.#inner!, this.#before!, keep);
|
|
191
|
+
this.#before = undefined;
|
|
192
|
+
}
|
|
193
|
+
updateAndFetch(): [MlxArray, MlxArray] { return this.#inner!.updateAndFetch(); }
|
|
194
|
+
releaseRopeArr(): void { this.#inner?.releaseRopeArr(); }
|
|
195
|
+
mergeRows(rows: readonly Cache[]): void {
|
|
196
|
+
const owned: Cache[] = [];
|
|
197
|
+
try {
|
|
198
|
+
for (const row of rows) {
|
|
199
|
+
if (row instanceof RotatingAffineLayout) {
|
|
200
|
+
for (let i = 0; i < (row.batchSize ?? 0); i++) owned.push(row.extractRow(i));
|
|
201
|
+
} else if (row instanceof AlignedRotatingCache) owned.push(row.extract());
|
|
202
|
+
else owned.push(...cloneKvCaches([row]));
|
|
203
|
+
}
|
|
204
|
+
if (!owned.length) { this.dispose(); return; }
|
|
205
|
+
const aligned = alignRotatingRows(owned); owned.length = 0;
|
|
206
|
+
try { this.adoptAlignedRows(aligned); } finally { for (const row of aligned) row.dispose(); }
|
|
207
|
+
} finally { for (const row of owned) row.dispose(); }
|
|
208
|
+
}
|
|
209
|
+
adoptAlignedRows(rows: readonly AlignedRotatingCache[]): void {
|
|
210
|
+
const rings = rows.map(row => (row as AlignedRotatingCache).inner as BatchedRotatingQuantCache);
|
|
211
|
+
const position = combinedPosition(rings);
|
|
212
|
+
if (rings.every(row => !row.keys)) {
|
|
213
|
+
const empty = BatchedRotatingQuantCache.adoptPhysical(null, null, this.groupSize, this.bits,
|
|
214
|
+
position);
|
|
215
|
+
this.#inner?.dispose(); this.#inner = empty; this.#minimum = rows.map(row => row.minimumReusableOffset ?? 0);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const merge = (field: "keys" | "values") => {
|
|
219
|
+
const planes: MlxArray[] = [];
|
|
220
|
+
try {
|
|
221
|
+
for (const part of fields) planes.push(ops.concatAxis(rings.map(row => row[field]![part]), 0));
|
|
222
|
+
return { packed: planes[0]!, scales: planes[1]!, biases: planes[2]! };
|
|
223
|
+
} catch (error) { for (const plane of planes) plane.dispose(); throw error; }
|
|
224
|
+
};
|
|
225
|
+
const keys = merge("keys"); let values: ops.QuantizedTensor;
|
|
226
|
+
try { values = merge("values"); } catch (error) { quantizedRowStorage.dispose(keys); throw error; }
|
|
227
|
+
const inner = BatchedRotatingQuantCache.adoptPhysical(keys, values, this.groupSize, this.bits,
|
|
228
|
+
position);
|
|
229
|
+
this.#inner?.dispose(); this.#inner = inner; this.#minimum = rows.map(row => row.minimumReusableOffset ?? 0);
|
|
230
|
+
}
|
|
231
|
+
filterRows(keep: readonly number[]): void { this.#inner!.filterRows(keep); this.#minimum = keep.map(row => this.#minimum[row]!); }
|
|
232
|
+
extractRow(row: number): Cache {
|
|
233
|
+
return this.extract(row);
|
|
234
|
+
}
|
|
235
|
+
protected extract(row: number, limit?: number): Cache {
|
|
236
|
+
const result = this.#inner!.extractRow(row, limit) ?? new RotatingQuantizedKVCache(this.maxSize, this.groupSize, this.bits);
|
|
237
|
+
result.minimumReusableOffset = this.#minimum[row] ?? 0; return result;
|
|
238
|
+
}
|
|
239
|
+
state(): MlxArray[] { return this.#inner?.state() ?? []; }
|
|
240
|
+
dispose(): void { this.#inner?.dispose(); this.#inner = undefined; this.#minimum = []; this.#before = undefined; }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** A committed speculative checkpoint contains only the newest valid window. */
|
|
244
|
+
export class SpeculativeRotatingAffineLayout extends RotatingAffineLayout {
|
|
245
|
+
#ropeForOffset = -1;
|
|
246
|
+
override get ropeOffsetArr(): MlxArray | undefined {
|
|
247
|
+
if (this.#ropeForOffset !== this.offset) {
|
|
248
|
+
this.releaseRopeArr(); this.#ropeForOffset = this.offset;
|
|
249
|
+
}
|
|
250
|
+
return super.ropeOffsetArr;
|
|
251
|
+
}
|
|
252
|
+
override makeEmptyBatch(): SpeculativeRotatingAffineLayout {
|
|
253
|
+
return new SpeculativeRotatingAffineLayout(this.maxSize, this.groupSize, this.bits);
|
|
254
|
+
}
|
|
255
|
+
override makeMask(tokens: number, window: number | null): Mask {
|
|
256
|
+
if (this.leftPad.every(pad => pad === 0) &&
|
|
257
|
+
this.offset + tokens <= Math.min(this.maxSize, window ?? this.maxSize))
|
|
258
|
+
return { mode: tokens === 1 ? "" : "causal", arr: null };
|
|
259
|
+
return super.makeMask(tokens, window);
|
|
260
|
+
}
|
|
261
|
+
override extractRow(row: number): Cache { return this.extract(row, this.maxSize); }
|
|
262
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as ops from "../mlx/ops";
|
|
2
|
+
import { BatchedRotatingCache } from "./batched-rotating";
|
|
3
|
+
import { BatchedRotatingQuantCache } from "./batched-rotating-quant";
|
|
4
|
+
import { rotatingRollbackPlan } from "./batched-rotating-state";
|
|
5
|
+
import { plainRowStorage, quantizedRowStorage, rowRollIndices } from "./batched-row-storage";
|
|
6
|
+
|
|
7
|
+
export type RotatingRing = BatchedRotatingCache | BatchedRotatingQuantCache;
|
|
8
|
+
|
|
9
|
+
/** Retain accepted columns without decoding or recomputing the ring. Ownership
|
|
10
|
+
* moves to the returned ring; aligned members preserve their shared rectangle. */
|
|
11
|
+
export function rollbackRotatingRing(ring: BatchedRotatingCache, before: readonly number[], keep: number | readonly number[], preserveColumns?: boolean): BatchedRotatingCache;
|
|
12
|
+
export function rollbackRotatingRing(ring: BatchedRotatingQuantCache, before: readonly number[], keep: number | readonly number[], preserveColumns?: boolean): BatchedRotatingQuantCache;
|
|
13
|
+
export function rollbackRotatingRing(ring: RotatingRing, before: readonly number[], keep: number | readonly number[], preserveColumns?: boolean): RotatingRing;
|
|
14
|
+
export function rollbackRotatingRing(ring: RotatingRing, before: readonly number[],
|
|
15
|
+
keep: number | readonly number[], preserveColumns = false): RotatingRing {
|
|
16
|
+
const plan = rotatingRollbackPlan(ring.positionSnapshot, before, keep, preserveColumns);
|
|
17
|
+
if ("trim" in plan) { ring.trim(plan.trim); ring.releaseRopeArr(); return ring; }
|
|
18
|
+
let next: RotatingRing;
|
|
19
|
+
if (ring instanceof BatchedRotatingQuantCache) {
|
|
20
|
+
using indices = rowRollIndices(ring.keys!.packed.shape[2]!, plan.shifts);
|
|
21
|
+
const keys = quantizedRowStorage.rollRows(ring.keys!, indices);
|
|
22
|
+
let values: ops.QuantizedTensor;
|
|
23
|
+
try { values = quantizedRowStorage.rollRows(ring.values!, indices); }
|
|
24
|
+
catch (error) { quantizedRowStorage.dispose(keys); throw error; }
|
|
25
|
+
next = BatchedRotatingQuantCache.adoptPhysical(keys, values, ring.groupSize, ring.bits, plan.position);
|
|
26
|
+
} else {
|
|
27
|
+
using indices = rowRollIndices(ring.keys!.shape[2]!, plan.shifts);
|
|
28
|
+
const keys = plainRowStorage.rollRows(ring.keys!, indices);
|
|
29
|
+
let values: import("../mlx/array").MlxArray;
|
|
30
|
+
try { values = plainRowStorage.rollRows(ring.values!, indices); }
|
|
31
|
+
catch (error) { keys.dispose(); throw error; }
|
|
32
|
+
next = BatchedRotatingCache.adoptPhysical(keys, values, plan.position);
|
|
33
|
+
}
|
|
34
|
+
ring.dispose(); return next;
|
|
35
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { rotatingSourcePosition } from "./rotating-kv-layout";
|
|
2
|
+
import { MlxArray } from "../mlx/array";
|
|
3
|
+
import * as ops from "../mlx/ops";
|
|
4
|
+
import { type BatchableCache, type Cache, type Mask, type PrefillPadding, RotatingKVCache } from "./gemma4-base";
|
|
5
|
+
import { BatchedRotatingCache } from "./batched-rotating";
|
|
6
|
+
import { rollbackRotatingRing } from "./rotating-row-transaction";
|
|
7
|
+
import { BatchedRotatingState } from "./batched-rotating-state";
|
|
8
|
+
import { plainRowStorage, temporalStorageView } from "./batched-row-storage";
|
|
9
|
+
import { disposeResources } from "../engine/resources";
|
|
10
|
+
|
|
11
|
+
/** Ring transactions retain accepted KV columns, including rounds that cross
|
|
12
|
+
* the window boundary. The verify block already retains the complete history
|
|
13
|
+
* needed by its earliest query; no target recomputation or scheduler policy. */
|
|
14
|
+
export class SpeculativeRotatingKVCache implements BatchableCache {
|
|
15
|
+
#inner: BatchedRotatingCache;
|
|
16
|
+
#before?: number[];
|
|
17
|
+
constructor(readonly maxSize: number) { this.#inner = new BatchedRotatingCache(maxSize, []); }
|
|
18
|
+
signature(): string { return "kv:rotating-plain"; }
|
|
19
|
+
get offset(): number { return this.#inner.offset; }
|
|
20
|
+
get rowOffsets(): readonly number[] { return this.#inner.offsetArr; }
|
|
21
|
+
get leftPad(): readonly number[] { return this.#inner.leftPad; }
|
|
22
|
+
get batchSize(): number { return this.#inner.batchSize; }
|
|
23
|
+
get ropeOffsetArr(): MlxArray { return this.#inner.ropeOffsetArr; }
|
|
24
|
+
makeEmptyBatch(): SpeculativeRotatingKVCache { return new SpeculativeRotatingKVCache(this.maxSize); }
|
|
25
|
+
makeMask(tokens: number, window: number | null): Mask { return this.#inner.makeMask(tokens, window); }
|
|
26
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] { return this.#inner.updateAndFetch(k, v); }
|
|
27
|
+
preparePrefill(padding: PrefillPadding): void { this.#inner.preparePrefill(padding); }
|
|
28
|
+
finalizePrefill(): void { this.#inner.finalizePrefill(); }
|
|
29
|
+
state(): MlxArray[] { return this.#inner.state(); }
|
|
30
|
+
isTrimmable(): boolean { return this.#inner.isTrimmable(); }
|
|
31
|
+
trim(count: number): void { this.#inner.trim(count); }
|
|
32
|
+
bytesPerToken(): number {
|
|
33
|
+
const state = this.state();
|
|
34
|
+
return state.length ? state.reduce((n, a) => n + a.nbytes, 0) / (this.batchSize * state[0]!.shape[2]!) : 0;
|
|
35
|
+
}
|
|
36
|
+
projectedBytes(tokens: number): number { return this.bytesPerToken() * Math.min(tokens, this.maxSize); }
|
|
37
|
+
extractRow(row: number): Cache {
|
|
38
|
+
const result = new RotatingKVCache(this.maxSize);
|
|
39
|
+
if (!this.rowOffsets[row] || !this.#inner.keys || !this.#inner.values) return result;
|
|
40
|
+
const position = new BatchedRotatingState(this.maxSize, []); position.restore(this.#inner.positionSnapshot);
|
|
41
|
+
const range = { row, from: Math.max(0, position.leftPad[row]!, position.activeLength - this.maxSize),
|
|
42
|
+
to: position.activeLength, copy: true };
|
|
43
|
+
const keys = temporalStorageView(plainRowStorage, this.#inner.keys, position, range);
|
|
44
|
+
let values: MlxArray;
|
|
45
|
+
try { values = temporalStorageView(plainRowStorage, this.#inner.values, position, range); }
|
|
46
|
+
catch (error) { keys.dispose(); throw error; }
|
|
47
|
+
result.restoreState(keys, values, this.rowOffsets[row]!, keys.shape[2]!); return result;
|
|
48
|
+
}
|
|
49
|
+
captureDonorRows(): import("./gemma4-base").KvDonorRows {
|
|
50
|
+
const position = new BatchedRotatingState(this.maxSize, []); position.restore(this.#inner.positionSnapshot);
|
|
51
|
+
const from = Math.max(0, position.activeLength - this.maxSize);
|
|
52
|
+
const range = { from, to: position.activeLength };
|
|
53
|
+
const keys = temporalStorageView(plainRowStorage, this.#inner.keys!, position, range); let values: MlxArray;
|
|
54
|
+
try { values = temporalStorageView(plainRowStorage, this.#inner.values!, position, range); }
|
|
55
|
+
catch (error) { keys.dispose(); throw error; }
|
|
56
|
+
return { keys, values, offsets: [...this.rowOffsets], starts: this.leftPad.map(pad => Math.max(0, pad - from)),
|
|
57
|
+
ends: this.rowOffsets.map(() => position.activeLength - from) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
filterRows(keep: readonly number[]): void { this.#inner.filterRows(keep); }
|
|
61
|
+
mergeRows(sources: readonly Cache[]): void {
|
|
62
|
+
const only = sources.length === 1 ? sources[0] : undefined;
|
|
63
|
+
if (only instanceof SpeculativeRotatingKVCache) {
|
|
64
|
+
const keys = only.#inner.keys ? ops.copyOf(only.#inner.keys) : null;
|
|
65
|
+
let values: MlxArray | null;
|
|
66
|
+
try { values = only.#inner.values ? ops.copyOf(only.#inner.values) : null; }
|
|
67
|
+
catch (error) { keys?.dispose(); throw error; }
|
|
68
|
+
const next = BatchedRotatingCache.adoptPhysical(keys, values, only.#inner.positionSnapshot);
|
|
69
|
+
this.#inner.dispose(); this.#inner = next; return;
|
|
70
|
+
}
|
|
71
|
+
const held: Cache[] = [], planes: { keys: MlxArray; values: MlxArray }[] = [], offsets: number[] = [];
|
|
72
|
+
try {
|
|
73
|
+
const rows = sources.flatMap(source => source instanceof SpeculativeRotatingKVCache
|
|
74
|
+
? Array.from({ length: source.batchSize }, (_, row) => { const cache = source.extractRow(row); held.push(cache); return cache; })
|
|
75
|
+
: [source]) as RotatingKVCache[];
|
|
76
|
+
const prototype = rows.find(row => row.keys !== null);
|
|
77
|
+
if (!prototype) {
|
|
78
|
+
this.#inner.dispose(); this.#inner = new BatchedRotatingCache(this.maxSize, rows.map(() => 0)); return;
|
|
79
|
+
}
|
|
80
|
+
for (const cache of rows) {
|
|
81
|
+
const state = rotatingSourcePosition(cache);
|
|
82
|
+
const range = { from: Math.max(0, state.activeLength - this.maxSize), to: state.activeLength };
|
|
83
|
+
const plane = (field: "keys" | "values") => cache[field]
|
|
84
|
+
? temporalStorageView(plainRowStorage, cache[field]!, state, range)
|
|
85
|
+
: ops.zeros([1, prototype[field]!.shape[1]!, 0, prototype[field]!.shape[3]!], prototype[field]!.dtype);
|
|
86
|
+
const keys = plane("keys"); let values: MlxArray;
|
|
87
|
+
try { values = plane("values"); } catch (error) { keys.dispose(); throw error; }
|
|
88
|
+
planes.push({ keys, values }); offsets.push(cache.offset);
|
|
89
|
+
}
|
|
90
|
+
const next = BatchedRotatingCache.merge(planes, offsets, this.maxSize);
|
|
91
|
+
this.#inner.dispose(); this.#inner = next;
|
|
92
|
+
} finally { disposeResources([...held, ...planes.flatMap(row => [row.keys, row.values])]); }
|
|
93
|
+
}
|
|
94
|
+
specRoundBegin(): void { this.#before = [...this.rowOffsets]; }
|
|
95
|
+
specRoundCommit(): void { this.#before = undefined; }
|
|
96
|
+
specRoundRollback(keep: number | readonly number[]): void {
|
|
97
|
+
this.#inner = rollbackRotatingRing(this.#inner, this.#before!, keep);
|
|
98
|
+
this.#before = undefined;
|
|
99
|
+
}
|
|
100
|
+
dispose(): void { this.#before = undefined; this.#inner.dispose(); }
|
|
101
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PrefillPadding } from "./gemma4-base";
|
|
2
|
+
import type { MlxArray } from "../mlx/array";
|
|
3
|
+
import { Dtype } from "../mlx/ffi";
|
|
4
|
+
import * as ops from "../mlx/ops";
|
|
5
|
+
|
|
6
|
+
/** ArraysCache's remaining lengths and padding from mlx-lm (MIT).
|
|
7
|
+
* Recurrent state has no token axis to roll: masked tokens must not update it. */
|
|
8
|
+
export class SsmPrefillPadding {
|
|
9
|
+
#left: number[];
|
|
10
|
+
#lengths: number[];
|
|
11
|
+
constructor(padding: PrefillPadding) {
|
|
12
|
+
this.#left = padding.leftPadding ? [...padding.leftPadding] : padding.lengths.map(() => 0);
|
|
13
|
+
this.#lengths = [...padding.lengths];
|
|
14
|
+
}
|
|
15
|
+
convolutionLengths(tokens: number): number[] {
|
|
16
|
+
return this.#lengths.map(length => Math.max(0, Math.min(tokens, length)));
|
|
17
|
+
}
|
|
18
|
+
makeMask(tokens: number): MlxArray {
|
|
19
|
+
const B = this.#lengths.length;
|
|
20
|
+
using arange = ops.arange(0, tokens, 1, Dtype.int32);
|
|
21
|
+
using positions = ops.reshape(arange, [1, tokens]);
|
|
22
|
+
using left = ops.fromInt32(this.#left, [B, 1]);
|
|
23
|
+
using ends = ops.fromInt32(this.#lengths, [B, 1]);
|
|
24
|
+
using after = ops.greaterEqual(positions, left);
|
|
25
|
+
using before = ops.less(positions, ends);
|
|
26
|
+
return ops.logicalAnd(after, before);
|
|
27
|
+
}
|
|
28
|
+
advance(tokens: number): number[] {
|
|
29
|
+
const counts = this.#lengths.map((length, row) => Math.max(0,
|
|
30
|
+
Math.min(tokens, length) - Math.max(0, Math.min(tokens, this.#left[row]!))));
|
|
31
|
+
this.#lengths = this.#lengths.map(length => length - tokens);
|
|
32
|
+
this.#left = this.#left.map(left => left - tokens);
|
|
33
|
+
return counts;
|
|
34
|
+
}
|
|
35
|
+
filter(keep: readonly number[]): void {
|
|
36
|
+
this.#left = keep.map(row => this.#left[row]!);
|
|
37
|
+
this.#lengths = keep.map(row => this.#lengths[row]!);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -403,7 +403,7 @@ function lutFor(L: number): MlxArray {
|
|
|
403
403
|
return a;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
/** Decode variant (see HEADER). Default
|
|
406
|
+
/** Decode variant (see HEADER). Default passed the M4 Pro closeout matrix;
|
|
407
407
|
* `MLX_BUN_TRELLIS_VARIANT` overrides for experiments. */
|
|
408
408
|
let variantOverride: number | null = null;
|
|
409
409
|
/** Explicit override (benches); otherwise the runtime flag — read per call so
|
|
@@ -411,7 +411,7 @@ let variantOverride: number | null = null;
|
|
|
411
411
|
* can A/B two decodes on one loaded model. */
|
|
412
412
|
export function setTrellisVariant(v: number | null): void { variantOverride = v; }
|
|
413
413
|
function variant(): number {
|
|
414
|
-
return variantOverride ?? runtimeNumber("MLX_BUN_TRELLIS_VARIANT",
|
|
414
|
+
return variantOverride ?? runtimeNumber("MLX_BUN_TRELLIS_VARIANT", 13);
|
|
415
415
|
}
|
|
416
416
|
// Variants 7..13 change work assignment/scheduling, not decoded weight values.
|
|
417
417
|
function decoderVariant(selected = variant()): number { return selected >= 7 && selected <= 13 ? 6 : selected; }
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as tq from "../mlx/turboquant-ops";
|
|
3
|
+
import type { KvCodec } from "../backends/mlx/kv-codec";
|
|
4
|
+
|
|
5
|
+
/** One TurboQuant-encoded (K, V) storage tuple — the 5 arrays kv-store.ts
|
|
6
|
+
* and toQuantized/fromKVCache pass around. Not `ops.QuantizedTensor`
|
|
7
|
+
* (mlx's affine int4/int8 scheme): this is the rotation + Lloyd-Max
|
|
8
|
+
* layout (docs/design/turboquant.md), asymmetric-affine for keys,
|
|
9
|
+
* FWHT+Lloyd-Max for values. Field order is the kv-store.ts tensor-slot
|
|
10
|
+
* contract — do not reorder without updating snapshotCache/loadKvCache. */
|
|
11
|
+
export interface TurboQuantTensor {
|
|
12
|
+
kIdx: MlxArray;
|
|
13
|
+
kScales: MlxArray;
|
|
14
|
+
kZeros: MlxArray;
|
|
15
|
+
vPacked: MlxArray;
|
|
16
|
+
vScales: MlxArray;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const disposeTurboQuant = (t: TurboQuantTensor): void => {
|
|
20
|
+
t.kIdx.dispose();
|
|
21
|
+
t.kScales.dispose();
|
|
22
|
+
t.kZeros.dispose();
|
|
23
|
+
t.vPacked.dispose();
|
|
24
|
+
t.vScales.dispose();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Numerical representation only. Storage owns capacity, positions, row
|
|
28
|
+
* membership and persistence; this codec retains the existing kernel choices. */
|
|
29
|
+
export class TurboQuantCodec implements KvCodec<TurboQuantTensor> {
|
|
30
|
+
constructor(readonly kBits: number, readonly vBits: number, readonly fusedDecode: boolean) {}
|
|
31
|
+
|
|
32
|
+
/** Encode k/v [B,H,L,D] into a fresh TurboQuantTensor (packed at rest). */
|
|
33
|
+
encode(k: MlxArray, v: MlxArray): TurboQuantTensor {
|
|
34
|
+
const signed = this.kBits === 8;
|
|
35
|
+
const kEnc = tq.encodeKeys(k, this.kBits, signed);
|
|
36
|
+
const kIdxPacked = this.kBits < 8 ? tq.packBits(kEnc.indices, this.kBits) : kEnc.indices;
|
|
37
|
+
if (kIdxPacked !== kEnc.indices) kEnc.indices.dispose();
|
|
38
|
+
const vEnc = tq.encodeValues(v, this.vBits);
|
|
39
|
+
const vIdxPacked = this.vBits < 8 ? tq.packBits(vEnc.indices, this.vBits) : vEnc.indices;
|
|
40
|
+
if (vIdxPacked !== vEnc.indices) vEnc.indices.dispose();
|
|
41
|
+
return {
|
|
42
|
+
kIdx: kIdxPacked, kScales: kEnc.scales, kZeros: kEnc.zeros,
|
|
43
|
+
vPacked: vIdxPacked, vScales: vEnc.scales,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Decode a TurboQuantTensor's active [B,H,upTo,*] window back to
|
|
48
|
+
* bf16 [B,H,upTo,headDim] (k, v). Caller owns the returned arrays.
|
|
49
|
+
* deferV leaves V in the rotated domain (decodeValuesRotated) — the
|
|
50
|
+
* caller un-rotates its attention output instead (tq.unrotateValues). */
|
|
51
|
+
decode(t: TurboQuantTensor, upTo: number, headDim: number, deferV = false): [MlxArray, MlxArray] {
|
|
52
|
+
const cut = (a: MlxArray): MlxArray => {
|
|
53
|
+
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
54
|
+
return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
|
|
55
|
+
};
|
|
56
|
+
if (this.fusedDecode) {
|
|
57
|
+
const inputs = [cut(t.kIdx), cut(t.kScales), cut(t.kZeros), cut(t.vPacked), cut(t.vScales)] as const;
|
|
58
|
+
try {
|
|
59
|
+
const decoded = tq.tryDecodePackedKv(inputs, this.kBits, this.vBits, headDim, deferV);
|
|
60
|
+
if (decoded) return decoded;
|
|
61
|
+
} finally { for (const input of inputs) input.dispose(); }
|
|
62
|
+
}
|
|
63
|
+
const kIdxCut = cut(t.kIdx);
|
|
64
|
+
const kIdxUnpacked = this.kBits < 8 ? tq.unpackBits(kIdxCut, this.kBits, headDim) : kIdxCut;
|
|
65
|
+
if (kIdxUnpacked !== kIdxCut) kIdxCut.dispose();
|
|
66
|
+
const kScalesCut = cut(t.kScales);
|
|
67
|
+
const kZerosCut = cut(t.kZeros);
|
|
68
|
+
const k = tq.decodeKeys(kIdxUnpacked, kScalesCut, kZerosCut);
|
|
69
|
+
for (const a of [kIdxUnpacked, kScalesCut, kZerosCut]) a.dispose();
|
|
70
|
+
|
|
71
|
+
const vPackedCut = cut(t.vPacked);
|
|
72
|
+
const vIdxUnpacked = this.vBits < 8 ? tq.unpackBits(vPackedCut, this.vBits, headDim) : vPackedCut;
|
|
73
|
+
if (vIdxUnpacked !== vPackedCut) vPackedCut.dispose();
|
|
74
|
+
const vScalesCut = cut(t.vScales);
|
|
75
|
+
const v = deferV
|
|
76
|
+
? tq.decodeValuesRotated(vIdxUnpacked, vScalesCut, this.vBits)
|
|
77
|
+
: tq.decodeValues(vIdxUnpacked, vScalesCut, this.vBits);
|
|
78
|
+
for (const a of [vIdxUnpacked, vScalesCut]) a.dispose();
|
|
79
|
+
|
|
80
|
+
return [k, v];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { captureKvAttention } from "../kv-attention-view";
|
|
1
2
|
// UniversalDense — the Tier-0 config-driven dense llama-family module
|
|
2
3
|
// (docs/design/generic-model-support.md §3.1). One module, shaped exactly
|
|
3
4
|
// like qwen3.ts, with every arch delta selected by the UniversalArgs
|
|
4
5
|
// descriptor (archs.ts). Monolith path only: no compiled
|
|
5
|
-
// decode
|
|
6
|
+
// decode. Standard attention uses the shared plain/affine KV port; the
|
|
7
|
+
// softcap branch retains plain KV. The bar is L1 bit-exactness
|
|
6
8
|
// vs mlx-lm on this machine's GPU (tests/parity/universal-parity.test.ts).
|
|
7
9
|
//
|
|
8
10
|
// Porting discipline: each branch transcribes its mlx-lm source op-for-op
|
|
@@ -154,19 +156,18 @@ class UniversalAttention {
|
|
|
154
156
|
? this.rope.applyDynamic(k, offArr)
|
|
155
157
|
: this.rope.apply(k, cache.offset));
|
|
156
158
|
|
|
157
|
-
const [keys, values] = cache.updateAndFetch(k, v);
|
|
158
|
-
k.dispose();
|
|
159
|
-
v.dispose();
|
|
160
|
-
|
|
161
159
|
let attn: MlxArray;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
160
|
+
try {
|
|
161
|
+
if (a.attnLogitSoftcap !== null) {
|
|
162
|
+
const [keys, values] = cache.updateAndFetch(k, v);
|
|
163
|
+
try { attn = this.#softcapAttention(q, keys, values, mask, B, L); }
|
|
164
|
+
finally { keys.dispose(); values.dispose(); }
|
|
165
|
+
} else {
|
|
166
|
+
const view = captureKvAttention(cache, k, v);
|
|
167
|
+
try { attn = view.attend(q, a.attnScale, mask); }
|
|
168
|
+
finally { view.dispose(); }
|
|
169
|
+
}
|
|
170
|
+
} finally { k.dispose(); v.dispose(); q.dispose(); }
|
|
170
171
|
|
|
171
172
|
const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
|
|
172
173
|
attn.dispose();
|