十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CANN Runtime 多 Stream 内存语义同步实战:aclrtValueWait 与 aclrtValueWrite 详解

CANN Runtime 多 Stream 内存语义同步实战:aclrtValueWait 与 aclrtValueWrite 详解 CANN Runtime 多 Stream 内存语义同步实战aclrtValueWait 与 aclrtValueWrite 详解【免费下载链接】runtime本项目提供CANN运行时组件和维测功能组件。项目地址: https://gitcode.com/cann/runtime导读本文以 CANN Runtime 仓库中的官方样例 9_multistream_sync_memory 为主线系统讲解如何在两个独立 StreamStream A 与 Stream B之间通过 Device 内存上的**值同步Value Wait/Write**机制实现跨 Stream 的内存语义同步。读者学完后将掌握aclrtValueWait、aclrtValueWrite两个核心接口的语义、等待模式 flag 的取值、与 Event/Notify 同步机制的差异以及如何搭建双线程 双 Stream 的同步场景并完成编译、运行与结果自校验。一、场景背景为什么需要“内存语义同步”在异构计算中多个 Stream 上的任务天然是异步并发的。当 Stream A 上的任务需要等待 Stream B 上的任务写入某个数据后才能继续执行时开发者通常有 Event、Notify 等同步手段可选。但这两类同步机制有一个共同限制同步双方是流上的任务/主机侧逻辑算子本身很难直接作为同步参与方。CANN Runtime 提供了基于通用 Device 内存的内存语义同步机制来补齐这一缺口。正如官方开发指南 03-07 内存语义同步 所述该机制允许用户基于通用 Device 内存实现同步并且支持算子作为同步参与方——即算子可以在执行过程中与另一条流进行同步这是 Event/Notify 机制不具备的能力。而本文要讲的样例9_multistream_sync_memory正是用纯 Host 侧 APIaclrtValueWait/aclrtValueWrite演示了这一机制的最小可运行版本一条流上的等待任务持续阻塞直到另一条流上的写任务把指定内存的值写入目标值。二、样例描述与产品支持2.1 样例行为按照 README.md 的描述本样例会触发两个线程线程 A等待方等待指定内存中的数据满足一定条件后解除阻塞线程 B写入方向指定内存中写入数据。在线程 B 写入满足条件的数据之前线程 A 将持续阻塞。这正好模拟了流水线/生产者-消费者模型中“依赖前序数据就绪”的典型场景。2.2 产品支持情况产品是否支持Ascend 950PR / Ascend 950DT√Atlas A3 训练系列产品 / Atlas A3 推理系列产品√Atlas A2 训练系列产品 / Atlas A2 推理系列产品√三、核心原理基于内存的值等待与值写入3.1 接口声明与语义两个核心接口的声明位于头文件 include/external/acl/acl_rt.h/** * brief mem write value * param [in] devAddr dev addr * param [in] value write value * param [in] flag reserved, must be 0 * param [in] stream asynchronized task stream */ aclError aclrtValueWrite(void* devAddr, uint64_t value, uint32_t flag, aclrtStream stream); /** * brief mem wait value * param [in] devAddr dev addr * param [in] value expect value * param [in] flag wait mode * param [in] stream asynchronized task stream */ aclError aclrtValueWait(void* devAddr, uint64_t value, uint32_t flag, aclrtStream stream);两个接口均为异步下发它们向指定 Stream 中下发一个 wait 任务或 write 任务任务在 Device 侧按 Stream 顺序执行。aclrtValueWrite向devAddr指向的 Device 内存写入value。其flag为保留参数必须传 0。aclrtValueWait阻塞等待直到devAddr指向的 Device 内存中的值满足flag指定的条件。3.2 等待模式 flag 详解等待模式由头文件 acl_rt.h 中的宏定义宏值语义ACL_STREAM_WAIT_VALUE_GEQ0x00000000U等待内存值≥期望值Greater-or-EqualACL_STREAM_WAIT_VALUE_EQ0x00000001U等待内存值期望值EqualACL_STREAM_WAIT_VALUE_AND0x00000002U等待内存值按位与期望值后结果非 0按位 AND 命中ACL_STREAM_WAIT_VALUE_NOR0x00000003U等待内存值按位或期望值后结果为 0NOR 命中本样例使用的是ACL_STREAM_WAIT_VALUE_EQ即等待内存值恰好等于目标值valueCompare 100。3.3 与 Event/Notify 机制的关键差异从 03-07 内存语义同步 可知内存语义同步机制有两大特性值得关注算子可以作为同步参与方Device 侧算子核函数可以通过读/写同一块 Device 内存参与同步。例如开发指南中展示的 Device 侧示例——myKernel1向syncMem写 1 并通过dcci指令刷新缓存myKernel2则用volatile指针配合dcci轮询阻塞直到内存值变为 2。这与 Host 侧样例中的aclrtValueWait/aclrtValueWrite可互相配合使用。同步基于通用 Device 内存由于没有额外的硬件同步对象同步用的内存可通过aclrtMemset/aclrtMemsetAsync初始化和清除使用上非常灵活。四、编译与运行4.1 前置条件已安装 CANN 软件包默认安装根目录为/usr/local/Ascend主机环境为 Linuxx86_64 或 aarch64并具备上述支持列表中的昇腾产品已下载本仓库源码。4.2 编译运行步骤按照 README.md 的步骤第 1 步切换到样例目录cd ${git_clone_path}/example/1_basic_features/memory/9_multistream_sync_memory其中${git_clone_path}为本仓库克隆到本地的根目录。第 2 步设置环境变量# ${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其中 set_sample_env.sh 是样例公共脚本它根据当前机器架构x86_64/aarch64和已安装的 CANN 软件自动探测并导出SOC_VERSION昇腾 AI 处理器型号如 Ascend910_9362、Ascend910B2 等与ASCENDC_CMAKE_DIRAscendC 编译器ascendc.cmake所在路径。第 3 步运行样例bash run.shrun.sh 脚本内部完成“构建 → 安装 → 运行 → 校验”的完整闭环set -euo pipefail _ASCEND_CANN_PATH${ASCEND_HOME_PATH:-} # ... 检查 ASCEND_HOME_PATH 是否已设置 source ${_ASCEND_CANN_PATH}/bin/setenv.bash echo [INFO]: Current compile soc version is ${SOC_VERSION} rm -rf build mkdir -p build cmake -B build -DASCEND_CANN_PACKAGE_PATH${_ASCEND_CANN_PATH} cmake --build build -j cmake --install build构建使用的 CMakeLists.txt 要点如下通过include(${ASCENDC_CMAKE_DIR}/ascendc.cmake)引入 AscendC 构建规则include_directories(${ASCEND_CANN_PACKAGE_PATH}/include)引入 CANN 头文件acl/acl.h等link_directories(${ASCEND_CANN_PACKAGE_PATH}/lib64)定位运行库target_link_libraries(main PRIVATE ascendcl Threads::Threads)链接 AscendCL 运行时库与系统线程库样例依赖std::thread。五、源码逐段剖析样例主程序为 main.cpp下面按执行流程拆解。5.1 主线程初始化与资源准备aclInit(nullptr); int32_t deviceId 0; aclrtSetDevice(deviceId); uint64_t size 1 * 1024 * 1024; void* devPtrA; CHECK_ERROR(aclrtMalloc(devPtrA, size, ACL_MEM_MALLOC_HUGE_FIRST)); INFO_LOG(Allocate memory on the device successfully); aclrtStream streamA nullptr; CHECK_ERROR(aclrtCreateStream(streamA)); aclrtStream streamB nullptr; CHECK_ERROR(aclrtCreateStream(streamB));关键点aclInit(nullptr)使用默认配置完成 Runtime 初始化对应aclFinalize()做去初始化aclrtSetDevice(deviceId)指定 Device 0 作为运算设备aclrtMalloc申请1 MiBDevice 内存分配策略为ACL_MEM_MALLOC_HUGE_FIRST优先申请大页内存降低 TLB miss这块内存就是后续用于跨 Stream 同步的“信令内存”创建两条独立 StreamstreamA等待方与streamB写入方。5.2 双线程编排等待方与写入方const char* filePath file/flag.txt; uint64_t valueCompare 100; constexpr uint32_t waitTime 1000000; std::thread threadA(ThreadWait, streamA, deviceId, devPtrA, valueCompare, filePath); (void)usleep(waitTime); uint64_t valueWrite 100; std::thread threadB(ThreadWrite, streamB, deviceId, devPtrA, valueWrite, filePath); threadB.join(); threadA.join();先启动等待线程threadA随后主线程usleep(1000000)1 秒确保线程 A 中的 wait 任务已下发到 Stream A 并处于阻塞状态再启动写入线程threadB向devPtrA写入目标值 100与valueCompare一致配合ACL_STREAM_WAIT_VALUE_EQ即可解除等待两个线程join后主线程依次执行aclrtDestroyStreamForce、aclrtFree、aclrtResetDeviceForce、aclFinalize完成资源回收。5.3 等待线程 ThreadWaitint ThreadWait(aclrtStream stream, int32_t deviceId, void* devPtr, uint64_t valueCompare, const char* filePath) { aclrtSetDevice(deviceId); CHECK_ERROR(aclrtValueWait(devPtr, valueCompare, ACL_STREAM_WAIT_VALUE_EQ, stream)); INFO_LOG(Stream A: wait for data at virtual memory %p to meet the condition, devPtr); CHECK_ERROR(aclrtSynchronizeStream(stream)); INFO_LOG(Stream A: the data in the specified memory has met the condition, all tasks are complete); int32_t waitFlag 0; memory::ReadFileEx(filePath, waitFlag, sizeof(waitFlag)); INFO_LOG(Flag value read by the waiting thread: %d, waitFlag); return 0; }线程在aclrtSetDevice后立即通过aclrtValueWait(devPtr, 100, ACL_STREAM_WAIT_VALUE_EQ, streamA)向 Stream A 下发等待任务。由于此时devPtrA中的值尚未被写入该任务在 Device 侧持续阻塞线程 A 不会返回aclrtSynchronizeStream阻塞等待 Stream A 上所有任务完成——只有 wait 条件满足、等待任务结束后才会继续解除阻塞后线程 A 通过memory::ReadFileEx读取写入线程写下的标志文件file/flag.txt用于验证等待期间确实发生了阻塞若未阻塞读到的是初始值 0 而非 123。5.4 写入线程 ThreadWriteint ThreadWrite(aclrtStream stream, int32_t deviceId, void* devPtr, uint64_t valueWrite, const char* filePath) { int32_t writeFlag 123; memory::WriteFileEx(filePath, writeFlag, sizeof(writeFlag)); INFO_LOG(Flag value after the writing thread starts: %d, writeFlag); aclrtSetDevice(deviceId); CHECK_ERROR(aclrtValueWrite(devPtr, valueWrite, 0, stream)); INFO_LOG(Stream B: write data at virtual memory %p, devPtr); CHECK_ERROR(aclrtSynchronizeStream(stream)); INFO_LOG(Stream B: the data in the specified memory has met the condition, all tasks are complete); return 0; }写入线程先写标志文件写入整数 123再下发写任务。这样做的意图是等待线程解除阻塞后读取该文件只有读到 123 才能证明它确实是在写入线程开始之后、写任务完成之后才继续执行的aclrtValueWrite(devPtr, 100, 0, streamB)flag传 0符合“保留参数必须为 0”的约束向devPtrA写入值 100从而解除 Stream A 上 wait 任务的阻塞aclrtSynchronizeStream(streamB)确保写任务真正完成。5.5 标志文件读写工具样例通过文件系统辅助验证阻塞行为其实现位于 file_ops.cppWriteFileEx以O_WRONLY | O_CREAT | O_TRUNC打开文件并写入指定大小数据写入字节数与请求不符时记录Partial write错误ReadFileEx以O_RDONLY打开并读取数据同样对部分读做错误处理。这是纯 Host 侧的“实验检测手段”与 Device 内存同步本身无关但为验证“等待线程确实被阻塞”提供了可观测证据。5.6 错误处理宏样例复用了仓库公共工具 utils.h 中的CHECK_ERROR宏任何 AscendCL 接口返回非ACL_SUCCESS时打印错误码并立即返回 -1保证失败路径可见、可定位。六、涉及的关键 CANN RUNTIME API以下是本样例涉及的关键功能点及接口清单与 README.md 一致并补充说明功能类别接口作用初始化aclInit初始化配置此处传nullptr使用默认配置初始化aclFinalize去初始化释放 Runtime 全局资源Device 管理aclrtSetDevice指定用于运算的 DeviceDevice 管理aclrtResetDeviceForce强制复位当前 Device回收 Device 上的资源Stream 管理aclrtCreateStream创建 StreamStream 管理aclrtSynchronizeStream阻塞等待 Stream 上任务全部完成Stream 管理aclrtDestroyStreamForce强制销毁 Stream丢弃所有任务内存管理aclrtValueWait等待指定内存数据满足条件后解除阻塞内存管理aclrtValueWrite向指定内存写入数据内存管理aclrtMalloc申请 Device 内存内存管理aclrtFree释放 Device 内存数据传输aclrtMemcpy通过内存复制实现数据传输样例用于数据搬运类场景的基础接口说明aclrtMemcpy在样例中作为数据传输基础接口被列出本样例的同步核心是aclrtValueWait/aclrtValueWrite与aclrtSynchronizeStream的组合。七、运行结果与自动校验7.1 示例输出运行成功后的典型输出来自 README.md[INFO] Allocate memory on the device successfully [INFO] Create Stream A successfully [INFO] Create Stream B successfully [INFO] Stream A: wait for data at virtual memory 0x... to meet the condition [INFO] Start writing to file/flag.txt [INFO] Flag value after the writing thread starts: 123 [INFO] Stream B: write data at virtual memory 0x... [INFO] Stream B: the data in the specified memory has met the condition, all tasks are complete [INFO] Stream A: the data in the specified memory has met the condition, all tasks are complete [INFO] Flag value read by the waiting thread: 123注意输出顺序的语义Stream A: wait ...先于Stream B: write ...而Stream A: the data ... all tasks are complete出现在Stream B完成之后——这正是“等待线程阻塞直到写入方满足条件”的直接证据。7.2 脚本自动校验逻辑run.sh 在运行后会对输出做断言wait_value$(awk -F: /Flag value read by the waiting thread:/ {gsub(/^ | $/, , $2); print $2; exit} ${file_path}) write_value_after$(awk -F: /Flag value after the writing thread starts:/ {gsub(/^ | $/, , $2); print $2; exit} ${file_path}) if [[ -n ${wait_value} ${wait_value} ${write_value_after} ]]; then echo [SUCCESS] Memory semantics synchronization across multiple streams is successful else echo [FAILURE] Memory semantics synchronization across multiple streams failed exit 1 fi校验思路等待线程读到的标志值Flag value read by the waiting thread必须等于写入线程写入的标志值Flag value after the writing thread starts即 123。若相等说明等待线程确实被阻塞到写入方完成之后才读取文件跨 Stream 内存语义同步生效否则脚本以非零退出码报[FAILURE]。通过解析日志文本并比对两个关键数值使样例具备可自动回归验证的能力。八、源码与测试层面的佐证8.1 单元测试对接口行为的约束仓库单元测试 tests/ut/acl/testcase/acl_runtime_unittest.cpp 覆盖了这两个接口的入参与转调行为TEST_F(UTEST_ACL_Runtime, aclrtValueWrite_failed_with_invalid_args) { auto ret aclrtValueWrite(nullptr, 100, 0, nullptr); EXPECT_EQ(ret, ACL_ERROR_INVALID_PARAM); // devAddr 为空 → 参数错误 // ... mock rtsValueWrite 返回 ACL_ERROR_RT_PARAM_INVALID 时透传 } TEST_F(UTEST_ACL_Runtime, aclrtValueWrite_success) { auto devAddr reinterpret_castvoid*(0x1000U); const auto ret aclrtValueWrite(devAddr, 100, 0, nullptr); EXPECT_EQ(ret, ACL_SUCCESS); } TEST_F(UTEST_ACL_Runtime, aclrtValueWait_failed_with_invalid_args) { auto ret aclrtValueWait(nullptr, 100, ACL_STREAM_WAIT_VALUE_GEQ, nullptr); EXPECT_EQ(ret, ACL_ERROR_INVALID_PARAM); // ... } TEST_F(UTEST_ACL_Runtime, aclrtValueWait_success) { auto devAddr reinterpret_castvoid*(0x1000U); const auto ret aclrtValueWait(devAddr, 100, ACL_STREAM_WAIT_VALUE_GEQ, nullptr); EXPECT_EQ(ret, ACL_SUCCESS); }从测试可见当devAddr为空时两个接口均返回ACL_ERROR_INVALID_PARAM底层实现经由rtsValueWrite/rtsValueWait转调mock 层在 tests/depends/acl_stub.h 与 tests/depends/runtime/src/runtime_stub.cpp 中声明接口成功路径返回ACL_SUCCESS。这印证了aclrtValueWait/aclrtValueWrite是 AscendCL 对外标准 API且入参校验严格。8.2 与算子级内存语义同步的衔接若要在真实算子场景中复刻本样例的同步逻辑可参考 docs/zh/dev_guide/03-07_memory_semantic_synchronization.md 中的 Device 侧写法写入侧算子对同步内存执行*flag 1;后调用dcci(flag, 0, 2)刷新 cache保证写入对另一条流可见等待侧算子使用volatile指针轮询while (*flag ! 2) { dcci(flag, 0, 2); }配合dcci保证读取的是最新值。Host 侧的aclrtValueWait/aclrtValueWrite与 Device 侧的该写法共享同一套“基于通用 Device 内存 缓存一致性维护”的同步语义二者可以混合编排在一条同步流水线中。九、注意事项与实战建议等待值必须与写入值、等待模式匹配样例中valueCompare 100、valueWrite 100模式为ACL_STREAM_WAIT_VALUE_EQ。若改用GEQ写入大于等于目标值的任意值即可解除等待若使用AND/NOR则按位运算命中即解除。aclrtValueWrite的flag必须传 0这是头文件注释中明确规定的保留参数约束见 acl_rt.h传非 0 值属于非法用法。同步内存建议显式初始化/清除由于同步基于通用 Device 内存推荐在开始同步前用aclrtMemset/aclrtMemsetAsync将内存初始化为已知值避免读到残留数据产生误判。区分同步粒度的选择若只是“流内任务完成”级别的串行化优先考虑 Event若需要“某一具体数值就绪”这一数据级条件且希望算子也能参与同步则内存语义同步aclrtValueWait/aclrtValueWrite更合适。异常路径处理样例通过CHECK_ERROR宏对每个接口做失败即退出的处理生产代码中建议对aclrtValueWait设置合理的超时/错误码处理防止死等。十、小结9_multistream_sync_memory样例以最小的双线程、双 Stream 结构完整演示了 CANN Runtime 内存语义同步机制的核心aclrtValueWait在等待流上下发阻塞任务aclrtValueWrite在写入流上下发写值任务二者通过一块共享的 Device 内存完成数据级同步且可通过日志数值比对实现自动化验证。结合 03-07 内存语义同步 的算子级示例开发者可以进一步将这一机制推广到多 Stream 流水线、生产者-消费者算子编排等真实场景中作为 Event/Notify 之外又一重要的同步原语使用。【免费下载链接】runtime本项目提供CANN运行时组件和维测功能组件。项目地址: https://gitcode.com/cann/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表