1use 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
21pub const BLOCK: usize = 32;
23
24fn 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
29fn 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
67fn 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
78fn 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
106pub fn quantize(r: &mut Rewriter, fold: bool) {
109 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 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 let folded = producer.filter(|_| fold).and_then(|p| {
162 let host = r.s.get(p);
163 let class = folded_class(host, r.g)?;
164 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 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#[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 pub const EXACT: Bound = Bound { of_block: 0.0, of_value: 0.0, floor: 0.0 };
200
201 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
211pub enum Block {
212 Values(usize),
213 OfTheWeights,
214 Row,
215}
216
217#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
221pub enum Scale {
222 MaxOver127,
223 LeastPowerOfTwo { significand_bits: u32 },
224 LeastPowerOfTwoOver255,
225}
226
227#[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 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 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#[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 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 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#[derive(Clone, Copy, Debug, PartialEq)]
315pub enum Taken {
316 Kept(Keep),
317 Quantized(Scheme),
318}
319
320#[derive(Clone, Copy, Debug, PartialEq)]
323pub struct AttentionNumerics {
324 pub queries: Taken,
325 pub keys_values: Taken,
326 pub weights: Taken,
327}
328
329#[derive(Clone, Debug, PartialEq)]
335pub struct Contract {
336 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
346pub 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}