From 2e5316232e282c696fe9801eaf2b247c60f8456a Mon Sep 17 00:00:00 2001 From: Kent Mewhort Date: Wed, 4 Jun 2025 06:46:59 -0400 Subject: [PATCH] Fix IS_CHANGED for marking node as not changed --- comfy_execution/caching.py | 20 ++++++++++++++++---- tests/inference/test_execution.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/comfy_execution/caching.py b/comfy_execution/caching.py index dbb37b89f..05b57b1bb 100644 --- a/comfy_execution/caching.py +++ b/comfy_execution/caching.py @@ -93,11 +93,18 @@ class CacheKeySetInputSignature(CacheKeySet): self.subcache_keys[node_id] = (node_id, node["class_type"]) def get_node_signature(self, dynprompt, node_id): + node = dynprompt.get_node(node_id) + class_type = node["class_type"] + class_def = nodes.NODE_CLASS_MAPPINGS[class_type] + signature = [] - ancestors, order_mapping = self.get_ordered_ancestry(dynprompt, node_id) - signature.append(self.get_immediate_node_signature(dynprompt, node_id, order_mapping)) - for ancestor_id in ancestors: - signature.append(self.get_immediate_node_signature(dynprompt, ancestor_id, order_mapping)) + if hasattr(class_def, "IS_CHANGED"): + signature.append(self.get_immediate_node_signature(dynprompt, node_id, {})) + else: + ancestors, order_mapping = self.get_ordered_ancestry(dynprompt, node_id) + signature.append(self.get_immediate_node_signature(dynprompt, node_id, order_mapping)) + for ancestor_id in ancestors: + signature.append(self.get_immediate_node_signature(dynprompt, ancestor_id, order_mapping)) return to_hashable(signature) def get_immediate_node_signature(self, dynprompt, node_id, ancestor_order_mapping): @@ -108,8 +115,13 @@ class CacheKeySetInputSignature(CacheKeySet): class_type = node["class_type"] class_def = nodes.NODE_CLASS_MAPPINGS[class_type] signature = [class_type, self.is_changed_cache.get(node_id)] + 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) + + if hasattr(class_def, "IS_CHANGED"): + return signature + inputs = node["inputs"] for key in sorted(inputs.keys()): if is_link(inputs[key]): diff --git a/tests/inference/test_execution.py b/tests/inference/test_execution.py index 5cda5c1ae..a31b9e7d7 100644 --- a/tests/inference/test_execution.py +++ b/tests/inference/test_execution.py @@ -497,6 +497,36 @@ class TestExecution: assert numpy.array(images[0]).min() == 63 and numpy.array(images[0]).max() == 63, "Image should have value 0.25" assert not result.did_run(test_node), "The execution should have been cached" + # This tests that a node does not re-execute when IS_CHANGED returs the same value (even if + # inputs change) + def test_is_changed_overrides_default_checks(self, client: ComfyClient, builder: GraphBuilder): + g = builder + changing_input = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1) + test_node = g.node("TestCustomIsChanged", should_change=False, image=changing_input.out(0)) + g.node("SaveImage", images=test_node.out(0)) + + result1 = client.run(g) + assert result1.did_run(test_node), "Node should run on first execution" + assert result1.did_run(changing_input), "Input should run on first execution" + + changing_input.set_input('content', "WHITE") # Change input content + + result2 = client.run(g) + assert result2.did_run(changing_input), "Input should re-execute due to content change" + assert not result2.did_run(test_node), "Node with IS_CHANGED should NOT re-execute when IS_CHANGED is consistent" + + # verify the behavior when IS_CHANGED actually changes + test_node.set_input('should_change', True) # This should cause IS_CHANGED to return different value + + result3 = client.run(g) + assert result3.did_run(test_node), "Node should re-execute when IS_CHANGED value actually changes" + + changing_input.set_input('content', "BLACK") # Change input back + + result4 = client.run(g) + assert result4.did_run(changing_input), "Input should re-execute due to content change" + assert result4.did_run(test_node), "Node should re-execute because IS_CHANGED returns changing value (NaN)" + # This tests that nodes with OUTPUT_IS_LIST function correctly when they receive an ExecutionBlocker # as input. We also test that when that list (containing an ExecutionBlocker) is passed to a node, # only that one entry in the list is blocked.