# ComfyUI GPU Resident Loader A ComfyUI custom-node pack for **faster time-to-VRAM**, **selective safetensors loading**, **sticky GPU residency control**, and **visibility into compatible external GPU model caches**. This repo does three related jobs: 1. **Installs startup-time monkey patches** before any workflow nodes run. 2. Ships **KJ-style resident loader nodes** for diffusion models and checkpoints. 3. Maintains a **live residency system** for native ComfyUI objects and compatible external caches, with preload / pin / evict / report controls for native tracked objects and automatic snapshot support plus provider-specific eviction for compatible external entries. It is not just a “clean RAM” addon. The main target is the path from model file -> tensors -> live ComfyUI object -> VRAM retention, including GPU-resident caches that live outside ComfyUI’s normal loaded-model list. ## Why this exists ComfyUI’s default behavior mixes together two separate concerns: - **ingest path** — where tensors are first materialized while a model is being read, and - **residency policy** — where the finished model tends to live afterwards. Those are not the same problem. This repo focuses on both: - For **`.safetensors`**, it tries to keep eligible loads on the narrowest, most GPU-friendly path it can. - For **resident diffusion and checkpoint-model loads**, it avoids broad checkpoint materialization by selecting only the detected UNet keys where possible. - For **runtime VRAM pressure**, it adds a sticky-priority registry and teaches ComfyUI’s unload path to protect higher-value resident entries until enough VRAM must be reclaimed. - For **compatible external GPU caches** that bypass `comfy.model_management.current_loaded_models`, it can discover supported providers at runtime and include their entries in snapshot output and provider-specific eviction decisions, with automatic trim kept opt-in. - For **manual control**, it exposes nodes that let you preload, pin, evict, and inspect tracked native models, CLIPs, and VAEs. ## What changes at startup `__init__.py` calls `startup.install_patches()`, which applies the core monkey patches exactly once when the custom node is imported. Those startup patches cover the built-in ComfyUI load and residency paths below. Compatible external-cache integrations are installed lazily later, on demand, when a supported module is actually present in the running process. ### Patched functions / methods Current patch surface: - `comfy.utils.load_torch_file` - `comfy.clip_vision.load_torch_file` (redirected to the patched `comfy.utils.load_torch_file` when present) - `comfy.model_management.free_memory` - `comfy.model_management.load_models_gpu` - `comfy.model_management.unet_offload_device` - `comfy.model_management.text_encoder_offload_device` - `comfy.model_management.vae_offload_device` - `comfy.model_management.text_encoder_device` - `comfy.model_management.vae_device` - `comfy.model_management.unet_inital_load_device` - `comfy.model_management.LoadedModel.model_unload` - `comfy.model_patcher.ModelPatcher.detach` - `comfy.sd.load_checkpoint_guess_config` - `comfy.sd.load_diffusion_model` - `comfy.sd.load_clip` - `comfy.sd.VAE.encode` - `comfy.sd.VAE.decode` - `comfy.clip_vision.load` - `comfy.controlnet.load_controlnet` - `comfy.diffusers_load.load_diffusers` ### Lazily installed external integrations Current external integration surface: - compatible **SeedVR2** `src/core/model_cache.py` modules discovered at runtime When a compatible SeedVR2 cache module is present, the repo wraps: - `GlobalModelCache.set_dit` - `GlobalModelCache.set_vae` - `GlobalModelCache.replace_dit` (when that method exists in the installed SeedVR2 build) - `GlobalModelCache.replace_vae` (when that method exists in the installed SeedVR2 build) - `GlobalModelCache.remove_dit` - `GlobalModelCache.remove_vae` That lazy integration lets the loader: - mirror SeedVR2-owned cached **DiT** and **VAE** objects into a separate external residency registry - refresh byte / device / claimed-state metadata from the live cached object - evict those entries through SeedVR2’s own removal path instead of assuming they live in `comfy.model_management.current_loaded_models` ## What those patches do ### 1) `load_torch_file` becomes residency-aware The patched loader: - detects the active load context (`model`, `clip`, `vae`, `checkpoint`, etc.) - picks an explicit GPU target device when the active policy wants GPU ingest - attempts **direct safetensors reads** on the requested device - falls back to **CPU read + tensor-by-tensor copy** if direct GPU safetensors loading fails - still uses **CPU-first `torch.load()`** for pickle formats (`.ckpt`, `.pt`, `.pth`, `.bin`) - records the actual load method in the residency registry For safetensors loads happening inside a `model` / `clip` / `vae` context, it can select only the detected component keys from the file header instead of pulling the full file into memory first. ### 2) Loader contexts are attached to stock ComfyUI load paths These stock paths are wrapped with registry context and output binding: - checkpoint loads - diffusion-model loads - CLIP loads - CLIP Vision loads - ControlNet loads - diffusers loads That means the native registry is not limited to the custom resident nodes. Stock ComfyUI loaders that pass through these paths are also tracked. ### 3) Compatible external caches can join the residency system lazily When a compatible SeedVR2 cache module is present, the loader installs cache-level hooks that register SeedVR2-owned cached DiT / VAE objects into a separate external registry. Those entries: - are refreshed from the live cached object at runtime - appear in **Registry Snapshot** output under `external_entries` - are evicted through SeedVR2’s own cache-removal methods rather than the normal Comfy unload path - remain outside automatic trim unless `COMFYUI_GPU_RESIDENT_TRIM_EXTERNAL=1` is set ### 4) Device/offload policy is overridden Depending on the active policy, the patcher can steer: - initial UNet load device - CLIP/Text Encoder device - VAE device - offload devices for UNet / CLIP / VAE This is how `prefer_gpu` and `sticky_gpu` keep more of the hot path on the GPU side than stock ComfyUI would. ### 5) `free_memory()` becomes sticky-aware Under `sticky_gpu`, `comfy.model_management.free_memory()` is patched so that: - sticky tracked wrappers are considered first - higher-priority sticky entries are protected first - lower-priority or older sticky entries yield first when VRAM must be reclaimed - a transient protection floor is applied so ComfyUI does not immediately tear down high-value resident entries for small requests ### 6) Clone replacement is hardened `load_models_gpu()` is patched to fully unload clone-conflict wrappers before replacement instead of relying on a shallow detach path that can leave base weights patched. ### 7) Unload / detach is redirected to CPU when needed `LoadedModel.model_unload()` and `ModelPatcher.detach()` are patched so that unloads which would otherwise not reclaim VRAM are redirected through a CPU offload target first. ### 8) VAE encode/decode gets a sticky-safe path Under `sticky_gpu`, patched `VAE.encode()` and `VAE.decode()`: - cap the working batch count when necessary to preserve transient VRAM headroom - retry with tiled VAE encode/decode on OOM That behavior is not a general performance feature toggle. It exists to reduce avoidable VRAM spikes while sticky residency is active. ## Policies The registry exposes four global policies: ### `legacy` Stay closest to stock ComfyUI behavior. Registry tracking still exists, but the patcher does not aggressively steer ingest/offload toward the GPU path. ### `balanced` Keep registry tracking and diagnostics without aggressive GPU residency behavior. ### `prefer_gpu` Prefer GPU ingest for tracked model-like loads and keep the faster side of the device/offload policy for: - diffusion models - CLIP / text encoders - ControlNets This policy does **not** auto-pin tracked objects. ### `sticky_gpu` Builds on `prefer_gpu` and additionally: - auto-pins newly bound **models** and **CLIPs** - keeps **VAE offload** on the GPU side as well - patches `free_memory()` to protect sticky tracked wrappers by priority - uses the sticky-safe VAE encode/decode behavior ### Default policy selection Selection order is: 1. `COMFYUI_GPU_RESIDENT_POLICY`, if set to a supported value 2. `sticky_gpu` when ComfyUI is started with `--gpu_only` 3. `sticky_gpu` when ComfyUI is started with `--highvram` 4. otherwise `prefer_gpu` Supported values are: - `legacy` - `balanced` - `prefer_gpu` - `sticky_gpu` ## Included nodes All nodes live under the `GPU Resident Loader` category. ### Loader nodes #### Diffusion Model Selector Resident Returns an absolute path string for a selected diffusion model. Notes: - resolves from `diffusion_models` - also exposes `text_encoders` entries whose filename contains `connector` #### Diffusion Model Loader Resident KJ-style diffusion-model loader with these controls: - `weight_dtype` - `compute_dtype` - `patch_cublaslinear` - `sage_attention` - `enable_fp16_accumulation` - optional `extra_state_dict` - optional `policy_override` Behavior: - for `.safetensors`, it loads only the detected UNet portion of the file - if `extra_state_dict` is provided, only matching UNet keys are merged - repeated loads reuse a live equivalent model when the source path and loader-relevant options still match - before GPU-bound loads, it estimates the upcoming footprint and trims only enough lower-priority residency to cover the request plus adaptive headroom #### Checkpoint Loader Resident Full checkpoint loader that returns: - `MODEL` - `CLIP` - `VAE` Behavior: - shares the same tuning knobs as the resident diffusion-model loader for the model component - reuses already-live equivalent components when possible - composes the final output from model / clip / vae component loaders instead of always rebuilding the whole checkpoint path from scratch #### Checkpoint Model Loader Resident Model-only checkpoint loader. Behavior: - takes the same selective safetensors UNet fast path as the diffusion-model loader - reuses a live equivalent model when available - uses the same dtype / attention / cublas / fp16-accumulation knobs as the full checkpoint loader #### Checkpoint Clip Loader Resident CLIP-only checkpoint loader. Behavior: - can reuse a live equivalent CLIP object - avoids rebuilding the diffusion model and VAE outputs when only CLIP is needed #### Checkpoint VAE Loader Resident VAE-only checkpoint loader. Behavior: - can reuse a live equivalent VAE object - avoids rebuilding the diffusion model and CLIP outputs when only VAE is needed ### Residency nodes #### Set Global Residency Policy Sets the active global policy and returns it as a `STRING`. The loader nodes also expose an optional `policy_override` string input for one-off loads. #### Registry Snapshot Returns a composite formatted JSON snapshot with: - `policy` for the active global policy - `entries` for native Comfy-managed registry entries - `external_entries` for compatible external cache entries discovered at runtime #### Pin Model Residency / Pin CLIP Residency / Pin VAE Residency Marks a tracked native object as sticky or non-sticky and optionally changes its priority. #### Preload Model To GPU / Preload CLIP To GPU / Preload VAE To GPU Calls `load_models_gpu(..., force_full_load=True)` for the selected native object, then updates sticky state / priority in the registry. #### Evict Model From GPU / Evict CLIP From GPU / Evict VAE From GPU Attempts to unload the selected native object from the current loaded-model set. `unpatch_weights=True` performs a full unload path. When eviction succeeds, the node returns `evicted`; otherwise `not_loaded`. #### Report Model Residency / Report CLIP Residency / Report VAE Residency Returns a JSON report for a single tracked native object. If the object is not currently bound in the registry, the node returns a JSON payload with `tracked: false`. ## Adaptive trimming before resident loads The resident loaders now do load-scoped VRAM trimming themselves. Before a GPU-bound resident load, the loader estimates required bytes from: - the safetensors header when possible - the detected checkpoint component subset when possible - otherwise the source file size as a fallback It then requests enough free VRAM for: - the estimated load size - adaptive headroom Current adaptive headroom policy: - ratio: `12.5%` of the estimated load - floor: `256 MiB` - ceiling: `1 GiB` The trim path prefers to: - unload non-sticky entries first - then lower-priority sticky entries - preserve explicitly kept models - use partial unload where available - include compatible external cache entries in the same candidate search only when `COMFYUI_GPU_RESIDENT_TRIM_EXTERNAL=1`, they are visible, on the same device, and not currently claimed/in use This logic lives in the resident loader path. You do not need a separate “target free VRAM” node for it. ## Registry and observability The snapshot now exposes two collections: - `entries` for native Comfy-managed tracked objects - `external_entries` for compatible external cache objects ### Native registry entries The native registry tracks residency metadata for bound objects. Typical per-entry fields include: - `entry_id` - `kind` - `source_path` - `basename` - `sticky` - `priority` - `created_at` - `last_touched` - `loaded_bytes` - `total_bytes` - `load_device` - `offload_device` - `current_device` - `last_method` - `last_report` - `loader_key` - `notes` - `alive` The `last_method` / `last_report` fields let you see whether a load actually used: - direct safetensors GPU ingest - safetensors CPU -> CUDA fallback - safetensors component-only load - CPU-first `torch.load()` compatibility path - a recorded load failure ### External registry entries Typical external-entry fields include: - `entry_id` - `cache_key` - `kind` - `source_path` - `basename` - `sticky` - `priority` - `created_at` - `last_touched` - `loaded_bytes` - `total_bytes` - `load_device` - `offload_device` - `current_device` - `claimed` - `notes` - `alive` - `external` For SeedVR2-backed entries, `claimed: true` means the cache object is currently marked in use and is skipped by the external trim candidate search when that opt-in path is enabled. ## What gets tracked ### Native tracked/bound paths Native tracked paths include: - resident node loads from this repo - stock checkpoint loads - stock diffusion-model loads - stock CLIP loads - stock CLIP Vision loads - stock diffusers loads ControlNet loads also participate in the patched load context and device-policy path, but this repo does not currently expose dedicated ControlNet residency nodes. ### External tracked paths Current external integration coverage is: - SeedVR2 global cached **DiT** entries - SeedVR2 global cached **VAE** entries Those entries are discovered lazily from compatible SeedVR2 cache modules at runtime. They are tracked separately from the native registry and participate in snapshot plus provider-specific eviction decisions, with load-scoped trim available only through the explicit external-trim opt-in. ## Important limits and non-goals ### Best path is still `.safetensors` The narrow fast path is built around `.safetensors`. That is where this repo can: - inspect headers cheaply - select only model / clip / vae subsets - estimate component bytes more accurately - attempt direct device-targeted reads ### `.ckpt` / `.pt` / pickle formats are still CPU-first For pickle-based formats, PyTorch still goes through `torch.load()` on CPU first. The repo can still: - track those loads - keep the resulting live objects resident - reuse equivalent live objects later It does **not** claim direct-to-GPU ingest for those formats. ### Cross-process persistence is out of scope This repo does **not** keep VRAM allocations alive after ComfyUI, Python, or WSL exits. CUDA memory lifetime is process/context scoped. True persistence across process shutdown would need a separate long-lived keeper process or service that owns the CUDA context. ### External integrations are compatibility-based, not universal The external registry does **not** automatically manage every third-party cache. At the moment, the documented external integration target is **SeedVR2**. Other custom nodes with private caches remain invisible until this repo grows a provider-specific integration for them. ### It does not automatically capture arbitrary custom loader implementations The native registry only sees objects that pass through the patched ComfyUI load paths or through this repo’s resident nodes. If another custom node loads models through its own private code path and bypasses those patched entry points, that object may never become a tracked native registry entry. In that case, the preload / pin / evict / report nodes from this repo cannot manage it until that external loader is integrated or patched. Likewise, even for supported external providers such as SeedVR2, the current external integration is about **observation + provider-specific eviction**, with automatic trim kept opt-in. This repo does **not** yet expose dedicated external preload / pin / report / evict nodes for provider-owned cache entries. ## Installation Clone into `custom_nodes`: ```bash git clone https://github.com/xmarre/ComfyUI-GPU-Resident-Loader ComfyUI/custom_nodes/ComfyUI-GPU-Resident-Loader ``` Install dependencies into the same Python environment ComfyUI uses: ```bash pip install -r ComfyUI/custom_nodes/ComfyUI-GPU-Resident-Loader/requirements.txt ``` Requirements declared by the repo: - Python `>=3.10` - `safetensors>=0.4.3` Optional SageAttention dependencies are **not** installed by default. Install those separately if you plan to use a SageAttention mode in the resident loaders. ## Basic usage patterns ### 1) Large-VRAM, mostly resident workflow Recommended baseline: - start ComfyUI with `--highvram` or set policy manually to `sticky_gpu` - prefer `.safetensors` for hot models - load diffusion models through **Diffusion Model Loader Resident** - use **Preload ... To GPU** for models you know you will reuse - inspect with **Report ... Residency** or **Registry Snapshot** ### 2) Full checkpoint workflow Use **Checkpoint Loader Resident** when you want `MODEL + CLIP + VAE` together. That path can reuse already-live components instead of always rebuilding all three outputs. ### 3) Staged checkpoint workflow Use component loaders when the graph does not need the whole checkpoint at once: - **Checkpoint Model Loader Resident** for diffusion model only - **Checkpoint Clip Loader Resident** for CLIP only - **Checkpoint VAE Loader Resident** for VAE only ### 4) Manual native residency control Use: - **Pin ... Residency** to mark a tracked native entry sticky / non-sticky - **Preload ... To GPU** to force a full live native load now - **Evict ... From GPU** to unload it from the current native loaded-model set ### 5) Mixed workflows with SeedVR2 external caching If SeedVR2 keeps DiT or VAE models in its own global cache, those objects can now show up in **Registry Snapshot** under `external_entries`. That means: - you can see that those bytes exist even though they are outside `current_loaded_models` - resident loader trim only reclaims them when `COMFYUI_GPU_RESIDENT_TRIM_EXTERNAL=1`, they are visible, on the same device, and not currently claimed/in use - eviction goes through SeedVR2’s own cache-removal path instead of a normal Comfy wrapper unload ## Notes on compatibility and migration ### Legacy wiring: `extra_state_dict` used as a policy string The resident diffusion-model loader contains a compatibility shim for older graphs: - if `extra_state_dict` receives one of the known policy names - and that value is **not** an existing file path - it is interpreted as `policy_override` instead New graphs should connect policy strings to **`policy_override`**, not to `extra_state_dict`. ### Convert hot pickle checkpoints to safetensors `scripts/convert_checkpoint_to_safetensors.py` is included for one-time conversion of hot `.ckpt` / `.pt` / `.pth` style checkpoints. Example: ```bash python ComfyUI/custom_nodes/ComfyUI-GPU-Resident-Loader/scripts/convert_checkpoint_to_safetensors.py \ --input /path/to/model.ckpt \ --output /path/to/model.safetensors ``` Optional flags: - `--state-dict-key ` to extract a different top-level dict key - `--allow-non-tensor-values` to skip non-tensor entries instead of failing ## License GPL-3.0-or-later. This repo stays GPL-compatible because it adapts behavior from GPL-licensed ComfyUI and mirrors relevant loader behavior from GPL-3.0-licensed KJNodes.