profiler/inspector: report ring buffer drops instead of losing data silently - #2304
profiler/inspector: report ring buffer drops instead of losing data silently#2304shaq918 wants to merge 1 commit into
Conversation
|
++ @rishdas can you take a look? Please let me know if we want to take in this contribution. |
rishdas
left a comment
There was a problem hiding this comment.
First, thank you for the contribution, I think this is good and essential change needed in Inspector.
Overall I agree with the change and think it should be accepted after suggested changes.
Few suggestions in these lines :
- The JSON metadata is written per record, not per dump so I just lets add a new record and report all the dump stats in that record. Its like marker for the beginning of dump cycle. (in the stats we should coll and p2p record drops as seperate stats)
- I would suggest renaming nccl_collective_count and nccl_p2p_count and instead of placing it per record report it once per dump along with dropped stats. ALso flag protect this feature with default disabled.
| INS_CHK(inspectorRingDrain<inspectorCompletedOpInfo>(&commInfo->completedCollRing, | ||
| drainedColl)); | ||
| commInfo->dump_coll = inspectorRingNonEmpty(&commInfo->completedCollRing); | ||
| droppedTotal = commInfo->completedCollRing.dropped; |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Good call — I'll extract this into inspectorCommInfoUpdateDropStats(...) so the collective and P2P paths share it. Thanks!
|
|
||
| JSON_CHK(jsonKey(jfo, "metadata")); | ||
| inspectorCommInfoMetaHeader(jfo); | ||
| inspectorCommInfoMetaHeader(jfo, droppedTotal, droppedSinceLastDump); |
There was a problem hiding this comment.
Instead of having this printed for every JSON record, I was wondering of we can print single record per dump and call dump stats and have dropped + even print record stats, make this feature more complete.
There was a problem hiding this comment.
Agreed, stamping this on every record is wasteful. I'll emit a dedicated stats record once per dump carrying the drop counts and the records-written count, so the capture ratio is readable in one place. Since the counters are per-communicator, I'm planning one stats record per comm per dump rather than a single global one, to preserve the per-comm attribution JSON mode has today — let me know if you'd rather it be aggregated.
One small thing I'll be careful about: a separate record type means a consumer filtering on coll_perf lines would skip it, which is a faint echo of the very bug we're fixing — so I'll name and document the record to make it hard to miss.
There was a problem hiding this comment.
Per comm dump sounds better.
| INS_CHK(inspectorRingDrain<inspectorCompletedOpInfo>(&commInfo->completedP2pRing, | ||
| drainedP2p)); | ||
| commInfo->dump_p2p = inspectorRingNonEmpty(&commInfo->completedP2pRing); | ||
| droppedTotal = commInfo->completedP2pRing.dropped; |
There was a problem hiding this comment.
Same comments as for the collective counterparts.
There was a problem hiding this comment.
Will apply the same per-dump stats-record treatment to the P2P path.
| inspectorUnlockRWLock(&commInfo->guard); | ||
| if (overflowed && !__atomic_exchange_n(&ringDropWarned, true, __ATOMIC_RELAXED)) { | ||
| WARN_INSPECTOR( | ||
| "NCCL Inspector: completed-op ring buffer overflowed on comm %s (%s ring size %u); " |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Happy to change it. One quick check so I stay consistent: the existing messages in the plugin use the "NCCL Inspector:" prefix (e.g. the pool init/exhaustion logs). Do you want just this line changed to "NCCL Inspector Profiler Plugin:", or is there a broader prefix rename you'd like? Just want to avoid introducing a third variant.
There was a problem hiding this comment.
Actually I take back what I said its better to stay consistent ignore this comment.
| "nccl_bus_bandwidth_gbs{%s} %.6g\n" | ||
| "nccl_collective_exec_time_microseconds{%s} %.6g\n", | ||
| "nccl_collective_exec_time_microseconds{%s} %.6g\n" | ||
| "nccl_collective_count{%s} %llu\n", |
There was a problem hiding this comment.
Lets flag protect this via env variable and have it as optional.
Rationale, Prometheus metrics emitted need to be picked by external exporters, adding new metrics would have implications on metrics storage footprint as usally metrics have their own quota.
There was a problem hiding this comment.
NCCL_INSPECTOR_PROM_DUMP_VERBOSE
There was a problem hiding this comment.
Actually I would suggest to not have this metric here at all
"nccl_collective_count / nccl_p2p_count are the wrong shape. These are per-window gauges named with the _count suffix Prometheus reserves for histogram/summary components. Worse, a per-window gauge is only correct when the scrape interval exactly matches the dump interval — a double scrape counts the window twice, a missed scrape loses it. A cumulative per device write counter (nccl_collectives_total) would make rate()
work and is strictly more useful."
There was a problem hiding this comment.
Makes sense — the footprint/quota concern is fair for anything that adds cardinality to an exporter people already scrape. I'll gate this behind an env var, default off. (Combined with your other note on this line about the metric shape, it'll become a cumulative counter that's opt-in.)
There was a problem hiding this comment.
Got it — I'll use NCCL_INSPECTOR_PROM_DUMP_VERBOSE for the per-window / verbose metrics.
There was a problem hiding this comment.
You're completely right, and thank you for this — it's the comment I'm most grateful for. I'll switch to a cumulative monotonic counter (e.g. nccl_collectives_total) so rate()/increase() handle the windowing at query time and it stays correct under scrape jitter, double scrapes, or a missed scrape — rather than the per-window gauge with the _count suffix. As a nice side effect, rate(dropped) / rate(total) then gives a clean capture-ratio SLI.
| labels, busMean, | ||
| labels, execMean); | ||
| labels, execMean, | ||
| labels, (unsigned long long)agg.count); |
There was a problem hiding this comment.
Same comments as above for Collective.
There was a problem hiding this comment.
Will gate the P2P equivalent the same way.
| * Not thread-safe. Onus of thread safety is on the caller/owner of | ||
| * the file handle. | ||
| */ | ||
| static inspectorResult_t inspectorPromWriteDeviceTotals(FILE* file, |
There was a problem hiding this comment.
Lets flag protect this metric by and you can make it more verbose if you want like per comminicator and stuff.
NCCL_INSPECTOR_PROM_DUMP_STATS
There was a problem hiding this comment.
Will do — I'll add NCCL_INSPECTOR_PROM_DUMP_STATS and can make it per-communicator when verbose.
One thing I'd love your read on for this specific counter. Since the premise of the change is that overflow is silent today, I'd like to keep the JSON dropped_total and the one-time WARN on by default, so the "you're losing data" signal is present out of the box — the WARN is one line per process lifetime, and the device-level dropped counter is a single series per GPU ({node, gpu}), which is tiny next to the existing default-on per-bucket metrics (those already multiply by collective × message_size × algo_proto).
So my proposal would be: JSON dropped_total + WARN default-on, and all the richer Prometheus emission (this metric included) opt-in via the flag. If you'd still prefer this counter flag-gated too, I'll happily do it — the WARN + JSON evidence still cover the silent-loss case — I just wanted to make the case before flipping the default.
There was a problem hiding this comment.
My take is JSON you can keep at as verbose as you want, you dont have to curb anything, I will leave that call upto you.
I envision JSON mode for Ninja User and aware of all the system level impications and Probably can modify code if needs be.
Prometheus Mode is for regular high level sys admin wanting to get handle on NCCL perf for their workload by plumbing the data to dashbaord.
So the default version should be minimalistic version any extra metrics or stats can be turned on and off by runtime knob.
Coming to the WARN, currently default make file translates WARN to Info because other upstream systems look at WARN level messages hence we are carefully on what NCCL puts as WARN.
You can keep it as WARN but in your org if you want to use be midful you will have to compile it with this flag NCCL_INSPECTOR_ENABLE_WARN to actually have it as WARn in running system.
I am fine with this proposal
"So my proposal would be: JSON dropped_total + WARN default-on, and all the richer Prometheus emission (this metric included) opt-in via the flag"
| "NCCL Inspector: completed-op ring buffer overflowed on comm %s (%s ring size %u); " | ||
| "oldest entries are being dropped before they can be dumped. Increase " | ||
| "NCCL_INSPECTOR_DUMP_COLL_RING_SIZE/NCCL_INSPECTOR_DUMP_P2P_RING_SIZE or lower " | ||
| "NCCL_INSPECTOR_DUMP_THREAD_INTERVAL_MICROSECONDS. Dropped counts are reported in the output.", |
There was a problem hiding this comment.
I would refrain from this suggestion on NCCL_INSPECTOR_DUMP_THREAD_INTERVAL_MICROSECONDS as its bit nuanced especially in Prometheus Textfile collector mode where you can cant drop below a certain value because of the exporter properties.
There was a problem hiding this comment.
Agreed — I'll drop the NCCL_INSPECTOR_DUMP_THREAD_INTERVAL_MICROSECONDS suggestion from the warning. You're right that it's misleading in Prometheus textfile-collector mode, where the interval can't go below the exporter's floor. I'll point at the ring-size knob and the docs instead.
|
Revised draft: Thanks for the rework — the Decisions, in one place
Must fix before merge
Should fix
|
Problem: Completed collective/P2P records are stored in a fixed-size per-communicator ring buffer drained periodically by the dump thread. When operations complete faster than the ring is drained, inspectorRingEnqueue overwrites the oldest unread entry silently: nothing is logged, no counter is incremented, and nothing in the output indicates data is missing. All three output modes (JSON, Prometheus, OTLP) drain the same rings, so each silently reports a biased sample. On a 2x8 H100 run of all_reduce_perf looping 256MB messages (~770 collectives/s vs a 1024-slot ring per 3s dump interval), JSON output captured under half of the collectives with no indication of loss. Solution: Count enqueues and overwrites on the ring (enqueued/dropped/ droppedReported fields, updated under the existing per-communicator guard; no new locks or allocations on the completion path) and surface the loss in every output mode: - A one-time WARN on first overflow (mode-independent, in the enqueue path), suggesting a larger ring. - JSON: a per-communicator "dump_stats" record once per dump carrying records-written plus cumulative and since-last-dump drop counts for coll and p2p; per-record metadata is unchanged (format v4.0 -> v4.1). The two rings are drained under one guard hold each, preserving the original per-ring locking. - Prometheus (opt-in via NCCL_INSPECTOR_PROM_DUMP_STATS, default 0): per- device cumulative counters nccl_collectives_total / nccl_p2p_total and nccl_collectives_dropped_total / nccl_p2p_dropped_total, emitted for every known device so rate() and rate(dropped)/rate(total) stay usable. - OTLP (under NCCL_INSPECTOR_OTEL_VERBOSE): the same per-device totals as OTLP data points. The default Prometheus and OTLP output is unchanged; JSON always emits the dump_stats record. The WARN maps to INFO unless built with NCCL_INSPECTOR_ENABLE_WARN=1, matching NCCL's conservative WARN policy. Limitations: Drop counts say how many records were lost, not which. In Prometheus/OTLP the counters are per device, not per communicator, to bound label cardinality; the summed cumulative counters can dip if a communicator is destroyed mid-job. Signed-off-by: Shashank Mohankumar <smohankumar@crusoe.ai>
9905b65 to
0140e55
Compare
|
Thanks — this was a really useful review. I've rebased the PR onto 1. Retarget to 2. OTLP mode. Extended rather than scoped out. Worth noting: the one-time overflow 3. Gate the Prometheus metrics. Done — 4. README. Reworked against 5. Idle-device totals. The per-device stats are now emitted for every known device each dump, not just those with 6. Lock hold time. Reverted to one guard hold per ring — the JSON path drains collectives and P2P under separate 7. Dead write. Removed — the Prometheus drain paths no longer touch |
|
/mirror dev |
1 similar comment
|
/mirror dev |
Problem
The Inspector plugin stores completed collective/P2P records in a fixed-size
per-communicator ring buffer (
NCCL_INSPECTOR_DUMP_COLL_RING_SIZE, default1024) that a background thread drains every
NCCL_INSPECTOR_DUMP_THREAD_INTERVAL_MICROSECONDS. When operations completefaster than the ring is drained,
inspectorRingEnqueueoverwrites the oldestunread entry — silently. Nothing is logged, no counter is incremented, and
nothing in the output indicates that data is missing, so downstream consumers
treat a biased sample as complete data.
This is easy to hit in practice. On a 2-node × 8 H100 run of nccl-tests
all_reduce_perflooping 256 MB messages (~770 collectives/s against 1024ring slots per 3 s dump interval), the JSON output captured only 48.4% of
collectives: 5,825,536 records out of a 12,036,596
coll_snspan across 16ranks (6,211,060 lost). Per-rank record counts were exact multiples of 1024
(355×1024 / 356×1024) — the ring-overwrite fingerprint. Prometheus mode reads
the same ring, so its windowed aggregates are computed over the same biased
sample with no indication either.
Relatedly, the per-window operation count (
agg.count) was already computedfor the exec-time mean in Prometheus mode but never emitted, so there was no
rate signal to notice the loss (or to use for straggler detection).
Solution
Count the overwrites and surface them in every output mode, without changing
behavior for jobs that never overflow:
inspectorCompletedRinggainsdropped(cumulative) anddroppedReported(snapshot at last drain). The enqueue path increments
droppedin theexisting overwrite branch — one increment under the already-held
per-communicator write lock; no new locks or allocations.
dropped_totalanddropped_since_last_dump(snapshotted under the same guard as the drain). Format version bumped
v4.0 → v4.1.
nccl_collective_count/nccl_p2p_count— per-bucket operation countper dump window (the previously computed-but-unemitted
agg.count);nccl_collectives_dropped_total/nccl_p2p_dropped_total— cumulativedrops with device-level labels (
version,slurm_job_id,node,gpu),summed across the device's communicators.
Format minor bumped →
version="v5.2".overflow, suggesting a larger ring or shorter dump interval.
Design notes: the drop counters are kept device-level in Prometheus mode to
avoid adding per-bucket cardinality; per-communicator detail is available in
JSON mode. Summing cumulative per-communicator counters can decrease if a
communicator is destroyed mid-job; in practice communicators outlive the
scrape windows and the counter is monotonic.
Validation
2 nodes × 8 H100, Slurm, nccl-tests
all_reduce_perf, plugin built with thestandalone Makefile (gcc
-Wall -Wextra, no warnings).dropped_total= 0 on all 16 ranks,coll_sngap-free (span == records,e.g. 1826/1826) — no behavior change on the non-overflow path, and no
warning logged.
~500k collectives/rank): on 15 of 16 ranks the final
dropped_totalequals the ground truth computed from the output itself,
(max(coll_sn) + 1) − records_written, exactly — e.g. rank 0:222,208 records, max
coll_sn499,750 → 277,543 expected, 277,543reported. The 16th rank differed by exactly 1 record out of ~500k,
consistent with a single event discarded by the default
NCCL_INSPECTOR_REQUIRE_KERNEL_TIMINGfilter (which consumes a sequencenumber without entering the ring). On every rank the
dropped_since_last_dumpvalues across dumps sum todropped_total.The overflow warning appeared exactly once per process (16 total).
Notably, each rank's first ~770–800 records were overwritten before the
first drain ever ran (first surviving
coll_sn≈ 770), so simplespan-based gap analysis undercounts the loss — the counter sees it.
mid-run:
nccl_collective_count= 1024 (exactly ring size per windowwhile overflowing),
nccl_collectives_dropped_total= 87,841 andgrowing,
nccl_p2p_dropped_total= 0, all underversion="v5.2".Limitations
came from within a dump interval.
communicators/buckets (deliberate, to bound label cardinality).