Skip to content

[PyTorch] NCCL EP eager mode and drop-on-overflow policy - #3229

Merged
phu0ngng merged 6 commits into
NVIDIA:mainfrom
phu0ngng:pyt_ep_eager
Jul 27, 2026
Merged

[PyTorch] NCCL EP eager mode and drop-on-overflow policy#3229
phu0ngng merged 6 commits into
NVIDIA:mainfrom
phu0ngng:pyt_ep_eager

Conversation

@phu0ngng

@phu0ngng phu0ngng commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Description

Add two NCCL EP dispatch options to the PyTorch API:

  • Eager mode: sizes dispatch/combine outputs to the per-step recv-token total instead of the static recv_capacity_per_rank.
  • Drop-on-overflow: tokens past max_recv_tokens_per_rank are dropped and dispatch continues, instead of trapping.

Type of change

  • Documentation change (change only to the documentation, either a fix or a new content)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Infra/Build change
  • Code refactoring

Changes

  • ep.h / ep_backend.cpp: add drop_on_overflow group policy and wire num_topk + AUTO recv budget through the group config.
  • pytorch/ep.py: ep_bootstrap(eager=...) toggle captured at bootstrap; EpBuffer reads it to size outputs from the per-step recv-token total and exposes num_recv_tokens. Eager and zero-copy are mutually exclusive, and eager rejects caller-provided output buffers.
  • pytorch/csrc/extensions: plumb the new fields through the dispatch binding.

Checklist:

  • I have read and followed the contributing guidelines
  • The functionality is complete
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

phu0ngng added 2 commits July 22, 2026 10:22
Signed-off-by: Phuong Nguyen <phuonguyen@nvidia.com>
…cv_tokens

Signed-off-by: Phuong Nguyen <phuonguyen@nvidia.com>
@phu0ngng
phu0ngng marked this pull request as ready for review July 22, 2026 18:17
@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds two new NCCL EP dispatch modes to the PyTorch API: eager mode, which sizes dispatch/combine output buffers dynamically to the per-step recv-token total (instead of the static recv_capacity_per_rank), and drop-on-overflow, which drops tokens exceeding max_recv_tokens_per_rank rather than trapping. Both features are wired through a new num_topk group config field and a drop_on_overflow overflow policy, with appropriate mutual-exclusivity checks.

  • ep_bootstrap gains num_topk (required) and drop_on_overflow flag; recv_capacity_per_rank becomes optional — omitting it enables eager mode. EpBuffer is updated to mirror the bootstrap mode and exposes total_recv_tokens (device int64 scalar) written by each prepare call.
  • ep_dispatch now always runs ep_prepare internally before sizing output buffers; in eager mode it also performs a host sync (.item()) to learn the per-step recv total, which is intentionally documented as incompatible with CUDA-graph capture.
  • The C++ binding (ep.cpp, ep_backend.cpp) and public C header (ep.h) add num_topk and drop_on_overflow to NVTEEpGroupConfig, and wire total_recv_tokens through the prepare path.

Confidence Score: 4/5

The new eager/overflow modes are internally consistent and well-guarded, but the public API changes — positional parameter reorder in ep_bootstrap/EpBuffer.init and the token_counts→tokens_per_expert rename with dtype change — silently break positional callers and attribute readers, despite being marked non-breaking.

The core eager/overflow logic is correct: mutual-exclusivity enforced at both layers, host-sync for dynamic sizing is intentional and documented, the backward is correctly reshaped. The unresolved breaking changes to positional argument order and the public EpBuffer attribute are the primary risk for downstream integrations.

Files Needing Attention: transformer_engine/pytorch/ep.py — public API surface changed in ways that silently break positional callers and attribute readers

Important Files Changed

Filename Overview
transformer_engine/pytorch/ep.py Core Python API changes: eager mode toggle, EpBuffer restructure, ep_dispatch now always calls ep_prepare internally. Public API: num_topk added as required positional arg to ep_bootstrap; recv_capacity_per_rank made optional; tokens_per_expert (int64) replaces token_counts (int32); positional parameter order changed — these are breaking changes documented elsewhere as previously flagged.
transformer_engine/pytorch/csrc/extensions/ep.cpp C++ binding: ep_prepare gains total_recv_tokens tensor arg (int64 mandatory); ep_initialize gains num_topk and drop_on_overflow; pybind11 registrations updated with named args and defaults. Logic is straightforward plumbing; no issues found.
transformer_engine/common/ep/ep_backend.cpp Backend: max_recv_tokens_per_rank check relaxed from >0 to >=0; new mutual-exclusivity checks for zero-copy+eager and drop_on_overflow+eager; total_recv_tokens_per_rank plumbed through ncclEpLayoutInfo_t.recv_total_counter. Changes are well-guarded.
transformer_engine/common/include/transformer_engine/ep.h Public C header: NVTEEpGroupConfig gains num_topk (int) and drop_on_overflow (int) at the end of the struct. struct_size-based versioning ensures backward ABI compatibility. Docs updated accordingly.
tests/pytorch/distributed/run_ep.py Test harness: two new env-flag test modes (EAGER, OVERFLOW) with decorator/skip logic; new test_eager_recv_sizing and test_overflow_drop test cases. Coverage is good; the assertLessEqual bound in test_eager_recv_sizing compares against a Python-computed capacity that NCCL's AUTO mode doesn't enforce, making it a loose sanity check.
tests/pytorch/distributed/run_test_ep.sh Shell harness: two new run_pass calls for eager and overflow modes; env var forwarding is correct.
examples/pytorch/ep/ep_moe.py Updated to new API: num_topk added to bootstrap, caller-owned buffer args moved from EpBuffer.init to ep_dispatch/ep_combine keyword args.
examples/pytorch/ep/bench/ep_bench.py Benchmark updated to new API: num_topk added, caller buffers moved to keyword args on ep_dispatch/ep_combine. No issues.
transformer_engine/pytorch/csrc/extensions.h Header declarations updated to match ep.cpp implementation changes. Consistent with the implementation.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant ep_dispatch
    participant ep_prepare
    participant NCCL_EP
    participant _EpDispatch

    Note over Caller,_EpDispatch: Eager mode (recv_capacity_per_rank=None)
    Caller->>ep_dispatch: ep_dispatch(buffer, tokens, topk_idx, topk_w)
    ep_dispatch->>ep_prepare: ep_prepare(buffer, topk_idx)
    ep_prepare->>NCCL_EP: ncclEpUpdateHandle (AllGather routing)
    NCCL_EP-->>ep_prepare: total_recv_tokens [device int64]
    ep_prepare->>ep_prepare: "buffer._host_total_recv_tokens = .item() [D2H sync]"
    ep_prepare-->>ep_dispatch: tokens_per_expert (buffer tensor)
    ep_dispatch->>ep_dispatch: "rows = buffer._host_total_recv_tokens"
    ep_dispatch->>ep_dispatch: alloc recv_tokens[rows,H] and recv_w[rows]
    ep_dispatch->>_EpDispatch: apply(handle_mem, recv_tokens, recv_w, topk_idx, tokens, topk_w)
    _EpDispatch->>NCCL_EP: ncclEpDispatch
    NCCL_EP-->>_EpDispatch: recv buffers filled
    _EpDispatch-->>ep_dispatch: (recv_tokens, recv_w)
    ep_dispatch-->>Caller: (recv_tokens, recv_w, tokens_per_expert)

    Note over Caller,_EpDispatch: Non-eager mode (fixed recv_capacity_per_rank)
    Caller->>ep_dispatch: ep_dispatch(buffer, tokens, topk_idx, topk_w)
    ep_dispatch->>ep_prepare: ep_prepare(buffer, topk_idx)
    ep_prepare->>NCCL_EP: ncclEpUpdateHandle
    NCCL_EP-->>ep_prepare: tokens_per_expert and total_recv_tokens [device only]
    ep_prepare-->>ep_dispatch: tokens_per_expert
    ep_dispatch->>ep_dispatch: "rows = buffer.recv_capacity_per_rank (static)"
    ep_dispatch->>_EpDispatch: apply(...)
    _EpDispatch-->>ep_dispatch: (recv_tokens, recv_w)
    ep_dispatch-->>Caller: (recv_tokens, recv_w, tokens_per_expert)
Loading

Reviews (4): Last reviewed commit: "Merge branch 'main' into pyt_ep_eager" | Re-trigger Greptile

Comment thread transformer_engine/pytorch/ep.py Outdated
Comment thread transformer_engine/pytorch/ep.py Outdated
@YangFei1990
YangFei1990 self-requested a review July 23, 2026 00:26
Comment thread transformer_engine/pytorch/ep.py Outdated
@@ -95,6 +98,9 @@ def ep_bootstrap(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With eager, do we still need to have recv_capacity_per_rank as a always required arg? I'm thinking to make it a kwarg, if user feed it, we mark eager to False, otherwise mark True. And we can get rid of eager kwarg.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do that too. Let me update the design.

Comment thread transformer_engine/pytorch/ep.py Outdated
bound. This requires a host sync each step, so it is not CUDA-graph
capturable. Mutually exclusive with ``zero_copy``. Defaults to ``False``.

``max_num_topk`` is the upper bound on per-token top-k; it sizes NCCL EP

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand this, is just just the top_k? upper bound on per-token top-k is a bit confusing. If it is just topk, I would recommend to make it a arg as it is something constant that user can always provide. User does not need to be aware of the logic that only eager mode requires the topk.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NCCL EP could work with the scenario in which there are multiple num_topk (in different layers), which we may not need.
We can simplify this to num_topk and make it mandatory.

Comment thread transformer_engine/pytorch/ep.py
Comment thread transformer_engine/pytorch/ep.py
Comment thread transformer_engine/pytorch/ep.py Outdated
f"topk_weights must be float32; got dtype={topk_weights.dtype}. "
"Cast with topk_weights.float() before calling."
)
skip_prepare = False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This skip_prepare logic looks a bit awkward. Can we just move the ep_prepare from the autograd function to here always? Seems to me it is ok, in this way we also do not need to pass buffer.total_recv_tokens to autograd fn

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment thread transformer_engine/pytorch/ep.py Outdated
raise ValueError("ep_bootstrap: zero_copy and eager modes are mutually exclusive")
if eager and max_num_topk < 1:
raise ValueError("ep_bootstrap: eager mode requires max_num_topk >= 1")
if eager and drop_on_overflow:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we really need to assert this? Overflow should be irrelevant to eager right? When it is in eager mode, drop_on_overflow should be ignored automatically?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For drop_on_overflow, we need recv_capacity which eager mode does not provide.

phu0ngng added 2 commits July 27, 2026 09:39
…nk, require num_topk, and run prepare in ep_dispatch

Signed-off-by: Phuong Nguyen <phuonguyen@nvidia.com>
…ecv-buffer API

Signed-off-by: Phuong Nguyen <phuonguyen@nvidia.com>
@phu0ngng

Copy link
Copy Markdown
Collaborator Author

/te-ci L1

YangFei1990
YangFei1990 previously approved these changes Jul 27, 2026
alignment = int(alignment)
if alignment > 1 and (alignment & (alignment - 1)) != 0:
raise ValueError(f"alignment must be 0, 1, or a power of two (got {alignment}).")
self.eager = _EAGER

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a dependency that buffer creation needs to happen after bootstrap, can we add assertion for it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

Signed-off-by: Phuong Nguyen <phuonguyen@nvidia.com>
@phu0ngng

Copy link
Copy Markdown
Collaborator Author

/te-ci L1

@phu0ngng
phu0ngng merged commit 98524e5 into NVIDIA:main Jul 27, 2026
9 of 14 checks passed
@phu0ngng
phu0ngng deleted the pyt_ep_eager branch July 27, 2026 22:10
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.

2 participants