Skip to main content

logicaffeine_compile/spatial/
describe.rs

1//! `describe` — what a lowered program actually DOES, as facts derived from the IR.
2//!
3//! ★ WHY THIS EXISTS: A KERNEL MUST NOT BE ABLE TO ASSERT ITS OWN CORRECTNESS IN PROSE.
4//!
5//! The tests this replaces asked the emitted C++ whether it did the right thing, by substring. That is
6//! a question generated text can always answer favourably. The concrete case:
7//! `fused_row_family_lock` asserted `contains("per-row rmsnorm (weighted)")` and passed for as long as
8//! the retired template PRINTED THAT PHRASE IN A COMMENT — while its body declared `cb_w` and never
9//! read it. The kernel claimed to apply a weight, in English, and did not apply one.
10//!
11//! Swapping the needle for an instruction name (`contains("mul_tiles_bcast_rows")`) does not fix that;
12//! it moves the same hole one level down, because a comment can contain an instruction name just as
13//! easily as a sentence. The only durable answer is to stop asking the TEXT.
14//!
15//! So these facts are computed from the [`SpatialProgram`] — from iterator kinds and access maps, the
16//! things the compiler actually decided — and never from a string. A reduction is counted because an
17//! axis is marked [`IterKind::Reduce`]. A per-column weight is counted because an operand's access map
18//! OMITS the row axis, which is what makes it constant down the rows. Nothing a printer emits, and no
19//! comment anyone writes, can change either number.
20
21use std::collections::BTreeSet;
22
23use super::ir::{ComputeOp, IterKind, SfpuKind, SpatialProgram, StageKind, TensorBody};
24
25/// What a program's compute stages do, in the vocabulary of the algebra rather than of a backend.
26#[derive(Clone, Debug, Default, PartialEq, Eq)]
27pub struct ComputeFacts {
28    /// Compute stages in the program.
29    pub stages: usize,
30    /// Stages that fold an axis away — the row reductions.
31    pub reductions: usize,
32    /// Stages applying a value that is constant DOWN THE ROWS: a per-COLUMN vector, which is what a
33    /// normalization's weight and bias are. The access map omits the row axis.
34    pub per_column_operands: usize,
35    /// Stages applying a value that is constant ACROSS A ROW: a per-ROW scalar, which is what a
36    /// reduction's result is when it is broadcast back. The access map omits the column axis.
37    pub per_row_operands: usize,
38    /// Every SFPU transform the program applies, anywhere in a body (including nested inside
39    /// arithmetic, which is where fusion puts them).
40    pub sfpu: BTreeSet<SfpuKind>,
41}
42
43impl ComputeFacts {
44    /// Whether the program applies a per-column vector — a weight or a bias.
45    pub fn applies_a_per_column_operand(&self) -> bool {
46        self.per_column_operands > 0
47    }
48    /// Whether the program broadcasts a per-row scalar back across its row — a rescale.
49    pub fn rescales_by_a_per_row_scalar(&self) -> bool {
50        self.per_row_operands > 0
51    }
52    /// Whether the program applies a given SFPU transform anywhere.
53    pub fn uses(&self, kind: SfpuKind) -> bool {
54        self.sfpu.contains(&kind)
55    }
56}
57
58fn collect_sfpu(b: &TensorBody, acc: &mut BTreeSet<SfpuKind>) {
59    match b {
60        TensorBody::Unary { kind, arg } => {
61            acc.insert(*kind);
62            collect_sfpu(arg, acc);
63        }
64        TensorBody::Add(x, y)
65        | TensorBody::Sub(x, y)
66        | TensorBody::Mul(x, y)
67        | TensorBody::Min(x, y)
68        | TensorBody::Max(x, y)
69        | TensorBody::FloorDiv(x, y) => {
70            collect_sfpu(x, acc);
71            collect_sfpu(y, acc);
72        }
73        TensorBody::Shr { arg, .. } => collect_sfpu(arg, acc),
74        TensorBody::Select {
75            then, otherwise, ..
76        } => {
77            collect_sfpu(then, acc);
78            collect_sfpu(otherwise, acc);
79        }
80        TensorBody::Operand(_) | TensorBody::Const(_) => {}
81    }
82}
83
84/// The facts of a program, computed from its stages. Cheap and total — it never fails, because every
85/// question it answers is structural.
86pub fn compute_facts(p: &SpatialProgram) -> ComputeFacts {
87    let mut f = ComputeFacts::default();
88    for s in &p.stages {
89        let StageKind::Compute(op) = &s.kind else {
90            continue;
91        };
92        f.stages += 1;
93        let ComputeOp::Generic {
94            iters, ins, body, ..
95        } = op
96        else {
97            continue;
98        };
99        let (row_ax, col_ax) = (iters.first(), iters.get(1));
100        if iters
101            .iter()
102            .any(|a| matches!(a.kind, IterKind::Reduce { .. }))
103        {
104            f.reductions += 1;
105        }
106        collect_sfpu(body, &mut f.sfpu);
107        // An operand CONSTANT along an axis is broadcast along it. The scaler tile a reduction takes is
108        // constant along BOTH axes and is not a broadcast of data, so it is excluded from both counts.
109        for m in ins {
110            let varies_row = row_ax.map(|a| m.varies_along(a.var)).unwrap_or(true);
111            let varies_col = col_ax.map(|a| m.varies_along(a.var)).unwrap_or(true);
112            match (varies_row, varies_col) {
113                (false, true) => f.per_column_operands += 1,
114                (true, false) => f.per_row_operands += 1,
115                _ => {}
116            }
117        }
118    }
119    f
120}