标签: 执行链

  • 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

  • 从使能到车轮:执行链上的八道门,以及它们各自不保证什么

    结论:从「软件认为该动作」到「车轮真的动了」,中间至少有八层各自独立的门。任何一层通过,都不代表下一层会放行:使能状态为真不代表执行器活动,控制消息的 valid 只是元数据,主机发出去的帧还可能被板端白名单拒掉。把「发出去」当成「执行了」,是这类系统里最贵的误判。

    八层门各自检查什么

    本文基线为源码快照 c8fb906,静态串联「状态机 → 控制器 → 车型网关 → 主机发送 → 板端固件」这条链:

    层 真正检查了什么 不能据此推断什么
    使能状态 非被动模式且已初始化才计算使能与活动 使能为真不等于横纵执行器都活动,也不等于板端已放行
    横纵活动位 横向还要求无临/永久转向故障与静止转向许可;纵向要求纵向能力且无纵向覆盖事件 此处没有统一的全量检查;纵向活动位基于「使能」而非「活动」
    控制数值与发布 关闭活动位时重置控制器;曲率限幅;非有限数值置零 消息的 valid 是元数据,不是硬阻断
    车型网关 非被动且事件集不含初始化中,才进入控制更新;控制消息存活时才应用车型控制器 该分支只检查单条消息存活,不是全量检查;sendcan.valid=False 仍可能带着候选帧
    主机 CAN 回调 指纹、固件查询、初始化可另发帧;安全模式握手等两个完成信号才切车型安全模式 「所有发送都来自控制器」不成立;握手参数也不是本机代码的权限认证
    发送线程 事件时间距现在小于 1 秒且非测试发送才走硬件发送 该函数不读事件有效性、不读使能状态,一秒是陈旧帧门而非安全截止期限
    心跳线程 约 10 Hz 依据状态消息的存活/有效与使能计算是否接管 心跳与发送线程相互独立;未接管不是逐帧否决
    板端发送 完整 CAN 包经白名单、车型发送钩子与继电器故障门 白名单中有帧不等于动作获准;车型钩子通常还查许可以及扭矩/角度/加速度限值

    失效时各层怎么反应

    这是排查线上问题的实用部分——同一类故障在不同层的行为并不一致:

    情况 本快照中的处理 仍待验证
    状态消息失联或无效 心跳侧据此令后续心跳显示未接管;控制器本轮计算并未据此统一停止 各进程更新顺序、残留帧、车型钩子与原厂接管时延
    控制消息失联 网关不再发布新的控制帧 独立诊断/初始化回调、最后一帧生效时间、板端与车辆超时
    CAN 无效但仍发了控制帧 网关把发送标记为无效,但发送线程只查年龄与测试标志 车型控制器是否真生成帧、板端是否拒帧、故障消息调度顺序
    主机帧陈旧 年龄达到 1 秒的发送事件被丢弃,板端从未见到该包 低于 1 秒但超过车型安全周期的行为及总线/接口堵塞
    板端心跳中断 点火时约 5 秒、非点火约 2 秒阈值后切入静默并清除接管标志 中断与接口故障的最坏墙上延迟、继电器与车端实际恢复
    许可与接管标志不一致 板端 1 Hz 段连续累计 3 次后清许可;上位机侧也对不一致累计并进入不匹配路径 两条路径异步,不能把 3 次与 200 次解释成相同的失效时限

    三个容易被当成「安全证明」的东西

    1. 消息的 valid 字段。它由 CAN 有效位赋值,是元数据。它既不阻断车型控制器生成帧,也不阻断发送线程,更不能替代板端校验。
    2. 界面上的使能图标。使能状态为真只说明状态机走到了这一步,横向与纵向是否真的活动要分别看活动位,板端是否放行还要再往下一层。
    3. 安全模式握手的完成参数。握手表示板端可以切到对应车型的安全模式,它不是本机代码的权限认证,也不是「动作已被批准」的凭据。

    做故障注入该覆盖哪些场景

    如果自研硬件或做插件化改造,下面这些场景必须逐条注入,而且顺序上应先回放与软硬件在环,再进封闭场地:

    注入场景 要观察什么
    CAN 无效 是否会带着无效标记继续产生候选帧,板端是否拦截
    控制消息丢失 最后一帧的实际生效时刻与停帧确认
    状态消息丢失 心跳切换与控制器是否继续计算之间存在的时间差
    送板延迟与接口堵塞 1 秒陈旧门以下、车型安全周期以上的中间地带
    心跳中断 板端静默阈值的实际墙上时间
    校验错与原厂接管 原厂巡航/制动/转向接管与诊断回调并发时的行为

    插件化改造的硬要求

    把传感器、车型或控制算法做成可替换插件时,状态消息与控制消息主题不应成为无认证的任意写入口。核心要维持唯一控制权、消息来源与新鲜度校验,板端保留独立的发送限制与安全模式回读。

    特别要把这五个环节分开记录:候选帧生成、发送消息发布、接口发送、板端接受、车辆执行。把这五件事混成一个「已下发」的日志,等于放弃了事后定责的能力。

    常见问题

    为什么「使能」了动作却没执行?

    因为使能只是第一层。横向和纵向各有独立活动条件(故障、静止许可、纵向覆盖事件),网关、发送线程和板端白名单还会各自过滤。任何一层不通过,动作就不会发生。

    发送消息标记为无效,为什么还可能发出去?

    因为有效性是元数据,发送线程实际只看事件年龄和测试标志,不读有效性也不读使能。所以带无效标记的候选帧仍可能到达板端——真正的拦截点在车型钩子和板端。

    板端心跳中断后多久会停发?

    源代码里的名义阈值是点火时约 5 秒、非点火约 2 秒后切入静默并清除接管标志。但这是 1 Hz 检查的计数阈值,最坏墙上延迟还要看中断与接口故障情况。

    安全模式握手完成就代表动作被批准了吗?

    不是。握手只表示板端可以切换到对应车型的安全模式,它既不是权限认证,也不代表具体动作已获准。动作是否放行仍由板端白名单与车型钩子逐帧判定。

    延伸阅读

    参考资料

    • 项目 02 —— NAS 谭健强/openpilot-research/ACTUATION_GATE_CHAIN_DEEP_DIVE.md(执行链门控与失效边界,含源码行号索引)
    • 项目 02 —— openpilot-research/CAN_PARSER_HEALTH_DEEP_DIVE.md(解析健康语义)
    • 项目 02 —— openpilot-research/CAR_STATE_NORMALIZATION_DEEP_DIVE.md(车辆状态归一化)

    说明:本文为对固定源码快照的静态分析,描述的是多层控制请求与板端过滤,不是实车执行时延证明。本轮未运行测试、未刷写板卡、未发送 CAN 报文、未连接车辆。车型控制器、板级电气与硬件在环结果仍须逐车核验。

    更新记录

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