C1 RUBY 简介
Ruby 来自多方面的 GEMS 项目。Ruby 提供了详细的缓存内存和缓存一致性模型以及详细的网络模型(Garnet)。
在gem5里,ruby和经典内存近乎是不兼容的,对用户来说,用ruby就行,不用管哪里不兼容。
Ruby 中最重要的结构是控制器或状态机。控制器是通过编写 SLICC 状态机文件来实现的。
SLICC 是一种用于指定一致性协议的领域特定语言(包括缓存一致性的规范语言)。SLICC 文件以“.sm”结尾,因为它们是状态机文件。每个文件描述状态、某些事件从开始状态到结束状态的转换以及转换期间要采取的操作。
每个一致性协议都由多个 SLICC 状态机文件组成。这些文件是用 SLICC 编译器编译的,该编译器是用 Python 编写的,是 gem5 源代码的一部分。SLICC 编译器获取状态机文件并输出一组 C++ 文件,这些文件与 gem5 的所有其他文件一起编译。这些文件包括 SimObject 声明文件以及 SimObjects 和其他 C++ 对象的实现文件。
目前,gem5 支持一次仅编译一个一致性协议。例如,您可以将 MI_example 编译为 gem5(默认的,性能较差的协议),或者您可以使用 MESI_Two_Level。但是,要使用 MESI_Two_Level,您必须重新编译 gem5,以便 SLICC 编译器可以为协议生成正确的文件。我们在编译部分 进一步讨论这个问题。
C2 缓存一致性基础
教程先是丢了本书出来,《A Primer on Memory Consistency and Cache Coherence》,该书作为 2011 年计算机体系结构综合讲座的一部分出版(DOI:10.2200) /S00346ED1V01Y201104CAC016)。
然后讲了 MSI 协议。(MSI协议具有三种稳定状态,具有读写权限的修改,具有只读权限的共享,以及没有权限的无效。)
这些代码在 gem5/configs/learning_gem5/part3/ 下,可以不用自己手打。
MSI 协议的scons文件
SConsopts 和 SConscript 是不同, SConsopts在 SConscript之前被读取。
先src/learning_gem5/ 创立一个文件夹,叫src/learning_gem5/MSI_protocol
然后新建 SConsopts 文件
Import('*')
# NOTE: All SLICC setup code found in src/mem/ruby/protocol/SConscript
# Register this protocol with gem5/SCons
main.Append(ALL_PROTOCOLS=['MSI'])
# Add this directory to the search path for SLICC
main.Append(PROTOCOL_DIRS=[Dir('.')])
我们在这个文件中做了两件事。首先,我们注册协议的名称 ( ‘MSI’)。由于我们已将协议命名为 MSI,SCons 将假定有一个指定MSI.slicc所有状态机文件和辅助文件的文件。我们将在写入所有状态机文件后创建该文件。其次,这些SConsopts文件告诉 SCons 在当前目录中查找要传递给 SLICC 编译器的文件。
编写状态机文件
下一步,也是编写协议的大部分工作,是创建状态机文件。状态机文件一般遵循以下大纲:
参数
这些是将从 SLICC 代码生成的 SimObject 的参数。
声明所需的结构和功能
本节声明状态机的状态、事件和许多其他所需的结构。
在端口代码块中
包含查看来自 ( in_port) 消息缓冲区的传入消息并确定要触发哪些事件的代码。
行动
这些是在经历转换时执行的简单单效代码块(例如,发送消息)。
过渡
指定在给定起始状态、事件和最终状态的情况下执行的操作。这是状态机定义的核心内容。
在接下来的几节中,我们将介绍如何编写协议的每个组件。
C3 声明状态机
创建一个名为 的文件MSI-cache.sm,以下代码声明状态机。
machine(MachineType:L1Cache, "MSI cache")
: <parameters>
{
<All state machine code>
}
C4 输入输出端口代码
在状态机文件中声明了我们需要的所有结构后,文件的第一个“功能”部分是“输入端口”。本节指定在不同的传入消息上触发哪些事件。
然而,在我们到达输入端口之前,我们必须声明我们的输出端口。
out_port(request_out, RequestMsg, requestToDir);
out_port(response_out, ResponseMsg, responseToDirOrSibling);
C8 编译不同的gem5 protocol
原文"大多数 SCons 默认值(在参考资料中找到build_opts/)将协议指定为 MI_example,但协议性能较差。 因此,我们不能简单地使用默认的构建名称(例如,X86或ARM)。我们必须在命令行上指定 SCons 选项。下面的命令行将使用 X86 ISA 构建我们的新协议。"
其实2023年,默认的x86已经用了PROTOCOL = ‘MESI_Two_Level’
#别用,只是粘贴一下教程用的命令行
#scons build/X86_MSI/gem5.opt --default=X86 PROTOCOL=MSI SLICC_HTML=True MI_example
这个命令行里指定了protocl,而我们这时候回去看默认的x86 build opt
比如我们在 https://blog.csdn.net/qq_34898487/article/details/134017309?spm=1001.2014.3001.5502 GEM5 full system Parsec tutorial 2024 里用到的
scons build/X86/gem5.opt -j 24
这个x86文件内,就指定PROTOCOL = ‘MESI_Two_Level’
而X86_MI_example
C9 配置一个简单的Ruby系统
我们将对此文件进行一些小更改以使用 Ruby,而不是直接将 CPU 连接到内存控制器。python代码在 gem5/configs/learning_gem5/part3/msi_caches.py, 不用额外下载或者手打
首先,我们可以使用两个 CPU 来测试我们的一致性协议。
system.cpu = [X86TimingSimpleCPU(), X86TimingSimpleCPU()]
C9 .1/2/3
这里合并了原文的L1 cache,DirControl 和System三个小部分,省略里具体代码,只讲了一下大概结构。因为具体代码也不过是复制粘贴。
接下来,实例化内存控制器后,我们将创建缓存系统并设置所有缓存。在创建 CPU 中断之后、实例化系统之前添加以下行。
system.caches = MyCacheSystem()
system.caches.setup(system, system.cpu, [system.mem_ctrl])
然后新建一个文件msi_caches.py 作为MyCacheSystem()的来源。原文是由小组件到大组件,我们这个帖子由大到小,会更清晰一些(具体的代码也会省略一些,只讲结构):
在msi_caches.py里,我们需要一个 class MyCacheSystem(RubySystem):
class MyCacheSystem(RubySystem):
def __init__(self):
if buildEnv['PROTOCOL'] != 'MSI':
fatal("This system assumes MSI from learning gem5!")
super(MyCacheSystem, self).__init__()
然后
def setup(self, system, cpus, mem_ctrls):
#...省略一些原文代码
#这里会用到 class DirController(Directory_Controller):
self.controllers = \
[L1Cache(system, self, cpu) for cpu in cpus] + \
[DirController(self, system.mem_ranges, mem_ctrls)]
# 所以在之前的地方,应该声明L1Cache 和DirController
class DirController(Directory_Controller):
#...省略一些原文DirController(代码
class L1Cache(L1Cache_Controller):
#...省略一些原文 L1Cache代码
C9.4 网络
最后,我们要实现的最后一个对象是网络。构造函数很简单,但我们需要为网络接口列表声明一个空列表(netifs)。
大部分代码都在connectControllers. 这个函数实现了一个 非常简单、不切实际的点对点网络。换句话说,每个控制器都与其他每个控制器有直接链接。
Ruby 网络由三部分组成:将数据从一个路由器路由到另一路由器或外部控制器的路由器、将控制器链接到路由器的外部链路以及将两个路由器链接在一起的内部链路。首先,我们为每个控制器创建一个路由器。然后,我们创建从该路由器到控制器的外部链接。最后,我们添加所有“内部”链接。每个路由器都连接到所有其他路由器以构成点对点网络。
我们重点关注网络,所以对下面的代码逐行解释
#声明一个类,类用大驼峰格式
class MyNetwork(SimpleNetwork):
#声明如何初始化
def __init__(self, ruby_system):
super(MyNetwork, self).__init__()
#一个空集的 子对象
self.netifs = []
#self.ruby_system 是输入的ruby_system
self.ruby_system = ruby_system
#声明如何连接的函数
def connectControllers(self, controllers):
# 创建一个对象列表,名字是 self.routers, 类别是Switch, 其中具有与控制器索引相对应的唯一路由器 ID. 而Switch是在 /home/yz/myprojects/2024GEM5/parsec-tests/yzmodifiedgem5/src/mem/ruby/network/simple/Switch.cc里定义的。
self.routers = [Switch(router_id = i) for i in range(len(controllers))]
#创建一个对象列表,名字叫self.ext_link, 类别是SimpleExtLink
#SimpleExtLink 是在 gem5/src/mem/ruby/network/simple/SimpleLink.py出现,gem5/src/mem/ruby/network/simple/SimpleLink.cc里详细定义的。
self.ext_links = [SimpleExtLink(link_id=i, ext_node=c,
int_node=self.routers[i])
for i, c in enumerate(controllers)]
#创建一个为0的计数器
link_count = 0
#创建一个link的空集。
self.int_links = []
#遍历每一个router
for ri in self.routers:
for rj in self.routers:
if ri == rj: continue # Don't connect a router to itself!
#link数加一
link_count += 1
#添加一个link,如果有n个router,比如64,会连接其他63个router而不是只连接上下左右的4个router。
#由此可见,simple的传输只有link的延迟,而不管router多远,比如有一个hopcount或在k个hocount,都是恒定的速度,这也是为什么它叫simple
#从硬件的角度看,每一个router上都插满里63根links的物理电线。。。这也是garne需要优化的地方
self.int_links.append(SimpleIntLink(link_id = link_count,
src_node = ri,
dst_node = rj))
Ruby simple的 网络 用户怎么用的
刚刚介绍了,gem5的教程在gem5/configs/learning_gem5/part3/msi_caches.py 提供了一个 class MyNetwork API 用于互联,而教程用户 写的是 class MyCacheSystem(RubySystem),在这个system内是怎么用 MyNetwork API 互联的呢?毕竟 MyNetwork API 内,连有多少个cpu都没有指定。
我们回过头看class MyCacheSystem(RubySystem): 而且我画了下面这个图帮助理解:
代码中的注释有细节。大概是: 先新建 self.controllers 其中包含了 L1 cache和dircontroller,然后新建 sequencers, cp
和sequencers 之间用橘色的线连接。
代码还封装好了
class MyCacheSystem(RubySystem):
#...跳过一些代码
#这里,system cpus, mem_ctrls都在 msi_caches.py没有指定,而是在configure file 也就是 simple_ruby.py里指定:simple_ruby.py
#在 simple_ruby.py里,system.caches.setup(system, system.cpu, [system.mem_ctrl]) set up了。
def setup(self, system, cpus, mem_ctrls):
#封装一个self.controllers
self.controllers = [L1Cache(system, self, cpu) for cpu in cpus] + [
DirController(self, system.mem_ranges, mem_ctrls)
]
#...跳过一些代码
##封装一个self.sequencers
self.sequencers = [
RubySequencer(
version=i,
# I/D cache is combined and grab from ctrl
dcache=self.controllers[i].cacheMemory,
clk_domain=self.controllers[i].clk_domain,
)
for i in range(len(cpus))
]
#...跳过一些代码
#连接 cpu和 self.sequencers
for i, cpu in enumerate(cpus):
self.sequencers[i].connectCpuPorts(cpu)
#...跳过一些代码
#语文上是 连接两个cpu核, 严谨一点,是将 n=2 个cpu的包含了l1cache和 dircontroller的 self.controllers连接起来。他们每个都能访问其他所有的 controller,也就是 crossbar结构。
self.network.connectControllers(self.controllers)
crossbar 大概长这样
如果是4个cpu,我们的系统长这样:
C10 运行简单的 Ruby 系统
代码是 gem5/configs/learning_gem5/part3/simple_ruby.py
里面关于模拟硬件的部分是:
#Create the Ruby System
#小写的system是在 gem5/configs/learning_gem5/part3/simple_ruby.py内,也就是本文件,设置好的
#ruby是系统中cache的一部分 # 阿。。这么看ruby也挺渺小的。。但用起来又很大很复杂
system.caches = MyCacheSystem()#这是我们在c9创建好的
#作为一个demo,它用了 1ghz,512mb的内存范围, n=2 个X86TimingSimpleCPU()
# 创建里ddr3 内存控制器并且连接上里membus
#system.caches.setup是一个gem5自己封装好的api,把user定义好的system, system.cpu, [system.mem_ctrl]丢进去就行。
system.caches.setup(system, system.cpu, [system.mem_ctrl])
#最后完成的system就是我们模拟的硬件架构。
然后有一个se 模式的 workload要干
system.workload = SEWorkload.init_compatible(binary)
总结
这篇大致讲了讲part 3 ruby的部分,主要是关于ruby作为缓存一致性的caches system。而且重点关注了多核情况下的互联。