Files
FearL0rd-ComfyUI-ParallelAn…/any_device_parallel.py
T
2026-02-02 10:48:52 -06:00

335 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import torch
import torch.nn as nn
import types
import copy
from concurrent.futures import ThreadPoolExecutor, as_completed
import comfy.model_management
class ParallelDevice:
"""Individual device configuration node with auto-detected hardware."""
@classmethod
def get_available_devices(cls):
"""Dynamically detect all available compute devices."""
devices = ["cpu"] # CPU always available
# NVIDIA CUDA (also covers AMD ROCm, which uses CUDA interface in PyTorch)
if torch.cuda.is_available():
count = torch.cuda.device_count()
for i in range(count):
name = torch.cuda.get_device_name(i)
# Format: "cuda:0 (NVIDIA RTX 4090)" truncated for readability
safe_name = name[:25] + "..." if len(name) > 28 else name
devices.append(f"cuda:{i}")
# Apple Silicon (Metal Performance Shaders)
if hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
devices.append("mps")
# Intel XPU (Arc GPUs, etc.)
if hasattr(torch, 'xpu') and torch.xpu.is_available():
count = torch.xpu.device_count()
for i in range(count):
devices.append(f"xpu:{i}")
# DirectML (Windows ML devices)
try:
import torch_directml
dml_count = torch_directml.device_count()
for i in range(dml_count):
devices.append(f"privateuseone:{i}") # DirectML device naming
except ImportError:
pass
return devices
@classmethod
def INPUT_TYPES(s):
available = s.get_available_devices()
# Prefer cuda:0 as default if available
default = "cuda:0" if any(d.startswith("cuda:0") for d in available) else available[0]
return {
"required": {
"device_id": (available, {
"default": default,
"tooltip": "Select available compute device (CPU/CUDA/MPS/XPU)"
}),
"percentage": ("FLOAT", {
"default": 50.0,
"min": 1.0,
"max": 100.0,
"step": 1.0,
"tooltip": "Percentage of batch to process on this device"
}),
},
"optional": {
"previous_devices": ("DEVICE_CHAIN", {
"tooltip": "Connect from another ParallelDevice node to chain multiple GPUs"
}),
}
}
RETURN_TYPES = ("DEVICE_CHAIN",)
RETURN_NAMES = ("device_chain",)
FUNCTION = "add_device"
CATEGORY = "utils/hardware"
DESCRIPTION = "Add a GPU/CPU/MPS/XPU device to the parallel processing chain"
def add_device(self, device_id, percentage, previous_devices=None):
if previous_devices is None:
previous_devices = []
# device_id comes directly from dropdown selection
config = {
"device": device_id,
"percentage": float(percentage),
"weight": float(percentage) / 100.0
}
# Append to chain
new_chain = previous_devices.copy()
new_chain.append(config)
return (new_chain,)
class ParallelDeviceList:
"""Alternative: Parallel layout with dropdowns for 1-4 devices."""
@classmethod
def get_available_devices(cls):
"""Get devices for dropdown options."""
devices = ["cpu"]
if torch.cuda.is_available():
for i in range(torch.cuda.device_count()):
devices.append(f"cuda:{i}")
if hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
devices.append("mps")
if hasattr(torch, 'xpu') and torch.xpu.is_available():
for i in range(torch.xpu.device_count()):
devices.append(f"xpu:{i}")
return devices
@classmethod
def INPUT_TYPES(s):
devices = s.get_available_devices()
def_dev = "cuda:0" if "cuda:0" in devices else devices[0]
return {
"required": {
"device_1": (devices, {"default": def_dev}),
"pct_1": ("FLOAT", {"default": 50.0, "min": 1.0, "max": 100.0, "step": 1.0}),
"device_2": (devices, {"default": devices[1] if len(devices) > 1 else def_dev}),
"pct_2": ("FLOAT", {"default": 50.0, "min": 0.0, "max": 100.0, "step": 1.0}),
},
"optional": {
"device_3": (devices, {"default": devices[2] if len(devices) > 2 else "cpu"}),
"pct_3": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 1.0}),
"device_4": (devices, {"default": devices[3] if len(devices) > 3 else "cpu"}),
"pct_4": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 1.0}),
}
}
RETURN_TYPES = ("DEVICE_CHAIN",)
RETURN_NAMES = ("device_chain",)
FUNCTION = "create_list"
CATEGORY = "utils/hardware"
def create_list(self, device_1, pct_1, device_2, pct_2, device_3="cpu", pct_3=0, device_4="cpu", pct_4=0):
chain = []
devices = [(device_1, pct_1), (device_2, pct_2), (device_3, pct_3), (device_4, pct_4)]
for dev_str, pct in devices:
if pct > 0:
chain.append({
"device": dev_str,
"percentage": float(pct),
"weight": float(pct) / 100.0
})
return (chain,)
class ParallelAnything:
"""Main execution node with TRUE parallelism via model replication."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL",),
"device_chain": ("DEVICE_CHAIN", {"tooltip": "Connect from ParallelDevice nodes"}),
},
"optional": {
"workload_split": ("BOOLEAN", {"default": True, "tooltip": "Enable multi-device processing"}),
}
}
RETURN_TYPES = ("MODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "setup_parallel"
CATEGORY = "utils/hardware"
def setup_parallel(self, model, device_chain, workload_split=True):
if model is None or not device_chain:
return (model,)
# Normalize weights
total_pct = sum(item["percentage"] for item in device_chain)
if total_pct <= 0:
return (model,)
device_names = []
weights = []
for item in device_chain:
weights.append(item["percentage"] / total_pct)
device_names.append(item["device"])
print(f"[ParallelAnything] Setup: {list(zip(device_names, [f'{w*100:.1f}%' for w in weights]))}")
lead_device = torch.device(device_names[0])
target_model = model.model.diffusion_model
if hasattr(target_model, "_true_parallel_active"):
return (model,)
# Validate devices
for dev in device_names:
try:
torch.device(dev)
except:
print(f"[ParallelAnything] Invalid device: {dev}")
return (model,)
# Create replicas (N× VRAM)
replicas = {}
try:
print(f"[ParallelAnything] Cloning to {len(device_names)} devices...")
state_dict = copy.deepcopy(target_model.state_dict())
for dev_name in device_names:
dev = torch.device(dev_name)
try:
model_class = target_model.__class__
if hasattr(target_model, 'config'):
replica = model_class(**target_model.config)
elif hasattr(target_model, 'unet_config'):
replica = model_class(**target_model.unet_config)
else:
replica = model_class()
replica.load_state_dict(state_dict, strict=False)
except Exception as e:
replica = copy.deepcopy(target_model)
replica.to(dev).eval()
for param in replica.parameters():
param.requires_grad = False
replicas[dev_name] = replica
print(f"[ParallelAnything] ✓ {dev_name}")
except RuntimeError as e:
print(f"[ParallelAnything] VRAM Error: {e}")
return (model,)
# Store references for closure
replicas_ref = replicas
devices_ref = device_names
weights_ref = weights
lead_ref = lead_device
def parallel_forward(self, x, timesteps, context=None, **kwargs):
batch_size = x.shape[0]
if batch_size < len(devices_ref) or not workload_split:
with torch.no_grad():
return replicas_ref[devices_ref[0]](x, timesteps, context=context, **kwargs)
# Calculate splits
split_sizes = [max(1, int(batch_size * w)) for w in weights_ref]
split_sizes[-1] = batch_size - sum(split_sizes[:-1])
active = []
for idx, (dev_name, size) in enumerate(zip(devices_ref, split_sizes)):
if size > 0:
active.append({
'idx': idx,
'dev_name': dev_name,
'device': torch.device(dev_name),
'replica': replicas_ref[dev_name],
'size': size
})
if len(active) == 1:
with torch.no_grad():
return active[0]['replica'](x, timesteps, context=context, **kwargs)
# Split tensors
x_chunks = torch.split(x, [a['size'] for a in active], dim=0)
t_chunks = torch.split(timesteps, [a['size'] for a in active], dim=0)
c_chunks = torch.split(context, [a['size'] for a in active], dim=0) if context is not None else [None] * len(active)
results = [None] * len(active)
def worker(task_idx):
task = active[task_idx]
dev = task['device']
replica = task['replica']
try:
# Handle device context
if dev.type == 'cuda' or dev.type == 'xpu':
with torch.cuda.device(dev) if dev.type == 'cuda' else torch.xpu.device(dev):
torch.cuda.synchronize(dev) if dev.type == 'cuda' else torch.xpu.synchronize(dev)
x_in = x_chunks[task_idx].to(dev, non_blocking=True)
t_in = t_chunks[task_idx].to(dev, non_blocking=True)
c_in = c_chunks[task_idx].to(dev, non_blocking=True) if c_chunks[task_idx] is not None else None
with torch.no_grad():
out = replica(x_in, t_in, context=c_in, **kwargs)
torch.cuda.synchronize(dev) if dev.type == 'cuda' else torch.xpu.synchronize(dev)
return task_idx, out.to(lead_ref, non_blocking=False)
else:
# CPU/MPS don't need explicit context managers
x_in = x_chunks[task_idx].to(dev)
t_in = t_chunks[task_idx].to(dev)
c_in = c_chunks[task_idx].to(dev) if c_chunks[task_idx] is not None else None
with torch.no_grad():
out = replica(x_in, t_in, context=c_in, **kwargs)
return task_idx, out.to(lead_ref)
except Exception as e:
return task_idx, e
# TRUE PARALLEL EXECUTION
with ThreadPoolExecutor(max_workers=len(active)) as executor:
futures = [executor.submit(worker, i) for i in range(len(active))]
for future in as_completed(futures):
idx, result = future.result()
if isinstance(result, Exception):
raise result
results[idx] = result
return torch.cat(results, dim=0)
target_model._original_forward = target_model.forward
target_model.forward = types.MethodType(parallel_forward, target_model)
target_model._true_parallel_active = True
target_model._parallel_replicas = replicas
model.load_device = lead_device
return (model,)
NODE_CLASS_MAPPINGS = {
"ParallelAnything": ParallelAnything,
"ParallelDevice": ParallelDevice,
"ParallelDeviceList": ParallelDeviceList
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ParallelAnything": "Parallel Anything (True Multi-GPU)",
"ParallelDevice": "Parallel Device Config",
"ParallelDeviceList": "Parallel Device List (1-4x)"
}