Skip to main content

candle_transformers/models/
qwen3.rs

1use crate::{
2    models::with_tracing::{linear_b, linear_no_bias, Linear, RmsNorm},
3    utils::repeat_kv,
4};
5use candle::{DType, Device, Module, Result, Tensor};
6use candle_nn::{kv_cache::ConcatKvCache, Activation, VarBuilder};
7use std::sync::Arc;
8
9#[cfg(feature = "flash-attn")]
10use candle_flash_attn;
11
12#[cfg(not(feature = "flash-attn"))]
13use candle_nn::attention::{flash_attn, AttnMask};
14
15#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
16pub struct Config {
17    pub vocab_size: usize,
18    pub hidden_size: usize,
19    pub intermediate_size: usize,
20    pub num_hidden_layers: usize,
21    pub num_attention_heads: usize,
22    pub head_dim: usize,
23    pub attention_bias: bool,
24    pub num_key_value_heads: usize,
25    pub max_position_embeddings: usize,
26    pub sliding_window: Option<usize>,
27    pub max_window_layers: usize,
28    pub tie_word_embeddings: bool,
29    pub rope_theta: f64,
30    pub rms_norm_eps: f64,
31    pub use_sliding_window: bool,
32    pub hidden_act: Activation,
33}
34
35#[derive(Debug, Clone)]
36pub(crate) struct Qwen3RotaryEmbedding {
37    sin: Tensor,
38    cos: Tensor,
39}
40
41impl Qwen3RotaryEmbedding {
42    pub(crate) fn new(dtype: DType, cfg: &Config, dev: &Device) -> Result<Self> {
43        let dim = cfg.head_dim;
44        let max_seq_len = cfg.max_position_embeddings;
45        let inv_freq: Vec<_> = (0..dim)
46            .step_by(2)
47            .map(|i| 1f32 / cfg.rope_theta.powf(i as f64 / dim as f64) as f32)
48            .collect();
49        let inv_freq_len = inv_freq.len();
50        let inv_freq = Tensor::from_vec(inv_freq, (1, inv_freq_len), dev)?.to_dtype(DType::F32)?;
51        let t = Tensor::arange(0u32, max_seq_len as u32, dev)?
52            .to_dtype(DType::F32)?
53            .reshape((max_seq_len, 1))?;
54        let freqs = t.matmul(&inv_freq)?;
55        Ok(Self {
56            sin: freqs.sin()?.to_dtype(dtype)?,
57            cos: freqs.cos()?.to_dtype(dtype)?,
58        })
59    }
60
61    /// Apply RoPE (q, k shape: B x H x L x D)
62    pub(crate) fn apply(&self, q: &Tensor, k: &Tensor, offset: usize) -> Result<(Tensor, Tensor)> {
63        let (_, _, seq_len, _) = q.dims4()?;
64        let cos = self.cos.narrow(0, offset, seq_len)?;
65        let sin = self.sin.narrow(0, offset, seq_len)?;
66        let q_embed = candle_nn::rotary_emb::rope(&q.contiguous()?, &cos, &sin)?;
67        let k_embed = candle_nn::rotary_emb::rope(&k.contiguous()?, &cos, &sin)?;
68        Ok((q_embed, k_embed))
69    }
70}
71
72#[derive(Debug, Clone)]
73pub(crate) struct Qwen3MLP {
74    gate_proj: Linear,
75    up_proj: Linear,
76    down_proj: Linear,
77    act_fn: Activation,
78}
79
80impl Qwen3MLP {
81    pub(crate) fn new(cfg: &Config, vb: VarBuilder) -> Result<Self> {
82        Ok(Self {
83            gate_proj: linear_no_bias(cfg.hidden_size, cfg.intermediate_size, vb.pp("gate_proj"))?,
84            up_proj: linear_no_bias(cfg.hidden_size, cfg.intermediate_size, vb.pp("up_proj"))?,
85            down_proj: linear_no_bias(cfg.intermediate_size, cfg.hidden_size, vb.pp("down_proj"))?,
86            act_fn: cfg.hidden_act,
87        })
88    }
89}
90
91impl Module for Qwen3MLP {
92    fn forward(&self, x: &Tensor) -> Result<Tensor> {
93        let lhs = x.apply(&self.gate_proj)?.apply(&self.act_fn)?;
94        let rhs = x.apply(&self.up_proj)?;
95        (lhs * rhs)?.apply(&self.down_proj)
96    }
97}
98
99#[derive(Debug, Clone)]
100pub(crate) struct Qwen3Attention {
101    // projections
102    q_proj: Linear,
103    k_proj: Linear,
104    v_proj: Linear,
105    o_proj: Linear,
106    // norms
107    q_norm: RmsNorm,
108    k_norm: RmsNorm,
109    // hyper params
110    num_heads: usize,
111    num_kv_heads: usize,
112    num_kv_groups: usize,
113    head_dim: usize,
114    hidden_size: usize,
115    // utils
116    rotary_emb: Arc<Qwen3RotaryEmbedding>,
117    kv_cache: ConcatKvCache,
118}
119
120impl Qwen3Attention {
121    pub(crate) fn new(
122        cfg: &Config,
123        rotary_emb: Arc<Qwen3RotaryEmbedding>,
124        vb: VarBuilder,
125    ) -> Result<Self> {
126        if cfg.use_sliding_window {
127            candle::bail!("sliding window is not supported")
128        }
129
130        let head_dim = cfg.head_dim;
131        let num_heads = cfg.num_attention_heads;
132        let num_kv_heads = cfg.num_key_value_heads;
133        let num_kv_groups = num_heads / num_kv_heads;
134
135        let q_proj = linear_b(
136            cfg.hidden_size,
137            num_heads * head_dim,
138            cfg.attention_bias,
139            vb.pp("q_proj"),
140        )?;
141        let k_proj = linear_b(
142            cfg.hidden_size,
143            num_kv_heads * head_dim,
144            cfg.attention_bias,
145            vb.pp("k_proj"),
146        )?;
147        let v_proj = linear_b(
148            cfg.hidden_size,
149            num_kv_heads * head_dim,
150            cfg.attention_bias,
151            vb.pp("v_proj"),
152        )?;
153        let o_proj = linear_b(
154            num_heads * head_dim,
155            cfg.hidden_size,
156            cfg.attention_bias,
157            vb.pp("o_proj"),
158        )?;
159
160        let q_norm = RmsNorm::new(head_dim, cfg.rms_norm_eps, vb.pp("q_norm"))?;
161        let k_norm = RmsNorm::new(head_dim, cfg.rms_norm_eps, vb.pp("k_norm"))?;
162
163        // Necessary because the hidden_size in the config isn't always accurate
164        let hidden_size = head_dim * cfg.num_attention_heads;
165
166        // dim=2 because we concatenate along the sequence dimension
167        // For tensors of shape [batch, heads, seq, head_dim]
168        let kv_cache = ConcatKvCache::new(2);
169
170        Ok(Self {
171            q_proj,
172            k_proj,
173            v_proj,
174            o_proj,
175            q_norm,
176            k_norm,
177            num_heads,
178            num_kv_heads,
179            num_kv_groups,
180            head_dim,
181            hidden_size,
182            rotary_emb,
183            kv_cache,
184        })
185    }
186
187    pub(crate) fn forward(
188        &mut self,
189        x: &Tensor,
190        attn_mask: Option<&Tensor>,
191        offset: usize,
192    ) -> Result<Tensor> {
193        let (b, l, _) = x.dims3()?;
194
195        // 1. Proj
196        let q = self.q_proj.forward(x)?;
197        let k = self.k_proj.forward(x)?;
198        let v = self.v_proj.forward(x)?;
199
200        // 2. Reshape: (B, L, H, D) -> (B, H, L, D)
201        let q = q
202            .reshape((b, l, self.num_heads, self.head_dim))?
203            .transpose(1, 2)?;
204        let k = k
205            .reshape((b, l, self.num_kv_heads, self.head_dim))?
206            .transpose(1, 2)?;
207        let v = v
208            .reshape((b, l, self.num_kv_heads, self.head_dim))?
209            .transpose(1, 2)?;
210
211        // 3. Per-head RMSNorm
212        let q_flat = q.flatten(0, 2)?;
213        let k_flat = k.flatten(0, 2)?;
214        let q_flat = self.q_norm.forward(&q_flat)?;
215        let k_flat = self.k_norm.forward(&k_flat)?;
216        let q = q_flat.reshape((b, self.num_heads, l, self.head_dim))?;
217        let k = k_flat.reshape((b, self.num_kv_heads, l, self.head_dim))?;
218
219        // 4. RoPE
220        let (q, k) = self.rotary_emb.apply(&q, &k, offset)?;
221
222        // 5. Accumulate KV cache
223        let (k, v) = self.kv_cache.append(&k, &v)?;
224
225        // 6. Attention dispatch: auto-select best available path
226        //    - CPU (no flash-attn feature): fused CPU flash kernel
227        //    - GPU (flash-attn feature):    CUDA flash attention
228        //    - Fallback:                    standard matmul attention
229        let on_cpu = x.device().is_cpu();
230
231        #[cfg(not(feature = "flash-attn"))]
232        if on_cpu {
233            return self.forward_cpu_flash_attn(&q, &k, &v, offset, b, l);
234        }
235        #[cfg(feature = "flash-attn")]
236        if !on_cpu {
237            return self.forward_flash_attn(&q, &k, &v, offset, b, l);
238        }
239
240        self.forward_standard_attn(&q, &k, &v, attn_mask, b, l)
241    }
242
243    /// GPU flash attention path (requires flash-attn feature)
244    #[cfg(feature = "flash-attn")]
245    fn forward_flash_attn(
246        &self,
247        q: &Tensor,
248        k: &Tensor,
249        v: &Tensor,
250        _offset: usize,
251        b: usize,
252        l: usize,
253    ) -> Result<Tensor> {
254        // Flash attention expects (B, S, H, D) format
255        let q = q.transpose(1, 2)?.contiguous()?;
256        let k = k.transpose(1, 2)?.contiguous()?;
257        let v = v.transpose(1, 2)?.contiguous()?;
258
259        let scale = 1.0 / (self.head_dim as f32).sqrt();
260        let causal = l > 1;
261        let ctx = candle_flash_attn::flash_attn(&q, &k, &v, scale, causal)?;
262
263        // Output: (B, S, H, D) -> (B, L, hidden_size)
264        ctx.reshape((b, l, self.hidden_size))?.apply(&self.o_proj)
265    }
266
267    /// CPU flash attention - optimized fused kernel for CPU
268    ///
269    /// The `flash_attn` dispatcher in candle-nn automatically selects:
270    /// - B=1: single-batch optimized kernels (direct slice access)
271    /// - B>1: packed varlen path (avoids batch-dim stride overhead)
272    #[cfg(not(feature = "flash-attn"))]
273    fn forward_cpu_flash_attn(
274        &self,
275        q: &Tensor,
276        k: &Tensor,
277        v: &Tensor,
278        offset: usize,
279        b: usize,
280        l: usize,
281    ) -> Result<Tensor> {
282        // CPU flash attention expects (B, S, H, D) format
283        let q = q.transpose(1, 2)?.contiguous()?;
284        let k = k.transpose(1, 2)?.contiguous()?;
285        let v = v.transpose(1, 2)?.contiguous()?;
286
287        let scale = 1.0 / (self.head_dim as f32).sqrt();
288
289        let ctx = match q.dtype() {
290            DType::F32 => flash_attn::<f32>(
291                &q,
292                &k,
293                &v,
294                scale,
295                AttnMask::causal_with_offset(offset),
296                None,
297                None,
298            )?,
299            // bf16/f16: the CPU kernels run in f32, so upcast the inputs, run, and
300            // narrow the result back to the model dtype. f64 is rejected in
301            // `Model::new`, so it can never reach this match.
302            other => {
303                let q_f32 = q.to_dtype(DType::F32)?;
304                let k_f32 = k.to_dtype(DType::F32)?;
305                let v_f32 = v.to_dtype(DType::F32)?;
306                let ctx_f32 = flash_attn::<f32>(
307                    &q_f32,
308                    &k_f32,
309                    &v_f32,
310                    scale,
311                    AttnMask::causal_with_offset(offset),
312                    None,
313                    None,
314                )?;
315                ctx_f32.to_dtype(other)?
316            }
317        };
318
319        // Output from CPU flash attention is (B, H, S, D), transpose to (B, S, H, D)
320        let ctx = ctx.transpose(1, 2)?;
321
322        ctx.reshape((b, l, self.hidden_size))?.apply(&self.o_proj)
323    }
324
325    /// Standard matmul-based attention (works on any device)
326    fn forward_standard_attn(
327        &self,
328        q: &Tensor,
329        k: &Tensor,
330        v: &Tensor,
331        attn_mask: Option<&Tensor>,
332        b: usize,
333        l: usize,
334    ) -> Result<Tensor> {
335        // GQA repeat_kv
336        let k = repeat_kv(k.clone(), self.num_kv_groups)?.contiguous()?;
337        let v = repeat_kv(v.clone(), self.num_kv_groups)?.contiguous()?;
338
339        // Attention score
340        let scale = 1.0 / (self.head_dim as f64).sqrt();
341        let mut scores = (q.matmul(&k.transpose(2, 3)?)? * scale)?;
342        if let Some(m) = attn_mask {
343            scores = scores.broadcast_add(m)?;
344        }
345        let probs = candle_nn::ops::softmax_last_dim(&scores)?;
346        let ctx = probs.matmul(&v)?; // (B, H, L, D)
347
348        // Output proj
349        ctx.transpose(1, 2)?
350            .reshape((b, l, self.hidden_size))?
351            .apply(&self.o_proj)
352    }
353
354    pub(crate) fn clear_kv_cache(&mut self) {
355        self.kv_cache.reset();
356    }
357}
358
359#[derive(Debug, Clone)]
360struct DecoderLayer {
361    self_attn: Qwen3Attention,
362    mlp: Qwen3MLP,
363    ln1: RmsNorm,
364    ln2: RmsNorm,
365}
366
367impl DecoderLayer {
368    fn new(cfg: &Config, rotary: Arc<Qwen3RotaryEmbedding>, vb: VarBuilder) -> Result<Self> {
369        let self_attn = Qwen3Attention::new(cfg, rotary, vb.pp("self_attn"))?;
370        let mlp = Qwen3MLP::new(cfg, vb.pp("mlp"))?;
371        let ln1 = RmsNorm::new(cfg.hidden_size, cfg.rms_norm_eps, vb.pp("input_layernorm"))?;
372        let ln2 = RmsNorm::new(
373            cfg.hidden_size,
374            cfg.rms_norm_eps,
375            vb.pp("post_attention_layernorm"),
376        )?;
377        Ok(Self {
378            self_attn,
379            mlp,
380            ln1,
381            ln2,
382        })
383    }
384
385    fn forward(&mut self, x: &Tensor, mask: Option<&Tensor>, offset: usize) -> Result<Tensor> {
386        let h = self.ln1.forward(x)?;
387        let h = self.self_attn.forward(&h, mask, offset)?;
388        let x = (x + h)?;
389        let h2 = self.ln2.forward(&x)?;
390        let h2 = h2.apply(&self.mlp)?;
391        x + h2
392    }
393
394    fn clear_kv_cache(&mut self) {
395        self.self_attn.clear_kv_cache();
396    }
397}
398
399#[derive(Debug, Clone)]
400pub struct Model {
401    embed_tokens: candle_nn::Embedding,
402    layers: Vec<DecoderLayer>,
403    norm: RmsNorm,
404    device: Device,
405    dtype: DType,
406}
407
408impl Model {
409    pub fn new(cfg: &Config, vb: VarBuilder) -> Result<Self> {
410        // f64 is not a target for Qwen3 (CPU flash runs in f32, GPU flash in f16/bf16).
411        // Reject it here, at the single point where the model dtype is set, so no f64
412        // tensor can ever reach the attention kernels and the inner paths never branch on it.
413        if vb.dtype() == DType::F64 {
414            candle::bail!(
415                "Qwen3 does not support f64; load weights as f32 or bf16 (CPU) or f16/bf16 (GPU)"
416            );
417        }
418        let embed_tokens =
419            candle_nn::embedding(cfg.vocab_size, cfg.hidden_size, vb.pp("model.embed_tokens"))?;
420        let rotary = Arc::new(Qwen3RotaryEmbedding::new(vb.dtype(), cfg, vb.device())?);
421        let mut layers = Vec::with_capacity(cfg.num_hidden_layers);
422        let vb_l = vb.pp("model.layers");
423        for i in 0..cfg.num_hidden_layers {
424            layers.push(DecoderLayer::new(cfg, rotary.clone(), vb_l.pp(i))?);
425        }
426        Ok(Self {
427            embed_tokens,
428            layers,
429            norm: RmsNorm::new(cfg.hidden_size, cfg.rms_norm_eps, vb.pp("model.norm"))?,
430            device: vb.device().clone(),
431            dtype: vb.dtype(),
432        })
433    }
434
435    fn clear_kv_cache(&mut self) {
436        for l in &mut self.layers {
437            l.clear_kv_cache();
438        }
439    }
440
441    fn causal_mask(
442        &self,
443        b: usize,
444        tgt: usize,
445        offset: usize,
446        sw: Option<usize>,
447    ) -> Result<Tensor> {
448        let minf = f32::NEG_INFINITY;
449        let mask: Vec<_> = (0..tgt)
450            .flat_map(|i| {
451                (0..(tgt + offset)).map(move |j| {
452                    let past_ok = j <= i + offset;
453                    let sw_ok = match sw {
454                        Some(w) => (i + offset) as i64 - j as i64 <= w as i64,
455                        None => true,
456                    };
457                    if past_ok && sw_ok {
458                        0.
459                    } else {
460                        minf
461                    }
462                })
463            })
464            .collect();
465        Tensor::from_slice(&mask, (b, 1, tgt, tgt + offset), &self.device)?.to_dtype(self.dtype)
466    }
467
468    pub fn forward(&mut self, input: &Tensor, offset: usize) -> Result<Tensor> {
469        let (b, l) = input.dims2()?;
470        let mut h = self.embed_tokens.forward(input)?;
471
472        // Build causal mask only for the standard attention fallback path.
473        // Both CPU flash and GPU flash handle masking internally.
474        #[cfg(not(feature = "flash-attn"))]
475        let needs_mask = !self.device.is_cpu() && l > 1;
476        #[cfg(feature = "flash-attn")]
477        let needs_mask = self.device.is_cpu() && l > 1;
478        let causal = if needs_mask {
479            Some(self.causal_mask(b, l, offset, None)?)
480        } else {
481            None
482        };
483
484        for layer in &mut self.layers {
485            h = layer.forward(&h, causal.as_ref(), offset)?;
486        }
487        self.norm.forward(&h)
488    }
489}
490
491#[derive(Debug, Clone)]
492pub struct ModelForCausalLM {
493    base: Model,
494    lm_head: Linear,
495}
496
497impl ModelForCausalLM {
498    pub fn new(cfg: &Config, vb: VarBuilder) -> Result<Self> {
499        let base = Model::new(cfg, vb.clone())?;
500        let lm_head = if cfg.tie_word_embeddings {
501            Linear::from_weights(base.embed_tokens.embeddings().clone(), None)
502        } else {
503            linear_no_bias(cfg.hidden_size, cfg.vocab_size, vb.pp("lm_head"))?
504        };
505        Ok(Self { base, lm_head })
506    }
507
508    pub fn forward(&mut self, input: &Tensor, offset: usize) -> Result<Tensor> {
509        let (_, l) = input.dims2()?;
510        self.base
511            .forward(input, offset)?
512            .narrow(1, l - 1, 1)?
513            .apply(&self.lm_head)
514    }
515
516    pub fn clear_kv_cache(&mut self) {
517        self.base.clear_kv_cache();
518    }
519}