Vendor Kernel Behaviour - oRKLLM/ork-driver GitHub Wiki
Vendor Kernel Behaviour (rknpu as shipped)
Behaviours, contracts and defects of the vendor rknpu DRM driver itself โ true of the driver as
shipped, independent of our patches. This is a distinct category from:
- NPU-Quirks โ silicon/regcmd behaviour (what the hardware does).
- Kernel-Modifications โ the changelog of our patches (what we changed and why).
Read this before touching anything IOMMU-, submit- or reset-related in the driver.
Upstream status: the fixes for ยง1โยง5 below are proposed as
rockchip-linux/kernel#390 (closes
#387); branch rknpu-iommu-rework in
oRKLLM/rk3588-kernel. If that lands, most of this page
becomes history rather than a warning. Several entries
below cost multiple days each, and most are invisible until you are already deep in a wrong theory.
1. โ The DMA API is single-domain BY CONSTRUCTION โ multi-domain rests on a lie
dma-iommu.c:
static struct iommu_domain *iommu_get_dma_domain(struct device *dev)
{ return dev->iommu_group->default_domain; }
Every DMA-API mapping resolves through group->default_domain. There is no way to tell the DMA API
"map into domain N". The vendor's multi-domain support therefore overwrites the IOMMU core's
notion of the default domain on every switch:
rknpu_dev->iommu_group->default_domain = dst_domain;
reaching the field through a private copy of struct iommu_group pasted into the driver
(rknpu_iommu.c) โ layout-fragile, and silently wrong if the struct ever changes.
That overwrite is also load-bearing, not gratuitous: iommu_detach_device() โ
__iommu_group_set_core_domain() reattaches group->default_domain, and __iommu_attach_group()
returns -EBUSY unless group->domain == group->default_domain. Delete the line on its own and
switching stops working entirely.
Consequence for anyone replacing the switch (we did, Kernel-Modifications change 18): the moment the IOMMU core stops tracking your switches, every implicit DMA-API user silently targets domain 0. That produced three separate bugs for us (changes 20, 21, 22), each presenting as "job committed, never completes, no interrupt, no error".
2. โ
iommu_get_domain_for_dev() is NOT the live domain (once you own the switch)
It returns group->domain, which only tracks core attach/detach. Under a driver-owned switch it
reports the group's default forever. Every caller in the driver was wrong after change 18:
the post-map IOTLB flush, the SRAM/NBUF cache iommu_map/iommu_unmap, and both reset paths.
The grep is worth repeating after any change to the switch. It will not find the implicit ones โ see ยง3.
3. โ โ dma-buf imports: the domain is decided TWICE, and neither is the one you asked for
This was the multi-domain "dispatch stall" (change 22).
dma_buf_map_attachment()runs inside DRM core'sPRIME_FD_TO_HANDLE, before any rknpu hook, and maps through the DMA API โdefault_domain. Aiommu_get_domain_for_dev()grep cannot find this; the DMA API does it implicitly.PRIME_FD_TO_HANDLEcarries no domain at all. The target is named only by the subsequentMEM_CREATEโ which for an already-imported handle does not re-map. So the mapping lands wherever the device happened to be at PRIME time.
Native allocations are immune: their MEM_CREATE does the switch and the mapping together.
Related, older: rknpu_gem_prime_import_sg_table() did not record the domain at all, so destroy
unmapped from domain 0 and stranded the IOVA (Kernel-Modifications change 3).
Rule: make the target domain live BEFORE PRIME_FD_TO_HANDLE, and scope any default-domain
override across the attachment map and its matching unmap at destroy.
4. rknpu_soft_reset() entangles an IOMMU detach/attach with the hardware reset
The reset wipes the MMU, and the vendor re-establishes it with
iommu_detach_device(iommu_get_domain_for_dev(dev)) + iommu_attach_device(...). Correct only while
ยง1's overwrite is in force. There are TWO such paths โ the device-wide rknpu_soft_reset() and
its per-core twin (RKNPU_ACT_RESET_CORE, also driven by the progress watchdog). Patch one and miss
the other and the bug simply moves to whichever path the watchdog uses.
Contrast with mainline: accel/rocket's rocket_core_reset() performs no IOMMU operations at
all โ only reset-line assert/deassert. The vendor design is the outlier here.
5. The domain refcount is a wedge waiting to happen
rknpu_iommu_domain_get_and_switch() proceeds only when iommu_domain_refcount reads exactly 0.
So a single unbalanced put, or one job that never retires, means:
- every subsequent switch burns its full
RKNPU_SWITCH_DOMAIN_WAIT_TIME_MS(6 s) and fails, and - because
rknpu_gem_object_create()switches domains, every allocation fails-EINVALโ which surfaces asMEM_CREATE errno=22at zero IOVA used. That is not exhaustion; do not read it as a memory problem.
Recovery must reap and retry; reaping and then returning the error anyway (the original escalation) helps nobody, because the caller that triggered it still fails.
6. rknpu_job_timeout_clean() compares microseconds against milliseconds
if (job && ktime_us_delta(ktime_get(), job->timestamp) >= job->args->timeout)
args->timeout is in ms (msecs_to_jiffies(args->timeout) in the wait; the log prints
args->timeout * 1000 as ยตs). So the reap fires 1000ร too eagerly. Counter-intuitively this is
load-bearing: "fixing" the units made stalls 2.6ร worse (2026-08-25). Leave it alone unless you
are prepared to re-validate the whole reap path.
7. A "blocking" submit only blocks if RKNPU_JOB_PC is set
} else {
rknpu_job_schedule(job);
if (args->flags & RKNPU_JOB_PC)
job->ret = rknpu_job_wait(job);
ret = job->ret;
Without RKNPU_JOB_PC the path schedules and returns job->ret (0) immediately โ success for a job
that may never have run. We instrumented for this specifically (dbg_false_ok, dbg_nopc); it does
not fire in our workload because ork_ppflags() always sets PC, but any new submit flag combination
must keep it set.
rknpu_job_wait() itself is bounded (if (++wait_count >= 3) break;) โ it does not loop forever.
8. Other sharp edges
soft_resetinggates dispatch.rknpu_job_next()returns early while a soft reset is in progress, so work committed in that window is not scheduled.- The IRQ handler touches MMIO unconditionally. A late or spurious interrupt after
rknpu_power_off()(a deferred work item) takes an external abort and kills the board instantly โ no console, no ping. Guarded by apower_refcountcheck (change in Kernel-Modifications). rknpu_job_free()never signals the fence on the abort path, so every waiter blocks until its own timeout โ which defeats waiting on a fence to detect a stall.rknpu_gem_object_destroy()bails after 3 failed domain switches and returns without freeing. Deferring those objects for later reclaim does not help: reclaim needs a switch into the very domain that is unreachable (measureddeferred=20, reclaimed=0).- IOVA is 32-bit per domain (
rk_iommuv2): ~4 GiB periommu_domain_id, and that is the entire reason multi-domain exists. See npu-iova-4gb-cap context in the Experiment Log.
9. Debugging rules earned the hard way
-
MEM_CREATE errno=22at low IOVA is a stuck domain refcount, not memory pressure. -
A dmesg
NO TASK PROGRESSline is a watchdog DETECTION (120 ms threshold), not a failure. A cold weight-DMA can exceed it and still complete bit-exact. -
"Job committed, task counter
0x0, no interrupt, no error" does NOT imply silicon. It never has been, in this project. But it is not always a page-table mismatch either โ that was true of every instance until 2026-09-03, and the rule as previously written ("every instance was a page-table mismatch") sent this investigation down the wrong path for a while. The counterexample: an async submit whose completion interrupt was dropped because the block had been powered down under the running job, with the domains matching perfectly (submit_dom == weight_dom). See Kernel-Modifications โ#patch73.Order to check, cheapest first:
cat /sys/kernel/debug/rknpu/countersโunpownonzero means completions are being dropped;blocked_slownonzero means a core has an owner it will never release.dmesg | grep "NOT dispatching"โ names the stuck owner (flags, age,int_cnt,run_cnt).int_cntstill 1 withrun_cnt0 means dispatched-but-never-completed, not a page-table problem.- Only then compare submit vs weight domain.
Suspect the domain before the hardware โ but suspect job accounting before the domain.
-
job iommu domain idvsdev iommu domain idin the job-wait log is a cheap mismatch check โ but it compares the driver's bookkeeping on both sides, not the hardware page table. -
Mainline
accel/rockethas no stall detection at all โ no task-counter monitoring, no missing-interrupt logic, just a 500 msdrm_schedtimeout โ core reset. Its silence is not evidence that a stall is normal silicon behaviour.