Add NVIDIA nsys GPU capture for adhoc flamegraphs - #77
Open
prashantbytesyntax wants to merge 11 commits into
Open
Add NVIDIA nsys GPU capture for adhoc flamegraphs#77prashantbytesyntax wants to merge 11 commits into
prashantbytesyntax wants to merge 11 commits into
Conversation
prashantbytesyntax
force-pushed
the
gpu-nsys-adhoc
branch
from
August 9, 2026 21:47
a810f3f to
c5b99a6
Compare
prashantbytesyntax
changed the base branch from
workload-level-profiling-agent
to
master
August 9, 2026 21:47
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
force-pushed
the
gpu-nsys-adhoc
branch
from
August 13, 2026 07:17
825b8ab to
2ebb52f
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-detectnsys(PATH/NSYS_PATH/ common install dirs), run a capture, exportcuda_gpu_kern_sum(fallbackcuda_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.nsysis detected, never bundled (large and NVIDIA-licensed) — mirrors the PerfSpect approach.gprofiler/main.py—--enable-nsys/--nsys-path/--nsys-workload/--nsys-timeline/--nsys-timeline-stacksCLI 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— honorenable_nsys/nsys_path/nsys_workload/nsys_timeline/nsys_timeline_stacksfromcombined_config(PerfSpect-shaped control plane) and tagperf_eventswithnsys-cuda(plusnsys-timeline/nsys-stackswhen those modes are active) so the Adhoc UI can show a GPU / nsys chip and distinguish the view.scripts/prepare_centos.sh— addsqlite-develso 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) orcombined_config.nsys_timeline(heartbeat) exportscuda_gpu_trace+cuda_api_tracefrom the same capture and uploads a self-contained canvas timeline HTML instead of the flamegraph:cudaLaunchKernelto the kernel it launched.+/-zoom,n/pstep through events,0= full span). At low zoom, sub-pixel events shade lanes by occupancy instead of collapsing into solid bars.--nsys-timeline-stacks/combined_config.nsys_timeline_stacksadditionally captures with--cudabacktrace=kernel -s process-tree -b dwarf(heavier, so opt-in), exports the report to SQLite, and joinsRUNTIME.callchainId → CUDA_CALLCHAINS → StringIds(stdlibsqlite3, no new deps) into a deduped stacks table: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.py→ 27 passed (run with--noconftestin a no-Docker env; the repoconftest.pyimports thedockermodule)._sqlite3in the PyInstaller bundle, fixed here.🤖 Generated with Claude Code