Skip to main content

maolan_generate/acestep/
dit.rs

1//! ACE-Step 1.5 turbo DiT (`AceStepDiTModel`) ported to Burn, plus the turbo
2//! sampler (explicit Euler, no CFG, 8-step shift-3 schedule).
3//!
4//! Ground truth: `modeling_acestep_v15_turbo.py` (`AceStepDiTModel`,
5//! `AceStepDiTLayer`, `AceStepAttention`, `TimestepEmbedding`). Key behaviors:
6//!
7//! - Patchify: `proj_in` Conv1d(in_channels → hidden, k=patch, stride=patch,
8//!   bias) over frames zero-padded to a multiple of `patch_size`;
9//!   `proj_out` ConvTranspose1d(hidden → acoustic_dim, k=patch, stride=patch,
10//!   bias) de-patchifies and the output is cropped back to the input length.
11//! - AdaLN chunk order per layer: (shift_msa, scale_msa, gate_msa,
12//!   c_shift_msa, c_scale_msa, c_gate_msa) from
13//!   `scale_shift_table + timestep_proj` (each [B, 1, hidden]); residual gate
14//!   multiplies the sublayer output; cross-attention has NO modulation/gate.
15//! - Final modulation uses the summed `temb` ([B, hidden], NOT the 6× proj)
16//!   with table order (shift, scale).
17//! - Timestep embedding: scale 1000, max_period 10000, COS-then-SIN halves.
18//!   `time_embed_r` is always evaluated at `t - r == 0` at inference and
19//!   contributes a nonzero constant — it must not be dropped.
20//! - All attention is BIDIRECTIONAL. Sliding layers attend within the band
21//!   |i − j| ≤ sliding_window; full layers attend to everything. The DiT
22//!   ignores padding masks entirely, including the encoder tail.
23//! - RoPE (theta 1e6, duplicated halves, rotate_half) is applied AFTER the
24//!   per-head q/k RMSNorm, self-attention only. Cross-attention applies no
25//!   RoPE and attends to the full encoder sequence.
26//!
27//! # Canonical burnpack tensor names
28//!
29//! The offline converter renames the official `decoder.*` checkpoint tensors
30//! to exactly these module paths:
31//!
32//! - `proj_in.conv.{weight,bias}` — Conv1d [hidden, in_channels, patch], [hidden]
33//! - `proj_out.conv.{weight,bias}` — ConvTranspose1d [hidden, acoustic, patch], [acoustic]
34//! - `time_embed.linear_1.{weight,bias}` — 256 → hidden
35//! - `time_embed.linear_2.{weight,bias}` — hidden → hidden
36//! - `time_embed.time_proj.{weight,bias}` — hidden → 6 × hidden
37//! - `time_embed_r.linear_1.{weight,bias}`, `time_embed_r.linear_2.{weight,bias}`,
38//!   `time_embed_r.time_proj.{weight,bias}` — same trio
39//! - `condition_embedder.{weight,bias}` — hidden → hidden
40//! - `layers.{i}.self_attn_norm.weight`
41//! - `layers.{i}.self_attn.{q,k,v,o}_proj.weight` — q/o: hidden↔hidden,
42//!   k/v: hidden ↔ kv_heads × head_dim, all bias-free
43//! - `layers.{i}.self_attn.{q,k}_norm.weight` — [head_dim]
44//! - `layers.{i}.cross_attn_norm.weight`
45//! - `layers.{i}.cross_attn.{q,k,v,o}_proj.weight`, `layers.{i}.cross_attn.{q,k}_norm.weight`
46//! - `layers.{i}.mlp_norm.weight`
47//! - `layers.{i}.mlp.{gate,up,down}_proj.weight` — hidden ↔ intermediate, bias-free
48//! - `layers.{i}.scale_shift_table` — [1, 6, hidden]
49//! - `norm_out.weight`
50//! - `scale_shift_table` — [1, 2, hidden]
51
52use std::path::Path;
53
54use anyhow::{Context, Result};
55use burn::module::{Module, Param};
56use burn::nn::conv::{Conv1d, Conv1dConfig, ConvTranspose1d, ConvTranspose1dConfig};
57use burn::nn::{Linear, LinearConfig, LinearLayout};
58use burn::prelude::Backend;
59use burn::tensor::activation::{silu, softmax};
60use burn::tensor::{DType, Distribution, Tensor, TensorData};
61use burn_store::{BurnpackStore, ModuleSnapshot};
62
63use crate::acestep::config::AceStepConfig;
64use crate::acestep::qwen3::Qwen3RmsNorm;
65
66/// Turbo inference schedule (shift = 3.0 transform of uniform eighths), t: 1.0 → 0.3.
67/// Spec values (f64): [1.0, 0.9545454545454546, 0.9, 0.8333333333333334, 0.75,
68/// 0.6428571428571429, 0.5, 0.3]; the literals below round to the same f32.
69pub const TURBO_TIMESTEPS: [f32; 8] = [
70    1.0,
71    0.954_545_44,
72    0.9,
73    0.833_333_3,
74    0.75,
75    0.642_857_13,
76    0.5,
77    0.3,
78];
79
80/// SFT/base inference schedule (shift = 1.0, i.e. uniform), 50 steps,
81/// t: 1.0 → 0.02 (`t_i = 1 − i/50`, i in 0..50).
82pub const SFT_TIMESTEPS: [f32; 50] = {
83    let mut schedule = [0.0_f32; 50];
84    let mut i = 0;
85    while i < 50 {
86        schedule[i] = 1.0 - i as f32 / 50.0;
87        i += 1;
88    }
89    schedule
90};
91
92/// Sinusoidal timestep embedding width (`TimestepEmbedding in_channels`).
93const TIME_EMBED_CHANNELS: usize = 256;
94/// Timestep scale applied before the sinusoidal projection.
95const TIME_EMBED_SCALE: f32 = 1000.0;
96/// Maximum period of the sinusoidal timestep frequencies.
97const TIME_EMBED_MAX_PERIOD: f32 = 10_000.0;
98/// Additive attention mask value for disallowed positions (fp32 "-inf").
99const MASK_MIN: f32 = -1.0e30;
100
101/// Per-head GQA attention with q/k RMSNorm; self-attn applies RoPE,
102/// cross-attn consumes precomputed encoder K/V (no RoPE, no sliding).
103#[derive(Module, Debug)]
104pub struct AceStepAttention<B: Backend> {
105    pub q_proj: Linear<B>,
106    pub k_proj: Linear<B>,
107    pub v_proj: Linear<B>,
108    pub o_proj: Linear<B>,
109    pub q_norm: Qwen3RmsNorm<B>,
110    pub k_norm: Qwen3RmsNorm<B>,
111    #[module(skip)]
112    meta: AttentionMeta,
113}
114
115#[derive(Clone, Debug)]
116struct AttentionMeta {
117    num_heads: usize,
118    num_kv_heads: usize,
119    head_dim: usize,
120    scaling: f32,
121}
122
123impl<B: Backend> AceStepAttention<B> {
124    fn new(config: &AceStepConfig, device: &B::Device) -> Self {
125        let head_dim = config.head_dim;
126        Self {
127            q_proj: linear_no_bias(
128                device,
129                config.hidden_size,
130                config.num_attention_heads * head_dim,
131            ),
132            k_proj: linear_no_bias(
133                device,
134                config.hidden_size,
135                config.num_key_value_heads * head_dim,
136            ),
137            v_proj: linear_no_bias(
138                device,
139                config.hidden_size,
140                config.num_key_value_heads * head_dim,
141            ),
142            o_proj: linear_no_bias(
143                device,
144                config.num_attention_heads * head_dim,
145                config.hidden_size,
146            ),
147            q_norm: Qwen3RmsNorm::new(device, head_dim, config.rms_norm_eps),
148            k_norm: Qwen3RmsNorm::new(device, head_dim, config.rms_norm_eps),
149            meta: AttentionMeta {
150                num_heads: config.num_attention_heads,
151                num_kv_heads: config.num_key_value_heads,
152                head_dim,
153                scaling: (head_dim as f32).powf(-0.5),
154            },
155        }
156    }
157
158    /// Cross-attention K/V projection of (already condition-embedded) encoder
159    /// states: k_norm applied to K, heads swapped to [batch, heads, seq, dim]
160    /// and KV heads repeated to the full query head count. Computed once per
161    /// generation and reused across all sampler steps.
162    pub fn project_kv(&self, encoder: &Tensor<B, 3>) -> (Tensor<B, 4>, Tensor<B, 4>) {
163        let [batch, seq_len, _] = encoder.dims();
164        let kv_heads = self.meta.num_kv_heads;
165        let head_dim = self.meta.head_dim;
166
167        let k = self
168            .k_norm
169            .forward(
170                self.k_proj
171                    .forward(encoder.clone())
172                    .reshape([batch, seq_len, kv_heads, head_dim]),
173            )
174            .swap_dims(1, 2);
175        let v = self
176            .v_proj
177            .forward(encoder.clone())
178            .reshape([batch, seq_len, kv_heads, head_dim])
179            .swap_dims(1, 2);
180
181        let repeats = self.meta.num_heads / self.meta.num_kv_heads;
182        (repeat_kv(k, repeats), repeat_kv(v, repeats))
183    }
184
185    /// Bidirectional self-attention with RoPE and an optional additive
186    /// sliding-window mask of shape [seq, seq].
187    pub fn forward_self(
188        &self,
189        hidden: Tensor<B, 3>,
190        cos: &Tensor<B, 4>,
191        sin: &Tensor<B, 4>,
192        mask: Option<&Tensor<B, 2>>,
193    ) -> Tensor<B, 3> {
194        let [batch, seq_len, _] = hidden.dims();
195        let num_heads = self.meta.num_heads;
196        let kv_heads = self.meta.num_kv_heads;
197        let head_dim = self.meta.head_dim;
198
199        let q = self.q_norm.forward(
200            self.q_proj
201                .forward(hidden.clone())
202                .reshape([batch, seq_len, num_heads, head_dim]),
203        );
204        let k = self.k_norm.forward(
205            self.k_proj
206                .forward(hidden.clone())
207                .reshape([batch, seq_len, kv_heads, head_dim]),
208        );
209        let v = self
210            .v_proj
211            .forward(hidden)
212            .reshape([batch, seq_len, kv_heads, head_dim]);
213
214        // RoPE post q/k-norm, self-attention only.
215        let q = apply_rotary_pos_emb(q, cos, sin).swap_dims(1, 2);
216        let k = apply_rotary_pos_emb(k, cos, sin).swap_dims(1, 2);
217        let v = v.swap_dims(1, 2);
218
219        let repeats = num_heads / kv_heads;
220        let k = repeat_kv(k, repeats);
221        let v = repeat_kv(v, repeats);
222
223        let attended = self.attend(q, k, v, mask);
224        let attended = attended
225            .swap_dims(1, 2)
226            .reshape([batch, seq_len, num_heads * head_dim]);
227        self.o_proj.forward(attended)
228    }
229
230    /// Bidirectional cross-attention over the full encoder sequence using
231    /// precomputed K/V of shape [batch, heads, enc_seq, head_dim].
232    pub fn forward_cross(
233        &self,
234        hidden: Tensor<B, 3>,
235        key: &Tensor<B, 4>,
236        value: &Tensor<B, 4>,
237    ) -> Tensor<B, 3> {
238        let [batch, seq_len, _] = hidden.dims();
239        let num_heads = self.meta.num_heads;
240        let head_dim = self.meta.head_dim;
241
242        let q = self
243            .q_norm
244            .forward(
245                self.q_proj
246                    .forward(hidden)
247                    .reshape([batch, seq_len, num_heads, head_dim]),
248            )
249            .swap_dims(1, 2);
250
251        let attended = self.attend(q, key.clone(), value.clone(), None);
252        let attended = attended
253            .swap_dims(1, 2)
254            .reshape([batch, seq_len, num_heads * head_dim]);
255        self.o_proj.forward(attended)
256    }
257
258    /// scores = q @ kᵀ · head_dim^-0.5 (+ optional additive mask), softmax in
259    /// fp32, then @ v. Inputs are [batch, heads, seq, head_dim].
260    fn attend(
261        &self,
262        q: Tensor<B, 4>,
263        k: Tensor<B, 4>,
264        v: Tensor<B, 4>,
265        mask: Option<&Tensor<B, 2>>,
266    ) -> Tensor<B, 4> {
267        let dtype = q.dtype();
268        let mut scores = q.matmul(k.swap_dims(2, 3)).mul_scalar(self.meta.scaling);
269        if let Some(mask) = mask {
270            let [rows, cols] = mask.dims();
271            scores = scores + mask.clone().reshape([1, 1, rows, cols]);
272        }
273        let weights = softmax(scores.cast(DType::F32), 3).cast(dtype);
274        weights.matmul(v)
275    }
276}
277
278/// SwiGLU MLP: down(silu(gate(x)) * up(x)), all projections bias-free.
279#[derive(Module, Debug)]
280pub struct AceStepMlp<B: Backend> {
281    pub gate_proj: Linear<B>,
282    pub up_proj: Linear<B>,
283    pub down_proj: Linear<B>,
284}
285
286impl<B: Backend> AceStepMlp<B> {
287    fn new(config: &AceStepConfig, device: &B::Device) -> Self {
288        Self {
289            gate_proj: linear_no_bias(device, config.hidden_size, config.intermediate_size),
290            up_proj: linear_no_bias(device, config.hidden_size, config.intermediate_size),
291            down_proj: linear_no_bias(device, config.intermediate_size, config.hidden_size),
292        }
293    }
294
295    pub fn forward(&self, hidden: Tensor<B, 3>) -> Tensor<B, 3> {
296        let gate = silu(self.gate_proj.forward(hidden.clone()));
297        let up = self.up_proj.forward(hidden);
298        self.down_proj.forward(gate * up)
299    }
300}
301
302/// One DiT layer: AdaLN self-attention (gated), plain-residual cross-attention,
303/// AdaLN MLP (gated). Modulation chunk order is
304/// (shift_msa, scale_msa, gate_msa, c_shift_msa, c_scale_msa, c_gate_msa).
305#[derive(Module, Debug)]
306pub struct AceStepDiTLayer<B: Backend> {
307    pub self_attn_norm: Qwen3RmsNorm<B>,
308    pub self_attn: AceStepAttention<B>,
309    pub cross_attn_norm: Qwen3RmsNorm<B>,
310    pub cross_attn: AceStepAttention<B>,
311    pub mlp_norm: Qwen3RmsNorm<B>,
312    pub mlp: AceStepMlp<B>,
313    /// AdaLN modulation table of shape [1, 6, hidden].
314    pub scale_shift_table: Param<Tensor<B, 3>>,
315    #[module(skip)]
316    sliding: bool,
317}
318
319impl<B: Backend> AceStepDiTLayer<B> {
320    fn new(config: &AceStepConfig, layer: usize, device: &B::Device) -> Self {
321        let hidden = config.hidden_size;
322        Self {
323            self_attn_norm: Qwen3RmsNorm::new(device, hidden, config.rms_norm_eps),
324            self_attn: AceStepAttention::new(config, device),
325            cross_attn_norm: Qwen3RmsNorm::new(device, hidden, config.rms_norm_eps),
326            cross_attn: AceStepAttention::new(config, device),
327            mlp_norm: Qwen3RmsNorm::new(device, hidden, config.rms_norm_eps),
328            mlp: AceStepMlp::new(config, device),
329            scale_shift_table: Param::from_tensor(randn_scaled([1, 6, hidden], hidden, device)),
330            sliding: config.is_sliding_layer(layer),
331        }
332    }
333
334    pub fn forward(
335        &self,
336        hidden: Tensor<B, 3>,
337        rope: (&Tensor<B, 4>, &Tensor<B, 4>),
338        timestep_proj: &Tensor<B, 3>,
339        cross_kv: (&Tensor<B, 4>, &Tensor<B, 4>),
340        mask: Option<&Tensor<B, 2>>,
341    ) -> Tensor<B, 3> {
342        let (cos, sin) = rope;
343        let (cross_key, cross_value) = cross_kv;
344        // [1, 6, hidden] + [1, 6, hidden] → six [1, 1, hidden] chunks.
345        let modulation = self.scale_shift_table.val() + timestep_proj.clone();
346        let chunks = modulation.chunk(6, 1);
347        let (shift_msa, scale_msa, gate_msa) =
348            (chunks[0].clone(), chunks[1].clone(), chunks[2].clone());
349        let (c_shift_msa, c_scale_msa, c_gate_msa) =
350            (chunks[3].clone(), chunks[4].clone(), chunks[5].clone());
351
352        let normed = self.self_attn_norm.forward(hidden.clone()) * (scale_msa + 1.0) + shift_msa;
353        let attn = self.self_attn.forward_self(normed, cos, sin, mask);
354        let hidden = hidden + attn * gate_msa;
355
356        // Cross-attention: plain residual, no modulation and no gate.
357        let normed = self.cross_attn_norm.forward(hidden.clone());
358        let hidden = hidden
359            + self
360                .cross_attn
361                .forward_cross(normed, cross_key, cross_value);
362
363        let normed = self.mlp_norm.forward(hidden.clone()) * (c_scale_msa + 1.0) + c_shift_msa;
364        hidden + self.mlp.forward(normed) * c_gate_msa
365    }
366}
367
368/// TimestepEmbedding(256, hidden, scale=1000): sinusoidal COS-then-SIN
369/// projection followed by linear_1 → SiLU → linear_2 (temb) and
370/// time_proj(SiLU(temb)) reshaped to [1, 6, hidden] (timestep_proj).
371#[derive(Module, Debug)]
372pub struct TimestepEmbedding<B: Backend> {
373    pub linear_1: Linear<B>,
374    pub linear_2: Linear<B>,
375    pub time_proj: Linear<B>,
376    #[module(skip)]
377    meta: TimestepMeta,
378}
379
380#[derive(Clone, Debug)]
381struct TimestepMeta {
382    in_channels: usize,
383    embed_dim: usize,
384}
385
386impl<B: Backend> TimestepEmbedding<B> {
387    fn new(embed_dim: usize, device: &B::Device) -> Self {
388        Self {
389            linear_1: linear_with_bias(device, TIME_EMBED_CHANNELS, embed_dim),
390            linear_2: linear_with_bias(device, embed_dim, embed_dim),
391            time_proj: linear_with_bias(device, embed_dim, 6 * embed_dim),
392            meta: TimestepMeta {
393                in_channels: TIME_EMBED_CHANNELS,
394                embed_dim,
395            },
396        }
397    }
398
399    /// Returns (temb [1, hidden], timestep_proj [1, 6, hidden]).
400    pub fn forward(&self, t: f32, device: &B::Device) -> (Tensor<B, 2>, Tensor<B, 3>) {
401        let sinusoid = sinusoidal_timestep::<B>(t, self.meta.in_channels, device);
402        let temb = self.linear_2.forward(silu(self.linear_1.forward(sinusoid)));
403        let proj = self
404            .time_proj
405            .forward(silu(temb.clone()))
406            .reshape([1, 6, self.meta.embed_dim]);
407        (temb, proj)
408    }
409}
410
411/// `proj_in`: transposed strided Conv1d patchifier. The `conv` field name is
412/// load-bearing for the canonical burnpack path `proj_in.conv.*`.
413#[derive(Module, Debug)]
414pub struct PatchEmbed<B: Backend> {
415    pub conv: Conv1d<B>,
416}
417
418/// `proj_out`: transposed ConvTranspose1d de-patchifier. The `conv` field name
419/// is load-bearing for the canonical burnpack path `proj_out.conv.*`.
420#[derive(Module, Debug)]
421pub struct PatchUnembed<B: Backend> {
422    pub conv: ConvTranspose1d<B>,
423}
424
425/// Cross-attention K/V pairs, one per DiT layer, computed once per generation
426/// (encoder states are constant across sampler steps).
427pub struct CrossKv<B: Backend> {
428    keys: Vec<Tensor<B, 4>>,
429    values: Vec<Tensor<B, 4>>,
430}
431
432/// The ACE-Step 1.5 turbo DiT decoder (`AceStepDiTModel`).
433#[derive(Module, Debug)]
434pub struct AceStepDiT<B: Backend> {
435    pub proj_in: PatchEmbed<B>,
436    pub time_embed: TimestepEmbedding<B>,
437    pub time_embed_r: TimestepEmbedding<B>,
438    pub condition_embedder: Linear<B>,
439    pub layers: Vec<AceStepDiTLayer<B>>,
440    pub norm_out: Qwen3RmsNorm<B>,
441    pub proj_out: PatchUnembed<B>,
442    /// Final AdaLN modulation table of shape [1, 2, hidden].
443    pub scale_shift_table: Param<Tensor<B, 3>>,
444    #[module(skip)]
445    meta: DiTMeta,
446}
447
448#[derive(Clone, Debug)]
449struct DiTMeta {
450    head_dim: usize,
451    rope_theta: f64,
452    sliding_window: usize,
453    patch_size: usize,
454    any_sliding: bool,
455}
456
457impl<B: Backend> AceStepDiT<B> {
458    pub fn new(config: &AceStepConfig, device: &B::Device) -> Self {
459        let hidden = config.hidden_size;
460        Self {
461            proj_in: PatchEmbed {
462                conv: Conv1dConfig::new(config.in_channels, hidden, config.patch_size)
463                    .with_stride(config.patch_size)
464                    .with_bias(true)
465                    .init(device),
466            },
467            time_embed: TimestepEmbedding::new(hidden, device),
468            time_embed_r: TimestepEmbedding::new(hidden, device),
469            condition_embedder: linear_with_bias(device, hidden, hidden),
470            layers: (0..config.num_hidden_layers)
471                .map(|layer| AceStepDiTLayer::new(config, layer, device))
472                .collect(),
473            norm_out: Qwen3RmsNorm::new(device, hidden, config.rms_norm_eps),
474            proj_out: PatchUnembed {
475                conv: ConvTranspose1dConfig::new(
476                    [hidden, config.audio_acoustic_hidden_dim],
477                    config.patch_size,
478                )
479                .with_stride(config.patch_size)
480                .with_bias(true)
481                .init(device),
482            },
483            scale_shift_table: Param::from_tensor(randn_scaled([1, 2, hidden], hidden, device)),
484            meta: DiTMeta {
485                head_dim: config.head_dim,
486                rope_theta: config.rope_theta,
487                sliding_window: config.sliding_window,
488                patch_size: config.patch_size,
489                any_sliding: (0..config.num_hidden_layers)
490                    .any(|layer| config.is_sliding_layer(layer)),
491            },
492        }
493    }
494
495    pub fn from_burnpack(config: &AceStepConfig, path: &Path, device: &B::Device) -> Result<Self> {
496        let mut model = Self::new(config, device);
497        let mut store = BurnpackStore::from_file(path).zero_copy(true);
498        model.load_from(&mut store).with_context(|| {
499            format!("failed to load AceStep DiT weights from {}", path.display())
500        })?;
501        Ok(model)
502    }
503
504    /// Runs one DiT forward pass.
505    ///
506    /// - `xt`: current noisy latent [B, T, acoustic_dim]
507    /// - `t`: scalar flow timestep (timestep_r == t, so time_embed_r sees 0)
508    /// - `context`: [B, T, in_channels − acoustic_dim] (src_latents ++ chunk_masks)
509    /// - `encoder_hidden_states`: [B, S, hidden] raw encoder output;
510    ///   `condition_embedder` is applied inside.
511    ///
512    /// Returns the predicted velocity v [B, T, acoustic_dim].
513    pub fn forward(
514        &self,
515        xt: Tensor<B, 3>,
516        t: f32,
517        context: Tensor<B, 3>,
518        encoder_hidden_states: Tensor<B, 3>,
519    ) -> Tensor<B, 3> {
520        let kv = self.prepare_cross_kv(encoder_hidden_states);
521        self.forward_with_kv(xt, t, context, &kv)
522    }
523
524    /// Applies `condition_embedder` and every layer's cross-attention K/V
525    /// projection to the encoder states. Cache the result and call
526    /// [`AceStepDiT::forward_with_kv`] to skip redundant encoder work.
527    pub fn prepare_cross_kv(&self, encoder_hidden_states: Tensor<B, 3>) -> CrossKv<B> {
528        let conditioned = self.condition_embedder.forward(encoder_hidden_states);
529        let mut keys = Vec::with_capacity(self.layers.len());
530        let mut values = Vec::with_capacity(self.layers.len());
531        for layer in &self.layers {
532            let (key, value) = layer.cross_attn.project_kv(&conditioned);
533            keys.push(key);
534            values.push(value);
535        }
536        CrossKv { keys, values }
537    }
538
539    /// DiT forward with precomputed cross-attention K/V (see
540    /// [`AceStepDiT::prepare_cross_kv`]). Numerically identical to
541    /// [`AceStepDiT::forward`].
542    pub fn forward_with_kv(
543        &self,
544        xt: Tensor<B, 3>,
545        t: f32,
546        context: Tensor<B, 3>,
547        kv: &CrossKv<B>,
548    ) -> Tensor<B, 3> {
549        let device = xt.device();
550        let [batch, seq_len, _] = xt.dims();
551
552        // x_in = cat([context_latents, xt], -1) → [B, T, in_channels].
553        let x = Tensor::cat(vec![context, xt], 2);
554        // Zero-pad the frame count to a multiple of patch_size.
555        let remainder = seq_len % self.meta.patch_size;
556        let x = if remainder != 0 {
557            let pad_len = self.meta.patch_size - remainder;
558            let in_channels = x.dims()[2];
559            let pad = Tensor::zeros([batch, pad_len, in_channels], &device);
560            Tensor::cat(vec![x, pad], 1)
561        } else {
562            x
563        };
564
565        // Patchify: [B, T, C] → conv over [B, C, T] → [B, T/patch, hidden].
566        let mut hidden = self.proj_in.conv.forward(x.swap_dims(1, 2)).swap_dims(1, 2);
567        let patch_len = hidden.dims()[1];
568
569        let (cos, sin) =
570            rotary_cos_sin::<B>(patch_len, self.meta.head_dim, self.meta.rope_theta, &device);
571        let sliding_mask = if self.meta.any_sliding {
572            Some(sliding_window_mask::<B>(
573                patch_len,
574                self.meta.sliding_window,
575                &device,
576            ))
577        } else {
578            None
579        };
580
581        // timestep_r == timestep at inference → time_embed_r is evaluated at 0
582        // (a nonzero constant that must not be dropped).
583        let (temb_t, proj_t) = self.time_embed.forward(t, &device);
584        let (temb_r, proj_r) = self.time_embed_r.forward(0.0, &device);
585        let temb = temb_t + temb_r;
586        let timestep_proj = proj_t + proj_r;
587
588        for (index, layer) in self.layers.iter().enumerate() {
589            let mask = if layer.sliding {
590                sliding_mask.as_ref()
591            } else {
592                None
593            };
594            hidden = layer.forward(
595                hidden,
596                (&cos, &sin),
597                &timestep_proj,
598                (&kv.keys[index], &kv.values[index]),
599                mask,
600            );
601        }
602
603        // Final AdaLN uses temb (not the 6× proj), table order (shift, scale).
604        let modulation = self.scale_shift_table.val() + temb.unsqueeze_dim::<3>(1);
605        let chunks = modulation.chunk(2, 1);
606        let shift = chunks[0].clone();
607        let scale = chunks[1].clone();
608        let out = self.norm_out.forward(hidden) * (scale + 1.0) + shift;
609
610        // De-patchify and crop back to the original (unpadded) frame count.
611        let out = self
612            .proj_out
613            .conv
614            .forward(out.swap_dims(1, 2))
615            .swap_dims(1, 2);
616        out.narrow(1, 0, seq_len)
617    }
618
619    /// Turbo sampler: explicit Euler over `timesteps` (default
620    /// [`TURBO_TIMESTEPS`], 8 steps, t descending 1.0 → 0.3), no CFG.
621    ///
622    /// Flow convention: x1 = noise, x0 = data, x_t = t·x1 + (1−t)·x0 and the
623    /// model predicts v = x1 − x0. Step i applies
624    /// `xt ← xt − v·(t_cur − t_next)`, and the last step `xt ← xt − v·t_cur`
625    /// (recovering x0).
626    ///
627    /// - `noise`: initial latent x1 [B, T, acoustic_dim]
628    /// - `context`: [B, T, in_channels − acoustic_dim]
629    /// - `encoder_hidden_states`: [B, S, hidden] (cross K/V computed once)
630    /// - `progress`: optional callback invoked as (steps_done, total_steps)
631    ///
632    /// Returns the final latent [B, T, acoustic_dim].
633    pub fn sample_turbo(
634        &self,
635        noise: Tensor<B, 3>,
636        context: Tensor<B, 3>,
637        encoder_hidden_states: Tensor<B, 3>,
638        timesteps: &[f32],
639        mut progress: Option<&mut dyn FnMut(usize, usize)>,
640    ) -> Tensor<B, 3> {
641        let kv = self.prepare_cross_kv(encoder_hidden_states);
642        let total = timesteps.len();
643        let mut xt = noise;
644        for (index, &t_cur) in timesteps.iter().enumerate() {
645            let v = self.forward_with_kv(xt.clone(), t_cur, context.clone(), &kv);
646            let dt = if index + 1 == total {
647                t_cur
648            } else {
649                t_cur - timesteps[index + 1]
650            };
651            xt = xt - v * dt;
652            if let Some(callback) = progress.as_mut() {
653                callback(index + 1, total);
654            }
655        }
656        xt
657    }
658}
659
660/// Sinusoidal timestep embedding [1, channels]: t scaled by 1000, frequencies
661/// exp(-ln(10000) · k / half), COS first then SIN (matches the reference).
662fn sinusoidal_timestep<B: Backend>(t: f32, channels: usize, device: &B::Device) -> Tensor<B, 2> {
663    let half = channels / 2;
664    let scaled = t * TIME_EMBED_SCALE;
665    let mut data = Vec::with_capacity(channels);
666    for k in 0..half {
667        let freq = (-TIME_EMBED_MAX_PERIOD.ln() * k as f32 / half as f32).exp();
668        data.push((scaled * freq).cos());
669    }
670    for k in 0..half {
671        let freq = (-TIME_EMBED_MAX_PERIOD.ln() * k as f32 / half as f32).exp();
672        data.push((scaled * freq).sin());
673    }
674    Tensor::<B, 2>::from_data(TensorData::new(data, [1, channels]), device)
675}
676
677/// inv_freq[i] = 1 / theta^(2i / head_dim); cos/sin over positions 0..seq_len
678/// with duplicated halves (cat([freqs, freqs])). Returned as [1, L, 1, head_dim]
679/// so they broadcast against [batch, seq, heads, head_dim].
680fn rotary_cos_sin<B: Backend>(
681    seq_len: usize,
682    head_dim: usize,
683    theta: f64,
684    device: &B::Device,
685) -> (Tensor<B, 4>, Tensor<B, 4>) {
686    let half = head_dim / 2;
687    let inv_freq: Vec<f32> = (0..half)
688        .map(|i| 1.0 / theta.powf(2.0 * i as f64 / head_dim as f64) as f32)
689        .collect();
690    let mut cos_values = Vec::with_capacity(seq_len * head_dim);
691    let mut sin_values = Vec::with_capacity(seq_len * head_dim);
692    for pos in 0..seq_len {
693        for _ in 0..2 {
694            for &freq in &inv_freq {
695                let angle = pos as f32 * freq;
696                cos_values.push(angle.cos());
697                sin_values.push(angle.sin());
698            }
699        }
700    }
701    let shape = [1, seq_len, 1, head_dim];
702    (
703        Tensor::<B, 4>::from_data(TensorData::new(cos_values, shape), device),
704        Tensor::<B, 4>::from_data(TensorData::new(sin_values, shape), device),
705    )
706}
707
708fn rotate_half<B: Backend>(x: Tensor<B, 4>) -> Tensor<B, 4> {
709    let [batch, seq_len, heads, head_dim] = x.dims();
710    let half = head_dim / 2;
711    let x1 = x.clone().slice([0..batch, 0..seq_len, 0..heads, 0..half]);
712    let x2 = x.slice([0..batch, 0..seq_len, 0..heads, half..head_dim]);
713    Tensor::cat(vec![x2.neg(), x1], 3)
714}
715
716fn apply_rotary_pos_emb<B: Backend>(
717    x: Tensor<B, 4>,
718    cos: &Tensor<B, 4>,
719    sin: &Tensor<B, 4>,
720) -> Tensor<B, 4> {
721    x.clone() * cos.clone() + rotate_half(x) * sin.clone()
722}
723
724/// [batch, kv_heads, seq, head_dim] → [batch, kv_heads × repeats, seq, head_dim]
725/// (repeat-interleave along the head axis, matching HF `repeat_kv`).
726fn repeat_kv<B: Backend>(tensor: Tensor<B, 4>, repeats: usize) -> Tensor<B, 4> {
727    if repeats == 1 {
728        return tensor;
729    }
730    let [batch, heads, seq_len, head_dim] = tensor.dims();
731    tensor
732        .unsqueeze_dim::<5>(2)
733        .repeat_dim(2, repeats)
734        .reshape([batch, heads * repeats, seq_len, head_dim])
735}
736
737/// Additive bidirectional sliding-window mask [seq, seq]: 0 where
738/// |i − j| ≤ window, MASK_MIN elsewhere.
739fn sliding_window_mask<B: Backend>(
740    seq_len: usize,
741    window: usize,
742    device: &B::Device,
743) -> Tensor<B, 2> {
744    let mut data = Vec::with_capacity(seq_len * seq_len);
745    for row in 0..seq_len {
746        for col in 0..seq_len {
747            let visible = row.abs_diff(col) <= window;
748            data.push(if visible { 0.0 } else { MASK_MIN });
749        }
750    }
751    Tensor::<B, 2>::from_data(TensorData::new(data, [seq_len, seq_len]), device)
752}
753
754/// randn(shape) / sqrt(hidden), matching the reference parameter init.
755fn randn_scaled<B: Backend, const D: usize>(
756    shape: [usize; D],
757    hidden: usize,
758    device: &B::Device,
759) -> Tensor<B, D> {
760    Tensor::random(
761        shape,
762        Distribution::Normal(0.0, 1.0 / (hidden as f64).sqrt()),
763        device,
764    )
765}
766
767fn linear_no_bias<B: Backend>(device: &B::Device, d_input: usize, d_output: usize) -> Linear<B> {
768    LinearConfig::new(d_input, d_output)
769        .with_bias(false)
770        .with_layout(LinearLayout::Col)
771        .init(device)
772}
773
774fn linear_with_bias<B: Backend>(device: &B::Device, d_input: usize, d_output: usize) -> Linear<B> {
775    LinearConfig::new(d_input, d_output)
776        .with_bias(true)
777        .with_layout(LinearLayout::Col)
778        .init(device)
779}
780
781#[cfg(test)]
782mod tests {
783    use super::*;
784    use burn::backend::NdArray;
785
786    type TestBackend = NdArray<f32>;
787
788    /// Tiny DiT: hidden 64, 2 layers (layer 0 sliding, layer 1 full), 4 heads,
789    /// 2 KV heads, head_dim 16, intermediate 128, in_channels 12 = 4 (xt) +
790    /// 4 (src) + 4 (chunk_mask), acoustic latent dim 4, patch 2, window 2.
791    fn tiny_config() -> AceStepConfig {
792        let base = AceStepConfig::load(
793            &Path::new(env!("CARGO_MANIFEST_DIR"))
794                .join("src/acestep/testdata/acestep_v15_turbo_config.json"),
795        )
796        .expect("reference config must parse");
797        AceStepConfig {
798            hidden_size: 64,
799            intermediate_size: 128,
800            num_hidden_layers: 2,
801            num_attention_heads: 4,
802            num_key_value_heads: 2,
803            head_dim: 16,
804            sliding_window: 2,
805            in_channels: 12,
806            audio_acoustic_hidden_dim: 4,
807            patch_size: 2,
808            layer_types: vec![
809                "sliding_attention".to_string(),
810                "full_attention".to_string(),
811            ],
812            ..base
813        }
814    }
815
816    fn tiny_model() -> AceStepDiT<TestBackend> {
817        let device = Default::default();
818        AceStepDiT::new(&tiny_config(), &device)
819    }
820
821    fn assert_finite(tensor: &Tensor<TestBackend, 3>, what: &str) {
822        let values = tensor
823            .clone()
824            .to_data()
825            .to_vec::<f32>()
826            .expect("output should materialize as f32");
827        assert!(
828            values.iter().all(|v| v.is_finite()),
829            "{what} contains non-finite values"
830        );
831    }
832
833    #[test]
834    fn forward_shapes_and_finite() {
835        let model = tiny_model();
836        let device = Default::default();
837        let xt =
838            Tensor::<TestBackend, 3>::random([1, 10, 4], Distribution::Normal(0.0, 1.0), &device);
839        let context =
840            Tensor::<TestBackend, 3>::random([1, 10, 8], Distribution::Normal(0.0, 1.0), &device);
841        let encoder =
842            Tensor::<TestBackend, 3>::random([1, 7, 64], Distribution::Normal(0.0, 1.0), &device);
843
844        let v = model.forward(xt, 0.5, context, encoder);
845        assert_eq!(v.dims(), [1, 10, 4]);
846        assert_finite(&v, "velocity");
847    }
848
849    #[test]
850    fn odd_sequence_length_is_padded_and_cropped() {
851        let model = tiny_model();
852        let device = Default::default();
853        let xt =
854            Tensor::<TestBackend, 3>::random([1, 9, 4], Distribution::Normal(0.0, 1.0), &device);
855        let context =
856            Tensor::<TestBackend, 3>::random([1, 9, 8], Distribution::Normal(0.0, 1.0), &device);
857        let encoder =
858            Tensor::<TestBackend, 3>::random([1, 7, 64], Distribution::Normal(0.0, 1.0), &device);
859
860        let v = model.forward(xt, 0.5, context, encoder);
861        assert_eq!(v.dims(), [1, 9, 4]);
862        assert_finite(&v, "velocity for odd T");
863    }
864
865    #[test]
866    fn forward_with_cached_kv_matches_forward() {
867        let model = tiny_model();
868        let device = Default::default();
869        let xt =
870            Tensor::<TestBackend, 3>::random([1, 10, 4], Distribution::Normal(0.0, 1.0), &device);
871        let context =
872            Tensor::<TestBackend, 3>::random([1, 10, 8], Distribution::Normal(0.0, 1.0), &device);
873        let encoder =
874            Tensor::<TestBackend, 3>::random([1, 7, 64], Distribution::Normal(0.0, 1.0), &device);
875
876        let direct = model.forward(xt.clone(), 0.5, context.clone(), encoder.clone());
877        let kv = model.prepare_cross_kv(encoder);
878        let cached = model.forward_with_kv(xt, 0.5, context, &kv);
879        let diff = (direct - cached).abs().max().to_data().to_vec::<f32>();
880        assert!(
881            diff.expect("max diff")[0] <= 0.0,
882            "cached-KV forward must be numerically identical"
883        );
884    }
885
886    #[test]
887    fn sliding_window_mask_band_pattern() {
888        let device = Default::default();
889        let mask = sliding_window_mask::<TestBackend>(5, 2, &device);
890        assert_eq!(mask.dims(), [5, 5]);
891        let values = mask.to_data().to_vec::<f32>().expect("mask values");
892        for row in 0..5usize {
893            for col in 0..5usize {
894                let expected = if row.abs_diff(col) <= 2 {
895                    0.0
896                } else {
897                    MASK_MIN
898                };
899                assert_eq!(
900                    values[row * 5 + col],
901                    expected,
902                    "mask[{row}][{col}] should be {expected}"
903                );
904            }
905        }
906    }
907
908    #[test]
909    fn timestep_embedding_shapes_and_cos_sin_order() {
910        let model = tiny_model();
911        let device = Default::default();
912
913        let (temb, proj) = model.time_embed.forward(0.5, &device);
914        assert_eq!(temb.dims(), [1, 64]);
915        assert_eq!(proj.dims(), [1, 6, 64]);
916
917        // At t = 0 the sinusoid is cos(0)=1 in the first half, sin(0)=0 in the
918        // second half (COS-then-SIN order).
919        let sinusoid = sinusoidal_timestep::<TestBackend>(0.0, TIME_EMBED_CHANNELS, &device);
920        let values = sinusoid.to_data().to_vec::<f32>().expect("sinusoid values");
921        let half = TIME_EMBED_CHANNELS / 2;
922        assert!(values[..half].iter().all(|v| (*v - 1.0).abs() < 1e-6));
923        assert!(values[half..].iter().all(|v| v.abs() < 1e-6));
924
925        // time_embed_r(0) contributes a nonzero constant; it must not be dropped.
926        let (temb_r, proj_r) = model.time_embed_r.forward(0.0, &device);
927        let temb_r_values = temb_r.to_data().to_vec::<f32>().expect("temb_r values");
928        let proj_r_values = proj_r.to_data().to_vec::<f32>().expect("proj_r values");
929        assert!(temb_r_values.iter().any(|v| v.abs() > 1e-6));
930        assert!(proj_r_values.iter().any(|v| v.abs() > 1e-6));
931    }
932
933    #[test]
934    fn sample_turbo_euler_loop() {
935        let model = tiny_model();
936        let device = Default::default();
937        let noise =
938            Tensor::<TestBackend, 3>::random([1, 10, 4], Distribution::Normal(0.0, 1.0), &device);
939        let context =
940            Tensor::<TestBackend, 3>::random([1, 10, 8], Distribution::Normal(0.0, 1.0), &device);
941        let encoder =
942            Tensor::<TestBackend, 3>::random([1, 7, 64], Distribution::Normal(0.0, 1.0), &device);
943        let timesteps = [1.0, 0.75, 0.5, 0.3];
944
945        let mut calls = 0usize;
946        let mut progress = |done: usize, total: usize| {
947            calls += 1;
948            assert_eq!(done, calls);
949            assert_eq!(total, timesteps.len());
950        };
951        let latent = model.sample_turbo(noise, context, encoder, &timesteps, Some(&mut progress));
952
953        assert_eq!(calls, timesteps.len());
954        assert_eq!(latent.dims(), [1, 10, 4]);
955        assert_finite(&latent, "sampled latent");
956    }
957
958    #[test]
959    fn default_schedule_has_eight_descending_steps() {
960        assert_eq!(TURBO_TIMESTEPS.len(), 8);
961        assert_eq!(TURBO_TIMESTEPS[0], 1.0);
962        assert_eq!(TURBO_TIMESTEPS[7], 0.3);
963        assert!(TURBO_TIMESTEPS.windows(2).all(|w| w[0] > w[1]));
964    }
965}