Skip to content

test(ep-cuda): list the NMS and Unique materialization syncs, both reviewed - #2157

Closed
justinchuby wants to merge 2 commits into
mainfrom
squad/gaff-2149-capture-sync-allowlist
Closed

justinchuby wants to merge 2 commits into
mainfrom
squad/gaff-2149-capture-sync-allowlist

Conversation

@justinchuby

Copy link
Copy Markdown
Owner

Fixes the red in #2149. Two allowlist entries, no kernel changes.

The decision each half needed

#2149 asks the right question rather than reaching for the allowlist: an entry is only correct if the kernel can decline capture, otherwise the sync must be guarded with CudaRuntime::is_capturing and an entry would silence a real capture-graph hazard. Both halves check out, read from production source:

kernel declaration line
non_max_suppression.rs CaptureSupport::unsupported("NonMaxSuppression performs one 8-byte count D2H synchronization before dynamic ORT output allocation") 566
unique.rs CaptureSupport::unsupported("Unique performs an 8-byte count D2H synchronization before dynamic ORT output allocation") 584

Both are outside capture, so both take the reviewed entry. NMS is the half #2149 left open — its author did not have that file in their tree — and the answer is the same as Unique's.

One detail I did not want to lose, because it is the sort of thing that makes an entry unreviewable later: the sync being listed is not the sync the declaration names. The declaration names the 8-byte count D2H in the counting phase; the listed one is the trailing drain after the materialization launch, before the governed workspace is returned. Both entries say so. That matches how dft.rs and multi_head_attention.rs are written — they explain the specific barrier rather than pointing at the declaration.

How it was verified without a CUDA toolchain

The crate does not build here, but capture_sync_contract.rs is pure std — it scans source text. So I ran the contract's own code, verbatim, with a one-line redirection of kernel_root() to the real kernel directory (diff against the original is exactly that line):

$ KERNEL_ROOT=.../src/kernels cargo test
test every_allowlisted_file_can_decline_capture ... ok
test unconditional_syncs_are_limited_to_capture_unsupported_paths ... ok

That is a green, so on its own it is worth nothing. Two mutations make it mean something:

M1 — the harness reproduces the CI failure, byte for byte. Same file at origin/main (i.e. without this patch), same kernel tree:

left:  {..., "nary.rs::run", "non_max_suppression.rs::materialize", "nonzero.rs::execute", ...
        ..., "sparse_kv_gather.rs::execute", "unique.rs::materialize", "varlen_attention.rs::execute"}
right: {..., "nary.rs::run", "nonzero.rs::execute", ...
        ..., "sparse_kv_gather.rs::execute", "varlen_attention.rs::execute"}

Identical to the set diff in the CUDA compile (Linux x86_64) log on #2151. So the harness is measuring the same thing the lane measures, and this patch is what flips it.

M2 — the premise check is live, not vacuous. Adding softmax.rs::run (a file with no CaptureSupport::unsupported) to the allowlist:

every_allowlisted_file_can_decline_capture ... FAILED
softmax.rs is on the unconditional-sync allowlist for run, but no capture_support in it
returns CaptureSupport::unsupported

So if I had made the wrong call on either kernel, the second test would have caught it. cargo fmt --check clean.

What this does not do

Refs #2149.

…viewed

`unconditional_syncs_are_limited_to_capture_unsupported_paths` has been red on
main since #2113 (Unique) and #2130 (NMS) each added an unguarded
`.synchronize()` without an allowlist entry. Either commit reds the lane on its
own; both are in main, so every open PR inherits it.

The allowlist is a review decision, so the question is which repair each needs.
Both kernels declare `CaptureSupport::unsupported` in production source --
`non_max_suppression.rs:566` and `unique.rs:584`, both naming the 8-byte count
D2H that precedes dynamic ORT output allocation -- so both are outside capture
and the reviewed entry is the correct repair. Neither needs a
`CudaRuntime::is_capturing` guard, which would have been the answer had either
been capture-supported. The kernels are untouched.

The listed sync is not the one the declaration names: it is the trailing drain
after the materialization launch, before the governed workspace is returned.
That is recorded in each entry's rationale rather than citing the declaration
alone, matching the `dft.rs` and `multi_head_attention.rs` entries.

Verified by running the contract's own source over the real kernel tree, with
only `kernel_root()` redirected (the CUDA crate does not build without a
toolchain here, and the scanner is pure std). Before the change it reproduces
CI's failure with a byte-identical left/right set diff; after, both tests pass.
Mutation: allowlisting a capture-supported file (`softmax.rs::run`) fails
`every_allowlisted_file_can_decline_capture` with its own message, so the
premise check is live rather than vacuous.

Refs #2149.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov

codecov Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.58%. Comparing base (edc42d2) to head (49c523e).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #2157   +/-   ##
=======================================
  Coverage   80.57%   80.58%           
=======================================
  Files         432      432           
  Lines      218152   218156    +4     
  Branches   218152   218156    +4     
=======================================
+ Hits       175771   175795   +24     
+ Misses      36537    36520   -17     
+ Partials     5844     5841    -3     
Flag Coverage Δ
cli-ort-linux 72.51% <ø> (?)
cli-ort-windows 72.01% <ø> (ø)
mlas 85.80% <ø> (-0.10%) ⬇️
offline 80.70% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 6 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 added a commit that referenced this pull request Aug 25, 2026
…ident (#2161)

## What

`.roy-scratch/pr.md` reached `main` in #2150 (`b054ff63e`, 22:20:23Z).
This removes it, and records the incident in the two places that keep
the log.

`Root file allowlist` has been **red on `main` and on every PR opened
since** — it inherits, so it is currently red on #2157 and on anything
else branched after 22:20Z. Fixing it is not a judgement on #2150; the
check fired correctly and named the exact file and remedy.

## Why it is worth a PR rather than a `git rm`

The file is a `--body-file` staging artifact — and **not the body that
shipped**. It is the superseded first draft:

| | claim about the reserve half |
|---|---|
| committed `.roy-scratch/pr.md` | "The reserve half **is not** the
identity, and it bites at exactly one width." |
| #2150's merged body | "The reserve half **is** the identity on that
mask too. **This is a correction to the first version of this PR**…" |

So `main` currently carries a tracked, greppable, root-level copy of a
technical claim its own author retracted, with nothing marking it stale.
That is worse than ordinary scratch: ordinary scratch is merely noise.

## The three changes

1. **Remove the file.**
2. **`.gitignore`: `/.*-scratch/`** — root-anchored, per the `/*.log`
convention. The comment labels this as *the guessing half* of the
repair, because that is what the record shows it is: five artifacts of
this class have now reached `main` (`.msg.txt`, `.commitmsg`,
`.pris_v4.log`, `.body.md`, `.roy-scratch/pr.md`) and **each was named
outside every pattern written for the previous one** — `.body.md` landed
hours after two separate fixes to that very block, and the name it
should have had (`.pr-body.md`) was already ignored.
3. **Record the incident** in `.github/root-file-allowlist.txt`'s header
*and* the `diff-guard.yml` comment, and bump both counts: seven entries
/ six incidents → **eight / seven**. Both files state their count in
prose; a count that quietly stops being true is how a log stops being
read. Per #2036's precedent the row carries the introducing and removing
commits and the dwell time.

## One thing this settles rather than asserts

The allowlist header says #2036 shipped a directory-shaped blind spot
**while citing an instance of it in its own header** — "naming an
instance is not the same as testing its shape". `.roy-scratch/` is the
**first directory incident since that gap was closed**, and the check
named it, as a directory, with the right remedy:

```
::error::Root-level entr(ies) not on the allowlist:
::error::  .roy-scratch/   (directory)
```

That is the positive control the earlier fix never got, so it is
recorded next to the claim it settles.

## Verification

The workflow's logic run verbatim against three trees — because "the
check passes now" is worth nothing unless the check still fails on
something:

| tree | expectation | result |
|---|---|---|
| this branch | pass | `Root is exactly the 36 allowlisted entr(ies).` |
| `origin/main` (pre-fix) | report the cause, and only it | `unlisted:
.roy-scratch` |
| this branch + injected `.gaff-probe/x.md` | still discriminating |
`UNLISTED: .gaff-probe`, rc=1 |

And `git check-ignore -v` on the new pattern: `.roy-scratch/pr.md` and
`.pris-scratch/a.md` both ignored at line 90; `nested/.foo-scratch/b.md`
**not** ignored (rc=1), which is the documented root-only scope.

No other path in the tree references `.roy-scratch` (`grep -rn`,
excluding the three files changed here).

## Notes

- @roy — this is your draft; if you would rather keep it, keep it
*somewhere untracked* and say so and I will close this. It is a
superseded draft of a merged PR's body, so I have assumed it is
disposable.
- Base is `7cb5d98df`. Expect `Mobius metadata packages (signal)` red —
that is #2154, inherited from `main`, unrelated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby

Copy link
Copy Markdown
Owner Author

Refreshed onto main (edc42d2cb) by normal merge — 49c523ea6. That clears two inherited reds (Root file allowlist, fixed by #2156/#2161; Mobius metadata packages (signal), green again).

Holding this rather than merging it, and the reason is worth stating.

CUDA compile (Linux x86_64) is red on this PR, but not at the step this PR is about:

Check CUDA execution provider and Python feature   failure   <- #2158, inherited from main
Verify CUDA test inventory and skip honesty        skipped
Audit CUDA kernel capture-sync contract            skipped   <- the step this PR fixes

onnx-runtime-python stopped compiling on main at 66ca26a5d (#2132's merge commit), so the compile step ahead of the audit fails and the assertion this PR repairs never executes. Merging now would give me a green required set and a skipped for the only check that can tell me I am right. That is exactly the "empty is not green" pattern I have been filing about all week, so I am not doing it on my own PR.

Evidence the fix is correct in the meantime, since the lane cannot supply it: I ran capture_sync_contract.rs verbatim as a standalone crate with only kernel_root() redirected (one line, diff-confirmed), because the contract is pure std and reads kernel sources rather than touching a device.

arm expectation result
unpatched reproduce CI's failure left/right set diff byte-identical to the CI log
patched both tests pass pass
patched + softmax.rs::run wrongly allowlisted the decline check fires fires

The third arm is the one that matters: it shows the suite still rejects a bad entry, so the pass in arm 2 is the fix and not a check that stopped discriminating.

Plan: when #2158 lands, re-run this lane, confirm Audit CUDA kernel capture-sync contract goes green rather than skipped, and merge on that. If #2158 stalls I will say so here rather than quietly merging on a skip.

@justinchuby

Copy link
Copy Markdown
Owner Author

Re-validated at the merged head 49c523ea6, because main moved ~20 commits under this branch and a new kernel with an unconditional sync would have made this PR incomplete rather than merely blocked.

Same standalone harness (the contract is pure std — it reads kernel sources — so it runs verbatim outside the CUDA crate with one line changed, kernel_root() → an env var; diff confirms exactly one line differs):

$ CSC_KERNEL_ROOT=.../src/kernels cargo test --offline
test every_allowlisted_file_can_decline_capture ... ok
test unconditional_syncs_are_limited_to_capture_unsupported_paths ... ok

And the control, since a pass means nothing without one — delete "unique.rs::materialize" from the allowlist and re-run against the same kernels:

test unconditional_syncs_are_limited_to_capture_unsupported_paths ... FAILED

So: still exactly the two entries, still discriminating, no new unallowlisted sync arrived with the merge.

Also worth recording here because it decides how long this stays blocked — the two current CUDA reds are the same inherited stack seen at two depths, not two live causes. The distinguishing field is in every job log:

grep -m1 'HEAD is now at' <job log>

A base containing 66ca26a5d (#2132's merge) fails at the compile step with 40 E0277 (#2158) and skips everything behind it. A base older than that reaches the audit step and fails there — which is this PR's target, #2149. #2124 is currently showing the older shape purely because its merge ref predates #2132.

The consequence I care about: as bases roll forward, #2149 stops being visible to anyone, masked behind a compile error in an unrelated crate. It is not fixed by that; it is hidden by it. That is the reason to keep this open and merge it on a green audit step rather than quietly on a skip.

@justinchuby

Copy link
Copy Markdown
Owner Author

Closing this as superseded by #2180, which landed the same two allowlist entries and more.

I held this PR rather than merging it, on a published criterion: its target step, Audit CUDA kernel capture-sync contract, had to be green, not skipped. That criterion is now met — by someone else's fix — so the honest outcome is to close mine rather than reopen the same ground.

#2180 is a superset. It adds non_max_suppression.rs::materialize and unique.rs::materialize with the DeviceWorkspace/dynamic-output rationale, and does two things this PR did not: binds each entry to its owning kernel and two-phase output policy, and exercises a real begin_graph_capture refusal, proving it happens before either kernel phase runs. Mine asserted the rationale in a comment; #2180 asserts it in a test.

The defect was real, for the record. Before closing I ran the exact CI command locally against both versions of the contract file, on identical kernel sources:

arm expect got
this branch's allowlist PASS PASS (2 tests)
control: pre-#2180 main allowlist FAIL FAIL (3 tests, dynamic_output_syncs_are_bound_to_their_capture_refusal)
restored PASS PASS (2 tests)

And the step-8 history is worth recording, because the same job colour meant three different things in three days:

main step 6 (Check CUDA … Python feature) step 8 (Audit … capture-sync)
bc79b3ff1 failure skipped — fail-fast, never ran
2e1746448 failure failure — #2170 added if: !cancelled(), so it finally ran
97353d5a7 failure success — #2180

The job is red in all three rows. Only the step records distinguish "not passing" from "not run" from "fixed". I originally wrote the middle row as skipped on #2173 by citing a stale SHA; corrected there.

Step 6 is still red at main head — that is #2158, and #2173 is what fixes it.

Two instrument errors of mine that this cleanup exposed, both of which briefly produced a confident wrong reading:

  • git diff origin/main...HEAD (three dots) diffs from the merge base, so it reports only what I added and is structurally blind to what main gained since. It showed +11/-0 while main had in fact grown a whole new test my branch lacked. Two-dot for "am I behind".
  • git checkout origin/main -- <file> stages the result, so the plain git diff I used to confirm the revert showed nothing — and I read that as "the file is unchanged", i.e. as the opposite of what had happened.

Same shape both times, and the same shape as the --exact filter that matched no tests and printed ok: the reading I got when the instrument was pointed at the wrong thing was a plausible value, not an error. No merge, no bypass; nothing from this branch reached main.

@justinchuby
justinchuby deleted the squad/gaff-2149-capture-sync-allowlist branch August 27, 2026 05:25
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