结论:tinygrad 的 AMD 后端把「程序镜像」与「队列提交」分成两层:amd_build_program() 编译并缓存镜像,AMDComputeQueue/AMDComputeAQLQueue 才负责编码计算命令,AMDSDMAQueue 负责复制。三条队列路径(PM4、AQL、SDMA)语义不同,且是否具备复制队列是运行时探测结果,不是设备名承诺。源文档强调,exec() 只是编码 GPU 命令,不等于主机已同步拿到输出;USB 桥、时间线等待、GPU 完成与模型输出有效性应分别测量。本文为源码静态分析,缺少实板与大模型实体。
设备资格与队列创建
AMDDevice.__init__() 选择 KFD、PCI、USB 等接口,读取目标 gfx 属性,仅允许源码列出的 GFX9 子集以及 GFX11/12;根据接口确定恢复能力、运行时缓冲区大小、ISA 寄存器表、SDMA 版本与最大复制分段长度。USB 接口安装专用批处理函数,但计算命令仍由 AMDComputeQueue 或 AMDComputeAQLQueue 编码。源文档明确:它不是一个只需实现 Python Tensor 的抽象设备。
AMDDevice.compute_queue 懒创建计算 ring,USB 分支大小为 1 MiB,其他接口为 16 MiB;AQL 由 AMD_AQL 或多 XCC 默认条件决定。sdma_queue() 受 AMD_DISABLE_SDMA 控制,创建时的 OSError 被抑制并返回 None,因此复制队列是否可用是运行时探测结果。无复制队列时,通用 HCQ 会采取不同的复制降级路径,不能仍按 SDMA 延迟估算。
程序镜像与参数 ABI
amd_build_program() 用程序二进制和设备集合做缓存键。首次使用通过内部函数解析 ELF、处理 R_AMDGPU_REL64 重定位、读取 kernel descriptor,并对固定 LDS 资源做上限检查;其它重定位类型触发断言。其结果包含代码入口、寄存器配置、wave32 标志、私有/共享段与 kernargs 大小。
| 镜像字段 | 含义 |
|---|---|
| 代码入口 | kernel descriptor 解析得到的执行入口。 |
| 寄存器配置 | 程序执行所需的资源与用户寄存器设置。 |
| wave32 标志 | 波前宽度标志,随架构与程序而异。 |
| 私有/共享段与 kernargs 大小 | scratch 与参数段的尺寸依据。 |
AMDComputeQueue.kernargs() 按 prg.arg.globals 和符号变量打包地址/数值,依 kernel descriptor 的 kernargs_segment_size 布局;需要 dispatch pointer 时追加 dispatch packet。exec() 取得镜像、scratch 和参数表地址,发缓存 acquire、设置程序入口/资源寄存器/私有栈与用户寄存器,最后发 PACKET3_DISPATCH_DIRECT 并作部分 flush。源文档提醒,缓存是内存中的镜像复用,不证明新 GPU 的 ISA、固件或驱动接受该镜像。
PM4、AQL、SDMA 三条路径
| 队列 | 核心函数 | 具体语义 |
|---|---|---|
| PM4 计算 | AMDComputeQueue.submit()、push() |
将命令缓冲地址封成 indirect-buffer 包;push() 处理 ring 末尾回绕,写 write pointer 后敲 doorbell。 |
| AQL 计算 | AMDComputeAQLQueue.exec()、submit() |
生成 64 字节 kernel dispatch packet;夹杂的 PM4 命令封为 vendor-specific indirect buffer,提交时以 64 字节为 ring 单位。 |
| SDMA 复制 | AMDSDMAQueue.copy()、submit() |
复制按 max_copy_size 切段;提交缓冲需在 ring 内连续,若尾部空间不足则清零尾部并从头写入。 |
AMDComputeQueue.memory_barrier() 先等待 HDP flush 寄存器,再发 ACQUIRE_MEM;wait()、signal() 把时间线转换为 GPU 轮询和末端写回/中断。SDMA 有自己的 wait()、signal() 包格式。源文档指出,通用 HCQ 的跨队列依赖只有这些硬件指令和共享内存可见性都成立才可靠;USB 桥的 fence/哨兵握手是其外侧的另一层协议。
Scratch 与资源时序
scratch_buffer() 根据私有段大小、wave 和 CU 数量计算设备 scratch;需求变大时分配新的非 LRU 缓冲,并在 AQL 情况下更新队列描述符。源码还维护类级 max_scratch_psize,源文档提醒:一个设备触发更高 scratch 需求可能影响同进程后续设备的最小值。因此初次大内核与模型切换期间的分配和同步要单独量测,不可只用稳态推理时间估算内存峰值。
modeld 对 Chestnut 设定的 3000 ms wait timeout 也不是每个内部队列轮询点的统一墙钟期限;内核提交、时间线等待、USB 桥 fence、GPU 完成和模型输出有效性应分别测量与故障注入。
证据与待验证
仓库有队列测试和 mock GPU 基础设施;源文档明确本文没有运行 AMD/USB 队列测试,也没有目标 GPU。验证应先固定 gfx 架构、内核二进制和队列模式,覆盖 PM4/AQL/SDMA 可用与不可用分支,再以已知输入检查复制、scratch 扩容、时间线回执及输出数值,最后测 USB 断连与 GPU 挂起。
软件计算后端没有车辆 CAN 发送授权,仍必须让 card/panda 的独立安全约束保持生效。不同 KFD、直连 PCIe、USB 接口共用部分代码,但并不等价,不能把一条接口的验证结论外推到另一条。
常见问题
有 AMD 设备就一定有 SDMA 复制队列吗?
不一定。sdma_queue() 受 AMD_DISABLE_SDMA 控制,创建时的 OSError 被抑制并返回 None,源文档明确复制队列是否可用是运行时探测结果,不是设备名承诺;无复制队列时通用 HCQ 会走不同的复制降级路径。
exec() 执行完成是否代表主机已拿到输出?
不代表。源文档指出 exec() 是编码 GPU 命令,设置程序入口与资源寄存器后发 dispatch packet 并作部分 flush;时间线等待、USB 桥 fence 与 GPU 完成是另外的阶段,应分别测量。
编译缓存命中是否说明新 GPU 能接受该镜像?
不能说明。amd_build_program() 的缓存是内存中的编译后镜像/占位复用,源文档明确它不证明新 GPU 的 ISA、固件或驱动接受该镜像,相同 ELF 在另一设备集合上仍需自己的缓冲映射。
模型切换或大内核时内存峰值能按稳态推理估算吗?
不能。scratch_buffer() 在需求变大时分配新的非 LRU 缓冲,类级 max_scratch_psize 还可能被同进程后续设备抬高;源文档要求单独量测初次大内核与模型切换期间的分配和同步。
延伸阅读
- Chestnut USB/AMD 桥 —— USB 如何把主机访问送过桥到 AMD 队列。
- tinygrad GPU 显存分配与页表所有权 —— kernargs 与程序缓冲背后的显存所有权。
- tinygrad 张量调度与内存 —— 命令提交上游的调度与内存布局。
- AMD PSP 固件与 ring 信任契约 —— 提交链更底层的固件信任边界。
参考资料
- 项目 02 ——
openpilot-research/TINYGRAD_AMD_QUEUE_PROGRAM_SUBMISSION_DEEP_DIVE.md(取用了设备资格与队列创建、程序镜像与参数 ABI、PM4/AQL/SDMA 三路径表、scratch 与资源时序)。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复