Skip to main content

logicaffeine_compile/spatial/passes/
synthesize_handshakes.rs

1//! `synthesize_handshakes` — semaphore choreography DERIVED from the buffer/edge graph.
2//!
3//! For every site and engine the pass emits the exact op sequence the edges' handshake kinds demand:
4//! local FIFO / valid-ready hand-offs (`Reserve → fill → Push`, `Wait → use → Pop`), the multicast sender's
5//! ready-wait / reset / data multicast / \[flush\] / valid-flag multicast, the receiver's invalidate /
6//! ready-increment / valid-wait, the compute engine's acquire / \[reload\] / compute / pack / release with the
7//! first and last K block peeled (no runtime `if`), and the writer's wait / write / barrier / pop. Each
8//! program is one ENGINE's sequential code; loops over K blocks and sub-blocks are explicit; the item loop is
9//! `persist`'s.
10
11use super::{SpatialReject, LV_KBLOCK, LV_SUB_H, LV_SUB_W};
12
13/// Pages one work item pushes through `buf`: the producer's granularity, followed through a chain.
14///
15/// Bounded by the STAGE COUNT, not a literal: each step walks to the producer of the previous stage's input,
16/// so exceeding the number of stages means a cycle rather than a deep chain. A constant sized to today's
17/// longest chain silently returns the wrong page count the day someone fuses a longer one.
18fn pages_per_item(p: &SpatialProgram, buf: BufferId, depth: usize) -> usize {
19    if depth > p.stages.len() {
20        return 1;
21    }
22    let Some(e) = p.edges.iter().find(|e| e.buffer == buf) else {
23        return 1;
24    };
25    let producer = p.stage(e.from);
26    match &producer.kind {
27        StageKind::Compute(ComputeOp::MatmulTiles { sub_blocks, .. }) => {
28            sub_blocks.0 * sub_blocks.1
29        }
30        StageKind::Compute(_) | StageKind::Scalar(_) | StageKind::AllReduce { .. } => producer
31            .ins
32            .first()
33            .map(|b| pages_per_item(p, *b, depth + 1))
34            .unwrap_or(1),
35        StageKind::Load {
36            pattern,
37            num_blocks,
38            ..
39        } => {
40            if pattern
41                .origin
42                .steps
43                .iter()
44                .any(|(v, _)| *v == LV_SUB_H || *v == LV_SUB_W)
45            {
46                p.work
47                    .plan
48                    .map(|pl| {
49                        (pl.per_site_m / pl.block.osh.max(1))
50                            * (pl.per_site_n / pl.block.osw.max(1))
51                    })
52                    .unwrap_or(1)
53            } else {
54                *num_blocks
55            }
56        }
57        StageKind::Recv { .. } => p.work.plan.map(|pl| pl.inner.num_blocks).unwrap_or(1),
58        StageKind::Store { .. } => 1,
59    }
60}
61use crate::spatial::cost::is_streaming;
62use crate::spatial::ir::*;
63use crate::spatial::target::{SpatialTarget, WriteOrdering};
64use std::collections::BTreeMap;
65
66fn stage_group_semaphores(
67    p: &SpatialProgram,
68    stage: StageId,
69) -> Option<(GroupId, SemId, SemId, usize)> {
70    let s = p.stage(stage);
71    let g = p.stage_group(s)?;
72    for e in &p.edges {
73        if let Handshake::Semaphore {
74            ready,
75            valid,
76            ready_count,
77        } = &e.handshake
78        {
79            let sender_is_me = e.from == stage;
80            let receiver_is_me = e.to.contains(&stage);
81            if (sender_is_me || receiver_is_me) && p.stage_group(p.stage(e.from)) == Some(g) {
82                return Some((g, *ready, *valid, *ready_count));
83            }
84        }
85    }
86    None
87}
88
89/// The movement ops that land one block of a Load / Recv stage into its buffer.
90fn reader_block(p: &SpatialProgram, target: &SpatialTarget, s: &Stage, out: &mut Vec<TaggedOp>) {
91    let buf = s.outs[0];
92    let tag = |op: ChoreoOp| TaggedOp { stage: s.id, op };
93    match &s.kind {
94        StageKind::Load {
95            tensor,
96            pattern,
97            multicast,
98            ..
99        } => {
100            // ★ A GATHER WAITS FOR ITS INDEX. The tile address of an `IndirectTile` load is a value another
101            // stage produces; the read may not issue before that page is visible, and the page is retired
102            // once the read has resolved it.
103            let index = pattern.origin.indirect.map(|i| i.index);
104            if let Some(ix) = index {
105                out.push(tag(ChoreoOp::Wait { buf: ix, pages: 1 }));
106            }
107            out.push(tag(ChoreoOp::Reserve { buf, pages: 1 }));
108            out.push(tag(ChoreoOp::ReadTiles {
109                tensor: *tensor,
110                at: pattern.origin.clone(),
111                block_h: pattern.block_h,
112                block_w: pattern.block_w,
113                stride_w: pattern.stride_w,
114                stride_h: pattern.stride_h,
115                into: buf,
116            }));
117            out.push(tag(ChoreoOp::ReadBarrier));
118            if let Some(ix) = index {
119                out.push(tag(ChoreoOp::Pop { buf: ix, pages: 1 }));
120            }
121            if let Some(g) = multicast {
122                let (_, ready, valid, n) = stage_group_semaphores(p, s.id)
123                    .expect("a multicasting load has a semaphore edge");
124                out.push(tag(ChoreoOp::SemWait {
125                    sem: ready,
126                    until: SemCond::Equals(n),
127                }));
128                out.push(tag(ChoreoOp::SemSet {
129                    sem: ready,
130                    value: SemValue::Count(0),
131                }));
132                // The valid flag goes to the SAME rectangle immediately after the data, so the data
133                // transfer keeps the broadcast path it reserved and the flag rides it. A target whose writes
134                // need an explicit flush between the two cannot hold the path across that flush.
135                let hold_path = target.write_ordering != WriteOrdering::NeedsFlush;
136                out.push(tag(ChoreoOp::McastSend {
137                    buf,
138                    pages: 1,
139                    group: *g,
140                    hold_path,
141                }));
142                if target.write_ordering == WriteOrdering::NeedsFlush {
143                    out.push(tag(ChoreoOp::Flush {
144                        cond: ArchCond::Blackhole,
145                    }));
146                }
147                out.push(tag(ChoreoOp::McastSetValid {
148                    sem: valid,
149                    group: *g,
150                }));
151            }
152            out.push(tag(ChoreoOp::Push { buf, pages: 1 }));
153        }
154        StageKind::Recv { group } => {
155            let (_, ready, valid, _) =
156                stage_group_semaphores(p, s.id).expect("a receiver has a semaphore edge");
157            let sender = p.placement.as_ref().unwrap().groups[group.idx()].sender;
158            out.push(tag(ChoreoOp::Reserve { buf, pages: 1 }));
159            out.push(tag(ChoreoOp::SemSet {
160                sem: valid,
161                value: SemValue::Invalid,
162            }));
163            out.push(tag(ChoreoOp::SemInc {
164                sem: ready,
165                at: sender,
166                by: 1,
167            }));
168            out.push(tag(ChoreoOp::SemWait {
169                sem: valid,
170                until: SemCond::Equals(SemValue::Valid.as_u32() as usize),
171            }));
172            out.push(tag(ChoreoOp::Push { buf, pages: 1 }));
173        }
174        _ => {}
175    }
176}
177
178/// The body of one K block on a target whose accumulator LIVES IN REGISTERS: just the multiply.
179///
180/// No acquire, no pack, no spill — the caller owns one accumulate region around the whole K loop,
181/// so each block adds into the accumulators already held. That is the only shape the GPU backend
182/// can lower (its fragments never leave the registers), and it is what a real GEMM does.
183fn sub_block_body_regs(s: &Stage, op: &ComputeOp, ins: &[BufferId], tiles: usize) -> Vec<ChoreoOp> {
184    let _ = (s, tiles);
185    vec![ChoreoOp::Compute {
186        op: op.clone(),
187        ins: ins.to_vec(),
188        acc_reload: None,
189    }]
190}
191
192fn sub_block_body(
193    s: &Stage,
194    op: &ComputeOp,
195    ins: &[BufferId],
196    reload: Option<BufferId>,
197    spill_to: BufferId,
198    tiles: usize,
199    convert: crate::codegen_sva::fabric_target::Precision,
200) -> Vec<ChoreoOp> {
201    sub_block_body_acc(s, op, ins, reload, spill_to, tiles, convert, false)
202}
203
204/// The sub-block body, with the packer either replacing the spill page or accumulating into it.
205#[allow(clippy::too_many_arguments)]
206fn sub_block_body_acc(
207    s: &Stage,
208    op: &ComputeOp,
209    ins: &[BufferId],
210    reload: Option<BufferId>,
211    spill_to: BufferId,
212    tiles: usize,
213    convert: crate::codegen_sva::fabric_target::Precision,
214    accumulate: bool,
215) -> Vec<ChoreoOp> {
216    let _ = s;
217    let mut body = vec![ChoreoOp::AcquireAcc { tiles }];
218    if let Some(interm) = reload {
219        body.push(ChoreoOp::Wait {
220            buf: interm,
221            pages: 1,
222        });
223    }
224    body.push(ChoreoOp::Compute {
225        op: op.clone(),
226        ins: ins.to_vec(),
227        acc_reload: reload,
228    });
229    if let Some(interm) = reload {
230        body.push(ChoreoOp::Pop {
231            buf: interm,
232            pages: 1,
233        });
234    }
235    body.push(ChoreoOp::Reserve {
236        buf: spill_to,
237        pages: 1,
238    });
239    body.push(ChoreoOp::PackAcc {
240        to: spill_to,
241        tiles,
242        convert,
243        accumulate,
244    });
245    body.push(ChoreoOp::Push {
246        buf: spill_to,
247        pages: 1,
248    });
249    body.push(ChoreoOp::ReleaseAcc);
250    body
251}
252
253/// Tag every op of a single-stage sequence.
254fn tagged(stage: StageId, ops: Vec<ChoreoOp>) -> Vec<TaggedOp> {
255    ops.into_iter().map(|op| TaggedOp { stage, op }).collect()
256}
257
258/// Wrap a tagged body in the sub-block loops (the loop op itself carries the body's first stage).
259fn sub_loops(sub_blocks: (usize, usize), body: Vec<TaggedOp>) -> Vec<TaggedOp> {
260    let stage = body.first().map(|t| t.stage).unwrap_or(StageId::new(0));
261    let inner = if sub_blocks.1 > 1 {
262        vec![TaggedOp {
263            stage,
264            op: ChoreoOp::Loop {
265                var: LV_SUB_W,
266                count: LoopCount::Const(sub_blocks.1),
267                body,
268            },
269        }]
270    } else {
271        body
272    };
273    if sub_blocks.0 > 1 {
274        vec![TaggedOp {
275            stage,
276            op: ChoreoOp::Loop {
277                var: LV_SUB_H,
278                count: LoopCount::Const(sub_blocks.0),
279                body: inner,
280            },
281        }]
282    } else {
283        inner
284    }
285}
286
287/// Buffers an op sequence pushes to, and buffers it waits on.
288fn touches(
289    ops: &[TaggedOp],
290) -> (
291    std::collections::BTreeSet<usize>,
292    std::collections::BTreeSet<usize>,
293) {
294    let (mut produces, mut consumes) = (
295        std::collections::BTreeSet::new(),
296        std::collections::BTreeSet::new(),
297    );
298    for t in ops {
299        match &t.op {
300            ChoreoOp::Push { buf, .. } | ChoreoOp::Reserve { buf, .. } => {
301                produces.insert(buf.idx());
302            }
303            ChoreoOp::Wait { buf, .. } | ChoreoOp::Pop { buf, .. } => {
304                consumes.insert(buf.idx());
305            }
306            ChoreoOp::Loop { body, .. } => {
307                let (pr, co) = touches(body);
308                produces.extend(pr);
309                consumes.extend(co);
310            }
311            _ => {}
312        }
313    }
314    (produces, consumes)
315}
316
317/// Interleave adjacent loop nests over the same index space into one nest.
318///
319/// An engine program is serial, so two stages emitted as consecutive whole loops run one after the other in
320/// full. When those stages sit at opposite ends of the same pipeline — a load feeding the compute, and the
321/// store draining it — running the first to completion fills the buffer between them and the pair deadlocks
322/// against each other: the loader cannot place its next page because the consumer is blocked, and the store
323/// that would unblock it has not started. Interleaving them per iteration is what makes bounded buffers
324/// sufficient, and it is the same software-pipelining move that lets a fused compute chain hold at depth two.
325///
326/// Only nests over an identical index space fuse, and only when the later one does not drain a buffer the
327/// earlier one fills, since that hand-off is a real intra-engine dependence rather than a pipeline.
328fn fuse_adjacent_loops(ops: Vec<TaggedOp>) -> Vec<TaggedOp> {
329    let mut out: Vec<TaggedOp> = Vec::with_capacity(ops.len());
330    for t in ops {
331        let merged = match (out.last_mut().map(|l| &mut l.op), &t.op) {
332            (
333                Some(ChoreoOp::Loop {
334                    var: pv,
335                    count: pc,
336                    body: pb,
337                }),
338                ChoreoOp::Loop { var, count, body },
339            ) if pv == var && pc == count => {
340                let (produces, _) = touches(pb);
341                let (_, consumes) = touches(body);
342                if produces.is_disjoint(&consumes) {
343                    pb.extend(body.iter().cloned());
344                    true
345                } else {
346                    false
347                }
348            }
349            _ => false,
350        };
351        if !merged {
352            out.push(t);
353        }
354    }
355    // Fuse the bodies too, so a nest fuses at every level it matches.
356    for t in out.iter_mut() {
357        if let ChoreoOp::Loop { body, .. } = &mut t.op {
358            *body = fuse_adjacent_loops(std::mem::take(body));
359        }
360    }
361    out
362}
363
364/// A dataflow rank per stage: a stage that fills a buffer ranks before every stage that drains it.
365///
366/// Engine programs are serial, so the order stages take on ONE engine is a correctness property, not a
367/// cosmetic one: a stage that waits on data a LATER stage of the same engine produces can never run, and the
368/// program deadlocks against itself. Declaration order does not carry that guarantee — a lane that appends a
369/// load after the store it feeds is perfectly reasonable to write and fatal to schedule. Ranking by dataflow
370/// makes the safe order the only order any engine can be built in. Ties keep stage-id order, so a program
371/// with no cross-stage dependency schedules exactly as it always did.
372fn stage_ranks(p: &SpatialProgram) -> Vec<usize> {
373    let n = p.stages.len();
374    let mut producer_of: std::collections::BTreeMap<usize, Vec<usize>> = Default::default();
375    for s in &p.stages {
376        for b in &s.outs {
377            producer_of.entry(b.idx()).or_default().push(s.id.idx());
378        }
379    }
380    // ★ A CARRY IS READ BEFORE IT IS UPDATED. Within a K block every other body reader of a carry sees the value the
381    // block started with, so it ranks AHEAD of the carry's update — the edge the update's write would otherwise add
382    // (update → reader) is the wrong way round inside the block. The epilogue reads what the last update left.
383    let carries = p.carries();
384    let levels = if carries.is_empty() {
385        vec![FoldLevel::Prologue; n]
386    } else {
387        p.fold_levels()
388    };
389    let mut indeg = vec![0usize; n];
390    let mut succ: Vec<Vec<usize>> = vec![Vec::new(); n];
391    let mut arc = |from: usize, to: usize, succ: &mut Vec<Vec<usize>>| {
392        succ[from].push(to);
393        indeg[to] += 1;
394    };
395    for s in &p.stages {
396        for b in &s.ins {
397            let update = carries.get(b).map(|c| c.idx());
398            for &pr in producer_of
399                .get(&b.idx())
400                .map(|v| v.as_slice())
401                .unwrap_or(&[])
402            {
403                if pr == s.id.idx() {
404                    continue;
405                }
406                if Some(pr) == update && levels[s.id.idx()] == FoldLevel::Body {
407                    arc(s.id.idx(), pr, &mut succ);
408                } else {
409                    arc(pr, s.id.idx(), &mut succ);
410                }
411            }
412        }
413    }
414    let mut ready: std::collections::BTreeSet<usize> = (0..n).filter(|i| indeg[*i] == 0).collect();
415    let mut rank = vec![usize::MAX; n];
416    let mut next = 0usize;
417    while let Some(&i) = ready.iter().next() {
418        ready.remove(&i);
419        rank[i] = next;
420        next += 1;
421        for &t in &succ[i] {
422            indeg[t] -= 1;
423            if indeg[t] == 0 {
424                ready.insert(t);
425            }
426        }
427    }
428    // A cycle among stages is a template defect the validator reports; keep declaration order for those.
429    for (i, r) in rank.iter_mut().enumerate() {
430        if *r == usize::MAX {
431            *r = next + i;
432        }
433    }
434    rank
435}
436
437/// One pass of a compute chain over a single page: every stage waits its input, computes, pushes, pops.
438///
439/// ★ A PAGE IS POPPED BY ITS LAST READER, NOT ITS FIRST. A chain stage used to pop everything it
440/// read, which is right only while no two stages share an operand. The moment one does — a
441/// normalization that reduces `x` and then rescales `x`, or an `x·x` square whose operand is named
442/// twice — the first stage retires the page the second is still waiting on, and the site deadlocks.
443/// Popping at the last use (once per buffer, however many times it is named) makes the rule hold for
444/// any chain shape rather than for the shapes that happened to exist.
445fn chain_ops(p: &SpatialProgram, chain: &[&Stage]) -> Vec<TaggedOp> {
446    let mut last_use: std::collections::BTreeMap<BufferId, usize> = Default::default();
447    for (i, s) in chain.iter().enumerate() {
448        for b in operands(s) {
449            last_use.insert(b, i);
450        }
451    }
452    let mut body: Vec<TaggedOp> = Vec::new();
453    for (i, s) in chain.iter().enumerate() {
454        let op = match &s.kind {
455            StageKind::Compute(op) => op.clone(),
456            StageKind::Scalar(b) => ComputeOp::Scalar(b.clone()),
457            StageKind::AllReduce { combine, .. } => (**combine).clone(),
458            _ => continue,
459        };
460        let out = s.outs[0];
461        let out_dtype = p.buffer(out).dtype;
462        let tiles = p.buffer(out).kind.page_tiles();
463        let reads = operands(s);
464        let mut seq: Vec<ChoreoOp> = reads
465            .iter()
466            .map(|b| ChoreoOp::Wait { buf: *b, pages: 1 })
467            .collect();
468        seq.extend([
469            ChoreoOp::AcquireAcc { tiles },
470            ChoreoOp::Compute {
471                op,
472                ins: s.ins.clone(),
473                acc_reload: None,
474            },
475            ChoreoOp::Reserve { buf: out, pages: 1 },
476            ChoreoOp::PackAcc {
477                to: out,
478                tiles,
479                convert: out_dtype,
480                accumulate: false,
481            },
482            ChoreoOp::Push { buf: out, pages: 1 },
483            ChoreoOp::ReleaseAcc,
484        ]);
485        seq.extend(
486            reads
487                .iter()
488                .filter(|b| last_use.get(b) == Some(&i))
489                .map(|b| ChoreoOp::Pop { buf: *b, pages: 1 }),
490        );
491        body.extend(tagged(s.id, seq));
492    }
493    body
494}
495
496/// The distinct buffers a chain stage reads, in first-mention order — `s.ins` deduplicated, plus any
497/// operand the op carries inside itself rather than in `ins` (a reduction's scaler tile).
498fn operands(s: &Stage) -> Vec<BufferId> {
499    let mut out: Vec<BufferId> = Vec::new();
500    let mut push = |b: BufferId| {
501        if !out.contains(&b) {
502            out.push(b);
503        }
504    };
505    for b in &s.ins {
506        push(*b);
507    }
508    out
509}
510
511/// One chain stage's ops: wait its operands, compute into the accumulator, pack its page, then pop `pops`.
512fn stage_ops(p: &SpatialProgram, s: &Stage, pops: &[BufferId]) -> Vec<TaggedOp> {
513    let op = match &s.kind {
514        StageKind::Compute(op) => op.clone(),
515        StageKind::Scalar(b) => ComputeOp::Scalar(b.clone()),
516        StageKind::AllReduce { combine, .. } => (**combine).clone(),
517        _ => return Vec::new(),
518    };
519    let out = s.outs[0];
520    let tiles = p.buffer(out).kind.page_tiles();
521    let mut seq: Vec<ChoreoOp> = operands(s)
522        .into_iter()
523        .map(|b| ChoreoOp::Wait { buf: b, pages: 1 })
524        .collect();
525    seq.extend([
526        ChoreoOp::AcquireAcc { tiles },
527        ChoreoOp::Compute {
528            op,
529            ins: s.ins.clone(),
530            acc_reload: None,
531        },
532        ChoreoOp::Reserve { buf: out, pages: 1 },
533        ChoreoOp::PackAcc {
534            to: out,
535            tiles,
536            convert: p.buffer(out).dtype,
537            accumulate: false,
538        },
539        ChoreoOp::Push { buf: out, pages: 1 },
540        ChoreoOp::ReleaseAcc,
541    ]);
542    seq.extend(pops.iter().map(|b| ChoreoOp::Pop { buf: *b, pages: 1 }));
543    tagged(s.id, seq)
544}
545
546/// The compute chain of a K-block FOLD: its prologue once, its body once per K block (`nb`), its epilogue once
547/// ([`FoldLevel`]).
548///
549/// ★ EVERY PAGE IS POPPED ONCE, WHERE ITS LAST READER RUNS. A page the prologue made that the body reads stays
550/// visible across the blocks and is popped after them — or by its last epilogue reader; a body page is popped by its
551/// last reader in the block; a carry's page from before the block is popped by the carry's update once the new page
552/// is pushed, and the page the last block leaves by its last epilogue reader.
553/// The count of the K-block loops of `p`'s plan of `nb` blocks: its trip's, when its blocks run to one.
554fn k_count(p: &SpatialProgram, nb: usize) -> LoopCount {
555    match p.work.k_trip {
556        Some(trip) => LoopCount::Trip { trip, max: nb },
557        None => LoopCount::Const(nb),
558    }
559}
560
561fn fold_ops(
562    p: &SpatialProgram,
563    chain: &[&Stage],
564    levels: &[FoldLevel],
565    nb: usize,
566) -> Vec<TaggedOp> {
567    let carries = p.carries();
568    let at = |l: FoldLevel| {
569        chain
570            .iter()
571            .copied()
572            .filter(|s| levels[s.id.idx()] == l)
573            .collect::<Vec<&Stage>>()
574    };
575    let (pro, body, epi) = (
576        at(FoldLevel::Prologue),
577        at(FoldLevel::Body),
578        at(FoldLevel::Epilogue),
579    );
580    let last_reader = |stages: &[&Stage], b: BufferId| {
581        stages
582            .iter()
583            .rev()
584            .find(|s| s.ins.contains(&b))
585            .map(|s| s.id)
586    };
587    let producer_level = |b: BufferId| {
588        p.stages
589            .iter()
590            .filter(|s| s.outs.contains(&b))
591            .map(|s| levels[s.id.idx()])
592            .max()
593            .unwrap_or(FoldLevel::Prologue)
594    };
595    let mut pops: BTreeMap<StageId, Vec<BufferId>> = BTreeMap::new();
596    let mut after_blocks: Vec<BufferId> = Vec::new();
597    let mut read: Vec<BufferId> = Vec::new();
598    for s in chain {
599        for b in operands(s) {
600            if !read.contains(&b) {
601                read.push(b);
602            }
603        }
604    }
605    for b in read {
606        let final_pop = |pops: &mut BTreeMap<StageId, Vec<BufferId>>, after: &mut Vec<BufferId>| {
607            match last_reader(&epi, b) {
608                Some(r) => pops.entry(r).or_default().push(b),
609                None => after.push(b),
610            }
611        };
612        if let Some(update) = carries.get(&b) {
613            pops.entry(*update).or_default().push(b);
614            final_pop(&mut pops, &mut after_blocks);
615            continue;
616        }
617        match producer_level(b) {
618            FoldLevel::Body => {
619                if let Some(r) = last_reader(&body, b) {
620                    pops.entry(r).or_default().push(b);
621                }
622            }
623            FoldLevel::Prologue
624                if last_reader(&epi, b).is_some() || last_reader(&body, b).is_some() =>
625            {
626                final_pop(&mut pops, &mut after_blocks)
627            }
628            FoldLevel::Prologue => {
629                if let Some(r) = last_reader(&pro, b) {
630                    pops.entry(r).or_default().push(b);
631                }
632            }
633            FoldLevel::Epilogue => {
634                if let Some(r) = last_reader(&epi, b) {
635                    pops.entry(r).or_default().push(b);
636                }
637            }
638        }
639    }
640    let none: Vec<BufferId> = Vec::new();
641    let ops_of = |stages: &[&Stage]| {
642        stages
643            .iter()
644            .flat_map(|s| stage_ops(p, s, pops.get(&s.id).unwrap_or(&none)))
645            .collect::<Vec<TaggedOp>>()
646    };
647    let mut out = ops_of(&pro);
648    let blocks = ops_of(&body);
649    let last_body = body.last().map(|s| s.id).unwrap_or(StageId::new(0));
650    if nb > 1 {
651        if let Some(first) = blocks.first().map(|t| t.stage) {
652            out.push(TaggedOp {
653                stage: first,
654                op: ChoreoOp::Loop {
655                    var: LV_KBLOCK,
656                    count: k_count(p, nb),
657                    body: blocks,
658                },
659            });
660        }
661    } else {
662        out.extend(blocks);
663    }
664    // A carry's last page was pushed by the last block's update and never waited on — wait, then retire it.
665    out.extend(tagged(
666        last_body,
667        after_blocks
668            .into_iter()
669            .flat_map(|b| {
670                [
671                    ChoreoOp::Wait { buf: b, pages: 1 },
672                    ChoreoOp::Pop { buf: b, pages: 1 },
673                ]
674            })
675            .collect(),
676    ));
677    out.extend(ops_of(&epi));
678    out
679}
680
681/// The compute engine program of a site; `here` is the site's stages in id order, `rank` every stage's dataflow rank.
682fn compute_program(
683    p: &SpatialProgram,
684    here: &[&Stage],
685    rank: &[usize],
686    levels: &[FoldLevel],
687) -> Result<Vec<TaggedOp>, SpatialReject> {
688    let plan = p.work.plan.expect("tiled");
689    let mut ops = Vec::new();
690    let mut stages: Vec<&Stage> = here
691        .iter()
692        .copied()
693        .filter(|s| s.role == SiteRole::Compute)
694        .collect();
695    stages.sort_by_key(|s| rank[s.id.idx()]);
696    let stages = stages;
697    // The compute CHAIN after the matmul (requant → bias → relu …). Its iteration space is the matmul's
698    // own sub-block nest, so it is emitted INSIDE that nest: each output page is consumed by the chain in
699    // the same turn the matmul produced it. Emitting the two nests in sequence instead would have the
700    // matmul push every page of an item before the chain popped any, which needs a hand-off as deep as the
701    // item and deadlocks at depth two.
702    let mut chain: Vec<&Stage> = here
703        .iter()
704        .copied()
705        .filter(|s| {
706            s.role == SiteRole::Compute
707                && matches!(
708                    s.kind,
709                    StageKind::Compute(_) | StageKind::Scalar(_) | StageKind::AllReduce { .. }
710                )
711                && !matches!(s.kind, StageKind::Compute(ComputeOp::MatmulTiles { .. }))
712        })
713        .collect();
714    chain.sort_by_key(|s| rank[s.id.idx()]);
715    let chain = chain;
716    let chain_body = chain_ops(p, &chain);
717    let has_matmul = stages
718        .iter()
719        .any(|s| matches!(s.kind, StageKind::Compute(ComputeOp::MatmulTiles { .. })));
720    let fused = has_matmul && !chain.is_empty();
721    let fuse = |body: Vec<TaggedOp>| -> Vec<TaggedOp> {
722        if fused {
723            body.into_iter().chain(chain_body.iter().cloned()).collect()
724        } else {
725            body
726        }
727    };
728    for s in stages {
729        let tag = |op: ChoreoOp| TaggedOp { stage: s.id, op };
730        let out = s.outs[0];
731        let out_dtype = p.buffer(out).dtype;
732        match &s.kind {
733            StageKind::Compute(
734                op @ ComputeOp::MatmulTiles {
735                    block,
736                    sub_blocks,
737                    num_blocks,
738                    interm,
739                    ..
740                },
741            ) => {
742                if p.work.k_trip.is_some() {
743                    return Err(SpatialReject::NotLowered("a matmul whose K blocks run to a trip — its accumulate-and-spill forms peel blocks a count read from the data may not have"));
744                }
745                let ins = s.ins.clone();
746                let tiles = block.acc_tiles();
747                let nb = *num_blocks;
748                let sid = s.id;
749                if is_streaming(&plan) {
750                    // Accumulate every K step in the register file, pack once.
751                    let k_body = tagged(
752                        sid,
753                        vec![
754                            ChoreoOp::Wait {
755                                buf: ins[0],
756                                pages: 1,
757                            },
758                            ChoreoOp::Wait {
759                                buf: ins[1],
760                                pages: 1,
761                            },
762                            ChoreoOp::Compute {
763                                op: op.clone(),
764                                ins: ins.clone(),
765                                acc_reload: None,
766                            },
767                            ChoreoOp::Pop {
768                                buf: ins[0],
769                                pages: 1,
770                            },
771                            ChoreoOp::Pop {
772                                buf: ins[1],
773                                pages: 1,
774                            },
775                        ],
776                    );
777                    let mut seq = vec![tag(ChoreoOp::AcquireAcc { tiles })];
778                    if nb > 1 {
779                        seq.push(tag(ChoreoOp::Loop {
780                            var: LV_KBLOCK,
781                            count: LoopCount::Const(nb),
782                            body: k_body,
783                        }));
784                    } else {
785                        seq.extend(k_body);
786                    }
787                    seq.extend(fuse(tagged(
788                        sid,
789                        vec![
790                            ChoreoOp::Reserve { buf: out, pages: 1 },
791                            ChoreoOp::PackAcc {
792                                to: out,
793                                tiles,
794                                convert: out_dtype,
795                                accumulate: false,
796                            },
797                            ChoreoOp::Push { buf: out, pages: 1 },
798                            ChoreoOp::ReleaseAcc,
799                        ],
800                    )));
801                    ops.extend(seq);
802                    continue;
803                }
804                let waits = tagged(
805                    sid,
806                    vec![
807                        ChoreoOp::Wait {
808                            buf: ins[0],
809                            pages: 1,
810                        },
811                        ChoreoOp::Wait {
812                            buf: ins[1],
813                            pages: 1,
814                        },
815                    ],
816                );
817                let pops = tagged(
818                    sid,
819                    vec![
820                        ChoreoOp::Pop {
821                            buf: ins[0],
822                            pages: 1,
823                        },
824                        ChoreoOp::Pop {
825                            buf: ins[1],
826                            pages: 1,
827                        },
828                    ],
829                );
830                if nb > 1 && interm.is_none() {
831                    // ★ A REGISTER-RESIDENT ACCUMULATOR: the partial never touches memory, so the
832                    // whole K reduction runs inside ONE accumulate region and packs once at the end
833                    // — the same shape as streaming, but over a real block instead of one tile.
834                    //
835                    // This is the GPU. Its `wmma` fragments live in registers for the duration, which
836                    // is why the CUDA lowering refuses an accumulator spill outright; a plan that
837                    // spilled here would emit a construct the backend cannot lower. The tiling pass
838                    // guarantees `sub_blocks == (1, 1)` on such a target, so this single accumulator
839                    // set covers the block.
840                    let k_body = waits
841                        .iter()
842                        .cloned()
843                        .chain(tagged(sid, sub_block_body_regs(s, op, &ins, tiles)))
844                        .chain(pops.iter().cloned())
845                        .collect::<Vec<_>>();
846                    let mut seq = vec![tag(ChoreoOp::AcquireAcc { tiles })];
847                    seq.push(tag(ChoreoOp::Loop {
848                        var: LV_KBLOCK,
849                        count: LoopCount::Const(nb),
850                        body: k_body,
851                    }));
852                    seq.extend(fuse(tagged(
853                        sid,
854                        vec![
855                            ChoreoOp::Reserve { buf: out, pages: 1 },
856                            ChoreoOp::PackAcc {
857                                to: out,
858                                tiles,
859                                convert: out_dtype,
860                                accumulate: false,
861                            },
862                            ChoreoOp::Push { buf: out, pages: 1 },
863                            ChoreoOp::ReleaseAcc,
864                        ],
865                    )));
866                    ops.extend(seq);
867                    continue;
868                }
869                if nb == 1 {
870                    let body = sub_loops(
871                        *sub_blocks,
872                        fuse(tagged(
873                            sid,
874                            sub_block_body(s, op, &ins, None, out, tiles, out_dtype),
875                        )),
876                    );
877                    ops.extend(waits.into_iter().chain(body).chain(pops));
878                } else if let Some(interm) = interm.filter(|i| p.buffer(*i).alias_of == Some(out)) {
879                    // ★ THE PARTIAL IS THE RESULT. The intermediate shares the output's memory page for page, so
880                    // every block accumulates in place and the last one packs into the output itself — the page
881                    // the writer stores — with no reload. The item's output pages are reserved before any partial
882                    // lands in their memory: the reservation waits until the writer has stored the previous
883                    // item's, and the last block packs into it without reserving again.
884                    let interm_dtype = p.buffer(interm).dtype;
885                    let subs = sub_blocks.0 * sub_blocks.1;
886                    let drain: Vec<TaggedOp> = tagged(
887                        sid,
888                        (0..subs)
889                            .flat_map(|_| {
890                                [
891                                    ChoreoOp::Wait {
892                                        buf: interm,
893                                        pages: 1,
894                                    },
895                                    ChoreoOp::Pop {
896                                        buf: interm,
897                                        pages: 1,
898                                    },
899                                ]
900                            })
901                            .collect(),
902                    );
903                    let mut seq = vec![tag(ChoreoOp::Reserve {
904                        buf: out,
905                        pages: subs,
906                    })];
907                    // Block 0: fresh pages, drained so the write pointer returns to the first.
908                    seq.extend(waits.clone());
909                    seq.extend(sub_loops(
910                        *sub_blocks,
911                        tagged(
912                            sid,
913                            sub_block_body_acc(
914                                s,
915                                op,
916                                &ins,
917                                None,
918                                interm,
919                                tiles,
920                                interm_dtype,
921                                false,
922                            ),
923                        ),
924                    ));
925                    seq.extend(drain.clone());
926                    seq.extend(pops.clone());
927                    // Blocks 1…nb-2: accumulate, drained.
928                    if nb > 2 {
929                        let mut mid = waits.clone();
930                        mid.extend(sub_loops(
931                            *sub_blocks,
932                            tagged(
933                                sid,
934                                sub_block_body_acc(
935                                    s,
936                                    op,
937                                    &ins,
938                                    None,
939                                    interm,
940                                    tiles,
941                                    interm_dtype,
942                                    true,
943                                ),
944                            ),
945                        ));
946                        mid.extend(drain.clone());
947                        mid.extend(pops.clone());
948                        seq.push(tag(ChoreoOp::Loop {
949                            var: LV_KBLOCK,
950                            count: LoopCount::Const(nb - 2),
951                            body: mid,
952                        }));
953                    }
954                    // Block nb-1: accumulate into the output's page, which holds the partial, and hand it on.
955                    seq.extend(waits);
956                    seq.extend(sub_loops(
957                        *sub_blocks,
958                        tagged(
959                            sid,
960                            vec![
961                                ChoreoOp::AcquireAcc { tiles },
962                                ChoreoOp::Compute {
963                                    op: op.clone(),
964                                    ins: ins.clone(),
965                                    acc_reload: None,
966                                },
967                                ChoreoOp::PackAcc {
968                                    to: out,
969                                    tiles,
970                                    convert: out_dtype,
971                                    accumulate: true,
972                                },
973                                ChoreoOp::Push { buf: out, pages: 1 },
974                                ChoreoOp::ReleaseAcc,
975                            ],
976                        ),
977                    ));
978                    seq.extend(pops);
979                    ops.extend(seq);
980                } else if plan.pack_accumulate {
981                    // The packer accumulates the partials in place: blocks 0…nb-2 add into the intermediate
982                    // page and the register file is reloaded exactly once, at the last block. Every block but
983                    // the last two drains its pages so the intermediate write pointer returns to the page it
984                    // is accumulating into (the vendor's `pack_reconfig_l1_acc` discipline).
985                    let interm = interm.expect("a multi-block plan spills");
986                    let interm_dtype = p.buffer(interm).dtype;
987                    let subs = sub_blocks.0 * sub_blocks.1;
988                    let drain: Vec<TaggedOp> = tagged(
989                        sid,
990                        (0..subs)
991                            .flat_map(|_| {
992                                [
993                                    ChoreoOp::Wait {
994                                        buf: interm,
995                                        pages: 1,
996                                    },
997                                    ChoreoOp::Pop {
998                                        buf: interm,
999                                        pages: 1,
1000                                    },
1001                                ]
1002                            })
1003                            .collect(),
1004                    );
1005                    let mut seq = Vec::new();
1006                    // Block 0: fresh pages (replace), then drain.
1007                    seq.extend(waits.clone());
1008                    seq.extend(sub_loops(
1009                        *sub_blocks,
1010                        tagged(
1011                            sid,
1012                            sub_block_body_acc(
1013                                s,
1014                                op,
1015                                &ins,
1016                                None,
1017                                interm,
1018                                tiles,
1019                                interm_dtype,
1020                                false,
1021                            ),
1022                        ),
1023                    ));
1024                    seq.extend(drain.clone());
1025                    seq.extend(pops.clone());
1026                    // Blocks 1…nb-3: accumulate, then drain.
1027                    if nb > 3 {
1028                        let mut mid = waits.clone();
1029                        mid.extend(sub_loops(
1030                            *sub_blocks,
1031                            tagged(
1032                                sid,
1033                                sub_block_body_acc(
1034                                    s,
1035                                    op,
1036                                    &ins,
1037                                    None,
1038                                    interm,
1039                                    tiles,
1040                                    interm_dtype,
1041                                    true,
1042                                ),
1043                            ),
1044                        ));
1045                        mid.extend(drain.clone());
1046                        mid.extend(pops.clone());
1047                        seq.push(tag(ChoreoOp::Loop {
1048                            var: LV_KBLOCK,
1049                            count: LoopCount::Const(nb - 3),
1050                            body: mid,
1051                        }));
1052                    }
1053                    // Block nb-2: accumulate and LEAVE the pages for the reload.
1054                    seq.extend(waits.clone());
1055                    seq.extend(sub_loops(
1056                        *sub_blocks,
1057                        tagged(
1058                            sid,
1059                            sub_block_body_acc(
1060                                s,
1061                                op,
1062                                &ins,
1063                                None,
1064                                interm,
1065                                tiles,
1066                                interm_dtype,
1067                                true,
1068                            ),
1069                        ),
1070                    ));
1071                    seq.extend(pops.clone());
1072                    // Block nb-1: the single reload, packed to the output.
1073                    seq.extend(waits);
1074                    seq.extend(sub_loops(
1075                        *sub_blocks,
1076                        fuse(tagged(
1077                            sid,
1078                            sub_block_body(s, op, &ins, Some(interm), out, tiles, out_dtype),
1079                        )),
1080                    ));
1081                    seq.extend(pops);
1082                    ops.extend(seq);
1083                } else {
1084                    let interm = interm.expect("a multi-block plan spills");
1085                    let interm_dtype = p.buffer(interm).dtype;
1086                    let mut seq = Vec::new();
1087                    // Block 0: fresh accumulators, spill.
1088                    seq.extend(waits.clone());
1089                    seq.extend(sub_loops(
1090                        *sub_blocks,
1091                        tagged(
1092                            sid,
1093                            sub_block_body(s, op, &ins, None, interm, tiles, interm_dtype),
1094                        ),
1095                    ));
1096                    seq.extend(pops.clone());
1097                    // Middle blocks: reload, spill.
1098                    if nb > 2 {
1099                        let mut mid = waits.clone();
1100                        mid.extend(sub_loops(
1101                            *sub_blocks,
1102                            tagged(
1103                                sid,
1104                                sub_block_body(
1105                                    s,
1106                                    op,
1107                                    &ins,
1108                                    Some(interm),
1109                                    interm,
1110                                    tiles,
1111                                    interm_dtype,
1112                                ),
1113                            ),
1114                        ));
1115                        mid.extend(pops.clone());
1116                        seq.push(tag(ChoreoOp::Loop {
1117                            var: LV_KBLOCK,
1118                            count: LoopCount::Const(nb - 2),
1119                            body: mid,
1120                        }));
1121                    }
1122                    // Last block: reload, pack to the output.
1123                    seq.extend(waits);
1124                    seq.extend(sub_loops(
1125                        *sub_blocks,
1126                        fuse(tagged(
1127                            sid,
1128                            sub_block_body(s, op, &ins, Some(interm), out, tiles, out_dtype),
1129                        )),
1130                    ));
1131                    seq.extend(pops);
1132                    ops.extend(seq);
1133                }
1134            }
1135            StageKind::Compute(_) | StageKind::Scalar(_) | StageKind::AllReduce { .. } => {
1136                // Chain stages are emitted together below (one page loop, every stage per page).
1137                continue;
1138            }
1139            _ => {}
1140        }
1141    }
1142    // A chain that CARRIES a value across K blocks is a fold: its prologue, its blocks, its epilogue.
1143    let carries = p.carries();
1144    if !fused && chain.iter().any(|s| carries.values().any(|c| *c == s.id)) {
1145        let sub =
1146            (plan.per_site_m / plan.block.osh.max(1)) * (plan.per_site_n / plan.block.osw.max(1));
1147        if sub > 1 {
1148            return Err(SpatialReject::NotLowered("a chain that carries a value across K blocks over more than one output sub-block of its item"));
1149        }
1150        ops.extend(fold_ops(p, &chain, levels, plan.inner.num_blocks));
1151        return Ok(ops);
1152    }
1153    // A chain with no matmul ahead of it (a pure scalar or elementwise site) keeps its own page loop.
1154    if !chain.is_empty() && !fused {
1155        let pages = chain
1156            .iter()
1157            .flat_map(|s| s.ins.iter().map(|b| pages_per_item(p, *b, 0)))
1158            .max()
1159            .unwrap_or(1);
1160        let body = chain_body;
1161        if pages > 1 {
1162            let stage = body[0].stage;
1163            ops.push(TaggedOp {
1164                stage,
1165                op: ChoreoOp::Loop {
1166                    var: LV_SUB_H,
1167                    count: LoopCount::Const(pages),
1168                    body,
1169                },
1170            });
1171        } else {
1172            ops.extend(body);
1173        }
1174    }
1175    Ok(ops)
1176}
1177
1178/// The reader engine program of a site: the operand loads/receives (A before B) inside the K-block loop —
1179/// always a loop, as the vendor's readers are — then any per-sub-block loads (a bias row) in the
1180/// sub-block loops, then any once-per-item loads.
1181fn reader_program(
1182    p: &SpatialProgram,
1183    target: &SpatialTarget,
1184    here: &[&Stage],
1185    rank: &[usize],
1186    engine: Engine,
1187) -> Vec<TaggedOp> {
1188    let pl = p.placement.as_ref().unwrap();
1189    let mut stages: Vec<&Stage> = here
1190        .iter()
1191        .copied()
1192        .filter(|s| {
1193            pl.engine_of[s.id.idx()].engine == engine
1194                && matches!(s.kind, StageKind::Load { .. } | StageKind::Recv { .. })
1195        })
1196        .collect();
1197    stages.sort_by_key(|s| rank[s.id.idx()]);
1198    let stages = stages;
1199    if stages.is_empty() {
1200        return Vec::new();
1201    }
1202    let plan = p.work.plan;
1203    let nb = plan.map(|pl| pl.inner.num_blocks).unwrap_or(1);
1204    let sub_blocks = plan
1205        .map(|pl| {
1206            (
1207                (pl.per_site_m / pl.block.osh.max(1)).max(1),
1208                (pl.per_site_n / pl.block.osw.max(1)).max(1),
1209            )
1210        })
1211        .unwrap_or((1, 1));
1212    let uses = |s: &Stage, var: LoopVar| match &s.kind {
1213        StageKind::Load { pattern, .. } => pattern.origin.steps.iter().any(|(v, _)| *v == var),
1214        StageKind::Recv { .. } => var == LV_KBLOCK,
1215        _ => false,
1216    };
1217    let (mut k_block, mut sub_block, mut once) = (Vec::new(), Vec::new(), Vec::new());
1218    for s in &stages {
1219        let is_k = uses(s, LV_KBLOCK) || matches!(s.kind, StageKind::Recv { .. });
1220        let is_sub = uses(s, LV_SUB_H) || uses(s, LV_SUB_W);
1221        let target_vec = if is_k {
1222            &mut k_block
1223        } else if is_sub {
1224            &mut sub_block
1225        } else {
1226            &mut once
1227        };
1228        reader_block(p, target, s, target_vec);
1229    }
1230    // ★ WHAT IS LOADED ONCE IS LOADED FIRST. A once-per-item operand may be what the compute needs before its first
1231    // block — a fold's prologue reads it — and a reader that issued it after its block loop would fill the block
1232    // buffers and wait forever on a compute that waits for it.
1233    let mut ops = once;
1234    if let Some(first) = k_block.first() {
1235        let stage = first.stage;
1236        ops.push(TaggedOp {
1237            stage,
1238            op: ChoreoOp::Loop {
1239                var: LV_KBLOCK,
1240                count: k_count(p, nb),
1241                body: k_block,
1242            },
1243        });
1244    }
1245    if !sub_block.is_empty() {
1246        ops.extend(sub_loops(sub_blocks, sub_block));
1247    }
1248    ops
1249}
1250
1251/// The writer engine program of a site: one store per sub-block.
1252fn writer_program(
1253    p: &SpatialProgram,
1254    here: &[&Stage],
1255    rank: &[usize],
1256    engine: Engine,
1257) -> Vec<TaggedOp> {
1258    let pl = p.placement.as_ref().unwrap();
1259    let mut ops = Vec::new();
1260    let mut engine_stages: Vec<&Stage> = here
1261        .iter()
1262        .copied()
1263        .filter(|s| pl.engine_of[s.id.idx()].engine == engine)
1264        .collect();
1265    engine_stages.sort_by_key(|s| rank[s.id.idx()]);
1266    for s in engine_stages {
1267        if let StageKind::Store {
1268            tensor,
1269            pattern,
1270            sub_blocks,
1271        } = &s.kind
1272        {
1273            let buf = s.ins[0];
1274            // A scatter (an `IndirectTile` store) waits for its index as a gather does, and retires it after.
1275            let index = pattern.origin.indirect.map(|i| i.index);
1276            let mut body = vec![ChoreoOp::Wait { buf, pages: 1 }];
1277            if let Some(ix) = index {
1278                body.push(ChoreoOp::Wait { buf: ix, pages: 1 });
1279            }
1280            body.extend([
1281                ChoreoOp::WriteTiles {
1282                    from: buf,
1283                    tensor: *tensor,
1284                    at: pattern.origin.clone(),
1285                    block_h: pattern.block_h,
1286                    block_w: pattern.block_w,
1287                    stride_w: pattern.stride_w,
1288                    stride_h: pattern.stride_h,
1289                },
1290                ChoreoOp::WriteBarrier,
1291                ChoreoOp::Pop { buf, pages: 1 },
1292            ]);
1293            if let Some(ix) = index {
1294                body.push(ChoreoOp::Pop { buf: ix, pages: 1 });
1295            }
1296            ops.extend(sub_loops(*sub_blocks, tagged(s.id, body)));
1297        }
1298    }
1299    ops
1300}
1301
1302/// The `synthesize_handshakes` pass.
1303pub fn synthesize_handshakes(
1304    p: &SpatialProgram,
1305    target: &SpatialTarget,
1306) -> Result<SpatialProgram, SpatialReject> {
1307    if p.schedule.is_some() {
1308        return Ok(p.clone());
1309    }
1310    let Some(pl) = &p.placement else {
1311        return Err(SpatialReject::WrongPhase {
1312            pass: "synthesize_handshakes",
1313            expected: "Sized",
1314            found: p.phase(),
1315        });
1316    };
1317    if p.memory.is_none() {
1318        return Err(SpatialReject::WrongPhase {
1319            pass: "synthesize_handshakes",
1320            expected: "Sized",
1321            found: p.phase(),
1322        });
1323    }
1324    // The template's abstract local hand-off becomes the kind this target provides; every other kind was put
1325    // there by a pass that already checked the target.
1326    let local = crate::spatial::passes::from_tensor_op::local_handshake(target)?;
1327    let mut p = p.clone();
1328    for e in &mut p.edges {
1329        if e.handshake == Handshake::Fifo && local != Handshake::Fifo {
1330            e.handshake = local.clone();
1331        }
1332    }
1333    p.requires.handshakes = p.edges.iter().map(|e| e.handshake.kind()).collect();
1334    let p = &p;
1335    for e in &p.edges {
1336        let k = e.handshake.kind();
1337        if !target.provides_handshake(k) {
1338            return Err(SpatialReject::MissingHandshake {
1339                needed: k,
1340                target: target.name,
1341            });
1342        }
1343    }
1344    // Every stage's dataflow rank and each site's stages, derived once: each engine program of each site is built from
1345    // its own site's stages, not from a walk of the whole program.
1346    let rank = stage_ranks(p);
1347    let levels = if p.carries().is_empty() {
1348        vec![FoldLevel::Prologue; p.stages.len()]
1349    } else {
1350        p.fold_levels()
1351    };
1352    let mut on_site: BTreeMap<Site, Vec<&Stage>> = BTreeMap::new();
1353    for s in &p.stages {
1354        if let Some(site) = s.site {
1355            on_site.entry(site).or_default().push(s);
1356        }
1357    }
1358    let mut programs = Vec::new();
1359    let sites: Vec<Site> = pl.items_of_site.iter().map(|(s, _)| *s).collect();
1360    for site in sites {
1361        let here: &[&Stage] = on_site.get(&site).map(Vec::as_slice).unwrap_or(&[]);
1362        let mut engines: Vec<Engine> = here
1363            .iter()
1364            .map(|s| pl.engine_of[s.id.idx()].engine)
1365            .collect();
1366        engines.sort();
1367        engines.dedup();
1368        for engine in engines {
1369            let ops = match engine {
1370                Engine::Compute => compute_program(p, here, &rank, &levels)?,
1371                Engine::Movement(_) => {
1372                    let mut ops = reader_program(p, target, here, &rank, engine);
1373                    ops.extend(writer_program(p, here, &rank, engine));
1374                    fuse_adjacent_loops(ops)
1375                }
1376            };
1377            if ops.is_empty() {
1378                continue;
1379            }
1380            let stages: Vec<StageId> = here
1381                .iter()
1382                .filter(|s| pl.engine_of[s.id.idx()].engine == engine)
1383                .map(|s| s.id)
1384                .collect();
1385            programs.push(EngineProgram {
1386                site,
1387                engine,
1388                stages,
1389                ops,
1390            });
1391        }
1392    }
1393    let mut q = p.clone();
1394    q.schedule = Some(Schedule { programs });
1395    q.validate()?;
1396    Ok(q)
1397}