FastOcc 这个名字第一次出现在方案讨论会上时大家争的其实不是网络结构而是延迟预算表上那行数字到底写 50ms 还是 80ms。做三维占据栅格预测的人都清楚这类模型一旦做得更深、体素切得更细精度确实能往上抬但延迟和显存会立刻还回来而车端和机器人上的算力盒子不会因为你精度涨了两个点就多给你一块加速卡。FastOcc 要解决的就是这个矛盾在保住占据栅格预测可用精度的前提下把单帧前向压进实时区间让它从“离线跑得动”变成“车上跑得稳”。这篇东西写给两类人看。一类是刚接触占据栅格预测、想搞清楚一个实时方案到底由哪些零件拼起来的同学另一类是已经在调模型、但卡在帧率或者显存上的同行想找点能直接抄的配置和避坑经验。我不会只讲“用了稀疏卷积所以快”这种正确的废话而是把体素分辨率怎么定、特征通道砍到多少、时序怎么对齐、量化在哪一层必须放行这些细节一条条摊开参数背后的换算过程也一并写上。看完你至少能做到三件事自己算一遍显存账、搭出一条能跑通的训练加推理链路、在帧率不达标时知道先怀疑谁。1. 先把问题摆清楚FastOcc 为什么必须“快”1.1 从二维检测到三维占据需求到底变了什么早几年做感知主流思路是在鸟瞰图上做框回归车、人、路沿各出一个带朝向的矩形后处理一做规整漂亮。这套东西的隐含假设是“世界由有限类别的刚体组成”。现实很快就打脸了——施工围挡、掉落的货物、斜插出来的异形障碍、被雪半埋的路肩这些东西你没法给它们定义类别也没法用框套住但它们照样能把车顶停。占据栅格预测换了个思路不猜类别只回答“这个体素被占据的概率是多少”。体素是三维空间里切出来的小方块比如 0.4 米见方整个感知范围就被切成一张立体的棋盘每个格子给一个占据概率和一个语义标签。好处很直接异形障碍只要有体积就一定能被标出来不需要提前定义类别坏处也很直接格子太多了。鸟瞰图检测出一个框输出是几十个参数而占据栅格预测的输出是一个三维张量哪怕分辨率压到 0.4 米100 米见方的范围单层就有 6 万多个格子叠上高度维度直接上百万。这不只是输出变大的问题是整个中间特征都得在三维空间里维护计算模式和二维卷积完全不同。FastOcc 面对的正是这个“数据体量级跳变”的起点。1.2 速度瓶颈到底卡在哪算力、显存、带宽三本账很多人第一次做占据预测模型精度不错一上板子就傻眼帧率只有个位数。直觉会归因于“参数量太大”但实测下来瓶颈往往不在算子数量而在这三本账上。第一本是算力账。稠密三维卷积的复杂度是体素总数乘以卷积核大小乘以通道数而体素总数随分辨率呈立方增长——分辨率从 0.8 米提到 0.4 米格子数直接翻八倍。你以为只是“清楚了一点”实际算力需求翻了一个数量级。第二本是显存账。三维特征图不像二维图像那么“薄”一个 batch 的四维张量B×C×D×H×W很容易把显存吃干。更要命的是中间层的激活值反向传播时全得留着训练阶段显存爆掉比推理阶段常见得多。第三本是带宽账。就算算力够特征在片外显存和片上缓存之间来回搬运的时间也可能吃掉一半的延迟。稀疏结构带来的不规则访存恰恰是内存带宽最不喜欢的形态。FastOcc 的整套设计本质上就是在算一笔“怎么用最少的有效计算换最大的空间覆盖”的账。1.3 FastOcc 的整体取舍先定预算再谈结构我做这类方案有个固定习惯先画一张预算表把总延迟拆成几个硬性份额然后每个模块分配额度超了就得从别处省。FastOcc 的预算表大致是这样切的以 10Hz 传感器、100ms 单帧预算、留 30% 余量即 70ms 为总目标环节预算份额典型耗时说明图像预处理与体素化10%5~7ms相机去畸变、多视图拼 batch、点云体素化二维骨干提特征30%18~22ms六路相机共享权重分辨率下采样到 1/8视角抬升与三维编码35%22~26ms这部分是真正吃算力的地方时序融合10%5~7ms只融合单帧历史且只融合粗粒度特征输出头与后处理15%8~10ms语义头、占据头、连通域清理这张表最有价值的地方不是数字本身而是它强制你在动手写代码之前就想清楚哪些环节可以省哪些不能碰。比如输出头和后处理占了 15%很多人觉得这是“杂活”不重视结果模型做得飞快后处理在 CPU 上单线程跑连通域分析一下子干掉 20ms。FastOcc 里后处理是并行化的用 GPU 上的形态学算子做这才对得上预算。2. 架构拆解FastOcc 的四个模块各管什么2.1 输入表示体素化策略决定了后面所有事FastOcc 的输入是两路多视角环视图像和可选的激光点云。纯视觉版本更省成本但深度估计的不确定性会让远处体素的占据判断发飘加上点云能把远处的几何拉回来代价是多一路传感器和一份标定维护成本。我建议先跑纯视觉版本把链路跑通再考虑融合。体素化这一步看似是“把点塞进格子里”实则藏着几个必须提前定死的参数。第一个是体素边长常见取值 0.2、0.3、0.4 米。0.4 米时100×100×6.4 米的感知范围对应 250×250×16 的网格总格子数 100 万0.2 米时变成 500×500×32总格子数 800 万。第二个是高度方向的起始偏移路面不是一个平面坡道和减速带会让体素切片上下浮动通常按自车后轴中心往下偏移 2 米作为底面。注意体素边长一旦定死后面所有标定、标注、评估口径都要跟着变中途改一次等于数据全部重做。我见过有团队训练到一半想把 0.4 改成 0.3结果发现标注的语义标签是跟着网格走的只能推倒重来。这个参数在项目启动会上就该拍板。第三个容易被忽略的是“哪些点算有效点”。激光点云在雨雾天会有大量噪点如果全塞进体素非空体素数量会虚高稀疏卷积的计算量跟着涨。FastOcc 的做法是先做一次半径滤波加统计离群点剔除把孤立的零星点干掉非空体素数量能降 5% 到 10%训练也更稳。2.2 视角抬升二维特征怎么变成三维体素这是整个方案里技术含量最高、也最容易被讲玄的部分。六路环视图像给的是二维像素特征要变成三维体素特征中间必须建立“像素—空间点”的对应关系。业内主流有三条路一是显式深度估计给每个像素预测一组深度分布然后把特征按深度权重“泼”到三维空间里二是隐式查询直接在三维空间撒查询点反投影到各个视角去采样特征三是把高度维度压成一层先做鸟瞰图再往上堆。FastOcc 选的是第一条路的轻量版本理由很实在显式深度分布可以加监督训练收敛快且推理时计算图规整对加速引擎友好。代价是深度估计不准时特征会“糊”。为了压住这个代价做了两件事深度分箱只取 32 个有的方案取 80 个以上并且只在 4 个粗粒度尺度上做抬升不再对每个像素都做。具体到计算一次抬升的复杂度大致是像素数 × 深度分箱数 × 特征通道数。以六路相机各 900×1600 分辨率、下采样 8 倍后是 112×200、每路约 2.2 万像素计算六路合起来 13.4 万像素乘以 32 个深度分箱、再乘 32 个通道大约是 1.37 亿次乘加。这个量级在移动端加速卡上单次做还行做四五次就撑不住了所以抬升层数必须克制。2.3 稀疏化把稠密张量换成“只算有用的”真正让 FastOcc 快的不是某个花哨的算子而是稀疏化思路贯彻得彻底。三维空间里绝大多数体素是空的——天空、地面以下、远处没东西的区域占了九成以上。稠密卷积会老老实实把这些空格子也算一遍稀疏卷积只处理非空体素计算量与非空体素数量近似线性相关而不是与网格总数相关。这里有个很多人算错的账值得单独说。有人会想“我把分辨率提一倍非空体素是不是也涨八倍”。并不是。分辨率提高后同一块物体被切成的体素变多但空白区域的体素也会变多两者比例基本稳定在 1% 到 5% 之间具体取决于场景密度。我在一个城市道路场景上实测过0.4 米分辨率下非空体素占比约 2.3%0.2 米分辨率下约 3.1%。也就是说总格子数涨了八倍非空体素只涨了约 11 倍里的……准确说是 800 万 × 3.1% ÷ (100 万 × 2.3%) ≈ 10.8 倍。嗯这个数字说明什么说明分辨率翻倍计算量还是涨了近十倍稀疏化并没有魔法它只是把常数从“全体素”拉到了“非空体素”。真正省的地方在于0.4 米分辨率下稀疏卷积的输入只有 2.3 万个活动的“点”相比 100 万个格子省了 97% 以上的无效计算。这才是能跑到实时的根本原因。提示稀疏化之后“体素数量”这个指标会频繁出现在 profiling 里一定要和“非空体素数量”区分开。我曾经花了半天排查一个“显存莫名其妙爆掉”的问题最后发现是某层做特征聚合时偷偷把稀疏张量转成了稠密张量100 万格子乘 64 通道乘 2 字节直接 128MB 一层。2.4 时序融合与输出头别在这两处贪心时序融合的价值很高单帧看不全的地方前一帧的信息能补上。但融合也是最容易失控的地方因为它意味着特征要在时间维度上存多份。FastOcc 的做法是只融合一帧历史且只融合最粗的那一层特征通道 32、分辨率 0.8 米融合方式是把上一帧特征按自车位姿变换到当前帧坐标系做双线性采样后相加。这个设计的关键在于位姿精度。自车位姿如果差 0.1 米融合后远处物体的占据会明显拖影。所以 FastOcc 在融合前做了一次位姿一致性检查如果两帧之间的位姿跳变超过阈值比如 1 米直接放弃融合退化成单帧推理。这个降级策略在实车上救过我好几次尤其在定位短暂丢失的时候。输出头部分FastOcc 用的是两个轻量分支一个出占据概率二分类一个出语义类别十几类。输出层的体素分辨率不需要和前段一致可以用 0.8 米粗粒度输出再上采样展示实测对下游规划的可用性影响很小。这里我的个人经验是输出粒度粗一点比通道砍太狠更划算前者只是显示不精细后者会直接让模型学不动。3. 关键参数把“快”落到具体数字上3.1 体素分辨率与显存占用的换算过程这一段我直接把算式写出来你照着代自己的参数就行。假设感知范围长 100 米、宽 100 米、高 6.4 米体素边长 s 米网格尺寸Gx 100/sGy 100/sGz 6.4/s总格子数 N Gx × Gy × Gzs 0.4 时Gx Gy 250Gz 16N 1,000,000。 s 0.2 时Gx Gy 500Gz 32N 8,000,000。再看特征显存。某层特征通道数 C数据类型 float16每元素 2 字节稠密张量显存 N × C × 2 字节稀疏张量显存 N × 非空率 × C × 2 字节s 0.4、C 64、非空率 2.3%稠密是 1,000,000 × 64 × 2 128MB稀疏是 128MB × 2.3% ≈ 2.9MB。差了 44 倍。这就是为什么稀疏卷积方案能在车端跑而稠密三维卷积只能停在服务器上。但请注意上表算的只是一层。典型网络有十来层不同分辨率的特征只要有一层不慎稠密化显存就会跳一个台阶。训练时还要再乘一份梯度存量加上优化器状态如果用的是带动量的优化器还要再乘 2 到 3 倍。所以训练阶段 batch size 一般只能开到 2 到 4靠梯度累积凑等效 batch。3.2 感受野与下采样层数怎么配感受野决定了模型“能看多远”。三维卷积的感受野公式很朴素连续堆 n 层核大小 k 的卷积感受野 r 1 (k−1) × n步长全为 1 时。如果中间插了步长为 2 的下采样后面的层感受野要按尺度放大。s 0.4 米、堆 10 层 3×3×3 卷积感受野 r 1 2 × 10 21 个体素折合 8.4 米。这个尺度能覆盖一个路口的横向跨度够用。如果想看得更远有两个办法加层数或者加下采样。加层数会让延迟线性上涨加下采样会损失分辨率。FastOcc 的选择是在中段做两次步长为 2 的下采样把空间尺度压到 1.6 米然后在深段继续堆卷积。这样做的好处是深段的计算量骤降因为体素数量少了 64 倍代价是小物体在深段基本消失。所以浅段特征必须通过跳连传回去这也是为什么分割类结构在占据预测里几乎是标配。注意下采样维度别三个方向一起降。高度方向的信息量本来就少6.4 米只切 16 层再降两次就剩 4 层坡道和悬空障碍基本判不出来。我的做法是高度方向最多降一次平面方向降两次。3.3 通道裁剪与精度的平衡一组可参考的实验数据通道数是最好调的旋钮也是最容易调过头的。我在一个中等规模数据集上做过一组对照固定其他条件只改骨干最后一层的通道数结果大致如下数值是相对值只为说明趋势通道数参数量变化单帧延迟占据 IoU 相对变化远距离召回相对变化128基准38%1.4%2.1%96−22%9%0.6%0.8%64−46%基准0048−61%−14%−1.1%−3.2%32−74%−25%−3.8%−9.6%这张表里有意思的是拐点位置从 64 砍到 48延迟只降 14%但远距离召回掉了 3.2 个点而从 96 砍到 64延迟降了 9%精度几乎没动。所以 64 是这组配置下的甜点48 开始亏本32 是明显亏本。不过这个结论跟数据分布强相关。如果你的场景里远处小目标特别重要比如高速上的锥桶甜点会往右移如果是低速园区场景往左移也没事。别抄我的数字抄我的方法每次只动一个旋钮记录延迟和分距离段的召回找自己的拐点。3.4 量化与算子融合哪几层必须放行量化是省延迟最猛的手段但三维稀疏结构对量化不太友好。核心矛盾在于稀疏卷积的输入形状是动态的每个 batch、每一帧的非空体素数量都不一样而很多推理引擎在 INT8 模式下要求固定形状。绕过去的办法有两个一是按最大体素数做 padding二是把动态形状的层保留为浮点。FastOcc 的做法是混合精度骨干卷积和抬升后的三维编码层走 INT8体素化入口、稀疏索引生成、输出头这三处保留 FP16。理由是体素化入口涉及大量的索引计算和边界判断量化误差会被放大成坐标错误稀疏索引是整数运算本来就不该量化输出头直接影响最终的阈值判断INT8 的截断误差会让边缘体素的概率在 0.5 附近抖动后处理出来的形状毛毛躁躁。实测下来混合精度相比全 FP16 有约 1.7 倍的加速相比全 INT8 慢 15% 左右但精度损失从 2.8 个 IoU 点压到了 0.4 个点。这笔账我认为很值。另外算子融合也值得单独提一句。三维网络里连续的“卷积 归一化 激活”如果不融合中间结果要写回显存再读出来带宽开销能占到三成。加速引擎一般能自动融合这几层但要确认稀疏卷积的自定义算子也在融合范围内不然它会被当成黑盒隔断前后都退化成独立执行。4. 实操过程从零搭一条能跑通的链路4.1 环境与数据准备先把口径统一动手之前先把三件事定死否则后面全是返工。第一坐标系。所有传感器数据、标注、体素网格必须统一到同一个自车坐标系原点定在后轴中心地面投影点x 向前、y 向左、z 向上。相机外参转到这里点云也转到这里。这一步听着基础但我见过太多项目在融合阶段才发现某路相机的朝向差了一个符号。第二标注格式。占据栅格的真值通常由激光点云累积加人工修补生成。累积窗口别开太大车辆运动超过 20 米点云配准的误差就会让边缘糊掉。我的经验是取前后各 5 帧配准后只保留被观测到 2 次以上的体素作为“已知占据”观测到 1 次的标为“忽略”评估时跳过。第三数据划分。别按帧随机划分要按场景片段划分。同一段路的相邻帧高度相似随机划分会导致验证集泄漏指标虚高。我一般按连续 100 帧为一个片段整段整段地分。预处理流水线大致是图像去畸变 → 多路图像拼成 batch → 计算归一化参数 → 点云滤波 → 体素化 → 生成稀疏索引。这几步里体素化最容易成为 CPU 瓶颈一定要做成多进程或者直接在 GPU 上做。4.2 训练配置损失函数和不平衡问题占据预测的类别极度不平衡空体素能占到 95% 以上。直接上交叉熵模型会迅速学会“全判为空”loss 降得飞快指标惨不忍睹。FastOcc 用的是组合损失占据分支用带焦点机制的二元交叉熵聚焦参数取 2.0让模型把注意力放在难分的边界体素上语义分支用类别加权的交叉熵权重按类别频率的倒数开方再归一化别直接取倒数否则稀有类权重会大到把训练带偏加一路辅助监督在抬升阶段对深度分布做监督用激光点云投影出来的深度作为真值。这一路的权重不能高我一般从 0.1 开始训练中期衰减到 0.02。学习率调度上稀疏网络对初始学习率比较敏感太大容易在第一轮就发散。建议用带热启动的余弦退火峰值学习率取 2e-4 左右热启动占前 500 次迭代。batch size 开不大时梯度累积 4 次等效放大。还有一个实战细节三维数据增强一定要和标定一起变。图像翻转了相机内外参没跟着翻特征抬升的位置就全错了。我自己写了个增强封装把图像变换矩阵和相机参数绑在一起处理这个坑我踩过一次损失曲线看起来正常但模型学出来的东西整体偏移了半米。4.3 推理加速与部署验证训练完导出模型进入部署环节。这里的顺序很重要别一上来就上量化。第一步先跑通浮点推理确认输出和训练框架里的一致。做法是固定同一批输入逐层对比中间结果误差应该在 1e-3 以内。这一步能抓出算子实现不一致的问题尤其是稀疏卷积的索引排序不同实现可能给出不同的内存布局导致结果错位。第二步做算子替换。把能融合的层融掉把自定义算子换成加速引擎支持的版本。这一步之后通常能拿到 1.3 到 1.6 倍加速。第三步做量化校准。校准集要有代表性别只用晴天数据。我一般取 300 到 500 帧覆盖白天、夜间、雨雾、隧道几种场景每类至少占 10%。校准完之后重点看三个指标整体 IoU 掉多少、远距离段召回掉多少、输出头附近体素的概率分布有没有明显偏移。第四步端到端计时。别只测模型前向要把预处理、后处理、数据拷贝全算进去。测的时候用连续跑 500 帧取 95 分位数别用平均值偶发的长尾延迟才是真会让你掉帧的东西。4.4 结果自检与可视化怎么一眼看出问题模型跑起来不等于跑对了。我有一套固定的自检流程五分钟能过一遍。先看单帧的可视化把预测的占据体素和点云叠在一起看。健康的状态是墙体、车辆轮廓贴合点云地面平整远处逐渐稀疏。如果出现整片地面“鼓包”通常是高度方向的偏置算错了如果物体轮廓比点云大一圈可能是体素中心对齐方式不一致有人用角点对齐有人用中心对齐差半个体素。再看时序一致性连续播放 50 帧盯住静止物体。静止的墙如果在一帧帧地“呼吸”说明时序融合的位姿补偿有问题或者时间戳对齐不准。最后看边界情况贴着墙走、进隧道、大车遮挡这三类场景最容易暴露问题。隧道里光照剧变图像特征提不出来模型容易把整个空间判成空大车遮挡时被挡住的区域应该输出“未知”而不是“空闲”这一点很多方案做得不好下游规划会误以为可以通行。5. 常见问题与排查实录5.1 训练不收敛或损失震荡现象loss 在前几百次迭代降得挺快之后开始上下横跳验证指标不涨。排查顺序我一般是这么走的。先看学习率稀疏网络对小学习率更敏感试试降到 1e-4 看是否平稳。再看数据有没有某一批样本的体素数量异常大比如近距离贴墙的场景导致梯度爆炸可以在采样时按体素数量做分层。最后看损失权重辅助深度监督的权重如果给高了主任务会被带偏表现为语义分支的 loss 一直在某个值附近徘徊。还有一种隐蔽情况稀疏索引的排序不稳定。如果每次前向时非空体素的顺序不同批归一化的统计量就会抖。解决办法是在生成索引时强制按坐标字典序排序保证确定性。5.2 远处物体“凭空消失”或边缘毛刺远处消失多半是采样不足。体素网格在远处一个格子覆盖的物理范围大投影到图像上只有几个像素特征太弱。可以在抬升阶段对远处做额外的深度分箱或者干脆在损失里对距离分段加权让远距离体素的误差权重大一些。边缘毛刺是输出头的量化误差或者后处理太粗糙。先试把输出头保留为浮点如果毛刺消失就是量化问题如果还在检查后处理。占据栅格的连通域分析如果用固定阈值二值化边界会呈阶梯状做一次开运算先腐蚀后膨胀能明显改善但腐蚀核别开太大否则细杆状物体电线杆、路牌立柱会被抹掉。5.3 帧率不达标三步定位瓶颈不要凭感觉猜按这三步走。第一步分段计时。把流水线切成预处理、骨干、抬升、时序、输出、后处理六段各自打点。哪一段超预算问题就在哪。第二步看是不是在等数据。如果各段加起来的时间远小于端到端时间说明有同步等待或者数据拷贝在拖后腿。常见的是 CPU 到 GPU 的拷贝没做异步或者用了页锁定内存但忘了流水化。第三步看算子的实际利用率。同一条流水线上如果某些层的耗时是理论值的五倍以上多半是稀疏卷积退化成了稠密计算或者索引生成的开销盖过了计算本身。我遇到过最离谱的一次是模型本身 22ms但端到端 65ms。查下来是后处理里的连通域分析用了单线程 CPU 实现还每帧都重新分配内存。改成 GPU 并行加内存池复用之后直接降到 6ms。5.4 常见问题速查表现象最可能的原因处理办法显存突然跳高某层把稀疏张量转成了稠密逐层打印 shape 与存储类型检查聚合、池化、插值算子IoU 正常但可视化错位体素中心与角点对齐方式不一致全局统一为体素中心对齐检查标注生成脚本静止物体帧间抖动位姿补偿误差或时间戳未对齐用硬件同步时间戳融合前做位姿跳变检测被遮挡区域被判为空缺少“未知”类别或损失未覆盖增加忽略标签遮挡区域不参与损失计算夜间或隧道性能骤降图像特征退化深度分布变平加时序融合权重或引入点云分支兜底量化后边缘概率抖动输出头 INT8 截断输出头保留 FP16混合精度部署6. 我在这个方案上踩过的几个坑第一个坑是低估了体素化的开销。一开始我把体素化写在 Python 里做单帧要 40 毫秒比模型本身还慢。后来改成在 GPU 上直接用索引算子做降到 3 毫秒以内。这件事给我的教训是数据准备的每一环都要按“会不会成为瓶颈”来审视别默认它是小事。第二个坑是时序融合的降级策略。早期版本没有跳变检测定位一抖融合进去的历史特征全是错的表现为整片区域出现重影而且越叠越糊。加了阈值判断之后宁可退化成单帧也不吃脏数据稳定性提升非常明显。这类“宁可少用信息也不要用错信息”的设计思路我认为在实时系统里应该是默认项。第三个坑跟评估有关。有段时间指标一直上不去我以为是模型不行折腾了两周结构。后来发现是评估脚本里把忽略标签也算进了分母等于一直在拿“未观测区域”惩罚模型。改完评估口径指标直接涨了 9 个点。这件事之后我养成一个习惯任何指标异常先怀疑评估脚本再怀疑模型。最后分享一个我觉得挺实用的小技巧给稀疏卷积层加一个“体素数量监控”训练和推理时都记录每层的非空体素数量。正常情况下这个数字应该随层数递减下采样导致如果某一层突然暴涨那一定是上采样或者特征聚合出了问题。这个监控几乎零成本但能帮你抓住大部分显存和延迟的异常比事后翻 profiling 快得多。