Skip to content

Commit 811cfea

Browse files
committed
[distributed] Test the padding of a sequence that cp_size does not divide
A length of 7 over 2 ranks (plain, packed, `inputs_embeds` and through a 2D padding mask) and a length of 5 over 4 ranks, compared on the real positions of the logits only, plus a single-process test of the hook's padding of every input, of a positional `input_ids` and of a config without a `pad_token_id`.
1 parent 6a1ed4f commit 811cfea

1 file changed

Lines changed: 84 additions & 19 deletions

File tree

‎tests/context_parallel/test_context_parallel.py‎

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
import types
1415
import unittest
1516
from unittest import mock
1617

@@ -41,6 +42,7 @@
4142
UlyssesCP,
4243
all_to_all,
4344
check_context_parallelism,
45+
context_parallel_pre_forward,
4446
)
4547
from transformers.distributed.parallel_utils import ALL_PARALLEL_STYLES
4648
from transformers.distributed.utils import gather_full_state_dict
@@ -435,7 +437,8 @@ def _tiny_batch(config, batch_size, seq_len, packed=False, seed=0):
435437
if not packed:
436438
return input_ids, None
437439
# two documents back to back, which `position_ids` has to describe for the whole sequence
438-
position_ids = torch.cat([torch.arange(seq_len // 2), torch.arange(seq_len // 2)]).expand(batch_size, -1)
440+
first = seq_len // 2
441+
position_ids = torch.cat([torch.arange(first), torch.arange(seq_len - first)]).expand(batch_size, -1)
439442
return input_ids, position_ids
440443

441444

@@ -483,7 +486,8 @@ def _compare_with_unsharded(init_dir, batch, distributed_config, keep=None):
483486
Run the full `batch` through an unsharded model and through the same weights sharded with context parallelism,
484487
then compare the loss, the logits and every gradient.
485488
486-
`keep` masks the logits of the positions the comparison is about, e.g. the ones padding does not cover.
489+
`keep` is the 2D `attention_mask` of the unsharded batch: the logits of its padded positions are not part of what
490+
the model is trained on.
487491
"""
488492
tokens = batch["input_ids"] if batch.get("input_ids") is not None else batch["inputs_embeds"]
489493

@@ -506,11 +510,17 @@ def _compare_with_unsharded(init_dir, batch, distributed_config, keep=None):
506510
output = model(**local_batch)
507511
output.loss.backward()
508512

509-
# every rank computed the loss of the full sequences, and its own slice of the logits
513+
# every rank computed the loss of the full sequences, and its own slice of the logits of the padded sequence: the
514+
# padding at the end of the last rank's slice has no counterpart in the unsharded run
510515
cp_rank = model._mesh_manager.get_mesh("cp").get_local_rank()
511-
expected_logits = reference_logits[rows].chunk(distributed_config.cp_size, dim=1)[cp_rank]
512-
logits = _local(output.logits)
516+
local_seq = output.logits.shape[1]
517+
start = cp_rank * local_seq
518+
# the real positions of this rank's slice, which can be none of them when the padding fills it
519+
real = max(0, min(local_seq, reference_logits.shape[1] - start))
520+
expected_logits = reference_logits[rows][:, start : start + real]
521+
logits = _local(output.logits)[:, :real]
513522
if keep is not None:
523+
keep = (keep[:, start : start + real] == 1)[..., None]
514524
expected_logits, logits = expected_logits * keep, logits * keep
515525
torch.testing.assert_close(logits, expected_logits, rtol=CP_RTOL, atol=CP_ATOL, msg="Logits mismatch")
516526

@@ -566,9 +576,10 @@ def _test_tp_and_cp_compose(rank, tp_size, cp_size):
566576
dist.barrier()
567577

568578

569-
def _test_matches_unsharded(rank, cp_size, fsdp_size, dp_replicate_size, packed):
570-
config = _tiny_config()
571-
batch_size, seq_len = 2, 8
579+
def _test_matches_unsharded(rank, cp_size, fsdp_size, dp_replicate_size, packed, seq_len=8):
580+
# Ulysses splits the heads over the ranks, so four ranks need four key/value heads
581+
config = _tiny_config(num_key_value_heads=4) if cp_size > 2 else _tiny_config()
582+
batch_size = 2
572583
with _deterministic_init_model_dir(rank, config, torch.float32) as init_dir:
573584
input_ids, position_ids = _tiny_batch(config, batch_size * fsdp_size * dp_replicate_size, seq_len, packed)
574585
distributed_config = DistributedConfig(
@@ -582,10 +593,10 @@ def _test_matches_unsharded(rank, cp_size, fsdp_size, dp_replicate_size, packed)
582593
dist.barrier()
583594

584595

585-
def _test_padded(rank, cp_size, config=None, packed=False):
596+
def _test_padded(rank, cp_size, config=None, packed=False, seq_len=8):
586597
"""Left and right padding through a 2D `attention_mask`; the padded positions carry no label."""
587598
config = config or _tiny_config()
588-
batch_size, seq_len = 2, 8
599+
batch_size = 2
589600
with _deterministic_init_model_dir(rank, config, torch.float32) as init_dir:
590601
input_ids, position_ids = _tiny_batch(config, batch_size, seq_len, packed)
591602
attention_mask = torch.ones_like(input_ids)
@@ -599,9 +610,7 @@ def _test_padded(rank, cp_size, config=None, packed=False):
599610
}
600611
if position_ids is not None:
601612
batch["position_ids"] = position_ids
602-
# the logits of the padded positions are not part of what the model is trained on
603-
keep = (attention_mask.chunk(cp_size, dim=1)[rank] == 1)[..., None]
604-
_compare_with_unsharded(init_dir, batch, DistributedConfig(cp_size=cp_size), keep=keep)
613+
_compare_with_unsharded(init_dir, batch, DistributedConfig(cp_size=cp_size), keep=attention_mask)
605614
dist.barrier()
606615

607616

@@ -663,10 +672,10 @@ def _test_matches_unsharded_with_all_reduce_ep(rank, cp_size, ep_size, tp_size):
663672
dist.barrier()
664673

665674

666-
def _test_inputs_embeds(rank, cp_size):
675+
def _test_inputs_embeds(rank, cp_size, seq_len=8):
667676
"""`inputs_embeds` is sharded along its sequence dim, not along its last, hidden one."""
668-
config = _tiny_config()
669-
batch_size, seq_len = 2, 8
677+
config = _tiny_config(num_key_value_heads=4) if cp_size > 2 else _tiny_config()
678+
batch_size = 2
670679
with _deterministic_init_model_dir(rank, config, torch.float32) as init_dir:
671680
input_ids, _ = _tiny_batch(config, batch_size, seq_len)
672681
reference = AutoModelForCausalLM.from_pretrained(init_dir)
@@ -757,9 +766,6 @@ def _test_forward_refusals(rank, cp_size):
757766
model = AutoModelForCausalLM.from_pretrained(init_dir, distributed_config=DistributedConfig(cp_size=cp_size))
758767
input_ids, _ = _tiny_batch(config, 2, 8)
759768

760-
with pytest.raises(ValueError, match="must be divisible by `cp_size`"):
761-
model(input_ids=input_ids[:, :-1])
762-
763769
with pytest.raises(ValueError, match="`past_key_values` are not allowed"):
764770
model(input_ids=input_ids, past_key_values=DynamicCache())
765771

@@ -809,6 +815,51 @@ def _test_save_load_distributed_checkpoint(rank, cp_size):
809815
dist.barrier()
810816

811817

818+
class TestContextParallelPadding(unittest.TestCase):
819+
"""The root pre-hook pads every sequence-shaped input to a multiple of `cp_size`."""
820+
821+
def setUp(self):
822+
self.addCleanup(ALL_ATTENTION_FUNCTIONS.register, "sdpa", ALL_ATTENTION_FUNCTIONS["sdpa"])
823+
824+
def _prepare(self, module, args=(), **kwargs):
825+
return context_parallel_pre_forward(module, args, kwargs, cp_mesh=_MeshStub(2))
826+
827+
def _module(self, pad_token_id=7):
828+
config = types.SimpleNamespace(_attn_implementation="sdpa", pad_token_id=pad_token_id)
829+
return types.SimpleNamespace(config=config)
830+
831+
def test_pads_input_ids_labels_and_the_mask(self):
832+
input_ids = torch.tensor([[1, 2, 3, 4, 5]])
833+
kwargs = {
834+
"input_ids": input_ids,
835+
"position_ids": torch.tensor([[0, 1, 2, 0, 1]]),
836+
"labels": input_ids.clone(),
837+
"attention_mask": torch.ones_like(input_ids),
838+
}
839+
_, padded = self._prepare(self._module(), **kwargs)
840+
841+
# the id of the padding is the pad token, and the positions continue those of the last document
842+
torch.testing.assert_close(padded["input_ids"], torch.tensor([[1, 2, 3, 4, 5, 7]]))
843+
torch.testing.assert_close(padded["position_ids"], torch.tensor([[0, 1, 2, 0, 1, 2]]))
844+
torch.testing.assert_close(padded["attention_mask"], torch.tensor([[1, 1, 1, 1, 1, 0]]))
845+
torch.testing.assert_close(padded["labels"], torch.tensor([[1, 2, 3, 4, 5, -100]]))
846+
assert padded["use_cache"] is False
847+
848+
def test_pads_positionally_and_without_a_pad_token(self):
849+
input_ids = torch.tensor([[1, 2, 3, 4, 5]])
850+
args, padded = self._prepare(self._module(pad_token_id=None), (input_ids,), labels=input_ids.clone())
851+
852+
# no `pad_token_id` on the config: the padding token is 0, and a positional input stays positional
853+
torch.testing.assert_close(args[0], torch.tensor([[1, 2, 3, 4, 5, 0]]))
854+
torch.testing.assert_close(padded["labels"], torch.tensor([[1, 2, 3, 4, 5, -100]]))
855+
856+
def test_leaves_a_divisible_sequence_alone(self):
857+
input_ids = torch.tensor([[1, 2, 3, 4, 5, 6]])
858+
_, padded = self._prepare(self._module(), input_ids=input_ids, labels=input_ids.clone())
859+
torch.testing.assert_close(padded["input_ids"], input_ids)
860+
assert "position_ids" not in padded
861+
862+
812863
@unittest.skipUnless(is_torch_greater_or_equal("2.10"), "Context parallelism requires torch>=2.10")
813864
class TestContextParallelConfig(unittest.TestCase):
814865
def test_refuses_pipeline_parallelism(self):
@@ -899,6 +950,20 @@ def test_padded(self):
899950
with self.subTest(packed=packed):
900951
run_distributed(world_size=2, backend="gloo")(_test_padded)(2, None, packed)
901952

953+
def test_padded_sequences(self):
954+
for packed in (False, True):
955+
with self.subTest(packed=packed):
956+
run_distributed(world_size=2, backend="gloo")(_test_matches_unsharded)(2, 1, 1, packed, 7)
957+
958+
def test_padded_sequences_with_four_ranks(self):
959+
run_distributed(world_size=4, backend="gloo")(_test_matches_unsharded)(4, 1, 1, False, 5)
960+
961+
def test_padded_sequences_with_a_padding_mask(self):
962+
run_distributed(world_size=2, backend="gloo")(_test_padded)(2, None, False, 7)
963+
964+
def test_padded_inputs_embeds(self):
965+
run_distributed(world_size=2, backend="gloo")(_test_inputs_embeds)(2, 7)
966+
902967
def test_sliding_window(self):
903968
config = MistralConfig(
904969
hidden_size=32,

0 commit comments

Comments
 (0)