Files

305 lines
9.4 KiB
Python
Raw Permalink Normal View History

"""Runtime GPU / LLM endpoint selection with DB override above env defaults."""
from __future__ import annotations
import os
from datetime import datetime, timezone
from typing import Any
from urllib.parse import urlparse
import httpx
from sqlalchemy import Column, DateTime, String, Text, select
from sqlalchemy.orm import DeclarativeBase
from db import SessionLocal, engine
GPU_UI_PORT = int(os.getenv("GPU_UI_PORT", "9000"))
LLM_PORT = int(os.getenv("LLM_PORT", "8001"))
LLM_PATH = os.getenv("LLM_PATH", "/v1")
ENV_GPU_URL = os.getenv("GPU_URL", "http://10.0.10.106:9000")
ENV_GPU_UI_URL = os.getenv("GPU_UI_URL", ENV_GPU_URL)
ENV_LLM_URL = os.getenv("LLM_URL", "http://10.0.10.106:8001/v1")
class _Base(DeclarativeBase):
pass
class SystemSetting(_Base):
__tablename__ = "system_settings"
key = Column(String(64), primary_key=True)
value = Column(Text, nullable=False, default="")
updated_at = Column(DateTime(timezone=True), nullable=True)
GPU_PRESETS: list[dict[str, Any]] = [
{
"id": "gpu-prod",
"label": "atc-gpu-prod (VM306)",
"vm": "atc-gpu-prod",
"vmid": 306,
"host": "10.0.10.106",
"gpu_ui_port": 9000,
"llm_port": 8001,
"description": "4× V100 — shared production GPU lab",
},
{
"id": "gpu-dev",
"label": "atc-gpu-dev (VM303, legacy)",
"vm": "atc-gpu-dev",
"vmid": 303,
"host": "10.0.20.106",
"gpu_ui_port": 9000,
"llm_port": 8001,
"description": "Legacy dev VM — GPU passthrough removed",
},
{
"id": "gpu-bart",
"label": "atc-gpu-bart (VM301)",
"vm": "atc-gpu-bart",
"vmid": 301,
"host": "10.0.11.66",
"gpu_ui_port": 9000,
"llm_port": 8001,
"description": "Bart GPU VM — 2× V100",
},
]
def _ensure_table() -> None:
SystemSetting.metadata.create_all(engine, tables=[SystemSetting.__table__])
def _get_setting(key: str) -> str | None:
_ensure_table()
with SessionLocal() as db:
row = db.get(SystemSetting, key)
return row.value if row else None
def _set_settings(values: dict[str, str]) -> None:
_ensure_table()
now = datetime.now(timezone.utc)
with SessionLocal() as db:
for key, value in values.items():
row = db.get(SystemSetting, key)
if row:
row.value = value
row.updated_at = now
else:
db.add(SystemSetting(key=key, value=value, updated_at=now))
db.commit()
def _clear_settings(keys: list[str]) -> None:
_ensure_table()
with SessionLocal() as db:
for key in keys:
row = db.get(SystemSetting, key)
if row:
db.delete(row)
db.commit()
def _build_urls(host: str, gpu_ui_port: int, llm_port: int) -> dict[str, str]:
host = host.strip().replace("http://", "").replace("https://", "").split("/")[0]
if ":" in host:
base_host = host.split(":")[0]
else:
base_host = host
gpu_url = f"http://{base_host}:{gpu_ui_port}"
llm_url = f"http://{base_host}:{llm_port}{LLM_PATH}"
return {
"host": base_host,
"gpu_url": gpu_url,
"gpu_ui_url": gpu_url,
"llm_url": llm_url,
}
def _env_defaults() -> dict[str, Any]:
parsed = urlparse(ENV_GPU_URL)
host = parsed.hostname or "10.0.10.106"
return {
"source": "env",
"preset_id": "env",
"label": "Environment default",
**_build_urls(host, parsed.port or GPU_UI_PORT, LLM_PORT),
"env_gpu_url": ENV_GPU_URL,
"env_llm_url": ENV_LLM_URL,
}
def get_gpu_urls() -> dict[str, str]:
"""Effective GPU/LLM URLs — DB override wins over env."""
cfg = get_gpu_config()
return {
"gpu_url": cfg["gpu_url"],
"gpu_ui_url": cfg["gpu_ui_url"],
"llm_url": cfg["llm_url"],
"host": cfg["host"],
}
def resolve_gpu_identity(gpu: dict[str, Any] | None = None) -> dict[str, Any]:
"""Canonical GPU host/VM/URLs for topology, registry links, and presentation."""
urls = get_gpu_urls()
cfg = get_gpu_config()
g = gpu or {}
host = str(g.get("ip") or g.get("host") or urls["host"]).strip()
preset_id = g.get("preset_id") or cfg.get("preset_id")
preset = next((p for p in GPU_PRESETS if p["id"] == preset_id), None)
if preset is None:
preset = next((p for p in GPU_PRESETS if p["host"] == host), None)
if preset is None:
preset = GPU_PRESETS[0]
ui_url = str(g.get("ui_url") or urls["gpu_ui_url"])
llm_url = str(g.get("vllm_url") or urls["llm_url"])
return {
"host": host,
"ip": host,
"vm": preset.get("vm") or "atc-gpu-prod",
"vmid": preset.get("vmid") or 306,
"ui_url": ui_url,
"llm_url": llm_url,
"preset_id": preset.get("id") or preset_id or "gpu-prod",
"label": preset.get("label") or cfg.get("label") or preset.get("vm"),
}
def get_gpu_config() -> dict[str, Any]:
override_host = _get_setting("gpu_host")
if not override_host:
return _env_defaults()
preset_id = _get_setting("gpu_preset_id") or "custom"
gpu_ui_port = int(_get_setting("gpu_ui_port") or GPU_UI_PORT)
llm_port = int(_get_setting("llm_port") or LLM_PORT)
urls = _build_urls(override_host, gpu_ui_port, llm_port)
preset = next((p for p in GPU_PRESETS if p["id"] == preset_id), None)
return {
"source": "override",
"preset_id": preset_id,
"label": preset["label"] if preset else f"Custom ({override_host})",
**urls,
"env_gpu_url": ENV_GPU_URL,
"env_llm_url": ENV_LLM_URL,
"updated_at": _get_setting("gpu_updated_at"),
}
def get_gpu_config_payload() -> dict[str, Any]:
cfg = get_gpu_config()
return {
"active": cfg,
"presets": GPU_PRESETS,
"defaults": _env_defaults(),
}
def save_gpu_config(
*,
preset_id: str | None = None,
host: str | None = None,
gpu_ui_port: int | None = None,
llm_port: int | None = None,
) -> dict[str, Any]:
if preset_id and preset_id != "custom":
preset = next((p for p in GPU_PRESETS if p["id"] == preset_id), None)
if not preset:
raise ValueError(f"Unknown preset: {preset_id}")
host = preset["host"]
gpu_ui_port = preset.get("gpu_ui_port", GPU_UI_PORT)
llm_port = preset.get("llm_port", LLM_PORT)
if not host:
raise ValueError("host is required for custom GPU target")
gpu_ui_port = gpu_ui_port or GPU_UI_PORT
llm_port = llm_port or LLM_PORT
now = datetime.now(timezone.utc).isoformat()
_set_settings(
{
"gpu_host": host.strip(),
"gpu_ui_port": str(gpu_ui_port),
"llm_port": str(llm_port),
"gpu_preset_id": preset_id or "custom",
"gpu_updated_at": now,
}
)
return get_gpu_config()
def reset_gpu_config() -> dict[str, Any]:
_clear_settings(["gpu_host", "gpu_ui_port", "llm_port", "gpu_preset_id", "gpu_updated_at"])
return _env_defaults()
async def test_gpu_target(
host: str | None = None,
gpu_ui_port: int | None = None,
llm_port: int | None = None,
preset_id: str | None = None,
) -> dict[str, Any]:
if preset_id and preset_id != "custom":
preset = next((p for p in GPU_PRESETS if p["id"] == preset_id), None)
if preset:
host = preset["host"]
gpu_ui_port = preset.get("gpu_ui_port", GPU_UI_PORT)
llm_port = preset.get("llm_port", LLM_PORT)
if not host:
cfg = get_gpu_config()
host = cfg["host"]
gpu_ui_port = gpu_ui_port or GPU_UI_PORT
llm_port = llm_port or LLM_PORT
urls = _build_urls(host, gpu_ui_port or GPU_UI_PORT, llm_port or LLM_PORT)
result: dict[str, Any] = {
"ok": False,
"host": urls["host"],
"gpu_url": urls["gpu_url"],
"llm_url": urls["llm_url"],
"metrics_ok": False,
"llm_ok": False,
"gpu_count": 0,
"inference_active": False,
"active_model": None,
"errors": [],
}
async with httpx.AsyncClient(timeout=8.0) as client:
try:
mr = await client.get(f"{urls['gpu_url']}/api/gpu/metrics")
if mr.status_code == 200:
result["metrics_ok"] = True
gpus = mr.json().get("current", {}).get("gpus", [])
result["gpu_count"] = len(gpus)
else:
result["errors"].append(f"metrics HTTP {mr.status_code}")
except Exception as exc:
result["errors"].append(f"metrics: {exc}")
try:
model_r = await client.get(f"{urls['gpu_url']}/api/active-model")
if model_r.status_code == 200:
md = model_r.json()
result["inference_active"] = bool(md.get("inference_active"))
result["active_model"] = md.get("name")
except Exception as exc:
result["errors"].append(f"active-model: {exc}")
try:
lr = await client.get(f"{urls['llm_url']}/models")
if lr.status_code == 200:
result["llm_ok"] = True
else:
result["errors"].append(f"llm HTTP {lr.status_code}")
except Exception as exc:
result["errors"].append(f"llm: {exc}")
result["ok"] = result["metrics_ok"] and (
result["gpu_count"] > 0 or result["inference_active"] or result["llm_ok"]
)
return result