mirror of
https://git.datalinker.icu/comfyanonymous/ComfyUI
synced 2026-09-05 07:17:04 +08:00
Add fast_sampler.py with optimized sampling and VAE decoding, enhance PreviewImage
This commit introduces `fast_sampler.py`, a new module designed to enhance the performance of sampling and VAE decoding in ComfyUI. It replaces or augments functionality previously handled in `model_management.py`, providing better VRAM management, FP16 support, and tiled decoding for low-memory scenarios. Additionally, it improves the `PreviewImage` node in `nodes.py` for faster and more efficient preview generation. These changes improve efficiency, stability, and usability, particularly for GPU-based workflows. **Key Changes:** - Implemented `fast_ksampler` for optimized sampling with improved memory management, FP16 support via `torch.amp.autocast`, and `channels_last` memory format for better GPU performance. - Added `fast_vae_decode` for efficient VAE decoding, incorporating FP16 support, `channels_last`, and selective VRAM clearing to prevent out-of-memory errors. - Introduced `fast_vae_tiled_decode` for tiled VAE decoding, enabling processing of large latents on GPUs with limited VRAM by using configurable tile sizes and overlaps. - Added profiling and debugging utilities (`profile_section`, `profile_cuda_sync`) to track execution times and VRAM usage when `--profile` or `--debug` flags are enabled. - Improved VRAM management with `clear_vram`, ensuring sufficient free memory before loading models or VAE, with configurable thresholds and minimum free memory requirements. - Implemented `is_fp16_safe` to check GPU compatibility for FP16 operations, disabling them on unsupported hardware (e.g., GTX 1660/Turing). - Optimized tensor transfers with `optimized_transfer` and `optimized_conditioning` for synchronous device placement and dtype casting. - Enhanced model preloading with `preload_model`, which unloads VAE before loading U-Net to conserve VRAM and checks for already-loaded VAE to avoid redundant transfers. - Integrated `cudnn.benchmark` for for tests, disabled by default. - VRAM should now be managed efficiently. - Updated `PreviewImage` node in `nodes.py` to support adaptive resizing of preview images to a maximum dimension of ~512 pixels while preserving aspect ratio, using `Image.LANCZOS` for quality. Increased `compress_level` from 1 to 4 for faster PNG compression, optimizing preview generation. **Impact:** - Significantly reduces VRAM usage during sampling and VAE decoding, making ComfyUI more stable on GPUs with limited memory. - Improves performance for large-scale image generation through tiled decoding and FP16 optimizations. - Enhances debugging capabilities with detailed profiling and logging, aiding development and optimization. **Dependencies:** - Relies on `nodes.py` for integration with `KSampler`, `VAEDecode`, `VAEDecodeTiled`, and `PreviewImage` nodes. - Assumes compatibility with existing `ModelPatcher` functionality for model patching (e.g., in `LoraLoader`). **Notes:** - Users should enable `--profile` or `--debug` flags to access detailed performance logs. - FP16 support requires compatible GPU hardware (compute capability ≥ 8 or > 7). - Tiled decoding parameters (`tile_size`, `overlap`, etc.) may need tuning for specific workflows. - Preview images are now smaller and faster to generate, but users can adjust `max_size` in `PreviewImage` if higher resolution previews are needed. This is a foundational change to improve ComfyUI's performance and scalability, particularly for resource-constrained environments. Thanks to Grok @ xAI for help.
This commit is contained in:
parent
08368f8e00
commit
b9f5145f4d
@ -1,3 +1,8 @@
|
|||||||
|
"""
|
||||||
|
This file is part of ComfyUI.
|
||||||
|
Copyright (C) 2024 Comfy
|
||||||
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import enum
|
import enum
|
||||||
import os
|
import os
|
||||||
@ -22,9 +27,7 @@ class EnumAction(argparse.Action):
|
|||||||
choices = tuple(e.value for e in enum_type)
|
choices = tuple(e.value for e in enum_type)
|
||||||
kwargs.setdefault("choices", choices)
|
kwargs.setdefault("choices", choices)
|
||||||
kwargs.setdefault("metavar", f"[{','.join(list(choices))}]")
|
kwargs.setdefault("metavar", f"[{','.join(list(choices))}]")
|
||||||
|
|
||||||
super(EnumAction, self).__init__(**kwargs)
|
super(EnumAction, self).__init__(**kwargs)
|
||||||
|
|
||||||
self._enum = enum_type
|
self._enum = enum_type
|
||||||
|
|
||||||
def __call__(self, parser, namespace, values, option_string=None):
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
@ -35,6 +38,8 @@ class EnumAction(argparse.Action):
|
|||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
parser.add_argument("--debug", action="store_true", help="Enable debug logging.")
|
||||||
|
parser.add_argument("--profile", action="store_true", help="Enable profiling.")
|
||||||
parser.add_argument("--listen", type=str, default="127.0.0.1", metavar="IP", nargs="?", const="0.0.0.0,::", help="Specify the IP address to listen on (default: 127.0.0.1). You can give a list of ip addresses by separating them with a comma like: 127.2.2.2,127.3.3.3 If --listen is provided without an argument, it defaults to 0.0.0.0,:: (listens on all ipv4 and ipv6)")
|
parser.add_argument("--listen", type=str, default="127.0.0.1", metavar="IP", nargs="?", const="0.0.0.0,::", help="Specify the IP address to listen on (default: 127.0.0.1). You can give a list of ip addresses by separating them with a comma like: 127.2.2.2,127.3.3.3 If --listen is provided without an argument, it defaults to 0.0.0.0,:: (listens on all ipv4 and ipv6)")
|
||||||
parser.add_argument("--port", type=int, default=8188, help="Set the listen port.")
|
parser.add_argument("--port", type=int, default=8188, help="Set the listen port.")
|
||||||
parser.add_argument("--tls-keyfile", type=str, help="Path to TLS (SSL) key file. Enables TLS, makes app accessible at https://... requires --tls-certfile to function")
|
parser.add_argument("--tls-keyfile", type=str, help="Path to TLS (SSL) key file. Enables TLS, makes app accessible at https://... requires --tls-certfile to function")
|
||||||
@ -53,34 +58,37 @@ parser.add_argument("--cuda-device", type=int, default=None, metavar="DEVICE_ID"
|
|||||||
cm_group = parser.add_mutually_exclusive_group()
|
cm_group = parser.add_mutually_exclusive_group()
|
||||||
cm_group.add_argument("--cuda-malloc", action="store_true", help="Enable cudaMallocAsync (enabled by default for torch 2.0 and up).")
|
cm_group.add_argument("--cuda-malloc", action="store_true", help="Enable cudaMallocAsync (enabled by default for torch 2.0 and up).")
|
||||||
cm_group.add_argument("--disable-cuda-malloc", action="store_true", help="Disable cudaMallocAsync.")
|
cm_group.add_argument("--disable-cuda-malloc", action="store_true", help="Disable cudaMallocAsync.")
|
||||||
|
cm_group.add_argument("--model-dtype", type=str, choices=["fp16", "bf16", "fp32"], help="Force model data type (fp16, bf16, fp32)")
|
||||||
|
|
||||||
fp_group = parser.add_mutually_exclusive_group()
|
fp_group = parser.add_mutually_exclusive_group()
|
||||||
fp_group.add_argument("--force-fp32", action="store_true", help="Force fp32 (If this makes your GPU work better please report it).")
|
fp_group.add_argument("--force-fp32", action="store_true", help="Force fp32 (If this makes your GPU work better please report it).")
|
||||||
|
fp_group.add_argument("--force-fp32-vae", action="store_true", help="Force VAE to use FP32 precision")
|
||||||
fp_group.add_argument("--force-fp16", action="store_true", help="Force fp16.")
|
fp_group.add_argument("--force-fp16", action="store_true", help="Force fp16.")
|
||||||
|
fp_group.add_argument("--force-fp16-vae", action="store_true", help="Force VAE to use FP16 precision")
|
||||||
|
|
||||||
fpunet_group = parser.add_mutually_exclusive_group()
|
fpunet_group = parser.add_mutually_exclusive_group()
|
||||||
fpunet_group.add_argument("--fp32-unet", action="store_true", help="Run the diffusion model in fp32.")
|
fpunet_group.add_argument("--fp32-unet", action="store_true", help="Run the diffusion model in fp32.")
|
||||||
fpunet_group.add_argument("--fp64-unet", action="store_true", help="Run the diffusion model in fp64.")
|
fpunet_group.add_argument("--fp64-unet", action="store_true", help="Run the diffusion model in fp64 (not recommended).")
|
||||||
fpunet_group.add_argument("--bf16-unet", action="store_true", help="Run the diffusion model in bf16.")
|
fpunet_group.add_argument("--bf16-unet", action="store_true", help="Run the diffusion model in bf16 (requires SM >= 8.0).")
|
||||||
fpunet_group.add_argument("--fp16-unet", action="store_true", help="Run the diffusion model in fp16")
|
fpunet_group.add_argument("--fp16-unet", action="store_true", help="Run the diffusion model in fp16 (may cause black images on VRAM < 6 GB).")
|
||||||
fpunet_group.add_argument("--fp8_e4m3fn-unet", action="store_true", help="Store unet weights in fp8_e4m3fn.")
|
fpunet_group.add_argument("--fp8_e4m3fn-unet", action="store_true", help="Run UNet in fp8 e4m3fn (requires SM >= 9.0 or SM 8.9 with PyTorch >= 2.3).")
|
||||||
fpunet_group.add_argument("--fp8_e5m2-unet", action="store_true", help="Store unet weights in fp8_e5m2.")
|
fpunet_group.add_argument("--fp8_e5m2-unet", action="store_true", help="Run UNet in fp8 e5m2 (requires SM >= 9.0 or SM 8.9 with PyTorch >= 2.3).")
|
||||||
fpunet_group.add_argument("--fp8_e8m0fnu-unet", action="store_true", help="Store unet weights in fp8_e8m0fnu.")
|
fpunet_group.add_argument("--fp8_e8m0fnu-unet", action="store_true", help="Run UNet in fp8 e8m0fnu (requires SM >= 9.0 or SM 8.9 with PyTorch >= 2.3).") # UPDATED: Clarified requirements
|
||||||
|
|
||||||
fpvae_group = parser.add_mutually_exclusive_group()
|
fpvae_group = parser.add_mutually_exclusive_group()
|
||||||
fpvae_group.add_argument("--fp16-vae", action="store_true", help="Run the VAE in fp16, might cause black images.")
|
fpvae_group.add_argument("--fp16-vae", action="store_true", help="Run the VAE in fp16 (risks black images on VRAM < 6 GB).")
|
||||||
fpvae_group.add_argument("--fp32-vae", action="store_true", help="Run the VAE in full precision fp32.")
|
fpvae_group.add_argument("--fp32-vae", action="store_true", help="Run the VAE in fp32 (recommended for GPUs with VRAM < 6 GB).")
|
||||||
fpvae_group.add_argument("--bf16-vae", action="store_true", help="Run the VAE in bf16.")
|
fpvae_group.add_argument("--bf16-vae", action="store_true", help="Run the VAE in bf16 (requires SM >= 8.0).")
|
||||||
|
|
||||||
parser.add_argument("--cpu-vae", action="store_true", help="Run the VAE on the CPU.")
|
parser.add_argument("--cpu-vae", action="store_true", help="Run the VAE on the CPU (slower, but safe for low VRAM).")
|
||||||
|
|
||||||
fpte_group = parser.add_mutually_exclusive_group()
|
fpte_group = parser.add_mutually_exclusive_group()
|
||||||
fpte_group.add_argument("--fp8_e4m3fn-text-enc", action="store_true", help="Store text encoder weights in fp8 (e4m3fn variant).")
|
fpte_group.add_argument("--fp8_e4m3fn-text-enc", action="store_true", help="Run text encoder in fp8 e4m3fn (requires SM >= 9.0 or SM 8.9 with PyTorch >= 2.3).")
|
||||||
fpte_group.add_argument("--fp8_e5m2-text-enc", action="store_true", help="Store text encoder weights in fp8 (e5m2 variant).")
|
fpte_group.add_argument("--fp8_e5m2-text-enc", action="store_true", help="Run text encoder in fp8 e5m2 (requires SM >= 9.0 or SM 8.9 with PyTorch >= 2.3).")
|
||||||
fpte_group.add_argument("--fp16-text-enc", action="store_true", help="Store text encoder weights in fp16.")
|
fpte_group.add_argument("--fp8_e8m0fnu-text-enc", action="store_true", help="Run text encoder in fp8 e8m0fnu (requires SM >= 9.0 or SM 8.9 with PyTorch >= 2.3).") # NEW
|
||||||
fpte_group.add_argument("--fp32-text-enc", action="store_true", help="Store text encoder weights in fp32.")
|
fpte_group.add_argument("--fp16-text-enc", action="store_true", help="Run text encoder in fp16 (may cause issues on VRAM < 6 GB).")
|
||||||
fpte_group.add_argument("--bf16-text-enc", action="store_true", help="Store text encoder weights in bf16.")
|
fpte_group.add_argument("--fp32-text-enc", action="store_true", help="Run text encoder in fp32 (recommended for GPUs with VRAM < 6 GB).")
|
||||||
|
fpte_group.add_argument("--bf16-text-enc", action="store_true", help="Run text encoder in bf16 (requires SM >= 8.0).")
|
||||||
|
|
||||||
parser.add_argument("--force-channels-last", action="store_true", help="Force channels last format when inferencing the models.")
|
parser.add_argument("--force-channels-last", action="store_true", help="Force channels last format when inferencing the models.")
|
||||||
|
|
||||||
@ -96,7 +104,6 @@ class LatentPreviewMethod(enum.Enum):
|
|||||||
TAESD = "taesd"
|
TAESD = "taesd"
|
||||||
|
|
||||||
parser.add_argument("--preview-method", type=LatentPreviewMethod, default=LatentPreviewMethod.NoPreviews, help="Default preview method for sampler nodes.", action=EnumAction)
|
parser.add_argument("--preview-method", type=LatentPreviewMethod, default=LatentPreviewMethod.NoPreviews, help="Default preview method for sampler nodes.", action=EnumAction)
|
||||||
|
|
||||||
parser.add_argument("--preview-size", type=int, default=512, help="Sets the maximum preview size for sampler nodes.")
|
parser.add_argument("--preview-size", type=int, default=512, help="Sets the maximum preview size for sampler nodes.")
|
||||||
|
|
||||||
cache_group = parser.add_mutually_exclusive_group()
|
cache_group = parser.add_mutually_exclusive_group()
|
||||||
@ -117,7 +124,6 @@ upcast = parser.add_mutually_exclusive_group()
|
|||||||
upcast.add_argument("--force-upcast-attention", action="store_true", help="Force enable attention upcasting, please report if it fixes black images.")
|
upcast.add_argument("--force-upcast-attention", action="store_true", help="Force enable attention upcasting, please report if it fixes black images.")
|
||||||
upcast.add_argument("--dont-upcast-attention", action="store_true", help="Disable all upcasting of attention. Should be unnecessary except for debugging.")
|
upcast.add_argument("--dont-upcast-attention", action="store_true", help="Disable all upcasting of attention. Should be unnecessary except for debugging.")
|
||||||
|
|
||||||
|
|
||||||
vram_group = parser.add_mutually_exclusive_group()
|
vram_group = parser.add_mutually_exclusive_group()
|
||||||
vram_group.add_argument("--gpu-only", action="store_true", help="Store and run everything (text encoders/CLIP models, etc... on the GPU).")
|
vram_group.add_argument("--gpu-only", action="store_true", help="Store and run everything (text encoders/CLIP models, etc... on the GPU).")
|
||||||
vram_group.add_argument("--highvram", action="store_true", help="By default models will be unloaded to CPU memory after being used. This option keeps them in GPU memory.")
|
vram_group.add_argument("--highvram", action="store_true", help="By default models will be unloaded to CPU memory after being used. This option keeps them in GPU memory.")
|
||||||
@ -199,7 +205,7 @@ parser.add_argument(
|
|||||||
"--comfy-api-base",
|
"--comfy-api-base",
|
||||||
type=str,
|
type=str,
|
||||||
default="https://api.comfy.org",
|
default="https://api.comfy.org",
|
||||||
help="Set the base URL for the ComfyUI API. (default: https://api.comfy.org)",
|
help="Set the base URL for the ComfyUI API (default: https://api.comfy.org).",
|
||||||
)
|
)
|
||||||
|
|
||||||
if comfy.options.args_parsing:
|
if comfy.options.args_parsing:
|
||||||
@ -215,6 +221,8 @@ if args.disable_auto_launch:
|
|||||||
|
|
||||||
if args.force_fp16:
|
if args.force_fp16:
|
||||||
args.fp16_unet = True
|
args.fp16_unet = True
|
||||||
|
args.fp16_vae = True
|
||||||
|
args.fp16_text_enc = True
|
||||||
|
|
||||||
|
|
||||||
# '--fast' is not provided, use an empty set
|
# '--fast' is not provided, use an empty set
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -34,6 +34,15 @@ import comfy.hooks
|
|||||||
import comfy.patcher_extension
|
import comfy.patcher_extension
|
||||||
from comfy.patcher_extension import CallbacksMP, WrappersMP, PatcherInjection
|
from comfy.patcher_extension import CallbacksMP, WrappersMP, PatcherInjection
|
||||||
from comfy.comfy_types import UnetWrapperFunction
|
from comfy.comfy_types import UnetWrapperFunction
|
||||||
|
from comfy.cli_args import args
|
||||||
|
from comfy.ldm.models.autoencoder import AutoencoderKL
|
||||||
|
from comfy.ldm.modules.diffusionmodules.openaimodel import UNetModel
|
||||||
|
from comfy.model_management import get_free_memory, get_torch_device
|
||||||
|
|
||||||
|
# Global flag for profiling
|
||||||
|
PROFILING_ENABLED = args.profile
|
||||||
|
DEBUG_ENABLED = args.debug
|
||||||
|
VERBOSE_ENABLED = False
|
||||||
|
|
||||||
def string_to_seed(data):
|
def string_to_seed(data):
|
||||||
crc = 0xFFFFFFFF
|
crc = 0xFFFFFFFF
|
||||||
@ -200,10 +209,13 @@ class MemoryCounter:
|
|||||||
|
|
||||||
class ModelPatcher:
|
class ModelPatcher:
|
||||||
def __init__(self, model, load_device, offload_device, size=0, weight_inplace_update=False):
|
def __init__(self, model, load_device, offload_device, size=0, weight_inplace_update=False):
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"ModelPatcher init: model={type(model).__name__}, load_device={load_device}, offload_device={offload_device}, size={size / (1024**3):.2f} GB")
|
||||||
self.size = size
|
self.size = size
|
||||||
self.model = model
|
self.model = model
|
||||||
if not hasattr(self.model, 'device'):
|
if not hasattr(self.model, 'device'):
|
||||||
logging.debug("Model doesn't have a device attribute.")
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug("Model doesn't have a device attribute.")
|
||||||
self.model.device = offload_device
|
self.model.device = offload_device
|
||||||
elif self.model.device is None:
|
elif self.model.device is None:
|
||||||
self.model.device = offload_device
|
self.model.device = offload_device
|
||||||
@ -323,8 +335,44 @@ class ModelPatcher:
|
|||||||
return n
|
return n
|
||||||
|
|
||||||
def is_clone(self, other):
|
def is_clone(self, other):
|
||||||
if hasattr(other, 'model') and self.model is other.model:
|
"""
|
||||||
|
Check if another ModelPatcher is a clone of this one.
|
||||||
|
Compares model type, patches_uuid, patches content, and base model equivalence.
|
||||||
|
"""
|
||||||
|
if not isinstance(other, ModelPatcher) or not hasattr(other, 'model'):
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug("[DEBUG_CLONES] Not clones: invalid other ModelPatcher")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self is other:
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug("[DEBUG_CLONES] Models are clones: same ModelPatcher object")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if self.model.__class__ != other.model.__class__:
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug(f"[DEBUG_CLONES] Not clones: different model types {self.model.__class__.__name__} vs {other.model.__class__.__name__}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.patches_uuid == other.patches_uuid:
|
||||||
|
if self.patches != other.patches:
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug(f"[DEBUG_CLONES] Not clones: same patches_uuid={self.patches_uuid}, but different patches")
|
||||||
|
return False
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug(f"[DEBUG_CLONES] Models are clones: same patches_uuid={self.patches_uuid} and matching patches")
|
||||||
|
return True
|
||||||
|
|
||||||
|
self_base = getattr(self.model, 'real_model', getattr(self.model, 'model', self.model))
|
||||||
|
other_base = getattr(other.model, 'real_model', getattr(other.model, 'model', other.model))
|
||||||
|
|
||||||
|
if self_base is other_base:
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug(f"[DEBUG_CLONES] Models are clones: same base model object, type={self.model.__class__.__name__}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug(f"[DEBUG_CLONES] Not clones: different base model objects, type={self.model.__class__.__name__}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def clone_has_same_weights(self, clone: 'ModelPatcher'):
|
def clone_has_same_weights(self, clone: 'ModelPatcher'):
|
||||||
@ -584,6 +632,9 @@ class ModelPatcher:
|
|||||||
return loading
|
return loading
|
||||||
|
|
||||||
def load(self, device_to=None, lowvram_model_memory=0, force_patch_weights=False, full_load=False):
|
def load(self, device_to=None, lowvram_model_memory=0, force_patch_weights=False, full_load=False):
|
||||||
|
# Set default device if device_to is None to avoid errors in get_free_memory
|
||||||
|
device = device_to if device_to is not None else comfy.model_management.get_torch_device()
|
||||||
|
|
||||||
with self.use_ejected():
|
with self.use_ejected():
|
||||||
self.unpatch_hooks()
|
self.unpatch_hooks()
|
||||||
mem_counter = 0
|
mem_counter = 0
|
||||||
@ -591,6 +642,30 @@ class ModelPatcher:
|
|||||||
lowvram_counter = 0
|
lowvram_counter = 0
|
||||||
loading = self._load_list()
|
loading = self._load_list()
|
||||||
|
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
# Determine module name for logging
|
||||||
|
module_name = "Unknown"
|
||||||
|
if isinstance(self.model, (AutoencoderKL, comfy.sd.VAE)):
|
||||||
|
module_name = "VAE"
|
||||||
|
elif isinstance(self.model, UNetModel):
|
||||||
|
module_name = "UNet"
|
||||||
|
elif hasattr(self, "is_clip") and self.is_clip:
|
||||||
|
module_name = "CLIP"
|
||||||
|
elif "diffusion_model" in str(type(self.model)):
|
||||||
|
module_name = "DiffusionModel"
|
||||||
|
elif isinstance(self.model, torch.nn.Module):
|
||||||
|
module_name = f"{type(self.model).__name__}"
|
||||||
|
logging.debug(f"Loading module: {module_name}, type: {type(self.model).__name__}")
|
||||||
|
|
||||||
|
# Validate and normalize lowvram_model_memory
|
||||||
|
if not isinstance(lowvram_model_memory, (int, float)) or lowvram_model_memory < 0:
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.warning(f"Invalid lowvram_model_memory: {lowvram_model_memory}, resetting to 0")
|
||||||
|
lowvram_model_memory = 0
|
||||||
|
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"ModelPatcher.load: model type: {type(self.model).__name__}, device_to: {device_to}, lowvram_model_memory: {lowvram_model_memory / (1024 * 1024):.2f} MB, full_load: {full_load}")
|
||||||
|
|
||||||
load_completely = []
|
load_completely = []
|
||||||
loading.sort(reverse=True)
|
loading.sort(reverse=True)
|
||||||
for x in loading:
|
for x in loading:
|
||||||
@ -604,11 +679,23 @@ class ModelPatcher:
|
|||||||
weight_key = "{}.weight".format(n)
|
weight_key = "{}.weight".format(n)
|
||||||
bias_key = "{}.bias".format(n)
|
bias_key = "{}.bias".format(n)
|
||||||
|
|
||||||
|
is_vae = isinstance(self.model, (AutoencoderKL, comfy.sd.VAE))
|
||||||
|
if VERBOSE_ENABLED:
|
||||||
|
logging.debug(f"Processing module: {n}, type: {type(m).__name__}, is_vae: {is_vae}, module_mem: {module_mem / (1024 * 1024):.2f} MB")
|
||||||
|
|
||||||
|
# Skip VAE module if already loaded on the target device
|
||||||
|
if is_vae and hasattr(self.model, 'first_stage_model') and hasattr(self.model.first_stage_model, 'device') and self.model.first_stage_model.device == device and hasattr(self.model, '_loaded_to_device') and self.model._loaded_to_device == device:
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Skipping VAE module {n}, already on {device} with _loaded_to_device={self.model._loaded_to_device}")
|
||||||
|
continue
|
||||||
|
|
||||||
if not full_load and hasattr(m, "comfy_cast_weights"):
|
if not full_load and hasattr(m, "comfy_cast_weights"):
|
||||||
if mem_counter + module_mem >= lowvram_model_memory:
|
if mem_counter + module_mem >= lowvram_model_memory:
|
||||||
lowvram_weight = True
|
lowvram_weight = True
|
||||||
lowvram_counter += 1
|
lowvram_counter += 1
|
||||||
if hasattr(m, "prev_comfy_cast_weights"): #Already lowvramed
|
if hasattr(m, "prev_comfy_cast_weights"): #Already lowvramed
|
||||||
|
if VERBOSE_ENABLED:
|
||||||
|
logging.debug(f"Skipping module {n}: already in lowvram mode")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
cast_weight = self.force_cast_weights
|
cast_weight = self.force_cast_weights
|
||||||
@ -630,6 +717,9 @@ class ModelPatcher:
|
|||||||
m.bias_function = [LowVramPatch(bias_key, self.patches)]
|
m.bias_function = [LowVramPatch(bias_key, self.patches)]
|
||||||
patch_counter += 1
|
patch_counter += 1
|
||||||
|
|
||||||
|
if VERBOSE_ENABLED:
|
||||||
|
logging.debug(f"Module {n} set to lowvram, weight_key={weight_key}, bias_key={bias_key}, patch_counter={patch_counter}, lowvram_weight={lowvram_weight}")
|
||||||
|
|
||||||
cast_weight = True
|
cast_weight = True
|
||||||
else:
|
else:
|
||||||
if hasattr(m, "comfy_cast_weights"):
|
if hasattr(m, "comfy_cast_weights"):
|
||||||
@ -638,6 +728,8 @@ class ModelPatcher:
|
|||||||
if full_load or mem_counter + module_mem < lowvram_model_memory:
|
if full_load or mem_counter + module_mem < lowvram_model_memory:
|
||||||
mem_counter += module_mem
|
mem_counter += module_mem
|
||||||
load_completely.append((module_mem, n, m, params))
|
load_completely.append((module_mem, n, m, params))
|
||||||
|
if VERBOSE_ENABLED:
|
||||||
|
logging.debug(f"Module {n} added to load_completely, mem_counter={mem_counter / (1024**3):.2f} GB")
|
||||||
|
|
||||||
if cast_weight and hasattr(m, "comfy_cast_weights"):
|
if cast_weight and hasattr(m, "comfy_cast_weights"):
|
||||||
m.prev_comfy_cast_weights = m.comfy_cast_weights
|
m.prev_comfy_cast_weights = m.comfy_cast_weights
|
||||||
@ -649,7 +741,9 @@ class ModelPatcher:
|
|||||||
if bias_key in self.weight_wrapper_patches:
|
if bias_key in self.weight_wrapper_patches:
|
||||||
m.bias_function.extend(self.weight_wrapper_patches[bias_key])
|
m.bias_function.extend(self.weight_wrapper_patches[bias_key])
|
||||||
|
|
||||||
mem_counter += move_weight_functions(m, device_to)
|
mem_counter += move_weight_functions(m, device)
|
||||||
|
if VERBOSE_ENABLED:
|
||||||
|
logging.debug(f"Moved weight functions for {n} to device={device}, mem_counter={mem_counter / (1024**3):.2f} GB")
|
||||||
|
|
||||||
load_completely.sort(reverse=True)
|
load_completely.sort(reverse=True)
|
||||||
for x in load_completely:
|
for x in load_completely:
|
||||||
@ -658,34 +752,49 @@ class ModelPatcher:
|
|||||||
params = x[3]
|
params = x[3]
|
||||||
if hasattr(m, "comfy_patched_weights"):
|
if hasattr(m, "comfy_patched_weights"):
|
||||||
if m.comfy_patched_weights == True:
|
if m.comfy_patched_weights == True:
|
||||||
|
if VERBOSE_ENABLED:
|
||||||
|
logging.debug(f"Skipping module {n}: already patched")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for param in params:
|
for param in params:
|
||||||
self.patch_weight_to_device("{}.{}".format(n, param), device_to=device_to)
|
self.patch_weight_to_device("{}.{}".format(n, param), device_to=device)
|
||||||
|
|
||||||
logging.debug("lowvram: loaded module regularly {} {}".format(n, m))
|
if VERBOSE_ENABLED:
|
||||||
|
logging.debug(f"Loaded module {n} regularly, lowvram={self.model.model_lowvram}")
|
||||||
m.comfy_patched_weights = True
|
m.comfy_patched_weights = True
|
||||||
|
|
||||||
for x in load_completely:
|
for x in load_completely:
|
||||||
x[2].to(device_to)
|
x[2].to(device)
|
||||||
|
if VERBOSE_ENABLED:
|
||||||
|
logging.debug(f"Moved module {x[1]} to device={device}")
|
||||||
|
|
||||||
|
# Safe logging with module name
|
||||||
|
lowvram_mb = lowvram_model_memory / (1024 * 1024)
|
||||||
|
mem_counter_mb = mem_counter / (1024 * 1024)
|
||||||
if lowvram_counter > 0:
|
if lowvram_counter > 0:
|
||||||
logging.info("loaded partially {} {} {}".format(lowvram_model_memory / (1024 * 1024), mem_counter / (1024 * 1024), patch_counter))
|
if PROFILING_ENABLED:
|
||||||
|
logging.info(f"Loaded partially {module_name}: {lowvram_mb:.2f} MB, {mem_counter_mb:.2f} MB, patches: {patch_counter}")
|
||||||
self.model.model_lowvram = True
|
self.model.model_lowvram = True
|
||||||
else:
|
else:
|
||||||
logging.info("loaded completely {} {} {}".format(lowvram_model_memory / (1024 * 1024), mem_counter / (1024 * 1024), full_load))
|
if PROFILING_ENABLED:
|
||||||
|
logging.info(f"Loaded completely {module_name}: {lowvram_mb:.2f} MB, {mem_counter_mb:.2f} MB, full_load: {full_load}")
|
||||||
self.model.model_lowvram = False
|
self.model.model_lowvram = False
|
||||||
if full_load:
|
if full_load:
|
||||||
self.model.to(device_to)
|
self.model.to(device)
|
||||||
mem_counter = self.model_size()
|
mem_counter = self.model_size()
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.info(f"Moved entire model to device: {device}, mem_counter: {mem_counter / (1024 * 1024):.2f} MB")
|
||||||
|
|
||||||
self.model.lowvram_patch_counter += patch_counter
|
self.model.lowvram_patch_counter += patch_counter
|
||||||
self.model.device = device_to
|
self.model.device = device
|
||||||
self.model.model_loaded_weight_memory = mem_counter
|
self.model.model_loaded_weight_memory = mem_counter
|
||||||
self.model.current_weight_patches_uuid = self.patches_uuid
|
self.model.current_weight_patches_uuid = self.patches_uuid
|
||||||
|
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Load completed: model type: {type(self.model).__name__}, device: {self.model.device}, loaded_weight_memory: {self.model.model_loaded_weight_memory / (1024 * 1024):.2f} MB, lowvram: {self.model.model_lowvram}, patch_counter: {self.model.lowvram_patch_counter}")
|
||||||
|
|
||||||
for callback in self.get_all_callbacks(CallbacksMP.ON_LOAD):
|
for callback in self.get_all_callbacks(CallbacksMP.ON_LOAD):
|
||||||
callback(self, device_to, lowvram_model_memory, force_patch_weights, full_load)
|
callback(self, device, lowvram_model_memory, force_patch_weights, full_load)
|
||||||
|
|
||||||
self.apply_hooks(self.forced_hooks, force_apply=True)
|
self.apply_hooks(self.forced_hooks, force_apply=True)
|
||||||
|
|
||||||
@ -696,6 +805,13 @@ class ModelPatcher:
|
|||||||
if k not in self.object_patches_backup:
|
if k not in self.object_patches_backup:
|
||||||
self.object_patches_backup[k] = old
|
self.object_patches_backup[k] = old
|
||||||
|
|
||||||
|
# Validate and normalize lowvram_model_memory
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"patch_model: model={type(self.model).__name__}, lowvram_model_memory={lowvram_model_memory / (1024**3):.2f} GB, device_to={device_to}")
|
||||||
|
if not isinstance(lowvram_model_memory, (int, float)) or lowvram_model_memory < 0:
|
||||||
|
logging.warning(f"Invalid lowvram_model_memory in patch_model: {lowvram_model_memory}, resetting to 0")
|
||||||
|
lowvram_model_memory = 0
|
||||||
|
|
||||||
if lowvram_model_memory == 0:
|
if lowvram_model_memory == 0:
|
||||||
full_load = True
|
full_load = True
|
||||||
else:
|
else:
|
||||||
@ -812,21 +928,37 @@ class ModelPatcher:
|
|||||||
with self.use_ejected(skip_and_inject_on_exit_only=True):
|
with self.use_ejected(skip_and_inject_on_exit_only=True):
|
||||||
unpatch_weights = self.model.current_weight_patches_uuid is not None and (self.model.current_weight_patches_uuid != self.patches_uuid or force_patch_weights)
|
unpatch_weights = self.model.current_weight_patches_uuid is not None and (self.model.current_weight_patches_uuid != self.patches_uuid or force_patch_weights)
|
||||||
# TODO: force_patch_weights should not unload + reload full model
|
# TODO: force_patch_weights should not unload + reload full model
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"partially_load: unpatch_weights={unpatch_weights}, patches_uuid={self.patches_uuid}, current_weight_patches_uuid={self.model.current_weight_patches_uuid}")
|
||||||
used = self.model.model_loaded_weight_memory
|
used = self.model.model_loaded_weight_memory
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"partially_load: used={used / (1024**3):.2f} GB, model_loaded_weight_memory={self.model.model_loaded_weight_memory / (1024**3):.2f} GB")
|
||||||
|
|
||||||
self.unpatch_model(self.offload_device, unpatch_weights=unpatch_weights)
|
self.unpatch_model(self.offload_device, unpatch_weights=unpatch_weights)
|
||||||
if unpatch_weights:
|
if unpatch_weights:
|
||||||
extra_memory += (used - self.model.model_loaded_weight_memory)
|
extra_memory += (used - self.model.model_loaded_weight_memory)
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"partially_load: updated extra_memory={extra_memory / (1024**3):.2f} GB after unpatch, used={used / (1024**3):.2f} GB, model_loaded_weight_memory={self.model.model_loaded_weight_memory / (1024**3):.2f} GB")
|
||||||
|
|
||||||
self.patch_model(load_weights=False)
|
self.patch_model(load_weights=False)
|
||||||
full_load = False
|
full_load = False
|
||||||
if self.model.model_lowvram == False and self.model.model_loaded_weight_memory > 0:
|
if self.model.model_lowvram == False and self.model.model_loaded_weight_memory > 0:
|
||||||
self.apply_hooks(self.forced_hooks, force_apply=True)
|
self.apply_hooks(self.forced_hooks, force_apply=True)
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"partially_load: early return, model_lowvram={self.model.model_lowvram}, model_loaded_weight_memory={self.model.model_loaded_weight_memory / (1024**3):.2f} GB")
|
||||||
return 0
|
return 0
|
||||||
if self.model.model_loaded_weight_memory + extra_memory > self.model_size():
|
if self.model.model_loaded_weight_memory + extra_memory > self.model_size():
|
||||||
full_load = True
|
full_load = True
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"partially_load: full_load=True, model_loaded_weight_memory={self.model.model_loaded_weight_memory / (1024**3):.2f} GB, extra_memory={extra_memory / (1024**3):.2f} GB, model_size={self.model_size() / (1024**3):.2f} GB")
|
||||||
|
|
||||||
current_used = self.model.model_loaded_weight_memory
|
current_used = self.model.model_loaded_weight_memory
|
||||||
|
lowvram_model_memory = current_used + extra_memory
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"partially_load: calling load with lowvram_model_memory={lowvram_model_memory / (1024**3):.2f} GB, current_used={current_used / (1024**3):.2f} GB, extra_memory={extra_memory / (1024**3):.2f} GB")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.load(device_to, lowvram_model_memory=current_used + extra_memory, force_patch_weights=force_patch_weights, full_load=full_load)
|
self.load(device_to, lowvram_model_memory=lowvram_model_memory, force_patch_weights=force_patch_weights, full_load=full_load)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.detach()
|
self.detach()
|
||||||
raise e
|
raise e
|
||||||
@ -834,12 +966,34 @@ class ModelPatcher:
|
|||||||
return self.model.model_loaded_weight_memory - current_used
|
return self.model.model_loaded_weight_memory - current_used
|
||||||
|
|
||||||
def detach(self, unpatch_all=True):
|
def detach(self, unpatch_all=True):
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
free_vram_before = get_free_memory(get_torch_device()) / 1024**3
|
||||||
|
logging.debug(f"detach: Before, free_vram={free_vram_before:.2f} GB, model={self.model.__class__.__name__}")
|
||||||
|
|
||||||
|
if hasattr(self.model, 'on_patched'):
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug(f"Calling on_patched for {self.model.__class__.__name__}")
|
||||||
|
self.model.on_patched()
|
||||||
self.eject_model()
|
self.eject_model()
|
||||||
self.model_patches_to(self.offload_device)
|
self.model_patches_to(self.offload_device)
|
||||||
if unpatch_all:
|
if unpatch_all:
|
||||||
self.unpatch_model(self.offload_device, unpatch_weights=unpatch_all)
|
self.unpatch_model(self.offload_device)
|
||||||
for callback in self.get_all_callbacks(CallbacksMP.ON_DETACH):
|
|
||||||
callback(self, unpatch_all)
|
if hasattr(self.model, 'to'):
|
||||||
|
self.model.to(self.offload_device)
|
||||||
|
self.model.device = self.offload_device
|
||||||
|
self.model.model_loaded_weight_memory = 0
|
||||||
|
|
||||||
|
self.patches = []
|
||||||
|
self.model_patches = 0
|
||||||
|
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
free_vram_after = get_free_memory(get_torch_device()) / 1024**3
|
||||||
|
logging.debug(f"detach: After, free_vram={free_vram_after:.2f} GB, freed={(free_vram_after-free_vram_before):.2f} GB, model={self.model.__class__.__name__}")
|
||||||
|
|
||||||
return self.model
|
return self.model
|
||||||
|
|
||||||
def current_loaded_device(self):
|
def current_loaded_device(self):
|
||||||
@ -1206,4 +1360,3 @@ class ModelPatcher:
|
|||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.detach(unpatch_all=False)
|
self.detach(unpatch_all=False)
|
||||||
|
|
||||||
|
|||||||
14
comfy/ops.py
14
comfy/ops.py
@ -18,16 +18,16 @@
|
|||||||
|
|
||||||
import torch
|
import torch
|
||||||
import logging
|
import logging
|
||||||
import comfy.model_management
|
|
||||||
from comfy.cli_args import args, PerformanceFeature
|
from comfy.cli_args import args, PerformanceFeature
|
||||||
import comfy.float
|
import comfy.float
|
||||||
import comfy.rmsnorm
|
import comfy.rmsnorm
|
||||||
import contextlib
|
import contextlib
|
||||||
|
|
||||||
cast_to = comfy.model_management.cast_to #TODO: remove once no more references
|
from comfy.utils import cast_to
|
||||||
|
cast_to = cast_to # Maintain compatibility with code expecting comfy.ops.cast_to
|
||||||
|
|
||||||
def cast_to_input(weight, input, non_blocking=False, copy=True):
|
def cast_to_input(weight, input, non_blocking=False, copy=True):
|
||||||
return comfy.model_management.cast_to(weight, input.dtype, input.device, non_blocking=non_blocking, copy=copy)
|
return cast_to(weight, input.dtype, input.device, non_blocking=non_blocking, copy=copy)
|
||||||
|
|
||||||
def cast_bias_weight(s, input=None, dtype=None, device=None, bias_dtype=None):
|
def cast_bias_weight(s, input=None, dtype=None, device=None, bias_dtype=None):
|
||||||
if input is not None:
|
if input is not None:
|
||||||
@ -48,7 +48,7 @@ def cast_bias_weight(s, input=None, dtype=None, device=None, bias_dtype=None):
|
|||||||
non_blocking = comfy.model_management.device_supports_non_blocking(device)
|
non_blocking = comfy.model_management.device_supports_non_blocking(device)
|
||||||
if s.bias is not None:
|
if s.bias is not None:
|
||||||
has_function = len(s.bias_function) > 0
|
has_function = len(s.bias_function) > 0
|
||||||
bias = comfy.model_management.cast_to(s.bias, bias_dtype, device, non_blocking=non_blocking, copy=has_function, stream=offload_stream)
|
bias = cast_to(s.bias, bias_dtype, device, non_blocking=non_blocking, copy=has_function, stream=offload_stream)
|
||||||
|
|
||||||
if has_function:
|
if has_function:
|
||||||
with wf_context:
|
with wf_context:
|
||||||
@ -56,7 +56,7 @@ def cast_bias_weight(s, input=None, dtype=None, device=None, bias_dtype=None):
|
|||||||
bias = f(bias)
|
bias = f(bias)
|
||||||
|
|
||||||
has_function = len(s.weight_function) > 0
|
has_function = len(s.weight_function) > 0
|
||||||
weight = comfy.model_management.cast_to(s.weight, dtype, device, non_blocking=non_blocking, copy=has_function, stream=offload_stream)
|
weight = cast_to(s.weight, dtype, device, non_blocking=non_blocking, copy=has_function, stream=offload_stream)
|
||||||
if has_function:
|
if has_function:
|
||||||
with wf_context:
|
with wf_context:
|
||||||
for f in s.weight_function:
|
for f in s.weight_function:
|
||||||
@ -308,10 +308,10 @@ def fp8_linear(self, input):
|
|||||||
if scale_input is None:
|
if scale_input is None:
|
||||||
scale_input = torch.ones((), device=input.device, dtype=torch.float32)
|
scale_input = torch.ones((), device=input.device, dtype=torch.float32)
|
||||||
input = torch.clamp(input, min=-448, max=448, out=input)
|
input = torch.clamp(input, min=-448, max=448, out=input)
|
||||||
input = input.reshape(-1, input_shape[2]).to(dtype).contiguous()
|
input = input.reshape(-1, input_shape[2]).to(dtype)
|
||||||
else:
|
else:
|
||||||
scale_input = scale_input.to(input.device)
|
scale_input = scale_input.to(input.device)
|
||||||
input = (input * (1.0 / scale_input).to(input_dtype)).reshape(-1, input_shape[2]).to(dtype).contiguous()
|
input = (input * (1.0 / scale_input).to(input_dtype)).reshape(-1, input_shape[2]).to(dtype)
|
||||||
|
|
||||||
if bias is not None:
|
if bias is not None:
|
||||||
o = torch._scaled_mm(input, w, out_dtype=input_dtype, bias=bias, scale_a=scale_input, scale_b=scale_weight)
|
o = torch._scaled_mm(input, w, out_dtype=input_dtype, bias=bias, scale_a=scale_input, scale_b=scale_weight)
|
||||||
|
|||||||
@ -1078,3 +1078,23 @@ def upscale_dit_mask(mask: torch.Tensor, img_size_in, img_size_out):
|
|||||||
dim=1
|
dim=1
|
||||||
)
|
)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def cast_to(weight, dtype=None, device=None, non_blocking=False, copy=False, stream=None):
|
||||||
|
"""Cast tensor to specified dtype and device."""
|
||||||
|
if device is None or weight.device == device:
|
||||||
|
if not copy:
|
||||||
|
if dtype is None or weight.dtype == dtype:
|
||||||
|
return weight
|
||||||
|
if stream is not None:
|
||||||
|
with stream:
|
||||||
|
return weight.to(dtype=dtype, copy=copy)
|
||||||
|
return weight.to(dtype=dtype, copy=copy)
|
||||||
|
|
||||||
|
if stream is not None:
|
||||||
|
with stream:
|
||||||
|
r = torch.empty_like(weight, dtype=dtype, device=device)
|
||||||
|
r.copy_(weight, non_blocking=non_blocking)
|
||||||
|
else:
|
||||||
|
r = torch.empty_like(weight, dtype=dtype, device=device)
|
||||||
|
r.copy_(weight, non_blocking=non_blocking)
|
||||||
|
return r
|
||||||
477
fast_sampler.py
Normal file
477
fast_sampler.py
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
import torch
|
||||||
|
import comfy
|
||||||
|
import gc
|
||||||
|
import time
|
||||||
|
from torch.amp import autocast
|
||||||
|
from comfy.cli_args import args
|
||||||
|
from comfy.model_management import get_torch_device, vae_dtype, soft_empty_cache, free_memory, force_channels_last, estimate_vae_decode_memory, device_supports_non_blocking
|
||||||
|
from contextlib import contextmanager
|
||||||
|
import latent_preview
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Global flag for profiling
|
||||||
|
PROFILING_ENABLED = args.profile
|
||||||
|
DEBUG_ENABLED = args.debug
|
||||||
|
CUDNN_BENCHMARK_ENABLED = getattr(args, 'cudnn_benchmark', False) # Default: False
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG if PROFILING_ENABLED or DEBUG_ENABLED else logging.INFO)
|
||||||
|
|
||||||
|
# Cache for FP16 safety check
|
||||||
|
_fp16_safe_cache = {}
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def profile_section(name):
|
||||||
|
"""Context manager for profiling execution time."""
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
logging.debug(f"{name}: {time.time() - start:.3f} s")
|
||||||
|
else:
|
||||||
|
yield
|
||||||
|
|
||||||
|
def profile_cuda_sync(is_gpu, message="CUDA sync"):
|
||||||
|
"""Profile CUDA synchronization time if GPU is used."""
|
||||||
|
if PROFILING_ENABLED and is_gpu:
|
||||||
|
logging.debug(f"{message} started")
|
||||||
|
sync_start = time.time()
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
logging.debug(f"{message} took {time.time() - sync_start:.3f} s")
|
||||||
|
|
||||||
|
def is_fp16_safe(device):
|
||||||
|
"""Check if FP16 is safe for the GPU (disabled for GTX 1660/Turing)."""
|
||||||
|
if device.type != 'cuda':
|
||||||
|
return False
|
||||||
|
if device in _fp16_safe_cache:
|
||||||
|
return _fp16_safe_cache[device]
|
||||||
|
try:
|
||||||
|
props = torch.cuda.get_device_properties(device)
|
||||||
|
is_safe = props.major >= 8 or props.compute_capability[0] > 7
|
||||||
|
_fp16_safe_cache[device] = is_safe
|
||||||
|
return is_safe
|
||||||
|
except Exception:
|
||||||
|
_fp16_safe_cache[device] = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
def initialize_device_and_dtype(model, device=None):
|
||||||
|
"""Initialize device and dtype from model."""
|
||||||
|
if device is None:
|
||||||
|
device = get_torch_device()
|
||||||
|
dtype = getattr(model, 'dtype', torch.float32)
|
||||||
|
is_gpu = device.type == 'cuda' and torch.cuda.is_available()
|
||||||
|
return device, dtype, is_gpu
|
||||||
|
|
||||||
|
def clear_vram(device, threshold=0.5, min_free=1.5):
|
||||||
|
"""Clear VRAM if usage exceeds threshold or free memory is below min_free (in GB)."""
|
||||||
|
if device.type == 'cuda':
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
start_time = time.time()
|
||||||
|
mem_allocated = torch.cuda.memory_allocated(device) / 1024**3
|
||||||
|
mem_total = torch.cuda.get_device_properties(device).total_memory / 1024**3
|
||||||
|
critical_threshold = 0.05 * mem_total + 0.1 # 5% VRAM + 100 MB
|
||||||
|
if mem_allocated > threshold * mem_total or (mem_total - mem_allocated) < max(min_free, critical_threshold):
|
||||||
|
logging.debug(f"Clearing VRAM: allocated {mem_allocated:.2f} GB, free {mem_total - mem_allocated:.2f} GB, threshold {critical_threshold:.2f} GB")
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
#soft_empty_cache(clear=False)
|
||||||
|
mem_after = torch.cuda.memory_allocated(device) / 1024**3
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"VRAM cleared: {mem_allocated:.2f} GB -> {mem_after:.2f} GB, took {time.time() - start_time:.3f} s")
|
||||||
|
else:
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"VRAM not cleared: {mem_allocated:.2f} GB / {mem_total:.2f} GB, sufficient free memory")
|
||||||
|
return mem_allocated, mem_total
|
||||||
|
|
||||||
|
def preload_model(model, device, is_vae=False):
|
||||||
|
"""Preload model or VAE to device, avoiding unnecessary unloading."""
|
||||||
|
with profile_section("Model preload"):
|
||||||
|
if is_vae:
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
start_time = time.time()
|
||||||
|
logging.debug(f"Checking VAE device for {model.__class__.__name__}")
|
||||||
|
|
||||||
|
# Check if VAE is already loaded
|
||||||
|
if (hasattr(model, 'first_stage_model') and
|
||||||
|
hasattr(model.first_stage_model, 'device') and
|
||||||
|
model.first_stage_model.device == device and
|
||||||
|
hasattr(model, '_loaded_to_device') and
|
||||||
|
model._loaded_to_device == device):
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"VAE already loaded on {device}, skipping transfer, check took {time.time() - start_time:.3f} s")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load VAE
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Loading VAE to {device}")
|
||||||
|
transfer_start = time.time()
|
||||||
|
model.first_stage_model.to(device)
|
||||||
|
model._loaded_to_device = device
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"VAE transferred to {device}, took {time.time() - transfer_start:.3f} s")
|
||||||
|
logging.debug(f"VAE first_stage_model device: {model.first_stage_model.device}")
|
||||||
|
logging.debug(f"VAE has decode_tiled: {hasattr(model, 'decode_tiled')}")
|
||||||
|
else:
|
||||||
|
# Check if model is already loaded
|
||||||
|
if hasattr(model, '_loaded_to_device') and model._loaded_to_device == device:
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Model already loaded on {device}, skipping preload")
|
||||||
|
return
|
||||||
|
# Load U-Net
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Loading U-Net {model.__class__.__name__} to {device}")
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
comfy.model_management.load_model_gpu(model)
|
||||||
|
model._loaded_to_device = device
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
free_mem = (torch.cuda.get_device_properties(device).total_memory - torch.cuda.memory_allocated(device)) / 1024**3
|
||||||
|
logging.debug(f"U-Net loaded to {device}, VRAM free: {free_mem:.2f} GB")
|
||||||
|
|
||||||
|
def optimized_transfer(tensor, device, dtype):
|
||||||
|
"""Synchronous tensor transfer to device."""
|
||||||
|
pin_memory = comfy.model_management.is_device_cuda(device)
|
||||||
|
if isinstance(tensor, torch.Tensor) and tensor.device != device:
|
||||||
|
tensor = tensor.to(device=device, dtype=dtype, pin_memory=pin_memory)
|
||||||
|
return tensor
|
||||||
|
|
||||||
|
def optimized_conditioning(conditioning, device, dtype):
|
||||||
|
"""Efficiently transfer conditioning tensors."""
|
||||||
|
return [
|
||||||
|
optimized_transfer(p, device, dtype) if isinstance(p, torch.Tensor) else p
|
||||||
|
for p in conditioning
|
||||||
|
]
|
||||||
|
|
||||||
|
def finalize_images(images, device):
|
||||||
|
"""Process and finalize output images."""
|
||||||
|
if len(images.shape) == 5: # Combine batches
|
||||||
|
images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
|
||||||
|
return images.to(device=device, memory_format=torch.channels_last)
|
||||||
|
|
||||||
|
def fast_sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
|
||||||
|
denoise, disable_noise, start_step, last_step, force_full_denoise, noise_mask, callback, seed, device, dtype, is_gpu):
|
||||||
|
"""Optimized sampling function."""
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
start_time = time.time()
|
||||||
|
logging.debug(f"Starting sampling")
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
use_amp = is_gpu and dtype == torch.float16 and is_fp16_safe(device)
|
||||||
|
with autocast(device_type='cuda', enabled=use_amp):
|
||||||
|
samples = comfy.sample.sample(
|
||||||
|
model, noise, steps, cfg, sampler_name, scheduler,
|
||||||
|
positive, negative, latent_image,
|
||||||
|
denoise=denoise, disable_noise=disable_noise,
|
||||||
|
start_step=start_step, last_step=last_step,
|
||||||
|
force_full_denoise=force_full_denoise,
|
||||||
|
noise_mask=noise_mask, callback=callback, seed=seed
|
||||||
|
)
|
||||||
|
samples = samples.to(device=device, dtype=dtype, memory_format=torch.channels_last)
|
||||||
|
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Sampling completed, took {time.time() - start_time:.3f} s")
|
||||||
|
|
||||||
|
return samples
|
||||||
|
|
||||||
|
def fast_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent,
|
||||||
|
denoise=1.0, disable_noise=False, start_step=None, last_step=None,
|
||||||
|
force_full_denoise=False, device=None, dtype=None, is_gpu=None):
|
||||||
|
"""
|
||||||
|
Fast KSampler implementation with optimized memory management and optional cuDNN benchmark.
|
||||||
|
"""
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
if model is None:
|
||||||
|
logging.warning("fast_ksampler: model is None")
|
||||||
|
|
||||||
|
if device is None or dtype is None or is_gpu is None:
|
||||||
|
device, dtype, is_gpu = initialize_device_and_dtype(model.model)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Enable cuDNN benchmarking if requested
|
||||||
|
if is_gpu and comfy.model_management.is_device_cuda(device) and CUDNN_BENCHMARK_ENABLED:
|
||||||
|
torch.backends.cudnn.enabled = True
|
||||||
|
torch.backends.cudnn.benchmark = True
|
||||||
|
|
||||||
|
# Check and move model parameters once
|
||||||
|
if is_gpu:
|
||||||
|
if not hasattr(model, '_device_checked') or not model._device_checked:
|
||||||
|
for param in model.model.parameters():
|
||||||
|
if param.device.type != device.type:
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
logging.warning(f"U-Net parameter {param.shape} on {param.device.type}, moving to {device}")
|
||||||
|
model.model.to(device)
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"VRAM after moving U-Net: {torch.cuda.memory_allocated(device)/1024**3:.2f} GB")
|
||||||
|
model._device = device
|
||||||
|
model._device_checked = True
|
||||||
|
break
|
||||||
|
if hasattr(model, 'control_model'):
|
||||||
|
for param in model.control_model.parameters():
|
||||||
|
if param.device.type != device.type:
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
logging.warning(f"ControlNet parameter {param.shape} on {param.device.type}, moving to {device}")
|
||||||
|
model.control_model.to(device)
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"VRAM after moving ControlNet: {torch.cuda.memory_allocated(device)/1024**3:.2f} GB")
|
||||||
|
model._control_device = device
|
||||||
|
model._device_checked = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Preload model
|
||||||
|
preload_model(model, device)
|
||||||
|
|
||||||
|
# Transfer latents
|
||||||
|
with profile_section("Latent transfer"):
|
||||||
|
latent_image = latent["samples"]
|
||||||
|
latent_image = optimized_transfer(latent_image, device, dtype)
|
||||||
|
latent_image = comfy.sample.fix_empty_latent_channels(model, latent_image)
|
||||||
|
|
||||||
|
# Transfer conditioning
|
||||||
|
with profile_section("Conditioning transfer"):
|
||||||
|
positive = optimized_conditioning(positive, device, dtype)
|
||||||
|
negative = optimized_conditioning(negative, device, dtype)
|
||||||
|
|
||||||
|
# Prepare noise
|
||||||
|
if disable_noise:
|
||||||
|
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu")
|
||||||
|
else:
|
||||||
|
batch_inds = latent["batch_index"] if "batch_index" in latent else None
|
||||||
|
noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
|
||||||
|
|
||||||
|
# Handle noise mask if present
|
||||||
|
noise_mask = latent.get("noise_mask")
|
||||||
|
if noise_mask is not None:
|
||||||
|
noise_mask = optimized_transfer(noise_mask, device, dtype)
|
||||||
|
|
||||||
|
# Allocate output tensor
|
||||||
|
samples = torch.empty_like(latent_image, device=device, dtype=dtype)
|
||||||
|
|
||||||
|
# Perform sampling
|
||||||
|
with torch.no_grad():
|
||||||
|
callback = None if not comfy.utils.PROGRESS_BAR_ENABLED else latent_preview.prepare_callback(model, steps)
|
||||||
|
samples = fast_sample(
|
||||||
|
model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
|
||||||
|
denoise, disable_noise, start_step, last_step, force_full_denoise, noise_mask, callback, seed,
|
||||||
|
device, dtype, is_gpu
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log VRAM state after sampling
|
||||||
|
if is_gpu and PROFILING_ENABLED:
|
||||||
|
mem_total = torch.cuda.get_device_properties(device).total_memory / 1024**3
|
||||||
|
mem_allocated = torch.cuda.memory_allocated(device) / 1024**3
|
||||||
|
logging.debug(f"VRAM after sampling: {mem_allocated:.2f} GB / {mem_total:.2f} GB")
|
||||||
|
|
||||||
|
# Log completion of sampling
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Sampling completed, preparing for VAE")
|
||||||
|
profile_cuda_sync(is_gpu)
|
||||||
|
|
||||||
|
# Clear VRAM after sampling
|
||||||
|
if is_gpu:
|
||||||
|
if not PROFILING_ENABLED:
|
||||||
|
clear_vram(device, threshold=0.5, min_free=1.5)
|
||||||
|
else:
|
||||||
|
clear_start = time.time()
|
||||||
|
mem_allocated, mem_total = clear_vram(device, threshold=0.5, min_free=1.5)
|
||||||
|
logging.debug(f"VRAM after sampling: {mem_allocated:.2f} GB / {mem_total:.2f} GB, clear took {time.time() - clear_start:.3f} s")
|
||||||
|
logging.debug(f"Post-VRAM checkpoint: {time.time()}")
|
||||||
|
|
||||||
|
out = latent.copy()
|
||||||
|
out["samples"] = samples
|
||||||
|
return (out,)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
finally_start = time.time()
|
||||||
|
if is_gpu and CUDNN_BENCHMARK_ENABLED:
|
||||||
|
torch.backends.cudnn.benchmark = False
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Final cleanup took {time.time() - finally_start:.3f} s")
|
||||||
|
|
||||||
|
def fast_vae_decode(vae, samples):
|
||||||
|
"""
|
||||||
|
Fast VAE decoding with FP16, channels_last, universal VRAM management, and full logging.
|
||||||
|
"""
|
||||||
|
device = get_torch_device()
|
||||||
|
vae_dtype_val = vae_dtype(device=device)
|
||||||
|
is_gpu = device.type == 'cuda' and torch.cuda.is_available()
|
||||||
|
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug(f"VAE dtype: {vae_dtype_val}")
|
||||||
|
logging.debug(f"Pre-VAE checkpoint: {time.time()}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Disable cuDNN benchmark for VAE stability if enabled
|
||||||
|
if is_gpu and comfy.model_management.is_device_cuda(device) and CUDNN_BENCHMARK_ENABLED:
|
||||||
|
torch.backends.cudnn.benchmark = False
|
||||||
|
|
||||||
|
# Prepare VRAM for VAE
|
||||||
|
if is_gpu:
|
||||||
|
mem_total = torch.cuda.get_device_properties(device).total_memory / 1024**3
|
||||||
|
latent_size = samples["samples"].shape
|
||||||
|
model_for_memory = getattr(vae, 'first_stage_model', vae)
|
||||||
|
vae_memory_required = estimate_vae_decode_memory(model_for_memory, latent_size, vae_dtype_val) / 1024**3
|
||||||
|
vram_threshold = 1.0 if mem_total < 5.9 else 1.1
|
||||||
|
vae_memory_required *= vram_threshold
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Estimated VAE memory: {vae_memory_required:.2f} GB")
|
||||||
|
mem_allocated = torch.cuda.memory_allocated(device) / 1024**3
|
||||||
|
free_mem = mem_total - mem_allocated
|
||||||
|
if free_mem < vae_memory_required:
|
||||||
|
#free_memory(vae_memory_required)
|
||||||
|
mem_allocated, mem_total = clear_vram(device, threshold=0.4, min_free=2.0)
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"VRAM after free_memory: {mem_allocated:.2f} GB / {mem_total:.2f} GB")
|
||||||
|
|
||||||
|
# Preload VAE to device
|
||||||
|
preload_model(vae, device, is_vae=True)
|
||||||
|
|
||||||
|
# Transfer latents with channels_last
|
||||||
|
with profile_section("VAE latent transfer"):
|
||||||
|
non_blocking = is_gpu and device_supports_non_blocking(device)
|
||||||
|
latent_samples = samples["samples"].to(device, dtype=vae_dtype_val, non_blocking=non_blocking)
|
||||||
|
if is_gpu and force_channels_last():
|
||||||
|
latent_samples = latent_samples.to(memory_format=torch.channels_last)
|
||||||
|
vae.first_stage_model.to(memory_format=torch.channels_last)
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Latent samples device: {latent_samples.device}, dtype: {latent_samples.dtype}")
|
||||||
|
|
||||||
|
# Decode latents
|
||||||
|
with torch.no_grad():
|
||||||
|
use_amp = is_gpu and is_fp16_safe(device)
|
||||||
|
with autocast(device_type='cuda', enabled=use_amp, dtype=torch.float16 if use_amp else torch.float32):
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Decoding VAE, use_amp={use_amp}")
|
||||||
|
decode_start = time.time()
|
||||||
|
images = vae.decode(latent_samples).clamp(0, 1)
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"VAE decode took {time.time() - decode_start:.3f} s")
|
||||||
|
images = finalize_images(images, device)
|
||||||
|
|
||||||
|
return (images,)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.error(f"VAE decode failed: {e}\n{traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
finally_start = time.time()
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Final cleanup took {time.time() - finally_start:.3f} s")
|
||||||
|
|
||||||
|
def fast_vae_tiled_decode(vae, samples, tile_size=512, overlap=64, temporal_size=64, temporal_overlap=8):
|
||||||
|
"""Fast VAE decoding with tiling for low VRAM, consistent with fast_vae_decode."""
|
||||||
|
device, dtype, is_gpu = initialize_device_and_dtype(vae)
|
||||||
|
vae_dtype = vae_dtype(device=device)
|
||||||
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug(f"VAE dtype: {vae_dtype}")
|
||||||
|
logging.debug(f"Pre-VAE checkpoint: {time.time()}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Disable cuDNN benchmark for tiled decoding stability if enabled
|
||||||
|
if is_gpu and comfy.model_management.is_device_cuda(device) and CUDNN_BENCHMARK_ENABLED:
|
||||||
|
torch.backends.cudnn.benchmark = False # Ensure stability for variable tile sizes
|
||||||
|
|
||||||
|
# Clear VRAM before VAE
|
||||||
|
if is_gpu:
|
||||||
|
mem_total = torch.cuda.get_device_properties(device).total_memory / 1024**3
|
||||||
|
mem_allocated = torch.cuda.memory_allocated(device) / 1024**3
|
||||||
|
free_mem = mem_total - mem_allocated
|
||||||
|
# Estimate memory for tiled decoding (conservative, ~50% of full decode)
|
||||||
|
vae_memory_required = (vae.memory_used_decode(samples["samples"].shape, vae_dtype) / 1024**3 * 0.5
|
||||||
|
if hasattr(vae, 'memory_used_decode') else 0.75)
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"VRAM before tiled VAE: {mem_allocated:.2f} GB / {mem_total:.2f} GB")
|
||||||
|
logging.debug(f"Estimated tiled VAE memory: {vae_memory_required:.2f} GB")
|
||||||
|
|
||||||
|
# Skip VRAM cleanup if VAE is already loaded and memory is sufficient
|
||||||
|
if (hasattr(vae, '_loaded_to_device') and vae._loaded_to_device == device and
|
||||||
|
free_mem >= vae_memory_required * 1.1):
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"VAE already loaded, sufficient memory: {free_mem:.2f} GB")
|
||||||
|
elif mem_allocated > 0.4 * mem_total or free_mem < vae_memory_required:
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Clearing VRAM: {mem_allocated:.2f} GB used of {mem_total:.2f} GB")
|
||||||
|
mem_allocated, mem_total = clear_vram(device, threshold=0.4, min_free=0.75)
|
||||||
|
|
||||||
|
# Preload VAE
|
||||||
|
if not PROFILING_ENABLED:
|
||||||
|
preload_model(vae, device, is_vae=True)
|
||||||
|
else:
|
||||||
|
preload_start = time.time()
|
||||||
|
preload_model(vae, device, is_vae=True)
|
||||||
|
logging.debug(f"VAE preload took {time.time() - preload_start:.3f} s")
|
||||||
|
logging.debug(f"Post-preload checkpoint: {time.time()}")
|
||||||
|
|
||||||
|
# Transfer latents
|
||||||
|
with profile_section("VAE latent transfer"):
|
||||||
|
latent_samples = samples["samples"]
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Latent samples device: {latent_samples.device}, dtype: {latent_samples.dtype}")
|
||||||
|
latent_samples = optimized_transfer(latent_samples, device, vae_dtype)
|
||||||
|
if is_gpu and force_channels_last():
|
||||||
|
latent_samples = latent_samples.to(memory_format=torch.channels_last)
|
||||||
|
vae.first_stage_model.to(memory_format=torch.channels_last)
|
||||||
|
|
||||||
|
# Log before decoding
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Starting tiled VAE decoding")
|
||||||
|
logging.debug(f"Pre-decode checkpoint: {time.time()}")
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
use_amp = is_gpu and is_fp16_safe(device)
|
||||||
|
with autocast(device_type='cuda', enabled=use_amp, dtype=torch.float16 if use_amp else torch.float32):
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Tiled VAE decoding with tile_size={tile_size}, overlap={overlap}, "
|
||||||
|
f"temporal_size={temporal_size}, temporal_overlap={temporal_overlap}, use_amp={use_amp}, dtype={'torch.float16' if use_amp else 'torch.float32'}")
|
||||||
|
|
||||||
|
# Adjust tile parameters
|
||||||
|
if tile_size < overlap * 4:
|
||||||
|
overlap = tile_size // 4
|
||||||
|
if temporal_size < temporal_overlap * 2:
|
||||||
|
temporal_overlap = temporal_overlap // 2
|
||||||
|
|
||||||
|
temporal_compression = getattr(vae, 'temporal_compression_decode', lambda: None)()
|
||||||
|
spacial_compression = getattr(vae, 'spacial_compression_decode', lambda: 8)()
|
||||||
|
|
||||||
|
if temporal_compression is not None:
|
||||||
|
temporal_size = max(2, temporal_size // temporal_compression)
|
||||||
|
temporal_overlap = max(1, min(temporal_size // 2, temporal_overlap // temporal_compression))
|
||||||
|
else:
|
||||||
|
temporal_size = None
|
||||||
|
temporal_overlap = None
|
||||||
|
|
||||||
|
# Perform tiled decoding
|
||||||
|
decode_start = time.time()
|
||||||
|
images = vae.decode_tiled(
|
||||||
|
latent_samples,
|
||||||
|
tile_x=tile_size // spacial_compression,
|
||||||
|
tile_y=tile_size // spacial_compression,
|
||||||
|
overlap=overlap // spacial_compression,
|
||||||
|
tile_t=temporal_size,
|
||||||
|
overlap_t=temporal_overlap
|
||||||
|
)
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"VAE tiled decode took {time.time() - decode_start:.3f} s")
|
||||||
|
|
||||||
|
images = finalize_images(images, device)
|
||||||
|
|
||||||
|
if is_gpu and PROFILING_ENABLED:
|
||||||
|
mem_allocated = torch.cuda.memory_allocated(device) / 1024**3
|
||||||
|
mem_total = torch.cuda.get_device_properties(device).total_memory / 1024**3
|
||||||
|
logging.debug(f"VRAM after tiled decoding: {mem_allocated:.2f} GB / {mem_total:.2f} GB")
|
||||||
|
logging.debug(f"Post-decode checkpoint: {time.time()}")
|
||||||
|
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"VAE tiled decode finished, returning images: {time.time()}")
|
||||||
|
return (images,)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"VAE tiled decode failed: {e}\n{traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
finally_start = time.time()
|
||||||
|
if PROFILING_ENABLED:
|
||||||
|
logging.debug(f"Final cleanup took {time.time() - finally_start:.3f} s")
|
||||||
|
logging.debug(f"Post-final cleanup checkpoint: {time.time()}")
|
||||||
@ -9,6 +9,8 @@ from collections.abc import Collection
|
|||||||
|
|
||||||
from comfy.cli_args import args
|
from comfy.cli_args import args
|
||||||
|
|
||||||
|
DEBUG_ENABLED = args.debug
|
||||||
|
|
||||||
supported_pt_extensions: set[str] = {'.ckpt', '.pt', '.pt2', '.bin', '.pth', '.safetensors', '.pkl', '.sft'}
|
supported_pt_extensions: set[str] = {'.ckpt', '.pt', '.pt2', '.bin', '.pth', '.safetensors', '.pkl', '.sft'}
|
||||||
|
|
||||||
folder_names_and_paths: dict[str, tuple[list[str], set[str]]] = {}
|
folder_names_and_paths: dict[str, tuple[list[str], set[str]]] = {}
|
||||||
@ -245,7 +247,8 @@ def recursive_search(directory: str, excluded_dir_names: list[str] | None=None)
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logging.warning(f"Warning: Unable to access {directory}. Skipping this path.")
|
logging.warning(f"Warning: Unable to access {directory}. Skipping this path.")
|
||||||
|
|
||||||
logging.debug("recursive file list on directory {}".format(directory))
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug("recursive file list on directory {}".format(directory))
|
||||||
dirpath: str
|
dirpath: str
|
||||||
subdirs: list[str]
|
subdirs: list[str]
|
||||||
filenames: list[str]
|
filenames: list[str]
|
||||||
@ -267,7 +270,8 @@ def recursive_search(directory: str, excluded_dir_names: list[str] | None=None)
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logging.warning(f"Warning: Unable to access {path}. Skipping this path.")
|
logging.warning(f"Warning: Unable to access {path}. Skipping this path.")
|
||||||
continue
|
continue
|
||||||
logging.debug("found {} files".format(len(result)))
|
if DEBUG_ENABLED:
|
||||||
|
logging.debug("found {} files".format(len(result)))
|
||||||
return result, dirs
|
return result, dirs
|
||||||
|
|
||||||
def filter_files_extensions(files: Collection[str], extensions: Collection[str]) -> list[str]:
|
def filter_files_extensions(files: Collection[str], extensions: Collection[str]) -> list[str]:
|
||||||
|
|||||||
4
main.py
4
main.py
@ -8,9 +8,13 @@ import time
|
|||||||
from comfy.cli_args import args
|
from comfy.cli_args import args
|
||||||
from app.logger import setup_logger
|
from app.logger import setup_logger
|
||||||
import itertools
|
import itertools
|
||||||
|
import comfy.model_management
|
||||||
import utils.extra_config
|
import utils.extra_config
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
import atexit
|
||||||
|
|
||||||
|
atexit.register(comfy.model_management.soft_empty_cache, clear=True)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
#NOTE: These do not do anything on core ComfyUI, they are for custom nodes.
|
#NOTE: These do not do anything on core ComfyUI, they are for custom nodes.
|
||||||
|
|||||||
188
nodes.py
188
nodes.py
@ -27,6 +27,10 @@ import comfy.utils
|
|||||||
import comfy.controlnet
|
import comfy.controlnet
|
||||||
from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict, FileLocator
|
from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict, FileLocator
|
||||||
|
|
||||||
|
from fast_sampler import fast_vae_decode
|
||||||
|
from fast_sampler import fast_ksampler
|
||||||
|
from fast_sampler import fast_vae_tiled_decode
|
||||||
|
|
||||||
import comfy.clip_vision
|
import comfy.clip_vision
|
||||||
|
|
||||||
import comfy.model_management
|
import comfy.model_management
|
||||||
@ -282,49 +286,34 @@ class VAEDecode:
|
|||||||
RETURN_TYPES = ("IMAGE",)
|
RETURN_TYPES = ("IMAGE",)
|
||||||
OUTPUT_TOOLTIPS = ("The decoded image.",)
|
OUTPUT_TOOLTIPS = ("The decoded image.",)
|
||||||
FUNCTION = "decode"
|
FUNCTION = "decode"
|
||||||
|
|
||||||
CATEGORY = "latent"
|
CATEGORY = "latent"
|
||||||
DESCRIPTION = "Decodes latent images back into pixel space images."
|
DESCRIPTION = "Decodes latent images back into pixel space images."
|
||||||
|
|
||||||
def decode(self, vae, samples):
|
def decode(self, vae, samples):
|
||||||
images = vae.decode(samples["samples"])
|
return fast_vae_decode(vae, samples)
|
||||||
if len(images.shape) == 5: #Combine batches
|
|
||||||
images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
|
|
||||||
return (images, )
|
|
||||||
|
|
||||||
class VAEDecodeTiled:
|
class VAEDecodeTiled:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {"required": {"samples": ("LATENT", ), "vae": ("VAE", ),
|
return {
|
||||||
"tile_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 32}),
|
"required": {
|
||||||
"overlap": ("INT", {"default": 64, "min": 0, "max": 4096, "step": 32}),
|
"samples": ("LATENT", {"tooltip": "The latent to be decoded."}),
|
||||||
"temporal_size": ("INT", {"default": 64, "min": 8, "max": 4096, "step": 4, "tooltip": "Only used for video VAEs: Amount of frames to decode at a time."}),
|
"vae": ("VAE", {"tooltip": "The VAE model used for decoding the latent."}),
|
||||||
"temporal_overlap": ("INT", {"default": 8, "min": 4, "max": 4096, "step": 4, "tooltip": "Only used for video VAEs: Amount of frames to overlap."}),
|
"tile_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 32, "tooltip": "Tile size for tiled decoding."}),
|
||||||
}}
|
"overlap": ("INT", {"default": 64, "min": 0, "max": 4096, "step": 32, "tooltip": "Tile overlap for tiled decoding."}),
|
||||||
|
"temporal_size": ("INT", {"default": 64, "min": 8, "max": 4096, "step": 4, "tooltip": "Only used for video VAEs: Amount of frames to decode at a time."}),
|
||||||
|
"temporal_overlap": ("INT", {"default": 8, "min": 4, "max": 4096, "step": 4, "tooltip": "Only used for video VAEs: Amount of frames to overlap."}),
|
||||||
|
}
|
||||||
|
}
|
||||||
RETURN_TYPES = ("IMAGE",)
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
OUTPUT_TOOLTIPS = ("The decoded image.",)
|
||||||
FUNCTION = "decode"
|
FUNCTION = "decode"
|
||||||
|
|
||||||
CATEGORY = "_for_testing"
|
CATEGORY = "_for_testing"
|
||||||
|
DESCRIPTION = "Decodes latent images back into pixel space images using tiled decoding for VRAM efficiency."
|
||||||
|
|
||||||
def decode(self, vae, samples, tile_size, overlap=64, temporal_size=64, temporal_overlap=8):
|
def decode(self, vae, samples, tile_size=512, overlap=64, temporal_size=64, temporal_overlap=8):
|
||||||
if tile_size < overlap * 4:
|
return fast_vae_tiled_decode(vae, samples, tile_size=tile_size, overlap=overlap,
|
||||||
overlap = tile_size // 4
|
temporal_size=temporal_size, temporal_overlap=temporal_overlap)
|
||||||
if temporal_size < temporal_overlap * 2:
|
|
||||||
temporal_overlap = temporal_overlap // 2
|
|
||||||
temporal_compression = vae.temporal_compression_decode()
|
|
||||||
if temporal_compression is not None:
|
|
||||||
temporal_size = max(2, temporal_size // temporal_compression)
|
|
||||||
temporal_overlap = max(1, min(temporal_size // 2, temporal_overlap // temporal_compression))
|
|
||||||
else:
|
|
||||||
temporal_size = None
|
|
||||||
temporal_overlap = None
|
|
||||||
|
|
||||||
compression = vae.spacial_compression_decode()
|
|
||||||
images = vae.decode_tiled(samples["samples"], tile_x=tile_size // compression, tile_y=tile_size // compression, overlap=overlap // compression, tile_t=temporal_size, overlap_t=temporal_overlap)
|
|
||||||
if len(images.shape) == 5: #Combine batches
|
|
||||||
images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
|
|
||||||
return (images, )
|
|
||||||
|
|
||||||
class VAEEncode:
|
class VAEEncode:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
@ -1473,28 +1462,26 @@ class SetLatentNoiseMask:
|
|||||||
s["noise_mask"] = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1]))
|
s["noise_mask"] = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1]))
|
||||||
return (s,)
|
return (s,)
|
||||||
|
|
||||||
def common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0, disable_noise=False, start_step=None, last_step=None, force_full_denoise=False):
|
def common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent,
|
||||||
|
denoise=1.0, disable_noise=False, start_step=None, last_step=None,
|
||||||
|
force_full_denoise=False):
|
||||||
|
# Get device and dtype
|
||||||
|
device = comfy.model_management.get_torch_device()
|
||||||
|
dtype = getattr(model.model, 'dtype', torch.float32)
|
||||||
|
is_gpu = device.type == 'cuda' and torch.cuda.is_available()
|
||||||
|
|
||||||
|
# Prepare latent image
|
||||||
latent_image = latent["samples"]
|
latent_image = latent["samples"]
|
||||||
latent_image = comfy.sample.fix_empty_latent_channels(model, latent_image)
|
latent_image = comfy.sample.fix_empty_latent_channels(model, latent_image)
|
||||||
|
|
||||||
if disable_noise:
|
# Call fast_ksampler with device, dtype, and is_gpu
|
||||||
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu")
|
out = fast_ksampler(
|
||||||
else:
|
model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent,
|
||||||
batch_inds = latent["batch_index"] if "batch_index" in latent else None
|
denoise=denoise, disable_noise=disable_noise, start_step=start_step,
|
||||||
noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
|
last_step=last_step, force_full_denoise=force_full_denoise,
|
||||||
|
device=device, dtype=dtype, is_gpu=is_gpu
|
||||||
noise_mask = None
|
)
|
||||||
if "noise_mask" in latent:
|
return out
|
||||||
noise_mask = latent["noise_mask"]
|
|
||||||
|
|
||||||
callback = latent_preview.prepare_callback(model, steps)
|
|
||||||
disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED
|
|
||||||
samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
|
|
||||||
denoise=denoise, disable_noise=disable_noise, start_step=start_step, last_step=last_step,
|
|
||||||
force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=seed)
|
|
||||||
out = latent.copy()
|
|
||||||
out["samples"] = samples
|
|
||||||
return (out, )
|
|
||||||
|
|
||||||
class KSampler:
|
class KSampler:
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -1502,27 +1489,64 @@ class KSampler:
|
|||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"model": ("MODEL", {"tooltip": "The model used for denoising the input latent."}),
|
"model": ("MODEL", {"tooltip": "The model used for denoising the input latent."}),
|
||||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True, "tooltip": "The random seed used for creating the noise."}),
|
"seed": ("INT", {
|
||||||
"steps": ("INT", {"default": 20, "min": 1, "max": 10000, "tooltip": "The number of steps used in the denoising process."}),
|
"default": 0,
|
||||||
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01, "tooltip": "The Classifier-Free Guidance scale balances creativity and adherence to the prompt. Higher values result in images more closely matching the prompt however too high values will negatively impact quality."}),
|
"min": 0,
|
||||||
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"tooltip": "The algorithm used when sampling, this can affect the quality, speed, and style of the generated output."}),
|
"max": 0xffffffffffffffff,
|
||||||
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"tooltip": "The scheduler controls how noise is gradually removed to form the image."}),
|
"control_after_generate": True,
|
||||||
"positive": ("CONDITIONING", {"tooltip": "The conditioning describing the attributes you want to include in the image."}),
|
"tooltip": "The random seed used for creating the noise."
|
||||||
"negative": ("CONDITIONING", {"tooltip": "The conditioning describing the attributes you want to exclude from the image."}),
|
}),
|
||||||
|
"steps": ("INT", {
|
||||||
|
"default": 20,
|
||||||
|
"min": 1,
|
||||||
|
"max": 10000,
|
||||||
|
"tooltip": "The number of steps used in the denoising process."
|
||||||
|
}),
|
||||||
|
"cfg": ("FLOAT", {
|
||||||
|
"default": 8.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 100.0,
|
||||||
|
"step": 0.1,
|
||||||
|
"round": 0.01,
|
||||||
|
"tooltip": "The Classifier-Free Guidance scale balances creativity and adherence to the prompt."
|
||||||
|
}),
|
||||||
|
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, {
|
||||||
|
"tooltip": "The algorithm used when sampling."
|
||||||
|
}),
|
||||||
|
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {
|
||||||
|
"tooltip": "The scheduler controls how noise is gradually removed to form the image."
|
||||||
|
}),
|
||||||
|
"positive": ("CONDITIONING", {
|
||||||
|
"tooltip": "The conditioning describing the attributes to include."
|
||||||
|
}),
|
||||||
|
"negative": ("CONDITIONING", {
|
||||||
|
"tooltip": "The conditioning describing the attributes to exclude."
|
||||||
|
}),
|
||||||
"latent_image": ("LATENT", {"tooltip": "The latent image to denoise."}),
|
"latent_image": ("LATENT", {"tooltip": "The latent image to denoise."}),
|
||||||
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "The amount of denoising applied, lower values will maintain the structure of the initial image allowing for image to image sampling."}),
|
"denoise": ("FLOAT", {
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 1.0,
|
||||||
|
"step": 0.01,
|
||||||
|
"tooltip": "The amount of denoising applied."
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RETURN_TYPES = ("LATENT",)
|
RETURN_TYPES = ("LATENT",)
|
||||||
OUTPUT_TOOLTIPS = ("The denoised latent.",)
|
OUTPUT_TOOLTIPS = ("The denoised latent.",)
|
||||||
FUNCTION = "sample"
|
FUNCTION = "sample"
|
||||||
|
|
||||||
CATEGORY = "sampling"
|
CATEGORY = "sampling"
|
||||||
DESCRIPTION = "Uses the provided model, positive and negative conditioning to denoise the latent image."
|
DESCRIPTION = "Denoises the latent image using the provided model and conditioning."
|
||||||
|
|
||||||
def sample(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=1.0):
|
def sample(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
|
||||||
return common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise)
|
denoise=1.0):
|
||||||
|
latent = latent_image.copy()
|
||||||
|
if "samples" in latent:
|
||||||
|
latent["samples"] = latent["samples"].to(
|
||||||
|
comfy.model_management.get_torch_device(), non_blocking=True)
|
||||||
|
return common_ksampler(
|
||||||
|
model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=denoise)
|
||||||
|
|
||||||
class KSamplerAdvanced:
|
class KSamplerAdvanced:
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -1618,14 +1642,44 @@ class PreviewImage(SaveImage):
|
|||||||
self.output_dir = folder_paths.get_temp_directory()
|
self.output_dir = folder_paths.get_temp_directory()
|
||||||
self.type = "temp"
|
self.type = "temp"
|
||||||
self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
|
self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
|
||||||
self.compress_level = 1
|
self.compress_level = 4 # Faster for previews, SaveImage keeps 1 for fork
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(s):
|
def INPUT_TYPES(s):
|
||||||
return {"required":
|
return {"required": {"images": ("IMAGE", )},
|
||||||
{"images": ("IMAGE", ), },
|
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}}
|
||||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
|
||||||
}
|
def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
|
||||||
|
from PIL import Image
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
|
||||||
|
filename_prefix += self.prefix_append
|
||||||
|
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
|
||||||
|
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for batch_number, image in enumerate(images):
|
||||||
|
i = 255. * image.cpu().numpy()
|
||||||
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||||
|
# Adaptive resize to max dimension ~512, preserve aspect ratio
|
||||||
|
max_size = 512
|
||||||
|
if max(img.width, img.height) > max_size:
|
||||||
|
scale = max_size / max(img.width, img.height)
|
||||||
|
new_width = int(img.width * scale)
|
||||||
|
new_height = int(img.height * scale)
|
||||||
|
img = img.resize((new_width, new_height), Image.LANCZOS)
|
||||||
|
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
|
||||||
|
file = f"{filename_with_batch_num}_{counter:05}_.png"
|
||||||
|
img.save(os.path.join(full_output_folder, file), format="PNG", compress_level=self.compress_level, optimize=True)
|
||||||
|
results.append({
|
||||||
|
"filename": file,
|
||||||
|
"subfolder": subfolder,
|
||||||
|
"type": self.type
|
||||||
|
})
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
return {"ui": {"images": results}}
|
||||||
|
|
||||||
class LoadImage:
|
class LoadImage:
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user