Exp 2026 06 29 Prefill Parity Grind - oRKLLM/ork-driver GitHub Wiki

Prefill Parity Grind β€” lever-by-lever from 31 β†’ 40 t/s, and the diagnostic that says the rest is integration (not the matmul)

Commits: ork-driver main β€” per-N-slice chain prefill (af5bf78) + test_sn3 regression (aceffb5), multi-domain dom_activate Af fix (cb99c1c), chain-prefill/ksplit Sn==1 guard, build/test-suite repair. ggml-ork β€” lever D (attn_output routing), Bβ€² (NEON act-quant, both paths), C (CPUβ€–NPU overlap, gated ORK_OVERLAP) β€” consolidated on a fork branch. RK3588 10.3.0.236, governors max, complete .orkpack resident across 4 IOMMU domains, -t4, warm.

TL;DR

Ground-up grind on the Qwen2.5-7B-Q8_0 prefill, each lever measured and bit-exact-gated, against the closed librkllmrt reference (pp256 73.89 / decode 4.07 tok/s). Net: pp256 ~31 β†’ ~40 t/s (54% of rkllm), decode unchanged (M=1, residence-churn-bound). Two of the levers were the only big ones; three more were measured null/neutral/tiny β€” and the negative results, plus a CPU-operator profile and a per-op rknn_vs_ork diagnostic, are the real payload: they prove ork's NPU matmul is competitive with Rockchip's closed kernel (~1.3Γ— at the 3-core operating point), so the remaining gap is host-side integration + a per-core kernel residual β€” not a matmul wall.

Prerequisite: a trustworthy baseline (and two push-blocker bug fixes)

Cross-agent pp256 had swung 11.5/15.7/26/31/36.5 β€” all config artifacts. Reconciled: it's a residence continuum (IOMMU-domain count Γ— wcache budget), and the "fast" 36.5 was an artifact of a broken chain-prefill path skipping work. Establishing a clean number surfaced two real bugs (both fixed, both validated bit-exact, both were the board-wedge cause):

  • Multi-domain residence SIGSEGV at the first IOMMU domain transition β€” dom_activate re-seeded only regcmd+task for a fresh domain, leaving Af NULL. Fix: allocate Af at first-use too (cb99c1c). The "breakthrough" feature now actually runs.
  • chain-prefill wedge for Sn>1 β€” chaining N-slices across distinct weight buffers emitted a malformed cross-buffer regcmd β†’ RKNPU_SUBMIT errno 110 / cdma address wild / 60 s job timeout (the hang that wedged the board all session). Fix: gate chaining to Sn==1, route wide-N to the proven per-tile path.

Clean baseline (complete 168-tensor .orkpack, fixed multi-domain, ORK_MAXN=20000, zero eviction): pp256 26β†’31 t/s, decomposition NPU-run ~54% + activation-quant 6% + dequant 5% + cold .orkpack load (one-time) + ggml CPU ops.

The levers (measured, ranked by what actually moved)

lever what result
1 β€” per-N-slice chain prefill chain M-tiles within one N-slice buffer (single-buffer-legal) so Sn>1 weights (gate/up N=18944) stop going per-tile ioctls/matmul 55β†’5.17 (10.7Γ—), run_i8 βˆ’23%, pp256 30.94β†’36.11 (+16.7%) β€” the headline. bit-exact, wedge-free (test_sn3 Sn=3 regression added)
D β€” route attn_output to NPU device_supports_op guard strstr(name,"output") over-matched attn_output.weight, forcing all 28 attention-output [3584,3584] matmuls to CPU. Drop the substring; lm_head still excluded by N>max_n pp256 35.79β†’40.2 (+12.6%), bit-exact, no cont-copy needed (src1 was contiguous). One line. Gain ~half the 25% CPU-slice because NPU submit/quant recaptures only part
Bβ€² β€” NEON the chain act-quant lever-2 NEON'd only the single-node path; the dense 7B uses the chain path (still scalar) null (βˆ’0.2%) β€” chain act-quant isn't hot; arithmetic is only ~4% of wall. Landed anyway (bit-identical, lower CPU energy)
A — fuse dequant→requant skip the f32 round-trip on matmul→matmul edges structural no-op — 0 such edges (100% of NPU matmuls are fed by GGML_OP_MUL/RMSNorm; a norm always sits between matmuls)
C β€” CPUβ€–NPU overlap async-submit + overlap dequant of the next chained matmul (ORK_OVERLAP, default-off) ~3% ceiling (only dequant is movable; quant is shared up-front). Gated, low wedge-risk (single-in-flight)

The CPU-operator profile (what the host actually spends)

ORK_PROFILE only timed the ork-backend phases; instrumenting the ggml CPU nodes showed the rest. Per pass (pre-D): NPU matmul 54% + CPU MUL_MAT ~25% (the 28 attn_output, mis-routed β€” that's lever D) + act-quant 6% + dequant 5% + FlashAttn/softmax 4.6% + genuine activations (RMSNorm/RoPE/SwiGLU/add) only ~7%, already NEON + threaded 3.4–3.6Γ— across 4 cores. So faster activation kernels are not the lever (no headroom, and ork's neon_activations.c is dead code here β€” ggml's CPU kernels run them). The ~25% "CPU ops" hotspot was a matmul on the wrong backend β€” fixed by D.

The diagnostic β€” rknn_vs_ork: is our matmul the problem? (No.)

Same int8 (M,K,N) through ork vs the closed RKNN matmul API (librknnrt), M=256, median-of-5:

shape ork-3core RKNN-1core ork-3c / RKNN
q/o/attn_out 3584Γ—3584 9459 Β΅s 7299 1.30Γ—
gate/up 3584Γ—18944 47509 Β΅s 37436 1.27Γ—
down 18944Γ—3584 24991 Β΅s 1,552,220 0.02Γ— (RKNN pathological large-K β€” excluded)

Stable 1.27–1.31Γ— across M=64/128/256/512. AC-layout probe: RKNN's "native" layout is 1.28Γ— slower than normal β†’ ork isn't disadvantaged by layout; the comparison is fair.

Verdict: ork's matmul at the real (3-core) operating point is within ~30% of Rockchip's closed kernel β€” same efficiency class, no hard frontier. At 1.3Γ— and ~65% of the wall, the matmul residual cannot account for the 40β†’74 full-model gap β†’ the bulk is host-side integration/scheduling/engine (the closed rkllm's advantage is how it orchestrates the matmuls, not a faster matmul). The nuance: ork-1core is 3.5Γ— slower than RKNN-1core β€” ork uses all 3 cores to reach 1.3Γ—, so the per-core kernel has real (hard) headroom (matching a hand-tuned vendor kernel via regcmd RE). Because matmul dominates the wall and the same kernel runs at decode/M=1, closing the per-core residual is a both-regimes win β€” the next target.

Verdict & next

βœ… pp256 31β†’40 (54% of rkllm), bit-exact, two push-blocker wedges fixed, build/test-suite repaired. The grind's real value is the map: levers 1 + D were the only big prefill wins; activation kernels / dequant-fusion / chain-quant are spent; the matmul is competitive, so the remaining gap is per-core-kernel efficiency (deep regcmd RE) + host integration. ⏭️ NEXT: go after the per-core matmul kernel via regcmd RE (close the 3.5Γ—/core gap toward RKNN) β€” it lifts prefill and decode and the future M>1 verify path. Methodological note recorded for the community: every "fast" number this session was a config artifact until pinned β€” measure the exact config (ioctls/call fingerprint) before trusting a t/s.