- release comfy's load-time host registrations on swap blocks (unpin_all)
before installing our own pins - they are dead weight against the Windows
MAX_PINNED_MEMORY budget (RAM*0.40)
- materialize file-mapped (safetensors mmap) tensors into plain RAM before
using them as swap masters
- replace ModelPatcher.partially_unload on blockswapped patchers: free VRAM
by growing the swap set instead of comfy's backup-restore path
- extract _swap_out_block() shared by ON_LOAD and the partial-unload override
Known issue (open, todo-013): 43GB bf16 + 30-block swap still poisons the
CUDA context ('invalid argument' at the next module .to()) after pass-1
sampling in 2-pass workflows; see README. fp8 (29GB/14 blocks) unaffected.
Wan-family regression re-test pending after these changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ComfyUI-JITBlockSwap
Block Swap (RAM Offload) node for native ComfyUI MODEL — run DiT models
larger than VRAM (e.g. Wan 2.2 / Bernini-R 14B fp16, 28.6 GB on a 24 GB GPU)
by keeping the first N transformer blocks in system RAM and streaming each
block to the GPU only while its forward runs.
Node
advanced/model → Block Swap (RAM Offload) (BlockSwap)
| input | default | meaning |
|---|---|---|
model |
— | MODEL from UNETLoader (place AFTER LoRA loaders) |
blocks_to_swap |
20 | blocks kept in RAM (Wan 14B has 40). Raised automatically if the resident part still exceeds the VRAM weight budget |
pin_memory |
true | page-lock CPU copies for fast PCIe transfer |
How it works
- Hooks
ModelPatcherON_LOAD: after ComfyUI's normal load pass it re-organizes the diffusion model — swap blocks get their weight patches (LoRA) baked in once, moved to (pinned) RAM; everything else is made fully GPU-resident, removing the slow per-layer LowVramPatch cast path. - Each swapped block's
forwardis wrapped: params are repointed to a GPU copy just-in-time and repointed back to the CPU master afterwards (no device-to-host copy needed — weights never change during inference). Transfers are synchronized before compute: async submission of large pinned H2D copies interleaved with DiT kernels busy-loop hangs the GPU on Windows/WDDM (observed: 100% util at ~100 W). Cost is ~0 when compute-bound (measured 28-29 s/it either way on Wan 14B). ON_DETACHrestores state; placement is re-applied on every load, so model switching (high/low noise) and LoRA changes are safe.
Notes / limitations
- Chain:
UNETLoader → LoraLoaderModelOnly → BlockSwap → ModelSamplingSD3 → KSampler. - Requires launching ComfyUI with
--disable-dynamic-vram(legacy ModelPatcher). Without the flag the node does NOT error or stop the workflow — it passes the model through unchanged and prints[BlockSwap] dynamic VRAM patcher detected, skipping.to the console only (nothing is shown in the browser UI). The run then relies on ComfyUI's standard memory management: small jobs may still succeed, large models will OOM or fall back to slow partial loading. If block swap "doesn't seem to work", check the console for this line first. - Works on any model whose diffusion model exposes
.blocksor.transformer_blocks(Wan-family and LTX-2.3 22B AV tested; Flux double blocks are NOT covered). - Do not chain two BlockSwap nodes on the same model.
- Approx. cost: one PCIe H2D transfer per swapped block per forward call (~25-30 ms per 660 MB fp16 block on PCIe 4.0 x16).
LTX-2.3 22B (fp8) support
Tested 2026-07-10 on RTX 4090 24 GB with ltx-2.3-22b-dev-fp8.safetensors
(48 transformer_blocks): 360×360×49f I2V completed in 40.2 s with
blocks_to_swap=12 (auto-raised to 14 to fit the weight budget), 17.9 GB
resident vs a fully packed card without the node (47.4 s) — sample workflows
in workflows/.
fp8 checkpoints store weights as comfy_kitchen QuantizedTensor wrapper
subclasses, which need special handling implemented here:
- Wrapper subclasses cannot be moved with the
.datarepointing trick (their device is fixed at construction and the quantized payload lives in_qdata); swapped QuantizedTensor params are exchanged at the module attribute level instead. - A sibling ModelPatcher clone (a chain without BlockSwap) calling
unpatch_model → model.to()makesModule._applyrebuild the wrappers, which would free a still-pinned_qdataand poison the CUDA context (CUDA error: invalid argumentmuch later). The node guards the base model's.to()to unpin first.
Known issue (open)
Large bf16 checkpoints (LTX-2.3 22B bf16, 43 GB, 30/48 blocks swapped) fail
after the first sampling pass with CUDA error: invalid argument when the
next node moves its own model to the GPU (observed with LTXVLatentUpsampler
in a 2-pass workflow). Sampling itself completes at full speed; the CUDA
context is left poisoned. Ruled out so far: runtime LoRA, pin_memory (fails
with pinning disabled), comfy's own pin budget, ModelPatcher.partially_unload
(fails without it running), mmap-backed masters (fails after materializing
them to plain RAM), comfy-aimdo allocator (not active in legacy mode).
The fp8 checkpoint (29 GB, 14 blocks) and Wan 14B fp16 flows are unaffected.
Until fixed, do not combine BlockSwap with 40 GB-class bf16 checkpoints in
multi-model workflows.
Caveat — fp8 + runtime LoRA: with --disable-dynamic-vram, ComfyUI's
legacy loader merges LoRA into fp8 weights per-key on a VRAM-packed GPU and
can crawl for hours before this node's ON_LOAD hook ever runs. BlockSwap
cannot help there. Either run fp8+LoRA workflows with dynamic VRAM (default,
where this node is a no-op), or pre-merge the LoRA (bf16 merge → requantize
to fp8) and drop the LoRA loader from the chain. fp16/bf16 + LoRA are
unaffected.