标签: tinygrad

  • 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

  • tinygrad 张量调度:图到 LINEAR 的四步与 256 字节内存规划

    结论:tinygrad 把张量图变成可执行的 LINEAR 调用序列,分四步:Tensor.realize() 挑出需要存储的输出 → 图转 CALL → prepare_rangeify()/get_kernel_graph() 定物化边界与内核图 → create_schedule() 按依赖排序。排序必须同时满足 RAW(生产者先于消费者)与 WAR(读旧状态者先于覆盖者),否则可变张量会读到错值。要注意两点边界:调度缓存命中只表示跳过了图准备与排序,并不代表内核已编译或设备内存已分配;内存规划按「首次/末次出现」预测生存期并以 256 字节块放进 arena,是对调度图的推演,不是对设备完成时刻的测量。

    从 Tensor.realize() 到内核图:把四步串起来

    入口是 Tensor.realize(),它只挑 needs_storage() 为真的输出,再经 linear_with_vars() 调用 transform_to_call()。后者收集待实现的存储、给图加 tag、把已实现输入替换为 PARAM,生成外层 SINK.call(...) 以及原图到新缓冲的映射。调度不只是创建一张临时执行列表——_apply_map_to_tensors() 会更新作用域内引用旧节点的 Tensor,使它们指向新的 UOp。

    随后 lower_sink_to_linear() 只处理没有 KernelInfo 且输出已绑定的 SINK 调用;未绑定的 value call 留给前置准备阶段内联。它按 function.key 查进程内的 schedule_cache,未命中时依次执行准备、内核图生成与排序三步。下表把这一段的职责摊开,最后一列是容易被前端忽略的契约。

    阶段 / 关键函数 代码行为 硬件或插件契约
    prepare_rangeify()、resolve_function() 先处理多设备图、移动算子与最早的重写;未绑定输出函数可按参数 slot 内联,检查尺寸和 dtype。 动态形状、数据类型与多设备分片不是可忽略的装载细节。
    run_rangeify() 建消费者关系,给各节点传播/新建 range;多消费者不兼容或归约、广播边界可触发物化。 「一个图节点 = 一个内核」不成立,实际 fusion 与中间缓冲须从生成的内核图核对。
    get_kernel_graph()、split_store() 清理符号和临时物化,限制缓冲,转 STORE,把可分割存储封成带 KernelInfo 的 SINK.call。 这里只输出候选内核图;具体 ISA、GPU 源码与二进制在后续 codegen 阶段产生。
    create_schedule() 从 AFTER 状态构建生产者→消费者的 RAW 与读旧状态→覆盖写入的 WAR 边,按入度排序为 LINEAR;剩余边构成环时抛错。 可变 Tensor 的旧值和新值有顺序语义;只按数据流拓扑排序会漏掉覆盖风险。
    resolve_linear_call() 把参数 slot 绑定到外层缓冲/符号变量;嵌套 LINEAR 的位置参数覆盖外层同名绑定,未传的新变量可继承外层。 多层预编译函数与插件调用必须保持参数作用域及 dtype/尺寸一致。
    pm_copy_from_store、assert_all_same_devices() 特定跨设备纯复制模式可转 COPY;普通 SINK 若含多个设备的缓冲会报错。 算法插件不能隐式假定计算内核可直接跨设备读写;跨设备传输是独立调度节点。

    依赖排序:RAW 与 WAR 缺一不可

    create_schedule() 从每个 AFTER 提取待执行内核及其前置状态:读某状态的内核依赖写出该状态的内核,这是 RAW;而后续覆盖同一旧状态的写入还必须排在既有读者之后,这是 WAR。排序阶段生成的是 k.body.call(*buf_uops) 调用,不是立即执行。如果依赖图有环,函数抛 RuntimeError,不会返回一个「尽力而为」的顺序。

    之后 create_linear_with_vars() 解析嵌套调用与 bound Variables,仅保留调度实际用到的变量值,并对同名值冲突报错。这里有一个分叉:处于 JIT 捕获且 CAPTURING 开启时,它把 LINEAR 加入捕获器并返回空列表;只有非捕获路径才会调用内存规划。也就是说,「用 JIT 跑一遍」和「真正规划内存并执行」不是同一条路径。

    COPY 与内存规划:lane 分离与 256 字节 arena

    memory_plan_rewrite() 按 LINEAR 中内部缓冲的首次/末次出现估算生存期,以 256 字节块把可规划缓冲放进同设备、同 lane 的 arena。它排除传入的 held buffers 与 DISK/CL/WEBGPU 缓冲,把 COPY 缓冲和计算缓冲放在不同 lane,并为 COPY 延长释放时点,以减少因复用引入的依赖。

    这一层的措辞很重要:它按调度图预测生存期,不是对设备驱动的实际完成时刻做测量。因此 arena 复用依赖调度顺序与同步是否被正确尊重——不要把内部视图当成独立分配,也不要把它当成可以长期持有的外部指针。

    边界 源码语义 误读风险
    缓存命中 SCACHE 控制是否读写 schedule_cache,命中即跳过图准备与调度 命中不代表内核已编译或设备内存已分配
    图校验 SPEC 只在未命中分支验证 Tensor 图 缓存命中本身不重复该检查
    COPY 延长 为 COPY 缓冲延长释放时点以降低复用依赖 这是减少依赖的手段,不等于已同步设备完成
    可变 Tensor 旧值/新值有顺序语义,WAR 边防止读者被覆盖 只按数据流拓扑排序会漏掉覆盖风险

    多设备不是「把同一张量复制到所有设备」

    alu_multi() 尝试在分片上逐设备计算;copy_multi() 可把分片拼接回单设备,或经 allreduce 汇聚到多设备;handle_allreduce() 按设备数、元素数、具体形状以及 RING/ALL2ALL 配置,选择朴素、ring 或 all-to-all 路径。这带来一个明确的插件契约:新硬件若只实现单设备复制,并不能宣称支持这些分片与聚合语义。跨设备传输是独立调度节点,普通计算内核不能假定可以隐式跨设备读写。

    验证边界:缓存用例通过,视图与多张量用例未跑

    本机执行 python3 -m unittest test.null.test_schedule_cache -v,两个缓存/变量绑定用例通过。但尝试导入 test.null.test_schedule.TestContiguous 时,依赖模块因环境缺少 NumPy 而失败,视图用例并未运行;test/null/test_multitensor.py 也未运行。因此还须以实际模型产物比对调度内核数、COPY 次数、中间峰值内存与跨设备同步。硬件插件另需验证缓冲别名、重连后视图失效、设备完成信号与最坏时延——调度成功不等于车上可用。

    常见问题

    调度缓存命中,是否说明内核已经编译好了?

    不是。缓存命中只表示跳过了图准备与调度两步,源文档明确它并不意味后续内核已编译或设备内存已分配。此外 SPEC 只在未命中分支验证 Tensor 图,缓存命中本身不重复该检查。

    为什么调度要做 WAR 依赖,而不只做数据流的 RAW?

    因为可变 Tensor 的旧值和新值有顺序语义。create_schedule() 除了 RAW,还让后续覆盖同一旧状态的写入排在既有读者之后;只按数据流拓扑排序会漏掉覆盖风险。依赖图有环时函数直接抛 RuntimeError,不返回「尽力而为」的顺序。

    内存规划里的 256 字节块是实际分配单位吗?

    它是内存规划阶段用于把可规划缓冲放进同设备、同 lane arena 的起始粒度。规划按 LINEAR 中内部缓冲的首次/末次出现估算生存期,是对调度图的预测,不是对设备驱动实际完成时刻的测量,因此 arena 复用依赖调度顺序与同步是否被正确尊重。

    硬件只实现单设备复制,能声称支持多设备分片吗?

    不能。alu_multi() 做分片逐设备计算、copy_multi() 做拼接或 allreduce、handle_allreduce() 按设备数与形状选朴素/ring/all-to-all 路径。源文档明确:新硬件若只实现单设备复制,并不能宣称支持这些分片和聚合语义。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_TENSOR_SCHEDULING_MEMORY_DEEP_DIVE.md(取用了张量图到 LINEAR 的四步、RAW/WAR 依赖、COPY 与 256 字节内存规划、多设备边界与验证结果)。

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

    更新记录

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

  • tinygrad PCI 两条内存路径:alloc 与 map 的所有权不对称

    结论:tinygrad 的 PCI 接口把「本地发现与独占」「内存分配与映射」和「远程 RPC 调试」三件事放在同一模块里,普通插件很容易只看到最后一句 alloc(),却忽略它前面的宿主状态改动。两条内存路径的所有权并不对称:VRAM 路径走 MemoryManager.valloc(),释放时有 vfree();而 SYS 路径单独持有 VA、系统内存和 GPU 页表,free() 只做本地 munmap(),没有显式归还 VA 或拆页表。远程服务 serve.py 监听 0.0.0.0 且在协议处理前没有认证,属于开发工具。以下为固定归档的静态源码追踪,未解绑驱动、未映射 BAR、未启动远程服务、未访问真实 PCI 设备。

    本地调用链:构造对象即改变宿主 PCI 状态

    _System.pci_scan_bus() 按 vendor/device 掩码和可选 base class 筛选设备;list_devices() 在设置 REMOTE 时改走 TCP 远程扫描并缓存结果;pci_probe_device() 应用可见设备过滤后构造设备。

    真正需要警惕的是 PCIDevice.__init__():它取得文件锁、解绑现有内核驱动、移除同 slot 的 sibling functions、选择 VFIO 或 sysfs 启用,并打开 PCI config。也就是说,构造对象本身就会改变宿主机 PCI 状态,研究时不应把它当成只读操作。PCIIfaceBase.__init__() 随后探测设备、保留 VA 区间、尝试 resize BAR 并构造具体实现。

    步骤 负责函数 副作用 / 边界
    扫描与筛选 pci_scan_bus()、pci_probe_device() 按 vendor/device 掩码与可见设备过滤。
    远程分流 list_devices() 设置 REMOTE 时改走 TCP 扫描并缓存。
    独占与启用 PCIDevice.__init__() 取文件锁、解绑驱动、移除 sibling function、选 VFIO/sysfs。
    VA 与 BAR PCIIfaceBase.__init__() 保留 VA 区间、尝试 resize BAR;resize 异常被吞掉并继续。

    alloc 与 map:所有权不对称

    alloc() 依据 host/cpu_access/BAR 选择锁页系统内存或 VRAM。VRAM 路径调用 MemoryManager.valloc();SYS 路径则取得 VA、分配 sysmem 并写 GPU 页表。map() 允许同 host 的 CPU/PYTHON/NPY 缓冲或同 peer group 的 PCI GPU 缓冲进入 GPU VA;host 路径检查页对齐与 VA 范围并锁页,P2P 路径拒绝小 BAR 与跨 peer group 映射,p2p_paddrs() 用目标 BAR 物理基址形成系统地址空间映射。

    释放时的不对称是重点。PCIIfaceBase.free() 对 PHYS 路径调用 vfree()、对本地 CPU 映射调用 munmap(),但没有对 SYS 分配调用 unmap_range() 或 va_allocator.free()。同样,map() 对 host 缓冲调用 System.lock_memory(),对应的 unmap() 只调用页表 unmap_range(),未见 munlock()。

    入口 主要职责 需要核对的结果
    alloc_sysmem()、system_paddrs() MAP_LOCKED 匿名映射并从 pagemap 取物理页 依赖权限、页固定和完整物理页列表。
    map_bar()、bar_info() resource 文件长度/地址与 BAR mmap off/size、BAR 边界与 MADV_DONTFORK 是内核契约。
    alloc() 输出 BufferStorage 加 PCIAllocationMeta PHYS 有 valloc() 所有权;SYS 单独拥有 VA、映射和页表。
    free() PHYS 走 vfree();本地 CPU 映射 munmap() SYS 路径未显式归还 VA 或拆页表。
    map()、unmap() 外部缓冲映射与页表拆除 unmap() 仅拆 GPU PTE,host mlock() 解锁责任不明确。

    此外,USBPCIDevice 通过 ASM24 桥配置 PCI bus/BAR,并用 512 KiB SRAM Bump 分配器提供 DMA 暂存。这属于特定 USB 桥路径,不应混作通用主板 PCI 内存。另有几个静态可见的失败边界:PCIDevice.reset() 使用 shell 命令但不检查退出状态;BAR resize 异常在 __init__() 被吞掉并继续初始化——因此不能把构造成功等同于 reset/resize 已成功。

    远程 RPC:服务端没有认证

    RemotePCIDevice 经 TCP 的 REMOTE_REQ/REMOTE_RESP 与服务端通信。RemoteMMIOInterface 的读取使用往返 RPC,写入是 posted 命令,同一 TCP 流上的后续请求用于观察此前的写入。服务端 handle() 暴露设备探测、config/BAR 读写、reset、系统内存映射、直接内存读写,以及 LOAD_PROG/EXEC_PROG。

    信任边界很高:server.bind() 监听 0.0.0.0,在协议处理前没有认证或授权;SYSMEM_READ/WRITE 直接以请求提供的地址构造进程内存视图;LOAD_PROG/EXEC_PROG 可装载并执行客户端给出的程序。只要服务可被访问,客户端就获得远超「推理插件数据搬运」的能力。源码本身是开发工具,不应在车载生产网络上裸露运行,应物理或网络隔离,并增加认证、命令白名单与最小权限。这一判断是接口能力分析,不表示当前车辆上已启动该服务。

    静态可见的失败边界与验收

    除了 alloc/free 的 VA 与锁页不对称,还有两处条件性风险。其一,alloc() 的 SYS 路径先取 VA,再申请系统内存和建立页表,失败中途没有本层回滚,属条件性资源累积风险,需区分底层设备复位或进程退出清理与运行中的热重配。其二,远程 scan()/_connect() 缓存 socket,而 serve.py 的已发现设备、打开设备、BAR 映射和程序列表是模块全局状态,连接关闭不会在同一进程中显式清掉,热重连与多客户端所有权需要另测。

    面向自制硬件的验收应先在无设备 mock 中覆盖 VA/SYS 分配失败回滚、host 锁页与释放、BAR 边界和 P2P peer group;再在专用台架验证驱动解绑与恢复、VFIO/IOMMU、DMA 地址、重复启动和断电回收。远程调试服务只能放在隔离开发环境,绝不能成为「万物即插件」的通用车载执行接口;任何 PCI/计算插件都不得直接获得 sendcan 或 panda safety 配置权限。

    常见问题

    alloc 之后 free 就能把资源全部还回去吗?

    不一定对称。PHYS/VRAM 路径在 free() 里走 vfree(),本地 CPU 映射走 munmap();但 SYS 路径没有显式调用 unmap_range() 或 va_allocator.free(),host 锁页的解锁责任也未在这一对函数里明确。

    构造 PCIDevice 对象是只读操作吗?

    不是。PCIDevice.__init__() 会取得文件锁、解绑现有内核驱动、移除同 slot 的 sibling functions,并选择 VFIO 或 sysfs 启用,因而会改变宿主机 PCI 状态。

    远程 serve.py 暴露了什么能力?

    它暴露设备探测、config/BAR 读写、reset、系统内存映射、直接内存读写,以及 LOAD_PROG/EXEC_PROG;server.bind() 监听 0.0.0.0 且协议处理前没有认证,SYSMEM_READ/WRITE 直接按请求地址构造进程内存视图。

    BAR resize 失败会影响后续初始化吗?

    会存在被忽略的情况。BAR resize 的异常在 PCIIfaceBase.__init__() 被吞掉并继续初始化;PCIDevice.reset() 也使用 shell 命令但不检查退出状态,因此不能把构造成功等同于 reset/resize 已成功。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_PCI_LOCAL_REMOTE_MEMORY_TRUST_LIFECYCLE_DEEP_DIVE.md(取用了本地调用链、alloc/map/free 所有权不对称、USB 桥路径、远程 RPC 服务端无认证与静态失败边界)。

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

    更新记录

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

  • tinygrad BNXT 固件控制面:HWRM 长度信任与队列所有权缺口

    结论:tinygrad 的 BNXT 驱动控制面是一条「初始化—队列—故障」三段式链路:BNXTDev 负责 PCI/固件邮箱,BNXTQP 负责 QP 状态机与 doorbell。它的静态缺口集中在两处——hwrm() 未约束固件返回的 resp_len,fini() 仅注销功能驱动而无显式 QP/CQ 销毁与内存逐项释放。它是计算数据搬运组件,不是车辆控制或安全网关。

    分层初始化:从 PCI reset 到 QP 的 RTS

    BNXTDev.__init__() 复位 PCI 设备、开启 Bus Master、映射 BAR0/BAR2,再用 hwrm() 取得版本/能力、复位功能、注册驱动并获取 doorbell 偏移。setup_backing_store() 按固件报告的 entry size、子类型和最小容量分配系统内存并建立 PBL,随后 _open_rcfw() 建 CREQ/CMDQ/NQ 并初始化 RCFW。

    _open_l2() 建 L2 CQ、RX ring、VNIC 和 MAC filter,供 RoCE ingress 使用;源码注释称不向这个 L2 RX ring 投递接收缓冲。最后注册本地 GID。BNXTQP.__init__() 建两个 CQ 与 SQ/RQ,创建 QP 并置 INIT;connect() 通过两次 modify_qp 转 RTR/RTS。

    HWRM/RCFW 回包:长度信任与状态断言

    函数/组件 输入到输出 关键实现细节
    build_pbl() 物理页列表到 PBL 层级/地址 单页直指;多页分配页表;超过一张底层表时加顶层表,顶层最多 512 项;队列尾两页带终止标志。
    BNXTQueue.read()/write() 逻辑 index 到 ring 槽位 通过 index % entries 环绕;aux=True 写 SQ 后半的 MSN 表。调用方承担数据不跨槽与写入顺序。
    db_value() QP/CQ/NQ ID、index、epoch 到 64 位 doorbell 高位含 RoCE path/type/valid,低位含索引和 epoch;doorbell() 写前调用内存屏障。
    hwrm() 固件请求到 HWRM 回包 seq 递增、清空响应页、写邮箱、等待 resp_len/seq/valid,再解结构并断言 error_code == 0。
    rcfw() RCFW 请求到 CREQ 回包 16 字节槽位、首包标记、等待 toggle、推进 consumer 和 NQ_ARM doorbell,断言 status == 0。
    register_mem() 物理页/VA/长度到 MR key PBL 只取覆盖 size 的页;启用本地/远端写。unregister_mem() 按 key 注销。
    msn_entry() WQE 号、PSN、长度到 MSN 项和下一 PSN 每 4096 字节一包,最少耗一个 PSN;24 位回绕。
    cqe_ready() CQE 与 consumer 到可读布尔值 比较 toggle 与 CQ 环绕 epoch;poll() 另查状态字节,避免把错误完成视作成功。

    hwrm() 在读取响应页末项前未约束固件给出的 resp_len 是否超过响应页长度,也未先验证它足以覆盖期望结构。畸形大长度可产生 IndexError 而非设计的超时/协议错误;这是静态可见的输入校验缺口,真实固件是否会返回此值尚未验证。

    队列与 doorbell:环绕、epoch 与内存屏障

    队列通过 index % entries 环绕,调用方承担数据不跨槽与写入顺序的责任;db_value() 把 RoCE path/type/valid、索引与 epoch 拼成 64 位 doorbell,写前调用内存屏障。cqe_ready() 比较 toggle 与 CQ 环绕 epoch 判断可读,poll() 另查状态字节,以避免把错误完成当成成功。

    值得注意的是双端协议与完成语义的差别:connect() 设置 RoCEv2 IPv4、MTU 4096、目标 QPN/GID/MAC 与重试参数,这些是源码写入的配置,并未证明网络拓扑、交换机拥塞策略或异机互通可靠。CPU 示例的 poll() 检查 CQE status,而 GPU 提交路径只等待 toggle,两条路径的「成功」判据不能混同。

    环绕与终止标志同样需要调用方自己负责:BNXTQueue.read()/write() 通过 index % entries 环绕,aux=True 写 SQ 后半的 MSN 表,调用方需承担数据不跨槽与写入顺序;build_pbl() 在队列尾两页写入终止标志。这些约定在代码里由调用方保证,而不是由驱动在每次写入时校验。

    故障与资源所有权:fini 只调 unrgtr

    同步调用方面,wait_cond() 使用毫秒超时的轮询;HWRM/RCFW 各自有超时和非零状态断言,但没有在这里定义重试、故障分类或失联后的 QP 重建。硬件平台需要把超时、固件错误和传输错误分别上报。

    所有权方面,fini() 仅调用 func_drv_unrgtr。这份文件没有显式 destroy_qp/destroy_cq、backing store 逐项释放或初始化异常回滚;资源可能由功能复位/进程退出收走,但不能据此认定热重启无泄漏,需要目标驱动/固件证据和重启压力测试。

    阶段 当前证据 下一验收
    无设备封包 12 项单测通过,覆盖 PBL/backing store、邮箱请求、RCFW 环绕、MR/QP/WQE/CQ 格式。 增加畸形 HWRM resp_len、RCFW 超时、非零状态、重复初始化与清理注入。
    BNXT 台架 loopback.py 与 connect.py 存在,但本轮未运行。 本地 PHY loopback、异机 SEND/RECV、环绕、乱序启动、断线重连、长时间稳定性。
    GPU 数据面 仅静态追到 tinygrad rdma_copies()。 双节点 GPU 校验和、CQE 错误传播、缓冲视图/越界、缓存可见性、尾延迟。
    车端集成 未连接目标硬件、模型实体或车辆。 只把 BNXT 暴露为可选计算传输插件;网络或固件失效不得获得 CAN 发送与 panda safety 权限。

    验证矩阵与插件边界

    BNXT 插件的最小声明应包括 PCI ID、固件/芯片版本、拓扑 peer group、MR 对齐与生命周期、队列容量、端到端超时、CQE 状态映射和故障后的资源回收。该驱动是计算数据搬运组件,不是车辆控制或安全网关;把它当作推理加速之外的东西会越过它本不承担的安全边界。

    还要强调一点:HWRM 与 RCFW 的超时和状态断言都属于「失败即停」,而不是「失败即分类」。在自制硬件上,固件版本、邮箱响应页长度、doorbell 偏移都必须由设备身份绑定后写入契约;任何绕过 hwrm() 直接写邮箱的路径,都会跳过这里的版本与能力协商,也就失去了一次可核对的一致性检查。

    常见问题

    HWRM 回包的长度为什么是风险点?

    因为 hwrm() 在读取响应页末项前未约束固件给出的 resp_len 不超过响应页长度,也未先验证它足以覆盖期望结构。畸形大长度可产生 IndexError 而非设计的超时/协议错误,属静态可见的输入校验缺口。

    fini() 之后 QP 和内存会被释放吗?

    源文档未给出显式 destroy_qp/destroy_cq、backing store 逐项释放或初始化异常回滚。fini() 仅调用 func_drv_unrgtr,资源可能由功能复位/进程退出收走,但不能据此认定热重启无泄漏。

    CPU 示例与 GPU 路径的「完成」判据相同吗?

    不相同。CPU 示例的 BNXTQP.poll() 检查 CQE status,而 GPU 提交路径只等待 toggle。两条路径的成功判据不能混同。

    BNXT 驱动能承担车辆控制或安全网关吗?

    不能。源文档明确它是计算数据搬运组件,不是车辆控制或安全网关;车端集成时应只把它暴露为可选计算传输插件,网络或固件失效不得获得 CAN 发送与 panda safety 配置权限。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_BNXT_FIRMWARE_QUEUE_TRANSPORT_FAILURE_CONTRACT_DEEP_DIVE.md(取用了 BNXT 分层初始化、HWRM/RCFW 回包、队列与故障边界及验证矩阵)。
    • 项目 02 —— openpilot-research/TINYGRAD_RDMA_BNXT_QUEUE_MEMORY_COMPLETION_CONTRACT_DEEP_DIVE.md(仅用于确认 RDMA 提交与完成语义的边界)。

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

    更新记录

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

  • tinygrad 超越函数降解:SIN 阈值 30 与双归约路径

    结论:transcendental.py 中的 SIN、EXP2、LOG2、SQRT 近似改写,只在该算子未被 renderer 声明为原生支持、或 force_transcendental=True 时才注册;TRANSCENDENTAL 默认值为 1,需 >= 2 才会在 dtype 降解之后并入后期降解,所以「源码里有近似函数」并不等于车端总走软件路径。xsin() 默认在 abs(d) >= 30 就从 Cody–Waite 切到 Payne–Hanek,但函数注释自述其适用范围从 39800 起,中间区间并无全域精度证明。以下为对固定源码归档的静态分析,未运行目标后端、未加载模型实体、未做实车验证。

    编译入口与注册条件

    get_transcendental_patterns() 只在 SIN、EXP2、LOG2、SQRT 未列入 renderer 支持算子,或 force_transcendental=True 时注册相应改写。float16/32/64 直接走本文件的公式,其他浮点 dtype 先转 float32 执行、再转回原 dtype。因此「后端是否声明原生超越指令」是决定走原生还是走降级的第一道开关。

    full_rewrite_to_sink() 在 dtype 降级之后把该模式集并入后期降级,并传入 TRANSCENDENTAL >= 2。TRANSCENDENTAL 默认值为 1,仅凭源码存在近似函数不能推断车端总走软件路径,需要结合该环境变量与实际后端来判定。

    另需区分入口:普通 POW 由 symbolic.py 的规则降为 xpow(),不受「后端是否支持 SIN/LOG2/EXP2」这同一入口控制。也就是说,即使后端声明了这些超越算子,幂运算仍可能落到本文件的 xpow() 上。

    位操作与结构辅助

    整个文件依赖目标后端在 BITCAST、移位和浮点格式上与公式假设完全一致。下表按函数族列出实现与限制,其中多处只对有限输入成立,不应泛化为标准库语义。

    函数族 实现与限制
    _lazy_map_numbers() 用条件选择分别处理 +Inf、-Inf、NaN 与普通数;NaN 判断用 x != x;最终特殊值由各顶层函数按自身语义决定
    mantissa_bits() / exponent_bias() / exponent_mask() 从 dtype 浮点格式取尾数与指数结构;依赖后端 BITCAST 与浮点格式精确一致
    pow2if() / ilogb2k() 通过整数位型构造 2^q、从位型提取指数
    rintk() 先按符号加减 0.5 再转整数,实现「中点远离零」取整;转换溢出与非有限输入不由该函数统一处理
    ldexp3k() / ldexp2k() 前者直接调浮点指数位,后者把整数指数拆两段构造二次幂再相乘;后者注释要求正、非次正规数
    frexp() 拆位得尾数与指数,假定输入非零;float64 掩码还会清符号;此处主要用于 SIN 对绝对值的范围归约

    这些函数的共同前提是「目标后端的整数宽度、移位与浮点转整数语义符合假设」。若设备缺 uint64 或其乘移语义不同,下面的 Payne–Hanek 路径需要重点复核。

    SIN 的两条归约路径

    SIN 先把幅值归约到小角,再用按 dtype 选择的多项式逼近,最后按象限恢复移相与符号。cody_waite_reduction() 用 1/pi 估算象限并做分段常数减法;float64 分拆多段 pi,float16 的减法临时升到 float32。payne_hanek_reduction() 则从 190 位 2/pi 常量提取片段,以 uint64 乘法求大角余量和象限。

    xsin() 默认同时构造 Cody–Waite 与 Payne–Hanek 两条路径,以绝对值 30 为选择阈值,并恢复原符号;fast=True 时只使用 Cody–Waite,调用者需自行约束幅值。Inf/NaN 最后统一映射为 NaN。

    路径 / 参数 机制 阈值与边界
    Cody–Waite 1/pi 估象限 + 分段常数减法 小角与中等角;float64 多段 pi,float16 临时升 float32
    Payne–Hanek 190 位 2/pi 片段 + uint64 乘法 注释声称适用范围从 39800 起;对整数宽度、移位/溢出与浮点转整数语义敏感
    xsin() 选择 默认在 abs(d) >= 30 选择 Payne–Hanek 有限样本在 31、100、1000、39800 及 1e6 未显示明显误差,但不构成整个 30–39800 区间或全部浮点值的精度证明
    fast=True 只用 Cody–Waite 调用者应自行约束幅值,否则大角误差无保证

    需要注意的一处不一致:Payne–Hanek 函数注释写的是 39800 <= d,而 xsin() 默认在 abs(d) >= 30 就选择其结果。文中不把函数注释里的「1 ULP」当作已验证保证。

    EXP2、LOG2、POW 与插件验收

    xexp2() 用 rintk 把输入分解为整数指数与小数部分,小数用多项式逼近、整数用 ldexp2k 恢复,随后按 dtype 上下界置 +Inf/0 并单独处理 NaN。xlog2() 对小幅值输入先乘二次幂以避开次正规区,再抽指数、正规化尾数并用多项式计算,末尾依次处理 +Inf、零、负数和 NaN,负零另用倒数检测。xpow() 从 exp2(exponent * log2(abs(base))) 构造幂;负底数时用转 int32 后回转识别非整数及奇偶,指数为零时强制返回 1。

    函数 关键分界 需另行验证的边界
    xexp2() 按 dtype 上下界置 +Inf/0,NaN 单独处理 上溢/下溢边界的实际取值
    xlog2() 小幅值先乘二次幂避开次正规区;负零用倒数检测 负值、零、次正规数
    xpow() 负底数转 int32 判奇偶;指数为 0 强制返回 1 超出整数转换可靠范围的指数、非有限值组合
    验收样本 正常数、次正规数、正负零、Inf/NaN SIN 切换边界与大幅值、EXP2 上下溢、LOG2 负值、POW 负底数/非整数指数

    面向自制计算后端,应明确选择「原生超越指令」还是「编译器降级」,并把该选择与模型工件、dtype、编译器版本及浮点模式绑定。验收要记录真实后端与参考实现的绝对/相对/ULP 误差和最坏时延。模型输出的变化必须在离线回放与受控台架中评估,计算插件不得获得 CAN 发送权。

    验证方面,在 PYTHONPATH=. DEV=PYTHON 下上游 test_transcendental_helpers.py 五项通过;TRANSCENDENTAL=2 小样例中,float32 的 SIN/EXP2/LOG2 对给定有限样本与 Python math 的最大绝对差分别约 2.8e-8、2.5e-8、0,POW 的负底数整数/非整数、0^0 及 LOG2 特殊值样例符合预期。更广的图重写边界测试因缺 hypothesis 未导入;大角样本只做有限抽查,未运行全域误差证明、目标 QCOM 后端、LFS 模型或实车验证,归档源码未编辑。

    常见问题

    后端不支持 SIN,就一定会走软件近似吗?

    不一定等同于此结论的细节是:只有在 renderer 未声明支持这些算子、或 force_transcendental=True 时才注册改写,且还需 TRANSCENDENTAL >= 2 才会并入后期降级;TRANSCENDENTAL 默认值为 1。

    这里的 SQRT 是独立的牛顿迭代平方根吗?

    不是。在无原生路径时,SQRT 被改写成 xpow(x, 0.5),因此它复用幂函数的构造,而不是另一套牛顿迭代实现。

    xsin 在 30 到 39800 之间可靠吗?

    不能这样断言。Payne–Hanek 的函数注释自述适用范围从 39800 起,而 xsin() 默认在 abs(d) >= 30 就选择它;有限样本在若干点上未显示明显误差,但这不构成该区间的全域精度证明,注释中的「1 ULP」亦未经验证。

    自制计算后端应重点验收哪些点?

    至少覆盖 float16/32/64 的正常数、次正规数、正负零、Inf/NaN,SIN 切换边界与大幅值,EXP2 上下溢,LOG2 负值,POW 负底数与非整数指数;若设备缺 uint64 或乘移语义不同,还需重点验证 Payne–Hanek 路径与 dtype 模拟。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_TRANSCENDENTAL_REDUCTION_NUMERICS_PLUGIN_CONTRACT_DEEP_DIVE.md(逐函数实现、SIN 两条归约路径与 TRANSCENDENTAL 入口条件)
    • 项目 02 —— openpilot-research/TINYGRAD_OP_DECOMPOSITION_DIVMOD_THREEFRY_PLUGIN_CONTRACT_DEEP_DIVE.md(基础算子降级专题,作为本文的补充阅读)

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

    更新记录

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

  • tinygrad DSP 后端:ION 视图偏移与 FastRPC 反向服务边界

    结论:tinygrad 的 Hexagon DSP 后端(ops_dsp.py)在固定归档里只是一条可选、且未被车端选用的推理路径:openpilot 的 load_oob() 当前只选 USB+AMD、QCOM、METAL 或 CPU:LLVM,不选择 DSP。它把主机缓冲以「size + offset + ION share fd」三元组编入 FastRPC 参数,并在 DSP 侧生成 entry 包装读取 pra 数组;同时用一条常驻 daemon 线程向 DSP 提供 open/close/seek/read/stat/mmap 反向文件服务。这两处——非零 offset 视图的解映射地址,以及反向 RPC 的请求字段——都是跨处理器契约,不能当作无权限的数据通道。

    DSP 不是当前车端推理硬件

    源文档开篇即划定边界:openpilot 的 load_oob() 只选择 USB+AMD、QCOM、METAL 或 CPU:LLVM,不选择 DSP。因此 ops_dsp.py 是 tinygrad 侧的 Hexagon 后端实现,不能被当作现有车端推理硬件。归档源码不修改,本篇为静态分析与无设备契约检查。

    真实路径的初始化是多阶段过程:DSPDevice.__init__() 打开 /dev/ion、读取 /dsp/cdsp/fastrpc_shell_3 并复制到 ION 缓冲、初始化 /dev/adsprpc-smd,然后启动反向服务线程;mock 路径则只注册模拟 renderer/program。渲染器禁用了 fp8/bfloat16 与 SQRT 原生映射,global_size、local_size 与 wait 未参与启动参数。

    阶段 静态行为 源文档标注未验证
    编译 目标 hexagonv65 / HVX v65 128b;真实路径生成 4 KiB 节对齐共享库链接脚本 Hexagon 工具链与编译产物
    主机内存 ION 分配并 share fd,再 mmap 为 CPU 可访问缓冲;视图继承同一 fd 并累加 offset /dev/ion 与驱动行为
    FastRPC 装载 shell、初始化 CDSP、动态装载 tinylib,失败重连一次 /dev/adsprpc-smd 与真实调用
    反向服务 daemon 线程响应 DSP 发起的文件操作 请求长度与权限边界

    主机缓冲与 DSP ABI 的三元组

    DSPAllocator 返回 DSPBuffer(va_addr,size,share_info,offset);DSPProgram 从缓冲提取 size、offset、shared fd,rpc_prep_args() 把 size/标量、offset 数组、8 字节计时输出和 fd/attrs 打包进 FastRPC 调用。DSP 侧生成的 entry 按固定下标读这些参数,这个数组顺序本身就是后端 ABI,不能只改 allocator 或 renderer 一侧。

    DSP 侧参数 含义
    pra[0] 读 buffer size 或标量值
    pra[1] 读偏移 offi
    pra[2] 写计时输出
    pra[i+3] 取得该输入的 DMA fd

    DSPProgram.__call__() 至多 15 个缓冲,按 8 字节槽写入 buffer size 与标量,另传 32 位偏移和 ION shared fd;调用 exec_lib() 后返回 DSP 侧微秒计时换算值。这意味着调用期必须持有 memoryview 引用以覆盖 ioctl,否则缓冲可能在传输中被回收。

    非零 offset 视图:映射与解映射地址不一致

    源文档把「非零 offset 的视图」列为需要单独验收项。DSPAllocator._offset() 给视图保留 size 并累计 offset;而 DSPRenderer._render_entry() 生成 HAP_mmap(..., sz_or_val_i, ..., fd, 0)+offi,最后却用已加偏移的 buf_i 调 HAP_munmap(buf_i, sz_or_val_i)。无设备调用 _render_entry() 的最小样例实际输出了这两行。

    后果是条件性的:当 offi 大于 0 时,映射基址、传给 kernel 的指针及解映射地址三者不一致;若长度只等于视图大小,指针后方可用范围也小于视图大小。源文档把它定性为「源码/生成文本可复核的条件性视图契约问题」,并明确尚未在目标 DSP 上确认驱动行为。对插件 ABI 的直接含义是:应传原始基址和映射总长,分别持有映射租约与视图偏移。

    反向 RPC 是一条跨处理器信任边界

    RPCListener.run() 是响应 DSP 发起请求的 daemon 线程,处理 open/close/seek/read/stat/mmap;tinylib 由当前 binded_lib 提供,其他路径经宿主 os.open/os.stat 等访问。源文档明确:请求字段是跨处理器信任边界,循环中对输入/输出对象个数、长度及指针推进没有完整容量验证。即使本代码只期望可信 DSP shell 与编译产物,硬件插件设计也应把路径白名单、请求大小上限、fd 生命周期和异常隔离前置。

    生命周期还有两处需要故障注入:exec_lib() 在 invoke 成功后关闭库,异常时先重连再重试;若 invoke 已打开句柄而后续失败,关闭/连接恢复的边界需验证。正常路径关闭库句柄,失败路径没有 finally 保证配对关闭。此外 RPCListener 是无限 daemon 循环,没有显式停止/join,同一设备上的 binded_lib/offset 为共享可变字段,并发装载需单独验证;未知 sc 抛异常会终止线程。

    自制硬件的验收顺序

    源文档给出的顺序是:先在没有执行器的隔离台架验证参数布局、视图偏移、fd/映射回收、RPC 文件权限、超时与并发,再对参考模型做逐节点数值和完整模型尾延迟比较。测试层面归档包含 DSP 专属量化用例,但需要对应设备与工具链;本轮只执行了 _render_entry() 的无设备生成探针,未运行 /dev/ion、FastRPC、Hexagon 编译、qemu 或真实模型。

    最后一条边界是权限:DSP 插件不应拥有 CAN 发送或修改 panda 安全策略的权限。真实初始化中的 ION 分配、fd share、CPU mmap、shell 装载和 FastRPC 连接是多阶段过程,源文档指出未见统一逐阶段回滚,这也是台架要覆盖的失败路径。

    常见问题

    DSP 后端现在被 openpilot 车端推理使用吗?

    没有。源文档明确 openpilot 的 load_oob() 当前只选择 USB+AMD、QCOM、METAL 或 CPU:LLVM,不选择 DSP。ops_dsp.py 是 tinygrad 侧的 Hexagon 后端实现,不能当作现有车端推理硬件。

    非零 offset 的缓冲视图有什么风险?

    源文档探针显示,_render_entry() 生成映射时基址加了偏移,但解映射却用了已加偏移的缓冲指针。当 offi 大于 0 时,映射基址、传给 kernel 的指针与解映射地址不一致;若长度只等于视图大小,可用范围也小于视图大小。插件 ABI 应分别持有映射租约与视图偏移。

    反向 RPC 文件服务需要做哪些限制?

    该 daemon 线程把 DSP 请求的路径、大小与 fd 交给宿主文件操作,源文档指出循环中没有完整的容量验证,非 tinylib 名称可传到 os.open。即使只期望可信 DSP,也应前置路径白名单、请求大小上限、fd 生命周期和异常隔离。

    qemu 模拟出来的执行时间能当作 DSP 实时性数据吗?

    不能。mock 路径用 qemu-hexagon-static 执行静态程序,返回值按 1 GHz 假设把指令数伪装成秒数,源文档明确它不能作为 DSP 或车端实时性证据。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_DSP_FASTRPC_ION_VIEW_RPC_SECURITY_CONTRACT_DEEP_DIVE.md(取用了 DSP 后端组件表、pra 参数 ABI、非零 offset 视图探针结果与反向 RPC 安全边界)。

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

    更新记录

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

  • tinygrad C 风格渲染器:指针重解释、门条件与调用可信边界

    结论:tinygrad 的 C 风格渲染层(CStyleLanguage)不是「读任意计算图再生成代码」的入口,而是一道下游文本生成器:它只消费已完成范围展开、类型与算子降解、门搬移和线性化的 UOp 图,把模式表映射成 C 风格控制流、指针与类型声明。两个前提必须同时成立——门条件不能在这一层丢失,CALL 与 CUSTOM 必须当成可信编译输入边界。渲染成功既不证明指针可安全解引用,也不授予 CAN 发送权。

    渲染调用链:这一层消费的是「已完成」的图

    入口是 do_render():它对已线性化的 UOp 调用 renderer 的 render()。CStyleLanguage.render() 先经 _render() 生成内核体、名字与参数表,再由 render_kernel() 拼出源码;随后 do_compile() 把源码交给 compiler.compile_cached()。这是一条单向链路:上游做结构变换,这一层只做文本落地。

    关键在于它不能直接接收任意前端 UOp。范围展开、类型/算子降解、门搬移和线性化都必须在进入这一层之前完成;如果未匹配的模式漏了进来,_render() 会断言失败,而不是静默忽略。也就是说,本层对「输入是否已合法」是有断言的,但对「生成的指针是否真的可访问」没有保证。

    Clang、OpenCL、Metal、CUDA、HIP 与 QCOM 等子类都基于这一通用基类提供目标语法或能力。基类的 code_for_workitem 和 barrier 是空配置,正是留给具体后端填空的位置。

    模式表与关键函数

    下表列出这一层的核心组件及其行为前提。未匹配会在 _render() 断言失败,这是它与「静默降级」实现最本质的区别。

    函数或组件 行为与前提
    base_rewrite 用模式表把 RANGE/END/IF/ENDIF 渲染为 C 风格控制流,BARRIER 渲染为子类提供的 barrier 文本;CONST、CAST/BITCAST、INDEX/SHRINK、LOAD/STORE、ALU、WMMA、CALL 与 CUSTOM 也在此映射。
    CStyleLanguage 声明 kernel_typedef、地址空间前缀、code_for_workitem、code_for_op、向量语法与图像常量等可覆盖项。
    render_kernel() 用 SPECIAL 的局部维度上界计算 launch_bounds 并构造函数签名;有图像形状参数时附加固定的未归一化、边界钳制与最近邻采样器声明。只输出源码,不验证真实设备工作组资源。
    render_buffer() 为 BUFFER 生成局部/寄存器数组声明,LOCAL 可加对齐或共享内存前缀;数组长度来自 max_numel(),设备端资源上限仍需实际编译与启动验证。
    _render() 先统计子节点使用次数与被 STORE 指向的 PARAM;按 UOp 线性顺序命名临时变量,单消费者表达式多数直接内联,多消费者或控制节点发出语句并维护缩进。AFTER 仅取其值源名称。

    还有一组负责非原生浮点的函数:create_non_native_float_pats() 对不原生支持的浮点 dtype 在 ALU 前提交 weak 常量,通过 float 中间类型运算/比较;pm_manual_bf16_cast 与 pm_bf16_ushort_const 处理 BF16 位型转换及以 ushort 存储的常量文本。uops_to_dtypes() 与 wmma_args() 收集向量 dtype/宽度和 WMMA 形状,供子类生成 typedef、矩阵内建函数或额外声明。

    指针重解释、门条件与向量 ABI

    指针与访问类型的生成集中在 render_index()、_render_dtype()、render_type()、render_ptr() 和 render_access()。ALU 地址空间的索引按 C 数组或向量 lane 访问,其他地址空间返回缓冲指针加偏移;常量向量 lane 在小向量时可能变为 .xyzwabcd,阈值由 gep_arr_threshold 决定。

    环节 生成的文本行为 它不保证什么
    访问类型生成 按 lane 数选择向量访问类型。 不保证访问宽度与硬件对齐一致。
    向量指针重解释 向量访问宽于标量指针或访问 dtype 与缓冲 dtype 不同时,先重解释为目标指针再解引用。 不证明对齐、缓冲大小、别名或可访问性。
    图像类型选择 依据是否被写入选择 write_only/read_only image2d_t。 不校验真实采样规则与图像尺寸上限。
    门条件 LOAD base_rewrite 对有 gate 的 LOAD 生成条件表达式,避免 gate 假时实际求值访存。 普通 STORE 只接受无独立 gate 的形式,安全性同时依赖上游门搬移。

    这里有一条容易被忽略的两阶段依赖:更早的 pm_linearize_cleanups 把 gated STORE 拆为 IF-STORE-ENDIF,因此门的安全性同时依赖上游线性化和本层文本渲染。code_for_op 中的 CDIV/CMOD 是 C 的截断语义,FLOORDIV/FLOORMOD 的负数补偿已由前序降解处理,不能在后端直接合并为不等价指令。

    CALL 与 CUSTOM 是可信编译输入边界

    CALL 通过函数指针强制转换生成调用文本,CUSTOM/CUSTOMI 用 UOp 参数字符串格式化源码。源码文档明确把它们定义为可信编译输入边界,不应开放给未经审核的热插拔插件或远程数据。新后端应限制可用模板、绑定函数 ABI,并保留生成源码及二进制工件供审计。

    同样地,render_ptr() 的强制类型转换要求调用方保证对齐与长度;NV12/warp 输入缓冲不能仅凭生成的指针表达式就认定可安全读取。若后端仍保留未处理的 Invalid、门条件或不支持的 ALU,编译失败或运行时越界都必须使模型不可用,而不是继续向控制链输出「有效」观测。

    自制 C 风格后端的验收清单

    综合上述,自制 C 风格后端至少须定义线程/工作组坐标、真实屏障、GLOBAL/LOCAL 指针与向量 ABI、图像读写与采样规则、支持的 dtype/算子,以及 compiler。验收时可对照下表逐项确认边界来源。

    验收项 必须显式声明 来源边界
    坐标与工作组 线程/工作组坐标、launch_bounds 依据 基类默认空配置,须目标语言补齐。
    同步 真实屏障文本 基类 barrier 为空。
    指针与向量 GLOBAL/LOCAL 前缀、向量 ABI、对齐要求 render_ptr() 只做文本转换。
    图像 读写方向、采样规则、尺寸上限 render_kernel() 只发声明。
    可审计性 生成源码与二进制工件留存 CALL/CUSTOM 为可信输入边界。

    渲染成功不授予 CAN 发送权——车型控制与 panda 固件 safety 仍是独立边界。验证层面,源码作者用 PYTHONPATH=. DEV=PYTHON 的最小 UOp 编译探针确认:CStyleLanguage(Target()) 对普通 float LOAD/ADD/STORE 输出参数指针和赋值源码;以输入值比较作为门的 STORE 输出 if (...) { ... }。首次尝试用裸 UOp.variable() 构造 gate 被 spec_program 拒绝,改用真实 LOAD 比较后才通过——失败样例不能计为有效通过。

    常见问题

    能直接用 tinygrad 的 C 风格渲染器写自己的 GPU 后端吗?

    可以作为起点,但基类只提供模式表与可覆盖项,code_for_workitem 和 barrier 默认是空配置。自制后端必须补齐线程/工作组坐标、真实屏障、GLOBAL/LOCAL 指针与向量 ABI、图像读写与采样规则、支持的 dtype/算子,以及 compiler,否则生成的源码不具备可执行前提。

    为什么渲染成功不代表能发出 CAN?

    因为渲染层只输出源码文本,render_kernel() 也只发声明、不验证真实设备工作组资源。源码文档明确:渲染成功不授予 CAN 发送权,车型控制与 panda 固件 safety 是独立边界。编译通过只是计算路径的前一步,不影响车辆控制与安全许可。

    指针强制类型转换后读内存安全吗?

    不安全。render_ptr() 的向量指针重解释只是文本转换,源码文档说明它不证明对齐、缓冲大小、别名或可访问性,并要求调用方保证对齐与长度;NV12/warp 输入缓冲不能仅凭生成的指针表达式认定可安全读取。

    CALL 与 CUSTOM 能不能给热插拔插件或远程数据使用?

    不建议。源码把 CALL(函数指针强制转换)与 CUSTOM/CUSTOMI(按 UOp 参数字符串格式化源码)定义为可信编译输入边界,不应开放给未经审核的热插拔插件或远程数据;新后端应限制模板、绑定函数 ABI,并保留生成源码与二进制工件供审计。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_CSTYLE_RENDERER_POINTER_GATE_CALL_TRUST_CONTRACT_DEEP_DIVE.md(取用了 base_rewrite 模式表、指针/图像类型生成、门条件两阶段依赖、CALL/CUSTOM 可信边界与最小 UOp 编译探针结论)。
    • 项目 02 —— openpilot-research/TINYGRAD_RENDERER_CAPABILITY_ESTIMATES_LAUNCH_METADATA_DEEP_DIVE.md(仅用于确认渲染能力专题的接续关系,未取用其未公开内容)。

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

    更新记录

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

  • PyTorch PrivateUse1 前端:ATen 映射、别名回写与设备语义

    结论:这层 PyTorch PrivateUse1 前端是把 ATen 算子映射到 tinygrad Tensor 的开发兼容桥,不是车端实时推理后端的替身:导入即产生进程全局副作用(关闭 autograd 多线程、即时编译 C++ 扩展、把 PrivateUse1 重命名为 tiny 并注册后端),而设备守卫基本不做真实同步——不能从事件查询或流同步返回推导底层 tinygrad 工作已经完成。当前可见的车端模型进程使用 tinygrad ONNX 编译脚本并消费已捕获的模型,没有把 torch.device('tiny') 或本适配桥放入车端推理主链。本文为固定源码快照的静态阅读,未导入该前端,因此未触发扩展编译或改变本机 PyTorch 注册状态。

    导入与原生张量所有权

    实现点 函数级行为
    导入段 关闭 autograd 多线程,即时编译 C++ 扩展,把 PrivateUse1 重命名为 tiny,注册后端并生成设备方法;这些是进程全局副作用
    TinyBackend/设备守卫 Python 设备数读取 GPU 数(默认 1),C++ 设备数固定为 1;守卫的设备、流、事件操作基本不做实际同步
    wrap()/unwrap()/wrap_tensor() Python Tensor 作为安全对象被原生张量实现持有;Torch 元数据使用 tinygrad 的形状、重算的连续 strides 与存储偏移
    calculate_storage_offset()/update_metadata() 仅对收缩类操作累加基于连续布局的偏移;源码注明重排、扩展、填充、翻转等别名情形并不正确

    unwrap() 经动态类型检查取回原 Python 对象,不拥有独立的 Torch 存储。原生层允许后续更新尺寸、strides 与偏移,但元数据变化不自动证明真实存储视图语义正确;calculate_storage_offset() 的注释也明确指出,除收缩外的别名情形偏移计算并不正确。换句话说,Torch 侧并不拥有独立存储,视图与元数据都由 tinygrad 侧重算,因此不能假设两者可以各自独立演进。

    分发、视图与副作用

    入口 执行链与限制
    wrap_view_op()/视图操作表 视图、转置、压缩、选择、重排、切片等记录基础张量、派生视图弱引用与操作链,以便后续原位写入回放
    _view_write()/_apply_inplace() 纯 reshape 快路径直接改写基础张量;一般视图用索引张量计算扁平位置,克隆后 scatter,再替换基础并刷新派生视图
    wrap_out()/_inplace_op() out= 默认允许按输出 dtype 转换,随后断言形状、设备与类型并写回;原位操作按是否存在派生视图选择替换或别名传播
    包装器注册循环 扫描 ATen schema 的可写参数与视图原位标签,分别选择普通、首参原位、视图原位或 out 包装器;未识别写入模式直接抛错

    这里最需要留意的不是数值本身,而是别名语义:视图写入走「计算扁平位置、克隆、scatter、替换基础张量」的路径,重叠索引写入、复杂别名与异常中断后的对象关系应列入兼容性测试,不能假设完全等同 PyTorch 原生存储。out= 默认允许按输出 dtype 转换,意味着需要用数值测试确认与目标 PyTorch 版本的舍入语义;导入成功也不代表任意 ATen 算子都已实现。

    算子路径与兼容缺口

    算子表把数学、比较、归约、scatter、矩阵乘等 out= 形式与一般、原位、创建、随机、池化、排序和部分线性代数分开组织。其中两处静态可见差异是:empty_strided 明确忽略请求 stride 改为连续分配,logsumexp 映射只取第一个轴——因此不能拿「算子名已注册」当作数值等价证明。卷积正反向有独立映射,池化、填充和插值也有独立正反向;批量归一化管理运行统计量与梯度,必须同时验证训练和评估路径。

    路径 行为 兼容缺口
    归约与形状 empty_strided 改连续分配,logsumexp 只取首轴 语义与原生 PyTorch 有差异
    CPU 回退 部分索引写入、特征分解与插值反传显式搬到 CPU 再搬回 非阻塞参数没有独立异步语义
    设备互转 CPU 与 tiny 转换使用 NumPy 或复制 不能据名称承诺时限或零拷贝
    分解注册 为若干复合算子装配 PyTorch 分解,列表含禁用项与已知不适用路径 完整范围受版本、扩展编译与未覆盖算子影响

    该分解注册段还含注释中的禁用项与已知不适用路径;完整兼容范围受 PyTorch 版本、扩展编译、当前后端及未覆盖算子影响,需要以目标版本运行矩阵证明。非阻塞参数在对应转换函数里没有实现独立异步传输语义,不能据其名称承诺时限或零拷贝。总体而言,导入成功只说明注册流程跑通,可见的算子表与回退路径才是实际兼容边界。

    openpilot 与硬件插件边界

    当前可见的车端模型脚本使用 tinygrad ONNX 编译,运行时消费已捕获的模型,并没有把 torch.device('tiny') 或本适配桥放入车端推理主链。因此 Torch 前端及其动态 C++ 编译应限定在研发与训练进程,与 CAN、panda 安全配置和控制消息发布隔离。若把它作为硬件原型验证工具,必须记录实际执行设备、CPU 回退次数、数据搬运字节数、同步完成点、算子误差与最坏时延,不能仅凭 tiny 标签判断任务在目标加速器上完成。本轮逐段静态核对原生包装、别名与写入、注册机制、算子表和特殊回退,也未对每个 ATen 重载做版本化自动枚举。

    常见问题

    为什么 torch.device('tiny') 不能放进车端推理?

    因为这一层是开发兼容桥而非车端后端:导入会产生进程全局副作用,设备守卫基本不做真实同步,且部分算子有语义差异或在 CPU 回退。当前车端模型脚本使用 tinygrad ONNX 编译并消费已捕获的模型,没有把该桥放入推理主链,把它与 CAN、安全配置和控制发布隔离更稳妥。

    同步流返回就等于底层计算完成了吗?

    不能这样推导。设备守卫的设备、流、事件操作基本不做实际同步,因此不能从 PyTorch 事件查询或流同步返回推导底层 tinygrad 工作已经完成;需要记录实际同步完成点,并单独确认搬运与计算是否结束。

    视图写入和原生 PyTorch 存储一样吗?

    不能假设一致。一般视图的写入走「索引张量计算扁平位置、克隆、scatter、替换基础张量并刷新派生视图」的路径,源码也注明仅收缩类操作的偏移累加正确,重排、扩展、填充、翻转等别名情形的存储偏移并不正确,因此重叠索引写入与复杂别名应列入兼容性测试。

    empty_strided 和 logsumexp 有什么语义差异?

    empty_strided 明确忽略请求的 stride,改为连续分配;logsumexp 映射只取第一个轴。这些都是静态可见的语义差异,说明「算子名已注册」不等于数值等价,需要用目标版本做运行矩阵验证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_TORCH_PRIVATEUSE1_ATEN_ALIAS_DEVICE_DEEP_DIVE.md(取用了导入注册、原生张量所有权、视图与别名写入、算子表与兼容缺口)。
    • 项目 02 —— openpilot-research/TINYGRAD_DATASET_TORCH_FRONTEND_INGRESS_BOUNDARY_DEEP_DIVE.md(仅用于确认前端入口与研发/车端隔离的边界)。

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

    更新记录

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

  • tinygrad QCOM/KGSL:代际门与外部相机指针映射

    结论:QCOM/KGSL 后端在入口处设了一道明确的代际门——gpu_id[:2] >= (7, 3) 直接抛出不支持异常,而 QCOMCLRenderer 的编译器构造器只接受 a630,二者说明「运行时接受的 GPU ID」不等于「该编译器可用」。外部相机指针映射有一条容易被误读的回退:_gpu_map() 仅当 errno=14 时才把原指针当 GPU 地址,这不能被当作任何普通 CPU 指针的通用零拷贝保证。此外 signal() 的 7.3+ 分支尚未实现,与设备初始化的代际门一致。以下为固定源码快照 c8fb906 及固定 tinygrad 归档的静态追踪,不是 Qualcomm GPU、AGNOS 或车辆上的运行验证。

    入口与编译选择

    AGNOS 上 load_oob() 选择 DEV=QCOM;构建模型的 SConscript 还设置 IMAGE=1、FLOAT16=1 等标志,因此运行产物的设备名、图像布局和编译器必须匹配,改选新 SoC 不能只替换设备字符串。QCOMDevice.__init__() 打开 /dev/kgsl-3d0,创建带优先级/细粒度抢占等标志的 KGSL 上下文,设置功率约束并查询 chip_id;gpu_id[:2] >= (7, 3) 直接抛出不支持异常。

    随后选 QCOMCLRenderer 或 IR3Renderer,安装栈、cache-flush dummy 与 border-color 的缓冲替换规则。QCOMCLRenderer 使用 QCOMCompiler,其构造器只接受 a630;非 aarch64 主机尝试下载编译环境并经 QEMU/Docker 服务编译,不能推断任意被运行时接受的 GPU ID 都能走该编译器。IR3 是另一条编译路径,其可用性同样需要目标平台验证。

    内存与外部相机指针

    QCOMAllocator._alloc() 在 external_ptr 非零时走 _gpu_map(),否则走 _gpu_alloc()。所有权随来源变化:自分配对象走 GPUOBJ_FREE 和 munmap,映射的外部 CPU 指针走 SHAREDMEM_FREE,回退的外部 GPU 指针不调用 KGSL 释放。因此主机析构不能替相机驱动释放底层图像,外部相机缓冲复用必须晚于 GPU 完成。

    函数 源码行为 对车载硬件的契约
    QCOMAllocator._alloc() external_ptr 非零走 _gpu_map(),否则走 _gpu_alloc() 外部指针必须有明确长度、来源和所有者;空指针不进入外部映射分支。
    _gpu_alloc() 按 4 KiB 对齐向 KGSL 分配 GPU 对象,并 mmap 成 CPU 可见地址,可选 uncached/清零 要验证目标驱动的分配标志、CPU/GPU 一致性和峰值内存。
    _gpu_map() 页对齐外部地址与跨度,先调用 CPU dcache_flush(),再尝试 KGSL_MAP_USER_MEM;仅 errno=14 时退回把原指针当 GPU 地址 errno=14 回退不是任何普通 CPU 指针的通用零拷贝保证,须确认指针为 GPU 可用地址且租约有效。
    _free()、_gpu_free() 释放前同步;自分配走 GPUOBJ_FREE 和 munmap,映射的外部 CPU 指针走 SHAREDMEM_FREE,回退的外部 GPU 指针不调 KGSL 释放 所有权随来源变化,外部相机缓冲复用必须晚于 GPU 完成。
    _ensure_stack_size() 设备共享私有栈,仅在需要增长时同步并重分配 内核首次触发更大栈可能引入帧延迟尖峰,须预热与量测。

    QCOM has_copy_queue=False,通用 get_enqueue_devs() 还将 QCOM COPY 排除 HCQ 入队;exec_copy() 会根据缓冲能力选 host 可见复制或其他分支。不能把 Buffer.copy_from() 一律算成 QCOM DMA,也不能只测 kernel 耗时作为整帧推理时延。

    二进制、参数与命令包

    QCOMCompiler.compile() 在 aarch64 上通过专用 LLVM 接口编译/链接,再取设备二进制;checked() 在错误时重建编译实例并抛错,非 aarch64 走编译服务,存在额外环境与网络依赖。QCOMProgramData 依所选 renderer 解析 NIR 或专有库的程序镜像、寄存器占用、私有/共享内存、图像和参数布局;qcom_build_program() 以二进制内容和设备集合缓存程序占位。

    QCOMComputeQueue.kernargs() 把普通缓冲地址、标量、常量、sampler、texture 和 image buffer 编成参数表,NIR 分支还按编译器给出的映射重排图像槽位。exec() 检查资源/维度、取得程序镜像和私有栈,随后设置 Adreno A6xx 寄存器、装载常量与 shader,分别使用 CP_EXEC_CS 或 CP_RUN_OPENCL 启动。cmd()、reg() 将其包装为 Type7/Type4 命令,动态地址由 HCQ 链接期或运行期 patch 处理。

    源码中 exec() 的维度异常条件为「某全局乘局部维度超 65536 且 某局部维度超 1024」;单独超出前者不会由这个条件拒绝。它还单独检查 max_threads < prod(local_size)。这是静态可见的输入检查范围,不等于目标驱动一定接受其余组合;移植测试应分别覆盖每一维边界、组合边界及驱动错误回报。

    提交、缓存与失效

    QCOMComputeQueue.submit() 为命令缓冲构造 kgsl_command_object 和 kgsl_gpu_command,经 ioctl(KGSL_GPU_COMMAND) 提交。memory_barrier() 发缓存写回、失效和等待;signal()、wait() 用 GPU 事件/内存轮询配合 HCQ 时间线,而 signal() 的 7.3+ 分支尚未实现,这与设备初始化的代际门一致。

    QCOMDevice._wait_signal() 在共享信号未达目标值时先尝试 KGSL timestamp 等待,抑制该等待的 OSError/RuntimeError,再调用通用轮询检查;通用 Compiled.synchronize() 还等待 peer pending,并在运行时超时后调用 on_device_hang()。这不是车辆控制的故障降级策略;模型无输出、过期输出和 GPU 挂起应由上层状态/安全链处理,不得以成功提交命令代替有效推理结果。

    验证证据与待办

    验证项 归档测试覆盖 待补
    CPU dcache flush test/device/test_qcom.py 覆盖(仅 ARM64/Clang 条件) —
    KGSL 映射 未覆盖 缓冲租约与缓存一致性
    命令包/时间线/图像参数 未覆盖 帧级尾延迟、错误码与挂起超时
    模型数值 未覆盖 纯读输入与输出数值

    归档中的 test/device/test_qcom.py 仅有一个 ARM64/Clang 条件下的 CPU dcache flush 测试,不覆盖 KGSL 映射、图像参数、命令包、时间线或模型数值;本文没有运行 QCOM 硬件测试。硬件验收需要固定模型产物与目标驱动版本,依次验证缓冲租约/缓存一致性、纯读输入与输出数值、帧级尾延迟、错误码与挂起超时,再验证摄像头断流/重连和降级状态。任何上车步骤都应先在隔离台架执行,保持 panda 独立 safety 边界。

    常见问题

    QCOM 后端支持所有 Adreno GPU 吗?

    不是。QCOMDevice.__init__() 中 gpu_id[:2] >= (7, 3) 直接抛出不支持异常,这是一条明确的代际门;而 QCOMCompiler 构造器只接受 a630,运行时接受的 GPU ID 不等于该编译器可用。

    外部相机指针映射失败会怎样?

    _gpu_map() 先做 CPU dcache_flush() 再尝试 KGSL_MAP_USER_MEM,仅 errno=14 时退回把原指针当 GPU 地址。该回退不是任何普通 CPU 指针的通用零拷贝保证,必须确认指针为 GPU 可用地址且租约仍有效。

    exec() 一定拒绝超维度的启动吗?

    不完全是。维度异常条件为「某全局乘局部维度超 65536 且某局部维度超 1024」,单独超出前者不会被这个条件拒绝;它还单独检查 max_threads 小于 prod(local_size)。这只是静态可见的检查范围。

    7.3+ 代际的 GPU 时间线信号可用吗?

    源文档说明 signal() 的 7.3+ 分支尚未实现,这与设备初始化的代际门一致;GPU 挂起与超时应由上层状态/安全链处理,不能以命令成功提交代替有效推理结果。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_QCOM_KGSL_RUNTIME_DEEP_DIVE.md(代际门、外部指针映射、命令包与提交同步)。
    • 项目 02 —— openpilot-research/ 同目录模型运行时与 HCQ 专题(作为相邻边界参考)。

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

    更新记录

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

  • tinygrad NV 原生后端:RM/UVM、PCI 与映射所有权

    结论:tinygrad 的 NV 原生后端把设备发现、内存管理与队列提交分成三层接口:NVKIface 走内核驱动与 RM/UVM,PCIIface 走用户态 PCI 与 GSP,MOCKIface 用于模拟。块内存由 NVAllocator 经所选接口分配,NVKIface.alloc() 依次创建 VA、RM 物理句柄、CPU 映射、UVM range 与 DMA/GPU 映射。它的 VA 分配使用不回退的 BumpAllocator,且中途失败未见配对的逐阶段回滚,因此长时间运行、模型热替换或多插件加载必须专门设计可回收地址分配。本文为固定源码快照的静态阅读,openpilot 的 load_oob() 默认并不使用 NV,本文不代表车端已部署行为。

    接口选择与设备启动

    NVKIface.__init__() 先打开控制与 UVM 节点,按驱动主版本切换常量表,初始化 UVM 并按可见设备规则筛选 GPU,再打开指定设备描述符并读取 GPU instance 与 minor。root、全局描述符和 VA 分配器都是类级状态,因此同一进程内多次初始化会共享这些资源。

    setup_usermode()、setup_vm() 与 setup_gpfifo_vm() 查询可用硬件 class、映射门铃 MMIO,并注册 GPU 与虚拟地址空间及队列,已打开的 NV 设备之间还会尝试启用 P2P。类支持情况与同主机上的初始化顺序会影响启动结果。NVDevice.__init__() 再选择接口、创建设备树、vaspace 与 channel group、查询 SM 能力,并懒建计算与复制相关的 GPFIFO 对象。

    两条用户态路径互斥:PCIIface.__init__() 启动时会拒绝已初始化 NVKIface 的进程,原因是固定地址 mmap 可能覆盖 UVM 分配。因此不能在同一进程里随意从一个接口切到另一个。

    接口 底层路径 关键边界
    NVKIface 内核驱动 + RM/UVM 类级 root/描述符/VA 分配器;与 PCI 路径互斥
    PCIIface 用户态 PCI/GSP 拒绝已初始化 NVKIface 的进程;不能套用 UVM 生命周期
    MOCKIface 模拟器 用于无设备环境,不代表真实驱动行为

    RM/UVM 内存与跨设备映射

    底层 nv_iowr()、rm_alloc()、rm_control() 与 uvm() 只检查系统调用返回,RM 与 UVM 的包内状态分别再检查,显存不足会被映射为 MemoryError;因此不能以 ioctl 成功替代对象创建成功。NVKIface.alloc() 按 host、uncached 与大小选择页粒度,分配 VA 与 RM 物理对象,再创建 UVM external range、DMA 地址映射与 GPU 可见映射,cpu_access 会把内存映射到 CPU 地址。

    NVKIface.map() 与 _alloc_gpu_vaddr() 要求 CPU/PYTHON/NPY 缓冲 4 KiB 对齐;首次创建 host range,已存在 NV 映射时复用物理对象,NV 到 NV 则在既有 VA 上添加 GPU 映射。低、高地址池都是 wrap=False 的 BumpAllocator。

    位置 作用与边界
    nv_iowr() / rm_alloc() / uvm() ioctl 只检查系统调用返回,RM/UVM 包内状态另行检查
    NVKIface.alloc() / _gpu_uvm_map() 分配 VA、RM 物理对象、UVM range、DMA 与 GPU 映射
    NVKIface.free() / unmap() 按句柄条件 RM_FREE,再 UVM_FREE,必要时 munmap
    NVKIface.map() / _alloc_gpu_vaddr() 要求 4 KiB 对齐,复用物理对象或在既有 VA 上加映射

    所有权矩阵

    NVAllocator 把通用 BufferSpec 的 host、cpu_access、zero 转给选定接口,释放前先 synchronize(),跨设备映射与注销委托接口;external_ptr 由通用 Allocator.do_free() 跳过底层释放。Compiled.finalize() 在进程结束时先尝试同步,再在有 device_fini() 时调用它,它不是每次 Buffer.free() 的通用设备重启或销毁 API。

    对象 创建/拥有者 释放或完成门 待验证点
    普通 NV BufferStorage NVAllocator 经 iface 分配 先同步,再由 NVKIface.free() 释放 RM/UVM/CPU 映射 异常顺序下 RM 句柄与 UVM range 能否全部回滚
    BufferSpec.external_ptr 提供地址的上层 do_free() 不调用底层释放 提供方是否保持地址有效到最后一次 GPU 访问
    CPU 缓冲的 NV 映射 原缓冲持有宿主地址 宿主缓冲释放时触发 _unmap() 多卡重复映射与 owns_range=False 的引用回收
    VA 地址池 NVKIface 类级 BumpAllocator UVM_FREE 释放映射,但 bump 指针不后退 大量创建释放后 VA 是否耗尽
    设备树与 GPFIFO NVDevice 与 iface 进程结束时同步并调用可选 device_fini() NVKIface 分支没有逐对象显式销毁路径

    静态风险与边界

    NVKIface.alloc() 依次分配 VA、RM 物理句柄、CPU mmap、UVM range、DMA 映射与 GPU 映射;若中途失败,本函数未看到配对的逐阶段 try/finally 回滚,仅靠 Buffer 成功返回后的析构无法处理此前的半成品状态。NVKIface.free() 的 RM_FREE 条件按句柄与主机对象枚举比较,主机对象与显存对象释放走不同路径,因此不能把 UVM_FREE 等同为每一层资源都已归还。

    更结构性的问题是 VA 池:高低地址池使用 wrap=False 且没有对应 free,即使单个缓冲的 UVM 映射已释放,进程内地址预留仍单调增长。对长时间运行、模型热替换或多插件加载,必须设计可回收的地址分配或代际隔离,并测最坏地址消耗;这是代码结构上的上界问题,不等于当前可达负载已触发。

    需要区分的是,PCI/GSP 路径使用不同的基类,不能直接套用 NVKIface 的 UVM 生命周期推论。本机没有 NVIDIA 目标设备,也没有执行设备 ioctl 或映射操作,上述影响须在隔离设备上做故障注入与句柄、映射计数验证。

    硬件插件要求

    车载硬件插件必须显式声明控制面(驱动与 PCI)、地址空间、分配与映射与释放的权责、P2P 资格和故障回滚;推理插件不得拥有 CAN 发送或更改 panda 安全门的权限。若借鉴该后端,应把设备发现、内存所有权、队列提交与资源释放拆成彼此独立的契约,再逐项验收。

    常见问题

    ioctl 成功是不是就代表 RM/UVM 对象创建成功?

    不代表。nv_iowr() 等只检查系统调用返回,RM 与 UVM 的包内状态会分别再检查,显存不足被映射为 MemoryError。不能以 ioctl 成功替代对象创建成功。

    NVKIface 和 PCIIface 能同时用吗?

    不能随意混用。PCIIface.__init__() 会拒绝已初始化 NVKIface 的进程,原因是固定地址 mmap 可能覆盖 UVM 分配。两条路径使用不同的基类与生命周期。

    VA 地址池会随创建释放一起回收吗?

    不会。低、高地址池使用 wrap=False 的 BumpAllocator,没有对应的 free;即使单个缓冲的 UVM 映射已释放,进程内地址预留仍单调增长。长时间运行或多插件加载需要额外的回收或隔离设计。

    NV 后端是 openpilot 车端的默认推理路径吗?

    不是。openpilot 的 load_oob() 默认不使用 NV,因此本专题描述的是可用路径而非已部署行为,车端实际推理路径另有选择。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_NV_RM_UVM_PCI_MEMORY_OWNERSHIP_DEEP_DIVE.md(取用了接口互斥、RM/UVM 分配与映射、所有权矩阵、VA 池与失败回滚边界)。
    • 项目 02 —— openpilot-research/TINYGRAD_NV_QMD_GPFIFO_PROGRAM_CACHE_CONTRACT_DEEP_DIVE.md(关联的 NV 队列与程序镜像专题,用于区分内存所有权与队列提交,本文未展开其实现)。

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

    更新记录

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