1use crate::dit::Proj;
38
39pub static MMH3_PROF: [std::sync::atomic::AtomicU64; 9] = [
43 std::sync::atomic::AtomicU64::new(0),
44 std::sync::atomic::AtomicU64::new(0),
45 std::sync::atomic::AtomicU64::new(0),
46 std::sync::atomic::AtomicU64::new(0),
47 std::sync::atomic::AtomicU64::new(0),
48 std::sync::atomic::AtomicU64::new(0),
49 std::sync::atomic::AtomicU64::new(0),
50 std::sync::atomic::AtomicU64::new(0),
51 std::sync::atomic::AtomicU64::new(0),
52];
53
54pub(crate) fn mmh3_prof_on() -> bool {
55 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
56 *ON.get_or_init(|| std::env::var("CMF_MMH3_PROF").is_ok())
57}
58
59pub fn mmh3_prof_report() -> Option<String> {
62 if !mmh3_prof_on() {
63 return None;
64 }
65 const NAMES: [&str; 9] = [
66 "norm+mod", "qkv gemm", "qknorm+rope", "attention", "out gemm", "fc1 gemm",
67 "swiglu", "fc2 gemm", "residual",
68 ];
69 let mut v: Vec<(u64, &str)> = MMH3_PROF
70 .iter()
71 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
72 .zip(NAMES)
73 .collect();
74 let total: u64 = v.iter().map(|(us, _)| *us).sum();
75 v.sort_by(|a, b| b.0.cmp(&a.0));
76 let mut out = format!("mmh3 phases (total {:.1} s):", total as f64 / 1e6);
77 for (us, name) in v {
78 out.push_str(&format!(
79 "\n {name:<14} {:>7.1} s {:>5.1}%",
80 us as f64 / 1e6,
81 100.0 * us as f64 / total.max(1) as f64
82 ));
83 }
84 Some(out)
85}
86
87use crate::pool::Pool;
88use cortiq_core::CmfModel;
89use std::sync::Arc;
90
91const FRAME_PER_TOKEN: [f64; 5] = [1.0, 4.0, 4.0, 4.0, 4.0];
94const FRAME_RESCALE: f64 = 5.0 / 3.0;
95
96const TAG_VIDEO: usize = 0;
98const TAG_TEXT: usize = 1;
99const TAG_AUDIO: usize = 2;
100const MODALITIES: usize = 3;
101const EXPAND: usize = 6;
103const VISUAL_COND_TIMESTEP: f64 = 0.999;
105const AUDIO_COND_TIMESTEP: f64 = 1.0;
107
108pub fn time_shift_sigma(sigma: f64, from_shift: f64, to_shift: f64) -> f64 {
112 let base = sigma / (from_shift + sigma * (1.0 - from_shift));
113 to_shift * base / (1.0 + (to_shift - 1.0) * base)
114}
115
116pub fn time_shift_slope(sigma: f64, from_shift: f64, to_shift: f64) -> f64 {
118 let base = sigma / (from_shift + sigma * (1.0 - from_shift));
119 (to_shift * (1.0 + (from_shift - 1.0) * base).powi(2))
120 / (from_shift * (1.0 + (to_shift - 1.0) * base).powi(2))
121}
122
123#[derive(Clone, Copy, PartialEq, Debug)]
126pub enum Kind {
127 Text,
128 Cond,
130 RefImg,
132 RefAudio,
134 Audio,
135 Video,
136}
137
138#[derive(Clone, Copy, Debug)]
140pub struct Segment {
141 pub start: usize,
142 pub stop: usize,
143 pub kind: Kind,
144}
145
146pub struct Layout {
149 pub seq_len: usize,
150 pub segments: Vec<Segment>,
151 pub pos: Vec<[f64; 3]>,
153 pub text_len: usize,
154 pub audio_t: usize,
155 pub latent_t: usize,
156 pub lat_h: usize,
157 pub lat_w: usize,
158 pub frame_rows: usize,
160 pub text_tags: Vec<u8>,
163}
164
165#[derive(Clone, Debug)]
167pub enum Ref {
168 Image { lat_h: usize, lat_w: usize },
170 Audio { t: usize },
172 Video {
175 latent_t: usize,
176 lat_h: usize,
177 lat_w: usize,
178 audio_t: usize,
179 },
180}
181
182fn push_audio_grid(pos: &mut Vec<[f64; 3]>, cursor: f64, t: usize, w_low: f64, w_high: f64) {
186 for ch in 0..2 {
187 let w = if ch == 0 { w_low } else { w_high };
188 for i in 0..t {
189 pos.push([cursor + i as f64, 0.0, w]);
190 }
191 }
192}
193
194fn axis_from_sqrt_area(dim: usize, patch: usize, sqrt_area: f64) -> Vec<f64> {
196 let ratio = dim as f64 / sqrt_area;
197 let n = dim / patch;
198 (0..n)
199 .map(|i| (i as f64 * (ratio / n as f64) + (1.0 - ratio) / 2.0) * 32.0)
200 .collect()
201}
202
203impl Layout {
204 pub fn t2va(text_len: usize, latent_t: usize, lat_h: usize, lat_w: usize, audio_t: usize) -> Self {
208 Self::build(text_len, latent_t, lat_h, lat_w, audio_t, &[], &[])
209 }
210
211 pub fn fl2va(
221 text_len: usize,
222 latent_t: usize,
223 lat_h: usize,
224 lat_w: usize,
225 audio_t: usize,
226 frames: &[(usize, usize)],
227 text_tags: &[u8],
228 ) -> Self {
229 Self::build(text_len, latent_t, lat_h, lat_w, audio_t, frames, text_tags)
230 }
231
232 pub fn ref2va(
237 text_len: usize,
238 latent_t: usize,
239 lat_h: usize,
240 lat_w: usize,
241 audio_t: usize,
242 refs: &[Ref],
243 text_tags: &[u8],
244 ) -> Self {
245 Self::build_full(text_len, latent_t, lat_h, lat_w, audio_t, &[], refs, text_tags)
246 }
247
248 fn build(
249 text_len: usize,
250 latent_t: usize,
251 lat_h: usize,
252 lat_w: usize,
253 audio_t: usize,
254 frames: &[(usize, usize)],
255 text_tags: &[u8],
256 ) -> Self {
257 Self::build_full(text_len, latent_t, lat_h, lat_w, audio_t, frames, &[], text_tags)
258 }
259
260 #[allow(clippy::too_many_arguments)]
261 fn build_full(
262 text_len: usize,
263 latent_t: usize,
264 lat_h: usize,
265 lat_w: usize,
266 audio_t: usize,
267 frames: &[(usize, usize)],
268 refs: &[Ref],
269 text_tags: &[u8],
270 ) -> Self {
271 let area = ((lat_h * lat_w) as f64).sqrt();
272 let h_axis = axis_from_sqrt_area(lat_h, 2, area);
273 let w_axis = axis_from_sqrt_area(lat_w, 2, area);
274 let frame_rows = h_axis.len() * w_axis.len();
275
276 let mut pos: Vec<[f64; 3]> = Vec::new();
277 let mut segments = Vec::new();
278
279 segments.push(Segment { start: 0, stop: text_len, kind: Kind::Text });
280 for i in 0..text_len {
281 pos.push([i as f64, 0.0, 0.0]);
282 }
283
284 let mut cursor = text_len as f64;
288 let (w_low_t, w_high_t) = (w_axis[0], w_axis[w_axis.len() - 1]);
289
290 for r in refs {
291 match r {
292 Ref::Image { lat_h, lat_w } => {
293 let (rh, rw) = (
294 axis_from_sqrt_area(*lat_h, 2, ((lat_h * lat_w) as f64).sqrt()),
295 axis_from_sqrt_area(*lat_w, 2, ((lat_h * lat_w) as f64).sqrt()),
296 );
297 let start = pos.len();
298 for &h in &rh {
299 for &w in &rw {
300 pos.push([cursor, h, w]);
301 }
302 }
303 segments.push(Segment { start, stop: pos.len(), kind: Kind::RefImg });
304 cursor += 1.0;
305 }
306 Ref::Audio { t } => {
307 if *t > 0 {
308 let start = pos.len();
309 push_audio_grid(&mut pos, cursor, *t, w_low_t, w_high_t);
310 segments.push(Segment { start, stop: pos.len(), kind: Kind::RefAudio });
311 }
312 cursor += *t as f64;
313 }
314 Ref::Video { latent_t: vt, lat_h: rh_, lat_w: rw_, audio_t: rt } => {
315 let area = ((rh_ * rw_) as f64).sqrt();
316 let rh = axis_from_sqrt_area(*rh_, 2, area);
317 let rw = axis_from_sqrt_area(*rw_, 2, area);
318 if *rt > 0 {
322 let start = pos.len();
323 push_audio_grid(&mut pos, cursor, *rt, rw[0], rw[rw.len() - 1]);
324 segments.push(Segment { start, stop: pos.len(), kind: Kind::RefAudio });
325 }
326 let start = pos.len();
327 let mut t_coord = cursor;
328 for k in 0..*vt {
329 for &h in &rh {
330 for &w in &rw {
331 pos.push([t_coord, h, w]);
332 }
333 }
334 t_coord += FRAME_RESCALE * FRAME_PER_TOKEN[k % 5];
335 }
336 segments.push(Segment { start, stop: pos.len(), kind: Kind::RefImg });
337 let spans: f64 = (0..*vt)
338 .map(|k| FRAME_RESCALE * FRAME_PER_TOKEN[k % 5])
339 .sum();
340 cursor += (*rt as f64).max(spans);
341 }
342 }
343 }
344 let cursor = cursor;
345
346 let spans: f64 = (0..latent_t)
348 .map(|k| FRAME_RESCALE * FRAME_PER_TOKEN[k % 5])
349 .sum();
350 for &(pixel_index, frame_count) in frames {
351 let cond_t = if pixel_index == 0 {
352 cursor
353 } else if frame_count > 0 && pixel_index == frame_count - 1 {
354 cursor + spans - FRAME_RESCALE
355 } else {
356 panic!("only the first and last frame can anchor a keyframe");
357 };
358 let start = pos.len();
359 for &h in &h_axis {
360 for &w in &w_axis {
361 pos.push([cond_t, h, w]);
362 }
363 }
364 segments.push(Segment { start, stop: pos.len(), kind: Kind::Cond });
365 }
366
367 let a_start = pos.len();
371 push_audio_grid(&mut pos, cursor, audio_t, w_low_t, w_high_t);
372 segments.push(Segment { start: a_start, stop: pos.len(), kind: Kind::Audio });
373
374 let v_start = pos.len();
377 let mut t_coord = cursor;
378 for k in 0..latent_t {
379 for &h in &h_axis {
380 for &w in &w_axis {
381 pos.push([t_coord, h, w]);
382 }
383 }
384 t_coord += FRAME_RESCALE * FRAME_PER_TOKEN[k % 5];
385 }
386 segments.push(Segment { start: v_start, stop: pos.len(), kind: Kind::Video });
387
388 Self {
389 seq_len: pos.len(),
390 segments,
391 pos,
392 text_len,
393 audio_t,
394 latent_t,
395 lat_h,
396 lat_w,
397 frame_rows,
398 text_tags: if text_tags.is_empty() {
399 vec![TAG_TEXT as u8; text_len]
400 } else {
401 text_tags.to_vec()
402 },
403 }
404 }
405
406 pub fn cond_rows(&self) -> usize {
408 self.segments
409 .iter()
410 .filter(|s| s.kind == Kind::Cond)
411 .map(|s| s.stop - s.start)
412 .sum()
413 }
414
415 fn segment(&self, kind: Kind) -> Segment {
416 *self
417 .segments
418 .iter()
419 .find(|s| s.kind == kind)
420 .expect("every layout carries all three streams")
421 }
422}
423
424struct Adaln {
427 w: Proj,
429 b: Vec<f32>,
430 table: Vec<f32>,
432 rank: usize,
433 out: usize,
434}
435
436impl Adaln {
437 fn load(model: &Arc<CmfModel>, prefix: &str) -> Result<Self, String> {
438 let w = Proj::from_model(model, &format!("{prefix}.weight"))?;
439 let table = crate::dit::cmf_f32(model, &format!("{prefix}.table"))?;
440 let b = crate::dit::cmf_f32(model, &format!("{prefix}.bias"))?;
441 let out = w.rows();
442 let rank = table.len() / CURVE_GRID;
443 Ok(Self { w, b, table, rank, out })
444 }
445
446 fn eval(&self, ts: &[f64], pool: Option<&Pool>) -> Vec<f32> {
450 let g = CURVE_GRID;
451 let mut coords = vec![0f32; ts.len() * self.rank];
452 for (i, &t) in ts.iter().enumerate() {
453 let p = (t.clamp(0.0, 1.0) * (g - 1) as f64) as f32;
457 let i0 = (p.floor() as usize).min(g - 2);
458 let f = p - i0 as f32;
459 for k in 0..self.rank {
460 let (a, b) = (self.table[i0 * self.rank + k], self.table[(i0 + 1) * self.rank + k]);
461 coords[i * self.rank + k] = a + (b - a) * f;
462 }
463 }
464 let mut out = vec![0f32; ts.len() * self.out];
465 self.w.matmat(&coords, ts.len(), &mut out, pool);
466 for row in out.chunks_exact_mut(self.out) {
467 for (v, &bv) in row.iter_mut().zip(&self.b) {
468 *v += bv;
469 }
470 }
471 out
472 }
473}
474
475struct Block {
476 norm1: Vec<f32>,
477 norm2: Vec<f32>,
478 qkv: Proj, out: Proj, q_norm: Vec<f32>,
481 k_norm: Vec<f32>,
482 fc1: Proj, fc2: Proj, adaln: Option<Adaln>,
485}
486
487pub(crate) const CURVE_GRID: usize = 1025;
488
489pub struct MiniMaxH3 {
490 video_patch: Proj,
491 video_patch_b: Vec<f32>,
492 audio_patch: Proj,
493 audio_patch_b: Vec<f32>,
494 condition: Proj,
495 condition_b: Vec<f32>,
496 refiner: Vec<Block>,
497 refiner_norm: Vec<f32>,
498 blocks: Vec<Block>,
499 final_norm: Vec<f32>,
500 final_adaln: Adaln,
501 video_out: Proj,
502 video_out_b: Vec<f32>,
503 audio_out: Proj,
504 audio_out_b: Vec<f32>,
505 inv_freq: Vec<f32>,
506 pool: Option<Arc<Pool>>,
507 pub hidden: usize,
508 pub heads: usize,
509 pub head_dim: usize,
510 pub ffn: usize,
511 pub latents_dim: usize,
512 pub audio_dim: usize,
513 pub text_dim: usize,
514 pub shift_video: f64,
515 pub shift_audio: f64,
516 eps: f64,
517 qk_eps: f64,
518 final_eps: f64,
519 pub cond_aug: f64,
523 pub cond_aug_audio: f64,
526}
527
528fn rms_norm_into(x: &[f32], w: &[f32], eps: f64, dst: &mut [f32]) {
529 let ss = x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64;
530 let inv = 1.0 / (ss + eps).sqrt();
531 for ((d, &v), &g) in dst.iter_mut().zip(x).zip(w) {
532 *d = (v as f64 * inv) as f32 * g;
533 }
534}
535
536fn silu(v: f32) -> f32 {
537 v / (1.0 + (-v).exp())
538}
539
540impl MiniMaxH3 {
541 pub fn from_cmf(model: &Arc<CmfModel>) -> Result<Self, String> {
542 let cfg: serde_json::Value = serde_json::from_slice(
543 model.tensor_bytes("dit.config_json").map_err(|e| e.to_string())?,
544 )
545 .map_err(|e| format!("dit.config_json: {e}"))?;
546 let u = |k: &str| cfg[k].as_u64().unwrap_or(0) as usize;
547 let f = |k: &str, d: f64| cfg[k].as_f64().unwrap_or(d);
548 let n_blocks = u("num_layers");
549 let n_refiner = u("token_refiner_num_layers");
550 let f32v = |n: &str| crate::dit::cmf_f32(model, n);
551
552 let load_block = |prefix: &str, with_adaln: bool| -> Result<Block, String> {
553 Ok(Block {
554 norm1: f32v(&format!("dit.{prefix}.norm1.weight"))?,
555 norm2: f32v(&format!("dit.{prefix}.norm2.weight"))?,
556 qkv: Proj::from_model(model, &format!("dit.{prefix}.attn.qkv_proj.weight"))?,
557 out: Proj::from_model(model, &format!("dit.{prefix}.attn.out_proj.weight"))?,
558 q_norm: f32v(&format!("dit.{prefix}.attn.q_norm.weight"))?,
559 k_norm: f32v(&format!("dit.{prefix}.attn.k_norm.weight"))?,
560 fc1: Proj::from_model(model, &format!("dit.{prefix}.mlp.fc1.weight"))?,
561 fc2: Proj::from_model(model, &format!("dit.{prefix}.mlp.fc2.weight"))?,
562 adaln: if with_adaln {
563 Some(Adaln::load(model, &format!("dit.{prefix}.adaln"))?)
564 } else {
565 None
566 },
567 })
568 };
569 let blocks = (0..n_blocks)
570 .map(|i| load_block(&format!("blocks.{i}"), true))
571 .collect::<Result<Vec<_>, _>>()?;
572 let refiner = (0..n_refiner)
573 .map(|i| load_block(&format!("token_refiner.blocks.{i}"), false))
574 .collect::<Result<Vec<_>, _>>()?;
575
576 Ok(Self {
577 video_patch: Proj::from_model(model, "dit.video_patch_proj.weight")?,
578 video_patch_b: f32v("dit.video_patch_proj.bias")?,
579 audio_patch: Proj::from_model(model, "dit.audio_patch_proj.weight")?,
580 audio_patch_b: f32v("dit.audio_patch_proj.bias")?,
581 condition: Proj::from_model(model, "dit.condition_proj.weight")?,
582 condition_b: f32v("dit.condition_proj.bias")?,
583 refiner,
584 refiner_norm: f32v("dit.token_refiner.final_norm.weight")?,
585 blocks,
586 final_norm: f32v("dit.final_layer.norm.weight")?,
587 final_adaln: Adaln::load(model, "dit.final_layer.adaln")?,
588 video_out: Proj::from_model(model, "dit.final_layer.video_out.weight")?,
589 video_out_b: f32v("dit.final_layer.video_out.bias")?,
590 audio_out: Proj::from_model(model, "dit.final_layer.audio_out.weight")?,
591 audio_out_b: f32v("dit.final_layer.audio_out.bias")?,
592 inv_freq: f32v("dit.rope_inv_freq")?,
593 pool: Pool::from_env(),
594 hidden: u("hidden_size"),
595 heads: u("num_attention_heads"),
596 head_dim: u("attention_head_dim"),
597 ffn: u("ffn_hidden_size"),
598 latents_dim: u("latents_dim"),
599 audio_dim: u("audio_latents_dim"),
600 text_dim: u("text_dim"),
601 shift_video: f("sigma_shift_video", 12.0),
602 shift_audio: f("sigma_shift_audio", 3.0),
603 eps: f("norm_eps", 1e-5),
604 qk_eps: f("qk_norm_eps", 1e-5),
605 final_eps: f("final_norm_eps", 1e-5),
606 cond_aug: VISUAL_COND_TIMESTEP,
607 cond_aug_audio: AUDIO_COND_TIMESTEP,
608 })
609 }
610
611 pub fn refine_text(&self, states: &[f32], n: usize) -> Vec<f32> {
615 let pool = self.pool.as_deref();
616 let mut h = vec![0f32; n * self.hidden];
617 self.condition.matmat(states, n, &mut h, pool);
618 for row in h.chunks_exact_mut(self.hidden) {
619 for (v, &b) in row.iter_mut().zip(&self.condition_b) {
620 *v += b;
621 }
622 }
623 let ids: Vec<[f64; 3]> = Vec::new();
624 for blk in &self.refiner {
625 self.block_forward(blk, &mut h, n, None, &ids, &[]);
626 }
627 let mut out = vec![0f32; n * self.hidden];
628 for (o, x) in out.chunks_exact_mut(self.hidden).zip(h.chunks_exact(self.hidden)) {
629 rms_norm_into(x, &self.refiner_norm, self.final_eps, o);
630 }
631 out
632 }
633
634 fn rope_angles(&self, pos: &[[f64; 3]]) -> Vec<f32> {
638 let k = self.inv_freq.len();
639 let mut out = Vec::with_capacity(pos.len() * 3 * k);
640 for p in pos {
641 for axis in 0..3 {
642 for j in 0..k {
643 out.push((p[axis] * self.inv_freq[j] as f64) as f32);
644 }
645 }
646 }
647 out
648 }
649
650 #[allow(clippy::too_many_arguments)]
661 fn norm_rope_w(
662 &self,
663 v: &mut [f32],
664 n: usize,
665 heads: usize,
666 w: &[f32],
667 angles: &[f32],
668 stride: usize,
669 off: usize,
670 ) {
671 let hd = self.head_dim;
672 let pairs = if angles.is_empty() { 0 } else { angles.len() / n };
673 let pool = self.pool.as_deref();
674 let ptr = SendPtr(v.as_mut_ptr());
675 let work = |lo: usize, hi: usize| {
676 for p in lo..hi {
677 for h in 0..heads {
678 let x = unsafe { ptr.row(p * stride + off + h * hd, hd) };
681 let ss = x.iter().map(|&a| (a as f64) * (a as f64)).sum::<f64>() / hd as f64;
682 let inv = 1.0 / (ss + self.qk_eps).sqrt();
683 for (d, &g) in x.iter_mut().zip(w) {
684 *d = (*d as f64 * inv) as f32 * g;
685 }
686 for j in 0..pairs {
687 let a = angles[p * pairs + j];
688 let (s, c) = a.sin_cos();
689 let (lo_v, hi_v) = (x[j], x[j + pairs]);
690 x[j] = lo_v * c - hi_v * s;
691 x[j + pairs] = lo_v * s + hi_v * c;
692 }
693 }
694 }
695 };
696 match pool {
697 Some(pl) => pl.run_rows(n, &work),
698 None => work(0, n),
699 }
700 }
701
702 fn attention(
708 &self,
709 qkv: &[f32],
710 n: usize,
711 attn: &mut [f32],
712 nr: Option<(&[f32], &[f32], &[f32], f32)>,
713 ) {
714 let t_repack = std::time::Instant::now();
715 let (nh, hd) = (self.heads, self.head_dim);
716 let inner = nh * hd;
717 let scale = 1.0 / (hd as f32).sqrt();
718 let pool = self.pool.as_deref();
719 if std::env::var("CMF_MMH3_ATTN").as_deref() != Ok("cpu")
727 && crate::gpu::enabled_here()
728 && n >= 256
729 {
730 if std::env::var("CMF_MMH3_ATTN").as_deref() != Ok("repack")
743 && crate::gpu::dit_attention_packed(qkv, nh, n, hd, scale, nr, attn)
744 {
745 return;
751 }
752 assert!(
753 nr.is_none(),
754 "device qk-norm was requested but the device path refused; \
755 the host loops below expect q/k already normalized"
756 );
757 let mut qh = vec![0f32; nh * n * hd];
758 let mut kh = vec![0f32; nh * n * hd];
759 let mut vh = vec![0f32; nh * n * hd];
760 {
761 let (pq, pk, pv) = (
762 SendPtr(qh.as_mut_ptr()),
763 SendPtr(kh.as_mut_ptr()),
764 SendPtr(vh.as_mut_ptr()),
765 );
766 pool_rows(pool, n, &|lo, hi| {
767 for p in lo..hi {
768 let base = p * 3 * inner;
769 for h in 0..nh {
770 let dst = (h * n + p) * hd;
771 unsafe {
774 pq.row(dst, hd).copy_from_slice(
775 &qkv[base + h * hd..base + (h + 1) * hd],
776 );
777 pk.row(dst, hd).copy_from_slice(
778 &qkv[base + inner + h * hd..base + inner + (h + 1) * hd],
779 );
780 pv.row(dst, hd).copy_from_slice(
781 &qkv[base + 2 * inner + h * hd
782 ..base + 2 * inner + (h + 1) * hd],
783 );
784 }
785 }
786 }
787 });
788 }
789 Self::prof(2, t_repack);
796 if crate::gpu::dit_attention(&qh, &kh, &vh, nh, nh, n, hd, scale, attn) {
799 return;
800 }
801 }
802 let mut qh = vec![0f32; n * hd];
803 let mut kh = vec![0f32; n * hd];
804 let mut vt = vec![0f32; hd * n];
805 let mut scores = vec![0f32; n * n];
806 let mut oh = vec![0f32; n * hd];
807 for h in 0..nh {
808 for p in 0..n {
809 let base = p * 3 * inner;
810 let qs = &qkv[base + h * hd..base + (h + 1) * hd];
811 for (d, &val) in qs.iter().enumerate() {
812 qh[p * hd + d] = val * scale;
813 }
814 kh[p * hd..(p + 1) * hd]
815 .copy_from_slice(&qkv[base + inner + h * hd..base + inner + (h + 1) * hd]);
816 let vs = &qkv[base + 2 * inner + h * hd..base + 2 * inner + (h + 1) * hd];
817 for (d, &val) in vs.iter().enumerate() {
818 vt[d * n + p] = val;
819 }
820 }
821 crate::fcd_ops::gemm_nt(&qh, &kh, &mut scores, n, hd, n, pool);
822 let sp = SendPtr(scores.as_mut_ptr());
823 let soft = |lo: usize, hi: usize| {
824 for r in lo..hi {
825 softmax_inplace(unsafe { sp.row(r * n, n) });
827 }
828 };
829 match pool {
830 Some(pl) => pl.run_rows(n, &soft),
831 None => soft(0, n),
832 }
833 crate::fcd_ops::gemm_nt(&scores, &vt, &mut oh, n, n, hd, pool);
834 for p in 0..n {
835 attn[p * inner + h * hd..p * inner + (h + 1) * hd]
836 .copy_from_slice(&oh[p * hd..(p + 1) * hd]);
837 }
838 }
839 }
840
841 fn prof(slot: usize, t: std::time::Instant) {
845 if !mmh3_prof_on() {
846 return;
847 }
848 MMH3_PROF[slot].fetch_add(t.elapsed().as_micros() as u64, std::sync::atomic::Ordering::Relaxed);
849 }
850
851 fn block_forward(
855 &self,
856 blk: &Block,
857 x: &mut [f32],
858 n: usize,
859 mods: Option<&[f32]>,
860 pos: &[[f64; 3]],
861 rows: &[u32],
862 ) {
863 let hs = self.hidden;
864 let pool = self.pool.as_deref();
865 let inner = self.heads * self.head_dim;
866 let angles = if pos.is_empty() {
867 Vec::new()
868 } else {
869 self.rope_angles(pos)
870 };
871
872 let t = std::time::Instant::now();
873 let mut xn = vec![0f32; n * hs];
874 self.norm_rows(&mut xn, x, n, hs, &blk.norm1, self.eps);
875 if let (Some(m), false) = (mods, rows.is_empty()) {
876 self.modulate(&mut xn, hs, m, rows, 0, 1);
877 }
878 Self::prof(0, t);
879 let t = std::time::Instant::now();
880 let t_qkv = std::time::Instant::now();
887 let qk_gpu = std::env::var("CMF_MMH3_QKNORM").as_deref() != Ok("cpu")
888 && crate::gpu::enabled_here()
889 && n >= 256;
890 if qk_gpu && std::env::var("CMF_MMH3_FUSEQKV").as_deref() != Ok("0") {
891 let fuse_out = std::env::var("CMF_MMH3_FUSEOUT").as_deref() != Ok("0");
896 if std::env::var("CMF_GPU_DEBUG").is_ok() {
897 static ONCE: std::sync::Once = std::sync::Once::new();
898 ONCE.call_once(|| {
899 eprintln!(
900 "mmh3 fuse-out gate: qkv_q={} out_q={} qkv_map={} out_map={}",
901 matches!(&blk.qkv, Proj::Q(_)),
902 matches!(&blk.out, Proj::Q(_)),
903 matches!(&blk.qkv, Proj::Q(q) if q.mapped_q4tp().is_some()),
904 matches!(&blk.out, Proj::Q(o) if o.mapped_q4tp().is_some()),
905 )
906 });
907 }
908 if let (true, Proj::Q(q), Proj::Q(o)) = (fuse_out, &blk.qkv, &blk.out) {
909 if let (Some((m, i)), Some((_, oi))) = (q.mapped_q4tp(), o.mapped_q4tp()) {
910 let mut proj = vec![0f32; n * hs];
911 if crate::gpu::dit_qkv_attn_out(
912 m,
913 i,
914 oi,
915 &xn,
916 n,
917 hs,
918 self.heads,
919 self.head_dim,
920 1.0 / (self.head_dim as f32).sqrt(),
921 (
922 &angles[..],
923 &blk.q_norm[..],
924 &blk.k_norm[..],
925 self.qk_eps as f32,
926 ),
927 &mut proj,
928 ) {
929 Self::prof(1, t_qkv);
930 let t_res = std::time::Instant::now();
931 self.residual(x, hs, &proj, mods, rows, 2);
932 Self::prof(8, t_res);
933 self.ffn_tail(blk, x, n, mods, rows);
934 return;
935 }
936 }
937 }
938 if let Proj::Q(q) = &blk.qkv {
939 if let Some((m, i)) = q.mapped_q4tp() {
940 let mut attn = vec![0f32; n * inner];
941 if crate::gpu::dit_qkv_attention(
942 m,
943 i,
944 &xn,
945 n,
946 hs,
947 self.heads,
948 self.head_dim,
949 1.0 / (self.head_dim as f32).sqrt(),
950 (
951 &angles[..],
952 &blk.q_norm[..],
953 &blk.k_norm[..],
954 self.qk_eps as f32,
955 ),
956 &mut attn,
957 ) {
958 Self::prof(1, t_qkv);
964 let t_out = std::time::Instant::now();
965 let mut proj = vec![0f32; n * hs];
966 blk.out.matmat(&attn, n, &mut proj, pool);
967 Self::prof(4, t_out);
968 let t_res = std::time::Instant::now();
969 self.residual(x, hs, &proj, mods, rows, 2);
970 Self::prof(8, t_res);
971 self.ffn_tail(blk, x, n, mods, rows);
972 return;
973 }
974 }
975 }
976 }
977 let mut qkv = vec![0f32; n * 3 * inner];
978 blk.qkv.matmat(&xn, n, &mut qkv, pool);
979 Self::prof(1, t);
980 let qk_on_gpu = qk_gpu;
993 let t = std::time::Instant::now();
994 if !qk_on_gpu {
995 for (which, w) in [(0usize, &blk.q_norm), (1usize, &blk.k_norm)] {
996 self.norm_rope_w(&mut qkv, n, self.heads, w, &angles, 3 * inner, which * inner);
997 }
998 }
999 Self::prof(2, t);
1000 let t = std::time::Instant::now();
1001 let mut attn = vec![0f32; n * inner];
1002 self.attention(
1003 &qkv,
1004 n,
1005 &mut attn,
1006 qk_on_gpu.then_some((
1007 &angles[..],
1008 &blk.q_norm[..],
1009 &blk.k_norm[..],
1010 self.qk_eps as f32,
1011 )),
1012 );
1013 Self::prof(3, t);
1014 let t = std::time::Instant::now();
1015 let mut proj = vec![0f32; n * hs];
1016 blk.out.matmat(&attn, n, &mut proj, pool);
1017 Self::prof(4, t);
1018 let t = std::time::Instant::now();
1022 self.residual(x, hs, &proj, mods, rows, 2);
1023 Self::prof(8, t);
1024
1025 self.ffn_tail(blk, x, n, mods, rows);
1026 }
1027
1028 fn ffn_tail(
1033 &self,
1034 blk: &Block,
1035 x: &mut [f32],
1036 n: usize,
1037 mods: Option<&[f32]>,
1038 rows: &[u32],
1039 ) {
1040 let hs = self.hidden;
1041 let pool = self.pool.as_deref();
1042 let mut xn = vec![0f32; n * hs];
1043 let mut proj = vec![0f32; n * hs];
1044 let t = std::time::Instant::now();
1045 self.norm_rows(&mut xn, x, n, hs, &blk.norm2, self.eps);
1046 if let (Some(m), false) = (mods, rows.is_empty()) {
1047 self.modulate(&mut xn, hs, m, rows, 3, 4);
1048 }
1049 Self::prof(0, t);
1050 let t = std::time::Instant::now();
1051 if std::env::var("CMF_MMH3_FFN").as_deref() != Ok("cpu")
1056 && crate::gpu::enabled_here()
1057 && n >= 64
1058 {
1059 if let (Proj::Q(q1), Proj::Q(q2)) = (&blk.fc1, &blk.fc2) {
1060 if let (Some((m, i1)), Some((_, i2))) =
1061 (q1.mapped_q4tp(), q2.mapped_q4tp())
1062 {
1063 let mut fout = vec![0f32; n * hs];
1064 if crate::gpu::q4tp_ffn_packed(
1065 m, i1, i2, &xn, n, hs, self.ffn, None, &mut fout,
1066 ) {
1067 Self::prof(5, t);
1068 self.residual(x, hs, &fout, mods, rows, 5);
1069 return;
1070 }
1071 }
1072 }
1073 }
1074 let mut gu = vec![0f32; n * 2 * self.ffn];
1075 blk.fc1.matmat(&xn, n, &mut gu, pool);
1076 Self::prof(5, t);
1077 let t = std::time::Instant::now();
1078 let ffn = self.ffn;
1080 let mut act = vec![0f32; n * ffn];
1081 let ap = SendPtr(act.as_mut_ptr());
1082 pool_rows(pool, n, &|lo, hi| {
1083 for p in lo..hi {
1084 let row = &gu[p * 2 * ffn..(p + 1) * 2 * ffn];
1085 let (g, up) = row.split_at(ffn);
1086 for (o, (&a, &b)) in unsafe { ap.row(p * ffn, ffn) }
1088 .iter_mut()
1089 .zip(g.iter().zip(up))
1090 {
1091 *o = silu(a) * b;
1092 }
1093 }
1094 });
1095 Self::prof(6, t);
1096 let t = std::time::Instant::now();
1097 blk.fc2.matmat(&act, n, &mut proj, pool);
1098 Self::prof(7, t);
1099 self.residual(x, hs, &proj, mods, rows, 5);
1100 }
1101
1102
1103 fn norm_rows(&self, dst: &mut [f32], src: &[f32], n: usize, hs: usize, w: &[f32], eps: f64) {
1108 let ptr = SendPtr(dst.as_mut_ptr());
1109 pool_rows(self.pool.as_deref(), n, &|lo, hi| {
1110 for p in lo..hi {
1111 rms_norm_into(&src[p * hs..(p + 1) * hs], w, eps, unsafe {
1113 ptr.row(p * hs, hs)
1114 });
1115 }
1116 });
1117 }
1118
1119 fn modulate(
1121 &self,
1122 x: &mut [f32],
1123 hs: usize,
1124 mods: &[f32],
1125 rows: &[u32],
1126 shift_e: usize,
1127 scale_e: usize,
1128 ) {
1129 let stride = EXPAND * hs;
1130 let ptr = SendPtr(x.as_mut_ptr());
1131 pool_rows(self.pool.as_deref(), rows.len(), &|lo, hi| {
1132 for p in lo..hi {
1133 let base = rows[p] as usize * stride;
1134 let shift = &mods[base + shift_e * hs..base + (shift_e + 1) * hs];
1135 let scale = &mods[base + scale_e * hs..base + (scale_e + 1) * hs];
1136 for ((v, &sc), &sh) in unsafe { ptr.row(p * hs, hs) }
1138 .iter_mut()
1139 .zip(scale)
1140 .zip(shift)
1141 {
1142 *v = *v * (1.0 + sc) + sh;
1143 }
1144 }
1145 });
1146 }
1147
1148 fn residual(
1151 &self,
1152 x: &mut [f32],
1153 hs: usize,
1154 other: &[f32],
1155 mods: Option<&[f32]>,
1156 rows: &[u32],
1157 gate_e: usize,
1158 ) {
1159 let n = x.len() / hs;
1160 let stride = EXPAND * hs;
1161 let ptr = SendPtr(x.as_mut_ptr());
1162 let gated = mods.filter(|_| !rows.is_empty());
1163 pool_rows(self.pool.as_deref(), n, &|lo, hi| {
1164 for p in lo..hi {
1165 let row = unsafe { ptr.row(p * hs, hs) };
1167 let src = &other[p * hs..(p + 1) * hs];
1168 match gated {
1169 Some(m) => {
1170 let base = rows[p] as usize * stride;
1171 let gate = &m[base + gate_e * hs..base + (gate_e + 1) * hs];
1172 for ((v, &g), &o) in row.iter_mut().zip(gate).zip(src) {
1173 *v += g * o;
1174 }
1175 }
1176 None => {
1177 for (v, &o) in row.iter_mut().zip(src) {
1178 *v += o;
1179 }
1180 }
1181 }
1182 }
1183 });
1184 }
1185
1186 pub fn forward(
1193 &self,
1194 layout: &Layout,
1195 text: &[f32],
1196 video: &[f32],
1197 audio: &[f32],
1198 sigma_v: f64,
1199 cond: &[Vec<f32>],
1200 ) -> (Vec<f32>, Vec<f32>) {
1201 let hs = self.hidden;
1202 let pool = self.pool.as_deref();
1203 let sigma_v = sigma_v.max(1e-6);
1204 let t_v = 1.0 - sigma_v;
1205 let t_a = 1.0 - time_shift_sigma(sigma_v, self.shift_video, self.shift_audio);
1206
1207 let has_cond = layout
1212 .segments
1213 .iter()
1214 .any(|s| matches!(s.kind, Kind::Cond | Kind::RefImg));
1215 let has_ref_audio = layout.segments.iter().any(|s| s.kind == Kind::RefAudio);
1216 let t_cond_a = t_a.max(self.cond_aug_audio);
1219 let t_cond = t_v.max(self.cond_aug);
1224 let mut ts = vec![t_v, t_a];
1225 if has_cond {
1226 ts.push(t_cond);
1227 }
1228 if has_ref_audio {
1229 ts.push(t_cond_a);
1230 }
1231 ts.sort_by(|a, b| a.partial_cmp(b).unwrap());
1232 ts.dedup();
1233 let row_of = |t: f64| ts.iter().position(|&x| x == t).unwrap();
1234 let (row_v, row_a) = (row_of(t_v), row_of(t_a));
1235 let row_c = if has_cond { row_of(t_cond) } else { 0 };
1236 let row_ca = if has_ref_audio { row_of(t_cond_a) } else { 0 };
1237
1238 let mut rows = vec![0u32; layout.seq_len];
1242 for s in &layout.segments {
1243 match s.kind {
1244 Kind::Text => {
1245 for (i, v) in rows[s.start..s.stop].iter_mut().enumerate() {
1246 let tag = *layout.text_tags.get(i).unwrap_or(&(TAG_TEXT as u8));
1247 *v = (row_v * MODALITIES + tag as usize) as u32;
1248 }
1249 }
1250 Kind::Cond | Kind::RefImg => {
1254 for v in rows[s.start..s.stop].iter_mut() {
1255 *v = (row_c * MODALITIES + TAG_VIDEO) as u32;
1256 }
1257 }
1258 Kind::RefAudio => {
1259 for v in rows[s.start..s.stop].iter_mut() {
1260 *v = (row_ca * MODALITIES + TAG_AUDIO) as u32;
1261 }
1262 }
1263 Kind::Video => {
1264 for v in rows[s.start..s.stop].iter_mut() {
1265 *v = (row_v * MODALITIES + TAG_VIDEO) as u32;
1266 }
1267 }
1268 Kind::Audio => {
1269 for v in rows[s.start..s.stop].iter_mut() {
1270 *v = (row_a * MODALITIES + TAG_AUDIO) as u32;
1271 }
1272 }
1273 }
1274 }
1275
1276 let v_rows = patchify_video(video, self.latents_dim, layout.latent_t, layout.lat_h, layout.lat_w);
1278 let v_n = v_rows.len() / (self.latents_dim * 4);
1279 let a_rows = pack_audio(audio, self.audio_dim, layout.audio_t);
1280 let vd = self.latents_dim * 4;
1283 let cond_rows: Vec<Vec<f32>> = cond
1284 .iter()
1285 .enumerate()
1286 .map(|(i, z)| {
1287 let mut r = patchify_video(z, self.latents_dim, 1, layout.lat_h, layout.lat_w);
1288 if self.cond_aug < 1.0 {
1289 let noise = crate::videogen::gauss_pub(r.len(), 0x5EED ^ i as u64);
1294 let a = self.cond_aug as f32;
1295 for (v, n) in r.iter_mut().zip(&noise) {
1296 *v = a * *v + (1.0 - a) * n;
1297 }
1298 }
1299 r
1300 })
1301 .collect();
1302
1303 let mut h = vec![0f32; layout.seq_len * hs];
1304 let mut ci = 0usize;
1305 for s in layout.segments.iter().filter(|s| s.kind == Kind::Cond) {
1306 let n = s.stop - s.start;
1307 let r = cond_rows
1308 .get(ci)
1309 .unwrap_or_else(|| panic!("layout has {} cond segments, {} latents given", ci + 1, cond_rows.len()));
1310 self.video_patch
1311 .matmat(r, n, &mut h[s.start * hs..s.stop * hs], pool);
1312 for row in h[s.start * hs..s.stop * hs].chunks_exact_mut(hs) {
1313 for (v, &b) in row.iter_mut().zip(&self.video_patch_b) {
1314 *v += b;
1315 }
1316 }
1317 ci += 1;
1318 }
1319 let vseg = layout.segment(Kind::Video);
1320 let aseg = layout.segment(Kind::Audio);
1321 let tseg = layout.segment(Kind::Text);
1322 h[tseg.start * hs..tseg.stop * hs].copy_from_slice(&text[..(tseg.stop - tseg.start) * hs]);
1323 self.video_patch.matmat(&v_rows, v_n, &mut h[vseg.start * hs..vseg.stop * hs], pool);
1324 self.audio_patch.matmat(
1325 &a_rows,
1326 aseg.stop - aseg.start,
1327 &mut h[aseg.start * hs..aseg.stop * hs],
1328 pool,
1329 );
1330 for row in h[vseg.start * hs..vseg.stop * hs].chunks_exact_mut(hs) {
1331 for (v, &b) in row.iter_mut().zip(&self.video_patch_b) {
1332 *v += b;
1333 }
1334 }
1335 for row in h[aseg.start * hs..aseg.stop * hs].chunks_exact_mut(hs) {
1336 for (v, &b) in row.iter_mut().zip(&self.audio_patch_b) {
1337 *v += b;
1338 }
1339 }
1340
1341 for (i, blk) in self.blocks.iter().enumerate() {
1343 let mods = blk.adaln.as_ref().unwrap().eval(&ts, pool);
1344 self.block_forward(blk, &mut h, layout.seq_len, Some(&mods), &layout.pos, &rows);
1345 if std::env::var_os("CMF_DIT_PROGRESS").is_some() {
1346 eprint!("\r block {}/{}", i + 1, self.blocks.len());
1347 }
1348 }
1349
1350 let fm = self.final_adaln.eval(&ts, pool);
1352 let mut video_out = vec![0f32; (vseg.stop - vseg.start) * vd];
1353 let mut audio_out = vec![0f32; (aseg.stop - aseg.start) * self.audio_dim];
1354 for (seg, row, w, b, dst, dim) in [
1355 (vseg, row_v, &self.video_out, &self.video_out_b, &mut video_out, vd),
1356 (aseg, row_a, &self.audio_out, &self.audio_out_b, &mut audio_out, self.audio_dim),
1357 ] {
1358 let n = seg.stop - seg.start;
1359 let mut hn = vec![0f32; n * hs];
1360 for (o, src) in hn.chunks_exact_mut(hs).zip(h[seg.start * hs..seg.stop * hs].chunks_exact(hs)) {
1361 rms_norm_into(src, &self.final_norm, self.final_eps, o);
1362 }
1363 let shift = &fm[row * 2 * hs..row * 2 * hs + hs];
1366 let scale = &fm[row * 2 * hs + hs..(row + 1) * 2 * hs];
1367 for r in hn.chunks_exact_mut(hs) {
1368 for ((v, &sc), &sh) in r.iter_mut().zip(scale).zip(shift) {
1369 *v = *v * (1.0 + sc) + sh;
1370 }
1371 }
1372 w.matmat(&hn, n, dst, pool);
1373 for r in dst.chunks_exact_mut(dim) {
1374 for (v, &bv) in r.iter_mut().zip(b.iter()) {
1375 *v += bv;
1376 }
1377 }
1378 }
1379
1380 let video = unpatchify_video(
1384 &video_out,
1385 self.latents_dim,
1386 layout.latent_t,
1387 layout.lat_h,
1388 layout.lat_w,
1389 );
1390 let audio = unpack_audio(&audio_out, self.audio_dim, layout.audio_t);
1391 (
1392 video.iter().map(|&v| -v).collect(),
1393 audio.iter().map(|&v| -v).collect(),
1394 )
1395 }
1396}
1397
1398fn pool_rows(pool: Option<&Pool>, n: usize, f: &(dyn Fn(usize, usize) + Sync)) {
1402 match pool {
1403 Some(p) => p.run_rows(n, f),
1404 None => f(0, n),
1405 }
1406}
1407
1408pub fn patchify_video(x: &[f32], c: usize, t: usize, h: usize, w: usize) -> Vec<f32> {
1413 let (ph, pw) = (h / 2, w / 2);
1414 let mut out = vec![0f32; t * ph * pw * c * 4];
1415 let mut i = 0;
1416 for ti in 0..t {
1417 for hi in 0..ph {
1418 for wi in 0..pw {
1419 for ci in 0..c {
1420 for p in 0..2 {
1421 for q in 0..2 {
1422 out[i] = x[((ci * t + ti) * h + hi * 2 + p) * w + wi * 2 + q];
1423 i += 1;
1424 }
1425 }
1426 }
1427 }
1428 }
1429 }
1430 out
1431}
1432
1433pub fn unpatchify_video(rows: &[f32], c: usize, t: usize, h: usize, w: usize) -> Vec<f32> {
1435 let (ph, pw) = (h / 2, w / 2);
1436 let mut out = vec![0f32; c * t * h * w];
1437 let mut i = 0;
1438 for ti in 0..t {
1439 for hi in 0..ph {
1440 for wi in 0..pw {
1441 for ci in 0..c {
1442 for p in 0..2 {
1443 for q in 0..2 {
1444 out[((ci * t + ti) * h + hi * 2 + p) * w + wi * 2 + q] = rows[i];
1445 i += 1;
1446 }
1447 }
1448 }
1449 }
1450 }
1451 }
1452 out
1453}
1454
1455pub fn pack_audio(x: &[f32], c: usize, t: usize) -> Vec<f32> {
1458 let mut out = vec![0f32; 2 * t * c];
1459 for ch in 0..2 {
1460 for ti in 0..t {
1461 for ci in 0..c {
1462 out[(ch * t + ti) * c + ci] = x[(ci * 2 + ch) * t + ti];
1463 }
1464 }
1465 }
1466 out
1467}
1468
1469pub fn unpack_audio(rows: &[f32], c: usize, t: usize) -> Vec<f32> {
1471 let mut out = vec![0f32; c * 2 * t];
1472 for ch in 0..2 {
1473 for ti in 0..t {
1474 for ci in 0..c {
1475 out[(ci * 2 + ch) * t + ti] = rows[(ch * t + ti) * c + ci];
1476 }
1477 }
1478 }
1479 out
1480}
1481
1482struct SendPtr(*mut f32);
1485unsafe impl Send for SendPtr {}
1486unsafe impl Sync for SendPtr {}
1487impl SendPtr {
1488 #[allow(clippy::mut_from_ref)]
1490 unsafe fn row(&self, off: usize, len: usize) -> &mut [f32] {
1491 unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) }
1492 }
1493}
1494
1495fn softmax_inplace(row: &mut [f32]) {
1496 let mx = row.iter().cloned().fold(f32::MIN, f32::max);
1497 let mut den = 0f32;
1498 for r in row.iter_mut() {
1499 *r = (*r - mx).exp();
1500 den += *r;
1501 }
1502 if den > 0.0 {
1503 let inv = 1.0 / den;
1504 for r in row.iter_mut() {
1505 *r *= inv;
1506 }
1507 }
1508}
1509
1510#[cfg(test)]
1511mod tests {
1512 use super::*;
1513
1514 #[test]
1515 fn schedule_remap_is_an_involution() {
1516 for &s in &[1e-3, 0.25, 0.5, 0.8, 0.972_973, 1.0] {
1517 let a = time_shift_sigma(s, 12.0, 3.0);
1518 let back = time_shift_sigma(a, 3.0, 12.0);
1519 assert!((back - s).abs() < 1e-9, "{s} -> {a} -> {back}");
1520 }
1521 }
1522
1523 #[test]
1524 fn slope_matches_a_finite_difference() {
1525 let h = 1e-6;
1526 for &s in &[0.2, 0.5, 0.9] {
1527 let num = (time_shift_sigma(s + h, 12.0, 3.0) - time_shift_sigma(s - h, 12.0, 3.0))
1528 / (2.0 * h);
1529 let got = time_shift_slope(s, 12.0, 3.0);
1530 assert!((num - got).abs() < 1e-5, "{s}: {num} vs {got}");
1531 }
1532 }
1533
1534 #[test]
1535 fn patchify_round_trips() {
1536 let (c, t, h, w) = (3usize, 2usize, 4usize, 6usize);
1537 let x: Vec<f32> = (0..c * t * h * w).map(|i| i as f32).collect();
1538 let rows = patchify_video(&x, c, t, h, w);
1539 assert_eq!(rows.len(), t * (h / 2) * (w / 2) * c * 4);
1540 assert_eq!(unpatchify_video(&rows, c, t, h, w), x);
1541 }
1542
1543 #[test]
1544 fn audio_pack_round_trips() {
1545 let (c, t) = (5usize, 7usize);
1546 let x: Vec<f32> = (0..c * 2 * t).map(|i| i as f32).collect();
1547 let rows = pack_audio(&x, c, t);
1548 assert_eq!(unpack_audio(&rows, c, t), x);
1549 }
1550
1551 #[test]
1552 fn keyframes_sit_between_the_text_and_the_audio() {
1553 let (tl, lt, lh, lw, at) = (8usize, 3usize, 8usize, 12usize, 5usize);
1554 let base = Layout::t2va(tl, lt, lh, lw, at);
1555 let l = Layout::fl2va(tl, lt, lh, lw, at, &[(0, 39), (38, 39)], &[]);
1557 assert_eq!(l.cond_rows(), 2 * l.frame_rows);
1558 assert_eq!(l.seq_len, base.seq_len + 2 * l.frame_rows);
1559 let kinds: Vec<_> = l.segments.iter().map(|s| s.kind).collect();
1560 assert_eq!(
1561 kinds,
1562 vec![Kind::Text, Kind::Cond, Kind::Cond, Kind::Audio, Kind::Video]
1563 );
1564 let (a0, v0) = (l.segment(Kind::Audio), l.segment(Kind::Video));
1567 let (ba, bv) = (base.segment(Kind::Audio), base.segment(Kind::Video));
1568 assert_eq!(l.pos[a0.start][0], base.pos[ba.start][0]);
1569 assert_eq!(l.pos[v0.start][0], base.pos[bv.start][0]);
1570 let c: Vec<_> = l.segments.iter().filter(|s| s.kind == Kind::Cond).collect();
1573 assert_eq!(l.pos[c[0].start][0], tl as f64);
1574 let spans: f64 = (0..lt).map(|k| FRAME_RESCALE * FRAME_PER_TOKEN[k % 5]).sum();
1575 assert!((l.pos[c[1].start][0] - (tl as f64 + spans - FRAME_RESCALE)).abs() < 1e-12);
1576 assert_eq!(l.pos[c[0].start][1], l.pos[v0.start][1]);
1578 assert_eq!(l.pos[c[0].start][2], l.pos[v0.start][2]);
1579 }
1580
1581 #[test]
1582 fn layout_places_the_target_streams_last() {
1583 let l = Layout::t2va(8, 3, 8, 12, 5);
1584 assert_eq!(l.frame_rows, 4 * 6);
1585 assert_eq!(l.seq_len, 8 + 2 * 5 + 3 * 24);
1586 assert_eq!(l.segments[0].kind, Kind::Text);
1587 assert_eq!(l.segments[1].kind, Kind::Audio);
1588 assert_eq!(l.segments[2].kind, Kind::Video);
1589 let v = l.segment(Kind::Video);
1591 let t0 = l.pos[v.start][0];
1592 let t1 = l.pos[v.start + l.frame_rows][0];
1593 assert!((t1 - t0 - FRAME_RESCALE).abs() < 1e-12);
1594 }
1595}