fix(parametrize): cache-gate hooks leak the global parametrize cache under activation checkpointing (all dequantized params retained)#1999
Conversation
…tion checkpointing use_reentrant=False checkpointing aborts its backward recompute mid-forward by design (early stop) once the last needed activation is rematerialized. A plain forward hook is skipped for the module holding that last save, so the global parametrize._cache_enabled counter leaked +1 per checkpointed region per step; once stuck above zero the cache is never cleared again and every dequantized parameter is retained for the rest of training -- a memory leak of the full dequantized model size (4x the packed 4-bit bytes; e.g. ~53 GiB for a 30B MoE, which can never fit a 24 GB device). Register the disable hook with always_call=True so it also runs on the aborted recompute, and clamp the decrement at zero (a negative counter is truthy, so 'if not P._cache_enabled' would stop clearing forever). Regression tests cover the checkpoint-early-stop shape and the clamp. Co-authored-by: Claude <noreply@anthropic.com>
3fbde4f to
98989eb
Compare
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
|
Thanks for the PR. This should fix #2005. One thing I should maybe note is that I never really took the effort to highlight this feature in release notes, examples, demos, etc mainly for the reason that it was just a sort of experiment. Overall, it's not an ideal solution for MoEs, even if it sort of works. I think Axolotl might use it in some way, so the PR here is OK. |
d47c88a
into
bitsandbytes-foundation:main
|
Thanks for the quick review and merge. Hit it benchmarking Qwen3-30B QLoRA on a 24 GB 3090, where the retained dequant cache quietly erases most of 4-bit's memory win — glad it lands before anyone else meets #2005's path at scale. Agreed on the MoE fit of parametrize generally: the recompute interaction is intrinsic to caching dequantized params under checkpointing, rather than something a patch fully cures. |
The bug
replace_parameter_4bit/replace_parameter_4bit_prequantizedregister aforward_pre_hook/forward_hookpair that gates torch's global parametrizationcache by counter: pre-hook does
parametrize._cache_enabled += 1, post-hookdecrements and clears
parametrize._cachewhen it reaches 0.Activation checkpointing with
use_reentrant=False(the recommended mode, andwhat HF
gradient_checkpointing_enableuses) aborts its backward recomputemid-forward by design — early stop raises an internal exception the moment
the last needed activation has been rematerialized. A plain
forward_hookisskipped when the forward is interrupted, so for the module holding the last
recomputed save the pair fires pre-only: the global counter leaks +1 per
checkpointed region per step and never returns to zero. From then on the cache
is permanently enabled and never cleared — every dequantized parameter the
model touches stays resident for the rest of training, i.e. a memory leak of
the full dequantized model size (4× the packed 4-bit bytes).
Observed in the wild training MoE models whose experts are quantized via this
path with gradient checkpointing on:
max_activeregardless of store); plain resident training OOMs a 16 GB card at step 0 with the leak and fits with the fix appliedand OOMs any 24 GB card at step 1 (allocated 22.37 GiB = base + ~14 blocks
of retained dequants,
Tried to allocate 384 MiBinsidebitsandbytes/nn/parametrize.py::forward → dequantize_4bit).Minimal repro of the torch-level mechanism (no bnb needed): a 3-Linear chain
under
checkpoint(..., use_reentrant=False), pre/post hooks counting on thetail module →
pre=2, post=1after one fwd/bwd.The fix
always_call=True(torch ≥ 2.0), so it alsoruns when the recompute is aborted — verified to balance the pair (2/2) under
early stop.
always_call=Truethe hook can also firewhen an earlier pre-hook raised before
_enableran; a negative counter istruthy, so
if not P._cache_enabledwould never clear the cache again.Tests
TestParametrizationCacheCounterUnderCheckpointing:test_counter_balanced_under_checkpoint_early_stop— real hook pair on thetail module of a checkpointed chain, 3 fwd/bwd steps, asserts counter == 0 and
cache empty. Fails on main, passes with the fix (verified by mutation).
test_counter_never_goes_negative— pins the clamp.CPU-only, no quantization kernels needed; runs in ~1.2 s.
Developed with Claude Code (Anthropic) — the diagnosis, fix, and regression tests were produced with AI assistance and validated by the author as described above.