Skip to content

Vendor Kernel Behaviour

Michael Fischer edited this page Sep 3, 2026 · 3 revisions

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:

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).

  1. dma_buf_map_attachment() runs inside DRM core's PRIME_FD_TO_HANDLE, before any rknpu hook, and maps through the DMA API → default_domain. A iommu_get_domain_for_dev() grep cannot find this; the DMA API does it implicitly.
  2. PRIME_FD_TO_HANDLE carries no domain at all. The target is named only by the subsequent MEM_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 as MEM_CREATE errno=22 at 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_reseting gates 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 a power_refcount check (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 (measured deferred=20, reclaimed=0).
  • IOVA is 32-bit per domain (rk_iommu v2): ~4 GiB per iommu_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=22 at low IOVA is a stuck domain refcount, not memory pressure.

  • A dmesg NO TASK PROGRESS line 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:

    1. cat /sys/kernel/debug/rknpu/counters — unpow nonzero means completions are being dropped; blocked_slow nonzero means a core has an owner it will never release.
    2. dmesg | grep "NOT dispatching" — names the stuck owner (flags, age, int_cnt, run_cnt). int_cnt still 1 with run_cnt 0 means dispatched-but-never-completed, not a page-table problem.
    3. Only then compare submit vs weight domain.

    Suspect the domain before the hardware — but suspect job accounting before the domain.

  • job iommu domain id vs dev iommu domain id in 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/rocket has no stall detection at all — no task-counter monitoring, no missing-interrupt logic, just a 500 ms drm_sched timeout → core reset. Its silence is not evidence that a stall is normal silicon behaviour.

Clone this wiki locally