RKNPU 编程架构

本页介绍 RKNPU 的编程模型:硬件没有指令集,软件提交的是内存里的一段 寄存器命令(regcmd),由 PC 块顺序读出并写入各计算块;驱动在内核侧只做 BO 的 DMA 与栅栏,不做算子编译。硬件结构见 硬件架构。

1. 从应用到硬件的分层

mainline 路线(本页以此为准)[1]:

应用 / 前端 (ggml / TFLite / ONNX Runtime)
        │  调用算子
        ▼
librocketnpu(用户态驱动 + matmul/算子库)
        │  生成 regcmd,提交 drm_rocket_submit
        ▼
rocket(内核 DRM accel 驱动)→ 写各块寄存器、DMA BO、收中断
        ▼
RKNPU 硬件(PC → CNA/CORE/DPU/PPU)
  • rocket 是通用寄存器命令提交器,不锁定某一个算子集。内核只负责把 BO DMA 到设备并按你给的寄存器程序触发各块;"怎么算"完全由用户态决定[1]。
  • 厂商路线是 BSP 里的 rknpu 驱动(设备名不是 /dev/accel/accelN);同一个 用户态库可通过提交层适配它[1]。

2. job、task 与 regcmd

一次 drm_rocket_submit(一个 job)携带一个或多个 task,每个 task 是一个 {regcmd IOVA, regcmd_count} 描述符[1]:

struct drm_rocket_task {
    __u64 regcmd;        /* regcmd 缓冲的 DMA 地址 */
    __u32 regcmd_count;  /* 命令字数 */
    /* ... */
};

regcmd 是一条 NPUOP(op, value, reg) 命令流,以控制尾部结束:

NPUOP(OP_NONE,          0,    0)
NPUOP(OP_REG_PC,        0,    PC_REGISTER_AMOUNTS)  # 驱动按 regcmd_count 回填
NPUOP(OP_40,            0,    0)
NPUOP(OP_ENABLE,        0x1D, PC_OPERATION_ENABLE)  # 0x1D = bit0,2,3,4 → 触发 PC+CNA+DPU+DPU_RDMA

命令字的编码是 (op << 48) | (value << 16) | reg,其中 op 高位带上块标签 (如 0x0201 对应 CNA、0x0801 对应 CORE、0x1001 对应 DPU)[1][2]。一个 int8 matmul task 的完整 regcmd 是 126 个 NPUOP 字[1]。

每个 task 的 regcmd 以 DPU_S_POINTER = 0xE、DPU_RDMA_S_POINTER = 0xE 开头, 武装这两个块的乒乓寄存器组;kernel 在每个 task 前对 CNA 和 CORE 做同样的事, 并加上每核位[1][3]。

3. PC 与 block enable 位图

PC_OPERATION_ENABLE(0x0008)在手册与 Mesa 的寄存器表里都被描述为"只有 bit0 一个使能位,高位保留",但实测它是一个逐块的参与位图[1]。尾部那条 OP_ENABLE 选择哪些块运行:

程序 值 位置
卷积 0x1D bit 0,2,3,4 → PC / CNA / DPU / DPU-RDMA
池化 0x60 bit 5,6 → PPU / PPU-RDMA

两组互不相交,0x60 甚至不含 bit0。把卷积的 0x1D 塞进池化生成器、其余不变, 输出缓冲原封不动;反之亦然。所以 bit0 既不是充分条件也不是必要条件,只有"位图" 解释自洽——把尾部命令读成"本程序要触发的块清单"[1]。

⚠️ 不要用回读值来反驳:该寄存器会自清,轮询恒返回 0。位含义与回读所见是两回事[1]。

4. ping-pong 与 delta task

寄存器状态在同一乒乓组内跨 task 保持[1]:

  • 出厂形态下,task 落到的组是"再往前数第二个 task"写过的组;
  • 一个 delta task 只写指针、三个缓冲地址和 4 字尾部;int8 matmul 的地址是 CNA_FEATURE_DATA_ADDR(0x1070)、CNA_DCOMP_ADDR0(0x1110)、 DPU_DST_BASE_ADD(0x4020),只 9 字,而完整 task 是 126 字[1];
  • delta 不能依赖上一个 job:寄存器文件跨 job、跨进程保持,调度器又决定 job 落在哪个核,所以上一个 task 是谁并不可知[1];
  • 用户态 regcmd 绝不能写 CNA/CORE 的 S_POINTER:那需要 kernel 的每核位, 缺位或写别的核会在 24/24 次测试中让 task 永不完成、无中断、输出未写, 最终靠 kernel 的 500 ms 超时退场[1]。

连续链(batched submit)把 N 个 task 压成一次硬件 kick、一个完成中断[1]:

  1. 把各 task 的 regcmd 连续摆放(步长取偶数化后的字数);
  2. 改写每个 task 的尾部,把 PC 重定向到下一个 task(复用 count-4 处的惰性 OP_NONE 位,嵌入 PC_BASE_ADDRESS 重定向 + 下一段的 PC_REGISTER_AMOUNTS);
  3. 设 PC_TASK_CON.TASK_NUMBER = N,PC 流式跑完 N 个 task 只报一次完成。

链内 task 是有序紧耦合的:前一个 task 的 WDMA 输出对后一个 task 的 ERDMA 读取 可见,因此链能表达跨 task 的数据依赖(例如 fp16 的 K 分块回加)[1]。

5. mainline rocket 的 uAPI

rocket 暴露四个 ioctl,作用于一个 /dev/accel/accelN[1][4]:

ioctl 作用
DRM_IOCTL_ROCKET_CREATE_BO 分配 buffer object(有 DMA 地址 / IOVA)
DRM_IOCTL_ROCKET_SUBMIT 提交 job(含 tasks),异步返回
DRM_IOCTL_ROCKET_PREP_BO 对 BO 做 cache 同步;在输出 BO 上作为栅栏等待
DRM_IOCTL_ROCKET_FINI_BO 结束 BO 的使用

SUBMIT 是异步的,输出 BO 上的 PREP_BO 才是栅栏。一个易犯的 bring-up bug: PREP_BO 的 timeout_ns 是绝对的 CLOCK_MONOTONIC 截止时刻,不是相对超时[1]。

sequenceDiagram participant App as 应用 / 前端 participant Lib as librocketnpu participant K as rocket (kernel) participant HW as RKNPU App->>Lib: 算子调用 (matmul / conv) Lib->>Lib: 生成 regcmd (NPUOP 流) Lib->>K: CREATE_BO / SUBMIT{task} K->>K: DMA regcmd, PREP_BO 输入 K->>HW: 写块寄存器, 触发 OP_ENABLE HW-->>K: 完成中断 K-->>Lib: 栅栏 (PREP_BO 输出) Lib-->>App: 结果

6. matmul 作为 1×11\times1 卷积

NPU 没有矩阵乘原语,librocketnpu 把

C[M,N]=A[M,K]B[N,K]⊤ C[M,N] = A[M,K]\,B[N,K]^{\top}

表达成一次 1×11\times1(pointwise)卷积[1][2]:

  • K(收缩轴)→ 卷积的输入通道;
  • N(输出轴)→ 输出通道,每个权重行 nn 是一个 K×1×1K\times1\times1 滤波器;
  • M(行)→ 一张 K×M×1K\times M\times1 的"图像"的空间位置。

单个输出 tile(MtM_t 行 × NtN_t 通道,收缩 KtK_t)的数据流是[1]:

  1. CNA 特征加载:按行 / 面 stride 把 A[Mt,Kt]A[M_t,K_t] 读进 CBUF(不重排);
  2. CNA 权重加载:把 B[Nt,Kt]B[N_t,K_t] 读进 CBUF;
  3. CORE 跑 MAC 阵列,一次 passthrough 内跨 KtK_t 归约,累加进宽 CACC;
  4. DPU 把 C[Mt,Nt]C[M_t,N_t] 写回 DDR(DPU-RDMA / 逐元素级在此参与)。
📝 布局:没有寄存器能转换布局。权重必须在主机预打散成 cube 布局;而

matmul 的激活与输出可以只改 6 个寄存器就保持行主序(GROUP_LINE_OFF、 行 / 面 stride、DST_SURF_STRIDE、DATA_CUBE_NOTCH、SURFACE_ADD)[1]。

几个实测约束:matmul 的 M 是卷积的空间高度,M<4 会算错,必须 M%4=0M\%4=0 (单行 matmul 要 pad 到 4 行)[1];一个卷积 task 至多 1022 输入行 × 2047 列[1]。

7. 提交路径上的性能要点

  • 不绑 MAC。 在 rocket、600 MHz、权重常驻的工作点,matmul 只有约 460 GOP/s (约 fp16 峰值的 15%、int4 峰值的 4%),fp16 ≈ int8 ≈ int4,瓶颈在 DMA 与 每 job 派发,不在 MAC 阵列[1]。量化省的是内存,不是时间。
  • 单行 decode 不要上 NPU。 M=1 的 GEMV 受 DDR 带宽限制,比 batched GEMM 慢约 82×,解码留在 CPU[1]。
  • 时钟默认被钉在 200 MHz(1 GHz 规格的五分之一)。只能在驱动内、电源域起来 之后升频;安全操作点是 600 MHz(约 1.43× prefill),900 MHz 不再提速,冷启动 直接写 PLL 会把 SCMI 固件卡死[1]。
  • 一核一 fd。 单 fd 的 job 串到一个核;用满三核要开 3 个及以上 fd[1]。

参考文献

[1] GREGORDINARY. Rockchip NPU reverse-engineering notes[EB/OL]. (2026)[2026-10-09]. https://github.com/gregordinary/rockchip-npu-notes/tree/81b0d17da0b8a472c1a1e6f7d2ddba6b34d10111

[2] GREGORDINARY. rocket-userspace: include/npu_hw.h[EB/OL]. (2026)[2026-10-09]. https://github.com/gregordinary/rocket-userspace/blob/f86cf52c666b4eddadc17d80d6c558b4067c0d6b/include/npu_hw.h#L25-L33

[3] Linux kernel. drivers/accel/rocket/rocket_job.c[EB/OL]. (2026)[2026-10-09]. https://github.com/torvalds/linux/blob/6c377d19d4a5116d9bec5203aa3c6c11523e7898/drivers/accel/rocket/rocket_job.c#L110-L140

[4] Linux kernel. include/uapi/drm/rocket_accel.h[EB/OL]. (2026)[2026-10-09]. https://github.com/torvalds/linux/blob/6c377d19d4a5116d9bec5203aa3c6c11523e7898/include/uapi/drm/rocket_accel.h#L14-L22


© 2026 Yang Huan · yanghuan9812@qq.com

results matching ""

    No results matching ""