Skip to content

fix: re-tie word embeddings before save to avoid duplicate lm_head - #2798

Merged
brian-dellabetta merged 11 commits into
vllm-project:mainfrom
EdalatiAli:ali/retie_embeddings
Jun 16, 2026
Merged

brian-dellabetta merged 11 commits into
vllm-project:mainfrom
EdalatiAli:ali/retie_embeddings

Conversation

@EdalatiAli

@EdalatiAli EdalatiAli commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

When a model with tied word embeddings is saved while offloaded, the offload conversion (to_accelerate) splits embed_tokens and lm_head into separate parameters. This defeats transformers' save-time de-duplication, so a redundant, bit-identical lm_head.weight gets written.

This re-ties the weights right after to_accelerate and before saving, so the tied head is dropped as intended. It's a cheap attribute reassignment (no onloading) and a no-op for models with untied embeddings.

Test plan

  • Exported an AWQ (sequential-pipeline) checkpoint and confirmed lm_head.weight no longer appears in model.safetensors.index.json.
  • Verified datafree/untied exports are unaffected.
  • passed the added test: pytest tests/llmcompressor/transformers/compression/test_compress_tensor_utils.py -k no_duplicate_tied_lm_head -v

… offloaded models

Signed-off-by: EdalatiAli <aliedalati@cohere.com>
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to llm-compressor. Please add the ready label when the PR is ready for review.

Note: This is required to complete the testing suite, please only add the label once the PR is code complete and local testing has been performed.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6b26ffa3-bab3-40b3-9884-4b9b7b901e5b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The PR fixes a bug where accelerate offloading conversion splits tied weights (such as lm_head), causing duplicate serialization. A new helper function re-ties weights after offloading, and is invoked in the save flow to restore shared parameters before they are written to disk.

Changes

Offloaded weights re-tying

Layer / File(s) Summary
Re-tie weights after accelerate offloading
src/llmcompressor/transformers/compression/compressed_tensors_utils.py
A new _retie_offloaded_weights(model) helper conditionally calls model.tie_weights() when available. It is invoked in modify_save_pretrained after to_accelerate(model) conversion to restore shared parameters split by offloading before serialization.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Suggested labels

bug, transforms

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately describes the main change: re-tying word embeddings before save to prevent duplicate lm_head weights. It directly addresses the primary objective and is concise and specific.
Description check ✅ Passed The description clearly explains the problem (tied word embeddings causing duplicate lm_head when saved while offloaded), the solution (re-tying weights after to_accelerate), and includes a test plan with specific verification steps.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Linked repositories: Your configuration references 1 linked repositories, but your current plan allows 0. Analyzed ``, skipped vllm-project/compressed-tensors.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mergify mergify Bot added the two-reviews When a PR requires two reviews label Jun 4, 2026
@mergify

mergify Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

Your pull request matches the following merge protections and will not be merged until they are valid.

🟢 Require two reviews

Wonderful, this rule succeeded.

PRs labelled "two-reviews" must have at least two approving reviews before merging.

  • #approved-reviews-by >= 2
  • #changes-requested-reviews-by = 0

@coderabbitai coderabbitai Bot added bug Something isn't working transforms Related to transforms-based modifiers like SpinQuant and Quip and removed two-reviews When a PR requires two reviews labels Jun 4, 2026
@mergify mergify Bot added the two-reviews When a PR requires two reviews label Jun 4, 2026
@EdalatiAli EdalatiAli changed the title [Fix] re-tie word embeddings before save to avoid duplicate lm_head fix: re-tie word embeddings before save to avoid duplicate lm_head Jun 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/llmcompressor/transformers/compression/compressed_tensors_utils.py (2)

108-118: ⚖️ Poor tradeoff

Consider adding error handling for robustness during tie_weights() invocation.

The implementation safely checks for the existence and callability of tie_weights() but doesn't handle potential exceptions raised during its invocation. While propagating exceptions is generally appropriate (as a failing tie_weights() indicates a model issue), adding defensive error handling with logging could aid debugging without silently swallowing errors.

♻️ Optional: Add defensive logging for tie_weights failures
 def _retie_offloaded_weights(model: PreTrainedModel):
     """Re-tie weights split by offload conversion so the tied head isn't saved twice.
 
     Offloading gives the input embeddings and a tied head (e.g. ``lm_head``)
     separate parameters, defeating transformers' save-time de-duplication.
     ``tie_weights`` restores the shared parameter and is a no-op for untied models.
     """
     tie_weights = getattr(model, "tie_weights", None)
     if callable(tie_weights):
-        tie_weights()
+        try:
+            tie_weights()
+        except Exception as e:
+            logger.warning(
+                f"Failed to re-tie weights after offload conversion: {e}. "
+                "Model may save duplicate tied weights."
+            )
+            # Re-raise to fail fast if this is critical
+            raise
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/llmcompressor/transformers/compression/compressed_tensors_utils.py`
around lines 108 - 118, The _retie_offloaded_weights function currently calls
model.tie_weights() without handling exceptions; wrap the callable check and
invocation of tie_weights (on model) in a try/except Exception block, obtain a
module logger (e.g., logging.getLogger(__name__)), log the failure with
logger.exception including context that tie_weights failed for this model, and
then re-raise the exception so failures are visible rather than silently
swallowed.

77-79: _retie_offloaded_weights should preserve explicit untying; optionally add a defensive config guard.

The save flow calls _retie_offloaded_weights(model) after to_accelerate(model), and _retie_offloaded_weights currently just unconditionally calls model.tie_weights(). In Hugging Face Transformers, PreTrainedModel.tie_weights() is gated by config.tie_word_embeddings, so if untie_word_embeddings set config.tie_word_embeddings = False, tie_weights() should be a no-op and explicit untying should be preserved.

Optional robustness: make that dependency explicit in _retie_offloaded_weights (also aligning with the helper docstring).

🛡️ Defensive check to preserve explicit untyling
 def _retie_offloaded_weights(model: PreTrainedModel):
     """Re-tie weights split by offload conversion so the tied head isn't saved twice.
 
     Offloading gives the input embeddings and a tied head (e.g. ``lm_head``)
     separate parameters, defeating transformers' save-time de-duplication.
     ``tie_weights`` restores the shared parameter and is a no-op for untied models.
     """
+    # Only retie if embeddings were originally tied
+    if hasattr(model.config, "tie_word_embeddings") and not model.config.tie_word_embeddings:
+        return
+    
     tie_weights = getattr(model, "tie_weights", None)
     if callable(tie_weights):
         tie_weights()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/llmcompressor/transformers/compression/compressed_tensors_utils.py`
around lines 77 - 79, The _retie_offloaded_weights call currently always invokes
model.tie_weights(), which will override an explicit untying; update
_retie_offloaded_weights to check the model.config.tie_word_embeddings flag
before calling model.tie_weights() so it becomes a no-op when untied (preserving
explicit untying), and optionally add a defensive guard/logging in
_retie_offloaded_weights's docstring or runtime to clarify this behavior;
reference the _retie_offloaded_weights function and the model.tie_weights() call
and the model.config.tie_word_embeddings flag when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/llmcompressor/transformers/compression/compressed_tensors_utils.py`:
- Around line 108-118: The _retie_offloaded_weights function currently calls
model.tie_weights() without handling exceptions; wrap the callable check and
invocation of tie_weights (on model) in a try/except Exception block, obtain a
module logger (e.g., logging.getLogger(__name__)), log the failure with
logger.exception including context that tie_weights failed for this model, and
then re-raise the exception so failures are visible rather than silently
swallowed.
- Around line 77-79: The _retie_offloaded_weights call currently always invokes
model.tie_weights(), which will override an explicit untying; update
_retie_offloaded_weights to check the model.config.tie_word_embeddings flag
before calling model.tie_weights() so it becomes a no-op when untied (preserving
explicit untying), and optionally add a defensive guard/logging in
_retie_offloaded_weights's docstring or runtime to clarify this behavior;
reference the _retie_offloaded_weights function and the model.tie_weights() call
and the model.config.tie_word_embeddings flag when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 550f9d57-b1f0-467e-8bee-54fb72d20b84

📥 Commits

Reviewing files that changed from the base of the PR and between e183fed and b18fd39.

📒 Files selected for processing (1)
  • src/llmcompressor/transformers/compression/compressed_tensors_utils.py

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a helper function _retie_offloaded_weights to re-tie weights that were split during offload conversion, preventing duplicate parameters (such as lm_head) from being saved. This helper is integrated into the save_pretrained_wrapper flow. I have no feedback to provide.

@brian-dellabetta brian-dellabetta left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thanks! Will confirm with @kylesayrs

@kylesayrs kylesayrs 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.

Will write up more information in a bit, but temporarily blocking this for now for the following changes:

  1. This logic needs a guard on if model.config.tie_embeddings:. Sometimes oneshot will untie embeddings, as is the case for spinquant, so checking for the presence of tie_weights is not alone.
  2. Some models implement tie_weights, but the implementation raises an error. We should try to catch this case and warn if possible

@kylesayrs

Copy link
Copy Markdown
Collaborator

Outside of the required changes mentioned above, I'll also mention that this usage creates a data configuration that accelerate isn't expecting and might break in the future, depending on the implementation of save_pretrained

tie_weights will simply reassign the weight parameter of lm_head to the value of embed_tokens. This works because transformers, when saving, only checks the id of the meta tensor (source, I actually wrote some of this logic).

However, typically accelerate expects all tied tensors to not only share the same meta tensor value, but also that you specify them in an explicit tied_params_map (source). By doing the above, we break this assumption.

This is nbd, but it might break in the future if accelerate changes how it implements save_pretrained.

I think a more stable pattern might be to perform the tying before converting to accelerate

if model.config.tie_embeddings:  # might need a more robust check
  _retie_offloaded_weights(model)  # add warning/logger if weights are retied

to_accelerate(model)

Signed-off-by: EdalatiAli <aliedalati@cohere.com>
Signed-off-by: EdalatiAli <aliedalati@cohere.com>
@EdalatiAli

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review @kylesayrs @brian-dellabetta ! Addressed the three points:

  1. Guard on tie config_retie_offloaded_weights now early-returns unless config.tie_word_embeddings is set (checked via get_text_config(decoder=True) so multimodal configs work too). Models that oneshot explicitly unties (e.g. SpinQuant via untie_word_embeddings, which sets tie_word_embeddings = False) are left untouched, so checking for tie_weights alone is no longer the only condition.
  2. Catch + warn on tie_weights failures — wrapped the call in a try/except that logs a warning instead of failing the save, for models whose tie_weights implementation raises.
  3. Re-tie before to_accelerate — moved the re-tie to run before the offload conversion, per your note about not creating a tied layout that accelerate isn't expecting (shared meta tensor + explicit tied_params_map).

I also added a regression test (test_no_duplicate_tied_lm_head_on_save in test_compress_tensor_utils.py) covering offloaded/non-offloaded × tied/untied, asserting lm_head.weight is dropped on disk for tied models and kept for untied ones.

@kylesayrs kylesayrs 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.

Great job!

@kylesayrs kylesayrs added the ready When a PR is ready for full CI testing before merge label Jun 9, 2026

@brian-dellabetta brian-dellabetta left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @EdalatiAli !

@mergify

mergify Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

The quality checks have failed. Please run make style and make quality under
the root directory to adddress the lint failures. You will need to install the
dev optional install to get the required linting packages:
https://github.com/vllm-project/llm-compressor/blob/main/CONTRIBUTING.md

@mergify mergify Bot removed the quality-failed label Jun 10, 2026
@mergify

mergify Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

The quality checks have failed. Please run make style and make quality under
the root directory to adddress the lint failures. You will need to install the
dev optional install to get the required linting packages:
https://github.com/vllm-project/llm-compressor/blob/main/CONTRIBUTING.md

@brian-dellabetta

Copy link
Copy Markdown
Contributor

@EdalatiAli can you run make style / make quality?

Signed-off-by: EdalatiAli <aliedalati@cohere.com>
@mergify mergify Bot removed the quality-failed label Jun 10, 2026
@brian-dellabetta
brian-dellabetta enabled auto-merge (squash) June 10, 2026 19:56
@brian-dellabetta

brian-dellabetta commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

pytest -s tests/llmcompressor/transformers/gptq/test_gptq_oneshot.py is failing with this warning in the logs:

[transformers] The tied weights mapping and config for this model specifies to tie model.embed_tokens.weight to lm_head.weight, but both are present in the checkpoints with different values, so we will NOT tie them. You should update the config with `tie_word_embeddings=False` to silence this warning.

Apparently our smoke model is incorrectly configured, its config has "tie_word_embeddings": true but both model.embed_tokens.weight and lm_head.weight appear in model.safetensors. The test succeeds with meta-llama/Llama-3.2-1B-Instruct.

I will confirm with original creator of smoke model that changing won't break anything or get overwritten by some other process

@brian-dellabetta

brian-dellabetta commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Updating the model config to tie_word_embeddings:False causes new errors with tests/llmcompressor/transformers/compression/test_compress_tensor_utils.py::test_no_duplicate_tied_lm_head_on_save. i've reverted the model config.json for now, will need to debug this next week and then we can get it in after release

brian-dellabetta and others added 2 commits June 15, 2026 16:40
Signed-off-by: Brian Dellabetta <bdellabe@redhat.com>
@EdalatiAli

Copy link
Copy Markdown
Contributor Author

Thank you @brian-dellabetta for fixing the issue with the failed test! Would be great to merge the PR if there are no other concerns

@brian-dellabetta

Copy link
Copy Markdown
Contributor

Thank you @brian-dellabetta for fixing the issue with the failed test! Would be great to merge the PR if there are no other concerns

Thanks @EdalatiAli , I'll cover this with Kyle when we meet this afternoon, hope to get it in today

@brian-dellabetta
brian-dellabetta merged commit b2a5795 into vllm-project:main Jun 16, 2026
13 of 14 checks passed
@kylesayrs

Copy link
Copy Markdown
Collaborator

I think this solution may be incorrect, as it messes with cases where only the input embedding is quantized

@brian-dellabetta

Copy link
Copy Markdown
Contributor

I think this solution may be incorrect, as it messes with cases where only the input embedding is quantized

Yeah, this PR and the PR that introduced embedding quantization ( #2830 and vllm-project/compressed-tensors#718) were considered independently, so we're hitting an issue at the intersection of them. But we can resolve in #2835

kylesayrs added a commit that referenced this pull request Jul 7, 2026
…cate lm_head (#2835)

## Summary

Quantizing a **tied** model's embedding is currently counterproductive.
When embeddings are targeted, `QuantizationMixin.start_calibration`
unties them (existing upstream behavior) so the input and output
embeddings can be quantized independently — but for a tied model the one
shared matrix then becomes a packed table **plus** a new full-precision
`lm_head`, so the checkpoint grows. This matters most for small SOTA
models (`Qwen3-0.6B`, `LFM2.5-350M`) where that shared matrix dominates
file size.

Per review (thanks @kylesayrs), this adds a **save-time** step that
re-ties the input and output embeddings when they were quantized to
identical values — the same matrix quantized twice — so they serialize
as a single shared table:

- **Re-tie identically-quantized embeddings before saving**
(`compressed_tensors_utils.py`). If the (compressed) input and output
embeddings have identical tensors, the output embedding's storage is
aliased to the input's so transformers' save-time de-duplication writes
a **single shared table**, and `tie_word_embeddings` is restored so the
tie is reconstructed at load. If they differ — quantized differently, or
only one was quantized — they are left untied and both are kept,
preserving the model's integrity. The decision is driven purely by
tensor equality, so nothing needs to track the original tie state.

The re-tie uses the existing compressed-tensors offload API —
`setattr`/`getattr` under `OffloadCache.disable_onloading()` — so the
two modules share one tensor with no copy and no new compressed-tensors
API. (The `disable_onloading` context can be removed once
compressed-tensors#709 makes `__setitem__` a non-copying replacement.)

### Relationship to #2798

Builds on (does not duplicate) the merged #2798. #2798 re-ties
**uncompressed** tied weights that offloading splits apart, via
`model.tie_weights()`. That path can't handle a **compressed**
embedding, which has no plain `.weight` to tie to (`tie_weights()`
raises and logs a warning), and it only runs while `tie_word_embeddings`
is still set. This PR covers exactly that gap. The two run back-to-back
at save and are complementary: `_retie_offloaded_weights` handles the
dense case, `_retie_quantized_embeddings` the compressed one.

### Why this is useful

The single packed table is reused for **both** the input lookup and the
logits matmul by runtimes that support it — vLLM consumer support:
vllm-project/vllm#45535. Accuracy (lm-eval): tied **W8** embedding is
near-lossless (Qwen3-0.6B wikitext bits-per-byte −0.0%, LFM2.5-350M
+0.45%); **W4** is heavier on these compression-sensitive models (+1.2%
/ +4.4%).

## Test Plan

```
pytest tests/llmcompressor/transformers/compression/test_compress_tensor_utils.py -k tied
```

`test_tied_quantized_embedding_no_duplicate_head` quantizes
`Qwen3-0.6B`'s embedding **and** `lm_head` (data-free) and asserts the
saved checkpoint stays tied (`tie_word_embeddings=True`) with the packed
embedding present and **no** `lm_head` weight tensors.

## Test Result

- New test passes.
- Existing `test_no_duplicate_tied_lm_head_on_save` (tied/untied ×
offload) still passes — an explicitly-untied model keeps its separate
head (the re-tie is scoped to the compressed case).
- End-to-end: the produced `Qwen3-0.6B` checkpoint has a single shared
packed table (no fp16 head), config declares both embeddings quantized,
`tie_word_embeddings=True`, 930 MB vs 1.5 GB fp16 — loads and generates
coherently in vLLM (with #45535).
- `ruff check` / `ruff format --check` clean.

---

This change was developed with AI assistance (Claude). All changed lines
were reviewed by the submitter.

---------

Signed-off-by: Karthik Kothuri <karthikkothuri2009@gmail.com>
Co-authored-by: Kyle Sayers <kylesayrs@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
HDCharles pushed a commit that referenced this pull request Aug 10, 2026
…2798)

## Summary
When a model with tied word embeddings is saved while offloaded, the
offload conversion (`to_accelerate`) splits `embed_tokens` and `lm_head`
into separate parameters. This defeats transformers' save-time
de-duplication, so a redundant, bit-identical `lm_head.weight` gets
written.

This re-ties the weights right after `to_accelerate` and before saving,
so the tied head is dropped as intended. It's a cheap attribute
reassignment (no onloading) and a no-op for models with untied
embeddings.

## Test plan
- Exported an AWQ (sequential-pipeline) checkpoint and confirmed
`lm_head.weight` no longer appears in `model.safetensors.index.json`.
- Verified datafree/untied exports are unaffected.
- passed the added test: `pytest
tests/llmcompressor/transformers/compression/test_compress_tensor_utils.py
-k no_duplicate_tied_lm_head -v`

---------

Signed-off-by: EdalatiAli <aliedalati@cohere.com>
Signed-off-by: Brian Dellabetta <bdellabe@redhat.com>
Co-authored-by: Brian Dellabetta <brian-dellabetta@users.noreply.github.com>
Co-authored-by: Brian Dellabetta <bdellabe@redhat.com>
HDCharles pushed a commit that referenced this pull request Aug 10, 2026
…cate lm_head (#2835)

## Summary

Quantizing a **tied** model's embedding is currently counterproductive.
When embeddings are targeted, `QuantizationMixin.start_calibration`
unties them (existing upstream behavior) so the input and output
embeddings can be quantized independently — but for a tied model the one
shared matrix then becomes a packed table **plus** a new full-precision
`lm_head`, so the checkpoint grows. This matters most for small SOTA
models (`Qwen3-0.6B`, `LFM2.5-350M`) where that shared matrix dominates
file size.

Per review (thanks @kylesayrs), this adds a **save-time** step that
re-ties the input and output embeddings when they were quantized to
identical values — the same matrix quantized twice — so they serialize
as a single shared table:

- **Re-tie identically-quantized embeddings before saving**
(`compressed_tensors_utils.py`). If the (compressed) input and output
embeddings have identical tensors, the output embedding's storage is
aliased to the input's so transformers' save-time de-duplication writes
a **single shared table**, and `tie_word_embeddings` is restored so the
tie is reconstructed at load. If they differ — quantized differently, or
only one was quantized — they are left untied and both are kept,
preserving the model's integrity. The decision is driven purely by
tensor equality, so nothing needs to track the original tie state.

The re-tie uses the existing compressed-tensors offload API —
`setattr`/`getattr` under `OffloadCache.disable_onloading()` — so the
two modules share one tensor with no copy and no new compressed-tensors
API. (The `disable_onloading` context can be removed once
compressed-tensors#709 makes `__setitem__` a non-copying replacement.)

### Relationship to #2798

Builds on (does not duplicate) the merged #2798. #2798 re-ties
**uncompressed** tied weights that offloading splits apart, via
`model.tie_weights()`. That path can't handle a **compressed**
embedding, which has no plain `.weight` to tie to (`tie_weights()`
raises and logs a warning), and it only runs while `tie_word_embeddings`
is still set. This PR covers exactly that gap. The two run back-to-back
at save and are complementary: `_retie_offloaded_weights` handles the
dense case, `_retie_quantized_embeddings` the compressed one.

### Why this is useful

The single packed table is reused for **both** the input lookup and the
logits matmul by runtimes that support it — vLLM consumer support:
vllm-project/vllm#45535. Accuracy (lm-eval): tied **W8** embedding is
near-lossless (Qwen3-0.6B wikitext bits-per-byte −0.0%, LFM2.5-350M
+0.45%); **W4** is heavier on these compression-sensitive models (+1.2%
/ +4.4%).

## Test Plan

```
pytest tests/llmcompressor/transformers/compression/test_compress_tensor_utils.py -k tied
```

`test_tied_quantized_embedding_no_duplicate_head` quantizes
`Qwen3-0.6B`'s embedding **and** `lm_head` (data-free) and asserts the
saved checkpoint stays tied (`tie_word_embeddings=True`) with the packed
embedding present and **no** `lm_head` weight tensors.

## Test Result

- New test passes.
- Existing `test_no_duplicate_tied_lm_head_on_save` (tied/untied ×
offload) still passes — an explicitly-untied model keeps its separate
head (the re-tie is scoped to the compressed case).
- End-to-end: the produced `Qwen3-0.6B` checkpoint has a single shared
packed table (no fp16 head), config declares both embeddings quantized,
`tie_word_embeddings=True`, 930 MB vs 1.5 GB fp16 — loads and generates
coherently in vLLM (with #45535).
- `ruff check` / `ruff format --check` clean.

---

This change was developed with AI assistance (Claude). All changed lines
were reviewed by the submitter.

---------

Signed-off-by: Karthik Kothuri <karthikkothuri2009@gmail.com>
Co-authored-by: Kyle Sayers <kylesayrs@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working ready When a PR is ready for full CI testing before merge transforms Related to transforms-based modifiers like SpinQuant and Quip two-reviews When a PR requires two reviews

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants