OrangePi AIpro 香橙派 昇腾 Ascend C算子开发 - HelloWorld
flyfish
Ascend C算子编程
是SPMD(Single-Program Multiple-Data)编程
假设,从输入数据到输出数据需要经过3个阶段任务的处理(T1、T2、T3)。如下图所示,SPMD会启动一组进程,并行处理待处理的数据。对待处理数据切分,把切分后数据分片分发给不同进程处理,每个进程对自己的数据分片进行3个任务的处理。
具体到Ascend C编程模型中的应用,是将需要处理的数据被拆分并同时在多个计算核心(类比于上文介绍中的多个进程)上运行,从而获取更高的性能。多个AI Core共享相同的指令代码,每个核上的运行实例唯一的区别是block_idx不同,每个核通过不同的block_idx来识别自己的身份。block的概念类似于上文中进程的概念,block_idx就是标识进程唯一性的进程ID。并行计算过程的示意图如下图所示。
核函数(Kernel Function)
是Ascend C算子设备侧实现的入口
。在核函数中,需要为在一个核上执行的代码规定要进行的数据访问和计算操作,当核函数被调用时,多个核都执行相同的核函数代码,具有相同的参数,并行执行。
Ascend C允许用户使用核函数这种C/C++函数的语法扩展来管理设备端的运行代码,用户在核函数中进行算子类对象的创建和其成员函数的调用,由此实现该算子的所有功能。核函数是主机端和设备端连接的桥梁。
一个Hello World例子展示Ascend C核函数(设备侧实现的入口函数)的基本写法和如何被调用的流程。
hello_world.cpp
cpp
#include "kernel_operator.h"
extern "C" __global__ __aicore__ void hello_world()
{
AscendC::printf("Hello World!!!\n");
}
void hello_world_do(uint32_t blockDim, void *stream)
{
hello_world<<<blockDim, nullptr, stream>>>();
}
main.cpp
cpp
#include "acl/acl.h"
extern void hello_world_do(uint32_t coreDim, void *stream);
int32_t main(int argc, char const *argv[])
{
aclInit(nullptr);
int32_t deviceId = 0;
aclrtSetDevice(deviceId);
aclrtStream stream = nullptr;
aclrtCreateStream(&stream);
constexpr uint32_t blockDim = 8;
hello_world_do(blockDim, stream);
aclrtSynchronizeStream(stream);
aclrtDestroyStream(stream);
aclrtResetDevice(deviceId);
aclFinalize();
return 0;
}
HelloWorldSample例子
进入源码目录执行
cpp
bash run.sh -v Ascend310B4
结果
cpp
opType=hello_world, DumpHead: AIV-0, CoreType=, block dim=8, total_block_num=8, block_remain_len=1048424, block_initial_space=1048576, rsv=0, magic=5aa5bccd
CANN Version: 901005402, TimeStamp: 20240821
Hello World!!!
opType=hello_world, DumpHead: AIV-1, CoreType=, block dim=8, total_block_num=8, block_remain_len=1048424, block_initial_space=1048576, rsv=0, magic=5aa5bccd
CANN Version: 901005402, TimeStamp: 20240821
Hello World!!!
opType=hello_world, DumpHead: AIV-2, CoreType=, block dim=8, total_block_num=8, block_remain_len=1048424, block_initial_space=1048576, rsv=0, magic=5aa5bccd
CANN Version: 901005402, TimeStamp: 20240821
Hello World!!!
opType=hello_world, DumpHead: AIV-3, CoreType=, block dim=8, total_block_num=8, block_remain_len=1048424, block_initial_space=1048576, rsv=0, magic=5aa5bccd
CANN Version: 901005402, TimeStamp: 20240821
Hello World!!!
opType=hello_world, DumpHead: AIV-4, CoreType=, block dim=8, total_block_num=8, block_remain_len=1048424, block_initial_space=1048576, rsv=0, magic=5aa5bccd
CANN Version: 901005402, TimeStamp: 20240821
Hello World!!!
opType=hello_world, DumpHead: AIV-5, CoreType=, block dim=8, total_block_num=8, block_remain_len=1048424, block_initial_space=1048576, rsv=0, magic=5aa5bccd
CANN Version: 901005402, TimeStamp: 20240821
Hello World!!!
opType=hello_world, DumpHead: AIV-6, CoreType=, block dim=8, total_block_num=8, block_remain_len=1048424, block_initial_space=1048576, rsv=0, magic=5aa5bccd
CANN Version: 901005402, TimeStamp: 20240821
Hello World!!!
opType=hello_world, DumpHead: AIV-7, CoreType=, block dim=8, total_block_num=8, block_remain_len=1048424, block_initial_space=1048576, rsv=0, magic=5aa5bccd
CANN Version: 901005402, TimeStamp: 20240821
Hello World!!!
extern "C" __global__ __aicore__ void hello_world()
核函数时需要遵循以下规则
使用函数类型限定符
除了需要按照C/C++函数声明的方式定义核函数之外,还要为核函数加上额外的函数类型限定符,包含__global__和__aicore__。
使用__global__
函数类型限定符来标识它是一个核函数,可以被<<<...>>>
调用;
使用__aicore__
函数类型限定符来标识该核函数在设备端AI Core上执行:
cpp
__global__ __aicore__ void kernel_name(argument list);
编程中使用到的函数可以分为三类:核函数(device侧执行)
、host侧执行函数
、device侧执行函数(除核函数之外的)
。三者的调用关系如下图所示:
host侧执行函数可以调用同类的host执行函数,也就是通用C/C++编程中的函数调用;也可以通过<<<>>>
调用核函数。
device侧执行函数(除核函数之外的)可以调用同类的device侧执行函数。
核函数可以调用device侧执行函数(除核函数之外的)。
核函数(device侧执行)、host侧执行函数、device侧执行函数(除核函数之外的)调用关系图
使用变量类型限定符
指针入参变量需要增加变量类型限定符__gm__。表明该指针变量指向Global Memory
上某处内存地址。
其他规则或建议
规则:核函数必须具有void返回类型。
规则:仅支持入参为指针或C/C++内置数据类型(Primitive data types),如:half* s0、float* s1、int32_t c
。
建议:为了统一表达,建议使用GM_ADDR宏来修饰入参,GM_ADDR宏定义如下:
cpp
#define GM_ADDR __gm__ uint8_t*
使用GM_ADDR修饰入参的样例如下:
cpp
extern "C" __global__ __aicore__ void add_custom(GM_ADDR x, GM_ADDR y, GM_ADDR z)
这里统一使用uint8_t
类型的指针,在后续的使用中需要将其转化为实际的指针类型。
代码解释 extern "C"
extern "C"
告诉编译器,不要对这些函数名进行C++的名称修饰,这样C语言的代码就可以正确地调用这些函数。
如果有一部分代码是用C编写的,另一部分代码是用C++编写的,extern "C"
会确保 my_c_function
以C语言的方式进行链接,C++编写的代码能够调用C语言编写的函数。
名称修饰是编译器生成唯一符号名的一种机制,目的是支持C++的高级功能,如函数重载。通过名称修饰,编译器确保每个函数或变量在链接阶段具有唯一性,避免命名冲突。
举个例子
考虑以下两个函数:
cpp
int add(int a, int b);
double add(double a, double b);
在C语言中,由于函数名称不能重载,这两个函数将会引起命名冲突。但是在C++中,编译器会将这两个函数分别转换为不同的符号名,例如(符号名称是编译器生成的,具体表示可能会不同):
cpp
_add_int_int
_add_double_double
这些修饰后的名称在编译后的二进制文件中存储,使得它们可以在链接时区分开来。
使用 extern "C"
避免名称修饰如果希望C++函数能够被C代码调用,或者希望C++代码调用C语言的函数,需要使用extern "C"
来告诉编译器不要对这些函数进行名称修饰。例如:
cpp
extern "C" void myFunction(int a);
在这种情况下,myFunction
的名字在编译后的二进制文件中将保持为 myFunction
,而不会被修饰。
代码解释 extern void hello_world_do(uint32_t coreDim, void *stream);
中的extern
在C++中,extern
关键字用于声明一个变量或函数是由其他文件定义的,而不是在当前文件中定义的。它告诉编译器这个函数的定义在另一个编译单元(例如另一个源文件)中,而不是在当前文件中。在提供的 main.cpp
文件中,extern void hello_world_do(uint32_t coreDim, void *stream);
这一行的作用是声明 hello_world_do
函数的存在,使得 main.cpp
文件可以调用这个函数,而不需要在 main.cpp
中定义它。
原理:
-
函数定义在另一个文件中 :
hello_world_do
函数实际上是在hello_world.cpp
文件中定义的。为了在main.cpp
中使用这个函数,编译器需要知道这个函数的签名(返回类型、参数类型等)。通过使用extern
,告诉编译器"这个函数在别的地方定义了,只需要知道它的签名就可以了"。 -
链接阶段的作用 :
编译器在编译
main.cpp
时,不需要知道hello_world_do
函数的具体实现,只需要知道它的签名。而在链接阶段,链接器会把hello_world.cpp
中的hello_world_do
函数实现与main.cpp
中的调用关联起来。
执行配置由3个参数决定:
blockDim,规定了核函数将会在几个核上执行。每个执行该核函数的核会被分配一个逻辑ID,即block_idx,可以在核函数的实现中调用GetBlockIdx来获取block_idx;
l2ctrl,保留参数,暂时设置为固定值nullptr,开发者无需关注;
stream,类型为aclrtStream,stream用于维护一些异步操作的执行顺序,确保按照应用程序中的代码调用顺序在device上执行。
计算单元包括了三种基础计算资源:Cube计算单元、Vector计算单元和Scalar计算单元。
存储单元包括内部存储和外部存储:
AI Core的内部存储,统称为Local Memory,对应的数据类型为LocalTensor。由于不同芯片间硬件资源不固定,可以为UB、L1、L0A、L0B等。
AI Core能够访问的外部存储称之为Global Memory,对应的数据类型为GlobalTensor。
DMA(Direct Memory Access)搬运单元:负责在Global Memory和Local Memory之间搬运数据