Skip to main content

logicaffeine_compile/spatial/passes/
from_q8.rs

1//! Q8_0 weights on the chip — a product `y = x · W` of up to a tile's rows over a weight the deployment stores Q8_0.
2//!
3//! ★ A DECODE STEP IS WEIGHT-BOUND, SO A WEIGHT MOVES AS IT IS STORED. Q8_0 keeps each column's rows in blocks of 32:
4//! one scale `d` and 32 int8 values `q`, `w = q·d`. The weight is two tensors on the chip — `W__q8`, the int8 values
5//! (`K × N`), and `W__q8_scale`, one fp32 scale per block (`K/32 × N`) — about 1.13 bytes a weight where a bf16 copy
6//! moves 2. The product FOLDS its K blocks of 1,024 rows — 32 Q8_0 blocks, one tile row of scales — into an fp32
7//! carry: per block, ONE stage a row, `y[n] += Σ_b s[b, n]·Σ_i x[32b + i]·q[32b + i, n]` — the lanes contracted over
8//! the int8 tile rows, the block's 32 Q8_0 blocks folded down each column with their scales. An int8 times a bf16
9//! activation is exact in fp32, and the scale is fp32.
10//!
11//! `x` is read as the `K/32 × 32` matrix of its 32-element blocks — one tile per K block — a view named for the value
12//! it views ([`q8_rows_name`]), made once and read by every product of that value. A BATCH of `m` rows (up to a tile's)
13//! is `m` such views interleaved by K block, and the fold runs each row's block dots against the SAME int8 and scale
14//! pages: the weight moves once for all the rows.
15
16use super::from_qwen_ops::{elem_precision, index_type, relayout_rows_to_dense, row_block_tiles};
17use super::from_tensor_op::{ident_map, init_of, page_width, per_col_map, MatmulNames, Template};
18use super::{SpatialReject, LV_GEN_COL, LV_GEN_K, LV_GEN_ROW, LV_ITEM_X, LV_ITEM_Y, LV_KBLOCK};
19use crate::codegen_sva::fabric_target::Precision;
20use crate::spatial::ir::*;
21use crate::spatial::target::SpatialTarget;
22use crate::tensor_recognize::{Fill, MatmulSpec};
23
24/// Rows of a Q8_0 block.
25pub const Q8_BLOCK: usize = 32;
26
27/// Which part of a Q8_0 weight a stored tensor holds — what the host repacks from the checkpoint's blocks into it.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum Q8Part {
30    /// The int8 values, `K × N`.
31    Values,
32    /// One fp32 scale per 32 rows of each column, `K/32 × N`.
33    Scales,
34}
35
36impl Q8Part {
37    /// The manifest's name for the repack.
38    pub fn repack(self) -> &'static str {
39        match self {
40            Q8Part::Values => "q8_0.values",
41            Q8Part::Scales => "q8_0.scales",
42        }
43    }
44}
45
46/// The tensor holding `part` of the Q8_0 weight `weight`.
47pub fn stored_name(weight: &str, part: Q8Part) -> String {
48    match part {
49        Q8Part::Values => format!("{weight}__q8"),
50        Q8Part::Scales => format!("{weight}__q8_scale"),
51    }
52}
53
54/// The `K/32 × 32` view of the one-row activation `x` a Q8_0 product reads — `x`'s 32-element blocks as the rows of a
55/// matrix, one tile per K block. It is named for the VALUE it views: `x` after the program has written it `writes`
56/// times. Every product of one value reads one view, and a value written again is viewed again under a new name (an
57/// activation has one writer).
58pub fn q8_rows_name(x: &str, writes: usize) -> String {
59    format!("{x}__q8_rows{writes}")
60}
61
62/// [`q8_rows_name`] for a product that runs on `chip` of a tensor-parallel deployment: a chip reads only what lives on
63/// it, so each chip makes its own view of a replicated value, shared by that chip's products.
64pub fn q8_rows_name_on(x: &str, writes: usize, chip: Option<u16>) -> String {
65    match chip {
66        Some(c) => format!("{}_c{c}", q8_rows_name(x, writes)),
67        None => q8_rows_name(x, writes),
68    }
69}
70
71/// The Q8_0 weight a tensor named by [`stored_name`] is part of, and the part.
72pub fn stored_part(name: &str) -> Option<(&str, Q8Part)> {
73    match (name.strip_suffix("__q8_scale"), name.strip_suffix("__q8")) {
74        (Some(w), _) => Some((w, Q8Part::Scales)),
75        (None, Some(w)) => Some((w, Q8Part::Values)),
76        (None, None) => None,
77    }
78}
79
80/// Whether a product over a Q8_0 weight folds on `target`: up to a tile's rows, whole Q8_0 blocks of K, on a square tile
81/// as tall as a Q8_0 block. Its columns are the weight's column tiles, the last padded with zero columns as every tensor's
82/// last tile is — a tensor-parallel chip's 24 of a gate's 48 heads, a width of 40 — so the pad's values and scales are
83/// zero and its sums land only in the result's own pad.
84pub fn q8_gemv_applies(spec: &MatmulSpec, target: &SpatialTarget) -> bool {
85    let (tr, tw) = (target.tile.rows, target.tile.cols);
86    (1..=tr).contains(&spec.m) && tr == Q8_BLOCK && tw == tr && spec.k > 0 && spec.k % Q8_BLOCK == 0 && spec.n > 0
87}
88
89/// The int8 values and fp32 scales of a Q8_0 weight from its declared `k × n` values — `None` when a block's values
90/// are not an int8 multiple of its own scale `max|w| / 127`.
91fn quantize(w: &[f64], k: usize, n: usize) -> Option<(Vec<i64>, Vec<f64>)> {
92    let (mut q, mut s) = (vec![0i64; k * n], vec![0.0; (k / Q8_BLOCK) * n]);
93    for b in 0..k / Q8_BLOCK {
94        for col in 0..n {
95            let at = |i: usize| (Q8_BLOCK * b + i) * n + col;
96            let amax = (0..Q8_BLOCK).map(|i| w[at(i)].abs()).fold(0.0, f64::max);
97            if amax == 0.0 {
98                continue;
99            }
100            let d = amax / 127.0;
101            // The scale is stored in fp32: it must round-trip, and so must every value it scales.
102            if (d as f32) as f64 != d {
103                return None;
104            }
105            for i in 0..Q8_BLOCK {
106                let v = w[at(i)] / d;
107                let r = v.round();
108                if (v - r).abs() > 1e-9 || r.abs() > 127.0 || r * d != w[at(i)] {
109                    return None;
110                }
111                q[at(i)] = r as i64;
112            }
113            s[b * n + col] = d;
114        }
115    }
116    Some((q, s))
117}
118
119/// `y = x · W` for an `m`-row `x` and a Q8_0 `W`: the reshape of `x` into its blocks, then the fold — or `None` when
120/// the fold would move more than reading the weight dense.
121///
122/// ★ A BATCH READS ITS VIEW ONCE PER COLUMN GROUP. Every item folds all `m` rows against its own columns of the weight,
123/// so the view's `m` tiles per K block are read by every item: `m · 2` bytes per K element per group of `32·wb`
124/// columns, against the weight's `1.125 · 32·wb`. A site holds `wb` of 2 at a batch of 8 — the view adds ~22% — and
125/// fewer at a larger batch, where the view would outweigh what the int8 values save. So the fold is taken only while
126/// its bytes stay below the bf16 weight's own, which bounds every dense lowering from below.
127pub fn q8_gemv(
128    spec: &MatmulSpec,
129    names: &MatmulNames,
130    fills: (&Fill, &Fill),
131    view: &str,
132    target: &SpatialTarget,
133) -> Result<Option<Vec<SpatialProgram>>, SpatialReject> {
134    let (m, k, n) = (spec.m, spec.k, spec.n);
135    let tw = target.tile.cols;
136    let precision = elem_precision(target)?;
137    let acc = AccPolicy::F32.interm_precision();
138    let (values_init, scales_init) = match fills.1 {
139        Fill::Unknown => (TensorInit::External, TensorInit::External),
140        // A weight the program declares and never sets is zeros: every value and every scale 0, nothing to quantize.
141        Fill::Const(0) => (TensorInit::Zero, TensorInit::Zero),
142        Fill::ConstF(b) if f64::from_bits(*b) == 0.0 => (TensorInit::Zero, TensorInit::Zero),
143        fill => {
144            let init = init_of(fill, k * n);
145            let w: Vec<f64> = (0..k * n)
146                .map(|p| init.element_f64(p).unwrap_or(0.0))
147                .collect();
148            let (q, s) = quantize(&w, k, n).ok_or_else(|| SpatialReject::NotQ8_0 {
149                weight: names.b.clone(),
150            })?;
151            (
152                TensorInit::Values(q),
153                TensorInit::FloatValues(s.iter().map(|d| d.to_bits()).collect()),
154            )
155        }
156    };
157    // ★ `x` AS ITS BLOCKS: a flat row holds 32 values per tile; its `K/32 × 32` view ([`q8_rows_name`]) holds a K
158    // block of 1,024 in one.
159    let mut out = if m == 1 {
160        relayout_rows_to_dense(
161            (&names.a, fills.0),
162            view,
163            k / Q8_BLOCK,
164            Q8_BLOCK,
165            precision,
166            target,
167        )?
168    } else {
169        relayout_rows_to_q8_view((&names.a, fills.0), view, m, k, precision, target)?
170    };
171    let budget = target.max_local_mem_bytes();
172    let n_t = n.div_ceil(tw);
173    // The widest block of columns a site holds — at most the accumulator's tiles, each width built and measured. A
174    // fold that cannot be BUILT is refused with its own reason; a batch whose rows outgrow a site at one column tile
175    // reads the weight dense.
176    let mut chosen = None;
177    for wb in (1..=n_t.min(row_block_tiles(target)))
178        .rev()
179        .filter(|w| n_t % w == 0)
180    {
181        let fold = q8_fold(
182            names,
183            view,
184            (m, k, n),
185            wb,
186            (&values_init, &scales_init),
187            precision,
188            acc,
189            target,
190        )?;
191        if fold
192            .buffers
193            .iter()
194            .map(|b| fold.buffer_bytes(b))
195            .sum::<usize>()
196            <= budget
197        {
198            chosen = Some((wb, fold));
199            break;
200        }
201    }
202    let Some((wb, fold)) = chosen else {
203        return Ok(None);
204    };
205    // Both weights move whole column tiles, the last one's pad with them.
206    let (tile, padded) = (target.tile.rows * tw, n_t * tw);
207    let weight = k * padded + 4 * (k / Q8_BLOCK) * padded;
208    let view_reads = (n_t / wb)
209        * k.div_ceil(tile)
210        * m
211        * tile
212        * crate::spatial::target::precision_bytes(precision);
213    if weight + view_reads >= 2 * k * padded {
214        return Ok(None);
215    }
216    out.push(fold);
217    Ok(Some(out))
218}
219
220/// The view of an `m × K` matrix `x` (`1 < m ≤ tile`): the matrix whose tile row `kb·m + r` is row `r`'s K block
221/// `kb` — its elements `1024·kb …` as rows of 32 ([`q8_view_rows`]). The whole blocks are one section and a tail another,
222/// each writing its part of the view.
223fn relayout_rows_to_q8_view(
224    x: (&str, &Fill),
225    view: &str,
226    m: usize,
227    k: usize,
228    dt: Precision,
229    target: &SpatialTarget,
230) -> Result<Vec<SpatialProgram>, SpatialReject> {
231    let block = target.tile.rows * Q8_BLOCK;
232    let (nb, tail) = (k / block, k % block);
233    let part = nb > 0 && tail > 0;
234    let mut out = Vec::new();
235    if nb > 0 {
236        out.push(q8_view_section(
237            x,
238            view,
239            (m, k),
240            (0, nb, block),
241            part,
242            dt,
243            target,
244        )?);
245    }
246    if tail > 0 {
247        out.push(q8_view_section(
248            x,
249            view,
250            (m, k),
251            (nb, 1, tail),
252            part,
253            dt,
254            target,
255        )?);
256    }
257    Ok(out)
258}
259
260/// K blocks `kb0 … kb0 + kbs − 1` of [`relayout_rows_to_q8_view`], each `width` elements of every row.
261///
262/// Item `(kb, c)` loads the block's tiles of `x` — every row — as one page and writes `rows` view tiles, rows `c·rows …`
263/// of the batch, as one page: view page element `(b, 32ρ + i)` is `x` page element `(c·rows + ρ, 32b + i)`, at offset
264/// `W·(c·rows + ρ) + 32b + i` for the page's width `W`. The column's row `ρ` and lane `i` are not affine in it, so
265/// `W·(c·rows + ρ) + i` comes from a table over the column, one tile per chunk `c`; `rows` divides `m` and fits the
266/// accumulator.
267fn q8_view_section(
268    x: (&str, &Fill),
269    view: &str,
270    (m, k): (usize, usize),
271    (kb0, kbs, width): (usize, usize, usize),
272    part: bool,
273    dt: Precision,
274    target: &SpatialTarget,
275) -> Result<SpatialProgram, SpatialReject> {
276    let (tr, tw) = (target.tile.rows, target.tile.cols);
277    let (x_t, page_t, g) = (k / tw, width / tw, width / Q8_BLOCK);
278    let page_w = page_t * tw;
279    let rows = (1..=m.min(row_block_tiles(target)))
280        .rev()
281        .find(|d| m % d == 0)
282        .unwrap_or(1);
283    let chunks = m / rows;
284    let ix = index_type(target, (m * page_w) as u64)?;
285    let mut t = Template::new(target)?;
286    let src = t.tensor(x.0, TensorRole::Input, m, k, dt, init_of(x.1, m * k));
287    let dst = t.tensor(
288        view,
289        if part {
290            TensorRole::State
291        } else {
292            TensorRole::Output
293        },
294        q8_view_rows(m, k),
295        Q8_BLOCK,
296        dt,
297        TensorInit::Zero,
298    );
299    let lanes: Vec<i64> = (0..chunks * rows * tw)
300        .map(|c| ((c / tw) * page_w + c % tw) as i64)
301        .collect();
302    let table = t.tensor(
303        &format!("{view}__lanes{kb0}"),
304        TensorRole::Param,
305        1,
306        chunks * rows * tw,
307        ix,
308        TensorInit::Values(lanes),
309    );
310    let x_in = t.buffer("in_block", page_t, dt);
311    let lane_in = t.buffer_deep("in_lanes", 1, rows, ix);
312    let out = t.buffer("out", rows, dt);
313    let l_x = t.load_block_at(
314        "load_block",
315        src,
316        kb0 * (tr * Q8_BLOCK / tw),
317        vec![(LV_ITEM_Y, page_t)],
318        1,
319        page_t,
320        x_t,
321        x_in,
322    );
323    let l_lanes = t.load_block_at(
324        "load_lanes",
325        table,
326        0,
327        vec![(LV_ITEM_X, rows)],
328        1,
329        rows,
330        chunks * rows,
331        lane_in,
332    );
333    let lift = ElemAccess {
334        base: 0,
335        terms: vec![(LV_GEN_ROW, tw as i64)],
336        indirect: Some(IndirectTerm {
337            operand: 1,
338            stride: 1,
339            modulus: m * page_w,
340        }),
341    };
342    let s = t.stage(
343        "lift_rows",
344        StageKind::Compute(ComputeOp::Generic {
345            iters: vec![
346                IterSpec {
347                    var: LV_GEN_ROW,
348                    extent: g,
349                    kind: IterKind::Parallel,
350                },
351                IterSpec {
352                    var: LV_GEN_COL,
353                    extent: rows * tw,
354                    kind: IterKind::Parallel,
355                },
356            ],
357            ins: vec![
358                lift,
359                ElemAccess {
360                    base: 0,
361                    terms: vec![(LV_GEN_COL, 1)],
362                    indirect: None,
363                },
364            ],
365            out: ident_map(page_width(rows, target)),
366            body: TensorBody::Operand(0),
367        }),
368        vec![x_in, lane_in],
369        vec![out],
370    );
371    let st = t.store_block(
372        "store_view",
373        dst,
374        kb0 * m,
375        vec![(LV_ITEM_Y, m), (LV_ITEM_X, rows)],
376        rows,
377        1,
378        1,
379        out,
380    );
381    t.edge(l_x, x_in, s);
382    t.edge(l_lanes, lane_in, s);
383    t.edge(s, out, st);
384    let items: Vec<WorkItem> = (0..kbs)
385        .flat_map(|kb| (0..chunks).map(move |c| (kb, c)))
386        .enumerate()
387        .map(|(id, (kb, c))| WorkItem {
388            id: WorkItemId::new(id),
389            coord: (kb, c),
390            out: TileRect {
391                row0_t: (kb0 + kb) * m + c * rows,
392                col0_t: 0,
393                h_t: rows,
394                w_t: 1,
395            },
396        })
397        .collect();
398    let n_items = items.len();
399    let mut p = t.finish(
400        format!("q8_view_{m}x{k}_from{kb0}"),
401        items,
402        AccPolicy::for_result(dt),
403        NumericsClaim::Exact,
404        target,
405    )?;
406    p.work.plan = Some(BlockPlan {
407        per_site_m: rows,
408        per_site_n: 1,
409        // A section writing PART of the view counts its items, as every part-writing relayout does.
410        grid: if part {
411            (1, n_items)
412        } else {
413            (kbs * chunks, 1)
414        },
415        inner: InnerLoop {
416            num_blocks: 1,
417            block_k_tiles: 1,
418        },
419        block: BlockShape {
420            osh: rows,
421            osw: 1,
422            ibw: 1,
423        },
424        acc: AccPolicy::for_result(dt),
425        pack_accumulate: false,
426        engines: EnginePacking::LoadsTogether,
427        granularity: Granularity::PerTile,
428    });
429    Ok(p)
430}
431
432/// The fold: item `y` owns `wb` column tiles of the product for all `m` rows; its K blocks are the weight's tile rows
433/// 32 at a time. Row `r`'s block dots read tile `r` of the view's K-block page; every row reads the same int8 and scale
434/// pages.
435///
436/// ★ A K THAT IS NOT WHOLE BLOCKS ENDS IN A TAIL. The last `K mod 1024` rows — whole Q8_0 blocks, `g` of them — are one
437/// more block, `g` tile rows of values and `g` scale rows, folded ONCE per item outside the K loop and added into the
438/// result: a tensor-parallel chip's rows of a row-split weight (768, 2,176) read as stored like any other.
439#[allow(clippy::too_many_arguments)]
440fn q8_fold(
441    names: &MatmulNames,
442    x_tiles: &str,
443    (m, k, n): (usize, usize, usize),
444    wb: usize,
445    (values_init, scales_init): (&TensorInit, &TensorInit),
446    precision: Precision,
447    acc: Precision,
448    target: &SpatialTarget,
449) -> Result<SpatialProgram, SpatialReject> {
450    let (tr, tw) = (target.tile.rows, target.tile.cols);
451    let block_rows = tr * Q8_BLOCK;
452    let (n_t, items, nb, g_tail) = (
453        n.div_ceil(tw),
454        n.div_ceil(tw) / wb,
455        k / block_rows,
456        (k % block_rows) / Q8_BLOCK,
457    );
458    let (w1, wn) = (page_width(1, target), page_width(wb, target));
459    let mut t = Template::new(target)?;
460    let xt = t.tensor(
461        x_tiles,
462        TensorRole::Input,
463        q8_view_rows(m, k),
464        Q8_BLOCK,
465        precision,
466        TensorInit::External,
467    );
468    let qt = t.tensor(
469        &stored_name(&names.b, Q8Part::Values),
470        TensorRole::Param,
471        k,
472        n,
473        Precision::Int8,
474        values_init.clone(),
475    );
476    let st = t.tensor(
477        &stored_name(&names.b, Q8Part::Scales),
478        TensorRole::Param,
479        k / Q8_BLOCK,
480        n,
481        acc,
482        scales_init.clone(),
483    );
484    let yt = t.tensor(&names.c, TensorRole::Output, m, n, acc, TensorInit::Zero);
485    let load = |t: &mut Template,
486                name: &str,
487                tensor: TensorId,
488                (base, steps): (usize, Vec<(LoopVar, usize)>),
489                (h, w, stride): (usize, usize, usize),
490                blocks: usize,
491                into: BufferId| {
492        let origin = TileAddr {
493            base,
494            steps,
495            indirect: None,
496        };
497        t.stage(
498            name,
499            StageKind::Load {
500                tensor,
501                pattern: AccessPattern {
502                    origin,
503                    block_h: h,
504                    block_w: w,
505                    stride_w: 1,
506                    stride_h: stride,
507                },
508                num_blocks: blocks,
509                multicast: None,
510            },
511            Vec::new(),
512            vec![into],
513        )
514    };
515    let map = |base: i64, terms: &[(LoopVar, i64)]| ElemAccess {
516        base,
517        terms: terms.to_vec(),
518        indirect: None,
519    };
520    let cols = wb * tw;
521    let named = |stem: &str, r: usize| {
522        if m == 1 {
523            stem.to_string()
524        } else {
525            format!("{stem}_{r}")
526        }
527    };
528    // One block of `g` Q8_0 blocks, every row, ONE stage a row: `y[r, j] = Σ_b s[b, j]·Σ_i x[r, 32b + i]·q[32b + i, j]` —
529    // the lanes `i` contracted, the block's `g` Q8_0 blocks `b` folded down each column. Row `r`'s block is tile `r` of
530    // the view page, its row `b`; the values' page holds block `b`'s tile row as tiles `b·wb …`, row `i`; the scales'
531    // page holds block `b`'s scales in its row `b`. Returns each row's stage and sum.
532    let block_sums = |t: &mut Template,
533                      stem: &str,
534                      (x_in, q_in, s_in): (BufferId, BufferId, BufferId),
535                      g: usize|
536     -> Vec<(StageId, BufferId)> {
537        let q_plane = (g * wb * tw) as i64;
538        (0..m)
539            .map(|r| {
540                let y_blk = t.scratch_deep(&named(stem, r), 1, wb, acc);
541                let iters = vec![
542                    IterSpec {
543                        var: LV_GEN_ROW,
544                        extent: g,
545                        kind: IterKind::Reduce {
546                            pool: ReducePool::Sum,
547                        },
548                    },
549                    IterSpec {
550                        var: LV_GEN_COL,
551                        extent: cols,
552                        kind: IterKind::Parallel,
553                    },
554                    IterSpec {
555                        var: LV_GEN_K,
556                        extent: Q8_BLOCK,
557                        kind: IterKind::Reduce {
558                            pool: ReducePool::Sum,
559                        },
560                    },
561                ];
562                let s_blk = t.stage(
563                    &named(stem, r),
564                    StageKind::Compute(ComputeOp::Generic {
565                        iters,
566                        ins: vec![
567                            map(
568                                (r * w1) as i64,
569                                &[(LV_GEN_ROW, (m * w1) as i64), (LV_GEN_K, 1)],
570                            ),
571                            map(
572                                0,
573                                &[
574                                    (LV_GEN_K, q_plane),
575                                    (LV_GEN_ROW, cols as i64),
576                                    (LV_GEN_COL, 1),
577                                ],
578                            ),
579                            map(0, &[(LV_GEN_ROW, cols as i64), (LV_GEN_COL, 1)]),
580                        ],
581                        out: per_col_map(),
582                        body: TensorBody::Mul(
583                            Box::new(TensorBody::Mul(
584                                Box::new(TensorBody::Operand(0)),
585                                Box::new(TensorBody::Operand(1)),
586                            )),
587                            Box::new(TensorBody::Operand(2)),
588                        ),
589                    }),
590                    vec![x_in, q_in, s_in],
591                    vec![y_blk],
592                );
593                (s_blk, y_blk)
594            })
595            .collect()
596    };
597    // Row `r`'s sum into row `r`: `a + Σ_r [row = r]·sum_r` — a single row's sum is every row's.
598    let row_of = |r: usize, at: usize| {
599        let op = Box::new(TensorBody::Operand(at));
600        let zero = || Box::new(TensorBody::Const(0.0f64.to_bits()));
601        if m == 1 {
602            op
603        } else {
604            Box::new(TensorBody::Select {
605                cond: Pred::AxisLt(LV_GEN_ROW, PredRhs::Const(r + 1)),
606                then: Box::new(TensorBody::Select {
607                    cond: Pred::AxisGe(LV_GEN_ROW, PredRhs::Const(r)),
608                    then: op,
609                    otherwise: zero(),
610                }),
611                otherwise: zero(),
612            })
613        }
614    };
615    let pointwise = vec![
616        IterSpec {
617            var: LV_GEN_ROW,
618            extent: tr,
619            kind: IterKind::Parallel,
620        },
621        IterSpec {
622            var: LV_GEN_COL,
623            extent: cols,
624            kind: IterKind::Parallel,
625        },
626    ];
627    let mut edges = Vec::new();
628    // The whole blocks, folded: K block `t` is the view's tile rows `m·t …`, the values' tile rows `32t … 32t + 31`, the
629    // scales' tile row `t` — zero before the first, each block's sums folded in, the carry read after the last.
630    let run = (nb > 0).then(|| {
631        let x_in = t.buffer("in_x", m, precision);
632        let q_in = t.buffer("in_q8", tr * wb, Precision::Int8);
633        let s_in = t.buffer("in_scale", wb, acc);
634        let l_x = load(
635            &mut t,
636            "load_x",
637            xt,
638            (0, vec![(LV_KBLOCK, m)]),
639            (m, 1, 1),
640            nb,
641            x_in,
642        );
643        let l_q = load(
644            &mut t,
645            "load_q8",
646            qt,
647            (0, vec![(LV_KBLOCK, tr * n_t), (LV_ITEM_Y, wb)]),
648            (tr, wb, n_t),
649            nb,
650            q_in,
651        );
652        let l_s = load(
653            &mut t,
654            "load_scale",
655            st,
656            (0, vec![(LV_KBLOCK, n_t), (LV_ITEM_Y, wb)]),
657            (1, wb, n_t),
658            nb,
659            s_in,
660        );
661        let sums = block_sums(&mut t, "block", (x_in, q_in, s_in), Q8_BLOCK);
662        edges.extend([
663            (l_x, x_in, sums[0].0),
664            (l_q, q_in, sums[0].0),
665            (l_s, s_in, sums[0].0),
666        ]);
667        let run = t.scratch_deep("run_sum", 2, wb, acc);
668        t.stage(
669            "init_sum",
670            StageKind::Compute(ComputeOp::Generic {
671                iters: pointwise.clone(),
672                ins: Vec::new(),
673                out: ident_map(wn),
674                body: TensorBody::Const(0.0f64.to_bits()),
675            }),
676            Vec::new(),
677            vec![run],
678        );
679        let update = (0..m).fold(TensorBody::Operand(0), |body, r| {
680            TensorBody::Add(Box::new(body), row_of(r, r + 1))
681        });
682        let mut fold_ins = vec![run];
683        fold_ins.extend(sums.iter().map(|(_, b)| *b));
684        t.stage(
685            "fold_sum",
686            StageKind::Compute(ComputeOp::Generic {
687                iters: pointwise.clone(),
688                ins: vec![ident_map(wn); m + 1],
689                out: ident_map(wn),
690                body: update,
691            }),
692            fold_ins,
693            vec![run],
694        );
695        run
696    });
697    // The tail, once per item: the view's tile rows `m·nb …`, the values' `g` tile rows from `32·nb`, the scales' tile row
698    // `nb` (its first `g` rows).
699    let tail = (g_tail > 0).then(|| {
700        let x_in = t.buffer_deep("in_x_tail", 1, m, precision);
701        let q_in = t.buffer_deep("in_q8_tail", 1, g_tail * wb, Precision::Int8);
702        let s_in = t.buffer_deep("in_scale_tail", 1, wb, acc);
703        let l_x = load(
704            &mut t,
705            "load_x_tail",
706            xt,
707            (nb * m, Vec::new()),
708            (m, 1, 1),
709            1,
710            x_in,
711        );
712        let l_q = load(
713            &mut t,
714            "load_q8_tail",
715            qt,
716            (tr * nb * n_t, vec![(LV_ITEM_Y, wb)]),
717            (g_tail, wb, n_t),
718            1,
719            q_in,
720        );
721        let l_s = load(
722            &mut t,
723            "load_scale_tail",
724            st,
725            (nb * n_t, vec![(LV_ITEM_Y, wb)]),
726            (1, wb, n_t),
727            1,
728            s_in,
729        );
730        let sums = block_sums(&mut t, "tail", (x_in, q_in, s_in), g_tail);
731        edges.extend([
732            (l_x, x_in, sums[0].0),
733            (l_q, q_in, sums[0].0),
734            (l_s, s_in, sums[0].0),
735        ]);
736        sums
737    });
738    // The product: the carried sum, the tail's rows added in.
739    let mut ins = Vec::new();
740    let mut body = run.map(|r| {
741        ins.push(r);
742        TensorBody::Operand(0)
743    });
744    for (r, (_, b)) in tail.iter().flatten().enumerate() {
745        ins.push(*b);
746        let term = *row_of(r, ins.len() - 1);
747        body = Some(match body {
748            Some(sum) => TensorBody::Add(Box::new(sum), Box::new(term)),
749            None => term,
750        });
751    }
752    let out = t.buffer("out", wb, acc);
753    let body = body.expect("a product has whole blocks or a tail");
754    let s_out = t.stage(
755        "result",
756        StageKind::Compute(ComputeOp::Generic {
757            iters: pointwise,
758            ins: vec![ident_map(wn); ins.len()],
759            out: ident_map(wn),
760            body,
761        }),
762        ins,
763        vec![out],
764    );
765    let st_y = t.store_block("store_y", yt, 0, vec![(LV_ITEM_Y, wb)], 1, wb, wb, out);
766    edges.push((s_out, out, st_y));
767    for (from, buf, to) in edges {
768        t.edge(from, buf, to);
769    }
770    let work: Vec<WorkItem> = (0..items)
771        .map(|y| WorkItem {
772            id: WorkItemId::new(y),
773            coord: (y, 0),
774            out: TileRect {
775                row0_t: 0,
776                col0_t: y * wb,
777                h_t: 1,
778                w_t: wb,
779            },
780        })
781        .collect();
782    let name = if m == 1 {
783        format!("q8_gemv_{k}x{n}_w{wb}")
784    } else {
785        format!("q8_gemm_{m}x{k}x{n}_w{wb}")
786    };
787    let mut p = t.finish(
788        name,
789        work,
790        AccPolicy::F32,
791        NumericsClaim::Approx {
792            reason: format!("a Q8_0 product over {m} {precision:?} row(s)"),
793        },
794        target,
795    )?;
796    p.work.plan = Some(BlockPlan {
797        per_site_m: 1,
798        per_site_n: wb,
799        grid: (1, items),
800        inner: InnerLoop {
801            num_blocks: nb.max(1),
802            block_k_tiles: 1,
803        },
804        block: BlockShape {
805            osh: 1,
806            osw: wb,
807            ibw: 1,
808        },
809        acc: AccPolicy::F32,
810        pack_accumulate: false,
811        engines: EnginePacking::LoadsTogether,
812        granularity: Granularity::PerTile,
813    });
814    p.work.k_tiles = nb.max(1);
815    Ok(p)
816}
817
818/// Rows of the view of an `m`-row `x` over `K`: one row's `K/32` blocks as they fall, or — for a batch — each row's K
819/// blocks a whole tile row apiece, the tail's included (its rows past `K mod 1024 / 32` zero).
820fn q8_view_rows(m: usize, k: usize) -> usize {
821    if m == 1 {
822        k / Q8_BLOCK
823    } else {
824        m * k.div_ceil(Q8_BLOCK * Q8_BLOCK) * Q8_BLOCK
825    }
826}