mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-18 20:38:15 +08:00
263 lines
9.1 KiB
Python
263 lines
9.1 KiB
Python
import asyncio
|
|
from types import SimpleNamespace
|
|
|
|
import torch
|
|
|
|
import nodes
|
|
from comfy_execution.caching import BasicCache, CacheKeySetID
|
|
from comfy_execution.graph import DynamicPrompt
|
|
from comfy_execution.progress import WebUIProgressHandler, get_progress_state, reset_progress_state
|
|
from comfy_execution.utils import get_current_client_id, has_current_client_id, reset_current_client_id, set_current_client_id
|
|
from execution import CacheSet, PromptExecutor, _send_cached_ui
|
|
|
|
|
|
class Server:
|
|
def __init__(self):
|
|
self.client_id = "shared-server-value"
|
|
self.messages = []
|
|
self.last_node_id = None
|
|
|
|
def send_sync(self, event, data, client_id):
|
|
self.messages.append((event, data, client_id))
|
|
|
|
|
|
def test_prompt_executor_messages_use_executor_client_id():
|
|
server = Server()
|
|
first = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0})
|
|
second = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0})
|
|
first.client_id = "first-client"
|
|
second.client_id = "second-client"
|
|
|
|
first.add_message("event", {"prompt_id": "first"}, broadcast=False)
|
|
second.add_message("event", {"prompt_id": "second"}, broadcast=False)
|
|
|
|
assert [message[2] for message in server.messages] == ["first-client", "second-client"]
|
|
assert server.client_id == "shared-server-value"
|
|
|
|
|
|
def test_cached_ui_is_recorded_without_a_connected_client():
|
|
server = Server()
|
|
ui_outputs = {}
|
|
cached = SimpleNamespace(ui={"output": {"images": []}, "meta": {"node_id": "node"}})
|
|
|
|
_send_cached_ui(server, None, "node", "node", cached, "prompt", ui_outputs)
|
|
|
|
assert ui_outputs == {"node": cached.ui}
|
|
assert server.messages == []
|
|
|
|
|
|
def test_progress_registry_is_isolated_between_async_prompt_tasks():
|
|
async def prompt(prompt_id, ready, release):
|
|
reset_progress_state(prompt_id, DynamicPrompt({}))
|
|
ready.set()
|
|
await release.wait()
|
|
return get_progress_state().prompt_id
|
|
|
|
async def run():
|
|
first_ready = asyncio.Event()
|
|
second_ready = asyncio.Event()
|
|
release = asyncio.Event()
|
|
first = asyncio.create_task(prompt("first", first_ready, release))
|
|
second = asyncio.create_task(prompt("second", second_ready, release))
|
|
await asyncio.gather(first_ready.wait(), second_ready.wait())
|
|
release.set()
|
|
return await asyncio.gather(first, second)
|
|
|
|
assert asyncio.run(run()) == ["first", "second"]
|
|
|
|
|
|
def test_webui_progress_handler_uses_prompt_client_id():
|
|
server = Server()
|
|
first = WebUIProgressHandler(server, "first-client")
|
|
second = WebUIProgressHandler(server, "second-client")
|
|
|
|
first._send_progress_state("first", {})
|
|
second._send_progress_state("second", {})
|
|
|
|
assert [message[2] for message in server.messages] == ["first-client", "second-client"]
|
|
|
|
|
|
def test_explicit_anonymous_client_does_not_fall_back_to_stale_server_client():
|
|
server = Server()
|
|
handler = WebUIProgressHandler(server, None)
|
|
handler._send_progress_state("anonymous", {})
|
|
assert server.messages[0][2] is None
|
|
|
|
assert not has_current_client_id()
|
|
token = set_current_client_id(None)
|
|
try:
|
|
assert has_current_client_id()
|
|
assert get_current_client_id() is None
|
|
finally:
|
|
reset_current_client_id(token)
|
|
|
|
|
|
def test_shared_cache_uses_task_local_prompt_signatures():
|
|
async def run():
|
|
cache = BasicCache(CacheKeySetID)
|
|
both_ready = asyncio.Event()
|
|
ready_count = 0
|
|
|
|
async def prompt(class_type, value):
|
|
nonlocal ready_count
|
|
dynprompt = DynamicPrompt({"same-id": {"class_type": class_type, "inputs": {}}})
|
|
await cache.set_prompt(dynprompt, ["same-id"], None)
|
|
cache.set_local("same-id", value)
|
|
ready_count += 1
|
|
if ready_count == 2:
|
|
both_ready.set()
|
|
await both_ready.wait()
|
|
result = cache.get_local("same-id")
|
|
cache.release_prompt()
|
|
return result
|
|
|
|
return await asyncio.gather(prompt("First", "first"), prompt("Second", "second"))
|
|
|
|
assert asyncio.run(run()) == ["first", "second"]
|
|
|
|
|
|
def test_cooperative_executors_do_not_share_stateful_node_instances():
|
|
instances = []
|
|
both_ready = None
|
|
ready = 0
|
|
|
|
class StatefulProbe:
|
|
def __init__(self):
|
|
instances.append(self)
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {}}
|
|
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "run"
|
|
|
|
async def run(self):
|
|
nonlocal ready
|
|
ready += 1
|
|
if ready == 2:
|
|
both_ready.set()
|
|
await both_ready.wait()
|
|
return ()
|
|
|
|
async def run():
|
|
nonlocal both_ready
|
|
both_ready = asyncio.Event()
|
|
server = Server()
|
|
server.prompt_queue = SimpleNamespace(cooperative=True, is_cancelled=lambda prompt_id: False)
|
|
cache_args = {"lru": 0, "ram": 0, "ram_inactive": 0}
|
|
shared_outputs = CacheSet(cache_args=cache_args).outputs
|
|
prompt = {"same-id": {"class_type": "StatefulInterleaveProbe", "inputs": {}}}
|
|
first = PromptExecutor(server, cache_args=cache_args, shared_outputs=shared_outputs)
|
|
second = PromptExecutor(server, cache_args=cache_args, shared_outputs=shared_outputs)
|
|
with torch.inference_mode():
|
|
await asyncio.gather(
|
|
first.execute_async(prompt, "first", {}, ["same-id"]),
|
|
second.execute_async(prompt, "second", {}, ["same-id"]),
|
|
)
|
|
|
|
nodes.NODE_CLASS_MAPPINGS["StatefulInterleaveProbe"] = StatefulProbe
|
|
try:
|
|
asyncio.run(run())
|
|
finally:
|
|
del nodes.NODE_CLASS_MAPPINGS["StatefulInterleaveProbe"]
|
|
assert len(instances) == 2
|
|
assert instances[0] is not instances[1]
|
|
|
|
|
|
def test_concurrent_cooperative_executors_do_not_clear_global_interrupt(monkeypatch):
|
|
interrupt_calls = []
|
|
monkeypatch.setattr(nodes, "interrupt_processing", lambda value=True: interrupt_calls.append(value))
|
|
|
|
async def run():
|
|
server = Server()
|
|
server.prompt_queue = SimpleNamespace(cooperative=True, is_cancelled=lambda prompt_id: False)
|
|
first = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0})
|
|
second = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0})
|
|
with torch.inference_mode():
|
|
await asyncio.gather(
|
|
first.execute_async({}, "first", {}, []),
|
|
second.execute_async({}, "second", {}, []),
|
|
)
|
|
|
|
asyncio.run(run())
|
|
assert interrupt_calls == []
|
|
|
|
|
|
def test_inference_mode_stays_enabled_across_interleaved_cooperative_tasks():
|
|
observations = []
|
|
both_ready = None
|
|
ready = 0
|
|
|
|
class Probe:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {}}
|
|
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "run"
|
|
|
|
async def run(self):
|
|
nonlocal ready
|
|
observations.append(torch.is_inference_mode_enabled())
|
|
ready += 1
|
|
if ready == 2:
|
|
both_ready.set()
|
|
await both_ready.wait()
|
|
observations.append(torch.is_inference_mode_enabled())
|
|
return ()
|
|
|
|
async def run():
|
|
nonlocal both_ready
|
|
both_ready = asyncio.Event()
|
|
server = Server()
|
|
server.prompt_queue = SimpleNamespace(cooperative=True, is_cancelled=lambda prompt_id: False)
|
|
prompt = {"probe": {"class_type": "InferenceModeInterleaveProbe", "inputs": {}}}
|
|
first = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0})
|
|
second = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0})
|
|
with torch.inference_mode():
|
|
await asyncio.gather(
|
|
first.execute_async(prompt, "first", {}, ["probe"]),
|
|
second.execute_async(prompt, "second", {}, ["probe"]),
|
|
)
|
|
assert torch.is_inference_mode_enabled()
|
|
|
|
nodes.NODE_CLASS_MAPPINGS["InferenceModeInterleaveProbe"] = Probe
|
|
try:
|
|
asyncio.run(run())
|
|
finally:
|
|
del nodes.NODE_CLASS_MAPPINGS["InferenceModeInterleaveProbe"]
|
|
assert observations == [True, True, True, True]
|
|
assert not torch.is_inference_mode_enabled()
|
|
|
|
|
|
def test_serial_prompt_executor_still_owns_inference_mode():
|
|
observations = []
|
|
|
|
class Probe:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {}}
|
|
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "run"
|
|
|
|
def run(self):
|
|
observations.append(torch.is_inference_mode_enabled())
|
|
return ()
|
|
|
|
async def run():
|
|
server = Server()
|
|
server.prompt_queue = SimpleNamespace(cooperative=False)
|
|
prompt = {"probe": {"class_type": "InferenceModeSerialProbe", "inputs": {}}}
|
|
executor = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0})
|
|
await executor.execute_async(prompt, "serial", {}, ["probe"])
|
|
|
|
nodes.NODE_CLASS_MAPPINGS["InferenceModeSerialProbe"] = Probe
|
|
try:
|
|
asyncio.run(run())
|
|
finally:
|
|
del nodes.NODE_CLASS_MAPPINGS["InferenceModeSerialProbe"]
|
|
assert observations == [True]
|
|
assert not torch.is_inference_mode_enabled()
|