前段时间社区的雾佬发了一篇使用 gem5 模拟 MI300X 的知乎,正好我最近在验证 AMDGPU 的浮点运算精度,就想着对比一下 gem5 MI300X 的 model 浮点精度和真实硬件有没有什么差异。

这里推荐使用服务器或者工作站来运行 gem5,个人电脑资源可能不太够用。

主要参考雾佬的文章,以及官方提供的文档 Full System AMD GPU model 来搭建。

基本思路是:

  1. 使用 qemu-system-x86_64 来制作包含 AMDGPU 驱动和 ROCm 环境的镜像,以及所需的内核;
  2. 使用 gem5 的 kvm 模式,模拟一个 x86 ,将 MI300X 的 model 作为 PCIe 卡集成进去;
  3. 最后使用 gem5 加载制作好的镜像和内核,就可以使用了。

为了方便,我使用 gem5 提供的 docker 环境来构建。

1docker pull ghcr.io/gem5/ubuntu-24.04_all-dependencies:v24-0

在本地拉取 gem5 相关仓库,并创建 gem5 的 docker 容器,然后将仓库所在目录共享给容器:

1git clone https://github.com/gem5/gem5.git
2git clone https://github.com/gem5/gem5-resources.git gem5/gem5-resources
3docker run --name gem5-amdgpu \
4--device /dev/kvm \
5--volume /home/zevorn/gem5:/gem5 \
6--privileged \
7-it ghcr.io/gem5/ubuntu-24.04_all-dependencies:v24-0 

接下来就是编译环节,分两步。

首先在容器外面的 gem5/gem5-resources 仓库里制作需要的镜像和内核:

1cd gem5/gem5-resources/src/x86-ubuntu-gpu-ml/
2./build.sh

这里需要耐心等待,这个构建脚本会使用 packer 来制作镜像,制作成功以后,会生成以下文件:

1├── disk-image
2│   └── x86-ubuntu-gpu-ml
3├── vmlinux-gpu-ml

然后进入容器内,开始构建 gem5:

1cd /gem5
2scons build/VEGA_X86/gem5.opt -j$(nproc)

我们需要修改默认的 MI300X 的 python 文件,避免运行完退出:

 1$ git diff configs/example/gpufs/mi300.py
 2@@ -153,7 +153,7 @@ def runMI300GPUFS(
 3         )
 4         b64file.write(runscriptStr)
 5
 6-    args.script = tempRunscript
 7+    # args.script = tempRunscript
 8
 9     # Defaults for CPU
10     args.cpu_type = "X86KvmCPU"

在 gem5 源码路径下新建一个 pytorch_test.py 文件,用于 gem5 运行 mi300 时加载,但实际我们不会执行它:

1#!/usr/bin/env python3
2
3import torch
4
5x = torch.rand(5, 3).to('cuda')
6y = torch.rand(3, 5).to('cuda')
7
8z = x @ y

我们在容器内安装一个 tmux,方便启动 gem5 以后,通过另一个窗格连接进终端:

1apt update
2apt install tmux

然后启动试试:

1build/VEGA_X86/gem5.opt configs/example/gpufs/mi300.py --disk-image gem5-resources/src/x86-ubuntu-gpu-ml/disk-image/x86-ubuntu-gpu-ml --kernel gem5-resources/src/x86-ubuntu-gpu-ml/vmlinux-gpu-ml --app pytorch_test.py

我们使用tmux,在当前窗口再新建一个终端,然后使用下面的命令连接到 gem5:

1./util/term/gem5term localhost 3456
2root@gem5:~#

然后我们需要手动加载 AMDGPU 驱动,可以直接在 gem5 终端输入:

1export LD_LIBRARY_PATH=/opt/rocm/lib:$LD_LIBRARY_PATH
2export HSA_ENABLE_INTERRUPT=0
3export HCC_AMDGPU_TARGET=gfx942
4dmesg -n8
5cat /proc/cpuinfo
6dd if=/root/roms/mi300.rom of=/dev/mem bs=1k seek=768 count=128

我们来验证一下是否加载成功:

 1root@gem5:~# rocminfo
 2ROCk module version 6.12.12 is loaded
 3=====================
 4HSA System Attributes
 5=====================
 6Runtime Version:         1.15
 7Runtime Ext Version:     1.7
 8System Timestamp Freq.:  0.001000MHz
 9Sig. Max Wait Duration:  18446744073709551615 (0xFFFFFFFFFFFFFFFF) (timestamp count)
10Machine Model:           LARGE
11System Endianness:       LITTLE
12Mwaitx:                  DISABLED
13XNACK enabled:           NO
14DMAbuf Support:          YES
15VMM Support:             YES
16...
17*******
18Agent 2
19*******
20  Name:                    gfx942
21  Uuid:                    GPU-XX
22  Marketing Name:          AMD Instinct MI300X
23  Vendor Name:             AMD
24  Feature:                 KERNEL_DISPATCH
25  Profile:                 BASE_PROFILE
26  Float Round Mode:        NEAR
27  Max Queue Number:        128(0x80)
28  Queue Min Size:          64(0x40)
29  Queue Max Size:          131072(0x20000)
30  Queue Type:              MULTI
31  Node:                    1
32  Device Type:             GPU
33...

接下来就可以正常使用了。

我在测试 gem5 MI300X 的 fma 指令的运算结果是,和 cpu 以及 MI300X 真实硬件进行 bit 级对比后,发现总是相差 3~4 ulp,于是我阅读 fma 的源码,发现是 gem5 用乘加两步运算来模拟的,这导致了和真实硬件的浮点精度存在误差,于是我修改源码:

 1$ git diff src/arch/amdgpu/vega/insts/instructions.hh
 2diff --git a/src/arch/amdgpu/vega/insts/instructions.hh b/src/arch/amdgpu/vega/insts/instructions.hh
 3index 7a328f9230..76bd96beee 100644
 4--- a/src/arch/amdgpu/vega/insts/instructions.hh
 5+++ b/src/arch/amdgpu/vega/insts/instructions.hh
 6@@ -44352,8 +44352,9 @@ namespace VegaISA
 7                         int lane_A = i + M * (block + B * (k / K_L));
 8                         int lane_B = j + N * (block + B * (k / K_L));
 9                         int item = k % K_L;
10-                        result[i][j] +=
11-                          src0[item][lane_A] * src1[item][lane_B];
12+                        result[i][j] =
13+                            std::fma(src0[item][lane_A], src1[item][lane_B],
14+                                     result[i][j]);
15                     }
16                 }
17             }

之后测试就正常了,我将这笔 bugfix,贡献到了上游社区,目前已被合并:arch-vega: Improve MFMA precision to match MI300X hardware