近年来,智能驾驶技术的演进速度明显加快,车辆对周围环境的实时感知与决策能力,已成为决定整个系统性能上限的关键因素。无论是目标检测、语义分割,还是多传感器融合,这些核心感知模块的背后,都离不开大规模深度学习模型的训练与部署。随着自动驾驶等级从L2向L3乃至L4迈进,模型复杂度与数据量呈现指数级增长,这对底层计算平台——尤其是算力、内存带宽、并行处理能力和能效比——提出了前所未有的高要求。
目前行业内主流的高性能计算平台大多基于高速GPU集群,这类方案在内存容量和带宽上具有显著优势,能够支撑大批量数据的分布式训练,也为更复杂的模型架构创造了条件。在此背景下,在典型的智能驾驶场景中——例如高精度目标检测、点云感知以及多模态融合感知任务——对不同算力卡进行全面的性能对比测试,成为一项极具实际价值的工作。它能为客户在选择算力资源时,提供真实可信的数据支撑。
先来看本次测试的基础配置:环境方面采用mmdet3d、mmcv、flash-attn、nuscenes-devkit,以及torchrun分布式训练框架;算力部分涉及两种不同的算力卡,分别定义为机器一和机器二;模型方面选择了maptr、sparsedrive、qcnet和Gaussianformer四个有代表性的小模型;数据集则使用了nuScenes和Agoverse。
技术架构与完整流程
整个对比测试基于PAI DSW实例完成。在DSW机器上进行训练实验,整体的训练步骤大致如下(以maptr模型为例):
第一步,选择合适的DSW镜像,核心原则是确保Python和CUDA版本与模型的要求相匹配。这里我们直接选了autodrive镜像,里面已经预装了必要的mmcv等依赖,可以减少不少环境配置的麻烦。
接下来,根据模型在GitHub上的官方文档安装剩余依赖。这一步往往是整个过程中最磨人的——版本冲突、缺少某个库、编译失败……碰到报错就换一个版本重新试,反复几次才算把环境清理干净。
创建DSW实例时,需要指定一个CPFS挂载点。模型文件和数据集就放在这个挂载的CPFS里,实现持久化存储,避免每次训练都重新下载。
最后,执行训练命令,通过日志记录下训练时间和吞吐量。
下面这张表列出了本次测试涉及的四个模型,它们都属于目标检测和感知领域的小模型,很适合智能驾驶的应用场景:
| 类型 | 模型 | 主要依赖 | 官方地址 |
| 感知-地图构造 | maptr v1 | mmdet3d 0.17.2 / mmcv 1.4.0 | https://github.com/hustvl/MapTR |
| 感知-端到端 | sparsedrive | mmcv 1.7.1 / flash-attn 2.3.2 | https://github.com/swc-17/SparseDrive |
| 感知-预测 | QCNet | mmdet3d 0.17.2 | https://github.com/ZikangZhou/QCNet |
| 感知-目标检测 | GaussianFormer | mmcv 2.0.1 / mmdet3d 1.1.1 | https://github.com/huang-yh/GaussianFormer |
实际遇到的坑与解法
环境依赖冲突:一个常见但棘手的问题
在实际测试中,环境依赖冲突几乎是绕不过去的坎。以maptr模型为例,安装mmcv=1.4.0时,build wheels直接报错了。
根据mmcv官方文档仔细排查后发现,1.4.0这个版本的mmcv属于较老的依赖,它要求torch版本必须在1.9.1到1.10.0之间。问题在于,DSW官方的镜像中已经很难找到满足这个要求的低版本torch了。如果强行卸载torch安装低版本,又会引发大量镜像自带库的兼容错误。而mmcv 1.4.0又是maptr的硬性依赖。
最终针对maptr的解决方案是:选择带py38+cu111版本的Ubuntu裸镜像,重新安装合适的torch版本,再装mmcv。虽然折腾,但确实有效。
另一个典型案例是在sparsedrive模型测试中碰到的flash-attn安装问题。安装flash-attn=2.6.1时,setup阶段再次失败,错误信息看起来像是CUDA版本不对。但查了flash-attn的官方文档,又显示它和torch 1.2.3是兼容的。深入排查后发现,根因出在Python 3.8的torch/_utils_internal.py文件里——它调用了transformers库的一个函数,而这个函数在高版本的transformers中已经被废弃了。只有该模型指定的transformers 4.30.1还在使用这个函数。缺少这个函数,就会导致"undefined symbol"的错误。
解决方法是降级transformers库:直接执行pip install transformers==4.30.1,问题即可解决。
源代码适配:不同硬件带来的差异
不同算力卡之间的底层编译环境差异,也会给模型测试带来困扰。处理这种问题,通常需要对源代码做一些适配。
local_rank处理就是一个典型的例子。local_rank是用来指定GPU序号的参数变量,在多卡并行训练时必须传入默认值。目前大部分模型——包括本次测试的四个模型——都默认以Nvidia算力作为环境,因此会在启动脚本中硬编码传入参数--local_rank。但不同的算力卡调度逻辑不一样,可能使用不同的GPU序号变量。
比如sparsedrive在机器一上执行启动命令时,就会出现变量错误。解决方式是在训练脚本中修改代码:parser.add_argument('--local-rank', type=int, default=0)。
再来看NCCL P2P的问题。DDP多卡并行训练时,默认会使用多卡之间的点对点通信,多张卡之间直接共享模型权重和数据集。点对点通信会利用卡间的内部通信网络,不经过主机内存中转。
在实际测试中,偶尔会出现rank0和rank1参数不一致的错误:rank1没有读取到rank0的状态,导致多卡通信失败。这时候需要禁用点对点通信来解决。具体做法是在训练脚本中加入EXPORT NCCL_P2P_DISABLE=1这个参数。
需要注意的是,虽然P2P被禁用后多卡状态共享不再通过高速网络,训练效率会略有下降,但至少能让训练跑起来。
性能优化:让算力卡真正释放潜能
在对比测试中,我们可以采用一些优化手段,让算力卡充分发挥算力优势。下面从CPU处理、应用层优化和外部挂载三个方面来聊。
CPU处理加速
在maptr模型测试中,我们发现了一个很有意思的现象:算力卡的显存使用率和显存占用量都比较低。监控数据显示,训练进程大量时间在CPU核心上执行,GPU上的运算很快就结束了,然后一直在等CPU处理完才能继续。
原因在于,maptr属于小模型,本身的模型计算节点数远少于大模型。根据源码分析,GPU主要负责模型的梯度计算和参数更新,而CPU要负责读取图片数据做预处理,生成tensor。监控结果清晰地表明,CPU的处理时间成了瓶颈,GPU只能干等着。这种情况下,算力卡自然无法发挥全力。
针对这个痛点,我们引入了两种优化手段:
进程池是一个很实用的工具。它通过预先创建一组工作进程,把多个任务分配给这些进程并发执行,从而充分利用多核CPU资源。用进程池的好处在于,可以避免频繁创建和销毁进程带来的性能开销,因为池里的进程是复用的。对于CPU计算要求高的小模型,进程池的加速效果非常明显。进程数一般设置为CPU核心数:
from multiprocessing import Pool
def image_process(image):
...
return tensor
if __name__ == '__main__':
with Pool(processes=n) as pool:
results = pool.map(image_process, image_list)
print(results)共享内存是另一种高效的进程间通信机制。它允许多个进程访问同一块内存区域,数据交换时不需要在内核与用户空间之间频繁复制,通信开销极低。多进程配合共享内存使用,可以显著加快CPU处理速度。共享内存的大小一般设置成实例的分配内存即可;但如果数据占用量很大,需要酌情增加。
Torch应用加速
PyTorch自身也提供了一些内置的加速方法,能够在数据加载和模型计算层面提升效率。
pin_memory这个参数的作用是将数据加载到page-locked内存中。这种内存不会被操作系统交换到磁盘,因此可以更高效地通过PCIe总线传输到GPU,让GPU直接异步访问。这样一来,从CPU到GPU的数据拷贝速度会明显加快。使用方法很简单:
dataloader = DataLoader(
dataset,
batch_size=32,
shuffle=True,
num_workers=4,
pin_memory=True)num_workers则是指定用于数据预处理和加载的子进程数量。每个子进程负责从磁盘读取数据、做预处理,然后把处理好的batch数据传给主进程。多个workers并行工作,能够实现“GPU计算+数据加载”同时进行,训练效率自然就上去了。原理和上面的进程池类似,只是这个是torch自带的专用加速方法。num_workers的值一般设置为CPU核心数。
最后是torch.compile,这个功能值得多说两句。它使用torchdynamo对模型进行追踪,把模型的计算路径记录下来,忽略掉非必要的控制流,构建出一个可优化的计算图。然后对这张计算图进行算子融合、常量折叠、内存布局优化等一系列操作,最后把优化后的图转换成目标平台的高效代码。PyTorch默认使用Inductor编译器来执行,在后续的前向/反向传播中直接调用,完全绕过Python解释器,实现高速执行。
不过有一点需要警惕:compile无法编译非PyTorch的指令,如果你用了自定义的C/CUDA扩展,可能会遇到兼容性问题。
model = torch.compile(
model,
backend='inductor',
dynamic=False,
fullgraph=False,
)CPFS优化加速
在maptr模型测试中,我们还发现了一个很奇怪的性能差异:模型在写入checkpoint文件到CPFS时,耗时的差别非常大。在同一地域乌兰察布的CPFS A区,机器一写入时延高达2分钟左右,而机器二秒级就能写完。理论上同一套CPFS不应该有这么大的读写性能差异。后台监控显示,两台机器上的CPFS时延差了整整3倍。
后来分析了CPFS的实例情况,发现挂载的CPFS都位于乌兰A区,但根据智算CPFS官方文档,CPFS使用高速eRDMA网络通信,目前只有乌兰C区支持。初步判断就是可用区的问题,导致机器一无法使用高速网络读写,效率才这么低。
重新新建一个C区的CPFS后重新训练,写入延迟大幅降低,问题迎刃而解。
这里可以总结一下:要启用CPFS的高速eRDMA网络读写能力,CPFS的可用区必须选择乌兰C区。
最终测试结果
从loss曲线来看,机器一在100,000 step时基本收敛,机器二在180,000 step时基本收敛,收敛趋势保持一致,基本符合预期的加速比。
总结
如果在实际业务场景中需要训练感知检测类的小模型,可以从调度层、应用层、挂载区三个维度来优化性能:
模型训练过程中的tensor计算,会先进入底层CPU做数据预处理,然后处理完的tensor交由Cuda GPU算力做梯度计算,训练过程中产生的checkpoint会写入到挂载区CPFS。针对这三个阶段,分别对应三方面的优化:
- 调度层:创建进程池,提高CPU并行处理速度;增加共享内存,加快CPU读取内存的速度。
- 应用层:在torch DataLoader中启用pin_memory,加快数据传输速度;增加num_workers,提高并行计算速度;用compile加速模型计算。
- 挂载区:使用有高速读写网络的CPFS存储,并选择合适的可用区。
