AI Core算子开发指南
说明:
- 算子开发过程中涉及的基本概念如Tiling、Kernel、Ascend C接口等,详细介绍请参考《Ascend C算子开发》。
- AI Core算子是使用Ascend C语言开发,运行在AI Core硬件单元的算子。
本指南以ops-rand项目中的随机数算子为例,介绍新算子开发流程以及涉及的交付件。
- 工程创建:开发算子前,需完成环境部署并创建算子目录。
- 算子定义:算子功能说明与原型定义。
- Tiling实现:实现Host侧Tiling策略。
- Kernel实现:实现Device侧算子核函数。
- 编译部署:通过工程编译脚本完成自定义算子的编译和安装。
- 算子验证:通过常见算子调用方式,验证自定义算子功能。
工程创建
1. 环境部署
开发算子前,请先参考环境部署完成基础环境搭建。
2. 目录创建
在src目录下创建新的算子目录,目录名为算子名的小写下划线形式。
# 创建算子目录
mkdir -p src/${op_name}
mkdir -p src/${op_name}/arch35
mkdir -p src/${op_name}/tests
创建完成后,目录结构如下所示:
${op_name}/ # 算子名的小写下划线形式
├── CMakeLists.txt # 算子编译配置文件
├── ${op_name}.cpp # Kernel入口文件
├── arch35/ # Ascend950特有实现
│ └── ${op_name}.h # Kernel实现头文件
└── tests/ # 测试用例目录
├── CMakeLists.txt
└── test_${op_name}_example.cpp # 算子测试用例
算子定义
开发算子前需要先确定目标算子的功能和计算逻辑。
以stateless_random_uniform_v2算子为例:
- 功能:生成无状态的均匀分布随机数
- 输入:seed(随机种子)、offset(偏移量)、shape(输出形状)
- 输出:填充了随机数的张量
- 支持的数据类型:FP32、FP16、BF16
API接口定义
在include/cann_ops_rand.h中定义算子的对外API接口:
#ifdef __cplusplus
extern "C" {
#endif
/* 无状态均匀分布随机数生成 (内部API) */
aclError _aclrandStatelessRandomUniformV2(
uint64_t seed,
uint64_t offset,
int32_t alg,
void *output,
uint64_t n,
int32_t dtype
);
#ifdef __cplusplus
}
#endif
说明:
output参数类型为void*,实际数据类型由dtype参数决定dtype参数:0=FP32, 1=FP16, 2=BF16
Tiling实现
Tiling简介
因NPU中AI Core内部存储空间有限,无法一次性将整个张量数据加载到计算单元中处理,因此需要将输入张量切分为多个小块(Tile),逐块进行计算,这一过程称为Tiling。
TilingData结构体定义
在Kernel实现头文件中定义TilingData结构体:
// stateless_random_uniform_v2.h
struct StatelessRandomUniformV2TilingData {
uint32_t tilingKey; // Tiling标识,用于区分不同场景
uint32_t blockNum; // 核数
uint32_t blockTilingSize; // 每个核处理的数据量
uint32_t tailBlockTilingSize; // 尾核处理的数据量
uint32_t ubTilingSize; // UB切分大小
uint32_t alg; // 算法类型
uint32_t key[2]; // Philox算法密钥(来自seed)
uint32_t counter[4]; // Philox算法计数器(来自offset)
};
说明:
tilingKey字段用于标识不同的 Tiling 场景,便于在 Kernel 中根据场景选择不同的处理逻辑。
Tiling策略计算
在Host侧(Kernel入口函数或API调用前)计算Tiling参数:
// 获取平台信息
auto ascendcPlatform = platform_ascendc::PlatformAscendCManager::GetInstance();
auto coreNum = ascendcPlatform->GetCoreNumAiv(); // 获取可用核数
// 计算Block切分参数
constexpr uint32_t CORE_ALIGN_SIZE = 512; // 核对齐大小
constexpr uint32_t MIN_TILING_SIZE = 256; // 最小切分大小
constexpr uint32_t BLOCK_SIZE_BYTES = 32; // 块大小(字节)
auto outputDtypeSize = sizeof(float);
auto outputSize = n * outputDtypeSize;
auto coreAlignFactor = CORE_ALIGN_SIZE / outputDtypeSize;
auto blockFactor = CeilDiv(outputSize, coreNum);
auto blockAlignFactor = CeilDiv(blockFactor, coreAlignFactor) * coreAlignFactor;
tilingData->blockTilingSize = std::max(blockAlignFactor, MIN_TILING_SIZE);
tilingData->blockNum = CeilDiv(outputSize, tilingData->blockTilingSize);
tilingData->tailBlockTilingSize = outputSize - tilingData->blockTilingSize * (tilingData->blockNum - 1);
// 计算UB切分参数
uint64_t ubSize = 0;
ascendcPlatform->GetCoreMemSize(platform_ascendc::CoreMemType::UB, ubSize);
auto quarterUbSize = ubSize / 4; // 使用1/4的UB空间
auto alignFactor = BLOCK_SIZE_BYTES / outputDtypeSize; // 块对齐因子
tilingData->ubTilingSize = CeilDiv(quarterUbSize / outputDtypeSize, alignFactor) * alignFactor;
// 设置随机数种子参数
tilingData->key[0] = static_cast<uint32_t>(seed);
tilingData->key[1] = static_cast<uint32_t>(seed >> 32);
tilingData->counter[0] = static_cast<uint32_t>(offset);
tilingData->counter[1] = static_cast<uint32_t>(offset >> 32);
Kernel实现
Kernel代码结构
ops-rand项目的Kernel实现主要包含以下文件:
src/${op_name}/
├── ${op_name}.cpp # Kernel入口文件 + API实现
└── arch35/
└── ${op_name}.h # Kernel类实现
Kernel类定义
// stateless_random_uniform_v2.h
template <typename T>
class StatelessRandomUniformV2 {
public:
__aicore__ inline StatelessRandomUniformV2() {}
__aicore__ inline void Init(
GM_ADDR y,
const StatelessRandomUniformV2TilingData* __restrict tilingData,
TPipe* pipeIn
);
__aicore__ inline void Process();
private:
__aicore__ inline void ParseTilingData(const StatelessRandomUniformV2TilingData* tilingData);
__aicore__ inline void Skip(const uint64_t count); // 跳过计数器
__aicore__ inline void DataTypeHandle(LocalTensor<T>& yOutput, const uint32_t calCount);
__aicore__ inline void CopyOut();
private:
TPipe* pipe;
// 队列和缓冲区
TQue<QuePosition::VECOUT, BUFFER_NUM> outQueY_; // 输出队列
TBuf<QuePosition::VECCALC> philoxQueBuf_; // Philox结果缓冲区
GlobalTensor<T> outputGm_; // 全局输出张量
// Tiling参数
uint32_t blockNum_ = 0;
uint32_t blockTilingSize_ = 0;
uint32_t ubTilingSize_ = 0;
// 随机数生成状态
uint32_t key_[2] = {0};
uint32_t counter_[4] = {0};
};
Init函数实现
template <typename T>
__aicore__ inline void StatelessRandomUniformV2<T>::Init(
GM_ADDR y,
const StatelessRandomUniformV2TilingData* tilingData,
TPipe* pipeIn
) {
// 1. 解析Tiling数据
ParseTilingData(tilingData);
// 2. 计算当前核的偏移量
auto blockIdx = GetBlockIdx();
blockOffset_ = blockTilingSize_ * blockIdx;
// 3. 处理尾核情况
if (blockIdx == blockNum_ - 1) {
currBlockTilingSize_ = tailBlockTilingSize_;
} else {
currBlockTilingSize_ = blockTilingSize_;
}
// 4. 初始化全局张量和队列
outputGm_.SetGlobalBuffer((__gm__ T*)y);
pipe = pipeIn;
pipe->InitBuffer(outQueY_, BUFFER_NUM, ubTilingSize_ * sizeof(T));
pipe->InitBuffer(philoxQueBuf_, ubTilingSize_ * sizeof(uint32_t));
}
Process函数实现
template <typename T>
__aicore__ inline void StatelessRandomUniformV2<T>::Process() {
// 1. 计算并跳过偏移量
auto groupCnt = (blockOffset_ + RESULT_ELEMENT_CNT - 1) / RESULT_ELEMENT_CNT;
Skip(groupCnt);
// 2. 分UB循环处理
ubLoopCnt_ = (currBlockTilingSize_ + ubTilingSize_ - 1) / ubTilingSize_;
for (auto idx = 0; idx < ubLoopCnt_; idx++) {
// 处理尾块
currUbTilingSize_ = ubTilingSize_;
if ((idx == ubLoopCnt_ - 1) && (currBlockTilingSize_ % ubTilingSize_ != 0)) {
currUbTilingSize_ = currBlockTilingSize_ % ubTilingSize_;
}
currOffset_ = blockOffset_ + idx * ubTilingSize_;
// 3. 生成随机数
LocalTensor<uint32_t> philoxRes = philoxQueBuf_.Get<uint32_t>();
LocalTensor<T> yOutput = outQueY_.AllocTensor<T>();
// 使用Philox算法生成随机数
PhiloxRandom<10>(
philoxRes,
{key_[0], key_[1]},
{counter_[0], counter_[1], counter_[2], counter_[3]},
currUbTilingSize_
);
// 4. 数据类型转换
DataTypeHandle(yOutput, currUbTilingSize_);
outQueY_.EnQue(yOutput);
// 5. 拷贝输出
CopyOut();
// 6. 更新计数器
groupCnt = (currUbTilingSize_ + RESULT_ELEMENT_CNT - 1) / RESULT_ELEMENT_CNT;
Skip(groupCnt);
}
}
数据类型转换
template <typename T>
__aicore__ inline void StatelessRandomUniformV2<T>::DataTypeHandle(
LocalTensor<T>& yOutput,
const uint32_t calCount
) {
if constexpr (AscendC::IsSameType<T, half>::value) {
// FP16: 取10位尾数,指数设为15
Uint16ToHalf(yOutput, calCount);
} else if constexpr (AscendC::IsSameType<T, float>::value) {
// FP32: 取23位尾数,指数设为127
Uint32ToFloat(yOutput, calCount);
} else if constexpr (AscendC::IsSameType<T, bfloat16_t>::value) {
// BF16: 取7位尾数,指数设为127
Uint16ToBfloat16(yOutput, calCount);
}
}
Kernel入口函数
// stateless_random_uniform_v2.cpp
extern "C" __global__ __aicore__ void stateless_random_uniform_v2(
GM_ADDR y,
StatelessRandomUniformV2TilingData tilingData
) {
TPipe pipe;
StatelessRandomUniformV2<float> op;
op.Init(y, &tilingData, &pipe);
op.Process();
}
Host侧API实现
aclError _aclrandStatelessRandomUniformV2(
uint64_t seed,
uint64_t offset,
int32_t alg,
float *output,
uint64_t n
) {
// 1. 参数校验
if (output == nullptr || n == 0) {
return ACL_ERROR;
}
// 2. 分配设备内存
void *devOut = nullptr;
aclrtMalloc(&devOut, n * sizeof(float), ACL_MEM_MALLOC_HUGE_FIRST);
// 3. 计算Tiling参数
auto ascendcPlatform = platform_ascendc::PlatformAscendCManager::GetInstance();
StatelessRandomUniformV2TilingData *tilingData = new StatelessRandomUniformV2TilingData();
// ... Tiling参数计算 ...
// 4. 启动Kernel
stateless_random_uniform_v2<<<tilingData->blockNum, nullptr, 0>>>(
(__gm__ uint8_t *)devOut, *tilingData
);
// 5. 拷贝结果到Host
aclrtMemcpy(output, n * sizeof(float), devOut, n * sizeof(float), ACL_MEMCPY_DEVICE_TO_HOST);
// 6. 释放资源
aclrtFree(devOut);
return ACL_SUCCESS;
}
编译部署
算子开发完成后,需对算子工程进行编译,生成ops-rand安装包。
1. 准备工作
参考工程创建完成基础环境搭建,同时检查算子开发交付件是否完备。
2. 配置环境变量
# 默认路径安装
source /usr/local/Ascend/cann/set_env.sh
# 指定路径安装
# source ${install_path}/cann/set_env.sh
3. 编译ops-rand包
进入项目根目录,执行编译命令:
# 编译所有算子
bash build.sh --pkg --soc=ascend950
# 编译指定算子
bash build.sh --pkg --soc=ascend950 --ops=${op_name}
若提示如下信息,说明编译成功:
Self-extractable archive "cann-950-ops-rand_9.0.0_linux-x86_64.run" successfully created.
4. 安装ops-rand包
./build_out/cann-${soc_name}-ops-rand_${cann_version}_linux-${arch}.run --full --install-path=${install_path}
- ${soc_name}:表示NPU型号名称,如
950。 - ${cann_version}:表示CANN版本号,如
9.0.0。 - ${arch}:表示CPU架构,如
aarch64、x86_64。 - ${install_path}:表示指定安装路径,需要与toolkit包安装在相同路径,默认安装在
/usr/local/Ascend目录。
ops-rand安装在${install_path}/cann路径中。
算子验证
# 编译并运行测试
bash build.sh --ops=${op_name} --run
随机数生成注意事项
Philox算法
ops-rand使用Philox算法生成随机数,该算法具有以下特点:
- 确定性:相同的seed和offset产生相同的随机序列
- 并行友好:支持多核并行生成,通过counter偏移实现
- 高质量:通过多轮加密混淆保证随机性
数据类型转换公式
| 数据类型 | 尾数位数 | 指数值 | 转换公式 |
|---|---|---|---|
| FP32 | 23 | 127 | `(exp << 23) |
| FP16 | 10 | 15 | `(exp << 10) |
| BF16 | 7 | 127 | `(exp << 7) |