CANN Runtime 异步任务调度:Stream 与 Event 的执行哲学

发布时间:2026/5/21 4:12:11

CANN Runtime 异步任务调度:Stream 与 Event 的执行哲学 推理框架的代码里aclrtLaunchJob提交算子和aclrtSynchronizeStream等算子执行完这两类调用占了大部分。不夸张地说Runtime 的异步调度机制直接决定了推理服务的吞吐上限。CANN Runtime 的核心调度模型是 Stream Event。Stream 让操作并发Event 让并发有序。这篇文章从执行链路入手讲清楚 Runtime 是怎么调度任务推进的。为什么 AI 推理需要异步执行推理一个模型的典型步骤CPU 预处理输入数据把预处理结果拷到 NPU 显存H2DNPU 执行模型推理把结果拷回 CPU 内存D2HCPU 后处理结果如果这 5 步是串行的——CPU 等着 NPU 算完才做下一步——NPU 执行期间 CPU 在空等CPU 做预处理时 NPU 在空等。推理服务的吞吐浪费了一半。异步执行的核心是把这些步骤重叠。CPU 在处理第 N 个请求的预处理时NPU 在同时跑第 N-1 个请求的推理。理想状态下 CPU 和 NPU 100% 并行工作互不等待。Runtime 如何调度任务Runtime 不直接暴露给应用层——应用层调的是 AscendCLAscendCL 内部调用 Runtime 的接口。以aclmdlExecute为例Runtime 内部做的事情解析模型句柄。根据 modelId 找到对应的 OM 模型内存映射获取 GE 优化后的 Task 列表Task 拆解。每个 Task 对应一个算子或融合算子。Runtime 为每个 Task 创建 Task 描述算子的 Kernel 句柄、输入输出地址、执行配置Stream 分发。Task 被推送到指定 Stream 的队列中硬件提交。Runtime 的调度器从 Stream 队列取 Task通过驱动提交到 AI Core 执行整个过程耗时约 5-15μs。对于单次推理来说微不足道但对于解码阶段每步都要 Launch 几十个 Task 的场景调度开销累加起来可能占推理延迟的 10-20%。Stream 为什么重要Stream 是 Runtime 调度的核心抽象。一个 Stream 是一个 FIFO 的任务队列——所有提交到该 Stream 的 Task 按顺序执行。单 Stream 模型提交: Task1 → Task2 → Task3 → Task4 → Task5 执行: [Task1] → [Task2] → [Task3] → [Task4] → [Task5]严格串行N 个 Task 的总延迟 各 Task 延迟之和。多 Stream 模型无依赖Stream A: [Task1] → [Task3] → [Task5] Stream B: [Task2] → [Task4]Task1 和 Task2 可以同时执行——如果 NPU 有足够的执行单元总延迟可能从 5 个单位降到 3 个单位。推理框架利用多 Stream 的典型做法是三条流水线aclrtStream preprocess_stream,compute_stream,postprocess_stream;// 每条 Stream 处理不同阶段的任务aclrtLaunchOp(preprocess_op,...,preprocess_stream);// CPU 预处理aclrtLaunchOp(compute_op,...,compute_stream);// NPU 推理aclrtLaunchOp(postprocess_op,...,postprocess_stream);// 后处理Runtime 的硬件调度器会尽可能让这 3 条 Stream 的 Task 在不同执行单元上并行——preprocess_stream 在 DVPP 上跑compute_stream 在 AI Core 上跑postprocess_stream 在 Vector Unit 上跑。Event 如何同步任务多 Stream 的问题在于不同 Stream 的任务之间可能有依赖关系。推理的计算必须在预处理完成之后开始后处理必须在推理完成之后开始。Event 就是来解决这个同步问题的。aclrtEvent preprocess_done;aclrtCreateEvent(preprocess_done);// 在预处理 Stream 上提交预处理并在完成后设置 EventaclrtLaunchOp(preprocess_op,...,preprocess_stream);aclrtRecordEvent(preprocess_done,preprocess_stream);// 在推理 Stream 上等待预处理完成后再开始推理aclrtStreamWaitEvent(compute_stream,preprocess_done);aclrtLaunchOp(compute_op,...,compute_stream);aclrtRecordEvent在指定 Stream 中插入一个 Event 标记——当 Stream 执行到这个标记时Event 变为完成状态。aclrtStreamWaitEvent让另一个 Stream 在继续执行前等待这个 Event。Event 的同步是在硬件层面完成的——不经过 CPU 中断或轮询。当 compute_stream 的调度器看到它依赖的 Event 还没完成时不去启动待运行的 Task而是去调度其他不依赖这个 Event 的 Task。Memory 管理与 Stream 的关联Runtime 的内存管理也跟 Stream 紧密相关。每个 Stream 有自己的内存分配上下文。两个 Stream 之间共享显存池但各自维护独立的分配指针。aclrtMalloc默认从进程级显存池分配。aclrtMallocFromStream可以从指定 Stream 的上下文中分配——Stream 退出时自动回收该内存。// 从 compute_stream 的上下文中分配显存// Stream 销毁时自动 Freevoid*bufaclrtMallocFromStream(size,compute_stream);这个机制在流水线推理中很有用——预处理 Stream 分配的临时 Buffer 在推理 Stream 消费完后自动回收不需要手动调用aclrtFree。昇腾 Runtime 的执行链路把完整链路串起来应用层调用 aclmdlExecute 或 aclmdlExecuteAsync ↓ AscendCL 解析模型 → 获取 GE 优化后的 Task 列表 ↓ Runtime 创建 Task 描述50-100 个 Task ↓ Task 按 Stream 分配到队列 ↓ 硬件调度器从各 Stream 队列取 Task ↓ HW Scheduler 检查 Event 依赖 ↓ 就绪的 Task 派发给 AI Core / Vector Unit / DVPP ↓ Task 完成 → 触发 Event如果有注册 ↓ 依赖该 Event 的 Stream 继续执行Runtime 的硬件调度器是整个链路中最被低估的组件。它不是简单的 FIFO 调度器——它会动态调整不同 Stream 的 Task 执行顺序来最大化硬件利用率。一个 Stream 的 Task 如果因为等待 Event 被阻塞调度器不会空转而是去运行另一个不依赖该 Event 的 Stream 的 Task。继续学习Runtime 的 Stream 和 Event 机制是 CANN 推理异步调度的核心。理解了这两层就能理解如何通过多 Stream 流水线把 NPU 利用率从 40% 拉到 80%。进一步学习 GE 的图执行机制可以了解 Task 在提交给 Runtime 之前已经做了哪些编排。CANN Runtime 仓库Graph Executor 图执行引擎内存池的 Stream 亲和性Runtime 的内存池对不同 Stream 的亲和性不同。aclrtMalloc从全局池分配——所有 Stream 共享。aclrtMallocFromStream从 Stream 的本地池分配——该 Stream 独享分配和回收不需要锁。// 全局分配——所有 Stream 都可访问但需要加锁void*bufaclrtMalloc(size,ACL_MEM_MALLOC_HUGE_FIRST);// Stream 本地分配——只有 compute_stream 可快速访问void*localaclrtMallocFromStream(size,compute_stream);Stream 本地池的分配速度比全局池快约 20%因为不需要锁争用。但本地池的内存只能被该 Stream 访问。如果是多 Stream 共享的 Tensor比如模型的权重必须用全局分配。Runtime 调试技巧推理性能不达预期时第一步是检查 Runtime 的调度效率# 开启 Runtime 的 Trace 日志exportACL_RUNTIME_TRACE1Trace 日志会输出每个 Task 的提交时间、执行开始时间、完成时间。从日志中可以看到Task 之间有没有空档Task 结束到下一个 Task 开始之间 NPU 空闲的时间Stream 之间有没有等待一个 Stream 等另一个 Stream 的 EventMemory 分配有没有持锁等待多 Stream 竞争全局池常见问题如果日志显示同一 Stream 内连续 Task 之间有空档说明数据搬运没跟上计算——需要优化 Double Buffer 或加大 Tile 尺寸。参考仓库CANN RuntimeGraph Executor 图执行引擎

相关新闻