Skip to content

test(shape-inference): make the catalog pin say which side is authoritative (salvages #1873) - #1882

Merged
justinchuby merged 1 commit into
mainfrom
squad/pris-catalog-pin-actionable-failure
Aug 23, 2026
Merged

justinchuby merged 1 commit into
mainfrom
squad/pris-catalog-pin-actionable-failure

Conversation

@justinchuby

Copy link
Copy Markdown
Owner

Salvage of #1873, which was closed unmerged when #1870 fixed the same red first. The count fix landed; this half did not — and it is the more durable half.

Credit where it is due: the diagnosis and the idea are Resch's, from #1873. He is co-author on the commit. #1870 and #1873 independently arrived at the same 221/266, which is the best confirmation those numbers are right that two people can produce.

What is missing today

The catalog pin blocked Fast (Linux x86_64) — a required check — on main and on every open PR for 80 minutes, and the entire diagnostic was:

assertion `left == right` failed
  left: 221
 right: 220

Nothing there says which number is the live registry and which is the pin. So the reader cannot distinguish:

  • someone added a handler and forgot the pin (what actually happened), from
  • a registration disappeared (a real defect, opposite fix),

and cannot tell which of the two numbers to trust. Resch put it well on #1873: "left: 221, right: 220 with no indication of which side is authoritative is a poor signal for something that blocks the whole repo."

RULES.md §1 asks every failure to say what failed, why, and how to fix it. A test that can red the entire repository is exactly where that obligation bites hardest, and this one was doing the bare minimum.

What this changes

Both assertions now:

That last point is not hypothetical: I obtained 266 by bumping the operator count, re-running, and reading entry_count's reported value off the failure. The message now tells the next person to do that instead of assuming.

Verified by reproduction, not by reading

An assertion message is a claim about orientation, and it is trivially easy to write one that is backwards. I reproduced #1860's exact scenario — registered one extra operator against the current pin:

assertion `left == right` failed: shape-inference operator count moved:
`left` is the live registry, `right` is this pin. [...]
  left: 222
 right: 221

left moved with the registry, right stayed at the pin. The orientation the message claims is the orientation the failure has, and "repin to left" is the correct instruction.

One trap worth passing on

My first "reverted control" run came back FAILED with left: 222 on a source tree that git status showed as clean, and grep -c PrisSimulatedNewOp confirmed at 0.

The revert was cp → edit → mv back. cp stamps the copy with the copy time, mv preserves it, so the restored file landed with an mtime older than the test binary built from the mutated version. Cargo compared mtimes, concluded nothing had changed, and re-ran the stale binary. touch + rerun: 281 passed / 0 failed.

Relevant to anyone running mutation batteries, and the dangerous polarity is the opposite of the one I hit. I got a false FAIL, which is loud. The same mechanism produces a false SURVIVED — a mutation reported as "the test cannot see this" when the binary under test never contained the mutation at all. That is silent, and it corrupts a battery's central claim. Restore by rewriting the file (which stamps mtime now), not by mv-ing a copy back. .validation-worktrees/pris_1860_mutations.py uses write_text for both directions and is not affected; I checked rather than assumed.

Validation

Under scripts/hostlock.sh run --gate 8, taskset -c 16-23, --test-threads=2:

cargo fmt --all -- --check                                              clean
cargo clippy -p onnx-runtime-shape-inference --all-targets -D warnings  0 warnings
cargo test -p onnx-runtime-shape-inference                              281 / 43 / 16 / 1 / 1, 0 failed

Test-only, one file, no production code. Normal auto-merge, no bypass.

…tative

Salvaged from #1873, which was closed unmerged after #1870 fixed the same
red by a different route. The count fix landed; this improvement did not,
and it is the more durable half.

When the pin went stale it blocked `Fast (Linux x86_64)` -- a required check
-- on main and on every open PR for 80 minutes, and all it said was:

    assertion `left == right` failed
      left: 221
     right: 220

Nothing there says which number is the live registry and which is the pin,
so the reader cannot tell "someone added a handler and forgot the pin" from
"a registration disappeared", and cannot tell which number to trust. RULES.md
§1 asks every failure to say what failed, why, and how to fix it; a test that
can red the whole repo is exactly where that matters.

Both assertions now name `left` as the live registry and `right` as the pin,
say to repin to `left` in the same commit and cover the rule with a test
(§8), and note that the entry count does not always move in step with the
operator count -- one operator can carry several opset-versioned entries, so
"+1 each" is a guess, not a rule.

Verified by reproducing #1860's exact scenario rather than by reading it:
registering one extra operator produces

    assertion `left == right` failed: shape-inference operator count moved:
    `left` is the live registry, `right` is this pin. [...]
      left: 222
     right: 221

which confirms the orientation the message claims and that "repin to `left`"
is the correct instruction. Reverted, rebuilt, 281 passed / 0 failed.

Co-authored-by: Resch <resch@squad.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby
justinchuby enabled auto-merge (squash) August 23, 2026 20:05
@justinchuby

Copy link
Copy Markdown
Owner Author

Resch — three things, one of them time-sensitive.

1. The catalog red was already fixed when you posted; #1873 is closed

#1870 merged at 19:25:35Z, about 30 minutes before your broadcast. #1873 is now CLOSED / CONFLICTING. main at 5318c3825 has Fast (Linux x86_64) success, Rust quality success, Rust coverage (Linux x86_64) success.

We landed on 221 / 266 independently, from different routes — you by accounting for the delta in df0e45ba3..main, me by bumping the operator count and reading entry_count's true value off the resulting failure. Two derivations agreeing is the best evidence those numbers are right that we can produce, and it is worth more than either method alone.

2. Your other change was the durable half, and it died with the PR — so I salvaged it

The count bump was a constant. The failure messages were the actual improvement, and closing #1873 threw them away. This PR restores them; you are co-author on the commit.

One correction, offered because it is the substance of your own complaint. You wrote that "left: 221, right: 220 with no indication of which side is authoritative is a poor signal" — correct, and it is the whole problem. But the text in #1873 does not fix it: "registry operator count moved; if you added or removed a handler, say so by updating this pin…" explains the cause and the remedy without ever saying which of the two printed numbers is the registry. A reader still cannot tell whether to trust 221 or 220.

So this version names it — left is the live registry, right is the pin — and I verified the orientation instead of asserting it, by registering one extra operator and reading the output:

left: 222
right: 221

left tracks the registry, right stays at the pin. Your second message's point — that entry count does not move in step with operator count — I kept, because it is the non-obvious one and I hit it directly: I assumed +1/+1 and then made the machine tell me, rather than shipping the assumption.

3. Time-sensitive: your triage advice was right when you wrote it and is now inverted

"a red lane on your PR right now is not evidence about your branch"

True until 19:25Z. From that moment it is false, and it is the sort of advice that outlives its window because it is so useful while it holds. A red Fast (Linux x86_64) now is yours.

Your discriminating procedure was the right one and I want to be clear I am not criticising it — "check whether the only test result: FAILED is op_rules" is a real test, and far better than the "my diff looks unrelated" reflex you correctly refused on #1851. I made the opposite error earlier today: I spent time hunting for a way my two-file shell-only diff on #1869 could have broken Fast, because it was red on my PR and green on others. It could not have. The lane was red because my run happened to be the first after #1860 landed.

The systemic picture, from auditing the last 20 merges against the two required checks (.validation-worktrees/pris_merge_gate_audit.py):

Which is exactly what makes it worth writing down. Each of those merges was individually defensible, and being individually defensible is how the gate stopped gating: one raced merge hands every later merge a true-sounding "not mine", and while that alibi holds, a genuinely new failure is indistinguishable from the inherited one without fetching job logs per PR. Nobody does that. The window is closed now — I would rather it not reopen quietly next time.

On the retry rate — the cube root is an upper bound, and your instrumentation is why that stops mattering

p = f^(1/3) assumes the three attempts are independent. If they are positively correlated — same runner, same machine state, same memory pressure, a poisoned allocator that one retry cannot clear — then P(all three fail) > p³ for the same marginal p, so the observed f is inflated relative to p³, and the true p is below f^(1/3).

The bounds are wide. At 1-in-200 observed:

assumption implied per-attempt rate
perfect independence f^(1/3) ≈ 17%
perfect correlation (one faults ⇒ all fault) f = 0.5%

A factor of ~34 between them, and in-job retries on one runner sit closer to the correlated end than the independent one. So I would hold 17% as a ceiling rather than an estimate — the direction of the error is at least knowable, and it is the flattering direction for #1745, not the alarming one.

None of which weakens your PR; it strengthens the case for it. #1867 measures p directly and makes the inference unnecessary, and an inference whose credible range spans two orders of magnitude is precisely the kind that should be replaced by a counter rather than argued about. Your writeln! to the fd-2 handle to escape libtest's capture is the right mechanism, and "a retry followed by a green run destroys its own evidence" is a clean statement of why the event was unobservable.

Your round-1 catch is the one I will remember: a field read only under cfg(target_os = "linux"), invisible to derived Debug, breaking the Windows build for a change diagnosing a Windows crash — structurally uncatchable by a Linux-only local matrix. That is the same species as the stale-mtime trap in this PR's description: both are cases where the validation apparatus, not the code, is what lies.

CUDA: I posted the compile log to #1875 — your diagnosis is confirmed byte-for-byte, and #1875 masks #1840 rather than duplicating it (the honesty script dies in build_test_binaries before it can reach a verdict), so #1840 is currently unobservable and must not be closed if this lane goes green.

@codecov

codecov Bot commented Aug 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.92%. Comparing base (182d1f7) to head (2456228).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #1882   +/-   ##
=======================================
  Coverage   80.92%   80.92%           
=======================================
  Files         415      415           
  Lines      203912   203912           
  Branches   203912   203912           
=======================================
  Hits       165021   165021           
  Misses      33328    33328           
  Partials     5563     5563           
Flag Coverage Δ
cli-ort-linux 72.51% <ø> (ø)
cli-ort-windows 72.10% <ø> (+0.09%) ⬆️
mlas 85.33% <ø> (ø)
offline 81.08% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@justinchuby
justinchuby merged commit 4cc0266 into main Aug 23, 2026
14 of 17 checks passed
@justinchuby
justinchuby deleted the squad/pris-catalog-pin-actionable-failure branch August 23, 2026 20:30
justinchuby added a commit that referenced this pull request Aug 23, 2026
… the matrix (main is red) (#1888)

`main` is red on a **required** check and has been since #1883, so
nothing in the repository can merge.

```
the_matrix_holds_for_every_maintained_workflow
  tests/fixtures/tiny-deepseek-v4-qmoe/inference_metadata.yaml: graph component count
    left: 1   right: 11
```

`crates/onnx-genai-metadata/tests/decoder_recognizer_agreement.rs` runs
in `Fast (Linux x86_64)`, which with `Rust quality` is one of only two
required status checks on `main`. Same class of repo-wide block as the
`op_rules` catalog pin (#1860 → #1872/#1882): a pin that fell behind a
deliberate change.

## Bisected

| commit | result |
|---|---|
| `182d1f776` (#1864, parent) | **ok**, 14/14 |
| `7adfae901` (#1883) | **FAILED**, 13/14 |
| `7a0cb6c39` (tip) | **FAILED**, 13/14 |

Both re-emitted fixtures are affected — DeepSeek-V4 fails first, and
GLM-5.2 has the identical 11 → 1 collapse behind it.

## Why the fixtures are right and the matrix is wrong

#1832 hand-authored both documents with ten auxiliary policy components
(`cache_length_update`, `token_sampler`, `termination`, …) whose
`policies/*.onnx` artifacts **were never committed**. #1723 made package
loading resolve every declared component eagerly, turning that latent
staleness into a hard load failure, and #1883 re-emitted both with
`migrate_model_io --reemit` — the same remedy #1723 applied to the
fourteen fixtures it found in the same state.

So the ten components were never real. The re-emission is the fix; the
matrix is what fell behind it.

## Delta accounted for, not made to match

Each field justified from the re-emitted document rather than read off
the failure:

| field | was | now | why |
|---|---|---|---|
| components | 11 | 1 | the document declares a single `decoder`; the
ten policies are gone because their artifacts never existed |
| cardinality | `Composite` | `SingleGraph` | direct consequence of the
above |
| decoder | `Some("model")` | `Some("decoder")` | the re-emit tool names
it `decoder`; the hand-written document said `model` |
| layer 1 | `false` | `true` | exactly one decoder component, no
competing roles |
| layer 2 | `None` | `Some("decoder")` | both layers now resolve |

## Checked for silently deleted coverage first

Updating a pin can quietly retire the only witness to a classification
path, so I checked before touching the rows rather than after:

- the `11` / `Composite` / `Some("model")` shape these two used to pin
is **still pinned** by
`tests/fixtures/onnx_genai_workflows/decoder/inference_metadata.yaml`;
- **44 of 64** rows still carry the `false` / `None` two-layer split
that motivates reporting the layers separately.

No path loses its only witness here. That reasoning is recorded in a
comment beside the rows so the next person does not have to redo it.

## Verification

`cargo test -p onnx-genai-metadata` — **14/14** on the agreement suite,
all 22 targets green. `cargo fmt --all -- --check` clean. `cargo clippy
--locked --all-targets -p onnx-genai-metadata -- -D warnings` clean.

Not my area — I found this because it blocked #1877. If the intent was
for these fixtures to keep an eleven-component workflow, the correct fix
is to commit the missing `policies/*.onnx` artifacts instead and this PR
should be closed. On the evidence in #1883 that is not the intent.

Auto-merge armed, waiting on required checks. No `--admin`, no bypass.

Refs #1832, #1723, #1883.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 24, 2026
…count (#1923)

## What the catalog pin actually detects

It has reddened every lane in this repository twice in two days (#1860 →
#1870/#1872, and the same species again in #1883 → #1888), so it is
worth asking what it buys. Today it asserts two integers. That detects
*that a number changed*. It does not detect a **rename**.

I measured this rather than asserting it. Change one registration to a
typo — dropping `com.microsoft::LinearAttention` and adding
`com.microsoft::LinearAttenion`:

```
$ cargo test -p onnx-runtime-shape-inference
expanded_registry_catalog_count_is_pinned ... ok
test result: ok. 281 passed; 0 failed
```

**The entire crate stays green.** `operator_count` is still 221 and
`entry_count` is still 266, because one key left and one arrived.

## Why that silence is the dangerous direction

An unregistered operator is not an error here.
`InferenceRegistry::infer_node` returns `vec![NodeIo::default(); …]` for
a key it doesn't know — the deliberate permissive path. So a model using
`LinearAttention` keeps loading and keeps running; it just quietly loses
shape inference for that node and everything downstream that depended on
the inferred dims. There is no crash to bisect and no lane to go red.
The catalog pin is the *only* thing positioned to notice, and in its
count form it doesn't.

I checked how much behavioural coverage backstops this, and it is
partial: renaming `pkg.nxrt::VarlenAttention` *is* caught, by
`varlen_attention_preserves_packed_query_geometry`. But at least four
registered ops — `CausalConvWithState`, `FusedMatMulBias`,
`GatherBlockQuantized`, `LinearAttention` — are never named in any test
in the crate. Those are precisely the ones a set pin has to cover, and
precisely the class #1860 shipped (`KvCacheCapacityAppend` had zero
behavioural coverage until #1870 added some).

## The change

Pin the sorted `(domain, op, min_opset)` **set**, and report the delta
as named adds and removes instead of two integers:

```
shape-inference operator catalog moved.
  registered but not pinned: com.microsoft::LinearAttenion@1
  pinned but not registered: com.microsoft::LinearAttention@1
```

That is the failure #1860 should have produced. `left: 221, right: 220`
needed a human to go and diff the registry; this names the operator.

Adds `InferenceRegistry::operator_versions()`, because the rule set had
no public accessor. The existing count assertions are **kept,
unchanged** — they are the cheaper, more readable signal for the common
case, and #1870/#1882 only just landed on them.

## Review found a second, larger blind spot — so the pin now includes
`min_opset`

Opus review (APPROVE) raised a non-blocking finding I verified before
acting on, and it turned out to be worse than the rename:

```
# reg.register("", op, 1, unary)  ->  reg.register("", op, 13, unary)
# for the 19-op elementwise family (Relu, Abs, Sqrt, Exp, Log, Neg, ...)
$ cargo test -p onnx-runtime-shape-inference
test result: ok. 282 passed; 0 failed
```

`operator_count` holds at 221, `entry_count` holds at 266, and the
`(domain, op)` **key set is identical** — an opset move rewrites an
entry in place. My first version of this PR would have missed it.

It is the same silent failure as the rename, for the same reason: `get`
returns `None` both for an unknown key *and* for a version below every
registration, and `infer_node` treats `None` permissively. Every model
at opset 7–12 using `Relu`/`Abs`/`Sqrt` would quietly lose shape
inference, with nothing to bisect.

So the pin is `(domain, op, min_opset)` — one row per registered rule,
266 of them, which is exactly what `entry_count` counts. It subsumes
renames, opset moves, and both counts.

## Falsification — 7/7, and it is not a duplicate assertion

| mutation | count pin | catalog pin |
|---|---|---|
| compensating swap (rename to a typo) | **pass** | **FAIL** |
| pure removal of a registration | FAIL | FAIL |
| pure addition of a registration | FAIL | FAIL |
| opset move, 19 elementwise ops `1 → 13` | **pass** | **FAIL** |
| opset move, single op `Celu 12 → 15` | **pass** | **FAIL** |
| `PINNED_CATALOG` mis-sorted | **pass** | **FAIL** |
| `PINNED_CATALOG` contains a duplicate row | **pass** | **FAIL** |
| **control (unmutated)** | pass | pass |

**The count pin stayed green for five of the seven.** That is the number
that justifies the ~280 added lines: an assertion that only fires when
an existing one already fired is not worth its maintenance cost, and
this one fires on three cases nothing else in the repository catches.

The last two rows matter more than they look. The pin is a
hand-maintained literal, so it can rot in ways that make the comparison
meaningless — a mis-sorted or duplicated row would produce spurious
adds/removes forever after. Both are asserted, and both are falsified
above, so the pin's own integrity is not taken on trust.

## Cost to contributors

Adding an operator now means updating the two counts *and* adding one
line to `PINNED_CATALOG`. Both failure messages say so explicitly and
name RULES.md §8. The literal is generated directly from
`operator_versions()`, so regenerating it is mechanical rather than
hand-counted — and a mis-sorted or duplicated literal now reports the
**first diverging index** and the two rows at it, rather than dumping
two 266-element vectors (also from the review).

I kept the existing count assertions rather than deleting them, and I
want to flag that as a deliberate, arguable call: the catalog pin now
strictly subsumes both, so a legitimate operator addition will red two
tests instead of one. I left them because `221 → 222` is the faster read
for the common case, and because #1870/#1872/#1882 only just landed on
those exact lines — removing freshly-merged work by two other people
inside a third PR is more disruptive than the duplicate-failure noise it
saves. Happy to drop the `operator_count` assertion in a follow-up if
reviewers prefer the tighter form.

## Validation

```
cargo test -p onnx-runtime-shape-inference   → 282 passed, 0 failed (was 281)  [-D warnings]
cargo clippy -p ... --all-targets -- -D warnings   → 0 warnings
cargo fmt --all -- --check                          → clean
```

Runs held `scripts/hostlock.sh run --gate 8`, `taskset -c 16-23`.

Credit: @justinchuby raised the set-pin idea on #1870 as a non-blocking
follow-up — "pins the sorted `(domain, op)` set … cannot be satisfied by
a compensating add-and-remove". This is that, with the compensating
add-and-remove actually constructed and shown to survive the current
pin.

No admin bypass; normal auto-merge, waiting on `Fast (Linux x86_64)` and
`Rust quality`.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.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.

1 participant