Exp 2026 08 05 fp16 Colsplit CONTIG - oRKLLM/ork-driver GitHub Wiki

Exp 2026-08-05 โ€” fp16 3-core colsplit: the drop is a cross-buffer fetch WILD; prevent with CONTIG, don't recover

Stack: ork-driver feat/ork-async-stream, src/npu.c + examples/test_f16colsplit.c. Board RK3588 (10.3.0.236), kernel 6.1.115-vendor-rk35xx, DDR 2112 MHz / A76 2.4 GHz, governors=performance, single-stream. Kernel source cross-referenced: rockchip-linux/kernel develop-6.1 drivers/rknpu/{rknpu_job.c, rknpu_iommu.c, rknpu_drv.c}.

Problem

The fp16 doorbell colsplit (ork_dyn_begin_colsplit, dt==DT_F16, N-columns split across 3 cores, K-sliced with host f32 accumulate) intermittently dropped a K-slice โ€” at K=3584 essentially every op. The happy path is 2.6ร— vs single-core and bit-exact, but the drop made it unshippable, so it was gated off (ORK_F16_COLSPLIT, default off). The int8/int4 colsplit recover their drops cleanly (mc_recover_resubmit); fp16 did not, and recovery attempts kept hard-wedging / kernel-panicking the board (โ‰ˆ5 power-cycles this session).

Two failure classes โ€” the key distinction

Benign miss (int8/int4) Wild-DMA drop (fp16 cross-buffer)
What happens Submit accepted, never dispatched โ€” no DMA fires, hw_elapse=0, nothing corrupted Mis-addressed weight fetch โ€” a wild DMA that writes wrong memory
Recoverable? Yes โ€” reset + reap + resubmit-to-landing (mc_recover_resubmit, 12000 iters proven) No โ€” corruption is done ฮผs after the drop, before any detect
Fix Recovery protocol Prevention (remove the cross-buffer boundary)

Conflating these is what cost the most time. The recovery protocol is correct for the benign class; it can't help the wild-DMA class because there's no damage to undo โ€” the wild write already landed.

Root cause of the fp16 drop: cross-boundary prefetch

The 3 cores each loop their K-slices independently. The NPU prefetches the next slice's weight base (Bb[ks+1]) while Bb[ks]'s fp16 weight-DMA is still draining โ†’ two distinct dma-buf fds on the CDMA at the same instant โ†’ the mis-addressed fetch ("cdma wild"). fp16's 2-byte weights double the fetch bytes vs int8, which tolerates the identical pattern.

Decisive evidence: every traced drop was on the boundary slice โ€” Kslice=1 at Sk=2, never Kslice=0 (campaign4/5/7, live-traced via ORK_F16_TRACE). That is the textbook cross-boundary-prefetch signature. Same-buffer concurrency (3 cores reading different column regions of ONE buffer) is benign โ€” only cross-buffer (distinct fds) wilds. A CPU lockstep barrier can't fix it: it gates the software loop, not the hardware prefetch, which latches the next base before the barrier releases.

Why recovery is impossible from userspace (exhaustively ruled out)

Strategy Outcome
In-place resubmit (int8's mc_recover_resubmit model) Sticky re-drop every attempt; by the 3rd reset a re-launched worker hangs in uninterruptible D-state (kernel continue_wait) โ†’ power-cycle
close(fd) reap refcount_t underflow
timeout_clean reap + fast-detect (24 ms) + reset-first Kernel panic โ€” corruption already in memory, manifested 1โ€“2 min later in unrelated procs

The kernel mechanism (source-confirmed): any BLOCKING submit that can't dispatch into a post-drop NPU (e.g. the nc=1 fallback) enters the kernel's 60โ†’120โ†’180 s continue_wait, then rknpu_job_abort runs:

rknpu_iommu_domain_put(rknpu_dev);   // release the device-GLOBAL domain ref
msleep(100);                          // ... 100 ms with the IOMMU torn down but DMA not halted
rknpu_soft_reset(rknpu_dev);          // only NOW halt the engine

The device-global iommu_domain_refcount gates domain switching (rknpu_iommu_domain_get_and_switch: switch/iommu_detach_device+attach only if refcount==0). A dropped job's double-cleanup (timeout_clean's cleanup_work racing the late completion / abort) over-decrements it โ†’ a later submit reads refcount==0 while a job is still live โ†’ detaches the IOMMU under a live matmul โ†’ wild write into kernel RAM โ†’ slab corruption โ†’ Oops/panic in innocent processes (percpu_ref_put, rb_insert_color, shmem eviction). rknpu_job_timeout_clean is safe only because it soft-resets before teardown; abort reverses that order. Fast-detect + reset-first did not prevent it (the wild write precedes detection; the abort race is separate).

The fix โ€” CONTIG (one contiguous weight buffer)

Concatenate all K-slices into one buffer w->Bbc (built lazily, Sn==1 only). There is then no dma-buf boundary โ€” the "next slice base" is just the next address in the same fd/mapping, so the hardware prefetch stays within one IOMMU mapping (benign). No cross-buffer overlap โ†’ no wild โ†’ no drop โ†’ recovery never fires. The 3 cores read regions of the one buffer concurrently (benign). Multicore speed is fully preserved.

The one-line blind spot that hid CONTIG for weeks: is_valid_dma_addr (src/npu.c) validated the weight base against w->Bb[]/w->Bf[]/scratch but had no case for w->Bbc. So a valid Bbc.dma+offset base (e.g. 0xfc67c000, inside the 25.7 MB Bbc) was FALSE-flagged "wild/unallocated (outside all valid buffers)" โ†’ validate_regcmd refused โ†’ ork_dyn_begin_colsplit returned NULL โ†’ fell back to the wedging per-slice path. CONTIG's wbase arithmetic (Bbc.dma + sloff + t0ยทKpยท32) was correct all along. Lesson: when a "wild address" lies inside an expected buffer's range, suspect the validator's known-buffer list before the address math.

Validation

256ร—3584ร—3584 (the drop-prone shape; the per-slice path drops on iter 0):

Path drops (WEDGE-DETECT) wrong panic nc3/nc1
per-slice (K buffers) iter 0 โ€” eventual 0.38ร—
CONTIG, 1000 iters 0 0 0 0.38ร— (2.6ร—), BIT-EXACT
  • Full shape suite (test_f16colsplit, Sk=1 and Sk=2, various K/N): all BIT-EXACT.
  • make test (every mode: matmul/layer/model fp16+int8, i4, stream, SDP, mode-transition): ALL TESTS PASSED.

CONTIG is now default-on for Sn==1 within the colsplit path (ORK_F16_NO_CONTIG opts out for A/B). The fp16 colsplit itself stays behind ORK_F16_COLSPLIT pending the end-to-end A/B-vs-mcworker gate + un-gate.

What's retained

The recovery protocol built along the way is kept as the safety net for the benign miss class and any future Sn>1 fp16 shape where CONTIG's single-buffer assumption doesn't hold: adaptive sentinel detect (1.5ร— the measured slice time, ~24 ms vs a flat 800 ms), reap_stuck (per-core nonblock dummy โ†’ clean timeout_clean reap), ork_dummy_probe (int8 nonblock host-bounded health-gate โ€” can never hang), reset-first, and the short reap-timeout precondition (submit timeout < detect window, so a dropped job is reapable at reap time). Diagnostics that made the whole thing legible: ORK_F16_TRACE=<file> (line-flushed, tailable step log โ€” survives a hard wedge via off-box netconsole) and ORK_CAMPAIGN (per-iter bit-exact vs nc=1, catches transient wrong answers an end-only check misses).

Verdict

Prevention beats recovery for a wild-DMA drop. The corruption is done microseconds after the drop, before any userspace detect can react, and every reap path trips a distinct unfixable vendor-kernel bug (abort domain-put-before-reset, the global-domain-refcount double-put, D-state continue_wait). Removing the cross-buffer boundary (CONTIG) removes the wild at its source โ€” one weight buffer, no boundary, no drop, 2.6ร— preserved, bit-exact. See NPU Quirks โ€” fp16 3-core colsplit drop.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ