Repository navigation
[diffusion] feat: allow a component use retain its layerwise resident set - #40592
Conversation
…yerwise offload `LayerwiseOffloadStrategy.finish_use` called `manager.release_all()`, and `release_all` did exactly what its name says: dropped every layer, the resident set included. Those are two different questions. When a component's use ends the streamed window is certainly dead, but whether the resident set is depends on whether anything will want it before another stage needs the room — and `release_all` is also the right call for the full reset in `enable_offload`, which must leave nothing behind. Conflating them is why `--layerwise-resident-layers` does nothing for any component whose use is a single forward pass rather than a denoise loop: the set is prefetched at the start of the use and dropped at the end of it, every request. For the DiT one use spans all denoise steps, so the set survives and earns its memory back; for a text encoder it never survives anything. This change only names the two operations apart: release_after_use(keep_resident=False) the use ended release_all() a full reset `finish_use` now says `release_after_use()`, and the default argument makes that byte-identical to what it did before — a test pins that. `enable_offload` keeps `release_all()`. `keep_resident=True` has no caller yet; giving the decision a home is a separate change. `release_after_use` rather than `end_use` because `ComponentManager.end_use` already exists with an unrelated signature, and `manager.end_use(...)` would read ambiguously across the two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Builds on the release_after_use / release_all split. That change named the
question; this one gives the answer a home, and deliberately not a threshold.
A layerwise component's resident set is released when its use ends. For the DiT
that is right — one use spans every denoise step, so the set has already earned
its memory back. For a component used once per forward it means the set is
prefetched and dropped on every request, which is why
`--layerwise-resident-layers text_encoder=0.8` reports `resident=53/66` on
Qwen-Image-2.1 and moves neither memory nor latency.
Whether holding it instead is right cannot be decided here. Measured with a
local headroom test: the same pipeline and the same flag value goes from
14.136s to 13.737s on a 31.4 GiB card, and stops a 23.5 GiB one from starting —
the OOM lands on a 720x720 warmup, a shape the decision point never sees. What
it needs is per-phase headroom across the whole pipeline.
So the decision is declared on the use, beside the other placement intentions
already there (`preferred_ready_after_request`, `keep_ready_after_warmup`):
ComponentUse.retain_resident_layers: bool = False
`LayerwiseOffloadStrategy.finish_use` passes it through, and skips parking the
non-layer weights when it is set, since parking would undo the transfer just
kept. It defaults off and nothing sets it yet, so behaviour is unchanged;
sgl-project#37918's apply/validate/rollback layer is the intended setter, using
`LayerwiseOffloadManager.retained_parameter_bytes()` added here to size what the
declaration costs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/tag-and-rerun-ci extra |
It sized what `retain_resident_layers` trades away, for a caller that decides on headroom -- and that caller is deliberately not in this PR, because the threshold it would need is what the 4090 measurement showed cannot be set safely from inside the manager. So the helper has no production reference, only the two tests that exercised it. Speculative API on a public manager surface is worth less than the question "who calls this?" costs to answer, and it can come back with its caller, sized against whatever that caller actually has in view. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
CI status note (from the author's triage, so reviewers need not re-derive it). Every red on this PR falls in one of three buckets, none of them from the diff:
|
|
CI note on
The same TP2 / Ulysses-2 / CFG-parallel / FSDP code paths ran at baseline on two other machines at the same minute; Also checked and excluded: the merge is clean ( The rerun (attempt 2) was scheduled onto the same |
|
Follow-up: the rerun (attempt 2) was scheduled onto the same Re-queued the job once more; if it lands on |
|
Closing the loop on |
Resolves the textual overlap with sgl-project#40593 (help strings, CLI reference, readiness log): the merged text keeps sgl-project#40593's user vocabulary and adds the lifetime axis on top, and the readiness log names the lifetime (`(forward)` / `(permanent)`) where sgl-project#40593 wrote `(per request)`. Brings in sgl-project#40590 and sgl-project#40592, which this PR composes with unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Second of three, stacked on #40590 — its diff is included below it, so review this one for the second commit. #40590 named the question; this one gives the answer a home, and deliberately not a threshold.
Stack
release_after_usefromrelease_all(no behaviour change)What is being decided
A layerwise component's resident set is released when its use ends. For the DiT that is right: one use spans every denoise step, so the set has already earned the memory it holds. For a component used once per forward it means the set is prefetched at the start of the use and dropped at the end, every request — which is why
--layerwise-resident-layers text_encoder=0.8on Qwen-Image-2.1 logsresident=53/66and moves neither memory nor latency.Why the decision is not made here
I tried making it here, with a local headroom test, and measured both outcomes.
The same pipeline and the same flag value:
Not a threshold that wants tuning. On the 4090 the right answer is simply "do not retain", and a test written at
finish_usecannot reach it: what it sees is how much is free now; what it needs is how much a later stage will want. The OOM lands on a 720×720 warmup — a shape that decision point never saw.Two attempts made it concrete, both caught by validation rather than review: measuring free memory during the use (the use's own transients are still allocated, so the patch changed nothing at all), then a fixed share of total
device memory (10% left at the decision point, 96% load peak on the 5090, and nothing boots on the 4090).
What this PR does
Declares the decision on the use, beside the placement intentions already there:
LayerwiseOffloadStrategy.finish_usepasses it torelease_after_use(keep_resident=…), and skipspark_non_layer_weights()whenit is set — parking pushes the component's non-layer weights to host, which
would undo the transfer just kept.
Nothing sets the flag in this PR, so behaviour is unchanged. #37918's
apply/validate/rollback layer is the intended setter — it already has the
per-phase device headroom #37917 enumerates candidates against, which is exactly
the resource this decision needs. The two cards above make a ready-made test
case: identical pipeline, identical resident set, opposite correct answers, and
the wrong answer on the 4090 is a boot failure rather than a slow request.
Tests
Two in
test_layerwise_offload.py: the default still releases and still parks,and the declaration reaches every manager and suppresses parking.
An earlier revision also added
LayerwiseOffloadManager.retained_parameter_bytes()to size what the declaration costs. It has been removed again (
6ccefc6): thecaller that would weigh it is deliberately not in this PR, so the helper had no
production reference — only the two tests that exercised it.
Measured before that removal:
test_layerwise_offload.py81 failed / 55 passedagainst #40590's 81 / 51 — the same pre-existing failures on a machine without
CUDA, plus the four tests of that revision. Dropping the helper drops two of
them;
multimodal-gen-unit-teston the current head is the run that covers it,as this file needs torch and the Mac it was edited on has none.
test_component_residency.py26 passed, 9 skipped, unchanged: adding a defaultedfield to
ComponentUsetouches nothing there.🤖 Generated with Claude Code
CI States
Latest PR Test (Base): ✅ Run #35683690473
Latest PR Test (Extra): ✅ Run #35683690268
Latest PR Test (AMD ROCm 10): ❌ Run #35683690421