在这里插入图片描述

算子编译好了,要在昇腾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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐