mlx-bun 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -32
- package/docs/README.md +58 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +1535 -50
- package/docs/design/decode-speed-program.md +394 -1171
- package/docs/design/generic-model-support.md +195 -3
- package/docs/design/kv-cache.md +538 -225
- package/docs/design/speculative-decoding.md +552 -50
- package/docs/design/turboquant.md +140 -17
- package/docs/design/unified-engine-frontier-plan.md +259 -150
- package/docs/planning/PRODUCT_ROADMAP.md +21 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3537 -1112
- package/docs/reference/cli.md +102 -2
- package/docs/reference/distribution.md +45 -13
- package/docs/reference/environment.md +159 -13
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +28 -7
- package/docs/reference/server-api.md +257 -33
- package/docs/reference/server-config.md +372 -151
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +2 -2
- package/src/audio/audiotoolbox.ts +112 -0
- package/src/audio/conformer.ts +8 -0
- package/src/audio/mic-capture.ts +98 -0
- package/src/audio/silero-vad.ts +379 -0
- package/src/audio/whisper-decode.ts +774 -0
- package/src/audio/whisper-fast.ts +732 -0
- package/src/audio/whisper-format.ts +73 -0
- package/src/audio/whisper-mel.ts +185 -0
- package/src/audio/whisper-timing.ts +274 -0
- package/src/audio/whisper-tokenizer.ts +239 -0
- package/src/audio/whisper-transcribe.ts +410 -0
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +464 -236
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +107 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- package/src/backends/mlx/encoder-cache.ts +25 -0
- package/src/backends/mlx/fill-append.ts +21 -8
- package/src/backends/mlx/fill-group.ts +497 -0
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +122 -19
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +161 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/mixed-iteration.ts +62 -0
- package/src/backends/mlx/model-prompt.ts +46 -32
- package/src/backends/mlx/model-serving.ts +5 -4
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +100 -0
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +244 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/prompt-input.ts +29 -0
- package/src/backends/mlx/qwen-prompt-input.ts +40 -0
- package/src/backends/mlx/request-state-policy.ts +54 -0
- package/src/backends/mlx/rollback.ts +45 -4
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +51 -73
- package/src/backends/mlx/speculative-group.ts +438 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +21 -1
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
- package/src/cli.ts +333 -36
- package/src/contracts/execution.ts +21 -0
- package/src/contracts/object-cache.ts +8 -0
- package/src/contracts/pi-provider.ts +1 -1
- package/src/contracts/pi.ts +3 -1
- package/src/contracts/prefix-cache.ts +33 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/engine/execution-plan.ts +38 -25
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +32 -7
- package/src/fill/fill-session.ts +14 -3
- package/src/fit.ts +29 -15
- package/src/generate.ts +105 -195
- package/src/grammar.ts +13 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/prefill.ts +6 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +92 -12
- package/src/kv-store.ts +183 -93
- package/src/lab/README.md +5 -1
- package/src/lab/paged-kv/paged-attention.ts +117 -0
- package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
- package/src/lab/paged-kv/paged-kv.ts +126 -46
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +39 -1
- package/src/mlx/ffi.ts +19 -0
- package/src/mlx/normalized-argmax.ts +116 -0
- package/src/mlx/ops.ts +97 -10
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +12 -4
- package/src/model/batched-quant.ts +8 -2
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +76 -15
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +64 -22
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +94 -0
- package/src/model/compiled-decode.ts +7 -1
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +57 -0
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +183 -82
- package/src/model/gemma4.ts +116 -28
- package/src/model/generated/gemma4-12b.ts +4 -1
- package/src/model/generated/gemma4-26b.ts +4 -1
- package/src/model/generated/gemma4-e4b.ts +4 -1
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/kv-attention-view.ts +70 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +177 -75
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +4 -4
- package/src/model/turboquant-codec.ts +89 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/model/whisper.ts +435 -0
- package/src/native/mic_capture.swift +92 -0
- package/src/native-pack.ts +6 -5
- package/src/pi-web.ts +9 -1
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +306 -86
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +37 -25
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +19 -14
- package/src/serve/completion-executor.ts +9 -1
- package/src/serve/discovery-routes.ts +17 -1
- package/src/serve/generated-token-history.ts +57 -0
- package/src/serve/generation-gateway.ts +88 -38
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/model-host.ts +3 -4
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +6 -3
- package/src/serve/request-prep.ts +40 -25
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +185 -261
- package/src/spec/assistant-rows.ts +93 -0
- package/src/spec/assistant-source.ts +28 -18
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +12 -10
- package/src/spec/dflash-source.ts +28 -4
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/deepspec-module.ts +95 -120
- package/src/spec/dspark/module-dflash.ts +126 -170
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +115 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +230 -0
- package/src/spec/qwen-mtp-source.ts +84 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +73 -69
- package/src/spec/source.ts +93 -15
- package/src/spec/two-model-rows.ts +62 -0
- package/src/spec/two-model.ts +45 -103
- package/src/ssd-cache.ts +107 -36
- package/src/ssd-durability.ts +29 -27
- package/src/storage/host-buffer.ts +24 -0
- package/src/storage/kv-writer.ts +83 -0
- package/src/storage/kv-writer.worker.js +264 -0
- package/src/storage/retention-policy.ts +37 -0
- package/src/tiered-prompt-cache.ts +45 -0
- package/src/vision/embedder.ts +8 -0
- package/src/vision/prompt.ts +101 -65
- package/src/vision/qwen3vl-prompt.ts +101 -64
- package/src/vision/qwen3vl-tower.ts +11 -0
- package/src/vision/siglip.ts +8 -0
- package/src/web/app.html +12 -0
- package/src/web/app.js +217 -57
- package/src/web/src/chat.ts +4 -1
- package/src/web/src/composer.ts +3 -0
- package/src/web/src/voice.ts +141 -0
- package/src/web/sw.js +1 -1
- package/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
|
@@ -0,0 +1,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
|
+
}
|
package/src/model/support.ts
CHANGED
|
@@ -61,6 +61,13 @@ export function isDiffusionGemmaConfig(config: ModelConfig): boolean {
|
|
|
61
61
|
return config.modelType === "diffusion_gemma";
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Whisper (model_type `whisper`, mlx-whisper ModelDimensions config):
|
|
65
|
+
* encoder-decoder speech recognition — audio in, text out. Routed through
|
|
66
|
+
* the transcription engine (src/audio/whisper-*.ts), never the chat loop. */
|
|
67
|
+
export function isWhisperConfig(config: ModelConfig): boolean {
|
|
68
|
+
return config.modelType === "whisper" && typeof config.raw.n_audio_ctx === "number";
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
/** Support tier for a registry record (docs/design/generic-model-support.md):
|
|
65
72
|
* "targeted" = dedicated/generated forward + L2/L3 paths;
|
|
66
73
|
* "generic" = the Tier-0 universal module (L1 monolith only);
|
|
@@ -74,6 +81,7 @@ export function supportTier(modelType: string, repoId = ""): "targeted" | "gener
|
|
|
74
81
|
if (modelType === "qwen3") return "targeted"; // plain Qwen3 (embedding backbone)
|
|
75
82
|
if (modelType === "qwen3_moe") return "targeted"; // Qwen3-MoE (sparse experts)
|
|
76
83
|
if (modelType === "glm_moe_dsa") return "targeted"; // dedicated GLM-5.2 MLA/DSA port
|
|
84
|
+
if (modelType === "whisper") return "targeted"; // speech-to-text (transcription engine)
|
|
77
85
|
if (modelType === "llama" && repoId.toLowerCase().includes("minicpm5-1b-optiq-4bit"))
|
|
78
86
|
return "targeted";
|
|
79
87
|
// Tier-0 generic fallback: the universal-dense descriptor table.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import type { Cache } from "./gemma4-base";
|
|
4
|
+
|
|
5
|
+
export interface TokenWorkOptions {
|
|
6
|
+
/** Verification keeps its established GEMV/GEMM geometry. Other groups may
|
|
7
|
+
* still pack their tokenwise work independently. */
|
|
8
|
+
readonly preserveTokenGeometry?: boolean;
|
|
9
|
+
/** Borrows a residual stream; the method copies only the layers it needs.
|
|
10
|
+
* The layer-count index denotes the post-final-norm output. */
|
|
11
|
+
readonly captureLayer?: (layer: number, hidden: MlxArray) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Independent sequences presented to one model execution. Each item keeps
|
|
15
|
+
* its existing rectangular row geometry and owns a disjoint cache. */
|
|
16
|
+
export interface TokenGroup extends TokenWorkOptions {
|
|
17
|
+
readonly ids: MlxArray;
|
|
18
|
+
readonly cache: Cache[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Apply a tokenwise block while respecting each method's numerical policy. */
|
|
22
|
+
export function mapTokenGroups(
|
|
23
|
+
groups: readonly TokenGroup[], inputs: readonly MlxArray[],
|
|
24
|
+
operation: (hidden: MlxArray) => MlxArray, pack = true,
|
|
25
|
+
): MlxArray[] {
|
|
26
|
+
const results: MlxArray[] = [];
|
|
27
|
+
const packedRows = groups.flatMap((group, row) => pack && !group.preserveTokenGeometry ? [row] : []);
|
|
28
|
+
try {
|
|
29
|
+
const packed = packedRows.length ? mapPackedTokens(packedRows.map(row => inputs[row]!), operation) : [];
|
|
30
|
+
for (const [index, row] of packedRows.entries()) results[row] = packed[index]!;
|
|
31
|
+
for (let row = 0; row < groups.length; row++) results[row] ??= operation(inputs[row]!);
|
|
32
|
+
return results;
|
|
33
|
+
} catch (error) { for (const result of results) result?.dispose(); throw error; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MixedTokenModel {
|
|
37
|
+
/** Borrows inputs/state; returns one owned hidden array per input group. */
|
|
38
|
+
forwardHiddenMixed(groups: readonly TokenGroup[]): MlxArray[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Pack real tokens for a tokenwise operation, then restore each group's
|
|
42
|
+
* geometry. Attention and recurrent operations must run outside this port.
|
|
43
|
+
* No padding, host readback, or evaluation is introduced here. */
|
|
44
|
+
export function mapPackedTokens(
|
|
45
|
+
inputs: readonly MlxArray[], operation: (packed: MlxArray) => MlxArray,
|
|
46
|
+
): MlxArray[] {
|
|
47
|
+
if (inputs.length === 1) return [operation(inputs[0]!)];
|
|
48
|
+
const flat = inputs.map(input => ops.reshape(input, [1, -1, input.shape.at(-1)!]));
|
|
49
|
+
let packed: MlxArray;
|
|
50
|
+
try { packed = ops.concatAxis(flat, 1); }
|
|
51
|
+
finally { for (const input of flat) input.dispose(); }
|
|
52
|
+
using joined = packed;
|
|
53
|
+
using output = operation(joined);
|
|
54
|
+
const results: MlxArray[] = [];
|
|
55
|
+
let position = 0;
|
|
56
|
+
try {
|
|
57
|
+
for (const input of inputs) {
|
|
58
|
+
const [batch, length] = input.shape;
|
|
59
|
+
const end = position + batch! * length!;
|
|
60
|
+
using part = output.slice([0, position, 0], [1, end, output.shape[2]!]);
|
|
61
|
+
results.push(ops.reshape(part, [batch!, length!, output.shape[2]!]));
|
|
62
|
+
position = end;
|
|
63
|
+
}
|
|
64
|
+
return results;
|
|
65
|
+
} catch (error) { for (const result of results) result.dispose(); throw error; }
|
|
66
|
+
}
|
|
@@ -41,7 +41,7 @@ import * as ops from "../mlx/ops";
|
|
|
41
41
|
import { MetalKernel } from "../mlx/metal-kernel";
|
|
42
42
|
import type { Weights } from "../weights";
|
|
43
43
|
import { quantFor, type ModelConfig, type QuantSpec } from "../config";
|
|
44
|
-
import { runtimeFlag, runtimeNumber } from "../runtime-config";
|
|
44
|
+
import { runtimeFlag, runtimeNumber, runtimeValue } from "../runtime-config";
|
|
45
45
|
import { lut1mad, wordsPerBlock } from "../quantize/trellis";
|
|
46
46
|
import { QuantizedLinear } from "./gemma4-base";
|
|
47
47
|
import { REDUCE_SHARED_M_SOURCE, GATEUP_SHARED_M_SOURCE } from "./trellis-shared-m";
|
|
@@ -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; }
|
|
@@ -475,7 +475,7 @@ export function expandTrellis(codes: MlxArray, scales: MlxArray, g: TrellisGeome
|
|
|
475
475
|
export type TrellisMode = "kernel" | "expand";
|
|
476
476
|
|
|
477
477
|
export function trellisModeFromEnv(): TrellisMode {
|
|
478
|
-
const v =
|
|
478
|
+
const v = runtimeValue("MLX_BUN_TRELLIS");
|
|
479
479
|
if (v === "expand") return "expand";
|
|
480
480
|
return "kernel";
|
|
481
481
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { runtimeConfig, type RuntimeConfig } from "../runtime-config";
|
|
2
|
+
import type { MlxArray } from "../mlx/array";
|
|
3
|
+
import * as tq from "../mlx/turboquant-ops";
|
|
4
|
+
import type { KvCodec } from "../backends/mlx/kv-codec";
|
|
5
|
+
|
|
6
|
+
/** Resolve once when a cache is created; storage copies retain this policy. */
|
|
7
|
+
export function turboQuantFusedDecode(config: RuntimeConfig = runtimeConfig()): boolean {
|
|
8
|
+
return config.flag("MLX_BUN_TURBOQUANT_FUSED_DECODE", true);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** One TurboQuant-encoded (K, V) storage tuple — the 5 arrays kv-store.ts
|
|
12
|
+
* and toQuantized/fromKVCache pass around. Not `ops.QuantizedTensor`
|
|
13
|
+
* (mlx's affine int4/int8 scheme): this is the rotation + Lloyd-Max
|
|
14
|
+
* layout (docs/design/turboquant.md), asymmetric-affine for keys,
|
|
15
|
+
* FWHT+Lloyd-Max for values. Field order is the kv-store.ts tensor-slot
|
|
16
|
+
* contract — do not reorder without updating snapshotCache/loadKvCache. */
|
|
17
|
+
export interface TurboQuantTensor {
|
|
18
|
+
kIdx: MlxArray;
|
|
19
|
+
kScales: MlxArray;
|
|
20
|
+
kZeros: MlxArray;
|
|
21
|
+
vPacked: MlxArray;
|
|
22
|
+
vScales: MlxArray;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const disposeTurboQuant = (t: TurboQuantTensor): void => {
|
|
26
|
+
t.kIdx.dispose();
|
|
27
|
+
t.kScales.dispose();
|
|
28
|
+
t.kZeros.dispose();
|
|
29
|
+
t.vPacked.dispose();
|
|
30
|
+
t.vScales.dispose();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Numerical representation only. Storage owns capacity, positions, row
|
|
34
|
+
* membership and persistence; this codec retains the existing kernel choices. */
|
|
35
|
+
export class TurboQuantCodec implements KvCodec<TurboQuantTensor> {
|
|
36
|
+
constructor(readonly kBits: number, readonly vBits: number, readonly fusedDecode: boolean) {}
|
|
37
|
+
|
|
38
|
+
/** Encode k/v [B,H,L,D] into a fresh TurboQuantTensor (packed at rest). */
|
|
39
|
+
encode(k: MlxArray, v: MlxArray): TurboQuantTensor {
|
|
40
|
+
const signed = this.kBits === 8;
|
|
41
|
+
const kEnc = tq.encodeKeys(k, this.kBits, signed);
|
|
42
|
+
const kIdxPacked = this.kBits < 8 ? tq.packBits(kEnc.indices, this.kBits) : kEnc.indices;
|
|
43
|
+
if (kIdxPacked !== kEnc.indices) kEnc.indices.dispose();
|
|
44
|
+
const vEnc = tq.encodeValues(v, this.vBits);
|
|
45
|
+
const vIdxPacked = this.vBits < 8 ? tq.packBits(vEnc.indices, this.vBits) : vEnc.indices;
|
|
46
|
+
if (vIdxPacked !== vEnc.indices) vEnc.indices.dispose();
|
|
47
|
+
return {
|
|
48
|
+
kIdx: kIdxPacked, kScales: kEnc.scales, kZeros: kEnc.zeros,
|
|
49
|
+
vPacked: vIdxPacked, vScales: vEnc.scales,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Decode a TurboQuantTensor's active [B,H,upTo,*] window back to
|
|
54
|
+
* bf16 [B,H,upTo,headDim] (k, v). Caller owns the returned arrays.
|
|
55
|
+
* deferV leaves V in the rotated domain (decodeValuesRotated) — the
|
|
56
|
+
* caller un-rotates its attention output instead (tq.unrotateValues). */
|
|
57
|
+
decode(t: TurboQuantTensor, upTo: number, headDim: number, deferV = false): [MlxArray, MlxArray] {
|
|
58
|
+
const cut = (a: MlxArray): MlxArray => {
|
|
59
|
+
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
60
|
+
return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
|
|
61
|
+
};
|
|
62
|
+
if (this.fusedDecode) {
|
|
63
|
+
const inputs = [cut(t.kIdx), cut(t.kScales), cut(t.kZeros), cut(t.vPacked), cut(t.vScales)] as const;
|
|
64
|
+
try {
|
|
65
|
+
const decoded = tq.tryDecodePackedKv(inputs, this.kBits, this.vBits, headDim, deferV);
|
|
66
|
+
if (decoded) return decoded;
|
|
67
|
+
} finally { for (const input of inputs) input.dispose(); }
|
|
68
|
+
}
|
|
69
|
+
const kIdxCut = cut(t.kIdx);
|
|
70
|
+
const kIdxUnpacked = this.kBits < 8 ? tq.unpackBits(kIdxCut, this.kBits, headDim) : kIdxCut;
|
|
71
|
+
if (kIdxUnpacked !== kIdxCut) kIdxCut.dispose();
|
|
72
|
+
const kScalesCut = cut(t.kScales);
|
|
73
|
+
const kZerosCut = cut(t.kZeros);
|
|
74
|
+
const k = tq.decodeKeys(kIdxUnpacked, kScalesCut, kZerosCut);
|
|
75
|
+
for (const a of [kIdxUnpacked, kScalesCut, kZerosCut]) a.dispose();
|
|
76
|
+
|
|
77
|
+
const vPackedCut = cut(t.vPacked);
|
|
78
|
+
const vIdxUnpacked = this.vBits < 8 ? tq.unpackBits(vPackedCut, this.vBits, headDim) : vPackedCut;
|
|
79
|
+
if (vIdxUnpacked !== vPackedCut) vPackedCut.dispose();
|
|
80
|
+
const vScalesCut = cut(t.vScales);
|
|
81
|
+
const v = deferV
|
|
82
|
+
? tq.decodeValuesRotated(vIdxUnpacked, vScalesCut, this.vBits)
|
|
83
|
+
: tq.decodeValues(vIdxUnpacked, vScalesCut, this.vBits);
|
|
84
|
+
for (const a of [vIdxUnpacked, vScalesCut]) a.dispose();
|
|
85
|
+
|
|
86
|
+
return [k, v];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
}
|