Skip to content

Fix runtime CP group handling in attention for hybrid CP - #6821

Merged
ilml merged 4 commits into
NVIDIA:mainfrom
ilml:fix/hybrid-cp-runtime-attention
Sep 5, 2026
Merged

ilml merged 4 commits into
NVIDIA:mainfrom
ilml:fix/hybrid-cp-runtime-attention

Conversation

@ilml

@ilml ilml commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Two fixes for runtime (hybrid/dynamic) CP in the attention stack, where a
model built with static context_parallel_size == 1 binds a CP group per
microbatch via PackedSeqParams.cp_group:

  1. Lazily create TE's auxiliary CP stream. The constructor only allocates
    TEDotProductAttention.cp_stream when static CP > 1, so the runtime branch
    passed cp_stream=None into TE and AttnFuncWithCPAndKVP2P crashed with
    AttributeError: 'NoneType' object has no attribute 'wait_event'.
  2. Route the runtime CP group to RoPE. apply_rotary_pos_emb derives
    (cp_size, cp_rank) for the fused THD RoPE kernel from its cp_group
    argument, but attention always passed the static pg_collection.cp. Under
    runtime CP each rank holds only its group's zigzag slice while cu_seqlens
    describe the full sequence, so the kernel indexed positions past the
    tensor: illegal memory accesses on short sub-samples (caught with
    CUDA_LAUNCH_BLOCKING=1 in fused_rope_forward), and wrong positions,
    NaN gradients, then a softmax-lse device assert on long ones. This is the
    dangerous one: with different luck it trains-but-wrong.

Test

New unit tests assert (a) RoPE receives the runtime group when
packed_seq_params.cp_group is bound and the static group otherwise, and
(b) the CP stream is lazily created and handed to TE's
set_context_parallel_group.

Validation

End to end on 4x GB200: 8B GPT, 100k-token THD workload, mixed CP1/2/4
groups, finite losses/grad norms. One of three independent fixes required to
make hybrid CP runnable (siblings: schema-bridge and rerun-iterator PRs);
supersedes the combined draft #6816.

Two fixes for models built with context_parallel_size == 1 that bind a
CP group per microbatch at runtime (hybrid/dynamic context parallelism):

1. TEDotProductAttention only allocates its auxiliary CP CUDA stream in
   the constructor when static CP > 1, so the runtime branch passed
   cp_stream=None into TE and AttnFuncWithCPAndKVP2P crashed with
   AttributeError: 'NoneType' object has no attribute 'wait_event'.
   Create the stream lazily when a runtime CP group is bound.
2. apply_rotary_pos_emb derives (cp_size, cp_rank) for the fused THD
   RoPE kernel from its cp_group argument, but attention always passed
   the static pg_collection.cp group. Under hybrid CP each rank holds
   only its runtime group's zigzag slice while cu_seqlens describe the
   full sequence, so the kernel indexed positions past the tensor:
   illegal memory accesses on short sub-samples, and wrong positions,
   NaN gradients and a softmax-lse device assert on long ones. Prefer
   packed_seq_params.cp_group when the runtime path binds one.

Unit tests cover both: RoPE must receive the runtime group (and the
static group when none is bound), and the CP stream must be created
lazily and passed to TE.

Validated end to end on 4x GB200 (8B GPT, 100k-token THD profile,
mixed CP1/2/4 groups, finite losses and grad norms).

Signed-off-by: Tom Long <tolong@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@ilml
ilml marked this pull request as ready for review August 24, 2026 22:22
@ilml
ilml requested review from a team as code owners August 24, 2026 22:22
Review follow-up: instead of a RoPE-local variable, re-assign
pg_collection.cp to the runtime CP group so every CP consumer in the
forward benefits. Because hybrid CP binds a different group per
microbatch and local_cp_size == 1 sub-samples carry no group, the
build-time group (captured at construction) is restored whenever no
runtime group is bound - otherwise a CP1 microbatch following a CP2 one
would inherit the stale larger group and recreate the wrong-positions
bug. The unit test now also asserts the bind and the restore on the
collection itself.

Signed-off-by: Tom Long <tolong@nvidia.com>
@ilml

ilml commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Done — took @parthmannan's suggestion and went one step further: the runtime group is now bound on pg_collection.cp itself so any CP consumer in the forward benefits. One subtlety this surfaced: hybrid CP binds a different group per microbatch, and local_cp_size == 1 sub-samples carry no group at all — a bare re-assign would leave a CP1 microbatch that follows a CP2 one seeing the stale larger group (wrong RoPE positions again). So the module captures the build-time group at construction and restores it whenever no runtime group is bound. The unit test now asserts both the bind and the restore on the collection; re-validated on GB200 (2 passed).

Comment thread megatron/core/transformer/attention.py Resolved
Keeps the tests consistent with the runtime-CP contract
(local_cp_size == 1 sub-samples carry no cp_group) that NVIDIA#6825 asserts.

Signed-off-by: Tom Long <tolong@nvidia.com>
@ilml

ilml commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Verified and audited, per the question above:

  • local_cp_size == 1 carries no CP group by construction: get_batch_on_this_cp_rank only binds hybrid_cp_group when local_cp_size > 1.
  • Swept every consumer of packed_seq_params.cp_group / local_cp_size in core for the "group always present" assumption: the TE wrapper handles and asserts the None case; gpt_model's three rotary call sites are safe (the packed_seq=True path never CP-slices — per-rank slicing happens in the fused THD kernel, which is what this PR fixes); MLA/absorbed-MLA hard-assert hybrid CP off. No other latent group-assumption bugs found.
  • The contract was only asserted in one direction ("no group ⇒ size 1"). Follow-up Assert the runtime-CP group contract both ways #6825 adds the converse ("size 1 ⇒ no group"), documents the invariant on PackedSeqParams, and adds a unit test — so a future producer that starts binding size-1 groups fails loudly instead of silently flipping the convention. Also updated this PR's tests to bind runtime groups with local_cp_size = 2 for consistency with that contract.

@zhongbozhu zhongbozhu left a comment

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.

LGTM

@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 Sep 4, 2026
@ilml
ilml enabled auto-merge September 4, 2026 17:54
@ilml

ilml commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 9493661

@ilml

ilml commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

/ok to test 14a19cc

The repo black config skips magic trailing commas; apply exactly the
hunks the lint job suggests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: ilml <tolong@nvidia.com>
@nemo-automation-bot

Copy link
Copy Markdown

🔄 Merge queue validation started!

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

Merged via the queue into NVIDIA:main with commit df4996f Sep 5, 2026
101 of 102 checks passed
@ilml
ilml deleted the fix/hybrid-cp-runtime-attention branch September 5, 2026 03:59

This branch was previously deployed

2 inactive deployments
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: low

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants