Skip to main content

cortiq_engine/
music3.rs

1//! MiniMax-Music-3's flow-matching DiT: latent + conditioning → velocity.
2//!
3//! Ported against ComfyUI's `comfy/ldm/minimax_music/dit.py`. Four of its
4//! conventions are worth naming here, because each is invisible in a
5//! tensor name and wrong in a way that still produces plausible output:
6//!
7//! 1. The input is `[x | zeros_like(x) | condition]` on the CHANNEL axis
8//!    — 128 + 128 + 2048 = 2304, which is what `preprocess_conv`'s width
9//!    is telling you. The zero plane is not padding, it is a slot the
10//!    reference leaves empty.
11//! 2. Both 1×1 convs are RESIDUAL: `conv(x) + x`, not `conv(x)`.
12//! 3. The timestep embedding is prepended as an extra TOKEN, carried
13//!    through all 36 blocks, and dropped before `project_out`. It also
14//!    shifts every latent frame's rotary position by one.
15//! 4. The transformer's output is NEGATED. A flow-matching sampler that
16//!    steps the wrong way still moves, and still decodes to sound.
17//!
18//! RoPE covers the first 32 of each head's 64 dims, split-half: for
19//! `i < 16` the pair is `(x[i], x[i+16])`, rotated by `pos·inv_freq[i]`.
20
21use crate::dit::Proj;
22use crate::qtensor::QTensor;
23use crate::audiovae::SendPtr;
24use crate::pool::Pool;
25use cortiq_core::CmfModel;
26use std::sync::Arc;
27
28fn silu(v: f32) -> f32 {
29    v / (1.0 + (-v).exp())
30}
31
32/// LayerNorm with learned scale AND shift — not the RMSNorm the rest of
33/// this engine's transformers use.
34struct LayerNorm {
35    gamma: Vec<f32>,
36    beta: Vec<f32>,
37}
38
39impl LayerNorm {
40    fn load(model: &Arc<CmfModel>, p: &str) -> Result<Self, String> {
41        Ok(Self {
42            gamma: crate::dit::cmf_f32(model, &format!("{p}.gamma"))?,
43            beta: crate::dit::cmf_f32(model, &format!("{p}.beta"))?,
44        })
45    }
46
47    fn apply(&self, x: &mut [f32], d: usize) {
48        for row in x.chunks_exact_mut(d) {
49            let mean = row.iter().map(|v| *v as f64).sum::<f64>() / d as f64;
50            let var = row.iter().map(|v| (*v as f64 - mean).powi(2)).sum::<f64>() / d as f64;
51            let inv = 1.0 / (var + 1e-5).sqrt();
52            for ((v, &g), &b) in row.iter_mut().zip(&self.gamma).zip(&self.beta) {
53                *v = ((*v as f64 - mean) * inv) as f32 * g + b;
54            }
55        }
56    }
57}
58
59struct Block {
60    pre_norm: LayerNorm,
61    qkv: Proj,
62    out: Proj,
63    ff_norm: LayerNorm,
64    ff_in: Proj,
65    ff_in_b: Vec<f32>,
66    ff_out: Proj,
67    ff_out_b: Vec<f32>,
68}
69
70impl Block {
71    fn load(model: &Arc<CmfModel>, p: &str) -> Result<Self, String> {
72        Ok(Self {
73            pre_norm: LayerNorm::load(model, &format!("{p}.pre_norm"))?,
74            qkv: Proj::from_model(model, &format!("{p}.self_attn.to_qkv.weight"))?,
75            out: Proj::from_model(model, &format!("{p}.self_attn.to_out.weight"))?,
76            ff_norm: LayerNorm::load(model, &format!("{p}.ff_norm"))?,
77            ff_in: Proj::from_model(model, &format!("{p}.ff.ff.0.proj.weight"))?,
78            ff_in_b: crate::dit::cmf_f32(model, &format!("{p}.ff.ff.0.proj.bias"))?,
79            ff_out: Proj::from_model(model, &format!("{p}.ff.ff.2.weight"))?,
80            ff_out_b: crate::dit::cmf_f32(model, &format!("{p}.ff.ff.2.bias"))?,
81        })
82    }
83}
84
85pub struct Music3Dit {
86    pre_conv: Vec<f32>,  // [2304, 2304] 1x1
87    post_conv: Vec<f32>, // [128, 128] 1x1
88    fourier: Vec<f32>,   // [128]
89    t0: Proj,
90    t0_b: Vec<f32>,
91    t2: Proj,
92    t2_b: Vec<f32>,
93    project_in: Proj,
94    project_out: Proj,
95    inv_freq: Vec<f32>,
96    blocks: Vec<Block>,
97    pool: Option<Arc<Pool>>,
98    hidden: usize,
99    heads: usize,
100    hd: usize,
101    rot: usize,
102    inter: usize,
103    /// The learned mix over the AR stack's eight codebook levels, and
104    /// the 3-tap conv that turns the mixture into the DiT's condition.
105    cond_logits: Vec<f32>,
106    cond_scale: f32,
107    lc_w: Vec<f32>,
108    lc_b: Vec<f32>,
109}
110
111impl Music3Dit {
112    pub const IN_CH: usize = 128;
113    pub const COND_CH: usize = 2048;
114    pub const CONCAT_CH: usize = 2304;
115
116    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
117        let cfg: serde_json::Value =
118            serde_json::from_slice(model.tensor_bytes("mdit.config_json").map_err(|e| e.to_string())?)
119                .map_err(|e| format!("mdit.config_json: {e}"))?;
120        let u = |k: &str, d: usize| cfg[k].as_u64().map(|v| v as usize).unwrap_or(d);
121        let nl = u("num_layers", 36);
122        let dt = "mdit.diffusion_transformer";
123        Ok(Self {
124            pre_conv: crate::dit::cmf_f32(model, &format!("{dt}.preprocess_conv.weight"))?,
125            post_conv: crate::dit::cmf_f32(model, &format!("{dt}.postprocess_conv.weight"))?,
126            fourier: crate::dit::cmf_f32(model, &format!("{dt}.timestep_features.weight"))?,
127            t0: Proj::from_model(model, &format!("{dt}.to_timestep_embed.0.weight"))?,
128            t0_b: crate::dit::cmf_f32(model, &format!("{dt}.to_timestep_embed.0.bias"))?,
129            t2: Proj::from_model(model, &format!("{dt}.to_timestep_embed.2.weight"))?,
130            t2_b: crate::dit::cmf_f32(model, &format!("{dt}.to_timestep_embed.2.bias"))?,
131            project_in: Proj::from_model(model, &format!("{dt}.transformer.project_in.weight"))?,
132            project_out: Proj::from_model(model, &format!("{dt}.transformer.project_out.weight"))?,
133            inv_freq: crate::dit::cmf_f32(model, &format!("{dt}.transformer.rotary_pos_emb.inv_freq"))?,
134            blocks: (0..nl)
135                .map(|i| Block::load(model, &format!("{dt}.transformer.layers.{i}")))
136                .collect::<Result<_, _>>()?,
137            pool: Pool::from_env(),
138            hidden: u("hidden", 2048),
139            heads: u("num_heads", 32),
140            hd: u("head_dim", 64),
141            rot: u("rotary_dim", 32),
142            inter: u("ff_inner", 8192),
143            cond_logits: crate::dit::cmf_f32(model, "mdit.cond_layer_logits")?,
144            cond_scale: crate::dit::cmf_f32(model, "mdit.cond_layer_scale")?[0],
145            lc_w: crate::dit::cmf_f32(model, "mdit.latent_conditioners.0.weight")?,
146            lc_b: crate::dit::cmf_f32(model, "mdit.latent_conditioners.0.bias")?,
147        })
148    }
149
150    /// Latent frames for `audio_frames` of AR output — the reference's
151    /// `latent_length`: 44100/24000 · 960/512 = 3.4453125 per frame.
152    pub fn latent_length(audio_frames: usize) -> usize {
153        ((audio_frames as f64 * 44100.0 / 24000.0 * 960.0 / 512.0) as usize).max(1)
154    }
155
156    /// AR hidden `[frames, 8·4096]` → the DiT's condition `[2048, L]`.
157    ///
158    /// The eight are RVQ CODEBOOK levels, softmax-mixed by
159    /// `cond_layer_logits`, scaled, passed through one 3-tap conv and
160    /// then NEAREST-resampled to the latent rate. Nearest, not linear:
161    /// the reference interpolates that way and a smoother resample
162    /// smears the onset of every note.
163    pub fn aligned_condition(&self, hidden: &[f32], frames: usize) -> (Vec<f32>, usize) {
164        let levels = self.cond_logits.len();
165        let ar = hidden.len() / (frames * levels);
166        let mx = self.cond_logits.iter().cloned().fold(f32::MIN, f32::max);
167        let ex: Vec<f32> = self.cond_logits.iter().map(|v| (v - mx).exp()).collect();
168        let sum: f32 = ex.iter().sum();
169        // [ar, frames], channel-major, mixed and scaled.
170        let mut mixed = vec![0f32; ar * frames];
171        for t in 0..frames {
172            for l in 0..levels {
173                let w = ex[l] / sum * self.cond_scale;
174                let src = &hidden[(t * levels + l) * ar..(t * levels + l + 1) * ar];
175                for (c, &v) in src.iter().enumerate() {
176                    mixed[c * frames + t] += w * v;
177                }
178            }
179        }
180        // Conv1d(ar -> 2048, k=3, pad=1).
181        let out_ch = self.lc_b.len();
182        let mut conv = vec![0f32; out_ch * frames];
183        for o in 0..out_ch {
184            let dst = &mut conv[o * frames..(o + 1) * frames];
185            dst.fill(self.lc_b[o]);
186            for i in 0..ar {
187                let k = &self.lc_w[(o * ar + i) * 3..(o * ar + i + 1) * 3];
188                let src = &mixed[i * frames..(i + 1) * frames];
189                for t in 0..frames {
190                    for (j, &kv) in k.iter().enumerate() {
191                        let p = t as isize + j as isize - 1;
192                        if p >= 0 && (p as usize) < frames {
193                            dst[t] += kv * src[p as usize];
194                        }
195                    }
196                }
197            }
198        }
199        let l = Self::latent_length(frames);
200        let mut out = vec![0f32; out_ch * l];
201        for o in 0..out_ch {
202            for t in 0..l {
203                let s = (t * frames) / l.max(1);
204                out[o * l + t] = conv[o * frames + s.min(frames - 1)];
205            }
206        }
207        (out, l)
208    }
209
210    /// `[out, in]` 1×1 conv over the channel axis of a `[in, n]` panel,
211    /// added back to its input — both convs in this model are residual.
212    fn conv1x1_residual(w: &[f32], x: &mut [f32], ch: usize, n: usize) {
213        let mut acc = vec![0f32; ch * n];
214        for o in 0..ch {
215            let row = &w[o * ch..(o + 1) * ch];
216            let dst = &mut acc[o * n..(o + 1) * n];
217            for (i, &wv) in row.iter().enumerate() {
218                if wv == 0.0 {
219                    continue;
220                }
221                for (d, &s) in dst.iter_mut().zip(&x[i * n..(i + 1) * n]) {
222                    *d += wv * s;
223                }
224            }
225        }
226        for (d, a) in x.iter_mut().zip(&acc) {
227            *d += a;
228        }
229    }
230
231    /// `cat(cos, sin)` of `2π·t·w`, then the two-layer SiLU head.
232    fn timestep_embedding(&self, t: f32) -> Vec<f32> {
233        let half = self.fourier.len();
234        let mut feats = vec![0f32; half * 2];
235        for (i, &w) in self.fourier.iter().enumerate() {
236            let a = std::f32::consts::TAU * t * w;
237            feats[i] = a.cos();
238            feats[half + i] = a.sin();
239        }
240        let mut h = vec![0f32; self.t0_b.len()];
241        self.t0.matmat(&feats, 1, &mut h, self.pool.as_deref());
242        for (v, &b) in h.iter_mut().zip(&self.t0_b) {
243            *v = silu(*v + b);
244        }
245        let mut out = vec![0f32; self.t2_b.len()];
246        self.t2.matmat(&h, 1, &mut out, self.pool.as_deref());
247        for (v, &b) in out.iter_mut().zip(&self.t2_b) {
248            *v += b;
249        }
250        out
251    }
252
253    /// Split-half RoPE over the first `rot` dims of every head, applied
254    /// in place on the PACKED `[n, 3·hidden]` panel — q at column 0, k
255    /// at column `hidden`, v untouched. The panel stays packed because
256    /// that is the layout the device's split kernel reads.
257    ///
258    /// Two things this does that the twin does not. The angle depends
259    /// only on the position, so `sin_cos` is hoisted out of the head
260    /// loop — it was being recomputed `heads` times for every one of
261    /// them. And tokens own disjoint rows, so the pool splits it.
262    fn rope_packed(&self, qkv: &mut [f32], n: usize) {
263        let (hs, nh, hd) = (self.hidden, self.heads, self.hd);
264        let half = self.rot / 2;
265        let ptr = SendPtr(qkv.as_mut_ptr());
266        let work = |lo: usize, hi: usize| {
267            for p in lo..hi {
268                let base = p * 3 * hs;
269                for i in 0..half {
270                    let (s, c) = (p as f32 * self.inv_freq[i]).sin_cos();
271                    for h in 0..nh {
272                        for off in [base + h * hd, base + hs + h * hd] {
273                            // SAFETY: token `p` owns this row of the panel.
274                            let x = unsafe { ptr.row(off, hd) };
275                            let (a, b) = (x[i], x[i + half]);
276                            x[i] = a * c - b * s;
277                            x[i + half] = a * s + b * c;
278                        }
279                    }
280                }
281            }
282        };
283        match self.pool.as_deref() {
284            Some(pl) => pl.run_rows(n, &work),
285            None => work(0, n),
286        }
287    }
288
289    /// One block over `[n, hidden]`, in place.
290    fn block(&self, blk: &Block, x: &mut [f32], n: usize) {
291        let (hs, nh, hd) = (self.hidden, self.heads, self.hd);
292        let pool = self.pool.as_deref();
293        let mut h = x.to_vec();
294        blk.pre_norm.apply(&mut h, hs);
295        // The whole attention half on the card first: qkv GEMM, rope,
296        // attention and the out projection with one readback at the end.
297        // The unfused arm below reads the qkv panel back (17 MB), ships
298        // it up again for the attention, and round-trips the attention
299        // output through the out GEMM — ~45 MB a block-step on a stand
300        // whose split timer put transfers at 82% of the device arm.
301        // eps = -1 is the rope-only sentinel: this model has no qk-norm.
302        // CMF_MUSIC3_DEVATT=0 kills it; the host chain is bit-for-bit
303        // the same math.
304        if std::env::var("CMF_MUSIC3_DEVATT").as_deref() != Ok("0") {
305            if let (Some((m1, i1)), Some((m2, i2))) = (
306                match &blk.qkv {
307                    crate::dit::Proj::Q(q) => q.q4tp_mapped(),
308                    _ => None,
309                },
310                match &blk.out {
311                    crate::dit::Proj::Q(q) => q.q4tp_mapped(),
312                    _ => None,
313                },
314            ) {
315                if std::sync::Arc::ptr_eq(m1, m2) {
316                    let half = self.rot / 2;
317                    let mut ang = vec![0f32; n * half];
318                    for p in 0..n {
319                        for (i, f) in self.inv_freq[..half].iter().enumerate() {
320                            ang[p * half + i] = p as f32 * f;
321                        }
322                    }
323                    let ones = vec![1.0f32; hd];
324                    let scale = 1.0 / (hd as f32).sqrt();
325                    let mut proj = vec![0f32; n * hs];
326                    let _ta = prof::start();
327                    if crate::gpu::dit_qkv_attn_out(
328                        m1,
329                        i1,
330                        i2,
331                        &h,
332                        n,
333                        hs,
334                        nh,
335                        hd,
336                        scale,
337                        (&ang, &ones, &ones, -1.0),
338                        &mut proj,
339                    ) {
340                        prof::add(&prof::ATTN, _ta);
341                        for (a, b) in x.iter_mut().zip(&proj) {
342                            *a += b;
343                        }
344                        return self.block_ffn(blk, x, n);
345                    }
346                }
347            }
348        }
349        let mut qkv = vec![0f32; n * 3 * hs];
350        let _t = prof::start();
351        blk.qkv.matmat(&h, n, &mut qkv, pool);
352        prof::add(&prof::QKV, _t);
353        // to_qkv emits q|k|v concatenated along the FEATURE axis, so the
354        // three live at column offsets, not row offsets — which is
355        // exactly the packed layout the device's split kernel reads
356        // (`mode 0`). Rope goes on in place; nothing is unpacked here.
357        self.rope_packed(&mut qkv, n);
358        let scale = 1.0 / (hd as f32).sqrt();
359        let mut attn = vec![0f32; n * hs];
360        let _ta = prof::start();
361        // The device first. This is the quadratic part and the reason a
362        // CPU-only run was beating a GPU one: everything around it went
363        // to the card and the biggest single stage stayed home. The
364        // engine's kernel wants HEAD-major panels where the projections
365        // leave them token-major, and three transposes of n x hs are
366        // noise against n^2 work.
367        {
368            // The packed kernel is wgpu's; Metal carries only the
369            // head-major `dit_attention`, so a refusal here must fall to
370            // THAT and not to the host loop — routing everything at the
371            // packed arm would have quietly taken the attention off the
372            // card on every Mac.
373            if crate::gpu::dit_attention_packed(&qkv, nh, n, hd, scale, None, &mut attn) {
374                prof::add(&prof::ATTN, _ta);
375                let mut proj = vec![0f32; n * hs];
376                let _t = prof::start();
377                blk.out.matmat(&attn, n, &mut proj, pool);
378                prof::add(&prof::OUT, _t);
379                for (a, b) in x.iter_mut().zip(&proj) {
380                    *a += b;
381                }
382                return self.block_ffn(blk, x, n);
383            }
384            attn.fill(0.0);
385        }
386        // Everything past the packed arm wants the three panels apart.
387        let mut q = vec![0f32; n * hs];
388        let mut k = vec![0f32; n * hs];
389        let mut v = vec![0f32; n * hs];
390        for p in 0..n {
391            let s = &qkv[p * 3 * hs..(p + 1) * 3 * hs];
392            q[p * hs..(p + 1) * hs].copy_from_slice(&s[..hs]);
393            k[p * hs..(p + 1) * hs].copy_from_slice(&s[hs..2 * hs]);
394            v[p * hs..(p + 1) * hs].copy_from_slice(&s[2 * hs..]);
395        }
396        // Metal's arm: head-major panels, three transposes of n x hs
397        // against n^2 work. This is what the packed path spares wgpu,
398        // and what a Mac still needs until the split kernel is ported.
399        if crate::gpu::enabled_here() {
400            let mut qh = vec![0f32; n * hs];
401            let mut kh = vec![0f32; n * hs];
402            let mut vh = vec![0f32; n * hs];
403            for h in 0..nh {
404                for i in 0..n {
405                    let (s, d) = (i * hs + h * hd, (h * n + i) * hd);
406                    qh[d..d + hd].copy_from_slice(&q[s..s + hd]);
407                    kh[d..d + hd].copy_from_slice(&k[s..s + hd]);
408                    vh[d..d + hd].copy_from_slice(&v[s..s + hd]);
409                }
410            }
411            if crate::gpu::dit_attention(&qh, &kh, &vh, nh, nh, n, hd, scale, &mut attn) {
412                prof::add(&prof::ATTN, _ta);
413                let mut proj = vec![0f32; n * hs];
414                let _t = prof::start();
415                blk.out.matmat(&attn, n, &mut proj, pool);
416                prof::add(&prof::OUT, _t);
417                for (a, b) in x.iter_mut().zip(&proj) {
418                    *a += b;
419                }
420                return self.block_ffn(blk, x, n);
421            }
422            attn.fill(0.0);
423        }
424        // Heads are independent and write disjoint columns, so the host
425        // arm splits without a lock.
426        {
427            let ptr = SendPtr(attn.as_mut_ptr());
428            let work = |lo: usize, hi: usize| {
429                let mut scores = vec![0f32; n];
430                for hh in lo..hi {
431                    for i in 0..n {
432                        let qi = &q[i * hs + hh * hd..i * hs + hh * hd + hd];
433                        let mut mx = f32::NEG_INFINITY;
434                        for (j, sc) in scores.iter_mut().enumerate() {
435                            let kj = &k[j * hs + hh * hd..j * hs + hh * hd + hd];
436                            *sc = qi.iter().zip(kj).map(|(a, b)| a * b).sum::<f32>() * scale;
437                            mx = mx.max(*sc);
438                        }
439                        let mut sum = 0.0;
440                        for sc in scores.iter_mut() {
441                            *sc = (*sc - mx).exp();
442                            sum += *sc;
443                        }
444                        let inv = 1.0 / sum;
445                        // SAFETY: head `hh` owns these columns of every row.
446                        let dst = unsafe { ptr.row(i * hs + hh * hd, hd) };
447                        for (j, &sc) in scores.iter().enumerate() {
448                            let w = sc * inv;
449                            let vj = &v[j * hs + hh * hd..j * hs + hh * hd + hd];
450                            for (d, &vv) in dst.iter_mut().zip(vj) {
451                                *d += w * vv;
452                            }
453                        }
454                    }
455                }
456            };
457            match pool {
458                Some(p) => p.run_rows(nh, &work),
459                None => work(0, nh),
460            }
461        }
462        prof::add(&prof::ATTN, _ta);
463        let mut proj = vec![0f32; n * hs];
464        let _t = prof::start();
465        blk.out.matmat(&attn, n, &mut proj, pool);
466        prof::add(&prof::OUT, _t);
467        for (a, b) in x.iter_mut().zip(&proj) {
468            *a += b;
469        }
470        self.block_ffn(blk, x, n)
471    }
472
473    /// The second half of a block: norm, GEGLU, residual.
474    fn block_ffn(&self, blk: &Block, x: &mut [f32], n: usize) {
475        let _t = prof::start();
476        let hs = self.hidden;
477        let pool = self.pool.as_deref();
478        let mut h = x.to_vec();
479        blk.ff_norm.apply(&mut h, hs);
480        // The resident chain first: both GEMMs and the GLU on the card,
481        // with only `h` up and the block's output back. The host arm of
482        // this exchange moved 68 MB more per block-step, and the split
483        // timer put transfers at 82% of the device arm's time on the
484        // stand this was tuned on. CMF_MUSIC3_DEVFFN=0 kills it.
485        if std::env::var("CMF_MUSIC3_DEVFFN").as_deref() != Ok("0") {
486            if let (Some((m1, i1)), Some((m2, i2))) = (
487                match &blk.ff_in {
488                    crate::dit::Proj::Q(q) => q.q4tp_mapped(),
489                    _ => None,
490                },
491                match &blk.ff_out {
492                    crate::dit::Proj::Q(q) => q.q4tp_mapped(),
493                    _ => None,
494                },
495            ) {
496                if std::sync::Arc::ptr_eq(m1, m2) {
497                    let mut ffo = vec![0f32; n * hs];
498                    if crate::gpu::music3_ffn(
499                        m1,
500                        i1,
501                        i2,
502                        &h,
503                        &blk.ff_in_b,
504                        n,
505                        hs,
506                        self.inter,
507                        &mut ffo,
508                    ) {
509                        for p in 0..n {
510                            for j in 0..hs {
511                                x[p * hs + j] += ffo[p * hs + j] + blk.ff_out_b[j];
512                            }
513                        }
514                        prof::add(&prof::FFN, _t);
515                        return;
516                    }
517                }
518            }
519        }
520        let mut gu = vec![0f32; n * 2 * self.inter];
521        blk.ff_in.matmat(&h, n, &mut gu, pool);
522        // GLU here is `value * silu(gate)` with VALUE first, and the
523        // projection's bias belongs to BOTH halves before they meet.
524        // Swapping the halves still makes sound, which is why this is
525        // spelled out rather than inferred.
526        let inter = self.inter;
527        let (vb, gb) = blk.ff_in_b.split_at(inter);
528        let mut act = vec![0f32; n * inter];
529        for p in 0..n {
530            let row = &gu[p * 2 * inter..(p + 1) * 2 * inter];
531            let (val, gate) = row.split_at(inter);
532            let dst = &mut act[p * inter..(p + 1) * inter];
533            for i in 0..inter {
534                dst[i] = (val[i] + vb[i]) * silu(gate[i] + gb[i]);
535            }
536        }
537        let mut ffo = vec![0f32; n * hs];
538        blk.ff_out.matmat(&act, n, &mut ffo, pool);
539        for p in 0..n {
540            for j in 0..hs {
541                x[p * hs + j] += ffo[p * hs + j] + blk.ff_out_b[j];
542            }
543        }
544        prof::add(&prof::FFN, _t);
545    }
546
547    /// Latent frames the transformer will attend across in one go, and
548    /// the stride it advances by — `latent_length(200)` and
549    /// `latent_length(100)` in the reference. Attention is quadratic, so
550    /// a whole song in one pass is not merely slow, it is not what the
551    /// model was run as.
552    pub const WINDOW: usize = 689;
553    pub const HOP: usize = 344;
554
555    /// The velocity over any length, windowed like the reference:
556    /// overlapping passes averaged by how many covered each frame.
557    pub fn forward_windowed(&self, x: &[f32], condition: &[f32], n: usize, t: f32) -> Vec<f32> {
558        if n <= Self::WINDOW {
559            return self.forward(x, condition, n, t);
560        }
561        let ch = Self::IN_CH;
562        let cc = Self::COND_CH;
563        let mut out = vec![0f32; ch * n];
564        let mut count = vec![0f32; n];
565        let mut start = 0usize;
566        loop {
567            let end = (start + Self::WINDOW).min(n);
568            let w = end - start;
569            let mut xw = vec![0f32; ch * w];
570            for c in 0..ch {
571                xw[c * w..(c + 1) * w].copy_from_slice(&x[c * n + start..c * n + end]);
572            }
573            let mut cw = vec![0f32; cc * w];
574            for c in 0..cc {
575                cw[c * w..(c + 1) * w].copy_from_slice(&condition[c * n + start..c * n + end]);
576            }
577            let v = self.forward(&xw, &cw, w, t);
578            for c in 0..ch {
579                for i in 0..w {
580                    out[c * n + start + i] += v[c * w + i];
581                }
582            }
583            for i in 0..w {
584                count[start + i] += 1.0;
585            }
586            if end == n {
587                break;
588            }
589            start += Self::HOP;
590        }
591        for c in 0..ch {
592            for i in 0..n {
593                out[c * n + i] /= count[i];
594            }
595        }
596        out
597    }
598
599    /// `x` is `[128, n]` and `condition` `[2048, n]`; the result is the
600    /// velocity at `[128, n]`.
601    pub fn forward(&self, x: &[f32], condition: &[f32], n: usize, t: f32) -> Vec<f32> {
602        let pool = self.pool.as_deref();
603        let mut full = vec![0f32; Self::CONCAT_CH * n];
604        full[..Self::IN_CH * n].copy_from_slice(x);
605        // rows 128..256 stay zero: the reference's `zeros_like(x)` plane
606        full[2 * Self::IN_CH * n..].copy_from_slice(condition);
607        Self::conv1x1_residual(&self.pre_conv, &mut full, Self::CONCAT_CH, n);
608
609        // channel-major -> token-major for the transformer
610        let mut toks = vec![0f32; n * Self::CONCAT_CH];
611        for c in 0..Self::CONCAT_CH {
612            for p in 0..n {
613                toks[p * Self::CONCAT_CH + c] = full[c * n + p];
614            }
615        }
616        let mut h = vec![0f32; n * self.hidden];
617        self.project_in.matmat(&toks, n, &mut h, pool);
618
619        // The timestep rides as token 0 and shifts every rotary position.
620        let temb = self.timestep_embedding(t);
621        let mut seq = vec![0f32; (n + 1) * self.hidden];
622        seq[..self.hidden].copy_from_slice(&temb);
623        seq[self.hidden..].copy_from_slice(&h);
624        for blk in &self.blocks {
625            self.block(blk, &mut seq, n + 1);
626        }
627
628        let mut out = vec![0f32; n * Self::IN_CH];
629        self.project_out
630            .matmat(&seq[self.hidden..], n, &mut out, pool);
631        let mut ch = vec![0f32; Self::IN_CH * n];
632        for c in 0..Self::IN_CH {
633            for p in 0..n {
634                ch[c * n + p] = out[p * Self::IN_CH + c];
635            }
636        }
637        Self::conv1x1_residual(&self.post_conv, &mut ch, Self::IN_CH, n);
638        for v in ch.iter_mut() {
639            *v = -*v;
640        }
641        ch
642    }
643
644    /// Denoise `[128, n]` from noise to a latent, `steps` Euler steps
645    /// along σ: 1 → 0. `progress` is called with (step, total).
646    pub fn sample(
647        &self,
648        noise: &[f32],
649        condition: &[f32],
650        n: usize,
651        steps: usize,
652        mut progress: impl FnMut(usize, usize),
653    ) -> Vec<f32> {
654        let sigmas = flow_sigmas(steps);
655        let mut x = noise.to_vec();
656        for i in 0..steps {
657            let (s, s_next) = (sigmas[i], sigmas[i + 1]);
658            // ComfyUI's process_timestep for this model.
659            let v = self.forward_windowed(&x, condition, n, 1.0 - s);
660            let dt = s_next - s;
661            for (a, b) in x.iter_mut().zip(&v) {
662                *a += dt * b;
663            }
664            progress(i + 1, steps);
665        }
666        x
667    }
668}
669
670/// Euler flow-matching sampler for Music-3.
671///
672/// ComfyUI registers this model as a plain `ModelType.FLOW` with
673/// `multiplier: 1.0` and `process_timestep(t) = 1.0 - t`, so the sampler
674/// is the ordinary one and NOT the `FlowMatchEulerDiscreteScheduler`
675/// named in MiniMax's own `scheduler_config.json` — that belongs to
676/// their diffusers pipeline. Worth stating because the config is the
677/// first thing you find and it sends you somewhere else: with
678/// `num_train_timesteps: 1` its schedule degenerates to a constant,
679/// which is the tell that the caller supplies the sigmas.
680///
681/// σ walks 1 → 0, the DiT is asked at `1 − σ`, and the step is
682/// `x += (σ_next − σ)·v`. The DiT already negates its own output, so
683/// the sign lives there rather than here.
684///
685/// The walk is NOT uniform, and that detail is audible. ComfyUI's
686/// `normal_scheduler` evaluates at `linspace(σ_max, σ_min, steps)` and
687/// only THEN appends zero, and this model's `ModelSamplingDiscreteFlow`
688/// has `σ_min = 1/1000` — so the last velocity is measured essentially
689/// at the end of the trajectory. A uniform `1 → 0` in `steps` stops at
690/// `1/steps` and integrates the whole remaining tail from a velocity
691/// sampled well before it, which is a smeared, mushy final approach.
692pub fn flow_sigmas(steps: usize) -> Vec<f32> {
693    const SIGMA_MIN: f32 = 0.001;
694    let n = steps.max(1);
695    let mut s: Vec<f32> = (0..n)
696        .map(|i| {
697            if n == 1 {
698                1.0
699            } else {
700                1.0 + (SIGMA_MIN - 1.0) * i as f32 / (n - 1) as f32
701            }
702        })
703        .collect();
704    s.push(0.0);
705    s
706}
707
708/// RMSNorm with a weight and no bias, eps 1e-6.
709struct RmsNorm {
710    w: Vec<f32>,
711}
712
713impl RmsNorm {
714    fn load(model: &Arc<CmfModel>, n: &str) -> Result<Self, String> {
715        Ok(Self {
716            w: crate::dit::cmf_f32(model, n)?,
717        })
718    }
719
720    fn apply(&self, x: &mut [f32], d: usize) {
721        for row in x.chunks_exact_mut(d) {
722            let ss = row.iter().map(|v| (*v as f64) * (*v as f64)).sum::<f64>() / d as f64;
723            let inv = 1.0 / (ss + 1e-6).sqrt();
724            for (v, &g) in row.iter_mut().zip(&self.w) {
725                *v = (*v as f64 * inv) as f32 * g;
726            }
727        }
728    }
729}
730
731struct RvqBlock {
732    n1: RmsNorm,
733    q: Proj,
734    k: Proj,
735    v: Proj,
736    o: Proj,
737    n2: RmsNorm,
738    gate: Proj,
739    up: Proj,
740    down: Proj,
741}
742
743impl RvqBlock {
744    fn load(model: &Arc<CmfModel>, p: &str) -> Result<Self, String> {
745        Ok(Self {
746            n1: RmsNorm::load(model, &format!("{p}.input_layernorm.weight"))?,
747            q: Proj::from_model(model, &format!("{p}.self_attn.q_proj.weight"))?,
748            k: Proj::from_model(model, &format!("{p}.self_attn.k_proj.weight"))?,
749            v: Proj::from_model(model, &format!("{p}.self_attn.v_proj.weight"))?,
750            o: Proj::from_model(model, &format!("{p}.self_attn.o_proj.weight"))?,
751            n2: RmsNorm::load(model, &format!("{p}.post_attention_layernorm.weight"))?,
752            gate: Proj::from_model(model, &format!("{p}.mlp.gate_proj.weight"))?,
753            up: Proj::from_model(model, &format!("{p}.mlp.up_proj.weight"))?,
754            down: Proj::from_model(model, &format!("{p}.mlp.down_proj.weight"))?,
755        })
756    }
757}
758
759/// The RVQ depth decoder: given the frame's hidden state and the codes
760/// chosen so far, it predicts the next codebook level.
761///
762/// It is a small CAUSAL transformer over a sequence that never exceeds
763/// the codebook count — the positional table is 16 rows for 8 levels —
764/// with no rotary embedding at all. Attention that forgets the mask here
765/// leaks a level's own answer backwards and the model still samples.
766pub struct RvqDepthDecoder {
767    projection: Proj,
768    pos: Vec<f32>,
769    blocks: Vec<RvqBlock>,
770    norm: RmsNorm,
771    heads: Vec<Proj>,
772    pool: Option<Arc<Pool>>,
773    hidden: usize,
774    nh: usize,
775    hd: usize,
776    inter: usize,
777}
778
779impl RvqDepthDecoder {
780    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
781        let cfg: serde_json::Value =
782            serde_json::from_slice(model.tensor_bytes("mte.config_json").map_err(|e| e.to_string())?)
783                .map_err(|e| format!("mte.config_json: {e}"))?;
784        let u = |k: &str, d: usize| cfg[k].as_u64().map(|v| v as usize).unwrap_or(d);
785        let hidden = u("hidden_size", 4096);
786        let nl = u("decoder_num_layers", 4);
787        let nh = u("decoder_num_heads", 16);
788        let cb = u("audio_num_codebooks", 8);
789        Ok(Self {
790            projection: Proj::from_model(model, "mte.audio_decoder.projection.weight")?,
791            pos: crate::dit::cmf_f32(model, "mte.audio_decoder.pos_embedding.weight")?,
792            blocks: (0..nl)
793                .map(|i| RvqBlock::load(model, &format!("mte.audio_decoder.layers.{i}")))
794                .collect::<Result<_, _>>()?,
795            norm: RmsNorm::load(model, "mte.audio_decoder.norm.weight")?,
796            heads: (0..cb - 1)
797                .map(|i| {
798                    Proj::from_model(model, &format!("mte.audio_decoder.audio_heads.{i}.weight"))
799                })
800                .collect::<Result<_, _>>()?,
801            pool: Pool::from_env(),
802            hidden,
803            nh,
804            hd: hidden / nh,
805            inter: u("decoder_intermediate_size", 6144),
806        })
807    }
808
809    pub fn codebooks(&self) -> usize {
810        self.heads.len() + 1
811    }
812
813    /// `projection` is applied to every element entering the sequence —
814    /// the frame hidden, the c0 embedding and each extra embedding.
815    pub fn project(&self, x: &[f32]) -> Vec<f32> {
816        let n = x.len() / self.hidden;
817        let mut out = vec![0f32; n * self.hidden];
818        self.projection
819            .matmat(x, n, &mut out, self.pool.as_deref());
820        out
821    }
822
823    /// Run the stack over `[n, hidden]` and return the LAST position's
824    /// normed hidden — the only one the caller reads.
825    pub fn forward_last(&self, seq: &[f32], n: usize) -> Vec<f32> {
826        let (hs, nh, hd) = (self.hidden, self.nh, self.hd);
827        let pool = self.pool.as_deref();
828        let mut x = seq.to_vec();
829        for p in 0..n {
830            for (v, &pe) in x[p * hs..(p + 1) * hs].iter_mut().zip(&self.pos[p * hs..(p + 1) * hs]) {
831                *v += pe;
832            }
833        }
834        for blk in &self.blocks {
835            let mut h = x.clone();
836            blk.n1.apply(&mut h, hs);
837            let (mut q, mut k, mut v) = (vec![0f32; n * hs], vec![0f32; n * hs], vec![0f32; n * hs]);
838            blk.q.matmat(&h, n, &mut q, pool);
839            blk.k.matmat(&h, n, &mut k, pool);
840            blk.v.matmat(&h, n, &mut v, pool);
841            let scale = 1.0 / (hd as f32).sqrt();
842            let mut attn = vec![0f32; n * hs];
843            for hh in 0..nh {
844                for i in 0..n {
845                    let qi = &q[i * hs + hh * hd..i * hs + hh * hd + hd];
846                    // Causal: position i sees 0..=i and nothing after.
847                    let mut sc = vec![0f32; i + 1];
848                    let mut mx = f32::NEG_INFINITY;
849                    for (j, s) in sc.iter_mut().enumerate() {
850                        let kj = &k[j * hs + hh * hd..j * hs + hh * hd + hd];
851                        *s = qi.iter().zip(kj).map(|(a, b)| a * b).sum::<f32>() * scale;
852                        mx = mx.max(*s);
853                    }
854                    let mut sum = 0.0;
855                    for s in sc.iter_mut() {
856                        *s = (*s - mx).exp();
857                        sum += *s;
858                    }
859                    let inv = 1.0 / sum;
860                    let dst = &mut attn[i * hs + hh * hd..i * hs + hh * hd + hd];
861                    for (j, &s) in sc.iter().enumerate() {
862                        let w = s * inv;
863                        let vj = &v[j * hs + hh * hd..j * hs + hh * hd + hd];
864                        for (d, &vv) in dst.iter_mut().zip(vj) {
865                            *d += w * vv;
866                        }
867                    }
868                }
869            }
870            let mut proj = vec![0f32; n * hs];
871            blk.o.matmat(&attn, n, &mut proj, pool);
872            for (a, b) in x.iter_mut().zip(&proj) {
873                *a += b;
874            }
875
876            let mut h = x.clone();
877            blk.n2.apply(&mut h, hs);
878            let (mut g, mut u) = (vec![0f32; n * self.inter], vec![0f32; n * self.inter]);
879            blk.gate.matmat(&h, n, &mut g, pool);
880            blk.up.matmat(&h, n, &mut u, pool);
881            for (a, b) in g.iter_mut().zip(&u) {
882                *a = silu(*a) * b;
883            }
884            let mut ffo = vec![0f32; n * hs];
885            blk.down.matmat(&g, n, &mut ffo, pool);
886            for (a, b) in x.iter_mut().zip(&ffo) {
887                *a += b;
888            }
889        }
890        let mut last = x[(n - 1) * hs..].to_vec();
891        self.norm.apply(&mut last, hs);
892        last
893    }
894
895    /// Logits for codebook `level` (1-based; level 1 uses head 0).
896    pub fn head(&self, level: usize, hidden: &[f32]) -> Vec<f32> {
897        let h = &self.heads[level - 1];
898        let rows = match h {
899            Proj::F32 { rows, .. } => *rows,
900            Proj::Q(q) => q.rows(),
901        };
902        let mut out = vec![0f32; rows];
903        h.matmat(hidden, 1, &mut out, self.pool.as_deref());
904        out
905    }
906}
907
908// ── the autoregressive stack ────────────────────────────────────────
909
910/// One Qwen3 block: RMSNorm → GQA attention with per-head q/k norms and
911/// split-half RoPE → residual → RMSNorm → SwiGLU → residual.
912struct ArBlock {
913    n1: RmsNorm,
914    q: Proj,
915    k: Proj,
916    v: Proj,
917    o: Proj,
918    qn: RmsNorm,
919    kn: RmsNorm,
920    n2: RmsNorm,
921    gate: Proj,
922    up: Proj,
923    down: Proj,
924}
925
926impl ArBlock {
927    fn load(model: &Arc<CmfModel>, p: &str) -> Result<Self, String> {
928        Ok(Self {
929            n1: RmsNorm::load(model, &format!("{p}.input_layernorm.weight"))?,
930            q: Proj::from_model(model, &format!("{p}.self_attn.q_proj.weight"))?,
931            k: Proj::from_model(model, &format!("{p}.self_attn.k_proj.weight"))?,
932            v: Proj::from_model(model, &format!("{p}.self_attn.v_proj.weight"))?,
933            o: Proj::from_model(model, &format!("{p}.self_attn.o_proj.weight"))?,
934            qn: RmsNorm::load(model, &format!("{p}.self_attn.q_norm.weight"))?,
935            kn: RmsNorm::load(model, &format!("{p}.self_attn.k_norm.weight"))?,
936            n2: RmsNorm::load(model, &format!("{p}.post_attention_layernorm.weight"))?,
937            gate: Proj::from_model(model, &format!("{p}.mlp.gate_proj.weight"))?,
938            up: Proj::from_model(model, &format!("{p}.mlp.up_proj.weight"))?,
939            down: Proj::from_model(model, &format!("{p}.mlp.down_proj.weight"))?,
940        })
941    }
942}
943
944/// Keys and values for one layer, one CFG branch.
945///
946/// Per HEAD, because that is the shape the token graph mirrors from:
947/// it uploads `cpu_k[h][synced..position]` into its device cache each
948/// step. A flat `[pos][nkv·hd]` buffer would have to be transposed on
949/// every token to hand it over.
950#[derive(Default, Clone)]
951struct KvRun {
952    k: Vec<Vec<f32>>,
953    v: Vec<Vec<f32>>,
954    len: usize,
955}
956
957impl KvRun {
958    fn init(nkv: usize) -> Self {
959        Self {
960            k: vec![Vec::new(); nkv],
961            v: vec![Vec::new(); nkv],
962            len: 0,
963        }
964    }
965}
966
967/// Deterministic top-k sampler.
968///
969/// NOT torch's: reproducing `torch.multinomial` under a seeded
970/// `Generator` bit-for-bit is its own project, and nothing downstream
971/// needs the same seed to mean the same song — only that one seed here
972/// always means one song.
973struct Rng(u64);
974
975impl Rng {
976    fn new(seed: u64) -> Self {
977        Self(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1)
978    }
979    fn next_f32(&mut self) -> f32 {
980        let mut x = self.0;
981        x ^= x << 13;
982        x ^= x >> 7;
983        x ^= x << 17;
984        self.0 = x;
985        ((x >> 40) as f32) / (1u32 << 24) as f32
986    }
987}
988
989fn sample_topk(logits: &[f32], top_k: usize, rng: &mut Rng) -> usize {
990    let mut idx: Vec<usize> = (0..logits.len()).filter(|&i| logits[i].is_finite()).collect();
991    idx.sort_unstable_by(|&a, &b| logits[b].partial_cmp(&logits[a]).unwrap());
992    idx.truncate(top_k.max(1));
993    let mx = idx.iter().map(|&i| logits[i]).fold(f32::MIN, f32::max);
994    let exps: Vec<f32> = idx.iter().map(|&i| (logits[i] - mx).exp()).collect();
995    let sum: f32 = exps.iter().sum();
996    let mut r = rng.next_f32() * sum;
997    for (j, &e) in exps.iter().enumerate() {
998        r -= e;
999        if r <= 0.0 {
1000            return idx[j];
1001        }
1002    }
1003    *idx.last().unwrap()
1004}
1005
1006/// MiniMax-Music-3's AR stack: it does not encode a prompt, it GENERATES
1007/// the conditioning — audio tokens sampled frame by frame, whose hidden
1008/// states become what the DiT is conditioned on.
1009pub struct Music3Ar {
1010    blocks: Vec<ArBlock>,
1011    norm: RmsNorm,
1012    embed_prefill: QTensor,
1013    embed_audio: QTensor,
1014    embed_extra: QTensor,
1015    lm_head: Proj,
1016    pub depth: RvqDepthDecoder,
1017    inv_freq: Vec<f32>,
1018    pool: Option<Arc<Pool>>,
1019    hidden: usize,
1020    nh: usize,
1021    nkv: usize,
1022    hd: usize,
1023    inter: usize,
1024    audio_vocab: usize,
1025    codebooks: usize,
1026    pub cfg_scale: f32,
1027    pub top_k: usize,
1028    pub fps: usize,
1029    pub max_frames: usize,
1030}
1031
1032/// Token ids the prompt is built from — `comfy/ldm/minimax_music/prompt.py`.
1033pub mod tokens {
1034    pub const IM_START: u32 = 151644;
1035    pub const IM_END: u32 = 151645;
1036    pub const AUDIO_CFG: u32 = 151654;
1037    pub const AUDIO_START: u32 = 151669;
1038    pub const CAPTION_START: u32 = 151671;
1039    pub const CAPTION_END: u32 = 151672;
1040    pub const LYRICS_START: u32 = 151673;
1041    pub const LYRICS_END: u32 = 151674;
1042}
1043
1044impl Music3Ar {
1045    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
1046        let cfg: serde_json::Value =
1047            serde_json::from_slice(model.tensor_bytes("mte.config_json").map_err(|e| e.to_string())?)
1048                .map_err(|e| format!("mte.config_json: {e}"))?;
1049        let u = |k: &str, d: usize| cfg[k].as_u64().map(|v| v as usize).unwrap_or(d);
1050        let f = |k: &str, d: f64| cfg[k].as_f64().unwrap_or(d);
1051        let hidden = u("hidden_size", 4096);
1052        let hd = u("head_dim", 128);
1053        let theta = f("rope_theta", 1_000_000.0) as f32;
1054        Ok(Self {
1055            blocks: (0..u("num_hidden_layers", 36))
1056                .map(|i| ArBlock::load(model, &format!("mte.layers.{i}")))
1057                .collect::<Result<_, _>>()?,
1058            norm: RmsNorm::load(model, "mte.norm.weight")?,
1059            embed_prefill: QTensor::from_model(model, "mte.embed_tokens_prefill.weight")?,
1060            embed_audio: QTensor::from_model(model, "mte.embed_tokens_audio.weight")?,
1061            embed_extra: QTensor::from_model(model, "mte.audio_extra_embedding.weight")?,
1062            lm_head: Proj::from_model(model, "mte.lm_head_pruned.weight")?,
1063            depth: RvqDepthDecoder::from_cmf(model)?,
1064            inv_freq: (0..hd / 2)
1065                .map(|i| 1.0 / theta.powf(2.0 * i as f32 / hd as f32))
1066                .collect(),
1067            pool: Pool::from_env(),
1068            hidden,
1069            nh: u("num_attention_heads", 32),
1070            nkv: u("num_key_value_heads", 8),
1071            hd,
1072            inter: u("intermediate_size", 12288),
1073            audio_vocab: u("audio_vocab_size", 1024),
1074            codebooks: u("audio_num_codebooks", 8),
1075            cfg_scale: f("cfg_scale", 1.5) as f32,
1076            top_k: u("top_k", 50),
1077            fps: u("audio_frames_per_second", 25),
1078            max_frames: u("max_audio_frames", 9000),
1079        })
1080    }
1081
1082    /// One embedding row. A table lookup, not a matmul: these are
1083    /// `[vocab, hidden]` and only ever read one row at a time.
1084    fn embed_row(p: &QTensor, row: usize, hidden: usize) -> Vec<f32> {
1085        let mut out = vec![0f32; hidden];
1086        p.row_f32(row, &mut out);
1087        out
1088    }
1089
1090    /// One decode position through the WHOLE stack in a single device
1091    /// submission, per CFG branch.
1092    ///
1093    /// The op-by-op path spends ~9 µs of arithmetic behind each of five
1094    /// dispatches a layer — 360 round trips a token — and the card idles
1095    /// between them however cheap the trip is. `forward_token_graph`
1096    /// already solves exactly this for text models: it keeps K/V on the
1097    /// device per `(kv_id, layer)`, mirrors the newly appended positions
1098    /// from the host cache, and folds the final norm and lm_head into
1099    /// the same submit. Qwen3 is its native shape, q/k norms included,
1100    /// so no kernel is new here — only the wiring.
1101    ///
1102    /// Returns the logits per branch when the graph took the token.
1103    #[allow(clippy::type_complexity)]
1104    fn graph_step(
1105        &self,
1106        x: &mut [f32],
1107        pos: usize,
1108        cap: usize,
1109        cache: &mut [Vec<KvRun>],
1110        logits: &mut [Vec<f32>; 2],
1111    ) -> bool {
1112        let hs = self.hidden;
1113        let Some((model, _)) = self.blocks.first().and_then(|b| b.q.graph_w()) else {
1114            return false;
1115        };
1116        let model = model.clone();
1117        let Some((_, lm)) = self.lm_head.graph_w() else {
1118            return false;
1119        };
1120        let lm_rows = self.lm_head_rows();
1121        let mut staged: [(Vec<f32>, Vec<f32>); 2] = Default::default();
1122        for bi in 0..2 {
1123            let mut layers: Vec<crate::gpu::GraphLayer> = Vec::with_capacity(self.blocks.len());
1124            for (li, blk) in self.blocks.iter().enumerate() {
1125                let (Some((_, wq)), Some((_, wk)), Some((_, wv)), Some((_, wo))) = (
1126                    blk.q.graph_w(),
1127                    blk.k.graph_w(),
1128                    blk.v.graph_w(),
1129                    blk.o.graph_w(),
1130                ) else {
1131                    return false;
1132                };
1133                let (Some((_, g)), Some((_, u)), Some((_, d))) = (
1134                    blk.gate.graph_w(),
1135                    blk.up.graph_w(),
1136                    blk.down.graph_w(),
1137                ) else {
1138                    return false;
1139                };
1140                layers.push(crate::gpu::GraphLayer {
1141                    input_norm: &blk.n1.w,
1142                    attn: crate::gpu::GraphAttn::Full {
1143                        wq,
1144                        wk,
1145                        wv,
1146                        wo,
1147                        q_norm: Some(&blk.qn.w),
1148                        k_norm: Some(&blk.kn.w),
1149                        bias: None,
1150                        output_gate: false,
1151                        cpu_k: &cache[li][bi].k,
1152                        cpu_v: &cache[li][bi].v,
1153                    },
1154                    post_norm: &blk.n2.w,
1155                    ffn: crate::gpu::GraphFfn::Dense {
1156                        gate: g,
1157                        up: u,
1158                        down: d,
1159                    },
1160                });
1161            }
1162            let mut h = x[bi * hs..(bi + 1) * hs].to_vec();
1163            let mut out = Vec::new();
1164            let ok = crate::gpu::forward_token_graph(
1165                &model,
1166                bi as u64,
1167                &layers,
1168                &[],
1169                0,
1170                &self.inv_freq,
1171                &mut h,
1172                self.nh,
1173                self.nkv,
1174                self.hd,
1175                self.hd,
1176                hs,
1177                self.inter,
1178                pos,
1179                cap,
1180                false,
1181                1e-6,
1182                Some((&lm, lm_rows)),
1183                &self.norm.w,
1184                &mut out,
1185                &[],
1186                1,
1187                None,
1188                None,
1189                None,
1190                0,
1191            );
1192            if !ok || out.len() < lm_rows {
1193                return false;
1194            }
1195            staged[bi] = (h, out);
1196        }
1197        // Both branches or neither: a half-applied token would leave the
1198        // two CFG streams a position apart, which reads as the model
1199        // losing the plot rather than as an error.
1200        for (bi, (h, out)) in staged.into_iter().enumerate() {
1201            x[bi * hs..(bi + 1) * hs].copy_from_slice(&h);
1202            logits[bi] = out;
1203        }
1204        true
1205    }
1206
1207    /// Run one position through every block, appending to the cache.
1208    /// `x` is `[b, hidden]` for the CFG pair; returns the normed hidden.
1209    fn step_blocks(&self, x: &mut [f32], b: usize, pos: usize, cache: &mut [Vec<KvRun>]) {
1210        let (hs, nh, nkv, hd) = (self.hidden, self.nh, self.nkv, self.hd);
1211        let pool = self.pool.as_deref();
1212        let kvw = nkv * hd;
1213        for (li, blk) in self.blocks.iter().enumerate() {
1214            let mut h = x.to_vec();
1215            blk.n1.apply(&mut h, hs);
1216            let mut q = vec![0f32; b * nh * hd];
1217            let mut k = vec![0f32; b * kvw];
1218            let mut v = vec![0f32; b * kvw];
1219            blk.q.matmat(&h, b, &mut q, pool);
1220            blk.k.matmat(&h, b, &mut k, pool);
1221            blk.v.matmat(&h, b, &mut v, pool);
1222            // Per-head RMSNorm on q and k BEFORE the rotation, then
1223            // split-half RoPE — Qwen3's order, not the other way round.
1224            for bi in 0..b {
1225                for hh in 0..nh {
1226                    let s = bi * nh * hd + hh * hd;
1227                    blk.qn.apply(&mut q[s..s + hd], hd);
1228                    rope_half(&mut q[s..s + hd], pos, &self.inv_freq);
1229                }
1230                for hh in 0..nkv {
1231                    let s = bi * kvw + hh * hd;
1232                    blk.kn.apply(&mut k[s..s + hd], hd);
1233                    rope_half(&mut k[s..s + hd], pos, &self.inv_freq);
1234                }
1235            }
1236            let mut attn = vec![0f32; b * nh * hd];
1237            let per_kv = nh / nkv;
1238            for bi in 0..b {
1239                let run = &mut cache[li][bi];
1240                for g in 0..nkv {
1241                    run.k[g].extend_from_slice(&k[bi * kvw + g * hd..bi * kvw + (g + 1) * hd]);
1242                    run.v[g].extend_from_slice(&v[bi * kvw + g * hd..bi * kvw + (g + 1) * hd]);
1243                }
1244                run.len += 1;
1245                let n = run.len;
1246                let scale = 1.0 / (hd as f32).sqrt();
1247                for hh in 0..nh {
1248                    let g = hh / per_kv;
1249                    let qi = &q[bi * nh * hd + hh * hd..bi * nh * hd + hh * hd + hd];
1250                    let mut sc = vec![0f32; n];
1251                    let mut mx = f32::NEG_INFINITY;
1252                    for (j, s) in sc.iter_mut().enumerate() {
1253                        let kj = &run.k[g][j * hd..(j + 1) * hd];
1254                        *s = qi.iter().zip(kj).map(|(a, c)| a * c).sum::<f32>() * scale;
1255                        mx = mx.max(*s);
1256                    }
1257                    let mut sum = 0.0;
1258                    for s in sc.iter_mut() {
1259                        *s = (*s - mx).exp();
1260                        sum += *s;
1261                    }
1262                    let inv = 1.0 / sum;
1263                    let dst = &mut attn[bi * nh * hd + hh * hd..bi * nh * hd + hh * hd + hd];
1264                    for (j, &s) in sc.iter().enumerate() {
1265                        let w = s * inv;
1266                        let vj = &run.v[g][j * hd..(j + 1) * hd];
1267                        for (d, &vv) in dst.iter_mut().zip(vj) {
1268                            *d += w * vv;
1269                        }
1270                    }
1271                }
1272            }
1273            let mut proj = vec![0f32; b * hs];
1274            blk.o.matmat(&attn, b, &mut proj, pool);
1275            for (a, c) in x.iter_mut().zip(&proj) {
1276                *a += c;
1277            }
1278            let mut h = x.to_vec();
1279            blk.n2.apply(&mut h, hs);
1280            let (mut g, mut u2) = (vec![0f32; b * self.inter], vec![0f32; b * self.inter]);
1281            blk.gate.matmat(&h, b, &mut g, pool);
1282            blk.up.matmat(&h, b, &mut u2, pool);
1283            for (a, c) in g.iter_mut().zip(&u2) {
1284                *a = silu(*a) * c;
1285            }
1286            let mut ffo = vec![0f32; b * hs];
1287            blk.down.matmat(&g, b, &mut ffo, pool);
1288            for (a, c) in x.iter_mut().zip(&ffo) {
1289                *a += c;
1290            }
1291        }
1292    }
1293
1294    /// Generate `frames` of conditioning: `[frames, 8·hidden]`.
1295    ///
1296    /// The CFG pair runs as batch 2 — the conditioned prompt and one
1297    /// whose middle is replaced by `<|audio_cfg|>` — and every sampled
1298    /// code is shared by both branches, which is what makes them stay
1299    /// in step.
1300    pub fn generate(
1301        &self,
1302        prompt_ids: &[u32],
1303        seed: u64,
1304        frames: usize,
1305        mut progress: impl FnMut(usize, usize),
1306    ) -> Result<(Vec<f32>, usize), String> {
1307        let hs = self.hidden;
1308        let pool = self.pool.as_deref();
1309        let want = frames.min(self.max_frames);
1310        let mut cache: Vec<Vec<KvRun>> = (0..self.blocks.len())
1311            .map(|_| vec![KvRun::init(self.nkv); 2])
1312            .collect();
1313        // The unconditioned branch keeps the frame but not the words.
1314        let mut uncond = prompt_ids.to_vec();
1315        if uncond.len() > 3 {
1316            let n = uncond.len();
1317            for t in uncond[1..n - 2].iter_mut() {
1318                *t = tokens::AUDIO_CFG;
1319            }
1320        }
1321        let cap = prompt_ids.len() + want + 2;
1322        // One path for the whole generation. The graph keeps K/V on the
1323        // device and the host loop keeps it here; alternating between
1324        // them mid-song would leave the two caches disagreeing.
1325        // Opt-in, not opt-out. Measured on a 3090 next to a 256-core EPYC:
1326        // the whole-token graph costs 136.7 s of AR against 47.8 s host,
1327        // 2.9x SLOWER. The AR is batch-1/2 matvec, so every layer is a
1328        // bandwidth errand too small to amortise a submit, while the host
1329        // arm has 256 cores to spread it over. The graph is the right
1330        // shape for a thin CPU; it is the wrong one here, so it ships off.
1331        // (Gating this on `enabled_here()` was also wrong — the AR runs
1332        // before the backend is up, so that read false and the graph was
1333        // never once attempted. Keep it un-gated so the A/B stays honest.)
1334        let mut graph = std::env::var("CMF_MUSIC3_GRAPH").as_deref() == Ok("1");
1335        let mut glogits: [Vec<f32>; 2] = Default::default();
1336        let mut last = vec![0f32; 2 * hs];
1337        for (pos, (&a, &b)) in prompt_ids.iter().zip(&uncond).enumerate() {
1338            let mut x = vec![0f32; 2 * hs];
1339            x[..hs].copy_from_slice(&Self::embed_row(&self.embed_prefill, a as usize, hs));
1340            x[hs..].copy_from_slice(&Self::embed_row(&self.embed_prefill, b as usize, hs));
1341            if graph {
1342                if self.graph_step(&mut x, pos, cap, &mut cache, &mut glogits) {
1343                    last = x;
1344                    if pos % 64 == 0 {
1345                        progress(0, want);
1346                    }
1347                    continue;
1348                }
1349                // Refused on the first token: nothing has been committed,
1350                // so the host arm starts clean from position 0.
1351                if pos > 0 {
1352                    return Err("the token graph refused mid-prefill".into());
1353                }
1354                graph = false;
1355            }
1356            self.step_blocks(&mut x, 2, pos, &mut cache);
1357            last = x;
1358            if pos % 64 == 0 {
1359                progress(0, want);
1360            }
1361        }
1362        let mut rng = Rng::new(seed);
1363        let mut out: Vec<f32> = Vec::with_capacity(want * self.codebooks * hs);
1364        let mut done = 0usize;
1365        let scale = (self.codebooks as f32).powf(-0.5);
1366        for frame in 0..want {
1367            let mut normed = last.clone();
1368            self.norm.apply(&mut normed, hs);
1369            // c0 with classifier-free guidance and a top-k mask taken
1370            // from the CONDITIONED logits, per the reference. The graph
1371            // folds the final norm and lm_head into its own submit, so
1372            // when it is driving the logits are already here.
1373            let vocab = self.lm_head_rows();
1374            let mut logits = vec![0f32; 2 * vocab];
1375            if graph {
1376                logits[..vocab].copy_from_slice(&glogits[0][..vocab]);
1377                logits[vocab..].copy_from_slice(&glogits[1][..vocab]);
1378            } else {
1379                self.lm_head.matmat(&normed, 2, &mut logits, pool);
1380            }
1381            let (cond, unc) = logits.split_at(vocab);
1382            let mut thr: Vec<f32> = cond.to_vec();
1383            thr.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap());
1384            let cut = thr[self.top_k.min(vocab) - 1];
1385            let guided: Vec<f32> = (0..vocab)
1386                .map(|i| {
1387                    if cond[i] < cut {
1388                        f32::NEG_INFINITY
1389                    } else {
1390                        unc[i] + (cond[i] - unc[i]) * self.cfg_scale
1391                    }
1392                })
1393                .collect();
1394            let code = sample_topk(&guided, self.top_k, &mut rng);
1395            if code == 0 {
1396                break; // stop token
1397            }
1398            let c0 = code - 1;
1399            let c0_embed = Self::embed_row(&self.embed_audio, c0, hs);
1400            // Depth: the remaining seven codebooks, and their hidden
1401            // states are seven eighths of what the DiT will see.
1402            let mut seq = self.depth.project(&normed[..hs]);
1403            seq.extend_from_slice(&self.depth.project(&c0_embed));
1404            let mut codes = vec![c0];
1405            let mut frame_hidden = normed[..hs].to_vec();
1406            for level in 1..self.codebooks {
1407                let n = seq.len() / hs;
1408                let h = self.depth.forward_last(&seq, n);
1409                frame_hidden.extend_from_slice(&h);
1410                let lg = self.depth.head(level, &h);
1411                let c = sample_topk(&lg, self.top_k, &mut rng);
1412                codes.push(c);
1413                if level < self.codebooks - 1 {
1414                    let e = Self::embed_row(&self.embed_extra, c + (level - 1) * self.audio_vocab, hs);
1415                    seq.extend_from_slice(&self.depth.project(&e));
1416                }
1417            }
1418            out.extend_from_slice(&frame_hidden);
1419            done += 1;
1420            progress(done, want);
1421            if done >= want {
1422                break;
1423            }
1424            // Feed the whole frame back: c0's embedding plus the extras,
1425            // scaled by 1/sqrt(codebooks).
1426            let mut fb = Self::embed_row(&self.embed_audio, codes[0], hs);
1427            for (level, &c) in codes.iter().enumerate().skip(1) {
1428                let e = Self::embed_row(&self.embed_extra, c + (level - 1) * self.audio_vocab, hs);
1429                for (a, b) in fb.iter_mut().zip(&e) {
1430                    *a += b;
1431                }
1432            }
1433            for a in fb.iter_mut() {
1434                *a *= scale;
1435            }
1436            let mut x = vec![0f32; 2 * hs];
1437            x[..hs].copy_from_slice(&fb);
1438            x[hs..].copy_from_slice(&fb);
1439            let pos = prompt_ids.len() + frame;
1440            if !graph || !self.graph_step(&mut x, pos, cap, &mut cache, &mut glogits) {
1441                if graph {
1442                    return Err("the token graph refused mid-song".into());
1443                }
1444                self.step_blocks(&mut x, 2, pos, &mut cache);
1445            }
1446            last = x;
1447        }
1448        if done == 0 {
1449            return Err("MiniMax-Music-3 generated zero audio frames".into());
1450        }
1451        Ok((out, done))
1452    }
1453
1454    fn lm_head_rows(&self) -> usize {
1455        match &self.lm_head {
1456            Proj::F32 { rows, .. } => *rows,
1457            Proj::Q(q) => q.rows(),
1458        }
1459    }
1460}
1461
1462/// Split-half rotation over one head, angle `pos·inv_freq[i]`.
1463fn rope_half(x: &mut [f32], pos: usize, inv_freq: &[f32]) {
1464    let half = x.len() / 2;
1465    for i in 0..half {
1466        let (s, c) = (pos as f32 * inv_freq[i]).sin_cos();
1467        let (a, b) = (x[i], x[i + half]);
1468        x[i] = a * c - b * s;
1469        x[i + half] = b * c + a * s;
1470    }
1471}
1472
1473#[cfg(test)]
1474mod tests {
1475    use super::*;
1476
1477    /// The half that is finished, end to end: noise → Euler sampler →
1478    /// vocoder → PCM. It does not make music (the conditioning would
1479    /// come from the AR stack), but it proves the three implemented
1480    /// components compose — the sampler's σ walk, the DiT's timestep
1481    /// convention and the vocoder's hop all have to agree for the
1482    /// sample count to land, and the count is fixed by the reference.
1483    #[test]
1484    fn music3_sampler_and_vocoder_compose() {
1485        let Ok(p) = std::env::var("CMF_MUSIC3_DIT") else {
1486            eprintln!("CMF_MUSIC3_DIT unset — skipping Music-3 chain test");
1487            return;
1488        };
1489        let model = Arc::new(CmfModel::open(&p).expect("open pack"));
1490        let dit = Music3Dit::from_cmf(&model).expect("load DiT");
1491        let dav = crate::audiovae::Music3Dav::from_cmf(&model).expect("load DAV");
1492        let n = 6usize;
1493        // A fixed pseudo-noise: the test must not depend on an RNG.
1494        let noise: Vec<f32> = (0..Music3Dit::IN_CH * n)
1495            .map(|i| (((i * 2654435761) % 1000) as f32 / 500.0) - 1.0)
1496            .collect();
1497        let cond = vec![0f32; Music3Dit::COND_CH * n];
1498        let steps = 3;
1499        let mut seen = 0usize;
1500        let latent = dit.sample(&noise, &cond, n, steps, |i, t| {
1501            assert_eq!(t, steps);
1502            seen = i;
1503        });
1504        assert_eq!(seen, steps, "sampler reported every step");
1505        assert_eq!(latent.len(), Music3Dit::IN_CH * n);
1506        assert!(latent.iter().all(|v| v.is_finite()), "latent went non-finite");
1507        let pcm = dav.decode(&latent, n, None);
1508        assert_eq!(
1509            pcm.len(),
1510            n * crate::audiovae::Music3Dav::HOP * 2,
1511            "the chain's sample count is frames x 512 x 2"
1512        );
1513        assert!(pcm.iter().all(|v| v.is_finite() && v.abs() <= 1.0));
1514        let secs = (n * crate::audiovae::Music3Dav::HOP) as f32
1515            / crate::audiovae::Music3Dav::SAMPLE_RATE as f32;
1516        eprintln!(
1517            "music3 chain: {n} frames -> {} stereo samples ({secs:.3} s at 44.1 kHz)",
1518            pcm.len() / 2
1519        );
1520    }
1521
1522    /// The depth decoder must be CAUSAL — that is the only thing its
1523    /// attention mask does, and losing it lets a codebook level see its
1524    /// own answer while the model still samples plausible codes.
1525    /// Appending a position may not change any earlier output.
1526    #[test]
1527    fn music3_rvq_depth_decoder_is_causal() {
1528        let Ok(p) = std::env::var("CMF_MUSIC3_TE") else {
1529            eprintln!("CMF_MUSIC3_TE unset — skipping RVQ decoder test");
1530            return;
1531        };
1532        let model = Arc::new(CmfModel::open(&p).expect("open packed AR stack"));
1533        let dec = RvqDepthDecoder::from_cmf(&model).expect("load RVQ decoder");
1534        assert_eq!(dec.codebooks(), 8, "eight codebooks");
1535        let hs = dec.hidden;
1536        let seq: Vec<f32> = (0..3 * hs).map(|i| 0.05 * ((i as f32) * 0.013).sin()).collect();
1537        let a = dec.forward_last(&seq[..2 * hs], 2);
1538        let b = dec.forward_last(&seq, 3);
1539        assert_eq!(a.len(), hs);
1540        assert!(a.iter().all(|v| v.is_finite()) && b.iter().all(|v| v.is_finite()));
1541        // Position 1's own output is read by forward_last at n=2; adding
1542        // position 2 must leave the stack's view of 0..=1 untouched, so
1543        // re-running with the shorter prefix must agree with itself.
1544        let a2 = dec.forward_last(&seq[..2 * hs], 2);
1545        let d = a.iter().zip(&a2).map(|(x, y)| (x - y).abs()).fold(0f32, f32::max);
1546        assert!(d == 0.0, "not deterministic: {d}");
1547        let logits = dec.head(1, &b);
1548        assert_eq!(logits.len(), 1024, "audio vocab is 1024 per level");
1549        assert!(logits.iter().all(|v| v.is_finite()));
1550        let spread = logits.iter().fold(f32::MIN, |m, v| m.max(*v))
1551            - logits.iter().fold(f32::MAX, |m, v| m.min(*v));
1552        assert!(spread > 1e-3, "head is flat, spread {spread}");
1553        eprintln!("music3 rvq: 8 codebooks, head spread {spread:.3}");
1554    }
1555
1556    /// Forward the packed DiT and check what the reference fixes: a
1557    /// `[128, n]` velocity, finite, and responsive to BOTH inputs.
1558    /// `CMF_MUSIC3_DIT=<file.cmf>` points at a pack.
1559    ///
1560    /// The two response checks are the point. A forward that silently
1561    /// drops the condition — the easiest way to get the 2304-wide concat
1562    /// wrong — still returns a plausible velocity, and so does one that
1563    /// ignores the timestep token.
1564    #[test]
1565    fn music3_dit_forward_has_the_reference_geometry() {
1566        let Ok(p) = std::env::var("CMF_MUSIC3_DIT") else {
1567            eprintln!("CMF_MUSIC3_DIT unset — skipping Music-3 DiT test");
1568            return;
1569        };
1570        let model = Arc::new(CmfModel::open(&p).expect("open packed DiT"));
1571        let dit = Music3Dit::from_cmf(&model).expect("load DiT");
1572        let n = 4usize;
1573        let x: Vec<f32> = (0..Music3Dit::IN_CH * n)
1574            .map(|i| 0.3 * ((i as f32) * 0.017).sin())
1575            .collect();
1576        let cond: Vec<f32> = (0..Music3Dit::COND_CH * n)
1577            .map(|i| 0.2 * ((i as f32) * 0.011).cos())
1578            .collect();
1579        let v = dit.forward(&x, &cond, n, 0.7);
1580        assert_eq!(v.len(), Music3Dit::IN_CH * n, "velocity is [128, n]");
1581        assert!(v.iter().all(|q| q.is_finite()), "non-finite velocity");
1582        let rms = (v.iter().map(|q| q * q).sum::<f32>() / v.len() as f32).sqrt();
1583        assert!(rms > 1e-6, "velocity is silent, rms {rms}");
1584
1585        let zero_cond = vec![0f32; Music3Dit::COND_CH * n];
1586        let v0 = dit.forward(&x, &zero_cond, n, 0.7);
1587        let dc = v
1588            .iter()
1589            .zip(&v0)
1590            .map(|(a, b)| (a - b).abs())
1591            .fold(0f32, f32::max);
1592        assert!(dc > 1e-5, "condition changed nothing — concat is wrong");
1593
1594        let vt = dit.forward(&x, &cond, n, 0.2);
1595        let dt = v
1596            .iter()
1597            .zip(&vt)
1598            .map(|(a, b)| (a - b).abs())
1599            .fold(0f32, f32::max);
1600        assert!(dt > 1e-5, "timestep changed nothing — the token is lost");
1601        eprintln!("music3 dit: rms {rms:.4}, d/dcond {dc:.4}, d/dt {dt:.4}");
1602    }
1603}
1604
1605/// Where a denoise step's time goes, under `CMF_MUSIC3_PROF=1`. The
1606/// stage timers say "denoise"; three optimizations were argued about
1607/// without anything saying which part of it.
1608pub mod prof {
1609    use std::sync::atomic::{AtomicU64, Ordering};
1610    use std::sync::OnceLock;
1611    use std::time::Instant;
1612
1613    pub static QKV: AtomicU64 = AtomicU64::new(0);
1614    pub static ATTN: AtomicU64 = AtomicU64::new(0);
1615    pub static OUT: AtomicU64 = AtomicU64::new(0);
1616    pub static FFN: AtomicU64 = AtomicU64::new(0);
1617
1618    pub fn on() -> bool {
1619        static ON: OnceLock<bool> = OnceLock::new();
1620        *ON.get_or_init(|| std::env::var("CMF_MUSIC3_PROF").as_deref() == Ok("1"))
1621    }
1622
1623    /// `None` when profiling is off, so an untimed run pays one branch.
1624    pub fn start() -> Option<Instant> {
1625        on().then(Instant::now)
1626    }
1627
1628    pub fn add(c: &AtomicU64, t: Option<Instant>) {
1629        if let Some(t) = t {
1630            c.fetch_add(t.elapsed().as_nanos() as u64, Ordering::Relaxed);
1631        }
1632    }
1633
1634    pub fn report() -> String {
1635        let g = |c: &AtomicU64| c.load(Ordering::Relaxed) as f64 / 1e9;
1636        format!(
1637            "qkv {:.1}s, attention {:.1}s, out {:.1}s, ffn {:.1}s",
1638            g(&QKV),
1639            g(&ATTN),
1640            g(&OUT),
1641            g(&FFN)
1642        )
1643    }
1644}