gpu(titan-24): protect desktop image sharing

This commit is contained in:
jenkins 2026-08-11 05:52:49 -03:00
parent 48aeb4873c
commit df310df1b5
3 changed files with 185 additions and 4 deletions

View File

@ -10,6 +10,7 @@ import json
import os import os
import ssl import ssl
import threading import threading
import time
from datetime import UTC, datetime from datetime import UTC, datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
@ -35,6 +36,23 @@ LEASE_IDLE_OWNER = os.environ.get("LEASE_IDLE_OWNER", "hermes")
LEASE_IMAGE_OWNER = os.environ.get("LEASE_IMAGE_OWNER", "hermes-image") 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))) 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")) QUEUE_TIMEOUT_SECONDS = float(os.environ.get("HERMES_LOCAL_IMAGE_QUEUE_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_HOST = os.environ.get("KUBERNETES_SERVICE_HOST", "kubernetes.default.svc")
API_PORT = os.environ.get("KUBERNETES_SERVICE_PORT_HTTPS", "443") API_PORT = os.environ.get("KUBERNETES_SERVICE_PORT_HTTPS", "443")
TOKEN_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount/token") TOKEN_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount/token")
@ -56,9 +74,101 @@ _state: dict[str, Any] = {
"phase": "idle", "phase": "idle",
"last_error": None, "last_error": None,
"last_model": 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: def _set_state(**values: Any) -> None:
"""Update health state without exposing prompts or image content.""" """Update health state without exposing prompts or image content."""
with _state_lock: with _state_lock:
@ -203,6 +313,11 @@ def _generate(payload: dict[str, Any]) -> dict[str, Any]:
claimed = False claimed = False
_set_state(busy=True, phase="claiming-gpu", last_error=None) _set_state(busy=True, phase="claiming-gpu", last_error=None)
try: 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) claimed = _change_owner(LEASE_IDLE_OWNER, LEASE_IMAGE_OWNER)
if not claimed: if not claimed:
owner = _lease_owner() owner = _lease_owner()
@ -252,11 +367,23 @@ class Handler(BaseHTTPRequestHandler):
owner = _lease_owner() owner = _lease_owner()
except Exception: except Exception:
owner = "unavailable" 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( self._json(
200, 200,
{ {
"success": True, "success": True,
"available": owner == LEASE_IDLE_OWNER and not state["busy"], "available": (
owner == LEASE_IDLE_OWNER
and not state["busy"]
and not activity["interactive_active"]
),
"gpu_owner": owner, "gpu_owner": owner,
"model": "flux-2-klein-4b-local", "model": "flux-2-klein-4b-local",
**state, **state,

View File

@ -20,8 +20,8 @@ spec:
app: hermes-local-image app: hermes-local-image
annotations: annotations:
ai.bstein.dev/model: black-forest-labs/FLUX.2-klein-4B ai.bstein.dev/model: black-forest-labs/FLUX.2-klein-4B
ai.bstein.dev/gpu: titan-24 lease-shared image and Wolf lane ai.bstein.dev/gpu: titan-24 lease-shared image, desktop, and Wolf lane
ai.bstein.dev/config-rev: "20260811-flux2-klein-image-only" ai.bstein.dev/config-rev: "20260811-flux2-klein-interactive-guard"
spec: spec:
serviceAccountName: hermes-gpu-runtime serviceAccountName: hermes-gpu-runtime
runtimeClassName: nvidia runtimeClassName: nvidia
@ -43,7 +43,7 @@ spec:
sizeLimit: 2Gi sizeLimit: 2Gi
containers: containers:
- name: local-image - name: local-image
image: registry.bstein.dev/bstein/hermes-local-image@sha256:316d123cc48509a53fb32c26bfb384a3ed4d934cb334d73faa0840a62e047e65 image: registry.bstein.dev/bstein/hermes-local-image@sha256:769a16f2b56b1de401f7dc4adafb522d39c8509d8de741668f69ee7a206502f7
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
ports: ports:
- name: local-image - name: local-image
@ -63,6 +63,20 @@ spec:
value: hermes value: hermes
- name: LEASE_IMAGE_OWNER - name: LEASE_IMAGE_OWNER
value: hermes-image value: hermes-image
- name: HERMES_LOCAL_IMAGE_GPU_ACTIVITY_URL
value: http://nvidia-process-exporter.monitoring.svc.cluster.local:9401/metrics
- name: HERMES_LOCAL_IMAGE_GPU_ACTIVITY_NODE
value: titan-24
# The idle desktop stack and Wolf daemon use about 1.9 GiB and
# 3% SM. Active desktop/game sessions cross one of these bounds.
- name: HERMES_LOCAL_IMAGE_GPU_ACTIVE_SM_PERCENT
value: "8"
- name: HERMES_LOCAL_IMAGE_GPU_MAX_EXTERNAL_MEMORY_BYTES
value: "3221225472"
- name: HERMES_LOCAL_IMAGE_GPU_ACTIVITY_SAMPLES
value: "3"
- name: HERMES_LOCAL_IMAGE_GPU_ACTIVITY_SAMPLE_INTERVAL
value: "1"
- name: HF_HOME - name: HF_HOME
value: /models/huggingface value: /models/huggingface
- name: HOME - name: HOME

View File

@ -608,6 +608,14 @@ def test_local_flux_runtime_and_gpu_handoff_are_flux_managed():
assert model_env["HERMES_LOCAL_IMAGE_REVISION"] == ( assert model_env["HERMES_LOCAL_IMAGE_REVISION"] == (
"e7b7dc27f91deacad38e78976d1f2b499d76a294" "e7b7dc27f91deacad38e78976d1f2b499d76a294"
) )
assert model_env["HERMES_LOCAL_IMAGE_GPU_ACTIVITY_NODE"] == "titan-24"
assert model_env["HERMES_LOCAL_IMAGE_GPU_ACTIVE_SM_PERCENT"] == "8"
assert model_env["HERMES_LOCAL_IMAGE_GPU_MAX_EXTERNAL_MEMORY_BYTES"] == (
"3221225472"
)
assert "nvidia-process-exporter.monitoring.svc.cluster.local" in model_env[
"HERMES_LOCAL_IMAGE_GPU_ACTIVITY_URL"
]
models_volume = next(item for item in pod["volumes"] if item["name"] == "models") models_volume = next(item for item in pod["volumes"] if item["name"] == "models")
assert models_volume["persistentVolumeClaim"]["claimName"] == ( assert models_volume["persistentVolumeClaim"]["claimName"] == (
"hermes-image-models" "hermes-image-models"
@ -672,6 +680,38 @@ def test_titan20_serializes_classifier_and_local_chat_model_residency():
) )
def test_local_image_gpu_guard_distinguishes_idle_and_active_desktop(monkeypatch):
"""Idle display shells may coexist, but active desktop/game work blocks FLUX."""
source = ROOT / "dockerfiles" / "hermes-local-image-server.py"
spec = importlib.util.spec_from_file_location("hermes_local_image_server", source)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
monkeypatch.setattr(module, "GPU_ACTIVITY_NODE", "titan-24")
monkeypatch.setattr(module, "GPU_ACTIVE_SM_PERCENT", 8.0)
monkeypatch.setattr(module, "GPU_MAX_EXTERNAL_MEMORY_BYTES", 3 << 30)
idle = module._parse_gpu_activity(
'\n'.join(
[
'nvidia_process_gpu_sm_util_percent{node="titan-24",namespace="host",process="Xorg"} 0',
'nvidia_process_gpu_memory_used_bytes{node="titan-24",namespace="host",process="Xorg"} 1900000000',
'nvidia_process_gpu_sm_util_percent{node="titan-24",namespace="game-stream",process="wolf"} 3',
'nvidia_process_gpu_memory_used_bytes{node="titan-24",namespace="hermes",process="python"} 9000000000',
]
)
)
assert idle["interactive_active"] is False
assert idle["external_gpu_memory_bytes"] == 1900000000
assert idle["external_gpu_sm_percent"] == 3
active = module._parse_gpu_activity(
'nvidia_process_gpu_sm_util_percent{node="titan-24",namespace="host",process="steam"} 41\n'
)
assert active["interactive_active"] is True
assert "41%" in active["gpu_guard_reason"]
def test_chat_auth_file_mount_survives_atomic_provider_refresh(): def test_chat_auth_file_mount_survives_atomic_provider_refresh():
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0] statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
containers = statefulset["spec"]["template"]["spec"]["containers"] containers = statefulset["spec"]["template"]["spec"]["containers"]