mlx-bun 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +66 -21
  2. package/THIRD_PARTY_LICENSES.md +1 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +255 -0
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/compat-cli-surface-design.md +4 -3
  10. package/docs/design/decode-speed-program.md +172 -0
  11. package/docs/design/docs-reorg-plan.md +1 -1
  12. package/docs/design/dspark-serving-program.md +371 -0
  13. package/docs/design/dspark-speculative-decoding.md +167 -5
  14. package/docs/design/faithful-l1-consolidation.md +310 -0
  15. package/docs/design/generic-model-support.md +8 -5
  16. package/docs/design/grammar-spec-batching-integration.md +301 -0
  17. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  18. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  19. package/docs/design/omlx-adoption-map.md +98 -0
  20. package/docs/design/orpo-training.md +34 -30
  21. package/docs/design/paged-kv-cache.md +214 -0
  22. package/docs/design/parallel-slots.md +82 -49
  23. package/docs/design/parity-tier-dag.md +43 -33
  24. package/docs/design/repo-cleanup-plan.md +189 -0
  25. package/docs/design/runtime-isolation.md +19 -3
  26. package/docs/design/segmented-backward-training.md +18 -2
  27. package/docs/design/ssd-kv-cold-tier.md +288 -0
  28. package/docs/design/structured-output.md +484 -0
  29. package/docs/design/the-dreaming-master-plan.md +2 -3
  30. package/docs/design/turboquant-kv.md +235 -0
  31. package/docs/design/unified-engine-frontier-plan.md +774 -0
  32. package/docs/design/web-chat-beat-matrix.md +315 -0
  33. package/docs/design/web-chat-redesign.md +1112 -0
  34. package/docs/investigations/decode-roofline-lookagain.md +92 -6
  35. package/docs/investigations/dspark-handoff.md +182 -16
  36. package/docs/investigations/kernel-perf-review-2026-07.md +105 -26
  37. package/docs/investigations/lab-build-journal.md +2 -2
  38. package/docs/investigations/openwiki-evaluation.md +249 -0
  39. package/docs/planning/IDEAS.md +14 -0
  40. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  41. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  42. package/docs/planning/chat-ui-vision.md +50 -0
  43. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  44. package/docs/planning/release-notes-v0.0.10.md +57 -0
  45. package/docs/planning/release-notes-v0.0.11.md +239 -0
  46. package/docs/reference/cli.md +35 -16
  47. package/docs/reference/distribution.md +13 -6
  48. package/docs/reference/embedding.md +35 -14
  49. package/docs/reference/features-matrix.md +97 -0
  50. package/docs/reference/library-api.md +30 -9
  51. package/docs/reference/memory.md +26 -6
  52. package/docs/reference/models.md +11 -1
  53. package/docs/reference/orpo-quickstart.md +3 -4
  54. package/docs/reference/server-api.md +634 -22
  55. package/docs/reference/server-config.md +251 -119
  56. package/docs/reference/training.md +20 -16
  57. package/package.json +5 -3
  58. package/src/anthropic.ts +13 -0
  59. package/src/audio/conformer.ts +647 -0
  60. package/src/audio/decode.ts +133 -0
  61. package/src/audio/features.ts +202 -0
  62. package/src/audio/transcode.ts +67 -0
  63. package/src/cli.ts +402 -107
  64. package/src/config.ts +49 -2
  65. package/src/download.ts +40 -0
  66. package/src/eval/README.md +7 -6
  67. package/src/eval/kl.ts +59 -3
  68. package/src/eval/quality-db.ts +2 -2
  69. package/src/eval/runner.ts +3 -3
  70. package/src/expert-offload.ts +15 -2
  71. package/src/flags.ts +13 -0
  72. package/src/generate.ts +330 -16
  73. package/src/grammar.ts +585 -0
  74. package/src/hub-rest.ts +242 -0
  75. package/src/kv-store.ts +618 -122
  76. package/src/lora.ts +8 -0
  77. package/src/md-modules.d.ts +42 -0
  78. package/src/media-fetch.ts +284 -0
  79. package/src/memory/rest.ts +245 -0
  80. package/src/memory/vault.ts +74 -0
  81. package/src/mlx/array.ts +127 -19
  82. package/src/mlx/autograd.ts +2 -2
  83. package/src/mlx/ffi.ts +27 -3
  84. package/src/mlx/metal-kernel.ts +7 -4
  85. package/src/mlx/ops.ts +179 -23
  86. package/src/mlx/turboquant-ops.ts +831 -0
  87. package/src/mlx/turboquant-tables.ts +237 -0
  88. package/src/model/batched-mask.ts +62 -1
  89. package/src/model/batched-quant.ts +206 -0
  90. package/src/model/batched-rotating-quant.ts +346 -0
  91. package/src/model/batched-rotating.ts +46 -1
  92. package/src/model/compiled-decode.ts +4 -15
  93. package/src/model/factory.ts +10 -4
  94. package/src/model/gemma4-base.ts +391 -34
  95. package/src/model/gemma4.ts +122 -33
  96. package/src/model/generated/gemma4-12b.ts +9 -13
  97. package/src/model/generated/gemma4-26b.ts +10 -5
  98. package/src/model/generated/gemma4-e4b.ts +14 -9
  99. package/src/model/minicpm5.ts +58 -102
  100. package/src/model/paged-kv.ts +253 -0
  101. package/src/model/qwen3-delta.ts +123 -15
  102. package/src/model/qwen3-moe.ts +404 -0
  103. package/src/model/qwen3.ts +9 -8
  104. package/src/model/qwen3_5.ts +99 -25
  105. package/src/model/support.ts +12 -1
  106. package/src/model/universal/dense.ts +40 -5
  107. package/src/model/universal/rope.ts +36 -21
  108. package/src/pi-web.ts +1299 -82
  109. package/src/preflight.ts +27 -8
  110. package/src/prompt-cache.ts +338 -32
  111. package/src/quantize/quantizer.ts +16 -1
  112. package/src/registry.ts +61 -3
  113. package/src/serve/batch-scheduler.ts +1001 -76
  114. package/src/serve/generation-gateway.ts +292 -30
  115. package/src/serve/isolate.ts +417 -0
  116. package/src/serve/lane-registry.ts +44 -0
  117. package/src/serve/session-search.ts +326 -0
  118. package/src/server.ts +1460 -105
  119. package/src/spec/assistant-source.ts +150 -0
  120. package/src/spec/deepspec-source.ts +143 -0
  121. package/src/spec/dflash-source.ts +124 -0
  122. package/src/spec/dspark/ab-stats.ts +136 -0
  123. package/src/spec/dspark/calibration.ts +112 -0
  124. package/src/spec/dspark/deepspec-module.ts +801 -0
  125. package/src/spec/dspark/generate-dflash.ts +72 -20
  126. package/src/spec/dspark/loader.ts +23 -0
  127. package/src/spec/dspark/module-dflash.ts +278 -36
  128. package/src/spec/dspark/quantize-drafter.ts +91 -0
  129. package/src/spec/generate.ts +59 -25
  130. package/src/spec/ngram-source.ts +132 -0
  131. package/src/spec/serve-loop.ts +536 -0
  132. package/src/spec/source.ts +93 -0
  133. package/src/spec/two-model.ts +171 -0
  134. package/src/ssd-cache.ts +285 -0
  135. package/src/tokenizer.ts +288 -2
  136. package/src/tool-approvals.ts +131 -0
  137. package/src/tool-call.ts +408 -20
  138. package/src/train/flash-cce.ts +29 -26
  139. package/src/train/job.ts +0 -2
  140. package/src/train/loss.ts +53 -11
  141. package/src/train/prefix-shared.ts +30 -7
  142. package/src/train/segmented.ts +150 -46
  143. package/src/train/trainer.ts +22 -15
  144. package/src/vision/prompt.ts +217 -48
  145. package/src/web/app.html +1184 -1789
  146. package/src/web/app.js +5667 -0
  147. package/src/web/icon.svg +13 -0
  148. package/src/web/manifest.webmanifest +24 -0
  149. package/src/web/src/adapters-panel.ts +288 -0
  150. package/src/web/src/api.ts +57 -0
  151. package/src/web/src/assistant.ts +485 -0
  152. package/src/web/src/chat.ts +1050 -0
  153. package/src/web/src/composer.ts +959 -0
  154. package/src/web/src/dataset.ts +132 -0
  155. package/src/web/src/finetune.ts +267 -0
  156. package/src/web/src/hub.ts +428 -0
  157. package/src/web/src/main.ts +98 -0
  158. package/src/web/src/markdown.ts +0 -0
  159. package/src/web/src/memory-panel.ts +572 -0
  160. package/src/web/src/model-picker.ts +159 -0
  161. package/src/web/src/palette.ts +346 -0
  162. package/src/web/src/protocol.ts +62 -0
  163. package/src/web/src/quantize.ts +167 -0
  164. package/src/web/src/rag.ts +274 -0
  165. package/src/web/src/sessions.ts +469 -0
  166. package/src/web/src/shell.ts +804 -0
  167. package/src/web/src/status.ts +252 -0
  168. package/src/web/src/ui-catalog.ts +92 -0
  169. package/src/web/sw.js +71 -0
  170. package/src/web/vendor/README.md +61 -0
  171. package/src/web/vendor/hljs-LICENSE +29 -0
  172. package/src/web/vendor/hljs-theme.css +59 -0
  173. package/src/web/vendor/hljs.js +26 -0
  174. package/src/model/fused-decode-kernel.ts +0 -312
  175. package/src/model/fused-geglu-kernel.ts +0 -198
  176. package/src/model/fused-mlp-kernel.ts +0 -169
  177. package/src/model/fused-swiglu-kernel.ts +0 -240
  178. package/src/model/steel-linear-kernel.ts +0 -96
  179. 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
+ }
@@ -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
- const sig = ops.sigmoid(gate);
110
- const silu = ops.mul(gate, sig);
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;
@@ -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 = ops.silu(conv); // [B,S,convDim]
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
- const gf = gate.astype(Dtype.float32);
148
- const sg = ops.silu(gf); // silu(z) in f32
149
- gf.dispose();
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;
@@ -212,8 +275,17 @@ class Qwen3Attention {
212
275
  v = disposing(v, ops.reshape(v, [B, L, this.nKvHeads, this.headDim]));
213
276
  v = disposing(v, ops.transposeAxes(v, [0, 2, 1, 3]));
214
277
 
215
- q = disposing(q, ops.rope(q, this.ropeDims, this.ropeBase, cache.offset, null));
216
- k = disposing(k, ops.rope(k, this.ropeDims, this.ropeBase, cache.offset, null));
278
+ // Batched decode: the scheduler's mask wrapper exposes each row's REAL
279
+ // position as ropeOffsetArr (rows have different prompt lengths); the
280
+ // dynamic-offset kernel is the same fast::rope, bit-exact vs the static
281
+ // form (tests/compile.test.ts). Serial lane: scalar offset, unchanged.
282
+ const offArr = (cache as { ropeOffsetArr?: MlxArray }).ropeOffsetArr;
283
+ q = disposing(q, offArr
284
+ ? ops.ropeDynamic(q, this.ropeDims, this.ropeBase, offArr, null)
285
+ : ops.rope(q, this.ropeDims, this.ropeBase, cache.offset, null));
286
+ k = disposing(k, offArr
287
+ ? ops.ropeDynamic(k, this.ropeDims, this.ropeBase, offArr, null)
288
+ : ops.rope(k, this.ropeDims, this.ropeBase, cache.offset, null));
217
289
 
218
290
  let attn: MlxArray;
219
291
  if (cache instanceof QuantizedKVCache) {
@@ -235,20 +307,22 @@ class Qwen3Attention {
235
307
 
236
308
  const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
237
309
  attn.dispose();
238
- let merged = ops.reshape(attnT, [B, L, -1]);
310
+ const merged = ops.reshape(attnT, [B, L, -1]);
239
311
  attnT.dispose();
240
- // Output gate: o_proj(output * sigmoid(gate)).
241
- const sg = ops.sigmoid(gate);
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);
242
315
  gate.dispose();
243
- merged = disposing(merged, ops.mul(merged, sg));
244
- sg.dispose();
245
- const out = this.oProj.forward(merged);
316
+ const gated = ops.mul(merged, sig);
246
317
  merged.dispose();
318
+ sig.dispose();
319
+ const out = this.oProj.forward(gated);
320
+ gated.dispose();
247
321
  return out;
248
322
  }
249
323
  }
250
324
 
251
- class Qwen3MLP {
325
+ export class Qwen3MLP {
252
326
  readonly gate: QuantizedLinear;
253
327
  readonly up: QuantizedLinear;
254
328
  readonly down: QuantizedLinear;
@@ -262,10 +336,10 @@ class Qwen3MLP {
262
336
  forward(x: MlxArray): MlxArray {
263
337
  const g = this.gate.forward(x);
264
338
  const u = this.up.forward(x);
265
- const silu = ops.silu(g);
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);
266
342
  g.dispose();
267
- const hidden = ops.mul(silu, u);
268
- silu.dispose();
269
343
  u.dispose();
270
344
  const out = this.down.forward(hidden);
271
345
  hidden.dispose();
@@ -273,7 +347,7 @@ class Qwen3MLP {
273
347
  }
274
348
  }
275
349
 
276
- class Qwen3Layer {
350
+ export class Qwen3Layer {
277
351
  readonly isLinear: boolean;
278
352
  readonly linearAttn: GatedDeltaNet | null = null;
279
353
  readonly selfAttn: Qwen3Attention | null = null;
@@ -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) || isDiffusionGemmaConfig(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).