1. 先厘清 FastOcc 到底在解决什么问题
三年前我第一次把占用栅格预测的网络跑通,盯着 200×200×16 的体素网格里慢慢浮出车道、路沿,还有远处那堵墙,那感觉像把一台老收音机调到正确频率——噪声里突然冒出一段清晰的信号。兴奋没撑过一周,实测帧率只有个位数,车端那点算力根本吃不下。后来我盯上 FastOcc 这类方案,原因很朴素:它不是再刷一个精度点的模型,而是围绕“让占用预测真正落进车端时间预算”重新做取舍的一整套工程打法。
这里说的占用栅格预测,行业里习惯简称 Occ,做的事是把自车周围的空间切成一格一格的立方体,判断每一格里有没有东西、是什么东西。它跟传统 3D 检测最大的区别在于,检测是“找出若干个体,每个体给个框和类别”,Occ 是“把整个空间填满,每格都要有结论”。好处是形状不规则的东西、被遮挡的东西、从没见过的东西,都能以“被占用”的形式表达出来;代价是计算量大得离谱,因为你要处理的不是一个稀疏的点集,而是一整个三维空间。
FastOcc 瞄准的就是这个矛盾:在保持可用的几何精度前提下,把单帧推理压进几十毫秒量级,同时让模型能真正部署到车端芯片或受限 GPU 上。适合读这篇的人分三类:正在做车端感知部署、被 Occ 模型拖到掉帧的工程师;刚入门 BEV 与占用预测、想搞清楚“为什么慢、慢在哪”的同学;以及手上只有中低算力平台、需要在精度和速度之间找平衡点的项目负责人。
需要说明,下面涉及的架构细节与参数,一部分来自公开方案的通用做法,一部分是我在实际项目中反复试出来的经验值,凡是补全的部分我都会点明是“常见实践”,不代表某个特定产品的实现。
1.1 从“看得见”到“测得准”,占用预测的定位在哪
先说清楚 Occ 在整个感知栈里的位置,不然后面所有的优化都缺少参照。相机图像进来,经过主干网络提特征,再做视角变换把 2D 特征投到 3D 空间,然后要么用稠密 3D 卷积、要么用稀疏卷积在体素上做特征聚合,最后每个体素接一个分类头(空/占用)和一个语义头(车、人、路面、植被等)。激光雷达点云则可以直接体素化后作为监督信号或输入分支。
这个链路里有两个天然的瓶颈。第一个是视角变换:把图像特征“抬”到三维,本质上是一个没有唯一解的病态问题,一个像素对应一条射线上的无数个点,模型只能靠深度分布去猜。第二个是三维卷积本身:体素网格的规模随分辨率呈三次方增长,你把边长精度从 0.5 米提到 0.4 米,体素数量直接涨到原来的近两倍。这两处一叠加,帧率就崩了。
我的判断是,视角变换决定了精度的天花板,三维特征处理决定了速度的地板。FastOcc 这类方案的重心明显压在后半段——它承认深度估计不可能完美,转而把工程力气花在“如何用更少的有效计算把体素填对”上。这个选型思路我认为是对的,因为深度估计的精度靠的是数据和训练策略,短期很难有量级突破,而稀疏化和算子优化是能实打实换来几倍加速的。
1.2 算力账本摊开算,慢到底慢在哪
很多人调优是凭感觉的,看到掉帧就去砍分辨率,结果精度崩了还没提速多少。我习惯先把账算明白。假设感知范围前后左右各 50 米,高度从地面下 5 米到地面上 3 米,体素边长 0.5 米,那么网格尺寸是:
- 横向:100 m / 0.5 m = 200 格
- 纵向:100 m / 0.5 m = 200 格
- 高度:8 m / 0.5 m = 16 格
总体素数 200 × 200 × 16 = 640,000。若特征通道是 32,单精度浮点存储,一个特征张量就是 640000 × 32 × 4 字节 ≈ 81.9 MB。听着还行,但训练时反向传播要缓存每一层的激活值,一个十几层的 3D 主干下来,光激活就能吃掉好几 GB 显存。推理时虽然不存梯度,但每一层的读写带宽同样是负担。
更关键的是,这 64 万个体素里,真正有东西的通常只占 3% 到 8%。也就是说,九成以上的计算是在一堆空的空气格子上做卷积。这就是为什么稀疏化是绕不过去的路。但稀疏化不是免费的午餐,它带来的是索引管理开销、算子碎片化、以及部署阶段的一堆麻烦,后文会详细说。
还有一个常被忽略的点:时序融合。单帧 Occ 对遮挡区域基本靠猜,加了历史帧之后精度能提好几个点,但代价是要缓存并对齐多帧体素特征,显存和带宽翻倍。FastOcc 在这块的思路是“只融合有用的部分”,具体做法我在第 2 章展开。
1.3 FastOcc 的三条主线:速度、精度、可部署性
我把这类方案的优化拆成三条并行推进的主线,理解了这个框架,你看任何一篇 Occ 加速工作都能快速归类。
第一条是空间稀疏化。核心是把稠密体素换成稀疏张量,只在非空位置做计算。这条线能带来数倍加速,但需要自定义算子和部署侧的配合。
第二条是特征维度压缩与多尺度设计。高分辨率只在浅层用,深层快速下采样,用一个“粗到细”的结构把感受野撑开而不用付三次方的代价。这条线对精度影响可控,属于性价比很高的优化。
第三条是部署友好性。包括算子是否能在目标推理引擎里找到对应实现、能否量化、有没有动态 shape、内存访问是否连续。很多实验室里跑得飞快的模型,一上板子就废掉,问题全出在这条线上。
三条线之间存在明显张力:越稀疏,部署越难;越激进降采样,小目标召回越差。FastOcc 的价值就在于它给出了一个明确的三线平衡点,而不是单点突破。
2. 整体架构设计,为什么这么选
聊完问题,进入设计层面。我先把一个常见误区摆出来:不少人以为 Occ 加速的关键是换个更轻的主干网络。实测下来,主干换小带来的收益有限,因为图像主干的耗时在整体链路里可能只占三成,大头在三维部分和视角变换。所以真正有效的做法是把三维部分的结构重做,而不是把二维主干砍掉。
2.1 主干与视角变换的取舍逻辑
图像主干这块,我倾向选一个“深度可分离 + 通道可裁剪”的结构,宽度系数按目标平台算力定,不追求 SOTA。原因是 Occ 任务对纹理细节的敏感度不如分类任务,它更依赖几何结构,所以主干的通道冗余度可以压得比较狠。实测把宽度系数从 1.0 降到 0.5,mIoU 掉大约 1.5 个点,但主干耗时能砍一半以上,这笔交易划算。
视角变换这里有两个主流方向,差别很大,我列个表对比。
| 方案类型 | 核心机制 | 优点 | 代价 | 适用场景 |
|---|---|---|---|---|
| 深度分布抬升 | 每个像素预测深度分布,按权重把特征撒到体素 | 结构简单,易并行 | 深度估计误差直接传导,远处精度差 | 中低算力平台,追求实时 |
| 查询式变换 | 用一组 3D 查询去图像特征里采样 | 几何一致性更好 | 采样算子重,训练慢 | 高算力平台,追求精度 |
| 射线采样 | 沿相机射线在体素里均匀取点再聚合 | 显存友好,可控制点数 | 点密度需要调参 | 折中方案,工程上常用 |
我在实际项目里多数时候选第一种或第三种。理由很直接:查询式变换的采样算子在部署时经常找不到高效实现,最后不得不退回稠密版本,前面的优化白做。而深度分布抬升虽然精度略低,但整个流程都是常规算子,量化、导出、加速都顺。
注意:选深度分布方案时,深度监督信号的构造方式会极大影响远处精度。别只用稀疏的激光点做监督,那样近处准远处糊,一定要补上基于邻域稠密化的伪监督,否则你会误以为是网络容量不够。
2.2 稀疏化怎么落地,别一上来就全稀疏
稀疏化这件事,我踩过的最大坑是“一刀切”。一开始我把整个三维主干全换成稀疏卷积,训练能跑,但收敛特别慢,而且小目标召回掉了不少。后来才想明白:浅层特征密度高,稀疏化的收益小、索引开销反而大;深层特征稀疏度高,收益才明显。
所以比较稳的做法是分层混合:前一到两级用稠密卷积,因为此时分辨率最高但通道最少,计算量还在可控范围;从第三级开始转稀疏,此时空间已经下采样过,非空比例降低,稀疏卷积的索引开销摊薄了。这个切换点不是拍脑袋定的,要看你的实际占用比例,一般建议在非空率降到 15% 以下时切换。
稀疏张量的组织方式也有讲究。常见的是坐标加特征的组合,坐标用一个形状为 (N, 4) 的整型张量存批次索引和 xyz,特征用 (N, C) 存。这种格式灵活,但每次卷积都要做坐标映射,开销不小。另一种是规则化的稀疏块格式,把 8×8×8 的邻域压成固定长度的向量,牺牲一点灵活性换取访存连续。我实测后者在车端平台上快 20% 到 30%,代价是边界处理麻烦。
2.3 时序融合的轻量化处理
时序融合是精度提升性价比最高的一招,但也是最容易把速度拖垮的一招。朴素做法是缓存过去若干帧的体素特征,按自车位姿变换到当前坐标系,然后拼起来或者做注意力融合。问题在于,自车一动,历史特征要做重采样,重采样是个逐体素的插值操作,非常吃带宽。
我的处理思路是“只对齐、不插值”。具体说,位姿变换只做整数格点的平移旋转,不做三线性插值,误差靠后续卷积层自己学回来。这么做精度会掉一点点,但耗时能降一大截。另一个技巧是历史帧降采样后再融合,比如当前帧用 0.5 米分辨率,历史帧用 1.0 米分辨率,融合时上采样对齐。因为历史信息本来就是补充遮挡区域用的,不需要那么高的空间精度。
还有一个隐性成本是缓存管理。多帧特征常驻显存,帧数一多就爆。我一般把历史帧限制在 2 到 4 帧,并且用环形缓冲区加固定地址分配,避免训练时反复申请释放造成碎片。
2.4 监督信号怎么配,别只盯着一个损失
Occ 的监督比检测复杂得多。我的配置通常是三项加权:体素级的二分类损失(有没有东西)、语义分类损失(是什么)、以及几何一致性损失(相邻体素的预测要平滑)。
这里有个经验:二分类损失用 focal 变体,语义损失用类别平衡的交叉熵,权重比大概 1 : 1。为什么?因为空体素占了九成以上,不加 focal 的话网络会倾向于全预测为空,训练前期 loss 降得飞快但 mIoU 一直是零。而语义损失只在非空体素上算,本身就不存在极端不均衡问题,用普通交叉熵加类别权重就够了。
还有一项容易被忽略的是射线方向的深度监督。如果你用深度分布抬升做视角变换,一定要给深度头单独的监督,并且这个损失的权重要在训练中期衰减,不然后期会压制几何精度的进一步提升。我一般用 0.5 起步,到总 epoch 的 60% 时线性降到 0.1。
3. 核心细节与实操要点,参数怎么定
这一章是干货密集区,我把几个反复调过的参数拿出来讲清楚推导过程,你可以直接照着改。
3.1 体素分辨率与感受野的参数计算
分辨率选择是整个方案的锚点,它决定了后面所有参数。我给一个从需求倒推的方法。
先定感知距离。城市工况下,自车要提前 2 秒做决策,按 60 km/h 算就是 33 米,加上制动余量,前方 50 米是底线。侧向因为要处理加塞和路口,一般取 30 到 40 米。这样一来,网格尺寸就定死在 100×100 到 100×80 米的范围内。
再定精度需求。要区分行人和非机动车,体素边长不能大于 0.5 米;要区分路沿和车道线,0.3 到 0.4 米比较合适。但前面算过,边长缩小 20%,体素数量涨 46%。我的经验值是城区用 0.4 米,高速用 0.5 米,分开训练两套权重,别指望一套通吃。
高度方向的网格数也要克制。地面以下 2 米到地上 3 米,5 米高度、0.5 米边长是 10 格,取 16 格是 2 的幂次,方便对齐和后续的池化操作,多出来的空间当作安全余量。
感受野的算法很简单,堆 k 层 3×3×3 卷积,感受野是 2k+1 个体素。要在最深层覆盖 40 米以上的上下文,按 0.4 米体素算需要 100 格,那就要堆到 50 层,显然不现实。所以必须靠下采样:每次空间尺寸减半,感受野翻倍。四层下采样之后,同样的卷积层数能覆盖的范围扩大 16 倍,这才是撑开感受野的正确方式。
下面这张表是我实测的不同配置对比,硬件是一块中端车载 GPU,输入 6 路环视,分辨率 800×448。
| 体素边长 | 网格尺寸 | 非空率 | 显存占用 | 单帧耗时 | mIoU 参考 |
|---|---|---|---|---|---|
| 0.5 m | 200×200×16 | 5.2% | 3.1 GB | 42 ms | 基准 |
| 0.4 m | 250×250×16 | 4.1% | 4.6 GB | 63 ms | +1.8 |
| 0.4 m + 稀疏主干 | 250×250×16 | 4.1% | 2.4 GB | 38 ms | +1.6 |
| 0.3 m + 稀疏主干 | 334×334×20 | 3.3% | 3.8 GB | 高速档不可用 | +2.9 |
可以看到,单纯提分辨率会同时吃显存和时间,稀疏化之后才能把两头都压下来。0.3 米那一档在部分平台上根本跑不动,属于“实验室可用、量产不可用”的范畴。
3.2 稀疏卷积的通道配置与显存预算
通道配置是最容易拍脑袋的地方。我的做法是按“空间尺寸的平方根”来分配,让各层的计算量尽量均衡。
具体说,第一级空间尺寸最大,通道给 16 或 24;第二级空间减半,通道翻到 32;第三级再减半,通道 64;第四级 96 或 128。这个梯度比常见的 32/64/128/256 要窄,原因是三维卷积的计算量是通道数的平方乘以空间大小,通道堆太猛计算量涨得比二维更厉害。
显存预算我一般按“特征图 + 索引 + 激活缓存”三块估。特征图按前面的公式算;索引大约是特征图大小的 1/4,因为它存的是整型坐标;激活缓存只在训练时有,大概是特征图总量的 2 到 3 倍(取决于是否用梯度检查点)。开启梯度检查点能省掉一大半激活显存,代价是训练时间增加约 20%,长训任务我会默认开。
提示:稀疏张量的批次拼接是显存杀手。多帧融合时如果每帧独立存一份坐标,索引内存会翻倍。做法是用一个共享的坐标表加偏移量,把多帧压进同一个稀疏张量里处理,实测能省 30% 左右的索引开销。
3.3 数据增强与类别不均衡处理
三维场景的数据增强跟二维图像完全不同,别直接套图像那套。我做过的有效增强有这几样:
- 全局旋转:绕竖直轴随机旋转 ±15 度,模拟路口不同朝向。注意旋转后要同步变换监督标签和位姿,很多人只转了输入忘了标签。
- 局部抠除:随机挖掉一个立方体区域,模拟遮挡。这个对提升鲁棒性效果明显,但挖的区域别太大,边长控制在 4 到 8 米。
- 尺度抖动:整体缩放 ±5%,这个要谨慎,会影响深度估计的绝对尺度。
- 稀有类别过采样:行人、非机动车在体素里占比可能不到 1%,我把包含这些类别的场景帧采样权重提到 3 倍。
类别不均衡还有个处理是损失加权,但我不建议直接按频率倒数加权,那样会把稀有类别权重抬到几百倍,训练直接发散。稳妥做法是 cap 在 10 倍以内,剩下的靠采样解决。
3.4 训练策略里几个必须调对的参数
学习率用 2e-4 起步,配合余弦退火和 500 步线性 warmup。为什么不用常见的 1e-3?因为稀疏卷积的梯度尺度比稠密卷积大,学习率过高前几百步容易炸。
批大小受显存限制,我用 2 到 4。批太小会导致批归一化统计不稳,所以要么改用组归一化,要么在稀疏层里干脆不用归一化。我的选择是图像主干用批归一化,三维部分用组归一化,实测下来收敛曲线比较平顺。
EMA 权重要开,衰减率 0.999。稀疏模型的参数更新噪声大,EMA 能明显稳住验证集指标。
梯度裁剪设 10,这个是防止个别帧的极端场景导致梯度爆掉。我遇到过一帧数据标错了高度范围,不裁的话整个训练直接崩。
训练轮次 24 到 36 之间,看数据规模。太短欠拟合,太长开始过拟合,验证集 mIoU 会先涨后跌,拐点一般在 20 轮以后。
4. 从训练到部署,工程落地全流程
实验室跑通和车端跑通是两码事,这一章按顺序把流程走一遍。
4.1 环境与依赖准备
基础环境用 PyTorch,稀疏部分依赖稀疏卷积库,这些在开源生态里都有现成实现,不建议自己从零写。要注意的是版本对齐:稀疏卷积库对 PyTorch 版本和 GPU 计算能力有要求,装错版本会在编译阶段直接失败。
一个能跑通的配置组合大概是:
python 3.9 torch 2.1 spconv 2.3.6 onnx 1.15 onnxsim 0.4.36装稀疏卷积库时如果编译报错,通常是 CUDA 版本和 PyTorch 编译版本不匹配。先跑一行python -c "import torch; print(torch.version.cuda)"看清版本,再决定装哪个预编译包。自己编译的话,记得把CUDA_HOME指对,并且编译并行度别开太高,容易内存溢出。
数据加载这块,三维数据的读取是隐藏的性能瓶颈。一张点云加六路图像,磁盘 IO 能占到整个训练步的 30%。我的做法是预处理时把数据转成内存映射格式,配合多进程预取,把num_workers设到 8 以上,prefetch_factor设 4。
4.2 训练脚本的关键配置
配置文件我习惯用 yaml,把结构参数和数据参数分开,方便做消融。核心片段大概长这样:
model: backbone_width: 0.5 voxel_size: [0.4, 0.4, 0.5] grid_size: [250, 250, 16] use_sparse: true sparse_start_stage: 2 channels: [24, 32, 64, 96] temporal_frames: 3 depth_supervision: true train: batch_size: 4 lr: 2.0e-4 weight_decay: 0.01 warmup_steps: 500 epochs: 30 ema_decay: 0.999 grad_clip: 10.0 focal_alpha: 0.25 focal_gamma: 2.0 depth_loss_weight_init: 0.5 depth_loss_weight_final: 0.1sparse_start_stage: 2就是从第二级开始转稀疏,这个参数我建议单独做消融,不同平台的收益拐点不一样。
损失函数这部分,我把 focal 和语义损失的实现写一下,很多人卡在 mask 处理上:
def occ_loss(pred_cls, pred_sem, gt_occ, gt_sem, cls_weight=1.0, sem_weight=1.0): # pred_cls: (N, 1) logits, pred_sem: (N, C) logits # gt_occ: (N,) 0/1, gt_sem: (N,) 0..C-1 cls_loss = sigmoid_focal_loss(pred_cls.squeeze(-1), gt_occ.float(), alpha=0.25, gamma=2.0, reduction="mean") non_empty = gt_occ > 0.5 if non_empty.sum() > 0: sem_logits = pred_sem[non_empty] sem_target = gt_sem[non_empty] sem_loss = F.cross_entropy(sem_logits, sem_target, weight=class_weights, label_smoothing=0.05) else: sem_loss = torch.zeros((), device=pred_cls.device) return cls_weight * cls_loss + sem_weight * sem_loss这段里有三个细节值得说。第一,语义损失只在非空体素上算,别在全体素上算,否则空体素会把语义头的梯度稀释掉。第二,标签平滑给 0.05,能缓解相邻类别(比如路面和人行道)的过拟合。第三,if分支是为了防止某些批全是空体素导致除零,这个在实际训练里真的会碰上。
4.3 导出与推理加速的处理方式
导出是整个流程里最容易翻车的一环。稀疏算子在标准的 ONNX 算子集里没有对应实现,直接导出会失败或者导出成一大堆不支持的自定义节点。
我的做法是准备双路径:训练和验证用稀疏路径,导出时切到稠密回退路径。稠密路径不是简单的全稠密,而是用一个固定的掩码把计算限制在可能非空的区域(比如以自车为中心的柱状区域),把无意义的天空和远处体积排除掉。这个做法的精度损失通常在 0.5 个点以内,但换来了完全的部署兼容性。
具体流程是:
- 训练完成后,用一批校准数据统计各层的实际非空位置分布,得到一个保守的空间掩码。
- 修改模型的前向逻辑,让三维主干在这个掩码内计算。
- 导出 ONNX,用简化工具去掉冗余节点,特别关注那些因为动态 shape 产生的
If和Loop节点,尽量把它们静态化。 - 在推理引擎里做算子融合,把卷积、归一化、激活合成一个节点。
- 开 INT8 量化,用校准集跑 200 到 500 个样本,注意校准集要覆盖雨雾、夜间这些极端场景,否则量化后这些场景掉点严重。
量化的坑我单独说一下。三维卷积的激活值动态范围比二维大,逐张量量化会掉点,必须用逐通道量化。另外第一层和最后一层不要量化,保留浮点,这两层对精度影响最敏感。
4.4 精度对齐与性能基线
部署完之后必须做精度对齐。我的做法是分三层对:
第一层对数值,随机取 100 个输入,比对浮点推理和训练框架输出的最大绝对误差,控制在 1e-4 以内算通过。
第二层对指标,在验证集上跑 mIoU,跟训练框架的结果比,掉点不超过 1.0 个点可接受。
第三层对场景,按距离分段统计(0-20 米、20-40 米、40-60 米),按类别统计,找出掉点集中的地方。我遇到过一次整体只掉 0.6 个点,但远处行人这一类掉了 8 个点,原因就是量化校准集里远处样本太少。
性能基线要测三个数:纯模型推理耗时、端到端耗时(含前后处理)、以及峰值内存。端到端里前后处理经常被低估,体素化和坐标变换加起来可能跟模型本身一样久。我这边的实测数据是模型 38 ms,前后处理 11 ms,端到端 49 ms,一开始只测前者,上车之后发现整体 60 ms 打不住,就是漏算了后处理。
5. 常见问题与排查技巧实录
这一章是我这几年踩坑攒下来的,按症状归类,你可以当速查表用。
5.1 训练不收敛的六种典型症状
症状一:loss 降得很快但 mIoU 一直是零。这是空体素太多导致的坍缩。检查 focal 的 alpha 是不是设错了方向,二分类里正样本是“占用”,如果 alpha 给了空类,网络会拼命学空。另外确认一下正样本比例,低于 3% 的话要配合过采样。
症状二:loss 震荡剧烈。学习率太高或者批太小。先用 5e-5 跑 200 步看曲线平不平,平了再往上加。如果批大小只有 2,把组归一化的组数调小。
症状三:前几轮正常,第 8 轮突然 NaN。大概率是梯度爆炸或者除零。开梯度裁剪,检查损失函数里有没有对零元素求对数的地方。我遇到过一次是深度损失里预测值取对数时没加 epsilon。
症状四:训练集好验证集差。过拟合,但也可能是数据泄漏。检查一下时序融合的缓存有没有跨样本污染,这个 bug 特别隐蔽,表现就是验证集指标虚高或者忽高忽低。
症状五:某些类别召回一直是零。类别权重没设或者设得太保守。先看这个类别在训练集里的总样本数,少于 10 万个体素的话,靠权重救不回来,必须采样。
症状六:稀疏层梯度为 NaN 但稠密层正常。稀疏卷积的索引里出现了越界坐标。检查体素化时有没有对边界做过 clamp,点云坐标超出网格范围就会产生非法索引。
5.2 部署后速度不达预期的定位方法
速度不达预期是最高频的问题,我用一套固定的排查顺序,能覆盖八成情况。
第一步,看是不是被数据搬运拖住了。用性能分析工具看各阶段耗时占比,如果预处理阶段占比超过 25%,优化重心就该放在体素化和坐标变换上,而不是模型本身。优化手段包括把坐标计算搬到 GPU、用定点数代替浮点、以及把重复的坐标变换合并。
第二步,看算子有没有回退。推理引擎遇到不支持的算子会回退到通用实现,速度可能差十倍。导出后一定要看引擎的日志,把所有回退算子列出来,逐个替换或者融合。
第三步,看内存访问是否连续。三维张量的内存布局对性能影响很大,通道优先和空间优先差很多。我的经验是在推理路径上用通道优先布局,卷积前后不需要转置。
第四步,看批大小和并发的匹配。车端往往只跑单帧,但有些引擎在批大小为 1 时效率很低,要开特定的优化档位。这个细节不同引擎不一样,得实测。
第五步,看热节流。车载环境温度高,连续跑几分钟后频率会掉,实测耗时可能比冷启动高 20%。做性能基线一定要跑够 10 分钟再看稳定值。
5.3 常见问题速查表
| 现象 | 可能原因 | 排查动作 | 处理方式 |
|---|---|---|---|
| 导出 ONNX 失败 | 稀疏算子无对应实现 | 看报错节点名 | 切稠密回退路径 |
| 量化后远处掉点严重 | 校准集分布偏差 | 按距离分段统计 | 补远处与极端场景样本 |
| 帧率波动大 | 动态 shape 导致重编译 | 打印每帧输入形状 | 固定 shape 或预分配缓存 |
| 显存缓慢增长 | 缓存未释放或碎片 | 监控显存曲线 | 用固定地址环形缓冲 |
| 边缘区域预测空洞 | 网格边界无上下文 | 看边界体素的感受野 | 扩大网格或补零填充 |
| 时序融合后精度反降 | 位姿对齐误差累积 | 关掉时序做对照 | 缩短历史帧数或降采样融合 |
| 小目标召回低 | 深层特征分辨率不足 | 分类别统计召回 | 加浅层特征直连或提高输入分辨率 |
提示:表格里“时序融合后精度反降”这条我单独强调一下。很多人默认加时序一定涨点,其实位姿标定有微小误差时,历史特征会“糊”到错误位置,反而干扰当前帧。判断方法是把历史帧数从 3 减到 1,如果精度回升,问题就在对齐上。
另外分享一个排查小技巧:把模型某一层的输出可视化出来,用简单的俯视图投影到 BEV 平面看。很多时候指标看不出问题,但图上一眼就能看出特征图整体偏移了半个体素——这种系统性偏移往往是坐标系的定义不一致导致的,比如体素中心用格点索引还是用格点加半格,训练和部署两边不一致就会整体错位。
6. 我在这套方案上的一些实际体会
最后说几个不太好归类的经验,都是踩过才明白的。
关于精度和速度的取舍,我现在的判断是优先保几何精度,语义精度可以妥协。原因是下游规划控制真正依赖的是“那里有没有障碍物、离我多远”,而不是“那到底是轿车还是 SUV”。所以我在调参时会把二分类的指标看得比语义 mIoU 更重,资源紧张时先砍语义头的通道数。这个取舍跟很多论文的优化目标不一致,但更贴近实际使用。
关于体素化这一步,别小看它。我见过一个项目,模型优化了三个月提升 15% 的速度,结果换个更高效的体素化实现直接又省了 8 ms。体素化的核心是把连续坐标映射到整数格点,用查表代替除法和取整能快不少,另外把点云按坐标排序后做前缀和,也能避免哈希冲突。
关于数据标注,Occ 的标注成本是检测的好几倍。我们后来的做法是用模型预标注加人工修正,先跑一遍高精度的离线大模型生成稠密占用,人工只修正错误区域。这个方法把标注效率提了三四倍,但要注意预标注模型的系统性偏差会传导到训练数据里,得定期用纯人工标注的小集做校验。
关于验证策略,我强烈建议按场景切片做验证,而不是只看一个总体指标。雨天、夜间、隧道、地库这四类场景单独看,往往会发现某一类掉得很厉害。我遇到过整体 mIoU 62,但地库场景只有 41 的情况,原因是训练集里地库数据不到 2%。这种问题不切片根本发现不了。
还有一个想说的点:别过早优化。我一开始就上稀疏卷积,结果调试了两周才发现真正的瓶颈在数据加载。正确的顺序是先建一个能跑通的稠密基线,用性能分析工具找出真实瓶颈,再决定往哪个方向优化。基线跑通可能只要三天,但它给你的信息量比盲目优化一个月还多。
这套方案后续还能往几个方向延展:一是把占用预测和端到端规划联合训练,让感知直接对齐下游目标;二是用蒸馏把大模型的知识压到小模型上,这在车端算力受限时很实用;三是把时序融合从特征级下沉到体素级,减少重复计算。这几个方向我都在试,有确定的结论再拿出来聊。