前言

昇腾 NPU 的达芬奇架构以 “高并行” 为核心特性,而 Ascend C 核函数的线程模型是发挥硬件并行能力的关键。很多开发者入门时会因线程块(block)、线程(thread)的配置逻辑不清晰,导致算子性能无法达标。本文基于 CANN 训练营的内容,从线程模型的底层逻辑出发,结合实战案例讲清线程配置方法、并行粒度选择,以及如何通过线程索引适配不同维度的 Tensor。

一、Ascend C 的线程模型:SPMD+SIMD 的双层并行

Ascend C 的线程模型是 “线程块(grid)→线程(block)→向量计算单元” 的三层结构,结合了 SPMD 与 SIMD 的并行特性:

  1. SPMD(单程序多数据)

    • 一个核函数对应一个 “线程网格(grid)”,包含多个 “线程块(block)”;
    • 每个线程块包含多个 “线程(thread)”,所有线程执行同一核函数代码,通过blockIdx/threadIdx区分数据分片。
  2. SIMD(单指令多数据)

    • 每个线程对应达芬奇架构的一个 “向量计算单元”,可同时处理 8 个 float16 或 4 个 float32 数据;
    • Ascend C 通过vadd/vmul等向量指令自动实现 SIMD 并行,开发者无需手动编写向量操作。
二、线程配置的核心参数:gridDim 与 blockDim

核函数的启动需指定gridDim(线程块数量)与blockDim(每个线程块的线程数),两者的乘积是总线程数,需满足:

  • blockDim.x(线程块的线程数):达芬奇架构推荐为256 或 512(匹配硬件的计算单元数量);
  • gridDim.x(线程块数量):由 “总元素数 ÷blockDim.x” 向上取整得到,保证总线程数≥总元素数。

示例:若 Tensor 总元素数为 1024,选择blockDim.x=256,则gridDim.x=(1024+256-1)/256=4

三、线程索引与 Tensor 维度的映射方法

核函数中,线程通过blockIdx.x * blockDim.x + threadIdx.x计算出全局线程 ID(tid),再将 tid 映射到 Tensor 的元素索引。针对不同维度的 Tensor,映射方法不同:

3.1 一维 Tensor 的映射

一维 Tensor 的元素索引与 tid 直接对应:

c

运行

__global__ void OneDimKernel(const float* input, float* output, uint32_t size) {
    uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x;
    if (tid < size) {
        output[tid] = input[tid] * 2;  // 每个线程处理一个元素
    }
}
3.2 二维 Tensor 的映射

二维 Tensor(如[H, W])需将 tid 拆分为行索引与列索引:

c

运行

__global__ void TwoDimKernel(const float* input, float* output, uint32_t H, uint32_t W) {
    uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x;
    if (tid >= H * W) {
        return;
    }
    uint32_t h = tid / W;  // 行索引
    uint32_t w = tid % W;  // 列索引
    output[h * W + w] = input[h * W + w] * 2;
}
3.3 四维 Tensor 的映射

四维 Tensor(如[N, C, H, W])需将 tid 拆分为四个维度的索引:

c

运行

__global__ void FourDimKernel(const float* input, float* output, uint32_t N, uint32_t C, uint32_t H, uint32_t W) {
    uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x;
    uint32_t totalElements = N * C * H * W;
    if (tid >= totalElements) {
        return;
    }
    // 拆分索引:N→C→H→W
    uint32_t n = tid / (C * H * W);
    uint32_t c = (tid % (C * H * W)) / (H * W);
    uint32_t h = (tid % (H * W)) / W;
    uint32_t w = tid % W;
    // 计算元素位置
    uint32_t idx = n * C * H * W + c * H * W + h * W + w;
    output[idx] = input[idx] * 2;
}
四、并行优化:线程粒度与硬件的匹配

线程配置的核心是 “让线程数匹配硬件的计算能力”,需注意:

  1. 避免线程数过少:若总线程数远小于 AICore 数量(如 Ascend 910 有 32 个 AICore),会导致硬件资源闲置;
  2. 避免线程数过多:若总线程数远大于总元素数,会导致大量线程空转(需通过if (tid < size)过滤);
  3. 向量计算单元的利用:Ascend C 的向量指令默认处理 8 个 float16 数据,因此若输入是 float16 类型,可将线程数设置为 “总元素数 ÷8”,提升 SIMD 并行效率。

示例(float16 的向量并行):

c

运行

__global__ void Float16VectorKernel(const __half* input, __half* output, uint32_t size) {
    uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x;
    uint32_t vecSize = 8;  // float16的向量长度
    if (tid * vecSize >= size) {
        return;
    }
    // 向量加载(一次性读取8个float16)
    __half8 vecInput = vload8(tid, input);
    // 向量运算(一次性处理8个数据)
    __half8 vecOutput = vecInput * (__half)2.0f;
    // 向量存储
    vstore8(vecOutput, tid, output);
}
五、实战:线程配置对性能的影响

以 “1024×1024 的二维 Tensor 元素级乘法” 为例,对比不同线程配置的性能:

  • 配置 1blockDim.x=128gridDim.x=8192(总线程数 = 128×8192=1,048,576);
  • 配置 2blockDim.x=256gridDim.x=4096(总线程数 = 256×4096=1,048,576);
  • 配置 3blockDim.x=512gridDim.x=2048(总线程数 = 512×2048=1,048,576)。

测试结果(Ascend 910):

配置 执行时间(ms) 说明
1 0.82 线程块过小,硬件调度开销大
2 0.45 匹配达芬奇架构的线程块推荐值
3 0.51 线程块过大,部分 AICore 负载不均

结论:blockDim.x=256是更优的配置。

结语

Ascend C 的线程模型是发挥昇腾 NPU 并行能力的核心,其本质是 “用线程索引映射数据分片,用向量指令提升计算密度”。线程配置的关键是 “匹配硬件特性”—— 既需要选择 256/512 的线程块大小,也需要根据 Tensor 维度合理拆分索引。后续进阶内容中,线程模型还会与 “内存复用”“指令流水线” 结合,进一步提升算子性能。建议大家在实际开发中,通过 CANN 提供的aclprof性能分析工具,对比不同线程配置的执行时间,找到最优的并行粒度。

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

报名链接:https://www.hiascend.com/developer/activities/cann20252

Logo

CANN开发者社区旨在汇聚广大开发者,围绕CANN架构重构、算子开发、部署应用优化等核心方向,展开深度交流与思想碰撞,携手共同促进CANN开放生态突破!

更多推荐