1use super::ir::*;
4#[allow(unused_imports)]
5use super::ir::{BlockPlan, EnginePacking};
6use super::link::{dense_tables, link, Form, Prelude};
7use super::passes::from_tensor_op::{from_matmul, MatmulNames};
8use super::passes::recognize_stages::{float_tensors, links, state_tensors, Link, TensorOpSpec};
9use super::passes::{
10 fuse_generic, fuse_readers, persist, place, size_buffers, synthesize_handshakes, tile, verify,
11 SpatialReject,
12};
13use super::target::SpatialTarget;
14use crate::codegen_sva::fabric_target::Precision;
15use crate::tensor_graph::graph::{Lifted, Op, Storage, TensorGraph};
16use crate::tensor_recognize::{
17 recognize_elementwise, recognize_fill, recognize_fused_row, recognize_matmul, recognize_swiglu,
18 Fill, MatmulSpec,
19};
20
21pub type Pass = fn(&SpatialProgram, &SpatialTarget) -> Result<SpatialProgram, SpatialReject>;
23
24pub const PASS_ORDER: &[(&str, Pass)] = &[
26 ("fuse_generic", fuse_generic::fuse_generic),
30 ("tile", tile::tile),
31 ("place", place::place),
32 ("fuse_readers", fuse_readers::fuse_readers),
33 ("size_buffers", size_buffers::size_buffers),
34 (
35 "synthesize_handshakes",
36 synthesize_handshakes::synthesize_handshakes,
37 ),
38 ("persist", persist::persist),
39 ("verify", verify::verify),
40];
41
42pub fn check_datatypes(p: &SpatialProgram, target: &SpatialTarget) -> Result<(), SpatialReject> {
49 let mut gated: std::collections::BTreeSet<BufferId> = Default::default();
50 for s in &p.stages {
51 match &s.kind {
52 StageKind::Compute(ComputeOp::Generic { .. }) => {
53 gated.extend(s.ins.iter().chain(&s.outs).copied())
54 }
55 StageKind::Load { pattern, .. } | StageKind::Store { pattern, .. } => {
56 gated.extend(pattern.origin.indirect.map(|i| i.index))
57 }
58 _ => {}
59 }
60 }
61 let refuse = |needed: Precision| {
62 Err(SpatialReject::MissingPrecision {
63 needed,
64 target: target.name,
65 })
66 };
67 for b in &gated {
68 let d = p.buffer(*b).dtype;
69 if !target.generic_holds(d) {
70 return refuse(d);
71 }
72 }
73 for s in &p.stages {
74 let moved = match &s.kind {
75 StageKind::Load { tensor, .. } if s.outs.iter().any(|b| gated.contains(b)) => {
76 Some(*tensor)
77 }
78 StageKind::Store { tensor, .. } if s.ins.first().is_some_and(|b| gated.contains(b)) => {
79 Some(*tensor)
80 }
81 _ => None,
82 };
83 if let Some(t) = moved {
84 let d = p.tensor(t).dtype;
85 if !target.generic_holds(d) {
86 return refuse(d);
87 }
88 }
89 }
90 Ok(())
91}
92
93pub fn run_passes(
95 p: SpatialProgram,
96 target: &SpatialTarget,
97) -> Result<SpatialProgram, SpatialReject> {
98 run_pass_list(p, target, PASS_ORDER)
99}
100
101pub fn run_passes_through_tile(p: SpatialProgram, target: &SpatialTarget) -> Result<SpatialProgram, SpatialReject> {
104 let at = PASS_ORDER.iter().position(|(n, _)| *n == "tile").expect("a tiling pass");
105 run_pass_list(p, target, &PASS_ORDER[..=at])
106}
107
108fn run_passes_after_tile(p: SpatialProgram, target: &SpatialTarget, certify: bool) -> Result<SpatialProgram, SpatialReject> {
111 let at = PASS_ORDER.iter().position(|(n, _)| *n == "tile").expect("a tiling pass");
112 let end = if certify { PASS_ORDER.len() } else { PASS_ORDER.len() - 1 };
113 run_pass_list(p, target, &PASS_ORDER[at + 1..end])
114}
115
116pub fn run_passes_uncertified(
119 p: SpatialProgram,
120 target: &SpatialTarget,
121) -> Result<SpatialProgram, SpatialReject> {
122 let (last, before) = PASS_ORDER.split_last().expect("passes");
123 assert_eq!(last.0, "verify", "the certifying pass is the last one");
124 run_pass_list(p, target, before)
125}
126
127fn run_pass_list(
128 p: SpatialProgram,
129 target: &SpatialTarget,
130 passes: &[(&str, Pass)],
131) -> Result<SpatialProgram, SpatialReject> {
132 let mut cur = p;
133 let tr = std::env::var("LOGOS_PASS_TIME").is_ok() && cur.name.contains("gd_");
134 for (name, pass) in passes {
135 let t0 = std::time::Instant::now();
136 cur = pass(&cur, target)?;
137 let dt = t0.elapsed();
138 cur.validate()?;
139 if tr {
140 eprintln!(
141 "TIME {} {name}: {:?} stages={} buffers={} edges={} items={}",
142 cur.name,
143 dt,
144 cur.stages.len(),
145 cur.buffers.len(),
146 cur.edges.len(),
147 cur.work.items.len()
148 );
149 }
150 }
151 Ok(cur)
152}
153
154#[derive(Clone, Debug, PartialEq, Eq)]
158pub struct StageNames {
159 pub out: String,
160 pub ins: Vec<String>,
161 pub fills: Vec<Fill>,
162}
163
164impl StageNames {
165 pub fn operand(&self, i: usize) -> (&str, &Fill) {
167 (self.ins[i].as_str(), &self.fills[i])
168 }
169}
170
171fn recognize_any(source: &str) -> Result<TensorGraph, SpatialReject> {
173 crate::ui_bridge::with_parsed_program(source, |parsed, interner| {
174 let (stmts, _, _) = parsed.map_err(|e| SpatialReject::Parse(format!("{e:?}")))?;
175 let operand = |s, len: usize| (s, len, recognize_fill(stmts, s));
176 let matmul = recognize_matmul(stmts, interner);
177 if let Ok(spec) = matmul {
178 let ins = vec![operand(spec.a, spec.m * spec.k), operand(spec.b, spec.k * spec.n)];
179 let out = spec.c;
180 return Ok(TensorGraph::of_op(TensorOpSpec::Matmul(spec), ins, out, interner));
181 }
182 let elementwise = recognize_elementwise(stmts, interner);
183 if let Ok(spec) = elementwise {
184 let ins = vec![operand(spec.a, spec.n), operand(spec.b, spec.n)];
185 let out = spec.c;
186 return Ok(TensorGraph::of_op(TensorOpSpec::Elementwise(spec), ins, out, interner));
187 }
188 if let Ok(spec) = recognize_swiglu(stmts, interner) {
189 let ins = vec![operand(spec.gate, spec.total), operand(spec.up, spec.total)];
190 let out = spec.y;
191 return Ok(TensorGraph::of_op(TensorOpSpec::SwiGlu(spec), ins, out, interner));
192 }
193 let reduction = recognize_fused_row(stmts, interner);
194 if let Ok(spec) = reduction {
195 let (rows, cols) = (spec.rows, spec.cols);
196 let ins = std::iter::once(operand(spec.x, rows * cols))
197 .chain(spec.weight.map(|s| operand(s, cols)))
198 .chain(spec.residual.map(|s| operand(s, rows * cols)))
199 .chain(spec.bias.map(|s| operand(s, cols)))
200 .collect();
201 let out = spec.y;
202 return Ok(TensorGraph::of_op(TensorOpSpec::FusedRow(spec), ins, out, interner));
203 }
204 Err(SpatialReject::Unrecognized {
205 matmul: matmul.unwrap_err(),
206 elementwise: elementwise.unwrap_err(),
207 reduction: reduction.unwrap_err(),
208 })
209 })
210}
211
212pub fn template_from_source_with(
214 source: &str,
215 target: &SpatialTarget,
216 numerics: NumericsPolicy,
217) -> Result<SpatialProgram, SpatialReject> {
218 let mut programs = link(&recognize_any(source)?, 0, &Prelude::default(), Form::Alone, numerics, target)?;
219 if programs.len() != 1 {
220 return Err(SpatialReject::NotLowered(
221 "a multi-section op as a single template — this shape only exists inside a composition",
222 ));
223 }
224 Ok(programs.remove(0))
225}
226
227pub fn template_from_source(
228 source: &str,
229 target: &SpatialTarget,
230) -> Result<SpatialProgram, SpatialReject> {
231 template_from_source_with(source, target, NumericsPolicy::Exact)
232}
233
234pub fn template_from_spec(
236 spec: &MatmulSpec,
237 names: &MatmulNames,
238 fills: (&Fill, &Fill),
239 ranges: (Option<(i64, i64)>, Option<(i64, i64)>),
240 numerics: NumericsPolicy,
241 target: &SpatialTarget,
242) -> Result<SpatialProgram, SpatialReject> {
243 from_matmul(spec, names, fills, ranges, numerics, target)
244}
245
246pub fn compile_to_spatial_with(
248 source: &str,
249 target: &SpatialTarget,
250 numerics: NumericsPolicy,
251) -> Result<SpatialProgram, SpatialReject> {
252 run_passes(template_from_source_with(source, target, numerics)?, target)
253}
254
255pub fn compile_to_spatial(
256 source: &str,
257 target: &SpatialTarget,
258) -> Result<SpatialProgram, SpatialReject> {
259 compile_to_spatial_with(source, target, NumericsPolicy::Exact)
260}
261
262#[derive(Clone, Debug, PartialEq, Eq)]
273pub struct MissingStage {
274 pub stmt_index: usize,
276 pub binder: String,
278 pub kind: String,
280 pub why: String,
282}
283
284#[derive(Clone, Debug)]
290pub struct ProgramLowering {
291 pub program: Option<SpatialProgram>,
292 pub statements: usize,
293 pub lowered: usize,
294 pub sections: usize,
295 pub missing: Vec<MissingStage>,
296 pub state: Vec<String>,
298 pub carried: Vec<String>,
301}
302
303impl ProgramLowering {
304 pub fn whole(&self) -> Option<&SpatialProgram> {
307 self.program
308 .as_ref()
309 .filter(|_| self.lowered == self.statements)
310 }
311 pub fn into_whole(self) -> Result<SpatialProgram, String> {
313 if self.whole().is_none() {
314 return Err(self.refusal());
315 }
316 Ok(self.program.expect("a whole lowering carries its program"))
317 }
318 pub fn refusal(&self) -> String {
320 let mut why: Vec<String> = self
321 .missing
322 .iter()
323 .take(MISSING_NAMED)
324 .map(|m| {
325 format!(
326 "statement {} (`{}`, {}): {}",
327 m.stmt_index, m.binder, m.kind, m.why
328 )
329 })
330 .collect();
331 if self.missing.len() > MISSING_NAMED {
332 why.push(format!("… and {} more", self.missing.len() - MISSING_NAMED));
333 }
334 if self.missing.is_empty() && self.program.is_none() {
335 return format!(
336 "lifted {} of {} statements into {} sections, and the composer did not join them into one program",
337 self.lowered, self.statements, self.sections
338 );
339 }
340 format!(
341 "lowered {} of {} statements; not lowered: {}",
342 self.lowered,
343 self.statements,
344 why.join("; ")
345 )
346 }
347}
348
349const MISSING_NAMED: usize = 6;
351
352pub fn compile_program_to_spatial(
358 source: &str,
359 target: &SpatialTarget,
360) -> Result<ProgramLowering, SpatialReject> {
361 compile_program_to_spatial_agreeing(source, target, &std::collections::BTreeSet::new())
362}
363
364pub fn compile_program_to_spatial_agreeing(
368 source: &str,
369 target: &SpatialTarget,
370 dense: &std::collections::BTreeSet<String>,
371) -> Result<ProgramLowering, SpatialReject> {
372 lower_program(
373 source,
374 target,
375 dense,
376 None,
377 &std::collections::BTreeSet::new(),
378 &Results::EveryNamed,
379 )
380}
381
382pub fn compile_program_to_spatial_returning(
386 source: &str,
387 target: &SpatialTarget,
388 results: &std::collections::BTreeSet<String>,
389) -> Result<ProgramLowering, SpatialReject> {
390 let none = std::collections::BTreeSet::new();
391 lower_program(source, target, &none, None, &none, &Results::Only(results.clone()))
392}
393
394pub fn compile_program_to_spatial_stored(
398 source: &str,
399 target: &SpatialTarget,
400 q8: &std::collections::BTreeSet<String>,
401) -> Result<ProgramLowering, SpatialReject> {
402 lower_program(source, target, &std::collections::BTreeSet::new(), None, q8, &Results::EveryNamed)
403}
404
405pub fn compile_program_to_spatial_agreeing_stored(
407 source: &str,
408 target: &SpatialTarget,
409 dense: &std::collections::BTreeSet<String>,
410 q8: &std::collections::BTreeSet<String>,
411) -> Result<ProgramLowering, SpatialReject> {
412 lower_program(source, target, dense, None, q8, &Results::EveryNamed)
413}
414
415pub fn compile_program_to_spatial_tp(
418 source: &str,
419 target: &SpatialTarget,
420 manifest: &super::tensor_parallel::TpManifest,
421) -> Result<ProgramLowering, SpatialReject> {
422 compile_program_to_spatial_tp_agreeing(
423 source,
424 target,
425 manifest,
426 &std::collections::BTreeSet::new(),
427 )
428}
429
430pub fn compile_program_to_spatial_tp_agreeing(
433 source: &str,
434 target: &SpatialTarget,
435 manifest: &super::tensor_parallel::TpManifest,
436 dense: &std::collections::BTreeSet<String>,
437) -> Result<ProgramLowering, SpatialReject> {
438 compile_program_to_spatial_tp_agreeing_stored(
439 source,
440 target,
441 manifest,
442 dense,
443 &std::collections::BTreeSet::new(),
444 )
445}
446
447pub fn compile_program_to_spatial_tp_agreeing_stored(
450 source: &str,
451 target: &SpatialTarget,
452 manifest: &super::tensor_parallel::TpManifest,
453 dense: &std::collections::BTreeSet<String>,
454 q8: &std::collections::BTreeSet<String>,
455) -> Result<ProgramLowering, SpatialReject> {
456 lower_program(source, target, dense, Some(manifest), q8, &Results::EveryNamed)
457}
458
459#[derive(Clone, Copy, Debug, PartialEq, Eq)]
461pub enum SectionCertification {
462 Composed,
465 EachSection,
468}
469
470fn lower_program(
476 source: &str,
477 target: &SpatialTarget,
478 dense: &std::collections::BTreeSet<String>,
479 tp: Option<&super::tensor_parallel::TpManifest>,
480 q8: &std::collections::BTreeSet<String>,
481 results: &Results,
482) -> Result<ProgramLowering, SpatialReject> {
483 lower_program_on(source, target, dense, tp, q8, None, results)
484}
485
486fn lower_program_on(
489 source: &str,
490 target: &SpatialTarget,
491 dense: &std::collections::BTreeSet<String>,
492 tp: Option<&super::tensor_parallel::TpManifest>,
493 q8: &std::collections::BTreeSet<String>,
494 chip: Option<(u16, &super::tensor_parallel::TpManifest)>,
495 results: &Results,
496) -> Result<ProgramLowering, SpatialReject> {
497 let composed = lower_program_stored(
498 source,
499 target,
500 dense,
501 tp,
502 SectionCertification::Composed,
503 q8,
504 chip,
505 results,
506 )?;
507 if composed.whole().is_some() {
508 return Ok(composed);
509 }
510 lower_program_stored(
511 source,
512 target,
513 dense,
514 tp,
515 SectionCertification::EachSection,
516 q8,
517 chip,
518 results,
519 )
520}
521
522#[derive(Clone, Debug)]
525pub struct ChipLowering {
526 pub chips: Vec<ProgramLowering>,
529 pub transfers: Vec<super::tensor_parallel::ChipTransfer>,
530}
531
532pub fn compile_program_to_chips(
540 source: &str,
541 target: &SpatialTarget,
542 manifest: &super::tensor_parallel::TpManifest,
543) -> Result<ChipLowering, SpatialReject> {
544 lower_chips(source, target, manifest, &Results::EveryNamed)
545}
546
547pub fn compile_program_to_chips_returning(
551 source: &str,
552 target: &SpatialTarget,
553 manifest: &super::tensor_parallel::TpManifest,
554 results: &std::collections::BTreeSet<String>,
555) -> Result<ChipLowering, SpatialReject> {
556 lower_chips(source, target, manifest, &Results::Only(results.clone()))
557}
558
559fn lower_chips(
560 source: &str,
561 target: &SpatialTarget,
562 manifest: &super::tensor_parallel::TpManifest,
563 results: &Results,
564) -> Result<ChipLowering, SpatialReject> {
565 let rp = TensorGraph::lift_source(source, &|_| Storage::Values)?.program;
566 let transfers = super::tensor_parallel::transfers(&rp.stages, manifest);
567 let none = std::collections::BTreeSet::new();
568 let mut chips = (0..manifest.chips)
569 .map(|c| lower_program_on(source, target, &none, None, &none, Some((c as u16, manifest)), results))
570 .collect::<Result<Vec<_>, _>>()?;
571 for (c, pl) in chips.iter_mut().enumerate() {
572 if let Some(p) = pl.program.as_mut() {
573 match super::tensor_parallel::link_points(p, c as u16, &transfers) {
574 Ok(links) => p.links = links,
575 Err(e) => {
576 pl.missing.push(MissingStage {
577 stmt_index: 0,
578 binder: format!("chip {c}"),
579 kind: "link".into(),
580 why: format!("{e:?}"),
581 });
582 pl.program = None;
583 }
584 }
585 }
586 }
587 Ok(ChipLowering { chips, transfers })
588}
589
590pub fn lower_program_with(
593 source: &str,
594 target: &SpatialTarget,
595 dense: &std::collections::BTreeSet<String>,
596 tp: Option<&super::tensor_parallel::TpManifest>,
597 sections: SectionCertification,
598) -> Result<ProgramLowering, SpatialReject> {
599 lower_program_stored(
600 source,
601 target,
602 dense,
603 tp,
604 sections,
605 &std::collections::BTreeSet::new(),
606 None,
607 &Results::EveryNamed,
608 )
609}
610
611fn share_q8_views(
616 sections: Vec<SpatialProgram>,
617 view: Option<&String>,
618 made: &mut std::collections::BTreeSet<String>,
619) -> Vec<SpatialProgram> {
620 let Some(view) = view else {
621 return sections;
622 };
623 let makes_view = |p: &SpatialProgram| {
624 p.tensors
625 .iter()
626 .any(|t| t.name == *view && t.role != TensorRole::Input)
627 };
628 if !sections.iter().any(makes_view) || made.insert(view.clone()) {
629 return sections;
630 }
631 sections.into_iter().filter(|p| !makes_view(p)).collect()
632}
633
634struct Held {
636 at: usize,
637 stmt_index: usize,
638 out: String,
639 kind: String,
640 templates: Vec<SpatialProgram>,
641}
642
643struct Run {
645 held: Vec<Held>,
646 joined: Vec<SpatialProgram>,
647}
648
649enum Piece {
652 Run(Run),
653 Done(Vec<SpatialProgram>),
654}
655
656#[derive(Clone, Debug, PartialEq, Eq)]
661pub enum Results {
662 EveryNamed,
664 Only(std::collections::BTreeSet<String>),
667}
668
669#[allow(clippy::too_many_arguments)]
671fn lower_program_stored(
672 source: &str,
673 target: &SpatialTarget,
674 dense: &std::collections::BTreeSet<String>,
675 tp: Option<&super::tensor_parallel::TpManifest>,
676 sections: SectionCertification,
677 q8: &std::collections::BTreeSet<String>,
678 chip: Option<(u16, &super::tensor_parallel::TpManifest)>,
679 results: &Results,
680) -> Result<ProgramLowering, SpatialReject> {
681 use super::tensor_parallel::{allreduce_section, relocate};
682 let certification = sections;
683 let passes = |t: SpatialProgram| match certification {
684 SectionCertification::Composed => run_passes_uncertified(t, target),
685 SectionCertification::EachSection => run_passes(t, target),
686 };
687 let storage = |t: &str| {
688 if q8.contains(t) {
689 Storage::Rows { format: crate::model::quant::GgmlType::Q8_0, transposed: true }
690 } else {
691 Storage::Values
692 }
693 };
694 let Lifted { program: whole, graph } = TensorGraph::lift_source(source, &storage)?;
695 let spatial = super::shim::SpatialShim { target };
698 let parallel = tp.or(chip.map(|(_, m)| m)).map(|manifest| super::tensor_parallel::TensorParallelShim { base: &spatial, manifest });
699 let shim: &dyn crate::tensor_graph::shim::Shim = match ¶llel {
700 Some(s) => s,
701 None => &spatial,
702 };
703 let g = crate::tensor_graph::fuse::optimize(&graph, shim, &[]).graph;
704 let stmts: Vec<usize> = g.ops.iter().map(|op| whole.stages[op.covers[0]].stmt_index).collect();
706 let stmt_of = |k: usize| stmts[k];
707 let lifted_stmts: Vec<usize> = whole.stages.iter().map(|st| st.stmt_index).collect();
709 let float_out = float_tensors(&whole);
712 let ranges = super::value_range::value_ranges(&whole, &float_out);
713 let rp = match chip {
714 Some((c, m)) => super::passes::recognize_stages::RecognizedProgram {
715 stages: whole.stages.iter().filter(|st| m.chip_running(&st.out) == c).cloned().collect(),
716 ..whole.clone()
717 },
718 None => whole,
719 };
720 if rp.stages.is_empty() {
721 return Err(recognize_any(source)
724 .err()
725 .unwrap_or(SpatialReject::NotLowered(
726 "a source with no tensor statements",
727 )));
728 }
729 let statements = rp.stages.len() + rp.unrecognized.len() + rp.unlifted_writes.len();
730 let mut missing: Vec<MissingStage> = rp
731 .unrecognized
732 .iter()
733 .map(|(i, b)| MissingStage {
734 stmt_index: *i,
735 binder: b.clone(),
736 kind: "unrecognized".into(),
737 why: "no recognizer of the whole-program walk lifts this statement".into(),
738 })
739 .chain(rp.unlifted_writes.iter().map(|(i, t, why)| MissingStage {
740 stmt_index: *i,
741 binder: t.clone(),
742 kind: "write".into(),
743 why: (*why).to_string(),
744 }))
745 .collect();
746 let mut known = ranges.clone();
749 let ours = |op: &Op| chip.is_none_or(|(c, m)| m.chip_running(&op.out) == c);
750 let linked: Vec<usize> = (0..g.ops.len()).filter(|&k| ours(&g.ops[k])).collect();
751 let prelude = Prelude::of(&g, &linked, target, dense, ranges.clone(), q8, &|out| tp.and_then(|m| m.chip_of.get(out).copied()));
752 let mut twins = std::collections::BTreeSet::new();
753 let mut q8_views = std::collections::BTreeSet::new();
754 let mut sections: Vec<SpatialProgram> = Vec::new();
755 let mut lowered = 0usize;
756 let carried: std::collections::BTreeSet<String> = state_tensors(&rp.stages).into_iter().collect();
767 let touches = g.touches();
768 let forwards = |t: &str, writer: usize, reader: usize| {
769 let read = match results {
770 Results::EveryNamed => return false,
771 Results::Only(read) => read,
772 };
773 tp.is_none()
774 && !read.contains(t)
775 && !carried.contains(t)
776 && g.ops[reader].ins.iter().filter(|x| *x == t).count() == 1
777 && touches.read_only_by(t, writer, reader)
778 };
779 let certify = matches!(certification, SectionCertification::EachSection);
780 let names = |ps: &[SpatialProgram], t: &str| ps.iter().any(|p| p.tensors.iter().any(|x| x.name == t));
782 let keeps = |j: &super::passes::forward::Joint, t: &str, known: &std::collections::BTreeMap<String, super::value_range::Interval>| {
788 let Some(&bound) = known.get(t) else { return true };
789 let proven = super::value_range::program_ranges(&declare_ranges(j.program.clone(), known));
790 proven.buffers[j.carrier.idx()].is_some_and(|iv| bound.0 <= iv.0 && iv.1 <= bound.1)
791 };
792 let join = |t: &str,
796 writer: usize,
797 reader: usize,
798 (before, a): (&[SpatialProgram], &SpatialProgram),
799 (b, after): (&SpatialProgram, &[SpatialProgram]),
800 known: &std::collections::BTreeMap<String, super::value_range::Interval>| {
801 (forwards(t, writer, reader) && !names(before, t) && !names(after, t))
802 .then(|| super::passes::forward::forward(a, b, t))
803 .flatten()
804 .filter(|j| keeps(j, t, known))
805 };
806 let mut pieces: Vec<Piece> = Vec::new();
807 for &k in &linked {
808 let op = &g.ops[k];
809 if let Some(c) = tp.and_then(|m| m.collective_folding(&op.out)) {
813 if op.out == c.out {
814 let n = match op.prim() {
815 Some(TensorOpSpec::Elementwise(e)) => e.n,
816 _ => 0,
817 };
818 let floats = float_out.contains(&c.out) || c.partials.iter().any(|x| float_out.contains(x));
819 let mut narrow = |t: SpatialProgram| narrow_storage(t, &mut known, target);
820 match allreduce_section(c, g.shape_of(&c.out, n), floats, target, certification, &mut narrow) {
821 Ok(p) => pieces.push(Piece::Done(vec![p])),
822 Err(e) => {
823 pieces.push(Piece::Done(Vec::new()));
824 missing.push(MissingStage {
825 stmt_index: stmt_of(k),
826 binder: op.out.clone(),
827 kind: "AllReduce".into(),
828 why: format!("{e:?}"),
829 })
830 }
831 }
832 } else {
833 pieces.push(Piece::Done(Vec::new()));
834 }
835 lowered += 1;
836 continue;
837 }
838 let floats = match op.prim() {
841 Some(TensorOpSpec::ArgMax(_)) => float_out.contains(&op.ins[0]),
842 _ => float_out.contains(&op.out),
843 };
844 let made = match &op.sym {
850 crate::tensor_graph::graph::Sym::Fused(f) if f.class == crate::tensor_graph::symbols::ClassId::ElementwiseRegions => {
851 let mut held: Vec<Held> = Vec::new();
852 let mut alone: Vec<SpatialProgram> = Vec::new();
853 let mut refused = None;
854 let mut one = TensorGraph { identity: g.identity.clone(), io: g.io.clone(), tensors: g.tensors.clone(), ops: Vec::new() };
856 for m in &f.parts {
857 one.ops = vec![m.clone()];
858 match link(&one, 0, &prelude, Form::Section { floats: float_out.contains(&m.out) }, NumericsPolicy::Exact, target) {
859 Ok(ts) => {
860 alone.extend(ts.iter().cloned());
861 held.push(Held { at: k, stmt_index: lifted_stmts[m.covers[0]], out: m.out.clone(), kind: m.kind().to_string(), templates: ts });
862 }
863 Err(e) => {
864 refused = Some(e);
865 break;
866 }
867 }
868 }
869 match refused {
870 Some(e) => Err(e),
871 None => {
872 let single = held.iter().all(|h| h.templates.len() == 1) && !matches!(results, Results::EveryNamed) && tp.is_none();
873 let mut joined = single.then(|| held.last().expect("a region has members").templates[0].clone());
874 for h in held.iter().rev().skip(1) {
875 joined = joined.and_then(|acc| {
876 super::passes::forward::forward(&h.templates[0], &acc, &h.out).filter(|j| keeps(j, &h.out, &known)).map(|j| j.program)
877 });
878 }
879 Ok((held, joined.map_or(alone, |p| vec![p])))
880 }
881 }
882 }
883 _ => link(&g, k, &prelude, Form::Section { floats }, NumericsPolicy::Exact, target)
884 .map(|ts| share_q8_views(ts, prelude.views.get(&k), &mut q8_views))
885 .map(|ts| (vec![Held { at: k, stmt_index: stmt_of(k), out: op.out.clone(), kind: op.kind().to_string(), templates: ts.clone() }], ts)),
886 };
887 match made {
888 Ok((held, ts)) => {
889 let joint = match pieces.last() {
890 Some(Piece::Run(run)) => {
891 let last = run.held.last().expect("a run holds a statement");
892 run.joined
893 .split_last()
894 .zip(ts.split_first())
895 .and_then(|((a, before), (b, after))| join(&last.out, last.at, k, (before, a), (b, after), &known))
896 }
897 _ => None,
898 };
899 match (joint, pieces.last_mut()) {
900 (Some(j), Some(Piece::Run(run))) => {
901 run.joined.pop();
902 run.joined.push(j.program);
903 run.joined.extend(ts.into_iter().skip(1));
904 run.held.extend(held);
905 }
906 _ => pieces.push(Piece::Run(Run { held, joined: ts })),
907 }
908 }
909 Err(e) => {
910 pieces.push(Piece::Done(Vec::new()));
911 missing.push(MissingStage {
912 stmt_index: stmt_of(k),
913 binder: op.out.clone(),
914 kind: op.kind().to_string(),
915 why: format!("{e:?}"),
916 });
917 }
918 }
919 }
920 let alone = |h: &Held,
922 known: &mut std::collections::BTreeMap<String, super::value_range::Interval>,
923 twins: &mut std::collections::BTreeSet<String>|
924 -> Result<Vec<SpatialProgram>, String> {
925 let ps = h
926 .templates
927 .iter()
928 .cloned()
929 .map(|t| passes(narrow_storage(t, known, target)))
930 .collect::<Result<Vec<_>, _>>()
931 .map_err(|e| format!("{e:?}"))?;
932 let ps = prelude.agree_layouts(&g, ps, twins, &passes, target).map_err(|e| format!("{e:?}"))?;
933 match tp {
934 Some(m) => ps.iter().map(|p| relocate(p, &h.out, m).map_err(|e| format!("{e:?}"))).collect(),
935 None => Ok(ps),
936 }
937 };
938 let plan_run = |run: &Run, known: &mut std::collections::BTreeMap<String, super::value_range::Interval>| -> Option<Vec<SpatialProgram>> {
940 if run.held.len() == 1 {
941 return None;
942 }
943 run.joined
944 .iter()
945 .cloned()
946 .map(|t| run_passes_through_tile(narrow_storage(t, known, target), target))
947 .collect::<Result<Vec<_>, _>>()
948 .ok()
949 };
950 let place = |ps: Vec<SpatialProgram>, twins: &mut std::collections::BTreeSet<String>| -> Option<Vec<SpatialProgram>> {
951 let ps = ps.into_iter().map(|p| run_passes_after_tile(p, target, certify)).collect::<Result<Vec<_>, _>>().ok()?;
952 prelude.agree_layouts(&g, ps, twins, &passes, target).ok()
953 };
954 let lower_alone = |run: &Run,
955 sections: &mut Vec<SpatialProgram>,
956 missing: &mut Vec<MissingStage>,
957 lowered: &mut usize,
958 known: &mut std::collections::BTreeMap<String, super::value_range::Interval>,
959 twins: &mut std::collections::BTreeSet<String>| {
960 for h in &run.held {
961 match alone(h, known, twins) {
962 Ok(mut ps) => {
963 *lowered += 1;
964 sections.append(&mut ps);
965 }
966 Err(why) => missing.push(MissingStage { stmt_index: h.stmt_index, binder: h.out.clone(), kind: h.kind.clone(), why }),
967 }
968 }
969 };
970 let mut planned: Vec<(Run, Vec<SpatialProgram>)> = Vec::new();
973 let flush_planned = |planned: &mut Vec<(Run, Vec<SpatialProgram>)>,
974 sections: &mut Vec<SpatialProgram>,
975 missing: &mut Vec<MissingStage>,
976 lowered: &mut usize,
977 known: &mut std::collections::BTreeMap<String, super::value_range::Interval>,
978 twins: &mut std::collections::BTreeSet<String>| {
979 let chain = std::mem::take(planned);
980 let mut units: Vec<(Vec<usize>, Vec<SpatialProgram>)> = Vec::new();
984 for (k, (run, ps)) in chain.iter().enumerate() {
985 let joint = match (k.checked_sub(1), units.last()) {
986 (Some(p), Some((_, joined))) => {
987 let prev = chain[p].0.held.last().expect("a run holds a statement");
988 chain[p]
989 .1
990 .split_last()
991 .map(|(_, before)| before)
992 .zip(joined.last())
993 .zip(ps.split_first())
994 .and_then(|((before, a), (b, after))| join(&prev.out, prev.at, run.held[0].at, (before, a), (b, after), known))
995 }
996 _ => None,
997 };
998 match (joint, units.last_mut()) {
999 (Some(j), Some((runs, joined))) => {
1000 joined.pop();
1001 joined.push(j.program);
1002 joined.extend(ps.iter().skip(1).cloned());
1003 runs.push(k);
1004 }
1005 _ => units.push((vec![k], ps.clone())),
1006 }
1007 }
1008 for (runs, ps) in units {
1009 if runs.len() > 1 {
1010 if let Some(mut ps) = place(ps, twins) {
1011 *lowered += runs.iter().map(|k| chain[*k].0.held.len()).sum::<usize>();
1012 sections.append(&mut ps);
1013 continue;
1014 }
1015 }
1016 for k in runs {
1017 let (run, ps) = &chain[k];
1018 match place(ps.clone(), twins) {
1019 Some(mut ps) => {
1020 *lowered += run.held.len();
1021 sections.append(&mut ps);
1022 }
1023 None => lower_alone(run, sections, missing, lowered, known, twins),
1024 }
1025 }
1026 }
1027 };
1028 for piece in pieces {
1029 match piece {
1030 Piece::Done(mut ps) => {
1031 flush_planned(&mut planned, &mut sections, &mut missing, &mut lowered, &mut known, &mut twins);
1032 sections.append(&mut ps);
1033 }
1034 Piece::Run(run) => {
1035 let ps = if tp.is_some() {
1036 None
1037 } else {
1038 plan_run(&run, &mut known).or_else(|| {
1039 run.joined
1040 .iter()
1041 .cloned()
1042 .filter(|_| run.held.len() == 1)
1043 .map(|t| run_passes_through_tile(narrow_storage(t, &mut known, target), target))
1044 .collect::<Result<Vec<_>, _>>()
1045 .ok()
1046 .filter(|ps| !ps.is_empty())
1047 })
1048 };
1049 match ps {
1050 Some(ps) => planned.push((run, ps)),
1051 None => {
1052 flush_planned(&mut planned, &mut sections, &mut missing, &mut lowered, &mut known, &mut twins);
1053 lower_alone(&run, &mut sections, &mut missing, &mut lowered, &mut known, &mut twins);
1054 }
1055 }
1056 }
1057 }
1058 }
1059 flush_planned(&mut planned, &mut sections, &mut missing, &mut lowered, &mut known, &mut twins);
1060 let stored: std::collections::BTreeSet<&str> = sections
1064 .iter()
1065 .flat_map(|p| {
1066 p.stages.iter().filter_map(move |s| match &s.kind {
1067 StageKind::Store { tensor, .. } => Some(p.tensor(*tensor).name.as_str()),
1068 _ => None,
1069 })
1070 })
1071 .collect();
1072 for p in §ions {
1073 for s in &p.stages {
1074 let StageKind::Load { tensor, .. } = &s.kind else { continue };
1075 let name = &p.tensor(*tensor).name;
1076 if stored.contains(name.as_str()) || missing.iter().any(|m| m.binder == *name) {
1077 continue;
1078 }
1079 if let Some(st) = rp.stages.iter().find(|st| st.out == *name) {
1080 missing.push(MissingStage {
1081 stmt_index: st.stmt_index,
1082 binder: name.clone(),
1083 kind: "read".into(),
1084 why: format!("section {} loads it and no section stores it", p.name),
1085 });
1086 }
1087 }
1088 }
1089 missing.sort_by_key(|m| m.stmt_index);
1090 let state = state_tensors(&rp.stages);
1091 let n_sections = sections.len();
1092 let mut program = None;
1093 if lowered == rp.stages.len() && missing.is_empty() {
1094 let (sections, barriers) = match tp {
1097 Some(_) => {
1098 let (order, barriers) = super::tensor_parallel::phases(§ions);
1099 let mut slots: Vec<Option<SpatialProgram>> =
1100 sections.into_iter().map(Some).collect();
1101 (
1102 order
1103 .iter()
1104 .map(|k| slots[*k].take().expect("each section once"))
1105 .collect(),
1106 Some(barriers),
1107 )
1108 }
1109 None => match super::passes::pack::pack_phases(sections, target) {
1110 Ok((sections, barriers)) => (sections, Some(barriers)),
1111 Err(sections) => (sections, None),
1112 },
1113 };
1114 match super::passes::compose::compose_phased(
1115 sections,
1116 &links(&rp.stages),
1117 &state,
1118 super::passes::compose::SectionPlacement::Stacked,
1119 barriers.as_deref(),
1120 target,
1121 ) {
1122 Ok(p) => match super::passes::verify::verify(&declare_ranges(p, &ranges), target) {
1128 Ok(v) => program = Some(v),
1129 Err(e) => missing.push(MissingStage {
1130 stmt_index: rp.stages.last().map(|s| s.stmt_index).unwrap_or(0),
1131 binder: rp.stages.last().map(|s| s.out.clone()).unwrap_or_default(),
1132 kind: "verify".into(),
1133 why: format!("the composed program does not certify: {e:?}"),
1134 }),
1135 },
1136 Err(e) => missing.push(MissingStage {
1137 stmt_index: rp.stages.last().map(|s| s.stmt_index).unwrap_or(0),
1138 binder: rp.stages.last().map(|s| s.out.clone()).unwrap_or_default(),
1139 kind: "compose".into(),
1140 why: format!("{e:?}"),
1141 }),
1142 }
1143 }
1144 let carried = super::passes::recognize_stages::carried_state(&rp);
1145 Ok(ProgramLowering {
1146 program,
1147 statements,
1148 lowered,
1149 sections: n_sections,
1150 missing,
1151 state,
1152 carried,
1153 })
1154}
1155
1156fn narrow_storage(
1165 mut t: SpatialProgram,
1166 ranges: &mut std::collections::BTreeMap<String, super::value_range::Interval>,
1167 target: &SpatialTarget,
1168) -> SpatialProgram {
1169 use super::target::precision_int_bits;
1170 let storable = target.storable_ints();
1171 let matmul = |s: &Stage| matches!(s.kind, StageKind::Compute(ComputeOp::MatmulTiles { .. }));
1172 let operands: std::collections::BTreeSet<BufferId> =
1173 t.stages.iter().filter(|s| matmul(s)).flat_map(|s| s.ins.iter().copied()).collect();
1174 let proven = super::value_range::program_ranges(&declare_ranges(t.clone(), ranges));
1175 let whole = !t.stages.iter().any(matmul);
1176 if whole {
1179 for st in &t.stages {
1180 if let StageKind::Store { tensor, .. } = &st.kind {
1181 let name = &t.tensor(*tensor).name;
1182 if let (false, Some(iv)) = (ranges.contains_key(name), proven.tensors[tensor.idx()]) {
1183 if iv != super::value_range::UNBOUNDED {
1184 ranges.insert(name.clone(), iv);
1185 }
1186 }
1187 }
1188 }
1189 }
1190 if storable.iter().all(|p| *p == Precision::Int64) {
1191 return t;
1192 }
1193 let loaded: std::collections::BTreeSet<BufferId> = t
1194 .stages
1195 .iter()
1196 .filter(|s| matches!(s.kind, StageKind::Load { .. }))
1197 .flat_map(|s| s.outs.iter().copied())
1198 .collect();
1199 for b in &mut t.buffers {
1200 let Some(now) = precision_int_bits(b.dtype) else { continue };
1201 if operands.contains(&b.id) || !(whole || loaded.contains(&b.id)) {
1202 continue;
1203 }
1204 let Some(iv) = proven.buffers[b.id.idx()] else { continue };
1205 let fits = |p: &Precision| {
1206 let bits = precision_int_bits(*p).unwrap_or(64);
1207 bits >= 64 || (iv.0 >= -(1i64 << (bits - 1)) && iv.1 < (1i64 << (bits - 1)))
1208 };
1209 if let Some(&p) = storable.iter().find(|p| fits(p)) {
1210 if precision_int_bits(p).unwrap_or(64) < now {
1211 b.dtype = p;
1212 }
1213 }
1214 }
1215 t
1216}
1217
1218fn declare_ranges(
1223 mut p: SpatialProgram,
1224 ranges: &std::collections::BTreeMap<String, super::value_range::Interval>,
1225) -> SpatialProgram {
1226 for t in &mut p.tensors {
1227 if super::target::precision_int_bits(t.dtype).is_none() {
1228 continue;
1229 }
1230 let Some(&r) = ranges.get(&t.name) else { continue };
1231 if r == super::value_range::UNBOUNDED {
1232 continue;
1233 }
1234 t.range = Some(match t.range {
1235 Some(d) if d.0.max(r.0) <= d.1.min(r.1) => (d.0.max(r.0), d.1.min(r.1)),
1236 _ => r,
1237 });
1238 }
1239 p
1240}
1241
1242pub fn dense_tables_of(
1244 source: &str,
1245 target: &SpatialTarget,
1246) -> Result<std::collections::BTreeSet<String>, SpatialReject> {
1247 let g = TensorGraph::lift_source(source, &|_| Storage::Values)?.graph;
1248 let every: Vec<usize> = (0..g.ops.len()).collect();
1249 Ok(dense_tables(&g, &every, target, &std::collections::BTreeSet::new()))
1250}
1251
1252pub fn value_ranges_of(
1255 source: &str,
1256) -> Result<std::collections::BTreeMap<String, super::value_range::Interval>, SpatialReject> {
1257 let rp = TensorGraph::lift_source(source, &|_| Storage::Values)?.program;
1258 Ok(super::value_range::value_ranges(&rp, &float_tensors(&rp)))
1259}
1260
1261pub fn program_links(source: &str) -> Result<Vec<Link>, SpatialReject> {
1263 TensorGraph::lift_source(source, &|_| Storage::Values).map(|l| links(&l.program.stages))
1264}
1265
1266#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1268pub enum CandidateKind {
1269 Best,
1270 Streamed,
1271 Fused,
1272 Unfused,
1273}
1274
1275impl CandidateKind {
1276 pub fn label(self) -> &'static str {
1278 match self {
1279 CandidateKind::Best => "best",
1280 CandidateKind::Streamed => "streamed",
1281 CandidateKind::Fused => "multicast",
1282 CandidateKind::Unfused => "blocked",
1283 }
1284 }
1285}
1286
1287impl PlanSummary {
1288 pub fn arrangement_label(&self) -> &'static str {
1293 let split = self.engines == EnginePacking::OperandWithStore;
1294 match (self.streaming, self.multicast_groups > 0, split) {
1295 (true, _, _) => "streamed",
1296 (_, true, false) => "multicast",
1297 (_, true, true) => "multicast-split",
1298 (_, false, false) => "blocked",
1299 (_, false, true) => "blocked-split",
1300 }
1301 }
1302}
1303
1304#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1310pub struct PlanFilter {
1311 pub pack_accumulate: Option<bool>,
1312 pub engines: Option<EnginePacking>,
1313 pub per_site: Option<(usize, usize)>,
1315 pub block: Option<BlockShape>,
1317}
1318
1319impl PlanFilter {
1320 fn allows(&self, plan: &BlockPlan) -> bool {
1321 self.pack_accumulate
1322 .map(|w| plan.pack_accumulate == w)
1323 .unwrap_or(true)
1324 && self.engines.map(|w| plan.engines == w).unwrap_or(true)
1325 && self
1326 .per_site
1327 .map(|w| (plan.per_site_m, plan.per_site_n) == w)
1328 .unwrap_or(true)
1329 && self.block.map(|w| plan.block == w).unwrap_or(true)
1330 }
1331 fn is_empty(&self) -> bool {
1332 *self == PlanFilter::default()
1333 }
1334}
1335
1336pub fn calibration_candidates(
1337 source: &str,
1338 target: &SpatialTarget,
1339 numerics: NumericsPolicy,
1340) -> Result<Vec<(CandidateKind, SpatialProgram)>, SpatialReject> {
1341 calibration_candidates_matching(source, target, numerics, PlanFilter::default())
1342}
1343
1344pub fn calibration_candidates_matching(
1346 source: &str,
1347 target: &SpatialTarget,
1348 numerics: NumericsPolicy,
1349 want: PlanFilter,
1350) -> Result<Vec<(CandidateKind, SpatialProgram)>, SpatialReject> {
1351 let template = template_from_source_with(source, target, numerics)?;
1352 let mut out = Vec::new();
1353 if want.is_empty() {
1354 out.push((CandidateKind::Best, run_passes(template.clone(), target)?));
1355 }
1356 let cands: Vec<tile::Candidate> =
1357 tile::candidates_where(&template, target, &|plan| want.allows(plan))
1358 .into_iter()
1359 .filter(|c| want.allows(&c.plan))
1360 .collect();
1361 let objective = crate::spatial::work::Objective::default();
1365 let pick = |filter: &dyn Fn(&tile::Candidate) -> bool| {
1366 let arrangement: Vec<tile::Candidate> =
1367 cands.iter().filter(|c| filter(c)).cloned().collect();
1368 tile::best_among(&arrangement, objective).cloned()
1369 };
1370 let variants: [(CandidateKind, Option<tile::Candidate>); 4] = [
1371 (
1372 CandidateKind::Best,
1373 if want.is_empty() {
1374 None
1375 } else {
1376 tile::best_among(&cands, objective).cloned()
1377 },
1378 ),
1379 (
1380 CandidateKind::Streamed,
1381 pick(&|c| super::cost::is_streaming(&c.plan)),
1382 ),
1383 (CandidateKind::Fused, pick(&|c| c.fused)),
1384 (
1385 CandidateKind::Unfused,
1386 pick(&|c| !c.fused && !super::cost::is_streaming(&c.plan)),
1387 ),
1388 ];
1389 for (kind, cand) in variants {
1390 let Some(c) = cand else { continue };
1391 out.push((kind, build_candidate(&template, &c, target)?));
1392 }
1393 Ok(out)
1394}
1395
1396pub fn build_candidate(
1399 template: &SpatialProgram,
1400 c: &tile::Candidate,
1401 target: &SpatialTarget,
1402) -> Result<SpatialProgram, SpatialReject> {
1403 verify::verify(&build_candidate_uncertified(template, c, target)?, target)
1404}
1405
1406pub fn build_candidate_uncertified(
1409 template: &SpatialProgram,
1410 c: &tile::Candidate,
1411 target: &SpatialTarget,
1412) -> Result<SpatialProgram, SpatialReject> {
1413 let tiled = tile::apply_plan(template, c.plan, target);
1414 let placed = place::place(&tiled, target)?;
1415 let fused = fuse_readers::fuse_readers_forced(&placed, target, c.fused)?;
1416 let sized = size_buffers::size_buffers(&fused, target)?;
1417 let sched = synthesize_handshakes::synthesize_handshakes(&sized, target)?;
1418 persist::persist(&sched, target)
1419}