mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-10-12 02:16:43 +00:00
fix(wan): empty CUDA cache between A14B expert swaps
The dual-expert swapper releases the active expert via its context manager exit, but PyTorch's caching allocator retains the freed blocks as reserved-not-yet-claimable space until empty_cache runs. The next partial_load_to_vram for the incoming expert then sees a fragmented free pool and offloads layers it could otherwise have kept on device. Users running A14B observed the low-noise expert ending up far more CPU-resident than the high-noise one on otherwise identical settings — that was the leftover reservation from the high-noise expert masking real free VRAM. Call TorchDevice.empty_cache() between the release and the next load. Same pattern as the VAE-encode fix earlier in this branch. Regression test in test_wan_expert_swapper.py mocks empty_cache and asserts it fires on every actual swap but not on a same-label re-get. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
5ec7e3327f
commit
53b2f4d4c7
@@ -172,6 +172,15 @@ class _ExpertSwapper:
|
||||
# Release current GPU residency before bringing the other expert on device.
|
||||
self._release()
|
||||
|
||||
# Hand the PyTorch allocator a clean slate before the next partial_load_to_vram
|
||||
# decides how much of the incoming expert can be GPU-resident. The previous
|
||||
# expert's freed blocks stay pinned in the caching allocator until empty_cache
|
||||
# is called, so partial_load sees fragmented free space and offloads layers it
|
||||
# could otherwise have kept on device — A14B users observed the low-noise
|
||||
# transformer ending up far more CPU-resident than the high-noise one purely
|
||||
# because of leftover reservations from the previous swap.
|
||||
TorchDevice.empty_cache()
|
||||
|
||||
# Load the requested expert lazily so its ``LoadedModel`` handle is
|
||||
# always fresh — see class docstring for the cache-eviction reasoning.
|
||||
model_id = self._high_model if label == self.HIGH else self._low_model
|
||||
|
||||
@@ -360,3 +360,54 @@ def test_lazy_load_per_swap_not_upfront():
|
||||
assert ctx.models.load_calls == ["high", "low", "high"]
|
||||
|
||||
swapper.close()
|
||||
|
||||
|
||||
def test_empty_cache_called_on_swap():
|
||||
"""Regression: each expert swap must trigger ``TorchDevice.empty_cache()`` so
|
||||
the next ``partial_load_to_vram`` sees an un-fragmented allocator.
|
||||
|
||||
A14B users reported the low-noise expert ending up far more CPU-resident than
|
||||
the high-noise one — the previous expert's freed blocks stayed pinned in the
|
||||
PyTorch caching allocator across the swap, and partial_load decided there
|
||||
wasn't room for as much of the incoming expert as there actually was."""
|
||||
log: list[str] = []
|
||||
high_nn = nn.Linear(1, 1)
|
||||
low_nn = nn.Linear(1, 1)
|
||||
ctx = _FakeContext(
|
||||
{"high": _FakeInfo("HIGH", high_nn, log), "low": _FakeInfo("LOW", low_nn, log)},
|
||||
log,
|
||||
)
|
||||
|
||||
stub, _ = _stub_lora_context_manager(log)
|
||||
with (
|
||||
patch(
|
||||
"invokeai.app.invocations.wan_denoise.LayerPatcher.apply_smart_model_patches",
|
||||
side_effect=stub,
|
||||
),
|
||||
patch("invokeai.app.invocations.wan_denoise.TorchDevice.empty_cache") as empty_cache_mock,
|
||||
):
|
||||
swapper = _ExpertSwapper(
|
||||
context=ctx,
|
||||
high_model="high",
|
||||
low_model="low",
|
||||
inference_dtype=torch.bfloat16,
|
||||
high_lora_factory=_make_factory(log, "HIGH"),
|
||||
low_lora_factory=_make_factory(log, "LOW"),
|
||||
)
|
||||
swapper.get(_ExpertSwapper.HIGH)
|
||||
first_call_count = empty_cache_mock.call_count
|
||||
assert first_call_count >= 1, "empty_cache should run on the initial expert load too"
|
||||
|
||||
swapper.get(_ExpertSwapper.LOW)
|
||||
assert empty_cache_mock.call_count > first_call_count, (
|
||||
"empty_cache must be called on each HIGH→LOW (or LOW→HIGH) swap"
|
||||
)
|
||||
|
||||
# Same-label re-get is a no-op; empty_cache must NOT be re-invoked.
|
||||
before_no_op = empty_cache_mock.call_count
|
||||
swapper.get(_ExpertSwapper.LOW)
|
||||
assert empty_cache_mock.call_count == before_no_op, (
|
||||
"Re-getting the active expert must short-circuit before empty_cache."
|
||||
)
|
||||
|
||||
swapper.close()
|
||||
|
||||
Reference in New Issue
Block a user