Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/transformers/integrations/tensor_parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,10 @@ def shard_tensor(
) -> torch.Tensor:
raise NotImplementedError

def validate_module(self, module: nn.Module, device_mesh, layer_name: str = ""):
"""Raise if the module cannot be sharded with this style on the given mesh."""
pass

def prepare_module_tp(self, module: nn.Module, device_mesh, **kwargs) -> nn.Module:
distribute_module(
module,
Expand Down Expand Up @@ -719,6 +723,16 @@ def __init__(self, gather_output: bool = False, **kwargs):
super().__init__(**kwargs)
self.gather_output = gather_output

def validate_module(self, module: nn.Module, device_mesh, layer_name: str = ""):
out_features = getattr(module, "out_features", None)
if self.gather_output and out_features is not None and out_features % device_mesh.size() != 0:
raise ValueError(
f"`{layer_name}` ({type(module).__name__} with out_features={out_features}) is sharded with "
f"'colwise_gather_output', which requires out_features to be divisible by the number of ranks "
f"({device_mesh.size()}) to all-gather equal-size shards. Resize the weight (e.g. "
f"`model.resize_token_embeddings` for LM heads) or override this module's entry in the tp_plan."
)

Comment on lines +726 to +735

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.

since only this layer uses it, we can not make it generic for now

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.

yeah it's a bit awkward. Wdy suggest? move it the the layer?

def _prepare_input_fn(self, mod, inputs, device_mesh):
input_tensor = inputs[0] if inputs else inputs
return all_reduce_backward(input_tensor, device_mesh)
Expand Down Expand Up @@ -1512,6 +1526,7 @@ def add_tensor_parallel_hooks_to_module(
"""
if current_module_plan is not None:
tp_layer = ALL_PARALLEL_STYLES[current_module_plan]
tp_layer.validate_module(module, device_mesh, layer_name)
try:
tp_layer.prepare_module_tp(module, device_mesh, config=model.config)
except NotImplementedError as e:
Expand Down
20 changes: 13 additions & 7 deletions src/transformers/modeling_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,9 @@ class PreTrainedModel(nn.Module, EmbeddingAccessMixin, ModuleUtilsMixin, PushToH
# models, this attribute is currently defined in respective model code. For base models, it comes from
# `config.base_model_pp_plan` during `post_init`.
_pp_plan: dict[str, tuple[str, str]] = None
# An expert parallel plan used instead of `_tp_plan` when expert parallelism is enabled. For base models, it comes

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 super

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.

can you elaborate? 😁

# from `config.base_model_ep_plan` during `post_init`.
_ep_plan: dict[str, str] = None
# FSDP2 sharding plan of the form `{"layers.*": "free_full_weight"}`. For top-level models, this attribute is
# defined on the head class (e.g. `*ForCausalLM`). For base models, it comes from `config.base_model_fsdp_plan`
# during `post_init`.
Expand Down Expand Up @@ -1415,15 +1418,18 @@ def post_init(self):
"""
# Attach the different parallel plans and tied weight keys to the top-most model, so that everything is
# easily available.
self._tp_plan, self._ep_plan, self._pp_plan, self._fsdp_plan = {}, {}, {}, {}
# Start from the class-level plans (e.g. `{"lm_head": "colwise_rep"}` on `...ForCausalLM` classes), copying
# them as they are mutated below and would otherwise contaminate the class attribute shared by all instances

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.

absolutely, this was already the case.... do you know where the regression comes from?

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.

yeah wrote that mostly for review. Yes, it seems to be coming from a big Revert, likely an oversight #46246

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it's actually in #36677 and then we keep the pattern

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.

Are you sure @3outeille ? it doesn't seem to be there that we set
self._tp_plan, self._ep_plan, self._pp_plan, self._fsdp_plan = {}, {}, {}, {}

self._tp_plan = dict(self._tp_plan or {})
self._ep_plan = dict(self._ep_plan or {})
self._pp_plan = dict(self._pp_plan or {})
self._fsdp_plan = dict(self._fsdp_plan or {})
# If current model is a base model, attach `base_model_tp_plan` and `base_model_pp_plan` from config
if self.base_model is self:
self._pp_plan = self.config.base_model_pp_plan.copy() if self.config.base_model_pp_plan is not None else {}
self._tp_plan = self.config.base_model_tp_plan.copy() if self.config.base_model_tp_plan is not None else {}
self._ep_plan = self.config.base_model_ep_plan.copy() if self.config.base_model_ep_plan is not None else {}
self._fsdp_plan = (
self.config.base_model_fsdp_plan.copy() if self.config.base_model_fsdp_plan is not None else {}
)
self._pp_plan.update(self.config.base_model_pp_plan or {})
self._tp_plan.update(self.config.base_model_tp_plan or {})
self._ep_plan.update(self.config.base_model_ep_plan or {})
self._fsdp_plan.update(self.config.base_model_fsdp_plan or {})
# Current submodel should register its tied weights
self.all_tied_weights_keys = self.get_expanded_tied_weights_keys(all_submodels=False)
# Current submodel should register its `_keep_in_fp32_modules`
Expand Down
27 changes: 27 additions & 0 deletions tests/tensor_parallel/test_tensor_parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
PackedColwiseParallel,
PackedRowwiseParallel,
RowwiseParallel,
add_tensor_parallel_hooks_to_module,
get_packed_weights,
repack_weights,
)
Expand Down Expand Up @@ -166,6 +167,17 @@ def test_tp_plan_none_handling(self):
model.tp_plan = {"model.layers.*.self_attn.q_proj": "colwise"}
self.assertEqual(model.tp_plan, {"model.layers.*.self_attn.q_proj": "colwise"})

def test_post_init_keeps_class_level_plans(self):
"""Class-level plans (e.g. `lm_head` on ForCausalLM classes) must survive post_init alongside the base model plan."""
model_id = "hf-internal-testing/tiny-random-LlamaForCausalLM"
model = AutoModelForCausalLM.from_pretrained(model_id, dtype="auto")

self.assertIn("lm_head", model._tp_plan)
self.assertIn("model.layers.*.self_attn.q_proj", model._tp_plan)
self.assertIn("lm_head", model._pp_plan)
# The merge must not have mutated the class attribute shared by all instances
self.assertEqual(set(type(model)._tp_plan), {"lm_head"})


@is_tensor_parallel_test
class TestTensorParallelLayer(TestCasePlus):
Expand All @@ -181,6 +193,21 @@ def size(self):
def get_local_rank(self):
return self.rank

def test_colwise_gather_output_rejects_indivisible_out_features(self):
device_mesh = self.MockDeviceMesh(world_size=2, rank=0)

with self.assertRaises(ValueError) as context:
add_tensor_parallel_hooks_to_module(
model=SimpleNamespace(config=None),
module=torch.nn.Linear(8, 99),
current_module_plan="colwise_gather_output",
layer_name="lm_head",
device_mesh=device_mesh,
)

self.assertIn("lm_head", str(context.exception))
self.assertIn("divisible", str(context.exception))

def test_colwise_get_expected_sharded_shape(self):
world_size = 3
size = 10 # not divisible by world_size to test edge case
Expand Down
21 changes: 15 additions & 6 deletions tests/test_tensor_parallel_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,15 @@ def _get_tp_model_class(self):
return self.model_tester.causal_lm_class
return self.all_model_classes[0]

def _get_tp_config(self):
"""Tiny config with `vocab_size` rounded up to a multiple of the world size, as sharded dims (typically `lm_head`) have to be split across ranks."""
config = self.model_tester.get_config()
text_config = config.get_text_config()
remainder = text_config.vocab_size % self.tensor_parallel_size
if remainder:
text_config.vocab_size += self.tensor_parallel_size - remainder
return config
Comment on lines +496 to +501

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.

this is because we have typically vocab size 99 on tiny tests, which can't be sharded on 2 ranks obviously

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'm thinking wheter we should automatically extend an embedding if we notice this? So in from pretrained, if we notice

  1. TP
  2. Embeddings that get shared we resize the embeddings before

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.

ah auto-pad you mean? hmm we could ig, I feel like we are already doing it in a couple other places IIRC

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.

Yep, because users will complain I'm sure 😅 Maybe a warning but having users to think leads to problems

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.

but we should pad just before the gather and unpad right after, no?

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.

other idea (sorry for the noise): we should just validate, feels less magic. like if TP is provided with a wrong vocab size just raise from the get-go. Should be in another PR though because it could break existing setups

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 think this is already breaking in itself no? Users could have arbitrary vocab sizes before this

But yea, I can see the validation path - less magic and add how to properly do 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.

ah yes, you're right... before it was silently replicating so of course this will raise. I'll add the validation logic then

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.

IMO, if it does not cost much and we can resize for the user, would be nice. People probably expect us to do so, and we are the ones setting the plan to default to colwise rep.
The main concern is for the gather to not take into account the padding, which is important


def _skip_if_not_supported(self, expert_parallel: bool = False):
"""Check and skip the test if tensor/expert parallel is not supported for this model/environment."""
parallelism = "Expert" if expert_parallel else "Tensor"
Expand Down Expand Up @@ -539,7 +548,7 @@ def _skip_if_not_supported(self, expert_parallel: bool = False):
def test_tp_forward(self):
self._skip_if_not_supported()

config = self.model_tester.get_config()
config = self._get_tp_config()
model_class = self._get_tp_model_class()
atol = self.tensor_parallel_atol
rtol = self.tensor_parallel_rtol
Expand All @@ -555,7 +564,7 @@ def test_tp_forward(self):
def test_tp_backward(self):
self._skip_if_not_supported()

config = self.model_tester.get_config()
config = self._get_tp_config()
model_class = self._get_tp_model_class()
atol = self.tensor_parallel_atol
rtol = self.tensor_parallel_rtol
Expand All @@ -572,7 +581,7 @@ def test_tp_generation(self):
# Test TP generation: unfused checkpoint → conversion mapping (if needed) → TP sharding → model → generate
self._skip_if_not_supported()

config = self.model_tester.get_config()
config = self._get_tp_config()

model_class = self._get_tp_model_class()
atol = self.tensor_parallel_atol
Expand All @@ -594,7 +603,7 @@ def test_tp_generation_quantized(self):
if not is_torchao_available():
self.skipTest("Test requires torchao")

config = self.model_tester.get_config()
config = self._get_tp_config()
model_class = self._get_tp_model_class()
max_new_tokens = 25

Expand All @@ -611,7 +620,7 @@ def test_tp_generation_quantized(self):
def test_ep_forward(self):
self._skip_if_not_supported(expert_parallel=True)

config = self.model_tester.get_config()
config = self._get_tp_config()
model_class = self._get_tp_model_class()
atol = self.tensor_parallel_atol
rtol = self.tensor_parallel_rtol
Expand All @@ -627,7 +636,7 @@ def test_ep_forward(self):
def test_ep_backward(self):
self._skip_if_not_supported(expert_parallel=True)

config = self.model_tester.get_config()
config = self._get_tp_config()
model_class = self._get_tp_model_class()
atol = self.tensor_parallel_atol
rtol = self.tensor_parallel_rtol
Expand Down
Loading