前言
杭州某AI实验室,深夜十一点。
两个工程师对着屏幕发呆------他们的模型在GPU上跑得好好的,迁移到昇腾NPU之后,性能直接腰斩。
"算子没问题,通信没问题,图编译也没问题......到底卡在哪了?"
排查了三天,最后发现问题出在Stream调度上------GPU和NPU的执行模型不一样,他们照搬了GPU的调度方式,导致NPU的Cube Unit大量空闲时间。
解决方案:重新配置Runtime的Stream调度策略。
改完之后,性能恢复到了GPU的1.3倍。
Runtime,就是那个藏在最底层、平时不声不响、但出了问题会让你抓狂的东西。
一、Runtime是什么?
Runtime是昇腾CANN的核心运行时库,负责管理NPU硬件资源、调度计算任务、处理数据搬运。
如果把CANN比作一座修炼宗门:
- ops-transformer 是功法招式(算子)
- ge 是阵法编排(图引擎)
- hccl 是传音术(集合通信)
- ATB 是御剑术(推理加速)
- Runtime 就是------灵脉根基
没有灵脉,功法无处借力,阵法无法运转,传音无处传递。Runtime就是CANN所有上层组件的底座,它直接和NPU硬件打交道,把上层发来的计算任务翻译成硬件能执行的指令。
仓库地址:https://atomgit.com/cann/runtime
二、Runtime的五大核心职责
1. 设备管理:灵脉的"开关"
Runtime负责管理NPU设备的生命周期:
// 初始化设备
aclError ret = aclInit(nullptr); // 初始化ACL运行时
ret = aclrtSetDevice(deviceId); // 指定使用的NPU设备
// 查询设备信息
uint32_t deviceCount = 0;
aclrtGetDeviceCount(&deviceCount); // 查询可用NPU数量
// 释放设备
aclrtResetDevice(deviceId); // 释放指定设备
aclFinalize(); // 反初始化运行时
关键概念:
- Device:一张NPU卡就是一个Device
- Context:Device上的执行上下文,包含Stream、Event等资源
- 一个Device可以创建多个Context(但同一时刻只有一个Context是当前Context)
⚠️ 踩坑点:aclrtSetDevice 和 aclrtResetDevice 必须配对使用。忘记Reset会导致设备资源泄露,长时间运行后NPU会变成"僵尸状态"------设备还在,但新的Context创建不了。
2. 内存管理:灵脉的"灵力调度"
NPU有自己独立的显存(Device Memory),和CPU内存(Host Memory)是分开的。Runtime负责管理两种内存之间的数据搬运。
// 分配NPU显存
void* devPtr = nullptr;
aclrtMalloc(&devPtr, size, ACL_MEM_MALLOC_HUGE_FIRST);
// Host → Device(数据从CPU搬到NPU)
aclrtMemcpy(devPtr, size, hostPtr, size, ACL_MEMCPY_HOST_TO_DEVICE);
// Device → Host(数据从NPU搬回CPU)
aclrtMemcpy(hostPtr, size, devPtr, size, ACL_MEMCPY_DEVICE_TO_HOST);
// 释放NPU显存
aclrtFree(devPtr);
关键优化点:
Huge Page(大页内存) :NPU支持2MB大页,减少TLB Miss。分配时用ACL_MEM_MALLOC_HUGE_FIRST优先使用大页,性能提升5-10%。
HostPin内存 :如果Host内存需要频繁和Device交互,可以用aclrtMallocHost分配Pinned Memory(锁页内存),DMA传输更快,但会占用物理内存,不能swap到磁盘。
// 分配锁页内存(DMA传输更快)
void* hostPinPtr = nullptr;
aclrtMallocHost(&hostPinPtr, size);
内存池:频繁Malloc/Free会导致显存碎片。Runtime提供了内存池机制:
aclrtMemPool pool;
aclrtMemPoolCreate(&pool, deviceId, maxSize);
aclrtMallocFromPool(&devPtr, size, pool);
aclrtFreeToPool(devPtr, pool);
aclrtMemPoolDestroy(pool);
3. Stream与Event:灵脉的"运行经脉"
这是Runtime最核心、也是最容易踩坑的部分。
**Stream(流)**是NPU上的任务队列。提交到同一个Stream的任务按顺序执行,不同Stream的任务可以并行执行。
// 创建Stream
aclrtStream stream;
aclrtCreateStream(&stream);
// 在指定Stream上执行任务
aclrtLaunchKernel(kernel, stream, ...);
// 同步等待Stream上所有任务完成
aclrtSynchronizeStream(stream);
**Event(事件)**是Stream之间的同步机制。
// 创建Event
aclrtEvent event;
aclrtCreateEvent(&event);
// 在Stream1上记录Event
aclrtRecordEvent(event, stream1);
// Stream2等待Event
aclrtStreamWaitEvent(stream2, event);
这就好比两条灵脉(Stream)在运行,Event就是灵脉交汇处的"关隘"------一条灵脉到了关隘,另一条才能继续流动。
实战案例:多Stream并行
// 数据搬运用Stream1,计算用Stream2
// 实现搬运和计算的流水线并行
aclrtStream h2dStream, computeStream;
aclrtCreateStream(&h2dStream);
aclrtCreateStream(&computeStream);
// 第一批数据搬运
aclrtMemcpyAsync(devPtr1, size, hostPtr1, size, ACL_MEMCPY_HOST_TO_DEVICE, h2dStream);
aclrtRecordEvent(dataReady1, h2dStream);
// 等第一批数据就绪后计算
aclrtStreamWaitEvent(computeStream, dataReady1);
aclrtLaunchKernel(kernel1, computeStream, ...);
// 同时搬运第二批数据(和计算并行)
aclrtMemcpyAsync(devPtr2, size, hostPtr2, size, ACL_MEMCPY_HOST_TO_DEVICE, h2dStream);
aclrtRecordEvent(dataReady2, h2dStream);
性能差距 :单Stream vs 双Stream流水线,吞吐量差1.5-2倍。这就是开头那个故事的根因------GPU上单Stream也能跑得快(因为GPU的SM调度更灵活),但NPU上必须用多Stream才能把Cube Unit喂满。
4. 模型执行:灵脉的"功法运转"
Runtime负责在NPU上执行编译好的模型(OM文件)。
// 加载模型
uint32_t modelId;
aclmdlLoadFromFile("model.om", &modelId);
// 准备输入输出
aclmdlDataset* input = aclmdlCreateDataset();
aclmdlDataset* output = aclmdlCreateDataset();
// ... 添加输入输出buffer ...
// 执行模型
aclmdlExecute(modelId, input, output);
// 卸载模型
aclmdlUnload(modelId);
关键概念:
-
OM文件:由ATC编译器生成的离线模型文件,包含算子编译后的二进制码
-
Dynamic Shape:如果模型输入shape不固定,需要在加载时指定dynamic batch/resize信息
-
AIPP:AI Pre-Processing,在模型执行前自动做图像预处理(缩放、裁剪、归一化等)
// 加载支持Dynamic Batch的模型
aclmdlConfigHandle* config = aclmdlCreateConfigHandle();
aclmdlSetConfigOpt(config, ACL_MDL_DYNAMIC_BATCH_SIZE, "1,4,8,16");
aclmdlLoadWithConfig(config, &modelId);
5. 亲和性调度:灵脉的"走穴"
NPU内部不是铁板一块。Atlas 800T A2的一张NPU里,有:
- Cube Unit(矩阵乘法,算力主力)
- Vector Unit(向量运算,辅助计算)
- MTE(Memory Transfer Engine,数据搬运)
Runtime负责把任务调度到合适的执行单元:
| 任务类型 | 推荐执行单元 | 原因 |
|---|---|---|
| 矩阵乘法 | Cube | 专为大矩阵乘法优化 |
| 激活函数、归一化 | Vector | 逐元素操作 |
| Host↔Device数据搬运 | MTE | DMA引擎,不占用计算资源 |
| NPU间通信 | MTE + HCCL | 通过HCCS链路 |
如果调度不合理(比如把矩阵乘法发到Vector上),性能可能差10倍以上。
三、Runtime在CANN五层架构里的位置
第1层:AscendCL(应用开发接口) ← Runtime是AscendCL的底层实现
第2层:AOL算子库 + ATB
第3层:Graph Compiler + BiSheng
第4层:Runtime + Graph Executor + HCCL ← Runtime在这里
第5层:驱动(Driver)
Runtime位于第4层(昇腾计算执行层),是所有上层组件的"地基"。
调用链:
应用代码
→ AscendCL API(aclrtMemcpy, aclrtLaunch等)
→ Runtime(管理Stream、内存、设备)
→ Driver(操作NPU硬件寄存器)
→ NPU硬件执行
注意:上层开发者通常不直接调用Runtime,而是通过AscendCL API(aclrt*系列函数)。但AscendCL的运行时部分本质上就是Runtime的封装。
四、Runtime vs CUDA Runtime:灵脉 vs 走脉
从GPU迁移到NPU的工程师,最容易犯的错误就是"照搬CUDA的玩法"。两个Runtime的设计哲学差异很大:
| 维度 | CUDA Runtime | 昇腾Runtime |
|---|---|---|
| 编程模型 | SPMD(每线程独立执行) | Task-based(任务队列驱动) |
| Stream | CUDA Stream(类似) | aclrtStream(类似,但调度策略不同) |
| 内存模型 | 统一内存(可选) | Host/Device分离(必须显式拷贝) |
| kernel launch | <<<grid, block>>>语法 | aclrtLaunchKernel函数调用 |
| 错误处理 | 异步错误(容易吞错) | 同步返回码(更安全) |
| 动态shape | 原生支持 | 需要预声明dynamic batch |
最大差异:编程模型。
CUDA是SPMD(Single Program Multiple Data)------写一个kernel函数,GPU上成千上万个线程同时执行同一个函数。
昇腾是Task-based------写一个Task(算子),Runtime把Task调度到NPU的执行单元(Cube/Vector)上执行。不需要你关心"几号线程在干什么",Runtime自动调度。
这意味着:在昇腾上,你调优的重点不是"线程怎么分配",而是"Stream怎么安排、数据怎么搬运、计算和通信怎么重叠"。
五、实战:用Runtime跑一个完整的推理流程
#include "acl/acl.h"
int main() {
// 1. 初始化
aclInit(nullptr);
aclrtSetDevice(0);
aclrtCreateStream(&stream);
// 2. 加载模型
uint32_t modelId;
aclmdlLoadFromFile("llama.om", &modelId);
// 3. 准备输入
void* inputDev = nullptr;
aclrtMalloc(&inputDev, inputSize, ACL_MEM_MALLOC_HUGE_FIRST);
aclrtMemcpy(inputDev, inputSize, inputHost, inputSize,
ACL_MEMCPY_HOST_TO_DEVICE);
// 4. 执行推理
aclmdlDataset* input = aclmdlCreateDataset();
aclmdlAddDatasetBuffer(input, inputDev, inputSize);
aclmdlDataset* output = aclmdlCreateDataset();
// ... 准备output buffer ...
aclmdlExecute(modelId, input, output);
// 5. 获取输出
aclrtMemcpy(outputHost, outputSize, outputDev, outputSize,
ACL_MEMCPY_DEVICE_TO_HOST);
// 6. 清理
aclmdlUnload(modelId);
aclrtFree(inputDev);
aclrtDestroyStream(stream);
aclrtResetDevice(0);
aclFinalize();
return 0;
}
这是最简化的流程。实际生产环境要复杂得多------多Stream流水线、内存池复用、Dynamic Batch处理等。
六、那些你可能想问的问题
Q1:Runtime和Driver是什么关系?
A:Runtime是用户态库,Driver是内核态驱动。Runtime通过系统调用和Driver通信,Driver操作NPU硬件寄存器。用户代码只和Runtime交互,不直接调用Driver。
Q2:Runtime支持多进程吗?
A:支持。多个进程可以同时使用不同的NPU设备(不同deviceId),或通过Virtual Function(VF)共享同一张NPU卡。但同一进程内,同一Device的Context有互斥限制。
Q3:aclrtMemcpy和cudaMemcpy有什么区别?
A:接口类似,但昇腾的Memcpy支持更多优化:Huge Page传输、异步DMA(通过MTE引擎)、跨Device传输(通过HCCS链路)。但注意,昇腾没有"统一内存"(Unified Memory),必须显式指定拷贝方向(H2D/D2H/D2D)。
Q4:Runtime有性能分析工具吗?
A:有。msprof(MindStudio Profiler)可以采集Runtime层面的性能数据:Stream执行时间、内存拷贝耗时、Device利用率等。命令:msprof --application=./my_app --output=./prof_data
结尾
回到开头那个故事。
杭州那个AI实验室的工程师,后来把排查过程写成了内部文档,标题叫《从GPU到NPU:一个Runtime调优的血泪史》。
文档最后写了一句话:"GPU给你的是自由(线程随便跑),NPU给你的是秩序(任务有序调度)。自由容易浪费,秩序需要设计。"
Runtime就是那个"秩序"的守护者。
它藏在最底层,没有花哨的API,没有炫酷的算子,但它决定了上层一切组件能不能跑得起来、跑得快不快。
灵脉无声,根基不动。但根基稳了,上面才能修出万丈高楼。
仓库地址: