From 44b82f5988762eedf191a8e8a84b792cd3822315 Mon Sep 17 00:00:00 2001 From: Basem Mohammed <95645899+Basemism@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:45:02 +0200 Subject: [PATCH 1/2] mem-ruby: add an optional GPU L3 to MOESI_AMD_Base Add a configurable GPU L3 cache to the GPU-only MOESI_AMD_Base directory and support both inclusive and exclusive allocation policies. Keep the feature disabled by default so existing GPU_VIPER configurations retain their current behavior. Handle L3 hits, fills, writebacks, atomics, DMA reads, and probe responses without leaving stale copies behind. Preserve inclusive L3 entries on ordinary GPU and DMA reads, invalidate or update an L3 entry when a dirty TCC probe supplies newer data, and retain the exclusive mode as an explicit option. Also compute directory and block index widths before processing --numa-high-bit so configurations using an explicit NUMA bit can construct the L3 cache safely. --- configs/ruby/GPU_VIPER.py | 44 ++++-- src/mem/ruby/protocol/MOESI_AMD_Base-dir.sm | 146 +++++++++++++++----- 2 files changed, 140 insertions(+), 50 deletions(-) diff --git a/configs/ruby/GPU_VIPER.py b/configs/ruby/GPU_VIPER.py index 56d01b6d52e..57308e3c2b5 100644 --- a/configs/ruby/GPU_VIPER.py +++ b/configs/ruby/GPU_VIPER.py @@ -98,7 +98,7 @@ class L2Cache(RubyCache): def create(self, size, assoc, options): self.size = MemorySize(size) self.assoc = assoc - self.replacement_policy = TreePLRURP() + self.replacement_policy = BRRIPRP() class CPCntrl(GPU_VIPER_CorePair_Controller, CntrlBase): @@ -336,18 +336,21 @@ class L3Cache(RubyCache): dataArrayBanks = 16 tagArrayBanks = 16 - def create(self, options, ruby_system, system): + def create(self, options, ruby_system, system, num_dirs=None): + # num_dirs: number of directory controllers sharing this L3 pool. + # Defaults to options.num_dirs (CPU-side) if not specified. + if num_dirs is None: + num_dirs = options.num_dirs self.size = MemorySize(options.l3_size) - self.size.value /= options.num_dirs + self.size.value /= num_dirs self.assoc = options.l3_assoc - self.dataArrayBanks /= options.num_dirs - self.tagArrayBanks /= options.num_dirs - self.dataArrayBanks /= options.num_dirs - self.tagArrayBanks /= options.num_dirs + # Distribute banks evenly across directory controllers + self.dataArrayBanks /= num_dirs + self.tagArrayBanks /= num_dirs self.dataAccessLatency = options.l3_data_latency self.tagAccessLatency = options.l3_tag_latency self.resourceStalls = False - self.replacement_policy = TreePLRURP() + self.replacement_policy = BRRIPRP() class L3Cntrl(GPU_VIPER_L3Cache_Controller, CntrlBase): @@ -382,7 +385,7 @@ def connectWireBuffers( class DirCntrl(GPU_VIPER_Directory_Controller, CntrlBase): - def create(self, options, dir_ranges, ruby_system, system): + def create(self, options, dir_ranges, ruby_system, system, num_dirs=None): self.version = self.versionCount() self.response_latency = 30 @@ -393,7 +396,8 @@ def create(self, options, dir_ranges, ruby_system, system): ) self.L3CacheMemory = L3Cache() - self.L3CacheMemory.create(options, ruby_system, system) + self.L3CacheMemory.create(options, ruby_system, system, + num_dirs=num_dirs) self.l3_hit_latency = max( self.L3CacheMemory.dataAccessLatency, @@ -438,6 +442,13 @@ def define_options(parser): "--no-tcc-resource-stalls", action="store_false", default=True ) parser.add_argument("--use-L3-on-WT", action="store_true", default=False) + parser.add_argument("--use-gpu-l3", action="store_true", default=False, + help="Enable L3 (Infinity Cache) fills for GPU " + "directory controllers") + parser.add_argument("--l3-exclusive", action="store_true", default=False, + help="Experimental non-CDNA3 victim-cache policy: " + "GPU reads consume L3 entries and fills occur " + "from lower-level evictions") parser.add_argument("--num-tbes", type=int, default=256) parser.add_argument("--l2-latency", type=int, default=50) # load to use parser.add_argument( @@ -560,14 +571,14 @@ def construct_dirs(options, system, ruby_system, network): # For an odd number of CPUs, still create the right number of controllers TCC_bits = int(math.log(options.num_tccs, 2)) + dir_bits = int(math.log(options.num_dirs, 2)) + block_size_bits = int(math.log(options.cacheline_size, 2)) if options.numa_high_bit: numa_bit = options.numa_high_bit else: # if the numa_bit is not specified, set the directory bits as the # lowest bits above the block offset bits, and the numa_bit as the # highest of those directory bits - dir_bits = int(math.log(options.num_dirs, 2)) - block_size_bits = int(math.log(options.cacheline_size, 2)) numa_bit = block_size_bits + dir_bits - 1 for i in range(options.num_dirs): @@ -584,6 +595,7 @@ def construct_dirs(options, system, ruby_system, network): dir_cntrl = DirCntrl(noTCCdir=True, TCC_select_num_bits=TCC_bits) dir_cntrl.create(options, dir_ranges, ruby_system, system) + dir_cntrl.L3CacheMemory.start_index_bit = block_size_bits + dir_bits dir_cntrl.number_of_TBEs = options.num_tbes dir_cntrl.useL3OnWT = options.use_L3_on_WT dir_cntrl.L2isWB = options.WB_L2 @@ -651,9 +663,13 @@ def construct_gpudirs(options, system, ruby_system, network): TCC_select_num_bits=TCC_bits, clk_domain=system.fabric_clk, ) - dir_cntrl.create(options, [addr_range], ruby_system, system) + dir_cntrl.create(options, [addr_range], ruby_system, system, + num_dirs=options.dgpu_num_dirs) + dir_cntrl.L3CacheMemory.start_index_bit = block_size_bits + dir_bits dir_cntrl.number_of_TBEs = options.num_tbes - dir_cntrl.useL3OnWT = False + dir_cntrl.GPUonly = True + dir_cntrl.useL3OnWT = options.use_gpu_l3 + dir_cntrl.L3Exclusive = options.l3_exclusive dir_cntrl.L2isWB = options.WB_L2 # Connect the Directory controller to the ruby network diff --git a/src/mem/ruby/protocol/MOESI_AMD_Base-dir.sm b/src/mem/ruby/protocol/MOESI_AMD_Base-dir.sm index 2bdcc9624d7..8a18e1d24eb 100644 --- a/src/mem/ruby/protocol/MOESI_AMD_Base-dir.sm +++ b/src/mem/ruby/protocol/MOESI_AMD_Base-dir.sm @@ -39,6 +39,7 @@ machine(MachineType:Directory, "AMD Baseline protocol") bool GPUonly := "False"; int TCC_select_num_bits; bool useL3OnWT := "False"; + bool L3Exclusive := "False"; bool L2isWB; Cycles to_memory_controller_latency := 1; @@ -643,7 +644,14 @@ machine(MachineType:Directory, "AMD Baseline protocol") tbe.L3Hit := true; tbe.MemData := true; - L3CacheMemory.deallocate(address); + if (GPUonly && !L3Exclusive) { + // CDNA 3 Infinity Cache is a non-destructive memory-side cache, + // including for DMA/I/O traffic. + L3CacheMemory.setMRU(address); + } else { + // CPU-side behavior or experimental GPU exclusive policy. + L3CacheMemory.deallocate(address); + } } else { enqueue(memQueue_out, MemoryMsg, to_memory_controller_latency) { out_msg.addr := address; @@ -670,16 +678,22 @@ machine(MachineType:Directory, "AMD Baseline protocol") tbe.LastSender := entry.LastSender; tbe.L3Hit := true; tbe.MemData := true; - L3CacheMemory.deallocate(address); + if (GPUonly && !L3Exclusive) { + // CDNA 3 memory-side cache: retain the clean memory copy. + L3CacheMemory.setMRU(address); + } else { + // CPU-side behavior or experimental GPU exclusive policy. + L3CacheMemory.deallocate(address); + } } else { enqueue(memQueue_out, MemoryMsg, to_memory_controller_latency) { - out_msg.addr := address; - out_msg.Type := MemoryRequestType:MEMORY_READ; - out_msg.Sender := machineID; - out_msg.MessageSize := MessageSizeType:Request_Control; - } - } - } + out_msg.addr := address; + out_msg.Type := MemoryRequestType:MEMORY_READ; + out_msg.Sender := machineID; + out_msg.MessageSize := MessageSizeType:Request_Control; + } + } + } } //This action profiles a hit or miss for a given request or write back. @@ -984,12 +998,17 @@ machine(MachineType:Directory, "AMD Baseline protocol") action(d_writeDataToMemory, "d", desc="Write data to memory") { peek(requestNetwork_in, CPURequestMsg) { - enqueue(memQueue_out, MemoryMsg, to_memory_controller_latency) { - out_msg.addr := address; - out_msg.Type := MemoryRequestType:MEMORY_WB; - out_msg.Sender := machineID; - out_msg.MessageSize := MessageSizeType:Writeback_Data; - out_msg.DataBlk := in_msg.DataBlk; + // GPU: only write dirty data to memory. Clean evictions are no-ops + // since memory already has the data. + if (!GPUonly || in_msg.Type == CoherenceRequestType:VicDirty || + in_msg.Dirty) { + enqueue(memQueue_out, MemoryMsg, to_memory_controller_latency) { + out_msg.addr := address; + out_msg.Type := MemoryRequestType:MEMORY_WB; + out_msg.Sender := machineID; + out_msg.MessageSize := MessageSizeType:Writeback_Data; + out_msg.DataBlk := in_msg.DataBlk; + } } if (tbe.Dirty == false) { // have to update the TBE, too, because of how this @@ -1094,7 +1113,10 @@ machine(MachineType:Directory, "AMD Baseline protocol") } action(wd_writeBackData, "wd", desc="Write back data if needed") { - if (tbe.wtData || tbe.atomicData || tbe.Dirty == false) { + // Write memory for data that must be persisted: + // WT/atomic updates, or dirty data returned by probes. + // Clean read data does not need writeback — memory already has it. + if (tbe.wtData || tbe.atomicData || tbe.Dirty) { // Only perform atomics in the directory if the SLC bit is set, or // if the L2 is WT if (tbe.atomicData && (tbe.isSLCSet || !L2isWB)) { @@ -1205,6 +1227,9 @@ machine(MachineType:Directory, "AMD Baseline protocol") } action(al_allocateL3Block, "al", desc="allocate the L3 block on WB") { + // GPU memory-side policy: write dirty data through to memory, then retain a + // clean copy in L3. Experimental exclusive policy uses victim-cache fills. + // CPU: original behavior — always fill. peek(requestNetwork_in, CPURequestMsg) { if (L3CacheMemory.isTagPresent(address)) { CacheEntry entry := static_cast(CacheEntry, "pointer", L3CacheMemory.lookup(address)); @@ -1219,14 +1244,19 @@ machine(MachineType:Directory, "AMD Baseline protocol") } else { if (L3CacheMemory.cacheAvail(address) == false) { Addr victim := L3CacheMemory.cacheProbe(address); - CacheEntry victim_entry := static_cast(CacheEntry, "pointer", - L3CacheMemory.lookup(victim)); - enqueue(memQueue_out, MemoryMsg, to_memory_controller_latency) { - out_msg.addr := victim; - out_msg.Type := MemoryRequestType:MEMORY_WB; - out_msg.Sender := machineID; - out_msg.MessageSize := MessageSizeType:Writeback_Data; - out_msg.DataBlk := victim_entry.DataBlk; + if (GPUonly) { + // GPU L3 (Infinity Cache) is read-only — evicted entries are + // always clean, so silent drop is correct. + } else { + CacheEntry victim_entry := static_cast(CacheEntry, "pointer", + L3CacheMemory.lookup(victim)); + enqueue(memQueue_out, MemoryMsg, to_memory_controller_latency) { + out_msg.addr := victim; + out_msg.Type := MemoryRequestType:MEMORY_WB; + out_msg.Sender := machineID; + out_msg.MessageSize := MessageSizeType:Writeback_Data; + out_msg.DataBlk := victim_entry.DataBlk; + } } L3CacheMemory.deallocate(victim); } @@ -1241,7 +1271,16 @@ machine(MachineType:Directory, "AMD Baseline protocol") } action(alwt_allocateL3BlockOnWT, "alwt", desc="allocate the L3 block on WT") { - if ((tbe.wtData || tbe.atomicData) && useL3OnWT) { + // GPU dirs: fill L3 on all completions (reads + writes) when enabled. + // In memory-side mode: skip refill on clean GPU read L3 hits (already kept by + // setMRU). If a probe supplied newer dirty data, refresh the L3 entry. + // Still fill on an L3 miss (new data from HBM) and WT/atomic L3 hit. + // In experimental exclusive mode, do not fill on read completion; lower-level + // evictions populate L3 through al_allocateL3Block instead. + // CPU dirs: original behavior — only fill on WT/atomic data. + if ((GPUonly && useL3OnWT && !L3Exclusive && + !(tbe.L3Hit && !tbe.Dirty && !tbe.wtData && !tbe.atomicData)) || + (!GPUonly && (tbe.wtData || tbe.atomicData) && useL3OnWT)) { //This tag check does not need to be counted as a hit or Miss, it has already been recorded. if (L3CacheMemory.isTagPresent(address)) { CacheEntry entry := static_cast(CacheEntry, "pointer", L3CacheMemory.lookup(address)); @@ -1251,14 +1290,19 @@ machine(MachineType:Directory, "AMD Baseline protocol") } else { if (L3CacheMemory.cacheAvail(address) == false) { Addr victim := L3CacheMemory.cacheProbe(address); - CacheEntry victim_entry := static_cast(CacheEntry, "pointer", - L3CacheMemory.lookup(victim)); - enqueue(memQueue_out, MemoryMsg, to_memory_controller_latency) { - out_msg.addr := victim; - out_msg.Type := MemoryRequestType:MEMORY_WB; - out_msg.Sender := machineID; - out_msg.MessageSize := MessageSizeType:Writeback_Data; - out_msg.DataBlk := victim_entry.DataBlk; + if (GPUonly) { + // GPU L3 (Infinity Cache) is read-only — evicted entries are + // always clean, so silent drop is correct. + } else { + CacheEntry victim_entry := static_cast(CacheEntry, "pointer", + L3CacheMemory.lookup(victim)); + enqueue(memQueue_out, MemoryMsg, to_memory_controller_latency) { + out_msg.addr := victim; + out_msg.Type := MemoryRequestType:MEMORY_WB; + out_msg.Sender := machineID; + out_msg.MessageSize := MessageSizeType:Writeback_Data; + out_msg.DataBlk := victim_entry.DataBlk; + } } L3CacheMemory.deallocate(victim); } @@ -1539,23 +1583,42 @@ machine(MachineType:Directory, "AMD Baseline protocol") ptl_popTriggerQueue; } - transition(BDR_M, MemData, U) { + transition(BDR_M, MemData, U) {L3TagArrayWrite, L3DataArrayWrite} { mt_writeMemDataToTBE; dd_sendResponseDmaData; + alwt_allocateL3BlockOnWT; wada_wakeUpAllDependentsAddr; dt_deallocateTBE; pm_popMemQueue; } - transition(BDW_M, MemData, U) { + transition(BDR_M, L3Hit, U) {L3TagArrayWrite, L3DataArrayWrite} { + dd_sendResponseDmaData; + alwt_allocateL3BlockOnWT; + wada_wakeUpAllDependentsAddr; + dt_deallocateTBE; + ptl_popTriggerQueue; + } + + transition(BDW_M, MemData, U) {L3TagArrayWrite, L3DataArrayWrite} { mt_writeMemDataToTBE; wd_writeBackData; da_sendResponseDmaAck; + alwt_allocateL3BlockOnWT; wada_wakeUpAllDependentsAddr; dt_deallocateTBE; pm_popMemQueue; } + transition(BDW_M, L3Hit, U) {L3DataArrayWrite, L3TagArrayWrite} { + wd_writeBackData; + da_sendResponseDmaAck; + alwt_allocateL3BlockOnWT; + wada_wakeUpAllDependentsAddr; + dt_deallocateTBE; + ptl_popTriggerQueue; + } + transition(BS_M, MemData, B){L3TagArrayWrite, L3DataArrayWrite} { mt_writeMemDataToTBE; wd_writeBackData; @@ -1666,16 +1729,26 @@ machine(MachineType:Directory, "AMD Baseline protocol") pm_popMemQueue; } - transition(BL2_M, MemData, U) { + transition(BL2_M, MemData, U) {L3TagArrayWrite, L3DataArrayWrite} { // Got the memory we were waiting for. We can unblock now. mt_writeMemDataToTBE; dd_sendResponseDmaData; + alwt_allocateL3BlockOnWT; wada_wakeUpAllDependentsAddr; pd_popDmaRequestQueue; dt_deallocateTBE; pm_popMemQueue; } + transition(BL2_M, L3Hit, U) {L3TagArrayWrite, L3DataArrayWrite} { + dd_sendResponseDmaData; + alwt_allocateL3BlockOnWT; + wada_wakeUpAllDependentsAddr; + pd_popDmaRequestQueue; + dt_deallocateTBE; + ptl_popTriggerQueue; + } + transition(BDR_PM, ProbeAcksComplete, BDR_M) { pt_popTriggerQueue; } @@ -1757,6 +1830,7 @@ machine(MachineType:Directory, "AMD Baseline protocol") transition(U, Flush, F) {L3TagArrayRead, L3TagArrayWrite} { t_allocateTBE; f_writeFlushDataToMemory; + alwt_allocateL3BlockOnWT; w_sendResponseWBAck; p_popRequestQueue; } From 114ae95a1987b7346de9bd362af238b7e147ea60 Mon Sep 17 00:00:00 2001 From: Basem Mohammed <95645899+Basemism@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:45:44 +0200 Subject: [PATCH 2/2] configs: make TCC and L3 cache bank counts configurable Use the existing --tcc-num-banks option when constructing each TCC instead of deriving its internal bank count from the number of TCC controllers. Add --l3-num-banks to configure the data and tag banks in each directory's L3 slice. The default of 16 preserves the cache object's prior per-slice bank count while making the modeled organization explicit. --- configs/ruby/GPU_VIPER.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/configs/ruby/GPU_VIPER.py b/configs/ruby/GPU_VIPER.py index 57308e3c2b5..b1c654212c5 100644 --- a/configs/ruby/GPU_VIPER.py +++ b/configs/ruby/GPU_VIPER.py @@ -295,10 +295,8 @@ def create(self, options): self.tagArrayBanks = 64 else: self.size = MemorySize(options.tcc_size) - self.dataArrayBanks = ( - 256 / options.num_tccs - ) # number of data banks - self.tagArrayBanks = 256 / options.num_tccs # number of tag banks + self.dataArrayBanks = options.tcc_num_banks # number of data banks + self.tagArrayBanks = options.tcc_num_banks # number of tag banks self.size.value = self.size.value / options.num_tccs if (self.size.value / int(self.assoc)) < 128: self.size.value = int(128 * self.assoc) @@ -344,9 +342,11 @@ def create(self, options, ruby_system, system, num_dirs=None): self.size = MemorySize(options.l3_size) self.size.value /= num_dirs self.assoc = options.l3_assoc - # Distribute banks evenly across directory controllers - self.dataArrayBanks /= num_dirs - self.tagArrayBanks /= num_dirs + # Each directory controller owns one L3 slice. Configure the internal + # data and tag bank count of that slice independently of the number of + # directory controllers. + self.dataArrayBanks = options.l3_num_banks + self.tagArrayBanks = options.l3_num_banks self.dataAccessLatency = options.l3_data_latency self.tagAccessLatency = options.l3_tag_latency self.resourceStalls = False @@ -433,6 +433,12 @@ def define_options(parser): parser.add_argument("--tcp-issue-latency", type=int, default=1) parser.add_argument("--l3-data-latency", type=int, default=20) parser.add_argument("--l3-tag-latency", type=int, default=15) + parser.add_argument( + "--l3-num-banks", + type=int, + default=16, + help="Number of data and tag banks in each directory L3 slice", + ) parser.add_argument("--cpu-to-dir-latency", type=int, default=120) parser.add_argument("--gpu-to-dir-latency", type=int, default=120) parser.add_argument(