Skip to main content

candle_transformers/models/voxtral/
model.rs

1use super::voxtral_llama::{VoxtralLlama, VoxtralLlamaCache, VoxtralLlamaConfig};
2use candle::{DType, Device, IndexOp, Module, Result, Tensor, D};
3use candle_nn::{
4    layer_norm, linear, linear_no_bias, Conv1d, Dropout, LayerNorm, Linear, VarBuilder,
5};
6use rand::Rng;
7
8#[derive(Debug, Clone)]
9pub struct VoxtralEncoderConfig {
10    pub vocab_size: usize,
11    pub hidden_size: usize,
12    pub intermediate_size: usize,
13    pub num_hidden_layers: usize,
14    pub num_attention_heads: usize,
15    pub num_key_value_heads: usize,
16    pub head_dim: usize,
17    pub scale_embedding: bool,
18    pub activation_function: String,
19    pub num_mel_bins: usize,
20    pub max_source_positions: usize,
21    pub initializer_range: f64,
22    pub attention_dropout: f64,
23    // These are set to 0.0 for compatibility with Whisper modular architecture
24    pub dropout: f64,
25    pub layerdrop: f64,
26    pub activation_dropout: f64,
27}
28
29#[derive(Debug, Clone)]
30pub struct VoxtralConfig {
31    pub audio_config: VoxtralEncoderConfig,
32    pub text_config: VoxtralLlamaConfig,
33    pub audio_token_id: usize,
34    pub projector_hidden_act: String,
35}
36
37impl Default for VoxtralConfig {
38    fn default() -> Self {
39        Self {
40            audio_config: VoxtralEncoderConfig::default(),
41            text_config: VoxtralLlamaConfig::voxtral_3b(),
42            audio_token_id: 24,
43            projector_hidden_act: "gelu".to_string(),
44        }
45    }
46}
47
48impl Default for VoxtralEncoderConfig {
49    fn default() -> Self {
50        Self {
51            vocab_size: 51866,
52            hidden_size: 1280,
53            intermediate_size: 5120,
54            num_hidden_layers: 32,
55            num_attention_heads: 20,
56            num_key_value_heads: 20,
57            head_dim: 64,
58            scale_embedding: false,
59            activation_function: "gelu".to_string(),
60            num_mel_bins: 128,
61            max_source_positions: 1500,
62            initializer_range: 0.02,
63            attention_dropout: 0.0,
64            // Set for Whisper compatibility
65            dropout: 0.0,
66            layerdrop: 0.0,
67            activation_dropout: 0.0,
68        }
69    }
70}
71
72impl VoxtralEncoderConfig {
73    /// Ensures dropout values are properly set for Whisper compatibility
74    pub fn with_whisper_compatibility(mut self) -> Self {
75        self.dropout = 0.0;
76        self.layerdrop = 0.0;
77        self.activation_dropout = 0.0;
78        self
79    }
80}
81
82/// Custom cache for multimodal inputs
83#[derive(Debug, Clone)]
84pub struct VoxtralCache {
85    cache: VoxtralLlamaCache,
86    audio_processed: bool,
87    cached_audio_embeds: Option<Tensor>,
88    cached_audio_positions: Option<Vec<(usize, usize)>>,
89}
90
91#[derive(Debug, Clone)]
92pub struct VoxtralGenerationConfig {
93    pub max_new_tokens: usize,
94    pub temperature: f64,
95    pub top_p: Option<f64>,
96    pub device: Device,
97    /// If cache is None, the model will create a new cache.
98    pub cache: Option<VoxtralCache>,
99}
100
101impl VoxtralGenerationConfig {
102    pub fn new(device: Device) -> Self {
103        Self {
104            max_new_tokens: 500,
105            temperature: 0.0,
106            top_p: None,
107            device,
108            cache: None,
109        }
110    }
111}
112
113impl VoxtralCache {
114    pub fn new(
115        use_kv_cache: bool,
116        dtype: DType,
117        config: &VoxtralLlamaConfig,
118        device: &Device,
119    ) -> Result<Self> {
120        Ok(Self {
121            cache: VoxtralLlamaCache::new(use_kv_cache, dtype, config, device)?,
122            audio_processed: false,
123            cached_audio_embeds: None,
124            cached_audio_positions: None,
125        })
126    }
127
128    pub fn reset(&mut self) {
129        // Reset the audio cache state
130        self.audio_processed = false;
131        self.cached_audio_embeds = None;
132        self.cached_audio_positions = None;
133        // Note: LlamaCache reset needs to be handled at a higher level
134        // as it requires device access
135    }
136}
137
138/// Safely clamp tensor values for different dtypes
139fn safe_clamp(x: &Tensor) -> Result<Tensor> {
140    match x.dtype() {
141        DType::F16 => {
142            // Match PyTorch exactly: torch.finfo(torch.float16).max - 1000 = 64504.0
143            let max_val = 64504.0;
144            x.clamp(-max_val, max_val)
145        }
146        DType::BF16 => {
147            // BF16 has larger range, typically doesn't need clamping
148            Ok(x.clone())
149        }
150        _ => Ok(x.clone()),
151    }
152}
153
154/// Replace audio tokens in embeddings with projected audio features
155pub fn replace_audio_tokens(
156    inputs_embeds: &Tensor,
157    audio_embeds: &Tensor,
158    audio_positions: &[(usize, usize)],
159    device: &Device,
160) -> Result<Tensor> {
161    if audio_positions.is_empty() {
162        return Ok(inputs_embeds.clone());
163    }
164
165    let (batch_size, seq_len, hidden_size) = inputs_embeds.dims3()?;
166    let num_audio_tokens = audio_positions.len();
167
168    // HF-style: audio_embeds shape is (total_audio_seq_len, hidden_size)
169    let audio_embeds_dims = audio_embeds.dims2()?;
170    let total_audio_embeds = audio_embeds_dims.0;
171
172    // HF-style: Use audio embeddings one-to-one with audio tokens
173    // We should now have the right number of audio tokens in the input sequence
174    let audio_embeds = if total_audio_embeds >= num_audio_tokens {
175        // Take the first num_audio_tokens embeddings to match the audio tokens
176        if num_audio_tokens == total_audio_embeds {
177            audio_embeds.clone()
178        } else {
179            audio_embeds.i(0..num_audio_tokens)?
180        }
181    } else {
182        candle::bail!(
183            "Not enough audio embeddings: need {}, got {}. Input sequence should have {} audio tokens.",
184            num_audio_tokens,
185            total_audio_embeds,
186            total_audio_embeds
187        );
188    };
189
190    // Create result tensor starting with text embeddings
191    let mut result = inputs_embeds.clone();
192
193    // Replace audio tokens with audio embeddings
194    // Since we don't have scatter operations, we'll do this manually
195    for (idx, &(batch_idx, seq_idx)) in audio_positions.iter().enumerate() {
196        if batch_idx >= batch_size || seq_idx >= seq_len {
197            candle::bail!(
198                "Invalid audio position: ({}, {}) for tensor shape ({}, {}, {})",
199                batch_idx,
200                seq_idx,
201                batch_size,
202                seq_len,
203                hidden_size
204            );
205        }
206
207        // Get the audio embedding for this position
208        let audio_embed = audio_embeds.i(idx)?;
209
210        // Create a mask for this specific position
211        let mut position_mask = vec![0f32; batch_size * seq_len];
212        position_mask[batch_idx * seq_len + seq_idx] = 1.0;
213        let position_mask = Tensor::new(position_mask.as_slice(), device)?
214            .reshape((batch_size, seq_len, 1))?
215            .to_dtype(inputs_embeds.dtype())?;
216
217        // Broadcast audio embedding to full tensor shape
218        let audio_embed_broadcast = audio_embed.unsqueeze(0)?.unsqueeze(0)?.broadcast_as((
219            batch_size,
220            seq_len,
221            hidden_size,
222        ))?;
223
224        // Update result: keep original where mask is 0, use audio where mask is 1
225        let inverse_mask = (1.0 - &position_mask)?;
226        result = (result.broadcast_mul(&inverse_mask)?
227            + audio_embed_broadcast.broadcast_mul(&position_mask)?)?;
228    }
229
230    Ok(result)
231}
232
233/// Find positions of audio tokens in input sequences
234pub fn find_audio_token_positions(
235    input_ids: &Tensor,
236    audio_token_id: usize,
237) -> Result<Vec<(usize, usize)>> {
238    // Handle both i64 and u32 token types by converting to i64 first if needed
239    let input_ids = if input_ids.dtype() == candle::DType::U32 {
240        input_ids.to_dtype(candle::DType::I64)?
241    } else {
242        input_ids.clone()
243    };
244
245    let input_ids = input_ids.to_vec2::<i64>()?;
246    let mut positions = Vec::new();
247
248    for (batch_idx, sequence) in input_ids.iter().enumerate() {
249        for (seq_idx, &token_id) in sequence.iter().enumerate() {
250            if token_id as usize == audio_token_id {
251                positions.push((batch_idx, seq_idx));
252            }
253        }
254    }
255
256    Ok(positions)
257}
258
259#[derive(Debug, Clone)]
260struct VoxtralAttention {
261    q_proj: Linear,
262    k_proj: Linear,
263    v_proj: Linear,
264    out_proj: Linear,
265    num_heads: usize,
266    head_dim: usize,
267    scaling: f64,
268    attention_dropout: Dropout,
269}
270
271impl VoxtralAttention {
272    fn new(cfg: &VoxtralEncoderConfig, vb: VarBuilder) -> Result<Self> {
273        let embed_dim = cfg.hidden_size;
274        let num_heads = cfg.num_attention_heads;
275        let head_dim = embed_dim / num_heads;
276
277        if head_dim * num_heads != embed_dim {
278            candle::bail!(
279                "embed_dim must be divisible by num_heads ({} % {} != 0)",
280                embed_dim,
281                num_heads
282            );
283        }
284
285        let scaling = (head_dim as f64).powf(-0.5);
286
287        let q_proj = linear(embed_dim, embed_dim, vb.pp("q_proj"))?;
288        let k_proj = linear_no_bias(embed_dim, embed_dim, vb.pp("k_proj"))?;
289        let v_proj = linear(embed_dim, embed_dim, vb.pp("v_proj"))?;
290        let out_proj = linear(embed_dim, embed_dim, vb.pp("out_proj"))?;
291
292        let attention_dropout = Dropout::new(cfg.attention_dropout as f32);
293
294        Ok(Self {
295            q_proj,
296            k_proj,
297            v_proj,
298            out_proj,
299            num_heads,
300            head_dim,
301            scaling,
302            attention_dropout,
303        })
304    }
305
306    fn reshape_for_scores(&self, x: &Tensor, seq_len: usize, bsz: usize) -> Result<Tensor> {
307        x.reshape((bsz, seq_len, self.num_heads, self.head_dim))?
308            .transpose(1, 2)?
309            .contiguous()
310    }
311}
312
313impl Module for VoxtralAttention {
314    fn forward(&self, x: &Tensor) -> Result<Tensor> {
315        let (bsz, seq_len, _) = x.dims3()?;
316
317        // Project queries, keys, and values - apply scaling to queries to match PyTorch SDPA
318        let q = (self.q_proj.forward(x)? * self.scaling)?;
319        let k = self.k_proj.forward(x)?;
320        let v = self.v_proj.forward(x)?;
321
322        // Reshape for multi-head attention: (batch, seq_len, num_heads, head_dim) -> (batch, num_heads, seq_len, head_dim)
323        let q = self.reshape_for_scores(&q, seq_len, bsz)?;
324        let k = self.reshape_for_scores(&k, seq_len, bsz)?;
325        let v = self.reshape_for_scores(&v, seq_len, bsz)?;
326
327        // Manual SDPA-like implementation to match Python's numerical behavior exactly
328        // Use F16 precision throughout to match PyTorch's F16 model
329        let scores = q.matmul(&k.transpose(D::Minus2, D::Minus1)?)?;
330
331        // Apply softmax in same precision as input (F16) to match Python
332        let attn_weights = candle_nn::ops::softmax_last_dim(&scores)?;
333
334        // Apply attention dropout (disabled during inference)
335        let attn_weights = self.attention_dropout.forward(&attn_weights, false)?;
336
337        // Apply attention to values
338        let attn_output = attn_weights.matmul(&v)?;
339
340        // Reshape back to (batch, seq_len, embed_dim)
341        let attn_output = attn_output.transpose(1, 2)?.contiguous()?.reshape((
342            bsz,
343            seq_len,
344            self.num_heads * self.head_dim,
345        ))?;
346
347        self.out_proj.forward(&attn_output)
348    }
349}
350
351#[derive(Debug, Clone)]
352struct VoxtralEncoderLayer {
353    self_attn: VoxtralAttention,
354    self_attn_layer_norm: LayerNorm,
355    fc1: Linear,
356    fc2: Linear,
357    final_layer_norm: LayerNorm,
358    activation: candle_nn::Activation,
359    dropout: Dropout,
360    activation_dropout: Dropout,
361}
362
363impl VoxtralEncoderLayer {
364    fn new(cfg: &VoxtralEncoderConfig, vb: VarBuilder) -> Result<Self> {
365        let embed_dim = cfg.hidden_size;
366
367        let self_attn = VoxtralAttention::new(cfg, vb.pp("self_attn"))?;
368        let self_attn_layer_norm = layer_norm(embed_dim, 1e-5, vb.pp("self_attn_layer_norm"))?;
369        let fc1 = linear(embed_dim, cfg.intermediate_size, vb.pp("fc1"))?;
370        let fc2 = linear(cfg.intermediate_size, embed_dim, vb.pp("fc2"))?;
371        let final_layer_norm = layer_norm(embed_dim, 1e-5, vb.pp("final_layer_norm"))?;
372
373        let activation = match cfg.activation_function.as_str() {
374            "gelu" => candle_nn::Activation::Gelu,
375            "relu" => candle_nn::Activation::Relu,
376            _ => candle::bail!(
377                "Unsupported activation function: {}",
378                cfg.activation_function
379            ),
380        };
381
382        let dropout = Dropout::new(cfg.dropout as f32);
383        let activation_dropout = Dropout::new(cfg.activation_dropout as f32);
384
385        Ok(Self {
386            self_attn,
387            self_attn_layer_norm,
388            fc1,
389            fc2,
390            final_layer_norm,
391            activation,
392            dropout,
393            activation_dropout,
394        })
395    }
396
397    pub fn get_fc1_out_dim(&self) -> usize {
398        // Return the intermediate size from the config
399        // Since Linear doesn't expose out_dim
400        self.fc1.weight().dims()[0]
401    }
402
403    fn forward(&self, x: &Tensor, training: bool) -> Result<Tensor> {
404        // Self-attention with residual connection
405        let residual = x;
406        let x = self.self_attn_layer_norm.forward(x)?;
407        let x = self.self_attn.forward(&x)?;
408        let x = self.dropout.forward(&x, training)?;
409        let x = (x + residual)?;
410
411        // Feed-forward network with residual connection
412        let residual = &x;
413        let x = self.final_layer_norm.forward(&x)?;
414        let x = self.fc1.forward(&x)?;
415        let x = x.apply(&self.activation)?;
416        let x = self.activation_dropout.forward(&x, training)?;
417        let x = self.fc2.forward(&x)?;
418        let x = self.dropout.forward(&x, training)?;
419        let x = (x + residual)?;
420
421        // Safe clamping for numerical stability
422        safe_clamp(&x)
423    }
424}
425
426#[derive(Debug, Clone)]
427pub struct VoxtralEncoder {
428    conv1: Conv1d,
429    conv2: Conv1d,
430    embed_positions: Tensor,
431    layers: Vec<VoxtralEncoderLayer>,
432    layer_norm: LayerNorm,
433    dropout: Dropout,
434    layerdrop: f64,
435}
436
437impl VoxtralEncoder {
438    pub fn new(cfg: &VoxtralEncoderConfig, vb: VarBuilder) -> Result<Self> {
439        // Ensure Whisper compatibility
440        let cfg = cfg.clone().with_whisper_compatibility();
441
442        let embed_dim = cfg.hidden_size;
443
444        // Convolutional layers for processing mel features
445        let conv1 = candle_nn::conv1d(
446            cfg.num_mel_bins,
447            embed_dim,
448            3,
449            candle_nn::Conv1dConfig {
450                padding: 1,
451                ..Default::default()
452            },
453            vb.pp("conv1"),
454        )?;
455
456        let conv2 = candle_nn::conv1d(
457            embed_dim,
458            embed_dim,
459            3,
460            candle_nn::Conv1dConfig {
461                stride: 2,
462                padding: 1,
463                ..Default::default()
464            },
465            vb.pp("conv2"),
466        )?;
467
468        // Position embeddings
469        let embed_positions = vb.get(
470            (cfg.max_source_positions, embed_dim),
471            "embed_positions.weight",
472        )?;
473
474        // Transformer layers
475        let mut layers = Vec::with_capacity(cfg.num_hidden_layers);
476        for i in 0..cfg.num_hidden_layers {
477            layers.push(VoxtralEncoderLayer::new(
478                &cfg,
479                vb.pp(format!("layers.{i}")),
480            )?);
481        }
482
483        let layer_norm = layer_norm(embed_dim, 1e-5, vb.pp("layer_norm"))?;
484        let dropout = Dropout::new(cfg.dropout as f32);
485
486        Ok(Self {
487            conv1,
488            conv2,
489            embed_positions,
490            layers,
491            layer_norm,
492            dropout,
493            layerdrop: cfg.layerdrop,
494        })
495    }
496
497    pub fn forward(&self, input_features: &Tensor) -> Result<Tensor> {
498        self.forward_with_training(input_features, false)
499    }
500
501    pub fn forward_with_training(&self, input_features: &Tensor, training: bool) -> Result<Tensor> {
502        // Keep conv layers in F16 to avoid shape issues
503        let expected_dtype = self.conv1.weight().dtype();
504        let input_features = if input_features.dtype() != expected_dtype {
505            input_features.to_dtype(expected_dtype)?
506        } else {
507            input_features.clone()
508        };
509
510        // Apply convolutional layers with GELU activation
511        let x = if false {
512            // Keep conv layers in F16
513            // Convert conv1 weights to F32 for computation
514            let conv1_weight_f32 = self.conv1.weight().to_dtype(DType::F32)?;
515            let conv1_bias_f32 = if let Some(bias) = self.conv1.bias() {
516                Some(bias.to_dtype(DType::F32)?)
517            } else {
518                None
519            };
520
521            // Manual conv1d operation with F32 precision - conv1 has stride=1, padding=1
522            let mut conv_result = input_features.conv1d(&conv1_weight_f32, 1, 1, 1, 1)?;
523            if let Some(bias) = conv1_bias_f32 {
524                conv_result = conv_result.broadcast_add(&bias.unsqueeze(0)?.unsqueeze(2)?)?;
525            }
526            conv_result
527        } else {
528            self.conv1.forward(&input_features)?
529        };
530
531        // Apply GELU activation after conv1 (matches Python: conv1 -> GELU)
532        let x = x.gelu()?;
533
534        // Apply conv2 (matches Python: conv2)
535        let x = if false {
536            // Keep conv layers in F16
537            // Convert conv2 weights to F32 for computation
538            let conv2_weight_f32 = self.conv2.weight().to_dtype(DType::F32)?;
539            let conv2_bias_f32 = if let Some(bias) = self.conv2.bias() {
540                Some(bias.to_dtype(DType::F32)?)
541            } else {
542                None
543            };
544
545            // Manual conv1d operation with F32 precision - conv2 has stride=2, padding=1
546            let mut conv_result = x.conv1d(&conv2_weight_f32, 2, 1, 1, 1)?;
547            if let Some(bias) = conv2_bias_f32 {
548                conv_result = conv_result.broadcast_add(&bias.unsqueeze(0)?.unsqueeze(2)?)?;
549            }
550            conv_result
551        } else {
552            self.conv2.forward(&x)?
553        };
554
555        // Apply GELU activation after conv2 (FIX: matches Python: conv2 -> GELU)
556        let x = x.gelu()?;
557
558        // Reshape: (batch, embed_dim, seq_len) -> (batch, seq_len, embed_dim)
559        let x = x.transpose(1, 2)?;
560
561        // Add position embeddings - handle F32 position embeddings + F16 hidden states like PyTorch
562        let seq_len = x.dim(1)?;
563        let positions = self.embed_positions.i(..seq_len)?;
564
565        // PyTorch automatically promotes F16 + F32 -> F32, then converts back to original dtype
566        // We need to match this behavior exactly
567        let x = if false {
568            // Keep position embeddings in mixed precision
569            // Force F32 computation for position embeddings
570            let x_f32 = x.to_dtype(candle::DType::F32)?;
571            let positions_f32 = positions.to_dtype(candle::DType::F32)?;
572            x_f32.broadcast_add(&positions_f32)? // Keep result in F32
573        } else if x.dtype() != positions.dtype() {
574            // Convert hidden states to F32 for addition (positions are already F32)
575            let x_f32 = x.to_dtype(candle::DType::F32)?;
576            let result_f32 = x_f32.broadcast_add(&positions)?;
577            // Convert back to original hidden states dtype (F16)
578            result_f32.to_dtype(x.dtype())?
579        } else {
580            x.broadcast_add(&positions)?
581        };
582
583        // Apply dropout
584        let mut x = self.dropout.forward(&x, training)?;
585
586        for (idx, layer) in self.layers.iter().enumerate() {
587            // Keep all computation in F16
588            x = self.forward_layer_with_dropout(&x, layer, idx, training)?;
589        }
590
591        // Apply final layer normalization (critical for proper output values!)
592        let x = self.layer_norm.forward(&x)?;
593
594        Ok(x)
595    }
596
597    /// Forward a single layer with stochastic depth (layer dropout)
598    fn forward_layer_with_dropout(
599        &self,
600        x: &Tensor,
601        layer: &VoxtralEncoderLayer,
602        _layer_idx: usize,
603        training: bool,
604    ) -> Result<Tensor> {
605        if training && self.layerdrop > 0.0 {
606            // Apply stochastic depth with proper randomization
607            let mut rng = rand::rng();
608            let keep_prob = 1.0 - self.layerdrop;
609            let keep: bool = rng.random::<f64>() < keep_prob;
610
611            if !keep {
612                // Skip layer entirely (identity mapping)
613                return Ok(x.clone());
614            }
615        }
616
617        layer.forward(x, training)
618    }
619
620    /// Get the output dimension of the first FC layer (needed for projector)
621    pub fn get_intermediate_size(&self) -> usize {
622        if !self.layers.is_empty() {
623            self.layers[0].get_fc1_out_dim()
624        } else {
625            // Fallback to config value
626            5120 // Default intermediate size
627        }
628    }
629
630    /// Process long audio sequences in chunks to save memory
631    pub fn process_long_audio(
632        &self,
633        input_features: &Tensor,
634        chunk_size: usize,
635        overlap: usize,
636    ) -> Result<Tensor> {
637        let (_batch_size, _num_mel, seq_len) = input_features.dims3()?;
638
639        if seq_len <= chunk_size {
640            return self.forward(input_features);
641        }
642
643        let mut outputs = Vec::new();
644        let step = chunk_size - overlap;
645
646        for start in (0..seq_len).step_by(step) {
647            let end = (start + chunk_size).min(seq_len);
648            let chunk = input_features.i((.., .., start..end))?;
649
650            // Process chunk
651            let output = self.forward(&chunk)?;
652
653            // Handle overlap by averaging
654            if !outputs.is_empty() && overlap > 0 {
655                let overlap_frames = overlap / 2; // Account for conv2 stride
656                let last_output: &mut Tensor = outputs.last_mut().unwrap();
657                let last_len = last_output.dim(1)?;
658
659                // Average overlapping regions
660                let overlap_start = last_len.saturating_sub(overlap_frames);
661                let overlap_new = output.i((.., ..overlap_frames, ..))?;
662                let overlap_old = last_output.i((.., overlap_start.., ..))?;
663                let averaged = ((overlap_old + overlap_new)? * 0.5)?;
664
665                // Update last output
666                *last_output =
667                    Tensor::cat(&[&last_output.i((.., ..overlap_start, ..))?, &averaged], 1)?;
668
669                // Add non-overlapping part of current chunk
670                outputs.push(output.i((.., overlap_frames.., ..))?);
671            } else {
672                outputs.push(output);
673            }
674        }
675
676        // Concatenate all outputs
677        let outputs_ref: Vec<&Tensor> = outputs.iter().collect();
678        Tensor::cat(&outputs_ref, 1)
679    }
680}
681
682#[derive(Debug, Clone)]
683pub struct VoxtralMultiModalProjector {
684    linear_1: Linear,
685    linear_2: Linear,
686    activation: candle_nn::Activation,
687}
688
689impl VoxtralMultiModalProjector {
690    pub fn new(cfg: &VoxtralConfig, vb: VarBuilder) -> Result<Self> {
691        let linear_1 = linear_no_bias(
692            cfg.audio_config.intermediate_size,
693            cfg.text_config.hidden_size,
694            vb.pp("linear_1"),
695        )?;
696
697        let linear_2 = linear_no_bias(
698            cfg.text_config.hidden_size,
699            cfg.text_config.hidden_size,
700            vb.pp("linear_2"),
701        )?;
702
703        let activation = match cfg.projector_hidden_act.as_str() {
704            "gelu" => candle_nn::Activation::Gelu,
705            "relu" => candle_nn::Activation::Relu,
706            _ => candle::bail!(
707                "Unsupported projector activation: {}",
708                cfg.projector_hidden_act
709            ),
710        };
711
712        Ok(Self {
713            linear_1,
714            linear_2,
715            activation,
716        })
717    }
718
719    pub fn forward(&self, audio_features: &Tensor) -> Result<Tensor> {
720        let x = self.linear_1.forward(audio_features)?;
721        let x = x.apply(&self.activation)?;
722        self.linear_2.forward(&x)
723    }
724}
725
726#[derive(Debug, Clone)]
727pub struct VoxtralForConditionalGeneration {
728    audio_tower: VoxtralEncoder,
729    language_model: VoxtralLlama,
730    multi_modal_projector: VoxtralMultiModalProjector,
731    audio_token_id: usize,
732    audio_config: VoxtralEncoderConfig,
733    text_config: VoxtralLlamaConfig,
734}
735
736impl VoxtralForConditionalGeneration {
737    pub fn new(cfg: &VoxtralConfig, vb: VarBuilder) -> Result<Self> {
738        let audio_tower = VoxtralEncoder::new(&cfg.audio_config, vb.pp("audio_tower"))?;
739        let language_model = VoxtralLlama::load(vb.pp("language_model"), &cfg.text_config)?;
740        let multi_modal_projector =
741            VoxtralMultiModalProjector::new(cfg, vb.pp("multi_modal_projector"))?;
742
743        Ok(Self {
744            audio_tower,
745            language_model,
746            multi_modal_projector,
747            audio_token_id: cfg.audio_token_id,
748            audio_config: cfg.audio_config.clone(),
749            text_config: cfg.text_config.clone(),
750        })
751    }
752
753    /// Get the audio token ID used for this model
754    pub fn audio_token_id(&self) -> usize {
755        self.audio_token_id
756    }
757
758    /// Get the text model configuration
759    pub fn text_config(&self) -> &VoxtralLlamaConfig {
760        &self.text_config
761    }
762
763    /// Get the audio encoder configuration
764    pub fn audio_config(&self) -> &VoxtralEncoderConfig {
765        &self.audio_config
766    }
767
768    /// Process audio features through encoder and projector
769    pub fn get_audio_embeds(&self, input_features: &Tensor) -> Result<Tensor> {
770        let audio_outputs = self.audio_tower.forward(input_features)?;
771
772        // Following HF implementation: reshape to (-1, config.intermediate_size) before projection
773        // Python: audio_hidden_states.reshape(-1, self.config.audio_config.intermediate_size)
774        // This transforms [1, 1500, 1280] -> [375, 5120] using intermediate_size from config
775        let (batch_size, seq_len, hidden_size) = audio_outputs.dims3()?;
776
777        // The key insight: Python reshapes from [1, 1500, 1280] to [375, 5120]
778        // This means 1500 * 1280 = 375 * 5120 (1920000 elements)
779        // So we need: new_batch_size = (batch_size * seq_len * hidden_size) / intermediate_size
780        let total_elements = batch_size * seq_len * hidden_size;
781        let new_batch_size = total_elements / self.audio_config.intermediate_size;
782
783        // Verify the division is exact
784        if total_elements % self.audio_config.intermediate_size != 0 {
785            return Err(candle::Error::DimOutOfRange {
786                shape: candle::Shape::from_dims(&[batch_size, seq_len, hidden_size]),
787                dim: 0,
788                op: "reshape",
789            });
790        }
791
792        let audio_hidden =
793            audio_outputs.reshape((new_batch_size, self.audio_config.intermediate_size))?;
794
795        // Project to text space - this gives us embeddings for each audio position
796        let projected = self.multi_modal_projector.forward(&audio_hidden)?;
797
798        // Return shape: (batch_size * seq_len, text_hidden_size)
799        // This matches HF implementation - no pooling, keep all audio token embeddings
800        Ok(projected)
801    }
802
803    /// Process long audio sequences efficiently
804    pub fn get_audio_embeds_chunked(
805        &self,
806        input_features: &Tensor,
807        chunk_size: usize,
808        overlap: usize,
809    ) -> Result<Tensor> {
810        let audio_outputs =
811            self.audio_tower
812                .process_long_audio(input_features, chunk_size, overlap)?;
813
814        // Reshape and project (now outputs hidden_size, needs reshape to intermediate_size)
815        let (batch_size, seq_len, hidden_size) = audio_outputs.dims3()?;
816        // Apply same reshape logic as get_audio_embeds
817        let total_elements = batch_size * seq_len * hidden_size;
818        let new_batch_size = total_elements / self.audio_config.intermediate_size;
819        let audio_hidden =
820            audio_outputs.reshape((new_batch_size, self.audio_config.intermediate_size))?;
821
822        let projected = self.multi_modal_projector.forward(&audio_hidden)?;
823
824        // Reshape back to (batch_size, seq_len, text_hidden_size) for pooling
825        let text_hidden_size = self.text_config.hidden_size;
826        let projected = projected.reshape((batch_size, seq_len, text_hidden_size))?;
827
828        // Apply mean pooling to reduce to single audio embedding per batch
829        let pooled = projected.mean(1)?; // Mean across sequence dimension
830
831        // Return shape: (batch_size, text_hidden_size)
832        Ok(pooled)
833    }
834
835    /// Forward pass with audio features and text input
836    pub fn forward(
837        &self,
838        input_ids: &Tensor,
839        input_features: Option<&Tensor>,
840        cache: &mut VoxtralCache,
841        index_pos: usize,
842    ) -> Result<Tensor> {
843        // Get text embeddings
844        let mut inputs_embeds = self.language_model.embed(input_ids)?;
845
846        // If audio features are provided and not yet processed
847        if let Some(features) = input_features {
848            if !cache.audio_processed {
849                let audio_embeds = self.get_audio_embeds(features)?;
850
851                let audio_positions = find_audio_token_positions(input_ids, self.audio_token_id)?;
852
853                // Cache for future use
854                cache.cached_audio_embeds = Some(audio_embeds.clone());
855                cache.cached_audio_positions = Some(audio_positions.clone());
856                cache.audio_processed = true;
857
858                inputs_embeds = replace_audio_tokens(
859                    &inputs_embeds,
860                    &audio_embeds,
861                    &audio_positions,
862                    input_ids.device(),
863                )?;
864            }
865        }
866
867        // Forward through language model using forward_input_embed
868        self.language_model
869            .forward_input_embed(&inputs_embeds, index_pos, &mut cache.cache)
870    }
871
872    /// Generate text given audio input
873    pub fn generate(
874        &self,
875        input_ids: &Tensor,
876        input_features: Option<&Tensor>,
877        config: VoxtralGenerationConfig,
878    ) -> Result<Vec<u32>> {
879        // Validate inputs
880        if config.max_new_tokens == 0 {
881            return input_ids.i(0)?.to_vec1::<u32>(); // Get first batch
882        }
883
884        if config.temperature < 0.0 {
885            candle::bail!(
886                "Temperature must be non-negative, got {}",
887                config.temperature
888            );
889        }
890
891        if let Some(p) = config.top_p {
892            if !(0.0..=1.0).contains(&p) {
893                candle::bail!("top_p must be between 0 and 1, got {}", p);
894            }
895        }
896
897        let mut final_cache = if let Some(cache) = config.cache {
898            cache
899        } else {
900            // Get the dtype from the language model by creating a small embedding
901            let dummy_token = Tensor::new(&[1u32], &config.device)?;
902            let dummy_embed = self.language_model.embed(&dummy_token)?;
903            let model_dtype = dummy_embed.dtype();
904            VoxtralCache::new(true, model_dtype, &self.text_config, &config.device)?
905        };
906        let mut tokens = input_ids.i(0)?.to_vec1::<u32>()?; // Get first batch
907        let initial_len = tokens.len();
908
909        for idx in 0..config.max_new_tokens {
910            let (input, index_pos) = if idx == 0 {
911                (input_ids.clone(), 0)
912            } else {
913                // For subsequent generation steps, use only the last token
914                let last_token = tokens[tokens.len() - 1];
915                let calculated_pos = initial_len + idx - 1;
916                (
917                    Tensor::new(&[last_token], &config.device)?.unsqueeze(0)?,
918                    calculated_pos,
919                )
920            };
921
922            let logits = if idx == 0 {
923                // First pass - include audio features
924                match self.forward(&input, input_features, &mut final_cache, index_pos) {
925                    Ok(logits) => logits,
926                    Err(e) => {
927                        return Err(candle::Error::Msg(format!(
928                            "Failed to generate tokens: {e}"
929                        )));
930                    }
931                }
932            } else {
933                // Subsequent passes - text only
934                match self.forward(&input, None, &mut final_cache, index_pos) {
935                    Ok(logits) => logits,
936                    Err(e) => {
937                        return Err(candle::Error::Msg(format!(
938                            "Failed to generate tokens: {e}"
939                        )));
940                    }
941                }
942            };
943
944            // Handle both 2D [batch, vocab] and 3D [batch, seq_len, vocab] logits
945            let logits = if logits.dims().len() == 3 {
946                // 3D case: [batch, seq_len, vocab] -> get last token
947                logits.i((.., logits.dim(1)? - 1, ..))?
948            } else {
949                // 2D case: [batch, vocab] -> already the right shape
950                logits
951            };
952
953            let next_token = if config.temperature > 0.0 {
954                // Sample with temperature
955                let prs = (logits / config.temperature)?;
956                let prs = candle_nn::ops::softmax_last_dim(&prs)?;
957
958                if let Some(top_p_val) = config.top_p {
959                    // Apply top-p sampling
960                    sample_top_p(&prs.squeeze(0)?, top_p_val, &config.device)?
961                } else {
962                    // Sample from full distribution
963                    let probs_vec = prs.squeeze(0)?.to_vec1::<f32>()?;
964                    let mut rng = rand::rng();
965                    let mut cumsum = 0.0;
966                    let rand_val: f32 = rng.random();
967                    let mut sampled = 0u32;
968
969                    for (idx, &prob) in probs_vec.iter().enumerate() {
970                        cumsum += prob;
971                        if cumsum > rand_val {
972                            sampled = idx as u32;
973                            break;
974                        }
975                    }
976                    sampled
977                }
978            } else {
979                // Greedy decoding - find the token with highest probability
980                let argmax_result = match logits.argmax(D::Minus1) {
981                    Ok(result) => result,
982                    Err(e) => {
983                        return Err(candle::Error::Msg(format!("Argmax failed: {e}")));
984                    }
985                };
986
987                // Handle the case where argmax returns [1] instead of scalar
988
989                if argmax_result.dims().is_empty() {
990                    // Already a scalar
991                    match argmax_result.to_scalar::<u32>() {
992                        Ok(token) => token,
993                        Err(e) => {
994                            return Err(candle::Error::Msg(format!("to_scalar failed: {e}")));
995                        }
996                    }
997                } else if argmax_result.dims() == [1] {
998                    // Shape [1] - extract the single element
999                    match argmax_result.i(0) {
1000                        Ok(scalar_tensor) => match scalar_tensor.to_scalar::<u32>() {
1001                            Ok(token) => token,
1002                            Err(e) => {
1003                                return Err(candle::Error::Msg(format!(
1004                                    "to_scalar on extracted element failed: {e}"
1005                                )));
1006                            }
1007                        },
1008                        Err(e) => {
1009                            return Err(candle::Error::Msg(format!(
1010                                "indexing argmax result failed: {e}"
1011                            )));
1012                        }
1013                    }
1014                } else {
1015                    return Err(candle::Error::Msg(format!(
1016                        "Unexpected argmax result shape: {:?}",
1017                        argmax_result.shape()
1018                    )));
1019                }
1020            };
1021
1022            tokens.push(next_token);
1023
1024            // Check for EOS tokens - Voxtral uses different EOS tokens than hardcoded 2
1025            // Based on the Mistral/Voxtral tokenizer, common EOS tokens are:
1026            // 2 = </s>, 0 = <pad>, 128001, 128009 from various chat formats
1027            let eos_tokens = [2u32, 128001, 128009, 128256]; // Don't include 0 as it might be valid generation
1028
1029            // Check for EOS tokens only if not ignoring them
1030            if eos_tokens.contains(&next_token) {
1031                break;
1032            }
1033
1034            // Also break if we get repeated pad tokens (might indicate the model is stuck)
1035            if next_token == 0 && tokens.len() > 5 {
1036                let last_5_tokens = &tokens[tokens.len() - 5..];
1037                if last_5_tokens.iter().all(|&t| t == 0) {
1038                    break;
1039                }
1040            }
1041        }
1042
1043        Ok(tokens)
1044    }
1045}
1046
1047/// Sample from top-p probability distribution
1048fn sample_top_p(probs: &Tensor, top_p: f64, _device: &Device) -> Result<u32> {
1049    let (sorted_probs, sorted_indices) = probs.sort_last_dim(false)?;
1050    let cumsum = sorted_probs.cumsum(D::Minus1)?;
1051    let mask = cumsum.le(top_p)?;
1052
1053    // Apply mask and renormalize
1054    let filtered_probs = sorted_probs.where_cond(&mask, &Tensor::zeros_like(&sorted_probs)?)?;
1055    let filtered_probs = (&filtered_probs / filtered_probs.sum_keepdim(D::Minus1)?)?;
1056
1057    // Sample from filtered distribution
1058    // Since multinomial is not available, we'll use a simple sampling approach
1059    let probs_vec = filtered_probs.to_vec1::<f32>()?;
1060    let mut cumsum = 0.0;
1061    let mut rng = rand::rng();
1062    let rand_val: f32 = rng.random();
1063    let mut sample_idx = 0;
1064
1065    for (idx, &prob) in probs_vec.iter().enumerate() {
1066        cumsum += prob;
1067        if cumsum > rand_val {
1068            sample_idx = idx;
1069            break;
1070        }
1071    }
1072
1073    sorted_indices.i(sample_idx)?.to_scalar::<u32>()
1074}