Reproducing the UE4 Baseline: Matching Official Scores, Part 1
Bench2Drive(NeurIPS’24 D&B Track)是 CARLA 0.9.x 生态里闭环评测的标准基准:220 条短路线、44 种交互场景类型,版本钉死 v0.0.3。它不是单一程序,而是三个独立仓库拼成的流水线,各自职责清晰。
220 条路线的 XML 文件,每条定义起点/终点位姿、途经的城镇(Town01–Town15 中的 12 个)以及触发场景的路点。它只回答”考什么”,不含任何执行逻辑。同一份 XML 是评分的锚——两份评测树里路线 XML 逐字节一致,分数才同源可比。
CARLA 官方的 ScenarioRunner 负责在路线上”埋雷”:事故车横路(AccidentTwoWays)、施工障碍(ConstructionObstacle)、行人穿行、对向车流等 44 类原子场景(atomic scenario)。它监听英雄车(hero vehicle)到达触发点,然后激活对应的 NPC 行为树。评测的”交互性”全部来自这一层。
leaderboard(排行榜评测器)是闭环主循环的拥有者:
run_step(),拿回控制量world.tick() 推进仿真(同步模式)三者拼起来才是一次闭环评测。版本钉死的意义:场景触发参数、罚分系数、路线集任何一处漂移,分数就不可横向比较——这是基准(benchmark)与普通 demo 的本质区别。
一条路线的评测是严格的同步闭环(synchronous closed-loop)。CARLA 的同步模式下,server 推进仿真前必须等到 client 调 world.tick(),帧间隔固定(CARLA 默认 s,即仿真 20 FPS):
解析路线 XML → 生成全局路径(RoutePlanner)
→ 加载城镇 + scenario_runner 布景(NPC、天气)
→ 循环:
传感器渲染 → agent.run_step() 推理 → 控制量下发 → world.tick()
→ 路线终止(完成 / 超时 / 犯规终止)→ 结算
同步模式的意义:仿真时间与现实墙钟解耦。模型推理再慢(SimLingo 实测只有 0.04–0.065 倍实时),仿真世界都会等它,物理不会”趁你思考时继续走”。这是闭环评测公平性的根基,也是墙钟爆炸的直接原因——仿真等模型,模型多慢墙钟就多长。
驾驶分(Driving Score, DS)是路线完成度与罚分系数的乘积:
乘法结构决定了 DS 的性质:单次严重违章的代价是非线性的。撞两辆车()比完成度少 10 个点疼得多。同时注意不在乘积里的项:低速行驶(MinSpeed)、让行类测试不进 DS 罚分乘积——它们影响的是别的指标或仅作诊断记录。
SR(Success Rate)是另一维度:DS 满分(100)的路线占比。一个”从不违章但经常卡死”的模型 SR 会很低,DS 却不一定难看——两个指标刻画的是不同失效模式。
CARLA server 跑在 Windows 宿主(原生进程),评测代码全在 WSL2。这套拓扑能工作,靠的是 WSL2 网络栈的几个具体事实。
WSL2 默认走 NAT 模式:WSL 虚拟机有自己的虚拟网卡,localhost 指向虚拟机自身,不是 Windows 宿主。宿主 IP 要从默认路由里现场解析:
ip route show | awk '/default/{print $3}' # WSL 视角的 Windows 宿主 IP
这个地址会随 WSL 重启变化,所以硬性规则是禁硬编码 localhost,也禁硬编码 IP——每次启动现场解析。Windows 防火墙侧需放行对应端口入站。
Windows 的 winNAT 服务会动态保留一批 TCP 端口段(netsh int ipv4 show excludedportrange tcp 可查)。绑定落在保留段里的端口会直接 WSAEACCES(权限拒绝),CARLA server 未捕获该异常即崩(0xe06d7363)。
本项目实测被吞过:最初约定的 2000–2002/2010–2012 段恰好落在保留段 1921–2020 内,CP0 时改约定为 UE5 2021–2023 / UE4 2031–2033。更麻烦的是保留段会漂移:重启后旧段消失、新段出现(实测出现过 2269–4289、2756–3355、1937–2036 等段)。因此纪律是:
netsh 排除段,再查显卡Bench2Drive 的 leaderboard_evaluator 默认假设自己是 CARLA server 的”家长”:启动时去拉起 CarlaUE4.sh,评测结束负责杀掉。在 WSL 里这条路径直接不存在——不打补丁,启动即 FileNotFoundError 退出。
if os.environ.get("B2D_EXTERNAL_SERVER") == "1":
# 外部 server 模式:跳过自起逻辑,
# 直接连 WSL 外宿主上已运行的 server(端口 2031)
pass
else:
# 官方原逻辑:自起 CarlaUE4.sh —— 一字未删
...
对 vendored(随仓携带)的第三方代码打补丁,有三条经验原则,这条补丁全占了:
else 分支。上游更新对照 diff 时一眼看清改了什么,也随时可切回原路径做对照实验。ensure_server 看门狗——它本来就有选卡轮询和显存校验能力,evaluator 内那套简陋的自起逻辑反而是故障源。本地补丁(commit c82e5b8)按此口径只含接线与子集支持,不动评分语义——这是 CP1 判据里”Bench2Drive v0.0.3 钉死”一项能通过的前提。
CARLA 的崩溃不是小概率事件,是常态。harness(评测挽具)的核心设计是把崩溃当作必然输入,套三层保险:
attempt = 0
while attempt <= 3:
┌─ 第一层:ensure_server ─────────────┐
│ server 活着?否→重启(选卡轮询+校验) │
│ 重启也失败 → exit 2,人来看 │
└─────────────────────────────────────┘
┌─ 第二层:跑 leaderboard evaluator ──┐
│ rc == 0 → 全部完成,跳出循环收工 │
└─────────────────────────────────────┘
┌─ 第三层:rc != 0 ───────────────────┐
│ attempt += 1,回到第一层续跑 │
└─────────────────────────────────────┘
续跑而非重跑:崩溃只丢当前在跑的一条路线——result.json 按已完成路线落盘,重启后从断点继续,已有成绩全部保留。这把单次崩溃的代价从”整批重来”压到”单条重跑”。
触发条件的盲区:这套自愈只在进程退出后触发。它假设失败必然表现为进程死亡——进程活着但停止推进(死锁、挂起)不在覆盖范围内。识别”活着但停了”需要另一类机制:对输出做心跳检测(看日志是否还在写),而不是看进程在不在。
CARLA server 用 -graphicsadapter=N 指定渲染显卡。这个整数索引不是稳定标识。
-graphicsadapter 的编号来自 DXGI(DirectX Graphics Infrastructure)的适配器枚举顺序,而枚举顺序受驱动加载次序、会话类型(本地/RDP)、虚拟显示适配器注册状态影响。实测事实:
=2 昨天选中 RTX 5090,今天选中 Microsoft Basic Render Driver(软渲染),server 直接崩(访问冲突 0x18)=0 才是 5090结论:索引只能当候选,不能当判据。
ensure_server 按固定顺序轮询候选索引 [0, 2, 1, 3](经验排序:0 和 2 最常命中 5090),每个候选起 server 后做一次显存增量校验:
baseline = gpu_used_mb() # 起 server 前采样
launch(graphicsadapter=candidate)
sleep(wait_seconds)
delta = gpu_used_mb() - baseline
if delta >= 4096: # 显存比基线涨 4GB 以上
return candidate # 才认定选对了卡
用”显存涨了多少”而不是”server 进程在不在”做判据的原因:选错卡时进程往往也活着,只有显存占用能区分”真在 5090 上渲染”和”崩了/落在虚拟卡上”。CARLA Shipping 版不写日志,nvidia-smi 的显存读数是少数可靠的外部观测手段。
注意双实例场景下这个校验会变钝:另一侧 server 的显存会掩盖本侧增量,因此 launch 时刻的增量校验(而非绝对水位)才是主判据。
type=boolleaderboard 的命令行参数里有一个 Python 社区最著名的坑:
parser.add_argument('--resume', type=bool) # 反模式
type 是 argparse 对命令行字符串做的转换函数。bool 作为转换函数,语义是 Python 的真值测试——任何非空字符串都是 True:
bool("True") # True
bool("False") # True ← 非空字符串!
bool("0") # True
bool("") # False(但你几乎传不出空串)
于是 --resume False 解析结果是 True。官方评测脚本恰好恒传 resume=False,效果是永远在续跑:首轮评测时 evaluator 去找根本不存在的 result.json 试图续跑,行为不可预期。
# 写法一:flag 式,传了就是 True,不传就是 False
parser.add_argument('--resume', action='store_true')
# 写法二:显式字符串选择
parser.add_argument('--resume', choices=['true', 'false'], default='false')
本项目不打补丁改 argparse(保持上游原样),而是在调用侧立规矩:
--resume 参数——不传就没有解析错误的机会result.json 里已有完成路线的记录,才显式传 --resume True这条规矩的价值在崩溃自愈链路里放大:harness 每次重启 evaluator 都要做续跑判定,如果靠人工记忆”这轮是第几次”,五起事故里至少有两起会变成成绩损失。
冒烟子集的目的不是”随便跑几条看看通不通”,而是用小子集外推全量墙钟与 DS 量级。外推要可信,抽样结构必须对上总体的结构。
Bench2Drive 220 条路线在 12 个城镇间的分布极不均匀:Town12 有 104 条、Town13 有 61 条,两个大地图(LargeMap)合计占 75%。而大地图单条墙钟 35–80 分钟,小图只要几分钟。用”全量平均 × 220”或”随手抽 20 条”都会严重失真——官方自带的 split_xml.py 只做顺序等分,无分层,不可用。
自建抽样脚本 t12_select_split20.py,两条规则:
分层均值估计量为
其中 是城镇层, 是该层路线总数, 是抽中样本的平均墙钟。每层用层内均值外推、再加权汇总,大地图的重价就不会被小图的便宜样本稀释。
split20 结果:meanDS 92.54,19/20 完成,总墙钟 23159 秒(6.43 小时)。三种外推口径互相印证:
| 口径 | 220 全量估计 |
|---|---|
| 分层城镇均值 | 63.7 h |
| 长度归一回归 | 62.7 h |
| 朴素均值 ×220 | 68.3 h |
三法同指 ~63h,超 36h 阈值,提速成为硬需求。注意 split20 的 meanDS 92.54 偏易——它的合法用途是量墙钟、测链路,不是预估全量分数。单条最长 81 分钟(事故绕行卡死的长尾),长尾来自场景动力学而非抽样偏差,这一点分层设计给不了免疫。
FASTPATH(kv-cache 化生成)。SimLingo 是 VLM 逐帧生成驾驶决策文本,profiling 显示生成占帧耗时 93%。官方路径每次生成重复计算全部前缀的注意力——自回归第 个 token 的注意力需要对前 个 token 的 key/value 重算,单帧生成长度为 时注意力开销是 。kv-cache 把历史 token 的 key/value 张量缓存复用,每步只对当前 token 算一次注意力,增量开销降为 。配套措施:复用驾驶 query 的 cache、跳过 lm_head 死代码。
LoRA 权重合并(merge_and_unload)。LoRA(Low-Rank Adaptation)把增量权重分解为低秩矩阵乘积:
其中 是冻结的基座权重,、,秩 。推理时每次前向多走一条 支路并做加法。merge_and_unload 预先把增量并进基座:
前向只剩一次矩阵乘,支路开销归零。效果:模型段耗时 1555 ms → 543 ms,离线基准 2.86×。
提速改变了数值路径(bf16 kernel 累加顺序、merge 舍入),非 bit 等价。采纳与否不能靠”理论上差不多”,要走闭环闸门(DS gate):同一 split20 子集、原路径与 fast 路径各跑一遍。
| 臂 | DS | 墙钟 |
|---|---|---|
| orig | 92.54 | 22363 s |
| fast | 94.39 | 9965 s(2.24×) |
fast 不降分(噪声带内,甚至略高)→ 采纳。注意闭环评测的噪声不是高斯噪声,是行为混沌:单条路线 game time 双向漂移,28330 从 Completed 翻成 TickRuntime,25845 反而从 25.2 涨到 70.5。闸门看的是聚合 DS,不是逐条一致。
FASTPATH 与官方路径有 bf16 级差异,对外引用绝对 DS 必须声明评测路径;但 UE4/UE5 两侧用同一路径,行为扰动同源,对比实验的内部效度(internal validity)不受影响。再叠加双实例并发(两台 server 对半分路线、TM 端口错开 8000/8010,约 1.45×),合计 ~3.2×,220 全量 22.5 小时收官。
220 条里 13 条非 Completed。leaderboard 的路线状态(route status)本身就是第一层归因工具——不同终止原因指向不同失效机理:
| 状态 | 含义 | 条数 |
|---|---|---|
| TickRuntime | 路线预算耗尽(每米给定秒数,约 200 s/500 m 量级) | 9 |
| AgentBlocked | 车辆长期静止被判堵死,罚分终止 | 2 |
| Deviated | 偏离计划路线 | 2 |
9 条 TickRuntime 里,场景类型高度集中:AccidentTwoWays ×3、ConstructionObstacle、StaticCutIn、ParkingExit、MergerIntoSlowTrafficV2……共同点是静止或慢速障碍。行为模式一致:SimLingo 遇到事故车/施工障碍倾向停车等待,其卡死恢复机制(creep,低速蠕动脱困)在 200 秒路线预算内不足以完成绕行。25845 的逐帧分析佐证:车辆物理卡死后 force_move 兜底也无效,属于耗尽预算而非推理崩溃。
两条 Deviated 均发生在高速/车流场景,是另一类失效(高速下横向控制偏航)。
三个正交检查让结论站得住:
归因到”模型弱点”而非”评测噪声”,是把复现工作从”对齐分数”升级到”理解模型”的关键一步。