Add GPU fan control, live telemetry, and per-profile fan curves in HyperSwap dashboard
This commit is contained in:
68
server.py
68
server.py
@@ -11,6 +11,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
import ram_optimizer
|
||||
import vram_arbitrator
|
||||
import overclock_manager
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
||||
logger = logging.getLogger("model_manager_server")
|
||||
@@ -31,6 +32,14 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.on_event("startup")
|
||||
async def on_startup():
|
||||
await vram_arbitrator.arbitrator.start()
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def on_shutdown():
|
||||
await vram_arbitrator.arbitrator.stop()
|
||||
|
||||
# Pydantic Request Models
|
||||
class SwitchRequest(BaseModel):
|
||||
model: str = Field(..., description="Name of the Ollama model to hot-swap to in VRAM", example="qwen3.8fast:latest")
|
||||
@@ -44,6 +53,17 @@ class BenchmarkRequest(BaseModel):
|
||||
iterations: Optional[int] = Field(2, description="Number of back-and-forth switch iterations to measure", example=2)
|
||||
models: Optional[List[str]] = Field(None, description="Optional pair of models to benchmark between", example=["qwen3.8fast:latest", "smtek/Qwen3.8-27B:Q2_K_XL"])
|
||||
|
||||
class OverclockApplyRequest(BaseModel):
|
||||
profile: str = Field(..., description="Profile name: ollama | comfy | balanced", example="ollama")
|
||||
|
||||
class OverclockProfileUpdate(BaseModel):
|
||||
config: Dict[str, Any] = Field(..., description="Profile settings dict", example={"power_limit_w": 370, "core_offset_mhz": 100})
|
||||
|
||||
class FanRequest(BaseModel):
|
||||
mode: str = Field("auto", description="'auto' or 'manual'", example="manual")
|
||||
percent: Optional[int] = Field(None, description="Fan speed 30-100 when mode=manual", example=70)
|
||||
speed_pct: Optional[int] = Field(None, description="Alias for percent (30-100)", example=70)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# REST API ENDPOINTS
|
||||
@@ -58,6 +78,7 @@ async def get_all_stats() -> Dict[str, Any]:
|
||||
comfy_state = await vram_arbitrator.get_comfyui_live_state()
|
||||
history = vram_arbitrator.get_switch_history()
|
||||
comfy_models = ram_optimizer.find_comfy_model_files()
|
||||
arbitrator_status = vram_arbitrator.arbitrator.get_status()
|
||||
|
||||
return {
|
||||
"timestamp": asyncio.get_event_loop().time(),
|
||||
@@ -65,10 +86,12 @@ async def get_all_stats() -> Dict[str, Any]:
|
||||
"ram": mem_stats,
|
||||
"ollama": ollama_state,
|
||||
"comfyui": comfy_state,
|
||||
"arbitrator": arbitrator_status,
|
||||
"history": history,
|
||||
"comfy_models_count": len(comfy_models),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/gpu", summary="GPU Sensors and VRAM Breakdown", tags=["Telemetry"])
|
||||
async def get_gpu_metrics() -> Dict[str, Any]:
|
||||
"""Retrieve detailed NVML sensors (utilization %, temp, power, fan, clocks, and per-process VRAM allocation)."""
|
||||
@@ -160,6 +183,51 @@ async def api_run_benchmark(req: BenchmarkRequest):
|
||||
res_str = await run_model_switch_benchmark(iterations=req.iterations or 2)
|
||||
return json.loads(res_str)
|
||||
|
||||
# ==========================================
|
||||
# OVERCLOCK MANAGEMENT
|
||||
# ==========================================
|
||||
|
||||
@app.get("/api/overclock", summary="Overclock Status & Profiles", tags=["Overclock"])
|
||||
async def api_overclock_status():
|
||||
"""Get live GPU overclock state, active profile, and all per-app profiles."""
|
||||
return overclock_manager.get_status()
|
||||
|
||||
@app.post("/api/overclock/apply", summary="Apply Overclock Profile", tags=["Overclock"])
|
||||
async def api_overclock_apply(req: OverclockApplyRequest):
|
||||
"""Apply a named overclock profile (ollama | comfy | balanced) to the GPU immediately."""
|
||||
res = overclock_manager.apply_profile(req.profile)
|
||||
if not res.get("success"):
|
||||
raise HTTPException(status_code=400, detail=res.get("error"))
|
||||
return res
|
||||
|
||||
@app.get("/api/overclock/profiles", summary="List Overclock Profiles", tags=["Overclock"])
|
||||
async def api_overclock_profiles():
|
||||
"""List all overclock profiles with their current settings."""
|
||||
return overclock_manager.get_profiles()
|
||||
|
||||
@app.post("/api/overclock/profiles/{name}", summary="Update Overclock Profile", tags=["Overclock"])
|
||||
async def api_overclock_update_profile(name: str, req: OverclockProfileUpdate):
|
||||
"""Update a profile's settings (persisted to disk)."""
|
||||
res = overclock_manager.set_profile(name, req.config)
|
||||
if not res.get("success"):
|
||||
raise HTTPException(status_code=400, detail=res.get("error"))
|
||||
return {"success": True, "profile": name, "profiles": res.get("profiles")}
|
||||
|
||||
@app.get("/api/overclock/fan", summary="Get GPU Fan Status", tags=["Overclock"])
|
||||
@app.get("/api/gpu/fan", summary="Get GPU Fan Status", tags=["Overclock"])
|
||||
async def api_get_fan_status():
|
||||
"""Get current GPU fan control mode and speed."""
|
||||
return overclock_manager.get_fan_status()
|
||||
|
||||
@app.post("/api/overclock/fan", summary="Set GPU Fan Speed", tags=["Overclock"])
|
||||
@app.post("/api/gpu/fan", summary="Set GPU Fan Speed", tags=["Overclock"])
|
||||
async def api_set_fan(req: FanRequest):
|
||||
"""Set the GPU fan to manual speed (30-100%) or back to automatic control."""
|
||||
pct = req.percent if req.percent is not None else req.speed_pct
|
||||
if req.mode == "manual" and pct is not None:
|
||||
return overclock_manager.set_fan_speed(pct)
|
||||
return overclock_manager.set_fan_auto()
|
||||
|
||||
# Mount static web UI files
|
||||
app.mount("/static", StaticFiles(directory="/home/drjones/unified-model-manager/static"), name="static")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user