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
package/src/model/gemma4-base.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// DecoderLayer, Gemma4Model) stays in gemma4.ts and becomes the
|
|
8
8
|
// per-model generated code in Phase C.
|
|
9
9
|
|
|
10
|
+
import { decodedKvDonorAttention } from "./decoded-kv-donor";
|
|
10
11
|
import type { ModelConfig } from "../config";
|
|
11
12
|
import { quantFor } from "../config";
|
|
12
13
|
import type { Weights } from "../weights";
|
|
@@ -16,6 +17,9 @@ import * as ops from "../mlx/ops";
|
|
|
16
17
|
import { quantizedMatmulRows } from "../mlx/quantized-rows";
|
|
17
18
|
import { expertOffloadArray } from "../expert-offload";
|
|
18
19
|
import * as tq from "../mlx/turboquant-ops";
|
|
20
|
+
import type { KvCodec } from "../backends/mlx/kv-codec";
|
|
21
|
+
import { TurboQuantCodec, disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
|
|
22
|
+
export { disposeTurboQuant, type TurboQuantTensor } from "./turboquant-codec";
|
|
19
23
|
import { runtimeValue } from "../runtime-config";
|
|
20
24
|
|
|
21
25
|
export type MaskMode = "" | "causal";
|
|
@@ -249,9 +253,9 @@ export class QuantizedEmbedding {
|
|
|
249
253
|
|
|
250
254
|
/** Fetched KV handed from a donor layer to its sharers (and to the
|
|
251
255
|
* speculative drafter). Owned by forwardLayers for the pass.
|
|
252
|
-
* `offsetArr` is
|
|
253
|
-
*
|
|
254
|
-
*
|
|
256
|
+
* `offsetArr` is an owned pre-write position snapshot. Donor and sharing
|
|
257
|
+
* attention layers keep it until the fetched KV is released, even if the
|
|
258
|
+
* cache advances or replaces its borrowed row-position array. */
|
|
255
259
|
export type SharedKv =
|
|
256
260
|
| { kind: "plain"; keys: MlxArray; values: MlxArray; offset: number;
|
|
257
261
|
offsetArr?: MlxArray;
|
|
@@ -260,11 +264,87 @@ export type SharedKv =
|
|
|
260
264
|
* (tq.unrotateValues) after sdpa. Attention is linear in V, so the
|
|
261
265
|
* result is the same decode, just transformed once per query row
|
|
262
266
|
* instead of once per cached token. */
|
|
263
|
-
vRotated?: boolean
|
|
267
|
+
vRotated?: boolean;
|
|
268
|
+
restoreValues?: (output: MlxArray) => MlxArray }
|
|
264
269
|
| { kind: "quant"; keys: ops.QuantizedTensor; values: ops.QuantizedTensor;
|
|
265
|
-
offset: number; groupSize: number; bits: number; offsetArr?: MlxArray }
|
|
270
|
+
offset: number; groupSize: number; bits: number; offsetArr?: MlxArray }
|
|
271
|
+
| { kind: "view"; attention: KvAttentionView; offset: number; offsetArr?: MlxArray };
|
|
272
|
+
|
|
273
|
+
/** Retain an independent array handle for positions used across cache writes.
|
|
274
|
+
* The contiguous view preserves the integer values and shares their storage;
|
|
275
|
+
* callers release it with the fetched attention state. */
|
|
276
|
+
export function captureRopeOffsets(cache: Pick<Cache, "ropeOffsetArr">): MlxArray | undefined {
|
|
277
|
+
const offsets = cache.ropeOffsetArr;
|
|
278
|
+
return offsets ? ops.contiguous(offsets) : undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Quantized attention consumes a numerical storage port, independent of the
|
|
282
|
+
* concrete layout used for row positions, retention and persistence. */
|
|
283
|
+
export interface QuantizedAttentionState {
|
|
284
|
+
readonly groupSize: number;
|
|
285
|
+
readonly bits: number;
|
|
286
|
+
updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Values may stay in the codec's rotated domain until after attention. */
|
|
290
|
+
export interface RotatedValueAttentionState {
|
|
291
|
+
/** Capture a fetched value domain independently of later row changes. */
|
|
292
|
+
captureValueTransform?(): (output: MlxArray) => MlxArray;
|
|
293
|
+
updateAndFetchDeferredV(k: MlxArray, v: MlxArray): [MlxArray, MlxArray];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** A captured numerical view supports multiple queries without appending KV
|
|
297
|
+
* again. It owns its tensor handles independently of later cache membership or
|
|
298
|
+
* precision changes. Query/mask inputs are borrowed; outputs are owned. */
|
|
299
|
+
export interface KvAttentionView {
|
|
300
|
+
attend(q: MlxArray, scale: number, mask: Mask): MlxArray;
|
|
301
|
+
dispose(): void;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Storage appends once and hands every attention consumer the same view. */
|
|
305
|
+
export interface KvAttentionState {
|
|
306
|
+
appendAndFetch(k: MlxArray, v: MlxArray): KvAttentionView;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Owned read-only planes with validity separate from physical padding. */
|
|
310
|
+
export interface KvDonorRows {
|
|
311
|
+
readonly keys: MlxArray;
|
|
312
|
+
readonly values: MlxArray;
|
|
313
|
+
readonly offsets: readonly number[];
|
|
314
|
+
readonly starts: readonly number[];
|
|
315
|
+
readonly ends: readonly number[];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Captured Q-only attention hides plain or encoded donor storage. */
|
|
319
|
+
export interface KvDonorAttention extends KvAttentionView {
|
|
320
|
+
readonly width: number;
|
|
321
|
+
readonly dtype: Dtype;
|
|
322
|
+
readonly offsets: readonly number[];
|
|
323
|
+
readonly starts: readonly number[];
|
|
324
|
+
readonly ends: readonly number[];
|
|
325
|
+
}
|
|
266
326
|
|
|
267
327
|
export interface Cache {
|
|
328
|
+
captureDonorRows?(): KvDonorRows;
|
|
329
|
+
captureDonorAttention?(): KvDonorAttention;
|
|
330
|
+
/** A preparation method commits each request's own precision boundary.
|
|
331
|
+
* Splitting a forward for a sibling must not move that boundary. */
|
|
332
|
+
readonly prefillMaintenance?: {
|
|
333
|
+
beginPrefill(): void;
|
|
334
|
+
commitPrefill(rows: readonly number[]): void;
|
|
335
|
+
endPrefill(): void;
|
|
336
|
+
};
|
|
337
|
+
/** Optional representation-owned affine conversion. Logical position can
|
|
338
|
+
* differ from the physical write head in a padded rotating layout. */
|
|
339
|
+
readonly affineConversion?: { readonly offset: number; toQuantized(groupSize: number, bits: number): Cache };
|
|
340
|
+
readonly turboConversion?: { readonly offset: number; toTurboQuantized(kBits: number, vBits: number): Cache };
|
|
341
|
+
readonly attentionState?: KvAttentionState;
|
|
342
|
+
/** Earliest prefix whose retained representation can resume generation.
|
|
343
|
+
* Irreversible transitions may preserve bytes while invalidating older
|
|
344
|
+
* precision boundaries. Reuse policy must not trim below this offset. */
|
|
345
|
+
minimumReusableOffset?: number;
|
|
346
|
+
readonly rotatedValueAttention?: RotatedValueAttentionState;
|
|
347
|
+
readonly quantizedAttention?: QuantizedAttentionState;
|
|
268
348
|
offset: number;
|
|
269
349
|
/** Stable storage identity for compatibility guards and persistence.
|
|
270
350
|
* REQUIRED: every capability predicate (isPlainKvCache, isRotating*,
|
|
@@ -364,6 +444,19 @@ export interface BatchableCache extends Cache {
|
|
|
364
444
|
projectedBytes(tokens: number): number;
|
|
365
445
|
}
|
|
366
446
|
|
|
447
|
+
/** Padding is interpreted by cache geometry, independently of scheduling.
|
|
448
|
+
* Left padding initializes an empty group; lengths count input columns before
|
|
449
|
+
* trailing padding. Finalize before ordinary decode or publishing checkpoints. */
|
|
450
|
+
export interface PrefillPadding {
|
|
451
|
+
readonly leftPadding?: readonly number[];
|
|
452
|
+
readonly lengths: readonly number[];
|
|
453
|
+
readonly rightPadding?: readonly number[];
|
|
454
|
+
}
|
|
455
|
+
export interface PaddedPrefillCache extends Cache {
|
|
456
|
+
preparePrefill(padding: PrefillPadding): void;
|
|
457
|
+
finalizePrefill(): void;
|
|
458
|
+
}
|
|
459
|
+
|
|
367
460
|
export function isBatchableCache(cache: Cache): cache is BatchableCache {
|
|
368
461
|
const candidate = cache as Partial<BatchableCache>;
|
|
369
462
|
return (
|
|
@@ -487,6 +580,13 @@ export class KVCache implements Cache {
|
|
|
487
580
|
}
|
|
488
581
|
|
|
489
582
|
/** Chronological (K, V) view sliced to offset (drafter donor read). */
|
|
583
|
+
captureDonorRows(): KvDonorRows {
|
|
584
|
+
const [keys, values] = this.temporalView();
|
|
585
|
+
const B = keys.shape[0]!, width = keys.shape[2]!;
|
|
586
|
+
return { keys, values, offsets: Array(B).fill(this.offset),
|
|
587
|
+
starts: Array(B).fill(0), ends: Array(B).fill(width) };
|
|
588
|
+
}
|
|
589
|
+
|
|
490
590
|
temporalView(): [MlxArray, MlxArray] {
|
|
491
591
|
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
492
592
|
const [B, H, , D] = this.keys.shape as [number, number, number, number];
|
|
@@ -578,6 +678,7 @@ export class KVCache implements Cache {
|
|
|
578
678
|
* quantization is NYI upstream; sliding layers are window-capped
|
|
579
679
|
* anyway). Attention dispatches to quantizedSdpa for these. */
|
|
580
680
|
export class QuantizedKVCache implements Cache {
|
|
681
|
+
minimumReusableOffset?: number;
|
|
581
682
|
static readonly STEP = 256;
|
|
582
683
|
/** Set only by compiled-decode trace adapters (see Cache). */
|
|
583
684
|
readonly ropeOffsetArr?: MlxArray;
|
|
@@ -587,6 +688,8 @@ export class QuantizedKVCache implements Cache {
|
|
|
587
688
|
|
|
588
689
|
constructor(readonly groupSize: number, readonly bits: number) {}
|
|
589
690
|
|
|
691
|
+
get quantizedAttention(): QuantizedAttentionState { return this; }
|
|
692
|
+
|
|
590
693
|
signature(): string { return `kv:quant:${this.bits}:${this.groupSize}`; }
|
|
591
694
|
|
|
592
695
|
bytesPerToken(): number {
|
|
@@ -774,6 +877,16 @@ export class QuantizedKVCache implements Cache {
|
|
|
774
877
|
/** Chronological (K, V) triples sliced to offset — the quantized twin of
|
|
775
878
|
* KVCache.temporalView (batched merge/extend/filter reads). Caller owns
|
|
776
879
|
* the returned views. */
|
|
880
|
+
captureDonorAttention(): KvDonorAttention {
|
|
881
|
+
const [keys, values] = this.temporalView();
|
|
882
|
+
const B = keys.packed.shape[0]!, width = keys.packed.shape[2]!;
|
|
883
|
+
return { width, dtype: keys.scales.dtype, offsets: Array(B).fill(this.offset),
|
|
884
|
+
starts: Array(B).fill(0), ends: Array(B).fill(width),
|
|
885
|
+
attend: (q, scale, mask) => quantizedSdpa(q, keys, values, scale, mask, this.groupSize, this.bits),
|
|
886
|
+
dispose() { disposeTriple(keys); disposeTriple(values); },
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
777
890
|
temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
778
891
|
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
779
892
|
const cut = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
@@ -798,6 +911,7 @@ export class QuantizedKVCache implements Cache {
|
|
|
798
911
|
* entries, so decode attends over at most the window. RoPE offsets use
|
|
799
912
|
* the true position; masks use the buffer-clamped offset. */
|
|
800
913
|
export class RotatingKVCache implements Cache {
|
|
914
|
+
declare minimumReusableOffset?: number;
|
|
801
915
|
static readonly STEP = 256;
|
|
802
916
|
/** Set only by compiled-decode trace adapters (see Cache). */
|
|
803
917
|
readonly ropeOffsetArr?: MlxArray;
|
|
@@ -1065,6 +1179,13 @@ export class RotatingKVCache implements Cache {
|
|
|
1065
1179
|
|
|
1066
1180
|
/** Chronological (K, V) view, valid length min(offset, maxSize)
|
|
1067
1181
|
* (port of optiq kv_view._read_cache_temporal). */
|
|
1182
|
+
captureDonorRows(): KvDonorRows {
|
|
1183
|
+
const [keys, values] = this.temporalView();
|
|
1184
|
+
const B = keys.shape[0]!, width = keys.shape[2]!;
|
|
1185
|
+
return { keys, values, offsets: Array(B).fill(this.offset),
|
|
1186
|
+
starts: Array(B).fill(0), ends: Array(B).fill(width) };
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1068
1189
|
temporalView(): [MlxArray, MlxArray] {
|
|
1069
1190
|
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
1070
1191
|
const tk = this.#temporalOrder(this.keys);
|
|
@@ -1133,6 +1254,8 @@ export const disposeTriple = (t: ops.QuantizedTensor): void => {
|
|
|
1133
1254
|
* + SDPA patches are unnecessary here: our SharedKv carries
|
|
1134
1255
|
* groupSize/bits through the donor→sharer plumbing explicitly. */
|
|
1135
1256
|
export class RotatingQuantizedKVCache implements Cache {
|
|
1257
|
+
get quantizedAttention(): QuantizedAttentionState { return this; }
|
|
1258
|
+
declare minimumReusableOffset?: number;
|
|
1136
1259
|
static readonly STEP = 256;
|
|
1137
1260
|
/** Set only by compiled-decode trace adapters (see Cache). */
|
|
1138
1261
|
readonly ropeOffsetArr?: MlxArray;
|
|
@@ -1390,6 +1513,16 @@ export class RotatingQuantizedKVCache implements Cache {
|
|
|
1390
1513
|
/** Chronological (K, V) triples cut to the valid window — the quantized
|
|
1391
1514
|
* twin of RotatingKVCache.temporalView (batched merge reads, Phase 3
|
|
1392
1515
|
* milestone 2). Caller owns the returned views. */
|
|
1516
|
+
captureDonorAttention(): KvDonorAttention {
|
|
1517
|
+
const [keys, values] = this.temporalView();
|
|
1518
|
+
const B = keys.packed.shape[0]!, width = keys.packed.shape[2]!;
|
|
1519
|
+
return { width, dtype: keys.scales.dtype, offsets: Array(B).fill(this.offset),
|
|
1520
|
+
starts: Array(B).fill(0), ends: Array(B).fill(width),
|
|
1521
|
+
attend: (q, scale, mask) => quantizedSdpa(q, keys, values, scale, mask, this.groupSize, this.bits),
|
|
1522
|
+
dispose() { disposeTriple(keys); disposeTriple(values); },
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1393
1526
|
temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
1394
1527
|
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
1395
1528
|
const valid = Math.min(this.offset, this.maxSize);
|
|
@@ -1504,28 +1637,6 @@ export class RotatingQuantizedKVCache implements Cache {
|
|
|
1504
1637
|
}
|
|
1505
1638
|
}
|
|
1506
1639
|
|
|
1507
|
-
/** One TurboQuant-encoded (K, V) storage tuple — the 5 arrays kv-store.ts
|
|
1508
|
-
* and toQuantized/fromKVCache pass around. Not `ops.QuantizedTensor`
|
|
1509
|
-
* (mlx's affine int4/int8 scheme): this is the rotation + Lloyd-Max
|
|
1510
|
-
* layout (docs/design/turboquant.md), asymmetric-affine for keys,
|
|
1511
|
-
* FWHT+Lloyd-Max for values. Field order is the kv-store.ts tensor-slot
|
|
1512
|
-
* contract — do not reorder without updating snapshotCache/loadKvCache. */
|
|
1513
|
-
export interface TurboQuantTensor {
|
|
1514
|
-
kIdx: MlxArray;
|
|
1515
|
-
kScales: MlxArray;
|
|
1516
|
-
kZeros: MlxArray;
|
|
1517
|
-
vPacked: MlxArray;
|
|
1518
|
-
vScales: MlxArray;
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
export const disposeTurboQuant = (t: TurboQuantTensor): void => {
|
|
1522
|
-
t.kIdx.dispose();
|
|
1523
|
-
t.kScales.dispose();
|
|
1524
|
-
t.kZeros.dispose();
|
|
1525
|
-
t.vPacked.dispose();
|
|
1526
|
-
t.vScales.dispose();
|
|
1527
|
-
};
|
|
1528
|
-
|
|
1529
1640
|
/** TurboQuant KV cache — v1 (docs/design/turboquant.md): dequantize-
|
|
1530
1641
|
* on-fetch. Deliberately does NOT subclass KVCache/RotatingKVCache (or
|
|
1531
1642
|
* QuantizedKVCache) — a novel class fails every generated-file
|
|
@@ -1545,6 +1656,7 @@ export const disposeTurboQuant = (t: TurboQuantTensor): void => {
|
|
|
1545
1656
|
* a full-window dequant every step; the deferred-InvFWHT trick is a
|
|
1546
1657
|
* documented non-goal until the quality gate passes. */
|
|
1547
1658
|
export class TurboQuantKVCache implements Cache {
|
|
1659
|
+
minimumReusableOffset = 0;
|
|
1548
1660
|
readonly stateNeedsDispose = true;
|
|
1549
1661
|
static readonly STEP = 256;
|
|
1550
1662
|
/** Set only by compiled-decode trace adapters (see Cache) — TurboQuant
|
|
@@ -1557,10 +1669,22 @@ export class TurboQuantKVCache implements Cache {
|
|
|
1557
1669
|
/** Validated lazily on first update, once head_dim is known. */
|
|
1558
1670
|
#headDim: number | null = null;
|
|
1559
1671
|
/** Experimental operation selection, captured once for this cache. */
|
|
1560
|
-
readonly #
|
|
1672
|
+
readonly #codec: KvCodec<TurboQuantTensor>;
|
|
1673
|
+
|
|
1674
|
+
constructor(readonly kBits: number, readonly vBits: number) {
|
|
1675
|
+
this.#codec = new TurboQuantCodec(kBits, vBits, process.env.MLX_BUN_TURBOQUANT_FUSED_DECODE === "1");
|
|
1676
|
+
}
|
|
1561
1677
|
|
|
1562
|
-
|
|
1678
|
+
captureDonorRows(): KvDonorRows {
|
|
1679
|
+
if (!this.#kv || this.#headDim === null) throw new Error("cache is empty");
|
|
1680
|
+
const [keys, values] = this.#decode(this.#kv, this.offset, this.#headDim, false);
|
|
1681
|
+
const B = keys.shape[0]!;
|
|
1682
|
+
return { keys, values, offsets: Array(B).fill(this.offset),
|
|
1683
|
+
starts: Array(B).fill(0), ends: Array(B).fill(this.offset) };
|
|
1684
|
+
}
|
|
1685
|
+
captureDonorAttention(): KvDonorAttention { return decodedKvDonorAttention(this.captureDonorRows()); }
|
|
1563
1686
|
|
|
1687
|
+
get rotatedValueAttention(): RotatedValueAttentionState { return this; }
|
|
1564
1688
|
signature(): string { return `kv:turboquant:${this.kBits}:${this.vBits}`; }
|
|
1565
1689
|
|
|
1566
1690
|
bytesPerToken(): number {
|
|
@@ -1612,55 +1736,9 @@ export class TurboQuantKVCache implements Cache {
|
|
|
1612
1736
|
return this.#kv ? this.#kv.kIdx.shape[2]! : 0;
|
|
1613
1737
|
}
|
|
1614
1738
|
|
|
1615
|
-
|
|
1616
|
-
#encode(k: MlxArray, v: MlxArray): TurboQuantTensor {
|
|
1617
|
-
const signed = this.kBits === 8;
|
|
1618
|
-
const kEnc = tq.encodeKeys(k, this.kBits, signed);
|
|
1619
|
-
const kIdxPacked = this.kBits < 8 ? tq.packBits(kEnc.indices, this.kBits) : kEnc.indices;
|
|
1620
|
-
if (kIdxPacked !== kEnc.indices) kEnc.indices.dispose();
|
|
1621
|
-
const vEnc = tq.encodeValues(v, this.vBits);
|
|
1622
|
-
const vIdxPacked = this.vBits < 8 ? tq.packBits(vEnc.indices, this.vBits) : vEnc.indices;
|
|
1623
|
-
if (vIdxPacked !== vEnc.indices) vEnc.indices.dispose();
|
|
1624
|
-
return {
|
|
1625
|
-
kIdx: kIdxPacked, kScales: kEnc.scales, kZeros: kEnc.zeros,
|
|
1626
|
-
vPacked: vIdxPacked, vScales: vEnc.scales,
|
|
1627
|
-
};
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
/** Decode a TurboQuantTensor's active [B,H,upTo,*] window back to
|
|
1631
|
-
* bf16 [B,H,upTo,headDim] (k, v). Caller owns the returned arrays.
|
|
1632
|
-
* deferV leaves V in the rotated domain (decodeValuesRotated) — the
|
|
1633
|
-
* caller un-rotates its attention output instead (tq.unrotateValues). */
|
|
1739
|
+
#encode(k: MlxArray, v: MlxArray): TurboQuantTensor { return this.#codec.encode(k, v); }
|
|
1634
1740
|
#decode(t: TurboQuantTensor, upTo: number, headDim: number, deferV = false): [MlxArray, MlxArray] {
|
|
1635
|
-
|
|
1636
|
-
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1637
|
-
return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
|
|
1638
|
-
};
|
|
1639
|
-
if (this.#fusedDecode) {
|
|
1640
|
-
const inputs = [cut(t.kIdx), cut(t.kScales), cut(t.kZeros), cut(t.vPacked), cut(t.vScales)] as const;
|
|
1641
|
-
try {
|
|
1642
|
-
const decoded = tq.tryDecodePackedKv(inputs, this.kBits, this.vBits, headDim, deferV);
|
|
1643
|
-
if (decoded) return decoded;
|
|
1644
|
-
} finally { for (const input of inputs) input.dispose(); }
|
|
1645
|
-
}
|
|
1646
|
-
const kIdxCut = cut(t.kIdx);
|
|
1647
|
-
const kIdxUnpacked = this.kBits < 8 ? tq.unpackBits(kIdxCut, this.kBits, headDim) : kIdxCut;
|
|
1648
|
-
if (kIdxUnpacked !== kIdxCut) kIdxCut.dispose();
|
|
1649
|
-
const kScalesCut = cut(t.kScales);
|
|
1650
|
-
const kZerosCut = cut(t.kZeros);
|
|
1651
|
-
const k = tq.decodeKeys(kIdxUnpacked, kScalesCut, kZerosCut);
|
|
1652
|
-
for (const a of [kIdxUnpacked, kScalesCut, kZerosCut]) a.dispose();
|
|
1653
|
-
|
|
1654
|
-
const vPackedCut = cut(t.vPacked);
|
|
1655
|
-
const vIdxUnpacked = this.vBits < 8 ? tq.unpackBits(vPackedCut, this.vBits, headDim) : vPackedCut;
|
|
1656
|
-
if (vIdxUnpacked !== vPackedCut) vPackedCut.dispose();
|
|
1657
|
-
const vScalesCut = cut(t.vScales);
|
|
1658
|
-
const v = deferV
|
|
1659
|
-
? tq.decodeValuesRotated(vIdxUnpacked, vScalesCut, this.vBits)
|
|
1660
|
-
: tq.decodeValues(vIdxUnpacked, vScalesCut, this.vBits);
|
|
1661
|
-
for (const a of [vIdxUnpacked, vScalesCut]) a.dispose();
|
|
1662
|
-
|
|
1663
|
-
return [k, v];
|
|
1741
|
+
return this.#codec.decode(t, upTo, headDim, deferV);
|
|
1664
1742
|
}
|
|
1665
1743
|
|
|
1666
1744
|
/** Quantize the newly-appended k/v, append into growable packed
|
|
@@ -1805,6 +1883,7 @@ export class TurboQuantKVCache implements Cache {
|
|
|
1805
1883
|
q.#headDim = D;
|
|
1806
1884
|
}
|
|
1807
1885
|
q.offset = cache.offset;
|
|
1886
|
+
q.minimumReusableOffset = (cache as Cache).minimumReusableOffset ?? 0;
|
|
1808
1887
|
cache.dispose();
|
|
1809
1888
|
return q;
|
|
1810
1889
|
}
|
|
@@ -1813,6 +1892,7 @@ export class TurboQuantKVCache implements Cache {
|
|
|
1813
1892
|
if (this.#kv) disposeTurboQuant(this.#kv);
|
|
1814
1893
|
this.#kv = null;
|
|
1815
1894
|
this.offset = 0;
|
|
1895
|
+
this.minimumReusableOffset = 0;
|
|
1816
1896
|
}
|
|
1817
1897
|
}
|
|
1818
1898
|
|
|
@@ -1885,9 +1965,11 @@ export function quantizedSdpaUnfused(
|
|
|
1885
1965
|
}
|
|
1886
1966
|
}
|
|
1887
1967
|
if (maskArr) {
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1968
|
+
let masked: MlxArray;
|
|
1969
|
+
if (maskArr.dtype === Dtype.bool) {
|
|
1970
|
+
using ninf = ops.scalarLike(FINFO_MIN[scores.dtype] ?? -3.4e38, scores);
|
|
1971
|
+
masked = ops.where(maskArr, scores, ninf);
|
|
1972
|
+
} else masked = ops.add(scores, maskArr);
|
|
1891
1973
|
if (ownsMask) maskArr.dispose();
|
|
1892
1974
|
owned.push(masked);
|
|
1893
1975
|
scores = masked;
|
package/src/model/gemma4.ts
CHANGED
|
@@ -45,11 +45,11 @@ import {
|
|
|
45
45
|
RMSNorm,
|
|
46
46
|
RotatingKVCache,
|
|
47
47
|
RotatingQuantizedKVCache,
|
|
48
|
-
TurboQuantKVCache,
|
|
49
48
|
quantizedSdpa,
|
|
50
49
|
type Cache,
|
|
51
50
|
type Mask,
|
|
52
51
|
type SharedKv,
|
|
52
|
+
captureRopeOffsets,
|
|
53
53
|
type LoraWeights,
|
|
54
54
|
} from "./gemma4-base";
|
|
55
55
|
import { Checkpoint } from "../mlx/checkpoint";
|
|
@@ -304,15 +304,11 @@ class Attention {
|
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
const offset = cache.offset;
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
// prerequisite for batched decode, where ropeOffsetArr carries per-row
|
|
313
|
-
// positions derived from the pre-write offset and MUST be identical for
|
|
314
|
-
// this step's K and Q.
|
|
315
|
-
const offsetArr = cache.ropeOffsetArr;
|
|
307
|
+
// Own the pre-write positions through every query and shared-KV
|
|
308
|
+
// consumer. Advancing a row cache may replace and release its borrowed
|
|
309
|
+
// offset array before Q's RoPE is built. The fetched shared state owns
|
|
310
|
+
// this retained handle and releases it with K/V at the end of the pass.
|
|
311
|
+
const offsetArr = captureRopeOffsets(cache);
|
|
316
312
|
|
|
317
313
|
const kNormed = this.kNorm!.forward(k);
|
|
318
314
|
const kT = ops.transposeAxes(kNormed, [0, 2, 1, 3]);
|
|
@@ -328,23 +324,29 @@ class Attention {
|
|
|
328
324
|
if (v !== k) v.dispose();
|
|
329
325
|
k.dispose();
|
|
330
326
|
|
|
331
|
-
if (cache
|
|
332
|
-
const
|
|
327
|
+
if (cache.attentionState) {
|
|
328
|
+
const attention = cache.attentionState.appendAndFetch(kRoped, vT);
|
|
329
|
+
kRoped.dispose(); vT.dispose();
|
|
330
|
+
shared = { kind: "view", attention, offset, offsetArr };
|
|
331
|
+
} else if (cache.quantizedAttention) {
|
|
332
|
+
const quantized = cache.quantizedAttention;
|
|
333
|
+
const [kq, vq] = quantized.updateAndFetchQuantized(kRoped, vT);
|
|
333
334
|
kRoped.dispose();
|
|
334
335
|
vT.dispose();
|
|
335
336
|
shared = {
|
|
336
337
|
kind: "quant", keys: kq, values: vq, offset,
|
|
337
|
-
groupSize:
|
|
338
|
+
groupSize: quantized.groupSize, bits: quantized.bits,
|
|
338
339
|
offsetArr,
|
|
339
340
|
};
|
|
340
|
-
} else if (cache
|
|
341
|
+
} else if (cache.rotatedValueAttention) {
|
|
341
342
|
// Deferred-V read: values stay rotated; the attention output is
|
|
342
343
|
// un-rotated below (shared.vRotated) — including by KV-shared
|
|
343
344
|
// consumer layers, which see the flag through sharedIn.
|
|
344
|
-
const [keys, values] = cache.updateAndFetchDeferredV(kRoped, vT);
|
|
345
|
+
const [keys, values] = cache.rotatedValueAttention.updateAndFetchDeferredV(kRoped, vT);
|
|
345
346
|
kRoped.dispose();
|
|
346
347
|
vT.dispose();
|
|
347
|
-
shared = { kind: "plain", keys, values, offset, offsetArr, vRotated: true
|
|
348
|
+
shared = { kind: "plain", keys, values, offset, offsetArr, vRotated: true,
|
|
349
|
+
restoreValues: cache.rotatedValueAttention.captureValueTransform?.() };
|
|
348
350
|
} else {
|
|
349
351
|
const [keys, values] = cache.updateAndFetch(kRoped, vT);
|
|
350
352
|
kRoped.dispose();
|
|
@@ -360,7 +362,9 @@ class Attention {
|
|
|
360
362
|
|
|
361
363
|
let attn: MlxArray;
|
|
362
364
|
const ta = getTrainingAttn();
|
|
363
|
-
if (shared.kind === "
|
|
365
|
+
if (shared.kind === "view") {
|
|
366
|
+
attn = shared.attention.attend(q, 1.0, mask);
|
|
367
|
+
} else if (shared.kind === "quant") {
|
|
364
368
|
attn = quantizedSdpa(q, shared.keys, shared.values, 1.0, mask, shared.groupSize, shared.bits);
|
|
365
369
|
} else if (
|
|
366
370
|
ta === "flash" && flashSupported(q) && (mask.mode === "causal" || mask.mode === "array")
|
|
@@ -383,7 +387,7 @@ class Attention {
|
|
|
383
387
|
if (shared.kind === "plain" && shared.vRotated) {
|
|
384
388
|
// Attention is linear in V: one InvFWHT on the [B,H,L,D] output
|
|
385
389
|
// replaces one per cached token (TurboQuant deferred-V).
|
|
386
|
-
attn = disposing(attn, tqUnrotateValues(attn));
|
|
390
|
+
attn = disposing(attn, shared.restoreValues ? shared.restoreValues(attn) : tqUnrotateValues(attn));
|
|
387
391
|
}
|
|
388
392
|
const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
|
|
389
393
|
attn.dispose();
|
|
@@ -851,6 +855,13 @@ export class Gemma4Model {
|
|
|
851
855
|
);
|
|
852
856
|
}
|
|
853
857
|
|
|
858
|
+
/** Computed model constants must retain their constructor arithmetic when
|
|
859
|
+
* a restored request enters compiled decode without a prefill first. */
|
|
860
|
+
materializeGraphConstants(): void {
|
|
861
|
+
const constants = this.layers.flatMap(layer => layer.attn.ropeFreqs ? [layer.attn.ropeFreqs] : []);
|
|
862
|
+
if (constants.length) ops.evalAll(constants);
|
|
863
|
+
}
|
|
864
|
+
|
|
854
865
|
/** ids [1, L] → final-norm hidden states [1, L, hidden]. */
|
|
855
866
|
forwardHidden(ids: MlxArray, cache: Cache[]): MlxArray {
|
|
856
867
|
let h = this.embed.encode(ids);
|
|
@@ -1000,13 +1011,7 @@ export class Gemma4Model {
|
|
|
1000
1011
|
for (let i = 0; i < this.numDonors; i++) {
|
|
1001
1012
|
const s = intermediates[i];
|
|
1002
1013
|
if (!s) continue;
|
|
1003
|
-
|
|
1004
|
-
s.keys.dispose();
|
|
1005
|
-
s.values.dispose();
|
|
1006
|
-
} else {
|
|
1007
|
-
for (const t of [s.keys, s.values])
|
|
1008
|
-
for (const a of [t.packed, t.scales, t.biases]) a.dispose();
|
|
1009
|
-
}
|
|
1014
|
+
Gemma4Model.disposeSharedKv(s);
|
|
1010
1015
|
}
|
|
1011
1016
|
for (const m of masks.values()) m.arr?.dispose();
|
|
1012
1017
|
|
|
@@ -1056,7 +1061,10 @@ export class Gemma4Model {
|
|
|
1056
1061
|
/** Dispose a fetched donor K/V (plain or quantized) — mirrors forwardLayers'
|
|
1057
1062
|
* donor cleanup. */
|
|
1058
1063
|
private static disposeSharedKv(s: SharedKv): void {
|
|
1059
|
-
|
|
1064
|
+
s.offsetArr?.dispose();
|
|
1065
|
+
if (s.kind === "view") {
|
|
1066
|
+
s.attention.dispose();
|
|
1067
|
+
} else if (s.kind === "plain") {
|
|
1060
1068
|
s.keys.dispose();
|
|
1061
1069
|
s.values.dispose();
|
|
1062
1070
|
} else {
|
|
@@ -34,7 +34,10 @@ export const FINGERPRINT = "9f812d2eb461fcbe";
|
|
|
34
34
|
export let generatedForwardUses = 0;
|
|
35
35
|
|
|
36
36
|
function disposeSharedKv(s: SharedKv): void {
|
|
37
|
-
if (s.kind === "
|
|
37
|
+
if (s.kind === "view") {
|
|
38
|
+
s.attention.dispose();
|
|
39
|
+
s.offsetArr?.dispose();
|
|
40
|
+
} else if (s.kind === "plain") {
|
|
38
41
|
s.keys.dispose();
|
|
39
42
|
s.values.dispose();
|
|
40
43
|
} else {
|
|
@@ -34,7 +34,10 @@ export const FINGERPRINT = "c9dd67ed5a525231";
|
|
|
34
34
|
export let generatedForwardUses = 0;
|
|
35
35
|
|
|
36
36
|
function disposeSharedKv(s: SharedKv): void {
|
|
37
|
-
if (s.kind === "
|
|
37
|
+
if (s.kind === "view") {
|
|
38
|
+
s.attention.dispose();
|
|
39
|
+
s.offsetArr?.dispose();
|
|
40
|
+
} else if (s.kind === "plain") {
|
|
38
41
|
s.keys.dispose();
|
|
39
42
|
s.values.dispose();
|
|
40
43
|
} else {
|
|
@@ -35,7 +35,10 @@ export const FINGERPRINT = "418e9adc386ea67c";
|
|
|
35
35
|
export let generatedForwardUses = 0;
|
|
36
36
|
|
|
37
37
|
function disposeSharedKv(s: SharedKv): void {
|
|
38
|
-
if (s.kind === "
|
|
38
|
+
if (s.kind === "view") {
|
|
39
|
+
s.attention.dispose();
|
|
40
|
+
s.offsetArr?.dispose();
|
|
41
|
+
} else if (s.kind === "plain") {
|
|
39
42
|
s.keys.dispose();
|
|
40
43
|
s.values.dispose();
|
|
41
44
|
} else {
|