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.
Files changed (173) 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 +58 -35
  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 +14 -6
  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 +3 -2
  27. package/docs/design/ssd-kv-cold-tier.md +130 -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/dspark-handoff.md +182 -16
  35. package/docs/investigations/lab-build-journal.md +2 -2
  36. package/docs/investigations/openwiki-evaluation.md +249 -0
  37. package/docs/planning/IDEAS.md +14 -0
  38. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  39. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  40. package/docs/planning/chat-ui-vision.md +50 -0
  41. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  42. package/docs/planning/release-notes-v0.0.11.md +239 -0
  43. package/docs/reference/cli.md +35 -16
  44. package/docs/reference/distribution.md +13 -6
  45. package/docs/reference/embedding.md +35 -14
  46. package/docs/reference/features-matrix.md +97 -0
  47. package/docs/reference/library-api.md +30 -9
  48. package/docs/reference/memory.md +26 -6
  49. package/docs/reference/models.md +11 -1
  50. package/docs/reference/orpo-quickstart.md +3 -4
  51. package/docs/reference/server-api.md +634 -22
  52. package/docs/reference/server-config.md +250 -121
  53. package/docs/reference/training.md +14 -15
  54. package/package.json +5 -3
  55. package/src/anthropic.ts +13 -0
  56. package/src/audio/conformer.ts +647 -0
  57. package/src/audio/decode.ts +133 -0
  58. package/src/audio/features.ts +202 -0
  59. package/src/audio/transcode.ts +67 -0
  60. package/src/cli.ts +356 -105
  61. package/src/config.ts +49 -2
  62. package/src/download.ts +40 -0
  63. package/src/eval/README.md +7 -6
  64. package/src/eval/kl.ts +59 -3
  65. package/src/eval/quality-db.ts +2 -2
  66. package/src/eval/runner.ts +3 -3
  67. package/src/expert-offload.ts +15 -2
  68. package/src/flags.ts +13 -0
  69. package/src/generate.ts +330 -24
  70. package/src/grammar.ts +585 -0
  71. package/src/hub-rest.ts +242 -0
  72. package/src/kv-store.ts +442 -88
  73. package/src/lora.ts +8 -0
  74. package/src/md-modules.d.ts +42 -0
  75. package/src/media-fetch.ts +284 -0
  76. package/src/memory/rest.ts +245 -0
  77. package/src/memory/vault.ts +74 -0
  78. package/src/mlx/array.ts +106 -19
  79. package/src/mlx/autograd.ts +2 -2
  80. package/src/mlx/ffi.ts +27 -3
  81. package/src/mlx/metal-kernel.ts +7 -4
  82. package/src/mlx/ops.ts +153 -23
  83. package/src/mlx/turboquant-ops.ts +831 -0
  84. package/src/mlx/turboquant-tables.ts +237 -0
  85. package/src/model/batched-mask.ts +62 -1
  86. package/src/model/batched-quant.ts +206 -0
  87. package/src/model/batched-rotating-quant.ts +346 -0
  88. package/src/model/batched-rotating.ts +46 -1
  89. package/src/model/compiled-decode.ts +4 -15
  90. package/src/model/factory.ts +10 -4
  91. package/src/model/gemma4-base.ts +363 -39
  92. package/src/model/gemma4.ts +122 -33
  93. package/src/model/generated/gemma4-12b.ts +9 -13
  94. package/src/model/generated/gemma4-26b.ts +10 -5
  95. package/src/model/generated/gemma4-e4b.ts +14 -9
  96. package/src/model/minicpm5.ts +58 -102
  97. package/src/model/paged-kv.ts +253 -0
  98. package/src/model/qwen3-delta.ts +76 -15
  99. package/src/model/qwen3-moe.ts +404 -0
  100. package/src/model/qwen3.ts +9 -8
  101. package/src/model/qwen3_5.ts +88 -23
  102. package/src/model/support.ts +12 -1
  103. package/src/model/universal/dense.ts +40 -5
  104. package/src/model/universal/rope.ts +36 -21
  105. package/src/pi-web.ts +1299 -82
  106. package/src/preflight.ts +27 -8
  107. package/src/prompt-cache.ts +293 -33
  108. package/src/quantize/quantizer.ts +16 -1
  109. package/src/registry.ts +57 -2
  110. package/src/serve/batch-scheduler.ts +965 -72
  111. package/src/serve/generation-gateway.ts +214 -11
  112. package/src/serve/isolate.ts +417 -0
  113. package/src/serve/lane-registry.ts +44 -0
  114. package/src/serve/session-search.ts +326 -0
  115. package/src/server.ts +1380 -135
  116. package/src/spec/assistant-source.ts +150 -0
  117. package/src/spec/deepspec-source.ts +143 -0
  118. package/src/spec/dflash-source.ts +124 -0
  119. package/src/spec/dspark/ab-stats.ts +136 -0
  120. package/src/spec/dspark/calibration.ts +112 -0
  121. package/src/spec/dspark/deepspec-module.ts +801 -0
  122. package/src/spec/dspark/generate-dflash.ts +72 -20
  123. package/src/spec/dspark/loader.ts +23 -0
  124. package/src/spec/dspark/module-dflash.ts +278 -36
  125. package/src/spec/dspark/quantize-drafter.ts +91 -0
  126. package/src/spec/generate.ts +59 -25
  127. package/src/spec/ngram-source.ts +132 -0
  128. package/src/spec/serve-loop.ts +536 -0
  129. package/src/spec/source.ts +93 -0
  130. package/src/spec/two-model.ts +171 -0
  131. package/src/ssd-cache.ts +107 -35
  132. package/src/tokenizer.ts +288 -2
  133. package/src/tool-approvals.ts +131 -0
  134. package/src/tool-call.ts +408 -20
  135. package/src/train/flash-cce.ts +2 -2
  136. package/src/train/job.ts +0 -2
  137. package/src/train/trainer.ts +1 -4
  138. package/src/vision/prompt.ts +217 -48
  139. package/src/web/app.html +1184 -1789
  140. package/src/web/app.js +5667 -0
  141. package/src/web/icon.svg +13 -0
  142. package/src/web/manifest.webmanifest +24 -0
  143. package/src/web/src/adapters-panel.ts +288 -0
  144. package/src/web/src/api.ts +57 -0
  145. package/src/web/src/assistant.ts +485 -0
  146. package/src/web/src/chat.ts +1050 -0
  147. package/src/web/src/composer.ts +959 -0
  148. package/src/web/src/dataset.ts +132 -0
  149. package/src/web/src/finetune.ts +267 -0
  150. package/src/web/src/hub.ts +428 -0
  151. package/src/web/src/main.ts +98 -0
  152. package/src/web/src/markdown.ts +0 -0
  153. package/src/web/src/memory-panel.ts +572 -0
  154. package/src/web/src/model-picker.ts +159 -0
  155. package/src/web/src/palette.ts +346 -0
  156. package/src/web/src/protocol.ts +62 -0
  157. package/src/web/src/quantize.ts +167 -0
  158. package/src/web/src/rag.ts +274 -0
  159. package/src/web/src/sessions.ts +469 -0
  160. package/src/web/src/shell.ts +804 -0
  161. package/src/web/src/status.ts +252 -0
  162. package/src/web/src/ui-catalog.ts +92 -0
  163. package/src/web/sw.js +71 -0
  164. package/src/web/vendor/README.md +61 -0
  165. package/src/web/vendor/hljs-LICENSE +29 -0
  166. package/src/web/vendor/hljs-theme.css +59 -0
  167. package/src/web/vendor/hljs.js +26 -0
  168. package/src/model/fused-decode-kernel.ts +0 -312
  169. package/src/model/fused-geglu-kernel.ts +0 -198
  170. package/src/model/fused-mlp-kernel.ts +0 -169
  171. package/src/model/fused-swiglu-kernel.ts +0 -240
  172. package/src/model/steel-linear-kernel.ts +0 -96
  173. 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;
@@ -244,20 +307,22 @@ class Qwen3Attention {
244
307
 
245
308
  const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
246
309
  attn.dispose();
247
- let merged = ops.reshape(attnT, [B, L, -1]);
310
+ const merged = ops.reshape(attnT, [B, L, -1]);
248
311
  attnT.dispose();
249
- // Output gate: o_proj(output * sigmoid(gate)).
250
- 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);
251
315
  gate.dispose();
252
- merged = disposing(merged, ops.mul(merged, sg));
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
- 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);
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;
@@ -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).