Skip to main content

logicaffeine_compile/spatial/decode_engine/
image.rs

1//! The weight image: a checkpoint's tensors laid out over the card's memory channels in the order a pass reads
2//! them, so each channel streams its share of every pass as one run of lines.
3//!
4//! A tensor is its rows, a row being its innermost dimension: a matrix's rows are its output rows, a vector is one
5//! row. The image cuts each tensor into tiles of whole rows packed end to end, as many as fill [`TILE_LINES`] lines
6//! (one row when a row alone is larger), and starts every tile on a line. A kernel stored tap-major has a tap's every
7//! channel for a row; a tap larger than a tile is cut into the fewest equal runs of channels that fit one, each run a
8//! row of the image, so the machine's own layout never makes a tile past the tile's lines. A tensor's tiles go to the channels in turn,
9//! least loaded first: the channels sorted by the lines they hold before it, ties to the lower channel, and the tiles
10//! dealt round that order. Every tile but a tensor's last holds the same rows, so a row's place is arithmetic
11//! ([`Image::locate`]): the tensor's tile, the channel its turn falls on, and that channel's first line of the tensor
12//! plus the tensor's earlier tiles there.
13//!
14//! The deal keeps the channels within one tile of each other after every tile, so when all four stream at one rate
15//! each tile arrives within one tile's time of its turn, and the order on every channel is the read order. With a
16//! check word ([`ImageLayout::check`]) every tile ends in the CRC-32C of its lines before it, little-endian, in the
17//! last four bytes of its last line, the bytes between its rows and the check word zero: the integrity a memory
18//! controller built without ECC leaves to the image when all 72 bits of a beat carry data.
19//!
20//! A layout with a stripe ([`ImageLayout::stripe`]) of `n` pairs deals every tensor a matvec job streams through the
21//! array in tiles of `2n` rows instead, each tile block-major: every row's block 0, then every row's block 1, and so
22//! on, so a channel's tile is the row pairs its columns hold, a block of each at a time, in the order the columns
23//! take them. The tiles 4s..4s+3 of a striped tensor are the four channels' shares of its stripe `s`, which the array
24//! runs a block step at a time; the default layout has no stripe.
25//!
26//! The order is the caller's. [`image_of`] lays out a machine layout's tensors (`decode_engine::machine::Layout`, in
27//! the order the program first reads them) as the machine stores them, a convolution kernel tap-major, so the image
28//! and the machine hold one tensor list; the card's images and the ledger are made so ([`machine_of`] reads the
29//! layout off a header ledger). [`TensorTable::read_order`] orders a bare tensor table as the engine's step reads a
30//! `qwen35` checkpoint, then every tensor the step does not read, in the table's order.
31
32use std::collections::HashMap;
33use std::path::Path;
34
35use serde::Serialize;
36
37use crate::codegen_sva::ddr4_jedec::Ddr4Grade;
38use crate::model::gguf::Gguf;
39use crate::model::quant::GgmlType;
40use crate::model::reference::WeightSource;
41use crate::spatial::d5005::{Beat, DDR_CHANNELS};
42use crate::spatial::decode::GgufHeader;
43
44use super::machine::Layout as MachineLayout;
45use super::model::EngineNumerics;
46use super::program::lower;
47
48/// Lines a tile fills before the next channel's turn. Modeled: the skew one channel's tile buffer must cover.
49pub const TILE_LINES: u64 = 512;
50/// Bytes of a tile's check word.
51pub const CHECK_BYTES: u64 = 4;
52
53/// One tensor as the image stores it: its name, format and stored dims (innermost first); for a convolution kernel
54/// stored tap-major, its taps: then each stored row is one tap's every channel, the transpose of the checkpoint's rows
55/// of each channel's taps; and whether a matvec job streams it through the array, which a layout with a stripe lays in
56/// stripes.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct TensorEntry {
59    pub name: String,
60    pub ty: GgmlType,
61    pub dims: Vec<u64>,
62    pub tap_major: Option<u64>,
63    pub streamed: bool,
64}
65
66impl TensorEntry {
67    pub fn rows(&self) -> u64 {
68        self.dims.iter().skip(1).product()
69    }
70
71    pub fn row_bytes(&self) -> u64 {
72        self.ty.bytes_for(self.dims[0] as usize) as u64
73    }
74}
75
76/// Why a checkpoint has no image.
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub enum ImageError {
79    MissingMetadata(String),
80    UnknownType { tensor: String, id: u32 },
81    /// A row that is not a whole number of its format's blocks.
82    RaggedRow { tensor: String, elems: u64, block: u64 },
83    /// An order that does not name every tensor exactly once.
84    Order(String),
85    /// A channel the image overflows.
86    TooBig { channel: u32, lines: u64, capacity: u64 },
87    /// A tap-major kernel's tap that no cut into equal runs of channels fits a tile.
88    RowPastTile { tensor: String, row_bytes: u64, room: u64 },
89    /// A stripe of no row pairs.
90    EmptyStripe,
91    /// A tensor both streamed through the array and stored tap-major.
92    StreamedKernel(String),
93}
94
95/// A checkpoint's tensor table and the blocks its step runs (the rest are drafting blocks).
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct TensorTable {
98    pub blocks: u64,
99    pub tensors: Vec<TensorEntry>,
100}
101
102/// A block's tensors in the order the engine's step reads them: the mixer's norm; a Gated-DeltaNet mixer's
103/// projections, its convolution, decay and norm vectors, its output; an attention mixer's projections, its query and
104/// key norms, its output; the FFN's norm and its gate, up and down projections.
105const BLOCK_READS: &[&str] = &[
106    "attn_norm.weight",
107    "attn_qkv.weight",
108    "attn_gate.weight",
109    "ssm_beta.weight",
110    "ssm_alpha.weight",
111    "ssm_conv1d.weight",
112    "ssm_dt.bias",
113    "ssm_a",
114    "ssm_norm.weight",
115    "ssm_out.weight",
116    "attn_q.weight",
117    "attn_k.weight",
118    "attn_v.weight",
119    "attn_q_norm.weight",
120    "attn_k_norm.weight",
121    "attn_output.weight",
122    "post_attention_norm.weight",
123    "ffn_norm.weight",
124    "ffn_gate.weight",
125    "ffn_up.weight",
126    "ffn_down.weight",
127];
128
129impl TensorTable {
130    /// The table of `rows` (name, dims, GGUF type id), the first `blocks` blocks being the step's.
131    pub fn from_rows(blocks: u64, rows: impl IntoIterator<Item = (String, Vec<u64>, u32)>) -> Result<TensorTable, ImageError> {
132        let mut tensors = Vec::new();
133        for (name, dims, id) in rows {
134            let ty = GgmlType::from_id(id).map_err(|id| ImageError::UnknownType { tensor: name.clone(), id })?;
135            let elems = dims.first().copied().unwrap_or(0);
136            let block = ty.block_len() as u64;
137            if elems % block != 0 || dims.contains(&0) || dims.is_empty() {
138                return Err(ImageError::RaggedRow { tensor: name, elems, block });
139            }
140            tensors.push(TensorEntry { name, ty, dims, tap_major: None, streamed: false });
141        }
142        Ok(TensorTable { blocks, tensors })
143    }
144
145    /// The table of a checkpoint on disk.
146    pub fn of_gguf(g: &Gguf) -> Result<TensorTable, ImageError> {
147        let meta = |key: &str| g.arch_meta(key).and_then(|v| v.as_u64());
148        let blocks = meta("block_count").ok_or_else(|| ImageError::MissingMetadata("block_count".into()))?;
149        TensorTable::from_rows(blocks.saturating_sub(meta("nextn_predict_layers").unwrap_or(0)), g.tensor_names())
150    }
151
152    /// The table of a checkpoint's header ledger.
153    pub fn of_header(h: &GgufHeader) -> Result<TensorTable, ImageError> {
154        let blocks = h.meta_u64("block_count").ok_or_else(|| ImageError::MissingMetadata("block_count".into()))?;
155        let rows = h.tensors.iter().map(|t| (t.name.clone(), t.dims.clone(), t.type_id));
156        TensorTable::from_rows(blocks.saturating_sub(h.meta_u64("nextn_predict_layers").unwrap_or(0)), rows)
157    }
158
159    /// The order a pass reads the tensors: the embedding table, each of the step's blocks as `BLOCK_READS` lists
160    /// it, the output norm and the head (a head tied to the embedding table reads the table already placed), then
161    /// every tensor the step does not read, in the table's order.
162    pub fn read_order(&self) -> Vec<usize> {
163        let at: HashMap<&str, usize> = self.tensors.iter().enumerate().map(|(i, t)| (t.name.as_str(), i)).collect();
164        let mut order = Vec::with_capacity(self.tensors.len());
165        let push = |name: &str, order: &mut Vec<usize>| {
166            if let Some(&i) = at.get(name) {
167                order.push(i);
168            }
169        };
170        push("token_embd.weight", &mut order);
171        for b in 0..self.blocks {
172            for key in BLOCK_READS {
173                push(&format!("blk.{b}.{key}"), &mut order);
174            }
175        }
176        push("output_norm.weight", &mut order);
177        push("output.weight", &mut order);
178        let mut placed = vec![false; self.tensors.len()];
179        for &i in &order {
180            placed[i] = true;
181        }
182        order.extend((0..self.tensors.len()).filter(|&i| !placed[i]));
183        order
184    }
185}
186
187/// How an image lays its tiles: over how many channels, in lines of how many bytes, how many lines a tile fills, how
188/// many lines a channel holds, whether every tile carries a check word, the row pairs a channel's tile of a streamed
189/// tensor holds when the image is laid in stripes for the streaming engine's array, and whether a streamed Q8_0
190/// tensor's blocks lie eight to one of the array's blocks (its octets: a row's eight blocks one after another, 256
191/// elements in 272 bytes), and the bytes a block of a tensor laid in stripes takes rounded up to (its slot: the
192/// block's bytes, then zeros; 1 for blocks packed as the checkpoint packs them).
193#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
194pub struct ImageLayout {
195    pub channels: u32,
196    pub line_bytes: u64,
197    pub tile_lines: u64,
198    pub capacity_lines: u64,
199    pub check: bool,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub stripe: Option<u64>,
202    #[serde(skip_serializing_if = "std::ops::Not::not")]
203    pub octets: bool,
204    #[serde(skip_serializing_if = "packed")]
205    pub slot: u64,
206}
207
208fn packed(slot: &u64) -> bool {
209    *slot == 1
210}
211
212impl ImageLayout {
213    /// The D5005's four channels at `beat`: 64 data bytes a line, or all 72 with a check word on every tile.
214    pub fn d5005(beat: Beat) -> ImageLayout {
215        let ddr = Ddr4Grade::D5005_EMIF.at();
216        let (line_bytes, check) = match beat {
217            Beat::Data64 => (64, false),
218            Beat::Full72 => (ddr.burst_bits() / 8, true),
219        };
220        ImageLayout { channels: DDR_CHANNELS, line_bytes, tile_lines: TILE_LINES, capacity_lines: ddr.lines(), check, stripe: None, octets: false, slot: 1 }
221    }
222
223    fn check_bytes(&self) -> u64 {
224        if self.check {
225            CHECK_BYTES
226        } else {
227            0
228        }
229    }
230
231    /// Lines a tile of `bytes` row bytes takes, its check word included.
232    pub fn lines_for(&self, bytes: u64) -> u64 {
233        (bytes + self.check_bytes()).div_ceil(self.line_bytes)
234    }
235}
236
237/// Where one tensor lies: its rows in the image and their size — each stored row cut into `pieces` equal runs, more
238/// than one only for a tap-major kernel's tap past a tile — the rows every tile but its last holds and the lines such a
239/// tile takes, the channels in the order its tiles are dealt, the line each of them holds its first tile of the tensor
240/// at, its tiles' run in [`Image::tiles`], its taps when it is stored tap-major, the bytes of its format's block,
241/// whether its tiles hold their rows block-major (laid in stripes), and the zeros each of a striped tensor's blocks is
242/// padded with to the image's slot.
243#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
244pub struct Placement {
245    pub name: String,
246    pub ty: &'static str,
247    pub dims: Vec<u64>,
248    pub rows: u64,
249    pub row_bytes: u64,
250    pub pieces: u64,
251    pub rows_per_tile: u64,
252    pub tile_lines: u64,
253    pub channels: Vec<u32>,
254    pub first_line: Vec<u64>,
255    pub first_tile: usize,
256    pub tile_count: usize,
257    pub tap_major: Option<u64>,
258    pub block_bytes: u64,
259    pub block_major: bool,
260    /// The zeros after each block of a tensor laid in stripes, to the image's slot.
261    #[serde(skip_serializing_if = "unpadded")]
262    pub pad: u64,
263}
264
265fn unpadded(pad: &u64) -> bool {
266    *pad == 0
267}
268
269impl Placement {
270    /// The bytes one of the tensor's blocks takes in the image: its block's, and its pad.
271    pub fn slot_bytes(&self) -> u64 {
272        self.block_bytes + self.pad
273    }
274
275    /// The bytes one of the tensor's rows takes in the image: its stored bytes, each block's pad beside it.
276    pub fn image_row_bytes(&self) -> u64 {
277        if self.pad == 0 {
278            self.row_bytes
279        } else {
280            self.row_bytes / self.block_bytes * self.slot_bytes()
281        }
282    }
283}
284
285/// A run of one tensor's rows `[first_row, end_row)` on one channel from line `line`, over `lines` lines.
286#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
287pub struct Tile {
288    pub tensor: usize,
289    pub first_row: u64,
290    pub end_row: u64,
291    pub channel: u32,
292    pub line: u64,
293    pub lines: u64,
294}
295
296/// A checkpoint laid out on the channels: its tensors in the order given, its tiles in that order, and the lines each
297/// channel holds.
298#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
299pub struct Image {
300    pub layout: ImageLayout,
301    pub tensors: Vec<Placement>,
302    pub tiles: Vec<Tile>,
303    pub channel_lines: Vec<u64>,
304}
305
306/// Lay `table`'s tensors out in `order` (each index once) under `layout`.
307pub fn weight_image(table: &TensorTable, order: &[usize], layout: &ImageLayout) -> Result<Image, ImageError> {
308    let mut seen = vec![false; table.tensors.len()];
309    for &i in order {
310        match seen.get_mut(i) {
311            None => return Err(ImageError::Order(format!("index {i} of {} tensors", table.tensors.len()))),
312            Some(true) => return Err(ImageError::Order(format!("`{}` twice", table.tensors[i].name))),
313            Some(s) => *s = true,
314        }
315    }
316    if let Some(i) = seen.iter().position(|s| !s) {
317        return Err(ImageError::Order(format!("`{}` left out", table.tensors[i].name)));
318    }
319    if layout.stripe == Some(0) {
320        return Err(ImageError::EmptyStripe);
321    }
322    let c = layout.channels as usize;
323    let room = (layout.tile_lines * layout.line_bytes).saturating_sub(layout.check_bytes());
324    let mut loads = vec![0u64; c];
325    let mut tensors = Vec::with_capacity(order.len());
326    let mut tiles = Vec::new();
327    for &i in order {
328        let e = &table.tensors[i];
329        let stripe = layout.stripe.filter(|_| e.streamed);
330        if stripe.is_some() && e.tap_major.is_some() {
331            return Err(ImageError::StreamedKernel(e.name.clone()));
332        }
333        let pieces = pieces_of(e, room)?;
334        let (rows, row_bytes) = (e.rows() * pieces, e.row_bytes() / pieces);
335        if stripe.is_some() && layout.octets && e.ty == GgmlType::Q8_0 && e.dims[0] % 256 != 0 {
336            return Err(ImageError::RaggedRow { tensor: e.name.clone(), elems: e.dims[0], block: 256 });
337        }
338        let block_bytes = e.ty.block_bytes() as u64 * if stripe.is_some() && layout.octets && e.ty == GgmlType::Q8_0 { 8 } else { 1 };
339        let pad = if stripe.is_some() { block_bytes.next_multiple_of(layout.slot) - block_bytes } else { 0 };
340        let image_row = row_bytes / block_bytes * (block_bytes + pad);
341        let rows_per_tile = match stripe {
342            Some(pairs) => 2 * pairs,
343            None => room / row_bytes,
344        }
345        .max(1)
346        .min(rows.max(1));
347        let tile_lines = layout.lines_for(rows_per_tile * image_row);
348        let mut channels: Vec<u32> = (0..layout.channels).collect();
349        channels.sort_by_key(|&ch| (loads[ch as usize], ch));
350        let first_line: Vec<u64> = channels.iter().map(|&ch| loads[ch as usize]).collect();
351        let first_tile = tiles.len();
352        let mut first_row = 0;
353        let mut turn = 0;
354        while first_row < rows {
355            let end_row = (first_row + rows_per_tile).min(rows);
356            let channel = channels[turn % c];
357            let lines = layout.lines_for((end_row - first_row) * image_row);
358            tiles.push(Tile { tensor: tensors.len(), first_row, end_row, channel, line: loads[channel as usize], lines });
359            loads[channel as usize] += lines;
360            first_row = end_row;
361            turn += 1;
362        }
363        tensors.push(Placement {
364            name: e.name.clone(),
365            ty: e.ty.name(),
366            dims: e.dims.clone(),
367            rows,
368            row_bytes,
369            pieces,
370            rows_per_tile,
371            tile_lines,
372            channels,
373            first_line,
374            first_tile,
375            tile_count: tiles.len() - first_tile,
376            tap_major: e.tap_major,
377            block_bytes,
378            block_major: stripe.is_some(),
379            pad,
380        });
381    }
382    for (ch, &lines) in loads.iter().enumerate() {
383        if lines > layout.capacity_lines {
384            return Err(ImageError::TooBig { channel: ch as u32, lines, capacity: layout.capacity_lines });
385        }
386    }
387    Ok(Image { layout: *layout, tensors, tiles, channel_lines: loads })
388}
389
390/// The fewest equal runs of whole blocks a tap-major kernel's taps are cut into so that one fits `room` bytes; 1 for
391/// a tap that fits, and for every row of any other tensor.
392fn pieces_of(e: &TensorEntry, room: u64) -> Result<u64, ImageError> {
393    let row_bytes = e.row_bytes();
394    if e.tap_major.is_none() || row_bytes <= room {
395        return Ok(1);
396    }
397    let blocks = e.dims[0] / e.ty.block_len() as u64;
398    let block_bytes = row_bytes / blocks;
399    (row_bytes.div_ceil(room)..=blocks)
400        .find(|&p| blocks % p == 0 && blocks / p * block_bytes <= room)
401        .ok_or(ImageError::RowPastTile { tensor: e.name.clone(), row_bytes, room })
402}
403
404/// The weight image the machine runs over: `machine`'s tensors in its order, each stored as the machine reads it — a
405/// convolution kernel tap-major, its taps rows of every channel — laid out under `layout`.
406pub fn image_of(machine: &MachineLayout, layout: &ImageLayout) -> Result<Image, ImageError> {
407    let mut tensors = Vec::with_capacity(machine.tensors.len());
408    for p in &machine.tensors {
409        let entry = match p.taps {
410            None => TensorEntry { name: p.name.clone(), ty: p.ty, dims: p.dims.clone(), tap_major: None, streamed: p.streamed },
411            Some(taps) => {
412                if p.dims.len() != 2 || p.dims[0] != taps as u64 {
413                    return Err(ImageError::RaggedRow { tensor: p.name.clone(), elems: p.dims[0], block: taps as u64 });
414                }
415                TensorEntry { name: p.name.clone(), ty: p.ty, dims: vec![p.dims[1], taps as u64], tap_major: Some(taps as u64), streamed: p.streamed }
416            }
417        };
418        tensors.push(entry);
419    }
420    let order: Vec<usize> = (0..tensors.len()).collect();
421    weight_image(&TensorTable { blocks: 0, tensors }, &order, layout)
422}
423
424/// The weight image of the matrices `machine`'s matvec jobs stream, in its order and no other tensor, laid out under
425/// `layout` (in stripes when it has one), with each image tensor's index in the machine's: what the hybrid engine's
426/// streaming matvec reads beside the flat space the reference engine reads the rest of the memory from.
427pub fn streamed_image_of(machine: &MachineLayout, layout: &ImageLayout) -> Result<(Image, Vec<usize>), ImageError> {
428    let indices: Vec<usize> = (0..machine.tensors.len()).filter(|&i| machine.tensors[i].streamed).collect();
429    let tensors = indices
430        .iter()
431        .map(|&i| {
432            let p = &machine.tensors[i];
433            TensorEntry { name: p.name.clone(), ty: p.ty, dims: p.dims.clone(), tap_major: None, streamed: true }
434        })
435        .collect();
436    let order: Vec<usize> = (0..indices.len()).collect();
437    Ok((weight_image(&TensorTable { blocks: 0, tensors }, &order, layout)?, indices))
438}
439
440/// A tensor table as a weight source that describes its tensors and holds none of their bytes: what a header ledger
441/// gives a machine layout to place.
442struct Described<'a>(&'a TensorTable);
443
444impl WeightSource for Described<'_> {
445    fn describe(&self, name: &str) -> std::io::Result<(GgmlType, Vec<u64>)> {
446        self.0
447            .tensors
448            .iter()
449            .find(|t| t.name == name)
450            .map(|t| (t.ty, t.dims.clone()))
451            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, format!("no tensor {name}")))
452    }
453    fn row_bytes(&mut self, name: &str, _: usize, _: usize) -> std::io::Result<Vec<u8>> {
454        Err(std::io::Error::new(std::io::ErrorKind::Unsupported, format!("a header holds no bytes of {name}")))
455    }
456    fn has(&self, name: &str) -> bool {
457        self.0.tensors.iter().any(|t| t.name == name)
458    }
459}
460
461/// The machine layout of the checkpoint `h` describes, at the engine's fastest knob point, one sequence of one
462/// position: what its weight image is laid out from.
463pub fn machine_of(h: &GgufHeader) -> Result<MachineLayout, String> {
464    let table = TensorTable::of_header(h).map_err(|e| format!("{e:?}"))?;
465    let spec = h.spec().map_err(|e| format!("{e:?}"))?;
466    let numerics = EngineNumerics::fastest();
467    let program = lower(&spec, numerics, 1).map_err(|e| format!("{}: {e}", spec.arch))?.program;
468    MachineLayout::of(&spec, numerics, &program, &Described(&table), 1, 1).map_err(|e| format!("{e:?}"))
469}
470
471impl Image {
472    /// Bytes each channel holds.
473    pub fn channel_bytes(&self) -> Vec<u64> {
474        self.channel_lines.iter().map(|l| l * self.layout.line_bytes).collect()
475    }
476
477    /// The channel and the byte within it where row `row` of tensor `tensor` (an index into [`Image::tensors`])
478    /// starts: for a tensor laid block-major, where its block 0 is.
479    pub fn locate(&self, tensor: usize, row: u64) -> (u32, u64) {
480        self.locate_block(tensor, row, 0)
481    }
482
483    /// The channel and the byte within it where block `block` of row `row` of tensor `tensor` starts, for a tensor of
484    /// weight blocks (its row's blocks one after another, or, laid block-major, the tile's every row's block before
485    /// any row's next).
486    pub fn locate_block(&self, tensor: usize, row: u64, block: u64) -> (u32, u64) {
487        let p = &self.tensors[tensor];
488        let tile = row / p.rows_per_tile;
489        let turn = (tile % p.channels.len() as u64) as usize;
490        let line = p.first_line[turn] + tile / p.channels.len() as u64 * p.tile_lines;
491        let (r, held) = (row % p.rows_per_tile, p.rows_per_tile.min(p.rows - tile * p.rows_per_tile));
492        let within = if p.block_major { (block * held + r) * p.slot_bytes() } else { r * p.row_bytes + block * p.block_bytes };
493        (p.channels[turn], line * self.layout.line_bytes + within)
494    }
495
496    /// The image's rows `[first, first + count)` of tensor `tensor`, read from `source`: the pieces of the stored rows
497    /// they lie in.
498    pub fn stored_rows(&self, tensor: usize, source: &mut dyn WeightSource, first: u64, count: u64) -> std::io::Result<Vec<u8>> {
499        let p = &self.tensors[tensor];
500        let (lo, hi) = (first / p.pieces, (first + count).div_ceil(p.pieces));
501        let whole = self.whole_rows(tensor, source, lo, hi - lo)?;
502        let skip = ((first - lo * p.pieces) * p.row_bytes) as usize;
503        Ok(whole[skip..skip + (count * p.row_bytes) as usize].to_vec())
504    }
505
506    /// Stored rows `[first, first + count)` of tensor `tensor`, read from `source`: the checkpoint's rows, or for a
507    /// tensor stored tap-major, each tap's every channel gathered from the checkpoint's rows of each channel's taps.
508    fn whole_rows(&self, tensor: usize, source: &mut dyn WeightSource, first: u64, count: u64) -> std::io::Result<Vec<u8>> {
509        let p = &self.tensors[tensor];
510        let Some(taps) = p.tap_major else {
511            return source.row_bytes(&p.name, first as usize, count as usize);
512        };
513        let channels = p.dims[0];
514        let element = p.row_bytes * p.pieces / channels;
515        let checkpoint = source.row_bytes(&p.name, 0, channels as usize)?;
516        let mut out = Vec::with_capacity((count * p.row_bytes * p.pieces) as usize);
517        for tap in first..first + count {
518            for c in 0..channels {
519                let at = ((c * taps + tap) * element) as usize;
520                out.extend_from_slice(&checkpoint[at..at + element as usize]);
521            }
522        }
523        Ok(out)
524    }
525
526    /// Tile `t`'s lines: its stored rows read from `source` (block-major in a stripe), zeros to its check word, the
527    /// check word.
528    fn tile_bytes(&self, t: &Tile, source: &mut dyn WeightSource) -> std::io::Result<Vec<u8>> {
529        let held = t.end_row - t.first_row;
530        let mut rows = self.stored_rows(t.tensor, source, t.first_row, held)?;
531        let p = &self.tensors[t.tensor];
532        if p.block_major {
533            let (bb, pad, row_bytes) = (p.block_bytes as usize, p.pad as usize, p.row_bytes as usize);
534            let mut out = Vec::with_capacity(rows.len() / bb * (bb + pad));
535            for b in 0..row_bytes / bb {
536                for r in 0..held as usize {
537                    out.extend_from_slice(&rows[r * row_bytes + b * bb..r * row_bytes + (b + 1) * bb]);
538                    out.resize(out.len() + pad, 0);
539                }
540            }
541            rows = out;
542        }
543        let mut span = vec![0u8; (t.lines * self.layout.line_bytes) as usize];
544        span[..rows.len()].copy_from_slice(&rows);
545        if self.layout.check {
546            let body = span.len() - CHECK_BYTES as usize;
547            let word = crc32c(&span[..body]);
548            span[body..].copy_from_slice(&word.to_le_bytes());
549        }
550        Ok(span)
551    }
552
553    /// The channels' bytes, every tile's rows read from `source` and its check word written.
554    pub fn fill(&self, source: &mut dyn WeightSource) -> std::io::Result<Vec<Vec<u8>>> {
555        let lb = self.layout.line_bytes;
556        let mut channels: Vec<Vec<u8>> = self.channel_lines.iter().map(|&l| vec![0u8; (l * lb) as usize]).collect();
557        for t in &self.tiles {
558            let at = (t.line * lb) as usize;
559            let span = self.tile_bytes(t, source)?;
560            channels[t.channel as usize][at..at + span.len()].copy_from_slice(&span);
561        }
562        Ok(channels)
563    }
564
565    /// The bytes [`Image::fill`] gives each channel written to that channel's writer in `out`, a tile at a time, so no
566    /// more than a tile is ever held: each channel takes its tiles in line order from line 0.
567    pub fn write_channels(&self, source: &mut dyn WeightSource, out: &mut [&mut dyn std::io::Write]) -> std::io::Result<()> {
568        if out.len() != self.channel_lines.len() {
569            return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} writers for {} channels", out.len(), self.channel_lines.len())));
570        }
571        let mut next = vec![0u64; out.len()];
572        for t in &self.tiles {
573            let ch = t.channel as usize;
574            if t.line != next[ch] {
575                return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("channel {ch}'s tile at line {} after line {}", t.line, next[ch])));
576            }
577            out[ch].write_all(&self.tile_bytes(t, source)?)?;
578            next[ch] += t.lines;
579        }
580        Ok(())
581    }
582
583    /// The tile table a host loads and scrubs the image by, little-endian 64-bit words after the eight bytes
584    /// `LOGOSIMG`: the format (1), the line's bytes, whether tiles end in check words, the channels, each channel's
585    /// lines, the tiles, and each tile's channel, first line and lines.
586    pub fn tile_table(&self) -> Vec<u8> {
587        let mut words = vec![1, self.layout.line_bytes, u64::from(self.layout.check), self.channel_lines.len() as u64];
588        words.extend(&self.channel_lines);
589        words.push(self.tiles.len() as u64);
590        for t in &self.tiles {
591            words.extend([u64::from(t.channel), t.line, t.lines]);
592        }
593        let mut out = b"LOGOSIMG".to_vec();
594        for w in words {
595            out.extend(w.to_le_bytes());
596        }
597        out
598    }
599
600    /// The first tile whose check word does not match its lines in `channels`, or `None` when every one does (or the
601    /// layout carries none).
602    pub fn first_bad_tile(&self, channels: &[Vec<u8>]) -> Option<usize> {
603        if !self.layout.check {
604            return None;
605        }
606        let lb = self.layout.line_bytes as usize;
607        self.tiles.iter().position(|t| {
608            let at = t.line as usize * lb;
609            let span = &channels[t.channel as usize][at..at + t.lines as usize * lb];
610            let body = span.len() - CHECK_BYTES as usize;
611            crc32c(&span[..body]).to_le_bytes() != span[body..]
612        })
613    }
614}
615
616/// The Qwen3.8-27B checkpoints whose images the ledger lays out.
617pub const HEADERS: &[&str] = &[
618    "qwen3.8-27b-iq4_xs",
619    "qwen3.8-27b-q3_k_m",
620    "qwen3.8-27b-q4_0",
621    "qwen3.8-27b-q4_k_m",
622    "qwen3.8-27b-q5_k_m",
623    "qwen3.8-27b-q6_k",
624    "qwen3.8-27b-q8_0",
625];
626
627/// One checkpoint's image at one beat, summed: its tensors and tiles, the bytes of rows, of padding and of check
628/// words, each channel's lines, the most two channels differ by and the largest tile.
629#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
630pub struct ImageRow {
631    pub header: &'static str,
632    pub beat: Beat,
633    pub layout: ImageLayout,
634    pub tensors: usize,
635    pub tiles: usize,
636    pub data_bytes: u64,
637    pub padding_bytes: u64,
638    pub check_bytes: u64,
639    pub channel_lines: Vec<u64>,
640    pub spread_lines: u64,
641    pub largest_tile_lines: u64,
642}
643
644#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
645pub struct ImageLedger {
646    pub rows: Vec<ImageRow>,
647}
648
649impl Image {
650    /// The image summed as a ledger row.
651    pub fn row(&self, header: &'static str, beat: Beat) -> ImageRow {
652        let data_bytes: u64 = self.tiles.iter().map(|t| (t.end_row - t.first_row) * self.tensors[t.tensor].row_bytes).sum();
653        let check_bytes = if self.layout.check { CHECK_BYTES * self.tiles.len() as u64 } else { 0 };
654        let total: u64 = self.channel_bytes().iter().sum();
655        let (most, least) = (self.channel_lines.iter().max(), self.channel_lines.iter().min());
656        ImageRow {
657            header,
658            beat,
659            layout: self.layout,
660            tensors: self.tensors.len(),
661            tiles: self.tiles.len(),
662            data_bytes,
663            padding_bytes: total - data_bytes - check_bytes,
664            check_bytes,
665            channel_lines: self.channel_lines.clone(),
666            spread_lines: most.zip(least).map_or(0, |(m, l)| m - l),
667            largest_tile_lines: self.tiles.iter().map(|t| t.lines).max().unwrap_or(0),
668        }
669    }
670}
671
672/// Every [`HEADERS`] checkpoint's machine image ([`image_of`] over [`machine_of`]) on the D5005 at both beats.
673pub fn image_ledger(decode_dir: &Path) -> Result<ImageLedger, String> {
674    let mut rows = Vec::new();
675    for &header in HEADERS {
676        let h = GgufHeader::read_ledger(&decode_dir.join(format!("headers/{header}.json")))?;
677        let machine = machine_of(&h).map_err(|e| format!("{header}: {e}"))?;
678        for beat in [Beat::Data64, Beat::Full72] {
679            let image = image_of(&machine, &ImageLayout::d5005(beat)).map_err(|e| format!("{header} {beat:?}: {e:?}"))?;
680            rows.push(image.row(header, beat));
681        }
682    }
683    Ok(ImageLedger { rows })
684}
685
686/// CRC-32C (Castagnoli: the reflected polynomial 0x82F63B78, initial value and final xor all ones), a byte at a time.
687pub fn crc32c(bytes: &[u8]) -> u32 {
688    const TABLE: [u32; 256] = {
689        let mut t = [0u32; 256];
690        let mut i = 0;
691        while i < 256 {
692            let mut c = i as u32;
693            let mut k = 0;
694            while k < 8 {
695                c = if c & 1 != 0 { 0x82F6_3B78 ^ (c >> 1) } else { c >> 1 };
696                k += 1;
697            }
698            t[i] = c;
699            i += 1;
700        }
701        t
702    };
703    !bytes.iter().fold(!0u32, |c, &b| TABLE[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8))
704}