CANN 昇腾 3DGS 视锥剔除优化:GaussianFilter 融合算子设计与实现解析
2026/9/18 16:41:51 网站建设 项目流程

CANN 昇腾 3DGS 视锥剔除优化:GaussianFilter 融合算子设计与实现解析

【免费下载链接】cann-recipes-spatial-intelligence本项目针对空间智能业务中的典型模型、加速算法,提供基于CANN平台的优化样例项目地址: https://gitcode.com/cann/cann-recipes-spatial-intelligence

导读

本文围绕 CANN 昇腾平台上 3D Gaussian Splatting(3DGS)训推优化实践中的视锥剔除(Culling)融合算子优化展开,系统讲解投影预处理阶段四类高斯剔除判据、基于 int8 位图掩码的GaussianFilter算子设计、8 元素对齐的多核 tiling 策略、基于SyncAll的两阶段分核计算流程,以及反向传播中的 Scatter 数据还原与Transpose搬运优化。读者阅读后可掌握该算子从 tiling 到 kernel 再到反向还原的完整实现链路,并可直接在 algorithms/gaussian_splatting 样例中验证其效果。

一、算子背景:为什么渲染前必须做视锥剔除

在 3DGS 中,Culling(剔除)是渲染前的关键优化步骤,目的是提前过滤掉对当前帧渲染无贡献的 3D 高斯基元,减少后续光栅化、Alpha blending 的计算量,大幅提升渲染效率。它的核心逻辑很简单:只保留"当前相机能看到"的高斯,丢弃"看不到"的高斯

在投影预处理阶段采用的是视锥体剔除,具体包含四类判据:

剔除类型判据逻辑目的
无效高斯剔除高斯必须是合法的正定矩阵(非退化、非扁平)剔除协方差矩阵行列式非正的高斯
视锥体深度方向剔除剔除深度小于近裁切面(太近)或大于远裁切面(太远)的高斯只保留视锥体深度范围内的高斯
视锥体水平/垂直方向剔除剔除高斯投影到屏幕空间后的 2D 椭圆 x/y 方向半径为 0 的高斯剔除投影退化的高斯
屏幕外剔除仅保留"投影椭圆与屏幕有重叠"的高斯剔除完全在屏幕外的高斯

从仓库测试用例 test_gaussian_filter.py 中的 CPU 参考实现可以精确还原这四类判据的组合方式:

valid = (det > 0) & (depths > near_plane) & (depths < far_plane) radius[~valid] = 0.0 inside = ( (means2d[..., 0] + radius[..., 0] > 0) & (means2d[..., 0] - radius[..., 0] < width) & (means2d[..., 1] + radius[..., 1] > 0) & (means2d[..., 1] - radius[..., 1] < height) ) proj_filter = torch.logical_and(inside, valid)

其中det为 2D 协方差矩阵行列式,depths为投影深度,means2d/radius为投影到屏幕空间的 2D 椭圆中心与半径,width/height为屏幕分辨率。GatherMask核内实现(gaussian_filter.h)通过CompareScalarAnd逐条完成x±rxy±ry与屏幕边界的比较、det>0near_plane<depth<far_plane的掩码累积,与参考实现严格对应。

二、算子实现与优化总览

GaussianFilter算子的实现与优化分为两步:

  1. 前向融合:实现GaussianFilter,将视锥剔除融合为一个算子,避免相当耗时的取 index +tensormove操作,并返回filter——其中比特位为 1 代表对应高斯有效,比特位为 0 代表对应高斯无效。测试参考实现(test_gaussian_filter.py)展示了 8 个高斯打包进 1 个 uint8 的编码方式:
filter_bool = proj_filter.bool() remainder = gaussian_nums % 8 if remainder != 0: pad_size = 8 - remainder filter_bool = F.pad(proj_filter, (0, pad_size), mode='constant', value=False) matrix_r = (gaussian_nums + 7) // 8 filter_reshaped = filter_bool.reshape(batch_dims, camera_nums, matrix_r, 8) powers = torch.tensor([1, 2, 4, 8, 16, 32, 64, 128], dtype=torch.uint8, ...) filter_uint8 = (filter_reshaped.to(torch.uint8) * powers).sum(dim=-1, dtype=torch.uint8)
  1. 反向还原:投影预处理反向算子计算前,进行反视锥剔除还原出原始 Tensor,无效高斯的梯度计算中自然置为 0。

三、实现难点

  • 比特位与元素的映射对齐:int8 类型每个元素可存储 8 个高斯的掩码状态,因此在计算时必须严格保证高斯数据与 int8 的比特位一一映射。这要求在核内迭代计算时,输入高斯数据的分片必须是 8 的整数倍,否则会出现比特位跨元素映射的混乱,导致掩码判断错误。
  • 核间数据依赖与全局同步:由于算子的切分涉及核间数据依赖,必须引入全局同步操作,但同步操作会带来性能开销,需要在保证精度正常的情况下,减少全局同步的次数
  • 反向索引构建:反向操作的核心是将过滤后的数据分散还原到原始位置,核心难点在于对于无规则的filter构建GatherMask的数据分散索引。

四、tiling 分核:8 元素对齐与尾核反推

考虑到高斯数量普遍为十万以上(测试用例覆盖 [1,1,10000]、[2,7,18471]、[1,1,117611]、[1,1,188152] 等规模,见 test_gaussian_filter.py),因此在 tiling 阶段并不对 batchsize 和相机数进行分核,而是对高斯(N 维)进行分核

具体 tiling 需要考虑以下 2 点:

  • filter 类型为 int8,需要对头核进行 8 元素对齐;
  • 对分核进行 8 元素对齐后,尾核可能计算出负数,因此需要重新反推使用的核数,再进行尾核计算。

假设 NPU 的核数为coreNum、高斯总数为N、头核处理的高斯数为perCoreN、尾核处理的高斯数为lastCoreN,分核计算如下:

$$ perCoreN = \left \lceil \left \lceil N / coreNum \right \rceil / 8 \right \rceil \cdot 8 $$

$$ usedCoreNum = \left \lceil N / perCoreN \right \rceil $$

$$ lastCoreN = N - usedCoreNum \cdot perCoreN $$

这与 host 侧 tiling 实现 gaussian_filter.cpp 完全对应:

int64_t blockLength = ((gaussNum + maxCoreNum - 1) / maxCoreNum / SIZE_OF_FILTER + SIZE_OF_FILTER - 1) * SIZE_OF_FILTER; maxCoreNum = (gaussNum + blockLength - 1) / blockLength; int64_t lastBlockLength = gaussNum - blockLength * (maxCoreNum - 1); tiling.set_blockLength(blockLength); tiling.set_lastcoreNum(lastBlockLength);

其中SIZE_OF_FILTER = 8(gaussian_filter.cpp),即掩码位图的对齐粒度。尾核lastBlockLengthgaussNum - blockLength * (maxCoreNum - 1)计算,避免了对齐后尾核为负数的问题。此外,tiling 还根据 UB 大小计算单次迭代可处理的高斯数perloopNum(gaussian_filter.cpp),并预留 workspace 用于核间计数交换(RESERVED_WORKSPACE_SIZE = 16MB)。tiling 数据字段定义见 gaussian_filter_tiling.h,包括needCoreNumbatchNumcameraNumgaussNumwidthheightnearPlanefarPlaneblockLengthlastcoreNumperloopNumhasCompensations

五、kernel 实现

GaussianFilter中 kernel 部分主要考虑的点是分核计算完后的搬运到 GM 上时的偏移计算。kernel 入口(gaussian_filter.cpp)按 tiling key 分发到模板类GaussianFilter<true/false>true/false表示是否存在可选的compensations输入。

5.1 偏移计算

因为 UB 大小限制,每个核一次迭代中无法过滤出所有有效高斯,因此引入两个变量:

  • 第 $i$ 个核($i$ 取值从 0 到 $usedCoreNum-1$)计算出来的有效高斯数量为 $cntPerCore_i$;
  • 第 $i$ 个核的第 $j$ 次迭代($j$ 取值从 0 到 $loopN_i$,$loopN_i$ 取决于 UB 大小)计算出来的有效高斯数量为 $cntPerCore_{i,j}$。

那么第 $i$ 个核的第 $j$ 次迭代中,需要把计算结果搬运出去的偏移为:

$$ offset = \sum_{k=0}^{i-1}{cntPerCore_k} + \sum_{k=0}^{j-1}{cntPerCore_{i,k}} $$

即"前面所有核的有效高斯总数"加上"本核前面所有迭代的有效高斯总数"。该偏移公式在 kernel 的SubProcess中通过offsetFilterCore_(核间偏移,由cntPerCoreLocal.GetValue(k)累加得到)与offsetFilterLoop(核内迭代偏移,由cntPreSum_.GetValue(k-1)得到)相加实现(gaussian_filter.h)。

5.2 两阶段计算流程(SyncAll 同步分阶段)

显然第 $i$ 个核的计算偏移依赖于第 $0$ 到 $i-1$ 核的计算结果,而所有的核又是同步计算的,无法一次直接算出每次迭代的偏移。因此这里使用SyncAll同步分阶段来实现 Culling 的分核计算,全程只做一次全局同步,兼顾精度与性能。

Phase1(过滤与计数)

首先执行 $loopN$ 次循环迭代,在每次迭代中,先完成filter的搬入、计算与搬出;同时统计当前迭代内 cnt 结果的累积值,将核内迭代计数数组cntPerLoop,并同步更新核内总计数cntPerCore。待该阶段循环结束后将cntPerCore存入位于workspace上的暂存空间,执行全局同步操作(SyncAll)以确保所有计算核完成第一阶段计算,随后更新并迁出 GM 上的输出偏移量(coreOffset);仅当当前计算核为最后一个核时,执行全局计数的汇总计算并将最终计数结果迁出至 GM 上。

对应源码(gaussian_filter.h)的执行顺序为:循环内CopyInFilterSourceCalcFilterCalcCntPerLoopCopyOutFilter,每轮将cntPerLoop写入cntPerLoops_、将累积值写入cntPreSum_;循环结束后CopyOutCnt(cntPerCoreGm_[blockIdx_], cntPerCore)迁出到 workspace,随后SyncAll()全局同步,再从 workspace 搬回所有核的计数计算核间偏移offsetFilterCore_,尾核负责把总有效高斯数写入cntGm_

Phase2(掩码过滤与输出)

将计算出的filter重新搬入,再次执行 $loopN$ 次循环迭代,在每次迭代中,依次搬入需要进行视锥剔除的输入,以filter作为 mask 使用GatherMask执行视锥剔除,随后结合第一阶段得到的核间偏移量(coreOffset)与对应迭代的核内计数(cntPerLoop),确定数据迁出的全局内存偏移地址。使用 3 个TQue保证 MTE2、Vector、MTE3 流水可以同步进行。

对应源码中SubProcess的 Phase2 循环(gaussian_filter.h)依次调用CopyInFilterProcessMeans2dAndRadiusProcessMeansAndDepthProcessCovars2dProcessColorsProcessConicsProcessOpacities(AndCompensations)七类数据通道的处理函数,每个函数内部都是"DataCopyIn 搬入 →GatherMask按掩码收集 → DataCopyOut 按偏移迁出",输出地址统一使用(b*C_*N_ + c*N_)*dim + offsetNDim定位。当存在compensations输入时,ProcessOpacitiesAndCompensations还会先执行Mul(opacities, compensations)完成不透明度补偿再过滤(gaussian_filter.h)。

5.3 反向梯度还原与数据搬运(Scatter 实现)

本算子的反向处理需将过滤后的数据恢复到原来位置,需要进行数据分散操作。为了避免通过循环 + 标量计算带来的较长耗时,本算子使用GatherMaskSort以及Gather等多 API 结合的方式实现数据分散的优化。根据cntPerLoop对齐的Filter的掩码进行恢复,恢复的目的数据分散如图:

Phase1:构建新的排序索引newIndex。先构造与待处理元素等长的元素索引Index(0,1,...,cntPerLoop),通过GatherMask接口对Filter的比特位为 1 的元素进行Index收集,存入头核headIndexTensor;再对Filter的比特位为 0 的元素进行Index收集,存入尾核tailIndexTensor。将两者拼接得到新的、用于Sort排序的索引newIndex。由于 Tensor 操作都需要 32 字节对齐,拼接实现时需注意对齐问题,这里采用的是先将两部分拷贝到 GM 上连续的地址,再重新拷贝到 UB 上

Phase2:Sort排序 +Gather重排恢复Sort接口对newIndex排序后得到排序值score以及每一个score对应的Sort前所在的位置索引。Sort接口提供Extract接口实现排序值和排序索引的分离,这里得到的索引正是用于gather进行元素收集的取元素地址偏移。在具体实现时,还需注意Sort接口只支持降序排序,需构造等差数列对Extract得到的索引再重排,得到升序排序索引:

5.4 数据搬运优化:用 Transpose 替代构造索引

数据在LocalTensor以 N 维度切分处理,搬运至GlobalMemory上,在计算时,为方便 vector 运算,将高斯球维度放到 Tensor 的最后,而为了得到原本输入的梯度的真实 shape,数据输出要求改变最后一维和倒数第二维的顺序。

以维度是(B,3,N)的数据搬运成维度是(B,N,3)为例:在进行数据切分后的 UB 上的 shape 为(3, perLoopN),原本需要构造形如[0, perLoopN, perLoopN*2, 1, 1+perLoopN, 1+perLoopN*2, ...]的 index,然后使用GatherMask收集对应索引的数据,而构建 index 需要大量 Scalar 运算,性能较差。因此,这里使用Transpose接口搬运替代构造 index 的搬运进行优化,思路如下图:

六、算子接口、封装与验证

6.1 算子定义与属性

GaussianFilter算子在 gaussian_filter.cpp 中以 OpDef 方式注册,输入输出均为 ND 格式的 float32:

  • 输入(9 个必需 + 1 个可选)means(B,3,N)、colors(B,3,N)、det(B,C,N)、opacities(B,N)、means2d(B,C,2,N)、depths(B,C,N)、radius(B,C,2,N)、conics(B,C,3,N)、covars2d(B,C,3,N),以及可选的compensations(B,C,N);
  • 输出means_cullingcolors_cullingmeans2d_cullingdepths_cullingradius_cullingcovars2d_cullingconics_cullingopacities_culling八个过滤后的稠密张量,外加filter(uint8 位图掩码,B*C*ceil(N/8))与cnt(int32 有效计数);
  • 属性widthheight(屏幕分辨率,Int)、near_planefar_plane(近远裁切面,Float);
  • 平台AICore配置支持ascend910bascend910_93

6.2 Python 侧调用

算子通过 pybind 与 Python 封装暴露给上层:C++ host 层 GaussianFilter.cpp 完成输入维度校验(如means.dim()==3means2d.dim()==4)、输出张量分配(含filter(gaussianNum+7)/8对齐)后调用aclnnGaussianFilter;Python 封装 gaussian_filter.py 透传全部输入与width/height/near_plane/far_plane,返回 8 个 culling 输出 +proj_filter+cnt

6.3 精度验证

测试用例 test_gaussian_filter.py 针对[1,1,10000][2,7,18471][1,1,117611][1,1,188152]四组形状,分别用 CPU 参考实现(_gaussian_filter,纯 torch 算子复现判据与位图编码)和 NPU 自定义算子执行,逐项assertRtolEqual对比 8 个 culling 输出、proj_filtercnt,保证融合算子与标准实现逐比特等价。

七、在 3DGS 训推样例中的使用

该算子属于 algorithms/gaussian_splatting 样例的四大 NPU 优化点之一(视锥剔除融合算子优化)。使用前需按样例 README 完成 CANN 环境准备(CANN 8.2.RC1 与 torch/torch_npu 2.1.0/2.1.0.post12),编译并安装自定义算子包:

cd ops/ascendc bash build.sh --python=3.8 # 生成 dist/*.whl pip install dist/*.whl --force-reinstall

随后即可在单卡 Atlas A2 环境启动 3DGS 训练或推理,视锥剔除算子作为投影预处理链路的前置步骤,自动参与每次前向渲染与反向梯度计算,帮助过滤掉对当前帧无贡献的高斯基元,从而降低下游光栅化与 Alpha blending 的计算量。

总结

GaussianFilter视锥剔除融合算子通过 int8 位图掩码、8 元素对齐 tiling、单次SyncAll的两阶段分核流程,以及GatherMask+Sort+Gather的反向 Scatter 还原,将"过滤无效高斯、压缩渲染负载"这一关键优化完整下沉到 NPU 算子层;配合Transpose搬运优化规避标量索引构建开销,最终以融合算子的形态接入 3DGS 训推链路,是理解昇腾平台上空间智能算法算子化改造的典型范例。

【免费下载链接】cann-recipes-spatial-intelligence本项目针对空间智能业务中的典型模型、加速算法,提供基于CANN平台的优化样例项目地址: https://gitcode.com/cann/cann-recipes-spatial-intelligence

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询