Skip to main content

maolan_generate/acestep/
condition.rs

1//! ACE-Step 1.5 condition encoder stack + audio token detokenizer, ported to Burn.
2//!
3//! Covers the lyric encoder, timbre encoder, condition assembly (`pack_sequences`),
4//! and the 5Hz-codes → 25Hz-latents detokenizer from
5//! `modeling_acestep_v15_turbo.py`. All attention is BIDIRECTIONAL; layers whose
6//! global `layer_types` entry is "sliding_attention" restrict each query to keys
7//! with `|i - j| <= sliding_window`. RoPE (theta 1e6, duplicated halves) is applied
8//! post q/k-norm; q/k/v/o and MLP projections carry no bias. The encoders honor the
9//! lyric key-padding mask; the detokenizer and timbre encoder use no padding mask.
10//!
11//! The checkpoint's `timbre_encoder.special_token` is dead code upstream (its
12//! prepend is commented out) and is intentionally NOT modeled here — the converter
13//! drops it. Pooled timbre output is position 0 of the encoder output.
14//!
15//! Canonical burnpack tensor names (= module paths of `AceStepCondition`, what the
16//! offline converter writes into the condition `.bpk`):
17//!
18//! - `text_projector.weight`                                   — Linear(1024→2048), no bias
19//! - `lyric_encoder.embed_tokens.{weight,bias}`                — Linear(1024→2048)
20//! - `lyric_encoder.norm.weight`
21//! - `lyric_encoder.layers.{0..7}.input_layernorm.weight`
22//! - `lyric_encoder.layers.{i}.post_attention_layernorm.weight`
23//! - `lyric_encoder.layers.{i}.self_attn.{q,k,v,o}_proj.weight`
24//! - `lyric_encoder.layers.{i}.self_attn.{q,k}_norm.weight`
25//! - `lyric_encoder.layers.{i}.mlp.{gate,up,down}_proj.weight`
26//! - `timbre_encoder.embed_tokens.{weight,bias}`               — Linear(64→2048)
27//! - `timbre_encoder.norm.weight`
28//! - `timbre_encoder.layers.{0..3}.<same per-layer keys>`      (NO `special_token`)
29//! - `detokenizer.embed_tokens.{weight,bias}`                  — Linear(2048→2048)
30//! - `detokenizer.special_tokens`                              — [1, 5, 2048]
31//! - `detokenizer.norm.weight`
32//! - `detokenizer.layers.{0..1}.<same per-layer keys>`
33//! - `detokenizer.proj_out.{weight,bias}`                      — Linear(2048→64)
34//! - `quantizer.project_in.{weight,bias}`                      — Linear(2048→6)
35//! - `quantizer.project_out.{weight,bias}`                     — Linear(6→2048)
36
37use std::path::Path;
38
39use anyhow::{Context, Result};
40use burn::module::{Module, Param};
41use burn::nn::{Linear, LinearConfig, LinearLayout};
42use burn::prelude::Backend;
43use burn::tensor::activation::{silu, softmax};
44use burn::tensor::{DType, Int, Tensor, TensorData};
45use burn_store::{BurnpackStore, ModuleSnapshot};
46
47use crate::acestep::config::AceStepConfig;
48use crate::acestep::fsq::ResidualFsq;
49
50/// Weight-only RMSNorm (Qwen3 style): `x * rsqrt(mean(x^2) + eps) * weight`,
51/// with the variance computed in fp32.
52#[derive(Module, Debug)]
53pub struct ConditionRmsNorm<B: Backend> {
54    pub weight: Param<Tensor<B, 1>>,
55    pub epsilon: f64,
56}
57
58impl<B: Backend> ConditionRmsNorm<B> {
59    pub fn new(device: &B::Device, hidden_size: usize, epsilon: f64) -> Self {
60        Self {
61            weight: Param::from_tensor(Tensor::<B, 1>::ones([hidden_size], device)),
62            epsilon,
63        }
64    }
65
66    pub fn forward<const D: usize>(&self, hidden: Tensor<B, D>) -> Tensor<B, D> {
67        let dtype = hidden.dtype();
68        let rms = (hidden.clone().cast(DType::F32).square().mean_dim(D - 1) + self.epsilon).sqrt();
69        (hidden / rms.cast(dtype)) * self.weight.val().unsqueeze()
70    }
71}
72
73#[derive(Clone, Debug)]
74struct AttentionMeta {
75    num_heads: usize,
76    num_kv_heads: usize,
77    head_dim: usize,
78    scaling: f32,
79    /// `Some(window)` for sliding-attention layers (`|i - j| <= window`),
80    /// `None` for full bidirectional attention.
81    sliding_window: Option<usize>,
82}
83
84/// Bidirectional self-attention with per-head q/k RMSNorm, post-norm RoPE and GQA.
85#[derive(Module, Debug)]
86pub struct AceStepAttention<B: Backend> {
87    pub q_proj: Linear<B>,
88    pub k_proj: Linear<B>,
89    pub v_proj: Linear<B>,
90    pub o_proj: Linear<B>,
91    pub q_norm: ConditionRmsNorm<B>,
92    pub k_norm: ConditionRmsNorm<B>,
93    #[module(skip)]
94    meta: AttentionMeta,
95}
96
97impl<B: Backend> AceStepAttention<B> {
98    pub fn new(config: &AceStepConfig, layer_idx: usize, device: &B::Device) -> Self {
99        let head_dim = config.head_dim;
100        let sliding_window = config
101            .is_sliding_layer(layer_idx)
102            .then_some(config.sliding_window);
103        Self {
104            q_proj: linear_no_bias(
105                device,
106                config.hidden_size,
107                config.num_attention_heads * head_dim,
108            ),
109            k_proj: linear_no_bias(
110                device,
111                config.hidden_size,
112                config.num_key_value_heads * head_dim,
113            ),
114            v_proj: linear_no_bias(
115                device,
116                config.hidden_size,
117                config.num_key_value_heads * head_dim,
118            ),
119            o_proj: linear_no_bias(
120                device,
121                config.num_attention_heads * head_dim,
122                config.hidden_size,
123            ),
124            q_norm: ConditionRmsNorm::new(device, head_dim, config.rms_norm_eps),
125            k_norm: ConditionRmsNorm::new(device, head_dim, config.rms_norm_eps),
126            meta: AttentionMeta {
127                num_heads: config.num_attention_heads,
128                num_kv_heads: config.num_key_value_heads,
129                head_dim,
130                scaling: (head_dim as f32).powf(-0.5),
131                sliding_window,
132            },
133        }
134    }
135
136    /// `hidden`: [B, L, hidden]; `cos`/`sin`: [1, L, 1, head_dim];
137    /// `additive_mask`: optional [B, 1, L, L] with 0 for allowed and a large
138    /// negative value for disallowed query/key pairs.
139    pub fn forward(
140        &self,
141        hidden: Tensor<B, 3>,
142        cos: &Tensor<B, 4>,
143        sin: &Tensor<B, 4>,
144        additive_mask: Option<Tensor<B, 4>>,
145    ) -> Tensor<B, 3> {
146        let [batch, seq_len, _] = hidden.dims();
147        let num_heads = self.meta.num_heads;
148        let num_kv_heads = self.meta.num_kv_heads;
149        let head_dim = self.meta.head_dim;
150
151        let q = self
152            .q_proj
153            .forward(hidden.clone())
154            .reshape([batch, seq_len, num_heads, head_dim]);
155        let k =
156            self.k_proj
157                .forward(hidden.clone())
158                .reshape([batch, seq_len, num_kv_heads, head_dim]);
159        let v = self
160            .v_proj
161            .forward(hidden)
162            .reshape([batch, seq_len, num_kv_heads, head_dim]);
163
164        // Per-head RMSNorm on the head dim, RoPE applied after the norm.
165        let q = apply_rotary_pos_emb(self.q_norm.forward(q), cos, sin).swap_dims(1, 2);
166        let k = apply_rotary_pos_emb(self.k_norm.forward(k), cos, sin).swap_dims(1, 2);
167        let v = v.swap_dims(1, 2);
168
169        let (k, v) = if num_heads != num_kv_heads {
170            let repeats = num_heads / num_kv_heads;
171            (repeat_kv(k, repeats), repeat_kv(v, repeats))
172        } else {
173            (k, v)
174        };
175
176        let scores = q.matmul(k.swap_dims(2, 3)).mul_scalar(self.meta.scaling);
177        let scores = match additive_mask {
178            Some(mask) => scores + mask,
179            None => scores,
180        };
181        // Softmax in fp32, then back to the value dtype.
182        let dtype = scores.dtype();
183        let weights = softmax(scores.cast(DType::F32), 3).cast(dtype);
184        let attended =
185            weights
186                .matmul(v)
187                .swap_dims(1, 2)
188                .reshape([batch, seq_len, num_heads * head_dim]);
189        self.o_proj.forward(attended)
190    }
191}
192
193/// SwiGLU MLP: `down(silu(gate(x)) * up(x))`, no biases.
194#[derive(Module, Debug)]
195pub struct AceStepMlp<B: Backend> {
196    pub gate_proj: Linear<B>,
197    pub up_proj: Linear<B>,
198    pub down_proj: Linear<B>,
199}
200
201impl<B: Backend> AceStepMlp<B> {
202    pub fn new(config: &AceStepConfig, device: &B::Device) -> Self {
203        Self {
204            gate_proj: linear_no_bias(device, config.hidden_size, config.intermediate_size),
205            up_proj: linear_no_bias(device, config.hidden_size, config.intermediate_size),
206            down_proj: linear_no_bias(device, config.intermediate_size, config.hidden_size),
207        }
208    }
209
210    pub fn forward(&self, hidden: Tensor<B, 3>) -> Tensor<B, 3> {
211        let gate = silu(self.gate_proj.forward(hidden.clone()));
212        let up = self.up_proj.forward(hidden);
213        self.down_proj.forward(gate * up)
214    }
215}
216
217/// Pre-norm bidirectional encoder layer shared by the lyric encoder, timbre
218/// encoder and detokenizer:
219/// `x += self_attn(input_layernorm(x)); x += mlp(post_attention_layernorm(x))`.
220#[derive(Module, Debug)]
221pub struct AceStepEncoderLayer<B: Backend> {
222    pub self_attn: AceStepAttention<B>,
223    pub mlp: AceStepMlp<B>,
224    pub input_layernorm: ConditionRmsNorm<B>,
225    pub post_attention_layernorm: ConditionRmsNorm<B>,
226}
227
228impl<B: Backend> AceStepEncoderLayer<B> {
229    pub fn new(config: &AceStepConfig, layer_idx: usize, device: &B::Device) -> Self {
230        Self {
231            self_attn: AceStepAttention::new(config, layer_idx, device),
232            mlp: AceStepMlp::new(config, device),
233            input_layernorm: ConditionRmsNorm::new(device, config.hidden_size, config.rms_norm_eps),
234            post_attention_layernorm: ConditionRmsNorm::new(
235                device,
236                config.hidden_size,
237                config.rms_norm_eps,
238            ),
239        }
240    }
241
242    /// `padding_mask`: optional [B, L] integer mask (1 = valid key). It is
243    /// combined with the sliding-window geometry mask into one additive mask.
244    /// A row whose keys are all masked out yields NaN — never pass an all-zero
245    /// mask row (upstream has the same caveat).
246    pub fn forward(
247        &self,
248        hidden: Tensor<B, 3>,
249        cos: &Tensor<B, 4>,
250        sin: &Tensor<B, 4>,
251        padding_mask: Option<&Tensor<B, 2, Int>>,
252    ) -> Tensor<B, 3> {
253        let sliding_window = self.self_attn.meta.sliding_window;
254        let additive_mask = if sliding_window.is_some() || padding_mask.is_some() {
255            let [batch, seq_len, _] = hidden.dims();
256            let device = hidden.device();
257            let padding = padding_mask.map(|mask| {
258                mask.clone()
259                    .to_data()
260                    .convert::<i64>()
261                    .to_vec::<i64>()
262                    .expect("padding mask must materialize as i64")
263            });
264            Some(additive_attention_mask::<B>(
265                batch,
266                seq_len,
267                sliding_window,
268                padding,
269                &device,
270            ))
271        } else {
272            None
273        };
274
275        let residual = hidden.clone();
276        let normed = self.input_layernorm.forward(hidden);
277        let hidden = residual + self.self_attn.forward(normed, cos, sin, additive_mask);
278
279        let residual = hidden.clone();
280        let normed = self.post_attention_layernorm.forward(hidden);
281        residual + self.mlp.forward(normed)
282    }
283}
284
285/// Lyric encoder: projects precomputed Qwen3-Embedding lyric hidden states to
286/// the model hidden size and runs `num_lyric_encoder_hidden_layers` encoder
287/// layers (bidirectional, alternating sliding/full, padding mask honored).
288#[derive(Module, Debug)]
289pub struct LyricEncoder<B: Backend> {
290    pub embed_tokens: Linear<B>,
291    pub layers: Vec<AceStepEncoderLayer<B>>,
292    pub norm: ConditionRmsNorm<B>,
293    head_dim: usize,
294    rope_theta: f64,
295}
296
297impl<B: Backend> LyricEncoder<B> {
298    pub fn new(config: &AceStepConfig, device: &B::Device) -> Self {
299        Self {
300            embed_tokens: linear_with_bias(device, config.text_hidden_dim, config.hidden_size),
301            layers: (0..config.num_lyric_encoder_hidden_layers)
302                .map(|layer_idx| AceStepEncoderLayer::new(config, layer_idx, device))
303                .collect(),
304            norm: ConditionRmsNorm::new(device, config.hidden_size, config.rms_norm_eps),
305            head_dim: config.head_dim,
306            rope_theta: config.rope_theta,
307        }
308    }
309
310    /// `lyric_hidden_states`: [B, Ll, text_hidden_dim]; `lyric_mask`: [B, Ll]
311    /// integer (1 = valid). Returns [B, Ll, hidden_size].
312    ///
313    /// For instrumental tracks pass ONE dummy lyric token with mask [1]; an
314    /// all-zero mask row produces NaN (all-masked softmax row), as upstream.
315    pub fn forward(
316        &self,
317        lyric_hidden_states: Tensor<B, 3>,
318        lyric_mask: Tensor<B, 2, Int>,
319    ) -> Tensor<B, 3> {
320        let [_, seq_len, _] = lyric_hidden_states.dims();
321        let device = lyric_hidden_states.device();
322        let (cos, sin) = rotary_cos_sin::<B>(seq_len, self.head_dim, self.rope_theta, &device);
323
324        let mut hidden = self.embed_tokens.forward(lyric_hidden_states);
325        for layer in &self.layers {
326            hidden = layer.forward(hidden, &cos, &sin, Some(&lyric_mask));
327        }
328        self.norm.forward(hidden)
329    }
330}
331
332/// Timbre encoder: embeds 750-frame (timbre_fix_frame) VAE latents of one
333/// reference clip per batch item and pools by taking position 0 of the final
334/// normed output. The checkpoint's `special_token` prepend is dead code
335/// upstream and is not modeled.
336#[derive(Module, Debug)]
337pub struct TimbreEncoder<B: Backend> {
338    pub embed_tokens: Linear<B>,
339    pub layers: Vec<AceStepEncoderLayer<B>>,
340    pub norm: ConditionRmsNorm<B>,
341    head_dim: usize,
342    rope_theta: f64,
343}
344
345impl<B: Backend> TimbreEncoder<B> {
346    pub fn new(config: &AceStepConfig, device: &B::Device) -> Self {
347        Self {
348            embed_tokens: linear_with_bias(
349                device,
350                config.audio_acoustic_hidden_dim,
351                config.hidden_size,
352            ),
353            layers: (0..config.num_timbre_encoder_hidden_layers)
354                .map(|layer_idx| AceStepEncoderLayer::new(config, layer_idx, device))
355                .collect(),
356            norm: ConditionRmsNorm::new(device, config.hidden_size, config.rms_norm_eps),
357            head_dim: config.head_dim,
358            rope_theta: config.rope_theta,
359        }
360    }
361
362    /// `refer_latents`: [B, timbre_fix_frame, audio_acoustic_hidden_dim] — one
363    /// reference clip per batch item (VAE-encoded silence for text2music).
364    /// Returns the pooled embedding [B, 1, hidden_size].
365    pub fn forward(&self, refer_latents: Tensor<B, 3>) -> Tensor<B, 3> {
366        let [batch, seq_len, _] = refer_latents.dims();
367        let device = refer_latents.device();
368        let (cos, sin) = rotary_cos_sin::<B>(seq_len, self.head_dim, self.rope_theta, &device);
369
370        let mut hidden = self.embed_tokens.forward(refer_latents);
371        for layer in &self.layers {
372            hidden = layer.forward(hidden, &cos, &sin, None);
373        }
374        let hidden = self.norm.forward(hidden);
375        let hidden_dim = self.norm.weight.dims()[0];
376        hidden.slice([0..batch, 0..1, 0..hidden_dim])
377    }
378}
379
380/// Audio token detokenizer: expands each 5Hz code embedding into
381/// `pool_window_size` (5) consecutive 25Hz latent frames. Each code is
382/// processed independently: embed → repeat 5× → add learned per-position
383/// `special_tokens` → 2 encoder layers → norm → project to the acoustic dim.
384#[derive(Module, Debug)]
385pub struct AudioTokenDetokenizer<B: Backend> {
386    pub embed_tokens: Linear<B>,
387    pub special_tokens: Param<Tensor<B, 3>>,
388    pub layers: Vec<AceStepEncoderLayer<B>>,
389    pub norm: ConditionRmsNorm<B>,
390    pub proj_out: Linear<B>,
391    pool_window_size: usize,
392    head_dim: usize,
393    rope_theta: f64,
394    acoustic_dim: usize,
395}
396
397impl<B: Backend> AudioTokenDetokenizer<B> {
398    pub fn new(config: &AceStepConfig, device: &B::Device) -> Self {
399        Self {
400            embed_tokens: linear_with_bias(device, config.hidden_size, config.hidden_size),
401            special_tokens: Param::from_tensor(Tensor::<B, 3>::zeros(
402                [1, config.pool_window_size, config.hidden_size],
403                device,
404            )),
405            layers: (0..config.num_attention_pooler_hidden_layers)
406                .map(|layer_idx| AceStepEncoderLayer::new(config, layer_idx, device))
407                .collect(),
408            norm: ConditionRmsNorm::new(device, config.hidden_size, config.rms_norm_eps),
409            proj_out: linear_with_bias(
410                device,
411                config.hidden_size,
412                config.audio_acoustic_hidden_dim,
413            ),
414            pool_window_size: config.pool_window_size,
415            head_dim: config.head_dim,
416            rope_theta: config.rope_theta,
417            acoustic_dim: config.audio_acoustic_hidden_dim,
418        }
419    }
420
421    /// `q`: [B, T5, hidden_size] (FSQ `project_out` output) →
422    /// 25Hz hint latents [B, T5 * pool_window_size, audio_acoustic_hidden_dim].
423    pub fn forward(&self, q: Tensor<B, 3>) -> Tensor<B, 3> {
424        let [batch, t5, _] = q.dims();
425        let device = q.device();
426        let (cos, sin) = rotary_cos_sin::<B>(
427            self.pool_window_size,
428            self.head_dim,
429            self.rope_theta,
430            &device,
431        );
432
433        let embedded = self.embed_tokens.forward(q);
434        let mut hidden = self.expand_codes(embedded);
435        for layer in &self.layers {
436            hidden = layer.forward(hidden, &cos, &sin, None);
437        }
438        let hidden = self.norm.forward(hidden);
439        self.proj_out.forward(hidden).reshape([
440            batch,
441            t5 * self.pool_window_size,
442            self.acoustic_dim,
443        ])
444    }
445
446    /// [B, T5, hidden] → [(B·T5), pool_window_size, hidden]: repeat each code
447    /// 5× along a new axis and add the learned per-position special tokens.
448    fn expand_codes(&self, embedded: Tensor<B, 3>) -> Tensor<B, 3> {
449        let [batch, t5, hidden_size] = embedded.dims();
450        let repeated = embedded
451            .unsqueeze_dim::<4>(2)
452            .repeat_dim(2, self.pool_window_size);
453        let special = self.special_tokens.val().unsqueeze_dim::<4>(1);
454        (repeated + special).reshape([batch * t5, self.pool_window_size, hidden_size])
455    }
456}
457
458/// Loadable root of the condition stack: text projector, lyric encoder,
459/// timbre encoder, detokenizer and the FSQ quantizer (decode direction, used
460/// to turn 5Hz LM codes into continuous hints for the detokenizer).
461#[derive(Module, Debug)]
462pub struct AceStepCondition<B: Backend> {
463    pub text_projector: Linear<B>,
464    pub lyric_encoder: LyricEncoder<B>,
465    pub timbre_encoder: TimbreEncoder<B>,
466    pub detokenizer: AudioTokenDetokenizer<B>,
467    pub quantizer: ResidualFsq<B>,
468}
469
470impl<B: Backend> AceStepCondition<B> {
471    pub fn new(config: &AceStepConfig, device: &B::Device) -> Self {
472        Self {
473            text_projector: linear_no_bias(device, config.text_hidden_dim, config.hidden_size),
474            lyric_encoder: LyricEncoder::new(config, device),
475            timbre_encoder: TimbreEncoder::new(config, device),
476            detokenizer: AudioTokenDetokenizer::new(config, device),
477            quantizer: ResidualFsq::new(device),
478        }
479    }
480
481    /// Load weights from a burnpack file whose keys are the canonical tensor
482    /// names documented at the top of this module.
483    pub fn from_burnpack(config: &AceStepConfig, path: &Path, device: &B::Device) -> Result<Self> {
484        let mut model = Self::new(config, device);
485        let mut store = BurnpackStore::from_file(path).zero_copy(true);
486        model
487            .load_from(&mut store)
488            .with_context(|| format!("failed to load condition weights from {}", path.display()))?;
489        Ok(model)
490    }
491
492    /// Assemble the conditioning sequence for the DiT.
493    ///
494    /// - `text_hidden`: [B, Lt, text_hidden_dim] — Qwen3-Embedding caption states
495    /// - `lyric_hidden`: [B, Ll, text_hidden_dim] — Qwen3-Embedding lyric states
496    /// - `lyric_mask`: [B, Ll] Int (1 = valid)
497    /// - `silence_ref_latents`: [B, timbre_fix_frame, audio_acoustic_hidden_dim] —
498    ///   timbre reference (VAE-encoded silence for text2music)
499    ///
500    /// Returns `encoder_hidden_states` [B, S, hidden_size] with
501    /// S = Ll + 1 + Lt, layout [valid lyric…, valid timbre…, valid text…,
502    /// (masked tail)]. The final mask is computed internally (it drives the
503    /// lyric encoder's padding behavior via `pack_sequences`) but is NOT
504    /// returned: the DiT discards it and cross-attends to the entire packed
505    /// sequence, tail included.
506    pub fn encode(
507        &self,
508        text_hidden: Tensor<B, 3>,
509        lyric_hidden: Tensor<B, 3>,
510        lyric_mask: Tensor<B, 2, Int>,
511        silence_ref_latents: Tensor<B, 3>,
512    ) -> Tensor<B, 3> {
513        let [batch, text_len, _] = text_hidden.dims();
514        let device = text_hidden.device();
515
516        let lyric = self.lyric_encoder.forward(lyric_hidden, lyric_mask.clone());
517        let timbre = self.timbre_encoder.forward(silence_ref_latents);
518        let text = self.text_projector.forward(text_hidden);
519
520        let timbre_mask = Tensor::<B, 2, Int>::ones([batch, 1], &device);
521        let text_mask = Tensor::<B, 2, Int>::ones([batch, text_len], &device);
522
523        let (packed, mask) = pack_sequences(lyric, timbre, lyric_mask, timbre_mask);
524        let (encoder_hidden_states, _final_mask) = pack_sequences(packed, text, mask, text_mask);
525        encoder_hidden_states
526    }
527
528    /// 5Hz LM codes [B, T5] Int → 25Hz hint latents
529    /// [B, T5 * pool_window_size, audio_acoustic_hidden_dim]
530    /// (`quantizer.decode_indices` + detokenizer). These REPLACE `src_latents`
531    /// in the DiT input when is_covers > 0.
532    pub fn codes_to_hints(&self, audio_codes: Tensor<B, 2, Int>) -> Tensor<B, 3> {
533        let hints = self.quantizer.decode_indices(audio_codes);
534        self.detokenizer.forward(hints)
535    }
536}
537
538/// Concatenate two [B, L1/L2, D] sequences with their [B, L] masks along the
539/// sequence dimension, then stable-sort each batch row so mask=1 positions
540/// come first, preserving relative order within equal mask values. The new
541/// mask marks the first `sum(mask)` positions of each row as valid.
542///
543/// Runs on the host (B is 1 in practice, and this is called twice per
544/// generation); the returned hidden states keep the input dtype.
545pub fn pack_sequences<B: Backend>(
546    hidden1: Tensor<B, 3>,
547    hidden2: Tensor<B, 3>,
548    mask1: Tensor<B, 2, Int>,
549    mask2: Tensor<B, 2, Int>,
550) -> (Tensor<B, 3>, Tensor<B, 2, Int>) {
551    let [batch, len1, dim] = hidden1.dims();
552    let len2 = hidden2.dims()[1];
553    let len = len1 + len2;
554    let dtype = hidden1.dtype();
555    let device = hidden1.device();
556
557    let hidden1_data = hidden1
558        .to_data()
559        .convert::<f32>()
560        .to_vec::<f32>()
561        .expect("hidden1 must materialize as f32");
562    let hidden2_data = hidden2
563        .to_data()
564        .convert::<f32>()
565        .to_vec::<f32>()
566        .expect("hidden2 must materialize as f32");
567    let mask1_data = mask1
568        .to_data()
569        .convert::<i64>()
570        .to_vec::<i64>()
571        .expect("mask1 must materialize as i64");
572    let mask2_data = mask2
573        .to_data()
574        .convert::<i64>()
575        .to_vec::<i64>()
576        .expect("mask2 must materialize as i64");
577
578    let mut packed = vec![0.0f32; batch * len * dim];
579    let mut new_mask = vec![0i64; batch * len];
580
581    for b in 0..batch {
582        // Stable descending sort by mask: valid (1) first, order preserved
583        // within each group (Vec::sort_by_key is stable).
584        let mut order: Vec<(i64, usize)> = (0..len1)
585            .map(|i| (mask1_data[b * len1 + i], i))
586            .chain((0..len2).map(|j| (mask2_data[b * len2 + j], len1 + j)))
587            .collect();
588        order.sort_by_key(|&(mask, _)| std::cmp::Reverse(mask));
589
590        let valid_count = order.iter().filter(|&&(mask, _)| mask > 0).count();
591        for (new_pos, &(_, old_pos)) in order.iter().enumerate() {
592            let source = if old_pos < len1 {
593                &hidden1_data[(b * len1 + old_pos) * dim..][..dim]
594            } else {
595                &hidden2_data[(b * len2 + old_pos - len1) * dim..][..dim]
596            };
597            packed[(b * len + new_pos) * dim..][..dim].copy_from_slice(source);
598        }
599        for (new_pos, slot) in new_mask[b * len..(b + 1) * len].iter_mut().enumerate() {
600            *slot = i64::from(new_pos < valid_count);
601        }
602    }
603
604    let packed =
605        Tensor::<B, 3>::from_data(TensorData::new(packed, [batch, len, dim]), &device).cast(dtype);
606    let new_mask = Tensor::<B, 2, Int>::from_data(TensorData::new(new_mask, [batch, len]), &device);
607    (packed, new_mask)
608}
609
610/// Additive [B, 1, L, L] attention mask: 0 where query i may attend to key j,
611/// `f32::MIN` elsewhere. A key is allowed when it is inside the sliding window
612/// (`|i - j| <= window`, if any) AND not padding.
613fn additive_attention_mask<B: Backend>(
614    batch: usize,
615    seq_len: usize,
616    sliding_window: Option<usize>,
617    padding: Option<Vec<i64>>,
618    device: &B::Device,
619) -> Tensor<B, 4> {
620    let mut values = vec![f32::MIN; batch * seq_len * seq_len];
621    for b in 0..batch {
622        for i in 0..seq_len {
623            for j in 0..seq_len {
624                let within_window = sliding_window.is_none_or(|window| i.abs_diff(j) <= window);
625                let key_valid = padding
626                    .as_ref()
627                    .is_none_or(|mask| mask[b * seq_len + j] != 0);
628                if within_window && key_valid {
629                    values[(b * seq_len + i) * seq_len + j] = 0.0;
630                }
631            }
632        }
633    }
634    Tensor::<B, 4>::from_data(
635        TensorData::new(values, [batch, 1, seq_len, seq_len]),
636        device,
637    )
638}
639
640/// inv_freq[i] = 1 / theta^(2i / head_dim); cos/sin over positions 0..seq_len
641/// with duplicated halves (cat([freqs, freqs])). Returned as [1, L, 1, head_dim]
642/// so they broadcast against [batch, seq, heads, head_dim].
643fn rotary_cos_sin<B: Backend>(
644    seq_len: usize,
645    head_dim: usize,
646    theta: f64,
647    device: &B::Device,
648) -> (Tensor<B, 4>, Tensor<B, 4>) {
649    let half = head_dim / 2;
650    let inv_freq: Vec<f32> = (0..half)
651        .map(|i| 1.0 / theta.powf(2.0 * i as f64 / head_dim as f64) as f32)
652        .collect();
653    let mut cos_values = Vec::with_capacity(seq_len * head_dim);
654    let mut sin_values = Vec::with_capacity(seq_len * head_dim);
655    for pos in 0..seq_len {
656        for _ in 0..2 {
657            for &freq in &inv_freq {
658                let angle = pos as f32 * freq;
659                cos_values.push(angle.cos());
660                sin_values.push(angle.sin());
661            }
662        }
663    }
664    let shape = [1, seq_len, 1, head_dim];
665    (
666        Tensor::<B, 4>::from_data(TensorData::new(cos_values, shape), device),
667        Tensor::<B, 4>::from_data(TensorData::new(sin_values, shape), device),
668    )
669}
670
671fn rotate_half<B: Backend>(x: Tensor<B, 4>) -> Tensor<B, 4> {
672    let [batch, seq_len, heads, head_dim] = x.dims();
673    let half = head_dim / 2;
674    let x1 = x.clone().slice([0..batch, 0..seq_len, 0..heads, 0..half]);
675    let x2 = x.slice([0..batch, 0..seq_len, 0..heads, half..head_dim]);
676    Tensor::cat(vec![x2.neg(), x1], 3)
677}
678
679fn apply_rotary_pos_emb<B: Backend>(
680    x: Tensor<B, 4>,
681    cos: &Tensor<B, 4>,
682    sin: &Tensor<B, 4>,
683) -> Tensor<B, 4> {
684    x.clone() * cos.clone() + rotate_half(x) * sin.clone()
685}
686
687/// [batch, kv_heads, seq, head_dim] -> [batch, kv_heads * repeats, seq, head_dim]
688fn repeat_kv<B: Backend>(tensor: Tensor<B, 4>, repeats: usize) -> Tensor<B, 4> {
689    let [batch, heads, seq_len, head_dim] = tensor.dims();
690    tensor
691        .unsqueeze_dim::<5>(2)
692        .repeat_dim(2, repeats)
693        .reshape([batch, heads * repeats, seq_len, head_dim])
694}
695
696fn linear_no_bias<B: Backend>(device: &B::Device, d_input: usize, d_output: usize) -> Linear<B> {
697    LinearConfig::new(d_input, d_output)
698        .with_bias(false)
699        .with_layout(LinearLayout::Col)
700        .init(device)
701}
702
703fn linear_with_bias<B: Backend>(device: &B::Device, d_input: usize, d_output: usize) -> Linear<B> {
704    LinearConfig::new(d_input, d_output)
705        .with_layout(LinearLayout::Col)
706        .init(device)
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712    use burn::backend::NdArray;
713
714    type TestBackend = NdArray<f32>;
715
716    fn tiny_config() -> AceStepConfig {
717        AceStepConfig {
718            hidden_size: 32,
719            intermediate_size: 64,
720            num_hidden_layers: 2,
721            num_attention_heads: 4,
722            num_key_value_heads: 2,
723            head_dim: 8,
724            rms_norm_eps: 1e-6,
725            rope_theta: 1_000_000.0,
726            sliding_window: 2,
727            in_channels: 12,
728            audio_acoustic_hidden_dim: 4,
729            patch_size: 2,
730            text_hidden_dim: 8,
731            num_lyric_encoder_hidden_layers: 2,
732            num_timbre_encoder_hidden_layers: 2,
733            timbre_fix_frame: 750,
734            pool_window_size: 5,
735            num_attention_pooler_hidden_layers: 2,
736            fsq_dim: 32,
737            fsq_input_levels: vec![8, 8, 8, 5, 5, 5],
738            vocab_size: 64003,
739            layer_types: vec![
740                "sliding_attention".to_string(),
741                "full_attention".to_string(),
742            ],
743            is_turbo: true,
744        }
745    }
746
747    fn assert_finite(tensor: Tensor<TestBackend, 3>) {
748        let values = tensor
749            .to_data()
750            .to_vec::<f32>()
751            .expect("output should materialize as f32");
752        assert!(
753            values.iter().all(|v| v.is_finite()),
754            "output contains non-finite values"
755        );
756    }
757
758    #[test]
759    fn encode_shapes_and_finiteness() {
760        let config = tiny_config();
761        let device = Default::default();
762        let condition = AceStepCondition::<TestBackend>::new(&config, &device);
763
764        let text_hidden =
765            Tensor::from_data(TensorData::new(vec![0.01f32; 3 * 8], [1, 3, 8]), &device);
766        let lyric_hidden =
767            Tensor::from_data(TensorData::new(vec![-0.02f32; 2 * 8], [1, 2, 8]), &device);
768        let lyric_mask = Tensor::<TestBackend, 2, Int>::from_data([[1, 1]], &device);
769        let silence_ref_latents = Tensor::zeros([1, 750, 4], &device);
770
771        let encoded = condition.encode(text_hidden, lyric_hidden, lyric_mask, silence_ref_latents);
772        // S = Ll (2) + 1 (timbre) + Lt (3) = 6.
773        assert_eq!(encoded.dims(), [1, 6, 32]);
774        assert_finite(encoded);
775    }
776
777    #[test]
778    fn pack_sequences_stable_sorts_valid_first() {
779        let device = Default::default();
780        // Batch row 0: masks [1,0,1] + [0,1] -> order 10,30,50,20,40, mask 11100.
781        // Batch row 1: masks [0,1,0] + [1,0] -> order 70,90,60,80,100, mask 11000.
782        let hidden1 = Tensor::<TestBackend, 3>::from_data(
783            [[[10.0], [20.0], [30.0]], [[60.0], [70.0], [80.0]]],
784            &device,
785        );
786        let hidden2 =
787            Tensor::<TestBackend, 3>::from_data([[[40.0], [50.0]], [[90.0], [100.0]]], &device);
788        let mask1 = Tensor::<TestBackend, 2, Int>::from_data([[1, 0, 1], [0, 1, 0]], &device);
789        let mask2 = Tensor::<TestBackend, 2, Int>::from_data([[0, 1], [1, 0]], &device);
790
791        let (packed, new_mask) = pack_sequences(hidden1, hidden2, mask1, mask2);
792        assert_eq!(packed.dims(), [2, 5, 1]);
793        let packed_values = packed.to_data().to_vec::<f32>().expect("packed values");
794        assert_eq!(
795            packed_values,
796            vec![10.0, 30.0, 50.0, 20.0, 40.0, 70.0, 90.0, 60.0, 80.0, 100.0]
797        );
798        let mask_values: Vec<i64> = new_mask
799            .to_data()
800            .convert::<i64>()
801            .to_vec::<i64>()
802            .expect("mask values");
803        assert_eq!(mask_values, vec![1, 1, 1, 0, 0, 1, 1, 0, 0, 0]);
804    }
805
806    #[test]
807    fn codes_to_hints_shape_and_finiteness() {
808        let config = tiny_config();
809        let device = Default::default();
810        let mut condition = AceStepCondition::<TestBackend>::new(&config, &device);
811        // The real quantizer is fixed at 2048 dims (FSQ contract); swap in a
812        // tiny project_out so the detokenizer sees the tiny hidden size.
813        condition.quantizer = ResidualFsq {
814            project_in: LinearConfig::new(32, 6).with_bias(true).init(&device),
815            project_out: LinearConfig::new(6, 32).with_bias(true).init(&device),
816        };
817
818        let codes = Tensor::<TestBackend, 2, Int>::from_data([[0, 63999]], &device);
819        let hints = condition.codes_to_hints(codes);
820        assert_eq!(hints.dims(), [1, 10, 4]);
821        assert_finite(hints);
822    }
823
824    #[test]
825    fn detokenizer_expand_repeats_and_adds_special_tokens() {
826        let config = tiny_config();
827        let device = Default::default();
828        let mut detokenizer = AudioTokenDetokenizer::<TestBackend>::new(&config, &device);
829
830        // Known special tokens: special[0, p, h] = (p * 32 + h) * 0.001.
831        let special: Vec<f32> = (0..5 * 32).map(|i| i as f32 * 0.001).collect();
832        detokenizer.special_tokens = Param::from_tensor(Tensor::from_data(
833            TensorData::new(special, [1, 5, 32]),
834            &device,
835        ));
836
837        let q: Vec<f32> = (0..2 * 32).map(|i| i as f32 * 0.01).collect();
838        let q = Tensor::<TestBackend, 3>::from_data(TensorData::new(q, [1, 2, 32]), &device);
839        let embedded = detokenizer.embed_tokens.forward(q);
840        let expanded = detokenizer.expand_codes(embedded.clone());
841        assert_eq!(expanded.dims(), [2, 5, 32]);
842
843        let embedded_values = embedded.to_data().to_vec::<f32>().expect("embedded");
844        let expanded_values = expanded.to_data().to_vec::<f32>().expect("expanded");
845        for t in 0..2 {
846            for p in 0..5 {
847                for h in 0..32 {
848                    let expected = embedded_values[t * 32 + h] + (p * 32 + h) as f32 * 0.001;
849                    let actual = expanded_values[(t * 5 + p) * 32 + h];
850                    assert!(
851                        (actual - expected).abs() < 1e-5,
852                        "mismatch at t={t} p={p} h={h}: {actual} vs {expected}"
853                    );
854                }
855            }
856        }
857    }
858
859    #[test]
860    fn lyric_encoder_ignores_padded_values() {
861        let config = tiny_config();
862        let device = Default::default();
863        let encoder = LyricEncoder::<TestBackend>::new(&config, &device);
864        let mask = Tensor::<TestBackend, 2, Int>::from_data([[1, 0]], &device);
865
866        let mut row_a = vec![0.05f32; 8];
867        row_a.extend(vec![1.0f32; 8]);
868        let mut row_b = vec![0.05f32; 8];
869        row_b.extend(vec![-7.0f32; 8]);
870        let input_a =
871            Tensor::<TestBackend, 3>::from_data(TensorData::new(row_a, [1, 2, 8]), &device);
872        let input_b =
873            Tensor::<TestBackend, 3>::from_data(TensorData::new(row_b, [1, 2, 8]), &device);
874
875        let out_a = encoder.forward(input_a, mask.clone());
876        let out_b = encoder.forward(input_b, mask);
877        let valid_a = out_a
878            .slice([0..1, 0..1, 0..32])
879            .to_data()
880            .to_vec::<f32>()
881            .expect("out_a");
882        let valid_b = out_b
883            .slice([0..1, 0..1, 0..32])
884            .to_data()
885            .to_vec::<f32>()
886            .expect("out_b");
887        for (a, b) in valid_a.iter().zip(valid_b.iter()) {
888            assert!(
889                (a - b).abs() < 1e-5,
890                "padded key values leaked into valid output: {a} vs {b}"
891            );
892        }
893    }
894}