exl3_int8_verify_min_rows 40 -> 36 — SERVED ACCEPTANCE + THROUGHPUT A/B
=======================================================================
Box 10.1.0.110 (arbi-whitebox-110), one RTX 4090, GPU-EXCLUSIVE.
2026-09-07.  Issue #2122, verify path.  Pays the debt
docs/receipts/int8-verify-slate-geometry-2026-09-07.txt left open.


WHAT WAS OWED
-------------
The geometry sweep measured, against the leg the engine SERVES, that the int8
verify chain beats the trellis leg on all seven 27B geometries from M = 36 up,
and that `min_rows = 40` therefore left one captured rung -- 36 rows, which is
`compose.serve.yaml`'s B = 6 at K = 5 -- on a leg it loses on.  It did not move
the threshold, because moving it routes a slate onto DIFFERENT ARITHMETIC and
that owes an acceptance result, which no kernel bench can pay.  This is that
result.

Everything below is a SERVED number.  No figure here is kernel-level.


THE ARMS, AND WHY ONLY ONE RUNG MOVES
-------------------------------------
One flag apart, identical code, same container, same image, same worktree.

  arm A   ARBI_EXL3_INT8_VERIFY_MIN_ROWS=40   (the shipped default)
  arm B   ARBI_EXL3_INT8_VERIFY_MIN_ROWS=36

At K = 5 a verify slate is B*(K+1) rows, so the captured ladder is
6/12/18/24/30/36/42/48 and the threshold separates the arms at EXACTLY ONE
rung: 36.  c = 1 (6 rows) and c = 8 (48 rows) take the same leg in both arms
and are driven as NULL CONTROLS -- if they move, the instrument moved.

The witness that the arm is the arm is `exl3_int8_verify_gemm_captured`, read
off /v1/admin/flag_truth after each boot.  Verify runs entirely under
cudagraph replay (`cudagraph_verify_replay` fires once per verify step and the
SERVE-window counter never advances during a drive), so the leg is decided at
GRAPH RECORD and replayed; the captured counter is the only place the decision
is visible.  400 exl3 linears reach the seam per verify forward:

  arm A   fired  800 / refused 2400   ->  2 of 8 rungs on int8   (42, 48)
  arm B   fired 1200 / refused 2000   ->  3 of 8 rungs on int8   (36, 42, 48)

Identical on all three boots of each arm.  One rung moved, and it is the rung
under test.  Both arms also witness `PREFILL: 400 of 402 bound linears` -- the
int8 leg BUILT.  That is not decoration: on this box's previous drives the
image's prebaked `arbi_serve_exl3_i8_gemm_v1` was stale against main's csrc and
the leg served NOTHING (experiments/accept_decomp/RESULTS.md).  #2139 rebuilds
it from the installed sources; the first boot here paid ~1380 s of nvcc for that
and every later boot in both arms hit the cache.


THROUGHPUT AND TPOT, PER SHAPE, n = 3 BOOTS PER ARM
---------------------------------------------------
median [min-max] over three separate boots.  Each boot drives c = 1, 6, 8 in
that order, `bench_mtp_decode.py` on q36_prompts.jsonl, `--endpoint chat
--sampling canonical --mtp-k 5 --expect-isl 0 --min-requests 40`, BENCH_SEED
1234, 40 requests per level, max_tokens 256.

  agg tok/s (system throughput, prefill included)

    c   rows   arm A (40)                   arm B (36)                    B/A
    1      6   164.78 [163.11-165.16]       164.22 [164.01-165.42]      0.997
    6     36   435.90 [435.82-438.77]       523.37 [520.49-527.19]      1.201
    8     48   604.97 [593.59-620.83]       619.52 [614.41-622.02]      1.024

  median TPOT ms (lower is better)

    c   rows   arm A (40)                   arm B (36)                    B/A
    1      6   5.457 [5.420-5.508]          5.528 [5.412-5.556]         1.013
    6     36   12.515 [12.475-12.533]       10.109 [10.003-10.139]      0.808
    8     48   10.968 [10.962-11.829]       10.891 [10.378-11.012]      0.993

The c = 6 ranges are DISJOINT on both metrics and by a wide margin: arm A's
best agg (438.77) is 82 tok/s below arm B's worst (520.49), and arm A's best
TPOT (12.475 ms) is 2.3 ms above arm B's worst (10.139 ms).  Both null
controls OVERLAP on both metrics in both directions, which is what says the
1.20x is the rung and not the box.

READ THE LADDER, not just the delta.  Under arm A, c = 6 is SLOWER PER TOKEN
THAN c = 8 (12.515 ms vs 10.968 ms, medians over the same three boots) --
FEWER ROWS, MORE TIME PER TOKEN.
That inversion is the B=4->B=5 doubling the geometry sweep names, sitting on a
served rung: at 36 rows the trellis leg reads the whole 11.3 GiB verify trellis
twice, and at 48 rows the int8 leg does not.  Arm B removes the inversion.


ACCEPTANCE, PER SHAPE, n = 3 BOOTS PER ARM
------------------------------------------
`accept_len` = emitted tokens per verify step = 1 + accepted/row.  K = 5, so
the ceiling is 6.  ACCEPTED PER ROW OF 5 is accept_len - 1.

    c   rows   arm A (40)                   arm B (36)                  B - A
    1      6   4.446 [4.410-4.479]          4.460 [4.448-4.479]         +0.014
    6     36   4.620 [4.600-4.646]          4.626 [4.572-4.652]         +0.006
    8     48   4.638 [4.596-4.693]          4.607 [4.588-4.642]         -0.031

  as ACCEPTED PER ROW OF 5 (accept_len - 1):

    c   rows   arm A (40)                   arm B (36)
    1      6   3.446 [3.410-3.479]          3.460 [3.448-3.479]
    6     36   3.620 [3.600-3.646]          3.626 [3.572-3.652]
    8     48   3.638 [3.596-3.693]          3.607 [3.588-3.642]

FLAT, and it moves UP on the treated rung, not down.  Every arm-B range
overlaps its arm-A range at every rung, the treated rung's difference of
medians is +0.006, and the LARGEST difference in the table (-0.031) is on
c = 8, a rung whose verify arithmetic is BIT-IDENTICAL in both arms.  That is
the reading that sizes the noise: the instrument cannot resolve 0.03 here, and
the treated rung moved a fifth of that, upward.

The treated rung's spread (arm B 4.572-4.652, width 0.080) is not larger than
the untreated c = 8 spread in arm A (4.596-4.693, width 0.097), which is what
would show if the new arithmetic were adding variance rather than none.

The two null controls are the load-bearing half of this table.  c = 1 and c = 8
take bit-identical legs in both arms, so their acceptance MUST be flat; they
are, and their residual spread is the floor everything else is read against.


WHERE ACCEPTANCE IS LOST — THE MISS-vs-DRAW SPLIT
-------------------------------------------------
The table above says acceptance did not move.  This says its COMPOSITION did
not move either, which is the thing a change of verify arithmetic could move
while leaving the total alone: a leg that shifts the target's logits could
trade drafter misses for sampling rejections at a constant sum.

One extra boot per arm with ARBI_MTP_ACCEPT_DUMP on, c = 6 only, everything
else identical.  `experiments/accept_decomp/analyse.py` over the drive
records.  THE DUMP IS A HOST-SIDE COST on every token-emitting step, so these
two boots' THROUGHPUT IS VOID and no timing from them appears anywhere in this
receipt; the counts are exact.

Guard, and it is the one this instrument needs: the writer's own
`mtp_accept_dump_write` matches its file exactly in both arms -- 677 = 176
boot-probe records set aside + 501 drive records (arm A), 664 = 176 + 488 (arm
B).  The boot probes drive real verify steps with the dump on and their
records are synthetic traffic; they are moved aside before the drive rather
than filtered afterwards.  These records are a SAMPLE of the drive's verify
commits (the diagnostic gathers logits on the gathered lane), not every
commit, which is why the split is read as rates and the aggregate acceptance
above -- a count of every token, off the engine's own accepted counter -- is
what the verdict rests on.

                                   arm A (40)      arm B (36)      B - A
  verify rows sampled                    2119            2043
  drafted per row                        5.00            5.00
  ACCEPTED PER ROW OF 5                 3.423           3.495       +0.072
  tested slots                           8295            8132

  of every TESTED slot:
    drafter right, draw kept it         81.6%           81.8%       +0.2pp
    drafter right, draw REJECTED it      1.9%            1.9%        0.0pp
    drafter wrong, draw rejected it     10.7%           10.3%       -0.4pp
    drafter wrong, draw kept it          5.9%            6.0%       +0.1pp

  of every REJECTION:
    sampling (drafter was right)        15.1%           15.2%       +0.1pp
    drafter miss                        84.9%           84.8%       -0.1pp

  per-slot drafter hit rate, hit%(all) — unbiased over every drafted slot:
    slot 0                               83.7            84.4
    slot 1                               79.6            81.4
    slot 2                               77.3            78.1
    slot 3                               74.7            74.8
    slot 4                               72.6            72.4

THE SPLIT IS THE SAME.  Sampling loss is 1.9% of tested slots in both arms to
the digit; the drafter/sampling division of rejections moves 0.1pp.  The
per-slot hit rates differ by at most 1.8pp at slot 1 on n ~ 2100 (binomial sd
~ 0.9pp) and carry the SAME depth curve, so nothing about how the drafter is
scored changed -- which is what a change of verify arithmetic would have shown
up as here first.

Sign, stated because it matters which way an inconclusive result leans: every
difference that is not zero leans toward arm B.  This is not read as a gain --
it is inside the noise, and the primary table's +0.006 is too -- but the
change is not being landed over an acceptance cost that the sample is too
small to see.

CONDITIONS AND GUARDS
---------------------
arbi-serve `ba4e44170` (main), bind-mounted over
registry.arbi.work/arbi-serve:test-latest from a detached worktree; the image's
tkv 0.53.0 is the shipped wheel and was NOT overlaid (nothing under test is in
tkv).  compose.dflash.yaml + compose.bindmount.yaml + compose.benchmark.yaml,
--env-file dflash-local.env, ARBI_MTP_K=5, container `arbi-verify-minrows`,
port 8010.  Qwen3.8-27B-exl3-4.0bpw verifier + Qwen3.8-27B-DFlash2-exl3-6.0bpw
drafter, vq2 hadamard k4v4 bundle, mb8, GMU 1.0, max-context auto.  K=5 rather
than the recipe's 7 because the disputed width exists only on the K=5 ladder:
at K=7 the rungs step 32 -> 40 and `min_rows` 36 and 40 admit the same set, so
the change is INERT there and a K=7 drive would measure nothing.

  * No clock pin.  `nvidia-smi -q -d CLOCK` before every drive: Applications
    Clocks Setting "Not Active", no locked graphics clock, 450 W limit, idle
    210 MHz / ~22 W between runs.  HW Slowdown / HW Thermal Slowdown "Not
    Active" in every before/after snapshot.
  * GPU exclusive.  Nothing else on the card in any run; `nvidia-smi
    --query-compute-apps` empty before the first boot.
  * `restart: "no"` (compose.benchmark.yaml), and `RestartCount` 0 with
    `StartedAt` unmoved across all three levels of all six drives.
  * Both arms carry ARBI_SERVE_LOGPROBS=1 and unauthenticated admin from
    compose.benchmark.yaml.  Those cost KV (a 424 MiB logprobs tile) and are
    identical in both arms.
  * Boots ALTERNATE arms (A, B, A, B, A, B) so any monotone drift in the box
    splits across arms rather than loading one.
  * Every cell is [VALID] by the bench's own gate; degenerate-response flags
    and the reasoning fraction (58-64%) sit in the same band in both arms.
  * All six drives use one seed base and one prompt file, so the two arms see
    the same 40 prompts in the same order.


VERDICT
-------
LAND IT.  Acceptance is flat -- treated-rung delta +0.006 accept_len against a
0.031 null-control spread on an untreated rung -- the speed win is 1.20x agg /
0.81x TPOT on the treated rung with DISJOINT ranges across three boots per arm,
and neither untreated rung moves.  Decode is not regressed at any shape: c = 1,
which is one decode row plus a 6-row verify slate BOTH arms refuse, reads
164.78 vs 164.22 tok/s and 5.457 vs 5.528 ms TPOT, overlapping in both.

WHAT THIS DOES NOT SAY
  * Nothing about K = 7 deployments, where the change is inert by construction.
  * No exchange rate between acceptance and tok/s is quoted or implied.  The
    old "+17.5% tok/s per accepted token" figure is WITHDRAWN -- it was the
    identity 1/(committed per step-row), and it was taken on boots where the
    int8 leg served nothing.  This A/B needs no exchange rate, because
    acceptance did not move: the speed win and the acceptance result are two
    independent readings, not one converted into the other.
  * The threshold is still a value measured on ONE card at ONE draft depth.
    It stays a knob for that reason.
  * Nothing here measures the SERVED distribution of slate widths, so how
    often a deployment lands on the 36-row rung is still unknown -- that is
    the open item the geometry receipt names, and this result raises rather
    than lowers its value.
