[RFC] Device Profiler Hook - #2285
Draft
dolpm wants to merge 2 commits into
Draft
Conversation
Add an optional device-side profiler hook: a __device__ callback that
ncclKernelMain's profiler() invokes at each kernel work-item boundary
(START/STOP) with a small POD event (ncclProfilerDevEvent_t). The plugin owns
its telemetry end-to-end (buffer, format, atomicity, depth, drain).
The hook is additive and independent of the legacy path: it fires per work item
whenever registered, needs no proxy op or host callback, and does not disable
the workStarted/workCompleted ring (which stays gated by each work item's
profilerEnabled flag). A plugin gets graph-launch-overhead-free kernel timing by
using the hook and not enabling ncclProfileKernelCh, so no proxy op / host
callback is armed.
The hook is provided by the profiler plugin .so through the versioned profiler
interface. ncclProfiler_v7 reuses the v6 event descriptor/state args and adds
one method:
ncclResult_t (*getDeviceHook)(int cudaDev, void** devHook, void** devCtx);
NCCL pulls the per-device __device__ hook from the loaded plugin at comm init
(devCommSetup -> ncclProfilerDevGetHook -> ncclProfiler->getDeviceHook). A
v6-or-older plugin has no method => nullptr => legacy ring. No new exported
libnccl symbol; the device-side kernel path is identical whether or not a hook
is present.
Device side:
- profiler_dev.h: device hook + event ABI (POD, fixed layout, reserved field)
- device/common.h: profiler() calls the hook per work item; legacy ring stays
gated by profilerEnabled and runs alongside it
- device.h: profilerDevHook/profilerDevCtx on ncclKernelComm (void*, cast in
common.h)
- device/generate.py: emit ncclDevFuncName[]/ncclDevFuncIdCount; backs the
exported ncclProfilerDevFuncName(funcId) name resolver
Plugin interface:
- plugin/profiler/profiler_v7.{h,cc}: v7 interface + getNcclProfiler_v7() shim
- nccl_profiler.h: make v7 the default ncclProfiler_t
- profiler_v6.cc: adapt v6 plugins into v7 (host callbacks pass through,
getDeviceHook = nullptr)
- profiler.cc: try v7 first in the load chain; ncclProfilerDevGetHook forwards
to ncclProfiler->getDeviceHook
- profiler.h/profiler.cc: ncclProfilerState carries devHook/devCtx; init.cc
propagates them into the device comm
An example plugin exercising the device hook follows in the next commit.
Signed-off-by: dolpm <34420038+dolpm@users.noreply.github.com>
Bump the example profiler plugin to ncclProfiler_v7 and add a real device hook (plugin_dev.cu): a __device__ callback that NCCL invokes once per work item from inside ncclKernelMain's profiler(). The hook counts events per device into its own device context and, at finalize, prints the counts and resolves the last funcId via the libnccl-exported ncclProfilerDevFuncName(). Opt-in via NCCL_PROFILER_EXAMPLE_DEV_HOOK so the example's host-side behavior is unchanged by default. The v7 struct reuses the existing v6 host callbacks and adds getDeviceHook. Built with nvcc + relocatable device code (the __device__ hook needs a real, callable global address for NCCL's kernel to invoke it); Makefile and CMakeLists updated accordingly. Run: NCCL_PROFILER_EXAMPLE_DEV_HOOK=1 NCCL_PROFILER_PLUGIN=./libnccl-profiler-example.so <app> Signed-off-by: dolpm <34420038+dolpm@users.noreply.github.com>
dolpm
force-pushed
the
profiler-device-hook
branch
from
July 13, 2026 19:17
4f38fa8 to
96b8c97
Compare
Collaborator
|
Thank you for the contribution. We are reviewing the PR. |
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.
RFC: Device-Side Profiler Hook
Summary
Add an optional device-side profiler hook: a
__device__callback thatncclKernelMain'sprofiler()invokes at each kernel work-item boundary(START / STOP). A profiler plugin
.soprovides the hook + opaque contextper device, NCCL discovers it through the same plugin-
.sopath as the hostprofiler, and from then on calls it directly from the kernel with a small POD
event. The plugin owns its telemetry end-to-end — buffer, format, atomicity,
depth, and draining.
The hook is additive and independent of the legacy path: it fires per work
item whenever registered, needs no proxy op or host callback, and does not
disable the ring. A plugin gets graph-launch-overhead-free kernel timing simply
by using the hook and not enabling
ncclProfileKernelCh— so no proxy op /host callback is armed at all — which the legacy ring alone cannot offer.
This is an alternative to the fixed
workStarted/workCompletedring +proxy-delivery path for consumers that want it, while leaving that path fully
intact for everyone else.
Motivation
Today NCCL's device profiler writes two fixed-size per-channel rings
(
workStarted,workCompleted), each entry a{timestamp, counter}pair, anddelivers them to the host profiler plugin via a proxy op. That design has two
limits:
consumer wanting richer per-work telemetry (func identity, custom fields,
its own ring depth/format) can't get it without patching core.
CUDA graphs, which perturbs exactly the kernel timing a profiler is trying to
measure.
We want a path where a plugin can capture whatever it needs, directly on the
device, with zero added host-side work in the graph.
Design
Event ABI (
src/include/profiler_dev.h, new)A POD event with a fixed layout for ABI stability:
The hook is called on lane 0, once per profiler-enabled work item. It must
be cheap and must not touch the collective's data or synchronization.
A helper resolves a
funcIdto a human-readable name (e.g."AllReduce_Sum_f32_RING_LL"):Discovery (
src/plugin/profiler/profiler_v7.{h,cc}, new)The device hook is provided by the profiler plugin
.soas a new method on theversioned profiler interface.
ncclProfiler_v7reuses the v6 event descriptor /state args verbatim (no new host-side events) and adds one method:
ncclProfiler_v7_tbecomes the defaultncclProfiler_t.profiler.cctriesgetNcclProfiler_v7()first in the existing version-fallback chain(v7 → v6 → … → v1).
descriptor is identical) and
getDeviceHookis set tonullptr. v5-and-oldershims already adapt into
ncclProfiler_t; their zero-initialized struct leavesgetDeviceHooknull. So a plugin built against any version ≤ v6 simply has nodevice hook.
ncclProfilerDevGetHook(device, …)(internal) forwards toncclProfiler->getDeviceHookwhen non-null. The plugin resolves its own__device__symbol ondevice(a__device__function has a distinct addressper device — the plugin owns the device code, so resolution lives where it
belongs).
passive plugin), exactly like the rest of the profiler.
ncclProfilerDevFuncNameremains exported for plugins to resolve
funcId→ name.Propagation (
src/init.cc)At comm init (
devCommSetup, afterncclProfilerPluginInithas loaded theplugin), NCCL pulls the hook for the comm's device into the device comm:
nullptr(no plugin, or plugin returns none) => legacy ring behavior, unchanged.ncclKernelComm(src/include/device.h) gains twovoid*fields(
profilerDevHook,profilerDevCtx), typed asvoid*to keepdevice.hfreeof the hook typedef;
common.hcasts them.ncclProfilerState(
src/include/profiler.h) gains the matching host-sidedevHook/devCtx.Kernel path (
src/device/common.h)profiler()is restructured into a single loop over work items. For eachprofiler-enabled item:
ncclProfilerDevEvent_tand call thehook.
profilerEnabled, write the legacyworkStarted/workCompletedring as before.The hook fires whenever registered and is independent of the host profiler
plugin; the legacy ring stays gated by
profilerEnabled(the plugin's per-workflag). Both can run simultaneously.
Func-name table (
src/device/generate.py)Codegen now emits
ncclDevFuncName[]— one human-readable name per primaryfunc id, indexed the same as
ncclDevKernelForFunc[]— plusncclDevFuncIdCount. This backsncclProfilerDevFuncName()so a consumer canresolve
funcIdwithout reproducing the codegen's variant enumeration.Example / testing (
plugins/profiler/example)The example profiler plugin is bumped to v7 and gains a real device hook
(
plugin_dev.cu): a__device__callback that counts events per device into itsown device context and, at finalize, prints the counts and resolves the last
funcIdviancclProfilerDevFuncName(). It is opt-in viaNCCL_PROFILER_EXAMPLE_DEV_HOOK, so the example's host-side behavior isunchanged by default. It is built with nvcc + relocatable device code (the
__device__hook needs a real, callable global address for the kernel to invokeit indirectly). This ships as a separate commit on top of the core change.
Validated end to end on 2× GB200 via
all_reduce_perf: with the hook enabled itfires per work item (e.g. ~6.7k events/device for a size sweep), resolves func
names (e.g.
AllReduce_Sum_f32_RING_LL), and reports#wrong = 0; with the hookdisabled (default) behavior and results are unchanged.
Compatibility
nullptr) => existingworkStarted/workCompletedring + proxy path is unchanged.ncclProfilerDevEvent_t) is POD with fixed layout and a reservedfield for forward evolution.
ncclKernelComm/ncclProfilerState; no existingfields removed or reordered.
Alternatives considered
ncclProfilerDevSetHook). The consumer resolves its own__device__fn ptr and pushes it into a process-wide per-device store inlibnccl before creating comms. Rejected as the OSS ABI: it adds a new exported
libnccl symbol and a second registration mechanism unlike the established
plugin-discovery model. Note the tradeoff — push is a more direct fit for a
consumer that generates its hook at runtime (e.g. NVRTC), since pull requires
such a consumer to wrap the runtime-resolved pointer behind a plugin
.so+its own setter. We chose pull for ABI cleanliness; a runtime consumer builds a
thin plugin whose
getDeviceHookreturns its runtime-set pointer.ncclProfilerDev_v1symbol on the same.so, independent of thehost profiler version. Rejected: its only real advantage is letting a plugin
provide a device hook without being the host profiler — a marginal case, since
only one profiler plugin loads and a device profiler is a profiler. Folding
the method onto
ncclProfiler_v7keeps a single versioned interface and reusesthe existing fallback machinery; v7 reuses the v6 descriptor by typedef, so there
is no struct duplication.
graph overhead, and bakes one more fixed format into core.
reason; the whole point is to avoid host callbacks in captured graphs.
Open questions
not supported (existing device comms keep their copy).