Skip to content

feat(m2n): quantized tensor support (scale forwarding + on-the-fly FP8) (#7) - #11

Open
kwen2501 wants to merge 434 commits into
mainfrom
kwen/m2n-quantized-tensor-support
Open

feat(m2n): quantized tensor support (scale forwarding + on-the-fly FP8) (#7)#11
kwen2501 wants to merge 434 commits into
mainfrom
kwen/m2n-quantized-tensor-support

Conversation

@kwen2501

Copy link
Copy Markdown
Collaborator

Summary

  • Migrates the #7 prototype from nccl-rl (kwen/m2n-scale-forwarding) onto nccl-extensions.
  • Adds ncclReshardScaled / ncclReshardScaledWithWindow for coupled (payload, scales) reshard with block-alignment validation.
  • Adds ncclReshardQuantized for on-the-fly FP8 (E4M3 / MXFP8) wire compression with keep-quantized or dequantize modes.
  • Ports Python facade (ScalePlane, QuantSpec) and private CI unit tests for scale-plane validation + quantize API/numerics.

Test plan

  • make -C nccl_m2n lib with an existing NCCL 2.30.x build
  • Private CI unit_tests --gtest_filter=ScalePlane* → 20/20 passed (host)
  • Private CI unit_tests --gtest_filter=Quantize* → 10 API tests passed; 10 GPU kernel tests skipped (no device on login host)
  • GPU unit + MPI scaled_reshard on EOS (sync_nccl_ext_issue7.sh)
  • Regenerate cybind bindings once NCCL_PIN header is ≥ 2.30.5 (manual binding port used for now)

xiakun-lu and others added 30 commits May 27, 2026 06:58
Replace the strict sm < 90 error in both the Makefile and CMakeLists.txt
with a per-CUDA-version selection trimmed to Hopper and newer, mirroring
the patterns in makefiles/common.mk and the root CMakeLists.txt:

- Makefile: guard the override with $(origin NVCC_GENCODE)=file so a
  user-supplied env/command-line value passes through unchanged while
  common.mk's ?= default is replaced with the trimmed sm_90+ list.
- CMakeLists.txt: guard with NOT DEFINED CMAKE_CUDA_ARCHITECTURES so
  -D overrides pass through, and apply CMAKE_CUDA_ARCHITECTURES on the
  target the same way src/CMakeLists.txt does.

When the user supplies an override that contains an arch below sm_90,
let nvcc surface the error rather than pre-validating.

Signed-off-by: Xiakun Lu <xiakunl@nvidia.com>
If user provides NCCL windows for the output token tensor
and NVLink-only configuration is detected,
write tokens directly into the output locations over NVLink,
skipping the receive-stage copy.

Only token headers are written into the staging buffer
for token source information tracking.

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
Take advantage of the z-copy path in LL/Dispatch

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
## Summary
Expanding testing matrix for NCCL EP CI, add testing for different layouts and batch size

## Implementation
**ep_test**

| Invocation  | Algorithm | Layout         | Tokens | Hidden |
|-------------|-----------|----------------|--------|--------|
| -a ll       | LL        | EM (hardcoded) | 128    | 7168   |
| -a ht -L fl | HT        | FLAT           | 4096   | 7168   |
| -a ht -L em | HT        | EM             | 4096   | 7168   |

**ep_bench**

Low-latency:
| Layout       | 128 | 256 | 1K | 4K | 8K |
|--------------|-----|-----|----|----|----|
| expert-major | ✓   | ✓   | ✓  | ✓  | X  |
| rank-major   | ✓   | ✓   | ✓  | ✓  | X  |

High-throughput
| Layout       | 128 | 256 | 1K | 4K | 8K |
|--------------|-----|-----|----|----|----|
| flat         | ✓   | ✓   | ✓  | ✓  | X  |
| expert-major | ✓   | ✓   | ✓  | ✓  | X  |

8K batch size was disabled for now due to the failure on EOS: https://gitlab-master.nvidia.com/nccl/nccl/-/jobs/328613355

## Testing
CI

## Release notes
(Optional.) This serves as a base when compiling the release notes.
If you feel this should be mentioned then you can add a tentative text here. Otherwise
indicate it should not be mentioned.

## Tracking
https://nvbugspro.nvidia.com/bug/6169264

add coverages

Signed-off-by: Chris Cao <chricao@nvidia.com>

See merge request nccl/nccl!2789

Signed-off-by: Chris Cao (SW-GPU) <chricao@nvidia.com>
Stage libnccl_ep.so plus the headers its JIT runtime needs (nccl_ep.h,
ep_enums.h, common.hpp, device/*.cuh) into nccl/ep/{lib,include}/ so the
nccl4py wheel ships them as package data. A new build_assets/build_native.sh
builds libnccl.so (link-time dep, not shipped) and libnccl_ep.so under a
sterile env, then copies the artifacts into the package tree. For production
wheels, build_wheels.sh runs the script inside a manylinux_2_28 container
selected by uname -m, so the .so has glibc-2.28 symbol versions.

auditwheel repair excludes libnccl.so.2 (from nvidia-nccl-cu13) and
libcuda.so.1 (driver, not in container). nccl/ep/__init__.py points
NCCL_EP_JIT_SOURCE_DIR / NCCL_EP_JIT_BUILD_INCLUDE_DIR at the bundled
headers before any binding load, so HT-mode JIT finds them post-install.

contrib/nccl_ep no longer bakes a local rpath into libnccl_ep.so: the
Makefile drops -Xlinker -rpath -Xlinker \$(NCCL_LIBDIR), and CMakeLists
sets BUILD_WITH_INSTALL_RPATH=TRUE with INSTALL_RPATH="". Matches
libnccl.so's no-RUNPATH convention; the cu13 wheel's pre-dlopen of
libnccl.so.2 with RTLD_GLOBAL is what satisfies the NEEDED at runtime.

Signed-off-by: Xiakun Lu <xiakunl@nvidia.com>
Fix mis-allocation of the rdma_inter_node_group_flags.
This aray is used in both Dispatch and Combine in HT mode.
The indexing is `(node_id * max_num_of_chunks_per_rank + chunk_idx)`

However, at buffer allocation the size of this array is calculated as
flags_sz = align_size(rdma_team_size * sizeof(uint64_t), GIN_ALIGNMENT);
Thanks to GIN_ALIGNMENT, the actual size is no less than 4KB

At max batch size of 8192, chunk=128 → max_chunks=64
The 4KB allocation It works up to 8 nodes:
7 (nodes) × 64 (max chunk) × 8 = 3584 B
However, for 9 nodes it will be overflown:
9 × 64 × 8 = 4608 B

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
Signed-off-by: Bharath Ramesh <bhramesh@nvidia.com>
This commit fixes a problem when number of nodes
exceed the number of G2S stages (NUM_OF_STAGES_G2S).

The RDMA portion of multi-node path in
inter_node_G2S_warp_group_device_function
was not accounting for exceeding the NUM_OF_STAGES_G2S.
Up to 4 nodes this would not happen, but starting from
5 nodes and topK of 5 it is possible to exceed
the number of stages and thus data should be loaded in
batches of NUM_OF_STAGES_G2S.
Thank you to Yi Liu (@yliu.16) from ByteDance to
reporting and investigating this fix.

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
Currently, the Combine send buffer size is calculated
based on the worst-case expert-major requirements.
For 16 rank, 512 experts, 4K batch size and 16KB tokens
this results in 512 x 4K x 16 KB = 35 GB buffer.

Rank-majer, however, only depends on number of ranks thus
16 x 4K x 16KB = 1GB

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
As we are getting a lot of concerns about memory consumption,
extending ep_bench to report the consumption

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
Currently, the layout size is calculated by separately
picking max of the send and recv buffers for dispatch/combine.
This is not optimal, as dispatch has larger receive and
combine has larger send buffer.
Optimize the calculation by selecting the max of the
send+recv buffers sizes.

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
1. Honor User-provided rdma buffer size and allocate it unconditionally
Fail in the handle init if the buffer space is insufficient

2. Optimize the AUTO scenario.
Allocating inside the EP Group creation is unoptimal as
topK is not known and we have to use MAX=32.
This significantly impacts the combine receive buffer size.
Delay actual allocation to the first Handle init.

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
Update documentation to reflect the consequences of
lazy staging buffer allocation for LL mode.
For `rdma_buffer_size == NCCL_EP_AUTO`, Handle init
operation may become collective and reallocate
staging buffer (rendering undefined behavior of
any in-flight communication).

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
Decouple the LL/active mask synchronization buffer from the staging buffer.
The staging buffer is lazily allocated and may change it's shape.
Active mask management code currently assumed that layout is
allocated for the worst-case which would cause buffer overflow
for other cases.

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
…copy

Use epGetTensorData to resolve dispatch output pointers in HT validation
paths. tensor->data is null for window-backed tensors, causing validation
to fail with a null deref.

Signed-off-by: Subhadeep Bhattacharya <subhadeepb@nvidia.com>
Templatize the LL dispatch/combine kernels and host wrappers on
TopkIdxT (int32_t or int64_t). The C-side API gates in
ncclEpUpdateHandle now accept either dtype for LL; HT keeps strict
ncclInt64. Callers select the kernel specialisation through the
cached handle->topk_idx.datatype at dispatch / combine time.

ep_bench gains a --topk-idx-int32 flag for validating the new
specialisation. Caller is responsible for ensuring expert ids fit
in the chosen integer width.

Eventual landing target: separate MR.

Signed-off-by: Ludwig Schneider <lschneider@nvidia.com>
Signed-off-by: Ludwig Schneider <lschneider@nvidia.com>
Signed-off-by: Ludwig Schneider <lschneider@nvidia.com>
Signed-off-by: Ludwig Schneider <lschneider@nvidia.com>
Signed-off-by: Phuong Nguyen <phuonguyen@nvidia.com>
Signed-off-by: Phuong Nguyen <phuonguyen@nvidia.com>
Catches the bug introduced by 7732e3d ("HT handle memory
optimization"), which dropped the per-iteration memset of
global_routing_map. With the memset gone, the convert kernel writes
only rows 0..num_tokens-1, but ncclAllGather ships the full
max_tokens rows — stale tail rows from a prior iteration get sent
to peers and interpreted as live routing.

The test drives ncclEpUpdateHandle twice on the same handle:
  iter 1: num_tokens=max_tokens=4, routes every expert across 4 ranks
          (max-dirties the routing map)
  iter 2: num_tokens=1, single token routed to expert 0
          (convert writes row 0; rows 1..3 retain iter-1 bits)

After iter-2's update, the test asserts per-rank
ncclEpHandle_test_getNumRecvTokens against the with-fix expected
{rank0: 4, rank1: 0, rank2: 0, rank3: 0}. Without the fix the
counts are deterministically {6, 4, 2, 4} - every rank fails its
EXPECT_EQ.

The detector is independent of any CUDA OOB / page fault, so it
works at the small unit-test fixture sizes (max_tokens=4,
kHidden=16, kMaxRecvSlots=16) where the original illegal-address
signature was absent.

The test fails on this commit, demonstrating the bug. The follow-up
commit restores the cudaMemsetAsync fix and the test passes.

Signed-off-by: Hsin-Chun Yin <hsinchuny@nvidia.com>
Alternative fix to 0632add70 ("nccl-ep: zero local routing-map slot in
HT update_handle"): instead of a separate cudaMemsetAsync on the local
routing send slot, extend convert_topk_to_routing_map_kernel to launch
over max_tokens rows and zero the tail rows [num_tokens, max_tokens)
from the convert kernel itself. The downstream ncclAllGather over the
full max_tokens stride then ships clean tail bytes to peers.

Trade-offs vs the memset fix:
* Saves one kernel launch (~5-10 us) per HT dispatch.
* Costs nothing in the uniform num=max case -- the grid is already
  sized to max_tokens and tail threads do zero work.
* In non-uniform / num<max cases, the extra work is (max_tokens -
  num_tokens) rows of num_experts_packed bytes at HBM speed (sub-us
  for the vLLM Qwen3-MoE config: max_tokens=8192, packed=16).
* Restores the kernel's pre-existing "the caller does not pre-zero"
  invariant -- ownership of the routing map's zero-state stays with
  the kernel that writes it.

Mechanical changes:
* hybridep_adapter.cu: add max_tokens parameter to kernel and host
  wrapper, change early-return bound to max_tokens, add an early
  return for tail threads after row zeroing, and resize the launch
  grid to ceil(max_tokens / block_size).
* hybridep_adapter.cuh: thread max_tokens through the declaration.
* nccl_ep.cc: pass max_tokens at the ncclEpUpdateHandle call site
  (max_tokens is already in scope as group->config.max_dispatch_tokens_per_rank).

Validated by the same test_ht_stale_routing_map regression test added
in 99bace8cd -- the test asserts the iter-2 per-rank recv counts and
will reproduce the stale-tail bug if the tail zeroing is removed.

Signed-off-by: Hsin-Chun Yin <hsinchuny@nvidia.com>
Asymmetric per-rank token counts (e.g. [32, 0, 16, 0] across an
ep_size=4 group) aborted any rank whose input was empty. Three
over-strict checks combined to make a legitimate zero-token
configuration unrepresentable:

1. tensorHasBinding rejected descriptors with data == nullptr, but
   any zero-extent CUDA tensor has no element to address. The
   binding check is the right contract for non-empty buffers; for
   empty ones it should be waived.
2. ncclEpDispatch and ncclEpCombine had a lazy "if num_tokens == 0,
   re-derive from inputs->tokens" branch that conflated "not yet
   set via ncclEpUpdateHandle" with a legitimate zero, then fell
   back to tensor_required(inputs->tokens) and tripped (1).
3. resolveTensorWindowBinding called ncclWinGetUserPtr on a NULL
   win_hdl for empty tensors with no external window.

Fixes:
  - tensorIsEmpty() (any sizes[i] == 0); tensorHasBinding() returns
    true for empty tensors.
  - bool num_tokens_set on ncclEpHandle, set in ncclEpUpdateHandle;
    the two lazy branches now key off !num_tokens_set.
  - resolveTensorWindowBinding short-circuits on empty tensors.

The dispatch / combine kernels are already empty-input safe: sender
loops in device/low_latency.cu degrade to no-ops and grid sizing is
independent of numTokens.

Signed-off-by: Ludwig Schneider <lschneider@nvidia.com>
The non-uniform sampler skipped 0 (sampled in [1, max_tokens] and
hardcoded the last rank to 1), so the asymmetric zero-token path
in libnccl_ep.so was never exercised at the C level. Three
bench-side issues prevented it from running cleanly:

1. generateRandomTopkIndicesLL computed a uniform distribution over
   [0, num_tokens - 1]. At num_tokens = 0 the unsigned subtraction
   underflowed to UINT_MAX and the masking write indexed far past
   the empty topk_idx_host buffer.
2. epGetTensorData returned ncclInvalidUsage when the descriptor's
   data pointer was NULL (cudaMalloc(0) yields nullptr) and the
   tensor was not in the optional alloc-state map. Empty tensors
   legitimately have nothing to address.
3. The "Non-uniform tokens in [1, %u]" startup banner advertised a
   range the sampler no longer uses.

Fixes:
  - Guard the LL masking loop on num_tokens > 0.
  - epGetTensorData hands back data=nullptr/success when any
    sizes[i] == 0; consumers must pass count=0 to CUDA APIs (the
    bench already does).
  - Sample non-uniform counts in [0, max_tokens] and pin the last
    rank to 0 so the regression case is always exercised.
  - Update the banner to "[0, %u] (last rank forced to 0)".
Signed-off-by: Ludwig Schneider <lschneider@nvidia.com>
As of v0.1, NCCL EP does not support quantization.
Remove remainings of it from ep_bench to avoid confusion.
The support is planned to be added and expected in
future releases

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
Signed-off-by: Phuong Nguyen <phuonguyen@nvidia.com>
The zero-copy mode enum (dc26057) introduced an opt-in for skipping
library-owned staging, but on the LL dispatch path the new mode was
only consumed as an implicit branch on recv_x->win_hdl. When the
caller requested ON but did not satisfy the opportunistic conditions
(nvlink-only topology, rank-major layout, !use_fp8, windowed
recv_x), the library silently fell back to a state where the kernel
either staged through buffers it expected to be elided, or addressed
windows that were not registered. The result was undefined writes
rather than a clean diagnostic.

Add explicit gating on LL dispatch: zero_copy=ON errors with
ncclInvalidArgument if any of nvlink_only, rank-major layout,
!use_fp8_outer, or recv_x->win_hdl set is violated. AUTO/OFF stay
opportunistic on HT (master parity) and on LL dispatch (matches the
dc26057 opportunistic branch); no warnings emitted.

LL combine is out of scope. The combine kernel reads inputs->tokens
directly via pointer and uses windows only to translate peer
recv-buffer pointers in ncclGetP2pPtr; the kernel-level zeroCopy
flag is dispatch-shaped (sender-side P2P writes) and plumbing
config.zero_copy through it on combine silently picks the wrong
send path. Revert the LL combine plumbing to a hardcoded false
(matches origin/master) and treat config.zero_copy as a
dispatch-only switch.

Signed-off-by: Ludwig Schneider <lschneider@nvidia.com>
Align the README with the implementation before v0.1 release

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
Avoid outdated info in README.md due to dynamic
nature of the test.

Signed-off-by: Artem Y. Polyakov <artemp@nvidia.com>
kingchc and others added 30 commits August 5, 2026 15:27
Reshard collectives and call-time resource allocation are not safe during
CUDA graph capture, which could otherwise fail late or replay stale state.
The public APIs now reject capture immediately with an actionable
invalid-usage error.

Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
Staging allocations could grow without a fixed bound and reservations could
remain occupied after failures. Fixed-size buckets now bound
per-communicator staging memory, while failed or abandoned reservations are
released safely.

Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
Reshard staging now supports a default direct path and an opt-in
packed-window path selected at runtime. Unsupported selections are rejected
clearly instead of being silently ignored.

Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
Partial-group reshards now use communicators containing only participating
ranks. Adaptive selection reduces rank-varying inputs so all ranks agree on
whether to split and on the required resources.

Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
M2N calls can now be batched in groups and submitted together at the
outermost group end, while an abort discards an incomplete group. Group
execution reports which recorded call failed.

Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
Correctness-only tests could not detect staged-group fusion regressing to
one-at-a-time execution. Testing instrumentation now counts fused
submissions so grouped tests can verify fusion without affecting production
builds.

Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
CI verified only that the library and header existed, so missing public
symbols could go unnoticed. The export check now derives public API
declarations from the header and verifies that each is defined and exported
by the shared library.

Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
Signed-off-by: Ke Wen <kwen@nvidia.com>
Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
The default copy algorithm was DIRECT here and PACKWINDOW upstream, so an
unconfigured caller got a different path than intended. Both the inline
default and the runtime config reset now select PACKWINDOW.

Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
Chunk granularity, GIN context count and the cross-dim transpose threshold
were fixed at their compile-time constants, so the environment variables
documented for them had no effect. Each now reads a runtime value that falls
back to the same constant.

Co-authored-by: Kaushik Kandadi Suresh <kkandadisure@nvidia.com>
Co-authored-by: Sreeram Potluri <spotluri@nvidia.com>
Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
An early return between reserving the transpose buffer and recording its
completion event left the reservation held, so later calls on that
communicator failed with a spurious concurrency error. The reservation is now
released by a scope guard on both the success and error paths.

Co-authored-by: Kaushik Kandadi Suresh <kkandadisure@nvidia.com>
Co-authored-by: Sreeram Potluri <spotluri@nvidia.com>
Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
Co-authored-by: Kaushik Kandadi Suresh <kkandadisure@nvidia.com>
Co-authored-by: Sreeram Potluri <spotluri@nvidia.com>
Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
The shipped matrix was missing regression groups that cover cache churn
across fresh streams and two pack-window paths. The cases and the MPI
stream-churn driver they need are restored.

Co-authored-by: Kaushik Kandadi Suresh <kkandadisure@nvidia.com>
Co-authored-by: Sreeram Potluri <spotluri@nvidia.com>
Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
… first failure

Signed-off-by: Ke Wen <kwen@nvidia.com>
Signed-off-by: Ke Wen <kwen@nvidia.com>
Signed-off-by: Ke Wen <kwen@nvidia.com>
Signed-off-by: Ke Wen <kwen@nvidia.com>
Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
`NCCL_RESHARD_NUM_CTAS` can select an invalid GIN context when the CTA count is not divisible by the context count. The change affects CTA-to-context assignment for reshard kernels. No other behavior changes are intended.

Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
Signed-off-by: Ke Wen <kwen@nvidia.com>
Signed-off-by: Ke Wen <kwen@nvidia.com>
Signed-off-by: Ching-Hsiang Chu <chichu@nvidia.com>
Port scale forwarding and on-the-fly FP8 quantization from the
nccl-rl prototype onto nccl-extensions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.