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        #[allow(unused_mut)] // push()es below are cfg-gated on backend features
738        let mut v = vec![Device::Cpu];
739        #[cfg(feature = "metal")]
740        if rlx_runtime::is_available(Device::Metal) {
741            v.push(Device::Metal);
742        }
743        #[cfg(feature = "mlx")]
744        if rlx_runtime::is_available(Device::Mlx) {
745            v.push(Device::Mlx);
746        }
747        #[cfg(feature = "gpu")]
748        if rlx_runtime::is_available(Device::Gpu) {
749            v.push(Device::Gpu);
750        }
751        v
752    }
753
754    fn check(
755        name: &str,
756        x: &Array2<f32>,
757        reference: &Array2<f32>,
758        build: impl Fn(&mut Ctx, HirNodeId, usize, usize) -> (HirNodeId, usize, usize),
759    ) {
760        for dev in devices() {
761            let got = run_unary(dev, x, &build);
762            let err = max_abs(&got, reference);
763            assert!(err < 2e-3, "{name} on {dev:?}: max|Δ| = {err}");
764            eprintln!("{name} {dev:?} ok: max|Δ| = {err:.2e}");
765        }
766    }
767
768    fn conv_layer(
769        r: &mut Lcg,
770        c_out: usize,
771        c_in: usize,
772        k: usize,
773        stride: usize,
774        dil: usize,
775        mode: PadMode,
776    ) -> Conv1dLayer {
777        Conv1dLayer {
778            weight: r.a3(c_out, c_in, k),
779            bias: Some(r.a1(c_out)),
780            stride,
781            dilation: dil,
782            pad_mode: mode,
783        }
784    }
785
786    #[test]
787    fn causal_conv_constant() {
788        let mut r = Lcg(1);
789        let x = Array2::from_shape_fn((4, 40), |(_, t)| (t as f32 * 0.1).sin());
790        let conv = conv_layer(&mut r, 6, 4, 7, 1, 1, PadMode::Constant);
791        let reference = conv.forward(x.view());
792        check("causal_conv_constant", &x, &reference, |ctx, xin, _c, t| {
793            causal_conv(ctx, xin, t, &conv)
794        });
795    }
796
797    #[test]
798    fn causal_conv_dilated() {
799        let mut r = Lcg(2);
800        let x = Array2::from_shape_fn((5, 50), |(c, t)| ((t + c) as f32 * 0.07).cos());
801        let conv = conv_layer(&mut r, 5, 5, 3, 1, 2, PadMode::Constant);
802        let reference = conv.forward(x.view());
803        check("causal_conv_dilated", &x, &reference, |ctx, xin, _c, t| {
804            causal_conv(ctx, xin, t, &conv)
805        });
806    }
807
808    #[test]
809    fn causal_conv_strided_replicate() {
810        let mut r = Lcg(3);
811        let x = Array2::from_shape_fn((4, 41), |(_, t)| (t as f32 * 0.05).sin());
812        let conv = conv_layer(&mut r, 4, 4, 4, 2, 1, PadMode::Replicate);
813        let reference = conv.forward(x.view());
814        check(
815            "causal_conv_strided_replicate",
816            &x,
817            &reference,
818            |ctx, xin, _c, t| causal_conv(ctx, xin, t, &conv),
819        );
820    }
821
822    #[test]
823    fn elu_matches() {
824        let x = Array2::from_shape_fn((4, 20), |(c, t)| (t as f32 * 0.3 - 3.0) + c as f32 * 0.5);
825        let mut reference = x.clone();
826        elu_inplace(&mut reference);
827        check("elu", &x, &reference, |ctx, xin, c, t| {
828            (elu(ctx, xin, c, t), c, t)
829        });
830    }
831
832    #[test]
833    fn trans_conv_matches() {
834        let mut r = Lcg(4);
835        let x = Array2::from_shape_fn((6, 16), |(_, t)| (t as f32 * 0.2).sin());
836        let weight = r.a3(6, 3, 4); // [C_in, C_out, K]
837        let bias = r.a1(3);
838        let reference = conv_transpose1d(x.view(), &weight, Some(&bias), 2, 1.0);
839        check("trans_conv", &x, &reference, |ctx, xin, _c, t| {
840            trans_conv(
841                ctx,
842                xin,
843                t,
844                &weight,
845                Some(bias.as_slice().unwrap()),
846                2,
847                1.0,
848                1,
849            )
850        });
851    }
852
853    #[test]
854    fn grouped_upsample_matches() {
855        let mut r = Lcg(5);
856        let x = Array2::from_shape_fn((8, 12), |(c, t)| ((t + c) as f32 * 0.15).cos());
857        let weight = r.a3(8, 1, 4); // depthwise [channels, 1, K]
858        let reference = grouped_conv_transpose1d(x.view(), &weight, 2, 1.0);
859        check("grouped_upsample", &x, &reference, |ctx, xin, c, t| {
860            upsample(ctx, xin, c, t, &weight, 2, 1.0)
861        });
862    }
863
864    #[test]
865    fn resnet_block_matches() {
866        let mut r = Lcg(6);
867        let block = ResnetBlock {
868            conv_a: conv_layer(&mut r, 4, 4, 3, 1, 1, PadMode::Constant),
869            conv_b: conv_layer(&mut r, 4, 4, 1, 1, 1, PadMode::Constant),
870        };
871        let x = Array2::from_shape_fn((4, 30), |(c, t)| (t as f32 * 0.1).sin() + c as f32 * 0.05);
872        let reference = block.forward(x.view());
873        check("resnet_block", &x, &reference, |ctx, xin, c, t| {
874            resnet_block(ctx, xin, c, t, &block)
875        });
876    }
877
878    fn a2(r: &mut Lcg, rows: usize, cols: usize, scale: f32) -> Array2<f32> {
879        Array2::from_shape_fn((rows, cols), |_| r.f() * scale)
880    }
881
882    fn tiny_transformer(
883        r: &mut Lcg,
884        c: usize,
885        h: usize,
886        d: usize,
887        inter: usize,
888        n_layers: usize,
889        window: usize,
890    ) -> MimiTransformer {
891        use crate::transformer::{LayerScale, Mlp};
892        let half = d / 2;
893        let inv_freq = Array1::from_shape_fn(half, |i| {
894            1.0 / (10000f64.powf(i as f64 / half as f64) as f32)
895        });
896        let mut layers = Vec::new();
897        for _ in 0..n_layers {
898            layers.push(TransformerLayer {
899                input_norm_w: Array1::from_shape_fn(c, |_| 1.0 + r.f() * 0.1),
900                input_norm_b: r.a1(c),
901                post_norm_w: Array1::from_shape_fn(c, |_| 1.0 + r.f() * 0.1),
902                post_norm_b: r.a1(c),
903                attn: Attention {
904                    q_w: a2(r, c, c, 0.3),
905                    k_w: a2(r, c, c, 0.3),
906                    v_w: a2(r, c, c, 0.3),
907                    o_w: a2(r, c, c, 0.3),
908                    num_heads: h,
909                    head_dim: d,
910                    scaling: 1.0 / (d as f32).sqrt(),
911                },
912                attn_scale: LayerScale {
913                    scale: Array1::from_shape_fn(c, |_| 0.1 + r.f() * 0.02),
914                },
915                mlp: Mlp {
916                    fc1_w: a2(r, inter, c, 0.3),
917                    fc2_w: a2(r, c, inter, 0.3),
918                },
919                mlp_scale: LayerScale {
920                    scale: Array1::from_shape_fn(c, |_| 0.1 + r.f() * 0.02),
921                },
922            });
923        }
924        MimiTransformer {
925            layers,
926            inv_freq,
927            sliding_window: window,
928            norm_eps: 1e-5,
929        }
930    }
931
932    fn run_tc(dev: Device, x_tc: &Array2<f32>, tf: &MimiTransformer) -> Array2<f32> {
933        let (t, c) = x_tc.dim();
934        let mut hir = HirModule::new("tf");
935        let mut g = HirMut::new(&mut hir);
936        let mut ctx = Ctx::new(&mut g);
937        let xin = ctx.g.input("x", Shape::new(&[t, c], F32));
938        let out = transformer(&mut ctx, xin, t, c, tf);
939        let params = std::mem::take(&mut ctx.params);
940        hir.set_outputs(vec![out]);
941        let graph = Graph::from_hir(hir).unwrap();
942        let mut compiled = Session::new(dev).compile(graph);
943        for (n, d) in &params {
944            compiled.set_param(n, d);
945        }
946        compiled.finalize_params();
947        let flat: Vec<f32> = x_tc.iter().copied().collect();
948        let got = compiled.run(&[("x", &flat)]).into_iter().next().unwrap();
949        Array2::from_shape_vec((t, c), got).unwrap()
950    }
951
952    #[test]
953    fn transformer_matches() {
954        let mut r = Lcg(7);
955        let (c, h, d, inter, t) = (8usize, 2usize, 4usize, 16usize, 12usize);
956        let tf = tiny_transformer(&mut r, c, h, d, inter, 2, 100);
957        let x = Array2::from_shape_fn((t, c), |(ti, ci)| ((ti + ci) as f32 * 0.13).sin() * 0.5);
958        let reference = tf.forward(x.view());
959        for dev in devices() {
960            let got = run_tc(dev, &x, &tf);
961            let err = max_abs(&got, &reference);
962            assert!(err < 3e-3, "transformer on {dev:?}: max|Δ| = {err}");
963            eprintln!("transformer {dev:?} ok: max|Δ| = {err:.2e}");
964        }
965    }
966
967    #[test]
968    fn transformer_sliding_window() {
969        let mut r = Lcg(8);
970        let (c, h, d, inter, t) = (8usize, 2usize, 4usize, 16usize, 20usize);
971        let tf = tiny_transformer(&mut r, c, h, d, inter, 1, 4); // narrow window
972        let x = Array2::from_shape_fn((t, c), |(ti, ci)| ((ti * 2 + ci) as f32 * 0.09).cos() * 0.5);
973        let reference = tf.forward(x.view());
974        for dev in devices() {
975            let got = run_tc(dev, &x, &tf);
976            let err = max_abs(&got, &reference);
977            assert!(
978                err < 3e-3,
979                "transformer(window=4) on {dev:?}: max|Δ| = {err}"
980            );
981            eprintln!("transformer_sliding_window {dev:?} ok: max|Δ| = {err:.2e}");
982        }
983    }
984}