Files
ttulttul-ComfyUI-Modal/remote/modal_app.py
T

3443 lines
135 KiB
Python

"""Remote Modal runtime and local execution fallback."""
import asyncio
import copy
from dataclasses import dataclass, field
import importlib
import importlib.util
from io import BytesIO
import logging
import os
import queue
import select
import shutil
import socket
import subprocess
import sys
import tempfile
import threading
import time
import zipfile
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
from contextlib import contextmanager, nullcontext
from pathlib import Path
from typing import Any, Callable, Iterator
from ..serialization import (
join_mapped_values,
coerce_serialized_node_outputs,
deserialize_value,
deserialize_node_inputs,
deserialize_node_outputs,
split_mapped_value,
serialize_node_outputs,
serialize_node_inputs,
)
from ..session_state import (
InMemoryRemoteSessionStore,
RemoteSessionHandle,
RemoteSessionStateError,
is_remote_session_handle_payload,
is_remote_session_value_ref_payload,
)
from ..settings import get_settings
logger = logging.getLogger(__name__)
_MODAL_CLOUD_MODULE_NAME = "comfyui_modal_sync_cloud"
_MODAL_AUTO_DEPLOY_LOCK = threading.Lock()
_MODAL_AUTO_DEPLOYED_APPS: set[tuple[str, str | None]] = set()
_MODAL_INTERRUPT_DICTS_LOCK = threading.Lock()
_MODAL_INTERRUPT_DICTS: dict[tuple[str, str | None], Any] = {}
_MAPPED_PROGRESS_NODE_IDS_LOCK = threading.Lock()
_MAPPED_PROGRESS_NODE_IDS: dict[tuple[str, str, str], str] = {}
_PROMPT_WARMUP_STATES_LOCK = threading.Lock()
_PROMPT_WARMUP_STATES: dict[str, "_PromptWarmupState"] = {}
_PROMPT_WARMUP_STATE_ORDER: queue.SimpleQueue[str] | None = None
_PROMPT_WARMUP_STATE_CACHE_LIMIT = 256
_PRIMITIVE_WIDGET_INPUT_TYPES = frozenset({"INT", "FLOAT", "BOOLEAN", "STRING"})
_REMOTE_CONTAINER_LOG_STREAMS_LOCK = threading.Lock()
_REMOTE_CONTAINER_LOG_STREAMS: dict[str, "_RemoteContainerLogStreamState"] = {}
_REMOTE_CONTAINER_LOG_STDERR_LOCK = threading.Lock()
_REMOTE_SESSION_STORE = InMemoryRemoteSessionStore()
@dataclass
class _PromptWarmupState:
"""Track proactive warmup state for one local prompt."""
scheduled_slots: set[int] = field(default_factory=set)
exact_component_parallelism: dict[str, int] = field(default_factory=dict)
@dataclass
class _RemoteContainerLogLineBuffer:
"""Buffer partial remote log lines so stderr mirroring stays line-oriented."""
task_id: str
buffered_text: str = ""
@dataclass
class _RemoteContainerLogStreamState:
"""Track one active local watcher mirroring logs for a Modal container."""
task_id: str
stop_event: threading.Event
thread: threading.Thread
refcount: int = 0
def _remote_modal_call_worker_count() -> int:
"""Return the number of local worker threads reserved for blocking Modal calls."""
configured_parallelism = get_settings().max_containers or 0
return max(4, os.cpu_count() or 1, configured_parallelism)
_REMOTE_MODAL_CALL_EXECUTOR = ThreadPoolExecutor(max_workers=_remote_modal_call_worker_count())
_REMOTE_MODAL_WARMUP_EXECUTOR = ThreadPoolExecutor(max_workers=_remote_modal_call_worker_count())
try:
import modal # type: ignore
except ModuleNotFoundError: # pragma: no cover - exercised by local fallback tests.
modal = None
class RemoteSubgraphExecutionError(RuntimeError):
"""Raised when remote subgraph execution fails."""
class ModalRemoteInvocationError(RuntimeError):
"""Raised when the Modal client cannot invoke the remote runtime."""
def _is_remote_container_log_stream_enabled() -> bool:
"""Return whether remote Modal container logs should be mirrored locally."""
return bool(get_settings().stream_remote_container_logs)
def _coerce_modal_task_id(value: Any) -> str | None:
"""Normalize one streamed Modal task id into a non-empty string."""
if value is None:
return None
task_id = str(value).strip()
return task_id or None
def _write_remote_container_log_line(
task_id: str,
line: str,
*,
stream: Any = None,
) -> None:
"""Write one complete remote container log line to the local stderr stream."""
target_stream = stream if stream is not None else sys.stderr
with _REMOTE_CONTAINER_LOG_STDERR_LOCK:
target_stream.write(f"[modal:{task_id}] {line}")
target_stream.flush()
def _write_remote_container_log_chunk(
line_buffer: _RemoteContainerLogLineBuffer,
chunk: str,
*,
stream: Any = None,
) -> None:
"""Buffer one remote log chunk and mirror complete lines to local stderr."""
line_buffer.buffered_text += chunk
while True:
newline_index = line_buffer.buffered_text.find("\n")
if newline_index < 0:
return
next_line = line_buffer.buffered_text[: newline_index + 1]
line_buffer.buffered_text = line_buffer.buffered_text[newline_index + 1 :]
_write_remote_container_log_line(line_buffer.task_id, next_line, stream=stream)
def _flush_remote_container_log_chunk(
line_buffer: _RemoteContainerLogLineBuffer,
*,
stream: Any = None,
) -> None:
"""Flush any partial remote log text remaining in the local line buffer."""
if not line_buffer.buffered_text:
return
_write_remote_container_log_line(
line_buffer.task_id,
f"{line_buffer.buffered_text}\n",
stream=stream,
)
line_buffer.buffered_text = ""
async def _close_modal_client(client: Any) -> None:
"""Close one Modal SDK client if the concrete client exposes a close hook."""
close_callable = getattr(client, "aclose", None)
if callable(close_callable):
close_result = close_callable()
if asyncio.iscoroutine(close_result):
await close_result
return
close_callable = getattr(client, "close", None)
if callable(close_callable):
close_result = close_callable()
if asyncio.iscoroutine(close_result):
await close_result
async def _stream_remote_container_logs_via_modal_sdk_async(
task_id: str,
stop_event: threading.Event,
) -> None:
"""Follow one Modal container log stream through the Python SDK internals."""
if modal is None:
raise ModuleNotFoundError("Modal SDK is unavailable.")
client_module = importlib.import_module("modal.client")
exception_module = importlib.import_module("modal.exception")
api_pb2 = importlib.import_module("modal_proto.api_pb2")
grpclib_exceptions = importlib.import_module("grpclib.exceptions")
client = await client_module._Client.from_env()
line_buffer = _RemoteContainerLogLineBuffer(task_id=task_id)
last_entry_id = ""
try:
while not stop_event.is_set():
request = api_pb2.AppGetLogsRequest(
task_id=task_id,
timeout=5,
last_entry_id=last_entry_id,
)
try:
async for log_batch in client.stub.AppGetLogs.unary_stream(request):
if stop_event.is_set():
break
if log_batch.entry_id:
last_entry_id = str(log_batch.entry_id)
if bool(log_batch.app_done):
logger.info("Modal SDK log stream finished for task_id=%s.", task_id)
return
for log_item in log_batch.items:
log_data = getattr(log_item, "data", "")
if not log_data:
continue
_write_remote_container_log_chunk(line_buffer, str(log_data))
except (
exception_module.ServiceError,
exception_module.InternalError,
grpclib_exceptions.StreamTerminatedError,
socket.gaierror,
) as exc:
if stop_event.is_set():
break
logger.warning(
"Retrying Modal SDK log stream for task_id=%s after transient failure: %s",
task_id,
exc,
)
continue
except AttributeError as exc:
if stop_event.is_set():
break
if "_write_appdata" in str(exc):
logger.warning(
"Retrying Modal SDK log stream for task_id=%s after connection loss: %s",
task_id,
exc,
)
continue
raise
finally:
_flush_remote_container_log_chunk(line_buffer)
await _close_modal_client(client)
def _stream_remote_container_logs_via_modal_sdk(
task_id: str,
stop_event: threading.Event,
) -> bool:
"""Try to mirror one Modal container log stream through the installed SDK."""
if modal is None:
return False
asyncio.run(_stream_remote_container_logs_via_modal_sdk_async(task_id, stop_event))
return True
def _stream_remote_container_logs_via_modal_cli(
task_id: str,
stop_event: threading.Event,
) -> bool:
"""Try to mirror one Modal container log stream through the Modal CLI."""
modal_cli = shutil.which("modal")
if modal_cli is None:
return False
command = [modal_cli, "container", "logs", task_id, "-f"]
line_buffer = _RemoteContainerLogLineBuffer(task_id=task_id)
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0,
)
line_buffer = _RemoteContainerLogLineBuffer(task_id=task_id)
try:
stdout_stream = process.stdout
if stdout_stream is None:
raise RuntimeError("Modal CLI log process did not expose a stdout stream.")
while not stop_event.is_set():
ready_streams, _, _ = select.select([stdout_stream], [], [], 0.25)
if ready_streams:
next_chunk = stdout_stream.read(4096)
if next_chunk:
_write_remote_container_log_chunk(
line_buffer,
next_chunk.decode("utf-8", errors="replace"),
)
continue
if process.poll() is not None:
trailing_chunk = stdout_stream.read()
if trailing_chunk:
_write_remote_container_log_chunk(
line_buffer,
trailing_chunk.decode("utf-8", errors="replace"),
)
break
if stop_event.is_set() and process.poll() is None:
process.terminate()
finally:
if process.poll() is None:
try:
process.wait(timeout=2.0)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=2.0)
_flush_remote_container_log_chunk(line_buffer)
if process.returncode not in {0, None} and not stop_event.is_set():
raise RuntimeError(
f"Modal CLI exited with status {process.returncode} while streaming logs for task_id={task_id}."
)
return True
def _run_remote_container_log_stream(task_id: str, stop_event: threading.Event) -> None:
"""Run the best available remote log streaming backend for one Modal container."""
logger.info("Starting remote Modal container log stream for task_id=%s.", task_id)
try:
if _stream_remote_container_logs_via_modal_cli(task_id, stop_event):
logger.info("Stopped remote Modal container log stream for task_id=%s.", task_id)
return
except (OSError, RuntimeError, subprocess.SubprocessError, ValueError) as exc:
logger.warning(
"Falling back from Modal CLI log streaming for task_id=%s after failure: %s",
task_id,
exc,
)
try:
if _stream_remote_container_logs_via_modal_sdk(task_id, stop_event):
logger.info("Stopped remote Modal container log stream for task_id=%s.", task_id)
return
except (AttributeError, ImportError, ModuleNotFoundError, OSError, RuntimeError, ValueError) as exc:
logger.warning(
"Modal SDK log streaming failed for task_id=%s: %s",
task_id,
exc,
)
logger.warning(
"Unable to mirror remote Modal container logs for task_id=%s because neither the Modal SDK nor CLI is available locally.",
task_id,
)
def _retain_remote_container_log_stream(task_id: str) -> str:
"""Increment one shared Modal container log watcher and start it if needed."""
with _REMOTE_CONTAINER_LOG_STREAMS_LOCK:
stream_state = _REMOTE_CONTAINER_LOG_STREAMS.get(task_id)
if stream_state is None:
stop_event = threading.Event()
stream_state = _RemoteContainerLogStreamState(
task_id=task_id,
stop_event=stop_event,
thread=threading.Thread(
target=_run_remote_container_log_stream,
args=(task_id, stop_event),
name=f"modal-log-stream-{task_id}",
daemon=True,
),
)
_REMOTE_CONTAINER_LOG_STREAMS[task_id] = stream_state
logger.info("Creating remote Modal container log stream for task_id=%s.", task_id)
stream_state.thread.start()
elif not stream_state.thread.is_alive() and stream_state.refcount == 0:
stop_event = threading.Event()
stream_state = _RemoteContainerLogStreamState(
task_id=task_id,
stop_event=stop_event,
thread=threading.Thread(
target=_run_remote_container_log_stream,
args=(task_id, stop_event),
name=f"modal-log-stream-{task_id}",
daemon=True,
),
)
_REMOTE_CONTAINER_LOG_STREAMS[task_id] = stream_state
logger.info("Restarting remote Modal container log stream for task_id=%s.", task_id)
stream_state.thread.start()
else:
logger.info("Reusing remote Modal container log stream for task_id=%s.", task_id)
stream_state.refcount += 1
logger.info(
"Remote Modal container log stream retain task_id=%s refcount=%d.",
task_id,
stream_state.refcount,
)
return task_id
def _release_remote_container_log_stream(task_id: str) -> None:
"""Release one retained Modal container log watcher when a payload finishes."""
stream_state: _RemoteContainerLogStreamState | None = None
should_stop = False
with _REMOTE_CONTAINER_LOG_STREAMS_LOCK:
stream_state = _REMOTE_CONTAINER_LOG_STREAMS.get(task_id)
if stream_state is None:
return
stream_state.refcount = max(0, stream_state.refcount - 1)
logger.info(
"Remote Modal container log stream release task_id=%s refcount=%d.",
task_id,
stream_state.refcount,
)
if stream_state.refcount == 0:
should_stop = True
_REMOTE_CONTAINER_LOG_STREAMS.pop(task_id, None)
if should_stop and stream_state is not None:
logger.info("Stopping remote Modal container log stream for task_id=%s.", task_id)
stream_state.stop_event.set()
stream_state.thread.join(timeout=0.2)
def _payload_remote_session_handle(payload: dict[str, Any]) -> RemoteSessionHandle | None:
"""Return the decoded prompt-scoped remote session handle for one payload."""
remote_session = payload.get("remote_session")
if not is_remote_session_handle_payload(remote_session):
return None
return RemoteSessionHandle.from_payload(remote_session)
def _resolve_remote_session_inputs(
hydrated_inputs: dict[str, Any],
*,
component_id: str | None = None,
) -> dict[str, Any]:
"""Resolve any remote-session value refs embedded in boundary inputs."""
ref_input_names = [
input_name
for input_name, input_value in hydrated_inputs.items()
if is_remote_session_value_ref_payload(input_value)
]
if ref_input_names:
logger.info(
"Resolving %d remote session input refs for component=%s inputs=%s.",
len(ref_input_names),
component_id or "<unknown>",
sorted(ref_input_names),
)
return {
input_name: _REMOTE_SESSION_STORE.resolve_value(input_value)
for input_name, input_value in hydrated_inputs.items()
}
def _remote_session_affinity_key(payload: dict[str, Any]) -> str | None:
"""Return the affinity key that should keep split proxy calls on one remote worker."""
session_handle = _payload_remote_session_handle(payload)
if session_handle is None:
return None
return session_handle.session_id
class _NullPromptServer:
"""Minimal PromptExecutor server stub for headless subgraph execution."""
def __init__(self) -> None:
"""Initialize the no-op prompt server state."""
self.client_id: str | None = None
self.last_node_id: str | None = None
def send_sync(self, event: str, data: dict[str, Any], client_id: str | None) -> None:
"""Discard PromptExecutor progress and status events."""
logger.debug("Suppressed remote prompt event %s for client %s.", event, client_id)
def _extract_custom_nodes_bundle(bundle_path: str | None) -> None:
"""Extract a mirrored custom_nodes archive into a temporary import path."""
if not bundle_path:
return
settings = get_settings()
if settings.execution_mode == "local":
logger.debug("Skipping custom_nodes bundle extraction in local execution mode.")
return
local_bundle = settings.local_storage_root / bundle_path.lstrip("/")
if not local_bundle.exists():
logger.warning("Custom nodes bundle %s was not found in local storage.", local_bundle)
return
extraction_root = Path(tempfile.gettempdir()) / "comfy-modal-sync-custom-nodes"
extraction_root.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(local_bundle, "r") as archive:
archive.extractall(extraction_root)
if str(extraction_root) not in sys.path:
sys.path.insert(0, str(extraction_root))
logger.info("Extracted remote custom_nodes bundle to %s", extraction_root)
def _load_nodes_module() -> Any:
"""Import the ComfyUI nodes module lazily."""
import nodes
return nodes
def _load_execution_module() -> Any:
"""Import the ComfyUI execution module lazily."""
import execution
return execution
@contextmanager
def _temporary_node_mapping(node_mapping: dict[str, type[Any]] | None) -> Iterator[None]:
"""Temporarily overlay node mappings for tests or custom runtimes."""
if node_mapping is None:
yield
return
nodes_module = _load_nodes_module()
original_mappings = dict(nodes_module.NODE_CLASS_MAPPINGS)
original_display_mappings = dict(getattr(nodes_module, "NODE_DISPLAY_NAME_MAPPINGS", {}))
try:
nodes_module.NODE_CLASS_MAPPINGS.update(node_mapping)
for class_type in node_mapping:
nodes_module.NODE_DISPLAY_NAME_MAPPINGS.setdefault(class_type, class_type)
yield
finally:
nodes_module.NODE_CLASS_MAPPINGS.clear()
nodes_module.NODE_CLASS_MAPPINGS.update(original_mappings)
if hasattr(nodes_module, "NODE_DISPLAY_NAME_MAPPINGS"):
nodes_module.NODE_DISPLAY_NAME_MAPPINGS.clear()
nodes_module.NODE_DISPLAY_NAME_MAPPINGS.update(original_display_mappings)
def _invoke_original_node(
node_class: type[Any],
node_data: dict[str, Any],
kwargs: dict[str, Any],
) -> tuple[Any, ...]:
"""Execute an original V1 or V3 node class and normalize its outputs."""
class_type = node_data["class_type"]
logger.info("Executing remote node %s", class_type)
if hasattr(node_class, "GET_SCHEMA"):
node_output = node_class.execute(**kwargs)
if hasattr(node_output, "result"):
result = node_output.result
return tuple(result) if result is not None else tuple()
return tuple(node_output)
instance = node_class()
function_name = getattr(node_class, "FUNCTION", "execute")
function = getattr(instance, function_name)
result = function(**kwargs)
if result is None:
return tuple()
if isinstance(result, tuple):
return result
if isinstance(result, list):
return tuple(result)
return (result,)
def execute_node_locally(
node_data: dict[str, Any],
kwargs_payload: bytes | bytearray | str | dict[str, Any],
node_mapping: dict[str, type[Any]] | None = None,
) -> bytes:
"""Execute a single target node in-process and return serialized outputs."""
_extract_custom_nodes_bundle(node_data.get("custom_nodes_bundle"))
kwargs = deserialize_node_inputs(kwargs_payload)
if node_mapping is not None:
class_type = node_data["class_type"]
if class_type not in node_mapping:
raise KeyError(f"Remote node class {class_type!r} is not registered.")
outputs = _invoke_original_node(node_mapping[class_type], node_data, kwargs)
return serialize_node_outputs(outputs)
with _temporary_node_mapping(node_mapping):
resolved_node_mapping = _load_nodes_module().NODE_CLASS_MAPPINGS
class_type = node_data["class_type"]
if class_type not in resolved_node_mapping:
raise KeyError(f"Remote node class {class_type!r} is not registered.")
outputs = _invoke_original_node(resolved_node_mapping[class_type], node_data, kwargs)
return serialize_node_outputs(outputs)
def _apply_boundary_inputs(
prompt: dict[str, Any],
boundary_input_specs: list[dict[str, Any]],
hydrated_inputs: dict[str, Any],
) -> None:
"""Inject hydrated local boundary inputs into a remote subgraph prompt."""
logger.info("Applying %d hydrated boundary inputs to remote subgraph prompt.", len(boundary_input_specs))
for boundary_input in boundary_input_specs:
proxy_input_name = str(boundary_input["proxy_input_name"])
if proxy_input_name not in hydrated_inputs:
raise KeyError(f"Missing hydrated boundary input {proxy_input_name!r}.")
value = hydrated_inputs[proxy_input_name]
io_type = (
str(boundary_input["io_type"])
if boundary_input.get("io_type") is not None
else None
)
logger.info(
"Applying boundary input %s to %d targets.",
proxy_input_name,
len(boundary_input.get("targets", [])),
)
for target in boundary_input.get("targets", []):
node_id = str(target["node_id"])
input_name = str(target["input_name"])
prompt[node_id]["inputs"][input_name] = _normalize_prompt_input_value(
value,
io_type=io_type,
)
def _collapse_cache_slot(slot_values: Any, is_list: bool) -> Any:
"""Convert a PromptExecutor cache slot back into a node-style output value."""
if is_list:
return slot_values
if not isinstance(slot_values, list):
return slot_values
if len(slot_values) == 1:
return slot_values[0]
return slot_values
def _extract_prompt_executor_error(executor: Any) -> str:
"""Extract a useful failure message from a PromptExecutor run."""
for event, data in reversed(executor.status_messages):
if event == "execution_error":
return _format_prompt_executor_error_payload(data)
if event == "execution_interrupted":
return "Remote subgraph execution was interrupted."
return "Remote subgraph execution failed."
def _format_prompt_executor_error_payload(data: Any) -> str:
"""Return a richer human-readable PromptExecutor failure message when available."""
if not isinstance(data, dict):
return "Remote subgraph execution failed."
message = str(data.get("exception_message") or "Remote subgraph execution failed.")
node_id = data.get("node_id")
node_type = data.get("node_type")
current_inputs = data.get("current_inputs")
if node_id is None and node_type is None and not current_inputs:
return message
details: list[str] = [message]
if node_id is not None or node_type is not None:
details.append(f"node_id={node_id!r} node_type={node_type!r}")
if current_inputs:
details.append(f"current_inputs={current_inputs!r}")
return " | ".join(details)
def _node_input_type_map(node_class: type[Any]) -> dict[str, str]:
"""Return one node class's declared V1 input types keyed by input name."""
input_types_callable = getattr(node_class, "INPUT_TYPES", None)
if not callable(input_types_callable):
return {}
raw_input_types = input_types_callable()
if not isinstance(raw_input_types, dict):
return {}
input_type_map: dict[str, str] = {}
for section_name in ("required", "optional", "hidden"):
section = raw_input_types.get(section_name)
if not isinstance(section, dict):
continue
for input_name, input_config in section.items():
if not isinstance(input_config, tuple) or not input_config:
continue
declared_type = input_config[0]
if isinstance(declared_type, str):
input_type_map[str(input_name)] = declared_type
return input_type_map
def _coerce_primitive_prompt_input_value(
*,
node_id: str,
class_type: str,
input_name: str,
declared_type: str,
input_value: Any,
) -> Any:
"""Coerce one primitive prompt literal using ComfyUI's `validate_inputs` semantics."""
literal_value = (
input_value.get("__value__")
if isinstance(input_value, dict) and "__value__" in input_value
else input_value
)
if isinstance(literal_value, list):
return input_value
try:
if declared_type == "INT":
return int(literal_value)
if declared_type == "FLOAT":
return float(literal_value)
if declared_type == "STRING":
return str(literal_value)
if declared_type == "BOOLEAN":
return bool(literal_value)
except (OverflowError, TypeError, ValueError) as exc:
raise RemoteSubgraphExecutionError(
"Remote subgraph input could not be coerced to the declared primitive socket type."
f" node_id={node_id!r} node_type={class_type!r}"
f" input_name={input_name!r} declared_type={declared_type!r}"
f" received_value={literal_value!r}"
) from exc
return input_value
def _coerce_prompt_primitive_input_values(
prompt: dict[str, Any],
node_mapping: dict[str, type[Any]],
) -> None:
"""Mutate prompt literals in-place to match ComfyUI's primitive widget coercion."""
for node_id, prompt_node in sorted(prompt.items()):
class_type = str(prompt_node.get("class_type"))
node_class = node_mapping.get(class_type)
if node_class is None:
continue
input_type_map = _node_input_type_map(node_class)
if not input_type_map:
continue
inputs = prompt_node.get("inputs") or {}
for input_name, input_value in list(inputs.items()):
declared_type = input_type_map.get(str(input_name))
if declared_type not in _PRIMITIVE_WIDGET_INPUT_TYPES:
continue
if (
isinstance(input_value, list)
and len(input_value) == 2
and isinstance(input_value[0], str)
):
continue
coerced_value = _coerce_primitive_prompt_input_value(
node_id=str(node_id),
class_type=class_type,
input_name=str(input_name),
declared_type=declared_type,
input_value=input_value,
)
if coerced_value is not input_value:
logger.debug(
"Coerced remote primitive input %s.%s from %r to %r for type %s.",
node_id,
input_name,
input_value,
coerced_value,
declared_type,
)
inputs[input_name] = coerced_value
def _validate_prompt_input_shapes(
prompt: dict[str, Any],
node_mapping: dict[str, type[Any]],
boundary_input_specs: list[dict[str, Any]] | None = None,
) -> None:
"""Reject prompt inputs that still look invalid for primitive widget sockets."""
boundary_targets = {
(str(target.get("node_id")), str(target.get("input_name")))
for boundary_input in (boundary_input_specs or [])
for target in boundary_input.get("targets", [])
if target.get("node_id") is not None and target.get("input_name") is not None
}
for node_id, prompt_node in sorted(prompt.items()):
class_type = str(prompt_node.get("class_type"))
node_class = node_mapping.get(class_type)
if node_class is None:
continue
input_type_map = _node_input_type_map(node_class)
if not input_type_map:
continue
for input_name, input_value in (prompt_node.get("inputs") or {}).items():
declared_type = input_type_map.get(str(input_name))
if declared_type not in _PRIMITIVE_WIDGET_INPUT_TYPES:
continue
if (
isinstance(input_value, list)
and len(input_value) == 2
and isinstance(input_value[0], str)
):
continue
if (str(node_id), str(input_name)) in boundary_targets:
continue
literal_value = (
input_value.get("__value__")
if isinstance(input_value, dict) and "__value__" in input_value
else input_value
)
if isinstance(literal_value, list):
raise RemoteSubgraphExecutionError(
"Remote subgraph input has an invalid list value for a primitive socket."
f" node_id={node_id!r} node_type={class_type!r}"
f" input_name={input_name!r} declared_type={declared_type!r}"
f" received_value={literal_value!r}"
)
def _resolve_required_subgraph_nodes(
prompt: dict[str, Any],
execute_node_ids: list[str],
) -> list[str]:
"""Return the dependency closure needed to execute the requested subgraph nodes."""
required: set[str] = set()
pending = list(execute_node_ids)
logger.info("Resolving dependency closure for remote execute targets: %s", execute_node_ids)
while pending:
node_id = str(pending.pop())
if node_id not in prompt:
logger.warning(
"Skipping missing remote execute target %s while resolving dependency closure.",
node_id,
)
continue
if node_id in required:
continue
required.add(node_id)
for input_value in (prompt[node_id].get("inputs") or {}).values():
if _is_link(input_value):
pending.append(str(input_value[0]))
resolved = sorted(required)
logger.info("Resolved remote dependency closure: %s", resolved)
return resolved
def _trim_subgraph_payload_to_required_nodes(payload: dict[str, Any]) -> dict[str, Any]:
"""Trim a subgraph payload down to the dependency closure of its execute targets."""
trimmed_payload = copy.deepcopy(payload)
prompt = trimmed_payload.get("subgraph_prompt", {})
if not isinstance(prompt, dict):
return trimmed_payload
prompt_node_ids = {str(node_id) for node_id in prompt}
requested_execute_node_ids = [
str(node_id) for node_id in trimmed_payload.get("execute_node_ids", [])
]
available_execute_node_ids = [
node_id for node_id in requested_execute_node_ids if node_id in prompt_node_ids
]
dropped_execute_node_ids = [
node_id for node_id in requested_execute_node_ids if node_id not in prompt_node_ids
]
if dropped_execute_node_ids:
logger.warning(
"Dropping remote execute targets absent from subgraph prompt for component=%s: %s",
payload.get("component_id"),
dropped_execute_node_ids,
)
required_node_ids = set(
_resolve_required_subgraph_nodes(
prompt=prompt,
execute_node_ids=available_execute_node_ids,
)
)
if not required_node_ids:
return trimmed_payload
original_node_ids = list(prompt.keys())
trimmed_payload["subgraph_prompt"] = {
str(node_id): prompt[node_id]
for node_id in original_node_ids
if str(node_id) in required_node_ids
}
trimmed_payload["boundary_inputs"] = [
{
**copy.deepcopy(boundary_input),
"targets": [
copy.deepcopy(target)
for target in boundary_input.get("targets", [])
if str(target.get("node_id")) in required_node_ids
],
}
for boundary_input in trimmed_payload.get("boundary_inputs", [])
if any(str(target.get("node_id")) in required_node_ids for target in boundary_input.get("targets", []))
]
trimmed_payload["boundary_outputs"] = [
copy.deepcopy(boundary_output)
for boundary_output in trimmed_payload.get("boundary_outputs", [])
if str(boundary_output.get("node_id")) in required_node_ids
]
trimmed_payload["component_node_ids"] = [
str(node_id)
for node_id in trimmed_payload.get("component_node_ids", [])
if str(node_id) in required_node_ids
]
trimmed_payload["execute_node_ids"] = [
str(node_id)
for node_id in trimmed_payload.get("execute_node_ids", [])
if str(node_id) in required_node_ids
]
trimmed_payload["mapped_execute_node_ids"] = [
str(node_id)
for node_id in trimmed_payload.get("mapped_execute_node_ids", [])
if str(node_id) in required_node_ids
]
trimmed_payload["static_execute_node_ids"] = [
str(node_id)
for node_id in trimmed_payload.get("static_execute_node_ids", [])
if str(node_id) in required_node_ids
]
logger.info(
"Trimmed remote subgraph payload %s from %d prompt nodes to %d required nodes.",
payload.get("component_id"),
len(original_node_ids),
len(trimmed_payload["subgraph_prompt"]),
)
return trimmed_payload
def _is_link(value: Any) -> bool:
"""Return whether a prompt input value is a ComfyUI link."""
return (
isinstance(value, list)
and len(value) == 2
and all(not isinstance(item, dict) for item in value)
)
def _normalize_link_output_index(value: Any) -> Any:
"""Unwrap a singleton list around a prompt-link output index when present."""
while isinstance(value, list) and len(value) == 1:
value = value[0]
return value
def _unwrap_wrapped_prompt_link(value: Any) -> Any:
"""Collapse nested singleton wrappers around one serialized prompt link when present."""
candidate = value
while isinstance(candidate, list) and len(candidate) == 1:
candidate = candidate[0]
if _is_link(candidate):
return [candidate[0], _normalize_link_output_index(candidate[1])]
return value
def _normalize_prompt_input_value(value: Any, io_type: str | None = None) -> Any:
"""Unwrap transport-added singleton wrappers only for scalar-like prompt input values."""
wrapped_link = _unwrap_wrapped_prompt_link(value)
if wrapped_link is not value:
return wrapped_link
while (
isinstance(value, list)
and len(value) == 1
and (
io_type in _PRIMITIVE_WIDGET_INPUT_TYPES
or value[0] is None
or isinstance(value[0], bool | int | float | str)
)
):
value = value[0]
if isinstance(value, list) and len(value) == 2 and isinstance(value[0], str):
return [value[0], _normalize_link_output_index(value[1])]
if value is None or isinstance(value, bool | int | float | str):
return value
return value
def _normalize_subgraph_payload(payload: dict[str, Any]) -> dict[str, Any]:
"""Return a subgraph payload with canonical prompt-link and output-index shapes."""
normalized_payload = copy.deepcopy(payload)
for node_info in normalized_payload.get("subgraph_prompt", {}).values():
inputs = node_info.get("inputs") or {}
for input_name, input_value in list(inputs.items()):
inputs[input_name] = _normalize_prompt_input_value(input_value)
for boundary_output in normalized_payload.get("boundary_outputs", []):
if "node_id" in boundary_output and isinstance(boundary_output["node_id"], list):
boundary_output["node_id"] = _normalize_prompt_input_value(boundary_output["node_id"])
if "output_index" in boundary_output:
boundary_output["output_index"] = _normalize_link_output_index(
boundary_output["output_index"]
)
normalized_payload["execute_node_ids"] = [
_normalize_prompt_input_value(node_id)
for node_id in normalized_payload.get("execute_node_ids", [])
]
return normalized_payload
def _execute_subgraph_with_mapping(
payload: dict[str, Any],
hydrated_inputs: dict[str, Any],
node_mapping: dict[str, type[Any]],
) -> tuple[Any, ...]:
"""Execute a rewritten remote component using an explicit node mapping."""
normalized_payload = _trim_subgraph_payload_to_required_nodes(
_normalize_subgraph_payload(payload)
)
resolved_inputs = _resolve_remote_session_inputs(
dict(hydrated_inputs),
component_id=str(payload.get("component_id") or ""),
)
session_handle = _payload_remote_session_handle(normalized_payload)
if session_handle is not None:
logger.info(
"Executing mapped remote subgraph %s with remote_session session_id=%s prompt_id=%s owner_component_id=%s.",
payload.get("component_id"),
session_handle.session_id,
session_handle.prompt_id,
session_handle.owner_component_id,
)
prompt = copy.deepcopy(normalized_payload["subgraph_prompt"])
logger.info(
"Executing remote subgraph %s via test mapping with %d prompt nodes.",
payload.get("component_id"),
len(prompt),
)
_apply_boundary_inputs(
prompt=prompt,
boundary_input_specs=list(normalized_payload.get("boundary_inputs", [])),
hydrated_inputs=resolved_inputs,
)
_coerce_prompt_primitive_input_values(prompt, node_mapping)
_validate_prompt_input_shapes(
prompt,
node_mapping,
list(normalized_payload.get("boundary_inputs", [])),
)
required_node_ids = _resolve_required_subgraph_nodes(
prompt=prompt,
execute_node_ids=list(normalized_payload.get("execute_node_ids", [])),
)
executed_outputs: dict[str, tuple[Any, ...]] = {}
pending = set(required_node_ids)
while pending:
progressed = False
logger.info("Mapped remote subgraph pending nodes: %s", sorted(pending))
for node_id in list(sorted(pending)):
prompt_node = prompt[node_id]
kwargs: dict[str, Any] = {}
unresolved_dependency = False
for input_name, input_value in (prompt_node.get("inputs") or {}).items():
if _is_link(input_value):
upstream_node_id = str(input_value[0])
if upstream_node_id not in executed_outputs:
unresolved_dependency = True
break
kwargs[str(input_name)] = executed_outputs[upstream_node_id][int(input_value[1])]
else:
kwargs[str(input_name)] = input_value
if unresolved_dependency:
continue
class_type = str(prompt_node["class_type"])
if class_type not in node_mapping:
raise KeyError(f"Remote node class {class_type!r} is not registered.")
logger.info(
"Executing mapped remote node %s (%s) with %d inputs.",
node_id,
class_type,
len(kwargs),
)
executed_outputs[node_id] = _invoke_original_node(
node_mapping[class_type],
prompt_node,
kwargs,
)
pending.remove(node_id)
progressed = True
if not progressed:
raise RemoteSubgraphExecutionError(
"Unable to resolve execution order for remote subgraph payload."
)
outputs: list[Any] = []
for boundary_output in normalized_payload.get("boundary_outputs", []):
node_id = str(boundary_output["node_id"])
output_index = int(boundary_output["output_index"])
node_outputs = executed_outputs.get(node_id)
if node_outputs is None:
raise RemoteSubgraphExecutionError(
f"Remote subgraph did not execute boundary output node {node_id}."
)
output_value = node_outputs[output_index]
if bool(boundary_output.get("session_output")):
if session_handle is None:
raise RemoteSessionStateError(
"Session-backed boundary outputs require payload.remote_session."
)
output_value = _REMOTE_SESSION_STORE.put_output(
session_handle,
node_id=node_id,
output_index=output_index,
value=output_value,
).to_payload()
outputs.append(output_value)
logger.info(
"Mapped remote subgraph %s produced %d exported outputs.",
payload.get("component_id"),
len(outputs),
)
return tuple(outputs)
def _execute_subgraph_prompt(
payload: dict[str, Any],
hydrated_inputs: dict[str, Any],
node_mapping: dict[str, type[Any]] | None = None,
) -> tuple[Any, ...]:
"""Execute a remote component prompt and return its exported outputs."""
if node_mapping is not None:
return _execute_subgraph_with_mapping(payload, hydrated_inputs, node_mapping)
normalized_payload = _trim_subgraph_payload_to_required_nodes(
_normalize_subgraph_payload(payload)
)
resolved_inputs = _resolve_remote_session_inputs(
dict(hydrated_inputs),
component_id=str(payload.get("component_id") or ""),
)
session_handle = _payload_remote_session_handle(normalized_payload)
if session_handle is not None:
logger.info(
"Executing PromptExecutor remote subgraph %s with remote_session session_id=%s prompt_id=%s owner_component_id=%s.",
payload.get("component_id"),
session_handle.session_id,
session_handle.prompt_id,
session_handle.owner_component_id,
)
prompt = copy.deepcopy(normalized_payload["subgraph_prompt"])
logger.info(
"Executing remote subgraph %s through PromptExecutor with %d prompt nodes, %d boundary inputs, and %d exported outputs.",
payload.get("component_id"),
len(prompt),
len(normalized_payload.get("boundary_inputs", [])),
len(normalized_payload.get("boundary_outputs", [])),
)
_apply_boundary_inputs(
prompt=prompt,
boundary_input_specs=list(normalized_payload.get("boundary_inputs", [])),
hydrated_inputs=resolved_inputs,
)
execution = _load_execution_module()
resolved_node_mapping = _load_nodes_module().NODE_CLASS_MAPPINGS
_coerce_prompt_primitive_input_values(prompt, resolved_node_mapping)
_validate_prompt_input_shapes(
prompt,
resolved_node_mapping,
list(normalized_payload.get("boundary_inputs", [])),
)
with _temporary_node_mapping(node_mapping):
executor = execution.PromptExecutor(_NullPromptServer())
execution_started_at = time.perf_counter()
logger.info(
"Starting PromptExecutor for remote subgraph %s with execute targets %s.",
payload.get("component_id"),
normalized_payload.get("execute_node_ids", []),
)
executor.execute(
prompt=prompt,
prompt_id=str(
payload.get("prompt_id")
or payload.get("component_id", "modal-subgraph")
),
extra_data=copy.deepcopy(normalized_payload.get("extra_data") or {}),
execute_outputs=list(normalized_payload.get("execute_node_ids", [])),
)
logger.info(
"PromptExecutor finished for remote subgraph %s in %.3fs with success=%s and %d status messages.",
payload.get("component_id"),
time.perf_counter() - execution_started_at,
executor.success,
len(executor.status_messages),
)
if executor.status_messages:
logger.info(
"Remote subgraph %s status events: %s",
payload.get("component_id"),
[event for event, _data in executor.status_messages],
)
if not executor.success:
raise RemoteSubgraphExecutionError(_extract_prompt_executor_error(executor))
outputs: list[Any] = []
for boundary_output in normalized_payload.get("boundary_outputs", []):
node_id = str(boundary_output["node_id"])
output_index = int(boundary_output["output_index"])
cache_entry = executor.caches.outputs.get(node_id)
if cache_entry is None:
raise RemoteSubgraphExecutionError(
f"Remote subgraph did not produce cache entry for node {node_id}."
)
if output_index >= len(cache_entry.outputs):
raise RemoteSubgraphExecutionError(
f"Remote subgraph output index {output_index} is missing for node {node_id}."
)
output_value = _collapse_cache_slot(
slot_values=cache_entry.outputs[output_index],
is_list=bool(boundary_output.get("is_list", False)),
)
if bool(boundary_output.get("session_output")):
if session_handle is None:
raise RemoteSessionStateError(
"Session-backed boundary outputs require payload.remote_session."
)
output_value = _REMOTE_SESSION_STORE.put_output(
session_handle,
node_id=node_id,
output_index=output_index,
value=output_value,
).to_payload()
outputs.append(output_value)
logger.info(
"Collected exported output %s from node %s output %d.",
boundary_output.get("proxy_output_name"),
node_id,
output_index,
)
return tuple(outputs)
def execute_subgraph_locally(
payload: dict[str, Any],
kwargs_payload: bytes | bytearray | str | dict[str, Any],
node_mapping: dict[str, type[Any]] | None = None,
) -> bytes:
"""Execute a rewritten remote component in-process and return serialized outputs."""
_extract_custom_nodes_bundle(payload.get("custom_nodes_bundle"))
hydrated_inputs = deserialize_node_inputs(kwargs_payload)
session_handle = _payload_remote_session_handle(payload)
logger.info(
"Executing local fallback subgraph %s with %d hydrated inputs session_id=%s clear_remote_session=%s.",
payload.get("component_id"),
len(hydrated_inputs),
session_handle.session_id if session_handle is not None else None,
bool(payload.get("clear_remote_session")),
)
try:
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(_execute_subgraph_prompt, payload, hydrated_inputs, node_mapping)
try:
outputs = future.result()
except Exception:
logger.exception(
"Local fallback subgraph %s raised while running in worker thread.",
payload.get("component_id"),
)
raise
finally:
if bool(payload.get("clear_remote_session")) and session_handle is not None:
logger.info(
"Clearing remote session after component=%s session_id=%s.",
payload.get("component_id"),
session_handle.session_id,
)
_REMOTE_SESSION_STORE.clear_session(session_handle)
logger.info(
"Local fallback subgraph %s completed with %d outputs.",
payload.get("component_id"),
len(outputs),
)
return serialize_node_outputs(outputs)
def _modal_lookup_error_types() -> tuple[type[BaseException], ...]:
"""Return Modal exception types that indicate lookup or hydration failure."""
if modal is None:
return tuple()
exception_module = getattr(modal, "exception", None)
if exception_module is None:
return tuple()
error_types: list[type[BaseException]] = []
for error_name in ("NotFoundError", "ExecutionError", "InvalidError"):
error_type = getattr(exception_module, error_name, None)
if isinstance(error_type, type) and issubclass(error_type, BaseException):
error_types.append(error_type)
return tuple(error_types)
def _load_modal_cloud_module() -> Any:
"""Load the stable Modal cloud entry module under a valid Python name."""
existing_module = sys.modules.get(_MODAL_CLOUD_MODULE_NAME)
if existing_module is not None and getattr(existing_module, "app", None) is not None:
return existing_module
if existing_module is not None:
logger.warning(
"Discarding partially initialized Modal cloud module %s before reload.",
_MODAL_CLOUD_MODULE_NAME,
)
sys.modules.pop(_MODAL_CLOUD_MODULE_NAME, None)
cloud_module_path = Path(__file__).resolve().parents[1] / f"{_MODAL_CLOUD_MODULE_NAME}.py"
module_spec = importlib.util.spec_from_file_location(
_MODAL_CLOUD_MODULE_NAME,
cloud_module_path,
)
if module_spec is None or module_spec.loader is None:
raise ModalRemoteInvocationError(
f"Unable to create module spec for Modal cloud entrypoint at {cloud_module_path}."
)
cloud_module = importlib.util.module_from_spec(module_spec)
sys.modules[_MODAL_CLOUD_MODULE_NAME] = cloud_module
try:
module_spec.loader.exec_module(cloud_module)
except BaseException:
sys.modules.pop(_MODAL_CLOUD_MODULE_NAME, None)
raise
return cloud_module
def _lookup_local_prompt_server() -> Any | None:
"""Return the live local ComfyUI PromptServer instance when available."""
try:
import server
except ModuleNotFoundError:
return None
return getattr(server.PromptServer, "instance", None)
def _emit_local_modal_status(
*,
prompt_id: str | None,
client_id: str | None,
phase: str,
node_ids: list[str],
active_node_id: str | None = None,
active_node_class_type: str | None = None,
active_node_role: str | None = None,
error_message: str | None = None,
status_message: str | None = None,
status_current: int | None = None,
status_total: int | None = None,
) -> None:
"""Forward remote execution progress into the local ComfyUI websocket stream."""
if client_id is None:
return
prompt_server = _lookup_local_prompt_server()
if prompt_server is None:
return
payload: dict[str, Any] = {
"phase": phase,
"prompt_id": prompt_id,
"node_ids": list(node_ids),
}
if active_node_id is not None:
payload["active_node_id"] = active_node_id
if active_node_class_type is not None:
payload["active_node_class_type"] = active_node_class_type
if active_node_role is not None:
payload["active_node_role"] = active_node_role
if error_message is not None:
payload["error_message"] = error_message
if status_message is not None:
payload["status_message"] = status_message
if status_current is not None:
payload["status_current"] = int(status_current)
if status_total is not None:
payload["status_total"] = int(status_total)
prompt_server.send_sync("modal_status", payload, client_id)
def _emit_local_modal_progress(
*,
prompt_id: str | None,
client_id: str | None,
node_id: str,
value: float,
max_value: float,
display_node_id: str | None = None,
real_node_id: str | None = None,
lane_id: str | None = None,
clear: bool = False,
item_index: int | None = None,
aggregate_only: bool = False,
) -> None:
"""Forward remote numeric node progress into the local ComfyUI websocket stream."""
if client_id is None:
return
prompt_server = _lookup_local_prompt_server()
if prompt_server is None:
return
payload: dict[str, Any] = {
"prompt_id": prompt_id,
"node_id": node_id,
"value": float(value),
"max": float(max_value),
}
if display_node_id is not None:
payload["display_node_id"] = display_node_id
if real_node_id is not None:
payload["real_node_id"] = real_node_id
if lane_id is not None:
payload["lane_id"] = lane_id
if clear:
payload["clear"] = True
if item_index is not None:
payload["item_index"] = int(item_index)
if aggregate_only:
payload["aggregate_only"] = True
prompt_server.send_sync("modal_progress", payload, client_id)
def _emit_local_executed_output(
*,
prompt_id: str | None,
client_id: str | None,
node_id: str,
display_node_id: str | None,
output_payload: Any,
) -> None:
"""Forward one remote node's executed UI payload into the local websocket stream."""
if client_id is None:
return
prompt_server = _lookup_local_prompt_server()
if prompt_server is None:
return
payload = {
"prompt_id": prompt_id,
"node": node_id,
"display_node": display_node_id or node_id,
"output": output_payload,
}
prompt_server.send_sync("executed", payload, client_id)
def _emit_local_preview_image(
*,
prompt_id: str | None,
client_id: str | None,
node_id: str,
display_node_id: str | None,
parent_node_id: str | None,
real_node_id: str | None,
image_type: str,
image_bytes: bytes,
max_size: int | None,
) -> None:
"""Forward one remote preview image into the local ComfyUI preview websocket path."""
if client_id is None:
return
prompt_server = _lookup_local_prompt_server()
if prompt_server is None:
return
try:
from PIL import Image
from protocol import BinaryEventTypes
except ModuleNotFoundError:
logger.warning("Preview forwarding is unavailable because Pillow or ComfyUI protocol imports failed.")
return
with BytesIO(image_bytes) as image_buffer:
image = Image.open(image_buffer)
image.load()
metadata: dict[str, Any] = {
"node_id": node_id,
"prompt_id": prompt_id,
"display_node_id": display_node_id or node_id,
"real_node_id": real_node_id or node_id,
}
if parent_node_id is not None:
metadata["parent_node_id"] = parent_node_id
prompt_server.send_sync(
BinaryEventTypes.PREVIEW_IMAGE_WITH_METADATA,
((image_type, image, max_size), metadata),
client_id,
)
def _emit_local_preview_boundary_output(
*,
prompt_id: str | None,
client_id: str | None,
preview_target_node_ids: list[str],
image_value: Any,
) -> None:
"""Render one streamed remote boundary IMAGE value into local PreviewImage UI events."""
if client_id is None or not preview_target_node_ids:
return
prompt_server = _lookup_local_prompt_server()
if prompt_server is None:
return
try:
import nodes
except ModuleNotFoundError:
logger.warning("Preview boundary streaming is unavailable because ComfyUI nodes could not be imported.")
return
preview_factory = getattr(nodes, "PreviewImage", None)
if preview_factory is None:
logger.warning("Preview boundary streaming is unavailable because PreviewImage is not registered.")
return
preview_result = preview_factory().save_images(images=image_value)
if not isinstance(preview_result, dict):
return
output_payload = preview_result.get("ui")
if not isinstance(output_payload, dict):
return
for preview_target_node_id in preview_target_node_ids:
prompt_server.send_sync(
"executed",
{
"prompt_id": prompt_id,
"node": preview_target_node_id,
"display_node": preview_target_node_id,
"output": output_payload,
},
client_id,
)
def _allowed_suppressed_stream_node_ids(payload: dict[str, Any]) -> set[str]:
"""Return the node ids that may surface UI events for a suppressed mapped/static stream."""
allowed_node_ids = {
str(node_id)
for node_id in payload.get("execute_node_ids", [])
if str(node_id)
}
allowed_node_ids.update(
str(boundary_output["node_id"])
for boundary_output in payload.get("boundary_outputs", [])
if boundary_output.get("node_id") is not None and str(boundary_output["node_id"])
)
return allowed_node_ids
def _should_forward_suppressed_stream_event(
payload: dict[str, Any],
reported_node_id: Any,
) -> bool:
"""Return whether a suppressed mapped/static stream event belongs to this payload."""
if not bool(payload.get("suppress_status_stream")):
return True
if reported_node_id is None:
return False
allowed_node_ids = _allowed_suppressed_stream_node_ids(payload)
if not allowed_node_ids:
return True
return str(reported_node_id) in allowed_node_ids
def _progress_stream_event_node_id(stream_event: dict[str, Any]) -> str | None:
"""Return the best node id to use for progress filtering and forwarding."""
for candidate in (
stream_event.get("real_node_id"),
stream_event.get("node_id"),
stream_event.get("display_node_id"),
):
if candidate is None:
continue
candidate_text = str(candidate)
if candidate_text:
return candidate_text
return None
def _progress_stream_event_metadata(stream_event: dict[str, Any]) -> dict[str, str | None] | None:
"""Return normalized metadata for one streamed progress event."""
reported_node_id = (
str(stream_event["node_id"])
if stream_event.get("node_id") is not None
else None
)
display_node_id = (
str(stream_event["display_node_id"])
if stream_event.get("display_node_id") is not None
else reported_node_id
)
real_node_id = (
str(stream_event["real_node_id"])
if stream_event.get("real_node_id") is not None
else None
)
filter_node_id = real_node_id or reported_node_id or display_node_id
if filter_node_id is None:
return None
return {
"node_id": reported_node_id or filter_node_id,
"display_node_id": display_node_id,
"real_node_id": real_node_id,
"filter_node_id": filter_node_id,
}
def _should_stream_remote_progress(payload: dict[str, Any]) -> bool:
"""Return whether the local client has enough context to mirror remote node progress."""
extra_data = payload.get("extra_data") or {}
return (
payload.get("payload_kind") in {"subgraph", "mapped_subgraph"}
and isinstance(payload.get("prompt_id"), str)
and bool(payload.get("prompt_id"))
and isinstance(extra_data.get("client_id"), str)
and bool(extra_data.get("client_id"))
and isinstance(payload.get("component_node_ids"), list)
and len(payload.get("component_node_ids")) > 0
)
def _local_processing_interrupted() -> bool:
"""Return whether the current local ComfyUI execution was interrupted."""
try:
import comfy.model_management
except ModuleNotFoundError:
return False
return bool(comfy.model_management.processing_interrupted())
def _raise_local_interrupt() -> None:
"""Raise ComfyUI's native interruption exception for the current execution."""
import comfy.model_management
raise comfy.model_management.InterruptProcessingException()
def _remote_interrupt_key(payload: dict[str, Any]) -> tuple[str, str]:
"""Return the prompt/component pair used to interrupt one remote execution."""
prompt_id = str(payload.get("prompt_id") or payload.get("component_id") or "modal-subgraph")
component_id = str(payload.get("component_id") or "single-node")
return prompt_id, component_id
def _remote_interrupt_flag_key(prompt_id: str, component_id: str) -> str:
"""Return the shared Modal interrupt-store key for one payload execution."""
return f"{prompt_id}:{component_id}"
def _lookup_modal_interrupt_store() -> Any | None:
"""Return the shared Modal Dict used to signal remote cancellation requests."""
if modal is None or not hasattr(modal, "Dict"):
return None
settings = get_settings()
cache_key = (settings.interrupt_dict_name, _modal_environment_name())
with _MODAL_INTERRUPT_DICTS_LOCK:
cached_store = _MODAL_INTERRUPT_DICTS.get(cache_key)
if cached_store is not None:
return cached_store
interrupt_store = modal.Dict.from_name(
settings.interrupt_dict_name,
environment_name=cache_key[1],
create_if_missing=True,
)
with _MODAL_INTERRUPT_DICTS_LOCK:
_MODAL_INTERRUPT_DICTS[cache_key] = interrupt_store
return interrupt_store
def _request_remote_interrupt(payload: dict[str, Any]) -> bool:
"""Write one remote cancellation request into the shared Modal interrupt store."""
interrupt_store = _lookup_modal_interrupt_store()
if interrupt_store is None:
return False
prompt_id, component_id = _remote_interrupt_key(payload)
interrupt_store.put(
_remote_interrupt_flag_key(prompt_id, component_id),
{"requested_at": time.time()},
)
logger.info(
"Propagated local interrupt to Modal prompt=%s component=%s through shared control state.",
prompt_id,
component_id,
)
return True
def _sync_local_interrupt_to_cancellation_event(
payload: dict[str, Any],
cancellation_event: threading.Event | None,
) -> bool:
"""Mirror ComfyUI's interrupt flag into the current Modal cancellation event."""
if cancellation_event is not None and cancellation_event.is_set():
return True
if not _local_processing_interrupted():
return False
if cancellation_event is not None and not cancellation_event.is_set():
logger.info(
"Observed local interrupt while Modal component=%s was running; requesting remote cancellation.",
payload.get("component_id"),
)
cancellation_event.set()
return True
def _propagate_remote_interrupt_request(
payload: dict[str, Any],
interrupt_remote_call: Callable[[], Any] | None,
) -> None:
"""Send one best-effort remote cancellation request for an active Modal payload."""
prompt_id, component_id = _remote_interrupt_key(payload)
if interrupt_remote_call is None:
logger.warning(
"Local interrupt requested for component=%s, but no remote interrupt method is available.",
component_id,
)
return
try:
interrupt_remote_call()
logger.info(
"Propagated local interrupt to Modal prompt=%s component=%s.",
prompt_id,
component_id,
)
except Exception:
logger.exception(
"Failed to propagate local interrupt to Modal prompt=%s component=%s.",
prompt_id,
component_id,
)
def _invoke_remote_call_with_interrupts(
*,
payload: dict[str, Any],
invoke_remote_call: Callable[[], bytes],
interrupt_remote_call: Callable[[], Any] | None,
cancellation_event: threading.Event | None,
) -> bytes:
"""Run one blocking remote call while optionally propagating cancellation to Modal."""
result_queue: queue.Queue[tuple[str, Any]] = queue.Queue()
def execute_remote_call() -> None:
"""Run the blocking Modal request in a worker thread."""
try:
result_queue.put(("result", invoke_remote_call()))
except BaseException as exc:
result_queue.put(("error", exc))
request_thread = threading.Thread(
target=execute_remote_call,
name=f"modal-request-{payload.get('component_id', 'payload')}",
daemon=True,
)
request_thread.start()
interrupt_sent = False
try:
while True:
try:
result_kind, result_payload = result_queue.get(timeout=0.1)
except queue.Empty:
if (
_sync_local_interrupt_to_cancellation_event(payload, cancellation_event)
and not interrupt_sent
):
_propagate_remote_interrupt_request(payload, interrupt_remote_call)
interrupt_sent = True
continue
if result_kind == "result":
return bytes(result_payload)
raise result_payload
finally:
request_thread.join(timeout=1.0)
def _consume_remote_payload_stream(
payload: dict[str, Any],
stream_events: Iterator[dict[str, Any]],
) -> bytes:
"""Forward remote progress events into the local UI and return the final payload bytes."""
prompt_id = str(payload.get("prompt_id")) if payload.get("prompt_id") is not None else None
extra_data = payload.get("extra_data") or {}
client_id = str(extra_data.get("client_id")) if extra_data.get("client_id") is not None else None
node_ids = [str(node_id) for node_id in payload.get("component_node_ids", [])]
suppress_status_stream = bool(payload.get("suppress_status_stream"))
result_payload: bytes | bytearray | None = None
suppressed_progress_node_metadata: dict[str, dict[str, str | None]] = {}
active_remote_log_task_id: str | None = None
try:
for stream_event in stream_events:
event_kind = str(stream_event.get("kind", ""))
if event_kind == "remote_logs":
task_id = _coerce_modal_task_id(stream_event.get("task_id"))
if (
task_id is not None
and active_remote_log_task_id is None
and _is_remote_container_log_stream_enabled()
):
active_remote_log_task_id = _retain_remote_container_log_stream(task_id)
continue
if event_kind == "progress":
event_type = str(stream_event.get("event_type", ""))
if event_type == "node_progress":
progress_metadata = _progress_stream_event_metadata(stream_event)
filter_node_id = (
progress_metadata["filter_node_id"] if progress_metadata is not None else None
)
lane_id = (
str(stream_event["lane_id"])
if stream_event.get("lane_id") is not None
else (
str(payload["mapped_progress_lane_id"])
if payload.get("mapped_progress_lane_id") is not None
else None
)
)
aggregate_only = bool(stream_event.get("aggregate_only", False))
if (
suppress_status_stream
and lane_id is None
and not aggregate_only
and not _should_forward_suppressed_stream_event(payload, filter_node_id)
):
logger.debug(
"Suppressing streamed Modal node progress for component=%s node_id=%s real_node_id=%s because it does not belong to this mapped/static payload.",
payload.get("component_id"),
stream_event.get("node_id"),
stream_event.get("real_node_id"),
)
continue
reported_node_id = (
progress_metadata["node_id"] if progress_metadata is not None else None
)
if reported_node_id is not None:
display_node_id = (
progress_metadata["display_node_id"]
if progress_metadata is not None
else str(reported_node_id)
)
real_node_id = (
progress_metadata["real_node_id"] if progress_metadata is not None else None
)
progress_node_id = real_node_id or display_node_id
if lane_id is not None:
_remember_mapped_lane_node_id(payload, lane_id, progress_node_id)
elif suppress_status_stream and not aggregate_only and progress_metadata is not None:
suppressed_progress_node_metadata[str(progress_metadata["filter_node_id"])] = {
"node_id": str(reported_node_id),
"display_node_id": display_node_id,
"real_node_id": real_node_id,
}
logger.debug(
"Forwarding streamed Modal node progress for component=%s node_id=%s real_node_id=%s value=%s max=%s lane_id=%s.",
payload.get("component_id"),
reported_node_id,
real_node_id,
stream_event.get("value"),
stream_event.get("max"),
lane_id,
)
_emit_local_modal_progress(
prompt_id=prompt_id,
client_id=client_id,
node_id=str(reported_node_id),
value=float(stream_event.get("value", 0.0)),
max_value=float(stream_event.get("max", 1.0)),
display_node_id=display_node_id,
real_node_id=real_node_id,
lane_id=lane_id,
clear=bool(stream_event.get("clear", False)),
item_index=(
int(stream_event["item_index"])
if stream_event.get("item_index") is not None
else (
int(payload["map_item_index"])
if payload.get("map_item_index") is not None
else None
)
),
aggregate_only=aggregate_only,
)
continue
if event_type == "executed":
reported_node_id = stream_event.get("node_id")
if reported_node_id is not None:
if not _should_forward_suppressed_stream_event(payload, reported_node_id):
logger.debug(
"Suppressing streamed Modal executed output for component=%s node_id=%s because it does not belong to this mapped/static payload.",
payload.get("component_id"),
reported_node_id,
)
continue
logger.debug(
"Forwarding streamed Modal executed output for component=%s node_id=%s.",
payload.get("component_id"),
reported_node_id,
)
_emit_local_executed_output(
prompt_id=prompt_id,
client_id=client_id,
node_id=str(reported_node_id),
display_node_id=(
str(stream_event["display_node_id"])
if stream_event.get("display_node_id") is not None
else None
),
output_payload=deserialize_value(stream_event.get("output")),
)
continue
if event_type == "preview":
reported_node_id = stream_event.get("node_id")
image_bytes = deserialize_value(stream_event.get("image_bytes"))
if reported_node_id is not None and isinstance(image_bytes, bytes):
if not _should_forward_suppressed_stream_event(payload, reported_node_id):
logger.debug(
"Suppressing streamed Modal preview image for component=%s node_id=%s because it does not belong to this mapped/static payload.",
payload.get("component_id"),
reported_node_id,
)
continue
logger.debug(
"Forwarding streamed Modal preview image for component=%s node_id=%s.",
payload.get("component_id"),
reported_node_id,
)
_emit_local_preview_image(
prompt_id=prompt_id,
client_id=client_id,
node_id=str(reported_node_id),
display_node_id=(
str(stream_event["display_node_id"])
if stream_event.get("display_node_id") is not None
else None
),
parent_node_id=(
str(stream_event["parent_node_id"])
if stream_event.get("parent_node_id") is not None
else None
),
real_node_id=(
str(stream_event["real_node_id"])
if stream_event.get("real_node_id") is not None
else None
),
image_type=str(stream_event.get("image_type", "PNG")),
image_bytes=image_bytes,
max_size=(
int(stream_event["max_size"])
if stream_event.get("max_size") is not None
else None
),
)
continue
if event_type == "boundary_output":
preview_target_node_ids = [
str(node_id)
for node_id in stream_event.get("preview_target_node_ids", [])
if str(node_id)
]
if preview_target_node_ids:
logger.debug(
"Forwarding streamed Modal boundary output previews for component=%s source_node=%s targets=%s.",
payload.get("component_id"),
stream_event.get("node_id"),
preview_target_node_ids,
)
_emit_local_preview_boundary_output(
prompt_id=prompt_id,
client_id=client_id,
preview_target_node_ids=preview_target_node_ids,
image_value=deserialize_value(stream_event.get("value")),
)
continue
logger.info(
"Forwarding streamed Modal progress for component=%s phase=%s active_node_id=%s.",
payload.get("component_id"),
stream_event.get("phase"),
stream_event.get("active_node_id"),
)
if suppress_status_stream:
remote_phase = str(stream_event.get("phase", "executing"))
if remote_phase in {"execution_success", "execution_error", "execution_interrupted"}:
for progress_metadata in suppressed_progress_node_metadata.values():
_emit_local_modal_progress(
prompt_id=prompt_id,
client_id=client_id,
node_id=str(progress_metadata["node_id"]),
value=0.0,
max_value=1.0,
display_node_id=progress_metadata["display_node_id"],
real_node_id=progress_metadata["real_node_id"],
clear=True,
)
suppressed_progress_node_metadata.clear()
continue
remote_phase = str(stream_event.get("phase", "executing"))
if remote_phase == "execution_success":
_emit_local_modal_status(
prompt_id=prompt_id,
client_id=client_id,
phase="finalizing",
node_ids=node_ids,
status_message="Receiving Modal outputs",
)
continue
_emit_local_modal_status(
prompt_id=prompt_id,
client_id=client_id,
phase=remote_phase,
node_ids=node_ids,
active_node_id=(
str(stream_event["active_node_id"])
if stream_event.get("active_node_id") is not None
else None
),
active_node_class_type=(
str(stream_event["active_node_class_type"])
if stream_event.get("active_node_class_type") is not None
else None
),
active_node_role=(
str(stream_event["active_node_role"])
if stream_event.get("active_node_role") is not None
else None
),
)
continue
if event_kind == "result":
candidate_outputs = stream_event.get("outputs")
try:
result_payload = coerce_serialized_node_outputs(candidate_outputs)
except TypeError as exc:
raise ModalRemoteInvocationError(
"Modal streamed payload result did not include transport-safe outputs."
) from exc
continue
logger.debug(
"Ignoring unexpected streamed Modal event kind=%s for component=%s.",
event_kind,
payload.get("component_id"),
)
finally:
if active_remote_log_task_id is not None:
_release_remote_container_log_stream(active_remote_log_task_id)
if result_payload is None:
raise ModalRemoteInvocationError(
f"Modal streamed payload for component={payload.get('component_id')!r} did not yield a final result."
)
return bytes(result_payload)
def _mapped_execution_parallelism(item_count: int) -> int:
"""Return the local worker width used to schedule mapped Modal item executions."""
settings = get_settings()
configured_limit = settings.max_containers or _remote_modal_call_worker_count()
return max(1, min(item_count, configured_limit))
def _mapped_phase_definition(payload: dict[str, Any], phase_key: str) -> dict[str, Any] | None:
"""Return one explicit mapped phase definition when queue-time planning provided it."""
phase_payload = payload.get(phase_key)
if isinstance(phase_payload, dict):
return phase_payload
return None
def _shared_subgraph_payload_fields(payload: dict[str, Any]) -> dict[str, Any]:
"""Return the payload fields shared by every explicit mapped phase."""
return {
"prompt_id": payload.get("prompt_id"),
"extra_data": copy.deepcopy(payload.get("extra_data") or {}),
"requires_volume_reload": bool(payload.get("requires_volume_reload", True)),
"volume_reload_marker": payload.get("volume_reload_marker"),
"uploaded_volume_paths": list(payload.get("uploaded_volume_paths", [])),
"terminate_container_on_error": bool(payload.get("terminate_container_on_error", True)),
"custom_nodes_bundle": payload.get("custom_nodes_bundle"),
}
def _build_phase_subgraph_payload(
payload: dict[str, Any],
phase_key: str,
component_id: str,
*,
suppress_status_stream: bool = False,
lane_id: str | None = None,
item_index: int | None = None,
) -> dict[str, Any]:
"""Return one explicit static or mapped subgraph payload."""
phase_definition = _mapped_phase_definition(payload, phase_key)
if phase_definition is None:
raise KeyError(f"Mapped payload is missing phase definition {phase_key!r}.")
phase_payload = {
"payload_kind": "subgraph",
"component_id": component_id,
**_shared_subgraph_payload_fields(payload),
"component_node_ids": [
str(node_id)
for node_id in phase_definition.get("component_node_ids", [])
if str(node_id)
],
"subgraph_prompt": copy.deepcopy(phase_definition.get("subgraph_prompt", {})),
"boundary_inputs": copy.deepcopy(phase_definition.get("boundary_inputs", [])),
"boundary_outputs": copy.deepcopy(phase_definition.get("boundary_outputs", [])),
"execute_node_ids": [
str(node_id)
for node_id in phase_definition.get("execute_node_ids", [])
if str(node_id)
],
}
if suppress_status_stream:
phase_payload["suppress_status_stream"] = True
if lane_id is not None:
phase_payload["mapped_progress_lane_id"] = str(lane_id)
phase_payload["mapped_progress_display_node_id"] = str(
payload.get("component_id", "modal-subgraph")
)
if item_index is not None:
phase_payload["map_item_index"] = int(item_index)
return phase_payload
def _split_phase_outputs(
phase_outputs: tuple[Any, ...],
boundary_outputs: list[dict[str, Any]],
internal_output_names: set[str],
) -> tuple[dict[str, Any], tuple[Any, ...]]:
"""Split one phase result tuple into bridge values and external outputs."""
internal_outputs: dict[str, Any] = {}
external_outputs: list[Any] = []
for boundary_output, output_value in zip(boundary_outputs, phase_outputs, strict=True):
output_name = str(boundary_output.get("proxy_output_name") or "")
if output_name in internal_output_names:
internal_outputs[output_name] = output_value
continue
external_outputs.append(output_value)
return internal_outputs, tuple(external_outputs)
def _execute_mapped_subgraph_payload(
payload: dict[str, Any],
hydrated_inputs: dict[str, Any],
node_mapping: dict[str, type[Any]] | None = None,
) -> tuple[Any, ...]:
"""Execute one mapped payload locally using explicit static and mapped phases."""
mapped_input = payload.get("mapped_input") or {}
mapped_input_name = str(mapped_input.get("proxy_input_name") or "")
if not mapped_input_name:
raise ModalRemoteInvocationError("Mapped remote payloads must define mapped_input.proxy_input_name.")
if mapped_input_name not in hydrated_inputs:
raise KeyError(f"Mapped remote payload input {mapped_input_name!r} was not provided.")
mapped_items = split_mapped_value(
hydrated_inputs[mapped_input_name],
str(mapped_input.get("io_type", "*")),
)
if not mapped_items:
raise ValueError("Mapped remote execution requires at least one input item.")
broadcast_inputs = dict(hydrated_inputs)
broadcast_inputs.pop(mapped_input_name, None)
static_to_mapped_boundaries = list(payload.get("static_to_mapped_boundaries", []))
bridge_output_names = {
str(boundary_spec.get("proxy_name") or "")
for boundary_spec in static_to_mapped_boundaries
if str(boundary_spec.get("proxy_name") or "")
}
static_outputs: tuple[Any, ...] = ()
static_phase_payload: dict[str, Any] | None = None
if payload.get("static_phase") is not None:
static_phase_payload = _build_phase_subgraph_payload(
payload,
"static_phase",
f"{payload.get('component_id', 'modal-subgraph')}::static",
suppress_status_stream=True,
)
elif payload.get("static_execute_node_ids"):
static_phase_payload = _build_static_mapped_payload(payload)
if static_phase_payload is not None:
if static_phase_payload.get("execute_node_ids"):
logger.info(
"Executing static mapped phase for component=%s with execute nodes=%s.",
payload.get("component_id"),
static_phase_payload.get("execute_node_ids", []),
)
static_phase_outputs = _execute_subgraph_prompt(
static_phase_payload,
dict(broadcast_inputs),
node_mapping,
)
bridge_inputs, static_outputs = _split_phase_outputs(
static_phase_outputs,
list(static_phase_payload.get("boundary_outputs", [])),
bridge_output_names,
)
broadcast_inputs.update(bridge_inputs)
total_items = len(mapped_items)
_emit_local_mapped_progress(payload, 0, total_items)
per_item_outputs: list[tuple[Any, ...]] = []
for item_index, item_value in enumerate(mapped_items):
if _local_processing_interrupted():
_raise_local_interrupt()
if payload.get("mapped_phase") is not None:
item_payload = _build_phase_subgraph_payload(
payload,
"mapped_phase",
f"{payload.get('component_id', 'modal-subgraph')}::item:{item_index}",
suppress_status_stream=True,
lane_id="0",
item_index=item_index,
)
else:
item_payload = _build_mapped_item_payload(payload, item_index, 0)
item_inputs = dict(broadcast_inputs)
item_inputs[mapped_input_name] = item_value
logger.info(
"Executing mapped item %d/%d for component=%s with execute nodes=%s.",
item_index + 1,
total_items,
payload.get("component_id"),
item_payload.get("execute_node_ids", []),
)
per_item_outputs.append(
_execute_subgraph_prompt(
item_payload,
item_inputs,
node_mapping,
)
)
_emit_local_mapped_progress(payload, item_index + 1, total_items)
if payload.get("mapped_phase") is not None:
mapped_phase_payload = _build_phase_subgraph_payload(
payload,
"mapped_phase",
f"{payload.get('component_id', 'modal-subgraph')}::mapped",
)
else:
mapped_phase_payload = {
**payload,
"boundary_outputs": [
boundary_output
for boundary_output in payload.get("boundary_outputs", [])
if _is_mapped_boundary_output(boundary_output, payload)
],
}
mapped_outputs = _aggregate_mapped_outputs(
per_item_outputs,
{
**payload,
"boundary_outputs": list(mapped_phase_payload.get("boundary_outputs", [])),
},
)
return _merge_static_and_mapped_outputs(
static_outputs=static_outputs,
mapped_outputs=mapped_outputs,
payload=payload,
)
def _build_mapped_item_payload(
payload: dict[str, Any],
item_index: int,
lane_index: int,
) -> dict[str, Any]:
"""Return one per-item subgraph payload derived from a mapped remote component payload."""
if _mapped_phase_definition(payload, "mapped_phase") is not None:
return _build_phase_subgraph_payload(
payload,
"mapped_phase",
f"{payload.get('component_id', 'modal-subgraph')}::item:{item_index}",
suppress_status_stream=True,
lane_id=str(lane_index),
item_index=item_index,
)
item_payload = copy.deepcopy(payload)
item_payload["payload_kind"] = "subgraph"
item_payload["component_id"] = f"{payload.get('component_id', 'modal-subgraph')}::item:{item_index}"
item_payload["mapped_input"] = None
item_payload["suppress_status_stream"] = True
item_payload["map_item_index"] = item_index
item_payload["mapped_progress_lane_id"] = str(lane_index)
item_payload["mapped_progress_display_node_id"] = str(
payload.get("component_id", "modal-subgraph")
)
item_payload["execute_node_ids"] = list(
payload.get("mapped_execute_node_ids") or payload.get("execute_node_ids", [])
)
item_payload["boundary_outputs"] = [
copy.deepcopy(boundary_output)
for boundary_output in payload.get("boundary_outputs", [])
if _is_mapped_boundary_output(boundary_output, payload)
]
return item_payload
def _aggregate_mapped_outputs(
per_item_outputs: list[tuple[Any, ...]],
payload: dict[str, Any],
) -> tuple[Any, ...]:
"""Reassemble ordered per-item outputs from mapped execution into one proxy result tuple."""
if not per_item_outputs:
raise ValueError("Mapped execution produced no per-item outputs to aggregate.")
output_count = len(per_item_outputs[0])
if any(len(item_outputs) != output_count for item_outputs in per_item_outputs):
raise RemoteSubgraphExecutionError("Mapped remote execution produced inconsistent output arity.")
boundary_outputs = list(payload.get("boundary_outputs", []))
aggregated_outputs: list[Any] = []
for output_index in range(output_count):
boundary_output = boundary_outputs[output_index] if output_index < len(boundary_outputs) else {}
aggregated_outputs.append(
join_mapped_values(
[item_outputs[output_index] for item_outputs in per_item_outputs],
io_type=str(boundary_output.get("io_type", "*")),
is_list=bool(boundary_output.get("is_list", False)),
)
)
return tuple(aggregated_outputs)
def _is_mapped_boundary_output(boundary_output: dict[str, Any], payload: dict[str, Any]) -> bool:
"""Return whether one boundary output belongs to the mapped per-item branch."""
mapped_output = boundary_output.get("mapped_output")
if mapped_output is not None:
return bool(mapped_output)
return bool(payload.get("mapped_input")) and not bool(payload.get("static_execute_node_ids"))
def _build_static_mapped_payload(payload: dict[str, Any]) -> dict[str, Any]:
"""Return the one-time static subgraph payload for a hybrid mapped component."""
if _mapped_phase_definition(payload, "static_phase") is not None:
return _build_phase_subgraph_payload(
payload,
"static_phase",
f"{payload.get('component_id', 'modal-subgraph')}::static",
suppress_status_stream=True,
)
static_payload = copy.deepcopy(payload)
static_payload["payload_kind"] = "subgraph"
static_payload["component_id"] = f"{payload.get('component_id', 'modal-subgraph')}::static"
static_payload["mapped_input"] = None
static_payload["suppress_status_stream"] = True
static_payload["execute_node_ids"] = list(payload.get("static_execute_node_ids") or [])
static_payload["boundary_outputs"] = [
copy.deepcopy(boundary_output)
for boundary_output in payload.get("boundary_outputs", [])
if not _is_mapped_boundary_output(boundary_output, payload)
]
return static_payload
def _merge_static_and_mapped_outputs(
*,
static_outputs: tuple[Any, ...],
mapped_outputs: tuple[Any, ...],
payload: dict[str, Any],
) -> tuple[Any, ...]:
"""Reassemble one hybrid mapped component's static and mapped outputs in original order."""
combined_outputs: list[Any] = []
static_output_index = 0
mapped_output_index = 0
for boundary_output in payload.get("boundary_outputs", []):
if _is_mapped_boundary_output(boundary_output, payload):
if mapped_output_index >= len(mapped_outputs):
raise RemoteSubgraphExecutionError(
"Mapped remote execution returned fewer mapped outputs than expected."
)
combined_outputs.append(mapped_outputs[mapped_output_index])
mapped_output_index += 1
continue
if static_output_index >= len(static_outputs):
raise RemoteSubgraphExecutionError(
"Mapped remote execution returned fewer static outputs than expected."
)
combined_outputs.append(static_outputs[static_output_index])
static_output_index += 1
if static_output_index != len(static_outputs) or mapped_output_index != len(mapped_outputs):
raise RemoteSubgraphExecutionError(
"Mapped remote execution produced extra outputs that did not match the declared boundary outputs."
)
return tuple(combined_outputs)
def _emit_local_mapped_progress(
payload: dict[str, Any],
completed_items: int,
total_items: int,
) -> None:
"""Emit one aggregate mapped-execution progress update for the component representative node."""
prompt_id = str(payload.get("prompt_id")) if payload.get("prompt_id") is not None else None
extra_data = payload.get("extra_data") or {}
client_id = str(extra_data.get("client_id")) if extra_data.get("client_id") is not None else None
display_node_id = str(
payload.get("mapped_progress_display_node_id")
or payload.get("component_id")
or ""
)
if not prompt_id or not client_id or not display_node_id:
return
_emit_local_modal_progress(
prompt_id=prompt_id,
client_id=client_id,
node_id=display_node_id,
value=float(completed_items),
max_value=float(total_items),
display_node_id=display_node_id,
aggregate_only=True,
)
def _mapped_progress_owner_component_id(payload: dict[str, Any]) -> str | None:
"""Return the stable component id used to track one mapped worker lane locally."""
owner_component_id = payload.get("mapped_progress_display_node_id", payload.get("component_id"))
if owner_component_id is None:
return None
owner_component = str(owner_component_id)
return owner_component or None
def _remember_mapped_lane_node_id(payload: dict[str, Any], lane_id: str, node_id: str) -> None:
"""Remember the last real node id that emitted progress for one mapped worker lane."""
prompt_id = str(payload.get("prompt_id")) if payload.get("prompt_id") is not None else None
owner_component_id = _mapped_progress_owner_component_id(payload)
if not prompt_id or not owner_component_id or not node_id:
return
with _MAPPED_PROGRESS_NODE_IDS_LOCK:
_MAPPED_PROGRESS_NODE_IDS[(prompt_id, owner_component_id, lane_id)] = node_id
def _pop_mapped_lane_node_id(payload: dict[str, Any], lane_id: str) -> str | None:
"""Forget and return the last real node id that emitted progress for one mapped worker lane."""
prompt_id = str(payload.get("prompt_id")) if payload.get("prompt_id") is not None else None
owner_component_id = _mapped_progress_owner_component_id(payload)
if not prompt_id or not owner_component_id:
return None
with _MAPPED_PROGRESS_NODE_IDS_LOCK:
return _MAPPED_PROGRESS_NODE_IDS.pop((prompt_id, owner_component_id, lane_id), None)
def _clear_local_mapped_lane_progress(
payload: dict[str, Any],
lane_index: int,
item_index: int,
) -> None:
"""Remove one mapped worker lane from the local node overlay."""
prompt_id = str(payload.get("prompt_id")) if payload.get("prompt_id") is not None else None
extra_data = payload.get("extra_data") or {}
client_id = str(extra_data.get("client_id")) if extra_data.get("client_id") is not None else None
lane_id = str(lane_index)
display_node_id = _pop_mapped_lane_node_id(payload, lane_id) or str(payload.get("component_id") or "")
if not prompt_id or not client_id or not display_node_id:
return
_emit_local_modal_progress(
prompt_id=prompt_id,
client_id=client_id,
node_id=display_node_id,
value=0.0,
max_value=1.0,
display_node_id=display_node_id,
lane_id=lane_id,
clear=True,
item_index=item_index,
)
def _emit_local_mapped_lane_progress_start(
payload: dict[str, Any],
lane_index: int,
item_index: int,
) -> None:
"""Create or reset one mapped worker lane before remote progress begins arriving."""
prompt_id = str(payload.get("prompt_id")) if payload.get("prompt_id") is not None else None
extra_data = payload.get("extra_data") or {}
client_id = str(extra_data.get("client_id")) if extra_data.get("client_id") is not None else None
display_node_id = str(payload.get("component_id") or "")
if not prompt_id or not client_id or not display_node_id:
return
_emit_local_modal_progress(
prompt_id=prompt_id,
client_id=client_id,
node_id=display_node_id,
value=0.0,
max_value=1.0,
display_node_id=display_node_id,
lane_id=str(lane_index),
item_index=item_index,
)
def _split_batch_boundary_inputs(
payload: dict[str, Any],
hydrated_inputs: dict[str, Any],
) -> tuple[dict[str, list[Any]], int] | None:
"""Return zipped per-item boundary inputs when an ordinary subgraph receives batched values."""
implicitly_batchable_scalar_io_types = frozenset({"BOOLEAN", "FLOAT", "INT", "STRING"})
implicitly_batchable_transport_io_types = frozenset({"IMAGE", "LATENT", "MASK", "NOISE", "SIGMAS"})
split_inputs: dict[str, list[Any]] = {}
for boundary_input in payload.get("boundary_inputs", []):
proxy_input_name = str(boundary_input.get("proxy_input_name") or "")
if not proxy_input_name or proxy_input_name not in hydrated_inputs:
continue
io_type = str(boundary_input.get("io_type", "*"))
input_value = hydrated_inputs[proxy_input_name]
if isinstance(input_value, list) and io_type not in (
implicitly_batchable_scalar_io_types | implicitly_batchable_transport_io_types
):
logger.info(
"Skipping implicit batch split for boundary input %s io_type=%s because list-backed non-scalar values stay broadcast.",
proxy_input_name,
io_type,
)
continue
try:
items = split_mapped_value(
input_value,
io_type,
)
except (TypeError, ValueError):
continue
if len(items) <= 1:
continue
split_inputs[proxy_input_name] = items
if not split_inputs:
return None
item_counts = {input_name: len(items) for input_name, items in split_inputs.items()}
unique_counts = set(item_counts.values())
if len(unique_counts) != 1:
raise ModalRemoteInvocationError(
"Implicit Modal batch boundary inputs must all have the same item count. "
f"Received counts: {item_counts!r}"
)
return split_inputs, next(iter(unique_counts))
def _partition_implicit_batched_execute_nodes(
payload: dict[str, Any],
split_inputs: dict[str, list[Any]],
) -> tuple[list[str], list[str]]:
"""Split one implicitly batched subgraph into static and per-item execute targets."""
prompt = payload.get("subgraph_prompt", {})
if not isinstance(prompt, dict):
execute_node_ids = [str(node_id) for node_id in payload.get("execute_node_ids", [])]
return [], execute_node_ids
batched_target_node_ids: set[str] = set()
for boundary_input in payload.get("boundary_inputs", []):
proxy_input_name = str(boundary_input.get("proxy_input_name") or "")
if proxy_input_name not in split_inputs:
continue
for target in boundary_input.get("targets", []):
target_node_id = target.get("node_id")
if target_node_id is not None:
batched_target_node_ids.add(str(target_node_id))
execute_node_ids = [str(node_id) for node_id in payload.get("execute_node_ids", [])]
static_execute_node_ids: list[str] = []
mapped_execute_node_ids: list[str] = []
for execute_node_id in execute_node_ids:
required_node_ids = set(
_resolve_required_subgraph_nodes(
prompt=prompt,
execute_node_ids=[execute_node_id],
)
)
if required_node_ids & batched_target_node_ids:
mapped_execute_node_ids.append(execute_node_id)
continue
static_execute_node_ids.append(execute_node_id)
if not mapped_execute_node_ids and execute_node_ids:
logger.warning(
"Implicitly batched Modal component=%s had batched inputs %s but no execute target depended on them; "
"falling back to per-item execution for all execute nodes.",
payload.get("component_id"),
sorted(split_inputs),
)
return [], execute_node_ids
logger.info(
"Partitioned implicitly batched Modal component=%s into static execute nodes=%s and mapped execute nodes=%s.",
payload.get("component_id"),
static_execute_node_ids,
mapped_execute_node_ids,
)
return static_execute_node_ids, mapped_execute_node_ids
def _annotate_implicit_batched_boundary_outputs(
payload: dict[str, Any],
mapped_execute_node_ids: list[str],
) -> list[dict[str, Any]]:
"""Mark which boundary outputs belong to the per-item branch of an implicitly batched subgraph."""
prompt = payload.get("subgraph_prompt", {})
if not isinstance(prompt, dict) or not mapped_execute_node_ids:
return [copy.deepcopy(boundary_output) for boundary_output in payload.get("boundary_outputs", [])]
mapped_required_node_ids = set(
_resolve_required_subgraph_nodes(
prompt=prompt,
execute_node_ids=[str(node_id) for node_id in mapped_execute_node_ids],
)
)
annotated_outputs: list[dict[str, Any]] = []
for boundary_output in payload.get("boundary_outputs", []):
annotated_output = copy.deepcopy(boundary_output)
annotated_output["mapped_output"] = (
str(boundary_output.get("node_id")) in mapped_required_node_ids
)
annotated_outputs.append(annotated_output)
return annotated_outputs
async def _invoke_implicitly_mapped_subgraph_async(payload: dict[str, Any], kwargs_payload: bytes) -> bytes:
"""Fan out one ordinary subgraph payload when batchable boundary inputs arrive zipped."""
hydrated_inputs = deserialize_node_inputs(kwargs_payload)
split_batch_inputs = _split_batch_boundary_inputs(payload, hydrated_inputs)
if split_batch_inputs is None:
raise ModalRemoteInvocationError(
"Implicit mapped subgraph execution requires at least one batched boundary input."
)
split_inputs, total_items = split_batch_inputs
parallelism = _mapped_execution_parallelism(total_items)
refined_prompt_warmup_target = _register_exact_component_parallelism(payload, parallelism)
ensure_remote_warm_capacity(
_build_prompt_warmup_request(payload),
warmup_target=refined_prompt_warmup_target,
reason="implicit_mapped_component_exact_parallelism",
)
logger.info(
"Scheduling implicitly mapped Modal component=%s for %d item(s) with local parallelism=%d prompt_warmup_target=%d across inputs=%s.",
payload.get("component_id"),
total_items,
parallelism,
refined_prompt_warmup_target,
sorted(split_inputs),
)
_emit_local_mapped_progress(payload, 0, total_items)
broadcast_inputs = {
input_name: value
for input_name, value in hydrated_inputs.items()
if input_name not in split_inputs
}
static_execute_node_ids, mapped_execute_node_ids = _partition_implicit_batched_execute_nodes(
payload,
split_inputs,
)
hybrid_payload = copy.deepcopy(payload)
hybrid_payload["static_execute_node_ids"] = static_execute_node_ids
hybrid_payload["mapped_execute_node_ids"] = mapped_execute_node_ids
hybrid_payload["boundary_outputs"] = _annotate_implicit_batched_boundary_outputs(
payload,
mapped_execute_node_ids,
)
static_outputs: tuple[Any, ...] = ()
if static_execute_node_ids:
static_response = await invoke_remote_engine_async(
_build_static_mapped_payload(hybrid_payload),
serialize_node_inputs(broadcast_inputs),
)
static_outputs = deserialize_node_outputs(static_response)
per_item_outputs: list[tuple[Any, ...] | None] = [None] * total_items
completed_items = 0
item_queue: asyncio.Queue[int | None] = asyncio.Queue()
for item_index in range(total_items):
item_queue.put_nowait(item_index)
for _ in range(parallelism):
item_queue.put_nowait(None)
async def run_worker(lane_index: int) -> None:
"""Execute queued implicit mapped items through one stable local worker lane."""
nonlocal completed_items
while True:
item_index = await item_queue.get()
if item_index is None:
return
if _local_processing_interrupted():
_raise_local_interrupt()
try:
item_payload = _build_mapped_item_payload(hybrid_payload, item_index, lane_index)
item_inputs = dict(broadcast_inputs)
for input_name, items in split_inputs.items():
item_inputs[input_name] = items[item_index]
item_response = await invoke_remote_engine_async(
item_payload,
serialize_node_inputs(item_inputs),
)
per_item_outputs[item_index] = deserialize_node_outputs(item_response)
completed_items += 1
_emit_local_mapped_progress(payload, completed_items, total_items)
finally:
_clear_local_mapped_lane_progress(payload, lane_index, item_index)
tasks = [asyncio.create_task(run_worker(lane_index)) for lane_index in range(parallelism)]
try:
await asyncio.gather(*tasks)
except Exception:
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
raise
return serialize_node_outputs(
_merge_static_and_mapped_outputs(
static_outputs=static_outputs,
mapped_outputs=_aggregate_mapped_outputs(
[item_outputs for item_outputs in per_item_outputs if item_outputs is not None],
{
**hybrid_payload,
"boundary_outputs": [
boundary_output
for boundary_output in hybrid_payload.get("boundary_outputs", [])
if _is_mapped_boundary_output(boundary_output, hybrid_payload)
],
},
),
payload=hybrid_payload,
)
)
async def _invoke_mapped_remote_engine_async(payload: dict[str, Any], kwargs_payload: bytes) -> bytes:
"""Execute one mapped payload locally using explicit static and mapped phases."""
hydrated_inputs = deserialize_node_inputs(kwargs_payload)
return await asyncio.to_thread(
lambda: serialize_node_outputs(
_execute_mapped_subgraph_payload(
payload,
hydrated_inputs,
)
)
)
def _lookup_deployed_remote_engine(payload: dict[str, Any]) -> Any:
"""Look up the deployed Modal runtime class instance."""
if modal is None:
raise ModalRemoteInvocationError("Modal SDK is unavailable.")
settings = get_settings()
affinity_key = _remote_session_affinity_key(payload)
logger.info(
"Attempting deployed Modal invocation for app=%s class=%s component=%s session_affinity=%s.",
settings.app_name,
"RemoteEngine",
payload.get("component_id"),
affinity_key,
)
remote_cls = modal.Cls.from_name(settings.app_name, "RemoteEngine")
if affinity_key is not None:
return remote_cls(session_affinity_key=affinity_key)
return remote_cls()
def _modal_environment_name() -> str | None:
"""Return the active Modal environment name when explicitly configured."""
environment_name = os.getenv("MODAL_ENVIRONMENT")
if environment_name is None:
return None
normalized = environment_name.strip()
return normalized or None
def _modal_deploy_cache_key() -> tuple[str, str | None]:
"""Return the cache key for auto-deployed Modal apps."""
settings = get_settings()
return (settings.app_name, _modal_environment_name())
def _warmup_prompt_id(warmup_request: dict[str, Any]) -> str | None:
"""Return the prompt id that scopes proactive warmup state."""
prompt_id = warmup_request.get("prompt_id")
if prompt_id is None:
return None
normalized_prompt_id = str(prompt_id).strip()
return normalized_prompt_id or None
def _ensure_prompt_warmup_state(prompt_id: str) -> _PromptWarmupState:
"""Return the cached warmup state bucket for one prompt."""
global _PROMPT_WARMUP_STATE_ORDER
state = _PROMPT_WARMUP_STATES.get(prompt_id)
if state is not None:
return state
state = _PromptWarmupState()
_PROMPT_WARMUP_STATES[prompt_id] = state
if _PROMPT_WARMUP_STATE_ORDER is None:
_PROMPT_WARMUP_STATE_ORDER = queue.SimpleQueue()
_PROMPT_WARMUP_STATE_ORDER.put(prompt_id)
while len(_PROMPT_WARMUP_STATES) > _PROMPT_WARMUP_STATE_CACHE_LIMIT:
expired_prompt_id = _PROMPT_WARMUP_STATE_ORDER.get()
_PROMPT_WARMUP_STATES.pop(expired_prompt_id, None)
return state
def _clamp_prompt_warmup_target(warmup_target: int) -> int:
"""Clamp one proactive warmup target to the configured Modal container cap."""
normalized_target = max(0, int(warmup_target))
max_containers = get_settings().max_containers
if max_containers is not None:
return min(normalized_target, max_containers)
return normalized_target
def _build_prompt_warmup_request(payload: dict[str, Any]) -> dict[str, Any]:
"""Extract the prompt-scoped warmup-relevant fields from one payload."""
return {
"prompt_id": (
str(payload.get("prompt_id"))
if payload.get("prompt_id") is not None
else None
),
"component_id": str(
payload.get("mapped_progress_display_node_id", payload.get("component_id", "modal-warmup"))
),
"requires_volume_reload": bool(payload.get("requires_volume_reload", True)),
"volume_reload_marker": payload.get("volume_reload_marker"),
"uploaded_volume_paths": list(payload.get("uploaded_volume_paths", [])),
"custom_nodes_bundle": payload.get("custom_nodes_bundle"),
}
def _component_parallelism_metadata(payload: dict[str, Any]) -> dict[str, Any]:
"""Return the prompt-level parallelism metadata attached by the queue rewrite."""
extra_data = payload.get("extra_data") or {}
modal_metadata = extra_data.get("modal") or {}
if not isinstance(modal_metadata, dict):
return {}
return modal_metadata
def _prompt_parallelism_target(
payload: dict[str, Any],
*,
exact_component_parallelism: dict[str, int] | None = None,
) -> int:
"""Return the current best-effort prompt-wide warmup target for one payload."""
metadata = _component_parallelism_metadata(payload)
execution_stages = metadata.get("component_execution_stages")
if not isinstance(execution_stages, list):
return 1
mapped_component_ids = {
str(component_id)
for component_id in metadata.get("mapped_component_ids", [])
if str(component_id)
}
exact_parallelism = exact_component_parallelism or {}
stage_parallelism = 0
for stage in execution_stages:
if not isinstance(stage, list):
continue
current_stage_parallelism = 0
for component_id_value in stage:
component_id = str(component_id_value)
if component_id in exact_parallelism:
current_stage_parallelism += max(1, int(exact_parallelism[component_id]))
elif component_id in mapped_component_ids:
current_stage_parallelism += 1
else:
current_stage_parallelism += 1
stage_parallelism = max(stage_parallelism, current_stage_parallelism)
fallback_parallelism = metadata.get("estimated_max_parallel_requests")
if stage_parallelism <= 0 and isinstance(fallback_parallelism, int):
stage_parallelism = max(stage_parallelism, fallback_parallelism)
return _clamp_prompt_warmup_target(max(1, stage_parallelism))
def _register_exact_component_parallelism(payload: dict[str, Any], component_parallelism: int) -> int:
"""Record exact mapped-component parallelism and return the refined prompt-wide warmup target."""
prompt_id = str(payload.get("prompt_id")) if payload.get("prompt_id") is not None else None
component_id = str(payload.get("component_id")) if payload.get("component_id") is not None else None
if not prompt_id or not component_id:
return _clamp_prompt_warmup_target(component_parallelism)
with _PROMPT_WARMUP_STATES_LOCK:
warmup_state = _ensure_prompt_warmup_state(prompt_id)
warmup_state.exact_component_parallelism[component_id] = max(1, int(component_parallelism))
exact_parallelism = dict(warmup_state.exact_component_parallelism)
return _prompt_parallelism_target(payload, exact_component_parallelism=exact_parallelism)
def _warmup_slot_payload(warmup_request: dict[str, Any], slot_index: int) -> dict[str, Any]:
"""Return the remote warmup payload for one desired container slot."""
slot_payload = copy.deepcopy(warmup_request)
component_id = str(slot_payload.get("component_id") or slot_payload.get("prompt_id") or "modal-warmup")
slot_payload["component_id"] = f"{component_id}::warmup:{slot_index}"
slot_payload["warmup_slot_index"] = int(slot_index)
slot_payload["warmup_only"] = True
return slot_payload
def _invoke_remote_engine_warmup(remote_engine: Any, warmup_request: dict[str, Any]) -> Any:
"""Ask one prepared remote engine instance to warm a container for a prompt."""
warmup_method = getattr(remote_engine, "warmup_for_request", None)
if warmup_method is None:
logger.warning(
"Remote warmup method is unavailable for prompt=%s component=%s; skipping proactive warmup.",
warmup_request.get("prompt_id"),
warmup_request.get("component_id"),
)
return None
if hasattr(warmup_method, "remote"):
return warmup_method.remote(warmup_request)
return warmup_method(warmup_request)
def _invoke_modal_warmup_blocking(warmup_request: dict[str, Any]) -> Any:
"""Warm one Modal container slot using deployed or ephemeral app state."""
if modal is None:
return None
lookup_error_types = _modal_lookup_error_types()
settings = get_settings()
if lookup_error_types:
try:
remote_engine = _lookup_deployed_remote_engine(warmup_request)
return _invoke_remote_engine_warmup(remote_engine, warmup_request)
except lookup_error_types as exc:
if settings.auto_deploy:
_auto_deploy_modal_app(warmup_request, exc)
try:
remote_engine = _lookup_deployed_remote_engine(warmup_request)
return _invoke_remote_engine_warmup(remote_engine, warmup_request)
except lookup_error_types as retry_exc:
exc = retry_exc
if not settings.allow_ephemeral_fallback:
raise ModalRemoteInvocationError(
"Proactive Modal warmup requires a deployed Modal app or a successful first-run auto-deploy. "
f"Lookup failed for app={settings.app_name!r}: {exc}."
) from exc
else:
remote_engine = _lookup_deployed_remote_engine(warmup_request)
return _invoke_remote_engine_warmup(remote_engine, warmup_request)
cloud_module = _load_modal_cloud_module()
cloud_app = getattr(cloud_module, "app", None)
cloud_remote_engine = getattr(cloud_module, "RemoteEngine", None)
if cloud_app is None or cloud_remote_engine is None:
raise ModalRemoteInvocationError(
"Stable Modal cloud entry module did not expose app and RemoteEngine."
)
run_context = cloud_app.run() if hasattr(cloud_app, "run") else nullcontext()
with run_context:
remote_engine = cloud_remote_engine()
return _invoke_remote_engine_warmup(remote_engine, warmup_request)
def _run_prompt_warmup_slot(
prompt_id: str,
slot_index: int,
warmup_request: dict[str, Any],
reason: str,
) -> None:
"""Execute one proactive warmup slot and release it for retry on failure."""
try:
logger.info(
"Starting proactive Modal warmup for prompt=%s slot=%d component=%s reason=%s.",
prompt_id,
slot_index,
warmup_request.get("component_id"),
reason,
)
_invoke_modal_warmup_blocking(_warmup_slot_payload(warmup_request, slot_index))
except Exception:
with _PROMPT_WARMUP_STATES_LOCK:
warmup_state = _PROMPT_WARMUP_STATES.get(prompt_id)
if warmup_state is not None:
warmup_state.scheduled_slots.discard(slot_index)
logger.exception(
"Proactive Modal warmup failed for prompt=%s slot=%d component=%s.",
prompt_id,
slot_index,
warmup_request.get("component_id"),
)
def ensure_remote_warm_capacity(
warmup_request: dict[str, Any],
*,
warmup_target: int,
reason: str,
) -> int:
"""Best-effort background warmup so enough Modal containers are ready for one prompt."""
settings = get_settings()
if not settings.enable_proactive_warmup:
return 0
if settings.execution_mode == "local" or modal is None:
return 0
prompt_id = _warmup_prompt_id(warmup_request)
if prompt_id is None:
return 0
clamped_target = _clamp_prompt_warmup_target(warmup_target)
if clamped_target <= 0:
return 0
with _PROMPT_WARMUP_STATES_LOCK:
warmup_state = _ensure_prompt_warmup_state(prompt_id)
missing_slots = [
slot_index
for slot_index in range(clamped_target)
if slot_index not in warmup_state.scheduled_slots
]
for slot_index in missing_slots:
warmup_state.scheduled_slots.add(slot_index)
if not missing_slots:
return clamped_target
logger.info(
"Scheduling proactive Modal warmup for prompt=%s target=%d missing_slots=%s component=%s reason=%s.",
prompt_id,
clamped_target,
missing_slots,
warmup_request.get("component_id"),
reason,
)
for slot_index in missing_slots:
_REMOTE_MODAL_WARMUP_EXECUTOR.submit(
_run_prompt_warmup_slot,
prompt_id,
slot_index,
copy.deepcopy(warmup_request),
reason,
)
return clamped_target
def _auto_deploy_modal_app(payload: dict[str, Any], lookup_error: BaseException) -> None:
"""Deploy the stable Modal cloud app once when deployed lookup fails."""
if modal is None:
raise ModalRemoteInvocationError("Modal SDK is unavailable.")
settings = get_settings()
deploy_key = _modal_deploy_cache_key()
cloud_module = _load_modal_cloud_module()
cloud_app = getattr(cloud_module, "app", None)
if cloud_app is None:
raise ModalRemoteInvocationError(
"Stable Modal cloud entry module did not expose a deployable app."
)
with _MODAL_AUTO_DEPLOY_LOCK:
if deploy_key in _MODAL_AUTO_DEPLOYED_APPS:
logger.info(
"Auto-deploy already completed for app=%s env=%s; reusing cached deployment state.",
settings.app_name,
deploy_key[1] or "<default>",
)
return
logger.warning(
"Deployed Modal app lookup failed for app=%s component=%s: %s. "
"Attempting first-run auto-deploy from the custom node.",
settings.app_name,
payload.get("component_id"),
lookup_error,
)
output_context = modal.enable_output() if hasattr(modal, "enable_output") else nullcontext()
deploy_started_at = time.perf_counter()
with output_context:
cloud_app.deploy(
name=settings.app_name,
environment_name=deploy_key[1],
)
_MODAL_AUTO_DEPLOYED_APPS.add(deploy_key)
logger.info(
"Auto-deployed Modal app %s for env=%s in %.3fs.",
settings.app_name,
deploy_key[1] or "<default>",
time.perf_counter() - deploy_started_at,
)
def _build_remote_interrupt_callback(_remote_engine: Any, payload: dict[str, Any]) -> Callable[[], Any] | None:
"""Return a callable that requests interruption for one active Modal payload."""
interrupt_store = _lookup_modal_interrupt_store()
if interrupt_store is None:
return None
return lambda: _request_remote_interrupt(payload)
def _invoke_remote_engine_payload(
remote_engine: Any,
payload: dict[str, Any],
kwargs_payload: bytes,
cancellation_event: threading.Event | None,
) -> bytes:
"""Invoke one prepared remote engine instance with optional progress streaming."""
stream_method = getattr(remote_engine, "execute_payload_stream", None)
interrupt_remote_call = _build_remote_interrupt_callback(remote_engine, payload)
if _should_stream_remote_progress(payload) and hasattr(stream_method, "remote_gen"):
logger.info(
"Using streamed Modal progress path for component=%s via execute_payload_stream.remote_gen(...).",
payload.get("component_id"),
)
return _invoke_remote_call_with_interrupts(
payload=payload,
invoke_remote_call=lambda: _consume_remote_payload_stream(
payload,
stream_method.remote_gen(payload, kwargs_payload),
),
interrupt_remote_call=interrupt_remote_call,
cancellation_event=cancellation_event,
)
if _should_stream_remote_progress(payload):
logger.warning(
"Streamed Modal progress is unavailable for component=%s; falling back to execute_payload.remote(...).",
payload.get("component_id"),
)
return _invoke_remote_call_with_interrupts(
payload=payload,
invoke_remote_call=lambda: remote_engine.execute_payload.remote(payload, kwargs_payload),
interrupt_remote_call=interrupt_remote_call,
cancellation_event=cancellation_event,
)
def _invoke_modal_payload_blocking(
payload: dict[str, Any],
kwargs_payload: bytes,
cancellation_event: threading.Event | None = None,
) -> bytes:
"""Invoke the Modal runtime from a worker thread using deployed or ephemeral app state."""
if modal is None:
raise ModalRemoteInvocationError("Modal SDK is unavailable.")
lookup_error_types = _modal_lookup_error_types()
settings = get_settings()
if lookup_error_types:
try:
remote_engine = _lookup_deployed_remote_engine(payload)
logger.info(
"Using deployed Modal app %s for component %s.",
settings.app_name,
payload.get("component_id"),
)
return _invoke_remote_engine_payload(
remote_engine,
payload,
kwargs_payload,
cancellation_event,
)
except lookup_error_types as exc:
if settings.auto_deploy:
_auto_deploy_modal_app(payload, exc)
try:
remote_engine = _lookup_deployed_remote_engine(payload)
logger.info(
"Using auto-deployed Modal app %s for component %s.",
settings.app_name,
payload.get("component_id"),
)
return _invoke_remote_engine_payload(
remote_engine,
payload,
kwargs_payload,
cancellation_event,
)
except lookup_error_types as retry_exc:
exc = retry_exc
if not settings.allow_ephemeral_fallback:
raise ModalRemoteInvocationError(
"Remote execution requires a deployed Modal app or a successful first-run auto-deploy. "
f"Lookup failed for app={settings.app_name!r} component={payload.get('component_id')!r}: {exc}. "
"Ensure Modal credentials are configured so the custom node can auto-deploy, "
"or set COMFY_MODAL_ALLOW_EPHEMERAL_FALLBACK=true to allow slow ephemeral app.run() fallback behavior."
) from exc
logger.warning(
"Deployed Modal app lookup failed for app=%s component=%s: %s. Falling back to ephemeral app.run(); this creates a temporary Modal app session, not a persistent deployment or endpoint.",
settings.app_name,
payload.get("component_id"),
exc,
)
else:
remote_engine = _lookup_deployed_remote_engine(payload)
logger.info(
"Using deployed Modal app %s for component %s.",
settings.app_name,
payload.get("component_id"),
)
return _invoke_remote_engine_payload(
remote_engine,
payload,
kwargs_payload,
cancellation_event,
)
if "app" not in globals() or "RemoteEngine" not in globals():
logger.debug("Local module Modal runtime objects are unavailable; loading stable cloud entry module.")
cloud_module = _load_modal_cloud_module()
cloud_app = getattr(cloud_module, "app", None)
cloud_remote_engine = getattr(cloud_module, "RemoteEngine", None)
if cloud_app is None or cloud_remote_engine is None:
raise ModalRemoteInvocationError(
"Stable Modal cloud entry module did not expose app and RemoteEngine."
)
logger.info(
"Starting ephemeral Modal app.run() for component %s. This does not create a persistent deployed app or web endpoint.",
payload.get("component_id"),
)
run_context = cloud_app.run() if hasattr(cloud_app, "run") else nullcontext()
with run_context:
remote_engine = cloud_remote_engine()
result = _invoke_remote_engine_payload(
remote_engine,
payload,
kwargs_payload,
cancellation_event,
)
logger.info(
"Ephemeral Modal app.run() invocation completed for component %s.",
payload.get("component_id"),
)
return result
def invoke_remote_engine(payload: dict[str, Any], kwargs_payload: bytes) -> bytes:
"""Invoke Modal when configured, or fall back to local in-process execution."""
execution_mode = os.getenv("COMFY_MODAL_EXECUTION_MODE", "local")
if payload.get("payload_kind") == "mapped_subgraph":
if execution_mode == "local" or modal is None:
hydrated_inputs = deserialize_node_inputs(kwargs_payload)
return serialize_node_outputs(
_execute_mapped_subgraph_payload(payload, hydrated_inputs)
)
if payload.get("payload_kind") == "subgraph":
hydrated_inputs = deserialize_node_inputs(kwargs_payload)
if _split_batch_boundary_inputs(payload, hydrated_inputs) is not None:
return asyncio.run(_invoke_implicitly_mapped_subgraph_async(payload, kwargs_payload))
if execution_mode == "local" or modal is None:
if payload.get("payload_kind") == "subgraph":
return execute_subgraph_locally(payload, kwargs_payload)
return execute_node_locally(payload, kwargs_payload)
logger.info(
"Dispatching Modal remote invocation for component=%s payload_kind=%s.",
payload.get("component_id"),
payload.get("payload_kind"),
)
cancellation_event = threading.Event()
future = _REMOTE_MODAL_CALL_EXECUTOR.submit(
_invoke_modal_payload_blocking,
dict(payload),
kwargs_payload,
cancellation_event,
)
try:
while True:
try:
response = future.result(timeout=0.1)
break
except FutureTimeoutError:
_sync_local_interrupt_to_cancellation_event(payload, cancellation_event)
continue
except Exception:
if cancellation_event.is_set() or _local_processing_interrupted():
logger.info(
"Reraising Modal failure as a local interrupt for component=%s after cancellation.",
payload.get("component_id"),
)
_raise_local_interrupt()
logger.exception(
"Modal remote invocation failed for component=%s.",
payload.get("component_id"),
)
raise
if cancellation_event.is_set() or _local_processing_interrupted():
logger.info(
"Remote invocation for component=%s finished after interruption; raising local interrupt.",
payload.get("component_id"),
)
_raise_local_interrupt()
logger.info(
"Modal remote invocation completed for component=%s.",
payload.get("component_id"),
)
return response
async def invoke_remote_engine_async(payload: dict[str, Any], kwargs_payload: bytes) -> bytes:
"""Invoke Modal asynchronously so multiple proxy nodes can wait on remote work in parallel."""
execution_mode = os.getenv("COMFY_MODAL_EXECUTION_MODE", "local")
if payload.get("payload_kind") == "mapped_subgraph":
if execution_mode == "local" or modal is None:
return await _invoke_mapped_remote_engine_async(payload, kwargs_payload)
if payload.get("payload_kind") == "subgraph":
hydrated_inputs = deserialize_node_inputs(kwargs_payload)
if _split_batch_boundary_inputs(payload, hydrated_inputs) is not None:
return await _invoke_implicitly_mapped_subgraph_async(payload, kwargs_payload)
if execution_mode == "local" or modal is None:
return await asyncio.to_thread(invoke_remote_engine, payload, kwargs_payload)
logger.info(
"Dispatching async Modal remote invocation for component=%s payload_kind=%s.",
payload.get("component_id"),
payload.get("payload_kind"),
)
cancellation_event = threading.Event()
future = _REMOTE_MODAL_CALL_EXECUTOR.submit(
_invoke_modal_payload_blocking,
dict(payload),
kwargs_payload,
cancellation_event,
)
wrapped_future = asyncio.wrap_future(future)
try:
while True:
try:
response = await asyncio.wait_for(asyncio.shield(wrapped_future), timeout=0.1)
break
except asyncio.TimeoutError:
_sync_local_interrupt_to_cancellation_event(payload, cancellation_event)
continue
except asyncio.CancelledError:
cancellation_event.set()
raise
except Exception:
if cancellation_event.is_set() or _local_processing_interrupted():
logger.info(
"Reraising async Modal failure as a local interrupt for component=%s after cancellation.",
payload.get("component_id"),
)
_raise_local_interrupt()
logger.exception(
"Async Modal remote invocation failed for component=%s.",
payload.get("component_id"),
)
raise
if cancellation_event.is_set() or _local_processing_interrupted():
logger.info(
"Async remote invocation for component=%s finished after interruption; raising local interrupt.",
payload.get("component_id"),
)
_raise_local_interrupt()
logger.info(
"Async Modal remote invocation completed for component=%s.",
payload.get("component_id"),
)
return response
if modal is not None: # pragma: no branch - simple import-time configuration.
settings = get_settings()
app = modal.App(settings.app_name)
vol = modal.Volume.from_name(settings.volume_name, create_if_missing=True)
image = modal.Image.debian_slim().pip_install("torch", "safetensors", "pillow", "numpy")
@app.cls(
gpu="A100",
volumes={settings.remote_storage_root: vol},
scaledown_window=60,
image=image,
)
class RemoteEngine:
"""Modal runtime class that executes proxied ComfyUI payloads."""
session_affinity_key: str = modal.parameter(default="")
@modal.enter()
def setup(self) -> None:
"""Prepare the container process for headless node execution."""
logger.info(
"RemoteEngine setup complete for session_affinity_key=%s.",
self.session_affinity_key or None,
)
@modal.method()
def execute_payload(self, payload: dict[str, Any], kwargs_payload: bytes) -> bytes:
"""Execute a proxied node or subgraph inside the Modal container."""
if payload.get("payload_kind") == "mapped_subgraph":
hydrated_inputs = deserialize_node_inputs(kwargs_payload)
return serialize_node_outputs(
_execute_mapped_subgraph_payload(payload, hydrated_inputs)
)
if payload.get("payload_kind") == "subgraph":
return execute_subgraph_locally(payload, kwargs_payload)
return execute_node_locally(payload, kwargs_payload)
@modal.method()
def warmup_for_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""No-op local warmup entrypoint for the simplified Modal runtime."""
return {"component_id": str(payload.get("component_id") or "modal-warmup")}
else:
class RemoteEngine:
"""Local fallback runtime used when the Modal SDK is unavailable."""
def __init__(self, session_affinity_key: str | None = None) -> None:
"""Record the optional session-affinity key for split-proxy reuse."""
self.session_affinity_key = session_affinity_key
def setup(self) -> None:
"""No-op setup for local fallback execution."""
def execute_payload(self, payload: dict[str, Any], kwargs_payload: bytes) -> bytes:
"""Execute the proxied node or subgraph locally."""
if payload.get("payload_kind") == "mapped_subgraph":
hydrated_inputs = deserialize_node_inputs(kwargs_payload)
return serialize_node_outputs(
_execute_mapped_subgraph_payload(payload, hydrated_inputs)
)
if payload.get("payload_kind") == "subgraph":
return execute_subgraph_locally(payload, kwargs_payload)
return execute_node_locally(payload, kwargs_payload)
def warmup_for_request(self, payload: dict[str, Any]) -> dict[str, Any]:
"""Return a local no-op warmup result when Modal is unavailable."""
return {"component_id": str(payload.get("component_id") or "modal-warmup")}