Exp 2026 06 27 Zero Copy Weight Import - oRKLLM/ork-driver GitHub Wiki

Zero-Copy Weight Import (dma-buf PRIME) + Fixed-Slot Streaming

Commit: ork-driver 57feca4 (v0.4.0) Β· API ork_dma_import/ork_dma_import_sync/ork_dma_import_free + ork_mm_load_i8_import Β· uABI src/rknpu_ioctl.h (DRM PRIME fd↔handle, dma-heap alloc, dma_buf_sync) Β· test tools/zerocopy_import_test.c. RK3588 10.3.0.236, governors pinned.

TL;DR

The closed RKNN runtime imports externally-allocated memory zero-copy via rknn_create_mem_from_fd/_from_phys. An LD_PRELOAD strace showed this is not a new ioctl β€” it's an unused input mode of the MEM_CREATE we already had: DRM_IOCTL_PRIME_FD_TO_HANDLE (generic DRM, import a dma-buf fd β†’ GEM handle) then RKNPU_MEM_CREATE{handleβ‰ 0, flags=0, size=0} β†’ the kernel IOMMU-maps the existing pages, no alloc, no copy. Built it into ork-driver: a dma-heap buffer filled with pre-tiled weights and imported as a matmul tensor is read directly by the NPU, bit-exact vs the alloc+copy path, make test fully green. Weight load is 2.2–4.4Γ— faster (the host memcpy is gone), and a fixed-uniform-size-slot streaming pool cycled a 4295 MB working set (>4 GiB) through one slot, 1024 swaps, 0 fails / 0 mismatches β€” the earlier streaming wall was variable-size MEM_CREATE/DESTROY churn, not the swap itself. It does NOT escape the 4 GiB IOVA cap (the imported dma_addr is still a 32-bit 0xff……) and does not change M=1 decode bandwidth β€” the win is the eliminated copy in the M>1 streaming swap/load path.

1. The mechanism (strace of librknnrt, then replicated)

Importing a /dev/dma_heap/system dma-buf as a matmul B (weight) tensor, the decoded syscall sequence:

PRIME_FD_TO_HANDLE  in_fd=8 β†’ handle=6           (generic DRM 0x2e: import dma-buf β†’ GEM handle)
MEM_CREATE  in{handle=6, flags=0x0, size=0} β†’ out{handle=6, dma_addr=0xff400000}

vs an internal allocation (rknn_create_mem / ork's existing path):

MEM_CREATE  in{handle=0, flags=0x403, size=4194304} β†’ out{handle=3, dma_addr=0xffbfe000}

So RKNPU_MEM_CREATE (nr 0x42) is overloaded: handle==0 β‡’ allocate + DMA-map (what ork-driver did); handle!=0 (from PRIME_FD_TO_HANDLE) β‡’ map the pre-existing dma-buf's pages, no allocation, no copy. The prior RE recorded MEM_CREATE as alloc-only and missed the import-by-handle mode. Zero new driver-private opcodes β€” only the generic DRM PRIME ioctls (0x2e/0x2d) and the handle!=0 input.

2. The coherency gotcha (a real bug, found and fixed)

RKNPU_MEM_SYNC does not flush a foreign imported cacheable dma-buf β€” it only covers buffers rknpu itself allocated. On the cacheable system heap that gave all-wrong output (CPU dirty lines never reached the NPU); system-uncached was correct but ~5Γ— slower to fill. The fix is the dma-buf's own DMA_BUF_IOCTL_SYNC (START|WRITE / END|WRITE bracket around the CPU fill) β€” then the cacheable heap is correct and fast. This is the import analogue of the output zero-copy coherency fix (3fad74a): the NPU and CPU share the pages, so the CPU's caches must be reconciled around the device read.

3. Results β€” validated vs the CPU reference

Load time (alloc+copy β†’ import), bit-exact:

weight shape (int8) speedup
K512 Γ— N256 4.4Γ—
K2048 Γ— N512 2.2Γ—
K4096 Γ— N1024 2.6Γ—

Fixed-slot streaming (16 MB experts, load_import β†’ run β†’ free, round-robin):

per-swap cost ms
alloc + copy 53.2
import 25.2 (2.1Γ—)

The fragmentation test (the earlier wall): cycle a 4295 MB working set (>4 GiB cumulative IOVA churn), 256 distinct experts Γ— 256 iters = 1024 swaps through a single uniform-size slot β†’ 0 load-fails, 0 output mismatches on both the import and alloc+copy paths. Uniform fixed-slot reuse does not fragment the IOVA β€” the earlier streaming / MoE-persist cost was variable-size alloc/free churn, and import additionally halves the swap cost on top.

make test stayed fully green (matmul, W4A4, layer, decode, model, speed, chain) with the new API in tree.

Verdict

βœ… Zero-copy weight import is real and buildable on the existing uABI (the handle!=0 MEM_CREATE mode + generic PRIME), and a uniform fixed-slot pool makes streaming crash-free past 4 GiB cumulative churn. The win is concrete where M>1 streaming spends its time: load 2.2–4.4Γ—, swap 2.1Γ—, no copy. ⚠️ It does NOT raise the ~4 GiB resident ceiling β€” the imported dma_addr is still 32-bit IOVA (rknn_tensor_mem.phys_addr is hidden, size is uint32_t), so big models still stream; the "4 GiB accelerator, not UMA" model holds. And it does not touch M=1 decode (bandwidth-bound). NEXT: wire ork_mm_load_i8_import into the ggml-ork streaming path (the 27B/35B verify / MoE) for the end-to-end effect β€” does cheaper, crash-free streaming move the M>1 prefill/verify number?