CANN Runtime 模型运行时实例(Model RI)任务更新实战:基于 aclmdlRICaptureTaskGrp 与 aclmdlRICaptureTaskUpdate 的算子级模型更新
【免费下载链接】runtime本项目提供CANN运行时组件和维测功能组件。项目地址: https://gitcode.com/cann/runtime
导读
本文基于 CANN Runtime 开源仓库中的1_model_update示例(example/2_advanced_features/model_ri/1_model_update),讲解如何在使用aclmdlRICaptureBegin捕获模型运行时实例(Model RI)之后,对实例中已捕获的算子任务组进行动态更新。读完本文,你将掌握 Model RI 的捕获、任务组分段标记、通过句柄热更新算子(如更换aclnnAdd的alpha标量值)的完整调用链,以及配套的 Event 同步机制,能够直接在 Atlas A2/A3 训练与推理系列产品或 Ascend 950 系列硬件上复现运行。
一、背景:为什么需要"捕获后的任务更新"
在 0_simple_model 示例中,模型运行时实例的用法是"捕获一段 Stream 上的任务序列,封装成aclmdlRI,再反复执行"。这种模式的优点是任务编排一次成型、多次复用,执行开销低。
但真实推理/训练场景往往存在"同一份计算骨架、参数动态变化"的需求:例如每次推理时算子的标量参数、权重或分支选择不同。如果每次参数变化都重新捕获整个模型,会带来明显的重建开销。
1_model_update示例解决的正是这个问题:在捕获阶段用aclmdlRICaptureTaskGrpBegin/End将一段任务标记为"可更新任务组",捕获完成后通过句柄用aclmdlRICaptureTaskUpdateBegin/End在运行期动态替换该任务组中的算子。模型实例本身无需重建,仅需替换其中一处算子计算即可得到新的输出结果。
二、示例的数学语义与总体流程
示例在main.cpp中构建了一个两层aclnnAdd的复合计算,aclnnAdd的计算语义为out = self + other * alpha(见 main.cpp 的注释):
- 第一层:
outTmp = self + other * alpha,其中alpha = 1.1; - 第二层:
outTmp = outTmp + other * alpha,其中alpha = 1.1; - 更新后:第二层被替换为
outTmp = outTmp + other * updateAlpha,其中updateAlpha = 2.2。
输入数据:self = {1,2,3,4,5,6,7,8},other = {2,2,2,2,2,2,2,2},shape 为{4, 2},数据类型为ACL_FLOAT。
因此输出从第一轮循环的self + 2 * alpha * other = self + 2.2 * other = {5.4, 6.4, ..., 12.4},变为更新后的self + (alpha + updateAlpha) * other = self + 3.3 * other = {7.6, 8.6, ..., 14.6},与文档 Sample Output 中两次循环的打印结果完全吻合。
整体流程分三个阶段:
- 准备阶段:初始化 AscendCL、设置 Device/Context/Stream,创建输入输出张量与三个算子执行器(
firstAddExecutor、secondAddExecutor、updateAddExecutor); - 捕获阶段:在捕获 Stream 上依次下发
memcpy + 第一层 add + 第二层 add,其中第二层被aclmdlRICaptureTaskGrpBegin/End包裹成可更新任务组,最后aclmdlRICaptureEnd拿到modelRI句柄; - 执行/更新阶段:循环执行
aclmdlRIExecuteAsync,在第二轮循环中通过句柄调用aclmdlRICaptureTaskUpdateBegin/End,把任务组内的算子替换为携带updateAlpha的执行器,并借助 Event 保证更新先于模型任务执行完成。
三、运行环境与构建步骤
3.1 产品支持情况
依据 README_en.md,该示例在以下产品上受支持:
| 产品 | 支持情况 |
|---|---|
| Ascend 950PR / Ascend 950DT | 支持 |
| Atlas A3 训练系列 / A3 推理系列 | 支持 |
| Atlas A2 训练系列 / A2 推理系列 | 支持 |
3.2 环境准备与编译运行
按文档给出的标准流程(更多细节见 example 目录 README):
# 将 ${install_root} 替换为 CANN 安装根目录,默认安装于 /usr/local/Ascend source ${install_root}/cann/set_env.sh # 自动识别 SOC_VERSION 和 ASCENDC_CMAKE_DIR source ${git_clone_path}/example/set_sample_env.sh # 构建并运行 bash run.shrun.sh(example/2_advanced_features/model_ri/1_model_update/run.sh)的实际行为是:
source $ASCEND_INSTALL_PATH/bin/setenv.bash引入 CANN 环境;- 以
-DASCEND_CANN_PACKAGE_PATH=${ASCEND_INSTALL_PATH}参数执行cmake -B build,随后cmake --build build -j编译、cmake --install build安装; - 运行
./build/main,并将输出通过tee同时写入output_msg.txt。
构建所需的链接库在 CMakeLists.txt 中声明为三个:
libascendcl.so:AscendCL 运行时主库,提供aclInit、aclrt*及aclmdlRI*系列接口;libnnopbase.so:算子基础库;libopapi.so:提供aclnnAdd等算子 API 的aclnnAddGetWorkspaceSize与aclnnAdd。
头文件搜索路径包括$ASCEND_CANN_PACKAGE_PATH/include、$ASCEND_CANN_PACKAGE_PATH/aclnn,以及仓库内的example公共目录(用于引用utils.h和model_utils.h)。
3.3 预期输出
[INFO] Execute model, loop count: 1. [INFO] The vector data is: 5.4000 6.4000 7.4000 8.4000 9.4000 10.4000 11.4000 12.4000 [INFO] Execute model, loop count: 2. [INFO] Update alpha value of aclnnAdd [INFO] The vector data is: 7.6000 8.6000 9.6000 10.6000 11.6000 12.6000 13.6000 14.6000两次循环输出的差异(第二项输出整体 +2.2)正是任务组更新生效的直观证据。
四、源码级解析:三段式关键调用链
下面按 main.cpp 的执行顺序拆解关键调用链。
4.1 准备阶段:初始化、张量与执行器
CHECK_ERROR(aclInit(NULL)); CHECK_ERROR(aclrtSetDevice(deviceId)); CHECK_ERROR(aclrtCreateContext(&context, deviceId));deviceId = 0,使用默认设备。接着通过ModelUtils::CreateAclTensor(实现在 model_utils.cpp)为self、other、out、outTmp分配 Device 内存并创建aclTensor:该工具函数先aclrtMalloc申请GetShapeSize(shape) * sizeof(float)字节的 Device 内存,再按连续排布计算strides,最后以ACL_FORMAT_ND格式调用aclCreateTensor绑定地址。
三个算子执行器通过aclnnAddGetWorkspaceSize创建,并按需分配 workspace:
aclnnAddGetWorkspaceSize(self, other, alpha, outTmp, &firstAddWorkspaceSize, &firstAddExecutor); if (firstAddWorkspaceSize > 0) { CHECK_ERROR(aclrtMalloc(&firstAddWorkspaceAddr, firstAddWorkspaceSize, ACL_MEM_MALLOC_HUGE_FIRST)); }第二层执行器secondAddExecutor(使用alpha=1.1)与更新执行器updateAddExecutor(使用updateAlpha=2.2)用同样方式创建。注意更新执行器在捕获阶段之前就已准备好,这是实现"运行时零重建"的前提——更新只是在执行阶段切换执行器,而不是现场编译算子。
4.2 捕获阶段:标记可更新任务组
捕获阶段的核心代码如下:
aclmdlRI modelRI; aclrtStream stream; aclrtEvent event; CHECK_ERROR(aclrtCreateStream(&stream)); // 该 flag 专门用于捕获场景下的任务更新 CHECK_ERROR(aclrtCreateEventWithFlag(&event, ACL_EVENT_EXTERNAL)); CHECK_ERROR(aclmdlRICaptureBegin(stream, ACL_MODEL_RI_CAPTURE_MODE_RELAXED)); CHECK_ERROR(aclrtMemcpy(selfDevice, size, selfHostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE)); CHECK_ERROR(aclrtMemcpy(otherDevice, size, otherHostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE)); aclnnAdd(firstAddWorkspaceAddr, firstAddWorkspaceSize, firstAddExecutor, stream); // 阻塞 stream 等待 event,确保后续执行的任务是更新后的 CHECK_ERROR(aclrtStreamWaitEvent(stream, event)); CHECK_ERROR(aclrtResetEvent(event, stream)); aclrtTaskGrp handle; // 将该处的任务设置为可更新的,后续可通过句柄 handle 更新 CHECK_ERROR(aclmdlRICaptureTaskGrpBegin(stream)); aclnnAdd(secondAddWorkspaceAddr, secondAddWorkspaceSize, secondAddExecutor, stream); CHECK_ERROR(aclmdlRICaptureTaskGrpEnd(stream, &handle)); CHECK_ERROR(aclmdlRICaptureEnd(stream, &modelRI));几个关键点:
- 捕获模式:
aclmdlRICaptureBegin的第二个参数是aclmdlRICaptureMode枚举。在 include/external/acl/acl_rt.h 中定义为三种取值:ACL_MODEL_RI_CAPTURE_MODE_GLOBAL = 0、ACL_MODEL_RI_CAPTURE_MODE_THREAD_LOCAL、ACL_MODEL_RI_CAPTURE_MODE_RELAXED。本示例使用RELAXED模式,它允许捕获的任务序列中带有 Stream 等待、Event 记录等同步原语,适合多任务组、可更新的复杂模型。 - 任务组句柄:
aclmdlRICaptureTaskGrpEnd(stream, &handle)输出的aclrtTaskGrp handle是后续更新的唯一凭证(接口定义见 acl_rt.h)。捕获阶段每对GrpBegin/GrpEnd对应一个可独立更新的任务组。 - Event 的捕获语义:注释明确指出
ACL_EVENT_EXTERNAL这个 flag 专门用于捕获场景下的任务更新。aclrtStreamWaitEvent(stream, event)会让 model 内的任务在更新完成前"停在原地",从而保证模型执行时读到的是最新算子,而不是陈旧版本。
4.3 执行与更新阶段:热替换任务组
for (int i = 0; i < loopCount; i++) { INFO_LOG("Execute model, loop count: %d.", i + 1); CHECK_ERROR(aclmdlRIExecuteAsync(modelRI, stream)); if (i == 1) { // 通过 handle 对可更新区域进行更新,更新后的任务类型和数量应与更新前保持一致 CHECK_ERROR(aclmdlRICaptureTaskUpdateBegin(updateStream, handle)); aclnnAdd(updateAddWorkspaceAddr, updateAddWorkspaceSize, updateAddExecutor, updateStream); INFO_LOG("Update alpha value of aclnnAdd"); CHECK_ERROR(aclmdlRICaptureTaskUpdateEnd(updateStream)); } // 更新时使用 event 进行同步,确保 model 执行任务中执行的是更新后的任务 CHECK_ERROR(aclrtRecordEvent(event, updateStream)); CHECK_ERROR(aclrtSynchronizeStream(updateStream)); CHECK_ERROR(aclrtSynchronizeStream(stream)); CHECK_ERROR(aclrtMemcpy(outHostData.data(), size, outDevice, size, ACL_MEMCPY_DEVICE_TO_HOST)); ModelUtils::PrintArray(outHostData); }更新机制的关键约束(代码注释中明确强调):更新后的任务类型和数量必须与更新前保持一致。也就是说,aclmdlRICaptureTaskUpdateBegin与aclmdlRICaptureTaskUpdateEnd之间下发的算子,必须在结构上与原任务组一一对应——本示例中都是单个aclnnAdd,只是标量参数从alpha换成了updateAlpha。这是"原位替换"语义决定的:Runtime 复用原任务组的任务槽位,只替换算子参数,因此不允许增减任务数量。
更新与执行之间的时序由 Event 协调:
aclrtRecordEvent(event, updateStream)在更新流上记录事件(写入ACL_EVENT_EXTERNAL事件);- 捕获阶段埋入 model 的
aclrtStreamWaitEvent(stream, event)等待该事件,因此只有当更新流的任务全部完成后,model 中的第二层算子才真正执行; - 两次
aclrtSynchronizeStream分别保证更新流与执行流排空,随后aclrtMemcpy把结果拷回 Host 打印。
4.4 收尾阶段
CHECK_ERROR(aclmdlRIDestroy(modelRI)); CHECK_ERROR(aclrtDestroyStream(stream)); CHECK_ERROR(aclrtDestroyStream(updateStream)); // ... aclDestroyTensor / aclDestroyScalar / aclrtFree 逐一释放 ... CHECK_ERROR(aclrtDestroyContext(context)); CHECK_ERROR(aclrtResetDeviceForce(deviceId)); CHECK_ERROR(aclFinalize());资源释放顺序为:先销毁模型实例(aclmdlRIDestroy,接口见 acl_rt.h),再销毁两条 Stream、销毁张量/标量、释放 Device 内存(含三个 workspace),最后销毁 Context 并aclrtResetDeviceForce强制复位计算设备、aclFinalize反初始化。注意文档的 API 清单中列举的是aclrtDestroyStreamForce(强制销毁 Stream),而示例代码在收尾阶段使用普通aclrtDestroyStream——两者在接口层面均可接受,Force变体适用于需要丢弃未完成任务、立即回收资源的场景。
五、接口总览:本示例用到的 CANN RUNTIME API
汇总 README_en.md 的接口清单,分类如下:
| 分类 | 接口 | 作用 |
|---|---|---|
| 初始化 | aclInit/aclFinalize | 初始化 / 反初始化 AscendCL 配置 |
| Device 管理 | aclrtSetDevice/aclrtResetDeviceForce | 指定计算 Device / 强制复位并回收 Device 资源 |
| Context 管理 | aclrtCreateContext/aclrtDestroyContext | 创建 / 销毁 Context |
| Stream 管理 | aclrtCreateStream/aclrtSynchronizeStream/aclrtDestroyStreamForce | 创建 Stream / 阻塞等待任务完成 / 强制销毁并丢弃任务 |
| 模型管理(捕获) | aclmdlRICaptureBegin/aclmdlRICaptureThreadExchangeMode | 开始捕获 / 切换模型捕获模式 |
| 模型管理(任务组) | aclmdlRICaptureTaskGrpBegin/aclmdlRICaptureTaskGrpEnd | 标记可更新任务组开始 / 结束,并获得更新句柄 |
| 模型管理(执行) | aclmdlRICaptureEnd/aclmdlRIExecuteAsync/aclmdlRIDestroy | 结束捕获得到 modelRI / 异步执行推理 / 销毁实例 |
| 模型管理(更新) | aclmdlRICaptureTaskUpdateBegin/aclmdlRICaptureTaskUpdateEnd | 标记待更新任务开始 / 结束 |
| 内存管理 | aclrtMalloc/aclrtFree | 分配 / 释放 Device 内存 |
| 数据传输 | aclrtMemcpy/aclrtMemcpyAsync | 同步 / 异步内存拷贝 |
其中任务组与更新四件套的接口原型均可直接在 include/external/acl/acl_rt.h 中查到:
aclmdlRICaptureTaskGrpBegin(aclrtStream stream)(L4865):任务组起始标记;aclmdlRICaptureTaskGrpEnd(aclrtStream stream, aclrtTaskGrp* handle)(L4875):任务组结束标记并输出句柄;aclmdlRICaptureTaskUpdateBegin(aclrtStream stream, aclrtTaskGrp handle)(L4885):按句柄开始更新;aclmdlRICaptureTaskUpdateEnd(aclrtStream stream)(L4894):结束更新。
六、关键实现细节与易错点
任务数量与类型必须一致:
UpdateBegin与UpdateEnd之间下发的任务需与原任务组一一对应。若尝试在更新时插入或删除算子,Runtime 无法完成原位替换,行为未定义。要改变计算结构,应重新捕获整个模型。更新执行器需提前准备:示例在捕获阶段之前就通过
aclnnAddGetWorkspaceSize创建了updateAddExecutor并预分配了 workspace,更新阶段只做"下发执行器"这一件事,避免了在更新路径中引入高开销的算子编译/内存申请。Event 同步不可省略:
ACL_EVENT_EXTERNAL事件配合aclrtStreamWaitEvent+aclrtRecordEvent构成了"更新先于执行"的 happens-before 关系。去掉该同步,模型可能使用旧参数执行,输出将与预期不符。捕获模式选择:RELAXED 模式允许捕获带同步原语的任务序列,是实现可更新任务组的前提;GLOBAL/THREAD_LOCAL 模式的捕获约束更严格,适用于简单纯任务序列(可参考 0_simple_model 的场景)。
七、总结
1_model_update示例完整展示了 CANN Runtime 模型运行时实例的"捕获 → 分段标记 → 热更新 → 再执行"能力:以两个aclnnAdd组成的复合模型为例,在第二轮执行前仅通过切换算子的alpha标量就改变了整体计算结果(输出向量整体平移 +2.2)。这一机制让"固定计算骨架 + 动态参数"的推理场景无需反复重建模型实例,是 Model RI 高性能复用能力的重要补充。如需在自有业务中复用,可直接以 main.cpp 为模板,重点把握任务组数量一致性约束与 Event 同步时序两个核心要点。
【免费下载链接】runtime本项目提供CANN运行时组件和维测功能组件。项目地址: https://gitcode.com/cann/runtime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考