Skip to main content

gwr_models/memory/
cache.rs

1// Copyright (c) 2025 Graphcore Ltd. All rights reserved.
2
3//! A basic n-way set-associative cache model.
4//!
5//! The cache provides no memory ordering guarantees.
6//!
7//! TODO: Should cache accesses return an error if they are not
8//! cache-line aligned or sized?
9//!
10//! ```text
11//!  ----------------------------
12//!  |          Device          |
13//!  ----------------------------
14//!       |               |
15//!       |               |
16//!  ----------------------------
17//!  |  dev_rx          dev_tx  |
18//!  |    |               ^     |
19//!  |    |               |     |
20//!  |    |             delay   |
21//!  |    |               |     |
22//!  |    |        0  response  |
23//!  |    +---------> arbiter   |
24//!  |    |               ^     |
25//!  |  delay             | 1   |
26//!  |    |     Cache     |     |
27//!  |    v               |     |
28//!  |  mem_tx          mem_rx  |
29//!  ----------------------------
30//!       |              |
31//!       |              |
32//!  ----------------------------
33//!  |         Mem/Bus          |
34//!  ----------------------------
35//! ```
36use std::cell::RefCell;
37use std::fmt::{self, Display};
38use std::rc::Rc;
39
40use async_trait::async_trait;
41use gwr_components::arbiter::Arbiter;
42use gwr_components::arbiter::policy::RoundRobin;
43use gwr_components::delay::Delay;
44use gwr_components::{connect_tx, port_rx, take_option};
45use gwr_engine::engine::Engine;
46use gwr_engine::executor::Spawner;
47use gwr_engine::port::{InPort, OutPort, PortStateResult};
48use gwr_engine::sim_error;
49use gwr_engine::time::clock::Clock;
50use gwr_engine::time::compute_adjusted_value_and_rate;
51use gwr_engine::traits::{Runnable, SimObject};
52use gwr_engine::types::{AccessType, SimError, SimResult};
53use gwr_model_builder::{EntityDisplay, EntityGet};
54use gwr_track::entity::Entity;
55use gwr_track::tracker::aka::Aka;
56use gwr_track::{build_aka, trace};
57
58use crate::log_stats;
59#[cfg(test)]
60use crate::memory::memory_access::MemoryAccess;
61use crate::memory::traits::{AccessMemory, ReadMemory};
62
63type Tag = u64;
64type Index = usize;
65
66#[derive(Clone)]
67pub struct CacheConfig {
68    line_size_bytes: usize,
69    bw_bytes_per_cycle: usize,
70    num_sets: usize,
71    num_ways: usize,
72    delay_ticks: usize,
73}
74
75impl CacheConfig {
76    #[must_use]
77    pub fn new(
78        line_size_bytes: usize,
79        bw_bytes_per_cycle: usize,
80        num_sets: usize,
81        num_ways: usize,
82        delay_ticks: usize,
83    ) -> Self {
84        Self {
85            line_size_bytes,
86            bw_bytes_per_cycle,
87            num_sets,
88            num_ways,
89            delay_ticks,
90        }
91    }
92}
93
94#[derive(Clone, Default)]
95struct CacheMetrics {
96    payload_bytes_read: usize,
97    payload_bytes_written: usize,
98    num_hits: usize,
99    num_misses: usize,
100}
101
102pub struct CacheStatsDisplay {
103    prefix: String,
104    time_now_ns: f64,
105    payload_bytes_read: usize,
106    payload_bytes_written: usize,
107    num_hits: usize,
108    num_misses: usize,
109}
110
111impl CacheStatsDisplay {
112    #[must_use]
113    pub fn new(
114        prefix: impl Into<String>,
115        time_now_ns: f64,
116        payload_bytes_read: usize,
117        payload_bytes_written: usize,
118        num_hits: usize,
119        num_misses: usize,
120    ) -> Self {
121        Self {
122            prefix: prefix.into(),
123            time_now_ns,
124            payload_bytes_read,
125            payload_bytes_written,
126            num_hits,
127            num_misses,
128        }
129    }
130}
131
132impl Display for CacheStatsDisplay {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        let (read_value, read_per_second) =
135            compute_adjusted_value_and_rate(self.time_now_ns, self.payload_bytes_read);
136        let (write_value, write_per_second) =
137            compute_adjusted_value_and_rate(self.time_now_ns, self.payload_bytes_written);
138        let num_accesses = self.num_hits + self.num_misses;
139        let hit_rate = if num_accesses == 0 {
140            0.0
141        } else {
142            self.num_hits as f64 / num_accesses as f64 * 100.0
143        };
144
145        writeln!(f, "{}:", self.prefix)?;
146        writeln!(
147            f,
148            "  Payload read: {} bytes, {read_value:.2}, {read_per_second:.2}/s",
149            self.payload_bytes_read
150        )?;
151        writeln!(
152            f,
153            "  Payload written: {} bytes, {write_value:.2}, {write_per_second:.2}/s",
154            self.payload_bytes_written
155        )?;
156        write!(
157            f,
158            "  Hits: {}, misses: {}, hit rate: {hit_rate:.2}%",
159            self.num_hits, self.num_misses
160        )
161    }
162}
163
164#[derive(Copy, Clone, Debug, Default, PartialEq)]
165enum EntryState {
166    #[default]
167    Available,
168    Allocated,
169    ValidData,
170}
171
172#[derive(Default, Clone)]
173struct CacheEntry {
174    state: EntryState,
175    tag: Tag,
176}
177
178// Cache structure:
179//  A set comprises N-ways
180type Set = Vec<CacheEntry>;
181//  The cache comprises M-sets
182type Sets = Vec<Set>;
183
184struct CacheContents<T>
185where
186    T: SimObject + AccessMemory,
187{
188    config: CacheConfig,
189    sets: Sets,
190    waiting_for_response: Vec<(Tag, Index, T)>,
191    lru_indices: Vec<usize>,
192}
193
194impl<T> CacheContents<T>
195where
196    T: SimObject + AccessMemory,
197{
198    fn new(config: CacheConfig) -> Self {
199        let sets = vec![vec![CacheEntry::default(); config.num_ways]; config.num_sets];
200        let lru_indices = vec![0; config.num_sets];
201        Self {
202            config,
203            sets,
204            waiting_for_response: Vec::new(),
205            lru_indices,
206        }
207    }
208
209    /// Split up an address into its component parts:
210    ///
211    ///  msb                  lsb
212    ///  +-----+-------+--------+
213    ///  | tag | index | offset |
214    ///  +-----+-------+--------+
215    ///
216    /// Where:
217    ///  - offset within a cache line
218    ///  - index is the part of the address used to select a cache set (n-ways)
219    ///  - tag contains the rest of the address that is compared to determine
220    ///    address matches
221    fn tag_and_index_for_addr(&self, addr: u64) -> (Tag, Index) {
222        let index = (addr as usize / self.config.line_size_bytes) % self.config.num_sets;
223        let tag = addr / self.config.line_size_bytes as u64 / self.config.num_sets as u64;
224        (tag, index)
225    }
226
227    fn state_for(&self, addr: u64) -> Option<EntryState> {
228        let (tag, index) = self.tag_and_index_for_addr(addr);
229        for i in 0..self.config.num_ways {
230            if (self.sets[index][i].state != EntryState::Available)
231                && self.sets[index][i].tag == tag
232            {
233                return Some(self.sets[index][i].state);
234            }
235        }
236        None
237    }
238
239    fn allocate(&mut self, addr: u64) {
240        let (tag, index) = self.tag_and_index_for_addr(addr);
241
242        let insert_index = self.lru_indices[index];
243        self.lru_indices[index] = (self.lru_indices[index] + 1) % self.config.num_ways;
244
245        self.sets[index][insert_index].tag = tag;
246        self.sets[index][insert_index].state = EntryState::Allocated;
247    }
248
249    fn set_data_valid(&mut self, addr: u64) {
250        let (tag, index) = self.tag_and_index_for_addr(addr);
251
252        for i in 0..self.config.num_ways {
253            if (self.sets[index][i].state != EntryState::Available)
254                && self.sets[index][i].tag == tag
255            {
256                self.sets[index][i].state = EntryState::ValidData;
257                break;
258            }
259        }
260    }
261
262    fn invalidate(&mut self, addr: u64) {
263        let (tag, index) = self.tag_and_index_for_addr(addr);
264
265        for i in 0..self.config.num_ways {
266            if self.sets[index][i].tag == tag {
267                self.sets[index][i].state = EntryState::Available;
268                self.sets[index][i].tag = 0;
269                break;
270            }
271        }
272    }
273
274    fn add_waiting_for_response(&mut self, request: T) {
275        let (tag, index) = self.tag_and_index_for_addr(request.dst_addr());
276        self.waiting_for_response.push((tag, index, request));
277    }
278
279    fn get_requests_waiting_for_response(&mut self, response: &T) -> Option<Vec<T>> {
280        let (response_tag, response_index) = self.tag_and_index_for_addr(response.dst_addr());
281
282        // If there are any requests waiting for this response then return matching sets
283        if self
284            .waiting_for_response
285            .iter()
286            .any(|(tag, index, _x)| *tag == response_tag && *index == response_index)
287        {
288            let all: Vec<(Tag, Index, T)> = std::mem::take(&mut self.waiting_for_response);
289            let (matching, not_matching) = all
290                .into_iter()
291                .partition(|(tag, index, _x)| *tag == response_tag && *index == response_index);
292            self.waiting_for_response = not_matching;
293            let matching = matching.into_iter().map(|(_, _, x)| x).collect();
294            Some(matching)
295        } else {
296            None
297        }
298    }
299}
300
301impl<T> ReadMemory for CacheContents<T>
302where
303    T: SimObject + AccessMemory,
304{
305    fn read(&self) -> Vec<u8> {
306        Vec::new()
307    }
308}
309
310impl<T> ReadMemory for RefCell<CacheContents<T>>
311where
312    T: SimObject + AccessMemory,
313{
314    fn read(&self) -> Vec<u8> {
315        Vec::new()
316    }
317}
318
319#[derive(EntityGet, EntityDisplay)]
320pub struct Cache<T>
321where
322    T: SimObject + AccessMemory,
323{
324    entity: Rc<Entity>,
325
326    clock: Clock,
327    spawner: Spawner,
328    metrics: Rc<RefCell<CacheMetrics>>,
329    contents: Rc<RefCell<CacheContents<T>>>,
330
331    response_delay: RefCell<Option<Rc<Delay<T>>>>,
332    request_delay: RefCell<Option<Rc<Delay<T>>>>,
333
334    dev_rx: RefCell<Option<InPort<T>>>,
335    mem_rx: RefCell<Option<InPort<T>>>,
336
337    bw_bytes_per_cycle: usize,
338
339    // Internal ports
340    req: RefCell<Option<OutPort<T>>>,
341    rsp_arb_0: RefCell<Option<OutPort<T>>>,
342    rsp_arb_1: RefCell<Option<OutPort<T>>>,
343}
344
345impl<T> Cache<T>
346where
347    T: SimObject + AccessMemory,
348{
349    /// Create an instance of the cache and register it with the Engine.
350    pub fn new_and_register_with_renames(
351        engine: &Engine,
352        clock: &Clock,
353        parent: &Rc<Entity>,
354        name: &str,
355        aka: Option<&Aka>,
356        config: CacheConfig,
357    ) -> Result<Rc<Self>, SimError> {
358        let bw_bytes_per_cycle = config.bw_bytes_per_cycle;
359        let entity = Rc::new(Entity::new(parent, name));
360
361        let policy = Box::new(RoundRobin::new());
362        let response_arbiter =
363            Arbiter::new_and_register(engine, clock, &entity, "rsp_arb", 2, policy);
364
365        let response_delay_aka = build_aka!(aka, &entity, &[("dev_tx", "tx")]);
366        let response_delay = Delay::new_and_register_with_renames(
367            engine,
368            clock,
369            &entity,
370            "rsp_delay",
371            Some(&response_delay_aka),
372            config.delay_ticks,
373        );
374
375        let request_delay_aka = build_aka!(aka, &entity, &[("mem_tx", "tx")]);
376        let request_delay = Delay::new_and_register_with_renames(
377            engine,
378            clock,
379            &entity,
380            "req_delay",
381            Some(&request_delay_aka),
382            config.delay_ticks,
383        );
384
385        response_arbiter
386            .connect_port_tx(response_delay.port_rx())
387            .expect("Internal ports should connect without error");
388
389        // Create internal ports that are driven by the cache logic
390        let mut req = OutPort::new(&entity, "req_arb_0");
391        req.connect(request_delay.port_rx())
392            .expect("Internal ports should connect without error");
393
394        let mut rsp_arb_0 = OutPort::new(&entity, "rsp_arb_0");
395        rsp_arb_0
396            .connect(response_arbiter.port_rx_i(0))
397            .expect("Internal ports should connect without error");
398
399        let mut rsp_arb_1 = OutPort::new(&entity, "rsp_arb_1");
400        rsp_arb_1
401            .connect(response_arbiter.port_rx_i(1))
402            .expect("Internal ports should connect without error");
403
404        let dev_rx = InPort::new_with_renames(engine, clock, &entity, "dev_rx", aka);
405        let mem_rx = InPort::new_with_renames(engine, clock, &entity, "mem_rx", aka);
406
407        let spawner = engine.spawner();
408        let rc_self = Rc::new(Self {
409            entity,
410            clock: clock.clone(),
411            spawner,
412            metrics: Rc::new(RefCell::new(CacheMetrics::default())),
413            contents: Rc::new(RefCell::new(CacheContents::new(config))),
414            response_delay: RefCell::new(Some(response_delay)),
415            request_delay: RefCell::new(Some(request_delay)),
416            dev_rx: RefCell::new(Some(dev_rx)),
417            mem_rx: RefCell::new(Some(mem_rx)),
418            bw_bytes_per_cycle,
419
420            req: RefCell::new(Some(req)),
421            rsp_arb_0: RefCell::new(Some(rsp_arb_0)),
422            rsp_arb_1: RefCell::new(Some(rsp_arb_1)),
423        });
424        engine.register(rc_self.clone());
425        Ok(rc_self)
426    }
427
428    /// Create an instance of the cache and register it with the Engine.
429    pub fn new_and_register(
430        engine: &Engine,
431        clock: &Clock,
432        parent: &Rc<Entity>,
433        name: &str,
434        config: CacheConfig,
435    ) -> Result<Rc<Self>, SimError> {
436        Self::new_and_register_with_renames(engine, clock, parent, name, None, config)
437    }
438
439    pub fn connect_port_dev_tx(&self, port_state: PortStateResult<T>) -> SimResult {
440        connect_tx!(self.response_delay, connect_port_tx ; port_state)
441    }
442
443    pub fn connect_port_mem_tx(&self, port_state: PortStateResult<T>) -> SimResult {
444        connect_tx!(self.request_delay, connect_port_tx ; port_state)
445    }
446
447    pub fn port_dev_rx(&self) -> PortStateResult<T> {
448        port_rx!(self.dev_rx, state)
449    }
450
451    pub fn port_mem_rx(&self) -> PortStateResult<T> {
452        port_rx!(self.mem_rx, state)
453    }
454
455    #[must_use]
456    pub fn payload_bytes_read(&self) -> usize {
457        self.metrics.borrow().payload_bytes_read
458    }
459
460    #[must_use]
461    pub fn payload_bytes_written(&self) -> usize {
462        self.metrics.borrow().payload_bytes_written
463    }
464
465    #[must_use]
466    pub fn num_hits(&self) -> usize {
467        self.metrics.borrow().num_hits
468    }
469
470    #[must_use]
471    pub fn num_misses(&self) -> usize {
472        self.metrics.borrow().num_misses
473    }
474
475    pub fn dump_stats(&self, time_now_ns: f64) {
476        let metrics = self.metrics.borrow();
477        log_stats(
478            &self.entity,
479            CacheStatsDisplay::new(
480                format!("Cache {}", self.entity.full_name()),
481                time_now_ns,
482                metrics.payload_bytes_read,
483                metrics.payload_bytes_written,
484                metrics.num_hits,
485                metrics.num_misses,
486            ),
487        );
488    }
489}
490
491struct RxHandlingState<T>
492where
493    T: SimObject + AccessMemory,
494{
495    entity: Rc<Entity>,
496    rx: InPort<T>,
497    clock: Clock,
498    contents: Rc<RefCell<CacheContents<T>>>,
499    metrics: Rc<RefCell<CacheMetrics>>,
500    bw_bytes_per_cycle: usize,
501}
502
503#[async_trait(?Send)]
504impl<T> Runnable for Cache<T>
505where
506    T: SimObject + AccessMemory,
507{
508    async fn run(&self) -> SimResult {
509        {
510            // Spawn a worker to handle requests from the device side
511            let state = RxHandlingState {
512                entity: self.entity.clone(),
513                rx: take_option!(self.dev_rx),
514                clock: self.clock.clone(),
515                contents: self.contents.clone(),
516                metrics: self.metrics.clone(),
517                bw_bytes_per_cycle: self.bw_bytes_per_cycle,
518            };
519            let req = take_option!(self.req);
520            let rsp_arb_1 = take_option!(self.rsp_arb_1);
521            self.spawner
522                .spawn(async move { run_dev_rx(state, req, rsp_arb_1).await });
523        }
524
525        // Handle responses from the memory side
526        let state = RxHandlingState {
527            entity: self.entity.clone(),
528            rx: take_option!(self.mem_rx),
529            clock: self.clock.clone(),
530            contents: self.contents.clone(),
531            metrics: self.metrics.clone(),
532            bw_bytes_per_cycle: self.bw_bytes_per_cycle,
533        };
534        let rsp_arb_0 = take_option!(self.rsp_arb_0);
535        run_mem_rx(state, rsp_arb_0).await
536    }
537}
538
539async fn run_dev_rx<T>(
540    mut state: RxHandlingState<T>,
541    mut req: OutPort<T>,
542    mut rsp_arb_1: OutPort<T>,
543) -> SimResult
544where
545    T: SimObject + AccessMemory,
546{
547    loop {
548        let request = state.rx.get()?.await;
549        trace!(state.entity ; "Device request {}", request);
550        let total_bytes = request.total_bytes();
551        handle_request(&state, &mut req, &mut rsp_arb_1, request).await?;
552        let ticks = total_bytes.div_ceil(state.bw_bytes_per_cycle);
553        state.clock.wait_ticks(ticks as u64).await;
554    }
555}
556
557async fn handle_request<T>(
558    state: &RxHandlingState<T>,
559    req: &mut OutPort<T>,
560    rsp_arb_1: &mut OutPort<T>,
561    request: T,
562) -> SimResult
563where
564    T: SimObject + AccessMemory,
565{
566    let addr = request.dst_addr();
567    let access_type = request.access_type();
568    match access_type {
569        AccessType::Control => {
570            state.contents.borrow_mut().invalidate(addr);
571        }
572        AccessType::ReadRequest => {
573            state.metrics.borrow_mut().payload_bytes_read += request.access_size_bytes();
574            let line_state = state.contents.borrow().state_for(addr);
575            match line_state {
576                Some(EntryState::ValidData) => {
577                    let response = request.to_response(state.contents.as_ref())?;
578                    rsp_arb_1.put(response)?.await;
579                    state.metrics.borrow_mut().num_hits += 1;
580                }
581                Some(EntryState::Allocated) => {
582                    // There is an outstanding request to memory for this address already
583                    state
584                        .contents
585                        .borrow_mut()
586                        .add_waiting_for_response(request);
587                    state.metrics.borrow_mut().num_hits += 1;
588                }
589                Some(EntryState::Available) | None => {
590                    state.contents.borrow_mut().allocate(addr);
591                    req.put(request)?.await;
592                    state.metrics.borrow_mut().num_misses += 1;
593                }
594            }
595        }
596
597        AccessType::WriteRequest | AccessType::WriteNonPostedRequest => {
598            state.metrics.borrow_mut().payload_bytes_written += request.access_size_bytes();
599            state.contents.borrow_mut().invalidate(addr);
600            req.put(request)?.await;
601        }
602
603        AccessType::ReadResponse | AccessType::WriteNonPostedResponse => {
604            return sim_error!(
605                "{}: unsupported AccessType from device: {access_type}",
606                state.entity
607            );
608        }
609    }
610
611    Ok(())
612}
613
614async fn run_mem_rx<T>(mut state: RxHandlingState<T>, mut rsp_arb_0: OutPort<T>) -> SimResult
615where
616    T: SimObject + AccessMemory,
617{
618    loop {
619        let response = state.rx.get()?.await;
620        trace!(state.entity ; "Memory response {}", response);
621        let total_bytes = response.total_bytes();
622        handle_response(&state, &mut rsp_arb_0, response).await?;
623        let ticks = total_bytes.div_ceil(state.bw_bytes_per_cycle);
624        state.clock.wait_ticks(ticks as u64).await;
625    }
626}
627
628async fn handle_response<T>(
629    state: &RxHandlingState<T>,
630    rsp_arb_0: &mut OutPort<T>,
631    access: T,
632) -> SimResult
633where
634    T: SimObject + AccessMemory,
635{
636    let access_type = access.access_type();
637    match access_type {
638        AccessType::Control => {
639            // Drop and ignore for now
640        }
641        AccessType::WriteNonPostedResponse => {
642            // Forward this response back to the memory (via the arbiter)
643            rsp_arb_0.put(access)?.await;
644        }
645        AccessType::ReadRequest | AccessType::WriteRequest | AccessType::WriteNonPostedRequest => {
646            return sim_error!(
647                "{}: unsupported {access_type} on response port",
648                state.entity
649            );
650        }
651        AccessType::ReadResponse => {
652            state
653                .contents
654                .borrow_mut()
655                .set_data_valid(access.dst_addr());
656            let matching = state
657                .contents
658                .borrow_mut()
659                .get_requests_waiting_for_response(&access);
660
661            // Forward this response back to the memory (via the arbiter)
662            rsp_arb_0.put(access)?.await;
663
664            // Forward on any other waiting reads that were waiting for this response
665            if let Some(m) = matching {
666                for x in m {
667                    let response = x.to_response(state.contents.as_ref())?;
668                    rsp_arb_0.put(response)?.await;
669                }
670            }
671        }
672    }
673
674    Ok(())
675}
676
677#[test]
678fn basic_ways() {
679    let line_size_bytes = 32;
680    let bw_bytes_per_cycle = 32;
681    let num_sets = 1024;
682    let num_ways = 4;
683    let config = CacheConfig::new(line_size_bytes, bw_bytes_per_cycle, num_sets, num_ways, 8);
684    let mut state: CacheContents<MemoryAccess> = CacheContents::new(config);
685
686    let mut addrs = Vec::new();
687    let mut addr = 0x0100_0000;
688    for _ in 0..num_ways + 1 {
689        addrs.push(addr);
690        addr += (line_size_bytes * num_sets * num_ways) as u64;
691    }
692
693    for addr in addrs.iter().take(num_ways) {
694        assert_eq!(state.state_for(*addr), None);
695        state.allocate(*addr);
696        assert_eq!(state.state_for(*addr), Some(EntryState::Allocated));
697    }
698
699    state.allocate(addrs[num_ways]);
700
701    // Should have been evicted
702    assert_eq!(state.state_for(addrs[0]), None);
703    for i in 0..num_ways {
704        // While all the rest remain
705        assert_eq!(state.state_for(addrs[i + 1]), Some(EntryState::Allocated));
706    }
707}
708
709#[test]
710fn invalidate() {
711    let num_ways = 4;
712    let config = CacheConfig::new(32, 32, 1024, num_ways, 8);
713    let mut state: CacheContents<MemoryAccess> = CacheContents::new(config);
714
715    let addr = 0x40000;
716    state.allocate(addr);
717    assert_eq!(state.state_for(addr), Some(EntryState::Allocated));
718    state.invalidate(addr);
719    assert_eq!(state.state_for(addr), None);
720}