Skip to content
Merged
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 @@ -75,6 +75,7 @@ Changelog
- Add FP8 KV-cache cast variants for the partial-NVFP4 and weight-only general PTQ recipes: ``general/ptq/nvfp4_mlp_only-kv_fp8_cast``, ``general/ptq/nvfp4_experts_only-kv_fp8_cast``, ``general/ptq/nvfp4_omlp_only-kv_fp8_cast``, and ``general/ptq/nvfp4_weight_only-kv_fp8_cast``. These compose the same model-quant configs as their ``-kv_fp8`` siblings with the ``kv_fp8_cast`` unit (constant-amax FP8 KV cache, no KV calibration forward pass).
- Add Nemotron-3-Super-120B-A12B PTQ recipes ``modelopt_recipes/huggingface/models/nvidia/Nemotron-3-Super-120B-A12B/ptq/nvfp4-mse.yaml`` (MSE-mixed) and ``nvfp4-max-calib.yaml`` (max-calib mixed): NVFP4 W4A4 routed experts + FP8 per-tensor shared experts / Mamba in/out_proj + FP8 KV cache.
- Group layerwise calibration options under a nested ``LayerwiseConfig`` and add two knobs: ``get_qdq_activations_from_prev_layer`` (correct GPTQ-Hessian vs max-calib activation semantics — defaults to True for GPTQ, False for max/mse/local_hessian) and ``save_every`` (gate per-window ``next_inputs.pt`` activation-cache writes). Legacy bool ``layerwise`` and flat ``layerwise_checkpoint_dir`` keys still work; the bool form emits a ``DeprecationWarning``.
- Add two layerwise-calibration memory optimizations: ``calib_mutates_weights`` (set False for amax-only algorithms — max/mse/local_hessian — to skip the per-layer weight checkpoint blob and in-memory writeback, persisting only quantizer state), and meta-device skip-layer placeholders (already-calibrated layers emit zero-filled ``meta`` tensors instead of real-device buffers, eliminating their activation memory — models with real-device inter-layer ops on the hidden state are unsupported).
- Add ``examples/alpamayo`` showing FP8, NVFP4, and AutoQuantize (mixed-precision) quantization of the Alpamayo (formerly Alpamayo-R1) ~10B vision-language-action model, with a joint VLM + diffusion calibration loop and both fake-quant and ``--real-quant`` packed-checkpoint export. See `examples/alpamayo/README.md <https://github.com/NVIDIA/Model-Optimizer/tree/main/examples/alpamayo>`_ for details.
- Refactor ``llm_qat`` example with unified YAML-based configuration and flexible dataset blending.
``ModelOptArgParser`` adds ``--config`` YAML support with CLI overrides and auto-generates ``ARGUMENTS.md`` from dataclass definitions.
Expand Down
33 changes: 33 additions & 0 deletions modelopt/torch/quantization/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,17 @@ class LayerwiseConfig(ModeloptBaseConfig):
),
)

calib_mutates_weights: bool = ModeloptField(
default=True,
title="Whether layerwise calibration mutates layer weights.",
description=(
"Set to False only for algorithms that update solely "
"``TensorQuantizer._amax`` (max, mse, local_hessian). Rejected for "
"weight-mutating algorithms (GPTQ, AWQ, SmoothQuant) where it would "
"silently lose updates on resume."
),
)


def _coerce_layerwise_input(value):
"""Normalize a raw ``layerwise`` value to a dict; warn on deprecated bool."""
Expand All @@ -784,6 +795,11 @@ def _coerce_layerwise_input(value):
class QuantizeAlgorithmConfig(ModeloptBaseConfig):
"""Calibration algorithm config base."""

# Whether this algorithm mutates ``layer.weight`` during calibration. Amax-only
# algorithms (max/mse/local_hessian) set this False; it gates whether
# ``layerwise.calib_mutates_weights=False`` is allowed.
_mutates_weights: ClassVar[bool] = True

method: Literal[None] = ModeloptField(
None,
title="This field specifies the name of the calibration algorithm. If None, no calibration is performed.",
Expand Down Expand Up @@ -862,6 +878,17 @@ def validate_layerwise_checkpoint_dir(self):
)
return self

@model_validator(mode="after")
def _validate_non_mutating_layerwise_supported(self):
"""Enforce the ``calib_mutates_weights=False`` whitelist."""
if not self.layerwise.calib_mutates_weights and self._mutates_weights:
raise ValueError(
f"Algorithm '{self.method}' mutates layer weights in-place; "
"calib_mutates_weights=False would lose those updates on resume. "
"Only max/mse/local_hessian (amax-only) support this flag."
)
return self


class _SharedStatesConfig(ModeloptBaseConfig):
"""The ``shared_states`` grouping knob, shared by max / mse / local_hessian calibration."""
Expand Down Expand Up @@ -920,6 +947,8 @@ class MaxCalibConfig(_SharedStatesConfig, QuantizeAlgorithmConfig):
See `Integer Quantization <https://arxiv.org/pdf/2004.09602>`_ for the concepts.
"""

_mutates_weights: ClassVar[bool] = False

method: Literal["max"] = ModeloptField("max")

distributed_sync: bool | None = ModeloptField(
Expand Down Expand Up @@ -968,6 +997,8 @@ class MseCalibConfig(_SharedStatesConfig, QuantizeAlgorithmConfig):
When fp8_scale_sweep is enabled for a supported FP8-scale format, step_size is ignored.
"""

_mutates_weights: ClassVar[bool] = False

method: Literal["mse"] = ModeloptField("mse")

step_size: float | None = ModeloptField(
Expand Down Expand Up @@ -1020,6 +1051,8 @@ class LocalHessianCalibConfig(_SharedStatesConfig, QuantizeAlgorithmConfig):

"""

_mutates_weights: ClassVar[bool] = False

method: Literal["local_hessian"] = ModeloptField("local_hessian")

step_size: float | None = ModeloptField(
Expand Down
2 changes: 2 additions & 0 deletions modelopt/torch/quantization/mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ def wrapped_calib_func(
checkpoint_dir = layerwise_cfg.get("checkpoint_dir")
qdq_from_prev = layerwise_cfg.get("get_qdq_activations_from_prev_layer", False)
save_every = layerwise_cfg.get("save_every", 1)
calib_mutates_weights = layerwise_cfg.get("calib_mutates_weights", True)
if method is not None and "awq" in method:
# For backward compatibility
kwargs["algorithm"] = method
Expand Down Expand Up @@ -264,6 +265,7 @@ def wrapped_calib_func(
checkpoint_dir=checkpoint_dir,
get_qdq_activations_from_prev_layer=qdq_from_prev,
save_every=save_every,
calib_mutates_weights=calib_mutates_weights,
**kwargs,
)
else:
Expand Down
25 changes: 20 additions & 5 deletions modelopt/torch/quantization/model_calib.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
_CheckpointState,
)
from modelopt.torch.utils import print_rank_0, warn_rank_0
from modelopt.torch.utils.distributed import DistributedProcessGroup, ParallelState
from modelopt.torch.utils.distributed import DistributedProcessGroup, ParallelState, is_master
from modelopt.torch.utils.distributed import is_initialized as dist_is_initialized
from modelopt.torch.utils.distributed import size as dist_size
from modelopt.torch.utils.network import bind_forward_method, unpatch_forward_method
Expand Down Expand Up @@ -1925,6 +1925,7 @@ def layerwise_calibrate(
checkpoint_dir = calib_kwargs.pop("checkpoint_dir", None)
qdq_from_prev = calib_kwargs.pop("get_qdq_activations_from_prev_layer", False)
save_every = calib_kwargs.pop("save_every", 1)
calib_mutates_weights = calib_kwargs.pop("calib_mutates_weights", True)

if forward_loop is None:
raise ValueError(
Expand All @@ -1946,15 +1947,27 @@ def layerwise_calibrate(
checkpoint_dir,
num_layers,
save_every=save_every,
calib_mutates_weights=calib_mutates_weights,
)
start_layer = ckpt.start_layer if ckpt else 0

input_getter = LayerActivationCollector(model)
input_getter._patch_all_layers(decoder_layers=transformer_layers)
layer_pbar = tqdm(
total=num_layers,
initial=start_layer,
desc="Layerwise calibration",
disable=not is_master(),
dynamic_ncols=True,
)

def _set_layer_status(status: str):
layer_pbar.set_postfix_str(status, refresh=True)

resumed_inputs = ckpt.setup_resume(transformer_layers) if ckpt and start_layer > 0 else None
input_getter = LayerActivationCollector(model, status_callback=_set_layer_status)

try:
input_getter._patch_all_layers(decoder_layers=transformer_layers)
resumed_inputs = ckpt.setup_resume(transformer_layers) if ckpt and start_layer > 0 else None

# Bootstrap: get first layer's inputs (or use resumed inputs).
layer_inputs = input_getter.get_first_layer_inputs(
start_layer, resumed_inputs, forward_loop
Expand Down Expand Up @@ -1984,7 +1997,7 @@ def _layer_forward_loop(m, _inputs=layer_inputs):

is_last = layer_idx + 1 >= num_layers

with persistent_materialization(layer):
with persistent_materialization(layer, writeback=calib_mutates_weights):
# qdq_from_prev=False: capture before calib_func so the forward
# replay uses the original FP weights. Disable quantizers too in
# case any pre-calibration observer behavior would perturb the
Expand Down Expand Up @@ -2014,11 +2027,13 @@ def _layer_forward_loop(m, _inputs=layer_inputs):
if ckpt:
ckpt.save(layer_idx, model, transformer_layers, next_inputs)
Comment on lines 2000 to 2028

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect whether nested weight-access contexts can override outer writeback=False.

# Expectation: either nested contexts pass writeback=False/inherit an outer read-only state,
# or enable_weight_access_and_writeback has an explicit reentrant guard.
rg -n -C3 'enable_weight_access_and_writeback\(' --type=py

# Inspect writeback handling in the context manager implementation.
rg -n -C10 'def (enable_weight_access_and_writeback|persistent_materialization)\b|writeback' --type=py

Repository: NVIDIA/Model-Optimizer

Length of output: 50379


🏁 Script executed:

# Inspect persistent_materialization implementation
rg -n 'def persistent_materialization' --type=py -A 20

# Check what calib_func might be calling
rg -n 'def .*_mse_calibrate|def weight_only_quantize' --type=py -A 10

# Look for calls to enable_weight_access_and_writeback without explicit writeback param
rg -n 'enable_weight_access_and_writeback\([^)]*\)' --type=py | grep -v 'writeback' | head -20

Repository: NVIDIA/Model-Optimizer

Length of output: 8564


🏁 Script executed:

# Read the full enable_weight_access_and_writeback implementation
rg -n 'def enable_weight_access_and_writeback' --type=py -A 40

# Check what _mse_calibrate_weights does internally - full definition
sed -n '570,650p' modelopt/torch/quantization/model_calib.py

# Check if there are guards against nested writeback override
rg -n 'reentrant|_reentr|stack|nesting' --type=py modelopt/torch/quantization/utils/core_utils.py

Repository: NVIDIA/Model-Optimizer

Length of output: 11731


🏁 Script executed:

# Check the accelerate weight_access_and_writeback_context implementation
rg -n 'def weight_access_and_writeback_context' --type=py -A 30

# Check fsdp2_weight_access_and_writeback_context implementation
rg -n 'def fsdp2_weight_access_and_writeback_context' --type=py -A 30

# Look at actual layerwise_calib code around line 1877 to see what calib_func is
sed -n '1870,1910p' modelopt/torch/quantization/model_calib.py

Repository: NVIDIA/Model-Optimizer

Length of output: 8043


🏁 Script executed:

# Check if accelerate context actually uses the writeback parameter beyond line 107
sed -n '78,130p' modelopt/torch/quantization/plugins/accelerate.py

# Look for post_forward or writeback handling in accelerate plugin
rg -n 'post_forward|writeback' modelopt/torch/quantization/plugins/accelerate.py -B 2 -A 2

# Verify calib_func is indeed _mse_calibrate_weights - search for where it's passed
sed -n '1850,1880p' modelopt/torch/quantization/model_calib.py | grep -i 'calib_func\|def\|='

Repository: NVIDIA/Model-Optimizer

Length of output: 4125


🏁 Script executed:

# Find what calib_func is - search the function signature and where it's set
sed -n '1800,1850p' modelopt/torch/quantization/model_calib.py | grep -E 'def |calib_func|='

# Check if _mse_calibrate_weights passes writeback=False
rg -n 'enable_weight_access_and_writeback' modelopt/torch/quantization/model_calib.py -B 2 -A 1 | grep -A 3 'model_calib.py-590'

# Check weight_only_quantize for same pattern
sed -n '183,200p' modelopt/torch/quantization/model_calib.py

Repository: NVIDIA/Model-Optimizer

Length of output: 2062


🏁 Script executed:

# Find the function containing line 1877 and its signature
sed -n '1700,1810p' modelopt/torch/quantization/model_calib.py | grep -E 'def |calib_func'

# Or search more directly for function defs around layerwise
rg -n 'def.*calib_func|def layerwise_calibrate' modelopt/torch/quantization/model_calib.py -A 5 | head -40

Repository: NVIDIA/Model-Optimizer

Length of output: 431


🏁 Script executed:

# Check if there are any nested enable_weight_access_and_writeback calls within calib functions
rg -n 'def (weight_only_quantize|_mse_calibrate_weights|_make_weight_mse_calibrator)' --type=py -A 30 | grep -E 'def |enable_weight_access_and_writeback'

# Check what calib_func typically is - look for callers of layerwise_calibrate
rg -n 'layerwise_calibrate\(' --type=py -B 2 | head -30

Repository: NVIDIA/Model-Optimizer

Length of output: 3349


Nested enable_weight_access_and_writeback contexts override outer writeback=False, causing unintended weight rematerialization and writeback.

When calib_mutates_weights=False (line 1824), persistent_materialization is called with writeback=False (line 1877) to preserve memory. However, the invoked calib_func — typically weight_only_quantize (line 192) or _mse_calibrate_weights (line 590) — calls enable_weight_access_and_writeback without specifying the writeback parameter, defaulting to writeback=True. This nested context then calls _writeback_params_to_weights_map (accelerate plugin, line 110) or equivalent for FSDP2, rematerializing and writing back weights despite the outer read-only intent, negating the memory savings.

Fix: Pass writeback=False from the outer context to nested enable_weight_access_and_writeback calls in calibration helpers, or use a thread-local/context variable to propagate the writeback intent down the call stack.

🤖 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 `@modelopt/torch/quantization/model_calib.py` around lines 1877 - 1905, The
nested enable_weight_access_and_writeback calls in the calibration helper
functions weight_only_quantize and _mse_calibrate_weights are defaulting to
writeback=True, which overrides the outer persistent_materialization context's
writeback=False intent. To fix this, propagate the writeback intent down the
call stack by either passing the calib_mutates_weights parameter (or a derived
writeback=not calib_mutates_weights value) to the
enable_weight_access_and_writeback calls in these calibration helpers, or
establish a context variable to carry the writeback intent through the nested
function calls so that weight rematerialization and writeback only occurs when
explicitly intended at the outer level.


layer_pbar.update(1)
del layer_inputs
torch.cuda.empty_cache()
layer_inputs = next_inputs # noqa: F841 (used in next iteration's closure)
finally:
input_getter._unpatch_all_layers()
layer_pbar.close()

if ckpt:
ckpt.full_restore(transformer_layers, model)
Expand Down
16 changes: 12 additions & 4 deletions modelopt/torch/quantization/plugins/accelerate.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,22 @@ def _writeback_params_to_weights_map(module, align_hook):
# on-disk version. OffloadedWeightsLoader.__getitem__ gives
# state_dict priority over index, so this is sufficient.
w_map[key] = tensor.detach().cpu()
warnings.warn(
"Accelerate disk-offload writeback is currently kept in CPU state_dict; "
"writing updates back to disk offload files is TODO.",
RuntimeWarning,
stacklevel=2,
)


@contextmanager
def weight_access_and_writeback_context(module):
def weight_access_and_writeback_context(module, writeback: bool = True):
"""Context manager for weight access and writeback for modules managed by accelerate.

Handles CPU-offloaded and disk-offloaded models. Iterates over the module and all
its descendants, materializing weights from any offload hook found and writing them
back on exit. ``pre_forward`` is skipped on modules whose weights are already
its descendants, materializing weights from any offload hook found. If ``writeback``
is True, writes materialized tensors back on exit. ``pre_forward`` is skipped on
modules whose weights are already
materialized (not on meta) to avoid overwriting them with stale CPU copies.
"""
assert hasattr(module, "_hf_hook")
Expand All @@ -99,7 +106,8 @@ def weight_access_and_writeback_context(module):
finally:
for mod, hook, was_materialized in materialized:
hook.offload = True
_writeback_params_to_weights_map(mod, hook)
if writeback:
_writeback_params_to_weights_map(mod, hook)
if was_materialized:
hook.post_forward(mod, None)

Expand Down
52 changes: 42 additions & 10 deletions modelopt/torch/quantization/utils/core_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

from modelopt.torch.quantization.config import QuantizerCfgEntry
from modelopt.torch.utils import get_unwrapped_name, print_rank_0
from modelopt.torch.utils.network import temporarily_remove_accelerate_hook

if TYPE_CHECKING:
from collections.abc import Generator
Expand Down Expand Up @@ -486,7 +487,24 @@ def _set_parameter(module: nn.Module, name: str, value: nn.Parameter):


@contextmanager
def fsdp2_weight_access_and_writeback_context(module: nn.Module, root_model: nn.Module):
def _fsdp2_unshard_context(fsdp_module: FSDPModule):

@sugunav14 sugunav14 Jul 21, 2026

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.

is this for the case where we don't mutate weights? Have you considered reusing the original context with an option to skip updating the parameter groups instead of a new context? Might help readability so that we don't have to maintain multiple contexts for similar type of task.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The single public entry is already unified — fsdp2_weight_access_and_writeback_context(..., writeback=...) is the "option to skip updating the parameter groups" you're describing; _fsdp2_unshard_context is just a private helper it delegates to for the writeback=False branch. I factored it out rather than inlining an if/else because the two paths are different mechanisms, not one path with a flag: writeback does per-DTensor redistribute + parameter swap + copy-back, while the no-writeback path unshards/reshards the whole group in place and relies on the specific unshard()-before-_disable_fsdp_unshard_reshard ordering from the recent bug fix. Keeping that isolated in a named helper reads more clearly to me than a branch inside the larger function

"""Unshard an FSDP2 module without replacing individual DTensor parameters."""
fsdp_param_group = fully_shard.state(fsdp_module)._fsdp_param_group
was_sharded = fsdp_param_group.is_sharded
if was_sharded:
fsdp_module.unshard()
try:
with _disable_fsdp_unshard_reshard(fsdp_module):
yield
finally:
if was_sharded:
fsdp_module.reshard()


@contextmanager
def fsdp2_weight_access_and_writeback_context(
module: nn.Module, root_model: nn.Module, writeback: bool = True
):
"""Context manager for FSDP2 weight access and writeback.

Gathers sharded DTensor parameters across FSDP/HSDP shards so they can be
Expand All @@ -501,6 +519,11 @@ def fsdp2_weight_access_and_writeback_context(module: nn.Module, root_model: nn.
assert not hasattr(module, "_hf_hook"), "We dont support FSDP2 with HF accelerate hooks"
fsdp_module = _get_enclosing_fsdp_module(module, root_model)
assert fsdp_module is not None, "Module is not wrapped by FSDP"
if not writeback:
with _fsdp2_unshard_context(fsdp_module):
yield
return

fsdp_device_mesh = _get_fsdp2_mesh(fsdp_module)
fsdp_dim = fsdp_device_mesh.ndim

Expand Down Expand Up @@ -540,7 +563,9 @@ def fsdp2_weight_access_and_writeback_context(module: nn.Module, root_model: nn.


@contextmanager
def enable_weight_access_and_writeback(module, root_model, name_to_module: dict | None = None):
def enable_weight_access_and_writeback(
module, root_model, name_to_module: dict | None = None, writeback: bool = True
):
"""Enable weight access and writeback for a module.

Useful for modules with weight not intact such as Linear layer in FSDP wrapped model or
Expand All @@ -554,16 +579,18 @@ def enable_weight_access_and_writeback(module, root_model, name_to_module: dict
total cost when called in a loop. This causes significant CPU overhead on large
models, particularly Sparse MoE architectures where each expert is typically
implemented as its own module.
writeback: Whether modified weights must be written back to the owning sharded/offload
representation when exiting the context.
"""
if _get_enclosing_fsdp_module(module, root_model, name_to_module) is not None:
context = fsdp2_weight_access_and_writeback_context(module, root_model)
context = fsdp2_weight_access_and_writeback_context(module, root_model, writeback)
elif is_quantized_parallel_linear(module) and hasattr(module, "_hf_tp_plan"):
# HF transformers TP sharded linear layer
context = module.enable_weight_access_and_writeback()
Comment on lines 587 to 589

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if HF TP sharded linear is used in layerwise calibration code paths
rg -n "_hf_tp_plan" --type=py -C3

Repository: NVIDIA/Model-Optimizer

Length of output: 3913


🏁 Script executed:

#!/bin/bash
# Find the definition of enable_weight_access_and_writeback method
rg -n "def enable_weight_access_and_writeback" --type=py -A5

Repository: NVIDIA/Model-Optimizer

Length of output: 1313


🏁 Script executed:

#!/bin/bash
# Check the full HFParallelLinear class implementation
sed -n '354,400p' modelopt/torch/quantization/plugins/huggingface.py

Repository: NVIDIA/Model-Optimizer

Length of output: 1841


🏁 Script executed:

#!/bin/bash
# Get the full enable_weight_access_and_writeback method from HFParallelLinear
sed -n '400,430p' modelopt/torch/quantization/plugins/huggingface.py

Repository: NVIDIA/Model-Optimizer

Length of output: 1450


🏁 Script executed:

#!/bin/bash
# Also check if there's a _QuantHFParallelLinear variant
rg -n "class _QuantHFParallelLinear" --type=py -A30

Repository: NVIDIA/Model-Optimizer

Length of output: 3030


🏁 Script executed:

#!/bin/bash
# Check the fsdp2_weight_access_and_writeback_context function
rg -n "def fsdp2_weight_access_and_writeback_context" --type=py -A20

Repository: NVIDIA/Model-Optimizer

Length of output: 2112


🏁 Script executed:

#!/bin/bash
# Check the weight_access_and_writeback_context function
rg -n "def weight_access_and_writeback_context" --type=py -A20

Repository: NVIDIA/Model-Optimizer

Length of output: 2273


🏁 Script executed:

#!/bin/bash
# Search for calls to enable_weight_access_and_writeback with writeback parameter
rg -n "enable_weight_access_and_writeback.*writeback\s*=" --type=py

Repository: NVIDIA/Model-Optimizer

Length of output: 334


🏁 Script executed:

#!/bin/bash
# Check layerwise calibration code to see if it passes writeback=False
rg -n "calib_mutates_weights\|writeback" modelopt/torch/quantization/calib/ --type=py -B2 -A2 | head -50

Repository: NVIDIA/Model-Optimizer

Length of output: 48


HF TP sharded linear path does not propagate writeback parameter.

The HF transformers TP sharded linear path calls module.enable_weight_access_and_writeback() without forwarding the writeback parameter. This causes inconsistent behavior in layerwise calibration when writeback=False: the FSDP2 and Accelerate paths respect this flag and skip weight restoration, but the HF TP path unconditionally restores weights on context exit, potentially causing calibration modifications to be lost.

🤖 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 `@modelopt/torch/quantization/utils/core_utils.py` around lines 572 - 574, The
HF TP sharded linear path at the `enable_weight_access_and_writeback()` call is
not forwarding the `writeback` parameter that should control whether weights are
restored on context exit. Modify the call to
`module.enable_weight_access_and_writeback()` to pass the `writeback` parameter
so that when `writeback=False`, weights are not unconditionally restored, making
the behavior consistent with the FSDP2 and Accelerate paths.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[IMPORTANT Compatibility] The new writeback parameter is not propagated to the HF TP-sharded linear branch.

The FSDP2 (line 571) and Accelerate (line 578) branches both forward writeback, but module.enable_weight_access_and_writeback() here is called with no args — meaning a caller doing persistent_materialization(layer, writeback=False) on an HF TP-sharded model still goes through the full DTensor unwrap/wrap path and pays the same writeback cost. That defeats the memory/perf optimization for one of the three supported sharding paths and silently produces inconsistent behavior across backends.

Either:

  1. Plumb writeback through _QuantHFParallelLinear.enable_weight_access_and_writeback in plugins/huggingface.py:399-411 and pass it here, or
  2. Add an assert writeback, "HF TP path does not yet support writeback=False" so non-mutating layerwise + HF TP fails loudly instead of silently regressing.

Worth tracing call sites since quantizer_buffers.pt resume + HF TP would otherwise look like it works but cost more than the documented optimization implies.

elif hasattr(module, "_hf_hook"):
from ..plugins.accelerate import weight_access_and_writeback_context

context = weight_access_and_writeback_context(module)
context = weight_access_and_writeback_context(module, writeback)
else:
context = nullcontext()

Expand All @@ -572,18 +599,23 @@ def enable_weight_access_and_writeback(module, root_model, name_to_module: dict


@contextmanager
def persistent_materialization(layer):
def persistent_materialization(layer, writeback: bool = True):
"""Keep all layer weights materialized on GPU for the duration.

Suppresses per-forward weight transfers so that N calibration batches
pay the cost of one load/unload instead of N.

- **FSDP2**: patches ``FSDPParamGroup.unshard/reshard`` to no-ops, then
gathers weights once via ``enable_weight_access_and_writeback``.
- **Accelerate**: materializes weights and sets ``hook.offload = False``
so per-forward hooks skip materialization/offloading.
- **FSDP2**: gathers weights once via ``enable_weight_access_and_writeback``,
then patches ``FSDPParamGroup.unshard/reshard`` to no-ops.
- **Accelerate**: materializes weights, sets ``hook.offload = False``,
and bypasses the layer's top-level accelerate hook while the weights are
materialized.
"""
with _disable_fsdp_unshard_reshard(layer), enable_weight_access_and_writeback(layer, layer):
with (
enable_weight_access_and_writeback(layer, layer, writeback=writeback),
_disable_fsdp_unshard_reshard(layer),
temporarily_remove_accelerate_hook(layer),
):
yield


Expand Down
Loading
Loading