Borrowed from VoxCPM (HF download stack, settings, routes support) and VibeVoice (registry, patcher, device/dtype/ attention). Deletes chatterbox_handler.py.
319 lines
10 KiB
Python
319 lines
10 KiB
Python
"""Device utilities for Chatterbox TTS nodes.
|
|
|
|
Unified device handling on top of ComfyUI's ``comfy.model_management``, so
|
|
device detection matches what the rest of ComfyUI is doing on this machine.
|
|
|
|
``comfy.model_management`` is imported **lazily**, inside
|
|
:func:`_get_model_management`, never at module scope: the node package has to be
|
|
importable (and unit-testable) before ComfyUI is guaranteed to be on
|
|
``sys.path``. ``dtype_utils`` imports ``_get_model_management`` from here
|
|
cross-module; the underscore is part of that contract, not a leftover.
|
|
"""
|
|
|
|
import logging
|
|
from typing import List, Optional
|
|
|
|
import torch
|
|
|
|
__all__ = [
|
|
"DEVICE_AUTO",
|
|
"DEVICE_CPU",
|
|
"DEVICE_CUDA",
|
|
"DEVICE_MPS",
|
|
"DEVICE_XPU",
|
|
"DEVICE_NPU",
|
|
"DEVICE_DEFAULT",
|
|
"get_available_devices",
|
|
"get_device_options",
|
|
"get_device_option_labels",
|
|
"get_device_display_name",
|
|
"get_device_type",
|
|
"get_torch_device",
|
|
"get_offload_device",
|
|
"get_device_memory_info",
|
|
"is_gpu_device",
|
|
"should_use_fp16",
|
|
"should_use_bf16",
|
|
"supports_fp8_compute",
|
|
"get_autocast_device",
|
|
]
|
|
|
|
|
|
# Lazy import to avoid a circular dependency at package-import time.
|
|
_model_management = None
|
|
|
|
|
|
def _get_model_management():
|
|
"""Return ``comfy.model_management``, importing it on first use."""
|
|
global _model_management
|
|
if _model_management is None:
|
|
import comfy.model_management as mm
|
|
|
|
_model_management = mm
|
|
return _model_management
|
|
|
|
|
|
# Device type constants. The first block are legacy aliases this project and
|
|
# VibeVoice accept as *input*; they are never produced by ``get_device_options``
|
|
# unless ComfyUI itself produced them.
|
|
DEVICE_AUTO = "auto"
|
|
DEVICE_DEFAULT = "default"
|
|
DEVICE_CPU = "cpu"
|
|
DEVICE_CUDA = "cuda"
|
|
DEVICE_MPS = "mps"
|
|
DEVICE_XPU = "xpu"
|
|
DEVICE_NPU = "npu"
|
|
DEVICE_DIRECTML = "directml"
|
|
DEVICE_HIP = "hip"
|
|
|
|
|
|
def get_available_devices() -> List[str]:
|
|
"""Device option strings ComfyUI itself offers, in its own order.
|
|
|
|
Delegates to ``mm.get_gpu_device_options()`` (``["default", "cpu"]``, plus
|
|
``"gpu:N"`` on a multi-GPU machine) so the widget offers exactly the labels
|
|
ComfyUI's own value checks know about.
|
|
|
|
Returns:
|
|
List of device option strings, without the ``"auto"`` alias.
|
|
"""
|
|
mm = _get_model_management()
|
|
getter = getattr(mm, "get_gpu_device_options", None)
|
|
if callable(getter):
|
|
return [str(option) for option in getter()]
|
|
|
|
# Older core without the helper: fall back to a capability probe.
|
|
devices: List[str] = [DEVICE_DEFAULT]
|
|
if mm.is_nvidia() or mm.is_amd() or mm.mps_mode():
|
|
devices.append(DEVICE_CUDA)
|
|
devices.append(DEVICE_CPU)
|
|
return devices
|
|
|
|
|
|
def get_device_options() -> List[str]:
|
|
"""Device values for the node dropdown.
|
|
|
|
``"auto"`` is a real, supported value: :func:`get_torch_device` maps it to
|
|
ComfyUI's default compute device. It has to appear in the combo options,
|
|
otherwise ComfyUI's "Value not in list" check rejects a workflow saved with
|
|
``device="auto"`` before ``execute()`` ever runs.
|
|
|
|
It is appended **last**, deliberately. ComfyUI binds a saved workflow's
|
|
widget values **by index**, so inserting an option at the front silently
|
|
re-binds every existing graph's device field to whatever now sits at that
|
|
index. Appending is the only ordering that keeps old indices valid.
|
|
|
|
Returns:
|
|
List of device option strings for the UI dropdown.
|
|
"""
|
|
return [*get_available_devices(), DEVICE_AUTO]
|
|
|
|
|
|
def get_device_option_labels() -> dict:
|
|
"""Mapping of device option values to human-readable labels."""
|
|
return {device: get_device_display_name(device) for device in get_device_options()}
|
|
|
|
|
|
def get_device_display_name(device_type: str) -> str:
|
|
"""Human-readable label for a device option value.
|
|
|
|
Args:
|
|
device_type: A value from :func:`get_device_options`, or one of the
|
|
legacy aliases this module still accepts as input.
|
|
|
|
Returns:
|
|
Human-readable device name.
|
|
"""
|
|
if device_type == DEVICE_AUTO:
|
|
return "Auto (ComfyUI default)"
|
|
if device_type == DEVICE_DEFAULT:
|
|
return "Default"
|
|
if device_type.startswith("gpu:"):
|
|
index = device_type.split(":", 1)[1]
|
|
try:
|
|
return f"GPU {index} ({torch.cuda.get_device_name(int(index))})"
|
|
except Exception:
|
|
return f"GPU {index}"
|
|
|
|
mm = _get_model_management()
|
|
|
|
if device_type == DEVICE_CUDA:
|
|
if mm.is_nvidia():
|
|
try:
|
|
return f"CUDA ({torch.cuda.get_device_name(0)})"
|
|
except Exception:
|
|
return "CUDA (NVIDIA GPU)"
|
|
if mm.is_amd():
|
|
return "CUDA (AMD ROCm/HIP)"
|
|
return "CUDA"
|
|
if device_type == DEVICE_MPS:
|
|
return "MPS (Apple Silicon)"
|
|
if device_type == DEVICE_XPU:
|
|
return "XPU (Intel Arc)"
|
|
if device_type == DEVICE_NPU:
|
|
return "NPU (Huawei Ascend)"
|
|
if device_type == DEVICE_DIRECTML:
|
|
return "DirectML (Windows)"
|
|
if device_type == DEVICE_CPU:
|
|
return "CPU"
|
|
|
|
return str(device_type).upper()
|
|
|
|
|
|
def get_torch_device(device_type: Optional[str] = None) -> torch.device:
|
|
"""Resolve a device option value to a ``torch.device``.
|
|
|
|
Honours MPS / XPU / NPU requests when the backend is actually available on
|
|
this machine; otherwise it logs a warning and falls back to ComfyUI's
|
|
default compute device.
|
|
|
|
Resolution order:
|
|
|
|
1. ``None`` / ``"auto"`` / ``"default"`` / any ``"cuda*"`` -> ComfyUI's
|
|
default compute device.
|
|
2. ComfyUI's own option grammar (``"cpu"``, ``"gpu:N"``) via
|
|
``mm.resolve_gpu_device_option``.
|
|
3. Explicit accelerator names (``"mps"`` / ``"xpu"`` / ``"npu"``), each
|
|
guarded by an availability probe.
|
|
4. Anything unrecognised falls back to the default device, loudly.
|
|
|
|
Args:
|
|
device_type: A device option value. ``None`` means "auto".
|
|
|
|
Returns:
|
|
A ``torch.device``.
|
|
"""
|
|
mm = _get_model_management()
|
|
|
|
if device_type is None or device_type in (DEVICE_AUTO, DEVICE_DEFAULT):
|
|
return mm.get_torch_device()
|
|
|
|
# CUDA and any "cuda:<index>" map to ComfyUI's default compute device.
|
|
if isinstance(device_type, str) and device_type.startswith(DEVICE_CUDA):
|
|
return mm.get_torch_device()
|
|
|
|
resolver = getattr(mm, "resolve_gpu_device_option", None)
|
|
if callable(resolver):
|
|
resolved = resolver(device_type)
|
|
if resolved is not None:
|
|
return resolved
|
|
elif device_type == DEVICE_CPU:
|
|
return torch.device(DEVICE_CPU)
|
|
|
|
if device_type == DEVICE_CPU:
|
|
return torch.device(DEVICE_CPU)
|
|
|
|
if device_type == DEVICE_MPS:
|
|
if getattr(torch.backends, "mps", None) is not None and torch.backends.mps.is_available():
|
|
return torch.device(DEVICE_MPS)
|
|
logging.warning(
|
|
"[Chatterbox TTS] Device 'mps' requested but not available; using default device."
|
|
)
|
|
return mm.get_torch_device()
|
|
|
|
if device_type == DEVICE_XPU:
|
|
if hasattr(torch, "xpu") and torch.xpu.is_available():
|
|
return torch.device(DEVICE_XPU)
|
|
logging.warning(
|
|
"[Chatterbox TTS] Device 'xpu' requested but not available; using default device."
|
|
)
|
|
return mm.get_torch_device()
|
|
|
|
if device_type == DEVICE_NPU:
|
|
if hasattr(torch, "npu") and torch.npu.is_available():
|
|
return torch.device(DEVICE_NPU)
|
|
logging.warning(
|
|
"[Chatterbox TTS] Device 'npu' requested but not available; using default device."
|
|
)
|
|
return mm.get_torch_device()
|
|
|
|
logging.warning(
|
|
"[Chatterbox TTS] Device '%s' not available; using default device.", device_type
|
|
)
|
|
return mm.get_torch_device()
|
|
|
|
|
|
def get_device_type(device: torch.device) -> str:
|
|
"""Convert a ``torch.device`` to this project's device-type string."""
|
|
if device.type in (DEVICE_CPU, DEVICE_MPS, DEVICE_XPU, DEVICE_NPU, DEVICE_CUDA):
|
|
return device.type
|
|
return device.type
|
|
|
|
|
|
def get_offload_device() -> torch.device:
|
|
"""The offload device (typically CPU), via ComfyUI's ``intermediate_device``."""
|
|
return _get_model_management().intermediate_device()
|
|
|
|
|
|
def should_use_fp16(device: Optional[torch.device] = None) -> bool:
|
|
"""Whether FP16 is appropriate for ``device`` (ComfyUI's own answer)."""
|
|
mm = _get_model_management()
|
|
if device is None:
|
|
device = mm.get_torch_device()
|
|
return mm.should_use_fp16(device)
|
|
|
|
|
|
def should_use_bf16(device: Optional[torch.device] = None) -> bool:
|
|
"""Whether BF16 is appropriate for ``device`` (ComfyUI's own answer)."""
|
|
mm = _get_model_management()
|
|
if device is None:
|
|
device = mm.get_torch_device()
|
|
return mm.should_use_bf16(device)
|
|
|
|
|
|
def supports_fp8_compute(device: Optional[torch.device] = None) -> bool:
|
|
"""Whether FP8 compute is supported on ``device``."""
|
|
mm = _get_model_management()
|
|
if device is None:
|
|
device = mm.get_torch_device()
|
|
return mm.supports_fp8_compute(device)
|
|
|
|
|
|
def get_autocast_device(device: torch.device) -> str:
|
|
"""The device-type string ``torch.autocast`` expects."""
|
|
return _get_model_management().get_autocast_device(device)
|
|
|
|
|
|
def is_gpu_device(device_type: str) -> bool:
|
|
"""Whether a device-type string names an accelerator rather than the CPU.
|
|
|
|
Args:
|
|
device_type: Device type string.
|
|
|
|
Returns:
|
|
``True`` for a GPU-class device type.
|
|
"""
|
|
gpu_types = {
|
|
DEVICE_CUDA,
|
|
DEVICE_MPS,
|
|
DEVICE_XPU,
|
|
DEVICE_NPU,
|
|
DEVICE_DIRECTML,
|
|
DEVICE_HIP,
|
|
}
|
|
if not isinstance(device_type, str):
|
|
return False
|
|
if device_type.startswith("gpu:") or device_type == DEVICE_DEFAULT:
|
|
return True
|
|
return device_type.lower() in gpu_types
|
|
|
|
|
|
def get_device_memory_info(device_type: Optional[str] = None) -> dict:
|
|
"""Total / used / free memory in bytes for ``device_type``.
|
|
|
|
Returns an empty dict for anything that is not a CUDA device -- an honest
|
|
"unknown" rather than a fabricated zero.
|
|
"""
|
|
device = get_torch_device(device_type)
|
|
|
|
if device.type == DEVICE_CUDA:
|
|
try:
|
|
total = torch.cuda.get_device_properties(device).total_memory
|
|
reserved = torch.cuda.memory_reserved(device)
|
|
allocated = torch.cuda.memory_allocated(device)
|
|
free = total - reserved + (reserved - allocated)
|
|
return {"total": total, "used": allocated, "free": free}
|
|
except Exception:
|
|
pass
|
|
|
|
return {} |