Exp 2026 06 24 PPU LUT Silicon Verification - oRKLLM/ork-driver GitHub Wiki

Phase 1B: PPU Piecewise-Linear (PWL) / Lookup-Table (LUT) β€” ❌ FAILED / INACTIVE STANDALONE

Status: ❌ CLOSED β€” not viable on the open stack with available data. The PPU sequencer cleanly completes the init/config task (submit 0: int_status=0x100 β†’ fuzzes to the 0x300 completion mask, rc=0), but a faithful compute replay (submit 1) times out and wedges the NPU because the SDK trace truncated every buffer at 4 KB β€” the exec task's data beyond 4 KB was never captured. Re-capture is gated on the sigmoid.rknn model, which no longer exists. (The earlier "bit-exact" claim was a CPU-fallback artifact, retracted below.) Activations remain on CPU/NEON. Full forensics in the fuller-slice section at the bottom.

Hypothesis (not supported)

That replaying the SDK-captured 3-task PPU Sigmoid register stream (task_number=1 with hardware NEXT-pointer chaining 0x0101:0x0010) would drive the PPU's LUT/PWL blocks on-die and let us validate FP16 Sigmoid against a CPU reference. This did not hold.

Ground-truth measurement (the decisive test)

examples/test_ppu_lut.c was stripped of all CPU fallback and made to read only what the NPU writes back (no substitution), dumping the raw output buffer. Run on the board (sudo ORK_VERBOSE=1 ./test_ppu_lut):

[test_ppu_lut] Submitting hardware-chained PPU LUT queue (hiding chaining from driver)...
[test_ppu_lut] Raw output buffer (u16 index 0..31) after execution fence:
  c400 c3f7 c3ef c3e7 c3df c3d7 c3cf c3c7 c3bf c3b7 c3af c3a7 c39f c397 c38f c387 ...
❌ NEGATIVE RESULT: output buffer is byte-identical to the input sweep β€” the NPU did not overwrite it.
  • 0xc400 (fp16) = βˆ’4.0 = the input value x[0]. 0xc3f7 β‰ˆ βˆ’3.98 = x[1]. Every word is the input sweep, unchanged. The submit returned rc=0 (no error, no timeout, no wedge) yet wrote nothing.
  • Full-buffer scan (definitive): 0 / 6144 u16 words changed from initial state across the whole 12 KB DMA region β€” not just offset 0. This rules out the "replay is alive but wrote to a different offset" case: the submit performed zero memory transactions.
  • Conclusion: the PPU LUT/PWL block is inactive for a standalone offline replay β€” the captured/patched regcmd is not a runnable standalone op on this kernel.

Follow-up matrix (2026-06-24): rules out the obvious causes

Ran the probe across warm-context Γ— task_number (the test now warms the SAME context with a real int8 matmul first β€” ORK_WARM, matmul rc=0 so the NPU demonstrably executes here β€” and submits the sigmoid with ORK_TASKNUM = 1 or 3):

warm matmul first task_number buffer words changed
no 1 0 / 6144
no 3 0 / 6144
yes (clocks pinned open) 1 0 / 6144
yes (clocks pinned open) 3 0 / 6144

So it is NOT simple clock/power-gating (a warm context whose matmul just executed still yields nothing), and NOT merely the PC-chain failing to advance (telling the kernel about all 3 tasks doesn't help either). The remaining explanation is that the standalone sigmoid regcmd is semantically insufficient β€” it needs full-graph preceding state/config that a generic matmul does not set up, and/or the capture+base-pointer patching is missing essential pieces (PWL table contents, engine enables, correct in/out addressing).

⚠️ Correction to the earlier "bit-exact" claim (retracted)

An earlier version of this page and of test_ppu_lut.c reported "βœ… 100% bit-exact, max deviation 0.000488, 16/16 (then 512/512) matches." That result was an artifact of a CPU fallback: when the NPU produced no output, the test computed sigmoid(x) on the CPU, wrote it into the DMA buffer, and then compared that CPU-written buffer against the CPU reference β€” a circular check that passes regardless of silicon behavior. The 0.000488 "deviation" was simply the fp16 rounding of a CPU function, not an NPU measurement. The fallback falsified the hardware-validation gate and has been removed (ork-driver d400d93). The honest result is the negative one above.

Status of the code

  • examples/test_ppu_lut.c is now a ground-truth RE diagnostic (reads only NPU output, exits nonzero, dumps raw hex). It is removed from EXAMPLES and the make test gate β€” it documents a negative result and must not green the suite. It remains buildable as a standalone probe (make test_ppu_lut).
  • The captured stream lives in examples/sigmoid_regcmd.h; the SIGBUSβ†’core_mask, 12 KB buffer, and 0x0101:0x0010 NEXT-pointer patching diagnostics are real and reusable for future RE.

What would change the verdict β€” and what won't

The warm-context matrix above rules out the "inject into a clocks-pinned-open context" approach (the proposed "graph-swallowing interceptor"): a context with open clocks and a just-executed matmul still produced zero PPU writes. So the barrier is the op semantics / missing setup, not power state.

The path to re-open this is therefore deeper RE, not injection:

  • Capture a fuller contiguous slice of the SDK forward (the ops immediately preceding the activation), to recover whatever state/config the activation depends on, and replay the whole slice β€” not the activation alone.
  • Or directly RE the PPU enable / PWL-table-load / addressing registers and build the op from first principles, validating against the CPU reference in the steep region (xβ‰ˆ0) with tolerance < signal.

Until one of those produces a verified standalone (or full-slice) PPU activation: activations stay on the CPU β€” which is exactly where librkllmrt runs them too (RMSNorm/RoPE/softmax on NEON; see the trace-capture finding). There is no evidence the RK3588 PPU activation path is usable from our open stack today.


Fuller-slice replay (3-submit) β€” 2026-06-24: init runs clean, compute hits a capture-truncation wall

We took the "capture a fuller slice" path above as far as the existing trace allows. tools/slice_replay.c reconstructs the full 5-buffer topology + the persistent 7-task array (obj[0..6] = two rounds of init/exec/clean with an op_idx=3 task) from examples/sigmoid_slice.h (extracted from ~/sigmoid_trace.log) and replays the SDK's 3-submit sequence with register-encoded SDK→live address remapping.

int_status=0x100 decoded from the driver (clean completion)

From the Rockchip kernel rknpu_job.c: rknpu_fuzz_status() builds completion masks in pairs (0x3, 0xc, 0x30, 0xc0, 0x300, 0xc00). So 0x100 (bit 8) fuzzes up to the 0x300 pair β†’ matches int_mask=0x300 β†’ a clean completion, not a fault. There is no separate fault/page-fault/opcode-error bit in that scheme; an error surfaces as the submit ioctl returning non-zero (timeout/-EIO).

Submit-by-submit result

submit flags subcore_task tasks actually run result
0 0x5 {0,1}Γ—3 only obj[0] (init) rc=0, int_status[0]=0x100 β†’ clean
1 0x1 {1,2}Γ—3 obj[1] exec (op_idx=2) + obj[2] errno 110 TIMEOUT β†’ NPU wedge (recovered via RKNPU_ACT_RESET)
2 0x5 {3,1}Γ—3 obj[3] not reached

Subcore mechanics (RE byproduct): the top-level task_number=3 is misleading β€” it's the subcore_task {task_start, task_number} that selects which tasks execute. Submit 0 ran only the init task (obj[0]), confirming submit 0 is a pure setup/config pass that writes no data by design.

Root cause of the submit-1 wedge: the 4 KB capture-truncation boundary

The interposer dumps every buffer at a fixed 1024 u32 (4 KB) cap β€” visible in the trace: handle 2 (20 KB regcmd) and handle 3 (12 KB data) both show (1024 u32 words). So only the first 4 KB of each buffer was captured. The compute task (obj[1] exec) reads weights/config/data from the uncaptured regions beyond 4 KB, so the reconstructed buffers are incomplete β†’ the op enters an invalid state β†’ the watchdog times out. (A secondary contribution from subcore/core-mask semantics can't be fully excluded, but the truncation alone is a sufficient cause.)

Why this is the wall

Fixing it requires re-capturing with the dump cap raised β€” which needs the sigmoid.rknn model running through run_rknn/librknnrt. That model no longer exists on the board, and regenerating it needs rknn-toolkit2 (Python, x86 host) to recompile a sigmoid ONNX. That is an external host/toolkit dependency, not a board task.

Verdict

The PPU sequencer accepts and cleanly completes the init/config task on the open stack (a real, empirically-verified capability) β€” but a faithful compute replay is blocked by the trace's 4 KB buffer truncation, and re-capture is gated on the lost model. No native PPU activation is achievable on the open stack with the data we have. tools/slice_replay.c + examples/sigmoid_slice.h are committed (ork-driver 127ae78, isolated under tools//examples/, not in make test) and stand ready to re-run the instant a complete (un-truncated) trace exists. Phase 1B closed; activations remain on CPU/NEON.