Skip to main content

candle_transformers/models/gemma4/
audio.rs

1//! Gemma 4 audio encoder (Conformer-based).
2//!
3//! SSCP conv projection + conformer blocks with chunked attention,
4//! relative position embeddings, light conv1d, and clippable linears.
5
6use candle::{DType, Module, Result, Tensor, D};
7use candle_nn::{Conv1d, Conv2d, Conv2dConfig, VarBuilder};
8
9use super::config::Gemma4AudioConfig;
10
11// ── RmsNorm (standard, no +1 offset for audio) ─────────────────────────────
12
13#[derive(Debug, Clone)]
14struct RmsNorm {
15    weight: Tensor,
16    eps: f64,
17}
18
19impl RmsNorm {
20    fn new(dim: usize, eps: f64, vb: VarBuilder) -> Result<Self> {
21        let weight = vb.get(dim, "weight")?;
22        Ok(Self { weight, eps })
23    }
24}
25
26impl Module for RmsNorm {
27    fn forward(&self, x: &Tensor) -> Result<Tensor> {
28        let x_dtype = x.dtype();
29        let internal_dtype = match x_dtype {
30            DType::F16 | DType::BF16 => DType::F32,
31            d => d,
32        };
33        let hidden_size = x.dim(D::Minus1)?;
34        let x = x.to_dtype(internal_dtype)?;
35        let norm_x = (x.sqr()?.sum_keepdim(D::Minus1)? / hidden_size as f64)?;
36        let x_normed = x.broadcast_div(&(norm_x + self.eps)?.sqrt()?)?;
37        x_normed.to_dtype(x_dtype)?.broadcast_mul(&self.weight)
38    }
39}
40
41// ── LayerNorm (for SSCP conv blocks) ────────────────────────────────────────
42
43#[derive(Debug, Clone)]
44struct LayerNorm {
45    eps: f64,
46    dim: usize,
47}
48
49impl LayerNorm {
50    fn new(dim: usize, eps: f64) -> Self {
51        Self { eps, dim }
52    }
53}
54
55impl Module for LayerNorm {
56    fn forward(&self, x: &Tensor) -> Result<Tensor> {
57        let x_dtype = x.dtype();
58        let x = x.to_dtype(DType::F32)?;
59        let mean = x.mean_keepdim(D::Minus1)?;
60        let x = x.broadcast_sub(&mean)?;
61        let var = (x.sqr()?.sum_keepdim(D::Minus1)? / self.dim as f64)?;
62        let x = x.broadcast_div(&(var + self.eps)?.sqrt()?)?;
63        x.to_dtype(x_dtype)
64    }
65}
66
67// ── SSCP Conv Blocks ────────────────────────────────────────────────────────
68
69#[derive(Debug, Clone)]
70struct SSCPConvBlock {
71    conv: Conv2d,
72    norm: LayerNorm,
73    manual_padding: (usize, usize, usize, usize), // (f_left, f_right, t_top, t_bottom)
74    time_stride: usize,
75    #[allow(dead_code)]
76    out_channels: usize,
77}
78
79impl SSCPConvBlock {
80    fn new(
81        cfg: &Gemma4AudioConfig,
82        idx: usize,
83        input_freq_dim: usize,
84        vb: VarBuilder,
85    ) -> Result<Self> {
86        let in_channels = if idx == 0 {
87            1
88        } else {
89            cfg.sscp_conv_channel_size[idx - 1]
90        };
91        let out_channels = cfg.sscp_conv_channel_size[idx];
92        let kernel_t = cfg.sscp_conv_kernel_size[idx][0];
93        let _kernel_f = cfg.sscp_conv_kernel_size[idx][1];
94        let stride_t = cfg.sscp_conv_stride_size[idx][0];
95        let _stride_f = cfg.sscp_conv_stride_size[idx][1];
96
97        // Semicausal padding
98        let half = kernel_t / 2;
99        let (pad_t_top, pad_t_bottom) = (half, half);
100        let pad_f_left = 1;
101        let pad_f_right = 1;
102
103        let _ = input_freq_dim; // used for future freq-dim tracking
104
105        let conv = candle_nn::conv2d_no_bias(
106            in_channels,
107            out_channels,
108            kernel_t, // assumes kernel_t == kernel_f
109            Conv2dConfig {
110                stride: stride_t,
111                padding: 0,
112                dilation: 1,
113                groups: 1,
114                cudnn_fwd_algo: None,
115            },
116            vb.pp("conv"),
117        )?;
118        let norm = LayerNorm::new(out_channels, cfg.rms_norm_eps);
119
120        Ok(Self {
121            conv,
122            norm,
123            manual_padding: (pad_f_left, pad_f_right, pad_t_top, pad_t_bottom),
124            time_stride: stride_t,
125            out_channels,
126        })
127    }
128
129    fn forward(
130        &self,
131        audio_encodings: &Tensor,
132        audio_mel_mask: &Tensor,
133    ) -> Result<(Tensor, Tensor)> {
134        // Zero out padded positions
135        let valid_mask = audio_mel_mask
136            .eq(0.0)?
137            .unsqueeze(1)?
138            .unsqueeze(D::Minus1)?
139            .to_dtype(audio_encodings.dtype())?;
140        let audio_encodings = audio_encodings.broadcast_mul(&valid_mask)?;
141
142        // Manual padding
143        let audio_encodings = audio_encodings
144            .pad_with_zeros(D::Minus1, self.manual_padding.0, self.manual_padding.1)?
145            .pad_with_zeros(D::Minus2, self.manual_padding.2, self.manual_padding.3)?;
146
147        let audio_encodings = self.conv.forward(&audio_encodings)?;
148
149        // Subsample mask
150        let t_out = audio_encodings.dim(2)?;
151        let output_mask = subsample_mask(audio_mel_mask, self.time_stride, t_out)?;
152
153        // Norm: permute to (b, t, f, c), norm on c, then back
154        let x = audio_encodings.permute((0, 2, 3, 1))?;
155        let x = self.norm.forward(&x)?;
156        let x = x.permute((0, 3, 1, 2))?.relu()?;
157        Ok((x, output_mask))
158    }
159}
160
161fn subsample_mask(mask: &Tensor, stride: usize, target_len: usize) -> Result<Tensor> {
162    let mask_len = mask.dim(1)?;
163    let indices: Vec<u32> = (0..target_len)
164        .map(|i| (i * stride).min(mask_len - 1) as u32)
165        .collect();
166    let indices = Tensor::from_vec(indices, target_len, mask.device())?;
167    mask.index_select(&indices, 1)
168}
169
170// ── SubSampleConvProjection ─────────────────────────────────────────────────
171
172#[derive(Debug, Clone)]
173struct SubSampleConvProjection {
174    conv_0: SSCPConvBlock,
175    conv_1: SSCPConvBlock,
176    input_proj_linear: candle_nn::Linear,
177}
178
179impl SubSampleConvProjection {
180    fn new(cfg: &Gemma4AudioConfig, vb: VarBuilder) -> Result<Self> {
181        let mut current_f = cfg.input_feat_size;
182        let mut f_out_dims = Vec::new();
183
184        for i in 0..2 {
185            let kernel_w = cfg.sscp_conv_kernel_size[i][1];
186            let stride_w = cfg.sscp_conv_stride_size[i][1];
187            let f_in_padded = current_f + 2; // pad_f_left + pad_f_right
188            let f_out = (f_in_padded - kernel_w) / stride_w + 1;
189            f_out_dims.push(f_out);
190            current_f = f_out;
191        }
192
193        let conv_0 = SSCPConvBlock::new(cfg, 0, cfg.input_feat_size, vb.pp("layer0"))?;
194        let conv_1 = SSCPConvBlock::new(cfg, 1, f_out_dims[0], vb.pp("layer1"))?;
195
196        let final_c_out = cfg.sscp_conv_channel_size[1];
197        let final_f_out = f_out_dims[1];
198        let input_proj_linear = candle_nn::linear_no_bias(
199            final_c_out * final_f_out,
200            cfg.hidden_size,
201            vb.pp("input_proj_linear"),
202        )?;
203
204        Ok(Self {
205            conv_0,
206            conv_1,
207            input_proj_linear,
208        })
209    }
210
211    fn forward(&self, audio_mel: &Tensor, audio_mel_mask: &Tensor) -> Result<(Tensor, Tensor)> {
212        let x = audio_mel.unsqueeze(1)?;
213        let (x, mask) = self.conv_0.forward(&x, audio_mel_mask)?;
214        let (x, mask) = self.conv_1.forward(&x, &mask)?;
215
216        let (b, c_out, t_out, f_out) = x.dims4()?;
217        let x = x
218            .transpose(1, 2)?
219            .transpose(2, 3)?
220            .reshape((b, t_out, f_out * c_out))?;
221        Ok((self.input_proj_linear.forward(&x)?, mask))
222    }
223}
224
225// ── Relative Position Embedding ─────────────────────────────────────────────
226
227#[derive(Debug, Clone)]
228struct RelativePositionEmbedding {
229    pos_proj: candle_nn::Linear,
230    inv_timescales: Tensor,
231    pos_indices: Tensor,
232    num_heads: usize,
233    head_dim: usize,
234}
235
236impl RelativePositionEmbedding {
237    fn new(cfg: &Gemma4AudioConfig, vb: VarBuilder) -> Result<Self> {
238        let num_heads = cfg.conf_num_attention_heads;
239        let channels = cfg.hidden_size;
240        let head_dim = channels / num_heads;
241        let max_backward = cfg.conf_attention_context_left.saturating_sub(1);
242        let max_forward = cfg.conf_attention_context_right;
243        let num_timescales = channels / 2;
244
245        let pos_proj =
246            candle_nn::linear_no_bias(channels, num_heads * head_dim, vb.pp("relative_k_proj"))?;
247
248        let min_timescale = 1.0_f64;
249        let max_timescale = 10_000.0_f64;
250        let log_timescale_increment =
251            (max_timescale / min_timescale).ln() / num_timescales.saturating_sub(1).max(1) as f64;
252        let inv_timescales = Tensor::from_vec(
253            (0..num_timescales)
254                .map(|i| (min_timescale * (-log_timescale_increment * i as f64).exp()) as f32)
255                .collect::<Vec<_>>(),
256            (1, 1, num_timescales),
257            vb.device(),
258        )?;
259
260        let pos_values: Vec<i64> = (-(max_forward as i64)..=max_backward as i64)
261            .rev()
262            .collect();
263        let span = pos_values.len();
264        let pos_indices = Tensor::from_vec(pos_values, (1, span), vb.device())?;
265
266        Ok(Self {
267            pos_proj,
268            inv_timescales,
269            pos_indices,
270            num_heads,
271            head_dim,
272        })
273    }
274
275    fn get_timing_signal(&self, position: &Tensor, dtype: DType) -> Result<Tensor> {
276        let position = position.to_dtype(DType::F32)?.unsqueeze(D::Minus1)?;
277        let inv_timescales = self.inv_timescales.to_device(position.device())?;
278        let scaled_time = position.broadcast_mul(&inv_timescales)?;
279        let sin_emb = scaled_time.sin()?;
280        let cos_emb = scaled_time.cos()?;
281        Tensor::cat(&[sin_emb, cos_emb], D::Minus1)?.to_dtype(dtype)
282    }
283
284    fn forward(&self, queries: &Tensor, keys: &Tensor) -> Result<Tensor> {
285        let (batch_size, num_query_blocks, query_block_size, _num_heads, head_dim) =
286            queries.dims5()?;
287        let key_context_size = keys.dim(2)?;
288        let max_span_plus_1 = self.pos_indices.dim(1)?;
289
290        let pos_indices = self.pos_indices.to_device(queries.device())?;
291        let sin_emb_timing = self.get_timing_signal(&pos_indices, queries.dtype())?;
292        let projected_sin_emb = self.pos_proj.forward(&sin_emb_timing)?;
293        let sin_emb = projected_sin_emb
294            .reshape((1, max_span_plus_1, self.num_heads, self.head_dim))?
295            .squeeze(0)?;
296
297        // term_ac: query * key^T
298        let queries_p = queries.transpose(1, 3)?.transpose(2, 3)?.contiguous()?;
299        let keys_p_t = keys
300            .transpose(1, 3)?
301            .transpose(2, 3)?
302            .transpose(3, 4)?
303            .contiguous()?;
304
305        let queries_3d = queries_p.reshape((
306            batch_size * self.num_heads * num_query_blocks,
307            query_block_size,
308            head_dim,
309        ))?;
310        let keys_3d = keys_p_t.reshape((
311            batch_size * self.num_heads * num_query_blocks,
312            head_dim,
313            key_context_size,
314        ))?;
315        let term_ac = queries_3d.matmul(&keys_3d)?.reshape((
316            batch_size,
317            self.num_heads,
318            num_query_blocks,
319            query_block_size,
320            key_context_size,
321        ))?;
322
323        // term_bd: query * sin_emb^T (relative position bias)
324        let q_transposed = queries.transpose(1, 3)?.transpose(2, 3)?;
325        let s_transposed = sin_emb.transpose(0, 2)?.transpose(0, 1)?;
326        let q_reshaped = q_transposed.reshape((
327            batch_size * self.num_heads,
328            num_query_blocks * query_block_size,
329            head_dim,
330        ))?;
331        let s_broadcast = s_transposed
332            .unsqueeze(0)?
333            .broadcast_as((batch_size, self.num_heads, head_dim, max_span_plus_1))?
334            .reshape((batch_size * self.num_heads, head_dim, max_span_plus_1))?
335            .contiguous()?;
336        let term_bd_unshifted = q_reshaped.contiguous()?.matmul(&s_broadcast)?.reshape((
337            batch_size,
338            self.num_heads,
339            num_query_blocks,
340            query_block_size,
341            max_span_plus_1,
342        ))?;
343
344        // Relative shift
345        let pad_amount = (key_context_size + 1) - max_span_plus_1;
346        let term_bd_padded = term_bd_unshifted.pad_with_zeros(D::Minus1, 0, pad_amount)?;
347        let term_bd_reshaped = term_bd_padded.reshape((
348            batch_size,
349            self.num_heads,
350            num_query_blocks,
351            query_block_size * (key_context_size + 1),
352        ))?;
353        let term_bd_sliced =
354            term_bd_reshaped.narrow(D::Minus1, 0, query_block_size * key_context_size)?;
355        let term_bd = term_bd_sliced.reshape((
356            batch_size,
357            self.num_heads,
358            num_query_blocks,
359            query_block_size,
360            key_context_size,
361        ))?;
362
363        term_ac.broadcast_add(&term_bd)
364    }
365}
366
367// ── Conformer Attention ─────────────────────────────────────────────────────
368
369#[derive(Debug, Clone)]
370struct ConformerAttention {
371    q_proj: candle_nn::Linear,
372    k_proj: candle_nn::Linear,
373    v_proj: candle_nn::Linear,
374    post: candle_nn::Linear,
375    relative_position_embedding: RelativePositionEmbedding,
376    per_dim_scale_softplus: Tensor,
377    pre_attn_norm: RmsNorm,
378    post_norm: RmsNorm,
379    num_heads: usize,
380    head_dim: usize,
381    chunk_size: usize,
382    max_past_horizon: usize,
383    max_future_horizon: usize,
384    context_size: usize,
385    q_scale: f64,
386    k_scale: f64,
387    softcap: f64,
388    invalid_logits_value: f64,
389    local_causal_valid_mask: Tensor,
390    gradient_clipping: f64,
391    hidden_size: usize,
392}
393
394impl ConformerAttention {
395    fn new(cfg: &Gemma4AudioConfig, vb: VarBuilder) -> Result<Self> {
396        let num_heads = cfg.conf_num_attention_heads;
397        let hidden_size = cfg.hidden_size;
398        let head_dim = hidden_size / num_heads;
399        let chunk_size = cfg.conf_attention_chunk_size;
400        let max_past_horizon = cfg.conf_attention_context_left.saturating_sub(1);
401        let max_future_horizon = cfg.conf_attention_context_right;
402        let context_size = chunk_size + max_past_horizon + max_future_horizon;
403
404        let attn_vb = vb.pp("self_attn");
405        let relative_position_embedding = RelativePositionEmbedding::new(cfg, attn_vb.clone())?;
406        let per_dim_scale = attn_vb.get(head_dim, "per_dim_scale")?;
407        let q_proj =
408            candle_nn::linear_no_bias(hidden_size, num_heads * head_dim, attn_vb.pp("q_proj"))?;
409        let k_proj =
410            candle_nn::linear_no_bias(hidden_size, num_heads * head_dim, attn_vb.pp("k_proj"))?;
411        let v_proj =
412            candle_nn::linear_no_bias(hidden_size, num_heads * head_dim, attn_vb.pp("v_proj"))?;
413        let post = candle_nn::linear_no_bias(hidden_size, hidden_size, attn_vb.pp("post"))?;
414
415        let pre_attn_norm = RmsNorm::new(hidden_size, cfg.rms_norm_eps, vb.pp("norm_pre_attn"))?;
416        let post_norm = RmsNorm::new(hidden_size, cfg.rms_norm_eps, vb.pp("norm_post_attn"))?;
417
418        let q_scale = (head_dim as f64).powf(-0.5) / 2.0_f64.ln();
419        let k_scale = (1.0_f64 + std::f64::consts::E).ln() / 2.0_f64.ln();
420
421        // Build local causal valid mask
422        let mut mask_vec = vec![0u8; chunk_size * context_size];
423        for i in 0..chunk_size {
424            for j in 0..context_size {
425                let lower = j >= i;
426                let upper =
427                    (j as isize) <= (i as isize + (max_past_horizon + max_future_horizon) as isize);
428                if lower && upper {
429                    mask_vec[i * context_size + j] = 1;
430                }
431            }
432        }
433        let local_causal_valid_mask =
434            Tensor::from_vec(mask_vec, (chunk_size, context_size), vb.device())?
435                .to_dtype(DType::U8)?;
436
437        let per_dim_scale_softplus = {
438            let ones = Tensor::ones_like(&per_dim_scale)?.to_dtype(DType::F32)?;
439            let exp_scale = per_dim_scale.to_dtype(DType::F32)?.exp()?;
440            ones.broadcast_add(&exp_scale)?.log()?
441        };
442
443        Ok(Self {
444            q_proj,
445            k_proj,
446            v_proj,
447            post,
448            relative_position_embedding,
449            per_dim_scale_softplus,
450            pre_attn_norm,
451            post_norm,
452            num_heads,
453            head_dim,
454            chunk_size,
455            max_past_horizon,
456            max_future_horizon,
457            context_size,
458            q_scale,
459            k_scale,
460            softcap: cfg.conf_attention_logit_cap,
461            invalid_logits_value: cfg.conf_attention_invalid_logits_value,
462            local_causal_valid_mask,
463            gradient_clipping: cfg.gradient_clipping,
464            hidden_size: cfg.hidden_size,
465        })
466    }
467
468    fn convert_to_block(&self, x: &Tensor) -> Result<Tensor> {
469        let dims = x.dims().to_vec();
470        let (b, t) = (dims[0], dims[1]);
471        let num_blocks = t.div_ceil(self.chunk_size);
472        let padding_len = num_blocks * self.chunk_size - t;
473        let x = if padding_len > 0 {
474            x.pad_with_zeros(1, 0, padding_len)?
475        } else {
476            x.clone()
477        };
478        let mut new_shape = vec![b, num_blocks, self.chunk_size];
479        new_shape.extend_from_slice(&dims[2..]);
480        x.reshape(new_shape)
481    }
482
483    fn extract_block_context(&self, x: &Tensor) -> Result<Tensor> {
484        let pad_left = self.max_past_horizon;
485        let pad_right = self.max_future_horizon + self.chunk_size - 1;
486        let x = x.pad_with_zeros(1, pad_left, pad_right)?;
487        let frame_len = self.context_size;
488        let frame_step = self.chunk_size;
489        let time_dim = x.dim(1)?;
490        let num_windows = (time_dim - frame_len) / frame_step + 1;
491
492        let mut windows = Vec::with_capacity(num_windows);
493        for i in 0..num_windows {
494            let start_idx = i * frame_step;
495            windows.push(x.narrow(1, start_idx, frame_len)?);
496        }
497        Tensor::stack(&windows, 1)
498    }
499
500    fn forward(&self, x: &Tensor, mask: &Tensor) -> Result<Tensor> {
501        let residual = x;
502        let x = x.clamp(-self.gradient_clipping, self.gradient_clipping)?;
503        let x = self.pre_attn_norm.forward(&x)?;
504
505        let q = self.q_proj.forward(&x)?.to_dtype(DType::F32)?;
506        let k = self.k_proj.forward(&x)?.to_dtype(DType::F32)?;
507        let v = self.v_proj.forward(&x)?.to_dtype(DType::F32)?;
508
509        let (b, t, _) = x.dims3()?;
510
511        let q = q.reshape((b, t, self.num_heads, self.head_dim))?;
512        let k = k.reshape((b, t, self.num_heads, self.head_dim))?;
513        let v = v.reshape((b, t, self.num_heads, self.head_dim))?;
514
515        let per_dim_scale = self
516            .per_dim_scale_softplus
517            .to_device(x.device())?
518            .to_dtype(DType::F32)?;
519
520        // Scale Q and K
521        let q = q
522            .affine(self.q_scale, 0.0)?
523            .broadcast_mul(&per_dim_scale.reshape((1, 1, 1, self.head_dim))?)?;
524        let k = k.affine(self.k_scale, 0.0)?;
525
526        // Convert to blocks
527        let query_blocks = self.convert_to_block(&q)?;
528        let key_blocks = self.extract_block_context(&k)?;
529        let value_blocks = self.extract_block_context(&v)?;
530        let num_query_blocks = query_blocks.dim(1)?;
531
532        // Ensure key/value blocks match context_size
533        let key_blocks = if key_blocks.dim(2)? != self.context_size {
534            if key_blocks.dim(2)? < self.context_size {
535                key_blocks.pad_with_zeros(2, 0, self.context_size - key_blocks.dim(2)?)?
536            } else {
537                key_blocks.narrow(2, 0, self.context_size)?
538            }
539        } else {
540            key_blocks
541        };
542        let value_blocks = if value_blocks.dim(2)? != self.context_size {
543            if value_blocks.dim(2)? < self.context_size {
544                value_blocks.pad_with_zeros(2, 0, self.context_size - value_blocks.dim(2)?)?
545            } else {
546                value_blocks.narrow(2, 0, self.context_size)?
547            }
548        } else {
549            value_blocks
550        };
551
552        // Align block counts
553        let key_blocks = if key_blocks.dim(1)? > num_query_blocks {
554            key_blocks.narrow(1, 0, num_query_blocks)?
555        } else {
556            key_blocks
557        };
558        let value_blocks = if value_blocks.dim(1)? > num_query_blocks {
559            value_blocks.narrow(1, 0, num_query_blocks)?
560        } else {
561            value_blocks
562        };
563
564        // Build validity mask from input mask + causality
565        let original_valid = mask.eq(0.0)?.to_dtype(DType::U8)?;
566        let extracted_valid = self.extract_block_context(&original_valid)?;
567        let extracted_valid = if extracted_valid.rank() == 4 {
568            extracted_valid.reshape((b, num_query_blocks, self.context_size))?
569        } else {
570            extracted_valid
571        };
572        let extracted_valid = if extracted_valid.dim(D::Minus1)? != self.context_size {
573            if extracted_valid.dim(D::Minus1)? < self.context_size {
574                extracted_valid.pad_with_zeros(
575                    D::Minus1,
576                    0,
577                    self.context_size - extracted_valid.dim(D::Minus1)?,
578                )?
579            } else {
580                extracted_valid.narrow(D::Minus1, 0, self.context_size)?
581            }
582        } else {
583            extracted_valid
584        };
585
586        let cond_input = extracted_valid.unsqueeze(1)?.unsqueeze(3)?;
587        let cond_causal = self
588            .local_causal_valid_mask
589            .to_device(x.device())?
590            .unsqueeze(0)?
591            .unsqueeze(0)?
592            .unsqueeze(0)?;
593        let final_cond = cond_input
594            .to_dtype(DType::U8)?
595            .broadcast_mul(&cond_causal.to_dtype(DType::U8)?)?;
596
597        // Relative position logits
598        let logits = self
599            .relative_position_embedding
600            .forward(&query_blocks, &key_blocks)?;
601        let logits = ((logits / self.softcap)?.tanh()? * self.softcap)?;
602
603        // Broadcast mask to logits shape
604        let final_cond = final_cond.broadcast_as(logits.shape())?;
605
606        let invalid_logits = Tensor::new(self.invalid_logits_value as f32, logits.device())?
607            .broadcast_as(logits.shape())?;
608        let masked_logits = final_cond.where_cond(&logits, &invalid_logits)?;
609        let probabilities = candle_nn::ops::softmax_last_dim(&masked_logits.to_dtype(DType::F32)?)?;
610
611        // Weighted sum of values
612        let (b_dim, n_dim, u_dim, w_dim, c_dim) = probabilities.dims5()?;
613        let h_dim = value_blocks.dim(D::Minus1)?;
614        let probs_p = probabilities.permute((0, 2, 1, 3, 4))?.reshape((
615            b_dim * u_dim * n_dim,
616            w_dim,
617            c_dim,
618        ))?;
619        let vals_p = value_blocks.permute((0, 1, 3, 2, 4))?.reshape((
620            b_dim * u_dim * n_dim,
621            c_dim,
622            h_dim,
623        ))?;
624        let context_vectors = probs_p
625            .matmul(&vals_p)?
626            .reshape((b_dim, u_dim, n_dim, w_dim, h_dim))?
627            .permute((0, 1, 3, 2, 4))?
628            .reshape((
629                b,
630                num_query_blocks * self.chunk_size,
631                self.num_heads,
632                self.head_dim,
633            ))?
634            .narrow(1, 0, t)?;
635
636        let context_vectors = context_vectors.reshape((b, t, self.hidden_size))?;
637        let out = self
638            .post
639            .forward(&context_vectors)?
640            .clamp(-self.gradient_clipping, self.gradient_clipping)?;
641        residual.broadcast_add(&self.post_norm.forward(&out)?)
642    }
643}
644
645// ── Conformer FeedForward ───────────────────────────────────────────────────
646
647#[derive(Debug, Clone)]
648struct ConformerFeedForward {
649    scale: f64,
650    pre_layer_norm: RmsNorm,
651    ffw_layer_1: candle_nn::Linear,
652    ffw_layer_2: candle_nn::Linear,
653    post_layer_norm: RmsNorm,
654    gradient_clipping: f64,
655}
656
657impl ConformerFeedForward {
658    fn new(cfg: &Gemma4AudioConfig, vb: VarBuilder) -> Result<Self> {
659        Ok(Self {
660            scale: cfg.conf_residual_weight,
661            pre_layer_norm: RmsNorm::new(
662                cfg.hidden_size,
663                cfg.rms_norm_eps,
664                vb.pp("pre_layer_norm"),
665            )?,
666            ffw_layer_1: candle_nn::linear_no_bias(
667                cfg.hidden_size,
668                cfg.hidden_size * 4,
669                vb.pp("ffw_layer_1"),
670            )?,
671            ffw_layer_2: candle_nn::linear_no_bias(
672                cfg.hidden_size * 4,
673                cfg.hidden_size,
674                vb.pp("ffw_layer_2"),
675            )?,
676            post_layer_norm: RmsNorm::new(
677                cfg.hidden_size,
678                cfg.rms_norm_eps,
679                vb.pp("post_layer_norm"),
680            )?,
681            gradient_clipping: cfg.gradient_clipping,
682        })
683    }
684
685    fn forward(&self, x: &Tensor) -> Result<Tensor> {
686        let residual = x;
687        let x = x.clamp(-self.gradient_clipping, self.gradient_clipping)?;
688        let x = self.pre_layer_norm.forward(&x)?;
689        let x = candle_nn::ops::silu(&self.ffw_layer_1.forward(&x)?)?;
690        let x = self
691            .ffw_layer_2
692            .forward(&x)?
693            .clamp(-self.gradient_clipping, self.gradient_clipping)?;
694        let x = self.post_layer_norm.forward(&x)?;
695        residual.broadcast_add(&(x * self.scale)?)
696    }
697}
698
699// ── Conformer LightConv1d ───────────────────────────────────────────────────
700
701#[derive(Debug, Clone)]
702struct ConformerLightConv1d {
703    pre_layer_norm: RmsNorm,
704    depthwise_conv1d: Conv1d,
705    conv_norm: RmsNorm,
706    linear_start: candle_nn::Linear,
707    linear_end: candle_nn::Linear,
708    causal_padding: usize,
709    gradient_clipping: f64,
710}
711
712impl ConformerLightConv1d {
713    fn new(cfg: &Gemma4AudioConfig, vb: VarBuilder) -> Result<Self> {
714        Ok(Self {
715            pre_layer_norm: RmsNorm::new(
716                cfg.hidden_size,
717                cfg.rms_norm_eps,
718                vb.pp("pre_layer_norm"),
719            )?,
720            linear_start: candle_nn::linear_no_bias(
721                cfg.hidden_size,
722                cfg.hidden_size * 2,
723                vb.pp("linear_start"),
724            )?,
725            depthwise_conv1d: candle_nn::conv1d_no_bias(
726                cfg.hidden_size,
727                cfg.hidden_size,
728                cfg.conf_conv_kernel_size,
729                candle_nn::Conv1dConfig {
730                    stride: 1,
731                    padding: 0,
732                    dilation: 1,
733                    groups: cfg.hidden_size,
734                    cudnn_fwd_algo: None,
735                },
736                vb.pp("depthwise_conv1d"),
737            )?,
738            conv_norm: RmsNorm::new(cfg.hidden_size, cfg.rms_norm_eps, vb.pp("conv_norm"))?,
739            linear_end: candle_nn::linear_no_bias(
740                cfg.hidden_size,
741                cfg.hidden_size,
742                vb.pp("linear_end"),
743            )?,
744            causal_padding: cfg.conf_conv_kernel_size - 1,
745            gradient_clipping: cfg.gradient_clipping,
746        })
747    }
748
749    fn forward(&self, audio_encodings: &Tensor) -> Result<Tensor> {
750        let residual = audio_encodings;
751        let x = self.pre_layer_norm.forward(audio_encodings)?;
752        let x = self.linear_start.forward(&x)?;
753        let half = x.dim(D::Minus1)? / 2;
754        let x1 = x.narrow(D::Minus1, 0, half)?;
755        let x2 = x.narrow(D::Minus1, half, half)?;
756        let x = (x1 * candle_nn::ops::sigmoid(&x2)?)?;
757        let x = x.transpose(D::Minus1, D::Minus2)?;
758        let x = x.pad_with_zeros(D::Minus1, self.causal_padding, 0)?;
759        let x = self
760            .depthwise_conv1d
761            .forward(&x.to_dtype(DType::F32)?)?
762            .to_dtype(audio_encodings.dtype())?
763            .transpose(D::Minus2, D::Minus1)?
764            .clamp(-self.gradient_clipping, self.gradient_clipping)?;
765        let x = self.conv_norm.forward(&x)?;
766        let x = candle_nn::ops::silu(&x)?;
767        let x = self.linear_end.forward(&x)?;
768        residual.broadcast_add(&x)
769    }
770}
771
772// ── ConformerBlock ──────────────────────────────────────────────────────────
773
774#[derive(Debug, Clone)]
775struct ConformerBlock {
776    ffw_layer_start: ConformerFeedForward,
777    attention: ConformerAttention,
778    lconv1d: ConformerLightConv1d,
779    ffw_layer_end: ConformerFeedForward,
780    norm: RmsNorm,
781    gradient_clipping: f64,
782}
783
784impl ConformerBlock {
785    fn new(cfg: &Gemma4AudioConfig, vb: VarBuilder) -> Result<Self> {
786        Ok(Self {
787            ffw_layer_start: ConformerFeedForward::new(cfg, vb.pp("feed_forward1"))?,
788            attention: ConformerAttention::new(cfg, vb.clone())?,
789            lconv1d: ConformerLightConv1d::new(cfg, vb.pp("lconv1d"))?,
790            ffw_layer_end: ConformerFeedForward::new(cfg, vb.pp("feed_forward2"))?,
791            norm: RmsNorm::new(cfg.hidden_size, cfg.rms_norm_eps, vb.pp("norm_out"))?,
792            gradient_clipping: cfg.gradient_clipping,
793        })
794    }
795
796    fn forward(&self, audio_encodings: &Tensor, audio_mel_mask: &Tensor) -> Result<Tensor> {
797        let x = self.ffw_layer_start.forward(audio_encodings)?;
798        let x = self.attention.forward(&x, audio_mel_mask)?;
799        let x = self.lconv1d.forward(&x)?;
800        let x = self
801            .ffw_layer_end
802            .forward(&x)?
803            .clamp(-self.gradient_clipping, self.gradient_clipping)?;
804        self.norm.forward(&x)
805    }
806}
807
808// ── AudioModel (public) ─────────────────────────────────────────────────────
809
810#[derive(Debug, Clone)]
811pub struct AudioModel {
812    subsample_conv_projection: SubSampleConvProjection,
813    conformer: Vec<ConformerBlock>,
814    conf_reduction_factor: usize,
815    output_proj: Option<candle_nn::Linear>,
816}
817
818impl AudioModel {
819    pub fn new(cfg: &Gemma4AudioConfig, vb: VarBuilder) -> Result<Self> {
820        let subsample_conv_projection =
821            SubSampleConvProjection::new(cfg, vb.pp("subsample_conv_projection"))?;
822        let mut conformer = Vec::with_capacity(cfg.conf_num_hidden_layers);
823        let vb_layers = vb.pp("layers");
824        for i in 0..cfg.conf_num_hidden_layers {
825            conformer.push(ConformerBlock::new(cfg, vb_layers.pp(i))?);
826        }
827        let output_proj = if let Some(output_dim) = cfg.output_proj_dims {
828            Some(candle_nn::linear(
829                cfg.hidden_size,
830                output_dim,
831                vb.pp("output_proj"),
832            )?)
833        } else {
834            None
835        };
836        Ok(Self {
837            subsample_conv_projection,
838            conformer,
839            conf_reduction_factor: cfg.conf_reduction_factor,
840            output_proj,
841        })
842    }
843
844    pub fn forward(&self, audio_mel: &Tensor, audio_mel_mask: &Tensor) -> Result<(Tensor, Tensor)> {
845        let (mut audio_encodings, mut current_mask) = self
846            .subsample_conv_projection
847            .forward(audio_mel, audio_mel_mask)?;
848
849        for block in &self.conformer {
850            audio_encodings = block.forward(&audio_encodings, &current_mask)?;
851        }
852
853        // Reduction factor subsampling
854        if self.conf_reduction_factor > 1 {
855            let stride = self.conf_reduction_factor;
856            let enc_len = audio_encodings.dim(1)?;
857            let reduced_len = enc_len.div_ceil(stride);
858            let indices: Vec<u32> = (0..reduced_len)
859                .map(|i| (i * stride).min(enc_len - 1) as u32)
860                .collect();
861            let indices = Tensor::from_vec(indices, reduced_len, audio_encodings.device())?;
862            audio_encodings = audio_encodings.index_select(&indices, 1)?;
863            current_mask = current_mask.index_select(&indices, 1)?;
864        }
865
866        if let Some(ref output_proj) = self.output_proj {
867            audio_encodings = output_proj.forward(&audio_encodings)?;
868        }
869
870        // Align mask length
871        let enc_len = audio_encodings.dim(1)?;
872        let mask_len = current_mask.dim(1)?;
873        if mask_len != enc_len {
874            if enc_len < mask_len {
875                current_mask = current_mask.narrow(1, 0, enc_len)?;
876            } else {
877                current_mask = current_mask.pad_with_zeros(1, 0, enc_len - mask_len)?;
878            }
879        }
880
881        // Zero out invalid positions
882        let valid_mask = current_mask.eq(0.0)?;
883        let zeros = Tensor::zeros_like(&audio_encodings)?;
884        let audio_encodings = valid_mask
885            .unsqueeze(D::Minus1)?
886            .broadcast_as(audio_encodings.shape())?
887            .where_cond(&audio_encodings, &zeros)?;
888
889        Ok((audio_encodings, current_mask))
890    }
891}