标签: JIT

  • tinygrad 执行引擎:JIT 到四类后端的 Buffer 与同步边界

    结论:openpilot 的 modeld 走的是「捕获图一次链接、逐帧重放」的执行链,而不是每帧重新生成模型:lower_and_compile() 按编译键去重并把 SINK/PROGRAM 编译为程序,CapturedJit.linear 首次访问时 link_linear(..., allow_cache=False) 并把链接结果缓存。要替换推理硬件,不只是实现一个算子 API,还必须同时保持 Buffer 映射、命令提交、同步与编译产物契约。源文档列出四类已用后端(CPU:LLVM、METAL、QCOM、USB+AMD:LLVM),并明确后端类存在不等于当前机器能打开设备。

    从 JIT 到执行器:捕获图如何被补齐

    modeld.ModelState.__init__() 装入模型/warp 产物,对捕获图调用 lower_and_compile()。后者识别尚未完成的 SINK/PROGRAM,按编译键去重,编译为程序并替换进调用图。它不等同于每帧重新生成模型。

    _TinyJit.__call__() 在重放时比较输入名称与输入视图、变量、dtype、设备等签名;CapturedJit.linear 首次访问时调用 link_linear(..., allow_cache=False),把链接结果缓存在该捕获对象中。随后 CapturedJit.__call__() 替换需写入的输入,调用 run_linear(..., jit=True),返回捕获时的返回对象。

    run_linear() 的 jit=True 路径直接执行已链接的 LINEAR;普通路径先 compile_linear() 和 link_linear()。pm_exec 将 COPY、PROGRAM、graph、校验等调用分发到不同执行器;带 HCQInfo 的调用经 exec_hcq() 处理地址表、队列执行及必要同步。

    在这条 HCQ 路径里,hcq_compile() 对调用图进行缓冲参数替换、复制 staging、分批、命令编码和程序编译,hcq_link() 处理链接期缓冲与地址。源文档据此总结:推理硬件替换不仅是实现一个算子 API,还要保持 Buffer 映射、命令提交、同步和编译产物契约。

    内存与传输的四条边界

    边界 已核对实现 对新硬件的含义
    缓冲别名 Buffer.view() 保留 base/offset;allocate() 为视图取得底层存储并追踪已分配视图 输入切片与跨帧状态不能被当成独立存储;重置和释放顺序要保持别名关系
    输入复制 Buffer.copy_from() 构造 COPY 的 LINEAR 并调用 run_linear();exec_copy() 依设备能力选择传输 一次 copy_from() 是逻辑上传,不保证任何后端都只做一次物理 DMA 或没有同步
    外部图像指针 Tensor.from_blob() 用 external_ptr 分配但不接管原内存;QCOM 分配器进入用户内存映射 座舱图像指针必须在 GPU 使用完毕前有效;相机 ringbuffer 重用和重连需要台架验证
    回读 Buffer.as_memoryview() 对 host 可见存储先同步,否则经 PYTHON 缓冲复制 输出 .numpy() 是同步/传输边界,不能仅用 GPU kernel 耗时估计帧级延迟

    源文档把「外部图像指针」与「回读」特别分开:Tensor.from_blob() 用 external_ptr 分配但不接管原内存,QCOM 分配器进入用户内存映射,因此座舱图像指针必须在 GPU 使用完毕前保有效;相机 ringbuffer 重用与重连需要台架验证。

    四类设备后端与各自待验证项

    路径 关键入口及源码行为 尚需验证
    PC CPU:LLVM CPUDevice 选 HostAllocator、LLVM renderer 和 CPUProgram;程序可通过本机可执行内存调用 目标 CPU 指令集、编译器/LLVM 版本、数值和最坏时延
    macOS METAL MetalDevice 创建设备与命令队列;程序绑定缓冲并提交命令,wait 时检查错误 macOS/Metal 版本、虚拟化环境 graph 限制、异步错误和回读时延
    AGNOS QCOM QCOMDevice 打开 KGSL、创建上下文并校验 GPU 代际;_gpu_map() 处理外部 CPU 指针 SoC/KGSL 兼容性、相机内存映射、缓存一致性及异常后的输出门控
    Chestnut USB+AMD:LLVM AMDDevice 选接口与 renderer;USB 接口发现特定设备,经控制窗口和 VRAM 管理提交 USB 断连/复连、传输峰值、显存故障、超时回退及大小模型切换后的状态一致性

    Device.__getitem__() 懒加载设备,Compiled._select_renderer() 按 DEV 目标挑选 renderer;源文档明确后端类存在不等于当前机器能打开设备。Compiled.synchronize() 等待本设备 timeline 与跨设备 pending,并在超时/故障时进入 on_device_hang();这只是运行时故障检测,不是车辆安全退出策略。

    四类后端都通过 DEV 目标选择 renderer,但设备打开方式、命令提交与同步机制各不相同:CPU 走本机可执行内存,METAL 走命令缓冲,QCOM 走 KGSL 与外部映射,USB+AMD 走控制窗口与 VRAM 管理。源文档把外部指针失效、USB 断连、GPU 挂起与配置不匹配列为需要故障注入的对象,并明确这些结论来自对 modeld 实际调用路径的审阅,不代表 tinygrad 全部算子、调度优化或设备驱动均已审计。

    modeld 设置 AMD 等待超时并保留小模型回退。车辆控制仍必须由 selfdrived/panda 安全链约束,推理后端的替换不能改变这条约束。

    验证缺口与顺序

    现有测试入口有 JIT 单元测试、JIT 后端测试和 HCQ 测试,但本次未运行,不能替代 openpilot 实际模型及设备验证。源文档给出的顺序是:先补齐 LFS 与编译产物,在目标平台运行固定日志的道路/座舱模型数值对比;随后测量上传、warp、模型、回读和异常切换的分项与最坏时延;最后做外部指针失效、USB 断连、GPU 挂起和配置不匹配的故障注入。测试通过前不应把该后端视作可上车的插件。

    源文档还列出 modeld 对 AMD 的等待超时与小模型回退属于待验证项,其大小模型切换后的状态一致性要与 USB 断连/复连一起测试。

    常见问题

    每帧推理都会重新编译模型吗?

    不会。lower_and_compile() 识别尚未完成的 SINK/PROGRAM 并按编译键去重,编译一次后替换进调用图;CapturedJit.linear 首次访问时链接并把结果缓存到该捕获对象,重放时按签名校验后直接执行。

    一次 copy_from() 是否等于一次物理 DMA?

    不等同。源文档指出它是一次逻辑上传:Buffer.copy_from() 构造 COPY 的 LINEAR,exec_copy() 依设备能力选择传输、磁盘复制、共享 host 映射或 CPU 拷贝,不保证只做一次物理 DMA 或没有同步。

    输出 .numpy() 能用来估算帧级延迟吗?

    不能直接用来估算。Buffer.as_memoryview() 对 host 可见存储先同步,否则经 PYTHON 缓冲复制,源文档明确它是同步/传输边界,只能配合分项时延测量使用。

    后端类存在是否说明当前设备可用?

    不说明。Device.__getitem__() 懒加载设备、Compiled._select_renderer() 按目标挑 renderer,源文档明确后端类存在不等于当前机器能打开设备。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_ENGINE_BACKENDS_DEEP_DIVE.md(取用了 JIT 到执行器的调用链、内存与传输边界表、四类后端待验证项清单)。

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

    更新记录

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

  • TinyJit 捕获与重放:三阶段调用、输入签名与输出缓冲复用

    结论:TinyJit 的调用是三阶段:cnt=0 直接执行原 Python 函数、cnt=1 捕获 LINEAR、cnt>=2 校验输入签名并重放。重放只执行已记录的设备调用,Python 条件分支、闭包常量与普通副作用在捕获时就被冻结;而输入签名并不等于完整 shape 校验——本机探针用裸一维 BUFFER 长度 4 捕获后以长度 8 重放,两者签名相同且都未抛 JitError,返回仍是长度 4 的 Tensor。以下为固定 tinygrad 归档与本机最小探针的静态结构分析,未运行依赖 NumPy 的完整测试,也不代表模型数值或车载时延已验证。

    三阶段调用与关键函数

    _prepare_jit_inputs() 收集顶层位置/关键字 Tensor 以及容器中一层 Tensor,拒绝虚拟或重复输入缓冲,并记录视图、符号变量、dtype 与设备;它按底层缓冲检查重复,非 Tensor 的 Python 标量不会自动成为运行时参数。_TinyJit.__call__() 按计数分派:cnt=0 执行原函数,cnt=1 捕获,cnt>=2 检查签名并重放;JIT=0 走原函数,但仍会在函数末尾递增 cnt。

    函数 本版本实际行为 需要保留的契约
    _prepare_jit_inputs() 收集顶层与容器一层的 Tensor,拒绝虚拟或重复缓冲,记录视图、符号变量、dtype、设备。 视图可参与签名,但裸 BUFFER 长度未必被记录;非 Tensor 标量不自动成为运行时参数。
    _TinyJit.__call__() cnt=0 执行原函数;cnt=1 捕获;cnt>=2 检查签名并重放。 JIT=0 仍递增 cnt;切换配置前应理解计数状态,有原函数时可用 reset() 重新捕获。
    prune_linear() prune=True 时保留触及输入缓冲及其后续相关缓冲的调用,其余作为一次性列表在捕获时执行。 输入无关的初始化可被固定为捕获期结果,不能假定每帧重新运行。
    jit_lower() 把输入缓冲参数化,对非持有缓冲做内存规划,调用 compile_linear(),JIT<2 时尝试组成设备图。 须固定输入、临时缓冲、编译配置及图后端的完成/错误语义。
    graph_split_rewrite()、GraphRunner.supports_uop() 只有后端声明支持时才分组;默认单设备只支持同设备的 PROGRAM。 「JIT 已捕获」不等于所有调用都在单个 GPU graph 内,也不等于没有主机/设备同步。

    输入签名、返回值与分支冻结

    捕获后重放会比较名称列表及由输入视图/变量/dtype/设备组成的 expected_input_info;同一函数从位置参数改关键字参数或换视图偏移可能被拒绝,但不能把此签名当作完整 shape 校验。输入内容本身可以变,却不能让 Python 布尔开关或闭包常量每帧重新执行。

    本机最小探针在默认 METAL 后端用裸一维 BUFFER 长度 4 完成捕获,再以长度 8 的同 dtype/设备 BUFFER 重放:两者的 expected_input_info 都是相同的 NOOP 视图、空变量、float 和 METAL,没有抛 JitError,返回的仍是捕获时长度 4 的 Tensor。这只证明该输入形态下签名未拦截长度变化,不证明所有 reshape/视图都绕过检查;仓库已有使用不同输入构造的 shape 变化用例,仍需运行和区分。对模型插件应在 JIT 之外显式检查形状、步长、字节长度和布局。

    第二次调用才在 capturing 非空期间收集 LINEAR;嵌套 TinyJit 被拒绝,未捕获任何调用或返回含非 Tensor 的对象也会报错。_check_no_non_tensor_return() 允许 None、Tensor 以及递归包含它们的 tuple/list/dict。需要强调的是 CapturedJit 保存的是捕获时的返回对象,不是每次调用重新构造的 Tensor;仓库的 test_output_buffer_reuse() 明确断言后续重放会覆盖先前捕获结果,需要保留某帧输出时须另行复制或在下游完成消费后再复用。

    Python 条件分支、闭包常量和普通副作用只在首轮直接执行及捕获轮执行,重放只执行已记录的设备调用。仓库已有把这种差异固定为回归用例的 test_python_constants_frozen() 与 test_conditional_branches_frozen()。对驾驶模型的驾驶位策略、道路模型的大/小模型选择或相机格式,不能只改变 Python 变量而继续复用同一捕获图,必须确认模型实例与捕获输入契约是否重新建立。

    与 openpilot 模型的实际连接

    load_pickle() 反序列化已捕获的模型产物,并用分块复制恢复权重缓冲。modeld.ModelState.__init__() 对道路模型及 warp 的 captured._linear 做 lower_and_compile();ModelState.run() 先上传打包输入,再跑 warp 与模型,最后读回输出。由于 CapturedJit.linear 是 cached_property,这条替换路径依赖首次 linear 访问发生在替换之后;当前构造函数顺序符合该前提,但未来插件若提前读取 captured.linear,随后仅修改 _linear 不会自动刷新已缓存链接结果。

    dmonitoringmodeld.ModelState.run() 则缓存指向相机缓冲的 from_blob Tensor,运行座舱 warp 和模型。两条模型路径都需要保证图捕获输入地址、相机缓冲生命周期和输出消费顺序;但座舱路径不能直接套用道路模型构造函数里的显式预编译步骤。

    验证边界

    调用轮次 cnt 行为 返回
    第一次 0 直接执行原 Python 函数 实现的 Tensor
    第二次 1 捕获 LINEAR 捕获结果
    第三次及以后 >=2 校验签名并重放 捕获时保存的返回对象
    长度 4→8 重放 >=2 签名相同、未抛 JitError 仍是长度 4 的 Tensor

    本机还运行了不依赖模型文件的 JIT 探针:第一次调用后 cnt=1 且未捕获,第二次后 cnt=2 且已捕获,第三次后 cnt=3;捕获轮与重放轮返回同一 Tensor 对象。优先继续验证相机 ringbuffer 复用、输出复制时点、设备图异常及重连后的重新捕获。最小探针不能替代实际道路/座舱模型的逐帧数值对比、时延上界及安全退出验证。

    常见问题

    TinyJit 第几次调用才是捕获?

    第二次。源码按计数分派:第一次(cnt=0)直接执行原 Python 函数,第二次(cnt=1)捕获 LINEAR,第三次及以后(cnt>=2)校验签名并重放。

    重放返回的 Tensor 是每帧新建的吗?

    不是。CapturedJit 保存的是捕获时的返回对象,仓库 test_output_buffer_reuse() 明确断言后续重放会覆盖先前捕获结果,所以需要保留某帧输出必须另行复制。

    改了 Python 里的开关,重放会跟着变吗?

    不会。Python 条件分支、闭包常量与副作用只在执行轮与捕获轮运行,重放只执行已记录的设备调用。仓库的 test_conditional_branches_frozen() 与 test_python_constants_frozen() 把这一差异作为回归用例。

    输入签名能保证形状正确吗?

    不能当作完整 shape 校验。本机探针用长度 4 的裸 BUFFER 捕获后以长度 8 重放,两者签名相同且未抛 JitError,返回仍是长度 4 的 Tensor。插件应在 JIT 之外显式检查形状、步长、字节长度与布局。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_JIT_CAPTURE_REPLAY_DEEP_DIVE.md(三阶段调用、输入签名探针、输出缓冲复用与分支冻结)。
    • 项目 02 —— openpilot-research/ 同目录张量调度与执行引擎专题(jit_lower 与图后端作为相邻边界参考)。

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

    更新记录

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