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

评论

发表回复

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