14 KiB
ComfyUI Performance Investigation Notes
System Specs
- GPU: NVIDIA GeForce RTX 3060 (12GB VRAM, compute capability 8.6)
- RAM: 32GB
- Storage: 1TB Seagate Barracuda 7200rpm HDD (ST1000DM010) — 97% full, ~0.23 GB/s sequential read
- Swap: 24GB file swap + 15.6GB zram (barely used: pswpin=12, pswpout=1382)
- OS: Linux (swappiness=20)
- Driver: 580.173.02 (
nvidia-smi; driver was upgraded in the same window as the torch upgrade) - Compute capability: 8.6 (Ampere)
GPU driver / CUDA details (as measured)
| Component | Version | Notes |
|---|---|---|
| NVIDIA driver | 580.173.02 | Upgraded alongside torch — one of the two suspected causes of the regression |
| CUDA driver API (torch) | 13.0 | torch.version.cuda |
| CUDA runtime (pip) | 13.0.2 | cuda-toolkit |
| cuda-bindings | 13.3.0 | |
| cuDNN (runtime) | 9.15.1.9 | Upgraded from 9.13.0.50 to clear the Conv3D workaround — see Issue 2 |
| cuBLAS | 13.0.0.19 | |
| cuSPARSELt | 0.8.0 | |
| NCCL | 2.27.7 | |
| NVJITLink | 13.0.39 |
Python / torch stack (venv at venv/)
| Package | Version | Notes |
|---|---|---|
| torch | 2.9.1+cu130 | Requires nvidia-cudnn-cu13==9.13.0.50 per its metadata — ignored when 9.15.1.9 installed (ABI-compatible) |
| torchvision | 0.24.1+cu130 | |
| torchaudio | 2.9.1+cu130 | |
| triton | 3.5.1 | |
| comfy-kitchen | 0.2.31 | Kitchen CUDA ops gated on cu130+ (comfy/quant_ops.py) |
| comfy-aimdo | 0.4.13 | Underlies DynamicVRAM (VBAR faulting); disables CUDA ops with --disable-dynamic-vram |
Relevant torch-version gates hit by this stack
NVIDIA_MEMORY_CONV_BUG_WORKAROUND(comfy/ops.py:101-110) activates for cuDNN[91002, 91500)AND torch 2.9–2.10 — WAS active on 9.13.0 + torch 2.9.1, cleared by cuDNN 9.15.1.- DynamicVRAM is auto-enabled on NVIDIA with torch >= 2.8 (
main.py:251-253) — this is the per-prompt overhead (Issue 1). SDPA_BACKEND_PRIORITY = [FLASH, CUDNN, EFFICIENT, MATH]active on NVIDIA torch 2.9 (comfy/ops.py:68-73).ENABLE_PYTORCH_ATTENTIONauto-set on NVIDIA torch 2.x (comfy/model_management.py:464-467).--use-pytorch-cross-attention,--use-split-cross-attention,--use-quad-cross-attention,--use-pytorch-splat-attentionoverride the default attention path if needed.
Model Files
| Model | File | Size | Format |
|---|---|---|---|
| Klein 9B | diffusion_models/KLEIN/flux2Klein9BKVInt8_v10.safetensors |
8.8 GB | int8 KV quantized |
| MiniMax H3 | diffusion_models/minimax_h3_* |
20-22 GB | int8 quantized variants |
Issue 1: Klein 9B — Slow on Every Run
Symptom
Every prompt execution took ~175s, with only ~27s spent in actual sampling. The 150s overhead occurred on every run, not just the first.
Root Cause: DynamicVRAM Per-Prompt Overhead
The ModelPatcherDynamic (enabled by default with comfy-aimdo) runs the full load() path on every prompt execution via partially_load() at model_patcher.py:2141:
def partially_load(self, device_to, extra_memory=0, force_patch_weights=False):
with self.use_ejected(skip_and_inject_on_exit_only=True):
dirty = self.model.current_weight_patches_uuid is not None and \
(self.model.current_weight_patches_uuid != self.patches_uuid)
self.unpatch_model(self.offload_device, unpatch_weights=False)
self.patch_model(load_weights=False)
self.load(device_to, dirty=dirty) # <-- runs every prompt
load() (model_patcher.py:1853) rebuilds the entire per-module setup:
_load_list()— iterates every module (~500+ for a 9B model)- Per-module
setup_param()+get_key_weight()loop - VBAR allocation checks (skipped if
_vexists, but loop still runs) - Buffer copies to GPU
Even when dirty=False (no LoRA changes, same prompt), the full per-module Python loop runs. The VBAR data stays resident (signatures preserved), but the setup overhead adds up across 500+ modules.
Why --disable-dynamic-vram Fixes It
With --disable-dynamic-vram, the legacy ModelPatcher is used instead:
- First run: slower (disk→RAM→VRAM, estimation-based partial load)
- Subsequent runs: fast (~52s total, 27s sampling) because weights stay fully resident in VRAM after first load
- No per-module VBAR setup loop on each prompt
Verdict
For Klein 9B (8.8GB quantized on 12GB VRAM), the legacy patcher is the better choice. The model fits partially in VRAM, and the legacy path keeps it resident without per-prompt rebuild overhead.
Working config for Klein 9B:
--disable-dynamic-vram --cache-classic
What Was NOT the Bottleneck
- Swappiness — Not the issue. Model stays in RAM (22GB available, 8.8GB model). Swap barely used.
- Disk I/O — 0.23 GB/s HDD read is slow but only affects first load. Subsequent runs read from page cache.
- cuDNN Conv3D workaround — Was active (cuDNN 9.13.0 + torch 2.9.1 triggers
NVIDIA_MEMORY_CONV_BUG_WORKAROUND). Upgraded to cuDNN 9.15.1 to clear it. Did NOT help — the workaround only affects VAE Conv3d decode, which wasn't the bottleneck. --disable-mmap— Added 15s upfront (eager read into RAM) but didn't help warm runs.- CPU→GPU transfer bandwidth — Measured at 2.26 GB/s. 8.8GB would transfer in ~4s. Not the bottleneck.
--cache-classic— Changes node output caching strategy, not model loading. The speedup came from--disable-dynamic-vram.
Issue 2: cuDNN Conv3D Workaround (Resolved)
Symptom
After upgrading torch to 2.9.1+cu130 and driver to 580.173.02, NVIDIA_MEMORY_CONV_BUG_WORKAROUND was activated.
Cause
cuDNN 9.13.0 (torch 2.9.1+cu130's bundled version) falls in the affected range:
# comfy/ops.py:105
if (cudnn_version >= 91002 and cudnn_version < 91500) and \
torch_version_numeric >= (2, 9) and torch_version_numeric <= (2, 10):
NVIDIA_MEMORY_CONV_BUG_WORKAROUND = True
This replaces the normal Conv3d forward with a raw torch.cudnn_convolution call:
# comfy/ops.py:614-618
if NVIDIA_MEMORY_CONV_BUG_WORKAROUND and weight.dtype in (torch.float16, torch.bfloat16):
out = torch.cudnn_convolution(input, weight, self.padding, self.stride,
self.dilation, self.groups, benchmark=False,
deterministic=False, allow_tf32=True)
Fix
Upgraded cuDNN from 9.13.0.50 to 9.15.1.9:
pip install nvidia-cudnn-cu13==9.15.1.9
cuDNN 9.15.1 (reports as 91501) is above the < 91500 threshold, so the workaround no longer triggers. The torch dependency conflict warning is expected but safe due to cuDNN ABI compatibility.
Impact
Minimal for Klein (affects only VAE Conv3d decode path). Would be more significant for models with heavier Conv3d usage.
Issue 3: MiniMax H3 — 20GB on 12GB VRAM
Status
Resolved (2026-09-08). Working config found. MiniMax H3 is 20GB (int8 quantized) on a 12GB GPU. DynamicVRAM is mandatory; the legacy patcher path cannot be used with H3 at all (it freezes the machine — see Issue 4).
Recommendation (working startup flags)
DynamicVRAM is the correct tool for models that don't fit in VRAM. The per-prompt overhead is a ComfyUI design limitation that would need a code fix (caching VBAR setup state across identical prompts) to resolve properly.
Working startup line for MiniMax H3 (RTX 3060 12GB / 32GB RAM):
./venv/bin/python main.py --listen --vram-headroom 2.0 --cache-ram 7 7
- Do NOT use
--disable-dynamic-vramwith H3. The legacy LOWVRAM path materializes the LoRA-merged DiT (~20GB) and the CLIP encoder (~15GB) as non-evictable RAM tensors simultaneously — ~35GB on a 32GB machine → RAM collapse + swap thrash + machine freeze (Issue 4). DynamicVRAM (default on, viacomfy-aimdo) evicts RAM properly. --vram-headroom 2.0keeps 2GB of VRAM free for the activation spike during DiT layer forwards (the anti-OOM knob; legacy equivalent is--reserve-vram 2.0). It does not force the model to RAM — the 20GB model streams from RAM regardless, but the headroom stopstorch.cuda.OutOfMemoryErrormid-step.--cache-ram 7 7— both values are free-RAM floors, not cache sizes (see Issue 4 for proof and details).7 7= keep ≥7GB of RAM free for the OS at all times.
Measured performance (2026-09-08, RTX 3060 12GB / 32GB RAM, 7200rpm HDD)
| Run | Config | Wall time | Result |
|---|---|---|---|
| Staged 3×10s → ~30s master (243f/shot, 640×640, seed 42) | ref2v turbo 4st v0.1 @ 0.75, 4 steps | 26m06s (1566 s) | Completed end-to-end, RAM stable |
| Single-shot 3.4s test (81f, 640×640) | fl2v turbo 8st v1.0 @ 1.0, 8 steps | 10m11s (611 s) | Completed; ~100s was model load + CLIP encode, the rest sampling |
During sampling at dynamic VRAM: GPU util ~100%, VRAM in-use ~6-7GB (weight streaming), RAM available bottomed at ~3.2GB (floor held), swap used held at ~1.4GB (no thrash). First load is slow because the 20GB weights come off a 0.23GB/s HDD; subsequent runs benefit from page cache.
Quality findings (which turbo LoRA to use)
minimax_h3_ref2v_turbo_4step_v0.1is a Ref2VA-only distill — it needs reference conditioning (character/pose/motion/voice) and the Ref2VA graph. Used on a plain text-to-video FL2VA workflow it produced visibly poor output (soft/blurry, "not shareable").- The correct turbo checkpoint for plain T2V on the FL2VA base is
minimax_h3_fl2v_turbo_8step_v1.0at strength 1.0, 8 steps,res_multistep/simple— verified much better (user: "really good"). - Always validate quality on a short single-shot clip before committing to a full 3×10s run (~26-40min).
Silver H3 Batch node (comfyui-silver-nodes)
Given that each MiniMax H3 generation pays heavy model-swap costs (the model doesn't fit in VRAM, so the system pages modules in/out around sampling), the SilverH3Batch node exists to amortize those swaps across a batch of clips rather than paying them per-clip:
- Queuing the minimax node N times pays
~3 x Nmodel swaps: text encoder -> DiT -> VAE per clip (each clip re-loads the TE to encode, the DiT to sample, the VAE to decode). SilverH3Batchstages the work into three phases so each model loads exactly once for the whole batch:- Phase A — ENCODE: every prompt text-encoded in one TE session; start frames / character image / character voice / guide audio encoded once and reused.
- Phase B — SAMPLE: DiT hot, every video sampled back-to-back (no chaining / handoff unless combining).
- Phase C — DECODE + SAVE: VAEs hot, each latent decoded, streamed to its own mp4 and freed immediately — at most ONE clip's frames are resident.
- Net: 2 model swaps per batch (TE -> DiT, DiT -> VAE) instead of
~3 x N, and RAM stays ~one clip regardless of batch size. The_evict()helper performs a controlled VRAM sweep at each phase boundary, never mid-phase. - This directly mitigates the 20GB-on-12GB problem: it doesn't make the model fit, it removes the repeated load/unload churn that a naive per-clip queue would cause on a tightly-paged GPU.
- Implementation:
custom_nodes/comfyui-silver-nodes/silver_h3_batch.py(andsilver_h3_staged.pyfor the chained multishot variant). - Without dynamic VRAM, the classic fallback would still thrash model weights during each video — the node's value is in keeping the loaded models resident across all N clips.
Issue 4: MiniMax H3 — RAM collapse / system freeze on --disable-dynamic-vram (resolved)
Symptom
With the legacy patcher (--disable-dynamic-vram), H3 runs drove RAM to 0 and the whole system became unresponsive (kernel swap thrash) mid-generation.
Run history (attempt log)
| Prompt | Flags | Outcome |
|---|---|---|
17c3959e |
dynamic ON (--reserve-vram 2.0 online unused) + --cache-ram 25.0 |
Slow (>1min/step), canceled — no freeze |
32c6ab68 |
--disable-dynamic-vram --reserve-vram 2.0 --cache-ram 25.0 |
Freeze: RAM 30.6/32GB used, 232MB free, swap 11GB used (pswpin 134350 / pswpout 2954515) |
6a40c75b |
--disable-dynamic-vram --reserve-vram 2.0 --cache-ram 7 7 |
Freeze again: ramAvail 19→15→5.7→4.2→1.4→0.2GB, swap 1.26→2.9GB, API dead at t≈438s |
ce4fa882 |
dynamic ON + --vram-headroom 2.0 --cache-ram 7 7 |
✅ Complete 26m06s, RAM stable |
Root cause
Two independent things, both on the legacy path:
- Model weights are non-evictable RAM.
--disable-dynamic-vramuses the legacyModelPatcher+ LOWVRAM. It holds the LoRA-merged DiT (~20GB) and the CLIP encoder (~15GB) in RAM as real tensors at the same time → ~35GB demand on 32GB → swap → freeze. --cache-ramfloors fix none of it. Both--cache-ramvalues are free-RAM floors for node cache entries only (execution.py:746,800-812,comfy_execution/caching.py:550-566). They never touch model weights. A single value25.0sets the second floor (inactive/pinned cache) to effectively unbounded (main.py:331-349), so the pinned cache grows until RAM is exhausted — but even--cache-ram 7 7cannot prevent the weights-driven collapse. This is why both legacy runs froze.
Fix
Use DynamicVRAM (default). cache-ram 7 7 keeps ≥7GB free for the OS; DynamicVRAM's RAM eviction keeps the workspace livable. --disable-dynamic-vram is now only used for models that fully fit VRAM (see Klein 9B, Issue 1).
Guardrail
When running long H3 gens, watch a RAM safety tripwire: if MemAvailable drops below ~2GB, POST /interrupt; if it stays there >25s, SIGTERM the main.py process so the machine never hard-freezes. (/tmp/opencode/memwatch.py samples RAM/VRAM/swap; /tmp/opencode/watchdog_watcher.py auto-interrupts.)
Key Code Locations
| Concern | File | Lines |
|---|---|---|
| DynamicVRAM per-prompt load() | comfy/model_patcher.py |
2141-2158 (partially_load), 1853-2035 (load) |
| Per-module VBAR setup loop | comfy/model_patcher.py |
1911-2010 |
_load_list() module iteration |
comfy/model_patcher.py |
945-980 |
dirty flag check |
comfy/model_patcher.py |
2144 |
_v_signature preservation |
comfy/model_patcher.py |
1914-1916 (set_dirty) |
| VBAR fault + pin + DMA | comfy/ops.py |
126-264 (cast_modules_with_vbar) |
| Conv3D workaround | comfy/ops.py |
101-110, 607-620 |
| DynamicVRAM init | main.py |
251-282 |
| Legacy vs Dynamic patcher swap | comfy/model_patcher.py |
2178 |
| Pin eviction between prompts | comfy/model_management.py |
647-736 |
| MAX_PINNED_MEMORY calc | comfy/model_management.py |
1581-1588 |
| load_models_gpu orchestration | comfy/model_management.py |
909-1009 |
| free_memory / model eviction | comfy/model_management.py |
863-907 |