logicaffeine_compile/codegen_sva/pmu.rs
1//! The **PMU banking model** — P2 of the Plasticine lane: the Pattern Memory Unit's scratchpad as the papers
2//! describe it (ISCA'17 §3.2), with an executable meaning.
3//!
4//! A PMU is `banks` SRAM banks of `words_per_bank` 32-bit words (ISCA'17: 16 banks × 16 KB = 256 KB) behind an
5//! address-calculation datapath of `stages` FU stages. Banking modes: **strided** (a configurable granularity
6//! so a stride-`s` vector access spreads across all banks), **FIFO**, **line buffer**, and **duplication**
7//! (every bank holds a copy, so any gather is conflict-free at the cost of broadcast writes). **N-buffering**
8//! partitions each bank's address space into `n_buffers` disjoint regions so a metapipeline's producer writes
9//! region `t+1` while its consumer reads region `t`.
10//!
11//! The executable meaning: [`Pmu`] is banked storage whose reads/writes equal a flat heap's (the banking is a
12//! bijection), and every vector access costs `conflicts` cycles = the most lanes that land in one bank. That
13//! cost is the stage's **memory II** (`metapipeline::Stage::memory_ii`). MODELED from the paper; never a
14//! Plasticine simulation.
15
16/// How addresses map to banks.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
18pub enum BankingMode {
19 /// `bank = (addr / granularity) % banks`: consecutive `granularity`-word chunks rotate through the banks,
20 /// so a vector access with stride == granularity hits every bank once.
21 Strided { granularity: usize },
22 /// A queue: consecutive pushes rotate through the banks (`granularity` 1); pops in push order.
23 Fifo,
24 /// Sliding window over rows of `width` words: `bank = (addr % width) % banks`, so a row's lanes spread.
25 LineBuffer { width: usize },
26 /// Every bank holds a full copy: any gather reads conflict-free; a write is broadcast to all banks.
27 Duplication,
28}
29
30/// A PMU's published shape.
31#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
32pub struct PmuSpec {
33 pub banks: usize,
34 pub words_per_bank: usize,
35 /// N-buffering factor (1 = single buffer, 2 = double buffer, …).
36 pub n_buffers: usize,
37 /// Address-datapath FU stages.
38 pub stages: usize,
39}
40
41impl PmuSpec {
42 /// ISCA'17 Table 3: 16 banks × 16 KB (4096 32-bit words), 4 address-datapath stages, single-buffered.
43 pub fn isca17() -> PmuSpec {
44 PmuSpec {
45 banks: 16,
46 words_per_bank: 4096,
47 n_buffers: 1,
48 stages: 4,
49 }
50 }
51 /// Total words.
52 pub fn capacity_words(&self) -> usize {
53 self.banks * self.words_per_bank
54 }
55 /// Words available to ONE buffer under N-buffering (each bank's space is split `n_buffers` ways; when
56 /// the split is uneven the smallest region is what a tile must fit).
57 pub fn buffer_words(&self) -> usize {
58 self.capacity_words() / self.n_buffers.max(1)
59 }
60 /// The disjoint, exhaustive `[first, last)` word ranges of the N buffers in flat word addresses; the
61 /// remainder of an uneven split goes to the last region so the regions always cover the capacity.
62 pub fn n_buffer_regions(&self) -> Vec<(usize, usize)> {
63 let n = self.n_buffers.max(1);
64 let per = self.capacity_words() / n;
65 (0..n)
66 .map(|i| {
67 (
68 i * per,
69 if i + 1 == n {
70 self.capacity_words()
71 } else {
72 (i + 1) * per
73 },
74 )
75 })
76 .collect()
77 }
78 /// Words a banking mode can address: everything for strided / FIFO / duplication; a line buffer holds
79 /// whole rows, `ceil(width / banks)` words of every row in each bank, so its capacity is the whole rows
80 /// that fit — the rest of the SRAM is unaddressable in that mode.
81 pub fn usable_words(&self, mode: BankingMode) -> usize {
82 match mode {
83 BankingMode::LineBuffer { width } => {
84 let w = width.max(1);
85 let cols_per_bank = (w + self.banks - 1) / self.banks;
86 (self.words_per_bank / cols_per_bank) * w
87 }
88 // Strided banking rotates `granularity`-word chunks through the banks: a bank holds whole chunks,
89 // so the usable capacity is the whole chunk-rounds that fit (`granularity` 1 = everything).
90 BankingMode::Strided { granularity } => {
91 let g = granularity.max(1);
92 (self.words_per_bank / g) * g * self.banks
93 }
94 // Duplication replicates every word into every bank: the usable capacity is ONE bank's worth.
95 BankingMode::Duplication => self.words_per_bank,
96 BankingMode::Fifo => self.capacity_words(),
97 }
98 }
99 /// Does a tile of `words` fit one buffer? Typed refusal otherwise.
100 pub fn tile_fits(&self, words: usize) -> Result<(), String> {
101 if words <= self.buffer_words() {
102 Ok(())
103 } else {
104 Err(format!("a {words}-word tile does not fit one of {} buffers of {} words ({} banks x {} words)", self.n_buffers.max(1), self.buffer_words(), self.banks, self.words_per_bank))
105 }
106 }
107}
108
109/// One vector access: lane `l` reads/writes `base + stride * l` for `l` in `0..lanes`.
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub struct AffineAccess {
112 pub base: usize,
113 pub stride: usize,
114 pub lanes: usize,
115}
116
117impl AffineAccess {
118 pub fn addrs(&self) -> Vec<usize> {
119 (0..self.lanes)
120 .map(|l| self.base + self.stride * l)
121 .collect()
122 }
123}
124
125/// The bank an address lives in under `mode`.
126pub fn bank_of(addr: usize, mode: BankingMode, banks: usize) -> usize {
127 match mode {
128 BankingMode::Strided { granularity } => (addr / granularity.max(1)) % banks,
129 BankingMode::Fifo => addr % banks,
130 BankingMode::LineBuffer { width } => (addr % width.max(1)) % banks,
131 BankingMode::Duplication => 0,
132 }
133}
134
135/// Cycles a vector access costs: the most lanes that land in one bank (1 = conflict-free). Under
136/// duplication a read is always 1 (every bank has the data; lanes are served from distinct copies).
137pub fn conflicts(access: &AffineAccess, mode: BankingMode, spec: &PmuSpec) -> usize {
138 if access.lanes == 0 {
139 return 0;
140 }
141 if mode == BankingMode::Duplication {
142 return 1;
143 }
144 let mut per_bank = vec![0usize; spec.banks];
145 for a in access.addrs() {
146 per_bank[bank_of(a, mode, spec.banks)] += 1;
147 }
148 per_bank.into_iter().max().unwrap_or(1).max(1)
149}
150
151/// The strided-banking configuration that makes a stride-`stride` vector access conflict-free: granularity
152/// = stride (lane `l` lands in bank `(base/stride + l) % banks`, all distinct while `lanes <= banks`).
153pub fn strided_banking_for(stride: usize) -> BankingMode {
154 BankingMode::Strided {
155 granularity: stride.max(1),
156 }
157}
158
159/// The memory II of a stage: the costliest of its vector accesses per iteration.
160pub fn memory_ii(accesses: &[AffineAccess], mode: BankingMode, spec: &PmuSpec) -> usize {
161 accesses
162 .iter()
163 .map(|a| conflicts(a, mode, spec))
164 .max()
165 .unwrap_or(1)
166 .max(1)
167}
168
169/// FU stages the address datapath needs for an affine address `base + Σ stride_k * idx_k` over `dims`
170/// counters (+1 for the lane offset when vectorized): one multiply-add per dimension, chained.
171pub fn address_datapath_stages(dims: usize, vectorized: bool) -> usize {
172 dims + if vectorized { 1 } else { 0 }
173}
174
175/// Gate an address pattern against the PMU's datapath depth — typed refusal.
176pub fn address_fits(spec: &PmuSpec, dims: usize, vectorized: bool) -> Result<usize, String> {
177 let need = address_datapath_stages(dims, vectorized);
178 if need <= spec.stages {
179 Ok(need)
180 } else {
181 Err(format!(
182 "a {dims}-dimensional{} address needs {need} FU stages; the PMU datapath has {}",
183 if vectorized { " vectorized" } else { "" },
184 spec.stages
185 ))
186 }
187}
188
189/// Banked storage with a flat-address interface: the executable meaning of the banking modes.
190#[derive(Clone, Debug)]
191pub struct Pmu {
192 pub spec: PmuSpec,
193 pub mode: BankingMode,
194 banks: Vec<Vec<i64>>,
195 /// Under duplication every bank holds every word; otherwise each word lives in exactly one bank at
196 /// offset `addr / banks` (strided/FIFO) or `addr / width`-derived (line buffer) — see [`Pmu::slot`].
197 fifo_head: usize,
198 fifo_tail: usize,
199}
200
201impl Pmu {
202 pub fn new(spec: PmuSpec, mode: BankingMode) -> Pmu {
203 Pmu {
204 spec,
205 mode,
206 banks: vec![vec![0; spec.words_per_bank]; spec.banks],
207 fifo_head: 0,
208 fifo_tail: 0,
209 }
210 }
211
212 /// `(bank, offset)` of a flat address under a non-duplicating mode: the bank from [`bank_of`], the
213 /// offset = the address's rank among the addresses that share its bank — an injective map onto the
214 /// bank's words for every address below the capacity.
215 pub fn slot(&self, addr: usize) -> (usize, usize) {
216 assert!(
217 addr < self.spec.usable_words(self.mode),
218 "address {addr} beyond the PMU's {} usable words under {:?}",
219 self.spec.usable_words(self.mode),
220 self.mode
221 );
222 let b = bank_of(addr, self.mode, self.spec.banks);
223 let off = match self.mode {
224 BankingMode::Strided { granularity } => {
225 let g = granularity.max(1);
226 (addr / (g * self.spec.banks)) * g + addr % g
227 }
228 BankingMode::Fifo => addr / self.spec.banks,
229 BankingMode::LineBuffer { width } => {
230 // Row `r`, column `c`: bank `c % banks`, and within the bank the row's `ceil(w/banks)` columns
231 // are packed as `c / banks` — dense per bank, whole rows only (see `usable_words`).
232 let w = width.max(1);
233 let row = addr / w;
234 let col = addr % w;
235 row * ((w + self.spec.banks - 1) / self.spec.banks) + col / self.spec.banks
236 }
237 BankingMode::Duplication => addr,
238 };
239 (b, off)
240 }
241
242 pub fn write(&mut self, addr: usize, v: i64) {
243 if self.mode == BankingMode::Duplication {
244 assert!(
245 addr < self.spec.words_per_bank,
246 "duplicated address {addr} beyond one bank's {} words",
247 self.spec.words_per_bank
248 );
249 for b in &mut self.banks {
250 b[addr] = v;
251 }
252 return;
253 }
254 let (b, off) = self.slot(addr);
255 self.banks[b][off] = v;
256 }
257
258 pub fn read(&self, addr: usize) -> i64 {
259 if self.mode == BankingMode::Duplication {
260 assert!(
261 addr < self.spec.words_per_bank,
262 "duplicated address {addr} beyond one bank's {} words",
263 self.spec.words_per_bank
264 );
265 return self.banks[addr % self.spec.banks][addr];
266 }
267 let (b, off) = self.slot(addr);
268 self.banks[b][off]
269 }
270
271 /// A vector read: the lane values and the cycles it costs (the bank conflicts).
272 pub fn vector_read(&self, access: &AffineAccess) -> (Vec<i64>, usize) {
273 (
274 access.addrs().iter().map(|&a| self.read(a)).collect(),
275 conflicts(access, self.mode, &self.spec),
276 )
277 }
278
279 /// A vector write: cycles it costs (bank conflicts; a duplicated write is broadcast, one cycle per lane
280 /// round since every bank must absorb every lane).
281 pub fn vector_write(&mut self, access: &AffineAccess, vals: &[i64]) -> usize {
282 for (a, v) in access.addrs().into_iter().zip(vals) {
283 self.write(a, *v);
284 }
285 if self.mode == BankingMode::Duplication {
286 access.lanes.max(1)
287 } else {
288 conflicts(access, self.mode, &self.spec)
289 }
290 }
291
292 /// FIFO push (mode must be `Fifo`); returns false when full.
293 pub fn push(&mut self, v: i64) -> bool {
294 assert_eq!(self.mode, BankingMode::Fifo);
295 if self.fifo_tail - self.fifo_head >= self.spec.capacity_words() {
296 return false;
297 }
298 let a = self.fifo_tail % self.spec.capacity_words();
299 self.write(a, v);
300 self.fifo_tail += 1;
301 true
302 }
303
304 /// FIFO pop in push order.
305 pub fn pop(&mut self) -> Option<i64> {
306 assert_eq!(self.mode, BankingMode::Fifo);
307 if self.fifo_head == self.fifo_tail {
308 return None;
309 }
310 let a = self.fifo_head % self.spec.capacity_words();
311 self.fifo_head += 1;
312 Some(self.read(a))
313 }
314}