昇腾CANN训练营实战:30分钟搞定你的第一个Ascend C算子(附完整代码)
最近身边不少朋友都在聊昇腾生态,尤其是那个听起来有点“硬核”的算子开发。很多人觉得,这玩意儿是不是得先啃完几本并行计算和硬件架构的厚书才能入门?其实真没那么玄乎。我刚开始接触Ascend C的时候也犯怵,但跟着训练营的节奏走下来,发现从零写出一个能跑起来的算子,最快半小时就能搞定。这篇文章,我就想用最直白的方式,带你走一遍这个“神奇”的半小时,让你亲手感受一下,在昇腾AI处理器上跑通第一个自定义算子是什么体验。
我们今天的核心目标非常明确:不扯理论,直接动手。你会看到一个完整的、可运行的向量加法算子(AddCustom)从无到有的全过程。我会把环境准备、代码编写、编译运行、结果验证这些环节掰开揉碎了讲,确保你每一步都能跟上。过程中遇到的坑,比如环境变量怎么配、编译报错怎么解、结果不对怎么调,我都会提前给你打好预防针。相信我,当你看到自己写的代码在昇腾硬件上正确输出结果的那一刻,那种成就感,比看十篇技术综述都来得实在。
1. 环境准备:搭建你的第一个昇腾开发沙箱
在开始写代码之前,我们得先把“战场”布置好。对于零基础的朋友,我最推荐的方式是直接使用华为云ModelArts提供的云端开发环境。这能帮你跳过最头疼的本地驱动安装、库依赖冲突等问题,把精力完全集中在算子开发本身。
1.1 云端环境:零配置快速启动
如果你选择云端路径,可以访问昇腾社区的活动页面,找到CANN训练营的专属入口。通常,新用户注册并参与活动后,会获得一定额度的免费算力体验券。领取后,在ModelArts控制台选择“Ascend C算子开发”场景的Notebook实例。
创建实例时,关键配置如下:
| 配置项 | 推荐选择 | 说明 |
|---|---|---|
| 资源规格 | ascend:1*v100 (或类似的Ascend资源) | 确保实例带有昇腾AI处理器资源。 |
| 镜像 | CANN 8.0.RC2 (Ubuntu 20.04) 或更高版本 | 镜像已预装CANN Toolkit、驱动、编译工具链。 |
| 存储 | 至少20GB | 用于存放代码和编译中间文件。 |
| 自动停止 | 建议设置(如2小时) | 节省算力,避免忘记关机产生费用。 |
实例启动后,通过网页终端或JupyterLab即可访问。第一件事,验证环境是否就绪:
# 检查NPU设备状态
npu-smi info
如果看到类似下面的输出,显示有可用的昇腾AI处理器(如Ascend 310P),说明硬件环境正常。
+-------------------------------------------------------------------------------------------+
| npu-smi 23.0.0 Version: 23.0.0 |
+----------------------+---------------+----------------------------------------------------+
| NPU Name | Health | Power(W) Temp(C) Hugepages-Usage(page) |
| Chip | Bus-Id | AICore(%) Memory-Usage(MB) AICPU-Usage(%) |
+======================+===============+====================================================+
| 0 Ascend 310P | OK | 15.3 45 0 / 0 |
| 0 | 0000:82:00.0 | 0 546 / 8192 0 |
+======================+===============+====================================================+
1.2 本地环境:追求极致控制感
如果你更喜欢在本地工作站上折腾,也可以自行搭建环境。这需要一台安装了昇腾AI加速卡(如Atlas 300I)的服务器,并确保已正确安装驱动和CANN软件包。
注意:本地环境搭建涉及系统权限和版本匹配,是新手最容易卡住的地方。务必严格按照昇腾社区文档的步骤操作,并留意操作系统版本、GCC编译器版本与CANN版本的兼容性列表。
无论是云端还是本地,环境就绪后,我们都需要确认几个关键工具的路径和版本:
# 检查CANN安装路径和版本
echo $ASCEND_HOME
ls $ASCEND_HOME/ascend-toolkit/latest/
# 检查编译器
which gcc
gcc --version
# 检查Ascend C头文件路径(非常重要!)
find $ASCEND_HOME -name "kernel_operator.h" | head -1
确保你能找到 kernel_operator.h 这个文件,它是我们编写算子核函数的基石。如果找不到,可能是CANN Toolkit没有安装完整,需要重新安装或检查环境变量。
2. 第一个算子:手把手实现向量加法
环境搞定,我们正式进入编码环节。别被“算子”这个词吓到,你可以把它理解为一个能在AI处理器上并行执行的计算函数。我们今天要实现的 AddCustom,功能就是最简单的逐元素相加:C = A + B。
2.1 理解Ascend C的编程模型
在动手写之前,花两分钟理解Ascend C的基本思想,能让你后面的代码看得更明白。Ascend C的核函数运行在昇腾AI处理器的AI Core上,它的执行模型和CUDA有些神似,但更贴近昇腾硬件的特性。
核心概念就三个:
- Global Memory (GM): 片外大容量内存,所有输入输出数据最初都存放在这里。访问速度慢。
- Local Memory (LM/UB): 片上高速缓存,核函数计算时,需要先把数据从GM搬到这里,算完再写回去。访问速度快。
- 并行维度: 计算任务会被网格(Grid)中的多个块(Block)并行执行。每个Block处理数据的一部分。
我们的任务流程可以概括为:分块 -> 搬运 -> 计算 -> 写回。下面这张表对比了开发流程中的关键步骤与传统CPU编程的差异:
| 步骤 | Ascend C算子开发 | 传统CPU编程 | 核心目的 |
|---|---|---|---|
| 数据准备 | 数据位于Host内存,需明确传入设备指针。 | 数据直接在进程内存中。 | 明确数据在异构系统中的位置。 |
| 任务划分 | 开发者需定义Grid/Block维度,或使用内置接口自动划分。 | 通常是单线程顺序处理,或使用OpenMP等库做简单并行。 | 将计算任务映射到海量AI Core上。 |
| 内存管理 | 需显式调用DataCopy在GM和LM间搬运数据块。 | 内存访问对程序员透明,由CPU缓存体系管理。 | 克服“内存墙”,利用片上高速存储。 |
| 计算执行 | 在__aicore__函数中编写针对LM数据的计算循环。 | 直接对内存地址进行计算。 | 发挥AI Core的并行计算能力。 |
| 同步控制 | 使用PipeBarrier等原语确保数据搬运与计算的顺序。 | 依赖线程库的锁或屏障。 | 保证数据依赖正确性,实现流水线。 |
2.2 编写核函数:逐行解析
现在,我们创建一个名为 add_custom.cpp 的文件,开始编写算子的核心——核函数。
// add_custom.cpp
#include "kernel_operator.h" // Ascend C核心头文件
using namespace AscendC; // 使用AscendC命名空间
// 核函数声明:extern "C" 确保C链接,__aicore__ 表示在AI Core上执行
extern "C" __aicore__ void add_custom_kernel(GM_ADDR a, GM_ADDR b, GM_ADDR c, uint32_t totalSize) {
// 1. 获取当前块的索引和总块数
uint32_t blockIdx = get_block_idx();
uint32_t blockNum = get_block_num();
// 2. 计算当前块需要处理的数据范围
// 将总数据量平均分给所有Block,最后一个Block处理剩余部分
uint32_t perBlockSize = (totalSize + blockNum - 1) / blockNum; // 向上取整
uint32_t startIdx = blockIdx * perBlockSize;
uint32_t endIdx = (startIdx + perBlockSize) < totalSize ? (startIdx + perBlockSize) : totalSize;
uint32_t realSize = endIdx - startIdx; // 当前块实际处理的数据量
// 如果当前块没有数据处理,直接返回
if (realSize == 0) {
return;
}
// 3. 在Local Memory中申请临时缓冲区(Tensor)
// 这里申请的是连续内存块,用于存放当前块要处理的a和b的数据,以及计算结果
LocalTensor<half> localA = LP_ALLOC_TENSOR<half>();
LocalTensor<half> localB = LP_ALLOC_TENSOR<half>();
LocalTensor<half> localC = LP_ALLOC_TENSOR<half>();
// 4. 数据搬运:将Global Memory中当前块的数据拷贝到Local Memory
// 这是性能关键点,批量搬运比单个元素访问高效得多
DataCopy(localA, a + startIdx, realSize);
DataCopy(localB, b + startIdx, realSize);
// 5. 等待数据搬运完成(建立内存屏障)
// 确保localA和localB的数据已经就绪,再进行计算
PipeBarrier<PIPE_ALL>();
// 6. 核心计算循环:逐元素相加
for (uint32_t i = 0; i < realSize; ++i) {
localC[i] = localA[i] + localB[i];
}
// 7. 等待计算完成
PipeBarrier<PIPE_ALL>();
// 8. 将结果从Local Memory写回Global Memory
DataCopy(c + startIdx, localC, realSize);
// 9. 释放Local Memory资源(有些环境下可省略,但显式释放是好习惯)
LP_FREE_TENSOR(localA);
LP_FREE_TENSOR(localB);
LP_FREE_TENSOR(localC);
}
代码要点拆解:
GM_ADDR: 这是一个指向Global Memory的地址指针。你可以把它想象成一个只能通过特定接口(如DataCopy)访问的远程内存地址。get_block_idx()和get_block_num(): 这两个内置函数帮你获取并行执行的上下文信息,是实现数据并行的关键。LocalTensor<half>: 在Local Memory中申请一个张量(一段连续内存)。<half>表示数据类型为FP16(半精度浮点数),这是AI计算中常用的格式,能节省内存和带宽。你也可以根据需求使用float。DataCopy: 内存搬运的“搬运工”。它负责在GM和LM之间高效地移动数据。注意它的参数顺序:目标地址,源地址,数据量。PipeBarrier<PIPE_ALL>(): 管道屏障。你可以把它理解为一个“检查点”,告诉硬件:“在这之前的所有操作(比如数据搬运)必须完成,才能继续后面的操作(比如计算)”。这是保证数据正确性的关键。- 计算循环: 这里就是最朴素的加法。在真实的复杂算子中,这个循环会包含更密集的计算,并可能涉及向量化指令。
提示:初次编写时,最容易出错的地方是索引计算。务必反复检查
startIdx、endIdx和realSize的计算逻辑,确保没有越界访问。一个简单的验证方法是,假设totalSize=100,blockNum=3,手动推算每个Block的startIdx和endIdx应该是多少。
2.3 编写Host侧调用代码
核函数是“士兵”,负责在AI Core上战斗。我们还需要一个“指挥官”(Host侧代码)来部署任务、准备弹药(数据)、并检阅战果(验证结果)。创建一个 main.cpp 文件。
// main.cpp
#include <iostream>
#include <cstdlib>
#include <cmath>
#include "acl/acl.h" // CANN运行时库头文件
// 声明我们刚才写的核函数
extern "C" __aicore__ void add_custom_kernel(GM_ADDR a, GM_ADDR b, GM_ADDR c, uint32_t totalSize);
// 生成随机数据的辅助函数
void generate_random_half_data(half* data, uint32_t size) {
for (uint32_t i = 0; i < size; ++i) {
// 生成0~1之间的随机半精度浮点数
data[i] = static_cast<half>((float)rand() / RAND_MAX);
}
}
// CPU参考实现,用于验证结果
void cpu_add_reference(const half* a, const half* b, half* c, uint32_t size) {
for (uint32_t i = 0; i < size; ++i) {
c[i] = a[i] + b[i];
}
}
int main() {
// 1. 初始化ACL运行时环境
aclError ret = aclInit(nullptr);
if (ret != ACL_ERROR_NONE) {
std::cerr << "Failed to init ACL, error code: " << ret << std::endl;
return -1;
}
// 2. 设置运行设备(例如第0号NPU)
int32_t deviceId = 0;
ret = aclrtSetDevice(deviceId);
if (ret != ACL_ERROR_NONE) {
std::cerr << "Failed to set device, error code: " << ret << std::endl;
aclFinalize();
return -1;
}
// 3. 准备测试数据
const uint32_t TOTAL_SIZE = 1024 * 1024; // 1M个元素
const uint32_t dataSizeInBytes = TOTAL_SIZE * sizeof(half);
// 在Host侧分配并初始化内存
half *h_a = new half[TOTAL_SIZE];
half *h_b = new half[TOTAL_SIZE];
half *h_c = new half[TOTAL_SIZE];
half *h_c_ref = new half[TOTAL_SIZE]; // CPU参考结果
srand(12345); // 固定随机种子,保证每次运行数据一致
generate_random_half_data(h_a, TOTAL_SIZE);
generate_random_half_data(h_b, TOTAL_SIZE);
// 计算CPU参考结果
cpu_add_reference(h_a, h_b, h_c_ref, TOTAL_SIZE);
// 4. 在Device侧(NPU的Global Memory)分配内存
half *d_a = nullptr;
half *d_b = nullptr;
half *d_c = nullptr;
ret = aclrtMalloc((void**)&d_a, dataSizeInBytes, ACL_MEM_MALLOC_HUGE_FIRST);
ret |= aclrtMalloc((void**)&d_b, dataSizeInBytes, ACL_MEM_MALLOC_HUGE_FIRST);
ret |= aclrtMalloc((void**)&d_c, dataSizeInBytes, ACL_MEM_MALLOC_HUGE_FIRST);
if (ret != ACL_ERROR_NONE) {
std::cerr << "Failed to allocate device memory!" << std::endl;
goto FINALIZE;
}
// 5. 将数据从Host拷贝到Device
ret = aclrtMemcpy(d_a, dataSizeInBytes, h_a, dataSizeInBytes, ACL_MEMCPY_HOST_TO_DEVICE);
ret |= aclrtMemcpy(d_b, dataSizeInBytes, h_b, dataSizeInBytes, ACL_MEMCPY_HOST_TO_DEVICE);
if (ret != ACL_ERROR_NONE) {
std::cerr << "Failed to copy data to device!" << std::endl;
goto FREE_DEVICE_MEM;
}
// 6. 配置核函数执行参数并启动
// 这是关键一步:决定启动多少个Block来并行执行我们的核函数
const uint32_t BLOCK_NUM = 256; // 假设启动256个Block
void* args[] = {&d_a, &d_b, &d_c, &TOTAL_SIZE};
// 使用rtKernelLaunch接口启动核函数
// 参数依次为:核函数指针、Grid维度(这里用Block数代替)、Block维度(这里为1)、参数列表、流(默认0)
ret = rtKernelLaunch((void*)add_custom_kernel, BLOCK_NUM, 1, args, 0, nullptr);
if (ret != RT_ERROR_NONE) {
std::cerr << "Failed to launch kernel! Error code: " << ret << std::endl;
goto FREE_DEVICE_MEM;
}
// 7. 等待核函数执行完成
ret = aclrtSynchronizeStream(0); // 同步默认流
if (ret != ACL_ERROR_NONE) {
std::cerr << "Failed to synchronize stream!" << std::endl;
goto FREE_DEVICE_MEM;
}
// 8. 将计算结果从Device拷贝回Host
ret = aclrtMemcpy(h_c, dataSizeInBytes, d_c, dataSizeInBytes, ACL_MEMCPY_DEVICE_TO_HOST);
if (ret != ACL_ERROR_NONE) {
std::cerr << "Failed to copy result back to host!" << std::endl;
goto FREE_DEVICE_MEM;
}
// 9. 验证结果:对比NPU计算结果和CPU参考结果
uint32_t errorCount = 0;
const half tolerance = static_cast<half>(1e-3); // 允许的误差范围
for (uint32_t i = 0; i < TOTAL_SIZE; ++i) {
if (std::fabs(static_cast<float>(h_c[i] - h_c_ref[i])) > static_cast<float>(tolerance)) {
errorCount++;
if (errorCount < 5) { // 只打印前几个错误示例
std::cout << "Mismatch at index " << i << ": NPU=" << static_cast<float>(h_c[i])
<< ", CPU=" << static_cast<float>(h_c_ref[i]) << std::endl;
}
}
}
if (errorCount == 0) {
std::cout << "SUCCESS! AddCustom kernel output matches CPU reference exactly." << std::endl;
} else {
std::cout << "FAILED! Found " << errorCount << " mismatches out of " << TOTAL_SIZE << " elements." << std::endl;
}
// 10. 资源清理
FREE_DEVICE_MEM:
if (d_a) aclrtFree(d_a);
if (d_b) aclrtFree(d_b);
if (d_c) aclrtFree(d_c);
FINALIZE:
delete[] h_a;
delete[] h_b;
delete[] h_c;
delete[] h_c_ref;
aclrtResetDevice(deviceId);
aclFinalize();
return (errorCount == 0) ? 0 : -1;
}
Host侧代码虽然长,但逻辑是线性的,就是一套标准的异构计算流程:初始化 -> 分配内存 -> 拷贝数据 -> 启动内核 -> 同步等待 -> 取回结果 -> 验证 -> 清理。其中 rtKernelLaunch 是触发核函数在NPU上执行的关键调用。
3. 编译与运行:见证你的代码“活”过来
代码写完了,怎么把它变成能在NPU上跑的程序呢?这就需要编译。Ascend C算子通常需要两阶段编译:首先将核函数代码编译成设备端代码,然后与Host侧代码链接成可执行文件。
3.1 编写编译脚本
创建一个 CMakeLists.txt 文件来管理编译过程,这是最规范的方式。
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(AddCustomOperator)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 寻找CANN Toolkit
find_package(CANN REQUIRED PATHS $ENV{ASCEND_HOME}/ascend-toolkit/latest)
# 包含CANN头文件目录
include_directories(${CANN_INCLUDE_DIRS})
# 添加可执行文件目标
add_executable(add_custom_demo main.cpp)
# 单独编译核函数为目标文件,使用不同的编译选项
set(KERNEL_SRC add_custom.cpp)
set(KERNEL_OBJ add_custom.o)
# 使用CANN提供的编译器编译核函数
add_custom_command(
OUTPUT ${KERNEL_OBJ}
COMMAND ${CANN_CXX_COMPILER} -x c++ -std=c++17 -O2
-I${CANN_INCLUDE_DIRS}
-D__CCE_KT_TEST__ # 定义宏,影响kernel_operator.h的行为
-c ${KERNEL_SRC} -o ${KERNEL_OBJ}
DEPENDS ${KERNEL_SRC}
COMMENT "Compiling Ascend C kernel..."
)
# 将核函数目标文件链接到主可执行文件
add_custom_target(compile_kernel DEPENDS ${KERNEL_OBJ})
add_dependencies(add_custom_demo compile_kernel)
target_link_libraries(add_custom_demo ${CANN_LIBRARIES} ${KERNEL_OBJ})
如果觉得CMake稍复杂,也可以写一个简单的 build.sh 脚本:
#!/bin/bash
# build.sh
set -e # 遇到错误立即退出
ASCEND_HOME=${ASCEND_HOME:-/usr/local/Ascend}
CANN_PATH=$ASCEND_HOME/ascend-toolkit/latest
# 1. 编译核函数设备代码
echo "[1/3] Compiling kernel code..."
$CANN_PATH/compiler/ccec_compiler/bin/ccec -x c++ -std=c++17 -O2 \
-I$CANN_PATH/runtime/include \
-I$CANN_PATH/runtime/include/ascendc \
-D__CCE_KT_TEST__ \
-c add_custom.cpp -o add_custom.o
# 2. 编译Host侧代码
echo "[2/3] Compiling host code..."
g++ -std=c++17 -O2 \
-I$CANN_PATH/runtime/include \
-I$CANN_PATH/runtime/include/ascendc \
-c main.cpp -o main.o
# 3. 链接成可执行文件
echo "[3/3] Linking executable..."
g++ -std=c++17 -o add_custom_demo main.o add_custom.o \
-L$CANN_PATH/runtime/lib64 \
-L$CANN_PATH/runtime/lib64/stub \
-lascendcl -lrt -ldl -lpthread
echo "Build successful! Executable: ./add_custom_demo"
给脚本加上执行权限并运行:
chmod +x build.sh
./build.sh
如果一切顺利,你会看到 Build successful! 的输出,并生成一个名为 add_custom_demo 的可执行文件。
3.2 运行与调试
激动人心的时刻到了,运行你的第一个算子!
./add_custom_demo
如果输出 SUCCESS! AddCustom kernel output matches CPU reference exactly.,那么恭喜你,你的第一个Ascend C算子已经正确地在昇腾AI处理器上运行了!
但现实往往没那么顺利。下面是一些你可能会遇到的常见问题及排查思路:
undefined reference to 'add_custom_kernel': 链接错误。核函数编译时没有生成正确的符号,或者链接时没找到目标文件。检查核函数编译命令是否正确,特别是-D__CCE_KT_TEST__宏是否定义。ACL_ERROR_NONE失败: 运行时初始化或内存分配失败。检查npu-smi确认设备状态正常,检查当前用户是否有访问NPU设备的权限。- 结果不匹配: 这是最典型的问题。首先,检查Host侧数据初始化和CPU参考计算是否正确。其次,在核函数中加入一些调试打印(注意,设备端打印需要特殊方式,初期建议通过比较更多样本定位问题)。最后,仔细核对核函数中的索引计算,确保每个Block处理的数据范围没有重叠或遗漏。
- 性能远低于预期: 对于这个简单例子,性能可能不是重点。但如果想探究,可以使用
msprof工具进行性能分析,看看时间是花在了计算上还是数据搬运上。
注意:在NPU上直接调试核函数比较困难。Ascend C提供了 “CPU/NPU孪生调试” 的强大功能。简单说,就是你可以用一套宏,让同一个核函数代码先在CPU上运行(方便用GDB等工具调试逻辑),然后再放到NPU上运行验证。对于复杂算子,这几乎是必备的调试手段。在我们的简单例子中,由于逻辑直接,用CPU参考结果对比验证就足够了。
4. 进阶思考:从“能跑”到“跑好”
恭喜你完成了从零到一的突破!但写出一个能正确运行的算子,只是算子开发的第一步。在真实的生产环境中,我们更关心算子是否“跑得好”——也就是性能是否足够高。这里分享几个最基础的优化方向,你可以基于现在的 AddCustom 代码进行尝试。
4.1 优化数据搬运
在我们当前的实现中,每个Block独立地从GM搬运数据到LM。对于向量加法这种计算强度很低(计算量/数据量比值小)的算子,数据搬运的时间很可能远远超过计算本身。优化搬运是提升性能的第一要务。
尝试思路:向量化搬运
Ascend C提供了 DataCopy 的向量化版本,可以一次搬运更多数据。查看 kernel_operator.h 头文件,寻找类似 DataCopyV2 或支持指定搬运宽度的接口。目标是让一次搬运操作填满AI Core的本地内存带宽。
尝试思路:调整Block大小
我们代码中 BLOCK_NUM = 256 是随意设定的。实际上,Block数量过多或过少都会影响性能。你可以尝试一个实验:固定 TOTAL_SIZE,分别用 64, 128, 256, 512 个Block来运行,记录时间。你会发现存在一个“甜点”值。
4.2 探索并行计算模式
我们目前只使用了Block级别的并行(每个Block处理一段连续数据)。实际上,Ascend C支持更丰富的并行层次:
- Core级并行: 在一个Block内部,可以启动多个“计算核心”同时工作。这需要用到
get_core_idx()和get_core_num()等接口。 - 向量指令: AI Core支持SIMD(单指令多数据)指令。对于加法这种规整操作,可以使用向量数据类型(如
float4)一次处理4个数据,而不是循环4次。
一个简单的向量化计算改造示意:
// 假设我们使用float4(需要确认硬件支持)
using float4 = struct { float x, y, z, w; };
LocalTensor<float4> localA4 = LP_ALLOC_TENSOR<float4>();
// ... 数据搬运时,以float4为单位搬运,数据量变为 realSize/4
for (uint32_t i = 0; i < realSize/4; ++i) {
localC4[i].x = localA4[i].x + localB4[i].x;
localC4[i].y = localA4[i].y + localB4[i].y;
// ... 以此类推
}
// 注意处理不能被4整除的尾部数据
4.3 理解Tiling与流水线
对于更复杂的算子(如矩阵乘法、卷积),数据量可能远超Local Memory的容量。这时就需要 “分块”(Tiling) 技术:将一大块数据分成许多小“瓷砖”(Tile),一次只搬运和计算一块瓷砖,循环处理。
更进一步,可以利用 “双缓冲”(Double Buffering) 或流水线技术,让数据搬运和计算重叠起来。当AI Core在计算当前Tile时,DMA控制器已经在搬运下一个Tile的数据了,从而隐藏数据搬运的延迟。
这些是算子性能优化的核心技巧,也是昇腾CANN训练营中“码力全开特辑”等进阶课程会深入讲解的内容。从我们今天的简单加法出发,理解这些概念,你就能逐步驾驭更复杂的算子开发任务。
当你成功运行起第一个算子,并开始思考如何让它更快时,你就已经踏上了昇腾AI底层开发这条既充满挑战又极具价值的道路。这条路没有捷径,但每一步的成长都清晰可见——从让一个加法跑起来,到优化一个卷积算子获得数倍性能提升,再到为某个特定模型设计出创新的融合算子。这种深入硬件、亲手塑造计算效率的体验,是应用层开发难以比拟的。我建议你把今天这个 add_custom_demo 项目保存好,它不仅是你的第一个里程碑,也是未来尝试各种优化想法的绝佳实验场。
1148



被折叠的 条评论
为什么被折叠?



