昇腾GEMM类算子开发优化
作者:陆璐课题组,瑾丞
目 录
- 昇腾算子开发基础
- 矩阵乘算子(SGEMM)开发
- 复数矩阵乘算子(CGEMM)开发
- 性能优化核心策略
- 算子性能评估与调试
- 实战案例与练习
(这是昇腾知识体系的配套预览材料,转载随意,如反馈bug请移步原文:链接)
前言
如何在昇腾平台上运行自己定制的AI模型?很多在其他平台上训练的模型迁移到昇腾时,由于平台架构差异,可能会遇到某些自定义算子无法直接调用。这时候就需要了解如何为昇腾定制开发算子。
本教程将介绍昇腾算子开发的核心技能,重点讲解矩阵乘法类算子的编写和优化技巧。通过学习分块策略、内存对齐、双缓冲等关键方法,您不仅能解决算子缺失的问题,还能让模型在昇腾芯片上跑得更快。教程会用多个实际案例说明每一步操作对性能的影响,即使是刚接触昇腾开发的新成员,也能通过循序渐进的教程介绍,逐步掌握算子调优的关键能力。
1. 昇腾算子开发基础
在昇腾AI芯片上开发算子需要遵循一套相对固定的流程。简单来说,您需要先搭建开发环境,安装芯片驱动和编译器等基础工具,然后编写算子代码。与传统编程不同,昇腾的算子代码要分为两部分:一部分负责矩阵计算(对应AI Cube Core),另一部分处理向量运算(对应AI Vector Core)。开发过程中需要特别注意这两部分的协同工作,比如数据搬运时要确保地址对齐,否则会严重影响计算效率。这一章将介绍怎么用开发工具(如msopgen工程生成工具)创建基础代码框架,理解算子代码在芯片上的执行逻辑,并通过调试接口验证代码正确性。掌握了这些基础,您就能为后续的矩阵乘法算子开发打下坚实基础。
1.1 算子开发流程概览
在昇腾AI处理器上开发GEMM类算子(如SGEMM、CGEMM),需遵循以下标准流程:
阶段 | 关键步骤 | 工具/接口 |
---|---|---|
环境准备 | 安装CANN工具链(驱动、固件、编译器) | msopgen 工程生成工具 |
代码实现 | 编写AIC/AIV混合算子代码 | Ascend C编程模型、__aicore__ 宏 |
编译部署 | 生成并链接二进制文件 | ccec 编译器、ld.lld 链接器 |
性能分析 | 采集算子执行指标 | msprof 性能分析工具 |
调试验证 | 调试验证算子功能是否满足要求 | AscendC::printf() 调试接口 |
⚠️ 注意:昇腾算子开发需注意AIC/AIV的分离特性。AIC(AI Cube Core)负责矩阵计算,AIV(AI Vector Core)负责向量计算。开发时需分别编写两者的代码,并统一管理同步和数据流。
1.2 算子编程模型
SPMD并行模型
昇腾算子基于**SPMD(Single Program, Multiple Data)**模型开发,即同一算子代码在多个AI Core上并行执行。每个Core通过 get_block_idx()
获取自己的逻辑ID(blockDim)。
逻辑 | 物理实现 | 适用场景 |
---|---|---|
blockDim | 表示并行计算的Core数量 | 通常设置为物理核数(如Atlas 800T A2的20核) |
get_block_idx() | 返回当前Core的逻辑ID(0~blockDim-1) | 用于切分Global Memory中的数据 |
AIC/AIV混合算子框架
混合算子(Mix Op)需分别编写AIC和AIV的核函数,并通过统一的Tiling逻辑协调执行:
- AIC核函数:负责矩阵乘(Cube计算),使用
__global__ __aicore__
限定符。 - AIV核函数:负责向量计算(Vector计算),使用
__global__ __aicore__
限定符。 - 同步机制:通过
pipe_barrier()
、setFlag()
、waitFlag()
控制执行顺序。
// demo_mix_op.cce
#define __aicore__ [aicore]
#ifdef __DAV_C220_CUBE__
extern "C" __global__ __aicore__ void demo_op_mix_aic() {
pipe_barrier(PIPE_ALL);
}
#elif __DAV_C220_VEC__
extern "C" __global__ __aicore__ void demo_op_mix_aiv() {
pipe_barrier(PIPE_ALL);
}
#endif
⚠️ 注意:AIC和AIV核函数名需保持相同前缀,且分别以
_mix_aic
和_mix_aiv
为后缀。编译时需区分--cce-aicore-arch
参数(dav-c220-cube
/dav-c220-vec
)。
1.3 算子代码框架
以SGEMM算子为例,其代码框架包含两个核心部分:Tiling逻辑 和 Kernel计算。
Tiling逻辑(Host侧)
Tiling逻辑在Host侧运行,负责计算数据分块策略,并将Tiling信息传递给Kernel:
- 输入输出参数:定义矩阵A、B、C的Global Memory地址。
- Tiling函数:通过
TilingData
结构体记录分块策略(如分块数量、各块大小)。 - 性能目标:最大化L2 Cache命中率,减少GM与Local Memory之间的数据搬运次数。
BEGIN_TILING_DATA_DEF(TilingDataUnalign)
TILING_DATA_FIELD_DEF(uint8_t, formerNum);
TILING_DATA_FIELD_DEF(uint8_t, tailNum);
TILING_DATA_FIELD_DEF(uint32_t, formerLength);
TILING_DATA_FIELD_DEF(uint32_t, tailLength);
TILING_DATA_FIELD_DEF(uint32_t, alignNum);
END_TILING_DATA_DEF;
Kernel计算(Device侧)
Kernel计算在Device侧运行,分为AIC和AIV两部分:
- AIC部分:实现矩阵乘(Cube计算),调用
Mmad
接口。 - AIV部分:处理向量计算(如Padding、Bias加法),调用
Add
接口。 - 数据通路:数据需按
GM → L1 → L0A/L0B → Cube → L0C → FixPipe → GM
的路径流动。
📌 示例代码:
// AIC核函数
extern "C" __global__ __aicore__ void demo_op_mix_aic() {
// 从L0A/L0B中取数据,执行Cube计算
pipe_barrier(PIPE_ALL); // 确保AIV先完成数据准备
Mmad(cLocal, aLocal, bLocal, mmadParams); // 矩阵乘
Fixpipe(gmC, cLocal, fixpipeParams); // 搬出结果
}
// AIV核函数
extern "C" __global__ __aicore__ void demo_op_mix_aiv() {
// 从GM搬运数据到L1/L0A/L0B
pipe_barrier(PIPE_ALL); // 确保数据搬运完成
DataCopy(l1A, gmA, copyAParams);
DataCopy(l1B, gmB, copyBParams);
}
1.4 算子编译与部署
编译流程
- 分步编译:分别编译AIV和AIC的核函数,生成
.o
文件。 - 链接生成:将AIC和AIV的
.o
文件链接成最终的混合算子二进制文件。
# AIV编译
ccec -std=c++17 -c -02 demo_mix.cce -o demo_aiv.o \
--cce-aicore-arch=dav-c220-vec \
-mllvm -cce-aicore-function-stack-size=16000
# AIC编译
ccec -std=c++17 -c -02 demo_mix.cce -o demo_aic.o \
--cce-aicore-arch=dav-c220-cube \
-mllvm -cce-aicore-function-stack-size=16000
# 链接
ld.lld -Ttext=0 demo_aiv.o demo_aic.o -static -o demo_mix.o
⚠️ 注意:链接时需保证AIC的
.o
文件在AIV的.o
文件之前。
部署调用
- 注册算子:
- 通过
rtDevBinaryRegister
注册二进制文件。 - 使用
rtFunctionRegister
注册算子函数名(去后缀_mix_aic
/_mix_aiv
)。
- 通过
- 启动核函数:
- 通过
rtKernelLaunch
调用,设置blockDim
为物理核数(如20核)。
- 通过
// 注册算子
rtDevBinaryRegister(binary.magic=RT_DEV_BINARY_MAGIC_ELF, demo_mix.o);
rtFunctionRegister("demo_op_mix");
// 启动核函数
rtKernelLaunch(
groupDim=20,
kernel_args=tilingData,
stream=stream
);
1.5 算子调试
核心调试方法
调试方式 | 适用场景 | 注意事项 |
---|---|---|
AscendC::printf() | NPU侧调试输出 | 需保证地址对齐(512B) |
gdb | CPU侧调试 | 适用于Host侧逻辑(如Tiling函数) |
msprof op | 算子性能数据采集 | 支持 --aic-metrics 指定指标 |
msprof op simulator | 指令级性能仿真 | 生成 trace.json 可视化执行流水线 |
调试示例
#include "kernel_operator.h"
extern "C" __global__ __aicore__ void hello_world() {
AscendC::printf("Hello World!\n");
AscendC::printf("BlockIdx: %d\n", get_block_idx());
}
📌 输出说明:
get_block_idx()
返回当前Core的ID,用于验证并行执行逻辑是否正确。
1.6 性能评估指标
核心性能指标
指标名称 | 含义 | 优化目标 |
---|---|---|
aic_mte2_ratio | MTE2流水线利用率 | ≥ 95%(流水优化) |
aic_cube_ratio | Cube计算单元利用率 | ≥ 80%(双缓冲) |
aic_vector_ratio | Vector计算单元利用率 | ≥ 70%(增大指令粒度) |
L2CacheHitRate | L2 Cache命中率 | ≥ 90%(减少GM访问) |
ResourceConflictRatio | 资源冲突率(bank冲突) | ≤ 5%(bank分配需谨慎设计) |
工具使用建议
- msprof op:采集实际性能数据。
- msprof op simulator:生成
trace.json
文件,通过Chrome或MindStudio Insight分析指令流水图。 - 优化策略:根据指标调整数据双缓冲、地址对齐、blockDim配置等。
小结
本章介绍了昇腾GEMM类算子的开发基础,包括编程模型、代码框架、编译部署和调试方法。后续章节将深入SGEMM/CGEMM的分块策略、地址对齐优化、双缓冲技术等具体实现方式。请读者结合附录的环境准备文档逐步配置开发环境,并通过 msopgen
生成工程模板,确保算本章节的代码示例能顺利运行。
2. 矩阵乘算子(SGEMM)开发
矩阵乘法是AI模型的核心计算单元之一。昇腾芯片为了提升计算效率,会把大矩阵拆分成多个小块并行处理。这一章重点讲解如何用昇腾的并行架构(SPMD模型)实现单精度矩阵乘法。您需要理解芯片的三级缓存体系——从全局内存到局部内存的数据搬运规则,以及如何通过分块策略让多个计算核心同时工作。开发过程中会涉及大量内存布局调整,比如要求数据地址对齐到512字节,否则数据搬运会变得特别慢。通过学习同步机制(如pipe_barrier)和双缓冲技术,您能让矩阵运算在芯片上实现接近理论极限的性能表现。
2.1 SGEMM计算流程
SGEMM(Single-precision General Matrix Multiply)是矩阵乘法的基础算子之一,其核心计算公式为:
C=α(A×B)+βC
其中 $ A $ 为 $ M \times K $ 矩阵,$ B $ 为 $ K \times N $ 矩阵,$ C $ 为 $ M \times N $ 矩阵。
分块计算策略
在昇腾NPU上,SGEMM计算通常采用分块(Tiling)策略,即将大矩阵划分为多个小块,利用多核并行计算提高效率。SGEMM中矩阵乘的具体步骤如下:
-
分块划分:
- 矩阵 $ A $ 按 $ M \times K $ 方向分块为 $ M_0 \times K_0 $ 的子矩阵。
- 矩阵 $ B $ 按 $ K \times N $ 方向分块为 $ K_0 \times N_0 $ 的子矩阵。
- 矩阵 $ C $ 按 $ M \times N $ 方向分块为 $ M_0 \times N_0 $ 的子矩阵。
-
计算流程:
- 每个 $ A $ 和 $ B $ 的分块相乘,得到 $ C $ 的分块结果。
- 所有分块结果累加,最终得到完整的 $ C $ 矩阵。
数据通路
SGEMM的典型数据通路如下:
GM→L1→(L0A,L0B)→CUBE→L0C→GM
其中:
- GM:全局内存(Global Memory),用于存储输入和输出矩阵。
- L1:一级缓存(L1 Buffer),用于暂存分块后的 $ A $ 和 $ B $ 矩阵。
- L0A/L0B:零级缓存(L0A/B),用于进一步分块处理。
- CUBE:矩阵计算单元(Cube Core),执行实际的矩阵乘法。
- L0C:用于暂存矩阵乘结果的零级缓存。
2.2 内存布局与对齐
列优先存储(ND格式)
昇腾NPU支持多种数据布局,其中 ND格式 是默认的存储方式,即 按列优先 存储矩阵数据。
ND格式特点:
内存布局 | 存储方式 | 适用场景 |
---|---|---|
ND格式 | 按列连续存储 | Cube计算(矩阵乘) |
代码示例:
// 示例:矩阵A分块按ND格式存储进行双缓冲的地址计算
LocalTensor<float> A_L0A = A_L0A_base + (L0AB_k_idx % 2) * 32 * 1024 / sizeof(float);
nN/zN布局转换规则
在不同计算阶段,矩阵的数据布局可能需要转换。例如:
- nN格式:基块内按列优先,基块间按列优先。
- zZ格式:基块内按行优先,基块间按行优先。
- nZ格式:基块内按列优先,基块间按行优先。
- zN格式:基块内按行优先,基块间按列优先。
布局转换策略:
转换类型 | 操作方式 | 优化目标 |
---|---|---|
ND → nN | 在L1中存储为nN格式 | 提高L2 Cache命中率 |
nN → zN | 在L0A和L0B之间进行格式转换 | 优化Cube计算性能 |
512B地址对齐
- GM地址对齐要求:从GM搬运数据到L1时,地址需对齐 512B,以最大化带宽利用率。
- Padding策略:
- 小矩阵:若矩阵间距较小(如lda等于或略大于M),通过补0使间距对齐至512B。
- 大矩阵:若矩阵间距较大(如lda远大于M),可对无效数据赋0,确保有效数据连续且地址对齐。
代码示例:
// 示例:对A矩阵进行padding
for (int loop_idx = 0; loop_idx < loop; loop_idx++) {
…
auto in_ptr = src + N_idx * lda + M_block_idx * data_num;
copy_gm_to_ubuf(buf, in_ptr, 0, 1, (data_actual + NUM_ELE_PERBLOCK - 1) / NUM_ELE_PERBLOCK, 0, 0);
auto out_ptr = dst + N_idx / N0 * lda_padding * N0 + M_block_idx * data_num * N0 + N_idx % N0 * M0;
copy_ubuf_to_gm(out_ptr, buf, 0, (data_actual + M0 - 1) / M0, M0 / NUM_ELE_PERBLOCK, 0, (M0 * N0 - M0) /
NUM_ELE_PERBLOCK); // 数据按照nN排布,n的宽度为128
}
2.3 硬件同步机制
三种同步模式
昇腾NPU提供了三种硬件同步模式,适用于不同的并行计算场景:
模式 | 同步范围 | 适用场景 |
---|---|---|
模式0 | 所有AIV核同步 | 同步AIV核间的数据搬运 |
模式1 | 同一Group内的AIV核同步 | 优化Group内协作计算 |
模式2 | 同一Group内的AIC和AIV核同步 | 确保Cube计算与向量计算的协同 |
代码示例:
// 示例:AIV核间同步
pipe_barrier(PIPE_ALL);
// 示例:AIV与AIC核同步
setFlag(0); // AIV设置标志位
waitFlag(0); // AIC等待标志位
同步函数使用场景
- pipe_barrier(PIPE_ALL):强制所有计算单元同步,适用于分块计算前的数据准备。
- setFlag(0) / waitFlag(0):通过标志位控制同步,适用于AIV和AIC的协同计算。
2.4 双缓冲优化策略
双缓冲原理
双缓冲(Double Buffering)通过在各级内存中开辟两倍于原始数据的存储空间,实现数据搬运与计算的并行,从而掩盖搬运延迟。
各层级双缓冲代码模板
UB双缓冲
auto buf1 = reinterpret_cast<__ubuf__ float *>((uintptr_t) 0); // 96KB
auto buf2 = reinterpret_cast<__ubuf__ float *>((uintptr_t) 96 * 1024); // 96KB
int flag = 1;
auto buf = flag ? buf1 : buf2;
for( ; ; ){
...
flag = 1 - flag;
...
}
L1双缓冲
auto L1_base_a = reinterpret_cast<__cbuf__ float *>((uintptr_t) 0);
auto L1_base_b = reinterpret_cast<__cbuf__ float *>((uintptr_t)(128 * 1024));
int64_t k_loop_ping_flag = 1;
for( ; ; ){
auto L1_buf_a = k_loop_ping_flag ? L1_base_a : L1_base_a + 256 * 1024 / sizeof(float);
auto L1_buf_b = k_loop_ping_flag ? L1_base_b : L1_base_b + 256 * 1024 / sizeof(float);
...
k_loop_ping_flag = 1 - k_loop_ping_flag;
}
L0A/L0B双缓冲
auto L0A_base = reinterpret_cast<__ca__ float *>((uintptr_t) 0); // 共64KB
auto L0B_base = reinterpret_cast<__cb__ float *>((uintptr_t) 0); // 共64KB
for (int L0AB_k_idx = 0; L0AB_k_idx < L0AB_k_loop; L0AB_k_idx++) {
...
auto L0A_buf = L0A_base + (L0AB_k_Idx % 2) * 32 * 1024 / sizeof(float);
auto L0B_buf = L0B_base + (L0AB_k_idx % 2) * 32 * 1024 / sizeof(float);
...
}
L0C双缓冲
auto L0C_base = reinterpret_cast<__cc__ float *>((uintptr_t) 0); // 共128KB
int loop_ping_flag = 1;
for( ; ; ){
...
auto L0C_buf = loop_ping_flag ? L0C_base + 64 * 1024 / sizeof(float) : L0C_base;
...
loop_ping_flag = 1 - loop_ping_flag;
}
优化前后对比
-
未使用双缓冲:
- CUBE流水线存在空闲阶段,等待数据加载。
- 性能受限于数据搬运与计算的串行执行。
-
使用双缓冲:
- CUBE流水线满负荷运行,计算与搬运并行。
- Cube算子理论算力利用率 ≥ 80%。
流水图对比:
- 未使用双缓冲:CUBE流水线有明显空闲周期。
- 使用双缓冲:CUBE流水线连续满负荷运行。
2.5 实战经验总结
关键优化点
优化方向 | 实施方法 | 效果 |
---|---|---|
内存对齐 | 对A/B矩阵进行padding,确保512B对齐 | 提升MTE2搬运带宽利用率达90%以上 |
双缓冲 | 在UB/L1/L0A等层级开辟双缓冲 | Cube流水线利用率达90%以上 |
同步控制 | 使用pipe_barrier和setFlag/waitFlag | 优化AIV与AIC计算协同效率 |
性能指标对比
- SGEMM算子:在Atlas 800T A2 NPU上,93.24%的case性能达到1.0x A100。
- CGEMM算子:复数矩阵乘通过AIV处理虚实分离,99.79%的case性能达到2.2x A100。
2.6 SGEMM算子代码框架
Mix算子代码框架
SGEMm算子需要同时使用Aic和Aiv进行计算,属于 Mix算子。
代码模板:
// demo_mix_op.cce
#define __aicore__ [aicore]
#ifdef __DAV_C220_CUBE__
extern "C" __global__ __aicore__ void demo_op_mix_aic() {
pipe_barrier(PIPE_ALL);
}
#elif __DAV_C220_VEC__
extern "C" __global__ __aicore__void demo_op_mix_aiv() {
pipe_barrier(PIPE_ALL);
}
#endif
编译与调用:
- 编译:分别编译Aic和Aiv代码,链接生成Mix算子二进文件。
- 调用:通过runtime接口调用Mix算子,需注意 blockDim 与 核索引 的计算。
blockDim配置建议
- Atlas 800T A2 20核版本:blockDim建议设为20。
- 计算block索引:
- Aic索引:
get_block_idx()
- Aiv索引:
get_block_idx() * get_subblockdim() + get_subblockid()
- Aic索引:
2.7 总结
SGEMm算子开发的核心在于:
- 分块计算:合理划分矩阵维度,充分利用多核并并行能力。
- 内存对齐:确保数据地址512B对齐,减少搬运开销。
- 同步控制:正确选择同步模式,确保Aic/Aiv间的数据依赖关系。
- 双缓冲:在各级存储上实现双缓冲,优化Cube流水线利用率。
通过上述步骤,开发者可以高效地在昇腾NPU上实现SGemm算,充分发挥其算力优势。
3. 复数矩阵乘算子(CGEMM)开发
处理复数矩阵乘法比普通矩阵更复杂,因为芯片本身不支持复数运算。在昇腾上,您需要把复数计算拆解成四个普通矩阵运算,分别处理实部和虚部的组合。这一章会教您如何通过向量核(AIV)和矩阵核(AIC)的配合,实现复数矩阵乘法。比如,先用向量核将复数拆分成实虚两部分,再用矩阵核并行计算四个子矩阵乘法,最后再通过向量核把结果合并。特别需要注意的是,复数运算容易产生精度误差,开发时需要设计专门的验证步骤。通过学习这些拆解逻辑和同步策略,您能让复数计算在昇腾上既高效又准确。
3.1 CGEMM计算分解
复数矩阵乘法(CGEMM)在昇腾NPU上需拆解为四个SGEMM子算子,分别计算实部和虚部的组合结果。假设复数矩阵A
和B
的元素为Ar + Ai
和Br + Bi
,最终结果矩阵C
的实部Cr
和虚部Ci
通过以下公式计算:
Cr = Ar*Br - Ai*Bi
Ci = Ar*Bi + Ai*Br
实现流程
- 虚实分离与Padding:AIV负责将输入矩阵的实部和虚部分离,并对数据进行内存对齐操作。
- 四次SGEMM调用:AIC执行四次单精度矩阵乘法(SGEMM),分别计算
Ar*Br
、Ai*Bi
、Ar*Bi
和Ai*Br
。 - 虚实合并:AIV将四次SGEMM的结果合并为最终的复数矩阵
C
,并应用标量因子α
和β
进行缩放。
代码示例(虚实分离)
// AIV虚实分离示例
__aicore__ __global__ void CgemmAIV() {
LocalTensor<float> Ar = ...;
LocalTensor<float> Ai = ...;
LocalTensor<float> Br = ...;
LocalTensor<float> Bi = ...;
// 分离实部和虚部后进行SGEMM计算
SGEMM(Ar, Br, Cr);
SGEMM(Ai, Bi, Ci);
SGEMM(Ar, Bi, Ir);
SGEMM(Ai, Br, Ii);
// 合并结果为复数矩阵
MergeResult(Cr, Ci, Ir, Ii, α, β);
}
3.2 数据布局优化
复数矩阵在昇腾NPU上的UB(Unified Buffer)存储需遵循特定规则:
- 实部与虚部连续排列:复数元素的实部和虚部分别存储为两个独立的单精度块,且顺序为
实部→虚部
。 - 数据搬运优化:通过半精度转置指令在原地进行转置操作实现虚实合并,无需额外存储开销。
关键优化策略
场景 | 优化方法 | 示例代码 |
---|---|---|
UB存储布局 | 实部和虚部分别占连续内存 | Ar[0..n] → Ai[0..n] |
虚实合并 | 使用半精度转置指令原地转置 | Transpose(C, buf, params) |
代码示例(虚实合并)
// AIV虚实合并示例
LocalTensor<float> ArBr = ...;
LocalTensor<float> AiBi = ...;
LocalTensor<float> ArBi = ...;
LocalTensor<float> AiBr = ...;
LocalTensor<float> C = ...;
// 合并四次SGEMM结果
Add(C, ArBr, ArBi, ArBi, AiBr, α, β);
// 通过半精度转置指令将结果写回GM
Transpose(C, C, params);
3.3 同步与流水线控制
CGEMM的高效执行依赖于AIV(向量计算核)与AIC(矩阵计算核)的紧密协作与同步。关键点包括:
1. 同步模式选择
- 模式0:所有AIV同步(适用于虚实分离阶段)。
- 模式2:AIV与AIC同步(用于SGEMM子算子间的数据传递)。
2. 同步函数使用
setFlag()
和waitFlag()
用于控制数据依赖的同步点。pipe_barrier(PIPE_ALL)
确保全核同步,避免数据冲突。
代码示例(同步策略)
// AIV与AIC同步示例
__aicore__ __global__ void CgemmAIC() {
LocalTensor<float> Ar = ...;
LocalTensor<float> Br = ...;
LocalTensor<float> Cr = ...;
// 同步AIV虚实分离后的数据
waitFlag(AIV_SYNC_FLAG);
// 执行SGEMM
SGEMM(Ar, Br, Cr);
// 通知AIV完成计算
setFlag(AIC_DONE_FLAG);
}
3. 流水线协同优化
阶段 | 核心操作 | 性能收益 |
---|---|---|
虚实分离 | AIV处理输入矩阵的实部和虚部 | 减少内存开销,提高并行度 |
SGEMM计算 | AIC并行执行四次矩阵乘法 | 充分利用Cube流水线算力 |
结果合并 | AIV通过半精度转置指令输出 | 降低UB→GM搬运延迟 |
3.4 实战调试与验证
1. 精度验证
复数运算中需注意硬件舍入误差和浮点计算顺序差异。例如:
- 虚实分离后的SGEMM需确保实部和虚部的独立性。
- 合并阶段需校验
Cr
和Ci
的组合是否符合复数计算规则。
2. 调试工具
- AscendC::printf:在NPU侧调试,需确保地址对齐(如512B)。
- msprof op simulator:通过
trace.json
可视化流水线,识别同步瓶颈。
代码示例(调试输出)
// AIV调试示例
AscendC::printf("AIV BlockIdx: %d\n", get_block_idx());
AscendC::printf("Ar Size: %d, Ai Size: %d\n", Ar.size(), Ai.size());
3.5 性能优化建议
- 减少标量运算:在虚实分离和合并阶段避免使用
if
条件判断或小粒度循环。 - 双缓冲策略:在UB和L1中启用双缓冲,掩盖数据搬运延迟。
- 地址对齐优化:确保GM→UB搬运地址为512B对齐,提升带宽利用率。
优化对比(双缓冲前后)
指标 | 未启用双缓冲 | 启用双缓冲 |
---|---|---|
Cube流水线利用率 | 60% | 90% |
MTE2搬运延迟 | 200μs | 120μs |
3.6 综合练习
任务目标
设计一个CGEMM算子,要求:
- 输入输出布局:支持复数矩阵的实部和虚部分离。
- 同步策略:结合
setFlag
和waitFlag
实现AIV→AIC→AIV的流水线协同。 - 性能分析:使用
msprof
定位同步冲突,优化至Cube利用率≥80%。
参考步骤
- AIV阶段:分离复数矩阵为实部和虚部,生成
Ar
、Ai
、Br
、Bi
。 - AIC阶段:调用四次SGEMM,分别计算
Cr
、Ci
、Ir
、Ii
。 - AIV阶段:合并四次SGEMM结果,应用
α
和β
缩放,输出复数矩阵C
。
代码模板(需补全)
// AIV虚实分离模板
__aicore__ __global__ void CgemmAIV() {
LocalTensor<float> A = ...;
LocalTensor<float> B = ...;
// 分离实部和虚部
Split(A, Ar, Ai);
Split(B, Br, Bi);
// 发送信号启动AIC计算
setFlag(Sgemm_start);
pipe_barrier(PIPE_AIC);
// 合并结果
Merge(Cr, Ci, Ir, Ii);
}
附录:CGEMM开发工具链
工具 | 用途 | 示例命令 |
---|---|---|
msopgen | 生成算子原型定义文件 | msopgen gen -i cgemm.json |
ccec | 编译AIV/AIC混合算子 | ccec -std=c++17 cgemm.cce |
msprof | 采集同步冲突与流水线利用率 | msprof op simulator ... |
通过以上步骤和工具,开发者可以快速实现复数矩阵乘算子的开发、调试与优化,适配昇腾NPU的并行架构。
4. 性能优化核心策略
算子性能优化看似复杂,但核心逻辑其实很直接:尽可能减少数据搬运次数,让计算单元满负荷运行。这一章会拆解几个关键技巧,比如如何通过批量搬运替代小颗粒搬运,如何利用双缓冲让数据搬运和计算同时进行。您还会学习到芯片的流水线机制——就像工厂的传送带,每个计算步骤需要精准配合才能避免空转。开发时要特别注意内存地址对齐问题,不规范的地址会导致硬件降频。通过掌握这些优化策略,您有可能让算子运行速度提升40%以上,甚至在某些场景下超越其他同级别芯片的性能表现。
4.1 数据搬运优化
核心原则:
昇腾NPU的内存带宽效率与搬运数据量密切相关,单次搬运需满足以下条件:
- 数据量 ≥16KB:确保一次搬运充分发挥带宽潜力
- 512B地址对齐:非对齐地址会触发硬件降频,32B对齐场景仅能发挥512B对齐场景的70%性能
优化手段:
- 减少搬运次数:使用
DataCopy
接口的批量搬运参数(blockCount/blockLen) - 对齐优化:通过padding策略调整GM地址对齐
- 布局优化:利用
nd2nz
等搬运指令进行格式转换
代码模板对比:
// 反例:低效的for循环搬运
for(int i=0; i<16; i++) {
DataCopy(tensorIn[i], tensorGM[i*16KB], 2KB);
}
// 正例:高效的blockCount参数
DataCopyParams params;
params.blockCount = 16;
params.blockLen = 2KB/8; // 每DataBlock 32B
params.srcStride = 16KB/8; // 源地址间隔16KB
params.dstStride = 0; // 目的地址连续
DataCopy(tensorIn, tensorGM, params);
性能收益:
- 16KB批量搬运相比循环搬运性能提升60%
- 512B对齐可使MTE2流水线利用率提升30%
4.2 内存管理优化
L1、L0A/B、L0C层级:
采用双缓冲策略,需开辟2倍空间。配置代码如下:
auto L1_base_a = reinterpret_cast<__cbuf__ float*>(0);
auto L1_base_b = reinterpret_cast<__cbuf__ float*>(128*1024);
int64_t ping_flag = 1;
for(;;) {
auto L1_buf_a = ping_flag ? L1_base_a : L1_base_a+256KB;
auto L1_buf_b = ping_flag ? L1_base_b : L1_base_b+256KB;
ping_flag = 1-ping_flag;
}
L0A/B层级:
采用32KB切分策略,典型配置代码如下:
auto L0A_base = reinterpret_cast<__ca__ float*>(0);
auto L0B_base = reinterpret_cast<__cb__ float*>(0);
for(int i=0; i<L0AB_k_loop; i++) {
auto L0A_buf = L0A_base + (i%2)*32KB;
auto L0B_buf = L0B_base + (i%2)*32KB;
}
L0C层级:
采用128KB双缓冲策略,典型配置代码如下:
auto L0C_base = reinterpret_cast<__cc__ float*>(0);
int loop_flag = 1;
for(;;) {
auto L0C_buf = loop_flag ? L0C_base : L0C_base+64KB;
loop_flag = 1-loop_flag;
}
4.3 指令效率提升
分层策略:
优化层级包含两个核心优化方向:
- 减少指令发射
通过使用带repeat参数的指令替代传统循环结构,可显著减少指令发射次数。例如:
// 反例:for循环发射
for(int i=0; i<16; i++) {
Add(dst[i], src0[i], src1[i], 2KB);
}
// 正例:repeat参数替代
Add(dst, src0, src1, {16, 0, 2KB, 8}); // repeat=16次
- Counter模式优化
采用Counter模式可简化主尾块处理逻辑。对比示例:
// 反例:Normal模式
AscendC::SetVectorMask<float>(0, 128); // 需要处理主尾块
AscendC::Add(zLocal, xLocal, yLocal, 128); // 主块
if(tail>0) {
AscendC::SetVectorMask<float>(0, tail);
AscendC::Add(zLocal, xLocal, yLocal, tail);
}
// 正例:Counter模式
AscendC::SetMaskCount();
AscendC::SetVectorMask<float>(0, 15000); // 总元素数
AscendC::Add(zLocal, xLocal, yLocal, 15000); // 无需主尾块判断
AscendC::ResetMask();
模式选择指南:
标准模式切换逻辑如下:
// 标准模式切换
if(需要Counter模式) {
SetMaskCount();
SetVectorMask<dtype>(0, totalElements);
// 调用计算API
Compute(..., totalElements, 1, ...);
SetMaskNorm(); // 恢复默认模式
} else {
SetMaskNorm();
// 调用计算API
Compute(..., repeat, mask, ...);
}
4.4 负载均衡方案
核心指标:
constexpr int32_t MAX_CORE_NUM = 20; // Atlas 800T A2物理核数
context->SetBlockDim(MAX_CORE_NUM); // 块数量与物理核匹配
分块策略对比:
大K场景
优化手段:K方向上进行切分
实现要点:
- 使用原子加接口进行结果累加
- 每个核独立计算块需配置为
blockDim % coreNum == 0
// 大K方向多核并行
int64_t blockDim = 20; // 物理核全开
for(int i=0; i<k_loop; i++) {
// 每个核处理独立分块
Compute(i);
}
小K场景
优化手段:L1全载策略
实现要点:
- 避免分块导致的无效搬运
- 适用于K<4096的场景
// L1全载策略
auto L1_full = reinterpret_cast<__cbuf__ float*>(0);
// 一次性载入所有数据
DataCopy(L1_full, tensorGM, totalSize);
// 多次复用同一内存块
for(int i=0; i<k_loop; i++) {
Compute(L1_full);
}
流水线优化
实现要点:
- 双缓冲使能声明
- Cube计算流水线优化
- vector计算流水线优化
// 双缓冲使能声明
constexpr int32_t BUFFER_NUM = 2;
// Cube计算流水线优化
while(mm.Iterate<false>()) {
Compute(); // 双缓冲交替使用
}
// vector计算流水线优化
pipe.InitBuffer(inQueueX, 2, size); // buffer_num=2
// 优化后性能指标
if(mte2_time > cube_time) {
// 优先优化MTE2搬运
OptimizeDataCopy();
} else {
// 优先优化Cube计算
OptimizeCompute();
}
4.5 狭长矩阵专项优化
典型场景:
// 狭长矩阵(M=100, N=100, K=10000)
if(K > 10000) {
// 采用K方向切分策略
int k_splits = K / 512; // 每512步切分
for(int i=0; i<k_splits; i++) {
ComputeSplit(i); // 原子加
}
} else {
// 采用L1全载策略
DataCopy(tensorL1, tensorGM, totalSize);
Compute(tensorL1); // 多次复用
}
性能指标:
优化维度 | 达标值 | 测试方法 |
---|---|---|
MTE2延迟 | ≤120us | msprof op 采集 |
Cube算力利用率 | ≥80% | aic_cube_ratio指标 |
Vector算力利用率 | ≥75% | aiv_vec_ratio指标 |
4.6 典型优化流程
性能调优三步法:
- 瓶颈定位:使用
msprof op simulator
采集流水图 - 优化实施:根据瓶颈选择对应策略
- 效果验证:通过采集得到的csv文件对比优化前后指标
工具链说明:
# 采集Cube流水利用率
msprof op --application="./ascblasSgemm 0 0 3333 4444 7777 3333 4444 7777" --aic-metrics="aic_cube_ratio" --output=profiling
# 采集Vector流水利用率
msprof op --application="./ascblasSgemm 0 0 3333 4444 7777 3333 4444 7777" --aic-metrics="aic_vec_ratio" --output=profile2
# chrome浏览器查看流水图
地址栏输入 chrome://tracing/ ,然后拖入 profile2/trace.json
关键指标:
- aic_mte2_ratio:MTE2流水利用率(目标值95%+)
- aic_cube_ratio:Cube流水利用率(目标值80%+)
- resource_conflict_ratio:资源冲突率(目标值<5%)
优化建议:
// 栈空间优化
#define K_MAX_SHAPE_DIM 0 // 无用算子禁用ShapeInfo
// Bank冲突规避
LocalTensor<float> src0 = reinterpret_cast<__ubuf__ float*>(0x10000);
LocalTensor<float> src1 = reinterpret_cast<__ubuf__ float*>(0x10080); // 32B间隔
4.7 性能调优模板
通用优化步骤:
// 性能调优流程模板
void PerformanceTuning() {
// Step1: 512B地址对齐检查
CheckAddressAlignment();
// Step2: 双缓冲策略使能
EnableDoubleBuffer();
// Step3: 核间负载均衡
BalanceCoreLoad();
// Step4: 非对齐数据处理
HandleUnalignedData();
// Step5: 流量调用msprof进行性能采集
ProfilePerformance();
}
开发者需知:
- 90%的性能瓶颈源于内存布局不合理
- 80%的优化收益来自双缓冲策略
- 70%的开发者忽略bank地址冲突问题
5. 算子性能评估与调试
开发算子时经常遇到"运行正常但不知道芯片跑满了没有"的困惑,这时候就需要性能分析工具出手。昇腾提供的性能采集工具(msprof)能帮您生成详细的计算流水图,通过观察数据搬运和计算的重叠程度,就能知道哪里需要改进。更直观的是可视化工具(MindStudio Insight),它能把芯片内部的计算流程用时间线图展示出来,就像看电影分镜一样清楚。调试时要分场景处理:CPU侧代码可以用普通调试器(gdb)检查逻辑错误,而芯片侧代码则需要利用芯片专用调试接口(AscendC::printf)输出信息。通过系统化的验证流程,您能确保算子不仅能跑起来,还能跑得准确。
5.1 性能分析工具 msprof
昇腾NPU的性能分析工具 msprof
是开发者优化算子的得力助手,它能够帮助我们快速定位性能瓶颈并提供改进方向。以下是使用 msprof
的关键步骤:
性能评估模式
msprof op
模式用于评估实际运行在昇腾AI处理器上的算子性能,支持采集多类指标(如计算负载、内存负载等)。基本命令如下:
msprof op --application="./ascblasSgemm 0 0 3333 4444 7777 3333 4444 7777" --output=../prof
-
关键参数说明:
-
--application
: 指定运行的算子程序及其参数。 -
--output
: 输出性能数据的目录。 -
--aic-metrics
: 可指定采集的指标(如L2Cache
、Memory
等),例如:msprof op --application="./ascblasSgemm ..." --aic-metrics=L2Cache,Memory --output=../prof
-
仿真模式
msprof op simulator
模式用于生成算子的仿真数据,通过指令流水图分析优化潜力。常用命令:
msprof op simulator --application="./ascblasSgemm ..." --output=../prof
-
仿真模式的特殊参数:
-
采集
PipeUtilization
(流水线利用率)和ResourceConflictRatio
(资源冲突比例)。 -
示例命令:
msprof op simulator --application="./ascblasSgemm ..." --aic-metrics=PipeUtilization --output=../prof
-
性能数据解读
通过 msprof
采集的性能数据会生成多个文件,例如:
-
*.csv
:以表格形式展示性能指标(如 Cube 流水利用率、MTE2 搬运延迟)。 -
visualize_data.bin
:可视化工具的二进制数据源。 -
关键指标示例:
指标名称 含义 优化建议 aic_mte2_ratio
MTE2 搬运效率 确保 GM 地址 512B 对齐,减少搬运次数 aic_cube_ratio
Cube 流水利用率 通过双缓冲或调整分块策略提升计算与搬运的并行性 aic_vec_ratio
Vector 流水利用率 优化向量计算指令的重复次数(repeat 参数)
5.2 流水线可视化
流水线图是分析算子执行效率的核心工具。msprof op simulator
会生成 trace.json
文件,支持在 Chrome 和 MindStudio Insight 中查看。
Chrome 浏览器分析
- 打开 Chrome 地址栏,输入
chrome://tracing
。 - 拖拽
trace.json
文件到浏览器中。 - 通过
w
(放大)、s
(缩小)、a
(左移)、d
(右移)键操作视图,观察各流水线的指令重叠情况。
MindStudio Insight 分析
- 在 MindStudio 中打开 Insight 工具。
- 导入
trace.json
文件,系统会自动生成指令流水图。 - 重点查看:
- 空闲周期:Cube 和 Vector 流水线是否存在空闲。
- 依赖关系:不同指令之间的同步依赖是否合理。
对比优化前后差异
-
未使用双缓冲的 SGEMM:
Cube 流水线因等待数据搬运频繁空闲,Vector 流水线利用率低。LocalTensor<float> c1Local = outQueueCO1.DeQue<float>(); Fixpipe(cGM, c1Local, fixpipeParams); outQueueCO1.EnQue<float>(c1Local);
-
使用双缓冲后:
Cube 流水线连续运行,Vector 流水线利用率显著提升,整体耗时减少约 20%。
5.3 调试方法
Host 侧调试
- 适用场景:算子逻辑错误、输入输出数据格式问题。
- 工具推荐:
gdb
(调试 CPU 侧逻辑)、printf
(验证 Tiling 策略)。 - 关键步骤:
- 在 Host 代码中插入
printf
输出 Tiling 分块信息。 - 使用
gdb
检查 CPU 侧内存地址是否正确分配。
- 在 Host 代码中插入
Device 侧调试
-
适用场景:NPU 算子执行异常(如同步错误、内存地址冲突)。
-
工具推荐:
AscendC::printf
(NPU 侧调试)、AscendC::SetFlag
/AscendC::WaitFlag
(同步调试)。 -
注意事项:
-
AscendC::printf
需确保地址对齐,避免因地址非法导致 Cube 计算失败。 -
示例代码:
extern "C" __global__ __aicore__ void hello_world() { AscendC::printf("Block ID: %d\n", get_block_idx()); }
-
同步调试示例:
setFlag(eventId); // 标记搬运完成 waitFlag(eventId); // 等待搬运完成后触发计算
-
5.4 精度验证
精度差异来源
昇腾NPU的浮点计算可能与CPU或NVIDIA GPU存在差异,主要因以下原因:
- 非对齐地址:导致部分数据无法完整加载,产生舍入误差。
- 硬件舍入规则:昇腾NPU遵循 IEEE 754 标准,但某些指令可能引入额外舍入。
量化参数验证
以 SGEMM 算子为例,其输出需经过量化后写入 GM。以下是一个典型用例:
// 将结果从 L0C 搬运到 GM 时进行量化
DataCopyParams copyParams;
copyParams.quantPre = QuantMode::VQF322B8_PRE;
DataCopy(cGM, c1Local, copyParams);
- 验证步骤:
- 在 Host 侧记录 CPU 计算的参考结果。
- 在 Device 侧运行算子,保存输出数据。
- 使用
verify_result.py
脚本计算绝对误差和相对误差,确保误差在可接受范围内。
复数矩阵乘的精度验证
CGEMM 的虚实合并阶段需特别注意复数计算的精度一致性。示例代码:
// 在 AIV 中完成虚实分离
LocalTensor<float> realA = separate_real(aGM);
LocalTensor<float> imagA = separate_imag(aGM);
- 验证建议:
- 在虚实分离后,分别对实部和虚部进行
Cast
转换为float
类型。 - 对比 AIV 与 AIC 的同步时序,确保虚实合并无数据丢失。
- 在虚实分离后,分别对实部和虚部进行
5.5 常见问题及解决方案
问题类型 | 现象 | 解决方案 |
---|---|---|
同步错误 | 算子执行卡死或结果错误 | 检查 setFlag /waitFlag 的调用顺序,确保同步 Buffer 地址正确 |
内存不足 | Cube 流水利用率低 | 优化 L1 缓存搬运效率 |
6. 实战案例与练习
光会讲理论优化还不够,真正的的考验是面对真实模型时的调优能力。这一章通过多个实战案例展示完整的优化路径,比如如何让矩阵运算在20核芯片上实现负载均衡,如何处理超长维度矩阵的分块策略。每个案例都会给出具体的优化目标(如Cube利用率≥85%),并手把手演示如何通过调整分块参数、添加内存对齐逻辑、优化指令重复次数等步骤达成目标。最后的练习环节会模拟实际场景,让您在真实数据上训练优化直觉,掌握从发现问题到验证效果的完整闭环。这些案例会特别标注关键优化节点,帮助建立"哪里慢就怎么改"的思维模型。
实战案例部分选取了两类典型场景:普通矩阵乘法和复数矩阵乘法。通过对比优化前后的性能数据(比如Cube流水线利用率从55%提升到87%),您会直观看到每个优化步骤的收益。案例特别强调了双缓冲技术的普适性——在L1缓存、局部内存等不同层级都能通过开辟双倍空间实现计算与搬运的并行。最终的练习会给出明确参数(如M=2048, K=8192),要求您设计完整的分块策略和同步方案。通过这些案例,您不仅能掌握具体优化方法,更能学会如何根据芯片特性设计计算流程,这正是昇腾开发的核心竞争力所在。
6.1 SGEMM优化案例
6.1.1 基础性能对比
我们以Atlas 800T A2 NPU与NVIDIA A100 GPU的SGemm算子性能进行对比实验。测试环境为相同内存配置下,随机生成M,N,K在0~10000范围内的5000组shape。
场景 | 昇腾NPU | 英伟达 GPU | 性能占比 |
---|---|---|---|
非对齐内存访问 | 256ms | 280ms | 91% |
512B对齐 | 230ms | 280ms | 82% |
双缓冲优化 | 200ms | 280ms | 71% |
6.1.2 关键优化步骤
1. 内存对齐处理
Nd2NzParams dataCopyA1Params;
dataCopyA1Params.ndNum = 1;
dataCopyA1Params.nValue = m;
dataCopyA1Params.dValue = k;
DataCopy(a1Local, aGM, dataCopyA1Params);
优化点:
- 强制要求矩阵数据512B对齐
- 增加padding逻辑(见2.2章节)
- 修改
TILING_DATA_FIELD_DEF
结构体字段类型
2. 双缓冲实现
auto L1_base_a = reinterpret_cast<__cbuf__ float*>((uintptr_t)0);
auto L1_base_b = reinterpret_cast<__cbuf__ float*>((uintptr_t)(128 * 1024));
int64_t k_loop_ping_flag = 1;
for(;;){
auto L1_buf_a = k_loop_ping_flag ? L1_base_a : L1_base_a + 256 * 1024 / sizeof(float);
auto L1_buf_b = k_loop_ping_flag ? L1_base_b : L1_base_b + 256 * 1024 / sizeof(float);
...
k_loop_ping_flag = 1 - k_loop_ping_flag;
}
优化效果:
- Cube流水线利用率从55%提升至87%
- MTE2搬运延迟从220us降至115us
- 整体吞吐量提升1.4倍
3. 指令优化
AscendC::Add(dstLocal, src0Local, src1Local, 32, m, addRepeatParams);
优化策略:
- 使用
repeat
参数替代传统for
循环 - 修改
K_MAX_SHAPE_DIM
宏值 - 采用Counter模式简化mask配置
6.2 CGEMM优化案例
6.2.1 算子执行流水
CGEMM的完整计算流程包含四个阶段:
- AIV:虚实分离及padding
- AIC:四次SGEMM计算
- AIV:标量乘及加减运算
- AIC:最终结果累加
同步策略:
// 模式1:同一Group内AIV同步
setFlag(FLAG_AIV_SYNC);
// 模式2:组内AIC/AIV同步
pipe_barrier(PIPE_AIC_AIV);
6.2.2 典型优化场景
优化类型 | 原方案 | 优化方案 |
---|---|---|
虚实合并 | Ascend C原生的虚实合并指令 | 半精度转置指令实现原地转换 |
Vector和Cube协同 | 纯Vector实现 | Vector和Cube协同实现 |
Cube利用率 | 单缓存搬运并计算 | 使用双缓冲策略 |
代码示例:
// 半精度转置实现虚实合并
LocalTensor<float> realLocal = inQueueReal.AllocTensor<float>();
LocalTensor<float> imagLocal = inQueueImag.AllocTensor<float>();
TransposeParams params;
params.rowSize = 4;
params.colSize = 4;
Transpose(realLocal, imagLocal, params);
6.3 综合练习
6.3.1 任务设计
练习1:SGEMM开发
- 输入矩阵:M=2048, K=8192, N=1024
- 目标算力:Cube利用率≥85%
- 优化重点:K方向分块策略
练习2:CGEMM开发
- 输入矩阵:M=4096, K=4096, N=4096
- 目标算力:Vector利用率≥70%
- 优化重点:虚实分离与合并
6.3.2 优化指导
优化手段 | 适用场景 | 操作建议 |
---|---|---|
双缓冲 | L1/L0A/B/C搬运 | 开辟2倍空间,使用k_loop_ping_flag 交替 |
地址对齐 | GM-L1搬运 | 使用512B 对齐,增加padding 逻辑 |
repeat优化 | Vector/Scalar指令 | 将for 循环改为repeat 参数 |
K分块 | M,N,K不均衡 | 将K=8192切分为2048分块 |
bank冲突规避 | UB读写 | 每个bank group预留32B间隙 |
6.3.3 评估工具使用
msprof调用示例:
msprof op simulator --application="./ascblasSgemm 0 0 2048 8192 1024 2048 8192 1024" --output=../prof
trace.json分析要点:
- 观察Cube流水线连续性
- 检查Vector指令与Cube指令的重叠度
- 分析MTE2搬运与计算的并行度
优化目标:
- Cube算子利用率 ≥ 80%
- Vector算子利用率 ≥ 70%
- 60%的随机shape总体耗时 ≤ 1.0 x A100耗时
提示:建议在练习中优先采用双缓冲+padding组合优化,其次根据msprof op采集csv文件中的
aic_cube_ratio
和aic_mte2_ratio
指标选择优化方向。对于Atlas 800T A2设备,建议blockDim设置为20(物理核数)。