Skip to main content

runtime/models_v2/
llama.rs

1//! Llama Model V2 - Clean implementation using solid abstractions
2//!
3//! This is a complete rewrite of the Llama model using our new architecture:
4//! - Uses unified Tensor type from tensor_core
5//! - Implements Model trait from model_core
6//! - Supports loading via weight_loader_core
7//! - Clean, maintainable code with proper abstractions
8
9use crate::model_config;
10use crate::kv_cache::{KVCache, LayerKVCache};
11use super::traits::*;
12
13use anyhow::Result;
14use serde::{Serialize, Deserialize};
15use candle_core::quantized::QMatMul;
16use candle_nn::Module;
17use std::sync::Arc;
18
19/// Llama model configuration using the model_config macro
20model_config!(LlamaConfig {
21    vocab_size: usize = 32000,
22    hidden_size: usize = 4096,
23    intermediate_size: usize = 11008,
24    num_hidden_layers: usize = 32,
25    num_attention_heads: usize = 32,
26    num_key_value_heads: usize = 32, // Changed from Option<usize>
27    hidden_act: String = "silu".to_string(),
28    max_position_embeddings: usize = 2048,
29    initializer_range: f32 = 0.02,
30    rms_norm_eps: f32 = 1e-6,
31    use_cache: bool = true,
32    pad_token_id: i64 = 0, // Changed from Option<i64>
33    bos_token_id: i64 = 1, // Changed from Option<i64>
34    eos_token_id: i64 = 2, // Changed from Option<i64>
35    tie_word_embeddings: bool = false,
36    rope_theta: f32 = 10000.0,
37    attention_bias: bool = false,
38});
39
40impl LlamaConfig {
41    /// Create LlamaConfig from GGUF model configuration
42    pub fn from_gguf_config(gguf: &crate::weight_loader_core::GGUFModelConfig) -> Self {
43        Self {
44            vocab_size: gguf.vocab_size,
45            hidden_size: gguf.hidden_size,
46            intermediate_size: gguf.intermediate_size,
47            num_hidden_layers: gguf.num_hidden_layers,
48            num_attention_heads: gguf.num_attention_heads,
49            num_key_value_heads: gguf.num_key_value_heads,
50            rms_norm_eps: gguf.rms_norm_eps,
51            rope_theta: gguf.rope_theta,
52            max_position_embeddings: gguf.max_position_embeddings,
53            ..Default::default()
54        }
55    }
56}
57
58/// Main Llama model implementation
59pub struct LlamaModelV2 {
60    config: LlamaConfig,
61    device: Device,
62
63    // Model components using unified Tensor type
64    embed_tokens: Tensor,
65    layers: Vec<LlamaLayer>,
66    norm: Tensor,
67    lm_head: Tensor,
68    // Quantized lm_head (optional)
69    lm_head_q: Option<Arc<QMatMul>>,
70}
71
72/// Llama transformer layer
73pub struct LlamaLayer {
74    self_attn: LlamaAttention,
75    mlp: LlamaMLP,
76    input_layernorm: Tensor,
77    post_attention_layernorm: Tensor,
78}
79
80/// Llama attention mechanism
81pub struct LlamaAttention {
82    // F32 weights (fallback)
83    q_proj: Tensor,
84    k_proj: Tensor,
85    v_proj: Tensor,
86    o_proj: Tensor,
87    // Quantized weights (optional, for efficient inference)
88    q_proj_q: Option<Arc<QMatMul>>,
89    k_proj_q: Option<Arc<QMatMul>>,
90    v_proj_q: Option<Arc<QMatMul>>,
91    o_proj_q: Option<Arc<QMatMul>>,
92    // SIMD quantized weights (optional, for native SIMD inference)
93    #[cfg(feature = "simd")]
94    q_proj_simd: Option<Arc<crate::simd::quant::QuantizedTensor>>,
95    #[cfg(feature = "simd")]
96    k_proj_simd: Option<Arc<crate::simd::quant::QuantizedTensor>>,
97    #[cfg(feature = "simd")]
98    v_proj_simd: Option<Arc<crate::simd::quant::QuantizedTensor>>,
99    #[cfg(feature = "simd")]
100    o_proj_simd: Option<Arc<crate::simd::quant::QuantizedTensor>>,
101    // Config
102    num_heads: usize,
103    num_key_value_heads: usize,
104    head_dim: usize,
105    scale: f32,
106}
107
108/// Llama MLP (feed-forward network)
109pub struct LlamaMLP {
110    // F32 weights (fallback)
111    gate_proj: Tensor,  // Gate projection
112    up_proj: Tensor,    // Up projection
113    down_proj: Tensor,  // Down projection
114    // Quantized weights (optional, for efficient inference)
115    gate_proj_q: Option<Arc<QMatMul>>,
116    up_proj_q: Option<Arc<QMatMul>>,
117    down_proj_q: Option<Arc<QMatMul>>,
118    // SIMD quantized weights (optional, for native SIMD inference)
119    #[cfg(feature = "simd")]
120    gate_proj_simd: Option<Arc<crate::simd::quant::QuantizedTensor>>,
121    #[cfg(feature = "simd")]
122    up_proj_simd: Option<Arc<crate::simd::quant::QuantizedTensor>>,
123    #[cfg(feature = "simd")]
124    down_proj_simd: Option<Arc<crate::simd::quant::QuantizedTensor>>,
125    // Config
126    hidden_act: String,
127}
128
129impl Model for LlamaModelV2 {
130    type Config = LlamaConfig;
131
132    fn new(config: Self::Config) -> Result<Self> {
133        let device = Device::CPU;
134
135        // Create model tensors with correct shapes
136        let embed_tokens = ops_fn::zeros(
137            &[config.vocab_size, config.hidden_size],
138            DataType::Float32,
139            &device
140        )?;
141
142        let norm = ops_fn::zeros(
143            &[config.hidden_size],
144            DataType::Float32,
145            &device
146        )?;
147
148        let lm_head = if config.tie_word_embeddings {
149            embed_tokens.clone()
150        } else {
151            ops_fn::zeros(
152                &[config.hidden_size, config.vocab_size],
153                DataType::Float32,
154                &device
155            )?
156        };
157
158        // Create transformer layers
159        let mut layers = Vec::with_capacity(config.num_hidden_layers);
160        for _ in 0..config.num_hidden_layers {
161            layers.push(LlamaLayer::new(&config, &device)?);
162        }
163
164        Ok(Self {
165            config,
166            device,
167            embed_tokens,
168            layers,
169            norm,
170            lm_head,
171            lm_head_q: None,
172        })
173    }
174
175    fn from_weights(config: Self::Config, weights: ModelWeights) -> Result<Self> {
176        let mut model = Self::new(config)?;
177
178        // Load weights from the unified weight container
179        // Note: embedding weight is not transposed (used for index lookup)
180        if let Some(embed_weights) = weights.get("model.embed_tokens.weight") {
181            model.embed_tokens = embed_weights.clone();
182        }
183
184        if let Some(norm_weights) = weights.get("model.norm.weight") {
185            model.norm = norm_weights.clone();
186        }
187
188        // lm_head weight needs transpose: [vocab, hidden] -> [hidden, vocab] for matmul
189        if let Some(lm_head_weights) = weights.get("lm_head.weight") {
190            model.lm_head = ops_fn::transpose(lm_head_weights)?;
191        }
192        // Load quantized lm_head if available
193        model.lm_head_q = weights.get_quantized("lm_head.weight");
194
195        // Load layer weights (including quantized)
196        for (i, layer) in model.layers.iter_mut().enumerate() {
197            layer.load_weights(&weights, i)?;
198        }
199
200        Ok(model)
201    }
202
203    fn forward(&self, inputs: &ModelInputs) -> Result<ModelOutputs> {
204        match inputs {
205            ModelInputs::Text { input_ids, attention_mask, .. } => {
206                // Debug: print input shape
207                // println!("Forward: input_ids shape: {:?}", input_ids.shape());
208
209                // 1. Token embedding
210                let mut hidden_states = ops_fn::embedding(input_ids, &self.embed_tokens)?;
211
212                // Debug: check embedding output (disabled for cleaner output)
213                // let emb_candle = hidden_states.to_candle()?;
214                // let emb_slice: Vec<f32> = emb_candle.flatten_all()?.to_vec1()?;
215                // let emb_sum: f32 = emb_slice.iter().take(100).sum();
216                // let emb_max = emb_slice.iter().take(100).cloned().fold(f32::NEG_INFINITY, f32::max);
217                // let emb_min = emb_slice.iter().take(100).cloned().fold(f32::INFINITY, f32::min);
218                // println!("After embedding: shape={:?}, sample sum={:.4}, min={:.4}, max={:.4}",
219                //     hidden_states.shape(), emb_sum, emb_min, emb_max);
220
221                // 2. Apply transformer layers (with RoPE)
222                for layer in &self.layers {
223                    hidden_states = layer.forward(&hidden_states, attention_mask.as_ref(), self.config.rope_theta)?;
224                }
225
226                // 3. Final layer norm
227                hidden_states = ops_fn::layer_norm(&hidden_states, &self.norm, None, self.config.rms_norm_eps)?;
228
229                // 4. Language modeling head (use quantized if available)
230                let logits = self.lm_head_forward(&hidden_states)?;
231
232                Ok(ModelOutputs::Logits {
233                    logits,
234                    hidden_states: None,  // Don't return hidden states to save memory
235                })
236            }
237            ModelInputs::Multimodal { input_ids, .. } => {
238                // For multimodal inputs, just process text part for now
239                let text_inputs = ModelInputs::Text {
240                    input_ids: input_ids.clone(),
241                    attention_mask: None,
242                    position_ids: None,
243                };
244                self.forward(&text_inputs)
245            }
246            _ => Err(anyhow::anyhow!("Llama model only supports text and multimodal inputs")),
247        }
248    }
249
250    fn generate(&self, prompt: &str, config: &GenerationConfig) -> Result<String> {
251        use crate::tokenizer::Tokenizer;
252        use rand::Rng;
253
254        // 1. Tokenize prompt
255        let tokenizer = Tokenizer::new();
256        let mut tokens: Vec<u32> = tokenizer.encode(prompt);
257
258        // 2. Generation loop
259        for _ in 0..config.max_new_tokens {
260            // Create input tensor from current tokens
261            let tokens_i64: Vec<i64> = tokens.iter().map(|&t| t as i64).collect();
262            let input_tensor = Tensor::from_i64_slice(&tokens_i64, &[1, tokens.len()], &self.device)?;
263
264            let inputs = ModelInputs::Text {
265                input_ids: input_tensor,
266                attention_mask: None,
267                position_ids: None,
268            };
269
270            // 3. Forward pass
271            let outputs = self.forward(&inputs)?;
272
273            // 4. Get logits and sample next token
274            let logits = match outputs {
275                ModelOutputs::Logits { logits, .. } => logits,
276                _ => return Err(anyhow::anyhow!("Expected logits output")),
277            };
278
279            // Get last token logits
280            let logits_candle = logits.to_candle()?;
281            let shape = logits_candle.dims();
282
283            // Extract last position logits [batch, seq, vocab] -> [vocab]
284            let last_logits = if shape.len() == 3 {
285                let seq_len = shape[1];
286                logits_candle
287                    .narrow(1, seq_len - 1, 1)?
288                    .squeeze(1)?
289                    .squeeze(0)?
290            } else {
291                let seq_len = shape[0];
292                logits_candle
293                    .narrow(0, seq_len - 1, 1)?
294                    .squeeze(0)?
295            };
296
297            // Convert to probabilities and sample
298            let logits_vec: Vec<f32> = last_logits.to_vec1()?;
299
300            let next_token = if config.do_sample && config.temperature > 0.0 {
301                // Temperature sampling
302                let scaled: Vec<f32> = logits_vec.iter()
303                    .map(|&x| x / config.temperature)
304                    .collect();
305
306                // Softmax
307                let max_val = scaled.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
308                let exp_sum: f32 = scaled.iter().map(|&x| (x - max_val).exp()).sum();
309                let probs: Vec<f32> = scaled.iter()
310                    .map(|&x| (x - max_val).exp() / exp_sum)
311                    .collect();
312
313                // Sample from distribution
314                let mut rng = rand::thread_rng();
315                let random_val: f32 = rng.gen();
316                let mut cumulative = 0.0;
317                let mut sampled = 0u32;
318
319                for (idx, &prob) in probs.iter().enumerate() {
320                    cumulative += prob;
321                    if random_val <= cumulative {
322                        sampled = idx as u32;
323                        break;
324                    }
325                }
326                sampled
327            } else {
328                // Greedy sampling
329                let mut max_idx = 0;
330                let mut max_val = logits_vec[0];
331                for (idx, &val) in logits_vec.iter().enumerate() {
332                    if val > max_val {
333                        max_val = val;
334                        max_idx = idx;
335                    }
336                }
337                max_idx as u32
338            };
339
340            // 5. Check for EOS
341            if next_token == config.eos_token_id {
342                break;
343            }
344
345            // 6. Append token
346            tokens.push(next_token);
347        }
348
349        // 7. Decode and return
350        Ok(tokenizer.decode(&tokens))
351    }
352
353    fn config(&self) -> &Self::Config {
354        &self.config
355    }
356
357    fn memory_requirements(&self) -> MemoryRequirements {
358        // Calculate approximate memory requirements
359        let param_size = self.config.vocab_size * self.config.hidden_size + // embeddings
360                        self.config.num_hidden_layers * (
361                            4 * self.config.hidden_size * self.config.hidden_size + // attention
362                            3 * self.config.hidden_size * self.config.intermediate_size // MLP
363                        );
364
365        let param_bytes = param_size * 4; // float32
366        let kv_cache_bytes = 2 * self.config.num_hidden_layers *
367                           self.config.max_position_embeddings *
368                           self.config.hidden_size * 4; // K and V caches
369
370        MemoryRequirements {
371            gpu_memory: param_bytes,
372            cpu_memory: param_bytes / 4, // Reduced for CPU
373            kv_cache_memory: kv_cache_bytes,
374            peak_memory: param_bytes + kv_cache_bytes,
375        }
376    }
377
378    fn to_device(&mut self, device: &Device) -> Result<()> {
379        // Move all tensors to the specified device
380        self.embed_tokens = self.embed_tokens.to_device(device)?;
381        self.norm = self.norm.to_device(device)?;
382        self.lm_head = self.lm_head.to_device(device)?;
383
384        for layer in &mut self.layers {
385            layer.to_device(device)?;
386        }
387
388        self.device = device.clone();
389        Ok(())
390    }
391}
392
393// Helper methods
394impl LlamaModelV2 {
395    /// Apply lm_head projection, using quantized if available
396    fn lm_head_forward(&self, hidden_states: &Tensor) -> Result<Tensor> {
397        if let Some(ref qmatmul) = self.lm_head_q {
398            let input_candle = hidden_states.to_candle()?;
399            let output = qmatmul.forward(&input_candle)
400                .map_err(|e| anyhow::anyhow!("QMatMul lm_head forward failed: {}", e))?;
401            Ok(Tensor::from_candle(output))
402        } else {
403            ops_fn::matmul(hidden_states, &self.lm_head)
404        }
405    }
406}
407
408// KV Cache methods for efficient autoregressive generation
409impl LlamaModelV2 {
410    /// Forward pass with KV cache support
411    ///
412    /// This method enables efficient autoregressive generation by caching
413    /// Key and Value tensors from previous tokens.
414    ///
415    /// # Arguments
416    /// * `inputs` - Model inputs (only new tokens)
417    /// * `cache` - Optional mutable reference to KV cache
418    ///
419    /// # Returns
420    /// Model outputs with logits for the new tokens
421    pub fn forward_with_cache(
422        &self,
423        inputs: &ModelInputs,
424        mut cache: Option<&mut KVCache>,
425    ) -> Result<ModelOutputs> {
426        match inputs {
427            ModelInputs::Text { input_ids, .. } => {
428                // Get position offset from cache
429                let position_offset = cache.as_ref().map(|c| c.seq_len()).unwrap_or(0);
430
431                // 1. Token embedding (only for new tokens)
432                let mut hidden_states = ops_fn::embedding(input_ids, &self.embed_tokens)?;
433
434                // 2. Apply transformer layers with cache
435                for (layer_idx, layer) in self.layers.iter().enumerate() {
436                    let layer_cache = cache.as_mut().map(|c| c.layer_mut(layer_idx));
437                    hidden_states = layer.forward_with_cache(
438                        &hidden_states,
439                        layer_cache,
440                        position_offset,
441                        self.config.rope_theta,
442                    )?;
443                }
444
445                // 3. Final layer norm
446                hidden_states = ops_fn::layer_norm(&hidden_states, &self.norm, None, self.config.rms_norm_eps)?;
447
448                // 4. Language modeling head (use quantized if available)
449                let logits = self.lm_head_forward(&hidden_states)?;
450
451                // 5. Update cache sequence length
452                if let Some(cache) = cache {
453                    let new_tokens = input_ids.shape().get(1).copied().unwrap_or(1);
454                    cache.set_seq_len(position_offset + new_tokens);
455                }
456
457                Ok(ModelOutputs::Logits {
458                    logits,
459                    hidden_states: None,
460                })
461            }
462            _ => Err(anyhow::anyhow!("forward_with_cache only supports text inputs")),
463        }
464    }
465
466    /// Generate text with KV caching for efficient autoregressive generation
467    ///
468    /// This method is significantly faster than `generate()` for long sequences
469    /// because it caches Key and Value tensors instead of recomputing them.
470    ///
471    /// # Arguments
472    /// * `prompt` - Input text prompt
473    /// * `config` - Generation configuration
474    ///
475    /// # Returns
476    /// Generated text including the prompt
477    pub fn generate_with_cache(&self, prompt: &str, config: &GenerationConfig) -> Result<String> {
478        use crate::tokenizer::Tokenizer;
479
480        // 1. Tokenize prompt
481        let tokenizer = Tokenizer::new();
482        let prompt_tokens: Vec<u32> = tokenizer.encode(prompt);
483        let mut tokens = prompt_tokens.clone();
484
485        // 2. Initialize KV cache
486        let mut cache = KVCache::new(self.config.num_hidden_layers);
487
488        // 3. PREFILL: Process entire prompt at once
489        let prompt_i64: Vec<i64> = prompt_tokens.iter().map(|&t| t as i64).collect();
490        let prompt_tensor = Tensor::from_i64_slice(&prompt_i64, &[1, prompt_tokens.len()], &self.device)?;
491        let prompt_inputs = ModelInputs::Text {
492            input_ids: prompt_tensor,
493            attention_mask: None,
494            position_ids: None,
495        };
496
497        // Prefill: process all prompt tokens, populate cache
498        let outputs = self.forward_with_cache(&prompt_inputs, Some(&mut cache))?;
499
500        // Get the last token's logits from prefill to sample first new token
501        let logits = match outputs {
502            ModelOutputs::Logits { logits, .. } => logits,
503            _ => return Err(anyhow::anyhow!("Expected logits output")),
504        };
505
506        // Sample first new token from prefill output
507        let logits_candle = logits.to_candle()?;
508        let shape = logits_candle.dims();
509        let seq_len = if shape.len() == 3 { shape[1] } else { shape[0] };
510        let last_logits = if shape.len() == 3 {
511            logits_candle.narrow(1, seq_len - 1, 1)?.squeeze(1)?.squeeze(0)?
512        } else {
513            logits_candle.narrow(0, seq_len - 1, 1)?.squeeze(0)?
514        };
515
516        let mut next_token = self.sample_token_from_logits(&last_logits, config)?;
517
518        // Check EOS
519        if next_token == config.eos_token_id {
520            return Ok(tokenizer.decode(&tokens));
521        }
522        tokens.push(next_token);
523
524        // 4. DECODE: Generate tokens one at a time using cache
525        for _ in 1..config.max_new_tokens {
526            // Create input tensor for SINGLE new token
527            let input_tensor = Tensor::from_i64_slice(
528                &[next_token as i64],
529                &[1, 1],
530                &self.device
531            )?;
532
533            let inputs = ModelInputs::Text {
534                input_ids: input_tensor,
535                attention_mask: None,
536                position_ids: None,
537            };
538
539            // Forward with cache: only processes the new token
540            let outputs = self.forward_with_cache(&inputs, Some(&mut cache))?;
541
542            // Get logits and sample
543            let logits = match outputs {
544                ModelOutputs::Logits { logits, .. } => logits,
545                _ => return Err(anyhow::anyhow!("Expected logits output")),
546            };
547
548            let logits_candle = logits.to_candle()?;
549            let last_logits = logits_candle.squeeze(0)?.squeeze(0)?;
550
551            next_token = self.sample_token_from_logits(&last_logits, config)?;
552
553            // Check EOS
554            if next_token == config.eos_token_id {
555                break;
556            }
557
558            tokens.push(next_token);
559        }
560
561        // 5. Decode and return
562        Ok(tokenizer.decode(&tokens))
563    }
564
565    /// Sample a token from logits vector
566    fn sample_token_from_logits(&self, logits: &candle_core::Tensor, config: &GenerationConfig) -> Result<u32> {
567        use rand::Rng;
568
569        let logits_vec: Vec<f32> = logits.to_vec1()?;
570
571        let next_token = if config.do_sample && config.temperature > 0.0 {
572            // Temperature sampling
573            let scaled: Vec<f32> = logits_vec.iter()
574                .map(|&x| x / config.temperature)
575                .collect();
576
577            // Softmax
578            let max_val = scaled.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
579            let exp_sum: f32 = scaled.iter().map(|&x| (x - max_val).exp()).sum();
580            let probs: Vec<f32> = scaled.iter()
581                .map(|&x| (x - max_val).exp() / exp_sum)
582                .collect();
583
584            // Sample from distribution
585            let mut rng = rand::thread_rng();
586            let random_val: f32 = rng.gen();
587            let mut cumulative = 0.0;
588            let mut sampled = 0u32;
589
590            for (idx, &prob) in probs.iter().enumerate() {
591                cumulative += prob;
592                if random_val <= cumulative {
593                    sampled = idx as u32;
594                    break;
595                }
596            }
597            sampled
598        } else {
599            // Greedy sampling
600            let mut max_idx = 0;
601            let mut max_val = logits_vec[0];
602            for (idx, &val) in logits_vec.iter().enumerate() {
603                if val > max_val {
604                    max_val = val;
605                    max_idx = idx;
606                }
607            }
608            max_idx as u32
609        };
610
611        Ok(next_token)
612    }
613}
614
615impl LlamaLayer {
616    fn new(config: &LlamaConfig, device: &Device) -> Result<Self> {
617        let self_attn = LlamaAttention::new(config, device)?;
618        let mlp = LlamaMLP::new(config, device)?;
619
620        let input_layernorm = ops_fn::zeros(&[config.hidden_size], DataType::Float32, device)?;
621        let post_attention_layernorm = ops_fn::zeros(&[config.hidden_size], DataType::Float32, device)?;
622
623        Ok(Self {
624            self_attn,
625            mlp,
626            input_layernorm,
627            post_attention_layernorm,
628        })
629    }
630
631    fn forward(&self, hidden_states: &Tensor, attention_mask: Option<&Tensor>, rope_theta: f32) -> Result<Tensor> {
632        // 1. Pre-attention layer norm
633        let normed = ops_fn::layer_norm(hidden_states, &self.input_layernorm, None, 1e-6)?;
634
635        // 2. Self attention (with RoPE)
636        let attn_output = self.self_attn.forward(&normed, attention_mask, rope_theta)?;
637
638        // 3. Residual connection
639        let hidden_states = ops_fn::add(hidden_states, &attn_output)?;
640
641        // 4. Pre-MLP layer norm
642        let normed = ops_fn::layer_norm(&hidden_states, &self.post_attention_layernorm, None, 1e-6)?;
643
644        // 5. MLP
645        let mlp_output = self.mlp.forward(&normed)?;
646
647        // 6. Residual connection
648        let output = ops_fn::add(&hidden_states, &mlp_output)?;
649
650        Ok(output)
651    }
652
653    /// Forward pass with KV cache support
654    fn forward_with_cache(
655        &self,
656        hidden_states: &Tensor,
657        cache: Option<&mut LayerKVCache>,
658        position_offset: usize,
659        rope_theta: f32,
660    ) -> Result<Tensor> {
661        // 1. Pre-attention layer norm
662        let normed = ops_fn::layer_norm(hidden_states, &self.input_layernorm, None, 1e-6)?;
663
664        // 2. Self attention with cache
665        let attn_output = self.self_attn.forward_with_cache(&normed, cache, position_offset, rope_theta)?;
666
667        // 3. Residual connection
668        let hidden_states = ops_fn::add(hidden_states, &attn_output)?;
669
670        // 4. Pre-MLP layer norm
671        let normed = ops_fn::layer_norm(&hidden_states, &self.post_attention_layernorm, None, 1e-6)?;
672
673        // 5. MLP (unchanged - no caching needed)
674        let mlp_output = self.mlp.forward(&normed)?;
675
676        // 6. Residual connection
677        let output = ops_fn::add(&hidden_states, &mlp_output)?;
678
679        Ok(output)
680    }
681
682    fn load_weights(&mut self, weights: &ModelWeights, layer_idx: usize) -> Result<()> {
683        let prefix = format!("model.layers.{}", layer_idx);
684
685        // Load attention weights (transpose for matmul: [out, in] -> [in, out])
686        if let Some(q_proj) = weights.get(&format!("{}.self_attn.q_proj.weight", prefix)) {
687            self.self_attn.q_proj = ops_fn::transpose(q_proj)?;
688        }
689        if let Some(k_proj) = weights.get(&format!("{}.self_attn.k_proj.weight", prefix)) {
690            self.self_attn.k_proj = ops_fn::transpose(k_proj)?;
691        }
692        if let Some(v_proj) = weights.get(&format!("{}.self_attn.v_proj.weight", prefix)) {
693            self.self_attn.v_proj = ops_fn::transpose(v_proj)?;
694        }
695        if let Some(o_proj) = weights.get(&format!("{}.self_attn.o_proj.weight", prefix)) {
696            self.self_attn.o_proj = ops_fn::transpose(o_proj)?;
697        }
698
699        // Load quantized attention weights if available
700        self.self_attn.q_proj_q = weights.get_quantized(&format!("{}.self_attn.q_proj.weight", prefix));
701        self.self_attn.k_proj_q = weights.get_quantized(&format!("{}.self_attn.k_proj.weight", prefix));
702        self.self_attn.v_proj_q = weights.get_quantized(&format!("{}.self_attn.v_proj.weight", prefix));
703        self.self_attn.o_proj_q = weights.get_quantized(&format!("{}.self_attn.o_proj.weight", prefix));
704
705        // Load SIMD quantized attention weights if available
706        #[cfg(feature = "simd")]
707        {
708            self.self_attn.q_proj_simd = weights.get_simd_quantized(&format!("{}.self_attn.q_proj.weight", prefix));
709            self.self_attn.k_proj_simd = weights.get_simd_quantized(&format!("{}.self_attn.k_proj.weight", prefix));
710            self.self_attn.v_proj_simd = weights.get_simd_quantized(&format!("{}.self_attn.v_proj.weight", prefix));
711            self.self_attn.o_proj_simd = weights.get_simd_quantized(&format!("{}.self_attn.o_proj.weight", prefix));
712        }
713
714        // Load MLP weights (transpose for matmul: [out, in] -> [in, out])
715        if let Some(gate_proj) = weights.get(&format!("{}.mlp.gate_proj.weight", prefix)) {
716            self.mlp.gate_proj = ops_fn::transpose(gate_proj)?;
717        }
718        if let Some(up_proj) = weights.get(&format!("{}.mlp.up_proj.weight", prefix)) {
719            self.mlp.up_proj = ops_fn::transpose(up_proj)?;
720        }
721        if let Some(down_proj) = weights.get(&format!("{}.mlp.down_proj.weight", prefix)) {
722            self.mlp.down_proj = ops_fn::transpose(down_proj)?;
723        }
724
725        // Load quantized MLP weights if available
726        self.mlp.gate_proj_q = weights.get_quantized(&format!("{}.mlp.gate_proj.weight", prefix));
727        self.mlp.up_proj_q = weights.get_quantized(&format!("{}.mlp.up_proj.weight", prefix));
728        self.mlp.down_proj_q = weights.get_quantized(&format!("{}.mlp.down_proj.weight", prefix));
729
730        // Load SIMD quantized MLP weights if available
731        #[cfg(feature = "simd")]
732        {
733            self.mlp.gate_proj_simd = weights.get_simd_quantized(&format!("{}.mlp.gate_proj.weight", prefix));
734            self.mlp.up_proj_simd = weights.get_simd_quantized(&format!("{}.mlp.up_proj.weight", prefix));
735            self.mlp.down_proj_simd = weights.get_simd_quantized(&format!("{}.mlp.down_proj.weight", prefix));
736        }
737
738        // Load layer norm weights (no transpose needed - 1D tensors)
739        if let Some(input_ln) = weights.get(&format!("{}.input_layernorm.weight", prefix)) {
740            self.input_layernorm = input_ln.clone();
741        }
742        if let Some(post_ln) = weights.get(&format!("{}.post_attention_layernorm.weight", prefix)) {
743            self.post_attention_layernorm = post_ln.clone();
744        }
745
746        Ok(())
747    }
748
749    fn to_device(&mut self, device: &Device) -> Result<()> {
750        self.self_attn.to_device(device)?;
751        self.mlp.to_device(device)?;
752        self.input_layernorm = self.input_layernorm.to_device(device)?;
753        self.post_attention_layernorm = self.post_attention_layernorm.to_device(device)?;
754        Ok(())
755    }
756}
757
758/// Apply Rotary Position Embedding (RoPE) to Q and K tensors
759/// Input shape: [batch, heads, seq, head_dim]
760/// Returns tensors with same shape but with positional information encoded
761fn apply_rope(
762    q: &candle_core::Tensor,
763    k: &candle_core::Tensor,
764    seq_len: usize,
765    head_dim: usize,
766    rope_theta: f32,
767) -> Result<(candle_core::Tensor, candle_core::Tensor)> {
768    use candle_core::{DType, Device};
769
770    let device = q.device();
771
772    // Compute inverse frequencies: 1 / (theta^(2i/d)) for i in [0, d/2)
773    let half_dim = head_dim / 2;
774    let inv_freq: Vec<f32> = (0..half_dim)
775        .map(|i| 1.0 / rope_theta.powf((2 * i) as f32 / head_dim as f32))
776        .collect();
777
778    // Create position indices [0, 1, 2, ..., seq_len-1]
779    let positions: Vec<f32> = (0..seq_len).map(|p| p as f32).collect();
780
781    // Compute angles: pos * inv_freq -> [seq_len, half_dim]
782    let mut angles = Vec::with_capacity(seq_len * half_dim);
783    for pos in &positions {
784        for freq in &inv_freq {
785            angles.push(pos * freq);
786        }
787    }
788
789    let angles_tensor = candle_core::Tensor::from_vec(angles, &[seq_len, half_dim], device)?;
790
791    // Compute cos and sin
792    let cos = angles_tensor.cos()?;
793    let sin = angles_tensor.sin()?;
794
795    // Reshape for broadcasting: [1, 1, seq_len, half_dim]
796    let cos = cos.unsqueeze(0)?.unsqueeze(0)?;
797    let sin = sin.unsqueeze(0)?.unsqueeze(0)?;
798
799    // Apply RoPE rotation
800    // Split q and k into two halves along head_dim
801    // q = [q1, q2], k = [k1, k2] where each half has shape [..., half_dim]
802    // rotated_q = [q1*cos - q2*sin, q1*sin + q2*cos]
803    // rotated_k = [k1*cos - k2*sin, k1*sin + k2*cos]
804
805    let q_half1 = q.narrow(3, 0, half_dim)?;
806    let q_half2 = q.narrow(3, half_dim, half_dim)?;
807    let k_half1 = k.narrow(3, 0, half_dim)?;
808    let k_half2 = k.narrow(3, half_dim, half_dim)?;
809
810    // Apply rotation
811    let q_rot1 = (q_half1.broadcast_mul(&cos)? - q_half2.broadcast_mul(&sin)?)?;
812    let q_rot2 = (q_half1.broadcast_mul(&sin)? + q_half2.broadcast_mul(&cos)?)?;
813    let k_rot1 = (k_half1.broadcast_mul(&cos)? - k_half2.broadcast_mul(&sin)?)?;
814    let k_rot2 = (k_half1.broadcast_mul(&sin)? + k_half2.broadcast_mul(&cos)?)?;
815
816    // Concatenate rotated halves
817    let q_rotated = candle_core::Tensor::cat(&[&q_rot1, &q_rot2], 3)?;
818    let k_rotated = candle_core::Tensor::cat(&[&k_rot1, &k_rot2], 3)?;
819
820    Ok((q_rotated, k_rotated))
821}
822
823/// Apply Rotary Position Embedding (RoPE) with a position offset
824/// This is used for KV caching where we need to apply RoPE starting from a specific position
825/// Input shape: [batch, heads, seq, head_dim]
826/// Returns tensors with same shape but with positional information encoded
827fn apply_rope_with_offset(
828    q: &candle_core::Tensor,
829    k: &candle_core::Tensor,
830    seq_len: usize,
831    head_dim: usize,
832    rope_theta: f32,
833    position_offset: usize,
834) -> Result<(candle_core::Tensor, candle_core::Tensor)> {
835    let device = q.device();
836
837    // Compute inverse frequencies: 1 / (theta^(2i/d)) for i in [0, d/2)
838    let half_dim = head_dim / 2;
839    let inv_freq: Vec<f32> = (0..half_dim)
840        .map(|i| 1.0 / rope_theta.powf((2 * i) as f32 / head_dim as f32))
841        .collect();
842
843    // Create position indices starting from offset: [offset, offset+1, ..., offset+seq_len-1]
844    let positions: Vec<f32> = (0..seq_len)
845        .map(|p| (p + position_offset) as f32)
846        .collect();
847
848    // Compute angles: pos * inv_freq -> [seq_len, half_dim]
849    let mut angles = Vec::with_capacity(seq_len * half_dim);
850    for pos in &positions {
851        for freq in &inv_freq {
852            angles.push(pos * freq);
853        }
854    }
855
856    let angles_tensor = candle_core::Tensor::from_vec(angles, &[seq_len, half_dim], device)?;
857
858    // Compute cos and sin
859    let cos = angles_tensor.cos()?;
860    let sin = angles_tensor.sin()?;
861
862    // Reshape for broadcasting: [1, 1, seq_len, half_dim]
863    let cos = cos.unsqueeze(0)?.unsqueeze(0)?;
864    let sin = sin.unsqueeze(0)?.unsqueeze(0)?;
865
866    // Apply RoPE rotation
867    let q_half1 = q.narrow(3, 0, half_dim)?;
868    let q_half2 = q.narrow(3, half_dim, half_dim)?;
869    let k_half1 = k.narrow(3, 0, half_dim)?;
870    let k_half2 = k.narrow(3, half_dim, half_dim)?;
871
872    // Apply rotation
873    let q_rot1 = (q_half1.broadcast_mul(&cos)? - q_half2.broadcast_mul(&sin)?)?;
874    let q_rot2 = (q_half1.broadcast_mul(&sin)? + q_half2.broadcast_mul(&cos)?)?;
875    let k_rot1 = (k_half1.broadcast_mul(&cos)? - k_half2.broadcast_mul(&sin)?)?;
876    let k_rot2 = (k_half1.broadcast_mul(&sin)? + k_half2.broadcast_mul(&cos)?)?;
877
878    // Concatenate rotated halves
879    let q_rotated = candle_core::Tensor::cat(&[&q_rot1, &q_rot2], 3)?;
880    let k_rotated = candle_core::Tensor::cat(&[&k_rot1, &k_rot2], 3)?;
881
882    Ok((q_rotated, k_rotated))
883}
884
885impl LlamaAttention {
886    fn new(config: &LlamaConfig, device: &Device) -> Result<Self> {
887        let num_heads = config.num_attention_heads;
888        let num_key_value_heads = config.num_key_value_heads;
889        let head_dim = config.hidden_size / num_heads;
890        let scale = 1.0 / (head_dim as f32).sqrt();
891
892        let q_proj = ops_fn::zeros(&[config.hidden_size, num_heads * head_dim], DataType::Float32, device)?;
893        let k_proj = ops_fn::zeros(&[config.hidden_size, num_key_value_heads * head_dim], DataType::Float32, device)?;
894        let v_proj = ops_fn::zeros(&[config.hidden_size, num_key_value_heads * head_dim], DataType::Float32, device)?;
895        let o_proj = ops_fn::zeros(&[num_heads * head_dim, config.hidden_size], DataType::Float32, device)?;
896
897        Ok(Self {
898            q_proj,
899            k_proj,
900            v_proj,
901            o_proj,
902            q_proj_q: None,
903            k_proj_q: None,
904            v_proj_q: None,
905            o_proj_q: None,
906            #[cfg(feature = "simd")]
907            q_proj_simd: None,
908            #[cfg(feature = "simd")]
909            k_proj_simd: None,
910            #[cfg(feature = "simd")]
911            v_proj_simd: None,
912            #[cfg(feature = "simd")]
913            o_proj_simd: None,
914            num_heads,
915            num_key_value_heads,
916            head_dim,
917            scale,
918        })
919    }
920
921    fn forward(&self, hidden_states: &Tensor, _attention_mask: Option<&Tensor>, rope_theta: f32) -> Result<Tensor> {
922        // Get batch and sequence length from hidden_states shape
923        let shape = hidden_states.shape();
924        let (batch_size, seq_len, _hidden_size) = if shape.len() == 3 {
925            (shape[0], shape[1], shape[2])
926        } else if shape.len() == 2 {
927            (1, shape[0], shape[1])
928        } else {
929            return Err(anyhow::anyhow!("Invalid hidden_states shape: {:?}", shape));
930        };
931
932        // 1. Project to Q, K, V (use quantized if available)
933        // Q: [batch, seq, hidden] @ [hidden, num_heads * head_dim] -> [batch, seq, num_heads * head_dim]
934        // K: [batch, seq, hidden] @ [hidden, num_kv_heads * head_dim] -> [batch, seq, num_kv_heads * head_dim]
935        // V: [batch, seq, hidden] @ [hidden, num_kv_heads * head_dim] -> [batch, seq, num_kv_heads * head_dim]
936        let query_states = self.quantized_matmul(hidden_states, &self.q_proj, &self.q_proj_q)?;
937        let key_states = self.quantized_matmul(hidden_states, &self.k_proj, &self.k_proj_q)?;
938        let value_states = self.quantized_matmul(hidden_states, &self.v_proj, &self.v_proj_q)?;
939
940        // 2. Reshape for multi-head attention
941        // Q: [batch, seq, num_heads * head_dim] -> [batch, num_heads, seq, head_dim]
942        // K: [batch, seq, num_kv_heads * head_dim] -> [batch, num_kv_heads, seq, head_dim]
943        let q_candle = query_states.to_candle()?;
944        let k_candle = key_states.to_candle()?;
945        let v_candle = value_states.to_candle()?;
946
947        // Reshape: [batch, seq, heads*head_dim] -> [batch, seq, heads, head_dim] -> [batch, heads, seq, head_dim]
948        let q_reshaped = q_candle
949            .reshape(&[batch_size, seq_len, self.num_heads, self.head_dim])?
950            .transpose(1, 2)?;  // [batch, heads, seq, head_dim]
951
952        let k_reshaped = k_candle
953            .reshape(&[batch_size, seq_len, self.num_key_value_heads, self.head_dim])?
954            .transpose(1, 2)?;  // [batch, kv_heads, seq, head_dim]
955
956        let v_reshaped = v_candle
957            .reshape(&[batch_size, seq_len, self.num_key_value_heads, self.head_dim])?
958            .transpose(1, 2)?;  // [batch, kv_heads, seq, head_dim]
959
960        // 2.5 Apply RoPE (Rotary Position Embedding) to Q and K
961        let (q_with_rope, k_with_rope) = apply_rope(&q_reshaped, &k_reshaped, seq_len, self.head_dim, rope_theta)?;
962
963        // 3. Handle GQA (Grouped Query Attention) - repeat K/V heads to match Q heads
964        let num_groups = self.num_heads / self.num_key_value_heads;
965        let (k_expanded, v_expanded) = if num_groups > 1 {
966            // Repeat K and V along the head dimension
967            // [batch, kv_heads, seq, head_dim] -> [batch, num_heads, seq, head_dim]
968            let k_rep = k_with_rope
969                .unsqueeze(2)?  // [batch, kv_heads, 1, seq, head_dim]
970                .broadcast_as(&[batch_size, self.num_key_value_heads, num_groups, seq_len, self.head_dim])?
971                .reshape(&[batch_size, self.num_heads, seq_len, self.head_dim])?;
972            let v_rep = v_reshaped
973                .unsqueeze(2)?
974                .broadcast_as(&[batch_size, self.num_key_value_heads, num_groups, seq_len, self.head_dim])?
975                .reshape(&[batch_size, self.num_heads, seq_len, self.head_dim])?;
976            (k_rep, v_rep)
977        } else {
978            (k_with_rope, v_reshaped)
979        };
980
981        // 4. Fused scaled dot-product attention with causal masking
982        // Uses ops_fn::flash_attention which fuses: Q @ K^T, scale, mask, softmax, @ V
983        // Input shapes: [batch, heads, seq, head_dim]
984
985        // Make tensors contiguous (required by some backends)
986        let q_contiguous = q_with_rope.contiguous()?;
987        let k_contiguous = k_expanded.contiguous()?;
988        let v_contiguous = v_expanded.contiguous()?;
989
990        // Convert to Tensor type for ops_fn
991        let q_tensor = Tensor::from_candle(q_contiguous);
992        let k_tensor = Tensor::from_candle(k_contiguous);
993        let v_tensor = Tensor::from_candle(v_contiguous);
994
995        // Fused attention: softmax((Q @ K^T) * scale) @ V with causal masking
996        let attn_output_tensor = ops_fn::flash_attention(
997            &q_tensor,
998            &k_tensor,
999            &v_tensor,
1000            self.scale,
1001            true,  // causal = true for autoregressive
1002        )?;
1003
1004        // Convert back to Candle tensor for the rest of the computation
1005        let attn_output = attn_output_tensor.to_candle()?;
1006
1007        // 5. Reshape back: [batch, heads, seq, head_dim] -> [batch, seq, heads * head_dim]
1008        let attn_output = attn_output
1009            .transpose(1, 2)?  // [batch, seq, heads, head_dim]
1010            .reshape(&[batch_size, seq_len, self.num_heads * self.head_dim])?;
1011
1012        let attn_output = Tensor::from_candle(attn_output);
1013
1014        // 6. Output projection (use quantized if available)
1015        let output = self.quantized_matmul(&attn_output, &self.o_proj, &self.o_proj_q)?;
1016
1017        Ok(output)
1018    }
1019
1020    /// Forward pass with KV cache support for efficient autoregressive generation
1021    ///
1022    /// # Arguments
1023    /// * `hidden_states` - Input hidden states for NEW tokens only
1024    /// * `cache` - Optional mutable reference to layer KV cache
1025    /// * `position_offset` - Starting position for RoPE (cache.seq_len for incremental)
1026    /// * `rope_theta` - RoPE theta parameter
1027    fn forward_with_cache(
1028        &self,
1029        hidden_states: &Tensor,
1030        cache: Option<&mut LayerKVCache>,
1031        position_offset: usize,
1032        rope_theta: f32,
1033    ) -> Result<Tensor> {
1034        // Get batch and sequence length from hidden_states shape (new tokens only)
1035        let shape = hidden_states.shape();
1036        let (batch_size, new_seq_len, _hidden_size) = if shape.len() == 3 {
1037            (shape[0], shape[1], shape[2])
1038        } else if shape.len() == 2 {
1039            (1, shape[0], shape[1])
1040        } else {
1041            return Err(anyhow::anyhow!("Invalid hidden_states shape: {:?}", shape));
1042        };
1043
1044        // 1. Project to Q, K, V for NEW tokens only (use quantized if available)
1045        let query_states = self.quantized_matmul(hidden_states, &self.q_proj, &self.q_proj_q)?;
1046        let key_states = self.quantized_matmul(hidden_states, &self.k_proj, &self.k_proj_q)?;
1047        let value_states = self.quantized_matmul(hidden_states, &self.v_proj, &self.v_proj_q)?;
1048
1049        // 2. Reshape for multi-head attention
1050        let q_candle = query_states.to_candle()?;
1051        let k_candle = key_states.to_candle()?;
1052        let v_candle = value_states.to_candle()?;
1053
1054        let q_reshaped = q_candle
1055            .reshape(&[batch_size, new_seq_len, self.num_heads, self.head_dim])?
1056            .transpose(1, 2)?;  // [batch, heads, new_seq, head_dim]
1057
1058        let k_reshaped = k_candle
1059            .reshape(&[batch_size, new_seq_len, self.num_key_value_heads, self.head_dim])?
1060            .transpose(1, 2)?;  // [batch, kv_heads, new_seq, head_dim]
1061
1062        let v_reshaped = v_candle
1063            .reshape(&[batch_size, new_seq_len, self.num_key_value_heads, self.head_dim])?
1064            .transpose(1, 2)?;  // [batch, kv_heads, new_seq, head_dim]
1065
1066        // 3. Apply RoPE with position offset
1067        let (q_with_rope, k_with_rope) = apply_rope_with_offset(
1068            &q_reshaped, &k_reshaped,
1069            new_seq_len, self.head_dim, rope_theta,
1070            position_offset
1071        )?;
1072
1073        // 4. Handle GQA expansion for K before caching (expand once, cache expanded)
1074        let num_groups = self.num_heads / self.num_key_value_heads;
1075        let (k_expanded, v_expanded) = if num_groups > 1 {
1076            let k_rep = k_with_rope
1077                .unsqueeze(2)?
1078                .broadcast_as(&[batch_size, self.num_key_value_heads, num_groups, new_seq_len, self.head_dim])?
1079                .reshape(&[batch_size, self.num_heads, new_seq_len, self.head_dim])?;
1080            let v_rep = v_reshaped
1081                .unsqueeze(2)?
1082                .broadcast_as(&[batch_size, self.num_key_value_heads, num_groups, new_seq_len, self.head_dim])?
1083                .reshape(&[batch_size, self.num_heads, new_seq_len, self.head_dim])?;
1084            (k_rep, v_rep)
1085        } else {
1086            (k_with_rope, v_reshaped)
1087        };
1088
1089        // 5. Update cache and get full K, V (using pre-allocated buffers)
1090        let (full_k, full_v, total_seq_len) = if let Some(cache) = cache {
1091            // Append new K,V to cache using slice_set (zero-allocation!)
1092            if let Err(e) = cache.append(&k_expanded, &v_expanded) {
1093                return Err(anyhow::anyhow!("KV cache append failed: {}", e));
1094            }
1095
1096            // Get view of full cached K,V (narrow, no copy)
1097            match cache.get_kv() {
1098                Some((k, v)) => {
1099                    let total_len = k.dims()[2];
1100                    (k, v, total_len)
1101                }
1102                None => return Err(anyhow::anyhow!("Cache should not be empty after append")),
1103            }
1104        } else {
1105            // No caching, use new K,V directly
1106            (k_expanded, v_expanded, new_seq_len)
1107        };
1108
1109        // 6. Scaled dot-product attention
1110        // Q: [batch, heads, new_seq, head_dim]
1111        // K: [batch, heads, total_seq, head_dim]
1112        // scores: [batch, heads, new_seq, total_seq]
1113        let k_t = full_k.transpose(2, 3)?;
1114        let q_contiguous = q_with_rope.contiguous()?;
1115        let k_contiguous = k_t.contiguous()?;
1116
1117        let scores = q_contiguous.matmul(&k_contiguous)?;
1118        let scaled_scores = (scores * (self.scale as f64))?;
1119
1120        // 7. Apply causal mask
1121        // For cached generation, new tokens at positions [offset, offset+new_seq)
1122        // can attend to all positions [0, offset+new_seq)
1123        let device = scaled_scores.device();
1124        let causal_mask = {
1125            let mut mask_data = vec![0.0f32; new_seq_len * total_seq_len];
1126            for i in 0..new_seq_len {
1127                let query_pos = position_offset + i;
1128                for j in 0..total_seq_len {
1129                    if j > query_pos {
1130                        // Future position - mask out
1131                        mask_data[i * total_seq_len + j] = f32::NEG_INFINITY;
1132                    }
1133                }
1134            }
1135            candle_core::Tensor::from_vec(mask_data, &[1, 1, new_seq_len, total_seq_len], device)?
1136        };
1137
1138        let masked_scores = scaled_scores.broadcast_add(&causal_mask)?;
1139        let attention_weights = candle_nn::ops::softmax_last_dim(&masked_scores)?;
1140
1141        // 8. Apply attention to values
1142        let v_contiguous = full_v.contiguous()?;
1143        let attn_output = attention_weights.matmul(&v_contiguous)?;
1144
1145        // 9. Reshape back: [batch, heads, new_seq, head_dim] -> [batch, new_seq, heads * head_dim]
1146        let attn_output = attn_output
1147            .transpose(1, 2)?
1148            .reshape(&[batch_size, new_seq_len, self.num_heads * self.head_dim])?;
1149
1150        let attn_output = Tensor::from_candle(attn_output);
1151
1152        // 10. Output projection (use quantized if available)
1153        let output = self.quantized_matmul(&attn_output, &self.o_proj, &self.o_proj_q)?;
1154
1155        Ok(output)
1156    }
1157
1158    /// Helper: use SIMD/QMatMul when available, fall back to F32 matmul
1159    /// Priority: SIMD > QMatMul > F32
1160    #[cfg(feature = "simd")]
1161    fn quantized_matmul_simd(
1162        &self,
1163        input: &Tensor,
1164        weight: &Tensor,
1165        quantized: &Option<Arc<QMatMul>>,
1166        simd_quantized: &Option<Arc<crate::simd::quant::QuantizedTensor>>,
1167    ) -> Result<Tensor> {
1168        // Try SIMD first (highest priority for performance)
1169        if let Some(ref simd_weight) = simd_quantized {
1170            return simd_matmul(input, simd_weight);
1171        }
1172        // Fall back to QMatMul
1173        if let Some(ref qmatmul) = quantized {
1174            let input_candle = input.to_candle()?;
1175            let output = qmatmul.forward(&input_candle)
1176                .map_err(|e| anyhow::anyhow!("QMatMul forward failed: {}", e))?;
1177            return Ok(Tensor::from_candle(output));
1178        }
1179        // Fall back to F32 matmul
1180        ops_fn::matmul(input, weight)
1181    }
1182
1183    /// Helper: use QMatMul when available, fall back to F32 matmul
1184    fn quantized_matmul(&self, input: &Tensor, weight: &Tensor, quantized: &Option<Arc<QMatMul>>) -> Result<Tensor> {
1185        if let Some(ref qmatmul) = quantized {
1186            // Use quantized matmul
1187            let input_candle = input.to_candle()?;
1188            let output = qmatmul.forward(&input_candle)
1189                .map_err(|e| anyhow::anyhow!("QMatMul forward failed: {}", e))?;
1190            Ok(Tensor::from_candle(output))
1191        } else {
1192            // Fall back to F32 matmul
1193            ops_fn::matmul(input, weight)
1194        }
1195    }
1196
1197    fn to_device(&mut self, device: &Device) -> Result<()> {
1198        self.q_proj = self.q_proj.to_device(device)?;
1199        self.k_proj = self.k_proj.to_device(device)?;
1200        self.v_proj = self.v_proj.to_device(device)?;
1201        self.o_proj = self.o_proj.to_device(device)?;
1202        Ok(())
1203    }
1204}
1205
1206/// SIMD-accelerated matrix multiplication for QuantizedTensor
1207#[cfg(feature = "simd")]
1208fn simd_matmul(
1209    input: &Tensor,
1210    weight: &crate::simd::quant::QuantizedTensor,
1211) -> Result<Tensor> {
1212    use crate::simd::get_simd_backend;
1213    use crate::simd::matmul::gemm::q4_gemm;
1214
1215    let shape = input.shape();
1216    let input_candle = input.to_candle()?;
1217    let input_flat = input_candle.flatten_all()?;
1218    let input_vec: Vec<f32> = input_flat.to_vec1()?;
1219
1220    let (n, k) = (weight.rows(), weight.cols());
1221
1222    // Determine M (batch size * sequence length)
1223    let m = if shape.len() >= 2 {
1224        shape[..shape.len()-1].iter().product()
1225    } else {
1226        1
1227    };
1228
1229    let mut output = vec![0.0f32; m * n];
1230
1231    if m == 1 {
1232        // GEMV for single token (decode phase)
1233        get_simd_backend().q4_gemv(weight, &input_vec, &mut output);
1234    } else {
1235        // GEMM for multiple tokens (prefill phase)
1236        q4_gemm(weight, &input_vec, &mut output, m, k, n);
1237    }
1238
1239    // Convert back to Tensor
1240    let device = input_candle.device();
1241    let output_candle = candle_core::Tensor::from_vec(output, &[m, n], device)?;
1242
1243    // Reshape to match expected output shape
1244    let mut out_shape = shape[..shape.len()-1].to_vec();
1245    out_shape.push(n);
1246    let output_reshaped = output_candle.reshape(out_shape.as_slice())?;
1247
1248    Ok(Tensor::from_candle(output_reshaped))
1249}
1250
1251impl LlamaMLP {
1252    fn new(config: &LlamaConfig, device: &Device) -> Result<Self> {
1253        let gate_proj = ops_fn::zeros(&[config.hidden_size, config.intermediate_size], DataType::Float32, device)?;
1254        let up_proj = ops_fn::zeros(&[config.hidden_size, config.intermediate_size], DataType::Float32, device)?;
1255        let down_proj = ops_fn::zeros(&[config.intermediate_size, config.hidden_size], DataType::Float32, device)?;
1256
1257        Ok(Self {
1258            gate_proj,
1259            up_proj,
1260            down_proj,
1261            gate_proj_q: None,
1262            up_proj_q: None,
1263            down_proj_q: None,
1264            #[cfg(feature = "simd")]
1265            gate_proj_simd: None,
1266            #[cfg(feature = "simd")]
1267            up_proj_simd: None,
1268            #[cfg(feature = "simd")]
1269            down_proj_simd: None,
1270            hidden_act: config.hidden_act.clone(),
1271        })
1272    }
1273
1274    fn forward(&self, hidden_states: &Tensor) -> Result<Tensor> {
1275        // 1. Gate and up projections (use quantized if available)
1276        let gate_output = self.quantized_matmul(hidden_states, &self.gate_proj, &self.gate_proj_q)?;
1277        let up_output = self.quantized_matmul(hidden_states, &self.up_proj, &self.up_proj_q)?;
1278
1279        // 2. Apply activation and gating (fused for SiLU, separate for others)
1280        let gated = match self.hidden_act.as_str() {
1281            "silu" | "swish" => {
1282                // Use fused SwiGLU: silu(gate) * up in one operation
1283                ops_fn::fused_swiglu(&gate_output, &up_output)?
1284            }
1285            "gelu" => {
1286                // GELU + element-wise multiply (not fused)
1287                let gate_activated = ops_fn::gelu(&gate_output)?;
1288                ops_fn::mul(&gate_activated, &up_output)?
1289            }
1290            _ => return Err(anyhow::anyhow!("Unsupported activation: {}", self.hidden_act)),
1291        };
1292
1293        // 3. Down projection (use quantized if available)
1294        let output = self.quantized_matmul(&gated, &self.down_proj, &self.down_proj_q)?;
1295
1296        Ok(output)
1297    }
1298
1299    /// Helper: use SIMD/QMatMul when available, fall back to F32 matmul
1300    /// Priority: SIMD > QMatMul > F32
1301    #[cfg(feature = "simd")]
1302    fn quantized_matmul_simd(
1303        &self,
1304        input: &Tensor,
1305        weight: &Tensor,
1306        quantized: &Option<Arc<QMatMul>>,
1307        simd_quantized: &Option<Arc<crate::simd::quant::QuantizedTensor>>,
1308    ) -> Result<Tensor> {
1309        // Try SIMD first (highest priority for performance)
1310        if let Some(ref simd_weight) = simd_quantized {
1311            return simd_matmul(input, simd_weight);
1312        }
1313        // Fall back to QMatMul
1314        if let Some(ref qmatmul) = quantized {
1315            let input_candle = input.to_candle()?;
1316            let output = qmatmul.forward(&input_candle)
1317                .map_err(|e| anyhow::anyhow!("QMatMul forward failed: {}", e))?;
1318            return Ok(Tensor::from_candle(output));
1319        }
1320        // Fall back to F32 matmul
1321        ops_fn::matmul(input, weight)
1322    }
1323
1324    /// Helper: use QMatMul when available, fall back to F32 matmul
1325    fn quantized_matmul(&self, input: &Tensor, weight: &Tensor, quantized: &Option<Arc<QMatMul>>) -> Result<Tensor> {
1326        if let Some(ref qmatmul) = quantized {
1327            // Use quantized matmul
1328            let input_candle = input.to_candle()?;
1329            let output = qmatmul.forward(&input_candle)
1330                .map_err(|e| anyhow::anyhow!("QMatMul forward failed: {}", e))?;
1331            Ok(Tensor::from_candle(output))
1332        } else {
1333            // Fall back to F32 matmul
1334            ops_fn::matmul(input, weight)
1335        }
1336    }
1337
1338    fn to_device(&mut self, device: &Device) -> Result<()> {
1339        self.gate_proj = self.gate_proj.to_device(device)?;
1340        self.up_proj = self.up_proj.to_device(device)?;
1341        self.down_proj = self.down_proj.to_device(device)?;
1342        Ok(())
1343    }
1344}
1345
1346// Helper functions are now available in ops_fn module
1347
1348#[cfg(test)]
1349mod tests {
1350    use super::*;
1351
1352    #[test]
1353    fn test_llama_model_creation() {
1354        let config = LlamaConfig {
1355            vocab_size: 1000,
1356            hidden_size: 128,
1357            intermediate_size: 512,
1358            num_hidden_layers: 2,
1359            num_attention_heads: 8,
1360            ..Default::default()
1361        };
1362
1363        let model = LlamaModelV2::new(config).unwrap();
1364        assert_eq!(model.config().vocab_size(), 1000);
1365        assert_eq!(model.config().hidden_size(), 128);
1366        assert_eq!(model.config().num_layers(), 2);
1367    }
1368
1369    #[test]
1370    fn test_llama_forward_pass() {
1371        let config = LlamaConfig {
1372            vocab_size: 100,
1373            hidden_size: 64,
1374            intermediate_size: 256,
1375            num_hidden_layers: 1,
1376            num_attention_heads: 4,
1377            num_key_value_heads: 4, // Must match num_attention_heads for standard attention
1378            ..Default::default()
1379        };
1380
1381        let model = LlamaModelV2::new(config).unwrap();
1382        let input_ids = ops_fn::zeros(&[2, 8], DataType::Int64, &Device::CPU).unwrap();
1383        let inputs = ModelInputs::text(input_ids);
1384
1385        let outputs = model.forward(&inputs).unwrap();
1386        match outputs {
1387            ModelOutputs::Logits { logits, .. } => {
1388                assert_eq!(logits.shape(), &[2, 8, 100]); // batch, seq, vocab
1389            }
1390            _ => panic!("Expected logits output"),
1391        }
1392    }
1393
1394    #[test]
1395    fn test_llama_generation() {
1396        // Use a small config for testing
1397        // vocab_size must be >= basic tokenizer's vocab (~200 tokens)
1398        let config = LlamaConfig {
1399            vocab_size: 256,
1400            hidden_size: 64,
1401            intermediate_size: 256,
1402            num_hidden_layers: 1,
1403            num_attention_heads: 4,
1404            num_key_value_heads: 4,
1405            ..Default::default()
1406        };
1407        let model = LlamaModelV2::new(config).unwrap();
1408        let gen_config = GenerationConfig {
1409            max_new_tokens: 5, // Generate only a few tokens for testing
1410            ..Default::default()
1411        };
1412
1413        let output = model.generate("Hello", &gen_config).unwrap();
1414        assert!(!output.is_empty());
1415    }
1416}