8892 lines
337 KiB
Python
8892 lines
337 KiB
Python
"""Remote Modal runtime and local execution fallback."""
|
|
|
|
import asyncio
|
|
import copy
|
|
from dataclasses import dataclass, field, replace
|
|
from datetime import datetime, timedelta, timezone
|
|
from decimal import Decimal, InvalidOperation
|
|
import hashlib
|
|
import importlib
|
|
import importlib.util
|
|
import inspect
|
|
from io import BytesIO
|
|
import json
|
|
import logging
|
|
import os
|
|
import queue
|
|
import select
|
|
import shutil
|
|
import socket
|
|
import statistics
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
from types import ModuleType
|
|
import uuid
|
|
import zipfile
|
|
from concurrent.futures import (
|
|
Future,
|
|
ThreadPoolExecutor,
|
|
TimeoutError as FutureTimeoutError,
|
|
)
|
|
from contextlib import contextmanager, nullcontext
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
from typing import Any, Callable, Iterable, Iterator, Mapping
|
|
|
|
from ..serialization import (
|
|
coerce_serialized_node_outputs,
|
|
deserialize_value,
|
|
deserialize_node_inputs,
|
|
deserialize_node_outputs,
|
|
is_mapped_output_value,
|
|
join_mapped_values_for_scheduler,
|
|
serialize_mapping,
|
|
serialize_node_inputs,
|
|
serialize_node_outputs,
|
|
serialize_value,
|
|
split_mapped_value,
|
|
unwrap_mapped_output_value,
|
|
)
|
|
from ..durable_state import (
|
|
DurableObjectRef,
|
|
DurableStateError,
|
|
FileDurableObjectStore,
|
|
read_modal_volume_file,
|
|
stable_remote_invocation_id,
|
|
)
|
|
from ..output_artifacts import (
|
|
RemoteExecutionResult,
|
|
materialize_remote_output_artifacts,
|
|
unpack_remote_execution_result,
|
|
)
|
|
from ..session_state import (
|
|
InMemoryRemoteSessionBridgeStore,
|
|
InMemoryRemoteSessionStore,
|
|
RemoteSessionBridgeRecord,
|
|
RemoteSessionBridgeRecoveryKind,
|
|
RemoteSessionBridgeRef,
|
|
RemoteSessionHandle,
|
|
RemoteSessionStateError,
|
|
RemoteSessionValueRef,
|
|
is_remote_session_bridge_ref_payload,
|
|
is_remote_session_handle_payload,
|
|
is_remote_session_value_ref_payload,
|
|
stable_session_bridge_key,
|
|
)
|
|
from ..runtime_environment import (
|
|
REMOTE_APP_PROTOCOL_VERSION,
|
|
RemoteRuntimeIdentity,
|
|
build_remote_runtime_identity,
|
|
)
|
|
from ..llm_profiles import (
|
|
get_llm_profile,
|
|
llm_model_references_from_payload,
|
|
rewrite_llm_model_references,
|
|
)
|
|
from ..llm_recovery import (
|
|
LLM_FORCE_VLLM_THROUGHPUT_PAYLOAD_KEY,
|
|
exhausted_recovery_used_vllm_throughput,
|
|
is_llm_memory_recovery_exhausted,
|
|
)
|
|
from ..settings import (
|
|
MODAL_GPU_TYPES,
|
|
ModalSyncSettings,
|
|
get_settings,
|
|
modal_deployment_app_name,
|
|
settings_for_modal_gpu,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
_MODAL_CLOUD_MODULE_NAME = "comfyui_modal_sync_cloud"
|
|
_REMOTE_APP_PROTOCOL_VERSION = REMOTE_APP_PROTOCOL_VERSION
|
|
_MODAL_CLOUD_MODULE_LOCK = threading.Lock()
|
|
_MODAL_CLOUD_SETTINGS_STATE = threading.local()
|
|
_MODAL_AUTO_DEPLOY_LOCK = threading.Lock()
|
|
_MODAL_AUTO_DEPLOY_STATES: dict[tuple[str, str | None], "_ModalAutoDeployState"] = {}
|
|
_MODAL_REMOTE_APP_VERSION_OK: set[tuple[str, str | None, str]] = set()
|
|
_STAGED_LLM_PROFILES_LOCK = threading.Lock()
|
|
_STAGED_LLM_PROFILES: set[tuple[str, str, str]] = set()
|
|
_STAGED_LLM_PROFILE_RESULTS: dict[tuple[str, str], dict[str, Any]] = {}
|
|
_MODAL_APP_STOP_TIMEOUT_SECONDS = 120.0
|
|
_MODAL_INTERRUPT_DICTS_LOCK = threading.Lock()
|
|
_MODAL_INTERRUPT_DICTS: dict[tuple[str, str | None], Any] = {}
|
|
_MODAL_SESSION_BRIDGE_DICTS_LOCK = threading.Lock()
|
|
_MODAL_SESSION_BRIDGE_DICTS: dict[tuple[str, str | None], Any] = {}
|
|
_MODAL_DURABLE_VOLUMES_LOCK = threading.Lock()
|
|
_MODAL_DURABLE_VOLUMES: 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
|
|
_LOCAL_GAP_KEEPALIVES_LOCK = threading.Lock()
|
|
_LOCAL_GAP_KEEPALIVES: dict[tuple[str, int], "_LocalGapKeepaliveState"] = {}
|
|
_SNAPSHOT_PROFILE_RECORDS_LOCK = threading.Lock()
|
|
_SNAPSHOT_PROFILE_RECORDS: dict[str, dict[str, Any]] = {}
|
|
_PRIMITIVE_WIDGET_INPUT_TYPES = frozenset({"INT", "FLOAT", "BOOLEAN", "STRING"})
|
|
_ROOT_LOADER_PREWARM_CLASS_TYPES = frozenset(
|
|
{
|
|
"CheckpointLoaderSimple",
|
|
"UNETLoader",
|
|
"CLIPLoader",
|
|
"DualCLIPLoader",
|
|
}
|
|
)
|
|
_SPECULATIVE_PREWARM_TARGET_KEY = "speculative_remote_prewarm_target"
|
|
_BOUNDARY_INPUT_SIGNATURES_KEY = "__comfy_modal_boundary_input_signatures__"
|
|
_REMOTE_CONTAINER_LOG_STREAMS_LOCK = threading.Lock()
|
|
_REMOTE_CONTAINER_LOG_STREAMS: dict[str, "_RemoteContainerLogStreamState"] = {}
|
|
_REMOTE_CONTAINER_LOG_STDERR_LOCK = threading.Lock()
|
|
_REMOTE_CONTAINER_LOG_STREAM_IDLE_GRACE_SECONDS = 30.0
|
|
_MODAL_BILLING_COLLECTION_BUFFER = timedelta(minutes=10)
|
|
_MODAL_BILLING_INTERVAL = timedelta(hours=1)
|
|
_MODAL_HOURLY_BILLING_CACHE_LOCK = threading.Lock()
|
|
_MODAL_HOURLY_BILLING_CACHE: dict[
|
|
tuple[str, str, datetime], "ModalHourlyBillingStatus"
|
|
] = {}
|
|
_MODAL_HOURLY_BILLING_ERROR_CACHE: dict[tuple[str, str, datetime], str] = {}
|
|
_MODAL_HOURLY_BILLING_CACHE_LIMIT = 64
|
|
_IMPLICIT_BATCH_PRESERVING_TARGETS = frozenset({("CreateVideo", "images")})
|
|
_ACTIVE_REMOTE_INVOCATIONS_LOCK = threading.Lock()
|
|
_ACTIVE_REMOTE_INVOCATIONS_BY_PROMPT: dict[
|
|
str, dict[str, "_ActiveRemoteInvocation"]
|
|
] = {}
|
|
_REMOTE_SESSION_STORE = InMemoryRemoteSessionStore()
|
|
_REMOTE_SESSION_BRIDGE_STORE = InMemoryRemoteSessionBridgeStore()
|
|
_DURABLE_OBJECT_STORE_LOCK = threading.Lock()
|
|
_DURABLE_OBJECT_STORE: FileDurableObjectStore | None = None
|
|
_REMOTE_SESSION_BRIDGE_REPLAY_STATE = threading.local()
|
|
_REMOTE_SESSION_BRIDGE_VALUE_CACHE_LOCK = threading.Lock()
|
|
_REMOTE_SESSION_BRIDGE_VALUE_CACHE: dict[str, Any] = {}
|
|
_REMOTE_SESSION_BRIDGE_VALUE_CACHE_ORDER: list[str] = []
|
|
_REMOTE_SESSION_BRIDGE_VALUE_CACHE_LIMIT = 32
|
|
MODAL_GPU_PRICING_EFFECTIVE_DATE = "2026-08-13"
|
|
MODAL_GPU_ESTIMATED_USD_PER_SECOND: dict[str, float] = {
|
|
"T4": 0.000164,
|
|
"L4": 0.000222,
|
|
"A10": 0.000306,
|
|
"L40S": 0.000542,
|
|
"A100": 0.000583,
|
|
"A100-40GB": 0.000583,
|
|
"A100-80GB": 0.000694,
|
|
"RTX-PRO-6000": 0.000842,
|
|
"H100": 0.001097,
|
|
"H100!": 0.001097,
|
|
"H200": 0.001261,
|
|
"B200": 0.001736,
|
|
"B200+": 0.001736,
|
|
"B300": 0.001972,
|
|
}
|
|
|
|
|
|
def _settings_for_payload(payload: Mapping[str, Any]) -> ModalSyncSettings:
|
|
"""Resolve settings with the workflow-selected GPU carried by one payload."""
|
|
settings = get_settings()
|
|
modal_gpu = payload.get("modal_gpu")
|
|
if modal_gpu is not None:
|
|
settings = settings_for_modal_gpu(settings, modal_gpu)
|
|
raw_max_containers = payload.get("modal_max_containers")
|
|
if raw_max_containers is None:
|
|
return settings
|
|
max_containers = int(raw_max_containers)
|
|
if max_containers <= 0:
|
|
raise ValueError("modal_max_containers must be positive.")
|
|
return replace(settings, max_containers=max_containers)
|
|
|
|
|
|
@contextmanager
|
|
def _modal_cloud_settings_override(settings: ModalSyncSettings) -> Iterator[None]:
|
|
"""Expose request-specific settings while constructing the deployable cloud module."""
|
|
previous_settings = getattr(_MODAL_CLOUD_SETTINGS_STATE, "settings", None)
|
|
_MODAL_CLOUD_SETTINGS_STATE.settings = settings
|
|
try:
|
|
yield
|
|
finally:
|
|
if previous_settings is None:
|
|
try:
|
|
delattr(_MODAL_CLOUD_SETTINGS_STATE, "settings")
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
_MODAL_CLOUD_SETTINGS_STATE.settings = previous_settings
|
|
|
|
|
|
_DURABLE_BRIDGE_SERIALIZATION_IO_TYPES = frozenset(
|
|
{
|
|
"AUDIO",
|
|
"BOOLEAN",
|
|
"CONDITIONING",
|
|
"FLOAT",
|
|
"IMAGE",
|
|
"INT",
|
|
"LATENT",
|
|
"MASK",
|
|
"SIGMAS",
|
|
"STRING",
|
|
"VIDEO",
|
|
}
|
|
)
|
|
_DURABLE_BRIDGE_REHYDRATION_IO_TYPES = frozenset(
|
|
{"CLIP", "MODEL", "NOISE", "SAMPLER", "VAE"}
|
|
)
|
|
|
|
|
|
@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)
|
|
slot_futures: dict[int, Future[Any]] = field(default_factory=dict)
|
|
scheduled_speculative_affinities: set[str] = field(default_factory=set)
|
|
speculative_affinity_futures: dict[str, Future[Any]] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class _LocalGapKeepaliveState:
|
|
"""Track one prompt-scoped worker-retention loop during local execution."""
|
|
|
|
component_id: str
|
|
stop_event: threading.Event
|
|
future: Future[Any]
|
|
|
|
|
|
@dataclass
|
|
class _ModalAutoDeployState:
|
|
"""Track one thread-safe deployed-app readiness lifecycle."""
|
|
|
|
condition: threading.Condition = field(default_factory=threading.Condition)
|
|
deploy_in_progress: bool = False
|
|
ready: bool = False
|
|
last_error: BaseException | None = None
|
|
|
|
|
|
@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
|
|
idle_stop_timer: threading.Timer | None = None
|
|
|
|
|
|
@dataclass
|
|
class _ActiveRemoteInvocation:
|
|
"""Track one local proxy call that is currently waiting on remote Modal work."""
|
|
|
|
prompt_id: str
|
|
component_id: str
|
|
cancellation_event: threading.Event | None
|
|
interrupt_remote_call: Callable[[], Any] | None
|
|
|
|
|
|
@dataclass
|
|
class _RemoteSessionBridgeResolutionStats:
|
|
"""Track how one local fallback payload resolved session-backed inputs."""
|
|
|
|
input_ref_count: int = 0
|
|
live_session_hits: int = 0
|
|
bridge_cache_hits: int = 0
|
|
durable_bridge_hits: int = 0
|
|
bridge_record_lookups: int = 0
|
|
bridge_record_lookup_seconds: float = 0.0
|
|
replay_count: int = 0
|
|
replay_seconds: float = 0.0
|
|
direct_restore_seconds: float = 0.0
|
|
session_restore_writes: int = 0
|
|
|
|
|
|
def _remote_modal_call_worker_count() -> int:
|
|
"""Return the number of local worker threads reserved for blocking Modal calls."""
|
|
return max(1, int(get_settings().max_inflight_calls))
|
|
|
|
|
|
_REMOTE_MODAL_CALL_EXECUTOR = ThreadPoolExecutor(
|
|
max_workers=_remote_modal_call_worker_count()
|
|
)
|
|
_REMOTE_MODAL_WARMUP_EXECUTOR = ThreadPoolExecutor(
|
|
max_workers=_remote_modal_call_worker_count()
|
|
)
|
|
_REMOTE_MODAL_KEEPALIVE_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."""
|
|
|
|
|
|
class ModalRemoteAppOutOfDateError(ModalRemoteInvocationError):
|
|
"""Raised when a deployed Modal app is incompatible with the local client."""
|
|
|
|
|
|
class ModalContainerStatusError(RuntimeError):
|
|
"""Raised when active Modal container status cannot be queried."""
|
|
|
|
|
|
class ModalBillingStatusError(RuntimeError):
|
|
"""Raised when Modal hourly billing data cannot be queried."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ModalContainerStatus:
|
|
"""Describe one active Modal container owned by this ComfyUI instance."""
|
|
|
|
container_id: str
|
|
app_id: str
|
|
app_name: str
|
|
modal_gpu: str
|
|
estimated_gpu_cost_per_second: float
|
|
state: str
|
|
enqueued_at: float | None
|
|
started_at: float | None
|
|
|
|
def as_dict(self) -> dict[str, str | float | None]:
|
|
"""Return a JSON-serializable representation for the frontend."""
|
|
return {
|
|
"container_id": self.container_id,
|
|
"app_id": self.app_id,
|
|
"app_name": self.app_name,
|
|
"modal_gpu": self.modal_gpu,
|
|
"estimated_gpu_cost_per_second": self.estimated_gpu_cost_per_second,
|
|
"state": self.state,
|
|
"enqueued_at": self.enqueued_at,
|
|
"started_at": self.started_at,
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ModalHourlyBillingStatus:
|
|
"""Describe one completed hourly billing interval for a GPU-specific app."""
|
|
|
|
app_id: str | None
|
|
app_name: str
|
|
environment_name: str
|
|
modal_gpu: str
|
|
interval_start: datetime
|
|
interval_end: datetime
|
|
app_cost_usd_before_credits: Decimal
|
|
has_usage: bool
|
|
fetched_at: datetime
|
|
next_refresh_at: datetime
|
|
|
|
def as_dict(self) -> dict[str, str | float | bool | None]:
|
|
"""Return a JSON-serializable representation for the frontend."""
|
|
return {
|
|
"app_id": self.app_id,
|
|
"app_name": self.app_name,
|
|
"environment_name": self.environment_name,
|
|
"modal_gpu": self.modal_gpu,
|
|
"resolution": "hour",
|
|
"interval_start": self.interval_start.isoformat(),
|
|
"interval_end": self.interval_end.isoformat(),
|
|
"app_cost_usd_before_credits": float(
|
|
self.app_cost_usd_before_credits
|
|
),
|
|
"has_usage": self.has_usage,
|
|
"fetched_at": self.fetched_at.isoformat(),
|
|
"next_refresh_at": self.next_refresh_at.isoformat(),
|
|
"collection_buffer_seconds": int(
|
|
_MODAL_BILLING_COLLECTION_BUFFER.total_seconds()
|
|
),
|
|
}
|
|
|
|
|
|
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 = ""
|
|
|
|
|
|
def _managed_modal_app_gpus(settings: ModalSyncSettings) -> dict[str, str]:
|
|
"""Map every GPU-specific app name owned by this ComfyUI instance to its GPU."""
|
|
return {
|
|
modal_deployment_app_name(
|
|
settings_for_modal_gpu(settings, modal_gpu)
|
|
): modal_gpu
|
|
for modal_gpu in MODAL_GPU_TYPES
|
|
}
|
|
|
|
|
|
def _optional_modal_timestamp(value: Any) -> float | None:
|
|
"""Normalize an unset Modal protobuf timestamp to ``None``."""
|
|
timestamp = float(value or 0.0)
|
|
return timestamp if timestamp > 0 else None
|
|
|
|
|
|
def _completed_modal_billing_interval(
|
|
now: datetime,
|
|
) -> tuple[datetime, datetime, datetime]:
|
|
"""Return the buffered completed hour and its next eligible refresh time."""
|
|
if now.tzinfo is None:
|
|
normalized_now = now.replace(tzinfo=timezone.utc)
|
|
else:
|
|
normalized_now = now.astimezone(timezone.utc)
|
|
buffered_now = normalized_now - _MODAL_BILLING_COLLECTION_BUFFER
|
|
interval_end = buffered_now.replace(minute=0, second=0, microsecond=0)
|
|
interval_start = interval_end - _MODAL_BILLING_INTERVAL
|
|
next_refresh_at = (
|
|
interval_end
|
|
+ _MODAL_BILLING_INTERVAL
|
|
+ _MODAL_BILLING_COLLECTION_BUFFER
|
|
)
|
|
return interval_start, interval_end, next_refresh_at
|
|
|
|
|
|
def _modal_billing_row_value(row: Any, key: str) -> Any:
|
|
"""Read one billing row field from a mapping or SDK dataclass."""
|
|
if isinstance(row, Mapping):
|
|
return row.get(key)
|
|
return getattr(row, key, None)
|
|
|
|
|
|
def _modal_billing_cost(value: Any) -> Decimal:
|
|
"""Normalize one Modal billing cost without losing decimal precision."""
|
|
try:
|
|
return Decimal(str(value))
|
|
except (InvalidOperation, TypeError, ValueError) as exc:
|
|
raise ModalBillingStatusError(
|
|
f"Modal returned an invalid billing cost {value!r}."
|
|
) from exc
|
|
|
|
|
|
def _resolved_modal_environment_name() -> str | None:
|
|
"""Return the explicitly selected Modal environment, if one is configured."""
|
|
object_module = importlib.import_module("modal._object")
|
|
environments_module = importlib.import_module("modal.environments")
|
|
environment = environments_module.ensure_env(_modal_environment_name())
|
|
environment_name = object_module._get_environment_name(environment)
|
|
if environment_name is None:
|
|
return None
|
|
normalized_environment_name = str(environment_name).strip()
|
|
return normalized_environment_name or None
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
def _synchronized_modal_callable(
|
|
async_callable: Callable[..., Any],
|
|
) -> Callable[..., Any]:
|
|
"""Wrap one async Modal SDK operation on Modal's managed event loop."""
|
|
async_utils_module = importlib.import_module("modal._utils.async_utils")
|
|
return async_utils_module.synchronize_api(async_callable)
|
|
|
|
|
|
async def _list_modal_tasks_on_sdk_loop(
|
|
client_module: Any,
|
|
api_pb2: Any,
|
|
environment_name: str | None,
|
|
) -> Any:
|
|
"""List Modal tasks while running on Modal's managed event loop."""
|
|
client = await client_module._Client.from_env()
|
|
return await client.stub.TaskList(
|
|
api_pb2.TaskListRequest(environment_name=environment_name)
|
|
)
|
|
|
|
|
|
def _list_modal_tasks_synchronously(
|
|
client_module: Any,
|
|
api_pb2: Any,
|
|
environment_name: str | None,
|
|
) -> Any:
|
|
"""Run one Modal task-list query through the synchronized SDK bridge."""
|
|
blocking_callable = _synchronized_modal_callable(_list_modal_tasks_on_sdk_loop)
|
|
return blocking_callable(client_module, api_pb2, environment_name)
|
|
|
|
|
|
def _prune_modal_hourly_billing_cache() -> None:
|
|
"""Bound retained hourly billing records across apps and GPU selections."""
|
|
retained_keys = set(_MODAL_HOURLY_BILLING_CACHE) | set(
|
|
_MODAL_HOURLY_BILLING_ERROR_CACHE
|
|
)
|
|
overflow = len(retained_keys) - _MODAL_HOURLY_BILLING_CACHE_LIMIT
|
|
if overflow <= 0:
|
|
return
|
|
oldest_keys = sorted(
|
|
retained_keys,
|
|
key=lambda cache_key: cache_key[2],
|
|
)[:overflow]
|
|
for cache_key in oldest_keys:
|
|
_MODAL_HOURLY_BILLING_CACHE.pop(cache_key, None)
|
|
_MODAL_HOURLY_BILLING_ERROR_CACHE.pop(cache_key, None)
|
|
|
|
|
|
def _matching_modal_hourly_billing_rows(
|
|
rows: Any,
|
|
*,
|
|
app_name: str,
|
|
environment_name: str | None,
|
|
interval_start: datetime,
|
|
) -> tuple[list[Any], str]:
|
|
"""Select every historical app identity from one billing environment."""
|
|
candidate_rows = [
|
|
row
|
|
for row in rows
|
|
if str(_modal_billing_row_value(row, "description") or "") == app_name
|
|
and _modal_billing_row_value(row, "interval_start") == interval_start
|
|
]
|
|
if environment_name is not None:
|
|
matching_rows = [
|
|
row
|
|
for row in candidate_rows
|
|
if str(_modal_billing_row_value(row, "environment_name") or "")
|
|
== environment_name
|
|
]
|
|
return matching_rows, environment_name
|
|
|
|
candidate_environment_names = {
|
|
str(_modal_billing_row_value(row, "environment_name") or "").strip()
|
|
for row in candidate_rows
|
|
}
|
|
if len(candidate_environment_names) > 1:
|
|
raise ModalBillingStatusError(
|
|
f"Modal returned billing for app {app_name!r} in multiple "
|
|
"environments; set MODAL_ENVIRONMENT to select one."
|
|
)
|
|
reported_environment_name = next(
|
|
(
|
|
str(_modal_billing_row_value(row, "environment_name") or "").strip()
|
|
for row in candidate_rows
|
|
if str(_modal_billing_row_value(row, "environment_name") or "").strip()
|
|
),
|
|
"<workspace default>",
|
|
)
|
|
return candidate_rows, reported_environment_name
|
|
|
|
|
|
def _modal_hourly_billing_status_from_rows(
|
|
rows: Any,
|
|
*,
|
|
app_name: str,
|
|
environment_name: str | None,
|
|
modal_gpu: str,
|
|
interval_start: datetime,
|
|
interval_end: datetime,
|
|
fetched_at: datetime,
|
|
next_refresh_at: datetime,
|
|
) -> ModalHourlyBillingStatus:
|
|
"""Build one app billing status from an hourly workspace report."""
|
|
matching_rows, reported_environment_name = _matching_modal_hourly_billing_rows(
|
|
rows,
|
|
app_name=app_name,
|
|
environment_name=environment_name,
|
|
interval_start=interval_start,
|
|
)
|
|
app_cost = sum(
|
|
(
|
|
_modal_billing_cost(_modal_billing_row_value(row, "cost"))
|
|
for row in matching_rows
|
|
),
|
|
start=Decimal("0"),
|
|
)
|
|
app_ids = {
|
|
str(app_id)
|
|
for row in matching_rows
|
|
if (app_id := _modal_billing_row_value(row, "object_id"))
|
|
}
|
|
return ModalHourlyBillingStatus(
|
|
app_id=next(iter(app_ids)) if len(app_ids) == 1 else None,
|
|
app_name=app_name,
|
|
environment_name=reported_environment_name,
|
|
modal_gpu=modal_gpu,
|
|
interval_start=interval_start,
|
|
interval_end=interval_end,
|
|
app_cost_usd_before_credits=app_cost,
|
|
has_usage=bool(matching_rows),
|
|
fetched_at=fetched_at,
|
|
next_refresh_at=next_refresh_at,
|
|
)
|
|
|
|
|
|
def _fetch_modal_hourly_billing_synchronously(
|
|
report: Callable[..., Any],
|
|
error_types: tuple[type[BaseException], ...],
|
|
*,
|
|
app_name: str,
|
|
environment_name: str | None,
|
|
modal_gpu: str,
|
|
interval_start: datetime,
|
|
interval_end: datetime,
|
|
fetched_at: datetime,
|
|
next_refresh_at: datetime,
|
|
) -> ModalHourlyBillingStatus:
|
|
"""Fetch and cache one hourly report while serializing concurrent UI polls."""
|
|
cache_environment_name = environment_name or "<workspace default>"
|
|
cache_key = (cache_environment_name, app_name, interval_start)
|
|
with _MODAL_HOURLY_BILLING_CACHE_LOCK:
|
|
cached_status = _MODAL_HOURLY_BILLING_CACHE.get(cache_key)
|
|
if cached_status is not None:
|
|
return cached_status
|
|
cached_error = _MODAL_HOURLY_BILLING_ERROR_CACHE.get(cache_key)
|
|
if cached_error is not None:
|
|
raise ModalBillingStatusError(cached_error)
|
|
|
|
try:
|
|
rows = report(
|
|
start=interval_start,
|
|
end=interval_end,
|
|
resolution="h",
|
|
)
|
|
status = _modal_hourly_billing_status_from_rows(
|
|
rows,
|
|
app_name=app_name,
|
|
environment_name=environment_name,
|
|
modal_gpu=modal_gpu,
|
|
interval_start=interval_start,
|
|
interval_end=interval_end,
|
|
fetched_at=fetched_at,
|
|
next_refresh_at=next_refresh_at,
|
|
)
|
|
except error_types as exc:
|
|
cached_error = str(exc)
|
|
_MODAL_HOURLY_BILLING_ERROR_CACHE[cache_key] = cached_error
|
|
_prune_modal_hourly_billing_cache()
|
|
raise ModalBillingStatusError(cached_error) from exc
|
|
_MODAL_HOURLY_BILLING_CACHE[cache_key] = status
|
|
_prune_modal_hourly_billing_cache()
|
|
return status
|
|
|
|
|
|
async def get_hourly_modal_app_billing(
|
|
modal_gpu: str,
|
|
settings: ModalSyncSettings | None = None,
|
|
*,
|
|
now: datetime | None = None,
|
|
) -> ModalHourlyBillingStatus:
|
|
"""Return cached billing for the latest buffered hour of one GPU app."""
|
|
if modal is None:
|
|
raise ModalBillingStatusError("The Modal SDK is unavailable.")
|
|
|
|
resolved_settings = settings_for_modal_gpu(settings or get_settings(), modal_gpu)
|
|
app_name = modal_deployment_app_name(resolved_settings)
|
|
requested_now = now or datetime.now(timezone.utc)
|
|
fetched_at = (
|
|
requested_now.replace(tzinfo=timezone.utc)
|
|
if requested_now.tzinfo is None
|
|
else requested_now.astimezone(timezone.utc)
|
|
)
|
|
interval_start, interval_end, next_refresh_at = _completed_modal_billing_interval(
|
|
fetched_at
|
|
)
|
|
try:
|
|
environment_name = _resolved_modal_environment_name()
|
|
billing_module = importlib.import_module("modal.billing")
|
|
exception_module = importlib.import_module("modal.exception")
|
|
except (ModuleNotFoundError, AttributeError, RuntimeError) as exc:
|
|
raise ModalBillingStatusError(
|
|
"The installed Modal SDK does not expose hourly billing reports."
|
|
) from exc
|
|
|
|
report = getattr(billing_module, "workspace_billing_report", None)
|
|
if not callable(report):
|
|
raise ModalBillingStatusError(
|
|
"The installed Modal SDK does not expose workspace_billing_report()."
|
|
)
|
|
modal_error_type = getattr(exception_module, "Error", RuntimeError)
|
|
billing_error_types = (
|
|
modal_error_type,
|
|
ModalBillingStatusError,
|
|
OSError,
|
|
AttributeError,
|
|
RuntimeError,
|
|
TypeError,
|
|
ValueError,
|
|
)
|
|
try:
|
|
status = await asyncio.to_thread(
|
|
_fetch_modal_hourly_billing_synchronously,
|
|
report,
|
|
billing_error_types,
|
|
app_name=app_name,
|
|
environment_name=environment_name,
|
|
modal_gpu=resolved_settings.modal_gpu,
|
|
interval_start=interval_start,
|
|
interval_end=interval_end,
|
|
fetched_at=fetched_at,
|
|
next_refresh_at=next_refresh_at,
|
|
)
|
|
except billing_error_types as exc:
|
|
raise ModalBillingStatusError(
|
|
f"Unable to fetch Modal hourly billing: {exc}"
|
|
) from exc
|
|
return status
|
|
|
|
|
|
async def list_active_modal_containers(
|
|
settings: ModalSyncSettings | None = None,
|
|
) -> list[ModalContainerStatus]:
|
|
"""List active Modal containers belonging to this ComfyUI instance."""
|
|
if modal is None:
|
|
raise ModalContainerStatusError("The Modal SDK is unavailable.")
|
|
|
|
resolved_settings = settings or get_settings()
|
|
managed_app_gpus = _managed_modal_app_gpus(resolved_settings)
|
|
try:
|
|
client_module = importlib.import_module("modal.client")
|
|
exception_module = importlib.import_module("modal.exception")
|
|
api_pb2 = importlib.import_module("modal_proto.api_pb2")
|
|
except ModuleNotFoundError as exc:
|
|
raise ModalContainerStatusError(
|
|
"The installed Modal SDK does not expose the container list API."
|
|
) from exc
|
|
|
|
modal_error_type = getattr(exception_module, "Error", RuntimeError)
|
|
try:
|
|
environment_name = _resolved_modal_environment_name()
|
|
response = await asyncio.to_thread(
|
|
_list_modal_tasks_synchronously,
|
|
client_module,
|
|
api_pb2,
|
|
environment_name,
|
|
)
|
|
except (modal_error_type, OSError, AttributeError, RuntimeError) as exc:
|
|
raise ModalContainerStatusError(
|
|
f"Unable to list Modal containers: {exc}"
|
|
) from exc
|
|
|
|
containers: list[ModalContainerStatus] = []
|
|
for task in response.tasks:
|
|
app_name = str(task.app_description)
|
|
modal_gpu = managed_app_gpus.get(app_name)
|
|
if modal_gpu is None:
|
|
continue
|
|
started_at = _optional_modal_timestamp(task.started_at)
|
|
containers.append(
|
|
ModalContainerStatus(
|
|
container_id=str(task.task_id),
|
|
app_id=str(task.app_id),
|
|
app_name=app_name,
|
|
modal_gpu=modal_gpu,
|
|
estimated_gpu_cost_per_second=MODAL_GPU_ESTIMATED_USD_PER_SECOND[
|
|
modal_gpu
|
|
],
|
|
state="running" if started_at is not None else "starting",
|
|
enqueued_at=_optional_modal_timestamp(task.enqueued_at),
|
|
started_at=started_at,
|
|
)
|
|
)
|
|
|
|
containers.sort(
|
|
key=lambda container: (
|
|
container.enqueued_at or container.started_at or 0.0,
|
|
container.container_id,
|
|
)
|
|
)
|
|
logger.debug(
|
|
"Listed %d active Modal container(s) for the status UI.", len(containers)
|
|
)
|
|
return containers
|
|
|
|
|
|
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)
|
|
|
|
|
|
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
|
|
blocking_callable = _synchronized_modal_callable(
|
|
_stream_remote_container_logs_via_modal_sdk_async
|
|
)
|
|
blocking_callable(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 _new_remote_container_log_stream_state(
|
|
task_id: str,
|
|
) -> _RemoteContainerLogStreamState:
|
|
"""Create an unstarted container-log watcher state."""
|
|
stop_event = threading.Event()
|
|
return _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,
|
|
),
|
|
)
|
|
|
|
|
|
def _cancel_remote_container_log_idle_stop(
|
|
stream_state: _RemoteContainerLogStreamState,
|
|
) -> None:
|
|
"""Cancel one pending idle-stop timer before a container is reused."""
|
|
idle_stop_timer = stream_state.idle_stop_timer
|
|
if idle_stop_timer is None:
|
|
return
|
|
idle_stop_timer.cancel()
|
|
stream_state.idle_stop_timer = None
|
|
|
|
|
|
def _stop_remote_container_log_stream_if_idle(
|
|
task_id: str,
|
|
expected_state: _RemoteContainerLogStreamState,
|
|
) -> None:
|
|
"""Stop one watcher after its grace period if no payload reused it."""
|
|
with _REMOTE_CONTAINER_LOG_STREAMS_LOCK:
|
|
stream_state = _REMOTE_CONTAINER_LOG_STREAMS.get(task_id)
|
|
if stream_state is not expected_state or stream_state.refcount != 0:
|
|
return
|
|
stream_state.idle_stop_timer = None
|
|
_REMOTE_CONTAINER_LOG_STREAMS.pop(task_id, None)
|
|
|
|
logger.info(
|
|
"Stopping idle remote Modal container log stream for task_id=%s.",
|
|
task_id,
|
|
)
|
|
stream_state.stop_event.set()
|
|
stream_state.thread.join(timeout=0.2)
|
|
|
|
|
|
def _schedule_remote_container_log_idle_stop(
|
|
stream_state: _RemoteContainerLogStreamState,
|
|
) -> None:
|
|
"""Schedule one daemon timer to stop an unreferenced log watcher."""
|
|
idle_stop_timer = threading.Timer(
|
|
_REMOTE_CONTAINER_LOG_STREAM_IDLE_GRACE_SECONDS,
|
|
_stop_remote_container_log_stream_if_idle,
|
|
args=(stream_state.task_id, stream_state),
|
|
)
|
|
idle_stop_timer.daemon = True
|
|
stream_state.idle_stop_timer = idle_stop_timer
|
|
idle_stop_timer.start()
|
|
|
|
|
|
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 not None and not stream_state.thread.is_alive():
|
|
_cancel_remote_container_log_idle_stop(stream_state)
|
|
_REMOTE_CONTAINER_LOG_STREAMS.pop(task_id, None)
|
|
stream_state = None
|
|
|
|
if stream_state is None:
|
|
stream_state = _new_remote_container_log_stream_state(task_id)
|
|
_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()
|
|
else:
|
|
_cancel_remote_container_log_idle_stop(stream_state)
|
|
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 watcher while allowing prompt-local container reuse."""
|
|
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:
|
|
return
|
|
if not stream_state.thread.is_alive():
|
|
_REMOTE_CONTAINER_LOG_STREAMS.pop(task_id, None)
|
|
return
|
|
if stream_state.idle_stop_timer is None:
|
|
logger.info(
|
|
"Keeping remote Modal container log stream alive for %.1fs "
|
|
"to allow task reuse without replaying history task_id=%s.",
|
|
_REMOTE_CONTAINER_LOG_STREAM_IDLE_GRACE_SECONDS,
|
|
task_id,
|
|
)
|
|
_schedule_remote_container_log_idle_stop(stream_state)
|
|
|
|
|
|
def _close_remote_payload_stream(stream_events: Iterator[dict[str, Any]]) -> None:
|
|
"""Best-effort close one streamed Modal iterator after the terminal result arrives."""
|
|
close_callable = getattr(stream_events, "close", None)
|
|
if not callable(close_callable):
|
|
return
|
|
try:
|
|
close_result = close_callable()
|
|
except (AttributeError, RuntimeError, TypeError, ValueError) as exc:
|
|
logger.debug("Ignoring remote payload stream close failure: %s", exc)
|
|
return
|
|
if not asyncio.iscoroutine(close_result):
|
|
return
|
|
try:
|
|
asyncio.run(close_result)
|
|
except (RuntimeError, ValueError) as exc:
|
|
logger.debug("Ignoring async remote payload stream close failure: %s", exc)
|
|
|
|
|
|
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 _sanitize_payload_for_session_bridge_record(
|
|
payload: dict[str, Any]
|
|
) -> dict[str, Any]:
|
|
"""Strip run-scoped fields from one producer payload before persisting replay metadata."""
|
|
sanitized_payload = copy.deepcopy(payload)
|
|
sanitized_payload.pop("prompt_id", None)
|
|
sanitized_payload.pop("remote_session", None)
|
|
sanitized_payload.pop("clear_remote_session", None)
|
|
sanitized_payload["extra_data"] = {}
|
|
return sanitized_payload
|
|
|
|
|
|
def _durable_object_store() -> FileDurableObjectStore:
|
|
"""Return the process-local handle for durable binary objects."""
|
|
global _DURABLE_OBJECT_STORE
|
|
|
|
with _DURABLE_OBJECT_STORE_LOCK:
|
|
if _DURABLE_OBJECT_STORE is not None:
|
|
return _DURABLE_OBJECT_STORE
|
|
settings = get_settings()
|
|
commit_callback: Callable[[], Any] | None = None
|
|
committed_read_callback: Callable[[str], bytes] | None = None
|
|
if os.getenv("MODAL_IS_REMOTE") == "1" or os.getenv("MODAL_TASK_ID"):
|
|
object_root = Path(settings.remote_storage_root) / "durable_objects"
|
|
volume = globals().get("vol")
|
|
volume_commit = getattr(volume, "commit", None)
|
|
if callable(volume_commit):
|
|
commit_callback = volume_commit
|
|
if callable(getattr(volume, "read_file", None)):
|
|
|
|
def read_committed_object(object_path: str) -> bytes:
|
|
"""Read one durable object without reloading the mounted volume."""
|
|
volume_path = (Path("durable_objects") / object_path).as_posix()
|
|
return read_modal_volume_file(volume, volume_path)
|
|
|
|
committed_read_callback = read_committed_object
|
|
else:
|
|
object_root = settings.local_storage_root / "durable_objects"
|
|
_DURABLE_OBJECT_STORE = FileDurableObjectStore(
|
|
object_root,
|
|
commit_callback=commit_callback,
|
|
committed_read_callback=committed_read_callback,
|
|
)
|
|
return _DURABLE_OBJECT_STORE
|
|
|
|
|
|
def _json_payload_size_bytes(payload: Any) -> int:
|
|
"""Return the compact UTF-8 size of one JSON-safe payload."""
|
|
return len(
|
|
json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
)
|
|
|
|
|
|
def _select_remote_session_bridge_recovery_kind(
|
|
*,
|
|
serialized_output: Any | None,
|
|
rehydration_plan: Mapping[str, Any] | None,
|
|
) -> RemoteSessionBridgeRecoveryKind:
|
|
"""Choose the least expensive complete recovery mechanism for one bridge."""
|
|
if serialized_output is not None:
|
|
return RemoteSessionBridgeRecoveryKind.SERIALIZED_OUTPUT
|
|
if isinstance(rehydration_plan, Mapping):
|
|
plan_kind = str(rehydration_plan.get("kind") or "")
|
|
if plan_kind == "single_node_output":
|
|
return RemoteSessionBridgeRecoveryKind.SINGLE_NODE_PLAN
|
|
if plan_kind == "subgraph_output":
|
|
return RemoteSessionBridgeRecoveryKind.SUBGRAPH_PLAN
|
|
return RemoteSessionBridgeRecoveryKind.PRODUCER_REPLAY
|
|
|
|
|
|
def _remote_session_bridge_recovery_input_names(
|
|
*,
|
|
recovery_kind: RemoteSessionBridgeRecoveryKind,
|
|
rehydration_plan: Mapping[str, Any] | None,
|
|
hydrated_inputs: Mapping[str, Any],
|
|
) -> tuple[str, ...]:
|
|
"""Return producer input names that the selected recovery mechanism needs."""
|
|
if recovery_kind in {
|
|
RemoteSessionBridgeRecoveryKind.SERIALIZED_OUTPUT,
|
|
RemoteSessionBridgeRecoveryKind.SINGLE_NODE_PLAN,
|
|
}:
|
|
return ()
|
|
if recovery_kind is RemoteSessionBridgeRecoveryKind.PRODUCER_REPLAY:
|
|
return tuple(str(input_name) for input_name in hydrated_inputs)
|
|
|
|
plan_payload = (
|
|
rehydration_plan.get("payload")
|
|
if isinstance(rehydration_plan, Mapping)
|
|
else None
|
|
)
|
|
boundary_inputs = (
|
|
plan_payload.get("boundary_inputs")
|
|
if isinstance(plan_payload, Mapping)
|
|
else None
|
|
)
|
|
if not isinstance(boundary_inputs, list):
|
|
raise RemoteSessionStateError(
|
|
"Durable bridge subgraph recovery plans must define boundary_inputs."
|
|
)
|
|
input_names = tuple(
|
|
dict.fromkeys(
|
|
str(boundary_input.get("proxy_input_name") or "").strip()
|
|
for boundary_input in boundary_inputs
|
|
if isinstance(boundary_input, Mapping)
|
|
and str(boundary_input.get("proxy_input_name") or "").strip()
|
|
)
|
|
)
|
|
missing_input_names = [
|
|
input_name for input_name in input_names if input_name not in hydrated_inputs
|
|
]
|
|
if missing_input_names:
|
|
raise RemoteSessionStateError(
|
|
"Durable bridge subgraph recovery inputs are missing hydrated values: "
|
|
f"{missing_input_names}."
|
|
)
|
|
return input_names
|
|
|
|
|
|
def _offload_large_bridge_payloads(
|
|
*,
|
|
hydrated_inputs: Mapping[str, Any],
|
|
producer_inputs: dict[str, Any],
|
|
output_value: Any,
|
|
serialized_output: Any | None,
|
|
) -> tuple[
|
|
dict[str, Any],
|
|
DurableObjectRef | None,
|
|
Any | None,
|
|
DurableObjectRef | None,
|
|
]:
|
|
"""Move oversized bridge inputs and outputs into durable binary objects."""
|
|
max_inline_bytes = get_settings().bridge_inline_max_bytes
|
|
producer_inputs_object: DurableObjectRef | None = None
|
|
serialized_output_object: DurableObjectRef | None = None
|
|
if producer_inputs and _json_payload_size_bytes(producer_inputs) > max_inline_bytes:
|
|
producer_inputs_object = _durable_object_store().put(
|
|
"bridge_objects",
|
|
serialize_node_inputs(hydrated_inputs),
|
|
)
|
|
producer_inputs = {}
|
|
if (
|
|
serialized_output is not None
|
|
and _json_payload_size_bytes(serialized_output) > max_inline_bytes
|
|
):
|
|
serialized_output_object = _durable_object_store().put(
|
|
"bridge_objects",
|
|
serialize_node_outputs((output_value,)),
|
|
)
|
|
serialized_output = None
|
|
return (
|
|
producer_inputs,
|
|
producer_inputs_object,
|
|
serialized_output,
|
|
serialized_output_object,
|
|
)
|
|
|
|
|
|
def _deserialize_remote_session_bridge_producer_inputs(
|
|
record: RemoteSessionBridgeRecord,
|
|
) -> dict[str, Any]:
|
|
"""Restore bridge producer inputs from inline metadata or durable storage."""
|
|
if record.producer_inputs_object is not None:
|
|
return deserialize_node_inputs(
|
|
_durable_object_store().get(record.producer_inputs_object)
|
|
)
|
|
if record.producer_inputs_retained is False:
|
|
recovery_kind = (
|
|
record.recovery_kind.value
|
|
if record.recovery_kind is not None
|
|
else "unspecified"
|
|
)
|
|
raise RemoteSessionStateError(
|
|
"Remote session bridge producer inputs were intentionally omitted for "
|
|
f"bridge_key={record.bridge_key!r} recovery_kind={recovery_kind!r}."
|
|
)
|
|
return deserialize_node_inputs(record.producer_inputs)
|
|
|
|
|
|
def _build_remote_session_bridge_record(
|
|
*,
|
|
payload: dict[str, Any],
|
|
hydrated_inputs: dict[str, Any],
|
|
node_id: str,
|
|
output_index: int,
|
|
io_type: str,
|
|
output_value: Any,
|
|
) -> RemoteSessionBridgeRecord:
|
|
"""Build one durable bridge record for a session-backed boundary output."""
|
|
producer_payload = _sanitize_payload_for_session_bridge_record(payload)
|
|
producer_input_identity = serialize_mapping(hydrated_inputs)
|
|
serialized_output = _serialize_durable_bridge_output(output_value, io_type)
|
|
rehydration_plan = _build_durable_bridge_rehydration_plan(
|
|
payload=producer_payload,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
io_type=io_type,
|
|
)
|
|
recovery_kind = _select_remote_session_bridge_recovery_kind(
|
|
serialized_output=serialized_output,
|
|
rehydration_plan=rehydration_plan,
|
|
)
|
|
recovery_input_names = _remote_session_bridge_recovery_input_names(
|
|
recovery_kind=recovery_kind,
|
|
rehydration_plan=rehydration_plan,
|
|
hydrated_inputs=hydrated_inputs,
|
|
)
|
|
producer_inputs_retained = recovery_kind in {
|
|
RemoteSessionBridgeRecoveryKind.SUBGRAPH_PLAN,
|
|
RemoteSessionBridgeRecoveryKind.PRODUCER_REPLAY,
|
|
}
|
|
retained_hydrated_inputs = {
|
|
input_name: hydrated_inputs[input_name] for input_name in recovery_input_names
|
|
}
|
|
producer_inputs = {
|
|
input_name: producer_input_identity[input_name]
|
|
for input_name in recovery_input_names
|
|
}
|
|
bridge_key = stable_session_bridge_key(
|
|
producer_payload=producer_payload,
|
|
producer_inputs=producer_input_identity,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
)
|
|
(
|
|
producer_inputs,
|
|
producer_inputs_object,
|
|
serialized_output,
|
|
serialized_output_object,
|
|
) = _offload_large_bridge_payloads(
|
|
hydrated_inputs=retained_hydrated_inputs,
|
|
producer_inputs=producer_inputs,
|
|
output_value=output_value,
|
|
serialized_output=serialized_output,
|
|
)
|
|
logger.info(
|
|
"Prepared remote session bridge bridge_key=%s recovery_kind=%s "
|
|
"producer_inputs_retained=%s retained_input_count=%d omitted_input_count=%d.",
|
|
bridge_key,
|
|
recovery_kind.value,
|
|
producer_inputs_retained,
|
|
len(recovery_input_names),
|
|
len(producer_input_identity) - len(recovery_input_names),
|
|
)
|
|
return RemoteSessionBridgeRecord(
|
|
bridge_key=bridge_key,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
producer_payload=producer_payload,
|
|
producer_inputs=producer_inputs,
|
|
recovery_kind=recovery_kind,
|
|
producer_inputs_retained=producer_inputs_retained,
|
|
producer_inputs_object=producer_inputs_object,
|
|
serialized_output=serialized_output,
|
|
serialized_output_object=serialized_output_object,
|
|
serialized_output_io_type=(
|
|
str(io_type)
|
|
if serialized_output is not None or serialized_output_object is not None
|
|
else None
|
|
),
|
|
rehydration_plan=rehydration_plan,
|
|
rehydration_plan_io_type=(
|
|
str(io_type) if rehydration_plan is not None else None
|
|
),
|
|
)
|
|
|
|
|
|
def _record_remote_session_resolution_event(
|
|
resolution_stats: "_RemoteSessionBridgeResolutionStats | None",
|
|
event_name: str,
|
|
event_payload: Mapping[str, Any],
|
|
) -> None:
|
|
"""Accumulate one local fallback remote-session resolution event."""
|
|
del event_payload
|
|
if resolution_stats is None:
|
|
return
|
|
if event_name in {"session-value-hit", "bridge-target-hit", "bridge-source-hit"}:
|
|
resolution_stats.live_session_hits += 1
|
|
|
|
|
|
def _clone_cached_bridge_value(value: Any) -> Any:
|
|
"""Clone one cached bridge value when the runtime object exposes a safe clone path."""
|
|
clone_method = getattr(value, "clone", None)
|
|
if callable(clone_method):
|
|
return clone_method()
|
|
return value
|
|
|
|
|
|
def _store_remote_session_bridge_value(
|
|
bridge_key: str,
|
|
value: Any,
|
|
) -> None:
|
|
"""Retain one live bridge value in-process so later mapped phases can skip replay."""
|
|
with _REMOTE_SESSION_BRIDGE_VALUE_CACHE_LOCK:
|
|
_REMOTE_SESSION_BRIDGE_VALUE_CACHE[bridge_key] = _clone_cached_bridge_value(
|
|
value
|
|
)
|
|
if bridge_key in _REMOTE_SESSION_BRIDGE_VALUE_CACHE_ORDER:
|
|
_REMOTE_SESSION_BRIDGE_VALUE_CACHE_ORDER.remove(bridge_key)
|
|
_REMOTE_SESSION_BRIDGE_VALUE_CACHE_ORDER.append(bridge_key)
|
|
while (
|
|
len(_REMOTE_SESSION_BRIDGE_VALUE_CACHE_ORDER)
|
|
> _REMOTE_SESSION_BRIDGE_VALUE_CACHE_LIMIT
|
|
):
|
|
evicted_key = _REMOTE_SESSION_BRIDGE_VALUE_CACHE_ORDER.pop(0)
|
|
_REMOTE_SESSION_BRIDGE_VALUE_CACHE.pop(evicted_key, None)
|
|
|
|
|
|
def _get_remote_session_bridge_value(bridge_key: str) -> Any | None:
|
|
"""Return one retained bridge value when the current process still has it."""
|
|
with _REMOTE_SESSION_BRIDGE_VALUE_CACHE_LOCK:
|
|
cached_value = _REMOTE_SESSION_BRIDGE_VALUE_CACHE.get(bridge_key)
|
|
if cached_value is None:
|
|
return None
|
|
if bridge_key in _REMOTE_SESSION_BRIDGE_VALUE_CACHE_ORDER:
|
|
_REMOTE_SESSION_BRIDGE_VALUE_CACHE_ORDER.remove(bridge_key)
|
|
_REMOTE_SESSION_BRIDGE_VALUE_CACHE_ORDER.append(bridge_key)
|
|
return _clone_cached_bridge_value(cached_value)
|
|
|
|
|
|
def _serialize_durable_bridge_output(output_value: Any, io_type: str) -> Any | None:
|
|
"""Serialize one bridge output when its io_type supports durable direct restore."""
|
|
normalized_io_type = str(io_type or "")
|
|
if normalized_io_type not in _DURABLE_BRIDGE_SERIALIZATION_IO_TYPES:
|
|
return None
|
|
try:
|
|
return serialize_value(output_value)
|
|
except TypeError:
|
|
logger.warning(
|
|
"Skipping durable bridge serialization for io_type=%s value_type=%s.",
|
|
normalized_io_type,
|
|
type(output_value).__name__,
|
|
)
|
|
return None
|
|
|
|
|
|
def _restore_serialized_remote_session_bridge_value(
|
|
record: RemoteSessionBridgeRecord,
|
|
*,
|
|
target_session_handle: RemoteSessionHandle,
|
|
resolution_stats: "_RemoteSessionBridgeResolutionStats | None" = None,
|
|
) -> Any | None:
|
|
"""Restore one bridge value directly from a durable serialized payload."""
|
|
if record.serialized_output is None and record.serialized_output_object is None:
|
|
return None
|
|
|
|
restore_started_at = time.perf_counter()
|
|
if record.serialized_output_object is not None:
|
|
restored_outputs = deserialize_node_outputs(
|
|
_durable_object_store().get(record.serialized_output_object)
|
|
)
|
|
if len(restored_outputs) != 1:
|
|
raise DurableStateError(
|
|
f"Bridge object {record.serialized_output_object.object_path!r} "
|
|
"must contain exactly one output."
|
|
)
|
|
restored_value = restored_outputs[0]
|
|
storage_kind = "object-backed"
|
|
else:
|
|
restored_value = deserialize_value(record.serialized_output)
|
|
storage_kind = "inline"
|
|
_REMOTE_SESSION_STORE.put_bridge_output(
|
|
target_session_handle,
|
|
bridge_key=record.bridge_key,
|
|
node_id=record.node_id,
|
|
output_index=record.output_index,
|
|
value=restored_value,
|
|
)
|
|
_store_remote_session_bridge_value(record.bridge_key, restored_value)
|
|
if resolution_stats is not None:
|
|
resolution_stats.durable_bridge_hits += 1
|
|
resolution_stats.session_restore_writes += 1
|
|
resolution_stats.direct_restore_seconds += (
|
|
time.perf_counter() - restore_started_at
|
|
)
|
|
logger.info(
|
|
"Resolved remote session bridge bridge_key=%s from durable %s serialized %s payload into session_id=%s.",
|
|
record.bridge_key,
|
|
storage_kind,
|
|
record.serialized_output_io_type or "bridge",
|
|
target_session_handle.session_id,
|
|
)
|
|
return restored_value
|
|
|
|
|
|
def materialize_remote_session_bridge_ref_locally(
|
|
ref_payload: Mapping[str, Any],
|
|
) -> Any:
|
|
"""Download and deserialize one durable bridge value for local ComfyUI work."""
|
|
if not is_remote_session_bridge_ref_payload(ref_payload):
|
|
raise TypeError(
|
|
"Local Modal bridge materialization requires a remote session bridge ref."
|
|
)
|
|
ref = RemoteSessionBridgeRef.from_payload(ref_payload)
|
|
materialization_started_at = time.perf_counter()
|
|
logger.info(
|
|
"Starting local remote-bridge materialization bridge_key=%s node=%s output=%d.",
|
|
ref.bridge_key,
|
|
ref.node_id,
|
|
ref.output_index,
|
|
)
|
|
record_lookup_started_at = time.perf_counter()
|
|
record = _load_local_materialization_bridge_record(ref.bridge_key)
|
|
logger.info(
|
|
"Loaded local materialization bridge record in %.3fs bridge_key=%s object_backed=%s.",
|
|
time.perf_counter() - record_lookup_started_at,
|
|
ref.bridge_key,
|
|
record.serialized_output_object is not None,
|
|
)
|
|
if record.serialized_output_object is not None:
|
|
object_download_started_at = time.perf_counter()
|
|
restored_outputs = deserialize_node_outputs(
|
|
_read_local_materialization_bridge_object(
|
|
record.serialized_output_object
|
|
)
|
|
)
|
|
logger.info(
|
|
"Downloaded and deserialized local bridge object in %.3fs bridge_key=%s bytes=%d.",
|
|
time.perf_counter() - object_download_started_at,
|
|
ref.bridge_key,
|
|
record.serialized_output_object.size_bytes,
|
|
)
|
|
if len(restored_outputs) != 1:
|
|
raise DurableStateError(
|
|
f"Bridge object {record.serialized_output_object.object_path!r} "
|
|
"must contain exactly one output."
|
|
)
|
|
restored_value = restored_outputs[0]
|
|
storage_kind = "object-backed"
|
|
elif record.serialized_output is not None:
|
|
restored_value = deserialize_value(record.serialized_output)
|
|
storage_kind = "inline"
|
|
else:
|
|
raise RemoteSessionStateError(
|
|
"Remote bridge cannot be materialized locally without a durable "
|
|
f"serialized output bridge_key={ref.bridge_key!r}."
|
|
)
|
|
logger.info(
|
|
"Materialized remote bridge locally in %.3fs bridge_key=%s node=%s output=%d storage=%s io_type=%s.",
|
|
time.perf_counter() - materialization_started_at,
|
|
ref.bridge_key,
|
|
ref.node_id,
|
|
ref.output_index,
|
|
storage_kind,
|
|
record.serialized_output_io_type or "bridge",
|
|
)
|
|
return restored_value
|
|
|
|
|
|
def _load_local_materialization_bridge_record(
|
|
bridge_key: str,
|
|
) -> RemoteSessionBridgeRecord:
|
|
"""Load a bridge record from local fallback state or Modal's shared Dict."""
|
|
settings = get_settings()
|
|
if settings.execution_mode != "remote":
|
|
return _REMOTE_SESSION_BRIDGE_STORE.get_record(bridge_key)
|
|
bridge_store = _lookup_modal_session_bridge_store()
|
|
payload = bridge_store.get(bridge_key)
|
|
if not isinstance(payload, Mapping):
|
|
raise RemoteSessionStateError(
|
|
f"Remote session bridge record {bridge_key!r} was not found."
|
|
)
|
|
return RemoteSessionBridgeRecord.from_payload(payload)
|
|
|
|
|
|
def _read_local_materialization_bridge_object(
|
|
object_ref: DurableObjectRef,
|
|
) -> bytes:
|
|
"""Read and validate one bridge object locally from fallback or Modal storage."""
|
|
settings = get_settings()
|
|
if settings.execution_mode != "remote":
|
|
return _durable_object_store().get(object_ref)
|
|
volume_path = (Path("durable_objects") / object_ref.object_path).as_posix()
|
|
payload = read_modal_volume_file(
|
|
_lookup_modal_durable_volume(),
|
|
volume_path,
|
|
)
|
|
if len(payload) != object_ref.size_bytes:
|
|
raise DurableStateError(
|
|
f"Durable bridge object {object_ref.object_path!r} has an unexpected size."
|
|
)
|
|
if hashlib.sha256(payload).hexdigest() != object_ref.sha256:
|
|
raise DurableStateError(
|
|
f"Durable bridge object {object_ref.object_path!r} failed its content-address check."
|
|
)
|
|
return payload
|
|
|
|
|
|
def _build_durable_bridge_rehydration_plan(
|
|
*,
|
|
payload: dict[str, Any],
|
|
node_id: str,
|
|
output_index: int,
|
|
io_type: str,
|
|
) -> dict[str, Any] | None:
|
|
"""Return a direct rehydration plan when one bridge output can be rebuilt without replay."""
|
|
if str(io_type or "") not in _DURABLE_BRIDGE_REHYDRATION_IO_TYPES:
|
|
return None
|
|
prompt = payload.get("subgraph_prompt")
|
|
if not isinstance(prompt, dict):
|
|
return None
|
|
prompt_node = prompt.get(str(node_id))
|
|
if not isinstance(prompt_node, dict):
|
|
return None
|
|
class_type = prompt_node.get("class_type")
|
|
inputs = prompt_node.get("inputs")
|
|
if (
|
|
not isinstance(class_type, str)
|
|
or not class_type.strip()
|
|
or not isinstance(inputs, dict)
|
|
):
|
|
return None
|
|
|
|
normalized_inputs: dict[str, Any] = {}
|
|
has_linked_input = False
|
|
for input_name, input_value in inputs.items():
|
|
normalized_value = _normalize_prompt_input_value(copy.deepcopy(input_value))
|
|
if _is_link(normalized_value):
|
|
has_linked_input = True
|
|
continue
|
|
normalized_inputs[str(input_name)] = normalized_value
|
|
|
|
if not has_linked_input:
|
|
node_data: dict[str, Any] = {"class_type": class_type}
|
|
custom_nodes_bundle = payload.get("custom_nodes_bundle")
|
|
if isinstance(custom_nodes_bundle, str) and custom_nodes_bundle.strip():
|
|
node_data["custom_nodes_bundle"] = custom_nodes_bundle
|
|
return {
|
|
"kind": "single_node_output",
|
|
"node_data": node_data,
|
|
"node_inputs": normalized_inputs,
|
|
}
|
|
|
|
required_node_ids = set(
|
|
_resolve_required_subgraph_nodes(
|
|
prompt=prompt,
|
|
execute_node_ids=[str(node_id)],
|
|
)
|
|
)
|
|
if str(node_id) not in required_node_ids:
|
|
return None
|
|
if _subgraph_contains_sampling_node(prompt, required_node_ids):
|
|
logger.info(
|
|
"Skipping durable %s bridge subgraph rehydration plan for node_id=%s class_type=%s because its dependency closure includes a sampler.",
|
|
io_type,
|
|
node_id,
|
|
class_type,
|
|
)
|
|
return None
|
|
|
|
rehydration_payload = copy.deepcopy(payload)
|
|
rehydration_payload[
|
|
"component_id"
|
|
] = f"{payload.get('component_id', 'component')}::rehydrate:{node_id}"
|
|
rehydration_payload["subgraph_prompt"] = {
|
|
str(current_node_id): copy.deepcopy(prompt[current_node_id])
|
|
for current_node_id in prompt
|
|
if str(current_node_id) in required_node_ids
|
|
}
|
|
rehydration_payload["component_node_ids"] = sorted(required_node_ids)
|
|
rehydration_payload["execute_node_ids"] = [str(node_id)]
|
|
rehydration_payload["mapped_execute_node_ids"] = []
|
|
rehydration_payload["static_execute_node_ids"] = []
|
|
rehydration_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 payload.get("boundary_inputs", [])
|
|
if any(
|
|
str(target.get("node_id")) in required_node_ids
|
|
for target in boundary_input.get("targets", [])
|
|
)
|
|
]
|
|
rehydration_payload["boundary_outputs"] = [
|
|
{
|
|
"node_id": str(node_id),
|
|
"output_index": int(output_index),
|
|
"io_type": str(io_type),
|
|
"is_list": False,
|
|
"session_output": False,
|
|
"proxy_output_name": f"{node_id}_rehydrated_{output_index}",
|
|
}
|
|
]
|
|
return {
|
|
"kind": "subgraph_output",
|
|
"payload": rehydration_payload,
|
|
}
|
|
|
|
|
|
def _restore_planned_remote_session_bridge_value(
|
|
record: RemoteSessionBridgeRecord,
|
|
*,
|
|
target_session_handle: RemoteSessionHandle,
|
|
node_mapping: dict[str, type[Any]] | None = None,
|
|
resolution_stats: "_RemoteSessionBridgeResolutionStats | None" = None,
|
|
) -> Any | None:
|
|
"""Restore one bridge value directly from a durable node rehydration plan."""
|
|
if not isinstance(record.rehydration_plan, Mapping):
|
|
return None
|
|
|
|
restore_started_at = time.perf_counter()
|
|
plan_kind = str(record.rehydration_plan.get("kind") or "")
|
|
if plan_kind == "single_node_output":
|
|
node_data = record.rehydration_plan.get("node_data")
|
|
node_inputs = record.rehydration_plan.get("node_inputs")
|
|
if not isinstance(node_data, Mapping) or not isinstance(node_inputs, Mapping):
|
|
return None
|
|
outputs = _execute_node_locally_raw(
|
|
dict(node_data),
|
|
dict(node_inputs),
|
|
node_mapping=node_mapping,
|
|
)
|
|
output_index = record.output_index
|
|
elif plan_kind == "subgraph_output":
|
|
plan_payload = record.rehydration_plan.get("payload")
|
|
if not isinstance(plan_payload, Mapping):
|
|
return None
|
|
executable_payload = dict(plan_payload)
|
|
executable_payload["remote_session"] = target_session_handle.to_payload()
|
|
executable_payload.pop("clear_remote_session", None)
|
|
outputs = _execute_subgraph_prompt(
|
|
executable_payload,
|
|
_deserialize_remote_session_bridge_producer_inputs(record),
|
|
node_mapping,
|
|
)
|
|
output_index = 0
|
|
else:
|
|
return None
|
|
if output_index < 0 or output_index >= len(outputs):
|
|
raise RemoteSessionStateError(
|
|
f"Durable bridge rehydration plan for {record.bridge_key!r} did not produce output index {record.output_index}."
|
|
)
|
|
restored_value = outputs[output_index]
|
|
_REMOTE_SESSION_STORE.put_bridge_output(
|
|
target_session_handle,
|
|
bridge_key=record.bridge_key,
|
|
node_id=record.node_id,
|
|
output_index=record.output_index,
|
|
value=restored_value,
|
|
)
|
|
_store_remote_session_bridge_value(record.bridge_key, restored_value)
|
|
if resolution_stats is not None:
|
|
resolution_stats.durable_bridge_hits += 1
|
|
resolution_stats.session_restore_writes += 1
|
|
resolution_stats.direct_restore_seconds += (
|
|
time.perf_counter() - restore_started_at
|
|
)
|
|
logger.info(
|
|
"Resolved remote session bridge bridge_key=%s from durable %s rehydration plan into session_id=%s.",
|
|
record.bridge_key,
|
|
record.rehydration_plan_io_type or "bridge",
|
|
target_session_handle.session_id,
|
|
)
|
|
return restored_value
|
|
|
|
|
|
def _remote_session_bridge_replay_stack() -> set[str]:
|
|
"""Return the thread-local guard set for bridge replay recursion detection."""
|
|
replay_stack = getattr(_REMOTE_SESSION_BRIDGE_REPLAY_STATE, "bridge_keys", None)
|
|
if replay_stack is None:
|
|
replay_stack = set()
|
|
_REMOTE_SESSION_BRIDGE_REPLAY_STATE.bridge_keys = replay_stack
|
|
return replay_stack
|
|
|
|
|
|
def _bridge_record_replays_sampling_node(record: RemoteSessionBridgeRecord) -> bool:
|
|
"""Return whether replaying one bridge record would rerun sampler work."""
|
|
execute_node_ids = [
|
|
str(node_id)
|
|
for node_id in record.producer_payload.get("execute_node_ids", [])
|
|
if str(node_id)
|
|
]
|
|
if not execute_node_ids:
|
|
return False
|
|
subgraph_prompt = record.producer_payload.get("subgraph_prompt")
|
|
if not isinstance(subgraph_prompt, dict):
|
|
return False
|
|
required_node_ids = _resolve_required_subgraph_nodes(
|
|
prompt=subgraph_prompt,
|
|
execute_node_ids=execute_node_ids,
|
|
)
|
|
return _subgraph_contains_sampling_node(subgraph_prompt, required_node_ids)
|
|
|
|
|
|
def _subgraph_contains_sampling_node(
|
|
subgraph_prompt: Mapping[str, Any],
|
|
node_ids: Iterable[str],
|
|
) -> bool:
|
|
"""Return whether any selected node looks like sampler work."""
|
|
for node_id in node_ids:
|
|
prompt_node = subgraph_prompt.get(node_id)
|
|
if not isinstance(prompt_node, Mapping):
|
|
continue
|
|
class_type = str(prompt_node.get("class_type") or "")
|
|
if "sampler" in class_type.lower():
|
|
return True
|
|
return False
|
|
|
|
|
|
def _rehydrate_remote_session_bridge_value(
|
|
ref: RemoteSessionBridgeRef,
|
|
*,
|
|
target_session_handle: RemoteSessionHandle | None,
|
|
node_mapping: dict[str, type[Any]] | None = None,
|
|
resolution_stats: "_RemoteSessionBridgeResolutionStats | None" = None,
|
|
) -> Any:
|
|
"""Replay one producer phase into the current session when the live value is gone."""
|
|
if target_session_handle is None:
|
|
raise RemoteSessionStateError(
|
|
"Remote session bridge replay requires a target remote_session handle."
|
|
)
|
|
|
|
cached_value = _get_remote_session_bridge_value(ref.bridge_key)
|
|
if cached_value is not None:
|
|
restore_started_at = time.perf_counter()
|
|
_REMOTE_SESSION_STORE.put_bridge_output(
|
|
target_session_handle,
|
|
bridge_key=ref.bridge_key,
|
|
node_id=ref.node_id,
|
|
output_index=ref.output_index,
|
|
value=cached_value,
|
|
)
|
|
if resolution_stats is not None:
|
|
resolution_stats.bridge_cache_hits += 1
|
|
resolution_stats.session_restore_writes += 1
|
|
resolution_stats.direct_restore_seconds += (
|
|
time.perf_counter() - restore_started_at
|
|
)
|
|
logger.info(
|
|
"Resolved remote session bridge bridge_key=%s directly from warm cache into session_id=%s.",
|
|
ref.bridge_key,
|
|
target_session_handle.session_id,
|
|
)
|
|
return cached_value
|
|
|
|
replay_stack = _remote_session_bridge_replay_stack()
|
|
if ref.bridge_key in replay_stack:
|
|
raise RemoteSessionStateError(
|
|
f"Detected recursive remote session bridge replay for {ref.bridge_key!r}."
|
|
)
|
|
|
|
record_lookup_started_at = time.perf_counter()
|
|
record = _REMOTE_SESSION_BRIDGE_STORE.get_record(ref.bridge_key)
|
|
if resolution_stats is not None:
|
|
resolution_stats.bridge_record_lookups += 1
|
|
resolution_stats.bridge_record_lookup_seconds += (
|
|
time.perf_counter() - record_lookup_started_at
|
|
)
|
|
restored_value = _restore_serialized_remote_session_bridge_value(
|
|
record,
|
|
target_session_handle=target_session_handle,
|
|
resolution_stats=resolution_stats,
|
|
)
|
|
if restored_value is not None:
|
|
return restored_value
|
|
restored_value = _restore_planned_remote_session_bridge_value(
|
|
record,
|
|
target_session_handle=target_session_handle,
|
|
node_mapping=node_mapping,
|
|
resolution_stats=resolution_stats,
|
|
)
|
|
if restored_value is not None:
|
|
return restored_value
|
|
if _bridge_record_replays_sampling_node(record):
|
|
message = (
|
|
"Remote session bridge replay would rerun a sampler component "
|
|
f"for bridge_key={ref.bridge_key!r}; expected live or warm bridge reuse instead."
|
|
)
|
|
logger.error(message)
|
|
raise RemoteSessionStateError(message)
|
|
replay_payload = copy.deepcopy(record.producer_payload)
|
|
replay_payload["remote_session"] = target_session_handle.to_payload()
|
|
replay_payload.pop("clear_remote_session", None)
|
|
if target_session_handle.prompt_id is not None:
|
|
replay_payload["prompt_id"] = target_session_handle.prompt_id
|
|
replay_inputs = _deserialize_remote_session_bridge_producer_inputs(record)
|
|
|
|
logger.info(
|
|
"Replaying remote session bridge bridge_key=%s into session_id=%s via component=%s.",
|
|
ref.bridge_key,
|
|
target_session_handle.session_id,
|
|
replay_payload.get("component_id"),
|
|
)
|
|
replay_stack.add(ref.bridge_key)
|
|
replay_started_at = time.perf_counter()
|
|
try:
|
|
_execute_subgraph_prompt(replay_payload, replay_inputs, node_mapping)
|
|
finally:
|
|
replay_stack.remove(ref.bridge_key)
|
|
if resolution_stats is not None:
|
|
resolution_stats.replay_count += 1
|
|
resolution_stats.replay_seconds += time.perf_counter() - replay_started_at
|
|
|
|
return _REMOTE_SESSION_STORE.get_output(
|
|
RemoteSessionValueRef(
|
|
session_id=target_session_handle.session_id,
|
|
node_id=ref.node_id,
|
|
output_index=ref.output_index,
|
|
)
|
|
)
|
|
|
|
|
|
def _resolve_remote_session_inputs(
|
|
hydrated_inputs: dict[str, Any],
|
|
*,
|
|
component_id: str | None = None,
|
|
target_session_handle: RemoteSessionHandle | None = None,
|
|
node_mapping: dict[str, type[Any]] | None = None,
|
|
resolution_stats: "_RemoteSessionBridgeResolutionStats | None" = None,
|
|
) -> dict[str, Any]:
|
|
"""Resolve any remote-session value refs embedded in boundary inputs."""
|
|
|
|
def count_refs(value: Any) -> int:
|
|
"""Return the number of live or durable session refs nested in one value."""
|
|
if is_remote_session_value_ref_payload(
|
|
value
|
|
) or is_remote_session_bridge_ref_payload(value):
|
|
return 1
|
|
if isinstance(value, list | tuple):
|
|
return sum(count_refs(item) for item in value)
|
|
if isinstance(value, Mapping):
|
|
return sum(count_refs(item) for item in value.values())
|
|
return 0
|
|
|
|
ref_counts_by_input = {
|
|
input_name: count_refs(input_value)
|
|
for input_name, input_value in hydrated_inputs.items()
|
|
}
|
|
ref_input_names = [
|
|
input_name
|
|
for input_name, ref_count in ref_counts_by_input.items()
|
|
if ref_count > 0
|
|
]
|
|
total_ref_count = sum(ref_counts_by_input.values())
|
|
if ref_input_names:
|
|
logger.info(
|
|
"Resolving %d remote session input refs for component=%s inputs=%s.",
|
|
total_ref_count,
|
|
component_id or "<unknown>",
|
|
sorted(ref_input_names),
|
|
)
|
|
if resolution_stats is not None:
|
|
resolution_stats.input_ref_count += total_ref_count
|
|
return {
|
|
input_name: _REMOTE_SESSION_STORE.resolve_value_with_bridges(
|
|
input_value,
|
|
target_session_handle=target_session_handle,
|
|
resolution_callback=(
|
|
lambda event_name, event_payload: _record_remote_session_resolution_event(
|
|
resolution_stats,
|
|
event_name,
|
|
event_payload,
|
|
)
|
|
)
|
|
if resolution_stats is not None
|
|
else None,
|
|
bridge_resolver=lambda ref: _rehydrate_remote_session_bridge_value(
|
|
ref,
|
|
target_session_handle=target_session_handle,
|
|
node_mapping=node_mapping,
|
|
resolution_stats=resolution_stats,
|
|
),
|
|
)
|
|
for input_name, input_value in hydrated_inputs.items()
|
|
}
|
|
|
|
|
|
def _log_remote_session_resolution_summary(
|
|
*,
|
|
component_id: str,
|
|
resolution_stats: "_RemoteSessionBridgeResolutionStats",
|
|
) -> None:
|
|
"""Emit one high-signal summary for local fallback bridge resolution work."""
|
|
if resolution_stats.input_ref_count <= 0:
|
|
return
|
|
logger.info(
|
|
"Remote session resolution summary component=%s refs=%d live_hits=%d warm_bridge_hits=%d durable_bridge_hits=%d bridge_record_lookups=%d bridge_record_lookup_seconds=%.3f replay_count=%d replay_seconds=%.3f direct_restore_seconds=%.3f session_restore_writes=%d.",
|
|
component_id,
|
|
resolution_stats.input_ref_count,
|
|
resolution_stats.live_session_hits,
|
|
resolution_stats.bridge_cache_hits,
|
|
resolution_stats.durable_bridge_hits,
|
|
resolution_stats.bridge_record_lookups,
|
|
resolution_stats.bridge_record_lookup_seconds,
|
|
resolution_stats.replay_count,
|
|
resolution_stats.replay_seconds,
|
|
resolution_stats.direct_restore_seconds,
|
|
resolution_stats.session_restore_writes,
|
|
)
|
|
|
|
|
|
def _remote_worker_pool_affinity_key(slot_index: int) -> str:
|
|
"""Return the stable worker-pool affinity key for one reusable remote slot."""
|
|
return f"worker-pool:slot:{int(slot_index)}"
|
|
|
|
|
|
def _component_pool_slot_index(payload: dict[str, Any]) -> int:
|
|
"""Return the reusable worker-pool slot index for one ordinary remote payload."""
|
|
warmup_slot_index = payload.get("warmup_slot_index")
|
|
if warmup_slot_index is not None:
|
|
return max(0, int(warmup_slot_index))
|
|
if bool(payload.get("remote_local_gap_pool")):
|
|
return 0
|
|
|
|
prompt_parallelism_target = _prompt_parallelism_target(payload)
|
|
slot_count = max(1, int(prompt_parallelism_target))
|
|
component_id = str(payload.get("component_id") or "")
|
|
if not component_id:
|
|
return 0
|
|
|
|
component_hash = hashlib.sha256(component_id.encode("utf-8")).digest()
|
|
return int.from_bytes(component_hash[:8], "big") % slot_count
|
|
|
|
|
|
def _remote_worker_affinity_key(payload: dict[str, Any]) -> str:
|
|
"""Return the reusable worker-pool affinity key for one remote payload."""
|
|
configured_affinity_group = payload.get("remote_worker_affinity_group")
|
|
if configured_affinity_group is None:
|
|
return _remote_worker_pool_affinity_key(_component_pool_slot_index(payload))
|
|
affinity_group = str(configured_affinity_group).strip().lower()
|
|
if affinity_group not in {"comfy", "llm"}:
|
|
affinity_group = "comfy"
|
|
slot_index = _component_pool_slot_index(payload)
|
|
return f"worker-pool:{affinity_group}:slot:{slot_index}"
|
|
|
|
|
|
def _mapped_lane_affinity_key(payload: dict[str, Any], lane_index: int) -> str | None:
|
|
"""Return the stable per-lane worker-pool affinity key used for mapped execution."""
|
|
del payload
|
|
return _remote_worker_pool_affinity_key(lane_index)
|
|
|
|
|
|
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 bundle ZIP or manifest 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"
|
|
/ local_bundle.stem
|
|
)
|
|
extraction_root.mkdir(parents=True, exist_ok=True)
|
|
for archive_path in _resolve_local_custom_nodes_archives(
|
|
local_bundle, settings.local_storage_root
|
|
):
|
|
with zipfile.ZipFile(archive_path, "r") as archive:
|
|
archive.extractall(extraction_root)
|
|
_materialize_local_custom_node_assets(
|
|
local_bundle,
|
|
settings.local_storage_root,
|
|
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 _resolve_local_custom_nodes_archives(
|
|
local_bundle: Path, storage_root: Path
|
|
) -> list[Path]:
|
|
"""Return the archive paths described by one local custom_nodes bundle ZIP or manifest."""
|
|
if local_bundle.suffix.lower() == ".zip":
|
|
return [local_bundle]
|
|
if local_bundle.suffix.lower() != ".json":
|
|
raise RuntimeError(
|
|
f"Unsupported custom_nodes bundle format {local_bundle.suffix!r} for {local_bundle}."
|
|
)
|
|
|
|
manifest_payload = _load_local_custom_nodes_manifest(local_bundle)
|
|
entry_payloads = manifest_payload.get("entries")
|
|
if not isinstance(entry_payloads, list):
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} did not contain a valid entries list."
|
|
)
|
|
|
|
archive_paths: list[Path] = []
|
|
for entry_payload in entry_payloads:
|
|
if not isinstance(entry_payload, dict):
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} contained a non-object entry."
|
|
)
|
|
remote_path = entry_payload.get("remote_path")
|
|
if not isinstance(remote_path, str) or not remote_path.strip():
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} contained an entry without remote_path."
|
|
)
|
|
archive_path = storage_root / remote_path.lstrip("/")
|
|
if not archive_path.exists():
|
|
raise RuntimeError(
|
|
f"Custom nodes archive {remote_path} referenced by {local_bundle} was not found in local storage."
|
|
)
|
|
archive_paths.append(archive_path)
|
|
return archive_paths
|
|
|
|
|
|
def _load_local_custom_nodes_manifest(local_bundle: Path) -> dict[str, Any]:
|
|
"""Load and validate one local custom-node bundle manifest."""
|
|
try:
|
|
manifest_payload = json.loads(local_bundle.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} is unreadable."
|
|
) from exc
|
|
if not isinstance(manifest_payload, dict):
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} must be a JSON object."
|
|
)
|
|
if manifest_payload.get("version", 1) not in {1, 2}:
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} has an unsupported version."
|
|
)
|
|
return manifest_payload
|
|
|
|
|
|
def _materialize_local_custom_node_assets(
|
|
local_bundle: Path,
|
|
storage_root: Path,
|
|
extraction_root: Path,
|
|
) -> None:
|
|
"""Link version-two package assets into the local fallback extraction tree."""
|
|
if local_bundle.suffix.lower() != ".json":
|
|
return
|
|
manifest_payload = _load_local_custom_nodes_manifest(local_bundle)
|
|
if manifest_payload.get("version", 1) < 2:
|
|
return
|
|
for asset_payload in _iter_local_custom_node_assets(local_bundle, manifest_payload):
|
|
relative_path = _validated_local_custom_node_asset_path(
|
|
local_bundle, asset_payload
|
|
)
|
|
source_path = storage_root / str(asset_payload["remote_path"]).lstrip("/")
|
|
if not source_path.is_file():
|
|
raise RuntimeError(
|
|
f"Custom-node asset {source_path} was not found in local storage."
|
|
)
|
|
if source_path.stat().st_size != int(asset_payload["size_bytes"]):
|
|
raise RuntimeError(
|
|
f"Custom-node asset {source_path} size did not match its manifest."
|
|
)
|
|
destination = extraction_root / relative_path
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
if destination.exists() or destination.is_symlink():
|
|
if (
|
|
destination.is_symlink()
|
|
and destination.resolve() == source_path.resolve()
|
|
):
|
|
continue
|
|
raise RuntimeError(
|
|
f"Custom-node asset destination {destination} already exists."
|
|
)
|
|
destination.symlink_to(source_path)
|
|
|
|
|
|
def _iter_local_custom_node_assets(
|
|
local_bundle: Path,
|
|
manifest_payload: Mapping[str, Any],
|
|
) -> Iterator[dict[str, Any]]:
|
|
"""Yield asset objects from one validated version-two local manifest."""
|
|
entries = manifest_payload.get("entries")
|
|
if not isinstance(entries, list):
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} has no valid entries list."
|
|
)
|
|
for entry in entries:
|
|
if not isinstance(entry, dict) or not isinstance(entry.get("assets", []), list):
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} contains invalid assets."
|
|
)
|
|
for asset_payload in entry.get("assets", []):
|
|
if not isinstance(asset_payload, dict):
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} contains an invalid asset."
|
|
)
|
|
yield asset_payload
|
|
|
|
|
|
def _validated_local_custom_node_asset_path(
|
|
local_bundle: Path,
|
|
asset_payload: Mapping[str, Any],
|
|
) -> Path:
|
|
"""Return one safe local extraction-relative package asset path."""
|
|
relative_path = asset_payload.get("relative_path")
|
|
remote_path = asset_payload.get("remote_path")
|
|
sha256 = asset_payload.get("sha256")
|
|
size_bytes = asset_payload.get("size_bytes")
|
|
if not isinstance(relative_path, str) or not relative_path:
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} contains an asset without a path."
|
|
)
|
|
if not isinstance(remote_path, str) or not remote_path:
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} contains an asset without storage."
|
|
)
|
|
if (
|
|
not isinstance(sha256, str)
|
|
or len(sha256) != 64
|
|
or not Path(remote_path).name.startswith(f"{sha256}_")
|
|
):
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} contains an invalid asset digest."
|
|
)
|
|
if (
|
|
isinstance(size_bytes, bool)
|
|
or not isinstance(size_bytes, int)
|
|
or size_bytes < 0
|
|
):
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} contains an invalid asset size."
|
|
)
|
|
normalized_path = Path(relative_path)
|
|
if normalized_path.is_absolute() or ".." in normalized_path.parts:
|
|
raise RuntimeError(
|
|
f"Custom nodes manifest {local_bundle} contains an unsafe asset path."
|
|
)
|
|
return normalized_path
|
|
|
|
|
|
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."""
|
|
outputs = _execute_node_locally_raw(
|
|
node_data,
|
|
kwargs_payload,
|
|
node_mapping=node_mapping,
|
|
)
|
|
return serialize_node_outputs(outputs)
|
|
|
|
|
|
def _execute_node_locally_raw(
|
|
node_data: dict[str, Any],
|
|
kwargs_payload: bytes | bytearray | str | dict[str, Any],
|
|
node_mapping: dict[str, type[Any]] | None = None,
|
|
) -> tuple[Any, ...]:
|
|
"""Execute a single target node in-process and return raw node 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.")
|
|
return _invoke_original_node(node_mapping[class_type], node_data, kwargs)
|
|
|
|
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.")
|
|
|
|
return _invoke_original_node(
|
|
resolved_node_mapping[class_type], node_data, kwargs
|
|
)
|
|
|
|
|
|
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 = unwrap_mapped_output_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 = prompt[node_id]
|
|
prompt_node["inputs"][input_name] = _normalize_prompt_input_value(
|
|
value,
|
|
io_type=io_type,
|
|
)
|
|
source_signature = boundary_input.get("source_signature")
|
|
if isinstance(source_signature, str) and source_signature:
|
|
boundary_signatures = prompt_node.setdefault(
|
|
_BOUNDARY_INPUT_SIGNATURES_KEY, {}
|
|
)
|
|
if isinstance(boundary_signatures, dict):
|
|
boundary_signatures[input_name] = source_signature
|
|
|
|
|
|
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_types(
|
|
node_class: type[Any],
|
|
live_inputs: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Return one node class's input schema finalized for the live prompt inputs."""
|
|
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 {}
|
|
|
|
has_v3_dynamic_inputs = any(
|
|
isinstance(input_config, tuple)
|
|
and bool(input_config)
|
|
and isinstance(input_config[0], str)
|
|
and input_config[0].startswith("COMFY_")
|
|
and input_config[0].endswith("_V3")
|
|
for section_name in ("required", "optional")
|
|
for input_config in (
|
|
raw_input_types.get(section_name, {}).values()
|
|
if isinstance(raw_input_types.get(section_name), dict)
|
|
else ()
|
|
)
|
|
)
|
|
if not has_v3_dynamic_inputs:
|
|
return raw_input_types
|
|
|
|
from comfy_api.latest import _io
|
|
|
|
finalized_input_types, _, _ = _io.get_finalized_class_inputs(
|
|
raw_input_types,
|
|
live_inputs or {},
|
|
)
|
|
logger.debug(
|
|
"Finalized V3 input schema for %s against prompt inputs %s.",
|
|
node_class.__name__,
|
|
sorted(str(input_name) for input_name in (live_inputs or {})),
|
|
)
|
|
return finalized_input_types
|
|
|
|
|
|
def _node_input_type_map(
|
|
node_class: type[Any],
|
|
live_inputs: dict[str, Any] | None = None,
|
|
) -> dict[str, str]:
|
|
"""Return one node class's finalized input types keyed by prompt input name."""
|
|
input_types = _node_input_types(node_class, live_inputs)
|
|
|
|
input_type_map: dict[str, str] = {}
|
|
for section_name in ("required", "optional", "hidden"):
|
|
section = 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 _node_required_input_names(
|
|
node_class: type[Any],
|
|
live_inputs: dict[str, Any] | None = None,
|
|
) -> set[str]:
|
|
"""Return required input names after expanding V3 dynamic prompt inputs."""
|
|
required_inputs = _node_input_types(node_class, live_inputs).get("required")
|
|
if not isinstance(required_inputs, dict):
|
|
return set()
|
|
return {str(input_name) for input_name in required_inputs}
|
|
|
|
|
|
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
|
|
inputs = prompt_node.get("inputs") or {}
|
|
input_type_map = _node_input_type_map(node_class, inputs)
|
|
if not input_type_map:
|
|
continue
|
|
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
|
|
inputs = prompt_node.get("inputs") or {}
|
|
input_type_map = _node_input_type_map(node_class, inputs)
|
|
if not input_type_map:
|
|
continue
|
|
for input_name, input_value in 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
|
|
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 _validate_required_prompt_inputs(
|
|
prompt: dict[str, Any],
|
|
node_mapping: dict[str, type[Any]],
|
|
) -> None:
|
|
"""Reject remote prompt nodes that are missing declared required inputs."""
|
|
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
|
|
inputs = prompt_node.get("inputs") or {}
|
|
missing_inputs = sorted(
|
|
_node_required_input_names(node_class, inputs) - set(inputs.keys())
|
|
)
|
|
if not missing_inputs:
|
|
continue
|
|
raise RemoteSubgraphExecutionError(
|
|
"Remote subgraph prompt is missing required node inputs before execution."
|
|
f" node_id={node_id!r} node_type={class_type!r}"
|
|
f" missing_inputs={missing_inputs!r} available_inputs={sorted(str(key) for key in inputs)!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 _iter_prompt_links(value: Any) -> Iterator[list[Any]]:
|
|
"""Yield ComfyUI prompt links found inside a prompt input value."""
|
|
if _is_link(value):
|
|
yield value
|
|
return
|
|
if isinstance(value, Mapping):
|
|
for nested_value in value.values():
|
|
yield from _iter_prompt_links(nested_value)
|
|
return
|
|
if isinstance(value, list):
|
|
for nested_value in value:
|
|
yield from _iter_prompt_links(nested_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)
|
|
)
|
|
session_handle = _payload_remote_session_handle(normalized_payload)
|
|
resolution_stats = _RemoteSessionBridgeResolutionStats()
|
|
resolved_inputs = _resolve_remote_session_inputs(
|
|
dict(hydrated_inputs),
|
|
component_id=str(payload.get("component_id") or ""),
|
|
target_session_handle=session_handle,
|
|
node_mapping=node_mapping,
|
|
resolution_stats=resolution_stats,
|
|
)
|
|
_log_remote_session_resolution_summary(
|
|
component_id=str(payload.get("component_id") or "modal-subgraph"),
|
|
resolution_stats=resolution_stats,
|
|
)
|
|
short_circuit_outputs = _short_circuit_restored_session_output_subgraph(
|
|
payload=normalized_payload,
|
|
hydrated_inputs=hydrated_inputs,
|
|
session_handle=session_handle,
|
|
resolution_stats=resolution_stats,
|
|
)
|
|
if short_circuit_outputs is not None:
|
|
logger.info(
|
|
"Skipping mapped-node execution for component=%s because all %d session-backed outputs were restored into session_id=%s.",
|
|
payload.get("component_id"),
|
|
len(short_circuit_outputs),
|
|
session_handle.session_id if session_handle is not None else None,
|
|
)
|
|
return short_circuit_outputs
|
|
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", [])),
|
|
)
|
|
_validate_required_prompt_inputs(prompt, node_mapping)
|
|
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."
|
|
)
|
|
bridge_record = _build_remote_session_bridge_record(
|
|
payload=normalized_payload,
|
|
hydrated_inputs=hydrated_inputs,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
io_type=str(boundary_output.get("io_type") or "*"),
|
|
output_value=output_value,
|
|
)
|
|
_REMOTE_SESSION_BRIDGE_STORE.put_record(bridge_record)
|
|
_store_remote_session_bridge_value(bridge_record.bridge_key, output_value)
|
|
live_ref = _REMOTE_SESSION_STORE.put_bridge_output(
|
|
session_handle,
|
|
bridge_key=bridge_record.bridge_key,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
value=output_value,
|
|
)
|
|
output_value = RemoteSessionBridgeRef(
|
|
bridge_key=bridge_record.bridge_key,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
session_id=live_ref.session_id,
|
|
).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)
|
|
)
|
|
session_handle = _payload_remote_session_handle(normalized_payload)
|
|
resolution_stats = _RemoteSessionBridgeResolutionStats()
|
|
resolved_inputs = _resolve_remote_session_inputs(
|
|
dict(hydrated_inputs),
|
|
component_id=str(payload.get("component_id") or ""),
|
|
target_session_handle=session_handle,
|
|
node_mapping=node_mapping,
|
|
resolution_stats=resolution_stats,
|
|
)
|
|
_log_remote_session_resolution_summary(
|
|
component_id=str(payload.get("component_id") or "modal-subgraph"),
|
|
resolution_stats=resolution_stats,
|
|
)
|
|
short_circuit_outputs = _short_circuit_restored_session_output_subgraph(
|
|
payload=normalized_payload,
|
|
hydrated_inputs=hydrated_inputs,
|
|
session_handle=session_handle,
|
|
resolution_stats=resolution_stats,
|
|
)
|
|
if short_circuit_outputs is not None:
|
|
logger.info(
|
|
"Skipping PromptExecutor execution for component=%s because all %d session-backed outputs were restored into session_id=%s.",
|
|
payload.get("component_id"),
|
|
len(short_circuit_outputs),
|
|
session_handle.session_id if session_handle is not None else None,
|
|
)
|
|
return short_circuit_outputs
|
|
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", [])),
|
|
)
|
|
_validate_required_prompt_inputs(prompt, resolved_node_mapping)
|
|
|
|
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."
|
|
)
|
|
bridge_record = _build_remote_session_bridge_record(
|
|
payload=normalized_payload,
|
|
hydrated_inputs=hydrated_inputs,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
io_type=str(boundary_output.get("io_type") or "*"),
|
|
output_value=output_value,
|
|
)
|
|
_REMOTE_SESSION_BRIDGE_STORE.put_record(bridge_record)
|
|
_store_remote_session_bridge_value(
|
|
bridge_record.bridge_key, output_value
|
|
)
|
|
live_ref = _REMOTE_SESSION_STORE.put_bridge_output(
|
|
session_handle,
|
|
bridge_key=bridge_record.bridge_key,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
value=output_value,
|
|
)
|
|
output_value = RemoteSessionBridgeRef(
|
|
bridge_key=bridge_record.bridge_key,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
session_id=live_ref.session_id,
|
|
).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 _short_circuit_restored_session_output_subgraph(
|
|
*,
|
|
payload: dict[str, Any],
|
|
hydrated_inputs: dict[str, Any],
|
|
session_handle: RemoteSessionHandle | None,
|
|
resolution_stats: _RemoteSessionBridgeResolutionStats,
|
|
) -> tuple[Any, ...] | None:
|
|
"""Return session-backed outputs directly when bridge restoration already satisfied them all."""
|
|
boundary_outputs = list(payload.get("boundary_outputs", []))
|
|
if session_handle is None or not boundary_outputs:
|
|
return None
|
|
if resolution_stats.input_ref_count <= 0 or resolution_stats.replay_count > 0:
|
|
return None
|
|
if any(
|
|
not bool(boundary_output.get("session_output"))
|
|
for boundary_output in boundary_outputs
|
|
):
|
|
return None
|
|
|
|
restored_outputs: list[Any] = []
|
|
for boundary_output in boundary_outputs:
|
|
node_id = str(boundary_output["node_id"])
|
|
output_index = int(boundary_output["output_index"])
|
|
try:
|
|
output_value = _REMOTE_SESSION_STORE.get_output(
|
|
RemoteSessionValueRef(
|
|
session_id=session_handle.session_id,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
)
|
|
)
|
|
except RemoteSessionStateError:
|
|
return None
|
|
|
|
bridge_record = _build_remote_session_bridge_record(
|
|
payload=payload,
|
|
hydrated_inputs=hydrated_inputs,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
io_type=str(boundary_output.get("io_type") or "*"),
|
|
output_value=output_value,
|
|
)
|
|
_REMOTE_SESSION_BRIDGE_STORE.put_record(bridge_record)
|
|
_store_remote_session_bridge_value(bridge_record.bridge_key, output_value)
|
|
live_ref = _REMOTE_SESSION_STORE.put_bridge_output(
|
|
session_handle,
|
|
bridge_key=bridge_record.bridge_key,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
value=output_value,
|
|
)
|
|
restored_outputs.append(
|
|
RemoteSessionBridgeRef(
|
|
bridge_key=bridge_record.bridge_key,
|
|
node_id=node_id,
|
|
output_index=output_index,
|
|
session_id=live_ref.session_id,
|
|
).to_payload()
|
|
)
|
|
return tuple(restored_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 _is_missing_modal_deployment_error(exc: BaseException) -> bool:
|
|
"""Return whether one Modal lookup failure indicates missing deployed app state."""
|
|
message = str(exc).strip().lower()
|
|
if any(
|
|
marker in message
|
|
for marker in (
|
|
"could not deserialize remote exception",
|
|
"remote traceback",
|
|
"remotesubgraphexecutionerror",
|
|
"object of type",
|
|
"is not json serializable",
|
|
)
|
|
):
|
|
return False
|
|
if "not deployed" in message:
|
|
return True
|
|
if "not found" not in message:
|
|
return False
|
|
return any(
|
|
marker in message
|
|
for marker in (
|
|
"lookup failed for cls",
|
|
"app '",
|
|
'app "',
|
|
"class '",
|
|
'class "',
|
|
"not found in environment",
|
|
)
|
|
)
|
|
|
|
|
|
def _load_modal_cloud_module() -> Any:
|
|
"""Load the stable Modal cloud entry module under a valid Python name."""
|
|
settings = getattr(_MODAL_CLOUD_SETTINGS_STATE, "settings", None) or get_settings()
|
|
deployment_app_name = modal_deployment_app_name(settings)
|
|
with _MODAL_CLOUD_MODULE_LOCK:
|
|
existing_module = sys.modules.get(_MODAL_CLOUD_MODULE_NAME)
|
|
existing_gpu = getattr(
|
|
existing_module, "__comfy_modal_gpu__", settings.modal_gpu
|
|
)
|
|
existing_app_name = getattr(existing_module, "__comfy_modal_app_name__", None)
|
|
existing_secret_name = getattr(
|
|
existing_module, "__comfy_modal_secret_name__", None
|
|
)
|
|
if (
|
|
existing_module is not None
|
|
and getattr(existing_module, "app", None) is not None
|
|
and existing_gpu == settings.modal_gpu
|
|
and existing_app_name == deployment_app_name
|
|
and existing_secret_name == settings.modal_secret_name
|
|
):
|
|
return existing_module
|
|
if existing_module is not None:
|
|
logger.warning(
|
|
"Discarding Modal cloud module %s before reload for app=%s gpu=%s secret=%s (previous_app=%s previous_gpu=%s previous_secret=%s).",
|
|
_MODAL_CLOUD_MODULE_NAME,
|
|
deployment_app_name,
|
|
settings.modal_gpu,
|
|
settings.modal_secret_name,
|
|
existing_app_name,
|
|
existing_gpu,
|
|
existing_secret_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)
|
|
setattr(cloud_module, "__comfy_modal_settings_override__", settings)
|
|
setattr(cloud_module, "__comfy_modal_gpu__", settings.modal_gpu)
|
|
setattr(cloud_module, "__comfy_modal_app_name__", deployment_app_name)
|
|
setattr(cloud_module, "__comfy_modal_secret_name__", settings.modal_secret_name)
|
|
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 _install_modal_cloud_exception_compatibility_module() -> None:
|
|
"""Expose cloud exception definitions without loading the deployable cloud app."""
|
|
if _MODAL_CLOUD_MODULE_NAME in sys.modules:
|
|
return
|
|
|
|
compatibility_module = ModuleType(_MODAL_CLOUD_MODULE_NAME)
|
|
setattr(
|
|
compatibility_module,
|
|
"RemoteSubgraphExecutionError",
|
|
RemoteSubgraphExecutionError,
|
|
)
|
|
exception_bases: dict[str, type[BaseException]] = {
|
|
"RemoteInvocationInProgressError": RuntimeError,
|
|
"RemoteInvocationAbandonedError": RuntimeError,
|
|
"RemoteCanaryInterruptedError": RuntimeError,
|
|
"RemoteCanaryBarrierTimeoutError": TimeoutError,
|
|
"ExistingModalAppError": RuntimeError,
|
|
}
|
|
for exception_name, exception_base in exception_bases.items():
|
|
setattr(
|
|
compatibility_module,
|
|
exception_name,
|
|
type(
|
|
exception_name,
|
|
(exception_base,),
|
|
{"__module__": _MODAL_CLOUD_MODULE_NAME},
|
|
),
|
|
)
|
|
sys.modules[_MODAL_CLOUD_MODULE_NAME] = compatibility_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 _record_local_modal_ui_event(
|
|
event: str, payload: Mapping[str, Any], client_id: str | None
|
|
) -> None:
|
|
"""Record one local Modal UI event for browser refocus replay when available."""
|
|
if client_id is None:
|
|
return
|
|
try:
|
|
from ..api_intercept import record_modal_ui_event
|
|
except (ImportError, AttributeError):
|
|
logger.debug(
|
|
"Modal UI event replay recorder is unavailable for event %s.", event
|
|
)
|
|
return
|
|
|
|
record_modal_ui_event(event, payload, client_id)
|
|
|
|
|
|
def _emit_local_modal_status(
|
|
*,
|
|
prompt_id: str | None,
|
|
client_id: str | None,
|
|
phase: str,
|
|
node_ids: list[str],
|
|
modal_gpu: str | None = None,
|
|
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,
|
|
completed_ancestor_node_ids: list[str] | None = None,
|
|
execution_provider: str | None = None,
|
|
execution_environment_id: str | None = None,
|
|
execution_location: str | 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 modal_gpu is not None:
|
|
payload["modal_gpu"] = modal_gpu
|
|
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)
|
|
if completed_ancestor_node_ids:
|
|
payload["completed_ancestor_node_ids"] = list(completed_ancestor_node_ids)
|
|
if execution_provider:
|
|
payload["execution_provider"] = execution_provider
|
|
if execution_environment_id:
|
|
payload["execution_environment_id"] = execution_environment_id
|
|
if execution_location:
|
|
payload["execution_location"] = execution_location
|
|
_record_local_modal_ui_event("modal_status", payload, client_id)
|
|
prompt_server.send_sync("modal_status", payload, client_id)
|
|
|
|
|
|
def _remote_execution_destination(payload: Mapping[str, Any]) -> str:
|
|
"""Return the user-facing destination for a provider-stamped payload."""
|
|
environment_id = str(payload.get("execution_environment_id") or "").strip()
|
|
provider = str(payload.get("execution_provider") or "modal").strip()
|
|
return (
|
|
environment_id
|
|
if provider != "modal" and environment_id
|
|
else "Modal"
|
|
)
|
|
|
|
|
|
def _remote_execution_identity(
|
|
payload: Mapping[str, Any],
|
|
modal_task_id: str | None = None,
|
|
) -> dict[str, str]:
|
|
"""Return non-empty provider, environment, and runtime location fields."""
|
|
provider = str(
|
|
payload.get("execution_provider") or ("modal" if modal_task_id else "")
|
|
).strip()
|
|
environment_id = str(payload.get("execution_environment_id") or "").strip()
|
|
location = (
|
|
modal_task_id
|
|
if provider == "modal" and modal_task_id
|
|
else str(payload.get("execution_location") or "").strip()
|
|
)
|
|
return {
|
|
key: value
|
|
for key, value in {
|
|
"execution_provider": provider,
|
|
"execution_environment_id": environment_id,
|
|
"execution_location": location,
|
|
}.items()
|
|
if value
|
|
}
|
|
|
|
|
|
def _emit_local_remote_dispatch_status(payload: dict[str, Any]) -> None:
|
|
"""Tell the local UI a remote component was dispatched before progress streams."""
|
|
_emit_local_remote_startup_status(
|
|
payload,
|
|
phase="starting",
|
|
status_message=(
|
|
f"Starting remote component on {_remote_execution_destination(payload)}"
|
|
),
|
|
)
|
|
|
|
|
|
def _emit_local_remote_startup_status(
|
|
payload: Mapping[str, Any],
|
|
*,
|
|
phase: str,
|
|
status_message: str,
|
|
) -> None:
|
|
"""Show one prompt-scoped Modal startup phase in the local global status pill."""
|
|
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", [])
|
|
if str(node_id)
|
|
]
|
|
if not node_ids and payload.get("component_id") is not None:
|
|
node_ids = [str(payload["component_id"])]
|
|
if not prompt_id or not client_id or not node_ids:
|
|
return
|
|
_emit_local_modal_status(
|
|
prompt_id=prompt_id,
|
|
client_id=client_id,
|
|
phase=phase,
|
|
node_ids=node_ids,
|
|
modal_gpu=(
|
|
str(payload["modal_gpu"]) if payload.get("modal_gpu") is not None else None
|
|
),
|
|
status_message=status_message,
|
|
**_remote_execution_identity(payload),
|
|
)
|
|
|
|
|
|
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,
|
|
setup_only: bool = False,
|
|
cached_hit: bool = False,
|
|
completed_ancestor_node_ids: list[str] | None = None,
|
|
stage: str | None = None,
|
|
message: str | None = None,
|
|
unit: str | None = None,
|
|
indeterminate: bool = False,
|
|
elapsed_seconds: float | None = None,
|
|
time_to_first_token_seconds: float | None = None,
|
|
tokens_per_second: float | None = None,
|
|
pre_gpu: bool = False,
|
|
execution_provider: str | None = None,
|
|
execution_environment_id: str | None = None,
|
|
execution_location: str | None = None,
|
|
) -> None:
|
|
"""Forward remote numeric and stage progress into the local 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
|
|
if setup_only:
|
|
payload["setup_only"] = True
|
|
if cached_hit:
|
|
payload["cached_hit"] = True
|
|
if completed_ancestor_node_ids:
|
|
payload["completed_ancestor_node_ids"] = list(completed_ancestor_node_ids)
|
|
if stage:
|
|
payload["stage"] = stage
|
|
if message:
|
|
payload["message"] = message
|
|
if unit:
|
|
payload["unit"] = unit
|
|
if indeterminate:
|
|
payload["indeterminate"] = True
|
|
if elapsed_seconds is not None:
|
|
payload["elapsed_seconds"] = float(elapsed_seconds)
|
|
if time_to_first_token_seconds is not None:
|
|
payload["time_to_first_token_seconds"] = float(
|
|
time_to_first_token_seconds
|
|
)
|
|
if tokens_per_second is not None:
|
|
payload["tokens_per_second"] = float(tokens_per_second)
|
|
if pre_gpu:
|
|
payload["pre_gpu"] = True
|
|
if execution_provider:
|
|
payload["execution_provider"] = execution_provider
|
|
if execution_environment_id:
|
|
payload["execution_environment_id"] = execution_environment_id
|
|
if execution_location:
|
|
payload["execution_location"] = execution_location
|
|
_record_local_modal_ui_event("modal_progress", payload, client_id)
|
|
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 _remote_prompt_ancestor_node_ids(
|
|
payload: dict[str, Any], node_id: str | None
|
|
) -> list[str]:
|
|
"""Return subgraph dependency ancestors for one currently executing remote node."""
|
|
if node_id is None:
|
|
return []
|
|
prompt = payload.get("subgraph_prompt", {})
|
|
if not isinstance(prompt, dict) or node_id not in prompt:
|
|
return []
|
|
|
|
ancestors: set[str] = set()
|
|
pending = [node_id]
|
|
while pending:
|
|
current_node_id = str(pending.pop())
|
|
prompt_node = prompt.get(current_node_id)
|
|
if not isinstance(prompt_node, dict):
|
|
continue
|
|
inputs = prompt_node.get("inputs") or {}
|
|
if not isinstance(inputs, dict):
|
|
continue
|
|
for input_value in inputs.values():
|
|
for input_link in _iter_prompt_links(input_value):
|
|
upstream_node_id = str(input_link[0])
|
|
if upstream_node_id == node_id or upstream_node_id in ancestors:
|
|
continue
|
|
if upstream_node_id not in prompt:
|
|
continue
|
|
ancestors.add(upstream_node_id)
|
|
pending.append(upstream_node_id)
|
|
return sorted(ancestors)
|
|
|
|
|
|
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 _exception_indicates_interruption(exc: BaseException) -> bool:
|
|
"""Return whether an exception represents cancellation or interrupted execution."""
|
|
if isinstance(exc, asyncio.CancelledError):
|
|
return True
|
|
message = str(exc).lower()
|
|
return "interrupt" in message or "cancel" in message
|
|
|
|
|
|
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 _remote_interrupt_flag_value() -> dict[str, float]:
|
|
"""Return the shared Modal interrupt-store value for one cancellation request."""
|
|
return {"requested_at": time.time()}
|
|
|
|
|
|
def _write_remote_interrupt_flag(
|
|
interrupt_store: Any, prompt_id: str, component_id: str
|
|
) -> None:
|
|
"""Write one remote cancellation request with the blocking Modal Dict API."""
|
|
interrupt_store.put(
|
|
_remote_interrupt_flag_key(prompt_id, component_id),
|
|
_remote_interrupt_flag_value(),
|
|
)
|
|
|
|
|
|
async def _write_remote_interrupt_flag_async(
|
|
interrupt_store: Any,
|
|
prompt_id: str,
|
|
component_id: str,
|
|
) -> None:
|
|
"""Write one remote cancellation request without blocking the async caller."""
|
|
put_method = getattr(interrupt_store, "put", None)
|
|
put_async = getattr(put_method, "aio", None)
|
|
if callable(put_async):
|
|
result = put_async(
|
|
_remote_interrupt_flag_key(prompt_id, component_id),
|
|
_remote_interrupt_flag_value(),
|
|
)
|
|
if inspect.isawaitable(result):
|
|
await result
|
|
return
|
|
|
|
await asyncio.to_thread(
|
|
_write_remote_interrupt_flag, interrupt_store, prompt_id, component_id
|
|
)
|
|
|
|
|
|
def _request_remote_interrupt(payload: dict[str, Any]) -> bool:
|
|
"""Write one remote cancellation request into the shared Modal interrupt store."""
|
|
_abandon_local_modal_workflow_gate(payload, "local interrupt requested")
|
|
interrupt_store = _lookup_modal_interrupt_store()
|
|
if interrupt_store is None:
|
|
return False
|
|
|
|
prompt_id, component_id = _remote_interrupt_key(payload)
|
|
_write_remote_interrupt_flag(interrupt_store, prompt_id, component_id)
|
|
logger.info(
|
|
"Propagated local interrupt to Modal prompt=%s component=%s through shared control state.",
|
|
prompt_id,
|
|
component_id,
|
|
)
|
|
return True
|
|
|
|
|
|
async def _request_remote_interrupt_async(payload: dict[str, Any]) -> bool:
|
|
"""Write one remote cancellation request into the shared Modal interrupt store asynchronously."""
|
|
_abandon_local_modal_workflow_gate(payload, "local interrupt requested")
|
|
interrupt_store = await asyncio.to_thread(_lookup_modal_interrupt_store)
|
|
if interrupt_store is None:
|
|
return False
|
|
|
|
prompt_id, component_id = _remote_interrupt_key(payload)
|
|
await _write_remote_interrupt_flag_async(interrupt_store, prompt_id, component_id)
|
|
logger.info(
|
|
"Propagated local interrupt to Modal prompt=%s component=%s through shared control state.",
|
|
prompt_id,
|
|
component_id,
|
|
)
|
|
return True
|
|
|
|
|
|
def active_remote_modal_prompt_ids() -> set[str]:
|
|
"""Return prompt ids that currently have local proxies waiting on Modal work."""
|
|
with _ACTIVE_REMOTE_INVOCATIONS_LOCK:
|
|
return set(_ACTIVE_REMOTE_INVOCATIONS_BY_PROMPT)
|
|
|
|
|
|
@contextmanager
|
|
def _registered_active_remote_invocation(
|
|
payload: dict[str, Any],
|
|
cancellation_event: threading.Event | None,
|
|
interrupt_remote_call: Callable[[], Any] | None,
|
|
) -> Iterator[None]:
|
|
"""Register one active Modal call so targeted ComfyUI interrupts can find it."""
|
|
prompt_id, component_id = _remote_interrupt_key(payload)
|
|
invocation = _ActiveRemoteInvocation(
|
|
prompt_id=prompt_id,
|
|
component_id=component_id,
|
|
cancellation_event=cancellation_event,
|
|
interrupt_remote_call=interrupt_remote_call,
|
|
)
|
|
with _ACTIVE_REMOTE_INVOCATIONS_LOCK:
|
|
prompt_invocations = _ACTIVE_REMOTE_INVOCATIONS_BY_PROMPT.setdefault(
|
|
prompt_id, {}
|
|
)
|
|
prompt_invocations[component_id] = invocation
|
|
logger.info(
|
|
"Registered active Modal invocation prompt=%s component=%s for targeted cancellation.",
|
|
prompt_id,
|
|
component_id,
|
|
)
|
|
try:
|
|
yield
|
|
finally:
|
|
with _ACTIVE_REMOTE_INVOCATIONS_LOCK:
|
|
prompt_invocations = _ACTIVE_REMOTE_INVOCATIONS_BY_PROMPT.get(prompt_id)
|
|
if prompt_invocations is not None:
|
|
prompt_invocations.pop(component_id, None)
|
|
if not prompt_invocations:
|
|
_ACTIVE_REMOTE_INVOCATIONS_BY_PROMPT.pop(prompt_id, None)
|
|
logger.info(
|
|
"Unregistered active Modal invocation prompt=%s component=%s.",
|
|
prompt_id,
|
|
component_id,
|
|
)
|
|
|
|
|
|
def request_remote_modal_prompt_interrupt(prompt_id: str) -> bool:
|
|
"""Request cancellation for every active Modal invocation belonging to one prompt."""
|
|
normalized_prompt_id = str(prompt_id)
|
|
_abandon_local_modal_workflow_gate(
|
|
{"prompt_id": normalized_prompt_id},
|
|
"prompt-level interrupt requested",
|
|
)
|
|
with _ACTIVE_REMOTE_INVOCATIONS_LOCK:
|
|
invocations = list(
|
|
_ACTIVE_REMOTE_INVOCATIONS_BY_PROMPT.get(normalized_prompt_id, {}).values()
|
|
)
|
|
if not invocations:
|
|
return False
|
|
|
|
logger.info(
|
|
"Requesting remote Modal cancellation for prompt=%s across %d active component(s).",
|
|
normalized_prompt_id,
|
|
len(invocations),
|
|
)
|
|
for invocation in invocations:
|
|
if invocation.cancellation_event is not None:
|
|
invocation.cancellation_event.set()
|
|
_propagate_remote_interrupt_request(
|
|
{
|
|
"prompt_id": invocation.prompt_id,
|
|
"component_id": invocation.component_id,
|
|
},
|
|
invocation.interrupt_remote_call,
|
|
)
|
|
return True
|
|
|
|
|
|
async def request_remote_modal_prompt_interrupt_async(prompt_id: str) -> bool:
|
|
"""Request cancellation for every active Modal invocation belonging to one prompt asynchronously."""
|
|
normalized_prompt_id = str(prompt_id)
|
|
_abandon_local_modal_workflow_gate(
|
|
{"prompt_id": normalized_prompt_id},
|
|
"prompt-level interrupt requested",
|
|
)
|
|
with _ACTIVE_REMOTE_INVOCATIONS_LOCK:
|
|
invocations = list(
|
|
_ACTIVE_REMOTE_INVOCATIONS_BY_PROMPT.get(normalized_prompt_id, {}).values()
|
|
)
|
|
if not invocations:
|
|
return False
|
|
|
|
logger.info(
|
|
"Requesting async remote Modal cancellation for prompt=%s across %d active component(s).",
|
|
normalized_prompt_id,
|
|
len(invocations),
|
|
)
|
|
for invocation in invocations:
|
|
if invocation.cancellation_event is not None:
|
|
invocation.cancellation_event.set()
|
|
await _request_remote_interrupt_async(
|
|
{"prompt_id": invocation.prompt_id, "component_id": invocation.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()
|
|
_abandon_local_modal_workflow_gate(payload, "observed local interrupt")
|
|
return True
|
|
|
|
|
|
def _abandon_local_modal_workflow_gate(payload: dict[str, Any], reason: str) -> None:
|
|
"""Release the local prompt gate for a Modal prompt that ComfyUI has cancelled."""
|
|
prompt_id = payload.get("prompt_id")
|
|
if prompt_id is None:
|
|
return
|
|
|
|
try:
|
|
from ..modal_executor_node import abandon_modal_workflow_execution_prompt
|
|
except ImportError:
|
|
logger.debug(
|
|
"Unable to import Modal workflow gate helper while abandoning prompt."
|
|
)
|
|
return
|
|
|
|
abandon_modal_workflow_execution_prompt(str(prompt_id), reason)
|
|
|
|
|
|
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 _handle_modal_wait_cancellation(
|
|
payload: dict[str, Any],
|
|
cancellation_event: threading.Event,
|
|
*,
|
|
interrupt_sent: bool,
|
|
cancellation_started_at: float | None,
|
|
) -> tuple[bool, float | None]:
|
|
"""Propagate and bound local waiting after cancellation during any Modal wait phase."""
|
|
if not _sync_local_interrupt_to_cancellation_event(payload, cancellation_event):
|
|
return interrupt_sent, cancellation_started_at
|
|
|
|
if not interrupt_sent:
|
|
_request_remote_interrupt(payload)
|
|
return True, time.monotonic()
|
|
|
|
if cancellation_started_at is None:
|
|
return interrupt_sent, time.monotonic()
|
|
|
|
grace_seconds = max(0.0, get_settings().remote_cancel_grace_seconds)
|
|
if time.monotonic() - cancellation_started_at >= grace_seconds:
|
|
logger.info(
|
|
"Modal component=%s did not reach a cancellable remote call within %.3fs of local interrupt; releasing the local prompt while remote cancellation continues.",
|
|
payload.get("component_id"),
|
|
grace_seconds,
|
|
)
|
|
raise ModalRemoteInvocationError(
|
|
"Remote Modal call did not reach a cancellable remote phase after local interrupt propagation."
|
|
)
|
|
|
|
return interrupt_sent, cancellation_started_at
|
|
|
|
|
|
async def _handle_modal_wait_cancellation_async(
|
|
payload: dict[str, Any],
|
|
cancellation_event: threading.Event,
|
|
*,
|
|
interrupt_sent: bool,
|
|
cancellation_started_at: float | None,
|
|
) -> tuple[bool, float | None]:
|
|
"""Propagate and bound local waiting after cancellation during an async Modal wait phase."""
|
|
if not _sync_local_interrupt_to_cancellation_event(payload, cancellation_event):
|
|
return interrupt_sent, cancellation_started_at
|
|
|
|
if not interrupt_sent:
|
|
await _request_remote_interrupt_async(payload)
|
|
return True, time.monotonic()
|
|
|
|
if cancellation_started_at is None:
|
|
return interrupt_sent, time.monotonic()
|
|
|
|
grace_seconds = max(0.0, get_settings().remote_cancel_grace_seconds)
|
|
if time.monotonic() - cancellation_started_at >= grace_seconds:
|
|
logger.info(
|
|
"Modal component=%s did not reach a cancellable remote call within %.3fs of local interrupt; releasing the local prompt while remote cancellation continues.",
|
|
payload.get("component_id"),
|
|
grace_seconds,
|
|
)
|
|
raise ModalRemoteInvocationError(
|
|
"Remote Modal call did not reach a cancellable remote phase after local interrupt propagation."
|
|
)
|
|
|
|
return interrupt_sent, cancellation_started_at
|
|
|
|
|
|
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()
|
|
cancellation_started_at: float | None = None
|
|
|
|
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:
|
|
with _registered_active_remote_invocation(
|
|
payload, cancellation_event, interrupt_remote_call
|
|
):
|
|
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
|
|
):
|
|
if not interrupt_sent:
|
|
_propagate_remote_interrupt_request(
|
|
payload, interrupt_remote_call
|
|
)
|
|
interrupt_sent = True
|
|
cancellation_started_at = time.monotonic()
|
|
elif cancellation_started_at is not None:
|
|
grace_seconds = max(
|
|
0.0, get_settings().remote_cancel_grace_seconds
|
|
)
|
|
if (
|
|
time.monotonic() - cancellation_started_at
|
|
>= grace_seconds
|
|
):
|
|
logger.info(
|
|
"Modal component=%s did not return within %.3fs of local interrupt propagation; releasing the local prompt while remote cancellation continues.",
|
|
payload.get("component_id"),
|
|
grace_seconds,
|
|
)
|
|
raise ModalRemoteInvocationError(
|
|
"Remote Modal call did not finish after local interrupt propagation."
|
|
)
|
|
continue
|
|
|
|
if result_kind == "result":
|
|
return bytes(result_payload)
|
|
raise result_payload
|
|
finally:
|
|
request_thread.join(
|
|
timeout=0.1
|
|
if cancellation_event is not None and cancellation_event.is_set()
|
|
else 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", [])]
|
|
modal_gpu = (
|
|
str(payload["modal_gpu"]) if payload.get("modal_gpu") is not None else None
|
|
)
|
|
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_task_id: str | None = None
|
|
active_remote_log_task_id: str | None = None
|
|
should_close_stream = False
|
|
component_id = str(payload.get("component_id") or "payload")
|
|
invocation_id = str(payload.get("invocation_id") or "none")
|
|
stream_started_at = time.monotonic()
|
|
previous_event_at = stream_started_at
|
|
event_count = 0
|
|
progress_event_count = 0
|
|
speculative_prewarm_checked = False
|
|
logger.info(
|
|
"Starting local Modal stream consumption component=%s prompt_id=%s invocation_id=%s.",
|
|
component_id,
|
|
prompt_id or "none",
|
|
invocation_id,
|
|
)
|
|
|
|
try:
|
|
for stream_event in stream_events:
|
|
event_received_at = time.monotonic()
|
|
seconds_since_previous_event = event_received_at - previous_event_at
|
|
previous_event_at = event_received_at
|
|
event_count += 1
|
|
if not speculative_prewarm_checked:
|
|
speculative_prewarm_checked = True
|
|
_schedule_speculative_affinity_prewarm(
|
|
payload,
|
|
reason="current_remote_stream_started",
|
|
)
|
|
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_task_id is None:
|
|
active_remote_task_id = 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":
|
|
progress_event_count += 1
|
|
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
|
|
completed_ancestor_node_ids = _remote_prompt_ancestor_node_ids(
|
|
payload,
|
|
progress_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,
|
|
)
|
|
progress_kwargs = {
|
|
"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,
|
|
}
|
|
progress_kwargs.update(
|
|
_remote_execution_identity(payload, active_remote_task_id)
|
|
)
|
|
for string_field in ("stage", "message", "unit"):
|
|
if stream_event.get(string_field) is not None:
|
|
progress_kwargs[string_field] = str(
|
|
stream_event[string_field]
|
|
)
|
|
if stream_event.get("indeterminate") is not None:
|
|
progress_kwargs["indeterminate"] = bool(
|
|
stream_event["indeterminate"]
|
|
)
|
|
for numeric_field in (
|
|
"elapsed_seconds",
|
|
"time_to_first_token_seconds",
|
|
"tokens_per_second",
|
|
):
|
|
if stream_event.get(numeric_field) is not None:
|
|
progress_kwargs[numeric_field] = float(
|
|
stream_event[numeric_field]
|
|
)
|
|
if completed_ancestor_node_ids:
|
|
progress_kwargs[
|
|
"completed_ancestor_node_ids"
|
|
] = completed_ancestor_node_ids
|
|
_emit_local_modal_progress(**progress_kwargs)
|
|
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
|
|
if event_type == "node_cached":
|
|
progress_metadata = _progress_stream_event_metadata(stream_event)
|
|
filter_node_id = (
|
|
progress_metadata["filter_node_id"]
|
|
if progress_metadata is not None
|
|
else None
|
|
)
|
|
if (
|
|
suppress_status_stream
|
|
and not _should_forward_suppressed_stream_event(
|
|
payload, filter_node_id
|
|
)
|
|
):
|
|
logger.debug(
|
|
"Suppressing streamed Modal cached-node marker 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
|
|
)
|
|
logger.debug(
|
|
"Forwarding streamed Modal cached-node marker for component=%s node_id=%s real_node_id=%s.",
|
|
payload.get("component_id"),
|
|
reported_node_id,
|
|
real_node_id,
|
|
)
|
|
_emit_local_modal_progress(
|
|
prompt_id=prompt_id,
|
|
client_id=client_id,
|
|
node_id=str(reported_node_id),
|
|
value=0.0,
|
|
max_value=1.0,
|
|
display_node_id=display_node_id,
|
|
real_node_id=real_node_id,
|
|
cached_hit=True,
|
|
**_remote_execution_identity(
|
|
payload, active_remote_task_id
|
|
),
|
|
)
|
|
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,
|
|
modal_gpu=modal_gpu,
|
|
status_message=(
|
|
"Receiving remote outputs from "
|
|
f"{_remote_execution_destination(payload)}"
|
|
),
|
|
**_remote_execution_identity(payload, active_remote_task_id),
|
|
)
|
|
continue
|
|
_emit_local_modal_status(
|
|
prompt_id=prompt_id,
|
|
client_id=client_id,
|
|
phase=remote_phase,
|
|
node_ids=node_ids,
|
|
modal_gpu=modal_gpu,
|
|
active_node_id=(
|
|
str(stream_event["active_node_id"])
|
|
if stream_event.get("active_node_id") is not None
|
|
else None
|
|
),
|
|
completed_ancestor_node_ids=_remote_prompt_ancestor_node_ids(
|
|
payload,
|
|
str(stream_event["active_node_id"])
|
|
if stream_event.get("active_node_id") is not None
|
|
else None,
|
|
)
|
|
or 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
|
|
),
|
|
**_remote_execution_identity(payload, active_remote_task_id),
|
|
)
|
|
continue
|
|
if event_kind == "result":
|
|
candidate_outputs = stream_event.get("outputs")
|
|
candidate_bytes = (
|
|
len(candidate_outputs)
|
|
if isinstance(candidate_outputs, bytes | bytearray)
|
|
else -1
|
|
)
|
|
logger.info(
|
|
"Received streamed Modal result component=%s prompt_id=%s invocation_id=%s "
|
|
"result_bytes=%d stream_elapsed_seconds=%.3f seconds_since_previous_event=%.3f "
|
|
"event_count=%d progress_event_count=%d.",
|
|
component_id,
|
|
prompt_id or "none",
|
|
invocation_id,
|
|
candidate_bytes,
|
|
event_received_at - stream_started_at,
|
|
seconds_since_previous_event,
|
|
event_count,
|
|
progress_event_count,
|
|
)
|
|
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
|
|
should_close_stream = True
|
|
break
|
|
logger.debug(
|
|
"Ignoring unexpected streamed Modal event kind=%s for component=%s.",
|
|
event_kind,
|
|
payload.get("component_id"),
|
|
)
|
|
finally:
|
|
if should_close_stream:
|
|
close_started_at = time.monotonic()
|
|
logger.info(
|
|
"Starting local Modal result stream close component=%s prompt_id=%s invocation_id=%s.",
|
|
component_id,
|
|
prompt_id or "none",
|
|
invocation_id,
|
|
)
|
|
_close_remote_payload_stream(stream_events)
|
|
logger.info(
|
|
"Finished local Modal result stream close in %.3fs component=%s prompt_id=%s "
|
|
"invocation_id=%s.",
|
|
time.monotonic() - close_started_at,
|
|
component_id,
|
|
prompt_id or "none",
|
|
invocation_id,
|
|
)
|
|
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."
|
|
)
|
|
logger.info(
|
|
"Finished local Modal stream consumption in %.3fs component=%s prompt_id=%s invocation_id=%s "
|
|
"result_bytes=%d event_count=%d progress_event_count=%d.",
|
|
time.monotonic() - stream_started_at,
|
|
component_id,
|
|
prompt_id or "none",
|
|
invocation_id,
|
|
len(result_payload),
|
|
event_count,
|
|
progress_event_count,
|
|
)
|
|
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_inflight_calls
|
|
if settings.max_containers is not None:
|
|
configured_limit = min(configured_limit, settings.max_containers)
|
|
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."""
|
|
shared_fields = {
|
|
"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"),
|
|
}
|
|
snapshot_profile_key = payload.get("snapshot_profile_key")
|
|
if isinstance(snapshot_profile_key, str) and snapshot_profile_key.strip():
|
|
shared_fields["snapshot_profile_key"] = snapshot_profile_key.strip()
|
|
remote_session = payload.get("remote_session")
|
|
if remote_session is not None:
|
|
shared_fields["remote_session"] = copy.deepcopy(remote_session)
|
|
if bool(payload.get("clear_remote_session")):
|
|
shared_fields["clear_remote_session"] = True
|
|
return shared_fields
|
|
|
|
|
|
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,
|
|
)
|
|
static_phase_payload.pop("clear_remote_session", None)
|
|
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.pop("clear_remote_session", None)
|
|
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_for_scheduler(
|
|
[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)),
|
|
scheduler_is_list=bool(boundary_output.get("scheduler_is_list", False)),
|
|
)
|
|
)
|
|
return tuple(aggregated_outputs)
|
|
|
|
|
|
def _build_remote_session_cleanup_payload(
|
|
payload: dict[str, Any]
|
|
) -> dict[str, Any] | None:
|
|
"""Return a dedicated cleanup payload that clears one shared remote session once."""
|
|
remote_session = payload.get("remote_session")
|
|
if not bool(
|
|
payload.get("clear_remote_session")
|
|
) or not is_remote_session_handle_payload(remote_session):
|
|
return None
|
|
return {
|
|
"payload_kind": "subgraph",
|
|
"component_id": f"{payload.get('component_id', 'modal-subgraph')}::cleanup",
|
|
**_shared_subgraph_payload_fields(payload),
|
|
"component_node_ids": [],
|
|
"subgraph_prompt": {},
|
|
"boundary_inputs": [],
|
|
"boundary_outputs": [],
|
|
"execute_node_ids": [],
|
|
"remote_session": copy.deepcopy(remote_session),
|
|
"clear_remote_session": True,
|
|
"suppress_status_stream": True,
|
|
"terminate_container_on_error": False,
|
|
}
|
|
|
|
|
|
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.pop("clear_remote_session", None)
|
|
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 = None,
|
|
) -> 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,
|
|
setup_only=True,
|
|
)
|
|
|
|
|
|
def _implicit_batch_preserving_targets(
|
|
payload: dict[str, Any],
|
|
boundary_input: dict[str, Any],
|
|
) -> list[str]:
|
|
"""Return target sockets that must receive one complete tensor batch."""
|
|
prompt = payload.get("subgraph_prompt", {})
|
|
if not isinstance(prompt, dict):
|
|
return []
|
|
|
|
preserving_targets: list[str] = []
|
|
for target in boundary_input.get("targets", []):
|
|
node_id = str(target.get("node_id") or "")
|
|
input_name = str(target.get("input_name") or "")
|
|
prompt_node = prompt.get(node_id)
|
|
if not node_id or not input_name or not isinstance(prompt_node, dict):
|
|
continue
|
|
class_type = str(prompt_node.get("class_type") or "")
|
|
if (class_type, input_name) in _IMPLICIT_BATCH_PRESERVING_TARGETS:
|
|
preserving_targets.append(f"{node_id}.{input_name}")
|
|
return sorted(preserving_targets)
|
|
|
|
|
|
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]] = {}
|
|
|
|
def is_session_ref_list(value: Any) -> bool:
|
|
"""Return whether `value` is a non-empty list of remote session refs."""
|
|
return (
|
|
isinstance(value, list)
|
|
and len(value) > 0
|
|
and all(
|
|
is_remote_session_value_ref_payload(item)
|
|
or is_remote_session_bridge_ref_payload(item)
|
|
for item in value
|
|
)
|
|
)
|
|
|
|
def unwrap_singleton_session_ref_list(value: Any) -> Any:
|
|
"""Unwrap Comfy's output-list wrapper around mapped remote session refs."""
|
|
if (
|
|
isinstance(value, list)
|
|
and len(value) == 1
|
|
and is_session_ref_list(value[0])
|
|
):
|
|
return value[0]
|
|
return value
|
|
|
|
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
|
|
input_value = unwrap_singleton_session_ref_list(
|
|
hydrated_inputs[proxy_input_name]
|
|
)
|
|
io_type = _implicit_batch_boundary_effective_io_type(
|
|
payload=payload,
|
|
boundary_input=boundary_input,
|
|
input_value=input_value,
|
|
)
|
|
preserving_targets = _implicit_batch_preserving_targets(
|
|
payload,
|
|
boundary_input,
|
|
)
|
|
if preserving_targets:
|
|
logger.info(
|
|
"Skipping implicit batch split for boundary input %s io_type=%s "
|
|
"because target sockets consume the complete tensor batch: %s.",
|
|
proxy_input_name,
|
|
io_type,
|
|
preserving_targets,
|
|
)
|
|
continue
|
|
input_is_session_ref_list = is_session_ref_list(input_value)
|
|
if (
|
|
isinstance(input_value, list)
|
|
and not is_mapped_output_value(input_value)
|
|
and not input_is_session_ref_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 _is_latent_like_mapping(value: Any) -> bool:
|
|
"""Return whether one runtime value looks like a ComfyUI LATENT mapping."""
|
|
return isinstance(value, Mapping) and "samples" in value
|
|
|
|
|
|
def _list_is_latent_like_batch(value: Any) -> bool:
|
|
"""Return whether one runtime value is a list of LATENT-like mappings."""
|
|
return (
|
|
isinstance(value, list)
|
|
and len(value) > 0
|
|
and all(_is_latent_like_mapping(item) for item in value)
|
|
)
|
|
|
|
|
|
def _implicit_batch_boundary_target_input_types(
|
|
payload: dict[str, Any],
|
|
boundary_input: dict[str, Any],
|
|
) -> set[str]:
|
|
"""Return the declared input types of one boundary input's target sockets."""
|
|
prompt = payload.get("subgraph_prompt", {})
|
|
if not isinstance(prompt, dict):
|
|
return set()
|
|
|
|
try:
|
|
resolved_node_mapping = _load_nodes_module().NODE_CLASS_MAPPINGS
|
|
except ModuleNotFoundError:
|
|
logger.debug(
|
|
"Skipping target input type discovery for implicit batching because ComfyUI nodes are unavailable."
|
|
)
|
|
return set()
|
|
|
|
target_input_types: set[str] = set()
|
|
for target in boundary_input.get("targets", []):
|
|
node_id = str(target.get("node_id") or "")
|
|
input_name = str(target.get("input_name") or "")
|
|
if not node_id or not input_name:
|
|
continue
|
|
prompt_node = prompt.get(node_id)
|
|
if not isinstance(prompt_node, dict):
|
|
continue
|
|
node_class = resolved_node_mapping.get(str(prompt_node.get("class_type")))
|
|
if node_class is None:
|
|
continue
|
|
prompt_inputs = prompt_node.get("inputs") or {}
|
|
declared_type = _node_input_type_map(node_class, prompt_inputs).get(input_name)
|
|
if isinstance(declared_type, str) and declared_type:
|
|
target_input_types.add(declared_type)
|
|
return target_input_types
|
|
|
|
|
|
def _implicit_batch_boundary_effective_io_type(
|
|
*,
|
|
payload: dict[str, Any],
|
|
boundary_input: dict[str, Any],
|
|
input_value: Any,
|
|
) -> str:
|
|
"""Return the best effective io_type for implicit batching of one boundary input."""
|
|
declared_io_type = str(boundary_input.get("io_type", "*"))
|
|
if declared_io_type != "*":
|
|
return declared_io_type
|
|
|
|
if _list_is_latent_like_batch(input_value):
|
|
return "LATENT"
|
|
|
|
target_input_types = _implicit_batch_boundary_target_input_types(
|
|
payload, boundary_input
|
|
)
|
|
if len(target_input_types) == 1:
|
|
return next(iter(target_input_types))
|
|
return declared_io_type
|
|
|
|
|
|
def _implicit_batch_input_is_list_target_node_ids(
|
|
payload: dict[str, Any],
|
|
split_inputs: dict[str, list[Any]],
|
|
) -> list[str]:
|
|
"""Return split-boundary target nodes that must consume the full list in one execution."""
|
|
prompt = payload.get("subgraph_prompt", {})
|
|
if not isinstance(prompt, dict):
|
|
return []
|
|
|
|
try:
|
|
resolved_node_mapping = _load_nodes_module().NODE_CLASS_MAPPINGS
|
|
except ModuleNotFoundError:
|
|
logger.debug(
|
|
"Skipping INPUT_IS_LIST detection for implicit batching because ComfyUI nodes are unavailable."
|
|
)
|
|
return []
|
|
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 = str(target.get("node_id") or "")
|
|
if not target_node_id:
|
|
continue
|
|
prompt_node = prompt.get(target_node_id)
|
|
if prompt_node is None:
|
|
continue
|
|
class_type = str(prompt_node.get("class_type"))
|
|
node_class = resolved_node_mapping.get(class_type)
|
|
if node_class is None:
|
|
continue
|
|
if bool(getattr(node_class, "INPUT_IS_LIST", False)):
|
|
target_node_ids.add(target_node_id)
|
|
return sorted(target_node_ids)
|
|
|
|
|
|
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
|
|
|
|
|
|
def _log_detached_mapped_lane_result(
|
|
task: asyncio.Task[None],
|
|
*,
|
|
component_id: str,
|
|
lane_index: int,
|
|
) -> None:
|
|
"""Consume and log any late failure from one detached mapped worker lane."""
|
|
try:
|
|
task.result()
|
|
except asyncio.CancelledError:
|
|
return
|
|
except BaseException as exc:
|
|
logger.warning(
|
|
"Detached mapped worker lane=%d for component=%s finished with an unawaited failure: %s",
|
|
lane_index,
|
|
component_id,
|
|
exc,
|
|
)
|
|
|
|
|
|
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
|
|
input_is_list_target_node_ids = _implicit_batch_input_is_list_target_node_ids(
|
|
payload,
|
|
split_inputs,
|
|
)
|
|
if input_is_list_target_node_ids:
|
|
logger.info(
|
|
"Executing implicitly batched Modal component=%s as one ordinary subgraph because split boundary inputs target INPUT_IS_LIST nodes=%s.",
|
|
payload.get("component_id"),
|
|
input_is_list_target_node_ids,
|
|
)
|
|
return await invoke_remote_engine_async(
|
|
payload,
|
|
kwargs_payload,
|
|
allow_implicit_mapping=False,
|
|
)
|
|
|
|
parallelism, refined_prompt_warmup_target = boost_mapped_component_warmup(
|
|
payload,
|
|
total_items=total_items,
|
|
reason="implicit_mapped_component_exact_parallelism",
|
|
)
|
|
prompt_id = (
|
|
str(payload.get("prompt_id")) if payload.get("prompt_id") is not None else None
|
|
)
|
|
if prompt_id is not None:
|
|
await _await_prompt_warmup_slots(
|
|
prompt_id,
|
|
list(range(refined_prompt_warmup_target)),
|
|
_prompt_warmup_head_start_seconds(),
|
|
)
|
|
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,
|
|
)
|
|
cleanup_payload = _build_remote_session_cleanup_payload(hybrid_payload)
|
|
execution_mode = os.getenv("COMFY_MODAL_EXECUTION_MODE", "local")
|
|
use_seeded_remote_lanes = (
|
|
execution_mode != "local"
|
|
and modal is not None
|
|
and _payload_remote_session_handle(hybrid_payload) is not None
|
|
and _mapped_phase_definition(hybrid_payload, "static_phase") is not None
|
|
)
|
|
|
|
static_outputs: tuple[Any, ...] = ()
|
|
lane_remote_engines: list[Any] = []
|
|
seeded_lane_indices: set[int] = set()
|
|
skip_cleanup_after_interrupt = False
|
|
try:
|
|
if use_seeded_remote_lanes:
|
|
lane_remote_engines = [
|
|
_lookup_deployed_remote_engine(
|
|
hybrid_payload,
|
|
affinity_key_override=_mapped_lane_affinity_key(
|
|
hybrid_payload, lane_index
|
|
),
|
|
)
|
|
for lane_index in range(parallelism)
|
|
]
|
|
logger.info(
|
|
"Seeding %d mapped worker lane(s) for component=%s and allowing ready lanes to start item dispatch independently.",
|
|
parallelism,
|
|
payload.get("component_id"),
|
|
)
|
|
|
|
async def seed_lane(lane_index: int) -> None:
|
|
"""Run the static bridge-producing phase once on one mapped worker lane."""
|
|
lane_seed_payload = _build_phase_subgraph_payload(
|
|
hybrid_payload,
|
|
"static_phase",
|
|
f"{payload.get('component_id', 'modal-subgraph')}::seed:{lane_index}",
|
|
suppress_status_stream=True,
|
|
)
|
|
lane_seed_payload.pop("clear_remote_session", None)
|
|
await _invoke_bound_remote_engine_async(
|
|
lane_remote_engines[lane_index],
|
|
lane_seed_payload,
|
|
serialize_node_inputs(broadcast_inputs),
|
|
)
|
|
seeded_lane_indices.add(lane_index)
|
|
|
|
elif 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)
|
|
else:
|
|
lane_remote_engines = []
|
|
|
|
per_item_outputs: list[tuple[Any, ...] | None] = [None] * total_items
|
|
completed_items = 0
|
|
all_items_completed = asyncio.Event()
|
|
stop_dispatch_requested = asyncio.Event()
|
|
worker_failure: asyncio.Future[
|
|
BaseException
|
|
] = asyncio.get_running_loop().create_future()
|
|
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)
|
|
|
|
def request_interrupt_stop() -> None:
|
|
"""Stop queued mapped item dispatch and suppress cleanup after interruption."""
|
|
nonlocal skip_cleanup_after_interrupt
|
|
skip_cleanup_after_interrupt = True
|
|
stop_dispatch_requested.set()
|
|
|
|
def raise_if_local_interrupted() -> None:
|
|
"""Raise the native ComfyUI interrupt after marking mapped dispatch as stopped."""
|
|
if _local_processing_interrupted():
|
|
request_interrupt_stop()
|
|
_raise_local_interrupt()
|
|
|
|
async def run_worker(lane_index: int) -> None:
|
|
"""Execute queued implicit mapped items through one stable local worker lane."""
|
|
nonlocal completed_items
|
|
try:
|
|
raise_if_local_interrupted()
|
|
if use_seeded_remote_lanes:
|
|
_emit_local_mapped_lane_progress_start(payload, lane_index)
|
|
await seed_lane(lane_index)
|
|
while True:
|
|
if stop_dispatch_requested.is_set():
|
|
return
|
|
item_index = await item_queue.get()
|
|
if item_index is None:
|
|
return
|
|
if stop_dispatch_requested.is_set():
|
|
return
|
|
raise_if_local_interrupted()
|
|
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]
|
|
if use_seeded_remote_lanes:
|
|
item_response = await _invoke_bound_remote_engine_async(
|
|
lane_remote_engines[lane_index],
|
|
item_payload,
|
|
serialize_node_inputs(item_inputs),
|
|
)
|
|
else:
|
|
item_response = await invoke_remote_engine_async(
|
|
item_payload,
|
|
serialize_node_inputs(item_inputs),
|
|
)
|
|
raise_if_local_interrupted()
|
|
per_item_outputs[item_index] = deserialize_node_outputs(
|
|
item_response
|
|
)
|
|
completed_items += 1
|
|
_emit_local_mapped_progress(
|
|
payload, completed_items, total_items
|
|
)
|
|
if completed_items >= total_items:
|
|
all_items_completed.set()
|
|
finally:
|
|
_clear_local_mapped_lane_progress(
|
|
payload, lane_index, item_index
|
|
)
|
|
except BaseException as exc:
|
|
if all_items_completed.is_set():
|
|
logger.info(
|
|
"Ignoring late mapped worker failure after component=%s already completed all items lane=%d: %s",
|
|
payload.get("component_id"),
|
|
lane_index,
|
|
exc,
|
|
)
|
|
return
|
|
stop_dispatch_requested.set()
|
|
if _exception_indicates_interruption(exc):
|
|
request_interrupt_stop()
|
|
if not worker_failure.done():
|
|
worker_failure.set_result(exc)
|
|
raise
|
|
|
|
tasks = [
|
|
asyncio.create_task(run_worker(lane_index))
|
|
for lane_index in range(parallelism)
|
|
]
|
|
for lane_index, task in enumerate(tasks):
|
|
task.add_done_callback(
|
|
lambda completed_task, lane_index=lane_index: _log_detached_mapped_lane_result(
|
|
completed_task,
|
|
component_id=str(payload.get("component_id", "modal-subgraph")),
|
|
lane_index=lane_index,
|
|
)
|
|
)
|
|
completion_task = asyncio.create_task(all_items_completed.wait())
|
|
try:
|
|
while not all_items_completed.is_set():
|
|
wait_set: set[asyncio.Task[Any] | asyncio.Future[BaseException]] = {
|
|
completion_task,
|
|
worker_failure,
|
|
*tasks,
|
|
}
|
|
done, _ = await asyncio.wait(
|
|
wait_set, return_when=asyncio.FIRST_COMPLETED
|
|
)
|
|
if completion_task in done or all_items_completed.is_set():
|
|
break
|
|
if worker_failure in done:
|
|
stop_dispatch_requested.set()
|
|
raise worker_failure.result()
|
|
for task in done:
|
|
if task in tasks:
|
|
task_exc = task.exception()
|
|
if task_exc is not None:
|
|
stop_dispatch_requested.set()
|
|
raise task_exc
|
|
if not all(item_outputs is not None for item_outputs in per_item_outputs):
|
|
await asyncio.gather(*tasks)
|
|
except BaseException as exc:
|
|
stop_dispatch_requested.set()
|
|
if _exception_indicates_interruption(exc):
|
|
skip_cleanup_after_interrupt = True
|
|
for task in tasks:
|
|
task.cancel()
|
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
raise
|
|
finally:
|
|
completion_task.cancel()
|
|
await asyncio.gather(completion_task, return_exceptions=True)
|
|
|
|
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,
|
|
)
|
|
)
|
|
finally:
|
|
if cleanup_payload is not None and not skip_cleanup_after_interrupt:
|
|
if use_seeded_remote_lanes and lane_remote_engines:
|
|
await asyncio.gather(
|
|
*(
|
|
_invoke_bound_remote_engine_async(
|
|
lane_remote_engines[lane_index],
|
|
{
|
|
**cleanup_payload,
|
|
"component_id": (
|
|
f"{payload.get('component_id', 'modal-subgraph')}::cleanup:{lane_index}"
|
|
),
|
|
},
|
|
serialize_node_inputs({}),
|
|
)
|
|
for lane_index in sorted(seeded_lane_indices)
|
|
)
|
|
)
|
|
else:
|
|
await invoke_remote_engine_async(
|
|
cleanup_payload,
|
|
serialize_node_inputs({}),
|
|
)
|
|
elif cleanup_payload is not None:
|
|
logger.info(
|
|
"Skipping remote-session cleanup for implicitly mapped Modal component=%s because execution was interrupted.",
|
|
payload.get("component_id"),
|
|
)
|
|
|
|
|
|
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],
|
|
*,
|
|
affinity_key_override: str | None = None,
|
|
protocol_probe: bool = False,
|
|
) -> Any:
|
|
"""Look up the deployed runtime, optionally omitting new parameters for probing."""
|
|
if modal is None:
|
|
raise ModalRemoteInvocationError("Modal SDK is unavailable.")
|
|
|
|
settings = _settings_for_payload(payload)
|
|
deployment_app_name = modal_deployment_app_name(settings)
|
|
snapshot_profile_key = _prepare_snapshot_profile_fields(payload)
|
|
gpu_snapshot_enabled = (
|
|
False
|
|
if protocol_probe
|
|
else _select_gpu_snapshot_for_profile(payload, snapshot_profile_key)
|
|
)
|
|
worker_affinity_key = affinity_key_override or _remote_worker_affinity_key(payload)
|
|
logger.info(
|
|
"Attempting deployed Modal invocation for app=%s class=%s component=%s worker_affinity=%s snapshot_profile=%s gpu_snapshot_enabled=%s.",
|
|
deployment_app_name,
|
|
"RemoteEngine",
|
|
payload.get("component_id"),
|
|
worker_affinity_key,
|
|
snapshot_profile_key or None,
|
|
gpu_snapshot_enabled,
|
|
)
|
|
remote_cls = modal.Cls.from_name(deployment_app_name, "RemoteEngine")
|
|
if bool(payload.get("remote_local_gap_pool")) and hasattr(remote_cls, "with_options"):
|
|
local_gap_scaledown_seconds = max(
|
|
int(settings.scaledown_window_seconds),
|
|
int(settings.local_gap_keepalive_seconds),
|
|
)
|
|
remote_cls = remote_cls.with_options(
|
|
scaledown_window=local_gap_scaledown_seconds,
|
|
)
|
|
logger.info(
|
|
"Using local-gap Modal pool component=%s worker_affinity=%s scaledown_window=%ds.",
|
|
payload.get("component_id"),
|
|
worker_affinity_key,
|
|
local_gap_scaledown_seconds,
|
|
)
|
|
remote_engine_kwargs: dict[str, Any] = {
|
|
"gpu_snapshot_enabled": gpu_snapshot_enabled,
|
|
}
|
|
if snapshot_profile_key:
|
|
remote_engine_kwargs["snapshot_profile_key"] = snapshot_profile_key
|
|
if not protocol_probe:
|
|
remote_engine_kwargs["worker_affinity_key"] = worker_affinity_key
|
|
else:
|
|
logger.info(
|
|
"Probing deployed RemoteEngine protocol without affinity parameters component=%s.",
|
|
payload.get("component_id"),
|
|
)
|
|
remote_engine = remote_cls(**remote_engine_kwargs)
|
|
with _MODAL_AUTO_DEPLOY_LOCK:
|
|
runtime_is_known_current = (
|
|
_modal_runtime_cache_key(payload) in _MODAL_REMOTE_APP_VERSION_OK
|
|
)
|
|
if runtime_is_known_current:
|
|
_ensure_llm_profiles_staged(payload, deployment_app_name)
|
|
return remote_engine
|
|
|
|
|
|
def _emit_local_llm_staging_progress(
|
|
payload: Mapping[str, Any],
|
|
stage_event: Mapping[str, Any],
|
|
) -> None:
|
|
"""Render one CPU ModelStager update on the target component's node bar."""
|
|
prompt_id = (
|
|
str(payload["prompt_id"]) if payload.get("prompt_id") is not None else None
|
|
)
|
|
extra_data = payload.get("extra_data") or {}
|
|
client_id = (
|
|
str(extra_data["client_id"])
|
|
if isinstance(extra_data, Mapping) and extra_data.get("client_id") is not None
|
|
else None
|
|
)
|
|
component_id = str(payload.get("component_id") or "")
|
|
if not component_id:
|
|
return
|
|
maximum = stage_event.get("max")
|
|
_emit_local_modal_progress(
|
|
prompt_id=prompt_id,
|
|
client_id=client_id,
|
|
node_id=component_id,
|
|
value=float(stage_event.get("value") or 0.0),
|
|
max_value=float(maximum) if maximum is not None else 1.0,
|
|
stage=str(stage_event.get("stage") or "staging"),
|
|
message=str(stage_event.get("message") or "Staging LLM snapshot"),
|
|
unit=(
|
|
str(stage_event["unit"])
|
|
if stage_event.get("unit") is not None
|
|
else None
|
|
),
|
|
indeterminate=bool(stage_event.get("indeterminate", False)),
|
|
pre_gpu=True,
|
|
)
|
|
|
|
|
|
def _ensure_llm_profiles_staged(
|
|
payload: dict[str, Any],
|
|
deployment_app_name: str,
|
|
) -> None:
|
|
"""Stage every LLM profile in a payload on a CPU worker before GPU dispatch."""
|
|
if modal is None:
|
|
raise ModalRemoteInvocationError("Modal SDK is unavailable.")
|
|
model_references = llm_model_references_from_payload(payload)
|
|
if not model_references:
|
|
return
|
|
with _STAGED_LLM_PROFILES_LOCK:
|
|
if not _STAGED_LLM_PROFILES:
|
|
_STAGED_LLM_PROFILE_RESULTS.clear()
|
|
missing_model_references = [
|
|
reference
|
|
for reference in model_references
|
|
if (deployment_app_name, reference) not in _STAGED_LLM_PROFILE_RESULTS
|
|
]
|
|
if missing_model_references:
|
|
_emit_local_remote_startup_status(
|
|
payload,
|
|
phase="llm_staging",
|
|
status_message=(
|
|
"Inspecting and staging LLM on CPU; no GPU is allocated yet"
|
|
),
|
|
)
|
|
logger.info(
|
|
"Dispatching CPU model resolution/staging app=%s models=%s "
|
|
"before GPU component=%s.",
|
|
deployment_app_name,
|
|
missing_model_references,
|
|
payload.get("component_id"),
|
|
)
|
|
stager_cls = modal.Cls.from_name(deployment_app_name, "ModelStager")
|
|
stager = stager_cls()
|
|
stage_results: list[dict[str, Any]] = []
|
|
stage_stream = getattr(stager, "stage_profiles_stream", None)
|
|
remote_generator = getattr(stage_stream, "remote_gen", None)
|
|
if callable(remote_generator):
|
|
for stage_event in remote_generator(missing_model_references):
|
|
if not isinstance(stage_event, Mapping):
|
|
continue
|
|
if stage_event.get("kind") == "result":
|
|
candidate_results = stage_event.get("results")
|
|
if isinstance(candidate_results, list):
|
|
stage_results = candidate_results
|
|
continue
|
|
if stage_event.get("kind") != "progress":
|
|
continue
|
|
_emit_local_llm_staging_progress(payload, stage_event)
|
|
else:
|
|
stage_results = stager.stage_profiles.remote(
|
|
missing_model_references
|
|
)
|
|
confirmed_references: set[str] = set()
|
|
for stage_result in stage_results:
|
|
if not isinstance(stage_result, Mapping):
|
|
continue
|
|
requested_reference = str(
|
|
stage_result.get("requested_reference")
|
|
or stage_result.get("profile_id")
|
|
or ""
|
|
)
|
|
profile_id = str(stage_result.get("profile_id") or "")
|
|
revision = str(stage_result.get("revision") or "")
|
|
if not revision and requested_reference == profile_id:
|
|
try:
|
|
revision = get_llm_profile(profile_id).revision
|
|
except ValueError:
|
|
pass
|
|
if not requested_reference or not profile_id or not revision:
|
|
continue
|
|
normalized_result = dict(stage_result)
|
|
normalized_result["requested_reference"] = requested_reference
|
|
normalized_result["profile_id"] = profile_id
|
|
normalized_result["revision"] = revision
|
|
_STAGED_LLM_PROFILE_RESULTS[
|
|
(deployment_app_name, requested_reference)
|
|
] = normalized_result
|
|
_STAGED_LLM_PROFILE_RESULTS[
|
|
(deployment_app_name, profile_id)
|
|
] = normalized_result
|
|
_STAGED_LLM_PROFILES.add((deployment_app_name, profile_id, revision))
|
|
confirmed_references.add(requested_reference)
|
|
missing_results = set(missing_model_references) - confirmed_references
|
|
if missing_results:
|
|
raise ModalRemoteInvocationError(
|
|
f"Modal ModelStager did not confirm models {sorted(missing_results)}."
|
|
)
|
|
downloaded_gib = sum(
|
|
float(result.get("artifact_bytes") or 0) / 1024**3
|
|
for result in stage_results
|
|
if isinstance(result, Mapping) and result.get("downloaded")
|
|
)
|
|
_emit_local_remote_startup_status(
|
|
payload,
|
|
phase="llm_staged",
|
|
status_message=(
|
|
f"LLM staging complete ({downloaded_gib:.1f} GiB downloaded); "
|
|
"starting GPU worker"
|
|
),
|
|
)
|
|
resolved_results = {
|
|
reference: _STAGED_LLM_PROFILE_RESULTS[(deployment_app_name, reference)]
|
|
for reference in model_references
|
|
}
|
|
profile_ids_by_reference = {
|
|
reference: str(result["profile_id"])
|
|
for reference, result in resolved_results.items()
|
|
}
|
|
rewrite_llm_model_references(payload, profile_ids_by_reference)
|
|
revisions = ",".join(
|
|
f"{result['profile_id']}:{result['revision']}"
|
|
for result in sorted(
|
|
resolved_results.values(),
|
|
key=lambda value: str(value["profile_id"]),
|
|
)
|
|
)
|
|
payload["requires_volume_reload"] = True
|
|
payload["volume_reload_marker"] = hashlib.sha256(
|
|
f"llm-profiles:{revisions}".encode("utf-8")
|
|
).hexdigest()
|
|
logger.info(
|
|
"Modal LLM models are resolved and staged for component=%s profiles=%s "
|
|
"reload_marker=%s.",
|
|
payload.get("component_id"),
|
|
sorted(profile_ids_by_reference.values()),
|
|
payload["volume_reload_marker"],
|
|
)
|
|
|
|
|
|
def _rewrite_staged_llm_kwargs_payload(
|
|
kwargs_payload: bytes,
|
|
deployment_app_name: str,
|
|
) -> bytes:
|
|
"""Replace a direct node input model reference with its staged profile ID."""
|
|
hydrated_inputs = deserialize_node_inputs(kwargs_payload)
|
|
if not isinstance(hydrated_inputs, Mapping):
|
|
return kwargs_payload
|
|
model_reference = hydrated_inputs.get("model_profile")
|
|
if not isinstance(model_reference, str) or not model_reference.strip():
|
|
return kwargs_payload
|
|
normalized_reference = model_reference.strip()
|
|
with _STAGED_LLM_PROFILES_LOCK:
|
|
stage_result = _STAGED_LLM_PROFILE_RESULTS.get(
|
|
(deployment_app_name, normalized_reference)
|
|
)
|
|
if stage_result is None:
|
|
return kwargs_payload
|
|
profile_id = str(stage_result.get("profile_id") or "").strip()
|
|
if not profile_id or profile_id == normalized_reference:
|
|
return kwargs_payload
|
|
rewritten_inputs = dict(hydrated_inputs)
|
|
rewritten_inputs["model_profile"] = profile_id
|
|
logger.info(
|
|
"Rewrote direct Modal LLM input model=%s to generated profile=%s.",
|
|
normalized_reference,
|
|
profile_id,
|
|
)
|
|
return serialize_node_inputs(rewritten_inputs)
|
|
|
|
|
|
async def _invoke_bound_remote_engine_async(
|
|
remote_engine: Any,
|
|
payload: dict[str, Any],
|
|
kwargs_payload: bytes,
|
|
) -> bytes:
|
|
"""Invoke one pre-bound remote engine handle asynchronously with local interrupt mirroring."""
|
|
logger.info(
|
|
"Dispatching async Modal remote invocation via bound engine 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_remote_engine_payload_with_recovery,
|
|
remote_engine,
|
|
dict(payload),
|
|
kwargs_payload,
|
|
cancellation_event,
|
|
)
|
|
wrapped_future = asyncio.wrap_future(future)
|
|
interrupt_sent = False
|
|
cancellation_started_at: float | None = None
|
|
try:
|
|
while True:
|
|
try:
|
|
response = await asyncio.wait_for(
|
|
asyncio.shield(wrapped_future), timeout=0.1
|
|
)
|
|
break
|
|
except asyncio.TimeoutError:
|
|
(
|
|
interrupt_sent,
|
|
cancellation_started_at,
|
|
) = await _handle_modal_wait_cancellation_async(
|
|
payload,
|
|
cancellation_event,
|
|
interrupt_sent=interrupt_sent,
|
|
cancellation_started_at=cancellation_started_at,
|
|
)
|
|
continue
|
|
except asyncio.CancelledError:
|
|
cancellation_event.set()
|
|
prompt_id = (
|
|
str(payload.get("prompt_id"))
|
|
if payload.get("prompt_id") is not None
|
|
else None
|
|
)
|
|
if prompt_id is not None:
|
|
await request_remote_modal_prompt_interrupt_async(prompt_id)
|
|
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 via bound engine failed for component=%s.",
|
|
payload.get("component_id"),
|
|
)
|
|
raise
|
|
if cancellation_event.is_set() or _local_processing_interrupted():
|
|
logger.info(
|
|
"Async bound-engine invocation for component=%s finished after interruption; raising local interrupt.",
|
|
payload.get("component_id"),
|
|
)
|
|
_raise_local_interrupt()
|
|
logger.info(
|
|
"Async Modal remote invocation via bound engine completed for component=%s.",
|
|
payload.get("component_id"),
|
|
)
|
|
return response
|
|
|
|
|
|
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 _lookup_modal_session_bridge_store() -> Any:
|
|
"""Return the shared Modal Dict that contains durable bridge records."""
|
|
if modal is None or not hasattr(modal, "Dict"):
|
|
raise RuntimeError(
|
|
"Modal SDK is required to materialize a remote bridge locally."
|
|
)
|
|
settings = get_settings()
|
|
cache_key = (settings.session_bridge_dict_name, _modal_environment_name())
|
|
with _MODAL_SESSION_BRIDGE_DICTS_LOCK:
|
|
cached_store = _MODAL_SESSION_BRIDGE_DICTS.get(cache_key)
|
|
if cached_store is not None:
|
|
return cached_store
|
|
bridge_store = modal.Dict.from_name(
|
|
settings.session_bridge_dict_name,
|
|
environment_name=cache_key[1],
|
|
create_if_missing=True,
|
|
)
|
|
with _MODAL_SESSION_BRIDGE_DICTS_LOCK:
|
|
_MODAL_SESSION_BRIDGE_DICTS[cache_key] = bridge_store
|
|
return bridge_store
|
|
|
|
|
|
def _lookup_modal_durable_volume() -> Any:
|
|
"""Return the shared Modal Volume that contains durable bridge objects."""
|
|
if modal is None or not hasattr(modal, "Volume"):
|
|
raise RuntimeError(
|
|
"Modal SDK is required to download a remote bridge object locally."
|
|
)
|
|
settings = get_settings()
|
|
cache_key = (settings.volume_name, _modal_environment_name())
|
|
with _MODAL_DURABLE_VOLUMES_LOCK:
|
|
cached_volume = _MODAL_DURABLE_VOLUMES.get(cache_key)
|
|
if cached_volume is not None:
|
|
return cached_volume
|
|
volume = modal.Volume.from_name(
|
|
settings.volume_name,
|
|
environment_name=cache_key[1],
|
|
create_if_missing=True,
|
|
)
|
|
with _MODAL_DURABLE_VOLUMES_LOCK:
|
|
_MODAL_DURABLE_VOLUMES[cache_key] = volume
|
|
return volume
|
|
|
|
|
|
def _modal_deploy_cache_key(
|
|
payload: Mapping[str, Any] | None = None,
|
|
) -> tuple[str, str | None]:
|
|
"""Return the cache key for auto-deployed Modal apps."""
|
|
settings = _settings_for_payload(payload) if payload is not None else get_settings()
|
|
return (modal_deployment_app_name(settings), _modal_environment_name())
|
|
|
|
|
|
@lru_cache(maxsize=8)
|
|
def _remote_runtime_identity_for_settings(
|
|
settings: ModalSyncSettings,
|
|
) -> RemoteRuntimeIdentity:
|
|
"""Build and cache the local runtime identity for one resolved settings object."""
|
|
return build_remote_runtime_identity(
|
|
repo_root=Path(__file__).resolve().parents[1],
|
|
comfyui_root=settings.comfyui_root,
|
|
custom_nodes_dir=settings.custom_nodes_dir,
|
|
settings=settings,
|
|
)
|
|
|
|
|
|
def _expected_remote_runtime_fingerprint(
|
|
payload: Mapping[str, Any] | None = None
|
|
) -> str:
|
|
"""Return the exact deployment fingerprint required by this local client."""
|
|
settings = _settings_for_payload(payload) if payload is not None else get_settings()
|
|
return _remote_runtime_identity_for_settings(settings).fingerprint
|
|
|
|
|
|
def _modal_runtime_cache_key(
|
|
payload: Mapping[str, Any] | None = None,
|
|
) -> tuple[str, str | None, str]:
|
|
"""Return the version cache key for the current app, environment, and runtime."""
|
|
settings = _settings_for_payload(payload) if payload is not None else get_settings()
|
|
return (
|
|
modal_deployment_app_name(settings),
|
|
_modal_environment_name(),
|
|
_expected_remote_runtime_fingerprint(payload),
|
|
)
|
|
|
|
|
|
def _call_modal_method(method: Any, *args: Any, **kwargs: Any) -> Any:
|
|
"""Invoke a Modal method handle or an in-process test double."""
|
|
remote_method = getattr(method, "remote", None)
|
|
if callable(remote_method):
|
|
return remote_method(*args, **kwargs)
|
|
return method(*args, **kwargs)
|
|
|
|
|
|
def _remote_engine_runtime_version(remote_engine: Any) -> dict[str, Any] | None:
|
|
"""Return runtime version metadata from a deployed engine when available."""
|
|
version_method = getattr(remote_engine, "runtime_version", None)
|
|
if version_method is None:
|
|
return None
|
|
version_payload = _call_modal_method(version_method)
|
|
if not isinstance(version_payload, dict):
|
|
return None
|
|
return version_payload
|
|
|
|
|
|
def _remote_runtime_version_from_cpu_stager(
|
|
payload: Mapping[str, Any],
|
|
) -> dict[str, Any] | None:
|
|
"""Read deployment identity from the CPU stager when that method is available."""
|
|
if modal is None:
|
|
return None
|
|
settings = _settings_for_payload(payload)
|
|
deployment_app_name = modal_deployment_app_name(settings)
|
|
stager_cls = modal.Cls.from_name(deployment_app_name, "ModelStager")
|
|
stager = stager_cls()
|
|
version_method = getattr(stager, "runtime_version", None)
|
|
if version_method is None:
|
|
return None
|
|
try:
|
|
version_payload = _call_modal_method(version_method)
|
|
except AttributeError:
|
|
return None
|
|
except _modal_lookup_error_types() as exc:
|
|
if _is_missing_modal_deployment_error(exc):
|
|
raise
|
|
logger.info(
|
|
"CPU ModelStager runtime identity is unavailable for app=%s; "
|
|
"falling back to the legacy GPU protocol probe: %s",
|
|
deployment_app_name,
|
|
exc,
|
|
)
|
|
return None
|
|
if not isinstance(version_payload, dict):
|
|
return None
|
|
return version_payload
|
|
|
|
|
|
def _runtime_fingerprint_from_payload(
|
|
version_payload: dict[str, Any] | None
|
|
) -> str | None:
|
|
"""Return a normalized runtime fingerprint from remote version metadata."""
|
|
if version_payload is None:
|
|
return None
|
|
runtime_fingerprint = version_payload.get("runtime_fingerprint")
|
|
if not isinstance(runtime_fingerprint, str):
|
|
return None
|
|
normalized = runtime_fingerprint.strip()
|
|
return normalized or None
|
|
|
|
|
|
def _is_runtime_version_payload_current(
|
|
version_payload: dict[str, Any] | None,
|
|
payload: Mapping[str, Any] | None = None,
|
|
) -> bool:
|
|
"""Return whether remote version metadata exactly matches this local runtime."""
|
|
if version_payload is None:
|
|
return False
|
|
protocol_version = version_payload.get("protocol_version")
|
|
if isinstance(protocol_version, bool) or not isinstance(protocol_version, int):
|
|
return False
|
|
return (
|
|
protocol_version == _REMOTE_APP_PROTOCOL_VERSION
|
|
and _runtime_fingerprint_from_payload(version_payload)
|
|
== _expected_remote_runtime_fingerprint(payload)
|
|
)
|
|
|
|
|
|
def _is_remote_engine_runtime_current(
|
|
remote_engine: Any,
|
|
payload: Mapping[str, Any] | None = None,
|
|
) -> bool:
|
|
"""Return whether a deployed engine exactly matches this local runtime."""
|
|
return _is_runtime_version_payload_current(
|
|
_remote_engine_runtime_version(remote_engine),
|
|
payload,
|
|
)
|
|
|
|
|
|
def _stop_modal_app_via_sdk(app_name: str) -> bool:
|
|
"""Try to stop a Modal app through the SDK if this SDK version exposes app stopping."""
|
|
if modal is None:
|
|
return False
|
|
try:
|
|
experimental_namespace = importlib.import_module("modal.experimental")
|
|
except ModuleNotFoundError as exc:
|
|
if exc.name not in {"modal", "modal.experimental"}:
|
|
raise
|
|
else:
|
|
stop_app = getattr(experimental_namespace, "stop_app", None)
|
|
if callable(stop_app):
|
|
try:
|
|
_call_modal_method(
|
|
stop_app,
|
|
app_name,
|
|
environment_name=_modal_environment_name(),
|
|
)
|
|
except _modal_lookup_error_types():
|
|
return True
|
|
return True
|
|
|
|
app_namespace = getattr(modal, "App", None)
|
|
app_lookup = getattr(app_namespace, "lookup", None)
|
|
if not callable(app_lookup):
|
|
return False
|
|
try:
|
|
lookup_signature = inspect.signature(app_lookup)
|
|
except (TypeError, ValueError):
|
|
lookup_signature = None
|
|
lookup_kwargs: dict[str, Any] = {}
|
|
if lookup_signature is not None:
|
|
if "create_if_missing" in lookup_signature.parameters:
|
|
lookup_kwargs["create_if_missing"] = False
|
|
elif "create" in lookup_signature.parameters:
|
|
lookup_kwargs["create"] = False
|
|
try:
|
|
app_handle = app_lookup(app_name, **lookup_kwargs)
|
|
except _modal_lookup_error_types():
|
|
return True
|
|
stop_method = getattr(app_handle, "stop", None)
|
|
if not callable(stop_method):
|
|
return False
|
|
_call_modal_method(stop_method)
|
|
return True
|
|
|
|
|
|
def _stop_modal_app_via_cli(app_name: str) -> bool:
|
|
"""Try to stop a Modal app non-interactively through the Modal CLI."""
|
|
modal_cli = shutil.which("modal")
|
|
if modal_cli is None:
|
|
return False
|
|
command = [modal_cli, "app", "stop", app_name, "--yes"]
|
|
environment_name = _modal_environment_name()
|
|
if environment_name is not None:
|
|
command.extend(("--env", environment_name))
|
|
try:
|
|
completed = subprocess.run(
|
|
command,
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=_MODAL_APP_STOP_TIMEOUT_SECONDS,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
logger.warning(
|
|
"Modal CLI app stop timed out for app=%s environment=%s after %.1fs.",
|
|
app_name,
|
|
environment_name or "<default>",
|
|
_MODAL_APP_STOP_TIMEOUT_SECONDS,
|
|
)
|
|
return False
|
|
if completed.returncode != 0:
|
|
logger.warning(
|
|
"Modal CLI app stop failed for app=%s exit_code=%s stderr=%s",
|
|
app_name,
|
|
completed.returncode,
|
|
completed.stderr.strip(),
|
|
)
|
|
return False
|
|
return True
|
|
|
|
|
|
def _stop_modal_app_for_replacement(app_name: str) -> None:
|
|
"""Stop an out-of-date Modal app before replacing it with a fresh deployment."""
|
|
if _stop_modal_app_via_sdk(app_name):
|
|
logger.warning(
|
|
"Stopped out-of-date Modal app %s through the Modal SDK.", app_name
|
|
)
|
|
return
|
|
if _stop_modal_app_via_cli(app_name):
|
|
logger.warning(
|
|
"Stopped out-of-date Modal app %s through the Modal CLI.", app_name
|
|
)
|
|
return
|
|
raise ModalRemoteInvocationError(
|
|
f"Deployed Modal app {app_name!r} is out of date, but Modal-Sync could not stop it automatically. "
|
|
f"Stop it manually with `modal app stop {app_name}` and retry."
|
|
)
|
|
|
|
|
|
def _mark_modal_deploy_state_not_ready(deploy_key: tuple[str, str | None]) -> None:
|
|
"""Invalidate the local in-process deploy readiness cache for one app."""
|
|
with _MODAL_AUTO_DEPLOY_LOCK:
|
|
deploy_state = _MODAL_AUTO_DEPLOY_STATES.get(deploy_key)
|
|
if deploy_state is None:
|
|
return
|
|
with deploy_state.condition:
|
|
deploy_state.ready = False
|
|
deploy_state.last_error = None
|
|
deploy_state.condition.notify_all()
|
|
|
|
|
|
def _replace_outdated_modal_app(
|
|
payload: dict[str, Any],
|
|
remote_engine: Any,
|
|
*,
|
|
version_payload: dict[str, Any] | None = None,
|
|
) -> Any:
|
|
"""Stop and auto-deploy a replacement for an incompatible deployed app."""
|
|
settings = _settings_for_payload(payload)
|
|
deployment_app_name = modal_deployment_app_name(settings)
|
|
deploy_key = _modal_deploy_cache_key(payload)
|
|
runtime_cache_key = _modal_runtime_cache_key(payload)
|
|
if version_payload is None:
|
|
version_payload = _remote_engine_runtime_version(remote_engine)
|
|
protocol_version = (
|
|
version_payload.get("protocol_version")
|
|
if isinstance(version_payload, dict)
|
|
else None
|
|
)
|
|
remote_fingerprint = _runtime_fingerprint_from_payload(version_payload)
|
|
local_fingerprint = _expected_remote_runtime_fingerprint(payload)
|
|
logger.warning(
|
|
"Deployed Modal app %s is out of date for component=%s remote_protocol=%s local_protocol=%s remote_fingerprint=%s local_fingerprint=%s; stopping and replacing it.",
|
|
deployment_app_name,
|
|
payload.get("component_id"),
|
|
protocol_version,
|
|
_REMOTE_APP_PROTOCOL_VERSION,
|
|
remote_fingerprint,
|
|
local_fingerprint,
|
|
)
|
|
with _MODAL_AUTO_DEPLOY_LOCK:
|
|
_MODAL_REMOTE_APP_VERSION_OK.discard(runtime_cache_key)
|
|
_mark_modal_deploy_state_not_ready(deploy_key)
|
|
_stop_modal_app_for_replacement(deployment_app_name)
|
|
stale_error = ModalRemoteAppOutOfDateError(
|
|
f"Modal app {deployment_app_name!r} runtime identity does not match the local client "
|
|
f"(remote protocol={protocol_version!r}, local protocol={_REMOTE_APP_PROTOCOL_VERSION}, "
|
|
f"remote fingerprint={remote_fingerprint!r}, local fingerprint={local_fingerprint!r})."
|
|
)
|
|
replacement_engine = _auto_deploy_modal_app(payload, stale_error)
|
|
with _MODAL_AUTO_DEPLOY_LOCK:
|
|
_MODAL_REMOTE_APP_VERSION_OK.add(runtime_cache_key)
|
|
return replacement_engine
|
|
|
|
|
|
def _ensure_remote_engine_protocol_current(
|
|
remote_engine: Any, payload: dict[str, Any]
|
|
) -> Any:
|
|
"""Return a compatible remote engine, replacing the deployed app when allowed."""
|
|
settings = _settings_for_payload(payload)
|
|
runtime_cache_key = _modal_runtime_cache_key(payload)
|
|
with _MODAL_AUTO_DEPLOY_LOCK:
|
|
runtime_is_known_current = runtime_cache_key in _MODAL_REMOTE_APP_VERSION_OK
|
|
if runtime_is_known_current:
|
|
logger.info(
|
|
"Rebinding deployed RemoteEngine after cached protocol validation "
|
|
"component=%s worker_affinity=%s.",
|
|
payload.get("component_id"),
|
|
_remote_worker_affinity_key(payload),
|
|
)
|
|
return _lookup_deployed_remote_engine(payload)
|
|
version_payload = _remote_engine_runtime_version(remote_engine)
|
|
if _is_runtime_version_payload_current(version_payload, payload):
|
|
with _MODAL_AUTO_DEPLOY_LOCK:
|
|
_MODAL_REMOTE_APP_VERSION_OK.add(runtime_cache_key)
|
|
return _lookup_deployed_remote_engine(payload)
|
|
if not settings.auto_deploy:
|
|
raise ModalRemoteInvocationError(
|
|
"Deployed Modal app runtime fingerprint is out of date and "
|
|
"COMFY_MODAL_AUTO_DEPLOY=false prevents automatic replacement."
|
|
)
|
|
return _replace_outdated_modal_app(
|
|
payload,
|
|
remote_engine,
|
|
version_payload=version_payload,
|
|
)
|
|
|
|
|
|
def _lookup_protocol_current_remote_engine(payload: dict[str, Any]) -> Any:
|
|
"""Return an affinity-bound engine without allocating a cached protocol probe."""
|
|
runtime_cache_key = _modal_runtime_cache_key(payload)
|
|
with _MODAL_AUTO_DEPLOY_LOCK:
|
|
runtime_is_known_current = runtime_cache_key in _MODAL_REMOTE_APP_VERSION_OK
|
|
if runtime_is_known_current:
|
|
logger.info(
|
|
"Using cached Modal protocol validation without creating a parameterless "
|
|
"probe component=%s worker_affinity=%s.",
|
|
payload.get("component_id"),
|
|
_remote_worker_affinity_key(payload),
|
|
)
|
|
return _lookup_deployed_remote_engine(payload)
|
|
|
|
version_payload = _remote_runtime_version_from_cpu_stager(payload)
|
|
if version_payload is not None:
|
|
if _is_runtime_version_payload_current(version_payload, payload):
|
|
with _MODAL_AUTO_DEPLOY_LOCK:
|
|
_MODAL_REMOTE_APP_VERSION_OK.add(runtime_cache_key)
|
|
logger.info(
|
|
"Validated Modal runtime through CPU ModelStager without allocating "
|
|
"a GPU protocol probe component=%s worker_affinity=%s.",
|
|
payload.get("component_id"),
|
|
_remote_worker_affinity_key(payload),
|
|
)
|
|
return _lookup_deployed_remote_engine(payload)
|
|
settings = _settings_for_payload(payload)
|
|
if not settings.auto_deploy:
|
|
raise ModalRemoteInvocationError(
|
|
"Deployed Modal app runtime fingerprint is out of date and "
|
|
"COMFY_MODAL_AUTO_DEPLOY=false prevents automatic replacement."
|
|
)
|
|
return _replace_outdated_modal_app(
|
|
payload,
|
|
None,
|
|
version_payload=version_payload,
|
|
)
|
|
|
|
logger.info(
|
|
"CPU Modal runtime validation method is unavailable for component=%s; "
|
|
"using the legacy parameterless GPU protocol probe.",
|
|
payload.get("component_id"),
|
|
)
|
|
return _ensure_remote_engine_protocol_current(
|
|
_lookup_deployed_remote_engine(payload, protocol_probe=True),
|
|
payload,
|
|
)
|
|
|
|
|
|
def _modal_auto_deploy_state(
|
|
deploy_key: tuple[str, str | None],
|
|
) -> _ModalAutoDeployState:
|
|
"""Return the shared auto-deploy state bucket for one Modal app/environment."""
|
|
with _MODAL_AUTO_DEPLOY_LOCK:
|
|
state = _MODAL_AUTO_DEPLOY_STATES.get(deploy_key)
|
|
if state is None:
|
|
state = _ModalAutoDeployState()
|
|
_MODAL_AUTO_DEPLOY_STATES[deploy_key] = state
|
|
return state
|
|
|
|
|
|
def _lookup_deployed_remote_engine_with_retry(
|
|
payload: dict[str, Any],
|
|
*,
|
|
timeout_seconds: float = 15.0,
|
|
initial_delay_seconds: float = 0.25,
|
|
max_delay_seconds: float = 2.0,
|
|
) -> Any:
|
|
"""Poll deployed Modal lookup until the freshly deployed app becomes discoverable."""
|
|
lookup_error_types = _modal_lookup_error_types()
|
|
if not lookup_error_types:
|
|
return _lookup_deployed_remote_engine(payload)
|
|
|
|
deadline = time.monotonic() + max(0.0, timeout_seconds)
|
|
delay_seconds = max(0.0, initial_delay_seconds)
|
|
last_error: BaseException | None = None
|
|
while True:
|
|
try:
|
|
return _lookup_deployed_remote_engine(payload)
|
|
except lookup_error_types as exc:
|
|
last_error = exc
|
|
if not _is_missing_modal_deployment_error(exc):
|
|
raise
|
|
remaining_seconds = deadline - time.monotonic()
|
|
if remaining_seconds <= 0.0:
|
|
raise
|
|
time.sleep(min(delay_seconds, remaining_seconds))
|
|
delay_seconds = min(
|
|
max_delay_seconds, max(delay_seconds * 2.0, initial_delay_seconds)
|
|
)
|
|
if last_error is not None:
|
|
raise last_error
|
|
raise ModalRemoteInvocationError(
|
|
"Deployed Modal lookup retry loop exited unexpectedly."
|
|
)
|
|
|
|
|
|
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 _iter_loader_prewarm_prompt_payloads(
|
|
payload: dict[str, Any]
|
|
) -> Iterator[dict[str, Any]]:
|
|
"""Yield subgraph-like payload fragments that may contain root loader nodes."""
|
|
split_proxy_payloads = payload.get("split_proxy_payloads")
|
|
if isinstance(split_proxy_payloads, dict):
|
|
for phase_payload in split_proxy_payloads.values():
|
|
if isinstance(phase_payload, dict):
|
|
yield phase_payload
|
|
return
|
|
if isinstance(split_proxy_payloads, list):
|
|
for phase_payload in split_proxy_payloads:
|
|
if isinstance(phase_payload, dict):
|
|
yield phase_payload
|
|
return
|
|
if isinstance(payload.get("subgraph_prompt"), dict):
|
|
yield payload
|
|
|
|
|
|
def _is_root_literal_loader_node(prompt_node: Mapping[str, Any]) -> bool:
|
|
"""Return whether one prompt node is a supported loader with only literal inputs."""
|
|
class_type = str(prompt_node.get("class_type") or "")
|
|
if class_type not in _ROOT_LOADER_PREWARM_CLASS_TYPES:
|
|
return False
|
|
inputs = prompt_node.get("inputs")
|
|
if not isinstance(inputs, Mapping):
|
|
return False
|
|
return not any(_is_link(input_value) for input_value in inputs.values())
|
|
|
|
|
|
def _loader_prewarm_plan_signature(class_type: str, inputs: Mapping[str, Any]) -> str:
|
|
"""Return a stable signature for one synthetic loader prewarm plan."""
|
|
return json.dumps(
|
|
{
|
|
"class_type": class_type,
|
|
"inputs": copy.deepcopy(dict(inputs)),
|
|
},
|
|
sort_keys=True,
|
|
default=str,
|
|
)
|
|
|
|
|
|
def _loader_snapshot_profile_key(loader_prewarm_plans: list[dict[str, Any]]) -> str:
|
|
"""Return the stable snapshot-profile key for one set of loader prewarm plans."""
|
|
if not loader_prewarm_plans:
|
|
return ""
|
|
|
|
profile_payload = {
|
|
"plan_signatures": sorted(
|
|
str(plan.get("signature") or "")
|
|
for plan in loader_prewarm_plans
|
|
if str(plan.get("signature") or "")
|
|
)
|
|
}
|
|
if not profile_payload["plan_signatures"]:
|
|
return ""
|
|
profile_digest = hashlib.sha256(
|
|
json.dumps(profile_payload, sort_keys=True).encode("utf-8")
|
|
).hexdigest()
|
|
return f"loader-profile:{profile_digest}"
|
|
|
|
|
|
def _normalize_loader_snapshot_profile_record(
|
|
loader_prewarm_plans: list[dict[str, Any]],
|
|
) -> dict[str, Any] | None:
|
|
"""Return the normalized snapshot-profile record for one loader-plan set."""
|
|
snapshot_profile_key = _loader_snapshot_profile_key(loader_prewarm_plans)
|
|
if not snapshot_profile_key:
|
|
return None
|
|
return {
|
|
"snapshot_profile_key": snapshot_profile_key,
|
|
"loader_prewarm_plans": copy.deepcopy(loader_prewarm_plans),
|
|
"snapshot_policy": {
|
|
"selected_variant": None,
|
|
"measurements": {"snapshot": [], "direct": []},
|
|
},
|
|
}
|
|
|
|
|
|
def _store_loader_snapshot_profile(loader_prewarm_plans: list[dict[str, Any]]) -> str:
|
|
"""Persist one loader snapshot profile so `snap=True` can retrieve it later."""
|
|
settings = get_settings()
|
|
if not settings.enable_gpu_memory_snapshot or modal is None:
|
|
return ""
|
|
|
|
normalized_record = _normalize_loader_snapshot_profile_record(loader_prewarm_plans)
|
|
if normalized_record is None:
|
|
return ""
|
|
|
|
snapshot_profile_key = str(normalized_record["snapshot_profile_key"])
|
|
with _SNAPSHOT_PROFILE_RECORDS_LOCK:
|
|
cached_record = _SNAPSHOT_PROFILE_RECORDS.get(snapshot_profile_key)
|
|
if cached_record is not None:
|
|
return snapshot_profile_key
|
|
snapshot_profiles = modal.Dict.from_name(
|
|
settings.snapshot_profile_dict_name,
|
|
create_if_missing=True,
|
|
)
|
|
stored_record = snapshot_profiles.get(snapshot_profile_key)
|
|
if isinstance(stored_record, dict):
|
|
merged_record = copy.deepcopy(stored_record)
|
|
merged_record["snapshot_profile_key"] = snapshot_profile_key
|
|
merged_record["loader_prewarm_plans"] = copy.deepcopy(
|
|
loader_prewarm_plans
|
|
)
|
|
else:
|
|
merged_record = normalized_record
|
|
snapshot_profiles[snapshot_profile_key] = merged_record
|
|
_SNAPSHOT_PROFILE_RECORDS[snapshot_profile_key] = merged_record
|
|
return snapshot_profile_key
|
|
|
|
|
|
def _prepare_snapshot_profile_fields(payload: dict[str, Any]) -> str:
|
|
"""Derive, persist, and attach the loader profile used by one request."""
|
|
loader_prewarm_plans = _build_loader_prewarm_plans(payload)
|
|
stored_profile_key = _store_loader_snapshot_profile(loader_prewarm_plans)
|
|
existing_profile_key = str(payload.get("snapshot_profile_key") or "").strip()
|
|
snapshot_profile_key = stored_profile_key or existing_profile_key
|
|
if snapshot_profile_key:
|
|
payload["snapshot_profile_key"] = snapshot_profile_key
|
|
return snapshot_profile_key
|
|
|
|
|
|
def _snapshot_profile_store_for_payload(payload: Mapping[str, Any]) -> Any | None:
|
|
"""Return the shared profile store when Modal and snapshot policy are enabled."""
|
|
if modal is None:
|
|
return None
|
|
settings = _settings_for_payload(payload)
|
|
return modal.Dict.from_name(
|
|
settings.snapshot_profile_dict_name,
|
|
create_if_missing=True,
|
|
)
|
|
|
|
|
|
def _snapshot_policy_record(
|
|
payload: Mapping[str, Any], snapshot_profile_key: str
|
|
) -> dict[str, Any] | None:
|
|
"""Load one profile record without discarding locally cached measurements."""
|
|
with _SNAPSHOT_PROFILE_RECORDS_LOCK:
|
|
cached = _SNAPSHOT_PROFILE_RECORDS.get(snapshot_profile_key)
|
|
store = _snapshot_profile_store_for_payload(payload)
|
|
if store is None:
|
|
return copy.deepcopy(cached) if cached is not None else None
|
|
stored = store.get(snapshot_profile_key)
|
|
if not isinstance(stored, dict):
|
|
return copy.deepcopy(cached) if cached is not None else None
|
|
with _SNAPSHOT_PROFILE_RECORDS_LOCK:
|
|
_SNAPSHOT_PROFILE_RECORDS[snapshot_profile_key] = copy.deepcopy(stored)
|
|
return copy.deepcopy(stored)
|
|
|
|
|
|
def _select_gpu_snapshot_for_profile(
|
|
payload: dict[str, Any], snapshot_profile_key: str
|
|
) -> bool:
|
|
"""Choose snapshot or direct startup from persisted per-profile measurements."""
|
|
settings = _settings_for_payload(payload)
|
|
if not settings.enable_gpu_memory_snapshot or not snapshot_profile_key:
|
|
payload["gpu_snapshot_variant"] = "direct"
|
|
return False
|
|
requested_variant = str(payload.get("gpu_snapshot_variant") or "").strip()
|
|
if requested_variant in {"snapshot", "direct"}:
|
|
return requested_variant == "snapshot"
|
|
|
|
record = _snapshot_policy_record(payload, snapshot_profile_key) or {}
|
|
policy = record.get("snapshot_policy")
|
|
if not isinstance(policy, Mapping):
|
|
policy = {}
|
|
selected_variant = policy.get("selected_variant")
|
|
if selected_variant not in {"snapshot", "direct"}:
|
|
measurements = policy.get("measurements")
|
|
if not isinstance(measurements, Mapping):
|
|
measurements = {}
|
|
snapshot_samples = measurements.get("snapshot")
|
|
direct_samples = measurements.get("direct")
|
|
snapshot_count = len(snapshot_samples) if isinstance(snapshot_samples, list) else 0
|
|
direct_count = len(direct_samples) if isinstance(direct_samples, list) else 0
|
|
selected_variant = "snapshot" if snapshot_count <= direct_count else "direct"
|
|
payload["gpu_snapshot_variant"] = selected_variant
|
|
logger.info(
|
|
"Selected Modal startup variant=%s snapshot_profile=%s component=%s.",
|
|
selected_variant,
|
|
snapshot_profile_key,
|
|
payload.get("component_id"),
|
|
)
|
|
return selected_variant == "snapshot"
|
|
|
|
|
|
def _record_snapshot_warmup_measurement(
|
|
payload: Mapping[str, Any], elapsed_seconds: float
|
|
) -> None:
|
|
"""Persist one warmup latency and select the faster profile after both variants run."""
|
|
snapshot_profile_key = str(payload.get("snapshot_profile_key") or "").strip()
|
|
variant = str(payload.get("gpu_snapshot_variant") or "").strip()
|
|
if not snapshot_profile_key or variant not in {"snapshot", "direct"}:
|
|
return
|
|
store = _snapshot_profile_store_for_payload(payload)
|
|
if store is None:
|
|
return
|
|
record = _snapshot_policy_record(payload, snapshot_profile_key) or {
|
|
"snapshot_profile_key": snapshot_profile_key,
|
|
"loader_prewarm_plans": [],
|
|
}
|
|
policy = record.get("snapshot_policy")
|
|
if not isinstance(policy, dict):
|
|
policy = {}
|
|
measurements = policy.get("measurements")
|
|
if not isinstance(measurements, dict):
|
|
measurements = {"snapshot": [], "direct": []}
|
|
for candidate in ("snapshot", "direct"):
|
|
samples = measurements.get(candidate)
|
|
if not isinstance(samples, list):
|
|
measurements[candidate] = []
|
|
measurements[variant].append(float(elapsed_seconds))
|
|
measurements[variant] = measurements[variant][-5:]
|
|
if measurements["snapshot"] and measurements["direct"]:
|
|
policy["selected_variant"] = min(
|
|
("snapshot", "direct"),
|
|
key=lambda candidate: statistics.median(measurements[candidate]),
|
|
)
|
|
else:
|
|
policy["selected_variant"] = None
|
|
policy["measurements"] = measurements
|
|
record["snapshot_policy"] = policy
|
|
store[snapshot_profile_key] = record
|
|
with _SNAPSHOT_PROFILE_RECORDS_LOCK:
|
|
_SNAPSHOT_PROFILE_RECORDS[snapshot_profile_key] = copy.deepcopy(record)
|
|
logger.info(
|
|
"Recorded Modal startup measurement snapshot_profile=%s variant=%s elapsed=%.3fs selected=%s.",
|
|
snapshot_profile_key,
|
|
variant,
|
|
elapsed_seconds,
|
|
policy["selected_variant"],
|
|
)
|
|
|
|
|
|
def _build_loader_prewarm_plans(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
|
"""Return synthetic one-node subgraph plans for supported root loader nodes."""
|
|
if not get_settings().enable_loader_prewarm:
|
|
return []
|
|
|
|
prompt_id = (
|
|
str(payload.get("prompt_id")) if payload.get("prompt_id") is not None else None
|
|
)
|
|
seen_signatures: set[str] = set()
|
|
plans: list[dict[str, Any]] = []
|
|
for prompt_payload in _iter_loader_prewarm_prompt_payloads(payload):
|
|
subgraph_prompt = prompt_payload.get("subgraph_prompt")
|
|
if not isinstance(subgraph_prompt, Mapping):
|
|
continue
|
|
for node_id, prompt_node in subgraph_prompt.items():
|
|
if not isinstance(prompt_node, Mapping) or not _is_root_literal_loader_node(
|
|
prompt_node
|
|
):
|
|
continue
|
|
class_type = str(prompt_node.get("class_type") or "")
|
|
inputs = prompt_node.get("inputs")
|
|
if not isinstance(inputs, Mapping):
|
|
continue
|
|
signature = _loader_prewarm_plan_signature(class_type, inputs)
|
|
if signature in seen_signatures:
|
|
continue
|
|
seen_signatures.add(signature)
|
|
plans.append(
|
|
{
|
|
"signature": signature,
|
|
"node_id": str(node_id),
|
|
"class_type": class_type,
|
|
"prompt_id": prompt_id,
|
|
"subgraph_prompt": {
|
|
str(node_id): copy.deepcopy(dict(prompt_node)),
|
|
},
|
|
"execute_node_ids": [str(node_id)],
|
|
}
|
|
)
|
|
return plans
|
|
|
|
|
|
def _iter_executable_subgraph_nodes(
|
|
payload: Mapping[str, Any],
|
|
) -> Iterator[Mapping[str, Any]]:
|
|
"""Yield only prompt nodes required by this payload's execution targets."""
|
|
for prompt_payload in _iter_loader_prewarm_prompt_payloads(dict(payload)):
|
|
raw_prompt = prompt_payload.get("subgraph_prompt")
|
|
if not isinstance(raw_prompt, Mapping):
|
|
continue
|
|
prompt = {
|
|
str(node_id): dict(prompt_node)
|
|
for node_id, prompt_node in raw_prompt.items()
|
|
if isinstance(prompt_node, Mapping)
|
|
}
|
|
execute_node_ids: list[str] = []
|
|
for field_name in (
|
|
"execute_node_ids",
|
|
"mapped_execute_node_ids",
|
|
"static_execute_node_ids",
|
|
):
|
|
field_value = prompt_payload.get(field_name)
|
|
if isinstance(field_value, (list, tuple)):
|
|
execute_node_ids.extend(str(node_id) for node_id in field_value)
|
|
required_node_ids = _resolve_required_subgraph_nodes(
|
|
prompt,
|
|
list(dict.fromkeys(execute_node_ids)),
|
|
)
|
|
for node_id in required_node_ids:
|
|
yield prompt[node_id]
|
|
|
|
|
|
def _build_llm_prewarm_plans(payload: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
"""Return LLM warmups found in executable subgraph dependency closures."""
|
|
plans: list[dict[str, Any]] = []
|
|
seen_profiles: set[str] = set()
|
|
for prompt_node in _iter_executable_subgraph_nodes(payload):
|
|
if prompt_node.get("class_type") != "ModalLLM":
|
|
continue
|
|
inputs = prompt_node.get("inputs")
|
|
if not isinstance(inputs, Mapping):
|
|
continue
|
|
model_profile = inputs.get("model_profile")
|
|
if not isinstance(model_profile, str) or not model_profile.strip():
|
|
continue
|
|
normalized_profile = model_profile.strip()
|
|
if normalized_profile in seen_profiles:
|
|
continue
|
|
seen_profiles.add(normalized_profile)
|
|
signature_payload = {
|
|
"model_profile": normalized_profile,
|
|
"representative_request_count": 3,
|
|
}
|
|
plans.append(
|
|
{
|
|
**signature_payload,
|
|
"signature": hashlib.sha256(
|
|
json.dumps(signature_payload, sort_keys=True).encode("utf-8")
|
|
).hexdigest(),
|
|
"prompt_node": copy.deepcopy(dict(prompt_node)),
|
|
}
|
|
)
|
|
return plans
|
|
|
|
|
|
def _build_prompt_warmup_request(payload: dict[str, Any]) -> dict[str, Any] | None:
|
|
"""Extract the prompt-scoped warmup-relevant fields from one payload."""
|
|
settings = _settings_for_payload(payload)
|
|
loader_prewarm_plans = _build_loader_prewarm_plans(payload)
|
|
llm_prewarm_plans = _build_llm_prewarm_plans(payload)
|
|
snapshot_profile_key = _store_loader_snapshot_profile(loader_prewarm_plans)
|
|
if settings.enable_gpu_memory_snapshot and not snapshot_profile_key and not llm_prewarm_plans:
|
|
logger.info(
|
|
"Skipping proactive Modal warmup request for component=%s because GPU snapshots are enabled and no loader snapshot profile was derived.",
|
|
payload.get("component_id"),
|
|
)
|
|
return None
|
|
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"),
|
|
)
|
|
),
|
|
"modal_gpu": settings.modal_gpu,
|
|
"remote_worker_affinity_group": payload.get(
|
|
"remote_worker_affinity_group"
|
|
),
|
|
"remote_local_gap_pool": bool(payload.get("remote_local_gap_pool")),
|
|
"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"),
|
|
"loader_prewarm_plans": loader_prewarm_plans,
|
|
"llm_prewarm_plans": llm_prewarm_plans,
|
|
"snapshot_profile_key": snapshot_profile_key,
|
|
}
|
|
|
|
|
|
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 _prompt_warmup_head_start_seconds() -> float:
|
|
"""Return the bounded delay the local scheduler may wait for exact warmup slots to start."""
|
|
return max(0.0, float(get_settings().proactive_warmup_head_start_seconds))
|
|
|
|
|
|
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
|
|
started_at = time.perf_counter()
|
|
if hasattr(warmup_method, "remote"):
|
|
result = warmup_method.remote(warmup_request)
|
|
else:
|
|
result = warmup_method(warmup_request)
|
|
_record_snapshot_warmup_measurement(
|
|
warmup_request,
|
|
time.perf_counter() - started_at,
|
|
)
|
|
return result
|
|
|
|
|
|
def _invoke_remote_engine_warmup_with_recovery(
|
|
remote_engine: Any,
|
|
warmup_request: dict[str, Any],
|
|
) -> Any:
|
|
"""Retry one warmup call after auto-deploy when a stale deployed handle vanishes."""
|
|
lookup_error_types = _modal_lookup_error_types()
|
|
settings = _settings_for_payload(warmup_request)
|
|
try:
|
|
return _invoke_remote_engine_warmup(remote_engine, warmup_request)
|
|
except lookup_error_types as exc:
|
|
if not settings.auto_deploy or not _is_missing_modal_deployment_error(exc):
|
|
raise
|
|
logger.warning(
|
|
"Modal warmup invocation failed for component=%s because the deployed app was missing at call time: %s. Recreating the app and retrying.",
|
|
warmup_request.get("component_id"),
|
|
exc,
|
|
)
|
|
recovered_remote_engine = _auto_deploy_modal_app(warmup_request, exc)
|
|
_ensure_llm_profiles_staged(
|
|
warmup_request,
|
|
modal_deployment_app_name(settings),
|
|
)
|
|
return _invoke_remote_engine_warmup(recovered_remote_engine, 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 = _settings_for_payload(warmup_request)
|
|
deployment_app_name = modal_deployment_app_name(settings)
|
|
if lookup_error_types:
|
|
try:
|
|
remote_engine = _lookup_protocol_current_remote_engine(warmup_request)
|
|
_ensure_llm_profiles_staged(warmup_request, deployment_app_name)
|
|
return _invoke_remote_engine_warmup_with_recovery(
|
|
remote_engine, warmup_request
|
|
)
|
|
except lookup_error_types as exc:
|
|
if settings.auto_deploy:
|
|
remote_engine = _auto_deploy_modal_app(warmup_request, exc)
|
|
try:
|
|
_ensure_llm_profiles_staged(warmup_request, deployment_app_name)
|
|
return _invoke_remote_engine_warmup_with_recovery(
|
|
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={deployment_app_name!r}: {exc}."
|
|
) from exc
|
|
else:
|
|
remote_engine = _lookup_protocol_current_remote_engine(warmup_request)
|
|
_ensure_llm_profiles_staged(warmup_request, deployment_app_name)
|
|
return _invoke_remote_engine_warmup_with_recovery(remote_engine, warmup_request)
|
|
|
|
with _modal_cloud_settings_override(settings):
|
|
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_with_recovery(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 _track_prompt_warmup_future(
|
|
prompt_id: str,
|
|
slot_index: int,
|
|
future: Future[Any],
|
|
) -> None:
|
|
"""Track one in-flight warmup future so mapped execution can await short exact warmup bursts."""
|
|
with _PROMPT_WARMUP_STATES_LOCK:
|
|
warmup_state = _PROMPT_WARMUP_STATES.get(prompt_id)
|
|
if warmup_state is not None:
|
|
warmup_state.slot_futures[slot_index] = future
|
|
|
|
def _clear_tracked_future(completed_future: Future[Any]) -> None:
|
|
"""Drop one completed future from the prompt warmup state."""
|
|
with _PROMPT_WARMUP_STATES_LOCK:
|
|
warmup_state = _PROMPT_WARMUP_STATES.get(prompt_id)
|
|
if warmup_state is None:
|
|
return
|
|
tracked_future = warmup_state.slot_futures.get(slot_index)
|
|
if tracked_future is completed_future:
|
|
warmup_state.slot_futures.pop(slot_index, None)
|
|
|
|
future.add_done_callback(_clear_tracked_future)
|
|
|
|
|
|
async def _await_prompt_warmup_slots(
|
|
prompt_id: str,
|
|
slot_indices: list[int],
|
|
timeout_seconds: float,
|
|
) -> int:
|
|
"""Wait briefly for any in-flight prompt warmup slots to finish."""
|
|
if timeout_seconds <= 0.0 or not slot_indices:
|
|
return 0
|
|
|
|
with _PROMPT_WARMUP_STATES_LOCK:
|
|
warmup_state = _PROMPT_WARMUP_STATES.get(prompt_id)
|
|
if warmup_state is None:
|
|
return 0
|
|
pending_slot_futures = [
|
|
warmup_state.slot_futures[slot_index]
|
|
for slot_index in slot_indices
|
|
if slot_index in warmup_state.slot_futures
|
|
and not warmup_state.slot_futures[slot_index].done()
|
|
]
|
|
|
|
if not pending_slot_futures:
|
|
return 0
|
|
|
|
wrapped_futures = [
|
|
asyncio.wrap_future(slot_future) for slot_future in pending_slot_futures
|
|
]
|
|
done_futures, pending_futures = await asyncio.wait(
|
|
wrapped_futures,
|
|
timeout=timeout_seconds,
|
|
return_when=asyncio.ALL_COMPLETED,
|
|
)
|
|
if done_futures:
|
|
logger.info(
|
|
"Prompt=%s warmup head-start completed %d/%d slot(s) before mapped dispatch.",
|
|
prompt_id,
|
|
len(done_futures),
|
|
len(wrapped_futures),
|
|
)
|
|
elif pending_futures:
|
|
logger.info(
|
|
"Prompt=%s warmup head-start timed out after %.3fs with %d slot(s) still warming.",
|
|
prompt_id,
|
|
timeout_seconds,
|
|
len(pending_futures),
|
|
)
|
|
return len(done_futures)
|
|
|
|
|
|
def ensure_remote_warm_capacity(
|
|
warmup_request: dict[str, Any] | None,
|
|
*,
|
|
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
|
|
if warmup_request 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:
|
|
future = _REMOTE_MODAL_WARMUP_EXECUTOR.submit(
|
|
_run_prompt_warmup_slot,
|
|
prompt_id,
|
|
slot_index,
|
|
copy.deepcopy(warmup_request),
|
|
reason,
|
|
)
|
|
_track_prompt_warmup_future(prompt_id, slot_index, future)
|
|
return clamped_target
|
|
|
|
|
|
def _run_speculative_affinity_prewarm(
|
|
prompt_id: str,
|
|
warmup_identity: str,
|
|
warmup_request: dict[str, Any],
|
|
reason: str,
|
|
) -> None:
|
|
"""Prepare one future affinity worker and make a failed attempt retryable."""
|
|
affinity_key = _remote_worker_affinity_key(warmup_request)
|
|
try:
|
|
logger.info(
|
|
"Starting speculative Modal prewarm prompt=%s affinity=%s component=%s reason=%s.",
|
|
prompt_id,
|
|
affinity_key,
|
|
warmup_request.get("component_id"),
|
|
reason,
|
|
)
|
|
_invoke_modal_warmup_blocking(_warmup_slot_payload(warmup_request, 0))
|
|
logger.info(
|
|
"Completed speculative Modal prewarm prompt=%s affinity=%s component=%s.",
|
|
prompt_id,
|
|
affinity_key,
|
|
warmup_request.get("component_id"),
|
|
)
|
|
except Exception:
|
|
with _PROMPT_WARMUP_STATES_LOCK:
|
|
warmup_state = _PROMPT_WARMUP_STATES.get(prompt_id)
|
|
if warmup_state is not None:
|
|
warmup_state.scheduled_speculative_affinities.discard(
|
|
warmup_identity
|
|
)
|
|
logger.exception(
|
|
"Speculative Modal prewarm failed prompt=%s affinity=%s component=%s.",
|
|
prompt_id,
|
|
affinity_key,
|
|
warmup_request.get("component_id"),
|
|
)
|
|
|
|
|
|
def _schedule_speculative_affinity_prewarm(
|
|
payload: Mapping[str, Any],
|
|
*,
|
|
reason: str,
|
|
) -> bool:
|
|
"""Schedule one planner-selected future worker without blocking current execution."""
|
|
target_payload = payload.get(_SPECULATIVE_PREWARM_TARGET_KEY)
|
|
if not isinstance(target_payload, Mapping):
|
|
return False
|
|
if (
|
|
payload.get("execution_provider") != "modal"
|
|
or target_payload.get("execution_provider") != "modal"
|
|
or payload.get("execution_environment_id")
|
|
!= target_payload.get("execution_environment_id")
|
|
):
|
|
logger.debug(
|
|
"Skipping speculative Modal prewarm across execution environments "
|
|
"source=%s target=%s.",
|
|
payload.get("execution_environment_id"),
|
|
target_payload.get("execution_environment_id"),
|
|
)
|
|
return False
|
|
|
|
normalized_target_payload = copy.deepcopy(dict(target_payload))
|
|
settings = _settings_for_payload(normalized_target_payload)
|
|
if (
|
|
not settings.enable_proactive_warmup
|
|
or settings.execution_mode == "local"
|
|
or modal is None
|
|
or (settings.max_containers is not None and settings.max_containers < 2)
|
|
):
|
|
return False
|
|
|
|
warmup_request = _build_prompt_warmup_request(normalized_target_payload)
|
|
if warmup_request is None:
|
|
return False
|
|
prompt_id = _warmup_prompt_id(warmup_request)
|
|
if prompt_id is None:
|
|
return False
|
|
|
|
current_affinity_key = _remote_worker_affinity_key(dict(payload))
|
|
target_affinity_key = _remote_worker_affinity_key(warmup_request)
|
|
if target_affinity_key == current_affinity_key:
|
|
return False
|
|
snapshot_profile_key = _prepare_snapshot_profile_fields(warmup_request)
|
|
_select_gpu_snapshot_for_profile(warmup_request, snapshot_profile_key)
|
|
warmup_identity = _speculative_warmup_identity(warmup_request)
|
|
|
|
with _PROMPT_WARMUP_STATES_LOCK:
|
|
warmup_state = _ensure_prompt_warmup_state(prompt_id)
|
|
if warmup_identity in warmup_state.scheduled_speculative_affinities:
|
|
return False
|
|
warmup_state.scheduled_speculative_affinities.add(warmup_identity)
|
|
|
|
logger.info(
|
|
"Scheduling speculative Modal prewarm prompt=%s current_component=%s current_affinity=%s target_component=%s target_affinity=%s reason=%s.",
|
|
prompt_id,
|
|
payload.get("component_id"),
|
|
current_affinity_key,
|
|
warmup_request.get("component_id"),
|
|
target_affinity_key,
|
|
reason,
|
|
)
|
|
future = _REMOTE_MODAL_WARMUP_EXECUTOR.submit(
|
|
_run_speculative_affinity_prewarm,
|
|
prompt_id,
|
|
warmup_identity,
|
|
copy.deepcopy(warmup_request),
|
|
reason,
|
|
)
|
|
with _PROMPT_WARMUP_STATES_LOCK:
|
|
warmup_state = _PROMPT_WARMUP_STATES.get(prompt_id)
|
|
if warmup_state is not None:
|
|
warmup_state.speculative_affinity_futures[warmup_identity] = future
|
|
|
|
def _clear_speculative_future(completed_future: Future[Any]) -> None:
|
|
"""Drop a completed speculative future while retaining successful dedupe state."""
|
|
with _PROMPT_WARMUP_STATES_LOCK:
|
|
warmup_state = _PROMPT_WARMUP_STATES.get(prompt_id)
|
|
if warmup_state is None:
|
|
return
|
|
tracked_future = warmup_state.speculative_affinity_futures.get(
|
|
warmup_identity
|
|
)
|
|
if tracked_future is completed_future:
|
|
warmup_state.speculative_affinity_futures.pop(
|
|
warmup_identity, None
|
|
)
|
|
|
|
future.add_done_callback(_clear_speculative_future)
|
|
return True
|
|
|
|
|
|
def _speculative_warmup_identity(payload: Mapping[str, Any]) -> str:
|
|
"""Return the exact deployed-worker identity shared by warmup and dispatch."""
|
|
settings = _settings_for_payload(payload)
|
|
identity = {
|
|
"app": modal_deployment_app_name(settings),
|
|
"gpu": settings.modal_gpu,
|
|
"affinity": _remote_worker_affinity_key(dict(payload)),
|
|
"snapshot_profile": str(payload.get("snapshot_profile_key") or ""),
|
|
"snapshot_variant": str(payload.get("gpu_snapshot_variant") or "direct"),
|
|
}
|
|
return json.dumps(identity, sort_keys=True, separators=(",", ":"))
|
|
|
|
|
|
def _await_matching_speculative_prewarm(
|
|
payload: dict[str, Any],
|
|
cancellation_event: threading.Event | None,
|
|
) -> None:
|
|
"""Join an active speculative warmup instead of racing a duplicate container."""
|
|
prompt_id = _warmup_prompt_id(payload)
|
|
if prompt_id is None:
|
|
return
|
|
snapshot_profile_key = _prepare_snapshot_profile_fields(payload)
|
|
_select_gpu_snapshot_for_profile(payload, snapshot_profile_key)
|
|
warmup_identity = _speculative_warmup_identity(payload)
|
|
with _PROMPT_WARMUP_STATES_LOCK:
|
|
warmup_state = _PROMPT_WARMUP_STATES.get(prompt_id)
|
|
future = (
|
|
warmup_state.speculative_affinity_futures.get(warmup_identity)
|
|
if warmup_state is not None
|
|
else None
|
|
)
|
|
if future is None or future.done():
|
|
return
|
|
started_at = time.perf_counter()
|
|
logger.info(
|
|
"Joining active speculative Modal prewarm before dispatch prompt=%s component=%s identity=%s.",
|
|
prompt_id,
|
|
payload.get("component_id"),
|
|
warmup_identity,
|
|
)
|
|
while not future.done():
|
|
if cancellation_event is not None and cancellation_event.is_set():
|
|
raise ModalRemoteInvocationError(
|
|
"Remote dispatch was cancelled while waiting for speculative warmup."
|
|
)
|
|
try:
|
|
future.result(timeout=0.1)
|
|
except FutureTimeoutError:
|
|
continue
|
|
future.result()
|
|
logger.info(
|
|
"Joined speculative Modal prewarm before dispatch prompt=%s component=%s wait_seconds=%.3f.",
|
|
prompt_id,
|
|
payload.get("component_id"),
|
|
time.perf_counter() - started_at,
|
|
)
|
|
|
|
|
|
def _schedule_post_deploy_runtime_seed(payload: Mapping[str, Any]) -> bool:
|
|
"""Seed the just-deployed profile and expose its future to the first dispatch."""
|
|
warmup_request = _build_prompt_warmup_request(copy.deepcopy(dict(payload)))
|
|
if warmup_request is None:
|
|
return False
|
|
prompt_id = _warmup_prompt_id(warmup_request)
|
|
if prompt_id is None:
|
|
return False
|
|
snapshot_profile_key = _prepare_snapshot_profile_fields(warmup_request)
|
|
_select_gpu_snapshot_for_profile(warmup_request, snapshot_profile_key)
|
|
warmup_identity = _speculative_warmup_identity(warmup_request)
|
|
with _PROMPT_WARMUP_STATES_LOCK:
|
|
warmup_state = _ensure_prompt_warmup_state(prompt_id)
|
|
if warmup_identity in warmup_state.scheduled_speculative_affinities:
|
|
return False
|
|
warmup_state.scheduled_speculative_affinities.add(warmup_identity)
|
|
future = _REMOTE_MODAL_WARMUP_EXECUTOR.submit(
|
|
_run_speculative_affinity_prewarm,
|
|
prompt_id,
|
|
warmup_identity,
|
|
copy.deepcopy(warmup_request),
|
|
"post_deploy_runtime_seed",
|
|
)
|
|
warmup_state.speculative_affinity_futures[warmup_identity] = future
|
|
|
|
def clear_seed_future(completed_future: Future[Any]) -> None:
|
|
"""Forget the completed seed future while retaining successful dedupe state."""
|
|
with _PROMPT_WARMUP_STATES_LOCK:
|
|
current_state = _PROMPT_WARMUP_STATES.get(prompt_id)
|
|
if current_state is None:
|
|
return
|
|
if (
|
|
current_state.speculative_affinity_futures.get(warmup_identity)
|
|
is completed_future
|
|
):
|
|
current_state.speculative_affinity_futures.pop(
|
|
warmup_identity, None
|
|
)
|
|
|
|
future.add_done_callback(clear_seed_future)
|
|
logger.info(
|
|
"Scheduled automatic post-deployment Modal runtime seed prompt=%s component=%s identity=%s.",
|
|
prompt_id,
|
|
warmup_request.get("component_id"),
|
|
warmup_identity,
|
|
)
|
|
return True
|
|
|
|
|
|
def _local_gap_keepalive_key(payload: Mapping[str, Any]) -> tuple[str, int] | None:
|
|
"""Return the prompt and affinity-slot key for a local-gap payload."""
|
|
prompt_id = _warmup_prompt_id(dict(payload))
|
|
if prompt_id is None:
|
|
return None
|
|
return (prompt_id, _component_pool_slot_index(dict(payload)))
|
|
|
|
|
|
def _stop_local_gap_keepalive(payload: Mapping[str, Any], *, reason: str) -> bool:
|
|
"""Stop a previously scheduled affinity-slot keepalive, if one exists."""
|
|
keepalive_key = _local_gap_keepalive_key(payload)
|
|
if keepalive_key is None:
|
|
return False
|
|
with _LOCAL_GAP_KEEPALIVES_LOCK:
|
|
keepalive_state = _LOCAL_GAP_KEEPALIVES.pop(keepalive_key, None)
|
|
if keepalive_state is None:
|
|
return False
|
|
keepalive_state.stop_event.set()
|
|
logger.info(
|
|
"Stopping remote local-gap keepalive prompt=%s slot=%d after component=%s reason=%s.",
|
|
keepalive_key[0],
|
|
keepalive_key[1],
|
|
keepalive_state.component_id,
|
|
reason,
|
|
)
|
|
return True
|
|
|
|
|
|
def _invoke_remote_local_gap_keepalive(payload: dict[str, Any]) -> Any:
|
|
"""Send one lightweight activity pulse to the payload's affinity slot."""
|
|
remote_engine = _lookup_deployed_remote_engine(payload)
|
|
keepalive_method = getattr(remote_engine, "keepalive_for_local_gap", None)
|
|
if keepalive_method is None:
|
|
raise ModalRemoteInvocationError(
|
|
"The deployed RemoteEngine does not expose keepalive_for_local_gap."
|
|
)
|
|
return _call_modal_method(keepalive_method, payload)
|
|
|
|
|
|
def _run_local_gap_keepalive(
|
|
payload: dict[str, Any],
|
|
stop_event: threading.Event,
|
|
duration_seconds: float,
|
|
interval_seconds: float,
|
|
) -> None:
|
|
"""Pulse one remote affinity slot until local execution finishes or the budget expires."""
|
|
deadline = time.monotonic() + duration_seconds
|
|
keepalive_count = 0
|
|
while not stop_event.is_set():
|
|
remaining_seconds = deadline - time.monotonic()
|
|
if remaining_seconds <= 0.0:
|
|
break
|
|
if stop_event.wait(min(interval_seconds, remaining_seconds)):
|
|
break
|
|
keepalive_count += 1
|
|
pulse_started_at = time.perf_counter()
|
|
_invoke_remote_local_gap_keepalive(payload)
|
|
logger.info(
|
|
"Completed remote local-gap keepalive pulse=%d prompt=%s component=%s in %.3fs.",
|
|
keepalive_count,
|
|
payload.get("prompt_id"),
|
|
payload.get("component_id"),
|
|
time.perf_counter() - pulse_started_at,
|
|
)
|
|
logger.info(
|
|
"Remote local-gap keepalive loop finished prompt=%s component=%s pulses=%d stopped=%s.",
|
|
payload.get("prompt_id"),
|
|
payload.get("component_id"),
|
|
keepalive_count,
|
|
stop_event.is_set(),
|
|
)
|
|
|
|
|
|
def _start_local_gap_keepalive(payload: Mapping[str, Any]) -> bool:
|
|
"""Retain one remote affinity slot while a downstream local gap executes."""
|
|
if (
|
|
payload.get("execution_provider") != "modal"
|
|
or not bool(payload.get("keepalive_after_remote_component"))
|
|
):
|
|
return False
|
|
settings = _settings_for_payload(payload)
|
|
duration_seconds = max(0.0, float(settings.local_gap_keepalive_seconds))
|
|
if duration_seconds <= 0.0 or settings.execution_mode == "local" or modal is None:
|
|
return False
|
|
keepalive_key = _local_gap_keepalive_key(payload)
|
|
if keepalive_key is None:
|
|
return False
|
|
|
|
_stop_local_gap_keepalive(payload, reason="replacement")
|
|
stop_event = threading.Event()
|
|
keepalive_payload = dict(payload)
|
|
keepalive_payload["warmup_slot_index"] = keepalive_key[1]
|
|
keepalive_payload["component_id"] = (
|
|
f"{payload.get('component_id', 'modal-component')}::local-gap-keepalive"
|
|
)
|
|
interval_seconds = min(
|
|
duration_seconds,
|
|
max(1.0, float(settings.local_gap_keepalive_interval_seconds)),
|
|
)
|
|
future = _REMOTE_MODAL_KEEPALIVE_EXECUTOR.submit(
|
|
_run_local_gap_keepalive,
|
|
keepalive_payload,
|
|
stop_event,
|
|
duration_seconds,
|
|
interval_seconds,
|
|
)
|
|
keepalive_state = _LocalGapKeepaliveState(
|
|
component_id=str(payload.get("component_id") or "modal-component"),
|
|
stop_event=stop_event,
|
|
future=future,
|
|
)
|
|
with _LOCAL_GAP_KEEPALIVES_LOCK:
|
|
_LOCAL_GAP_KEEPALIVES[keepalive_key] = keepalive_state
|
|
|
|
def clear_completed_keepalive(completed_future: Future[Any]) -> None:
|
|
"""Forget a finished keepalive and report any best-effort failure."""
|
|
with _LOCAL_GAP_KEEPALIVES_LOCK:
|
|
current_state = _LOCAL_GAP_KEEPALIVES.get(keepalive_key)
|
|
if current_state is keepalive_state:
|
|
_LOCAL_GAP_KEEPALIVES.pop(keepalive_key, None)
|
|
failure = completed_future.exception()
|
|
if failure is not None:
|
|
logger.warning(
|
|
"Remote local-gap keepalive failed prompt=%s slot=%d component=%s: %s",
|
|
keepalive_key[0],
|
|
keepalive_key[1],
|
|
keepalive_state.component_id,
|
|
failure,
|
|
)
|
|
|
|
future.add_done_callback(clear_completed_keepalive)
|
|
logger.info(
|
|
"Started remote local-gap keepalive prompt=%s slot=%d component=%s duration=%.1fs interval=%.1fs.",
|
|
keepalive_key[0],
|
|
keepalive_key[1],
|
|
keepalive_state.component_id,
|
|
duration_seconds,
|
|
interval_seconds,
|
|
)
|
|
return True
|
|
|
|
|
|
def boost_mapped_component_warmup(
|
|
payload: dict[str, Any],
|
|
*,
|
|
total_items: int,
|
|
reason: str,
|
|
) -> tuple[int, int]:
|
|
"""Record exact mapped fan-out and top up prompt warmup for the resulting lane count."""
|
|
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=reason,
|
|
)
|
|
logger.info(
|
|
"Boosted exact mapped warmup for component=%s total_items=%d local_parallelism=%d prompt_warmup_target=%d reason=%s.",
|
|
payload.get("component_id"),
|
|
total_items,
|
|
parallelism,
|
|
refined_prompt_warmup_target,
|
|
reason,
|
|
)
|
|
return parallelism, refined_prompt_warmup_target
|
|
|
|
|
|
def _auto_deploy_modal_app(payload: dict[str, Any], lookup_error: BaseException) -> Any:
|
|
"""Deploy the stable Modal cloud app once and wait until deployed lookup becomes ready."""
|
|
if modal is None:
|
|
raise ModalRemoteInvocationError("Modal SDK is unavailable.")
|
|
|
|
settings = _settings_for_payload(payload)
|
|
deployment_app_name = modal_deployment_app_name(settings)
|
|
deploy_key = _modal_deploy_cache_key(payload)
|
|
deploy_state = _modal_auto_deploy_state(deploy_key)
|
|
with _modal_cloud_settings_override(settings):
|
|
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."
|
|
)
|
|
|
|
while True:
|
|
with deploy_state.condition:
|
|
if (
|
|
not _is_missing_modal_deployment_error(lookup_error)
|
|
and deploy_state.ready
|
|
):
|
|
logger.info(
|
|
"Auto-deploy already completed for app=%s env=%s; reusing cached deployment state.",
|
|
deployment_app_name,
|
|
deploy_key[1] or "<default>",
|
|
)
|
|
return _lookup_deployed_remote_engine(payload)
|
|
if _is_missing_modal_deployment_error(lookup_error) and deploy_state.ready:
|
|
logger.warning(
|
|
"Discarding stale auto-deploy ready state for app=%s env=%s after missing deployment lookup failure: %s",
|
|
deployment_app_name,
|
|
deploy_key[1] or "<default>",
|
|
lookup_error,
|
|
)
|
|
deploy_state.ready = False
|
|
if deploy_state.deploy_in_progress:
|
|
logger.info(
|
|
"Waiting for in-flight auto-deploy readiness for app=%s env=%s component=%s.",
|
|
deployment_app_name,
|
|
deploy_key[1] or "<default>",
|
|
payload.get("component_id"),
|
|
)
|
|
_emit_local_remote_startup_status(
|
|
payload,
|
|
phase="setup",
|
|
status_message="Waiting for Modal app rebuild",
|
|
)
|
|
deploy_state.condition.wait()
|
|
if deploy_state.ready:
|
|
_emit_local_remote_dispatch_status(payload)
|
|
return _lookup_deployed_remote_engine(payload)
|
|
if deploy_state.last_error is not None:
|
|
logger.warning(
|
|
"Retrying Modal auto-deploy for app=%s env=%s after previous readiness failure: %s",
|
|
deployment_app_name,
|
|
deploy_key[1] or "<default>",
|
|
deploy_state.last_error,
|
|
)
|
|
continue
|
|
deploy_state.deploy_in_progress = True
|
|
deploy_state.last_error = None
|
|
break
|
|
|
|
_emit_local_remote_startup_status(
|
|
payload,
|
|
phase="setup",
|
|
status_message="Rebuilding Modal app",
|
|
)
|
|
try:
|
|
logger.warning(
|
|
"Deployed Modal app lookup failed for app=%s component=%s: %s. "
|
|
"Attempting first-run auto-deploy from the custom node.",
|
|
deployment_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=deployment_app_name,
|
|
environment_name=deploy_key[1],
|
|
)
|
|
logger.info(
|
|
"Auto-deployed Modal app %s for env=%s in %.3fs; waiting for deployed lookup readiness.",
|
|
deployment_app_name,
|
|
deploy_key[1] or "<default>",
|
|
time.perf_counter() - deploy_started_at,
|
|
)
|
|
_ensure_llm_profiles_staged(payload, deployment_app_name)
|
|
remote_engine = _lookup_deployed_remote_engine_with_retry(payload)
|
|
if not _is_remote_engine_runtime_current(remote_engine, payload):
|
|
raise ModalRemoteInvocationError(
|
|
f"Auto-deployed Modal app {deployment_app_name!r} did not report expected protocol "
|
|
f"{_REMOTE_APP_PROTOCOL_VERSION} and fingerprint "
|
|
f"{_expected_remote_runtime_fingerprint(payload)!r}."
|
|
)
|
|
except BaseException as exc:
|
|
with deploy_state.condition:
|
|
deploy_state.deploy_in_progress = False
|
|
deploy_state.last_error = exc
|
|
deploy_state.condition.notify_all()
|
|
raise
|
|
|
|
with deploy_state.condition:
|
|
deploy_state.deploy_in_progress = False
|
|
deploy_state.ready = True
|
|
deploy_state.last_error = None
|
|
deploy_state.condition.notify_all()
|
|
with _MODAL_AUTO_DEPLOY_LOCK:
|
|
_MODAL_REMOTE_APP_VERSION_OK.add(_modal_runtime_cache_key(payload))
|
|
logger.info(
|
|
"Deployed Modal app %s for env=%s is now lookup-ready.",
|
|
deployment_app_name,
|
|
deploy_key[1] or "<default>",
|
|
)
|
|
if _schedule_post_deploy_runtime_seed(payload):
|
|
_await_matching_speculative_prewarm(payload, None)
|
|
_emit_local_remote_dispatch_status(payload)
|
|
return remote_engine
|
|
|
|
|
|
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."""
|
|
_install_modal_cloud_exception_compatibility_module()
|
|
if cancellation_event is not None and cancellation_event.is_set():
|
|
_request_remote_interrupt(payload)
|
|
raise ModalRemoteInvocationError(
|
|
"Remote Modal payload dispatch was cancelled before the remote call started."
|
|
)
|
|
|
|
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 _fresh_llm_memory_recovery_affinity_key(payload: Mapping[str, Any]) -> str:
|
|
"""Return a one-use affinity identity that cannot select the dirty worker."""
|
|
return (
|
|
f"{_remote_worker_affinity_key(dict(payload))}:llm-memory-recovery:"
|
|
f"{uuid.uuid4().hex[:12]}"
|
|
)
|
|
|
|
|
|
def _retry_exhausted_llm_memory_on_fresh_worker(
|
|
*,
|
|
payload: dict[str, Any],
|
|
kwargs_payload: bytes,
|
|
cancellation_event: threading.Event | None,
|
|
error: BaseException,
|
|
) -> bytes:
|
|
"""Retry one exhausted post-eviction admission on a distinct Modal worker."""
|
|
recovery_payload = dict(payload)
|
|
if exhausted_recovery_used_vllm_throughput(error):
|
|
recovery_payload[LLM_FORCE_VLLM_THROUGHPUT_PAYLOAD_KEY] = True
|
|
recovery_affinity_key = _fresh_llm_memory_recovery_affinity_key(payload)
|
|
logger.warning(
|
|
"Modal LLM memory recovery timed out for component=%s; retrying "
|
|
"invocation_id=%s once on fresh worker_affinity=%s.",
|
|
payload.get("component_id"),
|
|
payload["invocation_id"],
|
|
recovery_affinity_key,
|
|
)
|
|
_emit_local_remote_startup_status(
|
|
recovery_payload,
|
|
phase="starting",
|
|
status_message="Retrying LLM on a fresh Modal worker",
|
|
)
|
|
recovered_remote_engine = _lookup_deployed_remote_engine(
|
|
recovery_payload,
|
|
affinity_key_override=recovery_affinity_key,
|
|
)
|
|
return _invoke_remote_engine_payload(
|
|
recovered_remote_engine,
|
|
recovery_payload,
|
|
kwargs_payload,
|
|
cancellation_event,
|
|
)
|
|
|
|
|
|
def _invoke_remote_engine_payload_with_recovery(
|
|
remote_engine: Any,
|
|
payload: dict[str, Any],
|
|
kwargs_payload: bytes,
|
|
cancellation_event: threading.Event | None,
|
|
) -> bytes:
|
|
"""Recover missing deployments and exhausted LLM workers with one safe retry."""
|
|
payload = dict(payload)
|
|
settings = _settings_for_payload(payload)
|
|
if llm_model_references_from_payload(payload):
|
|
kwargs_payload = _rewrite_staged_llm_kwargs_payload(
|
|
kwargs_payload,
|
|
modal_deployment_app_name(settings),
|
|
)
|
|
payload.setdefault("capture_remote_outputs", True)
|
|
payload.setdefault(
|
|
"invocation_id",
|
|
stable_remote_invocation_id(payload, kwargs_payload),
|
|
)
|
|
lookup_error_types = _modal_lookup_error_types()
|
|
invocation_error_types = tuple(
|
|
dict.fromkeys((*lookup_error_types, RemoteSubgraphExecutionError, RuntimeError))
|
|
)
|
|
try:
|
|
response = _invoke_remote_engine_payload(
|
|
remote_engine,
|
|
payload,
|
|
kwargs_payload,
|
|
cancellation_event,
|
|
)
|
|
except invocation_error_types as exc:
|
|
if is_llm_memory_recovery_exhausted(exc):
|
|
response = _retry_exhausted_llm_memory_on_fresh_worker(
|
|
payload=payload,
|
|
kwargs_payload=kwargs_payload,
|
|
cancellation_event=cancellation_event,
|
|
error=exc,
|
|
)
|
|
elif not isinstance(exc, lookup_error_types):
|
|
raise
|
|
elif not settings.auto_deploy or not _is_missing_modal_deployment_error(exc):
|
|
raise
|
|
else:
|
|
logger.warning(
|
|
"Modal payload invocation failed for component=%s because the deployed app was missing at call time: %s. Recreating the app and retrying.",
|
|
payload.get("component_id"),
|
|
exc,
|
|
)
|
|
recovered_remote_engine = _auto_deploy_modal_app(payload, exc)
|
|
_ensure_llm_profiles_staged(
|
|
payload,
|
|
modal_deployment_app_name(settings),
|
|
)
|
|
response = _invoke_remote_engine_payload(
|
|
recovered_remote_engine,
|
|
payload,
|
|
kwargs_payload,
|
|
cancellation_event,
|
|
)
|
|
return _materialize_remote_execution_result(response, settings=settings)
|
|
|
|
|
|
def _local_comfy_output_directory(settings: ModalSyncSettings) -> Path:
|
|
"""Return the effective output directory of the local ComfyUI process."""
|
|
try:
|
|
import folder_paths
|
|
except ModuleNotFoundError as exc:
|
|
if exc.name != "folder_paths":
|
|
raise
|
|
else:
|
|
get_output_directory = getattr(folder_paths, "get_output_directory", None)
|
|
if callable(get_output_directory):
|
|
return Path(get_output_directory()).expanduser().resolve()
|
|
if settings.comfyui_root is None:
|
|
raise ModalRemoteInvocationError(
|
|
"Modal returned ComfyUI output files, but the local ComfyUI output directory "
|
|
"could not be resolved. Set COMFY_MODAL_COMFYUI_ROOT."
|
|
)
|
|
return (settings.comfyui_root / "output").resolve()
|
|
|
|
|
|
def _materialize_remote_execution_result(
|
|
response: bytes | bytearray,
|
|
*,
|
|
settings: ModalSyncSettings | None = None,
|
|
) -> bytes:
|
|
"""Download bundled remote files and return the ordinary serialized node outputs."""
|
|
result: RemoteExecutionResult = unpack_remote_execution_result(response)
|
|
if not result.artifacts:
|
|
return result.outputs
|
|
resolved_settings = settings or get_settings()
|
|
output_directory = _local_comfy_output_directory(resolved_settings)
|
|
logger.info(
|
|
"Materializing %d remote ComfyUI output file(s) into %s.",
|
|
len(result.artifacts),
|
|
output_directory,
|
|
)
|
|
materialized_paths = materialize_remote_output_artifacts(
|
|
result,
|
|
output_directory=output_directory,
|
|
app_name=modal_deployment_app_name(resolved_settings),
|
|
)
|
|
logger.info(
|
|
"Finished downloading %d remote ComfyUI output file(s): %s.",
|
|
len(materialized_paths),
|
|
[str(path) for path in materialized_paths],
|
|
)
|
|
return result.outputs
|
|
|
|
|
|
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.")
|
|
|
|
_await_matching_speculative_prewarm(payload, cancellation_event)
|
|
lookup_error_types = _modal_lookup_error_types()
|
|
settings = _settings_for_payload(payload)
|
|
deployment_app_name = modal_deployment_app_name(settings)
|
|
if lookup_error_types:
|
|
try:
|
|
remote_engine = _lookup_protocol_current_remote_engine(payload)
|
|
_ensure_llm_profiles_staged(payload, deployment_app_name)
|
|
logger.info(
|
|
"Using deployed Modal app %s for component %s.",
|
|
deployment_app_name,
|
|
payload.get("component_id"),
|
|
)
|
|
return _invoke_remote_engine_payload_with_recovery(
|
|
remote_engine,
|
|
payload,
|
|
kwargs_payload,
|
|
cancellation_event,
|
|
)
|
|
except lookup_error_types as exc:
|
|
missing_deployment = _is_missing_modal_deployment_error(exc)
|
|
if settings.auto_deploy and missing_deployment:
|
|
remote_engine = _auto_deploy_modal_app(payload, exc)
|
|
try:
|
|
_ensure_llm_profiles_staged(payload, deployment_app_name)
|
|
logger.info(
|
|
"Using auto-deployed Modal app %s for component %s.",
|
|
deployment_app_name,
|
|
payload.get("component_id"),
|
|
)
|
|
return _invoke_remote_engine_payload_with_recovery(
|
|
remote_engine,
|
|
payload,
|
|
kwargs_payload,
|
|
cancellation_event,
|
|
)
|
|
except lookup_error_types as retry_exc:
|
|
exc = retry_exc
|
|
missing_deployment = _is_missing_modal_deployment_error(exc)
|
|
if not missing_deployment:
|
|
raise
|
|
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={deployment_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.",
|
|
deployment_app_name,
|
|
payload.get("component_id"),
|
|
exc,
|
|
)
|
|
else:
|
|
remote_engine = _lookup_protocol_current_remote_engine(payload)
|
|
_ensure_llm_profiles_staged(payload, deployment_app_name)
|
|
logger.info(
|
|
"Using deployed Modal app %s for component %s.",
|
|
deployment_app_name,
|
|
payload.get("component_id"),
|
|
)
|
|
return _invoke_remote_engine_payload_with_recovery(
|
|
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."
|
|
)
|
|
|
|
with _modal_cloud_settings_override(settings):
|
|
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_with_recovery(
|
|
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,
|
|
*,
|
|
allow_implicit_mapping: bool = True,
|
|
) -> bytes:
|
|
"""Invoke Modal when configured, or fall back to local in-process execution."""
|
|
execution_mode = get_settings().execution_mode
|
|
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 allow_implicit_mapping and 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"),
|
|
)
|
|
_emit_local_remote_dispatch_status(payload)
|
|
cancellation_event = threading.Event()
|
|
future = _REMOTE_MODAL_CALL_EXECUTOR.submit(
|
|
_invoke_modal_payload_blocking,
|
|
dict(payload),
|
|
kwargs_payload,
|
|
cancellation_event,
|
|
)
|
|
interrupt_sent = False
|
|
cancellation_started_at: float | None = None
|
|
try:
|
|
while True:
|
|
try:
|
|
response = future.result(timeout=0.1)
|
|
break
|
|
except FutureTimeoutError:
|
|
(
|
|
interrupt_sent,
|
|
cancellation_started_at,
|
|
) = _handle_modal_wait_cancellation(
|
|
payload,
|
|
cancellation_event,
|
|
interrupt_sent=interrupt_sent,
|
|
cancellation_started_at=cancellation_started_at,
|
|
)
|
|
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,
|
|
*,
|
|
allow_implicit_mapping: bool = True,
|
|
) -> bytes:
|
|
"""Invoke Modal asynchronously so multiple proxy nodes can wait on remote work in parallel."""
|
|
execution_mode = get_settings().execution_mode
|
|
if (
|
|
execution_mode != "local"
|
|
and modal is not None
|
|
and bool(payload.get("stop_local_gap_keepalive_before_remote_component"))
|
|
):
|
|
_stop_local_gap_keepalive(payload, reason="next_remote_component_started")
|
|
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 allow_implicit_mapping and payload.get("payload_kind") == "subgraph":
|
|
hydrated_inputs = deserialize_node_inputs(kwargs_payload)
|
|
if _split_batch_boundary_inputs(payload, hydrated_inputs) is not None:
|
|
if execution_mode != "local" and modal is not None:
|
|
await asyncio.to_thread(
|
|
_lookup_protocol_current_remote_engine,
|
|
payload,
|
|
)
|
|
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,
|
|
allow_implicit_mapping=allow_implicit_mapping,
|
|
)
|
|
|
|
logger.info(
|
|
"Dispatching async Modal remote invocation for component=%s payload_kind=%s.",
|
|
payload.get("component_id"),
|
|
payload.get("payload_kind"),
|
|
)
|
|
_emit_local_remote_dispatch_status(payload)
|
|
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)
|
|
interrupt_sent = False
|
|
cancellation_started_at: float | None = None
|
|
try:
|
|
while True:
|
|
try:
|
|
response = await asyncio.wait_for(
|
|
asyncio.shield(wrapped_future), timeout=0.1
|
|
)
|
|
break
|
|
except asyncio.TimeoutError:
|
|
(
|
|
interrupt_sent,
|
|
cancellation_started_at,
|
|
) = await _handle_modal_wait_cancellation_async(
|
|
payload,
|
|
cancellation_event,
|
|
interrupt_sent=interrupt_sent,
|
|
cancellation_started_at=cancellation_started_at,
|
|
)
|
|
continue
|
|
except asyncio.CancelledError:
|
|
cancellation_event.set()
|
|
prompt_id = (
|
|
str(payload.get("prompt_id"))
|
|
if payload.get("prompt_id") is not None
|
|
else None
|
|
)
|
|
if prompt_id is not None:
|
|
await request_remote_modal_prompt_interrupt_async(prompt_id)
|
|
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"),
|
|
)
|
|
_start_local_gap_keepalive(payload)
|
|
return response
|
|
|
|
|
|
if modal is not None: # pragma: no branch - simple import-time configuration.
|
|
settings = get_settings()
|
|
app = modal.App(modal_deployment_app_name(settings))
|
|
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=settings.modal_gpu,
|
|
volumes={settings.remote_storage_root: vol},
|
|
scaledown_window=60,
|
|
image=image,
|
|
)
|
|
class RemoteEngine:
|
|
"""Modal runtime class that executes proxied ComfyUI payloads."""
|
|
|
|
snapshot_profile_key: str = modal.parameter(default="")
|
|
gpu_snapshot_enabled: bool = modal.parameter(default=False)
|
|
worker_affinity_key: str = modal.parameter(default="worker-pool:slot:0")
|
|
|
|
@modal.enter()
|
|
def setup(self) -> None:
|
|
"""Prepare the container process for headless node execution."""
|
|
logger.info(
|
|
"RemoteEngine setup complete for snapshot_profile_key=%s gpu_snapshot_enabled=%s.",
|
|
self.snapshot_profile_key or None,
|
|
bool(self.gpu_snapshot_enabled),
|
|
)
|
|
|
|
@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")}
|
|
|
|
@modal.method()
|
|
def keepalive_for_local_gap(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
"""Return a lightweight keepalive acknowledgement for one affinity slot."""
|
|
return {
|
|
"component_id": str(payload.get("component_id") or "modal-keepalive"),
|
|
"worker_affinity_key": self.worker_affinity_key,
|
|
}
|
|
|
|
else:
|
|
|
|
class RemoteEngine:
|
|
"""Local fallback runtime used when the Modal SDK is unavailable."""
|
|
|
|
def __init__(
|
|
self,
|
|
snapshot_profile_key: str | None = None,
|
|
gpu_snapshot_enabled: bool = False,
|
|
worker_affinity_key: str = "worker-pool:slot:0",
|
|
) -> None:
|
|
"""Record the optional snapshot-profile key and GPU snapshot mode."""
|
|
self.snapshot_profile_key = snapshot_profile_key
|
|
self.gpu_snapshot_enabled = gpu_snapshot_enabled
|
|
self.worker_affinity_key = worker_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")}
|
|
|
|
def keepalive_for_local_gap(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
"""Return a local keepalive acknowledgement when Modal is unavailable."""
|
|
return {
|
|
"component_id": str(payload.get("component_id") or "modal-keepalive"),
|
|
"worker_affinity_key": self.worker_affinity_key,
|
|
}
|