|
@@ -9,7 +9,6 @@ import time
|
|
|
|
|
|
|
|
from fastapi import FastAPI, HTTPException, Request, Response
|
|
from fastapi import FastAPI, HTTPException, Request, Response
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
-from pydantic import BaseModel
|
|
|
|
|
import httpx
|
|
import httpx
|
|
|
|
|
|
|
|
from fabric.shared.schemas import Heartbeat, ModuleState
|
|
from fabric.shared.schemas import Heartbeat, ModuleState
|
|
@@ -26,22 +25,6 @@ NODE_IP = os.environ.get("FABRIC_NODE_IP", "")
|
|
|
AGENT_PORT = int(os.environ.get("FABRIC_AGENT_PORT", "7701"))
|
|
AGENT_PORT = int(os.environ.get("FABRIC_AGENT_PORT", "7701"))
|
|
|
HYPERVISOR = os.environ.get("FABRIC_HYPERVISOR", "unknown")
|
|
HYPERVISOR = os.environ.get("FABRIC_HYPERVISOR", "unknown")
|
|
|
|
|
|
|
|
-
|
|
|
|
|
-class GpuMemInfo(BaseModel):
|
|
|
|
|
- """Per-physical-GPU memory capacity entry (additive VRAM-capacity reporting)."""
|
|
|
|
|
- gpu_index: int = 0
|
|
|
|
|
- mem_total_mb: int = 0
|
|
|
|
|
- mem_used_mb: int = 0
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-class HeartbeatExt(Heartbeat):
|
|
|
|
|
- """Heartbeat + additive VRAM-capacity fields. Subclassing keeps the base
|
|
|
|
|
- payload identical for control planes that haven't been updated yet (they
|
|
|
|
|
- ignore the extra fields) while carrying per-GPU capacity to updated ones."""
|
|
|
|
|
- gpus: list[GpuMemInfo] = []
|
|
|
|
|
- vram_total_mb: int = 0
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
app = FastAPI(title=f"Fabric Agent ({NODE_ID})", version="0.1.0")
|
|
app = FastAPI(title=f"Fabric Agent ({NODE_ID})", version="0.1.0")
|
|
|
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
|
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
|
|
|
|
|
|
@@ -62,38 +45,65 @@ def _detect_ip() -> str:
|
|
|
return "127.0.0.1"
|
|
return "127.0.0.1"
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _get_gpu_mem_info() -> list[dict]:
|
|
|
|
|
- """Per-physical-GPU total/used VRAM via nvidia-smi (one entry per GPU, counted
|
|
|
|
|
- once — NOT per process). Robust if nvidia-smi is missing or fails -> returns []."""
|
|
|
|
|
|
|
+def _get_gpu_info() -> dict:
|
|
|
|
|
+ """Per-physical-GPU memory + telemetry via nvidia-smi (each GPU counted once,
|
|
|
|
|
+ NOT per process). Returns {gpus, vram_total_mb, vram_free_mb, driver_version}.
|
|
|
|
|
+ Robust if nvidia-smi is missing or fails -> zeros / empty list."""
|
|
|
|
|
+ empty = {"gpus": [], "vram_total_mb": 0, "vram_free_mb": 0, "driver_version": ""}
|
|
|
try:
|
|
try:
|
|
|
r = subprocess.run(
|
|
r = subprocess.run(
|
|
|
- ["nvidia-smi", "--query-gpu=index,memory.total,memory.used",
|
|
|
|
|
|
|
+ ["nvidia-smi",
|
|
|
|
|
+ "--query-gpu=index,name,memory.total,memory.used,utilization.gpu,temperature.gpu,power.draw,driver_version",
|
|
|
"--format=csv,noheader,nounits"],
|
|
"--format=csv,noheader,nounits"],
|
|
|
capture_output=True, text=True, timeout=5,
|
|
capture_output=True, text=True, timeout=5,
|
|
|
)
|
|
)
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- log.warning("nvidia-smi capacity query failed: %s", e)
|
|
|
|
|
- return []
|
|
|
|
|
|
|
+ log.warning("nvidia-smi query failed: %s", e)
|
|
|
|
|
+ return empty
|
|
|
if r.returncode != 0:
|
|
if r.returncode != 0:
|
|
|
- return []
|
|
|
|
|
|
|
+ return empty
|
|
|
|
|
+
|
|
|
|
|
+ def _to_int(s):
|
|
|
|
|
+ try:
|
|
|
|
|
+ return int(float(s))
|
|
|
|
|
+ except (ValueError, TypeError):
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _to_float(s):
|
|
|
|
|
+ try:
|
|
|
|
|
+ return float(s)
|
|
|
|
|
+ except (ValueError, TypeError):
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
gpus: list[dict] = []
|
|
gpus: list[dict] = []
|
|
|
|
|
+ driver = ""
|
|
|
for line in r.stdout.strip().splitlines():
|
|
for line in r.stdout.strip().splitlines():
|
|
|
parts = [p.strip() for p in line.split(",")]
|
|
parts = [p.strip() for p in line.split(",")]
|
|
|
- if len(parts) < 2:
|
|
|
|
|
|
|
+ if len(parts) < 4:
|
|
|
continue
|
|
continue
|
|
|
- try:
|
|
|
|
|
- idx = int(parts[0])
|
|
|
|
|
- total = int(float(parts[1]))
|
|
|
|
|
- except ValueError:
|
|
|
|
|
|
|
+ idx = _to_int(parts[0])
|
|
|
|
|
+ if idx is None:
|
|
|
continue
|
|
continue
|
|
|
- used = 0
|
|
|
|
|
- if len(parts) >= 3:
|
|
|
|
|
- try:
|
|
|
|
|
- used = int(float(parts[2]))
|
|
|
|
|
- except ValueError:
|
|
|
|
|
- used = 0
|
|
|
|
|
- gpus.append({"gpu_index": idx, "mem_total_mb": total, "mem_used_mb": used})
|
|
|
|
|
- return gpus
|
|
|
|
|
|
|
+ total = _to_int(parts[2]) or 0
|
|
|
|
|
+ used = _to_int(parts[3]) or 0
|
|
|
|
|
+ if len(parts) > 7 and parts[7]:
|
|
|
|
|
+ driver = parts[7]
|
|
|
|
|
+ gpus.append({
|
|
|
|
|
+ "gpu_index": idx,
|
|
|
|
|
+ "name": parts[1],
|
|
|
|
|
+ "mem_total_mb": total,
|
|
|
|
|
+ "mem_used_mb": used,
|
|
|
|
|
+ "mem_free_mb": max(total - used, 0),
|
|
|
|
|
+ "util_pct": _to_int(parts[4]) if len(parts) > 4 else None,
|
|
|
|
|
+ "temp_c": _to_int(parts[5]) if len(parts) > 5 else None,
|
|
|
|
|
+ "power_w": _to_float(parts[6]) if len(parts) > 6 else None,
|
|
|
|
|
+ })
|
|
|
|
|
+ return {
|
|
|
|
|
+ "gpus": gpus,
|
|
|
|
|
+ "vram_total_mb": sum(g["mem_total_mb"] for g in gpus),
|
|
|
|
|
+ "vram_free_mb": sum(g["mem_free_mb"] for g in gpus),
|
|
|
|
|
+ "driver_version": driver,
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_modules():
|
|
def _load_modules():
|
|
@@ -117,20 +127,22 @@ def _get_pid_vram_map() -> dict[int, int]:
|
|
|
return m
|
|
return m
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _build_heartbeat() -> HeartbeatExt:
|
|
|
|
|
|
|
+def _build_heartbeat() -> Heartbeat:
|
|
|
gpu_status = get_per_pid_vram()
|
|
gpu_status = get_per_pid_vram()
|
|
|
pid_vram = _get_pid_vram_map()
|
|
pid_vram = _get_pid_vram_map()
|
|
|
modules = []
|
|
modules = []
|
|
|
for name, rt in _runtimes.items():
|
|
for name, rt in _runtimes.items():
|
|
|
rt.update_state(pid_vram)
|
|
rt.update_state(pid_vram)
|
|
|
modules.append(rt.get_status())
|
|
modules.append(rt.get_status())
|
|
|
- gpus = _get_gpu_mem_info()
|
|
|
|
|
- return HeartbeatExt(
|
|
|
|
|
|
|
+ info = _get_gpu_info()
|
|
|
|
|
+ return Heartbeat(
|
|
|
node_id=NODE_ID, node_ip=_detect_ip(), agent_port=AGENT_PORT,
|
|
node_id=NODE_ID, node_ip=_detect_ip(), agent_port=AGENT_PORT,
|
|
|
hypervisor=HYPERVISOR, gpu_status=gpu_status, modules=modules,
|
|
hypervisor=HYPERVISOR, gpu_status=gpu_status, modules=modules,
|
|
|
uptime_s=int(time.time() - _start_time),
|
|
uptime_s=int(time.time() - _start_time),
|
|
|
- gpus=gpus,
|
|
|
|
|
- vram_total_mb=sum(g["mem_total_mb"] for g in gpus),
|
|
|
|
|
|
|
+ gpus=info["gpus"],
|
|
|
|
|
+ vram_total_mb=info["vram_total_mb"],
|
|
|
|
|
+ vram_free_mb=info["vram_free_mb"],
|
|
|
|
|
+ driver_version=info["driver_version"],
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@@ -147,9 +159,11 @@ async def list_modules():
|
|
|
async def gpu_status():
|
|
async def gpu_status():
|
|
|
from fabric.agent.gpu_monitor import get_local_gpu_summary
|
|
from fabric.agent.gpu_monitor import get_local_gpu_summary
|
|
|
summary = get_local_gpu_summary()
|
|
summary = get_local_gpu_summary()
|
|
|
- gpus = _get_gpu_mem_info()
|
|
|
|
|
- summary["gpus"] = gpus
|
|
|
|
|
- summary["vram_total_mb"] = sum(g["mem_total_mb"] for g in gpus)
|
|
|
|
|
|
|
+ info = _get_gpu_info()
|
|
|
|
|
+ summary["gpus"] = info["gpus"]
|
|
|
|
|
+ summary["vram_total_mb"] = info["vram_total_mb"]
|
|
|
|
|
+ summary["vram_free_mb"] = info["vram_free_mb"]
|
|
|
|
|
+ summary["driver_version"] = info["driver_version"]
|
|
|
return summary
|
|
return summary
|
|
|
|
|
|
|
|
@app.post("/load/{module_name}")
|
|
@app.post("/load/{module_name}")
|