Exp 2026 07 08 INT4 BChain Batch Chain - oRKLLM/ork-driver GitHub Wiki
Exp 2026-07-08 β INT4 batchΓchain (ORK_I4_BCHAIN): bit-exact multi-M prefill at production K
Result
A new int4 prefill path β chain the H-row native batches, tiled at the weight-bank width β is
bit-exact and beats the default run_i4_mc:
- Microbench (
i4ORK_I4_BENCH, single-core vs 3-core default, K=2048):- M=128 N=2048: 10.88 β 6.69 ms/matmul (1.63Γ)
- M=128 N=6144: 28.61 β 20.29 ms (1.41Γ)
- M=256 N=2048: 23.86 β 13.41 ms (1.78Γ)
- Matmul, multi-core (N-chunks split across 3 cores): M=128 N=2048 10.89 β 4.15 ms (2.62Γ), M=128 N=6144 28.85 β 12.42 ms (2.32Γ) vs default.
- End-to-end (
ork_bench, Qwen3-1.7B-UD-Q4_K_XL, hadamard W4A4,.orkpack), all PPL 1.8991 (bit-exact): baseline 22.28 β 1-core bchain 27.11 (+21.7%) β 3-core bchain 31.02 tok/s (+39.2%); NPU run portion 3545 β 1929 ms (1.84Γ). Prefill is now becoming quant-bound (quant 36% / run 57%).
Multi-core = split the bank-width N-chunks across cores (i4_bchain_worker, core_mask=1<<i, per-core
scratch β the safe independent-submit pattern).
CBATCH (batch-in-compact) β NOT VIABLE on RK3588 (2026-07-08)
Follow-on attempt: carry the H-row batch in the lean compact/STRATEGY-C incremental task (weight-DMA regs
not re-issued β resident) instead of BCHAIN's full 116-reg batch tasks, to trim per-task regcmd ~7Γ.
Hardware rejects it. A multi-row (mc_phys>1) compact returns errno-110 (chain-walk timeout) for
every register configuration swept (ORK_I4_CB_REGS bitmask over 0x107c/0x1020/0x102c/0x1010/
0x4034/0x4038/0x3014; ORK_I4_CB_AMT chain-amount) β while the same base format with mc=1 (the
proven STRATEGY-C 1-row compact) works. The batch needs the full task's weight-DMA context; a compact
that omits it can't sustain mc>1. This matches the vendor: their int4 batch is task_number=96 of
1-row compacts, never multi-row.
β The duality is asymmetric on this silicon: you can chain batches (BCHAIN, full tasks) but not batch chains (CBATCH). BCHAIN's full-task-per-batch is REQUIRED, not an inefficiency β the per-task regcmd cost is inherent to how the NPU batches int4. BCHAIN is the final form.
Mechanism
Two int4 primitives were each leaving throughput on the table:
- STRATEGY A (in-task multi-M batch,
synth_i4mc>1): computes H =16384/Krows/submit but re-loads the weight every submit βM/Hweight streams. - STRATEGY C (
ORK_I4_INCR, incremental-task chain): weight loaded once, but each chained task is a single row βMtasks of per-task overhead. Measured slower than default at wide-N.
ORK_I4_BCHAIN is the union: chain the H-row batches so the weight loads once and each task does H
rows β M/H batch-tasks, one weight stream per bank.
The bank-width tiling (the crux)
The naΓ―ve "chain full-N batches" fails at K=2048, N>64 β it hits the 0x1040 K-reduction "poison
pill" wall (only row 0 computes). Diagnosis: a native multi-M batch is valid only within one weight
bank. One 64-block int4 weight at K=2048 = 64Γ2048Γ0.5 B = 64 KB = exactly one CBUF bank, and there
is no D_BANK to hold a second (see regcmd-ISA-Reference). So the batch must
not straddle banks.
β Tile N by the bank width Wb = 131072/K columns (64 @ K=2048, 128 @ K=1024, 256 @ K=512) β each
chunk is a wall-free single-bank batch. Chain H-row batches across all (M-group Γ N-chunk) tasks in one
submit. This matches the pass/fail exactly: M=8 K512 N256 (Wb=256, one bank) always worked; K2048 N256 (needs 4 banks) failed until tiled into four 64-wide chunks. After tiling: all shapes maxerr=0.
Implementation
run_i4_bchain in src/npu.c, routed from ork_mm_run_i4, gated ORK_I4_BCHAIN (default OFF,
single-core). RE knobs ORK_I4_BCH_H (batch rows), ORK_I4_BCH_W (chunk width). Reuses the validated
STRATEGY-A synth_i4(mc>1) regcmd + stride-2 A-pack + de-tile verbatim (no new register RE); the only
new structure is the chaining + the bank-width N-tiling.
Post-BCHAIN: "quant-bound" was WEIGHT-LOAD-bound (a cold-start artifact)
With the run halved, prefill looked quant-bound (quant 36% / run 57%). A wload/actq profile split
(ggml-ork.cpp, ORK_VERBOSE) showed the truth: activation FWHT+int4-quant is actqβ0 β the "quant"
time is ~entirely the per-forward-pass weight-load from the .orkpack (wloadβ621ms). It's
independent of M (amortizes with prefill length: P=128 30.8 β P=228 36.1 tok/s) and unaffected by
ORK_WCACHE_BUDGET_MB (budget defaults to ~RAMβ1 GiB; no eviction here).
Root cause = a benchmark artifact: ork_bench's warmup decoded 1 token (M=1), which supports_op
routes to CPU (M=1 < ORK_MINM) β so it never warmed the NPU prefill path, and the timed prefill paid
the one-time cold weight-load. Warming with a prefill-shaped (M>1) batch populates the wcache first:
warm int4 prefill = 38.24 tok/s vs cold 30.79 (+24%). In real serving (persistent process, wcache
never evicts at this budget) prefill runs warm β the load is paid once and amortized. Fix committed
(tools/re/ork_bench warmup β 32-token M>1 batch). β NEON on the activation path is moot; the only
residual prefill cost is the NPU run itself (BCHAIN's domain; 3-core scaling is contention-capped ~1.6Γ).
The 0x1040 K-widening / bank-widening RE β CLOSED as a hardware wall (2026-07-08)
The one remaining lever was to make the NPU run cheaper by fewer/wider submits (the warm prefill floor is ~1.9 s = 74k tasks of 8 rows Γ 1 bank). Two axes to widen a submit; both proved hardware-capped:
- Rows/submit:
16384/K= 8 @ K=2048 (activation cube, exhaustively fuzzed) β and BCHAIN already uses it. - N-cols/submit (banks): probed exhaustively with the repaired
ork_npu_probe_i4_mm+i4_multim_fuzz(ORK_FUZZ_N): base multi-M is 1-bank-only at K=2048 (2/4/8/16 banks β 0 full-width rows); a0x1040sweep at all-banks (N=1024) and thefullsweep of every present register at 2 banks both found no config spanning >1 bank. No reachable register escapes it (consistent with no-D_BANK). Harness note: the probe had regressed to errno-22 (int8regcfg_amounton the sharedc->task); fixed toREGCMD_I4_N/2. Run base withORK_I4_PROBE_TO_MSlow β a 60 s-timeout hang + SIGTERM-mid-submit hard-wedged the board once.
β β RETRACTED 2026-10-12 β the conclusion below is REFUTED. Read this banner first.
"No further register lever exists" is wrong, and this page is the one a newcomer reads first. Measured on both boards (wiki: Exp-2026-10-12 INT4-BCHAIN Dispatch Envelope Β§Β§1β14, and Exp-2026-10-12 W4A4 Tier Trade Β§8):
Wbreaches 320 at K=2048 onceHis paired via0x1040β 3626 β 1707 Β΅s, 2.09Γ, bit-exact, program count 512 β 56. The shippedWb = (131072/K)&~63is 5Γ below the legal ceiling.Hreaches 16β64 withDBNK β₯ 2; the ceiling isDBNKΒ·16384/K, not a fixed 8.- "8 rows Γ 1 bank" is not a hardware lock.
Wb=64(NBc=1) is exempt from any weight-tile limit at every K from 512 to 8192 β 256 KB/program runs fine.- A per-program capacity law proposed on 2026-10-12 was itself refuted the same day: equal byte counts land on both sides, the CBUF bank split is inert, and raising
Hrescues a byte-identical tile. The real rule is a race with a LOWER bound onH(H_race(K,Wb), rising with K), which is why measurements taken atM=H=2β the worst corner of a race β looked like capacity.- The two boards are the same law at two DRAM-clock points, not two dies:
.236(2112 MHz, LPDDR4X) has the uniformly larger envelope and the slower DRAM, and lowering clock grows the envelope monotonically on both parts.What stands: the per-bank tiling mechanism, the +21.7% end-to-end result, and the capacity argument for W4A4. What does not: that the landscape was "fully mapped and closed".
β A single int4 submit is hardware-locked to 8 rows Γ 1 bank; BCHAIN's per-bank tiling is the optimal int4 schedule and the ~1.9 s run floor is genuine hardware. The int4-prefill lever landscape is fully mapped and closed: code (BCHAIN), benchmark honesty (warm), config (ubatch) β ~47 tok/s ceiling; no further register lever exists. (Absent-block D_BANK would need the capture path, already concluded absent.)
Head-to-head: native W4A4 (BCHAIN) vs inflated JIT W4A8 β same model, warm, ork_bench (2026-07-08)
The decisive comparison. Same Qwen3-1.7B-UD-Q4_K_XL.gguf, warm, both via ork_bench; W4A8 = ORK_MIXED_DISPATCH
(int4 weights inflated to int8 at pack, computed on the W8A8 datapath β int4 as storage, not compute):
| W4A4-BCHAIN (SOTA) | Inflated W4A8 | |
|---|---|---|
| prefill ubatch 128 | 37.9 tok/s | 92.8 (2.4Γ) |
| prefill ubatch 512 | 47.6 tok/s | 154.6 (3.25Γ) |
| PPL | 1.899 | 1.420 |
| resident orkpack | 657 MB | 1.4 GB (2.1Γ) |
Inflated W4A8 dominates native W4A4 on BOTH speed (~3Γ) AND quality β W4A4 wins only on RAM (Β½). Cause:
W8A8's mg_max*64 M-tile amortization (100s of rows/submit) vs W4A4's HW-locked 8-rowsΓ1-bank submit (the
wall proven above); and int8 activations are far more faithful than int4-act+Hadamard. β the productive q4
path on RK3588 is inflateβW8A8; native W4A4 compute is a capacity-only fallback (fit >4 GiB-IOVA
models at Β½ weight RAM), never a speed/quality play. (Inflated-W4A8 154 t/s β 53% of rkllm w8a8 ~292 β the
residual gap is the known large-M prefill kernel, not the quant path.)
Perspective
int4 prefill is still 0.093Γ of rkllm's w8a8 (27 vs 292 tok/s) β int4 is not prefill-competitive vs W8A8 on RK3588, and this doesn't change that. But it is the first structural int4-prefill gain that is both faster than the default and bit-exact, and it composes with the multi-core split. It's most valuable in the small-N/MoE/verify regime where int4 batching already pays off.