标签: openpilot

  • 编码配置表:录制、音频与直播的进程快照分层

    结论:loggerd 的相机/编码表是进程静态对象,不是运行时可重建的配置:encoderd 与 loggerd 各自加载同一头文件并构造本进程的静态表,运行中改参数不会重建配置表。RecordFront=false 在这条路径只代表「不写本地媒体文件」,座舱流仍会发布 cabinEncodeData 并可能写派生索引;RecordAudio 至少参与两个独立时刻的读取,二者不在同一事务。直播是独立的 stream_encoderd 进程与主题,录制与直播的保留/远程访问策略必须独立设计。

    静态描述表与进程边界

    EncoderSettings 给出编码参数:主流按输入宽度选 5/10 Mbps、GOP 20/30;qcamera 为 H.264、256 kbps、GOP 15;直播默认 H.264、5 Mbps、GOP 5,默认 B 帧数为 0。EncoderInfo/LogCameraInfo 由 topic、文件名、录制/音频/直播标志、输出尺寸、FPS 和 Cap’n Proto union 读写函数指针组成。

    编码目标 编码与码率 GOP 默认 B 帧
    主流(按输入宽度) 5/10 Mbps 20/30 0
    qcamera H.264、256 kbps 15 0
    直播默认 H.264、5 Mbps 5 0

    cameras_logged 含窄路面主流与 qcamera 两个编码器(共享同一路 VisionIPC 帧),广角与座舱各一个;stream_cameras_logged 是三路独立直播主题,record=false,没有媒体文件名也不配置缩略图。encoderd main() 无参数使用录制表,--stream 使用直播表,两种是同一可执行文件的不同进程模式。

    录制策略不是编码生产开关

    RecordFront 在座舱编码信息的静态初始化期间读一次,用作 record 字段。encoderd 的编码线程没用该字段过滤编码器:座舱流一旦被发现,仍会发布 cabinEncodeData。loggerd 也仍订阅非直播编码主题;record=false 时不创建 dcamera.hevc writer,但通过关键帧门后仍写 cabinEncodeIdx。

    因此源文档的结论是:「关闭座舱录制」在这条路径代表不写本地媒体文件,不是停止座舱图像采集/编码,也不是证明下游没有索引。直播座舱流使用独立 topic 和进程,不能由 RecordFront 的本地文件策略自动推导其权限。

    RecordAudio 的两个读取时刻

    RecordAudio 至少参与两个独立时刻:qcamera 编码信息的 include_audio 在各进程静态初始化时读取;loggerd_thread() 建订阅时再次读取,只有当时为真才订阅原始音频。micd 的 callback() 本身持续发布 rawAudioData,并不以 RecordAudio 阻止采集。

    在正常一致的启动快照下,loggerd 收音频后把它同时作为 rlog 普通事件和 qcamera 音轨输入。但两次 Params 读取不在同一事务,运行中切换也不会刷新已有订阅或配置表:若启动窗口内值变化,静态 include_audio 与实际订阅可能不一致。源文档指出,例如等待音频的 qcamera writer 未得到音频初始化包,会走有界包数的等待/丢包路径,是否在常规 manager 时序下可达需故障注入验证。

    平台格式、直播与时间边界

    项 静态行为
    MAIN_ENCODE_TYPE 依 Hardware::PC() 选择 PC 的 BIG_BOX_LOSSLESS 或设备的 FULL_H_E_V_C;固定 fcamera.hevc 文件名不能单独证明实际编码为 HEVC。
    直播输出尺寸 livestream_width()/height() 对 TIZI 为 1152×720、MICI 为 1280×720,其他设备返回 -1;VideoEncoder 构造器对非正输出尺寸退回输入尺寸。
    STREAM_BITRATE 用 atoi 读取且无配置层范围校验;运行时 LivestreamEncoderBitrate 由上层逐帧读 Params,PC 后端不支持动态调整。
    SEGMENT_LENGTH 通常固定 60 秒;LOGGERD_TEST 存在时改用 atoi(LOGGERD_SEGMENT_LENGTH),是测试配置入口,不应作为生产插件 API。

    直播三个 EncoderInfo 都没有文件名,且 loggerd 订阅时明确排除以 livestream 开头的 EncodeData,WebRTC 另行消费这些主题。录制与直播可以同用相机源,但数据保留/远程访问策略必须独立设计。两个进程各自加载同一头文件并构造本进程的静态对象,因此不能把它们的 Params 快照视为原子一致。

    验证与重构要求

    已有测试检查多路段媒体与索引、座舱文件有无、qcamera 音轨和 rlog 原始音频;源文档指出它们未直接断言 RecordFront 运行中变化时生产端是否继续编码、跨进程 RecordAudio 快照竞争、直播/录制权限隔离或静态表重建。本轮未运行这些测试。

    插件宿主应将「采集许可、编码生产、本地存储、音频复用、上传、远程直播」拆为不同能力,并在一个版本化配置事务中确认生效时刻。隐私开关变更需要可观察的撤销/排空及下游回执,而不是只写 Params;录制与直播要按 topic、相机和车辆状态独立授权。台架验收应覆盖启动竞态、开关热变、进程重启不一致、qcamera 等音频、座舱关闭录制但仍发布 EncodeData、直播单独启停及文件/索引残留。

    常见问题

    关闭座舱录制(RecordFront=false)会停止座舱图像采集吗?

    不会。源文档明确 encoderd 的编码线程没有用该字段过滤编码器,座舱流一旦被发现仍会发布 cabinEncodeData;record=false 只是不创建 dcamera.hevc writer,但通过关键帧门后仍写 cabinEncodeIdx。

    RecordAudio 关掉后麦克风就停止采集了吗?

    不会。源文档指出 micd 的 callback() 本身持续发布 rawAudioData,并不以 RecordAudio 阻止采集;RecordAudio 只影响 qcamera 的 include_audio 与 loggerd 是否订阅原始音频。

    固定文件名 fcamera.hevc 能否说明实际编码是 HEVC?

    不能。MAIN_ENCODE_TYPE 依 Hardware::PC() 选择 PC 的 BIG_BOX_LOSSLESS 或设备的 FULL_H_E_V_C,源文档明确固定文件名不能单独证明实际编码为 HEVC。

    运行中修改录制/音频参数会立即生效吗?

    不会。源文档指出配置在进程静态初始化时读取,运行中切换不会刷新已有订阅或配置表;两次 RecordAudio 读取也不在同一事务,启动窗口内值变化可能造成 include_audio 与实际订阅不一致。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/ENCODER_CONFIG_RECORDING_LIVESTREAM_SNAPSHOT_DEEP_DIVE.md(取用了静态编码配置表、录制/音频/直播的进程快照边界、平台格式与时间边界、重构验证要求)。

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

    更新记录

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

  • Honda 车型接口:能力参数、雷达静默与恢复边界

    结论:Honda 车型接口把「能力参数」和「能力兑现」分得很清楚:openpilotLongitudinalControl、ControlsReady 与 tester-present 候选都只是软件侧的请求或标记,不是 ECU 行为的回执。雷达静默依赖 disable_ecu(),而它对通信控制请求传入空预期响应前缀、随后无条件打印「ecu disabled」并返回 True,因此至多证明会话得到匹配应答且后续查询没抛异常,不能证明 ECU 已停止发送。没有闭环证据时应保持 noOutput 或只读。

    关键函数与组件

    基线是本地 source/ 归档提交 c8fb906,只分析 Honda interface.py、通用诊断函数及 card/pandad 交接;不代表任何具体年款的线束、ECU 响应或原车 AEB 行为已验证。

    入口 职责
    CarInterfaceBase.get_params() 从平台规格建立基础 CarParams,再交给 Honda _get_params() 补齐能力与参数。
    CarInterface._get_params() 选择 Bosch/Nidec 安全模式、纵向策略、指纹能力、调校和安全参数。
    get_pid_accel_limits() Bosch 给定静态加速度范围;Nidec 的最大值靠近巡航设定速度时下降。
    init() / deinit() 有雷达 Bosch 自控纵向的 UDS 通信控制请求与恢复请求。
    disable_ecu() 扩展会话重试、通信控制发送及布尔结果;注释明确警告禁用原车 AEB。
    IsoTpParallelQuery.get_data() 按预期响应前缀收集结果;空前缀不会验证通信控制的具体正响应码。

    能力与安全配置

    get_params() 先装载车型平台的质量、轴距、转向比和静态 flags,再调用 Honda _get_params()。Bosch 设置 hondaBosch、radarUnavailable=True;alphaLongitudinalAvailable 对 Bosch CAN FD 为假,openpilotLongitudinalControl 还取决于启动时传入的 alpha_long。它们是软件能力/选择位,不等于雷达实际静默。Bosch CAN FD 且 PT 总线编号至少 4 时,安全配置列表前插 noOutput,但并非所有 CAN FD 都无输出。

    Nidec 选择 hondaNidec,同时设置 openpilotLongitudinalControl=True 和 pcmCruise=True,因此不能用 pcmCruise 单独推断执行权由原车 ACC 完全负责。指纹分支检测特定 CR-V BSM 标志、扩展 HUD、混动标志、自动/CVT/手动变速箱以及备用制动帧;EPS 固件版本含逗号时设 dashcamOnly。若 card 判定控制器不可用、用户关闭开关或 dashcamOnly,它会把整组安全配置替换为 noOutput 并标记 passive。

    能力位/标记 含义与边界
    openpilotLongitudinalControl 取决于启动时 alpha_long,是软件选择位,不等于雷达已静默。
    pcmCruise Nidec 同时置真,不能单独推断执行权归属。
    safetyParam 对备用制动、Nidec SCM 变体、Bosch 自控纵向、radarless 和 CAN FD 按位编码;固件还需依自身编译门与白名单解释这些位。
    ControlsReady card 在初始化门满足时立即写,不验证雷达静默结果。

    特别地,Bosch 自控纵向在固件里受 ALLOW_DEBUG 条件编译约束,Python 参数置位本身不足以证明固件允许该模式。此外,多数车型设置 PID 横向参数,部分车型走通用扭矩调校;三处分支直接改写 CarControllerParams.BOSCH_GAS_LOOKUP_V 类属性,不是写入本次 ret 或控制器实例,且函数内没有恢复默认值——同一进程先构建某一车型参数、再构建另一车型时,后者可能继承前者的查表值。

    禁用、保活与恢复链

    只有 BOSCH、非 BOSCH_RADARLESS 且 openpilotLongitudinalControl 时,CarInterface.init() 才调用 disable_ecu(),目标是固定的诊断目标地址与 PT 总线。它先尝试 UDS 扩展会话并要求匹配应答;随后发通信控制请求,子功能带 suppress-response 位。disable_ecu() 对后一步传入空的预期响应前缀并调用 get_data(0),然后无条件打印「ecu disabled」并返回 True。所以它至多证明前一步会话得到匹配应答且后一步查询没有抛出异常,不能证明 ECU 已停止发送;Honda init() 又没有返回该布尔值。

    card 首次满足初始化门时调用 CI.init(),紧接着写 ControlsReady=True,没有验证雷达静默结果。pandad 在 FirmwareQueryDone 和 ControlsReady 都为真后读取参数、设置安全模式;这两项参数并非 ECU 行为回执。保持发送候选、MCU 接受、ECU 收到以及 ECU 持续静默是四个不同事实。

    CarInterface.deinit() 用通信控制「启用 RX/TX」请求复用 init() 路径,仍不检查恢复应答。当前主 card 运行链只见 init() 调用,未见对 Honda deinit() 的调用;进程退出或异常时不能由这个函数的存在推断原车雷达/AEB 已恢复。恢复行为是否由 ECU 会话超时、断电或其他机制完成,必须在目标车型台架和受控场地验证。

    插件与硬件验收

    将「雷达静默」设计成受信任诊断服务持有的事务能力:记录目标 ECU、物理总线、请求、正/负响应、发送持续性、实际雷达报文消失与恢复证据,并把原车 AEB/FCW 影响作为显式安全决策。没有闭环证据时保持 noOutput 或只读,不能让车型插件以 openpilotLongitudinalControl、ControlsReady 或 tester-present 候选代替验收。多车型参数生成必须隔离/冻结 BOSCH_GAS_LOOKUP_V 等调校状态;至少测试 Civic Bosch、Odyssey MMR、RDX MMR 与普通 Bosch 的不同构建顺序。

    常见问题

    disable_ecu() 返回 True 就代表雷达被禁用了吗?

    不代表。disable_ecu() 对通信控制请求传入空的预期响应前缀并调用 get_data(0),然后无条件打印「ecu disabled」并返回 True。源码说明它至多证明前一步扩展会话得到匹配应答且后一步查询没有抛出异常,不能证明 ECU 已停止发送;Honda init() 也没有返回该布尔值。

    pcmCruise 为真表示减速完全由原车负责吗?

    不能这样推断。Nidec 选择 hondaNidec 时同时设置 openpilotLongitudinalControl=True 和 pcmCruise=True,源码因此明确指出不能用 pcmCruise 单独推断执行权由原车 ACC 完全负责。

    BOSCH_GAS_LOOKUP_V 为什么会有跨车型风险?

    因为部分调校分支直接改写 CarControllerParams.BOSCH_GAS_LOOKUP_V 类属性,不是写入本次 ret 或控制器实例,且函数内没有恢复默认值。源码可推断:同一 Python 进程先构建某一车型参数、再构建另一车型时,后者可能继承前者的查表值。这需用 Civic Bosch、Odyssey MMR、RDX MMR 等不同构建顺序验证。

    进程退出后原车雷达/AEB 会自动恢复吗?

    不能由源码推断。deinit() 用通信控制「启用 RX/TX」请求复用 init() 路径、仍不检查恢复应答,而当前主 card 运行链只见 init() 调用、未见 Honda deinit() 调用。恢复行为是否由 ECU 会话超时、断电或其他机制完成,必须在目标车型台架和受控场地验证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HONDA_INTERFACE_CAPABILITY_RADAR_LIFECYCLE_DEEP_DIVE.md(取用了 Honda 接口函数表、能力与安全配置、禁用/保活/恢复链、插件与硬件验收要求)。
    • 项目 02 —— openpilot-research/HONDA_CARSTATE_SPEED_BRAKE_CRUISE_INPUT_DEEP_DIVE.md(仅用于确认 Honda 输入状态专题的接续关系,未取用其未公开内容)。

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

    更新记录

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

  • 四个开发工具的信任边界:回放导出与 CarParams 注入

    结论:四个开发工具的信任等级并不相同:print_flags.py 只读枚举,test_fw_query_on_routes.py 只读已记录固件,run_process_on_route.py 会把新旧消息合并导出,而 set_car_params.py 会写入当前参数库。前两者可分析,后两者带副作用,其中最危险的是把 CarParams 写入下放给任意插件。

    工具清单与副作用等级

    工具 源码链路 静态结论
    test_fw_query_on_routes.py 参数可为单一路段或文件路径;文件分支取每行再交 SegmentRange,未指定段则拼 /0,随后按 QLOG 读 carParams 并分别做精确/模糊固件匹配。 原主入口探针确认列表行尾换行未剥离,正则在内部 try 前抛断言并中断整批;读取器构造失败也在 try 前传播。脚本只读已记录固件,不向 ECU 发查询。
    批量统计与不匹配输出 以 dongle_id 去重,在读路段前就把它加入集合;对不匹配固件按车型、地址去重汇总,最终打印精确/未识别/模糊计数。 假消息探针确认首个路段没有 carParams 时,同设备第二路段仍被跳过;不支持的 panda 类型会提前结束,随后又报告无 CarParams。输出含 VIN 与固件字节,需视为敏感诊断数据。
    run_process_on_route.py 默认从 CONFIGS 选全部进程,再减去被排除的两个模型进程;读完整路段调用回放,只按实际产生的消息类型删除旧输入,再将新旧消息按时间合并并保存。 探针确认零回放输出会保留并重排全部旧消息,未知白名单会选出空配置但仍读取和导出原日志。文件名进程部分来自集合迭代顺序,不是稳定标识。导出不调用车型日志净化器,副本可能携带原始 CAN/VIN。
    set_car_params.py 有路段时只读第一条 qlog 中第一条 carParams;无参数时新建空白消息,只写两个纵向标志;随后为每个键新建 Params(),按三个键顺序阻塞写入。 探针确认缺少 carParams 时在任何写入前抛 IndexError;第二键抛异常时第一键已调用成功、第三键未调用。原 Python Params.put() 会丢弃底层负错误码,故底层写盘失败可能不抛异常。本层无回滚或读回,不校验平台、VIN、safety 或当前车辆身份。
    print_flags.py 遍历 BRANDS 与各平台配置标志,按标志列出车型名称。 Platforms 与归档的 CAR 枚举均未定义脚本调用的 with_flags();存在非零标志配置,非零假枚举时原主体抛 AttributeError,零标志枚举则无输出。

    run_process_on_route:替换与导出的边界

    该脚本默认从 CONFIGS 选全部进程,再减去被排除的两个模型进程,读完整路段后调用 replay_process(),只按本轮实际产生的消息类型删除旧输入,再把新旧消息按 logMonoTime 合并并 save_log()。

    假依赖主入口探针确认:零回放输出会保留并重排全部旧消息;未知白名单会选出空配置但仍读取和导出原日志。另外,文件名中进程部分来自集合迭代顺序,不是稳定标识;save_log() 不调用车型日志净化器,副本可能携带原始 CAN/VIN。因此不能以脚本名称替代对进程副作用的审查。

    回放导出还有一个隐含前提:所选进程必须在当前代码版本下可复现,否则混合出的日志既不等于原始输入,也不等于一次真实回放;这正是需要把代码版本与被替换主题一并写入导出元数据的原因。

    test_fw_query_on_routes:批量审计的失败面

    该脚本的参数可为单一路段或文件路径;文件分支用 list(open(...)) 直接取每行,再交 SegmentRange;未指定段则拼 /0,随后按 QLOG 读取 carParams 并分别做精确/模糊固件匹配。探针确认列表行尾换行未剥离,正则在内部 try 前抛断言并中断整批;读取器构造失败同样在 try 前传播。

    统计侧以 dongle_id 去重,且在读路段之前就把该键加入集合;对不匹配固件按车型、地址去重汇总,最终打印精确、未识别与模糊匹配计数。需要提醒的是:首个路段没有 carParams 时,同设备第二路段仍被跳过;不支持的 panda 类型会提前结束,随后又报告无 CarParams。这些输出包含 VIN 与固件字节,应视为敏感诊断数据。

    set_car_params:三键写入无补偿

    有路段时,脚本只读取第一条 qlog 中第一条 carParams;无参数时新建空白消息,只把 openpilotLongitudinalControl=True、alphaLongitudinalAvailable=False 写入。随后为每个键新建 Params(),按 CarParams、CarParamsCache、CarParamsPersistent 的顺序阻塞写入。

    假 Params 探针确认:缺少 carParams 时在任何写入前抛 IndexError;第二键抛异常时第一键已调用成功、第三键未调用。但原 Python Params.put() 会丢弃底层负错误码,故底层写盘失败可能不抛异常,脚本仍继续写第三键。本层没有回滚或读回,也不校验平台、VIN、safety 或当前车辆身份。另据键属性,前两者会被 manager 启动清理,CarParamsPersistent 持久化。

    键 写入时机 生命周期
    CarParams 三键顺序写入的第一键 被 manager 启动清理。
    CarParamsCache 第二键 被 manager 启动清理。
    CarParamsPersistent 第三键 持久化。

    插件化:写入权归谁

    print_flags.py 的只读枚举也提示了一件事:即便修复了缺少 with_flags() 的问题,它仍只枚举本地声明,不证明硬件或 safety 已启用。实际车型能力仍须从车型接口、DBC 与 panda safety 交叉验证。这也说明开发工具的结论天然是局部的:它们验证的是脚本自身的控制流,而不是目标车辆的参数正确性。

    探针 覆盖的契约
    标志枚举探针 核对 Platforms 与各品牌方法清单,确认缺少 with_flags() 的异常与零输出分支。
    参数注入主入口探针 验证默认两个标志、三键写入顺序、只取首个 carParams,以及第二键异常后不触发第三键。
    固件审计主入口探针 验证行尾换行、读取器构造异常、同设备去重及不支持 panda 后的状态报告。
    回放导出主入口探针 确认剔除被排除的模型进程、只替换实际产生的类型、零输出仍导出全部旧消息。

    批量固件审计应先把路段列表解析、去重与错误计数做成显式结果;精确/模糊候选只能用于平台审核,不自动修改固件库。回放导出的日志需携带输入路段、所选进程、代码版本、被替换主题、生成主题和隐私标记,避免把混合旧/新消息误当统一来源。CarParams 的写入应归受控车辆身份服务所有,要求平台/线束/DBC/safety 绑定、三键一致性门和读回验证,不能下放给任意插件。

    常见问题

    为什么 set_car_params.py 不能在车端随意试跑?

    因为它会写入当前参数库:按 CarParams、CarParamsCache、CarParamsPersistent 三键顺序阻塞写入,且本层没有回滚或读回、不校验平台/VIN/safety/车辆身份。底层写盘失败还可能因负错误码被丢弃而不抛异常。

    回放导出会混入旧消息吗?

    会。探针确认零回放输出时,全部旧消息仍会进入导出并按时间排序;脚本只按本轮实际产生的消息类型删除旧输入。导出还不调用车型日志净化器,副本可能携带原始 CAN/VIN。

    为什么固件审计脚本可能整批中断?

    因为文件分支取每行时行尾换行未剥离,SegmentRange 的正则在内部 try 之前就抛断言并中断整批;读取器构造失败也在 try 前传播,同样会中断。

    print_flags.py 能证明某车型能力已启用吗?

    不能。它只遍历本地品牌与平台配置来列出车型名称,且归档中并未定义它调用的 with_flags();即使修复,它仍只枚举本地声明,不证明硬件或 safety 已启用。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DEV_FW_REPLAY_CARPARAMS_TOOLS_TRUST_BOUNDARY_DEEP_DIVE.md(取用了四个开发工具的入口、数据流与失败边界及探针结论)。
    • 项目 02 —— openpilot-research/PROCESS_REPLAY_VALIDATION_DEEP_DIVE.md(仅用于确认回放进程与验证的边界)。

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

    更新记录

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

  • VisionIPC 缓冲契约:FD 握手、帧通知与复用覆盖窗口

    结论:VisionIPC 用两条独立通道搬运图像:Unix socket 用 SCM_RIGHTS 传 FD 与缓冲描述,msgq 只发 {server_id, idx, extra} 小包。图像字节在共享缓冲里循环复用,因此「msgq 收到帧通知」不等于「当前像素通过完整性与时序验证」——包顺序并不保证旧包对应的像素仍是旧帧。

    双通道结构:FD 握手与帧通知

    相机采集器创建固定数量的 VisionBuf(NV12 共享内存或 ION),客户端通过 Unix socket 的 SCM_RIGHTS 拿到 FD 并导入内存;随后每次发布只发一个含 server_id、缓冲索引与 extra 的小包。通知通道与描述/FD 通道是不同的故障面:前者决定图像何时被认为可读,后者决定缓冲能否安全导入。

    VisionIpcServer.listener() 在 Unix socket 接受流类型请求,可返回流列表,或把该流的元数据与所有缓冲 FD 一起传给客户端,返回描述里附上服务端随机 server_id 并清掉私有 ION handle 字段。VisionIpcClient.connect() 先释放旧映射,再接收 FD、导入内存并初始化 NV12 指针;源码明确写明该操作非线程安全,调用期间不能使用旧缓冲。

    缓冲创建与 NV12 布局

    默认 create_buffers() 用 width*height*3/2、stride=width、uv_offset=width*height,不能直接套在有 ISP 行对齐的硬件上。相机侧使用带实际尺寸的接口创建 18 个 YUV 缓冲。num_buffers 只要求小于 VISIONIPC_MAX_FDS=128,没有在此处证明任意数量足以避免覆盖。

    缓冲分配还涉及平台差异:通用平台用共享文件 FD 加 mmap,图像区尾部再留 8 字节存 frame_id;通用实现的 sync() 是空操作,不代表所有 GPU/ISP 一致性已保证。ION 分支申请共享句柄并 mmap,sync() 通过 ION ioctl 做缓存操作;新硬件的 DMA-BUF 分配和 cache API 不能仅替换文件名或跳过同步。

    函数与数据契约

    组件/入口 行为 不能假定
    VisionBuf::allocate() 通用平台用共享文件 FD 加 mmap,图像区尾部再留 8 字节存 frame_id。 通用实现的 sync() 是空操作,不代表所有 GPU/ISP 一致性已保证。
    VisionBuf::allocate() ION 版 Qualcomm/ION 分支申请共享句柄并 mmap,sync() 通过 ION ioctl 做缓存操作。 新硬件的 DMA-BUF 分配和 cache API 不能仅替换文件名或跳过同步。
    VisionIpcServer::get_buffer() 默认按索引循环取缓冲,或由摄像头指定索引。 无消费者释放/引用计数等待;源码此处还有「是否检查已发送」的待办。
    VisionIpcServer::send() 可选执行缓存同步,然后发 {server_id, idx, extra} 小包。 msgq 包不是图像快照;发送后对应共享缓冲仍可被生产者重用。
    VisionIpcClient::recv() 按 msgq 包索引找已导入缓冲;越界或 server_id 不同会断开,成功时缓存同步并返回指针。 不比对共享缓冲尾部 frame_id 与 packet extra.frame_id,也不验证像素读取期间未被再次覆盖。
    VisionIpcBufExtra 通知含 frame_id、SOF/EOF 时间戳和 valid。 Python 发送包装固定写 valid=False;本快照原生相机的三字段聚合初始化也令其默认为 false,不能把该位作为普遍帧健康门。
    send() Python 包装 复制输入字节进下一个缓冲、写尾部帧号并发通知。 这是一次 CPU 拷贝路径,不等于通用零拷贝采集。

    复用覆盖窗口:包顺序不等于像素帧

    缓冲与通知异步推进,包顺序不保证旧包对应的像素仍是旧帧。例如只创建一个缓冲,连续发送帧 1、帧 2 后再让非 conflate 客户端读取:通知可能先返回 extra.frame_id=1,但同一缓冲已被帧 2 改写。现有非合并测试只检查两次通知的 frame_id,未在这种情况下比较两次读取的像素内容。这是代码结构推得的覆盖风险,源文档未声称已在本机实际复现。

    conflate=True 只跳过旧通知,不能独立证明最新缓冲在 GPU/CPU 读取完成前不被下一轮覆盖。因此需要在目标板上记录 packet.frame_id、共享尾部 frame_id、像素哈希、SOF/EOF 与推理/显示使用时刻,注入断连、重启、丢帧和消费者长尾耗时,才能验证这一窗口。

    重连与插件契约

    VisionIpcClient::recv() 的越界/server_id 检查能识别服务端重建、索引集合变化,但只在收到新通知时触发;没有通知时返回 nullptr,并不会自动重新握手。UI 的 CameraView 有连接重试/切流逻辑;道路 modeld 和 dmonitoringmodeld 在启动时循环连接,主循环遇到 recv() is None 时继续等待,源码未在循环里显式重新 connect()。不能从 UI 的恢复路径推断模型进程也能无重启恢复。

    消费者 连接与重连行为 conflate
    UI CameraView 有连接重试/切流逻辑 —
    modeld 主相机 启动时循环连接,主循环 recv() is None 只继续等待 True
    modeld 广角附加流 同上,并以 SOF 时间戳对齐 False
    dmonitoringmodeld 启动时循环连接,源码未见循环内显式重连 —

    modeld 主相机使用 conflate=True,广角附加流使用 False 并以 SOF 时间戳对齐;即使两路通知的时间相差超过 10 ms,代码记录错误后仍向后执行。它依赖同一时钟域和合理缓冲复用窗口。自制相机插件必须说明:SOF/EOF 来自何处、时钟域/单位、帧号如何跨 VisionBuf 尾部与通知保持一致、行步长/UV 偏移、缓冲数和最大持有时长、缓存同步方向、服务端重启后的 FD 重新导入。

    常见问题

    msgq 收到帧通知,是否代表像素已经就绪?

    不代表。msgq 只发 {server_id, idx, extra} 小包,图像字节在共享缓冲里;缓冲与通知异步推进,包顺序不保证旧包对应的像素仍是旧帧。recv() 也不比对共享缓冲尾部 frame_id 与 packet 的 extra.frame_id。

    为什么只创建一个缓冲特别危险?

    因为连续发送帧 1、帧 2 后,通知可能先返回 extra.frame_id=1,但同一缓冲已被帧 2 改写,客户端读到的像素已不是帧 1。现有非合并测试只检查两次通知的 frame_id,未比较像素内容。

    conflate=True 能保证最新缓冲不被覆盖吗?

    不能。conflate=True 只跳过旧通知,不能独立证明最新缓冲在 GPU/CPU 读取完成前不被下一轮覆盖;仍需在目标板上用像素哈希与时间戳验证覆盖窗口。

    模型进程会像 UI 一样自动重连吗?

    源文档未给出该结论。UI 的 CameraView 有连接重试/切流逻辑,但道路 modeld 与 dmonitoringmodeld 在启动时循环连接,主循环遇到 recv() is None 只继续等待,源码未在循环里显式重新 connect()。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/VISIONIPC_BUFFER_CONTRACT_DEEP_DIVE.md(取用了双通道结构、缓冲创建与 NV12 布局、函数与数据契约及复用覆盖窗口)。
    • 项目 02 —— openpilot-research/VISIONIPC_UNIX_FD_HANDSHAKE_ERROR_CONTRACT_DEEP_DIVE.md(仅用于确认 FD 握手与错误码传播的边界)。

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

    更新记录

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

  • VehicleModel 的 0.1 与 1e-6:低速回退与横滚补偿奇异点

    结论:VehicleModel 是主机上的纯计算模型,不是独立的安全许可,它的两个数字最容易被误用。第一是速度下限 0.1:steady_state_sol() 在 u > 0.1 时选动态解,否则回退到运动学解,而动力学矩阵 A 含 1/u,不能把零速送入动态分支。第二是 1e-6:roll_compensation() 在 abs(sf) < 1e-6 时直接给零,否则按 g*roll/(1/sf - u²) 计算。此外 panda 固件的曲率因子不包含 Python 侧的 chi 或横滚补偿,两端不能宣称逐点数学等价。以下基于固定快照 c8fb906 的静态源码分析,未做实车动力学辨识或 CAN 发送验证。

    输入、状态与公式

    VehicleModel.__init__() 从 CarParams 保存质量 m、转动惯量 j、轴距 l、质心到前后轴距离 aF/aR、后轮转向比 chi 以及前后原始侧偏刚度。update_params() 仅把原始刚度乘以在线 stiffness_factor,并替换方向盘转向比 sR,不会累乘上次结果。

    三条核心公式:calc_slip_factor() 算 sf = m(cF*aF − cR*aR) / (l²*cF*cR);curvature_factor() 给出 k = (1 − chi) / (l * (1 − sf*u²));calc_curvature() 返回 k*sa/sR + roll_compensation,其中 sa 为方向盘弧度、u 为 m/s,输出单位为 1/m。get_steer_from_curvature() 先扣除横滚补偿再除以 k,不是简单以轴距乘曲率。

    函数 作用 输入与边界
    steady_state_sol() 速度 u>0.1 时选动态解,否则选运动学解 返回 [侧向速度, 横摆角速度];低速分支不使用 roll。
    kin_ss_sol() 低速按轴距、后轴到质心距离、转向比得线性解 即使 u=0 也返回零;不显式使用 chi。
    create_dyn_state_matrices() 构造状态 [侧向速度, 横摆角速度]、输入 [方向盘角, 横滚角] 的 2×2 A/B A 含 1/u,不能把零速送入动态分支。
    dyn_ss_sol() 用 −solve(A, B) @ [sa, roll] 求稳态 solve 可能因参数退化或矩阵奇异失败,函数本身不做回退。
    get_steer_from_yaw_rate() 先以 yaw_rate/u 求目标曲率,再反算方向盘角 u=0 无保护;与低速运动学回退是两条不同 API。

    低速回退与奇异点约束

    模型里有两个需要调用方自己守住的数值边界。其一是低速回退:u=0 时动态分支不可用,必须走运动学近似,且该近似即使 u=0 也返回零。其二是横滚补偿的奇异点:roll_compensation() 在 abs(sf) < 1e-6 时直接给零,避开分母 1/sf − u² 的退化。

    边界量 取值 作用
    动态/运动学切换 u>0.1 决定用动态解还是低速运动学解。
    侧偏因子近零门 abs(sf)<1e-6 横滚补偿直接给零,避开奇异分母。
    在线更新下限 0.1 controlsd 更新模型时取的下限,仅防非正值。
    角度限幅最小速度 1 m/s apply_steer_angle_limits_vm() 里原始速度至少取 1 m/s。

    需要注意的是,controlsd 从 vehicleParameters 读取估计值并各自以 0.1 为下限更新模型,这个下限仅防止非正值,不能证明估计值物理可信。上游 paramsd 发布时另带有效性字段,但该调用点没有以这些字段阻止更新。

    运行调用链与安全边界

    paramsd 发布估计的转向比、刚度、横滚和转角偏移,controlsd 每周期更新模型,再用转角减偏移估算实际曲率(此处曲率加负号以匹配控制层符号约定)。LatControlCurvature.update() 用模型曲率构成跟踪误差;LatControlTorque.update() 将其乘 vEgo² 作为横向加速度测量,并用模型把方向盘死区换算为加速度死区。模型误差会传播到控制反馈,但模型本身不决定 latActive。

    apply_steer_angle_limits_vm() 用反算方向盘角把横向 jerk/加速度上限转换为主机角度限幅;Tesla 与 BYD 的控制器各用固定安全车型参数实例化独立模型,避免把 controlsd 在线估计参数直接当作固件许可条件。

    主机与固件的差异

    panda 的 steer_angle_cmd_checks_vm() 在固件内独立计算角度/加速度/jerk 门;其 get_curvature_factor() 与 Python 基式相近,但采用固件给定 slip_factor/轴距/转向比,不包含 Python 的 chi 或 roll_compensation,还使用固件车速与额外容差。因此不能声称两端逐点数学等价。

    项 Python 主机模型 panda 固件
    后轮转向比 chi 参与 curvature_factor() 不包含
    横滚补偿 roll_compensation() 参与 不包含
    侧偏因子/轴距/转向比 来自 CarParams 或在线估计 固件给定值
    车速来源与容差 主机估计 固件车速加额外容差

    joystickd_thread() 也会用模型估算测试转角幅度,这不代表绕过车端许可。车辆模型可作为纯计算插件提供曲率、横摆角速度及反算接口,但应在边界明确参数来源、单位、版本、速度有效域和非有限值处理;在线估计值应视为观测数据而非授权数据,主机控制器与 panda 固件必须绑定同一车辆身份和经校准的安全参数集,版本失配时禁用执行。

    现有验证与缺口

    现有 test_vehicle_model.py 以 Honda Civic 参数验证 1–30 m/s 下的横摆角速度往返、动态稳态解与直接公式一致,以及一秒离散仿真逼近稳态;它没有覆盖 u=0 反算、参数退化、在线更新突变、固件与 Python 跨实现一致性或实车传感器误差。本专题只做静态源码核查,未运行测试或实车验证。下一阶段宜先在隔离仿真中加入零速/奇异点、参数边界、符号一致性和主机-固件限幅差异矩阵,再讨论车端实验。

    常见问题

    为什么 0.1 这个速度下限会出现在模型里?

    因为动态状态矩阵 A 含 1/u,steady_state_sol() 在 u > 0.1 时才选动态解、否则回退运动学解;controlsd 更新模型时也取 0.1 为下限,但那只是防止非正值,不能证明估计值物理可信。

    roll_compensation() 在什么情况下直接返回零?

    当 abs(sf) < 1e-6 时直接给零,否则按 g*roll/(1/sf − u²) 计算。这个门槛是为了避开分母退化,属于奇异点约束。

    主机模型和 panda 固件算的曲率一样吗?

    不一样,也不能宣称逐点等价。固件的 get_curvature_factor() 与 Python 基式相近,但采用固件给定的 slip_factor/轴距/转向比,不包含 Python 的 chi 或 roll_compensation,还使用固件车速与额外容差。

    VehicleModel 是一个转向安全许可吗?

    不是。它是主机上的纯计算模型,不决定 latActive;候选转角经宿主限幅后仍须由不可被普通插件替换的独立固件安全门检查。在线估计值应视为观测数据而非授权数据。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/VEHICLE_MODEL_DYNAMICS_CONTROL_SAFETY_CONTRACT_DEEP_DIVE.md(取用了输入与公式、关键函数索引、低速回退与 1e-6 奇异点、运行调用链、主机与固件差异以及现有测试缺口)。

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

    更新记录

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

  • 模型回放:瞬时 50 ms、平均 30/18 ms 与 PC 放宽门控

    结论:模型回放的时序门是样本统计而非端到端保证——modelV2 瞬时上限 50 ms、平均上限 30 ms(检测到大模型时 50 ms),driverStateV2 瞬时上限 50 ms、平均上限 18 ms,二者都丢弃第一条执行时间再取 max/mean。PC 模式会额外忽略位置、车道线、道路边缘、前车轨迹等字段并使用 0.3 的比较容差,CI 下比较结果 failed 被强制清零,因此 CI 的零退出码不能作为严格数值门的通过证据。以下为固定源码归档的静态阅读与纯消息替身探针,未下载路段、未加载模型、未执行模型。

    关键组件与函数

    该入口把道路模型和座舱模型分成两条独立回放链,各自裁取消息、插入标定与设备状态,再顺序调用回放。下表按组件列出职责与约束。

    函数 / 组件 实际职责 约束与风险
    get_log_fn() 由路段、CHESTNUT 和参考名构造 .zst 文件名 CHESTNUT 来自命令行 --chestnut;文件名不是模型权重/设备身份的完整证明
    plot() / generate_report() 把指标序列画成 PNG,并从 modelV2 取 8 项、driverStateV2 取 7 项,共生成 15 张曲线 proposed == master 是 Python 序列精确相等,仅决定报告分组,不参与主判定
    comment_replay_report() 有关联 PR 时上传图片及本次完整日志,并发布评论 其中上传/评论异常在主程序的比较 try 内会转为 failed=True,不能把报告服务失败直接解读为模型输出不等价
    trim_logs() 按相机状态计数选窗口,再把指定辅助主题的全部原始消息追加 默认 START_FRAME=0 时从首条消息开始纳入;追加可能重复和打乱时间顺序
    model_replay() 道路模型和座舱模型分别裁取消息、插入标定/设备状态,顺序调用回放;随后检查大模型状态及执行耗时 两条回放链分别启动进程,不是单个端到端闭环;缺首条标定或设备状态会 StopIteration
    get_frames() 从固定缓存反序列化,或取三路 .hevc,预热 [START_FRAME, END_FRAME) 的 NV12 帧后把 FrameReader pickle 落盘 缓存名只有路段/段号/窗口,未包含权重、软件提交、解码器和视频内容哈希;pickle.load() 要求缓存文件可信
    __main__ 读取路段、回放、按模式比较或更新参考,写 model_diff.txt,用 failed 退出 更新、CI、PC 三个模式改变判定语义,不能只看退出码

    帧窗口与消息来源

    道路模型以 narrowRoadCameraState 与 wideRoadCameraState 双主题计数,座舱模型以 cabinCameraState 计数,并分别追加 EncodeIdx、车辆/CAN 等辅助主题,默认窗口 START_FRAME=0、END_FRAME=60。SEND_EXTRA_INPUTS=0 时会删除裁取日志里的 extrinsicsCalibration,但随后从原路段无条件取首条标定和 deviceState,将其时间改为原日志首条时间并插入两条回放链的索引 1。故原日志为空或缺主题不是可降级场景。

    用不依赖 Cap’n Proto 的假消息执行原版 trim_logs():输入时间序列为 carState@1, other@2, narrow@3, wide@4,窗口 end_frame=1,并要求全量追加 carState。start_frame=0 的返回顺序是 carState@1, other@2, narrow@3, wide@4, carState@1;start_frame=1 返回 narrow@3, wide@4, carState@1。这说明辅助主题既可能重复又可能后置成非时间顺序;下游 migrate() 排序消息,不自动去重。另需注意双摄像头窗口的结束条件是所有相机状态计数恰等于 END_FRAME,其中一路缺帧时可能继续遍历到日志尾部。

    时序门与数值门

    时序门给出各主题的瞬时与平均上限,并统一丢弃第一条执行时间。

    主题 瞬时上限 平均上限
    modelV2 50 ms 30 ms(检测到大模型时 50 ms)
    driverStateV2 50 ms 18 ms
    统计方式 丢弃第一条执行时间 再用 np.max/np.mean;未覆盖首帧冷启动、端到端帧年龄或最坏调度延迟

    chestnut 由道路模型消息中是否有任一 big 标志推断;只有命令行指定 --chestnut 才断言道路模型每条均为大模型且至少有一条大模型输出。最后的 assert timings_ok or PC 使 PC 时序超限只打印,不阻断退出。

    非更新模式下,主程序按道路模型每帧预期 3 条输出、座舱每帧 1 条输出,从远端参考日志按位置切片,忽略 logMonoTime 和部分掉帧/执行时间字段,再调用比较。PC 又额外忽略位置、车道线、道路边缘、前车轨迹等字段,并使用 0.3 的比较容差;这不是「全部模型输出相等」。设置 CI 环境变量时,比较结果 failed 被强制清零,差异仍会写入报告。因此 CI 的零退出码不能作为严格数值门的通过证据。更新模式(--update 或 GIT_BRANCH=master)跳过参考比较,非 PC 才保存并上传新参考;上传异常只打印,不改变 failed,所以零退出码也不证明远端参考已更新。

    自制硬件与插件化验收

    应把道路模型、座舱模型和相机采集分别建成有版本的插件契约:输入包括 NV12 格式/尺寸、相机时钟和帧 ID、标定、设备状态、权重哈希;输出包括 schema 版本、每帧关联键、模型能力和执行时长。缓存以视频/权重/解码器/软件版本的内容哈希为身份,且不加载不可信 pickle。

    参考比较须声明忽略字段与阈值,单独记录实际覆盖帧数、缺帧、重复输入、首次启动耗时、持续时延与输出完整性。PC、CI 和目标车机分别给出独立通过位;实板热态/冷态、负载、供电降额、相机断流和故障回退仍需台架及封闭场地验证。本文没有验证这些动态指标。

    常见问题

    50 ms 与 30 ms 是端到端时延保证吗?

    不是。它们是样本统计:modelV2 瞬时上限 50 ms、平均上限 30 ms,两者都丢弃第一条执行时间再取 max/mean,未覆盖首帧冷启动、端到端帧年龄或最坏调度延迟。

    CI 通过就代表数值回归通过吗?

    不代表。设置 CI 环境变量时比较结果 failed 被强制清零,差异仍会写入报告;报告上传/评论异常则仍可能进入 except 导致失败。零退出码不能作为严格数值门的通过证据。

    PC 模式比较的是全部模型输出吗?

    不是。PC 额外忽略位置、车道线、道路边缘、前车轨迹等字段,并使用 0.3 的比较容差;这并不等于「全部模型输出相等」。

    输入日志的时间顺序有保证吗?

    没有。trim_logs() 把指定辅助主题的全部原始消息追加,可能重复且后置成非时间顺序;下游 migrate() 只排序,不自动去重。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PROCESS_REPLAY_MODEL_FRAMES_TIMING_REFERENCE_DEEP_DIVE.md(模型回放的帧输入、时序门、数值门与报告语义)
    • 项目 02 —— openpilot-research/ARCHITECTURE_GUIDE_CN.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

  • JotPluggler 绘图:状态块、分桶抽样与时间交互边界

    结论:JotPluggler 的绘图是一条显示语义链,不是控制语义链。曲线进入绘图前会经过时间窗裁剪、导数/缩放变换和分桶抽样,跟踪时间的图例值还会按曲线类型做阶梯取值或线性插值;这些都属于屏幕显示行为。抽样结果只用于显示,导出与控制决策必须回到原始数据;车端部署应把诊断绘图插件限定为只读,不能因其消费 CAN 曲线就获得 CAN TX 或安全策略写权限。

    曲线进入绘图的路径

    基线是主仓库 c8fb906 本地源码归档,仅静态阅读;未编译、运行 GUI 或测量大路段绘制性能。draw_pane_windows() 按 pane 类型分派地图、缩略图、相机或 draw_plot()。图表仅处理 visible 且有本地样本或有效路段样本的曲线;RouteSeries 至少需要两个等长的时间和值样本。

    build_curve_series() 将曲线转换成 PreparedCurve:普通路段曲线先按共享时间窗二分查找,保留窗边相邻样本;本地曲线直接复制已有 xs/ys,不走该时间窗裁剪。路段样本在收集阶段经时间排序,但这里未重新排序本地样本;插件提供本地序列时应明确单调时间契约。

    导数模式逐相邻点计算差分,derivative_dt > 0 时使用指定间隔,否则使用实际时间差;dt <= 0 的样本对被跳过。随后统一施加 value_scale 和 value_offset。枚举元数据只在未变换的原始路段曲线上保留。跟踪时间的图例值由采样函数获得:阶梯曲线取前一值,普通曲线线性插值,越界时间取端点值——这是显示语义,不应直接作为控制回路的时序/插值语义。

    渲染与抽样语义

    函数/组件 关键行为 边界
    app_decimate_samples_impl() 将样本分桶,每桶保留首点、最小、最大和末点,按原索引去重;目标点数从绘图区宽度的约两倍计算。 显示抽样,不是无损数据导出;保留极值不保证每个瞬态的形状与准确持续时间。
    build_curve_series() integer_like 曲线改用阶梯模式,直接保留全部变换后样本;非阶梯曲线才按桶抽样。 大量整数/CAN 状态样本仍可能全量进入每帧绘制;格式判断最多约每 256 个样本取一步,稀疏小数可能被漏判。
    compute_plot_bounds() 依据已准备曲线求 Y 极值,布尔型至少覆盖 0/1,补边距,再应用 pane 的显式 Y 限值。 Y 范围来源于准备后的显示样本;非有限值、极端值和混合单位需单独测试。
    draw_state_blocks_pane() 当可见曲线全部具有枚举元数据时,按连续相同状态聚合为彩色块,悬停显示标签、时间区间和持续时间。 块按 llround 后的整数状态划分;不是原始事件边沿的独立校验。
    draw_plot() 普通曲线用 ImPlot PlotLine,整数型用 PlotStairs;共享 X 轴、图例当前值、tracker 竖线和点击选时。 状态块、阶梯与线图有不同采样和显示语义,不应合并成一个无差别插件接口。

    时间窗、播放与持久化

    ensure_shared_range() 优先按布局中的 tab 范围初始化离线路段,实时模式按缓冲时长从最新时间回看;clamp_shared_range() 限制最小水平跨度为 2 秒,并分别处理离线边界和流式右边界;update_follow_range() 将流式窗口保持在最新样本处,infer_stream_follow_state() 根据窗口右缘和目标跨度判断是否恢复跟随。绘图时 ImPlot::SetupAxisLinks 直接绑定 UI 中的 x_view_min/max,所以 pane 之间共用水平视窗。

    函数 行为 边界
    ensure_shared_range() 离线路段按布局 tab 范围初始化,实时模式按缓冲时长从最新时间回看。 实时窗口依赖缓冲时长,不等于数据源已完整。
    clamp_shared_range() 限制最小水平跨度为 2 秒,分别处理离线边界与流式右边界。 跨度限制是显示约束,不改动原始样本。
    advance_playback() 按 ImGui 帧间隔与播放倍率推进 tracker,支持视窗循环,非循环时光标越过 70% 视窗后滚动。 播放是 UI 时间光标行为,不控制车辆。
    apply_axis_limits_editor() 检查有限数与上下界次序,把共享 X 范围写到 tab 所有 pane、Y 限值写回布局,入撤销栈并自动保存草稿。 继承撤销栈「修改前快照」语义问题。

    render_layout() 每帧先维护时间窗,再处理快捷键、播放器和工作区。advance_playback() 按 ImGui 帧间隔与播放倍率推进 tracker,支持选定视窗循环,非循环时在光标越过 70% 视窗后滚动;step_tracker() 以配置步长单步移动。绘图区点击更新 tracker 并画竖线;播放/点击都是 UI 时间光标行为,不会直接控制车辆或改变日志源。

    apply_axis_limits_editor() 检查有限数和上下界次序,把共享 X 范围写到 tab 的所有 pane,并把该 pane 的 Y 限值写回布局,随后入撤销栈、自动保存草稿;它继承布局专题指出的撤销栈「修改前快照」语义问题。draw_pane_context_menu() 提供轴编辑、分割、重置视窗与清空曲线;「曲线样式」、复制图片、保存图表和统计项在本版本被显式禁用,不能列为已实现功能。

    对「万物即插件」的约束

    建议将 SeriesProvider(时间和值及来源)、SeriesTransform(导数/缩放/自定义计算)、PlotRenderer(线/阶梯/状态块)和 TimeCursor(共享窗口/播放)分别定义契约。输入需带时间基准、单调性、单位、枚举表、无值/非有限值规则及最大点数;渲染端有可取消任务与逐帧预算。抽样结果只用于屏幕显示,导出与控制决策须回到原始数据。车端部署仍应把诊断绘图插件限定为只读。

    当前同目录 test_help() 仅覆盖 -h。本篇未执行绘图性能、异常时间戳、NaN/Inf、轴编辑或跨 pane 交互测试;地图、相机/缩略图、日志浏览器和导出仍待专题分析。

    常见问题

    绘图里的曲线抽样会不会丢数据?

    抽样只影响显示。app_decimate_samples_impl() 把样本分桶、每桶保留首点/最小/最大/末点并按原索引去重,源码明确它是显示抽样、不是无损数据导出,保留极值并不保证每个瞬态的形状与准确持续时间。导出与控制决策必须回到原始数据。

    状态块是按事件边沿画的吗?

    不是。draw_state_blocks_pane() 在可见曲线全部具有枚举元数据时,按连续相同状态聚合为彩色块,块按 llround 后的整数状态划分,悬停显示标签、时间区间和持续时间。源码明确这不是原始事件边沿的独立校验。

    跟踪时间的图例值能当控制插值用吗?

    不能。图例值由采样函数获得:阶梯曲线取前一值,普通曲线线性插值,越界时间取端点值。源码说明这是显示语义,不应直接作为控制回路的时序/插值语义;状态块、阶梯与线图也各有不同采样语义,不应合并成一个无差别插件接口。

    JotPluggler 能直接控制车辆吗?

    不能。绘图区的点击与播放都只是 UI 时间光标行为,源码明确它们不会直接控制车辆或改变日志源。车端部署应把诊断绘图插件限定为只读,不能因其消费 CAN 曲线而获得 CAN TX 或安全策略写权限。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/JOTPLUGGLER_PLOT_INTERACTION_DEEP_DIVE.md(取用了曲线进入绘图路径、抽样与状态块语义、时间窗/播放/轴编辑边界与插件约束)。
    • 项目 02 —— openpilot-research/JOTPLUGGLER_LAYOUT_SESSION_DEEP_DIVE.md(仅用于确认撤销栈「修改前快照」语义的接续关系,未取用其未公开内容)。

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

    更新记录

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

  • Chrysler panda safety:CUSW 双位按钮与两套模式矩阵

    结论:Chrysler panda safety 有两套互不通用的安全模式:常规模式按参数位选 Pacifica/Jeep、Ram DT 或调试构建中的 Ram HD,CUSW 是独立且仅调试注册的另一套钩子。最需要记住的边界是按钮载荷:常规模式以 data[0]==1 判取消、data[0]==0x10 判恢复,两个值不能同时匹配;而 CUSW 用两个独立位,表达式 is_cancel || (is_resume && controls_allowed) 使 0x11 同时置位在控制未许可时仍会通过。

    本文基于固定源码快照,分析常规 Chrysler 与独立 CUSW safety 模式,并用本机隔离探针核对若干分支。ALLOW_DEBUG 是编译条件,不等于目标 panda 固件已经启用;探针不代表刷写、台架或实车验证。

    模式入口与函数索引

    chrysler_init() 按参数位选择 RX/TX 表,Ram DT 分支优先于 Ram HD;非调试构建忽略 Ram HD 位,落入 Pacifica/Jeep 配置。CUSW 的安全模式注册受 ALLOW_DEBUG 限制,属于独立钩子。

    函数 作用与关键差异
    chrysler_init() 按参数位选 Pacifica/Jeep、Ram DT 或调试构建中的 Ram HD;Ram DT 优先于 Ram HD,非调试构建忽略 Ram HD 位
    chrysler_rx_hook() 按平台地址与总线更新 EPS 电机扭矩、ACC 接合、停车状态、油门及制动
    chrysler_tx_hook() 解码目标转向扭矩及请求位,调用共享限幅;按钮仅允许取消,或在 controls_allowed 时恢复
    chrysler_cusw_init() 建立独立的五组 RX 与三组 TX 白名单;安全模式注册受 ALLOW_DEBUG 限制
    chrysler_cusw_tx_hook() 扭矩限制为 250 原始单位;取消与恢复按钮分别取 bit 0、bit 4
    chrysler_compute_checksum() 对除末字节外的载荷按多项式 0x1D 做 CRC8,初始值 0xFF,末尾按位取反;末字节为接收校验值

    平台与信号矩阵

    四种模式的电机扭矩误差项均为 80。表内上限是 safety 内部原始单位,不是物理方向盘扭矩,也不是可直接移植的标定值。常规模式的 EPS 测量来自载荷字节 4/5,CUSW 来自字节 3/4;常规模式以 0x1F4/0x99 的 bit 21 判断 ACC,CUSW 以 0x2EC 的 bit 7。

    模式 关键 RX 来源 TX 地址、总线、长度 扭矩上限 / 每帧升降 / 实时变化
    Pacifica/Jeep EPS 0x220/bus 0;ACC 0x1F4/bus 0;车速 0x202/bus 0;油门 0x22F、制动 0x140/bus 0 按钮 0x23B/0/3;转向 0x292/0/6;HUD 0x2A6/0/8 261 / 3、3 / 112
    Ram DT EPS 0x31/bus 0;ACC 0x99/bus 2;车速 0x79/bus 0;油门 0x9D、制动 0x83/bus 0 按钮 0xB1/2/3;转向 0xA6/0/8;HUD 0xFA/0/8 350 / 6、6 / 112
    Ram HD,仅 ALLOW_DEBUG EPS 0x220/bus 0;ACC 0x1F4/bus 2;车速 0x11C/bus 0;油门 0x22F、制动 0x140/bus 0 按钮 0x23A/2/3;转向 0x276/0/8;HUD 0x275/0/8 361 / 14、14 / 182
    CUSW,仅 ALLOW_DEBUG 电机扭矩 0x1EC;ACC 0x2EC;车速 0x1E4;油门 0x1FE;制动 0x1E8,均 bus 0 按钮 0x2FA/0/3;转向 0x1F6/0/4;HUD 0x5DC/0/4 250 / 4、4 / 150

    移动判断也按平台分:Pacifica/Jeep 用左右轮速 0x202,Ram 用 ESP_8 双字节非零,CUSW 用 0x1E4 车速字段非零。健康检查方面,常规 RX 表监视五组;Pacifica/Jeep 的 0x202 特别忽略校验和与计数器,其余四组检查 CRC/计数器但忽略质量位;常规计数器为字节 6 高半字节,CUSW 计数器为倒数第二字节低半字节。CRC/计数健康不等于报文来源认证,总线身份、线束和原车 ECU 时序仍需单独验证。

    按钮载荷与转发边界

    常规按钮帧以 data[0] == 1 判断取消,以 data[0] == 0x10 判断恢复,因此两值不能同时匹配,但其它字节没有车型专属检查。CUSW 以两个独立位判断,表达式 is_cancel || (is_resume && controls_allowed) 使 0x11 同时置位在控制未许可时仍通过。

    项目 常规模式 CUSW
    取消判定 data[0] == 1 bit 0
    恢复判定 data[0] == 0x10 bit 4
    双位组合 两值互斥,组合无法同时匹配 0x11 在控制未许可时仍通过
    测试覆盖 明确测试组合被拒 测了中性、单独取消/恢复,未断言双位组合

    各模式的 HUD 地址在 TX 白名单,但两套车型 TX 钩子都没有 HUD 载荷检查。转向与 HUD 的 check_relay=true 会参与共享继电器故障检查和来自 bus 2 的静态转发阻断;按钮的 check_relay=false 不具备这两项效果。

    转发边界的判定要分三步:safety_tx_hook() 先做通用地址/总线/长度白名单与继电器故障门,再由车型钩子做载荷判定,而来自另一总线的静态转发阻断由 safety_fwd_hook() 处理。因此「能不能发」与「会不会阻断原车转发」是两个独立问题,不能只看白名单。

    两套测试的口径也不同:现有 test_chrysler_cusw.py 测了中性、单独取消或恢复,但没有断言双位组合;常规 test_chrysler.py 则明确测试组合被拒。这一差异是现有钩子行为,不应当视作车辆接受或设计允许组合键。

    软件层与固件层不是同一件事

    软件 CarInterface._get_params() 把 Ram HD 与 CUSW 标为 dashcamOnly,CUSW 另选 chryslerCusw 模式;其 CarControllerParams 还设置软件侧限幅与周期。尤其 Ram DT 软件上限为 261,而板端限值为 350,说明软件策略和固件许可不是同一层。

    自制硬件插件不能把 dashcamOnly、软件限幅或模式参数当作板端已经正确配置的回执。插件化时应把平台指纹、DBC、总线映射、safety 模式/参数、编译能力与线束版本作为不可分割的准入清单。HUD 与按钮原始帧均须列为受控输出,普通 UI/诊断插件不能绕过 card 和板端安全层直接发送。

    本机隔离探针在 release/debug 两种 ALLOW_DEBUG 编译条件下均以 -Wall -Wextra -Werror 与 UBSan 通过,覆盖平台按钮总线、Ram HD 非调试回退、CUSW 非调试不可注册、CUSW 双位组合、HUD 无逐字段门及静态转发阻断。它没有输入有效 RX 序列、验证 CRC 错误/计数回绕或执行完整车型测试;更不代表目标固件、台架或实车。

    常见问题

    CUSW 模式和常规 Chrysler 模式能混用吗?

    不能。CUSW 是独立钩子,拥有自己的五组 RX 与三组 TX 白名单、独立的扭矩编码与限值,且安全模式注册受 ALLOW_DEBUG 限制;常规模式的地址、总线、长度与按钮位都不能按品牌名复用。

    Ram HD 在普通构建里会怎样?

    非调试构建会忽略 Ram HD 位,落入 Pacifica/Jeep 配置;而且 Ram DT 分支优先于 Ram HD。所以不是「选了 Ram HD 位就一定按 Ram HD 配置」,还要看构建条件与优先级。

    为什么 CUSW 的 0x11 组合键仍能通过?

    因为 CUSW 用两个独立位判断,表达式为 is_cancel || (is_resume && controls_allowed),只要有取消位就通过,所以 0x11 在控制未许可时仍会通过。常规模式则因两值互斥无法同时匹配,且测试明确覆盖组合被拒。这是现有钩子行为,不应视作车辆接受或设计允许组合键。

    HUD 地址在白名单里,是不是就有逐字段校验?

    不是。各模式的 HUD 地址在 TX 白名单,但两套车型 TX 钩子都没有 HUD 载荷检查。白名单控制的是地址、总线、长度,并受继电器故障门约束,不等于载荷逐字段校验。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CHRYSLER_PANDA_SAFETY_MODE_DEEP_DIVE.md(取用了常规与 CUSW 两套模式矩阵、按钮载荷差异、平台信号表与隔离探针结论)。
    • 项目 02 —— openpilot-research/ACTUATION_GATE_CHAIN_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