Skip to content

coverage: capture what the Windows coverage crash actually is - #2426

Draft
rluo8 wants to merge 9 commits into
NVIDIA:mainfrom
rluo8:ci/coverage-crash-diagnostics
Draft

coverage: capture what the Windows coverage crash actually is#2426
rluo8 wants to merge 9 commits into
NVIDIA:mainfrom
rluo8:ci/coverage-crash-diagnostics

Conversation

@rluo8

@rluo8 rluo8 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Description

This is a draft pr for diagnostics. Please do not merge.

The Run cuda.core tests (with 8MB stack) step of CI: Coverage always exited 139. Its log is three lines — the traced command, Segmentation fault, exit 139 — with no pytest header, no collected N items, no test ids and no faulthandler traceback. I couldn't reproduce this failures on my local systems.

Three diagnostics, no behaviour change:

  • PYTHONFAULTHANDLER / PYTHONUNBUFFERED — names the exception and dumps every thread's stack, and unlike pytest's built-in plugin it reaches tests that spawn their own python -c children.
  • --isolate — runs pytest in a child process so a surviving parent reports the raw NTSTATUS instead of the shell's translation of it (Git Bash renders an access violation as Segmentation fault / 139). The child's status is passed through unchanged.
  • pytest_crashlog — line-buffered progress log whose last line survives a native fault, localising it to interpreter startup, conftest import, a module's collection, a test, or shutdown. Opened before pytest.main, since pytest imports the conftests — which pull in cuda.core — before pytest_configure. A no-op unless PYTEST_CRASHLOG is set.

The "Run cuda.core tests (with 8MB stack)" step on the Windows coverage
runner exits 139 on every nightly, and its log holds nothing else: three
lines, no pytest output at all. The step before it runs cuda.bindings
through the same wrapper, the same 8 MB thread and the same coverage
wheel and finishes normally with 479 passed, so the crash is specific to
cuda.core -- and with no "collected N items" line to go on, we cannot
even tell whether it reaches a test.

PYTHONFAULTHANDLER is set job-wide because it costs nothing; the rest is
scoped to the one step that crashes. No behaviour change.

- PYTHONFAULTHANDLER names the exception and dumps every thread's Python
  stack. Unlike pytest's built-in faulthandler plugin, the env var also
  covers tests that spawn their own `python -c` children.
  PYTHONUNBUFFERED keeps the final lines out of the block buffer of a
  non-tty pipe.

- run_pytest_with_stack.py --isolate runs pytest in a child process so a
  surviving parent can report its raw exit status. The shell only shows
  a translation of that status; the parent names the NTSTATUS outright.
  The child's status is passed through unchanged.

- pytest_crashlog records session progress to a line-buffered file, so
  the last line survives a native fault and localises it to interpreter
  startup, conftest import, a named module's collection, a named test,
  or interpreter shutdown. The wrapper opens the log before calling
  pytest.main, because pytest imports the initial conftests -- and for
  cuda_core that pulls in cuda.core -- before pytest_configure runs.
  A no-op unless PYTEST_CRASHLOG is set.

Verified on a Windows GPU runner against the linetrace coverage wheel:
identical test results with and without the change.

Signed-off-by: Rui Luo <ruluo@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the CI/CD CI/CD infrastructure label Jul 27, 2026
@rluo8

rluo8 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Hi @mdboom , could you please help trigger a coverage run with this draft PR ? The Windows coverage run always failed when running cuda.core tests. This is to add some debug logs for triage.
Thanks!

@mdboom

mdboom commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

/ok to test 8291f92

@mdboom

mdboom commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

/ok to test 9ed18a7

@github-actions

Copy link
Copy Markdown

rluo8 added 2 commits July 28, 2026 17:36
TEMPORARY -- not for merge.

The exit-139 crash this branch instruments only reproduces on the
Windows coverage runner, and coverage.yml is reachable only from the
nightly schedule and from workflow_dispatch on the default branch. Every
iteration of the diagnostics has therefore needed a maintainer to sync
the mirror branch by hand.

copy-pr-bot already pushes this PR to pull-request/<n> on the upstream
repo, and a push event reads the workflow from the pushed commit, so the
trigger takes effect from the PR itself without merging. ci.yml uses the
same trigger.

A PR run covers the Windows leg alone -- coverage-vars,
build-wheel-windows, coverage-windows -- roughly 28 minutes, 18 of them
on an A100. The two skipped jobs are skipped for different reasons:
coverage-linux has no bearing on a Windows crash, and combine-and-deploy
would both fail on the missing .coverage.linux and publish a half report
to gh-pages. always() does not stop the latter, so its condition is
gated as well. concurrency cancels superseded runs, which would
otherwise queue another A100 job per push.

The nightly is unchanged: on refs/heads/main both conditions hold and
all five jobs run as before.

Signed-off-by: Rui Luo <ruluo@nvidia.com>
@rluo8
rluo8 marked this pull request as ready for review July 29, 2026 02:34
rluo8 added 3 commits July 29, 2026 14:56
The first round of diagnostics placed the fault: the crash log holds
`open pid=...` and `entering pytest.main` and nothing else, so the
process dies while pytest loads its initial conftests -- before
collection, and far before any test runs. What it cannot say is which
module. Three additions, none of which touch machine state:

PYTHONVERBOSE makes the interpreter announce every import as it begins
one, so the last line written before the process dies names the module
it died in. Its output is stderr, so the session is redirected to a file
rather than left in the step log: a crash takes the tail of a pipe with
it, while each unbuffered write to a file has already reached the OS.

The Windows event log carries an Application Error record for every
process the OS kills, naming the faulting module and the offset within
it, and it does so whether or not crash dumps are configured. That is
the one fact no Python-level log can produce. Reading it needs no
registry key set and nothing restored afterwards, which a LocalDumps
setup would have on a machine shared with other jobs.

PYTHONFAULTHANDLER has so far written nothing at all, which leaves two
very different readings: the handler produced a traceback the dying
process failed to flush, or it never ran because the fault happened
outside any Python frame. run_pytest_with_stack.py now points
faulthandler at its own unbuffered file when PYTEST_FAULTLOG is set, so
still empty there means the handler genuinely had nothing to say.

The three logs upload together as an artifact. No behaviour change: the
step already carries continue-on-error, and every addition is inert on a
run that does not crash.

Signed-off-by: Rui Luo <ruluo@nvidia.com>
The stack from the last round places the fault inside an extension
module's initialisation, two exec_module frames below cuda.core._context,
but does not name it. PYTHONVERBOSE was meant to: it announces every
import, so its last line would be the module. It could not deliver --
its output goes through the interpreter's C-level stdio, which buffers
when redirected to a file, and the child's tail stopped at `import
'colorama'` several hundred lines before the crash.

The crash log does not have that problem. It is line buffered, and both
of its lines survived every run so far, so the same question goes through
that file instead. A meta path finder records each name and returns None,
leaving the real finders to do the work; because the write lands before
the module begins executing, a fault during an extension's init leaves
that module as the last IMPORT line.

sys.unraisablehook goes to the same file. A Cython `cdef ... noexcept`
function cannot propagate an exception, so one raised inside it is
printed and cleared and whatever it was computing keeps its default --
in _resource_handles.pyx, a NULL driver function pointer that faults only
when something later calls through it. Recording unraisables in sequence
with the IMPORT lines puts a swallowed error next to the module that
swallowed it. The previous hook still runs, so stderr is unchanged.

Both are off unless PYTEST_CRASHLOG is set, and tracing starts before
pytest.main because the fault happens before pytest_configure would run.
Import tracing put the fault in cuda.core._event, reached through _stream
from _context, but the smallest thing that reproduces it is still a
pytest session over 3869 tests. These probes strip that down: one fresh
interpreter per module, importing exactly one name, under coverage and
nothing else. The linetrace wheel and the ctracer stay because they are
the two conditions test-wheel-windows.yml lacks while running the same
tests on the same runner without faulting.

The order is leaf-first, so the first line that fails to print PROBE OK
is the smallest unit that reproduces. Importing _event directly also
enters the _context/_stream/_event cimport cycle from the opposite side,
which says whether the direction matters -- worth knowing, since the same
cycle exists on Linux and on our own runner without faulting, so
circularity cannot be the whole story.

They run after cuda.bindings rather than before pathfinder because that
is the machine state the crash is known to occur in. A clean-machine
placement is the follow-up if these come back clean, which would itself
say the earlier suites leave something behind.

COVERAGE_FILE points the probes at their own data file; `coverage run`
would otherwise truncate the one the suites are appending to. Each exit
code is captured rather than left to `set -e`, so one crash does not hide
the modules after it.
@rluo8
rluo8 marked this pull request as draft July 29, 2026 09:29
faulthandler stops exactly where the answer starts. The fault is inside an
extension module's initialiser, so the frames that matter are the ones it
cannot reach, and every report ends `<cannot get C stack on this system>`.

Every ordinary way to see past that is closed on this runner. cdb is not
installed: the SDK is present without the Debugging Tools feature, so
Windows Kits ships the debugger DLLs and none of the executables. procdump
would mean an outbound download on a runner whose agent exists to police
outbound requests. A postmortem debugger under AeDebug never fires, because
this image does not record user-process crashes with WER, which is why the
crash-dumps directory came back empty when that was tried. And there are no
PDBs in any case, since build_hooks.py refuses debuggable builds on Windows.

dbgcore.dll in System32 is always there though, and exports
MiniDumpWriteDump, so the process reports on itself. A vectored handler
writes the exception record, the faulting address resolved to module + RVA,
the native return chain resolved the same way, the module map, and a
minidump, then returns EXCEPTION_CONTINUE_SEARCH so the process still dies
with the exit code CI would have seen. Symbols are absent, but which binary
faulted is the open question and module + RVA answers it.

crash_probe.py drives four runs. A selftest faults on purpose, so a clean
report proves the handler works here and an empty one is never mistaken for
"nothing crashed". A run under coverage reproduces the nightly's own
conditions on the 8 MB stack the pytest wrapper uses. A run with a bare
trace function instead of coverage arms Cython's linetrace the same way, so
a fault there means any tracer will do rather than coverage specifically,
and the last line of its breadcrumb names the .pyx statement that was
executing. env_fingerprint.py records the environment around the import --
driver, CPU, and the loaded-module map -- which is what comparing against a
machine that does not fault requires.

This replaces the per-submodule probes, which measured nothing: importing
cuda.core._resource_handles executes cuda/core/__init__.py on the way in, so
all six walked the same path and all six failed identically.

Verified on two Windows runners that do not fault, where the selftest
produced a resolved address, a 44-frame backtrace and a 300 KB dump, and the
breadcrumb recorded .pyx line numbers through to the end of the import.
@rluo8
rluo8 marked this pull request as ready for review July 30, 2026 08:34
@rluo8
rluo8 marked this pull request as draft July 30, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI/CD CI/CD infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants