大数跨境

CoralNPU 的 DMA 与计算流水:内核/外壳分层、内存空间、计算融合与同步

CoralNPU 的 DMA 与计算流水:内核/外壳分层、内存空间、计算融合与同步 ai算法芯片与系统
2026-11-14
0
导读:CoralNPU 是 Google 开源的端侧 RISC-V 加速器 IP,集成标量、RVV 向量与 VME 矩阵,以 TCM 加 DMA 实现确定性低功耗执行。用于 AI 眼镜、戒指等始终在线设备的

 

CoralNPU 的编程模型可以浓缩为一句话:内核只认 DTCM,外壳负责 DRAM↔DTCM 的 DMA 搬运,多个算子可在 DTCM 上直接融合,三者通过静态地址和显式同步粘合。这不是权宜之计,而是架构意图——把“确定性计算”和“长延迟内存”彻底解耦,让计算内核在任何 CoralNPU 变体上可复用,让数据搬运根据 DMA 能力独立演进,让 算子融合 因为数据已经在 DTCM 里而变得几乎零成本。

本文从内存层次与数据通路入手,逐层展开到内存空间表达、矩阵引擎的启动与等待接口、内核/外壳分层、DTCM 上的多算子融合、双缓冲流水、IR 与汇编的对应关系,最后回到这个模式的边界与代价。

📚 目录

一、内存层次与数据通路总览
二、内存空间:C 代码里怎么区分 TCM 和 DRAM
三、RVV 能直接从 DRAM 加载吗
四、矩阵引擎的启动与等待接口
五、内核/外壳分层:标准编程范式
六、DTCM 上的计算融合:零拷贝的算子流水
七、双缓冲:外壳的流水线化
八、DMA 的底层展开:描述符 + CSR + 轮询
九、IR 中的对应:memory space 与 async.copy
十、汇编关键序列:一次 tile 迭代
十一、同步点清单
十二、边界与代价
十三、总结

🗂️ 一、内存层次与数据通路总览

CoralNPU 的内存层次没有 Cache,只有 TCM(紧耦合内存) 和 外部 DRAM。两者之间没有硬件自动填充的桥梁,所有跨层数据移动必须由 DMA 显式完成。下图把内存层次、总线、执行单元和 DMA 的关系一次性摊开:

图 1:CoralNPU 内存层次与数据通路。LSU 按地址解码路由到 ITCM/DTCM/EBUS;DMA 是唯一能批量跨 DRAM↔DTCM 搬运的引擎,且不占用 LSU 派发口。

从这张图可以读出三个关键事实:

  • • LSU 是唯一的内存出口,每周期只能派发一条内存指令。所有执行单元的 load/store 共享这个派发口。
  • • DMA 独立于 LSU。它通过 Host 端口直接读写 DRAM 和 DTCM,不经过 LSU 的派发口。这正是它能把长延迟搬运从关键路径上剥离的原因。
  • • DTCM 是 4-bank SRAM,标量、向量、矩阵、DMA 可以并发访问不同 bank 而不冲突。

🧭 二、内存空间:C 代码里怎么区分 TCM 和 DRAM

C 语言本身没有内存空间的概念。CoralNPU 对 TCM 和 DRAM 的区分,完全依赖 链接脚本和段属性,是一种“约定大于语法”的机制。

🗺️ 地址区间决定路由

LSU 的地址解码器按固定区间路由:

图 2:LSU 地址解码与总线路由。地址落在哪个区间,决定了走哪条总线,与指令类型无关。

链接脚本把特定数据段锚定到这些地址区间。C 代码通过 __attribute__((section(...))) 把变量放入链接脚本定义的段:


   
   
   
   
    
   
   
   
   #define DTCM_DATA __attribute__((section(".dtcm_data")))

int8_t
 weights[1024] DTCM_DATA;   // 地址落在 0x10000 段
int8_t
 activations[1024];          // 默认 .data/.bss,地址在 DRAM 段

指针本身不携带空间信息,它只是一个 32 位地址。

当你把指针传给 vle32.v 时,

LSU 只关心地址值落在哪个硬件区间,

并自动路由到 DBUS 或 EBUS。

C 代码中“区分”空间的方式是隐式的:

你通过段属性和链接脚本决定了数据的归属,

运行时由 LSU 的地址解码器完成路由。

🐢 三、RVV 能直接从 DRAM 加载吗

硬件上可以,性能上不应该。

vle32.v 本身不区分 TCM 或 DRAM。当基址落在 0x2000_0000 之后时,LSU 的地址解码器自动将请求路由到 EBUS,发起 AXI 事务。功能上完全可行。

但代价是结构性的:

  • • LSU 派发口被长延迟阻塞:LSU 每周期只能派发一条内存指令。一条指向 DRAM 的 vle32.v 会长时间占用派发口,等待 EBUS 完成。在此期间,所有其他内存操作——包括对 DTCM 的高速访问——都无法派发。
  • • 没有 Cache 兜底:CoralNPU 没有 D-Cache。DRAM 访问的延迟是完全暴露的,每一次访问都要付全额总线往返时间。
  • • 向量加载放大延迟:vle32.v 可能展开成多个微操作。如果都指向 DRAM,就会在 LSU 派发口上形成一长串 EBUS 事务。

图 3:RVV 直接读 DRAM 与 DMA 预取的对比。直接读 DRAM 会阻塞 LSU 派发口;DMA 预取后,RVV 只读 DTCM,单周期完成。

标准做法:

DRAM →(DMA 批量搬运)→ DTCM →(RVV vle32.v,单周期)→ 向量寄存器

DMA 是独立于 LSU 的异步引擎,

它在后台搬运数据时不占用 LSU 派发口。

等数据就绪后,RVV 再从 DTCM 加载,

每次访问都是单周期延迟,且不阻塞其他内存指令。

⚙️ 四、矩阵引擎的启动与等待接口

矩阵引擎没有指令队列,靠 csrw 写 KISA/KSCM 自定义 CSR 触发,靠 csrr 读 MATRIX_STATUS 确认完成。把这两件事封装成两个函数,内核代码就不必在每个算子内部重复写四条 csrw 和一个轮询循环。


   
   
   
   
    
   
   
   
   // 启动:四条 csrw,均独占派发槽 0
static
 inline void matrix_start(uint32_t addr_a, uint32_t addr_b,
                                uint32_t
 addr_o)
 {
    __asm__ volatile (
        "csrw KSCM_ADDR_A, %0\n\t"

        "csrw KSCM_ADDR_B, %1\n\t"

        "csrw KSCM_ADDR_O, %2\n\t"

        "csrw KISA, %3"

        :
        : "r"(addr_a), "r"(addr_b), "r"(addr_o), "r"(START_BIT)
        : "memory"
    )
;
}

// 等待:轮询 MATRIX_STATUS.done

static
 inline void matrix_wait(void) {
    uint32_t
 v;
    do
 {
        __asm__ volatile ("csrr %0, MATRIX_STATUS"
                          : "=r"(v) : : "memory")
;
    } while (!(v & DONE_BIT));
}

图 4:matrix_start 与 matrix_wait 的展开。四条 csrw 独占槽 0;轮询循环每轮至少三条指令,csrr 独占槽 0。

⚠️ 两个函数的实现要点

matrix_start 和 matrix_wait 都必须带 memory clobber。前者防止编译器把后续访存重排到启动之前;后者防止编译器把轮询优化成“只读一次”的死循环。csrr 读的是会变化的硬件状态,编译器默认不知道这一点。

轮询的节奏是“重”的。csrr 独占槽 0,andi 和 beqz 在下一个周期派发,一轮至少 3 个周期。等待期间标量核无法派发其他指令——这不是效率问题,而是特殊指令的硬件约束。

🔀 同步与分离两种用法


   
   
   
   
    
   
   
   
   // 同步版本:不关心重叠
matrix_start(A_tcm, B_tcm, C_tcm);
matrix_wait();

// 分离版本:等待期间预取下一 tile

matrix_start(A_tcm, B_tcm, C_tcm);
dma_memcpy(SLOT_A_NEXT, A_dram_next, TILE_K);
dma_memcpy(SLOT_B_NEXT, B_dram_next, TILE_K);
matrix_wait();   // 此时矩阵大概率已完成

分离 start/wait 的价值在于重叠。

轮询循环本身不能让标量核做别的事,

但把 matrix_wait() 推迟到真正需要结果之前,

中间就可以插入 DMA 编程或其他标量工作。

🧱 五、内核/外壳分层:标准编程范式

CoralNPU 的标准编程范式是两层结构:内核只认 DTCM,外壳负责搬运。

图 5:内核/外壳分层。外壳只碰 DMA,内核只碰 DTCM;矩阵引擎通过 matrix_start/matrix_wait 驱动。

⚙️ 内核层:只认 DTCM


   
   
   
   
    
   
   
   
   void gemm_kernel_dtcm(int8_t *A_tcm, int8_t *B_tcm, int32_t *C_tcm,
                      int
 M, int N, int K)
 {
    // 所有指针指向 0x10000 段,LSU 走 DBUS,单周期

    matrix_start(A_tcm, B_tcm, C_tcm);
    matrix_wait();
    vector_postprocess(C_tcm);
    // 完全不关心数据从哪来、到哪去

}

🚚 外壳层:负责 DRAM↔DTCM 的搬运


   
   
   
   
    
   
   
   
   void gemm_shell(int8_t *A_dram, int8_t *B_dram, int32_t *C_dram,
                int
 M, int N, int K)
 {
    int
 num_tiles = K / TILE_K;
    for
 (int t = 0; t < num_tiles; t++) {
        dma_memcpy(SLOT_A, A_dram + t*TILE_K, TILE_K);
        dma_memcpy(SLOT_B, B_dram + t*TILE_K, TILE_K);
        dma_wait_all();

        gemm_kernel_dtcm(SLOT_A, SLOT_B, SLOT_C, M, N, TILE_K);

        dma_memcpy(C_dram + t*TILE_OUT, SLOT_C, TILE_OUT);
    }
    dma_wait_all();
}

内核完全不知道 DRAM 存在,

它看到的只是 0x10000 段的一小块 buffer。

外壳完全不知道矩阵和向量怎么算,

它只负责“搬进来、调用、搬出去”。

这个分层的好处是:内核代码可以在任何有 DTCM 的 CoralNPU 变体上复用,外壳代码可以根据 DMA 能力(单通道/多通道、有中断/无中断)独立演进。

🔀 三种变体

变体
内核输入/输出
外壳职责
典型场景
纯 DTCM 内核
全在 DTCM
预取输入、写回输出
矩阵乘、卷积
DTCM 输入 + 寄存器输出
输入 DTCM,输出直接进向量寄存器
只预取输入
逐元素激活、归一化
软件缓存内核
输入通过 tag 检测动态换入
维护 tag、按需 DMA
Gather、embedding

前两种是静态流水,外壳可以完全展开成固定循环。第三种是动态缓存,外壳需要 tag 数组和 if-else 分支。

🧬 六、DTCM 上的计算融合:零拷贝的算子流水

DTCM 上做计算融合比 DRAM 上容易得多,而且这种容易是架构级的,不是靠编译器优化硬撑出来的。原因很简单:中间结果不需要离开 DTCM,也就不需要 DMA 写回再读入。

图 6:融合 vs 不融合的数据流对比。不融合时中间结果 ACC 要经历两次 DMA 和两次轮询;融合后 ACC 只在 DTCM 内传递,外壳只管理首尾两次 DMA。

🔗 为什么 DTCM 上融合更容易

在传统 DRAM 架构里,两个算子之间的中间结果必须完整落盘:

算子 A 把结果写入 DRAM → 算子 B 再从 DRAM 读回来

中间隔着一次长延迟写和一次长延迟读,

还有两次DMA 描述符编程 + 轮询。

而在 CoralNPU 上,算子 A 的输出槽位就在 DTCM 里,算子 B 可以直接把它当输入:

算子 A 写 SLOT_MID(DTCM)→ 算子 B 读 SLOT_MID(DTCM)

没有 DMA,没有 DRAM,没有轮询,没有总线事务。

中间结果的“传递”退化成了一次指针参数的传递。

具体优势可以拆成四点:

  • • 零拷贝:中间结果不经过 DRAM,节省了一次写回和一次读入的完整 DMA 事务。
  • • 零等待:算子 B 不需要 dma_wait_all(),因为数据已经在 DTCM 里,LSU 直接可读。
  • • 零描述符:不需要为中间结果构建描述符、写 DESC_ADDR、写 CTRL.start、轮询 STATUS.done。
  • • 编译器可静态规划:中间槽位的生命周期由编译器在 tiling 阶段确定,不需要运行时动态分配。

🧩 融合链的典型形态

一个典型的融合链是 GEMM → bias add → ReLU → quantize:


   
   
   
   
    
   
   
   
   // 融合内核:所有中间结果都在 DTCM
void
 fused_kernel_dtcm(int8_t *A_tcm, int8_t *B_tcm,
                       int32_t
 *bias_tcm, int8_t *C_tcm,
                       int
 M, int N, int K)
 {
    // ① 矩阵引擎:A × B → SLOT_ACC(DTCM)

    matrix_start(A_tcm, B_tcm, SLOT_ACC);
    matrix_wait();

    // ② 向量引擎:读 SLOT_ACC → +bias → ReLU → quantize → 写 SLOT_OUT

    //    中间结果 SLOT_ACC 一直在 DTCM,没有离开过

    vint32m1_t
 acc   = vle32_v_i32m1(SLOT_ACC, vl);
    vint32m1_t
 b     = vle32_v_i32m1(bias_tcm, vl);
    vint32m1_t
 sum   = vadd_vv_i32m1(acc, b, vl);
    vint32m1_t
 relu  = vmax_vx_i32m1(sum, 0, vl);
    vint8m1_t
  q     = vnclip_wx_i8m1(relu, 0, vl);
    vse8_v_i8m1(C_tcm, q, vl);
}

整条链上,数据只在 DTCM 内部流动:

A_tcm/B_tcm → SLOT_ACC → C_tcm。

外壳只需要把 A_tcm/B_tcm 搬进来、把 C_tcm 搬出去,

中间的 SLOT_ACC 完全不碰 DRAM。

🎯 融合对编译器意味着什么

融合把“算子间调度”从运行时拉到了编译期:

  • • 槽位生命周期静态化:SLOT_A、SLOT_B、SLOT_ACC、SLOT_OUT 的复用关系在 tiling 阶段就确定,不需要运行时分配器。
  • • 同步点合并:多个算子之间的同步可以合并成一次 matrix_wait(),而不是每个算子都各自轮询。
  • • 寄存器压力可控:中间结果在 DTCM 而不是寄存器,向量寄存器只承载当前算子的工作集。
  • • 融合边界清晰:哪些算子能融合,取决于它们能否共享 DTCM 槽位和同步节奏。

融合的边界:

如果两个算子之间需要 跨 tile 的全局归约(例如 softmax 的 max/sum),

中间结果无法只留在单个 DTCM 槽位,

此时仍需写回 DRAM 或使用更大的 DTCM 缓冲区。

融合不是无条件的,它依赖数据流的局部性和槽位可复用性。

🔁 七、双缓冲:外壳的流水线化

单缓冲的外壳是“搬→算→搬→算”串行。双缓冲让 DMA 预取 t+1 与 计算 t 重叠:


   
   
   
   
    
   
   
   
   void gemm_shell_pipelined(...) {
    dma_memcpy(SLOT_A0, A_dram, TILE_K);
    dma_memcpy(SLOT_B0, B_dram, TILE_K);

    for
 (int t = 0; t < num_tiles; t++) {
        int
 cur  = t & 1;
        int
 next = (t+1) & 1;

        dma_wait_all();
        if
 (t+1 < num_tiles) {
            dma_memcpy(SLOT_A[next], A_dram+(t+1)*TILE_K, TILE_K);
            dma_memcpy(SLOT_B[next], B_dram+(t+1)*TILE_K, TILE_K);
        }
        gemm_kernel_dtcm(SLOT_A[cur], SLOT_B[cur], SLOT_C);
        dma_memcpy(C_dram+t*TILE_OUT, SLOT_C, TILE_OUT);
    }
    dma_wait_all();
}

双缓冲的软件状态只有两个:

cur/next 的槽位号(t & 1),

以及“当前 tile 的 DMA 是否完成”(dma_wait_all)。

没有 tag 数组、没有替换策略,

因为 tile 是顺序消费的,不是随机 Gather。

图 7:双缓冲流水时序。DMA 预取 t+1 与矩阵计算 t 并行;单通道 DMA 上预取与写回仍串行。

双缓冲让 DMA 预取 t+1 与矩阵计算 t 并行。

但单通道 DMA 上,预取 t+1 与 写回 t 仍然串行。

增大 tile 大小或用描述符链可以摊薄这个串行开销。

📦 八、DMA 的底层展开:描述符 + CSR + 轮询

dma_memcpy 在 runtime 里展开为三步:


   
   
   
   
    
   
   
   
   void dma_memcpy(uint32_t dst, uint32_t src, uint32_t len) {
    volatile
 uint32_t *d = (uint32_t*)(DESC_BASE + desc_idx*32);
    d[0] = src;
    d[1] = dst;
    d[2] = len;
    d[3] = 0;          // next_desc = 0,单条链
    d[4] = 0; d[5] = 0; d[6] = 0;

    csr_write(DMA_DESC_ADDR, DESC_BASE + desc_idx*32);
    csr_write(DMA_CTRL, ENABLE | START);

    desc_idx = (desc_idx + 1) & 0xF;
}

void
 dma_wait_all(void) {
    while
 (!(csr_read(DMA_STATUS) & DONE_BIT)) {}
    csr_write(DMA_STATUS, DONE_BIT);
}

图 8:DMA 传输的完整握手序列。描述符写、CSR 启动、单 outstanding 读-写、轮询完成,全程无中断。

DMA 在 IR/汇编里没有“专用指令”。

它就是普通 store 写描述符 + CSR 写启动 + 轮询状态。

next_desc 为 0 表示单次传输;

构建多描述符链时,一次 CTRL.start 就能遍历整条链。

🧾 九、IR 中的对应:memory space 与 async.copy

图 9:IR 的三层 lowering。memory space 0/1 是外壳与内核的分界线;matrix_start/matrix_wait 在低层展开为 CSR 序列;融合的中间结果没有 async.copy。

高层:整个算子是一个 linalg.matmul


   
   
   
   
    
   
   
   
   linalg.matmul ins(%A, %B : memref<MxKxi8, 0>, memref<KxNxi8, 0>)
              outs(%C : memref<MxNxi32, 0>)

中层:tiling 切成 tile,生成 scf.for + async.copy


   
   
   
   
    
   
   
   
   scf.for %t = %c0 to %K step %TILE_K {
  %tok_a = async.copy %A[%t], %slot_a : memref<Txi8, 0> to memref<Txi8, 1>
  %tok_b = async.copy %B[%t], %slot_b : memref<Txi8, 0> to memref<Txi8, 1>
  async.await %tok_a, %tok_b

  coralnpu.matrix_start %slot_a, %slot_b, %slot_c
      : memref<Txi8,1>, memref<Txi8,1>, memref<Txi32,1>
  coralnpu.matrix_wait

  %tok_c = async.copy %slot_c, %C[%t] : memref<Txi32, 1> to memref<Txi32, 0>
}

融合后的 IR:中间结果不经过 async.copy


   
   
   
   
    
   
   
   
   scf.for %t = %c0 to %K step %TILE_K {
  %tok_a = async.copy %A[%t], %slot_a : memref<Txi8, 0> to memref<Txi8, 1>
  %tok_b = async.copy %B[%t], %slot_b : memref<Txi8, 0> to memref<Txi8, 1>
  async.await %tok_a, %tok_b

  coralnpu.matrix_start %slot_a, %slot_b, %slot_acc
      : memref<Txi8,1>, memref<Txi8,1>, memref<Txi32,1>
  coralnpu.matrix_wait

  %acc  = vector.load %slot_acc[%c0] : memref<Txi32, 1>
  %bias = vector.load %bias_slot[%c0] : memref<Txi32, 1>
  %sum  = arith.addi %acc, %bias : vector<Txi32>
  %relu = arith.maxsi %sum, %zero : vector<Txi32>
  %q    = arith.trunci %relu : vector<Txi32> to vector<Txi8>
  vector.store %q, %slot_out[%c0] : memref<Txi8, 1>

  %tok_c = async.copy %slot_out, %C[%t] : memref<Txi8, 1> to memref<Txi8, 0>
}

memory space 是分界线:

0 的 memref 只出现在外壳的 async.copy 里,

1 的 memref 只出现在内核的计算里。

融合的 IR 特征:slot_acc 只出现在 memref<..., 1> 的位置,

没有对应的 async.copy——它从未离开 DTCM。

💻 十、汇编关键序列:一次 tile 迭代


   
   
   
   
    
   
   
   
   tile_loop:
    # ① 等 DMA 完成(当前 tile)
dma_wait:
    csrr    t0, DMA_STATUS
    andi    t0, t0, DONE_BIT
    beqz    t0, dma_wait

    # ② 预取下一 tile:写描述符 + 启动
    li      t1, DESC_BASE
    sw      s5, 0(t1)
    sw      s3, 4(t1)
    li      t2, TILE_K
    sw      t2, 8(t1)
    sw      zero, 12(t1)
    csrw    DMA_DESC_ADDR, t1
    csrw    DMA_CTRL, START_BIT

    # ③ 矩阵引擎:matrix_start 展开为四条 csrw
    csrw    KSCM_ADDR_A, s0
    csrw    KSCM_ADDR_B, s1
    csrw    KSCM_ADDR_O, s2
    csrw    KISA, START_BIT

    # ④ matrix_wait:轮询矩阵完成
mat_wait:
    csrr    t0, MATRIX_STATUS
    andi    t0, t0, DONE_BIT
    beqz    t0, mat_wait

    # ⑤ 向量后处理:读 SLOT_ACC,写 SLOT_OUT
    vsetvli t0, zero, e32, m1
    vle32.v v0, (s2)
    vle32.v v1, (bias_ptr)
    vadd.vv v2, v0, v1
    vmax.vx v2, v2, zero
    vse32.v v2, (s9)

    # ⑥ DMA 写回最终结果到 DRAM
    li      t1, DESC_BASE+32
    sw      s9, 0(t1)
    sw      s7, 4(t1)
    sw      t2, 8(t1)
    sw      zero, 12(t1)
    csrw    DMA_DESC_ADDR, t1
    csrw    DMA_CTRL, START_BIT

    # ⑦ 推进指针,切换双缓冲
    addi    s5, s5, TILE_K
    addi    s6, s6, TILE_K
    addi    s7, s7, TILE_OUT
    xor     s0, s0, 0x4000
    xor     s1, s1, 0x4000
    addi    s8, s8, -1
    bnez    s8, tile_loop

所有 csrw/csrr 都是特殊指令,

只能从派发槽 0 发出并独占派发周期。

这保证了矩阵启动和状态查询是原子控制点,

但也意味着它们不能与普通指令同周期混排。

③ 和 ④ 正是 matrix_start 与 matrix_wait 的展开。

🔐 十一、同步点清单

同步点
机制
为什么必须显式
DMA 完成
轮询 DMA_STATUS.done
v1 无中断;Scoreboard 不跟踪 DMA
矩阵完成
轮询 MATRIX_STATUS.done(matrix_wait)
Scoreboard 只跟踪标量寄存器 RAW/WAW
矩阵地址配置原子性
csrw
 槽 0 独占派发
防止配置与启动被其他指令穿插
向量 store → DMA 读
LSU 硬件总序
普通 FENCE 是 NOP,不承担数据同步
双缓冲切换
标量寄存器 xor 翻转 slot
纯软件指针管理,无硬件语义
融合算子间传递 DTCM 内 LSU 访问 无需 DMA 轮询,LSU 总序保证可见性

整个流程没有一条普通 FENCE 参与数据同步。

完成判断全部来自状态轮询,

内存可见性由 LSU 的集中排序保证。

融合的算子之间连轮询都省了——因为数据在 DTCM 里,

LSU 的总序直接保证了下一条向量 load 能看到上一条向量 store 的结果。

🚧 十二、边界与代价

✅ 前提

  • • 静态形状:tile 大小、循环次数在编译期确定,外壳才能展开成固定 DMA 序列。
  • • 顺序消费:tile 按顺序处理,双缓冲才能用 t & 1 这种简单轮转。
  • • 内核不越界:内核只访问传给它的 DTCM 槽位,不自行计算 DRAM 地址。
  • • 融合的局部性:融合的算子之间不能需要跨 tile 的全局归约,否则中间结果无法只留在单个 DTCM 槽位。
  • • matrix_wait 的阻塞语义:轮询期间标量核无法派发其他指令,需要重叠时把 matrix_wait() 推迟到需要结果之前。

⚠️ 边界

  • • 动态形状:tile 数运行时才知道,外壳需要动态循环,DMA 描述符要动态构建。
  • • 随机访问:Gather 无法用“顺序双缓冲”覆盖,需要 tag 检测 + 按需换入。软件管理的命中检测需要串行执行,miss 路径是阻塞式 DMA + 忙轮询,没有 Cache 的并行 miss 和延迟隐藏能力。
  • • 单通道 DMA:预取和写回在 DMA 上串行,双缓冲只能隐藏一部分延迟。增大 tile 大小或使用描述符链可以摊薄固定开销。
  • • DTCM 容量:融合链越长,中间槽位越多,DTCM 压力越大。编译器需要在融合收益和槽位占用之间做权衡。

🐢 大规模随机 Gather 的特殊问题

对于工作集远大于 DTCM、索引完全随机的 Gather,软件管理会明显慢于硬件 Cache:

  • • miss 路径是阻塞式的:选 victim、写描述符、启动 DMA、轮询完成,标量核完全阻塞。
  • • 没有并行 miss:DMA 单通道、单 outstanding,无法像 Cache 那样多个 miss 同时在途。
  • • 没有硬件预取:软件必须显式预测,预测失败就是全额延迟。

这不是实现缺陷,而是“用静态性和确定性换能效”的前提被打破后的必然结果。

真正的解法不是优化 tag 比较,而是改变算法恢复局部性(重排、分桶、分块),

或者换用带 Cache 的硬件。

🏁 十三、总结

CoralNPU 的 DMA 与计算流水可以压缩为一条数据流:

DRAM →(DMA 描述符 + CSR 启动,异步)→ DTCM 双缓冲槽 →(matrix_start + matrix_wait)→ 外积 MAC →(向量 load/store,入队 CQ)→ 加偏置/激活 →(DMA 写回)→ DRAM

融合链则把中间步骤省掉:

DTCM 内的算子 A →(指针传递,零拷贝)→ DTCM 内的算子 B

没有 DMA,没有 DRAM,没有轮询。

三个层面的对应关系:

  • • C 代码:看不到 TCM 指令,只有地址常量和 csr_write/csr_read 封装。内存空间由链接脚本和段属性隐式决定。矩阵启动和等待通过 matrix_start/matrix_wait 完成。
  • • MLIR IR:memref 的 memory space 标记 TCM/DRAM;async.copy 表达 DMA;coralnpu.matrix_start/matrix_wait 在低层展开为 CSR 序列;csr_poll 是显式同步节点。融合体现为中间 memref 没有对应的 async.copy。
  • • 汇编:DMA 是“store 描述符 + csrw + 轮询”;矩阵是“四条 csrw + 轮询”;向量是普通 RVV 指令入队。融合体现为中间结果不经过 DMA 序列。

性能关键不在某条指令,而在重叠与融合:

  • • DMA 预取与矩阵计算重叠;
  • • 向量后处理与下一轮 DMA 写回重叠;
  • • 双缓冲让数据搬运不停顿;
  • • DTCM 上的计算融合让中间结果零拷贝传递。

正确性关键在显式同步:

  • • 每个异步引擎的完成都由软件轮询确认,没有硬件自动兜底;
  • • 融合算子之间的可见性由 LSU 总序保证,不需要额外 FENCE;
  • • matrix_start 和 matrix_wait 的 memory clobber 保证编译器不会把等待提前、不会把轮询优化成死循环。

内核(DTCM-only)+ 外壳(DMA 搬运)+ 融合(零拷贝算子链)+ 双缓冲(流水线重叠)

这个模式让计算内核与数据搬运解耦,

使内核代码可以在任何有 DTCM 的 CoralNPU 变体上复用,

而外壳代码根据 DMA 能力独立演进。

DTCM 上的融合是这套模式最大的红利之一:

中间结果不离开 DTCM,

就省掉了一整轮 DMA 描述符编程、CSR 写和轮询,

把“算子间调度”从运行时拉到了编译期。

代价是动态形状和随机访问需要外壳承担更多软件复杂度,

这正是 CoralNPU “静态性换确定性”在编程模型上的体现。

 


【声明】内容源于网络
0
0
ai算法芯片与系统
长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
内容 251
粉丝 0
ai算法芯片与系统 长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
总阅读8.0k
粉丝0
内容251