Skip to main content

logicaffeine_compile/model/
bind.rs

1//! Which checkpoint tensor fills each weight an authored program declares, and how.
2//!
3//! The author names a layer's weights the same way for every model (`an3`, `Wq3`, `Qn3`, `Wo3`, `fn3`, `Wg3` …), in
4//! the layout the language reads them: a projection `Y = X matmul W` holds `W` as `in × out`, row-major. A GGUF
5//! checkpoint holds the same projection as `out` rows of `in` weights. The binding names the tensor and the view
6//! that turns one into the other:
7//!
8//!   • a projection is transposed;
9//!   • a stacked expert tensor (`ffn_gate_exps` `[d, dff, E]`) is already the `(E·dff) × d` stack the `moe` sugar
10//!     reads, and the embedding table already `vocab × d`;
11//!   • a checkpoint whose rotation pairs neighbouring dimensions (`2i`, `2i+1` — Llama) has each head's query and key
12//!     rows (and their norm weights) permuted so the pairs fall `dims/2` apart, the pairing the `rope` sugar turns.
13//!     Attention scores are unchanged: the same orthogonal permutation multiplies every query and every key.
14//!
15//!   • a projection that holds several outputs per head (Qwen3.5's gated attention: each head's queries, then its
16//!     gate) is read a part of every head at a time, and one that stacks several projections (Gated DeltaNet's
17//!     `attn_qkv`, its convolution kernel) a range of rows at a time.
18//!
19//! The rotary tables (`RC`/`RS`, `Rcos`/`Rsin`) are not weights: [`rope_tables`] computes them from the spec. The
20//! carried state — a KV cache, a convolution ring, a delta-rule memory — is not read from the checkpoint at all: it
21//! starts at zero ([`Source::State`]).
22//!
23//! A weight a spatial program reads as it is stored Q8_0 is two tensors, its int8 values and its block scales
24//! ([`q8_planes`]), repacked from the checkpoint's blocks through the same view. A rotary table's values are
25//! [`rotary_values`].
26
27use super::quant::{f16_to_f32, GgmlType};
28use super::reference::WeightSource;
29use super::spec::{Attention, Ffn, GatedDelta, Gating, Mixer, Mla, ModelSpec, Rope, RopeScaling};
30
31/// How a declared weight is read out of its tensor.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub enum View {
34    /// The tensor's elements in order.
35    Same,
36    /// The tensor's `out × in` rows as an `in × out` matrix.
37    Transposed,
38    /// Within every head of `head_dim` rows (or elements), the `dims` rotated ones from `from` reordered from
39    /// interleaved pairs to halves; then transposed when `transpose`.
40    NeoxPairs {
41        head_dim: usize,
42        from: usize,
43        dims: usize,
44        transpose: bool,
45    },
46    /// Rows `first .. first + count`, then transposed when `transpose`.
47    RowRange {
48        first: usize,
49        count: usize,
50        transpose: bool,
51    },
52    /// In every group of `stride` rows, the `width` rows starting at `offset` — one part of every head — then
53    /// transposed when `transpose`.
54    HeadPart {
55        stride: usize,
56        offset: usize,
57        width: usize,
58        transpose: bool,
59    },
60    /// In every group of `stride` rows, the `width` rows starting at `offset` — one head's block — each block
61    /// transposed on its own and the transposes stacked: the weight of a per-head matmul whose checkpoint rows are
62    /// each head's outputs.
63    HeadBlocks {
64        stride: usize,
65        offset: usize,
66        width: usize,
67    },
68    /// One part of a fused query-key-value projection: rows `first .. first + count` of its weight, read transposed, or
69    /// elements `first .. first + count` of its bias (`bias`); within the part, each head of `head_dim` with its first
70    /// `dims` dimensions reordered from interleaved pairs to halves (none when `dims` is 0).
71    FusedPart {
72        first: usize,
73        count: usize,
74        head_dim: usize,
75        dims: usize,
76        bias: bool,
77    },
78}
79
80/// One declared weight's source.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct Binding {
83    pub tensor: String,
84    pub view: View,
85}
86
87/// Where a declared name's values come from.
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub enum Source {
90    /// A checkpoint tensor, through a view.
91    Weight(Binding),
92    /// Carried state, zero before the first position.
93    State,
94}
95
96/// Why a declared name has no source.
97#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct Unbound(pub String);
99
100/// The base and layer of an authored weight name: `Wq12` is `("Wq", Some(12))`, `Emb` is `("Emb", None)`.
101fn split(var: &str) -> (&str, Option<usize>) {
102    let digits = var.len() - var.trim_end_matches(|c: char| c.is_ascii_digit()).len();
103    if digits == 0 {
104        return (var, None);
105    }
106    let (base, l) = var.split_at(var.len() - digits);
107    (base, l.parse().ok())
108}
109
110/// Where `var` is read from, for a model of `spec`.
111pub fn bind(spec: &ModelSpec, var: &str) -> Result<Source, Unbound> {
112    let unbound = || Unbound(var.to_string());
113    let t = |name: String, view: View| Ok(Source::Weight(Binding { tensor: name, view }));
114    let (base, layer) = split(var);
115    let Some(l) = layer else {
116        return match base {
117            "Emb" => t("token_embd.weight".into(), View::Same),
118            "on" => t("output_norm.weight".into(), View::Same),
119            "Wout" if spec.tied_output => t("token_embd.weight".into(), View::Transposed),
120            "Wout" => t("output.weight".into(), View::Transposed),
121            _ => Err(unbound()),
122        };
123    };
124    let layer = spec.layers.get(l).ok_or_else(unbound)?;
125    let blk = |suffix: &str| format!("blk.{l}.{suffix}.weight");
126    match base {
127        "an" => return t(blk("attn_norm"), View::Same),
128        "fn" => return t(blk(spec.ffn_norm), View::Same),
129        "pan" if layer.post_mixer_norm => return t(blk("post_attention_norm"), View::Same),
130        "pfn" if layer.post_ffn_norm => return t(blk("post_ffw_norm"), View::Same),
131        _ => {}
132    }
133    let mixer = match &layer.mixer {
134        Mixer::Attention(a) => attention(a, base, l, &blk),
135        Mixer::GatedDelta(g) => gated_delta(g, base, l, &blk),
136        Mixer::Mla(m) => mla(m, base, &blk),
137    };
138    if let Some(found) = mixer {
139        return Ok(found);
140    }
141    match (base, &layer.ffn) {
142        ("Wg", Ffn::Dense { .. }) => t(blk("ffn_gate"), View::Transposed),
143        ("Wu", Ffn::Dense { .. }) => t(blk("ffn_up"), View::Transposed),
144        ("Wd", Ffn::Dense { .. }) => t(blk("ffn_down"), View::Transposed),
145        ("Wr", Ffn::Moe(_)) => t(blk("ffn_gate_inp"), View::Transposed),
146        ("Wg", Ffn::Moe(_)) => t(blk("ffn_gate_exps"), View::Same),
147        ("Wu", Ffn::Moe(_)) => t(blk("ffn_up_exps"), View::Same),
148        ("Wd", Ffn::Moe(_)) => t(blk("ffn_down_exps"), View::Same),
149        ("Wsg", Ffn::Moe(m)) if m.shared_experts > 0 => t(blk("ffn_gate_shexp"), View::Transposed),
150        ("Wsu", Ffn::Moe(m)) if m.shared_experts > 0 => t(blk("ffn_up_shexp"), View::Transposed),
151        ("Wsd", Ffn::Moe(m)) if m.shared_experts > 0 => t(blk("ffn_down_shexp"), View::Transposed),
152        // The shared experts' gate: one output row over the input, read as the column the program declares.
153        ("Wsgi", Ffn::Moe(m)) if m.shared_gate => t(blk("ffn_gate_inp_shexp"), View::Transposed),
154        ("Br", Ffn::Moe(m)) if m.biases => t(format!("blk.{l}.ffn_gate_inp.bias"), View::Same),
155        ("Bg", Ffn::Moe(m)) if m.biases => t(format!("blk.{l}.ffn_gate_exps.bias"), View::Same),
156        ("Bu", Ffn::Moe(m)) if m.biases => t(format!("blk.{l}.ffn_up_exps.bias"), View::Same),
157        ("Bd", Ffn::Moe(m)) if m.biases => t(format!("blk.{l}.ffn_down_exps.bias"), View::Same),
158        ("Bs", Ffn::Moe(m)) if m.gating == (Gating::Sigmoid { bias: true }) => t(format!("blk.{l}.exp_probs_b.bias"), View::Same),
159        _ => Err(unbound()),
160    }
161}
162
163fn weight(tensor: String, view: View) -> Option<Source> {
164    Some(Source::Weight(Binding { tensor, view }))
165}
166
167/// An attention block's names: its projections and their biases, QK-norm weights, sinks and KV cache.
168fn attention(a: &Attention, base: &str, l: usize, blk: &dyn Fn(&str) -> String) -> Option<Source> {
169    let interleaved = a.rope.as_ref().filter(|r| !r.neox).map(|r| r.dims);
170    let hd = a.head_dim;
171    // A per-head vector of an interleaved checkpoint reaches the program in the halves basis its projections do.
172    let per_head = || match interleaved {
173        Some(dims) => View::NeoxPairs { head_dim: hd, from: 0, dims, transpose: false },
174        None => View::Same,
175    };
176    match base {
177        // A fused projection holds each output row's queries, then its keys, then its values; its bias likewise.
178        "Wq" | "Wk" | "Wv" | "Bq" | "Bk" | "Bv" if a.fused_qkv && (a.bias || base.starts_with('W')) => {
179            let (aq, akv, vd) = (a.heads * hd, a.kv_heads * hd, a.value_dim);
180            let (first, count, head_dim, dims) = match &base[1..] {
181                "q" => (0, aq, hd, interleaved.unwrap_or(0)),
182                "k" => (aq, akv, hd, interleaved.unwrap_or(0)),
183                _ => (aq + akv, a.kv_heads * vd, vd, 0),
184            };
185            let bias = base.starts_with('B');
186            let tensor = if bias { format!("blk.{l}.attn_qkv.bias") } else { blk("attn_qkv") };
187            weight(tensor, View::FusedPart { first, count, head_dim, dims, bias })
188        }
189        "Bq" | "Bk" if a.bias => weight(format!("blk.{l}.{}.bias", if base == "Bq" { "attn_q" } else { "attn_k" }), per_head()),
190        "Bv" if a.bias => weight(format!("blk.{l}.attn_v.bias"), View::Same),
191        "Bo" if a.output_bias => weight(format!("blk.{l}.attn_output.bias"), View::Same),
192        "Sk" if a.sinks => weight(blk("attn_sinks"), View::Same),
193        "Wq" if a.output_gate => {
194            weight(blk("attn_q"), View::HeadPart { stride: 2 * hd, offset: 0, width: hd, transpose: true })
195        }
196        "Wqg" if a.output_gate => {
197            weight(blk("attn_q"), View::HeadPart { stride: 2 * hd, offset: hd, width: hd, transpose: true })
198        }
199        "Wq" | "Wk" => {
200            let view = match interleaved {
201                Some(dims) => View::NeoxPairs { head_dim: hd, from: 0, dims, transpose: true },
202                None => View::Transposed,
203            };
204            weight(blk(if base == "Wq" { "attn_q" } else { "attn_k" }), view)
205        }
206        "Qn" | "Kn" => weight(blk(if base == "Qn" { "attn_q_norm" } else { "attn_k_norm" }), per_head()),
207        "Wv" => weight(blk("attn_v"), View::Transposed),
208        "Wo" => weight(blk("attn_output"), View::Transposed),
209        "Kc" | "Vc" => Some(Source::State),
210        _ => None,
211    }
212}
213
214/// A latent attention block's names: the query projection (or its two halves and their norm), the latent's projection
215/// and norm, the expansion's key and value halves as per-head matmul weights, the output projection and the cache.
216///
217///   • `Wq` (or `Wqa`, `Qan`, `Wqb`) each head's `qk_nope` unrotated then `qk_rope` rotated query dimensions, and `Wkva`
218///     the latent's `kv_lora_rank` then the shared rotated key's `qk_rope` — an interleaved rotation's dimensions
219///     reordered to halves where they sit, as an attention's are;
220///   • `Wkb` (`(heads·qk_nope) × kv_lora_rank`) absorbs each head's unrotated query into the latent: the key half of
221///     `attn_kv_b` read as it is stored (its rows are the absorption's inputs), or `attn_k_b`'s blocks transposed;
222///   • `Wvb` (`(heads·kv_lora_rank) × v_dim`) expands each head's attended latent into its values: the value half of
223///     `attn_kv_b`, or `attn_v_b`, each head's block transposed.
224fn mla(m: &Mla, base: &str, blk: &dyn Fn(&str) -> String) -> Option<Source> {
225    let (nope, rope, lora, vd) = (m.qk_nope, m.qk_rope, m.kv_lora_rank, m.v_dim);
226    let rotated = |head_dim: usize, from: usize| match m.rope.neox {
227        true => View::Transposed,
228        false => View::NeoxPairs { head_dim, from, dims: rope, transpose: true },
229    };
230    match base {
231        "Wq" if m.q_lora_rank.is_none() => weight(blk("attn_q"), rotated(nope + rope, nope)),
232        "Wqa" if m.q_lora_rank.is_some() => weight(blk("attn_q_a"), View::Transposed),
233        "Qan" if m.q_lora_rank.is_some() => weight(blk("attn_q_a_norm"), View::Same),
234        "Wqb" if m.q_lora_rank.is_some() => weight(blk("attn_q_b"), rotated(nope + rope, nope)),
235        "Wkva" => weight(blk("attn_kv_a_mqa"), rotated(lora + rope, lora)),
236        "Kan" => weight(blk("attn_kv_a_norm"), View::Same),
237        "Wkb" if m.kv_b_split => weight(blk("attn_k_b"), View::HeadBlocks { stride: lora, offset: 0, width: lora }),
238        "Wkb" => weight(blk("attn_kv_b"), View::HeadPart { stride: nope + vd, offset: 0, width: nope, transpose: false }),
239        "Wvb" if m.kv_b_split => weight(blk("attn_v_b"), View::HeadBlocks { stride: vd, offset: 0, width: vd }),
240        "Wvb" => weight(blk("attn_kv_b"), View::HeadBlocks { stride: nope + vd, offset: nope, width: vd }),
241        "Wo" => weight(blk("attn_output"), View::Transposed),
242        "Kc" => Some(Source::State),
243        _ => None,
244    }
245}
246
247/// A Gated DeltaNet block's names: the `q|k|v` projection and convolution split by rows, the gate, the decay and
248/// write-strength projections, and the carried convolution rings and memory.
249fn gated_delta(g: &GatedDelta, base: &str, l: usize, blk: &dyn Fn(&str) -> String) -> Option<Source> {
250    let (qk, vw) = (g.k_heads * g.head_dim, g.v_heads * g.head_dim);
251    let rows = |first, count| View::RowRange { first, count, transpose: true };
252    match base {
253        "Wq" => weight(blk("attn_qkv"), rows(0, qk)),
254        "Wk" => weight(blk("attn_qkv"), rows(qk, qk)),
255        "Wv" => weight(blk("attn_qkv"), rows(2 * qk, vw)),
256        "Wz" => weight(blk("attn_gate"), View::Transposed),
257        "Wcq" => weight(blk("ssm_conv1d"), rows(0, qk)),
258        "Wck" => weight(blk("ssm_conv1d"), rows(qk, qk)),
259        "Wcv" => weight(blk("ssm_conv1d"), rows(2 * qk, vw)),
260        "Wb" => weight(blk("ssm_beta"), View::Transposed),
261        "Wa" => weight(blk("ssm_alpha"), View::Transposed),
262        "Dt" => weight(format!("blk.{l}.ssm_dt.bias"), View::Same),
263        "Al" => weight(format!("blk.{l}.ssm_a"), View::Same),
264        "Gn" => weight(blk("ssm_norm"), View::Same),
265        "Wo" => weight(blk("ssm_out"), View::Transposed),
266        "Csq" | "Csk" | "Csv" | "St" => Some(Source::State),
267        _ => None,
268    }
269}
270
271/// The source position of element `i` of a head's rotated block under [`View::NeoxPairs`]: halves `[0, dims/2)` and
272/// `[dims/2, dims)` take the even and the odd members of the interleaved pairs; dimensions past `dims` stay put.
273pub(crate) fn neox_source(i: usize, dims: usize) -> usize {
274    let half = dims / 2;
275    match i {
276        i if i < half => 2 * i,
277        i if i < dims => 2 * (i - half) + 1,
278        i => i,
279    }
280}
281
282/// [`neox_source`] for a head whose rotated block starts at dimension `from`: the dimensions before it stay put.
283pub(crate) fn neox_source_from(i: usize, from: usize, dims: usize) -> usize {
284    if i < from {
285        i
286    } else {
287        from + neox_source(i - from, dims)
288    }
289}
290
291/// `v` (whole heads of `head_dim`) with each head's rotated `dims` reordered from interleaved pairs to halves — the
292/// basis a [`View::NeoxPairs`]-bound program's queries and keys live in.
293pub fn neox_order(v: &[f64], head_dim: usize, dims: usize) -> Vec<f64> {
294    (0..v.len())
295        .map(|i| v[(i / head_dim) * head_dim + neox_source(i % head_dim, dims)])
296        .collect()
297}
298
299/// A declared weight's values, in the layout the program declares it (`rows × cols` row-major, [`View::declared`]).
300pub fn values(b: &Binding, source: &mut dyn WeightSource) -> std::io::Result<Vec<f64>> {
301    let (ty, inner, rows, raw) = stored(b, source)?;
302    let mut flat = Vec::with_capacity(inner * rows);
303    ty.dequantize(&raw, &mut flat);
304    Ok(b.view.apply(flat.into_iter().map(f64::from).collect(), inner, rows))
305}
306
307/// The tensor `b` binds as it is stored: its format, its row length, its row count and its bytes.
308fn stored(b: &Binding, source: &mut dyn WeightSource) -> std::io::Result<(GgmlType, usize, usize, Vec<u8>)> {
309    let (ty, dims) = source.describe(&b.tensor)?;
310    let inner = dims.first().copied().unwrap_or(1) as usize;
311    let rows = dims.iter().product::<u64>() as usize / inner.max(1);
312    let raw = source.row_bytes(&b.tensor, 0, rows)?;
313    Ok((ty, inner, rows, raw))
314}
315
316impl View {
317    /// The `rows × cols` layout the view reads a tensor of `rows` rows of `inner` elements into: the layout the program
318    /// declares the weight.
319    pub fn declared(&self, inner: usize, rows: usize) -> (usize, usize) {
320        let laid = |n: usize, transpose: bool| if transpose { (inner, n) } else { (n, inner) };
321        match *self {
322            View::Same => (rows, inner),
323            View::Transposed => (inner, rows),
324            View::NeoxPairs { transpose, .. } => laid(rows, transpose),
325            View::RowRange { count, transpose, .. } => laid(count, transpose),
326            View::HeadPart { stride, offset, width, transpose } => {
327                laid((0..rows).filter(|r| (offset..offset + width).contains(&(r % stride))).count(), transpose)
328            }
329            View::HeadBlocks { stride, width, .. } => (rows / stride.max(1) * inner, width),
330            View::FusedPart { count, bias: true, .. } => (1, count),
331            View::FusedPart { count, bias: false, .. } => (inner, count),
332        }
333    }
334
335    /// Whether the view lays each checkpoint row down a declared column, so a Q8_0 tensor's blocks run down the declared
336    /// rows: the layout a spatial program reads a Q8_0 weight in ([`q8_planes`]).
337    pub fn keeps_blocks_down_rows(&self) -> bool {
338        match *self {
339            View::Same => false,
340            View::Transposed | View::HeadBlocks { .. } => true,
341            View::NeoxPairs { transpose, .. } | View::RowRange { transpose, .. } | View::HeadPart { transpose, .. } => transpose,
342            View::FusedPart { bias, .. } => !bias,
343        }
344    }
345
346    /// `flat`, a tensor of `rows` rows of `inner` elements, in the layout the view reads it into.
347    fn apply(&self, flat: Vec<f64>, inner: usize, rows: usize) -> Vec<f64> {
348        let transpose = |m: &[f64]| -> Vec<f64> {
349            (0..inner)
350                .flat_map(|c| (0..rows).map(move |r| (r, c)))
351                .map(|(r, c)| m[r * inner + c])
352                .collect()
353        };
354        match self {
355            View::Same => flat,
356            View::Transposed => transpose(&flat),
357            View::NeoxPairs {
358                head_dim,
359                from,
360                dims,
361                transpose: t,
362            } => {
363                let source = |i: usize| (i / head_dim) * head_dim + neox_source_from(i % head_dim, *from, *dims);
364                let permuted: Vec<f64> = if *t {
365                    // Rows are output channels: row `h·head_dim + i` takes the source row `h·head_dim + neox_source(i)`.
366                    (0..rows).flat_map(|r| flat[source(r) * inner..(source(r) + 1) * inner].to_vec()).collect()
367                } else {
368                    (0..flat.len()).map(|i| flat[source(i)]).collect()
369                };
370                if *t {
371                    transpose(&permuted)
372                } else {
373                    permuted
374                }
375            }
376            &View::RowRange { first, count, transpose: t } => {
377                let picked = flat[first * inner..(first + count) * inner].to_vec();
378                if t {
379                    transpose_rows(&picked, count, inner)
380                } else {
381                    picked
382                }
383            }
384            &View::HeadPart { stride, offset, width, transpose: t } => {
385                let picked: Vec<f64> = (0..rows)
386                    .filter(|r| (offset..offset + width).contains(&(r % stride)))
387                    .flat_map(|r| flat[r * inner..(r + 1) * inner].to_vec())
388                    .collect();
389                let n = picked.len() / inner.max(1);
390                if t {
391                    transpose_rows(&picked, n, inner)
392                } else {
393                    picked
394                }
395            }
396            &View::HeadBlocks { stride, offset, width } => (0..rows / stride.max(1))
397                .flat_map(|h| {
398                    let first = h * stride + offset;
399                    transpose_rows(&flat[first * inner..(first + width) * inner], width, inner)
400                })
401                .collect(),
402            &View::FusedPart { first, count, head_dim, dims, bias } => {
403                let source = |i: usize| first + (i / head_dim) * head_dim + neox_source(i % head_dim, dims);
404                if bias {
405                    (0..count).map(|i| flat[source(i)]).collect()
406                } else {
407                    let picked: Vec<f64> = (0..count).flat_map(|r| flat[source(r) * inner..(source(r) + 1) * inner].to_vec()).collect();
408                    transpose_rows(&picked, count, inner)
409                }
410            }
411        }
412    }
413}
414
415/// Whether the weight `b` binds is stored as the two planes [`q8_planes`] repacks — Q8_0, its blocks down its declared
416/// rows — read from the checkpoint's description of the tensor alone, never its bytes.
417pub fn stored_as_q8_planes(b: &Binding, source: &dyn WeightSource) -> std::io::Result<bool> {
418    Ok(source.describe(&b.tensor)?.0 == GgmlType::Q8_0 && b.view.keeps_blocks_down_rows())
419}
420
421/// A Q8_0 weight as a spatial program reads it (`spatial::passes::from_q8`): the int8 values in the layout the program
422/// declares the weight, `K × N` row-major, and one scale per 32 rows of each column, `K/32 × N`.
423#[derive(Clone, Debug, PartialEq)]
424pub struct Q8Planes {
425    /// `K`, the declared rows.
426    pub rows: usize,
427    /// `N`, the declared columns.
428    pub cols: usize,
429    /// The int8 values, `K × N`.
430    pub values: Vec<f64>,
431    /// The scales, `K/32 × N`.
432    pub scales: Vec<f64>,
433}
434
435/// The planes of the Q8_0 weight `b` binds, repacked from the checkpoint's blocks: what a bundle manifest names
436/// `q8_0.values` and `q8_0.scales`. The view moves each block's int8 values as [`values`] moves the weights they
437/// scale, and a plane block's scale is the scale of the one checkpoint block its 32 rows come from, so
438/// `values[k][n] · scales[k/32][n]` is the weight [`values`] reads. Refused: a tensor stored in another format,
439/// declared rows that are not whole blocks of 32, and a view that lays a checkpoint block along the declared columns.
440pub fn q8_planes(b: &Binding, source: &mut dyn WeightSource) -> std::io::Result<Q8Planes> {
441    let refuse = |why: String| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("`{}`: {why}", b.tensor));
442    let (ty, inner, rows, raw) = stored(b, source)?;
443    if ty != GgmlType::Q8_0 {
444        return Err(refuse(format!("stored {}, so it has no Q8_0 planes to repack", ty.name())));
445    }
446    let (k, n) = b.view.declared(inner, rows);
447    let len = ty.block_len();
448    if k % len != 0 {
449        return Err(refuse(format!("declared {k} rows, not whole Q8_0 blocks of {len}")));
450    }
451    // Each element's int8 value, and the index of the block it is stored in: the view moves both as it moves a weight.
452    let (mut q, mut block, mut scale) = (Vec::with_capacity(inner * rows), Vec::with_capacity(inner * rows), Vec::new());
453    for (i, bytes) in raw.chunks_exact(ty.block_bytes()).enumerate() {
454        scale.push(f64::from(f16_to_f32(u16::from_le_bytes([bytes[0], bytes[1]]))));
455        q.extend(bytes[2..].iter().map(|&v| f64::from(v as i8)));
456        block.extend(std::iter::repeat(i as f64).take(len));
457    }
458    let (values, block) = (b.view.apply(q, inner, rows), b.view.apply(block, inner, rows));
459    let mut scales = Vec::with_capacity(k / len * n);
460    for kb in 0..k / len {
461        for c in 0..n {
462            let id = block[len * kb * n + c];
463            if let Some(r) = (len * kb..len * (kb + 1)).find(|&r| block[r * n + c] != id) {
464                return Err(refuse(format!(
465                    "declared rows {}..{} of column {c} are not one Q8_0 block of the checkpoint: row {r} comes from another",
466                    len * kb,
467                    len * (kb + 1)
468                )));
469            }
470            scales.push(scale[id as usize]);
471        }
472    }
473    Ok(Q8Planes { rows: k, cols: n, values, scales })
474}
475
476/// `rows × cols` row-major as `cols × rows` row-major.
477fn transpose_rows(m: &[f64], rows: usize, cols: usize) -> Vec<f64> {
478    (0..cols).flat_map(|c| (0..rows).map(move |r| m[r * cols + c])).collect()
479}
480
481/// The rotation a layer's mixer turns by: an attention's, when it has one, and a latent attention's.
482fn rope_of(mixer: &Mixer) -> Option<&Rope> {
483    match mixer {
484        Mixer::Attention(Attention { rope, .. }) => rope.as_ref(),
485        Mixer::Mla(m) => Some(&m.rope),
486        Mixer::GatedDelta(_) => None,
487    }
488}
489
490/// The distinct rotations `spec`'s attention layers turn by, in the order of the first layer to turn by each: a
491/// program declares one pair of rotary tables per rotation (Gemma's local and global layers turn by two bases).
492pub fn rotations(spec: &ModelSpec) -> Vec<&Rope> {
493    let mut out: Vec<&Rope> = Vec::new();
494    for r in spec.layers.iter().filter_map(|l| rope_of(&l.mixer)) {
495        if !out.contains(&r) {
496            out.push(r);
497        }
498    }
499    out
500}
501
502/// The rotation layer `l` turns by, as its index in [`rotations`] — `None` for a layer that does not rotate.
503pub fn rotation_of(spec: &ModelSpec, l: usize) -> Option<usize> {
504    let r = rope_of(&spec.layers.get(l)?.mixer)?;
505    rotations(spec).iter().position(|x| *x == r)
506}
507
508/// A rotary table's role: which rotation, a decode step's caches (`cached`) or a forward's one row, cosines or sines.
509#[derive(Clone, Copy, Debug, PartialEq, Eq)]
510pub struct RotaryTable {
511    pub rotation: usize,
512    pub cached: bool,
513    pub cos: bool,
514}
515
516impl RotaryTable {
517    /// The table's declared name: `Rcos`/`Rsin` (a forward's `RC`/`RS`) for the first rotation, the rotation's index
518    /// after it for each later one (`Rcos1`, `RS2`, …).
519    pub fn name(self) -> String {
520        let base = match (self.cached, self.cos) {
521            (true, true) => "Rcos",
522            (true, false) => "Rsin",
523            (false, true) => "RC",
524            (false, false) => "RS",
525        };
526        match self.rotation {
527            0 => base.to_string(),
528            k => format!("{base}{k}"),
529        }
530    }
531
532    /// The table `name` names, when it names one.
533    pub fn named(name: &str) -> Option<RotaryTable> {
534        let (cached, cos, rest) = [("Rcos", true, true), ("Rsin", true, false), ("RC", false, true), ("RS", false, false)]
535            .into_iter()
536            .find_map(|(base, cached, cos)| name.strip_prefix(base).map(|rest| (cached, cos, rest)))?;
537        let rotation = match rest {
538            "" => 0,
539            k if !k.starts_with('0') && k.bytes().all(|b| b.is_ascii_digit()) => k.parse().ok()?,
540            _ => return None,
541        };
542        Some(RotaryTable { rotation, cached, cos })
543    }
544}
545
546/// A table the host computes from the spec rather than reads from the checkpoint.
547#[derive(Clone, Copy, Debug, PartialEq, Eq)]
548pub enum HostTable {
549    /// A rotation's cosines or sines ([`RotaryTable`]).
550    Rotary(RotaryTable),
551    /// Llama 4's attention temperature as a rotation of `half` pairs: cosines `Temperature::at(p)` at every pair, sines
552    /// zero — a decode step's caches (`cached`, row `p` for position `p`) or a forward's one row (position 0). Read by
553    /// the rope sugar, it scales each query row by its own position's factor.
554    Temperature { cached: bool, cos: bool, half: usize },
555    /// A row of `n` ones: the weight of an RMSNorm that has none (Llama 4's QK norm), and the row a shared expert's
556    /// gate is spread across (Qwen's mixtures).
557    Ones(usize),
558}
559
560impl HostTable {
561    /// The table's declared name: a rotary table's own, `Tcos{half}`/`Tsin{half}` (a forward's `TC{half}`/`TS{half}`),
562    /// `Ones{n}`.
563    pub fn name(self) -> String {
564        match self {
565            HostTable::Rotary(r) => r.name(),
566            HostTable::Temperature { cached, cos, half } => {
567                let base = match (cached, cos) {
568                    (true, true) => "Tcos",
569                    (true, false) => "Tsin",
570                    (false, true) => "TC",
571                    (false, false) => "TS",
572                };
573                format!("{base}{half}")
574            }
575            HostTable::Ones(n) => format!("Ones{n}"),
576        }
577    }
578
579    /// The table `name` names, when it names one.
580    pub fn named(name: &str) -> Option<HostTable> {
581        if let Some(r) = RotaryTable::named(name) {
582            return Some(HostTable::Rotary(r));
583        }
584        let count = |rest: &str| -> Option<usize> {
585            (!rest.is_empty() && !rest.starts_with('0') && rest.bytes().all(|b| b.is_ascii_digit())).then(|| rest.parse().ok()).flatten()
586        };
587        if let Some(n) = name.strip_prefix("Ones").and_then(count) {
588            return Some(HostTable::Ones(n));
589        }
590        [("Tcos", true, true), ("Tsin", true, false), ("TC", false, true), ("TS", false, false)]
591            .into_iter()
592            .find_map(|(base, cached, cos)| name.strip_prefix(base).and_then(count).map(|half| HostTable::Temperature { cached, cos, half }))
593    }
594}
595
596/// The values of the host table `var` names ([`HostTable::named`]) for a model of `spec`: a decode step's caches of
597/// `positions` rows, or a forward's one row (position 0). `None` when `var` names no table.
598pub fn host_values(spec: &ModelSpec, var: &str, positions: usize, source: &mut dyn WeightSource) -> std::io::Result<Option<Vec<f64>>> {
599    match HostTable::named(var) {
600        None => Ok(None),
601        Some(HostTable::Rotary(_)) => rotary_values(spec, var, positions, source),
602        Some(table) => computed_values(spec, table, positions).map(Some).ok_or_else(|| {
603            std::io::Error::new(std::io::ErrorKind::InvalidData, format!("`{var}`: the model has no attention temperature"))
604        }),
605    }
606}
607
608/// The values of a host table the spec alone gives (every one but a rotary table, whose frequency factors a checkpoint
609/// may hold): a decode step's caches of `positions` rows, or a forward's one row. `None` for a rotary table, or for a
610/// temperature table of a model that has none.
611pub fn computed_values(spec: &ModelSpec, table: HostTable, positions: usize) -> Option<Vec<f64>> {
612    match table {
613        HostTable::Rotary(_) => None,
614        HostTable::Ones(n) => Some(vec![1.0; n]),
615        HostTable::Temperature { cached, cos, half } => {
616            let t = temperature(spec)?;
617            let rows = if cached { positions } else { 1 };
618            Some((0..rows).flat_map(|p| std::iter::repeat_n(if cos { t.at(p) } else { 0.0 }, half)).collect())
619        }
620    }
621}
622
623/// The host tables beyond the rotary ones that a decode step of `spec` reads, each once: a row of ones as wide as each
624/// head that a layer norms without a weight, a row of ones as wide as the model that spreads a shared expert's gate
625/// across its row, and the cosines and sines of the attention temperature for each head width that takes one.
626pub fn computed_tables(spec: &ModelSpec) -> Vec<HostTable> {
627    let mut out = Vec::new();
628    for l in &spec.layers {
629        let mut add = |t: HostTable| {
630            if !out.contains(&t) {
631                out.push(t);
632            }
633        };
634        if matches!(&l.ffn, Ffn::Moe(m) if m.shared_gate) {
635            add(HostTable::Ones(spec.d));
636        }
637        let Mixer::Attention(a) = &l.mixer else { continue };
638        if a.qk_unit_norm {
639            add(HostTable::Ones(a.head_dim));
640        }
641        if a.temperature.is_some() {
642            for cos in [true, false] {
643                add(HostTable::Temperature { cached: true, cos, half: a.head_dim / 2 });
644            }
645        }
646    }
647    out
648}
649
650/// The attention temperature `spec`'s layers take — one for the model, as every family that has one sets it.
651pub fn temperature(spec: &ModelSpec) -> Option<crate::model::spec::Temperature> {
652    spec.layers.iter().find_map(|l| match &l.mixer {
653        Mixer::Attention(a) => a.temperature,
654        _ => None,
655    })
656}
657
658/// The rotary tables for positions `0..rows`: row `p` holds the `(cos, sin)` of each of the `dims/2` frequencies
659/// ([`Rope::rotation`] — the checkpoint's per-frequency factors, its sections, linear and YaRN scaling, the
660/// magnitude folded into both).
661pub fn rope_tables(rope: &Rope, rows: usize, factors: Option<&[f64]>) -> (Vec<f64>, Vec<f64>) {
662    let half = rope.dims / 2;
663    let (mut cos, mut sin) = (Vec::with_capacity(rows * half), Vec::with_capacity(rows * half));
664    for p in 0..rows {
665        for i in 0..half {
666            let factor = match (&rope.scaling, factors) {
667                (RopeScaling::Factors, Some(fs)) => Some(fs[i]),
668                _ => None,
669            };
670            let (c, s) = rope.rotation(i, p, factor);
671            cos.push(c);
672            sin.push(s);
673        }
674    }
675    (cos, sin)
676}
677
678/// The values of the rotary table `var` names ([`RotaryTable::named`]) for a model of `spec`: a decode step's caches of
679/// `positions` rows, or a forward's one row (position 0), of the rotation the name gives, the frequency factors read
680/// from `source` when the rotation reads the checkpoint's (`rope_freqs.weight`). `None` when `var` names no table.
681pub fn rotary_values(spec: &ModelSpec, var: &str, positions: usize, source: &mut dyn WeightSource) -> std::io::Result<Option<Vec<f64>>> {
682    let Some(table) = RotaryTable::named(var) else { return Ok(None) };
683    let rope = *rotations(spec).get(table.rotation).ok_or_else(|| {
684        std::io::Error::new(std::io::ErrorKind::InvalidData, format!("`{var}`: the model has no rotation {}", table.rotation))
685    })?;
686    let factors = match rope.scaling {
687        RopeScaling::Factors => Some(values(&Binding { tensor: "rope_freqs.weight".into(), view: View::Same }, source)?),
688        _ => None,
689    };
690    let (cos, sin) = rope_tables(rope, if table.cached { positions } else { 1 }, factors.as_deref());
691    Ok(Some(if table.cos { cos } else { sin }))
692}