CANN Runtime 模型运行实例(Model RI)实战:用 aclrtSwitchStream 与 aclrtActiveStream 实现图内 Stream 条件跳转与激活
【免费下载链接】runtime本项目提供CANN运行时组件和维测功能组件。项目地址: https://gitcode.com/cann/runtime
导读
本篇文章基于 CANN Runtime 开源仓库中的2_model_switch样例,深入讲解如何通过aclmdlRIBuildBegin系列接口构建"模型运行实例"(Model Runtime Instance,Model RI),并在图任务中利用aclrtSwitchStream(条件 Stream 跳转)与aclrtActiveStream(Stream 激活)实现多分支控制流。读完本文,你将掌握 Model RI 从创建、绑定 Stream、下发任务到执行、解绑的完整生命周期,理解图内条件分支与并行分支的编排方式,并能在自己的算子任务中复现这套"数据依赖驱动分支"的编程范式。
一、背景:为什么需要"模型运行实例"与图内控制流
常规的算子执行方式是把单个算子或一组算子按顺序下发到 Stream 上串行执行。但真实推理/训练场景中,图(Graph)内部往往存在条件分支(根据某个值决定走哪条计算路径)与并行激活(一个 Stream 完成后激活另一条 Stream 开始执行)。CANN Runtime 提供了两套互补的能力:
- 模型运行实例(Model RI):通过
aclmdlRI*系列接口,可以在**不依赖离线模型文件(.om)**的情况下,把一组绑定的 Stream 及下发到其上的任务"打包"成一个可重复执行的模型实例,再通过aclmdlRIExecuteAsync异步执行整张图; - 图内控制流原语:
aclrtSwitchStream在任务执行期间根据 Device 侧数据的比较结果决定跳转到哪条 Stream;aclrtActiveStream在一条 Stream 执行到某一点时激活另一条 Stream,形成并行执行关系。
2_model_switch样例正是把两者结合:先构建一个绑定 5 条 Stream 的模型实例,在其中编排 2 条条件跳转分支,再连续执行两次(分别命中不同分支),验证控制流正确性。
二、样例概述与产品支持情况
样例位于 example/2_advanced_features/model_ri/2_model_switch,目录结构如下:
2_model_switch/ ├── CMakeLists.txt # 构建配置(链接 libascendcl / libnnopbase / libopapi) ├── README.md # 中文说明 ├── README_en.md # 英文说明 ├── main.cpp # 样例主程序 └── run.sh # 编译 + 运行脚本关键接口在不同产品上的支持情况如下(来自 README_en.md):
| 接口 | Ascend 950PR / Ascend 950DT | Atlas A3 训练/推理系列 | Atlas A2 训练/推理系列 |
|---|---|---|---|
| aclmdlRIBuildBegin | 支持 | 支持 | 支持 |
| aclmdlRIBindStream | 支持 | 支持 | 支持 |
| aclmdlRIEndTask | 支持 | 支持 | 支持 |
| aclmdlRIBuildEnd | 支持 | 支持 | 支持 |
| aclmdlRIUnbindStream | 支持 | 支持 | 支持 |
| aclmdlRIExecuteAsync | 支持 | 支持 | 支持 |
| aclrtSwitchStream | 支持 | 不支持 | 支持 |
| aclrtActiveStream | 支持 | 不支持 | 支持 |
注意:
aclrtSwitchStream与aclrtActiveStream在 Atlas A3 系列上不支持,移植该样例到 A3 系列产品前需确认硬件与 CANN 版本支持情况。
三、环境准备与编译运行
3.1 前置条件
- 已安装 CANN 软件包(默认安装在
/usr/local/Ascend,下文以${install_root}指代安装根目录); - 已克隆本仓库,样例根目录为
example/; - 具备编译工具链与 CMake(
CMakeLists.txt要求最低版本 3.16.0)。
3.2 编译运行步骤
# 1. 加载 CANN 运行环境(${install_root} 替换为实际安装根目录) source ${install_root}/cann/set_env.sh # 2. 自动识别 SOC_VERSION 与 ASCENDC_CMAKE_DIR source ${git_clone_path}/example/set_sample_env.sh # 3. 编译并运行(编译产物安装到 ./out,运行日志输出到 output_msg.txt) bash run.shrun.sh 内部依次执行:加载setenv.bash→cmake -B build(传入ASCEND_CANN_PACKAGE_PATH)→cmake --build build -j→cmake --install build→ 运行./build/main | tee output_msg.txt,日志同时打印到终端并落盘到output_msg.txt。
3.3 构建配置要点
CMakeLists.txt 展示了本例的链接依赖:
include(${ASCENDC_CMAKE_DIR}/ascendc.cmake):引入 AscendC 构建宏(SOC_VERSION 与 ASCENDC_CMAKE_DIR 由set_sample_env.sh注入环境变量);ascendc_library(kernels STATIC ../../../kernel_func/easy_OP.cpp):把样例共用的算子 kernel 源文件编译成静态库;target_link_libraries(main ... libascendcl.so libnnopbase.so libopapi.so):链接 AscendCL 运行时、NN 算子底座与算子 API 库;- 编译目标
main由main.cpp与上一级目录的 model_utils.cpp 共同构成。
四、核心 API 全景解析
样例同时覆盖"模型实例生命周期"与"图内控制流"两类接口,全部声明于 include/external/acl/acl_rt.h。
4.1 模型运行实例(Model RI)接口
| 接口 | 作用 | 关键参数 |
|---|---|---|
aclmdlRIBuildBegin(aclmdlRI* modelRI, uint32_t flag) | 开始构建模型运行实例 | flag为保留参数,必须传0(acl_rt.h L4904) |
aclmdlRIBindStream(modelRI, stream, flag) | 将模型实例与 Stream 绑定 | flag取值ACL_MODEL_STREAM_FLAG_HEAD(入口流)或ACL_MODEL_STREAM_FLAG_DEFAULT(acl_rt.h L4915) |
aclmdlRIEndTask(modelRI, stream) | 标记任务下发结束(EndGraph) | 一条模型只能有一个 EndTask,样例中只放在汇聚流 endStream 上 |
aclmdlRIBuildEnd(modelRI, void* reserve) | 结束构建,通知 Runtime 模型已就绪 | reserve必须传nullptr(acl_rt.h L4935) |
aclmdlRIUnbindStream(modelRI, stream) | 解除模型实例与 Stream 的绑定 | 见 acl_rt.h L4945 |
aclmdlRIExecuteAsync(modelRI, stream) | 异步执行模型推理 | 执行流可与绑定流分离 |
aclmdlRIDestroy(modelRI) | 销毁模型运行实例 | 须在解绑所有 Stream 之后调用 |
在 src/runtime/feature/model/model.cc 中可以看到这些接口的构建期校验逻辑:调用aclmdlRIBuildEnd之前必须先调用aclmdlRIBindStream绑定 Stream,且必须先调用aclmdlRIEndTask标记任务下发结束,否则会返回RT_ERROR_MODEL_NOT_END(错误码 EE1018)等构建错误。
4.2 图内控制流接口
aclrtSwitchStream—— 根据条件跳转 Stream(acl_rt.h L4342):
aclError aclrtSwitchStream(void* leftValue, aclrtCondition cond, void* rightValue, aclrtCompareDataType dataType, aclrtStream trueStream, aclrtStream falseStream, aclrtStream stream);leftValue/rightValue:参与比较的左右值,均为Device 侧内存地址(比较在 Device 上执行);cond:比较条件,取aclrtCondition枚举(acl_rt.h L727-L734):
typedef enum { ACL_RT_EQUAL = 0, // 相等 ACL_RT_NOT_EQUAL, // 不相等 ACL_RT_GREATER, // 大于 ACL_RT_GREATER_OR_EQUAL, // 大于等于 ACL_RT_LESS, // 小于 ACL_RT_LESS_OR_EQUAL // 小于等于 } aclrtCondition;dataType:比较值的数据类型,取aclrtCompareDataType(acl_rt.h L736-L739):ACL_RT_SWITCH_INT32 = 0或ACL_RT_SWITCH_INT64 = 1;trueStream:条件成立时跳转的目标 Stream;falseStream:保留参数,必须传nullptr;stream:下发该跳转任务的 Stream。
底层实现在 src/acl/aclrt_impl/stream.cpp L374-L394:aclrtSwitchStreamImpl会对leftValue、rightValue、trueStream、stream做非空校验,并通过ACL_CHECK_INVALID_PARAM_NO_VALUE(falseStream == nullptr, ...)强制falseStream必须为nullptr(当前版本未开放 false 分支),随后调用rtsSwitchStream下探到 Runtime 驱动层。
aclrtActiveStream—— 激活一条 Stream(acl_rt.h L4327):
aclError aclrtActiveStream(aclrtStream activeStream, aclrtStream stream);在stream上插入一个"激活任务":当stream执行到该点时,激活activeStream开始执行,形成并行执行关系。底层aclrtActiveStreamImpl(stream.cpp L361-L372)对两个入参做非空校验后调用rtsActiveStream。
使用提示:
aclrtActiveStream的两个 Stream应都已绑定到同一个模型实例,否则激活语义不受图调度保护(详见样例代码注释)。
4.3 配套基础接口
样例还演示了完整的初始化/资源管理链路:
- 初始化:
aclInit/aclFinalize; - Device:
aclrtSetDevice/aclrtResetDeviceForce; - Context:
aclrtCreateContext/aclrtDestroyContext; - Stream:
aclrtCreateStream/aclrtCreateStreamWithConfig(如ACL_STREAM_PERSISTENT)/aclrtSynchronizeStream/aclrtDestroyStream/aclrtDestroyStreamForce; - 内存:
aclrtMalloc/aclrtFree; - 数据传输:
aclrtMemcpy/aclrtMemcpyAsync(支持 H2D、D2D、D2H)。
五、样例主程序逐段解析
完整源码见 example/2_advanced_features/model_ri/2_model_switch/main.cpp,下面按逻辑分段说明。
5.1 数据准备:三个 aclnnAdd 算子任务
样例定义了三组 shape 为{4, 2}的 float 输入,alpha = 1.2,调用aclnnAddGetWorkspaceSize获取 workspace 并分配 Device 内存,随后通过ModelUtils::CreateAclTensor创建aclTensor(该工具函数在 model_utils.cpp 中完成aclrtMalloc+ stride 计算 +aclCreateTensor):
// out = self + other * alpha // stream1: out1 = self1 + other1 * alpha self1={1} other1={2} → 3.4 // stream2: out2 = self2 + other2 * alpha self2={2} other2={2} → 4.4 // stream3: out3 = self3 + other3 * alpha self3={3} other3={2} → 5.4 vector<float> selfHostData1 = {1, 1, 1, 1, 1, 1, 1, 1}; vector<float> otherHostData1 = {2, 2, 2, 2, 2, 2, 2, 2}; // ... self2/other2、self3/other3 同理 float alphaValue = 1.2f;aclrtMalloc统一使用ACL_MEM_MALLOC_HUGE_FIRST大页优先策略。
5.2 构造跳转条件:Device 侧比较值
aclrtSwitchStream的比较发生在 Device 侧,因此需要把比较值先拷贝到 Device 内存(main.cpp L97-L115):
int32_t rightValue1 = 1; // 分支条件:numDevice == 1 int32_t rightValue2 = 2; // 分支条件:numDevice == 2 aclrtCondition condition = ACL_RT_EQUAL; aclrtCompareDataType dataType = ACL_RT_SWITCH_INT32; // aclrtMalloc + aclrtMemcpy(HOST_TO_DEVICE) 拷贝 rightValue1/rightValue2 // 再申请 numDevice(uint32_t),初值为 0图内部还会执行一次ACL_MEMCPY_DEVICE_TO_DEVICE拷贝,把targetValDev_1(值为 1 或 2,可被 Host 侧改写)写入numDevice,作为 SwitchStream 的实际判断依据——这就是"运行期动态决定分支"的关键设计。
5.3 创建 Stream 并构建模型实例
创建 5 条持久化工作流 + 1 条独立拷贝流(main.cpp L125-L130):
aclrtCreateStreamWithConfig(&stream1, 0x00U, ACL_STREAM_PERSISTENT); // stream2、stream3、stream4、endStream 同理 aclrtCreateStream(©Stream); // 独立拷贝流,不绑定模型,避免流状态异常随后aclmdlRIBuildBegin开始构建实例,并将 5 条流全部绑定,stream1标记为入口流(HEAD),其余为 DEFAULT(main.cpp L132-L137):
aclmdlRIBuildBegin(&modelRI, 0x00U); aclmdlRIBindStream(modelRI, stream1, ACL_MODEL_STREAM_FLAG_HEAD); aclmdlRIBindStream(modelRI, stream2, ACL_MODEL_STREAM_FLAG_DEFAULT); aclmdlRIBindStream(modelRI, stream3, ACL_MODEL_STREAM_FLAG_DEFAULT); aclmdlRIBindStream(modelRI, stream4, ACL_MODEL_STREAM_FLAG_DEFAULT); aclmdlRIBindStream(modelRI, endStream, ACL_MODEL_STREAM_FLAG_DEFAULT);5.4 编排各条 Stream 上的任务
stream1(入口流):拷贝 self1/other1 → 执行 add1 → D2D 拷贝把 1 写入numDevice(main.cpp L139-L147):
aclrtMemcpyAsync(selfDevice1, size, selfHostData1.data(), size, ACL_MEMCPY_HOST_TO_DEVICE, stream1); // otherDevice1 同理 aclnnAdd(addWorkspaceAddr1, addWorkspaceSize1, addExecutor1, stream1); aclrtMemcpyAsync(numDevice, sizeof(uint32_t), targetValDev_1, sizeof(uint32_t), ACL_MEMCPY_DEVICE_TO_DEVICE, stream1);stream2 / stream3:各执行一个 add,并在完成后激活汇聚流 endStream(main.cpp L149-L153):
aclnnAdd(addWorkspaceAddr2, addWorkspaceSize2, addExecutor2, stream2); aclrtActiveStream(endStream, stream2); // stream2 完成后激活 endStream aclnnAdd(addWorkspaceAddr3, addWorkspaceSize3, addExecutor3, stream3); aclrtActiveStream(endStream, stream3); // stream3 完成后激活 endStreamstream1 上的两条条件跳转(main.cpp L156-L157):
// numDevice == 1 → 跳转到 stream3 aclrtSwitchStream(numDevice, condition, rightDevice1, dataType, stream3, nullptr, stream1); // numDevice == 2 → 跳转到 stream4 aclrtSwitchStream(numDevice, condition, rightDevice2, dataType, stream4, nullptr, stream1);stream4:激活 stream2(两个流并行执行,且都已绑定模型实例,main.cpp L159):
aclrtActiveStream(stream2, stream4);EndTask 收束:只在汇聚流 endStream 上标记任务下发结束(一个模型只能有一个 EndGraph),然后结束构建(main.cpp L160-L163):
aclmdlRIEndTask(modelRI, endStream); aclmdlRIBuildEnd(modelRI, NULL);5.5 执行模型并回拷结果
aclmdlRIExecuteAsync使用独立的executeStream异步执行整张图,aclrtSynchronizeStream阻塞等待完成;结果的 D2H 回拷使用未绑定模型的copyStream,避免与图内流互相干扰(main.cpp L164-L173)。
5.6 第二次执行:验证另一条分支
第二次执行前,Host 侧通过copyStream把targetValDev_1的值从 1 改写为 2,并把上一次的 Host 结果清零,再次执行同一模型实例(main.cpp L183-L199)。由于图内 D2D 拷贝会把 2 写入numDevice,本次执行会命中第二条跳转分支——同一个模型实例无需重建即可按不同数据走不同路径,这是 Model RI 相比一次性下发任务的显著优势。
5.7 资源回收
样例严格按逆序释放资源(main.cpp L207-L256):aclmdlRIUnbindStream逐条解绑 →aclmdlRIDestroy销毁实例 → 销毁全部 Stream →aclDestroyTensor/aclDestroyScalar→aclrtFree全部内存 →aclrtDestroyContext→aclrtResetDeviceForce→aclFinalize。
六、执行路径推演与示例输出解读
6.1 两种分支路径
- 路径 1(numDevice == 1):
stream1(add1=3.4)→ SwitchStream 命中 →stream3(add3=5.4,激活 endStream)→endStreamEndTask。stream2与stream4从未被激活,因此out2 保持 0.0。 - 路径 2(numDevice == 2):
stream1(add1=3.4)→ SwitchStream 命中 →stream4(激活 stream2)→stream2(add2=4.4,激活 endStream)→endStreamEndTask。stream3不被执行,因此out3 保持 0.0。
6.2 示例输出
[INFO] After executing, print data1. [INFO] The vector data is: 3.4000 3.4000 3.4000 3.4000 3.4000 3.4000 3.4000 3.4000 [INFO] After executing, print data2. [INFO] The vector data is: 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 [INFO] After executing, print data3. [INFO] The vector data is: 5.4000 5.4000 5.4000 5.4000 5.4000 5.4000 5.4000 5.4000 ... [INFO] After second execution, print data2. [INFO] The vector data is: 4.4000 4.4000 4.4000 4.4000 4.4000 4.4000 4.4000 4.4000数值与公式out = self + other × alpha完全吻合(3.4 = 1 + 2×1.2,5.4 = 3 + 2×1.2,4.4 = 2 + 2×1.2),且两次执行分别验证了"data2 为 0"(分支未命中,stream2 未执行)与"data2 为 4.4"(分支命中)两种状态,说明条件跳转与 Stream 激活均按预期工作。README 中...省略号之后的部分对应第二次执行的 data1/data2/data3 全部打印(实际 main.cpp 会完整打印三组数据)。
七、关键注意事项与最佳实践
- falseStream 必须为 nullptr:
aclrtSwitchStream的 false 分支参数为保留项,底层实现会强制校验(见 stream.cpp L386-L387),误传非空值会返回参数错误。 - 比较值必须在 Device 侧:
leftValue/rightValue指向的是 Device 内存地址,Host 侧数据需先用aclrtMemcpy(H2D)或图内aclrtMemcpyAsync(D2D)就位。 - 一条模型只能有一个 EndTask:多个分支汇聚后,仅在汇聚流上调用一次
aclmdlRIEndTask;aclmdlRIBuildEnd的reserve参数必须传nullptr。 - 执行流与绑定流分离是安全的:
aclmdlRIExecuteAsync的executeStream不必是模型绑定的流;同理,独立的copyStream用于数据搬运,可避免把拷贝任务混入图调度导致状态异常。 aclrtActiveStream的双方流应绑定同一模型实例:否则并行激活语义不受图级调度保护。- 产品兼容性:
aclrtSwitchStream/aclrtActiveStream在 Atlas A3 系列上不支持(见第二节表格),跨产品移植前务必核对。 - 接口顺序约束:
aclmdlRIBindStream必须在aclmdlRIBuildEnd之前完成,aclmdlRIEndTask也必须在aclmdlRIBuildEnd之前调用(源码构建校验见 src/runtime/feature/model/model.cc),否则构建失败并报错。
八、延伸阅读
- 样例根目录环境说明:example/README_en.md;
- 样例环境变量脚本:example/set_sample_env.sh;
- 模型运行实例管理接口说明:docs/zh/api_ref/15_model_running_instance_management.md;
- 同目录下其他 Model RI 样例:
0_simple_model(基础创建/执行)、1_model_update(模型更新)、3_cond_model(条件模型)、4_model_sync_external(外部同步)、5_reusable_buffer_reset(复用缓冲区重置); - ACL Graph 捕获与任务组机制可参考:docs/zh/dev_guide/04_ACL-Graph.md。
九、已知问题
当前版本该样例无已知问题(Known Issues: None)。
【免费下载链接】runtime本项目提供CANN运行时组件和维测功能组件。项目地址: https://gitcode.com/cann/runtime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考