昇腾CANN的“灵脉根基“:Runtime仓库探秘

前言

杭州某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)

⚠️ 踩坑点:aclrtSetDeviceaclrtResetDevice 必须配对使用。忘记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,没有炫酷的算子,但它决定了上层一切组件能不能跑得起来、跑得快不快。

灵脉无声,根基不动。但根基稳了,上面才能修出万丈高楼。

仓库地址:

https://atomgit.com/cann/runtime

相关推荐
5201-7 小时前
ops-conv:卷积算子从 CPU 到昇腾 NPU 的优化之路
人工智能·深度学习
HIT_Weston8 小时前
92、【Agent】【OpenCode】edit 工具提示词
人工智能·agent·opencode
Shan12058 小时前
机器学习评价指标之基础指标与综合指标
人工智能·机器学习
硅谷秋水8 小时前
智体Harness工程:综述(下)
人工智能·深度学习·机器学习·语言模型
KaMeidebaby8 小时前
卡梅德生物技术快报|抗独特型抗体开发:半抗原检测技术瓶颈拆解,抗独特型抗体开发工程化实践
前端·数据库·人工智能·其他·百度·新浪微博
NiceCloud喜云8 小时前
Claude Files API 深入:从上传、复用到配额管理的工程化指南
android·java·数据库·人工智能·python·json·飞书
ujainu8 小时前
CANN pto-isa:虚拟指令集如何连接编译与执行
android·ascend
oo哦哦8 小时前
2026年多平台内容管理系统技术选型:从架构设计到工程落地
人工智能·线性代数·矩阵