Skip to main content

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}