1use rustc_hash::FxHashMap;
21use std::collections::{BTreeMap, BTreeSet};
22
23use serde::{Deserialize, Serialize};
24
25use super::eval::{eval_choreography_dry, eval_choreography_traced, ChoreoFault, Inputs, Value};
26use super::ir::*;
27use crate::codegen_sva::bitblast::{word_size, BlastRefusal};
28use crate::codegen_sva::fabric_grid::{
29 cells_datapath, cells_to_gates, grid_output_to_bounded, grid_output_to_gates, route_loop_body,
30 unroutable_cell, BoundedBuild, TILE_OPERAND_CHANNELS,
31};
32use crate::codegen_sva::fabric_rtl::{AluOp, FabricOp, LoopCell, Src};
33use crate::codegen_sva::fabric_target::Precision;
34use crate::codegen_sva::pnr_certificate::{
35 certify_gates_within, certify_mapping, PlacementCertificate, GATE_BUDGET_NODES,
36};
37use crate::codegen_sva::sva_to_verify::BoundedExpr;
38use crate::spatial::target::precision_is_int;
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
41pub enum PlaceKind {
42 Space {
43 buf: BufferId,
44 k: usize,
45 },
46 Data {
47 buf: BufferId,
48 k: usize,
49 },
50 Ready {
51 sem: SemId,
52 receiver: Site,
53 k: usize,
54 },
55 Valid {
56 sem: SemId,
57 receiver: Site,
58 k: usize,
59 },
60 McastData {
61 group: GroupId,
62 receiver: Site,
63 k: usize,
64 },
65 Pc {
66 site: Site,
67 engine: Engine,
68 k: usize,
69 },
70 Arrival {
81 sem: SemId,
82 at: Site,
83 k: usize,
84 },
85}
86
87#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
88pub struct Place {
89 pub kind: PlaceKind,
90 pub marking: usize,
91}
92
93#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
94pub struct Transition {
95 pub site: Site,
96 pub engine: Engine,
97 pub op_index: usize,
98 pub consumes: Vec<usize>,
99 pub produces: Vec<usize>,
100}
101
102#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
103pub struct MarkedGraph {
104 pub places: Vec<Place>,
105 pub transitions: Vec<Transition>,
106}
107
108#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
109pub struct LivenessWitness {
110 pub topo_order: Vec<usize>,
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
114pub struct BufferBalance {
115 pub buf: BufferId,
116 pub depth_pages: usize,
117 pub reserved: u64,
118 pub pushed: u64,
119 pub popped: u64,
120 pub high_water: usize,
121}
122
123#[derive(Clone, Debug)]
124pub enum ComputeStageCert {
125 Sat(PlacementCertificate),
126 Uncertified { reason: String },
127}
128
129impl PartialEq for ComputeStageCert {
130 fn eq(&self, other: &Self) -> bool {
131 match (self, other) {
132 (ComputeStageCert::Sat(a), ComputeStageCert::Sat(b)) => {
133 a.len() == b.len() && a.num_vars() == b.num_vars()
134 }
135 (
136 ComputeStageCert::Uncertified { reason: a },
137 ComputeStageCert::Uncertified { reason: b },
138 ) => a == b,
139 _ => false,
140 }
141 }
142}
143
144#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
147pub struct SectionProof {
148 pub section: usize,
149 pub places: usize,
150 pub transitions: usize,
151 pub digest: u64,
153 pub witness: usize,
155 #[serde(default, skip_serializing_if = "Vec::is_empty")]
157 pub cases: Vec<TripCase>,
158}
159
160#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
164pub struct TripCase {
165 pub counts: Vec<usize>,
166 pub places: usize,
167 pub transitions: usize,
168 pub digest: u64,
169 pub witness: usize,
170}
171
172#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
189pub enum GraphProof {
190 Whole {
194 graph: MarkedGraph,
195 liveness: LivenessWitness,
196 #[serde(default, skip_serializing_if = "Vec::is_empty")]
198 cases: Vec<TripCase>,
199 #[serde(default, skip_serializing_if = "Vec::is_empty")]
200 case_witnesses: Vec<LivenessWitness>,
201 },
202 PerSection {
214 sections: Vec<SectionProof>,
215 witnesses: Vec<LivenessWitness>,
216 places: usize,
217 transitions: usize,
218 },
219}
220
221impl GraphProof {
222 pub fn places(&self) -> usize {
223 match self {
224 GraphProof::Whole { graph, .. } => graph.places.len(),
225 GraphProof::PerSection { places, .. } => *places,
226 }
227 }
228 pub fn transitions(&self) -> usize {
229 match self {
230 GraphProof::Whole { graph, .. } => graph.transitions.len(),
231 GraphProof::PerSection { transitions, .. } => *transitions,
232 }
233 }
234 pub fn whole_graph(&self) -> Option<&MarkedGraph> {
237 match self {
238 GraphProof::Whole { graph, .. } => Some(graph),
239 GraphProof::PerSection { .. } => None,
240 }
241 }
242 pub fn whole_graph_mut(&mut self) -> Option<&mut MarkedGraph> {
244 match self {
245 GraphProof::Whole { graph, .. } => Some(graph),
246 GraphProof::PerSection { .. } => None,
247 }
248 }
249 pub fn witnesses(&self) -> Vec<&LivenessWitness> {
251 match self {
252 GraphProof::Whole { liveness, .. } => vec![liveness],
253 GraphProof::PerSection { witnesses, .. } => witnesses.iter().collect(),
254 }
255 }
256 pub fn witness_of_section(&self) -> Option<Vec<usize>> {
258 match self {
259 GraphProof::Whole { .. } => None,
260 GraphProof::PerSection { sections, .. } => {
261 Some(sections.iter().map(|s| s.witness).collect())
262 }
263 }
264 }
265}
266
267#[derive(Clone, Debug, PartialEq)]
268pub struct ChoreographyCertificate {
269 pub schedule_digest: u64,
270 pub proof: GraphProof,
271 pub balances: Vec<BufferBalance>,
272 pub sem_counts: Vec<(SemId, u64, u64)>,
274 pub compute: Vec<(StageId, ComputeStageCert)>,
275 pub numerics: NumericsClaim,
276}
277
278impl ChoreographyCertificate {
279 pub fn places(&self) -> usize {
281 self.proof.places()
282 }
283 pub fn transitions(&self) -> usize {
285 self.proof.transitions()
286 }
287}
288
289#[derive(Clone, Debug, PartialEq)]
290pub enum CertifyFault {
291 NotScheduled,
292 Invalid(IrViolation),
293 Starved {
295 transition: usize,
296 place: PlaceKind,
297 },
298 Deadlock {
302 cycle: Vec<DeadlockNode>,
303 },
304 Bounds {
305 buffer: BufferId,
306 },
307 ActivationOrder {
311 tensor: TensorId,
312 writer: Option<usize>,
313 reader: usize,
314 reason: &'static str,
315 },
316 StateOrder {
321 tensor: TensorId,
322 reader: Option<usize>,
323 writer: Option<usize>,
324 reason: &'static str,
325 },
326 UnreducedShard {
332 tensor: TensorId,
333 chips: usize,
334 },
335 TripCasesExceed {
337 limit: usize,
338 },
339 Eval(ChoreoFault),
340}
341
342pub fn schedule_digest(p: &SpatialProgram) -> u64 {
344 fnv_json(&p.schedule)
345}
346
347fn fnv_json<T: Serialize>(v: &T) -> u64 {
350 struct Fnv(u64);
351 impl std::io::Write for Fnv {
352 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
353 for x in buf {
354 self.0 ^= u64::from(*x);
355 self.0 = self.0.wrapping_mul(0x100000001b3);
356 }
357 Ok(buf.len())
358 }
359 fn flush(&mut self) -> std::io::Result<()> {
360 Ok(())
361 }
362 }
363 let mut f = Fnv(0xcbf29ce484222325);
364 let _ = serde_json::to_writer(&mut f, v);
365 f.0
366}
367
368#[derive(Clone, Debug)]
377struct Event<'a> {
378 op: &'a ChoreoOp,
379}
380
381pub fn item_window(p: &SpatialProgram) -> usize {
387 p.buffers
388 .iter()
389 .map(|b| b.kind.depth_pages())
390 .max()
391 .unwrap_or(1)
392 + 2
393}
394
395fn unroll<'a>(ops: &'a [TaggedOp], out: &mut Vec<Event<'a>>, window: usize, counts: &[usize]) {
397 for t in ops {
398 match &t.op {
399 ChoreoOp::Loop { var, count, body } => {
400 let n = if *var == crate::spatial::passes::LV_ITEM {
411 count.at(counts).min(window)
412 } else {
413 count.at(counts)
414 };
415 for _ in 0..n {
417 unroll(body, out, window, counts);
418 }
419 }
420 other => out.push(Event { op: other }),
421 }
422 }
423}
424
425struct Builder {
429 places: Vec<Place>,
430 index: FxHashMap<PlaceKind, usize>,
431 transitions: Vec<Transition>,
432}
433
434impl Builder {
435 fn place(&mut self, kind: PlaceKind, marking: usize) -> usize {
436 if let Some(i) = self.index.get(&kind) {
437 return *i;
438 }
439 let i = self.places.len();
440 self.places.push(Place { kind, marking });
441 self.index.insert(kind, i);
442 i
443 }
444}
445
446pub fn build_marked_graph(p: &SpatialProgram) -> Result<MarkedGraph, CertifyFault> {
448 build_graph_over(p, &GraphContext::of(p), None, &p.bound_counts())
449}
450
451type Selection = Vec<(usize, std::ops::Range<usize>)>;
454
455struct GraphContext {
458 window: usize,
460 depth: Vec<usize>,
462 recv_group: BTreeMap<BufferId, GroupId>,
464 spreads: Vec<usize>,
466}
467
468impl GraphContext {
469 fn of(p: &SpatialProgram) -> Self {
470 let recv_group = p
471 .stages
472 .iter()
473 .filter_map(|s| match (&s.kind, s.outs.first()) {
474 (StageKind::Recv { group }, Some(o)) => Some((*o, *group)),
475 _ => None,
476 })
477 .collect();
478 GraphContext {
479 window: item_window(p),
480 depth: p.buffers.iter().map(|bf| bf.kind.depth_pages()).collect(),
481 recv_group,
482 spreads: trip_spreads(p),
483 }
484 }
485}
486
487fn build_graph_over(
498 p: &SpatialProgram,
499 cx: &GraphContext,
500 sel: Option<&[(usize, std::ops::Range<usize>)]>,
501 counts: &[usize],
502) -> Result<MarkedGraph, CertifyFault> {
503 let sched = p.schedule.as_ref().ok_or(CertifyFault::NotScheduled)?;
504 let pl = p.placement.as_ref().ok_or(CertifyFault::NotScheduled)?;
505 let mut b = Builder {
506 places: Vec::new(),
507 index: FxHashMap::default(),
508 transitions: Vec::new(),
509 };
510 let chosen: Vec<(&EngineProgram, &[TaggedOp])> = match sel {
511 Some(r) => r
512 .iter()
513 .map(|(pi, ops)| (&sched.programs[*pi], &sched.programs[*pi].ops[ops.clone()]))
514 .collect(),
515 None => sched
516 .programs
517 .iter()
518 .map(|prog| (prog, prog.ops.as_slice()))
519 .collect(),
520 };
521 let mut events: Vec<Vec<Event>> = Vec::new();
523 for (_, ops) in &chosen {
524 let mut out = Vec::new();
525 unroll(ops, &mut out, cx.window, counts);
526 events.push(out);
527 }
528 let mut base = Vec::new();
530 let mut total = 0;
531 for ev in &events {
532 base.push(total);
533 total += ev.len();
534 }
535 for (ci, (prog, _)) in chosen.iter().enumerate() {
536 for (ei, _) in events[ci].iter().enumerate() {
537 b.transitions.push(Transition {
538 site: prog.site,
539 engine: prog.engine,
540 op_index: ei,
541 consumes: Vec::new(),
542 produces: Vec::new(),
543 });
544 }
545 }
546 for (ci, (prog, _)) in chosen.iter().enumerate() {
548 for ei in 0..events[ci].len() {
549 let t = base[ci] + ei;
550 let pc_in = b.place(
551 PlaceKind::Pc {
552 site: prog.site,
553 engine: prog.engine,
554 k: ei,
555 },
556 if ei == 0 { 1 } else { 0 },
557 );
558 b.transitions[t].consumes.push(pc_in);
559 let pc_out = b.place(
560 PlaceKind::Pc {
561 site: prog.site,
562 engine: prog.engine,
563 k: ei + 1,
564 },
565 0,
566 );
567 b.transitions[t].produces.push(pc_out);
568 }
569 }
570 let depth = &cx.depth;
573 let (mut reserved, mut pushed, mut waited_hi, mut popped): (
574 FxHashMap<usize, usize>,
575 FxHashMap<usize, usize>,
576 FxHashMap<usize, usize>,
577 FxHashMap<usize, usize>,
578 ) = Default::default();
579 let mut inc_round: FxHashMap<(SemId, Site), usize> = FxHashMap::default();
581 let mut wait_round: FxHashMap<(SemId, Site), usize> = FxHashMap::default();
582 let mut arrival_round: FxHashMap<(SemId, Site), usize> = FxHashMap::default();
584 let mut valid_set_round: FxHashMap<(SemId, GroupId), usize> = FxHashMap::default();
586 let mut mcast_round: FxHashMap<GroupId, usize> = FxHashMap::default();
587 let mut recv_push_round: FxHashMap<(GroupId, Site), usize> = FxHashMap::default();
588 let recv_group = &cx.recv_group;
589 for (ci, (prog, _)) in chosen.iter().enumerate() {
592 for (ei, ev) in events[ci].iter().enumerate() {
593 let t = base[ci] + ei;
594 match ev.op {
595 ChoreoOp::Reserve { buf, pages } => {
596 for _ in 0..*pages {
597 let k = bump(&mut reserved, buf.idx());
598 let initial = k <= depth[buf.idx()];
599 let pl_id = b.place(
600 PlaceKind::Space { buf: *buf, k },
601 if initial { 1 } else { 0 },
602 );
603 b.transitions[t].consumes.push(pl_id);
604 }
605 }
606 ChoreoOp::Push { buf, pages } => {
607 for _ in 0..*pages {
608 let k = bump(&mut pushed, buf.idx());
609 let pl_id = b.place(PlaceKind::Data { buf: *buf, k }, 0);
610 b.transitions[t].produces.push(pl_id);
611 }
612 if let Some(g) = recv_group.get(buf) {
613 let r = *recv_push_round.entry((*g, prog.site)).or_insert(0);
614 recv_push_round.insert((*g, prog.site), r + 1);
615 let pl_id = b.place(
616 PlaceKind::McastData {
617 group: *g,
618 receiver: prog.site,
619 k: r + 1,
620 },
621 0,
622 );
623 b.transitions[t].consumes.push(pl_id);
624 }
625 }
626 ChoreoOp::Wait { buf, pages } => {
627 let need = popped.get(&buf.idx()).copied().unwrap_or(0) + *pages;
628 while waited_hi.get(&buf.idx()).copied().unwrap_or(0) < need {
629 let k = bump(&mut waited_hi, buf.idx());
630 let pl_id = b.place(PlaceKind::Data { buf: *buf, k }, 0);
631 b.transitions[t].consumes.push(pl_id);
632 }
633 }
634 ChoreoOp::Pop { buf, pages } => {
635 for _ in 0..*pages {
636 let k = bump(&mut popped, buf.idx()) + depth[buf.idx()];
637 let pl_id = b.place(PlaceKind::Space { buf: *buf, k }, 0);
638 b.transitions[t].produces.push(pl_id);
639 }
640 let done = popped.get(&buf.idx()).copied().unwrap_or(0);
641 let hi = waited_hi.entry(buf.idx()).or_insert(0);
642 *hi = (*hi).max(done);
643 }
644 ChoreoOp::SemInc { sem, at, by: _ }
645 if matches!(p.semaphores[sem.idx()].role, SemRole::Barrier { .. }) =>
646 {
647 let r = *arrival_round.entry((*sem, *at)).or_insert(0);
648 arrival_round.insert((*sem, *at), r + 1);
649 let pl_id = b.place(
650 PlaceKind::Arrival {
651 sem: *sem,
652 at: *at,
653 k: r + 1,
654 },
655 0,
656 );
657 b.transitions[t].produces.push(pl_id);
658 }
659 ChoreoOp::SemInc { sem, at, by: _ } => {
660 let r = *inc_round.entry((*sem, prog.site)).or_insert(0);
661 inc_round.insert((*sem, prog.site), r + 1);
662 let _ = at;
663 let pl_id = b.place(
664 PlaceKind::Ready {
665 sem: *sem,
666 receiver: prog.site,
667 k: r + 1,
668 },
669 0,
670 );
671 b.transitions[t].produces.push(pl_id);
672 }
673 ChoreoOp::SemWait { sem, until }
677 if matches!(p.semaphores[sem.idx()].role, SemRole::Barrier { .. }) =>
678 {
679 let (SemCond::AtLeast(c) | SemCond::Equals(c)) = *until;
680 for k in 1..=c {
681 let pl_id = b.place(
682 PlaceKind::Arrival {
683 sem: *sem,
684 at: prog.site,
685 k,
686 },
687 0,
688 );
689 b.transitions[t].consumes.push(pl_id);
690 }
691 }
692 ChoreoOp::SemWait { sem, .. } => {
693 let r = *wait_round.entry((*sem, prog.site)).or_insert(0);
694 wait_round.insert((*sem, prog.site), r + 1);
695 match p.semaphores[sem.idx()].role {
696 SemRole::Barrier { .. } => {
697 unreachable!("a rendezvous wait is matched above")
698 }
699 SemRole::ReadyCounter { .. } => {
700 let sem_ref = &p.semaphores[sem.idx()];
702 let own_group = sem_ref.operand.and_then(|op| {
703 pl.groups
704 .iter()
705 .find(|g| g.operand == op && g.sender == prog.site)
706 });
707 let receivers: Vec<Site> = match own_group {
708 Some(gg) => gg
709 .rect
710 .sites()
711 .into_iter()
712 .filter(|s| *s != gg.sender)
713 .collect(),
714 None => sem_ref
715 .sites
716 .iter()
717 .copied()
718 .filter(|s| *s != prog.site)
719 .collect(),
720 };
721 for rcv in receivers {
722 let pl_id = b.place(
723 PlaceKind::Ready {
724 sem: *sem,
725 receiver: rcv,
726 k: r + 1,
727 },
728 0,
729 );
730 b.transitions[t].consumes.push(pl_id);
731 }
732 }
733 SemRole::ValidFlag => {
734 let pl_id = b.place(
735 PlaceKind::Valid {
736 sem: *sem,
737 receiver: prog.site,
738 k: r + 1,
739 },
740 0,
741 );
742 b.transitions[t].consumes.push(pl_id);
743 }
744 }
745 }
746 ChoreoOp::McastSetValid { sem, group } => {
747 let r = *valid_set_round.entry((*sem, *group)).or_insert(0);
748 valid_set_round.insert((*sem, *group), r + 1);
749 let g = &pl.groups[group.idx()];
750 for rcv in g.rect.sites().into_iter().filter(|s| *s != g.sender) {
751 let pl_id = b.place(
752 PlaceKind::Valid {
753 sem: *sem,
754 receiver: rcv,
755 k: r + 1,
756 },
757 0,
758 );
759 b.transitions[t].produces.push(pl_id);
760 }
761 }
762 ChoreoOp::McastSend { group, .. } => {
763 let r = *mcast_round.entry(*group).or_insert(0);
764 mcast_round.insert(*group, r + 1);
765 let g = &pl.groups[group.idx()];
766 for rcv in g.rect.sites().into_iter().filter(|s| *s != g.sender) {
767 let pl_id = b.place(
768 PlaceKind::McastData {
769 group: *group,
770 receiver: rcv,
771 k: r + 1,
772 },
773 0,
774 );
775 b.transitions[t].produces.push(pl_id);
776 }
777 }
778 _ => {}
779 }
780 }
781 }
782 Ok(MarkedGraph {
783 places: b.places,
784 transitions: b.transitions,
785 })
786}
787
788fn bump(counts: &mut FxHashMap<usize, usize>, key: usize) -> usize {
790 let c = counts.entry(key).or_insert(0);
791 *c += 1;
792 *c
793}
794
795#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
797pub struct DeadlockNode {
798 pub site: Site,
799 pub engine: Engine,
800 pub op_index: usize,
801 pub waiting_on: PlaceKind,
802}
803
804fn token_free_cycle(
811 g: &MarkedGraph,
812 indeg: &[usize],
813 succ: &[Vec<usize>],
814 producer: &[Option<usize>],
815) -> Vec<DeadlockNode> {
816 let n = g.transitions.len();
817 let stuck = |i: usize| indeg[i] > 0;
818 let mut color = vec![0u8; n];
820 let mut stack: Vec<usize> = Vec::new();
821 let mut ring: Vec<usize> = Vec::new();
822 for root in (0..n).filter(|i| stuck(*i)) {
823 if color[root] != 0 {
824 continue;
825 }
826 let mut work: Vec<(usize, usize)> = vec![(root, 0)];
828 color[root] = 1;
829 stack.push(root);
830 while !work.is_empty() {
831 let (node, idx) = work.last().map(|t| (t.0, t.1)).expect("non-empty");
832 match succ[node].get(idx).copied() {
833 Some(next) => {
834 work.last_mut().expect("non-empty").1 += 1;
835 if !stuck(next) {
836 continue;
837 }
838 if color[next] == 1 {
839 let at = stack.iter().position(|&x| x == next).unwrap_or(0);
840 ring = stack[at..].to_vec();
841 break;
842 }
843 if color[next] == 0 {
844 color[next] = 1;
845 stack.push(next);
846 work.push((next, 0));
847 }
848 }
849 None => {
850 color[node] = 2;
851 stack.pop();
852 work.pop();
853 }
854 }
855 }
856 if !ring.is_empty() {
857 break;
858 }
859 }
860 if ring.is_empty() {
862 ring = (0..n).filter(|i| stuck(*i)).take(1).collect();
863 }
864 ring.iter()
865 .take(12)
866 .map(|&ti| {
867 let t = &g.transitions[ti];
868 let waiting_on = t
870 .consumes
871 .iter()
872 .find(|pl| {
873 g.places[**pl].marking == 0 && producer[**pl].map(stuck).unwrap_or(false)
874 })
875 .or_else(|| t.consumes.iter().find(|pl| g.places[**pl].marking == 0))
876 .map(|pl| g.places[*pl].kind)
877 .unwrap_or(g.places[0].kind);
878 DeadlockNode {
879 site: t.site,
880 engine: t.engine,
881 op_index: t.op_index,
882 waiting_on,
883 }
884 })
885 .collect()
886}
887
888pub fn liveness_witness(g: &MarkedGraph) -> Result<LivenessWitness, CertifyFault> {
891 let n = g.transitions.len();
892 let mut producer = vec![None; g.places.len()];
893 for (ti, t) in g.transitions.iter().enumerate() {
894 for pl in &t.produces {
895 producer[*pl] = Some(ti);
896 }
897 }
898 let mut indeg = vec![0usize; n];
899 let mut succ: Vec<Vec<usize>> = vec![Vec::new(); n];
900 for (ti, t) in g.transitions.iter().enumerate() {
901 for pl in &t.consumes {
902 if g.places[*pl].marking > 0 {
903 continue;
904 }
905 match producer[*pl] {
906 Some(pr) => {
907 succ[pr].push(ti);
908 indeg[ti] += 1;
909 }
910 None => {
911 return Err(CertifyFault::Starved {
912 transition: ti,
913 place: g.places[*pl].kind,
914 })
915 }
916 }
917 }
918 }
919 let mut order = Vec::with_capacity(n);
921 let mut ready: std::collections::BinaryHeap<std::cmp::Reverse<usize>> = (0..n)
922 .filter(|i| indeg[*i] == 0)
923 .map(std::cmp::Reverse)
924 .collect();
925 while let Some(std::cmp::Reverse(t)) = ready.pop() {
926 order.push(t);
927 for s in &succ[t] {
928 indeg[*s] -= 1;
929 if indeg[*s] == 0 {
930 ready.push(std::cmp::Reverse(*s));
931 }
932 }
933 }
934 if order.len() != n {
935 return Err(CertifyFault::Deadlock {
936 cycle: token_free_cycle(g, &indeg, &succ, &producer),
937 });
938 }
939 Ok(LivenessWitness { topo_order: order })
940}
941
942fn verify_witness(g: &MarkedGraph, w: &LivenessWitness) -> bool {
943 let n = g.transitions.len();
944 if w.topo_order.len() != n {
945 return false;
946 }
947 let mut pos = vec![usize::MAX; n];
948 for (i, t) in w.topo_order.iter().enumerate() {
949 if *t >= n || pos[*t] != usize::MAX {
950 return false;
951 }
952 pos[*t] = i;
953 }
954 let mut producer = vec![None; g.places.len()];
955 for (ti, t) in g.transitions.iter().enumerate() {
956 for pl in &t.produces {
957 producer[*pl] = Some(ti);
958 }
959 }
960 for (ti, t) in g.transitions.iter().enumerate() {
961 for pl in &t.consumes {
962 if g.places[*pl].marking > 0 {
963 continue;
964 }
965 match producer[*pl] {
966 Some(pr) => {
967 if pos[pr] >= pos[ti] {
968 return false;
969 }
970 }
971 None => return false,
972 }
973 }
974 }
975 true
976}
977
978fn balances(p: &SpatialProgram, high_water: &BTreeMap<BufferId, usize>) -> Vec<BufferBalance> {
980 let sched = p.schedule.as_ref().unwrap();
981 let mut reserved = vec![0u64; p.buffers.len()];
982 let mut pushed = vec![0u64; p.buffers.len()];
983 let mut popped = vec![0u64; p.buffers.len()];
984 for prog in &sched.programs {
985 crate::spatial::ir::walk_tagged_ops(&prog.ops, 1, &mut |op, m| match op {
986 ChoreoOp::Reserve { buf, pages } => reserved[buf.idx()] += m * *pages as u64,
987 ChoreoOp::Push { buf, pages } => pushed[buf.idx()] += m * *pages as u64,
988 ChoreoOp::Pop { buf, pages } => popped[buf.idx()] += m * *pages as u64,
989 _ => {}
990 });
991 }
992 p.buffers
993 .iter()
994 .map(|b| BufferBalance {
995 buf: b.id,
996 depth_pages: b.kind.depth_pages(),
997 reserved: reserved[b.id.idx()],
998 pushed: pushed[b.id.idx()],
999 popped: popped[b.id.idx()],
1000 high_water: high_water.get(&b.id).copied().unwrap_or(0),
1001 })
1002 .collect()
1003}
1004
1005fn sem_counts(p: &SpatialProgram) -> Vec<(SemId, u64, u64)> {
1006 let sched = p.schedule.as_ref().unwrap();
1007 let mut incs = vec![0u64; p.semaphores.len()];
1008 let mut waits = vec![0u64; p.semaphores.len()];
1009 for prog in &sched.programs {
1010 crate::spatial::ir::walk_tagged_ops(&prog.ops, 1, &mut |op, m| match op {
1011 ChoreoOp::SemInc { sem, by, .. } => incs[sem.idx()] += m * *by as u64,
1012 ChoreoOp::McastSetValid { sem, .. } => incs[sem.idx()] += m,
1013 ChoreoOp::SemWait { sem, until } => {
1014 let n = match (p.semaphores[sem.idx()].role, until) {
1015 (SemRole::ValidFlag, _) => 1,
1016 (_, SemCond::Equals(n)) | (_, SemCond::AtLeast(n)) => *n as u64,
1017 };
1018 waits[sem.idx()] += m * n;
1019 }
1020 _ => {}
1021 });
1022 }
1023 p.semaphores
1024 .iter()
1025 .map(|s| (s.id, incs[s.id.idx()], waits[s.id.idx()]))
1026 .collect()
1027}
1028
1029pub fn cells_to_bounded(cells: &[LoopCell], result: Src, width: u32) -> Option<BoundedExpr> {
1038 cells_datapath(
1039 &mut BoundedBuild::new(width, &|k| format!("in{k}")),
1040 cells,
1041 result,
1042 )
1043}
1044
1045fn pe_certificate(bits: u32) -> ComputeStageCert {
1047 let width = bits.min(8);
1048 let pe = vec![
1049 FabricOp {
1050 op: AluOp::Mul,
1051 a: Src::In(0),
1052 b: Src::In(2),
1053 },
1054 FabricOp {
1055 op: AluOp::Add,
1056 a: Src::In(1),
1057 b: Src::Val(0),
1058 },
1059 ];
1060 let cells: Vec<LoopCell> = pe
1061 .iter()
1062 .map(|o| LoopCell::Arith {
1063 op: o.op,
1064 a: o.a,
1065 b: o.b,
1066 })
1067 .collect();
1068 let g = route_loop_body(&cells, 3, &[Src::Val(1)]);
1069 let placed = grid_output_to_bounded(&g, 0, width, &|k| format!("in{k}"));
1070 let spec = crate::codegen_sva::fabric_rtl::fabric_to_bounded(&pe, 1, width);
1071 match certify_mapping(placed, spec) {
1072 Some(c) => ComputeStageCert::Sat(c),
1073 None => ComputeStageCert::Uncertified {
1074 reason: "the PE obligation escaped the propositional fragment".into(),
1075 },
1076 }
1077}
1078
1079pub fn scalar_certificate(body: &ScalarBody) -> ComputeStageCert {
1082 scalar_certificate_within(body, GATE_BUDGET_NODES)
1083}
1084
1085pub fn scalar_certificate_within(body: &ScalarBody, gate_budget: u64) -> ComputeStageCert {
1088 let combinational = body
1089 .cells
1090 .iter()
1091 .all(|c| matches!(c, LoopCell::Arith { .. } | LoopCell::Select { .. }));
1092 if !combinational {
1093 return ComputeStageCert::Uncertified {
1094 reason: "a functional-unit or memory cell has no bit-blast lowering".into(),
1095 };
1096 }
1097 let Src::Val(_) = body.result else {
1098 return ComputeStageCert::Uncertified {
1099 reason: "the body's result is not a cell value".into(),
1100 };
1101 };
1102 if let Some((cell, demand)) = unroutable_cell(&body.cells) {
1108 return ComputeStageCert::Uncertified { reason: format!(
1109 "cell {cell} needs {demand} distinct computed values routed to one tile, and a tile has \
1110 {TILE_OPERAND_CHANNELS} operand channels (north and west) — a predicated select that \
1111 compares a computed value AND selects between two more has no placed form"
1112 ) };
1113 }
1114 let width = 8;
1115 let Some(spec) = cells_to_gates(&body.cells, body.result, width) else {
1125 return ComputeStageCert::Uncertified {
1126 reason: "the body escaped the bounded fragment".into(),
1127 };
1128 };
1129 let size = word_size(&spec);
1130 if size.dag_nodes > gate_budget {
1131 let refusal = BlastRefusal::GateBudget {
1132 dag_nodes: size.dag_nodes,
1133 budget: gate_budget,
1134 };
1135 return ComputeStageCert::Uncertified {
1136 reason: refusal.reason(),
1137 };
1138 }
1139 let g = route_loop_body(&body.cells, body.n_in, &[body.result]);
1140 let Some(placed) = grid_output_to_gates(&g, 0, width, &|k| format!("in{k}")) else {
1141 return ComputeStageCert::Uncertified {
1142 reason: "the placed grid escaped the bounded fragment".into(),
1143 };
1144 };
1145 match certify_gates_within(&placed, &spec, gate_budget) {
1146 Ok(c) => ComputeStageCert::Sat(c),
1147 Err(r) => ComputeStageCert::Uncertified { reason: r.reason() },
1148 }
1149}
1150
1151pub const SCALAR_DATAPATH_BUDGET_NODES: u128 = 50_000;
1161
1162pub fn bounded_datapath_nodes(body: &ScalarBody) -> u128 {
1168 fn src(s: &Src, vals: &[u128]) -> u128 {
1169 match s {
1170 Src::In(_) | Src::Const(_) => 1,
1171 Src::Val(k) => vals.get(*k).copied().unwrap_or(1),
1172 }
1173 }
1174 let mut vals: Vec<u128> = Vec::with_capacity(body.cells.len());
1175 for c in &body.cells {
1176 let v = match c {
1177 LoopCell::Arith { a, b, .. } => src(a, &vals)
1178 .saturating_add(src(b, &vals))
1179 .saturating_add(1),
1180 LoopCell::Select { ca, cb, t, e, .. } => src(ca, &vals)
1181 .saturating_add(src(cb, &vals))
1182 .saturating_add(1)
1183 .saturating_add(src(t, &vals))
1184 .saturating_add(src(e, &vals))
1185 .saturating_add(1),
1186 _ => 1,
1189 };
1190 vals.push(v);
1191 }
1192 src(&body.result, &vals)
1193}
1194
1195fn has_compute_certificate(s: &Stage) -> bool {
1197 matches!(
1198 s.kind,
1199 StageKind::Compute(ComputeOp::MatmulTiles { .. }) | StageKind::Scalar(_)
1200 )
1201}
1202
1203fn compute_certificates(p: &SpatialProgram) -> Vec<(StageId, ComputeStageCert)> {
1204 let mut by_origin: BTreeMap<String, ComputeStageCert> = BTreeMap::new();
1205 let mut out = Vec::new();
1206 for s in p.stages.iter().filter(|s| has_compute_certificate(s)) {
1207 let key_cert: Option<(String, Box<dyn Fn() -> ComputeStageCert>)> = match &s.kind {
1208 StageKind::Compute(ComputeOp::MatmulTiles { operand, .. }) => {
1209 let operand = *operand;
1210 Some((
1211 format!("mm:{operand:?}"),
1212 Box::new(move || {
1213 if precision_is_int(operand) {
1214 pe_certificate(
1215 crate::spatial::target::precision_int_bits(operand).unwrap_or(64),
1216 )
1217 } else {
1218 ComputeStageCert::Uncertified { reason: format!("float tensor datapath ({operand:?} operands, fp32 accumulate) cannot be bit-blasted; equivalence is by evaluation") }
1219 }
1220 }),
1221 ))
1222 }
1223 StageKind::Scalar(body) => {
1224 let body = body.clone();
1225 Some((
1226 format!(
1227 "scalar:{}",
1228 serde_json::to_string(&body).unwrap_or_default()
1229 ),
1230 Box::new(move || scalar_certificate(&body)),
1231 ))
1232 }
1233 _ => None,
1234 };
1235 if let Some((key, make)) = key_cert {
1236 let cert = by_origin.entry(key).or_insert_with(|| make()).clone();
1237 out.push((s.id, cert));
1238 }
1239 }
1240 out
1241}
1242
1243pub fn dry_inputs(p: &SpatialProgram) -> Inputs {
1245 let mut inputs = Inputs::default();
1246 for t in &p.tensors {
1247 if t.role == TensorRole::Output {
1248 continue;
1249 }
1250 let n = t.elems.rows * t.elems.cols;
1251 let vals: Vec<i64> = (0..n).map(|i| t.init.element(i).unwrap_or(0)).collect();
1252 inputs.tensors.insert(t.id, Value::Int(vals));
1253 }
1254 inputs
1255}
1256
1257struct OrderIndex<'a> {
1263 p: &'a SpatialProgram,
1264 sec_of: Vec<usize>,
1266 loads: BTreeMap<TensorId, Vec<StageId>>,
1267 stores: BTreeMap<TensorId, Vec<StageId>>,
1268 succ: Vec<Vec<StageId>>,
1270 barriers_before: Vec<usize>,
1272}
1273
1274impl<'a> OrderIndex<'a> {
1275 fn new(p: &'a SpatialProgram) -> Self {
1276 let mut sec_of = vec![0usize; p.stages.len()];
1277 for (k, sp) in p.sections.iter().enumerate() {
1278 for slot in sec_of
1279 .iter_mut()
1280 .take(sp.stages.1.min(p.stages.len()))
1281 .skip(sp.stages.0)
1282 {
1283 *slot = k;
1284 }
1285 }
1286 let (mut loads, mut stores): (
1287 BTreeMap<TensorId, Vec<StageId>>,
1288 BTreeMap<TensorId, Vec<StageId>>,
1289 ) = (BTreeMap::new(), BTreeMap::new());
1290 for s in &p.stages {
1291 match &s.kind {
1292 StageKind::Load { tensor, .. } => loads.entry(*tensor).or_default().push(s.id),
1293 StageKind::Store { tensor, .. } => stores.entry(*tensor).or_default().push(s.id),
1294 _ => {}
1295 }
1296 }
1297 let mut succ = vec![Vec::new(); p.stages.len()];
1298 for e in &p.edges {
1299 if let Some(out) = succ.get_mut(e.from.idx()) {
1300 out.extend(e.to.iter().copied());
1301 }
1302 }
1303 let mut barriers_before = Vec::with_capacity(p.sections.len() + 1);
1304 barriers_before.push(0);
1305 for sp in &p.sections {
1306 barriers_before.push(
1307 barriers_before.last().copied().unwrap_or(0)
1308 + usize::from(sp.barrier_after.is_some()),
1309 );
1310 }
1311 OrderIndex {
1312 p,
1313 sec_of,
1314 loads,
1315 stores,
1316 succ,
1317 barriers_before,
1318 }
1319 }
1320
1321 fn touch_sections(&self, t: TensorId) -> (Vec<usize>, Vec<usize>) {
1323 let secs = |stages: Option<&Vec<StageId>>| -> Vec<usize> {
1324 let mut v: Vec<usize> = stages
1325 .map(|s| s.iter().map(|id| self.sec_of[id.idx()]).collect())
1326 .unwrap_or_default();
1327 v.sort_unstable();
1328 v.dedup();
1329 v
1330 };
1331 (secs(self.loads.get(&t)), secs(self.stores.get(&t)))
1332 }
1333
1334 fn barriers_between(&self, from: usize, to: usize) -> bool {
1340 let at = |k: usize| {
1341 self.barriers_before
1342 .get(k.min(self.barriers_before.len() - 1))
1343 .copied()
1344 .unwrap_or(0)
1345 };
1346 from < to && at(to) > at(from)
1347 }
1348
1349 fn reaches(&self, from: StageId, to: StageId) -> bool {
1359 let mut seen = vec![false; self.p.stages.len()];
1360 let mut queue = vec![from];
1361 seen[from.idx()] = true;
1362 while let Some(s) = queue.pop() {
1363 if s == to {
1364 return true;
1365 }
1366 for t in &self.succ[s.idx()] {
1367 if !seen[t.idx()] {
1368 seen[t.idx()] = true;
1369 queue.push(*t);
1370 }
1371 }
1372 }
1373 false
1374 }
1375
1376 fn read_before_write_in_section(&self, t: TensorId, section: usize) -> bool {
1392 let in_section = |ids: Option<&Vec<StageId>>| -> Vec<&Stage> {
1393 ids.map(|v| {
1394 v.iter()
1395 .filter(|id| self.sec_of[id.idx()] == section)
1396 .map(|id| self.p.stage(*id))
1397 .collect()
1398 })
1399 .unwrap_or_default()
1400 };
1401 let (loads, stores) = (
1402 in_section(self.loads.get(&t)),
1403 in_section(self.stores.get(&t)),
1404 );
1405 if loads.is_empty() || stores.is_empty() {
1406 return false;
1407 }
1408 stores.iter().all(|w| {
1410 loads
1411 .iter()
1412 .filter(|l| w.site.is_none() || l.site == w.site)
1413 .any(|l| self.reaches(l.id, w.id))
1414 })
1415 }
1416}
1417
1418fn ordering_obligations(p: &SpatialProgram) -> Result<(), CertifyFault> {
1424 let index = std::cell::OnceCell::new();
1425 let ix = || index.get_or_init(|| OrderIndex::new(p));
1426 for t in &p.tensors {
1427 match t.role {
1428 TensorRole::Activation => {
1433 let (readers, writers) = ix().touch_sections(t.id);
1434 if writers.is_empty() {
1435 return Err(CertifyFault::ActivationOrder {
1436 tensor: t.id,
1437 writer: None,
1438 reader: 0,
1439 reason: "no section writes it",
1440 });
1441 }
1442 for &w in &writers {
1443 for &r in &readers {
1444 if r <= w {
1445 return Err(CertifyFault::ActivationOrder { tensor: t.id, writer: Some(w), reader: r, reason: "a section reads the activation at or before a section that writes it" });
1446 }
1447 if !ix().barriers_between(w, r) {
1448 return Err(CertifyFault::ActivationOrder { tensor: t.id, writer: Some(w), reader: r, reason: "no barrier chain separates a writing section from the reading one" });
1449 }
1450 }
1451 }
1452 }
1453 TensorRole::State => {
1461 let (readers, writers) = ix().touch_sections(t.id);
1462 if writers.is_empty() {
1463 return Err(CertifyFault::StateOrder {
1464 tensor: t.id,
1465 reader: readers.first().copied(),
1466 writer: None,
1467 reason: "a carried tensor the step never writes is not carried",
1468 });
1469 }
1470 for &w in &writers {
1471 for &r in &readers {
1472 if r == w {
1473 if !ix().read_before_write_in_section(t.id, r) {
1474 return Err(CertifyFault::StateOrder {
1475 tensor: t.id,
1476 reader: Some(r),
1477 writer: Some(w),
1478 reason: "one section reads and writes the carried state with nothing ordering the two — the write must be downstream of the read in the section's own dataflow, or the halves must be separate sections",
1479 });
1480 }
1481 continue;
1482 }
1483 let (a, b) = if r < w { (r, w) } else { (w, r) };
1484 if !ix().barriers_between(a, b) {
1485 return Err(CertifyFault::StateOrder { tensor: t.id, reader: Some(r), writer: Some(w), reason: "no barrier chain separates the reading section from the writing one" });
1486 }
1487 }
1488 for &other in writers.iter().filter(|o| **o > w) {
1489 if !ix().barriers_between(w, other) {
1490 return Err(CertifyFault::StateOrder { tensor: t.id, reader: None, writer: Some(other), reason: "no barrier chain separates two sections that write the same carried state" });
1491 }
1492 }
1493 }
1494 }
1495 _ => {}
1496 }
1497 }
1498 shard_obligation(p)?;
1499 Ok(())
1500}
1501
1502fn shard_obligation(p: &SpatialProgram) -> Result<(), CertifyFault> {
1519 use std::collections::BTreeSet;
1520 let chips: BTreeSet<u16> = p
1521 .stages
1522 .iter()
1523 .filter_map(|s| s.site.map(|x| x.chip))
1524 .collect();
1525 if chips.len() <= 1 {
1526 return Ok(());
1527 }
1528 let complete = |s: &Stage| -> bool {
1532 let StageKind::AllReduce { combine, .. } = &s.kind else {
1533 return false;
1534 };
1535 let folds_all = match combine.as_ref() {
1536 ComputeOp::Generic { body, .. } => body.max_operand() == s.ins.len().checked_sub(1),
1537 _ => false,
1538 };
1539 let from: BTreeSet<u16> = s
1540 .ins
1541 .iter()
1542 .filter_map(|b| p.buffer(*b).site.map(|x| x.chip))
1543 .collect();
1544 folds_all && from == chips
1545 };
1546 let chip_local_tensor = |t: TensorId| p.tensor(t).chip.is_some();
1547 let mut local: BTreeSet<BufferId> = BTreeSet::new();
1548 loop {
1549 let before = local.len();
1550 for s in &p.stages {
1551 let derived = match &s.kind {
1552 StageKind::Load { tensor, .. } => {
1553 chip_local_tensor(*tensor) || s.ins.iter().any(|b| local.contains(b))
1554 }
1555 StageKind::Recv { .. } => true,
1556 StageKind::AllReduce { .. } => !complete(s),
1557 StageKind::Store { .. } => false,
1558 _ => s.ins.iter().any(|b| local.contains(b)),
1559 };
1560 if derived {
1561 local.extend(s.outs.iter().copied());
1562 }
1563 }
1564 if local.len() == before {
1565 break;
1566 }
1567 }
1568 for s in &p.stages {
1569 let StageKind::Store { tensor, .. } = &s.kind else {
1570 continue;
1571 };
1572 if !chip_local_tensor(*tensor) && s.ins.first().is_some_and(|b| local.contains(b)) {
1573 return Err(CertifyFault::UnreducedShard {
1574 tensor: *tensor,
1575 chips: chips.len(),
1576 });
1577 }
1578 }
1579 Ok(())
1580}
1581
1582fn graph_digest(g: &MarkedGraph) -> u64 {
1586 fnv_json(g)
1587}
1588
1589fn op_resources(op: &ChoreoOp, out: &mut Vec<(u8, usize)>) {
1592 const BUF: u8 = 0;
1593 const SEM: u8 = 1;
1594 const GRP: u8 = 2;
1595 match op {
1596 ChoreoOp::Reserve { buf, .. }
1597 | ChoreoOp::Push { buf, .. }
1598 | ChoreoOp::Wait { buf, .. }
1599 | ChoreoOp::Pop { buf, .. } => out.push((BUF, buf.idx())),
1600 ChoreoOp::ReadTiles { into, .. } => out.push((BUF, into.idx())),
1601 ChoreoOp::WriteTiles { from, .. } => out.push((BUF, from.idx())),
1602 ChoreoOp::McastSend { buf, group, .. } => {
1603 out.push((BUF, buf.idx()));
1604 out.push((GRP, group.idx()));
1605 }
1606 ChoreoOp::McastSetValid { sem, group } => {
1607 out.push((SEM, sem.idx()));
1608 out.push((GRP, group.idx()));
1609 }
1610 ChoreoOp::SemSet { sem, .. }
1611 | ChoreoOp::SemInc { sem, .. }
1612 | ChoreoOp::SemWait { sem, .. } => out.push((SEM, sem.idx())),
1613 ChoreoOp::Compute {
1614 ins, acc_reload, ..
1615 } => {
1616 for b in ins {
1617 out.push((BUF, b.idx()));
1618 }
1619 if let Some(b) = acc_reload {
1620 out.push((BUF, b.idx()));
1621 }
1622 }
1623 ChoreoOp::PackAcc { to, .. } => out.push((BUF, to.idx())),
1624 ChoreoOp::Barrier { group } => out.push((GRP, group.idx())),
1625 _ => {}
1626 }
1627}
1628
1629pub fn decomposes_by_section(p: &SpatialProgram) -> bool {
1634 section_ranges(p).is_some()
1635}
1636
1637fn section_ranges(p: &SpatialProgram) -> Option<Vec<Selection>> {
1652 macro_rules! refuse {
1653 ($($a:tt)*) => {{
1654 if std::env::var_os("LOGOS_SECTION_DEBUG").is_some() {
1655 eprintln!("section_ranges refuses: {}", format!($($a)*));
1656 }
1657 return None;
1658 }};
1659 }
1660 let sched = p.schedule.as_ref()?;
1661 if p.sections.len() < 2 {
1662 refuse!("only {} section(s)", p.sections.len());
1663 }
1664 let mut sec_of_stage = vec![usize::MAX; p.stages.len()];
1665 for (si, sp) in p.sections.iter().enumerate() {
1666 let Some(slots) = sec_of_stage.get_mut(sp.stages.0..sp.stages.1.min(p.stages.len())) else {
1667 refuse!(
1668 "section {si} span {:?} is out of range of {} stages",
1669 sp.stages,
1670 p.stages.len()
1671 );
1672 };
1673 for slot in slots {
1674 if *slot != usize::MAX {
1675 refuse!("section {si} overlaps section {} on a stage", *slot);
1676 }
1677 *slot = si;
1678 }
1679 }
1680 let mut ranges: Vec<Selection> = vec![Vec::new(); p.sections.len()];
1681 let mut owner: BTreeMap<(u8, usize), usize> = BTreeMap::new();
1682 let mut res: Vec<(u8, usize)> = Vec::new();
1683 for (pi, prog) in sched.programs.iter().enumerate() {
1684 let mut cur: Option<(usize, usize)> = None;
1685 for (oi, t) in prog.ops.iter().enumerate() {
1686 let mut sec: Option<usize> = None;
1687 let mut straddles = false;
1688 res.clear();
1689 walk_tagged(std::slice::from_ref(t), 1, &mut |tt, _| {
1690 op_resources(&tt.op, &mut res);
1691 match sec_of_stage.get(tt.stage.idx()).copied() {
1692 None | Some(usize::MAX) => straddles = true,
1693 Some(x) => match sec {
1694 None => sec = Some(x),
1695 Some(a) if a != x => straddles = true,
1696 Some(_) => {}
1697 },
1698 }
1699 });
1700 let Some(sec) = sec else {
1701 refuse!("program {pi} op {oi} tags no stage")
1702 };
1703 if straddles {
1704 refuse!("program {pi} op {oi} straddles sections (or names a sectionless stage)");
1705 }
1706 for r in &res {
1707 match owner.entry(*r) {
1708 std::collections::btree_map::Entry::Vacant(v) => {
1709 v.insert(sec);
1710 }
1711 std::collections::btree_map::Entry::Occupied(o) => {
1712 if *o.get() != sec {
1713 refuse!("program {pi} op {oi}: resource {:?} is touched from sections {} and {sec}", r, *o.get());
1714 }
1715 }
1716 }
1717 }
1718 match cur {
1719 Some((s, _)) if s == sec => {}
1720 Some((s, st)) => {
1721 if sec < s {
1722 refuse!(
1723 "program {pi} op {oi} (stage {}): section {sec} follows section {s}",
1724 t.stage.idx()
1725 );
1726 }
1727 ranges[s].push((pi, st..oi));
1728 cur = Some((sec, oi));
1729 }
1730 None => cur = Some((sec, oi)),
1731 }
1732 }
1733 if let Some((s, st)) = cur {
1734 ranges[s].push((pi, st..prog.ops.len()));
1735 }
1736 }
1737 for st in &p.stages {
1738 if let (StageKind::Recv { group }, Some(o)) = (&st.kind, st.outs.first()) {
1739 match (owner.get(&(0, o.idx())), owner.get(&(2, group.idx()))) {
1740 (Some(a), Some(b)) if a != b => {
1741 refuse!("recv buffer in section {a} fed by a group in section {b}")
1742 }
1743 _ => {}
1744 }
1745 }
1746 }
1747 Some(ranges)
1748}
1749
1750pub fn certify_choreography(p: &SpatialProgram) -> Result<ChoreographyCertificate, CertifyFault> {
1751 if p.schedule.is_none() {
1752 return Err(CertifyFault::NotScheduled);
1753 }
1754 p.validate().map_err(CertifyFault::Invalid)?;
1755 ordering_obligations(p)?;
1756 let proof = build_proof(p)?;
1757 let trace = eval_choreography_dry(p).map_err(CertifyFault::Eval)?;
1758 let balances = balances(p, &trace.high_water);
1759 for bl in &balances {
1760 if bl.high_water > bl.depth_pages || bl.reserved != bl.pushed || bl.pushed != bl.popped {
1761 return Err(CertifyFault::Bounds { buffer: bl.buf });
1762 }
1763 }
1764 Ok(ChoreographyCertificate {
1765 schedule_digest: schedule_digest(p),
1766 proof,
1767 balances,
1768 sem_counts: sem_counts(p),
1769 compute: compute_certificates(p),
1770 numerics: p.claim.clone(),
1771 })
1772}
1773
1774pub fn trip_window(p: &SpatialProgram, trip: TripId) -> std::ops::RangeInclusive<usize> {
1791 window_of(p.trip(trip), trip_spreads(p)[trip.idx()])
1792}
1793
1794fn trip_spreads(p: &SpatialProgram) -> Vec<usize> {
1799 let mut out = vec![0usize; p.trips.len()];
1800 let Some(sched) = p.schedule.as_ref() else {
1801 return out;
1802 };
1803 let n = p.buffers.len();
1804 let (mut producer, mut consumer) = (vec![None; n], vec![None; n]);
1805 let mut outside = vec![0usize; n];
1806 let mut moved_in: Vec<BTreeSet<usize>> = vec![BTreeSet::new(); n];
1807 struct Walk<'a> {
1808 prog: usize,
1809 producer: &'a mut [Option<usize>],
1810 consumer: &'a mut [Option<usize>],
1811 moved_in: &'a mut [BTreeSet<usize>],
1812 outside: &'a mut [usize],
1813 }
1814 fn walk(w: &mut Walk, ops: &[TaggedOp], trip: Option<usize>, mult: usize) {
1815 for t in ops {
1816 let (buf, pages, produces) = match &t.op {
1817 ChoreoOp::Loop { var, count, body } => {
1818 match count.trip() {
1819 Some(id) => walk(w, body, Some(id.idx()), mult),
1820 None if *var == crate::spatial::passes::LV_ITEM => {
1821 walk(w, body, trip, mult)
1822 }
1823 None => walk(w, body, trip, mult * count.bound()),
1824 }
1825 continue;
1826 }
1827 ChoreoOp::Push { buf, pages } => (*buf, *pages, true),
1828 ChoreoOp::Pop { buf, pages } => (*buf, *pages, false),
1829 _ => continue,
1830 };
1831 let i = buf.idx();
1832 if produces {
1833 w.producer[i] = Some(w.prog);
1834 } else {
1835 w.consumer[i] = Some(w.prog);
1836 }
1837 match trip {
1838 Some(id) => {
1839 w.moved_in[i].insert(id);
1840 }
1841 None => w.outside[i] += mult * pages,
1842 }
1843 }
1844 }
1845 for (prog, q) in sched.programs.iter().enumerate() {
1846 let mut w = Walk {
1847 prog,
1848 producer: &mut producer,
1849 consumer: &mut consumer,
1850 moved_in: &mut moved_in,
1851 outside: &mut outside,
1852 };
1853 walk(&mut w, &q.ops, None, 1);
1854 }
1855 let mut coupling: Vec<Vec<(usize, usize, usize)>> = vec![Vec::new(); p.trips.len()];
1857 for i in 0..n {
1858 let (Some(a), Some(b)) = (producer[i], consumer[i]) else {
1859 continue;
1860 };
1861 if a == b {
1862 continue;
1863 }
1864 for &trip in &moved_in[i] {
1865 coupling[trip].push((a, b, p.buffers[i].kind.depth_pages() + outside[i] + 1));
1866 }
1867 }
1868 fn root(parent: &mut rustc_hash::FxHashMap<usize, usize>, mut x: usize) -> usize {
1869 while let Some(&up) = parent.get(&x).filter(|up| **up != x) {
1870 let grand = parent.get(&up).copied().unwrap_or(up);
1871 parent.insert(x, grand);
1872 x = up;
1873 }
1874 x
1875 }
1876 for (trip, edges) in coupling.iter().enumerate() {
1877 let mut parent: rustc_hash::FxHashMap<usize, usize> = rustc_hash::FxHashMap::default();
1878 for (a, b, _) in edges {
1879 let (ra, rb) = (root(&mut parent, *a), root(&mut parent, *b));
1880 if ra != rb {
1881 parent.insert(ra, rb);
1882 }
1883 }
1884 let mut per_set: rustc_hash::FxHashMap<usize, usize> = rustc_hash::FxHashMap::default();
1885 for (a, _, lead) in edges {
1886 *per_set.entry(root(&mut parent, *a)).or_default() += lead;
1887 }
1888 out[trip] = per_set.values().copied().max().unwrap_or(0);
1889 }
1890 out
1891}
1892
1893fn window_of(t: &Trip, spread: usize) -> std::ops::RangeInclusive<usize> {
1895 t.lo()..=t.hi().min(t.lo().max(spread + 1))
1896}
1897
1898fn part_trips(p: &SpatialProgram, sel: Option<&[(usize, std::ops::Range<usize>)]>) -> Vec<TripId> {
1900 let Some(sched) = p.schedule.as_ref() else {
1901 return Vec::new();
1902 };
1903 let mut found = std::collections::BTreeSet::new();
1904 let mut visit = |ops: &[TaggedOp]| {
1905 walk_tagged(ops, 1, &mut |t, _| {
1906 if let ChoreoOp::Loop { count, .. } = &t.op {
1907 found.extend(count.trip());
1908 }
1909 })
1910 };
1911 match sel {
1912 Some(r) => r
1913 .iter()
1914 .for_each(|(pi, ops)| visit(&sched.programs[*pi].ops[ops.clone()])),
1915 None => sched.programs.iter().for_each(|q| visit(&q.ops)),
1916 }
1917 found.into_iter().collect()
1918}
1919
1920const TRIP_CASES_LIMIT: usize = 4096;
1922
1923fn part_cases(
1926 p: &SpatialProgram,
1927 cx: &GraphContext,
1928 sel: Option<&[(usize, std::ops::Range<usize>)]>,
1929) -> Result<Vec<(Vec<usize>, Vec<usize>)>, CertifyFault> {
1930 let trips = part_trips(p, sel);
1931 if trips.is_empty() {
1932 return Ok(Vec::new());
1933 }
1934 let windows: Vec<Vec<usize>> = trips
1935 .iter()
1936 .map(|t| window_of(p.trip(*t), cx.spreads[t.idx()]).collect())
1937 .collect();
1938 let cases = windows
1939 .iter()
1940 .try_fold(1usize, |n, w| n.checked_mul(w.len()))
1941 .filter(|n| *n <= TRIP_CASES_LIMIT);
1942 let Some(cases) = cases else {
1943 return Err(CertifyFault::TripCasesExceed {
1944 limit: TRIP_CASES_LIMIT,
1945 });
1946 };
1947 let bound = p.bound_counts();
1948 Ok((0..cases)
1949 .map(|mut k| {
1950 let mut full = bound.clone();
1951 let mut own = vec![0; trips.len()];
1952 for (i, w) in windows.iter().enumerate().rev() {
1953 own[i] = w[k % w.len()];
1954 k /= w.len();
1955 full[trips[i].idx()] = own[i];
1956 }
1957 (own, full)
1958 })
1959 .collect())
1960}
1961
1962fn prove_cases(
1964 p: &SpatialProgram,
1965 cx: &GraphContext,
1966 sel: Option<&[(usize, std::ops::Range<usize>)]>,
1967 witnesses: &mut Vec<LivenessWitness>,
1968 index_of: &mut BTreeMap<Vec<usize>, usize>,
1969) -> Result<Vec<TripCase>, CertifyFault> {
1970 let mut out = Vec::new();
1971 for (counts, full) in part_cases(p, cx, sel)? {
1972 let g = build_graph_over(p, cx, sel, &full)?;
1973 let liveness = liveness_witness(&g)?;
1974 let witness = *index_of
1975 .entry(liveness.topo_order.clone())
1976 .or_insert_with(|| {
1977 witnesses.push(liveness);
1978 witnesses.len() - 1
1979 });
1980 out.push(TripCase {
1981 counts,
1982 places: g.places.len(),
1983 transitions: g.transitions.len(),
1984 digest: graph_digest(&g),
1985 witness,
1986 });
1987 }
1988 Ok(out)
1989}
1990
1991fn recheck_cases(
1994 p: &SpatialProgram,
1995 cx: &GraphContext,
1996 sel: Option<&[(usize, std::ops::Range<usize>)]>,
1997 cases: &[TripCase],
1998 witnesses: &[LivenessWitness],
1999) -> bool {
2000 let Ok(want) = part_cases(p, cx, sel) else {
2001 return false;
2002 };
2003 if want.len() != cases.len() {
2004 return false;
2005 }
2006 want.iter().zip(cases).all(|((counts, full), case)| {
2007 if *counts != case.counts {
2008 return false;
2009 }
2010 let Ok(g) = build_graph_over(p, cx, sel, full) else {
2011 return false;
2012 };
2013 let Some(w) = witnesses.get(case.witness) else {
2014 return false;
2015 };
2016 g.places.len() == case.places
2017 && g.transitions.len() == case.transitions
2018 && graph_digest(&g) == case.digest
2019 && verify_witness(&g, w)
2020 })
2021}
2022
2023fn build_proof(p: &SpatialProgram) -> Result<GraphProof, CertifyFault> {
2027 let cx = GraphContext::of(p);
2028 let bound = p.bound_counts();
2029 let Some(ranges) = section_ranges(p) else {
2030 let graph = build_graph_over(p, &cx, None, &bound)?;
2031 let liveness = liveness_witness(&graph)?;
2032 let (mut case_witnesses, mut index_of) = (Vec::new(), BTreeMap::new());
2033 let cases = prove_cases(p, &cx, None, &mut case_witnesses, &mut index_of)?;
2034 return Ok(GraphProof::Whole {
2035 graph,
2036 liveness,
2037 cases,
2038 case_witnesses,
2039 });
2040 };
2041 let mut sections = Vec::with_capacity(ranges.len());
2042 let mut witnesses: Vec<LivenessWitness> = Vec::new();
2043 let mut index_of: BTreeMap<Vec<usize>, usize> = BTreeMap::new();
2044 let (mut places, mut transitions) = (0usize, 0usize);
2045 for (si, r) in ranges.iter().enumerate() {
2046 let g = build_graph_over(p, &cx, Some(r), &bound)?;
2047 let liveness = liveness_witness(&g)?;
2048 places += g.places.len();
2049 transitions += g.transitions.len();
2050 let witness = *index_of
2051 .entry(liveness.topo_order.clone())
2052 .or_insert_with(|| {
2053 witnesses.push(liveness);
2054 witnesses.len() - 1
2055 });
2056 let (digest, section_places, section_transitions) =
2057 (graph_digest(&g), g.places.len(), g.transitions.len());
2058 drop(g);
2059 let cases = prove_cases(p, &cx, Some(r), &mut witnesses, &mut index_of)?;
2060 sections.push(SectionProof {
2061 section: si,
2062 places: section_places,
2063 transitions: section_transitions,
2064 digest,
2065 witness,
2066 cases,
2067 });
2068 }
2069 Ok(GraphProof::PerSection {
2070 sections,
2071 witnesses,
2072 places,
2073 transitions,
2074 })
2075}
2076
2077impl ChoreographyCertificate {
2078 pub fn recheck(&self, p: &SpatialProgram) -> bool {
2082 if p.schedule.is_none() || self.schedule_digest != schedule_digest(p) {
2083 return false;
2084 }
2085 if p.validate().is_err() || ordering_obligations(p).is_err() {
2086 return false;
2087 }
2088 if !self.recheck_proof(p) {
2089 return false;
2090 }
2091 let hw: BTreeMap<BufferId, usize> = self
2092 .balances
2093 .iter()
2094 .map(|b| (b.buf, b.high_water))
2095 .collect();
2096 if balances(p, &hw) != self.balances {
2097 return false;
2098 }
2099 if self
2100 .balances
2101 .iter()
2102 .any(|b| b.high_water > b.depth_pages || b.reserved != b.pushed || b.pushed != b.popped)
2103 {
2104 return false;
2105 }
2106 if sem_counts(p) != self.sem_counts {
2107 return false;
2108 }
2109 if self.numerics != p.claim
2113 || self.compute.iter().map(|(s, _)| *s).ne(p
2114 .stages
2115 .iter()
2116 .filter(|s| has_compute_certificate(s))
2117 .map(|s| s.id))
2118 {
2119 return false;
2120 }
2121 for (_, c) in &self.compute {
2122 if let ComputeStageCert::Sat(pc) = c {
2123 if !pc.recheck() {
2124 return false;
2125 }
2126 }
2127 }
2128 true
2129 }
2130
2131 fn recheck_proof(&self, p: &SpatialProgram) -> bool {
2138 match &self.proof {
2139 GraphProof::Whole {
2140 graph,
2141 liveness,
2142 cases,
2143 case_witnesses,
2144 } => {
2145 let Ok(rebuilt) = build_marked_graph(p) else {
2146 return false;
2147 };
2148 rebuilt == *graph
2149 && verify_witness(graph, liveness)
2150 && recheck_cases(p, &GraphContext::of(p), None, cases, case_witnesses)
2151 }
2152 GraphProof::PerSection {
2153 sections,
2154 witnesses,
2155 places,
2156 transitions,
2157 } => {
2158 let Some(ranges) = section_ranges(p) else {
2159 return false;
2160 };
2161 if ranges.len() != sections.len() {
2162 return false;
2163 }
2164 let cx = GraphContext::of(p);
2165 let (mut tp, mut tt) = (0usize, 0usize);
2166 for (si, sp) in sections.iter().enumerate() {
2167 if sp.section != si {
2168 return false;
2169 }
2170 let Ok(g) = build_graph_over(p, &cx, Some(&ranges[si]), &p.bound_counts())
2171 else {
2172 return false;
2173 };
2174 if g.places.len() != sp.places || g.transitions.len() != sp.transitions {
2175 return false;
2176 }
2177 let Some(w) = witnesses.get(sp.witness) else {
2178 return false;
2179 };
2180 if graph_digest(&g) != sp.digest || !verify_witness(&g, w) {
2181 return false;
2182 }
2183 if !recheck_cases(p, &cx, Some(&ranges[si]), &sp.cases, witnesses) {
2184 return false;
2185 }
2186 tp += sp.places;
2187 tt += sp.transitions;
2188 }
2189 tp == *places && tt == *transitions
2190 }
2191 }
2192 }
2193
2194 #[must_use]
2202 pub fn with_witness_tampered(&self) -> Self {
2203 let mut c = self.clone();
2204 match &mut c.proof {
2205 GraphProof::Whole {
2206 graph, liveness, ..
2207 } => {
2208 let n = liveness.topo_order.len();
2209 if n >= 2 {
2210 let g: &MarkedGraph = graph;
2211 let mut swapped = false;
2212 for t in 0..n {
2213 for pl in &g.transitions[t].consumes {
2214 if g.places[*pl].marking == 0 {
2215 if let Some(pr) =
2216 g.transitions.iter().position(|x| x.produces.contains(pl))
2217 {
2218 let (ip, ic) = (
2219 liveness.topo_order.iter().position(|x| *x == pr).unwrap(),
2220 liveness.topo_order.iter().position(|x| *x == t).unwrap(),
2221 );
2222 liveness.topo_order.swap(ip, ic);
2223 swapped = true;
2224 break;
2225 }
2226 }
2227 }
2228 if swapped {
2229 break;
2230 }
2231 }
2232 if !swapped {
2233 liveness.topo_order.swap(0, 1);
2234 }
2235 }
2236 }
2237 GraphProof::PerSection { witnesses, .. } => {
2238 if let Some(w) = witnesses.iter_mut().find(|w| w.topo_order.len() >= 2) {
2239 w.topo_order.reverse();
2240 }
2241 }
2242 }
2243 c
2244 }
2245}
2246
2247pub fn certify_external_spatial(
2251 external: &SpatialProgram,
2252 source: &SpatialProgram,
2253) -> Option<ChoreographyCertificate> {
2254 if external.tensors != source.tensors
2255 || external.work.k_tiles != source.work.k_tiles
2256 || external.numerics != source.numerics
2257 {
2258 return None;
2259 }
2260 let kinds = |p: &SpatialProgram| -> Vec<String> {
2261 let mut v: Vec<String> = p
2262 .stages
2263 .iter()
2264 .map(|s| match &s.kind {
2265 StageKind::Load { tensor, .. } | StageKind::Store { tensor, .. } => {
2266 format!("{:?}:{}", std::mem::discriminant(&s.kind), tensor.idx())
2267 }
2268 StageKind::Recv { .. } => "load".into(),
2269 StageKind::AllReduce { pool, tensor, .. } => {
2270 format!("allreduce:{pool:?}:{}", tensor.idx())
2271 }
2272 StageKind::Compute(op) => format!("{:?}", std::mem::discriminant(op)),
2273 StageKind::Scalar(b) => {
2274 format!("scalar:{}", serde_json::to_string(b).unwrap_or_default())
2275 }
2276 })
2277 .collect();
2278 v.sort();
2279 v.dedup();
2280 v
2281 };
2282 if kinds(external) != kinds(source) {
2283 return None;
2284 }
2285 let inputs = dry_inputs(source);
2286 let (a, _) = eval_choreography_traced(external, &inputs).ok()?;
2287 let (b, _) = eval_choreography_traced(source, &inputs).ok()?;
2288 if a.tensors != b.tensors {
2289 return None;
2290 }
2291 let mut ext = external.clone();
2292 ext.certificate = None;
2293 certify_choreography(&ext).ok()
2294}
2295
2296pub fn certifiable_operand(p: Precision) -> bool {
2298 precision_is_int(p)
2299}