event机制主线:一个进程通过 Thunk 层提交了多个 User Queue,某个时刻需要知道"哪个 Queue 干完了"。event机制共 4 篇文章,围绕这一场景逐层展开 ROCm / KFD 的 Event 机制。
本篇是第 1 篇,目标是不看代码也能理解 Event 的定位、分类和全局架构。后续文章将深入内核实现(第 2 篇)、Thunk 层封装(第 3 篇)和多 Queue 完成检测实战(第 4 篇)。
1. GPU 异步编程的两个核心问题
GPU 计算的核心特征是 异步 ------CPU 把活儿扔给 GPU,然后各干各的。这带来两个必须回答的问题:双方相互通知的问题。在ROCm里,CPU通知GPU的机制叫Doorbell,GPU通知CPU的机制叫Event。
| 问题 | 方向 | 现有机制 |
|---|---|---|
| "活儿来了,快来取" | CPU → GPU | Doorbell(门铃寄存器写入) |
| "活儿干完了" / "出事了" | GPU → CPU | Event(事件通知) |
1.1 Doorbell 回顾
Doorbell 是一个 单向触发机制:CPU 向一个 MMIO 寄存器写入队列指针,GPU 的 Command Processor 感知到后开始拉取命令。它只负责"叫醒"GPU,不提供任何反馈。
Doorbell的机制分析请移步:doorbell机制概览。
1.2 为什么需要 Event
如果没有 Event,CPU 只有两种方式知道 GPU 是否完成:
- 盲等固定时间------浪费时间且不可靠。
- 疯狂轮询硬件寄存器------消耗 CPU 且无法扩展到多 Queue 场景。
Event 机制解决的就是 反馈问题:
- 精确感知:确切知道哪个 Queue 在哪个时间点完成了。
- 异常上报:Page Fault、ECC 错误、GPU Hang 等异常通过 Event 传递给用户态。
- 流水线编排:Queue B 可以在 Queue A 发出 Event 后自动启动,无需 CPU 介入。
2. Event 的九种类型
ROCm 在 hsakmttypes.h 中定义了完整的事件类型枚举:
c
typedef enum _HSA_EVENTTYPE
{
HSA_EVENTTYPE_SIGNAL = 0, // 用户态 GPU signal
HSA_EVENTTYPE_NODECHANGE = 1, // HSA 节点热插拔
HSA_EVENTTYPE_DEVICESTATECHANGE = 2, // 设备启停
HSA_EVENTTYPE_HW_EXCEPTION = 3, // GPU shader 异常
HSA_EVENTTYPE_SYSTEM_EVENT = 4, // GPU SYSCALL
HSA_EVENTTYPE_DEBUG_EVENT = 5, // 调试信号
HSA_EVENTTYPE_PROFILE_EVENT = 6, // 性能分析信号
HSA_EVENTTYPE_QUEUE_EVENT = 7, // Queue idle / EOP
HSA_EVENTTYPE_MEMORY = 8, // 内存访问异常
HSA_EVENTTYPE_MAXID,
} HSA_EVENTTYPE;
我们可以把它们分成三大类来理解:
2.1 同步类事件(最常用)
| 类型 | 典型场景 |
|---|---|
| SIGNAL (0) | 用户态最常用的完成通知。GPU 执行到命令流中的 RELEASE_MEM 包时触发。对应 ROCr 层的 hsa_signal_t。 |
| QUEUE_EVENT (7) | Queue 进入 idle 状态(EOP, End of Pipe)时触发。可用于检测整个 Queue 是否空闲。 |
| DEBUG_EVENT (5) | 调试专用 signal。在内核中行为与 SIGNAL 相同(共享 Signal Page slot),但语义上专用于调试器断点等。 |
| PROFILE_EVENT (6) | 性能分析专用信号。 |
关键点:SIGNAL 和 DEBUG_EVENT 在内核中统称为"signal event",它们共享同一个 Signal Page,每个 event 占一个 64-bit slot。这是整个 Event 机制的性能核心路径。
2.2 异常类事件(错误上报)
| 类型 | 典型场景 |
|---|---|
| MEMORY (8) | GPU 访问内存时发生异常:页不存在(NotPresent)、写只读页(ReadOnly)、执行 NX 页(NoExecute)、ECC 错误等。 |
| HW_EXCEPTION (3) | GPU Shader 执行异常、GPU Hang、需要 reset 等严重硬件错误。 |
| SYSTEM_EVENT (4) | GPU 发起的 SYSCALL,附带参数信息。 |
异常类事件不由用户态主动触发,而是由硬件/内核在检测到问题时自动产生。每种类型都有专门的数据结构来传递详细的错误信息。以 MEMORY 事件为例:
c
typedef struct _HsaMemoryAccessFault
{
HSAuint32 NodeId; // 发生异常的 GPU 节点
HSAuint64 VirtualAddress; // 出错的虚拟地址
HsaAccessAttributeFailure Failure; // 失败原因(位域)
HSA_EVENTID_MEMORYFLAGS Flags; // 可恢复 / 致命进程 / 致命 VM
} HsaMemoryAccessFault;
其中 HsaAccessAttributeFailure 是一个精心设计的位域结构:
c
typedef struct _HsaAccessAttributeFailure
{
unsigned int NotPresent : 1; // 页不存在
unsigned int ReadOnly : 1; // 写只读页
unsigned int NoExecute : 1; // 执行 NX 页
unsigned int GpuAccess : 1; // 仅 Host 可访问
unsigned int ECC : 1; // RAS ECC 错误(不可恢复)
unsigned int Imprecise : 1; // 无法确定精确地址
unsigned int ErrorType : 3; // 0=无RAS, 1=ECC_SRAM, 2=Poison, 3=GPU Hang
unsigned int Reserved : 23;
} HsaAccessAttributeFailure;
2.3 拓扑类事件(较少使用)
| 类型 | 典型场景 |
|---|---|
| NODECHANGE (1) | HSA 节点(GPU/CPU)动态添加或移除。 |
| DEVICESTATECHANGE (2) | 设备启动或停止。 |
这两种在常规计算场景中很少遇到,主要用于热插拔和设备管理。
3. 三层架构:硬件 → 内核 → 用户态
Event 机制并非单一技术,而是贯穿硬件、内核驱动和用户态运行时的完整链路。
+-----------------------------------------------------------+
| User Space |
| |
| +-------------+ +--------------+ +---------------+ |
| | Application |--->| ROCr Runtime |-->| libhsakmt | |
| | (HIP/HSA) | | hsa_signal_t | | hsaKmtWait.. | |
| +-------------+ +--------------+ +-------+-------+ |
| | ioctl |
+------------------------------------------------+----------+
| Kernel Space | |
| v |
| +-----------------------------------------------------+ |
| | KFD (Kernel Fusion Driver) | |
| | | |
| | +--------------+ +------------+ +-------------+ | |
| | | kfd_event | |Signal Page | | wait_queue | | |
| | | (IDR manage) | |(shared mem)| |(block wait) | | |
| | +--------------+ +------+-----+ +-------------+ | |
| +-------------------------------+---------------------+ |
| | interrupt |
+----------------------------------+------------------------+
| Hardware | |
| | |
| +-------------------------------v---------------------+ |
| | GPU | |
| | +--------------+ +--------------+ | |
| | | RELEASE_MEM | | MSI-X | | |
| | | (write slot) | | Controller | | |
| | +--------------+ +--------------+ | |
| +-----------------------------------------------------+ |
+-----------------------------------------------------------+
3.1 硬件层:两种触发方式
GPU 有两种方式告知 CPU "事情发生了":
方式一:内存原子写(RELEASE_MEM 包)
GPU 的 Command Processor 在执行命令流时遇到 RELEASE_MEM PM4 包,会执行一次原子写操作,将 event_id 写入预定的内存地址(Signal Page 中的一个 slot)。这不需要 CPU 参与,非常快。
方式二:硬件中断(MSI-X)
GPU 通过 PCIe MSI-X 机制向 CPU 发送中断。CPU 的中断处理程序(ISR)接收并解析中断,识别出是哪个进程、哪个 event 被触发。
在实际使用中,这两种方式通常 同时发生 :RELEASE_MEM 包可以配置为"写内存 + 发中断",这样:
- 用户态可以通过 轮询 slot 获得最低延迟
- 也可以通过 内核中断 + wait_queue 获得阻塞等待能力
3.2 内核层:KFD Event 对象
内核中的 KFD 模块充当中间人:
c
struct kfd_event {
u32 event_id; // 唯一标识,signal event 同时是 slot index
u64 event_age; // 年龄计数器(优化重复唤醒)
bool signaled; // 是否已被触发
bool auto_reset; // 触发后是否自动重置
int type; // KFD_EVENT_TYPE_SIGNAL / MEMORY / ...
spinlock_t lock; // 保护 event 状态
wait_queue_head_t wq; // 等待者链表
uint64_t __user *user_signal_address; // 指向 Signal Page 中对应 slot
union { // 异常事件的详细数据
struct kfd_hsa_memory_exception_data memory_exception_data;
struct kfd_hsa_hw_exception_data hw_exception_data;
};
};
内核的核心职责:
- 管理 Event 对象:通过 IDR(ID Radix tree)分配和查找 event。
- 管理 Signal Page:分配一整页(或多页)64-bit slot 数组,映射给用户态和 GPU。
- 中断分发 :接收硬件中断 → 查找对应 event → 设置
signaled = true→wake_up_all(&ev->wq)。 - 阻塞等待 :用户态 ioctl 进入内核 → 线程在
ev->wq上睡眠 → 被中断唤醒后返回。
3.3 用户态:Thunk 层封装
libhsakmt(Thunk 层)对内核 ioctl 做了薄封装:
| Thunk API | 对应内核 ioctl | 功能 |
|---|---|---|
hsaKmtCreateEvent() |
AMDKFD_IOC_CREATE_EVENT |
创建 event,分配 signal slot |
hsaKmtDestroyEvent() |
AMDKFD_IOC_DESTROY_EVENT |
销毁 event |
hsaKmtSetEvent() |
AMDKFD_IOC_SET_EVENT |
CPU 端手动 signal |
hsaKmtResetEvent() |
AMDKFD_IOC_RESET_EVENT |
重置 event 为 unsignaled |
hsaKmtWaitOnEvent() |
AMDKFD_IOC_WAIT_EVENTS |
阻塞等待 |
hsaKmtWaitOnMultipleEvents() |
AMDKFD_IOC_WAIT_EVENTS |
等待多个 event |
再往上,ROCr Runtime 将 Thunk Event 封装为 hsa_signal_t,HIP Runtime 进一步封装为 hipEvent_t。本专栏只专注于Thunk层,ROCr Runtime和HIP Runtime请查看其他相关专栏。
4. Signal Page:性能核心路径
Signal Page 是整个 Event 机制中最关键的数据结构,值得单独拿出来讲。这里先简要概括下,看后面的分析专文。
4.1 布局
Signal Page(一段连续内存,最多 KFD_SIGNAL_EVENT_LIMIT × 8 字节)
┌──────────┬──────────┬──────────┬─────┬──────────┐
│ Slot 0 │ Slot 1 │ Slot 2 │ ... │ Slot N-1 │
│ (64-bit) │ (64-bit) │ (64-bit) │ │ (64-bit) │
└──────────┴──────────┴──────────┴─────┴──────────┘
▲ ▲
│ │
event_id=0 event_id=N-1
- 每个 slot 初始值为
UNSIGNALED_EVENT_SLOT(即0xFFFFFFFFFFFFFFFF) - 当 GPU signal 一个 event 时,将 event_id 写入对应 slot
- 用户态可以直接读 slot 值来判断是否 signaled(轮询模式)
KFD_SIGNAL_EVENT_LIMIT默认为 4096(老内核兼容模式为 256)
4.2 三方共享
Signal Page 被三方同时访问:
| 访问者 | 如何访问 | 做什么 |
|---|---|---|
| GPU | 通过设备虚拟地址写入 | RELEASE_MEM 包将 event_id 写入 slot |
| 内核 | 通过 kernel_address 读取 |
中断处理时检查 slot 值,识别哪个 event 被 signal |
| 用户态 | 通过 mmap 得到的 user_address 读取 |
轮询模式直接读取 slot |
4.3 为什么 event_id 同时是 slot index
这是一个精巧的设计:signal event 的 event_id 就是它在 Signal Page 中的下标。这意味着:
- GPU 只需知道 event_id,就知道往哪个地址写
- 内核只需知道 event_id,就知道检查哪个 slot
- 无需额外的映射表,查找是 O(1) 的
非 signal event(MEMORY、HW_EXCEPTION 等)的 event_id 从 KFD_FIRST_NONSIGNAL_EVENT_ID(约 INT_MAX/2 + 1)开始,不占用 Signal Page slot。
5. Event 的完整生命周期
以最常见的 SIGNAL event 为例,因为是并发执行,这里并不能完美展现这种并发,请大家谅解。这里保留了两种时序图,大家习惯哪种看哪种。
CPU 侧 GPU 侧
───────── ─────────
1. Create hsaKmtCreateEvent(SIGNAL)
────ioctl──────────▶ 内核分配 kfd_event
分配 signal slot
返回 event_id
◀───────────────────
2. Submit 将 event 关联到命令流:
在 Queue 命令流末尾插入
RELEASE_MEM 包
(目标地址 = signal_slot_addr,
数据 = event_id,
INT_SEL = send_interrupt)
─────Doorbell──────▶ GPU 开始执行命令
3. Execute GPU 逐条执行命令
...
...
4. Signal 执行到 RELEASE_MEM:
① 将 event_id 写入 slot
② 发送 MSI-X 中断
◀──interrupt──
5. Deliver 内核 ISR 触发
kfd_signal_event_interrupt()
→ 查找 event
→ ev->signaled = true
→ wake_up_all(&ev->wq)
6. Wait hsaKmtWaitOnEvent() 返回
(或轮询 slot 发现值已改变)
─── 应用程序继续执行 ───
7. Destroy hsaKmtDestroyEvent()
────ioctl──────────▶ 内核释放 kfd_event
释放 signal slot
轮询模式替代路径 :在上图的 Wait 阶段,用户态也可以不走 ioctl,而是直接读 events_page[slot_index],当值从 0xFFFFFFFFFFFFFFFF 变为 event_id 时即可判定完成------延迟更低,但 CPU 开销更高。
下面是软件工程中常用的泳道时序图模式。
User App libhsakmt KFD (Kernel) Signal Page GPU
| | | | |
| CreateEvent() | | | |
|------------------>| | | |
| | ioctl(CREATE) | | |
| |-------------------->| | |
| | | alloc kfd_event | |
| | | alloc slot[id] | |
| | |------------------->| |
| | | slot[id] = 0xFF..FF (UNSIGNALED) |
| | | | |
| | return event_id | | |
| |<--------------------| | |
| event_id | | | |
|<------------------| | | |
| | | | |
| Insert RELEASE_MEM pkt into Queue | | |
| (addr=slot[id], data=event_id, | | |
| INT_SEL=send_interrupt) | | |
| | | | |
| Write Doorbell | | | |
|----------------------------------------------------------------- - - - - -- -->|
| | | | |
| | | | Execute |
| | | | commands |
| | | | ... |
| | | | |
| | | | RELEASE_MEM: |
| | | |<----------------|
| | | | write event_id |
| | | | into slot[id] |
| | | | |
| | | MSI-X interrupt |
| | |<-------------------------------------|
| | | | |
| | | kfd_signal_event_interrupt() |
| | | lookup event by partial_id |
| | | ev->signaled = true |
| | | wake_up_all(&ev->wq) |
| | | | |
| WaitOnEvent() | | | |
|------------------>| | | |
| | ioctl(WAIT) | | |
| |-------------------->| | |
| | | (already signaled)| |
| | return SUCCESS | | |
| |<--------------------| | |
| completed! | | | |
|<------------------| | | |
| | | | |
| DestroyEvent() | | | |
|------------------>| | | |
| | ioctl(DESTROY) | | |
| |-------------------->| | |
| | | wake waiters | |
| | | idr_remove | |
| | | kfree_rcu(event) | |
| | | free slot[id] | |
| | |------------------->| |
| | | | |
| | return OK | | |
| |<--------------------| | |
| done | | | |
|<------------------| | | |
| | | | |
6. 与其他同步原语的对比
理解 Event 在同步原语家族中的位置,有助于选择正确的工具。
6.1 KFD Event vs Linux Kernel DMA Fence
| 维度 | KFD Event | DMA Fence |
|---|---|---|
| 层次 | 用户态可见(通过 ioctl) | 纯内核态 |
| 粒度 | 任意命令流位置(插 RELEASE_MEM) | 通常与一个 job/submission 绑定 |
| 多进程 | 限于创建它的 KFD 进程 | 可跨驱动共享(通过 sync_file) |
| 等待方式 | ioctl 阻塞 或 轮询 signal slot | dma_fence_wait() 在内核态 |
| 场景 | ROCm 计算 | 图形渲染、跨驱动同步 |
6.2 KFD Event vs CUDA Event
| 维度 | KFD Event (HSA Signal) | CUDA Event |
|---|---|---|
| 记录时间戳 | 需额外机制 | 内建 cudaEventElapsedTime |
| 跨 Stream 同步 | 通过多 event wait | cudaStreamWaitEvent |
| 用户态轮询 | 直接读 signal slot(共享内存) | 需调用 cudaEventQuery |
| 底层机制 | Signal Page + MSI-X 中断 | Semaphore + 中断 |
6.3 快速选型指南
| 需求 | 推荐机制 |
|---|---|
| 知道 GPU Kernel 何时完成 | HSA_EVENTTYPE_SIGNAL + WaitOnEvent |
| 知道 Queue 是否空闲 | HSA_EVENTTYPE_QUEUE_EVENT(EOP) |
| 捕获 GPU 内存异常 | HSA_EVENTTYPE_MEMORY |
| 多 Queue 间依赖编排 | Signal Event + WAIT_REG_MEM PM4 包 |
| 超低延迟完成检测 | 轮询 Signal Page slot |
| 内核态 job 同步 | DMA Fence(不走 KFD Event) |
7. 回到我们的场景
让我们回到系列文章的主线问题:
一个进程提交了多个 User Queue,在某个时刻需要知道哪个 Queue 完成了。
有了本篇的知识,我们已经知道答案的框架:
- 为每个 Queue 创建一个 SIGNAL event
- 在每个 Queue 的命令流末尾插入 RELEASE_MEM 包,关联对应的 event
- 用
hsaKmtWaitOnMultipleEvents(events[], N, WaitOnAny, timeout)等待任一 Queue 完成 - 或者用轮询 Signal Page slot 的方式获得更低延迟
但具体如何实现?Signal Page 在内核中如何分配和映射?WaitOnMultipleEvents 在内核中走的是什么路径?这些问题将在后续文章中详细展开。
8. 小结
| 要点 | 内容 |
|---|---|
| Event 是什么 | GPU → CPU 的异步通知机制,解决"完成"和"异常"两个问题 |
| 九种类型 | 同步类(SIGNAL/QUEUE/DEBUG/PROFILE)、异常类(MEMORY/HW_EXCEPTION/SYSTEM)、拓扑类(NODECHANGE/DEVICESTATECHANGE) |
| 三层架构 | 硬件(RELEASE_MEM + MSI-X)→ 内核(kfd_event + Signal Page + wait_queue)→ 用户态(Thunk API → ROCr Signal → HIP Event) |
| Signal Page | 64-bit slot 数组,event_id 即 slot index,三方共享(GPU 写、内核读、用户态读) |
| 性能关键路径 | GPU 原子写 slot + 中断 → 内核唤醒 → 用户态返回;或用户态直接轮询 slot |
下篇预告 :第 2 篇将深入 kfd_events.c,详细分析内核如何管理 Event 对象、分配 Signal Page、处理中断、以及实现阻塞等待。我们将逐行走过 kfd_signal_event_interrupt() 这个中断处理的核心函数,理解从硬件中断到用户态唤醒的完整路径。