训练营简介
2025年昇腾CANN训练营第二季,基于CANN开源开放全场景,推出0基础入门系列、码力全开特辑、开发者案例等专题课程,助力不同阶段开发者快速提升算子开发技能。获得Ascend C算子中级认证,即可领取精美证书,完成社区任务更有机会赢取华为手机,平板、开发板等大奖。

报名链接:https://www.hiascend.com/developer/activities/cann20252#cann-camp-2502-intro

前言

在 PointPillars、CenterPoint 等主流 3D 检测网络中,第一步永远是 Voxelization。 它的任务是:把 3D 空间划分为均匀的网格(Voxel),并将落入同一个网格的点聚集起来,形成一个 Feature Map。

这个算子的难点在于:

  1. 输入不确定:点的数量是动态的(N),分布是稀疏的。

  2. 冲突处理:多个点可能落入同一个 Voxel,需要并发写入。

  3. 内存随机写:点在空间中是随机分布的,导致写回 Global Memory 时地址极度不连续。

这是一个典型的 Scatter(驱散) 类算子,也是检验 Ascend C 编程功底的试金石。

一、 核心图解:从“散沙”到“积木”

Voxelization 的过程,就像是在玩《我的世界》(Minecraft)。

二、 算法逻辑:哈希与冲突解决

假设我们有一个巨大的 Dense Map(代表整个 3D 空间),每个格子存一个计数器。

处理流程

  1. Coordinate:计算每个点 $(x, y, z)$ 对应的网格坐标 $(i, j, k)$。

  2. Address:计算网格在 Global Memory 中的线性地址。

  3. Atomic:对该地址的计数器做原子加 1 (cnt = atomic_add(addr, 1))。

  4. Filter:如果 cnt < max_points_per_voxel,则将该点的数据写在这个 Voxel 的第 cnt 个空位上。

三、 实战:Ascend C 实现

3.1 Kernel 类定义

输入是点云 points,输出是 voxels(特征)、coors(坐标)和 num_points(每个voxel里的点数)。

class KernelVoxelization {
public:
    __aicore__ inline void Init(GM_ADDR points, GM_ADDR voxels, GM_ADDR coors, GM_ADDR num_points, 
                                VoxelParams params) {
        // ... Init ...
        // grid_map 是一个辅助的 GM 空间,用于记录每个格子当前存了多少个点
        // 需要初始化为 0
        this->grid_map.SetGlobalBuffer((__gm__ int32_t*)workspace);
    }
    
    __aicore__ inline void Process() {
        // 按照点云数量进行 Tiling
        for (int i = 0; i < tileNum; i++) {
            Compute(i);
        }
    }
};

3.2 Compute 核心逻辑

这是最关键的部分,涉及大量的标量计算和随机访存。

__aicore__ inline void Compute(int32_t i) {
    // 1. 搬运一批点到 UB
    LocalTensor<float> pointsLoc = inQueueX.DeQue<float>();
    
    // 2. 遍历每一个点 (Vector 也可以做,但 Index 计算较复杂,这里演示标量逻辑)
    // 实际上,为了性能,应该尽量用 Vector 指令并行计算坐标
    
    // Step A: Vector 计算 Grid Index
    // grid_x = (x - x_min) / voxel_size_x
    // ... 使用 Muls, Adds, Cast ...
    
    // Step B: 标量处理冲突写入 (Scatter)
    for (int j = 0; j < points_per_tile; j++) {
        // 获取计算好的 Grid Linear Index
        int32_t grid_idx = indicesLoc.GetValue(j);
        
        // 3. 原子抢占位置
        // 这一步必须是原子的,因为不同核可能处理相邻的点
        // SetAtomicAdd 开启针对 grid_map 的原子加
        
        // 伪代码:AtomicAdd 返回旧值 (old_count)
        // int32_t old_count = AtomicAdd(grid_map[grid_idx], 1);
        
        // 注意:Ascend C 高阶 API 可能需要通过特定方式实现 Atomic Return
        // 或者使用 GlobalTensor 的原子接口
        
        // 假设我们拿到了 old_count
        if (old_count < max_points) {
            // 4. 将点数据写入 Voxels Tensor
            // 这是一个极其离散的写操作
            // voxels[grid_idx, old_count, :] = point_feature
            
            // 构造 Offset
            // 注意:这里 grid_idx 往往映射到一个压缩后的 Hash Key,或者直接是 Dense Index
            // 如果是 Dense,显存可能爆。通常配合 Unique 使用。
            
            int64_t dest_offset = grid_idx * max_points * feature_dim + old_count * feature_dim;
            
            // 写入 (使用 DataCopyPad 或 逐个元素赋值)
            // 这种随机写带宽极低,是性能瓶颈
            // DataCopy(voxelsGm[dest_offset], point_data, feature_dim);
            
            // 写入坐标
            // DataCopy(coorsGm[grid_idx], grid_coor, 4);
        }
    }
}

四、 性能优化的“生死劫”

上面的 Naive 实现有一个致命弱点:大量的 Global Memory 原子操作和随机写。在海量点云面前,MTE3 单元会崩溃。

进阶优化方案

  1. Point Sorting (预排序): 在 Voxelization 之前,先对点云按照 Grid Index 进行一次 Sort(排序)(参考第 41 期)。 排序后,属于同一个 Voxel 的点在内存中是连续的!

    • 效果:随机写变成了连续写(Burst Write)。原子操作变成了局部计数(Local Reduce)。

    • 代价:多了一个 Sort 算子。但在 Ascend 上,Bitonic Sort 比随机写快得多。

  2. UB Hashing (片上哈希): 如果 Voxel 数量不多,可以在 UB 中维护一个小的 Hash Table,先在 UB 聚合,满了一批再写回 GM。

五、 总结

Voxelization 是连接物理世界(点云)与数字世界(Tensor)的桥梁。

  1. 场景:自动驾驶、机器人感知。

  2. 挑战:极致的稀疏性与冲突处理。

  3. 对策:如果点云太乱,就先排序;如果写冲突太多,就用原子锁

掌握了这个算子,你就具备了优化 LiDAR 感知模型(如 PointPillars)底层的能力。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐