PM4 是 AMD GCN/RDNA 图形 / 通用渲染的硬件原生命令包;AQL 是 ROCm/HSA 面向计算调度的用户态队列标准包;
PM4 命令包(Packet Format 4)
定位:GPU 命令处理器(CP)直接执行的底层指令包,用于图形渲染、寄存器配置、IB 提交、同步等,是 amdgpu 驱动命令流的核心。
单位:32bit(DWORD)为基本单元,包头 1 DWORD, payload 可变。
类型:Type 0/2/3(Type 1 已废弃)。
PM4 通用头部规则
- 单位:32bit(DWORD)
- 最高 2bit [31:30] 决定类型:
00= Type 001= Type 1(废弃)10= Type 211= Type 3(最常用)
IB (Indirect Buffer)
IB = 一段连续的 GPU 内存,里面从头到尾全是 PM4 命令包;
GPU 从 IB 首地址开始,按 DWORD 逐条解析 PM4,直到 IB 长度耗尽。
IB = Indirect Buffer
- 一块GPU 可访问的显存缓冲区(BO)
- 里面只存 PM4 命令流
- 通过
PKT3_IB命令让 GPU 跳过去执行
你可以理解成:
IB 是 PM4 命令的 “代码段”PKT3_IB 是一条 “call 指令”
IB 的完整结构
一个 IB 内部是连续平铺的 PM4 包,格式如下:
[IB 开始] DWORD0 : PM4 包1 头部 DWORD1 : PM4 包1 数据 DWORD2 : PM4 包2 头部 DWORD3 : PM4 包2 数据0 DWORD4 : PM4 包2 数据1 ... [IB 结束,共 N 个 DWORD]GPU 执行规则:
- 从
IB_BASE开始 - 每次读一个 DWORD 作为当前 PM4 包头部
- 根据头部类型(Type0/2/3)解析这个包占用多少 DWORD
- 指针跳过整个包,继续下一个
- 直到已读取 DWORD 总数 == IB 长度,停止
PM4 包头格式(32bit)
Type 0(寄存器批量写)
[31:30] type = 00 [29:0] addr = 寄存器基地址(29bit)- 作用:连续写 N 个 GPU 寄存器(如 CP、SH、CONTEXT 寄存器)
- payload:N 个 DWORD 寄存器值
GPU 怎么知道要写多少个?
GPU 命令处理器(CP)只认一条规则:
Type 0 会一直写,直到 IB 结束!
也就是说:
- 你在 IB 里放:
DWORD0: 0x0000F000 (Type0 + 基地址) DWORD1: 数据1 DWORD2: 数据2 DWORD3: 数据3 - GPU 会写:
- 地址 F000 ← 数据 1
- 地址 F004 ← 数据 2
- 地址 F008 ← 数据 3
写多少个?= IB 里 Type0 包头后面剩下多少 DWORD,就写多少个!
为什么这么设计?
因为Type0 是 “流模式”:
- 包头只告诉 GPU:开始写寄存器
- 后面所有 DWORD 都是寄存器值
- 直到IB 结束才停止
这是 AMD GPU 从 R600 到 RDNA3 一脉相承的硬件行为。
Type 2(NOP 填充)
[31:30] type = 10 [29:0] 保留(无意义)- 作用:仅用于对齐(如 16 DWORD 对齐),不执行任何操作。
Type 3(功能命令,最常用)
[31:30] type = 11 [29:16] count = payload DWORD 数 - 1 [15:8] opcode = 命令码(如 SET_CONTEXT_REG、DRAW_INDEX2、IB) [7:1] 保留 [0] predicate = 条件执行开关- 作用:执行复杂操作(绘制、IB 提交、同步、 barrier、EOP 等)。
核心 Type 3 Opcode(常用)
PKT3_NOP:空操作PKT3_SET_CONTEXT_REG:写上下文寄存器PKT3_SET_SH_REG:写 shader 引擎寄存器PKT3_IB:提交 Indirect Buffer(IB)PKT3_DRAW_INDEX2:索引绘制PKT3_RELEASE_MEM:写内存 / 信号(EOP 同步)PKT3_WAIT_REG_MEM:等待寄存器 / 内存条件
PM4 完整结构总图
┌─────────────────────────────────────────────┐ │ PM4 Packet │ ├─────────────┬───────────────────────────────┤ │ DWORD0 头部 │ Type0 / Type2 / Type3 │ ├─────────────┼───────────────────────────────┤ │ DWORD1~N │ Payload(数据/参数) │ └─────────────┴───────────────────────────────┘ Type0: [31:30]=00 | [29:0]=寄存器基地址 → 连续写寄存器 Type2: [31:30]=10 | 全0 → 填充对齐 Type3: [31:30]=11 | count-1 | opcode | shader | P → 执行命令PM4 提交流程(内核态)
用户态(Mesa)构建 PM4 流 → 写入 IB(GEM BO) →amdgpu_cs_ioctl→ 内核写入 Ring → GPU CP 解析执行 → 中断完成。
三种 PM4 包在 IB 中如何被解析
① Type3(带 count,最正常)
DWORD0: 11 | count-1 | opcode | ... ← 头部 DWORD1 ~ DWORD[count]:载荷- GPU 一看
type=11 - 取出
count - 知道这个包总长度:1 + count DWORD
- 读完直接跳到下一个包
② Type2(NOP)
DWORD0: 10 | xxxxxxxxxxxxxxxx ← 头部- 只有 1 个 DWORD
- 读完就结束
③ Type0(无 count,读到 IB 结束)
DWORD0: 00 | 寄存器基地址 ← 头部 DWORD1: 值1 DWORD2: 值2 ... 直到 IB 结束- 一旦解析到
type=00 - GPU 进入连续写寄存器模式
- 后面所有剩余 DWORD 全是寄存器值
- 直到 IB 长度耗尽才退出这个模式
一个真实可执行的 IB 示例
假设 IB 长度 =9 DWORD
IB 地址: 0x10000000 IB 大小: 9 DWORD DW0: 0xB8106E00 → Type3, SET_SH_REG, count=1 DW1: 0x00001234 → 寄存器偏移 DW2: 0xABCD0001 → 寄存器值 DW3: 0x0000F000 → Type0, 基地址 0xF000 DW4: 0x00000001 → 写 F000 DW5: 0x00000002 → 写 F004 DW6: 0x00000003 → 写 F008 DW7: 0xB8000000 → Type3 NOP DW8: 0xB8506000 → Type3 RELEASE_MEM(但不会执行!)GPU 执行过程:
- 读 DW0 → Type3,count=1→ 包长度 = 1+1 = 2 DW→ 执行 DW0~DW1,跳过 DW2
- 当前位置 DW3
- 读 DW3 → Type0!→ 进入 “写寄存器直到 IB 结束” 模式→ 剩余 DW 数量 = 9 - 3 = 6 个→ 把 DW3 后面全部 6 个 DW 都当寄存器值写掉
- 到达 IB 长度 9 → IB 执行结束
- DW7/DW8 根本不会被当成 PM4 解析!
这就是为什么:
Type0 必须放在 IB 最后!
Type0 后面不能跟任何命令!
IB 是怎么被 “启动” 的?PKT3_IB
内核发给 Ring 的命令只有一条:
DW0: 0xB8304000 → Type3 | count=3 | opcode=IB DW1: IB 地址低 32 位 DW2: IB 地址高 32 位 DW3: IB 长度(DWORD 个数)GPU 收到后:
- 跳转到
IB 地址 - 按上面规则逐条解析 PM4
- 执行满
IB 长度个 DWORD 后退出 - 回到 Ring,继续执行下一条命令
PKT3_IB 命令
PKT3_IB = PM4 Type3 命令,作用:让 GPU 跳转到指定 IB 缓冲区执行 PM4 命令流
- 类型:Type3(11)
- 总长度:4 DWORD(1 个头部 + 3 个数据 DWORD)
- 核心:携带IB 64 位基地址+IB 长度(DWORD 数)
命令格式(完整 4 DWORD)
DWORD0:PM4 Type3 包头
bit31-30: type = 11 (Type3) bit29-16: count = 2 (因为数据段3 DWORD,count=3-1=2) bit15-8 : opcode = 0x40 (IB命令的操作码) bit7-0 : 保留/子功能(通常0)包头十六进制示例:0xB8204000
0xB8= 10111000 → type=110x20= count=20x40= opcode=IB
DWORD1:IB_BASE_LOW(IB 地址低 32 位)
- 存放IB 64 位地址的低 32 位(GPU 虚拟地址 / 物理地址)
DWORD2:IB_BASE_HIGH(IB 地址高 32 位)
- 存放IB 64 位地址的高 32 位
DWORD3:IB_SIZE + 控制位
bit19-0 : ib_size = IB长度(DWORD数,最大1M) bit20 : chain = 0/1(是否链式IB,0=不链) bit21 : offload_polling = 0/1(是否轮询卸载) bit31-22: 保留(填0)完整十六进制示例(可直接写入 Ring)
假设:
- IB 地址:
0x100000000(64 位) - IB 长度:
9 DWORD - chain=0,offload_polling=0
DWORD0: 0xB8204000 → Type3 | count=2 | opcode=0x40 DWORD1: 0x00000000 → IB_BASE_LOW DWORD2: 0x00000001 → IB_BASE_HIGH DWORD3: 0x00000009 → IB_SIZE=9 | chain=0 | offload=0GPU 执行流程(收到 PKT3_IB 后)
- 解析DWORD0:识别为 Type3 IB 命令,count=2 → 总长度 4 DWORD
- 读取DWORD1+2:得到IB 64 位基地址
- 读取DWORD3:得到IB 长度(DWORD)+ 控制位
- 初始化 IB 执行上下文:
- IB 指针 = IB_BASE
- 已读 DWORD = 0
- 进入IB 解析循环
- IB 执行完毕后,返回 Ring,继续执行下一条命令
关键规则
- IB 必须是 GPU 可访问的连续显存(BO 对象)
- IB_SIZE 单位是 DWORD(4 字节),不是字节
- Type0 必须放在 IB 末尾,否则会覆盖后续命令
- 一个 PKT3_IB 只能执行一个 IB;链式 IB 需 chain=1 并在 IB 末尾追加下一个 IB 地址
PM4命令流执行过程
- 驱动构建 IB:在显存中分配 IB 缓冲区,按顺序写入Type0/Type2/Type3PM4 包,Type0 必须放在 IB 末尾。
- 提交 PKT3_IB:驱动向 GPU Ring 发送一条 Type3 命令
PKT3_IB,携带 IB 的64 位地址和长度(DWORD 数)。 - CP 解析 PKT3_IB:命令处理器(CP)读取该命令,拿到 IB 的起始地址与总长度。
- 初始化执行上下文:设置 IB 读取指针 = IB 基地址,已读 DWORD 计数器 = 0。
- 循环解析 PM4 包:
- Type0:无 count,剩余所有 DWORD 都作为寄存器值写入,直到 IB 长度耗尽。
- Type2:仅 1 个 DWORD,直接跳过(NOP)。
- Type3:从包头提取 count,计算包总长 = 1+count,执行命令后跳过整个包。
- 结束:已读 DWORD 达到 IB 长度时,IB 执行完毕,CP 回到 Ring 继续执行后续命令。
完整流程图
用户态/内核构造 PM4 命令 ↓ 打包进显存 BO → 形成 IB ↓ 构造 PKT3_IB 命令(指向 IB) ↓ 将 PKT3_IB 写入 Ring Buffer ↓ 更新 WPTR 寄存器 → 通知 GPU ↓ GPU CP 读取 Ring → 解析 PKT3_IB ↓ 跳转到 IB → 执行 PM4 命令流 ↓ 执行完成 → 写中断/同步信号AQL 命令包(Architected Queuing Language)
定位:HSA/ROCm 标准,用户态直接写入硬件队列,专用于计算核调度(Kernel Dispatch),不经过内核命令提交路径。固定大小:64 字节(16 DWORD),所有 AQL 包统一长度。队列:用户态管理的环形队列(User Mode Queue),通过 Doorbell 寄存器通知 GPU。
1. AQL 核心包类型
- AQL_DISPATCH_PACKET:计算核调度(最常用)
- AQL_BARRIER_PACKET:队列 barrier 同步
- AQL_AGENT_DISPATCH_PACKET:跨 agent 调度
2. AQL_DISPATCH_PACKET 结构(64 字节)
struct hsa_kernel_dispatch_packet { uint16_t setup; // 包类型/尺寸/完成信号使能 uint16_t header; // 包类型(DISPATCH=1) uint16_t x, y, z; // 网格维度(workgroup 数量) uint32_t reserved0; uint64_t kernel_object; // 内核代码对象指针 uint32_t reserved1; uint32_t private_size; // 私有内存大小 uint32_t group_size_x, group_size_y, group_size_z; // workgroup 大小 uint32_t reserved2[4]; uint64_t kern_arg_address; // 参数地址 uint64_t completion_signal; // 完成信号地址 };- 关键:
kernel_object指向 ELF 格式的 GPU 内核代码;completion_signal用于用户态同步。
3. AQL 提交流程(用户态旁路内核)
用户态(ROCm/ROCr)构建 AQL 包 → 原子写入用户态队列 → 写 Doorbell 寄存器 → GPU 直接取包执行 → 写 completion_signal → 用户态轮询 / 等待完成。
PM4 vs AQL 核心对比
| 维度 | PM4 | AQL |
|---|---|---|
| 用途 | 图形渲染、寄存器控制、IB 提交、通用命令 | 计算核(Kernel)调度、HSA/ROCm 专用 |
| 大小 | 可变(1+N DWORD) | 固定 64 字节 |
| 执行单元 | GPU 命令处理器(CP) | GPU 计算调度器(KFD) |
| 提交路径 | 内核态(amdgpu_cs_ioctl → Ring) | 用户态直接写队列(旁路内核) |
| 队列 | 内核管理的 Ring Buffer | 用户态管理的 AQL Queue |
| 同步 | Fence、EOP、中断 | HSA Signal(用户态内存) |
| 架构 | GCN/RDNA 全架构支持 | GFX9+/CDNA 计算架构 |
| 代码位置 | Mesa/amdgpu 驱动 | ROCr/ROCm 运行时 |
关键关联
- PM4 是 AQL 的底层实现:AQL 包最终会被 GPU 翻译为 PM4 命令流执行。
- IB 是 PM4 的容器:用户态构建的 PM4 流通常放在 IB 中,通过
PKT3_IB提交。 - AQL 是 PM4 的高层抽象:面向计算场景,简化核调度,提升用户态控制能力。