CANN Runtime 统一虚拟内存(UVM)样例实战:用 aclrtMemAllocManaged 免去显式数据搬运
2026/9/19 13:10:25 网站建设 项目流程

CANN Runtime 统一虚拟内存(UVM)样例实战:用 aclrtMemAllocManaged 免去显式数据搬运

【免费下载链接】runtime本项目提供CANN运行时组件和维测功能组件。项目地址: https://gitcode.com/cann/runtime

本篇基于 CANN Runtime 仓库中的 UVM 内存申请样例(0_uvm_allocate)展开,讲解如何利用统一虚拟内存(Unified Virtual Memory)机制为算子输入、输出分配 Host/Device 均可访问的内存,从而消除参数追加与结果回写过程中的显式拷贝。读完后你将掌握aclrtMemAllocManaged的参数约束与页表映射原理、样例的完整调用链、编译运行步骤,以及在不支持 UVM 的 SOC 上的优雅降级方案。

1. UVM 机制解决什么问题

在传统 Device 内存编程模型中,Host 与 Device 各自拥有独立的物理内存:算子输入需要通过aclrtMemcpy显式地从 Host 拷入 Device,算子结束后再显式拷回。UVM 统一虚拟内存机制则让 Host 与 Device 共享同一套虚拟地址空间,同一块指针既可以在 Host 上读写,也可以直接作为 Kernel 参数在 Device 上使用。

结合 统一寻址 API 参考文档 的说明,UVM 内存的底层行为是:

  • 按需建立页表映射:通过aclrtMemAllocManaged申请的内存,仅在实际访问时才建立虚拟内存到物理内存的页表映射。访问发生在 Host 上时映射 Host 的物理内存;访问发生在 Device 上时映射 Device 的物理内存。
  • 缺页中断触发迁移:当访问对象发生变化(例如从 Host 变为 Device),新访问对象上会触发缺页中断,内存数据会被迁移到新的访问对象上并重建页表映射,同时前一个访问对象上的页表映射失效、物理内存释放。
  • 频繁迁移影响性能:如果频繁更换访问对象,会频繁触发缺页中断和数据迁移。为降低这一开销,Runtime 提供了 aclrtMemManagedAdvise 等策略接口(本样例未涉及)。

这正是样例的价值所在:在"Host 写入输入 → Device 执行计算"这类单向数据流中,UVM 内存可以让同一份指针贯穿全流程,无需手工管理 H2D/D2H 拷贝。

2. 产品支持情况与优雅降级

样例 README 声明的产品支持情况如下:

产品是否支持
Ascend 950PR/Ascend 950DT×
Atlas A3 训练系列产品/Atlas A3 推理系列产品
Atlas A2 训练系列产品/Atlas A2 推理系列产品

这一支持与 aclrtMemAllocManaged 接口文档 的产品支持列表一致(Atlas 200I/500 A2、Atlas 推理系列、Atlas 训练系列、IPV350 均不支持)。

针对不支持 UVM 的产品(如 Ascend 910 系列),样例实现了"检测-跳过-正常退出"的降级逻辑。从 main.cpp 的AllocateKernelBuffers实现看:

const aclError allocRet = aclrtMemAllocManaged(reinterpret_cast<void**>(&buffers->xPtr), inputByteSize, ACL_RT_MEM_ATTACH_GLOBAL); if (allocRet == ACL_ERROR_RT_FEATURE_NOT_SUPPORT) { const char* socName = aclrtGetSocName(); WARN_LOG( "[SKIP] uvm_allocate sample skipped: the current SOC (%s) does not support UVM, " "aclrtMemAllocManaged returned error code %d.", (socName != nullptr) ? socName : "unknown", static_cast<int32_t>(allocRet)); return kSampleSkipped; }

关键设计有三点:

  1. 运行时检查依赖 API 返回值:不支持 UVM 的 SOC 上,aclrtMemAllocManaged返回ACL_ERROR_RT_FEATURE_NOT_SUPPORT(错误码 207000),样例据此打印[SKIP]提示;
  2. 跳过不算失败:内部用kSampleSkipped = 1与真正的错误(-1)区分,main最终返回 0 正常退出;
  3. 构建脚本联动:run.sh 用grep -q '\[SKIP\]'检测日志,识别到[SKIP]后跳过后续 md5 比对与 Python 校验,同样以 0 退出码结束。

3. 样例目录结构与数据流

样例目录下各文件职责如下:

文件职责
main.cppHost 端主程序:初始化、UVM 内存申请、二进制加载、Kernel 下发、结果落盘
CMakeLists.txt编译配置:构建 fatbin 核函数库与 Host 可执行文件
run.sh一键构建、生成数据、运行、校验脚本
scripts/gen_data.py用 numpy 生成输入数据与期望结果(golden)
scripts/verify_result.py按容差比对 Kernel 输出与 golden 结果

数据流为:

  1. gen_data.py生成input/input_x.bininput/input_y.binoutput/golden.bin(期望结果);
  2. main.cpp将两份输入读入 UVM 内存,Kernel 在 Device 上计算z = x + y并写回 UVM 内存,Host 侧直接把zPtr内容落盘为output/output_z.bin(此处无需 D2H 拷贝,这正是 UVM 的核心收益);
  3. verify_result.py比对output_z.bingolden.bin

数据规模由 main.cpp 定义:

const uint32_t blockDim = 8; const size_t inputByteSize = 8 * 2048 * sizeof(uint16_t); // 32768 字节 const size_t outputByteSize = 8 * 2048 * sizeof(uint16_t); // 32768 字节

即输入/输出均为8 × 2048float16张量(与 gen_data.py 中np.random.uniform(1, 100, [8, 2048]).astype(np.float16)一一对应),blockDim为 8 个 AICore 块。

4. 核心流程逐段解析

4.1 运行时初始化与 Stream 创建

InitializeRuntime 完成三步标准初始化:

CHECK_ERROR(aclInit(nullptr)); // 初始化 ACL CHECK_ERROR(aclrtSetDevice(runtime->deviceId)); // 指定 Device 0 CHECK_ERROR(aclrtCreateStream(&runtime->stream)); // 创建 Stream

代码用RuntimeResources结构体记录每个资源是否已创建(aclInitializeddeviceSetstreamCreatedbinLoaded),保证后续清理阶段能按反向顺序精确释放,避免对未创建资源误调用释放接口。

4.2 UVM 内存申请:aclrtMemAllocManaged

对 x、y、z 三个缓冲区各调用一次 UVM 申请(见 main.cpp):

aclrtMemAllocManaged(reinterpret_cast<void**>(&buffers->xPtr), inputByteSize, ACL_RT_MEM_ATTACH_GLOBAL);

结合 接口头文件声明 与 API 参考文档,该接口的关键约束如下:

参数说明
ptr输出参数,"已分配内存指针"的指针。由于 Host 与 Device 虚拟地址统一编址,该参数不区分申请位置
size内存大小(Byte),不能为 0;申请大小会按 2MB 向上对齐;单个应用进程最大可申请 3TB UVM 虚拟内存
flag当前仅支持ACL_RT_MEM_ATTACH_GLOBAL0x01U),表示申请的内存可在 Device 和 Host 侧都被访问
释放必须通过aclrtFree接口释放(头文件注释明确要求)
初始化分配内容不做初始化,需要时用aclrtMemset清零(本样例随后立即用输入文件覆盖,故无需)

由于 UVM 指针 Host/Device 双端可访问,样例把./input/input_x.bin./input/input_y.bin直接读入xPtr/yPtr(PrepareInputData),无需任何aclrtMemcpy

4.3 二进制加载与参数组装

样例的 Kernel 是add_custom算子,由 CMakeLists.txt 编译为 fatbin:

file(GLOB KERNEL_FILES_SIMPLE ../../../kernel_func/add_custom.cpp) include(${ASCENDC_CMAKE_DIR}/ascendc.cmake) ascendc_fatbin_library(ascendc_kernels_simple ${KERNEL_FILES_SIMPLE})

核函数源码位于 example/kernel_func/add_custom.cpp。Host 端通过 BuildKernelArgs 完成加载与参数组装:

CHECK_ERROR(aclrtBinaryLoadFromFile("./out/fatbin/ascendc_kernels_simple/ascendc_kernels_simple.o", nullptr, &runtime->binHandle)); CHECK_ERROR(aclrtBinaryGetFunction(runtime->binHandle, "add_custom", funcHandle)); CHECK_ERROR(aclrtKernelArgsInit(*funcHandle, argsHandle)); // 追加三个指针参数(按 uintptr_t 长度传入) CHECK_ERROR(aclrtKernelArgsAppend(argsHandle, reinterpret_cast<void**>(&xPtr), sizeof(uintptr_t), &paramHandle1)); CHECK_ERROR(aclrtKernelArgsAppend(argsHandle, reinterpret_cast<void**>(&yPtr), sizeof(uintptr_t), &paramHandle2)); CHECK_ERROR(aclrtKernelArgsAppend(argsHandle, reinterpret_cast<void**>(&zPtr), sizeof(uintptr_t), &paramHandle3)); CHECK_ERROR(aclrtKernelArgsFinalize(*argsHandle));

这里体现的是 Kernel 加载与执行的标准接口序列:aclrtBinaryLoadFromFileaclrtBinaryGetFunctionaclrtKernelArgsInitaclrtKernelArgsAppendaclrtKernelArgsFinalize。由于传入的 x/y/z 均为 UVM 指针,参数中携带的地址对 Device 天然可见,不需要再提供一套 Device 侧地址。

4.4 Kernel 下发、同步与结果落盘

LaunchKernelAndWriteOutput 完成最后三步:

CHECK_ERROR(aclrtLaunchKernelWithConfig(funcHandle, blockDim, stream, nullptr, argsHandle, nullptr)); CHECK_ERROR(aclrtSynchronizeStream(stream)); if (!kernel::WriteFile("./output/output_z.bin", zPtr, outputByteSize)) { ... }
  • aclrtLaunchKernelWithConfigblockDim = 8的 AICore 块配置在指定 Stream 上下发 Kernel,dimGridhostArgsnullptr(本算子无共享内存与附加 host 参数);
  • aclrtSynchronizeStream阻塞等待任务执行完成,确保 Device 计算结束后才读zPtr
  • 随后 Host 直接将zPtr写盘——数据已由 UVM 机制保证对 Host 可见,全程没有出现一次显式拷贝 API。

4.5 资源释放的逆序清理

ReleaseKernelResources 按创建的反向顺序清理:aclrtBinaryUnLoadaclrtFree(依次释放 z、y、x)→aclrtDestroyStreamForceaclrtResetDeviceForceaclFinalize。每一步的返回值都会经UpdateFinalResultOnError检查,任一失败都会把最终结果置为-1。注意 UVM 内存统一使用aclrtFree释放,这与接口文档"需调用 aclrtFree 接口"的约束一致。

4.6 结果校验的容差设计

verify_result.py 针对 float16 精度设定了三组阈值:

常量取值用途
RELATIVE_TOL1e-3np.isclose的相对容差
ABSOLUTE_TOL1e-5np.isclose的绝对容差
ERROR_TOL1e-3允许的不一致元素占比上限(error ratio)

校验逻辑将两个.bin文件按float16平铺后逐元素比较,最多打印前 100 个不一致元素的索引与相对误差,最终以error ratio <= ERROR_TOL判定通过并输出[SUCCESS] result correct

5. 编译与运行

5.1 环境准备

按 README 说明,需先切换至样例目录并加载环境:

cd ${git_clone_path}/example/3_memory_advanced/managed_memory/0_uvm_allocate # ${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

数据生成与结果校验依赖numpy(版本 >= 1.19.0),运行run.sh前请确保 Python 环境已安装;run.sh 中也内置了python3 -c "import numpy"的检查,缺失时直接报错退出。

5.2 run.sh 做了什么

执行bash run.sh后,脚本(run.sh)依次完成:

  1. 环境自检:通过 common/resolve_cann_env.sh 解析 CANN 安装路径,并尝试 source set_sample_env.sh 自动识别SOC_VERSIONASCENDC_CMAKE_DIR;若二者缺失或${ASCENDC_CMAKE_DIR}/ascendc.cmake不存在则报错退出;

  2. CMake 构建:以Debug模式配置,关键参数为

    cmake -B build \ -DSOC_VERSION="${SOC_VERSION}" \ -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_INSTALL_PREFIX="${SCRIPT_DIR}/out" \ -DASCEND_CANN_PACKAGE_PATH="${ASCEND_INSTALL_PATH}" cmake --build build -j"$(nproc)" cmake --install build

    构建产物包括 fatbin 库out/fatbin/ascendc_kernels_simple/ascendc_kernels_simple.o和可执行文件out/bin/ascendc_kernels_bbit。CMake 中还固定了链接 libacl_rt.so 与-O2 -std=c++17等编译选项;

  3. 数据生成rm -rf input output && python3 scripts/gen_data.py

  4. 运行样例:将out/libout/lib64与 CANNlib64加入LD_LIBRARY_PATH后执行out/bin/ascendc_kernels_bbit simple,日志同步写入output_msg.txt

  5. 结果校验md5sum output/*.bin并用verify_result.py比对output_z.bingolden.bin

6. 示例输出

在支持 UVM 的产品上,正常运行的输出为:

Configuring CMake... Building... ... [INFO] Run the uvm_allocate sample successfully. ... output/output_z.bin ... output/golden.bin error ratio: 0.0000, tolerance: 0.0010 [SUCCESS] result correct

在不支持 UVM 的产品(如 Ascend 910 系列)上,输出为:

[WARN] [SKIP] uvm_allocate sample skipped: the current SOC (Ascend910A) does not support UVM, aclrtMemAllocManaged returned error code 207000. [SUCCESS] uvm_allocate sample skipped because the current SOC does not support UVM.

[SKIP]路径下run.sh会直接打印跳过成功信息并结束,不会执行结果比对。

7. 小结与延伸

本样例用一个最小闭环展示了 CANN Runtime 的 UVM 编程模式:aclrtMemAllocManagedACL_RT_MEM_ATTACH_GLOBAL)申请 Host/Device 共享指针 → 数据直接在统一地址空间上准备 → 指针原样作为 Kernel 参数 → 同步后 Host 直接读取结果。相比传统"malloc + memcpy + launch + memcpy"流程,它把显式拷贝环节全部消除,同时保留了标准的 Kernel 加载/执行/同步/清理接口序列。

需要留意的是:UVM 的自动迁移依赖缺页中断,频繁在 Host/Device 之间往返访问同一块内存会带来迁移开销;对迁移模式可预知的负载,可进一步结合 aclrtMemManagedAdvise、aclrtMemManagedPrefetchAsync 等策略接口优化。该样例的aclrtMemAllocManaged仅支持 Atlas A3 训练/推理系列与 Atlas A2 训练/推理系列产品,使用前请先确认目标 SOC 的支持情况,或参考 UVM 内存管理 API 文档 与 托管内存章节 获取策略设置、属性查询、批量预取等进阶接口的完整说明。

【免费下载链接】runtime本项目提供CANN运行时组件和维测功能组件。项目地址: https://gitcode.com/cann/runtime

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询