feat(multi-gpu): mark the remaining text encoders as idle-GPU offloadable - #9428
Conversation
…able The `idle_gpu_offloadable` flag (invoke-ai#9311) lets an encoder-only node run on a borrowed idle GPU when `offload_text_encoders_to_idle_gpus` is enabled. Every text encoder that existed at the time was marked; the four added since were not, so on a multi-GPU machine they always occupy the session's own GPU. Marks wan, krea2, ideogram4 and ernie_image. All four meet the flag's stated condition: each returns its result via `detach().to("cpu")` and saves only the conditioning name, doing no work on the session device. Krea-2's optional `mask` input is passed through as a TensorField and is not processed until the denoise node. No schema change: the flag is a ClassVar set by the @invocation decorator, not a pydantic field, so no node version bumps are needed.
…ders The four encoders this PR marks store their conditioning on the CPU, which is what makes the borrowed-GPU handoff safe -- but the flag lives on the @invocation decorator, so nothing in the node bodies hints that the contract exists. Add the regression tests that already exist for flux2_klein and flux_redux: - a registry-driven guard that every *_text_encoder node carries the flag, so the next encoder to be added is caught rather than the four already fixed; - per-node tests that each of the four detaches and moves its conditioning to the CPU, plus that krea2 forwards its regional mask TensorField untouched (resolving it here would pull a tensor onto the borrowed GPU). Also correct the borrow-cost documentation. device_pool.py claimed a lending session "waits out the (short) encoder node"; the borrow actually spans the node's model load, and caches are per-device, so the first borrow of a GPU always cold-loads the encoder there. That cost amortizes across later borrows, which hit the cache -- but work that recurs per execution does not amortize. ernie_image_text_encoder runs an autoregressive generate() for its optional prompt enhancer inside the borrow, so it stalls the lent GPU on every generation rather than once. Noted for reviewers in the flag docs and the node-authoring guide. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Reviewed this and pushed one commit to the branch ( The markings check outI traced each of the four against the contract. All four store their conditioning via
Two small notes: the body said "all 12 text-encoder invocations" — it's 14 after this PR (15 marked including What I addedThe flag lives on the decorator, so nothing in a node body signals that the CPU-store contract exists — dropping a The one thing I'd like your read on: ERNIE
I tried fixing that with a per-instance opt-out (offload only when the enhancer won't run) and backed it out — it's worse than either alternative. So the real options are:
I left (1) in place since it's what you wrote and it's defensible, and documented the tradeoff in |
lstein
left a comment
There was a problem hiding this comment.
LGTM and am happy to merge when you give the word. However, please take a look at the comment regarding the handling of ERNIE Image and let me know if you want to make the suggested architectural change of splitting the PE into its own node.
`ernie_image_text_encoder` is `idle_gpu_offloadable`, so its whole execution runs on a borrowed idle GPU whose exclusive-use lock is held until the node returns. An encoder forward is short and its model load amortizes into the borrowed device's cache; the bundled prompt enhancer's autoregressive `generate()` — up to 1024 tokens — runs afresh on every generation and never amortizes, so it re-stalled the lent GPU each time. A session dequeued onto that GPU logged `Executing queue item N` and then blocked in `acquire_session()` for the length of someone else's prompt rewrite. Carve the rewrite out into `ernie_image_prompt_enhancer`, a StringOutput node that is deliberately *not* offloadable. The encoder becomes genuinely encoder-only and keeps the flag; the enhancer stays on the session's own GPU. A per-instance opt-out was not viable: `buildErnieImageGraph` emits two encoders sharing one text encoder, and gating on the enhancer would split that pair across GPUs and cold-load the same encoder twice. The graph builder now wires the enhancer between the prompt node and the positive encoder when the toggle is on, and wires the prompt straight through otherwise. With no enhancer connected the node passes the prompt through, so pipelines that ship no PE submodel behave as before. Also make the rewrite cancelable: `generate()` gets a StoppingCriteria bound to the session's cancel event, and a cancelled run raises rather than encoding the truncated prompt. `ernie_image_text_encoder` goes to 2.0.0 — the six enhancer fields are removed, which breaks saved workflows that set them. It is a Prototype node. Tests pin the split in both directions: the enhancer must not become offloadable, and the encoder must not regain enhancer fields (either change would silently invalidate its flag with nothing else failing). Plus passthrough, the token cap, cancellation, and the new graph wiring in both toggle states.
Summary
The
idle_gpu_offloadableflag introduced in #9311 lets an encoder-only node run its whole execution on a borrowed idle GPU whenoffload_text_encoders_to_idle_gpusis enabled. Every text encoder that existed at the time was marked, but four have been added since and were never given the flag — so on a multi-GPU machine they always run on the session's own GPU, holding VRAM that the generation needs.Marks the four stragglers:
wan_text_encoder,krea2_text_encoder,ideogram4_text_encoder,ernie_image_text_encoder.All four satisfy the condition the flag documents ("encoder-only nodes that store their result on the CPU and do no work on the session's own GPU"): each returns via
detach().to("cpu")and persists only the conditioning name. Krea-2's optionalmaskinput is a TensorField that is passed through untouched and first processed inkrea2_denoise.After this change 14 text-encoder invocations carry the flag (15 including
flux_redux, which is an image-prompt encoder rather than a text one).Tests
The flag lives on the
@invocationdecorator, so nothing inside a node body hints that the CPU-store contract exists — a future edit dropping a.to("cpu")would break multi-GPU silently. Added the regression tests thatflux2_kleinandflux_reduxalready have:*_text_encodernode carries the flag (enumerating rather than listing the four, so the next encoder added is what it catches), with a documented_NOT_OFFLOADABLEescape hatch;TensorFielduntouched — resolving it in the encoder would pull a tensor onto the borrowed GPU.Borrow-cost documentation
device_pool.pyclaimed a lending session "waits out the (short) encoder node". That was already inaccurate: the borrow spans the node's model load, and caches are per-device, so the first borrow of a GPU always cold-loads the encoder there. That cost amortizes — later borrows are sticky and hit the cache — but work that recurs per execution does not.ernie_image_text_encoderis the one node here where that matters: its optional prompt enhancer runs an autoregressivegenerate()of up to 1024 tokens inside the borrow, so it re-stalls the lent GPU on every generation instead of once. Corrected the claim indevice_pool.py, theidle_gpu_offloadabledocstring, and the node-authoring guide so the tradeoff is visible.Reviewers may want to weigh in on ERNIE specifically. Marking it is still a net win in the common case and it is kept as-is here, but the options are not obvious — see the review comment below for the analysis, including why a per-instance opt-out (offload only when the enhancer is off) is worse than either alternative given how
buildErnieImageGraphemits its positive/negative encoder pair.Related Issues / Discussions
Follows up #9311 (which introduced the flag) and #9263 (multi-GPU parallel session execution).
QA Instructions
Requires two or more CUDA GPUs and
offload_text_encoders_to_idle_gpus: trueininvokeai.yaml.InvokeAIat DEBUG level, the session processor logsRunning <node type> on idle device cuda:N (session device cuda:M)for the text-encoder node. Before this PR that line never appeared for these four.Merge Plan
Nothing special. No node versions are bumped because no fields changed:
idle_gpu_offloadableis aClassVarset by the@invocationdecorator, not apydantic field, so it does not appear in the node schema and
schema.tsisuntouched. Bumping the version here would be actively harmful — it signals a
template change to the frontend and causes saved workflows to re-instantiate the
node, for no benefit.
Checklist
What's Newcopy (if doing a release after this PR)