3. NPU算子调度原语

NPU算子调度原语 学习卡片

任务说明

本案例目标是为NPU后端定义两类核心调度原语——数据搬运与矩阵计算,并将其注册到TVM调度器中,使得用户可以通过标准TVM调度API(如tir.schedule)显式控制NPU算子的执行顺序与资源映射。完成本案例后,你将获得一个可扩展的原语注册框架,后续NPU算子可基于此框架添加新的调度原语。

NPU调度原语设计

NPU与通用GPU的关键差异在于:数据搬运(DMA)与矩阵计算(MAC)是分离的硬件单元,且共享有限的片上缓冲区(SRAM)。因此调度原语需要显式表达:

  • 搬运原语:将数据从全局内存(DRAM)搬运至片上SRAM,或反向搬回;
  • 计算原语:在SRAM数据就绪后触发矩阵乘加运算;
  • 同步语义:计算必须等待其输入搬运完成,搬运输出必须等待计算完成。

本案例实现一个轻量级调度器扩展,通过TVM的tir.schedule自定义指令机制,将上述原语注册为可调度的“外部指令”,并提供一个最小NPU算子示例展示原语如何被调用与验证。

代码实现

// npu_schedule_primitives.cc
// 依赖:TVM 0.12+,需链接 libtvm
#include <tvm/tir/schedule/schedule.h>
#include <tvm/tir/expr.h>
#include <tvm/runtime/registry.h>
#include <string>
#include <vector>

namespace tvm {
namespace tir {

// 1. 定义NPU原语参数结构
struct NPUCopyParams {
  std::string direction;  // "dram_to_sram" 或 "sram_to_dram"
  int src_buffer_id;
  int dst_buffer_id;
  int bytes;
};

struct NPUMatMulParams {
  int a_buffer_id;   // 输入矩阵A所在SRAM buffer
  int b_buffer_id;   // 输入矩阵B所在SRAM buffer
  int c_buffer_id;   // 输出矩阵C所在SRAM buffer
  int m, n, k;       // 矩阵维度
};

// 2. 将原语编码为TIR外部指令(通过StringImm标记)
Instruction CopyToSRAM(NPUCopyParams p) {
  return Instruction::Make(
      /*kind=*/"npu_copy",
      /*inputs=*/{},
      /*outputs=*/{},
      /*attrs=*/{p.direction, std::to_string(p.src_buffer_id),
                 std::to_string(p.dst_buffer_id), std::to_string(p.bytes)});
}

Instruction ComputeMatMul(NPUMatMulParams p) {
  return Instruction::Make(
      /*kind=*/"npu_matmul",
      /*inputs=*/{},
      /*outputs=*/{},
      /*attrs=*/{std::to_string(p.a_buffer_id),
                 std::to_string(p.b_buffer_id),
                 std::to_string(p.c_buffer_id),
                 std::to_string(p.m), std::to_string(p.n),
                 std::to_string(p.k)});
}

// 3. 注册到TVM调度器:使原语可被Schedule::add_instruction调用
TVM_REGISTER_GLOBAL("npu.schedule.copy_to_sram")
    .set_body_typed([](Schedule sch, int src_id, int dst_id, int bytes,
                       String direction) {
      NPUCopyParams p{direction, src_id, dst_id, bytes};
      sch->AddInstruction(CopyToSRAM(p));
      return sch;
    });

TVM_REGISTER_GLOBAL("npu.schedule.matmul")
    .set_body_typed([](Schedule sch, int a_id, int b_id, int c_id,
                       int m, int n, int k) {
      NPUMatMulParams p{a_id, b_id, c_id, m, n, k};
      sch->AddInstruction(ComputeMatMul(p));
      return sch;
    });

// 4. 查询已添加的原语(用于验证)
TVM_REGISTER_GLOBAL("npu.schedule.get_instructions")
    .set_body_typed([](Schedule sch) {
      auto insts = sch->GetInstructions();
      std::vector<std::string> result;
      for (auto& inst : insts) {
        result.push_back(inst->kind->name_hint);
      }
      return result;
    });

}  // namespace tir
}  // namespace tvm

调度原语集成与调用示例

// 使用示例(Python侧通过TVM FFI调用)
// 假设已创建Schedule对象sch,并分配了buffer id:0=DRAM输入, 1=SRAM输入, 2=SRAM输出
import tvm
from tvm import tir

# 创建空调度
sch = tir.Schedule()

# 1. 数据搬运:DRAM(0) -> SRAM(1),搬运4096字节
tvm.get_global_func("npu.schedule.copy_to_sram")(sch, 0, 1, 4096, "dram_to_sram")

# 2. 矩阵计算:SRAM(1)与SRAM(2)计算,输出到SRAM(3),维度64x64x64
tvm.get_global_func("npu.schedule.matmul")(sch, 1, 2, 3, 64, 64, 64)

# 3. 数据搬回:SRAM(3) -> DRAM(4)
tvm.get_global_func("npu.schedule.copy_to_sram")(sch, 3, 4, 4096, "sram_to_dram")

# 4. 验证原语已注册
insts = tvm.get_global_func("npu.schedule.get_instructions")(sch)
print(insts)  # 预期输出: ["npu_copy", "npu_matmul", "npu_copy"]

验证方法

本案例的验证分为两层:

  1. 编译验证:将上述C++代码编译为共享库(g++ -shared -fPIC -o libnpu_sched.so npu_schedule_primitives.cc -ltvm),在Python中加载(tvm.load_lib("libnpu_sched.so"))后执行调用示例,观察get_instructions返回的指令序列是否与添加顺序一致。
  2. 语义验证:检查npu_copy与npu_matmul指令的attrs字段是否完整编码了搬运方向、buffer id与矩阵维度。可通过sch->GetInstructions()[i]->attrs读取并断言关键值。

判定标准:若编译无错误,且Python侧能依次调用三个注册函数并最终打印出["npu_copy", "npu_matmul", "npu_copy"],同时每个指令的attrs字段与传入参数一致,则判定本案例通过。注意:本案例仅验证调度原语的注册与参数编码,不涉及实际NPU硬件执行。

NPU调度原语数据流与指令序列 DRAM (全局) buffer 0, 4 SRAM (片上) buffer 1, 2, 3 MAC阵列 矩阵乘加 ①copy ②matmul ③copy回 调度器中的指令序列 npu_copy #0 dram_to_sram src=0 dst=1 bytes=4096 npu_matmul #1 a=1 b=2 c=3 64x64x64 npu_copy #2 sram_to_dram src=3 dst=4 bytes=4096 get_instructions() → ["npu_copy", "npu_matmul", "npu_copy"]