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; }关键设计有三点:
- 运行时检查依赖 API 返回值:不支持 UVM 的 SOC 上,
aclrtMemAllocManaged返回ACL_ERROR_RT_FEATURE_NOT_SUPPORT(错误码 207000),样例据此打印[SKIP]提示; - 跳过不算失败:内部用
kSampleSkipped = 1与真正的错误(-1)区分,main最终返回 0 正常退出; - 构建脚本联动:run.sh 用
grep -q '\[SKIP\]'检测日志,识别到[SKIP]后跳过后续 md5 比对与 Python 校验,同样以 0 退出码结束。
3. 样例目录结构与数据流
样例目录下各文件职责如下:
| 文件 | 职责 |
|---|---|
| main.cpp | Host 端主程序:初始化、UVM 内存申请、二进制加载、Kernel 下发、结果落盘 |
| CMakeLists.txt | 编译配置:构建 fatbin 核函数库与 Host 可执行文件 |
| run.sh | 一键构建、生成数据、运行、校验脚本 |
| scripts/gen_data.py | 用 numpy 生成输入数据与期望结果(golden) |
| scripts/verify_result.py | 按容差比对 Kernel 输出与 golden 结果 |
数据流为:
gen_data.py生成input/input_x.bin、input/input_y.bin和output/golden.bin(期望结果);main.cpp将两份输入读入 UVM 内存,Kernel 在 Device 上计算z = x + y并写回 UVM 内存,Host 侧直接把zPtr内容落盘为output/output_z.bin(此处无需 D2H 拷贝,这正是 UVM 的核心收益);verify_result.py比对output_z.bin与golden.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 × 2048的float16张量(与 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结构体记录每个资源是否已创建(aclInitialized、deviceSet、streamCreated、binLoaded),保证后续清理阶段能按反向顺序精确释放,避免对未创建资源误调用释放接口。
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_GLOBAL(0x01U),表示申请的内存可在 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), ¶mHandle1)); CHECK_ERROR(aclrtKernelArgsAppend(argsHandle, reinterpret_cast<void**>(&yPtr), sizeof(uintptr_t), ¶mHandle2)); CHECK_ERROR(aclrtKernelArgsAppend(argsHandle, reinterpret_cast<void**>(&zPtr), sizeof(uintptr_t), ¶mHandle3)); CHECK_ERROR(aclrtKernelArgsFinalize(*argsHandle));这里体现的是 Kernel 加载与执行的标准接口序列:aclrtBinaryLoadFromFile→aclrtBinaryGetFunction→aclrtKernelArgsInit→aclrtKernelArgsAppend→aclrtKernelArgsFinalize。由于传入的 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)) { ... }aclrtLaunchKernelWithConfig以blockDim = 8的 AICore 块配置在指定 Stream 上下发 Kernel,dimGrid与hostArgs传nullptr(本算子无共享内存与附加 host 参数);aclrtSynchronizeStream阻塞等待任务执行完成,确保 Device 计算结束后才读zPtr;- 随后 Host 直接将
zPtr写盘——数据已由 UVM 机制保证对 Host 可见,全程没有出现一次显式拷贝 API。
4.5 资源释放的逆序清理
ReleaseKernelResources 按创建的反向顺序清理:aclrtBinaryUnLoad→aclrtFree(依次释放 z、y、x)→aclrtDestroyStreamForce→aclrtResetDeviceForce→aclFinalize。每一步的返回值都会经UpdateFinalResultOnError检查,任一失败都会把最终结果置为-1。注意 UVM 内存统一使用aclrtFree释放,这与接口文档"需调用 aclrtFree 接口"的约束一致。
4.6 结果校验的容差设计
verify_result.py 针对 float16 精度设定了三组阈值:
| 常量 | 取值 | 用途 |
|---|---|---|
RELATIVE_TOL | 1e-3 | np.isclose的相对容差 |
ABSOLUTE_TOL | 1e-5 | np.isclose的绝对容差 |
ERROR_TOL | 1e-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)依次完成:
环境自检:通过 common/resolve_cann_env.sh 解析 CANN 安装路径,并尝试 source set_sample_env.sh 自动识别
SOC_VERSION与ASCENDC_CMAKE_DIR;若二者缺失或${ASCENDC_CMAKE_DIR}/ascendc.cmake不存在则报错退出;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等编译选项;数据生成:
rm -rf input output && python3 scripts/gen_data.py;运行样例:将
out/lib、out/lib64与 CANNlib64加入LD_LIBRARY_PATH后执行out/bin/ascendc_kernels_bbit simple,日志同步写入output_msg.txt;结果校验:
md5sum output/*.bin并用verify_result.py比对output_z.bin与golden.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 编程模式:aclrtMemAllocManaged(ACL_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),仅供参考