mlx-bun 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -21
- package/THIRD_PARTY_LICENSES.md +1 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +58 -35
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +14 -6
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +3 -2
- package/docs/design/ssd-kv-cold-tier.md +130 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/reference/cli.md +35 -16
- package/docs/reference/distribution.md +13 -6
- package/docs/reference/embedding.md +35 -14
- package/docs/reference/features-matrix.md +97 -0
- package/docs/reference/library-api.md +30 -9
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +11 -1
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +634 -22
- package/docs/reference/server-config.md +250 -121
- package/docs/reference/training.md +14 -15
- package/package.json +5 -3
- package/src/anthropic.ts +13 -0
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/cli.ts +356 -105
- package/src/config.ts +49 -2
- package/src/download.ts +40 -0
- package/src/eval/README.md +7 -6
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +3 -3
- package/src/expert-offload.ts +15 -2
- package/src/flags.ts +13 -0
- package/src/generate.ts +330 -24
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +442 -88
- package/src/lora.ts +8 -0
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +284 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +106 -19
- package/src/mlx/autograd.ts +2 -2
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +153 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +10 -4
- package/src/model/gemma4-base.ts +363 -39
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +76 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +88 -23
- package/src/model/support.ts +12 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +293 -33
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +57 -2
- package/src/serve/batch-scheduler.ts +965 -72
- package/src/serve/generation-gateway.ts +214 -11
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1380 -135
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +536 -0
- package/src/spec/source.ts +93 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +107 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +408 -20
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -2
- package/src/train/trainer.ts +1 -4
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1184 -1789
- package/src/web/app.js +5667 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +252 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// Concrete model graph for Qwen3-MoE (model_type `qwen3_moe`,
|
|
2
|
+
// Qwen3MoeForCausalLM). A STRAIGHT op-for-op transcription of the oracle
|
|
3
|
+
// (`/Users/joshrossi/Code/mlx-lm/.venv/.../mlx_lm/models/qwen3_moe.py` +
|
|
4
|
+
// switch_layers.py SwitchGLU + activations.py swiglu). Each op below carries
|
|
5
|
+
// the exact oracle line it copies — no abstraction, no reorder, no
|
|
6
|
+
// "optimization". The ONLY intentional deviation from a naive transcription is
|
|
7
|
+
// that the expert swiglu is mx.compile-COMPILED (like the oracle's
|
|
8
|
+
// activations.py `@partial(mx.compile) def swiglu`), matching both the greedy
|
|
9
|
+
// token stream AND the dispatched Metal kernel set.
|
|
10
|
+
//
|
|
11
|
+
// Attention is identical to plain qwen3 (per-head q/k RMSNorm over head_dim
|
|
12
|
+
// before RoPE, full-head RoPE, GQA, SDPA). The MoE block is
|
|
13
|
+
// Qwen3MoeSparseMoeBlock: gate Linear -> precise softmax over ALL experts ->
|
|
14
|
+
// argpartition top-k -> take_along_axis -> renormalize (norm_topk_prob) ->
|
|
15
|
+
// SwitchGLU(swiglu) experts -> weighted sum. Per decoder_sparse_step /
|
|
16
|
+
// mlp_only_layers a layer may instead be a dense MLP (MLP class); both are
|
|
17
|
+
// handled faithfully. Weights are untied (separate lm_head). Config: 48 layers,
|
|
18
|
+
// hidden 2048, 128 experts, top-8, moe_intermediate 768, rope_theta 1e7.
|
|
19
|
+
|
|
20
|
+
import type { ModelConfig } from "../config";
|
|
21
|
+
import type { Weights } from "../weights";
|
|
22
|
+
import { MlxArray } from "../mlx/array";
|
|
23
|
+
import * as ops from "../mlx/ops";
|
|
24
|
+
import { CompiledFunction } from "../mlx/compile";
|
|
25
|
+
import {
|
|
26
|
+
argmaxLastPosition,
|
|
27
|
+
disposing,
|
|
28
|
+
isCompiledTrace,
|
|
29
|
+
KVCache,
|
|
30
|
+
LoraState,
|
|
31
|
+
QuantizedEmbedding,
|
|
32
|
+
QuantizedLinear,
|
|
33
|
+
QuantizedSwitchLinear,
|
|
34
|
+
RMSNorm,
|
|
35
|
+
type Cache,
|
|
36
|
+
type Mask,
|
|
37
|
+
} from "./gemma4-base";
|
|
38
|
+
|
|
39
|
+
// ── activations.py swiglu (mx.compile) ───────────────────────────────────────
|
|
40
|
+
// `@partial(mx.compile, shapeless=True) def swiglu(gate, x): return nn.silu(gate) * x`
|
|
41
|
+
// nn.silu(g) == g * sigmoid(g); mx.compile fuses sigmoid + mul + mul into ONE
|
|
42
|
+
// kernel. Traced once (shapeless), replayed thereafter. Autograd-safe (mx.compile
|
|
43
|
+
// threads the VJP through the traced graph). Exported so the parity test can
|
|
44
|
+
// assert the closure exists and the expert path actually uses it.
|
|
45
|
+
let _swigluClosure: CompiledFunction | null = null;
|
|
46
|
+
export function compiledSwiglu(gate: MlxArray, up: MlxArray): MlxArray {
|
|
47
|
+
if (!_swigluClosure) {
|
|
48
|
+
_swigluClosure = new CompiledFunction((inputs) => {
|
|
49
|
+
const g = inputs[0]!, u = inputs[1]!; // nn.silu(gate) * x
|
|
50
|
+
const sig = ops.sigmoid(g);
|
|
51
|
+
const silu = ops.mul(g, sig); sig.dispose();
|
|
52
|
+
const out = ops.mul(silu, u); silu.dispose();
|
|
53
|
+
return [out];
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return _swigluClosure.apply([gate, up])[0]!;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── qwen3_moe.py Attention (== plain qwen3) ──────────────────────────────────
|
|
60
|
+
class Attention {
|
|
61
|
+
readonly qProj: QuantizedLinear;
|
|
62
|
+
readonly kProj: QuantizedLinear;
|
|
63
|
+
readonly vProj: QuantizedLinear;
|
|
64
|
+
readonly oProj: QuantizedLinear;
|
|
65
|
+
readonly qNorm: RMSNorm;
|
|
66
|
+
readonly kNorm: RMSNorm;
|
|
67
|
+
readonly nHeads: number;
|
|
68
|
+
readonly nKvHeads: number;
|
|
69
|
+
readonly headDim: number;
|
|
70
|
+
readonly scale: number;
|
|
71
|
+
readonly ropeBase: number;
|
|
72
|
+
|
|
73
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string) {
|
|
74
|
+
const t = config.text;
|
|
75
|
+
this.nHeads = t.numAttentionHeads; // self.n_heads
|
|
76
|
+
this.nKvHeads = t.numKeyValueHeads; // self.n_kv_heads
|
|
77
|
+
this.headDim = t.headDim; // head_dim
|
|
78
|
+
this.scale = Math.pow(this.headDim, -0.5); // self.scale = head_dim**-0.5
|
|
79
|
+
this.ropeBase = t.ropeParameters.full_attention?.ropeTheta ?? 10000000; // args.rope_theta
|
|
80
|
+
this.qProj = QuantizedLinear.load(weights, `${prefix}.q_proj`, config); // nn.Linear(dim, n_heads*head_dim, bias=False)
|
|
81
|
+
this.kProj = QuantizedLinear.load(weights, `${prefix}.k_proj`, config);
|
|
82
|
+
this.vProj = QuantizedLinear.load(weights, `${prefix}.v_proj`, config);
|
|
83
|
+
this.oProj = QuantizedLinear.load(weights, `${prefix}.o_proj`, config);
|
|
84
|
+
this.qNorm = new RMSNorm(weights.tensor(`${prefix}.q_norm.weight`), t.rmsNormEps); // nn.RMSNorm(head_dim)
|
|
85
|
+
this.kNorm = new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
forward(x: MlxArray, mask: Mask, cache: Cache): MlxArray {
|
|
89
|
+
const [B, L] = x.shape as [number, number, number]; // B, L, D = x.shape
|
|
90
|
+
let q = this.qProj.forward(x); // queries = self.q_proj(x)
|
|
91
|
+
let k = this.kProj.forward(x); // keys = self.k_proj(x)
|
|
92
|
+
let v = this.vProj.forward(x); // values = self.v_proj(x)
|
|
93
|
+
// queries = self.q_norm(queries.reshape(B,L,n_heads,-1)).transpose(0,2,1,3)
|
|
94
|
+
q = disposing(q, ops.reshape(q, [B, L, this.nHeads, this.headDim]));
|
|
95
|
+
k = disposing(k, ops.reshape(k, [B, L, this.nKvHeads, this.headDim]));
|
|
96
|
+
v = disposing(v, ops.reshape(v, [B, L, this.nKvHeads, this.headDim]));
|
|
97
|
+
q = disposing(q, this.qNorm.forward(q)); // self.q_norm(...)
|
|
98
|
+
k = disposing(k, this.kNorm.forward(k)); // self.k_norm(...)
|
|
99
|
+
q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3])); // .transpose(0,2,1,3)
|
|
100
|
+
k = disposing(k, ops.transposeAxes(k, [0, 2, 1, 3]));
|
|
101
|
+
v = disposing(v, ops.transposeAxes(v, [0, 2, 1, 3])); // values.reshape(...).transpose(0,2,1,3)
|
|
102
|
+
q = disposing(q, ops.rope(q, this.headDim, this.ropeBase, cache.offset, null)); // self.rope(queries, offset=cache.offset)
|
|
103
|
+
k = disposing(k, ops.rope(k, this.headDim, this.ropeBase, cache.offset, null)); // self.rope(keys, offset=cache.offset)
|
|
104
|
+
const [keys, values] = cache.updateAndFetch(k, v); // cache.update_and_fetch(keys, values)
|
|
105
|
+
k.dispose(); v.dispose();
|
|
106
|
+
let out = ops.sdpa(q, keys, values, this.scale, mask.mode, mask.arr); // scaled_dot_product_attention(...)
|
|
107
|
+
q.dispose(); keys.dispose(); values.dispose();
|
|
108
|
+
out = disposing(out, ops.transposeAxes(out, [0, 2, 1, 3])); // output.transpose(0,2,1,3)
|
|
109
|
+
out = disposing(out, ops.reshape(out, [B, L, -1])); // .reshape(B, L, -1)
|
|
110
|
+
const y = this.oProj.forward(out); out.dispose(); // self.o_proj(output)
|
|
111
|
+
return y;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── qwen3_moe.py MLP (dense-layer FFN) ───────────────────────────────────────
|
|
116
|
+
// def __call__(self, x): return self.down_proj(swiglu(self.gate_proj(x), self.up_proj(x)))
|
|
117
|
+
class MLP {
|
|
118
|
+
readonly gateProj: QuantizedLinear;
|
|
119
|
+
readonly upProj: QuantizedLinear;
|
|
120
|
+
readonly downProj: QuantizedLinear;
|
|
121
|
+
|
|
122
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string) {
|
|
123
|
+
this.gateProj = QuantizedLinear.load(weights, `${prefix}.gate_proj`, config); // nn.Linear(dim, hidden_dim, bias=False)
|
|
124
|
+
this.upProj = QuantizedLinear.load(weights, `${prefix}.up_proj`, config);
|
|
125
|
+
this.downProj = QuantizedLinear.load(weights, `${prefix}.down_proj`, config);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
forward(x: MlxArray): MlxArray {
|
|
129
|
+
const g = this.gateProj.forward(x); // self.gate_proj(x)
|
|
130
|
+
const u = this.upProj.forward(x); // self.up_proj(x)
|
|
131
|
+
const s = compiledSwiglu(g, u); // swiglu(gate, x) (mx.compile)
|
|
132
|
+
g.dispose(); u.dispose();
|
|
133
|
+
const y = this.downProj.forward(s); s.dispose(); // self.down_proj(...)
|
|
134
|
+
return y;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── switch_layers.py SwitchGLU (swiglu activation, quantized experts) ─────────
|
|
139
|
+
// The three per-expert projections are the stacked switch_mlp.{gate,up,down}_proj
|
|
140
|
+
// tensors; the activation is the SAME @mx.compile swiglu as activations.py.
|
|
141
|
+
class SwitchGLU {
|
|
142
|
+
readonly gateProj: QuantizedSwitchLinear;
|
|
143
|
+
readonly upProj: QuantizedSwitchLinear;
|
|
144
|
+
readonly downProj: QuantizedSwitchLinear;
|
|
145
|
+
|
|
146
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string) {
|
|
147
|
+
this.gateProj = QuantizedSwitchLinear.load(weights, `${prefix}.gate_proj`, config);
|
|
148
|
+
this.upProj = QuantizedSwitchLinear.load(weights, `${prefix}.up_proj`, config);
|
|
149
|
+
this.downProj = QuantizedSwitchLinear.load(weights, `${prefix}.down_proj`, config);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** x [B, L, H], indices [B, L, k] → [B, L, k, H]. */
|
|
153
|
+
forward(x: MlxArray, indices: MlxArray): MlxArray {
|
|
154
|
+
// x = mx.expand_dims(x, (-2, -3)) → [B, L, 1, 1, H]
|
|
155
|
+
let h = ops.expandDims(x, -2);
|
|
156
|
+
h = disposing(h, ops.expandDims(h, -3));
|
|
157
|
+
|
|
158
|
+
// do_sort = indices.size >= 64
|
|
159
|
+
const doSort = indices.size >= 64;
|
|
160
|
+
let idx = indices;
|
|
161
|
+
let invOrder: MlxArray | null = null;
|
|
162
|
+
let order: MlxArray | null = null;
|
|
163
|
+
if (doSort) {
|
|
164
|
+
// _gather_sort(x, indices): flatten indices, argsort → order, argsort(order) →
|
|
165
|
+
// inv_order, gather rows by order // M (M = k = last dim of indices).
|
|
166
|
+
const M = indices.shape[indices.ndim - 1]!; // *_, M = indices.shape
|
|
167
|
+
const idxFlat = ops.reshape(indices, [indices.size]); // indices.flatten()
|
|
168
|
+
order = ops.argsortAxis(idxFlat, 0); // order = mx.argsort(indices)
|
|
169
|
+
invOrder = ops.argsortAxis(order, 0); // inv_order = mx.argsort(order)
|
|
170
|
+
const mScalar = ops.scalarLike(M, order);
|
|
171
|
+
const rowIdx = ops.floorDivide(order, mScalar); // order // M
|
|
172
|
+
mScalar.dispose();
|
|
173
|
+
const [, , , , H] = h.shape as number[];
|
|
174
|
+
const flat = ops.reshape(h, [-1, 1, H!]); // x.flatten(0, -3)
|
|
175
|
+
h.dispose();
|
|
176
|
+
h = ops.takeAxis(flat, rowIdx, 0); // x.flatten(0,-3)[order // M]
|
|
177
|
+
flat.dispose(); rowIdx.dispose();
|
|
178
|
+
idx = ops.takeAxis(idxFlat, order, 0); // indices[order]
|
|
179
|
+
idxFlat.dispose();
|
|
180
|
+
}
|
|
181
|
+
// (self.training path — mx.stop_gradient(idx) — is inference-irrelevant here)
|
|
182
|
+
const xUp = this.upProj.forward(h, idx, doSort); // x_up = self.up_proj(x, idx, sorted_indices=do_sort)
|
|
183
|
+
const xGate = this.gateProj.forward(h, idx, doSort); // x_gate = self.gate_proj(x, idx, sorted_indices=do_sort)
|
|
184
|
+
h.dispose();
|
|
185
|
+
// self.activation(x_up, x_gate) == SwiGLU()(x_up, x_gate) == swiglu(x_gate, x_up)
|
|
186
|
+
// (SwiGLU.__call__(self, x, gate) returns swiglu(gate, x); called (x_up, x_gate)).
|
|
187
|
+
const mid = compiledSwiglu(xGate, xUp); // swiglu(gate=x_gate, x=x_up) (mx.compile)
|
|
188
|
+
xGate.dispose(); xUp.dispose();
|
|
189
|
+
let y = this.downProj.forward(mid, idx, doSort); // x = self.down_proj(activation, idx, sorted_indices=do_sort)
|
|
190
|
+
mid.dispose();
|
|
191
|
+
if (idx !== indices) idx.dispose();
|
|
192
|
+
|
|
193
|
+
if (doSort) {
|
|
194
|
+
// _scatter_unsort(x, inv_order, indices.shape): x[inv_order], unflatten to
|
|
195
|
+
// indices.shape (+ the trailing [1, H] the squeeze(-2) below removes).
|
|
196
|
+
y = disposing(y, ops.takeAxis(y, invOrder!, 0)); // x = x[inv_order]
|
|
197
|
+
invOrder!.dispose(); order!.dispose();
|
|
198
|
+
const [B, L, k] = indices.shape as number[];
|
|
199
|
+
const H = y.shape[y.ndim - 1]!;
|
|
200
|
+
y = disposing(y, ops.reshape(y, [B!, L!, k!, 1, H])); // mx.unflatten(x, 0, shape)
|
|
201
|
+
}
|
|
202
|
+
// return x.squeeze(-2)
|
|
203
|
+
const shape = y.shape;
|
|
204
|
+
shape.splice(shape.length - 2, 1);
|
|
205
|
+
return disposing(y, ops.reshape(y, shape));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── qwen3_moe.py Qwen3MoeSparseMoeBlock ──────────────────────────────────────
|
|
210
|
+
class Qwen3MoeSparseMoeBlock {
|
|
211
|
+
readonly gate: QuantizedLinear;
|
|
212
|
+
readonly switchMlp: SwitchGLU;
|
|
213
|
+
readonly numExperts: number;
|
|
214
|
+
readonly topK: number;
|
|
215
|
+
readonly normTopkProb: boolean;
|
|
216
|
+
|
|
217
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string) {
|
|
218
|
+
const t = config.text;
|
|
219
|
+
this.numExperts = t.numExperts; // self.num_experts
|
|
220
|
+
this.topK = t.topKExperts; // self.top_k
|
|
221
|
+
this.normTopkProb = t.normTopkProb; // self.norm_topk_prob
|
|
222
|
+
this.gate = QuantizedLinear.load(weights, `${prefix}.gate`, config); // nn.Linear(dim, num_experts, bias=False)
|
|
223
|
+
this.switchMlp = new SwitchGLU(weights, config, `${prefix}.switch_mlp`); // SwitchGLU(dim, moe_inter, num_experts)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
forward(x: MlxArray): MlxArray {
|
|
227
|
+
let gates = this.gate.forward(x); // gates = self.gate(x)
|
|
228
|
+
gates = disposing(gates, ops.softmaxAxis(gates, -1, true)); // gates = mx.softmax(gates, axis=-1, precise=True)
|
|
229
|
+
|
|
230
|
+
const k = this.topK;
|
|
231
|
+
const [B, L] = gates.shape as [number, number, number];
|
|
232
|
+
// inds = mx.argpartition(gates, kth=-k, axis=-1)[..., -k:]
|
|
233
|
+
const part = ops.argpartitionAxis(gates, this.numExperts - k, -1); // argpartition(..., kth=-k)
|
|
234
|
+
let inds: MlxArray;
|
|
235
|
+
if (isCompiledTrace()) {
|
|
236
|
+
const start = ops.fromInt32([this.numExperts - k], [1]);
|
|
237
|
+
inds = ops.sliceDynamic(part, start, [2], [B, L, k]);
|
|
238
|
+
start.dispose();
|
|
239
|
+
} else {
|
|
240
|
+
inds = part.slice([0, 0, this.numExperts - k], [B, L, this.numExperts]); // [..., -k:]
|
|
241
|
+
}
|
|
242
|
+
part.dispose();
|
|
243
|
+
|
|
244
|
+
let scores = ops.takeAlongAxis(gates, inds, -1); // scores = mx.take_along_axis(gates, inds, axis=-1)
|
|
245
|
+
gates.dispose();
|
|
246
|
+
if (this.normTopkProb) {
|
|
247
|
+
const denom = ops.sumAxis(scores, -1, true); // mx.sum(scores, axis=-1, keepdims=True)
|
|
248
|
+
scores = disposing(scores, ops.div(scores, denom)); // scores /= ...
|
|
249
|
+
denom.dispose();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const y = this.switchMlp.forward(x, inds); // y = self.switch_mlp(x, inds)
|
|
253
|
+
inds.dispose();
|
|
254
|
+
// y = (y * scores[..., None]).sum(axis=-2)
|
|
255
|
+
const sExp = ops.expandDims(scores, -1); // scores[..., None]
|
|
256
|
+
scores.dispose();
|
|
257
|
+
const wy = ops.mul(y, sExp); // y * scores[..., None]
|
|
258
|
+
y.dispose(); sExp.dispose();
|
|
259
|
+
return disposing(wy, ops.sumAxis(wy, -2, false)); // .sum(axis=-2)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── qwen3_moe.py Qwen3MoeDecoderLayer ────────────────────────────────────────
|
|
264
|
+
class Qwen3MoeDecoderLayer {
|
|
265
|
+
readonly selfAttn: Attention;
|
|
266
|
+
readonly mlp: Qwen3MoeSparseMoeBlock | MLP;
|
|
267
|
+
readonly inputLayernorm: RMSNorm;
|
|
268
|
+
readonly postAttentionLayernorm: RMSNorm;
|
|
269
|
+
|
|
270
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string, layerIdx: number) {
|
|
271
|
+
const t = config.text;
|
|
272
|
+
this.selfAttn = new Attention(weights, config, `${prefix}.self_attn`);
|
|
273
|
+
this.inputLayernorm = new RMSNorm(weights.tensor(`${prefix}.input_layernorm.weight`), t.rmsNormEps);
|
|
274
|
+
this.postAttentionLayernorm = new RMSNorm(weights.tensor(`${prefix}.post_attention_layernorm.weight`), t.rmsNormEps);
|
|
275
|
+
// if (layer_idx not in mlp_only_layers) and (num_experts > 0 and
|
|
276
|
+
// (layer_idx + 1) % decoder_sparse_step == 0): Qwen3MoeSparseMoeBlock else MLP
|
|
277
|
+
const isMoe =
|
|
278
|
+
!t.mlpOnlyLayers.includes(layerIdx) &&
|
|
279
|
+
t.numExperts > 0 &&
|
|
280
|
+
(layerIdx + 1) % t.decoderSparseStep === 0;
|
|
281
|
+
this.mlp = isMoe
|
|
282
|
+
? new Qwen3MoeSparseMoeBlock(weights, config, `${prefix}.mlp`)
|
|
283
|
+
: new MLP(weights, config, `${prefix}.mlp`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
forward(x: MlxArray, mask: Mask, cache: Cache): MlxArray {
|
|
287
|
+
const xn = this.inputLayernorm.forward(x); // self.input_layernorm(x)
|
|
288
|
+
const r1 = this.selfAttn.forward(xn, mask, cache); // r = self.self_attn(..., mask, cache)
|
|
289
|
+
xn.dispose();
|
|
290
|
+
const h = ops.add(x, r1); r1.dispose(); // h = x + r
|
|
291
|
+
const hn = this.postAttentionLayernorm.forward(h); // self.post_attention_layernorm(h)
|
|
292
|
+
const r2 = this.mlp.forward(hn); hn.dispose(); // r = self.mlp(...)
|
|
293
|
+
const out = ops.add(h, r2); h.dispose(); r2.dispose(); // out = h + r
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── qwen3_moe.py Qwen3MoeModel + Model ───────────────────────────────────────
|
|
299
|
+
export class Qwen3MoeModel {
|
|
300
|
+
readonly config: ModelConfig;
|
|
301
|
+
readonly weightsBytes: number;
|
|
302
|
+
readonly prefixBase = "model";
|
|
303
|
+
readonly loraState = new LoraState();
|
|
304
|
+
readonly embed: QuantizedEmbedding;
|
|
305
|
+
readonly layers: Qwen3MoeDecoderLayer[];
|
|
306
|
+
readonly finalNorm: RMSNorm;
|
|
307
|
+
/** Untied lm_head (tie_word_embeddings=false for this family). */
|
|
308
|
+
readonly lmHead: QuantizedLinear | null;
|
|
309
|
+
|
|
310
|
+
constructor(weights: Weights, config: ModelConfig) {
|
|
311
|
+
this.config = config;
|
|
312
|
+
this.weightsBytes = [...weights.shards.files.values()]
|
|
313
|
+
.reduce((a, f) => a + f.mmap.size, 0);
|
|
314
|
+
this.embed = QuantizedEmbedding.load(weights, "model.embed_tokens", config); // nn.Embedding
|
|
315
|
+
this.layers = Array.from(
|
|
316
|
+
{ length: config.text.numHiddenLayers },
|
|
317
|
+
(_, i) => new Qwen3MoeDecoderLayer(weights, config, `model.layers.${i}`, i),
|
|
318
|
+
);
|
|
319
|
+
this.finalNorm = new RMSNorm(weights.tensor("model.norm.weight"), config.text.rmsNormEps); // self.norm
|
|
320
|
+
this.lmHead = config.text.tieWordEmbeddings
|
|
321
|
+
? null
|
|
322
|
+
: QuantizedLinear.load(weights, "lm_head", config); // self.lm_head
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
loraTargets(): Map<string, QuantizedLinear> {
|
|
326
|
+
const out = new Map<string, QuantizedLinear>();
|
|
327
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
328
|
+
const l = this.layers[i]!;
|
|
329
|
+
const p = `model.layers.${i}`;
|
|
330
|
+
out.set(`${p}.self_attn.q_proj`, l.selfAttn.qProj);
|
|
331
|
+
out.set(`${p}.self_attn.k_proj`, l.selfAttn.kProj);
|
|
332
|
+
out.set(`${p}.self_attn.v_proj`, l.selfAttn.vProj);
|
|
333
|
+
out.set(`${p}.self_attn.o_proj`, l.selfAttn.oProj);
|
|
334
|
+
if (l.mlp instanceof MLP) {
|
|
335
|
+
out.set(`${p}.mlp.gate_proj`, l.mlp.gateProj);
|
|
336
|
+
out.set(`${p}.mlp.up_proj`, l.mlp.upProj);
|
|
337
|
+
out.set(`${p}.mlp.down_proj`, l.mlp.downProj);
|
|
338
|
+
}
|
|
339
|
+
// MoE switch_mlp / gate are QuantizedSwitchLinear (stacked experts) — not
|
|
340
|
+
// LoRA targets under our per-linear adapter machinery.
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
makeCache(): Cache[] {
|
|
346
|
+
return this.layers.map(() => new KVCache());
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
forwardHidden(ids: MlxArray, cache: Cache[]): MlxArray {
|
|
350
|
+
const h = this.embed.encode(ids); // self.embed_tokens(inputs)
|
|
351
|
+
return this.forwardLayers(h, cache);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
forwardEmbeddings(_embeds: MlxArray, _cache: Cache[], _bidir: MlxArray | null): MlxArray {
|
|
355
|
+
throw new Error("qwen3_moe input-embedding path is not supported");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
protected forwardLayers(h0: MlxArray, cache: Cache[]): MlxArray {
|
|
359
|
+
const L = h0.shape[1]!;
|
|
360
|
+
const mask = cache[0]!.makeMask(L, null); // create_attention_mask(h, cache[0])
|
|
361
|
+
let cur = h0;
|
|
362
|
+
for (let i = 0; i < this.layers.length; i++) { // for layer, c in zip(self.layers, cache)
|
|
363
|
+
const next = this.layers[i]!.forward(cur, mask, cache[i]!); // h = layer(h, mask, c)
|
|
364
|
+
cur.dispose();
|
|
365
|
+
cur = next;
|
|
366
|
+
}
|
|
367
|
+
mask.arr?.dispose();
|
|
368
|
+
return disposing(cur, this.finalNorm.forward(cur)); // return self.norm(h)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
logitsFromHidden(h: MlxArray): MlxArray {
|
|
372
|
+
return this.lmHead ? this.lmHead.forward(h) : this.embed.asLinear(h); // self.lm_head(out)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
forward(tokens: number[] | MlxArray, cache: Cache[]): MlxArray {
|
|
376
|
+
const ids = Array.isArray(tokens)
|
|
377
|
+
? ops.fromInt32(tokens, [1, tokens.length])
|
|
378
|
+
: tokens;
|
|
379
|
+
const h = this.forwardHidden(ids, cache);
|
|
380
|
+
if (Array.isArray(tokens)) ids.dispose();
|
|
381
|
+
const logits = this.logitsFromHidden(h);
|
|
382
|
+
h.dispose();
|
|
383
|
+
return logits;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
generate(promptTokens: number[], maxTokens: number, eosIds: number[] = []): number[] {
|
|
387
|
+
const cache = this.makeCache();
|
|
388
|
+
const out: number[] = [];
|
|
389
|
+
try {
|
|
390
|
+
let tokens = promptTokens;
|
|
391
|
+
for (let step = 0; step < maxTokens; step++) {
|
|
392
|
+
const logits = this.forward(tokens, cache);
|
|
393
|
+
const next = argmaxLastPosition(logits);
|
|
394
|
+
logits.dispose();
|
|
395
|
+
if (eosIds.includes(next)) break;
|
|
396
|
+
out.push(next);
|
|
397
|
+
tokens = [next];
|
|
398
|
+
}
|
|
399
|
+
} finally {
|
|
400
|
+
for (const c of cache) c.dispose();
|
|
401
|
+
}
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
}
|
package/src/model/qwen3.ts
CHANGED
|
@@ -24,8 +24,9 @@ import {
|
|
|
24
24
|
type Cache,
|
|
25
25
|
type Mask,
|
|
26
26
|
} from "./gemma4-base";
|
|
27
|
+
import { compiledSwiglu } from "./minicpm5";
|
|
27
28
|
|
|
28
|
-
class Qwen3Attention {
|
|
29
|
+
export class Qwen3Attention {
|
|
29
30
|
readonly qProj: QuantizedLinear;
|
|
30
31
|
readonly kProj: QuantizedLinear;
|
|
31
32
|
readonly vProj: QuantizedLinear;
|
|
@@ -92,7 +93,7 @@ class Qwen3Attention {
|
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
class Qwen3MLP {
|
|
96
|
+
export class Qwen3MLP {
|
|
96
97
|
readonly gate: QuantizedLinear;
|
|
97
98
|
readonly up: QuantizedLinear;
|
|
98
99
|
readonly down: QuantizedLinear;
|
|
@@ -106,12 +107,12 @@ class Qwen3MLP {
|
|
|
106
107
|
forward(x: MlxArray): MlxArray {
|
|
107
108
|
const gate = this.gate.forward(x);
|
|
108
109
|
const up = this.up.forward(x);
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
// Oracle (mlx_lm/models/qwen3.py MLP): down_proj(swiglu(gate_proj(x), up_proj(x)))
|
|
111
|
+
// where swiglu is the `@mx.compile` closure (silu(gate)*up). Compiled = the
|
|
112
|
+
// same libmlx kernel mlx-lm dispatches (L1-faithful) AND fuses the three
|
|
113
|
+
// standalone ops into one dispatch. Bit-identical to the old unfused path.
|
|
114
|
+
const hidden = compiledSwiglu(gate, up);
|
|
111
115
|
gate.dispose();
|
|
112
|
-
sig.dispose();
|
|
113
|
-
const hidden = ops.mul(silu, up);
|
|
114
|
-
silu.dispose();
|
|
115
116
|
up.dispose();
|
|
116
117
|
const out = this.down.forward(hidden);
|
|
117
118
|
hidden.dispose();
|
|
@@ -119,7 +120,7 @@ class Qwen3MLP {
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
class Qwen3Layer {
|
|
123
|
+
export class Qwen3Layer {
|
|
123
124
|
readonly attn: Qwen3Attention;
|
|
124
125
|
readonly mlp: Qwen3MLP;
|
|
125
126
|
readonly inputNorm: RMSNorm;
|
package/src/model/qwen3_5.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { Weights } from "../weights";
|
|
|
14
14
|
import { MlxArray } from "../mlx/array";
|
|
15
15
|
import { Dtype } from "../mlx/ffi";
|
|
16
16
|
import * as ops from "../mlx/ops";
|
|
17
|
+
import { CompiledFunction } from "../mlx/compile";
|
|
17
18
|
import {
|
|
18
19
|
argmaxLastPosition,
|
|
19
20
|
disposeTriple,
|
|
@@ -32,8 +33,76 @@ import { gatedDeltaUpdate, SSMCache } from "./qwen3-delta";
|
|
|
32
33
|
|
|
33
34
|
const PREFIX = "language_model";
|
|
34
35
|
|
|
36
|
+
// ── Compiled activations ─────────────────────────────────────────────────────
|
|
37
|
+
// The oracle (mlx_lm/models/activations.py + qwen3_next.py) wraps BOTH swiglu
|
|
38
|
+
// activations in `@partial(mx.compile, shapeless=True)`. We match it: every
|
|
39
|
+
// activation site below (the MLP swiglu, the RMSNormGated `_precise_swiglu`, and
|
|
40
|
+
// the conv `nn.silu`) runs through a compiled closure unconditionally, so the
|
|
41
|
+
// dispatched kernel set matches the oracle op-for-op (= mlx-lm, bit-exact). Traced
|
|
42
|
+
// once (shapeless), replayed thereafter; autograd-safe (mx.compile threads VJP
|
|
43
|
+
// through the traced graph). `compiledSwiglu` is exported so the parity test can
|
|
44
|
+
// assert the closure exists and the MLP actually uses it.
|
|
45
|
+
|
|
46
|
+
/** activations.py: `@mx.compile def swiglu(gate, x): return nn.silu(gate) * x`.
|
|
47
|
+
* nn.silu(g) == g * sigmoid(g); mx.compile fuses sigmoid+mul+mul → one kernel. */
|
|
48
|
+
let _swigluClosure: CompiledFunction | null = null;
|
|
49
|
+
export function compiledSwiglu(gate: MlxArray, up: MlxArray): MlxArray {
|
|
50
|
+
if (!_swigluClosure) {
|
|
51
|
+
_swigluClosure = new CompiledFunction((inputs) => {
|
|
52
|
+
const g = inputs[0]!, u = inputs[1]!;
|
|
53
|
+
const sig = ops.sigmoid(g);
|
|
54
|
+
const silu = ops.mul(g, sig); sig.dispose();
|
|
55
|
+
const out = ops.mul(silu, u); silu.dispose();
|
|
56
|
+
return [out];
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return _swigluClosure.apply([gate, up])[0]!;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** qwen3_next.py `_precise_swiglu(h, gate, x)`:
|
|
63
|
+
* gate = nn.silu(gate.astype(f32)); x = x.astype(f32); (gate*x).astype(h.dtype)
|
|
64
|
+
* Used by Qwen3NextRMSNormGated. Inputs: (h=hidden for the out dtype, gate=z,
|
|
65
|
+
* x=rms_norm(hidden)). mx.compile fuses the two casts + silu + mul + cast. */
|
|
66
|
+
let _preciseSwigluClosure: CompiledFunction | null = null;
|
|
67
|
+
export function compiledPreciseSwiglu(h: MlxArray, gate: MlxArray, x: MlxArray): MlxArray {
|
|
68
|
+
if (!_preciseSwigluClosure) {
|
|
69
|
+
_preciseSwigluClosure = new CompiledFunction((inputs) => {
|
|
70
|
+
const hh = inputs[0]!, g = inputs[1]!, xx = inputs[2]!;
|
|
71
|
+
const gf = g.astype(Dtype.float32);
|
|
72
|
+
const sig = ops.sigmoid(gf);
|
|
73
|
+
const silu = ops.mul(gf, sig); gf.dispose(); sig.dispose();
|
|
74
|
+
const xf = xx.astype(Dtype.float32);
|
|
75
|
+
const prod = ops.mul(silu, xf); silu.dispose(); xf.dispose();
|
|
76
|
+
const out = prod.astype(hh.dtype); prod.dispose();
|
|
77
|
+
return [out];
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return _preciseSwigluClosure.apply([h, gate, x])[0]!;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** mlx.nn.silu is `@partial(mx.compile, shapeless=True) def silu(x): x*sigmoid(x)`
|
|
84
|
+
* — a COMPILED kernel, not a standalone sigmoid+mul. Used for the conv
|
|
85
|
+
* activation `nn.silu(conv1d(...))`, matching mlx-lm's fused BV2ISigmoid…_V_. */
|
|
86
|
+
let _siluClosure: CompiledFunction | null = null;
|
|
87
|
+
export function compiledSilu(x: MlxArray): MlxArray {
|
|
88
|
+
if (!_siluClosure) {
|
|
89
|
+
_siluClosure = new CompiledFunction((inputs) => {
|
|
90
|
+
const xx = inputs[0]!;
|
|
91
|
+
const sig = ops.sigmoid(xx);
|
|
92
|
+
const out = ops.mul(xx, sig); sig.dispose(); // x * sigmoid(x)
|
|
93
|
+
return [out];
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return _siluClosure.apply([x])[0]!;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// NOTE: the attention output gate is `self.o_proj(output * mx.sigmoid(gate))`
|
|
100
|
+
// INLINE in mlx-lm (qwen3_next.py) — NOT @mx.compile. It is intentionally kept
|
|
101
|
+
// as a standalone sigmoid + multiply in Qwen3Attention.forward so the dispatched
|
|
102
|
+
// kernel set matches the reference; there is no compiled-output-gate helper.
|
|
103
|
+
|
|
35
104
|
/** Gated-DeltaNet linear-attention layer (mlx-lm GatedDeltaNet). */
|
|
36
|
-
class GatedDeltaNet {
|
|
105
|
+
export class GatedDeltaNet {
|
|
37
106
|
readonly inProjQkv: QuantizedLinear;
|
|
38
107
|
readonly inProjZ: QuantizedLinear;
|
|
39
108
|
readonly inProjB: QuantizedLinear;
|
|
@@ -99,7 +168,7 @@ class GatedDeltaNet {
|
|
|
99
168
|
|
|
100
169
|
const conv = ops.conv1d(convInput, this.convWeight, 1, 0, 1, convDim);
|
|
101
170
|
convInput.dispose();
|
|
102
|
-
const convOut =
|
|
171
|
+
const convOut = compiledSilu(conv); // nn.silu (mx.compile), matches mlx-lm
|
|
103
172
|
conv.dispose();
|
|
104
173
|
|
|
105
174
|
const [qFlat, kFlat, vFlat] = ops.split(
|
|
@@ -144,22 +213,16 @@ class GatedDeltaNet {
|
|
|
144
213
|
|
|
145
214
|
private rmsNormGated(hidden: MlxArray, gate: MlxArray): MlxArray {
|
|
146
215
|
const xn = ops.rmsNorm(hidden, this.normWeight, this.eps); // bf16
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const xf = xn.astype(Dtype.float32);
|
|
216
|
+
// Oracle: _precise_swiglu(hidden, gate, xn) — ALWAYS one @mx.compile kernel
|
|
217
|
+
// (matches mlx-lm; no unfused silu+mul+cast).
|
|
218
|
+
const res = compiledPreciseSwiglu(hidden, gate, xn);
|
|
151
219
|
xn.dispose();
|
|
152
|
-
const prod = ops.mul(sg, xf);
|
|
153
|
-
sg.dispose();
|
|
154
|
-
xf.dispose();
|
|
155
|
-
const res = prod.astype(hidden.dtype);
|
|
156
|
-
prod.dispose();
|
|
157
220
|
return res;
|
|
158
221
|
}
|
|
159
222
|
}
|
|
160
223
|
|
|
161
224
|
/** Full (softmax) attention with output gate + q/k norm + partial RoPE. */
|
|
162
|
-
class Qwen3Attention {
|
|
225
|
+
export class Qwen3Attention {
|
|
163
226
|
readonly qProj: QuantizedLinear;
|
|
164
227
|
readonly kProj: QuantizedLinear;
|
|
165
228
|
readonly vProj: QuantizedLinear;
|
|
@@ -244,20 +307,22 @@ class Qwen3Attention {
|
|
|
244
307
|
|
|
245
308
|
const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
|
|
246
309
|
attn.dispose();
|
|
247
|
-
|
|
310
|
+
const merged = ops.reshape(attnT, [B, L, -1]);
|
|
248
311
|
attnT.dispose();
|
|
249
|
-
//
|
|
250
|
-
|
|
312
|
+
// qwen3_next.py:158 return self.o_proj(output * mx.sigmoid(gate))
|
|
313
|
+
// INLINE, not @mx.compile — copy the exact ops.
|
|
314
|
+
const sig = ops.sigmoid(gate);
|
|
251
315
|
gate.dispose();
|
|
252
|
-
|
|
253
|
-
sg.dispose();
|
|
254
|
-
const out = this.oProj.forward(merged);
|
|
316
|
+
const gated = ops.mul(merged, sig);
|
|
255
317
|
merged.dispose();
|
|
318
|
+
sig.dispose();
|
|
319
|
+
const out = this.oProj.forward(gated);
|
|
320
|
+
gated.dispose();
|
|
256
321
|
return out;
|
|
257
322
|
}
|
|
258
323
|
}
|
|
259
324
|
|
|
260
|
-
class Qwen3MLP {
|
|
325
|
+
export class Qwen3MLP {
|
|
261
326
|
readonly gate: QuantizedLinear;
|
|
262
327
|
readonly up: QuantizedLinear;
|
|
263
328
|
readonly down: QuantizedLinear;
|
|
@@ -271,10 +336,10 @@ class Qwen3MLP {
|
|
|
271
336
|
forward(x: MlxArray): MlxArray {
|
|
272
337
|
const g = this.gate.forward(x);
|
|
273
338
|
const u = this.up.forward(x);
|
|
274
|
-
|
|
339
|
+
// Oracle: down_proj(swiglu(gate_proj(x), up_proj(x))) — swiglu ALWAYS compiled
|
|
340
|
+
// (mlx-lm's @mx.compile swiglu; no unfused silu+mul, matches its kernel set).
|
|
341
|
+
const hidden = compiledSwiglu(g, u);
|
|
275
342
|
g.dispose();
|
|
276
|
-
const hidden = ops.mul(silu, u);
|
|
277
|
-
silu.dispose();
|
|
278
343
|
u.dispose();
|
|
279
344
|
const out = this.down.forward(hidden);
|
|
280
345
|
hidden.dispose();
|
|
@@ -282,7 +347,7 @@ class Qwen3MLP {
|
|
|
282
347
|
}
|
|
283
348
|
}
|
|
284
349
|
|
|
285
|
-
class Qwen3Layer {
|
|
350
|
+
export class Qwen3Layer {
|
|
286
351
|
readonly isLinear: boolean;
|
|
287
352
|
readonly linearAttn: GatedDeltaNet | null = null;
|
|
288
353
|
readonly selfAttn: Qwen3Attention | null = null;
|
package/src/model/support.ts
CHANGED
|
@@ -30,6 +30,15 @@ export function isQwen3Config(config: ModelConfig): boolean {
|
|
|
30
30
|
return config.modelType === "qwen3";
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Qwen3-MoE (model_type `qwen3_moe`, Qwen3MoeForCausalLM) — plain-qwen3
|
|
34
|
+
* attention (per-head q/k norm, GQA, RoPE) with a sparse MoE FFN
|
|
35
|
+
* (gate Linear -> precise softmax -> top-k -> renormalize -> SwitchGLU experts
|
|
36
|
+
* with compiled swiglu). Distinct from the qwen3 embedding backbone and the
|
|
37
|
+
* qwen3_5 gated-DeltaNet hybrid. */
|
|
38
|
+
export function isQwen3MoeConfig(config: ModelConfig): boolean {
|
|
39
|
+
return config.modelType === "qwen3_moe";
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
/** Speculative-decoding drafters (e.g. `gemma4_assistant`) are companion
|
|
34
43
|
* artifacts to a target model — Q-only, centroid-head, no standalone LM
|
|
35
44
|
* head. They are never servable/selectable on their own (the spec path
|
|
@@ -57,6 +66,7 @@ export function supportTier(modelType: string, repoId = ""): "targeted" | "gener
|
|
|
57
66
|
// qwen3_5 / qwen3_5_text (dense hybrid). The MoE variant is a separate type.
|
|
58
67
|
if (modelType === "qwen3_5" || modelType === "qwen3_5_text") return "targeted";
|
|
59
68
|
if (modelType === "qwen3") return "targeted"; // plain Qwen3 (embedding backbone)
|
|
69
|
+
if (modelType === "qwen3_moe") return "targeted"; // Qwen3-MoE (sparse experts)
|
|
60
70
|
if (modelType === "llama" && repoId.toLowerCase().includes("minicpm5-1b-optiq-4bit"))
|
|
61
71
|
return "targeted";
|
|
62
72
|
// Tier-0 generic fallback: the universal-dense descriptor table.
|
|
@@ -72,7 +82,8 @@ export function isSupportedModelConfig(config: ModelConfig): boolean {
|
|
|
72
82
|
if (isDrafterModelType(config.modelType)) return false;
|
|
73
83
|
if (
|
|
74
84
|
config.modelType.startsWith("gemma4") || isMiniCPM5Config(config) ||
|
|
75
|
-
isQwen35Config(config) || isQwen3Config(config) ||
|
|
85
|
+
isQwen35Config(config) || isQwen3Config(config) || isQwen3MoeConfig(config) ||
|
|
86
|
+
isDiffusionGemmaConfig(config)
|
|
76
87
|
) return true;
|
|
77
88
|
// Generic tier: an arch descriptor exists for this model_type (and its
|
|
78
89
|
// config parses — a malformed config is unsupported, not a crash).
|