标签: tinygrad

  • tinygrad 优化器为何不能只存权重:zero_grad 置 None

    结论:tinygrad 的优化器是训练期参数更新器,不是车端规划器或控制器;把它接进车端推理链没有意义。有两个事实最容易被忽略。第一,zero_grad() 是把每个参数的 .grad 设为 None,不是对梯度缓冲填零,下一轮 backward() 才重新生成梯度。第二,step() 的执行包含参数、动量/矩和偏置幂三组状态的同批 realize,只序列化权重而遗漏优化器状态,不能无损续训。此外 FUSE_OPTIM 与 TRAINING 默认都是 0,它们是 tinygrad 的上下文/构建配置,不是车辆安全授权。以下为固定快照的静态分析,未运行完整优化器矩阵或实车测试,本地没有模型实体。

    公共生命周期

    Optimizer.__init__() 拒绝负学习率,只把 is_param 张量作为可更新参数并按对象去重,非参数进入 buffers;无可更新参数会断言。device 默认取第一个参数的设备,OPTIM_DTYPE 默认 float32,学习率 dtype 至少为 float32。_new_optim_param() 为动量/矩状态创建零张量:普通模式逐参数创建,设备为 tuple 时使用 zeros_like,融合模式只创建一条展平状态,长度由 pos_params 决定。

    调度与执行上,schedule_step() 要求 TRAINING=1:融合模式拼接参数与梯度、执行一次 _step() 再按原形状拆回,普通模式逐参数更新;它调用 assign() 排定状态和参数写入,返回额外状态、参数和 buffer 的实现列表,梯度缺失则通过 unwrap() 报错。step() 把 schedule_step() 返回的张量交给 Tensor.realize();_apply_update() 将更新量搬到参数设备并从 detach() 后的参数中减去。由于 schedule_step() 已有 assign 副作用,不应视为只读计划 API。

    入口 实现及外部契约
    Optimizer.__init__() 拒绝负学习率;仅 is_param 张量可更新并按对象去重;OPTIM_DTYPE 默认 float32。
    _new_optim_param() 创建零状态张量;融合模式只建一条展平状态,长度由 pos_params 决定。
    zero_grad() 把 .grad 设为 None,不是填零;下轮反传重新生成。
    schedule_step() 要求 TRAINING=1;融合拼接后一次 _step() 再拆回;有 assign 副作用。
    step() / _apply_update() 前者把张量交给 realize();后者在参数设备上从 detach() 参数减去更新量。
    OptimizerGroup 汇集多优化器的参数/buffer/排定更新;无跨组参数去重,也未调用基类构造器。

    优化器族与状态布局

    优化器族大致分为 LARS 家族与 LAMB 家族。SGD() 是 LARS(tcoef=0, pre_wd=True) 的包装;LARS._step() 可按参数/梯度范数计算 trust ratio,支持两种动量与 Nesterov,权重衰减和学习率在 classic 分支的作用次序不同。Muon() 复用 LARS 更新主干,先对梯度应用 Newton-Schulz,再做学习率及后置权重衰减,并明确拒绝 fused=True。

    优化器 实现要点 边界
    SGD() LARS(tcoef=0, pre_wd=True) 包装 复用 LARS 主干与权重衰减次序。
    LARS._step() 按参数/梯度范数算 trust ratio;两种动量与 Nesterov 权重衰减与学习率次序在 classic 分支不同。
    Muon() 先 Newton-Schulz 再学习率与后置权重衰减 拒绝 fused=True;形状与数值稳定性不能由接口名保证。
    Adam() 包装 LAMB(adam=True),trust ratio 固定为 1 不加权重衰减。
    AdamW() 同 Adam 包装 默认衰减为 0.01。
    LAMB._step() 更新幂状态与一、二阶矩,做偏置修正与 eps 分母稳定,可选算 trust ratio 梯度与状态设备不同则先搬至状态设备;更新量 shard_like(t) 在参数设备应用。

    状态布局的关键是 OPTIM_DTYPE 默认 float32、FUSE_OPTIM 默认关闭;融合时会把参数和梯度展平拼接,切换融合配置或设备布局不能直接复用旧状态形状。学习率 dtype 至少为 float32。

    状态机与调用顺序

    典型训练调用为:loss.backward() 前先 zero_grad(),随后 loss.backward()、step()。step() 的执行包含参数、动量/矩和偏置幂的实现;仅序列化权重而遗漏优化器状态,不能无损续训。FUSE_OPTIM 默认 0、TRAINING 默认 0,这两个开关是 tinygrad 的上下文/构建配置,不是车辆安全授权。OptimizerGroup 的交叉参数重叠、状态文件版本兼容、多设备搬运以及异常发生在部分 assign() 之后的恢复语义,均应列入训练插件测试矩阵。

    开关 / 状态 默认值 含义
    OPTIM_DTYPE float32 优化器状态张量的默认 dtype。
    FUSE_OPTIM 0(关闭) 融合时参数与梯度展平拼接为单条状态。
    TRAINING 0(关闭) schedule_step() 要求其为 1;是构建/上下文配置,非安全授权。
    优化器状态集合 — 参数 + 动量/矩 + 偏置幂,step() 时同批 realize。

    ONNX 与车端关系

    OnnxRunner._init_from_graph() 通过图中训练域节点设置 is_training,OnnxRunner.__call__() 在该标志下设置 Context(TRAINING=...)。ONNX 的 Adam() 借用 TinyAdam,但把外部 V/H 及学习率装入临时优化器,并依据步数初始化偏置幂——这属于 ONNX 训练域功能,不表明 openpilot 车端执行在线学习。

    车端一侧,openpilot 的 compile_model() 在构建时把 ONNX 编译为捕获的模型工件;运行期 ModelState.run() 调用 run_warp/run_model 并解析输出。当前本地源码未见该路径实例化 tinygrad.nn.optim 或调用 step();由于缺少模型实体,只能确认可见源码的调用边界,不能断言模型图内部的全部节点。

    硬件与插件重构建议

    把优化器、梯度、训练域 ONNX 和权重写入归入离线训练/构建插件;车端推理插件只接受已验证、版本绑定、只读的模型工件,不要给训练插件 CAN 发送、panda 安全模式或控制消息发布权限。模型工件契约应记录 ONNX/opset、tinygrad 版本、dtype、输入输出 shape、目标设备、权重哈希和参考数值;优化器状态另行版本化,fused、参数顺序和设备分片都需一致。硬件替换只接管相机帧、时间戳、缓冲拷贝/映射和推理执行端,保持 modelV2 等输出 schema、延迟预算和健康信号稳定,不可通过更换优化器绕开感知、规划、车型协议与 panda 固件安全门。

    常见问题

    zero_grad() 是把梯度清零吗?

    不是。它把每个参数的 .grad 设为 None,而不是对已有梯度缓冲填零;下一轮 backward() 会重新生成梯度。

    只保存权重就能无损续训吗?

    不能。step() 的执行包含参数、动量/矩和偏置幂三组状态的同批 realize;只序列化权重而遗漏优化器状态,无法无损续训,优化器状态需要另行版本化保存。

    tinygrad 优化器与 openpilot 车端推理是什么关系?

    没有直接关系。优化器是训练期参数更新器;openpilot 在构建时把 ONNX 编译为捕获的模型工件,运行期 ModelState.run() 只运行该工件,当前可见源码未见车端实例化 tinygrad.nn.optim 或调用 step()。

    FUSE_OPTIM 或 TRAINING 打开会影响车辆安全吗?

    不应这样理解。这两个开关默认都是 0,是 tinygrad 的上下文/构建配置,不是车辆安全授权;训练数值测试也不等于整车安全验证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_OPTIMIZER_TRAINING_STATE_INFERENCE_BOUNDARY_DEEP_DIVE.md(取用了优化器族与状态表、zero_grad 置 None、三组状态同批 realize、ONNX 训练域与车端推理边界)。

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

    更新记录

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

  • tinygrad 神经网络层:形状、状态与 Embedding 边界

    结论:tinygrad 的 nn/__init__.py 是通用模型层库,它的层按「形状契约 + 状态契约」工作:BatchNorm 的 running_mean/running_var/num_batches_tracked 是推理结果相关状态,Conv1d/ConvTranspose1d 复用二维实现,Embedding 默认路径对越界索引返回零向量而非显式拒绝。openpilot 是通过 ONNX 构建已捕获模型,不能把层库的训练能力直接等同于车端在线学习。

    组件与关键函数

    基线是 source/ 固定快照,逐类解析公开层和两个 Embedding 内部路径。各层的形状契约必须写清楚,不能只按名字替换。

    组件 实现与输入输出契约
    BatchNorm 通道轴固定为 1,均值/方差跨其余轴。训练时以 momentum 更新累计统计量;方差更新含 x.numel()/(x.numel()-x.shape[1]) 无偏校正,单通道单样本等分母为零的输入不属于可直接保证的有效域。BatchNorm2d/3d 只是别名。
    Conv1d / Conv2d 一维接口以单元素 kernel tuple 复用通用卷积层。权重为 (out_channels, in_channels/groups, *kernel);padding='same' 只接受 stride=1,按反向空间轴生成不对称 pad。
    ConvTranspose1d / ConvTranspose2d 初始化后重建权重为 (in_channels, out_channels/groups, *kernel),调用 conv_transpose2d() 并传入 stride、padding、dilation、groups 与 output_padding。加载普通卷积权重不能只凭同名字段互换。
    Linear 初始化权重 (out_features, in_features),前向转置后调用 x.linear(),在最后一维做变换;支持前置批/时间维。
    LSTMCell weight_ih/weight_hh 产生四门 i/f/g/o,输入为 (batch,input_size),可选 (h,c) 形状为 (batch,hidden_size);省略时创建零状态,调用者负责跨时间步保存与回传状态。

    归一化层各有约定:GroupNorm 把输入 reshape 为 (N, groups, -1)、InstanceNorm 为 (N, channels, -1),借 layernorm() 沿末维归一化再还原形状;输入通道数/分组整除与 NCHW 布局是适配契约。LayerNorm 断言输入尾维与 normalized_shape 完全一致,LayerNorm2d 把 NCHW 暂转 NHWC 归一化通道再转回,二者都不更新运行统计量。RMSNorm 按末维平方均值计算逆 RMS,先转 float 计算、再转回输入 dtype,半精度目标后端需单独比对数值误差。

    形状契约上还有几处必须显式记住:BatchNorm2d/3d 只是同一类的别名;ConvTranspose 系列在重建权重后调用 conv_transpose2d(),并把 output_padding 一并传入,普通卷积权重不能只凭字段同名就替换;Linear 前向会先转置权重、在最后一维做变换,因此前置的批/时间维是允许的。

    Embedding 的两条内部路径

    Embedding.__call__() 仅接受整数索引。默认通过与 arange(vocab) 全量比较、选取权重再沿词表归约,输出形状为 indices.shape + (embed_size,);索引超出词表时默认路径得到零向量,并非显式拒绝。当 USE_ATOMICS=1 时把前向包成 CALL,反向先清零 FP32 梯度,再按 token 原子 scatter-add。

    这里有一条语义差异:原子实现仅列出 CPU/NULL/AMD,词表分片只支持轴 0、其他分片会断言;索引在反向内 clip 到词表边界,因此无效 token 在默认前向和原子反向可能产生不同语义。插件入口应先验证索引在 0 到 vocab_size 范围内。

    状态管理方面,LSTMCell 在省略初始状态时创建零状态,返回新的 (h,c),跨时间步的保存与回传由调用者负责;LayerNorm、LayerNorm2d、GroupNorm、InstanceNorm 与 RMSNorm 都不更新运行统计量,只有 BatchNorm 会写入累计统计量,这也是它在导出时必须与权重一起版本化的原因。

    状态、硬件与插件契约

    BatchNorm 的运行统计量属于推理结果相关状态,模型导出必须与权重、eps、仿射配置一起版本化;切换 TRAINING 会改变统计来源及写入行为。TRAINING 与 USE_ATOMICS 默认都关闭。Conv2d、Linear 等初始化使用随机权重,加载模型工件后才能代表实际训练模型。

    契约项 必须声明的内容
    张量 ABI 输入 shape/dtype/布局、权重 hash、运算版本、目标后端、数值容差。
    状态 运行统计量、状态复位条件、最坏时延。
    权限 模型层实现不得获得 CAN 发送、panda 安全配置或车辆控制发布权限。
    原子路径 若启用原子自定义内核,需单独审计设备支持、重复索引并发与越界 token 行为。

    「万物即插件」宜以可验证的模型工件和张量 ABI为边界,而不是把每个层都做成车端可热替换插件。相机/推理硬件插件提供帧、时间戳、缓冲所有权和执行能力;模型层实现只消费签名/版本绑定的工件。

    反过来说,层库能训练、能加载随机权重,并不代表它在车端可用;只有把权重、运行统计量与运算版本一起冻结、并对目标后端做数值与时延验收之后,这些层才具备进入推理链的前提。

    与 openpilot 的关系与验证范围

    compile_model() 构建 ONNX/JIT 工件;运行期 ModelState.run() 调用已捕获模型。对 openpilot 与 examples 目录的 nn.<层名> 搜索未发现直接实例化上述层;这只说明可见 Python 源码,不能推断缺失的 LFS 模型图里没有对应 ONNX 算子。

    验证层面,以 PYTHONPATH=. DEV=PYTHON RUN_SLOW=1 运行六项上游定向测试:BatchNorm2d、Linear、LayerNorm2d、RMSNorm、Embedding、LSTMCell,全部通过(51.793 秒)。转置卷积、原子路径、分片、多后端、真实模型数值与目标车载硬件仍未验证;LFS 模型实体缺失,不能声称整模型或整车验证。

    常见问题

    Embedding 遇到越界索引会发生什么?

    默认路径会得到零向量,并非显式拒绝:_embedding_fwd() 通过与 arange(vocab) 全量比较、选取权重再沿词表归约。但索引在原子反向内会被 clip 到词表边界,所以无效 token 在默认前向和原子反向可能产生不同语义,插件入口应先验证索引在 0 到 vocab_size 范围内。

    BatchNorm 的运行统计量需要一起导出吗?

    需要。running_mean/running_var/num_batches_tracked 是推理结果相关状态,源码要求模型导出必须与权重、eps、仿射配置一起版本化;切换 TRAINING 会改变统计来源及写入行为。TRAINING 默认关闭。

    Conv1d 能直接用 Conv2d 的权重换吗?

    不能只凭同名字段互换。Conv1d 以单元素 kernel tuple 复用通用卷积层;而 ConvTranspose1d/2d 会在初始化后重建权重为 (in_channels, out_channels/groups, *kernel) 并调用 conv_transpose2d(),加载普通卷积权重不能只靠字段名相同就替换。

    可以把每个 nn 层做成车端热替换插件吗?

    不建议。源码建议以可验证的模型工件和张量 ABI 为边界,而不是把每个层都做成车端可热替换插件;契约至少包括输入 shape/dtype/布局、权重 hash、运算版本、目标后端、数值容差、运行统计量、状态复位条件和最坏时延。模型层实现不得获得 CAN 发送、panda 安全配置或车辆控制发布权限。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_NN_LAYERS_STATE_SHAPE_EMBEDDING_CONTRACT_DEEP_DIVE.md(取用了各层形状/状态契约、Embedding 两条内部路径、插件边界与六项上游测试结论)。
    • 项目 02 —— openpilot-research/TINYGRAD_DTYPE_UOP_LOWERING_EMULATION_DEEP_DIVE.md(仅用于确认 dtype 降解专题的接续关系,未取用其未公开内容)。

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

    更新记录

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

  • tinygrad AMD PSP:固件加载成功不等于 GPU 能跑模型

    结论:AMD GPU 的固件链里,「固件加载成功」「GPU 队列可工作」「模型输出有效」是三个不同状态:PSP 把经过选择的镜像交给 sOS 后,后续 SMU/GFX/SDMA 才依次使用。固定 SHA-256 只保证字节与清单一致,不等于签名、可信发布者或运行时兼容性证明;而 _ring_submit() 的写指针未见软件取模,属待验证项而非已定性缺陷。

    本文依据固定归档的 AMFirmware 与 AM_PSP 做静态源码与纯内存探针分析。这是 AMD GPU 计算设备的启动链,不是车辆控制或 panda 安全固件链;没有下载固件、访问 GPU/车辆、发送 CAN 或执行刷写。系统总图与 AM IP 启动顺序属于相邻专题。

    主调用与关键函数

    AMDev.init_sw() 先构造 AMFirmware,再创建七个 IP 对象并依次调用 init_sw();GPU 硬件启动期间,AMDev.init_hw() 按传入 IP 顺序调用 init_hw()。数据流依次是:IP 版本 → load_fw() → 按头部拆 SOS 与 IP 描述符 → PSP msg1 缓冲 → bootloader 装 sOS → 创建 ring → TOC 与 TMR → SMU 与其他 IP 固件命令 → 后续 GPU IP 初始化。

    入口 具体职责 需要验证的边界
    AMFirmware.__init__() 按 MP0/MP1/GC/SDMA IP 版本选文件;拆 SOS;形成 SMU、SDMA、PFP/ME/MEC、IMU、RLC 等描述符 固件集合必须与芯片/IP 版本、头部版本和硬件启动模式一致
    load_fw() / fetch_fw() 从本地 amdgpu 固件目录解压并比对固定 SHA-256;本地无匹配文件时取固定修订再校验哈希 哈希保证字节与清单一致,不等于签名或运行时兼容性证明
    desc() 以头部给出的 offset/size 切 memoryview,挂接 PSP 固件类型 普通切片会截短越界长度,未显式确认 len(slice)==size
    AM_PSP.init_sw() USB 路径用 SYS 映射 msg1;非 USB 路径用 VRAM;分配命令、fence、64 KiB ring,并设置 boot-time/autoload TMR 标志 物理/MC/虚拟地址单位、映射寿命和 DMA 可见性不能交给通用算法插件
    init_hw() 缺 sOS 时逐项经 bootloader 加载;重建 ring;可选 TOC/TMR;先 SMU,再设 TMR,后载各 IP 固件;最后处理 RLC 启动依赖顺序是状态机,不宜平铺为可任意重排的插件钩子
    _prep_msg1() / _bootloader_load_component() 复制固件到 msg1,补 4 字节并按 16 字节填充,flush HDP;bootloader 经消息寄存器读取地址及组件 ID 长度断言只检查原始数据,未覆盖追加和对齐后的长度
    _ring_create() / _ring_submit() 销毁旧 ring 后创建;写命令与 frame,推进写指针,等 fence,再查响应 status ring 写指针没有显式按 ring_size 取模,硬件语义尚未确定

    固件来源与信任边界

    load_fw() 用 ctypes.from_address() 读取版本头和具体头部,desc() 与 SOS 拆分依赖固件给出的数量、偏移、长度。固定哈希减小意外字节变更风险,但不足以替代解析边界检查。缓存文件、镜像哈希、芯片版本、驱动版本应一起进入部署清单与启动审计。

    信任环节 当前做法 它能证明什么 / 不能证明什么
    镜像完整性 解压后比对固定 SHA-256;必要时取固定修订再校验 证明字节与清单一致;不证明签名或可信发布者
    头部解析 按头部 offset/size 切 memoryview 依赖固件自报数量/偏移/长度;需补结构/长度校验
    离线部署 车端需预置并锁定镜像 无网络时无法即时取固定修订,必须提前锁定
    启动结果 命令返回 status==0 不能扩大解释成整个 GPU 或模型健康

    已确认边界与未验证问题

    纯内存探针直接调用 _prep_msg1(),提供 32 字节 memoryview 与假的 flush_hdp(),未触碰设备:输入 16、28 字节成功;29、32 字节通过现有断言,但因追加 4 字节后填充到 16 字节导致结构不匹配的 ValueError;33 字节由断言拒绝。因此检查对象应是填充后的长度。该结果只证明函数缓冲边界,不代表现实固件恰好接近 msg1 容量。

    问题 证据 当前定性
    msg1 长度检查对象 探针:28 字节通过、29 与 32 字节在填充后报结构不匹配、33 字节被拒 应检查填充后的长度,而非仅原始长度
    ring 写指针回卷 用 ring_paddr + prev_wptr * 4 定位 frame,未见软件取模;模拟实现也线性取 frame 待验证项,不将其定性为实机越界缺陷
    desc 切片越界 普通切片会截短越界长度,未显式确认长度相等 后续加载前需做结构/长度校验

    _ring_submit() 用 ring_paddr + prev_wptr * 4 定位 frame,未见软件取模;MockPSP._ring_submit() 也按旧写指针线性取 frame,不会证明真实硬件自动回卷。需要先获得 PSP 对写指针单位、回卷规则和 ring 最大积压的文档或设备跟踪,再用小 ring 故障注入验证。

    TMR 与命令编码各有边界:_tmr_init() 由 TOC 响应给出 TMR 大小,部分启动从 scratch5 恢复大小,再按平台模式申请并提交 TMR——scratch 恢复值及内存保留关系须经冷/暖启动台架确认。_load_ip_fw_cmd()、_load_toc_cmd() 与 _rlc_autoload_cmd() 把镜像、TOC 或自动装载请求编码为 PSP 命令,但不能把命令返回 status==0 扩大解释成整个 GPU 或模型健康。

    启动顺序本身也是契约的一部分:init_hw() 先经 bootloader 逐项加载 sOS,重建 ring,可选 TOC/TMR,然后先 SMU、再设置 TMR、后载入各 IP 固件,最后处理 RLC。把这条依赖顺序平铺成可任意重排的插件钩子,会丢掉「前一步未就绪则后一步无意义」的状态约束。

    对自制硬件与插件化的结论

    将 PSP/MMIO/DMA/固件装载封在受信任设备驱动,只向推理插件暴露带版本的「设备发现、固件校验、内存就绪、队列就绪、任务完成」能力及健康状态。

    1. 插件不得提供任意固件路径、物理地址、PSP 命令或重置入口。
    2. 新车载算力平台可替换计算驱动,但必须独立通过固件/ABI、内存一致性、冷暖启动、异常清理和模型数值/时限验证。
    3. 最终车辆输出仍受车型适配和 panda 板端 safety 约束,不能因「固件加载成功」就授权执行。
    4. 启动依赖顺序是状态机,不应把步骤平铺为可任意重排的插件钩子。

    边界说明:本专题没有 GPU、实车或 LFS 模型验证证据;探针只证明函数缓冲边界。

    常见问题

    固件加载成功就等于 GPU 能跑模型吗?

    不等于。PSP 把经过选择的镜像交给 sOS 后,后续 SMU/GFX/SDMA 才依次使用;「固件加载成功」「GPU 队列可工作」「模型输出有效」是三个不同状态,命令返回 status==0 不能扩大解释成整个 GPU 或模型健康。

    固定 SHA-256 能证明固件可信吗?

    不能。哈希只保证字节与清单一致,不等于签名、可信发布者或运行时兼容性证明。离线车端还需要预置并锁定镜像,并把缓存文件、镜像哈希、芯片版本、驱动版本一起纳入部署清单与启动审计。

    _ring_submit 的写指针没有取模一定是缺陷吗?

    不能下这个结论。源码用 ring_paddr + prev_wptr * 4 定位 frame,未见软件取模,模拟实现也按旧写指针线性取 frame,不会证明真实硬件自动回卷。这是待验证项,需要文档或设备跟踪,再用小 ring 故障注入验证。

    为什么消息缓冲的长度断言要按填充后的长度?

    因为探针显示:输入 29 或 32 字节虽通过现有断言,但因追加 4 字节后再填充到 16 字节而对齐结构不匹配并报错;28 字节可以成功,33 字节被断言拒绝。所以检查对象应是填充后的长度,而不是仅原始长度。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_AM_PSP_FIRMWARE_BOOT_RING_TRUST_CONTRACT_DEEP_DIVE.md(取用了 AMFirmware/AM_PSP 函数索引、固件信任边界、msg1 与 ring 的探针结论)。
    • 项目 02 —— openpilot-research/TINYGRAD_AM_DEVICE_IP_BOOT_PARTIAL_RESET_LIFECYCLE_DEEP_DIVE.md(仅用于确认 AM IP 启动顺序,未取用其未公开内容)。

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

    更新记录

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

  • tinygrad 隐式屏障两类:RAW 与 WAR 局部内存同步何时插入

    结论:tinygrad 的局部内存屏障不是运行时补救,而是代码生成末段的编译规则:pm_implicit_barriers 在算子/dtype 降解与门搬移之后才插入屏障,分两类——add_raw_barrier() 处理 LOCAL 地址空间的 AFTER(写后读),add_war_barrier() 在 REDUCE/WEAK/LOOP 的多迭代 END 上处理同缓冲读写的循环尾顺序(写后写覆盖前的读完成)。两条规则只依据 UOp 依赖与缓冲身份,既不证明任意自制 ISA 的内存一致性,也不覆盖不同工作组之间的全局同步。若后端丢失 AFTER/END/门条件或把屏障当空操作,模型输出可出现跨 lane 数据竞争。以下为固定归档的静态分析,未运行完整 GROUP_REDUCE/WMMA 数值矩阵或目标 GPU。

    编译阶段顺序

    full_rewrite_to_sink() 在范围与符号优化之后依次推进:用 expander 展开归约、特殊轴与 WMMA;用 pm_reduce_local 消除 REDUCE;用 pm_add_local_buffers 降解 STAGE;再添加 GPU 维度、显式 LOAD 与后期逐 lane 标量化;最后完成算子/dtype 降解及门搬移,才用 pm_implicit_barriers 加局部内存屏障,随后构造控制流依赖并线性化。因此局部缓冲与屏障既不是前端 Tensor 操作,也不是运行时临时补救。

    阶段 主要规则 产出
    范围与符号优化 — 基础 UOp 图。
    展开 expander / build_range_map() UNROLL/UPCAST RANGE 变为常量向量,处理 REDUCE/WMMA/范围展平。
    消除 REDUCE pm_reduce_local、fix_group_for_reduce() GROUP_REDUCE 经 LOCAL 缓冲两段完成。
    降解 STAGE pm_add_local_buffers、add_local_buffer() STAGE 变指定地址空间的占位缓冲。
    维度/LOAD/标量化 pm_add_loads、do_devectorize() 显式 LOAD 与逐 lane 元素运算。
    屏障 pm_implicit_barriers 插入 RAW/WAR 局部内存屏障。

    关键函数与边界

    展开阶段里有几个容易混淆的分工。expand_reduce() 只在 REDUCE 除显式 RANGE 外还涉及形状轴时,把非单例轴前移、记入归约轴数,再把结果 reshape 回原轴形状,若无新增形状轴则不改写。fix_group_for_reduce() 把 GROUP_REDUCE 分两段:先做非分组归约,把中间值 bufferize 到 LOCAL 缓冲(包括上游 LOCAL/WARP 轴),再以 REDUCE 轴读取并完成最终归约——不能把它当成单线程普通求和。

    函数或规则 源码责任
    expand_reduce() REDUCE 涉及形状轴时前移非单例轴、记入归约轴数、reshape 回原形状。
    expand_wmma()、contract_axis()、unroll_axis() WMMA 输入 upcast 轴打包为末轴、输出拆回原布局;仅当 arg[3] 带轴元数据时执行。
    expand_broadcast()、broadcast_and_devec_wmma() 对形状不一致的源显式广播;WMMA 外部批维按坐标展开后堆叠。
    pm_wmma_add 把 WMMA 之后的 ADD 融入累加器,并可把部分 PERMUTE/RESHAPE 搬过加法。
    reduce_ranges_to_acc()、merge_reduce_ends() 为剩余范围归约生成 REG 累加器;只合并同范围与同嵌套上下文的 END。
    expand_horizontal_reduce() 无显式范围的水平归约按被压平轴元素逐项索引,再用 ALU 串接。

    向量标量化侧有两个关键点。do_devectorize()/devectorizer2 逐 lane 重建元素运算与 LOAD/STORE,Invalid lane 保留特殊处理,STORE 输出 GROUP;第二阶段还拆 INDEX/STACK、标量 EXPAND、RESHAPE 与 WMMA 堆叠——输入广播若未先整理,函数会拒绝标量化。pm_cast_float_alu 则在 SIN/LOG2/EXP2/SQRT/RECIPROCAL 的输入 dtype 与输出不同时显式 CAST,保证后续浮点多项式降解接收合适 dtype。

    本地缓冲的引入也分阶段:add_local_buffer() 把 STAGE 改为指定地址空间的占位缓冲,并通过 STORE、END、AFTER 建立生产与消费顺序;pm_add_loads 对 GLOBAL/LOCAL/REG 值源显式插 LOAD。形状变化的 BITCAST 不按普通逐源载入规则处理,这一点在跨后端替换时需要单独核对。

    还有两处语义细节:pm_reduce_identity 在 REDUCE 来源含 Invalid 时用该运算的单位元替代无效 lane,它与无效地址的负载门不同但必须一致;pm_wmma_add 把 WMMA 之后的 ADD 融入累加器,会改变矩阵指令的累加位置,需核对 dtype 与舍入。

    两类屏障分别防什么

    add_raw_barrier() 只处理 LOCAL 地址空间的 AFTER:沿依赖图查找未被已有 BARRIER 截断的 LOCAL STORE,有则在后继访问前放置工作组屏障,针对写后读(RAW)可见性。add_war_barrier() 则在 REDUCE/WEAK/LOOP 的多迭代 END 上检查循环体内的 LOCAL STORE 与同缓冲 LOAD,命中时把 BARRIER 放到循环尾,针对下一轮覆盖前的读完成(WAR)顺序。

    规则 触发位置 防止的竞争
    add_raw_barrier() LOCAL 地址空间的 AFTER 写后读:LOCAL STORE 被后继访问读取前的可见性。
    add_war_barrier() REDUCE/WEAK/LOOP 的多迭代 END 写后写:下一轮覆盖前需先完成对同缓冲的读。

    已经是 BARRIER 的 END、单元素范围或不共享缓冲的访问不会触发相同规则。硬件插件必须定义 LOCAL 存储可见性、工作组屏障参与条件和循环发散约束;运行时故障与模型不可用状态仍需由上层安全链处理,计算后端不得直接获得 CAN 发送权。

    验证与限制

    在 PYTHONPATH=. DEV=PYTHON 下,两个向量标量化用例和一个归约线性化用例通过。独立 UOp 探针确认:LOCAL 写后读的 AFTER 插入 RAW 屏障;多迭代 REDUCE 中同缓冲读写的 END 插入 WAR 屏障;无 LOCAL STORE 和单元素 END 不触发。未运行完整 GROUP_REDUCE/WMMA 数值矩阵、真实工作组并发、目标 GPU、模型实体或实车,归档源码未编辑。

    常见问题

    局部内存屏障是在运行时临时加上的吗?

    不是。屏障由 pm_implicit_barriers 在代码生成末段插入:先完成算子/dtype 降解及门搬移,再加局部内存屏障,随后才构造控制流依赖并线性化,属于通用内核编译规则。

    RAW 屏障和 WAR 屏障分别插在哪里?

    add_raw_barrier() 处理 LOCAL 地址空间的 AFTER,在后继访问前放工作组屏障;add_war_barrier() 在 REDUCE/WEAK/LOOP 的多迭代 END 上,把屏障放到循环尾,处理同缓冲读写。

    这两个规则能保证任意自制 ISA 的内存一致性吗?

    不能。它们只依据 UOp 依赖与缓冲身份插入屏障,不负责证明任意自制 ISA 的内存一致性,也不覆盖不同工作组之间的全局同步;后端若丢失 AFTER/END/门条件或把屏障当空操作,输出可出现跨 lane 数据竞争。

    GROUP_REDUCE 能当成单线程普通求和吗?

    不能。fix_group_for_reduce() 把它分两段:先做非分组归约并把中间值 bufferize 到 LOCAL 缓冲(含上游 LOCAL/WARP 轴),再以 REDUCE 轴读取并完成最终归约。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_CODEGEN_EXPAND_REDUCE_DEVECTOR_BARRIER_CONTRACT_DEEP_DIVE.md(取用了编译阶段顺序、关键函数与边界表、RAW/WAR 屏障规则与限制、以及 UOp 探针验证结论)。

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

    更新记录

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

  • 端侧推理工具链快速上手:从张量到 safetensors 的七步

    结论:要做端侧推理,公开资料给出的最小闭环是七步:装依赖 → 建张量 → 组模型 → 写训练循环 → 评估 → 上 JIT → 存取权重。它的三条关键性质是:张量运算是惰性的,只有调用 .realize() 或 .numpy() 才真正执行;神经网络就是一组张量运算,模块不必须继承任何基类;JIT 用于加速固定输入形状的推理,但不支持变化输入尺寸与非本框架操作。权重标准格式为 safetensors。

    准备:导入与依赖

    快速上手指南假定读者不熟悉任何深度学习框架,但具备神经网络基础。起步只需要少量导入:数值库、计时辅助、张量类与数据类型。

    张量是这套工具链的基础数据结构,可以理解为某个数据类型的多维数组;所有高层操作都作用在张量上。张量既能从 Python 列表或数值库数组构造,也能用工厂方法生成:

    工厂方法 作用
    full / zeros / ones 按给定形状生成定值、全零或全一张量
    full_like / zeros_like / ones_like 按另一张量的形状生成对应值
    eye 单位矩阵
    arange 按起止与步长生成序列
    rand / randn / uniform 均匀分布、标准正态分布或指定区间的随机张量

    所有创建方法都可以带上 dtype 参数指定数据类型。运算则为惰性:例如 (t + 1) * 2 之后再接 relu() 与 log_softmax(),表达式不会立即求值,直到用 .realize() 或 .numpy() 触发。

    模型就是一组张量运算

    神经网络在这个框架里就是张量运算的组合。常见做法是把一组运算放进一个类的调用方法里,以便模块化复用;这些类不必继承任何基类,若无需可训练参数,甚至可以不是类。

    指南用一个全连接层示例说明:构造时按初始化方式生成权重张量,可选的偏置为全零;前向时对输入做线性变换。基于它,指南搭了一个两层网络——输入 784 维、隐藏层 128 维、输出 10 维(对应十个数字),两层都不带偏置,激活使用 Leaky ReLU。前向过程就是依次做线性变换与激活,再返回输出。

    训练与评估循环

    训练循环非常直接:定好网络、损失与优化器,对损失调用反向传播得到梯度,再用优化器更新参数。指南在这部分点出一个关键取舍:加载 / 存储类运算在框架里不原生支持,因为它增加了移植到不同后端的复杂度,而且大多数模型并不需要;示例里的交叉熵损失就用 arange 掩码实现,绕开了原生加载 / 存储。

    环节 指南中的取值与做法
    损失 稀疏分类交叉熵
    优化器 随机梯度下降,学习率 3e-4
    参数来源 显式传入,或用工具函数取全部参数
    数据 数据集加载器提供的示例数据集
    训练设置 训练 1000 步,批大小 64,用上下文开启训练模式
    每步动作 取批 → 前向 → 算损失 → 清零梯度 → 反向传播 → 更新参数 → 计算准确率

    评估沿用同样的批大小 64,跑 1000 个批次,并用计时辅助输出耗时。值得注意的是,指南把 Context(TRAINING=1) 用作训练模式开关,退出上下文时该标志会恢复为之前的值——这是用上下文管理状态,而不是全局变量。

    JIT 与模型存取

    指南的「额外能力」部分给出两个工程要点。

    其一,JIT 可以加速某些网络的运算:做法是给前向过程加装饰器,并确保输入输出都是已实现的张量。但指南明确其限制——当前不支持变化输入尺寸的模型,也不支持非本框架的操作。启用后评估耗时通常显著下降,加速器利用率更高。

    其二,权重存取的标准格式是 safetensors:这意味着其他同样使用该格式的模型权重也能直接加载进来。存取由状态字典相关函数完成——先取状态字典,保存到文件,再加载并写回网络。此外,预设模型目录里的许多模型提供了「从预训练加载」的方法,通常会下载权重;这类权重多为另一框架的格式,加载时需要安装对应框架。

    调试与可视化

    再有两点对排查很有用:一是环境变量控制运行时行为,常见的有 DEBUG 与各后端启用开关,完整清单在环境变量文档里;二是设置 VIZ=1 可以可视化神经网络的计算图,用于确认运算是否按预期组织。

    常见问题

    为什么定义了运算却没有立刻执行?

    因为张量运算是惰性的,表达式只有在调用 .realize() 或 .numpy() 时才真正执行。这是该工具链的基础性质,也是理解性能与调试输出的前提。

    可以用 JIT 加速所有模型吗?

    不能。当前 JIT 不支持变化输入尺寸的模型,也不支持非本框架的操作。启用时还需保证输入与输出都是已实现的张量。

    模型权重用什么格式保存?

    标准格式是 safetensors。先取状态字典,保存到文件;加载时读取状态字典再写回网络。使用同样格式的其他模型权重也能直接加载。

    如何确认网络的计算图结构?

    设置 VIZ=1 即可可视化计算图。日常排查运行时行为还可以借助 DEBUG 等环境变量,完整清单见环境变量文档。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/source/tinygrad_repo/docs/quickstart.md(取用张量工厂方法、惰性求值、两层网络示例、训练与评估循环、JIT 限制、safetensors 存取与调试开关)。

    说明:本文为对固定文档快照的整理,未在本机运行完整训练、未验证各后端可用性与性能数字,也未核对示例数据集与预训练权重的实际下载内容。文中所有 API、取值与限制均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

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

  • tinygrad 范围与归约化简:编译器插件契约与四个正确性门

    结论:codegen/simplify.py 处理 UOp 图中的循环范围、条件索引和归约,是编译器内核优化插件,不是可在线替换的控制插件。它的正确性靠几道静态门保证,最核心的是:合并范围只接受 FLOORDIV/FLOORMOD 总数不增加的结果;RANGE % 常量 拆分必须能静态证明整除、且范围长度节点本身为 CONST;遇到无门索引、STORE 或嵌套 REDUCE 时收缩或 collapse 会被阻止。源文档另指出 postopt symbolic 不受 if optimize 保护,插件输入必须是已通过类型、范围及缓冲 ABI 校验的 UOp 图。

    编译调用链:optimize 与非 optimize 的分界

    get_kernel_graph() 先 run_rangeify(),再用 symbolic + pm_reduce_simplify 等规则简化无负载归约,随后把缓冲图降到 STORE。full_rewrite_to_sink() 仅在 optimize=True 时依次执行 pm_load_collapse、pm_split_ranges + pm_flatten_range、初始符号化、pm_flatten_range + pm_simplify_ranges,再进入后范围优化;关闭优化并非仍会执行这些规则。

    源文档特别指出:同一函数的 postopt symbolic 不受上述 if optimize 保护,仍运行 pm_reduce_unparented;而 pm_reduce_load_collapse 是供 pm_load_collapse 内部使用的增强规则集,不能当成独立顶层 pass。

    范围规则与关键函数

    函数或规则 作用及必要条件
    flatten_range() 对 REDUCE/END 的普通范围经 SINK 重新收集并展平;void/bool 回边留在末尾,不作为普通循环范围结束。
    simplify_merge_adjacent() END 只试相邻范围,REDUCE 试排列组合;两轴必须同 AxisType、在所有内部 REDUCE 中归属一致;合并后的索引用商/余数还原,且只接受 FLOORDIV/FLOORMOD 总数不增加的结果;每次只合并一对。
    mark_gated() 从 INDEX 的 WHERE 有效条件中识别 RANGE < CONST,对同一范围取已见上界的最大值;若某次索引未受该范围的门保护,就记录原范围长度;REDUCE 也强制原长度。
    pm_simplify_ranges 汇集 END/REDUCE 合并、INDEX 门收集、REDUCE 禁止收缩;直到 SINK 才通过 do_substitute() 一次性替换范围并清空上下文。
    mark_range_mod() 只针对 RANGE % 常量;范围长度须为常量且 UOp.divides(c.val) 能证明被模数整除;WARP/DEVICE 轴排除;已有待拆轴不会重复登记。
    pm_split_ranges 在 SINK 用商轴与余数轴重建原索引:outer * c + inner,新增轴编号由原 arg 派生。
    reduce_unparented() 源表达式未引用的 REDUCE 轴可移除:ADD 乘以轴长,MUL 对轴长取幂,MAX 保持原值;其他归约运算不走此规则。

    源文档强调:这里的「整除」是静态证明,不是运行时猜测。UOp.divides() 对常量、STACK、ADD、MUL 和带 multiple_of 约束的 PARAM/BUFFER 做有限证明,无法证明即返回 None;mark_range_mod() 还要求范围长度节点本身是 CONST,所以动态长度即使数学上可能整除,也不会触发该拆分。

    四个正确性门

    正确性门 表现
    不增加 FLOORDIV/FLOORMOD 数量 合并相邻范围后,只接受商/余数总数不增加的结果。
    归约范围归属一致 两个轴必须在所有内部 REDUCE 中归属一致,否则不能合并。
    无门索引和归约阻止收缩 某次索引未受范围门保护时记录原长度,REDUCE 强制原长度。
    STORE 和嵌套 REDUCE 阻止 collapse reduce_collapse() 发现 STORE 或内层 REDUCE 即退出。

    pm_reduce_collapse 包含无父范围规则和几类代数重写:把与 RANGE 无关的加项或正整数乘项移出小于比较;把有上下界的布尔门求和化为截断区间长度;拆分加法求和;把布尔乘法改为 WHERE。源文档指出适用范围由模式和 no_range() 判断决定,不意味着所有求和都能闭式化简。

    pm_reduce_load_collapse 额外识别「用动态索引挑选某个 RANGE 元素」的 gated load 求和:替代 RANGE 前先检查索引在 [0, extent),并保留有效性条件;其最后一条规则在含加载索引的比较中逆转一种代数移项,以免对加载得到的索引做可能溢出的加法。源文档提醒,不能把这一优化等同于任意 gather 的安全实现。

    源文档还指出 reduce_collapse() 的处理方式:它逐归约轴提取依赖子图,外部输入先换成有范围界的临时 PARAM 再运行模式重写,结果仍含 RANGE 时放弃。这一设计避免把外部动态表达式误当成轴内常量,但只证明匹配规则覆盖的形式,不等于对所有求和都成立。

    面向硬件与插件化

    这层适合定义为编译器内核优化插件。插件输入应为已通过类型、范围及缓冲 ABI 校验的 UOp 图;输出须保留 RANGE 的轴类型与索引等价、门条件、归约 dtype/溢出语义以及 LOAD/STORE 副作用。ctx 是一次图重写的临时状态,不能跨模型或并发编译复用。

    车载推理部署应将编译器版本、模型工件、renderer/设备能力、缓冲布局及测试结果锁定在同一版本。关键回归包括非整除/动态轴长度、无门索引、归约嵌套、边界像素、整数溢出、不同后端浮点归约误差和最坏执行时延。源文档明确:编译通过不证明相机输入、模型输出或整车控制安全,执行许可仍由车型适配和 panda 固件安全门约束。

    验证与限制

    上游四项定向用例已通过:TestReduceCollapse.test_multi_range_reduce_add、test_reduce_shapeless_const_unroll、TestSchedule.test_arange_sum、test_arange_sum_alt(PYTHONPATH=. DEV=PYTHON)。test_simplify_valid_idx.py 的范围收缩用例因环境缺少 z3 而未运行。源文档明确未进行完整后端编译、LFS 模型数值、目标 GPU、实车或 CAN 验证,source/ 未修改。

    常见问题

    这层化简是可在线替换的控制插件吗?

    不是。源文档把它定义为编译器内核优化插件,不是可在线替换的控制插件:插件输入应为已通过类型、范围及缓冲 ABI 校验的 UOp 图,执行许可仍由车型适配和 panda 固件安全门约束。

    RANGE % 常量为什么不是总能拆分?

    因为它要求静态证明。mark_range_mod() 要求范围长度是常量,且 UOp.divides(c.val) 能证明被模数整除;UOp.divides() 只对常量、STACK、ADD、MUL 和带 multiple_of 约束的 PARAM/BUFFER 做有限证明,无法证明即返回 None,动态长度也不会触发拆分。

    为什么不把加载索引的求和都做闭式化简?

    因为需要保留边界条件。源文档指出 pm_reduce_load_collapse 只在替代 RANGE 前确认索引在 [0, extent) 并保留有效性条件,且最后一条规则刻意避免对加载索引做可能溢出的加法;不能把这一优化等同于任意 gather 的安全实现。

    编译器优化通过能否说明推理安全?

    不能。源文档明确编译通过不证明相机输入、模型输出或整车控制安全;车载部署应把编译器版本、模型工件、后端能力与测试结果锁定在同一版本,并覆盖溢出、动态轴长度与最坏执行时延等回归。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_RANGE_REDUCE_SIMPLIFY_PLUGIN_CONTRACT_DEEP_DIVE.md(取用了编译调用链分界、范围规则表、四个正确性门、插件化输入输出约束与定向用例结果)。

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

    更新记录

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

  • modeld 构建:6 通道 NV12 warp 与 256 字节对齐的带外权重

    结论:modeld 的构建产物链把三件事固定在一起:ONNX 模型编译(compile_onnx.py,--out-of-band)、相机布局相关的 warp 编译(道路 YUV420 双帧、座舱 luma 单帧)与运行期输入打包。道路 warp 把 NV12 拆成 Y 四相位加两个色度面,拼成模型的 6 通道张量;座舱 warp 只取 Y 平面并用 16 填充边框。要记住两个边界:构建脚本的种子 42/43 只是自洽检查,没有与独立 ONNX Runtime 或实车真值比较;工件是可执行对象序列化而非纯数据,不能把未认证的工件交给 pickle.load()。道路与座舱两条路径的缓冲所有权与过期帧风险不同,不能用道路双帧上传的验证代替座舱零拷贝验证。

    构建入口:三条后端路径与产物矩阵

    SConscript 在非 comma ARM64、非 Darwin 平台缺 libLLVM 时跳过模型编译。comma ARM64 用 DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1,Darwin 用 DEV=METAL JIT=2,其他平台用 DEV=CPU:LLVM。Darwin 分支的注释明确说明 JIT=2 是为了禁用在 buffer 变化后可能产生错误输出的 graph batching。这些标志写入构建动作和依赖,不能在不同后端之间只重命名 .pkl 产物。

    产物 构建调用 运行期消费者
    道路模型 driving_tinygrad.pkl 与座舱模型 dmonitoring_model_tinygrad.pkl compile_model() 调 compile_onnx.py,使用 --out-of-band --benchmark-runs 1。 modeld.ModelState.__init__() 与 dmonitoringmodeld.ModelState.__init__() 经 load_oob() 载入。
    每种相机尺寸的道路 warp SConscript 调 compile_warp.py --layout yuv420 --frames 2,frame size 按 stride 与 Y/UV 分配高度求得。 modeld 以 img/big_img 两帧和两个逆变换调用捕获图。
    每种相机尺寸的座舱 warp SConscript 调 --layout luma --border-fill 16 --transform-device NPY。 dmonitoringmodeld.ModelState.run() 用 VisionIPC 缓冲指针和主机变换矩阵执行。

    这里有一个显式的产物缺口:该 SConscript 没有定义 Chestnut 的 big_driving_tinygrad.pkl 或 big_driving_warp_* 构建动作,chestnut_compiled() 只检查这些文件是否存在。它们的来源和与当前模型的兼容性,不能由普通模型构建链推定。

    ONNX 捕获与序列化:自洽检查不等于数值正确

    onnx_metadata() 只扫描 ONNX 顶层 metadata 和 output 声明,不读取权重节点,计算由 OnnxRunner 负责。主程序从 graph input 推出 input_specs,先用随机填充输入运行一次得到 output_specs,再把 metadata、input_shapes、output_shapes、捕获的 run 与规格一起写入工件。运行时道路模型还从 metadata 的 output_slices 取切片,因此模型元数据和解析器版本属于同一接口契约。

    run() 以 TinyJit(prune=True) 捕获,对传入的 output_buffers 执行赋值并物化。构建脚本用种子 42 的固定随机输入作捕获、两次重复一致性和反序列化后一致性检查,并要求种子 43 的不同输入使输出发生变化,以避免完全忽略输入。但这些都只是自洽检查:没有与独立 ONNX Runtime、车辆标注或实车真值比较,--benchmark-runs 1 也不构成尾延迟统计。

    序列化层面,dump_pickle() 对默认设备上的 Buffer 使用 persistent_id,把内容按 256 字节对齐存到带外区域;load_pickle() 建立目标设备 arena,并按 32 MiB 逻辑块复制后恢复视图。这里的 pickle 是可执行对象序列化而非纯数据格式,自制插件不能把未认证的工件交给 pickle.load()。--retargetable 可去掉 CPU 程序中的二进制以便重新编译,但常规模型动作未传此参数。

    NV12 warp 的几何:逆 3×3、最近邻与越界填充

    warp_perspective_tinygrad() 对目标像素用逆 3×3 矩阵求源坐标,以 Tensor.round 做最近邻采样,先把索引裁到图像边界;若传 border_fill_val,再依取整后的坐标是否越界决定填充。它没有在本函数中显式处理齐次分母 src_w=0、NaN 或非法矩阵,上游标定/变换有效性必须单独校验。普通道路 warp 不给 border_fill,因此越界像素采用夹边采样;座舱 warp 明确用 16 填充。

    make_frame_prepare() 用 frame stride 与 Y 分配高度确定 UV 偏移,把 NV12 交错 UV 拆成 U/V,按缩放后的逆矩阵各自透视采样,再拼成 YUV420 面;frames_to_tensor() 把四个 Y 相位面和两个色度面排列成模型的 6 通道张量。它依赖偶数模型尺寸、相机 stride 与 Y/UV 分配高度同 get_nv12_info() 一致。道路运行时 ModelState.pack_inputs() 把两个帧的 Y/UV 有效分配区和浮点辅助输入放在一个 host upload 中,而不是复制包含尾部 padding 的整个 VisionIPC 缓冲;因此更换相机、NV12 pitch 或 buffer 长度须连同构建 warp、输入切片和运行时复制大小一起更新。

    两条运行路径的缓冲所有权不同

    make_luma_warp() 只取 NV12 的 Y 平面;座舱运行时在 ModelState.run() 中缓存 Tensor.from_blob(),让它指向 VisionIPC 帧内存,不复制完整帧。这与道路双帧的 host upload 路径在所有权和过期帧风险上都不一样,见下表。

    维度 道路模型(双帧 YUV420) 座舱模型(单帧 luma)
    输入布局 四个 Y 相位面 + 两个色度面,共 6 通道 仅 Y 平面,边框用 16 填充
    越界处理 不给 border_fill,夹边采样 明确 --border-fill 16
    上传方式 把两帧 Y/UV 有效分配区与浮点辅助输入合并为一次 host upload 缓存 Tensor.from_blob() 指向 VisionIPC 帧内存,零拷贝
    主要风险 复制大小与 NV12 pitch/stride 强耦合 帧内存所有权与过期帧

    移植边界

    源文档给出的做法是:目标硬件应固定 ONNX、元数据、相机尺寸/stride、逆变换、tinygrad 提交和设备编译器版本,先建立独立 CPU 数值基准;再比较道路 YUV420 六通道、座舱 luma 填充、模型输入/输出形状、跨帧状态与不同后端输出,最后测帧级尾延迟及断流/非法矩阵回退。核心原则是一句话:算法产物可插件化,但工件加载信任、结果有效性和车辆安全控制许可不能交由插件自证。本轮仅核对构建命令、源码和运行期消费,没有执行 ONNX/warp 构建,也未取得 LFS 模型实体。

    常见问题

    构建脚本里的数值一致性检查能证明模型正确吗?

    不能。种子 42 的捕获与一致性检查、种子 43 的输入敏感性检查都只是自洽检查,源文档明确它们没有与独立 ONNX Runtime、车辆标注或实车真值比较,--benchmark-runs 1 也不构成尾延迟统计。

    道路 warp 和座舱 warp 的关键差别是什么?

    道路 warp 用 --layout yuv420 --frames 2,把 NV12 拆成四个 Y 相位面加两个色度面凑成 6 通道,越界走夹边采样;座舱 warp 用 --layout luma --border-fill 16,只取 Y 平面并用 16 填充边框。两者的缓冲所有权与过期帧风险不同。

    warp 函数会处理非法变换矩阵吗?

    不会。warp_perspective_tinygrad() 用逆 3×3 矩阵求源坐标、Tensor.round 最近邻采样并裁到边界,但源文档明确指出它没有在本函数中显式处理齐次分母 src_w=0、NaN 或非法矩阵,上游标定/变换有效性必须单独校验。

    模型工件可以直接 pickle 加载吗?

    不可以随意加载。源文档说明这里的 pickle 是可执行对象序列化而非纯数据格式,自制插件不能把未认证的工件交给 pickle.load()。权重经 dump_pickle() 按 256 字节对齐存到带外区域,由 load_pickle() 建立 arena 后按 32 MiB 逻辑块复制恢复。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/MODELD_BUILD_ARTIFACT_WARP_CONTRACT_DEEP_DIVE.md(取用了构建后端标志与产物矩阵、ONNX 捕获与序列化边界、NV12 warp 几何与两条运行路径的缓冲所有权)。

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

    更新记录

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

  • tinygrad GPU 内存:分配器、页表与映射所有权

    结论:tinygrad 的 GPU 内存层把职责切成三层——分配地址、建立映射、拥有资源——并把「映射建立」和「硬件可见」明确分开。通用函数只保证 PTE 文本与虚拟地址记账,真正的 TLB 可见性由设备专用钩子 on_range_mapped() 或外层队列承担。valloc() 还存在失败路径:物理页分配失败分支释放了物理页,却没有对应释放已取得的 VA,静态上存在失败后保留地址配额的条件性风险。

    三层职责

    基线是固定归档 memory.py 及其 PCI、AMD/NV/RDMA 调用方;本文解析计算后端的内存基础设施,不代表 openpilot 默认推理路径必经每种后端。仅静态阅读与小型无设备探针,未运行外部 GPU 页表测试,也未验证目标硬件或整模型。

    分配地址由 BumpAllocator.alloc()(固定范围内的线性/环形偏移)和 TLSFAllocator.alloc()(两级桶找空闲块、按请求对齐并拆块,free() 插回并合并相邻空闲块)负责。MemoryManager 持有全局 VA TLSF,以及每设备 boot、页表和普通物理内存池。建立映射由 VirtMapping(记录 VA、大小、物理段、地址空间和缓存属性)和 PageTableTraverseContext.next() 完成;map_range() 先检查目标 PTE 未占用,再写 PTE 并调用设备钩子。拥有资源则收敛到 valloc()/vfree()。

    在这三层之上,PCIIfaceBase.alloc() 用同一套机制分配 VRAM 或映射锁页系统内存,PCIIfaceBase.map() 则把本机 host 或同一 peer group 的 GPU 缓冲映射到设备 VA。也就是说,插件看到的「一块可读写内存」背后同时牵扯虚拟地址池、物理页池和页表遍历三种资源,任一层记账不一致,都可能表现为越界或数据不一致,而不是立刻报错。

    重要函数与行为契约

    函数 输入/输出 需要核对的契约
    BumpAllocator.alloc() size, alignment 到地址 wrap=True 时满池重置指针,调用方需保证旧引用已失效;wrap=False 才在空间不足时报错。
    TLSFAllocator.alloc() 请求尺寸/显式 align 到地址 block_size 是最小分配量,不是默认返回地址对齐值;默认 align=1。
    map_range() VA、物理段、地址空间/缓存位到 VirtMapping size == sum(segment sizes),拒绝已占用 PTE;实际 PTE 格式由设备 pt_t 实现。
    unmap_range() VA/长度到清除 PTE 对未映射 PTE 断言;是否需额外 TLB shootdown 取决于设备钩子/调用方,不能只凭通用函数认定完成。
    valloc()/vfree() 分配/释放 VRAM 及其映射 GMMU=0 改为 identity VA;普通路径分配分段物理页并建页表。

    此外,PageTableTraverseContext.level_down()/level_up() 按 create_pts/free_pts/inspect 模式创建、释放或只读遍历,释放模式要求映射存在;alloc_vaddr() 的请求使用 max(最高置位 2 的幂, align) 对齐,不同设备共享类级 VA 池。

    页表遍历按页覆盖范围、对齐和设备支持的大页向下建表或向上归并;TLSF 侧的 _insert_block()、_split_block()、_merge_block() 依赖 blocks 中保存的前后邻接与空闲位,所以调用方必须成对管理 alloc/free,否则空闲桶与真实占用会失真。

    可复现的边界与测试状态

    边界 源码可见事实
    Bump 超大请求 wrap=True 时只在第一次比较中决定回绕,没有再检验 size <= capacity。无设备探针 BumpAllocator(16).alloc(32) 返回地址 0,内部 ptr=32 > size=16;属通用分配器边界,不等于默认车端已触发。
    TLSF 对齐语义 探针 TLSFAllocator(128, block_size=16) 连续 alloc(20), alloc(35) 返回 0,20;显式 align=16 则返回 0,32。
    VA 失败回滚 valloc() 取得 VA 后,物理页分配失败分支会释放已分配的物理页,但没有对应 va_allocator.free(va);map_range() 抛错时也未在 valloc() 层回收。
    映射与缓存 map_range() 对 PTE 重叠做预检查,但硬件 TLB 可见性由设备专用 on_range_mapped() 或外层队列承担。
    测试状态 external_test_tlsf.py 把 block_size 当默认对齐、使用旧的 start_addr 构造/属性;本轮运行 8 项,5 通过、1 失败、2 错误。不能把这 3 项直接归因于运行时内存损坏。

    AddrSpace.PHYS/SYS/PEER、uncached/snooped 不是可随意互换的插件参数;错误声明可能导致一致性故障,不能用 Python 层写读一致代替 GPU 校验。未运行的硬件测试方面,external_test_am.py 涉及 AMD 页表/设备,external_fuzz_tlsf.py 默认分配 1 GiB 做长时间 fuzz,本轮均未执行。

    对自制硬件及插件的约束

    计算设备插件应声明可用 VA 区间、页大小/大页能力、物理内存区域、对齐、缓存与 snoop 位、host/P2P 映射能力,以及 map → submit → synchronize → unmap → free 的所有权顺序。验证路径优先用无设备分配器不变量测试和 mock 页表故障注入;再在目标板验证映射重叠拒绝、OOM 回滚、TLB 可见性、跨设备 P2P 与反复热重启。内存插件只能影响计算数据通路,不得自行取得车辆 CAN 发送或 panda 安全授权。

    验收顺序也应与所有权顺序一致:先声明资源边界,再跑无设备不变量测试,最后才上目标板做映射重叠拒绝、OOM 回滚、TLB 可见性、跨设备 P2P 与反复热重启。任何一次「分配成功」都不能被解读为对车辆总线或安全策略的授权,因为内存层与车辆控制本就是两个相互独立的边界。

    对自制硬件而言,更稳妥的做法是把内存层当作可隔离、可回滚的计算资源服务,而不是与车辆控制共享状态的可信组件。

    常见问题

    tinygrad 的 map_range() 成功就等于映射对 GPU 可见了吗?

    不等于。map_range() 只做「检查 PTE 未占用、写 PTE、调用设备钩子」这一层,硬件 TLB 可见性由设备专用 on_range_mapped() 或外层队列承担;unmap_range() 是否需额外 TLB shootdown 也取决于具体设备钩子/调用方,不能只凭通用函数认定完成。

    valloc() 失败时地址配额会被回收吗?

    按源码存在条件性风险。valloc() 取得 VA 后,物理页分配失败分支会释放已分配的物理页,但没有对应 va_allocator.free(va);map_range() 抛错时也未在 valloc() 层回收 VA/物理页。静态上存在失败后保留地址配额的可能,需用故障注入验证具体设备恢复行为。

    BumpAllocator 的 wrap=True 有什么坑?

    wrap=True 时满池会重置指针,调用方需保证旧引用已失效;且源码只在第一次比较中决定回绕,没有再检验 size 是否小于等于 capacity。无设备探针 BumpAllocator(16).alloc(32) 返回地址 0、内部 ptr=32 大于 size=16。对永久 VA 或环形命令缓冲,应在调用侧/分配器层拒绝超大请求。

    为什么不能把 uncached/snooped 当普通插件参数?

    因为 AddrSpace.PHYS/SYS/PEER、uncached/snooped 决定映射的缓存与一致性属性,源码指出它们不是可随意互换的插件参数,错误声明可能导致一致性故障;也不能用 Python 层的写读一致代替 GPU 校验。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_GPU_MEMORY_ALLOCATOR_PAGETABLE_OWNERSHIP_DEEP_DIVE.md(取用了三层职责、函数契约表、Bump/TLSF/VA 回滚边界与自制硬件约束)。
    • 项目 02 —— openpilot-research/TINYGRAD_AMD_DEVICE_IP_BOOT_PARTIAL_RESET_LIFECYCLE_DEEP_DIVE.md(仅用于确认设备启动/复位专题的接续关系,未取用其未公开内容)。

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

    更新记录

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

  • tinygrad AMD 执行端:PM4、AQL 与 SDMA 三条队列

    结论:tinygrad 的 AMD 后端把「程序镜像」与「队列提交」分成两层:amd_build_program() 编译并缓存镜像,AMDComputeQueue/AMDComputeAQLQueue 才负责编码计算命令,AMDSDMAQueue 负责复制。三条队列路径(PM4、AQL、SDMA)语义不同,且是否具备复制队列是运行时探测结果,不是设备名承诺。源文档强调,exec() 只是编码 GPU 命令,不等于主机已同步拿到输出;USB 桥、时间线等待、GPU 完成与模型输出有效性应分别测量。本文为源码静态分析,缺少实板与大模型实体。

    设备资格与队列创建

    AMDDevice.__init__() 选择 KFD、PCI、USB 等接口,读取目标 gfx 属性,仅允许源码列出的 GFX9 子集以及 GFX11/12;根据接口确定恢复能力、运行时缓冲区大小、ISA 寄存器表、SDMA 版本与最大复制分段长度。USB 接口安装专用批处理函数,但计算命令仍由 AMDComputeQueue 或 AMDComputeAQLQueue 编码。源文档明确:它不是一个只需实现 Python Tensor 的抽象设备。

    AMDDevice.compute_queue 懒创建计算 ring,USB 分支大小为 1 MiB,其他接口为 16 MiB;AQL 由 AMD_AQL 或多 XCC 默认条件决定。sdma_queue() 受 AMD_DISABLE_SDMA 控制,创建时的 OSError 被抑制并返回 None,因此复制队列是否可用是运行时探测结果。无复制队列时,通用 HCQ 会采取不同的复制降级路径,不能仍按 SDMA 延迟估算。

    程序镜像与参数 ABI

    amd_build_program() 用程序二进制和设备集合做缓存键。首次使用通过内部函数解析 ELF、处理 R_AMDGPU_REL64 重定位、读取 kernel descriptor,并对固定 LDS 资源做上限检查;其它重定位类型触发断言。其结果包含代码入口、寄存器配置、wave32 标志、私有/共享段与 kernargs 大小。

    镜像字段 含义
    代码入口 kernel descriptor 解析得到的执行入口。
    寄存器配置 程序执行所需的资源与用户寄存器设置。
    wave32 标志 波前宽度标志,随架构与程序而异。
    私有/共享段与 kernargs 大小 scratch 与参数段的尺寸依据。

    AMDComputeQueue.kernargs() 按 prg.arg.globals 和符号变量打包地址/数值,依 kernel descriptor 的 kernargs_segment_size 布局;需要 dispatch pointer 时追加 dispatch packet。exec() 取得镜像、scratch 和参数表地址,发缓存 acquire、设置程序入口/资源寄存器/私有栈与用户寄存器,最后发 PACKET3_DISPATCH_DIRECT 并作部分 flush。源文档提醒,缓存是内存中的镜像复用,不证明新 GPU 的 ISA、固件或驱动接受该镜像。

    PM4、AQL、SDMA 三条路径

    队列 核心函数 具体语义
    PM4 计算 AMDComputeQueue.submit()、push() 将命令缓冲地址封成 indirect-buffer 包;push() 处理 ring 末尾回绕,写 write pointer 后敲 doorbell。
    AQL 计算 AMDComputeAQLQueue.exec()、submit() 生成 64 字节 kernel dispatch packet;夹杂的 PM4 命令封为 vendor-specific indirect buffer,提交时以 64 字节为 ring 单位。
    SDMA 复制 AMDSDMAQueue.copy()、submit() 复制按 max_copy_size 切段;提交缓冲需在 ring 内连续,若尾部空间不足则清零尾部并从头写入。

    AMDComputeQueue.memory_barrier() 先等待 HDP flush 寄存器,再发 ACQUIRE_MEM;wait()、signal() 把时间线转换为 GPU 轮询和末端写回/中断。SDMA 有自己的 wait()、signal() 包格式。源文档指出,通用 HCQ 的跨队列依赖只有这些硬件指令和共享内存可见性都成立才可靠;USB 桥的 fence/哨兵握手是其外侧的另一层协议。

    Scratch 与资源时序

    scratch_buffer() 根据私有段大小、wave 和 CU 数量计算设备 scratch;需求变大时分配新的非 LRU 缓冲,并在 AQL 情况下更新队列描述符。源码还维护类级 max_scratch_psize,源文档提醒:一个设备触发更高 scratch 需求可能影响同进程后续设备的最小值。因此初次大内核与模型切换期间的分配和同步要单独量测,不可只用稳态推理时间估算内存峰值。

    modeld 对 Chestnut 设定的 3000 ms wait timeout 也不是每个内部队列轮询点的统一墙钟期限;内核提交、时间线等待、USB 桥 fence、GPU 完成和模型输出有效性应分别测量与故障注入。

    证据与待验证

    仓库有队列测试和 mock GPU 基础设施;源文档明确本文没有运行 AMD/USB 队列测试,也没有目标 GPU。验证应先固定 gfx 架构、内核二进制和队列模式,覆盖 PM4/AQL/SDMA 可用与不可用分支,再以已知输入检查复制、scratch 扩容、时间线回执及输出数值,最后测 USB 断连与 GPU 挂起。

    软件计算后端没有车辆 CAN 发送授权,仍必须让 card/panda 的独立安全约束保持生效。不同 KFD、直连 PCIe、USB 接口共用部分代码,但并不等价,不能把一条接口的验证结论外推到另一条。

    常见问题

    有 AMD 设备就一定有 SDMA 复制队列吗?

    不一定。sdma_queue() 受 AMD_DISABLE_SDMA 控制,创建时的 OSError 被抑制并返回 None,源文档明确复制队列是否可用是运行时探测结果,不是设备名承诺;无复制队列时通用 HCQ 会走不同的复制降级路径。

    exec() 执行完成是否代表主机已拿到输出?

    不代表。源文档指出 exec() 是编码 GPU 命令,设置程序入口与资源寄存器后发 dispatch packet 并作部分 flush;时间线等待、USB 桥 fence 与 GPU 完成是另外的阶段,应分别测量。

    编译缓存命中是否说明新 GPU 能接受该镜像?

    不能说明。amd_build_program() 的缓存是内存中的编译后镜像/占位复用,源文档明确它不证明新 GPU 的 ISA、固件或驱动接受该镜像,相同 ELF 在另一设备集合上仍需自己的缓冲映射。

    模型切换或大内核时内存峰值能按稳态推理估算吗?

    不能。scratch_buffer() 在需求变大时分配新的非 LRU 缓冲,类级 max_scratch_psize 还可能被同进程后续设备抬高;源文档要求单独量测初次大内核与模型切换期间的分配和同步。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_AMD_QUEUE_PROGRAM_SUBMISSION_DEEP_DIVE.md(取用了设备资格与队列创建、程序镜像与参数 ABI、PM4/AQL/SDMA 三路径表、scratch 与资源时序)。

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

    更新记录

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

  • tinygrad 数据集与 Torch 前端:训练入口不该进车端

    结论:tinygrad 的 nn/datasets.py 与 nn/torch.py 都是开发/训练侧入口,不是 openpilot 车端实时模型加载主链。前者下载并解包第三方数据、按固定字节偏移切片;后者在导入时编译并载入 C++ 扩展、注册 PrivateUse1 后端,并修改全局进程状态。结论是明确的:数据集下载、PyTorch 兼容层与离线训练必须留在构建/研发域,网络、文件系统和原生扩展编译都不得通过运行期插件装载流入车辆控制进程。

    数据集组件与 I/O

    两条数据入口都返回原始字节张量,且都未传入固定哈希。mnist() 按 fashion 选择 MNIST 或 Fashion-MNIST 基址,分别读取训练/测试图像和标签四个 gzip IDX 文件;图像跳过 16 字节头并 reshape 为 (-1,1,28,28),标签跳过 8 字节头,返回原始字节值张量,device 只用于结果转移。cifar() 下载 CIFAR-10 二进制 tar.gz,每条 3073 字节记录的第 0 字节为类别,其余 reshape 为 (N,3,32,32)。

    入口 实际行为与限制
    mnist() 读四个 gzip IDX 文件,跳过 16/8 字节头并 reshape;未在此归一化、核验 IDX magic/声明样本数或固定哈希。
    cifar() 拼接五个训练 batch、取一个测试 batch;未对五批样本数、标签范围或归档哈希做独立校验。
    Tensor.from_url()、fetch() fetch 默认按 URL/请求头生成下载缓存路径,gzip 边读边解压,临时文件完成后重命名;只有调用方传入 sha256 才验证内容。两种 nn.datasets 入口均未传入固定哈希。
    tar_extract() 经 tarfile 读取归档目录,按 offset_data/size 返回常规文件 Tensor 切片;不是通用安全解包器,成员名和内容应在训练数据入口做白名单、大小及哈希校验。

    需要特别区分的是,extra.datasets.fetch_mnist() 是另一条训练示例路径:用 NumPy/gzip 解码后输出 float32 扁平图像和 int8 标签,不能与 nn.datasets.mnist() 的原始字节 NCHW 输出混为一谈。数据输入插件须声明像素范围、通道布局、标签 dtype 和样本数量,不能仅声明「MNIST/CIFAR」。

    下载侧还有一处顺序细节:from_url 用 fetch 得到本地文件后构造 Tensor,fetch 对 gzip 下载边读边解压、临时文件完成后重命名,并默认按 URL/请求头生成缓存路径。这种「先缓存后校验」的顺序意味着,即使同一 URL 已被下载过,只要调用方没传哈希,缓存命中就不能作为来源未被篡改的证据。

    PyTorch 前端组件与副作用

    nn/torch.py 在导入时把 tinygrad 根目录加到 sys.path 并导入 extra.torch_backend.backend;若发行包无 extra 前端则抛 ImportError——它不是纯类型别名或无需依赖的适配器。backend.py 导入段随后通过 torch.utils.cpp_extension.load 编译并载入 C++ 扩展,注册 tiny 的 PrivateUse1 后端,同时关闭 PyTorch autograd 多线程。这会触发构建、动态载入和全局进程状态修改,不适合在车端硬件插件发现阶段自动导入。

    入口 实际行为与限制
    nn/torch.py 导入时改 sys.path 并导入 extra 前端;无 extra 则 ImportError。
    backend.py 导入段、wrapped_tensor.cpp 编译并载入 C++ 扩展,注册 PrivateUse1,关闭 autograd 多线程;触发构建与全局状态修改。
    wrap()、unwrap()、wrap_tensor() Python Tensor 存入 C++ OpaqueTensorImpl;源码自标 calculate_storage_offset 对 PERMUTE/EXPAND/PAD/FLIP 假设不完整,不能把一般视图元数据视为已证明正确。
    _copy_between_devices()、_index_put_impl_() CPU 与 tiny 经 NumPy/copy_ 转换;部分索引、特征分解及上采样反传显式退回 CPU,不能由 device='tiny' 推断所有算子都留在目标加速器或满足实时延迟。
    CustomNoOpDeviceGuardImpl 设备切换、stream 同步及 event 记录/等待基本按 no-op 实现,deviceCount() 固定 1;不能用它作为车端多设备同步或完成回执。

    此外,tiny_backend 注册循环按 ATen schema 的写参数和 inplace-view 标签选择包装器,native_batch_norm() 有单独正反向实现以更新运行统计量,但算子表不是完整 PyTorch 兼容性保证。_apply_inplace()/wrap_view_op() 用弱引用追踪派生视图、对视图写入重放索引并刷新别名,涉及存储别名与 autograd 约束,并非普通零拷贝 wrapper。Python TinyBackend.device_count() 另读 GPUS,与 C++ 层的设备数量口径不一致。

    存储与视图元数据方面,wrap() 把 Python tinygrad Tensor 存入 C++ OpaqueTensorImpl,Torch 侧 shape/stride/offset 必须与底层张量保持一致;源码自己标明 calculate_storage_offset 对 PERMUTE/EXPAND/PAD/FLIP 的假设并不完整。因此一般视图的元数据不能直接当作已证明正确,涉及视图写入时还需要处理存储别名与原位更新约束。

    与 openpilot 和插件重构的关系

    compile_model() 调用 tinygrad ONNX 编译脚本,ModelState.run() 运行已捕获的模型。对 openpilot 与 examples 目录的直接调用搜索,未找到 nn.datasets 或 tinygrad.nn.torch 入口——这只能界定当前可见源码,不代表所有外部开发脚本。

    面向自制车载硬件,建议把数据集下载、PyTorch 兼容层和离线训练放在构建/研发插件域:网络、文件系统、原生扩展编译均不得通过运行期插件装载流入车辆控制进程。车端推理插件只消费签名/哈希/版本绑定的模型工件与明确的张量 ABI;相机输入需约定像素范围、色彩格式、行步长、时间戳和缓冲所有权。PyTorch 的 tiny 设备是研发兼容性工具,不可代替目标后端的执行完成、时限或安全认证。

    常见问题

    tinygrad 的数据集和 PyTorch 前端能在车端直接用吗?

    不应直接使用。两条路径都是开发/训练侧入口:nn/datasets.py 会联网下载并解包第三方归档,nn/torch.py 在导入时就编译并载入 C++ 扩展、修改全局进程状态。源码建议把它们放在构建/研发插件域,网络、文件系统与原生扩展编译不得通过运行期插件装载流入车辆控制进程。

    为什么说下载缓存命中不代表数据可信?

    因为 fetch() 默认只按 URL/请求头生成下载缓存路径、边读边解压、临时文件完成后重命名,只有调用方传入 sha256 才验证内容;而 mnist() 与 cifar() 两个入口都没有传入固定哈希。因此缓存命中不能作为来源或版本已被独立证明的依据。

    用 device=’tiny’ 是否等于所有算子都跑在加速器上?

    不等于。_copy_between_devices() 和 _index_put_impl_() 显示 CPU 与 tiny 之间经 NumPy/copy_ 转换,部分索引、特征分解及上采样反传显式退回 CPU;源码明确不能由 device='tiny' 推断所有算子都留在目标加速器或满足实时延迟。

    PyTorch 的 tiny 设备能当车端多设备同步用吗?

    不能。CustomNoOpDeviceGuardImpl 把设备切换、stream 同步及 event 记录/等待基本按 no-op 实现,deviceCount() 固定 1;Python TinyBackend.device_count() 又另读 GPUS,两层口径不一致。它不能作为车端多设备同步或完成回执。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_DATASET_TORCH_FRONTEND_INGRESS_BOUNDARY_DEEP_DIVE.md(取用了数据集 I/O 与哈希缺口、PyTorch 前端副作用、与 openpilot 的边界关系与验证范围)。
    • 项目 02 —— openpilot-research/TINYGRAD_TORCH_PRIVATEUSE1_ATEN_ALIAS_DEVICE_DEEP_DIVE.md(仅用于确认 PrivateUse1/ATen 别名专题的接续关系,未取用其未公开内容)。

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

    更新记录

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