CV算子硬件计算单元科普
│ Scalar: 灵活的指挥官 ││ - 控制流程 ││ - 处理复杂逻辑 ││ - 性能最低但不可或缺 ││ Vector: 高效的突击队 ││ - 向量并行计算 ││ - 数据预处理 ││ - 性能提升100倍 ││ Cube: 无敌的主力军 ││ - 矩阵运算专家 ││ - 深度学习核心 ││ - 性能提升400倍 │分析计算模式选择合适单元优化内存访问流水线并行。
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倍 │
└─────────────────────────────┘
性能优化的核心思想:
- 分析计算模式
- 选择合适单元
- 优化内存访问
- 流水线并行
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?
我的分析:
- 矩阵很小(<16×16):启动开销大于计算收益
- 非矩阵运算:无法利用Cube的矩阵乘法能力
- 数据重排开销过大:小任务不值得重排数据
- 内存带宽受限:计算快但数据搬运慢
思考题3:如何判断算子是否充分利用了硬件?
我的方法:
- 计算理论峰值性能
- 测试实际性能
- 计算利用率 = 实际性能 / 理论性能
- 利用率 >80% 说明优化较好
- 利用率 <50% 说明还有很大优化空间
十、拓展阅读与参考资料
10.1 官方文档
- 昇腾CANN开发文档:https://www.hiascend.com/document
- NPU架构白皮书:硬件架构详细说明
- 性能优化指南:官方优化建议
10.2 相关技术
- SIMD编程:了解向量化基础
- 脉动阵列:Cube的硬件原理
- Roofline模型:性能分析方法
10.3 经典论文
- Google TPU论文:矩阵计算单元设计
- Systolic Array论文:脉动阵列经典文献
课堂笔记整理人: [你的名字]
课程日期: 2025年X月X日
讲师: 周期龙老师
学习心得:这节课颠覆了我对硬件的认识!原来性能差距可以达到数百倍。理解硬件才能写出高性能代码,这句话太对了!接下来要多实践,真正掌握这三种计算单元的使用。💪
重点知识卡片:
Scalar: 1个/周期,灵活但慢
Vector: 128个/周期,向量并行
Cube: 16×16矩阵/周期,矩阵运算之王
性能差距:Cube > Vector(16x) > Scalar(400x)
更多推荐



所有评论(0)