Skip to content

[NCCL EP] Support eager mode and overflow handling - #6229

Merged
YangFei1990 merged 13 commits into
NVIDIA:mainfrom
YangFei1990:nccl_ep_eager_overflow
Aug 11, 2026
Merged

YangFei1990 merged 13 commits into
NVIDIA:mainfrom
YangFei1990:nccl_ep_eager_overflow

Conversation

@YangFei1990

Copy link
Copy Markdown
Contributor
  • I, the PR author, have personally reviewed every line of this PR.

Add eager mode and overflow handling to the ncclEP flex dispatcher.

The ncclEP backend now selects between two modes based on moe_expert_rank_capacity_factor: unset selects eager, where TE sizes the receive buffer per step from the actual received-token count; setting it selects the existing static path, which keeps a fixed budget and stays CUDA-graph capturable.

In static mode the dispatcher enables NCCL EP's drop-on-overflow policy and accumulates over_budget and the peak required capacity device-side per dispatch, so the happy path never syncs — a MAX all-reduce runs only once overflow is reported. On overflow, PagedStashRunner grows the receive budget to that observed peak, rebuilds the EP context once, and replays the step, note this is different compared with hybrid where it use eager mode for rerun and fallback to same capacity afterwards.

Contribution process

Pre-checks

  • I have added relevant unit tests
  • I have added relevant functional tests
  • I have added proper typing to my code Typing guidelines
  • I have added relevant documentation
  • I have run the autoformatter.sh on my PR

Code review

Feel free to message or comment @NVIDIA/mcore-oncall to help accelerate your merge into main. The less complex your PR is, the faster it will be approved and merged!

All PRs start as draft. If you open a non-draft PR, it will be automatically converted to draft.

Step 1: Mark PR as "Ready for Review"

  1. When your PR is ready, click Ready for Review.
  2. An oncall reviewer is auto-assigned and expert reviewers are notified based on your changes.
    • Some PRs may jump straight to step 2. This is determined by .github/CODEOWNERS.

⚠️ Only mark as ready once merge-conflicts are resolved and the CI is passing.
Final Review might get declined if these requirements are not fulfilled.

Step 2: Final Review

For PRs that change megatron/core, once all expert reviewers have approved, the Final Review label is applied automatically and final reviewers are assigned.

For PRs outside megatron/core, this step is skipped.

Step 3: Approved

Once all required reviewers have approved, the Approved label is applied automatically.

Merge

Any member of mcore-engineers will be able to merge your PR.

Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
@YangFei1990
YangFei1990 requested review from a team as code owners August 3, 2026 23:41
@svcnvidia-nemo-ci
svcnvidia-nemo-ci marked this pull request as draft August 3, 2026 23:41
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

This PR has been automatically converted to draft because all PRs must start as drafts.

When you are ready for review, click Ready for Review to begin the review process. This will:

  1. Add the oncall reviewer (optional reviewer)
  2. Add required review teams based on your changes

See the contribution guide for more details.

@copy-pr-bot

copy-pr-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

Signed-off-by: YangFei1990 <feiw@nvidia.com>
@YangFei1990
YangFei1990 marked this pull request as ready for review August 3, 2026 23:59
if config.moe_pad_expert_input_to_capacity or config.moe_expert_capacity_factor is not None:
raise ValueError("drop and pad is not supported yet with the 'ncclep' backend.")

if self.eager:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So when we use zero copy now, do we still need to specify an appropriate recv-token-nums-per-rank? As I remember, BF16 used a symmetric memory pool, while FP8 required a fixed-size symmetric memory buffer. Why doesn’t BF16 zero-copy support eager mode either?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We do not support zero-copy with eager mode at the moment due to the memory overhead introduced by the eager symm pool.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this because the symmetric memory pool and the normal memory pool are independent of each other, so they cannot be reused simultaneously, leading to additional GPU memory usage?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes symm buffer pool is independent with Pytorch normal pool. When using symm pool for eager, it will always be allocated to the outlier recv buffer size, which kind of defeats the purpose of eager.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Double thinking, it is totally unacceptable for bf16 path, because the symm tensor will be saved per-layer per-mb as activations, so the pool will be super large. But for fp8 path, because only the quantized tensor will be save for backward, the symm tensor can be reused (in this case we allocate persistent tensor for graph mode), so the pool size probably can be acceptable, despite it is still allocated to the outlier recv size, but just for one tensor.

I think we should support eager mode zero copy with fp8 path, but nccl ep internal does not support it yet. I will work with the team for it later.

Comment on lines +1269 to +1273
# Only after an actual rerun: rebootstrap NCCL EP
if num_tries > 1:
for mlp in self.moe_layers:
mlp.token_dispatcher.invalidate_ep_bootstrap()
nccl_ep_release_context()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[SUGGESTION Simplification] invalidate_ep_bootstrap() here is redundant with the nccl_ep_release_context() on the next line.

_ensure_bootstrap short-circuits on self._bootstrapped and is_nccl_ep_bootstrapped() (line 1617), so releasing the process-wide context is already sufficient to force every manager to rebuild — that is exactly what the new is_nccl_ep_bootstrapped() term in that condition buys you. The explicit per-layer invalidation adds a loop over every MoE layer for no behavioral change.

Either drop the loop, or keep it and note in the comment that it is belt-and-braces against a TE build where ep_finalize does not clear the bootstrap flag. As written the two mechanisms overlap without the reader being able to tell which one is load-bearing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No this is not redundant. nccl_ep_release_context is global, but invalidate_ep_bootstrap is per-layer. This is by design to have more fine-grained control.

Comment thread megatron/core/transformer/moe/paged_stash.py
Comment thread megatron/core/transformer/moe/fused_a2a.py
Comment thread megatron/core/transformer/transformer_config.py
Comment on lines +2054 to +2071
def check_required_capacity(self):
"""Peak per-rank receive capacity the dispatcher needed, or None if it is not tracked."""
if hasattr(self._comm_manager, 'required_recv'):
return self._comm_manager.required_recv
else:
return None

def grow_ep_recv_capacity(self, new_capacity: int) -> None:
"""Grow the backend's receive budget to ``new_capacity`` so an overflowed step can be
replayed without dropping. No-op unless the backend supports it."""
if hasattr(self._comm_manager, 'grow_recv_capacity'):
self._comm_manager.grow_recv_capacity(new_capacity)

def invalidate_ep_bootstrap(self) -> None:
"""Force the backend to re-bootstrap on its next dispatch. Call after changing the mode:
every manager must re-derive it, not just the one that happens to dispatch first."""
if hasattr(self._comm_manager, '_bootstrapped'):
self._comm_manager._bootstrapped = False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[SUGGESTION Simplification] These three new dispatcher methods each probe for a private attribute on _comm_manager with hasattr, which pushes backend capability detection into string literals.

hasattr(self._comm_manager, 'required_recv'), hasattr(..., 'grow_recv_capacity'), hasattr(..., '_bootstrapped') — the last is the worst of the three: it reaches into a private attribute of another object and assigns to it (self._comm_manager._bootstrapped = False). A rename of _bootstrapped inside _NCCLEPManager would make invalidate_ep_bootstrap() a silent no-op, and the overflow recovery would degrade to reusing a stale EP context with no error anywhere.

Prefer an explicit, overridable hook on the manager base class:

class _DispatchManager(ABC):
    def invalidate_bootstrap(self) -> None:
        """No-op for backends without a bootstrapped context."""

    def required_recv_capacity(self) -> Optional[torch.Tensor]:
        return None

    def grow_recv_capacity(self, new_capacity: int) -> None:
        """No-op for backends with no static receive budget."""

Then the dispatcher just forwards, and adding a fourth backend gets the right default for free instead of relying on hasattr misses. This matches the existing check_over_budget pattern's problem rather than making it worse, so if you'd rather keep consistency with that, at minimum swap _bootstrapped for a public invalidate_bootstrap() method on _NCCLEPManager — mutating another class's private state across a module boundary is the part most likely to break quietly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is to align with the existing code style of the flex dispatcher and comm manager

@YangFei1990

Copy link
Copy Markdown
Contributor Author

/ok to test b993cfe

@YangFei1990

Copy link
Copy Markdown
Contributor Author

/claude strict-review

@svcnvidia-nemo-ci svcnvidia-nemo-ci added Approved All necessary approvals have been made and removed Final Review PR is in the "final review" stage labels Aug 10, 2026
@YangFei1990

Copy link
Copy Markdown
Contributor Author

/ok to test 2f67d31

@svcnvidia-nemo-ci

Copy link
Copy Markdown
Contributor

🔄 Merge queue validation started!

You can track the progress here: https://github.com/NVIDIA/Megatron-LM/actions/runs/31486939140

This branch was previously deployed

2 inactive deployments
public — 2f67d31e Deployed Aug 10, 2026 by copy-pr-bot[bot] via finalize / publish-docs #5702
test — 2f67d31e Deployed Aug 10, 2026 by copy-pr-bot[bot] via cicd-wait-in-queue #18642
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Approved All necessary approvals have been made complexity: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants