Skip to main content

logicaffeine_compile/spatial/
value_range.rs

1//! Proven value ranges: the interval every integer tensor of a program holds over every run that meets the program's
2//! preconditions — every gather's index inside its table, every attention attending at least one position.
3//!
4//! The analysis interprets the program's tensor ops over intervals, in program order:
5//!
6//! - a fill holds its own least and greatest value;
7//! - a named row index holds every row its ops admit — the rows of the table a gather or a row write names, the
8//!   positions of the cache an attention reads — since it is data the host writes at every step;
9//! - a gather reads its table's range, whichever row its index names, so a step's range holds for every token and
10//!   position the host writes;
11//! - a table a row write updates holds its fill's range joined with every row ever written into it. The walk repeats
12//!   until no interval widens, so a cache carried from step to step is closed over its writes;
13//! - an integer op the analysis does not model writes a value bounded by nothing, the whole of i64 — never by the zero
14//!   its declaration starts it at.
15//!
16//! Float tensors are not ranged here.
17//!
18//! [`program_ranges`] carries the intervals through a scheduled [`SpatialProgram`]: from each tensor's declared range —
19//! which the whole-program front end sets from the intervals above — through every stage, to the interval each buffer's
20//! pages hold, each tensor holds, each node of each integer body computes and each stage keeps in its accumulator and
21//! folds. A fabric sizes its memories and its datapath by them.
22
23use std::collections::{BTreeMap, BTreeSet};
24
25use crate::spatial::ir::{
26    AccPolicy, ComputeOp, IterKind, ReducePool, SpatialProgram, StageKind, TensorBody, TensorRole,
27};
28use crate::spatial::passes::recognize_stages::{RecognizedProgram, TensorOpSpec};
29use crate::spatial::target::precision_int_bits;
30use crate::tensor_recognize::qwen::RowIndex;
31use crate::tensor_recognize::{ElementwiseOp, Fill};
32
33/// `[lo, hi]`, inclusive.
34pub type Interval = (i64, i64);
35
36/// Every value an i64 holds — the bound of a value no modeled op computes.
37pub const UNBOUNDED: Interval = (i64::MIN, i64::MAX);
38
39/// The walks after which a table that still widens is bounded by nothing.
40const MAX_WALKS: usize = 32;
41
42fn join(a: Interval, b: Interval) -> Interval {
43    (a.0.min(b.0), a.1.max(b.1))
44}
45
46/// `[lo, hi]` as an i64 interval, or [`UNBOUNDED`] past i64.
47fn fit(lo: i128, hi: i128) -> Interval {
48    if lo < i64::MIN as i128 || hi > i64::MAX as i128 {
49        UNBOUNDED
50    } else {
51        (lo as i64, hi as i64)
52    }
53}
54
55/// The interval of an integer fill over `len` elements.
56pub fn fill_range(fill: &Fill, len: usize) -> Option<Interval> {
57    match fill {
58        Fill::Const(v) => Some((*v, *v)),
59        Fill::Pattern { modulus, shift } => {
60            let last = (len.max(1) as i64 - 1).min(modulus - 1);
61            Some((*shift, last + shift))
62        }
63        // The base fill holds only where no override lands.
64        Fill::Overrides { base, sets } => {
65            let written: BTreeSet<usize> = sets.iter().map(|(i, _)| *i).filter(|i| *i < len).collect();
66            let set = sets.iter().map(|(_, v)| (*v, *v)).reduce(join);
67            if len > 0 && written.len() == len {
68                return set;
69            }
70            let base = fill_range(base, len)?;
71            Some(set.map_or(base, |s| join(base, s)))
72        }
73        Fill::ConstF(_) | Fill::FloatOverrides { .. } | Fill::RealPattern { .. } | Fill::Unknown => None,
74    }
75}
76
77/// The products of two intervals' ends, least and greatest.
78fn product(a: Interval, b: Interval) -> (i128, i128) {
79    let c = [
80        a.0 as i128 * b.0 as i128,
81        a.0 as i128 * b.1 as i128,
82        a.1 as i128 * b.0 as i128,
83        a.1 as i128 * b.1 as i128,
84    ];
85    (*c.iter().min().unwrap(), *c.iter().max().unwrap())
86}
87
88/// The first element of a declared tensor no op writes — its fill is its content.
89fn first_of(rp: &RecognizedProgram, name: &str) -> Option<i64> {
90    fn first(fill: &Fill) -> Option<i64> {
91        match fill {
92            Fill::Const(v) => Some(*v),
93            Fill::Pattern { shift, .. } => Some(*shift),
94            Fill::Overrides { base, sets } => sets.iter().rev().find(|(i, _)| *i == 0).map(|(_, v)| *v).or_else(|| first(base)),
95            Fill::ConstF(_) | Fill::FloatOverrides { .. } | Fill::RealPattern { .. } | Fill::Unknown => None,
96        }
97    }
98    let written = rp.stages.iter().any(|s| {
99        s.out == name || matches!(&s.op, TensorOpSpec::SetRow(w) if rp.name(w.table) == name)
100    });
101    if written {
102        return None;
103    }
104    rp.declared.iter().find(|d| d.name == name).and_then(|d| first(&d.fill))
105}
106
107/// The rows each named index admits: for every op that names it, the 1-based rows of the table it gathers from or
108/// writes into, or the positions of the cache it attends over — met across its ops, since a run every op accepts holds
109/// a row each of them admits. A superset of each op's precondition: a block from `P` needs `P + count − 1` among the
110/// rows as well.
111fn index_domains(rp: &RecognizedProgram) -> BTreeMap<String, Interval> {
112    let mut domains: BTreeMap<String, Interval> = BTreeMap::new();
113    let mut admit = |name: &str, rows: usize| {
114        let rows = (1, rows.max(1) as i64);
115        let met = domains.get(name).map_or(rows, |d| (d.0.max(rows.0), d.1.min(rows.1)));
116        domains.insert(name.to_string(), met);
117    };
118    let named = |index: Option<RowIndex>| match index {
119        Some(RowIndex::Named(s)) => Some(rp.name(s).to_string()),
120        _ => None,
121    };
122    for st in &rp.stages {
123        let (index, rows) = match &st.op {
124            TensorOpSpec::RowGather(g) => (named(Some(g.index)), g.rows),
125            TensorOpSpec::RowsGather(g) => (Some(rp.name(g.indices).to_string()), g.rows),
126            TensorOpSpec::SetRow(w) => (named(Some(w.index)), w.rows),
127            TensorOpSpec::Attention(a) => (named(a.pos), a.l),
128            TensorOpSpec::LutAttention(a) => (named(a.pos), a.l),
129            TensorOpSpec::Rope(r) => (named(r.from.map(|(p, _)| p)), r.from.map_or(0, |(_, n)| n)),
130            _ => continue,
131        };
132        if let Some(name) = index {
133            admit(&name, rows);
134        }
135    }
136    domains
137}
138
139/// The interval an integer op writes, from its operands' — `None` where it does not model the op.
140fn op_range(op: &TensorOpSpec, rp: &RecognizedProgram, r: &BTreeMap<String, Interval>) -> Option<Interval> {
141    let of = |s| r.get(rp.name(s)).copied();
142    match op {
143        TensorOpSpec::Matmul(m) => {
144            let (lo, hi) = product(of(m.a)?, of(m.b)?);
145            Some(fit(lo * m.k as i128, hi * m.k as i128))
146        }
147        TensorOpSpec::Elementwise(e) => {
148            let (a, b) = (of(e.a)?, of(e.b)?);
149            Some(match e.op {
150                ElementwiseOp::Add => fit(a.0 as i128 + b.0 as i128, a.1 as i128 + b.1 as i128),
151                ElementwiseOp::Mul => {
152                    let (lo, hi) = product(a, b);
153                    fit(lo, hi)
154                }
155            })
156        }
157        // `(x · m) >> s` is monotone in `x`: increasing for `m ≥ 0`, decreasing below.
158        TensorOpSpec::IntRequant(q) => {
159            let x = of(q.x)?;
160            let f = |v: i64| (v as i128 * q.m as i128) >> q.s;
161            let (a, b) = (f(x.0), f(x.1));
162            Some(fit(a.min(b), a.max(b)))
163        }
164        TensorOpSpec::IntClamp(c) => {
165            let x = of(c.x)?;
166            let clamp = |v: i64| v.max(c.lo).min(c.hi);
167            Some((clamp(x.0), clamp(x.1)))
168        }
169        TensorOpSpec::Relu(u) if u.int => {
170            let x = of(u.x)?;
171            Some((x.0.max(0), x.1.max(0)))
172        }
173        TensorOpSpec::Transpose(t) => of(t.x),
174        TensorOpSpec::RowGather(g) => of(g.table),
175        TensorOpSpec::RowsGather(g) => of(g.table),
176        TensorOpSpec::ArgMax(a) => Some((1, a.n as i64)),
177        // The attended position with the largest score is at distance zero, so it weighs the table's first entry; a
178        // first entry of at least one and no negative entry make the divisor positive, and the context — a mean of the
179        // values under weights none of which is negative, floored — lies among the values.
180        TensorOpSpec::LutAttention(a) => {
181            let table = of(a.table)?;
182            let first = first_of(rp, rp.name(a.table))?;
183            (table.0 >= 0 && first >= 1).then(|| of(a.vc)).flatten()
184        }
185        _ => None,
186    }
187}
188
189/// The proven interval of every integer tensor `rp` declares or computes, by name. `float` names the program's float
190/// tensors, which are not ranged.
191pub fn value_ranges(rp: &RecognizedProgram, float: &BTreeSet<String>) -> BTreeMap<String, Interval> {
192    let domains = index_domains(rp);
193    let mut fills: BTreeMap<String, Interval> = BTreeMap::new();
194    for d in &rp.declared {
195        if let Some(r) = fill_range(&d.fill, d.len) {
196            let held = domains.get(&d.name).map_or(r, |rows| join(r, *rows));
197            fills.insert(d.name.clone(), held);
198        }
199    }
200    let mut ranges = fills.clone();
201    for _ in 0..MAX_WALKS {
202        let before = ranges.clone();
203        for st in &rp.stages {
204            if let TensorOpSpec::SetRow(w) = &st.op {
205                let (table, source) = (rp.name(w.table).to_string(), rp.name(w.source));
206                let written = ranges.get(source).copied().unwrap_or(UNBOUNDED);
207                let held = ranges.get(&table).copied().map_or(written, |t| join(t, written));
208                ranges.insert(table, held);
209                continue;
210            }
211            if float.contains(&st.out) {
212                continue;
213            }
214            let wrote = op_range(&st.op, rp, &ranges).unwrap_or(UNBOUNDED);
215            let held = ranges.get(&st.out).copied().map_or(wrote, |r| join(r, wrote));
216            ranges.insert(st.out.clone(), held);
217        }
218        if ranges == before {
219            return ranges;
220        }
221    }
222    // A table that still widens after every walk: every value an op writes is bounded by nothing.
223    for st in &rp.stages {
224        if !float.contains(&st.out) {
225            ranges.insert(st.out.clone(), UNBOUNDED);
226        }
227        if let TensorOpSpec::SetRow(w) = &st.op {
228            ranges.insert(rp.name(w.table).to_string(), UNBOUNDED);
229        }
230    }
231    ranges
232}
233
234// ── Over the choreography ─────────────────────────────────────────────────────────────────────────────────────────
235
236/// The proven intervals of a scheduled program, each `None` where nothing is ever held: a buffer no stage fills, a
237/// stage with no arithmetic of its own.
238#[derive(Clone, Debug, PartialEq, Eq)]
239pub struct ProgramRanges {
240    /// By tensor index: every element the tensor holds over the run.
241    pub tensors: Vec<Option<Interval>>,
242    /// By buffer index: every element of every page the buffer holds.
243    pub buffers: Vec<Option<Interval>>,
244    /// By stage index.
245    pub stages: Vec<StageRanges>,
246}
247
248/// One stage's arithmetic.
249#[derive(Clone, Debug, Default, PartialEq, Eq)]
250pub struct StageRanges {
251    /// The value of each node of an integer Generic body, in the order [`TensorBody::post_order`] walks it, at every
252    /// point of the iteration space — both arms of a select, as hardware evaluates them; a requant's product before
253    /// its shift; a bias's sum. Empty for a stage with neither.
254    pub nodes: Vec<Interval>,
255    /// Every value the stage keeps in its accumulator over its run: a contraction's and a fold's partial results, a
256    /// matmul's partial sums, the values it writes.
257    pub held: Option<Interval>,
258}
259
260/// The signed two's-complement width that holds every value of `iv`.
261pub fn signed_bits(iv: Interval) -> u32 {
262    let need = |v: i64| if v >= 0 { 65 - v.leading_zeros() } else { 65 - (!v).leading_zeros() };
263    need(iv.0).max(need(iv.1)).clamp(1, 64)
264}
265
266/// The integers a precision holds — the whole of i64 for a float or an unbounded width.
267fn precision_interval(dtype: crate::codegen_sva::fabric_target::Precision) -> Option<Interval> {
268    precision_int_bits(dtype).map(|b| if b >= 64 { UNBOUNDED } else { (-(1i64 << (b - 1)), (1i64 << (b - 1)) - 1) })
269}
270
271/// `iv` after a store into `bits` bits: itself when it fits, the width's every value when it may wrap.
272fn wrapped(iv: Interval, bits: u32) -> Interval {
273    if bits >= 64 {
274        return iv;
275    }
276    let (lo, hi) = (-(1i64 << (bits - 1)), (1i64 << (bits - 1)) - 1);
277    if iv.0 >= lo && iv.1 <= hi {
278        iv
279    } else {
280        (lo, hi)
281    }
282}
283
284fn join_into(slot: &mut Option<Interval>, iv: Interval) -> bool {
285    let next = slot.map_or(iv, |h| join(h, iv));
286    let changed = *slot != Some(next);
287    *slot = Some(next);
288    changed
289}
290
291/// The interval of `⌊a / b⌋` over every divisor `b` holds but zero, at which the division faults and yields nothing.
292/// Over each sign of the divisor the quotient is monotone in each operand, so its extremes lie at the box's corners.
293fn floor_div(a: Interval, b: Interval) -> Interval {
294    let mut out: Option<Interval> = None;
295    for (d0, d1) in [(b.0.max(1), b.1), (b.0, b.1.min(-1))] {
296        if d0 > d1 {
297            continue;
298        }
299        for x in [a.0 as i128, a.1 as i128] {
300            for d in [d0 as i128, d1 as i128] {
301                let q = floor_quotient(x, d);
302                let iv = fit(q, q);
303                out = Some(out.map_or(iv, |o| join(o, iv)));
304            }
305        }
306    }
307    out.unwrap_or((0, 0))
308}
309
310/// `⌊x / d⌋` toward −∞ for a nonzero `d`, exactly.
311fn floor_quotient(x: i128, d: i128) -> i128 {
312    let q = x / d;
313    if x % d != 0 && ((x < 0) != (d < 0)) {
314        q - 1
315    } else {
316        q
317    }
318}
319
320/// The interval of every node of `b` over operands each lying in `operand(i)`, pushed in post order; the body's own.
321fn body_nodes(b: &TensorBody, operand: &dyn Fn(usize) -> Interval, nodes: &mut Vec<Interval>) -> Interval {
322    let iv = match b {
323        TensorBody::Operand(i) => operand(*i),
324        TensorBody::Const(bits) => {
325            let v = f64::from_bits(*bits) as i64;
326            (v, v)
327        }
328        TensorBody::Add(x, y) => {
329            let (a, c) = (body_nodes(x, operand, nodes), body_nodes(y, operand, nodes));
330            fit(a.0 as i128 + c.0 as i128, a.1 as i128 + c.1 as i128)
331        }
332        TensorBody::Sub(x, y) => {
333            let (a, c) = (body_nodes(x, operand, nodes), body_nodes(y, operand, nodes));
334            fit(a.0 as i128 - c.1 as i128, a.1 as i128 - c.0 as i128)
335        }
336        TensorBody::Mul(x, y) => {
337            let (a, c) = (body_nodes(x, operand, nodes), body_nodes(y, operand, nodes));
338            let (lo, hi) = product(a, c);
339            fit(lo, hi)
340        }
341        TensorBody::Min(x, y) => {
342            let (a, c) = (body_nodes(x, operand, nodes), body_nodes(y, operand, nodes));
343            (a.0.min(c.0), a.1.min(c.1))
344        }
345        TensorBody::Max(x, y) => {
346            let (a, c) = (body_nodes(x, operand, nodes), body_nodes(y, operand, nodes));
347            (a.0.max(c.0), a.1.max(c.1))
348        }
349        TensorBody::FloorDiv(x, y) => {
350            let (a, c) = (body_nodes(x, operand, nodes), body_nodes(y, operand, nodes));
351            floor_div(a, c)
352        }
353        TensorBody::Shr { arg, by } => {
354            let a = body_nodes(arg, operand, nodes);
355            (a.0 >> by, a.1 >> by)
356        }
357        TensorBody::Unary { arg, .. } => {
358            body_nodes(arg, operand, nodes);
359            UNBOUNDED
360        }
361        TensorBody::Select { then, otherwise, .. } => {
362            let (t, o) = (body_nodes(then, operand, nodes), body_nodes(otherwise, operand, nodes));
363            join(t, o)
364        }
365    };
366    nodes.push(iv);
367    iv
368}
369
370/// `(x · m) >> s`, an arithmetic shift, over `x`: the product's interval and the result's. The shift is monotone, so the
371/// product's ends give the result's.
372fn requant(x: Interval, m: i64, s: u32) -> (Interval, Interval) {
373    let (lo, hi) = product(x, (m, m));
374    let prod = fit(lo, hi);
375    let out = if prod == UNBOUNDED { (i64::MIN >> s, i64::MAX >> s) } else { (prod.0 >> s, prod.1 >> s) };
376    (prod, out)
377}
378
379/// `max(x, 0)` over `x`.
380fn relu(x: Interval) -> Interval {
381    (x.0.max(0), x.1.max(0))
382}
383
384/// The sum of `n` values each in `iv`, and every partial sum on the way: the extremes of `j · iv` for `1 ≤ j ≤ n`.
385fn sums(iv: Interval, n: usize) -> (Interval, Interval) {
386    let n = n.max(1) as i128;
387    let total = fit(n * iv.0 as i128, n * iv.1 as i128);
388    (total, join(iv, total))
389}
390
391/// A fold of `n` values in `iv` by `pool`: its result, and every value its register holds on the way.
392fn fold(pool: ReducePool, iv: Interval, n: usize) -> (Interval, Interval) {
393    match pool {
394        ReducePool::Sum => sums(iv, n),
395        ReducePool::Max => (iv, iv),
396        ReducePool::ArgMax => ((0, n.max(1) as i64 - 1), iv),
397    }
398}
399
400/// One stage's write and its arithmetic, from the intervals its input buffers hold — `None` where the stage writes
401/// nothing the analysis models.
402fn stage_effect(p: &SpatialProgram, si: usize, ins: &[Option<Interval>]) -> (Option<Interval>, StageRanges) {
403    let stage = &p.stages[si];
404    let t = p.tile.cols.max(1);
405    let trunc32 = p.plan_of_stage(stage.id).is_some_and(|pl| pl.acc == AccPolicy::I32);
406    let packed = |iv: Interval| if trunc32 { wrapped(iv, 32) } else { iv };
407    let any = |iv: Option<Interval>| iv.unwrap_or(UNBOUNDED);
408    let op = match &stage.kind {
409        StageKind::Compute(op) => op,
410        StageKind::AllReduce { combine, .. } => combine.as_ref(),
411        StageKind::Scalar(_) => return (Some(UNBOUNDED), StageRanges { nodes: Vec::new(), held: Some(UNBOUNDED) }),
412        _ => return (None, StageRanges::default()),
413    };
414    match op {
415        ComputeOp::MatmulTiles { block, num_blocks, operand, .. } => {
416            let bits = precision_int_bits(*operand).unwrap_or(64);
417            let (a, b) = (wrapped(any(ins.first().copied().flatten()), bits), wrapped(any(ins.get(1).copied().flatten()), bits));
418            let (lo, hi) = product(a, b);
419            let term = fit(lo.min(0), hi.max(0));
420            let k = num_blocks.max(&1) * block.ibw.max(1) * t;
421            let (total, _) = sums(term, k);
422            let out = packed(total);
423            (Some(join(out, (0, 0))), StageRanges { nodes: Vec::new(), held: Some(join(total, (0, 0))) })
424        }
425        ComputeOp::Requant { m, s } => {
426            let (prod, out) = requant(any(ins.first().copied().flatten()), *m, *s);
427            let out = packed(out);
428            (Some(out), StageRanges { nodes: vec![prod], held: Some(out) })
429        }
430        ComputeOp::Bias { .. } => {
431            let (x, b) = (any(ins.first().copied().flatten()), any(ins.get(1).copied().flatten()));
432            let sum = fit(x.0 as i128 + b.0 as i128, x.1 as i128 + b.1 as i128);
433            let out = packed(sum);
434            (Some(out), StageRanges { nodes: vec![sum], held: Some(out) })
435        }
436        ComputeOp::Relu => {
437            let out = packed(relu(any(ins.first().copied().flatten())));
438            (Some(out), StageRanges { nodes: Vec::new(), held: Some(out) })
439        }
440        ComputeOp::Scalar(_) => (Some(UNBOUNDED), StageRanges { nodes: Vec::new(), held: Some(UNBOUNDED) }),
441        ComputeOp::Generic { iters, body, .. } => {
442            let integral = body.integral() && stage.ins.iter().all(|b| precision_int_bits(p.buffer(*b).dtype).is_some());
443            if !integral {
444                return (Some(UNBOUNDED), StageRanges { nodes: Vec::new(), held: Some(UNBOUNDED) });
445            }
446            // An offset outside its operand's page reads zero.
447            let operand = |i: usize| join(any(ins.get(i).copied().flatten()), (0, 0));
448            let mut nodes = Vec::new();
449            let v = body_nodes(body, &operand, &mut nodes);
450            let (row, col, k) = match iters.as_slice() {
451                [r, c] => (*r, *c, None),
452                [r, c, k] => (*r, *c, Some(*k)),
453                _ => return (Some(UNBOUNDED), StageRanges { nodes, held: Some(UNBOUNDED) }),
454            };
455            let (value, mut held) = match k {
456                Some(k) => sums(v, k.extent),
457                None => (v, v),
458            };
459            let pool = |kind: IterKind| match kind {
460                IterKind::Reduce { pool } => Some(pool),
461                IterKind::Parallel => None,
462            };
463            let result = match (pool(row.kind), pool(col.kind)) {
464                (None, None) => value,
465                (None, Some(p)) => {
466                    let (r, h) = fold(p, value, col.extent);
467                    held = join(held, h);
468                    r
469                }
470                (Some(p), None) => {
471                    let (r, h) = fold(p, value, row.extent);
472                    held = join(held, h);
473                    r
474                }
475                (Some(rp), Some(cp)) => {
476                    let (inner, h) = fold(cp, value, col.extent);
477                    let (outer, ho) = fold(rp, inner, row.extent);
478                    held = join(join(held, h), join(inner, ho));
479                    outer
480                }
481            };
482            // The plane starts at zero: a point the space does not reach is packed as zero.
483            let out = packed(join(result, (0, 0)));
484            (Some(out), StageRanges { nodes, held: Some(join(held, out)) })
485        }
486    }
487}
488
489/// The intervals of `p`'s integer storage and arithmetic over every run whose tensors hold their declared ranges.
490///
491/// A tensor starts at its declared range where it has one; an `Input`, `Param` or `State` without one at every value
492/// its precision holds, since its contents may come from outside the program; an activation or a result empty, as a
493/// run starts it — a section reading it before its producer stored it faults. Every stage then carries
494/// its inputs' intervals to its outputs: a load reads its tensor, a receive its sender's page, a store joins its page
495/// into its tensor, and a compute stage writes what `stage_effect` proves. The walk repeats until no interval widens; a
496/// buffer still widening after every walk holds every value of i64. A tensor with a declared range keeps the narrower of
497/// it and what its stores prove — both hold every value it takes — and holds it as its precision stores it: an interval
498/// past the precision wraps to every value the precision holds. A load whose tensor ends inside a tile holds the tile's
499/// padding zeros as well.
500pub fn program_ranges(p: &SpatialProgram) -> ProgramRanges {
501    let declared = |i: usize| -> Option<Interval> {
502        let t = &p.tensors[i];
503        let width = precision_interval(t.dtype)?;
504        if let Some(r) = t.range {
505            return Some(r);
506        }
507        match t.role {
508            TensorRole::Input | TensorRole::Param | TensorRole::State => Some(width),
509            TensorRole::Activation | TensorRole::Output => None,
510        }
511    };
512    let start: Vec<Option<Interval>> = (0..p.tensors.len()).map(declared).collect();
513    let mut stored: Vec<Option<Interval>> = vec![None; p.tensors.len()];
514    let mut buffers: Vec<Option<Interval>> = vec![None; p.buffers.len()];
515    let mut stages: Vec<StageRanges> = vec![StageRanges::default(); p.stages.len()];
516    let sender = |g| {
517        p.stages.iter().find_map(|s| match &s.kind {
518            StageKind::Load { multicast: Some(m), .. } if *m == g => s.outs.first().copied(),
519            _ => None,
520        })
521    };
522    // The interval a read of tensor `i` sees: its start joined with every store, kept within a declared range, as its
523    // precision stores it — a value past the precision wraps.
524    let held = |start: &[Option<Interval>], stored: &[Option<Interval>], i: usize| -> Option<Interval> {
525        let both = match (start[i], stored[i]) {
526            (Some(a), Some(b)) => Some(join(a, b)),
527            (a, b) => a.or(b),
528        };
529        let kept = match (p.tensors[i].range, both) {
530            (Some(r), Some(b)) if r.0.max(b.0) <= r.1.min(b.1) => Some((r.0.max(b.0), r.1.min(b.1))),
531            (_, b) => b,
532        };
533        let bits = precision_int_bits(p.tensors[i].dtype);
534        kept.map(|iv| bits.map_or(iv, |b| wrapped(iv, b)))
535    };
536    let walks = 64;
537    for walk in 0..=walks {
538        let mut changed = false;
539        for (si, st) in p.stages.iter().enumerate() {
540            let ins: Vec<Option<Interval>> = st.ins.iter().map(|b| buffers[b.idx()]).collect();
541            let wrote: Option<Interval> = match &st.kind {
542                StageKind::Load { tensor, .. } => {
543                    let t = tensor.idx();
544                    let td = &p.tensors[t];
545                    // A tile past the tensor's edge is padded with zeros.
546                    let padded = td.tiles.rows_t * p.tile.rows > td.elems.rows || td.tiles.cols_t * p.tile.cols > td.elems.cols;
547                    held(&start, &stored, t).map(|iv| if padded { join(iv, (0, 0)) } else { iv })
548                }
549                StageKind::Recv { group } => sender(*group).and_then(|b| buffers[b.idx()]),
550                StageKind::Store { tensor, .. } => {
551                    if let Some(iv) = ins.first().copied().flatten() {
552                        changed |= join_into(&mut stored[tensor.idx()], iv);
553                    }
554                    continue;
555                }
556                StageKind::Compute(_) | StageKind::AllReduce { .. } | StageKind::Scalar(_) => {
557                    let (out, ranges) = stage_effect(p, si, &ins);
558                    if stages[si] != ranges {
559                        let merged = StageRanges {
560                            nodes: if stages[si].nodes.len() == ranges.nodes.len() {
561                                stages[si].nodes.iter().zip(&ranges.nodes).map(|(a, b)| join(*a, *b)).collect()
562                            } else {
563                                ranges.nodes.clone()
564                            },
565                            held: match (stages[si].held, ranges.held) {
566                                (Some(a), Some(b)) => Some(join(a, b)),
567                                (a, b) => a.or(b),
568                            },
569                        };
570                        changed |= stages[si] != merged;
571                        stages[si] = merged;
572                    }
573                    out
574                }
575            };
576            if let Some(iv) = wrote {
577                for b in &st.outs {
578                    changed |= join_into(&mut buffers[b.idx()], iv);
579                }
580                if let StageKind::Compute(ComputeOp::MatmulTiles { interm: Some(b), .. }) = &st.kind {
581                    changed |= join_into(&mut buffers[b.idx()], iv);
582                }
583            }
584        }
585        if !changed {
586            break;
587        }
588        if walk == walks {
589            // Still widening: every value held anywhere is bounded by nothing.
590            for b in buffers.iter_mut().filter(|b| b.is_some()) {
591                *b = Some(UNBOUNDED);
592            }
593            for t in stored.iter_mut().filter(|t| t.is_some()) {
594                *t = Some(UNBOUNDED);
595            }
596            for s in &mut stages {
597                s.nodes.iter_mut().for_each(|n| *n = UNBOUNDED);
598                s.held = s.held.map(|_| UNBOUNDED);
599            }
600        }
601    }
602    let tensors = (0..p.tensors.len()).map(|i| held(&start, &stored, i)).collect();
603    ProgramRanges { tensors, buffers, stages }
604}
605
606#[cfg(test)]
607#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/spatial/value_range/tests.rs"]
608mod tests;