Repository navigation
fix(app): guard model offloads during construction - #10218
wunianze666-netizen wants to merge 2 commits into
Conversation
Pfannkuchensack
left a comment
There was a problem hiding this comment.
Thanks for picking up #10009. The fix closes the race at all three sites and I found no blocker. One thing I'd like changed before merge, the rest are nits.
Reviewed at 35d0b87.
Should fix
The two offload methods on the node API now follow opposite lock rules, and neither docstring says so.
context.models.offload_from_vramnow takes the read lock itself (invocation_context.py:722).context.models.make_room_in_vramdoes not (invocation_context.py:743). Its only caller holds the lock from outside (qwen_image_text_encoder.py:547).
Both are public API for custom nodes, so two failures are possible:
- A node copies the Qwen pattern and wraps
offload_from_vraminMODEL_LOAD_LOCK.read_lock(). If a construction queues between the outer and the inner acquire, the thread deadlocks with itself, because the lock is non-reentrant and write-preferring. Every VRAM move and every construction in the process then stalls. Before this PR that pattern was safe. - A node calls
make_room_in_vramwithout the lock, as its docstring suggests is fine. That path reaches_move_model_to_ramandload_state_dict(assign=True)too, so it is the same race as #10009.
Suggested fix: take the read lock inside ModelsInterface.make_room_in_vram as well, drop the outer with in the Qwen encoder (and adjust its test), and add "the caller must not hold MODEL_LOAD_LOCK" to both docstrings, as model_load_base.py:46 does for its own contract. The docstrings are published in docs/src/generated/invocation-context.json, so that file needs regenerating.
If you'd rather keep this PR to the three sites, the minimum is the two docstrings plus the regenerated JSON.
Nits
- Cache docstrings.
offload_model_from_vram,offload_models_from_vram_except,offload_unlocked_models_from_vramandmake_room_in_vraminmodel_cache.pydon't say that the caller must hold the read lock. That gap predates this PR and is why three callers missed it; one sentence per method would close it. - Offloads under the read lock are not paced. Each of the three sites holds the read lock across the whole offload, including the
gc.collect()andempty_cache()in_offload_entries_from_vram.- With the default
keep_ram_copy_of_weights: truethis is short, since tensors are only re-pointed. - With
falseevery resident model is copied back, and a queued construction plus every VRAM move on other GPUs wait for that long. I have not measured it. - The first
lock()pass and the calibration offload already behave this way, so it is not a new kind of stall. - The comment on
VRAM_MOVE_PASS_BYTES("bounds any queued construction's wait to roughly one pass") does not hold for these paths. Worth a word in that comment.
- With the default
- Test run time. Each of the three race tests takes about 0.8 s here, nearly all of it the real
gc.collect()calls in the offload path.tests/AGENTS.mdasks forgc.freeze()around repeated collections (seetest_model_cache_ram_budget.py), or patchgc.collectin the fixture since it is not under test. - Tests that pass without the fix.
test_offloads_on_independent_caches_can_assign_weights_concurrentlyuses twoSimpleNamespacestand-ins and no construction, so the weight assertions cannot fail. What it checks is that readers overlap, which belongs intest_model_load_rw_lock.pyif it isn't covered there.test_failed_offload_releases_load_lock_and_preserves_exceptionand the addedassert_load_lock_idle()pairs test the lock's ownfinally, which awithat the call site cannot break.
- Fixture wiring.
offload_race = model_offload.offload_racein each test module is a third way of sharing fixtures. The repo registers shared ones intests/conftest.py(e.g.lost_races). - PR description.
- The ARM64 paragraph ("still being observed") is stale.
- The "equivalent repository commands" are not the ones that ran.
- The
model-cachescope doesn't match: all production changes are underinvokeai/app/.
What I checked
- Every path into
_move_model_to_ramfrom the three sites is inside the read lock (partial and full unload). - No in-repo caller already holds
MODEL_LOAD_LOCKor a cache lock: the seven PiD nodes,_on_before_run_sessionand the worker loop. The order is global lock before cache lock everywhere, also with several GPUs. - Exceptions, return values and the
empty_cache(force=True)after a successful session-end offload are unchanged; the latter now runs outside the lock. - Tests on Windows, CPU only:
- with the fix: 23 passed;
- with the three production files reverted to base: 3 failed with weights on
meta, 20 passed; - each of the three files passes on its own;
- 150 repeated runs under parallel load: no flake.
- Ruff 0.11.2 check and format are clean.
- CI is green except
windows-arm64, which fails before pytest on a Chocolatey 504.
Not checked: GPU runs with real models, the hold time with keep_ram_copy_of_weights: false, and custom node packs outside this repo.
|
Thank you for the detailed review. Addressed in 08998bb.
The shared race fixture is registered through These runs used Windows, Python 3.12, CPU Torch 2.13 and a Diffusers 0.42.1 overlay; they do not establish physical-GPU behavior, full-model behavior or unpaced offload duration. The previous ARM64 failure was an external Chocolatey FFmpeg 504 before pytest, and the new head's CI is pending. |
Summary
Protect application-initiated model offloads against concurrent model construction. Parameter assignment during Accelerate's process-global
init_empty_weightspatch can otherwise strand offloaded weights onmeta.Acquire the shared model-load read lock before the cache lock at architecture-switch, session-end and node API offloads. Both public node offload methods now own that lock; Qwen no longer acquires an outer lock. The public API docstrings, cache lock contracts and generated invocation-context documentation describe the ownership rule. Independent cache offloads retain shared-reader concurrency.
Related Issues / Discussions
Closes #10009.
QA Instructions
make_room_in_vramproduction method: one intended failure, with weights onmeta; the same fixture passes with the fix.pnpm run generate-docs-dataexited 0. Ruff 0.11.2 check and format check,git diff --check, and normal commit hooks passed. The unchangeduv lockhook had no files to check.pytest -n 0, with isolated application/temp roots and BLAS threads limited to one. Exact invocations are recorded in the task's command logs.The focused run used these pytest arguments after the task-local offline runner:
The tests exercise real Torch parameter assignment, Accelerate construction, ModelCache and the shared locks with small float32 CPU modules; VRAM residency/capabilities and allocator I/O are simulated. Windows / Python 3.12.14 / Torch 2.13 CPU / Diffusers 0.42.1 source overlay. Physical GPU, full model, family E2E, full locked environment and unpaced offload duration were not measured.
On the previous head, Windows ARM64 job
129337633587failed before pytest because Chocolatey's FFmpeg 9.0.2 feed returned HTTP 504. That attribution applies only to its actual checkout; checks for the new head must be evaluated separately.Review
The public lock ownership inconsistency and review nits were addressed; relevant checks were rerun. Lock waits remain non-cancellable, following the existing model-load lock contract. AI assistance was used for implementation and test authoring.
Checklist
What's Newcopy (if doing a release after this PR)Open in [code]smith · Need help on this PR? Tag
@codesmith-botwith what you need.