之前有个同学问我,ops-transformer里的FlashAttention算子写好了,直接调不就行了,为什么还要关心runtime?

我说你想想,你写了一道菜的菜谱(算子代码),这道菜能端上桌吗?不能。你还得有厨房、灶台、锅铲、燃气,还得有人按菜谱一步步操作。runtime就是那个厨房——它负责把算子代码变成真正在昇腾NPU上执行的指令序列。

今天就来拆一下runtime的架构,看看FlashAttention从代码到执行,中间经历了什么。

runtime在CANN五层架构里的位置

先定位一下runtime在昇腾CANN里的位置:

第1层:昇腾计算语言层 AscendCL
第2层:昇腾计算服务层
第3层:昇腾计算编译层
第4层:昇腾计算执行层 ← runtime在这里
 ├─ Runtime 运行时 ← 本篇主角
 ├─ Graph Executor 图执行器
 ├─ HCCL 集合通信库
 └─ DVPP/AIPP
第5层:昇腾计算基础层
硬件层:昇腾 AI 硬件(达芬奇架构)

runtime在第4层,属于昇腾计算执行层。它的上游是图编译器(GE编译出来的模型),下游是硬件驱动(DRV)。runtime不负责编译,只负责把编译好的计算图调度到NPU上执行

用厨房比喻:GE是菜谱设计(编译),runtime是厨房调度(执行),DRV是灶台开关(硬件控制)。

runtime的四大核心职责

1️⃣ 设备管理

昇腾NPU可能有单卡,也可能有多卡。runtime负责初始化NPU设备、管理设备上下文、分配设备内存。

#include "acl/acl.h"

// 初始化昇腾计算框架
aclError ret = aclInit(nullptr);
ret = aclrtSetDevice(0); // 选择第0号NPU

// 查询设备属性
aclrtDeviceInfo deviceInfo;
aclrtGetDeviceInfo(0, &deviceInfo);
printf("HBM大小: %lu GB\n", deviceInfo.totalMem / (1024*1024*1024));

// 创建上下文(每个线程独立)
aclrtContext ctx;
aclrtCreateContext(&ctx, 0);
aclrtSetCurrentContext(ctx);

aclInit初始化整个昇腾计算框架,aclrtSetDevice选择用哪张卡。之后的算子执行、内存分配都在这个设备上下文里进行。

2️⃣ 内存管理

昇腾NPU有复杂的存储层次:HBM(高带宽内存,几十GB)、L2 Cache(片上缓存,几MB)、L1 Buffer(每个计算单元私有,几百KB)。runtime负责在这些层次之间分配和管理内存。

// 在HBM上分配设备内存
void* devPtr = nullptr;
size_t size = 1024 * 1024 * 128; // 128MB
aclrtMalloc(&devPtr, size, ACL_MEM_MALLOC_HUGE_FIRST);

// Host到Device的数据搬运
aclrtMemcpy(devPtr, hostPtr, size, ACL_MEMCPY_HOST_TO_DEVICE);

// 释放内存
aclrtFree(devPtr);

为什么runtime要管内存? FlashAttention的分块策略依赖L1 Buffer的大小——runtime知道L1有多大,算子才能决定tile切多大。

3️⃣ 流与事件

昇腾NPU支持异步执行:你提交一个算子,它不立刻执行,而是进入命令队列。runtime通过事件管理这些队列。

// 创建流(命令队列)
aclrtStream stream1, stream2;
aclrtCreateStream(&stream1);
aclrtCreateStream(&stream2);

// 在不同流上异步提交算子
aclnnFlashAttention(workspace, executor, stream1);
aclnnMatMul(workspace2, executor2, stream2);

// 创建事件用于同步
aclrtEvent event;
aclrtCreateEvent(&event);
aclrtRecordEvent(event, stream1);
aclrtStreamWaitEvent(stream2, event); // stream2等待stream1完成

流的作用:让没有依赖关系的算子并行执行。FlashAttention里,不同head的attention计算是独立的,可以提交到不同流上并行跑。

4️⃣ 算子执行

这是runtime最核心的职责:把编译好的算子调度到NPU上执行

ops-transformer里的FlashAttention算子,编译后变成一个aclnn接口。runtime负责分配workspace内存、打包算子参数、下发到NPU命令队列、监控执行状态。

#include "aclnn/aclnn_flash_attention.h"

// 准备输入tensor
aclTensor* Q = CreateAclTensor(q_data, {B, H, S, D}, ACL_FORMAT_ND, ACL_FLOAT16);
aclTensor* K = CreateAclTensor(k_data, {B, H, S, D}, ACL_FORMAT_ND, ACL_FLOAT16);
aclTensor* V = CreateAclTensor(v_data, {B, H, S, D}, ACL_FORMAT_ND, ACL_FLOAT16);
aclTensor* output = CreateAclTensor(out_data, {B, H, S, D}, ACL_FORMAT_ND, ACL_FLOAT16);

// 获取workspace大小
uint64_t workspaceSize = 0;
aclOpExecutor* executor = nullptr;
aclnnFlashAttentionGetWorkspaceSize(Q, K, V, output, &workspaceSize, &executor);

// 分配workspace并执行
void* workspace = nullptr;
aclrtMalloc(&workspace, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST);
aclrtStream stream;
aclrtCreateStream(&stream);
aclnnFlashAttention(workspace, executor, stream);

// 同步等待完成
aclrtSynchronizeStream(stream);

runtime不关心算子内部怎么算(那是ops-transformer的事),runtime只关心怎么把算子调度到NPU上、怎么分配内存、怎么同步

FlashAttention在runtime层的执行流程

📝 用户调用 flash_attention(Q, K, V)
 ↓
📝 框架适配层把调用转发给 ops-transformer
 ↓
📝 ops-transformer 查找 FlashAttention 算子的编译产物
 ↓
📝 runtime 分配 workspace 内存
 ↓
📝 runtime 把算子参数打包,下发到 NPU 命令队列
 ↓
📝 NPU 执行:达芬奇架构的 Cube Unit 做矩阵乘
 ↓
📝 runtime 监控执行状态,完成后通知上层
 ↓
📝 用户拿到输出 tensor

内存分配策略

FlashAttention对内存有特殊要求:中间的注意力矩阵不能写回HBM。runtime在分配内存时,把中间结果留在L1 Buffer里。

// Q/K/V的tile要加载到L1 Buffer
LocalTensor<half> Q_tile = AllocL1Buffer<half>(BLOCK_M * BLOCK_D);
LocalTensor<half> K_tile = AllocL1Buffer<half>(BLOCK_N * BLOCK_D);
LocalTensor<half> V_tile = AllocL1Buffer<half>(BLOCK_N * BLOCK_D);

// 累加器也在L1 Buffer
LocalTensor<half> acc = AllocL1Buffer<half>(BLOCK_M * BLOCK_N);

// 最终输出要写回HBM
GlobalTensor<half> output = AllocHBM<half>(B * H * S * D);

runtime知道每张Ascend 910的L1 Buffer大小(约1MB),算子根据这个大小决定tile切多大。

多流并行

FlashAttention在多head场景下可以并行:每个head的attention计算独立,可以提交到不同流上。

int num_heads = 32;
aclrtStream streams[32];

// 为每个head创建一个流
for (int h = 0; h < num_heads; h++) {
 aclrtCreateStream(&streams[h]);
}

// 每个head的attention提交到对应的流
for (int h = 0; h < num_heads; h++) {
 aclnnFlashAttention(workspace[h], executor[h], streams[h]);
}

// 等待所有head完成
for (int h = 0; h < num_heads; h++) {
 aclrtSynchronizeStream(streams[h]);
}

Ascend 910有多个AI Core,每个AI Core可以独立执行一个流。runtime的流调度器会把不同流的算子分配到不同AI Core上并行执行。

runtime与GE(图编译器)的关系

runtime不负责编译,编译是GE的事。但runtime和GE有紧密协作:

GE(图编译器) Runtime(运行时)
 ↓ ↓
解析计算图结构 管理设备资源
 ↓ ↓
算子融合优化 分配内存
 ↓ ↓
生成执行计划 调度算子执行
 ↓ ↓
输出编译产物 → 加载并执行编译产物

编译产物是什么? 是一个序列化的执行计划,包含算子调用顺序、每个算子的参数、内存分配计划、流分配方案。

// 加载编译好的模型
aclmdlDesc* modelDesc = nullptr;
aclmdlLoadFromFile("model.om", &modelDesc);

// 执行模型(runtime按编译计划调度)
aclmdlExecute(modelDesc, input, output, stream);
aclrtSynchronizeStream(stream);

FlashAttention在编译阶段被GE融合成一个算子,runtime只看到融合后的算子,不需要知道内部细节。

runtime的性能调优能力

内存池

频繁分配释放HBM很慢。runtime提供内存池,预分配一大块HBM,算子从池子里取,用完归还。

aclrtMemPool pool;
aclrtMemPoolCreate(&pool, 1024 * 1024 * 1024); // 预分配1GB

void* ptr = aclrtMemPoolAlloc(pool, 1024 * 1024); // 从池里分配
aclrtMemPoolFree(pool, ptr); // 归还给池

性能分析

runtime提供性能分析接口,追踪每个算子的执行时间:

aclprofStart* profConfig = aclprofCreateConfig(0, ACL_PROF_MODEL, "prof_result");
aclprofStart(profConfig);
aclmdlExecute(modelDesc, input, output, stream);
aclprofStop(profConfig);

分析结果包含每个算子的执行时间、内存占用、流调度情况,用来优化FlashAttention的执行策略。

runtime的多卡支持

昇腾NPU通常以多卡集群形式部署。runtime支持多卡场景:

int deviceCount = 0;
aclrtGetDeviceCount(&deviceCount);

// 每张卡创建独立的上下文和流
for (int i = 0; i < deviceCount; i++) {
 aclrtSetDevice(i);
 aclrtCreateContext(&contexts[i], i);
 aclrtCreateStream(&streams[i]);
}

// 在不同卡上执行不同的计算
for (int i = 0; i < deviceCount; i++) {
 aclrtSetCurrentContext(contexts[i]);
 aclnnFlashAttention(..., streams[i]);
}

多卡场景下,runtime和HCCL(集合通信库)协作。runtime负责本地执行,HCCL负责卡间通信。

总结

runtime在昇腾CANN五层架构里,是承上启下的关键一层:

承上:接收GE编译好的执行计划,理解算子的内存需求和调度依赖。

启下:调度NPU硬件执行,管理存储层次,保证算子高效落地。

FlashAttention在ops-transformer里只是"菜谱"——定义了分块策略、在线softmax、融合kernel。真正让FlashAttention跑起来的是runtime:

  • runtime分配L1 Buffer,让tile能留在片上不回HBM
  • runtime调度多流,让不同head的attention并行执行
  • runtime管理设备上下文,让多卡集群协同工作

一句话说清楚:算子定义"怎么算",runtime负责"在哪算、什么时候算、算完放哪"。

昇腾NPU上的高性能应用,必须同时懂ops-transformer的算子原理和runtime的执行机制。算子写得再好,runtime调度不对,性能也会打折扣。

意外收获:runtime的设计思路跟CUDA Runtime很像——设备管理、内存管理、流、事件,概念一一对应。如果你熟悉CUDA,上手昇腾runtime会很快。区别在于昇腾的存储层次更复杂(L1/L2/HBM三层),内存分配策略需要更精细。

Logo

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

更多推荐