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}