CANN runtime:昇腾NPU的运行时怎么管理计算任务
昇腾NPU运行时(runtime)是连接算子编译与硬件执行的关键中间层,负责内存管理、任务调度和流控制。它通过Context管理设备资源,利用Stream实现并行计算,借助Event进行流间同步,并采用高效的内存分配策略。runtime采用命令队列模式下发任务,支持同步/异步执行,通过多Stream并行提升NPU利用率。开发者可通过内存池复用、多流并行等技术优化性能,并使用msprof工具监控硬件

算子编译好了,要在昇腾NPU上跑起来,中间还有一层——runtime。它负责内存分配、任务下发、流管理、事件同步。搞不懂 runtime,就搞不懂为什么你的算子跑不满 Cube 核的算力。
runtime 在 CANN 五层架构中的位置
runtime 属于昇腾异构计算架构第 4 层(执行层),直接和 driver 交互,把 GE 编译好的算子执行计划映射到具体的硬件核上。
从调用链路看:AscendCL API → runtime → driver → 硬件。AscendCL 是面向用户的编程接口,runtime 是底层执行引擎。
runtime 的核心职责是把"图"级别的执行计划拆解成"算子核"级别的指令序列,然后通过 driver 下发到昇腾NPU上执行。
核心对象:Context、Stream、Event、Memory
runtime 有四个核心对象,理解它们之间的交互关系就理解了整个 runtime 的工作方式。
Context 是设备上下文,绑定到特定的昇腾NPU上。一个 Context 管理该设备上的所有资源:内存分配、流、事件。创建 Context 是使用 NPU 的第一步。
// Context 的创建和销毁(C API 示例)
aclError ret;
aclContext *ctx;
// 初始化 ACL(Ascend Computing Language)
ret = aclInit(nullptr); // nullptr 用默认配置
// 打开设备 0
ret = aclrtSetDevice(0);
// 创建 Context 绑定到设备 0
ret = aclCreateContext(&ctx, 0); // &ctx 是输出参数
// ... 这里写算子执行代码 ...
// 销毁 Context
ret = aclDestroyContext(ctx);
// 关闭设备
ret = aclrtResetDevice(0);
// 退出 ACL
ret = aclFinalize();
Stream 是执行流。同一 Stream 里的任务是按顺序提交的,但执行顺序不一定是提交顺序——取决于硬件调度。不同 Stream 之间默认并行执行,除非显式同步。
Stream 是 runtime 并行化的核心工具。把独立的任务放到不同 Stream 上,它们会同时跑在不同的 AI Core 上。
aclrtStream stream1, stream2;
ret = aclrtCreateStream(&stream1);
ret = aclrtCreateStream(&stream2);
// Stream 1: 先做预处理
ret = aclrtMemcpy(d_a, size, h_a, size, ACL_MEMCPY_HOST_TO_DEVICE);
ret = aclrtLaunchKernel(preprocessKernel, // 预处理算子
blockDim, threadDim, sharedMem, stream1,
d_a, d_b, ...);
// Stream 2: 和 Stream 1 并行跑主计算
ret = aclrtLaunchKernel(mainKernel, // 主计算算子
blockDim, threadDim, sharedMem, stream2,
d_c, d_d, ...);
// Stream 1 和 Stream 2 完全并行执行(如果硬件有足够资源)
// Stream 2 依赖 Stream 1 的结果?用 Event 同步
aclrtEvent event1;
ret = aclrtCreateEvent(&event1);
ret = aclrtRecordEvent(event1, stream1); // 在 stream1 上记录 event1
// Stream 2 在 stream1 完成前不能开始?用 Stream Wait Event
ret = aclrtStreamWaitEvent(stream2, event1); // stream2 等 event1
// Stream 2 现在可以安全地使用 stream1 的结果了
ret = aclrtLaunchKernel(postprocessKernel, // 后处理算子(依赖 stream1)
blockDim, threadDim, sharedMem, stream2,
d_c, d_d, ...);
// 同步所有 stream
ret = aclrtSynchronizeStream(stream1);
ret = aclrtSynchronizeStream(stream2);
// 清理
ret = aclrtDestroyStream(stream1);
ret = aclrtDestroyStream(stream2);
ret = aclrtDestroyEvent(event1);
Event 用于流间同步。aclrtRecordEvent 在某个 Stream 的某个时间点记录一个事件,aclrtStreamWaitEvent 让另一个 Stream 等这个事件发生。Event 是 CUDA/HIP 开发者熟悉的概念,昇腾 runtime 的 Event API 几乎完全对标 CUDA。
Memory 是 Device 内存分配。昇腾NPU的 Device 内存(位于 HBM)和 Host 内存(位于 RAM)是两块独立的地址空间,需要显式拷贝。
// Device 内存分配(C API)
void *d_ptr;
size_t size = 1024 * 1024; // 1MB
// 分配 Device 内存
ret = aclrtMalloc(&d_ptr, size, ACL_MEM_MALLOC_HUGE_FIRST);
// Host 到 Device 拷贝
void *h_ptr = malloc(size);
ret = aclrtMemcpy(d_ptr, size, h_ptr, size, ACL_MEMCPY_HOST_TO_DEVICE);
// Device 到 Host 拷贝
ret = aclrtMemcpy(h_ptr, size, d_ptr, size, ACL_MEMCPY_DEVICE_TO_HOST);
// 释放
ret = aclrtFree(d_ptr);
free(h_ptr);
// PyTorch 的内存管理更简单,torch.npu 帮你自动处理
import torch
a = torch.randn(1024, 1024, device="npu:0") # 自动分配 NPU 内存
# PyTorch NPU tensor 不需要手动 free,GC 自动处理
任务下发机制
从 Host(CPU)到 Device(NPU)的任务下发是 runtime 的核心功能之一。
昇腾NPU的任务调度用的是命令队列(Command Queue)模式。Host 端把算子执行请求 push 到 Command Queue,NPU 端的 Command Processor 从 Queue 里取请求执行。
Command Queue 支持两种模式:同步模式(Host 等任务完成再返回)和异步模式(Host 提交任务后立即返回)。异步模式是性能优化的关键——让 Host 在 NPU 执行任务的同时做别的事情。
aclrtLaunchKernel 是异步的。调用它只是把算子下发到 Command Queue,Host 端立即返回。aclrtSynchronizeStream 才会真正等 NPU 执行完。
内存管理策略
runtime 的内存管理直接影响性能。
默认分配策略(ACL_MEM_MALLOC_HUGE_FIRST):优先从 2MB 的 huge page 分配,如果 huge page 不够再从 4KB page 分配。Huge page 的好处是 TLB miss 少,地址翻译快。
显存的复用:runtime 内部有一个 memory pool,释放的显存不会立即还给系统,而是放回 pool 供下次分配。复用比直接 free+alloc 快很多。
PyTorch NPU tensor 的显存管理和 runtime 的 memory pool 集成良好——tensor 超出作用域后显存自动回收,pool 会复用这块显存。
多卡内存池:分布式场景下,runtime 支持跨卡的显存管理。多卡之间可以通过 PCIe 或 NVLink 直接交换数据,不需要绕 Host。
多 Stream 并行
一个常见的问题是:单 Stream 跑的时候昇腾NPU利用率不高。原因是单 Stream 里的任务是串行依赖的——A 算子的输出是 B 算子的输入,所以 B 必须等 A 完成才能开始。
多 Stream 并行可以解决这个问题。思路是:把没有依赖的任务拆分到不同的 Stream 上,让它们同时跑。
比如推理的时候,数据预处理和模型计算没有数据依赖,可以并行:stream_preprocess 跑 DVPP 预处理,stream_compute 跑模型推理。两个 Stream 各自排队,硬件调度器看到两个 Stream 都有任务在等,就会并行调度。
Stream 并行的前提是硬件资源够用。昇腾 910 有 32 个 Cube 核和 32 个 Vector 核,足够同时跑多个任务。但如果拆分太多,每个任务的并行度反而下降。
# PyTorch 多 Stream 并行示例
import torch
import torch_npu
torch.npu.set_stream(torch.npu.Stream(stream_id=0)) # Stream 0: 主计算
# Stream 1: 预取下一批数据(在主计算的同时做)
with torch.npu.stream(torch.npu.Stream(stream_id=1)):
next_input = load_next_batch() # 预取
next_input = next_input.npu() # 搬到 NPU
# Stream 0: 主计算
current_output = model(current_input)
# 同步点:等主计算完成后才能用 next_input
torch.cuda.synchronize() # 或者 torch.npu.synchronize()
# Stream 1 的数据已经就绪,可以开始下一轮计算
性能监控:msprof 工具
runtime 暴露了详细的性能数据,通过 msprof 工具采集。msprof 不只是采集 timeline,它还能采集 hardware counter(Cube 利用率、Vector 利用率、Memory 带宽、HBM 带宽等)。
# 用 msprof 采集 NPU 执行的 profile 数据
# 先设环境变量启用 profiling
export ASCEND_PROFILING_ENABLED=1
export ASCEND_PROFILING_OUTPUT_PATH=/tmp/profiling_output
# 运行程序
python run_model.py
# 会在 /tmp/profiling_output/ 下生成 .json 文件
# 用 msprof_viewer 查看可视化报告
msprof --view /tmp/profiling_output/profiling_*.json
msprof 的报告中,最关键的几个指标:NPU Core Utilization(CUBE 和 Vector 核的实际利用率,对比峰值算力)、Memory Bandwidth Utilization(HBM 带宽利用率)、Task Queue Depth(任务排队长度,排队太长说明调度跟不上计算)。
如果 CUBE 利用率只有 40%,问题通常在三个地方:访存瓶颈(带宽不够或者数据局部性差)、任务依赖链太长(需要多 Stream 并行)、算子本身太轻量(Kernel launch overhead 占比高)。
https://atomgit.com/cann/runtime
更多推荐



所有评论(0)