mlx-bun 0.0.4

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 (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/THIRD_PARTY_LICENSES.md +41 -0
  4. package/bin/mlx-bun.mjs +32 -0
  5. package/docs/curve-designer.html +303 -0
  6. package/docs/design/adapters-end-to-end.md +201 -0
  7. package/docs/design/compat-cli-surface-design.md +227 -0
  8. package/docs/design/docs-reorg-plan.md +152 -0
  9. package/docs/design/hlg-sampling.md +340 -0
  10. package/docs/design/memory-system.md +339 -0
  11. package/docs/design/optimization_plan.md +344 -0
  12. package/docs/design/parallel-slots.md +515 -0
  13. package/docs/design/runtime-isolation.md +247 -0
  14. package/docs/design/segmented-backward-training.md +508 -0
  15. package/docs/investigations/chunk-finetune-experiment.md +84 -0
  16. package/docs/investigations/curve-bisector-routing.md +123 -0
  17. package/docs/investigations/curve-runs/bisector-route.json +98 -0
  18. package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
  19. package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
  20. package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
  21. package/docs/investigations/curve-terrain.html +105 -0
  22. package/docs/investigations/expert-offload-single-user-moe.md +292 -0
  23. package/docs/investigations/hlg-ab.html +34 -0
  24. package/docs/investigations/hlg-explorer.html +238 -0
  25. package/docs/investigations/hlg-figs/cliff.svg +1 -0
  26. package/docs/investigations/hlg-figs/cross.svg +1 -0
  27. package/docs/investigations/hlg-figs/edges.svg +1 -0
  28. package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
  29. package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
  30. package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
  31. package/docs/investigations/hlg-figs/terrain.svg +1 -0
  32. package/docs/investigations/hlg-report.html +262 -0
  33. package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
  34. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
  35. package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
  36. package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
  37. package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
  38. package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
  39. package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
  40. package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
  41. package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
  42. package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
  43. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
  44. package/docs/investigations/hlg-runs/hlg-map.json +383 -0
  45. package/docs/investigations/hlg-runs/run0-original.log +159 -0
  46. package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
  47. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
  48. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
  49. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
  50. package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
  51. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
  52. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
  53. package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
  54. package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
  55. package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
  56. package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
  57. package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
  58. package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
  59. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
  60. package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
  61. package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
  62. package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
  63. package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
  64. package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
  65. package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
  66. package/docs/investigations/hlg-runs/runN-verify.log +116 -0
  67. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
  68. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
  69. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
  70. package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
  71. package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
  72. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
  73. package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
  74. package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
  75. package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
  76. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
  77. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
  78. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
  79. package/docs/investigations/hlg-sampling-investigation.md +970 -0
  80. package/docs/investigations/lab-build-journal.md +157 -0
  81. package/docs/investigations/pi-builtin-investigation.md +308 -0
  82. package/docs/investigations/starter-model-port-handoff.md +323 -0
  83. package/docs/planning/IDEAS.md +26 -0
  84. package/docs/planning/PRODUCT_ROADMAP.md +426 -0
  85. package/docs/planning/ResearchTopics.md +119 -0
  86. package/docs/planning/chunk-task-roadmap.md +77 -0
  87. package/docs/planning/journal.md +155 -0
  88. package/docs/reference/distribution.md +121 -0
  89. package/docs/reference/embedding.md +168 -0
  90. package/docs/reference/library-api.md +153 -0
  91. package/docs/reference/server-api.md +224 -0
  92. package/docs/reference/server-config.md +243 -0
  93. package/docs/segmented-backward-handoff.md +148 -0
  94. package/package.json +68 -0
  95. package/src/anthropic.ts +491 -0
  96. package/src/chat-template.ts +186 -0
  97. package/src/cli.ts +1075 -0
  98. package/src/config.ts +229 -0
  99. package/src/curve-sampler.ts +122 -0
  100. package/src/dataset/generators.ts +1087 -0
  101. package/src/dataset/index.ts +12 -0
  102. package/src/dataset/job.ts +81 -0
  103. package/src/dataset/llm.ts +71 -0
  104. package/src/dataset/registry.ts +528 -0
  105. package/src/download.ts +283 -0
  106. package/src/eval/README.md +86 -0
  107. package/src/eval/kl-prompts.ts +29 -0
  108. package/src/eval/kl.ts +294 -0
  109. package/src/eval/quality-db.ts +92 -0
  110. package/src/eval/runner.ts +102 -0
  111. package/src/eval/score.ts +49 -0
  112. package/src/eval/tasks/bfcl.ts +500 -0
  113. package/src/eval/tasks/gsm8k.ts +85 -0
  114. package/src/eval/tasks/hashhop.ts +109 -0
  115. package/src/eval/tasks/humaneval.ts +252 -0
  116. package/src/eval/tasks/ifeval.ts +354 -0
  117. package/src/eval/tasks/mmlu.ts +145 -0
  118. package/src/evaldb.ts +199 -0
  119. package/src/expert-offload-build.ts +77 -0
  120. package/src/expert-offload.ts +61 -0
  121. package/src/expert-trace.ts +67 -0
  122. package/src/fit.ts +254 -0
  123. package/src/generate.ts +451 -0
  124. package/src/harness-pi.ts +177 -0
  125. package/src/hf-push.ts +628 -0
  126. package/src/index.ts +22 -0
  127. package/src/jobs/db.ts +139 -0
  128. package/src/jobs/index.ts +29 -0
  129. package/src/jobs/job-entry.ts +106 -0
  130. package/src/jobs/runner.ts +304 -0
  131. package/src/jobs/sse.ts +114 -0
  132. package/src/jobs/types.ts +58 -0
  133. package/src/kv-store.ts +185 -0
  134. package/src/lora.ts +314 -0
  135. package/src/md-modules.d.ts +7 -0
  136. package/src/mlx/array.ts +237 -0
  137. package/src/mlx/autograd.ts +267 -0
  138. package/src/mlx/checkpoint.ts +116 -0
  139. package/src/mlx/compile.ts +158 -0
  140. package/src/mlx/custom-vjp.ts +140 -0
  141. package/src/mlx/ffi.ts +433 -0
  142. package/src/mlx/metal-kernel.ts +130 -0
  143. package/src/mlx/ops.ts +707 -0
  144. package/src/mmap.ts +90 -0
  145. package/src/model/batched-mask.ts +204 -0
  146. package/src/model/batched-rotating.ts +300 -0
  147. package/src/model/compiled-decode.ts +681 -0
  148. package/src/model/factory.ts +27 -0
  149. package/src/model/fingerprint.ts +33 -0
  150. package/src/model/flash-attention.ts +674 -0
  151. package/src/model/fused-decode-kernel.ts +305 -0
  152. package/src/model/fused-geglu-kernel.ts +87 -0
  153. package/src/model/gemma4-base.ts +1616 -0
  154. package/src/model/gemma4.ts +1012 -0
  155. package/src/model/generated/gemma4-12b.ts +526 -0
  156. package/src/model/generated/gemma4-26b.ts +500 -0
  157. package/src/model/generated/gemma4-e4b.ts +757 -0
  158. package/src/model/generated/index.ts +18 -0
  159. package/src/model/minicpm5.ts +278 -0
  160. package/src/model/qwen3-delta.ts +224 -0
  161. package/src/model/qwen3_5.ts +429 -0
  162. package/src/model/support.ts +46 -0
  163. package/src/native-pack.ts +152 -0
  164. package/src/pi-provider.ts +95 -0
  165. package/src/pi-terminal.ts +264 -0
  166. package/src/pi-web.ts +924 -0
  167. package/src/preflight.ts +123 -0
  168. package/src/prompt-cache.ts +113 -0
  169. package/src/quantize/allocator.ts +627 -0
  170. package/src/quantize/calibration.ts +180 -0
  171. package/src/quantize/config-writer.ts +132 -0
  172. package/src/quantize/index.ts +57 -0
  173. package/src/quantize/job.ts +136 -0
  174. package/src/quantize/quantizer.ts +462 -0
  175. package/src/quantize/safetensors-writer.ts +171 -0
  176. package/src/quantize/sensitivity.ts +234 -0
  177. package/src/registry.ts +336 -0
  178. package/src/responses.ts +774 -0
  179. package/src/safetensors.ts +158 -0
  180. package/src/sampler.ts +517 -0
  181. package/src/serve/batch-scheduler.ts +362 -0
  182. package/src/serve/generation-gateway.ts +160 -0
  183. package/src/server.ts +1891 -0
  184. package/src/spec/drafter.ts +261 -0
  185. package/src/spec/generate.ts +221 -0
  186. package/src/status-page.html +497 -0
  187. package/src/tokenizer.ts +38 -0
  188. package/src/tool-call.ts +242 -0
  189. package/src/train/dataset.ts +461 -0
  190. package/src/train/export.ts +32 -0
  191. package/src/train/forward.ts +229 -0
  192. package/src/train/index.ts +43 -0
  193. package/src/train/job.ts +148 -0
  194. package/src/train/lora-params.ts +194 -0
  195. package/src/train/loss.ts +342 -0
  196. package/src/train/merge.ts +195 -0
  197. package/src/train/optimizer.ts +176 -0
  198. package/src/train/rank.ts +101 -0
  199. package/src/train/segmented.ts +520 -0
  200. package/src/train/trainer.ts +600 -0
  201. package/src/tui.ts +161 -0
  202. package/src/vision/embedder.ts +168 -0
  203. package/src/vision/preprocess.ts +247 -0
  204. package/src/vision/prompt.ts +158 -0
  205. package/src/vision/siglip.ts +599 -0
  206. package/src/web/app.html +2471 -0
  207. package/src/web/skills/web-research/SKILL.md +44 -0
  208. package/src/web/skills.ts +59 -0
  209. package/src/web-tools.ts +426 -0
  210. package/src/weights.ts +91 -0
@@ -0,0 +1,429 @@
1
+ // Concrete model graph for Qwen3.5 hybrid models (e.g. Qwen3.6-27B-OptiQ-4bit).
2
+ // Port target: mlx_lm.models.qwen3_5 (+ qwen3_next Attention/MLP/RMSNormGated,
3
+ // gated_delta recurrence). The architecture is a 64-layer stack where every
4
+ // `fullAttentionInterval`-th layer is standard softmax attention and the rest
5
+ // are gated-DeltaNet linear-attention layers. Weights carry a
6
+ // `language_model.` prefix.
7
+ //
8
+ // Parity bars: bf16 KV → bit-exact vs mlx-lm; mixed-precision KV → bit-exact vs
9
+ // mlx-optiq (the 16 full-attention layers quantized per kv_config.json, via the
10
+ // shared maybeQuantizeKv path).
11
+
12
+ import type { ModelConfig } from "../config";
13
+ import type { Weights } from "../weights";
14
+ import { MlxArray } from "../mlx/array";
15
+ import { Dtype } from "../mlx/ffi";
16
+ import * as ops from "../mlx/ops";
17
+ import {
18
+ argmaxLastPosition,
19
+ disposeTriple,
20
+ disposing,
21
+ KVCache,
22
+ LoraState,
23
+ QuantizedKVCache,
24
+ QuantizedEmbedding,
25
+ QuantizedLinear,
26
+ quantizedSdpa,
27
+ RMSNorm,
28
+ type Cache,
29
+ type Mask,
30
+ } from "./gemma4-base";
31
+ import { gatedDeltaUpdate, SSMCache } from "./qwen3-delta";
32
+
33
+ const PREFIX = "language_model";
34
+
35
+ /** Gated-DeltaNet linear-attention layer (mlx-lm GatedDeltaNet). */
36
+ class GatedDeltaNet {
37
+ readonly inProjQkv: QuantizedLinear;
38
+ readonly inProjZ: QuantizedLinear;
39
+ readonly inProjB: QuantizedLinear;
40
+ readonly inProjA: QuantizedLinear;
41
+ readonly outProj: QuantizedLinear;
42
+ readonly convWeight: MlxArray;
43
+ readonly aLog: MlxArray;
44
+ readonly dtBias: MlxArray;
45
+ readonly normWeight: MlxArray;
46
+ readonly eps: number;
47
+ readonly numKHeads: number;
48
+ readonly numVHeads: number;
49
+ readonly headKDim: number;
50
+ readonly headVDim: number;
51
+ readonly keyDim: number;
52
+ readonly valueDim: number;
53
+ readonly convKernel: number;
54
+
55
+ constructor(weights: Weights, config: ModelConfig, prefix: string) {
56
+ const t = config.text;
57
+ this.numKHeads = t.linearNumKeyHeads;
58
+ this.numVHeads = t.linearNumValueHeads;
59
+ this.headKDim = t.linearKeyHeadDim;
60
+ this.headVDim = t.linearValueHeadDim;
61
+ this.keyDim = this.headKDim * this.numKHeads;
62
+ this.valueDim = this.headVDim * this.numVHeads;
63
+ this.convKernel = t.linearConvKernelDim;
64
+ this.eps = t.rmsNormEps;
65
+ this.inProjQkv = QuantizedLinear.load(weights, `${prefix}.in_proj_qkv`, config);
66
+ this.inProjZ = QuantizedLinear.load(weights, `${prefix}.in_proj_z`, config);
67
+ this.inProjB = QuantizedLinear.load(weights, `${prefix}.in_proj_b`, config);
68
+ this.inProjA = QuantizedLinear.load(weights, `${prefix}.in_proj_a`, config);
69
+ this.outProj = QuantizedLinear.load(weights, `${prefix}.out_proj`, config);
70
+ this.convWeight = weights.tensor(`${prefix}.conv1d.weight`);
71
+ this.aLog = weights.tensor(`${prefix}.A_log`);
72
+ this.dtBias = weights.tensor(`${prefix}.dt_bias`);
73
+ this.normWeight = weights.tensor(`${prefix}.norm.weight`);
74
+ }
75
+
76
+ forward(x: MlxArray, cache: SSMCache): MlxArray {
77
+ const [B, S] = x.shape as [number, number, number];
78
+ const convDim = this.keyDim * 2 + this.valueDim;
79
+ const nKeep = this.convKernel - 1;
80
+
81
+ const qkv = this.inProjQkv.forward(x); // [B,S,convDim]
82
+ let z = this.inProjZ.forward(x);
83
+ z = disposing(z, ops.reshape(z, [B, S, this.numVHeads, this.headVDim]));
84
+ const b = this.inProjB.forward(x); // [B,S,numVHeads]
85
+ const a = this.inProjA.forward(x); // [B,S,numVHeads]
86
+
87
+ // Causal depthwise conv with the conv-state prefix (B=1: no ssm mask).
88
+ const convState =
89
+ cache.conv ?? ops.zeros([B, nKeep, convDim], x.dtype);
90
+ const convInput = ops.concatAxis([convState, qkv], 1); // [B,S+nKeep,convDim]
91
+ if (!cache.conv) convState.dispose();
92
+ qkv.dispose();
93
+ // New conv state = last nKeep rows (contiguous, the array is sliced).
94
+ const newConv = ops.contiguous(
95
+ convInput.slice([0, S, 0], [B, S + nKeep, convDim]),
96
+ );
97
+ cache.conv?.dispose();
98
+ cache.conv = newConv;
99
+
100
+ const conv = ops.conv1d(convInput, this.convWeight, 1, 0, 1, convDim);
101
+ convInput.dispose();
102
+ const convOut = ops.silu(conv); // [B,S,convDim]
103
+ conv.dispose();
104
+
105
+ const [qFlat, kFlat, vFlat] = ops.split(
106
+ convOut, [this.keyDim, 2 * this.keyDim], -1,
107
+ ) as [MlxArray, MlxArray, MlxArray];
108
+ convOut.dispose();
109
+ let q = ops.reshape(qFlat, [B, S, this.numKHeads, this.headKDim]);
110
+ qFlat.dispose();
111
+ let k = ops.reshape(kFlat, [B, S, this.numKHeads, this.headKDim]);
112
+ kFlat.dispose();
113
+ const v = disposing(vFlat, ops.reshape(vFlat, [B, S, this.numVHeads, this.headVDim]));
114
+
115
+ // inv_scale = head_k_dim ** -0.5; q *= inv_scale², k *= inv_scale.
116
+ const invScale = Math.pow(this.headKDim, -0.5);
117
+ q = disposing(q, ops.rmsNorm(q, null, 1e-6));
118
+ q = disposing(q, ops.mulScalar(q, invScale * invScale));
119
+ k = disposing(k, ops.rmsNorm(k, null, 1e-6));
120
+ k = disposing(k, ops.mulScalar(k, invScale));
121
+
122
+ const [out, newState] = gatedDeltaUpdate(
123
+ q, k, v, a, b, this.aLog, this.dtBias, cache.recurrent,
124
+ );
125
+ q.dispose();
126
+ k.dispose();
127
+ v.dispose();
128
+ a.dispose();
129
+ b.dispose();
130
+ cache.recurrent?.dispose();
131
+ cache.recurrent = newState;
132
+ cache.advance(S);
133
+
134
+ // RMSNormGated: silu(z_f32) * rms_norm(out)_f32, cast back to out dtype.
135
+ const gated = this.rmsNormGated(out, z);
136
+ out.dispose();
137
+ z.dispose();
138
+ const merged = ops.reshape(gated, [B, S, this.valueDim]);
139
+ gated.dispose();
140
+ const result = this.outProj.forward(merged);
141
+ merged.dispose();
142
+ return result;
143
+ }
144
+
145
+ private rmsNormGated(hidden: MlxArray, gate: MlxArray): MlxArray {
146
+ 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);
151
+ 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
+ return res;
158
+ }
159
+ }
160
+
161
+ /** Full (softmax) attention with output gate + q/k norm + partial RoPE. */
162
+ class Qwen3Attention {
163
+ readonly qProj: QuantizedLinear;
164
+ readonly kProj: QuantizedLinear;
165
+ readonly vProj: QuantizedLinear;
166
+ readonly oProj: QuantizedLinear;
167
+ readonly qNorm: RMSNorm;
168
+ readonly kNorm: RMSNorm;
169
+ readonly nHeads: number;
170
+ readonly nKvHeads: number;
171
+ readonly headDim: number;
172
+ readonly scale: number;
173
+ readonly ropeDims: number;
174
+ readonly ropeBase: number;
175
+
176
+ constructor(weights: Weights, config: ModelConfig, prefix: string) {
177
+ const t = config.text;
178
+ this.nHeads = t.numAttentionHeads;
179
+ this.nKvHeads = t.numKeyValueHeads;
180
+ this.headDim = t.headDim;
181
+ this.scale = Math.pow(this.headDim, -0.5);
182
+ this.ropeDims = Math.trunc(this.headDim * t.partialRotaryFactor);
183
+ this.ropeBase = t.ropeParameters.full_attention?.ropeTheta ?? 10000;
184
+ this.qProj = QuantizedLinear.load(weights, `${prefix}.q_proj`, config);
185
+ this.kProj = QuantizedLinear.load(weights, `${prefix}.k_proj`, config);
186
+ this.vProj = QuantizedLinear.load(weights, `${prefix}.v_proj`, config);
187
+ this.oProj = QuantizedLinear.load(weights, `${prefix}.o_proj`, config);
188
+ this.qNorm = new RMSNorm(weights.tensor(`${prefix}.q_norm.weight`), t.rmsNormEps);
189
+ this.kNorm = new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps);
190
+ }
191
+
192
+ forward(x: MlxArray, mask: Mask, cache: Cache): MlxArray {
193
+ const [B, L] = x.shape as [number, number, number];
194
+
195
+ // q_proj emits 2× head_dim per head → split into queries + gate.
196
+ const qp = this.qProj.forward(x);
197
+ const qpr = disposing(qp, ops.reshape(qp, [B, L, this.nHeads, this.headDim * 2]));
198
+ const [qHeads, gateHeads] = ops.split(qpr, [this.headDim], -1) as [MlxArray, MlxArray];
199
+ qpr.dispose();
200
+ const gate = disposing(gateHeads, ops.reshape(gateHeads, [B, L, this.nHeads * this.headDim]));
201
+
202
+ let k = this.kProj.forward(x);
203
+ let v = this.vProj.forward(x);
204
+
205
+ // q/k norm over head_dim BEFORE transpose (reference order).
206
+ let q = this.qNorm.forward(qHeads);
207
+ qHeads.dispose();
208
+ q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
209
+ k = disposing(k, ops.reshape(k, [B, L, this.nKvHeads, this.headDim]));
210
+ k = disposing(k, this.kNorm.forward(k));
211
+ k = disposing(k, ops.transposeAxes(k, [0, 2, 1, 3]));
212
+ v = disposing(v, ops.reshape(v, [B, L, this.nKvHeads, this.headDim]));
213
+ v = disposing(v, ops.transposeAxes(v, [0, 2, 1, 3]));
214
+
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));
217
+
218
+ let attn: MlxArray;
219
+ if (cache instanceof QuantizedKVCache) {
220
+ const [keys, values] = cache.updateAndFetchQuantized(k, v);
221
+ k.dispose();
222
+ v.dispose();
223
+ attn = quantizedSdpa(q, keys, values, this.scale, mask, cache.groupSize, cache.bits);
224
+ disposeTriple(keys);
225
+ disposeTriple(values);
226
+ } else {
227
+ const [keys, values] = cache.updateAndFetch(k, v);
228
+ k.dispose();
229
+ v.dispose();
230
+ attn = ops.sdpa(q, keys, values, this.scale, mask.mode, mask.arr);
231
+ keys.dispose();
232
+ values.dispose();
233
+ }
234
+ q.dispose();
235
+
236
+ const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
237
+ attn.dispose();
238
+ let merged = ops.reshape(attnT, [B, L, -1]);
239
+ attnT.dispose();
240
+ // Output gate: o_proj(output * sigmoid(gate)).
241
+ const sg = ops.sigmoid(gate);
242
+ gate.dispose();
243
+ merged = disposing(merged, ops.mul(merged, sg));
244
+ sg.dispose();
245
+ const out = this.oProj.forward(merged);
246
+ merged.dispose();
247
+ return out;
248
+ }
249
+ }
250
+
251
+ class Qwen3MLP {
252
+ readonly gate: QuantizedLinear;
253
+ readonly up: QuantizedLinear;
254
+ readonly down: QuantizedLinear;
255
+
256
+ constructor(weights: Weights, config: ModelConfig, prefix: string) {
257
+ this.gate = QuantizedLinear.load(weights, `${prefix}.gate_proj`, config);
258
+ this.up = QuantizedLinear.load(weights, `${prefix}.up_proj`, config);
259
+ this.down = QuantizedLinear.load(weights, `${prefix}.down_proj`, config);
260
+ }
261
+
262
+ forward(x: MlxArray): MlxArray {
263
+ const g = this.gate.forward(x);
264
+ const u = this.up.forward(x);
265
+ const silu = ops.silu(g);
266
+ g.dispose();
267
+ const hidden = ops.mul(silu, u);
268
+ silu.dispose();
269
+ u.dispose();
270
+ const out = this.down.forward(hidden);
271
+ hidden.dispose();
272
+ return out;
273
+ }
274
+ }
275
+
276
+ class Qwen3Layer {
277
+ readonly isLinear: boolean;
278
+ readonly linearAttn: GatedDeltaNet | null = null;
279
+ readonly selfAttn: Qwen3Attention | null = null;
280
+ readonly mlp: Qwen3MLP;
281
+ readonly inputNorm: RMSNorm;
282
+ readonly postAttnNorm: RMSNorm;
283
+
284
+ constructor(weights: Weights, config: ModelConfig, layerIdx: number) {
285
+ const prefix = `${PREFIX}.model.layers.${layerIdx}`;
286
+ this.isLinear = (layerIdx + 1) % config.text.fullAttentionInterval !== 0;
287
+ if (this.isLinear)
288
+ this.linearAttn = new GatedDeltaNet(weights, config, `${prefix}.linear_attn`);
289
+ else this.selfAttn = new Qwen3Attention(weights, config, `${prefix}.self_attn`);
290
+ this.mlp = new Qwen3MLP(weights, config, `${prefix}.mlp`);
291
+ this.inputNorm = new RMSNorm(weights.tensor(`${prefix}.input_layernorm.weight`), config.text.rmsNormEps);
292
+ this.postAttnNorm = new RMSNorm(weights.tensor(`${prefix}.post_attention_layernorm.weight`), config.text.rmsNormEps);
293
+ }
294
+
295
+ forward(x: MlxArray, faMask: Mask, cache: Cache): MlxArray {
296
+ const xn = this.inputNorm.forward(x);
297
+ const r = this.isLinear
298
+ ? this.linearAttn!.forward(xn, cache as SSMCache)
299
+ : this.selfAttn!.forward(xn, faMask, cache);
300
+ xn.dispose();
301
+ const h = ops.add(x, r);
302
+ r.dispose();
303
+ const hn = this.postAttnNorm.forward(h);
304
+ const m = this.mlp.forward(hn);
305
+ hn.dispose();
306
+ const out = ops.add(h, m);
307
+ h.dispose();
308
+ m.dispose();
309
+ return out;
310
+ }
311
+ }
312
+
313
+ export class Qwen35Model {
314
+ readonly config: ModelConfig;
315
+ readonly weightsBytes: number;
316
+ /** Base path for LoRA target keys (weights carry the language_model prefix). */
317
+ readonly prefixBase = "language_model.model";
318
+ readonly loraState = new LoraState();
319
+ readonly embed: QuantizedEmbedding;
320
+ readonly layers: Qwen3Layer[];
321
+ readonly finalNorm: RMSNorm;
322
+ /** null when tied: the output head reuses embed_tokens (embed.asLinear). */
323
+ readonly lmHead: QuantizedLinear | null;
324
+ readonly tied: boolean;
325
+ readonly faIdx: number;
326
+
327
+ constructor(weights: Weights, config: ModelConfig) {
328
+ this.config = config;
329
+ this.tied = config.text.tieWordEmbeddings;
330
+ this.weightsBytes = [...weights.shards.files.values()]
331
+ .reduce((a, f) => a + f.mmap.size, 0);
332
+ this.embed = QuantizedEmbedding.load(weights, `${PREFIX}.model.embed_tokens`, config);
333
+ this.layers = Array.from(
334
+ { length: config.text.numHiddenLayers },
335
+ (_, i) => new Qwen3Layer(weights, config, i),
336
+ );
337
+ this.finalNorm = new RMSNorm(weights.tensor(`${PREFIX}.model.norm.weight`), config.text.rmsNormEps);
338
+ // Tied models (e.g. Qwen3.5-4B) ship no lm_head; the reference uses
339
+ // embed_tokens.as_linear (mlx-lm qwen3_5 TextModel.__call__).
340
+ this.lmHead = this.tied ? null : QuantizedLinear.load(weights, `${PREFIX}.lm_head`, config);
341
+ this.faIdx = config.text.fullAttentionInterval - 1;
342
+ }
343
+
344
+ loraTargets(): Map<string, QuantizedLinear> {
345
+ const out = new Map<string, QuantizedLinear>();
346
+ for (let i = 0; i < this.layers.length; i++) {
347
+ const l = this.layers[i]!;
348
+ const p = `${PREFIX}.model.layers.${i}`;
349
+ if (l.selfAttn) {
350
+ out.set(`${p}.self_attn.q_proj`, l.selfAttn.qProj);
351
+ out.set(`${p}.self_attn.k_proj`, l.selfAttn.kProj);
352
+ out.set(`${p}.self_attn.v_proj`, l.selfAttn.vProj);
353
+ out.set(`${p}.self_attn.o_proj`, l.selfAttn.oProj);
354
+ } else if (l.linearAttn) {
355
+ out.set(`${p}.linear_attn.in_proj_qkv`, l.linearAttn.inProjQkv);
356
+ out.set(`${p}.linear_attn.in_proj_z`, l.linearAttn.inProjZ);
357
+ out.set(`${p}.linear_attn.in_proj_b`, l.linearAttn.inProjB);
358
+ out.set(`${p}.linear_attn.in_proj_a`, l.linearAttn.inProjA);
359
+ out.set(`${p}.linear_attn.out_proj`, l.linearAttn.outProj);
360
+ }
361
+ out.set(`${p}.mlp.gate_proj`, l.mlp.gate);
362
+ out.set(`${p}.mlp.up_proj`, l.mlp.up);
363
+ out.set(`${p}.mlp.down_proj`, l.mlp.down);
364
+ }
365
+ return out;
366
+ }
367
+
368
+ makeCache(): Cache[] {
369
+ return this.layers.map((l) => (l.isLinear ? new SSMCache() : new KVCache()));
370
+ }
371
+
372
+ forwardHidden(ids: MlxArray, cache: Cache[]): MlxArray {
373
+ const h = this.embed.encode(ids);
374
+ return this.forwardLayers(h, cache);
375
+ }
376
+
377
+ forwardEmbeddings(_embeds: MlxArray, _cache: Cache[], _bidir: MlxArray | null): MlxArray {
378
+ throw new Error("qwen3_5 vision/input-embedding path is not supported");
379
+ }
380
+
381
+ protected forwardLayers(h0: MlxArray, cache: Cache[]): MlxArray {
382
+ const L = h0.shape[1]!;
383
+ // One full-attention mask shared by all full layers (same offset); linear
384
+ // layers see no ssm mask at B=1.
385
+ const faMask = cache[this.faIdx]!.makeMask(L, null);
386
+ let h = h0;
387
+ for (let i = 0; i < this.layers.length; i++) {
388
+ const next = this.layers[i]!.forward(h, faMask, cache[i]!);
389
+ h.dispose();
390
+ h = next;
391
+ }
392
+ faMask.arr?.dispose();
393
+ return disposing(h, this.finalNorm.forward(h));
394
+ }
395
+
396
+ logitsFromHidden(h: MlxArray): MlxArray {
397
+ return this.tied ? this.embed.asLinear(h) : this.lmHead!.forward(h);
398
+ }
399
+
400
+ forward(tokens: number[] | MlxArray, cache: Cache[]): MlxArray {
401
+ const ids = Array.isArray(tokens)
402
+ ? ops.fromInt32(tokens, [1, tokens.length])
403
+ : tokens;
404
+ const h = this.forwardHidden(ids, cache);
405
+ if (Array.isArray(tokens)) ids.dispose();
406
+ const logits = this.logitsFromHidden(h);
407
+ h.dispose();
408
+ return logits;
409
+ }
410
+
411
+ generate(promptTokens: number[], maxTokens: number, eosIds: number[] = []): number[] {
412
+ const cache = this.makeCache();
413
+ const out: number[] = [];
414
+ try {
415
+ let tokens = promptTokens;
416
+ for (let step = 0; step < maxTokens; step++) {
417
+ const logits = this.forward(tokens, cache);
418
+ const next = argmaxLastPosition(logits);
419
+ logits.dispose();
420
+ if (eosIds.includes(next)) break;
421
+ out.push(next);
422
+ tokens = [next];
423
+ }
424
+ } finally {
425
+ for (const c of cache) c.dispose();
426
+ }
427
+ return out;
428
+ }
429
+ }
@@ -0,0 +1,46 @@
1
+ import type { ModelConfig } from "../config";
2
+
3
+ export function isMiniCPM5Config(config: ModelConfig): boolean {
4
+ const t = config.text;
5
+ return config.modelType === "llama" &&
6
+ t.hiddenSize === 1536 &&
7
+ t.numHiddenLayers === 24 &&
8
+ t.numAttentionHeads === 16 &&
9
+ t.numKeyValueHeads === 2 &&
10
+ t.headDim === 128 &&
11
+ t.vocabSize === 130560 &&
12
+ t.tieWordEmbeddings === false;
13
+ }
14
+
15
+ /** Qwen3.5 hybrid gated-DeltaNet family (model_type qwen3_5 / qwen3_5_text).
16
+ * Dense MLP only for now — the MoE variant (qwen3_5_moe) is deferred. */
17
+ export function isQwen35Config(config: ModelConfig): boolean {
18
+ return (config.modelType === "qwen3_5" || config.modelType === "qwen3_5_text") &&
19
+ !config.text.enableMoeBlock &&
20
+ config.text.numExperts === 0 &&
21
+ config.text.linearNumValueHeads > 0 &&
22
+ config.text.fullAttentionInterval > 0;
23
+ }
24
+
25
+ /** Speculative-decoding drafters (e.g. `gemma4_assistant`) are companion
26
+ * artifacts to a target model — Q-only, centroid-head, no standalone LM
27
+ * head. They are never servable/selectable on their own (the spec path
28
+ * loads them by explicit path), so they must be excluded from model
29
+ * resolution and the supported-model lists. See src/spec/drafter.ts. */
30
+ export function isDrafterModelType(modelType: string): boolean {
31
+ return modelType.endsWith("_assistant");
32
+ }
33
+
34
+ export function isSupportedModelRecord(modelType: string, repoId = ""): boolean {
35
+ if (isDrafterModelType(modelType)) return false;
36
+ if (modelType.startsWith("gemma4")) return true;
37
+ // qwen3_5 / qwen3_5_text (dense hybrid). The MoE variant is a separate type.
38
+ if (modelType === "qwen3_5" || modelType === "qwen3_5_text") return true;
39
+ return modelType === "llama" && repoId.toLowerCase().includes("minicpm5-1b-optiq-4bit");
40
+ }
41
+
42
+ export function isSupportedModelConfig(config: ModelConfig): boolean {
43
+ if (isDrafterModelType(config.modelType)) return false;
44
+ return config.modelType.startsWith("gemma4") || isMiniCPM5Config(config) ||
45
+ isQwen35Config(config);
46
+ }
@@ -0,0 +1,152 @@
1
+ // Native runtime pack (decision 2026-06-12, "one binary" discussion):
2
+ // the mlx-bun executable ships alone (~61 MB, 100% our code + Bun); the
3
+ // MLX native runtime (libmlx, libmlxc, libjaccl, mlx.metallib) is a
4
+ // separate versioned tarball downloaded ON FIRST RUN into
5
+ // ~/Library/Caches/mlx-bun/native-v<ver>-<arch>/ — resumable and
6
+ // sha256-verified through the same downloadOne machinery as model
7
+ // downloads, visible on the status page /downloads tracker.
8
+ //
9
+ // Resolution order for the libs (mirrored in src/mlx/ffi.ts):
10
+ // MLX_BUN_LIBMLXC env > beside the executable (sidecar dist/ layout,
11
+ // docs/reference/embedding.md) > the native-pack cache dir > homebrew.
12
+ // Dev trees and embedders therefore never download anything.
13
+
14
+ import { existsSync, mkdirSync, renameSync, rmSync } from "node:fs";
15
+ import { homedir, tmpdir } from "node:os";
16
+ import { dirname, join } from "node:path";
17
+ import { downloadOne } from "./download";
18
+
19
+ export const NATIVE_PACK_VERSION = "0.1.0";
20
+ export const NATIVE_PACK_FILES = [
21
+ "libmlxc.dylib", "libmlx.dylib", "libjaccl.dylib", "mlx.metallib",
22
+ ] as const;
23
+
24
+ const SHA256: Record<string, string> = {
25
+ arm64: "90fa6a85bae648910bb957df3757270d581caf3294a06fb5195b44c5937d99da",
26
+ };
27
+ const SIZE: Record<string, number> = {
28
+ arm64: 52_300_530,
29
+ };
30
+
31
+ export function nativePackName(arch = process.arch): string {
32
+ return `mlx-bun-native-v${NATIVE_PACK_VERSION}-${arch}.tar.gz`;
33
+ }
34
+
35
+ /** GitHub release asset; override with MLX_BUN_NATIVE_PACK_URL. While
36
+ * the repo is private the download needs a token (GITHUB_TOKEN or
37
+ * `gh auth token`); once public it works anonymously. */
38
+ export function nativePackUrl(arch = process.arch): string {
39
+ return process.env.MLX_BUN_NATIVE_PACK_URL ??
40
+ `https://github.com/joshuarossi/mlx-bun/releases/download/native-v${NATIVE_PACK_VERSION}/${nativePackName(arch)}`;
41
+ }
42
+
43
+ export function nativePackDir(arch = process.arch): string {
44
+ return join(homedir(), "Library", "Caches", "mlx-bun", `native-v${NATIVE_PACK_VERSION}-${arch}`);
45
+ }
46
+
47
+ /** Where the native runtime currently resolves from, or null if absent
48
+ * everywhere (= a fresh machine that needs the pack). */
49
+ export function nativeRuntimeDir(): string | null {
50
+ if (process.env.MLX_BUN_LIBMLXC) return dirname(process.env.MLX_BUN_LIBMLXC);
51
+ const candidates = [
52
+ dirname(process.execPath),
53
+ nativePackDir(),
54
+ "/opt/homebrew/lib",
55
+ "/usr/local/lib",
56
+ ];
57
+ for (const dir of candidates)
58
+ if (existsSync(join(dir, "libmlxc.dylib"))) return dir;
59
+ return null;
60
+ }
61
+
62
+ async function githubToken(): Promise<string | null> {
63
+ if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
64
+ try {
65
+ const proc = Bun.spawn(["gh", "auth", "token"], { stdout: "pipe", stderr: "ignore" });
66
+ const out = (await new Response(proc.stdout).text()).trim();
67
+ return (await proc.exited) === 0 && out ? out : null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /** Map a github.com/<o>/<r>/releases/download/<tag>/<name> URL to a
74
+ * presigned asset URL via the API. Returns null when no token is
75
+ * available or the lookup fails (caller falls back to the plain URL,
76
+ * which works for public repos). */
77
+ async function resolveGithubAssetUrl(url: string): Promise<string | null> {
78
+ const m = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/releases\/download\/([^/]+)\/(.+)$/.exec(url);
79
+ const token = await githubToken();
80
+ if (!m || !token) return null;
81
+ try {
82
+ const [, owner, repo, tag, name] = m;
83
+ const rel = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/tags/${tag}`, {
84
+ headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json" },
85
+ });
86
+ if (!rel.ok) return null;
87
+ const assets = ((await rel.json()) as { assets: Array<{ name: string; url: string }> }).assets;
88
+ const asset = assets.find((a) => a.name === name);
89
+ if (!asset) return null;
90
+ const head = await fetch(asset.url, {
91
+ redirect: "manual",
92
+ headers: { authorization: `Bearer ${token}`, accept: "application/octet-stream" },
93
+ });
94
+ return head.headers.get("location"); // presigned; no auth needed past here
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ export interface EnsureNativeOptions {
101
+ url?: string;
102
+ sha256?: string;
103
+ sizeBytes?: number;
104
+ destDir?: string;
105
+ onProgress?: (received: number, total: number) => void;
106
+ }
107
+
108
+ /** Make the native runtime available; returns its directory. No-op when
109
+ * any resolution candidate already has it. */
110
+ export async function ensureNativeRuntime(opts: EnsureNativeOptions = {}): Promise<string> {
111
+ const existing = nativeRuntimeDir();
112
+ if (existing && !opts.destDir) return existing;
113
+
114
+ const arch = process.arch;
115
+ const url = opts.url ?? nativePackUrl(arch);
116
+ const sha256 = opts.sha256 ?? SHA256[arch];
117
+ const sizeBytes = opts.sizeBytes ?? SIZE[arch];
118
+ const destDir = opts.destDir ?? nativePackDir(arch);
119
+ if (existsSync(join(destDir, "libmlxc.dylib"))) return destDir;
120
+ if (!sha256 || !sizeBytes)
121
+ throw new Error(`no native pack published for ${arch} — set MLX_BUN_LIBMLXC or install mlx via homebrew`);
122
+
123
+ const tarPath = join(tmpdir(), `mlx-bun-native-${sha256.slice(0, 12)}.tar.gz`);
124
+ let fetchUrl = url;
125
+ let token: string | null = null;
126
+ if (url.startsWith("https://github.com/")) {
127
+ // Public repo: the releases/download URL works anonymously. Private
128
+ // repo: it 404s — resolve the asset's signed URL via the API
129
+ // (Accept: octet-stream + token → 302 with a presigned location).
130
+ const resolved = await resolveGithubAssetUrl(url);
131
+ if (resolved) fetchUrl = resolved;
132
+ else token = await githubToken(); // public-repo path; token unused but harmless
133
+ }
134
+ await downloadOne(fetchUrl, token, tarPath, { size: sizeBytes, sha256, name: nativePackName(arch) },
135
+ (_file, received, total) => opts.onProgress?.(received, total));
136
+
137
+ // Extract to a staging dir, then rename into place (atomic-ish: a
138
+ // crashed extract never leaves a half-populated cache dir).
139
+ const staging = `${destDir}.staging-${process.pid}`;
140
+ rmSync(staging, { recursive: true, force: true });
141
+ mkdirSync(staging, { recursive: true });
142
+ const untar = Bun.spawn(["tar", "-xzf", tarPath, "-C", staging], { stderr: "pipe" });
143
+ if ((await untar.exited) !== 0)
144
+ throw new Error(`native pack extract failed: ${await new Response(untar.stderr).text()}`);
145
+ for (const f of NATIVE_PACK_FILES)
146
+ if (!existsSync(join(staging, f))) throw new Error(`native pack missing ${f}`);
147
+ rmSync(destDir, { recursive: true, force: true });
148
+ mkdirSync(dirname(destDir), { recursive: true });
149
+ renameSync(staging, destDir);
150
+ rmSync(tarPath, { force: true });
151
+ return destDir;
152
+ }