Skip to content

🚨 TP: give embeddings their own style, shard input embeddings by default, and shard widths that don't divide by the TP size - #49570

Merged
IlyasMoutawwakil merged 16 commits into
mainfrom
tp-embedding-parallel
Oct 19, 2026
Merged

IlyasMoutawwakil merged 16 commits into
mainfrom
tp-embedding-parallel

Conversation

@IlyasMoutawwakil

@IlyasMoutawwakil IlyasMoutawwakil commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

🤗 Transformers CI

What does this PR do?

Gives embeddings their own tensor-parallel style, shards every model's input embeddings by default, and lets layers shard widths that don't divide by the TP size. ColwiseParallel and RowwiseParallel now only handle linear layers.

Warning

Breaking: custom TP plans must use embedding_rowwise (vocabulary sharding) or embedding_colwise (hidden-dimension sharding) for embeddings. Linear styles (colwise* / rowwise*) now raise on embeddings. Eligible models also shard embed_tokens by default, including untied models; explicit plan entries take precedence.

Embeddings get their own style

ColwiseParallel and RowwiseParallel used to branch on isinstance(module, nn.Embedding) in their parameter sharding, input handling and validation. Those branches move to a dedicated EmbeddingParallel(shard_dim), which keeps the token ids replicated:

Plan entry Style Shards Output
embedding_rowwise EmbeddingParallel(shard_dim=0) the vocabulary (vocab-parallel, the usual choice: it shares its layout with a tied, vocab-sharded lm_head) partial lookups are all-reduced
embedding_colwise (new) EmbeddingParallel(shard_dim=1) the hidden dim (for large untied tables, e.g. Qwen4-Exp's ngram_embedding) slices are all-gathered

ColwiseParallel and RowwiseParallel are now linear-only: applying them to a module without out_features / in_features raises a clear error instead of silently taking the embedding path.

Plans that targeted embeddings with linear styles move to the embedding ones:

  • Doge: mlp.down_embed / mlp.up_embed → embedding_rowwise (was rowwise_split_input).
  • Qwen4-Exp: ple.ple_embedding.ngram_embedding → embedding_colwise (was colwise_gather_output).
  • Gemma4 vision: the plan targets the .linear inside the clippable-linear wrappers, which have no in_features/out_features themselves.

Input embeddings are sharded by default

Almost every model replicated its input embeddings while sharding its lm_head. A replicated embedding costs vocab x hidden on every rank (~1 GiB for Llama-3-8B in bf16, ~2 GiB for Llama-3-70B), and a tied model's vocab-sharded lm_head doesn't match the replicated embedding it shares a weight with.

The config used to add "embed_tokens": "embedding_rowwise" only for tied models. That default moves to init_parallel_plans, without the tie_word_embeddings condition: a base model with a TP plan and an nn.Embedding named embed_tokens shards it on the vocabulary, unless its plan already names it. Untied models (most large checkpoints) benefit the most. Composite models (VLMs, encoder-decoders) pick the entry up from their text sub-model's plan, e.g. model.language_model.embed_tokens. Vision towers and models without an embed_tokens embedding (GPT-NeoX's embed_in) get nothing, so no plan carries a key that matches no module.

No model plan is edited for this, and Inkling's and MuseGlimmer's explicit entries are removed as redundant. Models whose only plan is a class-level lm_head entry (AFMoE, DBRX, GPT-OSS, HunYuan, LFM2, PhiMoE) are unchanged: they need full plans first, in a follow-up.

Divisibility

Real vocab sizes often don't divide by the TP size (GPT-2's 50257, Whisper's 51865, GotOcr2's), and colwise_gather_output used to reject them outright. distribute_tensor already splits a parameter unevenly (the last rank holds a shorter slice), and DTensor ops track the real shape. The one gap was the local fast path (inference and quantized kernels): DTensor.from_local assumes every rank holds an equal slice. A small TensorParallelLayer._from_local_last_dim_shard helper wraps the local tensor with its real width, taken from the module's out_features / in_features (or embedding_dim).

Style Width that doesn't divide by the number of ranks
colwise*, rowwise* supported
embedding_rowwise, embedding_colwise supported
packed_colwise, packed_rowwise (MoE experts' fused gate_up_proj) supported: the strided split and the checkpoint loader both give each rank matching ceil-sized slices of gate and up. Verified with a TP forward and backward of a tiny Qwen3-MoE whose expert intermediate size splits 8/8/8/8/4 over 5 ranks.
expert parallelism rejected, as before (num_experts must divide by ep_size)

Cost. Divisible sizes run exactly as before. An uneven width only costs anything where it is gathered (a colwise_gather_output lm_head, embedding_colwise), where DTensor pads and unpads around the all-gather; layers that reduce a full-size Partial (attention, MLP, packed experts, vocab-parallel embeddings) are unaffected. Gathering bf16 logits over 2 GPUs (NCCL), vocab 128257 vs 128256:

Logits gathered Even Uneven
1 token (a decode step) 530 µs 676 µs
2048 tokens 82.6 ms 101.3 ms

generate only gathers the last token's logits, so that's about 0.15 ms per decode step.

Tests

  • New test_tp_plan_shards_input_embeddings in the TP mixin: a static check that a model whose TP plan covers more than the lm_head shards its nn.Embedding input embeddings. With test_tp_plan_matches_params, it passes for every model (258 passed).
  • The TP mixin no longer rounds the tester's vocab up to a multiple of the world size: vocab_size=99 exercises the uneven lm_head in every TP test. The backward test slices the reference gradients (plain and packed) with ceil-sized shards to match.
  • Unit tests cover the linear-only check and uneven local shapes for both embedding shardings.
  • CPU TP/EP mixin tests, with the allowlist forced off: Llama, Qwen3, Gemma3, DeepSeek-V3, Qwen3-MoE pass. Inkling still fails: its embedding module applies a norm to the partial lookup before it's reduced, which main already hits with its explicit embedding_rowwise entry (Inkling isn't in the CI allowlist).

IlyasMoutawwakil and others added 4 commits October 12, 2026 10:55
… styles

`ColwiseParallel` and `RowwiseParallel` no longer special-case `nn.Embedding`; they now require a linear layer.
Embeddings get `EmbeddingRowwiseParallel` (`embedding_rowwise`, vocab-parallel) and `EmbeddingColwiseParallel`
(`embedding_colwise`, hidden-dim parallel, used by qwen4_exp whose ngram table loads as vocab chunks).
Doge's MoE embeddings move to `embedding_rowwise`, and gemma4's vision plan targets the inner `.linear`
of `Gemma4ClippableLinear`, which holds the weights.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he TP size

`DTensor.from_local` assumes even shards, so colwise outputs and rowwise inputs are wrapped with the full
shape taken from the module's `out_features` / `in_features`. The colwise divisibility check goes away, and
the TP tester keeps the tiny vocab of 99 so `lm_head` exercises the uneven split end to end.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`embedding_rowwise` and `embedding_colwise` become `EmbeddingParallel(shard_dim=0)` and
`EmbeddingParallel(shard_dim=1)` instead of two classes, and a hidden-sharded embedding no longer
needs a hidden size divisible by the TP size: its local output is wrapped with
`_from_local_last_dim_shard`, like the linear layers'. The helper always shards the last dim, so it
drops its placement argument.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ranks

The strided split of a packed weight (an MoE experts' fused gate_up_proj) can't hold uneven
slices, so fail up front with a clear error instead of building a misshapen DTensor.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@IlyasMoutawwakil IlyasMoutawwakil changed the title TP embedding parallel TP: give embeddings their own style, and shard linear widths that don't divide by the TP size Oct 13, 2026
The packed-weight divisibility check was unneeded: DTensor's strided split (with
`src_data_rank=None`, as transformers distributes) and the checkpoint loader both give each rank
matching ceil-sized slices of every packed chunk, so an uneven MoE `gate_up_proj` works. Verified
with a TP forward and backward of a tiny Qwen3-MoE whose `moe_intermediate_size=36` splits 8/8/8/8/4
over 5 ranks. The TP mixin's packed-gradient slicing now uses the same ceil-sized shards.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@IlyasMoutawwakil

Copy link
Copy Markdown
Member Author

I added a cost section to discuss whether we should warn the user, bcz it might make sense to drop some tok ids from the vocab for faster inference/training.

Every TP plan now vocab-shards the model's input embeddings: 89 plans with a plain `nn.Embedding`,
the scaled-embedding family (Gemma*, VaultGemma, MiniCPM3, NeoMME, T5Gemma*), Qwen4-Exp's
`embed_tokens`, and the models whose only plan was a colwise `lm_head` (AFMoE, DBRX, GPT-OSS,
HunYuan, LFM2, PhiMoE). Replicated, the embedding cost `vocab x hidden` on every rank, and a tied
`lm_head` sharded on the vocabulary did not match the embedding weight it shares.

`test_tp_plan_shards_input_embeddings` keeps every model with a TP plan sharding its embeddings.
The models that now get TP tests for the first time skip what can't pass: PhiMoE's backward (it
routes by Gumbel sampling in training) and DBRX's tied forward (it doesn't support tying).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
"""

model_type = "afmoe"
base_model_tp_plan = {"embed_tokens": "embedding_rowwise"}

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.

no need to edit every tp plan, we have it by default in configuration_utils.py

   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",
            }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

nice !

@IlyasMoutawwakil IlyasMoutawwakil Oct 14, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

can i drop the tie_word embedding condition ? it's not necessary for sharding the embedding right ? actually checkpoints that aren't tied benefit the most bcz the tied ones might already have the lm_head sharded.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

also maybe better self.base_model_tp_plan = {"embed_tokens": "embedding_rowwise", **self.base_model_tp_plan} is better to let the model's tp plan take precedence (no model overrides it for now tho)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

updated !

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

okay there's a problem in this, setting self.base_model_tp_plan changes it from a class attr to instance attr, changing the nature of the attr and breaking some tests where we patch the class attr.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

adding it in the config to models that don't necessarily tie weights also broke some vlms that do have embed_tokens in the text decoder, but don't have it in the vision encoder so test_tp_plan_matches_params complained.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

i moved it to the distributed mixin

            # 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}

which you might not like 🥲 lmk what u think

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 would be best inside resolve_parallel_plans once I clean this PR #49527

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.

this is ready

IlyasMoutawwakil and others added 3 commits October 14, 2026 11:15
Drop the `tie_word_embeddings` gate on the config-level `embedding_rowwise` default (explicit plan entries win), remove the per-config `embed_tokens` entries, and leave the lm_head-only models out of the embedding rollout; `test_tp_plan_shards_input_embeddings` now only checks decoder plans.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The config-level default added `embed_tokens` to every plan, including vision towers and models whose embedding has another name, which `test_tp_plan_matches_params` rejects. The model now adds `embed_tokens: embedding_rowwise` only when its base model has an `nn.Embedding` of that name; composite models pick it up from their text sub-model's plan. The tied-embeddings config default is left as on main for backward compatibility.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`init_parallel_plans` now adds the same `embed_tokens` entry, tied or not, so the config-level one only duplicated it (and overrode a plan's own `embed_tokens` style).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@IlyasMoutawwakil IlyasMoutawwakil changed the title TP: give embeddings their own style, and shard linear widths that don't divide by the TP size TP: give embeddings their own style, shard input embeddings by default, and shard widths that don't divide by the TP size Oct 14, 2026

@ArthurZucker ArthurZucker left a comment

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.

BTW this IS breaking we need a small alert! And prob ping @hmellor

Comment on lines +87 to +88
# 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):

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.

arf yeah would be last resort!
I expect us to move more default plan for defaults so finding a solution is welcome 👁️

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

the explicit solution is to name it in every plan 😆 see conv in #49570 (comment)

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 Know !

return output


class EmbeddingParallel(TensorParallelLayer):

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.

❤️ exactly what we needed for al ong time ty!

"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",

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.

ow what?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yep 🥲 q_proj is a Gemma4ClippableLinear wrapper; the actual nn.Linear and weight live at q_proj.linear

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

was not deeply involved in gemma4 but wow indeedd.. super weird 😢

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.

yeah I am surprised it even worked before

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.

we will be able to catch those errors/regression soon with the distributed CI dashboard

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yeah I am surprised it even worked before

yeh normally we have a test that checks for plans matching params/modules, will check how it got past it. the encoder.layers.*.self_attn.q_proj is a module but now with the distinct linear/embedding colwise/rowwise plans we can check each plan is used with the class it's meant for for example.

@IlyasMoutawwakil IlyasMoutawwakil changed the title TP: give embeddings their own style, shard input embeddings by default, and shard widths that don't divide by the TP size 🚨 TP: give embeddings their own style, shard input embeddings by default, and shard widths that don't divide by the TP size Oct 16, 2026
@IlyasMoutawwakil

Copy link
Copy Markdown
Member Author

@ArthurZucker I added a warning with what's breaking + the 🚨

@hmellor

hmellor commented Oct 16, 2026

Copy link
Copy Markdown
Member

Thanks for the ping, I don't think this will cause me problems in vLLM.

I surgically replace all nn.Embeddings with vLLM's VocabParallelEmbedding/ParallelLMHead which have their TP behaviour built in, so I don't need the TP plan for these layers.

@ArthurZucker ArthurZucker left a comment

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.

small nit as the func is not really readable / understandable but ok

"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",

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.

yeah I am surprised it even worked before

@github-actions

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: doge, gemma4, inkling, muse_glimmer, nemotron_h, qwen4_exp

@IlyasMoutawwakil
IlyasMoutawwakil added this pull request to the merge queue Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

CI recap

Dashboard: View test results in Grafana
Latest run: 39218537734:1
Result: success | Jobs: 15 | Tests: 206,374 | Failures: 0 | Duration: 13h 6m

Merged via the queue into main with commit 718d12a Oct 19, 2026
113 checks passed
@IlyasMoutawwakil
IlyasMoutawwakil deleted the tp-embedding-parallel branch October 19, 2026 08:14
IlyasMoutawwakil added a commit that referenced this pull request Oct 20, 2026
Main gives embeddings their own TP style (#49570), shards input embeddings over the vocabulary by default, and lets colwise widths split unevenly. Quantized tables have no vocab-parallel lookup, so the quantizer turns the default `embedding_rowwise` into `embedding_colwise`; `EmbeddingParallel` rejects an explicit vocab shard and keeps the whole-scale-group check. A per-expert slice is any list entry with fewer dims than its parameter (per-expert 0-dim scales into an (E, n) param), not only one fewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BenjaminBossan

Copy link
Copy Markdown
Member

Just FYI, this broke some TP tests in PEFT. The fix seems to be simple enough though: huggingface/peft#4050.

If one of the TP experts (@3outeille or @michaelbenayoun) could confirm that this is valid, it would be great. Not sure if we need updates for LoRA embedding layers too.

ydshieh added a commit that referenced this pull request Nov 2, 2026
…sformers backend

transformers PR #49570 introduced embedding_rowwise/embedding_colwise TP
styles; the pinned SGLang commit predates this and raises ValueError for
unknown styles, causing both model-loading test 3 and transformers-backend
to crash (disguised as exit 137 via SIGQUIT propagation, not OOM).

Patch _normalize_tp_plan via regex to wrap _normalize_tp_style in
try/except and skip unknown styles.  For TP=1 CPU CI the module simply
runs unreplaced, which is correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ydshieh added a commit that referenced this pull request Nov 3, 2026
The SGLang transformers backend does not yet support the embedding_rowwise
TP style introduced in transformers PR #49570.  Rather than patching SGLang
at CI runtime, leave a TODO pointing to the right fix location in SGLang.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants