Skip to main content

logicaffeine_compile/spatial/passes/
forward.rs

1//! `forward` — one section's result handed to the next section's compute, never through memory.
2//!
3//! ★ A RESULT ONLY ITS NEXT READER NEEDS NEVER LEAVES THE SITE. Section k computes a tensor and stores it; section
4//! k+1 loads the same tiles back and computes on them. When nothing but section k+1 reads the tensor — no later
5//! section, no `Show`, no carried state — the round trip moves nothing anyone keeps: the store, the load, the boundary
6//! between the two sections and the second section's own acquire are all overhead on a chain of dependent sections.
7//! Forwarding joins the two TEMPLATES into one section: the buffer the producer's store read now feeds the consumer's
8//! compute, and the tensor is gone. It joins templates (`Phase::Template`), before `tile`, so the joined section is
9//! tiled, placed, scheduled and certified as one and `fuse_generic` then folds a map into the map it follows; and it
10//! joins two sections `tile` has planned (`Phase::Tiled`), where a matmul's items — a work item per output tile in its
11//! template — meet the one item a row map walks only once the plan has grouped them.
12//!
13//! ★ THE STORE AND THE LOAD AGREE, OR NOTHING IS JOINED. The producer stores the tensor once, from a buffer nothing
14//! else reads; the consumer loads it once, into a buffer only computes read; the two sections walk the same work items
15//! on the same tile, and for every one of them the store and the load address the same tiles of the same tensor in the
16//! same order ([`AccessPattern::tiles_at`]). Then the page the producer packs is exactly the page the consumer would
17//! have loaded. A store whose address steps with its item or its sub-block — a matmul's — agrees only under the plan
18//! the template's items were laid out for: one item, packed once. Two templates join through one only where each walks
19//! one item, a tile, which `tile` can only plan as one item packed once: a wider pair, whose plan packs a block of tiles
20//! an item where the consumer was laid out a tile an item, would compute its first tile alone. Two planned sections join
21//! through one only under a plan of one item whose every matmul packs once ([`one_block`]). Whether the tensor is dead
22//! after the consumer is the caller's to know (it reads the whole program); this pass never decides it. Nor is whether
23//! the join keeps what the value was proven to hold: the buffer that now carries it
24//! ([`Joint::carrier`]) holds what the producer's compute proves, and the caller, holding the front end's intervals,
25//! judges whether that is wider than what a load of the tensor would have read.
26
27use crate::spatial::ir::*;
28
29/// Two sections joined into one.
30#[derive(Clone, Debug)]
31pub struct Joint {
32    /// The joined section, named `first+second`: a template, or planned as the producer was.
33    pub program: SpatialProgram,
34    /// The buffer the producer's store read, which now feeds the consumer's compute: the forwarded tensor's values.
35    pub carrier: BufferId,
36}
37
38/// Whether `p`, a section the passes have planned, holds a join through a store that steps with its item or sub-block:
39/// one work item, and every matmul a single sub-block.
40pub fn one_block(p: &SpatialProgram) -> bool {
41    p.work.items.len() == 1
42        && p.stages.iter().all(|s| match &s.kind {
43            StageKind::Compute(ComputeOp::MatmulTiles { sub_blocks, .. }) => *sub_blocks == (1, 1),
44            _ => true,
45        })
46}
47
48/// `first` and `second` joined through `tensor`, the one `first` stores and `second` loads, or `None` where the store
49/// and the load do not agree (see the module doc).
50pub fn forward(first: &SpatialProgram, second: &SpatialProgram, tensor: &str) -> Option<Joint> {
51    // Two templates, or two sections `tile` has planned and nothing has placed: a planned consumer runs under the
52    // producer's plan, so it brings no plan of its own that counts trips.
53    let tiled = match (first.phase(), second.phase()) {
54        (Phase::Template, Phase::Template) => false,
55        (Phase::Tiled, Phase::Tiled) if second.trips.is_empty() && second.work.k_trip.is_none() => true,
56        _ => return None,
57    };
58    // The consumer walks the producer's items; a contraction is the producer's alone (a map's `k_tiles` is 1), and the
59    // joined section keeps it.
60    if first.tile != second.tile
61        || first.work.items != second.work.items
62        || (second.work.k_tiles != 1 && second.work.k_tiles != first.work.k_tiles)
63        || first.mem_banks != second.mem_banks
64        || first.numerics != second.numerics
65    {
66        return None;
67    }
68    let t1 = first.tensors.iter().find(|t| t.name == tensor)?;
69    let t2 = second.tensors.iter().find(|t| t.name == tensor)?;
70    if (t1.elems, t1.tiles, t1.dtype, t1.layout) != (t2.elems, t2.tiles, t2.dtype, t2.layout) {
71        return None;
72    }
73    // The producer's one store: of this tensor, and the only stage that reads its buffer.
74    let stores: Vec<&Stage> = first.stages.iter().filter(|s| matches!(s.kind, StageKind::Store { .. })).collect();
75    let [store] = stores.as_slice() else { return None };
76    let StageKind::Store { tensor: st, pattern: sp, sub_blocks } = &store.kind else { return None };
77    if *st != t1.id || store.ins.len() != 1 {
78        return None;
79    }
80    let o = store.ins[0];
81    if first.stages.iter().any(|s| s.id != store.id && s.ins.contains(&o)) {
82        return None;
83    }
84    // The tensor leaves both sections whole: no other stage of either stores, loads or reduces it.
85    let names = |s: &Stage, t: TensorId| {
86        matches!(&s.kind, StageKind::Load { tensor, .. } | StageKind::Store { tensor, .. } | StageKind::AllReduce { tensor, .. } if *tensor == t)
87    };
88    // The consumer's one load of it, into a buffer only computes read.
89    let loads: Vec<&Stage> = second
90        .stages
91        .iter()
92        .filter(|s| matches!(&s.kind, StageKind::Load { tensor: x, .. } if *x == t2.id))
93        .collect();
94    let [load] = loads.as_slice() else { return None };
95    if first.stages.iter().any(|s| s.id != store.id && names(s, t1.id)) || second.stages.iter().any(|s| s.id != load.id && names(s, t2.id)) {
96        return None;
97    }
98    let StageKind::Load { pattern: lp, num_blocks, multicast, .. } = &load.kind else { return None };
99    if *num_blocks != 1 || multicast.is_some() || load.outs.len() != 1 || first.work.items.is_empty() {
100        return None;
101    }
102    // The same tiles in the same order for every item the two sections walk.
103    for item in &first.work.items {
104        let stored = sp.tiles_at(item.coord, *sub_blocks)?;
105        let loaded = lp.tiles_at(item.coord, (1, 1))?;
106        if stored != loaded {
107            return None;
108        }
109    }
110    // A store that steps with its item or its sub-block packs, under the plan `tile` gives the joined section, a block of
111    // the producer's tiles an item, where the consumer's compute was laid out a tile an item: two templates join through
112    // one only where each walks one item — a tile, planned as one item packed once — and a wider pair is left to the
113    // planned join, which compares the planned items.
114    let one_block = !sp.origin.steps.is_empty();
115    if one_block && !tiled && first.work.items.len() != 1 {
116        return None;
117    }
118    let i = load.outs[0];
119    let readers: Vec<&Stage> = second.stages.iter().filter(|s| s.ins.contains(&i)).collect();
120    if readers.is_empty() || !readers.iter().all(|s| matches!(s.kind, StageKind::Compute(_))) {
121        return None;
122    }
123    let (bo, bi) = (first.buffer(o), second.buffer(i));
124    if bo.kind != bi.kind || bo.dtype != bi.dtype {
125        return None;
126    }
127
128    // Tensors: the producer's but the forwarded one, then the consumer's not already there. A tensor both read is one
129    // tensor, and must be declared alike.
130    let mut tensors: Vec<TensorDecl> = Vec::new();
131    let mut tmap1 = std::collections::BTreeMap::new();
132    for t in first.tensors.iter().filter(|t| t.id != t1.id) {
133        tmap1.insert(t.id, TensorId::new(tensors.len()));
134        tensors.push(TensorDecl { id: TensorId::new(tensors.len()), ..t.clone() });
135    }
136    let mut tmap2 = std::collections::BTreeMap::new();
137    for t in second.tensors.iter().filter(|t| t.id != t2.id) {
138        match tensors.iter().find(|x| x.name == t.name) {
139            Some(x) => {
140                if (TensorDecl { id: x.id, ..t.clone() }) != *x {
141                    return None;
142                }
143                tmap2.insert(t.id, x.id);
144            }
145            None => {
146                tmap2.insert(t.id, TensorId::new(tensors.len()));
147                tensors.push(TensorDecl { id: TensorId::new(tensors.len()), ..t.clone() });
148            }
149        }
150    }
151
152    // Buffers: every one of the producer's, then the consumer's but its load's, which the producer's store buffer is.
153    let mut buffers: Vec<Buffer> = first.buffers.clone();
154    let mut bmap2 = std::collections::BTreeMap::new();
155    bmap2.insert(i, o);
156    for b in second.buffers.iter().filter(|b| b.id != i) {
157        // A name is a buffer's identity in every printer: the consumer's own `out` beside the producer's takes a suffix.
158        let mut name = b.name.clone();
159        let mut k = 1;
160        while buffers.iter().any(|x| x.name == name) {
161            name = format!("{}_f{k}", b.name);
162            k += 1;
163        }
164        bmap2.insert(b.id, BufferId::new(buffers.len()));
165        buffers.push(Buffer { id: BufferId::new(buffers.len()), name, ..b.clone() });
166    }
167
168    // Stages: the producer's but its store, then the consumer's but its load.
169    let mut stages: Vec<Stage> = Vec::new();
170    let mut smap1 = std::collections::BTreeMap::new();
171    for s in first.stages.iter().filter(|s| s.id != store.id) {
172        let id = StageId::new(stages.len());
173        smap1.insert(s.id, id);
174        let mut kind = s.kind.clone();
175        retensor(&mut kind, &tmap1);
176        stages.push(Stage { id, kind, ..s.clone() });
177    }
178    let mut smap2 = std::collections::BTreeMap::new();
179    for s in second.stages.iter().filter(|s| s.id != load.id) {
180        let id = StageId::new(stages.len());
181        smap2.insert(s.id, id);
182        let mut kind = s.kind.clone();
183        retensor(&mut kind, &tmap2);
184        kind.remap_buffers(&|b| bmap2[&b]);
185        stages.push(Stage {
186            id,
187            kind,
188            ins: s.ins.iter().map(|b| bmap2[b]).collect(),
189            outs: s.outs.iter().map(|b| bmap2[b]).collect(),
190            ..s.clone()
191        });
192    }
193
194    // Edges: the store buffer's edge now reaches the consumer's readers; the load's edge is gone.
195    let mut edges: Vec<Edge> = Vec::new();
196    for e in &first.edges {
197        let to: Vec<StageId> = if e.buffer == o {
198            readers.iter().map(|s| smap2[&s.id]).collect()
199        } else {
200            e.to.iter().map(|s| smap1[s]).collect()
201        };
202        edges.push(Edge { id: EdgeId::new(edges.len()), from: smap1[&e.from], to, ..e.clone() });
203    }
204    for e in second.edges.iter().filter(|e| e.buffer != i) {
205        edges.push(Edge {
206            id: EdgeId::new(edges.len()),
207            from: smap2[&e.from],
208            buffer: bmap2[&e.buffer],
209            to: e.to.iter().map(|s| smap2[s]).collect(),
210            ..e.clone()
211        });
212    }
213
214    let claim = match (&first.claim, &second.claim) {
215        (NumericsClaim::Exact, c) | (c, NumericsClaim::Exact) => c.clone(),
216        (c, _) => c.clone(),
217    };
218    let mut requires = first.requires.clone();
219    requires.handshakes.extend(second.requires.handshakes.iter().copied());
220    requires.interconnect.extend(second.requires.interconnect.iter().copied());
221    let program = SpatialProgram {
222        name: format!("{}+{}", first.name, second.name),
223        tensors,
224        stages,
225        buffers,
226        edges,
227        claim,
228        requires,
229        acc_tiles_used: first.acc_tiles_used.max(second.acc_tiles_used),
230        ..first.clone()
231    };
232    // A planned join is checked against the plan it has: one item, packed once, or nothing.
233    if tiled && one_block && !self::one_block(&program) {
234        return None;
235    }
236    Some(Joint { program, carrier: o })
237}
238
239/// Point a stage kind's tensor at its id in the joined program.
240fn retensor(kind: &mut StageKind, map: &std::collections::BTreeMap<TensorId, TensorId>) {
241    match kind {
242        StageKind::Load { tensor, .. } | StageKind::Store { tensor, .. } | StageKind::AllReduce { tensor, .. } => *tensor = map[tensor],
243        StageKind::Recv { .. } | StageKind::Compute(_) | StageKind::Scalar(_) => {}
244    }
245}