Skip to main content

candle_transformers/models/
quantized_gemma3.rs

1//! Gemma 3 model implementation with quantization support.
2//!
3//! Gemma 3 is a family of multimodal language models developed by Google.
4//! This implementation provides quantization for reduced memory usage and faster inference.
5//!
6//! Key characteristics:
7//! - Group-Query Attention (GQA) with specialized key-value heads
8//! - RMSNorm for layer normalization
9//! - Specialized attention patterns with separate normalization for Q/K/V
10//! - Feed-forward network with SwiGLU activation
11//! - Support for 2/3/4/8-bit quantization
12//!
13//! References:
14//! - [Gemma 3 Models](https://blog.google/technology/developers/gemma-3/)
15//!
16
17use crate::quantized_nn::RmsNorm;
18use candle::quantized::gguf_file;
19use candle::quantized::QTensor;
20use candle::D;
21use candle::{DType, Device, IndexOp, Result, Tensor};
22use candle_nn::{Embedding, Module};
23
24pub const MAX_SEQ_LEN: usize = 131072; // Gemma 3 supports 128K context window
25pub const DEFAULT_SLIDING_WINDOW_TYPE: usize = 6;
26pub const DEFAULT_ROPE_FREQUENCY: f32 = 1_000_000.;
27pub const DEFAULT_ROPE_FREQUENCY_SLIDING: f32 = 10_000.;
28pub const DEFAULT_ROPE_FREQUENCY_SCALE_FACTOR: f32 = 1.;
29
30#[derive(Debug, Clone)]
31struct QMatMul {
32    inner: candle::quantized::QMatMul,
33    span: tracing::Span,
34}
35
36impl QMatMul {
37    fn from_qtensor(qtensor: QTensor) -> Result<Self> {
38        let inner = candle::quantized::QMatMul::from_qtensor(qtensor)?;
39        let span = tracing::span!(tracing::Level::TRACE, "qmatmul");
40        Ok(Self { inner, span })
41    }
42
43    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
44        let _enter = self.span.enter();
45        self.inner.forward(xs)
46    }
47}
48
49#[derive(Debug, Clone)]
50struct Mlp {
51    feed_forward_gate: QMatMul, // ffn_gate in GGUF
52    feed_forward_up: QMatMul,   // ffn_up in GGUF
53    feed_forward_down: QMatMul, // ffn_down in GGUF
54}
55
56impl Module for Mlp {
57    fn forward(&self, xs: &Tensor) -> Result<Tensor> {
58        let gate = self.feed_forward_gate.forward(xs)?;
59        let up = self.feed_forward_up.forward(xs)?;
60        let silu = candle_nn::ops::silu(&gate)?;
61        let gated = (silu * up)?;
62        self.feed_forward_down.forward(&gated)
63    }
64}
65
66#[derive(Debug, Clone)]
67struct RotaryEmbedding {
68    sin: Tensor,
69    cos: Tensor,
70}
71
72impl RotaryEmbedding {
73    fn new(head_dim: usize, rope_frequency: f32, device: &Device) -> Result<Self> {
74        let theta: Vec<_> = (0..head_dim)
75            .step_by(2)
76            .map(|i| 1f32 / rope_frequency.powf(i as f32 / head_dim as f32))
77            .collect();
78        let theta = Tensor::new(theta.as_slice(), device)?;
79        let idx_theta = Tensor::arange(0, MAX_SEQ_LEN as u32, device)?
80            .to_dtype(DType::F32)?
81            .reshape((MAX_SEQ_LEN, 1))?
82            .matmul(&theta.reshape((1, theta.elem_count()))?)?;
83        let cos = idx_theta.cos()?;
84        let sin = idx_theta.sin()?;
85        Ok(Self { sin, cos })
86    }
87
88    fn apply_rotary_emb_qkv(
89        &self,
90        q: &Tensor,
91        k: &Tensor,
92        index_pos: usize,
93    ) -> Result<(Tensor, Tensor)> {
94        let (_b_sz, _h, seq_len, _n_embd) = q.dims4()?;
95        let cos = self.cos.narrow(0, index_pos, seq_len)?;
96        let sin = self.sin.narrow(0, index_pos, seq_len)?;
97        let q_embed = candle_nn::rotary_emb::rope(&q.contiguous()?, &cos, &sin)?;
98        let k_embed = candle_nn::rotary_emb::rope(&k.contiguous()?, &cos, &sin)?;
99        Ok((q_embed, k_embed))
100    }
101}
102
103#[derive(Debug, Clone)]
104struct LayerWeights {
105    // Attention components
106    attention_wq: QMatMul,
107    attention_wk: QMatMul,
108    attention_wv: QMatMul,
109    attention_wo: QMatMul,
110
111    // Specialized normalization for Q and K
112    attention_q_norm: RmsNorm,
113    attention_k_norm: RmsNorm,
114
115    // Layer normalization
116    attention_norm: RmsNorm,      // Applied before attention
117    post_attention_norm: RmsNorm, // Applied after attention
118    ffn_norm: RmsNorm,            // Applied before feedforward
119    post_ffn_norm: RmsNorm,       // Applied after feedforward
120
121    // Feed-forward network
122    mlp: Mlp,
123
124    // Attention parameters
125    n_head: usize,    // Number of query heads
126    n_kv_head: usize, // Number of key-value heads
127    head_dim: usize,  // Dimension of each head
128    q_dim: usize,     // Total dimension for queries
129
130    sliding_window_size: Option<usize>,
131
132    rotary_embedding: RotaryEmbedding,
133    neg_inf: Tensor,
134
135    // Cache
136    kv_cache: Option<(Tensor, Tensor)>,
137
138    // Tracing
139    span_attn: tracing::Span,
140    span_mlp: tracing::Span,
141}
142
143impl LayerWeights {
144    fn mask(
145        &self,
146        b_sz: usize,
147        seq_len: usize,
148        index_pos: usize,
149        dtype: DType,
150        device: &Device,
151    ) -> Result<Tensor> {
152        let mask: Vec<_> = if let Some(sliding_window_size) = self.sliding_window_size {
153            (0..seq_len)
154                .flat_map(|i| {
155                    (0..seq_len).map(move |j| {
156                        if i < j || j + sliding_window_size < i {
157                            0u32
158                        } else {
159                            1u32
160                        }
161                    })
162                })
163                .collect()
164        } else {
165            (0..seq_len)
166                .flat_map(|i| (0..seq_len).map(move |j| if i < j { 0u32 } else { 1u32 }))
167                .collect()
168        };
169        let mask = Tensor::from_slice(&mask, (seq_len, seq_len), device)?;
170        let mask = if index_pos > 0 {
171            let mask0 = Tensor::zeros((seq_len, index_pos), DType::F32, device)?;
172            Tensor::cat(&[&mask0, &mask], D::Minus1)?
173        } else {
174            mask
175        };
176        mask.expand((b_sz, 1, seq_len, seq_len + index_pos))?
177            .to_dtype(dtype)
178    }
179
180    fn forward_attn(
181        &mut self,
182        x: &Tensor,
183        mask: Option<&Tensor>,
184        index_pos: usize,
185    ) -> Result<Tensor> {
186        let _enter = self.span_attn.enter();
187        let (b_sz, seq_len, _) = x.dims3()?;
188
189        let q = self.attention_wq.forward(x)?;
190        let k = self.attention_wk.forward(x)?;
191        let v = self.attention_wv.forward(x)?;
192
193        let q = q
194            .reshape((b_sz, seq_len, self.n_head, self.head_dim))?
195            .transpose(1, 2)?;
196        let k = k
197            .reshape((b_sz, seq_len, self.n_kv_head, self.head_dim))?
198            .transpose(1, 2)?;
199        let v = v
200            .reshape((b_sz, seq_len, self.n_kv_head, self.head_dim))?
201            .transpose(1, 2)?;
202
203        let q = self.attention_q_norm.forward(&q.contiguous()?)?;
204        let k = self.attention_k_norm.forward(&k.contiguous()?)?;
205
206        let (q, k) = self
207            .rotary_embedding
208            .apply_rotary_emb_qkv(&q, &k, index_pos)?;
209
210        let (k, v) = match &self.kv_cache {
211            None => (k, v),
212            Some((k_cache, v_cache)) => {
213                if index_pos == 0 {
214                    (k, v)
215                } else {
216                    let k = Tensor::cat(&[k_cache, &k], 2)?; // concat on seq dim
217                    let v = Tensor::cat(&[v_cache, &v], 2)?;
218                    (k, v)
219                }
220            }
221        };
222        self.kv_cache = Some((k.clone(), v.clone())); // update cache
223
224        // Repeat KV for GQA
225        let k = crate::utils::repeat_kv(k, self.n_head / self.n_kv_head)?;
226        let v = crate::utils::repeat_kv(v, self.n_head / self.n_kv_head)?;
227
228        // Scaled Dot-Product Attention
229        let scale = 1.0 / (self.head_dim as f64).sqrt();
230        let mut attn_weights = (q.matmul(&k.transpose(2, 3)?)? * scale)?;
231
232        if let Some(mask) = mask {
233            let mask = mask.broadcast_as(attn_weights.shape())?;
234            let neg_inf = self.neg_inf.broadcast_as(attn_weights.dims())?;
235            attn_weights = mask.eq(0u32)?.where_cond(&neg_inf, &attn_weights)?;
236        }
237
238        let attn_weights = candle_nn::ops::softmax_last_dim(&attn_weights)?;
239        let attn_output = attn_weights.matmul(&v)?;
240
241        let attn_output = attn_output
242            .transpose(1, 2)?
243            .reshape((b_sz, seq_len, self.q_dim))?;
244
245        self.attention_wo.forward(&attn_output)
246    }
247}
248
249#[derive(Debug, Clone)]
250pub struct ModelWeights {
251    tok_embeddings: Embedding,
252    embedding_length: usize,
253    layers: Vec<LayerWeights>,
254    norm: RmsNorm,
255    output: QMatMul,
256    span: tracing::Span,
257    span_output: tracing::Span,
258}
259
260impl ModelWeights {
261    pub fn from_gguf<R: std::io::Seek + std::io::Read>(
262        ct: gguf_file::Content,
263        reader: &mut R,
264        device: &Device,
265    ) -> Result<Self> {
266        // Detect architecture prefix by probing which keys exist in metadata.
267        // This supports gemma3, gemma2, gemma, gemma-embedding, and future variants.
268        let prefix = ["gemma3", "gemma2", "gemma", "gemma-embedding"]
269            .iter()
270            .find(|p| {
271                ct.metadata
272                    .contains_key(&format!("{}.attention.head_count", p))
273            })
274            .copied()
275            .unwrap_or("gemma3");
276
277        let md_get = |s: &str| {
278            let key = format!("{prefix}.{s}");
279            match ct.metadata.get(&key) {
280                None => candle::bail!("cannot find {key} in metadata"),
281                Some(v) => Ok(v),
282            }
283        };
284
285        let head_count = md_get("attention.head_count")?.to_u32()? as usize;
286        let head_count_kv = md_get("attention.head_count_kv")?.to_u32()? as usize;
287        let block_count = md_get("block_count")?.to_u32()? as usize;
288        let embedding_length = md_get("embedding_length")?.to_u32()? as usize;
289        let key_length = md_get("attention.key_length")?.to_u32()? as usize;
290        let _value_length = md_get("attention.value_length")?.to_u32()? as usize;
291        let rms_norm_eps = md_get("attention.layer_norm_rms_epsilon")?.to_f32()? as f64;
292        let sliding_window_size = md_get("attention.sliding_window")?.to_u32()? as usize;
293
294        let sliding_window_type = md_get("attention.sliding_window_type")
295            .and_then(|m| Ok(m.to_u32()? as usize))
296            .unwrap_or(DEFAULT_SLIDING_WINDOW_TYPE);
297
298        let rope_freq_base = md_get("rope.freq_base")
299            .and_then(|m| m.to_f32())
300            .unwrap_or(DEFAULT_ROPE_FREQUENCY);
301
302        let rope_freq_base_sliding = md_get("rope.local_freq_base")
303            .and_then(|m| m.to_f32())
304            .unwrap_or(DEFAULT_ROPE_FREQUENCY_SLIDING);
305
306        // Unused in Llama.cpp so we aren't using it here.
307        let _rope_freq_scaling_factor = md_get("rope.scaling.factor")
308            .and_then(|m| m.to_f32())
309            .unwrap_or(DEFAULT_ROPE_FREQUENCY_SCALE_FACTOR);
310
311        // Compute the dimensions for queries, keys, and values
312        // These are the total dimensions when projected across all heads
313        let q_dim = head_count * key_length;
314
315        let neg_inf = Tensor::new(f32::NEG_INFINITY, device)?;
316
317        // Load token embeddings and output projection
318        let tok_embeddings = ct.tensor(reader, "token_embd.weight", device)?;
319        let tok_embeddings = tok_embeddings.dequantize(device)?;
320        let norm = RmsNorm::from_qtensor(
321            ct.tensor(reader, "output_norm.weight", device)?,
322            rms_norm_eps,
323        )?;
324        let output = match ct.tensor(reader, "output.weight", device) {
325            Ok(tensor) => tensor,
326            Err(_) => ct.tensor(reader, "token_embd.weight", device)?, // Use tied weights if output.weight doesn't exist
327        };
328
329        let mut layers = Vec::with_capacity(block_count);
330        for layer_idx in 0..block_count {
331            let prefix = format!("blk.{layer_idx}");
332
333            let attention_wq = ct.tensor(reader, &format!("{prefix}.attn_q.weight"), device)?;
334            let attention_wk = ct.tensor(reader, &format!("{prefix}.attn_k.weight"), device)?;
335            let attention_wv = ct.tensor(reader, &format!("{prefix}.attn_v.weight"), device)?;
336            let attention_wo =
337                ct.tensor(reader, &format!("{prefix}.attn_output.weight"), device)?;
338
339            let attention_q_norm = RmsNorm::from_qtensor(
340                ct.tensor(reader, &format!("{prefix}.attn_q_norm.weight"), device)?,
341                rms_norm_eps,
342            )?;
343
344            let attention_k_norm = RmsNorm::from_qtensor(
345                ct.tensor(reader, &format!("{prefix}.attn_k_norm.weight"), device)?,
346                rms_norm_eps,
347            )?;
348
349            let attention_norm = RmsNorm::from_qtensor(
350                ct.tensor(reader, &format!("{prefix}.attn_norm.weight"), device)?,
351                rms_norm_eps,
352            )?;
353
354            let post_attention_norm = RmsNorm::from_qtensor(
355                ct.tensor(
356                    reader,
357                    &format!("{prefix}.post_attention_norm.weight"),
358                    device,
359                )?,
360                rms_norm_eps,
361            )?;
362
363            let ffn_norm = RmsNorm::from_qtensor(
364                ct.tensor(reader, &format!("{prefix}.ffn_norm.weight"), device)?,
365                rms_norm_eps,
366            )?;
367
368            let post_ffn_norm = RmsNorm::from_qtensor(
369                ct.tensor(reader, &format!("{prefix}.post_ffw_norm.weight"), device)?,
370                rms_norm_eps,
371            )?;
372
373            let feed_forward_gate =
374                ct.tensor(reader, &format!("{prefix}.ffn_gate.weight"), device)?;
375            let feed_forward_up = ct.tensor(reader, &format!("{prefix}.ffn_up.weight"), device)?;
376            let feed_forward_down =
377                ct.tensor(reader, &format!("{prefix}.ffn_down.weight"), device)?;
378
379            let mlp = Mlp {
380                feed_forward_gate: QMatMul::from_qtensor(feed_forward_gate)?,
381                feed_forward_up: QMatMul::from_qtensor(feed_forward_up)?,
382                feed_forward_down: QMatMul::from_qtensor(feed_forward_down)?,
383            };
384
385            // Sliding window pattern hardcoded to 6 because it's not explicitly defined
386            let is_sliding = (layer_idx + 1) % sliding_window_type > 0;
387            let sliding_window_size = is_sliding.then_some(sliding_window_size);
388            let layer_rope_frequency = if is_sliding {
389                rope_freq_base_sliding
390            } else {
391                rope_freq_base
392            };
393
394            let rotary_embedding = RotaryEmbedding::new(key_length, layer_rope_frequency, device)?;
395
396            // Tracing spans
397            let span_attn = tracing::span!(tracing::Level::TRACE, "attn");
398            let span_mlp = tracing::span!(tracing::Level::TRACE, "attn-mlp");
399
400            layers.push(LayerWeights {
401                attention_wq: QMatMul::from_qtensor(attention_wq)?,
402                attention_wk: QMatMul::from_qtensor(attention_wk)?,
403                attention_wv: QMatMul::from_qtensor(attention_wv)?,
404                attention_wo: QMatMul::from_qtensor(attention_wo)?,
405                attention_q_norm,
406                attention_k_norm,
407                attention_norm,
408                post_attention_norm,
409                ffn_norm,
410                post_ffn_norm,
411                mlp,
412                n_head: head_count,
413                n_kv_head: head_count_kv,
414                head_dim: key_length,
415                q_dim,
416                sliding_window_size,
417                rotary_embedding,
418                neg_inf: neg_inf.clone(),
419                kv_cache: None,
420                span_attn,
421                span_mlp,
422            })
423        }
424
425        let span = tracing::span!(tracing::Level::TRACE, "model");
426        let span_output = tracing::span!(tracing::Level::TRACE, "output");
427
428        Ok(Self {
429            tok_embeddings: Embedding::new(tok_embeddings, embedding_length),
430            embedding_length,
431            layers,
432            norm,
433            output: QMatMul::from_qtensor(output)?,
434            span,
435            span_output,
436        })
437    }
438
439    pub fn forward(&mut self, x: &Tensor, index_pos: usize) -> Result<Tensor> {
440        let (b_sz, seq_len) = x.dims2()?;
441        let _enter = self.span.enter();
442
443        let mut layer_in = self.tok_embeddings.forward(x)?;
444        layer_in = (layer_in * (self.embedding_length as f64).sqrt())?;
445
446        for layer in self.layers.iter_mut() {
447            let attention_mask = if seq_len == 1 {
448                None
449            } else {
450                Some(layer.mask(b_sz, seq_len, index_pos, x.dtype(), x.device())?)
451            };
452
453            // Attention block
454            let residual = &layer_in;
455            let x = layer.attention_norm.forward(&layer_in)?;
456            let x = layer.forward_attn(&x, attention_mask.as_ref(), index_pos)?;
457            let x = layer.post_attention_norm.forward(&x)?;
458            let x = (x + residual)?;
459
460            // Feed-forward block
461            let _enter = layer.span_mlp.enter();
462            let residual = &x;
463            let x = layer.ffn_norm.forward(&x)?;
464            let x = layer.mlp.forward(&x)?;
465            let x = layer.post_ffn_norm.forward(&x)?;
466            let x = (x + residual)?;
467            drop(_enter);
468
469            layer_in = x;
470        }
471
472        let _enter = self.span_output.enter();
473
474        let x = layer_in.i((.., seq_len - 1, ..))?;
475        let x = self.norm.forward(&x)?;
476        let output = self.output.forward(&x)?;
477
478        Ok(output)
479    }
480}