Add cache no cascade property

This commit is contained in:
Alexis Rolland 2026-06-16 16:05:43 +08:00
parent d3767dae9e
commit 513d9e995f
6 changed files with 93 additions and 38 deletions

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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")]
)

View File

@ -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")],
)

View File

@ -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"