Ascend C算子性能优化实用技巧04——Tiling优化

embedded/2024/10/18 0:50:49/

简介

Ascend C是CANN针对算子开发场景推出的编程语言,原生支持C和C++标准规范,兼具开发效率和运行性能。使用Ascend C,开发者可以基于昇腾AI硬件,高效的实现自定义的创新算法。

目前已经有越来越多的开发者使用Ascend C,我们将通过几期“Ascend C算子性能优化”专题分享,围绕开发者最为关心的算子性能优化环节,介绍Ascend C算子常用的优化技巧,帮助开发者自主构建出更优性能的算子。专题内容将围绕流水优化、搬运优化、内存优化、API使用优化以及Tiling优化等优化技巧,从方案讲解、优化案例、性能对比等多角度展开介绍。前期内容回顾:

  1. 《Ascend C算子性能优化实用技巧01——流水优化》
  2. 《Ascend C算子性能优化实用技巧02——内存优化》
  3. 《Ascend C算子性能优化实用技巧03——搬运优化》

下面进入第四期内容:Ascend C Tiling优化,您将了解到以下优化技巧:

  1. 多核切分
  2. L2Cache切分
  3. 核间负载均衡

多核切分

AI处理器上一般包括多个AI Core处理核心,为了实现多核并行,提升计算效率,需要将矩阵数据进行切分,分配到不同的核上进行处理。多核切分是最常见、最基本的Tiling策略。

通过SetBlockDim接口设置整个算子计算所用核数blockDim。

context->SetBlockDim(BLOCK_DIM);

blockDim规定了核函数将会在几个核上执行。例如,需要计算8M的数据,每个核上计算1M的数据,blockDim设置为8,但是为了充分利用硬件资源,一般将blockDim设置为硬件平台的核数,根据核数进行数据切分。

blockDim是逻辑核的概念,取值范围为[1,65535]。为了充分利用硬件资源,一般设置为物理核的核数或其倍数。对于耦合架构和分离架构,blockDim在运行时的意义和设置规则有一些区别:

  1. 耦合架构:由于其Vector、Cube单元是集成在一起的,blockDim用于设置启动多个AICore核实例执行,不区分Vector、Cube。AI Core的核数可以通过GetCoreNumAiv或者GetCoreNumAic获取。
  2. 分离架构
  • 针对仅包含Vector计算的算子,blockDim用于设置启动多少个Vector(AIV)实例执行,比如某款AI处理器上有40个Vector核,建议设置为40。
  • 针对仅包含Cube计算的算子,blockDim用于设置启动多少个Cube(AIC)实例执行,比如某款AI处理器上有20个Cube核,建议设置为20。
  • 针对Vector/Cube融合计算的算子,启动时,按照AIV和AIC组合启动,blockDim用于设置启动多少个组合执行,比如某款AI处理器上有40个Vector核和20个Cube核,一个组合是2个Vector核和1个Cube核,建议设置为20,此时会启动20个组合,即40个Vector核和20个Cube核。注意:该场景下,设置的blockDim逻辑核的核数不能超过物理核(2个Vector核和1个Cube核组合为1个物理核)的核数。
  • AIC/AIV的核数分别通过GetCoreNumAic和GetCoreNumAiv接口获取。

L2Cache切分

假设AI处理器的L2Cache大小为192MB,L2Cache读写混合带宽约为7TB/s,而AI Core外部存储Global Memory的带宽约为1.6TB/s,两者之间存在较大差距。搬入或搬出相同数据量的情况下,访问L2Cache读写数据比HBM更快。若数据无法命中L2Cache,即需要访问的数据不在L2Cache内,导致需要去HBM上读写,带宽利用效率较低,最终算子搬入或搬出数据变为算子整个运行过程的性能瓶颈。切分策略建议:当输入和输出数据的数据量超过L2Cache大小时,Tiling中使能L2Cache切分策略。下面举个例子来说明L2Cache切分帮助大家理解。

假设输入数据大小为InputTotalSize = 384MB,L2Cache大小为192MB,总核数为20个核,数据未切分的情况下,整体一次完成计算。假设20个核一次可以处理共192MB的数据,则每个核至少两次读取输入数据。

图1.未使能L2Cache切分

constexpr int32_t TOTAL_LENGTH = 384 * 1024 * 1024 / sizeof(half);
constexpr int32_t USE_CORE_NUM = 20;
constexpr int32_t TILE_NUM = 2;
constexpr int32_t BLOCK_LENGTH = TOTAL_LENGTH / USE_CORE_NUM;
constexpr int32_t TILE_LENGTH = BLOCK_LENGTH / TILE_NUM;class KernelSample {
public:__aicore__ inline KernelSample() {}__aicore__ inline void Init(GM_ADDR x){xGm.SetGlobalBuffer((__gm__ half*)x + BLOCK_LENGTH * GetBlockIdx(), BLOCK_LENGTH);pipe.InitBuffer(inQueueX, 1, BLOCK_LENGTH * sizeof(half));pipe.InitBuffer(inQueueY, 1, BLOCK_LENGTH * sizeof(half));}__aicore__ inline void Process(){// 示例演示对输入数据加2的运算constexpr int32_t loopCount = 2;for (int32_t i = 0; i < loopCount; i++) {// 外层的每次循环对输入数据进行加1的运算for (int32_t j = 0; j < TILE_NUM; j++) {// 内层循环分别处理每个核第0块和第1块数据CopyIn(j);Compute();CopyOut(j);}}}
private:__aicore__ inline void CopyIn(int32_t process){LocalTensor<half> xLocal = inQueueX.AllocTensor<half>();// 对于每个核,除了首次读取外,读取第0块数据时,L2Cache内缓存的是第1块数据;// 对于每个核,读取第1块数据时,L2Cache内缓存的是第0块数据;// 每个核需要4次读取GM上的数据DataCopy(xLocal, xGm[process * TILE_LENGTH], TILE_LENGTH );inQueueX.EnQue(xLocal);}__aicore__ inline void Compute(){LocalTensor<half> yLocal = inQueueY.AllocTensor<half>();LocalTensor<half> xLocal = inQueueX.DeQue<half>();Adds(yLocal, xLocal, 1, TILE_LENGTH);    inQueueY.EnQue<half>(yLocal);inQueueX.FreeTensor(xLocal);}__aicore__ inline void CopyOut(int32_t process){LocalTensor<half> yLocal = inQueueY.DeQue<half>();DataCopy(yGm[process * TILE_LENGTH], yLocal, TILE_LENGTH);inQueueY.FreeTensor(yLocal);}
}
...

使能L2Cache切分后,输入数据均等切分成2份数据,则整体分两次进行计算,每次的计算量为192MB,第一次20个核先计算前192MB的数据,第二次20个核计算后192MB的数据。每次计算前读取的数据能够命中L2Cache,提升算子性能。

图2.使能L2Cache切分

constexpr int32_t TOTAL_LENGTH = 384 * 1024 * 1024 / sizeof(half);
constexpr int32_t TILE_NUM = 2;
constexpr int32_t USE_CORE_NUM = 20;
constexpr int32_t TILE_LENGTH = TOTAL_LENGTH / TILE_NUM;
constexpr int32_t BLOCK_LENGTH = TILE_LENGTH / USE_CORE_NUM;class KernelSample {
public:__aicore__ inline KernelSample() {}__aicore__ inline void Init(GM_ADDR x, GM_ADDR y, int32_t index){xGm.SetGlobalBuffer((__gm__ half*)x + BLOCK_LENGTH * GetBlockIdx() + index * TILE_LENGTH, BLOCK_LENGTH);yGm.SetGlobalBuffer((__gm__ half*)y + BLOCK_LENGTH  * GetBlockIdx() + index * TILE_LENGTH, BLOCK_LENGTH);pipe.InitBuffer(inQueueX, 1, BLOCK_LENGTH * sizeof(half));pipe.InitBuffer(inQueueY, 1, BLOCK_LENGTH * sizeof(half));}__aicore__ inline void Process(){// 示例演示对输入数据加2的运算constexpr int32_t loopCount = 2;for (int32_t i = 0; i < loopCount; i++) {// 每次循环对输入数据进行加1的运算CopyIn();Compute();CopyOut();}}
private:__aicore__ inline void CopyIn(){LocalTensor<half> xLocal = inQueueX.AllocTensor<half>();// 对于每个核,除了首次读取外,第二次读取可以命中L2Cache;// 每个核2次读取GM上的数据,2次访问L2Cache读数据DataCopy(xLocal, xGm, BLOCK_LENGTH );inQueueX.EnQue(xLocal);}__aicore__ inline void Compute(){LocalTensor<half> yLocal = inQueueY.AllocTensor<half>();LocalTensor<half> xLocal = inQueueX.DeQue<half>();Adds(yLocal, xLocal, 1, BLOCK_LENGTH);    inQueueY.EnQue<half>(yLocal);inQueueX.FreeTensor(xLocal);}__aicore__ inline void CopyOut(){LocalTensor<half> yLocal = inQueueY.DeQue<half>();DataCopy(yGm, yLocal, BLOCK_LENGTH);inQueueY.FreeTensor(yLocal);}
}
...extern "C" __global__ __aicore__ void simple_kernel(__gm__ uint8_t* srcGm, __gm__ uint8_t* dstGm)
{AscendC::KernelAdd op;// 输入数据均等切分成2份数据进行计算for (int32_t i = 0; i < TILE_NUM; i++) {op.Init(srcGm, dstGm, i);op.Process();}
}
...

核间负载均衡

AI处理器的物理核数是固定的,当L2Cache切分之后,可能发生部分核有计算拖尾的情况,即每次所有核计算量除以每个核处理的数据量不能被核数整除,导致最后需要部分尾核来计算尾块数据。而在尾核计算时,部分核始终处于空闲状态,从而使得算子的整体性能变差。

如下图所示,假设总的数据量为TotalSize,L2Cache切分之后分为两份TotalSize / 2,每个核每次的计算量为TotalSize / 2 / 25,即需要25个核进行处理,由于AI处理器的核数为20,因此每次计算时,1到5核的每个核需要多算一份数据,导致发生拖尾的情况。

图3.计算拖尾示意图

针对上述切分策略,调整拖尾核的位置后可以达到全局负载最优,如下图所示,完成所有计算时,1到10核多一次数据块的计算,可以实现全局负载最优。

图4.核间负载均衡示意图

​​​​​​​更多学习资源

了解更多Ascend C算子性能优化手段和实践案例,请访问:昇腾社区Ascend C信息专区。


http://www.ppmy.cn/embedded/127288.html

相关文章

yolov8(Ultralytics)可视化界面ui设计,基于pyqt5,单py文件即插即用

本次yolov8可视化界面Ui相较之前文章中的界面进行了部分改动&#xff0c;比较明显的改动为本文使用的yolov8版本为8.2.0&#xff0c;有了较新的范围内统计目标数量的功能&#xff08;目前还没整合进来&#xff0c;后续更新&#xff09;&#xff0c;且分为左右两个区域分别显示原…

LangChain中使用Prompt01

1.引入提示模板 from langchain.prompts import (SystemMessagePromptTemplate,AIMessagePromptTemplate,HumanMessagePromptTemplate, )2.设置系统提示 system_template_text"你是一位专业的翻译&#xff0c;能够将{input_language}翻译成{output_language}&#xff0c…

观察者模式和发布-订阅模式的区别

观察者模式是&#xff0c;当被观察者的数据发生变化时&#xff0c;调用被观察者的 notify 方法&#xff0c;去通知所有观察者执行 update 方法进行更新&#xff1b; 对于发布-订阅模式&#xff0c;首先发布者与订阅者互相并不知道彼此的存在&#xff0c;他们是通过事件中心来进…

智能手机、平板和笔记本电脑出口俄罗斯认证解析

智能手机、笔记本电脑和平板电脑&#xff0c;它们的监管范围相似&#xff0c;需要获得EAC 合格证、FAC电信认证和FSS加密认证&#xff0c;才能进口、清关并在俄罗斯市场上销售。 一、海关联盟EAC 认证 是根据 EAC 要求强制批准的证书&#xff0c;并且受到所有国家海关和市场的…

SpringCloud的学习(四)Micrometer、GateWay

Micrometer 分布式链路追踪 在微服务框架中&#xff0c;一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果&#xff0c;每一个前段请求都会形成一条复杂的分布式服务调用链路&#xff0c;链路中的任何一环出现高延时或错误都会引起…

Path、File、FileInfo、Directory、DirectoryInfo

当程序运行时,系统为程序开辟内存空间,但是这种空间会随着程序的而结束而释放。如果需要将处理好的数据永久保存,那么就需要IO流技术将处理好的数据存入文件中,文件又是按照路径存储在磁盘上的,因此我们要学会路径的操作。 Path类就是系统为我们提供的用于操作路径的静态…

【C++ 真题】B2059 奇数求和

奇数求和 题目描述 计算非负整数 m m m 到 n n n&#xff08;包括 m m m 和 n n n&#xff09;之间的所有奇数的和&#xff0c;其中&#xff0c; m m m 不大于 n n n&#xff0c;且 n n n 不大于 300 300 300。例如 m 3 , n 12 , m3,n12, m3,n12, 其和则为&#xf…

sqli-labs less-20 less-21 less-22 cookie注入

COOKIE 作用&#xff1a;是由网络服务器存储在你电脑硬盘上的一个txt类型的小文件&#xff0c;它和你的网络行为有关&#xff0c;记录了当前用户的状态 形式&#xff1a;keyvalue 例如&#xff1a;当我们登录某个账号后&#xff0c;服务器会在cookies进行记录 个人理解&#xf…