mlx-bun 0.0.10 → 0.0.11
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 +66 -21
- package/THIRD_PARTY_LICENSES.md +1 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +58 -35
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +14 -6
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +3 -2
- package/docs/design/ssd-kv-cold-tier.md +130 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/reference/cli.md +35 -16
- package/docs/reference/distribution.md +13 -6
- package/docs/reference/embedding.md +35 -14
- package/docs/reference/features-matrix.md +97 -0
- package/docs/reference/library-api.md +30 -9
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +11 -1
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +634 -22
- package/docs/reference/server-config.md +250 -121
- package/docs/reference/training.md +14 -15
- package/package.json +5 -3
- package/src/anthropic.ts +13 -0
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/cli.ts +356 -105
- package/src/config.ts +49 -2
- package/src/download.ts +40 -0
- package/src/eval/README.md +7 -6
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +3 -3
- package/src/expert-offload.ts +15 -2
- package/src/flags.ts +13 -0
- package/src/generate.ts +330 -24
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +442 -88
- package/src/lora.ts +8 -0
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +284 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +106 -19
- package/src/mlx/autograd.ts +2 -2
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +153 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +10 -4
- package/src/model/gemma4-base.ts +363 -39
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +76 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +88 -23
- package/src/model/support.ts +12 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +293 -33
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +57 -2
- package/src/serve/batch-scheduler.ts +965 -72
- package/src/serve/generation-gateway.ts +214 -11
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1380 -135
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +536 -0
- package/src/spec/source.ts +93 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +107 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +408 -20
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -2
- package/src/train/trainer.ts +1 -4
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1184 -1789
- package/src/web/app.js +5667 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +252 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
package/src/model/gemma4-base.ts
CHANGED
|
@@ -14,9 +14,7 @@ import { MlxArray } from "../mlx/array";
|
|
|
14
14
|
import { Dtype } from "../mlx/ffi";
|
|
15
15
|
import * as ops from "../mlx/ops";
|
|
16
16
|
import { expertOffloadArray } from "../expert-offload";
|
|
17
|
-
import
|
|
18
|
-
fusedDecodeKernelSupported, fusedDecodeSdpa, perfKernelEnabled,
|
|
19
|
-
} from "./fused-decode-kernel";
|
|
17
|
+
import * as tq from "../mlx/turboquant-ops";
|
|
20
18
|
|
|
21
19
|
export type MaskMode = "" | "causal";
|
|
22
20
|
export interface Mask {
|
|
@@ -206,7 +204,13 @@ export class QuantizedEmbedding {
|
|
|
206
204
|
* position (see src/model/compiled-decode.ts). */
|
|
207
205
|
export type SharedKv =
|
|
208
206
|
| { kind: "plain"; keys: MlxArray; values: MlxArray; offset: number;
|
|
209
|
-
offsetArr?: MlxArray
|
|
207
|
+
offsetArr?: MlxArray;
|
|
208
|
+
/** TurboQuant deferred-V: `values` are still in the rotated (FWHT)
|
|
209
|
+
* domain — every consumer must un-rotate its attention OUTPUT
|
|
210
|
+
* (tq.unrotateValues) after sdpa. Attention is linear in V, so the
|
|
211
|
+
* result is the same decode, just transformed once per query row
|
|
212
|
+
* instead of once per cached token. */
|
|
213
|
+
vRotated?: boolean }
|
|
210
214
|
| { kind: "quant"; keys: ops.QuantizedTensor; values: ops.QuantizedTensor;
|
|
211
215
|
offset: number; groupSize: number; bits: number; offsetArr?: MlxArray };
|
|
212
216
|
|
|
@@ -306,9 +310,16 @@ export class KVCache implements Cache {
|
|
|
306
310
|
|
|
307
311
|
makeMask(N: number, windowSize: number | null): Mask {
|
|
308
312
|
if (N === 1) return { mode: "", arr: null };
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
313
|
+
// Windowless multi-token chunks get the STRING "causal" at ANY offset —
|
|
314
|
+
// mlx-lm cache.py:114-125 create_attention_mask does exactly this, and
|
|
315
|
+
// mx.fast SDPA aligns the string bottom-right, i.e. the same values as
|
|
316
|
+
// our materialized matrix. Shipping the array instead (the old
|
|
317
|
+
// offset>0 branch) forced a materialize-and-add mask path: a bool
|
|
318
|
+
// [N, offset+N] built PER CHUNK and read by every full-attention layer
|
|
319
|
+
// — the e4b 16k prefill gap vs mlx-lm (2026-07-06b, hd=512 layers
|
|
320
|
+
// where the fallback dispatch can't block-skip an array mask).
|
|
321
|
+
if (windowSize === null) return { mode: "causal", arr: null };
|
|
322
|
+
if (this.offset === 0 && N <= windowSize) return { mode: "causal", arr: null };
|
|
312
323
|
return { mode: "array", arr: createCausalMask(N, this.offset, windowSize) };
|
|
313
324
|
}
|
|
314
325
|
|
|
@@ -508,14 +519,13 @@ export class QuantizedKVCache implements Cache {
|
|
|
508
519
|
|
|
509
520
|
makeMask(N: number, windowSize: number | null): Mask {
|
|
510
521
|
if (N === 1) return { mode: "", arr: null };
|
|
511
|
-
|
|
522
|
+
// Same rule as KVCache.makeMask (mlx-lm cache.py:114-125): windowless
|
|
523
|
+
// multi-token chunks are the string "causal" at any offset. The old
|
|
524
|
+
// causalEquivalent escape hatch existed precisely because this case
|
|
525
|
+
// used to ship an array; only windowed continuations materialize now.
|
|
526
|
+
if (windowSize === null || (this.offset === 0 && N <= windowSize))
|
|
512
527
|
return { mode: "causal", arr: null };
|
|
513
|
-
return {
|
|
514
|
-
mode: "array",
|
|
515
|
-
arr: createCausalMask(N, this.offset, windowSize),
|
|
516
|
-
// a windowless continuation mask is exactly mlx-lm's "causal"
|
|
517
|
-
causalEquivalent: windowSize === null,
|
|
518
|
-
};
|
|
528
|
+
return { mode: "array", arr: createCausalMask(N, this.offset, windowSize) };
|
|
519
529
|
}
|
|
520
530
|
|
|
521
531
|
/** Compiled decode: growth half of updateAndFetchQuantized (L=1),
|
|
@@ -599,6 +609,21 @@ export class QuantizedKVCache implements Cache {
|
|
|
599
609
|
this.offset = offset;
|
|
600
610
|
}
|
|
601
611
|
|
|
612
|
+
/** Chronological (K, V) triples sliced to offset — the quantized twin of
|
|
613
|
+
* KVCache.temporalView (batched merge/extend/filter reads). Caller owns
|
|
614
|
+
* the returned views. */
|
|
615
|
+
temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
616
|
+
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
617
|
+
const cut = (t: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
618
|
+
const f = (a: MlxArray): MlxArray => {
|
|
619
|
+
const [b, h, , d] = a.shape as [number, number, number, number];
|
|
620
|
+
return a.slice([0, 0, 0, 0], [b, h, this.offset, d]);
|
|
621
|
+
};
|
|
622
|
+
return { packed: f(t.packed), scales: f(t.scales), biases: f(t.biases) };
|
|
623
|
+
};
|
|
624
|
+
return [cut(this.keys), cut(this.values)];
|
|
625
|
+
}
|
|
626
|
+
|
|
602
627
|
dispose(): void {
|
|
603
628
|
for (const a of this.state()) a.dispose();
|
|
604
629
|
this.keys = this.values = null;
|
|
@@ -1181,6 +1206,25 @@ export class RotatingQuantizedKVCache implements Cache {
|
|
|
1181
1206
|
this.ringIdx = idx;
|
|
1182
1207
|
}
|
|
1183
1208
|
|
|
1209
|
+
/** Chronological (K, V) triples cut to the valid window — the quantized
|
|
1210
|
+
* twin of RotatingKVCache.temporalView (batched merge reads, Phase 3
|
|
1211
|
+
* milestone 2). Caller owns the returned views. */
|
|
1212
|
+
temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
1213
|
+
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
1214
|
+
const valid = Math.min(this.offset, this.maxSize);
|
|
1215
|
+
const cutValid = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
|
|
1216
|
+
mapTriple(t, (a) => {
|
|
1217
|
+
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1218
|
+
return a.slice([0, 0, 0, 0], [B, H, valid, D]);
|
|
1219
|
+
});
|
|
1220
|
+
const tk = this.#temporalOrder(this.keys);
|
|
1221
|
+
const tv = this.#temporalOrder(this.values);
|
|
1222
|
+
const out: [ops.QuantizedTensor, ops.QuantizedTensor] = [cutValid(tk), cutValid(tv)];
|
|
1223
|
+
disposeTriple(tk);
|
|
1224
|
+
disposeTriple(tv);
|
|
1225
|
+
return out;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1184
1228
|
/** Oracle: to_quantized on an already-quantized rotating cache is
|
|
1185
1229
|
* idempotent. */
|
|
1186
1230
|
toQuantized(): RotatingQuantizedKVCache {
|
|
@@ -1279,6 +1323,297 @@ export class RotatingQuantizedKVCache implements Cache {
|
|
|
1279
1323
|
}
|
|
1280
1324
|
}
|
|
1281
1325
|
|
|
1326
|
+
/** One TurboQuant-encoded (K, V) storage tuple — the 5 arrays kv-store.ts
|
|
1327
|
+
* and toQuantized/fromKVCache pass around. Not `ops.QuantizedTensor`
|
|
1328
|
+
* (mlx's affine int4/int8 scheme): this is the rotation + Lloyd-Max
|
|
1329
|
+
* layout (docs/design/turboquant-kv.md), asymmetric-affine for keys,
|
|
1330
|
+
* FWHT+Lloyd-Max for values. Field order is the kv-store.ts tensor-slot
|
|
1331
|
+
* contract — do not reorder without updating snapshotCache/loadKvCache. */
|
|
1332
|
+
export interface TurboQuantTensor {
|
|
1333
|
+
kIdx: MlxArray;
|
|
1334
|
+
kScales: MlxArray;
|
|
1335
|
+
kZeros: MlxArray;
|
|
1336
|
+
vPacked: MlxArray;
|
|
1337
|
+
vScales: MlxArray;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
export const disposeTurboQuant = (t: TurboQuantTensor): void => {
|
|
1341
|
+
t.kIdx.dispose();
|
|
1342
|
+
t.kScales.dispose();
|
|
1343
|
+
t.kZeros.dispose();
|
|
1344
|
+
t.vPacked.dispose();
|
|
1345
|
+
t.vScales.dispose();
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
/** TurboQuant KV cache — v1 (docs/design/turboquant-kv.md): dequantize-
|
|
1349
|
+
* on-fetch. Deliberately does NOT subclass KVCache/RotatingKVCache (or
|
|
1350
|
+
* QuantizedKVCache) — a novel class fails every generated-file
|
|
1351
|
+
* `#matches()` guard and every batching `instanceof` allow-list, so it
|
|
1352
|
+
* gets solo-only routing and monolith fallback for free, the same way
|
|
1353
|
+
* QuantizedKVCache does today. Only full-attention (non-rotating)
|
|
1354
|
+
* layers use this in v1; sliding-window layers stay bf16.
|
|
1355
|
+
*
|
|
1356
|
+
* Storage: 5 arrays per the vllm-metal reference's `turbo_quant_encode`
|
|
1357
|
+
* tuple — kIdx (int8 unpacked at kBits=8, else bit-packed uint8),
|
|
1358
|
+
* kScales/kZeros (fp16, one pair per 32-group), vPacked (bit-packed
|
|
1359
|
+
* uint8, no-op at vBits=8), vScales (fp16, one per 32-group).
|
|
1360
|
+
* updateAndFetch quantizes ONLY the newly appended tokens (unlike
|
|
1361
|
+
* QuantizedKVCache's whole-buffer-toQuantized conversion path — the
|
|
1362
|
+
* streaming append is the whole point of a live cache) and returns the
|
|
1363
|
+
* DEQUANTIZED bf16 active window so ops.sdpa runs unmodified — v1 pays
|
|
1364
|
+
* a full-window dequant every step; the deferred-InvFWHT trick is a
|
|
1365
|
+
* documented non-goal until the quality gate passes. */
|
|
1366
|
+
export class TurboQuantKVCache implements Cache {
|
|
1367
|
+
static readonly STEP = 256;
|
|
1368
|
+
/** Set only by compiled-decode trace adapters (see Cache) — TurboQuant
|
|
1369
|
+
* is not a compiled-decode participant in v1 (novel class, monolith
|
|
1370
|
+
* fallback only), so this is always unset; captured once per step by
|
|
1371
|
+
* construction, never refreshed mid-step (batched-rotating-quant.ts:55-63). */
|
|
1372
|
+
readonly ropeOffsetArr?: MlxArray;
|
|
1373
|
+
#kv: TurboQuantTensor | null = null;
|
|
1374
|
+
offset = 0;
|
|
1375
|
+
/** Validated lazily on first update, once head_dim is known. */
|
|
1376
|
+
#headDim: number | null = null;
|
|
1377
|
+
|
|
1378
|
+
constructor(readonly kBits: number, readonly vBits: number) {}
|
|
1379
|
+
|
|
1380
|
+
get headDim(): number | null {
|
|
1381
|
+
return this.#headDim;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
#validateHeadDim(dim: number): void {
|
|
1385
|
+
if (this.#headDim !== null) return;
|
|
1386
|
+
const supported = [64, 128, 256, 512];
|
|
1387
|
+
if (dim % 32 !== 0 || !supported.includes(dim)) {
|
|
1388
|
+
throw new Error(
|
|
1389
|
+
`TurboQuantKVCache: head_dim ${dim} must be divisible by 32 and in {${supported.join(",")}}`,
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
this.#headDim = dim;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
#packedKDim(dim: number): number {
|
|
1396
|
+
return tq.packedDim(dim, this.kBits);
|
|
1397
|
+
}
|
|
1398
|
+
#packedVDim(dim: number): number {
|
|
1399
|
+
return tq.packedDim(dim, this.vBits);
|
|
1400
|
+
}
|
|
1401
|
+
#nGroups(dim: number): number {
|
|
1402
|
+
return dim / tq.TURBOQUANT_BLOCK_SIZE;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
#alloc(B: number, H: number, T: number, dim: number): TurboQuantTensor {
|
|
1406
|
+
const kIdxDtype = this.kBits === 8 ? Dtype.int8 : Dtype.uint8;
|
|
1407
|
+
return {
|
|
1408
|
+
kIdx: ops.zeros([B, H, T, this.#packedKDim(dim)], kIdxDtype),
|
|
1409
|
+
kScales: ops.zeros([B, H, T, this.#nGroups(dim)], Dtype.float16),
|
|
1410
|
+
kZeros: ops.zeros([B, H, T, this.#nGroups(dim)], Dtype.float16),
|
|
1411
|
+
vPacked: ops.zeros([B, H, T, this.#packedVDim(dim)], Dtype.uint8),
|
|
1412
|
+
vScales: ops.zeros([B, H, T, this.#nGroups(dim)], Dtype.float16),
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
#seqLen(): number {
|
|
1417
|
+
return this.#kv ? this.#kv.kIdx.shape[2]! : 0;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/** Encode k/v [B,H,L,D] into a fresh TurboQuantTensor (packed at rest). */
|
|
1421
|
+
#encode(k: MlxArray, v: MlxArray): TurboQuantTensor {
|
|
1422
|
+
const signed = this.kBits === 8;
|
|
1423
|
+
const kEnc = tq.encodeKeys(k, this.kBits, signed);
|
|
1424
|
+
const kIdxPacked = this.kBits < 8 ? tq.packBits(kEnc.indices, this.kBits) : kEnc.indices;
|
|
1425
|
+
if (kIdxPacked !== kEnc.indices) kEnc.indices.dispose();
|
|
1426
|
+
const vEnc = tq.encodeValues(v, this.vBits);
|
|
1427
|
+
const vIdxPacked = this.vBits < 8 ? tq.packBits(vEnc.indices, this.vBits) : vEnc.indices;
|
|
1428
|
+
if (vIdxPacked !== vEnc.indices) vEnc.indices.dispose();
|
|
1429
|
+
return {
|
|
1430
|
+
kIdx: kIdxPacked, kScales: kEnc.scales, kZeros: kEnc.zeros,
|
|
1431
|
+
vPacked: vIdxPacked, vScales: vEnc.scales,
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/** Decode a TurboQuantTensor's active [B,H,upTo,*] window back to
|
|
1436
|
+
* bf16 [B,H,upTo,headDim] (k, v). Caller owns the returned arrays.
|
|
1437
|
+
* deferV leaves V in the rotated domain (decodeValuesRotated) — the
|
|
1438
|
+
* caller un-rotates its attention output instead (tq.unrotateValues). */
|
|
1439
|
+
#decode(t: TurboQuantTensor, upTo: number, headDim: number, deferV = false): [MlxArray, MlxArray] {
|
|
1440
|
+
const cut = (a: MlxArray): MlxArray => {
|
|
1441
|
+
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1442
|
+
return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
|
|
1443
|
+
};
|
|
1444
|
+
const kIdxCut = cut(t.kIdx);
|
|
1445
|
+
const kIdxUnpacked = this.kBits < 8 ? tq.unpackBits(kIdxCut, this.kBits, headDim) : kIdxCut;
|
|
1446
|
+
if (kIdxUnpacked !== kIdxCut) kIdxCut.dispose();
|
|
1447
|
+
const kScalesCut = cut(t.kScales);
|
|
1448
|
+
const kZerosCut = cut(t.kZeros);
|
|
1449
|
+
const k = tq.decodeKeys(kIdxUnpacked, kScalesCut, kZerosCut);
|
|
1450
|
+
for (const a of [kIdxUnpacked, kScalesCut, kZerosCut]) a.dispose();
|
|
1451
|
+
|
|
1452
|
+
const vPackedCut = cut(t.vPacked);
|
|
1453
|
+
const vIdxUnpacked = this.vBits < 8 ? tq.unpackBits(vPackedCut, this.vBits, headDim) : vPackedCut;
|
|
1454
|
+
if (vIdxUnpacked !== vPackedCut) vPackedCut.dispose();
|
|
1455
|
+
const vScalesCut = cut(t.vScales);
|
|
1456
|
+
const v = deferV
|
|
1457
|
+
? tq.decodeValuesRotated(vIdxUnpacked, vScalesCut, this.vBits)
|
|
1458
|
+
: tq.decodeValues(vIdxUnpacked, vScalesCut, this.vBits);
|
|
1459
|
+
for (const a of [vIdxUnpacked, vScalesCut]) a.dispose();
|
|
1460
|
+
|
|
1461
|
+
return [k, v];
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/** Quantize the newly-appended k/v, append into growable packed
|
|
1465
|
+
* storage (256-step growth, mirroring KVCache), advance offset, then
|
|
1466
|
+
* dequantize the WHOLE active window (v1 semantics) so the standard
|
|
1467
|
+
* ops.sdpa path runs unmodified. */
|
|
1468
|
+
updateAndFetch(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
1469
|
+
const D = this.#append(k, v);
|
|
1470
|
+
return this.#decode(this.#kv!, this.offset, D);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/** Deferred-inverse-FWHT read path: same append, but the returned V
|
|
1474
|
+
* window stays in the ROTATED domain. The caller MUST un-rotate its
|
|
1475
|
+
* attention output with tq.unrotateValues (see SharedKv.vRotated) —
|
|
1476
|
+
* same decode by linearity, O(q·d log d) per step instead of
|
|
1477
|
+
* O(T·d log d). Opt-in per attention site; sites that don't know about
|
|
1478
|
+
* rotation keep calling updateAndFetch and stay correct. */
|
|
1479
|
+
updateAndFetchDeferredV(k: MlxArray, v: MlxArray): [MlxArray, MlxArray] {
|
|
1480
|
+
const D = this.#append(k, v);
|
|
1481
|
+
return this.#decode(this.#kv!, this.offset, D, true);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/** Shared append: grow storage, encode the new tokens, slice-write them
|
|
1485
|
+
* at the previous offset, advance offset. Returns head_dim. */
|
|
1486
|
+
#append(k: MlxArray, v: MlxArray): number {
|
|
1487
|
+
const [B, H, L, D] = k.shape as [number, number, number, number];
|
|
1488
|
+
this.#validateHeadDim(D);
|
|
1489
|
+
const prev = this.offset;
|
|
1490
|
+
|
|
1491
|
+
if (!this.#kv || prev + L > this.#seqLen()) {
|
|
1492
|
+
const nSteps = Math.floor((TurboQuantKVCache.STEP + L - 1) / TurboQuantKVCache.STEP);
|
|
1493
|
+
const newSteps = nSteps * TurboQuantKVCache.STEP;
|
|
1494
|
+
if (this.#kv && prev % TurboQuantKVCache.STEP !== 0) {
|
|
1495
|
+
const trimTo = (t: TurboQuantTensor): TurboQuantTensor => {
|
|
1496
|
+
const cut = (a: MlxArray): MlxArray => {
|
|
1497
|
+
const [b, h, , d] = a.shape as [number, number, number, number];
|
|
1498
|
+
const s = a.slice([0, 0, 0, 0], [b, h, prev, d]);
|
|
1499
|
+
a.dispose();
|
|
1500
|
+
return s;
|
|
1501
|
+
};
|
|
1502
|
+
return { kIdx: cut(t.kIdx), kScales: cut(t.kScales), kZeros: cut(t.kZeros),
|
|
1503
|
+
vPacked: cut(t.vPacked), vScales: cut(t.vScales) };
|
|
1504
|
+
};
|
|
1505
|
+
this.#kv = trimTo(this.#kv);
|
|
1506
|
+
}
|
|
1507
|
+
const grown = this.#alloc(B, H, newSteps, D);
|
|
1508
|
+
if (this.#kv) {
|
|
1509
|
+
const cat = (a: MlxArray, b: MlxArray): MlxArray => {
|
|
1510
|
+
const out = ops.concatAxis([a, b], 2);
|
|
1511
|
+
a.dispose();
|
|
1512
|
+
b.dispose();
|
|
1513
|
+
return out;
|
|
1514
|
+
};
|
|
1515
|
+
this.#kv = {
|
|
1516
|
+
kIdx: cat(this.#kv.kIdx, grown.kIdx),
|
|
1517
|
+
kScales: cat(this.#kv.kScales, grown.kScales),
|
|
1518
|
+
kZeros: cat(this.#kv.kZeros, grown.kZeros),
|
|
1519
|
+
vPacked: cat(this.#kv.vPacked, grown.vPacked),
|
|
1520
|
+
vScales: cat(this.#kv.vScales, grown.vScales),
|
|
1521
|
+
};
|
|
1522
|
+
} else {
|
|
1523
|
+
this.#kv = grown;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
this.offset += L;
|
|
1528
|
+
const enc = this.#encode(k, v);
|
|
1529
|
+
const write = (dst: MlxArray, src: MlxArray): MlxArray => {
|
|
1530
|
+
const [b, h, , d] = dst.shape as [number, number, number, number];
|
|
1531
|
+
const u = ops.sliceUpdate(dst, src, [0, 0, prev, 0], [b, h, this.offset, d]);
|
|
1532
|
+
dst.dispose();
|
|
1533
|
+
src.dispose();
|
|
1534
|
+
return u;
|
|
1535
|
+
};
|
|
1536
|
+
this.#kv = {
|
|
1537
|
+
kIdx: write(this.#kv!.kIdx, enc.kIdx),
|
|
1538
|
+
kScales: write(this.#kv!.kScales, enc.kScales),
|
|
1539
|
+
kZeros: write(this.#kv!.kZeros, enc.kZeros),
|
|
1540
|
+
vPacked: write(this.#kv!.vPacked, enc.vPacked),
|
|
1541
|
+
vScales: write(this.#kv!.vScales, enc.vScales),
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1544
|
+
return D;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/** Same rule as KVCache/QuantizedKVCache.makeMask (mlx-lm cache.py:114-125):
|
|
1548
|
+
* windowless multi-token chunks are the string "causal" at any offset —
|
|
1549
|
+
* TurboQuant v1 is full-attention-only, never windowed. */
|
|
1550
|
+
makeMask(N: number, windowSize: number | null): Mask {
|
|
1551
|
+
if (N === 1) return { mode: "", arr: null };
|
|
1552
|
+
if (windowSize === null) return { mode: "causal", arr: null };
|
|
1553
|
+
if (this.offset === 0 && N <= windowSize) return { mode: "causal", arr: null };
|
|
1554
|
+
return { mode: "array", arr: createCausalMask(N, this.offset, windowSize) };
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/** Trimmed-to-offset arrays in documented order: kIdx, kScales, kZeros,
|
|
1558
|
+
* vPacked, vScales (kv-store.ts tensor-slot ordering for kind
|
|
1559
|
+
* "turboquant"). Empty cache → []. */
|
|
1560
|
+
state(): MlxArray[] {
|
|
1561
|
+
if (!this.#kv) return [];
|
|
1562
|
+
const cut = (a: MlxArray): MlxArray => {
|
|
1563
|
+
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
1564
|
+
return a.slice([0, 0, 0, 0], [B, H, this.offset, D]);
|
|
1565
|
+
};
|
|
1566
|
+
return [cut(this.#kv.kIdx), cut(this.#kv.kScales), cut(this.#kv.kZeros),
|
|
1567
|
+
cut(this.#kv.vPacked), cut(this.#kv.vScales)];
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
isTrimmable(): boolean {
|
|
1571
|
+
return true;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/** Drop the last n tokens (byte-safe: groups run along head_dim, so a
|
|
1575
|
+
* token-axis slice never splits a group). Same front/back convention
|
|
1576
|
+
* as KVCache.trim: offset shrinks, storage is overwritten on next write. */
|
|
1577
|
+
trim(n: number): void {
|
|
1578
|
+
this.offset = Math.max(0, this.offset - n);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/** Adopt persisted/converted state (takes ownership of the arrays). */
|
|
1582
|
+
restoreState(t: TurboQuantTensor, offset: number, headDim: number): void {
|
|
1583
|
+
this.dispose();
|
|
1584
|
+
this.#kv = t;
|
|
1585
|
+
this.offset = offset;
|
|
1586
|
+
this.#headDim = headDim;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/** Convert an existing bf16 KVCache/RotatingKVCache's live window in one
|
|
1590
|
+
* shot: quantize the whole [.., :offset, :] region, preserve offset,
|
|
1591
|
+
* dispose the source's arrays. Mirrors KVCache.toQuantized/
|
|
1592
|
+
* RotatingKVCache.toQuantized's contract (source cache is consumed). */
|
|
1593
|
+
static fromKVCache(cache: KVCache | RotatingKVCache, kBits: number, vBits: number): TurboQuantKVCache {
|
|
1594
|
+
const q = new TurboQuantKVCache(kBits, vBits);
|
|
1595
|
+
const [k, v] = cache.keys && cache.values ? cache.temporalView() : [null, null];
|
|
1596
|
+
if (k && v) {
|
|
1597
|
+
const D = k.shape[3]!;
|
|
1598
|
+
q.#validateHeadDim(D);
|
|
1599
|
+
const enc = q.#encode(k, v);
|
|
1600
|
+
k.dispose();
|
|
1601
|
+
v.dispose();
|
|
1602
|
+
q.#kv = enc;
|
|
1603
|
+
q.#headDim = D;
|
|
1604
|
+
}
|
|
1605
|
+
q.offset = cache.offset;
|
|
1606
|
+
cache.dispose();
|
|
1607
|
+
return q;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
dispose(): void {
|
|
1611
|
+
if (this.#kv) disposeTurboQuant(this.#kv);
|
|
1612
|
+
this.#kv = null;
|
|
1613
|
+
this.offset = 0;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1282
1617
|
export const FINFO_MIN: Partial<Record<Dtype, number>> = {
|
|
1283
1618
|
[Dtype.bfloat16]: -3.3895313892515355e38,
|
|
1284
1619
|
[Dtype.float16]: -65504,
|
|
@@ -1338,6 +1673,14 @@ export function quantizedSdpaUnfused(
|
|
|
1338
1673
|
for (const a of [qIdx, kIdx, qCol, kRow]) a.dispose();
|
|
1339
1674
|
} else if (mask.mode === "array") {
|
|
1340
1675
|
maskArr = mask.arr;
|
|
1676
|
+
// Batched padding masks are [B,1,L,S] (buildBatchedDecodeMask). When GQA
|
|
1677
|
+
// reshaped the scores to 5-D [B,KV,nRep,L,S], trailing-dim broadcasting
|
|
1678
|
+
// would misalign the mask's B with KV — insert the unit axis explicitly:
|
|
1679
|
+
// [B,1,1,L,S] broadcasts correctly over (KV, nRep).
|
|
1680
|
+
if (maskArr && maskArr.ndim === 4 && nRep > 1) {
|
|
1681
|
+
maskArr = ops.expandDims(maskArr, 1);
|
|
1682
|
+
ownsMask = true;
|
|
1683
|
+
}
|
|
1341
1684
|
}
|
|
1342
1685
|
if (maskArr) {
|
|
1343
1686
|
const ninf = ops.scalarLike(FINFO_MIN[scores.dtype] ?? -3.4e38, scores);
|
|
@@ -1568,38 +1911,19 @@ function fusedSdpaSupported(q: MlxArray, mask: Mask, groupSize: number, bits: nu
|
|
|
1568
1911
|
/** Quantized-cache SDPA dispatch: L > 1 (prefill/continuation) with a
|
|
1569
1912
|
* supported config goes through the N-tiled fused path; decode (L = 1)
|
|
1570
1913
|
* and unsupported configs stay on the stock unfused port. Exported for
|
|
1571
|
-
* the dispatch-gate tests.
|
|
1572
|
-
*
|
|
1573
|
-
* MLX_BUN_FUSED_DECODE=1 (NEXT UP 1b experiment): tile decode too,
|
|
1574
|
-
* matching optiq's wrapper, which has no L gate — its serving decode
|
|
1575
|
-
* over quantized caches runs N tiles per step, and Phase 15 measured
|
|
1576
|
-
* its kv-mixed decode tax @8k as ~free where ours is ~3%. Off by
|
|
1577
|
-
* default until a cleared-machine A/B shows it pays; directional
|
|
1578
|
-
* numbers via scripts/bench-fused-decode.ts. */
|
|
1914
|
+
* the dispatch-gate tests. */
|
|
1579
1915
|
export function quantizedSdpa(
|
|
1580
1916
|
q: MlxArray, kq: ops.QuantizedTensor, vq: ops.QuantizedTensor,
|
|
1581
1917
|
scale: number, mask: Mask, groupSize: number, bits: number,
|
|
1582
1918
|
): MlxArray {
|
|
1583
|
-
|
|
1584
|
-
// dispatches. NOT bit-exact (online softmax) — gated against the
|
|
1585
|
-
// frozen perf oracle; compat (MLX_BUN_PERF_KERNEL=0) stays the -O0
|
|
1586
|
-
// reference. Never inside a compiled trace: CustomKernel has no
|
|
1587
|
-
// output_shapes, so it cannot live in a (shapeless) closure.
|
|
1588
|
-
if (
|
|
1589
|
-
mask.mode === "" && scale === 1.0 && perfKernelEnabled() &&
|
|
1590
|
-
!isCompiledTrace() && fusedDecodeKernelSupported(q, bits, groupSize)
|
|
1591
|
-
)
|
|
1592
|
-
return fusedDecodeSdpa(q, kq, vq, groupSize, bits);
|
|
1593
|
-
const tile = q.shape[2]! > 1 || process.env.MLX_BUN_FUSED_DECODE === "1";
|
|
1919
|
+
const tile = q.shape[2]! > 1;
|
|
1594
1920
|
if (tile && fusedSdpaSupported(q, mask, groupSize, bits)) {
|
|
1595
1921
|
// The N-tiled loop CANNOT be traced: its tile count and slice extents
|
|
1596
1922
|
// bake at trace-time N, so under shapeless replay a growing (concat-
|
|
1597
|
-
// phase) quantized cache's newest rows are silently never attended
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1600
|
-
//
|
|
1601
|
-
// compiled caller — it lands in the trace's transactional fallback
|
|
1602
|
-
// instead of producing silently wrong attention.
|
|
1923
|
+
// phase) quantized cache's newest rows are silently never attended.
|
|
1924
|
+
// This throw is the backstop for any compiled caller — it lands in the
|
|
1925
|
+
// trace's transactional fallback instead of producing silently wrong
|
|
1926
|
+
// attention.
|
|
1603
1927
|
if (isCompiledTrace())
|
|
1604
1928
|
throw new Error(
|
|
1605
1929
|
"quantizedSdpaTiled inside a compiled trace: the tile loop bakes trace-time N " +
|