mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-08-14 21:54:46 +08:00
feat(Core): support partial graph execution
This commit is contained in:
@@ -5,6 +5,7 @@ from .conditions import CONDITION_NODE_CLASS_MAPPINGS, CONDITION_NODE_DISPLAY_NA
|
||||
from .stubs import TEST_STUB_NODE_CLASS_MAPPINGS, TEST_STUB_NODE_DISPLAY_NAME_MAPPINGS
|
||||
from .async_test_nodes import ASYNC_TEST_NODE_CLASS_MAPPINGS, ASYNC_TEST_NODE_DISPLAY_NAME_MAPPINGS
|
||||
from .api_test_nodes import API_TEST_NODE_CLASS_MAPPINGS, API_TEST_NODE_DISPLAY_NAME_MAPPINGS
|
||||
from .cache_provider_test_nodes import CACHE_PROVIDER_TEST_NODE_CLASS_MAPPINGS, CACHE_PROVIDER_TEST_NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
# NODE_CLASS_MAPPINGS = GENERAL_NODE_CLASS_MAPPINGS.update(COMPONENT_NODE_CLASS_MAPPINGS)
|
||||
# NODE_DISPLAY_NAME_MAPPINGS = GENERAL_NODE_DISPLAY_NAME_MAPPINGS.update(COMPONENT_NODE_DISPLAY_NAME_MAPPINGS)
|
||||
@@ -17,6 +18,7 @@ NODE_CLASS_MAPPINGS.update(CONDITION_NODE_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(TEST_STUB_NODE_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(ASYNC_TEST_NODE_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(API_TEST_NODE_CLASS_MAPPINGS)
|
||||
NODE_CLASS_MAPPINGS.update(CACHE_PROVIDER_TEST_NODE_CLASS_MAPPINGS)
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(TEST_NODE_DISPLAY_NAME_MAPPINGS)
|
||||
@@ -26,3 +28,4 @@ NODE_DISPLAY_NAME_MAPPINGS.update(CONDITION_NODE_DISPLAY_NAME_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(TEST_STUB_NODE_DISPLAY_NAME_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(ASYNC_TEST_NODE_DISPLAY_NAME_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(API_TEST_NODE_DISPLAY_NAME_MAPPINGS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(CACHE_PROVIDER_TEST_NODE_DISPLAY_NAME_MAPPINGS)
|
||||
|
||||
@@ -135,6 +135,148 @@ class TestSyncError(ComfyNodeABC):
|
||||
raise RuntimeError("Intentional sync execution error for testing")
|
||||
|
||||
|
||||
class TestOOMError(ComfyNodeABC):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"value": (IO.ANY, {})}}
|
||||
|
||||
RETURN_TYPES = (IO.ANY,)
|
||||
FUNCTION = "oom_error"
|
||||
CATEGORY = "experimental/async"
|
||||
|
||||
def oom_error(self, value):
|
||||
raise torch.cuda.OutOfMemoryError("Intentional out of memory error for testing")
|
||||
|
||||
|
||||
class TestMixedExpansionFailure(ComfyNodeABC):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"value": ("INT", {})}}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "expand"
|
||||
CATEGORY = "experimental/async"
|
||||
|
||||
def expand(self, value):
|
||||
image = torch.zeros([1, 32, 32, 3])
|
||||
if value == 0:
|
||||
return (image,)
|
||||
|
||||
graph = GraphBuilder()
|
||||
error = graph.node("TestSyncError", value=image)
|
||||
return {
|
||||
"result": (error.out(0),),
|
||||
"expand": graph.finalize(),
|
||||
}
|
||||
|
||||
|
||||
class TestMalformedExpansion(ComfyNodeABC):
|
||||
"""Expands to a graph referencing a missing node class, so the failure
|
||||
happens in the executor after the node function has returned."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"value": (IO.ANY, {})}}
|
||||
|
||||
RETURN_TYPES = (IO.ANY,)
|
||||
FUNCTION = "expand"
|
||||
CATEGORY = "experimental/async"
|
||||
|
||||
def expand(self, value):
|
||||
graph = GraphBuilder()
|
||||
missing = graph.node("TestNodeClassThatDoesNotExist", value=value)
|
||||
return {
|
||||
"result": (missing.out(0),),
|
||||
"expand": graph.finalize(),
|
||||
}
|
||||
|
||||
|
||||
class TestMalformedResult(ComfyNodeABC):
|
||||
"""Returns a non-tuple result so the failure happens while the executor
|
||||
merges results, after the node function has returned."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"value": (IO.ANY, {})}}
|
||||
|
||||
RETURN_TYPES = (IO.ANY,)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "experimental/async"
|
||||
|
||||
def run(self, value):
|
||||
return 5
|
||||
|
||||
|
||||
class TestCyclicExpansion(ComfyNodeABC):
|
||||
"""Expands to an output node that consumes this node's own pending output,
|
||||
forming a cycle through the expansion completion link."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {"value": (IO.ANY, {})},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = (IO.ANY,)
|
||||
FUNCTION = "expand"
|
||||
CATEGORY = "experimental/async"
|
||||
|
||||
def expand(self, value, unique_id):
|
||||
graph = GraphBuilder()
|
||||
graph.node("TestAsyncOutput", value=[unique_id, 0], seconds=0.0)
|
||||
return {
|
||||
"result": (value,),
|
||||
"expand": graph.finalize(),
|
||||
}
|
||||
|
||||
|
||||
class TestExpansionWithFailingOutput(ComfyNodeABC):
|
||||
"""Expands to a subgraph whose result succeeds while a side branch ending
|
||||
in an output node fails."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"image": (IO.IMAGE, {})}}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "expand"
|
||||
CATEGORY = "experimental/async"
|
||||
|
||||
def expand(self, image):
|
||||
graph = GraphBuilder()
|
||||
error = graph.node("TestSyncError", value=image)
|
||||
graph.node("PreviewImage", images=error.out(0))
|
||||
passthrough = graph.node("StubImage", content="WHITE", height=32, width=32, batch_size=1)
|
||||
return {
|
||||
"result": (passthrough.out(0),),
|
||||
"expand": graph.finalize(),
|
||||
}
|
||||
|
||||
|
||||
class TestAsyncOutput(ComfyNodeABC):
|
||||
"""Async output node with no return sockets, used to test partial failure
|
||||
handling across pending async invocations."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"value": (IO.ANY, {}),
|
||||
"seconds": (IO.FLOAT, {"default": 0.1}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "experimental/async"
|
||||
|
||||
async def run(self, value, seconds=0.1):
|
||||
await asyncio.sleep(seconds)
|
||||
return {"ui": {"values": [1]}}
|
||||
|
||||
|
||||
class TestAsyncLazyCheck(ComfyNodeABC):
|
||||
"""Test node with async check_lazy_status."""
|
||||
|
||||
@@ -322,6 +464,13 @@ ASYNC_TEST_NODE_CLASS_MAPPINGS = {
|
||||
"TestAsyncValidationError": TestAsyncValidationError,
|
||||
"TestAsyncTimeout": TestAsyncTimeout,
|
||||
"TestSyncError": TestSyncError,
|
||||
"TestOOMError": TestOOMError,
|
||||
"TestMixedExpansionFailure": TestMixedExpansionFailure,
|
||||
"TestMalformedExpansion": TestMalformedExpansion,
|
||||
"TestMalformedResult": TestMalformedResult,
|
||||
"TestCyclicExpansion": TestCyclicExpansion,
|
||||
"TestExpansionWithFailingOutput": TestExpansionWithFailingOutput,
|
||||
"TestAsyncOutput": TestAsyncOutput,
|
||||
"TestAsyncLazyCheck": TestAsyncLazyCheck,
|
||||
"TestDynamicAsyncGeneration": TestDynamicAsyncGeneration,
|
||||
"TestAsyncResourceUser": TestAsyncResourceUser,
|
||||
@@ -335,6 +484,13 @@ ASYNC_TEST_NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"TestAsyncValidationError": "Test Async Validation Error",
|
||||
"TestAsyncTimeout": "Test Async Timeout",
|
||||
"TestSyncError": "Test Sync Error",
|
||||
"TestOOMError": "Test OOM Error",
|
||||
"TestMixedExpansionFailure": "Test Mixed Expansion Failure",
|
||||
"TestMalformedExpansion": "Test Malformed Expansion",
|
||||
"TestMalformedResult": "Test Malformed Result",
|
||||
"TestCyclicExpansion": "Test Cyclic Expansion",
|
||||
"TestExpansionWithFailingOutput": "Test Expansion With Failing Output",
|
||||
"TestAsyncOutput": "Test Async Output",
|
||||
"TestAsyncLazyCheck": "Test Async Lazy Check",
|
||||
"TestDynamicAsyncGeneration": "Test Dynamic Async Generation",
|
||||
"TestAsyncResourceUser": "Test Async Resource User",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
from comfy.comfy_types.node_typing import ComfyNodeABC
|
||||
from comfy_api.latest._caching import CacheProvider
|
||||
from comfy_execution.cache_provider import register_cache_provider
|
||||
|
||||
|
||||
class _RecordingCacheProvider(CacheProvider):
|
||||
"""Records the class types of every externally stored cache entry so tests
|
||||
can assert that failed or failure-blocked outputs never leave the process."""
|
||||
|
||||
def __init__(self):
|
||||
self.stored_class_types = []
|
||||
|
||||
async def on_lookup(self, context):
|
||||
return None
|
||||
|
||||
async def on_store(self, context, value):
|
||||
self.stored_class_types.append(context.class_type)
|
||||
|
||||
|
||||
RECORDING_CACHE_PROVIDER = _RecordingCacheProvider()
|
||||
register_cache_provider(RECORDING_CACHE_PROVIDER)
|
||||
|
||||
|
||||
class TestCacheProviderRecord(ComfyNodeABC):
|
||||
"""Reports which node class types have been stored through the external
|
||||
cache provider interface since the server started."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {}}
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls):
|
||||
return float("NaN")
|
||||
|
||||
RETURN_TYPES = ()
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "report"
|
||||
CATEGORY = "Testing/Nodes"
|
||||
|
||||
def report(self):
|
||||
return {"ui": {"stored_class_types": list(RECORDING_CACHE_PROVIDER.stored_class_types)}}
|
||||
|
||||
|
||||
CACHE_PROVIDER_TEST_NODE_CLASS_MAPPINGS = {
|
||||
"TestCacheProviderRecord": TestCacheProviderRecord,
|
||||
}
|
||||
|
||||
CACHE_PROVIDER_TEST_NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"TestCacheProviderRecord": "Test Cache Provider Record",
|
||||
}
|
||||
Reference in New Issue
Block a user