ADR-1428: Exact GPU twins are declared by one fragment file each, not by a shared literal¶
- Status: Accepted
- Date: 2026-10-01
- Deciders: lusoris
- Tags:
gpu-parity,ci,testing,docs,rc3,fork-local
Context¶
ADR-1397 made a parity-gate cell an equality when both sides are the CPU extractor or a twin listed in EXACT_TWINS in scripts/ci/cross_backend_calibration.py. Every RC3 pull request that makes a twin bit-identical lists it there. Each one therefore edited the same four places: the EXACT_TWINS dict literal, the test that asserted the whole dict, the table row and "Exact twins" paragraph in docs/development/cross-backend-gate.md, and the "Exact twins" paragraph in scripts/ci/AGENTS.md. Ten open pull requests conflicted pairwise on these lines, and each conflict was resolved by hand before the train could land it.
The repository already removed the same class of conflict for the changelog and the ADR index (ADR-0221): one file per entry, the shared list rendered from the files.
Decision¶
We will declare an exact twin by adding one file, scripts/ci/exact_twins.d/<feature>.<backend>, and editing nothing shared.
- A fragment holds
key: valuelines:adr:(one or moreADR-NNNN, each an existing file underdocs/adr/) andevidence:(one line: fixtures and result). Any other key, a missing key, a malformed value, an empty file or a name that is not<feature>.<backend>fails the loader. cross_backend_calibration.pybuildsEXACT_TWINSfrom the directory at import, sorted. A missing or empty directory raises instead of meaning "no exact twins". The public names and behaviour ofEXACT_TWINS,is_exact_pairand theEXACT_TWIN_*constants do not change.cross_backend_parity_gate.pychecks every fragment against itsFEATURE_METRICSandBACKEND_SUFFIXat import, so a fragment naming an unknown feature or backend fails the gate instead of never matching a cell.docs/development/cross-backend-exact-twins.mdis generated from the fragments byscripts/docs/generate-exact-twins.py, wired intomake docs-fragments-writeandmake docs-fragments-check. On a merge conflict the generated file takes master's side and is regenerated.- The tests assert properties that hold for any set of fragments, not the set itself.
The rule for listing is unchanged: a measurement that shows bit-identity and an ADR that records it. A listed twin that drifts is fixed, never given a tolerance.
Alternatives considered¶
| Option | Pros | Cons | Verdict |
|---|---|---|---|
| Keep the dict literal | No new mechanism | Conflicts on every RC3 pull request, in four files | Rejected: the cost is paid on every landing |
| One data file (YAML or JSON) listing all twins | One parser, one place to read | Still one shared file; two additions in neighbouring lines conflict | Rejected: moves the hotspot, does not remove it |
| One file per feature | Fewer files | Two backends of one feature landing together edit the same file and conflict | Rejected |
| One file per (feature, backend) | Two pull requests touch disjoint files; the name is the key, so a duplicate cannot exist | One more directory and a generator | Chosen |
Consequences¶
- Positive: declaring a twin exact is one added file; no shared line is edited, so the pull requests no longer conflict with each other.
- Positive: the documentation table cannot drift from the gate, because both read the same files.
- Negative: the pull requests open when this lands conflict with it once. Each owner deletes their
EXACT_TWINShunk, the test assertions and the prose they added, and adds a fragment instead (scripts/ci/AGENTS.mdlists the steps). - Neutral: the parse is a hand-written
key: valuereader, so no new dependency enters the gate (HISS-11).