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::pool::Pool;
24use cortiq_core::CmfModel;
25use std::sync::Arc;
26
27fn silu(v: f32) -> f32 {
28    v / (1.0 + (-v).exp())
29}
30
31/// LayerNorm with learned scale AND shift — not the RMSNorm the rest of
32/// this engine's transformers use.
33struct LayerNorm {
34    gamma: Vec<f32>,
35    beta: Vec<f32>,
36}
37
38impl LayerNorm {
39    fn load(model: &Arc<CmfModel>, p: &str) -> Result<Self, String> {
40        Ok(Self {
41            gamma: crate::dit::cmf_f32(model, &format!("{p}.gamma"))?,
42            beta: crate::dit::cmf_f32(model, &format!("{p}.beta"))?,
43        })
44    }
45
46    fn apply(&self, x: &mut [f32], d: usize) {
47        for row in x.chunks_exact_mut(d) {
48            let mean = row.iter().map(|v| *v as f64).sum::<f64>() / d as f64;
49            let var = row.iter().map(|v| (*v as f64 - mean).powi(2)).sum::<f64>() / d as f64;
50            let inv = 1.0 / (var + 1e-5).sqrt();
51            for ((v, &g), &b) in row.iter_mut().zip(&self.gamma).zip(&self.beta) {
52                *v = ((*v as f64 - mean) * inv) as f32 * g + b;
53            }
54        }
55    }
56}
57
58struct Block {
59    pre_norm: LayerNorm,
60    qkv: Proj,
61    out: Proj,
62    ff_norm: LayerNorm,
63    ff_in: Proj,
64    ff_in_b: Vec<f32>,
65    ff_out: Proj,
66    ff_out_b: Vec<f32>,
67}
68
69impl Block {
70    fn load(model: &Arc<CmfModel>, p: &str) -> Result<Self, String> {
71        Ok(Self {
72            pre_norm: LayerNorm::load(model, &format!("{p}.pre_norm"))?,
73            qkv: Proj::from_model(model, &format!("{p}.self_attn.to_qkv.weight"))?,
74            out: Proj::from_model(model, &format!("{p}.self_attn.to_out.weight"))?,
75            ff_norm: LayerNorm::load(model, &format!("{p}.ff_norm"))?,
76            ff_in: Proj::from_model(model, &format!("{p}.ff.ff.0.proj.weight"))?,
77            ff_in_b: crate::dit::cmf_f32(model, &format!("{p}.ff.ff.0.proj.bias"))?,
78            ff_out: Proj::from_model(model, &format!("{p}.ff.ff.2.weight"))?,
79            ff_out_b: crate::dit::cmf_f32(model, &format!("{p}.ff.ff.2.bias"))?,
80        })
81    }
82}
83
84pub struct Music3Dit {
85    pre_conv: Vec<f32>,  // [2304, 2304] 1x1
86    post_conv: Vec<f32>, // [128, 128] 1x1
87    fourier: Vec<f32>,   // [128]
88    t0: Proj,
89    t0_b: Vec<f32>,
90    t2: Proj,
91    t2_b: Vec<f32>,
92    project_in: Proj,
93    project_out: Proj,
94    inv_freq: Vec<f32>,
95    blocks: Vec<Block>,
96    pool: Option<Arc<Pool>>,
97    hidden: usize,
98    heads: usize,
99    hd: usize,
100    rot: usize,
101    inter: usize,
102    /// The learned mix over the AR stack's eight codebook levels, and
103    /// the 3-tap conv that turns the mixture into the DiT's condition.
104    cond_logits: Vec<f32>,
105    cond_scale: f32,
106    lc_w: Vec<f32>,
107    lc_b: Vec<f32>,
108}
109
110impl Music3Dit {
111    pub const IN_CH: usize = 128;
112    pub const COND_CH: usize = 2048;
113    pub const CONCAT_CH: usize = 2304;
114
115    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
116        let cfg: serde_json::Value =
117            serde_json::from_slice(model.tensor_bytes("mdit.config_json").map_err(|e| e.to_string())?)
118                .map_err(|e| format!("mdit.config_json: {e}"))?;
119        let u = |k: &str, d: usize| cfg[k].as_u64().map(|v| v as usize).unwrap_or(d);
120        let nl = u("num_layers", 36);
121        let dt = "mdit.diffusion_transformer";
122        Ok(Self {
123            pre_conv: crate::dit::cmf_f32(model, &format!("{dt}.preprocess_conv.weight"))?,
124            post_conv: crate::dit::cmf_f32(model, &format!("{dt}.postprocess_conv.weight"))?,
125            fourier: crate::dit::cmf_f32(model, &format!("{dt}.timestep_features.weight"))?,
126            t0: Proj::from_model(model, &format!("{dt}.to_timestep_embed.0.weight"))?,
127            t0_b: crate::dit::cmf_f32(model, &format!("{dt}.to_timestep_embed.0.bias"))?,
128            t2: Proj::from_model(model, &format!("{dt}.to_timestep_embed.2.weight"))?,
129            t2_b: crate::dit::cmf_f32(model, &format!("{dt}.to_timestep_embed.2.bias"))?,
130            project_in: Proj::from_model(model, &format!("{dt}.transformer.project_in.weight"))?,
131            project_out: Proj::from_model(model, &format!("{dt}.transformer.project_out.weight"))?,
132            inv_freq: crate::dit::cmf_f32(model, &format!("{dt}.transformer.rotary_pos_emb.inv_freq"))?,
133            blocks: (0..nl)
134                .map(|i| Block::load(model, &format!("{dt}.transformer.layers.{i}")))
135                .collect::<Result<_, _>>()?,
136            pool: Pool::from_env(),
137            hidden: u("hidden", 2048),
138            heads: u("num_heads", 32),
139            hd: u("head_dim", 64),
140            rot: u("rotary_dim", 32),
141            inter: u("ff_inner", 8192),
142            cond_logits: crate::dit::cmf_f32(model, "mdit.cond_layer_logits")?,
143            cond_scale: crate::dit::cmf_f32(model, "mdit.cond_layer_scale")?[0],
144            lc_w: crate::dit::cmf_f32(model, "mdit.latent_conditioners.0.weight")?,
145            lc_b: crate::dit::cmf_f32(model, "mdit.latent_conditioners.0.bias")?,
146        })
147    }
148
149    /// Latent frames for `audio_frames` of AR output — the reference's
150    /// `latent_length`: 44100/24000 · 960/512 = 3.4453125 per frame.
151    pub fn latent_length(audio_frames: usize) -> usize {
152        ((audio_frames as f64 * 44100.0 / 24000.0 * 960.0 / 512.0) as usize).max(1)
153    }
154
155    /// AR hidden `[frames, 8·4096]` → the DiT's condition `[2048, L]`.
156    ///
157    /// The eight are RVQ CODEBOOK levels, softmax-mixed by
158    /// `cond_layer_logits`, scaled, passed through one 3-tap conv and
159    /// then NEAREST-resampled to the latent rate. Nearest, not linear:
160    /// the reference interpolates that way and a smoother resample
161    /// smears the onset of every note.
162    pub fn aligned_condition(&self, hidden: &[f32], frames: usize) -> (Vec<f32>, usize) {
163        let levels = self.cond_logits.len();
164        let ar = hidden.len() / (frames * levels);
165        let mx = self.cond_logits.iter().cloned().fold(f32::MIN, f32::max);
166        let ex: Vec<f32> = self.cond_logits.iter().map(|v| (v - mx).exp()).collect();
167        let sum: f32 = ex.iter().sum();
168        // [ar, frames], channel-major, mixed and scaled.
169        let mut mixed = vec![0f32; ar * frames];
170        for t in 0..frames {
171            for l in 0..levels {
172                let w = ex[l] / sum * self.cond_scale;
173                let src = &hidden[(t * levels + l) * ar..(t * levels + l + 1) * ar];
174                for (c, &v) in src.iter().enumerate() {
175                    mixed[c * frames + t] += w * v;
176                }
177            }
178        }
179        // Conv1d(ar -> 2048, k=3, pad=1).
180        let out_ch = self.lc_b.len();
181        let mut conv = vec![0f32; out_ch * frames];
182        for o in 0..out_ch {
183            let dst = &mut conv[o * frames..(o + 1) * frames];
184            dst.fill(self.lc_b[o]);
185            for i in 0..ar {
186                let k = &self.lc_w[(o * ar + i) * 3..(o * ar + i + 1) * 3];
187                let src = &mixed[i * frames..(i + 1) * frames];
188                for t in 0..frames {
189                    for (j, &kv) in k.iter().enumerate() {
190                        let p = t as isize + j as isize - 1;
191                        if p >= 0 && (p as usize) < frames {
192                            dst[t] += kv * src[p as usize];
193                        }
194                    }
195                }
196            }
197        }
198        let l = Self::latent_length(frames);
199        let mut out = vec![0f32; out_ch * l];
200        for o in 0..out_ch {
201            for t in 0..l {
202                let s = (t * frames) / l.max(1);
203                out[o * l + t] = conv[o * frames + s.min(frames - 1)];
204            }
205        }
206        (out, l)
207    }
208
209    /// `[out, in]` 1×1 conv over the channel axis of a `[in, n]` panel,
210    /// added back to its input — both convs in this model are residual.
211    fn conv1x1_residual(w: &[f32], x: &mut [f32], ch: usize, n: usize) {
212        let mut acc = vec![0f32; ch * n];
213        for o in 0..ch {
214            let row = &w[o * ch..(o + 1) * ch];
215            let dst = &mut acc[o * n..(o + 1) * n];
216            for (i, &wv) in row.iter().enumerate() {
217                if wv == 0.0 {
218                    continue;
219                }
220                for (d, &s) in dst.iter_mut().zip(&x[i * n..(i + 1) * n]) {
221                    *d += wv * s;
222                }
223            }
224        }
225        for (d, a) in x.iter_mut().zip(&acc) {
226            *d += a;
227        }
228    }
229
230    /// `cat(cos, sin)` of `2π·t·w`, then the two-layer SiLU head.
231    fn timestep_embedding(&self, t: f32) -> Vec<f32> {
232        let half = self.fourier.len();
233        let mut feats = vec![0f32; half * 2];
234        for (i, &w) in self.fourier.iter().enumerate() {
235            let a = std::f32::consts::TAU * t * w;
236            feats[i] = a.cos();
237            feats[half + i] = a.sin();
238        }
239        let mut h = vec![0f32; self.t0_b.len()];
240        self.t0.matmat(&feats, 1, &mut h, self.pool.as_deref());
241        for (v, &b) in h.iter_mut().zip(&self.t0_b) {
242            *v = silu(*v + b);
243        }
244        let mut out = vec![0f32; self.t2_b.len()];
245        self.t2.matmat(&h, 1, &mut out, self.pool.as_deref());
246        for (v, &b) in out.iter_mut().zip(&self.t2_b) {
247            *v += b;
248        }
249        out
250    }
251
252    /// Split-half RoPE over the first `rot` dims of every head.
253    fn rope(&self, q: &mut [f32], k: &mut [f32], n: usize) {
254        let half = self.rot / 2;
255        for p in 0..n {
256            for h in 0..self.heads {
257                let off = p * self.heads * self.hd + h * self.hd;
258                for i in 0..half {
259                    let (s, c) = (p as f32 * self.inv_freq[i]).sin_cos();
260                    for x in [&mut *q, &mut *k] {
261                        let (a, b) = (x[off + i], x[off + i + half]);
262                        x[off + i] = a * c - b * s;
263                        x[off + i + half] = a * s + b * c;
264                    }
265                }
266            }
267        }
268    }
269
270    /// One block over `[n, hidden]`, in place.
271    fn block(&self, blk: &Block, x: &mut [f32], n: usize) {
272        let (hs, nh, hd) = (self.hidden, self.heads, self.hd);
273        let pool = self.pool.as_deref();
274        let mut h = x.to_vec();
275        blk.pre_norm.apply(&mut h, hs);
276        let mut qkv = vec![0f32; n * 3 * hs];
277        blk.qkv.matmat(&h, n, &mut qkv, pool);
278        // to_qkv emits q|k|v concatenated along the FEATURE axis, so the
279        // three live at column offsets, not row offsets.
280        let mut q = vec![0f32; n * hs];
281        let mut k = vec![0f32; n * hs];
282        let mut v = vec![0f32; n * hs];
283        for p in 0..n {
284            let s = &qkv[p * 3 * hs..(p + 1) * 3 * hs];
285            q[p * hs..(p + 1) * hs].copy_from_slice(&s[..hs]);
286            k[p * hs..(p + 1) * hs].copy_from_slice(&s[hs..2 * hs]);
287            v[p * hs..(p + 1) * hs].copy_from_slice(&s[2 * hs..]);
288        }
289        self.rope(&mut q, &mut k, n);
290        let scale = 1.0 / (hd as f32).sqrt();
291        let mut attn = vec![0f32; n * hs];
292        for hh in 0..nh {
293            for i in 0..n {
294                let qi = &q[i * hs + hh * hd..i * hs + hh * hd + hd];
295                let mut scores = vec![0f32; n];
296                let mut mx = f32::NEG_INFINITY;
297                for (j, sc) in scores.iter_mut().enumerate() {
298                    let kj = &k[j * hs + hh * hd..j * hs + hh * hd + hd];
299                    *sc = qi.iter().zip(kj).map(|(a, b)| a * b).sum::<f32>() * scale;
300                    mx = mx.max(*sc);
301                }
302                let mut sum = 0.0;
303                for sc in scores.iter_mut() {
304                    *sc = (*sc - mx).exp();
305                    sum += *sc;
306                }
307                let inv = 1.0 / sum;
308                let dst = &mut attn[i * hs + hh * hd..i * hs + hh * hd + hd];
309                for (j, &sc) in scores.iter().enumerate() {
310                    let w = sc * inv;
311                    let vj = &v[j * hs + hh * hd..j * hs + hh * hd + hd];
312                    for (d, &vv) in dst.iter_mut().zip(vj) {
313                        *d += w * vv;
314                    }
315                }
316            }
317        }
318        let mut proj = vec![0f32; n * hs];
319        blk.out.matmat(&attn, n, &mut proj, pool);
320        for (a, b) in x.iter_mut().zip(&proj) {
321            *a += b;
322        }
323
324        let mut h = x.to_vec();
325        blk.ff_norm.apply(&mut h, hs);
326        let mut gu = vec![0f32; n * 2 * self.inter];
327        blk.ff_in.matmat(&h, n, &mut gu, pool);
328        // GLU here is `value * silu(gate)` with VALUE first, and the
329        // projection's bias belongs to BOTH halves before they meet.
330        // Swapping the halves still makes sound, which is why this is
331        // spelled out rather than inferred.
332        let inter = self.inter;
333        let (vb, gb) = blk.ff_in_b.split_at(inter);
334        let mut act = vec![0f32; n * inter];
335        for p in 0..n {
336            let row = &gu[p * 2 * inter..(p + 1) * 2 * inter];
337            let (val, gate) = row.split_at(inter);
338            let dst = &mut act[p * inter..(p + 1) * inter];
339            for i in 0..inter {
340                dst[i] = (val[i] + vb[i]) * silu(gate[i] + gb[i]);
341            }
342        }
343        let mut ffo = vec![0f32; n * hs];
344        blk.ff_out.matmat(&act, n, &mut ffo, pool);
345        for p in 0..n {
346            for j in 0..hs {
347                x[p * hs + j] += ffo[p * hs + j] + blk.ff_out_b[j];
348            }
349        }
350    }
351
352    /// Latent frames the transformer will attend across in one go, and
353    /// the stride it advances by — `latent_length(200)` and
354    /// `latent_length(100)` in the reference. Attention is quadratic, so
355    /// a whole song in one pass is not merely slow, it is not what the
356    /// model was run as.
357    pub const WINDOW: usize = 689;
358    pub const HOP: usize = 344;
359
360    /// The velocity over any length, windowed like the reference:
361    /// overlapping passes averaged by how many covered each frame.
362    pub fn forward_windowed(&self, x: &[f32], condition: &[f32], n: usize, t: f32) -> Vec<f32> {
363        if n <= Self::WINDOW {
364            return self.forward(x, condition, n, t);
365        }
366        let ch = Self::IN_CH;
367        let cc = Self::COND_CH;
368        let mut out = vec![0f32; ch * n];
369        let mut count = vec![0f32; n];
370        let mut start = 0usize;
371        loop {
372            let end = (start + Self::WINDOW).min(n);
373            let w = end - start;
374            let mut xw = vec![0f32; ch * w];
375            for c in 0..ch {
376                xw[c * w..(c + 1) * w].copy_from_slice(&x[c * n + start..c * n + end]);
377            }
378            let mut cw = vec![0f32; cc * w];
379            for c in 0..cc {
380                cw[c * w..(c + 1) * w].copy_from_slice(&condition[c * n + start..c * n + end]);
381            }
382            let v = self.forward(&xw, &cw, w, t);
383            for c in 0..ch {
384                for i in 0..w {
385                    out[c * n + start + i] += v[c * w + i];
386                }
387            }
388            for i in 0..w {
389                count[start + i] += 1.0;
390            }
391            if end == n {
392                break;
393            }
394            start += Self::HOP;
395        }
396        for c in 0..ch {
397            for i in 0..n {
398                out[c * n + i] /= count[i];
399            }
400        }
401        out
402    }
403
404    /// `x` is `[128, n]` and `condition` `[2048, n]`; the result is the
405    /// velocity at `[128, n]`.
406    pub fn forward(&self, x: &[f32], condition: &[f32], n: usize, t: f32) -> Vec<f32> {
407        let pool = self.pool.as_deref();
408        let mut full = vec![0f32; Self::CONCAT_CH * n];
409        full[..Self::IN_CH * n].copy_from_slice(x);
410        // rows 128..256 stay zero: the reference's `zeros_like(x)` plane
411        full[2 * Self::IN_CH * n..].copy_from_slice(condition);
412        Self::conv1x1_residual(&self.pre_conv, &mut full, Self::CONCAT_CH, n);
413
414        // channel-major -> token-major for the transformer
415        let mut toks = vec![0f32; n * Self::CONCAT_CH];
416        for c in 0..Self::CONCAT_CH {
417            for p in 0..n {
418                toks[p * Self::CONCAT_CH + c] = full[c * n + p];
419            }
420        }
421        let mut h = vec![0f32; n * self.hidden];
422        self.project_in.matmat(&toks, n, &mut h, pool);
423
424        // The timestep rides as token 0 and shifts every rotary position.
425        let temb = self.timestep_embedding(t);
426        let mut seq = vec![0f32; (n + 1) * self.hidden];
427        seq[..self.hidden].copy_from_slice(&temb);
428        seq[self.hidden..].copy_from_slice(&h);
429        for blk in &self.blocks {
430            self.block(blk, &mut seq, n + 1);
431        }
432
433        let mut out = vec![0f32; n * Self::IN_CH];
434        self.project_out
435            .matmat(&seq[self.hidden..], n, &mut out, pool);
436        let mut ch = vec![0f32; Self::IN_CH * n];
437        for c in 0..Self::IN_CH {
438            for p in 0..n {
439                ch[c * n + p] = out[p * Self::IN_CH + c];
440            }
441        }
442        Self::conv1x1_residual(&self.post_conv, &mut ch, Self::IN_CH, n);
443        for v in ch.iter_mut() {
444            *v = -*v;
445        }
446        ch
447    }
448
449    /// Denoise `[128, n]` from noise to a latent, `steps` Euler steps
450    /// along σ: 1 → 0. `progress` is called with (step, total).
451    pub fn sample(
452        &self,
453        noise: &[f32],
454        condition: &[f32],
455        n: usize,
456        steps: usize,
457        mut progress: impl FnMut(usize, usize),
458    ) -> Vec<f32> {
459        let sigmas = flow_sigmas(steps);
460        let mut x = noise.to_vec();
461        for i in 0..steps {
462            let (s, s_next) = (sigmas[i], sigmas[i + 1]);
463            // ComfyUI's process_timestep for this model.
464            let v = self.forward_windowed(&x, condition, n, 1.0 - s);
465            let dt = s_next - s;
466            for (a, b) in x.iter_mut().zip(&v) {
467                *a += dt * b;
468            }
469            progress(i + 1, steps);
470        }
471        x
472    }
473}
474
475/// Euler flow-matching sampler for Music-3.
476///
477/// ComfyUI registers this model as a plain `ModelType.FLOW` with
478/// `multiplier: 1.0` and `process_timestep(t) = 1.0 - t`, so the sampler
479/// is the ordinary one and NOT the `FlowMatchEulerDiscreteScheduler`
480/// named in MiniMax's own `scheduler_config.json` — that belongs to
481/// their diffusers pipeline. Worth stating because the config is the
482/// first thing you find and it sends you somewhere else: with
483/// `num_train_timesteps: 1` its schedule degenerates to a constant,
484/// which is the tell that the caller supplies the sigmas.
485///
486/// σ walks 1 → 0, the DiT is asked at `1 − σ`, and the step is
487/// `x += (σ_next − σ)·v`. The DiT already negates its own output, so
488/// the sign lives there rather than here.
489pub fn flow_sigmas(steps: usize) -> Vec<f32> {
490    (0..=steps)
491        .map(|i| 1.0 - i as f32 / steps as f32)
492        .collect()
493}
494
495/// RMSNorm with a weight and no bias, eps 1e-6.
496struct RmsNorm {
497    w: Vec<f32>,
498}
499
500impl RmsNorm {
501    fn load(model: &Arc<CmfModel>, n: &str) -> Result<Self, String> {
502        Ok(Self {
503            w: crate::dit::cmf_f32(model, n)?,
504        })
505    }
506
507    fn apply(&self, x: &mut [f32], d: usize) {
508        for row in x.chunks_exact_mut(d) {
509            let ss = row.iter().map(|v| (*v as f64) * (*v as f64)).sum::<f64>() / d as f64;
510            let inv = 1.0 / (ss + 1e-6).sqrt();
511            for (v, &g) in row.iter_mut().zip(&self.w) {
512                *v = (*v as f64 * inv) as f32 * g;
513            }
514        }
515    }
516}
517
518struct RvqBlock {
519    n1: RmsNorm,
520    q: Proj,
521    k: Proj,
522    v: Proj,
523    o: Proj,
524    n2: RmsNorm,
525    gate: Proj,
526    up: Proj,
527    down: Proj,
528}
529
530impl RvqBlock {
531    fn load(model: &Arc<CmfModel>, p: &str) -> Result<Self, String> {
532        Ok(Self {
533            n1: RmsNorm::load(model, &format!("{p}.input_layernorm.weight"))?,
534            q: Proj::from_model(model, &format!("{p}.self_attn.q_proj.weight"))?,
535            k: Proj::from_model(model, &format!("{p}.self_attn.k_proj.weight"))?,
536            v: Proj::from_model(model, &format!("{p}.self_attn.v_proj.weight"))?,
537            o: Proj::from_model(model, &format!("{p}.self_attn.o_proj.weight"))?,
538            n2: RmsNorm::load(model, &format!("{p}.post_attention_layernorm.weight"))?,
539            gate: Proj::from_model(model, &format!("{p}.mlp.gate_proj.weight"))?,
540            up: Proj::from_model(model, &format!("{p}.mlp.up_proj.weight"))?,
541            down: Proj::from_model(model, &format!("{p}.mlp.down_proj.weight"))?,
542        })
543    }
544}
545
546/// The RVQ depth decoder: given the frame's hidden state and the codes
547/// chosen so far, it predicts the next codebook level.
548///
549/// It is a small CAUSAL transformer over a sequence that never exceeds
550/// the codebook count — the positional table is 16 rows for 8 levels —
551/// with no rotary embedding at all. Attention that forgets the mask here
552/// leaks a level's own answer backwards and the model still samples.
553pub struct RvqDepthDecoder {
554    projection: Proj,
555    pos: Vec<f32>,
556    blocks: Vec<RvqBlock>,
557    norm: RmsNorm,
558    heads: Vec<Proj>,
559    pool: Option<Arc<Pool>>,
560    hidden: usize,
561    nh: usize,
562    hd: usize,
563    inter: usize,
564}
565
566impl RvqDepthDecoder {
567    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
568        let cfg: serde_json::Value =
569            serde_json::from_slice(model.tensor_bytes("mte.config_json").map_err(|e| e.to_string())?)
570                .map_err(|e| format!("mte.config_json: {e}"))?;
571        let u = |k: &str, d: usize| cfg[k].as_u64().map(|v| v as usize).unwrap_or(d);
572        let hidden = u("hidden_size", 4096);
573        let nl = u("decoder_num_layers", 4);
574        let nh = u("decoder_num_heads", 16);
575        let cb = u("audio_num_codebooks", 8);
576        Ok(Self {
577            projection: Proj::from_model(model, "mte.audio_decoder.projection.weight")?,
578            pos: crate::dit::cmf_f32(model, "mte.audio_decoder.pos_embedding.weight")?,
579            blocks: (0..nl)
580                .map(|i| RvqBlock::load(model, &format!("mte.audio_decoder.layers.{i}")))
581                .collect::<Result<_, _>>()?,
582            norm: RmsNorm::load(model, "mte.audio_decoder.norm.weight")?,
583            heads: (0..cb - 1)
584                .map(|i| {
585                    Proj::from_model(model, &format!("mte.audio_decoder.audio_heads.{i}.weight"))
586                })
587                .collect::<Result<_, _>>()?,
588            pool: Pool::from_env(),
589            hidden,
590            nh,
591            hd: hidden / nh,
592            inter: u("decoder_intermediate_size", 6144),
593        })
594    }
595
596    pub fn codebooks(&self) -> usize {
597        self.heads.len() + 1
598    }
599
600    /// `projection` is applied to every element entering the sequence —
601    /// the frame hidden, the c0 embedding and each extra embedding.
602    pub fn project(&self, x: &[f32]) -> Vec<f32> {
603        let n = x.len() / self.hidden;
604        let mut out = vec![0f32; n * self.hidden];
605        self.projection
606            .matmat(x, n, &mut out, self.pool.as_deref());
607        out
608    }
609
610    /// Run the stack over `[n, hidden]` and return the LAST position's
611    /// normed hidden — the only one the caller reads.
612    pub fn forward_last(&self, seq: &[f32], n: usize) -> Vec<f32> {
613        let (hs, nh, hd) = (self.hidden, self.nh, self.hd);
614        let pool = self.pool.as_deref();
615        let mut x = seq.to_vec();
616        for p in 0..n {
617            for (v, &pe) in x[p * hs..(p + 1) * hs].iter_mut().zip(&self.pos[p * hs..(p + 1) * hs]) {
618                *v += pe;
619            }
620        }
621        for blk in &self.blocks {
622            let mut h = x.clone();
623            blk.n1.apply(&mut h, hs);
624            let (mut q, mut k, mut v) = (vec![0f32; n * hs], vec![0f32; n * hs], vec![0f32; n * hs]);
625            blk.q.matmat(&h, n, &mut q, pool);
626            blk.k.matmat(&h, n, &mut k, pool);
627            blk.v.matmat(&h, n, &mut v, pool);
628            let scale = 1.0 / (hd as f32).sqrt();
629            let mut attn = vec![0f32; n * hs];
630            for hh in 0..nh {
631                for i in 0..n {
632                    let qi = &q[i * hs + hh * hd..i * hs + hh * hd + hd];
633                    // Causal: position i sees 0..=i and nothing after.
634                    let mut sc = vec![0f32; i + 1];
635                    let mut mx = f32::NEG_INFINITY;
636                    for (j, s) in sc.iter_mut().enumerate() {
637                        let kj = &k[j * hs + hh * hd..j * hs + hh * hd + hd];
638                        *s = qi.iter().zip(kj).map(|(a, b)| a * b).sum::<f32>() * scale;
639                        mx = mx.max(*s);
640                    }
641                    let mut sum = 0.0;
642                    for s in sc.iter_mut() {
643                        *s = (*s - mx).exp();
644                        sum += *s;
645                    }
646                    let inv = 1.0 / sum;
647                    let dst = &mut attn[i * hs + hh * hd..i * hs + hh * hd + hd];
648                    for (j, &s) in sc.iter().enumerate() {
649                        let w = s * inv;
650                        let vj = &v[j * hs + hh * hd..j * hs + hh * hd + hd];
651                        for (d, &vv) in dst.iter_mut().zip(vj) {
652                            *d += w * vv;
653                        }
654                    }
655                }
656            }
657            let mut proj = vec![0f32; n * hs];
658            blk.o.matmat(&attn, n, &mut proj, pool);
659            for (a, b) in x.iter_mut().zip(&proj) {
660                *a += b;
661            }
662
663            let mut h = x.clone();
664            blk.n2.apply(&mut h, hs);
665            let (mut g, mut u) = (vec![0f32; n * self.inter], vec![0f32; n * self.inter]);
666            blk.gate.matmat(&h, n, &mut g, pool);
667            blk.up.matmat(&h, n, &mut u, pool);
668            for (a, b) in g.iter_mut().zip(&u) {
669                *a = silu(*a) * b;
670            }
671            let mut ffo = vec![0f32; n * hs];
672            blk.down.matmat(&g, n, &mut ffo, pool);
673            for (a, b) in x.iter_mut().zip(&ffo) {
674                *a += b;
675            }
676        }
677        let mut last = x[(n - 1) * hs..].to_vec();
678        self.norm.apply(&mut last, hs);
679        last
680    }
681
682    /// Logits for codebook `level` (1-based; level 1 uses head 0).
683    pub fn head(&self, level: usize, hidden: &[f32]) -> Vec<f32> {
684        let h = &self.heads[level - 1];
685        let rows = match h {
686            Proj::F32 { rows, .. } => *rows,
687            Proj::Q(q) => q.rows(),
688        };
689        let mut out = vec![0f32; rows];
690        h.matmat(hidden, 1, &mut out, self.pool.as_deref());
691        out
692    }
693}
694
695// ── the autoregressive stack ────────────────────────────────────────
696
697/// One Qwen3 block: RMSNorm → GQA attention with per-head q/k norms and
698/// split-half RoPE → residual → RMSNorm → SwiGLU → residual.
699struct ArBlock {
700    n1: RmsNorm,
701    q: Proj,
702    k: Proj,
703    v: Proj,
704    o: Proj,
705    qn: RmsNorm,
706    kn: RmsNorm,
707    n2: RmsNorm,
708    gate: Proj,
709    up: Proj,
710    down: Proj,
711}
712
713impl ArBlock {
714    fn load(model: &Arc<CmfModel>, p: &str) -> Result<Self, String> {
715        Ok(Self {
716            n1: RmsNorm::load(model, &format!("{p}.input_layernorm.weight"))?,
717            q: Proj::from_model(model, &format!("{p}.self_attn.q_proj.weight"))?,
718            k: Proj::from_model(model, &format!("{p}.self_attn.k_proj.weight"))?,
719            v: Proj::from_model(model, &format!("{p}.self_attn.v_proj.weight"))?,
720            o: Proj::from_model(model, &format!("{p}.self_attn.o_proj.weight"))?,
721            qn: RmsNorm::load(model, &format!("{p}.self_attn.q_norm.weight"))?,
722            kn: RmsNorm::load(model, &format!("{p}.self_attn.k_norm.weight"))?,
723            n2: RmsNorm::load(model, &format!("{p}.post_attention_layernorm.weight"))?,
724            gate: Proj::from_model(model, &format!("{p}.mlp.gate_proj.weight"))?,
725            up: Proj::from_model(model, &format!("{p}.mlp.up_proj.weight"))?,
726            down: Proj::from_model(model, &format!("{p}.mlp.down_proj.weight"))?,
727        })
728    }
729}
730
731/// Keys and values for one layer, one CFG branch: `[pos][nkv·hd]`.
732#[derive(Default, Clone)]
733struct KvRun {
734    k: Vec<f32>,
735    v: Vec<f32>,
736    len: usize,
737}
738
739/// Deterministic top-k sampler.
740///
741/// NOT torch's: reproducing `torch.multinomial` under a seeded
742/// `Generator` bit-for-bit is its own project, and nothing downstream
743/// needs the same seed to mean the same song — only that one seed here
744/// always means one song.
745struct Rng(u64);
746
747impl Rng {
748    fn new(seed: u64) -> Self {
749        Self(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1)
750    }
751    fn next_f32(&mut self) -> f32 {
752        let mut x = self.0;
753        x ^= x << 13;
754        x ^= x >> 7;
755        x ^= x << 17;
756        self.0 = x;
757        ((x >> 40) as f32) / (1u32 << 24) as f32
758    }
759}
760
761fn sample_topk(logits: &[f32], top_k: usize, rng: &mut Rng) -> usize {
762    let mut idx: Vec<usize> = (0..logits.len()).filter(|&i| logits[i].is_finite()).collect();
763    idx.sort_unstable_by(|&a, &b| logits[b].partial_cmp(&logits[a]).unwrap());
764    idx.truncate(top_k.max(1));
765    let mx = idx.iter().map(|&i| logits[i]).fold(f32::MIN, f32::max);
766    let exps: Vec<f32> = idx.iter().map(|&i| (logits[i] - mx).exp()).collect();
767    let sum: f32 = exps.iter().sum();
768    let mut r = rng.next_f32() * sum;
769    for (j, &e) in exps.iter().enumerate() {
770        r -= e;
771        if r <= 0.0 {
772            return idx[j];
773        }
774    }
775    *idx.last().unwrap()
776}
777
778/// MiniMax-Music-3's AR stack: it does not encode a prompt, it GENERATES
779/// the conditioning — audio tokens sampled frame by frame, whose hidden
780/// states become what the DiT is conditioned on.
781pub struct Music3Ar {
782    blocks: Vec<ArBlock>,
783    norm: RmsNorm,
784    embed_prefill: QTensor,
785    embed_audio: QTensor,
786    embed_extra: QTensor,
787    lm_head: Proj,
788    pub depth: RvqDepthDecoder,
789    inv_freq: Vec<f32>,
790    pool: Option<Arc<Pool>>,
791    hidden: usize,
792    nh: usize,
793    nkv: usize,
794    hd: usize,
795    inter: usize,
796    audio_vocab: usize,
797    codebooks: usize,
798    pub cfg_scale: f32,
799    pub top_k: usize,
800    pub fps: usize,
801    pub max_frames: usize,
802}
803
804/// Token ids the prompt is built from — `comfy/ldm/minimax_music/prompt.py`.
805pub mod tokens {
806    pub const IM_START: u32 = 151644;
807    pub const IM_END: u32 = 151645;
808    pub const AUDIO_CFG: u32 = 151654;
809    pub const AUDIO_START: u32 = 151669;
810    pub const CAPTION_START: u32 = 151671;
811    pub const CAPTION_END: u32 = 151672;
812    pub const LYRICS_START: u32 = 151673;
813    pub const LYRICS_END: u32 = 151674;
814}
815
816impl Music3Ar {
817    pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
818        let cfg: serde_json::Value =
819            serde_json::from_slice(model.tensor_bytes("mte.config_json").map_err(|e| e.to_string())?)
820                .map_err(|e| format!("mte.config_json: {e}"))?;
821        let u = |k: &str, d: usize| cfg[k].as_u64().map(|v| v as usize).unwrap_or(d);
822        let f = |k: &str, d: f64| cfg[k].as_f64().unwrap_or(d);
823        let hidden = u("hidden_size", 4096);
824        let hd = u("head_dim", 128);
825        let theta = f("rope_theta", 1_000_000.0) as f32;
826        Ok(Self {
827            blocks: (0..u("num_hidden_layers", 36))
828                .map(|i| ArBlock::load(model, &format!("mte.layers.{i}")))
829                .collect::<Result<_, _>>()?,
830            norm: RmsNorm::load(model, "mte.norm.weight")?,
831            embed_prefill: QTensor::from_model(model, "mte.embed_tokens_prefill.weight")?,
832            embed_audio: QTensor::from_model(model, "mte.embed_tokens_audio.weight")?,
833            embed_extra: QTensor::from_model(model, "mte.audio_extra_embedding.weight")?,
834            lm_head: Proj::from_model(model, "mte.lm_head_pruned.weight")?,
835            depth: RvqDepthDecoder::from_cmf(model)?,
836            inv_freq: (0..hd / 2)
837                .map(|i| 1.0 / theta.powf(2.0 * i as f32 / hd as f32))
838                .collect(),
839            pool: Pool::from_env(),
840            hidden,
841            nh: u("num_attention_heads", 32),
842            nkv: u("num_key_value_heads", 8),
843            hd,
844            inter: u("intermediate_size", 12288),
845            audio_vocab: u("audio_vocab_size", 1024),
846            codebooks: u("audio_num_codebooks", 8),
847            cfg_scale: f("cfg_scale", 1.5) as f32,
848            top_k: u("top_k", 50),
849            fps: u("audio_frames_per_second", 25),
850            max_frames: u("max_audio_frames", 9000),
851        })
852    }
853
854    /// One embedding row. A table lookup, not a matmul: these are
855    /// `[vocab, hidden]` and only ever read one row at a time.
856    fn embed_row(p: &QTensor, row: usize, hidden: usize) -> Vec<f32> {
857        let mut out = vec![0f32; hidden];
858        p.row_f32(row, &mut out);
859        out
860    }
861
862    /// Run one position through every block, appending to the cache.
863    /// `x` is `[b, hidden]` for the CFG pair; returns the normed hidden.
864    fn step_blocks(&self, x: &mut [f32], b: usize, pos: usize, cache: &mut [Vec<KvRun>]) {
865        let (hs, nh, nkv, hd) = (self.hidden, self.nh, self.nkv, self.hd);
866        let pool = self.pool.as_deref();
867        let kvw = nkv * hd;
868        for (li, blk) in self.blocks.iter().enumerate() {
869            let mut h = x.to_vec();
870            blk.n1.apply(&mut h, hs);
871            let mut q = vec![0f32; b * nh * hd];
872            let mut k = vec![0f32; b * kvw];
873            let mut v = vec![0f32; b * kvw];
874            blk.q.matmat(&h, b, &mut q, pool);
875            blk.k.matmat(&h, b, &mut k, pool);
876            blk.v.matmat(&h, b, &mut v, pool);
877            // Per-head RMSNorm on q and k BEFORE the rotation, then
878            // split-half RoPE — Qwen3's order, not the other way round.
879            for bi in 0..b {
880                for hh in 0..nh {
881                    let s = bi * nh * hd + hh * hd;
882                    blk.qn.apply(&mut q[s..s + hd], hd);
883                    rope_half(&mut q[s..s + hd], pos, &self.inv_freq);
884                }
885                for hh in 0..nkv {
886                    let s = bi * kvw + hh * hd;
887                    blk.kn.apply(&mut k[s..s + hd], hd);
888                    rope_half(&mut k[s..s + hd], pos, &self.inv_freq);
889                }
890            }
891            let mut attn = vec![0f32; b * nh * hd];
892            let per_kv = nh / nkv;
893            for bi in 0..b {
894                let run = &mut cache[li][bi];
895                run.k.extend_from_slice(&k[bi * kvw..(bi + 1) * kvw]);
896                run.v.extend_from_slice(&v[bi * kvw..(bi + 1) * kvw]);
897                run.len += 1;
898                let n = run.len;
899                let scale = 1.0 / (hd as f32).sqrt();
900                for hh in 0..nh {
901                    let g = hh / per_kv;
902                    let qi = &q[bi * nh * hd + hh * hd..bi * nh * hd + hh * hd + hd];
903                    let mut sc = vec![0f32; n];
904                    let mut mx = f32::NEG_INFINITY;
905                    for (j, s) in sc.iter_mut().enumerate() {
906                        let kj = &run.k[j * kvw + g * hd..j * kvw + g * hd + hd];
907                        *s = qi.iter().zip(kj).map(|(a, c)| a * c).sum::<f32>() * scale;
908                        mx = mx.max(*s);
909                    }
910                    let mut sum = 0.0;
911                    for s in sc.iter_mut() {
912                        *s = (*s - mx).exp();
913                        sum += *s;
914                    }
915                    let inv = 1.0 / sum;
916                    let dst = &mut attn[bi * nh * hd + hh * hd..bi * nh * hd + hh * hd + hd];
917                    for (j, &s) in sc.iter().enumerate() {
918                        let w = s * inv;
919                        let vj = &run.v[j * kvw + g * hd..j * kvw + g * hd + hd];
920                        for (d, &vv) in dst.iter_mut().zip(vj) {
921                            *d += w * vv;
922                        }
923                    }
924                }
925            }
926            let mut proj = vec![0f32; b * hs];
927            blk.o.matmat(&attn, b, &mut proj, pool);
928            for (a, c) in x.iter_mut().zip(&proj) {
929                *a += c;
930            }
931            let mut h = x.to_vec();
932            blk.n2.apply(&mut h, hs);
933            let (mut g, mut u2) = (vec![0f32; b * self.inter], vec![0f32; b * self.inter]);
934            blk.gate.matmat(&h, b, &mut g, pool);
935            blk.up.matmat(&h, b, &mut u2, pool);
936            for (a, c) in g.iter_mut().zip(&u2) {
937                *a = silu(*a) * c;
938            }
939            let mut ffo = vec![0f32; b * hs];
940            blk.down.matmat(&g, b, &mut ffo, pool);
941            for (a, c) in x.iter_mut().zip(&ffo) {
942                *a += c;
943            }
944        }
945    }
946
947    /// Generate `frames` of conditioning: `[frames, 8·hidden]`.
948    ///
949    /// The CFG pair runs as batch 2 — the conditioned prompt and one
950    /// whose middle is replaced by `<|audio_cfg|>` — and every sampled
951    /// code is shared by both branches, which is what makes them stay
952    /// in step.
953    pub fn generate(
954        &self,
955        prompt_ids: &[u32],
956        seed: u64,
957        frames: usize,
958        mut progress: impl FnMut(usize, usize),
959    ) -> Result<(Vec<f32>, usize), String> {
960        let hs = self.hidden;
961        let pool = self.pool.as_deref();
962        let want = frames.min(self.max_frames);
963        let mut cache: Vec<Vec<KvRun>> = (0..self.blocks.len())
964            .map(|_| vec![KvRun::default(); 2])
965            .collect();
966        // The unconditioned branch keeps the frame but not the words.
967        let mut uncond = prompt_ids.to_vec();
968        if uncond.len() > 3 {
969            let n = uncond.len();
970            for t in uncond[1..n - 2].iter_mut() {
971                *t = tokens::AUDIO_CFG;
972            }
973        }
974        let mut last = vec![0f32; 2 * hs];
975        for (pos, (&a, &b)) in prompt_ids.iter().zip(&uncond).enumerate() {
976            let mut x = vec![0f32; 2 * hs];
977            x[..hs].copy_from_slice(&Self::embed_row(&self.embed_prefill, a as usize, hs));
978            x[hs..].copy_from_slice(&Self::embed_row(&self.embed_prefill, b as usize, hs));
979            self.step_blocks(&mut x, 2, pos, &mut cache);
980            last = x;
981            if pos % 64 == 0 {
982                progress(0, want);
983            }
984        }
985        let mut rng = Rng::new(seed);
986        let mut out: Vec<f32> = Vec::with_capacity(want * self.codebooks * hs);
987        let mut done = 0usize;
988        let scale = (self.codebooks as f32).powf(-0.5);
989        for frame in 0..want {
990            let mut normed = last.clone();
991            self.norm.apply(&mut normed, hs);
992            // c0 with classifier-free guidance and a top-k mask taken
993            // from the CONDITIONED logits, per the reference.
994            let mut logits = vec![0f32; 2 * self.lm_head_rows()];
995            self.lm_head.matmat(&normed, 2, &mut logits, pool);
996            let vocab = self.lm_head_rows();
997            let (cond, unc) = logits.split_at(vocab);
998            let mut thr: Vec<f32> = cond.to_vec();
999            thr.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap());
1000            let cut = thr[self.top_k.min(vocab) - 1];
1001            let guided: Vec<f32> = (0..vocab)
1002                .map(|i| {
1003                    if cond[i] < cut {
1004                        f32::NEG_INFINITY
1005                    } else {
1006                        unc[i] + (cond[i] - unc[i]) * self.cfg_scale
1007                    }
1008                })
1009                .collect();
1010            let code = sample_topk(&guided, self.top_k, &mut rng);
1011            if code == 0 {
1012                break; // stop token
1013            }
1014            let c0 = code - 1;
1015            let c0_embed = Self::embed_row(&self.embed_audio, c0, hs);
1016            // Depth: the remaining seven codebooks, and their hidden
1017            // states are seven eighths of what the DiT will see.
1018            let mut seq = self.depth.project(&normed[..hs]);
1019            seq.extend_from_slice(&self.depth.project(&c0_embed));
1020            let mut codes = vec![c0];
1021            let mut frame_hidden = normed[..hs].to_vec();
1022            for level in 1..self.codebooks {
1023                let n = seq.len() / hs;
1024                let h = self.depth.forward_last(&seq, n);
1025                frame_hidden.extend_from_slice(&h);
1026                let lg = self.depth.head(level, &h);
1027                let c = sample_topk(&lg, self.top_k, &mut rng);
1028                codes.push(c);
1029                if level < self.codebooks - 1 {
1030                    let e = Self::embed_row(&self.embed_extra, c + (level - 1) * self.audio_vocab, hs);
1031                    seq.extend_from_slice(&self.depth.project(&e));
1032                }
1033            }
1034            out.extend_from_slice(&frame_hidden);
1035            done += 1;
1036            progress(done, want);
1037            if done >= want {
1038                break;
1039            }
1040            // Feed the whole frame back: c0's embedding plus the extras,
1041            // scaled by 1/sqrt(codebooks).
1042            let mut fb = Self::embed_row(&self.embed_audio, codes[0], hs);
1043            for (level, &c) in codes.iter().enumerate().skip(1) {
1044                let e = Self::embed_row(&self.embed_extra, c + (level - 1) * self.audio_vocab, hs);
1045                for (a, b) in fb.iter_mut().zip(&e) {
1046                    *a += b;
1047                }
1048            }
1049            for a in fb.iter_mut() {
1050                *a *= scale;
1051            }
1052            let mut x = vec![0f32; 2 * hs];
1053            x[..hs].copy_from_slice(&fb);
1054            x[hs..].copy_from_slice(&fb);
1055            self.step_blocks(&mut x, 2, prompt_ids.len() + frame, &mut cache);
1056            last = x;
1057        }
1058        if done == 0 {
1059            return Err("MiniMax-Music-3 generated zero audio frames".into());
1060        }
1061        Ok((out, done))
1062    }
1063
1064    fn lm_head_rows(&self) -> usize {
1065        match &self.lm_head {
1066            Proj::F32 { rows, .. } => *rows,
1067            Proj::Q(q) => q.rows(),
1068        }
1069    }
1070}
1071
1072/// Split-half rotation over one head, angle `pos·inv_freq[i]`.
1073fn rope_half(x: &mut [f32], pos: usize, inv_freq: &[f32]) {
1074    let half = x.len() / 2;
1075    for i in 0..half {
1076        let (s, c) = (pos as f32 * inv_freq[i]).sin_cos();
1077        let (a, b) = (x[i], x[i + half]);
1078        x[i] = a * c - b * s;
1079        x[i + half] = b * c + a * s;
1080    }
1081}
1082
1083#[cfg(test)]
1084mod tests {
1085    use super::*;
1086
1087    /// The half that is finished, end to end: noise → Euler sampler →
1088    /// vocoder → PCM. It does not make music (the conditioning would
1089    /// come from the AR stack), but it proves the three implemented
1090    /// components compose — the sampler's σ walk, the DiT's timestep
1091    /// convention and the vocoder's hop all have to agree for the
1092    /// sample count to land, and the count is fixed by the reference.
1093    #[test]
1094    fn music3_sampler_and_vocoder_compose() {
1095        let Ok(p) = std::env::var("CMF_MUSIC3_DIT") else {
1096            eprintln!("CMF_MUSIC3_DIT unset — skipping Music-3 chain test");
1097            return;
1098        };
1099        let model = Arc::new(CmfModel::open(&p).expect("open pack"));
1100        let dit = Music3Dit::from_cmf(&model).expect("load DiT");
1101        let dav = crate::audiovae::Music3Dav::from_cmf(&model).expect("load DAV");
1102        let n = 6usize;
1103        // A fixed pseudo-noise: the test must not depend on an RNG.
1104        let noise: Vec<f32> = (0..Music3Dit::IN_CH * n)
1105            .map(|i| (((i * 2654435761) % 1000) as f32 / 500.0) - 1.0)
1106            .collect();
1107        let cond = vec![0f32; Music3Dit::COND_CH * n];
1108        let steps = 3;
1109        let mut seen = 0usize;
1110        let latent = dit.sample(&noise, &cond, n, steps, |i, t| {
1111            assert_eq!(t, steps);
1112            seen = i;
1113        });
1114        assert_eq!(seen, steps, "sampler reported every step");
1115        assert_eq!(latent.len(), Music3Dit::IN_CH * n);
1116        assert!(latent.iter().all(|v| v.is_finite()), "latent went non-finite");
1117        let pcm = dav.decode(&latent, n, None);
1118        assert_eq!(
1119            pcm.len(),
1120            n * crate::audiovae::Music3Dav::HOP * 2,
1121            "the chain's sample count is frames x 512 x 2"
1122        );
1123        assert!(pcm.iter().all(|v| v.is_finite() && v.abs() <= 1.0));
1124        let secs = (n * crate::audiovae::Music3Dav::HOP) as f32
1125            / crate::audiovae::Music3Dav::SAMPLE_RATE as f32;
1126        eprintln!(
1127            "music3 chain: {n} frames -> {} stereo samples ({secs:.3} s at 44.1 kHz)",
1128            pcm.len() / 2
1129        );
1130    }
1131
1132    /// The depth decoder must be CAUSAL — that is the only thing its
1133    /// attention mask does, and losing it lets a codebook level see its
1134    /// own answer while the model still samples plausible codes.
1135    /// Appending a position may not change any earlier output.
1136    #[test]
1137    fn music3_rvq_depth_decoder_is_causal() {
1138        let Ok(p) = std::env::var("CMF_MUSIC3_TE") else {
1139            eprintln!("CMF_MUSIC3_TE unset — skipping RVQ decoder test");
1140            return;
1141        };
1142        let model = Arc::new(CmfModel::open(&p).expect("open packed AR stack"));
1143        let dec = RvqDepthDecoder::from_cmf(&model).expect("load RVQ decoder");
1144        assert_eq!(dec.codebooks(), 8, "eight codebooks");
1145        let hs = dec.hidden;
1146        let seq: Vec<f32> = (0..3 * hs).map(|i| 0.05 * ((i as f32) * 0.013).sin()).collect();
1147        let a = dec.forward_last(&seq[..2 * hs], 2);
1148        let b = dec.forward_last(&seq, 3);
1149        assert_eq!(a.len(), hs);
1150        assert!(a.iter().all(|v| v.is_finite()) && b.iter().all(|v| v.is_finite()));
1151        // Position 1's own output is read by forward_last at n=2; adding
1152        // position 2 must leave the stack's view of 0..=1 untouched, so
1153        // re-running with the shorter prefix must agree with itself.
1154        let a2 = dec.forward_last(&seq[..2 * hs], 2);
1155        let d = a.iter().zip(&a2).map(|(x, y)| (x - y).abs()).fold(0f32, f32::max);
1156        assert!(d == 0.0, "not deterministic: {d}");
1157        let logits = dec.head(1, &b);
1158        assert_eq!(logits.len(), 1024, "audio vocab is 1024 per level");
1159        assert!(logits.iter().all(|v| v.is_finite()));
1160        let spread = logits.iter().fold(f32::MIN, |m, v| m.max(*v))
1161            - logits.iter().fold(f32::MAX, |m, v| m.min(*v));
1162        assert!(spread > 1e-3, "head is flat, spread {spread}");
1163        eprintln!("music3 rvq: 8 codebooks, head spread {spread:.3}");
1164    }
1165
1166    /// Forward the packed DiT and check what the reference fixes: a
1167    /// `[128, n]` velocity, finite, and responsive to BOTH inputs.
1168    /// `CMF_MUSIC3_DIT=<file.cmf>` points at a pack.
1169    ///
1170    /// The two response checks are the point. A forward that silently
1171    /// drops the condition — the easiest way to get the 2304-wide concat
1172    /// wrong — still returns a plausible velocity, and so does one that
1173    /// ignores the timestep token.
1174    #[test]
1175    fn music3_dit_forward_has_the_reference_geometry() {
1176        let Ok(p) = std::env::var("CMF_MUSIC3_DIT") else {
1177            eprintln!("CMF_MUSIC3_DIT unset — skipping Music-3 DiT test");
1178            return;
1179        };
1180        let model = Arc::new(CmfModel::open(&p).expect("open packed DiT"));
1181        let dit = Music3Dit::from_cmf(&model).expect("load DiT");
1182        let n = 4usize;
1183        let x: Vec<f32> = (0..Music3Dit::IN_CH * n)
1184            .map(|i| 0.3 * ((i as f32) * 0.017).sin())
1185            .collect();
1186        let cond: Vec<f32> = (0..Music3Dit::COND_CH * n)
1187            .map(|i| 0.2 * ((i as f32) * 0.011).cos())
1188            .collect();
1189        let v = dit.forward(&x, &cond, n, 0.7);
1190        assert_eq!(v.len(), Music3Dit::IN_CH * n, "velocity is [128, n]");
1191        assert!(v.iter().all(|q| q.is_finite()), "non-finite velocity");
1192        let rms = (v.iter().map(|q| q * q).sum::<f32>() / v.len() as f32).sqrt();
1193        assert!(rms > 1e-6, "velocity is silent, rms {rms}");
1194
1195        let zero_cond = vec![0f32; Music3Dit::COND_CH * n];
1196        let v0 = dit.forward(&x, &zero_cond, n, 0.7);
1197        let dc = v
1198            .iter()
1199            .zip(&v0)
1200            .map(|(a, b)| (a - b).abs())
1201            .fold(0f32, f32::max);
1202        assert!(dc > 1e-5, "condition changed nothing — concat is wrong");
1203
1204        let vt = dit.forward(&x, &cond, n, 0.2);
1205        let dt = v
1206            .iter()
1207            .zip(&vt)
1208            .map(|(a, b)| (a - b).abs())
1209            .fold(0f32, f32::max);
1210        assert!(dt > 1e-5, "timestep changed nothing — the token is lost");
1211        eprintln!("music3 dit: rms {rms:.4}, d/dcond {dc:.4}, d/dt {dt:.4}");
1212    }
1213}