Model management (v2.4.0): - single-active-per-family registry releases the previous model fully (RAM, VRAM, ComfyUI current_loaded_models) before a new one loads - file-identity cache keys (basename+mtime+size+attention+q4+dtype) prevent cross-file collisions; ASR request keys share the consumer namespace so identical re-runs never spuriously evict Quant-resident runtime (v2.5.0): - GGUF weights stay raw-block resident end-to-end: uint8 parameters, per-matmul dequant kernels for Q8_0/Q4_K/Q5_K/Q6_K pinned bitwise to the gguf-py oracle; F32/F16/BF16 pass through at native dtype via zero-copy views; load-time RAM spike (~2x float size) eliminated - quantized safetensors via *.comfy_quant metadata: rotated ConvRot INT8 residents through comfy-kitchen, plain rowwise int8 / fp8 e4m3+e5m2 / int8_blockwise dequant-at-load (per-row, scalar, and per-gs-block scale layouts); unsupported formats hard-fail with actionable errors; dense gate rejects unplanned quant storages; rotated non-Linear targets (embeddings) fail with re-export guidance - dtype casts filter quant-resident storage (_quant_resident markers, fp32 weight_scale protection); SageAttention wrapper resolves activation dtype per module kind, fixing uint8-weight crash Tests: 937 passed / 5 pre-existing failures / 4 skipped
197 lines
6.3 KiB
Python
197 lines
6.3 KiB
Python
"""Dtype utilities for VibeVoice nodes.
|
|
|
|
Provides dtype resolution and casting helpers using ComfyUI's
|
|
model_management functions for consistent precision selection.
|
|
"""
|
|
|
|
import torch
|
|
import logging
|
|
from typing import List
|
|
|
|
from .device_utils import should_use_bf16, should_use_fp16, _get_model_management
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Dtype string constants
|
|
DTYPE_AUTO = "auto"
|
|
DTYPE_BF16 = "bf16"
|
|
DTYPE_FP16 = "fp16"
|
|
DTYPE_FP32 = "fp32"
|
|
|
|
# Mapping from string to torch.dtype
|
|
_DTYPE_MAP = {
|
|
DTYPE_BF16: torch.bfloat16,
|
|
DTYPE_FP16: torch.float16,
|
|
DTYPE_FP32: torch.float32,
|
|
}
|
|
|
|
# Reverse mapping
|
|
_DTYPE_STR_MAP = {
|
|
torch.bfloat16: DTYPE_BF16,
|
|
torch.float16: DTYPE_FP16,
|
|
torch.float32: DTYPE_FP32,
|
|
}
|
|
|
|
|
|
def get_dtype_options() -> List[str]:
|
|
"""Get list of available dtype options for node dropdown.
|
|
|
|
Returns:
|
|
List of dtype option strings.
|
|
"""
|
|
return [DTYPE_AUTO, DTYPE_BF16, DTYPE_FP16, DTYPE_FP32]
|
|
|
|
|
|
def resolve_dtype(dtype_str: str, device: torch.device = None) -> torch.dtype:
|
|
"""Resolve a dtype string to a torch.dtype.
|
|
|
|
Args:
|
|
dtype_str: Dtype string ("auto", "bf16", "fp16", "fp32").
|
|
device: torch.device for "auto" resolution. If None, uses default device.
|
|
|
|
Returns:
|
|
torch.dtype instance.
|
|
|
|
Raises:
|
|
ValueError: If dtype_str is not recognized.
|
|
"""
|
|
if dtype_str == DTYPE_AUTO or dtype_str is None:
|
|
mm = _get_model_management()
|
|
if device is None:
|
|
device = mm.get_torch_device()
|
|
if should_use_bf16(device):
|
|
return torch.bfloat16
|
|
elif should_use_fp16(device):
|
|
return torch.float16
|
|
else:
|
|
return torch.float32
|
|
|
|
if dtype_str in _DTYPE_MAP:
|
|
return _DTYPE_MAP[dtype_str]
|
|
|
|
raise ValueError(f"Unknown dtype: {dtype_str}. Valid options: {list(_DTYPE_MAP.keys())}")
|
|
|
|
|
|
def get_dtype_str(dtype: torch.dtype) -> str:
|
|
"""Convert a torch.dtype to its string representation.
|
|
|
|
Args:
|
|
dtype: torch.dtype instance.
|
|
|
|
Returns:
|
|
Dtype string (e.g., "bf16", "fp16", "fp32").
|
|
|
|
Raises:
|
|
ValueError: If dtype is not recognized.
|
|
"""
|
|
if dtype in _DTYPE_STR_MAP:
|
|
return _DTYPE_STR_MAP[dtype]
|
|
raise ValueError(f"Unknown torch.dtype: {dtype}. Supported: {list(_DTYPE_STR_MAP.values())}")
|
|
|
|
|
|
def _quant_protected_names(model) -> set:
|
|
"""Parameter names excluded from bulk dtype casts (plan 2026-08-24, E1).
|
|
|
|
- Every parameter of modules marked ``_quant_resident`` (GGUFLinear /
|
|
ConvRotInt8Linear): their weights are RAW BYTE / INT8 storage and the
|
|
fp32 ``weight_scale`` must stay fp32.
|
|
- Defense-in-depth: any parameter whose leaf name is ``weight_scale``.
|
|
"""
|
|
protected = set()
|
|
for name, mod in model.named_modules():
|
|
if getattr(mod, "_quant_resident", False):
|
|
prefix = f"{name}." if name else ""
|
|
protected.add(f"{prefix}weight")
|
|
protected.add(f"{prefix}weight_scale")
|
|
return protected
|
|
|
|
|
|
def representative_dtype(model):
|
|
"""First FLOATING parameter dtype (None when the tree has none).
|
|
|
|
Unlike transformers' ``dtype`` property this never reports an integer
|
|
raw-storage param (uint8 GGUF blocks), so callers comparing dtypes don't
|
|
spuriously schedule casts. Falls back to the model's ``dtype`` attribute
|
|
when the parameter tree cannot be walked (e.g. test doubles).
|
|
"""
|
|
try:
|
|
for p in model.parameters():
|
|
if p.dtype.is_floating_point:
|
|
return p.dtype
|
|
except Exception:
|
|
pass
|
|
return getattr(model, "dtype", None)
|
|
|
|
|
|
def _cast_mismatched_params(model, dtype: torch.dtype) -> None:
|
|
"""Cast only floating, unprotected, mismatched parameters, then re-tie."""
|
|
protected = _quant_protected_names(model)
|
|
for name, param in model.named_parameters():
|
|
if name in protected:
|
|
continue
|
|
if not param.dtype.is_floating_point:
|
|
continue # raw int8/uint8 quant storage, index tensors, etc.
|
|
if param.dtype != dtype:
|
|
param.data = param.data.to(dtype)
|
|
|
|
# Re-tie if the config marks weights as tied (a cast of one member of a
|
|
# tied pair replaces its storage and breaks the sharing).
|
|
config = getattr(model, "config", None)
|
|
if config is not None and hasattr(model, "tie_weights"):
|
|
decoder_config = getattr(config, "decoder_config", None)
|
|
tied = bool(getattr(decoder_config, "tie_word_embeddings", False)) or \
|
|
bool(getattr(config, "tie_word_embeddings", False))
|
|
if tied:
|
|
model.tie_weights()
|
|
|
|
|
|
def cast_model_to_dtype(model, dtype: torch.dtype) -> None:
|
|
"""Cast a model's FLOATING parameters to the specified dtype.
|
|
|
|
Quant-resident storage (raw uint8 GGUF blocks, int8 ConvRot weights,
|
|
fp32 ``weight_scale``) is NEVER touched. Torch's own ``Module.to(dtype)``
|
|
happens to skip integer tensors, but it WOULD recast fp32 scales — hence
|
|
this filtered walk instead of a bulk ``.to()``.
|
|
|
|
Args:
|
|
model: A torch.nn.Module to cast.
|
|
dtype: Target torch.dtype.
|
|
"""
|
|
if dtype is None:
|
|
return
|
|
_cast_mismatched_params(model, dtype)
|
|
logger.debug(f"Model cast to dtype (filtered): {dtype}")
|
|
|
|
|
|
def cast_model_to_dtype_if_needed(model, dtype: torch.dtype) -> None:
|
|
"""Cast a model to ``dtype`` only when some castable parameter mismatches.
|
|
|
|
Replaces the unconditional full-model ``.to(dtype)`` (RC-3). Walks the
|
|
parameters once; if every CASTABLE (floating, non-protected) parameter
|
|
already has the target dtype the function returns without touching the
|
|
model. Otherwise it casts ONLY the mismatched castable parameters in
|
|
place, then re-ties weights if the config marks them as tied.
|
|
|
|
Quant-resident parameters are excluded (see :func:`cast_model_to_dtype`).
|
|
|
|
Args:
|
|
model: A torch.nn.Module to cast.
|
|
dtype: Target torch.dtype. ``None`` is a no-op.
|
|
"""
|
|
if dtype is None:
|
|
return
|
|
|
|
protected = _quant_protected_names(model)
|
|
|
|
# Fast path: nothing castable to do.
|
|
for name, param in model.named_parameters():
|
|
if name in protected or not param.dtype.is_floating_point:
|
|
continue
|
|
if param.dtype != dtype:
|
|
break
|
|
else:
|
|
return
|
|
|
|
_cast_mismatched_params(model, dtype)
|
|
logger.debug(f"Model cast to dtype (mismatched params only): {dtype}")
|