Skip to main content

logicaffeine_compile/spatial/passes/
pack.rs

1//! Phase packing — independent sections run at once, on disjoint sites.
2//!
3//! ★ A BARRIER ONLY WHERE DATA FLOWS. A composed program's sections run in PHASES: every section of a phase starts
4//! together, and a barrier closes the phase. Two sections may share one when neither reads what the other writes nor
5//! writes what the other touches (by tensor name, the carried state included) — and, on one chip, when they hold
6//! DISJOINT SITES: a site's local memory is laid out for the one section it runs in a phase, so two sections never
7//! share a buffer. On a chip whose sites are interchangeable — a crossbar, no multicast rectangle whose geometry a
8//! section's plan depends on — where a section runs is a renaming ([`for_each_site_mut`]), so each section is packed onto the
9//! next free sites of the earliest phase after everything it depends on that still has room for it.
10//!
11//! A decode layer's sections each hold a few dozen of the SN50's 1,040 sites: the projections of one normalized row,
12//! the convolutions of its three streams, the per-head norms beside them run in one phase each instead of one after
13//! another with the rest of the fabric idle.
14
15use std::collections::BTreeSet;
16
17use crate::spatial::ir::*;
18use crate::spatial::target::{Interconnect, SpatialTarget};
19use crate::spatial::tensor_parallel::SectionTouch;
20
21/// Every site `p` names, rewritten by `f`: its stages', buffers', placement's, memory map's, semaphores' and engine
22/// programs', and the semaphore posts inside those programs. A bijection of sites moves where the program runs and
23/// nothing it computes.
24pub fn for_each_site_mut(p: &mut SpatialProgram, f: &mut dyn FnMut(&mut Site)) {
25    fn posts(ops: &mut [TaggedOp], f: &mut dyn FnMut(&mut Site)) {
26        for t in ops {
27            match &mut t.op {
28                ChoreoOp::SemInc { at, .. } => f(at),
29                ChoreoOp::Loop { body, .. } => posts(body, f),
30                _ => {}
31            }
32        }
33    }
34    for s in &mut p.stages {
35        if let Some(x) = &mut s.site {
36            f(x);
37        }
38    }
39    for b in &mut p.buffers {
40        if let Some(x) = &mut b.site {
41            f(x);
42        }
43    }
44    if let Some(pl) = &mut p.placement {
45        pl.site_of.iter_mut().for_each(|x| f(x));
46        pl.engine_of.iter_mut().for_each(|e| f(&mut e.site));
47        pl.items_of_site.iter_mut().for_each(|(x, _)| f(x));
48    }
49    if let Some(mm) = &mut p.memory {
50        mm.buffer_at.iter_mut().for_each(|a| f(&mut a.site));
51    }
52    for sem in &mut p.semaphores {
53        sem.sites.iter_mut().for_each(|x| f(x));
54    }
55    if let Some(sch) = &mut p.schedule {
56        for prog in &mut sch.programs {
57            f(&mut prog.site);
58            posts(&mut prog.ops, f);
59        }
60    }
61}
62
63/// The sites `p` runs on.
64pub fn sites_used(p: &mut SpatialProgram) -> BTreeSet<Site> {
65    let mut seen = BTreeSet::new();
66    for_each_site_mut(p, &mut |s| {
67        seen.insert(*s);
68    });
69    seen
70}
71
72/// Whether where a section runs is a renaming on `target`: its sites are interchangeable (no multicast rectangle), and
73/// no section of `sections` plans a multicast group or spans more than one chip.
74fn packable(sections: &mut [SpatialProgram], target: &SpatialTarget) -> bool {
75    !target.provides_interconnect(Interconnect::MulticastRect)
76        && sections.iter_mut().all(|p| {
77            p.placement.as_ref().is_some_and(|pl| pl.groups.is_empty())
78                && sites_used(p).iter().all(|s| s.chip == 0)
79        })
80}
81
82/// The sections packed into phases: each moved onto the next free sites of the earliest phase after every section it
83/// depends on that still has room, returned in phase order with `barrier_after[k]` closing each phase's last section
84/// — or the sections back, untouched, when they cannot be moved on `target` (see `packable`).
85///
86/// A section depends on an earlier one exactly when some name witnesses it: it reads what the earlier one writes, or
87/// writes what the earlier one reads or writes.
88pub fn pack_phases(
89    mut sections: Vec<SpatialProgram>,
90    target: &SpatialTarget,
91) -> Result<(Vec<SpatialProgram>, Vec<bool>), Vec<SpatialProgram>> {
92    if sections.len() < 2 || !packable(&mut sections, target) {
93        return Err(sections);
94    }
95    let mesh: Vec<Site> = {
96        let mut all: Vec<Site> = (0..target.sites.x)
97            .flat_map(|x| (0..target.sites.y).map(move |y| Site::new(x, y)))
98            .collect();
99        all.sort();
100        all
101    };
102    let footprints: Vec<Vec<Site>> = sections
103        .iter_mut()
104        .map(|p| sites_used(p).into_iter().collect())
105        .collect();
106    if footprints.iter().any(|used| used.len() > mesh.len()) {
107        return Err(sections);
108    }
109    let (mut written_in, mut read_in) = (
110        std::collections::BTreeMap::<String, usize>::new(),
111        std::collections::BTreeMap::<String, usize>::new(),
112    );
113    let mut occupied: Vec<usize> = Vec::new();
114    let mut phase_of = Vec::with_capacity(sections.len());
115    for (p, used) in sections.iter_mut().zip(&footprints) {
116        let touch = SectionTouch::of(p);
117        let raw = touch.reads.iter().filter_map(|n| written_in.get(n));
118        let war_waw = touch
119            .writes
120            .iter()
121            .flat_map(|n| [written_in.get(n), read_in.get(n)])
122            .flatten();
123        let after = raw.chain(war_waw).map(|ph| ph + 1).max().unwrap_or(0);
124        // The first phase from `after` with room; a phase past every occupied one is empty, and every section fits one.
125        let ph = (after..)
126            .find(|ph| occupied.get(*ph).copied().unwrap_or(0) + used.len() <= mesh.len())
127            .expect("an empty phase holds any placed section");
128        if occupied.len() <= ph {
129            occupied.resize(ph + 1, 0);
130        }
131        // The section's `i`-th site becomes the phase's next free one: a monotone renaming, so every list the program
132        // keeps in site order stays in it.
133        let target_sites = &mesh[occupied[ph]..occupied[ph] + used.len()];
134        let rename: std::collections::BTreeMap<Site, Site> = used
135            .iter()
136            .copied()
137            .zip(target_sites.iter().copied())
138            .collect();
139        for_each_site_mut(p, &mut |s| *s = rename[s]);
140        occupied[ph] += used.len();
141        phase_of.push(ph);
142        for n in touch.reads {
143            let r = read_in.entry(n).or_insert(ph);
144            *r = (*r).max(ph);
145        }
146        for n in touch.writes {
147            let w = written_in.entry(n).or_insert(ph);
148            *w = (*w).max(ph);
149        }
150    }
151    let mut order: Vec<usize> = (0..sections.len()).collect();
152    order.sort_by_key(|&k| (phase_of[k], k));
153    let barrier_after = order
154        .iter()
155        .enumerate()
156        .map(|(pos, &k)| {
157            order
158                .get(pos + 1)
159                .is_some_and(|&next| phase_of[next] != phase_of[k])
160        })
161        .collect();
162    let mut slots: Vec<Option<SpatialProgram>> = sections.into_iter().map(Some).collect();
163    Ok((
164        order
165            .iter()
166            .map(|&k| slots[k].take().expect("each section once"))
167            .collect(),
168        barrier_after,
169    ))
170}