经过前两篇文章的铺垫,我们已经对CANN samples仓库的结构、版本配套以及核心工具链有了宏观的认识。现在,是时候卷起袖子,写真正能在昇腾(Ascend)设备上跑起来的代码了。
本篇文章将以inference/ACLHelloWorld为例,带你走完一个最基础的AscendCL(Ascend Computing Language)应用的完整流程。这个例子虽然简单,但它就像学习任何一门编程语言时的"Hello, World!"一样,是理解CANN应用生命周期的最短路径。
1. ACLHelloWorld:你的第一个CANN应用
ACLHelloWorld这个示例程序并不执行任何复杂的模型推理,它的核心目标只有一个:演示一个标准的AscendCL应用是如何初始化、运行和退出的。通过它,你将直观地理解CANN应用与硬件交互的基本步骤。
1.1. 准备工作:编译与运行
在深入代码之前,我们先看看如何让这个程序跑起来。假设你已经按照上一篇文章的指引,正确地搭建了合一环境。
-
进入示例目录:
bashcd ${HOME}/samples/inference/ACLHelloWorld/scripts这里的
${HOME}/samples是你下载的samples仓库的根目录。 -
编译代码:
scripts目录下提供了一个方便的编译脚本sample_build.sh。它会使用CMake和g++来编译src目录下的C++代码。bashbash sample_build.sh编译成功后,会在
../out目录下生成一个可执行文件main。 -
运行程序:
同样,
scripts目录下也提供了运行脚本sample_run.sh。bashbash sample_run.sh如果一切顺利,你将会在屏幕上看到类似下面的输出:
[INFO] The sample starts to run [INFO] Acl Init Success [INFO] Acl Set Device Success,Current DeviceID:0 [INFO] Acl Create Context Success [INFO] Acl Create Stream Success [INFO] Acl Destroy Stream Success [INFO] Acl Destroy Context success [INFO] Acl Reset Device Success [INFO] Acl Finalize Success [INFO] The program runs successfully看到这个输出,恭喜你!你已经成功运行了你的第一个CANN应用。
2. 代码解读:AscendCL应用的生命周期
现在,让我们深入到src/main.cpp文件中,逐行解读这段代码,看看它到底做了什么。
一个AscendCL应用遵循一个非常经典且对称的资源管理模式:初始化 -> 创建资源 -> 执行业务 -> 销毁资源 -> 去初始化。这就像你早上起床后要先穿好衣服(初始化),然后使用各种工具(创建资源)完成一天的工作(执行业务),下班后把工具放回原位(销毁资源),最后脱衣睡觉(去初始化)一样,是一个有始有终的过程。
2.1. 初始化阶段
这是程序的入口,也是与昇腾硬件建立联系的第一步。
cpp
#include "acl/acl.h"
int main(int argc, char *argv[])
{
// ...
const char *aclConfigPath = "../src/acl.json";
aclError ret = aclInit(aclConfigPath);
if (ret != ACL_ERROR_NONE) { /* 错误处理 */ }
INFO_LOG("Acl Init Success");
ret = aclrtSetDevice(deviceId);
if (ret != ACL_ERROR_NONE) { /* 错误处理 */ }
INFO_LOG("Acl Set Device Success, Current DeviceID:%d", deviceId);
// ...
}
-
aclInit(aclConfigPath): 这是所有AscendCL调用的起点。它负责初始化AscendCL框架。你可以传入一个JSON配置文件的路径,用于一些高级配置,但对于大多数应用,一个空的acl.json({})就足够了。 -
aclrtSetDevice(deviceId): 我们的计算机上可能有多张昇腾AI加速卡,这张卡就是我们所说的Device 。这个函数的作用就是告诉AscendCL:"嘿,我接下来的所有操作,都想在deviceId这张卡上进行。" 在这个例子中,deviceId被硬编码为0,表示使用第一张卡。
2.2. 资源创建阶段
指定了Device之后,我们还需要在它上面创建两个非常重要的概念:Context 和Stream。
cpp
// ...
aclrtContext context = nullptr;
aclrtStream stream = nullptr;
ret = aclrtCreateContext(&context, deviceId);
if (ret != ACL_ERROR_NONE) { /* 错误处理 */ }
INFO_LOG("Acl Create Context Success");
ret = aclrtCreateStream(&stream);
if (ret != ACL_ERROR_NONE) { /* 错误处理 */ }
INFO_LOG("Acl Create Stream Success");
// ...
-
Context(上下文):你可以把它想象成一个"工作空间"。它管理着在同一个Device上运行的所有任务和资源。你在一个Context里分配的内存、加载的模型,都归这个Context所有。通常情况下,一个线程只使用一个Context。
-
Stream(流) :如果说Context是一个大的工作空间,那么Stream就是这个空间里的一条"流水线"。所有在同一个Stream里的任务,比如内存拷贝、模型推理等,都会按照你提交的顺序依次执行。CANN支持创建多个Stream,这使得我们可以通过并行执行不同的任务来提升应用的整体性能,这也就是所谓的"多流并发"。
2.3. 业务执行阶段
在ACLHelloWorld这个简单的例子中,并没有任何实际的业务逻辑。代码在这里留了一个注释,告诉你真正的模型推理、数据处理等代码应该放在这里。
cpp
/*
* 业务执行
*/
在后续的文章中,我们将会在这里填充上模型加载、内存申请、数据拷贝和模型执行等真实的代码。
2.4. 资源销毁与去初始化阶段
有借有还,再借不难。程序结束前,我们需要把自己申请的资源一一释放掉,这是一个好习惯,也能避免内存泄漏等问题。
资源的销毁顺序应该与创建的顺序严格相反。
cpp
// ...
ret = aclrtDestroyStream(stream);
if (ret != ACL_ERROR_NONE) { /* ... */ }
INFO_LOG("Acl Destroy Stream Success");
ret = aclrtDestroyContext(context);
if (ret != ACL_ERROR_NONE) { /* ... */ }
INFO_LOG("Acl Destroy Context success");
ret = aclrtResetDevice(deviceId);
if (ret != ACL_ERROR_NONE) { /* ... */ }
INFO_LOG("Acl Reset Device Success");
ret = aclFinalize();
if (ret != ACL_ERROR_NONE) { /* ... */ }
INFO_LOG("Acl Finalize Success");
// ...
aclrtDestroyStream(stream): 销毁我们创建的Stream。aclrtDestroyContext(context): 销毁我们创建的Context。aclrtResetDevice(deviceId): 重置我们之前指定的Device,释放该Device上的所有资源。aclFinalize(): 这是aclInit的对应操作,用于去初始化整个AscendCL框架。
3. 总结
通过ACLHelloWorld这个麻雀虽小五脏俱全的例子,我们走通了一个最基本的AscendCL应用的完整生命周期。请务必牢记这个"初始化 -> 创建 -> 执行 -> 销毁 -> 去初始化"的对称结构,它将贯穿我们后续所有的CANN应用开发过程。
现在,你已经不再是一个CANN编程的门外汉了。在下一篇文章中,我们将在这个"Hello, World"的基础上,引入真正的AI模型,看看如何加载一个OM模型并执行一次完整的推理。