Runtime不是跑kernel的——它是昇腾CANN里的执行层
前言昇腾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做了这几件事内存管理管理NPU的HBMHigh Bandwidth Memory包括内存分配、释放、地址映射流管理管理执行流stream支持多流并发和流间同步kernel部署把编译好的kernel部署到NPU上包括资源分配和参数配置任务调度调度task的执行顺序处理依赖关系设备管理能力管理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/VDMA比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的shapebatch, num_heads, seq_len, head_dim // - Softmax的缩放因子1/sqrt(head_dim) // - 在线Softmax的初始状态m-inf, l0 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在里面做了什么aclrtMallocRuntime的内存分配器。它不只是调用malloc——它需要考虑NPU内存的对齐要求通常128字节对齐、内存bank分配策略尽量让连续访问落在同一个bank、以及内存碎片问题。对于FlashAttention这种需要分配多块内存Q/K/V/O的算子Runtime会尽量让它们物理上连续减少地址转换开销。aclrtMemcpyRuntime的数据搬运。它底层调用的是DMADirect Memory Access引擎可以在不占用NPU计算单元的情况下搬运数据。但这里有个细节如果src和dst都在NPU上设备到设备Runtime会走另一条路径——直接NPU内部拷贝不经过CPU。aclrtCreateStream创建一个执行流。NPU支持多流并发不同stream上的kernel可以并行执行只要没有依赖关系。FlashAttention这种计算密集的kernel通常会独占一个stream。aclrtLaunchKernel这是Runtime最核心的API。它做了这几件事把kernel的二进制代码加载到NPU的代码区配置kernel的执行参数grid/block维度、shared memory大小等把参数写到NPU的参数区向NPU的调度器提交一个执行任务返回异步不等待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尽量分配物理连续的大页内存。但如果你直接用默认参数调用aclrtMallocRuntime的默认分配器不一定能做好这个优化。Runtime的流管理对多batch推理的影响另一个Runtime影响性能的地方是流管理。做多batch推理的时候通常的做法是把多个batch放到同一个kernel里处理kernel支持batch维度。但有时候batch之间的计算是独立的可以用多流并行。Runtime的流管理器需要处理几件事多流并发执行只要资源够流间同步通过event机制资源隔离不同流不能用同一个硬件资源FlashAttention在多batch场景下如果batch之间没有依赖可以让不同batch跑在不同stream上。但NPU的计算资源CUBE/AIV是有限的Runtime的调度器需要决定怎么把这些资源分配给不同stream。实测数据Llama 3 70Bbf16Ascend 910batch1单流执行28 tokens/s两流并发batch242 tokens/s不是56因为有资源争用四流并发batch451 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的kernelRuntime会把这个FlashAttention kernel排队等到有CUBE资源了再调度。这个排队机制是Runtime的任务调度器的一部分。它用的是优先级队列——高优先级的stream上的任务先得到资源。可以直接看Runtime的源码吗Runtime是CANN的开源组件它的源码在https://atomgit.com/cann/runtime 。如果你想深入理解Runtime的工作原理建议从这几个地方入手runtime仓库里的aclrt目录Runtime的API实现ops-transformer里调用Runtime API的代码示例搜索aclrt调用CANN社区里的Runtime调优指南cann-learning-hub仓库runtime仓库的代码量不小建议先从aclrtMalloc和aclrtLaunchKernel的实现看起——这两个API覆盖了Runtime最核心的两个功能内存管理和任务调度。runtime仓库地址https://atomgit.com/cann/runtimecann-learning-hubRuntime调优指南https://atomgit.com/cann/cann-learning-hub