NPU硬件计算单元科普

训练营简介

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

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


思维导图

mindmap
  root((NPU硬件计算单元))
    课程定位
      理解硬件才能优化
      性能差距400倍
      NPU vs CPU/GPU
    Scalar标量单元
      工作原理
        单点计算
        串行执行
        类似CPU
      性能特点
        灵活性高
        效率低
        1个数据/周期
      适用场景
        流程控制
        条件判断
        参数计算
        初始化阶段
    Vector向量单元
      工作原理
        SIMD架构
        数据级并行
        128个FP16/周期
      性能提升
        相比Scalar 128倍
        向量运算
      适用场景
        向量运算
        数据预处理
        权重计算
        Element-wise操作
      注意事项
        数据对齐
        连续访问
        边界处理
    Cube矩阵单元
      设计特点
        专用矩阵运算
        脉动阵列结构
        16×16矩阵/周期
      性能指标
        相比Vector 16倍
        相比Scalar 400倍
        8192次运算/周期
      实例对比
        16×16矩阵乘法
        Scalar::8192周期
        Vector::512周期
        Cube::1周期
      适用场景
        矩阵乘法
        深度学习核心
        大规模并行
    协同工作
      典型流程
        Scalar::控制
        Vector::预处理
        Cube::核心计算
      流水线设计
        并行执行
        计算访存重叠
      插值算子示例
    优化策略
      选择决策树
      数据规模考量
      内存访问优化
      性能测试对比
    课程总结
      三种单元定位
      性能优化checklist
      学习计划

一、为什么要学习硬件计算单元?

1.1 课程引入

这节课周期龙老师开门见山地说:“如果不理解硬件,就无法写出高性能的算子!

老师的比喻:
就像开车,不了解发动机、变速箱的特性,就无法发挥汽车的最佳性能。算子开发也是一样,必须深入理解硬件。

1.2 NPU与CPU/GPU的区别

老师先给我们科普了NPU的定位:

处理器类型 全称 特点 适用场景
CPU Central Processing Unit 通用性强,灵活 操作系统、通用计算
GPU Graphics Processing Unit 大规模并行 图形渲染、通用并行计算
NPU Neural Processing Unit 专为AI设计 深度学习推理和训练

我的理解:
NPU就像专门为跑步设计的跑鞋,虽然不能打篮球,但跑步性能极佳。GPU像运动鞋,啥都能干但不够专精。

1.3 昇腾NPU的三种计算单元

老师画了一个清晰的架构图:

┌─────────────────────────────────┐
│         昇腾NPU架构              │
├─────────────────────────────────┤
│  Scalar   │  Vector  │  Cube    │
│  标量单元  │  向量单元 │ 矩阵单元  │
│  (控制)   │  (并行)  │  (核心)   │
└─────────────────────────────────┘

老师强调: 这三个单元各有所长,合理使用才能发挥最大性能!


二、Scalar标量计算单元详解

2.1 工作原理

老师用一个非常形象的例子讲解Scalar:

比喻: Scalar就像一个人,一次只能搬一块砖。

技术描述:

  • 每个时钟周期处理1个数据
  • 类似于传统CPU的工作方式
  • 串行执行,需要循环

2.2 代码示例

老师给了一个简单的例子:

// Scalar方式:处理128个数据
for (int i = 0; i < 128; i++) {
    output[i] = input[i] * 2.0f;  // 每次处理1个
}
// 需要128个周期

我的笔记:
这种方式最直观,但性能最低。如果有128个数据,就要循环128次!

2.3 性能特点

老师总结的性能特点:

优点:

  • ✅ 灵活性高:可以处理任何类型的运算
  • ✅ 控制能力强:支持复杂的if-else、循环等
  • ✅ 实现简单:代码逻辑清晰

缺点:

  • ❌ 速度慢:串行执行效率低
  • ❌ 算力低:无法利用并行性
  • ❌ 不适合大数据:处理大规模数据耗时长

2.4 适用场景

老师详细讲解了Scalar的最佳使用场景:

(1)算子初始化阶段
// 计算参数
int block_size = calculate_block_size(input_h, input_w);
int num_blocks = (total_size + block_size - 1) / block_size;

// 设置控制标志
bool need_padding = (input_h % 16 != 0);

老师说: 这些一次性计算,用Scalar足够了,没必要用Vector/Cube。

(2)复杂条件判断
// 边界处理逻辑
if (x < 0 || x >= width || y < 0 || y >= height) {
    // 越界处理
    value = 0;
} else if (x < border || y < border) {
    // 边界特殊处理
    value = border_value;
} else {
    // 正常处理
    value = input[y][x];
}

我的理解:
这种复杂的分支逻辑,Vector和Cube都不擅长,必须用Scalar。

(3)流程控制
// 主控制流程
for (int block = 0; block < num_blocks; block++) {
    // 分发任务给Vector和Cube
    if (block_type[block] == COMPUTE_HEAVY) {
        dispatch_to_cube(block);
    } else {
        dispatch_to_vector(block);
    }
}

2.5 老师的经验分享

“新手常犯的错误是,什么都用Scalar写。这样代码是能跑,但性能差得离谱!Scalar只应该用在控制和初始化,核心计算一定要用Vector或Cube。”


三、Vector向量计算单元详解

3.1 从串行到并行的飞跃

老师说Vector是性能提升的关键一步!

比喻: 如果Scalar是一个人搬砖,Vector就是128个人同时搬砖!

技术原理:SIMD

  • Single Instruction Multiple Data
  • 一条指令同时处理多个数据
  • 数据级并行

3.2 性能指标

老师给出的关键数据:

处理能力:

  • 单周期处理128个FP16数据
  • 或64个FP32数据
  • 相比Scalar提升128倍!

老师强调:
这不是理论值,是实测可以达到的性能!

3.3 代码对比示例

老师现场演示了性能差异:

// Scalar实现:处理128个数据
void scalar_multiply(float* input, float* output, int n) {
    for (int i = 0; i < 128; i++) {
        output[i] = input[i] * 2.0f;
    }
}
// 耗时:128个周期

// Vector实现:处理128个数据
void vector_multiply(float* input, float* output, int n) {
    __vec128 vec_in = vec_load(input);    // 一次加载128个
    __vec128 vec_2 = vec_dup(2.0f);       // 广播常量
    __vec128 vec_out = vec_mul(vec_in, vec_2);  // 一次计算128个
    vec_store(output, vec_out);           // 一次存储128个
}
// 耗时:1个周期(加上加载存储约5-10个周期)

性能对比:

  • Scalar: 128周期
  • Vector: ~10周期
  • 加速比: ~12.8倍

我惊呆了! 同样的功能,Vector快了10多倍!

3.4 Vector的适用场景

老师详细讲解了Vector擅长的任务:

(1)向量运算
// 向量加法
C = A + B  // A、B、C都是向量

// 向量点积
dot = sum(A * B)

// 向量缩放
B = A * scale
(2)数据预处理
// 图像归一化
normalized = (input - mean) / std

// 数据类型转换
fp16_data = convert_fp32_to_fp16(fp32_data)

// 激活函数
relu_output = max(input, 0)
(3)权重计算(插值算子中)
// 计算双线性插值的权重
u = frac(src_x);  // 小数部分
v = frac(src_y);

weight[0] = (1-u) * (1-v);
weight[1] = u * (1-v);
weight[2] = (1-u) * v;
weight[3] = u * v;

// 所有输出点的权重可以向量化计算

3.5 使用注意事项

老师特别强调的几个坑:

(1)数据对齐要求
// ❌ 错误:数据未对齐
float* data = malloc(130 * sizeof(float));  // 不是128的倍数

// ✅ 正确:数据对齐
float* data = aligned_alloc(128, 256 * sizeof(float));  // 对齐到128字节

老师的经验:
数据不对齐,Vector指令性能会大幅下降,甚至可能出错!

(2)连续访存的重要性
// ❌ 性能差:跳跃访问
for (int i = 0; i < n; i++) {
    vec_data[i] = input[i * stride];  // stride > 1,跳跃访问
}

// ✅ 性能好:连续访问  
for (int i = 0; i < n; i++) {
    vec_data[i] = input[i];  // 连续访问
}

我的理解:
连续访问可以充分利用缓存,跳跃访问会导致大量缓存未命中。

(3)边界处理
// 处理不是128倍数的数据
int vec_count = n / 128;      // 完整的向量数
int remain = n % 128;         // 剩余元素

// 向量化处理主体部分
for (int i = 0; i < vec_count; i++) {
    // Vector处理
}

// Scalar处理剩余部分
for (int i = vec_count * 128; i < n; i++) {
    // Scalar处理
}

四、Cube矩阵计算单元——性能之王

4.1 Cube的强大之处

老师讲到Cube时特别兴奋:“这才是NPU的真正实力!”

比喻:

  • Scalar:一个人搬砖
  • Vector:128个人并排搬砖
  • Cube:16×16=256个人组成方阵,协同作战!

4.2 核心性能指标

单周期完成:16×16矩阵乘法

老师详细解释了这意味着什么:

一次矩阵乘法 = 16×16×16 = 4096次乘法
                        + 4096次加法
                        = 8192次基本运算

全部在1个时钟周期内完成!

性能对比:

  • 相比Vector:提升16倍
  • 相比Scalar:提升数千倍

4.3 矩阵乘法实例

老师现场演示了16×16矩阵乘法的三种实现:

(1)Scalar实现
// 三层嵌套循环
for (int i = 0; i < 16; i++) {
    for (int j = 0; j < 16; j++) {
        float sum = 0;
        for (int k = 0; k < 16; k++) {
            sum += A[i][k] * B[k][j];  // 16次乘法+加法
        }
        C[i][j] = sum;
    }
}

// 计算量:16×16×16×2 = 8192次运算
// 耗时:约8192个周期(假设每次运算1周期)
(2)Vector实现
// 内层循环向量化
for (int i = 0; i < 16; i++) {
    for (int j = 0; j < 16; j++) {
        __vec16 a_row = vec_load(A[i]);    // 加载A的一行
        __vec16 b_col = vec_load_column(B, j);  // 加载B的一列
        C[i][j] = vec_dot(a_row, b_col);   // 向量点积
    }
}

// 内层循环向量化,减少16倍
// 耗时:约512个周期
(3)Cube实现
// 直接调用矩阵乘法指令
cube_matmul(C, A, B, 16, 16, 16);

// 耗时:1个周期!
// (实际加上数据搬运约10-20周期)

性能对比表:

实现方式 计算周期 相对性能
Scalar ~8192 1x
Vector ~512 16x
Cube ~20 400x

我的感受: Cube的性能简直是碾压级的!

4.4 Cube的设计原理

老师讲解了Cube为什么这么快:

(1)脉动阵列架构
PE = Processing Element(处理单元)

PE -- PE -- PE -- PE
 |     |     |     |
PE -- PE -- PE -- PE
 |     |     |     |
PE -- PE -- PE -- PE
 |     |     |     |
PE -- PE -- PE -- PE

工作原理:

  • 每个PE负责一个乘累加运算
  • 数据像脉搏一样在阵列中流动
  • 所有PE同时工作,高度并行
(2)专用数据路径
  • A矩阵:横向流动
  • B矩阵:纵向流动
  • C矩阵:在每个PE中累加

老师的比喻:
“就像流水线工厂,每个工位同时工作,效率极高!”

4.5 Cube的应用场景

(1)深度学习的核心运算

老师列举了几个典型场景:

全连接层:

Y = X @ W + b
X: (batch, in_features)
W: (in_features, out_features)
Y: (batch, out_features)

典型尺寸:(256, 512) @ (512, 1024)
用Cube性能最优!

卷积层(转换为矩阵乘法):

通过im2col将卷积转换为矩阵乘法
然后用Cube高效计算
(2)插值算子的矩阵化

老师回顾了之前课程的内容:

插值计算可以表示为:
Output = V_weight @ Input @ H_weight

V_weight: 纵向权重矩阵
H_weight: 横向权重矩阵
通过两次矩阵乘法完成插值

性能提升:

  • 原始向量化实现:10ms
  • 矩阵化+Cube实现:0.5ms
  • 提升20倍!
(3)Transformer中的Attention
Q @ K^T 然后 @ V

都是矩阵乘法,Cube大展身手的地方!

4.6 使用Cube的注意事项

老师总结的几个关键点:

(1)矩阵尺寸对齐
// Cube要求矩阵尺寸是16的倍数

// ❌ 不推荐:14×14矩阵
// 需要padding到16×16,浪费算力

// ✅ 推荐:16×16, 32×32, 64×64等
(2)数据重排的开销
// 有时需要重排数据以适应Cube

// 计算重排开销
transpose_time = 2ms
cube_compute_time = 0.5ms
total_time = 2.5ms

// 对比Vector实现
vector_time = 3ms

// 仍然值得!

老师的建议:
只要计算量足够大,即使有重排开销,Cube仍然是最优选择。

(3)适合大矩阵
小矩阵(<16×16):Cube优势不明显
中矩阵(16×16 ~ 64×64):Cube开始显示优势
大矩阵(>64×64):Cube压倒性优势

五、三种计算单元的协同工作

5.1 典型的算子执行流程

老师画了一个完整的流程图:

┌─────────────────────────────────────┐
│        算子执行流程                  │
├─────────────────────────────────────┤
│  Scalar: 初始化和控制                │
│    ├─ 计算分块参数                   │
│    ├─ 设置控制标志                   │
│    └─ 分发任务                      │
│         ↓                           │
│  Vector: 数据预处理                 │
│    ├─ 坐标计算                      │
│    ├─ 权重计算                      │
│    └─ 数据转换                      │
│         ↓                           │
│  Cube: 核心计算                     │
│    ├─ 矩阵乘法                      │
│    └─ 大规模并行运算                 │
│         ↓                           │
│  Scalar: 后处理                     │
│    └─ 结果整合                      │
└─────────────────────────────────────┘

5.2 插值算子的完整实现

老师用插值算子作为综合案例,详细讲解了三种单元的配合:

// 伪代码示例

void interpolation_operator() {
    // ========== Scalar部分 ==========
    // 步骤1: 计算分块参数
    TilingParams params;
    params.block_h = calculate_block_size(output_h);
    params.block_w = calculate_block_size(output_w);
    params.num_blocks = calculate_num_blocks(params);
    
    // 步骤2: 主循环控制
    for (int block = 0; block < params.num_blocks; block++) {
        
        // ========== Vector部分 ==========
        // 步骤3: 计算坐标映射
        vec_src_x = calculate_source_coords_x(block);
        vec_src_y = calculate_source_coords_y(block);
        
        // 步骤4: 计算插值权重
        vec_weights_h = calculate_weights_horizontal(vec_src_x);
        vec_weights_v = calculate_weights_vertical(vec_src_y);
        
        // 步骤5: 构建权重矩阵
        WeightMatrix H_weight = build_matrix_from_vec(vec_weights_h);
        WeightMatrix V_weight = build_matrix_from_vec(vec_weights_v);
        
        // ========== Cube部分 ==========
        // 步骤6: 横向插值(矩阵乘法)
        IntermediateResult = cube_matmul(InputData, H_weight);
        
        // 步骤7: 纵向插值(矩阵乘法)
        OutputBlock = cube_matmul(V_weight, IntermediateResult);
        
        // ========== Scalar部分 ==========
        // 步骤8: 结果写回(可能需要边界处理)
        write_output_with_boundary_check(OutputBlock, block);
    }
}

老师的讲解:
“看到没有?三种单元各司其职,配合无间!这才是高性能算子的正确打开方式!”

5.3 流水线并行优化

老师进一步讲解了如何让三种单元并行工作:

时间轴 →

块0: [S初始化][V权重计算    ][C矩阵计算       ]
块1:          [S初始化][V权重计算    ][C矩阵计算       ]
块2:                   [S初始化][V权重计算    ][C矩阵计算       ]

S = Scalar, V = Vector, C = Cube

流水线的好处:

  • Scalar、Vector、Cube可以同时工作
  • 处理块N+1时,块N的结果还在计算
  • 大幅提升整体吞吐量

老师的经验:
“做好流水线优化,性能可以再提升30%-50%!”


六、实战案例:矩阵乘法性能分析

6.1 测试环境

老师现场做了性能测试:

硬件: Atlas 200 DK(昇腾310)
任务: 256×256矩阵乘法
数据类型: FP16

6.2 三种实现的性能对比

(1)Scalar实现
// 三层循环,朴素实现
Time: 1280ms
Throughput: 26 GFLOPS
(2)Vector实现
// 内层循环向量化
Time: 85ms
Throughput: 392 GFLOPS
Speedup: 15x
(3)Cube实现
// 矩阵乘法指令
Time: 3.2ms
Throughput: 10432 GFLOPS
Speedup: 400x

性能曲线图(我用文字描述):

性能(GFLOPS)
    |
10000|                          * Cube
    |
 1000|              * Vector
    |
  100|
    |
   10|  * Scalar
    |___________________________
       Scalar   Vector   Cube

我的震撼感受: Cube的性能是Scalar的400倍!这就是专用硬件的威力!

6.3 不同矩阵尺寸的性能表现

老师还测试了不同尺寸:

矩阵尺寸 Scalar Vector Cube Cube优势
16×16 1.2ms 0.15ms 0.08ms 15x
64×64 20ms 1.5ms 0.35ms 57x
256×256 1280ms 85ms 3.2ms 400x
1024×1024 82s 5.5s 205ms 400x

规律总结:

  • 矩阵越大,Cube优势越明显
  • 到达一定规模后,加速比趋于稳定(~400x)

七、优化策略与最佳实践

7.1 如何选择计算单元?

老师给出了一个决策树:

开始
  │
  ├─ 是否是矩阵运算?
  │   ├─ 是 → 矩阵大吗(>16×16)?
  │   │   ├─ 是 → 使用Cube ⭐⭐⭐⭐⭐
  │   │   └─ 否 → 使用Vector
  │   └─ 否 → 是否是向量运算?
  │       ├─ 是 → 数据量大吗(>128)?
  │       │   ├─ 是 → 使用Vector ⭐⭐⭐⭐
  │       │   └─ 否 → 使用Scalar
  │       └─ 否 → 是否有复杂控制流?
  │           ├─ 是 → 使用Scalar ⭐⭐⭐
  │           └─ 否 → 根据具体情况

7.2 数据规模的影响

老师强调要根据数据规模选择:

小规模数据(<1KB):

  • Scalar或简单Vector
  • 避免复杂的数据重排
  • 追求简单直接

中规模数据(1KB~1MB):

  • 优先Vector
  • 能矩阵化就矩阵化用Cube
  • 权衡重排开销

大规模数据(>1MB):

  • 必须用Cube(如果适用)
  • 数据重排开销可以接受
  • 追求极致性能

7.3 内存访问模式优化

(1)连续访问 vs 跳跃访问

老师做了一个对比测试:

// 测试:读取10MB数据

// 连续访问
for (int i = 0; i < n; i++) {
    data = input[i];  // 连续
}
Time: 2ms

// 跳跃访问(stride=16)
for (int i = 0; i < n; i++) {
    data = input[i * 16];  // 跳跃
}
Time: 15ms

// 性能差距:7.5倍!

原因分析:

  • 连续访问:缓存命中率高(>90%)
  • 跳跃访问:缓存命中率低(<20%)
(2)数据对齐的重要性
// 未对齐
float* data = malloc(n * sizeof(float));
vec_load(data);  // 可能很慢

// 对齐到128字节
float* data = aligned_alloc(128, n * sizeof(float));
vec_load(data);  // 快速

// 性能差距:2-3倍

7.4 算子开发的黄金法则

老师总结的开发经验:

法则1:能用Cube就用Cube

  • 矩阵运算优先Cube
  • 即使有数据重排开销也值得

法则2:不能用Cube就用Vector

  • 向量运算必须向量化
  • 避免不必要的Scalar循环

法则3:Scalar只做控制

  • 初始化、分支判断用Scalar
  • 不要用Scalar做大量计算

法则4:注意内存访问

  • 连续访问优先
  • 数据对齐很重要
  • 减少跳跃访存

法则5:充分利用流水线

  • 让三种单元并行工作
  • 计算和访存重叠

八、课程总结与学习心得

8.1 老师的总结

老师最后做了精彩总结:

三种计算单元的定位:

┌─────────────────────────────┐
│  Scalar: 灵活的指挥官        │
│  - 控制流程                  │
│  - 处理复杂逻辑              │
│  - 性能最低但不可或缺         │
├─────────────────────────────┤
│  Vector: 高效的突击队        │
│  - 向量并行计算              │
│  - 数据预处理                │
│  - 性能提升100倍             │
├─────────────────────────────┤
│  Cube: 无敌的主力军          │
│  - 矩阵运算专家              │
│  - 深度学习核心              │
│  - 性能提升400倍             │
└─────────────────────────────┘

性能优化的核心思想:

  1. 分析计算模式
  2. 选择合适单元
  3. 优化内存访问
  4. 流水线并行

8.2 我的个人收获

知识层面:

  • ✅ 理解了三种计算单元的工作原理
  • ✅ 掌握了各自的适用场景
  • ✅ 了解了性能差异的本质原因

实践层面:

  • ✅ 学会了如何选择计算单元
  • ✅ 知道了性能优化的关键点
  • ✅ 理解了硬件特性对代码的影响

思维层面:

  • 算子性能优化要从硬件出发
  • 合适的工具做合适的事
  • 性能提升是系统工程

8.3 性能优化checklist

我整理的优化检查清单:

□ 分析算子的计算模式(矩阵/向量/标量)
□ 选择合适的计算单元
□ 检查数据是否对齐
□ 确保内存访问连续
□ 评估数据重排开销
□ 设计合理的分块策略
□ 考虑流水线并行
□ 进行性能测试
□ 分析性能瓶颈
□ 持续优化迭代

8.4 后续学习计划

第一阶段:理论深化

  • 学习SIMD编程
  • 研究脉动阵列架构
  • 理解缓存原理

第二阶段:动手实践

  • 实现向量化版本算子
  • 尝试矩阵化优化
  • 对比性能差异

第三阶段:综合应用

  • 优化实际网络算子
  • 达到硬件理论性能
  • 分享优化经验

九、课后思考题与我的答案

思考题1:为什么Cube单元比Vector快16倍?

我的理解:

  • Vector:一次处理128个FP16(128×1的向量)
  • Cube:一次处理16×16矩阵 = 256个数据
  • 且Cube是专用矩阵乘法单元,指令更高效
  • 所以性能是Vector的16倍

思考题2:什么情况下不应该用Cube?

我的分析:

  1. 矩阵很小(<16×16):启动开销大于计算收益
  2. 非矩阵运算:无法利用Cube的矩阵乘法能力
  3. 数据重排开销过大:小任务不值得重排数据
  4. 内存带宽受限:计算快但数据搬运慢

思考题3:如何判断算子是否充分利用了硬件?

我的方法:

  1. 计算理论峰值性能
  2. 测试实际性能
  3. 计算利用率 = 实际性能 / 理论性能
  4. 利用率 >80% 说明优化较好
  5. 利用率 <50% 说明还有很大优化空间

十、拓展阅读与参考资料

10.1 官方文档

  1. 昇腾CANN开发文档:https://www.hiascend.com/document
  2. NPU架构白皮书:硬件架构详细说明
  3. 性能优化指南:官方优化建议

10.2 相关技术

  1. SIMD编程:了解向量化基础
  2. 脉动阵列:Cube的硬件原理
  3. Roofline模型:性能分析方法

10.3 经典论文

  1. Google TPU论文:矩阵计算单元设计
  2. Systolic Array论文:脉动阵列经典文献

课堂笔记整理人: [你的名字]
课程日期: 2025年X月X日
讲师: 周期龙老师

学习心得:这节课颠覆了我对硬件的认识!原来性能差距可以达到数百倍。理解硬件才能写出高性能代码,这句话太对了!接下来要多实践,真正掌握这三种计算单元的使用。💪

重点知识卡片:

Scalar: 1个/周期,灵活但慢
Vector: 128个/周期,向量并行
Cube: 16×16矩阵/周期,矩阵运算之王

性能差距:Cube > Vector(16x) > Scalar(400x)
Logo

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

更多推荐