-
Notifications
You must be signed in to change notification settings - Fork 518
feat: Layerwise calibration memory optimizations (non-mutating skip + meta placeholders) #1640
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
978b498
d5fd4f4
83ec149
7765a67
168231a
240c216
38b9e1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The single public entry is already unified — |
||
| """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 | ||
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 -C3Repository: 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 -A5Repository: 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.pyRepository: 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.pyRepository: 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 -A30Repository: 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 -A20Repository: 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 -A20Repository: 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=pyRepository: 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 -50Repository: NVIDIA/Model-Optimizer Length of output: 48 HF TP sharded linear path does not propagate The HF transformers TP sharded linear path calls 🤖 Prompt for AI AgentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [IMPORTANT Compatibility] The new The FSDP2 (line 571) and Accelerate (line 578) branches both forward Either:
Worth tracing call sites since |
||
| 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() | ||
|
|
||
|
|
@@ -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 | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 50379
🏁 Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 8564
🏁 Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 11731
🏁 Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 8043
🏁 Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 4125
🏁 Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 2062
🏁 Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 431
🏁 Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 3349
Nested
enable_weight_access_and_writebackcontexts override outerwriteback=False, causing unintended weight rematerialization and writeback.When
calib_mutates_weights=False(line 1824),persistent_materializationis called withwriteback=False(line 1877) to preserve memory. However, the invokedcalib_func— typicallyweight_only_quantize(line 192) or_mse_calibrate_weights(line 590) — callsenable_weight_access_and_writebackwithout specifying thewritebackparameter, defaulting towriteback=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=Falsefrom the outer context to nestedenable_weight_access_and_writebackcalls in calibration helpers, or use a thread-local/context variable to propagate the writeback intent down the call stack.🤖 Prompt for AI Agents