mlx-bun 0.0.9 → 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 +255 -0
- 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 +98 -0
- 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 +18 -2
- package/docs/design/ssd-kv-cold-tier.md +288 -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/decode-roofline-lookagain.md +92 -6
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/kernel-perf-review-2026-07.md +105 -26
- 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.10.md +57 -0
- 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 +251 -119
- package/docs/reference/training.md +20 -16
- 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 +402 -107
- 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 -16
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +618 -122
- 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 +127 -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 +179 -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 +391 -34
- 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 +123 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +99 -25
- 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 +338 -32
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +61 -3
- package/src/serve/batch-scheduler.ts +1001 -76
- package/src/serve/generation-gateway.ts +292 -30
- 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 +1460 -105
- 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 +285 -0
- 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 +29 -26
- package/src/train/job.ts +0 -2
- package/src/train/loss.ts +53 -11
- package/src/train/prefix-shared.ts +30 -7
- package/src/train/segmented.ts +150 -46
- package/src/train/trainer.ts +22 -15
- 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
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// Batched sliding-window QUANTIZED KV — Phase 3 milestone 2 of
|
|
2
|
+
// docs/design/unified-engine-frontier-plan.md: the composition that lets a
|
|
3
|
+
// per-layer kv_config naming ROTATING layers (gemma) batch. No stack ships
|
|
4
|
+
// this (mlx-lm's batched path is bf16-only; optiq's mixed-KV hook is
|
|
5
|
+
// serial-only), so per the composition rule the oracle is the SERIAL
|
|
6
|
+
// RotatingQuantizedKVCache per row: each joiner's solo prefill converts with
|
|
7
|
+
// the serial ops (BatchScheduler.#quantizeSolo), and this class only
|
|
8
|
+
// re-arranges those bytes across the batch axis + runs the mlx-lm
|
|
9
|
+
// BatchRotatingKVCache ring mechanics (batched-rotating.ts) over
|
|
10
|
+
// (packed, scales, biases) triples instead of bf16 tensors.
|
|
11
|
+
//
|
|
12
|
+
// Same safety argument as batched-quant.ts: quantization packs along
|
|
13
|
+
// HEAD_DIM, so the token axis stays token-granular in all three tensors —
|
|
14
|
+
// left-pad columns are zeros, masked by buildBatchedRotatingMask (token-axis
|
|
15
|
+
// only; shared verbatim with the bf16 twin), never dequantized into anything
|
|
16
|
+
// a real row reads.
|
|
17
|
+
//
|
|
18
|
+
// Subclasses RotatingQuantizedKVCache so Attention.forward's `instanceof`
|
|
19
|
+
// dispatch (gemma4.ts) routes to the quantized path with the L1 model file
|
|
20
|
+
// untouched. PERSISTENT (like BatchedRotatingCache, unlike the ephemeral
|
|
21
|
+
// per-step BatchedQuantDecodeMaskCache): the scheduler passes it through
|
|
22
|
+
// every step; it self-tracks per-row offset/leftPad and the scalar ring
|
|
23
|
+
// state. N=1 decode updates only (rows solo-prefill serially, then merge).
|
|
24
|
+
//
|
|
25
|
+
// Gates: tests/batched-rotating-quant.test.ts (model-free byte-identity vs
|
|
26
|
+
// the serial oracle per row, through ring wrap, B=1 and B=2) + the gemma
|
|
27
|
+
// scheduler gate in tests/batched-kv-quant-parity.test.ts.
|
|
28
|
+
|
|
29
|
+
import { MlxArray } from "../mlx/array";
|
|
30
|
+
import * as ops from "../mlx/ops";
|
|
31
|
+
import { Dtype } from "../mlx/ffi";
|
|
32
|
+
import { RotatingQuantizedKVCache, type Mask } from "./gemma4-base";
|
|
33
|
+
import { buildBatchedRotatingMask } from "./batched-rotating";
|
|
34
|
+
import type { QuantRow } from "./batched-quant";
|
|
35
|
+
|
|
36
|
+
const STEP = 256; // shared with RotatingQuantizedKVCache.STEP and the bf16 twin
|
|
37
|
+
|
|
38
|
+
const disposeTriple = (t: ops.QuantizedTensor): void => {
|
|
39
|
+
t.packed.dispose();
|
|
40
|
+
t.scales.dispose();
|
|
41
|
+
t.biases.dispose();
|
|
42
|
+
};
|
|
43
|
+
const mapTriple = (
|
|
44
|
+
t: ops.QuantizedTensor, f: (a: MlxArray) => MlxArray,
|
|
45
|
+
): ops.QuantizedTensor => ({ packed: f(t.packed), scales: f(t.scales), biases: f(t.biases) });
|
|
46
|
+
|
|
47
|
+
/** Batched ring over quantized triples — mlx-lm BatchRotatingKVCache
|
|
48
|
+
* mechanics (see batched-rotating.ts) with RotatingQuantizedKVCache
|
|
49
|
+
* storage. Scalar ring state (`ringIdx`/`offset` reuse the base fields);
|
|
50
|
+
* per-row `offsetArr` (absolute positions → RoPE) and `leftPad`. */
|
|
51
|
+
export class BatchedRotatingQuantCache extends RotatingQuantizedKVCache {
|
|
52
|
+
offsetArr: number[];
|
|
53
|
+
leftPad: number[];
|
|
54
|
+
#rotated = false;
|
|
55
|
+
/** Per-row RoPE positions. STABLE ACROSS A STEP — refreshed only at
|
|
56
|
+
* releaseRopeArr() (the scheduler's post-dispatch hook), never inside
|
|
57
|
+
* the update: the monolith CAPTURES it pre-update and ropes Q after
|
|
58
|
+
* (an in-update dispose is a use-after-dispose there), and the
|
|
59
|
+
* GENERATED specializations RE-READ it for Q post-update (an in-update
|
|
60
|
+
* refresh hands K and Q different positions there — found the hard way
|
|
61
|
+
* 2026-07-05: the twin passes the generated files' instanceof cache
|
|
62
|
+
* guards by subclassing, so an all-quant gemma batch decodes through
|
|
63
|
+
* the generated graph). Widens the base's readonly to mutable. */
|
|
64
|
+
override ropeOffsetArr: MlxArray;
|
|
65
|
+
|
|
66
|
+
private constructor(
|
|
67
|
+
maxSize: number, groupSize: number, bits: number, leftPad: number[], offsets: number[],
|
|
68
|
+
) {
|
|
69
|
+
super(maxSize, groupSize, bits);
|
|
70
|
+
this.leftPad = [...leftPad];
|
|
71
|
+
this.offsetArr = [...offsets];
|
|
72
|
+
this.ropeOffsetArr = MlxArray.fromInt32(Int32Array.from(offsets), [offsets.length]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get #B(): number {
|
|
76
|
+
return this.leftPad.length;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#seqLen2(): number {
|
|
80
|
+
return this.keys ? this.keys.packed.shape[2]! : 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Scheduler hook, called on every fwd cache AFTER the step's graph is
|
|
84
|
+
* built — the ONE safe point to swap the rope array for the advanced
|
|
85
|
+
* positions (the step's graph holds its own reference; no JS reads the
|
|
86
|
+
* old handle after this). */
|
|
87
|
+
releaseRopeArr(): void {
|
|
88
|
+
this.ropeOffsetArr.dispose();
|
|
89
|
+
this.ropeOffsetArr = MlxArray.fromInt32(Int32Array.from(this.offsetArr), [this.#B]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#allocTriple(B: number, H: number, T: number, dim: number, dtype: Dtype): ops.QuantizedTensor {
|
|
93
|
+
const elPerInt = 32 / this.bits;
|
|
94
|
+
return {
|
|
95
|
+
packed: ops.zeros([B, H, T, dim / elPerInt], Dtype.uint32),
|
|
96
|
+
scales: ops.zeros([B, H, T, dim / this.groupSize], dtype),
|
|
97
|
+
biases: ops.zeros([B, H, T, dim / this.groupSize], dtype),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override makeMask(N: number, windowSize: number | null): Mask {
|
|
102
|
+
const window = windowSize ?? this.maxSize;
|
|
103
|
+
return {
|
|
104
|
+
mode: "array",
|
|
105
|
+
arr: buildBatchedRotatingMask(
|
|
106
|
+
this.#B, N, this.leftPad, this.maxSize, window,
|
|
107
|
+
this.ringIdx, this.offset, this.#rotated,
|
|
108
|
+
),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** N=1 decode update — the bf16 twin's updateAndFetch (mlx-lm
|
|
113
|
+
* _update_in_place) over triples, with quantize-on-write from the
|
|
114
|
+
* serial oracle. */
|
|
115
|
+
override updateAndFetchQuantized(k: MlxArray, v: MlxArray): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
116
|
+
const [B, H, S, D] = k.shape as [number, number, number, number];
|
|
117
|
+
const vD = v.shape[3]!;
|
|
118
|
+
if (S !== 1)
|
|
119
|
+
throw new Error("BatchedRotatingQuantCache supports N=1 decode updates only (solo-prefill then merge)");
|
|
120
|
+
const prev = this.offset;
|
|
121
|
+
|
|
122
|
+
// Grow the buffer (in STEP chunks) until it reaches maxSize.
|
|
123
|
+
if (!this.keys || (prev >= this.#seqLen2() && this.#seqLen2() < this.maxSize)) {
|
|
124
|
+
const newSize = Math.min(STEP, this.maxSize - prev);
|
|
125
|
+
const newK = this.#allocTriple(B, H, newSize, D, k.dtype);
|
|
126
|
+
const newV = this.#allocTriple(B, H, newSize, vD, v.dtype);
|
|
127
|
+
if (this.keys && this.values) {
|
|
128
|
+
const grow = (old: ops.QuantizedTensor, add: ops.QuantizedTensor): ops.QuantizedTensor => {
|
|
129
|
+
const cat = (a: MlxArray, b: MlxArray): MlxArray => {
|
|
130
|
+
const out = ops.concatAxis([a, b], 2);
|
|
131
|
+
a.dispose();
|
|
132
|
+
b.dispose();
|
|
133
|
+
return out;
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
packed: cat(old.packed, add.packed),
|
|
137
|
+
scales: cat(old.scales, add.scales),
|
|
138
|
+
biases: cat(old.biases, add.biases),
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
this.keys = grow(this.keys, newK);
|
|
142
|
+
this.values = grow(this.values, newV);
|
|
143
|
+
} else {
|
|
144
|
+
this.keys = newK;
|
|
145
|
+
this.values = newV;
|
|
146
|
+
}
|
|
147
|
+
this.ringIdx = prev;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Trim any overshoot past maxSize (decrements left padding persistently).
|
|
151
|
+
const trimSize = this.#seqLen2() - this.maxSize;
|
|
152
|
+
if (trimSize > 0) {
|
|
153
|
+
const cutFrom = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
|
|
154
|
+
mapTriple(t, (a) => {
|
|
155
|
+
const [b2, h2, s2, d2] = a.shape as [number, number, number, number];
|
|
156
|
+
const s = a.slice([0, 0, trimSize, 0], [b2, h2, s2, d2]);
|
|
157
|
+
a.dispose();
|
|
158
|
+
return s;
|
|
159
|
+
});
|
|
160
|
+
this.keys = cutFrom(this.keys!);
|
|
161
|
+
this.values = cutFrom(this.values!);
|
|
162
|
+
this.ringIdx = this.maxSize;
|
|
163
|
+
this.leftPad = this.leftPad.map((x) => x - trimSize);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Rotate when the write head reaches the end of the ring.
|
|
167
|
+
if (this.ringIdx === this.maxSize) {
|
|
168
|
+
this.#rotated = true;
|
|
169
|
+
this.ringIdx = 0;
|
|
170
|
+
}
|
|
171
|
+
if (this.#rotated) this.leftPad = this.leftPad.map((x) => x - S);
|
|
172
|
+
|
|
173
|
+
// Quantize + write the new K/V at the ring head (six sliceUpdates).
|
|
174
|
+
const kq = ops.quantize(k, this.groupSize, this.bits);
|
|
175
|
+
const vq = ops.quantize(v, this.groupSize, this.bits);
|
|
176
|
+
const writeAt = (dst: ops.QuantizedTensor, src: ops.QuantizedTensor): ops.QuantizedTensor =>
|
|
177
|
+
({
|
|
178
|
+
packed: this.#assign2(dst.packed, src.packed, S),
|
|
179
|
+
scales: this.#assign2(dst.scales, src.scales, S),
|
|
180
|
+
biases: this.#assign2(dst.biases, src.biases, S),
|
|
181
|
+
});
|
|
182
|
+
this.keys = writeAt(this.keys!, kq);
|
|
183
|
+
this.values = writeAt(this.values!, vq);
|
|
184
|
+
disposeTriple(kq);
|
|
185
|
+
disposeTriple(vq);
|
|
186
|
+
|
|
187
|
+
this.offset += S;
|
|
188
|
+
this.offsetArr = this.offsetArr.map((x) => x + S);
|
|
189
|
+
this.ringIdx += S;
|
|
190
|
+
|
|
191
|
+
// Return the populated prefix (ring not yet full) or the whole buffer.
|
|
192
|
+
const upTo = this.offset < this.maxSize ? this.offset : this.#seqLen2();
|
|
193
|
+
const cut = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
|
|
194
|
+
mapTriple(t, (a) => {
|
|
195
|
+
const [b2, h2, , d2] = a.shape as [number, number, number, number];
|
|
196
|
+
return a.slice([0, 0, 0, 0], [b2, h2, upTo, d2]);
|
|
197
|
+
});
|
|
198
|
+
return [cut(this.keys!), cut(this.values!)];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#assign2(dst: MlxArray, src: MlxArray, S: number): MlxArray {
|
|
202
|
+
const [B, H, , D] = dst.shape as [number, number, number, number];
|
|
203
|
+
const out = ops.sliceUpdate(dst, src, [0, 0, this.ringIdx, 0], [B, H, this.ringIdx + S, D]);
|
|
204
|
+
dst.dispose();
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Ring contents in temporal order, cut to the valid length (extract) —
|
|
209
|
+
* overrides the base to use the BATCHED ring state. */
|
|
210
|
+
override temporalView(): [ops.QuantizedTensor, ops.QuantizedTensor] {
|
|
211
|
+
if (!this.keys || !this.values) throw new Error("cache is empty");
|
|
212
|
+
const valid = Math.min(this.offset, this.maxSize);
|
|
213
|
+
const order = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
|
|
214
|
+
mapTriple(t, (a) => {
|
|
215
|
+
const [B, H, Sbuf, D] = a.shape as [number, number, number, number];
|
|
216
|
+
let tt: MlxArray;
|
|
217
|
+
if (this.ringIdx === Sbuf) {
|
|
218
|
+
tt = a.slice([0, 0, 0, 0], [B, H, Sbuf, D]);
|
|
219
|
+
} else if (this.ringIdx < this.offset) {
|
|
220
|
+
const tail = a.slice([0, 0, this.ringIdx, 0], [B, H, Sbuf, D]);
|
|
221
|
+
const head = a.slice([0, 0, 0, 0], [B, H, this.ringIdx, D]);
|
|
222
|
+
tt = ops.concatAxis([tail, head], 2);
|
|
223
|
+
tail.dispose();
|
|
224
|
+
head.dispose();
|
|
225
|
+
} else {
|
|
226
|
+
tt = a.slice([0, 0, 0, 0], [B, H, this.ringIdx, D]);
|
|
227
|
+
}
|
|
228
|
+
const cut = tt.slice([0, 0, 0, 0], [B, H, valid, D]);
|
|
229
|
+
tt.dispose();
|
|
230
|
+
return cut;
|
|
231
|
+
});
|
|
232
|
+
return [order(this.keys), order(this.values)];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Quantized twin of BatchedRotatingCache.extractRow (mlx-lm
|
|
236
|
+
* BatchRotatingKVCache.extract, models/cache.py:1417, over triples):
|
|
237
|
+
* row `i` as a fresh SERIAL RotatingQuantizedKVCache — de-rolled to
|
|
238
|
+
* temporal order, left padding stripped, OWNED contiguous copies;
|
|
239
|
+
* offset = the row's absolute position, ringIdx = the new buffer length
|
|
240
|
+
* (the oracle's `cache._idx = cache.keys.shape[2]`). Token-axis slicing
|
|
241
|
+
* is byte-safe (packing along HEAD_DIM — file header); per-row byte
|
|
242
|
+
* identity vs the serial oracle is the class invariant
|
|
243
|
+
* (tests/batched-rotating-quant), extraction is a pure slice+copy. */
|
|
244
|
+
extractRow(i: number): RotatingQuantizedKVCache | null {
|
|
245
|
+
if (!this.keys || !this.values) return null;
|
|
246
|
+
const pad = Math.max(0, this.leftPad[i]!);
|
|
247
|
+
const valid = Math.min(this.offset, this.maxSize);
|
|
248
|
+
const cut = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
|
|
249
|
+
mapTriple(t, (a) => {
|
|
250
|
+
const [, H, Sbuf, D] = a.shape as [number, number, number, number];
|
|
251
|
+
const row = a.slice([i, 0, 0, 0], [i + 1, H, Sbuf, D]);
|
|
252
|
+
let tt: MlxArray;
|
|
253
|
+
if (this.ringIdx === Sbuf) {
|
|
254
|
+
tt = row;
|
|
255
|
+
} else if (this.ringIdx < this.offset) {
|
|
256
|
+
const tail = row.slice([0, 0, this.ringIdx, 0], [1, H, Sbuf, D]);
|
|
257
|
+
const head = row.slice([0, 0, 0, 0], [1, H, this.ringIdx, D]);
|
|
258
|
+
tt = ops.concatAxis([tail, head], 2);
|
|
259
|
+
tail.dispose();
|
|
260
|
+
head.dispose();
|
|
261
|
+
row.dispose();
|
|
262
|
+
} else {
|
|
263
|
+
tt = row.slice([0, 0, 0, 0], [1, H, this.ringIdx, D]);
|
|
264
|
+
row.dispose();
|
|
265
|
+
}
|
|
266
|
+
const cutV = tt.slice([0, 0, pad, 0], [1, H, valid, D]);
|
|
267
|
+
tt.dispose();
|
|
268
|
+
const own = ops.contiguous(cutV);
|
|
269
|
+
cutV.dispose();
|
|
270
|
+
return own;
|
|
271
|
+
});
|
|
272
|
+
const c = new RotatingQuantizedKVCache(this.maxSize, this.groupSize, this.bits);
|
|
273
|
+
const k = cut(this.keys);
|
|
274
|
+
const v = cut(this.values);
|
|
275
|
+
c.restoreState(k, v, this.offsetArr[i]!, k.packed.shape[2]!);
|
|
276
|
+
return c;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Keep only `keep` rows along the batch axis (eviction), in place. */
|
|
280
|
+
filter(keep: number[]): void {
|
|
281
|
+
if (this.keys && this.values) {
|
|
282
|
+
const idxArr = MlxArray.fromInt32(Int32Array.from(keep), [keep.length]);
|
|
283
|
+
const take = (t: ops.QuantizedTensor): ops.QuantizedTensor =>
|
|
284
|
+
mapTriple(t, (a) => ops.takeAxis(a, idxArr, 0));
|
|
285
|
+
const k = take(this.keys);
|
|
286
|
+
const v = take(this.values);
|
|
287
|
+
idxArr.dispose();
|
|
288
|
+
disposeTriple(this.keys);
|
|
289
|
+
disposeTriple(this.values);
|
|
290
|
+
this.keys = k;
|
|
291
|
+
this.values = v;
|
|
292
|
+
}
|
|
293
|
+
this.offsetArr = keep.map((i) => this.offsetArr[i]!);
|
|
294
|
+
this.leftPad = keep.map((i) => this.leftPad[i]!);
|
|
295
|
+
this.releaseRopeArr(); // safe here: filter runs between steps
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
override isTrimmable(): boolean {
|
|
299
|
+
return false; // batched rows never re-enter the prompt cache directly
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
override dispose(): void {
|
|
303
|
+
super.dispose();
|
|
304
|
+
this.releaseRopeArr();
|
|
305
|
+
this.ropeOffsetArr?.dispose();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Assemble a batch from per-row temporal quantized slices (the bf16
|
|
309
|
+
* twin's merge over triples). Each row is a solo/adopted/batched cache's
|
|
310
|
+
* temporalView ([1,H,Li,*], Li ≤ maxSize, temporal order) and
|
|
311
|
+
* `offsets[i]` its absolute position. Result is in temporal order
|
|
312
|
+
* (rotated=false, ringIdx=offset=width). */
|
|
313
|
+
static merge(
|
|
314
|
+
rows: QuantRow[], offsets: number[], maxSize: number, groupSize: number, bits: number,
|
|
315
|
+
): BatchedRotatingQuantCache {
|
|
316
|
+
const lens = rows.map((r) => r.keys.packed.shape[2]!);
|
|
317
|
+
const width = Math.max(...lens, 0);
|
|
318
|
+
const leftPad = lens.map((l) => width - l);
|
|
319
|
+
const cache = new BatchedRotatingQuantCache(maxSize, groupSize, bits, leftPad, offsets);
|
|
320
|
+
if (width === 0) return cache;
|
|
321
|
+
|
|
322
|
+
const padTo = (a: MlxArray, pad: number): MlxArray => {
|
|
323
|
+
if (pad === 0) return a.slice([0, 0, 0, 0], a.shape as number[]);
|
|
324
|
+
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
325
|
+
const z = ops.zeros([B, H, pad, D], a.dtype);
|
|
326
|
+
const out = ops.concatAxis([z, a], 2);
|
|
327
|
+
z.dispose();
|
|
328
|
+
return out;
|
|
329
|
+
};
|
|
330
|
+
const padded = rows.map((r, i) => ({
|
|
331
|
+
keys: mapTriple(r.keys, (a) => padTo(a, leftPad[i]!)),
|
|
332
|
+
values: mapTriple(r.values, (a) => padTo(a, leftPad[i]!)),
|
|
333
|
+
}));
|
|
334
|
+
const cat = (pick: (r: QuantRow) => ops.QuantizedTensor): ops.QuantizedTensor => ({
|
|
335
|
+
packed: ops.concatAxis(padded.map((r) => pick(r).packed), 0),
|
|
336
|
+
scales: ops.concatAxis(padded.map((r) => pick(r).scales), 0),
|
|
337
|
+
biases: ops.concatAxis(padded.map((r) => pick(r).biases), 0),
|
|
338
|
+
});
|
|
339
|
+
cache.keys = cat((r) => r.keys);
|
|
340
|
+
cache.values = cat((r) => r.values);
|
|
341
|
+
for (const r of padded) { disposeTriple(r.keys); disposeTriple(r.values); }
|
|
342
|
+
cache.ringIdx = width;
|
|
343
|
+
cache.offset = width;
|
|
344
|
+
return cache;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { MlxArray } from "../mlx/array";
|
|
26
26
|
import * as ops from "../mlx/ops";
|
|
27
|
-
import type
|
|
27
|
+
import { RotatingKVCache, type Cache, type Mask } from "./gemma4-base";
|
|
28
28
|
|
|
29
29
|
const STEP = 256;
|
|
30
30
|
|
|
@@ -229,6 +229,51 @@ export class BatchedRotatingCache implements Cache {
|
|
|
229
229
|
return [order(this.keys), order(this.values)];
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/** mlx-lm `BatchRotatingKVCache.extract` (models/cache.py:1417): row `i`
|
|
233
|
+
* as a fresh SERIAL RotatingKVCache — de-rolled to temporal order (the
|
|
234
|
+
* oracle's roll(-_idx) when rotated), left padding stripped
|
|
235
|
+
* (`max(0, left_padding[idx])` — post-wrap pads go negative), OWNED
|
|
236
|
+
* contiguous copies. `offset` = the row's absolute position (may exceed
|
|
237
|
+
* the buffer: wrapped rings have evicted tokens); ring idx = the new
|
|
238
|
+
* buffer length — temporal order ⇔ write head at the end, exactly the
|
|
239
|
+
* oracle's `cache._idx = cache.keys.shape[2]`. Bit-exact vs a solo run:
|
|
240
|
+
* merge/decode/filter keep each row's ring bytes identical to the serial
|
|
241
|
+
* cache's (tests/batched-rotating) and this is a pure slice+copy. */
|
|
242
|
+
extractRow(i: number): RotatingKVCache | null {
|
|
243
|
+
if (!this.keys || !this.values) return null;
|
|
244
|
+
const pad = Math.max(0, this.leftPad[i]!);
|
|
245
|
+
const valid = Math.min(this.#offset, this.maxSize);
|
|
246
|
+
const cut = (a: MlxArray): MlxArray => {
|
|
247
|
+
const [, H, Sbuf, D] = a.shape as [number, number, number, number];
|
|
248
|
+
const row = a.slice([i, 0, 0, 0], [i + 1, H, Sbuf, D]);
|
|
249
|
+
// temporal order — same three cases as temporalView, single row.
|
|
250
|
+
let t: MlxArray;
|
|
251
|
+
if (this.#idx === Sbuf) {
|
|
252
|
+
t = row;
|
|
253
|
+
} else if (this.#idx < this.#offset) {
|
|
254
|
+
const tail = row.slice([0, 0, this.#idx, 0], [1, H, Sbuf, D]);
|
|
255
|
+
const head = row.slice([0, 0, 0, 0], [1, H, this.#idx, D]);
|
|
256
|
+
t = ops.concatAxis([tail, head], 2);
|
|
257
|
+
tail.dispose();
|
|
258
|
+
head.dispose();
|
|
259
|
+
row.dispose();
|
|
260
|
+
} else {
|
|
261
|
+
t = row.slice([0, 0, 0, 0], [1, H, this.#idx, D]);
|
|
262
|
+
row.dispose();
|
|
263
|
+
}
|
|
264
|
+
const cutV = t.slice([0, 0, pad, 0], [1, H, valid, D]);
|
|
265
|
+
t.dispose();
|
|
266
|
+
const own = ops.contiguous(cutV);
|
|
267
|
+
cutV.dispose();
|
|
268
|
+
return own;
|
|
269
|
+
};
|
|
270
|
+
const c = new RotatingKVCache(this.maxSize);
|
|
271
|
+
const k = cut(this.keys);
|
|
272
|
+
const v = cut(this.values);
|
|
273
|
+
c.restoreState(k, v, this.offsetArr[i]!, k.shape[2]!);
|
|
274
|
+
return c;
|
|
275
|
+
}
|
|
276
|
+
|
|
232
277
|
/** Keep only `keep` rows along the batch axis (eviction). */
|
|
233
278
|
filter(keep: number[]): void {
|
|
234
279
|
if (this.keys && this.values) {
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
import { MlxArray } from "../mlx/array";
|
|
34
34
|
import { CompiledFunction } from "../mlx/compile";
|
|
35
35
|
import * as ops from "../mlx/ops";
|
|
36
|
-
import { perfKernelEnabled } from "./fused-decode-kernel";
|
|
37
36
|
import {
|
|
38
37
|
Gemma4Model,
|
|
39
38
|
KVCache,
|
|
@@ -208,10 +207,7 @@ function slotDesc(c: AnyCache, plan: DecodeStepPlan): SlotDesc {
|
|
|
208
207
|
* inside quantizedSdpa's dispatch). Offsets/capacities don't — they're
|
|
209
208
|
* array values or shapeless dims. */
|
|
210
209
|
function closureKey(descs: SlotDesc[]): string {
|
|
211
|
-
const flags =
|
|
212
|
-
`fd=${process.env.MLX_BUN_FUSED_DECODE === "1" ? 1 : 0}` +
|
|
213
|
-
`,nf=${process.env.MLX_BUN_NO_FUSED_SDPA === "1" ? 1 : 0}` +
|
|
214
|
-
`,pk=${perfKernelEnabled() ? 1 : 0}`;
|
|
210
|
+
const flags = `nf=${process.env.MLX_BUN_NO_FUSED_SDPA === "1" ? 1 : 0}`;
|
|
215
211
|
return descs.map((d) => `${d.kind}:${d.groupSize}:${d.bits}`).join(",") + "|" + flags;
|
|
216
212
|
}
|
|
217
213
|
|
|
@@ -272,12 +268,8 @@ function makeTraceFn(model: Gemma4Model, descs: SlotDesc[]) {
|
|
|
272
268
|
/** Non-mutating phase check (segmented mode decides layer placement
|
|
273
269
|
* BEFORE any cache bookkeeping runs). Mirrors prepareDecodeStep's
|
|
274
270
|
* outcome: rotating caches go ring once the next write lands at or past
|
|
275
|
-
* the window; everything else grows.
|
|
276
|
-
* layers run as JS layers regardless — the CustomKernel primitive has
|
|
277
|
-
* no output_shapes and cannot live inside a compiled closure. */
|
|
271
|
+
* the window; everything else grows. */
|
|
278
272
|
function decodePhase(c: AnyCache): "concat" | "ring" {
|
|
279
|
-
if (perfKernelEnabled() && (c instanceof QuantizedKVCache || c instanceof RotatingQuantizedKVCache))
|
|
280
|
-
return "concat";
|
|
281
273
|
if (c instanceof RotatingKVCache || c instanceof RotatingQuantizedKVCache)
|
|
282
274
|
return c.offset + 1 < c.maxSize ? "concat" : "ring";
|
|
283
275
|
return "concat";
|
|
@@ -563,9 +555,7 @@ export class CompiledDecode {
|
|
|
563
555
|
anyCaches
|
|
564
556
|
.map((c, i) => (phases[i] === "ring" ? `${slotDesc(c, plans[i]!).kind}:${slotDesc(c, plans[i]!).groupSize}:${slotDesc(c, plans[i]!).bits}` : "js"))
|
|
565
557
|
.join(",") +
|
|
566
|
-
`|
|
|
567
|
-
`,nf=${process.env.MLX_BUN_NO_FUSED_SDPA === "1" ? 1 : 0}` +
|
|
568
|
-
`,pk=${perfKernelEnabled() ? 1 : 0}`;
|
|
558
|
+
`|nf=${process.env.MLX_BUN_NO_FUSED_SDPA === "1" ? 1 : 0}`;
|
|
569
559
|
if (this.#broken.has(key)) throw new Error(`compiled decode: known-broken closure ${key}`);
|
|
570
560
|
|
|
571
561
|
let closures = this.#segClosures.get(key);
|
|
@@ -608,8 +598,7 @@ export class CompiledDecode {
|
|
|
608
598
|
// reverting offset (and ring index) — trim(1) semantics. The written
|
|
609
599
|
// KV row beyond the reverted offset is dead data the re-forward
|
|
610
600
|
// overwrites at the same position (updateAndFetch writes at offset/
|
|
611
|
-
// ringIdx), so post-recovery state equals a single clean write
|
|
612
|
-
// holds for the post-wrap quantized-under-perf-kernel case too: the
|
|
601
|
+
// ringIdx), so post-recovery state equals a single clean write; the
|
|
613
602
|
// in-place S=1 write slot is re-derived from the reverted ringIdx.
|
|
614
603
|
const stagedAdopts: { c: AnyCache; bufs: MlxArray[] }[] = [];
|
|
615
604
|
const committedJs: { c: AnyCache; offsetBefore: number }[] = [];
|
package/src/model/factory.ts
CHANGED
|
@@ -12,14 +12,15 @@ import { GENERATED } from "./generated";
|
|
|
12
12
|
import { MiniCPM5Model } from "./minicpm5";
|
|
13
13
|
import { Qwen35Model } from "./qwen3_5";
|
|
14
14
|
import { Qwen3Model } from "./qwen3";
|
|
15
|
+
import { Qwen3MoeModel } from "./qwen3-moe";
|
|
15
16
|
import { DiffusionGemmaModel } from "./diffusion-gemma";
|
|
16
17
|
import { UniversalDenseModel } from "./universal/dense";
|
|
17
18
|
import { genericArgsFor, GENERIC_MODEL_TYPES, remapModelType } from "./universal/archs";
|
|
18
|
-
import { isDiffusionGemmaConfig, isMiniCPM5Config, isQwen35Config, isQwen3Config } from "./support";
|
|
19
|
+
import { isDiffusionGemmaConfig, isMiniCPM5Config, isQwen35Config, isQwen3Config, isQwen3MoeConfig } from "./support";
|
|
19
20
|
|
|
20
21
|
export type RuntimeModel =
|
|
21
|
-
| Gemma4Model | MiniCPM5Model | Qwen35Model | Qwen3Model |
|
|
22
|
-
| UniversalDenseModel;
|
|
22
|
+
| Gemma4Model | MiniCPM5Model | Qwen35Model | Qwen3Model | Qwen3MoeModel
|
|
23
|
+
| DiffusionGemmaModel | UniversalDenseModel;
|
|
23
24
|
|
|
24
25
|
/** Dispatch ladder (docs/design/generic-model-support.md §3.3):
|
|
25
26
|
* dedicated → generated/monolith (gemma4*) → generic (Tier-0 universal)
|
|
@@ -33,8 +34,13 @@ export function createModel(weights: Weights, config: ModelConfig): RuntimeModel
|
|
|
33
34
|
if (isDiffusionGemmaConfig(config)) return new DiffusionGemmaModel(weights, config);
|
|
34
35
|
if (isMiniCPM5Config(config)) return new MiniCPM5Model(weights, config);
|
|
35
36
|
if (isQwen35Config(config)) return new Qwen35Model(weights, config);
|
|
37
|
+
if (isQwen3MoeConfig(config)) return new Qwen3MoeModel(weights, config);
|
|
36
38
|
if (isQwen3Config(config)) return new Qwen3Model(weights, config);
|
|
37
39
|
// 2. gemma4*: fingerprint-matched generated specialization, else monolith.
|
|
40
|
+
// Always prefer the model-specific generated file when its config
|
|
41
|
+
// fingerprint matches — the parity regime is selected by the OUTPUT-changing
|
|
42
|
+
// flags (kv-quant), which the generated forward already gates, NOT by
|
|
43
|
+
// detouring an optimized model through the generic monolith.
|
|
38
44
|
if (config.modelType.startsWith("gemma4")) {
|
|
39
45
|
const cls = GENERATED.get(configFingerprint(config)) ?? Gemma4Model;
|
|
40
46
|
return new cls(weights, config);
|
|
@@ -48,7 +54,7 @@ export function createModel(weights: Weights, config: ModelConfig): RuntimeModel
|
|
|
48
54
|
throw new Error(
|
|
49
55
|
`unsupported model_type "${config.modelType}"` +
|
|
50
56
|
(arch !== config.modelType ? ` (mlx-lm remaps it to "${arch}")` : "") +
|
|
51
|
-
` — targeted: gemma4*, diffusion_gemma, qwen3_5, qwen3, MiniCPM5;` +
|
|
57
|
+
` — targeted: gemma4*, diffusion_gemma, qwen3_5, qwen3, qwen3_moe, MiniCPM5;` +
|
|
52
58
|
` generic (Tier-0): ${[...GENERIC_MODEL_TYPES].sort().join(", ")}`,
|
|
53
59
|
);
|
|
54
60
|
}
|