Implement headless ComfyUI installation and improve backend status handling
- Added a new function to ensure headless installation of ComfyUI when backends are empty, enhancing the setup process for SwarmUI. - Updated documentation to clarify the installation flow and backend readiness checks, ensuring users understand the requirements for a successful setup. - Enhanced backend status checks to differentiate between 'empty' and 'idle' states, improving error handling and user feedback during provisioning. - Adjusted logging messages to provide clearer insights into the installation and backend status processes.
This commit is contained in:
+1
-1
@@ -231,7 +231,7 @@ Application credential для idle-killer CLI создаёт на `up` (узко
|
|||||||
|
|
||||||
1. Nova `ACTIVE`
|
1. Nova `ACTIVE`
|
||||||
2. TCP 22 / SSH
|
2. TCP 22 / SSH
|
||||||
3. Backend Idle (если SwarmUI) → toast (если `NOTIFY_READY`)
|
3. First-install Comfy (InstallConfirmWS, если backend был empty) → backend Idle → toast (если `NOTIFY_READY`)
|
||||||
4. На VM: HTTP сервисов стека (SwarmUI `:7801` / Ollama `:11434`) — `verify_stack_on_vm`
|
4. На VM: HTTP сервисов стека (SwarmUI `:7801` / Ollama `:11434`) — `verify_stack_on_vm`
|
||||||
5. На VM: **nvidia-smi / CUDA** (fail-fast) + **torch+cuda** в Comfy venv при SwarmUI (ждём) — `verify_gpu_env`
|
5. На VM: **nvidia-smi / CUDA** (fail-fast) + **torch+cuda** в Comfy venv при SwarmUI (ждём) — `verify_gpu_env`
|
||||||
6. В логе: строка **`тайминг up:`** (SSH / bootstrap / Idle / verify / …)
|
6. В логе: строка **`тайминг up:`** (SSH / bootstrap / Idle / verify / …)
|
||||||
|
|||||||
+4
-2
@@ -21,7 +21,7 @@
|
|||||||
7. Push непустых `Models/` / `Wildcards/` / `CustomWorkflows/`.
|
7. Push непустых `Models/` / `Wildcards/` / `CustomWorkflows/`.
|
||||||
8. systemd unit `swarmui`: `./launch-linux.sh --launch_mode none --host 127.0.0.1 --port 7801`.
|
8. systemd unit `swarmui`: `./launch-linux.sh --launch_mode none --host 127.0.0.1 --port 7801`.
|
||||||
9. **GPU probe** → `/mnt/swarm_data/.gpu-rent-gpu.json` (VRAM / compute cap / tier) — до старта UI; Ollama читает его при install.
|
9. **GPU probe** → `/mnt/swarm_data/.gpu-rent-gpu.json` (VRAM / compute cap / tier) — до старта UI; Ollama читает его при install.
|
||||||
10. Старт SwarmUI → seed LLM → idle-killer → **wait backend Idle**.
|
10. Старт SwarmUI → **headless InstallConfirmWS** (ComfyUI в `dlbackend`, models=none — веса уже с Civitai) → seed LLM → idle-killer → **wait backend Idle** (только `idle`; `empty` ≠ ready).
|
||||||
11. **Perf tune после Idle** — `triton`+`sageattention` в Comfy venv и `--use-sage-attention` в `Data/Backends.fds` (маркер `.gpu-rent-perf-tuned`; повтор при смене GPU).
|
11. **Perf tune после Idle** — `triton`+`sageattention` в Comfy venv и `--use-sage-attention` в `Data/Backends.fds` (маркер `.gpu-rent-perf-tuned`; повтор при смене GPU).
|
||||||
12. Авторизация SwarmUI включена, токен в `Data` на диске.
|
12. Авторизация SwarmUI включена, токен в `Data` на диске.
|
||||||
13. Один snapshot boot volume `gpu-rent-boot-ok`, если ещё нет.
|
13. Один snapshot boot volume `gpu-rent-boot-ok`, если ещё нет.
|
||||||
@@ -32,9 +32,11 @@
|
|||||||
|
|
||||||
Первый запуск качает backend в `/opt/swarmui/dlbackend` (это bind на data volume). Иначе каждый recreate потеряет часы.
|
Первый запуск качает backend в `/opt/swarmui/dlbackend` (это bind на data volume). Иначе каждый recreate потеряет часы.
|
||||||
|
|
||||||
|
**gpu-rent:** после `systemctl start swarmui` вызывается headless `InstallConfirmWS` (`backend=comfyui`, `models=none`), если `GetCurrentStatus` ещё `empty`. Без этого UI отвечает HTTP 200, а Comfy/torch так и не появляются — раньше `wait_backend_idle` ошибочно считал `empty` = Idle.
|
||||||
|
|
||||||
**Скорость без потери качества (gpu-rent):** после Idle backend, на GPU с compute capability ≥ 8.0 и ≥16 GiB VRAM — SageAttention. `Performance.AllowGpuSpecificOptimizations` у Swarm по умолчанию уже включает `--fast` для 30xx+. Если venv ещё не появился — маркер без `pip_ok`, догонит на следующем `up`.
|
**Скорость без потери качества (gpu-rent):** после Idle backend, на GPU с compute capability ≥ 8.0 и ≥16 GiB VRAM — SageAttention. `Performance.AllowGpuSpecificOptimizations` у Swarm по умолчанию уже включает `--fast` для 30xx+. Если venv ещё не появился — маркер без `pip_ok`, догонит на следующем `up`.
|
||||||
|
|
||||||
Пока CUDA/ComfyUI поднимаются, UI уже может отвечать. Для MCP и `/API/` — рано: `ready` после Idle backend (подтвердить на spike).
|
Пока CUDA/ComfyUI поднимаются, UI уже может отвечать. Для MCP и `/API/` — рано: `ready` после Idle backend.
|
||||||
|
|
||||||
## Доступ с ноутбука
|
## Доступ с ноутбука
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,20 @@ def tune_swarm_perf(cfg: Config, host: str, log: Log) -> bool:
|
|||||||
return "RESTART_SWARMUI=1" in out
|
return "RESTART_SWARMUI=1" in out
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_swarm_comfy_installed(cfg: Config, host: str, log: Log) -> None:
|
||||||
|
"""Headless SwarmUI InstallConfirmWS (ComfyUI) when backends are still empty."""
|
||||||
|
log("SwarmUI first-install: ComfyUI backend (если ещё empty)…")
|
||||||
|
run_python(
|
||||||
|
cfg,
|
||||||
|
host,
|
||||||
|
_pkg_text("install_swarm_comfy.py"),
|
||||||
|
remote_path="/tmp/gpu-rent-install_swarm_comfy.py",
|
||||||
|
# Cold: git clone Comfy + pip torch — often 15–40 min.
|
||||||
|
timeout=3900,
|
||||||
|
log=log,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def seed_extensions(cfg: Config, host: str, log: Log, *, update: bool = True) -> bool:
|
def seed_extensions(cfg: Config, host: str, log: Log, *, update: bool = True) -> bool:
|
||||||
from gpu_rent.llm_runtime import normalize_runtime
|
from gpu_rent.llm_runtime import normalize_runtime
|
||||||
|
|
||||||
|
|||||||
+21
-3
@@ -56,10 +56,16 @@ while time.time() < deadline:
|
|||||||
bstat = str(be.get("status") or "unknown").lower()
|
bstat = str(be.get("status") or "unknown").lower()
|
||||||
if waiting or live or loading:
|
if waiting or live or loading:
|
||||||
print(f"BUSY queue w={waiting} live={live} load={loading}")
|
print(f"BUSY queue w={waiting} live={live} load={loading}")
|
||||||
elif bstat not in ("idle", "disabled", "all_disabled", "empty"):
|
elif bstat == "empty":
|
||||||
|
# No backends registered — Comfy never installed (Install wizard).
|
||||||
|
# Not "Idle": treat as wait, never ready.
|
||||||
|
print("BUSY backend=empty (нужен first-install Comfy)")
|
||||||
|
elif bstat in ("disabled", "all_disabled"):
|
||||||
print(f"BUSY backend={bstat}")
|
print(f"BUSY backend={bstat}")
|
||||||
else:
|
elif bstat == "idle":
|
||||||
print(f"READY backend={bstat}")
|
print(f"READY backend={bstat}")
|
||||||
|
else:
|
||||||
|
print(f"BUSY backend={bstat}")
|
||||||
raise SystemExit(0)
|
raise SystemExit(0)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"WAIT {exc}")
|
print(f"WAIT {exc}")
|
||||||
@@ -316,7 +322,13 @@ def verify_gpu_env(
|
|||||||
from gpu_rent.ssh_ops import run_python
|
from gpu_rent.ssh_ops import run_python
|
||||||
|
|
||||||
# These never "appear later" on a broken image — don't burn the poll budget.
|
# These never "appear later" on a broken image — don't burn the poll budget.
|
||||||
|
# Empty-backend / missing install also won't self-heal without InstallConfirmWS.
|
||||||
instant_fail_names = {"nvidia-smi", "cuda"}
|
instant_fail_names = {"nvidia-smi", "cuda"}
|
||||||
|
instant_fail_detail_substrings = (
|
||||||
|
"dlbackend пуст",
|
||||||
|
"backend=empty",
|
||||||
|
"Install не прогоняли",
|
||||||
|
)
|
||||||
|
|
||||||
want_swarm = bool(getattr(cfg, "enable_swarmui", True))
|
want_swarm = bool(getattr(cfg, "enable_swarmui", True))
|
||||||
script = files("gpu_rent.remote").joinpath("stack_env_probe.py").read_text(
|
script = files("gpu_rent.remote").joinpath("stack_env_probe.py").read_text(
|
||||||
@@ -391,6 +403,11 @@ def verify_gpu_env(
|
|||||||
return last
|
return last
|
||||||
|
|
||||||
instant = [c for c in hard if c.name in instant_fail_names]
|
instant = [c for c in hard if c.name in instant_fail_names]
|
||||||
|
if not instant:
|
||||||
|
for c in hard:
|
||||||
|
detail_l = (c.detail or "").lower()
|
||||||
|
if any(s.lower() in detail_l for s in instant_fail_detail_substrings):
|
||||||
|
instant.append(c)
|
||||||
if instant:
|
if instant:
|
||||||
for c in last:
|
for c in last:
|
||||||
mark = "ok" if c.ok else "FAIL"
|
mark = "ok" if c.ok else "FAIL"
|
||||||
@@ -399,7 +416,8 @@ def verify_gpu_env(
|
|||||||
failed = [c.name for c in instant]
|
failed = [c.name for c in instant]
|
||||||
raise CloudError(
|
raise CloudError(
|
||||||
f"GPU-стек: нет {', '.join(failed)} (fail-fast). "
|
f"GPU-стек: нет {', '.join(failed)} (fail-fast). "
|
||||||
"Проверь образ Driver / nvidia на VM. "
|
"Проверь образ Driver / nvidia на VM; для SwarmUI — "
|
||||||
|
"first-install Comfy (InstallConfirmWS / dlbackend). "
|
||||||
"Дальше: gpu-rent status · gpu-rent logs · gpu-rent stop"
|
"Дальше: gpu-rent status · gpu-rent logs · gpu-rent stop"
|
||||||
)
|
)
|
||||||
return last
|
return last
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Headless SwarmUI first-install: ComfyUI backend via InstallConfirmWS.
|
||||||
|
|
||||||
|
Runs on the VM (stdlib only). Idempotent: skips when backends exist or
|
||||||
|
dlbackend/ComfyUI venv is already present and IsInstalled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SWARM = "http://127.0.0.1:7801"
|
||||||
|
DATA = Path("/mnt/swarm_data")
|
||||||
|
SWARM_ROOT = Path(os.environ.get("SWARM_ROOT") or "/opt/swarmui")
|
||||||
|
COMFY_VENV = DATA / "dlbackend" / "ComfyUI" / "venv" / "bin" / "python"
|
||||||
|
SETTINGS = DATA / "Data" / "Settings.fds"
|
||||||
|
# Comfy clone + torch can take 20–40+ min on a cold disk.
|
||||||
|
INSTALL_TIMEOUT = float(os.environ.get("GPU_RENT_COMFY_INSTALL_TIMEOUT") or "3600")
|
||||||
|
|
||||||
|
|
||||||
|
def post(path: str, payload: dict, timeout: float = 15.0) -> dict:
|
||||||
|
body = json.dumps(payload).encode("utf-8")
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{SWARM}{path}",
|
||||||
|
data=body,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def wait_http(deadline: float) -> None:
|
||||||
|
last = ""
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(f"{SWARM}/", method="GET")
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||||
|
if getattr(resp, "status", 200) == 200:
|
||||||
|
return
|
||||||
|
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError) as exc:
|
||||||
|
last = str(exc)[:160]
|
||||||
|
time.sleep(2)
|
||||||
|
raise SystemExit(f"SwarmUI HTTP not up: {last}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_session(deadline: float) -> str:
|
||||||
|
last = ""
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
data = post("/API/GetNewSession", {})
|
||||||
|
sid = str(data.get("session_id") or "")
|
||||||
|
if sid:
|
||||||
|
return sid
|
||||||
|
last = "no session_id"
|
||||||
|
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError, json.JSONDecodeError) as exc:
|
||||||
|
last = str(exc)[:160]
|
||||||
|
time.sleep(2)
|
||||||
|
raise SystemExit(f"SwarmUI session unavailable: {last}")
|
||||||
|
|
||||||
|
|
||||||
|
def backend_status() -> str:
|
||||||
|
try:
|
||||||
|
sid = str(post("/API/GetNewSession", {}).get("session_id") or "")
|
||||||
|
if not sid:
|
||||||
|
return "unknown"
|
||||||
|
data = post("/API/GetCurrentStatus", {"session_id": sid})
|
||||||
|
be = data.get("backend_status") or {}
|
||||||
|
return str(be.get("status") or "unknown").lower()
|
||||||
|
except Exception as exc:
|
||||||
|
return f"error:{exc}"
|
||||||
|
|
||||||
|
|
||||||
|
def settings_is_installed() -> bool | None:
|
||||||
|
if not SETTINGS.is_file():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
text = SETTINGS.read_text(encoding="utf-8", errors="replace")
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
for line in text.splitlines():
|
||||||
|
if "IsInstalled" in line:
|
||||||
|
low = line.lower()
|
||||||
|
if "true" in low:
|
||||||
|
return True
|
||||||
|
if "false" in low:
|
||||||
|
return False
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def comfy_venv_ok() -> bool:
|
||||||
|
return COMFY_VENV.is_file() and os.access(COMFY_VENV, os.X_OK)
|
||||||
|
|
||||||
|
|
||||||
|
def _recv_exact(sock: socket.socket, n: int) -> bytes:
|
||||||
|
buf = b""
|
||||||
|
while len(buf) < n:
|
||||||
|
chunk = sock.recv(n - len(buf))
|
||||||
|
if not chunk:
|
||||||
|
raise ConnectionError("websocket closed")
|
||||||
|
buf += chunk
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
|
def _ws_handshake(host: str, port: int, path: str) -> socket.socket:
|
||||||
|
key = base64.b64encode(os.urandom(16)).decode("ascii")
|
||||||
|
req = (
|
||||||
|
f"GET {path} HTTP/1.1\r\n"
|
||||||
|
f"Host: {host}:{port}\r\n"
|
||||||
|
"Upgrade: websocket\r\n"
|
||||||
|
"Connection: Upgrade\r\n"
|
||||||
|
f"Sec-WebSocket-Key: {key}\r\n"
|
||||||
|
"Sec-WebSocket-Version: 13\r\n"
|
||||||
|
"\r\n"
|
||||||
|
).encode("ascii")
|
||||||
|
sock = socket.create_connection((host, port), timeout=30)
|
||||||
|
sock.sendall(req)
|
||||||
|
# Read headers until blank line
|
||||||
|
data = b""
|
||||||
|
while b"\r\n\r\n" not in data:
|
||||||
|
chunk = sock.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
sock.close()
|
||||||
|
raise ConnectionError("no WS handshake response")
|
||||||
|
data += chunk
|
||||||
|
if len(data) > 65536:
|
||||||
|
sock.close()
|
||||||
|
raise ConnectionError("WS handshake too large")
|
||||||
|
head = data.split(b"\r\n\r\n", 1)[0].decode("latin-1", errors="replace")
|
||||||
|
if "101" not in head.split("\r\n", 1)[0]:
|
||||||
|
sock.close()
|
||||||
|
raise ConnectionError(f"WS handshake failed: {head.splitlines()[0]}")
|
||||||
|
expect = base64.b64encode(
|
||||||
|
hashlib.sha1(
|
||||||
|
(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode("ascii")
|
||||||
|
).digest()
|
||||||
|
).decode("ascii")
|
||||||
|
if expect not in head:
|
||||||
|
# Some servers omit echoing exact key in proxies; 101 is enough.
|
||||||
|
pass
|
||||||
|
sock.settimeout(120.0)
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def _ws_send_text(sock: socket.socket, text: str) -> None:
|
||||||
|
payload = text.encode("utf-8")
|
||||||
|
mask = os.urandom(4)
|
||||||
|
header = bytearray([0x81]) # FIN + text
|
||||||
|
n = len(payload)
|
||||||
|
if n < 126:
|
||||||
|
header.append(0x80 | n)
|
||||||
|
elif n < 65536:
|
||||||
|
header.append(0x80 | 126)
|
||||||
|
header.extend(struct.pack("!H", n))
|
||||||
|
else:
|
||||||
|
header.append(0x80 | 127)
|
||||||
|
header.extend(struct.pack("!Q", n))
|
||||||
|
header.extend(mask)
|
||||||
|
masked = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
|
||||||
|
sock.sendall(header + masked)
|
||||||
|
|
||||||
|
|
||||||
|
def _ws_recv_text(sock: socket.socket) -> str | None:
|
||||||
|
"""Return next text frame payload, or None on close."""
|
||||||
|
while True:
|
||||||
|
hdr = _recv_exact(sock, 2)
|
||||||
|
opcode = hdr[0] & 0x0F
|
||||||
|
masked = bool(hdr[1] & 0x80)
|
||||||
|
length = hdr[1] & 0x7F
|
||||||
|
if length == 126:
|
||||||
|
length = struct.unpack("!H", _recv_exact(sock, 2))[0]
|
||||||
|
elif length == 127:
|
||||||
|
length = struct.unpack("!Q", _recv_exact(sock, 8))[0]
|
||||||
|
mask_key = _recv_exact(sock, 4) if masked else b""
|
||||||
|
raw = _recv_exact(sock, length) if length else b""
|
||||||
|
if masked:
|
||||||
|
raw = bytes(b ^ mask_key[i % 4] for i, b in enumerate(raw))
|
||||||
|
if opcode == 0x8: # close
|
||||||
|
return None
|
||||||
|
if opcode == 0x9: # ping → pong
|
||||||
|
# build pong
|
||||||
|
frame = bytearray([0x8A, 0x80 | len(raw)])
|
||||||
|
m = os.urandom(4)
|
||||||
|
frame.extend(m)
|
||||||
|
frame.extend(bytes(b ^ m[i % 4] for i, b in enumerate(raw)))
|
||||||
|
sock.sendall(frame)
|
||||||
|
continue
|
||||||
|
if opcode in (0x1, 0x0): # text / continuation
|
||||||
|
return raw.decode("utf-8", errors="replace")
|
||||||
|
# ignore binary / other
|
||||||
|
|
||||||
|
|
||||||
|
def run_install(sid: str) -> None:
|
||||||
|
print("SwarmUI InstallConfirmWS: backend=comfyui models=none …")
|
||||||
|
sock = _ws_handshake("127.0.0.1", 7801, "/API/InstallConfirmWS")
|
||||||
|
payload = {
|
||||||
|
"session_id": sid,
|
||||||
|
"theme": "modern_dark",
|
||||||
|
"installed_for": "just_self",
|
||||||
|
"backend": "comfyui",
|
||||||
|
"models": "none",
|
||||||
|
"install_amd": False,
|
||||||
|
"language": "en",
|
||||||
|
"make_shortcut": False,
|
||||||
|
}
|
||||||
|
_ws_send_text(sock, json.dumps(payload))
|
||||||
|
deadline = time.time() + INSTALL_TIMEOUT
|
||||||
|
last_info = ""
|
||||||
|
try:
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
sock.settimeout(max(5.0, min(120.0, deadline - time.time())))
|
||||||
|
msg = _ws_recv_text(sock)
|
||||||
|
except (TimeoutError, socket.timeout):
|
||||||
|
print("… install still running (ws idle)")
|
||||||
|
continue
|
||||||
|
if msg is None:
|
||||||
|
raise SystemExit("InstallConfirmWS closed without success")
|
||||||
|
try:
|
||||||
|
data = json.loads(msg)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"ws raw: {msg[:200]}")
|
||||||
|
continue
|
||||||
|
if data.get("error"):
|
||||||
|
raise SystemExit(f"InstallConfirmWS error: {data['error']}")
|
||||||
|
if data.get("info"):
|
||||||
|
info = str(data["info"])
|
||||||
|
if info != last_info:
|
||||||
|
print(f"[installer] {info}")
|
||||||
|
last_info = info
|
||||||
|
if "progress" in data and data.get("progress"):
|
||||||
|
steps = data.get("steps")
|
||||||
|
total_steps = data.get("total_steps")
|
||||||
|
print(
|
||||||
|
f"[installer] progress step={steps}/{total_steps} "
|
||||||
|
f"bytes={data.get('progress')}/{data.get('total')}"
|
||||||
|
)
|
||||||
|
if data.get("success"):
|
||||||
|
print("InstallConfirmWS: success")
|
||||||
|
return
|
||||||
|
raise SystemExit(f"InstallConfirmWS timeout after {int(INSTALL_TIMEOUT)}s")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
sock.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
# WorkingDirectory for comfy-install-linux.sh is Swarm root when launched by
|
||||||
|
# Installation.cs; InstallConfirmWS handles that for us.
|
||||||
|
wait_http(time.time() + 180)
|
||||||
|
bstat = backend_status()
|
||||||
|
print(f"backend_status={bstat} venv={'yes' if comfy_venv_ok() else 'no'} "
|
||||||
|
f"IsInstalled={settings_is_installed()}")
|
||||||
|
|
||||||
|
if bstat == "idle":
|
||||||
|
print("Comfy backend already Idle — skip install")
|
||||||
|
return 0
|
||||||
|
if bstat not in ("empty", "unknown") and not bstat.startswith("error:"):
|
||||||
|
# loading / running / etc. — installer not needed
|
||||||
|
if comfy_venv_ok():
|
||||||
|
print(f"backends present ({bstat}) + venv — skip install")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
installed = settings_is_installed()
|
||||||
|
if installed is True and not comfy_venv_ok() and bstat == "empty":
|
||||||
|
print(
|
||||||
|
"WARN: Settings IsInstalled=true but backends empty and no Comfy venv. "
|
||||||
|
"Open SwarmUI → Server → Backends and add ComfyUI Self-Starting, "
|
||||||
|
"or delete Data/Settings.fds IsInstalled and re-run up."
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if installed is True and comfy_venv_ok():
|
||||||
|
print("IsInstalled + venv — skip InstallConfirmWS (ждём Idle отдельно)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if comfy_venv_ok() and bstat == "empty":
|
||||||
|
# Partial: script ran but backend never registered — still need install
|
||||||
|
# only if IsInstalled is false; otherwise user must add backend in UI.
|
||||||
|
if installed is not True:
|
||||||
|
print("venv есть, IsInstalled=false — запускаю InstallConfirmWS")
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
sid = get_session(time.time() + 120)
|
||||||
|
run_install(sid)
|
||||||
|
|
||||||
|
# Confirm outcome
|
||||||
|
time.sleep(3)
|
||||||
|
bstat2 = backend_status()
|
||||||
|
print(
|
||||||
|
f"after install: backend_status={bstat2} "
|
||||||
|
f"venv={'yes' if comfy_venv_ok() else 'no'}"
|
||||||
|
)
|
||||||
|
if not comfy_venv_ok() and bstat2 == "empty":
|
||||||
|
print("FAIL: install finished but still empty / no venv", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Ensure Installation.cs cwd paths resolve if anything shells out relative —
|
||||||
|
# InstallConfirmWS itself cds via WorkingDirectory of the service.
|
||||||
|
try:
|
||||||
|
os.chdir(str(SWARM_ROOT))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -80,6 +80,16 @@ def find_comfy_python() -> Path | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _dlbackend_empty_hint() -> str:
|
||||||
|
dl = DATA / "dlbackend"
|
||||||
|
try:
|
||||||
|
if not dl.is_dir() or not any(dl.iterdir()):
|
||||||
|
return "dlbackend пуст — SwarmUI Install не прогоняли (backend=empty)"
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return "ещё не поставился?"
|
||||||
|
|
||||||
|
|
||||||
def check_torch(py: Path) -> dict:
|
def check_torch(py: Path) -> dict:
|
||||||
script = (
|
script = (
|
||||||
"import json,sys\n"
|
"import json,sys\n"
|
||||||
@@ -150,7 +160,7 @@ def main() -> int:
|
|||||||
"name": "torch",
|
"name": "torch",
|
||||||
"required": True,
|
"required": True,
|
||||||
"ok": False,
|
"ok": False,
|
||||||
"detail": "ComfyUI venv python не найден (ещё не поставился?)",
|
"detail": f"ComfyUI venv python не найден ({_dlbackend_empty_hint()})",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+13
-1
@@ -21,7 +21,12 @@ from gpu_rent.cloud import (
|
|||||||
wait_volume,
|
wait_volume,
|
||||||
)
|
)
|
||||||
from gpu_rent.bootstrap import run_bootstrap
|
from gpu_rent.bootstrap import run_bootstrap
|
||||||
from gpu_rent.provision import provision_vm, seed_swarmui_api_keys, tune_swarm_perf
|
from gpu_rent.provision import (
|
||||||
|
ensure_swarm_comfy_installed,
|
||||||
|
provision_vm,
|
||||||
|
seed_swarmui_api_keys,
|
||||||
|
tune_swarm_perf,
|
||||||
|
)
|
||||||
from gpu_rent.ready import verify_gpu_env, verify_stack_on_vm, wait_backend_idle
|
from gpu_rent.ready import verify_gpu_env, verify_stack_on_vm, wait_backend_idle
|
||||||
from gpu_rent.snapshot import ensure_boot_snapshot
|
from gpu_rent.snapshot import ensure_boot_snapshot
|
||||||
from gpu_rent.notify import notify_ready
|
from gpu_rent.notify import notify_ready
|
||||||
@@ -152,10 +157,17 @@ def _bind_access(
|
|||||||
)
|
)
|
||||||
clock.mark("provision", log)
|
clock.mark("provision", log)
|
||||||
if swarm:
|
if swarm:
|
||||||
|
try:
|
||||||
|
ensure_swarm_comfy_installed(cfg, ip, log)
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"SwarmUI Comfy install: {exc}")
|
||||||
|
raise
|
||||||
|
clock.mark("comfy-install", log)
|
||||||
try:
|
try:
|
||||||
wait_backend_idle(cfg, ip, log)
|
wait_backend_idle(cfg, ip, log)
|
||||||
except CloudError as exc:
|
except CloudError as exc:
|
||||||
log(f"ready: {exc}")
|
log(f"ready: {exc}")
|
||||||
|
raise
|
||||||
clock.mark("Idle", log)
|
clock.mark("Idle", log)
|
||||||
try:
|
try:
|
||||||
if tune_swarm_perf(cfg, ip, log):
|
if tune_swarm_perf(cfg, ip, log):
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ def _mock_bind(monkeypatch):
|
|||||||
lambda cfg, host, log, update=True, light=False: None,
|
lambda cfg, host, log, update=True, light=False: None,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr("gpu_rent.session.provision_vm", lambda cfg, host, log, **kw: None)
|
monkeypatch.setattr("gpu_rent.session.provision_vm", lambda cfg, host, log, **kw: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"gpu_rent.session.ensure_swarm_comfy_installed",
|
||||||
|
lambda cfg, host, log: None,
|
||||||
|
)
|
||||||
monkeypatch.setattr("gpu_rent.session.wait_backend_idle", lambda cfg, host, log, **kw: None)
|
monkeypatch.setattr("gpu_rent.session.wait_backend_idle", lambda cfg, host, log, **kw: None)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"gpu_rent.session.verify_stack_on_vm",
|
"gpu_rent.session.verify_stack_on_vm",
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""Tests for wait_backend_idle READY semantics and install helpers."""
|
||||||
|
|
||||||
|
from gpu_rent.ready import _REMOTE_POLL
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_poll_empty_is_busy_not_ready():
|
||||||
|
assert 'bstat == "empty"' in _REMOTE_POLL
|
||||||
|
assert 'BUSY backend=empty' in _REMOTE_POLL
|
||||||
|
# Must not treat empty as READY anymore
|
||||||
|
assert '("idle", "disabled", "all_disabled", "empty")' not in _REMOTE_POLL
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_poll_idle_is_ready():
|
||||||
|
assert 'bstat == "idle"' in _REMOTE_POLL
|
||||||
|
assert "READY backend=" in _REMOTE_POLL
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_swarm_comfy_script_payload():
|
||||||
|
from importlib.resources import files
|
||||||
|
|
||||||
|
text = files("gpu_rent.remote").joinpath("install_swarm_comfy.py").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert "InstallConfirmWS" in text
|
||||||
|
assert '"backend": "comfyui"' in text
|
||||||
|
assert '"models": "none"' in text
|
||||||
|
assert "modern_dark" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_gpu_env_fail_fast_empty_dlbackend(monkeypatch):
|
||||||
|
import json
|
||||||
|
|
||||||
|
from gpu_rent.errors import CloudError
|
||||||
|
from gpu_rent.ready import verify_gpu_env
|
||||||
|
import gpu_rent.ssh_ops as ssh_ops
|
||||||
|
|
||||||
|
class Cfg:
|
||||||
|
enable_swarmui = True
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"ok": False,
|
||||||
|
"checks": [
|
||||||
|
{"name": "nvidia-smi", "required": True, "ok": True, "detail": "ok"},
|
||||||
|
{"name": "cuda", "required": True, "ok": True, "detail": "ok"},
|
||||||
|
{
|
||||||
|
"name": "torch",
|
||||||
|
"required": True,
|
||||||
|
"ok": False,
|
||||||
|
"detail": "ComfyUI venv python не найден (dlbackend пуст — SwarmUI Install не прогоняли)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
calls = {"n": 0}
|
||||||
|
|
||||||
|
def fake(*a, **k):
|
||||||
|
calls["n"] += 1
|
||||||
|
return json.dumps(payload)
|
||||||
|
|
||||||
|
monkeypatch.setattr(ssh_ops, "run_python", fake)
|
||||||
|
logs: list[str] = []
|
||||||
|
try:
|
||||||
|
verify_gpu_env(Cfg(), "1.2.3.4", logs.append, timeout=600.0, poll_every=0.1)
|
||||||
|
assert False, "expected CloudError"
|
||||||
|
except CloudError as exc:
|
||||||
|
assert "fail-fast" in str(exc).lower() or "torch" in str(exc).lower()
|
||||||
|
assert calls["n"] == 1
|
||||||
Reference in New Issue
Block a user