Repository navigation
Vendor Kernel Behaviour
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.
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".
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.
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.
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.
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.
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.
} 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.
-
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.
-
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.