Skip to content

fix: don't hold VRAM in an idle server (CUDA context at startup) - #9447

Merged
lstein merged 5 commits into
mainfrom
fix/idle-vram-cuda-context
Aug 3, 2026
Merged

fix: don't hold VRAM in an idle server (CUDA context at startup)#9447
lstein merged 5 commits into
mainfrom
fix/idle-vram-cuda-context

Conversation

@lstein

@lstein lstein commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

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-smi compute-apps after each call, not just torch.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() triggers cuInit and, 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:

  1. ModelCache.__init__ sizes the RAM cache via _calc_ram_available_to_model_cache(), which called torch.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_manager now constructs one ModelCache per generation device at startup, creating a context on every GPU. (It was also skipped entirely when the legacy ram: / max_cache_ram_gb setting was set — which is why the reporter's older images sat at 0 MiB.)
  2. Session-processor workers (new in feat: multi-GPU parallel session execution #9263) called 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 from torch.cuda.get_device_properties(...).total_memory instead of mem_get_info(). Verified byte-identical return value, no context created. The runtime mem_get_info() in _get_vram_available() (which genuinely needs free memory during model loads) is untouched.
  • Worker threads defer 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. The TorchDevice.set_session_device() threadlocal pin — which drives all device-selection logic (choose_torch_device, model loader, nodes) — still happens at thread start, unchanged.
  • One test constructed a ModelCache pinned to a nonexistent cuda:1 behind a mem_get_info patch; it now also patches get_device_properties.

Verification

  • Live server (fresh root, this branch): absent from nvidia-smi --query-compute-apps after 20+ s idle — while a pre-fix server on the same machine held 128 MiB as a control.
  • Enqueued a graph against the fixed server: completed successfully (1 completed, 0 failed), confirming the lazily-pinned worker loop functions.
  • All 295 model-cache + session-processor tests pass; ruff clean.

🤖 Generated with Claude Code

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>
@github-actions github-actions Bot added python PRs that change python files backend PRs that change backend files services PRs that change app services python-tests PRs that change python tests labels Aug 2, 2026
@lstein lstein added the 6.14.0 label Aug 2, 2026
@lstein lstein moved this to 6.14.x Theme: USER EXPERIENCE in Invoke - Community Roadmap Aug 2, 2026
lstein added a commit that referenced this pull request Aug 2, 2026
…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 JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 uses get_device_properties() and avoids mem_get_info(); modified existing test mocks both, so restoring old VRAM-holding call still passes. Test: construct CUDA ModelCache, assert get_device_properties(cuda:1) called and mem_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; assert set_device() is absent while empty, occurs before first runner call, and occurs exactly once.

@lstein
lstein enabled auto-merge (squash) August 3, 2026 14:26
@lstein
lstein merged commit b64c051 into main Aug 3, 2026
17 checks passed
@lstein
lstein deleted the fix/idle-vram-cuda-context branch August 3, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.14.0 backend PRs that change backend files python PRs that change python files python-tests PRs that change python tests services PRs that change app services

Projects

Status: 6.14.x Theme: USER EXPERIENCE

Development

Successfully merging this pull request may close these issues.

[bug]: CUDA context allocated at startup, before any generation (~256 MiB VRAM held while idle)

2 participants