1 user space queue
1.1 基本概念
ROCm 中使用了user queue,这个提法是与之前的kmd中的kernel queue相对的。其核心思想是让用户进程直接管理向 GPU 引擎提交的任务,绕过驱动程序的 IOCTL 调用,从而降低开销并允许 GPU 自行提交任务。应用程序可跨多个 GPU 引擎构建任务工作图,无需通过 CPU 中转。
UMD通过每个应用的共享内存区域直接与固件交互,主要载体是Queue。队列是一个包含读指针(rptr)和写指针(wptr)的环形缓冲区,并配有CPU对GPU的通知机制--doorbell。
1.2 术语表
| 术语 | 解释 |
|---|---|
| MQD(内存队列描述符) | 由内核驱动进行管理。引擎固件利用 MQD,能够动态地将队列从固件的运行列表中映射或取消映射。MQD 是内存里的一种数据结构,其作用是存储队列的配置信息。GPU 上各引擎的固件,会借助 MQD 加载驱动所指定的队列状态,并且在队列从固件的运行队列中取消映射时,存储来自硬件的队列状态。MQD 会存储队列的虚拟地址、读写指针、队列优先级以及描述队列状态的寄存器。对于内核模式队列,KGD 会创建并配置 MQD,然后通过固件对其进行映射以供使用。而在用户模式队列的情形下,创建队列的 IOCTL 调用会到达 KGD,由 KGD 为上下文创建一个 MQD,并按照用户的请求对其进行配置 |
| 队列内存 | 这是由 UMD 分配的、可供 GPU 访问的内存,用于存储引擎执行的队列。该内存是环形缓冲区的支撑,而环形缓冲区的作用是向引擎输送数据包 |
| 读取指针(RPTR)内存 | 由 UMD 分配的、可供 GPU 访问的内存,用于存储读取指针 |
| 写入指针(WPTR)内存 | 由 UMD 分配的、可供 GPU 访问的内存,用于存储写入指针 |
| 门铃内存 | UMD 可以分配的门铃 BAR 中的一个页面。门铃的用途是告知引擎环形缓冲区中有新数据需要执行。 |
| 门铃偏移量 | 门铃 BAR 中的一个索引,其作用是告知固件用户队列中有工作需要执行。每个引擎都会被分配一个双字(64 位)索引到门铃 BAR 中。在用户模式端,偏移量是相对于分配的门铃页面而言的。在核端,该偏移量会被转换为门铃 BAR 中的绝对偏移量,用于对 MQD 进行编程 |
1.3 工作原理
用户先把数据包写入环形缓冲区,接着对写入指针进行写入操作,以此告知引擎写入环形缓冲区的数据量。之后,用户通过读取读取指针,就能了解引擎的执行进度。随着 GPU 对数据包的不断消耗,读取指针最终会赶上写入指针。当两者相等时,引擎就处于空闲状态。若要真正启动引擎执行,用户还得对分配给该队列的门铃进行写入操作。门铃的作用是唤醒固件,并告知它检查环形缓冲区,查看是否有新数据。
因此每个用户队列需要多个缓冲区,包括环形缓冲区本身、rptr 缓冲区(固件向用户空间映射 rptr 的位置)、wptr 缓冲区(应用写入 wptr 供固件获取)和doorbell缓冲区。doorbell缓冲区是设备 MMIO BAR 的一部分,可映射到特定用户队列。
当应用创建用户队列时,需分配队列所需的缓冲区(环形缓冲区、wptr 和 rptr、doorbell、上下文保存区等),这些缓冲区可独立或属于一个更大的缓冲区。应用将缓冲区映射到 GPUVM,并使用用户队列所需内存区域的 GPU 虚拟地址,同时为用户队列分配门铃页面。随后,应用在 IOCTL 结构的 MQD 中填入 GPU 虚拟地址和所需的门铃索引,还可指定用户队列的属性(优先级、队列类型:AQL 或 PM4、是否为安全队列等),并调用用户队列创建 IOCTL 以基于指定的 MQD 创建队列。内核驱动验证应用提供的 MQD,并将其转换为特定 IP 的引擎 MQD 格式,分配特定 IP 的 MQD 并将队列添加到调度固件的运行列表中。队列创建后,应用可直接向队列写入数据包、更新 wptr,并写入门铃偏移量以启动用户队列的任务。
当应用不再使用用户队列时,调用用户队列销毁 IOCTL。内核驱动会抢占该队列并将其从调度固件的运行列表中移除,随后释放特定 IP 的 MQD 并清理用户队列状态。
总结下Queue 的基本工作流程如下:
-
应用写入数据包:应用将待执行的命令写入 queue 的环形缓冲区。
-
更新写指针(wptr):应用更新写指针,告知 GPU 新数据的范围。
-
写门铃(doorbell):应用向门铃寄存器写入,通知 GPU 检查 queue。
-
GPU 消费数据包:GPU 固件读取 queue,执行命令,并更新读指针(rptr)。
-
应用读取 rptr:应用可读取 rptr,了解 GPU 的执行进度。
-
队列空闲:当 rptr 追上 wptr,说明 queue 已被消费完毕,GPU 处于空闲状态。
1.3 IOCTL 接口
内核模式驱动需验证队列及相关数据(rptr、wptr、上下文保存区等)的 GPU 虚拟地址,防止用户指定无效地址。若用户提供无效 GPU 虚拟地址或门铃索引,IOCTL 应返回错误信息。驱动还应跟踪这些缓冲区,若用户尝试从 GPUVM 取消映射缓冲区,取消映射调用应返回错误。
-
INFO:用于查询用户队列所需元数据大小(如上下文保存区或映射缓冲区);
-
CREATE:创建用户队列,应用提供类似 MQD 的结构,指定队列类型及关联元数据和标志,返回队列 ID;
-
FREE:释放用户队列;
-
QUERY:查询队列状态,检查队列是否健康(如是否已重置);
-
USERQ_SIGNAL:USERQ_SIGNAL IOCTL 用于提供待信号通知的同步对象列表;
-
USERQ_WAIT:USERQ_WAIT IOCTL 用于提供待等待的同步对象列表。
2. KFD用户态实现
2.1 核心数据结构
typedef struct _HsaQueueResource
{
HSA_QUEUEID QueueId; /** queue ID */
/** Doorbell address to notify HW of a new dispatch */
union
{
HSAuint32* Queue_DoorBell;
HSAuint64* Queue_DoorBell_aql;
HSAuint64 QueueDoorBell;
};
/** virtual address to notify HW of queue write ptr value */
union
{
HSAuint32* Queue_write_ptr;
HSAuint64* Queue_write_ptr_aql;
HSAuint64 QueueWptrValue;
};
/** virtual address updated by HW to indicate current read location */
union
{
HSAuint32* Queue_read_ptr;
HSAuint64* Queue_read_ptr_aql;
HSAuint64 QueueRptrValue;
};
volatile HSAint64* ErrorReason; /** exception bits signal payload */
} HsaQueueResource;
可以看出,queue的资源数据结构与第一部分描述的完全一致:
1. 每个queue有一个QueueId;
2. 每个queue有一个用于通知GPU处理数据的doorbell;
3. 一个wptr和一个rptr。
该节就分析这么多,大家先记住这些概念,原理有个大概认识,后面会展示代码的具体实现。原理和代码反复对照看,相信一定会有收获。

被折叠的 条评论
为什么被折叠?



