1use 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
33pub type Interval = (i64, i64);
35
36pub const UNBOUNDED: Interval = (i64::MIN, i64::MAX);
38
39const 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
46fn 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
55pub 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 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
77fn 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
88fn 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
107fn 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
139fn 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 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 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
189pub 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 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#[derive(Clone, Debug, PartialEq, Eq)]
239pub struct ProgramRanges {
240 pub tensors: Vec<Option<Interval>>,
242 pub buffers: Vec<Option<Interval>>,
244 pub stages: Vec<StageRanges>,
246}
247
248#[derive(Clone, Debug, Default, PartialEq, Eq)]
250pub struct StageRanges {
251 pub nodes: Vec<Interval>,
255 pub held: Option<Interval>,
258}
259
260pub 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
266fn 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
271fn 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
291fn 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
310fn 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
320fn 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
370fn 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
379fn relu(x: Interval) -> Interval {
381 (x.0.max(0), x.1.max(0))
382}
383
384fn 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
391fn 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
400fn 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 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 let out = packed(join(result, (0, 0)));
484 (Some(out), StageRanges { nodes, held: Some(join(held, out)) })
485 }
486 }
487}
488
489pub 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 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 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 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;