Skip to main content

logicaffeine_compile/model/
layout.rs

1//! A checkpoint presented in the layout the spec's arms read.
2//!
3//! Qwen3-Next and Qwen3.5 compute one Gated DeltaNet, but a Qwen3-Next checkpoint (llama.cpp's `qwen3next`, as its
4//! converter writes it) pairs its value heads with its key heads grouped — value head `k·r + i` reads key head `k`, `r`
5//! value heads a key head — and keeps each key head's betas and alphas in one projection, `ssm_ba`. Qwen3.5's tiles them
6//! — value head `t` reads key head `t mod k` — and keeps `ssm_beta` and `ssm_alpha` apart. [`presented`] gives a
7//! Qwen3-Next checkpoint Qwen3.5's layout: every value-head axis reordered so tiled position `t` holds grouped head
8//! `(t mod k)·r + ⌊t/k⌋`, the head its key head pairs it with either way, and `ssm_ba` split. The spec, the author, the
9//! binder, the reference and every lowering then read one Gated DeltaNet; the spec refuses a checkpoint still holding
10//! `ssm_ba`, so a grouped layout never reaches them unpresented.
11//!
12//! A Phi-3 or GLM-4 checkpoint (llama.cpp's `phi3`, `glm4`) keeps each block's gate and up projections as one `ffn_up`
13//! twice `feed_forward_length` wide, the gate's rows first; [`presented`] gives it `ffn_gate` and `ffn_up` apart, as
14//! every other dense SwiGLU stores them, and the spec refuses such a checkpoint read raw.
15//!
16//! A GLM-4.5 checkpoint (llama.cpp's `glm4moe`) stores each block's FFN norm as `post_attention_norm`, the name other
17//! families give the norm on the attention's output; [`presented`] names it `ffn_norm`, and the spec refuses such a
18//! checkpoint read raw.
19
20use std::collections::BTreeMap;
21
22use super::gguf::MetaValue;
23use super::quant::GgmlType;
24use super::reference::WeightSource;
25use super::spec::SpecSource;
26use super::Checkpoint;
27
28/// `c` presented in the layout the spec's arms read: a Qwen3-Next checkpoint in Qwen3.5's, a Phi-3 or GLM-4 one with its
29/// gate and up projections apart, a GLM-4.5 one with its FFN norms named `ffn_norm`, every other as it is.
30pub fn presented(c: Box<dyn Checkpoint>) -> std::io::Result<Box<dyn Checkpoint>> {
31    if c.arch() == "qwen3next" && c.tensor_names().iter().any(|n| n.ends_with(".ssm_ba.weight")) {
32        Ok(Box::new(Presented::tiled(c)?))
33    } else if matches!(c.arch(), "phi3" | "glm4") && fused_gate_up(&*c) {
34        Ok(Box::new(Presented::split_gate_up(c)?))
35    } else if c.arch() == "glm4moe" && c.tensor_names().iter().any(|n| n.ends_with(".post_attention_norm.weight")) {
36        Ok(Box::new(Presented::renamed(c, ".post_attention_norm.weight", ".ffn_norm.weight")))
37    } else {
38        Ok(c)
39    }
40}
41
42/// Whether some block of `c` keeps its gate and up projections as one `ffn_up`, with no `ffn_gate` of its own.
43fn fused_gate_up(c: &dyn Checkpoint) -> bool {
44    let names = c.tensor_names();
45    names.iter().any(|n| n.ends_with(".ffn_up.weight") && !names.contains(&n.replace(".ffn_up.", ".ffn_gate.").as_str()))
46}
47
48/// What one presented tensor reads of the stored one.
49#[derive(Clone, Debug)]
50enum Read {
51    /// Presented row `r` is stored row `rows[r]`.
52    Rows { src: String, rows: Vec<usize> },
53    /// Every row's elements in chunks of `chunk`, presented chunk `c` the stored chunk `order[c]`.
54    Chunks { src: String, chunk: usize, order: Vec<usize> },
55    /// The stored tensor `src` as it is, under another name.
56    Same { src: String },
57}
58
59/// A checkpoint presented in the layout the spec's arms read.
60struct Presented {
61    inner: Box<dyn Checkpoint>,
62    names: Vec<String>,
63    dims: BTreeMap<String, Vec<u64>>,
64    reads: BTreeMap<String, Read>,
65}
66
67impl Presented {
68    /// A Phi-3 or GLM-4 checkpoint with each fused `ffn_up` read as `ffn_gate` (its first `feed_forward_length` rows) and
69    /// `ffn_up` (the rest).
70    fn split_gate_up(inner: Box<dyn Checkpoint>) -> std::io::Result<Presented> {
71        let bad = |why: String| std::io::Error::new(std::io::ErrorKind::InvalidData, why);
72        let ff = inner
73            .arch_meta("feed_forward_length")
74            .and_then(MetaValue::as_u64)
75            .ok_or_else(|| bad(format!("{}.feed_forward_length is missing", inner.arch())))?;
76        let all: Vec<String> = inner.tensor_names().iter().map(|n| n.to_string()).collect();
77        let mut names = Vec::new();
78        let mut dims = BTreeMap::new();
79        let mut reads = BTreeMap::new();
80        for name in &all {
81            let stored: Vec<u64> = inner.dims(name).map(<[u64]>::to_vec).unwrap_or_default();
82            let gate = name.replace(".ffn_up.", ".ffn_gate.");
83            if name.ends_with(".ffn_up.weight") && !all.contains(&gate) {
84                if stored.len() != 2 || stored[1] != 2 * ff {
85                    return Err(bad(format!("{name} is {stored:?}, not a gate and an up projection of {ff} each")));
86                }
87                let ff = ff as usize;
88                for (presented, rows) in [(gate.clone(), 0..ff), (name.clone(), ff..2 * ff)] {
89                    names.push(presented.clone());
90                    dims.insert(presented.clone(), vec![stored[0], ff as u64]);
91                    reads.insert(presented, Read::Rows { src: name.clone(), rows: rows.collect() });
92                }
93                continue;
94            }
95            names.push(name.clone());
96            dims.insert(name.clone(), stored);
97        }
98        Ok(Presented { inner, names, dims, reads })
99    }
100
101    /// `inner` with every tensor named `…<from>` presented as `…<to>`.
102    fn renamed(inner: Box<dyn Checkpoint>, from: &str, to: &str) -> Presented {
103        let mut names = Vec::new();
104        let mut dims = BTreeMap::new();
105        let mut reads = BTreeMap::new();
106        for name in inner.tensor_names() {
107            let stored: Vec<u64> = inner.dims(name).map(<[u64]>::to_vec).unwrap_or_default();
108            let presented = match name.strip_suffix(from) {
109                Some(stem) => {
110                    let presented = format!("{stem}{to}");
111                    reads.insert(presented.clone(), Read::Same { src: name.to_string() });
112                    presented
113                }
114                None => name.to_string(),
115            };
116            names.push(presented.clone());
117            dims.insert(presented, stored);
118        }
119        Presented { inner, names, dims, reads }
120    }
121
122    /// A Qwen3-Next checkpoint in Qwen3.5's tiled layout.
123    fn tiled(inner: Box<dyn Checkpoint>) -> std::io::Result<Presented> {
124        let bad = |why: String| std::io::Error::new(std::io::ErrorKind::InvalidData, why);
125        let key = |k: &str| -> std::io::Result<usize> {
126            inner
127                .arch_meta(k)
128                .and_then(MetaValue::as_u64)
129                .map(|v| v as usize)
130                .ok_or_else(|| bad(format!("qwen3next.{k} is missing")))
131        };
132        let (kh, vh, kd) = (key("ssm.group_count")?, key("ssm.time_step_rank")?, key("ssm.state_size")?);
133        let inner_size = key("ssm.inner_size")?;
134        if kh == 0 || vh % kh != 0 || inner_size % vh.max(1) != 0 {
135            return Err(bad(format!("{vh} value heads of {inner_size} over {kh} key heads do not pair")));
136        }
137        let (r, vd) = (vh / kh, inner_size / vh);
138        // Tiled position `t` holds grouped head `(t mod kh)·r + ⌊t/kh⌋`.
139        let head: Vec<usize> = (0..vh).map(|t| (t % kh) * r + t / kh).collect();
140        let heads_of = |d: usize| -> Vec<usize> { head.iter().flat_map(|&g| (0..d).map(move |e| g * d + e)).collect() };
141        let qk = 2 * kh * kd;
142        let mut names = Vec::new();
143        let mut dims = BTreeMap::new();
144        let mut reads = BTreeMap::new();
145        for name in inner.tensor_names() {
146            let stored: Vec<u64> = inner.dims(name).map(<[u64]>::to_vec).unwrap_or_default();
147            let suffix = name.splitn(3, '.').nth(2).unwrap_or("");
148            let read = match suffix {
149                "attn_qkv.weight" | "ssm_conv1d.weight" => {
150                    Some(Read::Rows { src: name.into(), rows: (0..qk).chain(heads_of(vd).into_iter().map(|o| qk + o)).collect() })
151                }
152                "attn_gate.weight" => Some(Read::Rows { src: name.into(), rows: heads_of(vd) }),
153                "ssm_dt.bias" | "ssm_a" => Some(Read::Chunks { src: name.into(), chunk: 1, order: head.clone() }),
154                "ssm_out.weight" => Some(Read::Chunks { src: name.into(), chunk: vd, order: head.clone() }),
155                "ssm_ba.weight" => {
156                    // Each key head's `r` betas, then its `r` alphas: grouped head `g` is key head `g / r`'s `g mod r`-th.
157                    let block = name.trim_end_matches("ssm_ba.weight");
158                    for (part, off) in [("ssm_beta.weight", 0), ("ssm_alpha.weight", r)] {
159                        let rows = head.iter().map(|&g| (g / r) * 2 * r + off + g % r).collect();
160                        let presented = format!("{block}{part}");
161                        names.push(presented.clone());
162                        dims.insert(presented.clone(), vec![stored[0], vh as u64]);
163                        reads.insert(presented, Read::Rows { src: name.into(), rows });
164                    }
165                    continue;
166                }
167                _ => None,
168            };
169            names.push(name.to_string());
170            dims.insert(name.to_string(), stored);
171            if let Some(read) = read {
172                reads.insert(name.to_string(), read);
173            }
174        }
175        Ok(Presented { inner, names, dims, reads })
176    }
177}
178
179impl SpecSource for Presented {
180    fn arch(&self) -> &str {
181        self.inner.arch()
182    }
183    fn arch_meta(&self, suffix: &str) -> Option<&MetaValue> {
184        self.inner.arch_meta(suffix)
185    }
186    fn dims(&self, name: &str) -> Option<&[u64]> {
187        self.dims.get(name).map(Vec::as_slice)
188    }
189    fn tensor_names(&self) -> Vec<&str> {
190        self.names.iter().map(String::as_str).collect()
191    }
192}
193
194impl WeightSource for Presented {
195    fn describe(&self, name: &str) -> std::io::Result<(GgmlType, Vec<u64>)> {
196        let dims = self
197            .dims
198            .get(name)
199            .cloned()
200            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, format!("no tensor {name}")))?;
201        let src = match self.reads.get(name) {
202            Some(Read::Rows { src, .. } | Read::Chunks { src, .. } | Read::Same { src }) => src.as_str(),
203            None => name,
204        };
205        Ok((self.inner.describe(src)?.0, dims))
206    }
207
208    fn row_bytes(&mut self, name: &str, first: usize, count: usize) -> std::io::Result<Vec<u8>> {
209        match self.reads.get(name).cloned() {
210            None => self.inner.row_bytes(name, first, count),
211            Some(Read::Same { src }) => self.inner.row_bytes(&src, first, count),
212            Some(Read::Rows { src, rows }) => {
213                let mut out = Vec::new();
214                let picked = rows.get(first..first + count).ok_or_else(|| {
215                    std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{name}: rows {first}..{} of {}", first + count, rows.len()))
216                })?;
217                // Runs of consecutive stored rows read at once.
218                let mut i = 0;
219                while i < picked.len() {
220                    let mut j = i + 1;
221                    while j < picked.len() && picked[j] == picked[j - 1] + 1 {
222                        j += 1;
223                    }
224                    out.extend(self.inner.row_bytes(&src, picked[i], j - i)?);
225                    i = j;
226                }
227                Ok(out)
228            }
229            Some(Read::Chunks { src, chunk, order }) => {
230                let (ty, dims) = self.inner.describe(&src)?;
231                let inner = dims.first().copied().unwrap_or(1) as usize;
232                let (len, bytes) = (ty.block_len(), ty.block_bytes());
233                if chunk % len != 0 || inner != chunk * order.len() {
234                    return Err(std::io::Error::new(
235                        std::io::ErrorKind::InvalidData,
236                        format!("{name}: chunks of {chunk} are not whole {} blocks of {len}", ty.name()),
237                    ));
238                }
239                let chunk_bytes = chunk / len * bytes;
240                let stored = self.inner.row_bytes(&src, first, count)?;
241                let row = chunk_bytes * order.len();
242                let mut out = Vec::with_capacity(stored.len());
243                for r in stored.chunks_exact(row) {
244                    for &c in &order {
245                        out.extend_from_slice(&r[c * chunk_bytes..(c + 1) * chunk_bytes]);
246                    }
247                }
248                Ok(out)
249            }
250        }
251    }
252
253    fn has(&self, name: &str) -> bool {
254        self.dims.contains_key(name)
255    }
256}