release the injected components BEFORE the teardown, not after

The release added in 9715d200 ran after _free_pipeline_vram, which is too
late: that function ends in the teardown-leak breaker, which walks live CUDA
tensors and nulls whatever still references them. By the time the release
ran the components had already been stripped, so it found nothing on the GPU
and printed nothing, while the breaker reported the full

    16.9 GB of this pipeline's tensors survived teardown;
    nulled 7581 external reference(s)

exactly as before the fix. Verified live 2026-10-29: the new line never
appeared and the figure was unchanged.

The ordering is not cosmetic, because what the breaker nulls is the modules'
own _parameters. It frees the VRAM and leaves the injected components gutted
-- and those components are reused by the very next attempt. accelerate's
offload hook then does

    current_device = next(module.parameters()).device

on a module with no parameters left, which raises StopIteration, which in a
worker thread becomes "StopIteration interacts badly with generators" and
fails the generation. Measured: the run whose resident attempt failed died
that way at encode_prompt, while the run that skipped the resident attempt
(and so never ran the breaker) generated normally on the same pipeline.

So the leak, the gutted components and that crash are one bug seen at three
removes. Releasing first keeps the components intact, leaves the breaker with
nothing to strip, and the retry gets working weights.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
parent 9715d200
......@@ -1955,10 +1955,18 @@ def _load_video_pipeline(model_name: str, device: str, mode: str, offload: str =
print(f" [resident] components left off-GPU: {', '.join(_kept)} "
f"— resident can't fit this model; freeing partial pipe and "
f"falling back to model CPU offload…")
# BEFORE the teardown, not after. _free_pipeline_vram
# ends in the leak breaker, which walks live CUDA
# tensors and nulls whatever still references them --
# so releasing afterwards finds the components already
# stripped and reports nothing, while the breaker has
# done the work the hard way. Measured 2026-10-29: the
# release printed nothing and the breaker still said
# "16.9 GB survived teardown; nulled 7581 reference(s)".
_release_injected_to_cpu('a partial resident fit')
_free_pipeline_vram(pipe)
pipe = None
_clear_mem()
_release_injected_to_cpu('a partial resident fit')
offload = 'model'
else:
_record_resident_fits(model_name, True)
......@@ -1970,8 +1978,8 @@ def _load_video_pipeline(model_name: str, device: str, mode: str, offload: str =
print(f" Video: resident all-GPU OOM ({e}) — "
f"falling back to model CPU offload…")
e.__traceback__ = None # release frames pinning the partial model
_clear_mem()
_release_injected_to_cpu('a resident OOM')
_clear_mem()
offload = 'model' # fall through to Attempt 1 (model CPU offload)
# ── Attempt 0: full GPU ──────────────────────────────────────────────
......
......@@ -85,3 +85,31 @@ def test_the_helper_parses_at_the_right_scope():
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
names.add(node.name)
assert "_release_injected_to_cpu" in names
def test_the_release_happens_BEFORE_the_teardown():
"""Ordering is the whole fix.
_free_pipeline_vram ends in the teardown-leak breaker, which walks live
CUDA tensors and nulls whatever still references them. Release after that
and the components are already stripped: the release reports nothing and
the breaker has done the work the hard way.
Measured 2026-10-29 with the call placed after the teardown -- the release
printed nothing at all and the breaker still reported "16.9 GB of this
pipeline's tensors survived teardown; nulled 7581 external reference(s)".
"""
body = _SRC.split("if offload == 'resident':", 1)[1][:4000]
rel = body.index("_release_injected_to_cpu('a partial resident fit')")
free = body.index("_free_pipeline_vram(pipe)", 0)
assert rel < free, (
"the injected components are released AFTER _free_pipeline_vram, so "
"the leak breaker strips them first and the release is a no-op")
def test_the_oom_exit_releases_before_clearing_too():
body = _SRC.split("if offload == 'resident':", 1)[1][:4000]
rel = body.index("_release_injected_to_cpu('a resident OOM')")
clear = body.index("_clear_mem()", rel - 400 if rel > 400 else 0)
assert rel < body.index("offload = 'model' # fall through"), (
"the OOM exit falls through before releasing the components")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment