Skip to main content

logicaffeine_compile/codegen_sva/
metapipeline.rs

1//! The **metapipeline initiation-interval (II) model** — P1 of the Plasticine lane, and the question an RDU
2//! engineer asks first: how often can a pipelined stage accept a new iteration, and how deep must the
3//! buffers between stages be?
4//!
5//! Definitions follow Spatial (Koeplinger et al., PLDI 2018, §4): each primitive has a **resource II** (1 for
6//! a fully pipelined unit, its occupancy for a sequential one); a loop-carried dependency has a
7//! **recurrence II** equal to the path length from the read of a carried value to its write; a controller's
8//! II is the maximum of all resource IIs and all recurrence IIs; an outer (metapipelined) controller's
9//! throughput is set by its slowest stage; and a memory written in stage `i` and read in stage `j` needs
10//! `j - i + 1` buffers (N-buffering — adjacent stages double-buffer).
11//!
12//! The model is built FROM the certified fabric IR ([`LoopFabric`]): the body is the stage's datapath, the
13//! carried state registers are the recurrences, and [`UnitModel`] supplies per-cell latency / occupancy.
14//! [`eval_loop_fabric_at_ii`] is the executable meaning of the number: a cycle-timed pipelined execution that
15//! issues one iteration every `ii` cycles and reads whatever a carried register holds at issue time — at the
16//! modeled II it reproduces the sequential oracle exactly, one cycle faster it reads stale state (a hazard),
17//! so `metapipeline_lock` proves the II is neither optimistic nor vacuous. MODELED: cycle counts here are the
18//! model's, never a Plasticine simulation.
19
20use std::collections::HashMap;
21
22use super::fabric_rtl::{eval_loop_body, FuncUnit, LoopCell, LoopFabric, Src};
23use super::fabric_target::{Capability, FabricTarget, Unplaceable};
24
25/// Per-cell timing: how many cycles a cell's result takes (`latency`) and how many cycles the cell's unit is
26/// busy per iteration (`occupancy`, the resource II). A fully pipelined unit has occupancy 1 whatever its
27/// latency; a sequential unit (an iterative divider) has occupancy == latency.
28#[derive(Clone, Debug, PartialEq)]
29pub struct UnitModel {
30    /// Latency / occupancy of every combinational tile (arith, select, vector, memory read).
31    pub tile_latency: usize,
32    /// Per functional unit: (latency, occupancy). Units absent here default to `default_func`.
33    pub func: HashMap<FuncUnit, (usize, usize)>,
34    pub default_func: (usize, usize),
35    /// The unit's pipeline depth: a result leaves the unit no earlier than this many cycles after its
36    /// inputs enter, however short its datapath (`0` for an ideal unit whose latency is the datapath's).
37    pub pipeline_depth: usize,
38    /// Cycles between a carried register's write (when its value leaves the pipeline) and the next
39    /// iteration's read of it: the turnaround a loop-carried dependency pays. `0` for ideal forwarding.
40    pub register_turnaround: usize,
41    /// Where the numbers come from.
42    pub cite: &'static str,
43}
44
45impl UnitModel {
46    /// The ideal fully pipelined PCU: every tile and FU accepts a new operand every cycle (Plasticine ISCA'17
47    /// §3.1: "multi-stage, reconfigurable SIMD pipeline" — one stage per FU, pipeline registers between).
48    pub fn ideal_pipelined() -> UnitModel {
49        UnitModel { tile_latency: 1, func: HashMap::new(), default_func: (1, 1), pipeline_depth: 0, register_turnaround: 0, cite: "Plasticine ISCA'17 §3.1: pipelined FU stages; latency 1 / occupancy 1 per stage, ideal register forwarding (modeled)" }
50    }
51
52    /// The ISCA'17 PCU as the public toolchain simulates it: one-cycle FU stages, a result leaves the
53    /// `stages`-deep pipeline `stages` cycles after entering it however short the datapath, and a carried
54    /// register written at the pipeline's end is read at its start one cycle later — so any loop-carried
55    /// dependency that fits one PCU costs `stages + 1` per iteration. CALIBRATED against Tungsten on our own
56    /// emitted loops (`benchmarks/silicon/plasticine_calibration.json`): on the 6-stage PCU, 1,000 iterations
57    /// of `s = s + i` simulate in 7,003 cycles, 30 of `s = s*3 + i` in 213, 12 of a four-op chain in 87, and
58    /// 1,000 iterations with no carried dependency in 1,009.
59    pub fn plasticine_pcu(stages: usize) -> UnitModel {
60        UnitModel { tile_latency: 1, func: HashMap::new(), default_func: (1, 1), pipeline_depth: stages, register_turnaround: 1, cite: "Plasticine ISCA'17 six-stage PCU; result latency = pipeline depth, carried-register turnaround 1 — calibrated on Tungsten (plasticine_calibration.json)" }
61    }
62
63    /// Our own fabric's units: the shared iterative divider family is SEQUENTIAL (occupancy = latency =
64    /// `bits` cycles, one quotient bit per cycle); everything else is a one-cycle tile.
65    pub fn sky_fabric(bits: usize) -> UnitModel {
66        let mut func = HashMap::new();
67        for u in [
68            FuncUnit::DivS,
69            FuncUnit::ModS,
70            FuncUnit::FloorDivS,
71            FuncUnit::Pow,
72            FuncUnit::FDivF,
73            FuncUnit::FSqrtF,
74        ] {
75            func.insert(u, (bits, bits));
76        }
77        UnitModel { tile_latency: 1, func, default_func: (1, 1), pipeline_depth: 0, register_turnaround: 0, cite: "codegen_sva shared_divider: bit-serial, `bits` cycles per divide (RTL); other units one-cycle tiles" }
78    }
79
80    pub fn latency(&self, cell: &LoopCell) -> usize {
81        match cell {
82            LoopCell::Func { unit, .. } => {
83                self.func.get(unit).copied().unwrap_or(self.default_func).0
84            }
85            _ => self.tile_latency,
86        }
87    }
88
89    pub fn occupancy(&self, cell: &LoopCell) -> usize {
90        match cell {
91            LoopCell::Func { unit, .. } => {
92                self.func.get(unit).copied().unwrap_or(self.default_func).1
93            }
94            _ => 1,
95        }
96    }
97}
98
99/// One metapipeline stage's timing.
100#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
101pub struct Stage {
102    pub name: String,
103    /// max over the stage's units of their occupancy.
104    pub resource_ii: usize,
105    /// max over carried registers of the read→write path length (0 when nothing is loop-carried).
106    pub recurrence_ii: usize,
107    /// The stage's critical path in cycles (first input to last output of one iteration).
108    pub latency: usize,
109    /// Cells in the stage's datapath (what a PCU allocation must hold).
110    pub cells: usize,
111    /// The costliest vector memory access per iteration in cycles (PMU bank conflicts; 1 = conflict-free).
112    pub memory_ii: usize,
113}
114
115impl Stage {
116    /// The stage's initiation interval: `max(resource II, recurrence II, memory II, 1)`.
117    pub fn ii(&self) -> usize {
118        self.resource_ii
119            .max(self.recurrence_ii)
120            .max(self.memory_ii)
121            .max(1)
122    }
123}
124
125/// A metapipeline: stages in issue order, with the producer→consumer buffer edges between them.
126#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
127pub struct Metapipeline {
128    pub stages: Vec<Stage>,
129    /// `(writer_stage, reader_stage)` pairs, writer < reader.
130    pub edges: Vec<(usize, usize)>,
131}
132
133impl Metapipeline {
134    /// The pipeline's II: its slowest stage.
135    pub fn ii(&self) -> usize {
136        self.stages.iter().map(Stage::ii).max().unwrap_or(1)
137    }
138    /// Iterations per cycle at steady state.
139    pub fn throughput(&self) -> f64 {
140        1.0 / self.ii() as f64
141    }
142    /// Spatial's N-buffer rule: a memory written in stage `writer` and read in stage `reader` needs
143    /// `reader - writer + 1` buffers (adjacent stages: 2, i.e. double-buffering).
144    pub fn buffer_depth(writer: usize, reader: usize) -> usize {
145        assert!(
146            reader >= writer,
147            "a buffer's reader stage ({reader}) cannot precede its writer ({writer})"
148        );
149        reader - writer + 1
150    }
151    /// `(writer, reader, depth)` for every edge.
152    pub fn buffer_depths(&self) -> Vec<(usize, usize, usize)> {
153        self.edges
154            .iter()
155            .map(|&(w, r)| (w, r, Self::buffer_depth(w, r)))
156            .collect()
157    }
158    /// Cycles to run `iters` iterations end to end: fill (sum of stage latencies) + `(iters - 1) * II`.
159    pub fn cycles(&self, iters: usize) -> usize {
160        let fill: usize = self.stages.iter().map(|s| s.latency).sum();
161        if iters == 0 {
162            0
163        } else {
164            fill + (iters - 1) * self.ii()
165        }
166    }
167    /// Gate the pipeline's depth against a target's `Metapipeline` capability — a typed refusal.
168    pub fn fits(&self, target: &FabricTarget) -> Result<(), Unplaceable> {
169        let cap = Capability::Metapipeline(self.stages.len());
170        if target.provides(cap) {
171            Ok(())
172        } else {
173            Err(Unplaceable::MissingCapability {
174                needed: cap,
175                target: target.name,
176            })
177        }
178    }
179}
180
181/// Longest-path depth (in cycles, per `model`) of every SSA value of `lf.body`, and the depth at which each
182/// carried register's next value is ready, counted from the reads of the carried state (`Src::In(k)`,
183/// `k < n_state`). A value that does not depend on any carried state has state-depth 0.
184fn depths(lf: &LoopFabric, model: &UnitModel) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
185    let n = lf.body.len();
186    let mut any_depth = vec![0usize; n]; // from any input
187    let mut state_depth = vec![0usize; n]; // from a carried-state read (0 = independent of state)
188    let src_any = |s: &Src, any: &Vec<usize>| -> usize {
189        match s {
190            Src::Val(v) => any[*v],
191            _ => 0,
192        }
193    };
194    let src_state = |s: &Src, st: &Vec<usize>| -> Option<usize> {
195        match s {
196            Src::In(k) if *k < lf.n_state => Some(0),
197            Src::Val(v) => {
198                if st[*v] > 0 {
199                    Some(st[*v])
200                } else {
201                    None
202                }
203            }
204            _ => None,
205        }
206    };
207    for (i, cell) in lf.body.iter().enumerate() {
208        let srcs: Vec<Src> = match cell {
209            LoopCell::Arith { a, b, .. }
210            | LoopCell::Func { a, b, .. }
211            | LoopCell::VectorArith { a, b, .. } => vec![*a, *b],
212            LoopCell::Select { ca, cb, t, e, .. } => vec![*ca, *cb, *t, *e],
213            LoopCell::MemRead { addr } | LoopCell::HeapRead { addr } => vec![*addr],
214            LoopCell::HeapWrite { addr, val } => vec![*addr, *val],
215            LoopCell::HeapAlloc { size } => vec![*size],
216        };
217        let lat = model.latency(cell);
218        any_depth[i] = srcs
219            .iter()
220            .map(|s| src_any(s, &any_depth))
221            .max()
222            .unwrap_or(0)
223            + lat;
224        let st = srcs.iter().filter_map(|s| src_state(s, &state_depth)).max();
225        state_depth[i] = st.map(|d| d + lat).unwrap_or(0);
226    }
227    // A carried register's next value leaves the unit `max(path depth, pipeline depth)` cycles after its read
228    // and is readable `register_turnaround` later; a register that holds (`s' = s`) or is overwritten by
229    // something state-independent has no carried path at all.
230    let next_state_depth: Vec<usize> = lf
231        .next
232        .iter()
233        .map(|s| match s {
234            Src::In(k) if *k < lf.n_state => 0,
235            Src::Val(v) => {
236                if state_depth[*v] > 0 {
237                    state_depth[*v].max(model.pipeline_depth) + model.register_turnaround
238                } else {
239                    0
240                }
241            }
242            _ => 0,
243        })
244        .collect();
245    (any_depth, state_depth, next_state_depth)
246}
247
248/// Model one counted loop as ONE metapipeline stage.
249pub fn loop_stage(lf: &LoopFabric, model: &UnitModel, name: &str) -> Stage {
250    let (any_depth, _, next_depth) = depths(lf, model);
251    Stage {
252        name: name.to_string(),
253        resource_ii: lf
254            .body
255            .iter()
256            .map(|c| model.occupancy(c))
257            .max()
258            .unwrap_or(1),
259        recurrence_ii: next_depth.iter().copied().max().unwrap_or(0),
260        latency: any_depth
261            .iter()
262            .copied()
263            .max()
264            .unwrap_or(0)
265            .max(model.pipeline_depth)
266            .max(1),
267        cells: lf.body.len(),
268        // A counted loop's `MemRead` streams the sequence at stride 1 across iterations: conflict-free under
269        // strided banking at granularity 1 (memory II 1). Explicit vector patterns come through `pmu::memory_ii`.
270        memory_ii: 1,
271    }
272}
273
274/// The result of a cycle-timed pipelined run of a counted loop.
275#[derive(Clone, Debug, PartialEq, Eq)]
276pub struct PipelinedRun {
277    pub value: i64,
278    /// Iterations that read a carried register before the previous iteration had written it.
279    pub hazards: usize,
280    pub iterations: usize,
281    pub cycles: usize,
282}
283
284/// Execute a counted loop as a pipeline issuing one iteration every `ii` cycles. Iteration `t` issues at
285/// cycle `t*ii`, reads each carried register's value as of that cycle (the latest write whose completion
286/// cycle `<=` the issue cycle), and completes register `k`'s write at `t*ii + depth_k`. With `ii >=` the
287/// recurrence II every read sees the previous iteration's write and the result equals
288/// [`eval_loop_fabric`](super::fabric_rtl::eval_loop_fabric); below it, reads are stale (hazards).
289pub fn eval_loop_fabric_at_ii(
290    lf: &LoopFabric,
291    params: &[i64],
292    ii: usize,
293    model: &UnitModel,
294) -> PipelinedRun {
295    assert!(ii >= 1, "an initiation interval is at least one cycle");
296    assert!(lf.seq_input.is_none() && lf.seq_heap_ptr.is_none() && lf.epilogue.is_empty(),
297            "eval_loop_fabric_at_ii models counted range loops (sequence / heap loops and epilogues are the sequential oracle's)");
298    let empty: [i64; 0] = [];
299    let (_, _, next_depth) = depths(lf, model);
300    let init: Vec<i64> = lf.init.iter().map(|s| s.eval(params, &empty)).collect();
301    let invs: Vec<i64> = lf
302        .invariants
303        .iter()
304        .map(|s| s.eval(params, &empty))
305        .collect();
306    let lo = lf.lo.eval(params, &empty);
307    let hi = lf.hi.eval(params, &empty);
308    // Per register: the history of (completion_cycle, value) writes, initial value at cycle 0.
309    let mut writes: Vec<Vec<(usize, i64)>> = init.iter().map(|&v| vec![(0usize, v)]).collect();
310    let mut hazards = 0usize;
311    let mut iterations = 0usize;
312    let mut last_cycle = 0usize;
313    let mut iv = lo;
314    let mut t = 0usize;
315    while iv <= hi {
316        let issue = t * ii;
317        let mut state = Vec::with_capacity(lf.n_state);
318        for k in 0..lf.n_state {
319            let ready: Vec<&(usize, i64)> = writes[k].iter().filter(|(c, _)| *c <= issue).collect();
320            let (_, v) = ready.last().copied().copied().unwrap_or((0, init[k]));
321            if t > 0 && writes[k].len() > t {
322                let (prev_done, _) = writes[k][t];
323                if prev_done > issue {
324                    hazards += 1;
325                }
326            }
327            state.push(v);
328        }
329        let mut bin: Vec<i64> = Vec::with_capacity(lf.n_state + 1 + invs.len());
330        bin.extend_from_slice(&state);
331        bin.push(iv);
332        bin.extend_from_slice(&invs);
333        let vals = eval_loop_body(lf, &bin, &[]);
334        for k in 0..lf.n_state {
335            let v = lf.next[k].eval(&bin, &vals);
336            let done = issue + next_depth[k];
337            writes[k].push((done, v));
338            last_cycle = last_cycle.max(done);
339        }
340        iterations += 1;
341        if iv == hi {
342            break;
343        }
344        iv += 1;
345        t += 1;
346    }
347    let value = writes[lf.result_state]
348        .last()
349        .map(|(_, v)| *v)
350        .unwrap_or(init[lf.result_state]);
351    PipelinedRun {
352        value,
353        hazards,
354        iterations,
355        cycles: last_cycle,
356    }
357}