Skip to content

evidence(rocm): re-race the gfx1151 corpus; refuse unproven rankings at dispatch - #671

Merged
gstoner merged 6 commits into
mainfrom
evidence/autotune-separation-rocm
Sep 1, 2026
Merged

gstoner merged 6 commits into
mainfrom
evidence/autotune-separation-rocm

Conversation

@gstoner

@gstoner gstoner commented Sep 1, 2026

Copy link
Copy Markdown
Owner

The ROCm half of AUTOTUNE-SEPARATION, plus the tightening you approved to follow it.

All 16 gfx1151 rows now carry a verdict

Zero never-asked, where all 12 previously had separation: None. 15 separate cleanly — margins 34–99.9% against 0.10–14.24% noise. The one refusal is paged_kv_decode 8192 end_to_end at 6.52% margin vs 5.59% noise, genuinely marginal.

Verified the numbers come from device_event, not the wall-clock fallback this backend is prone to — the hazard I'd recorded — so the noise floor is genuine rather than inflated.

This is the opposite of sm_120's 82%-unsupported result, and I'd flag how to read it. ROCm races two candidates that are far apart (generic HIP vs WMMA); NVIDIA races four that often sit within a few percent. 15/16 vs 19/74 is field width, not measurement quality. A backend that adds candidates should expect its separated fraction to fall — that's the field getting more honest, not the hardware getting worse.

Two recorder defects, both bugs in their own right

  • record_paged_kv_corpus built MeasureRecord from a single measurement. It had no spread, so every row it wrote was unprovable by constructionseparation: None wasn't an omission, it was the only possible outcome. Now measures --repeats times (default 7). One pass would report a spread of zero and earn separated: True on every row automatically, which is worse than recording nothing.
  • The fused rows can't be re-raced by re-running a recorder. load_corpus makes every key a hit and _record_raced_the_live_field never consults separation, so a present row comes back unchanged. New driver evicts the rows it owns, warm-starts the rest, prints a before/after row-and-evidence diff, and refuses to write if another device's row count would drop.

One trap worth carrying: candidate registration is a side effect of importing the backend's emit module. A driver that forgets it gets an empty registry, and measured_arbitrate returns None for every shape — which prints as "no verified candidate", reading as "this hardware cannot do it" rather than "I forgot an import". A confident wrong answer, not an error. The driver now imports explicitly and refuses to record at all on an empty registry.

The tightening — and it is deliberately not "reject None"

corpus_winner now refuses a row that ranks two or more candidates and has no verdict.

separation_verdict returns None when fewer than two were timed, because a sole candidate is chosen by applicability, not by a race, and has no margin to defend. 12 of the 23 remaining None rows are exactly that shape — refusing them would be a category error, not caution. inf likewise isn't a competitor (it marks "could not be timed"), so a row with one latency and one inf is a sole-candidate row wearing a pair's clothes.

Committed corpus: 113 rows — 67 refused as dispatch hints, 34 with a supported verdict, 12 sole-candidate.

Mutation-verified three ways, including that the literal "refuse all None" rule fails, because it rejects sound sole-candidate rows.

Cross-backend, key AUTOTUNE-SEPARATION-ROCM-2026-09-01

ROCm parity validated. NVIDIA follow-up required — 11 sm_120 rows are now inert at shapes the recorder's default flags don't cover; recoverable by widening those flags on Super-Bear, and until then those buckets fall back to lead-safe tier priority, which is correct degraded behaviour rather than a regression. Apple/x86 not applicable, but the entry bar changed: a first device timer with one sample earns separated: True for free.

🤖 Generated with Claude Code

gstoner and others added 5 commits September 1, 2026 06:32
The 12 committed rocm:gfx1151 rows predate #663 and carry separation: None.
They cannot be re-raced by re-running a recorder: load_corpus makes every key a
hit and _record_raced_the_live_field never consults separation, so a present
row is returned unchanged. This evicts the rows it owns first.

Carries both rules the NVIDIA half learned by nearly breaking them -- warm-start
always (save_corpus writes the WHOLE cache, so a fresh start deletes every other
device's rows), and print a before/after row-and-evidence diff, because both
near-misses were regenerations that SUCCEEDED while producing weaker evidence.
It also refuses to write when the other-device row count would drop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ered

Registration is a side effect of importing the backend emit module. Without it
the registry is empty and measured_arbitrate returns None for every shape --
which prints as 'no verified candidate', reading as 'this hardware cannot do
it' rather than 'the driver forgot an import'. A confident wrong answer, not an
error.

The driver now imports rocm_hip explicitly and refuses to record at all when
the registry is empty, rather than writing a corpus that claims the hardware
has no candidates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…verdict

That recorder built MeasureRecord directly from a SINGLE measurement, so every
row it wrote carried separation: None -- 'never asked' -- which corpus_winner
accepts as a dispatch hint on trust. It now measures --repeats times (default 7),
takes a per-candidate median with the spread beside it, and records a real
separation verdict.

One pass would report a spread of zero and earn separated: True on every row
automatically, which is worse than recording nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…at dispatch

AUTOTUNE-SEPARATION, ROCm half, plus the tightening it unblocks.

ALL 16 gfx1151 ROCm ROWS NOW CARRY A VERDICT -- zero never-asked, where all 12
previously had `separation: None`. 15 separate cleanly (margins 34-99.9% against
0.10-14.24% noise); the one refusal is paged_kv_decode 8192 end_to_end at 6.52%
margin vs 5.59% noise, which is genuinely marginal.

That is the OPPOSITE of sm_120, where 82% of rankings were unsupported, and the
reason is structural rather than a hardware difference: ROCm races two
candidates that are far apart (generic HIP vs WMMA), NVIDIA races four that
often sit within a few percent. Verified the numbers come from
`device_event`, not a wall-clock fallback -- the hazard recorded for this
backend -- so the noise floor is genuine.

Two recorder changes were needed and both were bugs in their own right:

  * `record_paged_kv_corpus` built MeasureRecord from a SINGLE measurement, so
    it had no spread and every row it wrote was unprovable by construction. It
    now measures --repeats times (default 7) and records a real verdict. One
    pass would report a spread of zero and earn `separated: True` on every row
    automatically, which is worse than recording nothing.
  * A new `record_autotune_separation` driver for the fused rows, because
    re-running a recorder cannot re-race them: `load_corpus` makes every key a
    hit and `_record_raced_the_live_field` never consults `separation`. It
    evicts the rows it owns, warm-starts everything else, prints a before/after
    row-and-evidence diff, and refuses to write if another device's row count
    would drop.

THE TIGHTENING, now that every fleet row has had its chance: `corpus_winner`
also refuses a row that ranks two or more candidates and has no verdict.

NOT a literal "reject None", and the difference matters. `separation_verdict`
returns None when fewer than two candidates were timed, because a sole
candidate is chosen by applicability rather than by a race and has no margin to
defend -- 12 of the 23 remaining None rows are exactly that shape. Refusing
them would be a category error, not caution. `inf` is likewise not a
competitor: it marks "could not be timed", so a row with one latency and one
inf is a sole-candidate row wearing a pair's clothes.

Committed corpus after this: 113 rows -- 67 refused as dispatch hints (11 of
them unproven rankings), 34 with a supported verdict, 12 sole-candidate.

Mutation-verified three ways, including that the literal "refuse all None" rule
FAILS because it rejects sound sole-candidate rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… backends

Key AUTOTUNE-SEPARATION-ROCM-2026-09-01.

  ROCm    parity validated  -- all 16 rows carry a verdict, 15 separated; two
                               recorder defects fixed (single-measurement
                               records, and rows that a re-run cannot re-race)
  NVIDIA  follow-up required -- 11 rows now inert, recoverable by widening the
                               recorder's shape flags on Super-Bear
  Apple   not applicable    -- but the entry bar changed: a first device timer
  x86     not applicable       with one sample earns separated:True for free

Records the contrast honestly: 15/16 vs 19/74 is field width (two far-apart
candidates vs four close ones), NOT evidence that one backend measures better.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T11:52:17.160932Z f20d857 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f20d857a06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread benchmarks/baselines/autotune_corpus.json
Comment thread benchmarks/rocm/record_autotune_separation.py
Comment thread benchmarks/rocm/record_autotune_separation.py Outdated
… timer gate

Three review findings on #671. All three verified against the code before
fixing; all three correct.

P1 -- A SECOND CONSUMER BYPASSED THE RULE. `corpus_winner` is not the only path
from the committed corpus to production dispatch:
`cache/paged_kv.py::_rocm_paged_attention_corpus_winner` reads the same file
directly and checked only that the winner was a known NAME. So the admission
rules landed in one consumer while the other kept serving `direct` for the
gfx1151 8192-token bucket on a ranking THIS BRANCH marks separated: false
(6.52% margin vs 5.59% noise). Reproduced exactly, then fixed.

This is the #670 finding one level down: there, a verdict with no consumer;
here, a rule with only some of its consumers. The checks are now one predicate
-- `record_is_admissible` -- called by both, rather than two copies to drift.

P1 -- THE TIMER GATE WAS DOCUMENTED AND ABSENT. The module docstring said "the
reason `--check-timer` exists"; no such flag was registered and
`rocm_last_timer_source` was never consulted. Rows recorded during a wall-clock
fallback would have been saved as timing="device" -- the wrong timing domain
wearing the right label, in published evidence. Documenting a gate that does
not exist is worse than not mentioning one, because a reader believes the write
was checked; it was checked by hand in a separate probe, which is how the claim
came to be written. Now implemented, on by default.

P2 -- THE GUARD HAD THE BLIND SPOT IT GUARDS AGAINST. Eviction matched
startswith("rocm") while `_summarise` counted every ROCm device as one bucket,
so the "did another device lose rows?" check could not see the loss of a
sibling ROCm device's fused rows. Both now key on the exact rocm:gfx1151
device. No live damage -- gfx1151 is the only ROCm device in the corpus -- but
the guard existed precisely to prevent silent evidence deletion.

Mutation-verified: removing the paged-KV admission check fails two tests, and
_summarise now classifies a sibling ROCm device as `other`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@gstoner

gstoner commented Sep 1, 2026

Copy link
Copy Markdown
Owner Author

Evaluated all three against the code before touching anything. All three are correct, and one of them lands harder than the reviewer stated.

P1 — the second consumer. Correct, and reproduced

corpus_winner is not the only path from the corpus to dispatch. Verified concretely:

row separated?  False   separation: {margin 6.52%, noise 5.59%, separated: False}
route lookup returns:  direct
-> production serves a route the corpus marks as noise

This is the #670 finding one level down: there, a verdict with no consumer; here, a rule with only some of its consumers. I fixed one call site and assumed it was the boundary.

Fixed by extracting one predicate — record_is_admissible — called by both, rather than duplicating three checks into a second function where they'd drift. After: the 8192 bucket returns None (falls through to a live race), the separated 512 bucket still returns direct.

P1 — the phantom flag. Correct, and worse than described

My docstring said "the reason --check-timer exists". There was no such flag and rocm_last_timer_source was never consulted — 0 occurrences.

The reviewer frames it as a missing option. I'd put it more sharply: documenting a gate that doesn't exist is worse than not mentioning one, because a reader believes the write was checked. It was checked — by me, by hand, in a separate probe — which is exactly how the claim came to be written as if the script did it. Now implemented and on by default, refusing to write device-timed rows when the timer wasn't device_event.

P2 — the guard's blind spot. Correct, and the irony is the point

Eviction matched startswith("rocm") while _summarise counted every ROCm device as one bucket — so the "did another device lose rows?" check could not see the loss of a sibling ROCm device's rows.

That guard exists specifically to prevent silent evidence deletion, after I nearly deleted 12 ROCm rows on the NVIDIA half. It had the same blind spot as the bug it guards against. Both now key on the exact rocm:gfx1151 device; verified _summarise classifies a hypothetical rocm:gfx942 as other.

No live damage — gfx1151 is the only ROCm device in the corpus — but that's luck, not design.

Verification

Mutation-verified: removing the paged-KV admission check fails two tests. 279 passed across corpus/autotune/arbiter/paged suites; mypy clean.

The pattern worth naming

Across #670 and #671 the same shape has now appeared three times: a rule that exists in fewer places than it applies. A verdict nothing read; a rule only one of two consumers applied; a guard that couldn't see the case it guarded. Each time the fix was to have one statement of the rule rather than a copy — which is Decision #31's "one implementation per boundary" showing up in admission logic rather than lowering paths.

@gstoner
gstoner merged commit 7fba15d into main Sep 1, 2026
17 checks passed
@gstoner
gstoner deleted the evidence/autotune-separation-rocm branch September 1, 2026 13:33
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.

1 participant