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 valuex[0].0xc3f7β β3.98 =x[1]. Every word is the input sweep, unchanged. The submit returnedrc=0(no error, no timeout, no wedge) yet wrote nothing.- Full-buffer scan (definitive):
0 / 6144 u16 words changed from initial stateacross 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.cis now a ground-truth RE diagnostic (reads only NPU output, exits nonzero, dumps raw hex). It is removed fromEXAMPLESand themake testgate β 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, and0x0101:0x0010NEXT-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.