算子调试介绍:

【图片知识总结】CPU和NPU孪生调试,提升算子调试效率

传统开发方式面临的两大问题:

问题具体表现
调试时间久NPU环境下调试困难;编译器编译过程隐藏并行细节
问题定位难并行执行时序、同步死锁问题;地址越界问题;数据计算精度、溢出问题

解决思路:通过 Ascend C 算子孪生调试技术,充分发挥 CPU 和 NPU 的调测优势,提升算子调试效率。同一份 Ascend C 算子源码经 Ascend C 类库编译,走两条链路:

  • GCC编译器 + GDB通用调试工具 → 生成通用 CPU.so(CPU域调试);

  • 毕昇编译器 + 打点图/Profiling工具 → 生成 NPU bin(NPU域调试)。

两域分工:

调试手段解决的问题
CPU域(调功能和精度)GDB调试;添加printf、cout打印调试;ASSERT断言调试逻辑错误定位;数据计算错误定位;内存问题定位
NPU域(调性能)仿真调试:Profiling流水图/指令日志/数据日志;上板调试:板上执行时间统计/数据打印性能问题定位;算子同步问题定位

原则:先在CPU域调对错(功能/精度),再到NPU域调性能。

算子调试分为:

在cpu上调试

【图片知识总结】CPU域最简单粗暴的调试手段:printf一下!

针对标量:在代码中直接编写 printf(...) 观察数值输出。样例:

printf("xLocal size: %d\n", xLocal.GetSize());
printf("tileLength: %d\n", tileLength);

针对LocalTensor:编写 LocalTensor.print() 观察数值输出,该接口在调试窗口中打印 LocalTensor 数据用于精度调试,每一行打印一个 datablock(32Bytes)的数据。样例:

// input_local:[0,1,2.......15]
input_local.Print();
// 0000: 0 1 2 3 4 5 6 7 8
// 0008: 9 10 11 12 13 14 15

【图片知识总结】CPU域精细调试手段:单步调试——GDB

可使用 gdb 单步调试算子计算精度。由于 CPU 调测已转为多进程调试(每个核都会拉起独立的子进程),故 gdb 需要转换成子进程调试的方式:

  • 针对 Atlas 推理系列产品、Atlas 训练系列产品:每个核拉起 1 个子进程

  • 针对 Atlas A2 训练系列产品 / Atlas 800I A2 推理产品:每个核拉起 3 个子进程(1个Cube + 2个Vector)

调试一个单独子进程的命令样例(前置条件:已是 kernel 直调工程且编译好可执行文件 add_custom_cpu):

gdb --args add_custom_cpu        # 启动gdb
set follow-fork-mode child       # 单独调试一个子进程
break add_custom.cpp:45          # 在add_custom.cpp源文件的第45行加断点
run                              # 执行至断点处
list                             # 列出断点附近代码
backtrace                        # 追踪栈信息
print i                          # 打印出标量i的值
break add_custom.cpp:56          # 在第56行加断点
continue                         # 继续执行至断点
display xLocal                   # 打印xLocal的相关信息
quit                             # 退出

在npu上调试

【图片知识总结】先调对错,再调性能:NPU域也可以print

针对标量:在代码中直接编写 AscendC::printf(...) 观察数值输出。样例:

AscendC::printf("xLocal size: %d\n", xLocal.GetSize());
AscendC::printf("tileLength: %d\n", tileLength);

针对LocalTensor/GlobalTensor:基于算子工程开发的算子,可以使用 DumpTensor 接口输出指定 Tensor 的内容,同时支持打印自定义的附加信息(仅支持 uint32_t 数据类型的信息,比如打印当前行号等)。在算子 kernel 侧实现代码中需要打印 Tensor 数据的地方调用:

DumpTensor(srcLocal, 5, dataLen);

Dump 时每个 block 核的 dump 信息前会增加信息头 DumpHead(32字节),用于记录核号和资源使用信息(block_id、total_block_num、block_remain_len、block_initial_space、magic);每次 Dump 的 Tensor 数据前也会添加信息头 DumpTensorHead(32字节),用于记录 Tensor 的相关信息(desc、addr、data_type、position=UB 等)。

内存检查工具

【图片知识总结】算子调试——内存检测 msSanitizer 功能介绍

msSanitizer 是基于昇腾AI处理器的一个异常检测工具,包含内存检测竞争检测两个子工具,可以针对算子开发调试中由内存异常导致的问题实现快速定位。

使用场景:算子内存检测、算子竞争检测、CANN软件栈内存检测。

支持的内存异常类型:非法读写、多核踩踏、非对齐访问、内存泄漏、非法释放、分配内存未使用。

使用命令

# 内存检测
mssanitizer --tool=memcheck ./add_custom_npu
# 竞争检测
mssanitizer --tool=racecheck ./add_custom_npu

输出结果:在工具运行的当前目录下生成 mssanitizer_{timestamp}_XXX.log 并打屏相关错误。例如实机日志显示:代码 matmul_leakyrelu_custom.cpp 的 115 行,在 block 1 的 AIV 计算单元上存在越界写的问题,大小为 65504B。日志解读要素:异常基本信息(非法读 read / 非法写 write 类型及被非法访问的字节数)、异常发生的内存位置信息(地址空间与内存首地址)、cube 核的 block 索引、pc 指针和调用 api 行为的序列号、异常发生代码的调用栈(文件名、行号、列号)。

【图片知识总结】msSanitizer 功能解读(编译选项与异常类型详解)

编译要求:编译时加 --cce-enable-sanitizer -g;链接阶段需要增加 --cce-enable-sanitizer --cce-aicore-arch={ARCH}

内存异常类型表:

异常名描述位置支持地址空间
非法读写由于访问了未分配的内存导致的异常kernel, hostGM
多核踩踏AI Core核心访问了重叠的内存导致的踩踏问题kernelGM
非对齐访问DMA(负责在Global Memory和Local Memory之间搬运数据)搬运的地址和长度与内存的最小访问粒度未对齐导致的异常kernelGM, UB, L0{A,B,C}, L1
分配内存未使用对内存分配后未使用导致的异常kernel, hostGM
内存泄漏申请内存使用后未释放,导致程序在运行过程中内存占用持续增加的异常hostGM
非法释放对未分配或已释放的地址进行释放导致的异常hostGM

竞争异常类型表:

异常名描述
Write-After-Write (WAW)两个内存事件尝试向同一块内存写入时存在风险,内存结果值取决于两个内存事件的实际访问顺序
Write-After-Read (WAR)一个事件读、一个事件写访问同一块内存时存在风险,即写操作实际在读操作之前执行完毕,导致读取到的内存值并非预期起始值
Read-After-Write (RAW)一个事件读、一个事件写访问同一块内存时存在风险,即读操作实际在写操作之前执行完毕,导致读取到的内存值还未更新

【图片知识总结】算子调优——msProf 功能介绍

msProf 用于采集和分析运行在昇腾AI处理器上算子的关键性能指标,用户可根据输出的性能数据,快速定位算子的软、硬件性能瓶颈,提升算子性能分析的效率。

重点特性:Block级数据采集;7类 aic-metrics 性能指标数据;指令流水图;代码行和指令耗时;代码热点图;指令与代码行映射。

使用命令

# 上板调优
msprof op ./add_custom_npu
# 仿真调优
msprof op simulator ./add_custom_npu

输出结果:在指定输出目录输出一个以 OPPROF_{time stamp} 开头的文件夹。

  • 上板调优输出:dump/、ArithmeticUtilization.csv、L2Cache.csv、Memory.csv、MemoryL0.csv、MemoryUB.csv、OpBasicInfo.csv、PipeUtilization.csv、ResourceConflictRatio.csv、visualize_data.bin;

  • 仿真调优输出:dump/simulator/ 下按 core0,veccore0 等目录存放各核的数据文件(code_exe.csv、instr_exe.csv、trace.json 仿真指令流水图文件)以及全量核的仿真指令流水图 trace.json。

矩阵编程

【图片知识总结】矩阵乘章节大纲:1. 矩阵乘基础知识;2. 矩阵乘的核函数;3. 矩阵乘的Tiling;4. 运行验证。

矩阵基础:

【图片知识总结】基础知识——矩阵乘基础

MatMul 的计算公式:C = A * B + Bias

  • A、B 为源操作数:A 为左矩阵,形状为 [M, K];B 为右矩阵,形状为 [K, N];

  • C 为目的操作数,存放矩阵乘结果的矩阵,形状为 [M, N];

  • Bias 为矩阵乘偏置,形状为 [N](按行广播叠加到结果矩阵上)。

【图片知识总结】逻辑位置(LocalTensor 的 Position/QueuePosition 概念)

在了解矩阵乘法数据流之前,需要先回顾几个重要的存储逻辑位置概念:

逻辑位置用途类比
A1存放整块A矩阵类比CPU二级缓存
B1存放整块B矩阵类比CPU二级缓存
C1存放整块Bias矩阵类比CPU二级缓存
A2存放切分后的小块A矩阵类比CPU一级缓存
B2存放切分后的小块B矩阵类比CPU一级缓存
C2存放切分后的小块Bias矩阵类比CPU一级缓存
CO1存放小块结果C矩阵可理解为Cube Out
CO2存放整块结果C矩阵可理解为Cube Out
VECCALC计算需要临时变量时使用此位置

【图片知识总结】矩阵乘法数据流

矩阵乘法数据流指矩阵乘的输入输出在各存储位置间的流向(五个阶段:CopyIn → Split → Compute → Aggregate → CopyOut):

  • A矩阵从输入位置到A2(输入位置可以是GM或者VECOUT):GM->A2,GM->A1->A2;VECOUT->A1->A2;

  • B矩阵从输入位置到B2(输入位置可以是GM或者VECOUT):GM->B2,GM->B1->B2;VECOUT->B1->B2;

  • 完成 A2*B2=CO1 计算;

  • CO1 数据汇聚到 CO2:CO1->CO2;

  • 从 CO2 到输出位置(输出位置可以是GM或者VECIN):CO2->GM / CO2->VECIN。

数据流图结构:GM 经 CopyIn 搬入 A1/B1/C1 与 VECIN,经 Split 切分到 A2/B2/C2,Compute 完成矩阵乘得到 CO1,Aggregate 汇聚到 CO2,最后 CopyOut 搬回 GM;各逻辑位置对应 Local Memory 上的 BufPool1~BufPool5 缓冲池。

【图片知识总结】Matmul矩阵乘——kernel实现五步骤

  1. 创建Matmul对象;2. 初始化;3. 设置左矩阵A、右矩阵B、Bias;4. 完成矩阵乘操作;5. 结束矩阵乘操作。

// 1、创建Matmul对象
typedef MatmulType<TPosition::GM, CubeFormat::ND, half> aType;
typedef MatmulType<TPosition::GM, CubeFormat::ND, half> bType;
typedef MatmulType<TPosition::GM, CubeFormat::ND, float> cType;
typedef MatmulType<TPosition::GM, CubeFormat::ND, float> biasType;
Matmul<aType, bType, cType, biasType> mm;
mm.Init(&tiling, &tpipe); // 初始化
​
// 2、设置左矩阵A、右矩阵B、Bias
mm.SetTensorA(gm_a);   // 设置左矩阵A
mm.SetTensorB(gm_b);   // 设置右矩阵B
mm.SetBias(gm_bias);   // 设置Bias
​
// 3、完成矩阵乘操作
while (mm.Iterate()) {   // 方式一:单次迭代叠加while循环
    mm.GetTensorC(gm_c);
}
//mm.IterateAll(gm_c);   // 方式二:IterateAll完成单核全量数据计算
​
// 4、结束矩阵乘操作
mm.End();

【图片知识总结】Matmul矩阵乘——实现整体流程(host侧 + kernel侧)

Ascend C 提供一组 Matmul 高阶API,封装了常用的切分和数据搬运、计算的算法逻辑,方便用户快速实现 Matmul 矩阵乘法的运算操作。开发者在 host侧通过调用API自动获取Tiling参数,该参数传递到 kernel侧后,在初始化操作时传入,通过几个简单的API即可完成矩阵乘操作。

host侧流程:创建Tiling对象 → 设置A、B、C、Bias的数据类型、格式 → 设置矩阵shape信息 → 设置可用空间大小信息 → 按需设置其他信息 → 获取Tiling参数。

kernel侧流程:创建Matmul对象 → 初始化操作(传入Tiling参数) → 设置左矩阵A、右矩阵B、Bias → 完成矩阵乘操作(方式一:单次迭代Iterate叠加while循环;方式二:IterateAll完成单核全量数据计算) → 结束矩阵乘操作。

AICore有两种架构:

第一种是分离架构,分离架构将AIcore分成两个计算单元,一个是AIC(cube-矩阵计算)另一个是AIV(标量和向量计算),这两个单元通过GM进行数据交互。分离架构比耦合架构多了两个缓存单元,一个是BTbuffer另一个是FPbuffer。

【图片知识总结】分离架构详解

分离架构将 AI Core 拆成矩阵计算(AI Cube,AIC)向量计算(AI Vector,AIV)两个独立的核,每个核都有自己的 Scalar 单元,能独立加载自己的代码段,从而实现矩阵计算与向量计算的解耦,在系统软件的统一调度下互相配合达到计算效率优化的效果。AIV 与 AIC 之间通过 Global Memory 进行数据传递,比耦合架构增加了两个 Buffer:BT Buffer(BiasTable Buffer,存放Bias)FP Buffer(Fixpipe Buffer,存放量化参数、Relu参数等)

  • AIC架构:包含5个并行执行单元(搬运单元和计算单元):MTE1、MTE2、MTE3、Cube、Scalar;包含7个内存单元:GM(核外)、L1、LOA、LOB、LOC、BiasTable Buffer、Fixpipe Buffer。

  • AIV架构:包含4个并行执行单元:MTE2、MTE3、Vector、Scalar;包含2个内存单元:GM(核外)、UB。

  • 典型计算数据流

    • Vector计算:GM-UB-[Vector]-UB-GM;

    • Cube计算:GM-L1-LOA/LOB-Cube-LOC-FixPipe-GM,或 GM-L1-LOA/LOB-Cube-LOC-FixPipe-L1。

第二种是耦合架构

【图片知识总结】耦合架构详解

耦合架构是指 Cube 计算单元和 Vector 计算单元同核部署。架构图中列出了计算架构中的存储单元和计算单元,箭头表示数据处理流向,MTE1/MTE2/MTE3 代表搬运单元。

数据通路:GM 经 MTE2 搬入 L1 Buffer(Cube 输入路径)和 UB(Vector 输入路径);L1 经 MTE1 搬入 Buffer LOA / Buffer LOB 供 Cube 计算,结果存入 Buffer LOC;LOC 与 UB 之间可互搬;UB 与 Scalar、Vector 计算单元交互。

注(虚线箭头含义)

  • Atlas 训练系列产品:不支持 Scalar 直接读写 GM 数据;

  • Atlas 推理系列产品(Ascend 310P处理器):支持 Scalar 直接读写 GM 数据。

融合算子

【图片知识总结】融合算子章节大纲:1. 再谈架构;2. 融合算子基础知识;3. 以Matmul+LeakyRelu为例;4. 代码精讲&运行验证。

什么是融合算子

【图片知识总结】融合算子定义与典型例子

融合算子是指将多个独立的"小算子"融合起来成为一个"大算子",多个小算子的功能和大算子的功能等价,大算子的性能优于独立的小算子。可以根据具体算法的实现自由融合 Vector、Cube 算子以达到性能上的收益。

典型例子:LLM 大模型中最核心的融合算子 Flash Attention——将 MatMul 算子(Cube)、Scale 算子(Vector)、Mask 算子(Vector)、SoftMax 算子(Vector)融合为一个大的算子。数据流:Q、K 先经 MatMul → Scale → Mask → SoftMax → 再与 V 一起经 MatMul 输出。

【图片知识总结】融合算子的使用场景和优势(为什么融合能带来性能收益)

当对算子的性能要求较高时,可以通过融合算子编程的方式,将矢量算子和矩阵算子进行融合,通过一个算子 kernel 函数来承载,由此来获得性能上的收益。时间轴对比图显示:独立矢量算子和矩阵算子串行执行(Cube→Vector→Cube→Vector…)耗时长;Mix 融合算子通过流水并行(Cube 与 Vector 交错重叠执行)耗时显著缩短。

  • 独立的矢量算子和矩阵算子实现:矩阵计算后的结果需要搬运到 Global Memory 上,然后由 Global Memory 搬运到 LocalMemory,再进行矢量算子的计算,计算和搬运都是串行执行;另外多个算子的调度执行,会增加算子的调度耗时。

  • 融合算子的实现方法:可以对数据进行切片,再通过流水的设计,使得矢量计算单元和矩阵计算单元实现并行计算;另外相比于不融合的单算子,减少了算子的调度耗时。

【图片知识总结】融合算子的其他优势(What else?)

除了有效提升算子性能、充分发挥AI处理器的算力,融合算子还有如下优势:

  • 减少计算量:融合算子可以将多个算子合并为一个,简化计算过程,减少计算量,提高计算效率;

  • 减少内存占用:融合算子可以将多个算子的中间结果合并为一个,从而减少内存占用,提高内存利用率;

  • 优化数据流:融合算子可以优化数据流,减少数据在不同算子之间的传输,从而提高数据处理效率;

  • 简化代码实现:融合算子可以简化代码实现,减少代码量,提高代码可读性和可维护性。

总之,融合算子是一种优化计算的有效手段,可以提高计算效率和内存利用率,优化数据流,简化代码实现。

算子编程范式

【图片知识总结】融合算子的编程范式

Ascend C 提供融合算子的编程范式,方便开发者基于该范式表达融合算子的数据流,快速实现自己的融合算子。融合算子数据流指融合算子的输入输出在各存储位置间的流向。以一个典型的 Cube 和 Vector 融合算子为例(五阶段:CopyIn → Split → Compute → Aggregate → CopyOut,对应 Local Memory 上的 BufPool1~BufPool5):

  • Cube的输出可以作为Vector的输入:CO2->VECIN;

  • Vector的输出可以作为Cube的输入:VECOUT->A1->A2、VECOUT->B1->B2。

在这个范式流程中,比如第一次是cube计算,可以将copyOut的结果直接传入VECIN,不用在存入LM搬出到GM在通过上述搬入到VECIN,省去了数据流转时间,存储消耗等。

举例融合算子

【图片知识总结】一个基础的MatMul+Vector计算数据流

基于 Matmul 高阶API的融合算子编程范式,简化表达为5步:

  1. 初始化一个MatMul对象,将输入数据从Global Memory搬运到Cube核上;

  2. 进行MatMul内部的计算;

  3. 将MatMul的计算结果搬运到Vector核上(AIC → VECIN);

  4. 进行Vector矢量计算(Vec);

  5. 将输出结果(VECOUT)搬运到Global Memory上。

算子分析

【图片知识总结】举个栗子——Matmul+LeakyRelu融合算子的算子分析

以 Matmul+LeakyRelu 融合算子的实现为例,介绍 Mix 融合算子的设计和实现流程。算子的设计过程分为算子分析、数据流分析、Tiling策略设计三部分。

① 明确算子的数学表达式及计算逻辑:先进行一个矩阵乘操作,然后将矩阵乘的结果与一个 alpha 参数进行 LeakyRelu 操作。数学表达式:c = LeakyRelu(a * b + bias, alpha);其中 LeakyRelu:f(x)=x(若 x≥0),f(x)=αx(若 x<0)。

② 明确输入和输出

  • 算子输入为 a、b、bias、alpha,输出为 c;

  • a、b 支持的数据类型为 half(float16),bias、alpha 支持的数据类型为 float32,输出 c 的数据类型为 float32;

  • 输入矩阵 a 的形状为 [M, K],输入矩阵 b 的形状为 [K, N],输出矩阵 c 的形状为 [M, N],输入 bias 的形状为 [1, N];

  • 算子输入输出支持的数据格式为:ND。

③ 确定核函数名称和参数:核函数命名为 matmul_leakyrelu_custom;参数 a、b、bias、alpha 为输入在 Global Memory 上的内存地址,c 为输出在 Global Memory 上的内存地址。

【图片知识总结】设计规格一览

项目内容
算子类型(OpType)MATMUL_LEAKYRELU
算子输入 ashape [M, K],data type half,format ND
算子输入 bshape [K, N],data type half,format ND
算子输入 biasshape [1, N],data type float32
算子输入 alpha标量,data type float32
算子输出 cshape [M, N],data type float32,format ND
核函数名称matmul_leakyrelu_custom
数据流分析

【图片知识总结】数据流分析(5个Stage的流水规划)

数据流向为在 Cube 核上完成 Matmul 计算后将数据搬运至 Vector 核进行 LeakyRelu 计算。根据上述数据流并结合融合算子的编程范式,规划并行的流水任务:

  • Stage1:将输入数据(aGlobal、bGlobal、biasGlobal)从 Global Memory 搬运到 Cube 核(aLocal、bLocal、biasLobal);

  • Stage2:进行 MatMul 内部的计算(Matmul Compute);

  • Stage3:将 MatMul 的计算结果(reluOutLocal)搬运到 Vector 核;

  • Stage4:进行 Vector 矢量计算(LeakyRelu Compute),结果经 EnQue 入队 reluOutQueue_;

  • Stage5:DeQue 出队后经 DataCopy 将输出结果(reluOutLocal → cGlobal)搬运到 Global Memory。

【图片知识总结】数据流简化

前三步的内容都封装在 Matmul 高阶API内,本样例中可以简化为3个stage:

  • Stage1: MatMul Compute:GlobalTensorA、GlobalTensorB → MatMul → LocalTensorC;

  • Stage2: LeakyRelu Compute:LeakyRelu 计算后 EnQue 到 VECOUT 队列;

  • Stage3: CopyOut:DeQue 后 DataCopy 到 GlobalTensorC。

根据上述分析,明确实现过程中会使用到 Matmul高阶API接口、LeakyRelu Vector计算接口、DataCopy、EnQue、DeQue 接口

Tiling策略设计

【图片知识总结】Tiling策略设计(多核切分 + 核内切分)

  • 多核切分:根据当前核数,对输入 shape M, K, N 进行多核切分,得到单核内 shape 大小 singleCoreM, singleCoreK, singleCoreN;

  • 核内切分:根据 Local Memory 的大小约束,对单核内的 shape 大小进一步切分,得到 A、B、C 矩阵参与一次矩阵乘指令的 shape 大小 baseM, baseN, baseK。切分时需要注意:GetTensorC 的结果如果放在 LocalMemory(UB)上,baseM * baseN 的大小不能超出 UB 的限制。

切分策略示意图:TensorA [M, K] 上先取 singleCoreM×singleCoreK 的单核块,再取 baseM×baseK 的指令块;TensorB [K, N] 上先取 singleCoreK×singleCoreN,再取 baseK×baseN;TensorC [M, N] 对应逐块累加输出。

算子实现

【图片知识总结】算子实现(host侧 + kernel侧整体流程)

Ascend C 提供一组 Matmul 高阶API,封装了常用的切分和数据搬运、计算的算法逻辑。融合算子中的矩阵编程的部分实现与之类似,开发者在 host 侧通过调用API自动获取Tiling参数,该参数传递到 kernel 侧后,在初始化操作时传入,通过几个简单的API即可完成矩阵乘操作。再结合上文的融合算子的编程范式,融合算子实现的步骤如下:

host侧:创建Tiling对象 → 设置A、B、C、Bias的数据类型、格式 → 设置矩阵shape信息 → 设置可用空间大小信息 → 按需设置其他信息 → 获取Tiling参数(分别提供 Matmul 计算需要的 Tiling 参数和 LeakyRelu 计算需要的 Tiling 参数)。

kernel侧:Stage1 Matmul Compute(将输入数据从GM搬运到Cube核 → Matmul计算 → 将Matmul的计算结果搬运到Vector核上) → Stage2 LeakyRelu Compute → Stage3 CopyOut → 结束。

算子的性能优化

【图片知识总结】学习分析算子的理论性能评估

1. 输入条件

  • 芯片参数:包括通路带宽、buff大小、计算指令的cycle数数据;

  • 计算flops分析、计算的数据搬运量分析。

2. 分析过程

  1. 首先评估计算所需时间 tc;

  2. 计算搬运数据所需时间 tb,通常包括 tbIn 和 tbOut(比如VEC);但如果是融合算子或者MM,则要计算每个路径上的时间;

  3. Tc > tb,则计算bound:理论时间可以按照 tc 作为基准,通常可以要求算子达成 tc*80%;

  4. Tb > tc,则搬运bound:则可以考虑使用这个作为理论基准,通常可以要求算子达成 tb*80%。

3. 注意事项

  1. 一个计算达成了某个执行单元的bound并不代表该算子已经达成了算子的性能可达上限;

  2. 如果是计算单元已经达成了bound,并且算法并没有重复计算过程,那么认为算子性能已经最优;

  3. 如果搬运单元已经达成bound,并且算法已经达成了搬运量最小的算法,那么可以认为算子性能已经达成最优;

  4. 部分bound是因为算法设计有问题,并没有找到搬运量最小算法来进行计算而产生bound,这种情况通常出现在MM计算。

【图片知识总结】GM地址尽量512B对齐

由于AI处理器内部设计约束,从GM向Local Memory搬运数据时,保证GM地址512B对齐可以最高效地发挥出带宽的效率。对比图展示了512B对齐以及32B对齐情况下单核的带宽效率(HBM->UB单核带宽、UB->HBM单核带宽随搬运数据长度的变化曲线):搬运同等数据量,带宽差距最大的情况,32B对齐场景只能达到512B对齐场景的70%

注意

  • 本性能优化手段仅针对 Atlas A2 训练系列产品 / Atlas 800I A2 推理产品生效;

  • 测试数据与处理器型号及HBM频率相关,且实际测试时可能会存在略微抖动,具体带宽数值并不一定严格一致。

【图片知识总结】高效率地使用搬运API

使用搬运API时,应该尽可能地使用API的 srcStride/dstStride/blockLen/blockCount 等参数实现连续搬运或者固定间隔搬运,而不是使用for循环,二者效率差距极大

示例场景:图片的每一行为16KB,需要从每一行中搬运前2KB。使用 srcStride/dstStride/blockLen/blockCount 等参数可以达到一次搬完的效果(每次搬运32KB);如果使用for循环遍历每行,每次仅能搬运2KB。建议通过 DataCopy 包含 srcStride/dstStride/blockLen/blockCount 的接口一次搬完。

反例(for循环,每次只能搬运2K,重复16次):

// 搬运数据存在间隔,从GM上每行16KB中搬运2KB数据,共16行
for (int i = 0; i < imgHeight; i++) {
    DataCopy(tensorIn[i * copyWidth], tensorGM[i*imgWidth], copyWidth);
}

正例(一次搬完):

DataCopyParams copyParams;
copyParams.blockCount = imgHeight;
copyParams.blockLen = copyWidth / 8;
copyParams.srcStride = (imgWidth - copyWidth) / 8;
copyParams.dstStride = 0;
DataCopy(tensorGM, tensorIn, copyParams);
内存优化

【图片知识总结】UB Buffer融合

算子在进行多次vector计算时,如果前一次计算的结果是后一次计算的输入数据,不需要将前一次的计算结果从UB(Unified Buffer)搬运到GM后再从GM搬运到UB作为下一次计算的输入。前一次计算结果可暂存在UB上直接作为下一次计算的输入,从而减少搬入搬出次数,连续进行vector计算,提升内存使用效率。

  • 反例数据流:GM → VECIN(UB) →[Exp]→ VECOUT(UB) → GM → VECIN(UB) →[Abs]→ VECOUT(UB) → GM(中间结果往返GM);

  • 正例数据流:GM → VECIN(UB) →[Exp]→ VECCALC(UB) →[Abs]→ VECOUT(UB) → GM(中间结果留在UB)。

此为十分基础的算子融合思想:在满足计算逻辑的前提下,尽可能减少数据交换次数。

【图片知识总结】通过BT Buffer实现高效的bias计算

算子中进行带bias的矩阵乘计算时,可将bias数据搬运至 C2(Bias Table Buffer,BT)上,调用一次Mmad接口实现矩阵乘加bias的计算。相比于先将矩阵乘的结果从CO1(LOC)搬运到GM上、再搬运到UB上进行加bias的过程,减少了数据搬运的次数,可提升内存使用效率。

  • 反例:将矩阵乘的计算结果从CO1(L0C)搬运到workspace上 → 从workspace搬运到UB上 → 在UB上进行加bias的运算 → 最后将结果搬运到GM。当循环n次该计算过程,则分别增加了n次 CO1->workspace、workspace->UB 的搬运;

  • 正例:算子进行带bias的矩阵乘计算时,先将bias搬运到BT上,调用一次Mmad接口实现矩阵乘加bias的计算(A1→A2、B1→B2、C1→C2(BT) 共同输入Mmad得到CO1)。

【图片知识总结】通过Fixpipe Buffer存放量化参数实现高效随路量化

算子中对矩阵乘结果进行量化计算时,可将量化参数搬运到 C2PIPE2GM(Fixpipe Buffer,FB)上,调用一次Fixpipe接口实现矩阵乘结果的量化计算。相比于矩阵乘的结果从CO1(L0C)搬运到GM上、再从GM搬运到UB上、在UB上进行量化计算的过程,减少了数据搬运的次数,提升内存使用效率。

  • 反例:将矩阵乘的结果从CO1搬运到workspace上 → 再从workspace搬运到UB上 → 将量化参数搬运到UB上和矩阵乘的结果一起在UB上进行一系列量化计算 → 将最终量化结果从UB搬运到GM上。相比于正确示例多增加了 CO1->workspace、workspace->UB 的搬运过程和量化的vector计算;

  • 正例:将量化参数搬运到FB(Fixpipe Buffer)上(C1→C2PIPE2GM),Mmad 得到 CO1 后经 Fixpipe 随路完成量化直接输出GM。

注意:本性能优化手段仅针对 Atlas A2 训练系列产品 / Atlas 800I A2 推理产品生效。

API使用优化:

【图片知识总结】Scalar优化——通用CPU的各种SCALAR优化手段都可以

1. Scalar优化技术

  1. 循环变量不外提,循环内重复计算消除;

  2. 计算强度消减:比如循环内的随迭代偏移的数值计算 i*offset 修改为加法计算;

  3. 计算外提到HOST:有些计算尽量放到host,以tiling数据方式传递到DEV;

  4. 大块的tiling块数据的计算:①会降低循环次数降低SCALAR计算需求;②计算时间长可以掩盖SCALAR的计算时间;

  5. 写的小函数要inline,多处调用的大函数不要inline(会增加执行代码大小)。

2. Scalar的优化意义

  1. Scalar的任务是完成地址计算、循环控制、条件控制、指令发射,期望的模式是无阻塞的一直到完成所有指令发射;

  2. 程序中如果scalar CPU计算任务太重,导致出现bound,VEC/DMA单元的计算会出现空闲,导致资源利用率低;

  3. 不正确的阻塞Scalar会阻断整个异步并行的流水,降低执行性能。

3. Scalar bound的发现

  1. 通过上板PMU统计,观察是否Scalar Bound,这是主要手段;

  2. 通过npu仿真工具,分析流水图和log来分析Scalar占用,辅助分析。

架构图说明:AI Core 中算子指令序列由 Scalar 计算单元解析后,向 Vector 计算单元、Cube 计算单元、DMA 搬运单元的指令队列发射指令(指令流),各单元与 Local Memory 交互数据(数据流),并通过同步信号协调;Scalar 与 GM(DDR内存)直接交互。

【图片知识总结】避免Scalar参与数据计算,可以避免Scalar阻塞

反例(Scalar计算数据)

S = vreducesum(TensorA);
S = S + 1.0;              // scalar计算,S是UB数据
A1 = muls(TensorB, S);

此时 Scalar 指令队列与 VEC 指令队列之间需要互相等待同步(Set v su1 / Wait v su1 / Set su v1 等),Scalar 参与流水导致阻塞。

正例(VEC计算数据)

S = vreducesum(TensorA);
S = Vadds(S, 1.0, mask=1);  // vec指令计算数据,但是这个指令只计算一个数
A1 = muls(TensorB, S);

将标量加法改用向量指令 Vadds 完成(只计算一个数),Scalar 未参与流水、一直做自己的本职工作,避免了指令队列间的互相等待。原则:尽量避免Scalar参与数据计算。

【图片知识总结】API的分级——追求极致性能可以使用0级指令,可控性更好

  • 计算API核心数据结构 LocalTensor:VEC/CUB 向量计算API操作数都为 LocalTensor;

  • 4级API定义:VEC/CUB 的API根据用户使用的场景分为4级:

级别名称说明示例
3级运算符重载支持 +,-,*,/,=,,&,^,>,<,>=,<=;实现计算的简单表达,针对整个tensor计算
2级一维连续计算针对源操作数的连续COUNT个数据进行计算连续写入DST操作数,解决一维tensor的连续count个数据的计算问题Add(dst,src1,src2,count)
1级多维slice计算解决多维数据中的切片计算问题(开发中)Dst[1:3,4:11]=add(src1[0:2,4:11],src2[0:2,2:9])
0级功能丰富计算API可以完整发挥硬件优势的计算API,充分发挥CANN系列芯片的强大功能指令,支持对每个操作数的 repeatTimes、repeatStride、MASK 的操作Add(dst,src1,src2,repeatTims,repeatParam)

0级API中要特别注意block结构(BlockStride、RepeatStride、Mask 的组织方式)。

【图片知识总结】0级API指令介绍举例

接口形式:abs(src1, dst1, mask, repeatnum, repeatstride, src1blckstr, dst1blckstr)

  • 内部SIMD计算的是按照固定32B(block概念)* 8 = 256B的宽度进行一个repeat的计算(1个cycle),这里8是固定的;

  • 需要指定 BlockStride(32B为单位);

  • 指定 RepeatStride(32B为单位)和 repeatNum

  • 指定一个 128bits的MASK 作用在每个repeat上的128个FP16元素(目前不支持int8向量计算)。

图示结构:每个 repeat(repeat0~repeat3)包含 BK0~BK7 共8个固定32B的block;BlockStride 为 repeat 内相邻block的间隔,RepeatStride 为相邻 repeat 首block 的间隔,Mask 作用于整个repeat的数据。

【图片知识总结】iCache优化——影响性能几个百分点

1. Icachemiss多的发现:通过上板运行的PMU统计会有ICACHEmiss的统计。

2. Icachemiss发生的原因:主要是 kernel大小超过ICACHE大小,以及内部循环和条件分支+长跳转导致。示例:

// example1: 大条件分支
if(blockidx == 23){ code1; } else{ code2; }
// example2: 长循环
for(i=0; i<M; i++){ code1; }

icache用于缓存即将执行的指令,如果跳转距离超过icache大小导致icache miss,AIC会重新从global去取指令,造成计算中断。

3. Icachemiss的优化措施

  • 优化1:减小代码段大小至icache能放下,减小的办法之一是把大的条件分支编译为2个kernel;

  • 优化2:执行次数多的分支代码在前。

【图片知识总结】大型矩阵reducesum实现:CUBE单元计算

通过构造辅助矩阵利用matmul计算reduce_sum

  • example:对一个shape为(16, 32)张量的最后一根轴做reduce sum;

  • 由于vector单元对fp16只有128 elements并行度,我们可通过构造一个shape为(32, 1)的全1矩阵,将reduce sum转为matmul计算:(16,32) × (32,1) = (16,1);

  • 通过计算单元的转换,只要M轴和K轴足够长,即可达到最大256 elements的并行度(高于Vector单元的128并行度)。


全文图片知识总纲(速查)

主题核心知识点
孪生调试总览CPU域调功能/精度(GDB、printf、ASSERT),NPU域调性能(Profiling流水图、上板统计);先调对错再调性能
CPU域调试printf打印标量;LocalTensor.Print()每行打印一个32B datablock;GDB多进程子进程调试(set follow-fork-mode child)
NPU域调试AscendC::printf打印标量;DumpTensor(srcLocal, 5, dataLen)输出Tensor内容,带DumpHead/DumpTensorHead 32字节信息头
msSanitizer内存检测memcheck + 竞争检测racecheck;编译加--cce-enable-sanitizer -g;支持非法读写/多核踩踏/非对齐访问/内存泄漏/非法释放/分配未使用;WAW/WAR/RAW竞争
msProf算子性能采集分析;msprof op(上板)/ msprof op simulator(仿真);输出OPPROF_目录含各类csv与流水图
矩阵乘基础C = A*B + Bias;A[M,K]、B[K,N]、C[M,N]、Bias[N]
逻辑位置A1/B1/C1(整块,类比L2)、A2/B2/C2(切分小块,类比L1)、CO1/CO2(Cube Out)、VECCALC(临时变量)
矩阵乘数据流GM/VECOUT → A1/B1 → A2/B2 → Compute(A2*B2=CO1) → Aggregate(CO1→CO2) → CopyOut(CO2→GM/VECIN)
Matmul高阶APIhost侧创建Tiling对象获取Tiling参数 → kernel侧 Init/SetTensorA/B/Bias/Iterate或IterateAll/End
AICore架构分离架构:AIC(MTE1/2/3、Cube、Scalar;GM、L1、LOA、LOB、LOC、BT、FP)+ AIV(MTE2/3、Vector、Scalar;GM、UB);耦合架构:Cube与Vector同核部署
融合算子多个小算子融合为一个大算子,功能等价性能更优;Flash Attention = MatMul+Scale+Mask+SoftMax+MatMul;优势:减少计算量/内存占用/优化数据流/简化代码
融合编程范式Cube输出可作Vector输入(CO2→VECIN);Vector输出可作Cube输入(VECOUT→A1→A2、VECOUT→B1→B2);中间结果不经GM
Matmul+LeakyRelu实例c = LeakyRelu(ab + bias, alpha);设计三步:算子分析→数据流分析(5 Stage简化为3 Stage)→Tiling策略设计(多核切分singleCoreM/K/N + 核内切分baseM/K/N,baseMbaseN不超UB)
理论性能评估比较计算时间tc与搬运时间tb:tc>tb计算bound(目标tc80%),tb>tc搬运bound(目标tb80%)
内存/搬运优化GM地址512B对齐(32B对齐带宽仅70%);DataCopy用srcStride/dstStride/blockLen/blockCount一次搬完代替for循环;UB Buffer融合减少搬入搬出;BT Buffer随路加bias(Mmad);Fixpipe Buffer随路量化
Scalar优化循环不变量外提、计算强度消减、计算外提host、大块tiling掩盖Scalar时间、小函数inline;避免Scalar参与数据计算(用Vadds等vec指令代替)防阻塞
API分级3级运算符重载 → 2级一维连续计算 → 1级多维slice → 0级功能丰富API(repeatTimes/repeatStride/MASK,固定32B*8=256B一个repeat)
iCache优化kernel超ICACHE大小或长跳转会miss;减小代码段(大条件分支拆2个kernel)、高频分支代码在前
Cube做reducesum构造(32,1)全1矩阵将reduce sum转matmul,并行度从Vector的128提升到最大256 elements
Logo

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

更多推荐