From 513d9e995f1a6fc0a5e844115b339b74ce0844ce Mon Sep 17 00:00:00 2001 From: Alexis Rolland Date: Tue, 16 Jun 2026 16:05:43 +0800 Subject: [PATCH] Add cache no cascade property --- comfy_api/latest/_io.py | 12 +++++++++ comfy_execution/caching.py | 20 ++++++++++++--- comfy_extras/nodes_audio.py | 24 ++++++++++++------ comfy_extras/nodes_images.py | 48 ++++++++++++++++++++---------------- comfy_extras/nodes_video.py | 11 ++++++++- nodes.py | 16 ++++++++---- 6 files changed, 93 insertions(+), 38 deletions(-) diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index 012fae3ac..bc9190b6f 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -1600,6 +1600,8 @@ class Schema: """Optional client-evaluated pricing badge declaration for this node.""" not_idempotent: bool=False """Flags a node as not idempotent; when True, the node will run and not reuse the cached outputs when identical inputs are provided on a different node in the graph.""" + cache_no_cascade: bool=False + """When True, changes to this node's widget inputs re-run this node but do not invalidate the caches of downstream nodes. Use for passthrough nodes whose widget inputs only affect side effects (e.g. a save node's filename), not the data passed to its outputs.""" enable_expand: bool=False """Flags a node as expandable, allowing NodeOutput to include 'expand' property.""" accept_all_inputs: bool=False @@ -2065,6 +2067,14 @@ class _ComfyNodeBaseInternal(_ComfyNodeInternal): cls.GET_SCHEMA() return cls._NOT_IDEMPOTENT + _CACHE_NO_CASCADE = None + @final + @classproperty + def CACHE_NO_CASCADE(cls): # noqa + if cls._CACHE_NO_CASCADE is None: + cls.GET_SCHEMA() + return cls._CACHE_NO_CASCADE + _ACCEPT_ALL_INPUTS = None @final @classproperty @@ -2115,6 +2125,8 @@ class _ComfyNodeBaseInternal(_ComfyNodeInternal): cls._INPUT_IS_LIST = schema.is_input_list if cls._NOT_IDEMPOTENT is None: cls._NOT_IDEMPOTENT = schema.not_idempotent + if cls._CACHE_NO_CASCADE is None: + cls._CACHE_NO_CASCADE = schema.cache_no_cascade if cls._ACCEPT_ALL_INPUTS is None: cls._ACCEPT_ALL_INPUTS = schema.accept_all_inputs diff --git a/comfy_execution/caching.py b/comfy_execution/caching.py index ba1e8bc84..038773736 100644 --- a/comfy_execution/caching.py +++ b/comfy_execution/caching.py @@ -14,6 +14,7 @@ import nodes from comfy_execution.graph_utils import is_link NODE_CLASS_CONTAINS_UNIQUE_ID: Dict[str, bool] = {} +NODE_CLASS_CACHE_NO_CASCADE: Dict[str, bool] = {} def include_unique_id_in_input(class_type: str) -> bool: @@ -23,6 +24,16 @@ def include_unique_id_in_input(class_type: str) -> bool: NODE_CLASS_CONTAINS_UNIQUE_ID[class_type] = "UNIQUE_ID" in class_def.INPUT_TYPES().get("hidden", {}).values() return NODE_CLASS_CONTAINS_UNIQUE_ID[class_type] + +def is_cache_no_cascade(class_type: str) -> bool: + """Whether changes to this node's widget inputs should not invalidate downstream caches.""" + if class_type in NODE_CLASS_CACHE_NO_CASCADE: + return NODE_CLASS_CACHE_NO_CASCADE[class_type] + class_def = nodes.NODE_CLASS_MAPPINGS[class_type] + NODE_CLASS_CACHE_NO_CASCADE[class_type] = bool(getattr(class_def, "CACHE_NO_CASCADE", False)) + return NODE_CLASS_CACHE_NO_CASCADE[class_type] + + class CacheKeySet(ABC): def __init__(self, dynprompt, node_ids, is_changed_cache): self.keys = {} @@ -101,12 +112,12 @@ class CacheKeySetInputSignature(CacheKeySet): async def get_node_signature(self, dynprompt, node_id): signature = [] ancestors, order_mapping = self.get_ordered_ancestry(dynprompt, node_id) - signature.append(await self.get_immediate_node_signature(dynprompt, node_id, order_mapping)) + signature.append(await self.get_immediate_node_signature(dynprompt, node_id, order_mapping, as_ancestor=False)) for ancestor_id in ancestors: - signature.append(await self.get_immediate_node_signature(dynprompt, ancestor_id, order_mapping)) + signature.append(await self.get_immediate_node_signature(dynprompt, ancestor_id, order_mapping, as_ancestor=True)) return to_hashable(signature) - async def get_immediate_node_signature(self, dynprompt, node_id, ancestor_order_mapping): + async def get_immediate_node_signature(self, dynprompt, node_id, ancestor_order_mapping, as_ancestor=False): if not dynprompt.has_node(node_id): # This node doesn't exist -- we can't cache it. return [float("NaN")] @@ -117,12 +128,13 @@ class CacheKeySetInputSignature(CacheKeySet): if self.include_node_id_in_input() or (hasattr(class_def, "NOT_IDEMPOTENT") and class_def.NOT_IDEMPOTENT) or include_unique_id_in_input(class_type): signature.append(node_id) inputs = node["inputs"] + skip_widgets = as_ancestor and is_cache_no_cascade(class_type) for key in sorted(inputs.keys()): if is_link(inputs[key]): (ancestor_id, ancestor_socket) = inputs[key] ancestor_index = ancestor_order_mapping[ancestor_id] signature.append((key,("ANCESTOR", ancestor_index, ancestor_socket))) - else: + elif not skip_widgets: signature.append((key, inputs[key])) return signature diff --git a/comfy_extras/nodes_audio.py b/comfy_extras/nodes_audio.py index 91ca01a23..7816cc1e4 100644 --- a/comfy_extras/nodes_audio.py +++ b/comfy_extras/nodes_audio.py @@ -168,6 +168,7 @@ class SaveAudio(IO.ComfyNode): hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], is_deprecated=True, is_output_node=True, + cache_no_cascade=True, outputs=[IO.Audio.Output("audio")] ) @@ -198,6 +199,7 @@ class SaveAudioMP3(IO.ComfyNode): hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], is_deprecated=True, is_output_node=True, + cache_no_cascade=True, outputs=[IO.Audio.Output("audio")] ) @@ -229,6 +231,7 @@ class SaveAudioOpus(IO.ComfyNode): hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], is_deprecated=True, is_output_node=True, + cache_no_cascade=True, outputs=[IO.Audio.Output("audio")] ) @@ -258,20 +261,25 @@ class SaveAudioAdvanced(IO.ComfyNode): IO.String.Input( "filename_prefix", default="audio/ComfyUI", - tooltip=( - "The prefix for the file to save. May include formatting tokens " - "such as %date:yyyy-MM-dd%." - ), + tooltip=("The prefix for the file to save. May include formatting tokens such as %date:yyyy-MM-dd%."), ), IO.DynamicCombo.Input( "format", options=[ IO.DynamicCombo.Option("flac", []), IO.DynamicCombo.Option("mp3", [ - IO.Combo.Input("quality", options=["V0", "128k", "320k"], default="V0"), + IO.Combo.Input( + "quality", + options=["V0", "128k", "320k"], + default="V0", + ), ]), IO.DynamicCombo.Option("opus", [ - IO.Combo.Input("quality", options=["64k", "96k", "128k", "192k", "320k"], default="128k"), + IO.Combo.Input( + "quality", + options=["64k", "96k", "128k", "192k", "320k"], + default="128k", + ), ]), ], tooltip="The file format in which to save the audio.", @@ -279,6 +287,8 @@ class SaveAudioAdvanced(IO.ComfyNode): ], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], is_output_node=True, + cache_no_cascade=True, + outputs=[IO.Audio.Output("audio")], ) @classmethod @@ -289,7 +299,7 @@ class SaveAudioAdvanced(IO.ComfyNode): ui=UI.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=file_format, quality=quality) else: ui=UI.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=file_format) - return IO.NodeOutput(ui=ui) + return IO.NodeOutput(audio, ui=ui) class PreviewAudio(IO.ComfyNode): diff --git a/comfy_extras/nodes_images.py b/comfy_extras/nodes_images.py index fba48eaba..4b7e57625 100644 --- a/comfy_extras/nodes_images.py +++ b/comfy_extras/nodes_images.py @@ -214,6 +214,7 @@ class SaveAnimatedWEBP(IO.ComfyNode): ], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], is_output_node=True, + cache_no_cascade=True, outputs=[IO.Image.Output(display_name="images")] ) @@ -249,6 +250,7 @@ class SaveAnimatedPNG(IO.ComfyNode): ], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], is_output_node=True, + cache_no_cascade=True, outputs=[IO.Image.Output(display_name="images")] ) @@ -513,6 +515,7 @@ class SaveSVGNode(IO.ComfyNode): ], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], is_output_node=True, + cache_no_cascade=True, outputs=[IO.SVG.Output("svg")], ) @@ -1156,40 +1159,42 @@ class SaveImageAdvanced(IO.ComfyNode): IO.String.Input( "filename_prefix", default="ComfyUI", - tooltip=( - "The prefix for the file to save. May include formatting tokens " - "such as %date:yyyy-MM-dd% or %Empty Latent Image.width%." - ), + tooltip=("The prefix for the file to save. May include formatting tokens such as %date:yyyy-MM-dd% or %Empty Latent Image.width%."), ), IO.DynamicCombo.Input( "format", options=[ IO.DynamicCombo.Option("png", [ - IO.Combo.Input("bit_depth", options=["8-bit", "16-bit"], - default="8-bit", advanced=True), - IO.Combo.Input("input_color_space", options=["sRGB"], - default="sRGB", advanced=True), + IO.Combo.Input( + "bit_depth", + options=["8-bit", "16-bit"], + default="8-bit", + advanced=True, + ), + IO.Combo.Input( + "input_color_space", + options=["sRGB"], + default="sRGB", + advanced=True, + ), ]), IO.DynamicCombo.Option("exr", [ - IO.Combo.Input("bit_depth", options=["32-bit float"], - default="32-bit float", advanced=True), + IO.Combo.Input( + "bit_depth", + options=["32-bit float"], + default="32-bit float", + advanced=True, + ), IO.Combo.Input( "input_color_space", options=["sRGB", "HDR", "linear"], default="sRGB", advanced=True, tooltip=( - "Colorspace of the input tensor. The EXR is " - "always written as scene-linear in the matching " - "gamut.\n" - " 'sRGB' — input is sRGB-encoded Rec.709; " - "the inverse sRGB EOTF is applied.\n" - " 'HDR' — input is HLG-encoded Rec.2020 " - "(BT.2100); the inverse HLG OETF is applied " - "to get scene-linear light.\n" - " 'linear' — input is already scene-linear " - "(Rec.709 primaries); written through unchanged. " - "Use this for renderer/compositor output." + "Colorspace of the input tensor. The EXR is always written as scene-linear in the matching gamut.\n" + "sRGB — input is sRGB-encoded Rec.709; the inverse sRGB EOTF is applied.\n" + "HDR — input is HLG-encoded Rec.2020 (BT.2100); the inverse HLG OETF is applied to get scene-linear light.\n" + "linear — input is already scene-linear (Rec.709 primaries); written through unchanged. Use this for renderer/compositor output." ), ), ]), @@ -1199,6 +1204,7 @@ class SaveImageAdvanced(IO.ComfyNode): ], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], is_output_node=True, + cache_no_cascade=True, outputs=[IO.Image.Output(display_name="images")] ) diff --git a/comfy_extras/nodes_video.py b/comfy_extras/nodes_video.py index 514edea20..0113bac55 100644 --- a/comfy_extras/nodes_video.py +++ b/comfy_extras/nodes_video.py @@ -23,10 +23,18 @@ class SaveWEBM(io.ComfyNode): io.String.Input("filename_prefix", default="ComfyUI"), io.Combo.Input("codec", options=["vp9", "av1"]), io.Float.Input("fps", default=24.0, min=0.01, max=1000.0, step=0.01), - io.Float.Input("crf", default=32.0, min=0, max=63.0, step=1, tooltip="Higher crf means lower quality with a smaller file size, lower crf means higher quality higher filesize."), + io.Float.Input( + "crf", + default=32.0, + min=0, + max=63.0, + step=1, + tooltip="Higher crf means lower quality with a smaller file size, lower crf means higher quality higher filesize.", + ), ], hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo], is_output_node=True, + cache_no_cascade=True, outputs=[io.Image.Output(display_name="images")] ) @@ -90,6 +98,7 @@ class SaveVideo(io.ComfyNode): ], hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo], is_output_node=True, + cache_no_cascade=True, outputs=[io.Video.Output("video")], ) diff --git a/nodes.py b/nodes.py index c36aae17d..5025e8e58 100644 --- a/nodes.py +++ b/nodes.py @@ -483,15 +483,17 @@ class SaveLatent: @classmethod def INPUT_TYPES(s): - return {"required": { "samples": ("LATENT", ), - "filename_prefix": ("STRING", {"default": "latents/ComfyUI"})}, - "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, - } + return { "required": { + "samples": ("LATENT",), + "filename_prefix": ("STRING", {"default": "latents/ComfyUI"})}, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } RETURN_TYPES = ("LATENT",) RETURN_NAMES = ("samples",) FUNCTION = "save" OUTPUT_NODE = True + CACHE_NO_CASCADE = True CATEGORY = "experimental" @@ -1632,7 +1634,10 @@ class SaveImage: return { "required": { "images": ("IMAGE", {"tooltip": "The images to save."}), - "filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."}) + "filename_prefix": ("STRING", { + "default": "ComfyUI", + "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes." + }) }, "hidden": { "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO" @@ -1644,6 +1649,7 @@ class SaveImage: FUNCTION = "save_images" OUTPUT_NODE = True + CACHE_NO_CASCADE = True CATEGORY = "image" ESSENTIALS_CATEGORY = "Basics"