220 lines
7.6 KiB
Python
220 lines
7.6 KiB
Python
"""Workflow-declared remote execution configuration models."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
if __package__:
|
|
from .execution_environments import ExecutionProvider
|
|
from .remote_hosts import SshHostConfig
|
|
from .vast_models import VastResourceProfile
|
|
else: # pragma: no cover - direct ComfyUI loading fallback.
|
|
from execution_environments import ExecutionProvider
|
|
from remote_hosts import SshHostConfig
|
|
from vast_models import VastResourceProfile
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RemoteConfiguration(ABC):
|
|
"""Describe one workflow-declared pool of remote execution capacity."""
|
|
|
|
configuration_id: str
|
|
display_name: str
|
|
|
|
def __post_init__(self) -> None:
|
|
"""Reject ambiguous identities before provider discovery begins."""
|
|
if not self.configuration_id.strip():
|
|
raise ValueError("Remote configuration_id must not be empty.")
|
|
if not self.display_name.strip():
|
|
raise ValueError("Remote configuration display_name must not be empty.")
|
|
|
|
@property
|
|
@abstractmethod
|
|
def provider(self) -> ExecutionProvider:
|
|
"""Return the provider represented by this configuration."""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def capacity_limit(self) -> int:
|
|
"""Return the maximum concurrent worker capacity contributed by this pool."""
|
|
|
|
@abstractmethod
|
|
def to_safe_dict(self) -> dict[str, Any]:
|
|
"""Return credential-free workflow and diagnostic metadata."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ModalRemoteConfiguration(RemoteConfiguration):
|
|
"""Declare a Modal GPU type and its maximum concurrent container capacity."""
|
|
|
|
gpu_type: str
|
|
instance_count: int = 1
|
|
|
|
def __post_init__(self) -> None:
|
|
"""Validate Modal capacity fields."""
|
|
super().__post_init__()
|
|
if not self.gpu_type.strip():
|
|
raise ValueError("Modal gpu_type must not be empty.")
|
|
if self.instance_count <= 0:
|
|
raise ValueError("Modal instance_count must be positive.")
|
|
|
|
@property
|
|
def provider(self) -> ExecutionProvider:
|
|
"""Return the Modal provider identifier."""
|
|
return ExecutionProvider.MODAL
|
|
|
|
@property
|
|
def capacity_limit(self) -> int:
|
|
"""Return the configured Modal container ceiling."""
|
|
return self.instance_count
|
|
|
|
def to_safe_dict(self) -> dict[str, Any]:
|
|
"""Return credential-free Modal configuration metadata."""
|
|
return {
|
|
"configuration_id": self.configuration_id,
|
|
"display_name": self.display_name,
|
|
"provider": self.provider.value,
|
|
"gpu_type": self.gpu_type,
|
|
"instance_count": self.instance_count,
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class VastRemoteConfiguration(RemoteConfiguration):
|
|
"""Declare one Vast.ai marketplace capacity pool."""
|
|
|
|
profile: VastResourceProfile
|
|
|
|
def __post_init__(self) -> None:
|
|
"""Keep the wrapper identity aligned with the underlying Vast profile."""
|
|
super().__post_init__()
|
|
if self.profile.profile_id != self.configuration_id:
|
|
raise ValueError(
|
|
"Vast profile_id must match its remote configuration_id."
|
|
)
|
|
if self.profile.profile_name != self.display_name:
|
|
raise ValueError(
|
|
"Vast profile_name must match its remote configuration display_name."
|
|
)
|
|
|
|
@property
|
|
def provider(self) -> ExecutionProvider:
|
|
"""Return the Vast.ai provider identifier."""
|
|
return ExecutionProvider.VAST
|
|
|
|
@property
|
|
def capacity_limit(self) -> int:
|
|
"""Return the maximum number of managed Vast.ai instances."""
|
|
return self.profile.maximum_instances
|
|
|
|
def to_safe_dict(self) -> dict[str, Any]:
|
|
"""Return credential-free Vast configuration metadata."""
|
|
return {
|
|
"configuration_id": self.configuration_id,
|
|
"display_name": self.display_name,
|
|
"provider": self.provider.value,
|
|
"maximum_instances": self.profile.maximum_instances,
|
|
"search": self.profile.search_payload(),
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SshRemoteConfiguration(RemoteConfiguration):
|
|
"""Declare one SSH-accessible Docker host and its worker capacity."""
|
|
|
|
host: SshHostConfig
|
|
|
|
def __post_init__(self) -> None:
|
|
"""Keep workflow and host identities aligned."""
|
|
super().__post_init__()
|
|
if self.host.environment_id != self.configuration_id:
|
|
raise ValueError(
|
|
"SSH environment_id must match its remote configuration_id."
|
|
)
|
|
if self.host.display_name != self.display_name:
|
|
raise ValueError(
|
|
"SSH display_name must match its remote configuration display_name."
|
|
)
|
|
if self.host.maximum_workers <= 0:
|
|
raise ValueError("SSH maximum_workers must be positive in a workflow.")
|
|
|
|
@property
|
|
def provider(self) -> ExecutionProvider:
|
|
"""Return the SSH Docker provider identifier."""
|
|
return ExecutionProvider.SSH_DOCKER
|
|
|
|
@property
|
|
def capacity_limit(self) -> int:
|
|
"""Return the configured worker-container ceiling for this host."""
|
|
return self.host.maximum_workers
|
|
|
|
def to_safe_dict(self) -> dict[str, Any]:
|
|
"""Return credential-free SSH configuration metadata."""
|
|
return {
|
|
"configuration_id": self.configuration_id,
|
|
"display_name": self.display_name,
|
|
"provider": self.provider.value,
|
|
"host": self.host.to_dict(),
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RemoteConfigurationSet:
|
|
"""Hold the complete authoritative remote capacity declaration for a workflow."""
|
|
|
|
configurations: tuple[RemoteConfiguration, ...]
|
|
|
|
def __post_init__(self) -> None:
|
|
"""Require at least one uniquely identified and named configuration."""
|
|
if not self.configurations:
|
|
raise ValueError(
|
|
"Remote Execution Configurator requires at least one configuration."
|
|
)
|
|
configuration_ids = [
|
|
configuration.configuration_id for configuration in self.configurations
|
|
]
|
|
if len(configuration_ids) != len(set(configuration_ids)):
|
|
raise ValueError("Remote configuration IDs must be unique.")
|
|
normalized_names = [
|
|
configuration.display_name.casefold()
|
|
for configuration in self.configurations
|
|
]
|
|
if len(normalized_names) != len(set(normalized_names)):
|
|
raise ValueError("Remote configuration names must be unique.")
|
|
modal_gpu_types = [
|
|
configuration.gpu_type
|
|
for configuration in self.configurations
|
|
if isinstance(configuration, ModalRemoteConfiguration)
|
|
]
|
|
if len(modal_gpu_types) != len(set(modal_gpu_types)):
|
|
raise ValueError(
|
|
"Each Modal GPU type may appear in only one remote configuration."
|
|
)
|
|
ssh_targets = [
|
|
configuration.host.ssh_target
|
|
for configuration in self.configurations
|
|
if isinstance(configuration, SshRemoteConfiguration)
|
|
]
|
|
if len(ssh_targets) != len(set(ssh_targets)):
|
|
raise ValueError(
|
|
"Each SSH destination may appear in only one remote configuration."
|
|
)
|
|
|
|
def to_safe_list(self) -> list[dict[str, Any]]:
|
|
"""Return ordered credential-free configuration metadata."""
|
|
return [
|
|
configuration.to_safe_dict()
|
|
for configuration in self.configurations
|
|
]
|
|
|
|
|
|
__all__ = [
|
|
"ModalRemoteConfiguration",
|
|
"RemoteConfiguration",
|
|
"RemoteConfigurationSet",
|
|
"SshRemoteConfiguration",
|
|
"VastRemoteConfiguration",
|
|
]
|