Skip to content

Experiment: eight-lane warm partial-comb evaluator (31% gate on Zen 5) - #1

Open
7layermagik wants to merge 10 commits into
mainfrom
claude/warm-comb-x8
Open

7layermagik wants to merge 10 commits into
mainfrom
claude/warm-comb-x8

Conversation

@7layermagik

Copy link
Copy Markdown
Contributor

Adds an eight-lane heterogeneous partial-comb evaluator and the Zen 5 gate for it. Experiment only: nothing is wired into the verifier and no dispatch changes.

Why

The promoted warm-comb verifier is x4-only. Its scheduling unit can already be eight signatures, because r51WarmDispatchWidth consumes aligned pairs to keep x8 occupancy in the surrounding pipeline. But the arithmetic underneath is two sequential YMM groups, not one ZMM group. The cold path already took that step; the warm path never did.

Result

Ten one-second samples per row, pinned physical core on a Ryzen 7 9700X, GOMAXPROCS=1, performance governor:

evaluator form median ns/signature min max allocations
two x4 groups (current shape) 2,287 2,285 2,287 0
one x8 group (candidate) 1,577 1,572 1,582 0

31.0% per signature, 1.45x, spread inside 0.5% on both rows. The same structural change on the cold path measured 26%, so the warm evaluator gains slightly more.

Evidence, environment and checksums: docs/results/zen5-warm-comb-x8-gate-2026-07-26/.

What the benchmark covers

The whole evaluator: the merged exponent loop, every doubling, both comb passes, and the balanced recoding. Not a single kernel.

That distinction is the point. The two-chain ZMM candidate passed its component gate and then regressed roughly 66% at the loop level on Zen 4 (zen4-two-chain-zmm-loop-gate-2026-07-26), so a component measurement is not evidence about a loop.

Cost of building it

Lower than expected:

  • No new assembly. The micro-AoS transpose-select, the cached add, the fixed-base comb and the element negate all already existed at x8. What was missing was pure Go: the round/digit types, the recoder, the two selectors, the passes, the evaluator.
  • No table change and no rebuild. The per-key table is a scalar table, so an x4 group owns four instances and an x8 group owns eight. Keys already promoted for the x4 path are consumed unchanged, with no extra bytes per key and no cache-policy change.

Nothing about the table format, the recoding rule, the schedule, or the acceptance predicate differs from the x4 evaluator. The exponent loop, pass ordering and balanced-digit semantics are the x4 ones with a wider lane count.

Table sizes this consumes, unchanged

Worth stating explicitly, because the obvious worry about a wider group is that it costs more memory. It does not: the per-key table is a scalar table, so an x8 group holds eight pointers to the same 120-byte-entry tables an x4 group holds four of. Every figure below is what ships today.

Per-key A table (promoted, per warm key)

spec digits rows entries/row online depth payload
A6/r9 (in use) 43 5 32 48 19,200 B

The promoted cache entry is r51WarmTableBytes = 19,424 B, which is the 19,200 B comb plus the 192 B decoded-A entry it supersedes (32 + 4*5*8). At the 128 MiB DefaultMaxTableBytes that is 6,909 warm keys, against 699,050 if the same budget held only decoded-A entries.

Neither is the effective ceiling: admissionEntryLimit is 131,072, so admission bookkeeping binds first. That number should not be quoted as a capacity.

Shared fixed-B table (one process-wide instance)

spec digits rows entries/row online depth payload pre-signed
B10/r5 (in use) 26 6 512 40 368,640 B 737,280 B
B8/r3 (retained) 32 11 128 16 168,960 B 337,920 B

B is pre-signed, storing both signs so the hot loop needs no post-selection negation; that is what doubles it. It is shared across every lane and every key, so the ~720 KiB is paid once per process, not per key. Per-key A tables deliberately stay single-sign, since pre-signing them would double 19,200 B on every key.

All of the above is pinned by TestHeterogeneousPartialCombTableShapesAndPayloadsExperiment.

On narrower combs. PERF_FINDINGS §5.3 floats A5/r9 and A4/r9 as ways to buy more keys. This PR also corrects that table: it divided the budget by the bare comb payload rather than the promoted cache entry, so every count was overstated (A6/r9 holds 6,909 keys, not 7,158), and admissionEntryLimit binds before the byte budget on every tier anyway. The two narrower rows are also arithmetic rather than configuration — heterogeneousPartialCombSpecExperiment.validate() admits widths 6 through 10 only, so both panic today. Whether either is worth a slower warm path still depends on the unmeasured fee-payer recurrence distribution, and this PR takes no position on it.

Correctness

The differential compares the x8 evaluator against two x4 evaluators, deliberately, rather than against a scalar reference: the x4 path is what ships, so a disagreement is a behaviour change whichever side is wrong.

  • eight rounds of distinct mixed-torsion bases under random projective scaling;
  • swept over active masks 0xff, 0x0f, 0xf0, 0b10110101, a single low lane, a single high lane, and zero;
  • a separate test nulls each lane's table in turn, carrying forward the guard added for the warm-comb nil-lane crash.

Both pass on the Zen 5 host; correctness.txt in the evidence directory is that run.

What this does NOT establish

  • No complete-verifier number. The evaluator is roughly half the warm path, so 31% here implies something closer to 15% end to end. Not run.
  • Not wired. The verifier still calls the x4 evaluator. This PR changes no dispatch.
  • Zen 4 unmeasured. Zen 4 double-pumps 512-bit operations and is exactly where the two-chain candidate died. This result must not be assumed to transfer.
  • Fill rate unaddressed, and it may dominate. A true x8 warm group needs eight homogeneous warm lanes rather than four, and mixed warm/cold lanes in one group stay out of scope (incompatible doubling schedules). Today a pair that is not fully warm still runs one warm x4 group plus a cold one; with a genuine x8 kernel there is no such partial credit. Whether eight consecutive promoted keys occur often enough depends on the fee-payer recurrence distribution, which is still unmeasured.

Commits

  1. d42d75d — the eight-lane evaluator and its differential tests
  2. a0604ad — the Zen 5 gate evidence bundle
  3. 9edbf2c — the PERF_FINDINGS capacity-arithmetic correction

Suggested next step

Wire it behind the existing PreferWarmX8IFMA policy bit and run a complete-verifier A/B through the public API, plus a Zen 4 gate before any dispatch change.

🤖 Generated with Claude Code

claude added 10 commits July 26, 2026 17:29
The warm-comb verifier is x4-only. Its scheduling unit can already be
eight signatures, but only as two aligned x4 groups run back to back:
the arithmetic is two YMM groups, not one ZMM group.

This adds the eight-lane evaluator. It changes no table format, no
recoding rule, no schedule and no acceptance predicate; the exponent
loop, pass ordering and balanced-digit semantics are the x4 ones with a
wider lane count. Every kernel it calls already existed in x8 form, so
no new assembly was needed.

The per-key table needs no rebuild: it is a scalar table, so an x4 group
owns four instances and an x8 group owns eight. Keys already promoted
for the x4 path are consumed unchanged.

Not wired into the verifier, and not gated yet. The differential test
compares the x8 evaluator against two x4 evaluators over the same inputs
across a sweep of active masks, since the x4 path is what ships and any
disagreement is a behaviour change. Nil lanes are swept at every
position, carrying forward the guard from the warm-comb crash fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ten one-second samples per row on a pinned Zen 5 physical core: one x8
group verifies eight signatures at a median 1,577 ns/signature against
2,287 for two x4 groups, a 31.0% reduction with the spread inside 0.5%
and zero allocations on both rows. The same structural change on the
cold path measured 26%, so the warm evaluator gains slightly more.

The benchmarked unit is the whole evaluator -- merged exponent loop,
every doubling, both comb passes, the recoding -- not a single kernel.
That distinction is deliberate: the two-chain ZMM candidate passed its
component gate and then regressed 66% at the loop level on Zen 4.

Recorded with the limits stated rather than implied. It is an upper
bound on the complete-verifier gain, since hashing, decoding and
finalization sit outside the loop and do not widen; the evaluator is
roughly half the warm path, so end to end is likely nearer 15%. Zen 4 is
unmeasured and must not be assumed. And a true x8 warm group needs eight
homogeneous warm lanes rather than four, with no partial credit, so the
unmeasured fee-payer recurrence distribution may decide whether the win
is reachable at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two errors in the comb-width table. It divided the 128 MiB budget by the
bare comb payload, but the promoted cache entry is r51WarmTableBytes --
the comb plus the 192-byte decoded-A entry it supersedes -- so every
count was overstated: A6/r9 holds 6,909 keys, not 7,158. And as already
noted for the decoded tier, the byte budget is not the binding limit
anyway, since admissionEntryLimit is 131,072 and applies to every tier.

Also records that the two narrower rows are arithmetic rather than
configuration. validate() admits widths 6 through 10 only, so A5/r9 and
A4/r9 panic today; adopting either means relaxing consensus-critical
shape validation, not turning a knob.

Found while stating the same figures in the x8 evaluator gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Decomposing the x8 warm evaluator put 16.5% of it in scalar recoding,
which was unexpected for a pure bookkeeping pass. The generated assembly
explains it: carry = (digit + half) / radix compiled to a hardware IDIVL
in the innermost loop, executed once per digit per lane -- 344 divisions
for the A term and 208 for B on every group of eight.

The division is avoidable and the replacement is exact. The bit extractor
yields a digit in [0, radix-1], the incoming carry is 0 or 1, and half is
radix/2, so digit+half lies in [half, radix+half] and is never negative.
Over a power-of-two radix a shift is then exactly the truncating division,
and the compiler only emitted a division because it could not prove the
numerator non-negative.

Applied to the shipping x4 recoder as well as the x8 experiment, so this
is a production-path change rather than an experimental one. No digit,
carry, sign or mask semantics move; the existing differentials cover it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Recaptures the gate at the post-division-fix commit and adds a component
decomposition that explains 89% of the evaluator.

Two findings the measurement produced. Recoding was 16.5% of a pure
bookkeeping pass because it compiled to a hardware IDIV per digit per
lane; removing it moved the shipping two-x4 form from 2,287 to 2,229
ns/signature. And selection is not the bottleneck at 14.2% -- addition
and doubling arithmetic is 62.1% -- so further large gains must come from
fewer or cheaper group operations, not faster table lookup.

Sizes the levers to a sub-microsecond warm path against the model. Three
of them reach roughly 1,050-1,100 ns/signature at no memory cost; going
decisively below one microsecond needs a wider A window, which is per-key
memory, which is capacity, which is gated on the same unmeasured
recurrence distribution as the comb width.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sweeps the comb shape on Zen 5 and answers whether the warm path can
reach one microsecond. It cannot, by shape alone: eliminating the
doubling chain entirely costs 8.6x the per-key memory, drops capacity to
811 keys, and still measures 1,133 ns/signature. The 69 additions, two
recodes and 69 selections set a floor near 1,040. Reaching a microsecond
needs a cheaper addition, not a different comb.

Two actionable findings fall out. Only depth matters, so the largest
passes count that reaches a target depth is always correct: A6/r7 and
A6/r5 both measure about 1,460 with B10/r5 because both already sit
under B's depth of 40, and r7 gets there for 22% less per-key memory.

And the shipping pair is mismatched in the expensive direction. A6/r9's
depth of 48 exceeds B10/r5's 40, so eight doublings are driven purely by
the per-key side. B is one process-wide table, so rebalancing toward it
-- B10/r3 with A6/r5 -- reaches depth 24 for 34,560 B/key, 12.8% faster,
with the extra rows paid once rather than per key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants