runtime 运行时:昇腾NPU的“操作系统内核”

刚接触 CANN 那会,我以为 runtime 就是个“运行时库”,调几个 API 就完事。后来帮一个朋友排查 FlashAttention 算子执行慢的问题,发现 runtime 才是真正的“幕后黑手”——算子再快,runtime 调度不当,NPU 算力也跑不满。

昇腾NPU 上跑模型,runtime 是绕不开的基础设施。CANN 把 runtime 放在第五层架构的“执行层”,负责把编译好的算子真正扔到 NPU 硬件上跑。这次把 runtime 的架构拆清楚,以后排查性能问题就不会再迷路。

环境准备:理解 runtime 在 CANN 中的位置

在开始拆解 runtime 架构之前,先得搞清楚它跟其他组件的关系。

runtime 的定位

  • CANN 五层架构中的第4层(昇腾计算执行层):它位于图编译器之下,驱动之上。
  • 上层对接:Graph Compiler(图编译器)、BiSheng/ATC(编译器)。
  • 下层对接:Driver(驱动)、NPU 硬件(达芬奇架构)。

依赖关系

你的 PyTorch 模型
  ↓ (框架适配层)
Graph Compiler(图编译)
  ↓ (编译成执行计划)
runtime(运行时)
  ↓ (调用底层接口)
driver(驱动)
  ↓ (真正干活)
昇腾NPU 达芬奇架构硬件

FlashAttention 的执行链路
ops-transformer 的 FlashAttention 算子 →\rightarrow 调用 Ascend C 编程接口 →\rightarrow runtime 把算子调度到 AI Core →\rightarrow 在 NPU 上执行矩阵分块计算。

如果 runtime 调度不当(比如任务分配不均、数据搬运没重叠),FlashAttention 的延迟就会上去。

逐步推进:runtime 的核心模块拆解

runtime 不是单一模块,而是一组协同工作的子系统。结合 CANN 8.0+(当前时间 2026 年 5 月的主流版本)的特性,核心模块有 5 个:

1. Runtime 运行时(核心调度器)

这是 runtime 的“大脑”,负责:

  • 设备管理:检测 NPU 设备、分配设备上下文。
  • 任务调度:把算子任务分配给多个 AI Core 并行执行。
  • 内存管理:管理 NPU 上的 GM(全局内存)、L1、L0。

关键 API(AscendCL 接口)

import acl

# 初始化运行时
acl.init()

# 申请 NPU 设备
device_id = 0
acl.rt.set_device(device_id)

# 分配内存(GM 上)
data_size = 1024 * 1024  # 1MB
data_ptr, _ = acl.rt.malloc(data_size, acl.rt.MemType.MEM_TYPE_NORMAL)

# 释放设备
acl.rt.reset_device(device_id)
acl.finalize()

⚠️ 踩坑预警acl.rt.malloc 申请的是 NPU 的 GM 内存,不是主机内存。如果你把主机指针传给 NPU 算子,会报“非法内存访问”错误。正确做法是先用 acl.rt.malloc 申请 NPU 内存,再用 acl.rt.memcpy 把数据从主机拷到 NPU。

2. Graph Executor(图执行器)

如果你的模型是用 CANN 的图开发接口构建的(比如用了 GE 图引擎),Graph Executor 负责:

  • 图解析:把编译好的图(.om 文件)解析成执行计划。
  • 节点调度:按拓扑顺序执行图中的算子节点。
  • 数据流转:管理算子之间的中间数据(不写回主机,直接在 NPU 上传递)。

FlashAttention 如果跑在图模式下,Graph Executor 会把 Q、K、V 的算子融合成一个子图,减少数据搬运次数。

验证 Graph Executor 是否工作

# 检查图执行日志
cat /var/log/npu/log/runtime.log | grep "GraphExecutor"

如果看到 “GraphExecutor: Load graph success”,说明图加载成功。

3. HCCL(集合通信库)

如果你做的是分布式训练/推理(比如 Megatron + 昇腾NPU),HCCL 负责:

  • 集合通信:AllReduce、AllGather、ReduceScatter 等。
  • 多卡协同:多个 NPU 之间的数据同步。

FlashAttention 在分布式场景下,需要 HCCL 做 Ring Attention(把序列维度切分到多个 NPU 上并行算)。

⚠️ 踩坑预警:HCCL 的初始化很慢(第一次要建通信组)。如果你在测试用例里频繁初始化/销毁 HCCL,性能会很差。正确做法是进程启动时初始化一次,整个生命周期复用

4. DVPP(数字视觉预处理)

如果你的模型输入是图像/视频,DVPP 负责:

  • 图像解码:JPEG/PNG 解码成 YUV/RGB。
  • 预处理:Resize、Crop、Color Space Conversion。
  • 零拷贝:预处理完直接扔给 NPU 算子,不写回主机。

FlashAttention 本身不调 DVPP,但如果你的模型是 Vision Transformer(ViT),输入图像就得先过 DVPP 做预处理。

验证 DVPP 是否工作

# 检查 DVPP 占用率
npu-smi info -t dvpp

如果 DVPP 占用率是 0%,说明预处理没走 NPU,而是在 CPU 上做的(性能会差很多)。

5. AIPP(AI 预处理)

AIPP 是 DVPP 的补充,负责:

  • 图像归一化:Mean/Std 归一化。
  • 格式转换:RGB → BGR、YUV → RGB。
  • 融合到算子:AIPP 的配置可以融合到算子执行计划里,不单独跑。

⚠️ 踩坑预警:AIPP 和 DVPP 都能做图像预处理,但 AIPP 更轻量(不占 DVPP 硬件单元)。如果你的模型只需要做归一化,用 AIPP 就够了,别浪费 DVPP 资源。

踩坑实录:runtime 使用中的常见错误

踩坑 1:内存泄漏——忘了释放 NPU 内存

# 错误写法(会泄漏)
data_ptr, _ = acl.rt.malloc(size, acl.rt.MemType.MEM_TYPE_NORMAL)
# ... 用完没释放 ...

# 正确写法
data_ptr, _ = acl.rt.malloc(size, acl.rt.MemType.MEM_TYPE_NORMAL)
try:
    # ... 使用 data_ptr ...
    pass
finally:
    acl.rt.free(data_ptr)  # 确保释放

踩坑 2:设备上下文没设置——算子执行失败

# 错误写法(没设置设备上下文)
acl.init()
# ... 直接调算子 ...  # 会报"设备未初始化"错误

# 正确写法
acl.init()
acl.rt.set_device(0)  # 先设置设备上下文
# ... 调算子 ...
acl.rt.reset_device(0)
acl.finalize()

踩坑 3:Host→Device 数据搬运没同步——数据没搬完就开始算

# 错误写法(没同步)
acl.rt.memcpy(dev_ptr, host_data, size, acl.rt.MemcpyKind.MEMCPY_HOST_TO_DEVICE)
# ... 立刻调算子 ...  # 数据可能还没搬完

# 正确写法(加同步)
acl.rt.memcpy(dev_ptr, host_data, size, acl.rt.MemcpyKind.MEMCPY_HOST_TO_DEVICE)
acl.rt.synchronize()  # 等数据搬运完成
# ... 再调算子 ...
验证环节:runtime 是否正常工作

1. 检查 NPU 设备状态

# 查看 NPU 设备列表
npu-smi info

# 查看运行时日志
cat /var/log/npu/log/runtime.log | tail -50

如果 runtime 初始化成功,日志里会有 “Runtime initialized successfully”。

2. 跑一个最简单的算子,验证 runtime 调度

你可以参考 cann-samples 仓库中的示例代码。

import acl
import numpy as np

# 初始化
acl.init()
acl.rt.set_device(0)

# 申请内存
size = 1024
host_data = np.random.randn(size).astype(np.float32)
dev_ptr, _ = acl.rt.malloc(size * 4, acl.rt.MemType.MEM_TYPE_NORMAL)

# 数据搬运(Host → Device)
acl.rt.memcpy(dev_ptr, host_data.tobytes(), size * 4, acl.rt.MemcpyKind.MEMCPY_HOST_TO_DEVICE)
acl.rt.synchronize()

# ... 这里调你的算子 ...

# 数据搬运(Device → Host)
output_host = np.zeros(size, dtype=np.float32)
acl.rt.memcpy(output_host.tobytes(), dev_ptr, size * 4, acl.rt.MemcpyKind.MEMCPY_DEVICE_TO_HOST)
acl.rt.synchronize()

# 释放
acl.rt.free(dev_ptr)
acl.rt.reset_device(0)
acl.finalize()

如果这个流程能跑通,说明 runtime 基本正常工作。

3. 性能验证:runtime 调度是否跑满 NPU

# 跑模型时,实时监控 NPU 利用率
npu-smi info -t aicore -i 0 -c 0

如果 AI Core 利用率长期低于 60%,说明 runtime 调度有问题(任务分配不均、数据搬运没重叠)。这时候得去查:

  • 算子是不是没切分好(大算子占满一个 AI Core,其他 AI Core 闲着)。
  • 数据搬运是不是同步的(没用异步搬运,计算和数据搬运串行)。
下一步行动建议
  1. 读 runtime 源码:从 acl.rt 接口看起,理解内存管理/任务调度的实现。
  2. 跑 CANN 的 runtime 示例cann-samples 仓库里有现成的调用代码。
  3. 用 npu-smi 监控 runtime 状态:学会看 AI Core 利用率、内存占用、通信带宽。
  4. 排查 FlashAttention 性能问题:如果 FlashAttention 跑不满 NPU,先查 runtime 调度,再查算子实现。

参考仓库

  • cann-samples(运行时调用示例):https://atomgit.com/cann/cann-samples
  • driver(运行时下层对接的驱动):https://atomgit.com/cann/driver
Logo

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

更多推荐