Skip to main content

rlx_mimi/
graph.rs

1// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
2//
3// Mimi codec as rlx-runtime HIR graphs running natively on every backend
4// (cpu/metal/mlx/cuda/rocm/wgpu/vulkan). The SEANet conv stacks, the frame-rate
5// down/up-sampling, and the encoder/decoder transformers are all expressed as a
6// single compiled graph per (device, length). The split RVQ codebook search
7// stays host-side (see `rvq.rs`) — it is a tiny argmin.
8//
9// Conventions (shared with rlx-dac, see its graph.rs for the why):
10//   * channel-major activations `[C, T]` are carried as NCHW `[1, C, T, 1]`
11//     (length in H, singleton W), kernel `[k, 1]`. MLX reads the length-axis
12//     conv params from index 0, so the length MUST sit in H.
13//   * padding is explicit (concat) — backends disagree on conv `padding`.
14//   * transposed conv is decomposed to zero-insert + regular conv (wgpu/coreml
15//     have no native ConvTranspose kernel).
16//   * the transformer runs in time-major `[T, C]`.
17
18use crate::conv::PadMode;
19use crate::seanet::{
20    Conv1dLayer, FrameRateDownsample, FrameRateUpsample, ResnetBlock, SeanetDecoder, SeanetEncoder,
21    SeanetLayer,
22};
23use crate::transformer::{Attention, MimiTransformer, TransformerLayer};
24use anyhow::{Context, Result};
25use ndarray::{Array2, Array3};
26use rlx_ir::hir::{HirModule, HirMut, HirNodeId};
27use rlx_ir::op::{Activation, Op};
28use rlx_ir::{DType, Graph, HirGraphExt, Shape};
29use rlx_runtime::{CompiledGraph, Device, Session};
30
31/// Named tensors `(name, data)` produced alongside a built graph.
32type NamedTensors = Vec<(String, Vec<f32>)>;
33
34const F32: DType = DType::F32;
35
36/// Builder context: accumulates params (name → row-major f32) and unique names.
37pub(crate) struct Ctx<'a, 'b> {
38    pub(crate) g: &'a mut HirMut<'b>,
39    pub(crate) params: NamedTensors,
40    next: usize,
41}
42
43impl<'a, 'b> Ctx<'a, 'b> {
44    pub(crate) fn new(g: &'a mut HirMut<'b>) -> Self {
45        Self {
46            g,
47            params: Vec::new(),
48            next: 0,
49        }
50    }
51
52    pub(crate) fn param(&mut self, data: Vec<f32>, shape: &[usize]) -> HirNodeId {
53        let name = format!("w{}", self.next);
54        self.next += 1;
55        debug_assert_eq!(
56            data.len(),
57            shape.iter().product::<usize>(),
58            "param {name} shape {shape:?} vs len {}",
59            data.len()
60        );
61        let id = self.g.param(name.clone(), Shape::new(shape, F32));
62        self.params.push((name, data));
63        id
64    }
65
66    fn expand(&mut self, id: HirNodeId, target: &[usize]) -> HirNodeId {
67        let shape = Shape::new(target, F32);
68        let tgt: Vec<i64> = target.iter().map(|&d| d as i64).collect();
69        self.g
70            .add_node(Op::Expand { target_shape: tgt }, vec![id], shape)
71    }
72
73    fn scalar(&mut self, v: f32, target: &[usize]) -> HirNodeId {
74        let ones = vec![1usize; target.len()];
75        let s = self.param(vec![v], &ones);
76        self.expand(s, target)
77    }
78
79    /// `[1, C, T, 1]` bias-add from a `[C]` bias.
80    fn add_bias(&mut self, y: HirNodeId, bias: &[f32], c: usize, t: usize) -> HirNodeId {
81        let b = self.param(bias.to_vec(), &[1, c, 1, 1]);
82        let be = self.expand(b, &[1, c, t, 1]);
83        self.g.add(y, be)
84    }
85
86    /// Asymmetric pad on the length (H) axis, zero or edge-replicate.
87    fn pad_len(
88        &mut self,
89        x: HirNodeId,
90        c: usize,
91        t: usize,
92        pl: usize,
93        pr: usize,
94        mode: PadMode,
95    ) -> (HirNodeId, usize) {
96        if pl == 0 && pr == 0 {
97            return (x, t);
98        }
99        let mut parts: Vec<HirNodeId> = Vec::new();
100        match mode {
101            PadMode::Constant => {
102                if pl > 0 {
103                    parts.push(self.param(vec![0.0; c * pl], &[1, c, pl, 1]));
104                }
105                parts.push(x);
106                if pr > 0 {
107                    parts.push(self.param(vec![0.0; c * pr], &[1, c, pr, 1]));
108                }
109            }
110            PadMode::Replicate => {
111                if pl > 0 {
112                    let first = self.g.narrow_(x, 2, 0, 1);
113                    parts.push(self.expand(first, &[1, c, pl, 1]));
114                }
115                parts.push(x);
116                if pr > 0 {
117                    let last = self.g.narrow_(x, 2, t - 1, 1);
118                    parts.push(self.expand(last, &[1, c, pr, 1]));
119                }
120            }
121        }
122        (self.g.concat_(parts, 2), t + pl + pr)
123    }
124
125    /// Zero-insert `s-1` samples between length steps: `[1,C,T,1]` → `[1,C,(T-1)s+1,1]`.
126    fn inflate_len(&mut self, x: HirNodeId, c: usize, t: usize, s: usize) -> (HirNodeId, usize) {
127        if s <= 1 {
128            return (x, t);
129        }
130        let z = self.param(vec![0.0; c * t * (s - 1)], &[1, c, t, s - 1]);
131        let cat = self.g.concat_(vec![x, z], 3);
132        let resh = self.g.reshape_(cat, vec![1, c as i64, (t * s) as i64, 1]);
133        let lu = (t - 1) * s + 1;
134        (self.g.narrow_(resh, 2, 0, lu), lu)
135    }
136
137    fn conv_raw(
138        &mut self,
139        x: HirNodeId,
140        w: HirNodeId,
141        c_out: usize,
142        t_out: usize,
143        k: usize,
144        stride: usize,
145        dil: usize,
146        groups: usize,
147    ) -> HirNodeId {
148        self.g.add_node(
149            Op::Conv {
150                kernel_size: vec![k, 1],
151                stride: vec![stride, 1],
152                padding: vec![0, 0],
153                dilation: vec![dil, 1],
154                groups,
155            },
156            vec![x, w],
157            Shape::new(&[1, c_out, t_out, 1], F32),
158        )
159    }
160}
161
162fn weight_data(w: &Array3<f32>) -> Vec<f32> {
163    w.as_standard_layout().iter().copied().collect()
164}
165
166/// ELU: `x>=0 ? x : exp(x)-1`, via `relu(x) + exp(min(x,0)) - 1`.
167fn elu(ctx: &mut Ctx, x: HirNodeId, c: usize, t: usize) -> HirNodeId {
168    let shape = Shape::new(&[1, c, t, 1], F32);
169    let r = ctx.g.activation(Activation::Relu, x, shape.clone());
170    let negx = ctx.g.activation(Activation::Neg, x, shape.clone());
171    let relu_neg = ctx.g.activation(Activation::Relu, negx, shape.clone()); // -min(x,0)
172    let minx = ctx.g.activation(Activation::Neg, relu_neg, shape.clone()); // min(x,0)
173    let e = ctx.g.activation(Activation::Exp, minx, shape);
174    let re = ctx.g.add(r, e);
175    let ones = ctx.scalar(1.0, &[1, c, t, 1]);
176    ctx.g.sub(re, ones)
177}
178
179/// Causal 1D conv matching `mimi_causal_conv1d`. Returns `(node, c_out, t_out)`.
180fn causal_conv(
181    ctx: &mut Ctx,
182    x: HirNodeId,
183    t_in: usize,
184    conv: &Conv1dLayer,
185) -> (HirNodeId, usize, usize) {
186    let (c_out, c_in, k) = conv.weight.dim();
187    let stride = conv.stride;
188    let dil = conv.dilation;
189    let effective_k = (k - 1) * dil + 1;
190    let padding_total = effective_k.saturating_sub(stride);
191    let num = t_in as i64 - effective_k as i64 + padding_total as i64;
192    let n_frames = if num <= 0 {
193        0
194    } else {
195        (num as usize).div_ceil(stride)
196    };
197    let ideal_len = n_frames * stride + effective_k - padding_total;
198    let pad_left = padding_total;
199    let pad_right = ideal_len.saturating_sub(t_in);
200
201    let (xp, t_pad) = ctx.pad_len(x, c_in, t_in, pad_left, pad_right, conv.pad_mode);
202    let t_out = (t_pad - effective_k) / stride + 1;
203    let w = ctx.param(weight_data(&conv.weight), &[c_out, c_in, k, 1]);
204    let mut y = ctx.conv_raw(xp, w, c_out, t_out, k, stride, dil, 1);
205    if let Some(b) = &conv.bias {
206        y = ctx.add_bias(y, b.as_slice().unwrap(), c_out, t_out);
207    }
208    (y, c_out, t_out)
209}
210
211/// Transposed conv (`weight [C_in, C_out/groups, K]`) with `trim_right_ratio`,
212/// decomposed to zero-insert + regular grouped conv. Returns `(node, c_out, t_out)`.
213fn trans_conv(
214    ctx: &mut Ctx,
215    x: HirNodeId,
216    t_in: usize,
217    weight: &Array3<f32>,
218    bias: Option<&[f32]>,
219    stride: usize,
220    trim_right_ratio: f32,
221    groups: usize,
222) -> (HirNodeId, usize, usize) {
223    let (in_ch, c_out_per_g, k) = weight.dim();
224    let c_out = c_out_per_g * groups;
225
226    // Flip the kernel and swap in/out channels per group:
227    //   W[oc, ic_per_g, j] = weight[ic, oc_per_g, k-1-j]
228    let c_in_per_g = in_ch / groups;
229    let mut wflip = vec![0f32; c_out * c_in_per_g * k];
230    for g in 0..groups {
231        for ocp in 0..c_out_per_g {
232            let oc = g * c_out_per_g + ocp;
233            for icp in 0..c_in_per_g {
234                let ic = g * c_in_per_g + icp;
235                for j in 0..k {
236                    wflip[(oc * c_in_per_g + icp) * k + j] = weight[[ic, ocp, k - 1 - j]];
237                }
238            }
239        }
240    }
241
242    let (u, lu) = ctx.inflate_len(x, in_ch, t_in, stride);
243    let (up, t_pad) = ctx.pad_len(u, in_ch, lu, k - 1, k - 1, PadMode::Constant);
244    let t_raw = t_pad - (k - 1); // = (t_in-1)*stride + k
245    let w = ctx.param(wflip, &[c_out, c_in_per_g, k, 1]);
246    let mut y = ctx.conv_raw(up, w, c_out, t_raw, k, 1, 1, groups);
247
248    let padding_total = k.saturating_sub(stride);
249    let padding_right = ((padding_total as f32) * trim_right_ratio).ceil() as usize;
250    let padding_left = padding_total - padding_right;
251    let t_out = t_raw - padding_total;
252    if padding_left > 0 || padding_right > 0 {
253        y = ctx.g.narrow_(y, 2, padding_left, t_out);
254    }
255    if let Some(b) = bias {
256        y = ctx.add_bias(y, b, c_out, t_out);
257    }
258    (y, c_out, t_out)
259}
260
261fn resnet_block(
262    ctx: &mut Ctx,
263    x: HirNodeId,
264    c: usize,
265    t: usize,
266    blk: &ResnetBlock,
267) -> (HirNodeId, usize, usize) {
268    let h = elu(ctx, x, c, t);
269    let (h, hc, ht) = causal_conv(ctx, h, t, &blk.conv_a);
270    let h = elu(ctx, h, hc, ht);
271    let (h, hc, ht) = causal_conv(ctx, h, ht, &blk.conv_b);
272    // mimi resnet keeps length (causal conv), so residual adds directly.
273    debug_assert_eq!((hc, ht), (c, t));
274    (ctx.g.add(x, h), hc, ht)
275}
276
277/// Walk a SEANet layer stack. Returns `(node, c_out, t_out)`.
278pub(crate) fn seanet_stack(
279    ctx: &mut Ctx,
280    mut x: HirNodeId,
281    mut c: usize,
282    mut t: usize,
283    layers: &[SeanetLayer],
284) -> (HirNodeId, usize, usize) {
285    for layer in layers {
286        match layer {
287            SeanetLayer::Conv(conv) => {
288                (x, c, t) = causal_conv(ctx, x, t, conv);
289            }
290            SeanetLayer::TransConv {
291                weight,
292                bias,
293                stride,
294                trim_right_ratio,
295            } => {
296                (x, c, t) = trans_conv(
297                    ctx,
298                    x,
299                    t,
300                    weight,
301                    bias.as_ref().map(|b| b.as_slice().unwrap()),
302                    *stride,
303                    *trim_right_ratio,
304                    1,
305                );
306            }
307            SeanetLayer::Res(blk) => {
308                (x, c, t) = resnet_block(ctx, x, c, t, blk);
309            }
310            SeanetLayer::Elu => {
311                x = elu(ctx, x, c, t);
312            }
313        }
314    }
315    (x, c, t)
316}
317
318pub(crate) fn seanet_encoder(
319    ctx: &mut Ctx,
320    x: HirNodeId,
321    c: usize,
322    t: usize,
323    enc: &SeanetEncoder,
324) -> (HirNodeId, usize, usize) {
325    seanet_stack(ctx, x, c, t, &enc.layers)
326}
327
328pub(crate) fn seanet_decoder(
329    ctx: &mut Ctx,
330    x: HirNodeId,
331    c: usize,
332    t: usize,
333    dec: &SeanetDecoder,
334) -> (HirNodeId, usize, usize) {
335    seanet_stack(ctx, x, c, t, &dec.layers)
336}
337
338/// Frame-rate downsample (single stride-2 replicate-padded causal conv).
339pub(crate) fn downsample(
340    ctx: &mut Ctx,
341    x: HirNodeId,
342    t: usize,
343    conv: &Conv1dLayer,
344) -> (HirNodeId, usize, usize) {
345    causal_conv(ctx, x, t, conv)
346}
347
348/// Frame-rate upsample (depthwise grouped transposed conv, `groups = channels`).
349pub(crate) fn upsample(
350    ctx: &mut Ctx,
351    x: HirNodeId,
352    c: usize,
353    t: usize,
354    weight: &Array3<f32>,
355    stride: usize,
356    trim_right_ratio: f32,
357) -> (HirNodeId, usize, usize) {
358    trans_conv(ctx, x, t, weight, None, stride, trim_right_ratio, c)
359}
360
361// ------------------------------- transformer -------------------------------
362//
363// Operates in time-major `[T, C]`. Uses native LayerNorm/Softmax, batched
364// matmul for per-head attention, and rotate-half RoPE with host-precomputed
365// cos/sin + sliding-window causal mask bound as params.
366
367/// `x [rows, in] @ Wᵀ` where `w_oi` is `[out, in]` (torch Linear weight, no bias).
368fn linear(ctx: &mut Ctx, x: HirNodeId, in_d: usize, out_d: usize, w_oi: &Array2<f32>) -> HirNodeId {
369    let mut wt = vec![0f32; in_d * out_d];
370    for o in 0..out_d {
371        for i in 0..in_d {
372            wt[i * out_d + o] = w_oi[[o, i]];
373        }
374    }
375    let wp = ctx.param(wt, &[in_d, out_d]);
376    ctx.g.mm(x, wp)
377}
378
379/// Per-channel scale (`LayerScale`) on a `[T, C]` tensor.
380fn layer_scale(ctx: &mut Ctx, x: HirNodeId, t: usize, c: usize, scale: &[f32]) -> HirNodeId {
381    let s = ctx.param(scale.to_vec(), &[1, c]);
382    let se = ctx.expand(s, &[t, c]);
383    ctx.g.mul(x, se)
384}
385
386/// Rotate-half RoPE on `[H, T, D]` with cos/sin already shaped `[1, T, D]`.
387fn rope(
388    ctx: &mut Ctx,
389    x: HirNodeId,
390    h: usize,
391    t: usize,
392    d: usize,
393    cos_p: HirNodeId,
394    sin_p: HirNodeId,
395) -> HirNodeId {
396    let half = d / 2;
397    let cos_e = ctx.expand(cos_p, &[h, t, d]);
398    let sin_e = ctx.expand(sin_p, &[h, t, d]);
399    let a = ctx.g.narrow_(x, 2, 0, half);
400    let b = ctx.g.narrow_(x, 2, half, half);
401    let nb = ctx
402        .g
403        .activation(Activation::Neg, b, Shape::new(&[h, t, half], F32));
404    let rot = ctx.g.concat_(vec![nb, a], 2);
405    let xc = ctx.g.mul(x, cos_e);
406    let rs = ctx.g.mul(rot, sin_e);
407    ctx.g.add(xc, rs)
408}
409
410#[allow(clippy::too_many_arguments)]
411fn attention(
412    ctx: &mut Ctx,
413    x: HirNodeId,
414    t: usize,
415    attn: &Attention,
416    cos_p: HirNodeId,
417    sin_p: HirNodeId,
418    mask_p: HirNodeId,
419) -> HirNodeId {
420    let h = attn.num_heads;
421    let d = attn.head_dim;
422    let c = h * d;
423
424    let q = linear(ctx, x, c, c, &attn.q_w);
425    let k = linear(ctx, x, c, c, &attn.k_w);
426    let v = linear(ctx, x, c, c, &attn.v_w);
427
428    // [T, C] → [T, H, D] → [H, T, D]
429    let to_htd = |ctx: &mut Ctx, n: HirNodeId| {
430        let r = ctx.g.reshape_(n, vec![t as i64, h as i64, d as i64]);
431        ctx.g.transpose_(r, vec![1, 0, 2])
432    };
433    let q = to_htd(ctx, q);
434    let k = to_htd(ctx, k);
435    let v = to_htd(ctx, v);
436
437    let q = rope(ctx, q, h, t, d, cos_p, sin_p);
438    let k = rope(ctx, k, h, t, d, cos_p, sin_p);
439
440    let kt = ctx.g.transpose_(k, vec![0, 2, 1]); // [H, D, T]
441    let scores = ctx.g.mm(q, kt); // [H, T, T]
442    let sc = ctx.scalar(attn.scaling, &[h, t, t]);
443    let scores = ctx.g.mul(scores, sc);
444    let mask_e = ctx.expand(mask_p, &[h, t, t]);
445    let scores = ctx.g.add(scores, mask_e);
446    let probs = ctx.g.sm(scores, -1);
447    let out = ctx.g.mm(probs, v); // [H, T, D]
448
449    let out = ctx.g.transpose_(out, vec![1, 0, 2]); // [T, H, D]
450    let out = ctx.g.reshape_(out, vec![t as i64, c as i64]);
451    linear(ctx, out, c, c, &attn.o_w)
452}
453
454fn transformer_layer(
455    ctx: &mut Ctx,
456    x: HirNodeId,
457    t: usize,
458    c: usize,
459    eps: f32,
460    layer: &TransformerLayer,
461    cos_p: HirNodeId,
462    sin_p: HirNodeId,
463    mask_p: HirNodeId,
464) -> HirNodeId {
465    let ng = ctx.param(layer.input_norm_w.to_vec(), &[c]);
466    let nb = ctx.param(layer.input_norm_b.to_vec(), &[c]);
467    let n = ctx.g.ln(x, ng, nb, eps);
468    let attn_out = attention(ctx, n, t, &layer.attn, cos_p, sin_p, mask_p);
469    let attn_out = layer_scale(
470        ctx,
471        attn_out,
472        t,
473        c,
474        layer.attn_scale.scale.as_slice().unwrap(),
475    );
476    let h = ctx.g.add(x, attn_out);
477
478    let pg = ctx.param(layer.post_norm_w.to_vec(), &[c]);
479    let pb = ctx.param(layer.post_norm_b.to_vec(), &[c]);
480    let n2 = ctx.g.ln(h, pg, pb, eps);
481    let inter = layer.mlp.fc1_w.dim().0;
482    let m = linear(ctx, n2, c, inter, &layer.mlp.fc1_w);
483    let m = ctx.g.gelu(m);
484    let m = linear(ctx, m, inter, c, &layer.mlp.fc2_w);
485    let m = layer_scale(ctx, m, t, c, layer.mlp_scale.scale.as_slice().unwrap());
486    ctx.g.add(h, m)
487}
488
489/// Build cos/sin `[t, head_dim]` (rotate-half layout) from `inv_freq`.
490fn rope_tables(inv_freq: &[f32], t: usize) -> (Vec<f32>, Vec<f32>) {
491    let half = inv_freq.len();
492    let d = half * 2;
493    let mut cos = vec![0f32; t * d];
494    let mut sin = vec![0f32; t * d];
495    for ti in 0..t {
496        for hi in 0..half {
497            let f = ti as f32 * inv_freq[hi];
498            let (s, c) = f.sin_cos();
499            cos[ti * d + hi] = c;
500            cos[ti * d + hi + half] = c;
501            sin[ti * d + hi] = s;
502            sin[ti * d + hi + half] = s;
503        }
504    }
505    (cos, sin)
506}
507
508fn sliding_causal_mask(t: usize, window: usize) -> Vec<f32> {
509    let mut m = vec![f32::NEG_INFINITY; t * t];
510    for i in 0..t {
511        let lo = i.saturating_sub(window - 1);
512        for j in lo..=i {
513            m[i * t + j] = 0.0;
514        }
515    }
516    m
517}
518
519/// `[1, C, T, 1]` → `[T, C]`.
520fn ct_to_tc(ctx: &mut Ctx, x: HirNodeId, c: usize, t: usize) -> HirNodeId {
521    let r = ctx.g.reshape_(x, vec![c as i64, t as i64]);
522    ctx.g.transpose_(r, vec![1, 0])
523}
524
525/// `[T, C]` → `[1, C, T, 1]`.
526fn tc_to_ct(ctx: &mut Ctx, x: HirNodeId, t: usize, c: usize) -> HirNodeId {
527    let r = ctx.g.transpose_(x, vec![1, 0]);
528    ctx.g.reshape_(r, vec![1, c as i64, t as i64, 1])
529}
530
531/// Run the full transformer over a `[T, C]` node. Returns the `[T, C]` output.
532pub(crate) fn transformer(
533    ctx: &mut Ctx,
534    x: HirNodeId,
535    t: usize,
536    c: usize,
537    tf: &MimiTransformer,
538) -> HirNodeId {
539    let d = tf.inv_freq.len() * 2;
540    let (cos, sin) = rope_tables(tf.inv_freq.as_slice().unwrap(), t);
541    let cos_p = ctx.param(cos, &[1, t, d]);
542    let sin_p = ctx.param(sin, &[1, t, d]);
543    let mask_p = ctx.param(sliding_causal_mask(t, tf.sliding_window), &[1, t, t]);
544    let mut h = x;
545    for layer in &tf.layers {
546        h = transformer_layer(ctx, h, t, c, tf.norm_eps, layer, cos_p, sin_p, mask_p);
547    }
548    h
549}
550
551// ----------------------------- full pipelines -----------------------------
552
553/// Weights for the encode path: SEANet encoder → transformer → downsample.
554pub struct EncodeWeights<'a> {
555    pub encoder: &'a SeanetEncoder,
556    pub transformer: &'a MimiTransformer,
557    pub downsample: &'a FrameRateDownsample,
558    pub audio_channels: usize,
559    pub hidden_size: usize,
560}
561
562/// Weights for the decode path: upsample → transformer → SEANet decoder.
563pub struct DecodeWeights<'a> {
564    pub upsample: &'a FrameRateUpsample,
565    pub transformer: &'a MimiTransformer,
566    pub decoder: &'a SeanetDecoder,
567    pub hidden_size: usize,
568}
569
570/// Encode graph: PCM `[1, 1, in_len, 1]` → downsampled latent `[1, hidden, t_ds, 1]`.
571pub fn build_encode_graph(
572    w: &EncodeWeights,
573    in_len: usize,
574) -> Result<(Graph, NamedTensors, (usize, usize))> {
575    let mut hir = HirModule::new("mimi_encode");
576    let mut g = HirMut::new(&mut hir);
577    let mut ctx = Ctx::new(&mut g);
578
579    let x = ctx
580        .g
581        .input("pcm", Shape::new(&[1, w.audio_channels, in_len, 1], F32));
582    let (h, c, t) = seanet_encoder(&mut ctx, x, w.audio_channels, in_len, w.encoder);
583    let tc = ct_to_tc(&mut ctx, h, c, t);
584    let tc = transformer(&mut ctx, tc, t, c, w.transformer);
585    let ct = tc_to_ct(&mut ctx, tc, t, c);
586    let (out, oc, ot) = downsample(&mut ctx, ct, t, &w.downsample.conv);
587    let params = ctx.params;
588    hir.set_outputs(vec![out]);
589    let graph = Graph::from_hir(hir).map_err(|e| anyhow::anyhow!("mimi encode lower: {e}"))?;
590    Ok((graph, params, (oc, ot)))
591}
592
593/// Decode graph: latent `[1, hidden, in_t, 1]` → waveform `[1, 1, out_len, 1]`.
594pub fn build_decode_graph(w: &DecodeWeights, in_t: usize) -> Result<(Graph, NamedTensors, usize)> {
595    let mut hir = HirModule::new("mimi_decode");
596    let mut g = HirMut::new(&mut hir);
597    let mut ctx = Ctx::new(&mut g);
598
599    let x = ctx
600        .g
601        .input("z", Shape::new(&[1, w.hidden_size, in_t, 1], F32));
602    let (h, c, t) = upsample(
603        &mut ctx,
604        x,
605        w.hidden_size,
606        in_t,
607        &w.upsample.weight,
608        w.upsample.stride,
609        w.upsample.trim_right_ratio,
610    );
611    let tc = ct_to_tc(&mut ctx, h, c, t);
612    let tc = transformer(&mut ctx, tc, t, c, w.transformer);
613    let ct = tc_to_ct(&mut ctx, tc, t, c);
614    let (out, _oc, ot) = seanet_decoder(&mut ctx, ct, c, t, w.decoder);
615    let params = ctx.params;
616    hir.set_outputs(vec![out]);
617    let graph = Graph::from_hir(hir).map_err(|e| anyhow::anyhow!("mimi decode lower: {e}"))?;
618    Ok((graph, params, ot))
619}
620
621/// A compiled encode/decode graph plus its `[out_c, out_t]` output dims.
622pub struct CodecGraph {
623    compiled: CompiledGraph,
624    out_c: usize,
625    out_t: usize,
626    input_name: &'static str,
627}
628
629impl CodecGraph {
630    fn compile(
631        device: Device,
632        graph: Graph,
633        params: NamedTensors,
634        out_c: usize,
635        out_t: usize,
636        input_name: &'static str,
637    ) -> Self {
638        let mut compiled = Session::new(device).compile(graph);
639        for (name, data) in &params {
640            compiled.set_param(name, data);
641        }
642        compiled.finalize_params();
643        Self {
644            compiled,
645            out_c,
646            out_t,
647            input_name,
648        }
649    }
650
651    pub fn encoder(device: Device, w: &EncodeWeights, in_len: usize) -> Result<Self> {
652        let (graph, params, (oc, ot)) = build_encode_graph(w, in_len)?;
653        Ok(Self::compile(device, graph, params, oc, ot, "pcm"))
654    }
655
656    pub fn decoder(device: Device, w: &DecodeWeights, in_t: usize) -> Result<Self> {
657        let (graph, params, out_len) = build_decode_graph(w, in_t)?;
658        Ok(Self::compile(device, graph, params, 1, out_len, "z"))
659    }
660
661    /// Run with row-major `[C_in, T_in]` input, returning `[out_c, out_t]`.
662    pub fn run(&mut self, input: &[f32]) -> Result<Array2<f32>> {
663        let outs = self.compiled.run(&[(self.input_name, input)]);
664        let flat = outs
665            .into_iter()
666            .next()
667            .context("mimi graph produced no output")?;
668        Array2::from_shape_vec((self.out_c, self.out_t), flat).context("mimi graph output reshape")
669    }
670
671    pub fn out_dims(&self) -> (usize, usize) {
672        (self.out_c, self.out_t)
673    }
674}
675
676#[cfg(test)]
677mod tests {
678    use super::*;
679    use crate::conv::{conv_transpose1d, elu_inplace, grouped_conv_transpose1d};
680    use crate::seanet::Conv1dLayer;
681    use ndarray::{Array1, Array2, Array3};
682    use rlx_ir::Graph;
683    use rlx_ir::hir::HirModule;
684    use rlx_runtime::{Device, Session};
685
686    struct Lcg(u64);
687    impl Lcg {
688        fn f(&mut self) -> f32 {
689            self.0 = self
690                .0
691                .wrapping_mul(6364136223846793005)
692                .wrapping_add(1442695040888963407);
693            ((self.0 >> 33) as f32 / (1u64 << 31) as f32) - 1.0
694        }
695        fn a3(&mut self, a: usize, b: usize, c: usize) -> Array3<f32> {
696            Array3::from_shape_fn((a, b, c), |_| self.f() * 0.3)
697        }
698        fn a1(&mut self, n: usize) -> Array1<f32> {
699            Array1::from_shape_fn(n, |_| self.f() * 0.2)
700        }
701    }
702
703    fn max_abs(a: &Array2<f32>, b: &Array2<f32>) -> f32 {
704        assert_eq!(a.dim(), b.dim(), "shape {:?} vs {:?}", a.dim(), b.dim());
705        a.iter()
706            .zip(b.iter())
707            .map(|(x, y)| (x - y).abs())
708            .fold(0.0, f32::max)
709    }
710
711    /// Build a one-shot graph from `build`, run on `dev`, return `[c_out, t_out]`.
712    fn run_unary(
713        dev: Device,
714        x: &Array2<f32>,
715        build: impl FnOnce(&mut Ctx, HirNodeId, usize, usize) -> (HirNodeId, usize, usize),
716    ) -> Array2<f32> {
717        let (c, t) = x.dim();
718        let mut hir = HirModule::new("t");
719        let mut g = HirMut::new(&mut hir);
720        let mut ctx = Ctx::new(&mut g);
721        let xin = ctx.g.input("x", Shape::new(&[1, c, t, 1], F32));
722        let (out, oc, ot) = build(&mut ctx, xin, c, t);
723        let params = std::mem::take(&mut ctx.params);
724        hir.set_outputs(vec![out]);
725        let graph = Graph::from_hir(hir).unwrap();
726        let mut compiled = Session::new(dev).compile(graph);
727        for (n, d) in &params {
728            compiled.set_param(n, d);
729        }
730        compiled.finalize_params();
731        let flat: Vec<f32> = x.iter().copied().collect();
732        let got = compiled.run(&[("x", &flat)]).into_iter().next().unwrap();
733        Array2::from_shape_vec((oc, ot), got).unwrap()
734    }
735
736    fn devices() -> Vec<Device> {
737        let v = vec![Device::Cpu];
738        #[cfg(feature = "metal")]
739        if rlx_runtime::is_available(Device::Metal) {
740            v.push(Device::Metal);
741        }
742        #[cfg(feature = "mlx")]
743        if rlx_runtime::is_available(Device::Mlx) {
744            v.push(Device::Mlx);
745        }
746        #[cfg(feature = "gpu")]
747        if rlx_runtime::is_available(Device::Gpu) {
748            v.push(Device::Gpu);
749        }
750        v
751    }
752
753    fn check(
754        name: &str,
755        x: &Array2<f32>,
756        reference: &Array2<f32>,
757        build: impl Fn(&mut Ctx, HirNodeId, usize, usize) -> (HirNodeId, usize, usize),
758    ) {
759        for dev in devices() {
760            let got = run_unary(dev, x, &build);
761            let err = max_abs(&got, reference);
762            assert!(err < 2e-3, "{name} on {dev:?}: max|Δ| = {err}");
763            eprintln!("{name} {dev:?} ok: max|Δ| = {err:.2e}");
764        }
765    }
766
767    fn conv_layer(
768        r: &mut Lcg,
769        c_out: usize,
770        c_in: usize,
771        k: usize,
772        stride: usize,
773        dil: usize,
774        mode: PadMode,
775    ) -> Conv1dLayer {
776        Conv1dLayer {
777            weight: r.a3(c_out, c_in, k),
778            bias: Some(r.a1(c_out)),
779            stride,
780            dilation: dil,
781            pad_mode: mode,
782        }
783    }
784
785    #[test]
786    fn causal_conv_constant() {
787        let mut r = Lcg(1);
788        let x = Array2::from_shape_fn((4, 40), |(_, t)| (t as f32 * 0.1).sin());
789        let conv = conv_layer(&mut r, 6, 4, 7, 1, 1, PadMode::Constant);
790        let reference = conv.forward(x.view());
791        check("causal_conv_constant", &x, &reference, |ctx, xin, _c, t| {
792            causal_conv(ctx, xin, t, &conv)
793        });
794    }
795
796    #[test]
797    fn causal_conv_dilated() {
798        let mut r = Lcg(2);
799        let x = Array2::from_shape_fn((5, 50), |(c, t)| ((t + c) as f32 * 0.07).cos());
800        let conv = conv_layer(&mut r, 5, 5, 3, 1, 2, PadMode::Constant);
801        let reference = conv.forward(x.view());
802        check("causal_conv_dilated", &x, &reference, |ctx, xin, _c, t| {
803            causal_conv(ctx, xin, t, &conv)
804        });
805    }
806
807    #[test]
808    fn causal_conv_strided_replicate() {
809        let mut r = Lcg(3);
810        let x = Array2::from_shape_fn((4, 41), |(_, t)| (t as f32 * 0.05).sin());
811        let conv = conv_layer(&mut r, 4, 4, 4, 2, 1, PadMode::Replicate);
812        let reference = conv.forward(x.view());
813        check(
814            "causal_conv_strided_replicate",
815            &x,
816            &reference,
817            |ctx, xin, _c, t| causal_conv(ctx, xin, t, &conv),
818        );
819    }
820
821    #[test]
822    fn elu_matches() {
823        let x = Array2::from_shape_fn((4, 20), |(c, t)| (t as f32 * 0.3 - 3.0) + c as f32 * 0.5);
824        let mut reference = x.clone();
825        elu_inplace(&mut reference);
826        check("elu", &x, &reference, |ctx, xin, c, t| {
827            (elu(ctx, xin, c, t), c, t)
828        });
829    }
830
831    #[test]
832    fn trans_conv_matches() {
833        let mut r = Lcg(4);
834        let x = Array2::from_shape_fn((6, 16), |(_, t)| (t as f32 * 0.2).sin());
835        let weight = r.a3(6, 3, 4); // [C_in, C_out, K]
836        let bias = r.a1(3);
837        let reference = conv_transpose1d(x.view(), &weight, Some(&bias), 2, 1.0);
838        check("trans_conv", &x, &reference, |ctx, xin, _c, t| {
839            trans_conv(
840                ctx,
841                xin,
842                t,
843                &weight,
844                Some(bias.as_slice().unwrap()),
845                2,
846                1.0,
847                1,
848            )
849        });
850    }
851
852    #[test]
853    fn grouped_upsample_matches() {
854        let mut r = Lcg(5);
855        let x = Array2::from_shape_fn((8, 12), |(c, t)| ((t + c) as f32 * 0.15).cos());
856        let weight = r.a3(8, 1, 4); // depthwise [channels, 1, K]
857        let reference = grouped_conv_transpose1d(x.view(), &weight, 2, 1.0);
858        check("grouped_upsample", &x, &reference, |ctx, xin, c, t| {
859            upsample(ctx, xin, c, t, &weight, 2, 1.0)
860        });
861    }
862
863    #[test]
864    fn resnet_block_matches() {
865        let mut r = Lcg(6);
866        let block = ResnetBlock {
867            conv_a: conv_layer(&mut r, 4, 4, 3, 1, 1, PadMode::Constant),
868            conv_b: conv_layer(&mut r, 4, 4, 1, 1, 1, PadMode::Constant),
869        };
870        let x = Array2::from_shape_fn((4, 30), |(c, t)| (t as f32 * 0.1).sin() + c as f32 * 0.05);
871        let reference = block.forward(x.view());
872        check("resnet_block", &x, &reference, |ctx, xin, c, t| {
873            resnet_block(ctx, xin, c, t, &block)
874        });
875    }
876
877    fn a2(r: &mut Lcg, rows: usize, cols: usize, scale: f32) -> Array2<f32> {
878        Array2::from_shape_fn((rows, cols), |_| r.f() * scale)
879    }
880
881    fn tiny_transformer(
882        r: &mut Lcg,
883        c: usize,
884        h: usize,
885        d: usize,
886        inter: usize,
887        n_layers: usize,
888        window: usize,
889    ) -> MimiTransformer {
890        use crate::transformer::{LayerScale, Mlp};
891        let half = d / 2;
892        let inv_freq = Array1::from_shape_fn(half, |i| {
893            1.0 / (10000f64.powf(i as f64 / half as f64) as f32)
894        });
895        let mut layers = Vec::new();
896        for _ in 0..n_layers {
897            layers.push(TransformerLayer {
898                input_norm_w: Array1::from_shape_fn(c, |_| 1.0 + r.f() * 0.1),
899                input_norm_b: r.a1(c),
900                post_norm_w: Array1::from_shape_fn(c, |_| 1.0 + r.f() * 0.1),
901                post_norm_b: r.a1(c),
902                attn: Attention {
903                    q_w: a2(r, c, c, 0.3),
904                    k_w: a2(r, c, c, 0.3),
905                    v_w: a2(r, c, c, 0.3),
906                    o_w: a2(r, c, c, 0.3),
907                    num_heads: h,
908                    head_dim: d,
909                    scaling: 1.0 / (d as f32).sqrt(),
910                },
911                attn_scale: LayerScale {
912                    scale: Array1::from_shape_fn(c, |_| 0.1 + r.f() * 0.02),
913                },
914                mlp: Mlp {
915                    fc1_w: a2(r, inter, c, 0.3),
916                    fc2_w: a2(r, c, inter, 0.3),
917                },
918                mlp_scale: LayerScale {
919                    scale: Array1::from_shape_fn(c, |_| 0.1 + r.f() * 0.02),
920                },
921            });
922        }
923        MimiTransformer {
924            layers,
925            inv_freq,
926            sliding_window: window,
927            norm_eps: 1e-5,
928        }
929    }
930
931    fn run_tc(dev: Device, x_tc: &Array2<f32>, tf: &MimiTransformer) -> Array2<f32> {
932        let (t, c) = x_tc.dim();
933        let mut hir = HirModule::new("tf");
934        let mut g = HirMut::new(&mut hir);
935        let mut ctx = Ctx::new(&mut g);
936        let xin = ctx.g.input("x", Shape::new(&[t, c], F32));
937        let out = transformer(&mut ctx, xin, t, c, tf);
938        let params = std::mem::take(&mut ctx.params);
939        hir.set_outputs(vec![out]);
940        let graph = Graph::from_hir(hir).unwrap();
941        let mut compiled = Session::new(dev).compile(graph);
942        for (n, d) in &params {
943            compiled.set_param(n, d);
944        }
945        compiled.finalize_params();
946        let flat: Vec<f32> = x_tc.iter().copied().collect();
947        let got = compiled.run(&[("x", &flat)]).into_iter().next().unwrap();
948        Array2::from_shape_vec((t, c), got).unwrap()
949    }
950
951    #[test]
952    fn transformer_matches() {
953        let mut r = Lcg(7);
954        let (c, h, d, inter, t) = (8usize, 2usize, 4usize, 16usize, 12usize);
955        let tf = tiny_transformer(&mut r, c, h, d, inter, 2, 100);
956        let x = Array2::from_shape_fn((t, c), |(ti, ci)| ((ti + ci) as f32 * 0.13).sin() * 0.5);
957        let reference = tf.forward(x.view());
958        for dev in devices() {
959            let got = run_tc(dev, &x, &tf);
960            let err = max_abs(&got, &reference);
961            assert!(err < 3e-3, "transformer on {dev:?}: max|Δ| = {err}");
962            eprintln!("transformer {dev:?} ok: max|Δ| = {err:.2e}");
963        }
964    }
965
966    #[test]
967    fn transformer_sliding_window() {
968        let mut r = Lcg(8);
969        let (c, h, d, inter, t) = (8usize, 2usize, 4usize, 16usize, 20usize);
970        let tf = tiny_transformer(&mut r, c, h, d, inter, 1, 4); // narrow window
971        let x = Array2::from_shape_fn((t, c), |(ti, ci)| ((ti * 2 + ci) as f32 * 0.09).cos() * 0.5);
972        let reference = tf.forward(x.view());
973        for dev in devices() {
974            let got = run_tc(dev, &x, &tf);
975            let err = max_abs(&got, &reference);
976            assert!(
977                err < 3e-3,
978                "transformer(window=4) on {dev:?}: max|Δ| = {err}"
979            );
980            eprintln!("transformer_sliding_window {dev:?} ok: max|Δ| = {err:.2e}");
981        }
982    }
983}