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

评论

发表回复

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