Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Changelog

**Bug Fixes**

- Fix ``mtq.fold_weight`` crashing with ``AttributeError: 'NoneType' object has no attribute 'data'`` on Megatron-Core models with tied word embeddings: the tied ``output_layer`` (built with ``skip_weight_param_allocation``) stores ``weight = None`` and borrows the embedding weight at forward time, yet still carries a ``weight_quantizer``. Weight-quantizer pairs whose weight is not a stored tensor are now skipped and left untouched.
- Fix EAGLE-3 training with context parallelism (``--cp_size > 1`` in ``examples/speculative_decoding``), which failed to start on ``accelerate >= 1.13`` and then raised ``got mixed torch.Tensor and DTensor``.

0.46 (2026-08-17)
Expand Down
12 changes: 12 additions & 0 deletions modelopt/torch/quantization/nn/modules/quant_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ def fold_weight(self, keep_attrs: bool = False):
transform is baked into the stored weight and then disabled, so subsequent forwards use
the stored weight directly. Calibration buffers (``_pre_quant_scale``, ``_amax``) are
dropped unless ``keep_attrs``.

Quantizers whose weight attribute is not a tensor are skipped and left untouched:
modules such as Megatron tied-embedding output layers (built with
``skip_weight_param_allocation``) store ``weight = None`` and receive the shared
weight as a forward-time argument, so there is nothing stored to fold. Such quantizers
stay enabled after folding, so callers that assert all weight quantizers are disabled
once folding completes (e.g. ``_check_all_weight_quantizers_disabled`` in the vLLM
fakequant export plugin) must account for them.
"""
# Handle all attributes that end with _weight_quantizer
for name in dir(self):
Expand All @@ -173,6 +181,10 @@ def fold_weight(self, keep_attrs: bool = False):
f"{name} doesn't have a corresponding {weight_name} in {self.__class__.__name__}"
)
weight = getattr(self, weight_name)
if not isinstance(weight, torch.Tensor):
# e.g. Megatron tied-embedding output_layer: weight is None and
# borrowed at forward time, so there is nothing stored to fold.
continue
Comment on lines +184 to +187

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] The skip is the right call, but it introduces the first documented exception to an invariant that two other places in the tree assert on: after fold_weight, no weight quantizer is enabled.

  • examples/vllm_serve/fakequant_worker.py:146-150 raises RuntimeError("... is still enabled after fold_weight — double-quantization would corrupt activations.") for any still-enabled weight-quantizer state key.
  • modelopt/torch/export/plugins/vllm_fakequant_hf.py:128-140 (_check_all_weight_quantizers_disabled) raises before writing metadata.

I traced this and believe it is currently unreachable: both are HF/vLLM-side paths, and vLLM's ParallelLMHead ties by sharing the embedding tensor (allocated weight), not by weight = None, so neither will see a skipped pair. Megatron's output_layer never flows through those checkers. So this is not a blocking issue.

Still, worth a line in the fold_weight docstring exception you added — something like "such quantizers stay enabled, so callers that assert all weight quantizers are disabled after folding must special-case them" — so the next person hitting one of those RuntimeErrors can find the reason here instead of re-deriving it. A pointer in the inline comment to _check_all_weight_quantizers_disabled would work equally well.

self._fold_weight_quantizer(attr, (weight,), keep_attrs)


Expand Down
19 changes: 19 additions & 0 deletions tests/unit/torch/quantization/test_tensor_quant_cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,25 @@ def test_fold_weight_keep_attrs_keeps_amax(monkeypatch):
unregister_quant_backend(backend_name)


def test_fold_weight_skips_none_weight():
"""A weight quantizer with no stored weight is skipped instead of crashing.

Megatron-Core tied-embedding output layers are built with
``skip_weight_param_allocation``: the module stores ``weight = None`` and borrows the
embedding weight at forward time, while still carrying a ``weight_quantizer``.
``fold_weight`` must skip the pair, leaving the quantizer intact for forward-time use.
"""
qlinear = QuantModuleRegistry.convert(torch.nn.Linear(4, 3))
qlinear.weight_quantizer.amax = torch.tensor(1.0)
expected_amax = qlinear.weight_quantizer.amax.detach().clone()
qlinear.register_parameter("weight", None)

qlinear.fold_weight() # must not raise

assert qlinear.weight_quantizer.is_enabled
assert torch.equal(qlinear.weight_quantizer.amax, expected_amax)


WINT4INT8_CFG = {
"quant_cfg": [
{"quantizer_name": "*", "enable": False},
Expand Down