mlx-bun 0.0.8 → 0.0.9

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 (133) hide show
  1. package/README.md +112 -20
  2. package/docs/design/batching-v2-plan.md +515 -0
  3. package/docs/design/bucketing-stage.md +196 -0
  4. package/docs/design/compat-cli-surface-design.md +11 -11
  5. package/docs/design/docs-reorg-plan.md +1 -1
  6. package/docs/design/dreaming-nightly-pipeline.md +291 -0
  7. package/docs/design/dspark-speculative-decoding.md +106 -0
  8. package/docs/design/generic-model-support.md +381 -0
  9. package/docs/design/memory-inference-path.md +422 -0
  10. package/docs/design/memory-synthesis.md +287 -0
  11. package/docs/design/minicpm5-decode-megakernel.md +484 -0
  12. package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
  13. package/docs/design/orpo-dynamic-lambda.md +41 -0
  14. package/docs/design/orpo-training.md +35 -5
  15. package/docs/design/parallel-slots.md +12 -1
  16. package/docs/design/parity-tier-dag.md +13 -5
  17. package/docs/design/spec-decode-larger-targets.md +229 -0
  18. package/docs/design/tauri-desktop-app.md +2 -2
  19. package/docs/design/the-dreaming-handoff.md +141 -0
  20. package/docs/design/the-dreaming-master-plan.md +446 -0
  21. package/docs/design/write-pipeline-entity.md +468 -0
  22. package/docs/investigations/curve-bisector-routing.md +1 -1
  23. package/docs/investigations/decode-roofline-lookagain.md +262 -0
  24. package/docs/investigations/dspark-handoff.md +58 -0
  25. package/docs/investigations/kernel-perf-review-2026-07.md +92 -0
  26. package/docs/planning/PRODUCT_ROADMAP.md +2 -2
  27. package/docs/planning/curve-sampler-research-plan.md +331 -0
  28. package/docs/planning/memory-docs-and-dag-plan.md +185 -0
  29. package/docs/planning/pi-integration-review.md +178 -0
  30. package/docs/planning/release-notes-v0.0.9.md +122 -0
  31. package/docs/planning/web-ui-pass-plan.md +213 -0
  32. package/docs/planning/website-readme-pass-plan.md +224 -0
  33. package/docs/reference/cli.md +255 -0
  34. package/docs/reference/embedding.md +7 -2
  35. package/docs/reference/library-api.md +22 -3
  36. package/docs/reference/memory.md +5 -5
  37. package/docs/reference/models.md +137 -0
  38. package/docs/reference/orpo-quickstart.md +6 -0
  39. package/docs/reference/server-api.md +187 -9
  40. package/docs/reference/server-config.md +40 -9
  41. package/docs/reference/training.md +25 -11
  42. package/package.json +1 -1
  43. package/src/anthropic.ts +22 -0
  44. package/src/assets/curve-designer.html +1 -1
  45. package/src/chat-template.ts +25 -7
  46. package/src/cli.ts +1029 -86
  47. package/src/config.ts +4 -1
  48. package/src/download.ts +99 -8
  49. package/src/embed.ts +77 -0
  50. package/src/eval/perplexity.ts +164 -0
  51. package/src/eval/runner.ts +15 -6
  52. package/src/eval/tasks/bfcl.ts +1 -1
  53. package/src/generate.ts +141 -18
  54. package/src/harness-pi.ts +43 -17
  55. package/src/index.ts +2 -0
  56. package/src/lora.ts +2 -2
  57. package/src/memory/article.ts +558 -0
  58. package/src/memory/chunk-validation.ts +150 -0
  59. package/src/memory/chunk.ts +305 -12
  60. package/src/memory/cluster.ts +344 -17
  61. package/src/memory/coverage.ts +134 -0
  62. package/src/memory/crosslink.ts +0 -0
  63. package/src/memory/db.ts +184 -45
  64. package/src/memory/entity.ts +203 -0
  65. package/src/memory/gate.ts +187 -0
  66. package/src/memory/history.ts +152 -0
  67. package/src/memory/ingest.ts +324 -11
  68. package/src/memory/model.ts +321 -0
  69. package/src/memory/normalize.ts +671 -0
  70. package/src/memory/parse.ts +22 -0
  71. package/src/memory/pipeline.ts +242 -30
  72. package/src/memory/prompts.ts +42 -0
  73. package/src/memory/query.ts +334 -0
  74. package/src/memory/reconcile.ts +734 -0
  75. package/src/memory/redirect.ts +108 -0
  76. package/src/memory/reindex.ts +325 -0
  77. package/src/memory/resolve.ts +307 -0
  78. package/src/memory/route.ts +272 -0
  79. package/src/memory/schedule.ts +6 -4
  80. package/src/memory/stages.ts +774 -0
  81. package/src/memory/synthesize.ts +1629 -20
  82. package/src/memory/tools.ts +446 -96
  83. package/src/memory/vault.ts +462 -35
  84. package/src/memory/wikify.ts +1057 -16
  85. package/src/mlx/ffi.ts +3 -0
  86. package/src/mlx/ops.ts +46 -0
  87. package/src/model/compiled-decode.ts +43 -8
  88. package/src/model/factory.ts +29 -8
  89. package/src/model/flash-attention.ts +4 -2
  90. package/src/model/fused-decode-kernel.ts +14 -7
  91. package/src/model/fused-mlp-kernel.ts +169 -0
  92. package/src/model/fused-swiglu-kernel.ts +240 -0
  93. package/src/model/gemma4-base.ts +62 -6
  94. package/src/model/gemma4.ts +33 -2
  95. package/src/model/minicpm5.ts +106 -8
  96. package/src/model/qwen3.ts +276 -0
  97. package/src/model/steel-linear-kernel.ts +96 -0
  98. package/src/model/support.ts +38 -8
  99. package/src/model/universal/archs.ts +332 -0
  100. package/src/model/universal/dense.ts +576 -0
  101. package/src/model/universal/modules.ts +163 -0
  102. package/src/model/universal/rope.ts +366 -0
  103. package/src/pi-provider.ts +1 -1
  104. package/src/pi-terminal.ts +29 -8
  105. package/src/pi-web.ts +19 -2
  106. package/src/quantize/config-writer.ts +1 -1
  107. package/src/registry.ts +215 -7
  108. package/src/responses.ts +23 -0
  109. package/src/sampler.ts +220 -9
  110. package/src/serve/batch-scheduler.ts +272 -80
  111. package/src/serve/generation-gateway.ts +86 -18
  112. package/src/server.ts +612 -49
  113. package/src/spec/drafter.ts +81 -49
  114. package/src/spec/dspark/data-dflash.ts +156 -0
  115. package/src/spec/dspark/data.ts +201 -0
  116. package/src/spec/dspark/generate-dflash.ts +193 -0
  117. package/src/spec/dspark/generate.ts +290 -0
  118. package/src/spec/dspark/loss.ts +213 -0
  119. package/src/spec/dspark/module-dflash.ts +361 -0
  120. package/src/spec/dspark/module.ts +540 -0
  121. package/src/spec/dspark/sample.ts +112 -0
  122. package/src/spec/generate.ts +40 -19
  123. package/src/status-page.html +1 -1
  124. package/src/tokenizer.ts +7 -0
  125. package/src/train/flash-cce.ts +70 -15
  126. package/src/train/fuse.ts +243 -0
  127. package/src/train/index.ts +3 -0
  128. package/src/train/job.ts +8 -0
  129. package/src/train/loss.ts +289 -30
  130. package/src/train/prefix-shared.ts +33 -5
  131. package/src/train/segmented.ts +67 -11
  132. package/src/train/trainer.ts +92 -28
  133. package/src/web/skills/memory/SKILL.md +40 -17
@@ -0,0 +1,576 @@
1
+ // UniversalDense — the Tier-0 config-driven dense llama-family module
2
+ // (docs/design/generic-model-support.md §3.1). One module, shaped exactly
3
+ // like qwen3.ts, with every arch delta selected by the UniversalArgs
4
+ // descriptor (archs.ts). Monolith path only: no perf kernels, no compiled
5
+ // decode, no kv-quant — slow, never broken. The bar is L1 bit-exactness
6
+ // vs mlx-lm on this machine's GPU (tests/universal-parity.test.ts).
7
+ //
8
+ // Porting discipline: each branch transcribes its mlx-lm source op-for-op
9
+ // (q/k-norm before vs after reshape, rope order, scale points, softcap,
10
+ // mask kinds). The descriptor selects branches; it never approximates.
11
+
12
+ import type { ModelConfig } from "../../config";
13
+ import type { Weights } from "../../weights";
14
+ import { MlxArray } from "../../mlx/array";
15
+ import * as ops from "../../mlx/ops";
16
+ import {
17
+ argmaxLastPosition,
18
+ createCausalMask,
19
+ disposing,
20
+ FINFO_MIN,
21
+ KVCache,
22
+ logitSoftcap,
23
+ LoraState,
24
+ QuantizedLinear,
25
+ RMSNorm,
26
+ RotatingKVCache,
27
+ type Cache,
28
+ type Mask,
29
+ } from "../gemma4-base";
30
+ import { genericArgsFor, type UniversalArgs } from "./archs";
31
+ import { initializeRope, NoRope, phi3SuRope, UniversalRope } from "./rope";
32
+ import {
33
+ loadEmbedding,
34
+ loadLayerNorm,
35
+ loadLinear,
36
+ loadRmsNorm,
37
+ WeightAudit,
38
+ type AnyEmbedding,
39
+ type AnyLinear,
40
+ type AnyNorm,
41
+ } from "./modules";
42
+
43
+ /** Rope for one attention module, per the descriptor (llama's
44
+ * initialize_rope, the plain-nn.RoPE archs, or phi3's Su/linear branch). */
45
+ function buildRope(a: UniversalArgs): UniversalRope {
46
+ const ropeDims = Math.floor(a.headDim * a.partialRotaryFactor);
47
+ if (a.rope === "phi3") {
48
+ const sc = a.ropeScaling;
49
+ if (sc && ["longrope", "su"].includes(String(sc.type))) {
50
+ return phi3SuRope(
51
+ ropeDims,
52
+ a.ropeTheta,
53
+ a.maxPositionEmbeddings ?? 131072,
54
+ a.originalMaxPositionEmbeddings,
55
+ (sc.long_factor as number[] | number | undefined) ?? 1.0,
56
+ );
57
+ }
58
+ let scale = 1.0;
59
+ if (sc && String(sc.type) === "linear") scale = 1 / (sc.factor as number);
60
+ return new UniversalRope(ropeDims, a.ropeTraditional, a.ropeTheta, scale, null, 1, false);
61
+ }
62
+ if (a.rope === "plain")
63
+ return new UniversalRope(ropeDims, a.ropeTraditional, a.ropeTheta, 1.0, null, 1, false);
64
+ // initialize_rope path (llama, qwen2, qwen3, olmo2, granite, smollm3)
65
+ return initializeRope(a.headDim, a.ropeTheta, a.ropeTraditional, a.ropeScaling, a.maxPositionEmbeddings);
66
+ }
67
+
68
+ class UniversalAttention {
69
+ readonly qProj: AnyLinear | null;
70
+ readonly kProj: AnyLinear | null;
71
+ readonly vProj: AnyLinear | null;
72
+ readonly qkvProj: AnyLinear | null;
73
+ readonly oProj: AnyLinear;
74
+ readonly qNorm: RMSNorm | null;
75
+ readonly kNorm: RMSNorm | null;
76
+
77
+ constructor(
78
+ weights: Weights,
79
+ config: ModelConfig,
80
+ prefix: string,
81
+ readonly a: UniversalArgs,
82
+ readonly rope: UniversalRope,
83
+ audit: WeightAudit,
84
+ ) {
85
+ if (a.fusedQkv) {
86
+ this.qkvProj = loadLinear(weights, `${prefix}.qkv_proj`, config, audit);
87
+ this.qProj = this.kProj = this.vProj = null;
88
+ } else {
89
+ this.qProj = loadLinear(weights, `${prefix}.q_proj`, config, audit);
90
+ this.kProj = loadLinear(weights, `${prefix}.k_proj`, config, audit);
91
+ this.vProj = loadLinear(weights, `${prefix}.v_proj`, config, audit);
92
+ this.qkvProj = null;
93
+ }
94
+ this.oProj = loadLinear(weights, `${prefix}.o_proj`, config, audit);
95
+ if (a.qkNorm !== "none") {
96
+ // qwen3 ("head"): RMSNorm(head_dim); olmo2 ("full"): RMSNorm(width) —
97
+ // both live at q_norm/k_norm and use rms_norm_eps.
98
+ this.qNorm = loadRmsNorm(weights, `${prefix}.q_norm`, a.normEps, false, audit);
99
+ this.kNorm = loadRmsNorm(weights, `${prefix}.k_norm`, a.normEps, false, audit);
100
+ } else {
101
+ this.qNorm = this.kNorm = null;
102
+ }
103
+ }
104
+
105
+ forward(x: MlxArray, mask: Mask, cache: Cache): MlxArray {
106
+ const a = this.a;
107
+ const [B, L] = x.shape as [number, number, number];
108
+
109
+ let q: MlxArray, k: MlxArray, v: MlxArray;
110
+ if (this.qkvProj) {
111
+ // phi3: fused projection, split as ACTIVATIONS (mx.split) like mlx-lm.
112
+ const qkv = this.qkvProj.forward(x);
113
+ const queryPos = a.numHeads * a.headDim;
114
+ const parts = ops.split(qkv, [queryPos, queryPos + a.numKvHeads * a.headDim], -1);
115
+ qkv.dispose();
116
+ [q, k, v] = parts as [MlxArray, MlxArray, MlxArray];
117
+ } else {
118
+ q = this.qProj!.forward(x);
119
+ k = this.kProj!.forward(x);
120
+ v = this.vProj!.forward(x);
121
+ }
122
+
123
+ if (a.qkNorm === "full") {
124
+ // olmo2: norm the FLAT projections before any reshape.
125
+ q = disposing(q, this.qNorm!.forward(q));
126
+ k = disposing(k, this.kNorm!.forward(k));
127
+ }
128
+
129
+ q = disposing(q, ops.reshape(q, [B, L, a.numHeads, -1]));
130
+ k = disposing(k, ops.reshape(k, [B, L, a.numKvHeads, -1]));
131
+ v = disposing(v, ops.reshape(v, [B, L, a.numKvHeads, -1]));
132
+
133
+ if (a.qkNorm === "head") {
134
+ // qwen3: per-head norm over head_dim, BEFORE transpose + rope.
135
+ q = disposing(q, this.qNorm!.forward(q));
136
+ k = disposing(k, this.kNorm!.forward(k));
137
+ }
138
+
139
+ q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
140
+ k = disposing(k, ops.transposeAxes(k, [0, 2, 1, 3]));
141
+ v = disposing(v, ops.transposeAxes(v, [0, 2, 1, 3]));
142
+
143
+ q = disposing(q, this.rope.apply(q, cache.offset));
144
+ k = disposing(k, this.rope.apply(k, cache.offset));
145
+
146
+ const [keys, values] = cache.updateAndFetch(k, v);
147
+ k.dispose();
148
+ v.dispose();
149
+
150
+ let attn: MlxArray;
151
+ if (a.attnLogitSoftcap !== null) {
152
+ attn = this.#softcapAttention(q, keys, values, mask, B, L);
153
+ } else {
154
+ attn = ops.sdpa(q, keys, values, a.attnScale, mask.mode, mask.arr);
155
+ }
156
+ keys.dispose();
157
+ values.dispose();
158
+ q.dispose();
159
+
160
+ const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
161
+ attn.dispose();
162
+ const merged = ops.reshape(attnT, [B, L, -1]);
163
+ attnT.dispose();
164
+ const out = this.oProj.forward(merged);
165
+ merged.dispose();
166
+ return out;
167
+ }
168
+
169
+ /** gemma2.py manual attention: scale → GQA 5-d reshape → q·kᵀ →
170
+ * tanh-softcap → bool-mask via where(finfo.min) → precise softmax → ·v.
171
+ * ops.sdpa can't express the score softcap. */
172
+ #softcapAttention(
173
+ q: MlxArray, keys: MlxArray, values: MlxArray, mask: Mask, B: number, L: number,
174
+ ): MlxArray {
175
+ const a = this.a;
176
+ const repeats = Math.floor(a.numHeads / a.numKvHeads);
177
+ let queries = ops.mulScalar(q, a.attnScale); // queries * self.scale
178
+
179
+ let kk = keys;
180
+ let vv = values;
181
+ let kExp: MlxArray | null = null;
182
+ let vExp: MlxArray | null = null;
183
+ if (repeats > 1) {
184
+ queries = disposing(queries, ops.reshape(queries, [B, a.numKvHeads, repeats, L, a.headDim]));
185
+ kExp = ops.expandDims(keys, 2);
186
+ vExp = ops.expandDims(values, 2);
187
+ kk = kExp;
188
+ vv = vExp;
189
+ }
190
+
191
+ const kT = ops.transposeAxes(kk, repeats > 1 ? [0, 1, 2, 4, 3] : [0, 1, 3, 2]);
192
+ let scores = ops.matmul(queries, kT);
193
+ kT.dispose();
194
+ queries.dispose();
195
+
196
+ // tanh(scores / cap) * cap — same composition as logitSoftcap.
197
+ scores = disposing(scores, logitSoftcap(scores, a.attnLogitSoftcap!));
198
+
199
+ if (mask.mode === "array" && mask.arr) {
200
+ // bool mask: where(mask, scores, finfo(scores.dtype).min)
201
+ const minVal = FINFO_MIN[scores.dtype];
202
+ if (minVal === undefined) throw new Error(`no finfo.min for dtype ${scores.dtypeName}`);
203
+ const minArr = ops.scalarLike(minVal, scores);
204
+ scores = disposing(scores, ops.where(mask.arr, scores, minArr));
205
+ minArr.dispose();
206
+ }
207
+
208
+ scores = disposing(scores, ops.softmaxAxis(scores, -1, true));
209
+ let out = ops.matmul(scores, vv);
210
+ scores.dispose();
211
+ kExp?.dispose();
212
+ vExp?.dispose();
213
+ if (repeats > 1) out = disposing(out, ops.reshape(out, [B, a.numHeads, L, a.headDim]));
214
+ return out;
215
+ }
216
+ }
217
+
218
+ class UniversalMLP {
219
+ readonly gate: AnyLinear | null;
220
+ readonly up: AnyLinear | null;
221
+ readonly gateUp: AnyLinear | null;
222
+ readonly down: AnyLinear;
223
+
224
+ constructor(
225
+ weights: Weights,
226
+ config: ModelConfig,
227
+ prefix: string,
228
+ readonly kind: UniversalArgs["mlp"],
229
+ audit: WeightAudit,
230
+ ) {
231
+ if (kind === "fused_swiglu") {
232
+ this.gateUp = loadLinear(weights, `${prefix}.gate_up_proj`, config, audit);
233
+ this.gate = this.up = null;
234
+ this.down = loadLinear(weights, `${prefix}.down_proj`, config, audit);
235
+ } else if (kind === "gelu_mlp") {
236
+ // starcoder2: c_fc → gelu(precise) → c_proj
237
+ this.gate = loadLinear(weights, `${prefix}.c_fc`, config, audit);
238
+ this.up = this.gateUp = null;
239
+ this.down = loadLinear(weights, `${prefix}.c_proj`, config, audit);
240
+ } else {
241
+ this.gate = loadLinear(weights, `${prefix}.gate_proj`, config, audit);
242
+ this.up = loadLinear(weights, `${prefix}.up_proj`, config, audit);
243
+ this.gateUp = null;
244
+ this.down = loadLinear(weights, `${prefix}.down_proj`, config, audit);
245
+ }
246
+ }
247
+
248
+ forward(x: MlxArray): MlxArray {
249
+ if (this.kind === "gelu_mlp") {
250
+ const h = this.gate!.forward(x);
251
+ const g = ops.geluPrecise(h);
252
+ h.dispose();
253
+ const out = this.down.forward(g);
254
+ g.dispose();
255
+ return out;
256
+ }
257
+
258
+ let gate: MlxArray;
259
+ let up: MlxArray;
260
+ if (this.kind === "fused_swiglu") {
261
+ const gu = this.gateUp!.forward(x);
262
+ const half = Math.floor(gu.shape[gu.shape.length - 1]! / 2);
263
+ const parts = ops.split(gu, [half], -1); // mx.split(x, 2, axis=-1)
264
+ gu.dispose();
265
+ [gate, up] = parts as [MlxArray, MlxArray];
266
+ } else {
267
+ gate = this.gate!.forward(x);
268
+ up = this.up!.forward(x);
269
+ }
270
+
271
+ let act: MlxArray;
272
+ if (this.kind === "geglu") {
273
+ act = ops.geluPrecise(gate); // gemma-1: nn.gelu (erf)
274
+ } else if (this.kind === "geglu_approx") {
275
+ act = ops.geluApprox(gate); // gemma2: nn.gelu_approx
276
+ } else {
277
+ // swiglu — the L1-verified uncompiled composition (minicpm5/qwen3):
278
+ const sig = ops.sigmoid(gate);
279
+ act = ops.mul(gate, sig);
280
+ sig.dispose();
281
+ }
282
+ gate.dispose();
283
+ const hidden = ops.mul(act, up);
284
+ act.dispose();
285
+ up.dispose();
286
+ const out = this.down.forward(hidden);
287
+ hidden.dispose();
288
+ return out;
289
+ }
290
+ }
291
+
292
+ class UniversalLayer {
293
+ readonly attn: UniversalAttention;
294
+ readonly mlp: UniversalMLP;
295
+ /** pre/gemma2/glm4: input_layernorm. post (olmo2): unused (null). */
296
+ readonly inputNorm: AnyNorm | null;
297
+ /** pre: post_attention_layernorm (pre-MLP). post: post_attention (on
298
+ * attn out). gemma2: post_attention (on attn out). glm4:
299
+ * post_attention (pre-MLP). */
300
+ readonly postAttnNorm: AnyNorm;
301
+ /** gemma2: pre_feedforward. glm4: post_self_attn (on attn out). */
302
+ readonly extraNorm1: AnyNorm | null;
303
+ /** gemma2: post_feedforward. glm4: post_mlp. post: post_feedforward. */
304
+ readonly extraNorm2: AnyNorm | null;
305
+
306
+ constructor(
307
+ weights: Weights,
308
+ config: ModelConfig,
309
+ prefix: string,
310
+ readonly a: UniversalArgs,
311
+ rope: UniversalRope,
312
+ audit: WeightAudit,
313
+ ) {
314
+ this.attn = new UniversalAttention(weights, config, `${prefix}.self_attn`, a, rope, audit);
315
+ this.mlp = new UniversalMLP(weights, config, `${prefix}.mlp`, a.mlp, audit);
316
+ const norm = (name: string): AnyNorm =>
317
+ a.norm === "layernorm"
318
+ ? loadLayerNorm(weights, `${prefix}.${name}`, a.normEps, audit)
319
+ : loadRmsNorm(weights, `${prefix}.${name}`, a.normEps, a.norm === "rmsnorm_plus_one", audit);
320
+
321
+ this.postAttnNorm = norm("post_attention_layernorm");
322
+ if (a.block === "post") {
323
+ this.inputNorm = null;
324
+ this.extraNorm1 = null;
325
+ this.extraNorm2 = norm("post_feedforward_layernorm");
326
+ } else if (a.block === "gemma2") {
327
+ this.inputNorm = norm("input_layernorm");
328
+ this.extraNorm1 = norm("pre_feedforward_layernorm");
329
+ this.extraNorm2 = norm("post_feedforward_layernorm");
330
+ } else if (a.block === "glm4") {
331
+ this.inputNorm = norm("input_layernorm");
332
+ this.extraNorm1 = norm("post_self_attn_layernorm");
333
+ this.extraNorm2 = norm("post_mlp_layernorm");
334
+ } else {
335
+ this.inputNorm = norm("input_layernorm");
336
+ this.extraNorm1 = null;
337
+ this.extraNorm2 = null;
338
+ }
339
+ }
340
+
341
+ forward(x: MlxArray, mask: Mask, cache: Cache): MlxArray {
342
+ const a = this.a;
343
+ if (a.block === "post") {
344
+ // olmo2: h = x + norm(attn(x)); out = h + norm(mlp(h))
345
+ const r1 = this.attn.forward(x, mask, cache);
346
+ const n1 = disposing(r1, this.postAttnNorm.forward(r1));
347
+ const h = ops.add(x, n1);
348
+ n1.dispose();
349
+ const r2 = this.mlp.forward(h);
350
+ const n2 = disposing(r2, this.extraNorm2!.forward(r2));
351
+ const out = ops.add(h, n2);
352
+ h.dispose();
353
+ n2.dispose();
354
+ return out;
355
+ }
356
+ if (a.block === "gemma2") {
357
+ // h = x + postAttn(attn(inputNorm(x))); out = h + postFfn(mlp(preFfn(h)))
358
+ const xn = this.inputNorm!.forward(x);
359
+ const r1 = this.attn.forward(xn, mask, cache);
360
+ xn.dispose();
361
+ const n1 = disposing(r1, this.postAttnNorm.forward(r1));
362
+ const h = ops.add(x, n1);
363
+ n1.dispose();
364
+ const fn = this.extraNorm1!.forward(h);
365
+ const r2 = this.mlp.forward(fn);
366
+ fn.dispose();
367
+ const n2 = disposing(r2, this.extraNorm2!.forward(r2));
368
+ const out = ops.add(h, n2);
369
+ h.dispose();
370
+ n2.dispose();
371
+ return out;
372
+ }
373
+ if (a.block === "glm4") {
374
+ // x1 = x + postSelfAttn(attn(inputNorm(x)));
375
+ // out = postMlp(mlp(postAttn(x1))) + x1
376
+ const xn = this.inputNorm!.forward(x);
377
+ const r1 = this.attn.forward(xn, mask, cache);
378
+ xn.dispose();
379
+ const n1 = disposing(r1, this.extraNorm1!.forward(r1));
380
+ const x1 = ops.add(x, n1);
381
+ n1.dispose();
382
+ const pn = this.postAttnNorm.forward(x1);
383
+ const r2 = this.mlp.forward(pn);
384
+ pn.dispose();
385
+ const n2 = disposing(r2, this.extraNorm2!.forward(r2));
386
+ const out = ops.add(n2, x1);
387
+ n2.dispose();
388
+ x1.dispose();
389
+ return out;
390
+ }
391
+ // pre (llama family): h = x + r·mult; out = h + r·mult
392
+ const xn = this.inputNorm!.forward(x);
393
+ let r1 = this.attn.forward(xn, mask, cache);
394
+ xn.dispose();
395
+ if (a.residualMultiplier !== null)
396
+ r1 = disposing(r1, ops.mulScalar(r1, a.residualMultiplier));
397
+ const h = ops.add(x, r1);
398
+ r1.dispose();
399
+ const hn = this.postAttnNorm.forward(h);
400
+ let r2 = this.mlp.forward(hn);
401
+ hn.dispose();
402
+ if (a.residualMultiplier !== null)
403
+ r2 = disposing(r2, ops.mulScalar(r2, a.residualMultiplier));
404
+ const out = ops.add(h, r2);
405
+ h.dispose();
406
+ r2.dispose();
407
+ return out;
408
+ }
409
+ }
410
+
411
+ export class UniversalDenseModel {
412
+ readonly config: ModelConfig;
413
+ readonly args: UniversalArgs;
414
+ readonly weightsBytes: number;
415
+ readonly prefixBase = "model";
416
+ readonly loraState = new LoraState();
417
+ readonly embed: AnyEmbedding;
418
+ readonly layers: UniversalLayer[];
419
+ readonly finalNorm: AnyNorm;
420
+ readonly lmHead: AnyLinear | null;
421
+ readonly sharedRope: UniversalRope;
422
+ readonly ropes: UniversalRope[];
423
+
424
+ constructor(weights: Weights, config: ModelConfig, args?: UniversalArgs) {
425
+ const a = args ?? genericArgsFor(config);
426
+ if (!a)
427
+ throw new Error(`UniversalDenseModel: no descriptor for model_type ${config.modelType}`);
428
+ this.config = config;
429
+ this.args = a;
430
+ this.weightsBytes = [...weights.shards.files.values()]
431
+ .reduce((acc, f) => acc + f.mmap.size, 0);
432
+
433
+ const audit = new WeightAudit();
434
+ this.embed = loadEmbedding(weights, "model.embed_tokens", config, audit);
435
+ // One rope instance shared by every rope-bearing layer (identical
436
+ // params ⇒ identical table); NoPE layers (smollm3) get the no-op.
437
+ this.sharedRope = buildRope(a);
438
+ const noRope = new NoRope();
439
+ this.ropes = Array.from({ length: a.numHiddenLayers }, (_, i) =>
440
+ a.noRopeLayers && a.noRopeLayers[i] === 0 ? noRope : this.sharedRope,
441
+ );
442
+ this.layers = Array.from(
443
+ { length: a.numHiddenLayers },
444
+ (_, i) => new UniversalLayer(weights, config, `model.layers.${i}`, a, this.ropes[i]!, audit),
445
+ );
446
+ this.finalNorm = a.norm === "layernorm"
447
+ ? loadLayerNorm(weights, "model.norm", a.normEps, audit)
448
+ : loadRmsNorm(weights, "model.norm", a.normEps, a.norm === "rmsnorm_plus_one", audit);
449
+ this.lmHead = a.tieWordEmbeddings
450
+ ? null
451
+ : loadLinear(weights, "lm_head", config, audit);
452
+
453
+ // Load-time weight audit: unconsumed tensors (outside the arch's
454
+ // sanitize drops) are a LOAD ERROR, never a silently-wrong model.
455
+ audit.finish(weights, a.dropWeightPatterns);
456
+ }
457
+
458
+ /** LoRA hot-swap targets — quantized linears only (dense checkpoints
459
+ * serve without adapter support at Tier-0). */
460
+ loraTargets(): Map<string, QuantizedLinear> {
461
+ const out = new Map<string, QuantizedLinear>();
462
+ const put = (path: string, lin: AnyLinear | null) => {
463
+ if (lin instanceof QuantizedLinear) out.set(path, lin);
464
+ };
465
+ for (let i = 0; i < this.layers.length; i++) {
466
+ const l = this.layers[i]!;
467
+ const p = `model.layers.${i}`;
468
+ put(`${p}.self_attn.q_proj`, l.attn.qProj);
469
+ put(`${p}.self_attn.k_proj`, l.attn.kProj);
470
+ put(`${p}.self_attn.v_proj`, l.attn.vProj);
471
+ put(`${p}.self_attn.qkv_proj`, l.attn.qkvProj);
472
+ put(`${p}.self_attn.o_proj`, l.attn.oProj);
473
+ put(`${p}.mlp.gate_proj`, l.mlp.gate);
474
+ put(`${p}.mlp.up_proj`, l.mlp.up);
475
+ put(`${p}.mlp.gate_up_proj`, l.mlp.gateUp);
476
+ put(`${p}.mlp.down_proj`, l.mlp.down);
477
+ }
478
+ return out;
479
+ }
480
+
481
+ makeCache(): Cache[] {
482
+ const a = this.args;
483
+ return Array.from({ length: a.numHiddenLayers }, (_, i) => {
484
+ const sliding = a.layerTypes?.[i] === "sliding_attention" && a.slidingWindow;
485
+ return sliding ? new RotatingKVCache(a.slidingWindow!) : new KVCache();
486
+ });
487
+ }
488
+
489
+ forwardHidden(ids: MlxArray, cache: Cache[]): MlxArray {
490
+ let h = this.embed.encode(ids);
491
+ if (this.args.embedMultiplier !== null)
492
+ h = disposing(h, ops.mulScalar(h, this.args.embedMultiplier));
493
+ return this.forwardLayers(h, cache);
494
+ }
495
+
496
+ forwardEmbeddings(_embeds: MlxArray, _cache: Cache[], _bidir: MlxArray | null): MlxArray {
497
+ throw new Error("universal-dense input-embedding path is not supported");
498
+ }
499
+
500
+ protected forwardLayers(h0: MlxArray, cache: Cache[]): MlxArray {
501
+ const a = this.args;
502
+ const L = h0.shape[1]!;
503
+
504
+ // Masks, mirroring the per-arch mlx-lm model __call__:
505
+ let faMask: Mask;
506
+ let swaMask: Mask | null = null;
507
+ if (a.maskArray) {
508
+ // gemma2: create_attention_mask(h, cache[0], return_array=True)
509
+ faMask = L === 1
510
+ ? { mode: "", arr: null }
511
+ : { mode: "array", arr: createCausalMask(L, cache[0]!.offset, null) };
512
+ } else if (a.layerTypes && a.layerTypes.includes("sliding_attention")) {
513
+ // llama.py: fa mask from the first full layer, swa mask (windowed)
514
+ // from the first sliding layer.
515
+ const faIdx = a.layerTypes.indexOf("full_attention");
516
+ const swaIdx = a.layerTypes.indexOf("sliding_attention");
517
+ faMask = cache[faIdx === -1 ? 0 : faIdx]!.makeMask(L, null);
518
+ swaMask = cache[swaIdx]!.makeMask(L, a.slidingWindow);
519
+ } else {
520
+ faMask = cache[0]!.makeMask(L, null);
521
+ }
522
+
523
+ let cur = h0;
524
+ for (let i = 0; i < this.layers.length; i++) {
525
+ const mask = a.layerTypes?.[i] === "sliding_attention" && swaMask ? swaMask : faMask;
526
+ const next = this.layers[i]!.forward(cur, mask, cache[i]!);
527
+ cur.dispose();
528
+ cur = next;
529
+ }
530
+ faMask.arr?.dispose();
531
+ swaMask?.arr?.dispose();
532
+ return disposing(cur, this.finalNorm.forward(cur));
533
+ }
534
+
535
+ logitsFromHidden(h: MlxArray): MlxArray {
536
+ let logits = this.lmHead ? this.lmHead.forward(h) : this.embed.asLinear(h);
537
+ if (this.args.finalLogitSoftcap !== null)
538
+ logits = disposing(logits, logitSoftcap(logits, this.args.finalLogitSoftcap));
539
+ if (this.args.logitsDivisor !== null) {
540
+ const d = ops.scalarLike(this.args.logitsDivisor, logits);
541
+ logits = disposing(logits, ops.div(logits, d));
542
+ d.dispose();
543
+ }
544
+ return logits;
545
+ }
546
+
547
+ forward(tokens: number[] | MlxArray, cache: Cache[]): MlxArray {
548
+ const ids = Array.isArray(tokens)
549
+ ? ops.fromInt32(tokens, [1, tokens.length])
550
+ : tokens;
551
+ const h = this.forwardHidden(ids, cache);
552
+ if (Array.isArray(tokens)) ids.dispose();
553
+ const logits = this.logitsFromHidden(h);
554
+ h.dispose();
555
+ return logits;
556
+ }
557
+
558
+ generate(promptTokens: number[], maxTokens: number, eosIds: number[] = []): number[] {
559
+ const cache = this.makeCache();
560
+ const out: number[] = [];
561
+ try {
562
+ let tokens = promptTokens;
563
+ for (let step = 0; step < maxTokens; step++) {
564
+ const logits = this.forward(tokens, cache);
565
+ const next = argmaxLastPosition(logits);
566
+ logits.dispose();
567
+ if (eosIds.includes(next)) break;
568
+ out.push(next);
569
+ tokens = [next];
570
+ }
571
+ } finally {
572
+ for (const c of cache) c.dispose();
573
+ }
574
+ return out;
575
+ }
576
+ }