标签: USB

  • Chestnut USB/AMD 推理后端:三层地址空间与双缓冲拷贝协议

    结论:Chestnut(USB+AMD:LLVM)推理后端不是一条本地 GPU 数据路径,而是「USB 桥控制内存 + VRAM 映射 + PCIe BAR」三层地址空间叠加双缓冲拷贝协议的组合。主机侧分配即使请求 host=True 也仍落在 VRAM,CPU 访问必须经 USB 桥映射;一次逻辑 COPY 会被拆成多次 USB 传输与 GPU 端复制。因此「USB 接口能打开」既不等于 GPU 就绪,也不等于模型输出有效。以下为固定源码快照 c8fb906 及固定 tinygrad 归档的静态结构分析,缺少 LFS 模型实体与 Chestnut 实板,所有时序与故障判断均为源码推导。

    从模型入口到设备的初始化链

    load_oob() 在 Chestnut 路径选择 USB+AMD:LLVM;modeld.main() 先按模型产物、设备或线缆判断是否尝试大模型,并把 AMDDevice.wait_timeout_ms 设为 3000 ms,加载或运行失败的回退属于上层逻辑,不是 USB 后端自身的保证。AMDDevice.__init__() 负责选接口、校验 AMD 目标架构、初始化设备属性;USB 分支采用 USBAllocator,设置 4 MiB 运行时分配区,并安装 pm_usb_batch、pm_usb_lower 与 pm_usb_bufferize。一般的 PCI/KFD 路径不走这套 USB 改写。

    USBIface.__init__() 从两个 USB VID/PID 集合中选设备,构造 USBPCIDevice 和 AMDev。后者先取得跨进程锁,创建 USB3,随后由 CustomASM24Controller 检查 LTSSM:未到 0x78 时尝试上电并复查,再配置 PCIe BAR。USB3 会读取产品字符串、必要时解绑内核驱动并重置设备、设置 configuration 并 claim interface。

    这里必须区分三层资格:VID/PID 匹配、PCIe 链路建立(LTSSM 到 0x78)、AMD 架构校验。任何单一的 chestnutPresent 位都不代表 GPU 已准备好执行推理。

    三种地址空间与缓冲所有权

    USBIface.ctrl 把系统页、计算队列页和 512 KiB SRAM 映射到 GPU 虚拟区,主机持有桥的 dma_view;初始化时清空 fence 与 SRAM,防止沿用旧哨兵。USBIface.alloc() 与 USBAllocator.map() 的语义容易被误读:即使请求 host=True 也落在 VRAM,CPU 侧访问需通过 USB 桥映射,并非本机普通 DRAM。

    层次 关键函数 源码契约
    USB 桥控制内存 USBIface.ctrl 系统页、计算队列页与 512 KiB SRAM 映射到 GPU 虚拟区;初始化清空 fence 与 SRAM。
    VRAM/CPU 可见视图 USBIface.alloc()、USBAllocator.map() 即使 host=True 也落在 VRAM;CPU 访问须经 USB 桥映射。
    PCIe BAR、XDATA、SRAM USBMMIOInterface BAR 读写需 4 字节对齐;桥 XDATA 用 0xE4/0xE5,SRAM 用 0xF2 加 bulk 端点。
    主机 staging 与传输结构 _host_block()、_xfer() 常驻 CPU 缓冲保存 libusb handle/context 与双半区 staging;提交后仍需保留源缓冲直至完成。

    USB3.list_devices() 带 functools.cache,结果中的设备指针用 libusb_ref_device 保留。该实现未在此函数内定义重枚举失效,因此断开后同一进程再次选设备是否拿到最新列表,须通过模拟或实板验证,不应假设缓存会自动更新。

    双缓冲复制协议与双半区

    常量 HALF/CHUNK/SLOT 分别是 0x40000、0x40000-512、0x4000 字节:每个 SRAM 半区末尾预留哨兵块。usb_wire() 为载荷加 512 字节并向 slot 对齐;usb_chunks() 对输入用一个 CHUNK、对输出用两个 CHUNK 作为窗口,因此逻辑 COPY 可能拆成多次 USB 传输和 GPU 端复制。

    usb_copy_rewriter() 只针对 host 与远端设备跨界的 COPY,按复制方向形成连续 run 并给每块编号;usb_copy_slicer() 在 GPU 提交图中插入等待、SRAM 复制、清哨兵与 usb_fence 更新。主机侧先用 usb_drained() 等待上轮完成,再通过 0xE5 设置 fence。

    方向 主机侧 GPU 侧
    host → GPU usb_copyin() 轮换两个半区;usb_chunk() 等前次 transfer 与 fence、填 staging 哨兵、发 0xF2,再异步 bulk OUT。 等对应 SRAM 哨兵,复制载荷到目标 VRAM,清哨兵并更新 fence。
    GPU → host usb_copyout() 先发 0xF2 arm read、写 usb_go,然后 bulk IN,再将两半区载荷复制回目标 host 地址。 等 usb_go,将 VRAM 复制到 SRAM 两半区,清 completion queue 并更新 fence。

    usb_reap() 和 usb_drained() 都是条件轮询,本身没有显式墙钟超时。libusb 传输有各自的 timeout,USB3.bulk_wait() 会抛异步错误,但不能据此推断任一桥接 fence 永远有界完成。对新硬件必须分别故障注入「传输成功、GPU 不更新 fence」与「GPU 更新、USB 回执丢失」两类状态。

    USB 访存降级与提交开销

    pm_usb_lower 将远端参数缓冲的标量 load/store 改成 USB 读/写;usb_copy() 对可识别的连续 copy/fill 循环合并为 stream,不能识别时退为逐元素写。64 位 kernargs 写入由 usb_store() 拆为两个 32 位 0xF0 写;usb_load() 则通过桥的 PCIe read stream 读取。这些路径决定命令提交开销与异常时可能的部分写入状态,不是普通进程内内存赋值。

    底层 USB3 使用控制传输与同步/异步 bulk;异步完成回调把状态锁存到 _async_err,等待方通过 bulk_wait() 消费事件并检查失败。桥的 pcie_request() 对读请求/配置写回执重试,对部分写操作走不等回执的快速路径。

    故障层次 含义 不能推出
    USB 传输成功 bulk/控制事务完成 PCIe TLP 已被 GPU 接受。
    PCIe TLP 被接受 桥层读写回执正常 GPU 队列已完成该命令。
    GPU 队列完成 fence 被更新 模型输出数值有效。
    模型输出有效 上层得到可用结果 安全放行条件成立。

    测试证据与硬件验收

    仓库存在 MockUSB3 和 mock GPU 环境,可作为后续隔离测试入口,但本文未运行 USB/AMD 模拟测试,也没有实板。建议先固定模型与目标固件版本、建立逐帧输入/输出数值基线;再量测分块、bulk、SRAM、VRAM、GPU 与回读的延迟分布;最后故障注入断连、重枚举、短传输、哨兵未清、fence 停止与 GPU 挂起。

    模型回退、ChestnutActive 与 selfdrived 的控制退出需要联合验证,不能把 USB 接口可打开当作安全放行条件。

    常见问题

    Chestnut 路径下 host=True 的分配真的在主机内存吗?

    不是。源文档说明 USBIface.alloc() 与 USBAllocator.map() 即使请求 host=True 也落在 VRAM,CPU 侧访问必须通过 USB 桥映射,并不是本机普通 DRAM。

    USB 传输成功就说明 GPU 已经算完了吗?

    不等价。源文档把故障归因分为四层:USB 传输成功、PCIe TLP 被接受、GPU 队列完成、模型输出有效。它们逐层递进,任一层成功都不能推出下一层成立。

    一次 COPY 会变成几次 USB 传输?

    不固定。逻辑 COPY 可能被拆成多次 USB 传输和 GPU 端复制:usb_chunks() 对输入用一个 CHUNK、对输出用两个 CHUNK 作为窗口,usb_copy_rewriter() 还会按方向形成连续 run。

    拔插设备后同一进程会重新枚举吗?

    源文档未给出重枚举失效机制。USB3.list_devices() 带 functools.cache,设备指针用 libusb_ref_device 保留,但该函数没有定义缓存失效,须通过模拟或实板验证,不能假设会自动更新。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_CHESTNUT_USB_AMD_BRIDGE_DEEP_DIVE.md(USB/AMD 初始化链、三种地址空间、双缓冲复制协议与故障分层)。
    • 项目 02 —— openpilot-research/ 同目录 Chestnut 与 HCQ 专题(固件刷写、通用队列链接作为相邻边界参考)。

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

    更新记录

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

  • 600 秒预算与三次重试:外置模块刷写为何不能只看退出码

    结论:外置计算模块的固件刷写里,脚本退出码 0 不等于升级完成。刷写脚本对镜像只做长度上限、魔数、字节校验和与 CRC32 的格式/传输完整性校验,它不是来源认证或数字签名验证;更关键的是,ROM 恢复已提交但设备没有重新出现时,脚本仍会正常返回,上层据退出码 0 记为「刷写成功」,而此时运行态镜像并未被回读校验。正确的做法是把「发现、写入、激活、运行」拆成六种状态,不让一个布尔值吞掉它们。

    它是什么,也不是什么

    该模块通过 USB/PCIe 接入外置 GPU,服务于大模型推理,属于推理设备。它不是车辆 CAN 安全网关:车辆控制输出仍须经过原有的车辆接口、进程与 panda 安全链。这条定位决定了后面所有设计约束——外置模块可以失联、可以回退,但不能绕过控制安全链。

    发现与状态:USB 快照不是同步断线检测器

    设备发现从系统总线读取 VID/PID、链路速率、产品字符串、链路错误与接口朝向,再写入设备状态;其中「模块在位」的判定只识别运行态 VID/PID,不要求产品字符串版本匹配。状态线程约每 10 秒或总线拓扑变化时更新一次快照,队列容量为 1,主线程可以沿用旧值。因此它是一份采样快照,不是同步的断线检测器——真正判断「用哪个模型」的是另外两层证据。

    刷写路径:格式校验不等于来源认证

    刷写入口读取内置封装镜像,检验封装格式、产品字符串与调用方期望版本;已匹配则跳过,并设定 600 秒全局预算。镜像校验检查的是长度上限、魔数、字节校验和与 CRC32——这些字段能发现传输或格式损坏,但不能证明镜像来源或版本内容不可替换。固件写入按 4 KiB 扇区擦除、128 B 页写入,并逐页与整扇区回读验证;配置页备份为 0x100 字节,且备份文件以主机名确定路径,已有备份会优先于当前设备配置。

    校验 / 步骤 检查什么 不保证什么
    格式与产品字符串 封装结构、目标产品名、期望版本 不证明镜像内容可信
    长度上限 / 魔数 / 校验和 / CRC32 传输与格式完整性 不是来源认证或数字签名
    页 / 扇区回读 写入范围与内存目标一致 不证明新固件已启动
    配置页备份(0x100 字节) 保留设备侧配置 备份与具体设备的绑定需台架确认

    六种状态不该合并成一个退出码

    刷写与恢复会经过多条分支,每条分支得到的「证据」并不相同:首次发现时无设备会直接正常返回(没写入、没激活);产品字符串已匹配且未强制刷写会直接返回(只核对枚举字符串,没读闪存);普通路径完成写入并回读一致后才调用激活;而 ROM 路径若已提交但仍停留在 ROM 或已离线,恢复函数返回失败,外层却正常返回,于是退出码 0 被记成「成功」。此外,激活函数的所有普通完成分支都返回空值——调用方无法从返回值判断设备到底是否已激活并重枚举。

    源码分支 脚本退出码 / 上层状态 实际得到的证据
    首次发现时无设备 正常返回,记为已刷写 仅说明发现时无设备,未写入、未激活
    产品字符串已匹配且未强制 正常返回,记为已刷写 只核对枚举字符串,未读闪存、未验证 GPU/PCIe
    普通路径完成写入并回读 调用激活,正常返回 写入范围与内存目标一致,不一定代表已启动
    ROM 已提交但仍停留 ROM 或离线 恢复返回失败,外层正常返回 未完成运行态回读校验,属「提交成功 / 验证完成」混淆
    镜像、备份、读写或预算抛异常 退出码 1,记为失败 明确失败,但不自动说明设备是否仍可恢复

    重试额度同样是状态的一部分:尝试次数在进程生命周期内不复位,产品字符串恢复匹配只清「已刷写」标志,不清尝试次数;同一进程用满三次后,即使后来版本又出现不匹配,也不会再自动重试。600 秒预算是协作式收敛预算,只在重试/恢复循环的调用点检查,不是能中断所有 USB 操作的硬截止。

    模型回退与插件约束

    推理侧的设计是分层的:先尝试加载并预热大模型,超时或失败则走小模型;运行中大模型异常也会回退,并把状态写入消息供上层生成加载/失败事件。监测主题标称 10 Hz,但 USB 不存在时监测线程不会按 10 Hz 发送有效消息。插件化时,固件维护接口应返回分离的状态:未找到、已匹配、已写入并验证、等待激活、运行态已验证、失败,并附目标设备身份、镜像摘要、采样时间与失败阶段;重试额度应绑定设备/版本/维护事务,而不是复用进程内布尔值。安全侧只有运行态重新枚举、产品版本与推理健康全部确认后,才能把模块标记为可用——这仍不能替代车辆控制安全链的验证,外置模块也不得获得发送 CAN 的权限。

    常见问题

    镜像通过 CRC32 校验,就能说明固件可信吗?

    不能。长度上限、魔数、字节校验和与 CRC32 属于传输与格式完整性校验,用于发现损坏或截断;它不是来源认证或数字签名验证,改变镜像内容并重新计算校验值后仍可通过校验,因此不能据此判断镜像来源或版本不可替换。

    刷写脚本返回成功,就代表升级完成了吗?

    不一定。ROM 恢复已提交但设备未重新出现时会返回失败,外层却正常返回、退出码 0 被记成成功,而此时还没有完成运行态镜像校验;激活函数也无法从返回值告知调用方设备是否真的已激活并重枚举。

    为什么要有三次尝试上限?

    为了限制自动刷写的次数与风险。尝试次数在进程生命周期内不复位,产品字符串恢复匹配也只清「已刷写」标志、不清次数,因此同一进程用满三次后不会再自动重试,避免在异常设备上反复写入。

    外置计算模块能接管车辆控制吗?

    不能。它只是推理设备,车辆控制输出仍须经过原有的车辆接口、进程与 panda 安全链;固件升级是驻车维护事务,模块本身不得获得发送 CAN 的权限,也不能替代独立安全网关。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CHESTNUT_USB_FIRMWARE_DEEP_DIVE.md(USB 发现、固件刷写路径、镜像与备份、ROM 恢复、模型回退与插件约束)

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

    更新记录

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