mudler / mudler/vllm.cpp

port(LOAD-SAFETENSORS): loaders across 28 TUs trust config tie_word_embeddings over the checkpoint, so a shipped lm_head is silently discarded

Open
#2,722 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
423
Forks
53
Avg merge
20h 26m
Merged PRs (30d)
310

Description

Row: LOAD-SAFETENSORS (.agents/engine-matrix.md:279, re-resolved by line number before filing)

Found by wave PORTQ-6 (#2718) re-deriving PORT-NOW entry [214], upstream f0c14b4f77 vllm#51665, of 5559679229..e126687a9a. Nothing was executed. This is a static reading of source.

What upstream does

Three mechanisms, of which one is portable here.

(a) The checkpoint outranks the config flag. vllm/config/model.py adds ModelConfig.maybe_untie_word_embeddings(): when hf_config.tie_word_embeddings is true and checkpoint_has_lm_head() says the artifact ships one, the flag is forced false at both config levels. vllm/config/vllm.py calls it. vllm/transformers_utils/config.py adds checkpoint_has_lm_head(), reading safetensors metadata for a name ending lm_head.weight and returning None when undeterminable. Upstream names quantization and fine-tuning tooling as the producers of exactly this artifact.

(b) Re-tie after loading. A new vllm/model_executor/model_loader/weight_tying.py re-ties only if torch.equal(lm_head.weight, embed_tokens.weight), else warns and keeps the loaded head. A memory optimisation plus a warning, not a correctness rule.

(c) AutoWeightsLoader learns about aliased tied params, and ~57 model files drop their manual skip_prefixes.

Mechanisms (a) and (b) are entirely post-pingit show 5559679229:vllm/model_executor/model_loader/weight_tying.py does not exist, and neither maybe_untie nor checkpoint_has_lm_head is at the pin. They are owed by this pin advance. Mechanism (c) edits AutoWeightsLoader, which is at the pin (models/utils.py:173) and was never ported here; that half is a pre-pin hole and a deliberate structural divergence (hand-written per-model loaders), not work this advance owes.

What is here: this tree trusts the config flag

All 24 loader sites that read tie_word_embeddings were enumerated and each was opened. Exactly one implements the post-commit policy, and it argues for it in as many words:

// src/vllm/model_executor/models/qwen3_5_dense_weights.cpp:1143-1153
if (dense_loaders::IsExl3Projection(has, "lm_head")) {
  // A REAL quantized head is preferred over a tied embedding table EVEN when
  // the config declares `tie_word_embeddings: true` ...

src/vllm/model_executor/models/llama_weights.cpp:152-161 carries the same argument, but only on its EXL3 arm; its bf16 arm falls through to else if (!w.tie_word_embeddings) at :161.

Every other site treats config-true as final. The full list, by file:line of the flag read and the guarded load:

gemma_weights.cpp:84,88 · gemma2_weights.cpp:90,94 · gemma3_weights.cpp:101,105 · gemma4_weights.cpp:384,395 · granite_weights.cpp:85,90 · glm4_weights.cpp:112,121 · llama_weights.cpp:137,162 · mistral_weights.cpp:104,113 · qwen3_weights.cpp:160,168 · qwen3_moe_weights.cpp:139,148 · olmo2_weights.cpp:246,257 · stablelm_weights.cpp:115,121 · internlm2_weights.cpp:179,184 · phi3_weights.cpp:137,142 · minicpm3_weights.cpp:116,245 · deepseek_v2_weights.cpp:217,408 · deepseek_v4_weights.cpp:129,1093,1377 · kimi_linear_weights.cpp:369,604 · kimi_k3_weights.cpp:124,236 · nemotron_h_weights.cpp:823,962,1086 · opt_weights.cpp:109,133 · dots3_note.cpp:194,448 · glm5_next_weights.cpp:489 · muse_glimmer_weights.cpp:298,550,781 · minimax_music3_loader.cpp:541,755

Two sites look at the checkpoint with the wrong polarity, which is worse than not looking:

  • minicpm_weights.cpp:83-85w.tie_word_embeddings = RawBool(..., true) || !has_lm_head;. An absent head forces tying; a present head does not force untying.
  • gemma4_weights.cpp:395if (!w.tie_word_embeddings && names.count("lm_head.weight")). The names.count is an AND after the flag, never an override of it.

The forward-side belts do not rescue this. Twenty-odd sites guard with || lm_head.Empty() (gemma.cpp:229, qwen3.cpp:377, deepseek_v2.cpp:644, laguna.cpp:1367,1628, and the rest). The loader never populated lm_head when the flag said tied, so Empty() is true and the belt agrees with the wrong answer.

Consequence: silent wrong output

A checkpoint whose config says tie_word_embeddings: true while the artifact ships a distinct lm_head.weight has its head discarded, and its logits are computed from the embedding table. Shapes match, loading succeeds, nothing is refused. A token gate against a bf16 oracle catches it only if the oracle is post-fix — so the gate that would see it does not exist until the pin advances.

Size

The read side is cheap because the local equivalent of checkpoint_has_lm_head already exists and is already used three ways: where.count("lm_head.weight") (minicpm_weights.cpp:83), names.count("lm_head.weight") (gemma4_weights.cpp:395), and DenseCheckpointHasLmHead (qwen3_5_dense_weights.cpp:1153).

~2 lines of shared helper plus ~2-3 lines at each of ~24 call sites, so ~60 product lines. Tests: a fixture pair per family touched — ~150 lines for all 24, ~40 for the highest-value three (llama, gemma, qwen3). Mechanism (b) is optional and separable.

Zeros, with their controls

  • AutoWeightsLoader / auto_weights_loader over src/, include/, tests/ returns 10 hits, all of them prose inside // comments citing upstream (minicpm_weights.cpp:13,81, qwen3_5_mtp.cpp:154, deepseek_v4_weights.cpp:47,1300, kimi_k3.h:35, deepseek_v4.h:349,452, kimi_linear.h:38, test_deepseek_v4_exl3_loader.cpp:76). No declaration, no instantiation.
  • checkpoint_has_lm_head / CheckpointHasLmHead / maybe_untie / MaybeUntie over the same scope returns 0. Positive control through the identical probe form: DenseCheckpointHasLmHead returns 4 hits, so the form does find this shape of name when it exists.

Ordering hazard, and it is inside the same tranche

PORT-NOW entry [216], 1fe3a1571a vllm#53106, lands two days later and deletes 13 of the lines this commit adds, including or any(qualname.startswith(p) for p in self.skip_prefixes) and the AutoWeightsLoader half's skip_prefixes machinery. Porting [214] and then [216] verbatim in SHA order is install-then-delete for mechanism (c). Since (c) is the half that is not portable here, the hazard costs nothing if only (a) and (b) are taken — but it must not be discovered mid-port.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with DenseCheckpointHasLmHead in src/vllm/model_executor/models/qwen3_5_dense_weights.cpp, then compare the tie_word_embeddings reads in llama_weights.cpp, gemma_weights.cpp, and qwen3_weights.cpp. Review the listed loader sites and add fixture coverage for representative families. Done means a shipped lm_head is retained despite a true config flag, with the existing tests or new fixture tests passing.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend, machine-learning
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.