Files
Claude 3adebb731f Rename FunPack to FunUI (5.0.3 Classy Chopper): /fun endpoint, wink logo, load-time migration of old names
Old project/workflow names and browser settings migrate on load; /funpack redirects to /fun.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016acRss69eH1235eRdFTZHY
2026-10-25 13:11:03 +00:00

62 lines
2.9 KiB
Python

"""Block swap: the frozen weights of most transformer blocks wait in system memory and come to the card one layer at a time.
Training a LoRA never changes the base weights, so they can live in (pinned) system memory. ComfyUI's operations already
copy a weight to the card for the moment a layer runs and drop that copy afterwards; they only do it for a layer that is
told to (`comfy_cast_weights`), so this moves the weights and tells every layer in those blocks. Costs speed (each swapped
weight crosses the bus twice per step: forward, then again when checkpointing recomputes the block), gains about a
block's weight in free card memory per swapped block.
The idea, and the number of blocks as the one knob, follow musubi-tuner's block swap for MiniMax H3 training
(kohya-ss, Apache License 2.0). This is an independent implementation on ComfyUI's own weight casting, not its code.
"""
from __future__ import annotations
def _host(tensor, pin: bool):
out = tensor.detach().to("cpu")
if pin:
try:
return out.pin_memory()
except RuntimeError: # the host will not lock more memory: this weight stays pageable
return out
return out
def move(blocks, count: int, torch) -> dict:
"""Put the weights of the last `count` blocks in system memory. -> {"blocks", "bytes"} actually moved.
A block already moved is left alone, so asking twice is harmless. Trainable weights stay where they are.
"""
chosen = list(blocks)[max(0, len(blocks) - count):] if count > 0 else []
moved, size, pin = 0, 0, True
for block in chosen:
if getattr(block, "_funui_swapped", False):
continue
for module in block.modules():
if hasattr(module, "comfy_cast_weights"):
module.comfy_cast_weights = True # this layer now fetches its weight for the moment it runs
for name, param in list(module.named_parameters(recurse=False)):
if param.requires_grad or param.device.type == "cpu":
continue
size += param.numel() * param.element_size()
param.data = _host(param.data, pin)
pin = pin and param.data.is_pinned()
for name, buf in list(module.named_buffers(recurse=False)):
if buf.device.type != "cpu":
size += buf.numel() * buf.element_size()
module._buffers[name] = _host(buf, pin)
block._funui_swapped = True
moved += 1
if moved and torch.cuda.is_available():
torch.cuda.empty_cache()
return {"blocks": moved, "bytes": size}
def still_away(blocks, count: int) -> bool:
"""Whether the first weight of the last swapped block is still in system memory (ComfyUI may move a model back)."""
for block in list(blocks)[max(0, len(blocks) - count):][-1:]:
for param in block.parameters():
return param.device.type == "cpu"
return False