NPU Quirks - oRKLLM/ork-driver GitHub Wiki
Vendor-driver behaviour lives on its own page: Vendor-Kernel-Behaviour. This page is silicon/regcmd behaviour; that one is the shipped
rknpudriver's own contracts and defects (IOMMU domains, imports, reset, refcounts). Several "hardware stalls" turned out to be entries there β in particular, job committed / task counter 0x0 / no interrupt has never once been silicon.
Hardware and rknpu-driver behaviours that are silent, order-dependent, or counter-intuitive β the kind that corrupt output or wedge the NPU without an obvious error, and that a fresh reader would otherwise re-derive the hard way. Each entry: the symptom, the root cause, and the guard ork-driver applies.
Scope note: perf-tuning levers live in the Optimization Roadmap; register/encoding detail lives in the regcmd ISA Reference; userspace safety layers in Robustness & Defensive Guards. This page is for "you must do X first or it breaks, and nothing tells you" traps.
The >4 GiB sliding window keeps weights resident across several 32-bit IOMMU domains (n_domains, dom_activate per submit β see NPU IOVA 4 GiB cap). Weights beyond domain 0 are imported zero-copy (dma-heap ALLOC β PRIME_FD_TO_HANDLE β MEM_CREATE) rather than native bcreate. Three separate constraints bite on this path β all root-caused 2026-07-06. Reproducer: tools/mc_import_probe.c.
β οΈ Validation trap first: verify bothC[0]andC[last](and ideally a full CPU-ref compare). The corruption here frequently leavesC[0](first output column) correct while wreckingC[last]. A probe that checks onlyC[0]gives a false pass β this misled the original diagnosis (tools/chain_import_probe.cchecked onlyC[0]and "passed" while output was actually corrupt).
-
Symptom: an imported (dma-buf) weight in a non-0 domain produces non-deterministic wrong results β dropped K-partials, e.g.
C[last]=14336then16896then13312across runs, expected18944.rc=0, no error, no wedge. Bad enough occasionally to wild-address β a 60 s NPU job timeout (job abort, ret: -22). -
NOT a multi-core bug (the tempting conclusion): forcing single-core still corrupts (core 0 alone:
C[0]=18432wrong). The per-coreC[0]-ok /C[last]-wrong pattern is just non-deterministic noise. Native weights on the same non-0 domain are always correct. -
Root cause: the
rknpudriver lazily initialises a domain's IOVA allocator / page table on its first buffer. If that first buffer is an imported dma-buf (SG-list), the import's pages land on wrong/aliased IOVAs β the NPU reads garbage for some weight tiles. A native allocation exercises the domain-setup path correctly; imports after that are bit-exact. -
Guard:
ork_dom_prime(c, dom)β one tiny (64 KB) nativebcreateper non-0 domain, before any import maps into it (no submit needed; alloc alone establishes the domain).dom_anchor[ORK_MAXDOM], once per domain (guarded), freed at teardown. Cost: 4 tiny allocs for a 7B, independent of chunking. Called at the top ofork_mm_load_i8_import. -
Upstream: looks like a genuine kernel first-buffer-domain-setup ordering bug (native-first works, import-first corrupts) β worth reporting / fixing in
rknpu_iommu_dma_map_sg/ the domain lazy-init; until then the anchor stays. Tracked as Optimization Roadmap Tier 10c.
-
Symptom: a K-split submit that PC-chains many programs (e.g.
ffn_downK=18944 βtask_number=19), each referencing a separately-imported dma-buf mapping, is rejected:RKNPU_SUBMITerrno=110(rc=-1,C=0). The CDMA chain-walker can't traverse that many foreign SG-list mappings. Nativebcreatechains of the same length are fine; ~6 imported mappings works, ~19 faults. -
Guard: size-bounded chunked consolidation β pack a weight's tiles into a handful of imported chunks (tiles are
chunk.dma+offviews), so a chain walks few mappings.ork_mm_load_i8_importown_bufs[],ORK_IMPORT_CHUNK_MB(default 16 β down-proj β 5 chunks).
-
Symptom: importing a single dma-buf β³ 16β64 MB fails at
PRIME_FD_TO_HANDLE: Cannot allocate memory(16 MB OK, 64 MB ENOMEM; full 68 MB βWEIGHT NULL, an earlier variant hung the load R-state 571 s). The 32-bit domain's IOVA allocator can't find a contiguous range that large (fragmentation) β so it likely shrinks as a domain fills. - Guard: the same chunking (Quirk 2) caps chunk size from above. 16 MB is a genuine sweet spot bounded on both sides β per-tile (many mappings) trips Quirk 2; 64 MB trips Quirk 3. Cannot consolidate a weight into one buffer.
Net: with the anchor (Q1) + 16 MB chunks (Q2+Q3), the zero-copy multi-domain import path is correct at full multi-core speed β validated mc_import_probe 5/5 bit-exact and 7B forced-import multi-core pp128 = 58.73 t/s, no wedge.
-
Symptom: a submit that runs a standalone SDP activation op (act-LUT:
exp/rsqrt/siluβork_npu_exp_i16etc.) immediately followed by a separate matmul submit on the same context reliablyRKNPU_SUBMITerrno=110(ETIMEDOUT) and trips the self-healing reset β per row-batch, which then discards the (correct) NPU activation output and falls back to CPU. The reverse order β matmul then activation β is fine (the on-NPU norm path does reduce-matmul βrsqrt-LUT with no stall; the fused-output SiLU rides the matmul's own SDP stage). Found building the composed softmax (ork_npu_softmax_f16:expon NPU, then a Ξ£ reduce-matmul). - Root cause (open-source cross-reference): the RK3588 NPU is NVDLA-derived, and NVDLA's datapath is a fixed unidirectional pipeline β Convolution/MAC β SDP β PDP β CDP β with no backward path (NVDLA hwarch). Activation (SDP) sits downstream of the MAC. So "matmulβactivation" is with the dataflow (and is exactly NVDLA fused mode, where the conv core streams into SDP via FIFOs); "activationβmatmul" runs against it β SDP output would have to re-enter the conv core, which the hardware has no path for. Chaining a standalone-SDP submit into a fresh matmul submit leaves the pipeline mis-staged and the matmul never drains β timeout.
-
Did the open stack note it / work around it? No β because it never issues this sequence. NVDLA/Rocket run activations as the fused output stage of a convolution (fused mode), or as a standalone memory-to-memory op in "independent mode" which "begins and ends with the assigned block performing memory-to-memory operations" (NVDLA hwarch) β i.e. a standalone SDP op lands its result in DRAM and is fully decoupled; it is never PC-chained backward into a matmul. The mainline accel/rocket driver + its Mesa userspace is convolution-centric and only resets the core at init (
rocket_core_resetinrocket_core.c, called fromrocket_core_initβ not per-job), so it has no per-submit state-scrub either; it simply doesn't produce the offending order. There is thus no documented workaround to borrow β the architecture just doesn't do activationβmatmul, and neither should we. -
Guard (ork-driver), interim: don't chain a standalone SDP activation into a matmul. Two safe shapes: (1) keep the activation on the NPU and do the following reduction/matmul on CPU β what
ork_npu_softmax_f16does (expon NPU, Ξ£+normalize on CPU); (2) when a reduction must precede the activation, order it matmulβactivation (the norm/rsqrtpath). -
The REAL fix β PC-chain in ONE submit (what the vendor does), CONFIRMED by capture (2026-07-09): a full softmax capture (
nn.Softmax, rknn-toolkit2) decodes to a 9-task PC-chained pipeline in a single submit β row-max β broadcast βxβmaxβ load-exp-LUT β exp (SDP) β Ξ£ = expΒ·ones (MATMUL) β1/Ξ£βexpΒ·(1/Ξ£)β output. Crucially the Ξ£ is a matmul that runs right after the exp SDP op β the exact activationβmatmul order that ETIMEDOUTs as two separate submits. It works because every task's0101:0x0010points to the next task's regcmd, so all 9 run within one submit β the pipeline never crosses a submit boundary / mode-switch re-warm. So the asymmetry is a per-submit-boundary effect, not a hardware "no backward path" law: SDPβMAC is fine when PC-chained in a single submit.ork_npu_softmax_f16stalled only because it issued exp and the reduce as two submits. The forward reduction is NOT window-capped β validated on-NPU for reduction length N up to 1024 (refutes the CDP LRN nβ€9 concern), and the geometry maps exactly onto attention scores[M,N]softmaxed over N (0x4058=Nβ1,0x4030=min(M,128)β1). Full register map + dataflow:FWD_SOFTMAX_RE_WIP.md/ regcmd ISA Reference. -
CONFIRMED on silicon (2026-07-10):
ork_npu_replay_softmax_f16runs the full vendor 9-task forward softmax on-NPU, bit-exact (uniform input β all outputs = 1/N). The correct mechanism is KERNEL-SEQUENCED per-task submits (onetask_number=1program per task, in dataflow order), NOT hardware PC-chaining β see the accel/rocket driver (one task per PC program, kernel re-arms on IRQ). Hardware-chaining attempts (concatenated regcmd +0101:0x0010+task_number=9) stalled at the chained LUT load; per-task submits (intermediates persisting in resident buffers, no reset between so the exp LUT survives) sail through. This is the empirical proof that the activationβmatmul asymmetry is a per-submit-boundary/reset effect, resolved by keeping the tasks in one un-reset sequence. Interimork_npu_softmax_f16(exp on NPU + Ξ£ on CPU) still applies until the geometry-parameterized replay is wired as a primitive. -
CORRECTION β the standalone-op cross-submit case has a concrete software root cause (2026-07-12,
tools/mode_probe.c): the "standalone SDP activation β separate matmul submit ETIMEDOUTs" case is not the NVDLA "no backward path" dataflow law β it is a stale shared task descriptor left by the LUT-activation op. Proof from a 2-op transition sweep:ewmul_i16/ewmul_f16/add_f16β matmul is SAFE, andexp_i16/silu_i16β multi-core matmul is SAFE β both impossible if SDP genuinely could not feed the MAC. Only LUT activation (exp/silu) β single-core matmul wedged. See the dedicated quirk LUT-activation op poisons the shared task descriptor below and Exp-2026-07-12. (The single-submit PC-chain / kernel-sequenced replay above remains the right structure for real graphs like softmax for other reasons β LUT-load ping-pong, resident intermediates β but per-op interleave is now safe too.)
-
Symptom: a standalone on-NPU LUT activation (
ork_npu_exp_i16/ork_npu_silu_i16/gelu/rsqrtβ all viaact_lut_i16βork_npu_probe_silu_std_i16) followed β not necessarily immediately β by a single-core matmul (small N, e.g. the SSD CumBAbmmat N=16)RKNPU_SUBMITerrno=110(task counter: 0x0β soft reset).dmesgshows a real job timeout. The activation itself succeeds; the matmul wedges.ACT_RESET,ORK_NO_PINGPONG, and a mode-invalidate do NOT clear it (measured βtools/mode_probe.cE2/E3/E4) because it is not a hardware-pipeline state. -
This is why
test_ssd_chunk_npumode 2 wedged on CumBA "mid-sequence" while mode 1 (sameexpβbmm) survived: mode 1's matmuls (Nβ₯64) all run multi-core; mode 2 adds the CumBA (N=16), the only single-core matmul, which reads the poisoned descriptor. -
Root cause: the LUT op memsets the SHARED context task descriptor
c->taskto its own SDP program (regcfg_amount1097 for the LUT-load, then 69 for the activation;enable_mask=0x18) and left it that way β it did not restore it. The single-core matmul path (run()βsubmit1) does not rebuildc->task; it relies on the init value (regcfg_amount=108,enable_mask=0xd,regcmd_addr=c->regcmd.dma, set once inork_npu_init) persisting. So a later single-core matmul submits its 108-word matmul regcmd under the stale 69-reg /0x18SDP descriptor β the NPU reads a truncated/mis-typed program, dispatches no task (task counter 0x0) β timeout. It is a poisoned software descriptor, not HW state, so a hardwareACT_RESETcannot fix it. -
Why the trap is delayed & intermittent: multi-core matmuls use per-core
c->mtk[](rebuilt every call) and are immune; the plain 2-input SDP ops (ewmul/add) save+restoreregcfg_amount/enable_maskaround their submit. Soc->taskcan stay poisoned across several ops (a multi-core matmul, then anewmulthat faithfully preserves the poisoned 69) until the next single-core matmul finally trips on it β which reads as "contextual/mid-sequence", not a clean AβB pair.
Transition matrix (RK3588, two ops as SEPARATE submits, warm context β tools/mode_probe.c):
| first op (A) | second op (B) | before fix |
|---|---|---|
| matmul (any dtype) | any SDP (exp/silu/ewmul/add) |
SAFE (matmul never poisons c->task) |
ewmul_i16 / ewmul_f16 / add_f16 (non-LUT SDP) |
matmul (single- or multi-core) |
SAFE (these save/restore c->task) |
exp_i16 / silu_i16 (LUT activation) |
multi-core matmul (Nβ³64) |
SAFE (rebuilds per-core c->mtk) |
exp_i16 / silu_i16 (LUT activation) |
single-core matmul (small N, e.g. N=16) |
WEDGE errno=110
|
-
The int8 twin was missed for 6 weeks β fixed 2026-08-22. The save/restore below landed on the i16
LUT path only (
npu/i16/act.c).ork_i8_npu_probe_silu_std(npu/i8/probe_sdp.c, behind everyork_i8_npu_silu/gelu/rsqrt/exp) kept poisoningc->taskand returning. Symptom on the int8 side was a 2500 ms stall rather than an obvious wedge βmode_probescored itok(0,2500)= "safe", so the matrix never flagged it β but the stall reset-stormed and eventually Oops'd the kernel inrknpu_gem_object_destroy(Exp-2026-08-22). Fixed by the same save/restore (whole-struct, since the activation setup memsets the descriptor), on every exit including the LUT-load wedge path.GELU_I8 -> MM_F16went 2500 ms -> 0.1 ms, and the fullmode_probesweep went from hanging-and-killing-the-board to 56 pairs in 48 s, 0 WEDGE. Measured proof it is not a mode-transition problem (fix ladder on the same pair):none = 2500 ms,invalidate = 5106 ms,reset = 5107 msβ a HWACT_RESETmakes it WORSE, because the cold re-warm is a 2-pass and each pass stalls. Do not reach forXSPEC/setdthere. -
Guard / fix (
ork_npu_probe_silu_std_i16, and since 2026-08-22ork_i8_npu_probe_silu_std): the LUT op savesc->task'sregcfg_amount/enable_mask/regcmd_addron entry and restores them on every exit β exactly the save/restore theewmul/addops already do. After the fix every transition above is SAFE with no interposed reset;test_ssd_chunk_npumode 2 goes from a hard wedge to PASS (full-G1 rel-L2 5.8e-3, full-G2 5.9e-3). The minimal safe-sequencing rule for composing heterogeneous per-op NPU sequences is therefore just "every op must leave the sharedc->taskas it found it"; a fused single-submit chain is not required for correctness (it remains preferable for the submit-floor + LUT-ping-pong reasons above). -
Diagnostic knobs added alongside:
ORK_MM_TIMEOUT(matmul-path submit timeout, mirrorsORK_EW_TIMEOUT; default 60000 β set low so a wedge-search self-terminates in ~2 s);ork_npu_mode_invalidate()/ork_npu_mode_reset()(the interposed-fix hooks the probe used to proveACT_RESETdoes not help). See Exp-2026-07-12.
-
Symptom: a multi-task hardware-chained submit (
task_number>1, the HW walks0101:0x0010βnext) that contains a LUT-load task stalls non-deterministically at the LUT-loadβconsumer transition β the kernel job times out (errno=110,raw_status 0x80000000/0xc0000000= a busy/hang state, NOT a maskable interrupt bit) with the task counter flaky (e.g. 4β5 across identical runs). Fewer chained tasks can stall earlier than more β the giveaway that it's a race, not a deterministic wall. -
Root cause: submit
flagsbitRKNPU_JOB_PINGPONG(1<<2, i.e. the0x4in the common0x5) enables double-buffered register banks (NVDLA PRODUCER/CONSUMER groups): the HW loads task N+1's register config into the alternate bank while task N runs, then swaps banks the instant task N's register config is done. That's a throughput win for register-config-only tasks (uniform matmuls) β but a LUT-load streams ~1000 entries into DPU LUT SRAM, a long-latency side effect NOT represented in the register bank. Ping-pong's fast swap advances to the consumer (e.g. the exp op) before the LUT SRAM has committed β the consumer reads a half-written LUT β hang/race. -
Guard: for any chain containing a LUT-load (or any task with an un-banked side effect), submit with
ping-pong OFF (
flags = RKNPU_JOB_PC=0x1, not0x5). The HW then fully completes each task β including the SRAM commit β before advancing. This is exactly what the vendor does: the captured softmax chain usesflags=0x1, while pure int8-matmul chains (run_chain_i8,flags=0x5) safely keep ping-pong on because their tasks are register-config-only. Validated: the full 9-task forward-softmax graph runs as ONE submit, bit-exact 9/9, ~471Β΅s (vs ~2.5ms per-task) once ping-pong is off (ork_npu_replay_softmax_f16). Full recipe (all required): contiguous single-buffer image + single-delta rebase (so the LUT-load writes into the regcmd region as the vendor lays it out) +0101:0x0014next-amount=(n+3)/2+ ping-pong off. See regcmd ISA Reference /FWD_SOFTMAX_RE_WIP.md.
This is the root cause of what the section below calls the "int4 doorbell miss". That name is wrong β it named the first place the symptom was seen. The failure has nothing to do with int4, with the doorbell, or with NONBLOCK submission.
A job committed within ~1 ms of an
iommu_attach_device()domain switch may fail to dispatch: accepted by the driver, PC completed-task counter stuck at0x0, no interrupt, no IOMMU fault, MMU idle. Beyond ~1 ms after a switch, it does not happen.
| workload | commits | stalls |
<1 ms after switch |
>=1 ms after switch |
|---|---|---|---|---|
1.5B-Q8 via blocking ork_dyn_colsplit
|
1155 | 86 | 86 / 159 = 54.1% | 0 / 996 |
| int4 probe via the async doorbell, ndom=2 | 12070 | 5 | 5 / 2997 = 0.2% | 0 / 9073 |
Zero stalls outside the window across ~10,000 commits, on two paths that share nothing but the kernel submit code.
Tempting to read this as "the first N commits after a switch are poisoned". The int4 probe separates the two: 9004 commits sat at position 0-2 after a switch and only 5 stalled β and all 5 were in the sub-1 ms subset, while 8993 early-but->=1 ms commits were clean. Position is a proxy; elapsed time is the operative variable.
Inside the window the rate varies enormously by workload and shape β tn=2 is 75.7% in the 1.5B run
and 0.2% in the int4 probe; within one run tn=4 is 90.9% while the larger tn=9 is 25.0%, so it is
not monotonic in size either. Something else modulates it. The window is a necessary condition, and
removing it removes every stall observed so far, but do not present it as the whole mechanism.
Each looked solid and each was a confound with switch-proximity:
-
"Domains >0 are stall-prone" (dom 2 at 42.9% vs dom 0 at 3.0%). Domain 2 is merely visited
briefly, so most of its commits land just after a switch. Controlling for position: dom 0 = 0/180,
dom 1 = 0/171, dom 2 = 0/20. The
iova_reserve_iommu_regionsasymmetry between domain 0 and the others is real but is NOT this bug. - "Stalls follow back-to-back commits (short inter-commit gap)". The first jobs into a new domain arrive in a burst, so short gaps co-occur with post-switch position. Enforcing minimum spacing between all commits gave no dose-response and inverted (500 us -> 12 stalls, 1500 us -> 46).
-
"
ndom=2triples the stall rate, therefore domain switching is expensive". Right observation, wrong reading: more switches means more exposure to the window.
dom_cooldown_us (rknpu, default 0) sleeps after a successful switch before the domain is released for
use. It can usleep_range() rather than busy-wait because rknpu_iommu_switch_domain() is only
reachable from process context, and it is charged once per SWITCH (~56 in a 1.5B run), not per commit.
Userspace already had the intuition and never quantified it: src/npu/core/domain.c:223 comments on a
"first-submit-into-a-freshly-switched-domain hazard" and ork_dom_prime() exists for it.
Per-commit kernel profiling of BOTH populations. The stalled population alone was already known from userspace logs and was useless without the healthy base rate β every wrong turn above came from comparing a numerator against the wrong denominator. See "Measurement discipline" below.
SUPERSEDED β see the section above. The name is a misnomer: the same failure occurs on plain BLOCKING submits with no doorbell and no int4 involved. The cause is committing within ~1 ms of an IOMMU domain switch. The observations below about the failure's signature still hold; the framing and the suspected causes do not.
Status: software path fully exonerated with controls; cause still open (hung sub-block or silicon). The practical target is CHEAP RECOVERY, not prevention β see the protocol at the end.
On a multi-domain int4 (W4A4) prefill, some doorbell rounds never complete. Cost is
time = 23.01 s + 3.61 s x misses (R2 0.967) β ~23% of 27B prefill. Numerics are never wrong
(PPL bit-identical 10.6780 on every run at every setting): recovery always produces the correct answer, so
this is purely a throughput defect.
It is a DISPATCH failure, not a stall. A kernel watchdog sampling the PC completed-task counter at 1 kHz reports, on every miss, all three cores at once:
core 0 NO TASK PROGRESS for 64 samples (64000 us), tasks=0/832 done (counter 0x0), cm=0x1 dom=1, job age 114993 us
The submit returns rc=0, the job is committed, and the hardware never executes a single task.
Control (essential β a dead register reads identically): at an 8 ms threshold a HEALTHY ~30 ms job does
NOT report β 3 reports across ~900 core-jobs, not hundreds. The counter genuinely advances on good work.
The output pattern misleads. The surface shows a contiguous written PREFIX ending on a 64-element
boundary. That is NOT a partial write: cores get CONTIGUOUS op ranges (lo = i*S/nc, hi = (i+1)*S/nc), so a
core that never runs leaves its contiguous slice at the sentinel. Nothing was written and then stopped.
| stage | verdict | evidence |
|---|---|---|
| queue / lost interrupts | OK |
commit-done == stall count exactly; irq == done; nojob = 0
|
| commit register writes | OK | full PC/INT block byte-identical healthy vs stalled |
| IOMMU mapping | OK | zero page faults / BUS_ERROR / not-attached across 14 stalls |
| task descriptor freshness | OK | monotonic stamp in rknpu_task.op_idx reads 737..6397 β always current |
| regcmd bytes in DRAM | OK | checksum cache vs post-invalidate DRAM: 0 mismatches; positive control DOES fire |
timeout_clean killing live jobs |
REFUTED | counts match, but the reaped job is 157 ms old = reaped during recovery |
| missed start trigger | REFUTED | re-pulsing PC_OP_EN on a stall: 4 kicks, 4 stalls, none recovered
|
| ping-pong | REFUTED | A-B-A n=4000: 146 / 111 / 116 β OFF sits inside the ON bracket |
| uneven K-split geometry | REFUTED | even split fails at least as often as uneven, on BOTH paths |
| miss-window / detection tuning | REFUTED | rate flat across 300 ms / 614 ms / 2 s / 30 s windows |
| read visibility / cache | REFUTED | kernel FROM_DEVICE re-count recovers nothing |
Register map for the block (vendor rknpu_ioctl.h): 0x00 VERSION, 0x08 PC_OP_EN, 0x10 PC_DATA_ADDR,
0x14 PC_DATA_AMOUNT, 0x20 INT_MASK, 0x24 INT_CLEAR, 0x28 INT_STATUS, 0x2c INT_RAW_STATUS,
0x30 PC_TASK_CONTROL, 0x34 PC_DMA_BASE_ADDR, 0x3c PC_TASK_STATUS, 0xf008 ENABLE_MASK.
ENABLE_MASK = 0 and INT_STATUS = 0 are NORMAL β they read the same on jobs that run. Do not chase
either. INT_RAW_STATUS = 0 too, so the hardware is not signalling internally.
| lever | effect |
|---|---|
retire the prior job before switching domains (orki_dom_drain) |
11.47% -> 1.99% (B-A-B, n=8000/12000) |
| programs per submit | Sk=1 0.15%, Sk=2 0.38-0.62%, BCHAIN ~850 progs 2-3% |
| number of IOMMU domains | ndom=2 roughly triples the rate vs ndom=1 |
| K geometry (even vs uneven split) | no effect β do not constrain alignment |
The failing shapes are not special, they are simply the BIGGEST: the 27B ffn_down is the largest, longest
int4 op in the graph, which is why 12/12 model misses landed there.
The drain barrier does NOT transfer end-to-end: 5.8x on the probe, a WASH on the real 27B (8 vs 7 misses). The probe switches domains every rep so its rate is race-dominated; the model is residual-dominated. Weight any probe result against a model run before believing it transfers.
A miss costs ~3.6 s: a ~614 ms guessed detection window, then a device-wide ACT_RESET, then a
resubmit. rknpu_soft_reset() asserts ALL SIX DT reset lines and msleep(100)s, and sets a global
soft_reseting flag that makes rknpu_job_next() bail for every core β so recovering one core resets
the whole NPU and destroys the other two cores' warm state.
The six lines are PER-CORE: reset-names = "srst_a0","srst_a1","srst_a2","srst_h0","srst_h1","srst_h2"
(AXI + AHB per core; index N and N+3). So a targeted per-core reset is possible and is the cheap-recovery
lever. RKNPU: soft reset, num: 6 prints num_srsts, the COUNT of DT reset phandles β it is a constant,
not a reset counter. Count log lines instead.
- Retire before switching domains (
ORK_DOM_DRAIN, default on). - Bound programs per submit; prefer fewer, smaller submits over one huge one.
- Batch work per domain to minimise switches.
- Recover per-core, not device-wide, and detect from the task counter rather than a guessed timeout.
-
netconsole WORKS and is the only thing that caught the hard-wedge panic. Persisted via
/etc/systemd/system/netconsole.service(must beAfter=network-online.target;modules-load.druns too early to bind eth0) +/usr/local/sbin/netconsole-up.sh. netpoll sends RAW frames so it needs a destination MAC β the gateway's MAC is the one that reaches the capture host here. -
β
console_logleveldefaults to 4, so only messages BELOW level 4 reach the console and hence netconsole.<4>/KERN_WARNING test messages are silently dropped; this cost an hour of wrongly concluding netconsole was broken until a<3>/KERN_ERR message arrived instantly.kernel.printk = 8 4 1 7is now persisted in/etc/sysctl.d/. Absence of low-priority lines in an old capture is NOT evidence the event did not happen βRKNPU: soft resetis LOG_INFO (level 6) and was invisible at loglevel 4. -
pstore/ramoops DOES NOT WORK on RK3588. Regions are reserved and re-registered every boot, but
/sys/fs/pstorecomes back empty even after a warm panic-reboot β the Rockchip restart re-initialises the DDR controller and wipes the region. Do not rely on it. -
/var/logis a 50 MB tmpfs (DietPi RAMlog,AUTO_SETUP_LOGGING_INDEX=-1) and journald retains no prior boots (journalctl -b -1-> "No journal boot entry found") despiteStorage=persistent. - A durable fallback: a reader on
/dev/kmsgwriting/home/michael/ork-outbox/kern.logwithfsyncper message, on the NVMe, outside RAMlog.
Kernel log capture that actually survives: /var/lib/ork-logs/kmsg.log (systemd unit
ork-kmsglog, dd if=/dev/kmsg ... oflag=dsync,append, on the NVMe rootfs). Two failure modes it
avoids, both of which cost a dataset here:
-
dmesgalone is not enough β the kernel ring OVERFLOWS on a bad run and silently truncates, which makes a coverage denominator read as zero. -
/var/logis tmpfs on this board (DietPi RAMlog, 50 MB), so anything written there is WIPED ON EVERY REBOOT and a verbose profiling run can fill it. An entire per-commit profile was lost to a reboot before it could be analysed.
The unit rotates the previous boot's capture to kmsg.log.prev on start, so the log from before a
crash or reboot is still readable afterwards β which is the case that matters for wedges.
-
A count that matches is not a cause. THREE false root causes this session came from exactly that:
lost interrupts, marginal K geometry, and
timeout_cleankilling live jobs. The last one hadtreap == stalls == commit-doneβ a perfect match β and was only caught because the KILL AGE was logged alongside (157 ms, not the 1.5 ms the theory needed). Always log a second independent quantity β an age, a sequence, a checksum β next to any count that "matches". - Always run the positive control in the same pass. "No mismatches" is worthless until the detector is shown to be able to fire; the regcmd checksum only became evidence once a deliberately corrupted word produced a mismatch.
-
Match the control to the subject. Comparing a 1-task ping-pong-off job against a 2-task ping-pong-on
stall produced two convincing false signals (
task_ctrlreadback,status(pre)0x7000 vs 0x5000). Both vanished with matched jobs. - Never sample a periodic workload on a modulo. The workload repeats a 4-job cycle (one real op plus three drain dummies); sampling every 500th completion aliased (500 %% 4 == 0) and captured ZERO matching jobs. Log the first N matches instead.
- Sample size. At a ~2% rate, n=400 gives ~10 events (Poisson +-32%) β direction only. Two wrong conclusions came from n=60 runs. Use >=4000 for any rate comparison, and bracket A-B-A: the absolute rate DRIFTS between sessions (the same config measured 5.25% and ~11% on different runs).
-
Instrumentation perturbs.
ORK_KMSG_SUBMIT(one extra syscall per submit) moved the stall rate 3.25% -> 6.00%. Use it to localise an event, never to quote a rate. -
Verify the knob fired. A
pp=1A/B returned a clean "no effect" from code that never executed (dyn.cearly-returns for int4). It was caught only because removing HW chaining failed to produce the expected slowdown. Print a one-time confirmation from inside the instrumented path. -
Rebuild the right target.
make i4_widek_stall_probeis a separate rule; a plainmakeleaves it stale, and a stale probe silently measures the old code.
ORK_RC_UNCACHED=1 (dropping RKNPU_MEM_CACHEABLE) makes the probe die with SIGBUS: the kernel then
maps the buffer as Device memory, and ARM64 faults on UNALIGNED ACCESS TO DEVICE MEMORY, which the regcmd
memcpy/NEON writes rely on. Not a viable way to test coherency.
-
int8 mode entry needs an NPU reset. The first int8 submit wedges unless the NPU is reset when switching into int8 (fp16 cold-starts stale and the warmup handles it; int8 does not).
run()issuesRKNPU_ACT_RESETon the fp16βint8 transition, then re-warms. Mixing dtypes on one context works but each mode switch triggers a one-time re-warm. Seesrc/npu.crun()and AGENTS.md Β§5. -
Never
kill -9an in-flight submit.SIGKILLmid-RKNPU_SUBMITwedges the NPU/IOMMU (and can corrupt SPI on a hard power-cut), forcing a reset/power-cycle. UseSIGTERM+ drain, andtimeouton board NPU commands. See AGENTS.md Β§3. -
Weight-DMA amortization / M-tile ceiling. Single-core int8 is weight-DMA-bound; the M-tile cap is the
0x1040schedule limitmg_max*64(bit-exact), not anR-1/CBUF-resident-rows bound. See AGENTS.md "Weight-DMA amortization" and regcmd ISA Reference. -
Output zero-copy (
ORK_ZC_OUT) is unsafe under concurrent multi-core. The per-core coherencybsyncs aren't serialized across cores. Off by default. See AGENTS.md and Optimization Roadmap Tier 7. -
32-bit IOMMU = ~4 GiB per domain.
rk_iommuv2 "40-bit" is physical PTE reach, not IOVA width. See NPU IOVA 4 GiB cap. -
Some int4-regcmd registers HARD-wedge the bus (kernel job-timeout can't recover; needs a power-cycle). Found while fuzzing the int4 multi-M scheduler (Exp-2026-07-07). Distinct from a soft/compute hang β a bad value mis-programs a DMA/partition so the bus locks and even
sub.timeoutwon't return; only a power cycle recovers. Confirmed hazards:0x1001/0x4010and0x1001/0x4100(PPU/DMA output config β arbitrary overrides slip the DMA), and the0x201/0x11xxCNA scratch region (e.g.0x1104, though that one proved state-dependent β inert in isolation, wedged only cumulatively). These need follow-up RE (why do they bus-lock? are they the IOMMU/output-stride/CBUF-partition controls?). The fuzzer (tools/i4_multim_fuzz.c) blacklists them (i4_fuzz_blacklist.txt) and skips the0x11xxrange by default. See Exp-2026-07-07-INT4-Multi-M-Schedule-RE. -
Two submit-stress hard-wedge circumstances (2026-07-15), for follow-up RE. (1) Concurrent multi-context submits β two ork contexts'
graph_computes on two threads with no serialization β IOMMUlevel 0 translation fault+ soft reset (the single queue can't service two IOMMU contexts at once). (2) int4 large-M submit storm β the int4 1-submit-per-row Γ per-group path at M=512 (~8192 submits) β hard wedge (no ping, power-cycle only); isolated safe at Mβ€16 (β€256 submits). Distinct axes (concurrency vs count), both single-queue+IOMMU fragility. Guards + root-cause questions in Exp-2026-07-15-NPU-Hard-Wedge-Circumstances. -
fp16 M-scheduler has a valid
KpWINDOW[128, 2048), not just an upper bound.sched=1programs the0x1040K-reduction schedule fromscale=Kp/256; for small Kp it extrapolates too HIGH (K=64β0x1040=188, K=32β190) and miscomputes β constant-garbage output (rc=0, no wedge). Kβ₯128 is fine (K=128β184), and the non-pow2 K=96 already tooksched=0(no0x1040override) so it worked, which masked the bug. This blocked fp16 attention at head_dim=32/64 (the common QKα΅/AV dim) β surfaced by the stridedork_bmm_fp16(i8 was exact at the same dims). Fix: gate fp16sched=1toKp>=128 && Kp<2048; outside the window,sched=0(general path) is correct. Validatedtest_bmmK-sweep 32β¦256. (The upper bound is the pre-existing ">8 rows garbage at Kpβ₯2048" gate.) Seesrc/npu.crun()/synth().-
Confirmed + extended 2026-07-12 (Exp-2026-07-12-Matmul-Shape-Boundaries): the single load-bearing register is
0x201:0x1040β a silicon register-bisect (ork_f16_fuzz_add(0x201,0x1040,Β·)) shows overriding only0x10400xBCβ0xB1(template default) at M=64,K=64 flips zero-output (rel-L2 1.0) β bit-exact (3.8e-8). The bug re-appeared inork_bmm_fp16_fused(the on-NPU SSD-scan chain) and the rawork_*_probe_*mmhelpers, which all still hardcodedsched=1β same gate now applied there (sched=(K&(K-1))==0&&K>=128&&K<2048), validated bit-exact across the scan shapes (Kβ{32,64,96,128}, Nβ{64,128}). int8 twin:synth_i8's formula scales by K/512 (vs fp16 K/256), so its invalid band is wider β K<256 (Kβ{32,64,96,128} all zero; Kβ₯256 correct); production int8 is safe because the run path only setssched=1at Kβ{512,1024}, but any raw-synth_i8chain must usesched=0below K=256. Boundary is N- and M-independent.
-
Confirmed + extended 2026-07-12 (Exp-2026-07-12-Matmul-Shape-Boundaries): the single load-bearing register is
-
int4 native multi-M is CBUF-limited batch mode. Reg
0x201/0x107c = K/16enables a resident-activation batch:rows/submit = min(M/2, 16384/K)(activation budget) AND the per-core N-slice weight must fitNcore*K <= 131072int4 (weight budget) β exceed either and far rows/blocks silently don't compute. int8 avoids this by streaming activations (its0x1040schedule, which corrupts int4). int4 streaming is unconfirmed (open RE). See the RE Roadmap "int4 STREAMING mode". -
Concurrent per-core NONBLOCK doorbell submits intermittently DROP (~1/2000β4000; accepted-but-never-dispatched). (2026-07-19) When an int8 matmul rides the multi-core NONBLOCK doorbell β
ork_dyn_begin_mc(S>1 streams: QKV, gate/up) orork_dyn_begin_colsplit(M=1 decode, N-columns split across cores) β a round occasionally is accepted (every per-coreRKNPU_SUBMITreturnsrc=0) yet the HW never executes it: no output sentinel ever lands, no task-completion IRQ fires. Timing-, gap-, ping-pong- and interleave-independent (all measured β not a retirement/EBUSY/mid-round-reset race; those were disproven dead-ends). Rate rises with submit density. Strongly consistent with a lost doorbell / IRQ-rearm race in the stockrknpudriver's concurrent per-core dispatch (kernel-side; unprovable from userspace becauseRKNPU_GET_TOTAL_RW_AMOUNTand taskint_statusread 0-always on this kernel β the output sentinel is the ONLY reliable completion signal; do not trust those counters).-
Deceptive colsplit signature: a dropped core's N-column slice retains the previous iteration's output (fixed test inputs β still correct) except its re-seeded sentinel column, so a whole-slice drop looks like a single wrong interior column. It's the per-core sentinel, not an interior-column compute bug β a validator that checks only
C[0]/C[last]can miss it entirely. -
Why it hid so long: introduced by the blockingβNONBLOCK-doorbell spine migration (prior blocking submits waited in-kernel β no dispatch race to lose). Below single-run detection:
make testruns the one guard (test_stream_interleave) ~420 rounds β passes ~80 % of the time β looked reliable; only surfaced under dozens of back-to-back runs. CI can't run the NPU tests, so no automated repetition accumulated. -
Fix (
ork_dyn_end+mc_recover_resubmit,src/npu.c): detect via a bounded sentinel timeout (300 ms for mc int8 β real decode/stream lands βͺ that), thenRKNPU_ACT_RESET+ ~1 ms settle (a resubmit into a not-yet-quiesced NPU re-drops β sticky/correlated miss) + re-clean + re-seed + resubmit, up to 6 retries. BothORK_MC_ROUNDand colsplit stash their round context in the chain handle soork_dyn_endcan replay it. Fires only on a real drop (β0 common-case cost).test_stream_interleavewent 25/40 β 40/40;tools/mc_miss_repro(stop-on-first-miss stress loop) survives 12000 iters bit-exact with recoveries logged (ORK_MC_DIAG=1).submit1_db(single-corenc==1) is not yet covered β no op was observed dropping on it (M=1 int8 decode routes through colsplit, not submit1_db); add the same pattern if one ever does. -
Lessons: (1) a self-validating test run once per gate cannot catch a sub-0.1 %/round drop β use a stop-on-first-miss stress loop; (2) verify a "counter" actually advances on this kernel before trusting it β
dma_rw=0-always was a trap that produced a false "never dispatched" reading and, when used to gate recovery, a reset storm that hard-wedged the board; (3) moving a path blockingβNONBLOCK trades latency for exposure to this dispatch race, so the recover is mandatory on every doorbell path.
-
Deceptive colsplit signature: a dropped core's N-column slice retains the previous iteration's output (fixed test inputs β still correct) except its re-seeded sentinel column, so a whole-slice drop looks like a single wrong interior column. It's the per-core sentinel, not an interior-column compute bug β a validator that checks only
NEVER free/reuse a DMA buffer a dropped (never-completed) NPU job still references β delayed kernel crash (2026-08-05)
-
Symptom (delayed, deadly): after an accepted-but-never-dispatched doorbell drop (the quirk directly above),
if recovery abandons the job's output buffer β frees it, or de-escalates to a different code path that reuses
a fresh buffer instead of completing the dropped job β the box looks fine for ~1β2 minutes, then the kernel
dies:
BUG: Bad page cache in process kworker β¦ pfn:β¦(a run of contiguous PFNs) βpage dumped because: still mapped when deletedon the shmem eviction path (filemap_unaccount_folio β truncate_inode_folio β shmem_undo_range β shmem_evict_inode β evict β iput β __dentry_kill β dput) βUnable to handle kernel NULL pointer dereferenceβ Oops inrknpu_gem_sync_ioctl(i.e. our ownbsync) β soft-reset cascade β hard wedge (power-cycle only). The delay makes it easy to mis-blame the next thing that runs. -
Root cause: an NPU job that was accepted but never dispatched (nonblock doorbell drop;
hw_elapse=0,int_status=0,iommu=1β the NPU is healthy, it just never ran) still holds references to its output buffer's shmem/DMA-buf pages. Those refs are released only when the job COMPLETES β either it finally dispatches and lands, or the kernel's ~60 s watchdog reaps it.RKNPU_ACT_RESETalone does NOT release them (verified: a reset-then-abandon still crashed at teardown). So destroying that buffer (context free /bdestroy, or the process exiting) while the phantom job still references it = pages "still mapped when deleted" = the crash. -
Rule: never free or hand back a DMA buffer while any in-flight OR dropped NPU job may still reference it.
On a doorbell-drop recovery you MUST get the job accounted first: (a) complete it on the original
buffers β resubmit β poll to landing, as int8
mc_recover_resubmitdoes; the colsplit/mc per-core buffers are context-level and reused, so the completing job reaps the phantom refs β or (b) let the kernel watchdog reap it before freeing. Do NOT de-escalate to a path that leaves the colsplit buffers behind with the drop still outstanding. - Why int8 nonblock is safe and the fp16-colsplit sentinel path was not: int8's recover always lands/completes on the reused buffers β refs released β clean teardown. The fp16 sentinel recovery detected the drop fast (800 ms nonblock poll) but then de-escalated to nc=1, abandoning the colsplit buffers with the dropped slice-1 job never completed β the delayed crash. Fix direction: complete-the-job (resubmit to landing), never abandon.
-
Diagnostic that nailed it:
ork_npu_dump_state/ork_dyn_dumpfired at detection, before recovery, withfflush+ live-streamed over ssh (not captured in a shell$(β¦), which buffers until exit and is lost when the box hangs). Shows the drop is always the 2nd K-slice (Kslice=1),hw_elapse=0. Fire your post-mortem before you touch the NPU, and stream it off-box β a hard wedge loses anything still in a local buffer. SeeCOLSPLIT_MULTIPREC_WIP.md"ROOT CAUSE FOUND via auto-dump (2026-08-05)".
The 2026-08-05 crash surfaced in rknpu_gem_sync_ioctl. The identical root cause also lands in the
destroy path, which is worth knowing because the trace looks unrelated:
Unable to handle kernel paging request at virtual address 0000002710000008
Internal error: Oops [#1] SMP Comm: mode_probe pc: virt_to_folio lr: kfree
virt_to_folio <- sg_kfree <- __sg_free_table <- sg_free_table
<- rknpu_gem_object_destroy <- rknpu_gem_free_object <- drm_gem_object_free
<- drm_gem_handle_delete <- rknpu_gem_destroy_ioctl <- drm_ioctl
Tell-tale: x20 = 000009c400000007 β 0x9c4 = 2500 = the ORK_MM_TIMEOUT that run was started
with, packed against a 7. Scalar data where the sgl pointer belongs: the table was freed and its
memory reused, then walked again at destroy.
Trigger (mode_probe, needs a SEQUENCE β neither pair alone does it): GELU_I8 -> MM_F16 completes
but burns the full timeout and emits ~14 back-to-back RKNPU: soft reset; the next transition's
buffer realloc then issues the GEM destroy into that state.
A post-reset SETTLE DELAY DOES NOT HELP β measured, don't retry it. A settle window inserted
before MEM_DESTROY was tried at 2 ms and at 100 ms: identical Oops both times, same trace, and
the 100 ms run took the board down. That is exactly what the rule above predicts β the refs are
released when the job completes (or the ~60 s watchdog reaps it), not after any amount of wall
time. The mitigation was reverted rather than shipped, because a delay that looks protective and
isn't is worse than nothing.
So the rule stands unchanged and applies to destroy as well as sync: complete the job on the original buffers, or let the watchdog reap it β never just wait, and never abandon.
Note this raises the value of the ork_dyn_progress frontier fix (2026-08-22, dyn_ctl.c): before
it, a hole at a non-final task read as success, so the recover loop never fired and the dropped job
was silently abandoned β precisely the pattern this rule forbids. With recover reachable again, those
jobs now complete on their original buffers and release their refs.
Aftermath if you hit it: Oops [#1] only taints the kernel. The box can survive minutes and then
die on the next activity β once on a purely read-only command β so the death gets mis-blamed on
whatever ran next. Full write-up + kernel-log capture recipe:
Exp-2026-08-22 mode_probe Kernel Oops.
fp16 3-core colsplit drop = a concurrent CROSS-BUFFER weight-fetch WILD β prevent with CONTIG, do NOT try to recover (2026-08-05)
This resolves the fp16-colsplit drop above. The fix is PREVENTION, not recovery β and the two failure classes must not be conflated:
-
The fp16 drop is NOT the benign int8 dispatch-miss. It is a wild DMA: when the 3 cores loop their K-slices, the NPU prefetches the next slice's weight base (
Bb[ks+1]) whileBb[ks]'s fp16 weight-DMA is still draining β two distinct dma-buf fds on the CDMA at once β mis-addressed fetch. fp16's 2-byte weights double the fetch bytes; int8 tolerates the same pattern, fp16 wilds. Signature: the drop is ALWAYS the boundary slice (Kslice=1at Sk=2, neverKslice=0) β textbook cross-boundary prefetch. Same-buffer concurrency (3 cores reading regions of ONE buffer) is benign; only cross-buffer (distinct fds) wilds. A CPU lockstep barrier cannot fix it β it can't gate a hardware prefetch. -
A real fp16 drop is UNRECOVERABLE from userspace (exhaustively tried 2026-08-05: in-place resubmit β sticky re-drop + uninterruptible D-state hang;
close(fd)β refcount underflow;timeout_cleanreap + fast-detect + reset-first β kernel panic). The wild write corrupts kernel RAM at the drop instant, microseconds before any 800 ms / 24 ms detect can react. And any BLOCKING submit that can't dispatch into the post-drop NPU (e.g. an nc=1 fallback) enters the kernel's 60β120β180 scontinue_waitβrknpu_job_abort, which doesrknpu_iommu_domain_put()βmsleep(100)βrknpu_soft_reset()β tears down the IOMMU 100 ms BEFORE halting DMA. The device-globaliommu_domain_refcountgates domain switching (switch only if refcount==0); a dropped job's double-cleanup over-decrements it β a later switchiommu_detach_deviceunder a live matmul β wild write β slab corruption βOops/panic in unrelated processes (percpu_ref_put,rb_insert_color, shmem eviction), delayed 1β2 min.rknpu_job_timeout_cleanis safe only because it resets BEFORE teardown;abortis not. -
THE FIX = CONTIG (
w->Bbc, default-on forSn==1): concatenate all K-slices into ONE weight buffer β there is no dma-buf boundary, so the "next base" is just the next address in the same fd/mapping β the prefetch stays in one mapping β no wild β no drop β recovery never fires. 3 cores read regions of the one buffer concurrently (benign). Validated: 1000-iter @256Γ3584Γ3584 = 0 drops / 0 wrong / 0 panic (the per-slice path dropped on iter 0), full shape suite BIT-EXACT (Sk=1 and Sk=2),make testall-PASS, 2.6Γ preserved (nc3/nc1=0.38Γ). -
The one-line blind spot that hid CONTIG:
is_valid_dma_addr(src/npu.c) validated the weight base againstw->Bb[]/w->Bf[]/scratch but notw->Bbcβ a validBbc.dma+offsetbase was FALSE-flagged "wild/unallocated (outside all valid buffers)" βvalidate_regcmdrefused β CONTIG fell back to the wedging per-slice path. CONTIG's address math was correct the whole time. Lesson: when a "wild address" is inside an expected buffer's range, suspect the validator's known-buffer list before the address arithmetic. -
Taxonomy to keep straight: benign accepted-but-never-dispatched miss (no DMA fired β int8/int4 doorbell drops) is recoverable (
mc_recover_resubmit, reset+reap+resubmit-to-landing, proven 12000 iters); wild-DMA drop (fp16 cross-buffer) is not β it must be prevented. The recovery protocol (adaptive sentinel detect,reap_stuckcleantimeout_cleanreap,ork_dummy_probeint8 health-gate, reset-first) is retained as the safety net for the benign class and any futureSn>1fp16 shape where CONTIG's single-buffer assumption doesn't hold. Gate/knobs:ORK_F16_COLSPLIT(path),ORK_F16_NO_CONTIG(A/B opt-out),ORK_F16_TRACE=<file>(tailable step log),ORK_CAMPAIGN(per-iter bit-exact).
RE'ing rkllm's fast int8 "M-fold" prefill matmul surfaced several traps. The full fold data layout (bit-exact
validated on RK3588 via tools/re/validate_layout.c):
-
A (input)
nc16=(k/16)*(M*16)+m*16+(k%16)β NC1HWC2 C2=16, M folded into WIDTH. -
W (weight)
woff=((n/32)*(K/32)+(k/32))*1024+(n%32)*32+(k%32)β the documented RK3588 int8rknn_matmulNATIVE layout(N/32,K/32,32,32)(fromrknn_matmul_api.h; per-chip(subN,subK)table in [Reverse-Engineering-Roadmap] "Porting the fold to other Rockchip NPUs"). Do not board-RE the weight pack β read the header. -
C (output)
c4=(n/4)*(M*4)+m*4+(n%4)β NC1HWC2 C2=4, int32.
Gotchas:
-
ORK_REPLAY_RESETis a WEDGE TRIGGER, not a stabilizer. It issuesRKNPU_ACT_RESETbefore the submit; on the mfold replay path that reset hard-wedges the board (needs a power-cycle). WITHOUT it the replay runs clean and repeatable (validated bit-exact twice). Do NOT set it for mfold replay. -
The captured output
mm_C.binis STALE β don't trust it as a bit-exact reference. rkllm reuses a tiny pool of ~5 scratch C buffers (e.g.0xb17a000), and therknpu_dump.soshim dumps C after the whole run, so it holds a LATER submit's result, not the captured tile's. The captured (A,W,C) is NOT a consistent triple. Validate with a SELF-CONSISTENT test instead (ork's own A/W β replay β compare CPU ref), not against the dumped C. (The int32 DST output IS raw signedAΒ·W: no requant / zero-point β confirmed by one-hot injection.) -
Injection-ramp aliasing when decoding a layout probe. A recoverable ramp
A[i]=(i%251)-125aliases a value toi β {v+125, v+125+251, β¦}; e.g. val β120 β i β {5, 256, β¦}. Pick the i that is a validnc16index (256 =nc16(0,32)β k=32), not the smallest. A naΓ―ve inverse mis-reads the k it maps to. -
rkllm runs the fold as a
task_number=21weight-resident chain, 3-core round-robin β the per-tile speed win is weight residency across the chain, NOT a single fold tile (a lone width-8 tile does not amortize the weight DMA and loses ~4Γ to ork's normal M-scheduler at M=228: ~17 ms vs 4.6 ms). -
0x40c0(DPU_SURFACE_ADD) is an elem-size CONFIG, NOT an output IOVA β preserve it, do not patch it.0x20int8 /0x80int32 /0x400M-fold (=128*M@M=8). Patching it as an address points the WDMA at an unmapped low IOVA and hard-wedges. (An earlier note in Exp-2026-07-28 called it an IOVA β corrected there.) -
M>8 mfold runs as a chain of M<=8 tasks, and it is bit-exact (2026-07-30).
ork_npu_mfold_chain_capchains P copies of the captured bit-exact width-8 tile (mm_regcmd_m8.txt) in onetask_number=Psubmit, shared resident weight, PC-chained at 216-219: 0-mismatch at M=8/16/32/64, no wedge. Required regcmd prep: preserve0x40c0(above) and neutralize the captured chain descriptor keeping the reg-ids (zero only the value halves of the0x0010/0x0014block-0x101entries, likeork_npu_replay_i8) β zeroing whole words 216-219 drops the0x0014terminator and even one task hangs. Perf caveat: each tile re-streams the full weight (0x1040=0xb1) -> linear, ~7x slower than ork-normal at M=228; the 1.6x needs weight residency (load-once/reuse) across tiles.