Skip to content

Commit fe07955

Browse files
Alok-Joshiclaude
andcommitted
Use pytest-isolated for tests that need a fresh process
Add pytest-isolated to the hatch test environment and use it where a test needs a clean interpreter: - Rewrite test_cuda_init_before_nsight as an isolated test that asserts on the raised ProfilerException instead of matching subprocess stderr. - Make conftest.py skip its tool setup inside the isolated child process, so the tests control import order themselves. Rename the --tool-init mode ncu-lazy to none, since it means that no tool is active up front. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Alok Joshi <aljoshi@nvidia.com>
1 parent 26e191e commit fe07955

4 files changed

Lines changed: 45 additions & 54 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ are skipped without them.
291291

292292
```bash
293293
pip install -e ".[cu13]" # or [cu12]; add ,cupti for the CUPTI backend
294-
pip install pytest torch # pick the torch build matching your CUDA, see pytorch.org
294+
pip install pytest pytest-isolated torch # pick the torch build matching your CUDA, see pytorch.org
295295
hatch run lint:check # black --check, isort --check, mypy --strict (whole repo, incl. examples/tests)
296296
hatch run lint:format # apply black + isort
297297
hatch run docs:build # sphinx-build -W (warnings are errors)

‎conftest.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"ncu-at-import": ("1", None),
1313
# Nothing is active up front, so nsight.analyze.kernel activates NCU on the
1414
# first call to a decorated function.
15-
"ncu-lazy": ("0", None),
15+
"none": ("0", None),
1616
# CUPTI is activated here instead; nsight must not claim NCU at import.
1717
"cupti": ("0", "cupti"),
1818
}
@@ -26,7 +26,7 @@ def pytest_addoption(parser: pytest.Parser) -> None:
2626
help=(
2727
"How the nsight tool is initialized for the suite. 'ncu-at-import' "
2828
"(the default) lets nsight activate NCU while it is imported. "
29-
"'ncu-lazy' leaves no tool active so the lazy NCU load in "
29+
"'none' leaves no tool active so the lazy NCU load in "
3030
"nsight.analyze.kernel is exercised on the first decorated call. "
3131
"'cupti' activates CUPTI before the suite runs. Only one tool can be "
3232
"active per process, so this mode fully determines both "
@@ -36,6 +36,14 @@ def pytest_addoption(parser: pytest.Parser) -> None:
3636

3737

3838
def pytest_configure(config: pytest.Config) -> None:
39+
# pytest-isolated re-runs selected tests in a fresh child pytest process
40+
# with the same arguments. That child must start with nsight unimported and
41+
# no tool active, since the isolated tests control import order themselves
42+
# (e.g. initializing CUDA before importing nsight). The parent's
43+
# NSPY_NCU_INIT_AT_IMPORT is inherited by the child through the environment.
44+
if os.environ.get("PYTEST_RUNNING_IN_SUBPROCESS") == "1":
45+
return
46+
3947
init_at_import, tool_name = _INIT_MODES[config.getoption("--tool-init")]
4048

4149
# Set before nsight is imported: it reads this while importing, which is why

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ format = [
105105
dependencies = [
106106
"pytest",
107107
"pytest-cov",
108+
"pytest-isolated",
108109
]
109110
system-packages = true
110111
[tool.hatch.envs.test.scripts]

‎tests/test_cuda_init_before_nsight.py‎

Lines changed: 33 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,65 +5,47 @@
55
This is used to test the scenario when a user initializes CUDA before importing nsight.
66
NCU attach-mode requires CUDA to not be initialized before the injection library is loaded
77
so profiling fails with an error like "Cuda is initialized before the tool".
8-
"""
98
10-
import subprocess
11-
import sys
12-
from pathlib import Path
9+
The test runs in a fresh subprocess via pytest-isolated, because nsight must not have
10+
been imported yet. The root conftest.py skips its tool setup in that subprocess.
11+
"""
1312

1413
import pytest
1514
import torch
1615

1716

18-
def main() -> None:
19-
20-
torch.cuda.init() # any torch call that initializes CUDA would do
21-
import nsight # this loads NCU, and the test needs it loaded after CUDA init
22-
23-
@nsight.analyze.kernel()
24-
def run_simple_kernel(n: int) -> None:
25-
a = torch.randn(n, n, device="cuda")
26-
b = torch.randn(n, n, device="cuda")
27-
with nsight.annotate("test"):
28-
_ = a @ b
29-
30-
# under NSPY_NCU_INIT_AT_IMPORT=0 the load is deferred to here instead, still after CUDA init
31-
run_simple_kernel(64)
32-
33-
34-
def test() -> None:
35-
"""Run this program as a subprocess; profiling must fail with the expected error."""
36-
# Imported here, not at module scope: importing nsight before torch.cuda.init()
37-
# in main() would defeat what this test exercises.
38-
import nsight
39-
40-
if nsight.get_active_tool() == nsight.Tool.CUPTI:
17+
@pytest.mark.isolated
18+
def test_cuda_init_before_nsight(request: pytest.FixtureRequest) -> None:
19+
"""Profiling must fail with the expected error when CUDA is initialized first."""
20+
if request.config.getoption("--tool-init") == "cupti":
4121
pytest.skip(
4222
"NCU-only test: validates injection failure when CUDA is pre-initialized"
4323
)
4424

45-
this_file = Path(__file__).resolve()
46-
result = subprocess.run(
47-
[sys.executable, str(this_file)],
48-
capture_output=True,
49-
text=True,
50-
timeout=120,
51-
cwd=this_file.parent.parent,
52-
)
53-
54-
# Process should fail with a ProfilerException raised by begin_profiling()
55-
assert result.returncode != 0, "Subprocess unexpectedly succeeded"
56-
57-
stderr_lower = (result.stderr or "").lower()
58-
# Expect the standardized injection error message with the code emitted by ncu injection
59-
# When CUDA is already initialized before injection, we expect code 3 (NV_INJ_ERROR_NOT_INITIALIZED)
60-
assert (
61-
"failed to begin profiling" in stderr_lower
62-
), f"Unexpected stderr: {result.stderr}"
63-
assert (
64-
"(code=3)" in stderr_lower
65-
), f"Expected injection error code 3 in stderr, got: {result.stderr}"
66-
25+
torch.cuda.init() # any torch call that initializes CUDA would do
6726

68-
if __name__ == "__main__":
69-
main()
27+
# Importing nsight (or, with --tool-init=none, the first decorated call) loads NCU,
28+
# and the test needs that to happen after CUDA init.
29+
with pytest.raises(Exception) as exc_info:
30+
import nsight
31+
32+
@nsight.analyze.kernel()
33+
def run_simple_kernel(n: int) -> None:
34+
a = torch.randn(n, n, device="cuda")
35+
b = torch.randn(n, n, device="cuda")
36+
with nsight.annotate("test"):
37+
_ = a @ b
38+
39+
run_simple_kernel(64)
40+
41+
from nsight.exceptions import ProfilerException
42+
43+
assert isinstance(
44+
exc_info.value, ProfilerException
45+
), f"Unexpected exception: {exc_info.value!r}"
46+
message = str(exc_info.value).lower()
47+
# Expect the standardized injection error message with the code emitted by ncu injection.
48+
# When CUDA is already initialized before injection, we expect code 3
49+
# (NV_INJ_ERROR_NOT_INITIALIZED)
50+
assert "failed to begin profiling" in message, f"Unexpected error: {message}"
51+
assert "(code=3)" in message, f"Expected injection error code 3, got: {message}"

0 commit comments

Comments
 (0)