在高性能异构计算栈中,Runtime 仓库所代表的执行引擎是连接上层 AI 模型框架与底层硬件加速单元的动态核心。它不仅负责加载和实例化编译后的计算图,更要在 LLM 推理这种对延迟和内存带宽有极高要求的场景下,实现精细化的资源调度、算子生命周期管理以及异构核函数的无缝切换。

CANN 组织链接https://atomgit.com/cann
Runtime 仓库链接https://atomgit.com/cann/runtime


1. 基础执行环境的建立与资源抽象管理

Runtime 在计算开始前必须初始化一个隔离的执行环境,并为所有计算任务抽象底层硬件的复杂性。

1.1 执行会话(Session)的生命周期控制

一个推理任务的完整生命周期由 Runtime 统一管理,确保资源可控。

  • 全局状态初始化:为每次模型推理(或连续的推理流)创建一个独立的执行会话。该会话封装了设备句柄、内存池的句柄,以及全局的并发控制机制(如互斥锁或信号量)。
  • 流(Stream)的并发调度:Runtime 抽象出执行流的概念,允许开发者或编译器将图中的非依赖部分分配到不同流中并行执行,最大化硬件的并行处理能力。例如,在 LLM 解码中,不同时间步的生成任务可能被分配到不同的流中,实现流水线加速。

1.2 异构内存的统一视图与管理

LLM 推理(特别是 KV Cache)对内存的访问模式和容量要求极高,Runtime 必须对设备内存进行精细化管理。

  • 内存池化机制:Runtime 集成内存分配器,从系统级预留一大块设备内存,并通过分块和复用的策略管理中间张量。这种机制极大地减少了昂贵的设备内存动态申请开销。
  • 零拷贝与数据一致性:Runtime 优先在 Host 和 Device 之间建立映射关系,以支持零拷贝(Zero-Copy)访问。对于 KV Cache 这种热点数据,它确保数据在更新后能被所有依赖它的核函数即时看到,保证数据一致性。

2. 算子实例的构建与异构核函数的调度

Runtime 接收已编译的执行图(Static Graph),并负责将图中的节点转化为实际执行的指令。

2.1 执行对象(Execution Object)的实例化

每个图节点在 Runtime 中都对应一个实例化的执行对象,该对象携带了所有运行时的信息。

  • 信息聚合:执行对象聚合了来自 MetaDef 的静态属性(如 Padding、Stride)、输入输出张量的物理设备地址(Device Pointers),以及指向底层核函数的入口地址。
  • 算子类型识别:Runtime 必须能区分当前对象是来自 ops-nn 的高性能算子、自定义的 Ascend C 核函数,还是 GE 融合出的复杂算子,并据此调用对应的启动逻辑。

2.2 核函数启动的参数注入

硬件加速单元启动需要精确的配置参数,Runtime 负责完成属性到硬件配置的转换。

  • 属性解析与转换:Runtime 会解析 MetaDef 声明的配置属性(Attributes),并将其转化为底层硬件启动所需的寄存器值或参数结构体。

    // Runtime 启动底层核函数的概念流程片段
    void LaunchKernel(const ExecutionContext* ctx, 
                      const TensorDesc* inputs[], 
                      const TensorDesc* outputs, 
                      const OpAttributes* attrs) {
        
        // 1. 从 attrs 中解析出 Tiling 块大小和精度模式
        int32_t block_size = attrs->GetAttribute<int32_t>("block_dim");
        
        // 2. 构建硬件启动参数结构体
        KernelLaunchParam launch_param = PrepareLaunchParameters(block_size, ctx->stream_id);
    
        // 3. 调用底层算子实现,例如 ops-nn 编译后的入口
        call_optimized_matmul_v3(inputs[0], inputs[1], outputs[0], launch_param);
    }
    

2.3 动态形状的运行时适应

LLM 解码阶段要求 Runtime 具备处理运行时变化的形状的能力。

  • 运行时维度绑定:当输入 Batch 或 Sequence 维度在运行时确定时,Runtime 必须能在启动前捕获这些维度,并根据它们选择合适的 Tiling 策略或触发 JIT 编译路径,以确保核函数能够正确处理当前数据布局。

3. 性能监控与调试支持的深度集成

作为计算的执行点,Runtime 是性能分析工具获取精确时间信息的唯一来源。

3.1 精确的时间戳捕获机制

为了进行细粒度的性能分析,Runtime 在所有算子执行边界注入了精确的时间度量点。

  • 事件标记插入:在启动一个核函数之前(记录进入时间)和核函数返回之后(记录退出时间),Runtime 会触发硬件级的时间戳捕获。这些时间戳被关联到具体的算子 ID、执行流和图节点上。
  • 延迟分析基础:这些精确的起止时间是所有后续性能分析(如计算时间、等待时间、内存搬运时间)的基础数据。

3.2 错误处理与上下文回溯

当硬件执行失败时,Runtime 必须能够提供清晰的错误诊断信息。

  • 错误捕获与封装:捕获底层硬件返回的错误码,并将其封装成对上层框架友好的异常信息。
  • 执行路径回溯:Runtime 维护了当前执行任务的依赖链。在发生错误时,它可以回溯到导致错误的特定算子、其配置的属性,甚至可以提供失败时输入张量的快照信息,从而简化复杂 LLM 模型部署中的调试过程。

4. 算子生命周期的终结与资源释放

推理任务的完成并不意味着资源被立即释放;Runtime 必须智能地回收资源。

4.1 中间张量的延迟回收策略

为了应对高并发的推理请求,内存的快速复用至关重要。

  • 依赖计数器:Runtime 维护一个内部依赖计数器。中间张量的内存只有在其所有下游依赖的算子都执行完毕后,才会被标记为可回收状态,随后释放回内存池。
  • KV Cache 状态管理:在 LLM 场景下,KV Cache 的内存需要维持跨批次(或跨步进)的有效性。Runtime 必须区分“有效数据”和“可回收内存”,确保在处理新的生成请求时,只有需要的部分被重置或覆盖。

4.2 会话清理与环境重置

推理任务完成后,Runtime 负责彻底清理其占用的所有资源。

  • 句柄关闭:释放所有设备内存分配、关闭异步流,并销毁执行会话中创建的同步对象。
  • 资源池重置:确保内存池恢复到干净状态,为下一个完全独立的推理任务做好准备。

5. 框架层面的适配与算子融合的最终协调

Runtime 必须作为算子框架的接口层,确保上层框架的意图能够被完美地映射到异构硬件上。

5.1 算子融合实例的运行时识别

当 GE 引擎将多个基础算子(如 Conv + BN + ReLU \text{Conv} + \text{BN} + \text{ReLU} Conv+BN+ReLU)融合为一个新的算子时,Runtime 必须能识别这个新的实体。

  • 融合 ID 注册:Runtime 接收来自 GE 的融合算子定义,并为其分配一个唯一的 ID,并注册其属性和执行入口。
  • 原子化执行:一旦图加载完毕,Runtime 将这个融合实例作为一个原子操作调度,避免了中间结果在设备内存中写入和读取的开销,这是 LLM 性能优化的关键技术之一。

5.2 兼容性层与版本切换的仲裁

Runtime 仲裁不同版本的算子实现。

  • API 抽象与适配:Runtime 内部维护了从通用框架 API 到 CANN 内部算子描述符的转换层,处理因框架版本更新导致的 API 差异。
  • 版本仲裁:当存在多个兼容的算子实现时(例如,旧的 MatMulops-nn 新增的 BatchMatMulV3),Runtime 根据图的编译信息或运行时配置,选择性能最优或兼容性最好的版本进行实例化和执行。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐