Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions docs/blogs/2026/qemu-camp-2026-liuhong-gpu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# QEMU 训练营 2026 专业阶段总结

!!! note "主要贡献者"

- 作者:[@liuhong](https://github.com/LiuHongsGitHub)

---

## 背景介绍

上海某学校大数据技术与工程研二学生,对系统编程感兴趣,希望通过QEMU训练营寻找一些idea,并且掌握一些CPU、GPU和模拟器的知识。

---

## 专业阶段

完成了GPU方向实验。

### GPU 实验理解概述

设备建模的核心是通过 MMIO 寄存器访问触发设备行为。GPGPU 实验的主要目标是模拟 RISC‑V 体系下的 GPU 寄存器读写、VRAM 访问、DMA 传输、SIMT 上下文调度,并实现一个简化的 RV32I/RV32F 指令解释器(含低精度浮点扩展)。

### 实验内容

17道测试覆盖了:设备识别、全局控制、VRAM访问、DMA传输、中断模拟、SIMT上下文与调度、kernel执行、低精度浮点数格式转换。

以执行指令的全流程为脉络,串联实验。

**阶段1:QEMU设备注册与初始化**

1.1 `gpgpu.c` 中执行 `type_init()` 方法将gpu这个类型注册到类型系统,父类为 `TYPE_PCI_DEVICE`。

1.2 基于QOM的面向对象思想,调用 `gpgpu_class_init` 进行类初始化。

1.3 QEMU 命令行指定 `-device gpgpu` 时,会调用 `gpgpu_realize()`。

```c
static void gpgpu_realize(PCIDevice *pdev, Error **errp)
{
GPGPUState *s = GPGPU(pdev);
uint8_t *pci_conf = pdev->config;

pci_config_set_interrupt_pin(pci_conf, 1);

s->vram_ptr = g_malloc0(s->vram_size);

/* BAR 0: control registers — 1MB MMIO */
memory_region_init_io(&s->ctrl_mmio, OBJECT(s), &gpgpu_ctrl_ops, s,
"gpgpu-ctrl", GPGPU_CTRL_BAR_SIZE);
pci_register_bar(pdev, 0, ... , &s->ctrl_mmio);

/* BAR 2: VRAM — 64MB MMIO */
memory_region_init_io(&s->vram, OBJECT(s), &gpgpu_vram_ops, s,
"gpgpu-vram", s->vram_size);
pci_register_bar(pdev, 2, ... , &s->vram);

/* BAR 4: doorbell — 64KB MMIO */
memory_region_init_io(&s->doorbell_mmio, OBJECT(s), &gpgpu_doorbell_ops, s,
"gpgpu-doorbell", GPGPU_DOORBELL_BAR_SIZE);
pci_register_bar(pdev, 4, ... , &s->doorbell_mmio);

// MSI-X 初始化
msix_init(pdev, GPGPU_MSIX_VECTORS, ...);

s->dma_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL, ...);

s->global_status = GPGPU_STATUS_READY;
}
```

例如 Guest CPU 读写 BAR0 地址时,QEMU 会通过 `MemoryRegionOps`(即 `gpgpu_ctrl_ops`)自动调用回调函数 `gpgpu_ctrl_read/write`,传入 **BAR 内部偏移量** 和 **设备状态指针**。

**阶段2:Guest 内核驱动初始化,进行 PCIe 设备发现/Probe。**

**阶段3:用户态程序准备数据。**

**阶段4:写 `GPGPU_REG_DISPATCH` 寄存器触发执行,对应实验4。**

4.1 调用 `gpgpu_dispatch_kernel`,再调用
```c
int gpgpu_core_exec_kernel(GPGPUState *s)
{
uint32_t grid_dim_x = s->kernel.grid_dim[0];
uint32_t grid_dim_y = s->kernel.grid_dim[1];
uint32_t grid_dim_z = s->kernel.grid_dim[2];
uint32_t block_dim_x = s->kernel.block_dim[0];
uint32_t block_dim_y = s->kernel.block_dim[1];
uint32_t block_dim_z = s->kernel.block_dim[2];

uint32_t threads_per_block = block_dim_x * block_dim_y * block_dim_z;
uint32_t warps_per_block = (threads_per_block + 31) / 32;
uint64_t kernel_addr = s->kernel.kernel_addr;

GPGPUWarp *warps = g_malloc(sizeof(GPGPUWarp) * warps_per_block);
...
```
4.2 Warp 初始化

```c
void gpgpu_core_init_warp(GPGPUWarp *warp, uint32_t pc, uint64_t kernel_args,
uint32_t thread_id_base, const uint32_t block_id[3],
uint32_t num_threads,
uint32_t warp_id, uint32_t block_id_linear)
{
// 清零 warp 结构
memset(warp, 0, sizeof(*warp));
// init warp meta data
warp->active_mask = num_threads >= 32 ? 0xFFFFFFFF : (1U << num_threads) - 1;
warp->thread_id_base = thread_id_base;
warp->warp_id = warp_id;
memcpy(warp->block_id, block_id, sizeof(warp->block_id));
for (int i = 0; i < GPGPU_WARP_SIZE; i++) {
GPGPULane *lane = &warp->lanes[i];
lane->pc = pc;
lane->mhartid = MHARTID_ENCODE(block_id_linear, warp_id, i);
lane->fcsr = 0;
lane->active = i < num_threads;
if (lane->active) {
lane->gpr[11] = thread_id_base + i;
lane->gpr[10] = (uint32_t)kernel_args;
}
set_default_nan_mode(1, &lane->fp_status);
set_float_default_nan_pattern(0b01000000, &lane->fp_status);
}
}
```

使用了 RISC-V 的 `a0` 寄存器保存发送给 gpgpu 的指针,`a1` 保存线程 id。

4.3 执行每个 warp,核心方法是 `decode_and_exec`。

**阶段5:译码 (decode)**

```c
static inline void decode_and_exec(GPGPUState *s, GPGPULane *lane, uint32_t inst)
{
uint8_t opcode = inst & 0x7F;
uint8_t rd = (inst >> 7) & 0x1F;
uint8_t rs1 = (inst >> 15) & 0x1F;
uint8_t rs2 = (inst >> 20) & 0x1F;
uint8_t rs3 = (inst >> 27) & 0x1F;

uint32_t imm_i = sext32(inst >> 20, 12);
uint32_t imm_s = sext32(((inst >> 25) << 5) | ((inst >> 7) & 0x1F), 12);
uint32_t imm_b = sext32(((inst >> 31) << 12) | ((inst >> 7) & 1) << 11 |
((inst >> 25) & 0x3F) << 5 | ((inst >> 8) & 0xF) << 1, 13);
uint32_t imm_u = inst & 0xFFFFF000;
int32_t imm_j = sext32(((inst >> 31) << 20) | ((inst >> 12) & 0xFF) << 12 |
((inst >> 20) & 1) << 11 | ((inst >> 21) & 0x3FF) << 1, 21);
uint32_t shamt = (inst >> 20) & 0x1F;
uint32_t funct3 = (inst >> 12) & 0x7;
uint32_t funct7 = (inst >> 25) & 0x7F;
switch (opcode) {
case 0x37:
if (rd != 0) lane->gpr[rd] = imm_u;
return;
...
```

译码过程中使用了相当多的 `switch case` 语句,单纯堆代码不是好的选择。

**阶段6:执行 (execute)**

6.1 模拟 `flw` 指令执行过程中,GPGPU 访问 VRAM 直接使用 `memcpy`。模拟 Host 访问 VRAM 的 DMA 数据搬运则通过 `MemoryRegionOps` 中的回调函数完成。

```c
case 0x07:
{
uint32_t addr = lane->gpr[rs1] + imm_i;
if (addr < s->vram_size) {
if (funct3 == 2) { // FLW
uint32_t val;
memcpy(&val, s->vram_ptr + addr, 4);
lane->fpr[rd] = val;
}
}
return;
}
...
```
6.2 模拟 fcvt.e2m1.s fd, fs1(FP32 → E2M1)。由于 E2M1 仅能表示 0/0.5/1.0/1.5/2.0/3.0/4.0/6.0 共 8 个正数值,转换时取 FP32 输入的绝对值的位模式(uint32_t),与各相邻 E2M1 代表值的 FP32 中点阈值做整数比较以确定所属区间。超出最大值 6.0 的输入饱和到 ±6.0,NaN/Inf 同样饱和。最后加回符号位得到 4-bit 结果。

区间划分 (正半轴):

| 区间 | 映射值 | 说明 |
|-----------------|--------|-------------------------------|
| `[0, 0.25)` | 0 | |
| `[0.25, 0.75)` | 0.5 | 中点在 0.5,0.25~0.75 选 0.5 |
| `[0.75, 1.25)` | 1.0 | |
| `[1.25, 1.75)` | 1.5 | |
| `[1.75, 2.5)` | 2.0 | |
| `[2.5, 3.5)` | 3.0 | |
| `[3.5, 5.0)` | 4.0 | |
| `[5.0, +∞)` | 6.0 | |

```c
case 1: {
uint32_t f32_val = lane->fpr[rs1];
bool sign = (f32_val >> 31) & 1;
uint8_t exp32 = (f32_val >> 23) & 0xFF;
uint32_t m23 = f32_val & 0x7FFFFF;
uint32_t abs_val = f32_val & 0x7FFFFFFFU;
uint8_t e2m1_result;
if (exp32 == 0 && m23 == 0) {
e2m1_result = sign ? 0x8 : 0x0;
} else if (exp32 == 255) {
e2m1_result = sign ? 0xF : 0x7;
} else {
if (abs_val < 0x3D800000U) {
e2m1_result = 0x0;
} else if (abs_val < 0x3F400000U) {
e2m1_result = 0x1; // 0.5
} else if (abs_val < 0x3FA00000U) {
e2m1_result = 0x2; // 1.0 (默认 RTZ/RNE)
} else if (abs_val < 0x3FE00000U) {
e2m1_result = 0x3; // 1.5
} else if (abs_val < 0x40200000U) {
e2m1_result = 0x4; // 2.0
} else if (abs_val < 0x40600000U) {
e2m1_result = 0x5; // 3.0
} else if (abs_val < 0x40A00000U) {
e2m1_result = 0x6; // 4.0
} else {
e2m1_result = 0x7; // 6.0
}
}
/* 加回符号位 */
if (sign) e2m1_result |= 0x8;
lane->fpr[rd] = e2m1_result;
}
break;
```

**阶段7:所有 warp 执行完后,返回 `gpgpu_dispatch_kernel` 方法,调用 `msix_notify`,向 Guest 注入 MSI-X 中断。**

---

### 问题

1. 对一些元概念和建模思想(如 MemoryRegion)缺乏了解时直接使用 AI 工具效率很低,先建立概念体系才能真正加速。

2. 代码较为冗余,不够简洁,可以让 AI 重新审查代码。

3. 缺乏建模概念和架构体系知识,对于设备的行为和模拟器的行为不能很好地对应。

---

### 总结

理解了 QEMU 设备建模的核心机制(PCI 注册、MemoryRegion/BAR 映射、MMIO 回调),掌握了 SIMT 架构下 Grid/Block/Warp 的调度与 RV32I/RV32F 指令解释器的简单实现方法,熟悉了 VRAM 访问和 DMA 传输的模拟路径。初步掌握 GDB 调试复杂项目的方法。后续继续使用 AI 辅助,多读 QEMU 源码以加深理解。

1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ nav:
- 专业阶段总结 yuanyiboyyb: blogs/2026/qemu-camp-2026-yuanyiboyyb.md
- 专业阶段总结 scrail: blogs/2026/qemu-camp-2026-scrail.md
- 专业阶段总结 Firegooder: blogs/2026/qemu-camp-2026-Firegooder.md
- 专业阶段总结 liuhong: blogs/2026/qemu-camp-2026-liuhong-gpu.md
- GPU 进阶实验总结:
- 进阶实验总结(一) - rd: blogs/2026/gpu/gpu-exper-blog-rd-ex1.md
- 进阶实验总结(一) - caspian: blogs/2026/gpu/gpu-exper-blog-caspian-ex1.md
Expand Down
Loading