跳转至

QEMU 训练营 2026 专业阶段总结

主要贡献者


背景介绍

我目前处于“研 0”阶段,未来准备学习虚拟设备建模相关方向。参加这次 QEMU 训练营之前,我对 QEMU 的认识主要停留在“用它启动虚拟机”这一层。我知道 PCIe、MMIO、DMA 和中断等概念,却没有真正把它们放进一个完整的设备模型中。

本次专业阶段,我在 Ubuntu 24.04 环境下完成了 GPGPU 方向的 10 个实验。实验时的记录为 17 个 QTest 测例全部通过。相比测试结果,我更看重这次实验帮我串联起的知识:一个虚拟设备如何被注册,如何对外暴露寄存器和显存,又如何在设备内部执行一段 kernel。

专业阶段

从 PCIe 外设开始

这次实验实现的是一个教学用途的虚拟 PCIe GPGPU。我主要修改了两个文件:hw/gpgpu/gpgpu.c 负责设备主体,hw/gpgpu/gpgpu_core.c 负责 SIMT 执行和指令解释。寄存器偏移、状态位和数据结构等基础定义已由实验框架提供,我的任务是把手册和测题要求的行为落实到代码中。

设备通过 QEMU 的 QOM 注册为 PCI 设备,并在 realize 阶段创建三个 BAR:BAR0 是 1 MiB 的控制寄存器空间,BAR2 是默认 64 MiB 的 VRAM,BAR4 是 64 KiB 的 Doorbell 空间。代码同时设置了 PCI 的 Vendor ID、Device ID 和 3D Controller 类别,使这个对象真正具备了 PCIe 设备的外形。

写完这一部分后,我对 MMIO 有了更直观的理解。CPU 读写的看似是普通地址,QEMU 却会把访问路由到设备的回调函数。读取设备 ID 时,回调返回固定标识;写入 GLOBAL_CTRL 时,会改变设备的使能或复位状态;写入 DISPATCH 时,则可能启动一次 kernel 执行。

因此,设备建模并不是给一组寄存器填值,而是实现一台可以被软件观察和控制的状态机。例如,设备复位后恢复 READY,执行 kernel 时进入 BUSY,无效的分发参数会设置错误位,W1C 寄存器则需要按“写 1 清除”的语义处理。这些状态之间的关系,比单个寄存器的数值更重要。

VRAM、DMA 与完成状态

BAR2 后面是一块动态分配的内存,用来模拟 VRAM。它既存放 kernel 指令,也存放输入和输出数据。在 kernel 测试中,QTest 首先把指令和数据写入 VRAM,再配置 kernel 地址、Grid 和 Block 维度,最后通过 DISPATCH 触发执行。SIMT 核心从同一块 VRAM 取指,计算后再把结果写回。

代码中还实现了 Host→VRAM 和 VRAM→Host 两个方向的 DMA 处理,使用 QEMU 的物理内存访问接口与 VRAM 之间搬运数据,并用 1 ms 虚拟定时器更新完成状态。如果 VRAM 偏移或长度不合法,代码会设置 DMA 错误。

这部分让我认识到,DMA 建模的核心不只是数据复制,还包括 BUSYCOMPLETEERROR 等软件可见的状态。当前代码也注册了 MSI-X 和 MSI 能力,并维护中断使能与状态寄存器;不过完成路径只更新 IRQ_STATUS,并未真正向 PCIe 总线递送 MSI/MSI-X 通知。这也说明,“有中断寄存器”和“完成整条中断链路”是两件事。

从设备模型走到 SIMT 执行

实验最有意思的部分,是在虚拟设备内加入一个简化的 SIMT 执行核心。它按 Grid、Block、Warp 和 Lane 组织线程。一个 Warp 最多包含 32 个 Lane,每个 Lane 有独立的整数寄存器、浮点寄存器、PC、fcsrmhartid。Warp 内的活跃 Lane 执行同一条指令,active_mask 用来记录哪些 Lane 仍在运行。

我按实验规格实现了 RV32I 基础指令的主要类别,包括算术逻辑、分支跳转、Load/Store 和 CSR 访问等。每个 Warp 还设置了 100,000 周期的执行上限,避免错误 kernel 让模拟器陷入死循环。

整数 kernel 测试的任务很简单:让 8 个线程分别把自己的 Lane ID 写入输出数组。Lane 通过 mhartid CSR 获取身份,提取低 5 位得到 Lane ID,再计算输出地址。这个小 kernel 把线程编号、指令解释和 VRAM 写回连在了一起。它也让我看到,上层编程模型中的“线程身份”,最终会落到明确的寄存器和位域上。

当前模型按顺序遍历 Block 和 Warp,没有模拟真实 GPU 的并行调度、缓存或流水线,也没有实现完整的分支发散与重汇合。但对于这个功能性实验,它已经表达了 Warp 锁步执行和 Lane 独立状态这两个核心概念。这让我理解到,虚拟设备建模首先要明确模型的目标,功能模型并不需要复制真实硬件的全部复杂性。

浮点指令与低精度格式

在 RV32I 之上,实验还要求实现 RV32F 浮点运算。代码使用 QEMU SoftFloat 处理加减乘除、平方根、比较、整数与浮点转换以及融合乘加等指令,并维护每个 Lane 的舍入模式和异常标志。浮点 kernel 测试让 8 个线程分别计算 2 * tid + 1,验证了指令解释、SIMT 执行和结果写回的组合路径。

最后三个测例覆盖了 BF16、FP8 E4M3、FP8 E5M2 和 FP4 E2M1 的代表值往返及饱和行为。我在代码中实现了相应的自定义转换指令:BF16 保留较宽的指数,E4M3 在本实验规格中对超出范围的值饱和到 448,E2M1 只有很少的可表示值,最大幅值为 6。

这部分让我意识到,低精度并不是简单删掉几个比特。指数和尾数如何分配,直接决定了数值范围和精度;目标格式能否表示 Inf/NaN,又决定了溢出时应当保留无穷还是饱和到最大值。虽然测例只验证了若干代表值,它已经让我对 AI 计算中“用数值精度换取存储和计算效率”有了更具体的认识。

测试与模型边界

本实验使用 QTest/QOS 在宿主机侧启动 QEMU,并直接访问 PCI BAR,不需要编写客户机驱动。我在实验中逐渐形成了一个很实用的顺序:先读硬件手册理解设计意图,再读寄存器定义和测例确定可观察行为,完成实现后先跑单项测试,最后用全部测例检查功能之间是否互相影响。

17 个测例通过,说明实验列出的这些检查点满足预期,但不代表它已经是一个完整的真实 GPU。当前模型中的 Doorbell、共享内存和跨 Warp Barrier 并未完整实现,调度也是顺序的,DMA 测例只检查了配置寄存器。认清“代码里有某条路径”、“测例验证了该路径”和“模型完整符合真实硬件”之间的差别,也是这次实验给我的一个重要提醒。

总结

作为一名即将进入研究生阶段的学生,这次实验帮我把原本分散的知识连成了一条清晰的链路:硬件规格经过 PCIe BAR 和 MMIO 变成软件可访问的接口,寄存器写入驱动设备状态转换,kernel 分发进一步连到 SIMT、指令集和数值格式,最后由 QTest 把这些行为变成可重复的验证。

我现在对“虚拟设备建模”的理解也比之前具体了许多:它不是机械翻译硬件手册,也不是追求把所有物理细节都复制出来,而是根据模型的目标,准确实现软件能够观察的接口、状态和边界行为。对研 0 阶段的我来说,这个教学用 GPGPU 模型并不是终点,而是我继续学习 QEMU、设备模型与加速器架构的一个很好起点。