1use std::collections::{BTreeMap, BTreeSet};
24use std::fmt;
25
26use crate::intern::{Interner, Symbol};
27use crate::model::program::{Cut, Identity};
28use crate::model::spec::ModelSpec;
29use crate::spatial::value_range::{Interval, UNBOUNDED};
30use crate::model::quant::GgmlType;
31use crate::tensor_recognize::qwen::RowIndex;
32use crate::tensor_recognize::Fill;
33
34use super::front::{RecognizedProgram, TensorOpSpec};
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub enum Role {
39 Param,
40 State,
41 Constant,
42 Activation,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
47pub enum Elem {
48 Float,
49 Int,
50}
51
52#[derive(Clone, Debug, PartialEq, Eq)]
54pub enum Storage {
55 Computed,
57 Values,
59 Rows { format: GgmlType, transposed: bool },
62}
63
64#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct Tensor {
67 pub name: String,
68 pub rows: usize,
69 pub cols: usize,
70 pub role: Role,
71 pub elem: Elem,
72 pub fill: Option<Fill>,
74 pub storage: Storage,
75 pub range: Option<Interval>,
78 pub observed: bool,
80 pub carried: bool,
83}
84
85impl Tensor {
86 pub fn len(&self) -> usize {
87 self.rows * self.cols
88 }
89
90 pub fn is_empty(&self) -> bool {
91 self.len() == 0
92 }
93}
94
95#[derive(Clone, Debug, PartialEq, Eq, Hash)]
97pub enum RowSel {
98 Param(String),
99 Fixed(usize),
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
104pub struct Part {
105 pub pitch: usize,
106 pub offset: usize,
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
112pub struct Strided {
113 pub pitch: usize,
114 pub offset: usize,
115 pub width: usize,
116}
117
118#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
120pub enum Access {
121 Whole,
122 Strided(Strided),
123}
124
125#[derive(Clone, Debug, PartialEq, Eq, Hash)]
128pub struct Dst {
129 pub buf: String,
130 pub row: Option<(RowSel, usize)>,
131 pub unit: Option<Strided>,
132}
133
134impl Dst {
135 pub fn to(buf: &str) -> Dst {
136 Dst { buf: buf.to_string(), row: None, unit: None }
137 }
138
139 pub fn own(&self) -> Option<&str> {
141 (self.row.is_none() && self.unit.is_none()).then_some(self.buf.as_str())
142 }
143}
144
145#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct StepIo {
150 pub token: String,
151 pub position: String,
152 pub logits: String,
153 pub next: String,
154 pub more: Vec<SeqIo>,
155}
156
157impl StepIo {
158 pub fn positions(&self) -> Vec<&str> {
160 std::iter::once(self.position.as_str()).chain(self.more.iter().map(|q| q.position.as_str())).collect()
161 }
162
163 pub fn names(&self) -> Vec<&str> {
165 let firsts = [self.token.as_str(), self.position.as_str(), self.logits.as_str(), self.next.as_str()];
166 firsts.into_iter().chain(self.more.iter().flat_map(|q| [q.position.as_str(), q.logits.as_str(), q.next.as_str()])).collect()
167 }
168}
169
170#[derive(Clone, Debug, PartialEq, Eq)]
172pub struct SeqIo {
173 pub position: String,
174 pub logits: String,
175 pub next: String,
176}
177
178#[derive(Clone, Debug)]
180pub struct Fused {
181 pub class: super::symbols::ClassId,
182 pub parts: Vec<Op>,
183}
184
185#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
188pub enum QuantFormat {
189 OneWord,
190 TwoWords,
191}
192
193#[derive(Clone, Debug)]
195pub enum Sym {
196 Prim(TensorOpSpec),
198 Fused(Fused),
200 View { offset: usize },
202 Quantize(QuantFormat),
204}
205
206#[derive(Clone, Debug)]
209pub struct Op {
210 pub sym: Sym,
211 pub ins: Vec<String>,
212 pub access: Vec<Access>,
213 pub out: String,
214 pub dst: Dst,
215 pub keeps: Vec<String>,
216 pub covers: Vec<usize>,
217}
218
219impl Op {
220 pub fn prim(&self) -> Option<&TensorOpSpec> {
222 match &self.sym {
223 Sym::Prim(spec) => Some(spec),
224 _ => None,
225 }
226 }
227
228 pub fn kind(&self) -> &str {
230 match &self.sym {
231 Sym::Prim(spec) => spec.kind(),
232 Sym::Fused(f) => f.class.id(),
233 Sym::View { .. } => "View",
234 Sym::Quantize(_) => "Quantize",
235 }
236 }
237
238 pub fn result(&self) -> Option<&str> {
241 let own = self.dst.own()?;
242 let in_place = matches!(self.sym, Sym::Quantize(_)) || self.state_inputs().into_iter().any(|k| self.ins[k] == own);
243 (!in_place).then_some(own)
244 }
245
246 pub fn state_inputs(&self) -> Vec<usize> {
248 match &self.sym {
249 Sym::Prim(spec) => spec.state_inputs(),
250 Sym::Fused(f) => {
251 let written: BTreeSet<&str> = f.parts.iter().flat_map(|p| p.state_inputs().into_iter().map(|k| p.ins[k].as_str())).collect();
252 self.ins.iter().enumerate().filter(|(_, n)| written.contains(n.as_str())).map(|(k, _)| k).collect()
253 }
254 Sym::View { .. } | Sym::Quantize(_) => Vec::new(),
255 }
256 }
257
258 pub fn writes(&self) -> Vec<&str> {
261 let mut w: Vec<&str> = vec![self.dst.buf.as_str()];
262 w.extend(self.state_inputs().into_iter().map(|k| self.ins[k].as_str()));
263 w.extend(self.keeps.iter().map(String::as_str));
264 if let Sym::Fused(f) = &self.sym {
265 w.extend(f.parts.iter().flat_map(|p| p.writes()).filter(|t| p_lands_outside(f, t)));
266 }
267 w.sort_unstable();
268 w.dedup();
269 w
270 }
271
272 pub fn row_params(&self) -> Vec<&str> {
274 let mut r: Vec<&str> = match &self.dst.row {
275 Some((RowSel::Param(p), _)) => vec![p.as_str()],
276 _ => Vec::new(),
277 };
278 if let Sym::Fused(f) = &self.sym {
279 r.extend(f.parts.iter().flat_map(|p| p.row_params()));
280 }
281 r
282 }
283}
284
285fn p_lands_outside(f: &Fused, t: &str) -> bool {
288 f.parts.iter().any(|p| p.dst.buf == t && p.dst.own().is_none()) || f.parts.iter().any(|p| p.state_inputs().iter().any(|&k| p.ins[k] == t))
289}
290
291pub struct Touches<'g> {
293 graph: &'g TensorGraph,
294 by: BTreeMap<&'g str, BTreeSet<usize>>,
295}
296
297impl Touches<'_> {
298 pub fn read_only_by(&self, tensor: &str, writer: usize, reader: usize) -> bool {
301 self.graph.tensors.get(tensor).is_some_and(|t| t.role == Role::Activation && !t.observed)
302 && self.by.get(tensor).is_none_or(|at| at.iter().all(|&i| i == writer || i == reader))
303 }
304}
305
306#[derive(Clone, Debug)]
308pub struct Lifted {
309 pub program: RecognizedProgram,
310 pub graph: TensorGraph,
311}
312
313#[derive(Clone, Debug)]
315pub struct TensorGraph {
316 pub identity: Option<Identity>,
318 pub io: Option<StepIo>,
320 pub tensors: BTreeMap<String, Tensor>,
321 pub ops: Vec<Op>,
323}
324
325#[derive(Clone, Debug, PartialEq, Eq)]
327pub enum GraphError {
328 Unrecognized { stmt: usize, what: String },
330 UnliftedWrite { stmt: usize, tensor: String, why: &'static str },
332 Undefined { op: usize, tensor: String },
334 Empty,
336 Unwalked { why: String },
338}
339
340impl fmt::Display for GraphError {
341 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342 match self {
343 GraphError::Unrecognized { stmt, what } => write!(f, "statement {stmt} (`{what}`) is no op a lane compiles"),
344 GraphError::UnliftedWrite { stmt, tensor, why } => {
345 write!(f, "statement {stmt} writes `{tensor}` outside any op: {why}")
346 }
347 GraphError::Undefined { op, tensor } => write!(f, "op {op} reads `{tensor}` before anything defines it"),
348 GraphError::Empty => write!(f, "the program computes nothing"),
349 GraphError::Unwalked { why } => write!(f, "the front end does not walk the program: {why}"),
350 }
351 }
352}
353
354impl std::error::Error for GraphError {}
355
356fn index_operands(op: &TensorOpSpec, name: &dyn Fn(crate::intern::Symbol) -> String) -> Vec<String> {
358 let named = |i: &RowIndex| match i {
359 RowIndex::Named(s) => Some(name(*s)),
360 RowIndex::Literal(_) => None,
361 };
362 match op {
363 TensorOpSpec::RowGather(r) => named(&r.index).into_iter().collect(),
364 TensorOpSpec::RowsGather(r) => vec![name(r.indices)],
365 TensorOpSpec::SetRow(r) => named(&r.index).into_iter().collect(),
366 TensorOpSpec::Attention(a) => a.pos.as_ref().and_then(named).into_iter().collect(),
367 TensorOpSpec::LutAttention(a) => a.pos.as_ref().and_then(named).into_iter().collect(),
368 TensorOpSpec::Rope(r) => r.from.as_ref().and_then(|(p, _)| named(p)).into_iter().collect(),
369 _ => Vec::new(),
370 }
371}
372
373pub fn out_len(op: &TensorOpSpec) -> usize {
375 match op {
376 TensorOpSpec::Matmul(m) => m.m * m.n,
377 TensorOpSpec::Elementwise(e) => e.n,
378 TensorOpSpec::SwiGlu(s) => s.total,
379 TensorOpSpec::FusedRow(r) => r.rows * r.cols,
380 TensorOpSpec::Rope(r) => r.h * r.hd,
381 TensorOpSpec::Silu(s) => s.n,
382 TensorOpSpec::Sigmoid(s) => s.n,
383 TensorOpSpec::Gelu(g) => g.n,
384 TensorOpSpec::Scale(c) => c.n,
385 TensorOpSpec::Softcap(c) => c.n,
386 TensorOpSpec::Decay(d) => d.n,
387 TensorOpSpec::L2Norm(l) => l.rows * l.cols,
388 TensorOpSpec::Transpose(t) => t.rows * t.cols,
389 TensorOpSpec::HeadSlice(s) => s.n / s.every * s.width,
390 TensorOpSpec::Join(j) => j.rows * (j.wa + j.wb),
391 TensorOpSpec::HeadMatmul(m) => m.m * m.heads * m.b,
392 TensorOpSpec::CausalConv(c) => c.rows * c.chan,
393 TensorOpSpec::GatedDelta(g) => g.rows * g.hv * g.d,
394 TensorOpSpec::Attention(a) => a.rows * a.h * a.vd,
395 TensorOpSpec::FullAttention(a) => a.t * a.h * a.hd,
396 TensorOpSpec::Moe(m) => m.d,
397 TensorOpSpec::RowGather(r) => r.cols,
398 TensorOpSpec::RowsGather(r) => r.count * r.cols,
399 TensorOpSpec::ArgMax(_) => 1,
400 TensorOpSpec::SetRow(r) => r.rows * r.cols,
401 TensorOpSpec::IntRequant(r) => r.n,
402 TensorOpSpec::IntClamp(c) => c.n,
403 TensorOpSpec::Relu(r) => r.n,
404 TensorOpSpec::LutAttention(a) => a.h * a.hd,
405 }
406}
407
408impl TensorGraph {
409 pub fn lift(
412 rp: &RecognizedProgram,
413 identity: Option<Identity>,
414 io: Option<StepIo>,
415 storage: &dyn Fn(&str) -> Storage,
416 ) -> Result<TensorGraph, GraphError> {
417 if let Some((stmt, what)) = rp.unrecognized.first() {
418 return Err(GraphError::Unrecognized { stmt: *stmt, what: what.clone() });
419 }
420 if let Some((stmt, tensor, why)) = rp.unlifted_writes.first() {
421 return Err(GraphError::UnliftedWrite { stmt: *stmt, tensor: tensor.clone(), why });
422 }
423 if rp.stages.is_empty() {
424 return Err(GraphError::Empty);
425 }
426 TensorGraph::build(rp, identity, io, storage, false)
427 }
428
429 pub fn placed(&self, storage: &dyn Fn(&str) -> Storage, io: Option<StepIo>) -> TensorGraph {
432 let mut g = self.clone();
433 let carried: BTreeSet<String> = io.iter().flat_map(|io| io.names()).map(String::from).collect();
434 for t in g.tensors.values_mut() {
435 if t.role == Role::Constant {
436 t.storage = storage(&t.name);
437 }
438 t.observed |= carried.contains(&t.name);
439 }
440 g.io = io;
441 g
442 }
443
444 pub fn lift_stages(rp: &RecognizedProgram, identity: Option<Identity>, io: Option<StepIo>, storage: &dyn Fn(&str) -> Storage) -> TensorGraph {
448 TensorGraph::build(rp, identity, io, storage, true).expect("a lenient lift refuses nothing")
449 }
450
451 pub fn lift_source(source: &str, storage: &dyn Fn(&str) -> Storage) -> Result<Lifted, crate::spatial::passes::SpatialReject> {
454 let program = super::front::recognize_stages(source)?;
455 let graph = TensorGraph::lift_stages(&program, None, None, storage);
456 Ok(Lifted { program, graph })
457 }
458
459 pub fn of_cut(spec: &ModelSpec, cut: Cut, io: Option<StepIo>, storage: &dyn Fn(&str) -> Storage) -> Result<TensorGraph, GraphError> {
462 let (authored, identity) = crate::model::program::written(spec, cut);
463 let rp = super::front::recognize_stages(&authored.0).map_err(|e| GraphError::Unwalked { why: format!("{e:?}") })?;
464 TensorGraph::lift(&rp, Some(identity), io, storage)
465 }
466
467 pub fn of_op(op: TensorOpSpec, ins: Vec<(Symbol, usize, Fill)>, out: Symbol, interner: &Interner) -> TensorGraph {
471 let name = |s: Symbol| interner.resolve(s).to_string();
472 let rp = RecognizedProgram {
473 stages: vec![super::front::RecognizedStage { op, stmt_index: 0, out: name(out), ins: ins.iter().map(|(s, _, _)| name(*s)).collect() }],
474 declared: ins.iter().map(|(s, len, fill)| super::front::DeclaredTensor { name: name(*s), len: *len, fill: fill.clone() }).collect(),
475 symbols: ins.iter().map(|(s, _, _)| *s).chain([out]).map(|s| (s, name(s))).collect(),
476 ..RecognizedProgram::default()
477 };
478 TensorGraph::lift_stages(&rp, None, None, &|_| Storage::Values)
479 }
480
481 fn build(
482 rp: &RecognizedProgram,
483 identity: Option<Identity>,
484 io: Option<StepIo>,
485 storage: &dyn Fn(&str) -> Storage,
486 lenient: bool,
487 ) -> Result<TensorGraph, GraphError> {
488 let name = |s| rp.name(s).to_string();
489 let mut params = BTreeSet::new();
490 let mut state = BTreeSet::new();
491 for st in &rp.stages {
492 params.extend(index_operands(&st.op, &name));
493 for k in st.op.state_inputs() {
494 state.insert(st.ins[k].clone());
495 }
496 }
497 let stage_stmts: BTreeSet<usize> = rp.stages.iter().map(|s| s.stmt_index).collect();
499 let observed = |t: &str| {
500 !rp.opaque.is_empty() || rp.mentions.get(t).is_some_and(|at| at.iter().any(|i| !stage_stmts.contains(i) && !rp.quiet.contains(i)))
501 };
502 let floats = super::front::float_tensors(rp);
504 let ranges = crate::spatial::value_range::value_ranges(rp, &floats);
505 let elem = |t: &str| if floats.contains(t) { Elem::Float } else { Elem::Int };
506 let range = |t: &str| (!floats.contains(t)).then(|| ranges.get(t).copied()).flatten().filter(|r| *r != UNBOUNDED);
507 let carried: BTreeSet<String> = super::front::carried_state(rp).into_iter().collect();
508 let mut tensors = BTreeMap::new();
509 for d in &rp.declared {
510 let role = if params.contains(&d.name) {
511 Role::Param
512 } else if state.contains(&d.name) {
513 Role::State
514 } else {
515 Role::Constant
516 };
517 let (rows, cols) = rp.shape_of(&d.name, d.len);
518 let storage = if role == Role::Constant { storage(&d.name) } else { Storage::Computed };
519 tensors.insert(
520 d.name.clone(),
521 Tensor {
522 name: d.name.clone(),
523 rows,
524 cols,
525 role,
526 elem: elem(&d.name),
527 fill: Some(d.fill.clone()),
528 storage,
529 range: range(&d.name),
530 observed: false,
531 carried: role == Role::State && carried.contains(&d.name),
532 },
533 );
534 }
535 let mut ops = Vec::with_capacity(rp.stages.len());
536 for (i, st) in rp.stages.iter().enumerate() {
537 for x in &st.ins {
538 if tensors.contains_key(x) {
539 continue;
540 }
541 if !lenient {
542 return Err(GraphError::Undefined { op: i, tensor: x.clone() });
543 }
544 let (rows, cols) = rp.shapes.get(x).copied().unwrap_or((1, 0));
545 tensors.insert(
546 x.clone(),
547 Tensor {
548 name: x.clone(),
549 rows,
550 cols,
551 role: Role::Activation,
552 elem: elem(x),
553 fill: None,
554 storage: Storage::Computed,
555 range: range(x),
556 observed: false,
557 carried: false,
558 },
559 );
560 }
561 if !tensors.contains_key(&st.out) {
562 let n = out_len(&st.op);
563 let (rows, cols) = rp.shape_of(&st.out, n);
564 tensors.insert(
565 st.out.clone(),
566 Tensor {
567 name: st.out.clone(),
568 rows,
569 cols,
570 role: Role::Activation,
571 elem: elem(&st.out),
572 fill: None,
573 storage: Storage::Computed,
574 range: range(&st.out),
575 observed: false,
576 carried: false,
577 },
578 );
579 }
580 ops.push(Op {
581 sym: Sym::Prim(st.op.clone()),
582 access: vec![Access::Whole; st.ins.len()],
583 ins: st.ins.clone(),
584 dst: Dst::to(&st.out),
585 out: st.out.clone(),
586 keeps: Vec::new(),
587 covers: vec![i],
588 });
589 }
590 let carried: BTreeSet<&str> = io.iter().flat_map(|io| io.names()).collect();
592 let names: Vec<String> = tensors.keys().cloned().collect();
593 for n in names {
594 let seen = observed(&n) || carried.contains(n.as_str());
595 tensors.get_mut(&n).expect("a tensor").observed = seen;
596 }
597 Ok(TensorGraph { identity, io, tensors, ops })
598 }
599
600 pub fn shape_of(&self, tensor: &str, n: usize) -> (usize, usize) {
603 match self.tensors.get(tensor) {
604 Some(t) if t.len() == n => (t.rows, t.cols),
605 _ => (1, n),
606 }
607 }
608
609 pub fn touches(&self) -> Touches<'_> {
612 use super::facts::{reads, writes};
613 let mut by: BTreeMap<&str, BTreeSet<usize>> = BTreeMap::new();
614 for (i, op) in self.ops.iter().enumerate() {
615 for t in reads(op).into_iter().chain(writes(op)) {
616 by.entry(t).or_default().insert(i);
617 }
618 }
619 Touches { graph: self, by }
620 }
621
622 pub fn of_role(&self, role: Role) -> impl Iterator<Item = &Tensor> {
624 self.tensors.values().filter(move |t| t.role == role)
625 }
626
627 pub fn readers(&self, tensor: &str) -> Vec<usize> {
629 self.ops.iter().enumerate().filter(|(_, o)| o.ins.iter().any(|x| x == tensor)).map(|(i, _)| i).collect()
630 }
631
632 pub fn producer(&self, tensor: &str) -> Option<usize> {
634 self.ops.iter().position(|o| o.result() == Some(tensor) || o.keeps.iter().any(|k| k == tensor))
635 }
636
637 fn members(&self) -> Vec<&Op> {
639 fn walk<'a>(op: &'a Op, out: &mut Vec<&'a Op>) {
640 out.push(op);
641 if let Sym::Fused(f) = &op.sym {
642 f.parts.iter().for_each(|p| walk(p, out));
643 }
644 }
645 let mut out = Vec::new();
646 self.ops.iter().for_each(|op| walk(op, &mut out));
647 out
648 }
649
650 pub fn writes_in_place(&self) -> BTreeSet<String> {
653 let mut out = BTreeSet::new();
654 for op in self.members() {
655 for k in op.state_inputs() {
656 out.insert(op.ins[k].clone());
657 }
658 if op.dst.row.is_some() || op.dst.unit.is_some() {
659 out.insert(op.dst.buf.clone());
660 }
661 }
662 out
663 }
664
665 pub fn caches(&self) -> BTreeSet<String> {
668 let members = self.members();
669 let producer: BTreeMap<&str, &Op> = members.iter().map(|o| (o.out.as_str(), *o)).collect();
670 let mut out = BTreeSet::new();
671 for op in members.iter().filter(|o| matches!(o.sym, Sym::Prim(TensorOpSpec::Attention(_)))) {
672 for read in &op.ins[1..3] {
673 let mut t = read.as_str();
674 loop {
675 if self.tensors.get(t).is_some_and(|x| x.role == Role::State) {
676 out.insert(t.to_string());
677 break;
678 }
679 match producer.get(t) {
680 Some(p) if matches!(p.sym, Sym::Prim(TensorOpSpec::HeadSlice(_))) => t = &p.ins[0],
681 _ => break,
682 }
683 }
684 }
685 }
686 out
687 }
688}