Nsight Compute Kernel Profiling: From Bottleneck Detection to Kernel Tuning
Note: This is a review article (no first-hand experimental data), based on public documentation and common tuning practices.
Experiment Objective
Earlier articles built cluster-level observability (ch02 DCGM) and single-card bandwidth baselines (ch01), but the root of slow training usually lives in one or two operator kernels. The goal: upgrade profiling from "guess by watching nvidia-smi" to "locate the kernel bottleneck with Nsight Compute" — master the three-phase profiling method (which kernel is slow → what limits it → warp-state details), Speed of Light, and the four bottleneck classes (throughput/latency/occupancy/tail effects), plus the tuning action for each. After reading, you can answer "why is this kernel slow" with evidence.
Code Walkthrough
Part 1: Three-phase profiling commands
# Phase 1: find "which kernel costs the most time" (Nsight Systems, low overhead)
nsys profile -o train_profile --stats=true \
python train.py --batch 32
# Phase 2: full profile of the target kernel (Nsight Compute, heavy; filter only)
ncu --kernel-name regex:matmul \
--launch-count 3 --launch-skip 10 \
-o kernel_prof python train.py --batch 32
# Phase 3: Speed of Light overview and bottleneck judgment
ncu --section SpeedOfLight \
--section Occupancy \
--section WarpStateStats \
-o sol_prof python train.py --batch 32Key points:
- Nsight Systems is the "map"; Nsight Compute is the "microscope":
nsysoverhead is tiny — run it fully to find the most expensive kernels;ncuprofiling is heavy (possibly 10× slower), so use--kernel-nameto filter to target kernels only, never profile everything. --launch-skip 10 / --launch-count 3: GPU kernel launches have warmup effects (first allocations, compile cache); skip the first 10 launches and profile 3 steady-state ones.- The three sections are the judgment core:
SpeedOfLight(theoretical vs measured throughput),Occupancy(SM occupancy),WarpStateStats(what warps are waiting for — memory, sync, or nothing to do).
Part 2: Scripted bottleneck classification (simplified)
def classify_bottleneck(sol):
mem_util = sol['Memory Throughput %']
compute_util = sol['Compute (SM) Throughput %']
achieved_occ = sol['Achieved Occupancy']
warp_stall = sol['Stall Long Scoreboard']
if mem_util > 85 and compute_util < 60:
return 'MEMORY_BOUND', 'reduce memory traffic: vectorize, reuse shared memory, float4 loads'
if compute_util > 85 and mem_util < 60:
return 'COMPUTE_BOUND', 'reduce compute: lookup tables, approximations, better algorithms'
if achieved_occ < 0.5:
return 'LATENCY_BOUND', 'raise occupancy: fewer registers/shared memory, more parallelism'
if warp_stall > 0.5:
return 'LATENCY_BOUND(stall)', 'hide memory latency: more in-flight requests, prefetch'
return 'BALANCED', 'no obvious bottleneck; check tail effects and scheduling overhead'Principles
Speed of Light and the four bottleneck classes
The Speed of Light report compares measured kernel throughput against hardware theoretical peaks:
- Memory Throughput %: actual SM memory traffic / theoretical bandwidth ceiling. Near 90%+ means the kernel is "memory-fed full" — adding compute does nothing; reduce memory traffic.
- Compute (SM) Throughput %: instruction throughput / theoretical compute ceiling. Near the ceiling means "compute-fed full" — reduce compute or change algorithms.
- Achieved Occupancy: actual occupancy = active warps / theoretical max. Low occupancy (<50%) means too little parallelism on the SM for latency hiding.
- Warp State: distribution of warp stall reasons —
Long Scoreboard(waiting on memory),Barrier(waiting on sync),Wait(dependency). The "why" of a bottleneck only shows up here: two kernels both at 80% throughput, one stuck on memory waits and the other on barrier sync — completely different tuning directions.
Three typical bottlenecks and actions
| Bottleneck | Signature | Tuning action |
|---|---|---|
| Memory-bound | Mem% high + Compute% low | vectorize (float4), shared-memory tiling reuse, coalesced access, L2 hit optimization |
| Compute-bound | Compute% high + Mem% low | fewer instructions: approximations/lookup, remove redundant work, algorithmic optimization |
| Latency/occupancy | Occupancy < 50%, high stalls | fewer registers, less dynamic shared memory, more independent parallelism (ILP) |
Key insight: kernel tuning is "qualify first, quantify second" — judge the bottleneck class first (qualitative), then act within that class (quantitative). Skipping the qualification and tuning parameters directly often fine-tunes in the wrong direction.
Division of labor with cluster monitoring
Ch02 DCGM is "what all cards in the cluster are doing"; this article's Nsight is "why this kernel on this card is slow." Practical chain: DCGM detects abnormal training throughput on a node → locate the low-utilization operator → Nsight profiles that kernel → fix and regression-compare. A single-operator optimization often yields 20%-50%, but only after monitoring "pulls it out" does profiling have a target.
Results
(Estimated, not measured; typical optimization examples)
| Tuning action | Typical gain | Applicable bottleneck |
|---|---|---|
| float4 vectorized loads | 1.5-2× | memory-bound |
| shared-memory tiling | 1.3-1.8× | memory-bound (repeated global reads) |
| fewer registers to raise occupancy | 1.2-1.5× | latency/occupancy |
| remove redundant instructions / algorithm swap | 1.3-2× | compute-bound |
Engineering Takeaways
- Map first, microscope second: always use nsys to find "the kernel worth profiling" before ncu. Direct full ncu profiling is the worst time-to-output ratio.
- Read the three sections together, not one metric: high Mem% is not automatically a memory bottleneck — check Occupancy and Warp State too; judge jointly to avoid misdiagnosis.
- Put profiling in the delivery flow: performance-sensitive kernels should keep ncu reports as baselines and compare on regression — "this version did not regress" needs evidence, not feeling.
- Optimize the biggest head first: sort profile results by time and chew the largest kernel first; small-kernel gains contribute little to total time.
Next article preview: profiling found "where the kernel is slow"; the next layer is "why the kernel is written this way" — "PTX and SASS: Intermediate Representations and Tuning Methods for GPU Kernels."