Skip to main content

candle_transformers/models/
quantized_qwen3.rs

1//! Qwen3 implementation with quantization support.
2//!
3//! Based on the Qwen3 architecture and implemented with quantized weights
4//! for reduced memory usage and faster inference on compatible hardware.
5//!
6//! References:
7//! - [Qwen3 Models](https://huggingface.co/Qwen/Qwen3-0.6B) (architecture based on official implementations)
8//!
9use super::with_tracing::QMatMul;
10use crate::{quantized_nn::RmsNorm, utils::repeat_kv};
11use candle::quantized::{gguf_file, QTensor};
12use candle::{DType, Device, Result, Storage, Tensor};
13use candle_nn::attention::cpu_flash::causal::causal_decode_f32_interleaved;
14use candle_nn::attention::{flash_attn, AttnMask};
15use candle_nn::kv_cache::{ConcatKvCache, InterleavedKvCache, RawInterleavedKvCache};
16use candle_nn::{Activation, Embedding, Module};
17use std::io::{Read, Seek};
18use std::sync::Arc;
19
20pub struct Gguf<R: Read + Seek> {
21    ct: gguf_file::Content,
22    reader: R,
23    device: Device,
24}
25
26impl<R: Read + Seek> Gguf<R> {
27    pub fn new(ct: gguf_file::Content, reader: R, device: Device) -> Self {
28        Self { ct, reader, device }
29    }
30
31    pub fn qmatmul(&mut self, name: &str) -> Result<QMatMul> {
32        let ws = self.ct.tensor(&mut self.reader, name, &self.device)?;
33        QMatMul::from_weights(ws.into())
34    }
35
36    pub fn rms_norm(&mut self, name: &str, eps: f64) -> Result<RmsNorm> {
37        let ws = self.ct.tensor(&mut self.reader, name, &self.device)?;
38        RmsNorm::from_qtensor(ws, eps)
39    }
40
41    pub fn metadata(&self) -> &std::collections::HashMap<String, gguf_file::Value> {
42        &self.ct.metadata
43    }
44
45    pub fn tensor(&mut self, name: &str) -> Result<QTensor> {
46        self.ct.tensor(&mut self.reader, name, &self.device)
47    }
48}
49
50#[derive(Debug, Clone)]
51struct MlpWeights {
52    gate_proj: QMatMul,
53    up_proj: QMatMul,
54    down_proj: QMatMul,
55    act_fn: Activation,
56    span: tracing::Span,
57}
58
59impl MlpWeights {
60    fn new<R: Read + Seek>(gg: &mut Gguf<R>, prefix: &str) -> Result<Self> {
61        let gate_proj = gg.qmatmul(&format!("{prefix}.ffn_gate.weight"))?;
62        let up_proj = gg.qmatmul(&format!("{prefix}.ffn_up.weight"))?;
63        let down_proj = gg.qmatmul(&format!("{prefix}.ffn_down.weight"))?;
64        let act_fn = Activation::Silu;
65        let span = tracing::span!(tracing::Level::TRACE, "mlp");
66        Ok(Self {
67            gate_proj,
68            up_proj,
69            down_proj,
70            act_fn,
71            span,
72        })
73    }
74}
75
76impl Module for MlpWeights {
77    fn forward(&self, x: &Tensor) -> Result<Tensor> {
78        let _enter = self.span.enter();
79        let gate = self.gate_proj.forward(x)?.apply(&self.act_fn)?;
80        let up = self.up_proj.forward(x)?;
81        let gated = (gate * up)?;
82        self.down_proj.forward(&gated)
83    }
84}
85
86#[derive(Debug, Clone)]
87pub struct RotaryEmbedding {
88    sin: Tensor,
89    cos: Tensor,
90    /// Pre-extracted flat f32 cos/sin for fused decode (zero allocation)
91    cos_f32: Vec<f32>,
92    sin_f32: Vec<f32>,
93    half_d: usize,
94}
95
96impl RotaryEmbedding {
97    pub fn new(
98        dtype: DType,
99        head_dim: usize,
100        max_position_embeddings: usize,
101        rope_theta: f64,
102        dev: &Device,
103    ) -> Result<Self> {
104        let dim = head_dim;
105        let max_seq_len = max_position_embeddings;
106        let inv_freq: Vec<_> = (0..dim)
107            .step_by(2)
108            .map(|i| 1f32 / rope_theta.powf(i as f64 / dim as f64) as f32)
109            .collect();
110        let inv_freq_len = inv_freq.len();
111        let inv_freq = Tensor::from_vec(inv_freq, (1, inv_freq_len), dev)?.to_dtype(dtype)?;
112        let t = Tensor::arange(0u32, max_seq_len as u32, dev)?
113            .to_dtype(dtype)?
114            .reshape((max_seq_len, 1))?;
115        let freqs = t.matmul(&inv_freq)?;
116        let sin_t = freqs.sin()?;
117        let cos_t = freqs.cos()?;
118        let cos_f32 = cos_t
119            .to_dtype(DType::F32)?
120            .flatten_all()?
121            .to_vec1::<f32>()?;
122        let sin_f32 = sin_t
123            .to_dtype(DType::F32)?
124            .flatten_all()?
125            .to_vec1::<f32>()?;
126        Ok(Self {
127            sin: sin_t,
128            cos: cos_t,
129            cos_f32,
130            sin_f32,
131            half_d: dim / 2,
132        })
133    }
134
135    /// Apply RoPE (q, k shape: B x H x L x D)
136    pub fn apply(&self, q: &Tensor, k: &Tensor, offset: usize) -> Result<(Tensor, Tensor)> {
137        let (_, _, seq_len, _) = q.dims4()?;
138        let cos = self.cos.narrow(0, offset, seq_len)?.to_dtype(q.dtype())?;
139        let sin = self.sin.narrow(0, offset, seq_len)?.to_dtype(q.dtype())?;
140        let q_embed = candle_nn::rotary_emb::rope(&q.contiguous()?, &cos, &sin)?;
141        let k_embed = candle_nn::rotary_emb::rope(&k.contiguous()?, &cos, &sin)?;
142        Ok((q_embed, k_embed))
143    }
144
145    /// Zero-allocation cos/sin slices for a single position.
146    #[inline]
147    pub fn cos_sin_at(&self, pos: usize) -> (&[f32], &[f32]) {
148        let start = pos * self.half_d;
149        let end = start + self.half_d;
150        (&self.cos_f32[start..end], &self.sin_f32[start..end])
151    }
152}
153
154#[derive(Debug, Clone)]
155struct AttentionWeights {
156    q_proj: QMatMul,
157    k_proj: QMatMul,
158    v_proj: QMatMul,
159    o_proj: QMatMul,
160    q_norm: RmsNorm,
161    k_norm: RmsNorm,
162    num_heads: usize,
163    num_kv_heads: usize,
164    num_kv_groups: usize,
165    head_dim: usize,
166    hidden_size: usize,
167    rotary_emb: Arc<RotaryEmbedding>,
168    kv_cache: Option<ConcatKvCache>,
169    interleaved_cache: Option<InterleavedKvCache>,
170    raw_cache: Option<RawInterleavedKvCache>,
171    span_attn: tracing::Span,
172}
173
174impl AttentionWeights {
175    #[allow(clippy::too_many_arguments)]
176    fn new<R: Read + Seek>(
177        gg: &mut Gguf<R>,
178        num_heads: usize,
179        num_kv_heads: usize,
180        head_dim: usize,
181        rms_norm_eps: f64,
182        rotary_emb: Arc<RotaryEmbedding>,
183        device: &Device,
184        prefix: &str,
185    ) -> Result<Self> {
186        let num_kv_groups = num_heads / num_kv_heads;
187        let hidden_size = num_heads * head_dim;
188
189        let q_proj = gg.qmatmul(&format!("{prefix}.attn_q.weight"))?;
190        let k_proj = gg.qmatmul(&format!("{prefix}.attn_k.weight"))?;
191        let v_proj = gg.qmatmul(&format!("{prefix}.attn_v.weight"))?;
192        let o_proj = gg.qmatmul(&format!("{prefix}.attn_output.weight"))?;
193
194        let q_norm = gg.rms_norm(&format!("{prefix}.attn_q_norm.weight"), rms_norm_eps)?;
195        let k_norm = gg.rms_norm(&format!("{prefix}.attn_k_norm.weight"), rms_norm_eps)?;
196
197        // CPU: use interleaved + raw caches for flash attention
198        // GPU: use standard concat KV cache (fallback path)
199        let on_cpu = device.is_cpu();
200        let kv_cache = if on_cpu {
201            None
202        } else {
203            Some(ConcatKvCache::new(2))
204        };
205        let interleaved_cache = if on_cpu {
206            Some(InterleavedKvCache::new(head_dim))
207        } else {
208            None
209        };
210        let raw_cache = if on_cpu {
211            Some(RawInterleavedKvCache::new(num_kv_heads, head_dim, 4096))
212        } else {
213            None
214        };
215
216        let span_attn = tracing::span!(tracing::Level::TRACE, "attn");
217
218        Ok(Self {
219            q_proj,
220            k_proj,
221            v_proj,
222            o_proj,
223            q_norm,
224            k_norm,
225            num_heads,
226            num_kv_heads,
227            num_kv_groups,
228            head_dim,
229            hidden_size,
230            rotary_emb,
231            kv_cache,
232            interleaved_cache,
233            raw_cache,
234            span_attn,
235        })
236    }
237
238    fn forward(&mut self, x: &Tensor, attn_mask: Option<&Tensor>, offset: usize) -> Result<Tensor> {
239        let _enter = self.span_attn.enter();
240        let (b, l, _) = x.dims3()?;
241
242        // QKV projections
243        let q = self.q_proj.forward(x)?;
244        let k = self.k_proj.forward(x)?;
245        let v = self.v_proj.forward(x)?;
246
247        let q = q
248            .reshape((b, l, self.num_heads, self.head_dim))?
249            .transpose(1, 2)?;
250        let k = k
251            .reshape((b, l, self.num_kv_heads, self.head_dim))?
252            .transpose(1, 2)?;
253        let v = v
254            .reshape((b, l, self.num_kv_heads, self.head_dim))?
255            .transpose(1, 2)?;
256
257        // Per-head Q/K norms (must stay as tensor ops)
258        let q_flat = q.flatten(0, 2)?;
259        let k_flat = k.flatten(0, 2)?;
260        let q_flat = self.q_norm.forward(&q_flat)?;
261        let k_flat = self.k_norm.forward(&k_flat)?;
262        let q = q_flat.reshape((b, self.num_heads, l, self.head_dim))?;
263        let k = k_flat.reshape((b, self.num_kv_heads, l, self.head_dim))?;
264
265        // RoPE
266        let (q, k) = self.rotary_emb.apply(&q, &k, offset)?;
267
268        // TODO: b > 1 needs varlen CPU flash with interleaved cache support.
269        if x.device().is_cpu() && b == 1 {
270            let scale = 1.0 / (self.head_dim as f32).sqrt();
271
272            if l == 1 && b == 1 && q.dtype() == DType::F32 {
273                // Fused decode: raw slices -> raw cache -> kernel.
274                let q_cont = q.squeeze(0)?.squeeze(1)?.contiguous()?;
275                let (q_g, q_l) = q_cont.storage_and_layout();
276                let q_data: &[f32] = match &*q_g {
277                    Storage::Cpu(cpu) => &cpu.as_slice::<f32>()?[q_l.start_offset()..],
278                    _ => candle::bail!("Expected CPU storage"),
279                };
280
281                let k_cont = k.squeeze(0)?.squeeze(1)?.contiguous()?;
282                let (k_g, k_l) = k_cont.storage_and_layout();
283                let k_data: &[f32] = match &*k_g {
284                    Storage::Cpu(cpu) => &cpu.as_slice::<f32>()?[k_l.start_offset()..],
285                    _ => candle::bail!("Expected CPU storage"),
286                };
287
288                let v_cont = v.squeeze(0)?.squeeze(1)?.contiguous()?;
289                let (v_g, v_l) = v_cont.storage_and_layout();
290                let v_data: &[f32] = match &*v_g {
291                    Storage::Cpu(cpu) => &cpu.as_slice::<f32>()?[v_l.start_offset()..],
292                    _ => candle::bail!("Expected CPU storage"),
293                };
294
295                // Write K, V into raw cache (no tensor allocation)
296                let k_len = self.num_kv_heads * self.head_dim;
297                let rc = self.raw_cache.as_mut().unwrap();
298                rc.write_kv(&k_data[..k_len], &v_data[..k_len]);
299
300                // Run interleaved decode kernel
301                let kv_len = rc.len();
302                let q_len = self.num_heads * self.head_dim;
303                let ctx = causal_decode_f32_interleaved(
304                    &q_data[..q_len],
305                    rc.data(),
306                    self.num_heads,
307                    self.num_kv_heads,
308                    self.head_dim,
309                    kv_len,
310                    scale,
311                )?;
312
313                ctx.reshape((b, l, self.hidden_size))?.apply(&self.o_proj)
314            } else {
315                // Prefill: interleaved cache + flash_attn; also populate raw cache for decode.
316                let ic = self.interleaved_cache.as_mut().unwrap();
317                let kv = ic.append(&k, &v)?;
318
319                // Populate raw cache for subsequent decode steps
320                {
321                    let k_cont = k.squeeze(0)?.transpose(0, 1)?.contiguous()?;
322                    let v_cont = v.squeeze(0)?.transpose(0, 1)?.contiguous()?;
323                    let (kg, kl) = k_cont.storage_and_layout();
324                    let k_d: &[f32] = match &*kg {
325                        Storage::Cpu(cpu) => &cpu.as_slice::<f32>()?[kl.start_offset()..],
326                        _ => candle::bail!("Expected CPU"),
327                    };
328                    let (vg, vl) = v_cont.storage_and_layout();
329                    let v_d: &[f32] = match &*vg {
330                        Storage::Cpu(cpu) => &cpu.as_slice::<f32>()?[vl.start_offset()..],
331                        _ => candle::bail!("Expected CPU"),
332                    };
333                    self.raw_cache.as_mut().unwrap().write_kv_batch(k_d, v_d, l);
334                }
335
336                let kv_k = kv.narrow(2, 0, self.head_dim)?.unsqueeze(0)?;
337                let kv_v = kv.narrow(2, self.head_dim, self.head_dim)?.unsqueeze(0)?;
338
339                let q = q.transpose(1, 2)?.contiguous()?;
340                let k = kv_k.contiguous()?;
341                let v = kv_v.contiguous()?;
342
343                let ctx = flash_attn::<f32>(
344                    &q,
345                    &k,
346                    &v,
347                    scale,
348                    AttnMask::causal_with_offset(offset),
349                    None,
350                    None,
351                )?;
352                let ctx = ctx.transpose(1, 2)?;
353                ctx.reshape((b, l, self.hidden_size))?.apply(&self.o_proj)
354            }
355        } else {
356            // Standard matmul attention (no flash)
357            let (k, v) = self.kv_cache.as_mut().unwrap().append(&k, &v)?;
358
359            let k = repeat_kv(k, self.num_kv_groups)?.contiguous()?;
360            let v = repeat_kv(v, self.num_kv_groups)?.contiguous()?;
361
362            let scale = 1.0 / (self.head_dim as f64).sqrt();
363            let mut scores = (q.matmul(&k.transpose(2, 3)?)? * scale)?;
364            if let Some(m) = attn_mask {
365                let scores_dtype = scores.dtype();
366                let mask = if m.dtype() != scores_dtype {
367                    m.to_dtype(scores_dtype)?
368                } else {
369                    m.clone()
370                };
371                scores = scores.broadcast_add(&mask)?;
372            }
373            let probs = candle_nn::ops::softmax_last_dim(&scores)?;
374            let ctx = probs.matmul(&v)?;
375            let reshaped_ctx = ctx.transpose(1, 2)?.reshape((b, l, self.hidden_size))?;
376            self.o_proj.forward(&reshaped_ctx)
377        }
378    }
379
380    fn clear_kv_cache(&mut self) {
381        if let Some(c) = &mut self.kv_cache {
382            c.reset();
383        }
384        if let Some(c) = &mut self.interleaved_cache {
385            c.reset();
386        }
387        if let Some(c) = &mut self.raw_cache {
388            c.reset();
389        }
390    }
391}
392
393#[derive(Debug, Clone)]
394struct LayerWeights {
395    self_attn: AttentionWeights,
396    mlp: MlpWeights,
397    ln1: RmsNorm,
398    ln2: RmsNorm,
399}
400
401impl LayerWeights {
402    #[allow(clippy::too_many_arguments)]
403    fn new<R: Read + Seek>(
404        gg: &mut Gguf<R>,
405        num_attention_heads: usize,
406        num_key_value_heads: usize,
407        head_dim: usize,
408        rms_norm_eps: f64,
409        rotary: Arc<RotaryEmbedding>,
410        device: &Device,
411        layer_idx: usize,
412    ) -> Result<Self> {
413        let prefix = format!("blk.{layer_idx}");
414
415        let ln1 = gg.rms_norm(&format!("{prefix}.attn_norm.weight"), rms_norm_eps)?;
416        let ln2 = gg.rms_norm(&format!("{prefix}.ffn_norm.weight"), rms_norm_eps)?;
417        let self_attn = AttentionWeights::new(
418            gg,
419            num_attention_heads,
420            num_key_value_heads,
421            head_dim,
422            rms_norm_eps,
423            rotary,
424            device,
425            &prefix,
426        )?;
427        let mlp = MlpWeights::new(gg, &prefix)?;
428        Ok(Self {
429            self_attn,
430            mlp,
431            ln1,
432            ln2,
433        })
434    }
435
436    fn forward(&mut self, x: &Tensor, mask: Option<&Tensor>, offset: usize) -> Result<Tensor> {
437        let h = self.ln1.forward(x)?;
438        let h = self.self_attn.forward(&h, mask, offset)?;
439        let x = (x + h)?;
440        let h2 = self.ln2.forward(&x)?;
441        let h2 = h2.apply(&self.mlp)?;
442        x + h2
443    }
444
445    fn clear_kv_cache(&mut self) {
446        self.self_attn.clear_kv_cache();
447    }
448}
449
450#[derive(Debug, Clone)]
451pub struct ModelWeights {
452    embed_tokens: Embedding,
453    layers: Vec<LayerWeights>,
454    norm: RmsNorm,
455    lm_head: QMatMul,
456    device: Device,
457    dtype: DType,
458    span: tracing::Span,
459    span_output: tracing::Span,
460}
461
462impl ModelWeights {
463    pub fn from_gguf<R: Read + Seek>(
464        ct: gguf_file::Content,
465        reader: &mut R,
466        device: &Device,
467    ) -> Result<Self> {
468        let mut gg = Gguf::new(ct, reader, device.clone());
469        let md_get = |s: &str| match gg.metadata().get(s) {
470            None => candle::bail!("cannot find {s} in metadata"),
471            Some(v) => Ok(v),
472        };
473
474        let num_attention_heads = md_get("qwen3.attention.head_count")?.to_u32()? as usize;
475        let num_kv_heads = md_get("qwen3.attention.head_count_kv")?.to_u32()? as usize;
476        let head_dim = md_get("qwen3.attention.key_length")?.to_u32()? as usize;
477        let num_layers = md_get("qwen3.block_count")?.to_u32()? as usize;
478        let hidden_size = md_get("qwen3.embedding_length")?.to_u32()? as usize;
479        let max_position_embeddings = md_get("qwen3.context_length")?.to_u32()? as usize;
480        let rms_norm_eps = md_get("qwen3.attention.layer_norm_rms_epsilon")?.to_f32()? as f64;
481        let rope_freq_base = md_get("qwen3.rope.freq_base")?.to_f32()? as f64;
482
483        let dtype = match gg.metadata().get("general.dtype") {
484            Some(v) => match v.to_u32() {
485                Ok(0) => DType::F32,
486                Ok(1) => DType::F16,
487                _ => DType::F16,
488            },
489            None => DType::F16,
490        };
491
492        let embed_tensor = gg.tensor("token_embd.weight")?;
493        let embed_tokens = Embedding::new(embed_tensor.dequantize(device)?, hidden_size);
494
495        let rotary = Arc::new(RotaryEmbedding::new(
496            dtype,
497            head_dim,
498            max_position_embeddings,
499            rope_freq_base,
500            device,
501        )?);
502
503        let mut layers = Vec::with_capacity(num_layers);
504        for i in 0..num_layers {
505            layers.push(LayerWeights::new(
506                &mut gg,
507                num_attention_heads,
508                num_kv_heads,
509                head_dim,
510                rms_norm_eps,
511                rotary.clone(),
512                device,
513                i,
514            )?);
515        }
516
517        let norm = gg.rms_norm("output_norm.weight", rms_norm_eps)?;
518        // Load output projection tensor, falling back to tied embeddings like gemma3
519        let lm_head_tensor = match gg.tensor("output.weight") {
520            Ok(tensor) => tensor,
521            Err(_) => gg.tensor("token_embd.weight")?,
522        };
523        let lm_head = QMatMul::from_weights(lm_head_tensor.into())?;
524        let span = tracing::span!(tracing::Level::TRACE, "model");
525        let span_output = tracing::span!(tracing::Level::TRACE, "output");
526        Ok(Self {
527            embed_tokens,
528            layers,
529            norm,
530            lm_head,
531            device: device.clone(),
532            dtype,
533            span,
534            span_output,
535        })
536    }
537
538    fn causal_mask(
539        &self,
540        b: usize,
541        tgt: usize,
542        offset: usize,
543        sw: Option<usize>,
544    ) -> Result<Tensor> {
545        let minf = f32::NEG_INFINITY;
546        let mask: Vec<_> = (0..tgt)
547            .flat_map(|i| {
548                (0..(tgt + offset)).map(move |j| {
549                    let past_ok = j <= i + offset;
550                    let sw_ok = match sw {
551                        Some(w) => (i + offset) as i64 - j as i64 <= w as i64,
552                        None => true,
553                    };
554                    if past_ok && sw_ok {
555                        0.
556                    } else {
557                        minf
558                    }
559                })
560            })
561            .collect();
562        Tensor::from_slice(&mask, (b, 1, tgt, tgt + offset), &self.device)?.to_dtype(self.dtype)
563    }
564
565    pub fn forward(&mut self, input: &Tensor, offset: usize) -> Result<Tensor> {
566        let _enter = self.span.enter();
567        let (b, l) = input.dims2()?;
568        let mut h = self.embed_tokens.forward(input)?;
569        // Skip mask materialization when using CPU flash attention
570        let causal_mask = if l == 1 || self.device.is_cpu() {
571            None
572        } else {
573            Some(self.causal_mask(b, l, offset, None)?)
574        };
575        for layer in &mut self.layers {
576            h = layer.forward(&h, causal_mask.as_ref(), offset)?;
577        }
578        let h = self.norm.forward(&h)?;
579        let _enter = self.span_output.enter();
580        let last_hidden = h.narrow(1, l - 1, 1)?;
581        self.lm_head.forward(&last_hidden)?.squeeze(1)
582    }
583
584    pub fn clear_kv_cache(&mut self) {
585        for layer in &mut self.layers {
586            layer.clear_kv_cache();
587        }
588    }
589}