gem5

Table of Contents

1. gem5

1.1. cpu model

https://www.gem5.org/documentation/general_docs/cpu_models/O3CPU

https://carrv.github.io/2017/papers/roelke-risc5-carrv2017.pdf

gem5 的 cpu 分为几种:

  1. atomic

    每条指令一个 cycle, 访存时没有时间的概念

  2. timing

    atomic 的基础上访存时有时间的概念

  3. minor

    timing 的基础上每条指令的 cycle 和 FU 有关, 有流水线, 但是 in-order

  4. o3

    在 minor 的基础上支持 out-of-order

一个不太准确的模拟速度的对比:

+------+    ->20    +--------+ ->3 +--------+  ->10 +---------+ ->10 +--------+
|  rtl |------------|   o3   |-----| atomic |-------+  fpga   |------|  qemu  |
+------+            +--------+     +--------+       +---------+      +--------+

其中 `A ->n B` 指 B 比 A 快 n 倍

1.2. O3PipeView

https://www.gem5.org/documentation/general_docs/cpu_models/visualization/

https://github.com/shioyadan/Konata

通过 O3PipeView, 可以看到:

  • O3 cpu 支持超标量
  • 分支预测失败导致的指令 flush
  • icache 对指令 fetch 的影响
  • dcache 对 load/store 的影响
  • 数据依赖对流水线的影响
  • ROB 对流水线的影响

1.3. memory model

https://www.gem5.org/documentation/general_docs/memory_system/

https://github.com/sunwayforever/gem5

$> gh repo clone sunwayforever/gem5
$> cd <path_to_gem5>/configs/toy
$> make gem5
$> make hello_cache

有几种不同的 memory 工作模式:

  1. atomic
  2. functional
  3. timing

其中前两者没有时间的概念, 例如 slave port 的 `recvFunctional(pkt)` 会直接以 nested function call 的形式调用到底层的 membus, 通过参数的直接 pkt 返回结果.

而 timing 有时间的概念, slave port 收到 `recvTimingReq(pkt)` 后需要在收到底层回复后通过 `sendTimingResp` 返回结果

functional 可以用来做一些和模拟本身无关的一些访存动作, 例如 load elf

hello_cache 的代码展示了一个简单的 cache 的工作过程:

  1. cpu_side 通过 `recvTimingReq` 收到来自 cpu 的请求
  2. cpu_side 通过 simple_cache 把请求通过 mem_side 的 `sendTimingReq` 发送给 membus
  3. 过一段时间后 mem_side 的 `recvTimingResp` 收到 membus 的回复
  4. mem_side 通过 simple_cache 把回复通过 cpu_side 的 `sendTimingResp` 把回复发送经 cpu

对于 slave port (cpu_side), 函数的命名为 `recv..Req`, `send..Resp`

对于 master port (mem_side), 函数的命名为 `send..Req`, `recv..Resp`

1.3.1. contention

另外, 在 timing 模式下, port 与外界的交互需要处理有 contention 的情况

1.3.1.1. cpu_side

cpu_side 收到 `recvTimingReq` 时, 可以返回 false 表示暂时无法处理该请求. 但 cpu 并不会自动重发请求: 需要 cpu_side 通过 `sendRetryReq` 通过 cpu 重新发送 `recvTimingReq`

cpu_side 通过 `sendTimingResq` 时, cpu 可以返回 false 表示无法处理, 同样的, cpu_side 不会主动重发 `sendTimingResq`, 而是需要通过 cpu 发送的 `recvRespRetry`

1.3.1.2. mem_side

mem_side 通过 `sendTimingReq` 时, membus 返回 false, mem_side 需要等待 membus 的 `recvReqRetry`

membus 通过 `recvTimingResq` 时, mem_side 返回 false, mem_side 需要发送 `sendRetryResp` 给 membus

1.3.1.3. 总结
        +-----------------------+             +-----------------------+
        | cpu_side              |             | mem_side              |
        |  +------------------+ |             |  +------------------+ |
+-----+ |  |  recvTimingReq   | |  +--------+ |  |  sendTimingReq   | |  +--------+
|     | |  |  --------------> | |  |        | |  |  ------------->  | |  |        |
|     |-+->|  sendRetryReq    +-+->|        +-+->|  recvReqRetry    |-+->|        |
| cpu | |  |  <-------------  | |  | simple | |  |  <-------------  | |  |        |
|     | |  +------------------+ |  | cache  | |  +------------------+ |  | membus |
|     | |  +------------------+ |  |        | |  +------------------+ |  |        |
|     |<+--|  sendTimingResp  | |  |        |<+--|  recvTimingResp  |<+--|        |
+-----+ |  |  <-------------- +<+--|        | |  |  <-------------  | |  |        |
        |  |  recvRespRetry   | |  +--------+ |  |  sendRetryResp   | |  +--------+
        |  |  --------------> | |             |  |  ------------->  | |
        |  +------------------+ |             |  +------------------+ |
        +-----------------------+             +-----------------------+

1.4. checkpoint

gem5 可以在执行过程中产生 checkpoint, 后续可以从 checkout 恢复执行. checkpoint 会包含系统状态(例如寄存器值) 和内存映像.

一个典型的使用 checkpoint 的场景是: gem5 使用较快的 atomic cpu 启动 linux, 然后保存 checkpoint. 后续可以直接使用 checkpoint 启动 gem5, 避免漫长的 linux 启动过程.

还有一个场景通过采样的方式加快 benchmark 的执行: 先完整执行一次 benchmark, 但每隔一段 tick N 保存一次 checkpoint. 后续再次执行 benchmark 时并行的从这些 benchmark 启动, 但可以只执行 N/2 个 tick (或者每隔一段 tick N 执行一次?), 达到均匀采样 (uniform sampling) 执行的效果. 后面提到的simpoint 也需要配置 checkpoint 工作

1.4.1. 创建 checkpoint

m5.instantiate()
exit_event = m5.simulate(10000000)
m5.checkpoint("/tmp/1.ckpt")

1.4.2. 使用 checkpoint

m5.instantiate("/tmp/1.ckpt")
exit_event = m5.simulate()

1.5. simpoint

Automatically Characterizing Large Scale Program Behavior

https://cseweb.ucsd.edu/~calder/simpoint/

https://xiangshan-doc.readthedocs.io/zh_CN/latest/tools/simpoint/

simpoint 不是均匀采样: 每一段 tick 根据其执行的代码 (根据 BBV 来判断) 被赋予不同的 cluster, 根据每个 cluster 的大小做为权重来采样执行.

simpoint 的基本作法是:

  1. 先用模拟器完整执行一次 workload, 假设一共执行了 N 条指令, 平均分为 K 份, 每份 N/K 条指令, 记为 T_i
  2. 统计 T_i 的 BBV (basic block vector), 即程序的 M 个 basic block 在执行 T_i 时分别进入了多少次, 是一个长度为 M 的向量
  3. 对 BBV_i 降维后, 使用 BBV_i 的欧氏距离对 T_i 进行 k-means 聚类. BBV_i 与 BBV_j 相似意味着 T_i 和 T_j 在执行类似的代码
  4. 使用最终得到的 k 个聚类中心 (C_i) 加权后 (每个聚类的大小做为权重) 理论上可以代表原始的 T, 例如原始 T 为 `T1,T2,T3,T4,T5,….`, 现在变成 `T2,T2,T3,T4,T3,…`, 其中 T2, T3, T4 是聚类中心
  5. 生成聚类中心对应的 checkpoint ckpt_i
  6. 后续需要重复模拟时, 只需要加载 ckpt_i 后执行 C_i, 再配合 C_i 的权重即可估计模拟的结果

1.5.1. 产生 simpoint 数据

# NOTE: gem5 只有 atomic cpu 支持 simport
system.cpu = RiscvAtomicSimpleCPU()
# NOTE: 20 是指 sample 间隔是每 20 条 inst
system.cpu.addSimPointProbe(20)
# NOTE: memory 也必须是 atomic 模式 (而不是 timing 模式)
system.mem_mode = "atomic"

通过 `make simple_simport` 产生 `m5out/simpoint.bb.gz`, 其内容为:

# NOTE: 格式为 T[:BB_id:count]*
T:1:1 :2:3 :3:12 :4:15
T:5:12
T:6:1 :7:4 :8:63
T:9:4
T:10:3
T:11:2
...

使用 SimPoint3.2 产生 sim points 和 weights (SimPoint 3.2 年代久远, 需要修改一些编译错误)

$> <path_to_simpoint>/bin/simpoint -maxK 5 -loadFVFile m5out/simpoint.bb.gz -inputVectorsGzipped \
   -saveSimpoints m5out/simpoints.txt -saveSimpointWeights m5out/weights.txt

$> cat m5out/simpoints.txt
43 0
10 1
173 2
87 3
187 4

$> cat m5out/weights.txt
0.0803859 0
0.472669 1
0.0321543 2
0.144695 3
0.270096 4

`43 0` 是指第 0 个 cluster 的聚类中心是第 43 次 sample, 即第 43x20 条指令处, 权重为 0.0803859

1.5.2. 使用 simpoint 产生 checkpoint

最后使用 simpoints.txt 让 gem5 产生 checkpoint:

# NOTE: x20 是因为 sample 间隔是 20 条指令
start_insts = [x * 20 for x in [10, 43, 87, 173, 187]]
system.cpu.simpoint_start_insts = start_insts
# ...
for i, _ in enumerate(start_insts):
    # NOTE: 无法直接在 m5.simulate 指定 tick 来 exit , 因为不知道 inst 对应的
    # tick, 只能依赖设置 system.cpu.simpoint_start_insts 让它在 start inst 处
    # exit
    exit_event = m5.simulate()

    if exit_event.getCause() == "simpoint starting point found":
        ckpt_dir = f"m5out/ckpt.{i}"
        m5.checkpoint(ckpt_dir)

1.5.3. 使用 checkpoint

# NOTE: 执行 20 条 inst 后退出
system.cpu.simpoint_start_insts = [20]
m5.instantiate("m5out/ckpt.0")
exit_event = m5.simulate()

1.6. 其它

1.6.1. tick/cycle

gem5 默认的 simFreq 是 1x10^12 tick/s, 即每个 tick 相当于 1ps 模拟时间 (而非实现时间). 所以 system.clk_domain.clock=`1GHz` 时每个 cycle 等于 1000 个 tick, system.clk_domain.clock=`2GHz` 时每个 cycle 等于 500 个 tick. 以上信息通过 m5out/stats.txt 可以看到

1.6.2. insn decoder

gem5 使用自定义的 DSL 来生成指令的 decoder 代码, 例如 decoder.isa 中关于 addi 的定义:

...
decode OPCODE {
0x03: decode FUNCT3 {
   format IOp {
       0x0: addi({{
           Rd_sd = Rs1_sd + imm;
       }});
       0x2: slti({{
           Rd = (Rs1_sd < imm) ? 1 : 0;
       }});
       ...
   }
}
...

表示 addi 的 opcode 为 0x03, FUNCT3 为 0x0

这个 isa 编译后会生成几个相关的 inc 文件给 c 代码使用:

decode-method.cc.inc

case 0x0:
    // IOp::addi(['\n                    Rd_sd = Rs1_sd + imm;\n '],{})
    return new Addi(machInst);
    break;

exec-ns.cc.inc

// IOp::addi(['\n                    Rd_sd = Rs1_sd + imm;\n                '],
// {})
Fault Addi::execute(ExecContext *xc, trace::InstRecord *traceData) const {
    int64_t Rd = 0;
    int64_t Rs1 = 0;

    Rs1 = xc->getRegOperand(this, 0);

    Rd = Rs1 + imm;

    {
        RegVal final_val = Rd;
        xc->setRegOperand(this, 0, final_val);
        if (traceData) {
            traceData->setData(intRegClass, final_val);
        }
    };
    return NoFault;
}

另外, isa 中还包含了 functional unit 信息, 例如 IntDivOp, 在模拟时 minor 和o3cpu 会选择 fu 并插入对应的 latency (opLat)

Backlinks

opcodes (binutils > opcodes > riscv_opcodes): as/objdump/gdb 都使用了 opcodes, qemu/spike/gem5 则定义了它们自己的一套类似的机制

Author: [email protected]
Date: 2023-09-06 Wed 16:48
Last updated: 2023-09-13 Wed 12:12

知识共享许可协议