1use std::collections::{BTreeMap, BTreeSet, HashSet};
11
12use super::ir::*;
13use super::passes::from_qwen_ops as q;
14use super::passes::from_tensor_op::{
15 from_elementwise_with, from_fused_row, from_matmul, from_swiglu_shaped, ElementwiseNames, FusedRowFills,
16 FusedRowNames, MatmulNames, SwiGluNames,
17};
18use super::passes::SpatialReject;
19use super::pipeline::{check_datatypes, StageNames};
20use super::target::SpatialTarget;
21use super::value_range::Interval;
22use crate::tensor_graph::front::TensorOpSpec;
23use crate::tensor_graph::graph::{Op, Sym, TensorGraph};
24use crate::tensor_recognize::{FusedRowSpec, Fill, RowReduceKind};
25
26#[derive(Clone, Debug, Default)]
28pub struct Prelude {
29 pub dense: BTreeSet<String>,
32 pub ranges: BTreeMap<String, Interval>,
35 pub views: BTreeMap<usize, String>,
37}
38
39impl Prelude {
40 pub fn of(
44 g: &TensorGraph,
45 linked: &[usize],
46 target: &SpatialTarget,
47 agreed: &BTreeSet<String>,
48 ranges: BTreeMap<String, Interval>,
49 q8: &BTreeSet<String>,
50 chip: &dyn Fn(&str) -> Option<u16>,
51 ) -> Prelude {
52 let mut views = BTreeMap::new();
55 let mut writes: BTreeMap<&str, usize> = BTreeMap::new();
56 for &k in linked {
57 let op = &g.ops[k];
58 if matches!(op.prim(), Some(TensorOpSpec::Matmul(_))) && q8.contains(&op.ins[1]) {
59 let a = op.ins[0].as_str();
60 views.insert(k, super::passes::from_q8::q8_rows_name_on(a, writes.get(a).copied().unwrap_or(0), chip(&op.out)));
61 }
62 *writes.entry(op.out.as_str()).or_default() += 1;
63 }
64 Prelude { dense: dense_tables(g, linked, target, agreed), ranges, views }
65 }
66
67 pub fn agree_layouts(
74 &self,
75 g: &TensorGraph,
76 ps: Vec<SpatialProgram>,
77 twins: &mut BTreeSet<String>,
78 passes: &dyn Fn(SpatialProgram) -> Result<SpatialProgram, SpatialReject>,
79 target: &SpatialTarget,
80 ) -> Result<Vec<SpatialProgram>, SpatialReject> {
81 use q::{relayout_dense_to_rows, relayout_rows_to_dense};
82 let mut roles: BTreeMap<&str, HashSet<TensorRole>> = BTreeMap::new();
85 for p in &ps {
86 for t in &p.tensors {
87 roles.entry(t.name.as_str()).or_default().insert(t.role);
88 }
89 }
90 let (mut before, mut after) = (Vec::new(), Vec::new());
91 let mut renames: BTreeMap<String, String> = BTreeMap::new();
92 for p in &ps {
93 for t in &p.tensors {
94 if roles.get(t.name.as_str()).is_some_and(|r| r.len() > 1) {
95 continue;
96 }
97 let Some((r, c)) = g.tensors.get(&t.name).map(|x| (x.rows, x.cols)).filter(|(r, _)| *r > 1) else {
98 continue;
99 };
100 let section_dense = match (t.elems.rows, t.elems.cols) {
101 d if d == (r, c) => true,
102 d if d == (1, r * c) => false,
103 _ => continue,
104 };
105 if section_dense == self.dense.contains(&t.name) || renames.contains_key(&t.name) {
106 continue;
107 }
108 let twin = format!("{}__{}", t.name, if section_dense { "dense" } else { "rows" });
109 match t.role {
110 TensorRole::Input => {
111 if twins.insert(twin.clone()) {
112 let from = (t.name.as_str(), &fill_of(g, &t.name));
113 let bridge = if section_dense {
114 relayout_rows_to_dense(from, &twin, r, c, t.dtype, target)?
115 } else {
116 vec![relayout_dense_to_rows(from, &twin, r, c, t.dtype, target)?]
117 };
118 for b in bridge {
119 before.push(passes(b)?);
120 }
121 }
122 }
123 TensorRole::Output => {
124 twins.insert(twin.clone());
125 let from = (twin.as_str(), &Fill::Unknown);
126 let bridge = if section_dense {
127 vec![relayout_dense_to_rows(from, &t.name, r, c, t.dtype, target)?]
128 } else {
129 relayout_rows_to_dense(from, &t.name, r, c, t.dtype, target)?
130 };
131 for b in bridge {
132 after.push(passes(b)?);
133 }
134 }
135 _ => continue,
136 }
137 renames.insert(t.name.clone(), twin);
138 }
139 }
140 let renamed = ps.into_iter().map(|mut p| {
141 for t in &mut p.tensors {
142 if let Some(twin) = renames.get(&t.name) {
143 t.name = twin.clone();
144 }
145 }
146 p
147 });
148 Ok(before.into_iter().chain(renamed).chain(after).collect())
149 }
150}
151
152pub fn dense_tables(g: &TensorGraph, linked: &[usize], target: &SpatialTarget, agreed: &BTreeSet<String>) -> BTreeSet<String> {
162 use q::{attention_reads_dense, full_attention_reads_dense};
163 let ops = || linked.iter().map(|&k| &g.ops[k]).filter_map(|op| op.prim().map(|spec| (op, spec)));
164 let matrix = |name: &str, n: usize| g.shape_of(name, n).0 > 1;
165 let mut dense = BTreeSet::new();
166 for (op, spec) in ops() {
167 let whole = || op.ins.iter().chain(std::iter::once(&op.out)).collect::<Vec<_>>();
168 let touched: Vec<&String> = match spec {
169 TensorOpSpec::Attention(a) if attention_reads_dense(a.h, a.kv, a.hd, a.vd, target) && a.rows > 1 => {
172 op.ins.iter().take(3).chain(std::iter::once(&op.out)).collect()
173 }
174 TensorOpSpec::Attention(a) if attention_reads_dense(a.h, a.kv, a.hd, a.vd, target) => op.ins.iter().skip(1).take(2).collect(),
175 TensorOpSpec::FullAttention(a) if full_attention_reads_dense(a.hd, target) && a.t > 1 => {
176 op.ins.iter().take(3).chain(std::iter::once(&op.out)).collect()
177 }
178 TensorOpSpec::FullAttention(a) if full_attention_reads_dense(a.hd, target) => op.ins.iter().skip(1).take(2).collect(),
179 TensorOpSpec::Matmul(m) => [(&op.ins[0], m.m > 1), (&op.ins[1], m.k > 1), (&op.out, m.m > 1)]
180 .into_iter()
181 .filter(|(_, rows)| *rows)
182 .map(|(t, _)| t)
183 .collect(),
184 TensorOpSpec::Elementwise(e) if matrix(&op.out, e.n) => whole(),
185 TensorOpSpec::SwiGlu(s) if matrix(&op.out, s.total) => whole(),
186 TensorOpSpec::Silu(u) if matrix(&op.out, u.n) => whole(),
187 TensorOpSpec::IntRequant(u) if matrix(&op.out, u.n) => whole(),
188 TensorOpSpec::IntClamp(u) if matrix(&op.out, u.n) => whole(),
189 TensorOpSpec::Relu(u) if matrix(&op.out, u.n) => whole(),
190 TensorOpSpec::Sigmoid(u) if matrix(&op.out, u.n) => whole(),
191 TensorOpSpec::Gelu(u) if matrix(&op.out, u.n) => whole(),
192 TensorOpSpec::Scale(u) if matrix(&op.out, u.n) => whole(),
193 TensorOpSpec::Softcap(u) if matrix(&op.out, u.n) => whole(),
194 TensorOpSpec::Decay(d) if d.rows > 1 => vec![&op.ins[0], &op.out],
195 TensorOpSpec::CausalConv(c) if c.rows > 1 => vec![&op.ins[0], &op.out],
196 _ => Vec::new(),
197 };
198 dense.extend(touched.into_iter().cloned());
199 }
200 dense.extend(agreed.iter().cloned());
201 loop {
202 let before = dense.len();
203 for (op, spec) in ops() {
206 if let TensorOpSpec::SetRow(r) = spec {
207 if r.count > 1 && dense.contains(&op.ins[0]) {
208 dense.insert(op.ins[1].clone());
209 }
210 }
211 }
212 for (op, spec) in ops() {
213 let TensorOpSpec::FusedRow(f) = spec else { continue };
214 if f.rows < 2 || g.shape_of(&op.out, f.rows * f.cols) != (f.rows, f.cols) {
215 continue;
216 }
217 let tied: Vec<&String> = std::iter::once(&op.ins[0])
218 .chain(f.residual.and(op.ins.get(1 + usize::from(f.weight.is_some()))))
219 .chain(std::iter::once(&op.out))
220 .collect();
221 if tied.iter().any(|t| dense.contains(*t)) {
222 dense.extend(tied.into_iter().cloned());
223 }
224 }
225 if dense.len() == before {
226 return dense;
227 }
228 }
229}
230
231#[derive(Clone, Copy, Debug, PartialEq, Eq)]
241pub enum Form {
242 Alone,
243 Section { floats: bool },
246}
247
248pub fn link(
254 g: &TensorGraph,
255 k: usize,
256 prelude: &Prelude,
257 form: Form,
258 numerics: NumericsPolicy,
259 target: &SpatialTarget,
260) -> Result<Vec<SpatialProgram>, SpatialReject> {
261 let programs = link_unchecked(g, k, prelude, form, numerics, target)?;
262 for p in &programs {
263 check_datatypes(p, target)?;
264 }
265 Ok(programs)
266}
267
268fn fill_of(g: &TensorGraph, tensor: &str) -> Fill {
270 g.tensors.get(tensor).and_then(|t| t.fill.clone()).unwrap_or(Fill::Unknown)
271}
272
273fn fill_is_float(fill: &Fill) -> bool {
277 !matches!(fill, Fill::Const(_) | Fill::Pattern { .. } | Fill::Overrides { .. })
278}
279
280fn row_norm_operands(op: &Op, g: &TensorGraph, f: &FusedRowSpec) -> (FusedRowNames, FusedRowFills) {
283 let mut rest = op.ins.iter().skip(1);
284 let mut slot = |have: bool| have.then(|| rest.next().cloned()).flatten();
285 let (weight, residual, bias) = (slot(f.weight.is_some()), slot(f.residual.is_some()), slot(f.bias.is_some()));
286 let fills = FusedRowFills {
287 x: fill_of(g, &op.ins[0]),
288 weight: weight.as_deref().map(|t| fill_of(g, t)),
289 residual: residual.as_deref().map(|t| fill_of(g, t)),
290 bias: bias.as_deref().map(|t| fill_of(g, t)),
291 };
292 (FusedRowNames { x: op.ins[0].clone(), y: op.out.clone(), weight, residual, bias }, fills)
293}
294
295fn link_unchecked(
296 g: &TensorGraph,
297 k: usize,
298 prelude: &Prelude,
299 form: Form,
300 numerics: NumericsPolicy,
301 target: &SpatialTarget,
302) -> Result<Vec<SpatialProgram>, SpatialReject> {
303 let op = &g.ops[k];
304 let Sym::Prim(spec) = &op.sym else {
305 return Err(SpatialReject::NotLowered("an op the front end did not lift — the spatial chips link every fused class by its parts"));
306 };
307 let fill = |i: usize| fill_of(g, &op.ins[i]);
308 let names = StageNames { out: op.out.clone(), ins: op.ins.clone(), fills: (0..op.ins.len()).map(fill).collect() };
309 let shape = |n: usize| g.shape_of(&op.out, n);
310 let dense = |t: &str| prelude.dense.contains(t);
311 let floats = || match form {
313 Form::Section { floats } => floats,
314 Form::Alone => fill_is_float(&names.fills[0]),
315 };
316 let one = |r: Result<SpatialProgram, SpatialReject>| r.map(|p| vec![p]);
317 match spec {
318 TensorOpSpec::Matmul(m) => {
319 let names = MatmulNames { a: op.ins[0].clone(), b: op.ins[1].clone(), c: op.out.clone() };
320 let fills = (fill(0), fill(1));
321 if let Some(view) = prelude.views.get(&k).filter(|_| super::passes::from_q8::q8_gemv_applies(m, target)) {
322 if let Some(sections) = super::passes::from_q8::q8_gemv(m, &names, (&fills.0, &fills.1), view, target)? {
323 return Ok(sections);
324 }
325 }
326 let ranges = (prelude.ranges.get(&names.a).copied(), prelude.ranges.get(&names.b).copied());
327 one(from_matmul(m, &names, (&fills.0, &fills.1), ranges, numerics, target))
328 }
329 TensorOpSpec::Elementwise(e) => {
333 let names = ElementwiseNames { a: op.ins[0].clone(), b: op.ins[1].clone(), c: op.out.clone() };
334 let float_activations = form == (Form::Section { floats: true });
335 one(from_elementwise_with(e, &names, (&fill(0), &fill(1)), float_activations, shape(e.n), target))
336 }
337 TensorOpSpec::SwiGlu(s) => {
338 let names = SwiGluNames { gate: op.ins[0].clone(), up: op.ins[1].clone(), y: op.out.clone() };
339 one(from_swiglu_shaped(s, &names, (&fill(0), &fill(1)), shape(s.total), target))
340 }
341 TensorOpSpec::FusedRow(f) => {
342 let (row_names, fills) = row_norm_operands(op, g, f);
343 let natural = (f.rows, f.cols);
344 let layout = match form {
346 Form::Alone => natural,
347 Form::Section { .. } if f.rows > 1 && !dense(&op.out) => (1, f.rows * f.cols),
348 Form::Section { .. } => shape(f.rows * f.cols),
349 };
350 match form {
361 Form::Section { .. }
362 if layout == natural && q::row_needs_blocking(f.cols, target) && f.reduce == RowReduceKind::RmsNorm && f.bias.is_none() =>
363 {
364 q::blocked_rms_norm(f, &names, target)
365 }
366 Form::Section { .. } if f.rows > 1 && layout == (1, f.rows * f.cols) => one(q::from_row_norm_flat(f, &names, target)),
367 Form::Section { .. } if layout != natural => Err(SpatialReject::NotLowered(
368 "a row norm whose rows cut across the rows of the matrix it normalizes — neither the matrix's tiling nor the flat row's",
369 )),
370 _ => one(from_fused_row(f, &row_names, &fills, target)),
371 }
372 }
373 TensorOpSpec::Rope(spec) => one(q::from_rope(spec, &names, target)),
374 TensorOpSpec::Silu(spec) => one(q::from_silu(spec, &names, shape(spec.n), target)),
375 TensorOpSpec::IntRequant(spec) => one(q::from_int_requant(spec, &names, shape(spec.n), target)),
376 TensorOpSpec::IntClamp(spec) => one(q::from_int_clamp(spec, &names, shape(spec.n), target)),
377 TensorOpSpec::Relu(spec) => one(q::from_relu(spec, &names, shape(spec.n), target)),
378 TensorOpSpec::LutAttention(spec) => q::from_lut_attention(spec, &names, target),
379 TensorOpSpec::Sigmoid(spec) => one(q::from_sigmoid(spec, &names, shape(spec.n), target)),
380 TensorOpSpec::Gelu(spec) => one(q::from_gelu(spec, &names, shape(spec.n), target)),
381 TensorOpSpec::Scale(spec) => one(q::from_scale(spec, &names, shape(spec.n), target)),
382 TensorOpSpec::Softcap(spec) => one(q::from_softcap(spec, &names, shape(spec.n), target)),
383 TensorOpSpec::Decay(spec) => one(q::from_decay(spec, &names, target)),
384 TensorOpSpec::L2Norm(spec) => one(q::from_l2norm(spec, &names, target)),
385 TensorOpSpec::Transpose(spec) => {
386 let floats = match form {
387 Form::Section { floats } => floats,
388 Form::Alone => true,
389 };
390 one(q::from_transpose(spec, &names, floats, target))
391 }
392 TensorOpSpec::HeadSlice(spec) => one(q::from_head_slice(spec, &names, target)),
393 TensorOpSpec::Join(spec) => one(q::from_join(spec, &names, target)),
394 TensorOpSpec::HeadMatmul(spec) => one(q::from_head_matmul(spec, &names, target)),
395 TensorOpSpec::CausalConv(spec) => q::from_causal_conv(spec, &names, target),
396 TensorOpSpec::GatedDelta(spec) => {
399 let tables: BTreeSet<String> = op.ins.iter().chain(std::iter::once(&op.out)).filter(|t| dense(t)).cloned().collect();
400 q::from_gated_delta(spec, &names, &tables, target)
401 }
402 TensorOpSpec::Attention(spec) => q::from_attention(spec, &names, target),
403 TensorOpSpec::FullAttention(spec) => q::from_full_attention(spec, &names, target),
404 TensorOpSpec::Moe(spec) => q::from_moe(spec, &names, target),
405 TensorOpSpec::RowGather(spec) => one(q::from_row_gather(spec, &names, floats(), dense(&op.ins[0]), target)),
406 TensorOpSpec::RowsGather(spec) => one(q::from_rows_gather(spec, &names, floats(), dense(&op.ins[0]), target)),
407 TensorOpSpec::ArgMax(spec) => q::from_argmax(spec, &names, floats(), target),
408 TensorOpSpec::SetRow(spec) => one(q::from_set_row(spec, &names, floats(), dense(&op.ins[0]), target)),
409 }
410}