huggingface/transformers · #49570
🚨 TP: give embeddings their own style, shard input embeddings by default, and shard widths that don't divide by the TP size
docs/source/en/perf_infer_gpu_multi.md5 + / 3 −
@@ -106,7 +106,8 @@ The `ParallelInterface` class maps each strategy name you can use in a `tp_plan` ```py class ParallelInterface(GeneralInterface): _global_mapping = {- "embedding_rowwise": RowwiseParallel(input_layouts=Replicate(), output_layouts=Replicate()),+ "embedding_rowwise": EmbeddingParallel(shard_dim=0),+ "embedding_colwise": EmbeddingParallel(shard_dim=1), "colwise_gather_output": ColwiseParallel(input_layouts=Replicate(), output_layouts=Replicate()), "colwise_rep": ColwiseParallel(input_layouts=Replicate(), output_layouts=Replicate()), "colwise": ColwiseParallel(input_layouts=Replicate(), output_layouts=Shard(-1)),@@ -134,8 +135,9 @@ The table below describes each strategy. | Strategy | Description | |---|---|-| `ColwiseParallel` | Shards a weight on its output-feature dim (`Shard(0)` for a 2D `nn.Linear` weight, `Shard(1)` for `nn.Embedding`) and shards a 1D bias. Redistributes the input to `Replicate()`, then places the output at `output_layouts`. |-| `RowwiseParallel` | Shards a weight on its input-feature dim (`Shard(-1)`, or `Shard(0)` for `nn.Embedding`) and replicates the bias, which is added after the reduction. Each rank's forward produces a `Partial()` output holding its share of the sum, and `transform_output_post_forward` redistributes that to `output_layouts`. Reducing to `Replicate()` issues an all-reduce, and reducing to `Shard(1)` issues a reduce-scatter. |+| `ColwiseParallel` | Shards a linear weight on its output-feature dim (`Shard(0)` for a 2D `nn.Linear` weight) and shards a 1D bias. Redistributes the input to `Replicate()`, then places the output at `output_layouts`. |+| `RowwiseParallel` | Shards a linear weight on its input-feature dim (`Shard(-1)`) and replicates the bias, which is added after the reduction. Each rank's forward produces a `Partial()` output holding its share of the sum, and `transform_output_post_forward` redistributes that to `output_layouts`. Reducing to `Replicate()` issues an all-reduce, and reducing to `Shard(1)` issues a reduce-scatter. |+| `EmbeddingParallel` | Shards an `nn.Embedding` with the token ids replicated. `shard_dim=0` (`embedding_rowwise`, the usual choice) shards the vocabulary and all-reduces each rank's partial lookup into the full output. `shard_dim=1` (`embedding_colwise`) shards the hidden dim and all-gathers each rank's slice of the output. | | `PackedColwiseParallel` | A variant of `ColwiseParallel` for fused weights, for example `up_proj` and `gate_proj` packed into `gate_up_proj`. Use `split_factor` when a weight packs a number of layers other than two. | | `PackedRowwiseParallel` | The row-wise counterpart, for weights packed along the final dim. Replicates 1D parameters. | | `SequenceParallel` | Replicates the module's parameters and shards its input on `sequence_dim` (defaults to `1`). Used for norms that operate per-token, such as `LayerNorm` and `RMSNorm`. |src/transformers/configuration_utils.py0 + / 7 −
@@ -507,13 +507,6 @@ def __post_init__(self, **kwargs): if per_layer_config is not None: self.per_layer_config = per_layer_config - # TODO: to support models whose input embedding module is not named `embed_tokens` (e.g. GPT-NeoX's `embed_in`).- if getattr(self, "tie_word_embeddings", False) and self.base_model_tp_plan is not None:- self.base_model_tp_plan = {- **self.base_model_tp_plan,- "embed_tokens": "embedding_rowwise",- }- # Remap layer types if needed remap_legacy_layer_types(config=self) src/transformers/distributed/mixin.py5 + / 0 −
@@ -19,6 +19,8 @@ import warnings from typing import TYPE_CHECKING +import torch+ from ..dynamic_module_utils import custom_object_save from ..utils import is_torch_greater_or_equal, logging from ..utils.hub import create_and_tag_model_card@@ -90,6 +92,9 @@ def init_parallel_plans(self) -> None: 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 {})+ # Shard the input embeddings on the vocabulary unless the plan already names them.+ if self._tp_plan and isinstance(getattr(self, "embed_tokens", None), torch.nn.Embedding):+ self._tp_plan = {"embed_tokens": "embedding_rowwise", **self._tp_plan} for name, module in self.named_children(): if plan := getattr(module, "_ep_plan", None):src/transformers/distributed/tensor_parallel.py82 + / 32 −
@@ -170,6 +170,15 @@ def context_around_forward(self, module, mesh): def transform_output_post_forward(self, module, output, mesh): return output + @staticmethod+ def _from_local_last_dim_shard(local, mesh, size):+ """Create a `DTensor` for a tensor sharded across its last dimension.+ The local shard can have a different size from the other ranks when `size` is not divisible by the TP size."""+ shape = (*local.shape[:-1], size)+ return DTensor.from_local(+ local, mesh, [Shard(-1)], run_check=False, shape=shape, stride=torch.empty(shape, device="meta").stride()+ )+ def install_forward(self, module, mesh): """Install pre / around / post transforms by replacing module.forward.""" original_forward = module.forward@@ -185,7 +194,9 @@ def tp_forward(*args, **kwargs): class ColwiseParallel(TensorParallelLayer):- """Column-wise: weight & bias → Shard(0) (Embedding: Shard(1)); input replicated, output Shard(-1)."""+ """Column-wise for linear layers: weight & bias → Shard(0); input replicated, output Shard(-1).++ The output width (`out_features`) need not divide by the TP size: the last rank then holds a shorter slice.""" def __init__(self, *, input_layouts=None, output_layouts=None, use_local_output: bool = True): self.input_layouts = input_layouts or Replicate()@@ -197,28 +208,16 @@ def should_use_local_tensors(self, module): uses_local_inference_kernel = isinstance(module, torch.nn.Linear) and not torch.is_grad_enabled() return use_local_quantized_path or uses_local_inference_kernel - def validate_param(self, module, param, mesh, parameter_name=None):- meta = module._parameters.get(param)- gathers_output = isinstance(self.output_layouts, Replicate)- if meta is None or not gathers_output:- return-- shard_dim = 1 if isinstance(module, torch.nn.Embedding) else meta.ndim - 2- output_size = meta.shape[shard_dim]- tp_size = mesh.size()- if output_size % tp_size != 0:- parameter_name = parameter_name or param- layer_name = parameter_name.rsplit(".", 1)[0]- raise ValueError(- f"The output size of `{layer_name}` ({output_size}) must be divisible by the tensor parallel size "- f"({tp_size}) when gathering a colwise output."- )+ def install_forward(self, module, mesh):+ if not hasattr(module, "out_features"):+ raise ValueError(f"{type(module).__name__} has no `out_features`: colwise shards linear layers.")+ return super().install_forward(module, mesh) def shard_param(self, module, param, mesh): meta = module._parameters.get(param) if meta is None: return- placement = Shard(1) if isinstance(module, torch.nn.Embedding) else Shard(meta.ndim - 2)+ placement = Shard(meta.ndim - 2) module._parameters[param] = torch.nn.Parameter( distribute_tensor(meta, mesh, [placement], src_data_rank=None), requires_grad=meta.requires_grad,@@ -261,18 +260,19 @@ def transform_output_post_forward(self, module, output, mesh): if self.should_use_local_tensors(module) and self.use_local_output and output_is_local_shard: return output if not isinstance(output, DTensor):- output = DTensor.from_local(output, mesh, [Shard(-1)], run_check=False)+ # `out_features`, not the weight's shape: a packed or quantized weight doesn't show the full width+ output = self._from_local_last_dim_shard(output, mesh, module.out_features) if output.placements != (self.output_layouts,): output = output.redistribute(placements=[self.output_layouts]) return output.to_local() if self.use_local_output else output class RowwiseParallel(TensorParallelLayer):- """Row-wise: weight → Shard(1), bias → Replicate (Embedding: weight → Shard(0)).+ """Row-wise for linear layers: weight → Shard(1), bias → Replicate; input sharded on the last dim. - Linear input is sharded on the last dim; Embedding input is replicated. The module- forward produces a Partial output which the boundary redistribute reduces to- output_layouts (Replicate → allreduce, Shard(1) → reduce-scatter).+ The module forward produces a Partial output which the boundary redistribute reduces to+ output_layouts (Replicate → allreduce, Shard(1) → reduce-scatter). The input width (`in_features`) need not+ divide by the TP size. """ def __init__(self, *, input_layouts=None, output_layouts=None, use_local_output: bool = True):@@ -289,27 +289,32 @@ def shard_param(self, module, param, mesh): meta = module._parameters.get(param) if meta is None: return- if isinstance(module, torch.nn.Embedding):- placement = Shard(0)- else:- # bias is replicated (added after the row-reduce); weight shards on input dim (-1)- placement = Replicate() if param == "bias" else Shard(-1)+ # bias is replicated (added after the row-reduce); weight shards on input dim (-1)+ placement = Replicate() if param == "bias" else Shard(-1) module._parameters[param] = torch.nn.Parameter( distribute_tensor(meta, mesh, [placement], src_data_rank=None), requires_grad=meta.requires_grad, ) + def install_forward(self, module, mesh):+ if not hasattr(module, "in_features"):+ raise ValueError(f"{type(module).__name__} has no `in_features`: rowwise shards linear layers.")+ return super().install_forward(module, mesh)+ def transform_inputs_pre_forward(self, module, args, kwargs, mesh):- # Embedding runtime sharding needs a replicated input; Linear needs Shard(-1).- desired = Replicate() if isinstance(module, torch.nn.Embedding) else Shard(-1)+ desired = Shard(-1) x = args[0] # A local kernel can use a plain input when the previous layer already split it. # avoid a redundant Tensor -> DTensor -> Tensor round trip. input_has_desired_layout = self.input_layouts == desired if self.should_use_local_tensors(module) and input_has_desired_layout and not isinstance(x, DTensor): return args, kwargs if not isinstance(x, DTensor):- x = DTensor.from_local(x, mesh, [self.input_layouts], run_check=False)+ if isinstance(self.input_layouts, Shard):+ # the input is this rank's slice of the module's declared `in_features`+ x = self._from_local_last_dim_shard(x, mesh, module.in_features)+ else:+ x = DTensor.from_local(x, mesh, [self.input_layouts], run_check=False) if x.placements != (desired,): x = x.redistribute(placements=[desired]) if self.should_use_local_tensors(module):@@ -360,6 +365,50 @@ def transform_output_post_forward(self, module, output, mesh): return output +class EmbeddingParallel(TensorParallelLayer):+ """Embedding with the token ids replicated on every rank. `shard_dim=0` is vocab-parallel: each rank holds a slice+ of the vocabulary, and the ranks' partial lookups are summed. `shard_dim=1` shards the hidden dim: each rank looks+ up a slice of every vector, and the slices are gathered. The sharded dim need not divide by the TP size."""++ def __init__(self, *, shard_dim: int = 0, use_local_output: bool = True):+ self.shard_dim = shard_dim+ self.use_local_output = use_local_output++ def should_use_local_tensors(self, module):+ return getattr(module, "_hf_quantized_needs_local_tp", False)++ def shard_param(self, module, param, mesh):+ meta = module._parameters.get(param)+ if meta is None:+ return+ module._parameters[param] = torch.nn.Parameter(+ distribute_tensor(meta, mesh, [Shard(self.shard_dim)], src_data_rank=None),+ requires_grad=meta.requires_grad,+ )++ def transform_inputs_pre_forward(self, module, args, kwargs, mesh):+ x = args[0]+ if self.should_use_local_tensors(module) and not isinstance(x, DTensor):+ return args, kwargs+ if not isinstance(x, DTensor):+ x = DTensor.from_local(x, mesh, [Replicate()], run_check=False)+ elif x.placements != (Replicate(),):+ x = x.redistribute(placements=[Replicate()])+ if self.should_use_local_tensors(module):+ x = x.to_local()+ return (x,) + args[1:], kwargs++ def transform_output_post_forward(self, module, output, mesh):+ if not isinstance(output, DTensor):+ if self.shard_dim == 0:+ output = DTensor.from_local(output, mesh, [Partial()], run_check=False)+ else:+ output = self._from_local_last_dim_shard(output, mesh, module.embedding_dim)+ if output.placements != (Replicate(),):+ output = output.redistribute(placements=[Replicate()])+ return output.to_local() if self.use_local_output else output++ class ReplicatedWithGradAllReduce(TensorParallelLayer): """Replicated parameter whose gradient is partial. @@ -945,7 +994,8 @@ class ParallelInterface(GeneralInterface): _global_mapping = ( {- "embedding_rowwise": RowwiseParallel(input_layouts=Replicate(), output_layouts=Replicate()),+ "embedding_rowwise": EmbeddingParallel(shard_dim=0),+ "embedding_colwise": EmbeddingParallel(shard_dim=1), "colwise_gather_output": ColwiseParallel(input_layouts=Replicate(), output_layouts=Replicate()), "colwise_rep": ColwiseParallel(input_layouts=Replicate(), output_layouts=Replicate()), "colwise": ColwiseParallel(input_layouts=Replicate(), output_layouts=Shard(-1)),src/transformers/models/doge/configuration_doge.py2 + / 2 −
@@ -61,8 +61,8 @@ class DogeConfig(PreTrainedConfig): "layers.*.mlp.up_proj": "colwise", "layers.*.mlp.down_proj": "rowwise", "layers.*.mlp.router_gate": "colwise_gather_output",- "layers.*.mlp.down_embed": "rowwise_split_input",- "layers.*.mlp.up_embed": "rowwise_split_input",+ "layers.*.mlp.down_embed": "embedding_rowwise",+ "layers.*.mlp.up_embed": "embedding_rowwise", } base_model_pp_plan = { "embed_tokens": (["input_ids"], ["inputs_embeds"]),src/transformers/models/doge/modular_doge.py2 + / 2 −
@@ -91,8 +91,8 @@ class DogeConfig(PreTrainedConfig): "layers.*.mlp.up_proj": "colwise", "layers.*.mlp.down_proj": "rowwise", "layers.*.mlp.router_gate": "colwise_gather_output",- "layers.*.mlp.down_embed": "rowwise_split_input",- "layers.*.mlp.up_embed": "rowwise_split_input",+ "layers.*.mlp.down_embed": "embedding_rowwise",+ "layers.*.mlp.up_embed": "embedding_rowwise", } base_model_pp_plan = { "embed_tokens": (["input_ids"], ["inputs_embeds"]),src/transformers/models/gemma4/configuration_gemma4.py7 + / 7 −
@@ -256,15 +256,15 @@ class Gemma4VisionConfig(PreTrainedConfig): model_type = "gemma4_vision" base_model_tp_plan = {- "encoder.layers.*.self_attn.q_proj": "colwise",- "encoder.layers.*.self_attn.k_proj": "colwise",- "encoder.layers.*.self_attn.v_proj": "colwise",+ "encoder.layers.*.self_attn.q_proj.linear": "colwise",+ "encoder.layers.*.self_attn.k_proj.linear": "colwise",+ "encoder.layers.*.self_attn.v_proj.linear": "colwise", "encoder.layers.*.self_attn.q_norm": "replicated_with_grad_allreduce", "encoder.layers.*.self_attn.k_norm": "replicated_with_grad_allreduce",- "encoder.layers.*.self_attn.o_proj": "rowwise",- "encoder.layers.*.mlp.gate_proj": "colwise",- "encoder.layers.*.mlp.up_proj": "colwise",- "encoder.layers.*.mlp.down_proj": "rowwise",+ "encoder.layers.*.self_attn.o_proj.linear": "rowwise",+ "encoder.layers.*.mlp.gate_proj.linear": "colwise",+ "encoder.layers.*.mlp.up_proj.linear": "colwise",+ "encoder.layers.*.mlp.down_proj.linear": "rowwise", } default_theta = 100.0src/transformers/models/inkling/configuration_inkling.py0 + / 1 −
@@ -76,7 +76,6 @@ class InklingTextConfig(PreTrainedConfig): model_type = "inkling_text" base_config_key = "text_config" base_model_tp_plan = {- "embed_tokens": "embedding_rowwise", "layers.*.mlp.experts.gate_up_proj": "colwise", "layers.*.mlp.experts.down_proj": "rowwise", "layers.*.mlp.experts": "moe_tp_experts",src/transformers/models/inkling/modular_inkling.py0 + / 1 −
@@ -108,7 +108,6 @@ class InklingTextConfig(PreTrainedConfig): model_type = "inkling_text" base_config_key = "text_config" base_model_tp_plan = {- "embed_tokens": "embedding_rowwise", "layers.*.mlp.experts.gate_up_proj": "colwise", "layers.*.mlp.experts.down_proj": "rowwise", "layers.*.mlp.experts": "moe_tp_experts",src/transformers/models/muse_glimmer/configuration_muse_glimmer.py0 + / 1 −
@@ -98,7 +98,6 @@ class MuseGlimmerTextConfig(PreTrainedConfig): model_type = "muse_glimmer_text" keys_to_ignore_at_inference = ["past_key_values"] base_model_tp_plan = {- "embed_tokens": "embedding_rowwise", "layers.*.self_attn.q_proj": "colwise", "layers.*.self_attn.k_proj": "colwise", "layers.*.self_attn.v_proj": "colwise",src/transformers/models/muse_glimmer/modular_muse_glimmer.py0 + / 1 −
@@ -585,7 +585,6 @@ class MuseGlimmerTextConfig(Gemma2Config, PreTrainedConfig): model_type = "muse_glimmer_text" base_model_tp_plan = {- "embed_tokens": "embedding_rowwise", "layers.*.self_attn.q_proj": "colwise", "layers.*.self_attn.k_proj": "colwise", "layers.*.self_attn.v_proj": "colwise",src/transformers/models/nemotron_h/configuration_nemotron_h.py1 + / 0 −
@@ -83,6 +83,7 @@ class NemotronHConfig(PreTrainedConfig): model_type = "nemotron_h" attribute_map = {"layer_types": "layers_block_type", "num_local_experts": "n_routed_experts"} base_model_tp_plan = {+ "embeddings": "embedding_rowwise", "layers.*.mixer.experts.up_proj": "colwise", "layers.*.mixer.experts.down_proj": "rowwise", "layers.*.mixer.experts": "moe_tp_experts",src/transformers/models/qwen4_exp/configuration_qwen4_exp.py1 + / 1 −
@@ -96,7 +96,7 @@ class Qwen4ExpTextConfig(PreTrainedConfig): "hyper_connection_mixer.input_mix_weight_down": "rowwise_split_input", # It's extremely important to shard this embedding as its size is ~45B == 90 GiB - we shard on dim 1, as the checkpoints # for it are sharded on dim0 (this way Concatenate and tp do not work on the same dim and everything is easy)- "layers.*.ple.ple_embedding.ngram_embedding": "colwise_gather_output",+ "layers.*.ple.ple_embedding.ngram_embedding": "embedding_colwise", } base_model_pp_plan = None base_model_ep_plan = {src/transformers/models/qwen4_exp/modular_qwen4_exp.py1 + / 1 −
@@ -141,7 +141,7 @@ class Qwen4ExpTextConfig(Qwen3_5MoeTextConfig): "hyper_connection_mixer.input_mix_weight_down": "rowwise_split_input", # It's extremely important to shard this embedding as its size is ~45B == 90 GiB - we shard on dim 1, as the checkpoints # for it are sharded on dim0 (this way Concatenate and tp do not work on the same dim and everything is easy)- "layers.*.ple.ple_embedding.ngram_embedding": "colwise_gather_output",+ "layers.*.ple.ple_embedding.ngram_embedding": "embedding_colwise", } base_model_fsdp_plan = { "embed_tokens": "free_full_weight",tests/tensor_parallel/test_tensor_parallel.py12 + / 21 −
@@ -234,18 +234,6 @@ def _make_dtensor_shard_op(self, mesh, placement, param_shape, local_shape): op._axis0_size = param_shape[0] return op - def test_colwise_gather_output_rejects_indivisible_out_features(self):- model = torch.nn.Module()- model.lm_head = torch.nn.Linear(8, 99)- model.tp_plan = {"lm_head": "colwise_gather_output"}- device_mesh = self.MockDeviceMesh(world_size=2, rank=0)-- with self.assertRaises(ValueError) as context:- tensor_parallel.apply_tensor_parallelism(model, device_mesh)-- self.assertIn("lm_head", str(context.exception))- self.assertIn("divisible", str(context.exception))- def test_colwise_uneven_local_shapes(self): module = torch.nn.Module() module.register_parameter("weight", torch.nn.Parameter(torch.empty(10, 32)))@@ -274,18 +262,21 @@ def test_rowwise_uneven_local_shapes(self): self.assertEqual(weight_shape, (32, expected_size)) self.assertEqual(bias_shape, (10,)) - def test_embedding_uneven_local_shapes(self):- rowwise_embedding = torch.nn.Embedding(10, 10)- rowwise_placement = self._get_parameter_placements(rowwise_embedding, RowwiseParallel())["weight"]+ def test_linear_styles_reject_embedding(self):+ for style in ("colwise", "rowwise"):+ with self.subTest(style=style):+ with self.assertRaises(ValueError) as context:+ ALL_PARALLEL_STYLES[style].install_forward(torch.nn.Embedding(8, 16), mesh=None)+ self.assertIn("linear layers", str(context.exception)) - colwise_embedding = torch.nn.Embedding(10, 10)- colwise_placement = self._get_parameter_placements(colwise_embedding, ColwiseParallel())["weight"]+ def test_embedding_uneven_local_shapes(self):+ rowwise = self._get_parameter_placements(torch.nn.Embedding(10, 10), ALL_PARALLEL_STYLES["embedding_rowwise"])+ colwise = self._get_parameter_placements(torch.nn.Embedding(10, 10), ALL_PARALLEL_STYLES["embedding_colwise"]) expected_local_sizes = (4, 4, 2) for rank, expected_size in enumerate(expected_local_sizes):- rowwise_shape = self._get_local_shape((10, 10), rowwise_placement, world_size=3, rank=rank)- colwise_shape = self._get_local_shape((10, 10), colwise_placement, world_size=3, rank=rank)-+ rowwise_shape = self._get_local_shape((10, 10), rowwise["weight"], world_size=3, rank=rank)+ colwise_shape = self._get_local_shape((10, 10), colwise["weight"], world_size=3, rank=rank) self.assertEqual(rowwise_shape, (expected_size, 10)) self.assertEqual(colwise_shape, (10, expected_size)) @@ -314,7 +305,7 @@ def test_shard_tensor_shape_consistency(self): }, "embedding_colwise": { "module": torch.nn.Embedding(32, 16),- "style": ColwiseParallel(),+ "style": ALL_PARALLEL_STYLES["embedding_colwise"], "expected_shapes": {"weight": (32, 4)}, }, }tests/test_tensor_parallel_mixin.py27 + / 11 −
@@ -98,14 +98,15 @@ def get_packed_grad_shard(grad, world_size, rank, dim): total_size = grad.shape[dim] # Packed weights have 2 blocks (gate and up) block_size = total_size // 2- shard_block_size = block_size // world_size+ # every shard but the last is ceil(block_size / world_size) long, like DTensor's split+ shard_block_size = -(-block_size // world_size) # Build interleaved indices indices = [] for block_idx in range(2): # gate block, then up block block_offset = block_idx * block_size- start = block_offset + rank * shard_block_size- stop = block_offset + (rank + 1) * shard_block_size+ start = block_offset + min(rank * shard_block_size, block_size)+ stop = block_offset + min((rank + 1) * shard_block_size, block_size) indices.extend(range(start, stop)) # Select along the sharded dimension@@ -301,10 +302,9 @@ def _test_tp_backward_impl(rank, model_path, model_class, atol, rtol): # interleaved slicing grad = get_packed_grad_shard(grad, world_size, rank, dim) else:- # regular slicing- shard_size = grad_tp.size(dim)- start = rank * shard_size- grad = grad.narrow(dim, start, shard_size)+ # regular slicing: every shard but the last is ceil(size / world_size) long+ start = rank * -(-grad.size(dim) // world_size)+ grad = grad.narrow(dim, start, grad_tp.size(dim)) break if not torch.allclose(grad.cpu(), grad_tp.cpu(), atol=atol, rtol=rtol):@@ -549,12 +549,10 @@ def _get_tp_model_class(self): ) def _get_tp_config(self, tie_word_embeddings: bool | None = None):- """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."""+ """The tester's tiny config. Its vocab stays as it is: 99 does not divide by the world size, which is+ the uneven sharding of `lm_head` that real vocabs need.""" 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 if tie_word_embeddings is not None: if hasattr(text_config, "tie_word_embeddings"): text_config.tie_word_embeddings = tie_word_embeddings@@ -599,6 +597,24 @@ def _skip_if_not_supported(self, expert_parallel: bool = False): # if hasattr(config, "vision_config") and config.vision_config is not None: # self.skipTest("VLM models are not yet supported in TP tests") + def test_tp_plan_shards_input_embeddings(self):+ """A model whose TP plan shards its decoder must shard its input embeddings too: replicated, they cost+ `vocab x hidden` on every rank, and a tied `lm_head` sharded on the vocabulary would not match the embedding+ weight it shares. A plan that only covers the `lm_head` is not a decoder plan."""+ config, _ = self.model_tester.prepare_config_and_inputs_for_common()+ for model_class in self.all_model_classes:+ model = model_class(copy.deepcopy(config))+ if not any(key.rsplit(".", 1)[-1] != "lm_head" for key in model.tp_plan):+ continue+ embeddings = model.get_input_embeddings()+ if not isinstance(embeddings, torch.nn.Embedding):+ continue+ name = next(name for name, module in model.named_modules() if module is embeddings)+ self.assertIsNotNone(+ _get_parameter_plan(f"{name}.weight", model.tp_plan, is_weight=True),+ f"{model_class.__name__}: the TP plan leaves `{name}` replicated; shard it with `embedding_rowwise`",+ )+ def test_moe_parallel_plans_shard_experts(self): """An MoE model's expert weights, the bulk of its parameters, must be sharded by every parallel plan it defines. A plan missing them keeps every expert on every rank without changing any output, so no numerical test sees it.