#!/usr/bin/env python3 """Lease-aware FLUX image generation for the shared titan-24 GPU.""" from __future__ import annotations import base64 import gc import io import json import os import ssl import subprocess import sys import threading import time from contextlib import redirect_stdout from datetime import UTC, datetime from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any from urllib.error import HTTPError from urllib.request import Request, urlopen HOST = os.environ.get("HERMES_LOCAL_IMAGE_HOST", "0.0.0.0") # Kubernetes reserves _PORT for service-link discovery. Keep the # listener setting out of that namespace so it is always numeric. PORT = int(os.environ.get("HERMES_LOCAL_IMAGE_LISTEN_PORT", "9004")) MODEL_ID = os.environ.get( "HERMES_LOCAL_IMAGE_MODEL", "black-forest-labs/FLUX.2-klein-4B" ) MODEL_REVISION = os.environ.get( "HERMES_LOCAL_IMAGE_REVISION", "e7b7dc27f91deacad38e78976d1f2b499d76a294" ) MODEL_CACHE = os.environ.get("HF_HOME", "/models/huggingface") OFFLOAD_MODE = os.environ.get("HERMES_LOCAL_IMAGE_OFFLOAD_MODE", "sequential") LEASE_NAMESPACE = os.environ.get("LEASE_NAMESPACE", "hermes") LEASE_NAME = os.environ.get("LEASE_NAME", "titan-24-gpu-owner") LEASE_IDLE_OWNER = os.environ.get("LEASE_IDLE_OWNER", "hermes") LEASE_IMAGE_OWNER = os.environ.get("LEASE_IMAGE_OWNER", "hermes-image") MAX_BODY_BYTES = int(os.environ.get("HERMES_LOCAL_IMAGE_MAX_BODY", str(96 << 20))) QUEUE_TIMEOUT_SECONDS = float(os.environ.get("HERMES_LOCAL_IMAGE_QUEUE_TIMEOUT", "1200")) RENDER_TIMEOUT_SECONDS = float( os.environ.get("HERMES_LOCAL_IMAGE_RENDER_TIMEOUT", "1200") ) GPU_ACTIVITY_URL = os.environ.get( "HERMES_LOCAL_IMAGE_GPU_ACTIVITY_URL", "http://nvidia-process-exporter.monitoring.svc.cluster.local:9401/metrics", ) GPU_ACTIVITY_NODE = os.environ.get("HERMES_LOCAL_IMAGE_GPU_ACTIVITY_NODE", "titan-24") GPU_ACTIVE_SM_PERCENT = float( os.environ.get("HERMES_LOCAL_IMAGE_GPU_ACTIVE_SM_PERCENT", "8") ) GPU_MAX_EXTERNAL_MEMORY_BYTES = int( os.environ.get("HERMES_LOCAL_IMAGE_GPU_MAX_EXTERNAL_MEMORY_BYTES", str(3 << 30)) ) GPU_ACTIVITY_SAMPLES = max( 1, int(os.environ.get("HERMES_LOCAL_IMAGE_GPU_ACTIVITY_SAMPLES", "3")) ) GPU_ACTIVITY_SAMPLE_INTERVAL = max( 0.0, float(os.environ.get("HERMES_LOCAL_IMAGE_GPU_ACTIVITY_SAMPLE_INTERVAL", "1")) ) API_HOST = os.environ.get("KUBERNETES_SERVICE_HOST", "kubernetes.default.svc") API_PORT = os.environ.get("KUBERNETES_SERVICE_PORT_HTTPS", "443") TOKEN_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount/token") CA_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") LEASE_URL = ( f"https://{API_HOST}:{API_PORT}/apis/coordination.k8s.io/v1/" f"namespaces/{LEASE_NAMESPACE}/leases/{LEASE_NAME}" ) ASPECTS = { "square": (1024, 1024), "landscape": (768, 1280), "portrait": (1280, 768), } _generation_lock = threading.Lock() _state_lock = threading.Lock() _state: dict[str, Any] = { "busy": False, "phase": "idle", "last_error": None, "last_model": None, "interactive_active": None, "external_gpu_memory_bytes": None, "external_gpu_sm_percent": None, "gpu_guard_reason": None, } def _prometheus_labels(raw: str) -> dict[str, str]: """Parse the simple quoted labels emitted by nvidia-process-exporter.""" labels: dict[str, str] = {} for item in raw.split(","): key, separator, value = item.partition("=") if not separator: continue labels[key.strip()] = value.strip().strip('"') return labels def _parse_gpu_activity(metrics: str) -> dict[str, Any]: """Summarize non-Hermes GPU processes on the configured image node.""" memory_bytes = 0 max_sm_percent = 0.0 processes: set[str] = set() for line in metrics.splitlines(): if not line or line.startswith("#") or "{" not in line or "}" not in line: continue metric, remainder = line.split("{", 1) raw_labels, separator, raw_value = remainder.partition("}") if not separator or metric not in { "nvidia_process_gpu_memory_used_bytes", "nvidia_process_gpu_sm_util_percent", }: continue labels = _prometheus_labels(raw_labels) if labels.get("node") != GPU_ACTIVITY_NODE or labels.get("namespace") == "hermes": continue try: value = float(raw_value.strip().split()[0]) except (ValueError, IndexError): continue process = labels.get("process") or "unknown" processes.add(f"{labels.get('namespace', 'unknown')}/{process}") if metric == "nvidia_process_gpu_memory_used_bytes": memory_bytes += max(0, int(value)) else: max_sm_percent = max(max_sm_percent, value) active = ( max_sm_percent >= GPU_ACTIVE_SM_PERCENT or memory_bytes >= GPU_MAX_EXTERNAL_MEMORY_BYTES ) reasons = [] if max_sm_percent >= GPU_ACTIVE_SM_PERCENT: reasons.append(f"external SM utilization is {max_sm_percent:g}%") if memory_bytes >= GPU_MAX_EXTERNAL_MEMORY_BYTES: reasons.append(f"external GPU memory is {memory_bytes} bytes") return { "interactive_active": active, "external_gpu_memory_bytes": memory_bytes, "external_gpu_sm_percent": max_sm_percent, "external_gpu_processes": sorted(processes), "gpu_guard_reason": "; ".join(reasons) or None, } def _gpu_activity_snapshot() -> dict[str, Any]: """Fetch live per-process GPU attribution, failing closed if unavailable.""" request = Request(GPU_ACTIVITY_URL, headers={"Accept": "text/plain"}) with urlopen(request, timeout=5) as response: metrics = response.read(2 << 20).decode("utf-8", errors="replace") return _parse_gpu_activity(metrics) def _ensure_interactive_lane_idle() -> dict[str, Any]: """Reject FLUX while desktop or Wolf activity indicates interactive use.""" worst: dict[str, Any] | None = None for sample in range(GPU_ACTIVITY_SAMPLES): snapshot = _gpu_activity_snapshot() if worst is None or ( snapshot["external_gpu_sm_percent"], snapshot["external_gpu_memory_bytes"] ) > (worst["external_gpu_sm_percent"], worst["external_gpu_memory_bytes"]): worst = snapshot if snapshot["interactive_active"]: break if sample + 1 < GPU_ACTIVITY_SAMPLES: time.sleep(GPU_ACTIVITY_SAMPLE_INTERVAL) assert worst is not None _set_state(**{key: value for key, value in worst.items() if key != "external_gpu_processes"}) if worst["interactive_active"]: raise RuntimeError( "titan-24 desktop/Wolf lane is active; reserve or release it from the Atlas GPU checkout" f" ({worst['gpu_guard_reason']})" ) return worst def _set_state(**values: Any) -> None: """Update health state without exposing prompts or image content.""" with _state_lock: _state.update(values) def _state_snapshot() -> dict[str, Any]: """Return a stable copy for health responses.""" with _state_lock: return dict(_state) def _kube_request(method: str, body: dict[str, Any] | None = None) -> dict[str, Any]: """Call the Lease API using the pod service account.""" token = TOKEN_PATH.read_text(encoding="utf-8").strip() raw = None if body is None else json.dumps(body).encode("utf-8") headers = {"Authorization": f"Bearer {token}"} if raw is not None: headers["Content-Type"] = "application/merge-patch+json" request = Request(LEASE_URL, data=raw, headers=headers, method=method) context = ssl.create_default_context(cafile=str(CA_PATH)) with urlopen(request, timeout=5, context=context) as response: return json.load(response) def _lease_owner() -> str: """Read the current GPU owner, failing closed on API errors.""" payload = _kube_request("GET") return str((payload.get("spec") or {}).get("holderIdentity") or "unavailable") def _change_owner(expected: str, desired: str) -> bool: """Change owner only if the resource version and holder still match.""" current = _kube_request("GET") if str((current.get("spec") or {}).get("holderIdentity")) != expected: return False resource_version = str((current.get("metadata") or {}).get("resourceVersion") or "") if not resource_version: return False patch = { "metadata": {"resourceVersion": resource_version}, "spec": { "holderIdentity": desired, "renewTime": datetime.now(UTC).isoformat().replace("+00:00", "Z"), }, } try: _kube_request("PATCH", patch) except HTTPError as exc: if exc.code == 409: return False raise return True def _recover_stale_image_lease() -> None: """Release this deployment's lease after a prior renderer crash.""" try: if _lease_owner() == LEASE_IMAGE_OWNER: _change_owner(LEASE_IMAGE_OWNER, LEASE_IDLE_OWNER) except Exception as exc: _set_state(last_error=f"lease recovery failed: {type(exc).__name__}: {exc}") def _decode_image(value: str): """Decode a bounded image data URL for FLUX editing.""" from PIL import Image if not value.startswith("data:image/") or ";base64," not in value: raise ValueError("local reference images must be data URLs") raw = base64.b64decode(value.split(",", 1)[1], validate=True) if not raw or len(raw) > 25 << 20: raise ValueError("reference image must be between 1 byte and 25 MiB") return Image.open(io.BytesIO(raw)).convert("RGB") def _render_in_process(payload: dict[str, Any]) -> dict[str, Any]: """Load and run FLUX inside the disposable CUDA worker process.""" import torch from diffusers import Flux2KleinPipeline prompt = str(payload.get("prompt") or "").strip() if not prompt: raise ValueError("prompt is required") aspect = str(payload.get("aspect_ratio") or "landscape").lower() if aspect not in ASPECTS: raise ValueError("aspect_ratio must be landscape, square, or portrait") height, width = ASPECTS[aspect] inputs = [] if payload.get("image_url"): inputs.append(_decode_image(str(payload["image_url"]))) for item in payload.get("reference_image_urls") or []: inputs.append(_decode_image(str(item))) inputs = inputs[:4] _set_state(phase="loading-flux", last_model=MODEL_ID) pipe = None try: pipe = Flux2KleinPipeline.from_pretrained( MODEL_ID, revision=MODEL_REVISION, torch_dtype=torch.bfloat16, cache_dir=MODEL_CACHE, ) if OFFLOAD_MODE == "sequential": # The shared 10 GiB card keeps the desktop and idle Wolf daemon # resident. Layer-level offload preserves 1024px output quality # without requiring those low-memory services to be torn down. pipe.enable_sequential_cpu_offload() elif OFFLOAD_MODE == "model": pipe.enable_model_cpu_offload() else: raise ValueError("HERMES_LOCAL_IMAGE_OFFLOAD_MODE must be sequential or model") _set_state(phase="rendering") arguments: dict[str, Any] = { "prompt": prompt, "height": height, "width": width, "guidance_scale": 1.0, "num_inference_steps": 4, } if inputs: arguments["image"] = inputs image = pipe(**arguments).images[0] output = io.BytesIO() image.save(output, format="PNG", optimize=True) return { "success": True, "image_b64": base64.b64encode(output.getvalue()).decode("ascii"), "mime_type": "image/png", "model": "flux-2-klein-4b-local", "quality": "local-high", "size": f"{width}x{height}", "input_image_count": len(inputs), "route": "local", } finally: del pipe gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() torch.cuda.ipc_collect() def _render(payload: dict[str, Any]) -> dict[str, Any]: """Render in a child process so process exit releases every CUDA allocation.""" _set_state(phase="render-worker", last_model=MODEL_ID) completed = subprocess.run( [sys.executable, str(Path(__file__).resolve()), "--render-worker"], input=json.dumps(payload, separators=(",", ":")).encode("utf-8"), stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=RENDER_TIMEOUT_SECONDS, check=False, ) if completed.returncode != 0: detail = completed.stderr.decode("utf-8", errors="replace").strip() if len(detail) > 4000: detail = detail[-4000:] raise RuntimeError(detail or f"FLUX worker exited {completed.returncode}") try: result = json.loads(completed.stdout) except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise RuntimeError("FLUX worker returned an invalid response") from exc if not isinstance(result, dict) or not result.get("success"): raise RuntimeError("FLUX worker returned an unsuccessful response") return result def _run_render_worker() -> int: """Read one bounded request and emit one JSON response for the parent.""" raw = sys.stdin.buffer.read(MAX_BODY_BYTES + 1) if not raw or len(raw) > MAX_BODY_BYTES: print("invalid render worker request size", file=sys.stderr) return 2 try: payload = json.loads(raw) if not isinstance(payload, dict): raise ValueError("JSON object required") # Keep library progress output off the machine-readable stdout channel. with redirect_stdout(sys.stderr): result = _render_in_process(payload) sys.stdout.write(json.dumps(result, separators=(",", ":"))) sys.stdout.flush() return 0 except Exception as exc: print(f"{type(exc).__name__}: {exc}", file=sys.stderr, flush=True) return 1 def _generate(payload: dict[str, Any]) -> dict[str, Any]: """Serialize renders and perform an exclusive lease-backed GPU handoff.""" acquired = _generation_lock.acquire(timeout=QUEUE_TIMEOUT_SECONDS) if not acquired: raise TimeoutError("local image queue is busy; retry shortly") claimed = False _set_state(busy=True, phase="claiming-gpu", last_error=None) try: if _lease_owner() != LEASE_IDLE_OWNER: owner = _lease_owner() raise RuntimeError(f"local image GPU unavailable while titan-24 owner is {owner}") _set_state(phase="checking-interactive-lane") _ensure_interactive_lane_idle() claimed = _change_owner(LEASE_IDLE_OWNER, LEASE_IMAGE_OWNER) if not claimed: owner = _lease_owner() raise RuntimeError(f"local image GPU unavailable while titan-24 owner is {owner}") result = _render(payload) _set_state(phase="releasing-flux") if _lease_owner() == LEASE_IMAGE_OWNER: if not _change_owner(LEASE_IMAGE_OWNER, LEASE_IDLE_OWNER): raise RuntimeError("GPU ownership changed while releasing the image lane") return result except Exception as exc: _set_state(last_error=f"{type(exc).__name__}: {exc}") raise finally: # If Wolf has moved the lease to wolf-draining, never overwrite that # transition. Ariadne waits for busy=false before using the GPU. if claimed: try: if _lease_owner() == LEASE_IMAGE_OWNER: _change_owner(LEASE_IMAGE_OWNER, LEASE_IDLE_OWNER) except Exception as exc: _set_state(last_error=f"restore failed: {type(exc).__name__}: {exc}") _set_state(busy=False, phase="idle") _generation_lock.release() class Handler(BaseHTTPRequestHandler): """Internal JSON interface used only by the owner image broker.""" server_version = "HermesLocalImage/1" def _json(self, status: int, value: dict[str, Any]) -> None: body = json.dumps(value, separators=(",", ":")).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", "no-store") self.end_headers() self.wfile.write(body) def do_GET(self) -> None: # noqa: N802 if self.path != "/health": self._json(404, {"success": False, "error": "not found"}) return state = _state_snapshot() try: owner = _lease_owner() except Exception: owner = "unavailable" try: activity = _gpu_activity_snapshot() _set_state(**{key: value for key, value in activity.items() if key != "external_gpu_processes"}) state = _state_snapshot() except Exception as exc: activity = {"interactive_active": True, "gpu_guard_reason": f"GPU guard unavailable: {exc}"} _set_state(**activity) state = _state_snapshot() self._json( 200, { "success": True, "available": ( owner == LEASE_IDLE_OWNER and not state["busy"] and not activity["interactive_active"] ), "gpu_owner": owner, "model": "flux-2-klein-4b-local", **state, }, ) def do_POST(self) -> None: # noqa: N802 if self.path != "/v1/images/generations": self._json(404, {"success": False, "error": "not found"}) return try: length = int(self.headers.get("Content-Length", "0")) except ValueError: length = 0 if length <= 0 or length > MAX_BODY_BYTES: self._json(413, {"success": False, "error": "invalid request size"}) return try: payload = json.loads(self.rfile.read(length)) if not isinstance(payload, dict): raise ValueError("JSON object required") result = _generate(payload) self._json(200, result) except ValueError as exc: self._json(400, {"success": False, "error": str(exc)}) except TimeoutError as exc: self._json(503, {"success": False, "error": str(exc)}) except Exception as exc: self._json( 503, {"success": False, "error": f"local image generation failed: {type(exc).__name__}: {exc}"}, ) def log_message(self, format_string: str, *args: Any) -> None: print(f"local-image {self.address_string()} {format_string % args}", flush=True) if __name__ == "__main__": if sys.argv[1:] == ["--render-worker"]: raise SystemExit(_run_render_worker()) _recover_stale_image_lease() ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()