Compare commits
10 Commits
9d8a817985
...
f00b249708
| Author | SHA1 | Date | |
|---|---|---|---|
| f00b249708 | |||
| 03be08f325 | |||
| 943105ccef | |||
| c9d4b636e6 | |||
| 94b1730c24 | |||
| a860d809d1 | |||
| 6918fd1efe | |||
| a2f1dc2bc6 | |||
|
|
ca7808f240 | ||
|
|
52e778fff3 |
7
Dockerfile
Normal file
7
Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM docker.io/mixa3607/pytorch-gfx906:v2.9.0-rocm-7.0.2-20251102214219
|
||||||
|
|
||||||
|
WORKDIR /comfyui
|
||||||
|
COPY ./requirements.txt ./requirements.txt
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
COPY ./ ./
|
||||||
|
ENTRYPOINT ["python", "main.py"]
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
# Fork of comfy UI adding systemd socket activation support and a ROCM docker file
|
||||||
|
|
||||||
|
**The socket activation support is the result of vibe coding and not sufficiently validated tobe ready for general usage or contributing back upstream. Use at your own risk!**
|
||||||
|
|
||||||
|
Instructions for socket activation in [utils/socket_activation](utils/socket_activation/README.md).
|
||||||
|
Original readme starts below:
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
# ComfyUI
|
# ComfyUI
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ class UserManager():
|
|||||||
return source
|
return source
|
||||||
|
|
||||||
dest = get_user_data_path(request, check_exists=False, param="dest")
|
dest = get_user_data_path(request, check_exists=False, param="dest")
|
||||||
if not isinstance(source, str):
|
if not isinstance(dest, str):
|
||||||
return dest
|
return dest
|
||||||
|
|
||||||
overwrite = request.query.get("overwrite", 'true') != "false"
|
overwrite = request.query.get("overwrite", 'true') != "false"
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ For source of truth on the allowed permutations of request fields, please refere
|
|||||||
- [Compatibility Table](https://app.klingai.com/global/dev/document-api/apiReference/model/skillsMap)
|
- [Compatibility Table](https://app.klingai.com/global/dev/document-api/apiReference/model/skillsMap)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Optional, TypeVar
|
|
||||||
import math
|
import math
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -66,9 +64,7 @@ from comfy_api_nodes.util import (
|
|||||||
poll_op,
|
poll_op,
|
||||||
)
|
)
|
||||||
from comfy_api.input_impl import VideoFromFile
|
from comfy_api.input_impl import VideoFromFile
|
||||||
from comfy_api.input.basic_types import AudioInput
|
from comfy_api.latest import ComfyExtension, IO, Input
|
||||||
from comfy_api.input.video_types import VideoInput
|
|
||||||
from comfy_api.latest import ComfyExtension, IO
|
|
||||||
|
|
||||||
KLING_API_VERSION = "v1"
|
KLING_API_VERSION = "v1"
|
||||||
PATH_TEXT_TO_VIDEO = f"/proxy/kling/{KLING_API_VERSION}/videos/text2video"
|
PATH_TEXT_TO_VIDEO = f"/proxy/kling/{KLING_API_VERSION}/videos/text2video"
|
||||||
@@ -94,8 +90,6 @@ AVERAGE_DURATION_IMAGE_GEN = 32
|
|||||||
AVERAGE_DURATION_VIDEO_EFFECTS = 320
|
AVERAGE_DURATION_VIDEO_EFFECTS = 320
|
||||||
AVERAGE_DURATION_VIDEO_EXTEND = 320
|
AVERAGE_DURATION_VIDEO_EXTEND = 320
|
||||||
|
|
||||||
R = TypeVar("R")
|
|
||||||
|
|
||||||
|
|
||||||
MODE_TEXT2VIDEO = {
|
MODE_TEXT2VIDEO = {
|
||||||
"standard mode / 5s duration / kling-v1": ("std", "5", "kling-v1"),
|
"standard mode / 5s duration / kling-v1": ("std", "5", "kling-v1"),
|
||||||
@@ -130,6 +124,8 @@ MODE_START_END_FRAME = {
|
|||||||
"pro mode / 10s duration / kling-v1-6": ("pro", "10", "kling-v1-6"),
|
"pro mode / 10s duration / kling-v1-6": ("pro", "10", "kling-v1-6"),
|
||||||
"pro mode / 5s duration / kling-v2-1": ("pro", "5", "kling-v2-1"),
|
"pro mode / 5s duration / kling-v2-1": ("pro", "5", "kling-v2-1"),
|
||||||
"pro mode / 10s duration / kling-v2-1": ("pro", "10", "kling-v2-1"),
|
"pro mode / 10s duration / kling-v2-1": ("pro", "10", "kling-v2-1"),
|
||||||
|
"pro mode / 5s duration / kling-v2-5-turbo": ("pro", "5", "kling-v2-5-turbo"),
|
||||||
|
"pro mode / 10s duration / kling-v2-5-turbo": ("pro", "10", "kling-v2-5-turbo"),
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
Returns a mapping of mode strings to their corresponding (mode, duration, model_name) tuples.
|
Returns a mapping of mode strings to their corresponding (mode, duration, model_name) tuples.
|
||||||
@@ -296,7 +292,7 @@ def get_video_from_response(response) -> KlingVideoResult:
|
|||||||
return video
|
return video
|
||||||
|
|
||||||
|
|
||||||
def get_video_url_from_response(response) -> Optional[str]:
|
def get_video_url_from_response(response) -> str | None:
|
||||||
"""Returns the first video url from the Kling video generation task result.
|
"""Returns the first video url from the Kling video generation task result.
|
||||||
Will not raise an error if the response is not valid.
|
Will not raise an error if the response is not valid.
|
||||||
"""
|
"""
|
||||||
@@ -315,7 +311,7 @@ def get_images_from_response(response) -> list[KlingImageResult]:
|
|||||||
return images
|
return images
|
||||||
|
|
||||||
|
|
||||||
def get_images_urls_from_response(response) -> Optional[str]:
|
def get_images_urls_from_response(response) -> str | None:
|
||||||
"""Returns the list of image urls from the Kling image generation task result.
|
"""Returns the list of image urls from the Kling image generation task result.
|
||||||
Will not raise an error if the response is not valid. If there is only one image, returns the url as a string. If there are multiple images, returns a list of urls.
|
Will not raise an error if the response is not valid. If there is only one image, returns the url as a string. If there are multiple images, returns a list of urls.
|
||||||
"""
|
"""
|
||||||
@@ -349,7 +345,7 @@ async def execute_text2video(
|
|||||||
model_mode: str,
|
model_mode: str,
|
||||||
duration: str,
|
duration: str,
|
||||||
aspect_ratio: str,
|
aspect_ratio: str,
|
||||||
camera_control: Optional[KlingCameraControl] = None,
|
camera_control: KlingCameraControl | None = None,
|
||||||
) -> IO.NodeOutput:
|
) -> IO.NodeOutput:
|
||||||
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V)
|
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V)
|
||||||
task_creation_response = await sync_op(
|
task_creation_response = await sync_op(
|
||||||
@@ -394,8 +390,8 @@ async def execute_image2video(
|
|||||||
model_mode: str,
|
model_mode: str,
|
||||||
aspect_ratio: str,
|
aspect_ratio: str,
|
||||||
duration: str,
|
duration: str,
|
||||||
camera_control: Optional[KlingCameraControl] = None,
|
camera_control: KlingCameraControl | None = None,
|
||||||
end_frame: Optional[torch.Tensor] = None,
|
end_frame: torch.Tensor | None = None,
|
||||||
) -> IO.NodeOutput:
|
) -> IO.NodeOutput:
|
||||||
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_I2V)
|
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_I2V)
|
||||||
validate_input_image(start_frame)
|
validate_input_image(start_frame)
|
||||||
@@ -451,8 +447,8 @@ async def execute_video_effect(
|
|||||||
model_name: str,
|
model_name: str,
|
||||||
duration: KlingVideoGenDuration,
|
duration: KlingVideoGenDuration,
|
||||||
image_1: torch.Tensor,
|
image_1: torch.Tensor,
|
||||||
image_2: Optional[torch.Tensor] = None,
|
image_2: torch.Tensor | None = None,
|
||||||
model_mode: Optional[KlingVideoGenMode] = None,
|
model_mode: KlingVideoGenMode | None = None,
|
||||||
) -> tuple[VideoFromFile, str, str]:
|
) -> tuple[VideoFromFile, str, str]:
|
||||||
if dual_character:
|
if dual_character:
|
||||||
request_input_field = KlingDualCharacterEffectInput(
|
request_input_field = KlingDualCharacterEffectInput(
|
||||||
@@ -499,13 +495,13 @@ async def execute_video_effect(
|
|||||||
|
|
||||||
async def execute_lipsync(
|
async def execute_lipsync(
|
||||||
cls: type[IO.ComfyNode],
|
cls: type[IO.ComfyNode],
|
||||||
video: VideoInput,
|
video: Input.Video,
|
||||||
audio: Optional[AudioInput] = None,
|
audio: Input.Audio | None = None,
|
||||||
voice_language: Optional[str] = None,
|
voice_language: str | None = None,
|
||||||
model_mode: Optional[str] = None,
|
model_mode: str | None = None,
|
||||||
text: Optional[str] = None,
|
text: str | None = None,
|
||||||
voice_speed: Optional[float] = None,
|
voice_speed: float | None = None,
|
||||||
voice_id: Optional[str] = None,
|
voice_id: str | None = None,
|
||||||
) -> IO.NodeOutput:
|
) -> IO.NodeOutput:
|
||||||
if text:
|
if text:
|
||||||
validate_string(text, field_name="Text", max_length=MAX_PROMPT_LENGTH_LIP_SYNC)
|
validate_string(text, field_name="Text", max_length=MAX_PROMPT_LENGTH_LIP_SYNC)
|
||||||
@@ -787,7 +783,7 @@ class KlingCameraControlT2VNode(IO.ComfyNode):
|
|||||||
negative_prompt: str,
|
negative_prompt: str,
|
||||||
cfg_scale: float,
|
cfg_scale: float,
|
||||||
aspect_ratio: str,
|
aspect_ratio: str,
|
||||||
camera_control: Optional[KlingCameraControl] = None,
|
camera_control: KlingCameraControl | None = None,
|
||||||
) -> IO.NodeOutput:
|
) -> IO.NodeOutput:
|
||||||
return await execute_text2video(
|
return await execute_text2video(
|
||||||
cls,
|
cls,
|
||||||
@@ -854,8 +850,8 @@ class KlingImage2VideoNode(IO.ComfyNode):
|
|||||||
mode: str,
|
mode: str,
|
||||||
aspect_ratio: str,
|
aspect_ratio: str,
|
||||||
duration: str,
|
duration: str,
|
||||||
camera_control: Optional[KlingCameraControl] = None,
|
camera_control: KlingCameraControl | None = None,
|
||||||
end_frame: Optional[torch.Tensor] = None,
|
end_frame: torch.Tensor | None = None,
|
||||||
) -> IO.NodeOutput:
|
) -> IO.NodeOutput:
|
||||||
return await execute_image2video(
|
return await execute_image2video(
|
||||||
cls,
|
cls,
|
||||||
@@ -965,15 +961,11 @@ class KlingStartEndFrameNode(IO.ComfyNode):
|
|||||||
IO.String.Input("prompt", multiline=True, tooltip="Positive text prompt"),
|
IO.String.Input("prompt", multiline=True, tooltip="Positive text prompt"),
|
||||||
IO.String.Input("negative_prompt", multiline=True, tooltip="Negative text prompt"),
|
IO.String.Input("negative_prompt", multiline=True, tooltip="Negative text prompt"),
|
||||||
IO.Float.Input("cfg_scale", default=0.5, min=0.0, max=1.0),
|
IO.Float.Input("cfg_scale", default=0.5, min=0.0, max=1.0),
|
||||||
IO.Combo.Input(
|
IO.Combo.Input("aspect_ratio", options=["16:9", "9:16", "1:1"]),
|
||||||
"aspect_ratio",
|
|
||||||
options=[i.value for i in KlingVideoGenAspectRatio],
|
|
||||||
default="16:9",
|
|
||||||
),
|
|
||||||
IO.Combo.Input(
|
IO.Combo.Input(
|
||||||
"mode",
|
"mode",
|
||||||
options=modes,
|
options=modes,
|
||||||
default=modes[2],
|
default=modes[8],
|
||||||
tooltip="The configuration to use for the video generation following the format: mode / duration / model_name.",
|
tooltip="The configuration to use for the video generation following the format: mode / duration / model_name.",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1254,8 +1246,8 @@ class KlingLipSyncAudioToVideoNode(IO.ComfyNode):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def execute(
|
async def execute(
|
||||||
cls,
|
cls,
|
||||||
video: VideoInput,
|
video: Input.Video,
|
||||||
audio: AudioInput,
|
audio: Input.Audio,
|
||||||
voice_language: str,
|
voice_language: str,
|
||||||
) -> IO.NodeOutput:
|
) -> IO.NodeOutput:
|
||||||
return await execute_lipsync(
|
return await execute_lipsync(
|
||||||
@@ -1314,7 +1306,7 @@ class KlingLipSyncTextToVideoNode(IO.ComfyNode):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def execute(
|
async def execute(
|
||||||
cls,
|
cls,
|
||||||
video: VideoInput,
|
video: Input.Video,
|
||||||
text: str,
|
text: str,
|
||||||
voice: str,
|
voice: str,
|
||||||
voice_speed: float,
|
voice_speed: float,
|
||||||
@@ -1471,7 +1463,7 @@ class KlingImageGenerationNode(IO.ComfyNode):
|
|||||||
human_fidelity: float,
|
human_fidelity: float,
|
||||||
n: int,
|
n: int,
|
||||||
aspect_ratio: KlingImageGenAspectRatio,
|
aspect_ratio: KlingImageGenAspectRatio,
|
||||||
image: Optional[torch.Tensor] = None,
|
image: torch.Tensor | None = None,
|
||||||
) -> IO.NodeOutput:
|
) -> IO.NodeOutput:
|
||||||
validate_string(prompt, field_name="prompt", min_length=1, max_length=MAX_PROMPT_LENGTH_IMAGE_GEN)
|
validate_string(prompt, field_name="prompt", min_length=1, max_length=MAX_PROMPT_LENGTH_IMAGE_GEN)
|
||||||
validate_string(negative_prompt, field_name="negative_prompt", max_length=MAX_PROMPT_LENGTH_IMAGE_GEN)
|
validate_string(negative_prompt, field_name="negative_prompt", max_length=MAX_PROMPT_LENGTH_IMAGE_GEN)
|
||||||
|
|||||||
115
server.py
115
server.py
@@ -37,13 +37,42 @@ from app.user_manager import UserManager
|
|||||||
from app.model_manager import ModelFileManager
|
from app.model_manager import ModelFileManager
|
||||||
from app.custom_node_manager import CustomNodeManager
|
from app.custom_node_manager import CustomNodeManager
|
||||||
from app.subgraph_manager import SubgraphManager
|
from app.subgraph_manager import SubgraphManager
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union, List, Tuple, Any
|
||||||
from api_server.routes.internal.internal_routes import InternalRoutes
|
from api_server.routes.internal.internal_routes import InternalRoutes
|
||||||
from protocol import BinaryEventTypes
|
from protocol import BinaryEventTypes
|
||||||
|
|
||||||
# Import cache control middleware
|
# Import cache control middleware
|
||||||
from middleware.cache_middleware import cache_control
|
from middleware.cache_middleware import cache_control
|
||||||
|
|
||||||
|
class NoListenSocket(socket.socket):
|
||||||
|
"""
|
||||||
|
A socket that pretends to be already listening.
|
||||||
|
The overridden ``listen`` simply returns without calling the kernel.
|
||||||
|
"""
|
||||||
|
def listen(self, backlog: int = socket.SOMAXCONN, *args: Any, **kwargs: Any) -> None: # type: ignore[override]
|
||||||
|
# If the socket is already in LISTEN state the kernel will reject a
|
||||||
|
# second listen() with EPERM (which is what triggered the SELinux
|
||||||
|
# denial). By turning it into a no‑op we avoid that system call.
|
||||||
|
# The socket is still usable by asyncio because it was created by
|
||||||
|
# systemd with ``listen`` already performed.
|
||||||
|
logging.debug("NoListenSocket.listen() called – no‑op (socket already listening)")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Helper --------------------------------------------------------------------
|
||||||
|
def _fd_to_socket(fd: int) -> socket.socket:
|
||||||
|
"""
|
||||||
|
Convert a file‑descriptor received from systemd into a socket that aiohttp
|
||||||
|
can use *without* performing a second listen().
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
sock = NoListenSocket(fileno=fd)
|
||||||
|
sock.setblocking(False)
|
||||||
|
return sock
|
||||||
|
except OSError as exc:
|
||||||
|
raise RuntimeError(f"Could not wrap fd {fd} as a NoListenSocket: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
async def send_socket_catch_exception(function, message):
|
async def send_socket_catch_exception(function, message):
|
||||||
try:
|
try:
|
||||||
await function(message)
|
await function(message)
|
||||||
@@ -1030,38 +1059,92 @@ class PromptServer():
|
|||||||
await self.start_multi_address([(address, port)], call_on_start=call_on_start)
|
await self.start_multi_address([(address, port)], call_on_start=call_on_start)
|
||||||
|
|
||||||
async def start_multi_address(self, addresses, call_on_start=None, verbose=True):
|
async def start_multi_address(self, addresses, call_on_start=None, verbose=True):
|
||||||
|
"""
|
||||||
|
Starts the aiohttp server. If systemd activation is detected the
|
||||||
|
provided ``addresses`` are ignored and the sockets returned by
|
||||||
|
``_get_systemd_sockets`` are used instead.
|
||||||
|
"""
|
||||||
runner = web.AppRunner(self.app, access_log=None)
|
runner = web.AppRunner(self.app, access_log=None)
|
||||||
await runner.setup()
|
await runner.setup()
|
||||||
ssl_ctx = None
|
ssl_ctx = None
|
||||||
scheme = "http"
|
scheme = "http"
|
||||||
if args.tls_keyfile and args.tls_certfile:
|
if args.tls_keyfile and args.tls_certfile:
|
||||||
ssl_ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER, verify_mode=ssl.CERT_NONE)
|
ssl_ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER,
|
||||||
|
verify_mode=ssl.CERT_NONE)
|
||||||
ssl_ctx.load_cert_chain(certfile=args.tls_certfile,
|
ssl_ctx.load_cert_chain(certfile=args.tls_certfile,
|
||||||
keyfile=args.tls_keyfile)
|
keyfile=args.tls_keyfile)
|
||||||
scheme = "https"
|
scheme = "https"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Systemd activation ------------------------------------------------
|
||||||
|
systemd_sockets = self._get_systemd_sockets()
|
||||||
|
if systemd_sockets:
|
||||||
if verbose:
|
if verbose:
|
||||||
logging.info("Starting server\n")
|
logging.info("Systemd socket activation detected – using supplied socket(s)")
|
||||||
for addr in addresses:
|
|
||||||
address = addr[0]
|
for sock in systemd_sockets:
|
||||||
port = addr[1]
|
# ``sock.getsockname()`` can be 2‑tuple (IPv4) or 4‑tuple (IPv6)
|
||||||
|
# or a string (Unix). Normalise for logging.
|
||||||
|
try:
|
||||||
|
raw_name = sock.getsockname()
|
||||||
|
if isinstance(raw_name, tuple):
|
||||||
|
host = raw_name[0]
|
||||||
|
port = raw_name[1]
|
||||||
|
if sock.family == socket.AF_INET6:
|
||||||
|
host = f"[{host}]"
|
||||||
|
else:
|
||||||
|
# Unix domain socket – just show the path.
|
||||||
|
host, port = raw_name, ""
|
||||||
|
except Exception:
|
||||||
|
host, port = "unknown", "unknown"
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
logging.info(f"GUI reachable at: {scheme}://{host}:{port}")
|
||||||
|
|
||||||
|
site = web.SockSite(runner, sock, ssl_context=ssl_ctx)
|
||||||
|
await site.start()
|
||||||
|
|
||||||
|
# Keep a reference – useful for debugging / graceful shutdown
|
||||||
|
self.systemd_sockets = systemd_sockets
|
||||||
|
else:
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Classic TCPSite fallback -----------------------------------------
|
||||||
|
if verbose:
|
||||||
|
logging.info("Systemd activation not detected – falling back to manual bind")
|
||||||
|
for address, port in addresses:
|
||||||
site = web.TCPSite(runner, address, port, ssl_context=ssl_ctx)
|
site = web.TCPSite(runner, address, port, ssl_context=ssl_ctx)
|
||||||
await site.start()
|
await site.start()
|
||||||
|
|
||||||
if not hasattr(self, 'address'):
|
if not hasattr(self, "address"):
|
||||||
self.address = address #TODO: remove this
|
self.address = address
|
||||||
self.port = port
|
self.port = port
|
||||||
|
|
||||||
if ':' in address:
|
# Nicely format IPv6 literals for the log line.
|
||||||
address_print = "[{}]".format(address)
|
address_print = f"[{address}]" if ":" in address else address
|
||||||
else:
|
|
||||||
address_print = address
|
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
logging.info("To see the GUI go to: {}://{}:{}".format(scheme, address_print, port))
|
logging.info(f"GUI reachable at: {scheme}://{address_print}:{port}")
|
||||||
|
|
||||||
if call_on_start is not None:
|
# -----------------------------------------------------------------
|
||||||
call_on_start(scheme, self.address, self.port)
|
if call_on_start:
|
||||||
|
await call_on_start()
|
||||||
|
|
||||||
|
def _get_systemd_sockets(self) -> List[socket.socket]:
|
||||||
|
sockets = []
|
||||||
|
if "LISTEN_FDS" not in os.environ or "LISTEN_PID" not in os.environ:
|
||||||
|
return sockets
|
||||||
|
listen_fds = int(os.getenv("LISTEN_FDS", "0"))
|
||||||
|
listen_pid = int(os.getenv("LISTEN_PID", "0"))
|
||||||
|
if listen_pid != os.getpid():
|
||||||
|
return sockets
|
||||||
|
for i in range(listen_fds):
|
||||||
|
fd = 3 + i
|
||||||
|
try:
|
||||||
|
sockets.append(_fd_to_socket(fd))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to convert fd {fd}: {e}")
|
||||||
|
os.unsetenv("LISTEN_FDS")
|
||||||
|
os.unsetenv("LISTEN_PID")
|
||||||
|
return sockets
|
||||||
|
|
||||||
def add_on_prompt_handler(self, handler):
|
def add_on_prompt_handler(self, handler):
|
||||||
self.on_prompt_handlers.append(handler)
|
self.on_prompt_handlers.append(handler)
|
||||||
|
|||||||
40
utils/socket_activation/README.md
Normal file
40
utils/socket_activation/README.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Rootless podman container with Systemd Socket activation
|
||||||
|
|
||||||
|
## Idea
|
||||||
|
By passing in the socket from systemd we minimize resource use when not in use.
|
||||||
|
Since no other network access is required for operation, we can configure the container
|
||||||
|
with network=none and minimize the risk of the AI escaping.
|
||||||
|
|
||||||
|
## Set up
|
||||||
|
|
||||||
|
Optional, if you want to run this as a separate user
|
||||||
|
```
|
||||||
|
sudo useradd comfy
|
||||||
|
sudo machinectl shell comfy@
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out this repository, navigate to its root directory and build the comfy
|
||||||
|
container with
|
||||||
|
```
|
||||||
|
podman build -t localhost/comfy:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
Place comfy.socket in ` ~/.config/systemd/user`, adjust ports and interfaces if needed.
|
||||||
|
Place comfy.container in `~/.config/containers/systemd`. Adjust paths for models and config if desired.
|
||||||
|
The files are in `utils/socket_activation`, next to this readme.
|
||||||
|
|
||||||
|
Put model files into the models directory (`~/models`).
|
||||||
|
|
||||||
|
Start the socket:
|
||||||
|
```
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now comfy.socket
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to run the service also when the user is not logged in, enable lingering:
|
||||||
|
```
|
||||||
|
sudo loginctl enable-linger <user>
|
||||||
|
```
|
||||||
|
|
||||||
|
Check that you can access comfy in browser. For troubleshooting, use, e. g., `journalctl -xe`.
|
||||||
|
|
||||||
24
utils/socket_activation/comfy.container
Normal file
24
utils/socket_activation/comfy.container
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Comfy ui in a ROCM container
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Image=localhost/comfy:latest
|
||||||
|
#AutoRemove=yes
|
||||||
|
#PublishPort=8080:8080
|
||||||
|
Environment=ROCR_VISIBLE_DEVICES="GGPU-3b0c81617337ec1b"
|
||||||
|
Network=none
|
||||||
|
Volume=%h/comfy/models:/comfyui/models:ro,z
|
||||||
|
Volume=%h/comfy/input:/comfyui/input:Z
|
||||||
|
Volume=%h/comfy/output:/comfyui/output:Z
|
||||||
|
Volume=%h/comfy/user:/comfyui/user:Z
|
||||||
|
AddDevice=/dev/dri
|
||||||
|
AddDevice=/dev/kfd
|
||||||
|
Exec=
|
||||||
|
|
||||||
|
#[Service]
|
||||||
|
#Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
8
utils/socket_activation/comfy.socket
Normal file
8
utils/socket_activation/comfy.socket
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=llama socket
|
||||||
|
|
||||||
|
[Socket]
|
||||||
|
ListenStream=0.0.0.0:8095
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
24
utils/socket_activation/comfy2.container
Normal file
24
utils/socket_activation/comfy2.container
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Comfy ui in a ROCM container
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Image=localhost/comfy:latest
|
||||||
|
#AutoRemove=yes
|
||||||
|
#PublishPort=8080:8080
|
||||||
|
Network=none
|
||||||
|
Environment=ROCR_VISIBLE_DEVICES="GPU-3ce290c173497dfb"
|
||||||
|
Volume=%h/comfy/models:/comfyui/models:ro,z
|
||||||
|
Volume=%h/comfy/input2:/comfyui/input:Z
|
||||||
|
Volume=%h/comfy/output2:/comfyui/output:Z
|
||||||
|
Volume=%h/comfy/user2:/comfyui/user:Z
|
||||||
|
AddDevice=/dev/dri
|
||||||
|
AddDevice=/dev/kfd
|
||||||
|
Exec=
|
||||||
|
|
||||||
|
#[Service]
|
||||||
|
#Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
8
utils/socket_activation/comfy2.socket
Normal file
8
utils/socket_activation/comfy2.socket
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Comfy second instance socket
|
||||||
|
|
||||||
|
[Socket]
|
||||||
|
ListenStream=0.0.0.0:8096
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
Reference in New Issue
Block a user