Markus Fritsche 6a8b20ab3f rkopnu Phase 4c: honor core_mask -> vendor parity (51.5 vs 51.6 tok/s)
Root cause of both a latent oops AND the perf gap: SUBMIT let drm_sched pick
an arbitrary core, but librknnrt targets a specific physical core via core_mask
(round-robining single-core matmuls across all 3) and only fills that core's
subcore_task[] slot. A mismatched core read the wrong (often garbage) slot ->
out-of-bounds rk_tasks[ts] -> kernel oops (dodged by luck in earlier runs), and
left core 2 permanently idle -> ~1/3 of NPU throughput on the floor.

Fix: per-core drm_sched entities; pin each SUBMIT to rknpu_wait_core_index(
core_mask)'s core so hw_submit reads the matching subcore slot. Plus a bounds
guard in hw_submit (fall back to the whole-job range if a subcore range escapes
[task_start, task_start+task_number)) so a bad index can never oops again.

Reverted the Phase 4b task-BO vmap cache: with all 3 cores now active,
librknnrt drives them from concurrent threads each with its own task BO; a
shared per-file vmap slot races (one thread's vunmap strands another's in-flight
rk_tasks). Back to per-submit vmap (thread-safe; the ioctl holds the map across
its blocking fence wait).

Added ktime latency instrumentation (worker/npu/thread segments). Trace proves
the drm_sched glue is negligible: worker=90us + thread=22us vs npu=3800us per
submit. The gap was parallelism, not submit-path overhead.

Measured (gemma-4-E2B Q8, 1204-tok prefill, cores 4-7, contenders stopped):
21.73 -> 39.33 -> 44.38 -> 51.49 tok/s; vendor RKNPU2 warm = 51.62. 3 cores
balanced (676 IRQs each), 0 oops, 0 timeouts.
2026-07-18 23:44:30 +02:00

rkopnu — RK Open NPU (mainline driver for librknnrt)

Goal: run the vendor librknnrt.so (efficient RK3588 NPU int8 GEMM, ~52 tok/s gemma-E2B-Q8) on a MAINLINE kernel, by providing a clean mainline driver that speaks the rknpu ioctl ABI — instead of optimizing ggml-rocket's GEMM ("fasterrocket", measured dead: 0.096 vs 1.83 TOPS, 19x below same-silicon ceiling; bigger batches tried 2x, no help).

Strategy: fork mainline rocket, swap its uAPI for rknpu's

Reuse rocket's proven mainline plumbing (3-node rockchip,rk3588-rknn-core DT bind, GEM/shmem, IOMMU, IRQ, register programming — rocket_*.c here) and replace its userspace-facing ioctl layer with the 6 rknpu ioctls librknnrt calls, aggregating the 3 core devices into the vendor's one-device multi-core (core_mask/subcore_task[5]) submit model.

rknpu ABI to implement (measured live via strace, gemma-E2B prefill):

ACTION 0x40 (9807 calls, 26 sub-ops) · SUBMIT 0x41 (2028, multi-core core_mask) · MEM_CREATE 0x42 (3032) · MEM_MAP 0x43 (3032) · MEM_DESTROY 0x44 (3032) · MEM_SYNC 0x45 (14097)

  • DRM core PRIME_HANDLE_TO_FD/GEM_FLINK/VERSION. BOs are create+destroy churned (NOT persistent). Device: librknnrt opens the /dev/dri/card node (not renderD). Spec: rknpu_ioctl.h (structs) + vendor rknpu_job.c (register seq) + rocket_job.c/rocket_registers.h (cross-ref).

Phases

  1. [DONE] Scaffold: fork rocket, out-of-tree build → rkopnu.ko builds on 7.0.0-rc3-npuclk+.
  2. Present rknpu uAPI: copy rknpu_ioctl.h as the driver's uapi, register a /dev/dri/card node, implement DRM_IOCTL_VERSION + RKNPU_ACTION(get hw/drv version) → librknnrt opens + probes OK.
  3. Memory: RKNPU_MEM_CREATE/MAP/DESTROY/SYNC on rocket's GEM/shmem + IOMMU domains.
  4. Submit+IRQ: RKNPU_SUBMIT → map rknpu task descriptors onto rocket's job/register machinery, multi-core via core_mask; IRQ/fence completion.
  5. ACTION sub-ops (freq/power/iommu-domain/sram/reset) + integration: librknnrt end-to-end, target ~52 tok/s gemma-E2B on mainline.

Test loop

rkopnu is mutually exclusive with rocket on the NPU (same DT nodes). Test: blacklist rocket at boot, modprobe rkopnu, run librknnrt (rk-llama.cpp build/). Do NOT rmmod rocket live (SoC hang).

S
Description
rkopnu (RK Open NPU): run Rockchip librknnrt.so on a mainline kernel via a clean-room open driver presenting the rknpu ioctl ABI. RK3588, vendor-parity.
Readme GPL-2.0 353 KiB
Languages
C 99.9%