Skip to main content

logicaffeine_compile/tensor_graph/
numerics.rs

1//! Quantize insertion: every product that reads its activation quantized, as the chip's shim asks, gets that activation's
2//! quantized form — from its producer's epilogue where the chip runs that whole (a `*_quant` class: the producer and the
3//! quantize as one op), or from a quantize op of its own right after the producer.
4//!
5//! A product reading a row of a tensor in place at a whole block reads the tensor's quantized form in place too, so the
6//! tensor is quantized once, at the root of its views, for every row a product reads of it. A tensor no op computes whole
7//! — a table assembled a row at a time — is quantized right before the first product that reads it, once its last row
8//! has landed; or, where each of its rows is set, once and in whole blocks, from a result its producer can quantize in
9//! its epilogue — a batch's sequences' rows — a row at a time by those producers, each set carrying its row's quantized
10//! form into the table's (PARITY-BATCH-ROW-QUANT).
11
12use std::collections::BTreeSet;
13
14use super::facts;
15use super::fuse::{binary, candidate, class, gemv, norm, Rewriter};
16use super::front::TensorOpSpec;
17use super::graph::{Access, Dst, Op, QuantFormat, Sym};
18use super::symbols::ClassId;
19use crate::tensor_recognize::qwen::RowIndex;
20
21/// The values of an activation's quantized form that share one scale.
22pub const BLOCK: usize = 32;
23
24/// A quantize of `x` in `format`, in place.
25fn quantize_op(x: &str, format: QuantFormat) -> Op {
26    Op { sym: Sym::Quantize(format), ins: vec![x.to_string()], access: vec![Access::Whole], out: x.to_string(), dst: Dst::to(x), keeps: Vec::new(), covers: Vec::new() }
27}
28
29/// The ops that quantize `x` a row at a time: where every row of the table `x` is set — once, in whole blocks, by a
30/// literal index — from a result read by nothing but its set and unseen by the host, whose producer the chip runs with
31/// that result's quantize in its epilogue, and the chip runs each set carrying its row's quantized form into the
32/// table's (`row_write_quant`): each producer and each set, with the class and the quantize it takes in; nothing
33/// otherwise.
34fn quantized_rows(r: &mut Rewriter, x: &str, format: QuantFormat) -> Option<Vec<(usize, ClassId, Op)>> {
35    let t = r.g.tensors.get(x)?;
36    let (rows_of, cols_of) = (t.rows, t.cols);
37    let sets: Vec<usize> = r.s.live().filter(|(_, op)| facts::writes(op).contains(&x)).map(|(k, _)| k).collect();
38    let (mut rows, mut hosts) = (BTreeSet::new(), Vec::new());
39    for k in sets {
40        let set = r.s.get(k);
41        let Some(TensorOpSpec::SetRow(sr)) = set.prim() else { return None };
42        let RowIndex::Literal(row) = sr.index else { return None };
43        if (sr.rows, sr.cols, sr.count) != (rows_of, cols_of, 1) || sr.cols % BLOCK != 0 || set.ins[0] != x || !rows.insert(row) {
44            return None;
45        }
46        let src = set.ins[1].clone();
47        let p = r.s.producer(&src)?;
48        if r.s.readers(&src) != [k] || r.observed(&src) {
49            return None;
50        }
51        let host = r.s.get(p).clone();
52        let class = folded_class(&host, r.g)?;
53        let quant = quantize_op(&src, format);
54        let cand = candidate(class, vec![host.clone(), quant.clone()], host.ins.clone(), &host.out, host.dst.clone());
55        let set = r.s.get(k).clone();
56        let carried = quantize_op(x, format);
57        let carry = candidate(ClassId::RowWriteQuant, vec![set.clone(), carried.clone()], set.ins.clone(), &set.out, set.dst.clone());
58        if !(r.admits(&cand) && r.admits(&carry)) {
59            return None;
60        }
61        hosts.push((p, class, quant));
62        hosts.push((k, ClassId::RowWriteQuant, carried));
63    }
64    (rows.len() == rows_of && rows.iter().copied().eq(1..=rows_of as i64)).then_some(hosts)
65}
66
67/// Fold `quant` into the epilogue of the op at `p` as one op of `class`: one firing of `fused_quant`.
68fn fold_into(r: &mut Rewriter, p: usize, class: ClassId, quant: Op) {
69    let host = r.s.take(p).expect("a live slot");
70    let mut covers = host.covers.clone();
71    covers.sort_unstable();
72    let (ins, access, out, dst, keeps) = (host.ins.clone(), host.access.clone(), host.out.clone(), host.dst.clone(), host.keeps.clone());
73    let parts = vec![host, quant];
74    r.s.put(p, Op { sym: Sym::Fused(super::graph::Fused { class, parts }), ins, access, out, dst, keeps, covers });
75    r.record("fused_quant", class, p);
76}
77
78/// The class a producer's epilogue quantize builds, by what the producer is: a norm, an elementwise op, a gated pair or
79/// SwiGLU, a delta rule, an attention, a product — or, for an op a rule built, the member that computes its result.
80fn folded_class(op: &Op, g: &super::graph::TensorGraph) -> Option<ClassId> {
81    if let Some(c) = direct_class(op).or_else(|| gemv(op, g).map(|_| ClassId::GemvQuant)) {
82        return Some(c);
83    }
84    match &op.sym {
85        Sym::Fused(f) => f.parts.iter().rev().find(|p| p.out == op.out).and_then(|p| folded_class(p, g)),
86        _ => None,
87    }
88}
89
90fn direct_class(op: &Op) -> Option<ClassId> {
91    if let Some(n) = norm(op) {
92        return Some(if n.add.is_some() { ClassId::AddNormQuant } else { ClassId::NormQuant });
93    }
94    if let Some(b) = binary(op) {
95        return Some(if b.gate.is_some() { ClassId::GluActMulQuant } else { ClassId::ElementwiseQuant });
96    }
97    match (op.prim(), class(op)) {
98        (Some(TensorOpSpec::SwiGlu(_)), _) | (_, Some(ClassId::GemvGateUpGlu)) => Some(ClassId::GluActMulQuant),
99        (Some(TensorOpSpec::GatedDelta(_)), _) => Some(ClassId::GdnOutputQuant),
100        (Some(TensorOpSpec::Attention(_)), _) => Some(ClassId::AttnOutputQuant),
101        (Some(TensorOpSpec::FusedRow(_)), _) => Some(ClassId::NormQuant),
102        _ => None,
103    }
104}
105
106/// Give every product that reads its activation quantized that activation's quantized form: in its producer's epilogue
107/// where the chip runs the two as one op and `fold` allows it, otherwise as a quantize of its own.
108pub fn quantize(r: &mut Rewriter, fold: bool) {
109    // The products' quantized activations, each at the root of the in-place views a product reads it through.
110    let views: std::collections::BTreeMap<String, (String, usize)> = r
111        .s
112        .live()
113        .filter_map(|(_, op)| match op.sym {
114            Sym::View { offset } => Some((op.out.clone(), (op.ins[0].clone(), offset))),
115            _ => None,
116        })
117        .collect();
118    let mut read: BTreeSet<(String, super::graph::QuantFormat)> = BTreeSet::new();
119    for (_, op) in r.s.live() {
120        for k in 0..op.ins.len() {
121            if let Some(f) = r.shim.activation(op, k, r.g) {
122                read.insert((op.ins[k].clone(), f));
123            }
124        }
125    }
126    let mut needed: Vec<(String, super::graph::QuantFormat)> = Vec::new();
127    for (x, f) in read {
128        let mut root = x;
129        while let Some((src, _)) = views.get(&root).filter(|(_, offset)| offset % BLOCK == 0) {
130            root = src.clone();
131        }
132        if !needed.iter().any(|(n, _)| *n == root) {
133            needed.push((root, f));
134        }
135    }
136    needed.sort();
137    for (x, format) in needed {
138        let Some(n) = r.g.tensors.get(&x).map(|t| t.len()) else { continue };
139        if n % BLOCK != 0 {
140            continue;
141        }
142        // The ops that read `x`, each with the name it reads it by: `x`, or a view reading its blocks in place.
143        let mut readers: Vec<(usize, String)> = Vec::new();
144        let mut names = vec![x.clone()];
145        while let Some(t) = names.pop() {
146            for at in r.s.readers(&t) {
147                match r.s.get(at).sym {
148                    Sym::View { offset } if offset % BLOCK == 0 => names.push(r.s.get(at).out.clone()),
149                    _ => readers.push((at, t.clone())),
150                }
151            }
152        }
153        readers.sort();
154        let quantized = |r: &Rewriter, at: usize, t: &str| {
155            let op = r.s.get(at);
156            op.ins.iter().enumerate().any(|(k, n)| n == t && r.shim.activation(op, k, r.g).is_some())
157        };
158        let producer = r.s.producer(&x);
159        let quant = quantize_op(&x, format);
160        // The producer's epilogue, where the chip runs the producer and the quantize as one op.
161        let folded = producer.filter(|_| fold).and_then(|p| {
162            let host = r.s.get(p);
163            let class = folded_class(host, r.g)?;
164            // The shim reads the producer as it stands, then the quantize it would take into its epilogue.
165            let cand = candidate(class, vec![host.clone(), quant.clone()], host.ins.clone(), &host.out, host.dst.clone());
166            r.admits(&cand).then_some((p, class))
167        });
168        // A table set a row at a time, its rows quantized by the producers that store them.
169        let rows = if folded.is_none() && fold { quantized_rows(r, &x, format) } else { None };
170        match (folded, rows) {
171            (Some((p, class)), _) => fold_into(r, p, class, quant),
172            (None, Some(hosts)) => {
173                for (p, class, quant) in hosts {
174                    fold_into(r, p, class, quant);
175                }
176            }
177            (None, None) => {
178                let first_product = readers.iter().find(|(at, t)| quantized(r, *at, t)).map_or(readers[0].0, |(at, _)| *at);
179                let at = producer.map_or(first_product, |p| p + 1);
180                r.s.insert(at, quant);
181            }
182        }
183    }
184}
185
186// ── Numerics contracts (benchmarks/FUSIONS_WE_NEED.md, OPT-6) ─────────────────────────────────────────────────────
187
188/// How far a value a chip keeps may lie from the value the meaning computes: `of_block` of the largest magnitude in its
189/// block, plus `of_value` of its own magnitude, plus `floor`.
190#[derive(Clone, Copy, Debug, PartialEq)]
191pub struct Bound {
192    pub of_block: f64,
193    pub of_value: f64,
194    pub floor: f64,
195}
196
197impl Bound {
198    /// Bit for bit.
199    pub const EXACT: Bound = Bound { of_block: 0.0, of_value: 0.0, floor: 0.0 };
200
201    /// Whether `kept` lies within the bound of `exact`, in a block whose largest magnitude is `block_max`.
202    pub fn holds(&self, exact: f64, kept: f64, block_max: f64) -> bool {
203        (exact - kept).abs() <= self.of_block * block_max + self.of_value * exact.abs() + self.floor
204    }
205}
206
207/// The values of a quantized activation that share one scale: a fixed count, the block of the matrix format that reads
208/// it, or the row the op takes it in — a head's queries, a cached key or value row, a query's weights over a tile of
209/// keys.
210#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
211pub enum Block {
212    Values(usize),
213    OfTheWeights,
214    Row,
215}
216
217/// How a block's scale is chosen from its largest magnitude: that magnitude over 127 in `f32`; the least `m·2^e` with a
218/// `significand_bits`-bit `m` at least that; or, for values never negative, the least `2^e` with the largest at most
219/// `255·2^e`, its codes unsigned eight bits.
220#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
221pub enum Scale {
222    MaxOver127,
223    LeastPowerOfTwo { significand_bits: u32 },
224    LeastPowerOfTwoOver255,
225}
226
227/// A quantized activation's scheme: its blocks, how each block's scale is chosen, and the int8 words each value takes —
228/// one, the nearest whole number of the scale; two, a high word and a low word whose code is the nearest whole number of a
229/// finer step (the high word's rest at a 128th of the scale, or a 16-bit code at a 256th); three, a third word at a 128th
230/// of the second's step.
231#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
232pub struct Scheme {
233    pub block: Block,
234    pub scale: Scale,
235    pub words: u8,
236}
237
238impl Scheme {
239    /// The scheme of an activation quantized in `format`: blocks of [`BLOCK`] under `max / 127`.
240    pub const fn of(format: QuantFormat) -> Scheme {
241        let words = match format {
242            QuantFormat::OneWord => 1,
243            QuantFormat::TwoWords => 2,
244        };
245        Scheme { block: Block::Values(BLOCK), scale: Scale::MaxOver127, words }
246    }
247
248    /// The bound a value read in this scheme keeps: half the finest step, where a scale `max / 127` is that value or, a
249    /// least power-of-two scale, up to `1 + 2^(1 − bits)` times it, and an unsigned code's half level under its scale
250    /// at most `max / 255`; the slack terms cover the value and the scale read in `f32` (a relative 2⁻²⁴ each, a
251    /// sixteenth of a step of the block where that is less than 2⁻²⁰ of it), a denormal flushed to zero, and a scale's
252    /// floor.
253    pub fn bound(&self) -> Bound {
254        let (coarse, step) = match (self.scale, self.words) {
255            (Scale::MaxOver127, words) => (1.0, 1.0 / (254.0 * 128f64.powi(i32::from(words) - 1))),
256            (Scale::LeastPowerOfTwoOver255, _) => (1.0, 1.0 / 255.0),
257            (Scale::LeastPowerOfTwo { significand_bits }, words) => {
258                let coarse = 1.0 + 2f64.powi(1 - significand_bits as i32);
259                (coarse, if words == 1 { 1.0 / 254.0 } else { 1.0 / (254.0 * 256.0) })
260            }
261        };
262        Bound { of_block: step * coarse + (step * coarse / 16.0).min(2f64.powi(-20)), of_value: 2f64.powi(-24), floor: 2f64.powi(-100) }
263    }
264}
265
266/// How a chip keeps the values it stores between steps — a cache's rows, a recurrent memory: as the meaning computes
267/// them, rounded to the nearest of a narrower float, or a row at a time as int8 codes under one binary16 scale
268/// (`max / 127`), the row first turned by a Walsh–Hadamard transform where it is `Int8RowsRotated`.
269#[derive(Clone, Copy, Debug, PartialEq, Eq)]
270pub enum Keep {
271    Exact,
272    F32,
273    F16,
274    Bf16,
275    Int8Rows,
276    Int8RowsRotated,
277}
278
279impl Keep {
280    /// The bound a value kept this way keeps. A float: half an ulp of its significand of the value, and its smallest
281    /// normal where a chip flushes what lies below it. A row of int8 codes: half a level of the row's scale, the scale
282    /// rounded to binary16 (a relative 2⁻¹¹, which can push a code past ±127 to be clamped), and a scale that underflows
283    /// binary16; its block is the row as stored — turned, for a rotated row, whose error the inverse turn averages.
284    pub fn bound(self) -> Bound {
285        match self {
286            Keep::Exact => Bound::EXACT,
287            Keep::F32 => Bound { of_block: 0.0, of_value: 2f64.powi(-24), floor: 2f64.powi(-126) },
288            Keep::Int8Rows | Keep::Int8RowsRotated => Bound { of_block: (1.0 / 254.0) * (1.0 + 2f64.powi(-10)) + 2f64.powi(-11) + 2f64.powi(-19), of_value: 0.0, floor: 2f64.powi(-16) },
289            Keep::F16 => Bound { of_block: 0.0, of_value: 2f64.powi(-11) + 2f64.powi(-24), floor: 2f64.powi(-25) },
290            Keep::Bf16 => Bound { of_block: 0.0, of_value: 2f64.powi(-8) + 2f64.powi(-24), floor: 2f64.powi(-134) },
291        }
292    }
293
294    /// `v` as this way keeps it, to the nearest, ties to even; `None` for a format that keeps a row at a time.
295    pub fn keep(self, v: f64) -> Option<f64> {
296        Some(match self {
297            Keep::Int8Rows | Keep::Int8RowsRotated => return None,
298            Keep::Exact => v,
299            Keep::F32 => f64::from(v as f32),
300            Keep::F16 => f64::from(crate::model::quant::f16_to_f32(crate::model::quant::f32_to_f16(v as f32))),
301            Keep::Bf16 => {
302                let x = v as f32;
303                if x.is_nan() {
304                    return Some(f64::from(x));
305                }
306                let bits = x.to_bits();
307                f64::from(f32::from_bits(bits.wrapping_add(0x7FFF + ((bits >> 16) & 1)) & 0xFFFF_0000))
308            }
309        })
310    }
311}
312
313/// How an op takes a value: kept in a float format, or quantized in a scheme.
314#[derive(Clone, Copy, Debug, PartialEq)]
315pub enum Taken {
316    Kept(Keep),
317    Quantized(Scheme),
318}
319
320/// How an attention a chip computes on narrower units than its values takes its operands: its queries, its keys and
321/// values, and its weights — each weight times its value row's scale where the values are quantized.
322#[derive(Clone, Copy, Debug, PartialEq)]
323pub struct AttentionNumerics {
324    pub queries: Taken,
325    pub keys_values: Taken,
326    pub weights: Taken,
327}
328
329/// What a chip computes at one knob point, and how far it may lie from the meaning: the scheme a product by a matrix in a
330/// block format reads its activation in (`None`, its values), how the chip keeps a cache's rows and a recurrent memory,
331/// how an attention takes its operands where it narrows them (`None`, it attends on its values as the chip keeps them),
332/// how far a fused op may lie from its parts run apart ([`Bound::EXACT`], bit for bit), and the widest integer a product
333/// or a sum of integers accumulates in.
334#[derive(Clone, Debug, PartialEq)]
335pub struct Contract {
336    /// The knob point, by name.
337    pub point: String,
338    pub activations: Option<Scheme>,
339    pub cache: Keep,
340    pub state: Keep,
341    pub attention: Option<AttentionNumerics>,
342    pub fused: Bound,
343    pub accumulator_bits: u32,
344}
345
346/// Whether a chip whose integers accumulate in `bits` holds every partial result `op` accumulates: `Ok`, or why not —
347/// the bits its operands' proven ranges need, or an operand no range bounds on a chip narrower than the language's own
348/// 64-bit integers.
349pub fn holds_accumulator(op: &Op, g: &super::graph::TensorGraph, bits: u32) -> Result<(), String> {
350    use super::facts::{accumulator, Accumulates};
351    for leaf in super::fuse::leaves(op.clone()) {
352        match accumulator(&leaf, g) {
353            Accumulates::Nothing => {}
354            Accumulates::Unproven if bits >= 64 => {}
355            Accumulates::Unproven => return Err(format!("`{}` sums integers no proven range bounds, in a {bits}-bit accumulator", leaf.out)),
356            within => {
357                let need = within.bits().expect("a proven interval");
358                if need > bits {
359                    return Err(format!("`{}`'s partial sums need {need} bits, past the {bits}-bit accumulator", leaf.out));
360                }
361            }
362        }
363    }
364    Ok(())
365}