mlx-bun 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -32
- package/docs/README.md +58 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +1535 -50
- package/docs/design/decode-speed-program.md +394 -1171
- package/docs/design/generic-model-support.md +195 -3
- package/docs/design/kv-cache.md +538 -225
- package/docs/design/speculative-decoding.md +552 -50
- package/docs/design/turboquant.md +140 -17
- package/docs/design/unified-engine-frontier-plan.md +259 -150
- package/docs/planning/PRODUCT_ROADMAP.md +21 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3537 -1112
- package/docs/reference/cli.md +102 -2
- package/docs/reference/distribution.md +45 -13
- package/docs/reference/environment.md +159 -13
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +28 -7
- package/docs/reference/server-api.md +257 -33
- package/docs/reference/server-config.md +372 -151
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +2 -2
- package/src/audio/audiotoolbox.ts +112 -0
- package/src/audio/conformer.ts +8 -0
- package/src/audio/mic-capture.ts +98 -0
- package/src/audio/silero-vad.ts +379 -0
- package/src/audio/whisper-decode.ts +774 -0
- package/src/audio/whisper-fast.ts +732 -0
- package/src/audio/whisper-format.ts +73 -0
- package/src/audio/whisper-mel.ts +185 -0
- package/src/audio/whisper-timing.ts +274 -0
- package/src/audio/whisper-tokenizer.ts +239 -0
- package/src/audio/whisper-transcribe.ts +410 -0
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +464 -236
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +107 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- package/src/backends/mlx/encoder-cache.ts +25 -0
- package/src/backends/mlx/fill-append.ts +21 -8
- package/src/backends/mlx/fill-group.ts +497 -0
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +122 -19
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +161 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/mixed-iteration.ts +62 -0
- package/src/backends/mlx/model-prompt.ts +46 -32
- package/src/backends/mlx/model-serving.ts +5 -4
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +100 -0
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +244 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/prompt-input.ts +29 -0
- package/src/backends/mlx/qwen-prompt-input.ts +40 -0
- package/src/backends/mlx/request-state-policy.ts +54 -0
- package/src/backends/mlx/rollback.ts +45 -4
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +51 -73
- package/src/backends/mlx/speculative-group.ts +438 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +21 -1
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
- package/src/cli.ts +333 -36
- package/src/contracts/execution.ts +21 -0
- package/src/contracts/object-cache.ts +8 -0
- package/src/contracts/pi-provider.ts +1 -1
- package/src/contracts/pi.ts +3 -1
- package/src/contracts/prefix-cache.ts +33 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/engine/execution-plan.ts +38 -25
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +32 -7
- package/src/fill/fill-session.ts +14 -3
- package/src/fit.ts +29 -15
- package/src/generate.ts +105 -195
- package/src/grammar.ts +13 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/prefill.ts +6 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +92 -12
- package/src/kv-store.ts +183 -93
- package/src/lab/README.md +5 -1
- package/src/lab/paged-kv/paged-attention.ts +117 -0
- package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
- package/src/lab/paged-kv/paged-kv.ts +126 -46
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +39 -1
- package/src/mlx/ffi.ts +19 -0
- package/src/mlx/normalized-argmax.ts +116 -0
- package/src/mlx/ops.ts +97 -10
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +12 -4
- package/src/model/batched-quant.ts +8 -2
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +76 -15
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +64 -22
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +94 -0
- package/src/model/compiled-decode.ts +7 -1
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +57 -0
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +183 -82
- package/src/model/gemma4.ts +116 -28
- package/src/model/generated/gemma4-12b.ts +4 -1
- package/src/model/generated/gemma4-26b.ts +4 -1
- package/src/model/generated/gemma4-e4b.ts +4 -1
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/kv-attention-view.ts +70 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +177 -75
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +4 -4
- package/src/model/turboquant-codec.ts +89 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/model/whisper.ts +435 -0
- package/src/native/mic_capture.swift +92 -0
- package/src/native-pack.ts +6 -5
- package/src/pi-web.ts +9 -1
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +306 -86
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +37 -25
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +19 -14
- package/src/serve/completion-executor.ts +9 -1
- package/src/serve/discovery-routes.ts +17 -1
- package/src/serve/generated-token-history.ts +57 -0
- package/src/serve/generation-gateway.ts +88 -38
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/model-host.ts +3 -4
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +6 -3
- package/src/serve/request-prep.ts +40 -25
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +185 -261
- package/src/spec/assistant-rows.ts +93 -0
- package/src/spec/assistant-source.ts +28 -18
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +12 -10
- package/src/spec/dflash-source.ts +28 -4
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/deepspec-module.ts +95 -120
- package/src/spec/dspark/module-dflash.ts +126 -170
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +115 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +230 -0
- package/src/spec/qwen-mtp-source.ts +84 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +73 -69
- package/src/spec/source.ts +93 -15
- package/src/spec/two-model-rows.ts +62 -0
- package/src/spec/two-model.ts +45 -103
- package/src/ssd-cache.ts +107 -36
- package/src/ssd-durability.ts +29 -27
- package/src/storage/host-buffer.ts +24 -0
- package/src/storage/kv-writer.ts +83 -0
- package/src/storage/kv-writer.worker.js +264 -0
- package/src/storage/retention-policy.ts +37 -0
- package/src/tiered-prompt-cache.ts +45 -0
- package/src/vision/embedder.ts +8 -0
- package/src/vision/prompt.ts +101 -65
- package/src/vision/qwen3vl-prompt.ts +101 -64
- package/src/vision/qwen3vl-tower.ts +11 -0
- package/src/vision/siglip.ts +8 -0
- package/src/web/app.html +12 -0
- package/src/web/app.js +217 -57
- package/src/web/src/chat.ts +4 -1
- package/src/web/src/composer.ts +3 -0
- package/src/web/src/voice.ts +141 -0
- package/src/web/sw.js +1 -1
- package/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
package/src/model/qwen3_5.ts
CHANGED
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
// shared maybeQuantizeKv path).
|
|
11
11
|
|
|
12
12
|
import type { ModelConfig } from "../config";
|
|
13
|
+
import { quantizedAppendAttention } from "./quantized-append-attention";
|
|
14
|
+
import { gatedDeltaState } from "./qwen3-delta-state";
|
|
13
15
|
import type { Weights } from "../weights";
|
|
16
|
+
import { runtimeFlag } from "../runtime-config";
|
|
17
|
+
import { mapTokenGroups, type TokenGroup } from "./token-groups";
|
|
14
18
|
import { MlxArray } from "../mlx/array";
|
|
15
19
|
import { Dtype, deviceArchitecture } from "../mlx/ffi";
|
|
16
20
|
import * as ops from "../mlx/ops";
|
|
@@ -23,7 +27,6 @@ import {
|
|
|
23
27
|
disposing,
|
|
24
28
|
KVCache,
|
|
25
29
|
LoraState,
|
|
26
|
-
QuantizedKVCache,
|
|
27
30
|
QuantizedEmbedding,
|
|
28
31
|
QuantizedLinear,
|
|
29
32
|
quantizedSdpa,
|
|
@@ -36,8 +39,8 @@ import { materializeCopy } from "../mlx/materialize";
|
|
|
36
39
|
import { gatedDeltaUpdate, SSMCache } from "./qwen3-delta";
|
|
37
40
|
import type { QwenConvolution } from "./qwen-conv";
|
|
38
41
|
import {
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
applyInterleavedRope, buildMropePositions, mropeInvFreq,
|
|
43
|
+
type MropeRequestState, type MropeForwardState,
|
|
41
44
|
} from "./qwen3-mrope";
|
|
42
45
|
|
|
43
46
|
const PREFIX = "language_model";
|
|
@@ -155,16 +158,20 @@ export class GatedDeltaNet {
|
|
|
155
158
|
this.normWeight = weights.tensor(`${prefix}.norm.weight`);
|
|
156
159
|
}
|
|
157
160
|
|
|
158
|
-
#convolve(qkv: MlxArray, state: MlxArray): [MlxArray, MlxArray] {
|
|
159
|
-
if (this.convolution)
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
#convolve(qkv: MlxArray, state: MlxArray, rowLengths?: readonly number[]): [MlxArray, MlxArray] {
|
|
162
|
+
if (this.convolution) {
|
|
163
|
+
const result = this.convolution(qkv, state, this.convWeight);
|
|
164
|
+
if (!rowLengths) return result;
|
|
165
|
+
using input = ops.concatAxis([state, qkv], 1);
|
|
166
|
+
const tail = this.#convTail(input, qkv.shape[1]!, rowLengths);
|
|
167
|
+
result[1].dispose();
|
|
168
|
+
return [result[0], tail];
|
|
169
|
+
}
|
|
170
|
+
const [, S, D] = qkv.shape as [number, number, number];
|
|
162
171
|
const input = ops.concatAxis([state, qkv], 1);
|
|
163
172
|
// MLX copy and contiguous can both alias the whole prefill buffer.
|
|
164
173
|
// Materialize only the tail so cache residency follows its logical size.
|
|
165
|
-
const
|
|
166
|
-
const tail = materializeCopy(view);
|
|
167
|
-
view.dispose();
|
|
174
|
+
const tail = this.#convTail(input, S, rowLengths);
|
|
168
175
|
const conv = ops.conv1d(input, this.convWeight, 1, 0, 1, D);
|
|
169
176
|
input.dispose();
|
|
170
177
|
const out = compiledSilu(conv);
|
|
@@ -172,7 +179,19 @@ export class GatedDeltaNet {
|
|
|
172
179
|
return [out, tail];
|
|
173
180
|
}
|
|
174
181
|
|
|
175
|
-
|
|
182
|
+
#convTail(input: MlxArray, processed: number, rowLengths?: readonly number[]): MlxArray {
|
|
183
|
+
const [B, , D] = input.shape as [number, number, number];
|
|
184
|
+
const nKeep = this.convKernel - 1;
|
|
185
|
+
if (rowLengths) {
|
|
186
|
+
using indices = ops.fromInt32(rowLengths.flatMap(length =>
|
|
187
|
+
Array.from({ length: nKeep }, (_, position) => length + position)), [B, nKeep, 1]);
|
|
188
|
+
return ops.takeAlongAxis(input, indices, 1);
|
|
189
|
+
}
|
|
190
|
+
using view = input.slice([0, processed, 0], [B, processed + nKeep, D]);
|
|
191
|
+
return materializeCopy(view);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
forward(x: MlxArray, cache: SSMCache, independentRows = false, mask?: MlxArray | null): MlxArray {
|
|
176
195
|
const [B, S] = x.shape as [number, number, number];
|
|
177
196
|
const convDim = this.keyDim * 2 + this.valueDim;
|
|
178
197
|
const nKeep = this.convKernel - 1;
|
|
@@ -194,7 +213,7 @@ export class GatedDeltaNet {
|
|
|
194
213
|
}
|
|
195
214
|
|
|
196
215
|
const t0 = prof ? performance.now() : 0;
|
|
197
|
-
|
|
216
|
+
let qkv = this.inProjQkv.forward(x, independentRows); // [B,S,convDim]
|
|
198
217
|
let z = this.inProjZ.forward(x, independentRows);
|
|
199
218
|
z = disposing(z, ops.reshape(z, [B, S, this.numVHeads, this.headVDim]));
|
|
200
219
|
const b = this.inProjB.forward(x, independentRows); // [B,S,numVHeads]
|
|
@@ -202,10 +221,19 @@ export class GatedDeltaNet {
|
|
|
202
221
|
if (prof) { ops.evalAll([qkv, z, b, a]); prof.proj = (prof.proj ?? 0) + performance.now() - t0; }
|
|
203
222
|
const tc = prof ? performance.now() : 0; // [B,S,numVHeads]
|
|
204
223
|
|
|
205
|
-
//
|
|
224
|
+
// The model shares one padding mask across recurrent layers. Direct layer
|
|
225
|
+
// callers can request the same cache-owned mask without a model wrapper.
|
|
226
|
+
using localMask = mask === undefined ? cache.prefillPadding?.makeMask(S) ?? null : null;
|
|
227
|
+
const ssmMask = mask ?? localMask;
|
|
228
|
+
if (ssmMask) {
|
|
229
|
+
using expanded = ops.reshape(ssmMask, [B, S, 1]);
|
|
230
|
+
using zero = MlxArray.fromBytesCopy(new Uint8Array(qkv.dtype === Dtype.float32 ? 4 : 2), [], qkv.dtype);
|
|
231
|
+
qkv = disposing(qkv, ops.where(expanded, qkv, zero));
|
|
232
|
+
}
|
|
233
|
+
// Convolution keeps each row's last real-token tail, including empty rows.
|
|
206
234
|
const convState =
|
|
207
235
|
cache.conv ?? ops.zeros([B, nKeep, convDim], x.dtype);
|
|
208
|
-
const [convOut, newConv] = this.#convolve(qkv, convState);
|
|
236
|
+
const [convOut, newConv] = this.#convolve(qkv, convState, cache.prefillPadding?.convolutionLengths(S));
|
|
209
237
|
if (!cache.conv) convState.dispose();
|
|
210
238
|
if (spec) spec.qkv = qkv;
|
|
211
239
|
else qkv.dispose();
|
|
@@ -236,7 +264,7 @@ export class GatedDeltaNet {
|
|
|
236
264
|
const tk = prof ? performance.now() : 0;
|
|
237
265
|
|
|
238
266
|
const [out, newState] = gatedDeltaUpdate(
|
|
239
|
-
q, k, v, a, b, this.aLog, this.dtBias, cache.recurrent,
|
|
267
|
+
q, k, v, a, b, this.aLog, this.dtBias, cache.recurrent, ssmMask,
|
|
240
268
|
);
|
|
241
269
|
if (prof) { ops.evalAll([out]); prof.kernel = (prof.kernel ?? 0) + performance.now() - tk; }
|
|
242
270
|
const to = prof ? performance.now() : 0;
|
|
@@ -275,18 +303,21 @@ export class GatedDeltaNet {
|
|
|
275
303
|
* recurrent state is BIT-EXACTLY what a forward over only the accepted
|
|
276
304
|
* prefix would have produced. Output projections (z / out_proj) are state-
|
|
277
305
|
* free and skipped: only the states matter here. */
|
|
278
|
-
#replaySpecPrefix(cache: SSMCache, keep: number): void {
|
|
306
|
+
#replaySpecPrefix(cache: SSMCache, keep: number | readonly number[]): void {
|
|
279
307
|
const r = cache.specRound;
|
|
280
|
-
if (!r || !r.qkv || !r.a || !r.b)
|
|
308
|
+
if (!r || !r.qkv || !r.a || !r.b)
|
|
309
|
+
throw new Error("GatedDeltaNet replay without recorded round inputs");
|
|
281
310
|
const B = r.qkv.shape[0]!;
|
|
311
|
+
const count = typeof keep === "number" ? keep : Math.max(...keep);
|
|
312
|
+
const rowLengths = typeof keep === "number" ? undefined : keep;
|
|
282
313
|
const convDim = this.keyDim * 2 + this.valueDim;
|
|
283
314
|
const nKeep = this.convKernel - 1;
|
|
284
315
|
|
|
285
|
-
const qkvPfxView = r.qkv.slice([0, 0, 0], [B,
|
|
316
|
+
const qkvPfxView = r.qkv.slice([0, 0, 0], [B, count, convDim]);
|
|
286
317
|
const qkvPfx = ops.contiguous(qkvPfxView);
|
|
287
318
|
qkvPfxView.dispose();
|
|
288
319
|
const convState = cache.conv ?? ops.zeros([B, nKeep, convDim], qkvPfx.dtype);
|
|
289
|
-
const [convOut, newConv] = this.#convolve(qkvPfx, convState);
|
|
320
|
+
const [convOut, newConv] = this.#convolve(qkvPfx, convState, rowLengths);
|
|
290
321
|
if (!cache.conv) convState.dispose();
|
|
291
322
|
qkvPfx.dispose();
|
|
292
323
|
cache.conv?.dispose();
|
|
@@ -296,36 +327,45 @@ export class GatedDeltaNet {
|
|
|
296
327
|
convOut, [this.keyDim, 2 * this.keyDim], -1,
|
|
297
328
|
) as [MlxArray, MlxArray, MlxArray];
|
|
298
329
|
convOut.dispose();
|
|
299
|
-
let q = ops.reshape(qFlat, [B,
|
|
330
|
+
let q = rowLengths ? null : ops.reshape(qFlat, [B, count, this.numKHeads, this.headKDim]);
|
|
300
331
|
qFlat.dispose();
|
|
301
|
-
let k = ops.reshape(kFlat, [B,
|
|
332
|
+
let k = ops.reshape(kFlat, [B, count, this.numKHeads, this.headKDim]);
|
|
302
333
|
kFlat.dispose();
|
|
303
|
-
const v = disposing(vFlat, ops.reshape(vFlat, [B,
|
|
334
|
+
const v = disposing(vFlat, ops.reshape(vFlat, [B, count, this.numVHeads, this.headVDim]));
|
|
304
335
|
const invScale = Math.pow(this.headKDim, -0.5);
|
|
305
|
-
|
|
306
|
-
|
|
336
|
+
if (q) {
|
|
337
|
+
q = disposing(q, ops.rmsNorm(q, null, 1e-6));
|
|
338
|
+
q = disposing(q, ops.mulScalar(q, invScale * invScale));
|
|
339
|
+
}
|
|
307
340
|
k = disposing(k, ops.rmsNorm(k, null, 1e-6));
|
|
308
341
|
k = disposing(k, ops.mulScalar(k, invScale));
|
|
309
342
|
|
|
310
|
-
const aPfxView = r.a.slice([0, 0, 0], [B,
|
|
343
|
+
const aPfxView = r.a.slice([0, 0, 0], [B, count, this.numVHeads]);
|
|
311
344
|
const aPfx = ops.contiguous(aPfxView);
|
|
312
345
|
aPfxView.dispose();
|
|
313
|
-
const bPfxView = r.b.slice([0, 0, 0], [B,
|
|
346
|
+
const bPfxView = r.b.slice([0, 0, 0], [B, count, this.numVHeads]);
|
|
314
347
|
const bPfx = ops.contiguous(bPfxView);
|
|
315
348
|
bPfxView.dispose();
|
|
316
349
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
350
|
+
let newState: MlxArray;
|
|
351
|
+
if (rowLengths) {
|
|
352
|
+
// Unequal prefixes need a row-length-aware recurrence. The state-only
|
|
353
|
+
// kernel omits query/output work. Uniform replay retains its measured
|
|
354
|
+
// kernel until the separate optimization demonstrates a serving win.
|
|
355
|
+
newState = gatedDeltaState(k, v, aPfx, bPfx, this.aLog, this.dtBias, cache.recurrent, rowLengths);
|
|
356
|
+
} else {
|
|
357
|
+
const [output, state] = gatedDeltaUpdate(q!, k, v, aPfx, bPfx, this.aLog, this.dtBias, cache.recurrent);
|
|
358
|
+
output.dispose(); newState = state;
|
|
359
|
+
}
|
|
360
|
+
q?.dispose();
|
|
321
361
|
k.dispose();
|
|
322
362
|
v.dispose();
|
|
323
363
|
aPfx.dispose();
|
|
324
364
|
bPfx.dispose();
|
|
325
|
-
y.dispose(); // only the state advance matters on the rollback path
|
|
326
365
|
cache.recurrent?.dispose();
|
|
327
366
|
cache.recurrent = newState;
|
|
328
|
-
cache.advance(keep);
|
|
367
|
+
if (typeof keep === "number") cache.advance(keep);
|
|
368
|
+
else cache.advanceRows(keep);
|
|
329
369
|
}
|
|
330
370
|
|
|
331
371
|
private rmsNormGated(hidden: MlxArray, gate: MlxArray): MlxArray {
|
|
@@ -369,7 +409,8 @@ export class Qwen3Attention {
|
|
|
369
409
|
this.kNorm = new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps);
|
|
370
410
|
}
|
|
371
411
|
|
|
372
|
-
forward(x: MlxArray, mask: Mask, cache: Cache, independentRows = false
|
|
412
|
+
forward(x: MlxArray, mask: Mask, cache: Cache, independentRows = false,
|
|
413
|
+
mrope: MropeForwardState | null = null): MlxArray {
|
|
373
414
|
const [B, L] = x.shape as [number, number, number];
|
|
374
415
|
|
|
375
416
|
// q_proj emits 2× head_dim per head → split into queries + gate.
|
|
@@ -396,11 +437,10 @@ export class Qwen3Attention {
|
|
|
396
437
|
// position as ropeOffsetArr (rows have different prompt lengths); the
|
|
397
438
|
// dynamic-offset kernel is the same fast::rope, bit-exact vs the static
|
|
398
439
|
// form (tests/unit/compile.test.ts). Serial lane: scalar offset, unchanged.
|
|
399
|
-
//
|
|
400
|
-
// forward: 3D interleaved positions via the manual apply — the reference
|
|
440
|
+
// Media forwards supply their own 3D interleaved positions. The reference
|
|
401
441
|
// never uses the fused fast-rope kernel when position_ids are supplied,
|
|
402
442
|
// so this IS the oracle's own arithmetic. Text-only stays on ops.rope.
|
|
403
|
-
const mr =
|
|
443
|
+
const mr = mrope;
|
|
404
444
|
const offArr = (cache as { ropeOffsetArr?: MlxArray }).ropeOffsetArr;
|
|
405
445
|
if (mr) {
|
|
406
446
|
q = disposing(q, applyInterleavedRope(q, mr));
|
|
@@ -415,11 +455,18 @@ export class Qwen3Attention {
|
|
|
415
455
|
}
|
|
416
456
|
|
|
417
457
|
let attn: MlxArray;
|
|
418
|
-
|
|
419
|
-
|
|
458
|
+
const quantized = cache.quantizedAttention;
|
|
459
|
+
if (cache.attentionState) {
|
|
460
|
+
const view = cache.attentionState.appendAndFetch(k, v);
|
|
461
|
+
k.dispose(); v.dispose();
|
|
462
|
+
try { attn = view.attend(q, this.scale, mask, independentRows); } finally { view.dispose(); }
|
|
463
|
+
} else if (quantized) {
|
|
464
|
+
const [keys, values] = quantized.updateAndFetchQuantized(k, v);
|
|
420
465
|
k.dispose();
|
|
421
466
|
v.dispose();
|
|
422
|
-
attn =
|
|
467
|
+
attn = independentRows && L > 1
|
|
468
|
+
? quantizedAppendAttention(q, keys, values, this.scale, quantized.groupSize, quantized.bits)
|
|
469
|
+
: quantizedSdpa(q, keys, values, this.scale, mask, quantized.groupSize, quantized.bits);
|
|
423
470
|
disposeTriple(keys);
|
|
424
471
|
disposeTriple(values);
|
|
425
472
|
} else {
|
|
@@ -517,23 +564,27 @@ export class Qwen3Layer {
|
|
|
517
564
|
this.postAttnNorm = new RMSNorm(weights.tensor(`${prefix}.post_attention_layernorm.weight`), config.text.rmsNormEps);
|
|
518
565
|
}
|
|
519
566
|
|
|
520
|
-
forward(x: MlxArray, faMask: Mask, cache: Cache, independentRows = false
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
567
|
+
forward(x: MlxArray, faMask: Mask, cache: Cache, independentRows = false, ssmMask?: MlxArray | null,
|
|
568
|
+
mrope: MropeForwardState | null = null): MlxArray {
|
|
569
|
+
using h = this.forwardAttn(x, faMask, cache, independentRows, ssmMask, mrope);
|
|
570
|
+
return this.forwardMlp(h, independentRows);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
forwardAttn(x: MlxArray, faMask: Mask, cache: Cache, independentRows = false, ssmMask?: MlxArray | null,
|
|
574
|
+
mrope: MropeForwardState | null = null): MlxArray {
|
|
575
|
+
using xn = this.inputNorm.forward(x);
|
|
576
|
+
using r = this.isLinear
|
|
577
|
+
? this.linearAttn!.forward(xn, cache as SSMCache, independentRows, ssmMask)
|
|
578
|
+
: this.selfAttn!.forward(xn, faMask, cache, independentRows, mrope);
|
|
579
|
+
return ops.add(x, r);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
forwardMlp(h: MlxArray, independentRows = false): MlxArray {
|
|
583
|
+
using hn = this.postAttnNorm.forward(h);
|
|
529
584
|
// RMSNorm allocates an aligned row-contiguous output. Pass that layout
|
|
530
585
|
// proof so Trellis can match native small-prefill matmul without an eval.
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const out = ops.add(h, m);
|
|
534
|
-
h.dispose();
|
|
535
|
-
m.dispose();
|
|
536
|
-
return out;
|
|
586
|
+
using m = this.mlp.forward(hn, true, independentRows);
|
|
587
|
+
return ops.add(h, m);
|
|
537
588
|
}
|
|
538
589
|
}
|
|
539
590
|
|
|
@@ -627,10 +678,16 @@ export class Qwen35Model {
|
|
|
627
678
|
return null;
|
|
628
679
|
}
|
|
629
680
|
return {
|
|
630
|
-
|
|
681
|
+
affineKvBits: [4, 8],
|
|
682
|
+
turboQuantFormats: [{ kBits: 8, vBits: 3 }],
|
|
683
|
+
maxChunkSize: (state: readonly Cache[], rows = 1) => rows === 1 ? qwenAppendChunkSize(state[0]!.offset) : 1,
|
|
631
684
|
forwardHidden: (ids: MlxArray, cache: Cache[]): MlxArray => {
|
|
685
|
+
// Wider cohorts retain their ordinary one-position numerical graph.
|
|
686
|
+
// The specialized multi-position append remains qualified at B=1.
|
|
687
|
+
if (ids.shape.length === 2 && ids.shape[0]! > 1 && ids.shape[1] === 1)
|
|
688
|
+
return this.forwardHidden(ids, cache);
|
|
632
689
|
if (ids.shape.length !== 2 || ids.shape[0] !== 1 || ids.shape[1]! > 4)
|
|
633
|
-
throw new Error("Qwen committed-token append supports one row
|
|
690
|
+
throw new Error("Qwen committed-token append supports one position per shared row or up to four positions at B=1");
|
|
634
691
|
const hidden = this.embed.encode(ids);
|
|
635
692
|
return this.forwardLayers(hidden, cache, true);
|
|
636
693
|
},
|
|
@@ -642,13 +699,62 @@ export class Qwen35Model {
|
|
|
642
699
|
return this.forwardLayers(h, cache);
|
|
643
700
|
}
|
|
644
701
|
|
|
702
|
+
/** Borrowed request positions, including different positions for each row. */
|
|
703
|
+
forwardHiddenAtPositions(ids: MlxArray, cache: Cache[], positions: MlxArray): MlxArray {
|
|
704
|
+
return this.forwardLayers(this.embed.encode(ids), cache, false, positions);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
forwardEmbeddingsAtPositions(embeds: MlxArray, cache: Cache[], positions: MlxArray): MlxArray {
|
|
708
|
+
return this.forwardLayers(ops.contiguous(embeds), cache, false, positions);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Attention and DeltaNet retain disjoint row state. Only tokenwise MLP
|
|
712
|
+
* work is eligible for packing; verification can preserve its geometry. */
|
|
713
|
+
forwardHiddenMixed(work: readonly TokenGroup[]): MlxArray[] {
|
|
714
|
+
if (work.length === 1 && !work[0]!.captureLayer) return [this.forwardHidden(work[0]!.ids, work[0]!.cache)];
|
|
715
|
+
const groups: Array<{ h: MlxArray; mask?: Mask; ssmMask?: MlxArray | null }> = [];
|
|
716
|
+
const results: MlxArray[] = [];
|
|
717
|
+
const pack = runtimeFlag("MLX_BUN_MIXED_PACKED_MLP", true);
|
|
718
|
+
const bounded = work.some(group => group.ids.shape[1]! > TRELLIS_MATVEC_MAX_M);
|
|
719
|
+
try {
|
|
720
|
+
for (const { ids, cache } of work) {
|
|
721
|
+
const group: typeof groups[number] = { h: this.embed.encode(ids) }; groups.push(group);
|
|
722
|
+
group.mask = cache[this.faIdx]!.makeMask(ids.shape[1]!, null);
|
|
723
|
+
group.ssmMask = (cache[0] as SSMCache).prefillPadding?.makeMask(ids.shape[1]!);
|
|
724
|
+
}
|
|
725
|
+
for (const [i, layer] of this.layers.entries()) {
|
|
726
|
+
const mids: MlxArray[] = [];
|
|
727
|
+
try {
|
|
728
|
+
for (const [row, group] of groups.entries())
|
|
729
|
+
mids.push(layer.forwardAttn(group.h, group.mask!, work[row]!.cache[i]!, false, group.ssmMask));
|
|
730
|
+
const outputs = mapTokenGroups(work, mids, hidden => layer.forwardMlp(hidden), pack);
|
|
731
|
+
for (const [row, group] of groups.entries()) { group.h.dispose(); group.h = outputs[row]!; }
|
|
732
|
+
// Materialize every layer's recurrent tail as well as the residual.
|
|
733
|
+
// It is not a dependency of h and otherwise pins prefill buffers.
|
|
734
|
+
if (bounded) {
|
|
735
|
+
const caches = work.map(group => group.cache[i]!);
|
|
736
|
+
const state = caches.map(cache => cache.state());
|
|
737
|
+
try { ops.evalAll([...groups.map(group => group.h), ...state.flat()]); }
|
|
738
|
+
finally { for (const [row, cache] of caches.entries())
|
|
739
|
+
if (cache.stateNeedsDispose) for (const value of state[row]!) value.dispose(); }
|
|
740
|
+
}
|
|
741
|
+
for (const [row, group] of groups.entries()) work[row]!.captureLayer?.(i, group.h);
|
|
742
|
+
} finally { for (const mid of mids) mid.dispose(); }
|
|
743
|
+
}
|
|
744
|
+
for (const [row, group] of groups.entries()) {
|
|
745
|
+
const hidden = this.finalNorm.forward(group.h); results.push(hidden);
|
|
746
|
+
work[row]!.captureLayer?.(this.layers.length, hidden);
|
|
747
|
+
}
|
|
748
|
+
return results;
|
|
749
|
+
} catch (error) { for (const result of results) result.dispose(); throw error; }
|
|
750
|
+
finally { for (const group of groups) { group.h.dispose(); group.mask?.arr?.dispose(); group.ssmMask?.dispose(); } }
|
|
751
|
+
}
|
|
752
|
+
|
|
645
753
|
/** Active vision mRoPE request state (serial lane; set by the generation
|
|
646
754
|
* gateway around a vision request's run, null for text-only — which keeps
|
|
647
|
-
* the bit-exact fast-rope path).
|
|
648
|
-
*
|
|
755
|
+
* the bit-exact fast-rope path). The explicit serial executor scopes this value.
|
|
756
|
+
* Shared media forwards pass request-owned positions directly. */
|
|
649
757
|
mrope: MropeRequestState | null = null;
|
|
650
|
-
/** Serving's memory-pressure check, scoped to the serial request. */
|
|
651
|
-
prefillMemoryGuard: (() => void) | null = null;
|
|
652
758
|
#mropeInvFreq: MlxArray | null = null;
|
|
653
759
|
|
|
654
760
|
/** Vision prefill: spliced input embeddings [1, L, H] (image features
|
|
@@ -682,23 +788,23 @@ export class Qwen35Model {
|
|
|
682
788
|
tap.captured.set(i, copy);
|
|
683
789
|
}
|
|
684
790
|
|
|
685
|
-
protected forwardLayers(h0: MlxArray, cache: Cache[], independentRows = false
|
|
791
|
+
protected forwardLayers(h0: MlxArray, cache: Cache[], independentRows = false,
|
|
792
|
+
positions?: MlxArray): MlxArray {
|
|
686
793
|
const L = h0.shape[1]!;
|
|
687
794
|
// One full-attention mask shared by all full layers (same offset); linear
|
|
688
795
|
// layers see no ssm mask at B=1.
|
|
689
796
|
const faMask = cache[this.faIdx]!.makeMask(L, null);
|
|
797
|
+
using ssmMask = (cache[0] as SSMCache).prefillPadding?.makeMask(L) ?? null;
|
|
690
798
|
// Vision requests: one interleaved-mRoPE cos/sin table per forward,
|
|
691
799
|
// shared by all 12 full-attention layers (positions are layer-invariant).
|
|
692
800
|
let mropeFwd: ReturnType<typeof buildMropePositions> | null = null;
|
|
693
|
-
if (this.mrope) {
|
|
801
|
+
if (positions || this.mrope) {
|
|
694
802
|
const t = this.config.text;
|
|
695
803
|
const ropeDims = Math.trunc(t.headDim * t.partialRotaryFactor);
|
|
696
804
|
const base = t.ropeParameters.full_attention?.ropeTheta ?? 10000;
|
|
697
805
|
this.#mropeInvFreq ??= mropeInvFreq(ropeDims, base);
|
|
698
|
-
mropeFwd =
|
|
699
|
-
this.mrope
|
|
700
|
-
);
|
|
701
|
-
setActiveMrope(mropeFwd);
|
|
806
|
+
mropeFwd = positions ? { posIds: positions, invFreq: this.#mropeInvFreq, rotaryDims: ropeDims }
|
|
807
|
+
: buildMropePositions(this.mrope!, cache[this.faIdx]!.offset, L, this.#mropeInvFreq, ropeDims);
|
|
702
808
|
}
|
|
703
809
|
let h: MlxArray | null = h0;
|
|
704
810
|
const prof = (globalThis as Record<string, unknown>).__deltaProf as
|
|
@@ -706,14 +812,13 @@ export class Qwen35Model {
|
|
|
706
812
|
| undefined;
|
|
707
813
|
try {
|
|
708
814
|
for (let i = 0; i < this.layers.length; i++) {
|
|
709
|
-
if (L > 1) this.prefillMemoryGuard?.();
|
|
710
815
|
const tl = prof ? performance.now() : 0;
|
|
711
|
-
const next = this.layers[i]!.forward(h, faMask, cache[i]!, independentRows);
|
|
816
|
+
const next = this.layers[i]!.forward(h, faMask, cache[i]!, independentRows, ssmMask, mropeFwd);
|
|
712
817
|
h.dispose();
|
|
713
818
|
h = next;
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
819
|
+
if (L > TRELLIS_MATVEC_MAX_M) {
|
|
820
|
+
// Affine prefills also need bounded evaluation: a retained prefix
|
|
821
|
+
// history can leave too little room for the entire deferred graph.
|
|
717
822
|
// Materialize the cache outputs too. In particular, the tiny
|
|
718
823
|
// copied conv tail is not a dependency of `h`; leaving it lazy
|
|
719
824
|
// pins the whole prefill conv buffer until the chunk ends.
|
|
@@ -732,10 +837,7 @@ export class Qwen35Model {
|
|
|
732
837
|
h = null; // consumed — the finally must not double-free
|
|
733
838
|
return out;
|
|
734
839
|
} finally {
|
|
735
|
-
if (mropeFwd)
|
|
736
|
-
setActiveMrope(null);
|
|
737
|
-
mropeFwd.posIds.dispose();
|
|
738
|
-
}
|
|
840
|
+
if (mropeFwd && !positions) mropeFwd.posIds.dispose();
|
|
739
841
|
// A mid-loop layer throw must not strand the mask array or the
|
|
740
842
|
// in-flight [1,L,H] residual (2026-08-18 review).
|
|
741
843
|
faMask.arr?.dispose();
|
|
@@ -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
|
+
}
|