Skip to content

Add NVIDIA nsys GPU capture for adhoc flamegraphs - #77

Open
prashantbytesyntax wants to merge 11 commits into
masterfrom
gpu-nsys-adhoc
Open

Add NVIDIA nsys GPU capture for adhoc flamegraphs#77
prashantbytesyntax wants to merge 11 commits into
masterfrom
gpu-nsys-adhoc

Conversation

@prashantbytesyntax

@prashantbytesyntax prashantbytesyntax commented Aug 9, 2026

Copy link
Copy Markdown

What

Capture NVIDIA CUDA kernel activity via host-installed Nsight Systems (nsys) and upload the result through the existing adhoc path, so GPU profiles appear in Performance Studio's Adhoc Profiling view next to CPU profiles. Two views from the same capture: a GPU flamegraph (default) and an opt-in CPU/GPU timeline with optional click-for-stack backtraces.

This is the agent-side half; the Studio UI + sandbox demo are in a companion PR on gprofiler-performance-studio (#86).

Changes

  • gprofiler/nsys_profiler.py — host-detect nsys (PATH / NSYS_PATH / common install dirs), run a capture, export cuda_gpu_kern_sum (fallback cuda_api_sum), convert the stats CSV to collapsed stacks, and render flamegraph HTML (template when the full agent runtime is present, a simple self-contained fallback otherwise). Also houses the timeline exports/rendering described below. nsys is detected, never bundled (large and NVIDIA-licensed) — mirrors the PerfSpect approach.
  • gprofiler/main.py--enable-nsys / --nsys-path / --nsys-workload / --nsys-timeline / --nsys-timeline-stacks CLI args; run the GPU capture on a background thread in parallel with CPU profilers and prefer the GPU HTML for the adhoc upload when a capture succeeds.
  • gprofiler/dynamic_profiling_management/__init__.py — honor enable_nsys / nsys_path / nsys_workload / nsys_timeline / nsys_timeline_stacks from combined_config (PerfSpect-shaped control plane) and tag perf_events with nsys-cuda (plus nsys-timeline / nsys-stacks when those modes are active) so the Adhoc UI can show a GPU / nsys chip and distinguish the view.
  • scripts/prepare_centos.sh — add sqlite-devel so the executable build's Python has _sqlite3 (the timeline-stacks SQLite export needs it; the agent degrades gracefully — timeline without backtraces plus a warning — if it's still missing at runtime).
  • tests/test_nsys_profiler.py — unit coverage for CSV→collapsed, nsys discovery, timeline trace parsing/rendering, and callchain extraction. No GPU/nsys required.
  • docs/NSYS_GPU_PROFILING.md — enabling, packaging, sandbox topology.

Timeline view (opt-in)

--nsys-timeline (CLI) or combined_config.nsys_timeline (heartbeat) exports cuda_gpu_trace + cuda_api_trace from the same capture and uploads a self-contained canvas timeline HTML instead of the flamegraph:

  • Swim lanes per CPU thread (CUDA API calls) and GPU device/stream (kernels/memcpy); clicking an event highlights its correlation ID, linking each cudaLaunchKernel to the kernel it launched.
  • Navigable at scale: opens auto-zoomed to a readable window, an overview minimap strip shows full-capture CPU/GPU density with a draggable viewport box, keyboard nav (arrows pan, +/- zoom, n/p step through events, 0 = full span). At low zoom, sub-pixel events shade lanes by occupancy instead of collapsing into solid bars.
  • Falls back to the flamegraph when the trace export yields no events; captures over 20k events keep the longest ones to bound upload size.

--nsys-timeline-stacks / combined_config.nsys_timeline_stacks additionally captures with --cudabacktrace=kernel -s process-tree -b dwarf (heavier, so opt-in), exports the report to SQLite, and joins RUNTIME.callchainId → CUDA_CALLCHAINS → StringIds (stdlib sqlite3, no new deps) into a deduped stacks table:

  • Clicking an event opens a panel with the launching CPU backtrace; GPU kernels resolve their launch stack through the correlation ID. Stacks are native frames (e.g. libtorch/ATen), not Python lines.
  • An aggregate launch-stack flamegraph renders below the timeline (frame width = summed event duration per call path, preferring GPU kernel time; click to zoom subtrees), answering "which call paths cost the most GPU time overall".

nsys vs iaprof

The implemented backend is NVIDIA nsys. There is no iaprof code in this PR — Intel iaprof (Xe/Battlemage EU-stall GPU flamegraphs) doesn't run on NVIDIA hardware and is documented as a possible later Phase-5 backend behind the same GPU-profiler control, not something integrated here.

Testing

  • python3 -m pytest tests/test_nsys_profiler.py27 passed (run with --noconftest in a no-Docker env; the repo conftest.py imports the docker module).
  • Offline CSV→collapsed→HTML path verified by hand on the sample kernel CSV.
  • Timeline HTML interactions (zoom/pan, minimap jump, keyboard nav, CorrID highlight, stack panel, flamegraph zoom/reset, occupancy shading) verified in headless Chromium against synthetic and real captures.
  • End-to-end on an A10G GPU host: timeline capture (3850 events) and timeline+stacks capture (5532 events, kernel clicks resolving full launch backtraces through CorrID) both verified through the sandbox upload path. The stacks run is what surfaced the missing _sqlite3 in the PyInstaller bundle, fixed here.

🤖 Generated with Claude Code

prashantbytesyntax and others added 4 commits August 13, 2026 07:15
Capture NVIDIA CUDA kernel activity via host-installed Nsight Systems
(nsys) and upload the resulting flamegraph HTML through the existing
adhoc path, so GPU profiles show up in Performance Studio's Adhoc
Profiling view alongside CPU profiles.

- nsys_profiler.py: host-detect nsys (PATH / NSYS_PATH / common install
  dirs), run a capture, export cuda_gpu_kern_sum (fallback cuda_api_sum),
  convert the stats CSV to collapsed stacks, and render flamegraph HTML
  (template when the full agent runtime is present, simple self-contained
  fallback otherwise). nsys is detected, never bundled.
- main.py: --enable-nsys / --nsys-path / --nsys-workload CLI args; run the
  GPU capture on a background thread in parallel with CPU profilers and
  prefer the GPU HTML for the adhoc upload when a capture succeeds.
- dynamic_profiling_management: honor enable_nsys / nsys_path /
  nsys_workload from combined_config (PerfSpect-shaped control plane) and
  tag perf_events with nsys-cuda so the Adhoc UI can show a GPU/nsys chip.
- tests: unit coverage for CSV->collapsed and nsys discovery (no GPU/nsys
  required).
- docs/NSYS_GPU_PROFILING.md: enabling, packaging, sandbox topology, and
  how this relates to a possible later Intel iaprof backend behind the
  same GPU-profiler control.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The agent runs as a PyInstaller bundle that prepends its own lib dir
(/tmp/_MEIxxxx) to LD_LIBRARY_PATH. A spawned nsys workload inherited that
and picked up the bundle's older libstdc++, so a dynamically-linked target
(e.g. PyTorch) failed to import (CXXABI_1.3.8 not found) and nsys captured
nothing — the adhoc flamegraph fell back to an empty CPU profile.

_workload_env() now builds the child environment from the pre-bundle values:
prefer PyInstaller's saved LD_LIBRARY_PATH_ORIG / LD_PRELOAD_ORIG, and if
absent, strip any _MEI bundle path so the child resolves system libraries.
Statically-linked workloads (e.g. cuda_burn) were unaffected and still are.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Reading the flamegraph: the prefix is a category not a call stack, weights
  are GPU time (CUPTI tracing) not samples or PMU counters, nsys-cuda is a
  capture-type tag, and real ML workloads surface library-dispatched kernels
  (cutlass GEMM with fused epilogues).
- Workload environment: why dynamically-linked workloads failed under the
  PyInstaller bundle's LD_LIBRARY_PATH and how _workload_env() prevents it,
  plus the bare-root symptom to look for.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents the full pipeline with PyTorch as the worked example: control
plane vs GPU agent container topology, the five agent-side steps from
profile_request to the /api/profiles upload, where the flamegraph HTML is
generated vs rendered, and the scope boundary against torch.profiler and
Nsight Compute (with the NVTX emit_nvtx bridge as a possible follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
prashantbytesyntax and others added 7 commits August 14, 2026 06:09
With --nsys-timeline (CLI) or combined_config.nsys_timeline (heartbeat),
export cuda_gpu_trace + cuda_api_trace from the same capture and upload a
self-contained timeline HTML instead of the GPU flamegraph: swim lanes per
CPU thread and GPU device/stream, wheel-zoom/drag-pan, and click-to-highlight
CorrID linking each cudaLaunchKernel to its kernel. Falls back to the
flamegraph when the trace export yields no events; captures over 20k events
keep the longest ones to bound upload size. perf_events gains nsys-timeline
so the UI can distinguish the view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
At full span most captures collapsed into solid bars: every sub-pixel event
was drawn at a 1px minimum, so a fully-packed lane and a half-idle lane
looked identical. Two changes:

- Open auto-zoomed to a window where the median event is a few pixels wide,
  centered mid-capture, with a "Full span" button to reset.
- At low zoom, sub-pixel events accumulate per-pixel occupancy and shade the
  lane by how busy it actually is, instead of tiling opaque 1px bars.

Verified in headless Chromium: a lane busy for half the capture now renders
visibly brighter on the busy half (mean red 224 vs 24) at full span.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--nsys-timeline-stacks / combined_config.nsys_timeline_stacks captures with
--cudabacktrace=kernel -s process-tree -b dwarf (opt-in; heavier than the
default -s none timeline), exports the report to SQLite, and joins
CUPTI_ACTIVITY_KIND_RUNTIME.callchainId -> CUDA_CALLCHAINS -> StringIds into
a deduped stacks table in the timeline HTML. Clicking an event opens a panel
with the launching CPU backtrace; GPU kernels resolve their launch stack
through CorrID. Timelines without callchains render unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GPU-host e2e run showed load_callchains_from_sqlite crashing with
ImportError: the CentOS 7 Python 3.10 used for the executable build is
compiled without sqlite-devel, so the bundle has no _sqlite3 extension.

Install sqlite-devel before building Python so _sqlite3 is available to
PyInstaller, and degrade gracefully (timeline without backtraces plus a
warning) if sqlite3 is still missing at runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With --nsys-timeline-stacks the per-event stack panel answers "who launched
this kernel", but not "which call paths cost the most GPU time overall".
Aggregate all backtraced events into a flamegraph below the timeline: frame
width = summed event duration per call path (GPU kernel time when GPU events
carry stacks, CPU launch time otherwise), outermost frame on top, kernel name
at the leaf. Click a frame to zoom into its subtree, root row to reset.
Rendered only when the capture has backtraces; no new flags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User feedback from the GPU-host e2e: the timeline is hard to navigate once
auto-zoomed (no sense of where you are in the capture) and the CorrID
mechanics were unexplained.

- Overview strip above the lanes: full-capture CPU/GPU density with a
  draggable viewport box, so you can jump anywhere without zooming out.
- Keyboard: arrows pan, +/- zoom, n/p select next/previous event in view
  (driving the CorrID highlight and stack panel), 0 resets to full span.
- The meta line now explains the lanes and correlation IDs in plain terms
  with color swatches instead of assuming nsys vocabulary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--nsys-upload-rep / nsys_upload_rep heartbeat key: after a successful nsys
capture and profile upload, POST the raw .nsys-rep to the server as an
octet-stream (new ProfilerAPIClient.submit_nsys_rep), tagged with the same
start_time + hostname so the server pairs it with the adhoc flamegraph.
Off by default since reps can be hundreds of MB. A failing rep upload or
on_rep callback never breaks HTML generation or the profile upload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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