Skip to main content

candle_transformers/models/smol/
smollm3.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::KvCache, Activation, VarBuilder};
7use std::sync::Arc;
8
9#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
10pub struct Config {
11    pub vocab_size: usize,
12    pub hidden_size: usize,
13    pub intermediate_size: usize,
14    pub num_hidden_layers: usize,
15    pub num_attention_heads: usize,
16    pub num_key_value_heads: usize,
17    pub max_position_embeddings: usize,
18    pub tie_word_embeddings: bool,
19    pub rope_theta: f64,
20    pub rms_norm_eps: f64,
21    pub hidden_act: Activation,
22    // Optional fields
23    pub attention_bias: Option<bool>,
24    pub attention_dropout: Option<f64>,
25    pub mlp_bias: Option<bool>,
26    pub sliding_window: Option<usize>,
27    pub use_sliding_window: Option<bool>,
28    pub rope_scaling: Option<serde_json::Value>,
29    pub bos_token_id: Option<u32>,
30    pub eos_token_id: Option<u32>,
31    pub pad_token_id: Option<u32>,
32    pub max_window_layers: Option<usize>,
33    // SmolLM3-specific: NoPE configuration
34    pub no_rope_layers: Option<Vec<usize>>,
35    pub no_rope_layer_interval: Option<usize>,
36}
37
38impl Config {
39    pub fn should_skip_rope(&self, layer_idx: usize) -> bool {
40        // Method 1: Explicit array (some model variants may provide this)
41        if let Some(ref no_rope_layers) = self.no_rope_layers {
42            if layer_idx < no_rope_layers.len() {
43                // 0 = skip RoPE (NoPE), 1 = use RoPE
44                return no_rope_layers[layer_idx] == 0;
45            }
46        }
47
48        // Method 2: Interval pattern (SmolLM3-3B uses this)
49        // With interval=4: layers 0,1,2 use RoPE; layer 3 skips RoPE (NoPE)
50        // Pattern: every 4th layer (3,7,11...) skips RoPE
51        if let Some(interval) = self.no_rope_layer_interval {
52            return (layer_idx + 1).is_multiple_of(interval);
53        }
54
55        // Default: use RoPE on all layers (standard Llama behavior)
56        false
57    }
58
59    /// Calculates head_dim from hidden_size and num_attention_heads
60    pub fn head_dim(&self) -> usize {
61        self.hidden_size / self.num_attention_heads
62    }
63}
64
65#[derive(Debug, Clone)]
66pub(crate) struct SmolLM3RotaryEmbedding {
67    sin: Tensor,
68    cos: Tensor,
69}
70
71impl SmolLM3RotaryEmbedding {
72    pub(crate) fn new(dtype: DType, cfg: &Config, dev: &Device) -> Result<Self> {
73        let dim = cfg.head_dim();
74        let max_seq_len = cfg.max_position_embeddings;
75        let inv_freq: Vec<_> = (0..dim)
76            .step_by(2)
77            .map(|i| 1f32 / cfg.rope_theta.powf(i as f64 / dim as f64) as f32)
78            .collect();
79        let inv_freq_len = inv_freq.len();
80        let inv_freq = Tensor::from_vec(inv_freq, (1, inv_freq_len), dev)?.to_dtype(DType::F32)?;
81        let t = Tensor::arange(0u32, max_seq_len as u32, dev)?
82            .to_dtype(DType::F32)?
83            .reshape((max_seq_len, 1))?;
84        let freqs = t.matmul(&inv_freq)?;
85        Ok(Self {
86            sin: freqs.sin()?.to_dtype(dtype)?,
87            cos: freqs.cos()?.to_dtype(dtype)?,
88        })
89    }
90
91    /// Apply RoPE (q, k shape: B x H x L x D)
92    pub(crate) fn apply(&self, q: &Tensor, k: &Tensor, offset: usize) -> Result<(Tensor, Tensor)> {
93        let (_, _, seq_len, _) = q.dims4()?;
94        let cos = self.cos.narrow(0, offset, seq_len)?;
95        let sin = self.sin.narrow(0, offset, seq_len)?;
96        let q_embed = candle_nn::rotary_emb::rope(&q.contiguous()?, &cos, &sin)?;
97        let k_embed = candle_nn::rotary_emb::rope(&k.contiguous()?, &cos, &sin)?;
98        Ok((q_embed, k_embed))
99    }
100}
101
102#[derive(Debug, Clone)]
103pub(crate) struct SmolLM3MLP {
104    gate_proj: Linear,
105    up_proj: Linear,
106    down_proj: Linear,
107    act_fn: Activation,
108}
109
110impl SmolLM3MLP {
111    pub(crate) fn new(cfg: &Config, vb: VarBuilder) -> Result<Self> {
112        let mlp_bias = cfg.mlp_bias.unwrap_or(false);
113        Ok(Self {
114            gate_proj: linear_b(
115                cfg.hidden_size,
116                cfg.intermediate_size,
117                mlp_bias,
118                vb.pp("gate_proj"),
119            )?,
120            up_proj: linear_b(
121                cfg.hidden_size,
122                cfg.intermediate_size,
123                mlp_bias,
124                vb.pp("up_proj"),
125            )?,
126            down_proj: linear_b(
127                cfg.intermediate_size,
128                cfg.hidden_size,
129                mlp_bias,
130                vb.pp("down_proj"),
131            )?,
132            act_fn: cfg.hidden_act,
133        })
134    }
135}
136
137impl Module for SmolLM3MLP {
138    fn forward(&self, x: &Tensor) -> Result<Tensor> {
139        let lhs = x.apply(&self.gate_proj)?.apply(&self.act_fn)?;
140        let rhs = x.apply(&self.up_proj)?;
141        (lhs * rhs)?.apply(&self.down_proj)
142    }
143}
144
145#[derive(Debug, Clone)]
146pub(crate) struct SmolLM3Attention {
147    // projections
148    q_proj: Linear,
149    k_proj: Linear,
150    v_proj: Linear,
151    o_proj: Linear,
152    // hyper params
153    num_heads: usize,
154    num_kv_heads: usize,
155    num_kv_groups: usize,
156    head_dim: usize,
157    hidden_size: usize,
158    // utils
159    rotary_emb: Option<Arc<SmolLM3RotaryEmbedding>>,
160    kv_cache: KvCache,
161    // NoPE flag
162    skip_rope: bool,
163}
164
165impl SmolLM3Attention {
166    pub(crate) fn new(
167        cfg: &Config,
168        layer_idx: usize,
169        rotary_emb: Option<Arc<SmolLM3RotaryEmbedding>>,
170        vb: VarBuilder,
171    ) -> Result<Self> {
172        let use_sliding_window = cfg.use_sliding_window.unwrap_or(false);
173        if use_sliding_window {
174            candle::bail!("sliding window is not supported")
175        }
176
177        let head_dim = cfg.head_dim();
178        let num_heads = cfg.num_attention_heads;
179        let num_kv_heads = cfg.num_key_value_heads;
180        let num_kv_groups = num_heads / num_kv_heads;
181
182        let attention_bias = cfg.attention_bias.unwrap_or(false);
183
184        let q_proj = linear_b(
185            cfg.hidden_size,
186            num_heads * head_dim,
187            attention_bias,
188            vb.pp("q_proj"),
189        )?;
190
191        let k_proj = linear_b(
192            cfg.hidden_size,
193            num_kv_heads * head_dim,
194            attention_bias,
195            vb.pp("k_proj"),
196        )?;
197
198        let v_proj = linear_b(
199            cfg.hidden_size,
200            num_kv_heads * head_dim,
201            attention_bias,
202            vb.pp("v_proj"),
203        )?;
204        let o_proj = linear_b(
205            num_heads * head_dim,
206            cfg.hidden_size,
207            attention_bias,
208            vb.pp("o_proj"),
209        )?;
210
211        // Necessary because the hidden_size in the config isn't always accurate
212        let hidden_size = head_dim * cfg.num_attention_heads;
213
214        // Initialize KV cache with 512 tokens capacity to reduce initial memory allocation.
215        // The cache will grow in chunks of 512 tokens when needed.
216        let kv_cache = KvCache::new(2, 512);
217
218        // Check if this layer should skip RoPE (NoPE)
219        let skip_rope = cfg.should_skip_rope(layer_idx);
220
221        Ok(Self {
222            q_proj,
223            k_proj,
224            v_proj,
225            o_proj,
226            num_heads,
227            num_kv_heads,
228            num_kv_groups,
229            head_dim,
230            hidden_size,
231            rotary_emb,
232            kv_cache,
233            skip_rope,
234        })
235    }
236
237    pub(crate) fn forward(
238        &mut self,
239        x: &Tensor,
240        attn_mask: Option<&Tensor>,
241        offset: usize,
242    ) -> Result<Tensor> {
243        let (b, l, _) = x.dims3()?;
244
245        // 1. Proj
246        let q = self.q_proj.forward(x)?;
247        let k = self.k_proj.forward(x)?;
248        let v = self.v_proj.forward(x)?;
249
250        // 2. Reshape: (B, L, H, D) -> (B, H, L, D)
251        let q = q
252            .reshape((b, l, self.num_heads, self.head_dim))?
253            .transpose(1, 2)?;
254        let k = k
255            .reshape((b, l, self.num_kv_heads, self.head_dim))?
256            .transpose(1, 2)?;
257        let v = v
258            .reshape((b, l, self.num_kv_heads, self.head_dim))?
259            .transpose(1, 2)?;
260
261        // 3. RoPE - only apply if this layer should use RoPE (not NoPE)
262        let (q, k) = if self.skip_rope {
263            // NoPE: Skip rotary embeddings, but ensure tensors are contiguous
264            (q.contiguous()?, k.contiguous()?)
265        } else {
266            // Apply RoPE
267            if let Some(ref rope) = self.rotary_emb {
268                rope.apply(&q, &k, offset)?
269            } else {
270                (q, k)
271            }
272        };
273
274        // 4. Accumulate KV cache
275        // Reset KV cache if we're at the first position
276        if offset == 0 {
277            self.kv_cache.reset();
278        }
279        let (k, v) = self.kv_cache.append(&k.contiguous()?, &v.contiguous()?)?;
280
281        // 5. GQA repeat_kv
282        let k = repeat_kv(k, self.num_kv_groups)?;
283        let v = repeat_kv(v, self.num_kv_groups)?;
284
285        // 6. Attention score
286        let scale = 1.0 / (self.head_dim as f64).sqrt();
287        let mut scores = (q.matmul(&k.transpose(2, 3)?)? * scale)?;
288        if let Some(m) = attn_mask {
289            scores = scores.broadcast_add(m)?;
290        }
291        let probs = candle_nn::ops::softmax_last_dim(&scores)?;
292        let ctx = probs.matmul(&v)?; // (B, H, L, D)
293
294        // 7. Output proj
295        ctx.transpose(1, 2)?
296            .reshape((b, l, self.hidden_size))?
297            .apply(&self.o_proj)
298    }
299
300    pub fn clear_kv_cache(&mut self) {
301        self.kv_cache.reset();
302    }
303}
304
305#[derive(Debug, Clone)]
306pub(crate) struct DecoderLayer {
307    self_attn: SmolLM3Attention,
308    mlp: SmolLM3MLP,
309    ln1: RmsNorm,
310    ln2: RmsNorm,
311}
312
313impl DecoderLayer {
314    fn new(
315        cfg: &Config,
316        layer_idx: usize,
317        rotary: Option<Arc<SmolLM3RotaryEmbedding>>,
318        vb: VarBuilder,
319    ) -> Result<Self> {
320        let self_attn = SmolLM3Attention::new(cfg, layer_idx, rotary, vb.pp("self_attn"))?;
321        let mlp = SmolLM3MLP::new(cfg, vb.pp("mlp"))?;
322        let ln1 = RmsNorm::new(cfg.hidden_size, cfg.rms_norm_eps, vb.pp("input_layernorm"))?;
323        let ln2 = RmsNorm::new(
324            cfg.hidden_size,
325            cfg.rms_norm_eps,
326            vb.pp("post_attention_layernorm"),
327        )?;
328        Ok(Self {
329            self_attn,
330            mlp,
331            ln1,
332            ln2,
333        })
334    }
335
336    fn forward(&mut self, x: &Tensor, mask: Option<&Tensor>, offset: usize) -> Result<Tensor> {
337        let h = self.ln1.forward(x)?;
338        let h = self.self_attn.forward(&h, mask, offset)?;
339        let x = (x + h)?;
340        let h2 = self.ln2.forward(&x)?;
341        let h2 = h2.apply(&self.mlp)?;
342        x + h2
343    }
344
345    pub fn clear_kv_cache(&mut self) {
346        self.self_attn.clear_kv_cache();
347    }
348}
349
350#[derive(Debug, Clone)]
351pub struct Model {
352    pub(crate) embed_tokens: candle_nn::Embedding,
353    pub(crate) layers: Vec<DecoderLayer>,
354    pub(crate) norm: RmsNorm,
355    device: Device,
356    dtype: DType,
357}
358
359impl Model {
360    pub fn new(cfg: &Config, vb: VarBuilder) -> Result<Self> {
361        let embed_tokens =
362            candle_nn::embedding(cfg.vocab_size, cfg.hidden_size, vb.pp("model.embed_tokens"))?;
363
364        // Only create rotary embedding if at least one layer uses RoPE
365        let needs_rope = (0..cfg.num_hidden_layers).any(|i| !cfg.should_skip_rope(i));
366        let rotary = if needs_rope {
367            Some(Arc::new(SmolLM3RotaryEmbedding::new(
368                vb.dtype(),
369                cfg,
370                vb.device(),
371            )?))
372        } else {
373            None
374        };
375
376        let mut layers = Vec::with_capacity(cfg.num_hidden_layers);
377        let vb_l = vb.pp("model.layers");
378        for i in 0..cfg.num_hidden_layers {
379            layers.push(DecoderLayer::new(cfg, i, rotary.clone(), vb_l.pp(i))?);
380        }
381        Ok(Self {
382            embed_tokens,
383            layers,
384            norm: RmsNorm::new(cfg.hidden_size, cfg.rms_norm_eps, vb.pp("model.norm"))?,
385            device: vb.device().clone(),
386            dtype: vb.dtype(),
387        })
388    }
389
390    pub fn clear_kv_cache(&mut self) {
391        for l in &mut self.layers {
392            l.clear_kv_cache();
393        }
394    }
395
396    fn causal_mask(
397        &self,
398        b: usize,
399        tgt: usize,
400        offset: usize,
401        sw: Option<usize>,
402    ) -> Result<Tensor> {
403        let minf = f32::NEG_INFINITY;
404        let mask: Vec<_> = (0..tgt)
405            .flat_map(|i| {
406                (0..(tgt + offset)).map(move |j| {
407                    let past_ok = j <= i + offset;
408                    let sw_ok = match sw {
409                        Some(w) => (i + offset) as i64 - j as i64 <= w as i64,
410                        None => true,
411                    };
412                    if past_ok && sw_ok {
413                        0.
414                    } else {
415                        minf
416                    }
417                })
418            })
419            .collect();
420        Tensor::from_slice(&mask, (b, 1, tgt, tgt + offset), &self.device)?.to_dtype(self.dtype)
421    }
422
423    pub fn forward(&mut self, input: &Tensor, offset: usize) -> Result<Tensor> {
424        let (b, l) = input.dims2()?;
425
426        let mut h = self.embed_tokens.forward(input)?;
427
428        let causal = if l == 1 {
429            None
430        } else {
431            Some(self.causal_mask(b, l, offset, None)?)
432        };
433
434        for layer in &mut self.layers {
435            h = layer.forward(&h, causal.as_ref(), offset)?;
436        }
437        self.norm.forward(&h)
438    }
439}
440
441#[derive(Debug, Clone)]
442pub struct ModelForCausalLM {
443    base: Model,
444    lm_head: Linear,
445}
446
447impl ModelForCausalLM {
448    pub fn new(cfg: &Config, vb: VarBuilder) -> Result<Self> {
449        let base = Model::new(cfg, vb.clone())?;
450        let lm_head = if cfg.tie_word_embeddings {
451            Linear::from_weights(base.embed_tokens.embeddings().clone(), None)
452        } else {
453            linear_no_bias(cfg.hidden_size, cfg.vocab_size, vb.pp("lm_head"))?
454        };
455        Ok(Self { base, lm_head })
456    }
457
458    pub fn forward(&mut self, input: &Tensor, offset: usize) -> Result<Tensor> {
459        let (_, l) = input.dims2()?;
460
461        self.base
462            .forward(input, offset)?
463            .narrow(1, l - 1, 1)?
464            .apply(&self.lm_head)
465    }
466
467    pub fn clear_kv_cache(&mut self) {
468        self.base.clear_kv_cache();
469    }
470}