Environment Setup: Two CARLA Servers on One Machine, Part 1
把 CARLA server 塞进 WSL2 的诱惑很大:全 Linux 一条栈,不用跨系统。但两代引擎各有各的死法:
EXCEPTION_ACCESS_VIOLATION(#9439)、黑窗秒退(#9409)的案底——原生尚且如此,套一层 WSL2 的 Vulkan 转译只会更脆。评测一跑几小时,渲染层崩一次就作废一批数据。所以切分原则是一句话:重图形给 Windows,重编译给 Linux。server 是重图形负载,放 Windows 原生;训练、评测客户端是重编译负载,放 WSL2。
WSL2 的 GPU 支持是驱动透传:Windows 宿主装好驱动后,WSL2 内核通过 dxgkrnl 把 CUDA/DX12 能力映射进去,WSL 里只需 CUDA toolkit 的用户态库(conda/pip 自带)。如果在 WSL2 里再装一份 Linux 版 NVIDIA 驱动,反而会覆盖透传链路,是最常见的自残操作。本项目宿主驱动 577.00 / CUDA 12.9,WSL 侧 torch 直接看到 RTX 5090(sm_120)。
跨系统操作没有免费午餐:WSL 里所有”在 Windows 侧做事”的动作——起 server、杀进程、查显存——都得走 powershell.exe 互操作调用。它慢(每次调用百毫秒级进程开销)、会偶发管道挂死(必须加 timeout 兜底),还引入 PowerShell 自身的转义陷阱(如 taskkill //IM 在 ps 里语义不同,得换 Stop-Process -Name)。这些是选型的固有成本,写脚本时按”每次 interop 都可能挂”来防御。
WSL2 是一台跑在 Hyper-V 里的真虚拟机,有自己独立的 ext4 虚拟磁盘(vhdx 文件)。在 WSL 里看到的 /mnt/c 并不是”同一个文件系统的另一个视图”,而是通过 9P 协议(Plan 9 文件协议)跨虚拟机边界访问宿主 NTFS 的文件服务。
9P 路径上每次 open/read/stat 都是一次跨 VM 的 RPC:协议开销大、小文件和元数据操作尤其慢、也没有 Linux 页缓存的高效利用。顺序读一个大 zip 还能看,一旦换成”每秒几千次小文件读”的负载,吞吐直接塌方。
图像数据集的 DataLoader(数据加载器)是最典型的小文件随机读负载:每个 step 要从磁盘读几十张 PNG/JPG,解码、增强、组 batch,喂给 GPU。GPU 算一个 step 可能只要 200ms,如果磁盘供给跟不上,GPU 就在等数据——表现为 nvidia-smi 里利用率周期性掉到 0%,训练墙钟时间成倍拉长。数据集放 /mnt/c,等于给 5090 拴上一条 9P 的鞋带。
正确落位:数据一律放 WSL2 的 ext4(如 ~/data),下载解压也在 Windows 侧做完再一次性搬进 ext4,避免大量小文件走 9P。
vhdx 所在盘实测剩约 582GB,全部数据预算登记后约 212GB(环境、权重、数据集、ckpt、实验产物),水位约 36%。两条硬线(监控对象是 vhdx 所在盘的 df):
以及一条场景红线:Bench2Drive Full 全集约 4TB,单机装不下也无需装,禁止一次性拉全量,按需下载子集。
两个模型骨架几乎一样(py3.10 + torch 2.7.1 + cu128 + carla 0.9.15 + flash-attn 2.8.3),直觉上该合住一个 conda env。合不了,卡点在一个包上。
| 环境 | transformers 版本 | 原因 |
|---|---|---|
mysim-simlingo | 4.46.3 | SimLingo 官方锁定;InternVL2 的 remote code 在该版本上验证过 |
mysim-automot | 4.57.3 | AutoMoT 用 Qwen3-VL 骨干,qwen3_vl 模型类只有新版本才有 |
一个环境同一时刻只能装一个 transformers 版本,这是 pip 的单版本约束,无解。而 VLM(视觉语言模型)项目对 transformers 极其敏感——remote code、tokenizer 行为、generation API 都在版本间漂移——降级或升级任何一个模型都不是”改个版本号”的事。
更深的坑:SimLingo 官方 environment.yaml 锁的是 py3.8.18 / torch 2.2.0 / transformers 4.46.3。这份文件在本机直接不可用:
no kernel image;carla==0.9.15 的 PyPI wheel 也没有适配这套组合的余地。结论:官方环境文件只当版本线索用(transformers 4.46.3 这个钉版就是从它继承的),环境本身必须围绕硬件约束自建。
拆 env 的成本是磁盘:两套 torch + CUDA 用户态库,每个 env 几个 GB(第二个 env 靠 pip 本地缓存秒装)。合 env 的成本是依赖地狱:一次 pip install 把 torch 顶到新版本、连带 cu12/cu13 的 nvidia-* 包共享目录互删(本项目实测踩过:卸 cu13 包连带删掉 cu12 的 libcudnn/libnccl,只能 --force-reinstall --no-deps 全量修复)。不对称很明显:拆 env 花磁盘,合 env 花命。
装环境时 Python 版本没有自由度,它是一串硬约束的最末一环。本项目的推导链:
carla==0.9.15 只发布了 cp27 / cp37 / cp38 / cp39 / cp310 的 wheel(cp310 即 CPython 3.10 的 ABI 标签)。要在装 torch 2.7 的同时 pip install carla==0.9.15,Python 版本只剩 3.10 一个交点。carla-ue5-api==0.10.0 在 PyPI 上只有 manylinux 的 cp311 / cp312 wheel(无 Windows 版;Windows 下只能用 zip 包内置的 PythonAPI wheel)。UE5 侧因此用 py3.11。三条链收束出两个版本:模型环境 py3.10,仿真环境 py3.11。没有”我习惯用 3.12”这种选项。
wheel 文件名里的 cp310、cp311 是 ABI 标签:标明这个预编译包内的 C 扩展(.so/.pyd)链接的是哪个 Python 版本的 ABI。carla 包的核心是对 C++ 仿真客户端库的 pybind 封装,属于带二进制扩展的包,必须 ABI 精确匹配——cp310 的 wheel 装不进 py3.11,强行装会在 import 时崩。纯 Python 包(标签 py3-none-any)才跨版本通用。
这也是”carla 与 carla-ue5-api 永不装同一环境”这条硬规则的同族原因:两套包各自封装了互不兼容的 C++ 客户端扩展,同居一个 site-packages 必炸。
装环境前先查 wheel 清单,而不是装完再撞墙:
# 查 PyPI 上某个版本的所有发布文件及其标签
curl -s https://pypi.org/pypi/carla/0.9.15/json | python3 -c "
import json,sys
for f in json.load(sys.stdin)['urls']:
print(f['filename'])
"
输出里的文件名直接告诉你支持哪些 Python 版本、哪些平台。依赖闭包里最”老”的那个包(这里是 carla)决定整个环境的 Python 天花板,以它为锚点向下推其余版本,顺序不能反。
默认网络模式下,WSL2 不是”Windows 里的一个进程”,而是跑在 Hyper-V 虚拟交换机后面的虚拟机,有自己的虚拟网卡(eth0)和独立的 RFC1918 私网地址(如 172.20.x.x)。Windows 宿主在这个私网里的地址是 172.20.x.1,同时充当 WSL 的默认网关——这就是 NAT(网络地址转换)模式:WSL 访问外网由宿主做地址转换。
关键推论:WSL 和 Windows 各有各的 localhost。在 WSL 里连 localhost:2021,到达的是 WSL 这台 VM 自己的回环口,而 CARLA server 监听在 Windows 宿主的协议栈上——连接永远失败。这不是 CARLA 的问题,是网络命名空间隔离的必然结果。
WSL2 确实提供 localhost 端口转发,但方向只有 Windows → WSL:在 Windows 浏览器里访问 localhost:8000 能命中 WSL 里起的服务。反向(WSL 里访问 localhost 命中 Windows 服务)没有这个机制,只能靠 mirrored 网络模式(Windows 11 22H2+ 可选,但有别的兼容性问题)或直接用宿主 IP。本项目用后者:WSL 侧所有客户端连 172.20.x.1:2021。
WSL 私网段由 Hyper-V 在每次 WSL 启动时分配,重启后宿主 IP 会变。把 172.20.16.1 写进配置文件的后果:某天重启后所有客户端集体连不上,排查半天发现是 IP 漂了。所以纪律是”启动时动态获取”(具体命令见配套代码摘录),一处实现、全局调用,写进 AGENTS.md 成为硬性规则。
ip route + awkip route show | awk '/default/{print $3}'
# 输出示例:172.20.16.1
拆开看:
ip route show 打印内核路由表。WSL2 里的典型输出:
default via 172.20.16.1 dev eth0 proto kernel
172.20.16.0/20 dev eth0 proto kernel scope link src 172.20.22.231
/default/ 是 awk 的模式匹配,只处理含 default 的那一行——即默认路由。print $3 取第三个字段,也就是 via 后面的默认网关地址。在 WSL2 的 NAT 拓扑里,默认网关就是 Windows 宿主在虚拟交换机上的地址。所以”默认网关 = 宿主”这条等式成立,ip route 顺势成了拿宿主 IP 最稳的一招:不依赖环境变量、不解析 /etc/resolv.conf(某些发行版方案)、不需要装任何包。
真实代码里它被收进一个函数,所有脚本统一调用:
# tools/server_watchdog.py
def host_ip():
return subprocess.check_output(
"ip route show | awk '/default/{print $3}'",
shell=True).decode().strip()
客户端连接、watchdog 探活、评测脚本拼 --host,全部走这一个函数。硬编码宿主 IP 被明确列为禁区——它是环境状态,不是常量。
本项目实测过一个相邻的坑:conda run -n env python3 - <<EOF 这种写法里,stdin 的 heredoc 会被 conda run 吞掉,python 收到空输入静默 exit 0——不报错,是最坏的一种失败。等就绪脚本一律写成 .py 文件再执行,不走 stdin。
CARLA 的 TrafficManager(TM,交通管理器)是 server 内的一个独立模块:接管背景车的油门、刹车、转向,让它们按交规巡游。它通过单独的 TCP 端口对外提供 RPC 服务(设置车距、忽略红灯概率等),默认端口 8000——注意它和 server 的主 RPC 端口(如 2021)是两个不同的监听口。
同机起两台 server(UE5 2021 段 + UE4 2031 段),各自内嵌一个 TM。如果不显式指定 TM 端口,两个 TM 都去 bind 8000:先起的占住,后起的 bind 失败或行为依赖实现。客户端调 client.get_trafficmanager() 时拿到的端口若指向了”对面那台 server”的 TM,于是出现诡异现象:
这类 bug 最毒的地方在于没有任何报错——server 活着、帧在渲染、日志在写,错的是语义。
修复是给两侧显式错开:UE5 用 TM 8000,UE4 用 TM 8010(双 UE4 实例并发时再按 side 错开),所有起 server 与接 client 的脚本同步改。
教训可以推广:同机多实例场景下,一切”默认端口”都是共享单点。server 主端口错开了不算完,TM、streaming、以及任何组件的隐式监听口都要列入端口规划表,逐个指定主人。
宿主机物理内存 64GiB,系统可见 63.4GiB。WSL2 的 .wslconfig 里 memory 字段是 WSL 虚拟机的内存上限。把它看成一个简单的不等式:宿主可支配内存必须盖住宿主侧最坏情形的峰值需求。
其中 是 .wslconfig 的上限, 是宿主侧最坏情形需求。最坏情形取:Windows 系统本身 6–8GB,加一台 CARLA server 8–16GB(UE5 server 实测 Epic 720p 占宿主 RAM 约 10GB,取区间上限留余量):
注意规划期就算出了 56GB 必炸(7.4 < 14 一眼可见),实测只是验证算术:56GB 口径下 WSL 压 30GB 时宿主空闲仅 2.9–3.2GiB,直接击穿 6GB 门禁线。
.wslconfig 的语义容易误读:
[wsl2]
memory=32GB # WSL 可占用的上限,不是开机就预占
processors=16
swap=16GB
WSL 平时用多少占多少(空闲时宿主内存还是自己的),只有 WSL 侧真正吃满 32GB 时宿主才开始贫血。所以门禁测试的正确姿势是”卡住最坏情形”:人为把 WSL 内存压满,同时起 server,盯宿主空闲——而不是看平时的空闲值。改完 .wslconfig 必须 wsl --shutdown 整 VM 重启才生效,热改无效。
大型下载本身是工程问题,本项目的策略分层:
| 资源 | 首选通道 | 回退 |
|---|---|---|
| HuggingFace 数据集/权重 | HF_ENDPOINT=https://hf-mirror.com | 官方源 |
| pip / conda 包 | 清华 TUNA 镜像 | 官方索引 |
| torch cu128 系列 | 只能官方索引 | —— |
| GitHub release 大文件(CARLA zip 等) | 代理加速 | 官方 |
有一个实测的例外要记牢:TUNA 没有 cu128 镜像(同名路径 404),aliyun/sjtu 同路径也不可用。torch cu128 全套约 3.3GB 只能走 download.pytorch.org 官方索引,直连实测约 1.4MB/s,一次安装约 40 分钟。省时间的正确姿势不是换源,而是 pip 本地缓存跨 env 复用——第二个环境装同样的 torch 栈直接命中缓存,秒装。
HF 镜像站本身不慢,慢的是单连接:实测单连接只有约 1MB/s。解法是开 hf_transfer——Rust 写的多线程分块下载后端:
pip install hf_transfer
export HF_HUB_ENABLE_HF_TRANSFER=1 # 让 huggingface_hub 走多线程下载
同一镜像站,吞吐从约 1MB/s 提到约 40MB/s,40 倍差距全部来自连接并行度,与带宽无关。下几十 GB 的数据集时,这个开关是一小时与一整天的差别。
Windows 侧下载 + 解压(/mnt/* 走 9P 吞吐差,大文件操作放原生侧快得多),校验过立即删 zip,不留瞬态。校验方式分档:官方公布 sha256 就校验散列;没公布就以文件大小 + 启动冒烟替代。镜像失效时回退官方源并把可用性登记进已知坑——下载通道的健康状况是会随时间变的运维事实。