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 输入 + 寄存器输出 |
|
|
|
| 软件缓存内核 |
|
|
|
前两种是静态流水,外壳可以完全展开成固定循环。第三种是动态缓存,外壳需要 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_STATUS.done
|
|
|
|
MATRIX_STATUS.done(matrix_wait)
|
|
|
|
csrw
|
|
|
|
|
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的memoryclobber 保证编译器不会把等待提前、不会把轮询优化成死循环。
内核(DTCM-only)+ 外壳(DMA 搬运)+ 融合(零拷贝算子链)+ 双缓冲(流水线重叠)
这个模式让计算内核与数据搬运解耦,
使内核代码可以在任何有 DTCM 的 CoralNPU 变体上复用,
而外壳代码根据 DMA 能力独立演进。
DTCM 上的融合是这套模式最大的红利之一:
中间结果不离开 DTCM,
就省掉了一整轮 DMA 描述符编程、CSR 写和轮询,
把“算子间调度”从运行时拉到了编译期。
代价是动态形状和随机访问需要外壳承担更多软件复杂度,
这正是 CoralNPU “静态性换确定性”在编程模型上的体现。

