AutoMoT Architecture: Async Dual Experts and a BEV World Model, Part 2
这张顶视图不是模型输入,是 agent 自报的一个可视化专用传感器。官方代码 mot_b2d_agent.py 的 sensors():
{
'type': 'sensor.camera.rgb',
'x': 0.0, 'y': 0.0, 'z': 50.0, # 自车顶上方 50 m
'roll': 0.0, 'pitch': -90.0, 'yaw': 0.0, # 垂直朝下
'width': 512, 'height': 512, 'fov': 50,
'id': 'bev'
}
针孔相机几何:安装高度 、视场角 时,地面覆盖边长
512 像素分摊 46.6 m,每像素约 0.09 m——够看清车辆轮廓与车道线,做监工视图刚好。
相机帧之上叠画的是 agent 缓存的模型输出(last_pred_traj / last_route_pred,见 ep11p1 B12 的输出头):
读图的要点在两类点的行为差异:行人出现时绿线收缩(纵向减速让行)而橙线可能不变(路径无需绕行)——纵向与横向解耦的输出结构,在画面上是可分的。
模型自己吃的 BEV 不是这张图,是 LiDAR 直方图(算法见本 part B06)。这张图只回答”模型做了什么”,回答”模型看到了什么”要去看激光通道。
AutoMoT 的 LiDAR 预处理是 TransFuser 一脉的经典做法,官方源码 team_code/bev_data_utils.py 的 lidar_to_histogram_features:
def splat_points(point_cloud):
xbins = np.linspace(config.min_x, config.max_x,
(config.max_x - config.min_x) * int(config.pixels_per_meter) + 1)
hist = np.histogramdd(point_cloud[:, :2], bins=(xbins, ybins))[0] # 俯视网格计数
hist[hist > config.hist_max_per_pixel] = config.hist_max_per_pixel # 截断
overhead_splat = hist / config.hist_max_per_pixel # 归一到 [0,1]
return overhead_splat.T
配合官方 config(bev_encoder/config.py)的参数,全部数字可算:
min_x=-32, max_x=32),每米 4 像素(pixels_per_meter=4.0)→ ,即 网格hist_max_per_pixel = 5,截断后除以 5 归一lidar_split_height = 0.2 m 把点分成上下两组分别 splat直方图是计数图,而点云密度极不均匀:一面正对 LiDAR 的墙,单格点数可以是空旷区域的几十倍。不截断,归一化后墙之外的几乎所有格子都被压成接近 0 的暗值——对比度被少数高密度格”吃掉”。上限 5 的含义:模型只需要知道”这格有东西”,不需要知道有 5 个还是 50 个点。
代码把 0.2 m 以下(below)和以上(above)分别成图,但发布版配置 use_ground_plane = False,最终只保留 above 通道——编码器输入通道数 in_chans = 1 + int(use_ground_plane) = 1。也就是说在官方发布配置里,0.2 m 分割线的实际作用是地面/近地面滤波,而不是输出双通道。读源码或复现时别把”代码里有两组 splat”当成”模型吃两通道”。
| 相机(语义图) | LiDAR(点云直方图) | |
|---|---|---|
| 语义 | 强:红绿灯、标牌、车道线、行人类别都可读 | 弱:只有”有东西、多高、多密”,行人和纸箱难分 |
| 几何 | 弱:单目深度靠推断,距离是猜的 | 强:测距是物理测量,厘米级 |
| 传感原理 | 被动:接收场景反射的光 | 主动:自己发射激光测回波 |
| 对渲染的依赖 | 完全依赖:画面=渲染管线的产物 | 几乎免疫:射线求交查的是场景几何 |
LiDAR 测距用飞行时间(Time of Flight, ToF):发射一束激光、测回波延迟 ,距离
为光速,除以 2 是因为光走了往返。CARLA 的 sensor.lidar.ray_cast 在仿真里复刻这个原理:向场景几何体投射射线求交——它查询的是网格和碰撞体,不经过光照、材质、后处理这一整条渲染管线。所以 UE4 换 UE5、Lumen 换光追,画面天翻地覆,同一面墙的回波距离不变。这正是”渲染保真度研究”里 LiDAR 是干扰变量的物理根源(处置见本 part B12)。
两种信息不是冗余而是正交:相机给”那是什么”,激光给”那在哪里”。BEV 编码器的四阶段融合(ep11p1 B10)就是在缝合这两种能力——缝得好,红灯可读且距离可信;缝不上,两路信息在深层特征里互相污染。
项目 T3.4 实验对两代仿真器的 sensor.lidar.ray_cast 做了逐项探测(报告存于 experiments/EXP-T3.4-automot-ue5/lidar_probe.md),默认值对表:
| 参数 | CARLA 0.10(UE5)默认 | CARLA 0.9.15(UE4)默认 = AutoMoT 训练口径 |
|---|---|---|
| channels(线数) | 64 | 32 |
| range(量程) | 50 m | 10 m |
| points_per_second | 600 000 | 56 000 |
| rotation_frequency | 60 Hz | 10 Hz |
| upper/lower_fov | 10° / −30° | 10° / −30° |
每秒点数差 10.7 倍、转速差 6 倍、量程差 5 倍——只有俯仰角一项碰巧一致。AutoMoT 的训练数据在 0.9.15 默认口径下采集(agent 侧无显式 set),所以 UE5 侧必须把全部关键属性逐项 set_attribute 显式对齐。探测结论:0.10 支持全部关键属性,对齐可行且已实测通过。
仿真传感器的默认值是 blueprint 的一部分,换仿真器版本不会替你继承旧默认。不显式对齐时,模型吃到的是一个线数翻倍、量程 5 倍、点数 10 倍的”新传感器”——输入分布(每格点数、几何覆盖)全面漂移,而下游直方图网格是固定的 ±32 m / 每米 4 像素:量程 10 m 的旧默认意味着直方图 84% 的格子恒为空。
这条坑的通用形式是:跨版本复现实验,凡是没有显式写进代码的参数,都是未受控变量。默认值不属于实验设计,属于环境细节——而环境细节两代之间从不承诺一致。探营(probe)先行、逐项登记差异,再决定哪些对齐、哪些保留为研究变量,是本项目的标准动作。
0.9.15 的 LiDAR 只返回命中的点;0.10 会把未命中的射线也写进点云,坐标填成约 的哨兵巨值(float32 上限附近的标记位)。数据格式本身兼容(xyz+intensity 四个 float32 同构),所以管线不报错——但巨值坐标进入下游一切按坐标运算的环节(ego 坐标变换、直方图网格化)都是污染源。
解法是在 server 侧先清洗再进链:
# MySim tools/ue5harness/route_executor.py — UE5 侧 LiDAR 回调
pts = np.frombuffer(ev.raw_data, dtype=np.float32).reshape(-1, 4)
pts = pts[np.isfinite(pts).all(axis=1)] # 0.10 未命中 sentinel 清洗
这类坑的危险性正在于”不报错”:格式对、维度对、程序照跑,只有数据内容坏了。
参数逐项对齐之后,UE5 侧实测每帧约 635 点(25 帧均值),远低于按训练口径推算的期望 2800 点/帧(56 000 pps ÷ 20 fps)。同参数、同网格,命中率本身就是两代仿真器射线-场景求交实现的差异。
注意一个诚实性口径:2800 是 0.9.15 的文档推算值,UE4 侧的同参数实测项目尚未补齐——所以”635 对 2800”目前是单侧实测对照文档口径,引用时必须带这个限定。
判定准则一句话:这个差异是”两代仿真器本应一致却没一致”(修),还是”两代仿真器本来就不同”(留)。
项目的研究命题是”渲染保真度对端到端模型的影响”。要让 UE4/UE5 的分数差可归因于渲染,渲染变化必须能实质性地传导进模型输入。
“稀释”的含义:AutoMoT 上测到的渲染效应是真实效应的一个下界——被 LiDAR 通道兜底吸收掉的部分观测不到。
项目实测里 AutoMoT 的 UE5 zero-shot 是 100.00(40/40 全完成、重跑零噪声),远高于 SimLingo 的 56.35。但这个差距里有多少来自更强的底座(5.6B 双专家 vs 1B)、多少来自 LiDAR 的几何兜底、多少来自渲染,观测上分不开——架构、传感器、训练数据同时变了,不满足单一变量原则。这正是”AutoMoT 的分数里有多少是激光的功劳说不清”的严格含义。
含 LiDAR 的模型被一票否决为主模型(项目关键决策:渲染变量被几何传感器稀释);AutoMoT 保留为第二模型,但一切涉及渲染的结论须强制注明”含 LiDAR 稀释”,并预留去激光消融来量化稀释幅度(三道防线的完整设计见本 part B13)。
AutoMoT 只做 UE4 基线 + UE5 zero-shot,不参与微调实验。这一刀同时由两个原因促成:研究侧,LiDAR 稀释使它的渲染结论先天降级(B12);工程侧,官方训练默认单卡 80 GB,本机 32 GB 的 5090 物理上训不了。两个原因指向同一处置,角色锁定没有争议。
项目内部把结论分两级引用:
分级的价值在于把偏差变成元数据:不是不说 AutoMoT 的结果,而是每条结果都背着限定语出场。
稀释幅度本身是未知量,预留的量化手段是去激光消融:把 LiDAR 分支从输入中拿掉(或置零),重跑同一批路线,比较带/不带激光时 UE4→UE5 的 变化。两条曲线之差就是稀释的量。这需要 AutoMoT 推理支持关断单模态,工程上可行,排期上属于可选深挖项。
三道防线共享同一个原则:科学不回避偏差,把偏差写进章程、标进每张表。无法消除的混杂变量,最差的处理是假装它不存在,次差的处理是含糊带过,正确的处理是限制它的作用范围、标注它的存在、预留量化它的路径。
可视化目录里每帧除画面外还有一份元数据:转向(steer)、油门(throttle)、刹车(brake)、速度(speed)、指令(command)。画面回答”模型看到了什么”,这五行数字回答”模型想做什么、车实际怎么动”——两者逐帧对齐(帧号一致),就是一条完整的诊断链。
最常用的一条判据:
模型输出了满油门,车却纹丝不动——决策意图正常,障碍感知失败(看不见挡住自己的东西),或权重层输出损坏。它的对立签名是”模型不想走”(throttle≈0):那是决策问题,不是感知问题。同为”车不动”,两个签名指向完全不同的故障层。
AutoMoT 发布 ckpt(2026-05-17)里 BEV 投影层叫 transfuser_proj.*,当代代码叫 bev_encoder_proj.*——跨代际改名、shape 完全一致,流式 loader 按名匹配不上就静默跳过,该层保持随机初始化。症状:64 个 BEV token 全是垃圾,车满油门顶死障碍物,18 条路线 meanDS 27.5 vs 官方 87.34。定位靠的正是 viz metadata 逐帧统计:throttle≈1 且 speed≈0 主导,先把嫌疑钉在”感知/权重层”,才逐层查到 ckpt 装载。教训沉淀为项目规则:ckpt 装载后必须把 missing/unexpected keys 当硬错误查,不能只看”加载没报错”。
SimLingo 微调权重评测全部超时,metadata 签名是另一种:throttle 1.0 + steer −1.0 持续饱和、中位速度 0.01——t=100 与 t=500 的控制输出完全相同,说明网络行为已坍缩到不动点。这个签名把嫌疑从”偶发卡死”直接指向”权重层行为崩溃”,是整个排除法证据链的入口。
可视化在这里不是装饰,是诊断仪。