ADR-0025: VRAM co-management across processes¶
Status: accepted (2026-08-14); substantially corrected 2026-08-15 after a four-reviewer audit measured the WDDM behaviour this record had called merely "advisory". Two claims below were wrong as written and are struck in place rather than quietly edited: the protocol was inert, not best-effort, on the majority platform, and the "one decision-maker" line overstated what the code does. The repairs are recorded in ADR-0034 and ADR-0035.
Correction: the budget protocol did not work on Windows
torch.cuda.mem_get_info -- the source of get_free_memory's
device term -- reports the calling process's budget on WDDM, not
device-wide free. Measured (RTX 4060 Ti 16 GB, driver 581.57): a
sibling process allocated 13.0 GiB; nvidia-smi free fell 13,443 MB
while the parent's mem_get_info free fell 75 MB; at 4 GiB the
parent's delta was 0 MB.
ComfyUI evicts only when memory_required - get_free_memory(device)
is positive (model_management.py). With that free value
stuck near full-card, the difference was negative and
free_memory() evicted nothing in response to a worker's
request. The worker, reading the same blind number, then over-loaded.
Both sides overcommitted, and the driver's sysmem fallback -- named
below as the "mitigation" -- was in fact the only thing keeping the
system alive, at ~10x slower. Fixed in
ADR-0034.
Decision¶
ComfyUI's VRAM manager stays the single authority; workers hold VRAM only on lease. Worker-resident models exist in ComfyUI's
current_loaded_modelsledger asSubprocessModelPatcherentries, get evicted by ComfyUI's normal pressure logic, and the worker asks the parent for room before loading. Two allocator populations, one decision-maker.
Correction: 'one decision-maker' is an aspiration, not a description
The worker runs its own ComfyUI with its own current_loaded_models
and its own free_memory, and only load_models_gpu is shimmed.
The parent additionally reaches into the worker's ledger to pop
entries by hand. There are two ledgers; the parent's authority
is real for admission and eviction, but the claim as written
overstated it. The honest version: the parent decides admission and
when to evict; the worker still manages its own residency in
between.
The protocol, as shipped:
- Detection: worker-side
Module.to()/.cuda()hooks auto-register models that land on GPU; metadata (id, size, device) rides back on the call response (_new_models), and the parent builds aSubprocessModelPatcherper model and inserts it intocurrent_loaded_models. What "counts": anything that moved to CUDA through the hooked paths -- false negatives (manual cudaMalloc, non-Module allocations) are unbudgeted worker overhead; false positives are possible for transient modules. No pack-side opt-out exists yet; add one if a real pack needs it. - Admission: the worker's
load_models_gpushim calls back (request_vram_budget); the parent evicts through ComfyUI's own machinery until the request x 1.1 headroom fits, then grants. The 10% headroom absorbs allocator slack and context growth; it is a guess that has held -- change it only with a measurement. - Eviction: parent-initiated
model_to_devicecommands move worker models to CPU via the worker's patcher path; ComfyUI decides when (its normal free-memory loop), comfy-env decides how. - Restart/kill interaction: on worker death the patchers follow
the ADR-0019
_STALE_PATCHERSprotocol; an ADR-0018 timeout kill therefore also invalidates every lease that worker held -- the caches are rebuilt on the next generation's first load. - ~~
COMFY_ENV_WORKER_VRAM_BUDGEToverrides the worker'svram_state(aNO_VRAMworker is promoted toNORMAL_VRAMunder an explicit budget) -- the escape hatch for setups where detection misjudges the card.~~ Removed 0.4.25: never set by anyone, untested, and the blindness-corrected negotiation (ADR-0034/0036) computes the honest number automatically. The callback itself -- points 1-4 -- is unchanged.
Single-device today, and the two recorded landmines¶
The entire ledger is single-device by assumption: budget negotiation
uses the singular mm.get_torch_device(), the worker binds
current_device(), and the parent-side pool patch hardcodes device 0.
Decision: acceptable until a real multi-GPU pack exists, but the
budget/model_to_device messages gain a device field the first time
anyone touches this protocol again (reserve now, semantics later), and
the wire's existing device_idx (CudaIPC/PoolIPC frames) silently
assumes identical device enumeration in parent and worker -- a
pack env setting its own CUDA_VISIBLE_DEVICES (some do, to hide GPUs
from bpy) turns device_idx into a wrong-device import with no error.
Any multi-GPU work starts by exchanging device UUIDs in the handshake
and mapping indices, never trusting them.
The WDDM position¶
The protocol reasons in Linux terms: free VRAM is a hard number,
exhaustion is an OOM. On Windows/WDDM -- the majority platform -- the
OS pages VRAM and mem_get_info is advisory. With the driver's default
sysmem-fallback policy (on since R510), overcommit does not fail: it
silently demotes allocations to system memory and runs ~10x slower;
a user who has set "prefer no sysmem fallback" gets a real OOM instead,
so the softer failure mode is the common case, not a guarantee.
~~Recorded position: the budget protocol is best-effort on
WDDM -- the 1.1 headroom plus ComfyUI's own conservative accounting
is the mitigation, the common failure mode is a slowdown rather than a
crash (better than the Linux failure mode), and no additional
WDDM-specific machinery (HAGS detection, residency queries) is built
until a profiled case shows the slowdown biting in practice.~~
Struck 2026-08-15. This was wrong in kind, not degree. "Advisory" implied an approximate number; the number was unrelated to the device. The 1.1 headroom was not a mitigation, it was ceremony -- it multiplied a target that a negative comparison then discarded entirely. And the slowdown was not an acceptable failure mode we had chosen; it was the symptom of the protocol not running. The profiled case the paragraph waited for is the measurement in the correction box above.
Current position: admission is decided by arithmetic comfy-env owns,
never by mem_get_info -- see
ADR-0034. The WDDM sysmem-fallback
description remains accurate and still explains why the failure was
survivable rather than loud.
Context¶
This bridge -- two independent allocator populations sharing one GPU without OOMing each other -- is arguably the hairiest correctness surface in the project and had no record; the 2026-08 reviews ranked it the largest unrecorded subsystem. It is also the deepest part of the ADR-0024 loan book (entries 1-4): the entire mechanism is a stand-in for a VRAM lease API that ComfyUI does not offer. This ADR documents the stand-in; 0024 records the ask that retires it.
Consequences¶
- One decision-maker means no split-brain: workers never evict each other directly; every eviction flows through ComfyUI's loop.
- The cross-worker call chain (worker A's budget request evicting into worker B) is the lock-ordering hazard named in ADR-0020; the pending-map work must keep it single-flight or order the locks.
- Detection-by-hook means the ledger is only as good as the hooks;
packs allocating GPU memory outside
nn.Modulemovement are invisible to the budget. Known, accepted, revisit per-pack.