NPU 3DGS 渲染优化实战:基于 Ascend C 的 CalcRender Alpha Blending 算子流水与剪枝设计
【免费下载链接】cann-recipes-spatial-intelligence本项目针对空间智能业务中的典型模型、加速算法,提供基于CANN平台的优化样例项目地址: https://gitcode.com/cann/cann-recipes-spatial-intelligence
导读
本文聚焦 CANN 平台上 3D 高斯泼溅(3DGS)渲染管线中核心的CalcRender(Alpha Blending)算子,针对 NPU SIMD(单指令多数据)向量计算架构,系统阐述该算子从"逐高斯球串行渲染"到"多高斯球批量流水 + 两级剪枝"的完整优化路径。读者通过本文可以掌握:Alpha Blending 在 SIMD 架构下的流水化实现原理、基于 PipeBarrier 频率优化的指令流水设计、Ping-Pong 双缓冲访存掩盖技巧,以及 Tile 级与 Sub Tile(像素行)级两级剪枝机制的实现细节与性能收益评估方法。文中所有结论均可在本仓库 algorithms/gaussian_splatting 与 ops/ascendc 的源码、测试用例中找到对应实现证据。
背景:3DGS 光栅化中的 Alpha Blending
3D 高斯泼溅(3DGS)将场景表达为一组带协方差、颜色与不透明度的 3D 高斯球。渲染时,给定特定相机视角与投影平面,需要把空间中的 3D 高斯球按深度顺序混合到每个像素上,从而生成该视角的渲染图像。这一"颜色累积"过程的核心步骤即为 Alpha Blending,它直接决定了渲染图像的质量与计算量。
在 SIMD 架构(NPU 向量核)上,Alpha Blending 的串行依赖特性与向量并行特性之间存在天然矛盾:混合过程要求高斯球严格按深度排序后依次累乘,而 SIMD 又希望同一时刻对大量像素执行相同运算。如何在保证数学等价的前提下,让向量计算单元满负荷运转,是本仓库 CalcRender 算子 优化的核心命题。本篇文档(gaussian_splatting_alpha_blending_optimization.md)即对该算子优化的具体阐述,主要包括三部分:3DGS 渲染流程介绍、Pipeline 流水优化和剪枝设计。
Alpha Blending 数学原理与算法流程
混合公式
给定按深度排序后的 N 个高斯球,像素最终颜色 C 由以下累乘公式决定:
$$ C = \sum_{i \in N} c_i \alpha_i \prod^{i-1}_{j=1}(1-\alpha_j) $$
其中每个高斯球对该像素的透明度由二维高斯概率密度决定:
$$ \alpha_i = o_i \exp(-\frac{1}{2} \delta ^ T * \Sigma * \delta) $$
- $c_i$:第 i 个高斯球的颜色(RGB);
- $\alpha_i$:第 i 个高斯球在当前像素处的透明度(alpha 值),由不透明度 $o_i$ 与二维投影协方差 $\Sigma$ 决定;
- $\delta$:像素坐标与该高斯球投影中心(均值)的偏移;
- $\prod^{i-1}_{j=1}(1-\alpha_j)$:前 i-1 个高斯球的累积透射率(transmittance),体现"遮挡"关系。
在 calc_render.py 的前向入口中可以看到,该公式所需的高斯属性被拼接为一个紧凑的张量gs,其每一行即一个高斯球的 10 维属性向量:means(2) + conic0/1/2(3) + opacity(1) + colors(3) + depth(1),与内核 calc_render_fwd_double_clip_gsids.cpp 中定义的ATTR_MEAN_X、ATTR_CONIC_0、ATTR_OPACITY、ATTR_COLOR_R、ATTR_DEPTH等属性索引一一对应(NUM_GS_ATTRIBUTES = 10)。
整体算法流程
- 按 Tile 切分:按 tile 对 vector core 进行切分,每个核循环渲染被分配到的 tile 上的所有像素;
- 有序遍历高斯球:由于累乘操作的存在,必须保证对高斯球排序后依次渲染,因此用 for 循环遍历每个高斯球;
- 并行计算像素影响:对每个高斯球,并行计算其对 tile 上所有像素的影响,计算出 alpha 值和颜色贡献;
- 更新累积状态:对每个像素位置,更新累积颜色和透明度值。
从内核的Process()实现可见,tile 的像素坐标(tileCoordX/tileCoordY)、累积颜色(colorR/G/B)、累积透射率对数(ln1SubAlphaSum)与误差项(error)均以 Ping/Pong 双缓冲形式驻留在 UB(Unified Buffer)中,外层循环按 tile 切分、内层循环按高斯球遍历,与上述流程完全对应(calc_render_fwd_double_clip_gsids.cpp)。其中透射率采用对数域累积(ln(1-α)累加后再Exp),配合 Kahan 求和风格的误差补偿变量error,在向量化计算中保持数值稳定性,这正是测试用例 test_calc_render_fwd.py 中 CPU 参考实现与 NPU 输出能够逐元素对齐的前提。
SIMD 架构下的实现难点
将上述串行依赖的混合算法映射到 SIMD 向量架构,主要面临三个难点:
- 流水设计复杂:由于 SIMD 架构的并行计算特性,流水设计需要充分考虑数据依赖和计算顺序,确保各个计算单元能够高效协同工作;
- 内存访问开销与计算开销相互掩盖:高斯球数量众多,且一个 tile 内需要渲染的高斯球在内存中是不连续的(需经
gsIds间接寻址),导致内存访问开销较大,影响计算效率; - 高斯球冗余计算:每个高斯球都需要对 tile 上的所有像素计算影响,而单个高斯球实际覆盖范围远小于整个 tile,产生大量冗余计算。
针对难点 2 与难点 3,仓库在光栅化前的 GaussianFilter(视锥剔除)算子 中已先行过滤掉对当前帧无贡献的高斯;本文则聚焦CalcRender算子内部,通过流水优化(难点 1、2)与两级剪枝(难点 3)进一步压榨性能。
高斯球流水优化
指令流水并行:减少 PipeBarrier 插入频率
由于高斯球之间以及单个高斯球内部计算存在前后依赖,在 vector 指令之间经常需要插入PipeBarrier保证数据正确性,导致流水效率较低。根据 msprof op simulator 的仿真结果:对 64 个 float 数计算 VMuls 需要 15ns,而对 2048 个 float 数计算 VMuls 并不是 64 个数的 32 倍(480ns),而只需 51ns——因为其中包含了指令解码和数据预取的开销。这直观说明:固定开销占比越高、指令粒度越细,流水停顿的代价越大,因此减少屏障插入频率是提升流水效率的关键杠杆。
最开始的逐高斯球流水设计中,每处理一个高斯球,各步骤运算之间都需要插入PipeBarrier。优化思路是:将多个高斯球中没有依赖的部分计算合并在一起,减少屏障频率。具体做法是:
- 将每次处理的高斯球数量增加到4 个(对应内核常量
NUM_GS_PER_LOOP = 4,见 calc_render_fwd_double_clip_gsids.cpp); - 每个高斯球的计算分为两个阶段:第一阶段计算 α 值,第二阶段计算颜色贡献;
- 第一阶段并行处理 4 个高斯球,计算各自的 α 并暂存;
- 第二阶段依次处理这 4 个高斯球,计算颜色贡献并更新累积颜色与透明度。
这样每处理完 4 个高斯球才需要插入一次PipeBarrier,大幅减少了屏障插入次数。
在内核实现中可以看到流水化的直接证据:x_1..x_4、y_1..y_4、gaussWeight_1..4、alpha_1..4等 4 组临时缓冲(SharedBuffInit),以及主循环中"先对 4 个高斯球批量求 x/y 偏移 → 批量求二次型 → 批量 Exp → 批量求 alpha → 批量 Mins(0.999)"的 SIMD 指令序列(L454-L528),中间只穿插少量必要的同步点。
该优化带来的代价是UB 空间使用量增加:需要存储 4 组中间结果。实现上必须仔细复用 UB 空间,例如在进行颜色渲染时复用前一个高斯球的 α 计算空间(内核中alphaT_与alpha_1共享同一块 UB 偏移,见 SharedBuffInit)。
图:多高斯球批量流水设计——通过合并无依赖的 α 计算、减少 PipeBarrier 插入频率,让指令解码与数据预取开销被摊薄到更多有效计算上。
Double Buffer 优化:掩盖内存访问开销
第二个优化手段是使用Double buffer(Ping-Pong 缓冲)提前读取高斯球属性,掩盖内存访问延迟。具体做法:在处理当前批次高斯球的同时,提前读取下一个高斯球的属性值到第二块 buffer 中;处理下一个高斯球时直接从已写好的 buffer 读取属性,避免内存访问延迟。
内核中这一点体现得非常明确:
- 高斯属性缓冲分为
gsAttr1..4Ping_与gsAttr1..4Pong_两组(UbBuffInit); - 主循环迭代尾部执行
gsPing = !gsPing翻转缓冲角色(L756),使当前批次的 MTE2(GM→UB 搬运)与下一批次的向量计算重叠; - 像素级状态(
tileCoord、colorR/G/B、depth、ln1SubAlphaSum、error)同样采用PingBuffInit/PongBuffInit双份布局,并通过EVENT_ID6/EVENT_ID7两个事件(pingId_/pongId_)配合SetFlag/WaitFlag实现 MTE2、Vector、MTE3 三段流水间的精确同步。
图:使用 Ping-Pong 双缓冲后,"搬入—计算—搬出"由串行变为重叠执行,消除了数据搬移的等待时间。
高斯球剪枝设计
3DGS 渲染过程中,许多高斯球对当前 tile 的像素其实没有贡献,造成大量冗余计算。为提升效率,算子设计了两级剪枝机制:Tile 级别剪枝与 Sub Tile(像素行)级别剪枝。
Tile 级别剪枝:累积透明度达到阈值即提前结束
根据渲染公式,当累积透明度(透射率)降到一定阈值以下时,后续高斯球对像素块的贡献可以忽略不计。因此引入 Tile 级剪枝:当某个 tile 的累积透明度达到预设阈值时,提前结束对该 tile 的渲染,避免不必要的计算。
受限于 SIMD 架构,无法对每个像素单独剪枝,只能在 tile 级别剪枝——即只有所有像素的累积透明度都达到阈值,才能结束渲染。具体实现流程:
- 使用
CompareScalar将 tile 上所有像素的累积透明度与阈值比较,生成掩码(按位写回,表示每个像素是否达标); - 使用
ReinterpretCast将掩码向量转为 64 位整数,方便后续判断; - for 循环将掩码整数与
UINT64_MAX按位与:若结果相等,说明所有像素都已达标,可以结束渲染; - 记录终止高斯球 ID:为在反向传播中正确计算梯度,需要记录每个 tile 终止渲染的高斯球 ID;反向梯度传播时从该 ID 继续计算梯度,保证梯度正确性。
内核实现与文档步骤完全一致:AscendC::CompareScalar(clipIs_, T_, 0.01f, CMPMODE::LT, calPixel_)判断累积透射率T_ = exp(ln1SubAlphaSum)是否小于 0.01(L413-L426),随后ReinterpretCast<uint64_t>并逐个与UINT64_MAX比较;一旦判定全部达标,则把当前高斯球下标i写入gsClipIndex并break(L427-L434)。该gsClipIndex作为算子输出(DT_INT64,见 calc_render_fwd_double_clip_gsids.cpp)保存,供反向算子使用。
由于 Tile 级剪枝要求所有像素同时达标才生效,剪枝效果有限(实验结果中性能收益约 5%~10%)。为进一步提升效率,引入像素行级剪枝。
Sub Tile 级别剪枝:基于高斯球影响范围的像素行剪枝
Sub Tile 级剪枝基于高斯球影响范围:计算每个高斯球在投影平面上的影响范围,若某像素行完全位于影响范围之外,则该高斯球对该行无贡献,可跳过该行计算。
由于 SIMD 架构需要连续地址计算,无法对每个像素单独细粒度剪枝,因此采用对像素行进行剪枝的做法。通过分析高斯球的协方差矩阵可算出其在投影平面上的影响范围,从而实现像素行级剪枝。实验结果评估显示:每个高斯球实际只影响 6×6 左右的像素,而预设 tile size 为 32×32,因此每个高斯球实际只影响 tile 上约 20% 的像素行。
具体实现流程:
- 在整个 tile 上计算 α 值(高斯球在投影平面上的影响范围),确定其覆盖的像素行范围;
- 使用
WholeReduceMax计算每个像素行 α 的最大值; - 使用
CompareScalar将每个像素行的最大 α 与阈值比较,生成掩码,表示每个像素行是否需要进行渲染计算; - 将掩码向量重新解释为 64 位整数,用位运算判断像素行状态:先用
ScalarCountLeadingZero计算掩码整数的前导零数量——由于小端存储特性,第一个 1 所在的位置实际对应最后一个需要渲染的像素行; - 再用
ScalarGetCountOfValue<1>计算掩码中 1 的数量,即需要渲染的像素行数量;由这两者确定需要渲染的像素行范围,跳过不必要的计算; - 记录像素行范围供反向使用:在 UB 空间用
UInt8保存每个高斯球的起始像素行与结束像素行索引,每累计 1024 个高斯球后写回 GM 空间,方便反向传播时读取。
内核实现中,WholeReduceMax(tmpRes_1, alpha_1, ...)按行归约出每行最大 α(L531-L538),随后CompareScalar(..., 0.01f, CMPMODE::GE, ...)判断"行内最大 α ≥ 0.01"的像素行予以保留(L541-L548);ScalarCountLeadingZero与ScalarGetCountOfValue<1>计算出行范围sta_pix/end_pix(L553-L556);alphaClipIndexUB_以UInt8成对写入起止行索引并分批写回 GM(L558-L566),对应输出alphaClipIndex(DT_UINT8,见 calc_render_fwd_double_clip_gsids.cpp)。
图:像素行级剪枝——先以像素网格上的行最大 α 生成掩码,再通过掩码位运算确定需要渲染的像素行范围(浅色区域为待处理像素)。
剪枝后的实际计算
剪枝确定有效行范围[sta_pix, end_pix)后,后续所有向量运算(如Axpy(colorR, alphaT_, colorR属性)、Ln(ln1SubAlpha_)、误差补偿Sub(error, ...)等)都只在calPixel_1 = (end_pix1 - sta_pix1) * nPixel_1d_个连续元素上执行(L576-L672)。同时内核还做了以下细节处理:
- α 上限截断:
alpha = min(alpha, 0.999f),与文档实验中的"不透明度阈值 0.99"配合,防止数值饱和(L524-L528); - 行剪枝失效保护:当某高斯球
calPixel_1 == 0(没有任何像素行被保留)时,写入占位索引(nPixel_1d_, 0)并跳过其颜色/深度累积(L559-L564); - 末批(tail)处理:主循环按
NUM_GS_PER_LOOP批量推进后,剩余不足 4 个的高斯球进入单高斯球 tail 循环,同样复用行剪枝逻辑(L759-L889)。
反向传播:如何利用剪枝信息正确计算梯度
两级剪枝的正确性依赖反向算子对剪枝信息的完整还原,仓库中对应实现为 calc_render_bwd_var_clip_gsids.cpp,其 host 侧入口calc_render_bwd_var_clip_gsids(CalcRender.cpp)将gsClipIndex与gsIds拼接后一并传给内核。反向内核的关键行为:
- 从终止 ID 处反向遍历:前向记录的
gsClipIndex(每个 tile 终止渲染的高斯球 ID)被用作反向循环的起点currOffset,反向循环for (i = currOffset - 1; i >= prevOffset; i--)从后向前重算透射率与梯度(L282),从而跳过前向已被剪枝的高斯球,这是反向收益的核心来源; - 还原像素行范围:读取前向写回的
alphaClipIndex(UInt8 起止行),换算回像素区间sta_pix1/end_pix1,只对有效像素计算梯度,无效行直接continue(L296-L300); - 原子累加梯度:由于同一高斯球可能被多个 tile 引用,梯度写回使用
SetAtomicAdd<float>()原子加(L475-L480),保证跨 tile 梯度正确聚合。
前向输出的lastCumsum、error也会被反向内核读回,用于数值稳定地重建累积透射率(Kahan 补偿风格,L338-L351)。
算子工程结构:从 Python 到内核的完整调用链
CalcRender算子在仓库中的完整实现链路如下,便于读者按图索骥:
| 层次 | 文件 | 职责 |
|---|---|---|
| Python 封装 | calc_render.py | torch.autograd.Function,拼装gs张量(10 属性),forward 调用前向算子,backward 调用反向算子并拆分梯度 |
| C++ 算子入口 | CalcRender.cpp | aclnnCalcRenderFwdDoubleClipGsids/aclnnCalcRenderBwdVarClipGsids的 ATen 封装,负责设备校验与输出张量分配 |
| Host tiling | calc_render_fwd_double_clip_gsids.cpp | 读取nPixel、tileNum、nGauss,SetBlockDim(GetCoreNumAiv())按 AIV 向量核数设置 block 维度 |
| 前向内核 | calc_render_fwd_double_clip_gsids.cpp | 本文所述的流水优化与两级剪枝实现(KERNEL_TYPE_AIV_ONLY) |
| 反向内核 | calc_render_bwd_var_clip_gsids.cpp | 利用gsClipIndex/alphaClipIndex还原剪枝范围并反向求梯度 |
| 测试用例 | test_calc_render_fwd.py、test_calc_render_bwd.py | 以 102000 个高斯、tile size 32 为例,与 CPU 参考实现比对颜色/深度/梯度 |
测试用例 test_calc_render_fwd.py 的用例规模为[102000, 32*3, 32*4, 32](10.2 万高斯、96×128 像素、tile size 32),其 CPU 参考实现render_sort_kv中reshape(alpha.shape[0], 16, 64)、max_values >= 0.01与transparency >= 0.01等逻辑(test_calc_render_fwd.py),恰好对应内核中"行最大 α ≥ 0.01 保留"与"透射率 < 0.01 剪枝"两条阈值策略,可作为理解剪枝语义的旁证。
实验结果分析
优化组合效果
在包含 13 万个高斯球的场景(实际过滤后需渲染的高斯球数仅为 67917 个)上训练 30000 个 iters,评估流水优化与剪枝设计对渲染效率的提升。实验中选取 α 阈值为 0.004、不透明度阈值为 0.99,结果如下(正向耗时收益约 10%,反向耗时收益约 30%):
| 优化方法 | PSNR | 前向 device 耗时 (ms) | 反向 device 耗时 (ms) |
|---|---|---|---|
| 无优化 | 25.008 | 4.807 | 18.710 |
| 流水优化 | 25.005 | 4.379 | 17.565 |
| 剪枝优化 | 25.010 | 5.101 | 13.141 |
| 流水+剪枝 | 24.974 | 4.495 | 13.107 |
分析要点:
- 流水优化在几乎不损失 PSNR(25.008 → 25.005)的前提下将前向耗时从 4.807ms 降到 4.379ms,说明减少 PipeBarrier 频率带来了可观的指令级并行收益;
- 剪枝优化单独使用前向耗时反而略升(5.101ms),这是因为剪枝判断本身引入了额外的比较与位运算开销,但其反向收益显著(18.710 → 13.141ms),反向遍历范围的大幅缩短抵消了判断开销;
- 流水 + 剪枝组合在 PSNR 基本持平(24.974)的情况下,前向 4.495ms、反向 13.107ms,是综合收益最高的配置。
不同 α 阈值对剪枝效果的影响
仅评估(不重新训练)不同 α 阈值对剪枝效果的影响,结果如下:
| 阈值选取 | PSNR | 前向 device 耗时 (ms) | 反向 device 耗时 (ms) |
|---|---|---|---|
| 0 | 25.082 | 4.379 | 18.710 |
| 0.004 | 25.009 | 4.495 | 13.107 |
| 0.01 | 24.949 | 4.358 | 11.267 |
| 0.02 | 24.872 | 4.236 | 9.827 |
| 0.05 | 24.672 | 4.130 | 7.937 |
| 0.1 | 24.274 | 3.539 | 1.410 |
| 0.2 | 23.309 | 3.764 | 1.043 |
可以看出:阈值提高对反向传播的收益提升明显。在选取阈值为 0.1 时,device 反向耗时仅为原来的 7.5%(18.710 → 1.410ms),而 PSNR 仅下降 3.3%(25.082 → 24.274)。这为不同精度/性能需求的场景提供了阈值调优空间:追求渲染质量时可取较低阈值(如 0.004),追求端到端训练吞吐时可取较高阈值(如 0.1)。
总结
本文围绕 3DGS 渲染核心算子CalcRender,完整还原了其在 NPU SIMD 架构下的优化脉络:
- 流水优化:将逐高斯球计算重构为"4 高斯球批量 α 计算 + 顺序颜色累积"的两阶段流水,显著减少
PipeBarrier插入频率;配合 Ping-Pong 双缓冲实现属性预取,掩盖非连续内存访问开销; - 两级剪枝:Tile 级剪枝在累积透射率全面达标后提前终止渲染;Sub Tile 级剪枝利用"行最大 α"掩码 + 位运算高效确定每个高斯球的有效像素行范围,将计算量压缩到实际影响范围(约 tile 的 20% 像素行);
- 剪枝信息贯通正反向:前向输出的
gsClipIndex(终止高斯球 ID)与alphaClipIndex(起止行索引)被反向算子读取,实现梯度计算的正确还原与反向计算量的进一步削减; - 可复现的实验基准:13 万高斯场景下流水+剪枝组合使前向耗时约降 10%、反向约降 30%;α 阈值调至 0.1 时反向耗时可降至原来的 7.5% 而 PSNR 仅降 3.3%。
上述设计与数据全部对应本仓库可验证的实现:内核源码见 calc_render_fwd_double_clip_gsids.cpp 与 calc_render_bwd_var_clip_gsids.cpp,配套测试见 ops/ascendc/tests,读者可结合测试用例自行复现验证。若需进一步了解渲染前的视锥剔除优化(GaussianFilter算子),可参考 gaussian_splatting_culling_optimization.md。
【免费下载链接】cann-recipes-spatial-intelligence本项目针对空间智能业务中的典型模型、加速算法,提供基于CANN平台的优化样例项目地址: https://gitcode.com/cann/cann-recipes-spatial-intelligence
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考