Skip to content

Commit c4c0ffc

Browse files
acollins3claude
andcommitted
Add AGENTS.md with repository guidance for coding agents
Describe the repository layout, the lint/test/docs commands, runtime requirements, and how the profiler runs benchmark code. Document known pitfalls found while profiling Triton kernels with the NCU and CUPTI backends: autotuning inside annotations, kernel-count rules, CUPTI activation order relative to importing torch, mangled kernel names under CUPTI, and how to interpret result columns including normalize_against. Some passages describe the error messages from the acollins/errors-and-terminal-output branch, so this builds on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Alex Collins <acollins@nvidia.com>
1 parent 5163f88 commit c4c0ffc

1 file changed

Lines changed: 276 additions & 0 deletions

File tree

‎AGENTS.md‎

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
# Agent instructions
2+
3+
Nsight Python (`nsight-python` on PyPI, imported as `nsight`) profiles GPU
4+
kernels from Python. You decorate a benchmark function with
5+
`@nsight.analyze.kernel`, mark kernel launches with `nsight.annotate`, and get
6+
back a `ProfileResults` whose `.to_dataframe()` holds aggregated metrics.
7+
Collection is done by NVIDIA Nsight Compute (`ncu`, default) or, for kernel
8+
time only, by the experimental CUPTI backend.
9+
10+
## Repository map
11+
12+
| Path | What it is |
13+
| --- | --- |
14+
| `nsight/analyze.py` | Public decorators: `kernel`, `plot`, `ignore_failures`. Docstrings here are the API reference. |
15+
| `nsight/annotation.py` | `nsight.annotate` (context manager and decorator). |
16+
| `nsight/collection/core.py` | Config handling, the run loop (`run_profile_session`), `ProfileResults`. |
17+
| `nsight/collection/ncu.py` | NCU injection loading, `ncu --mode attach` launch, version checks. |
18+
| `nsight/collection/cupti.py` | CUPTI activity-based timing backend. |
19+
| `nsight/extraction.py` | Report to raw DataFrame: kernel-per-annotation checks, `derive_metric`. |
20+
| `nsight/transformation.py` | Aggregation into `AvgValue`/`StdDev`/CI columns, `normalize_against`, `Geomean`. |
21+
| `nsight/visualization.py` | Matplotlib plotting used by `@nsight.analyze.plot`. |
22+
| `nsight/info_collector.py`, `nsight/experimental.py` | Custom info collectors (`@nsight.experimental.collect`). |
23+
| `nsight/thermovision.py` | GPU thermal-headroom throttling guard. |
24+
| `nsight/tools_manager.py` | `nsight.activate` / `deactivate` / `get_active_tool`, `nsight.Tool`. |
25+
| `examples/` | Numbered runnable examples, indexed in `examples/README.md`, run by `examples/test_examples.py`. |
26+
| `tests/` | Pytest suite. Most tests need a GPU and `ncu`. |
27+
| `conftest.py` | Adds `--tool-init {ncu-at-import,ncu-lazy,cupti}`. |
28+
| `docs/source/` | Sphinx docs. Release notes in `docs/source/release_notes/`. |
29+
30+
Read the docstring of `nsight.analyze.kernel` before using or changing the
31+
API. Do not invent parameters, result columns, metric names, or units; check
32+
them against the code, or against `ncu --query-metrics` for metric names.
33+
34+
## Commands
35+
36+
All of these are defined in `pyproject.toml` and `.github/workflows/ci.yaml`.
37+
38+
```bash
39+
pip install -e ".[cu13]" # or [cu12]; add ,cupti for the CUPTI backend
40+
hatch run lint:check # black --check, isort --check, mypy --strict (whole repo, incl. examples/tests)
41+
hatch run lint:format # apply black + isort
42+
hatch run docs:build # sphinx-build -W (warnings are errors)
43+
pytest tests -v # unit/integration tests (GPU + ncu)
44+
pytest examples -v # runs every example's main()
45+
pytest -v --tool-init=cupti # same suites on the CUPTI backend
46+
hatch run test_cu13:run # what CI runs (cu13 + cupti extras)
47+
```
48+
49+
- `mypy --strict` covers `examples/` and `tests/` too. Annotate every
50+
function. Use the existing `# type: ignore[untyped-decorator]` on
51+
`@triton.jit` and `@pytest.mark.*` decorators.
52+
- `scripts/git-hooks/pre-commit` runs `hatch run lint:check`.
53+
- Commits need DCO sign-off (`git commit -s`). See `Contributing.md` for the
54+
title format.
55+
56+
## Runtime requirements and environment checks
57+
58+
- NVIDIA Nsight Compute **2026.2.1 or newer** must be the `ncu` on `PATH`
59+
(`MIN_NCU_VERSION` in `nsight/collection/ncu.py`). `ncu-report` must not be
60+
older than `ncu`. Many machines have an older `ncu` from a CUDA toolkit
61+
first on `PATH`. Check `ncu --version` before running anything.
62+
- An older `ncu`, or a missing one, does not fail at `import nsight`. The
63+
error is stored and raised on the first call to a decorated function.
64+
- Non-admin profiling needs GPU performance counters unrestricted
65+
(`RmProfilingAdminOnly: 0` in `/proc/driver/nvidia/params`). Do not change
66+
driver settings, add capabilities, or use `sudo` to get around this. Report
67+
it instead.
68+
- Headless runs: set `MPLBACKEND=Agg` when using `@nsight.analyze.plot`.
69+
- `UserWarning: Nsight Python Thermovision is not supported on this machine`
70+
is harmless. Thermal throttling protection is simply off on that GPU.
71+
72+
## How the profiler runs your code
73+
74+
These facts decide how to write benchmark functions correctly:
75+
76+
1. **NCU must attach before CUDA is initialized.** `import nsight` activates
77+
NCU by default (`NSPY_NCU_INIT_AT_IMPORT=1`), so import `nsight` before
78+
anything that touches CUDA. Only one tool can be active per process, and
79+
NCU can never be deactivated.
80+
2. **The decorated function is called `len(configs) * runs` times.** Its
81+
return value is discarded (a `RuntimeWarning` is emitted if it returns
82+
something). The call returns `ProfileResults`. Keep anything whose result
83+
you need, such as correctness checks, in an undecorated function.
84+
3. **Only kernels inside `nsight.annotate(...)` are measured.** Setup such as
85+
`torch.randn` outside annotations is not profiled. Annotation names must be
86+
unique per run, and annotations cannot be nested.
87+
4. **One kernel per annotation by default.** More than one raises
88+
`RuntimeError: More than one (total=N) kernel is launched...`. Options are
89+
`combine_kernel_metrics=lambda a, b: a + b`, `ignore_kernel_list=[...]`, or
90+
`replay_mode="range"`. Every annotation must launch the **same number of
91+
kernels in every run and config**. The check is only
92+
`total % (configs * runs) == 0`, so varying counts can pass and give wrong
93+
numbers.
94+
5. **NCU replays each kernel** with caches flushed (`cache_control="all"`)
95+
and measures it in isolation. `gpu__time_duration.sum` is per-kernel GPU
96+
time in ns. It does not include launch overhead or host time, and it is
97+
not end-to-end wall-clock.
98+
6. Output files go to a fresh `$TMPDIR/nspy_*` directory unless
99+
`output_prefix` is set. `output_csv=True` writes `profiled_data-*.csv`
100+
(raw, one row per run) and `processed_data-*.csv` (same as
101+
`to_dataframe()`).
102+
103+
### Triton-specific pitfalls
104+
105+
- **Autotuning inside an annotation breaks profiling.** `@triton.autotune`
106+
benchmarks every config on first call, launching dozens of kernels. In a
107+
test with a 4-config autotuner this produced 58 kernels over 2 runs in one
108+
annotation, which raises `RuntimeError: More than one ... kernel is launched`.
109+
Before the decorated call, call the kernel once for **every shape** in
110+
`configs` so the autotune cache is warm. Don't use the error's other
111+
suggestions (`combine_kernel_metrics`, range replay) for this case.
112+
- JIT compilation on first call launches no kernels and is safe inside an
113+
annotation, but warming up outside keeps runs uniform.
114+
- Kernel names in results are the `@triton.jit` function name, for example
115+
`softmax_kernel`. Different `num_warps`/block variants of one kernel share a
116+
name, so name annotations by variant (`f"triton_bm{bm}_w{w}"`).
117+
- A Triton compile error inside an annotation propagates unchanged. With
118+
`nsight.annotate(name, ignore_failures=True)`, the row gets `AvgValue=NaN`,
119+
`NumRuns=0` and `Kernel="dummy_kernel_failure"`. The ignored exception is
120+
reported only as a `RuntimeWarning` ("Ignored failure in annotation ..."),
121+
so capture warnings if you need to know why a candidate failed.
122+
123+
### CUPTI backend (experimental, Linux only)
124+
125+
Opt in with `NSPY_NCU_INIT_AT_IMPORT=0` before `import nsight`, then call
126+
`nsight.activate(nsight.Tool.CUPTI)`. It has lower overhead than NCU for
127+
timing-only loops (no replay, no `ncu` subprocess), but:
128+
129+
- It supports only `gpu__time_duration.sum`. There is no replay, no cache
130+
flush and no clock control.
131+
- **Activate CUPTI before importing torch.** Observed with nsight-python
132+
1.1.0, cupti-python 13.4 and PyTorch 2.12: if `torch` is imported before
133+
`nsight.activate(nsight.Tool.CUPTI)` *and* a CUDA context is created before
134+
the first decorated call, no driver launch records arrive. Any torch op,
135+
tensor allocation, Triton launch or `torch.cuda.init()` creates a context.
136+
Every profiled call in that process then raises `RuntimeError: No kernels
137+
were profiled`, even though the annotated code did launch kernels. Set the
138+
environment variable and activate CUPTI before `import torch`. Then warm-up
139+
and correctness checks can run anywhere. Importing Triton or `cuda.core`
140+
first is fine.
141+
- Kernel names in the `Kernel` column are **mangled** under CUPTI (for
142+
example `_ZN2at6native29vectorized_elementwise_kernel...`) but demangled
143+
under NCU (`vectorized_elementwise_kernel`). `ignore_kernel_list` matches
144+
exact names, so write it for the backend in use.
145+
- Without cache flushing, short kernels can vary a lot between runs: about 32%
146+
relative stddev was seen for a 10 µs kernel. Check `RelativeStdDevPct`.
147+
148+
## Reading results
149+
150+
`ProfileResults.to_dataframe()` returns one row per
151+
(annotation, config, metric). Inspect `df.columns` rather than assuming them.
152+
They currently include:
153+
154+
- `Annotation`, one column per decorated-function parameter, and `Metric`
155+
(the raw metric name or `derive_metric` names).
156+
- `AvgValue`, `StdDev`, `MinValue`, `MaxValue`, `NumRuns`, `CI95_Lower`,
157+
`CI95_Upper`, `RelativeStdDevPct`, and `StableMeasurement` (nullable; True
158+
when the relative stddev is under 2%).
159+
- `Unit`, `Kernel` (first kernel name for the group), `GPU`, `Host`,
160+
`ComputeClock`, `MemoryClock`, `Normalized`, `Geomean` (geomean of
161+
`AvgValue` across configs per annotation and metric). `NormalizationValue`
162+
is added only with `normalize_against`.
163+
164+
Interpretation rules:
165+
166+
- **`NaN` means failed or unavailable, not zero.** `NumRuns` counts valid
167+
samples only.
168+
- **With `normalize_against`, only `AvgValue` is normalized.** `Unit` still
169+
shows the original unit, for example `ns`, and `StdDev`, `Min`/`Max` and
170+
`CI95_*` stay in raw units. For comparisons, prefer computing ratios
171+
yourself from un-normalized data. If you do use it, compute speedup as
172+
`1 / AvgValue`.
173+
- `derive_metric(m1, ..., mk, *config_args)` receives metrics in the order
174+
given in `metrics=`, followed by the function's parameters. Return
175+
`(value, unit)` or `{name: (value, unit)}`. Bare scalars get `Unit=NaN` and
176+
a warning.
177+
- With `runs=1`, `StdDev` and `CI95_*` are `NaN`. Use `runs >= 3` before
178+
claiming a difference, and compare against `RelativeStdDevPct`.
179+
- Throughput metrics (`*.pct_of_peak_sustained_elapsed`, unit `%`) tell you
180+
whether a kernel is memory- or compute-bound. Collect them alongside time
181+
rather than inferring a bottleneck from time alone.
182+
- Do not read plot PNGs to make decisions. Everything `@nsight.analyze.plot`
183+
shows comes from the same DataFrame.
184+
185+
## Writing benchmarks and examples
186+
187+
Use this shape. Validate first, then profile with a small, bounded
188+
configuration set:
189+
190+
```python
191+
import torch # importing torch does not initialize CUDA
192+
193+
import nsight # must be imported before the first CUDA call
194+
195+
SHAPES = [(4096, 1024), (4096, 4096)]
196+
197+
198+
def check(candidate, ref) -> None: # outside profiling
199+
torch.testing.assert_close(candidate, ref, atol=..., rtol=...)
200+
201+
202+
@nsight.analyze.kernel(
203+
configs=SHAPES,
204+
runs=3,
205+
metrics=[
206+
"gpu__time_duration.sum",
207+
"dram__throughput.avg.pct_of_peak_sustained_elapsed",
208+
],
209+
)
210+
def bench(rows: int, cols: int) -> None:
211+
x = torch.randn(rows, cols, device="cuda")
212+
with nsight.annotate("baseline"):
213+
torch.softmax(x, dim=1)
214+
with nsight.annotate("candidate"):
215+
my_softmax(x)
216+
```
217+
218+
- Validate every candidate with a dtype-appropriate tolerance **before**
219+
accepting its timing. Report incorrect candidates separately from profiling
220+
failures. Faster but wrong is not an improvement.
221+
- Each extra NCU metric can add replay passes. A sweep's cost is roughly
222+
configs × runs × annotations × passes. Start with one config and
223+
`runs=1` as a smoke test.
224+
- Check metric names first with `ncu --query-metrics --chip <chip>` (for
225+
example `ga102`, `gh100`). An invalid name only raises
226+
`ProfilerException: Invalid value ...` **after** the whole workload has run.
227+
- If no annotation launches a kernel, NCU writes no report and you get
228+
`ProfilerException: No NVIDIA Nsight Compute report found`. Check that
229+
every annotation really launches a kernel (views, no-ops and cached results
230+
do not).
231+
232+
When adding an example to `examples/`:
233+
234+
- Use the next free number, `NN_snake_case.py`. Include the SPDX header,
235+
a module docstring with the concepts it introduces, and a typed `main()`
236+
under `if __name__ == "__main__":`.
237+
- Add a `test_NN_*` that imports it and calls `main()` in
238+
`examples/test_examples.py`. Use `pytest.importorskip` for optional
239+
frameworks (Triton, cuTile), and the `_skip_cupti` marker if it needs NCU
240+
metrics.
241+
- Add it to `examples/README.md`, including any extra install step.
242+
- Keep default workloads small. `pytest examples` runs all of them in CI on
243+
H100 and L4.
244+
245+
## Changing library code
246+
247+
- Public behavior changes need a docstring update in `nsight/analyze.py` (the
248+
docs are autodoc-generated), relevant `docs/source/*.rst` pages, and a
249+
release-note entry under `docs/source/release_notes/topics/`.
250+
- Add tests in `tests/`. Use `nsight.get_active_tool() == nsight.Tool.CUPTI`
251+
with a `skipif` for NCU-only behavior, as `tests/test_profiler.py` does.
252+
Do not weaken assertions to make a test pass.
253+
- New result columns or output formats must be documented in both the
254+
`kernel` docstring (CSV section) and `ProfileResults.to_dataframe`.
255+
- Don't add runtime dependencies without justification. The core deps are
256+
listed in `pyproject.toml`.
257+
258+
## GPU execution rules
259+
260+
- Reading and editing code does not need a GPU. Before running profiling,
261+
check `nvidia-smi`, `ncu --version` (2026.2.1 or newer) and counter
262+
permissions.
263+
- Assume the GPU is shared. Do not kill other processes, lock clocks with
264+
`nvidia-smi`, change power limits, or start background profiling jobs. Use
265+
`clock_control="base"` (via ncu) only if asked.
266+
- Run with a timeout (`timeout 600 python ...`). Smoke-test with one config
267+
before a sweep.
268+
- If the GPU or `ncu` is unavailable, run lint and any CPU-only checks, then
269+
state exactly what was not verified. Never report a GPU test as passing
270+
without having run it.
271+
272+
## Completion report
273+
274+
Summarize what changed and why, which commands you ran and their outcome,
275+
whether GPU profiling actually ran (and on which GPU and `ncu` version), and
276+
anything not verified.

0 commit comments

Comments
 (0)