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/qwen3-delta.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// default (use_kernel = not training), and its float accumulation / simd_sum
|
|
6
6
|
// reduction order differ from the pure-ops fallback — so BIT-EXACT parity with
|
|
7
7
|
// mlx-lm requires the SAME kernel, dispatched with the SAME grid/threadgroup.
|
|
8
|
-
// We port the non-vectorized
|
|
9
|
-
//
|
|
8
|
+
// We port the non-vectorized variant (g.ndim == 3). Padded prefill selects
|
|
9
|
+
// the oracle masked specialization; unpadded execution retains its kernel.
|
|
10
10
|
//
|
|
11
11
|
// Numerics (must match the reference dtypes exactly — mlx infers the kernel's
|
|
12
12
|
// pointer element types from the input arrays):
|
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
// GQA is handled inside the kernel (hk_idx = hv_idx / (Hv/Hk)); q/k stay at Hk.
|
|
21
21
|
|
|
22
22
|
import { MlxArray } from "../mlx/array";
|
|
23
|
+
import { materializeCopy } from "../mlx/materialize";
|
|
23
24
|
import { Dtype } from "../mlx/ffi";
|
|
24
25
|
import { MetalKernel } from "../mlx/metal-kernel";
|
|
25
26
|
import { CompiledFunction } from "../mlx/compile";
|
|
26
27
|
import * as ops from "../mlx/ops";
|
|
28
|
+
import type { SsmPrefillPadding } from "./ssm-prefill-padding";
|
|
27
29
|
import type { Cache, Mask } from "./gemma4-base";
|
|
28
30
|
|
|
29
31
|
// Verbatim body of mlx-lm's gated_delta_step (has_mask=False, vectorized=False),
|
|
@@ -104,7 +106,15 @@ const SOURCE = String.raw`
|
|
|
104
106
|
`;
|
|
105
107
|
|
|
106
108
|
let kernel: MetalKernel | null = null;
|
|
107
|
-
|
|
109
|
+
let maskedKernel: MetalKernel | null = null;
|
|
110
|
+
function getKernel(masked = false): MetalKernel {
|
|
111
|
+
if (masked) return maskedKernel ??= new MetalKernel({
|
|
112
|
+
name: "gated_delta_step_mask",
|
|
113
|
+
inputNames: ["q", "k", "v", "g", "beta", "state_in", "Tin", "mask"],
|
|
114
|
+
outputNames: ["y", "state_out"],
|
|
115
|
+
source: SOURCE.replace("if (true)", "if (mask[b_idx * T + t])"),
|
|
116
|
+
ensureRowContiguous: true,
|
|
117
|
+
});
|
|
108
118
|
if (!kernel)
|
|
109
119
|
kernel = new MetalKernel({
|
|
110
120
|
name: "gated_delta_step",
|
|
@@ -154,7 +164,7 @@ export function computeG(aLog: MlxArray, a: MlxArray, dtBias: MlxArray): MlxArra
|
|
|
154
164
|
* state: [B, Hv, Dv, Dk] f32 (or null → zeros) */
|
|
155
165
|
export function gatedDeltaUpdate(
|
|
156
166
|
q: MlxArray, k: MlxArray, v: MlxArray, a: MlxArray, b: MlxArray,
|
|
157
|
-
aLog: MlxArray, dtBias: MlxArray, state: MlxArray | null,
|
|
167
|
+
aLog: MlxArray, dtBias: MlxArray, state: MlxArray | null, mask: MlxArray | null = null,
|
|
158
168
|
): [MlxArray, MlxArray] {
|
|
159
169
|
const [B, , Hk, Dk] = q.shape as [number, number, number, number];
|
|
160
170
|
const [, , Hv, Dv] = v.shape as [number, number, number, number];
|
|
@@ -171,7 +181,9 @@ export function gatedDeltaUpdate(
|
|
|
171
181
|
|
|
172
182
|
const T = q.shape[1]!;
|
|
173
183
|
const tArr = MlxArray.fromInt32(new Int32Array([T]), [1]);
|
|
174
|
-
const
|
|
184
|
+
const inputs = [q, k, v, g, beta, stateIn, tArr];
|
|
185
|
+
if (mask) inputs.push(mask);
|
|
186
|
+
const [y, stateOut] = getKernel(mask !== null).apply(inputs, {
|
|
175
187
|
outputs: [
|
|
176
188
|
{ shape: [B, T, Hv, Dv], dtype: q.dtype },
|
|
177
189
|
{ shape: [B, Hv, Dv, Dk], dtype: Dtype.float32 },
|
|
@@ -207,6 +219,7 @@ export interface SsmSpecRound {
|
|
|
207
219
|
prevConv: MlxArray | null;
|
|
208
220
|
prevRecurrent: MlxArray | null;
|
|
209
221
|
prevOffset: number;
|
|
222
|
+
prevOffsets: number[] | null;
|
|
210
223
|
/** Pre-conv in_proj output [B,S,convDim] — position-local. */
|
|
211
224
|
qkv: MlxArray | null;
|
|
212
225
|
/** Gate/beta projections [B,S,Hv] — position-local. */
|
|
@@ -216,14 +229,15 @@ export interface SsmSpecRound {
|
|
|
216
229
|
S: number;
|
|
217
230
|
/** Layer-bound prefix replay: called AFTER the snapshot is restored onto
|
|
218
231
|
* the cache; advances conv/recurrent/offset by `keep` tokens. */
|
|
219
|
-
replay: ((cache: SSMCache, keep: number) => void) | null;
|
|
232
|
+
replay: ((cache: SSMCache, keep: number | readonly number[]) => void) | null;
|
|
220
233
|
}
|
|
221
234
|
|
|
222
235
|
export class SSMCache implements Cache {
|
|
223
236
|
conv: MlxArray | null = null;
|
|
224
237
|
recurrent: MlxArray | null = null;
|
|
225
238
|
offset = 0;
|
|
226
|
-
|
|
239
|
+
prefillPadding: SsmPrefillPadding | null = null;
|
|
240
|
+
/** Live speculative verify round; null outside a verify transaction. */
|
|
227
241
|
specRound: SsmSpecRound | null = null;
|
|
228
242
|
/** Per-row token coverage (batch lane only; null on serial B=1 caches,
|
|
229
243
|
* where `offset` IS the row's count). mlx-lm's ArraysCache tracks no
|
|
@@ -248,15 +262,13 @@ export class SSMCache implements Cache {
|
|
|
248
262
|
throw new Error("SSMCache has no KV updateAndFetch (gated-DeltaNet layer)");
|
|
249
263
|
}
|
|
250
264
|
|
|
251
|
-
makeMask(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// batched decode feeds one real token per row — so unlike mlx-lm's
|
|
255
|
-
// left-padded batch-prefill there is never a pad position to mask.
|
|
256
|
-
return { mode: "", arr: null };
|
|
265
|
+
makeMask(N: number, _windowSize: number | null): Mask {
|
|
266
|
+
const arr = this.prefillPadding?.makeMask(N) ?? null;
|
|
267
|
+
return { mode: arr ? "array" : "", arr };
|
|
257
268
|
}
|
|
258
269
|
|
|
259
270
|
advance(n: number): void {
|
|
271
|
+
if (this.prefillPadding) { this.advanceRows(this.prefillPadding.advance(n)); return; }
|
|
260
272
|
this.offset += n;
|
|
261
273
|
// Batched rows step together (one forward advances every row), so the
|
|
262
274
|
// per-row counts move in lockstep; they DIFFER only in their merge-time
|
|
@@ -264,6 +276,13 @@ export class SSMCache implements Cache {
|
|
|
264
276
|
if (this.offsets) for (let i = 0; i < this.offsets.length; i++) this.offsets[i]! += n;
|
|
265
277
|
}
|
|
266
278
|
|
|
279
|
+
/** Methods may retain different verified prefixes for each active request. */
|
|
280
|
+
advanceRows(counts: readonly number[]): void {
|
|
281
|
+
if (!this.offsets) { this.offset += counts[0]!; return; }
|
|
282
|
+
for (let row = 0; row < this.offsets.length; row++) this.offsets[row]! += counts[row]!;
|
|
283
|
+
this.offset = Math.max(...this.offsets);
|
|
284
|
+
}
|
|
285
|
+
|
|
267
286
|
state(): MlxArray[] {
|
|
268
287
|
const out: MlxArray[] = [];
|
|
269
288
|
if (this.conv) out.push(this.conv);
|
|
@@ -279,7 +298,7 @@ export class SSMCache implements Cache {
|
|
|
279
298
|
throw new Error("SSMCache is not trimmable");
|
|
280
299
|
}
|
|
281
300
|
|
|
282
|
-
// --- speculative verify-round support
|
|
301
|
+
// --- speculative verify-round support -----------------------------------
|
|
283
302
|
// The serve loop arms a round before the verify forward and resolves it
|
|
284
303
|
// after the accept walk. The layer's forward, seeing an armed round, hands
|
|
285
304
|
// its replaced state slots to the round instead of disposing them, records
|
|
@@ -294,6 +313,7 @@ export class SSMCache implements Cache {
|
|
|
294
313
|
prevConv: null,
|
|
295
314
|
prevRecurrent: null,
|
|
296
315
|
prevOffset: this.offset,
|
|
316
|
+
prevOffsets: this.offsets ? [...this.offsets] : null,
|
|
297
317
|
qkv: null,
|
|
298
318
|
a: null,
|
|
299
319
|
b: null,
|
|
@@ -306,12 +326,12 @@ export class SSMCache implements Cache {
|
|
|
306
326
|
this.#dropSpecRound();
|
|
307
327
|
}
|
|
308
328
|
|
|
309
|
-
specRoundRollback(keep: number): void {
|
|
329
|
+
specRoundRollback(keep: number | readonly number[]): void {
|
|
310
330
|
const r = this.specRound;
|
|
311
331
|
if (!r) throw new Error("SSMCache.specRoundRollback without an armed round");
|
|
312
332
|
if (r.armed)
|
|
313
333
|
throw new Error("SSMCache.specRoundRollback before the verify forward recorded");
|
|
314
|
-
if (keep < 0 || keep > r.S)
|
|
334
|
+
if (typeof keep === "number" ? keep < 0 || keep > r.S : keep.some(count => count < 0 || count > r.S))
|
|
315
335
|
throw new Error(`SSMCache.specRoundRollback keep=${keep} outside window S=${r.S}`);
|
|
316
336
|
// Restore the pre-round snapshot (ownership moves back to the cache).
|
|
317
337
|
this.conv?.dispose();
|
|
@@ -321,9 +341,8 @@ export class SSMCache implements Cache {
|
|
|
321
341
|
r.prevConv = null;
|
|
322
342
|
r.prevRecurrent = null;
|
|
323
343
|
this.offset = r.prevOffset;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (keep > 0) {
|
|
344
|
+
this.offsets = r.prevOffsets;
|
|
345
|
+
if (typeof keep === "number" ? keep > 0 : keep.some(count => count > 0)) {
|
|
327
346
|
if (!r.replay) throw new Error("SSMCache.specRoundRollback with no recorded replay");
|
|
328
347
|
r.replay(this, keep); // advances conv/recurrent/offset by `keep`
|
|
329
348
|
}
|
|
@@ -347,6 +366,7 @@ export class SSMCache implements Cache {
|
|
|
347
366
|
this.recurrent?.dispose();
|
|
348
367
|
this.conv = null;
|
|
349
368
|
this.recurrent = null;
|
|
369
|
+
this.prefillPadding = null;
|
|
350
370
|
}
|
|
351
371
|
|
|
352
372
|
// --- batch-lane dynamic-B ops (BatchScheduler only) ----------------------
|
|
@@ -386,25 +406,30 @@ export class SSMCache implements Cache {
|
|
|
386
406
|
|
|
387
407
|
/** Evict rows not in `keep` (ascending row indices), in place. */
|
|
388
408
|
filter(keep: number[]): void {
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
409
|
+
if (this.conv && this.recurrent) {
|
|
410
|
+
const idx = ops.fromInt32(keep, [keep.length]);
|
|
411
|
+
const conv = ops.takeAxis(this.conv, idx, 0);
|
|
412
|
+
const recurrent = ops.takeAxis(this.recurrent, idx, 0);
|
|
413
|
+
idx.dispose();
|
|
414
|
+
this.conv.dispose();
|
|
415
|
+
this.recurrent.dispose();
|
|
416
|
+
this.conv = conv;
|
|
417
|
+
this.recurrent = recurrent;
|
|
418
|
+
}
|
|
419
|
+
this.prefillPadding?.filter(keep);
|
|
420
|
+
if (this.offsets) {
|
|
421
|
+
this.offsets = keep.map((i) => this.offsets![i]!);
|
|
422
|
+
this.offset = Math.max(0, ...this.offsets);
|
|
423
|
+
}
|
|
399
424
|
}
|
|
400
425
|
|
|
401
426
|
filterRows(keep: readonly number[]): void { this.filter([...keep]); }
|
|
402
427
|
|
|
403
428
|
/** Row `i` as a fresh SERIAL cache — port of mlx-lm ArraysCache.extract
|
|
404
429
|
* (cache.py:673-676: `cache.cache = [c[idx : idx + 1] for c in self.cache]`,
|
|
405
|
-
* a B-axis slice of every state slot).
|
|
406
|
-
*
|
|
407
|
-
*
|
|
430
|
+
* a B-axis slice of every state slot). A proper subset gets compact storage
|
|
431
|
+
* so retaining one row does not pin the other rows. A singleton retains an
|
|
432
|
+
* independent handle over its complete allocation. Offset =
|
|
408
433
|
* the row's OWN coverage, not the shared max. */
|
|
409
434
|
extractRow(i: number): SSMCache {
|
|
410
435
|
if (!this.conv || !this.recurrent)
|
|
@@ -414,10 +439,8 @@ export class SSMCache implements Cache {
|
|
|
414
439
|
lo[0] = i;
|
|
415
440
|
const hi = [...a.shape];
|
|
416
441
|
hi[0] = i + 1;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
view.dispose();
|
|
420
|
-
return own;
|
|
442
|
+
using view = a.slice(lo, hi);
|
|
443
|
+
return a.shape[0] === 1 ? ops.contiguous(view) : materializeCopy(view);
|
|
421
444
|
};
|
|
422
445
|
const out = new SSMCache();
|
|
423
446
|
out.conv = cut(this.conv);
|
package/src/model/qwen3_5.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// shared maybeQuantizeKv path).
|
|
11
11
|
|
|
12
12
|
import type { ModelConfig } from "../config";
|
|
13
|
+
import { gatedDeltaState } from "./qwen3-delta-state";
|
|
13
14
|
import type { Weights } from "../weights";
|
|
14
15
|
import { MlxArray } from "../mlx/array";
|
|
15
16
|
import { Dtype, deviceArchitecture } from "../mlx/ffi";
|
|
@@ -23,7 +24,6 @@ import {
|
|
|
23
24
|
disposing,
|
|
24
25
|
KVCache,
|
|
25
26
|
LoraState,
|
|
26
|
-
QuantizedKVCache,
|
|
27
27
|
QuantizedEmbedding,
|
|
28
28
|
QuantizedLinear,
|
|
29
29
|
quantizedSdpa,
|
|
@@ -155,16 +155,20 @@ export class GatedDeltaNet {
|
|
|
155
155
|
this.normWeight = weights.tensor(`${prefix}.norm.weight`);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
#convolve(qkv: MlxArray, state: MlxArray): [MlxArray, MlxArray] {
|
|
159
|
-
if (this.convolution)
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
#convolve(qkv: MlxArray, state: MlxArray, rowLengths?: readonly number[]): [MlxArray, MlxArray] {
|
|
159
|
+
if (this.convolution) {
|
|
160
|
+
const result = this.convolution(qkv, state, this.convWeight);
|
|
161
|
+
if (!rowLengths) return result;
|
|
162
|
+
using input = ops.concatAxis([state, qkv], 1);
|
|
163
|
+
const tail = this.#convTail(input, qkv.shape[1]!, rowLengths);
|
|
164
|
+
result[1].dispose();
|
|
165
|
+
return [result[0], tail];
|
|
166
|
+
}
|
|
167
|
+
const [, S, D] = qkv.shape as [number, number, number];
|
|
162
168
|
const input = ops.concatAxis([state, qkv], 1);
|
|
163
169
|
// MLX copy and contiguous can both alias the whole prefill buffer.
|
|
164
170
|
// Materialize only the tail so cache residency follows its logical size.
|
|
165
|
-
const
|
|
166
|
-
const tail = materializeCopy(view);
|
|
167
|
-
view.dispose();
|
|
171
|
+
const tail = this.#convTail(input, S, rowLengths);
|
|
168
172
|
const conv = ops.conv1d(input, this.convWeight, 1, 0, 1, D);
|
|
169
173
|
input.dispose();
|
|
170
174
|
const out = compiledSilu(conv);
|
|
@@ -172,7 +176,19 @@ export class GatedDeltaNet {
|
|
|
172
176
|
return [out, tail];
|
|
173
177
|
}
|
|
174
178
|
|
|
175
|
-
|
|
179
|
+
#convTail(input: MlxArray, processed: number, rowLengths?: readonly number[]): MlxArray {
|
|
180
|
+
const [B, , D] = input.shape as [number, number, number];
|
|
181
|
+
const nKeep = this.convKernel - 1;
|
|
182
|
+
if (rowLengths) {
|
|
183
|
+
using indices = ops.fromInt32(rowLengths.flatMap(length =>
|
|
184
|
+
Array.from({ length: nKeep }, (_, position) => length + position)), [B, nKeep, 1]);
|
|
185
|
+
return ops.takeAlongAxis(input, indices, 1);
|
|
186
|
+
}
|
|
187
|
+
using view = input.slice([0, processed, 0], [B, processed + nKeep, D]);
|
|
188
|
+
return materializeCopy(view);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
forward(x: MlxArray, cache: SSMCache, independentRows = false, mask?: MlxArray | null): MlxArray {
|
|
176
192
|
const [B, S] = x.shape as [number, number, number];
|
|
177
193
|
const convDim = this.keyDim * 2 + this.valueDim;
|
|
178
194
|
const nKeep = this.convKernel - 1;
|
|
@@ -194,7 +210,7 @@ export class GatedDeltaNet {
|
|
|
194
210
|
}
|
|
195
211
|
|
|
196
212
|
const t0 = prof ? performance.now() : 0;
|
|
197
|
-
|
|
213
|
+
let qkv = this.inProjQkv.forward(x, independentRows); // [B,S,convDim]
|
|
198
214
|
let z = this.inProjZ.forward(x, independentRows);
|
|
199
215
|
z = disposing(z, ops.reshape(z, [B, S, this.numVHeads, this.headVDim]));
|
|
200
216
|
const b = this.inProjB.forward(x, independentRows); // [B,S,numVHeads]
|
|
@@ -202,10 +218,19 @@ export class GatedDeltaNet {
|
|
|
202
218
|
if (prof) { ops.evalAll([qkv, z, b, a]); prof.proj = (prof.proj ?? 0) + performance.now() - t0; }
|
|
203
219
|
const tc = prof ? performance.now() : 0; // [B,S,numVHeads]
|
|
204
220
|
|
|
205
|
-
//
|
|
221
|
+
// The model shares one padding mask across recurrent layers. Direct layer
|
|
222
|
+
// callers can request the same cache-owned mask without a model wrapper.
|
|
223
|
+
using localMask = mask === undefined ? cache.prefillPadding?.makeMask(S) ?? null : null;
|
|
224
|
+
const ssmMask = mask ?? localMask;
|
|
225
|
+
if (ssmMask) {
|
|
226
|
+
using expanded = ops.reshape(ssmMask, [B, S, 1]);
|
|
227
|
+
using zero = MlxArray.fromBytesCopy(new Uint8Array(qkv.dtype === Dtype.float32 ? 4 : 2), [], qkv.dtype);
|
|
228
|
+
qkv = disposing(qkv, ops.where(expanded, qkv, zero));
|
|
229
|
+
}
|
|
230
|
+
// Convolution keeps each row's last real-token tail, including empty rows.
|
|
206
231
|
const convState =
|
|
207
232
|
cache.conv ?? ops.zeros([B, nKeep, convDim], x.dtype);
|
|
208
|
-
const [convOut, newConv] = this.#convolve(qkv, convState);
|
|
233
|
+
const [convOut, newConv] = this.#convolve(qkv, convState, cache.prefillPadding?.convolutionLengths(S));
|
|
209
234
|
if (!cache.conv) convState.dispose();
|
|
210
235
|
if (spec) spec.qkv = qkv;
|
|
211
236
|
else qkv.dispose();
|
|
@@ -236,7 +261,7 @@ export class GatedDeltaNet {
|
|
|
236
261
|
const tk = prof ? performance.now() : 0;
|
|
237
262
|
|
|
238
263
|
const [out, newState] = gatedDeltaUpdate(
|
|
239
|
-
q, k, v, a, b, this.aLog, this.dtBias, cache.recurrent,
|
|
264
|
+
q, k, v, a, b, this.aLog, this.dtBias, cache.recurrent, ssmMask,
|
|
240
265
|
);
|
|
241
266
|
if (prof) { ops.evalAll([out]); prof.kernel = (prof.kernel ?? 0) + performance.now() - tk; }
|
|
242
267
|
const to = prof ? performance.now() : 0;
|
|
@@ -275,18 +300,21 @@ export class GatedDeltaNet {
|
|
|
275
300
|
* recurrent state is BIT-EXACTLY what a forward over only the accepted
|
|
276
301
|
* prefix would have produced. Output projections (z / out_proj) are state-
|
|
277
302
|
* free and skipped: only the states matter here. */
|
|
278
|
-
#replaySpecPrefix(cache: SSMCache, keep: number): void {
|
|
303
|
+
#replaySpecPrefix(cache: SSMCache, keep: number | readonly number[]): void {
|
|
279
304
|
const r = cache.specRound;
|
|
280
|
-
if (!r || !r.qkv || !r.a || !r.b)
|
|
305
|
+
if (!r || !r.qkv || !r.a || !r.b)
|
|
306
|
+
throw new Error("GatedDeltaNet replay without recorded round inputs");
|
|
281
307
|
const B = r.qkv.shape[0]!;
|
|
308
|
+
const count = typeof keep === "number" ? keep : Math.max(...keep);
|
|
309
|
+
const rowLengths = typeof keep === "number" ? undefined : keep;
|
|
282
310
|
const convDim = this.keyDim * 2 + this.valueDim;
|
|
283
311
|
const nKeep = this.convKernel - 1;
|
|
284
312
|
|
|
285
|
-
const qkvPfxView = r.qkv.slice([0, 0, 0], [B,
|
|
313
|
+
const qkvPfxView = r.qkv.slice([0, 0, 0], [B, count, convDim]);
|
|
286
314
|
const qkvPfx = ops.contiguous(qkvPfxView);
|
|
287
315
|
qkvPfxView.dispose();
|
|
288
316
|
const convState = cache.conv ?? ops.zeros([B, nKeep, convDim], qkvPfx.dtype);
|
|
289
|
-
const [convOut, newConv] = this.#convolve(qkvPfx, convState);
|
|
317
|
+
const [convOut, newConv] = this.#convolve(qkvPfx, convState, rowLengths);
|
|
290
318
|
if (!cache.conv) convState.dispose();
|
|
291
319
|
qkvPfx.dispose();
|
|
292
320
|
cache.conv?.dispose();
|
|
@@ -296,36 +324,45 @@ export class GatedDeltaNet {
|
|
|
296
324
|
convOut, [this.keyDim, 2 * this.keyDim], -1,
|
|
297
325
|
) as [MlxArray, MlxArray, MlxArray];
|
|
298
326
|
convOut.dispose();
|
|
299
|
-
let q = ops.reshape(qFlat, [B,
|
|
327
|
+
let q = rowLengths ? null : ops.reshape(qFlat, [B, count, this.numKHeads, this.headKDim]);
|
|
300
328
|
qFlat.dispose();
|
|
301
|
-
let k = ops.reshape(kFlat, [B,
|
|
329
|
+
let k = ops.reshape(kFlat, [B, count, this.numKHeads, this.headKDim]);
|
|
302
330
|
kFlat.dispose();
|
|
303
|
-
const v = disposing(vFlat, ops.reshape(vFlat, [B,
|
|
331
|
+
const v = disposing(vFlat, ops.reshape(vFlat, [B, count, this.numVHeads, this.headVDim]));
|
|
304
332
|
const invScale = Math.pow(this.headKDim, -0.5);
|
|
305
|
-
|
|
306
|
-
|
|
333
|
+
if (q) {
|
|
334
|
+
q = disposing(q, ops.rmsNorm(q, null, 1e-6));
|
|
335
|
+
q = disposing(q, ops.mulScalar(q, invScale * invScale));
|
|
336
|
+
}
|
|
307
337
|
k = disposing(k, ops.rmsNorm(k, null, 1e-6));
|
|
308
338
|
k = disposing(k, ops.mulScalar(k, invScale));
|
|
309
339
|
|
|
310
|
-
const aPfxView = r.a.slice([0, 0, 0], [B,
|
|
340
|
+
const aPfxView = r.a.slice([0, 0, 0], [B, count, this.numVHeads]);
|
|
311
341
|
const aPfx = ops.contiguous(aPfxView);
|
|
312
342
|
aPfxView.dispose();
|
|
313
|
-
const bPfxView = r.b.slice([0, 0, 0], [B,
|
|
343
|
+
const bPfxView = r.b.slice([0, 0, 0], [B, count, this.numVHeads]);
|
|
314
344
|
const bPfx = ops.contiguous(bPfxView);
|
|
315
345
|
bPfxView.dispose();
|
|
316
346
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
347
|
+
let newState: MlxArray;
|
|
348
|
+
if (rowLengths) {
|
|
349
|
+
// Unequal prefixes need a row-length-aware recurrence. The state-only
|
|
350
|
+
// kernel omits query/output work. Uniform replay retains its measured
|
|
351
|
+
// kernel until the separate optimization demonstrates a serving win.
|
|
352
|
+
newState = gatedDeltaState(k, v, aPfx, bPfx, this.aLog, this.dtBias, cache.recurrent, rowLengths);
|
|
353
|
+
} else {
|
|
354
|
+
const [output, state] = gatedDeltaUpdate(q!, k, v, aPfx, bPfx, this.aLog, this.dtBias, cache.recurrent);
|
|
355
|
+
output.dispose(); newState = state;
|
|
356
|
+
}
|
|
357
|
+
q?.dispose();
|
|
321
358
|
k.dispose();
|
|
322
359
|
v.dispose();
|
|
323
360
|
aPfx.dispose();
|
|
324
361
|
bPfx.dispose();
|
|
325
|
-
y.dispose(); // only the state advance matters on the rollback path
|
|
326
362
|
cache.recurrent?.dispose();
|
|
327
363
|
cache.recurrent = newState;
|
|
328
|
-
cache.advance(keep);
|
|
364
|
+
if (typeof keep === "number") cache.advance(keep);
|
|
365
|
+
else cache.advanceRows(keep);
|
|
329
366
|
}
|
|
330
367
|
|
|
331
368
|
private rmsNormGated(hidden: MlxArray, gate: MlxArray): MlxArray {
|
|
@@ -415,11 +452,16 @@ export class Qwen3Attention {
|
|
|
415
452
|
}
|
|
416
453
|
|
|
417
454
|
let attn: MlxArray;
|
|
418
|
-
|
|
419
|
-
|
|
455
|
+
const quantized = cache.quantizedAttention;
|
|
456
|
+
if (cache.attentionState) {
|
|
457
|
+
const view = cache.attentionState.appendAndFetch(k, v);
|
|
458
|
+
k.dispose(); v.dispose();
|
|
459
|
+
try { attn = view.attend(q, this.scale, mask); } finally { view.dispose(); }
|
|
460
|
+
} else if (quantized) {
|
|
461
|
+
const [keys, values] = quantized.updateAndFetchQuantized(k, v);
|
|
420
462
|
k.dispose();
|
|
421
463
|
v.dispose();
|
|
422
|
-
attn = quantizedSdpa(q, keys, values, this.scale, mask,
|
|
464
|
+
attn = quantizedSdpa(q, keys, values, this.scale, mask, quantized.groupSize, quantized.bits);
|
|
423
465
|
disposeTriple(keys);
|
|
424
466
|
disposeTriple(values);
|
|
425
467
|
} else {
|
|
@@ -517,10 +559,10 @@ export class Qwen3Layer {
|
|
|
517
559
|
this.postAttnNorm = new RMSNorm(weights.tensor(`${prefix}.post_attention_layernorm.weight`), config.text.rmsNormEps);
|
|
518
560
|
}
|
|
519
561
|
|
|
520
|
-
forward(x: MlxArray, faMask: Mask, cache: Cache, independentRows = false): MlxArray {
|
|
562
|
+
forward(x: MlxArray, faMask: Mask, cache: Cache, independentRows = false, ssmMask?: MlxArray | null): MlxArray {
|
|
521
563
|
const xn = this.inputNorm.forward(x);
|
|
522
564
|
const r = this.isLinear
|
|
523
|
-
? this.linearAttn!.forward(xn, cache as SSMCache, independentRows)
|
|
565
|
+
? this.linearAttn!.forward(xn, cache as SSMCache, independentRows, ssmMask)
|
|
524
566
|
: this.selfAttn!.forward(xn, faMask, cache, independentRows);
|
|
525
567
|
xn.dispose();
|
|
526
568
|
const h = ops.add(x, r);
|
|
@@ -647,8 +689,6 @@ export class Qwen35Model {
|
|
|
647
689
|
* the bit-exact fast-rope path). While set, forwardLayers installs the
|
|
648
690
|
* per-forward interleaved cos/sin consumed by every full-attn layer. */
|
|
649
691
|
mrope: MropeRequestState | null = null;
|
|
650
|
-
/** Serving's memory-pressure check, scoped to the serial request. */
|
|
651
|
-
prefillMemoryGuard: (() => void) | null = null;
|
|
652
692
|
#mropeInvFreq: MlxArray | null = null;
|
|
653
693
|
|
|
654
694
|
/** Vision prefill: spliced input embeddings [1, L, H] (image features
|
|
@@ -687,6 +727,7 @@ export class Qwen35Model {
|
|
|
687
727
|
// One full-attention mask shared by all full layers (same offset); linear
|
|
688
728
|
// layers see no ssm mask at B=1.
|
|
689
729
|
const faMask = cache[this.faIdx]!.makeMask(L, null);
|
|
730
|
+
using ssmMask = (cache[0] as SSMCache).prefillPadding?.makeMask(L) ?? null;
|
|
690
731
|
// Vision requests: one interleaved-mRoPE cos/sin table per forward,
|
|
691
732
|
// shared by all 12 full-attention layers (positions are layer-invariant).
|
|
692
733
|
let mropeFwd: ReturnType<typeof buildMropePositions> | null = null;
|
|
@@ -706,14 +747,13 @@ export class Qwen35Model {
|
|
|
706
747
|
| undefined;
|
|
707
748
|
try {
|
|
708
749
|
for (let i = 0; i < this.layers.length; i++) {
|
|
709
|
-
if (L > 1) this.prefillMemoryGuard?.();
|
|
710
750
|
const tl = prof ? performance.now() : 0;
|
|
711
|
-
const next = this.layers[i]!.forward(h, faMask, cache[i]!, independentRows);
|
|
751
|
+
const next = this.layers[i]!.forward(h, faMask, cache[i]!, independentRows, ssmMask);
|
|
712
752
|
h.dispose();
|
|
713
753
|
h = next;
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
754
|
+
if (L > TRELLIS_MATVEC_MAX_M) {
|
|
755
|
+
// Affine prefills also need bounded evaluation: a retained prefix
|
|
756
|
+
// history can leave too little room for the entire deferred graph.
|
|
717
757
|
// Materialize the cache outputs too. In particular, the tiny
|
|
718
758
|
// copied conv tail is not a dependency of `h`; leaving it lazy
|
|
719
759
|
// pins the whole prefill conv buffer until the chunk ends.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { KvDonorAttention } from "./gemma4-base";
|
|
2
|
+
import { BatchedRotatingState } from "./batched-rotating-state";
|
|
3
|
+
import { BatchedRotatingQuantCache } from "./batched-rotating-quant";
|
|
4
|
+
import { plainRowStorage, quantizedRowStorage, temporalStorageView } from "./batched-row-storage";
|
|
5
|
+
import { quantizedDonorAttention } from "./kv-attention-view";
|
|
6
|
+
import { decodedKvDonorAttention } from "./decoded-kv-donor";
|
|
7
|
+
import type { RotatingRing } from "./rotating-row-transaction";
|
|
8
|
+
|
|
9
|
+
/** Capture the same chronological donor rectangle for plain and encoded rings. */
|
|
10
|
+
export function captureRotatingDonorAttention(ring: RotatingRing): KvDonorAttention {
|
|
11
|
+
const position = new BatchedRotatingState(ring.maxSize, []);
|
|
12
|
+
position.restore(ring.positionSnapshot);
|
|
13
|
+
const offsets = position.offsets.map((_, row) => position.validOffset(row));
|
|
14
|
+
const ends = offsets.map((offset, row) => position.activeLength - (position.offsets[row]! - offset));
|
|
15
|
+
// Pending prefill keeps a shared physical rectangle, including when mixed
|
|
16
|
+
// rows are captured separately. Cropping from each row's valid end would
|
|
17
|
+
// give siblings different widths and can discard an earlier valid window.
|
|
18
|
+
const from = position.hasPendingPadding ? 0 : Math.max(0, position.activeLength - ring.maxSize);
|
|
19
|
+
const range = { from, to: position.activeLength };
|
|
20
|
+
const validity = { offsets,
|
|
21
|
+
starts: position.leftPad.map((pad, row) => Math.max(0, pad - from, ends[row]! - from - ring.maxSize)),
|
|
22
|
+
ends: ends.map(end => Math.max(0, end - from)) };
|
|
23
|
+
if (ring instanceof BatchedRotatingQuantCache) {
|
|
24
|
+
const keys = temporalStorageView(quantizedRowStorage, ring.keys!, position, range);
|
|
25
|
+
let values: import("../mlx/ops").QuantizedTensor;
|
|
26
|
+
try { values = temporalStorageView(quantizedRowStorage, ring.values!, position, range); }
|
|
27
|
+
catch (error) { quantizedRowStorage.dispose(keys); throw error; }
|
|
28
|
+
return quantizedDonorAttention(keys, values, validity, ring.groupSize, ring.bits);
|
|
29
|
+
}
|
|
30
|
+
const keys = temporalStorageView(plainRowStorage, ring.keys!, position, range);
|
|
31
|
+
let values: import("../mlx/array").MlxArray;
|
|
32
|
+
try { values = temporalStorageView(plainRowStorage, ring.values!, position, range); }
|
|
33
|
+
catch (error) { keys.dispose(); throw error; }
|
|
34
|
+
return decodedKvDonorAttention({ keys, values, ...validity });
|
|
35
|
+
}
|