tinygrad HCQ 执行链:队列依赖、链接与提交生命周期

作者:

在

结论:tinygrad 的 HCQ 执行链把 LINEAR 调用编成 GPU 队列批次,再用主机提交程序驱动:hcq_compile() 负责分批、依赖、命令编码与编译,hcq_link() 在链接期把占位缓冲变成真实 Buffer,exec_hcq() 在运行时填地址表并执行提交。一次逻辑 copy_from() 不保证只做一次物理 DMA,多队列完成也不代表某个 kernel 已结束。本文为固定源码快照 c8fb906 的静态阅读,未在 GPU 上运行。

入口、分批和复制

在普通非 JIT 执行链中,compile_linear() 完成内核编译后调用 hcq_compile()。get_enqueue_devs() 只考虑带缓冲参数的 PROGRAM 或 COPY 调用;候选设备类型受 HCQ_DEVS 限制,AMD 只有开启 HCQ2 才进入该集合,QCOM 的 COPY 被排除,留给 host 与统一内存路径。因此不能把所有 tinygrad 调用都理解成 GPU 队列提交。

hcq_compile() 先在需要时把已有 BUFFER 参数化,再用 unwrap_call() 拆多设备 lane,用 pm_insert_copy_staging 处理复制。跨不同 peer group 且可用 RDMA NIC 时,split_rdma() 产生两段调用;不能映射源与目标时,stage_copy() 借用 host staging 缓冲并按两个 slot 分块;设备没有 copy queue 时,复制会被转成可编译的内核。它随后按连续可入队片段与 peer group 调用 sched_batches(),并把编码后的主机提交 SINK 交给 lower_and_compile()。

这些分支决定真实的传输次数与等待点,所以不能从一个逻辑 copy_from() 直接推算物理 DMA 数量。

分支条件 处理方式 含义
跨 peer group 且有 RDMA NIC split_rdma() 产生两段调用 真实传输次数与网络路径相关
源/目标无法映射 stage_copy() 借用 host staging 并按两个 slot 分块 多出主机中转与等待点
设备无 copy queue 把复制转为可编译内核 复制变成 kernel 提交
QCOM 的 COPY 被排除,留给 host/统一内存路径 不进入 HCQ 入队集合

队列依赖和时间线

BatchCtx 收集每个设备的计算/复制队列、上一调用、peer 访问、需要 signal 的提交及 slot 占位。HCQDepsTracker._key() 以 (base, lane, byte offset/end) 标识访问范围,能在同一底层缓冲中区分不重叠视图;缓冲映射的基址、lane 与跨度一旦出错,会直接污染依赖判断。_wait_ins() 由读写冲突选出每个来源队列最近的依赖:同队列依靠 FIFO,跨队列生成 wait,并把被等待的提交标记为 signal。

_start_ins() 与 _build_queues() 让队列首次使用时先等待本设备与 peer 的既有时间线,再插入依赖 wait、可选 timestamp、调用与 signal;收尾队列等其他队列后推进设备时间线。异步 COPY 与 compute 是否能正确重叠,取决于这套规则和设备驱动实现。

函数或结构 具体职责 硬件适配边界
BatchCtx 收集队列、上一调用、peer 访问、signal 与 slot 占位 新后端要提供队列、时间线与提交顺序能力
HCQDepsTracker._key() 以基址、lane、字节跨度标识访问范围 映射基址或跨度错误会污染依赖判断
_wait_ins() 读写冲突选最近依赖,跨队列生成 wait 异步重叠是否正确取决于驱动实现
hcq_fence() 等上次调度完成以避免 slot 冲突 循环留有 timeout TODO,最坏等待须故障注入
HWQueue.q() / encode_submit() 常量写命令字节,动态字段留 patch 新后端要实现命令编码、地址 patch 与错误回报

编译缓存、链接所有权和运行时地址

hcq_compile() 的模板缓存只在 cache=True 且提供输入 UOp 时启用,缓存键包含准备后的 LINEAR、profile 开关与 ALL2ALL>=1。较短调度(HCQ_CACHE_THRESH 默认 64)用运行期地址 patch,较长调度可带 lt_input 链接期输入,随后不能复用同一已烘焙链接。该缓存与内核编译缓存不是同一层。

lower_call() 经设备 pm_encode/pm_lower 编码队列,把动态输入地址改写为地址表槽位,合并同类临时缓冲并以 128 字节对齐;HCQInfo 记录地址表位置、slot、被写缓冲、host 依赖与 profile 元数据。链接时 hcq_link() 把占位缓冲变成实际 Buffer,解析 GETADDR 并保持引用。allow_cache=True 且没有 lt_input 的可缓存链接会分配自有缓冲并缓存结果;含 lt_input 的一次性链接才借用 runtime ring 槽;allow_cache=False 的 JIT 保留链接分配的自有缓冲。设备专属 pm_bufferize 仍可能接管具体缓冲分配,因此不能仅凭 eager 或 JIT 名称推断所有权。

每次 exec_hcq() 在有动态输入地址时先填地址表,再执行主机提交程序;它向内存所有者登记访问设备的 pending timeline,并仅在等待或性能采样条件下同步或读回时间戳。Compiled.synchronize() 等待本设备及 peer pending,超时路径调用 on_device_hang(),但这不自动证明提交程序内所有无超时等待都可恢复。

硬件适配边界

上车硬件适配须分别量化编译、链接、提交、COPY、GPU 完成与输出读回时间,并验证掉线或超时能传递为模型不可用状态,不能把主机提交返回当成推理成功。新后端不仅要能运行 kernel,还要实现命令编码、地址 patch 与错误回报。

本机一项不接触 GPU 的 HCQDepsTracker 探针显示:在同一 UOp 缓冲写入 [0,4) 后,读取 [8,12) 没有依赖,读取 [2,6) 则返回原写依赖。这验证了区间冲突的一个样例,但不覆盖队列 signal、实际设备缓存一致性或 peer 内存。

常见问题

是不是所有 tinygrad 调用都会走 GPU 队列提交?

不是。get_enqueue_devs() 只考虑带缓冲参数的 PROGRAM 或 COPY,且候选设备受 HCQ_DEVS 限制;AMD 需开启 HCQ2 才进入,QCOM 的 COPY 被留给 host 与统一内存路径。

一次逻辑 copy_from() 就等于一次物理 DMA 吗?

不等于。跨 peer group 且有 RDMA NIC 时会产生两段调用,源/目标无法映射时会借用 host staging 分块,设备没有 copy queue 时复制还会被转成内核。传输次数与等待点由这些分支决定。

多队列完成就代表某个 kernel 已经结束了吗?

不代表。源码指出多队列完成不等于某一 kernel 已结束,必须以最终 timeline 或回执为准。Compiled.synchronize() 等待本设备及 peer pending,超时路径调用 on_device_hang()。

主机提交返回是不是就代表推理成功?

不是。exec_hcq() 向内存所有者登记 pending timeline,并仅在等待或性能采样条件下同步。synchronize() 的超时恢复也不自动证明所有无超时等待可恢复,应将超时映射为模型不可用状态。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/TINYGRAD_HCQ_QUEUE_LINK_LIFECYCLE_DEEP_DIVE.md(取用了分批与复制分支、队列依赖与时间线、链接所有权与运行时地址、证据缺口)。
  • 项目 02 —— openpilot-research/TINYGRAD_MODEL_RUNTIME_DEEP_DIVE.md(关联的模型运行时专题,用于区分 HCQ 提交与模型集成链,本文未展开其实现)。

说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

更新记录

首次发布 2026-10-11 最近核对:2026-10-11

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注