Exp 2026 10 13 INT4 BCHAIN Completion Gate - oRKLLM/ork-driver GitHub Wiki

Exp-2026-10-13 β€” the INT4 BCHAIN completion gate

Board 10.3.0.236 Β· kernel 6.1.115-vendor-rk35xx-fencefix Β· DMC + A76 performance, verified before and after Β· zero faults, zero wedges. Shipped as 911203b (rebased aeb50f7), make test ALL PASS.

The motivating bound came from an ORK_I4_BLOCKING A/B in Exp-2026-10-12 INT4-BCHAIN Dispatch Envelope Β§21: removing the seed and drain-verify made the call ~40% faster while miscomputing. This page measures where that time actually is, attacks it, and records what is irreducible.

1. The phase breakdown β€” measured, not apportioned

ORK_BCH_PROF=1, cell M=160 H=32, K=512 N=2048 Wb=320, 6 iters Γ— both orders, baseline β‰ˆ1470 Β΅s:

phase Β΅s/call note
seed (mode 0, 3 cores parallel) ~50
seed flush (kernel TO_DEVICE, whole 4Γ—-padded slot) ~37
submit ioctl ~11
mode-1 gate spin ~430 ~1000 passes at 0.4 Β΅s β€” it early-exits at the first sentinel, so this is waiting, not scanning
gate sync (kernel FROM_DEVICE, whole 4Γ— slot, 6Γ—/call) ~145
mode-3 full verify (serial, ~2Γ— per core) ~365 the first attempt fails on essentially every call
end sync ~70 a duplicate of the gate sync
de-tile (parallel pool) ~128 re-reads the cells the verify just read

Ablation, medians over both orders: ship 1470 β†’ verify removed 1082 (βˆ’26%) β†’ no gate at all (ORK_I4_BLOCKING, miscomputes) 1027 (βˆ’30%).

β˜… Seed + flush + spin together are worth ~4%. The drain-verify is ~26% β€” essentially all of it. The per-poll rΒ·NBcΒ·64 scan is cheap because it early-exits; the expense is the post-completion full-surface verify, serialised across cores behind a kernel sync covering 4Γ— the written cells, then a second pass that re-reads the same cells to de-tile.

⚠ Correction to Β§21's framing. Its 1400 Β΅s for M=128 H=32 is the pre-tail-heuristic geometry; c2eb677 already re-tiles that cell to H=31, so the shipped baseline is ~1150–1250 and the attainable-if-incorrect bound is βˆ’27% to βˆ’33%, not βˆ’40%.

2. β˜…β˜… A kernel fence IS available β€” and it is SLOWER. Do not build on it.

  • CONFIG_ROCKCHIP_RKNPU_FENCE=y in the running kernel.
  • RKNPU_JOB_FENCE_OUT = 1<<4 = 0x10 (drivers/rknpu/include/rknpu_ioctl.h:103). rknpu_submit_ioctl calls rknpu_fence_alloc + rknpu_fence_get_fd and writes the fd back through the DRM_IOWR copy-out.
  • Worked first try, no reflash: fence_fd=4, poll() rc=1 revents=POLLIN, waited 605–632 Β΅s.
  • β˜… This is what "fencefix" in uname -r means: the running kernel carries a fix in rknpu_job_free that signals the fence with -ETIMEDOUT on the rknpu_job_abort / timeout_clean paths, so a waiter can distinguish a stall from a completion via dma_fence_get_status().
  • But it measured 1613/1536 Β΅s against ship 1430/1405 β€” 13% SLOWER.

Why, and this is the part to remember: the fence signals at job_done, and int4's DPU writeback LAGS job_done. So the entire drain-verify still runs afterwards, cold, with nothing overlapped, while the poll() wake-up costs more than the 430 Β΅s spin it replaced.

β˜… The fence is a correct completion signal, not a data-visible signal. It cannot remove the poll. Recorded so nobody re-derives it.

3. What shipped

Three changes, none of which touch what the gate checks β€” only how it executes:

  1. The mode-3 verify is fanned out over the existing pool (cores own disjoint scratch and disjoint column ranges of C). One fan-out per round, not one per core.
  2. Modes 3/6 invalidate exactly the lines a program wrote (dc civac Γ—2 per 64-cell block, one dsb ish, then read) instead of a kernel FROM_DEVICE sync over the whole slot β€” 4Γ— fewer lines and no ioctl. β˜… Fail-safe by construction: a line that is somehow still stale reads back the seed, i.e. not-done. It can delay completion; it cannot declare it early.
  3. A new mode 6 fuses verify + de-tile into one pass, making the separate de-tile and the duplicate whole-slot end-sync dead on the success path.

ORK_BCH_SLOWGATE=1 restores the old path exactly, for A/B and bisect.

Rejected after measurement: a targeted dc cvac for the seed (SELFCLEAN) was a loss β€” the kernel's range sync is cheaper because most of the 4Γ— slot is clean. A verified-prefix resume was neutral, because a failing verify already early-exits.

cell (K=512 N=2048 Wb=320) SLOWGATE (= old) shipped gain
M=128 H=32 1146 Β΅s 975 1.18Γ—
M=129 H=32 1210 973 1.24Γ—
M=160 H=32 1417 1231 1.15Γ—
M=160 H=31 1465 1225 1.20Γ—

Correctness, graded at a CALIBRATED sensitivity

A gate that returns early is silent, data-dependent corruption, so "it passed" means nothing without knowing the detector's power. All of this is i4_e2_probe against an absolute CPU int4Γ—int4 reference, full-random (dm=1), both orders, multiple seeds.

  • Negative control β€” no gate at all (ORK_I4_BLOCKING): caught in 5 of 12 runs / 5 of 240 graded iterations (and 10/12 runs in a second 240-iteration sweep). So the detector catches a missing gate at ~2–4% per iteration.
  • Second negative control (ORK_BCH_NOVERIFY, mode-1 gate only): 240/240 bit-exact. ⚠ Honest limitation: this harness cannot demonstrate the drain-verify is load-bearing on these shapes. It is kept because the shipped predicate is unchanged, not because this test proves it is needed.
  • Shipped build: 328 graded iterations bit-exact, zero failures, plus make test ALL PASS on the exact committed tree.

4. How much of the bound is reachable β€” about half

Honest bound βˆ’27% to βˆ’33%; shipped takes βˆ’13% to βˆ’19%, i.e. ~50–55% of it. What remains, and why it is irreducible while the gate stays sound:

  • ~160 Β΅s β€” the drain-verify itself. One full parallel pass over every written cell, plus ~3 early-exiting retries while the DPU writeback catches up. Mode-1 passing does not imply earlier programs are visible β€” measured, not assumed: the first verify fails on essentially every call.
  • ~90 Β΅s β€” the seed and its flush. Both already efficient; the targeted-clean alternative was worse.
  • The spin is real hardware wait, and Β§2 shows a fence does not shorten it.

β˜… The remaining ~13% is the cost of not trusting job_done. The only lever that would delete it rather than shrink it is an out-of-band completion signal β€” a cell the datapath provably cannot produce, so one witness per program suffices. That is now the only item with meaningful headroom left on this path, and it also removes the in-band 0x7fff sentinel collision (ORK_DYN_SENT16 is a legitimate saturated output).

5. The hcapβˆ’1 tail gate at Wb=128 β€” both boards agree, lower it

c2eb677 gated the tail-group heuristic on Wb >= 256. Production geometry, K=1024 β†’ Wb=128, N=2048, 6 iters both orders, pre-change gate so it is directly comparable with .235:

M H chosen (ON vs OFF) tail-ON tail-OFF ON vs OFF
64 15 vs 16 917 Β΅s 937 βˆ’2.1%
96 15 vs 16 1278 1339 βˆ’4.5%
128 16 vs 16 (A/A) 1698 1678 β€”
160 16 vs 16 (A/A) 2097 2206 β€”
192 16 vs 16 (A/A) 2589 2519 +2.8% (noise)
256 16 vs 16 (A/A) 3384 3409 βˆ’0.7% (noise)

At Wb=128 the heuristic only fires at M=64 and M=96; the other rows are A/A controls and pin the order-averaged noise band at roughly Β±3%. Where it fires it is βˆ’2.1% and βˆ’4.5%, the same sign as .235's 2.1–5.3%, and never a loss. The earlier ".236 null-not-loss" reading is consistent β€” it was taken at M where the heuristic does not fire.

β˜… Both boards agree in sign, so the gate should drop to Wb >= 128. Not yet changed: it should land as one coordinated edit carrying both boards' measurements.

β˜… And a bigger adjacent finding: the cost model's gam = Wmax + Wmax/4 is 2 at Wmax=2 and under-weights the measured tail there β€” it declines H=15 at M=128 by a margin of 2 while the M=96 gain is 4.5%. Recalibrating gam at narrow Wb is probably worth more than the gate change itself.


6. Out-of-band completion signal: not constructible β€” task order does not imply write-visibility order

2026-10-13, board .236, uname -r = 6.1.115-vendor-rk35xx-fencefix, DMC performance @2112 MHz, A76 performance.

Β§4 left one lever: a witness the datapath provably cannot produce, so one cell per program replaces the full-surface scan. The best candidate was a trailing marker task appended to the hardware chain β€” the chain already walks tasks in order via 0101:0x0010/0101:0x0014, so a tiny final task writing a known marker to a dedicated cell would be genuinely out-of-band. It is sound iff task execution order implies write-visibility order through the DPU writeback.

β˜… It does not. Measured, reproducibly, at production geometry. The marker line is closed.

6.1 The instrument, and its calibration

ORK_BCH_ORDER=2 (clone-only, ~/work/ord, patched on eb6edb6): the instant the shipped mode-1 gate reports the LAST program of a core fully written, walk that core's earlier programs in reverse (NT-2 … 0) on the same thread, with the shipped discipline β€” targeted dc civac over exactly the written lines, one dsb ish, then read.

Reading program t strictly later in wall time than program NT-1 and still finding seed cells cannot be explained by "the later program simply had more time". It is an unambiguous ordering violation.

Negative control ORK_BCH_ORDER=3: identical machinery, but gate on program 0 instead of the last. 17 of 45 gate passes flagged, up to 800 808 seed cells still outstanding. The detector has power.

Positive control: the same instrument on the brief's shape (K=512 N=2048 M=160 H=32 Wb=320) is clean β€” 36/36 gates, 0 violations. The detector is not simply always-on.

6.2 The answer: NO

sweep gates observed violations rate
11 geometries Γ— 2 seeds Γ— 12 it (forced H/Wb) 1 044 21 2.0%
8 geometries Γ— 3 seeds Γ— 4 reps Γ— 30 it (forced H/Wb) 45 948 1 109 2.4%
13 shapes Γ— 2 seeds Γ— 2 reps Γ— 20 it, PRODUCTION default geometry 34 480 1 998 5.8%

Per geometry, from the stress sweep:

geometry gates violations rate
K=1024 N=2048 M=129 H=16 Wb=128 3 719 394 10.6%
K=1024 N=4096 M=129 H=16 Wb=128 15 305 582 3.8%
K=1024 N=2048 M=145 H=16 Wb=128 21 465 133 0.6%
K=1024 N=2048 M=130 H=16 Wb=128 1 385 0 0
K=2048 N=2048 M=65 H=8 Wb=64 1 418 0 0
K=512 N=2048 M=129 H=32 Wb=320 1 387 0 0
K=512 N=2048 M=161 H=32 Wb=64 1 269 0 0

Anatomy of a violation (hundreds of instances, all identical in shape):

[ORDER2] VIOLATION core=0 NT=90 round=588 recov=0 | incomplete progs=3 lo=44 hi=46 sentcells=2072
  • 2–3 consecutive program indices hold ~2072 cells still at the seed β€” a contiguous β‰ˆ4 KiB window of the output scratch, not scattered singletons.
  • The gap is large: program 46 unwritten while program 89 β€” 43 tasks later β€” is fully written.
  • recov=0: no host recovery was in flight. Not a resubmit artifact.
  • It is stable, repeating identically across 20–40 consecutive poll rounds (and in the worst runs for over a second). Not a read race, not a stale cache line: dc civac + dsb ish precede every read, and the final result of every one of these runs was bit-exact once the surface drained.
  • Always core=0 β€” the first core submitted, hence the first whose gate passes, hence the one with the most outstanding writeback behind it when it is checked.

6.3 The drain-verify is LOAD-BEARING β€” Β§3's open question, closed

Β§3 recorded honestly that ORK_BCH_NOVERIFY ran 240/240 bit-exact and therefore could not demonstrate the drain-verify was needed. It could not because it ran on K=512 N=2048 M=160, a shape where ordering happens to hold (6.1, positive control).

ORK_BCH_NOVERIFY is exactly what a trailing marker buys: accept on the last program, then a kernel FROM_DEVICE sync (architecturally complete, so a failure cannot be a stale-cache artifact) and a plain de-tile. On the geometries where ordering fails:

arm geometry runs graded iterations iterations wrong worst single output
SHIP forced H/Wb 48 1 440 0 β€”
NOVER forced H/Wb 48 1 440 336 (23%) 163 006 of 528 384 (31%)
SHIP PRODUCTION default 36 2 160 0 β€”
NOVER PRODUCTION default 36 2 160 502 (23%) 163 012 of 528 384 (31%)

Both arm orders, 3 seeds, full-random dm=1, absolute CPU int4Γ—int4 reference, 30 and 60 iterations per run. Totals: SHIP 0 of 3 600 graded iterations wrong; NOVER 838 of 3 600 (23%).

β˜… It is not a forced-geometry curiosity. The production rows ran with no ORK_I4_H/ORK_I4_WB override and the ladder on β€” K=1024 N=2048 M=129 and K=1024 N=4096 M=129, for which the ladder picks H=16 Wb=320.

6.4 Why no out-of-band witness is constructible

Every cheap-witness design reduces to one of two hardware properties, and both are refuted:

  1. Cross-task ordering β€” "the last task's write lands last". Refuted above. A trailing marker task is therefore a completion signal, not a data-visible signal β€” i.e. exactly what Β§2 already measured RKNPU_JOB_FENCE_OUT to be, except that the marker also costs a chained task (~9–13 Β΅s at the end of a chain, where nothing hides it). It is strictly worse than the fence.
  2. Per-program or per-block atomicity β€” "a program (or a 64-cell block) is written all-or-nothing, so one cell witnesses it". Refuted by the same data: a violation spans 3 program indices holding 2072 of their 6144 cells β€” two of the three programs are partially written. Programs tear.

A witness in a separate surface inherits property 1 and dies with it. Widening the sentinel to a 32-bit or whole-block pattern removes the 0x7fff in-band collision but still needs property 2 to be sound, so it buys unambiguity, not a smaller scan.

β˜… So: no sound out-of-band witness exists on this hardware, because the DPU exposes no host-observable ordering between a chained task's output writes and any later task's. The only sound witness is the written cell set itself β€” which is what ships.

6.5 What that means for the remaining ~13%

The gap SHIP βˆ’ NOVER is +12% to +14% of the call, consistently, in both arm orders (2410β†’2754 Β΅s, 1264β†’1466 Β΅s, 1070β†’1220 Β΅s). Β§4 called it "the cost of not trusting job_done". It is more specific than that: it is the DPU writeback tail that job_done does not cover, plus the pass that proves the tail has drained. A correct gate has to wait for data that is genuinely not in DRAM yet. No witness scheme shortens that wait β€” only finishing the writeback sooner, or overlapping the wait with useful host work, could.

β˜… The int4 BCHAIN completion-gate line is closed. What is left is not a gate problem.

6.6 The trigger is not characterised

Every firing geometry so far has K=1024 (at Wb=128 and at the production Wb=320, at N=2048 and N=4096). Nothing fired at K=512, K=2048 or K=3584 in ~12 000 gate observations there.

An appealing pattern from the first stress sweep β€” M mod H == 1, a one-row final M-group (M=129,H=16 10.6%; M=130,H=16 0%) β€” did not survive its own test. A dedicated H-sweep at fixed K=1024 N=2048 M=129 Wb=320, 5 values of H Γ— 2 seeds Γ— 2 reps Γ— both orders Γ— 25 iterations:

H final-group rows gates violations
16 1 747 0
15 9 413 0
14 3 411 0
13 12 387 19
11 8 447 0

The one arm that fired has a 12-row tail, and the tail==1 arm did not fire at all. So the tail-size association was an artifact of the earlier sweep's low power on the non-firing arms. β˜… Do not act on it β€” in particular, do not add an M mod H != 1 rule to the geometry ladder on the strength of this entry.

6.7 Reproducing

# instrumented clone (ORDER=2 / ORDER=3 are clone-only; not shipped)
git archive --format=tar origin/main | ssh board 'mkdir -p ~/work/ord && tar x -C ~/work/ord'
# then apply tools/re patches from this entry's scratch and: make -j4 i4_e2_probe

# the question
sudo tools/util/npu_guard.sh -- timeout -s TERM 300 \
  env ORK_I4_NOLADDER=1 ORK_I4_H=16 ORK_I4_WB=128 ORK_STORM_SEED=777 ORK_BCH_ORDER=2 \
  ./i4_e2_probe 1024 4096 129 30 1
# the negative control
...  ORK_BCH_ORDER=3 ...
# the consequence (needs the ~/work/gate clone's knobs)
...  ORK_BCH_SELFINV=1 ORK_BCH_PARVER=1 ORK_BCH_PARALL=1 ORK_BCH_FUSE=1 ORK_BCH_NOVERIFY=1 ...

Drivers: board:~/work/g_order_v1.sh, g_order_stress_v1.sh, g_prodorder_v1.sh, g_loadbear_v2.sh, g_prodload_v1.sh.

6.8 What this did not show

  • It did not establish the mechanism. The window is β‰ˆ4 KiB and contiguous, which is suggestive of a page-granular stall (the mcc scratch is non-contiguous, flag 0x403), but nothing here distinguishes an IOMMU/TLB stall from a DPU write-queue stall from a kernel self-heal.
  • It did not isolate the trigger (6.6). An ORK_I4_WREUSE=0 arm was attempted and abandoned: at K=1024 N=4096 M=129 H=16 Wb=128 NC=32 it produced 17-second iterations and WRONG results, i.e. weight-reuse-off is itself broken at that geometry β€” a separate finding, not pursued, and the arm was stopped as wedge-prone. (It did eventually wedge NPU cores 1–2 at 100% with no owner; cleared by sudo reboot, governors verified afterwards.)
  • β˜… A kernel counter matches: /sys/kernel/debug/rknpu/counters showed blocked_slow = 1071, blocked_age = 1 319 022 us β€” "dispatch declined by an owner stuck over 1 s" β€” and 1 319 022 us is the same number as the max=1 320 201 us outlier of the run that logged 1 998 violations. So the violating window is a job that genuinely sits in the NPU for >1 s with a ~4 KiB slice of its output undelivered. That points at the kernel/IOMMU side, not at the gate; it is not proof.
  • The violation rate is per gate observation, and gate observations are not uniformly spaced in time (a stalled round spins the gate thousands of times). Read the per-run mism= counts, not the gate rate, for the probability that a call is affected.
  • Β§4's stated evidence β€” "the first verify fails on essentially every call" β€” does not reproduce. On the brief's shape the instrumented counters read roundsβ‰ˆ400–500, gatepass=1, fanout=1, vpass=3, vfail=0 on every call: the mode-1 gate early-exits ~450 times (that is the hardware wait) and then the full verify passes first time. The conclusion of Β§4 stands; the evidence cited for it was wrong, and the real evidence is the geometry-specific data above.

7. β˜…β˜… The mode-4 premise is BOARD-DEPENDENT, and a one-board census was read as a verdict

Added 2026-10-13, after this page's own evidence was used to justify shipping a gate that broke the other board. This is a correction to how Β§3's census was interpreted, not to its numbers.

cb0bf06 made the seed-collision verdict reach the frontier instead of stopping at edone. That was necessary β€” without it every W4A4 model decode died at progress=-1 β€” but it promoted bch_db_cells mode 4, whose entire test was "a landed program has β‰₯1 non-sentinel cell."

A program caught mid-write always has one. Mode 4's own comment carried the premise that makes it sound β€” "use ONLY at the poll timeout (by then a run program is fully written)" β€” and the ROCK 5B+ falsifies that premise constantly: its narrower int4 dispatch envelope means the drain times out mid-flight at geometries the ROCK 5B completes. So programs are torn, mode 4 says "ran", ork_dyn_end de-tiles a half-written surface, and the 0x7fff seed lands in C as data.

Measured on 10.3.0.235: i4 maxerr β‰ˆ 32767 at M=8 K=512 N=256, M=16 K=2048 N={64,256}, M=32 K=1024 N=128, and the sat case; test_slice_rescue missed three fnv64 goldens. Deterministic, bisected to cb0bf06 exactly. It shipped for six commits.

The interpretation error, stated plainly

This page recorded a .236 census of 1 seed cell over 2048 programs and that was read as "the adjudicator is fine." It showed no such thing. It showed only that that board never presents the ambiguity. A census is evidence about the board it ran on; the premise it was used to support β€” that a landed program is fully written β€” is a claim about all boards.

β˜… Generalise this: when a gate's soundness rests on a timing premise, a board that never violates the premise cannot validate it. Its green run is not weak evidence, it is evidence about a different question.

The fix, and why it is a strict superset

The two things being told apart differ in shape, not size: an unwritten region leaves whole 64-cell blocks at the seed; a saturating W4A4 accumulator pinned at 0x7fff is scattered and leaves none. Mode 4 now refuses on any entirely-seed block, backstopped by a survivor count (0.1% of written cells, floor 8) for a sub-block tear β€” the vanishing-fraction rule orki_dyn_collision_landed's non-BCHAIN arm already shipped and that this arm's doc-comment claimed but never implemented. An all-seed program has all-seed blocks, so every refusal the old gate made is still a refusal, at finer granularity.

The margin, measured over an entire make test (the adjudicator is reached 25 times):

count census
REFUSE 23 4…1901 entirely-dead blocks every time (weakest 256/512 cells, worst 136696/352256)
ACCEPT 2 both the sat case's constructed collision β€” 1/1024 cells, 0 dead blocks

So the cap sits 8Γ— above the only legitimate collision ever observed and 32Γ— below the weakest tear, with the dead-block axis clean on both sides. The large tears had partial blocks too, so the count backstop is load-bearing, not decorative.

No hot-path cost, by construction: mode 4 is reachable only via orki_dyn_collision_landed, which ork_dyn_done_i calls only when collide_ok is set, which ork_dyn_end sets once per recover round after the spin has exited. The drain's hot path is modes 1/3/6 and the de-tile is mode 2. Measured anyway at K=512 N=2048 M=128: βˆ’0.7% in favour of the fix, inside this board's arm-position bias. Β§3's 1.15–1.24Γ— is intact.

β˜… The false accept was also a performance bug

At K=2048 N=1024 M=64 the unpatched tip measures 0/12 correct at 1,000,388 Β΅s β€” pinned at the ladder probe's 1 s cap, because accepting the torn surface froze the ladder on a geometry this part cannot run. With the fix the gate refuses, the ladder steps down, and the same call is 12/12 bit-exact at 954 Β΅s β€” 1047Γ— faster and correct, 3/3 in both arm orders.

Worth keeping: a gate that wrongly accepts does not merely return a wrong answer, it removes the signal a self-healing path needs to heal. The adjudicator and the ladder are one mechanism.

Shipped

8fa449a, and it is the first tree in the repo carrying a two-board attest β€” BOARD=radxa,rock-5b and BOARD=radxa,rock-5b-plus on the same CORE_SHA, make check-attest-all passing. Reverting cb0bf06 was considered and rejected: it greens the 5B+ and re-breaks every W4A4 model decode on the 5B, so it does not ship either.