HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //opt/gpu_ondemand.py
#!/usr/bin/env python3
"""GPU-On-Demand-Manager für houston.

Lauscht auf den öffentlichen Ports der GPU-Dienste und startet den
jeweiligen systemd-Dienst erst bei eingehender Verbindung (TCP-Proxy auf
den internen Port). Nach IDLE_STOP_SECONDS ohne aktive Verbindung wird
der Dienst gestoppt, damit er kein VRAM belegt.

Konfiguration unten in SERVICES; rein stdlib (asyncio).
"""
from __future__ import annotations

import asyncio
import logging
import os
import time

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
LOG = logging.getLogger("gpu-ondemand")

IDLE_STOP_SECONDS = int(os.getenv("GPU_ONDEMAND_IDLE", "900"))
START_TIMEOUT_SECONDS = int(os.getenv("GPU_ONDEMAND_START_TIMEOUT", "600"))
LISTEN_HOST = "127.0.0.1"

SERVICES = {
    8088: {"unit": "voice2image.service", "backend_port": 18088},
    8026: {"unit": "ltx-video.service", "backend_port": 18026},
    8015: {"unit": "ace-step.service", "backend_port": 18015},
    8021: {"unit": "tts-clone.service", "backend_port": 18021},
}


class ServiceState:
    def __init__(self, unit: str, backend_port: int) -> None:
        self.unit = unit
        self.backend_port = backend_port
        self.active_connections = 0
        self.last_activity = 0.0
        self.start_lock = asyncio.Lock()


async def systemctl(*args: str) -> int:
    proc = await asyncio.create_subprocess_exec(
        "systemctl", *args,
        stdout=asyncio.subprocess.DEVNULL,
        stderr=asyncio.subprocess.DEVNULL,
    )
    return await proc.wait()


async def is_active(unit: str) -> bool:
    return await systemctl("is-active", "--quiet", unit) == 0


async def backend_ready(state: ServiceState) -> bool:
    try:
        _, writer = await asyncio.wait_for(
            asyncio.open_connection(LISTEN_HOST, state.backend_port), timeout=2.0)
        writer.close()
        await writer.wait_closed()
        return True
    except Exception:
        return False


async def ensure_backend(state: ServiceState) -> bool:
    """Startet den Dienst falls nötig und wartet, bis der Port erreichbar ist."""
    if await backend_ready(state):
        return True
    async with state.start_lock:
        if await backend_ready(state):
            return True
        LOG.info("Starte %s (on demand)…", state.unit)
        await systemctl("start", state.unit)
        deadline = time.monotonic() + START_TIMEOUT_SECONDS
        while time.monotonic() < deadline:
            if await backend_ready(state):
                LOG.info("%s bereit.", state.unit)
                return True
            await asyncio.sleep(1.0)
        LOG.error("%s wurde nicht rechtzeitig bereit.", state.unit)
        return False


async def pipe(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, state: ServiceState) -> None:
    try:
        while True:
            chunk = await reader.read(65536)
            if not chunk:
                break
            state.last_activity = time.monotonic()
            writer.write(chunk)
            await writer.drain()
    except (ConnectionResetError, BrokenPipeError, asyncio.CancelledError):
        pass
    finally:
        try:
            writer.close()
        except Exception:
            pass


async def handle_client(state: ServiceState, client_reader: asyncio.StreamReader, client_writer: asyncio.StreamWriter) -> None:
    state.active_connections += 1
    state.last_activity = time.monotonic()
    try:
        if not await ensure_backend(state):
            client_writer.close()
            return
        backend_reader, backend_writer = await asyncio.open_connection(LISTEN_HOST, state.backend_port)
        await asyncio.gather(
            pipe(client_reader, backend_writer, state),
            pipe(backend_reader, client_writer, state),
        )
    except Exception as exc:
        LOG.debug("Verbindungsfehler %s: %s", state.unit, exc)
        try:
            client_writer.close()
        except Exception:
            pass
    finally:
        state.active_connections -= 1
        state.last_activity = time.monotonic()


async def janitor(states: list[ServiceState]) -> None:
    while True:
        await asyncio.sleep(60)
        now = time.monotonic()
        for state in states:
            if state.active_connections > 0:
                continue
            if now - state.last_activity < IDLE_STOP_SECONDS:
                continue
            if await is_active(state.unit):
                LOG.info("Stoppe %s (idle seit %.0fs) — VRAM wird freigegeben.",
                         state.unit, now - state.last_activity)
                await systemctl("stop", state.unit)


async def main() -> None:
    states = []
    for port, cfg in SERVICES.items():
        state = ServiceState(cfg["unit"], cfg["backend_port"])
        state.last_activity = time.monotonic()
        states.append(state)
        server = await asyncio.start_server(
            lambda r, w, s=state: handle_client(s, r, w), LISTEN_HOST, port)
        LOG.info("Lausche auf %s:%d → %s (intern :%d)", LISTEN_HOST, port, cfg["unit"], cfg["backend_port"])

    asyncio.create_task(janitor(states))
    while True:
        await asyncio.sleep(3600)


if __name__ == "__main__":
    asyncio.run(main())