Skip to main content

combs_models/
llama.rs

1//! Llama-family architecture (Llama, SmolLM2, …) on the [`GenerativeModel`]
2//! contract.
3//!
4//! Expects HuggingFace weight names:
5//! `model.embed_tokens.weight`,
6//! `model.layers.{i}.self_attn.{q,k,v,o}_proj.weight`,
7//! `model.layers.{i}.mlp.{gate,up,down}_proj.weight`,
8//! `model.layers.{i}.{input,post_attention}_layernorm.weight`,
9//! `model.norm.weight`, `lm_head.weight` (optional when tied).
10//!
11//! Weights are hand-rolled matmuls (`y = x @ W^T`) rather than burn `nn`
12//! modules so they can be streamed straight from a [`ModelSource`] without
13//! record files. The family has no biases; the loader tolerates optional
14//! `*_proj.bias` tensors for related checkpoints that carry them.
15
16use std::ops::Range;
17
18use burn::tensor::{Device, Int, Tensor, backend::Backend};
19use combs_formats::{ModelMetadata, ModelSource};
20
21use crate::archspec::{ArchSpec, LayerKind, NormFlavor};
22use crate::kv::{CacheConfig, CacheKind, ContiguousKVCache, KVCache, PagedKVCache};
23use crate::matmul::safe_matmul;
24use crate::norm::{gemma_rms_norm, rms_norm};
25use crate::precision::{to_f32, to_float};
26use crate::qlinear::{Linear, try_quant_linear};
27use crate::rope::RotaryEmbedding;
28use crate::traits::GenerativeModel;
29use crate::{ModelError, Result};
30
31/// One decoder layer's weights. All projections are `[out, in]` (HF layout);
32/// each is a [`Linear`] — dense tensor or packed-quant kernel dispatch,
33/// decided per tensor at load from the source's stored format.
34///
35/// The optional norms cover the family's variants: `attn_out_norm` /
36/// `mlp_out_norm` are gemma's sandwich norms, `q_norm`/`k_norm` the
37/// per-head QK-norms (gemma-3, qwen-3). For plain llama they're all `None`
38/// and the forward reduces exactly to the classic block.
39struct LlamaLayer<B: Backend> {
40    q: Linear<B>,
41    k: Linear<B>,
42    v: Linear<B>,
43    o: Linear<B>,
44    q_bias: Option<Tensor<B, 1>>,
45    k_bias: Option<Tensor<B, 1>>,
46    v_bias: Option<Tensor<B, 1>>,
47    o_bias: Option<Tensor<B, 1>>,
48    q_norm: Option<Tensor<B, 1>>,
49    k_norm: Option<Tensor<B, 1>>,
50    gate: Linear<B>,
51    up: Linear<B>,
52    down: Linear<B>,
53    input_norm: Tensor<B, 1>,
54    /// Pre-MLP norm: llama's `post_attention_layernorm`, gemma's
55    /// `pre_feedforward_layernorm` (same role, different name).
56    pre_mlp_norm: Tensor<B, 1>,
57    attn_out_norm: Option<Tensor<B, 1>>,
58    mlp_out_norm: Option<Tensor<B, 1>>,
59}
60
61/// Llama-family causal LM, parameterized by the resolved [`ArchSpec`] —
62/// llama, smollm2, qwen2, and mistral today; the gemma/qwen3/phi presets
63/// migrate onto it stage by stage (roadmap wave 2).
64pub struct LlamaModel<B: Backend> {
65    metadata: ModelMetadata,
66    spec: ArchSpec,
67    /// `[vocab, hidden]`. Stays dense: embedding lookup needs `select`, and
68    /// the tied-head case reuses it as a dense matmul weight.
69    embed: Tensor<B, 2>,
70    lm_head: Option<Linear<B>>, // None => tied to `embed`
71    final_norm: Tensor<B, 1>,
72    layers: Vec<LlamaLayer<B>>,
73    rotary: RotaryEmbedding<B>,
74    /// Local-theta tables for sliding layers (gemma dual-RoPE).
75    rotary_local: Option<RotaryEmbedding<B>>,
76    /// `1/sqrt(query_pre_attn_scalar || head_dim)`.
77    scale: f64,
78}
79
80/// `y = x @ W^T (+ b)` for `[batch, seq, in] @ [out, in]`.
81pub(crate) fn linear<B: Backend>(
82    x: Tensor<B, 3>,
83    w: &Tensor<B, 2>,
84    bias: Option<&Tensor<B, 1>>,
85) -> Tensor<B, 3> {
86    // matmul is same-rank only, so the weight is batch-unsqueezed.
87    // `safe_matmul`: at seq >= 512 with in >= 512 this shape enters the
88    // broken wgpu/Metal matmul region (see combs_models::matmul docs).
89    let out = safe_matmul(x, w.clone().transpose().unsqueeze_dim::<3>(0));
90    match bias {
91        Some(b) => {
92            let [batch, seq, dim] = out.dims();
93            out + b.clone().reshape([1, 1, dim]).expand([batch, seq, dim])
94        }
95        None => out,
96    }
97}
98
99fn load_weight<B: Backend, const D: usize>(
100    source: &dyn ModelSource,
101    device: &Device<B>,
102    name: &str,
103) -> Result<Tensor<B, D>> {
104    source
105        .open_tensor(name)
106        .map_err(|e| match e {
107            combs_formats::FormatError::TensorNotFound(_) => {
108                ModelError::MissingTensor(name.to_string())
109            }
110            other => ModelError::Format(other),
111        })?
112        .load_to_tensor::<B, D>(device)
113        .map_err(ModelError::Format)
114}
115
116pub(crate) fn load_tensor<B: Backend, const D: usize>(
117    source: &dyn ModelSource,
118    device: &Device<B>,
119    name: &str,
120) -> Result<Tensor<B, D>> {
121    load_weight(source, device, name)
122}
123
124/// Loads a projection weight as a [`Linear`]: the packed-quant fast path
125/// when the source stores it in a kernel-supported GGUF format *and* the
126/// backend runs on wgpu, else the dense tensor (portable fallback).
127pub(crate) fn load_linear<B: Backend>(
128    source: &dyn ModelSource,
129    device: &Device<B>,
130    name: &str,
131) -> Result<Linear<B>> {
132    if let Some(op) = try_quant_linear::<B>(source, name, device)? {
133        return Ok(Linear::Quant(op));
134    }
135    Ok(Linear::Dense(load_weight(source, device, name)?))
136}
137
138fn load_optional_bias<B: Backend>(
139    source: &dyn ModelSource,
140    device: &Device<B>,
141    name: &str,
142) -> Result<Option<Tensor<B, 1>>> {
143    match source.open_tensor(name) {
144        Ok(reader) => Ok(Some(
145            reader.load_to_tensor::<B, 1>(device).map_err(ModelError::Format)?,
146        )),
147        Err(combs_formats::FormatError::TensorNotFound(_)) => Ok(None),
148        Err(e) => Err(ModelError::Format(e)),
149    }
150}
151
152/// Loads a fused projection dense and splits its rows into `N` groups (phi
153/// `qkv_proj` = `[q|k|v]`, `gate_up_proj` = `[gate|up]`, HF phi3 order).
154/// Dense-only on purpose: fused checkpoints are safetensors; GGUF phi files
155/// are row-sliced into split names by the format adapter before reaching
156/// this loader.
157fn split_fused_rows<B: Backend, const N: usize>(
158    source: &dyn ModelSource,
159    device: &Device<B>,
160    name: &str,
161    rows: [usize; N],
162) -> Result<[Linear<B>; N]> {
163    let w: Tensor<B, 2> = load_weight(source, device, name)?;
164    let [total, cols] = w.dims();
165    let expect: usize = rows.iter().sum();
166    if total != expect {
167        return Err(ModelError::BadShape {
168            tensor: name.to_string(),
169            expected: vec![expect, cols],
170            got: vec![total, cols],
171        });
172    }
173    let mut at = 0;
174    Ok(rows.map(|r| {
175        let part = w.clone().narrow(0, at, r);
176        at += r;
177        Linear::Dense(part)
178    }))
179}
180
181/// Row-splits a fused projection's bias when present.
182fn split_fused_bias<B: Backend, const N: usize>(
183    source: &dyn ModelSource,
184    device: &Device<B>,
185    name: &str,
186    rows: [usize; N],
187) -> Result<Option<[Tensor<B, 1>; N]>> {
188    let Some(b) = load_optional_bias(source, device, name)? else {
189        return Ok(None);
190    };
191    let [total] = b.dims();
192    let expect: usize = rows.iter().sum();
193    if total != expect {
194        return Err(ModelError::BadShape {
195            tensor: name.to_string(),
196            expected: vec![expect],
197            got: vec![total],
198        });
199    }
200    let mut at = 0;
201    Ok(Some(rows.map(|r| {
202        let part = b.clone().narrow(0, at, r);
203        at += r;
204        part
205    })))
206}
207
208impl<B: Backend> LlamaModel<B> {
209    pub(crate) fn expect_shape(name: &str, got: &[usize], expected: &[usize]) -> Result<()> {
210        if got == expected {
211            Ok(())
212        } else {
213            Err(ModelError::BadShape {
214                tensor: name.to_string(),
215                expected: expected.to_vec(),
216                got: got.to_vec(),
217            })
218        }
219    }
220
221    /// Norm in the spec's flavor (`x̂·w` vs gemma's `x̂·(1+w)`).
222    fn norm<const D: usize>(&self, x: Tensor<B, D>, w: &Tensor<B, 1>) -> Tensor<B, D> {
223        match self.spec.norm_flavor {
224            NormFlavor::RmsNorm => rms_norm(x, w.clone(), self.metadata.rms_norm_eps),
225            NormFlavor::GemmaRmsNorm => {
226                gemma_rms_norm(x, w.clone(), self.metadata.rms_norm_eps)
227            }
228        }
229    }
230
231    /// RoPE tables for a layer: sliding layers rotate with the local theta
232    /// when the spec defines one (gemma dual-RoPE), global layers with the
233    /// (possibly scaled) global tables.
234    fn rotary_for(&self, layer_idx: usize) -> &RotaryEmbedding<B> {
235        match (self.spec.layers.get(layer_idx), &self.rotary_local) {
236            (Some(LayerKind::Sliding(_)), Some(local)) => local,
237            _ => &self.rotary,
238        }
239    }
240
241    /// Shared trunk for prefill and decode: embeddings in, final-normed
242    /// hidden states out. `pos` is the absolute position of the first input
243    /// token.
244    pub(crate) fn forward_hidden(
245        &self,
246        mut x: Tensor<B, 3>,
247        cache: &mut dyn KVCache<B>,
248        pos: usize,
249    ) -> Tensor<B, 3> {
250        let m = &self.metadata;
251        let [_, seq, _] = x.dims();
252
253        for (layer_idx, layer) in self.layers.iter().enumerate() {
254            let window = match self.spec.layers.get(layer_idx) {
255                Some(LayerKind::Sliding(w)) => Some(*w),
256                _ => None,
257            };
258
259            // --- attention block ------------------------------------------------
260            let h = self.norm(x.clone(), &layer.input_norm);
261            let q = layer.q.forward(h.clone(), layer.q_bias.as_ref());
262            let k = layer.k.forward(h.clone(), layer.k_bias.as_ref());
263            let v = layer.v.forward(h, layer.v_bias.as_ref());
264
265            let mut q = q
266                .reshape([1, seq, m.num_attention_heads, m.head_dim])
267                .swap_dims(1, 2);
268            let mut k = k
269                .reshape([1, seq, m.num_key_value_heads, m.head_dim])
270                .swap_dims(1, 2);
271            let v = v
272                .reshape([1, seq, m.num_key_value_heads, m.head_dim])
273                .swap_dims(1, 2);
274
275            // Per-head QK-norm (gemma-3 / qwen-3), over head_dim.
276            if let Some(qn) = &layer.q_norm {
277                q = self.norm(q, qn);
278            }
279            if let Some(kn) = &layer.k_norm {
280                k = self.norm(k, kn);
281            }
282
283            let rotary = self.rotary_for(layer_idx);
284            let q = rotary.apply(q, pos);
285            let k = rotary.apply(k, pos);
286
287            // The cache owns K/V layout, GQA expansion and masking
288            // (causal + optional sliding window).
289            let ctx = cache.attention_opts(layer_idx, q, k, v, pos, self.scale, window);
290            let ctx = ctx
291                .swap_dims(1, 2)
292                .reshape([1, seq, m.num_attention_heads * m.head_dim]);
293            let mut attn_out = layer.o.forward(ctx, layer.o_bias.as_ref());
294            if let Some(n) = &layer.attn_out_norm {
295                attn_out = self.norm(attn_out, n);
296            }
297            x = x + attn_out;
298
299            // --- MLP block (gated) ----------------------------------------------
300            let h = self.norm(x.clone(), &layer.pre_mlp_norm);
301            let gated = crate::act::apply(self.spec.activation, layer.gate.forward(h.clone(), None))
302                * layer.up.forward(h.clone(), None);
303            let mut mlp_out = layer.down.forward(gated, None);
304            if let Some(n) = &layer.mlp_out_norm {
305                mlp_out = self.norm(mlp_out, n);
306            }
307            x = x + mlp_out;
308        }
309
310        self.norm(x, &self.final_norm)
311    }
312
313    /// Logits of every position: `[1, seq, hidden] -> [1, seq, vocab]`
314    /// (the perplexity / speculative-decode head).
315    pub(crate) fn all_logits(&self, hidden: Tensor<B, 3>) -> Tensor<B, 3> {
316        let [_, seq, hidden_size] = hidden.dims();
317        let logits: Tensor<B, 3> = match &self.lm_head {
318            Some(head) => head.forward(hidden, None),
319            None => {
320                // Tied head: dense matmul against the embedding table.
321                let flat = hidden.reshape([seq, hidden_size]);
322                let out = safe_matmul(flat, self.embed.clone().transpose());
323                let [_, vocab] = out.dims();
324                out.reshape([1, seq, vocab])
325            }
326        };
327        match self.spec.final_logit_softcap {
328            Some(cap) => logits.div_scalar(cap as f32).tanh().mul_scalar(cap as f32),
329            None => logits,
330        }
331    }
332
333    /// Logits of the last sequence position: `[1, hidden] -> [1, vocab]`.
334    pub(crate) fn last_logits(&self, hidden: Tensor<B, 3>) -> Tensor<B, 2> {
335        let [_, seq, hidden_size] = hidden.dims();
336        let last = hidden.narrow(1, seq - 1, 1); // [1, 1, hidden]
337        let logits: Tensor<B, 2> = match &self.lm_head {
338            Some(head) => {
339                let logits = head.forward(last, None);
340                let [_, _, vocab] = logits.dims();
341                logits.reshape([1, vocab])
342            }
343            None => {
344                // Tied head: dense matmul against the embedding table.
345                let last = last.reshape([1, hidden_size]);
346                safe_matmul(last, self.embed.clone().transpose())
347            }
348        };
349        match self.spec.final_logit_softcap {
350            Some(cap) => logits.div_scalar(cap as f32).tanh().mul_scalar(cap as f32),
351            None => logits,
352        }
353    }
354}
355
356impl<B: Backend> GenerativeModel<B> for LlamaModel<B> {
357    fn metadata(&self) -> &ModelMetadata {
358        &self.metadata
359    }
360
361    fn load(source: &dyn ModelSource, device: &Device<B>) -> Result<Self> {
362        // HF exports either the causal-LM wrapper (model.embed_tokens…) or
363        // the bare base model (embed_tokens…, the sentence-transformers /
364        // embedding-checkpoint layout); detect from the tensor names.
365        let prefix = if source
366            .tensor_names()
367            .iter()
368            .any(|n| n == "model.embed_tokens.weight" || n == "model.embed_tokens")
369        {
370            "model"
371        } else {
372            ""
373        };
374        Self::load_with_prefix(source, device, prefix)
375    }
376
377    fn create_kv_cache(&self, config: &CacheConfig) -> Box<dyn KVCache<B>> {
378        match config.kind {
379            CacheKind::Contiguous => {
380                Box::new(ContiguousKVCache::<B>::new(self.metadata.num_hidden_layers))
381            }
382            // Sliding layers (per the resolved layout) keep a rolling tensor
383            // instead of a paged arena; all-global specs pass all-None.
384            CacheKind::Paged => Box::new(PagedKVCache::<B>::new_with_windows(
385                self.metadata.num_hidden_layers,
386                *config,
387                self.spec.windows(),
388            )),
389        }
390    }
391
392    fn embed(&self, tokens: Tensor<B, 2, Int>) -> Tensor<B, 3> {
393        let [batch, seq] = tokens.dims();
394        let flat = tokens.reshape([batch * seq]);
395        let embedded = self
396            .embed
397            .clone()
398            .select(0, flat)
399            .reshape([batch, seq, self.metadata.hidden_size]);
400        if self.spec.embed_scale_sqrt_hidden {
401            // Gemma scales embeddings by sqrt(hidden); computed in f32 —
402            // the half-precision product was the f16 garbage-output bug.
403            let out_dtype = embedded.dtype();
404            let scale = (self.metadata.hidden_size as f64).sqrt();
405            to_float(to_f32(embedded).mul_scalar(scale), out_dtype)
406        } else {
407            embedded
408        }
409    }
410
411    fn prefill(
412        &mut self,
413        input: Tensor<B, 3>,
414        cache: &mut dyn KVCache<B>,
415        pos: Range<u32>,
416    ) -> Tensor<B, 2> {
417        let [_, seq, _] = input.dims();
418        assert_eq!(
419            seq,
420            (pos.end - pos.start) as usize,
421            "prefill pos range must match the input sequence length"
422        );
423        let hidden = self.forward_hidden(input, cache, pos.start as usize);
424        self.last_logits(hidden)
425    }
426
427    fn prefill_hidden(
428        &mut self,
429        input: Tensor<B, 3>,
430        cache: &mut dyn KVCache<B>,
431        pos: Range<u32>,
432    ) -> Result<Tensor<B, 3>> {
433        let [_, seq, _] = input.dims();
434        assert_eq!(
435            seq,
436            (pos.end - pos.start) as usize,
437            "prefill pos range must match the input sequence length"
438        );
439        Ok(self.forward_hidden(input, cache, pos.start as usize))
440    }
441
442    fn supports_hidden_states(&self) -> bool {
443        true
444    }
445
446    fn prefill_all_logits(
447        &mut self,
448        input: Tensor<B, 3>,
449        cache: &mut dyn KVCache<B>,
450        pos: Range<u32>,
451    ) -> Result<Tensor<B, 3>> {
452        let hidden = self.prefill_hidden(input, cache, pos)?;
453        Ok(self.all_logits(hidden))
454    }
455
456    fn decode(&mut self, input: Tensor<B, 3>, cache: &mut dyn KVCache<B>) -> Tensor<B, 2> {
457        let pos = cache.seq_len();
458        let hidden = self.forward_hidden(input, cache, pos);
459        self.last_logits(hidden)
460    }
461
462    fn decode_all_logits(
463        &mut self,
464        input: Tensor<B, 3>,
465        cache: &mut dyn KVCache<B>,
466    ) -> Result<Tensor<B, 3>> {
467        let pos = cache.seq_len();
468        let hidden = self.forward_hidden(input, cache, pos);
469        Ok(self.all_logits(hidden))
470    }
471
472    fn supports_decode_all_logits(&self) -> bool {
473        true
474    }
475}
476
477impl<B: Backend> LlamaModel<B> {
478    /// Loads the text stack with weight names under `prefix` (e.g. `"model"`
479    /// for plain Llama, `"model.text_model"` for Idefics3/SmolVLM, `""` for
480    /// bare base-model exports). `lm_head.weight` always stays top-level.
481    pub(crate) fn load_with_prefix(
482        source: &dyn ModelSource,
483        device: &Device<B>,
484        prefix: &str,
485    ) -> Result<Self> {
486        let m = source.metadata().clone();
487        let spec = ArchSpec::resolve(&m);
488        // Dotted prefix, or nothing for bare exports.
489        let prefix = if prefix.is_empty() {
490            String::new()
491        } else {
492            format!("{prefix}.")
493        };
494        let prefix = prefix.as_str();
495
496        let embed: Tensor<B, 2> =
497            load_weight(source, device, &format!("{prefix}embed_tokens.weight"))?;
498        Self::expect_shape(
499            "embed_tokens.weight",
500            &embed.dims(),
501            &[m.vocab_size, m.hidden_size],
502        )?;
503
504        let lm_head = if m.tie_word_embeddings {
505            None
506        } else {
507            match load_linear(source, device, "lm_head.weight") {
508                Ok(w) => {
509                    Self::expect_shape(
510                        "lm_head.weight",
511                        &w.dims(),
512                        &[m.vocab_size, m.hidden_size],
513                    )?;
514                    Some(w)
515                }
516                // Configs lie about tying (gemma3 omits the flag entirely
517                // but ships no lm_head): presence decides, like the GGUF
518                // output.weight rule — loudly, since a genuinely untied
519                // checkpoint missing its head would be corrupt.
520                Err(ModelError::MissingTensor(_)) => {
521                    eprintln!(
522                        "[load] lm_head.weight absent; falling back to tied embeddings"
523                    );
524                    None
525                }
526                Err(e) => return Err(e),
527            }
528        };
529
530        let final_norm: Tensor<B, 1> =
531            load_weight(source, device, &format!("{prefix}norm.weight"))?;
532
533        // Sandwich-norm architectures (gemma) rename the pre-MLP norm and
534        // add output norms around both residual adds.
535        let pre_mlp_name = if spec.sandwich_norms {
536            "pre_feedforward_layernorm"
537        } else {
538            "post_attention_layernorm"
539        };
540
541        let q_rows = m.num_attention_heads * m.head_dim;
542        let kv_rows = m.num_key_value_heads * m.head_dim;
543        let mut layers = Vec::with_capacity(m.num_hidden_layers);
544        for i in 0..m.num_hidden_layers {
545            let p = format!("{prefix}layers.{i}");
546            // Phi-family checkpoints fuse the attention input projections
547            // (`qkv_proj` = [q|k|v] rows); probe the split names first.
548            let (q, k, v, fused_qkv_bias) =
549                match load_linear(source, device, &format!("{p}.self_attn.q_proj.weight")) {
550                    Ok(q) => (
551                        q,
552                        load_linear(source, device, &format!("{p}.self_attn.k_proj.weight"))?,
553                        load_linear(source, device, &format!("{p}.self_attn.v_proj.weight"))?,
554                        None,
555                    ),
556                    Err(ModelError::MissingTensor(_)) => {
557                        let name = format!("{p}.self_attn.qkv_proj");
558                        let [q, k, v] = split_fused_rows(
559                            source,
560                            device,
561                            &format!("{name}.weight"),
562                            [q_rows, kv_rows, kv_rows],
563                        )?;
564                        let b = split_fused_bias(
565                            source,
566                            device,
567                            &format!("{name}.bias"),
568                            [q_rows, kv_rows, kv_rows],
569                        )?;
570                        (q, k, v, b)
571                    }
572                    Err(e) => return Err(e),
573                };
574            let o = load_linear(source, device, &format!("{p}.self_attn.o_proj.weight"))?;
575            Self::expect_shape(
576                &format!("{p}.self_attn.q_proj.weight"),
577                &q.dims(),
578                &[q_rows, m.hidden_size],
579            )?;
580            Self::expect_shape(
581                &format!("{p}.self_attn.k_proj.weight"),
582                &k.dims(),
583                &[kv_rows, m.hidden_size],
584            )?;
585            // Same fusion for the MLP input (`gate_up_proj` = [gate|up]).
586            let (gate, up) =
587                match load_linear(source, device, &format!("{p}.mlp.gate_proj.weight")) {
588                    Ok(gate) => (
589                        gate,
590                        load_linear(source, device, &format!("{p}.mlp.up_proj.weight"))?,
591                    ),
592                    Err(ModelError::MissingTensor(_)) => {
593                        let [gate, up] = split_fused_rows(
594                            source,
595                            device,
596                            &format!("{p}.mlp.gate_up_proj.weight"),
597                            [m.intermediate_size, m.intermediate_size],
598                        )?;
599                        (gate, up)
600                    }
601                    Err(e) => return Err(e),
602                };
603
604            // Bias loading is presence-driven: HF Qwen2 configs never emit
605            // `attention_bias` (the bias is implicit in the modeling code),
606            // so gating on metadata would silently skip real bias tensors.
607            // Plain llama/smollm checkpoints have none and probe to `None`.
608            let bias = |proj: &str| -> Result<Option<Tensor<B, 1>>> {
609                load_optional_bias(source, device, &format!("{p}.{proj}.bias"))
610            };
611            let (q_bias, k_bias, v_bias) = match fused_qkv_bias {
612                Some([qb, kb, vb]) => (Some(qb), Some(kb), Some(vb)),
613                None => (
614                    bias("self_attn.q_proj")?,
615                    bias("self_attn.k_proj")?,
616                    bias("self_attn.v_proj")?,
617                ),
618            };
619
620            let optional_norm = |name: &str| -> Result<Option<Tensor<B, 1>>> {
621                match source.open_tensor(&format!("{p}.{name}.weight")) {
622                    Ok(reader) => Ok(Some(
623                        reader.load_to_tensor::<B, 1>(device).map_err(ModelError::Format)?,
624                    )),
625                    Err(combs_formats::FormatError::TensorNotFound(_)) => Ok(None),
626                    Err(e) => Err(ModelError::Format(e)),
627                }
628            };
629
630            layers.push(LlamaLayer {
631                q,
632                k,
633                v,
634                o,
635                q_bias,
636                k_bias,
637                v_bias,
638                o_bias: bias("self_attn.o_proj")?,
639                q_norm: if spec.qk_norm { optional_norm("self_attn.q_norm")? } else { None },
640                k_norm: if spec.qk_norm { optional_norm("self_attn.k_norm")? } else { None },
641                gate,
642                up,
643                down: load_linear(source, device, &format!("{p}.mlp.down_proj.weight"))?,
644                input_norm: load_weight(source, device, &format!("{p}.input_layernorm.weight"))?,
645                pre_mlp_norm: load_weight(
646                    source,
647                    device,
648                    &format!("{p}.{pre_mlp_name}.weight"),
649                )?,
650                attn_out_norm: if spec.sandwich_norms {
651                    optional_norm("post_attention_layernorm")?
652                } else {
653                    None
654                },
655                mlp_out_norm: if spec.sandwich_norms {
656                    optional_norm("post_feedforward_layernorm")?
657                } else {
658                    None
659                },
660            });
661        }
662
663        let rotary = RotaryEmbedding::new_scaled(
664            m.head_dim,
665            spec.rope_theta,
666            m.max_position_embeddings,
667            &spec.rope_scaling,
668            device,
669        );
670        let rotary_local = spec.rope_local_theta.map(|theta| {
671            // Local (sliding) layers rotate unscaled at their own base.
672            RotaryEmbedding::new(m.head_dim, theta, m.max_position_embeddings, device)
673        });
674
675        Ok(LlamaModel {
676            scale: 1.0
677                / spec
678                    .query_pre_attn_scalar
679                    .unwrap_or(m.head_dim as f64)
680                    .sqrt(),
681            metadata: m,
682            spec,
683            embed,
684            lm_head,
685            final_norm,
686            layers,
687            rotary,
688            rotary_local,
689        })
690    }
691}