Files
ttulttul-ComfyUI-Modal/settings.py
T
Ken SimpsonandClaude Opus 5.5 218adb848d Default COMFY_MODAL_EXECUTION_MODE to remote
Local mode runs rewritten components inside the local ComfyUI process and
exists only for developing the extension, yet it was the default, so users
who did not set the variable silently ran in it. settings now owns the
default (remote) through configured_execution_mode(), and every host-side
reader uses it. The Modal image environment pins local explicitly, as SSH
and Vast workers already do, so container code keeps its in-process
behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:20:40 -07:00

744 lines
27 KiB
Python

"""Runtime configuration helpers for ComfyUI Modal-Sync."""
from __future__ import annotations
import hashlib
import importlib
import logging
import os
import re
from dataclasses import dataclass, replace
from functools import lru_cache
from pathlib import Path
from typing import Any, Mapping
if __package__:
from .instance_identity import (
INSTANCE_ID_FILENAME,
load_or_create_instance_id,
modal_app_name_for_instance,
)
else: # pragma: no cover - the stable cloud entrypoint imports this module top-level.
from instance_identity import (
INSTANCE_ID_FILENAME,
load_or_create_instance_id,
modal_app_name_for_instance,
)
logger = logging.getLogger(__name__)
MODAL_GPU_TYPES = (
"T4",
"L4",
"A10",
"L40S",
"A100",
"A100-40GB",
"A100-80GB",
"RTX-PRO-6000",
"H100",
"H100!",
"H200",
"B200",
"B200+",
"B300",
)
WORKFLOW_MODAL_CONFIG_KEY = "comfy_modal"
WORKFLOW_MODAL_GPU_KEY = "gpu"
DEFAULT_MODAL_GPU = "RTX-PRO-6000"
DEFAULT_MODAL_SECRET_NAME = "comfy"
_LEGACY_BASE_APP_GPU = "A100"
_MODAL_APP_NAME_MAX_LENGTH = 64
_MODAL_GPU_SLUG_MAX_LENGTH = 28
_SETTINGS_ENV_KEYS = (
"COMFYUI_ROOT",
"COMFY_MODAL_COMFYUI_ROOT",
"COMFY_MODAL_CUSTOM_NODES_DIR",
"COMFY_MODAL_EXECUTION_MODE",
"COMFY_MODAL_SECRET_NAME",
"COMFY_MODAL_SYNC_CUSTOM_NODES",
"COMFY_MODAL_LOCAL_STORAGE_ROOT",
"COMFY_MODAL_APP_NAME",
"COMFY_MODAL_INSTANCE_ID_PATH",
"COMFY_MODAL_AUTO_DEPLOY",
"COMFY_MODAL_ALLOW_EPHEMERAL_FALLBACK",
"COMFY_MODAL_ENABLE_MEMORY_SNAPSHOT",
"COMFY_MODAL_ENABLE_GPU_MEMORY_SNAPSHOT",
"COMFY_MODAL_VOLUME_NAME",
"COMFY_MODAL_ROUTE_PATH",
"COMFY_MODAL_MARKER_PROPERTY",
"COMFY_MODAL_REMOTE_STORAGE_ROOT",
"COMFY_MODAL_CUSTOM_NODES_ARCHIVE",
"COMFY_MODAL_INTERRUPT_DICT_NAME",
"COMFY_MODAL_NODE_CACHE_DICT_NAME",
"COMFY_MODAL_SESSION_BRIDGE_DICT_NAME",
"COMFY_MODAL_INVOCATION_DICT_NAME",
"COMFY_MODAL_SYNC_INDEX_DICT_NAME",
"COMFY_MODAL_SNAPSHOT_PROFILE_DICT_NAME",
"COMFY_MODAL_NODE_CACHE_MAX_BYTES",
"COMFY_MODAL_BRIDGE_INLINE_MAX_BYTES",
"COMFY_MODAL_INVOCATION_RESULT_INLINE_MAX_BYTES",
"COMFY_MODAL_TERMINATE_CONTAINER_ON_ERROR",
"COMFY_MODAL_GPU",
"COMFY_MODAL_MIN_CONTAINERS",
"COMFY_MODAL_MAX_CONTAINERS",
"COMFY_MODAL_BUFFER_CONTAINERS",
"COMFY_MODAL_MAX_INFLIGHT_CALLS",
"COMFY_MODAL_SCALEDOWN_WINDOW",
"COMFY_MODAL_LOCAL_GAP_KEEPALIVE_SECONDS",
"COMFY_MODAL_LOCAL_GAP_KEEPALIVE_INTERVAL_SECONDS",
"COMFY_MODAL_EXECUTION_TIMEOUT_SECONDS",
"COMFY_MODAL_STARTUP_TIMEOUT_SECONDS",
"COMFY_MODAL_ENABLE_LOADER_PREWARM",
"COMFY_MODAL_LOADER_PREWARM_WORKERS",
"COMFY_MODAL_MAX_LOADER_PREWARMS_PER_COMPONENT",
"COMFY_MODAL_ENABLE_PROACTIVE_WARMUP",
"COMFY_MODAL_PROACTIVE_WARMUP_HEAD_START_SECONDS",
"COMFY_MODAL_REMOTE_CANCEL_GRACE_SECONDS",
"COMFY_MODAL_REMOTE_CANCEL_RESTART_SECONDS",
"COMFY_MODAL_STREAM_EVENT_QUEUE_MAXSIZE",
"COMFY_MODAL_STREAM_REMOTE_CONTAINER_LOGS",
"COMFY_MODAL_LLM_MAX_RESIDENT_MODELS",
"COMFY_MODAL_LLM_RESERVE_FREE_GB",
"COMFY_MODAL_LLM_COMPILE_CACHE_VOLUME_NAME",
"COMFY_MODAL_LLM_VLLM_EXECUTION_MODE",
"COMFY_MODAL_LLM_MEMORY_RECOVERY_TIMEOUT_SECONDS",
)
VLLM_EXECUTION_MODES = ("auto", "eager", "throughput")
@dataclass(frozen=True)
class ModalSyncSettings:
"""Resolved configuration for local and remote Modal-Sync operations."""
app_name: str
auto_deploy: bool
allow_ephemeral_fallback: bool
enable_memory_snapshot: bool
enable_gpu_memory_snapshot: bool
execution_mode: str
sync_custom_nodes: bool
volume_name: str
route_path: str
marker_property: str
local_storage_root: Path
remote_storage_root: str
custom_nodes_archive_name: str
comfyui_root: Path | None
custom_nodes_dir: Path | None
modal_secret_name: str = DEFAULT_MODAL_SECRET_NAME
interrupt_dict_name: str = "comfy-modal-sync-interrupts"
node_output_cache_dict_name: str = "comfy-modal-sync-node-cache"
session_bridge_dict_name: str = "comfy-modal-sync-session-bridges"
invocation_dict_name: str = "comfy-modal-sync-invocations"
sync_index_dict_name: str = "comfy-modal-sync-sync-index"
snapshot_profile_dict_name: str = "comfy-modal-sync-snapshot-profiles"
node_output_cache_max_bytes: int = 5 * 1024 * 1024
bridge_inline_max_bytes: int = 4 * 1024 * 1024
invocation_result_inline_max_bytes: int = 4 * 1024 * 1024
terminate_container_on_error: bool = True
modal_gpu: str = DEFAULT_MODAL_GPU
scaledown_window_seconds: int = 600
local_gap_keepalive_seconds: float = 900.0
local_gap_keepalive_interval_seconds: float = 15.0
min_containers: int = 0
max_containers: int | None = None
buffer_containers: int | None = None
max_inflight_calls: int = 4
execution_timeout_seconds: int = 3600
startup_timeout_seconds: int = 900
enable_proactive_warmup: bool = True
enable_loader_prewarm: bool = True
loader_prewarm_workers: int = 2
proactive_warmup_head_start_seconds: float = 2.0
remote_cancel_grace_seconds: float = 2.0
remote_cancel_restart_seconds: float = 1.0
stream_event_queue_maxsize: int = 256
stream_remote_container_logs: bool = False
llm_max_resident_models: int = 2
llm_reserve_free_vram_gb: float = 24.0
llm_compile_cache_volume_name: str = "comfy-universal-storage-llm-compile-cache"
llm_vllm_execution_mode: str = "auto"
llm_memory_recovery_timeout_seconds: float = 15.0
def normalize_vllm_execution_mode(value: object) -> str:
"""Return one supported vLLM execution profile name."""
normalized = str(value).strip().lower()
if normalized not in VLLM_EXECUTION_MODES:
supported_values = ", ".join(VLLM_EXECUTION_MODES)
raise ValueError(
f"Unsupported vLLM execution mode {value!r}. "
f"Choose one of: {supported_values}."
)
return normalized
def normalize_modal_gpu_selection(value: object) -> str:
"""Return one supported workflow-level Modal GPU selection."""
if not isinstance(value, str):
raise ValueError("The workflow Modal GPU selection must be a string.")
normalized = value.strip().upper()
if normalized not in MODAL_GPU_TYPES:
supported_values = ", ".join(MODAL_GPU_TYPES)
raise ValueError(
f"Unsupported workflow Modal GPU selection {value!r}. "
f"Choose one of: {supported_values}."
)
return normalized
def modal_gpu_from_workflow(
workflow: object,
default_gpu: str,
) -> str:
"""Resolve the saved workflow GPU selection or return the configured fallback."""
if not isinstance(workflow, dict):
return default_gpu
extra = workflow.get("extra")
if not isinstance(extra, dict):
return default_gpu
modal_config = extra.get(WORKFLOW_MODAL_CONFIG_KEY)
if not isinstance(modal_config, dict) or WORKFLOW_MODAL_GPU_KEY not in modal_config:
return default_gpu
return normalize_modal_gpu_selection(modal_config[WORKFLOW_MODAL_GPU_KEY])
def settings_for_modal_gpu(
settings: ModalSyncSettings,
modal_gpu: object,
) -> ModalSyncSettings:
"""Return settings with one validated workflow-level GPU override."""
return replace(settings, modal_gpu=normalize_modal_gpu_selection(modal_gpu))
def _modal_gpu_app_slug(modal_gpu: str) -> str:
"""Return a readable, collision-resistant app-name suffix for one GPU target."""
expanded = (
modal_gpu.strip()
.lower()
.replace("!", "-priority")
.replace("+", "-plus")
.replace(":", "-x")
.replace(",", "-or-")
)
slug = "-".join(re.findall(r"[a-z0-9]+", expanded))
if not slug:
slug = f"target-{hashlib.sha256(modal_gpu.encode('utf-8')).hexdigest()[:8]}"
if len(slug) <= _MODAL_GPU_SLUG_MAX_LENGTH:
return slug
digest = hashlib.sha256(modal_gpu.encode("utf-8")).hexdigest()[:8]
prefix_length = _MODAL_GPU_SLUG_MAX_LENGTH - len(digest) - 1
return f"{slug[:prefix_length].rstrip('-')}-{digest}"
def modal_deployment_app_name(settings: ModalSyncSettings) -> str:
"""Return the persistent Modal app name dedicated to the configured GPU target."""
modal_gpu = str(getattr(settings, "modal_gpu", DEFAULT_MODAL_GPU)).strip()
if modal_gpu.upper() == _LEGACY_BASE_APP_GPU:
return settings.app_name
suffix = f"-gpu-{_modal_gpu_app_slug(modal_gpu)}"
candidate = f"{settings.app_name}{suffix}"
if len(candidate) <= _MODAL_APP_NAME_MAX_LENGTH:
return candidate
digest = hashlib.sha256(settings.app_name.encode("utf-8")).hexdigest()[:8]
prefix_length = _MODAL_APP_NAME_MAX_LENGTH - len(suffix) - len(digest) - 1
prefix = settings.app_name[:prefix_length].rstrip("-_.") or "app"
return f"{prefix}-{digest}{suffix}"
MODAL_DEPLOYMENT_PAYLOAD_FIELDS = ("modal_gpu", "modal_max_containers")
"""Payload fields that select deployment settings and so shape the runtime fingerprint.
Any payload derived from another (warmups, prewarm targets, mapped phases) must
copy these, or it will expect a different fingerprint for the same Modal app.
"""
MODAL_GLOBAL_COOLDOWN_FIELD = "modal_global_cooldown_seconds"
MODAL_RESERVE_COOLDOWN_COUNT_FIELD = "modal_reserve_cooldown_count"
MODAL_RESERVE_COOLDOWN_FIELD = "modal_reserve_cooldown_seconds"
MODAL_RESERVE_COOLDOWN_POOL_FIELD = "modal_reserve_cooldown_pool"
MODAL_COOLDOWN_PAYLOAD_FIELDS = (
MODAL_GLOBAL_COOLDOWN_FIELD,
MODAL_RESERVE_COOLDOWN_COUNT_FIELD,
MODAL_RESERVE_COOLDOWN_FIELD,
MODAL_RESERVE_COOLDOWN_POOL_FIELD,
)
"""Payload fields carrying the workflow's per-slot Modal cooldown policy.
They are applied per call through ``with_options`` and never change the
deployed app's fingerprint, but derived payloads must still inherit them.
"""
MODAL_INHERITED_PAYLOAD_FIELDS = (
*MODAL_DEPLOYMENT_PAYLOAD_FIELDS,
*MODAL_COOLDOWN_PAYLOAD_FIELDS,
)
MODAL_WORKER_POOL_MAPPED = "mapped"
MODAL_WORKER_POOL_COMFY = "comfy"
MODAL_WORKER_POOL_LLM = "llm"
MODAL_WORKER_POOLS = (
MODAL_WORKER_POOL_MAPPED,
MODAL_WORKER_POOL_COMFY,
MODAL_WORKER_POOL_LLM,
)
"""Independent Modal worker-slot namespaces.
Mapped lanes use ``worker-pool:slot:N``; ordinary components use
``worker-pool:<comfy|llm>:slot:N``. Slot N in different pools is a different
container, so the reserve cooldown must target exactly one pool per prompt.
"""
MODAL_COOLDOWN_MIN_SECONDS = 2
MODAL_COOLDOWN_MAX_SECONDS = 3600
DEFAULT_MODAL_GLOBAL_COOLDOWN_SECONDS = 60
DEFAULT_MODAL_RESERVE_COOLDOWN_COUNT = 0
DEFAULT_MODAL_RESERVE_COOLDOWN_SECONDS = 300
def modal_deployment_payload_fields(payload: Mapping[str, Any]) -> dict[str, Any]:
"""Return the deployment-identity and cooldown fields present on one payload."""
return {
field_name: payload[field_name]
for field_name in MODAL_INHERITED_PAYLOAD_FIELDS
if payload.get(field_name) is not None
}
def _validate_cooldown_seconds(name: str, value: int) -> None:
"""Reject a cooldown that Modal's autoscaler would not accept."""
if not MODAL_COOLDOWN_MIN_SECONDS <= value <= MODAL_COOLDOWN_MAX_SECONDS:
raise ValueError(
f"Modal {name} must be between {MODAL_COOLDOWN_MIN_SECONDS} and "
f"{MODAL_COOLDOWN_MAX_SECONDS} seconds; got {value}."
)
@dataclass(frozen=True)
class ModalCooldownPolicy:
"""Assign Modal idle cooldowns to reusable worker-pool slots.
Slots ``0 .. reserve_cooldown_count - 1`` keep the longer reserve cooldown so
a small set of workers stays warm; every other slot uses the global cooldown.
"""
global_cooldown_seconds: int = DEFAULT_MODAL_GLOBAL_COOLDOWN_SECONDS
reserve_cooldown_count: int = DEFAULT_MODAL_RESERVE_COOLDOWN_COUNT
reserve_cooldown_seconds: int = DEFAULT_MODAL_RESERVE_COOLDOWN_SECONDS
def __post_init__(self) -> None:
"""Validate cooldown bounds and the reserved slot count."""
_validate_cooldown_seconds("global_cooldown", self.global_cooldown_seconds)
_validate_cooldown_seconds("reserve_cooldown", self.reserve_cooldown_seconds)
if self.reserve_cooldown_count < 0:
raise ValueError("Modal reserve_cooldown_count must not be negative.")
def scaledown_window_for_worker(
self,
*,
worker_pool: str | None,
slot_index: int | None,
reserve_pool: str,
) -> int:
"""Return one worker's idle cooldown.
Only the first ``reserve_cooldown_count`` slots of ``reserve_pool`` keep the
reserve cooldown; every other worker, including unslotted ones, is global.
"""
if (
worker_pool == reserve_pool
and slot_index is not None
and 0 <= slot_index < self.reserve_cooldown_count
):
return self.reserve_cooldown_seconds
return self.global_cooldown_seconds
def to_payload_fields(self) -> dict[str, int]:
"""Return the payload fields that carry this policy to remote dispatch."""
return {
MODAL_GLOBAL_COOLDOWN_FIELD: self.global_cooldown_seconds,
MODAL_RESERVE_COOLDOWN_COUNT_FIELD: self.reserve_cooldown_count,
MODAL_RESERVE_COOLDOWN_FIELD: self.reserve_cooldown_seconds,
}
@classmethod
def from_payload(cls, payload: Mapping[str, Any]) -> ModalCooldownPolicy | None:
"""Return the policy carried by a payload, or None when it carries none."""
raw_global = payload.get(MODAL_GLOBAL_COOLDOWN_FIELD)
if raw_global is None:
return None
return cls(
global_cooldown_seconds=int(raw_global),
reserve_cooldown_count=int(
payload.get(
MODAL_RESERVE_COOLDOWN_COUNT_FIELD,
DEFAULT_MODAL_RESERVE_COOLDOWN_COUNT,
)
),
reserve_cooldown_seconds=int(
payload.get(
MODAL_RESERVE_COOLDOWN_FIELD,
DEFAULT_MODAL_RESERVE_COOLDOWN_SECONDS,
)
),
)
def _read_path_env(name: str) -> Path | None:
"""Resolve an environment variable into a path when present."""
value = os.getenv(name)
if not value:
return None
return Path(value).expanduser().resolve()
def _read_bool_env(name: str) -> bool | None:
"""Resolve an environment variable into a boolean when present."""
value = os.getenv(name)
if value is None:
return None
normalized = value.strip().lower()
if normalized in {"1", "true", "yes", "on"}:
return True
if normalized in {"0", "false", "no", "off"}:
return False
raise ValueError(f"Environment variable {name} must be a boolean, got {value!r}.")
def _discover_repo_root() -> Path:
"""Return the repository root containing this module."""
return Path(__file__).resolve().parent
def _read_int_env(name: str, default: int) -> int:
"""Resolve an environment variable into an integer when present."""
value = os.getenv(name)
if value is None:
return default
try:
return int(value)
except ValueError as exc:
raise ValueError(f"Environment variable {name} must be an integer, got {value!r}.") from exc
def _read_optional_int_env(name: str) -> int | None:
"""Resolve an environment variable into an optional integer when present."""
value = os.getenv(name)
if value is None:
return None
try:
return int(value)
except ValueError as exc:
raise ValueError(f"Environment variable {name} must be an integer, got {value!r}.") from exc
def _read_float_env(name: str, default: float) -> float:
"""Resolve an environment variable into a float when present."""
value = os.getenv(name)
if value is None:
return default
try:
return float(value)
except ValueError as exc:
raise ValueError(f"Environment variable {name} must be a float, got {value!r}.") from exc
def _looks_like_comfyui_root(candidate: Path) -> bool:
"""Return whether a path appears to be a ComfyUI checkout root."""
return (candidate / "main.py").exists() and (candidate / "nodes.py").exists()
def _discover_comfyui_root(repo_root: Path) -> Path | None:
"""Locate the local ComfyUI checkout used for tests and path resolution."""
env_root = _read_path_env("COMFYUI_ROOT")
if env_root is not None:
return env_root
modal_env_root = _read_path_env("COMFY_MODAL_COMFYUI_ROOT")
if modal_env_root is not None:
return modal_env_root
if repo_root.parent.name == "custom_nodes":
install_root = repo_root.parent.parent.resolve()
if _looks_like_comfyui_root(install_root):
return install_root
for default_name in ("ComfyUI", "Latest_ComfyUI"):
default_root = Path.home() / "git" / default_name
if _looks_like_comfyui_root(default_root):
return default_root.resolve()
return None
def _discover_custom_nodes_dir(repo_root: Path, comfyui_root: Path | None) -> Path | None:
"""Locate the custom_nodes directory that should be mirrored to Modal."""
env_dir = _read_path_env("COMFY_MODAL_CUSTOM_NODES_DIR")
if env_dir is not None:
return env_dir
if repo_root.parent.name == "custom_nodes":
return repo_root.parent.resolve()
if comfyui_root is not None:
candidate = comfyui_root / "custom_nodes"
if candidate.exists():
return candidate.resolve()
return None
def _discover_comfyui_user_directory(comfyui_root: Path | None) -> Path | None:
"""Return ComfyUI's effective user directory when it can be resolved."""
try:
folder_paths = importlib.import_module("folder_paths")
except ModuleNotFoundError as exc:
if exc.name != "folder_paths":
logger.debug("Unable to import folder_paths while resolving the user directory: %s", exc)
else:
get_user_directory = getattr(folder_paths, "get_user_directory", None)
if callable(get_user_directory):
return Path(get_user_directory()).expanduser().resolve()
if comfyui_root is None:
return None
return (comfyui_root / "user").resolve()
def discover_comfyui_user_directory(
settings: ModalSyncSettings | None = None,
) -> Path | None:
"""Return the effective ComfyUI user directory for extension-owned state."""
resolved_settings = settings or get_settings()
return _discover_comfyui_user_directory(resolved_settings.comfyui_root)
DEFAULT_EXECUTION_MODE = "remote"
"""Execution mode used when COMFY_MODAL_EXECUTION_MODE is unset.
Remote is the default because local mode only exists for developing the extension:
it runs rewritten components inside the local ComfyUI process. Remote workers
(Modal containers, SSH and Vast workers) set ``local`` explicitly for their own
nested execution.
"""
def configured_execution_mode() -> str:
"""Return the configured execution mode, defaulting to remote when unset or blank."""
configured = (os.getenv("COMFY_MODAL_EXECUTION_MODE") or "").strip().lower()
return configured or DEFAULT_EXECUTION_MODE
def _resolve_modal_app_name(comfyui_root: Path | None, execution_mode: str) -> str:
"""Return the explicit or persistent per-ComfyUI Modal app name."""
configured_name = os.getenv("COMFY_MODAL_APP_NAME")
if configured_name is not None:
resolved_name = configured_name.strip()
if not resolved_name:
raise ValueError("COMFY_MODAL_APP_NAME must not be empty.")
return resolved_name
identity_path = _read_path_env("COMFY_MODAL_INSTANCE_ID_PATH")
if identity_path is None:
user_directory = _discover_comfyui_user_directory(comfyui_root)
if user_directory is not None:
identity_path = user_directory / INSTANCE_ID_FILENAME
if identity_path is None:
if execution_mode == "remote":
raise RuntimeError(
"Remote execution requires a persistent ComfyUI instance identity, but the "
"ComfyUI user directory could not be resolved. Set COMFY_MODAL_COMFYUI_ROOT, "
"COMFY_MODAL_INSTANCE_ID_PATH, or COMFY_MODAL_APP_NAME explicitly."
)
logger.debug("Using the legacy local app name because no ComfyUI user directory was found.")
return "comfy-modal-sync"
app_name = modal_app_name_for_instance(load_or_create_instance_id(identity_path))
logger.info("Using per-ComfyUI Modal app %s from identity file %s.", app_name, identity_path)
return app_name
def _settings_env_signature() -> tuple[tuple[str, str | None], ...]:
"""Return the environment values that affect resolved Modal-Sync settings."""
return tuple((key, os.getenv(key)) for key in _SETTINGS_ENV_KEYS)
@lru_cache(maxsize=8)
def _get_settings_cached(
env_signature: tuple[tuple[str, str | None], ...],
) -> ModalSyncSettings:
"""Return cached extension settings for one environment signature."""
del env_signature
repo_root = _discover_repo_root()
comfyui_root = _discover_comfyui_root(repo_root)
custom_nodes_dir = _discover_custom_nodes_dir(repo_root, comfyui_root)
execution_mode = configured_execution_mode()
app_name = _resolve_modal_app_name(comfyui_root, execution_mode)
sync_custom_nodes = _read_bool_env("COMFY_MODAL_SYNC_CUSTOM_NODES")
if sync_custom_nodes is None:
sync_custom_nodes = execution_mode != "local"
local_storage_root = (
_read_path_env("COMFY_MODAL_LOCAL_STORAGE_ROOT")
or Path("/tmp/comfyui-modal-sync-storage")
)
volume_name = os.getenv("COMFY_MODAL_VOLUME_NAME", "comfy-universal-storage")
settings = ModalSyncSettings(
app_name=app_name,
auto_deploy=_read_bool_env("COMFY_MODAL_AUTO_DEPLOY") is not False,
allow_ephemeral_fallback=_read_bool_env("COMFY_MODAL_ALLOW_EPHEMERAL_FALLBACK") or False,
enable_memory_snapshot=_read_bool_env("COMFY_MODAL_ENABLE_MEMORY_SNAPSHOT") is not False,
enable_gpu_memory_snapshot=_read_bool_env("COMFY_MODAL_ENABLE_GPU_MEMORY_SNAPSHOT")
is not False,
execution_mode=execution_mode,
sync_custom_nodes=sync_custom_nodes,
volume_name=volume_name,
route_path=os.getenv("COMFY_MODAL_ROUTE_PATH", "/modal/queue_prompt"),
marker_property=os.getenv("COMFY_MODAL_MARKER_PROPERTY", "is_modal_remote"),
local_storage_root=local_storage_root.resolve(),
remote_storage_root=os.getenv("COMFY_MODAL_REMOTE_STORAGE_ROOT", "/storage"),
custom_nodes_archive_name=os.getenv(
"COMFY_MODAL_CUSTOM_NODES_ARCHIVE",
"custom_nodes_bundle.zip",
),
comfyui_root=comfyui_root,
custom_nodes_dir=custom_nodes_dir,
modal_secret_name=(
os.getenv("COMFY_MODAL_SECRET_NAME", DEFAULT_MODAL_SECRET_NAME).strip()
or DEFAULT_MODAL_SECRET_NAME
),
interrupt_dict_name=os.getenv(
"COMFY_MODAL_INTERRUPT_DICT_NAME",
f"{app_name}-interrupts",
),
node_output_cache_dict_name=os.getenv(
"COMFY_MODAL_NODE_CACHE_DICT_NAME",
f"{app_name}-node-cache",
),
session_bridge_dict_name=os.getenv(
"COMFY_MODAL_SESSION_BRIDGE_DICT_NAME",
f"{app_name}-session-bridges",
),
invocation_dict_name=os.getenv(
"COMFY_MODAL_INVOCATION_DICT_NAME",
f"{app_name}-invocations",
),
snapshot_profile_dict_name=os.getenv(
"COMFY_MODAL_SNAPSHOT_PROFILE_DICT_NAME",
f"{app_name}-snapshot-profiles",
),
sync_index_dict_name=os.getenv(
"COMFY_MODAL_SYNC_INDEX_DICT_NAME",
f"{app_name}-sync-index",
),
node_output_cache_max_bytes=_read_int_env(
"COMFY_MODAL_NODE_CACHE_MAX_BYTES",
5 * 1024 * 1024,
),
bridge_inline_max_bytes=max(
0,
_read_int_env("COMFY_MODAL_BRIDGE_INLINE_MAX_BYTES", 4 * 1024 * 1024),
),
invocation_result_inline_max_bytes=max(
0,
_read_int_env(
"COMFY_MODAL_INVOCATION_RESULT_INLINE_MAX_BYTES",
4 * 1024 * 1024,
),
),
terminate_container_on_error=_read_bool_env("COMFY_MODAL_TERMINATE_CONTAINER_ON_ERROR")
is not False,
modal_gpu=os.getenv("COMFY_MODAL_GPU", DEFAULT_MODAL_GPU).strip() or DEFAULT_MODAL_GPU,
scaledown_window_seconds=_read_int_env("COMFY_MODAL_SCALEDOWN_WINDOW", 600),
local_gap_keepalive_seconds=max(
0.0,
_read_float_env("COMFY_MODAL_LOCAL_GAP_KEEPALIVE_SECONDS", 900.0),
),
local_gap_keepalive_interval_seconds=max(
1.0,
_read_float_env(
"COMFY_MODAL_LOCAL_GAP_KEEPALIVE_INTERVAL_SECONDS",
15.0,
),
),
min_containers=_read_int_env("COMFY_MODAL_MIN_CONTAINERS", 0),
max_containers=_read_optional_int_env("COMFY_MODAL_MAX_CONTAINERS"),
buffer_containers=_read_optional_int_env("COMFY_MODAL_BUFFER_CONTAINERS"),
max_inflight_calls=max(
1,
_read_int_env("COMFY_MODAL_MAX_INFLIGHT_CALLS", 4),
),
execution_timeout_seconds=max(
1,
_read_int_env("COMFY_MODAL_EXECUTION_TIMEOUT_SECONDS", 3600),
),
startup_timeout_seconds=max(
1,
_read_int_env("COMFY_MODAL_STARTUP_TIMEOUT_SECONDS", 900),
),
enable_proactive_warmup=_read_bool_env("COMFY_MODAL_ENABLE_PROACTIVE_WARMUP") is not False,
enable_loader_prewarm=_read_bool_env("COMFY_MODAL_ENABLE_LOADER_PREWARM") is not False,
loader_prewarm_workers=max(
1,
_read_int_env("COMFY_MODAL_LOADER_PREWARM_WORKERS", 2),
),
proactive_warmup_head_start_seconds=_read_float_env(
"COMFY_MODAL_PROACTIVE_WARMUP_HEAD_START_SECONDS",
2.0,
),
remote_cancel_grace_seconds=_read_float_env(
"COMFY_MODAL_REMOTE_CANCEL_GRACE_SECONDS",
2.0,
),
remote_cancel_restart_seconds=_read_float_env(
"COMFY_MODAL_REMOTE_CANCEL_RESTART_SECONDS",
1.0,
),
stream_event_queue_maxsize=max(
4,
_read_int_env("COMFY_MODAL_STREAM_EVENT_QUEUE_MAXSIZE", 256),
),
stream_remote_container_logs=_read_bool_env("COMFY_MODAL_STREAM_REMOTE_CONTAINER_LOGS")
or False,
llm_max_resident_models=max(
1,
_read_int_env("COMFY_MODAL_LLM_MAX_RESIDENT_MODELS", 2),
),
llm_reserve_free_vram_gb=max(
0.0,
_read_float_env("COMFY_MODAL_LLM_RESERVE_FREE_GB", 24.0),
),
llm_compile_cache_volume_name=(
os.getenv(
"COMFY_MODAL_LLM_COMPILE_CACHE_VOLUME_NAME",
f"{volume_name}-llm-compile-cache",
).strip()
or f"{volume_name}-llm-compile-cache"
),
llm_vllm_execution_mode=normalize_vllm_execution_mode(
os.getenv("COMFY_MODAL_LLM_VLLM_EXECUTION_MODE", "auto")
),
llm_memory_recovery_timeout_seconds=max(
0.0,
_read_float_env(
"COMFY_MODAL_LLM_MEMORY_RECOVERY_TIMEOUT_SECONDS",
15.0,
),
),
)
logger.debug("Resolved Modal-Sync settings: %s", settings)
return settings
def get_settings() -> ModalSyncSettings:
"""Return extension settings derived from the current environment."""
return _get_settings_cached(_settings_env_signature())
get_settings.cache_clear = _get_settings_cached.cache_clear # type: ignore[attr-defined]