Skip to main content

logicaffeine_compile/tensor_graph/
facts.rs

1//! The dataflow facts every rule reads, computed once and kept current as ops are absorbed.
2//!
3//! The graph being rewritten is a list of slots in program order: each slot an op, or empty once its op is absorbed into
4//! another. Beside it, which live slots read each tensor, write it and compute it whole — updated through every change a
5//! slot takes, since the slots are reached only through these methods. A rule asks the facts instead of scanning the
6//! graph: who computes `t`, who reads it, whether anything between two slots touches it, and whether moving a slot up
7//! to another would cross a slot that sees the difference.
8
9use std::cell::{RefCell, RefMut};
10use std::collections::{BTreeSet, HashMap};
11
12use super::graph::Op;
13
14/// The tensors an op reads: its operands, and the parameters that name the rows its result, or any part's, lands in.
15pub fn reads(op: &Op) -> Vec<&str> {
16    let mut r: Vec<&str> = op.ins.iter().map(String::as_str).collect();
17    r.extend(op.row_params());
18    r.sort_unstable();
19    r.dedup();
20    r
21}
22
23/// The tensors an op writes: its destination's buffer, what it writes in place, and what it keeps.
24pub fn writes(op: &Op) -> Vec<&str> {
25    op.writes()
26}
27
28/// The tensor an op computes whole into a buffer of its own, if any.
29pub fn result(op: &Op) -> Option<&str> {
30    op.result()
31}
32
33/// The slots, and the index of what each reads, writes and computes.
34#[derive(Clone)]
35pub struct Slots {
36    slots: Vec<Option<Op>>,
37    index: RefCell<Index>,
38}
39
40#[derive(Clone, Default)]
41struct Index {
42    reads: HashMap<String, BTreeSet<usize>>,
43    writes: HashMap<String, BTreeSet<usize>>,
44    results: HashMap<String, BTreeSet<usize>>,
45    of: Vec<[Vec<String>; 3]>,
46    changed: BTreeSet<usize>,
47    moved: bool,
48}
49
50impl Index {
51    fn maps(&mut self) -> [&mut HashMap<String, BTreeSet<usize>>; 3] {
52        [&mut self.reads, &mut self.writes, &mut self.results]
53    }
54
55    fn enter(&mut self, i: usize, slot: &Option<Op>) {
56        let names = |v: Vec<&str>| v.into_iter().map(String::from).collect::<BTreeSet<_>>().into_iter().collect::<Vec<_>>();
57        let of = match slot {
58            Some(op) => [names(reads(op)), names(writes(op)), result(op).map(String::from).into_iter().collect()],
59            None => Default::default(),
60        };
61        for (map, names) in self.maps().into_iter().zip(&of) {
62            for t in names {
63                map.entry(t.clone()).or_default().insert(i);
64            }
65        }
66        self.of[i] = of;
67    }
68
69    fn leave(&mut self, i: usize) {
70        let of = std::mem::take(&mut self.of[i]);
71        for (map, names) in self.maps().into_iter().zip(&of) {
72            for t in names {
73                if let Some(at) = map.get_mut(t) {
74                    at.remove(&i);
75                    if at.is_empty() {
76                        map.remove(t);
77                    }
78                }
79            }
80        }
81    }
82}
83
84impl Slots {
85    pub fn new(ops: Vec<Op>) -> Self {
86        Slots { slots: ops.into_iter().map(Some).collect(), index: RefCell::new(Index { moved: true, ..Index::default() }) }
87    }
88
89    fn index(&self) -> RefMut<'_, Index> {
90        let mut ix = self.index.borrow_mut();
91        if ix.moved {
92            *ix = Index { of: vec![Default::default(); self.slots.len()], ..Index::default() };
93            for (i, slot) in self.slots.iter().enumerate() {
94                ix.enter(i, slot);
95            }
96        } else {
97            for i in std::mem::take(&mut ix.changed) {
98                ix.leave(i);
99                ix.enter(i, &self.slots[i]);
100            }
101        }
102        ix
103    }
104
105    fn changed(&self, i: usize) {
106        self.index.borrow_mut().changed.insert(i);
107    }
108
109    pub fn len(&self) -> usize {
110        self.slots.len()
111    }
112
113    pub fn is_empty(&self) -> bool {
114        self.slots.is_empty()
115    }
116
117    pub fn slot(&self, i: usize) -> Option<&Op> {
118        self.slots[i].as_ref()
119    }
120
121    pub fn get(&self, i: usize) -> &Op {
122        self.slots[i].as_ref().expect("a live slot")
123    }
124
125    pub fn get_mut(&mut self, i: usize) -> &mut Op {
126        self.changed(i);
127        self.slots[i].as_mut().expect("a live slot")
128    }
129
130    pub fn take(&mut self, i: usize) -> Option<Op> {
131        self.changed(i);
132        self.slots[i].take()
133    }
134
135    pub fn put(&mut self, i: usize, op: Op) {
136        self.changed(i);
137        self.slots[i] = Some(op);
138    }
139
140    /// A new slot at `i`, every slot from `i` on moved one along.
141    pub fn insert(&mut self, i: usize, op: Op) {
142        self.slots.insert(i, Some(op));
143        self.index.get_mut().moved = true;
144    }
145
146    pub fn live(&self) -> impl Iterator<Item = (usize, &Op)> {
147        self.slots.iter().enumerate().filter_map(|(i, s)| s.as_ref().map(|op| (i, op)))
148    }
149
150    pub fn into_ops(self) -> Vec<Op> {
151        self.slots.into_iter().flatten().collect()
152    }
153
154    /// The slots that read `t`, in order.
155    pub fn readers(&self, t: &str) -> Vec<usize> {
156        self.index().reads.get(t).map_or_else(Vec::new, |at| at.iter().copied().collect())
157    }
158
159    /// The slot that computes `t` whole into its own buffer.
160    pub fn producer(&self, t: &str) -> Option<usize> {
161        self.index().results.get(t).and_then(|at| at.first().copied())
162    }
163
164    /// The slot that writes `t` whole: as its result, or as a result it keeps beside it.
165    pub fn maker(&self, t: &str) -> Option<usize> {
166        self.producer(t).or_else(|| {
167            let at = self.index().writes.get(t).cloned().unwrap_or_default();
168            at.into_iter().find(|&k| self.get(k).keeps.iter().any(|kept| kept == t))
169        })
170    }
171
172    /// Whether no live slot strictly between `from` and `to`, but those of `skip`, writes `t` (`reads` too when asked).
173    pub fn untouched(&self, t: &str, from: usize, to: usize, reads: bool, skip: &[usize]) -> bool {
174        let ix = self.index();
175        let none_in = |map: &HashMap<String, BTreeSet<usize>>| {
176            map.get(t).is_none_or(|at| at.range(from + 1..to.max(from + 1)).all(|b| skip.contains(b)))
177        };
178        none_in(&ix.writes) && (!reads || none_in(&ix.reads))
179    }
180
181    /// Whether slot `j`'s op, moved up to slot `i`, would cross a live slot between them but those of `group`: one that
182    /// writes what it reads or writes, or reads what it writes.
183    pub fn crosses(&self, i: usize, j: usize, group: &[usize]) -> bool {
184        self.crosses_from(j, i, j, group)
185    }
186
187    /// Whether slot `i`'s op, moved down to slot `j`, would cross a live slot between them but those of `group`.
188    pub fn sinks_past(&self, i: usize, j: usize, group: &[usize]) -> bool {
189        self.crosses_from(i, i, j, group)
190    }
191
192    /// Whether slot `at`'s op meets a live slot strictly between `lo` and `hi`, but those of `group`, that writes what it
193    /// reads or writes, or reads what it writes.
194    fn crosses_from(&self, at: usize, lo: usize, hi: usize, group: &[usize]) -> bool {
195        let op = self.get(at);
196        let (r, w) = (reads(op), writes(op));
197        let ix = self.index();
198        let between = |map: &HashMap<String, BTreeSet<usize>>, t: &str| {
199            map.get(t).is_some_and(|slots| slots.range(lo + 1..hi.max(lo + 1)).any(|b| !group.contains(b)))
200        };
201        r.iter().chain(&w).any(|t| between(&ix.writes, t)) || w.iter().any(|t| between(&ix.reads, t))
202    }
203}
204
205/// What an op accumulates in integers: nothing, a sum whose operands' ranges no fact bounds, or every partial result
206/// within an interval.
207#[derive(Clone, Copy, Debug, PartialEq, Eq)]
208pub enum Accumulates {
209    Nothing,
210    Unproven,
211    Within(i128, i128),
212}
213
214impl Accumulates {
215    /// The fewest bits of a two's-complement integer that holds every partial result, by its largest magnitude.
216    pub fn bits(self) -> Option<u32> {
217        match self {
218            Accumulates::Within(lo, hi) => {
219                let magnitude = lo.unsigned_abs().max(hi.unsigned_abs());
220                Some(129 - magnitude.leading_zeros())
221            }
222            _ => None,
223        }
224    }
225}
226
227/// The interval every partial sum of `k` products of operands in `a` and `b` lies in: the running sums start at zero and
228/// each adds one product, every product between the corners'.
229pub fn product_sums(a: (i64, i64), b: (i64, i64), k: usize) -> (i128, i128) {
230    let corners = [a.0 as i128 * b.0 as i128, a.0 as i128 * b.1 as i128, a.1 as i128 * b.0 as i128, a.1 as i128 * b.1 as i128];
231    let (lo, hi) = (*corners.iter().min().expect("four corners"), *corners.iter().max().expect("four corners"));
232    let k = k as i128;
233    (lo.saturating_mul(k).min(0), hi.saturating_mul(k).max(0))
234}
235
236/// What `op` accumulates in integers, from the ranges `g` proves of its operands: a matrix product's running sums over
237/// its inner dimension, an elementwise sum or product, a requant's product before its shift.
238pub fn accumulator(op: &super::graph::Op, g: &super::graph::TensorGraph) -> Accumulates {
239    use super::front::TensorOpSpec;
240    use super::graph::Elem;
241    let int = |k: usize| op.ins.get(k).and_then(|t| g.tensors.get(t)).is_some_and(|t| t.elem == Elem::Int);
242    let range = |k: usize| op.ins.get(k).and_then(|t| g.tensors.get(t)).and_then(|t| t.range);
243    let two = |f: &dyn Fn((i64, i64), (i64, i64)) -> (i128, i128)| match (range(0), range(1)) {
244        (Some(a), Some(b)) => {
245            let (lo, hi) = f(a, b);
246            Accumulates::Within(lo, hi)
247        }
248        _ => Accumulates::Unproven,
249    };
250    match op.prim() {
251        Some(TensorOpSpec::Matmul(m)) if int(0) && int(1) => two(&|a, b| product_sums(a, b, m.k)),
252        Some(TensorOpSpec::Elementwise(e)) if int(0) && int(1) => match e.op {
253            crate::tensor_recognize::ElementwiseOp::Add => two(&|a, b| (a.0 as i128 + b.0 as i128, a.1 as i128 + b.1 as i128)),
254            crate::tensor_recognize::ElementwiseOp::Mul => two(&|a, b| product_sums(a, b, 1)),
255        },
256        Some(TensorOpSpec::IntRequant(q)) => match range(0) {
257            Some(x) => {
258                let (a, b) = (x.0 as i128 * q.m as i128, x.1 as i128 * q.m as i128);
259                Accumulates::Within(a.min(b), a.max(b))
260            }
261            None => Accumulates::Unproven,
262        },
263        _ => Accumulates::Nothing,
264    }
265}