Always tie structural weights, regardless of tie_word_embeddings - #47620
Always tie structural weights, regardless of tie_word_embeddings#47620jiqing-feng wants to merge 4 commits into
tie_word_embeddings#47620Conversation
`tie_word_embeddings` only controls whether the output embeddings are tied to the input embeddings, but `get_expanded_tied_weights_keys` was returning an empty mapping whenever it was False, dropping the structural entries as well. For encoder-decoder models, `encoder.embed_tokens` and `decoder.embed_tokens` are aliases of `shared` and are absent from the checkpoints, so they ended up randomly initialized (e.g. longt5, which has `tie_word_embeddings=False`). Only drop the entries pointing at `get_output_embeddings()` instead.
`tie_word_embeddings` only controls whether the output embeddings are tied to the input embeddings, but `get_expanded_tied_weights_keys` was returning an empty mapping whenever it was False, dropping the structural entries as well. For encoder-decoder models, `encoder.embed_tokens` and `decoder.embed_tokens` are aliases of `shared` and are absent from the checkpoints, so they ended up randomly initialized (e.g. longt5, which has `tie_word_embeddings=False`). Keep the entries tying an input embedding to another input embedding, and only drop the ones targeting the output head.
bd66db0 to
84193d1
Compare
`tie_word_embeddings=False` only unties the output head; the encoder/decoder embedding aliases are structural and stay tied. These two counts were 2 from huggingface#26292/huggingface#26422 until huggingface#41580 bumped them to 3/4 to match the all-or-nothing gate.
zucchini-nlp
left a comment
There was a problem hiding this comment.
Not 100% sure since I didn't check all official LongT5 ckpt, prob it is same as other T5 models that tie weights always? We had issue with T6 in teh past and hardcoded the value like this
ErenAta16
left a comment
There was a problem hiding this comment.
Ran the filter's predicate against the real _tied_weights_keys on 2ef79f8 and it picks exactly the intended split, but T5 shows a case the FSMT test doesn't cover.
T5ForConditionalGeneration with tie_word_embeddings=False, applying the new predicate by hand:
| entry | parent modules | kept |
|---|---|---|
lm_head.weight → shared.weight |
(Linear, Embedding) |
no |
encoder.embed_tokens.weight → shared.weight |
(Embedding, Embedding) |
yes |
decoder.embed_tokens.weight → shared.weight |
(Embedding, Embedding) |
yes |
That's the behaviour the PR argues for: the nn.Linear head stays gated on the flag, the two input-embedding aliases survive. FSMT splits the same way — decoder.output_projection is nn.Linear at modeling_fsmt.py:534 so it drops, and the encoder.embed_tokens → decoder.embed_tokens pair is (Embedding, Embedding) so it stays.
Worth adding a T5 case next to the FSMT one, because T5 exercises something FSMT can't: two targets sharing one source, where the dropped entry (lm_head) and the kept entries all point at shared.weight. FSMT's mapping has the source as decoder.embed_tokens.weight for both, so it never tests a partially-filtered fan-out. A regression that dropped the whole mapping when any entry fails the predicate would pass the FSMT test and fail on T5.
Two things about the predicate itself:
modules.get(...) returns None for a name that isn't a module, and isinstance(None, nn.Embedding) is False, so a regex-pattern key — which _tied_weights_keys explicitly supports, that's what the expansion below exists for — gets silently dropped when tie_word_embeddings=False. d_fine is called out in the comments a few lines down as having "complicated regex patterns". Those patterns won't rsplit into a real module name, so if any model has a regex-form entry that is structurally an embedding alias, this filter removes it rather than expanding it. Might be worth resolving the regex first, or at least a comment saying regex-form entries are intentionally treated as head-tying.
self.named_modules(remove_duplicate=False) is built on every call, and get_expanded_tied_weights_keys is called per submodule from the all_submodels=True branch. Only on the tie_word_embeddings=False path, so it's not the common one — just noting it's inside a loop.
Measured with real model classes (T5ForConditionalGeneration, FSMTForConditionalGeneration) on tiny configs, transformers 5.13.1, applying the predicate to _tied_weights_keys as read from main @ 2ef79f8. I did not run the modified get_expanded_tied_weights_keys end to end — the installed 5.13.1 predates the if not tie_word_embeddings: return {} early return this PR is rewriting, so its output isn't comparable. The parent-module types and the mappings above are from the current source.
|
Thanks for the detailed check — the predicted behaviour matches what I measured. Added the T5 test T5 test. Added One correction on the motivation though: FSMT does exercise a partially-filtered fan-out. Its two Regex-form keys. You're right that they don't resolve to a module and get dropped. I scanned all So there's no behaviour change today, and dropping it is semantically correct since
|
|
[For maintainers] Suggested jobs to run (before merge) run-slow: fsmt, mbart, t5 |
Problem
get_expanded_tied_weights_keysreturns{}as soon asconfig.tie_word_embeddingsisFalse, droppingevery entry of
_tied_weights_keys. But that dict mixes two kinds of entries:{"lm_head.weight": "model.shared.weight"}; this is what the config flag is meant to gate.{"encoder.embed_tokens.weight": "shared.weight", ...}; module aliasing the architecturerelies on, not configurable.
So for encoder-decoder models with
tie_word_embeddings=False, the encoder/decoder embeddings end uprandomly initialized.
longt5(Stancld/longt5-tglobal-large-16384-pubmed-3k_steps) generates'muralvoenseenseensevo(0)(0)(0)...'; loading reportsencoder.embed_tokens.weight | MISSINGand theirstdis 1.00 instead of 8.92. The Hub checkpoint only storesshared.weight+lm_head.weight, so thisis not recoverable. Regressed in #41541 / #41580.
Fix
Keep the entries whose both ends are
nn.Embedding, drop the rest:Output heads are
nn.Linear, input embeddings arenn.Embedding, so this isolates the structural entrieswithout relying on naming or
get_output_embeddings(). Of the 362 classes with adict_tied_weights_keys, 46 returnNonefromget_output_embeddings()— including models whose head is notnamed
lm_head(BlipForConditionalGeneration,LxmertForPreTraining,FlavaForPreTraining, ...), whicha name-based rule would have wrongly kept tied.
Test updates
mbart/fsmttest_ensure_weights_are_sharedcount distinctdata_ptr()s undertie_word_embeddings=False. Restored to their pre-#41580 values:6f6095e0cf^6f6095e0cf(#41580)#41580 is the commit that introduced the gate, and it bumped these numbers to match the new behaviour.
Consistent with the checkpoints:
facebook/mbart-large-50-many-to-many-mmtstores onlymodel.shared.weight,facebook/wmt19-en-deonlymodel.encoder.embed_tokens.weight.Verification
RUN_SLOW=1longt5test_summarization→ PASSED (fails onmain);lm_headstays untied.tests/models/{fsmt,mbart,bart,longt5,t5,blip,lxmert,flava}→ green.