开发 gem5 模型

在 gem5 中建模缓存一致性


大纲

我们不会做的事情

从头编写新协议(不过我们会填补一些缺失的部分)


gem5 历史

M5 + GEMS = gem5

M5: “经典”缓存、CPU 模型、请求者/响应者端口接口

GEMS: Ruby + 网络


缓存一致性提醒

单写多读(SWMR)不变性

cache coherence example


缓存一致性提醒

单写多读(SWMR)不变性

cache coherence example


Ruby 架构

ruby 架构:黑盒两侧的经典端口


黑盒内部的 Ruby

Ruby 内部:互连模型云周围的控制器


Ruby 组件

注意:Ruby 的主要目标是 灵活性,而不是 可用性


控制器模型


要实现的缓存一致性示例

MSI state diagramo table


SLICC 的原始目的

实际输出!

MSI state diagram table from SLICC


自动生成代码的工作原理

重要 永远不要修改这些文件!

Structure of auto-generated code


缓存状态机概述

输入端口读取缓存内存,然后触发事件事件根据状态导致转换,这些转换执行动作动作可以更新缓存内存并通过消息缓冲区发送消息


缓存内存

重要:访问 Entry 时始终调用 setMRU(),否则替换策略将不起作用。

(除非你正在修改 Ruby 本身,否则你永远不需要修改 CacheMemory。)


消息缓冲区

MessageBuffer * requestToDir, network="To", virtual_network="0", vnet_type="request";
MessageBuffer * forwardFromDir, network="From", virtual_network="1", vnet_type="forward";

实践:编写和调试协议

参见 materials/03-Developing-gem5-models/06-modeling-cache-coherence/README.md

你将:

  1. 为编译器声明协议
  2. 填写消息类型
  3. 完成消息缓冲区
  4. 测试协议
  5. 找到一个 bug
  6. 修复 bug
  7. 使用 ruby 随机测试器进行测试

步骤 0:复制模板

cp -r materials/03-Developing-gem5-models/06-modeling-cache-coherence/MyMSI* gem5/src/mem/ruby/protocol

声明协议

修改 src/mem/ruby/protocol/MyMSI.slicc

protocol "MyMSI";
include "RubySlicc_interfaces.slicc";
include "MyMSI-msg.sm";
include "MyMSI-cache.sm";
include "MyMSI-dir.sm";

记住每个协议必须单独编译的注意事项。 希望这不是永久要求。


声明消息类型

修改 src/mem/ruby/protocol/MyMSI-msg.sm

enumeration(CoherenceRequestType, desc="请求消息类型") {
    GetS,       desc="缓存请求具有读权限的块";
    GetM,       desc="缓存请求具有写权限的块";
    PutS,       desc="在 S 状态驱逐块时发送到目录(干净写回)";
    PutM,       desc="在 M 状态驱逐块时发送到目录";
    ...
}
enumeration(CoherenceResponseType, desc="响应消息类型") {
    Data,       desc="包含最新的数据";
    InvAck,     desc="来自另一个缓存的消息,表示它们已使该块无效";
}

目录的消息缓冲区

修改 src/mem/ruby/protocol/MyMSI-dir.sm

    // 从目录*到*缓存的转发请求。
    MessageBuffer *forwardToCache, network="To", virtual_network="1",
          vnet_type="forward";
    // 从目录*到*缓存的响应。
    MessageBuffer *responseToCache, network="To", virtual_network="2",
          vnet_type="response";

    // 从缓存*到*目录的请求
    MessageBuffer *requestFromCache, network="From", virtual_network="0",
          vnet_type="request";

    // 从缓存*到*目录的响应
    MessageBuffer *responseFromCache, network="From", virtual_network="2",
          vnet_type="response";

编译你的新协议

首先,在 Kconfig 构建器中注册协议。修改 src/mem/ruby/protocol/Kconfig

config PROTOCOL
    default "MyMSI" if RUBY_PROTOCOL_MYMSI

and

cont_choice "Ruby protocol"
    config RUBY_PROTOCOL_MYMSI
        bool "MyMSI"

运行 scons 进行编译

为带有你的协议的 gem5 二进制文件创建一个新的构建目录。让我们从 build_opts/ALL 的配置开始并修改它。你需要更改协议,并且应该启用 HTML 输出。

scons defconfig build/ALL_MyMSI build_opts/ALL

安装必要的语言环境并启动 menuconfig。

apt-get update && apt-get install locales
locale-gen en_US.UTF-8
export LANG="en_US.UTF-8"
scons menuconfig build/ALL_MyMSI
# Ruby -> Enable -> Ruby protocol -> MyMSI
scons -j$(nproc) build/ALL_MyMSI/gem5.opt PROTOCOL=MyMSI

创建运行脚本

修改 configs/learning_gem5/part3/msi_caches.py 以使用你的新协议。 此文件为 gem5 代码库中已有的 MSI 缓存设置 Ruby 协议。为了简单起见,我们将使用它。

build/ALL_MyMSI/gem5.opt configs/learning_gem5/part3/simple_ruby.py

在等待编译时,让我们看一下代码的一些细节。 (今天自己编写所有代码太多了…所以让我们只是阅读它)


让我们看一些代码:输入端口定义

来自 gem5/src/learning_gem5/part3/MSI-cache.sm

in_port(mandatory_in, RubyRequest, mandatoryQueue) {
    if (mandatory_in.isReady(clockEdge())) {
        peek(mandatory_in, RubyRequest, block_on="LineAddress") {
            Entry cache_entry := getCacheEntry(in_msg.LineAddress);
            TBE tbe := TBEs[in_msg.LineAddress];
            if (is_invalid(cache_entry) &&
                    cacheMemory.cacheAvail(in_msg.LineAddress) == false ) {
                Addr addr := cacheMemory.cacheProbe(in_msg.LineAddress);
                Entry victim_entry := getCacheEntry(addr);
                TBE victim_tbe := TBEs[addr];
                trigger(Event:Replacement, addr, victim_entry, victim_tbe);
            } else {
                if (in_msg.Type == RubyRequestType:LD ||
                        in_msg.Type == RubyRequestType:IFETCH) {
                    trigger(Event:Load, in_msg.LineAddress, cache_entry,
                            tbe);
                } else if (in_msg.Type == RubyRequestType:ST) {
                    trigger(Event:Store, in_msg.LineAddress, cache_entry,
                            tbe);
                } else {
                    error("Unexpected type from processor");
                }
            }
        }
    }
}

状态声明

参见 gem5/src/mem/ruby/protocol/MSI-cache.sm

state_declaration(State, desc="缓存状态") {
 I,      AccessPermission:Invalid, desc="不存在/无效";
 // 从 I 状态移出的状态
 IS_D,   AccessPermission:Invalid, desc="无效,移动到 S,等待数据";
 IM_AD,  AccessPermission:Invalid, desc="无效,移动到 M,等待确认和数据";
 IM_A,   AccessPermission:Busy,    desc="无效,移动到 M,等待确认";

 S,      AccessPermission:Read_Only, desc="共享。只读,其他缓存可能拥有该块";
 . . .
}

AccessPermission:...:用于功能访问 IS_D:无效,等待数据移动到共享状态


事件声明

参见 gem5/src/mem/ruby/protocol/MSI-cache.sm

enumeration(Event, desc="缓存事件") {
 // 来自处理器/序列器/强制队列
 Load,           desc="来自处理器的加载";
 Store,          desc="来自处理器的存储";

 // 内部事件(仅由处理器请求触发)
 Replacement,    desc="当块被选为牺牲者时触发";

 // 通过目录在转发网络上从其他缓存转发的请求
 FwdGetS,        desc="目录向我们发送请求以满足 GetS。";
                      "我们必须拥有 M 状态的块才能响应此请求。";
 FwdGetM,        desc="目录向我们发送请求以满足 GetM。";
 . . .

其他结构和函数

参见 gem5/src/mem/ruby/protocol/MSI-cache.sm


端口和消息缓冲区

不是 gem5 端口!

注意:(一般经验法则)你应该只在 in_port 块中有 if 语句。永远不要在动作中。


输入端口块

in_port(forward_in, RequestMsg, forwardToCache) {
 if (forward_in.isReady(clockEdge())) {
   peek(forward_in, RequestMsg) {
     Entry cache_entry := getCacheEntry(in_msg.addr);
     TBE tbe := TBEs[in_msg.addr];
     if (in_msg.Type == CoherenceRequestType:GetS) {
        trigger(Event:FwdGetS, in_msg.addr, cache_entry, tbe);
     } else
 . . .

这是看起来像函数调用的奇怪语法,但它不是。 自动填充一个名为 in_msg 的”局部变量”。

trigger() 查找转换。 它还自动确保所有资源都可用于完成转换。


动作

action(sendGetM, "gM", desc="向目录发送 GetM") {
 enqueue(request_out, RequestMsg, 1) {
    out_msg.addr := address;
    out_msg.Type := CoherenceRequestType:GetM;
    out_msg.Destination.add(mapAddressToMachine(address, MachineType:Directory));
    out_msg.MessageSize := MessageSizeType:Control;
    out_msg.Requestor := machineID;
 }
}

enqueue 类似于 peek,但它自动填充 out_msg

某些变量在动作中是隐式的。这些通过 in_port 中的 trigger() 传入。 这些是 addresscache_entrytbe


转换

transition(I, Store, IM_AD) {
  allocateCacheBlock;
  allocateTBE;
  ...
}
transition({IM_AD, SM_AD}, {DataDirNoAcks, DataOwner}, M) {
  ...
  externalStoreHit;
  popResponseQueue;
}

现在,练习

代码现在应该已经编译好了!

参见 materials/03-Developing-gem5-models/06-modeling-cache-coherence/README.md

你将:

  1. 为编译器声明协议
  2. 填写消息类型
  3. 完成消息缓冲区
  4. 测试协议
  5. 找到一个 bug
  6. 修复 bug
  7. 使用 ruby 随机测试器进行测试

调试协议

运行并行测试

build/ALL_MyMSI/gem5.opt configs/learning_gem5/part3/simple_ruby.py

结果是失败!

build/ALL_MyMSI/mem/ruby/protocol/L1Cache_Transitions.cc:266: panic: Invalid transition
system.caches.controllers0 time: 73 addr: 0x9100 event: DataDirNoAcks state: IS_D

使用协议跟踪运行

build/ALL_MyMSI/gem5.opt --debug-flags=ProtocolTrace configs/learning_gem5/part3/simple_ruby.py

开始修复错误并填写 MyMSI-cache.sm


修复错误:缺少转换

transition(IS_D, {DataDirNoAcks, DataOwner}, S) {
    writeDataToCache;
    deallocateTBE;
    externalLoadHit;
    popResponseQueue;
}

修复错误:缺少动作

action(writeDataToCache, "wd", desc="将数据写入缓存") {
    peek(response_in, ResponseMsg) {
        assert(is_valid(cache_entry));
        cache_entry.DataBlk := in_msg.DataBlk;
    }
}

重试(对协议进行任何更改后必须重新编译):

scons build/ALL_MyMSI/gem5.opt -j$(nproc) PROTOCOL=MYMSI
build/ALL_MyMSI/gem5.opt --debug-flags=ProtocolTrace configs/learning_gem5/part3/simple_ruby.py

修复错误:为什么断言失败?

action(allocateCacheBlock, "a", desc="分配缓存块") {
    assert(is_invalid(cache_entry));
    assert(cacheMemory.cacheAvail(address));
    set_cache_entry(cacheMemory.allocate(address, new Entry));
}

重试:

scons build/ALL_MyMSI/gem5.opt -j$(nproc) PROTOCOL=MYMSI
build/ALL_MyMSI/gem5.opt --debug-flags=ProtocolTrace configs/learning_gem5/part3/simple_ruby.py

当调试时间过长时:RubyRandomTester

在某些时候,可能需要一段时间才能遇到新错误,所以…

运行 Ruby 随机测试器。这是一个特殊的”CPU”,它测试一致性边界情况。

build/ALL_MyMSI/gem5.opt --debug-flags=ProtocolTrace configs/learning_gem5/part3/ruby_test.py

注意你可能想要更改 test_caches.py 中的 checks_to_completenum_cpus。 你可能还想减少内存延迟。


使用随机测试器

build/ALL_MyMSI/gem5.opt --debug-flags=ProtocolTrace configs/learning_gem5/part3/ruby_test.py

转换


transition(S, Inv, I) {
  sendInvAcktoReq;
  forwardEviction;
  deallocateCacheBlock;
  popForwardQueue;
}

transition(I, Store,IM_AD) {}
  allocateCacheBlock;
  allocateTBE;
  sendGetM;
  popMandatoryQueue;
}

再次运行 Scons 和 Python 脚本


修复错误:死锁

transition({SM_AD, SM_A}, {Store, Replacement, FwdGetS, FwdGetM}) {
    stall;
}

action(loadHit, "Lh", desc="加载命中") {
  // 将此条目设置为最近使用的,用于替换策略
  // 将数据发送回序列器/CPU。注意:False 表示这不是"外部命中",而是在此本地缓存中命中。
  assert(is_valid(cache_entry));
  // 将此条目设置为最近使用的,用于替换策略
  cacheMemory.setMRU(cache_entry);
  // 将数据发送回序列器/CPU。注意:False 表示这不是"外部命中",而是在此本地缓存中命中。
  sequencer.readCallback(address, cache_entry.DataBlk, false);
}

重试(Scons 和 Python 脚本)

scons build/ALL_MyMSI/gem5.opt -j$(nproc) PROTOCOL=MYMSI
build/ALL_MyMSI/gem5.opt --debug-flags=ProtocolTrace configs/learning_gem5/part3/ruby_test.py

修复错误:存储时该做什么

 action(sendGetM, "gM", desc="Send GetM to the directory") {
        // 在请求输出端口上用 enqueue 填写这个
    enqueue(request_out, RequestMsg, 1) {
      out_msg.addr := address;
      out_msg.Type := CoherenceRequestType:GetM;
      out_msg.Destination.add(mapAddressToMachine(address,
                                    MachineType:Directory));
      out_msg.MessageSize := MessageSizeType:Control;
      out_msg.Requestor := machineID;
    }
  }

运行 Scons 和 Python 脚本


最终错误:存在共享时该做什么?

build/ALL_MyMSI/gem5.opt configs/learning_gem5/part3/ruby_test.py

现在它工作了…查看统计信息

重新运行简单的 pthread 测试,让我们查看一些统计信息!

build/ALL_MyMSI/gem5.opt configs/learning_gem5/part3/simple_ruby.py

答案


Ruby 配置脚本

这些脚本中需要什么?

  1. 实例化控制器 这是你向 .sm 文件传递所有参数的地方
  2. 为每个 CPU(以及 DMA 等)创建一个 Sequencer 稍后会有更多详细信息
  3. 创建并连接所有网络路由器

创建拓扑

创建拓扑后(在模拟之前),Ruby 的网络模型将找到片上网络中从一个节点到另一个节点的所有有效路径。 因此,OCN 与控制器类型和协议完全分离。


点对点示例

self.routers = [Switch(router_id = i) for i in range(len(controllers))]
self.ext_links = [SimpleExtLink(link_id=i, ext_node=c, int_node=self.routers[i])
                  for i, c in enumerate(controllers)]
link_count = 0
self.int_links = []
for ri in self.routers:
    for rj in self.routers:
        if ri == rj: continue # 不要将路由器连接到自身!
        link_count += 1
        self.int_links.append(SimpleIntLink(link_id = link_count, src_node = ri, dst_node = rj))

端口到 Ruby 到端口接口

bg right width:600

还记得这张图吗?


在哪里…?

配置

SLICC:不要害怕修改编译器


在哪里…?


当前协议