mlx-bun 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/design/diffusion-gemma-port.md +602 -0
- package/package.json +1 -1
- package/src/config.ts +50 -2
- package/src/diffusion/diffusion-generate.ts +436 -0
- package/src/eval/runner.ts +5 -2
- package/src/generate.ts +55 -1
- package/src/mlx/ffi.ts +8 -0
- package/src/mlx/ops.ts +67 -0
- package/src/model/diffusion-gemma.ts +1026 -0
- package/src/model/factory.ts +8 -2
- package/src/model/support.ts +9 -1
- package/src/serve/generation-gateway.ts +4 -0
- package/src/server.ts +31 -1
- package/src/train/diffusion-lora.ts +216 -0
- package/src/vision/diffusion-vision.ts +620 -0
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
// DiffusionGemma-26B-A4B-it (model_type "diffusion_gemma") — the first
|
|
2
|
+
// NON-autoregressive model in the codebase. Instead of a left-to-right AR loop
|
|
3
|
+
// it runs an ENCODER prefill over the prompt (builds a KV cache) and then a
|
|
4
|
+
// DECODER pass over a fixed 256-token "canvas" that it denoises over a few
|
|
5
|
+
// steps. This file is the D1 static graph: one full forward
|
|
6
|
+
// encoder(prompt) -> cache ; decoder(canvas, cache) -> hidden -> tied head
|
|
7
|
+
// held bit-exact (KL/argmax where the quantized GEMV is 1-ULP off) against the
|
|
8
|
+
// mlx-optiq reference (optiq IS the oracle here — stock mlx-lm can't load it).
|
|
9
|
+
//
|
|
10
|
+
// Ported verbatim from optiq/vlm/_mlxvlm/models/diffusion_gemma/language.py.
|
|
11
|
+
// Reuses mlx-bun's quantized primitives (QuantizedLinear/Embedding/SwitchLinear,
|
|
12
|
+
// RMSNorm, KVCache/RotatingKVCache) from gemma4-base. Architecture deltas vs
|
|
13
|
+
// gemma4 (verified against the checkpoint, see docs/design/diffusion-gemma-port.md):
|
|
14
|
+
// - attention scale=1.0, NO attention softcap (only final-logit softcap 30 fp32)
|
|
15
|
+
// - QK/V-norm POST-proj / PRE-RoPE; v_norm is RMSNormNoScale; V gets no RoPE
|
|
16
|
+
// - full-attention layers (5,11,17,23,29) reuse k as v (no v_proj), hd=512 kv=2,
|
|
17
|
+
// partial-rotary 0.25; sliding layers hd=256 kv=8
|
|
18
|
+
// - per layer: parallel dense-MLP + 128-expert top-8 MoE, summed via 7 RMSNorms
|
|
19
|
+
// and a per-layer `layer_scalar`
|
|
20
|
+
// - experts use a FUSED gate_up_proj SwitchLinear (split at moe_intermediate)
|
|
21
|
+
// - plain nn.RMSNorm everywhere (NO Gemma 1+weight offset) — weights used as-is
|
|
22
|
+
// - tied LM head via the 4-bit QuantizedEmbedding.asLinear
|
|
23
|
+
// - SelfConditioning MLP wraps the canvas embeddings every step (zero signal on
|
|
24
|
+
// the first step still applies its RMSNormNoScale post-norm)
|
|
25
|
+
|
|
26
|
+
import type { ModelConfig } from "../config";
|
|
27
|
+
import type { Weights } from "../weights";
|
|
28
|
+
import { MlxArray } from "../mlx/array";
|
|
29
|
+
import { Dtype } from "../mlx/ffi";
|
|
30
|
+
import * as ops from "../mlx/ops";
|
|
31
|
+
import {
|
|
32
|
+
disposing,
|
|
33
|
+
KVCache,
|
|
34
|
+
LoraState,
|
|
35
|
+
QuantizedEmbedding,
|
|
36
|
+
QuantizedLinear,
|
|
37
|
+
QuantizedSwitchLinear,
|
|
38
|
+
RMSNorm,
|
|
39
|
+
RotatingKVCache,
|
|
40
|
+
type Cache,
|
|
41
|
+
} from "./gemma4-base";
|
|
42
|
+
import { DiffusionVisionTower } from "../vision/diffusion-vision";
|
|
43
|
+
|
|
44
|
+
/** A mask in the shape ops.sdpa wants: a mode string + optional bool array.
|
|
45
|
+
* `null` array with mode "" means "no mask" (full attention). */
|
|
46
|
+
interface SdpaMask {
|
|
47
|
+
mode: "" | "causal" | "array";
|
|
48
|
+
arr: MlxArray | null;
|
|
49
|
+
}
|
|
50
|
+
const NO_MASK: SdpaMask = { mode: "", arr: null };
|
|
51
|
+
|
|
52
|
+
/** KVCache / RotatingKVCache both expose temporalView() (chronological K/V
|
|
53
|
+
* sliced to offset); the base Cache interface does not declare it. The decoder
|
|
54
|
+
* only ever uses those two concrete caches. */
|
|
55
|
+
type TemporalCache = Cache & { temporalView(): [MlxArray, MlxArray] };
|
|
56
|
+
|
|
57
|
+
/** geglu(gate, x) = gelu_approx(gate) * x (reference language.py:geglu). */
|
|
58
|
+
function geglu(gate: MlxArray, x: MlxArray): MlxArray {
|
|
59
|
+
const act = ops.geluApprox(gate);
|
|
60
|
+
const out = ops.mul(act, x);
|
|
61
|
+
act.dispose();
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Fused fp32-upcast tanh softcap: tanh(x_fp32 / cap) * cap. Stays f32 (the
|
|
66
|
+
* reference make_compiled_softcap returns f32; the golden is dumped f32). */
|
|
67
|
+
function softcapFp32(logits: MlxArray, cap: number): MlxArray {
|
|
68
|
+
const xf = logits.astype(Dtype.float32);
|
|
69
|
+
const capArr = ops.scalarLike(cap, xf);
|
|
70
|
+
const scaled = ops.div(xf, capArr);
|
|
71
|
+
xf.dispose();
|
|
72
|
+
const t = ops.tanh(scaled);
|
|
73
|
+
scaled.dispose();
|
|
74
|
+
const out = ops.mul(t, capArr);
|
|
75
|
+
t.dispose();
|
|
76
|
+
capArr.dispose();
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Dense MLP: down(geglu(gate(x), up(x))). */
|
|
81
|
+
class DiffMLP {
|
|
82
|
+
readonly gate: QuantizedLinear;
|
|
83
|
+
readonly up: QuantizedLinear;
|
|
84
|
+
readonly down: QuantizedLinear;
|
|
85
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string) {
|
|
86
|
+
this.gate = QuantizedLinear.load(weights, `${prefix}.gate_proj`, config);
|
|
87
|
+
this.up = QuantizedLinear.load(weights, `${prefix}.up_proj`, config);
|
|
88
|
+
this.down = QuantizedLinear.load(weights, `${prefix}.down_proj`, config);
|
|
89
|
+
}
|
|
90
|
+
forward(x: MlxArray): MlxArray {
|
|
91
|
+
const g = this.gate.forward(x);
|
|
92
|
+
const u = this.up.forward(x);
|
|
93
|
+
const m = geglu(g, u);
|
|
94
|
+
g.dispose();
|
|
95
|
+
u.dispose();
|
|
96
|
+
const out = this.down.forward(m);
|
|
97
|
+
m.dispose();
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Router: pre-projection RMSNorm (no-scale) * scale * hidden**-0.5, top-8
|
|
103
|
+
* argpartition, softmax(precise) * per_expert_scale[idx]. */
|
|
104
|
+
class DiffRouter {
|
|
105
|
+
readonly proj: QuantizedLinear;
|
|
106
|
+
readonly scale: MlxArray; // per-hidden-dim scale (applied AFTER the no-scale norm)
|
|
107
|
+
readonly rootSize: number; // hidden**-0.5
|
|
108
|
+
readonly perExpertScale: MlxArray;
|
|
109
|
+
readonly eps: number;
|
|
110
|
+
readonly numExperts: number;
|
|
111
|
+
readonly topK: number;
|
|
112
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string) {
|
|
113
|
+
const t = config.text;
|
|
114
|
+
this.proj = QuantizedLinear.load(weights, `${prefix}.proj`, config);
|
|
115
|
+
this.scale = weights.tensor(`${prefix}.scale`);
|
|
116
|
+
this.rootSize = Math.pow(t.hiddenSize, -0.5);
|
|
117
|
+
this.perExpertScale = weights.tensor(`${prefix}.per_expert_scale`);
|
|
118
|
+
this.eps = t.rmsNormEps;
|
|
119
|
+
this.numExperts = t.numExperts;
|
|
120
|
+
this.topK = t.topKExperts;
|
|
121
|
+
}
|
|
122
|
+
/** x: [T, H] -> { indices: [T, k], weights: [T, k] }. */
|
|
123
|
+
forward(x: MlxArray): { indices: MlxArray; weights: MlxArray } {
|
|
124
|
+
// Match the reference EXACTLY (do NOT fold the post-scale into the norm
|
|
125
|
+
// weight — that changes the bf16 rounding and shifts the softmax weights):
|
|
126
|
+
// x = rms_norm(x, None, eps); x = x * scale * hidden**-0.5
|
|
127
|
+
let normed = ops.rmsNorm(x, null, this.eps);
|
|
128
|
+
normed = disposing(normed, ops.mul(normed, this.scale));
|
|
129
|
+
normed = disposing(normed, ops.mulScalar(normed, this.rootSize));
|
|
130
|
+
const scores = this.proj.forward(normed);
|
|
131
|
+
normed.dispose();
|
|
132
|
+
const part = ops.argpartitionAxis(scores, this.numExperts - this.topK, -1);
|
|
133
|
+
const [T] = scores.shape as [number, number];
|
|
134
|
+
const indices = part.slice([0, this.numExperts - this.topK], [T, this.numExperts]);
|
|
135
|
+
part.dispose();
|
|
136
|
+
let w = ops.takeAlongAxis(scores, indices, -1);
|
|
137
|
+
scores.dispose();
|
|
138
|
+
w = disposing(w, ops.softmaxAxis(w, -1, true));
|
|
139
|
+
const gathered = ops.takeAxis(this.perExpertScale, indices, 0);
|
|
140
|
+
w = disposing(w, ops.mul(w, gathered));
|
|
141
|
+
gathered.dispose();
|
|
142
|
+
return { indices, weights: w };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 128-expert top-8 MoE with a FUSED gate_up SwitchLinear (split at
|
|
147
|
+
* moe_intermediate_size) + a down SwitchLinear. _gather_sort / _scatter_unsort
|
|
148
|
+
* exactly as mlx_lm.models.switch_layers (sort threshold idx.size >= 64). */
|
|
149
|
+
class DiffExperts {
|
|
150
|
+
readonly gateUp: QuantizedSwitchLinear;
|
|
151
|
+
readonly down: QuantizedSwitchLinear;
|
|
152
|
+
readonly hiddenDims: number; // moe_intermediate_size (split point)
|
|
153
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string) {
|
|
154
|
+
this.gateUp = QuantizedSwitchLinear.load(weights, `${prefix}.gate_up_proj`, config);
|
|
155
|
+
this.down = QuantizedSwitchLinear.load(weights, `${prefix}.down_proj`, config);
|
|
156
|
+
this.hiddenDims = config.text.moeIntermediateSize;
|
|
157
|
+
}
|
|
158
|
+
/** x: [T, H], indices/weights: [T, k] -> [T, H]. */
|
|
159
|
+
forward(x: MlxArray, indices: MlxArray, weights: MlxArray): MlxArray {
|
|
160
|
+
let h = ops.expandDims(x, -2); // [T, 1, H]
|
|
161
|
+
h = disposing(h, ops.expandDims(h, -3)); // [T, 1, 1, H]
|
|
162
|
+
const doSort = indices.size >= 64;
|
|
163
|
+
let idx = indices;
|
|
164
|
+
let order: MlxArray | null = null;
|
|
165
|
+
let invOrder: MlxArray | null = null;
|
|
166
|
+
if (doSort) {
|
|
167
|
+
const k = indices.shape[indices.ndim - 1]!;
|
|
168
|
+
const idxFlat = ops.reshape(indices, [indices.size]);
|
|
169
|
+
order = ops.argsortAxis(idxFlat, 0);
|
|
170
|
+
invOrder = ops.argsortAxis(order, 0);
|
|
171
|
+
const kS = ops.scalarLike(k, order);
|
|
172
|
+
const rowIdx = ops.floorDivide(order, kS);
|
|
173
|
+
kS.dispose();
|
|
174
|
+
const H = h.shape[h.ndim - 1]!;
|
|
175
|
+
const flat = ops.reshape(h, [-1, 1, H]);
|
|
176
|
+
h.dispose();
|
|
177
|
+
h = ops.takeAxis(flat, rowIdx, 0);
|
|
178
|
+
flat.dispose();
|
|
179
|
+
rowIdx.dispose();
|
|
180
|
+
idx = ops.takeAxis(idxFlat, order, 0);
|
|
181
|
+
idxFlat.dispose();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const gateUp = this.gateUp.forward(h, idx, doSort);
|
|
185
|
+
h.dispose();
|
|
186
|
+
const parts = ops.split(gateUp, [this.hiddenDims], -1);
|
|
187
|
+
gateUp.dispose();
|
|
188
|
+
const gate = parts[0]!;
|
|
189
|
+
const up = parts[1]!;
|
|
190
|
+
const mid = geglu(gate, up);
|
|
191
|
+
gate.dispose();
|
|
192
|
+
up.dispose();
|
|
193
|
+
let y = this.down.forward(mid, idx, doSort);
|
|
194
|
+
mid.dispose();
|
|
195
|
+
if (idx !== indices) idx.dispose();
|
|
196
|
+
|
|
197
|
+
if (doSort) {
|
|
198
|
+
y = disposing(y, ops.takeAxis(y, invOrder!, 0));
|
|
199
|
+
invOrder!.dispose();
|
|
200
|
+
order!.dispose();
|
|
201
|
+
const shape = [...indices.shape, 1, y.shape[y.ndim - 1]!]; // [T, k, 1, H]
|
|
202
|
+
y = disposing(y, ops.reshape(y, shape));
|
|
203
|
+
}
|
|
204
|
+
// squeeze(-2): [T, k, 1, H] -> [T, k, H]
|
|
205
|
+
{
|
|
206
|
+
const shape = y.shape;
|
|
207
|
+
shape.splice(shape.length - 2, 1);
|
|
208
|
+
y = disposing(y, ops.reshape(y, shape));
|
|
209
|
+
}
|
|
210
|
+
// (y * weights[..., None]).sum(axis=-2) -> [T, H]
|
|
211
|
+
const w = ops.expandDims(weights, -1);
|
|
212
|
+
const wy = ops.mul(y, w);
|
|
213
|
+
w.dispose();
|
|
214
|
+
y.dispose();
|
|
215
|
+
return disposing(wy, ops.sumAxis(wy, -2, false));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** SelfConditioning: post_norm(inputs_embeds + down(geglu(gate(pre_norm(signal)),
|
|
220
|
+
* up(pre_norm(signal))))). pre_norm is RMSNorm, post_norm is RMSNormNoScale. */
|
|
221
|
+
class SelfConditioning {
|
|
222
|
+
readonly preNorm: RMSNorm;
|
|
223
|
+
readonly postNorm: RMSNorm; // RMSNormNoScale (null weight)
|
|
224
|
+
readonly gate: QuantizedLinear;
|
|
225
|
+
readonly up: QuantizedLinear;
|
|
226
|
+
readonly down: QuantizedLinear;
|
|
227
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string) {
|
|
228
|
+
const eps = config.text.rmsNormEps;
|
|
229
|
+
this.preNorm = new RMSNorm(weights.tensor(`${prefix}.pre_norm.weight`), eps);
|
|
230
|
+
this.postNorm = new RMSNorm(null, eps);
|
|
231
|
+
this.gate = QuantizedLinear.load(weights, `${prefix}.gate_proj`, config);
|
|
232
|
+
this.up = QuantizedLinear.load(weights, `${prefix}.up_proj`, config);
|
|
233
|
+
this.down = QuantizedLinear.load(weights, `${prefix}.down_proj`, config);
|
|
234
|
+
}
|
|
235
|
+
forward(inputsEmbeds: MlxArray, signal: MlxArray): MlxArray {
|
|
236
|
+
const normed = this.preNorm.forward(signal);
|
|
237
|
+
const g = this.gate.forward(normed);
|
|
238
|
+
const u = this.up.forward(normed);
|
|
239
|
+
normed.dispose();
|
|
240
|
+
const m = geglu(g, u);
|
|
241
|
+
g.dispose();
|
|
242
|
+
u.dispose();
|
|
243
|
+
const s = this.down.forward(m);
|
|
244
|
+
m.dispose();
|
|
245
|
+
const sum = ops.add(inputsEmbeds, s);
|
|
246
|
+
s.dispose();
|
|
247
|
+
const out = this.postNorm.forward(sum);
|
|
248
|
+
sum.dispose();
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Attention with the encoder (decoder=false, cache.updateAndFetch) and decoder
|
|
254
|
+
* (decoder=true, reads encoder K/V from cache temporal state and concatenates
|
|
255
|
+
* the canvas K/V) paths. scale=1.0, no softcap. */
|
|
256
|
+
class DiffAttention {
|
|
257
|
+
readonly isSliding: boolean;
|
|
258
|
+
readonly headDim: number;
|
|
259
|
+
readonly nHeads: number;
|
|
260
|
+
readonly nKvHeads: number;
|
|
261
|
+
readonly slidingWindow: number;
|
|
262
|
+
readonly qProj: QuantizedLinear;
|
|
263
|
+
readonly kProj: QuantizedLinear;
|
|
264
|
+
readonly vProj: QuantizedLinear | null;
|
|
265
|
+
readonly oProj: QuantizedLinear;
|
|
266
|
+
readonly qNorm: RMSNorm;
|
|
267
|
+
readonly kNorm: RMSNorm;
|
|
268
|
+
readonly vNorm: RMSNorm; // RMSNormNoScale (null weight)
|
|
269
|
+
readonly ropeBase: number | null;
|
|
270
|
+
readonly ropeFreqs: MlxArray | null;
|
|
271
|
+
|
|
272
|
+
constructor(weights: Weights, config: ModelConfig, prefix: string, layerType: string) {
|
|
273
|
+
const t = config.text;
|
|
274
|
+
this.isSliding = layerType === "sliding_attention";
|
|
275
|
+
this.slidingWindow = t.slidingWindow;
|
|
276
|
+
this.headDim = this.isSliding ? t.headDim : t.globalHeadDim;
|
|
277
|
+
this.nHeads = t.numAttentionHeads;
|
|
278
|
+
this.nKvHeads = this.isSliding ? t.numKeyValueHeads : t.numGlobalKeyValueHeads;
|
|
279
|
+
|
|
280
|
+
this.qProj = QuantizedLinear.load(weights, `${prefix}.q_proj`, config);
|
|
281
|
+
this.kProj = QuantizedLinear.load(weights, `${prefix}.k_proj`, config);
|
|
282
|
+
// Full-attention layers have no v_proj — they reuse the (pre-norm) k tensor.
|
|
283
|
+
this.vProj = this.isSliding
|
|
284
|
+
? QuantizedLinear.load(weights, `${prefix}.v_proj`, config)
|
|
285
|
+
: null;
|
|
286
|
+
this.oProj = QuantizedLinear.load(weights, `${prefix}.o_proj`, config);
|
|
287
|
+
this.qNorm = new RMSNorm(weights.tensor(`${prefix}.q_norm.weight`), t.rmsNormEps);
|
|
288
|
+
this.kNorm = new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps);
|
|
289
|
+
this.vNorm = new RMSNorm(null, t.rmsNormEps);
|
|
290
|
+
|
|
291
|
+
// RoPE: default (sliding, theta 1e4) or proportional (full, partial 0.25,
|
|
292
|
+
// theta 1e6). Proportional rotates only floor(head_dim*factor) dims; the
|
|
293
|
+
// unrotated tail is freq=Infinity (cos=1, sin=0 → identity). Mirrors
|
|
294
|
+
// gemma4.ts initialize_rope.
|
|
295
|
+
const rp = t.ropeParameters[this.isSliding ? "sliding_attention" : "full_attention"]!;
|
|
296
|
+
if (rp.ropeType === "default") {
|
|
297
|
+
this.ropeBase = rp.ropeTheta;
|
|
298
|
+
this.ropeFreqs = null;
|
|
299
|
+
} else {
|
|
300
|
+
const rotated = Math.floor(this.headDim * rp.partialRotaryFactor);
|
|
301
|
+
const n = this.headDim / 2;
|
|
302
|
+
const exponentsRaw = ops.arange(0, rotated, 2, Dtype.float32);
|
|
303
|
+
const dims = ops.scalarLike(this.headDim, exponentsRaw);
|
|
304
|
+
const exponents = ops.div(exponentsRaw, dims);
|
|
305
|
+
exponentsRaw.dispose();
|
|
306
|
+
dims.dispose();
|
|
307
|
+
const base = ops.scalarLike(rp.ropeTheta, exponents);
|
|
308
|
+
let rotFreqs = ops.pow(base, exponents);
|
|
309
|
+
base.dispose();
|
|
310
|
+
exponents.dispose();
|
|
311
|
+
if (rp.factor !== 1.0) {
|
|
312
|
+
const f = ops.scalarLike(rp.factor, rotFreqs);
|
|
313
|
+
rotFreqs = disposing(rotFreqs, ops.mul(f, rotFreqs));
|
|
314
|
+
f.dispose();
|
|
315
|
+
}
|
|
316
|
+
this.ropeBase = null;
|
|
317
|
+
const tailLen = n - rotated / 2;
|
|
318
|
+
if (tailLen > 0) {
|
|
319
|
+
const tail = MlxArray.fromFloat32(new Float32Array(tailLen).fill(Infinity), [tailLen]);
|
|
320
|
+
this.ropeFreqs = ops.concatAxis([rotFreqs, tail], 0);
|
|
321
|
+
rotFreqs.dispose();
|
|
322
|
+
tail.dispose();
|
|
323
|
+
} else {
|
|
324
|
+
this.ropeFreqs = rotFreqs;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#rope(x: MlxArray, offset: number): MlxArray {
|
|
330
|
+
return ops.rope(x, this.headDim, this.ropeBase, offset, this.ropeFreqs);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
forward(
|
|
334
|
+
x: MlxArray,
|
|
335
|
+
mask: SdpaMask,
|
|
336
|
+
cache: Cache | null,
|
|
337
|
+
decoder: boolean,
|
|
338
|
+
offset: number,
|
|
339
|
+
): MlxArray {
|
|
340
|
+
const [B, L] = x.shape as [number, number, number];
|
|
341
|
+
|
|
342
|
+
const qFlat = this.qProj.forward(x);
|
|
343
|
+
let q = ops.reshape(qFlat, [B, L, this.nHeads, this.headDim]);
|
|
344
|
+
qFlat.dispose();
|
|
345
|
+
q = disposing(q, this.qNorm.forward(q));
|
|
346
|
+
q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
|
|
347
|
+
q = disposing(q, this.#rope(q, offset));
|
|
348
|
+
|
|
349
|
+
const kFlat = this.kProj.forward(x);
|
|
350
|
+
const kReshaped = ops.reshape(kFlat, [B, L, this.nKvHeads, this.headDim]);
|
|
351
|
+
kFlat.dispose();
|
|
352
|
+
// v reuses the pre-norm k tensor on full layers (v_proj is None there).
|
|
353
|
+
let vReshaped: MlxArray;
|
|
354
|
+
if (this.vProj) {
|
|
355
|
+
const vFlat = this.vProj.forward(x);
|
|
356
|
+
vReshaped = ops.reshape(vFlat, [B, L, this.nKvHeads, this.headDim]);
|
|
357
|
+
vFlat.dispose();
|
|
358
|
+
} else {
|
|
359
|
+
vReshaped = kReshaped;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let k = this.kNorm.forward(kReshaped);
|
|
363
|
+
k = disposing(k, ops.transposeAxes(k, [0, 2, 1, 3]));
|
|
364
|
+
k = disposing(k, this.#rope(k, offset));
|
|
365
|
+
let v = this.vNorm.forward(vReshaped); // no RoPE on V
|
|
366
|
+
v = disposing(v, ops.transposeAxes(v, [0, 2, 1, 3]));
|
|
367
|
+
if (vReshaped !== kReshaped) vReshaped.dispose();
|
|
368
|
+
kReshaped.dispose();
|
|
369
|
+
|
|
370
|
+
let maskMode = mask.mode;
|
|
371
|
+
let maskArr = mask.arr;
|
|
372
|
+
let maskArrOwned = false;
|
|
373
|
+
|
|
374
|
+
if (decoder) {
|
|
375
|
+
// Read the encoder K/V (chronological, sliced to offset) from the cache
|
|
376
|
+
// and concatenate the canvas K/V. No cache update on the canvas pass.
|
|
377
|
+
if (cache) {
|
|
378
|
+
let [encK, encV] = (cache as TemporalCache).temporalView();
|
|
379
|
+
if (this.isSliding) {
|
|
380
|
+
const window = Math.max(this.slidingWindow - 1, 0);
|
|
381
|
+
const encoderLen = encK.shape[2]!;
|
|
382
|
+
if (window && encoderLen > window && offset >= encoderLen) {
|
|
383
|
+
const trimK = encK.slice([0, 0, encoderLen - window, 0], encK.shape);
|
|
384
|
+
const trimV = encV.slice([0, 0, encoderLen - window, 0], encV.shape);
|
|
385
|
+
encK.dispose();
|
|
386
|
+
encV.dispose();
|
|
387
|
+
encK = trimK;
|
|
388
|
+
encV = trimV;
|
|
389
|
+
if (maskArr) {
|
|
390
|
+
const ks = maskArr.shape;
|
|
391
|
+
const keep = window + L;
|
|
392
|
+
const trimmed = maskArr.slice(
|
|
393
|
+
[0, 0, 0, ks[3]! - keep],
|
|
394
|
+
ks,
|
|
395
|
+
);
|
|
396
|
+
maskArr = trimmed;
|
|
397
|
+
maskArrOwned = true;
|
|
398
|
+
maskMode = "array";
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const kCat = ops.concatAxis([encK, k], 2);
|
|
403
|
+
const vCat = ops.concatAxis([encV, v], 2);
|
|
404
|
+
encK.dispose();
|
|
405
|
+
encV.dispose();
|
|
406
|
+
k.dispose();
|
|
407
|
+
v.dispose();
|
|
408
|
+
k = kCat;
|
|
409
|
+
v = vCat;
|
|
410
|
+
}
|
|
411
|
+
} else if (cache) {
|
|
412
|
+
const [fk, fv] = cache.updateAndFetch(k, v);
|
|
413
|
+
k.dispose();
|
|
414
|
+
v.dispose();
|
|
415
|
+
k = fk;
|
|
416
|
+
v = fv;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const attn = ops.sdpa(q, k, v, 1.0, maskMode, maskArr);
|
|
420
|
+
q.dispose();
|
|
421
|
+
k.dispose();
|
|
422
|
+
v.dispose();
|
|
423
|
+
if (maskArrOwned) maskArr!.dispose();
|
|
424
|
+
|
|
425
|
+
let out = ops.transposeAxes(attn, [0, 2, 1, 3]);
|
|
426
|
+
attn.dispose();
|
|
427
|
+
out = disposing(out, ops.reshape(out, [B, L, this.nHeads * this.headDim]));
|
|
428
|
+
const o = this.oProj.forward(out);
|
|
429
|
+
out.dispose();
|
|
430
|
+
return o;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** One decoder layer: attention residual + a PARALLEL dense-MLP + MoE branch,
|
|
435
|
+
* combined through 7 RMSNorms and scaled by `layer_scalar` (the encoder pass
|
|
436
|
+
* overrides it with the per-layer encoder scalar). */
|
|
437
|
+
class DiffDecoderLayer {
|
|
438
|
+
readonly layerType: string;
|
|
439
|
+
readonly attn: DiffAttention;
|
|
440
|
+
readonly mlp: DiffMLP;
|
|
441
|
+
readonly router: DiffRouter;
|
|
442
|
+
readonly experts: DiffExperts;
|
|
443
|
+
readonly inputNorm: RMSNorm;
|
|
444
|
+
readonly postAttnNorm: RMSNorm;
|
|
445
|
+
readonly preFfNorm: RMSNorm;
|
|
446
|
+
readonly postFfNorm: RMSNorm;
|
|
447
|
+
readonly postFfNorm1: RMSNorm;
|
|
448
|
+
readonly postFfNorm2: RMSNorm;
|
|
449
|
+
readonly preFfNorm2: RMSNorm;
|
|
450
|
+
readonly layerScalar: MlxArray;
|
|
451
|
+
|
|
452
|
+
constructor(weights: Weights, config: ModelConfig, idx: number) {
|
|
453
|
+
const t = config.text;
|
|
454
|
+
const prefix = `model.decoder.layers.${idx}`;
|
|
455
|
+
this.layerType = t.layerTypes[idx]!;
|
|
456
|
+
const norm = (n: string) => new RMSNorm(weights.tensor(`${prefix}.${n}.weight`), t.rmsNormEps);
|
|
457
|
+
this.attn = new DiffAttention(weights, config, `${prefix}.self_attn`, this.layerType);
|
|
458
|
+
this.mlp = new DiffMLP(weights, config, `${prefix}.mlp`);
|
|
459
|
+
this.router = new DiffRouter(weights, config, `${prefix}.router`);
|
|
460
|
+
this.experts = new DiffExperts(weights, config, `${prefix}.experts`);
|
|
461
|
+
this.inputNorm = norm("input_layernorm");
|
|
462
|
+
this.postAttnNorm = norm("post_attention_layernorm");
|
|
463
|
+
this.preFfNorm = norm("pre_feedforward_layernorm");
|
|
464
|
+
this.postFfNorm = norm("post_feedforward_layernorm");
|
|
465
|
+
this.postFfNorm1 = norm("post_feedforward_layernorm_1");
|
|
466
|
+
this.postFfNorm2 = norm("post_feedforward_layernorm_2");
|
|
467
|
+
this.preFfNorm2 = norm("pre_feedforward_layernorm_2");
|
|
468
|
+
this.layerScalar = weights.tensor(`${prefix}.layer_scalar`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
forward(
|
|
472
|
+
x: MlxArray,
|
|
473
|
+
mask: SdpaMask,
|
|
474
|
+
cache: Cache | null,
|
|
475
|
+
decoder: boolean,
|
|
476
|
+
offset: number,
|
|
477
|
+
layerScalar: MlxArray | null,
|
|
478
|
+
): MlxArray {
|
|
479
|
+
// attention
|
|
480
|
+
let residual = x;
|
|
481
|
+
let h = this.inputNorm.forward(x);
|
|
482
|
+
h = disposing(h, this.attn.forward(h, mask, cache, decoder, offset));
|
|
483
|
+
h = disposing(h, this.postAttnNorm.forward(h));
|
|
484
|
+
h = disposing(h, ops.add(residual, h));
|
|
485
|
+
|
|
486
|
+
// parallel dense-MLP + MoE feedforward
|
|
487
|
+
residual = h;
|
|
488
|
+
const dense = this.preFfNorm.forward(h);
|
|
489
|
+
let h1 = this.mlp.forward(dense);
|
|
490
|
+
dense.dispose();
|
|
491
|
+
h1 = disposing(h1, this.postFfNorm1.forward(h1));
|
|
492
|
+
|
|
493
|
+
const shape = residual.shape;
|
|
494
|
+
const hidden = shape[shape.length - 1]!;
|
|
495
|
+
const flat = ops.reshape(residual, [-1, hidden]);
|
|
496
|
+
const { indices: rawIdx, weights: topw } = this.router.forward(flat);
|
|
497
|
+
// stop_gradient the routing indices so the MoE backward (gather_qmm vjp)
|
|
498
|
+
// doesn't try to differentiate the non-differentiable top-k indices (the
|
|
499
|
+
// reference does this in training mode). Identity in the forward, so D1/D2
|
|
500
|
+
// stay bit-exact; required for D5 LoRA grads to flow through the experts.
|
|
501
|
+
const indices = ops.stopGradient(rawIdx);
|
|
502
|
+
rawIdx.dispose();
|
|
503
|
+
let h2 = this.preFfNorm2.forward(flat);
|
|
504
|
+
flat.dispose();
|
|
505
|
+
h2 = disposing(h2, this.experts.forward(h2, indices, topw));
|
|
506
|
+
indices.dispose();
|
|
507
|
+
topw.dispose();
|
|
508
|
+
h2 = disposing(h2, ops.reshape(h2, shape));
|
|
509
|
+
h2 = disposing(h2, this.postFfNorm2.forward(h2));
|
|
510
|
+
|
|
511
|
+
const sum = ops.add(h1, h2);
|
|
512
|
+
h1.dispose();
|
|
513
|
+
h2.dispose();
|
|
514
|
+
h = this.postFfNorm.forward(sum);
|
|
515
|
+
sum.dispose();
|
|
516
|
+
h = disposing(h, ops.add(residual, h));
|
|
517
|
+
residual.dispose();
|
|
518
|
+
|
|
519
|
+
const scalar = layerScalar ?? this.layerScalar;
|
|
520
|
+
h = disposing(h, ops.mul(h, scalar));
|
|
521
|
+
return h;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export class DiffusionGemmaModel {
|
|
526
|
+
readonly config: ModelConfig;
|
|
527
|
+
readonly weightsBytes: number;
|
|
528
|
+
readonly embed: QuantizedEmbedding;
|
|
529
|
+
readonly embedScale: number;
|
|
530
|
+
readonly layers: DiffDecoderLayer[];
|
|
531
|
+
readonly finalNorm: RMSNorm;
|
|
532
|
+
readonly selfConditioning: SelfConditioning;
|
|
533
|
+
readonly encoderLayerScalars: MlxArray[];
|
|
534
|
+
readonly canvasLength: number;
|
|
535
|
+
readonly softcap: number;
|
|
536
|
+
readonly slidingWindow: number;
|
|
537
|
+
// Shared RuntimeModel surface. LoRA mounts onto the (quantized) decoder
|
|
538
|
+
// linears like the AR models; the diffusion engine activates it per request.
|
|
539
|
+
readonly loraState = new LoraState();
|
|
540
|
+
/** Dedicated DiffusionGemma SigLIP vision tower (image-text-to-text). */
|
|
541
|
+
readonly visionTower: DiffusionVisionTower | null;
|
|
542
|
+
readonly imageTokenId: number;
|
|
543
|
+
|
|
544
|
+
constructor(weights: Weights, config: ModelConfig) {
|
|
545
|
+
const t = config.text;
|
|
546
|
+
this.config = config;
|
|
547
|
+
this.weightsBytes = [...weights.shards.files.values()].reduce((a, f) => a + f.mmap.size, 0);
|
|
548
|
+
this.embed = QuantizedEmbedding.load(weights, "model.decoder.embed_tokens", config);
|
|
549
|
+
this.embedScale = Math.pow(t.hiddenSize, 0.5);
|
|
550
|
+
this.layers = Array.from(
|
|
551
|
+
{ length: t.numHiddenLayers },
|
|
552
|
+
(_, i) => new DiffDecoderLayer(weights, config, i),
|
|
553
|
+
);
|
|
554
|
+
this.finalNorm = new RMSNorm(weights.tensor("model.decoder.norm.weight"), t.rmsNormEps);
|
|
555
|
+
this.selfConditioning = new SelfConditioning(weights, config, "model.decoder.self_conditioning");
|
|
556
|
+
this.encoderLayerScalars = this.layers.map((_, i) =>
|
|
557
|
+
weights.tensor(`model.encoder.language_model.layers.${i}.layer_scalar`),
|
|
558
|
+
);
|
|
559
|
+
this.canvasLength = t.canvasLength ?? 256;
|
|
560
|
+
this.softcap = t.finalLogitSoftcapping ?? 30.0;
|
|
561
|
+
this.slidingWindow = t.slidingWindow;
|
|
562
|
+
const raw = (config as unknown as { raw: Record<string, any> }).raw;
|
|
563
|
+
this.imageTokenId = (raw.image_token_id as number) ?? 258880;
|
|
564
|
+
this.visionTower = raw.vision_config ? new DiffusionVisionTower(weights, config) : null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Build the merged prompt embeddings for an image+text turn: embed the text
|
|
568
|
+
* ids (image positions → pad) * embed_scale, then scatter the SigLIP vision
|
|
569
|
+
* features in at the contiguous <|image|> run. `pixels` is channel-first
|
|
570
|
+
* [1,3,H,W]. Reference EncoderModel._embed_inputs + get_image_features. */
|
|
571
|
+
#embedInputsVision(splicedIds: number[], pixels: MlxArray): MlxArray {
|
|
572
|
+
if (!this.visionTower) throw new Error("this checkpoint has no vision tower");
|
|
573
|
+
const padId = 0; // pad_token_id (reference where(vision_mask, pad, ids))
|
|
574
|
+
const llm = splicedIds.map((id) => (id === this.imageTokenId ? padId : id));
|
|
575
|
+
const ids = MlxArray.fromInt32(Int32Array.from(llm), [1, llm.length]);
|
|
576
|
+
let embeds = this.embed.encode(ids);
|
|
577
|
+
ids.dispose();
|
|
578
|
+
embeds = disposing(embeds, ops.mulScalar(embeds, this.embedScale)); // [1, L, hidden]
|
|
579
|
+
|
|
580
|
+
const feats = this.visionTower.getImageFeatures(pixels); // [1, numSoft, hidden]
|
|
581
|
+
const featsCast = feats.astype(embeds.dtype);
|
|
582
|
+
feats.dispose();
|
|
583
|
+
// image tokens are one contiguous run (boi + image*N + eoi)
|
|
584
|
+
const start = splicedIds.indexOf(this.imageTokenId);
|
|
585
|
+
const count = splicedIds.filter((id) => id === this.imageTokenId).length;
|
|
586
|
+
const hidden = embeds.shape[embeds.shape.length - 1]!;
|
|
587
|
+
embeds = disposing(
|
|
588
|
+
embeds,
|
|
589
|
+
ops.sliceUpdate(embeds, featsCast, [0, start, 0], [1, start + count, hidden]),
|
|
590
|
+
);
|
|
591
|
+
featsCast.dispose();
|
|
592
|
+
return embeds;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Encoder attention mask for the vision prompt: causal, OR'd with the
|
|
596
|
+
* bidirectional image-block overlay (tokens in the same <|image|> block
|
|
597
|
+
* attend both ways). For N < sliding_window the sliding window is a no-op, so
|
|
598
|
+
* one mask serves all layers. Reference _make_encoder_masks + _vision_block_overlay. */
|
|
599
|
+
#encoderVisionMask(splicedIds: number[]): MlxArray {
|
|
600
|
+
const N = splicedIds.length;
|
|
601
|
+
const isVision = splicedIds.map((id) => id === this.imageTokenId);
|
|
602
|
+
const bits = new Int32Array(N * N);
|
|
603
|
+
for (let i = 0; i < N; i++)
|
|
604
|
+
for (let j = 0; j < N; j++)
|
|
605
|
+
bits[i * N + j] = i >= j || (isVision[i] && isVision[j]) ? 1 : 0;
|
|
606
|
+
return MlxArray.fromInt32(bits, [1, 1, N, N]).astype(Dtype.bool);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Image-text-to-text encoder prefill: merged embeddings + the causal/overlay
|
|
610
|
+
* mask through all layers (decoder=false), filling `cache`. The canvas decoder
|
|
611
|
+
* then reads it like the text path. */
|
|
612
|
+
encodeVisionPrefill(splicedIds: number[], pixels: MlxArray, cache: Cache[]): void {
|
|
613
|
+
let h = this.#embedInputsVision(splicedIds, pixels); // [1, L, hidden] (*embed_scale)
|
|
614
|
+
const maskArr = this.#encoderVisionMask(splicedIds);
|
|
615
|
+
const mask: SdpaMask = { mode: "array", arr: maskArr };
|
|
616
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
617
|
+
const layer = this.layers[i]!;
|
|
618
|
+
const next = layer.forward(h, mask, cache[i]!, false, 0, this.encoderLayerScalars[i]!);
|
|
619
|
+
h.dispose();
|
|
620
|
+
h = next;
|
|
621
|
+
}
|
|
622
|
+
maskArr.dispose();
|
|
623
|
+
h.dispose();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Prefill from a pre-spliced image+text prompt → populated cache. */
|
|
627
|
+
prefillVision(splicedIds: number[], pixels: MlxArray): Cache[] {
|
|
628
|
+
const cache = this.makeCache();
|
|
629
|
+
this.encodeVisionPrefill(splicedIds, pixels, cache);
|
|
630
|
+
return cache;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** One KVCache (full) / RotatingKVCache (sliding) per layer — the encoder
|
|
634
|
+
* prefill fills these, the decoder canvas pass reads them. */
|
|
635
|
+
makeCache(): Cache[] {
|
|
636
|
+
return this.layers.map((l) =>
|
|
637
|
+
l.layerType === "sliding_attention"
|
|
638
|
+
? new RotatingKVCache(this.slidingWindow)
|
|
639
|
+
: new KVCache(),
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Encoder prefill: run all layers over the prompt (decoder=false), filling
|
|
644
|
+
* the cache. Text-only (no vision merge yet — D3). Discards the hidden. */
|
|
645
|
+
encodePrompt(promptIds: number[], cache: Cache[]): void {
|
|
646
|
+
const ids = MlxArray.fromInt32(Int32Array.from(promptIds), [1, promptIds.length]);
|
|
647
|
+
let h = this.embed.encode(ids);
|
|
648
|
+
ids.dispose();
|
|
649
|
+
h = disposing(h, ops.mulScalar(h, this.embedScale));
|
|
650
|
+
const N = promptIds.length;
|
|
651
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
652
|
+
const layer = this.layers[i]!;
|
|
653
|
+
const window = layer.layerType === "sliding_attention" ? this.slidingWindow : null;
|
|
654
|
+
const m = cache[i]!.makeMask(N, window) as SdpaMask;
|
|
655
|
+
const next = layer.forward(h, m, cache[i]!, false, 0, this.encoderLayerScalars[i]!);
|
|
656
|
+
if (m.arr) m.arr.dispose();
|
|
657
|
+
h.dispose();
|
|
658
|
+
h = next;
|
|
659
|
+
}
|
|
660
|
+
h.dispose();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Diagnostic: run the encoder, invoking `onLayer(i, h)` after each layer
|
|
664
|
+
* (h is the raw layer output, pre-final-norm) and `onEmbed(h)` on the
|
|
665
|
+
* embedding. Returns the final pre-norm hidden. Fills `cache`. */
|
|
666
|
+
encoderTrace(
|
|
667
|
+
promptIds: number[],
|
|
668
|
+
cache: Cache[],
|
|
669
|
+
onEmbed: (h: MlxArray) => void,
|
|
670
|
+
onLayer: (i: number, h: MlxArray) => void,
|
|
671
|
+
): MlxArray {
|
|
672
|
+
const ids = MlxArray.fromInt32(Int32Array.from(promptIds), [1, promptIds.length]);
|
|
673
|
+
let h = this.embed.encode(ids);
|
|
674
|
+
ids.dispose();
|
|
675
|
+
h = disposing(h, ops.mulScalar(h, this.embedScale));
|
|
676
|
+
onEmbed(h);
|
|
677
|
+
const N = promptIds.length;
|
|
678
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
679
|
+
const layer = this.layers[i]!;
|
|
680
|
+
const window = layer.layerType === "sliding_attention" ? this.slidingWindow : null;
|
|
681
|
+
const m = cache[i]!.makeMask(N, window) as SdpaMask;
|
|
682
|
+
const next = layer.forward(h, m, cache[i]!, false, 0, this.encoderLayerScalars[i]!);
|
|
683
|
+
if (m.arr) m.arr.dispose();
|
|
684
|
+
h.dispose();
|
|
685
|
+
h = next;
|
|
686
|
+
onLayer(i, h);
|
|
687
|
+
}
|
|
688
|
+
return h;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** Diagnostic: run encoder layer 0 sub-components (attention vs parallel
|
|
692
|
+
* dense/MoE) and dump each via `cb(name, h)`. */
|
|
693
|
+
diagLayer0(promptIds: number[], cache: Cache[], cb: (name: string, h: MlxArray) => void): void {
|
|
694
|
+
const ids = MlxArray.fromInt32(Int32Array.from(promptIds), [1, promptIds.length]);
|
|
695
|
+
let emb = this.embed.encode(ids);
|
|
696
|
+
ids.dispose();
|
|
697
|
+
emb = disposing(emb, ops.mulScalar(emb, this.embedScale));
|
|
698
|
+
const layer = this.layers[0]!;
|
|
699
|
+
const N = promptIds.length;
|
|
700
|
+
const window = layer.layerType === "sliding_attention" ? this.slidingWindow : null;
|
|
701
|
+
const m = cache[0]!.makeMask(N, window) as SdpaMask;
|
|
702
|
+
const ls0 = this.encoderLayerScalars[0]!;
|
|
703
|
+
|
|
704
|
+
const residual = emb;
|
|
705
|
+
const hn = layer.inputNorm.forward(emb);
|
|
706
|
+
const attnOut = layer.attn.forward(hn, m, cache[0]!, false, 0);
|
|
707
|
+
hn.dispose();
|
|
708
|
+
if (m.arr) m.arr.dispose();
|
|
709
|
+
cb("attn_out", attnOut);
|
|
710
|
+
const postAttn = layer.postAttnNorm.forward(attnOut);
|
|
711
|
+
attnOut.dispose();
|
|
712
|
+
const hMid = ops.add(residual, postAttn);
|
|
713
|
+
postAttn.dispose();
|
|
714
|
+
cb("h_mid", hMid);
|
|
715
|
+
|
|
716
|
+
const dense = layer.preFfNorm.forward(hMid);
|
|
717
|
+
let h1 = layer.mlp.forward(dense);
|
|
718
|
+
dense.dispose();
|
|
719
|
+
h1 = disposing(h1, layer.postFfNorm1.forward(h1));
|
|
720
|
+
cb("h1", h1);
|
|
721
|
+
h1.dispose();
|
|
722
|
+
|
|
723
|
+
const shape = hMid.shape;
|
|
724
|
+
const hidden = shape[shape.length - 1]!;
|
|
725
|
+
const flat = ops.reshape(hMid, [-1, hidden]);
|
|
726
|
+
const { indices, weights } = layer.router.forward(flat);
|
|
727
|
+
cb("router_idx", indices.astype(Dtype.float32));
|
|
728
|
+
cb("router_w", weights.astype(Dtype.float32));
|
|
729
|
+
let h2 = layer.preFfNorm2.forward(flat);
|
|
730
|
+
flat.dispose();
|
|
731
|
+
h2 = disposing(h2, layer.experts.forward(h2, indices, weights));
|
|
732
|
+
indices.dispose();
|
|
733
|
+
weights.dispose();
|
|
734
|
+
h2 = disposing(h2, ops.reshape(h2, shape));
|
|
735
|
+
cb("experts_raw", h2);
|
|
736
|
+
h2 = disposing(h2, layer.postFfNorm2.forward(h2));
|
|
737
|
+
cb("h2", h2);
|
|
738
|
+
h2.dispose();
|
|
739
|
+
void ls0;
|
|
740
|
+
emb.dispose();
|
|
741
|
+
hMid.dispose();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** Diagnostic: encoder prefill then finalNorm(h) — matches the reference
|
|
745
|
+
* EncoderModel.__call__ output (decoder.norm(h)) for per-stage localization. */
|
|
746
|
+
encodePromptHidden(promptIds: number[], cache: Cache[]): MlxArray {
|
|
747
|
+
const ids = MlxArray.fromInt32(Int32Array.from(promptIds), [1, promptIds.length]);
|
|
748
|
+
let h = this.embed.encode(ids);
|
|
749
|
+
ids.dispose();
|
|
750
|
+
h = disposing(h, ops.mulScalar(h, this.embedScale));
|
|
751
|
+
const N = promptIds.length;
|
|
752
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
753
|
+
const layer = this.layers[i]!;
|
|
754
|
+
const window = layer.layerType === "sliding_attention" ? this.slidingWindow : null;
|
|
755
|
+
const m = cache[i]!.makeMask(N, window) as SdpaMask;
|
|
756
|
+
const next = layer.forward(h, m, cache[i]!, false, 0, this.encoderLayerScalars[i]!);
|
|
757
|
+
if (m.arr) m.arr.dispose();
|
|
758
|
+
h.dispose();
|
|
759
|
+
h = next;
|
|
760
|
+
}
|
|
761
|
+
return disposing(h, this.finalNorm.forward(h));
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** Embed the canvas + self-conditioning. For the first step both signals are
|
|
765
|
+
* None → zero signal (which still applies SelfConditioning's post RMSNorm). */
|
|
766
|
+
#embedCanvas(
|
|
767
|
+
canvasIds: MlxArray,
|
|
768
|
+
scLogits: MlxArray | null,
|
|
769
|
+
scEmbeddings: MlxArray | null,
|
|
770
|
+
): MlxArray {
|
|
771
|
+
let inputsEmbeds = this.embed.encode(canvasIds);
|
|
772
|
+
inputsEmbeds = disposing(inputsEmbeds, ops.mulScalar(inputsEmbeds, this.embedScale));
|
|
773
|
+
let soft: MlxArray;
|
|
774
|
+
let softOwned = true;
|
|
775
|
+
if (scEmbeddings) {
|
|
776
|
+
soft = scEmbeddings.astype(inputsEmbeds.dtype);
|
|
777
|
+
} else if (scLogits === null) {
|
|
778
|
+
soft = ops.zeros(inputsEmbeds.shape, inputsEmbeds.dtype);
|
|
779
|
+
} else {
|
|
780
|
+
// probs @ embed_tokens.weight (quantized, transpose=false), * embed_scale.
|
|
781
|
+
const probs = ops.softmaxAxis(scLogits, -1, true);
|
|
782
|
+
const probsCast = probs.astype(inputsEmbeds.dtype);
|
|
783
|
+
probs.dispose();
|
|
784
|
+
const se = ops.quantizedMatmul(
|
|
785
|
+
probsCast,
|
|
786
|
+
this.embed.w,
|
|
787
|
+
this.embed.scales,
|
|
788
|
+
this.embed.biases,
|
|
789
|
+
this.embed.spec,
|
|
790
|
+
false,
|
|
791
|
+
);
|
|
792
|
+
probsCast.dispose();
|
|
793
|
+
soft = ops.mulScalar(se, this.embedScale);
|
|
794
|
+
se.dispose();
|
|
795
|
+
}
|
|
796
|
+
const out = this.selfConditioning.forward(inputsEmbeds, soft);
|
|
797
|
+
inputsEmbeds.dispose();
|
|
798
|
+
if (softOwned) soft.dispose();
|
|
799
|
+
return out;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** Build the per-layer-type decoder masks (reference _make_decoder_masks).
|
|
803
|
+
* Returns a map layerType -> SdpaMask. For the dynamic caches used here
|
|
804
|
+
* encoder_len == valid_encoder_len always, so the only non-None mask is the
|
|
805
|
+
* sliding-window case once the prompt exceeds sliding_window-1 (1023). */
|
|
806
|
+
#makeDecoderMasks(canvasLength: number, cache: Cache[]): Map<string, SdpaMask> {
|
|
807
|
+
const masks = new Map<string, SdpaMask>();
|
|
808
|
+
const types = new Set(this.layers.map((l) => l.layerType));
|
|
809
|
+
for (const layerType of types) {
|
|
810
|
+
const li = this.layers.findIndex((l) => l.layerType === layerType);
|
|
811
|
+
const c = cache[li]!;
|
|
812
|
+
const offset = c.offset;
|
|
813
|
+
// encoder_len = chronological cache length; for dynamic caches this equals
|
|
814
|
+
// min(offset, maxSize), and valid_encoder_len = min(offset, encoder_len)
|
|
815
|
+
// == encoder_len, so the "trailing invalid" branches never fire here.
|
|
816
|
+
const [encK, encV] = (c as TemporalCache).temporalView();
|
|
817
|
+
const encoderLen = encK.shape[2]!;
|
|
818
|
+
encK.dispose();
|
|
819
|
+
encV.dispose();
|
|
820
|
+
const validEncoderLen = Math.min(offset, encoderLen);
|
|
821
|
+
const keyLen = encoderLen + canvasLength;
|
|
822
|
+
|
|
823
|
+
if (layerType === "full_attention") {
|
|
824
|
+
masks.set(layerType, NO_MASK); // encoder_len == valid_encoder_len
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const windowPrefix = Math.max(this.slidingWindow - 1, 0);
|
|
828
|
+
if (encoderLen <= windowPrefix) {
|
|
829
|
+
masks.set(layerType, NO_MASK);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
// Sliding + wrapped prompt: keep encoder positions [start, valid) + canvas.
|
|
833
|
+
const start = Math.max(0, validEncoderLen - windowPrefix);
|
|
834
|
+
const positions = ops.arange(0, encoderLen, 1, Dtype.int32);
|
|
835
|
+
const startArr = ops.scalarLike(start, positions);
|
|
836
|
+
const validArr = ops.scalarLike(validEncoderLen, positions);
|
|
837
|
+
const geStart = ops.greaterEqual(positions, startArr);
|
|
838
|
+
const ltValid = ops.less(positions, validArr);
|
|
839
|
+
const encMask = ops.logicalAnd(geStart, ltValid);
|
|
840
|
+
positions.dispose();
|
|
841
|
+
startArr.dispose();
|
|
842
|
+
validArr.dispose();
|
|
843
|
+
geStart.dispose();
|
|
844
|
+
ltValid.dispose();
|
|
845
|
+
const canvasMask = ops.fromInt32(new Array(canvasLength).fill(1), [canvasLength]).astype(
|
|
846
|
+
encMask.dtype,
|
|
847
|
+
);
|
|
848
|
+
const row = ops.concatAxis([encMask, canvasMask], 0);
|
|
849
|
+
encMask.dispose();
|
|
850
|
+
canvasMask.dispose();
|
|
851
|
+
const reshaped = ops.reshape(row, [1, 1, 1, keyLen]);
|
|
852
|
+
row.dispose();
|
|
853
|
+
masks.set(layerType, { mode: "array", arr: reshaped });
|
|
854
|
+
}
|
|
855
|
+
return masks;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** Decoder canvas pass over an on-device canvas: embed canvas -> 30 layers
|
|
859
|
+
* (decoder=true) -> final norm. Returns hidden [1, canvasLength, hidden].
|
|
860
|
+
* The D2 engine drives this per denoising step (canvas stays on-device). */
|
|
861
|
+
decodeCanvasArr(
|
|
862
|
+
canvasArr: MlxArray,
|
|
863
|
+
cache: Cache[],
|
|
864
|
+
scLogits: MlxArray | null = null,
|
|
865
|
+
scEmbeddings: MlxArray | null = null,
|
|
866
|
+
): MlxArray {
|
|
867
|
+
const canvasLength = canvasArr.shape[canvasArr.shape.length - 1]!;
|
|
868
|
+
let h = this.#embedCanvas(canvasArr, scLogits, scEmbeddings);
|
|
869
|
+
const masks = this.#makeDecoderMasks(canvasLength, cache);
|
|
870
|
+
const offset = cache[0]!.offset;
|
|
871
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
872
|
+
const layer = this.layers[i]!;
|
|
873
|
+
const m = masks.get(layer.layerType)!;
|
|
874
|
+
const next = layer.forward(h, m, cache[i]!, true, offset, null);
|
|
875
|
+
h.dispose();
|
|
876
|
+
h = next;
|
|
877
|
+
}
|
|
878
|
+
for (const m of masks.values()) if (m.arr) m.arr.dispose();
|
|
879
|
+
h = disposing(h, this.finalNorm.forward(h));
|
|
880
|
+
return h;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** Decoder canvas pass from host ids (D1 path). */
|
|
884
|
+
decodeCanvas(
|
|
885
|
+
canvasIds: number[],
|
|
886
|
+
cache: Cache[],
|
|
887
|
+
scLogits: MlxArray | null = null,
|
|
888
|
+
scEmbeddings: MlxArray | null = null,
|
|
889
|
+
): MlxArray {
|
|
890
|
+
const ids = MlxArray.fromInt32(Int32Array.from(canvasIds), [1, canvasIds.length]);
|
|
891
|
+
const h = this.decodeCanvasArr(ids, cache, scLogits, scEmbeddings);
|
|
892
|
+
ids.dispose();
|
|
893
|
+
return h;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// ---- D2 denoising-engine surface ----
|
|
897
|
+
|
|
898
|
+
/** Encoder prefill over the prompt -> a fresh populated cache (the denoising
|
|
899
|
+
* loop reuses it across all steps of a canvas block). */
|
|
900
|
+
prefill(promptIds: number[]): Cache[] {
|
|
901
|
+
const cache = this.makeCache();
|
|
902
|
+
this.encodePrompt(promptIds, cache);
|
|
903
|
+
return cache;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/** One denoising-step decoder pass -> softcapped logits [1, canvas, vocab]. */
|
|
907
|
+
decoderLogits(canvasArr: MlxArray, cache: Cache[], scEmbeddings: MlxArray | null): MlxArray {
|
|
908
|
+
const h = this.decodeCanvasArr(canvasArr, cache, null, scEmbeddings);
|
|
909
|
+
const logits = this.logitsFromHidden(h);
|
|
910
|
+
h.dispose();
|
|
911
|
+
return logits;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** Dequantized embedding table (once per call), for self-conditioning soft
|
|
915
|
+
* embeddings — matches optiq's `_diffusion_soft_embedding_weight`. */
|
|
916
|
+
dequantEmbedWeight(): MlxArray {
|
|
917
|
+
return ops.dequantize(this.embed.w, this.embed.scales, this.embed.biases, this.embed.spec);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Self-conditioning soft embeddings for the NEXT step (confidence sampler):
|
|
921
|
+
* softmax(processed, precise) @ dequant(embed) * embed_scale. */
|
|
922
|
+
softEmbeddings(processedLogits: MlxArray, dequantWeight: MlxArray): MlxArray {
|
|
923
|
+
const probs = ops.softmaxAxis(processedLogits, -1, true);
|
|
924
|
+
const probsCast = probs.astype(dequantWeight.dtype);
|
|
925
|
+
probs.dispose();
|
|
926
|
+
const se = ops.matmul(probsCast, dequantWeight);
|
|
927
|
+
probsCast.dispose();
|
|
928
|
+
const cast = se.astype(dequantWeight.dtype);
|
|
929
|
+
se.dispose();
|
|
930
|
+
const out = ops.mulScalar(cast, this.embedScale);
|
|
931
|
+
cast.dispose();
|
|
932
|
+
return out;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** Re-prefill the encoder over an accepted canvas block (continuation), so the
|
|
936
|
+
* next block's decoder sees it as context. Appends to the existing cache. */
|
|
937
|
+
extendPrefill(canvasArr: MlxArray, cache: Cache[]): void {
|
|
938
|
+
let h = this.embed.encode(canvasArr);
|
|
939
|
+
h = disposing(h, ops.mulScalar(h, this.embedScale));
|
|
940
|
+
const N = canvasArr.shape[canvasArr.shape.length - 1]!;
|
|
941
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
942
|
+
const layer = this.layers[i]!;
|
|
943
|
+
const window = layer.layerType === "sliding_attention" ? this.slidingWindow : null;
|
|
944
|
+
const m = cache[i]!.makeMask(N, window) as SdpaMask;
|
|
945
|
+
const next = layer.forward(h, m, cache[i]!, false, 0, this.encoderLayerScalars[i]!);
|
|
946
|
+
if (m.arr) m.arr.dispose();
|
|
947
|
+
h.dispose();
|
|
948
|
+
h = next;
|
|
949
|
+
}
|
|
950
|
+
h.dispose();
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** Tied quantized head + fp32 softcap. hidden [1, L, hidden] -> [1, L, vocab]. */
|
|
954
|
+
logitsFromHidden(h: MlxArray): MlxArray {
|
|
955
|
+
const logits = this.embed.asLinear(h);
|
|
956
|
+
const capped = softcapFp32(logits, this.softcap);
|
|
957
|
+
logits.dispose();
|
|
958
|
+
return capped;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/** D1 entry point: prompt + canvas ids -> softcapped logits [1, canvas, vocab].
|
|
962
|
+
* Builds a fresh cache, prefills the encoder, runs one decoder pass. */
|
|
963
|
+
forwardCanvasLogits(promptIds: number[], canvasIds: number[]): MlxArray {
|
|
964
|
+
const cache = this.makeCache();
|
|
965
|
+
try {
|
|
966
|
+
this.encodePrompt(promptIds, cache);
|
|
967
|
+
const h = this.decodeCanvas(canvasIds, cache);
|
|
968
|
+
const logits = this.logitsFromHidden(h);
|
|
969
|
+
h.dispose();
|
|
970
|
+
return logits;
|
|
971
|
+
} finally {
|
|
972
|
+
for (const c of cache) c.dispose();
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** Training forward over an on-device canvas (D5 LoRA): prompt + canvas array
|
|
977
|
+
* -> softcapped logits [1, L, vocab], differentiable w.r.t. mounted LoRA. */
|
|
978
|
+
forwardCanvasLogitsArr(promptIds: number[], canvasArr: MlxArray): MlxArray {
|
|
979
|
+
const cache = this.makeCache();
|
|
980
|
+
try {
|
|
981
|
+
this.encodePrompt(promptIds, cache);
|
|
982
|
+
const h = this.decodeCanvasArr(canvasArr, cache);
|
|
983
|
+
const logits = this.logitsFromHidden(h);
|
|
984
|
+
h.dispose();
|
|
985
|
+
return logits;
|
|
986
|
+
} finally {
|
|
987
|
+
for (const c of cache) c.dispose();
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// ---- AR-only RuntimeModel surface (never called on a diffusion model) ----
|
|
992
|
+
// DiffusionGemma is non-autoregressive: generate() detects it and routes to
|
|
993
|
+
// the denoising engine (src/diffusion/diffusion-generate.ts). These exist so
|
|
994
|
+
// the model fits the shared RuntimeModel union; calling them is a bug.
|
|
995
|
+
forwardHidden(_ids: MlxArray, _cache: Cache[]): MlxArray {
|
|
996
|
+
throw new Error("DiffusionGemma is non-autoregressive — use the diffusion engine, not forwardHidden");
|
|
997
|
+
}
|
|
998
|
+
forward(_tokens: number[] | MlxArray, _cache: Cache[]): MlxArray {
|
|
999
|
+
throw new Error("DiffusionGemma is non-autoregressive — use the diffusion engine, not forward");
|
|
1000
|
+
}
|
|
1001
|
+
generate(_promptTokens: number[], _maxTokens: number, _eosIds?: number[]): number[] {
|
|
1002
|
+
throw new Error("DiffusionGemma is non-autoregressive — use diffusionGenerate, not generate");
|
|
1003
|
+
}
|
|
1004
|
+
forwardEmbeddings(_embeds: MlxArray, _cache: Cache[], _bidir: MlxArray | null): MlxArray {
|
|
1005
|
+
throw new Error("DiffusionGemma vision runs through its own encoder merge, not forwardEmbeddings");
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
readonly prefixBase = "model.decoder";
|
|
1009
|
+
|
|
1010
|
+
/** LoRA-able decoder linears (the optiq diffusion DEFAULT_LORA_KEYS: attention
|
|
1011
|
+
* q/k/v/o + dense MLP gate/up/down per layer; full layers have no v_proj). */
|
|
1012
|
+
loraTargets(): Map<string, QuantizedLinear> {
|
|
1013
|
+
const m = new Map<string, QuantizedLinear>();
|
|
1014
|
+
this.layers.forEach((layer, i) => {
|
|
1015
|
+
const p = `model.decoder.layers.${i}`;
|
|
1016
|
+
m.set(`${p}.self_attn.q_proj`, layer.attn.qProj);
|
|
1017
|
+
m.set(`${p}.self_attn.k_proj`, layer.attn.kProj);
|
|
1018
|
+
if (layer.attn.vProj) m.set(`${p}.self_attn.v_proj`, layer.attn.vProj);
|
|
1019
|
+
m.set(`${p}.self_attn.o_proj`, layer.attn.oProj);
|
|
1020
|
+
m.set(`${p}.mlp.gate_proj`, layer.mlp.gate);
|
|
1021
|
+
m.set(`${p}.mlp.up_proj`, layer.mlp.up);
|
|
1022
|
+
m.set(`${p}.mlp.down_proj`, layer.mlp.down);
|
|
1023
|
+
});
|
|
1024
|
+
return m;
|
|
1025
|
+
}
|
|
1026
|
+
}
|