fix: don't hold VRAM in an idle server (CUDA context at startup) - #9447
Conversation
Since #9263, a freshly started server held ~128-256 MiB of VRAM per GPU before serving any request. Two startup paths were responsible: - ModelCache.__init__ sized the RAM cache via torch.cuda.mem_get_info(), which creates a CUDA context on the execution device — and #9263 now builds one ModelCache per generation device at startup. Total VRAM is read from cudaGetDeviceProperties instead, which reports the same value (verified byte-identical) without creating a context. - Each session-processor worker called torch.cuda.set_device() at thread start (i.e. at boot). The CUDA-side pin is now deferred until the worker claims its first queue item; the pin is per-thread and the thread persists, so pinning before the first item is equivalent. The TorchDevice.set_session_device() threadlocal pin, which drives all device-selection logic, still happens at thread start. Verified on a live server: with this change the process no longer appears in nvidia-smi compute-apps while idle, and queue items still process correctly on the lazily-pinned worker. Closes #9413 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…9448) The four timeout=5 marks added in #9433 use method="thread", so the 5-second budget covers fixture setup and teardown as well as the test body. Tearing down the mm2_download_queue fixture alone takes up to ~1s (five worker threads polling the queue at a 1-second interval), and on a heavily loaded CI runner the total easily exceeds 5s: the py3.11 windows-cpu job on #9447 timed out in test_base_exception_during_startup_releases_import_waiters during download-queue teardown, and an unrelated branch hit the same timeout in test_import_fails_after_startup_failure on linux-cpu the same day. Neither dump showed a deadlock - the workers were in their normal poll loop. Bump these marks to 30s, matching the other timeout marks in this file. The timeouts exist to catch hangs, not to enforce speed, so the larger budget loses nothing. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
JPPhoto
left a comment
There was a problem hiding this comment.
Approved. I added some tests to this branch to guarantee behavior and everything is green:
-
invokeai/backend/model_manager/load/model_cache/model_cache.py:1166: No regression test proves startup usesget_device_properties()and avoidsmem_get_info(); modified existing test mocks both, so restoring old VRAM-holding call still passes. Test: construct CUDAModelCache, assertget_device_properties(cuda:1)called andmem_get_info()not called during initialization. -
invokeai/app/services/session_processor/session_processor_default.py:676: Lazy CUDA pinning has no automated coverage; future movement before dequeue could silently restore idle VRAM allocation. Test: run CUDA worker against empty queue, then two claimed items; assertset_device()is absent while empty, occurs before first runner call, and occurs exactly once.
Summary
Fixes #9413 — since #9263, a freshly started server allocated a CUDA context (~128–256 MiB of VRAM per GPU, size varies by GPU/driver) at startup and held it while idle, before any generation was requested.
Root cause
Empirical probing (checking
nvidia-smicompute-apps after each call, not justtorch.cuda.is_initialized()) narrowed the VRAM holder down:torch.cuda.mem_get_info()creates a VRAM-holding CUDA context (it must, to report free memory).torch.cuda.set_device()triggerscuInitand, per CUDA 12 semantics, may eagerly initialize on some drivers.get_device_properties()/get_device_name()/current_device()hold no VRAM (device property queries don't need a context).Two startup paths hit the problematic calls:
ModelCache.__init__sizes the RAM cache via_calc_ram_available_to_model_cache(), which calledtorch.cuda.mem_get_info()to read total VRAM. This path predates feat: multi-GPU parallel session execution #9263, but feat: multi-GPU parallel session execution #9263 amplified it:build_model_managernow constructs oneModelCacheper generation device at startup, creating a context on every GPU. (It was also skipped entirely when the legacyram:/max_cache_ram_gbsetting was set — which is why the reporter's older images sat at 0 MiB.)torch.cuda.set_device()at thread start, i.e. at server boot, on every configured generation device.Changes
ModelCache._calc_ram_available_to_model_cache()reads total VRAM fromtorch.cuda.get_device_properties(...).total_memoryinstead ofmem_get_info(). Verified byte-identical return value, no context created. The runtimemem_get_info()in_get_vram_available()(which genuinely needs free memory during model loads) is untouched.torch.cuda.set_device()until they claim their first queue item. The pin is per-thread and the thread persists, so pinning once before the first item is equivalent to pinning at thread start. TheTorchDevice.set_session_device()threadlocal pin — which drives all device-selection logic (choose_torch_device, model loader, nodes) — still happens at thread start, unchanged.ModelCachepinned to a nonexistentcuda:1behind amem_get_infopatch; it now also patchesget_device_properties.Verification
nvidia-smi --query-compute-appsafter 20+ s idle — while a pre-fix server on the same machine held 128 MiB as a control.🤖 Generated with Claude Code