From a96e65df18980b856a764a40bda524042434f8b2 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Thu, 26 Jun 2025 00:39:09 -0700 Subject: [PATCH 01/27] Disable omnigen2 fp16 on older pytorch versions. (#8672) --- comfy/model_management.py | 7 +++++++ comfy/supported_models.py | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/comfy/model_management.py b/comfy/model_management.py index 054291432..816caf18f 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -1290,6 +1290,13 @@ def supports_fp8_compute(device=None): return True +def extended_fp16_support(): + # TODO: check why some models work with fp16 on newer torch versions but not on older + if torch_version_numeric < (2, 7): + return False + + return True + def soft_empty_cache(force=False): global cpu_state if cpu_state == CPUState.MPS: diff --git a/comfy/supported_models.py b/comfy/supported_models.py index f4413d647..2669ca01e 100644 --- a/comfy/supported_models.py +++ b/comfy/supported_models.py @@ -1197,11 +1197,16 @@ class Omnigen2(supported_models_base.BASE): unet_extra_config = {} latent_format = latent_formats.Flux - supported_inference_dtypes = [torch.float16, torch.bfloat16, torch.float32] + supported_inference_dtypes = [torch.bfloat16, torch.float32] vae_key_prefix = ["vae."] text_encoder_key_prefix = ["text_encoders."] + def __init__(self, unet_config): + super().__init__(unet_config) + if comfy.model_management.extended_fp16_support(): + self.supported_inference_dtypes = [torch.float16] + self.supported_inference_dtypes + def get_model(self, state_dict, prefix="", device=None): out = model_base.Omnigen2(self, device=device) return out From ef5266b1c1ffabcfec147416f108da56abb565ad Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Thu, 26 Jun 2025 08:28:41 -0700 Subject: [PATCH 02/27] Support Flux Kontext Dev model. (#8679) --- comfy/ldm/flux/model.py | 42 ++++++++++++++++++++++++++++++----- comfy/model_base.py | 16 ++++++++++++++ comfy_extras/nodes_flux.py | 45 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/comfy/ldm/flux/model.py b/comfy/ldm/flux/model.py index 846703d52..8f4d99f54 100644 --- a/comfy/ldm/flux/model.py +++ b/comfy/ldm/flux/model.py @@ -195,20 +195,50 @@ class Flux(nn.Module): img = self.final_layer(img, vec) # (N, T, patch_size ** 2 * out_channels) return img - def forward(self, x, timestep, context, y=None, guidance=None, control=None, transformer_options={}, **kwargs): + def process_img(self, x, index=0, h_offset=0, w_offset=0): bs, c, h, w = x.shape patch_size = self.patch_size x = comfy.ldm.common_dit.pad_to_patch_size(x, (patch_size, patch_size)) img = rearrange(x, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=patch_size, pw=patch_size) - h_len = ((h + (patch_size // 2)) // patch_size) w_len = ((w + (patch_size // 2)) // patch_size) + + h_offset = ((h_offset + (patch_size // 2)) // patch_size) + w_offset = ((w_offset + (patch_size // 2)) // patch_size) + img_ids = torch.zeros((h_len, w_len, 3), device=x.device, dtype=x.dtype) - img_ids[:, :, 1] = img_ids[:, :, 1] + torch.linspace(0, h_len - 1, steps=h_len, device=x.device, dtype=x.dtype).unsqueeze(1) - img_ids[:, :, 2] = img_ids[:, :, 2] + torch.linspace(0, w_len - 1, steps=w_len, device=x.device, dtype=x.dtype).unsqueeze(0) - img_ids = repeat(img_ids, "h w c -> b (h w) c", b=bs) + img_ids[:, :, 0] = img_ids[:, :, 1] + index + img_ids[:, :, 1] = img_ids[:, :, 1] + torch.linspace(h_offset, h_len - 1 + h_offset, steps=h_len, device=x.device, dtype=x.dtype).unsqueeze(1) + img_ids[:, :, 2] = img_ids[:, :, 2] + torch.linspace(w_offset, w_len - 1 + w_offset, steps=w_len, device=x.device, dtype=x.dtype).unsqueeze(0) + return img, repeat(img_ids, "h w c -> b (h w) c", b=bs) + + def forward(self, x, timestep, context, y=None, guidance=None, ref_latents=None, control=None, transformer_options={}, **kwargs): + bs, c, h_orig, w_orig = x.shape + patch_size = self.patch_size + + h_len = ((h_orig + (patch_size // 2)) // patch_size) + w_len = ((w_orig + (patch_size // 2)) // patch_size) + img, img_ids = self.process_img(x) + img_tokens = img.shape[1] + if ref_latents is not None: + h = 0 + w = 0 + for ref in ref_latents: + h_offset = 0 + w_offset = 0 + if ref.shape[-2] + h > ref.shape[-1] + w: + w_offset = w + else: + h_offset = h + + kontext, kontext_ids = self.process_img(ref, index=1, h_offset=h_offset, w_offset=w_offset) + img = torch.cat([img, kontext], dim=1) + img_ids = torch.cat([img_ids, kontext_ids], dim=1) + h = max(h, ref.shape[-2] + h_offset) + w = max(w, ref.shape[-1] + w_offset) txt_ids = torch.zeros((bs, context.shape[1], 3), device=x.device, dtype=x.dtype) out = self.forward_orig(img, img_ids, context, txt_ids, timestep, y, guidance, control, transformer_options, attn_mask=kwargs.get("attention_mask", None)) - return rearrange(out, "b (h w) (c ph pw) -> b c (h ph) (w pw)", h=h_len, w=w_len, ph=2, pw=2)[:,:,:h,:w] + out = out[:, :img_tokens] + return rearrange(out, "b (h w) (c ph pw) -> b c (h ph) (w pw)", h=h_len, w=w_len, ph=2, pw=2)[:,:,:h_orig,:w_orig] diff --git a/comfy/model_base.py b/comfy/model_base.py index 12b0f3dc9..fcdfde378 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -816,6 +816,7 @@ class PixArt(BaseModel): class Flux(BaseModel): def __init__(self, model_config, model_type=ModelType.FLUX, device=None, unet_model=comfy.ldm.flux.model.Flux): super().__init__(model_config, model_type, device=device, unet_model=unet_model) + self.memory_usage_factor_conds = ("kontext",) def concat_cond(self, **kwargs): try: @@ -876,8 +877,23 @@ class Flux(BaseModel): guidance = kwargs.get("guidance", 3.5) if guidance is not None: out['guidance'] = comfy.conds.CONDRegular(torch.FloatTensor([guidance])) + + ref_latents = kwargs.get("reference_latents", None) + if ref_latents is not None: + latents = [] + for lat in ref_latents: + latents.append(self.process_latent_in(lat)) + out['ref_latents'] = comfy.conds.CONDList(latents) return out + def extra_conds_shapes(self, **kwargs): + out = {} + ref_latents = kwargs.get("reference_latents", None) + if ref_latents is not None: + out['ref_latents'] = list([1, 16, sum(map(lambda a: math.prod(a.size()), ref_latents)) // 16]) + return out + + class GenmoMochi(BaseModel): def __init__(self, model_config, model_type=ModelType.FLOW, device=None): super().__init__(model_config, model_type, device=device, unet_model=comfy.ldm.genmo.joint_model.asymm_models_joint.AsymmDiTJoint) diff --git a/comfy_extras/nodes_flux.py b/comfy_extras/nodes_flux.py index ad6c15f37..8a8a17698 100644 --- a/comfy_extras/nodes_flux.py +++ b/comfy_extras/nodes_flux.py @@ -1,4 +1,5 @@ import node_helpers +import comfy.utils class CLIPTextEncodeFlux: @classmethod @@ -56,8 +57,52 @@ class FluxDisableGuidance: return (c, ) +PREFERED_KONTEXT_RESOLUTIONS = [ + (672, 1568), + (688, 1504), + (720, 1456), + (752, 1392), + (800, 1328), + (832, 1248), + (880, 1184), + (944, 1104), + (1024, 1024), + (1104, 944), + (1184, 880), + (1248, 832), + (1328, 800), + (1392, 752), + (1456, 720), + (1504, 688), + (1568, 672), +] + + +class FluxKontextImageScale: + @classmethod + def INPUT_TYPES(s): + return {"required": {"image": ("IMAGE", ), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "scale" + + CATEGORY = "advanced/conditioning/flux" + DESCRIPTION = "This node resizes the image to one that is more optimal for flux kontext." + + def scale(self, image): + width = image.shape[2] + height = image.shape[1] + aspect_ratio = width / height + _, width, height = min((abs(aspect_ratio - w / h), w, h) for w, h in PREFERED_KONTEXT_RESOLUTIONS) + image = comfy.utils.common_upscale(image.movedim(-1, 1), width, height, "lanczos", "center").movedim(1, -1) + return (image, ) + + NODE_CLASS_MAPPINGS = { "CLIPTextEncodeFlux": CLIPTextEncodeFlux, "FluxGuidance": FluxGuidance, "FluxDisableGuidance": FluxDisableGuidance, + "FluxKontextImageScale": FluxKontextImageScale, } From 68f4496b8ea51934883df46fce946da74f7b78eb Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Thu, 26 Jun 2025 08:29:03 -0700 Subject: [PATCH 03/27] Update frontend to 1.23.3 (#8678) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9a1ed2072..68b9abd4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -comfyui-frontend-package==1.22.2 +comfyui-frontend-package==1.23.3 comfyui-workflow-templates==0.1.29 comfyui-embedded-docs==0.2.2 torch From 7d8cf4cacc45e0ab58f1446a51287de17f6de6f5 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Thu, 26 Jun 2025 08:39:40 -0700 Subject: [PATCH 04/27] Update requirements.txt (#8680) --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 68b9abd4f..2006d48d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ comfyui-frontend-package==1.23.3 -comfyui-workflow-templates==0.1.29 -comfyui-embedded-docs==0.2.2 +comfyui-workflow-templates==0.1.30 +comfyui-embedded-docs==0.2.3 torch torchsde torchvision From b976f934ae112ff515d2c7fe362a1a118ddd7072 Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Thu, 26 Jun 2025 08:44:12 -0700 Subject: [PATCH 05/27] Update frontend to 1.23.4 (#8681) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2006d48d9..82e168b52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -comfyui-frontend-package==1.23.3 +comfyui-frontend-package==1.23.4 comfyui-workflow-templates==0.1.30 comfyui-embedded-docs==0.2.3 torch From 6493709d6aa3db3fa0179b4d8da003145a750ded Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Thu, 26 Jun 2025 11:47:07 -0400 Subject: [PATCH 06/27] ComfyUI version 0.3.42 --- comfyui_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comfyui_version.py b/comfyui_version.py index fedd3466f..26cada11a 100644 --- a/comfyui_version.py +++ b/comfyui_version.py @@ -1,3 +1,3 @@ # This file is automatically generated by the build process when version is # updated in pyproject.toml. -__version__ = "0.3.41" +__version__ = "0.3.42" diff --git a/pyproject.toml b/pyproject.toml index c572ad4c6..2c6894c6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.3.41" +version = "0.3.42" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.9" From bd951a714f8c736680fe13e735eee71acf73dd4c Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Thu, 26 Jun 2025 09:26:29 -0700 Subject: [PATCH 07/27] Add Flux Kontext and Omnigen 2 models to readme. (#8682) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6366280e7..7e6a3a0b1 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,9 @@ See what ComfyUI can do with the [example workflows](https://comfyanonymous.gith - [Lumina Image 2.0](https://comfyanonymous.github.io/ComfyUI_examples/lumina2/) - [HiDream](https://comfyanonymous.github.io/ComfyUI_examples/hidream/) - [Cosmos Predict2](https://comfyanonymous.github.io/ComfyUI_examples/cosmos_predict2/) +- Image Editing Models + - [Omnigen 2](https://comfyanonymous.github.io/ComfyUI_examples/omnigen/) + - [Flux Kontext](https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-kontext-image-editing-model) - Video Models - [Stable Video Diffusion](https://comfyanonymous.github.io/ComfyUI_examples/video/) - [Mochi](https://comfyanonymous.github.io/ComfyUI_examples/mochi/) From 9093301a49a90b654a7f37f6f621784b78579e2d Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:14:56 -0700 Subject: [PATCH 08/27] Don't add tiny bit of random noise when VAE encoding. (#8705) Shouldn't change outputs but might make things a tiny bit more deterministic. --- comfy/ldm/models/autoencoder.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/comfy/ldm/models/autoencoder.py b/comfy/ldm/models/autoencoder.py index e6493155e..13bd6e16b 100644 --- a/comfy/ldm/models/autoencoder.py +++ b/comfy/ldm/models/autoencoder.py @@ -11,7 +11,7 @@ from comfy.ldm.modules.ema import LitEma import comfy.ops class DiagonalGaussianRegularizer(torch.nn.Module): - def __init__(self, sample: bool = True): + def __init__(self, sample: bool = False): super().__init__() self.sample = sample @@ -19,16 +19,12 @@ class DiagonalGaussianRegularizer(torch.nn.Module): yield from () def forward(self, z: torch.Tensor) -> Tuple[torch.Tensor, dict]: - log = dict() posterior = DiagonalGaussianDistribution(z) if self.sample: z = posterior.sample() else: z = posterior.mode() - kl_loss = posterior.kl() - kl_loss = torch.sum(kl_loss) / kl_loss.shape[0] - log["kl_loss"] = kl_loss - return z, log + return z, None class AbstractAutoencoder(torch.nn.Module): From c36be0ea09ddd89b69dd786dbb525402c7041408 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Fri, 27 Jun 2025 14:21:12 -0700 Subject: [PATCH 09/27] Fix memory estimation bug with kontext. (#8709) --- comfy/model_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy/model_base.py b/comfy/model_base.py index fcdfde378..4392355ea 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -816,7 +816,7 @@ class PixArt(BaseModel): class Flux(BaseModel): def __init__(self, model_config, model_type=ModelType.FLUX, device=None, unet_model=comfy.ldm.flux.model.Flux): super().__init__(model_config, model_type, device=device, unet_model=unet_model) - self.memory_usage_factor_conds = ("kontext",) + self.memory_usage_factor_conds = ("ref_latents",) def concat_cond(self, **kwargs): try: From e18f53cca9062cc6b165e16712772437b80333f2 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Fri, 27 Jun 2025 17:22:02 -0400 Subject: [PATCH 10/27] ComfyUI version 0.3.43 --- comfyui_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comfyui_version.py b/comfyui_version.py index 26cada11a..c98c90499 100644 --- a/comfyui_version.py +++ b/comfyui_version.py @@ -1,3 +1,3 @@ # This file is automatically generated by the build process when version is # updated in pyproject.toml. -__version__ = "0.3.42" +__version__ = "0.3.43" diff --git a/pyproject.toml b/pyproject.toml index 2c6894c6e..9d0f90032 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.3.42" +version = "0.3.43" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.9" From ba9548f75637fbc091c87083b9fc361264ccc4e6 Mon Sep 17 00:00:00 2001 From: xufeng <57523724+lgldlk@users.noreply.github.com> Date: Sun, 29 Jun 2025 03:24:02 +0800 Subject: [PATCH 11/27] =?UTF-8?q?=E2=80=9C--whitelist-custom-nodes?= =?UTF-8?q?=E2=80=9D=20args=20for=20comfy=20core=20to=20go=20with=20?= =?UTF-8?q?=E2=80=9C--disable-all-custom-nodes=E2=80=9D=20for=20developmen?= =?UTF-8?q?t=20purposes=20=20(#8592)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: “--whitelist-custom-nodes” args for comfy core to go with “--disable-all-custom-nodes” for development purposes * feat: Simplify custom nodes whitelist logic to use consistent code paths --- comfy/cli_args.py | 1 + main.py | 11 +++++++---- nodes.py | 3 +++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/comfy/cli_args.py b/comfy/cli_args.py index 741ecac3f..7234a7ba0 100644 --- a/comfy/cli_args.py +++ b/comfy/cli_args.py @@ -151,6 +151,7 @@ parser.add_argument("--windows-standalone-build", action="store_true", help="Win parser.add_argument("--disable-metadata", action="store_true", help="Disable saving prompt metadata in files.") parser.add_argument("--disable-all-custom-nodes", action="store_true", help="Disable loading all custom nodes.") +parser.add_argument("--whitelist-custom-nodes", type=str, nargs='+', default=[], help="Specify custom node folders to load even when --disable-all-custom-nodes is enabled.") parser.add_argument("--disable-api-nodes", action="store_true", help="Disable loading all api nodes.") parser.add_argument("--multi-user", action="store_true", help="Enables per-user storage.") diff --git a/main.py b/main.py index 0d7c97dcb..5dd3c92d2 100644 --- a/main.py +++ b/main.py @@ -66,9 +66,6 @@ def execute_prestartup_script(): logging.error(f"Failed to execute startup-script: {script_path} / {e}") return False - if args.disable_all_custom_nodes: - return - node_paths = folder_paths.get_folder_paths("custom_nodes") for custom_node_path in node_paths: possible_modules = os.listdir(custom_node_path) @@ -81,6 +78,9 @@ def execute_prestartup_script(): script_path = os.path.join(module_path, "prestartup_script.py") if os.path.exists(script_path): + if args.disable_all_custom_nodes and possible_module not in args.whitelist_custom_nodes: + logging.info(f"Prestartup Skipping {possible_module} due to disable_all_custom_nodes and whitelist_custom_nodes") + continue time_before = time.perf_counter() success = execute_script(script_path) node_prestartup_times.append((time.perf_counter() - time_before, module_path, success)) @@ -276,7 +276,10 @@ def start_comfyui(asyncio_loop=None): prompt_server = server.PromptServer(asyncio_loop) hook_breaker_ac10a0.save_functions() - nodes.init_extra_nodes(init_custom_nodes=not args.disable_all_custom_nodes, init_api_nodes=not args.disable_api_nodes) + nodes.init_extra_nodes( + init_custom_nodes=True, + init_api_nodes=not args.disable_api_nodes + ) hook_breaker_ac10a0.restore_functions() cuda_malloc_warning() diff --git a/nodes.py b/nodes.py index 11aa50fce..99411a1fe 100644 --- a/nodes.py +++ b/nodes.py @@ -2187,6 +2187,9 @@ def init_external_custom_nodes(): module_path = os.path.join(custom_node_path, possible_module) if os.path.isfile(module_path) and os.path.splitext(module_path)[1] != ".py": continue if module_path.endswith(".disabled"): continue + if args.disable_all_custom_nodes and possible_module not in args.whitelist_custom_nodes: + logging.info(f"Skipping {possible_module} due to disable_all_custom_nodes and whitelist_custom_nodes") + continue time_before = time.perf_counter() success = load_custom_node(module_path, base_node_names, module_parent="custom_nodes") node_import_times.append((time.perf_counter() - time_before, module_path, success)) From a3cf272522f9820c3f379aa821729404cb4cf821 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Sat, 28 Jun 2025 12:53:40 -0700 Subject: [PATCH 12/27] Skip custom node logic completely if disabled and no whitelisted nodes. (#8719) --- main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 5dd3c92d2..d488c0f4c 100644 --- a/main.py +++ b/main.py @@ -55,6 +55,9 @@ def apply_custom_paths(): def execute_prestartup_script(): + if args.disable_all_custom_nodes and len(args.whitelist_custom_nodes) == 0: + return + def execute_script(script_path): module_name = os.path.splitext(script_path)[0] try: @@ -277,7 +280,7 @@ def start_comfyui(asyncio_loop=None): hook_breaker_ac10a0.save_functions() nodes.init_extra_nodes( - init_custom_nodes=True, + init_custom_nodes=(not args.disable_all_custom_nodes) or len(args.whitelist_custom_nodes) > 0, init_api_nodes=not args.disable_api_nodes ) hook_breaker_ac10a0.restore_functions() From 396454fa410781008015c73f7e0a5014dac0609e Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Sat, 28 Jun 2025 15:12:56 -0700 Subject: [PATCH 13/27] Reorder the schedulers so simple is the default one. (#8722) --- comfy/samplers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/comfy/samplers.py b/comfy/samplers.py index efe9bf867..078a675f4 100644 --- a/comfy/samplers.py +++ b/comfy/samplers.py @@ -1039,13 +1039,13 @@ class SchedulerHandler(NamedTuple): use_ms: bool = True SCHEDULER_HANDLERS = { - "normal": SchedulerHandler(normal_scheduler), + "simple": SchedulerHandler(simple_scheduler), + "sgm_uniform": SchedulerHandler(partial(normal_scheduler, sgm=True)), "karras": SchedulerHandler(k_diffusion_sampling.get_sigmas_karras, use_ms=False), "exponential": SchedulerHandler(k_diffusion_sampling.get_sigmas_exponential, use_ms=False), - "sgm_uniform": SchedulerHandler(partial(normal_scheduler, sgm=True)), - "simple": SchedulerHandler(simple_scheduler), "ddim_uniform": SchedulerHandler(ddim_scheduler), "beta": SchedulerHandler(beta_scheduler), + "normal": SchedulerHandler(normal_scheduler), "linear_quadratic": SchedulerHandler(linear_quadratic_schedule), "kl_optimal": SchedulerHandler(kl_optimal_scheduler, use_ms=False), } From 5b4eb021cb392680c62ef5c2bc1afe560bde37b3 Mon Sep 17 00:00:00 2001 From: chaObserv <154517000+chaObserv@users.noreply.github.com> Date: Sun, 29 Jun 2025 06:13:13 +0800 Subject: [PATCH 14/27] Perpneg guider with updated pre and post-cfg (#8698) --- comfy_extras/nodes_perpneg.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/comfy_extras/nodes_perpneg.py b/comfy_extras/nodes_perpneg.py index 6c6f71767..f051cbf9a 100644 --- a/comfy_extras/nodes_perpneg.py +++ b/comfy_extras/nodes_perpneg.py @@ -69,8 +69,17 @@ class Guider_PerpNeg(comfy.samplers.CFGGuider): negative_cond = self.conds.get("negative", None) empty_cond = self.conds.get("empty_negative_prompt", None) - (noise_pred_pos, noise_pred_neg, noise_pred_empty) = \ - comfy.samplers.calc_cond_batch(self.inner_model, [positive_cond, negative_cond, empty_cond], x, timestep, model_options) + conds = [positive_cond, negative_cond, empty_cond] + + out = comfy.samplers.calc_cond_batch(self.inner_model, conds, x, timestep, model_options) + + # Apply pre_cfg_functions since sampling_function() is skipped + for fn in model_options.get("sampler_pre_cfg_function", []): + args = {"conds":conds, "conds_out": out, "cond_scale": self.cfg, "timestep": timestep, + "input": x, "sigma": timestep, "model": self.inner_model, "model_options": model_options} + out = fn(args) + + noise_pred_pos, noise_pred_neg, noise_pred_empty = out cfg_result = perp_neg(x, noise_pred_pos, noise_pred_neg, noise_pred_empty, self.neg_scale, self.cfg) # normally this would be done in cfg_function, but we skipped @@ -82,6 +91,7 @@ class Guider_PerpNeg(comfy.samplers.CFGGuider): "denoised": cfg_result, "cond": positive_cond, "uncond": negative_cond, + "cond_scale": self.cfg, "model": self.inner_model, "uncond_denoised": noise_pred_neg, "cond_denoised": noise_pred_pos, From e195c1b13ff96a9ee2c2de503cbf9a7a99babec5 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Sat, 28 Jun 2025 16:11:16 -0700 Subject: [PATCH 15/27] Make stable release workflow publish drafts. (#8723) --- .github/workflows/stable-release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml index a046ff9ea..61105abe4 100644 --- a/.github/workflows/stable-release.yml +++ b/.github/workflows/stable-release.yml @@ -102,5 +102,4 @@ jobs: file: ComfyUI_windows_portable_nvidia.7z tag: ${{ inputs.git_tag }} overwrite: true - prerelease: true - make_latest: false + draft: true From 2a0b138feb7e8ed36cbf195dd1c2d49b469f1490 Mon Sep 17 00:00:00 2001 From: bmcomfy Date: Sat, 28 Jun 2025 16:11:40 -0700 Subject: [PATCH 16/27] build: add gh action to process releases (#8652) --- .github/workflows/release-webhook.yml | 108 ++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 .github/workflows/release-webhook.yml diff --git a/.github/workflows/release-webhook.yml b/.github/workflows/release-webhook.yml new file mode 100644 index 000000000..6fceb7560 --- /dev/null +++ b/.github/workflows/release-webhook.yml @@ -0,0 +1,108 @@ +name: Release Webhook + +on: + release: + types: [published] + +jobs: + send-webhook: + runs-on: ubuntu-latest + steps: + - name: Send release webhook + env: + WEBHOOK_URL: ${{ secrets.RELEASE_GITHUB_WEBHOOK_URL }} + WEBHOOK_SECRET: ${{ secrets.RELEASE_GITHUB_WEBHOOK_SECRET }} + run: | + # Generate UUID for delivery ID + DELIVERY_ID=$(uuidgen) + HOOK_ID="release-webhook-$(date +%s)" + + # Create webhook payload matching GitHub release webhook format + PAYLOAD=$(cat < Date: Sun, 29 Jun 2025 03:38:40 -0700 Subject: [PATCH 17/27] Fix contiguous issue with pytorch nightly. (#8729) --- comfy/text_encoders/t5.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy/text_encoders/t5.py b/comfy/text_encoders/t5.py index 49f0ba4fe..36bf35309 100644 --- a/comfy/text_encoders/t5.py +++ b/comfy/text_encoders/t5.py @@ -146,7 +146,7 @@ class T5Attention(torch.nn.Module): ) values = self.relative_attention_bias(relative_position_bucket, out_dtype=dtype) # shape (query_length, key_length, num_heads) values = values.permute([2, 0, 1]).unsqueeze(0) # shape (1, num_heads, query_length, key_length) - return values + return values.contiguous() def forward(self, x, mask=None, past_bias=None, optimized_attention=None): q = self.q(x) From cf49a2c5b575d59574e17ba3268f1938107d635a Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:18:25 -0700 Subject: [PATCH 18/27] Dual cfg node optimizations when cfg is 1.0 (#8747) --- comfy_extras/nodes_custom_sampler.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/comfy_extras/nodes_custom_sampler.py b/comfy_extras/nodes_custom_sampler.py index 3e5be3d3c..fc506a0cc 100644 --- a/comfy_extras/nodes_custom_sampler.py +++ b/comfy_extras/nodes_custom_sampler.py @@ -609,8 +609,14 @@ class Guider_DualCFG(comfy.samplers.CFGGuider): def predict_noise(self, x, timestep, model_options={}, seed=None): negative_cond = self.conds.get("negative", None) middle_cond = self.conds.get("middle", None) + positive_cond = self.conds.get("positive", None) + if model_options.get("disable_cfg1_optimization", False) == False: + if math.isclose(self.cfg2, 1.0): + negative_cond = None + if math.isclose(self.cfg1, 1.0): + middle_cond = None - out = comfy.samplers.calc_cond_batch(self.inner_model, [negative_cond, middle_cond, self.conds.get("positive", None)], x, timestep, model_options) + out = comfy.samplers.calc_cond_batch(self.inner_model, [negative_cond, middle_cond, positive_cond], x, timestep, model_options) return comfy.samplers.cfg_function(self.inner_model, out[1], out[0], self.cfg2, x, timestep, model_options=model_options, cond=middle_cond, uncond=negative_cond) + (out[2] - out[1]) * self.cfg1 class DualCFGGuider: From c46268bf60454ce0634b56d1b29f50f04fbc162b Mon Sep 17 00:00:00 2001 From: ComfyUI Wiki Date: Tue, 1 Jul 2025 02:18:43 +0800 Subject: [PATCH 19/27] Update requirements.txt (#8741) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 82e168b52..479a29eec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ comfyui-frontend-package==1.23.4 -comfyui-workflow-templates==0.1.30 +comfyui-workflow-templates==0.1.31 comfyui-embedded-docs==0.2.3 torch torchsde From f02de13316b24436eb69222d7bc8181b73eeccb2 Mon Sep 17 00:00:00 2001 From: chaObserv <154517000+chaObserv@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:33:07 +0800 Subject: [PATCH 20/27] Add TCFG node (#8730) --- comfy_extras/nodes_tcfg.py | 71 ++++++++++++++++++++++++++++++++++++++ nodes.py | 1 + 2 files changed, 72 insertions(+) create mode 100644 comfy_extras/nodes_tcfg.py diff --git a/comfy_extras/nodes_tcfg.py b/comfy_extras/nodes_tcfg.py new file mode 100644 index 000000000..35b89a73f --- /dev/null +++ b/comfy_extras/nodes_tcfg.py @@ -0,0 +1,71 @@ +# TCFG: Tangential Damping Classifier-free Guidance - (arXiv: https://arxiv.org/abs/2503.18137) + +import torch + +from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict + + +def score_tangential_damping(cond_score: torch.Tensor, uncond_score: torch.Tensor) -> torch.Tensor: + """Drop tangential components from uncond score to align with cond score.""" + # (B, 1, ...) + batch_num = cond_score.shape[0] + cond_score_flat = cond_score.reshape(batch_num, 1, -1).float() + uncond_score_flat = uncond_score.reshape(batch_num, 1, -1).float() + + # Score matrix A (B, 2, ...) + score_matrix = torch.cat((uncond_score_flat, cond_score_flat), dim=1) + try: + _, _, Vh = torch.linalg.svd(score_matrix, full_matrices=False) + except RuntimeError: + # Fallback to CPU + _, _, Vh = torch.linalg.svd(score_matrix.cpu(), full_matrices=False) + + # Drop the tangential components + v1 = Vh[:, 0:1, :].to(uncond_score_flat.device) # (B, 1, ...) + uncond_score_td = (uncond_score_flat @ v1.transpose(-2, -1)) * v1 + return uncond_score_td.reshape_as(uncond_score).to(uncond_score.dtype) + + +class TCFG(ComfyNodeABC): + @classmethod + def INPUT_TYPES(cls) -> InputTypeDict: + return { + "required": { + "model": (IO.MODEL, {}), + } + } + + RETURN_TYPES = (IO.MODEL,) + RETURN_NAMES = ("patched_model",) + FUNCTION = "patch" + + CATEGORY = "advanced/guidance" + DESCRIPTION = "TCFG – Tangential Damping CFG (2503.18137)\n\nRefine the uncond (negative) to align with the cond (positive) for improving quality." + + def patch(self, model): + m = model.clone() + + def tangential_damping_cfg(args): + # Assume [cond, uncond, ...] + x = args["input"] + conds_out = args["conds_out"] + if len(conds_out) <= 1 or None in args["conds"][:2]: + # Skip when either cond or uncond is None + return conds_out + cond_pred = conds_out[0] + uncond_pred = conds_out[1] + uncond_td = score_tangential_damping(x - cond_pred, x - uncond_pred) + uncond_pred_td = x - uncond_td + return [cond_pred, uncond_pred_td] + conds_out[2:] + + m.set_model_sampler_pre_cfg_function(tangential_damping_cfg) + return (m,) + + +NODE_CLASS_MAPPINGS = { + "TCFG": TCFG, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "TCFG": "Tangential Damping CFG", +} diff --git a/nodes.py b/nodes.py index 99411a1fe..1b465b9e6 100644 --- a/nodes.py +++ b/nodes.py @@ -2283,6 +2283,7 @@ def init_builtin_extra_nodes(): "nodes_string.py", "nodes_camera_trajectory.py", "nodes_edit_model.py", + "nodes_tcfg.py" ] import_failed = [] From b22e97dcfa1736190cfcafd6091c4da885fcf48a Mon Sep 17 00:00:00 2001 From: chaObserv <154517000+chaObserv@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:38:52 +0800 Subject: [PATCH 21/27] Migrate ER-SDE from VE to VP algorithm and add its sampler node (#8744) Apply alpha scaling in the algorithm for reverse-time SDE and add custom ER-SDE sampler node for other solver types (SDE, ODE). --- comfy/k_diffusion/sampling.py | 65 ++++++++++++++++------------ comfy_extras/nodes_custom_sampler.py | 42 ++++++++++++++++++ 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/comfy/k_diffusion/sampling.py b/comfy/k_diffusion/sampling.py index 739468872..e231d6a3d 100644 --- a/comfy/k_diffusion/sampling.py +++ b/comfy/k_diffusion/sampling.py @@ -1447,14 +1447,15 @@ def sample_gradient_estimation(model, x, sigmas, extra_args=None, callback=None, old_d = d return x + @torch.no_grad() def sample_gradient_estimation_cfg_pp(model, x, sigmas, extra_args=None, callback=None, disable=None, ge_gamma=2.): return sample_gradient_estimation(model, x, sigmas, extra_args=extra_args, callback=callback, disable=disable, ge_gamma=ge_gamma, cfg_pp=True) + @torch.no_grad() -def sample_er_sde(model, x, sigmas, extra_args=None, callback=None, disable=None, s_noise=1., noise_sampler=None, noise_scaler=None, max_stage=3): - """ - Extended Reverse-Time SDE solver (VE ER-SDE-Solver-3). Arxiv: https://arxiv.org/abs/2309.06169. +def sample_er_sde(model, x, sigmas, extra_args=None, callback=None, disable=None, s_noise=1.0, noise_sampler=None, noise_scaler=None, max_stage=3): + """Extended Reverse-Time SDE solver (VP ER-SDE-Solver-3). arXiv: https://arxiv.org/abs/2309.06169. Code reference: https://github.com/QinpengCui/ER-SDE-Solver/blob/main/er_sde_solver.py. """ extra_args = {} if extra_args is None else extra_args @@ -1462,12 +1463,18 @@ def sample_er_sde(model, x, sigmas, extra_args=None, callback=None, disable=None noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler s_in = x.new_ones([x.shape[0]]) - def default_noise_scaler(sigma): - return sigma * ((sigma ** 0.3).exp() + 10.0) - noise_scaler = default_noise_scaler if noise_scaler is None else noise_scaler + def default_er_sde_noise_scaler(x): + return x * ((x ** 0.3).exp() + 10.0) + + noise_scaler = default_er_sde_noise_scaler if noise_scaler is None else noise_scaler num_integration_points = 200.0 point_indice = torch.arange(0, num_integration_points, dtype=torch.float32, device=x.device) + model_sampling = model.inner_model.model_patcher.get_model_object("model_sampling") + sigmas = offset_first_sigma_for_snr(sigmas, model_sampling) + half_log_snrs = sigma_to_half_log_snr(sigmas, model_sampling) + er_lambdas = half_log_snrs.neg().exp() # er_lambda_t = sigma_t / alpha_t + old_denoised = None old_denoised_d = None @@ -1478,32 +1485,36 @@ def sample_er_sde(model, x, sigmas, extra_args=None, callback=None, disable=None stage_used = min(max_stage, i + 1) if sigmas[i + 1] == 0: x = denoised - elif stage_used == 1: - r = noise_scaler(sigmas[i + 1]) / noise_scaler(sigmas[i]) - x = r * x + (1 - r) * denoised else: - r = noise_scaler(sigmas[i + 1]) / noise_scaler(sigmas[i]) - x = r * x + (1 - r) * denoised + er_lambda_s, er_lambda_t = er_lambdas[i], er_lambdas[i + 1] + alpha_s = sigmas[i] / er_lambda_s + alpha_t = sigmas[i + 1] / er_lambda_t + r_alpha = alpha_t / alpha_s + r = noise_scaler(er_lambda_t) / noise_scaler(er_lambda_s) - dt = sigmas[i + 1] - sigmas[i] - sigma_step_size = -dt / num_integration_points - sigma_pos = sigmas[i + 1] + point_indice * sigma_step_size - scaled_pos = noise_scaler(sigma_pos) + # Stage 1 Euler + x = r_alpha * r * x + alpha_t * (1 - r) * denoised - # Stage 2 - s = torch.sum(1 / scaled_pos) * sigma_step_size - denoised_d = (denoised - old_denoised) / (sigmas[i] - sigmas[i - 1]) - x = x + (dt + s * noise_scaler(sigmas[i + 1])) * denoised_d + if stage_used >= 2: + dt = er_lambda_t - er_lambda_s + lambda_step_size = -dt / num_integration_points + lambda_pos = er_lambda_t + point_indice * lambda_step_size + scaled_pos = noise_scaler(lambda_pos) - if stage_used >= 3: - # Stage 3 - s_u = torch.sum((sigma_pos - sigmas[i]) / scaled_pos) * sigma_step_size - denoised_u = (denoised_d - old_denoised_d) / ((sigmas[i] - sigmas[i - 2]) / 2) - x = x + ((dt ** 2) / 2 + s_u * noise_scaler(sigmas[i + 1])) * denoised_u - old_denoised_d = denoised_d + # Stage 2 + s = torch.sum(1 / scaled_pos) * lambda_step_size + denoised_d = (denoised - old_denoised) / (er_lambda_s - er_lambdas[i - 1]) + x = x + alpha_t * (dt + s * noise_scaler(er_lambda_t)) * denoised_d - if s_noise != 0 and sigmas[i + 1] > 0: - x = x + noise_sampler(sigmas[i], sigmas[i + 1]) * s_noise * (sigmas[i + 1] ** 2 - sigmas[i] ** 2 * r ** 2).sqrt().nan_to_num(nan=0.0) + if stage_used >= 3: + # Stage 3 + s_u = torch.sum((lambda_pos - er_lambda_s) / scaled_pos) * lambda_step_size + denoised_u = (denoised_d - old_denoised_d) / ((er_lambda_s - er_lambdas[i - 2]) / 2) + x = x + alpha_t * ((dt ** 2) / 2 + s_u * noise_scaler(er_lambda_t)) * denoised_u + old_denoised_d = denoised_d + + if s_noise > 0: + x = x + alpha_t * noise_sampler(sigmas[i], sigmas[i + 1]) * s_noise * (er_lambda_t ** 2 - er_lambda_s ** 2 * r ** 2).sqrt().nan_to_num(nan=0.0) old_denoised = denoised return x diff --git a/comfy_extras/nodes_custom_sampler.py b/comfy_extras/nodes_custom_sampler.py index fc506a0cc..b3a772714 100644 --- a/comfy_extras/nodes_custom_sampler.py +++ b/comfy_extras/nodes_custom_sampler.py @@ -2,6 +2,7 @@ import math import comfy.samplers import comfy.sample from comfy.k_diffusion import sampling as k_diffusion_sampling +from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict import latent_preview import torch import comfy.utils @@ -480,6 +481,46 @@ class SamplerDPMAdaptative: "s_noise":s_noise }) return (sampler, ) + +class SamplerER_SDE(ComfyNodeABC): + @classmethod + def INPUT_TYPES(cls) -> InputTypeDict: + return { + "required": { + "solver_type": (IO.COMBO, {"options": ["ER-SDE", "Reverse-time SDE", "ODE"]}), + "max_stage": (IO.INT, {"default": 3, "min": 1, "max": 3}), + "eta": ( + IO.FLOAT, + {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.01, "round": False, "tooltip": "Stochastic strength of reverse-time SDE.\nWhen eta=0, it reduces to deterministic ODE. This setting doesn't apply to ER-SDE solver type."}, + ), + "s_noise": (IO.FLOAT, {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.01, "round": False}), + } + } + + RETURN_TYPES = (IO.SAMPLER,) + CATEGORY = "sampling/custom_sampling/samplers" + + FUNCTION = "get_sampler" + + def get_sampler(self, solver_type, max_stage, eta, s_noise): + if solver_type == "ODE" or (solver_type == "Reverse-time SDE" and eta == 0): + eta = 0 + s_noise = 0 + + def reverse_time_sde_noise_scaler(x): + return x ** (eta + 1) + + if solver_type == "ER-SDE": + # Use the default one in sample_er_sde() + noise_scaler = None + else: + noise_scaler = reverse_time_sde_noise_scaler + + sampler_name = "er_sde" + sampler = comfy.samplers.ksampler(sampler_name, {"s_noise": s_noise, "noise_scaler": noise_scaler, "max_stage": max_stage}) + return (sampler,) + + class Noise_EmptyNoise: def __init__(self): self.seed = 0 @@ -787,6 +828,7 @@ NODE_CLASS_MAPPINGS = { "SamplerDPMPP_SDE": SamplerDPMPP_SDE, "SamplerDPMPP_2S_Ancestral": SamplerDPMPP_2S_Ancestral, "SamplerDPMAdaptative": SamplerDPMAdaptative, + "SamplerER_SDE": SamplerER_SDE, "SplitSigmas": SplitSigmas, "SplitSigmasDenoise": SplitSigmasDenoise, "FlipSigmas": FlipSigmas, From 772de7c00653fc3a825762f555e836d071a4dc80 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Tue, 1 Jul 2025 00:09:07 -0700 Subject: [PATCH 22/27] PerpNeg Guider optimizations. (#8753) --- comfy_extras/nodes_perpneg.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/comfy_extras/nodes_perpneg.py b/comfy_extras/nodes_perpneg.py index f051cbf9a..89e5eef90 100644 --- a/comfy_extras/nodes_perpneg.py +++ b/comfy_extras/nodes_perpneg.py @@ -4,6 +4,7 @@ import comfy.sampler_helpers import comfy.samplers import comfy.utils import node_helpers +import math def perp_neg(x, noise_pred_pos, noise_pred_neg, noise_pred_nocond, neg_scale, cond_scale): pos = noise_pred_pos - noise_pred_nocond @@ -69,6 +70,12 @@ class Guider_PerpNeg(comfy.samplers.CFGGuider): negative_cond = self.conds.get("negative", None) empty_cond = self.conds.get("empty_negative_prompt", None) + if model_options.get("disable_cfg1_optimization", False) == False: + if math.isclose(self.neg_scale, 0.0): + negative_cond = None + if math.isclose(self.cfg, 1.0): + empty_cond = None + conds = [positive_cond, negative_cond, empty_cond] out = comfy.samplers.calc_cond_batch(self.inner_model, conds, x, timestep, model_options) From 79ed75274874590967ff13ac73c5d84262d489d0 Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Tue, 1 Jul 2025 20:43:48 -0400 Subject: [PATCH 23/27] support upload 3d model to custom subfolder (#8597) --- comfy_extras/nodes_load_3d.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/comfy_extras/nodes_load_3d.py b/comfy_extras/nodes_load_3d.py index 40d03e18a..899608149 100644 --- a/comfy_extras/nodes_load_3d.py +++ b/comfy_extras/nodes_load_3d.py @@ -5,6 +5,8 @@ import os from comfy.comfy_types import IO from comfy_api.input_impl import VideoFromFile +from pathlib import Path + def normalize_path(path): return path.replace('\\', '/') @@ -16,7 +18,14 @@ class Load3D(): os.makedirs(input_dir, exist_ok=True) - files = [normalize_path(os.path.join("3d", f)) for f in os.listdir(input_dir) if f.endswith(('.gltf', '.glb', '.obj', '.fbx', '.stl'))] + input_path = Path(input_dir) + base_path = Path(folder_paths.get_input_directory()) + + files = [ + normalize_path(str(file_path.relative_to(base_path))) + for file_path in input_path.rglob("*") + if file_path.suffix.lower() in {'.gltf', '.glb', '.obj', '.fbx', '.stl'} + ] return {"required": { "model_file": (sorted(files), {"file_upload": True}), @@ -61,7 +70,14 @@ class Load3DAnimation(): os.makedirs(input_dir, exist_ok=True) - files = [normalize_path(os.path.join("3d", f)) for f in os.listdir(input_dir) if f.endswith(('.gltf', '.glb', '.fbx'))] + input_path = Path(input_dir) + base_path = Path(folder_paths.get_input_directory()) + + files = [ + normalize_path(str(file_path.relative_to(base_path))) + for file_path in input_path.rglob("*") + if file_path.suffix.lower() in {'.gltf', '.glb', '.fbx'} + ] return {"required": { "model_file": (sorted(files), {"file_upload": True}), From 111f583e00cbd7b08149856f2b6de7a58ea65c0b Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Tue, 1 Jul 2025 21:57:13 -0700 Subject: [PATCH 24/27] Fallback to regular op when fp8 op throws exception. (#8761) --- comfy/ops.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/comfy/ops.py b/comfy/ops.py index 431c8f89d..2cc9bbc27 100644 --- a/comfy/ops.py +++ b/comfy/ops.py @@ -336,9 +336,12 @@ class fp8_ops(manual_cast): return None def forward_comfy_cast_weights(self, input): - out = fp8_linear(self, input) - if out is not None: - return out + try: + out = fp8_linear(self, input) + if out is not None: + return out + except Exception as e: + logging.info("Exception during fp8 op: {}".format(e)) weight, bias = cast_bias_weight(self, input) return torch.nn.functional.linear(input, weight, bias) From 9f1069290c53c738998204eb87e82e595808871f Mon Sep 17 00:00:00 2001 From: Harel Cain Date: Wed, 2 Jul 2025 21:34:51 +0200 Subject: [PATCH 25/27] nodes_lt: fixes to latent conditioning at index > 0 (#8769) --- comfy_extras/nodes_lt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/comfy_extras/nodes_lt.py b/comfy_extras/nodes_lt.py index e6dc122ca..b5058667a 100644 --- a/comfy_extras/nodes_lt.py +++ b/comfy_extras/nodes_lt.py @@ -134,8 +134,8 @@ class LTXVAddGuide: _, num_keyframes = get_keyframe_idxs(cond) latent_count = latent_length - num_keyframes frame_idx = frame_idx if frame_idx >= 0 else max((latent_count - 1) * time_scale_factor + 1 + frame_idx, 0) - if guide_length > 1: - frame_idx = frame_idx // time_scale_factor * time_scale_factor # frame index must be divisible by 8 + if guide_length > 1 and frame_idx != 0: + frame_idx = (frame_idx - 1) // time_scale_factor * time_scale_factor + 1 # frame index - 1 must be divisible by 8 or frame_idx == 0 latent_idx = (frame_idx + time_scale_factor - 1) // time_scale_factor @@ -144,7 +144,7 @@ class LTXVAddGuide: def add_keyframe_index(self, cond, frame_idx, guiding_latent, scale_factors): keyframe_idxs, _ = get_keyframe_idxs(cond) _, latent_coords = self._patchifier.patchify(guiding_latent) - pixel_coords = latent_to_pixel_coords(latent_coords, scale_factors, True) + pixel_coords = latent_to_pixel_coords(latent_coords, scale_factors, causal_fix=frame_idx == 0) # we need the causal fix only if we're placing the new latents at index 0 pixel_coords[:, 0] += frame_idx if keyframe_idxs is None: keyframe_idxs = pixel_coords From 34c8eeec065856e835e5ccebfceb2ea3b76110a7 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Wed, 2 Jul 2025 12:35:11 -0700 Subject: [PATCH 26/27] Fix ImageColorToMask not returning right mask values. (#8771) --- comfy_extras/nodes_mask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy_extras/nodes_mask.py b/comfy_extras/nodes_mask.py index 99b264a32..ab387a2fc 100644 --- a/comfy_extras/nodes_mask.py +++ b/comfy_extras/nodes_mask.py @@ -152,7 +152,7 @@ class ImageColorToMask: def image_to_mask(self, image, color): temp = (torch.clamp(image, 0, 1.0) * 255.0).round().to(torch.int) temp = torch.bitwise_left_shift(temp[:,:,:,0], 16) + torch.bitwise_left_shift(temp[:,:,:,1], 8) + temp[:,:,:,2] - mask = torch.where(temp == color, 255, 0).float() + mask = torch.where(temp == color, 1.0, 0).float() return (mask,) class SolidMask: From d9277301d28e732e82d0de1d5948aa00acbf6b65 Mon Sep 17 00:00:00 2001 From: City <125218114+city96@users.noreply.github.com> Date: Thu, 3 Jul 2025 02:13:43 +0200 Subject: [PATCH 27/27] Initial code for new SLG node (#8759) --- comfy/model_patcher.py | 3 ++ comfy/samplers.py | 6 +++- comfy_extras/nodes_slg.py | 68 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/comfy/model_patcher.py b/comfy/model_patcher.py index b1d6d4395..52e76b5f3 100644 --- a/comfy/model_patcher.py +++ b/comfy/model_patcher.py @@ -379,6 +379,9 @@ class ModelPatcher: def set_model_sampler_pre_cfg_function(self, pre_cfg_function, disable_cfg1_optimization=False): self.model_options = set_model_options_pre_cfg_function(self.model_options, pre_cfg_function, disable_cfg1_optimization) + def set_model_sampler_calc_cond_batch_function(self, sampler_calc_cond_batch_function): + self.model_options["sampler_calc_cond_batch_function"] = sampler_calc_cond_batch_function + def set_model_unet_function_wrapper(self, unet_wrapper_function: UnetWrapperFunction): self.model_options["model_function_wrapper"] = unet_wrapper_function diff --git a/comfy/samplers.py b/comfy/samplers.py index 078a675f4..25ccaf39f 100644 --- a/comfy/samplers.py +++ b/comfy/samplers.py @@ -373,7 +373,11 @@ def sampling_function(model, x, timestep, uncond, cond, cond_scale, model_option uncond_ = uncond conds = [cond, uncond_] - out = calc_cond_batch(model, conds, x, timestep, model_options) + if "sampler_calc_cond_batch_function" in model_options: + args = {"conds": conds, "input": x, "sigma": timestep, "model": model, "model_options": model_options} + out = model_options["sampler_calc_cond_batch_function"](args) + else: + out = calc_cond_batch(model, conds, x, timestep, model_options) for fn in model_options.get("sampler_pre_cfg_function", []): args = {"conds":conds, "conds_out": out, "cond_scale": cond_scale, "timestep": timestep, diff --git a/comfy_extras/nodes_slg.py b/comfy_extras/nodes_slg.py index 2fa09e250..7adff202e 100644 --- a/comfy_extras/nodes_slg.py +++ b/comfy_extras/nodes_slg.py @@ -78,7 +78,75 @@ class SkipLayerGuidanceDiT: return (m, ) +class SkipLayerGuidanceDiTSimple: + ''' + Simple version of the SkipLayerGuidanceDiT node that only modifies the uncond pass. + ''' + @classmethod + def INPUT_TYPES(s): + return {"required": {"model": ("MODEL", ), + "double_layers": ("STRING", {"default": "7, 8, 9", "multiline": False}), + "single_layers": ("STRING", {"default": "7, 8, 9", "multiline": False}), + "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + }} + RETURN_TYPES = ("MODEL",) + FUNCTION = "skip_guidance" + EXPERIMENTAL = True + + DESCRIPTION = "Simple version of the SkipLayerGuidanceDiT node that only modifies the uncond pass." + + CATEGORY = "advanced/guidance" + + def skip_guidance(self, model, start_percent, end_percent, double_layers="", single_layers=""): + def skip(args, extra_args): + return args + + model_sampling = model.get_model_object("model_sampling") + sigma_start = model_sampling.percent_to_sigma(start_percent) + sigma_end = model_sampling.percent_to_sigma(end_percent) + + double_layers = re.findall(r'\d+', double_layers) + double_layers = [int(i) for i in double_layers] + + single_layers = re.findall(r'\d+', single_layers) + single_layers = [int(i) for i in single_layers] + + if len(double_layers) == 0 and len(single_layers) == 0: + return (model, ) + + def calc_cond_batch_function(args): + x = args["input"] + model = args["model"] + conds = args["conds"] + sigma = args["sigma"] + + model_options = args["model_options"] + slg_model_options = model_options.copy() + + for layer in double_layers: + slg_model_options = comfy.model_patcher.set_model_options_patch_replace(slg_model_options, skip, "dit", "double_block", layer) + + for layer in single_layers: + slg_model_options = comfy.model_patcher.set_model_options_patch_replace(slg_model_options, skip, "dit", "single_block", layer) + + cond, uncond = conds + sigma_ = sigma[0].item() + if sigma_ >= sigma_end and sigma_ <= sigma_start and uncond is not None: + cond_out, _ = comfy.samplers.calc_cond_batch(model, [cond, None], x, sigma, model_options) + _, uncond_out = comfy.samplers.calc_cond_batch(model, [None, uncond], x, sigma, slg_model_options) + out = [cond_out, uncond_out] + else: + out = comfy.samplers.calc_cond_batch(model, conds, x, sigma, model_options) + + return out + + m = model.clone() + m.set_model_sampler_calc_cond_batch_function(calc_cond_batch_function) + + return (m, ) NODE_CLASS_MAPPINGS = { "SkipLayerGuidanceDiT": SkipLayerGuidanceDiT, + "SkipLayerGuidanceDiTSimple": SkipLayerGuidanceDiTSimple, }