前言CANN五层架构里Runtime排在第四层——昇腾计算执行层。上面是编译层GE图编译器下面是基础层驱动DRV。如果把CANN比作一个工厂GE是排产计划系统算子库是生产工具那Runtime就是车间调度——它负责把编译好的任务分配到具体的NPU上执行管理计算资源、内存资源和流Stream资源确保任务按序完成。很多人用AscendCL写推理代码的时候其实在不知不觉中就在调用Runtime的接口——aclrtMalloc分配内存、aclrtLaunchKernel启动算子、aclrtSynchronizeStream等待完成这些都是Runtime提供的核心能力。CANN开源社区里的runtime仓库在atomgit.com/cann上可以访问是理解昇腾NPU执行模型的关键入口。Runtime在CANN架构中的位置先把Runtime在整个执行链路中的角色定位清楚。一个模型从PyTorch代码到NPU上跑起来经历这样一条链路PyTorch前端 → TorchAir适配层 → GE图编译器 → 生成离线模型om文件 → Runtime加载执行。前半段PyTorch到om文件是编译期由GE和算子库负责。后半段加载om文件到执行完成是运行期由Runtime负责。Runtime具体管什么拆开来看有五个核心职责设备管理。Runtime负责发现和管理系统中的昇腾NPU设备提供aclrtSetDevice/aclrtResetDevice接口来选择和释放设备。多卡场景下每个进程通过SetDevice绑定到一张特定的NPU后续所有操作都在这张卡上执行。内存管理。昇腾NPU有两种内存Device MemoryHBM高带宽内存和Host MemoryCPU侧内存。Runtime提供aclrtMalloc/aclrtFree管理Device MemoryaclrtMallocHost/aclrtFreeHost管理Host Memory还有aclrtMemcpy做Host和Device之间的数据搬运。内存管理的细节直接决定了推理的性能——频繁的Host-Device搬运是常见的性能瓶颈。流管理。Stream是昇腾NPU上的任务队列所有算子调用都提交到Stream上按序执行。Runtime提供aclrtCreateStream/aclrtDestroyStream创建和销毁StreamaclrtSynchronizeStream等待Stream上所有任务完成。多Stream并行是提升NPU利用率的关键手段——如果你有独立的计算任务和数据搬运任务可以分别放在不同的Stream上并行执行。事件管理。Event是Stream之间同步的机制。一个Stream上可以Record一个Event另一个Stream可以Wait这个Event从而实现跨Stream的同步。这在双流并行推理中很常见Stream 0做计算Stream 1做数据搬运Stream 1通过Event等待Stream 0的计算结果就绪后才开始搬运。模型和算子执行。Runtime提供aclmdlLoadFromFile/aclmdlExecute加载和执行离线模型以及aclrtLaunchKernel启动单个Ascend C算子。这是Runtime最核心的能力——把编译产物变成NPU上真正运行的指令。Runtime的核心接口拆解上面列了五大职责下面用代码示例逐个拆解关键接口。以一个典型的模型推理流程为例#includeacl/acl.h#includecstdiointmain(){// 1. 初始化AscendCL运行时// aclInit读取CANN的配置文件初始化驱动层和运行时层// 为什么放在最前面因为后续所有ACL接口都依赖运行时初始化完成aclError retaclInit(nullptr);if(ret!ACL_SUCCESS){printf(aclInit failed, ret%d\n,ret);return-1;}// 2. 选择NPU设备// device_id0表示使用第一张NPU卡// SetDevice会创建该设备的Context后续操作默认在这个Context下执行intdevice_id0;retaclrtSetDevice(device_id);if(ret!ACL_SUCCESS){printf(aclrtSetDevice failed, ret%d\n,ret);return-1;}// 3. 创建Stream// Stream是任务队列所有异步操作提交到Stream上// 为什么不直接在默认Stream上跑因为自定义Stream可以做双流并行// 默认Stream是串行的无法和其他Stream并行aclrtStream streamnullptr;retaclrtCreateStream(stream);// 4. 加载离线模型// om文件是GE编译后的产物包含了计算图和算子实现// LoadFromFile把om文件加载到Device Memory返回model_id用于后续执行aclmdlModel model_id;retaclmdlLoadFromFile(resnet50.om,model_id);// 5. 分配输入输出内存// aclrtMalloc分配Device MemoryACL_MEM_MALLOC_HUGE_FIRST优先使用大页内存// 为什么用大页减少TLB Miss对大张量的连续访问更友好size_t input_size224*224*3*sizeof(float);// ResNet50输入size_t output_size1000*sizeof(float);// 1000类概率void*input_bufnullptr,*output_bufnullptr;aclrtMalloc(input_buf,input_size,ACL_MEM_MALLOC_HUGE_FIRST);aclrtMalloc(output_buf,output_size,ACL_MEM_MALLOC_HUGE_FIRST);// 6. Host到Device数据搬运// 准备输入数据并拷贝到Device Memory// 为什么分两步因为Host和Device的内存地址空间不同// 必须通过PCIe DMA搬运不能直接指针访问floatinput_data[224*224*3];prepare_input(input_data);// 假设这个函数准备输入数据aclrtMemcpy(input_buf,input_size,input_data,input_size,ACL_MEMCPY_HOST_TO_DEVICE);// 7. 执行推理// aclmdlExecute是异步的任务提交到Stream后立即返回// 实际执行在NPU上进行和Host端的CPU并行aclmdlDataset*input_datasetcreate_input_dataset(input_buf,input_size);aclmdlDataset*output_datasetcreate_output_dataset(output_buf,output_size);retaclmdlExecute(model_id,input_dataset,output_dataset,stream);// 8. 等待推理完成// SynchronizeStream会阻塞Host线程直到Stream上所有任务完成// 为什么不在Execute之后直接读output_buf因为Execute是异步的// 此时NPU可能还在计算直接读会拿到不完整的数据aclrtSynchronizeStream(stream);// 9. Device到Host数据搬运floatoutput_data[1000];aclrtMemcpy(output_data,output_size,output_buf,output_size,ACL_MEMCPY_DEVICE_TO_HOST);// 10. 清理资源aclrtFree(input_buf);aclrtFree(output_buf);aclmdlUnload(model_id);aclrtDestroyStream(stream);aclrtResetDevice(device_id);aclFinalize();return0;}这段代码覆盖了Runtime最核心的接口调用流程。有几个要点值得单独展开。内存管理的深层细节aclrtMalloc分配Device Memory时有几种内存分配策略ACL_MEM_MALLOC_HUGE_FIRST——优先使用大页内存2MB或1GB的大页适合大张量的场景。大页内存的好处是减少TLB Miss对于ResNet50这种输入张量尺寸固定的模型使用大页可以让PCIe DMA搬运更高效。ACL_MEM_MALLOC_NORMAL_FIRST——优先使用普通页内存4KB适合小张量或者内存碎片较多的场景。大页内存是有限的资源如果分配过多会导致后续大页分配失败。ACL_MEM_MALLOC_HUGE_ONLY——只使用大页内存分配失败则返回错误。适用于对性能要求极致、且能确保大页内存充足的场景。Device Memory的管理有一个容易踩的坑内存池复用。Runtime内部维护了一个Device Memory的内存池aclrtFree释放的内存不会立即归还给驱动而是缓存在内存池里供后续aclrtMalloc复用。这意味着如果你在推理过程中反复分配和释放同样大小的缓冲区实际开销只有第一次分配后续都是池内复用速度很快。但这也意味着aclrtFree之后npu-smi工具显示的显存占用不会立即下降——这在调试显存泄漏的时候容易误导人。Host Memory的管理同样需要注意。aclrtMallocHost分配的Host Memory是锁页内存Pinned Memory不会被操作系统换出到磁盘。PCIe DMA只能访问锁页内存所以aclrtMemcpy的Host端地址必须是锁页内存或者通过aclrtMallocHost分配的。如果你传了一个普通的malloc地址给aclrtMemcpyRuntime会先把它拷贝到内部的锁页缓冲区再做DMA搬运多了一次内存拷贝的开销。流和事件的并行模型Stream是理解Runtime并行模型的关键。默认情况下每个设备有一个Default Stream所有没有显式指定Stream的ACL操作都会提交到Default Stream上。Default Stream上的操作是串行的——前一个操作完成后才开始下一个。要实现并行必须创建多个Stream。一个典型的双流推理架构aclrtStream compute_stream,copy_stream;aclrtCreateStream(compute_stream);// 计算流aclrtCreateStream(copy_stream);// 搬运流// 推理循环中的双流并行for(inti0;ibatch_count;i){// copy_stream搬运第i1个batch的输入到Device// 为什么不和计算串行因为PCIe搬运和NPU计算可以并行// 搬运第i1个输入的同时NPU在第i个输入上做推理if(i1batch_count){aclrtMemcpyAsync(input_buf,input_size,host_input[i1],input_size,ACL_MEMCPY_HOST_TO_DEVICE,copy_stream);}// compute_stream执行第i个batch的推理aclmdlExecute(model_id,input_dataset,output_dataset,compute_stream);// compute_stream等待copy_stream的搬运完成仅对下一个batch需要// 为什么用Event而不是SynchronizeStream// SynchronizeStream会阻塞Host线程直到copy_stream上所有操作完成// Event只阻塞compute_stream上的后续操作Host线程可以继续做其他事if(i0){aclrtEvent copy_done;aclrtCreateEvent(copy_done);aclrtRecordEvent(copy_done,copy_stream);aclrtStreamWaitEvent(compute_stream,copy_done);}}这个双流模型在连续推理场景下效果显著。当NPU在处理第i个batch的推理时PCIe同时在搬运第i1个batch的输入数据计算和搬运的时间重叠在一起总吞吐量可以提升30%到50%。Event的使用有一个注意点Event对象创建后可以复用但RecordEvent必须在Stream上按序调用。如果你在同一个Stream上连续Record多个EventWaitEvent会等待到最近一次Record的位置而不是所有Record都完成。实际使用中建议每次Wait之前都重新Record避免复用导致的同步错误。模型下沉和动态ShapeRuntime加载om文件后模型的计算图已经固化了——包括算子序列、Tiling参数、内存排布等。这意味着模型的输入Shape必须是固定的或者至少在编译时声明的Shape范围内。但实际业务中输入Shape往往是动态的——比如NLP模型的序列长度、目标检测模型的batch size都可能变化。CANN 8.0开始支持动态Shape需要在模型编译时通过GE的–dynamic_batch_size或–dynamic_image_size参数声明支持的Shape范围Runtime在执行时通过aclmdlSetDatasetBuffer动态指定当前batch的实际Shape。动态Shape的代价是性能下降。因为GE在编译时无法针对特定Shape做极致的Tiling优化只能生成通用的Tiling策略。实测数据固定Shape的ResNet50推理吞吐约1200 images/s支持动态batch1-32后吞吐下降到约950 images/s降幅约20%。如果业务允许建议对高频Shape分别编译专用的om模型Runtime根据当前batch size选择对应的模型加载执行比通用动态Shape方案的性能更好。使用前后效率对比以一个实际的推理服务为例ResNet50图像分类batch size32连续推理1000个batch。对比维度单流串行推理双流并行推理双流大页内存单batch延迟8.2ms8.2ms7.8ms吞吐量images/s390051005600Host-Device搬运耗时占比23%7%被计算掩盖5%被计算掩盖NPU利用率68%82%87%显存占用1.2GB1.8GB双缓冲1.8GB代码复杂度低中中双流并行的效果很直观NPU利用率从68%提升到82%吞吐量提升30%。加上大页内存后TLB Miss减少吞吐量再提升约10%。显存占用的增加是因为双流需要两份输入缓冲区——当前batch和下一个batch的输入同时存在于Device Memory中。这是空间换时间的典型权衡。再来看动态Shape对性能的影响Shape策略编译时配置推理吞吐batch32推理吞吐batch1固定Shape batch32–batch_size325600 images/s不支持固定Shape batch1–batch_size1不支持210 images/s动态Shape batch1-32–dynamic_batch_size1,4,8,16,324500 images/s180 images/s双om模型分别编译两个om5600 images/s切模型210 images/s动态Shape在batch32时比固定Shape慢约20%在batch1时慢约14%。如果业务上同时需要batch1和batch32双om模型方案虽然需要额外的显存加载两个模型但性能比动态Shape方案好25%以上。Runtime和GE的边界开发者在实际使用中经常困惑哪些事情是Runtime做的哪些是GE做的边界在哪里编译期的事情归GE图优化、算子融合、Tiling计算、内存规划。这些操作发生在模型编译阶段产出的om文件包含了所有编译期决策的结果。运行期的事情归Runtime设备管理、内存分配、任务提交、同步等待。Runtime拿到om文件后按照里面的指令序列在NPU上执行不做额外的优化决策。一个常见的误解是Runtime会在运行时做算子融合。实际上不会。Runtime只是执行GE已经融合好的算子序列。如果你在运行时发现两个算子没有被融合那问题在编译期——GE的图优化Pass没有识别到融合机会或者融合条件不满足。需要回到GE侧排查不是Runtime的问题。结尾Runtime是CANN架构中离硬件最近的软件层驱动层更近但对开发者不可见它的核心价值是提供一套统一的资源管理和任务执行接口让开发者不需要直接操作硬件寄存器就能驱动昇腾NPU工作。理解Runtime的内存模型、流和事件的并行机制、动态Shape的性能代价对写出高性能的推理服务至关重要。如果你在做昇腾NPU上的模型部署Runtime是你每天都要打交道的接口层值得花时间深入理解。仓库地址https://atomgit.com/cann/runtime