Files
jk 2f8173d31c Add device selection for improved QoL
- Add device parameter to all preprocessor wrapper nodes
- Allow users to specify processing device (cuda:0, cuda:1, etc.)
- Move input tensors to match selected device
- Improves MultiGPU workflow agency and eliminates device mismatches
2025-06-13 04:12:19 -04:00

245 lines
8.8 KiB
Python

import comfy.model_management as model_management
import torch
import logging
# Set up logging
logger = logging.getLogger(__name__)
def get_device_list():
"""Get list of available devices"""
devices = ["cpu"]
if torch.cuda.is_available():
devices.extend([f"cuda:{i}" for i in range(torch.cuda.device_count())])
return devices
class MultiGPUPreprocessorWrapper:
"""
Base wrapper class that temporarily overrides device placement during preprocessor model loading
and ensures input tensors are on the correct device to prevent multi-GPU device conflicts.
"""
def __init__(self, preprocessor_class):
self.preprocessor_class = preprocessor_class
@classmethod
def INPUT_TYPES(cls):
# Get base INPUT_TYPES from wrapped preprocessor
if hasattr(cls, 'preprocessor_class'):
base_inputs = cls.preprocessor_class.INPUT_TYPES()
else:
base_inputs = {"required": {}}
# Add device parameter
devices = get_device_list()
default_device = "cuda:0" if "cuda:0" in devices else (devices[1] if len(devices) > 1 else devices[0])
# Ensure optional section exists
if "optional" not in base_inputs:
base_inputs["optional"] = {}
base_inputs["optional"]["device"] = (devices, {"default": default_device})
return base_inputs
RETURN_TYPES = ("IMAGE",) # Most ControlNet preprocessors return IMAGE
FUNCTION = "execute"
CATEGORY = "preprocessors/gpu_wrapper"
def execute(self, device="cuda:1", **kwargs):
"""
Execute preprocessor with device override and input tensor movement.
"""
target_device = torch.device(device)
# Move input tensors to target device
moved_kwargs = {}
for key, value in kwargs.items():
if isinstance(value, torch.Tensor):
moved_kwargs[key] = value.to(target_device)
logger.debug(f"Moved input tensor '{key}' from {value.device} to {target_device}")
else:
moved_kwargs[key] = value
# Critical: Save original function
original_get_device = model_management.get_torch_device
try:
# Temporarily override with consistent device
model_management.get_torch_device = lambda: target_device
logger.debug(f"Temporarily overriding get_torch_device() to return: {target_device}")
# Create and execute original preprocessor using its specific function name
preprocessor = self.preprocessor_class()
function_name = getattr(self.preprocessor_class, 'FUNCTION', 'execute')
result = getattr(preprocessor, function_name)(**moved_kwargs)
logger.debug(f"Successfully executed {self.preprocessor_class.__name__} on {target_device}")
return result
except Exception as e:
logger.error(f"Error in MultiGPUPreprocessorWrapper execution: {e}")
raise
finally:
# ALWAYS restore original function, even on exception
model_management.get_torch_device = original_get_device
logger.debug("Restored original get_torch_device()")
# Import and create specific wrapper instances with error handling
# DepthAnything V2 Wrapper
try:
from comfyui_controlnet_aux import NODE_CLASS_MAPPINGS as AUX_NODE_MAPPINGS
DepthAnythingV2Preprocessor = AUX_NODE_MAPPINGS["DepthAnythingV2Preprocessor"]
class DepthAnythingV2Wrapper(MultiGPUPreprocessorWrapper):
preprocessor_class = DepthAnythingV2Preprocessor
def __init__(self):
super().__init__(DepthAnythingV2Preprocessor)
@classmethod
def INPUT_TYPES(cls):
return super().INPUT_TYPES()
RETURN_TYPES = DepthAnythingV2Preprocessor.RETURN_TYPES
CATEGORY = "preprocessors/gpu_wrapper"
logger.info("DepthAnythingV2Wrapper loaded successfully")
except (ImportError, KeyError) as e:
logger.warning(f"DepthAnythingV2Preprocessor not available: {e}")
DepthAnythingV2Wrapper = None
# DWPose Wrapper
try:
if 'AUX_NODE_MAPPINGS' not in locals():
from comfyui_controlnet_aux import NODE_CLASS_MAPPINGS as AUX_NODE_MAPPINGS
DWPreprocessor = AUX_NODE_MAPPINGS["DWPreprocessor"]
class DWPreprocessorWrapper(MultiGPUPreprocessorWrapper):
preprocessor_class = DWPreprocessor
def __init__(self):
super().__init__(DWPreprocessor)
@classmethod
def INPUT_TYPES(cls):
return super().INPUT_TYPES()
RETURN_TYPES = DWPreprocessor.RETURN_TYPES
CATEGORY = "preprocessors/gpu_wrapper"
logger.info("DWPreprocessorWrapper loaded successfully")
except (ImportError, KeyError) as e:
logger.warning(f"DWPreprocessor not available: {e}")
DWPreprocessorWrapper = None
# Canny Edge Wrapper
try:
if 'AUX_NODE_MAPPINGS' not in locals():
from comfyui_controlnet_aux import NODE_CLASS_MAPPINGS as AUX_NODE_MAPPINGS
CannyEdgePreprocessor = AUX_NODE_MAPPINGS["CannyEdgePreprocessor"]
class CannyEdgePreprocessorWrapper(MultiGPUPreprocessorWrapper):
preprocessor_class = CannyEdgePreprocessor
def __init__(self):
super().__init__(CannyEdgePreprocessor)
@classmethod
def INPUT_TYPES(cls):
return super().INPUT_TYPES()
RETURN_TYPES = CannyEdgePreprocessor.RETURN_TYPES
CATEGORY = "preprocessors/gpu_wrapper"
logger.info("CannyEdgePreprocessorWrapper loaded successfully")
except (ImportError, KeyError) as e:
logger.warning(f"CannyEdgePreprocessor not available: {e}")
CannyEdgePreprocessorWrapper = None
# OpenPose Wrapper
try:
if 'AUX_NODE_MAPPINGS' not in locals():
from comfyui_controlnet_aux import NODE_CLASS_MAPPINGS as AUX_NODE_MAPPINGS
OpenposePreprocessor = AUX_NODE_MAPPINGS["OpenposePreprocessor"]
class OpenposePreprocessorWrapper(MultiGPUPreprocessorWrapper):
preprocessor_class = OpenposePreprocessor
def __init__(self):
super().__init__(OpenposePreprocessor)
@classmethod
def INPUT_TYPES(cls):
return super().INPUT_TYPES()
RETURN_TYPES = OpenposePreprocessor.RETURN_TYPES
CATEGORY = "preprocessors/gpu_wrapper"
logger.info("OpenposePreprocessorWrapper loaded successfully")
except (ImportError, KeyError) as e:
logger.warning(f"OpenposePreprocessor not available: {e}")
OpenposePreprocessorWrapper = None
# Midas Depth Map Wrapper
try:
if 'AUX_NODE_MAPPINGS' not in locals():
from comfyui_controlnet_aux import NODE_CLASS_MAPPINGS as AUX_NODE_MAPPINGS
MidasDepthMapPreprocessor = AUX_NODE_MAPPINGS["MiDaS-DepthMapPreprocessor"]
class MidasDepthMapWrapper(MultiGPUPreprocessorWrapper):
preprocessor_class = MidasDepthMapPreprocessor
def __init__(self):
super().__init__(MidasDepthMapPreprocessor)
@classmethod
def INPUT_TYPES(cls):
return super().INPUT_TYPES()
RETURN_TYPES = MidasDepthMapPreprocessor.RETURN_TYPES
CATEGORY = "preprocessors/gpu_wrapper"
logger.info("MidasDepthMapWrapper loaded successfully")
except (ImportError, KeyError) as e:
logger.warning(f"MidasDepthMapPreprocessor not available: {e}")
MidasDepthMapWrapper = None
# Registration dictionaries
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
# Only register wrappers for available preprocessors
if DepthAnythingV2Wrapper:
NODE_CLASS_MAPPINGS["DepthAnythingV2Wrapper"] = DepthAnythingV2Wrapper
NODE_DISPLAY_NAME_MAPPINGS["DepthAnythingV2Wrapper"] = "DepthAnything V2 (GPU Wrapper)"
if DWPreprocessorWrapper:
NODE_CLASS_MAPPINGS["DWPreprocessorWrapper"] = DWPreprocessorWrapper
NODE_DISPLAY_NAME_MAPPINGS["DWPreprocessorWrapper"] = "DWPose (GPU Wrapper)"
if CannyEdgePreprocessorWrapper:
NODE_CLASS_MAPPINGS["CannyEdgePreprocessorWrapper"] = CannyEdgePreprocessorWrapper
NODE_DISPLAY_NAME_MAPPINGS["CannyEdgePreprocessorWrapper"] = "Canny Edge (GPU Wrapper)"
if OpenposePreprocessorWrapper:
NODE_CLASS_MAPPINGS["OpenposePreprocessorWrapper"] = OpenposePreprocessorWrapper
NODE_DISPLAY_NAME_MAPPINGS["OpenposePreprocessorWrapper"] = "OpenPose (GPU Wrapper)"
if MidasDepthMapWrapper:
NODE_CLASS_MAPPINGS["MidasDepthMapWrapper"] = MidasDepthMapWrapper
NODE_DISPLAY_NAME_MAPPINGS["MidasDepthMapWrapper"] = "Midas Depth (GPU Wrapper)"
logger.info(f"Registered {len(NODE_CLASS_MAPPINGS)} GPU wrapper nodes: {list(NODE_CLASS_MAPPINGS.keys())}")