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?