|
| 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