mlx-bun 0.4.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 +55 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +220 -7
- package/docs/design/decode-speed-program.md +121 -66
- package/docs/design/generic-model-support.md +116 -0
- package/docs/design/kv-cache.md +380 -211
- package/docs/design/speculative-decoding.md +165 -26
- package/docs/design/turboquant.md +20 -7
- package/docs/design/unified-engine-frontier-plan.md +120 -13
- package/docs/planning/PRODUCT_ROADMAP.md +5 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3463 -3489
- package/docs/reference/cli.md +62 -1
- package/docs/reference/distribution.md +29 -4
- package/docs/reference/environment.md +104 -8
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +17 -5
- package/docs/reference/server-api.md +181 -11
- package/docs/reference/server-config.md +232 -105
- 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/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +111 -27
- package/src/backends/mlx/cache-layout.ts +3 -3
- package/src/backends/mlx/continuation-persistence.ts +5 -4
- 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/gateway-binding.ts +56 -12
- package/src/backends/mlx/kv-maintenance.ts +30 -2
- 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 +4 -3
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +15 -4
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +57 -13
- 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 +18 -5
- package/src/backends/mlx/rollback.ts +6 -3
- package/src/backends/mlx/serial-executor.ts +16 -7
- package/src/backends/mlx/speculative-group.ts +105 -26
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/target-layout-capability.ts +1 -1
- package/src/backends/mlx/transitioning-kv-rows.ts +8 -0
- package/src/cli.ts +322 -0
- package/src/contracts/execution.ts +10 -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 +13 -3
- package/src/contracts/scheduling.ts +9 -0
- package/src/engine/execution-plan.ts +23 -12
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/scheduler.ts +22 -2
- package/src/fill/fill-session.ts +14 -3
- package/src/generate.ts +28 -22
- package/src/grammar.ts +13 -0
- package/src/inference/prefill.ts +6 -0
- package/src/kv-scheme.ts +22 -0
- package/src/kv-store.ts +115 -79
- 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 +33 -5
- package/src/lab/paged-kv/paged-kv.ts +108 -42
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +37 -0
- 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/batched-turboquant-kv.ts +6 -5
- package/src/model/delayed-quantized-kv.ts +3 -3
- package/src/model/delayed-rotating-quantized-kv.ts +1 -1
- package/src/model/delayed-turboquant-kv.ts +6 -4
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-row.ts +1 -1
- package/src/model/gemma4-base.ts +25 -6
- package/src/model/gemma4.ts +82 -2
- package/src/model/kv-attention-view.ts +4 -1
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +99 -37
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +2 -2
- package/src/model/turboquant-codec.ts +6 -0
- 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/prompt-cache.ts +265 -43
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +5 -4
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +17 -12
- 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 +52 -11
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +2 -0
- package/src/serve/request-prep.ts +38 -21
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +132 -164
- package/src/spec/assistant-rows.ts +1 -0
- package/src/spec/deepspec-rows.ts +1 -1
- package/src/spec/dflash-source.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +1 -0
- package/src/spec/ngram-source.ts +27 -5
- package/src/spec/projected-draft-rows.ts +2 -2
- package/src/spec/qwen-mtp-rows.ts +11 -0
- package/src/spec/qwen-mtp-source.ts +3 -0
- package/src/spec/serve-loop.ts +5 -3
- package/src/spec/source.ts +14 -1
- package/src/spec/two-model-rows.ts +1 -0
- package/src/spec/two-model.ts +2 -2
- package/src/ssd-cache.ts +76 -17
- package/src/ssd-durability.ts +20 -22
- 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 +160 -0
- 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/docs/{planning → archive/planning}/release-notes-v0.4.0.md +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import { quantizedSdpa } from "./gemma4-base";
|
|
4
|
+
|
|
5
|
+
/** Committed positions retain single-token attention arithmetic. Projection
|
|
6
|
+
* work can still share the span; each query sees its own causal KV prefix. */
|
|
7
|
+
export function quantizedAppendAttention(
|
|
8
|
+
q: MlxArray, keys: ops.QuantizedTensor, values: ops.QuantizedTensor,
|
|
9
|
+
scale: number, groupSize: number, bits: number,
|
|
10
|
+
): MlxArray {
|
|
11
|
+
const [batch, heads, length, dim] = q.shape as [number, number, number, number];
|
|
12
|
+
const prefix = keys.packed.shape[2]! - length;
|
|
13
|
+
const outputs: MlxArray[] = [];
|
|
14
|
+
const trim = (tensor: ops.QuantizedTensor, end: number) => {
|
|
15
|
+
const slice = (a: MlxArray) => a.slice([0, 0, 0, 0],
|
|
16
|
+
[a.shape[0]!, a.shape[1]!, end, a.shape[3]!]);
|
|
17
|
+
return { packed: slice(tensor.packed), scales: slice(tensor.scales), biases: slice(tensor.biases) };
|
|
18
|
+
};
|
|
19
|
+
try {
|
|
20
|
+
for (let row = 0; row < length; row++) {
|
|
21
|
+
using queryView = q.slice([0, 0, row, 0], [batch, heads, row + 1, dim]);
|
|
22
|
+
using query = ops.contiguous(queryView);
|
|
23
|
+
const k = trim(keys, prefix + row + 1), v = trim(values, prefix + row + 1);
|
|
24
|
+
try { outputs.push(quantizedSdpa(query, k, v, scale, { mode: "", arr: null }, groupSize, bits)); }
|
|
25
|
+
finally {
|
|
26
|
+
for (const t of [k, v]) { t.packed.dispose(); t.scales.dispose(); t.biases.dispose(); }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return ops.concatAxis(outputs, 2);
|
|
30
|
+
} finally { for (const output of outputs) output.dispose(); }
|
|
31
|
+
}
|
package/src/model/qwen3-mrope.ts
CHANGED
|
@@ -30,20 +30,13 @@ export interface MropeRequestState {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface MropeForwardState {
|
|
33
|
-
/** Effective positions [3,
|
|
33
|
+
/** Effective positions [3, B, L] int32 for the current forward window. */
|
|
34
34
|
posIds: MlxArray;
|
|
35
35
|
/** Borrowed model-owned f32 inv_freq (mropeInvFreq) — not disposed here. */
|
|
36
36
|
invFreq: MlxArray;
|
|
37
37
|
rotaryDims: number;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** Active per-forward mRoPE cos/sin, read by Qwen3Attention.forward. Null on
|
|
41
|
-
* the text-only path (serial lane only — vision requests never batch). */
|
|
42
|
-
export let activeMrope: MropeForwardState | null = null;
|
|
43
|
-
export function setActiveMrope(s: MropeForwardState | null): void {
|
|
44
|
-
activeMrope = s;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
40
|
const MROPE_SECTION = [11, 11, 10] as const;
|
|
48
41
|
|
|
49
42
|
/** Interleaved frequency→stream selector (rope_utils
|
|
@@ -85,6 +78,11 @@ export function buildMropePositions(
|
|
|
85
78
|
state: MropeRequestState, offset: number, L: number,
|
|
86
79
|
invFreq: MlxArray, rotaryDims: number,
|
|
87
80
|
): MropeForwardState {
|
|
81
|
+
return { posIds: mropePositionIds(state, offset, L), invFreq, rotaryDims };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Request-owned prompt positions; the returned native array belongs to the caller. */
|
|
85
|
+
export function mropePositionIds(state: MropeRequestState, offset: number, L: number): MlxArray {
|
|
88
86
|
const P = state.positions;
|
|
89
87
|
const promptLen = P[0].length;
|
|
90
88
|
const joint = new Int32Array(3 * L);
|
|
@@ -94,7 +92,7 @@ export function buildMropePositions(
|
|
|
94
92
|
joint[axis * L + l] = idx < promptLen ? P[axis]![idx]! : idx + state.delta;
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
|
-
return
|
|
95
|
+
return MlxArray.fromInt32(joint, [3, 1, L]);
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
// The reference language-side apply is a CUSTOM METAL KERNEL (rope_utils
|
package/src/model/qwen3_5.ts
CHANGED
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
// shared maybeQuantizeKv path).
|
|
11
11
|
|
|
12
12
|
import type { ModelConfig } from "../config";
|
|
13
|
+
import { quantizedAppendAttention } from "./quantized-append-attention";
|
|
13
14
|
import { gatedDeltaState } from "./qwen3-delta-state";
|
|
14
15
|
import type { Weights } from "../weights";
|
|
16
|
+
import { runtimeFlag } from "../runtime-config";
|
|
17
|
+
import { mapTokenGroups, type TokenGroup } from "./token-groups";
|
|
15
18
|
import { MlxArray } from "../mlx/array";
|
|
16
19
|
import { Dtype, deviceArchitecture } from "../mlx/ffi";
|
|
17
20
|
import * as ops from "../mlx/ops";
|
|
@@ -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";
|
|
@@ -406,7 +409,8 @@ export class Qwen3Attention {
|
|
|
406
409
|
this.kNorm = new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps);
|
|
407
410
|
}
|
|
408
411
|
|
|
409
|
-
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 {
|
|
410
414
|
const [B, L] = x.shape as [number, number, number];
|
|
411
415
|
|
|
412
416
|
// q_proj emits 2× head_dim per head → split into queries + gate.
|
|
@@ -433,11 +437,10 @@ export class Qwen3Attention {
|
|
|
433
437
|
// position as ropeOffsetArr (rows have different prompt lengths); the
|
|
434
438
|
// dynamic-offset kernel is the same fast::rope, bit-exact vs the static
|
|
435
439
|
// form (tests/unit/compile.test.ts). Serial lane: scalar offset, unchanged.
|
|
436
|
-
//
|
|
437
|
-
// forward: 3D interleaved positions via the manual apply — the reference
|
|
440
|
+
// Media forwards supply their own 3D interleaved positions. The reference
|
|
438
441
|
// never uses the fused fast-rope kernel when position_ids are supplied,
|
|
439
442
|
// so this IS the oracle's own arithmetic. Text-only stays on ops.rope.
|
|
440
|
-
const mr =
|
|
443
|
+
const mr = mrope;
|
|
441
444
|
const offArr = (cache as { ropeOffsetArr?: MlxArray }).ropeOffsetArr;
|
|
442
445
|
if (mr) {
|
|
443
446
|
q = disposing(q, applyInterleavedRope(q, mr));
|
|
@@ -456,12 +459,14 @@ export class Qwen3Attention {
|
|
|
456
459
|
if (cache.attentionState) {
|
|
457
460
|
const view = cache.attentionState.appendAndFetch(k, v);
|
|
458
461
|
k.dispose(); v.dispose();
|
|
459
|
-
try { attn = view.attend(q, this.scale, mask); } finally { view.dispose(); }
|
|
462
|
+
try { attn = view.attend(q, this.scale, mask, independentRows); } finally { view.dispose(); }
|
|
460
463
|
} else if (quantized) {
|
|
461
464
|
const [keys, values] = quantized.updateAndFetchQuantized(k, v);
|
|
462
465
|
k.dispose();
|
|
463
466
|
v.dispose();
|
|
464
|
-
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);
|
|
465
470
|
disposeTriple(keys);
|
|
466
471
|
disposeTriple(values);
|
|
467
472
|
} else {
|
|
@@ -559,23 +564,27 @@ export class Qwen3Layer {
|
|
|
559
564
|
this.postAttnNorm = new RMSNorm(weights.tensor(`${prefix}.post_attention_layernorm.weight`), config.text.rmsNormEps);
|
|
560
565
|
}
|
|
561
566
|
|
|
562
|
-
forward(x: MlxArray, faMask: Mask, cache: Cache, independentRows = false, ssmMask?: MlxArray | null
|
|
563
|
-
|
|
564
|
-
|
|
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
|
|
565
577
|
? this.linearAttn!.forward(xn, cache as SSMCache, independentRows, ssmMask)
|
|
566
|
-
: this.selfAttn!.forward(xn, faMask, cache, independentRows);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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);
|
|
571
584
|
// RMSNorm allocates an aligned row-contiguous output. Pass that layout
|
|
572
585
|
// proof so Trellis can match native small-prefill matmul without an eval.
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const out = ops.add(h, m);
|
|
576
|
-
h.dispose();
|
|
577
|
-
m.dispose();
|
|
578
|
-
return out;
|
|
586
|
+
using m = this.mlp.forward(hn, true, independentRows);
|
|
587
|
+
return ops.add(h, m);
|
|
579
588
|
}
|
|
580
589
|
}
|
|
581
590
|
|
|
@@ -669,10 +678,16 @@ export class Qwen35Model {
|
|
|
669
678
|
return null;
|
|
670
679
|
}
|
|
671
680
|
return {
|
|
672
|
-
|
|
681
|
+
affineKvBits: [4, 8],
|
|
682
|
+
turboQuantFormats: [{ kBits: 8, vBits: 3 }],
|
|
683
|
+
maxChunkSize: (state: readonly Cache[], rows = 1) => rows === 1 ? qwenAppendChunkSize(state[0]!.offset) : 1,
|
|
673
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);
|
|
674
689
|
if (ids.shape.length !== 2 || ids.shape[0] !== 1 || ids.shape[1]! > 4)
|
|
675
|
-
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");
|
|
676
691
|
const hidden = this.embed.encode(ids);
|
|
677
692
|
return this.forwardLayers(hidden, cache, true);
|
|
678
693
|
},
|
|
@@ -684,10 +699,61 @@ export class Qwen35Model {
|
|
|
684
699
|
return this.forwardLayers(h, cache);
|
|
685
700
|
}
|
|
686
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
|
+
|
|
687
753
|
/** Active vision mRoPE request state (serial lane; set by the generation
|
|
688
754
|
* gateway around a vision request's run, null for text-only — which keeps
|
|
689
|
-
* the bit-exact fast-rope path).
|
|
690
|
-
*
|
|
755
|
+
* the bit-exact fast-rope path). The explicit serial executor scopes this value.
|
|
756
|
+
* Shared media forwards pass request-owned positions directly. */
|
|
691
757
|
mrope: MropeRequestState | null = null;
|
|
692
758
|
#mropeInvFreq: MlxArray | null = null;
|
|
693
759
|
|
|
@@ -722,7 +788,8 @@ export class Qwen35Model {
|
|
|
722
788
|
tap.captured.set(i, copy);
|
|
723
789
|
}
|
|
724
790
|
|
|
725
|
-
protected forwardLayers(h0: MlxArray, cache: Cache[], independentRows = false
|
|
791
|
+
protected forwardLayers(h0: MlxArray, cache: Cache[], independentRows = false,
|
|
792
|
+
positions?: MlxArray): MlxArray {
|
|
726
793
|
const L = h0.shape[1]!;
|
|
727
794
|
// One full-attention mask shared by all full layers (same offset); linear
|
|
728
795
|
// layers see no ssm mask at B=1.
|
|
@@ -731,15 +798,13 @@ export class Qwen35Model {
|
|
|
731
798
|
// Vision requests: one interleaved-mRoPE cos/sin table per forward,
|
|
732
799
|
// shared by all 12 full-attention layers (positions are layer-invariant).
|
|
733
800
|
let mropeFwd: ReturnType<typeof buildMropePositions> | null = null;
|
|
734
|
-
if (this.mrope) {
|
|
801
|
+
if (positions || this.mrope) {
|
|
735
802
|
const t = this.config.text;
|
|
736
803
|
const ropeDims = Math.trunc(t.headDim * t.partialRotaryFactor);
|
|
737
804
|
const base = t.ropeParameters.full_attention?.ropeTheta ?? 10000;
|
|
738
805
|
this.#mropeInvFreq ??= mropeInvFreq(ropeDims, base);
|
|
739
|
-
mropeFwd =
|
|
740
|
-
this.mrope
|
|
741
|
-
);
|
|
742
|
-
setActiveMrope(mropeFwd);
|
|
806
|
+
mropeFwd = positions ? { posIds: positions, invFreq: this.#mropeInvFreq, rotaryDims: ropeDims }
|
|
807
|
+
: buildMropePositions(this.mrope!, cache[this.faIdx]!.offset, L, this.#mropeInvFreq, ropeDims);
|
|
743
808
|
}
|
|
744
809
|
let h: MlxArray | null = h0;
|
|
745
810
|
const prof = (globalThis as Record<string, unknown>).__deltaProf as
|
|
@@ -748,7 +813,7 @@ export class Qwen35Model {
|
|
|
748
813
|
try {
|
|
749
814
|
for (let i = 0; i < this.layers.length; i++) {
|
|
750
815
|
const tl = prof ? performance.now() : 0;
|
|
751
|
-
const next = this.layers[i]!.forward(h, faMask, cache[i]!, independentRows, ssmMask);
|
|
816
|
+
const next = this.layers[i]!.forward(h, faMask, cache[i]!, independentRows, ssmMask, mropeFwd);
|
|
752
817
|
h.dispose();
|
|
753
818
|
h = next;
|
|
754
819
|
if (L > TRELLIS_MATVEC_MAX_M) {
|
|
@@ -772,10 +837,7 @@ export class Qwen35Model {
|
|
|
772
837
|
h = null; // consumed — the finally must not double-free
|
|
773
838
|
return out;
|
|
774
839
|
} finally {
|
|
775
|
-
if (mropeFwd)
|
|
776
|
-
setActiveMrope(null);
|
|
777
|
-
mropeFwd.posIds.dispose();
|
|
778
|
-
}
|
|
840
|
+
if (mropeFwd && !positions) mropeFwd.posIds.dispose();
|
|
779
841
|
// A mid-loop layer throw must not strand the mask array or the
|
|
780
842
|
// in-flight [1,L,H] residual (2026-08-18 review).
|
|
781
843
|
faMask.arr?.dispose();
|
package/src/model/support.ts
CHANGED
|
@@ -61,6 +61,13 @@ export function isDiffusionGemmaConfig(config: ModelConfig): boolean {
|
|
|
61
61
|
return config.modelType === "diffusion_gemma";
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Whisper (model_type `whisper`, mlx-whisper ModelDimensions config):
|
|
65
|
+
* encoder-decoder speech recognition — audio in, text out. Routed through
|
|
66
|
+
* the transcription engine (src/audio/whisper-*.ts), never the chat loop. */
|
|
67
|
+
export function isWhisperConfig(config: ModelConfig): boolean {
|
|
68
|
+
return config.modelType === "whisper" && typeof config.raw.n_audio_ctx === "number";
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
/** Support tier for a registry record (docs/design/generic-model-support.md):
|
|
65
72
|
* "targeted" = dedicated/generated forward + L2/L3 paths;
|
|
66
73
|
* "generic" = the Tier-0 universal module (L1 monolith only);
|
|
@@ -74,6 +81,7 @@ export function supportTier(modelType: string, repoId = ""): "targeted" | "gener
|
|
|
74
81
|
if (modelType === "qwen3") return "targeted"; // plain Qwen3 (embedding backbone)
|
|
75
82
|
if (modelType === "qwen3_moe") return "targeted"; // Qwen3-MoE (sparse experts)
|
|
76
83
|
if (modelType === "glm_moe_dsa") return "targeted"; // dedicated GLM-5.2 MLA/DSA port
|
|
84
|
+
if (modelType === "whisper") return "targeted"; // speech-to-text (transcription engine)
|
|
77
85
|
if (modelType === "llama" && repoId.toLowerCase().includes("minicpm5-1b-optiq-4bit"))
|
|
78
86
|
return "targeted";
|
|
79
87
|
// Tier-0 generic fallback: the universal-dense descriptor table.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import type { Cache } from "./gemma4-base";
|
|
4
|
+
|
|
5
|
+
export interface TokenWorkOptions {
|
|
6
|
+
/** Verification keeps its established GEMV/GEMM geometry. Other groups may
|
|
7
|
+
* still pack their tokenwise work independently. */
|
|
8
|
+
readonly preserveTokenGeometry?: boolean;
|
|
9
|
+
/** Borrows a residual stream; the method copies only the layers it needs.
|
|
10
|
+
* The layer-count index denotes the post-final-norm output. */
|
|
11
|
+
readonly captureLayer?: (layer: number, hidden: MlxArray) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Independent sequences presented to one model execution. Each item keeps
|
|
15
|
+
* its existing rectangular row geometry and owns a disjoint cache. */
|
|
16
|
+
export interface TokenGroup extends TokenWorkOptions {
|
|
17
|
+
readonly ids: MlxArray;
|
|
18
|
+
readonly cache: Cache[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Apply a tokenwise block while respecting each method's numerical policy. */
|
|
22
|
+
export function mapTokenGroups(
|
|
23
|
+
groups: readonly TokenGroup[], inputs: readonly MlxArray[],
|
|
24
|
+
operation: (hidden: MlxArray) => MlxArray, pack = true,
|
|
25
|
+
): MlxArray[] {
|
|
26
|
+
const results: MlxArray[] = [];
|
|
27
|
+
const packedRows = groups.flatMap((group, row) => pack && !group.preserveTokenGeometry ? [row] : []);
|
|
28
|
+
try {
|
|
29
|
+
const packed = packedRows.length ? mapPackedTokens(packedRows.map(row => inputs[row]!), operation) : [];
|
|
30
|
+
for (const [index, row] of packedRows.entries()) results[row] = packed[index]!;
|
|
31
|
+
for (let row = 0; row < groups.length; row++) results[row] ??= operation(inputs[row]!);
|
|
32
|
+
return results;
|
|
33
|
+
} catch (error) { for (const result of results) result?.dispose(); throw error; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MixedTokenModel {
|
|
37
|
+
/** Borrows inputs/state; returns one owned hidden array per input group. */
|
|
38
|
+
forwardHiddenMixed(groups: readonly TokenGroup[]): MlxArray[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Pack real tokens for a tokenwise operation, then restore each group's
|
|
42
|
+
* geometry. Attention and recurrent operations must run outside this port.
|
|
43
|
+
* No padding, host readback, or evaluation is introduced here. */
|
|
44
|
+
export function mapPackedTokens(
|
|
45
|
+
inputs: readonly MlxArray[], operation: (packed: MlxArray) => MlxArray,
|
|
46
|
+
): MlxArray[] {
|
|
47
|
+
if (inputs.length === 1) return [operation(inputs[0]!)];
|
|
48
|
+
const flat = inputs.map(input => ops.reshape(input, [1, -1, input.shape.at(-1)!]));
|
|
49
|
+
let packed: MlxArray;
|
|
50
|
+
try { packed = ops.concatAxis(flat, 1); }
|
|
51
|
+
finally { for (const input of flat) input.dispose(); }
|
|
52
|
+
using joined = packed;
|
|
53
|
+
using output = operation(joined);
|
|
54
|
+
const results: MlxArray[] = [];
|
|
55
|
+
let position = 0;
|
|
56
|
+
try {
|
|
57
|
+
for (const input of inputs) {
|
|
58
|
+
const [batch, length] = input.shape;
|
|
59
|
+
const end = position + batch! * length!;
|
|
60
|
+
using part = output.slice([0, position, 0], [1, end, output.shape[2]!]);
|
|
61
|
+
results.push(ops.reshape(part, [batch!, length!, output.shape[2]!]));
|
|
62
|
+
position = end;
|
|
63
|
+
}
|
|
64
|
+
return results;
|
|
65
|
+
} catch (error) { for (const result of results) result.dispose(); throw error; }
|
|
66
|
+
}
|
|
@@ -41,7 +41,7 @@ import * as ops from "../mlx/ops";
|
|
|
41
41
|
import { MetalKernel } from "../mlx/metal-kernel";
|
|
42
42
|
import type { Weights } from "../weights";
|
|
43
43
|
import { quantFor, type ModelConfig, type QuantSpec } from "../config";
|
|
44
|
-
import { runtimeFlag, runtimeNumber } from "../runtime-config";
|
|
44
|
+
import { runtimeFlag, runtimeNumber, runtimeValue } from "../runtime-config";
|
|
45
45
|
import { lut1mad, wordsPerBlock } from "../quantize/trellis";
|
|
46
46
|
import { QuantizedLinear } from "./gemma4-base";
|
|
47
47
|
import { REDUCE_SHARED_M_SOURCE, GATEUP_SHARED_M_SOURCE } from "./trellis-shared-m";
|
|
@@ -475,7 +475,7 @@ export function expandTrellis(codes: MlxArray, scales: MlxArray, g: TrellisGeome
|
|
|
475
475
|
export type TrellisMode = "kernel" | "expand";
|
|
476
476
|
|
|
477
477
|
export function trellisModeFromEnv(): TrellisMode {
|
|
478
|
-
const v =
|
|
478
|
+
const v = runtimeValue("MLX_BUN_TRELLIS");
|
|
479
479
|
if (v === "expand") return "expand";
|
|
480
480
|
return "kernel";
|
|
481
481
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { runtimeConfig, type RuntimeConfig } from "../runtime-config";
|
|
1
2
|
import type { MlxArray } from "../mlx/array";
|
|
2
3
|
import * as tq from "../mlx/turboquant-ops";
|
|
3
4
|
import type { KvCodec } from "../backends/mlx/kv-codec";
|
|
4
5
|
|
|
6
|
+
/** Resolve once when a cache is created; storage copies retain this policy. */
|
|
7
|
+
export function turboQuantFusedDecode(config: RuntimeConfig = runtimeConfig()): boolean {
|
|
8
|
+
return config.flag("MLX_BUN_TURBOQUANT_FUSED_DECODE", true);
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
/** One TurboQuant-encoded (K, V) storage tuple — the 5 arrays kv-store.ts
|
|
6
12
|
* and toQuantized/fromKVCache pass around. Not `ops.QuantizedTensor`
|
|
7
13
|
* (mlx's affine int4/int8 scheme): this is the rotation + Lloyd-Max
|