Repository navigation
Fix MTP CP-boundary roll to communicate on the context-parallel process group #6746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
360e152
bb6d5b2
0fed350
b11538c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3931,6 +3931,90 @@ def test_attention_mask_validation_mamba(self): | |
| raise | ||
|
|
||
|
|
||
| class TestRollTensorWithCPSubgroup: | ||
| """Roll-tensor CP paths on a CP group that is a strict subgroup of WORLD. | ||
|
|
||
| Regression coverage for the CP-boundary exchange being issued on the | ||
| default (WORLD) process group instead of ``cp_group``: with | ||
| WORLD == CP group the two are indistinguishable, so these tests run | ||
| TP2 x CP2 to make the CP group a strict subgroup. | ||
| """ | ||
|
|
||
| def setup_method(self, method): | ||
| Utils.initialize_model_parallel(tensor_model_parallel_size=2, context_parallel_size=2) | ||
|
|
||
| def teardown_method(self, method): | ||
| Utils.destroy_model_parallel() | ||
|
|
||
| @staticmethod | ||
| def _cp_shard(tensor, cp_rank, cp_size, dim=-1): | ||
| """Shard along ``dim`` in the load-balanced two-chunk CP layout.""" | ||
| chunks = tensor.chunk(2 * cp_size, dim=dim) | ||
| return torch.cat([chunks[cp_rank], chunks[2 * cp_size - 1 - cp_rank]], dim=dim) | ||
|
|
||
| def test_roll_tensor_cp_boundary_exchange(self, monkeypatch): | ||
| cp_group = get_context_parallel_group() | ||
| cp_size = cp_group.size() | ||
| cp_rank = torch.distributed.get_rank(group=cp_group) | ||
|
|
||
| # Spy on the batched P2P launch: every boundary-exchange op must be | ||
| # addressed to the CP communicator, never the default (WORLD) group. | ||
| seen_groups = [] | ||
| real_batch_isend_irecv = torch.distributed.batch_isend_irecv | ||
|
|
||
| def spy_batch_isend_irecv(ops): | ||
| seen_groups.extend(op.group for op in ops) | ||
| return real_batch_isend_irecv(ops) | ||
|
|
||
| monkeypatch.setattr(torch.distributed, "batch_isend_irecv", spy_batch_isend_irecv) | ||
|
|
||
| full = torch.arange(2 * 16, dtype=torch.float32, device="cuda").reshape(2, 16) | ||
| # Oracle: the documented single-rank semantics on the full sequence. | ||
| expected_full, _ = roll_tensor(full.clone(), shifts=-1, dims=-1) | ||
| expected_local = self._cp_shard(expected_full, cp_rank, cp_size) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We build the reference by rolling the full sequence without distributed communication, then extracting this rank’s CP shard. |
||
|
|
||
| local = self._cp_shard(full, cp_rank, cp_size).contiguous() | ||
| rolled, rolled_sum = roll_tensor(local, shifts=-1, dims=-1, cp_group=cp_group) | ||
|
|
||
| assert torch.equal(rolled, expected_local) | ||
| assert rolled_sum == expected_local.sum() | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These check the local output values—including exchanged boundary tokens and zero padding—and the returned local sum. They verify numerical behavior is preserved. They can also pass before the fix because the old implementation communicates with the correct peers through WORLD. |
||
| assert seen_groups, "CP boundary exchange must go through batch_isend_irecv" | ||
| assert all(group is cp_group for group in seen_groups) | ||
|
|
||
| def test_roll_tensor_packed_seq_cp_boundary_exchange(self): | ||
| cp_group = get_context_parallel_group() | ||
| cp_size = cp_group.size() | ||
| cp_rank = torch.distributed.get_rank(group=cp_group) | ||
|
|
||
| seq_lengths = [8, 16] # each divisible by 2 * cp_size | ||
| cu_seqlens = torch.tensor([0, 8, 24], dtype=torch.int32, device="cuda") | ||
| full = torch.arange(1 * 24, dtype=torch.float32, device="cuda").reshape(1, 24) | ||
|
|
||
| packed_seq_params = PackedSeqParams(cu_seqlens_q=cu_seqlens) | ||
| # Oracle: single-rank packed semantics (per-sequence roll) on the full tensor. | ||
| expected_full, _ = roll_tensor( | ||
| full.clone(), shifts=-1, dims=-1, packed_seq_params=packed_seq_params | ||
| ) | ||
|
|
||
| def shard_packed(tensor): | ||
| pieces = [] | ||
| start = 0 | ||
| for length in seq_lengths: | ||
| seq = tensor[..., start : start + length] | ||
| pieces.append(self._cp_shard(seq, cp_rank, cp_size)) | ||
| start += length | ||
| return torch.cat(pieces, dim=-1) | ||
|
|
||
| expected_local = shard_packed(expected_full) | ||
| local = shard_packed(full).contiguous() | ||
| rolled, rolled_sum = roll_tensor( | ||
| local, shifts=-1, dims=-1, cp_group=cp_group, packed_seq_params=packed_seq_params | ||
| ) | ||
|
|
||
| assert torch.equal(rolled, expected_local) | ||
| assert rolled_sum == expected_local.sum() | ||
|
|
||
|
|
||
| class TestLearnedOutputContract: | ||
| """Tests for the learned n-stream to one-stream mHC contraction.""" | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The existing packed-sequence test uses TP1 × CP2 and checks numerical correctness without checking communicator selection. When run with two ranks, CP and WORLD contain the same ranks, hiding the communicator mismatch from numerical checks.
This test uses TP2 × CP2 to guarantee CP is a strict subgroup of WORLD, and explicitly checks that boundary exchanges use cp_group. The explicit group assertion detects the regression