fix(autoround): prevent input_capture_hook from accumulating GPU memory during optimization - #3024
fix(autoround): prevent input_capture_hook from accumulating GPU memory during optimization#3024xesdiny wants to merge 4 commits into
Conversation
|
👋 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. |
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
Merge Protections🔴 1 of 2 protections blocking · waiting on 👀 reviews
🔴 Require two reviewsWaiting for
This rule is failing.PRs labelled "two-reviews" must have at least two approving reviews before merging.
Show 1 satisfied protection🟢 Require one maintainer reviewAll PRs must have at least one approving review from a maintainer before merging.
|
There was a problem hiding this comment.
Code Review
This pull request introduces a _consumed_layers set to track and short-circuit input capture hooks once a layer's inputs are consumed during AutoRound calibration, effectively preventing GPU memory growth during optimization loops. The feedback suggests specifying the element type for the _consumed_layers set as set[str] to improve type safety and maintain consistency with other private attributes.
| # private variables | ||
| _all_module_input: dict[str, list[tuple]] = PrivateAttr(default_factory=dict) | ||
| _q_input: torch.Tensor | None = PrivateAttr(default=None) | ||
| _consumed_layers: set = PrivateAttr(default_factory=set) |
There was a problem hiding this comment.
To improve type safety and maintain consistency with other private attributes (such as _all_module_input), please specify the element type for the _consumed_layers set as set[str].
| _consumed_layers: set = PrivateAttr(default_factory=set) | |
| _consumed_layers: set[str] = PrivateAttr(default_factory=set) |
19bb805 to
580febe
Compare
|
@gemini-code-assist review |
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to track consumed layers during AutoRound calibration by adding a _consumed_layers set. This ensures that the input capture hook becomes a no-op once a layer's inputs are processed, preventing GPU memory growth from accumulated hidden states. The reviewer suggested using .pop(..., None) instead of .pop() when retrieving captured inputs to avoid a potential KeyError if calibration data is missing, and instead raising a more descriptive RuntimeError.
| self._consumed_layers.add(decoding_layer._tmp_name) | ||
| cur_inputs = self._all_module_input.pop(decoding_layer._tmp_name) |
There was a problem hiding this comment.
Using .pop() with only the key will raise a KeyError if the layer's temporary name is not present in _all_module_input (for example, if calibration data was empty or the forward pass did not run for this layer). Using .pop(..., None) and raising a descriptive RuntimeError provides a clearer error message for troubleshooting.
| self._consumed_layers.add(decoding_layer._tmp_name) | |
| cur_inputs = self._all_module_input.pop(decoding_layer._tmp_name) | |
| self._consumed_layers.add(decoding_layer._tmp_name) | |
| cur_inputs = self._all_module_input.pop(decoding_layer._tmp_name, None) | |
| if not cur_inputs: | |
| raise RuntimeError( | |
| f"No calibration inputs captured for layer {decoding_layer._tmp_name}. " | |
| "This can happen if calibration data is missing or the forward pass did not execute." | |
| ) |
| cur_inputs = self._all_module_input[decoding_layer._tmp_name] | ||
| # Mark consumed before pop so the hook immediately becomes a no-op | ||
| # for any forward call triggered during the optimization loop. | ||
| self._consumed_layers.add(decoding_layer._tmp_name) |
There was a problem hiding this comment.
Hi @xesdiny, good catch! Rather than adding type information to _consumed_layers, I’d prefer to remove the input-capture hook for each layer as soon as its calibration inputs have been collected, before AutoRound tuning begins.
The hooks for later layers can remain active until their own calibration pass. This way, we avoid retaining optimization inputs without introducing additional state for consumed layers.
|
Thanks for the reviews! Updated the implementation in cb3cbc0: Per @yiliu30: replaced the Local validation on 8× L20 (Qwen3.5-35B-A3B, |
…ptimization AutoRoundModifier registers `input_capture_hook` as a forward_pre hook on all decoding layers, but never disables it before the SignSGD optimization loop. During optimization, BlockForwardRunner.forward() calls decoding_layer.forward() on every mini-batch, triggering the hook each time. With gradient_accumulate_steps=8 this appends 8 × hidden_states tensors per outer iteration into _all_module_input; at ITERS=200 this accumulates ~200 × gradient_accumulate_steps × hidden_states_size bytes of GPU memory, causing OOM on models with large hidden states. Fix: introduce `_consumed_layers` (a PrivateAttr set) to track which layers have had their inputs consumed by apply_autoround(). The hook uses setdefault() for calibration-phase initialization but exits immediately for consumed layers, preventing any accumulation during optimization. apply_autoround() marks the layer consumed before pop()ing its inputs so that concurrent hook firings also no-op. on_calibration_end() clears the set for clean re-use. Verified: GPU allocated memory growth drops from +204 MB/iter to +0 MB/iter across 20 iterations on an 8-GPU setup with gradient_accumulate_steps=8. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Xesdiny <xesdiny@gmail.com>
Addresses Gemini Code Assist suggestion: specifying the element type improves type safety and is consistent with _all_module_input which already uses a parameterized type annotation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Xesdiny <xesdiny@gmail.com>
…emoval Per reviewer feedback (@yiliu30): instead of tracking consumed layers in a set and gating the hook with an early return, remove each layer's input-capture hook via remove_hooks() before AutoRound optimization begins. Hooks for subsequent layers remain active until their own calibration pass. This eliminates the extra _consumed_layers state entirely: _capture_hooks maps each decoding layer name to its RemovableHandle registered in on_calibration_start, and the handle is popped + removed immediately before quantize_block is called. Also per @gemini-code-assist: changed .pop(key) to .pop(key, None) with a descriptive RuntimeError on missing calibration inputs. Validated on 8× L20 (gradient_accumulate_steps=8, iters=200, Qwen3.5-35B-A3B): Δalloc=+0.0 MB across all 200 iterations on all 8 ranks, alloc locked at 9846 MB throughout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Xesdiny <xesdiny@gmail.com>
cb3cbc0 to
545fd91
Compare
|
Thanks @xesdiny! This fix LGTM. The failing UTs are unrelated to this change, so please wait for the fix to land on the main branch. |
|
@gemini-code-assist review |
There was a problem hiding this comment.
Code Review
This pull request introduces hook management in the AutoRoundModifier to prevent re-populating calibration inputs during the SignSGD optimization loop. It tracks registered input-capture hooks in a new _capture_hooks private attribute, removes each layer's hook before its optimization begins, and raises a RuntimeError if no calibration inputs are captured. The review feedback suggests improving type safety by using a more specific type hint (dict[str, Any]) for the _capture_hooks attribute.
| # private variables | ||
| _all_module_input: dict[str, list[tuple]] = PrivateAttr(default_factory=dict) | ||
| _q_input: torch.Tensor | None = PrivateAttr(default=None) | ||
| _capture_hooks: dict = PrivateAttr(default_factory=dict) |
There was a problem hiding this comment.
For better type safety and consistency with other private attributes in this class (such as _all_module_input), consider using a more specific type hint for _capture_hooks, such as dict[str, Any].
| _capture_hooks: dict = PrivateAttr(default_factory=dict) | |
| _capture_hooks: dict[str, Any] = PrivateAttr(default_factory=dict) |
Summary
Fixes #3023
AutoRoundModifier.input_capture_hookis registered on all decoding layers duringon_calibration_start()but is never disabled before the SignSGD optimization loop.During optimization,
BlockForwardRunner.forward()callsdecoding_layer.forward()on every mini-batch, triggering the hook each time. With
gradient_accumulate_steps=Nthis retains N hidden_states tensors per outer iteration, causing
~
N × hidden_states_sizeMB of GPU memory growth per iteration.At
gradient_accumulate_steps=8,hidden_size=2048,seq_len=6144(bf16):+204 MB/iter → ~40 GB accumulated at ITERS=200 → OOM on 44 GB GPUs.
Changes
_consumed_layers: setPrivateAttr to track layers processed byapply_autoround()input_capture_hook: usesetdefault()for calibration-phase initialization; exit immediately for consumed layersapply_autoround(): mark layer consumed beforepop()-ing its inputson_calibration_end(): clear_consumed_layersVerification
After fix, on 8 × RTX 5880 (44 GB),
Qwen/Qwen3.5-35B-A3B,gradient_accumulate_steps=8: