What the NVIDIA device trace from profile returns for a cuda submission (tool nsys), what it cannot answer, and what each refusal means.
Instal·la
npx skillscat add spcl/hpcagent-bench/nsys Instal·la des del registre de SkillsCat.
profile (POST /profile) on a cuda submission wraps Nsight Systems (nsys) around the same
measured child score times, on the same build. A GPU has no call stack to sample, so nsys
RECORDS instead: CUPTI hands it one activity record per kernel launch and per memory operation.nsys answers which kernel, how many times and when; tool:"ncu" answers what the device did
inside it (below). Host instruments are the profiling skill; a hip submission is rocprof's.
How it runs
- Body: the
scorebody plus e.g."tool":"nsys","reps":3,"min_percent":0. tooldefaults tonsysforcuda. Any othertoolexcepttool:"ncu"andtool:"opt-report"
is a 400 namingnsys, with nocause. Onlycudareachesnsys: on any other language (OpenMP-offload, OpenACC and
Triton arms included)tool:"nsys"is a 400.repsdefaults tomeasurement.repeat; one warmup rep (measurement.warmup) runs first.min_percent(0-100, else a 400; default 1): kernels below it are dropped and counted inkernels_omitted, ANDdevice_ns,device_ns_per_rep,device_pctandlaunch_countare summed
over the kept kernels only. Send0for complete totals.residencydoes nothing: acudatask is alwaysdevice("host"is coerced).threadsis
ignored.counters:trueis refused withcounters_unsupported.
What the harness does, and what is timed:
- Before every rep, warmup included, it copies each pointer argument to the GPU; after every
measured rep it copies each output back. Both copies are OUTSIDE the timed region. elapsed_nsis the FASTEST measured rep, timed with GPU events around your call plus a device
synchronize. It is not host wall time, and none of the harness's copies are in it.- The trace covers the whole child: setup, warmup and measured reps.
What nsys is told:
--trace=cuda,nvtxand nothing else (osrt,cublas,cudnnadd interception overhead). NVTX
ranges are recorded but no report reads them, so do not bracket phases withnvtxRangePush;
split them into separate kernels instead (seedivide-and-conquer).ranges[]comes back
empty: ranges are not reported on this route yet.--sample=none --cpuctxsw=none: no CPU sampling (which would needperf_event_paranoid<= 2)
and no context-switch trace.- No debug flags: kernel names come from CUPTI, so the traced build is the graded build.
- The recording is re-exported on every request, so the reports are never a previous run's.
What comes back
build_ok:falseplusdetail(compiler log tail): nothing was traced.- Echoed:
build_ok,kernel,language,tool(nsys),trace(cuda,nvtx),reports(the
four below),reps,warmup,symbol,preset,datatype,min_percent,occupancy_note,
andtext(the same data rendered). kernels[]fromcuda_gpu_kern_sum, largesttotal_nsfirst:name,instances,total_ns,mean_ns,min_ns,max_ns,time_pct(nsys columnsName,Instances,Total Time (ns),Avg (ns),Min (ns),Max (ns),Time (%)).memory[]fromcuda_gpu_mem_time_sumjoined per operation tocuda_gpu_mem_size_sum:operation,direction(h2d/d2h/d2d/h2h/memset/other),count,total_ns,mean_ns,total(3 decimals) andunit(fromTotal (MB)).total/unitare null when the
size report has no row for that operation.launches[]fromcuda_gpu_trace, one row per distinct geometry, most-launched first:name,grid(in blocks),block,threads_per_block,warps_per_block= ceil(threads_per_block/
32),blocks(product ofgrid),registers_per_thread(Reg/Trd),shared_memory(StcSMemDymSMem),shared_memory_unit,launches. A column the trace lacks comes back null, never 0.
- Totals:
elapsed_ns(above);device_ns(sum of kepttotal_ns);device_ns_per_rep=device_ns/ (reps + warmup);device_pct= 100 xdevice_ns_per_rep/elapsed_ns;launch_count(sum of keptinstances).
Pick the window
Every row covers the whole child; elapsed_ns covers one call. Only a per-rep figure goes next toelapsed_ns, and the payload has no per-launch timestamps to re-window with.
device_ns_per_repis the mean kept-kernel time per traced rep, set against the fastest call. It
assumes every rep launches the same kernels.device_pctabove ~100 means kernels ran outside the
timed calls (setup, extra work in the warmup rep) or the reps varied.- Runtime compilation your code does (NVRTC, first-call autotune) lands inside the trace. Done once,
it sits in the warmup rep: inmax_nsandmean_ns, never inelapsed_ns. - Setup's weight in
device_ns_per_repfalls as 1 / (reps + warmup): raiserepsto shrink it.
cuda_gpu_kern_sum -- which kernel
Rank by total_ns, act on mean_ns. A 5 us kernel launched 200,000 times owns more device
time than a 50 ms kernel launched once. A big mean_ns says the body; a small one with manyinstances says the launch. Compare two profiles by mean_ns: total_ns moves with reps.
time_pct is nsys's Time (%): the kernel's share of ALL traced kernel time, taken before the cut
(kept rows have time_pct >= min_percent). Copies are outside that denominator.
| the rows show | the finding | the change |
|---|---|---|
one kernel at 80%+, mean_ns >= ~100 us |
the kernel BODY is the cost | nothing on this route says why; change the body, re-profile |
many instances, mean_ns under ~10 us |
launch-bound | fuse, do more per launch, or capture a CUDA graph |
mean_ns flat as the input grows |
fixed overhead | read the copies and the gap |
mean_ns growing faster than the input |
an algorithmic term | change the algorithm; no geometry change reaches it |
| two or three kernels at ~30% each | no single hotspot | fusing them beats tuning any one |
max_ns far above mean_ns, min_ns near it |
one slow launch, often the warmup rep's | elapsed_ns leaves it out; judge the body by min_ns |
device_pct frames all of it: mean kept-kernel time per traced rep over the fastest bracketed call.
The harness's copies are in neither. Below ~50% the kernels are not what costs, and a faster kernel
moves the total by less than its own speedup.
The copies
The harness's copies are never graded. Per rep, warmup included, it makes one h2d per pointer
argument; per measured rep, one d2h per output. So h2d count >= (reps + warmup) x pointer
arguments and d2h count >= reps x outputs, and only the excess is your code's. Only copies your
code makes INSIDE the call are in elapsed_ns.
Never subtract memory[] from elapsed_ns. The rows cover the whole child and are mostly the
harness's.
total keeps nsys's own unit: releases disagree on whether MB is 10^6 or 2^20. Settle it against
a copy whose size you know (2 MiB reported as 2.097 MB means 10^6), and carry the unit into any
bandwidth you compute.
For copies your code makes inside the call (you are handed device pointers; staging through host
memory is what creates them):
- Transfer time near kernel time: the transfer is the cost. Work on the device pointers.
- Bandwidth near the link rate (table): the copy is as fast as the wire. Move less.
- Bandwidth far below the link, large copies: pageable host memory. Pin the buffer you copy from
(cudaHostAlloc); near the ceiling pinning buys single-digit percent. - High
count, tinymean_ns: per-copy latency dominates. Pack into one transfer. memsetrows are device work;d2dyou did not write is usually a library.
| link | per direction | a good copy lands near |
|---|---|---|
| gen3 x8 | 7.88 GB/s | 6 |
| gen3 x16, gen4 x8 | 15.75 GB/s | 12-13 |
| gen4 x16, gen5 x8 | 31.5 GB/s | 25 |
| gen5 x16 | 63 GB/s | 50 |
Use the board's maximum link width: an idle GPU downclocks its link. A measured 13.1 GB/s is 83% of
a gen4 x8 wire and 42% of a gen4 x16 one, so check the row before calling a copy slow.
Gaps
gap_per_rep = elapsed_ns - device_ns_per_repThe part of the fastest call no kept kernel accounts for:
- Your in-call copies are still inside the gap;
memory[]cannot be subtracted (above). - Launch overhead, a few us per launch:
launch_count/ (reps + warmup) x ~5 us againstdevice_ns_per_rep. The same order means launch-bound, and a faster body will not help. The
host-side API summary that would measure it directly is not on this route. - Host work between launches: index math, allocation, a Python frame.
- Synchronizations your code adds: the harness already synchronizes once per call; one of yours
per launch turns an asynchronous pipeline into round trips. - Dropped kernels: with
min_percentabove 0 they fall into the gap. Send0.
A negative gap is the device_pct above ~100 case. Context creation is not in the gap: it happens
in the harness's setup, before any timed call. If you capture a CUDA graph, compare launch_count
and kernels[] before and after: the route sets no graph-trace option.
Resident is not busy. nsys records that a kernel was RESIDENT, not whether the device was
SATURATED: a kernel filling the timeline on a few SMs gives the same rows and device_pct as one at
peak. Saturation is not in the trace; tool:"ncu" counts it. A low device_pct (with min_percent 0) IS
conclusive: the device was idle for that share of the call.
cuda_gpu_trace -- geometry bounds occupancy
launches[] gives caps, not a measurement:
- few
blocks: most of the device gets no work, whatever the body does. threads_per_blocknot a multiple of 32: every block's last warp has idle lanes (100 threads is
4 warps, 28 lanes idle).registers_per_threadxthreads_per_block, andshared_memoryper block, against the part's
per-SM register and shared-memory budgets (not in the payload), bound how many blocks are
resident per SM at once.
Achieved occupancy is a per-SM counter: not in this trace and not inferable from it, andoccupancy_note says so. Ask tool:"ncu" for it, or report it as unmeasured rather than deriving a
number that looks measured.
What nsys cannot answer: ask tool:"ncu"
nsys records activity, not counters: no achieved occupancy, no stall reason, no cache hit rate.
The trace's per-copy Throughput (MBps) column is not in the payload; divide total by total_ns.
Nsight Compute (ncu) owns the counter questions, and profile serves it as tool:"ncu" on acuda submission. counters:true stays counters_unsupported, and PAPI's cuda and nvml
components are not reachable through /profile.
- A separate run of the same build.
ncureplays ONE launch once per counter pass, clocks
pinned and caches flushed, so no number it returns is a time and none goes next toelapsed_ns
or a score. Trace first:kernels[]names the kernel and how often it launched. - Body: the
scorebody plus"tool":"ncu".device_kernelis the exactnamefromkernels[]; aregex:value is a 400, because it matches every kernel whose name contains it.
Without it, the first launch after the warmup launches is counted.repsdefaults to 1. - Answer:
metrics[]rows (section,metric,value,unit; a column not reported is null),
keyed by raw metric id: Speed Of Light memory throughput, the three memory limits (unit access,
interconnect request, SM memory issue rate), L1/TEX throughput, active warps, and the hardware
and launch warps-per-scheduler maxima.kernelsis null: one launch has no share table.metrics_missingsays why when the export carried none of them. - The whole report is in your shared workspace at
report_dir:report.ncu-rep,details.txt(every section, body tables included) andraw.csv(every metric by id).report_fileslists what arrived;report_omittednames each file left behind and why. - This route has not run on an NVIDIA part in this deployment. An empty
metrics[]withmetrics_missingset is the reader not recognising the export, anddetails.txtstill has it. - Refusals, 503 with
cause:ncu_missing(noncuon the judge),not_linux,no_gpu,insufficient_permissions(the profiling gate below),ncu_failed(no report; the output is
quoted),ncu_report_missing(clean exit, no report: no launch matched, so checkdevice_kernel
and that the kernel launches more often than the warmup does),timed_out.
For device counter numbers met elsewhere:
- Counter collection SERIALISES kernels and replays multi-pass metric sets, so a counted run's
wall clock is not the plain run's. Never put its milliseconds next to a timed run's;ncu
timings are not speeds for the same reason. - CUPTI changed profiling APIs at Volta (event groups before, PerfWorks after), so a metric
present on one box can be absent on the next. - One event set counts ONE device through ONE context, and the counted kernel must be launched
by the thread that armed the set. Work elsewhere is not counted, which looks like a kernel that
did nothing.
A number nobody measured comes back null or as a refusal cause, never as 0.
The permission gate
NVIDIA's driver can restrict profiling to admin users; CUPTI tools then refuse withERR_NVGPUCTRPERM. The gate is on counters, so plain activity tracing usually survives it.
insufficient_permissions is raised only when no recording was written AND the output tail matches
a permission marker (cap_sys_admin, permission, not permitted, nvgpuctrperm,administrator). An empty kernel summary is no_kernels, not this.
Clearing the gate needs root, so it is the host's to check; report "not measured". It has two
spellings: the module option NVreg_RestrictProfilingToAdminUsers, which older drivers echo back,
and the internal RmProfilingAdminOnly the open kernel module publishes instead (driver 595.84
shows RmProfilingAdminOnly: 1 while CUPTI refuses). Matching only one reports "no gate" on a
gated box.
Refusals: 503 with cause
Each one is "not measured", never "fast".
| cause | meaning | next |
|---|---|---|
counters_unsupported |
counters:true on a device submission |
drop counters |
not_linux, nsys_missing |
judge host is not Linux, or has no nsys on PATH |
host fault, do not retry |
no_gpu |
/dev/nvidiactl absent: no NVIDIA GPU visible |
host fault, do not retry |
insufficient_permissions |
no recording, and the output tail matched a permission marker | host fault, do not retry |
nsys_failed |
no recording, any other reason; the error quotes the last 600 characters of output | read the quoted output |
timed_out |
the traced run or the stats export ran past timeouts.kernel_s x (reps + warmup + 2) and was killed |
fewer reps, or find the hang |
nsys_report_missing |
the stats export returned none of the four report sections | not your code |
kernel_share_missing |
the kernel report has no Time (%) column (tool renamed it) |
not your code |
no_kernels |
the kernel report has zero rows | your code ran on the host, a launch failed silently, or it forked: below |
rocprof_unsupported |
raised only for hip inside the nsys path; the route answers that with a 400 first |
-- |
A 500 traced run failed (exit N) is your program dying under the tracer; score the same code to
see whether it also dies without it. A wrong tool or a min_percent outside 0-100 is a 400.
no_kernels beside a correct result: nsys follows the process tree but not a bare fork()
child, whose kernels never reach the trace. The harness already spawns its measured worker; do notfork() without exec inside the call.
Traps
- Tracing is not free, only cheap: compare a traced run with a traced run; take speedups from
score. - The whole child process tree is traced: a submission that starts workers gets all their device
activity in one summary. - Kernel names arrive demangled and long:
textcuts them at 44 characters, the JSON does not.
Match on the JSON.