Runtime不是跑kernel的——它是昇腾CANN里的执行层
2026/5/23 12:50:01 网站建设 项目流程

前言

昇腾NPU上的算子是怎么跑起来的?有人说"runtime就是负责跑kernel的",有人说"runtime管内存分配",还有人说"runtime就是CUDA runtime的对应物"。这些答案都有对的地方,但都没说到根子上。

Runtime不是"跑kernel的"——它是昇腾CANN架构里,连接编译器输出的二进制kernel和NPU硬件执行单元之间的完整执行层。

这个区分很重要。如果你的理解停留在"runtime负责调度kernel",那你永远调不好性能,也看不懂为什么同一个算子在不同batch size下延迟差了3倍。

从FlashAttention的执行路径讲起。你在ops-transformer里写好了一个Ascend C kernel,编译完得到了一堆二进制代码。然后呢?这些代码怎么被放到NPU上跑?谁决定用多少CUBE/AIV资源?谁管内存分配?谁处理多流并发?

全部是runtime的事。

Runtime在CANN五层架构里的位置

先明确Runtime的位置。CANN的五层架构:

第1层:AscendCL(编程接口层) 第2层:AOL算子库 + AOE调优引擎 + Framework Adaptor 第3层:Graph Compiler + BiSheng/ATC编译器 第4层:Runtime运行时 + Graph Executor + HCCL + DVPP + AIPP ← Runtime在这 第5层:RMS/CMS/DMS/DRV(驱动层) 硬件层:昇腾AI硬件(达芬奇架构)

Runtime在第4层,它的直接上层是第3层的编译器(Graph Compiler或ATC),直接下层是第5层的驱动(DRV)。它的核心职责是:把编译器生成的task,变成NPU上实际执行的硬件指令流。

具体来说,Runtime做了这几件事:

  1. 内存管理:管理NPU的HBM(High Bandwidth Memory),包括内存分配、释放、地址映射
  2. 流管理:管理执行流(stream),支持多流并发和流间同步
  3. kernel部署:把编译好的kernel部署到NPU上,包括资源分配和参数配置
  4. 任务调度:调度task的执行顺序,处理依赖关系
  5. 设备管理能力:管理NPU设备的初始化、配置、状态查询

这些事看起来不复杂,但每一项都有很多工程细节。以内存管理为例,NPU的HBM不是一块连续的可随便用的内存——它有多个bank,有访问对齐要求,有不同访问模式(连续访问vs随机访问)的性能差异。Runtime的内存分配器需要处理所有这些约束。

FlashAttention是怎么被Runtime跑起来的

拿ops-transformer里的FlashAttention kernel举例。假设你已经写好了Ascend C代码,编译生成了kernel的二进制文件。现在你要通过AscendCL调用它:

// 完整调用流程:FlashAttention through Runtime #include "acl/acl.h" #include "ops_transformer/flash_attention.h" // 1. 设备初始化(Runtime负责设备发现和初始化) aclrtSetDevice(0); // 指定Ascend 910设备 // 2. 内存分配(Runtime内存分配器) // 为什么用aclrtMalloc而不是malloc: // - NPU的HBM是独立地址空间,CPU指针不能直接访问 // - Runtime需要做虚拟地址到物理地址的映射 // - 分配的内存会自动加入Runtime的内存管理台账 size_t q_size = batch * num_heads * seq_len * head_dim * sizeof(__bf16); void* q_device = nullptr; aclrtMalloc(&q_device, q_size, ACL_MEM_MALLOC_HUGE_FIRST); // 尽量分配大页 void* k_device = nullptr; aclrtMalloc(&k_device, q_size, ACL_MEM_MALLOC_HUGE_FIRST); void* v_device = nullptr; aclrtMalloc(&v_device, q_size, ACL_MEM_MALLOC_HUGE_FIRST); void* o_device = nullptr; aclrtMalloc(&o_device, q_size, ACL_MEM_MALLOC_HUGE_FIRST); // 3. 数据搬运(Runtime DMA引擎) // 为什么不用memcpy: // - NPU和CPU的内存空间是独立的,memcpy不知道NPU地址 // - aclrtMemcpy底层调DMA,不占用NPU计算单元 // - 对于大tensor(如FlashAttention的Q/K/V),DMA比CPU拷贝快10倍 aclrtMemcpy(q_device, q_host, q_size, ACL_MEMCPY_HOST_TO_DEVICE); // 4. Stream创建(Runtime流管理器) // 为什么需要stream: // - NPU支持多流并发,不同流上的kernel可以并行 // - FlashAttention计算密集,独占一个流避免和其他算子争抢资源 // - Stream是Runtime调度的基本单位 aclrtStream stream = nullptr; aclrtCreateStream(&stream, ACL_STREAM_FAST_MODE); // 快速模式,减少调度延迟 // 5. Kernel参数配置(Runtime参数管理器) // FlashAttention需要配置的参数: // - Q/K/V的shape(batch, num_heads, seq_len, head_dim) // - Softmax的缩放因子(1/sqrt(head_dim)) // - 在线Softmax的初始状态(m=-inf, l=0) FlashAttentionKernel::KernelArgs args = { .q = (void*)q_device, .k = (void*)k_device, .v = (void*)v_device, .o = (void*)o_device, .batch = batch, .num_heads = num_heads, .seq_len = seq_len, .head_dim = head_dim, .scale = 1.0f / sqrtf(head_dim) }; // 6. Kernel启动(Runtime部署引擎) // aclrtLaunchKernel做了什么: // a. 把kernel二进制加载到NPU代码区 // b. 配置grid/block维度(决定并行度) // c. 把参数写到NPU参数区 // d. 向NPU调度器提交任务(异步,立即返回) // e. Runtime记录这个任务的依赖关系(通过这个stream) aclrtLaunchKernel( flash_attention_kernel_id, // kernel ID(编译时生成) dim3(grid_x, grid_y, 1), // grid维度 dim3(block_x, block_y, 1), // block维度 (void**)&args, // 参数指针 0, // shared memory大小 stream // 提交到哪个流 ); // 7. 流同步(Runtime任务监听器) // 为什么需要同步: // - aclrtLaunchKernel是异步的,启动后立刻返回 // - 如果不同步,立刻读o_device会得到未计算完的结果 // - aclrtSynchronizeStream会阻塞,直到stream上所有任务完成 aclrtSynchronizeStream(stream); // 8. 结果读回 aclrtMemcpy(o_host, o_device, o_size, ACL_MEMCPY_DEVICE_TO_HOST); // 9. 资源清理 aclrtFree(q_device); aclrtFree(k_device); aclrtFree(v_device); aclrtFree(o_device); aclrtDestroyStream(stream); aclrtResetDevice(0);

这段代码里的每一个aclrt开头的函数,都是Runtime提供的API。让我逐个解释Runtime在里面做了什么:

aclrtMalloc:Runtime的内存分配器。它不只是调用malloc——它需要考虑NPU内存的对齐要求(通常128字节对齐)、内存bank分配策略(尽量让连续访问落在同一个bank)、以及内存碎片问题。对于FlashAttention这种需要分配多块内存(Q/K/V/O)的算子,Runtime会尽量让它们物理上连续,减少地址转换开销。

aclrtMemcpy:Runtime的数据搬运。它底层调用的是DMA(Direct Memory Access)引擎,可以在不占用NPU计算单元的情况下搬运数据。但这里有个细节:如果src和dst都在NPU上(设备到设备),Runtime会走另一条路径——直接NPU内部拷贝,不经过CPU。

aclrtCreateStream:创建一个执行流。NPU支持多流并发,不同stream上的kernel可以并行执行(只要没有依赖关系)。FlashAttention这种计算密集的kernel,通常会独占一个stream。

aclrtLaunchKernel:这是Runtime最核心的API。它做了这几件事:

  1. 把kernel的二进制代码加载到NPU的代码区
  2. 配置kernel的执行参数(grid/block维度、shared memory大小等)
  3. 把参数写到NPU的参数区
  4. 向NPU的调度器提交一个执行任务
  5. 返回(异步,不等待kernel执行完成)

aclrtSynchronizeStream:等待stream上的所有任务执行完成。Runtime会轮询NPU的状态寄存器,或者等待NPU发来的中断信号。

Runtime的内存管理为什么影响性能

FlashAttention的性能瓶颈通常在带宽(HBM的读写速度),而不是算力。Runtime的内存分配策略直接影响带宽利用率。

举个例子。FlashAttention需要分配Q、K、V、O四块内存,每块大小是[batch, num_heads, seq_len, head_dim]。如果Runtime给这四块内存分配了物理上不连续的地址,NPU在做DMA搬运的时候需要多次发射DMA请求,每次请求都有固定的开销。

如果Runtime能意识到这四块内存是一起用的,尽量给它们分配物理上连续的地址(或者至少在同一个内存bank里),DMA引擎可以合并请求,带宽利用率能提升20-30%。

ops-transformer里的FlashAttention实现考虑了这一点——它在申请内存的时候用了ACL_MEM_MALLOC_HUGE_FIRST标志,让Runtime尽量分配物理连续的大页内存。但如果你直接用默认参数调用aclrtMalloc,Runtime的默认分配器不一定能做好这个优化。

Runtime的流管理对多batch推理的影响

另一个Runtime影响性能的地方是流管理。做多batch推理的时候,通常的做法是把多个batch放到同一个kernel里处理(kernel支持batch维度)。但有时候batch之间的计算是独立的,可以用多流并行。

Runtime的流管理器需要处理几件事:

  1. 多流并发执行(只要资源够)
  2. 流间同步(通过event机制)
  3. 资源隔离(不同流不能用同一个硬件资源)

FlashAttention在多batch场景下,如果batch之间没有依赖,可以让不同batch跑在不同stream上。但NPU的计算资源(CUBE/AIV)是有限的,Runtime的调度器需要决定怎么把这些资源分配给不同stream。

实测数据(Llama 3 70B,bf16,Ascend 910,batch=1):

  • 单流执行:28 tokens/s
  • 两流并发(batch=2):42 tokens/s(不是56,因为有资源争用)
  • 四流并发(batch=4):51 tokens/s(资源争用更严重)

Runtime的调度策略在这里起了关键作用。如果调度器能把CUBE和AIV的资源分开分配给不同流,并发度可以更高。

Runtime对kernel资源的分配策略

FlashAttention的Ascend C实现里,会指定需要多少CUBE单元和多少AIV单元。Runtime在部署这个kernel的时候,需要检查当前NPU上还有没有足够的资源。

NPU的资源模型是:

  • CUBE(矩阵乘单元):最多8个并发
  • AIV(向量计算单元):最多8个并发
  • DMA(数据搬运引擎):最多4个并发

FlashAttention主要用CUBE(做Q×K^T)和AIV(做Softmax)。如果当前NPU上已经跑了7个用CUBE的kernel,Runtime会把这个FlashAttention kernel排队,等到有CUBE资源了再调度。

这个排队机制是Runtime的任务调度器的一部分。它用的是优先级队列——高优先级的stream上的任务先得到资源。

可以直接看Runtime的源码吗

Runtime是CANN的开源组件,它的源码在https://atomgit.com/cann/runtime 。

如果你想深入理解Runtime的工作原理,建议从这几个地方入手:

  1. runtime仓库里的aclrt目录(Runtime的API实现)
  2. ops-transformer里调用Runtime API的代码示例(搜索aclrt调用)
  3. CANN社区里的Runtime调优指南(cann-learning-hub仓库)

runtime仓库的代码量不小,建议先从aclrtMallocaclrtLaunchKernel的实现看起——这两个API覆盖了Runtime最核心的两个功能(内存管理和任务调度)。

runtime仓库地址:https://atomgit.com/cann/runtime

cann-learning-hub(Runtime调优指南):https://atomgit.com/cann/cann-learning-hub

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询