ROCm rocr-libhsakmt分析系列5-1: Event 机制全景—从 Doorbell 到 Signal

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 是否完成:

  1. 盲等固定时间------浪费时间且不可靠。
  2. 疯狂轮询硬件寄存器------消耗 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;
    };
};

内核的核心职责:

  1. 管理 Event 对象:通过 IDR(ID Radix tree)分配和查找 event。
  2. 管理 Signal Page:分配一整页(或多页)64-bit slot 数组,映射给用户态和 GPU。
  3. 中断分发 :接收硬件中断 → 查找对应 event → 设置 signaled = truewake_up_all(&ev->wq)
  4. 阻塞等待 :用户态 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 完成了。

有了本篇的知识,我们已经知道答案的框架:

  1. 为每个 Queue 创建一个 SIGNAL event
  2. 在每个 Queue 的命令流末尾插入 RELEASE_MEM 包,关联对应的 event
  3. hsaKmtWaitOnMultipleEvents(events[], N, WaitOnAny, timeout) 等待任一 Queue 完成
  4. 或者用轮询 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() 这个中断处理的核心函数,理解从硬件中断到用户态唤醒的完整路径。

相关推荐
DeeplyMind15 天前
Rocm rocr-libhsakmt Event 机制技术文章预告
libhsakmt·rocr-runtime·hsa event
DeeplyMind1 个月前
AMDGPU驱动中Doorbell与Event机制简要对比
rocm·doorbell·hsa event
DeeplyMind2 个月前
第05章:HSA-API快速入门
agent·signal·queue·rocm·rocr-runtime·hsa
DeeplyMind3 个月前
ROCm rocr-libhsakmt分析系列4: HsaMemFlags分析
rocm·rocr·libhsakmt·hsamemflags
DeeplyMind7 个月前
rocr专栏介绍
linux·ai·amdgpu·rocm·rocr·libhsakmt·thunk