1use 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
31type NamedTensors = Vec<(String, Vec<f32>)>;
33
34const F32: DType = DType::F32;
35
36pub(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 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 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 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
166fn 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()); let minx = ctx.g.activation(Activation::Neg, relu_neg, shape.clone()); 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
179fn 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
211fn 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 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); 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 debug_assert_eq!((hc, ht), (c, t));
274 (ctx.g.add(x, h), hc, ht)
275}
276
277pub(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
338pub(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
348pub(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
361fn 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
379fn 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
386fn 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 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]); let scores = ctx.g.mm(q, kt); 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); let out = ctx.g.transpose_(out, vec![1, 0, 2]); 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
489fn 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
519fn 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
525fn 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
531pub(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
551pub 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
562pub 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
570pub 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
593pub 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
621pub 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 ¶ms {
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 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 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 ¶ms {
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); 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); 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 ¶ms {
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); 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}