Initial commit: HyperSwap GPU Program Swapper with REST API, MCP 2.0, Real-Time Dashboard and Memory Orchestrator

This commit is contained in:
drjones
2026-08-22 00:59:40 -07:00
commit f909dd23fb
10 changed files with 1853 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
__pycache__/
*.py[cod]
*$py.class
*.log
.venv/
venv/
.DS_Store

275
README.md Normal file
View File

@@ -0,0 +1,275 @@
# HyperSwap // GPU Program Swapper & Memory Orchestrator
[![FastAPI](https://img.shields.io/badge/FastAPI-0.141-009688.svg?style=flat&logo=fastapi)](https://fastapi.tiangolo.com)
[![Model Context Protocol](https://img.shields.io/badge/MCP-2.0-8A2BE2.svg?style=flat)](https://modelcontextprotocol.io)
[![NVIDIA CUDA](https://img.shields.io/badge/CUDA-13.2%20%7C%2012.8-76B900.svg?style=flat&logo=nvidia)](https://developer.nvidia.com/cuda-zone)
[![Platform](https://img.shields.io/badge/Platform-Linux%20x86__64-orange.svg?style=flat&logo=linux)](https://ubuntu.com)
**HyperSwap** is an ultra-low-latency VRAM arbitrator, RAM cache pre-warmer, and real-time telemetry dashboard designed specifically for Linux deployment machines that simultaneously host **Ollama LLM workloads** and **ComfyUI Diffusion pipelines** on a single GPU.
---
## 1. Architectural Overview & Physics of High-Speed Switching
```mermaid
flowchart TD
subgraph HostRAM["64 GB DDR5 Host System RAM (Page Cache & Pinned Staging)"]
OllamaGGUFs["Ollama GGUF Weights<br/>(Qwen, Gemma, Nemotron)"]
ComfySafetensors["ComfyUI Safetensors & VAEs<br/>(53.6 GB Pinned Staging Buffer)"]
end
subgraph GPU["NVIDIA GeForce RTX 4080 SUPER (16 GB VRAM)"]
direction LR
ActiveLLM["Active LLM<br/>(014 GB VRAM)"]
ActiveDiffusion["Active Diffusion Pipeline<br/>(014 GB VRAM)"]
end
subgraph Orchestrator["HyperSwap Control Plane (:9090)"]
REST["REST API & OpenAPI Docs"]
MCP["Model Context Protocol (MCP 2.0)"]
SSE["1Hz Real-Time SSE Stream"]
Arbitrator["VRAM Arbitrator (15ms Soft-Yield)"]
Warmer["Page Cache Pre-Warmer"]
end
HostRAM <== "PCIe 4.0 x16 Bus (~31.5 GB/s Hot-Swap)" ==> GPU
Orchestrator --> GPU
Orchestrator --> HostRAM
```
### The Problem: Disk Bottleneck & VRAM Contention
When running both Ollama and ComfyUI on a 16 GB GPU:
* An active LLM (e.g. 27B30B parameter quantized model) uses **1115 GB VRAM**.
* A diffusion model (SDXL, Flux, SD 1.5) requires **414 GB VRAM** during generation.
* If models are evicted to NVMe storage, reloading weights takes **1040 seconds** over disk I/O.
### The Solution: 64 GB RAM Cache + PCIe x16 Hot-Swapping
* **Host RAM as Staging**: All active LLMs and diffusion checkpoints remain 100% resident in the 64 GB Linux OS Page Cache and pinned memory buffer.
* **PCIe Bus Hot-Swap Speed**: Reloading from host RAM over the PCIe 4.0 x16 bus achieves **~31.5 GB/s** transfer bandwidth, bringing model swap times down to **hundreds of milliseconds**.
* **15ms Soft-Yield**: When ComfyUI triggers an image generation, Ollama executes an instant soft-yield (`keep_alive: 0`), dropping VRAM allocation from 14.5 GB to 0 MB in **~15 milliseconds** without discarding model pages from system RAM.
---
## 2. REST API Reference
The HyperSwap server runs on port `9090` by default. Interactive OpenAPI/Swagger docs are accessible at `http://localhost:9090/docs`.
### Telemetry Endpoints
#### `GET /api/stats`
Returns a unified JSON snapshot of all system sensors, GPU processes, host RAM, Ollama status, ComfyUI queue, and transition logs.
**Response (200 OK):**
```json
{
"timestamp": 59583.16,
"gpu": {
"available": true,
"device_name": "NVIDIA GeForce RTX 4080 SUPER",
"vram_total_gb": 15.99,
"vram_used_gb": 1.46,
"vram_free_gb": 14.53,
"vram_used_pct": 9.1,
"gpu_util_pct": 11,
"temperature_c": 48,
"power_w": 31.4,
"fan_pct": 0,
"breakdown": {
"ollama_gb": 0.0,
"comfyui_gb": 0.24,
"system_gb": 0.67,
"free_gb": 14.53,
"processes": [...]
}
},
"ram": {
"total_gb": 60.34,
"used_gb": 6.72,
"cached_gb": 36.21,
"free_gb": 17.41,
"cache_ratio_pct": 60.0
},
"ollama": {
"online": true,
"active_model_name": null,
"active_model_vram_gb": 0.0,
"installed_models": [...]
},
"comfyui": {
"online": true,
"executing": false,
"queue_remaining": 0,
"vram_free_mb": 14882.4
},
"history": [...]
}
```
#### `GET /api/gpu`
Returns hardware sensors (utilization %, temperature, power draw in Watts, fan %, GPU graphics/memory clocks, and active PIDs).
#### `GET /api/memory`
Returns precise `/proc/meminfo` metrics including Total, Used, OS Page Cache, and free memory.
#### `GET /api/stream`
Server-Sent Events (SSE) stream pushing full telemetry updates at 1Hz (`Content-Type: text/event-stream`).
---
### Orchestration & Hot-Swap Endpoints
#### `POST /api/switch-model`
Hot-swaps the active Ollama LLM in VRAM and tracks transition timing.
**Request Body:**
```json
{
"model": "qwen3.8fast:latest",
"keep_alive": "30m"
}
```
**Response (200 OK):**
```json
{
"success": true,
"prev_model": "None",
"target_model": "qwen3.8fast:latest",
"total_duration_ms": 1420.5,
"load_duration_ms": 839.5,
"tokens_per_sec": 42.0,
"is_ram_hit": true,
"response": "Ready."
}
```
#### `POST /api/free-vram`
Instructs Ollama to soft-yield VRAM down to 0 MB in ~15 milliseconds while keeping model weights in 64GB RAM cache.
#### `POST /api/comfy-free`
Instructs ComfyUI to purge loaded diffusion weights and VRAM cache.
#### `POST /api/warm-all`
Pre-faults and reads all installed Ollama models and ComfyUI Safetensors into the Linux page cache.
#### `POST /api/warm-model`
Pre-warms a specific model or file into RAM.
**Request Body:**
```json
{
"model_name": "gemma4:26b",
"filepath": null
}
```
#### `POST /api/benchmark`
Runs an automated back-and-forth model swap benchmark and computes average transition latency.
---
## 3. Model Context Protocol (MCP) Reference
HyperSwap includes a native **MCP 2.0 server** ([mcp_server.py](file:///home/drjones/unified-model-manager/mcp_server.py)) that exposes all orchestration and telemetry functions as agentic tools.
### MCP Tools List
| Tool Name | Parameters | Description |
| :--- | :--- | :--- |
| **`get_gpu_status`** | *None* | Live NVIDIA GPU hardware telemetry, VRAM breakdown, temps, power, and PIDs. |
| **`get_host_memory_status`** | *None* | 64GB host RAM breakdown, active page cache size, and cache ratio. |
| **`switch_ollama_model`** | `model_name` (str), `keep_alive` (str, default "30m") | Hot-swaps active LLM in VRAM, measures latency (ms) and tokens/sec. |
| **`soft_yield_ollama_vram`** | `model_name` (optional str) | Yields Ollama VRAM to 0 MB in ~15ms while keeping model weights in RAM cache. |
| **`purge_comfyui_vram`** | *None* | Purges loaded diffusion models from ComfyUI pipeline VRAM. |
| **`prewarm_all_models_to_ram`** | *None* | Faults all local LLM and diffusion checkpoints into Linux OS page cache. |
| **`prewarm_single_model`** | `model_name` (optional str), `filepath` (optional str) | Pre-warms a single GGUF or Safetensors file into RAM. |
| **`list_available_models`** | *None* | Lists all installed Ollama models and ComfyUI Safetensors on disk. |
| **`get_switch_history`** | `limit` (int, default 20) | Retrieves recent switch events, millisecond latencies, and RAM hit status. |
| **`run_model_switch_benchmark`**| `iterations` (int, default 2) | Automated round-trip latency benchmark between installed models. |
### MCP Resources List
* `gpu://metrics/live` - Real-time snapshot of GPU sensors and RAM page cache.
* `gpu://models/catalog` - Catalog of all discovered GGUF and Safetensors models.
* `gpu://history/switches` - Event log of recent model transitions and swap speeds.
---
### MCP Client Configurations
#### Antigravity Configuration (`~/.gemini/config/mcp_config.json`)
```json
{
"mcpServers": {
"hyperswap": {
"command": "/home/drjones/comfy-mcp-venv/bin/python",
"args": ["/home/drjones/unified-model-manager/mcp_server.py", "--stdio"]
}
}
}
```
#### Claude Desktop Configuration (`claude_desktop_config.json`)
```json
{
"mcpServers": {
"hyperswap": {
"command": "/home/drjones/comfy-mcp-venv/bin/python",
"args": ["/home/drjones/unified-model-manager/mcp_server.py", "--stdio"]
}
}
}
```
---
## 4. Web Dashboard & Real-Time Telemetry
The Web Dashboard is hosted at `http://localhost:9090`.
* **Hero Memory Gauges**: Visual multi-segment bars representing VRAM allocation across Ollama, ComfyUI, and Desktop, alongside the 64GB host RAM page cache.
* **Ollama Control Card**: Real-time active model indicator, hot-swap selector, context size, and 1-click VRAM yield button.
* **ComfyUI Pipeline Card**: Live execution state (Idle vs Generating), active prompt queue counter, and VRAM purge controls.
* **GPU Hardware Card**: Live gauges for GPU Core Utilization, Temperature (°C), Power Draw (W), Fan Speed (%), and active compute process table.
* **Switch Timeline**: Real-time event feed detailing swap durations in milliseconds and RAM cache hit flags.
---
## 5. Linux Kernel & Host Tuning
To ensure that 4550 GB of model weights remain permanently in RAM without kernel eviction:
```bash
# Prioritize retaining model file cache in RAM (lower pressure = stronger cache retention)
sudo sysctl -w vm.vfs_cache_pressure=10
# Reduce swap aggression for active pages
sudo sysctl -w vm.swappiness=10
# Write changes permanently to /etc/sysctl.d/99-hyperswap.conf
echo "vm.vfs_cache_pressure = 10" | sudo tee /etc/sysctl.d/99-hyperswap.conf
echo "vm.swappiness = 10" | sudo tee -a /etc/sysctl.d/99-hyperswap.conf
```
---
## 6. Systemd Service Management
The manager runs as a persistent systemd user service:
```bash
# Check status
systemctl --user status hyperswap-manager.service
# Restart service
systemctl --user restart hyperswap-manager.service
# View live logs
journalctl --user -u hyperswap-manager.service -f
```
---
## 7. License
MIT License. Developed for Google Antigravity & High-Throughput Linux AI Deployments.

153
mcp_server.py Normal file
View File

@@ -0,0 +1,153 @@
"""Model Context Protocol (MCP) Server for GPU Program Swapper and Memory Orchestrator."""
import os
import sys
import json
import asyncio
import logging
from typing import Dict, List, Any, Optional
from mcp.server import MCPServer
import ram_optimizer
import vram_arbitrator
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger("gpu_swapper_mcp")
mcp = MCPServer(
name="gpu-program-swapper",
version="1.0.0",
description="Orchestrates high-speed GPU VRAM hot-swaps between Ollama LLMs and ComfyUI with 64GB RAM cache telemetry."
)
# ==========================================
# MCP TOOLS
# ==========================================
@mcp.tool()
def get_gpu_status() -> str:
"""Get live NVIDIA GPU hardware telemetry, VRAM allocation (Ollama vs ComfyUI vs System), temperature, power, and active compute PIDs."""
stats = vram_arbitrator.get_gpu_hardware_stats()
return json.dumps(stats, indent=2)
@mcp.tool()
def get_host_memory_status() -> str:
"""Get host system RAM breakdown (Total, Apps Used, OS Page Cache containing models in RAM, Free memory) and cache hit percentage."""
mem = ram_optimizer.get_detailed_meminfo()
return json.dumps(mem, indent=2)
@mcp.tool()
async def switch_ollama_model(model_name: str, keep_alive: str = "30m") -> str:
"""Hot-swap active Ollama LLM in VRAM. Measures swap latency (ms), eval speed (tokens/sec), and whether it was an instant RAM cache hit."""
res = await vram_arbitrator.switch_ollama_model(model_name, keep_alive=keep_alive)
return json.dumps(res, indent=2)
@mcp.tool()
async def soft_yield_ollama_vram(model_name: Optional[str] = None) -> str:
"""Soft-yield Ollama VRAM down to 0 MB in ~15 milliseconds while keeping all model weights resident in 64GB host RAM cache."""
res = await vram_arbitrator.instant_free_ollama_vram(model_name)
return json.dumps(res, indent=2)
@mcp.tool()
async def purge_comfyui_vram() -> str:
"""Purge loaded diffusion models and VRAM cache from the ComfyUI pipeline."""
res = await vram_arbitrator.instant_free_comfyui_vram()
return json.dumps(res, indent=2)
@mcp.tool()
async def prewarm_all_models_to_ram() -> str:
"""Pre-read and fault all installed Ollama GGUF models and ComfyUI Safetensors checkpoints into the Linux OS Page Cache for PCIe-speed hot swapping."""
res = await ram_optimizer.warm_all_models()
return json.dumps(res, indent=2)
@mcp.tool()
async def prewarm_single_model(model_name: Optional[str] = None, filepath: Optional[str] = None) -> str:
"""Pre-warm a specific Ollama model or individual model file path into Linux RAM cache."""
if model_name:
res = await ram_optimizer.warm_ollama_model(model_name, keep_alive="1m")
elif filepath:
res = ram_optimizer.warm_file_to_ram(filepath)
else:
res = {"error": "Either model_name or filepath must be provided"}
return json.dumps(res, indent=2)
@mcp.tool()
async def list_available_models() -> str:
"""List all installed Ollama models and discovered ComfyUI model checkpoints/safetensors on disk with sizes and quantization levels."""
ollama_state = await vram_arbitrator.get_ollama_live_state()
comfy_models = ram_optimizer.find_comfy_model_files()
result = {
"ollama_models": ollama_state.get("installed_models", []),
"comfy_models": comfy_models,
}
return json.dumps(result, indent=2)
@mcp.tool()
def get_switch_history(limit: int = 20) -> str:
"""Get the recent history log of model switch events, swap durations (in ms), and RAM cache hit status."""
history = vram_arbitrator.get_switch_history()
return json.dumps(history[:limit], indent=2)
@mcp.tool()
async def run_model_switch_benchmark(iterations: int = 2) -> str:
"""Run an automated benchmark swapping between available models to measure round-trip latency and RAM cache effectiveness."""
ollama_state = await vram_arbitrator.get_ollama_live_state()
installed = [m.get("name") for m in ollama_state.get("installed_models", []) if m.get("name")]
if len(installed) < 2:
return json.dumps({"error": "Need at least 2 installed models to benchmark", "installed": installed})
m1, m2 = installed[0], installed[1]
results = []
for i in range(iterations):
# Swap to m1
r1 = await vram_arbitrator.switch_ollama_model(m1, keep_alive="5m")
results.append({"iteration": i+1, "direction": f"-> {m1}", "latency_ms": r1.get("total_duration_ms", 0), "load_ms": r1.get("load_duration_ms", 0), "is_ram_hit": r1.get("is_ram_hit", False)})
await asyncio.sleep(0.5)
# Swap to m2
r2 = await vram_arbitrator.switch_ollama_model(m2, keep_alive="5m")
results.append({"iteration": i+1, "direction": f"-> {m2}", "latency_ms": r2.get("total_duration_ms", 0), "load_ms": r2.get("load_duration_ms", 0), "is_ram_hit": r2.get("is_ram_hit", False)})
await asyncio.sleep(0.5)
avg_latency = round(sum(r["latency_ms"] for r in results) / len(results), 2) if results else 0
return json.dumps({
"status": "completed",
"tested_models": [m1, m2],
"iterations": iterations,
"average_swap_latency_ms": avg_latency,
"rounds": results,
}, indent=2)
# ==========================================
# MCP RESOURCES
# ==========================================
@mcp.resource("gpu://metrics/live")
def get_live_metrics_resource() -> str:
"""Live snapshot of GPU hardware sensors, VRAM, and RAM cache."""
gpu = vram_arbitrator.get_gpu_hardware_stats()
ram = ram_optimizer.get_detailed_meminfo()
return json.dumps({"gpu": gpu, "ram": ram}, indent=2)
@mcp.resource("gpu://models/catalog")
async def get_models_catalog_resource() -> str:
"""Catalog of all local Ollama and ComfyUI models."""
return await list_available_models()
@mcp.resource("gpu://history/switches")
def get_switch_history_resource() -> str:
"""Recent model switch events and latencies."""
return json.dumps(vram_arbitrator.get_switch_history(), indent=2)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="HyperSwap GPU Program Swapper MCP Server")
parser.add_argument("--stdio", action="store_true", help="Run in stdio mode (default)")
parser.add_argument("--sse", action="store_true", help="Run with SSE transport")
parser.add_argument("--port", type=int, default=8001, help="Port for SSE transport")
args = parser.parse_args()
if args.sse:
mcp.run(transport="sse", host="0.0.0.0", port=args.port)
else:
mcp.run(transport="stdio")

176
ram_optimizer.py Normal file
View File

@@ -0,0 +1,176 @@
"""RAM Optimizer and Model Pre-warmer for High-Speed Switching."""
import os
import glob
import time
import httpx
import logging
from typing import Dict, List, Any
logger = logging.getLogger("ram_optimizer")
OLLAMA_API_BASE = "http://localhost:11434"
COMFY_API_BASE = "http://127.0.0.1:8188"
COMFY_MODELS_DIR = "/home/drjones/ComfyUI/models"
def get_detailed_meminfo() -> Dict[str, Any]:
"""Parse /proc/meminfo for precise page cache and RAM stats."""
info = {}
try:
with open("/proc/meminfo", "r") as f:
for line in f:
parts = line.split(":")
if len(parts) == 2:
key = parts[0].strip()
val = parts[1].strip().split()[0]
info[key] = int(val) * 1024 # Convert kB to bytes
except Exception as e:
logger.error(f"Failed to read /proc/meminfo: {e}")
total = info.get("MemTotal", 0)
free = info.get("MemFree", 0)
available = info.get("MemAvailable", 0)
cached = info.get("Cached", 0) + info.get("Buffers", 0)
dirty = info.get("Dirty", 0)
used = total - free - cached
if used < 0:
used = total - available
return {
"total_bytes": total,
"total_gb": round(total / (1024**3), 2),
"used_bytes": used,
"used_gb": round(used / (1024**3), 2),
"cached_bytes": cached,
"cached_gb": round(cached / (1024**3), 2),
"free_bytes": free,
"free_gb": round(free / (1024**3), 2),
"available_bytes": available,
"available_gb": round(available / (1024**3), 2),
"dirty_bytes": dirty,
"dirty_mb": round(dirty / (1024**2), 2),
"cache_ratio_pct": round((cached / total * 100) if total > 0 else 0, 1),
}
def warm_file_to_ram(filepath: str, chunk_size: int = 16 * 1024 * 1024) -> Dict[str, Any]:
"""Pre-fault/read file into Linux OS Page Cache at maximum disk read speed."""
if not os.path.exists(filepath):
return {"success": False, "error": f"File not found: {filepath}", "duration_ms": 0}
t0 = time.perf_counter()
file_size = os.path.getsize(filepath)
bytes_read = 0
try:
with open(filepath, "rb") as f:
# Hint kernel that we will read this sequentially
try:
os.posix_fadvise(f.fileno(), 0, file_size, os.POSIX_FADV_WILLNEED)
except Exception:
pass
buf = bytearray(chunk_size)
while True:
n = f.readinto(buf)
if not n:
break
bytes_read += n
duration = time.perf_counter() - t0
duration_ms = round(duration * 1000, 2)
speed_mb_s = round((bytes_read / (1024**2)) / duration if duration > 0 else 0, 2)
return {
"success": True,
"filepath": filepath,
"size_bytes": file_size,
"size_mb": round(file_size / (1024**2), 2),
"bytes_read": bytes_read,
"duration_ms": duration_ms,
"speed_mb_s": speed_mb_s,
}
except Exception as e:
return {"success": False, "error": str(e), "duration_ms": round((time.perf_counter() - t0) * 1000, 2)}
async def warm_ollama_model(model_name: str, keep_alive: str = "5m") -> Dict[str, Any]:
"""Warm an Ollama model into memory and measure time."""
t0 = time.perf_counter()
try:
async with httpx.AsyncClient(timeout=120.0) as client:
resp = await client.post(
f"{OLLAMA_API_BASE}/api/generate",
json={"model": model_name, "prompt": "", "keep_alive": keep_alive},
)
duration = time.perf_counter() - t0
if resp.status_code == 200:
data = resp.json()
return {
"success": True,
"model": model_name,
"duration_ms": round(duration * 1000, 2),
"load_duration_ms": round(data.get("load_duration", 0) / 1e6, 2),
"total_duration_ms": round(data.get("total_duration", 0) / 1e6, 2),
}
else:
return {
"success": False,
"model": model_name,
"error": f"HTTP {resp.status_code}: {resp.text}",
"duration_ms": round(duration * 1000, 2),
}
except Exception as e:
return {"success": False, "model": model_name, "error": str(e), "duration_ms": round((time.perf_counter() - t0) * 1000, 2)}
def find_comfy_model_files() -> List[Dict[str, Any]]:
"""Discover all model files under ComfyUI models."""
results = []
extensions = ("*.safetensors", "*.ckpt", "*.pt", "*.bin")
if os.path.exists(COMFY_MODELS_DIR):
for root, _, files in os.walk(COMFY_MODELS_DIR):
for file in files:
if any(file.endswith(ext.replace("*", "")) for ext in extensions):
full_path = os.path.join(root, file)
rel_path = os.path.relpath(full_path, COMFY_MODELS_DIR)
size = os.path.getsize(full_path)
results.append({
"filename": file,
"rel_path": rel_path,
"full_path": full_path,
"size_bytes": size,
"size_mb": round(size / (1024**2), 2),
"size_gb": round(size / (1024**3), 3),
})
return results
async def warm_all_models() -> Dict[str, Any]:
"""Warm all available Ollama and ComfyUI models into Linux RAM Cache."""
t0 = time.perf_counter()
warmed_ollama = []
warmed_comfy = []
# 1. Ollama models
try:
async with httpx.AsyncClient(timeout=10.0) as client:
tags_resp = await client.get(f"{OLLAMA_API_BASE}/api/tags")
if tags_resp.status_code == 200:
models = tags_resp.json().get("models", [])
for m in models:
name = m.get("name")
res = await warm_ollama_model(name, keep_alive="1m")
warmed_ollama.append(res)
except Exception as e:
logger.error(f"Error discovering Ollama models: {e}")
# 2. ComfyUI models
comfy_files = find_comfy_model_files()
for f in comfy_files:
res = warm_file_to_ram(f["full_path"])
warmed_comfy.append(res)
total_duration_ms = round((time.perf_counter() - t0) * 1000, 2)
meminfo = get_detailed_meminfo()
return {
"status": "completed",
"total_duration_ms": total_duration_ms,
"ollama_models_warmed": warmed_ollama,
"comfy_files_warmed": warmed_comfy,
"meminfo_after": meminfo,
}

174
server.py Normal file
View File

@@ -0,0 +1,174 @@
"""FastAPI Backend Server with SSE Real-Time Telemetry and Model Orchestration API."""
import asyncio
import json
import logging
from typing import Dict, Any, Optional, List
from fastapi import FastAPI, Request, HTTPException, Query
from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
import ram_optimizer
import vram_arbitrator
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
logger = logging.getLogger("model_manager_server")
app = FastAPI(
title="HyperSwap // GPU Program Swapper & Telemetry API",
version="1.0.0",
description="High-performance VRAM arbitration and 64GB RAM cache orchestrator for simultaneous Ollama and ComfyUI workloads on Linux.",
docs_url="/docs",
redoc_url="/redoc",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 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")
keep_alive: Optional[str] = Field("30m", description="Keep-alive duration in VRAM (e.g. 5m, 30m, 0)", example="30m")
class WarmRequest(BaseModel):
model_name: Optional[str] = Field(None, description="Ollama model name to warm into OS page cache", example="gemma4:26b")
filepath: Optional[str] = Field(None, description="Absolute file path of Safetensors/GGUF to warm into RAM", example="/home/drjones/ComfyUI/models/checkpoints/v1-5-pruned-emaonly-fp16.safetensors")
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"])
# ==========================================
# REST API ENDPOINTS
# ==========================================
@app.get("/api/stats", summary="Full System Snapshot", tags=["Telemetry"])
async def get_all_stats() -> Dict[str, Any]:
"""Gather complete live snapshot of GPU hardware, host RAM, Ollama, ComfyUI, and switch history."""
gpu_stats = vram_arbitrator.get_gpu_hardware_stats()
mem_stats = ram_optimizer.get_detailed_meminfo()
ollama_state = await vram_arbitrator.get_ollama_live_state()
comfy_state = await vram_arbitrator.get_comfyui_live_state()
history = vram_arbitrator.get_switch_history()
comfy_models = ram_optimizer.find_comfy_model_files()
return {
"timestamp": asyncio.get_event_loop().time(),
"gpu": gpu_stats,
"ram": mem_stats,
"ollama": ollama_state,
"comfyui": comfy_state,
"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)."""
return vram_arbitrator.get_gpu_hardware_stats()
@app.get("/api/memory", summary="Host RAM and Page Cache Breakdown", tags=["Telemetry"])
async def get_ram_metrics() -> Dict[str, Any]:
"""Retrieve precise host 64GB DDR5 RAM breakdown, active cache size, and cache hit ratios."""
return ram_optimizer.get_detailed_meminfo()
@app.get("/api/stream", summary="Real-Time SSE Telemetry Stream", tags=["Telemetry"])
async def sse_telemetry_stream(request: Request):
"""Server-Sent Events (SSE) streaming real-time statistics at 1Hz for dynamic dashboards."""
async def event_generator():
while True:
if await request.is_disconnected():
break
try:
stats = await get_all_stats()
yield f"data: {json.dumps(stats)}\n\n"
except Exception as e:
logger.error(f"SSE stream error: {e}")
yield f"data: {json.dumps({'error': str(e)})}\n\n"
await asyncio.sleep(1.0)
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
}
)
@app.post("/api/switch-model", summary="Hot-Swap Ollama LLM in VRAM", tags=["Orchestration"])
async def api_switch_model(req: SwitchRequest):
"""Hot-swap the active Ollama model in VRAM and measure exact load duration and token evaluation speed."""
res = await vram_arbitrator.switch_ollama_model(req.model, keep_alive=req.keep_alive or "30m")
if not res.get("success"):
raise HTTPException(status_code=500, detail=res.get("error"))
return res
@app.post("/api/free-vram", summary="Soft-Yield Ollama VRAM", tags=["Orchestration"])
async def api_free_vram():
"""Instruct Ollama to instantly yield VRAM to 0MB in ~15ms while preserving model weights in the 64GB host RAM page cache."""
return await vram_arbitrator.instant_free_ollama_vram()
@app.post("/api/comfy-free", summary="Purge ComfyUI VRAM Cache", tags=["Orchestration"])
async def api_comfy_free():
"""Purge loaded diffusion models and VRAM cache from the ComfyUI pipeline."""
return await vram_arbitrator.instant_free_comfyui_vram()
@app.post("/api/warm-all", summary="Pre-warm All Models into RAM Cache", tags=["Memory Optimization"])
async def api_warm_all():
"""Pre-fault and read all installed Ollama GGUF models and ComfyUI Safetensors checkpoints into the Linux OS Page Cache."""
return await ram_optimizer.warm_all_models()
@app.post("/api/warm-model", summary="Pre-warm Single Model or File", tags=["Memory Optimization"])
async def api_warm_model(req: WarmRequest):
"""Pre-warm a specific Ollama model or individual file path into Linux RAM cache."""
if req.model_name:
return await ram_optimizer.warm_ollama_model(req.model_name, keep_alive="1m")
elif req.filepath:
return ram_optimizer.warm_file_to_ram(req.filepath)
else:
raise HTTPException(status_code=400, detail="model_name or filepath required")
@app.get("/api/models", summary="List All Installed Models", tags=["Catalog"])
async def api_get_models():
"""List all installed Ollama models and discovered ComfyUI model checkpoints/safetensors on disk with sizes and quantization levels."""
ollama_state = await vram_arbitrator.get_ollama_live_state()
comfy_models = ram_optimizer.find_comfy_model_files()
return {
"ollama_models": ollama_state.get("installed_models", []),
"comfy_models": comfy_models,
}
@app.get("/api/history", summary="Model Switch History Log", tags=["Analytics"])
async def api_get_history(limit: int = Query(20, description="Max history items to return")):
"""Get the recent history log of model switch events, swap durations (in ms), and RAM cache hit status."""
history = vram_arbitrator.get_switch_history()
return history[:limit]
@app.post("/api/benchmark", summary="Run Latency Benchmark", tags=["Analytics"])
async def api_run_benchmark(req: BenchmarkRequest):
"""Run an automated benchmark swapping between available models to measure round-trip latency and RAM cache effectiveness."""
from mcp_server import run_model_switch_benchmark
res_str = await run_model_switch_benchmark(iterations=req.iterations or 2)
return json.loads(res_str)
# Mount static web UI files
app.mount("/static", StaticFiles(directory="/home/drjones/unified-model-manager/static"), name="static")
@app.get("/", summary="Dashboard Web UI", tags=["UI"])
async def root_index():
with open("/home/drjones/unified-model-manager/static/index.html", "r") as f:
content = f.read()
return HTMLResponse(content=content)
if __name__ == "__main__":
import uvicorn
uvicorn.run("server:app", host="0.0.0.0", port=9090, reload=False, log_level="info")

4
start_manager.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
exec /home/drjones/comfy-mcp-venv/bin/python server.py

288
static/app.js Normal file
View File

@@ -0,0 +1,288 @@
let evtSource = null;
let currentInstalledModels = [];
// Initialize SSE Stream
function initSSE() {
if (evtSource) {
evtSource.close();
}
evtSource = new EventSource('/api/stream');
evtSource.onopen = () => {
document.getElementById('sse-badge').textContent = 'SSE LIVE';
document.getElementById('sse-badge').className = 'text-emerald-400 font-bold';
};
evtSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
updateDashboard(data);
} catch (err) {
console.error('Error parsing SSE event:', err);
}
};
evtSource.onerror = (err) => {
console.warn('SSE disconnected, retrying...', err);
document.getElementById('sse-badge').textContent = 'RECONNECTING...';
document.getElementById('sse-badge').className = 'text-rose-400 font-bold';
};
}
function updateDashboard(data) {
if (!data) return;
// 1. GPU VRAM Stats
const gpu = data.gpu || {};
if (gpu.available) {
document.getElementById('gpu-chip-name').textContent = gpu.device_name || 'NVIDIA GPU';
document.getElementById('vram-total-used').textContent = gpu.vram_used_gb || '0.0';
document.getElementById('vram-used-pct').textContent = `${gpu.vram_used_pct || 0}% USED`;
const bd = gpu.breakdown || {};
const totalBytes = gpu.vram_total_bytes || (16 * 1024**3);
const ollamaPct = ((bd.ollama_gb || 0) / (gpu.vram_total_gb || 16)) * 100;
const comfyPct = ((bd.comfyui_gb || 0) / (gpu.vram_total_gb || 16)) * 100;
const systemPct = ((bd.system_gb || 0) / (gpu.vram_total_gb || 16)) * 100;
const freePct = Math.max(0, 100 - (ollamaPct + comfyPct + systemPct));
document.getElementById('bar-ollama').style.width = `${ollamaPct}%`;
document.getElementById('bar-comfy').style.width = `${comfyPct}%`;
document.getElementById('bar-system').style.width = `${systemPct}%`;
document.getElementById('bar-free').style.width = `${freePct}%`;
document.getElementById('tooltip-ollama').textContent = `${bd.ollama_gb || 0} GB`;
document.getElementById('tooltip-comfy').textContent = `${bd.comfyui_gb || 0} GB`;
document.getElementById('tooltip-system').textContent = `${bd.system_gb || 0} GB`;
document.getElementById('legend-ollama').textContent = `${bd.ollama_gb || 0} GB`;
document.getElementById('legend-comfy').textContent = `${bd.comfyui_gb || 0} GB`;
document.getElementById('legend-system').textContent = `${bd.system_gb || 0} GB`;
document.getElementById('legend-free').textContent = `${bd.free_gb || 0} GB`;
// Hardware sensors
document.getElementById('gpu-util-val').textContent = `${gpu.gpu_util_pct || 0}%`;
document.getElementById('gpu-temp-val').textContent = `${gpu.temperature_c || 0}°C`;
document.getElementById('gpu-power-val').textContent = `${gpu.power_w || 0} W`;
document.getElementById('gpu-fan-val').textContent = `${gpu.fan_pct || 0}%`;
// Processes table
const tbody = document.getElementById('gpu-proc-table');
if (bd.processes && bd.processes.length > 0) {
tbody.innerHTML = bd.processes.map(p => {
let tagClass = 'text-slate-400';
let badge = '';
if (p.is_ollama) {
tagClass = 'text-purple-400 font-bold';
badge = '<span class="px-1 py-0.2 bg-purple-950 text-purple-300 rounded border border-purple-800 text-[9px] mr-1">OLLAMA</span>';
} else if (p.is_comfy) {
tagClass = 'text-cyan-400 font-bold';
badge = '<span class="px-1 py-0.2 bg-cyan-950 text-cyan-300 rounded border border-cyan-800 text-[9px] mr-1">COMFY</span>';
}
return `
<tr class="hover:bg-slate-900/50">
<td class="py-1 text-slate-500 font-mono text-[10px]">${p.pid}</td>
<td class="py-1 ${tagClass} text-[11px] truncate max-w-[140px]">${badge}${p.name}</td>
<td class="py-1 text-right font-mono font-bold text-slate-200 text-[11px]">${p.vram_mb} MB</td>
</tr>
`;
}).join('');
} else {
tbody.innerHTML = '<tr><td colspan="3" class="py-2 text-center text-slate-500">No compute processes running</td></tr>';
}
}
// 2. System RAM & Page Cache
const ram = data.ram || {};
if (ram.total_bytes) {
document.getElementById('ram-cached-gb').textContent = ram.cached_gb || '0.0';
document.getElementById('ram-total-text').textContent = `${ram.total_gb || 0} GB Total (${ram.cache_ratio_pct || 0}% in Cache)`;
const usedPct = ((ram.used_bytes || 0) / ram.total_bytes) * 100;
const cachePct = ((ram.cached_bytes || 0) / ram.total_bytes) * 100;
const freePct = Math.max(0, 100 - (usedPct + cachePct));
document.getElementById('bar-ram-used').style.width = `${usedPct}%`;
document.getElementById('bar-ram-cache').style.width = `${cachePct}%`;
document.getElementById('bar-ram-free').style.width = `${freePct}%`;
document.getElementById('legend-ram-used').textContent = `${ram.used_gb || 0} GB`;
document.getElementById('legend-ram-cached').textContent = `${ram.cached_gb || 0} GB`;
document.getElementById('legend-ram-free').textContent = `${ram.free_gb || 0} GB`;
}
// 3. Ollama State
const ollama = data.ollama || {};
if (ollama.online) {
document.getElementById('ollama-status-text').textContent = 'ONLINE';
document.getElementById('ollama-status-text').className = 'text-emerald-400 font-mono font-bold';
document.getElementById('ollama-dot').className = 'relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500';
document.getElementById('ollama-pulse').className = 'animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75';
if (ollama.active_model_name) {
document.getElementById('ollama-active-model').textContent = ollama.active_model_name;
document.getElementById('ollama-vram-badge').textContent = `${ollama.active_model_vram_gb || 0} GB VRAM`;
document.getElementById('ollama-vram-badge').className = 'px-2 py-0.5 text-[10px] font-mono rounded bg-purple-900/80 text-purple-200 border border-purple-600 font-bold';
document.getElementById('ollama-context').textContent = `${ollama.active_context || 0} ctx`;
} else {
document.getElementById('ollama-active-model').textContent = 'None Loaded (VRAM Free)';
document.getElementById('ollama-vram-badge').textContent = '0.0 GB VRAM';
document.getElementById('ollama-vram-badge').className = 'px-2 py-0.5 text-[10px] font-mono rounded bg-slate-800 text-slate-400 border border-slate-700';
document.getElementById('ollama-context').textContent = 'Idle';
}
if (ollama.installed_models && ollama.installed_models.length > 0) {
document.getElementById('ollama-total-models').textContent = ollama.installed_models.length;
updateModelSelect(ollama.installed_models, ollama.active_model_name);
}
} else {
document.getElementById('ollama-status-text').textContent = 'OFFLINE';
document.getElementById('ollama-status-text').className = 'text-rose-400 font-mono font-bold';
document.getElementById('ollama-dot').className = 'relative inline-flex rounded-full h-2.5 w-2.5 bg-rose-500';
document.getElementById('ollama-pulse').className = 'hidden';
}
// 4. ComfyUI State
const comfy = data.comfyui || {};
if (comfy.online) {
document.getElementById('comfy-status-text').textContent = 'ONLINE';
document.getElementById('comfy-status-text').className = 'text-emerald-400 font-mono font-bold';
document.getElementById('comfy-dot').className = 'relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500';
document.getElementById('comfy-pulse').className = 'animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75';
document.getElementById('comfy-queue-count').textContent = comfy.queue_running + comfy.queue_remaining;
if (comfy.executing) {
document.getElementById('comfy-exec-badge').textContent = 'GENERATING...';
document.getElementById('comfy-exec-badge').className = 'px-2 py-0.5 text-[10px] font-mono rounded bg-amber-950 text-amber-300 border border-amber-600 animate-pulse font-bold';
document.getElementById('comfy-model-info').textContent = `Prompt #${comfy.current_prompt_id || 'Active'}`;
} else {
document.getElementById('comfy-exec-badge').textContent = 'IDLE / READY';
document.getElementById('comfy-exec-badge').className = 'px-2 py-0.5 text-[10px] font-mono rounded bg-emerald-950 text-emerald-300 border border-emerald-800';
document.getElementById('comfy-model-info').textContent = 'Dynamic Model Offloader';
}
document.getElementById('comfy-vram-avail').textContent = `${(comfy.vram_free_mb / 1024).toFixed(1)} GB`;
} else {
document.getElementById('comfy-status-text').textContent = 'OFFLINE';
document.getElementById('comfy-status-text').className = 'text-rose-400 font-mono font-bold';
document.getElementById('comfy-dot').className = 'relative inline-flex rounded-full h-2.5 w-2.5 bg-rose-500';
document.getElementById('comfy-pulse').className = 'hidden';
}
document.getElementById('comfy-discovered-count').textContent = `${data.comfy_models_count || 0} Files`;
// 5. Switch History Timeline
const history = data.history || [];
const logContainer = document.getElementById('switch-log-container');
if (history.length > 0) {
const latest = history[0];
document.getElementById('ollama-last-swap').textContent = `${latest.duration_ms} ms`;
document.getElementById('ollama-cache-hit').textContent = latest.cache_status || 'OK';
logContainer.innerHTML = history.slice(0, 10).map(item => {
const isHit = (item.cache_status || '').includes('RAM Cache Hit') || (item.cache_status || '').includes('RAM-Cached');
const badgeClass = isHit
? 'bg-emerald-950/80 text-emerald-300 border-emerald-800'
: 'bg-amber-950/80 text-amber-300 border-amber-800';
return `
<div class="p-2.5 rounded-xl bg-slate-950 border border-slate-800 text-xs font-mono flex items-center justify-between">
<div class="space-y-0.5">
<div class="flex items-center space-x-1.5">
<span class="text-slate-500 text-[10px]">${item.timestamp}</span>
<span class="text-indigo-400 font-bold">${item.event_type}</span>
</div>
<div class="text-[11px] text-slate-300 truncate max-w-[260px]">
<span class="text-slate-500">${item.source}</span> → <span class="text-cyan-300 font-bold">${item.target}</span>
</div>
</div>
<div class="text-right">
<div class="text-slate-100 font-bold">${item.duration_ms} ms</div>
<span class="px-1.5 py-0.2 text-[9px] rounded border ${badgeClass}">${item.cache_status}</span>
</div>
</div>
`;
}).join('');
}
}
function updateModelSelect(models, activeModel) {
const select = document.getElementById('ollama-model-select');
const currentVal = select.value;
if (JSON.stringify(models.map(m => m.name)) === JSON.stringify(currentInstalledModels)) {
return;
}
currentInstalledModels = models.map(m => m.name);
select.innerHTML = models.map(m => {
const isSelected = m.name === activeModel || m.name === currentVal;
const sizeGb = (m.size / (1024**3)).toFixed(1);
const quant = m.details?.quantization_level || '';
return `<option value="${m.name}" ${isSelected ? 'selected' : ''}>${m.name} (${sizeGb} GB ${quant})</option>`;
}).join('');
}
// User Actions
async function triggerModelSwitch() {
const select = document.getElementById('ollama-model-select');
const targetModel = select.value;
if (!targetModel) return;
const btn = document.getElementById('btn-switch-model');
btn.disabled = true;
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> <span>Swapping...</span>';
try {
const resp = await fetch('/api/switch-model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: targetModel, keep_alive: '30m' })
});
const result = await resp.json();
if (!resp.ok) {
alert(`Switch failed: ${result.detail || 'Unknown error'}`);
}
} catch (err) {
alert(`Error: ${err}`);
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-shuffle"></i> <span>Hot Swap</span>';
}
}
async function freeOllamaVRAM() {
try {
const resp = await fetch('/api/free-vram', { method: 'POST' });
const data = await resp.json();
console.log('Ollama VRAM yielded:', data);
} catch (err) {
alert(`Error: ${err}`);
}
}
async function freeComfyVRAM() {
try {
const resp = await fetch('/api/comfy-free', { method: 'POST' });
const data = await resp.json();
console.log('ComfyUI VRAM freed:', data);
} catch (err) {
alert(`Error: ${err}`);
}
}
async function warmAllModels() {
try {
const resp = await fetch('/api/warm-all', { method: 'POST' });
const data = await resp.json();
alert(`Warming completed in ${data.total_duration_ms} ms! All models are now cached in 64GB RAM.`);
} catch (err) {
alert(`Error warming models: ${err}`);
}
}
// Startup
document.addEventListener('DOMContentLoaded', () => {
initSSE();
});

426
static/index.html Normal file
View File

@@ -0,0 +1,426 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HYPERSWAP // Dual-Engine Model Orchestrator & Live Telemetry</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/static/styles.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="bg-slate-950 text-slate-100 min-h-screen font-sans antialiased selection:bg-cyan-500 selection:text-white">
<!-- TOP HEADER -->
<header class="border-b border-slate-800 bg-slate-900/80 backdrop-blur sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3 flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-cyan-500 via-indigo-500 to-purple-500 flex items-center justify-center shadow-lg shadow-cyan-500/20">
<i class="fa-solid fa-bolt-lightning text-white text-lg"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h1 class="text-xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 via-sky-300 to-indigo-400">
HYPERSWAP
</h1>
<span class="text-xs uppercase tracking-widest px-2 py-0.5 rounded bg-cyan-950/80 text-cyan-400 border border-cyan-800 font-mono">
v1.0-DEPLOY
</span>
</div>
<p class="text-xs text-slate-400 font-mono">NVIDIA RTX 4080 SUPER 16GB // 64GB DDR5 RAM // Ubuntu Linux</p>
</div>
</div>
<div class="flex items-center space-x-3">
<!-- Service Status Indicators -->
<div class="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-slate-800/80 border border-slate-700/60 text-xs">
<span class="relative flex h-2.5 w-2.5">
<span id="ollama-pulse" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span id="ollama-dot" class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
</span>
<span class="font-medium text-slate-300">Ollama LLM:</span>
<span id="ollama-status-text" class="text-emerald-400 font-mono font-bold">ONLINE</span>
</div>
<div class="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-slate-800/80 border border-slate-700/60 text-xs">
<span class="relative flex h-2.5 w-2.5">
<span id="comfy-pulse" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span id="comfy-dot" class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
</span>
<span class="font-medium text-slate-300">ComfyUI:</span>
<span id="comfy-status-text" class="text-emerald-400 font-mono font-bold">ONLINE</span>
</div>
<div class="flex items-center space-x-1.5 px-3 py-1.5 rounded-lg bg-slate-800/80 border border-slate-700/60 text-xs text-slate-400 font-mono">
<i class="fa-solid fa-satellite-dish text-cyan-400 text-xs animate-pulse"></i>
<span id="sse-badge">SSE 1Hz</span>
</div>
</div>
</div>
</header>
<!-- MAIN CONTAINER -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6">
<!-- HERO MEMORY GAUGES -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- GPU VRAM ALLOCATION GAUGE -->
<div class="bg-slate-900/70 border border-slate-800 rounded-2xl p-5 relative overflow-hidden backdrop-blur">
<div class="flex justify-between items-start mb-3">
<div>
<div class="flex items-center space-x-2">
<i class="fa-solid fa-microchip text-cyan-400"></i>
<h2 class="text-sm font-semibold text-slate-200 tracking-wide uppercase">GPU VRAM (16 GB Dedicated)</h2>
</div>
<p class="text-xs text-slate-400 mt-0.5">Real-time allocation breakdown on RTX 4080 SUPER</p>
</div>
<div class="text-right font-mono">
<span id="vram-total-used" class="text-2xl font-bold text-slate-100">0.0</span>
<span class="text-xs text-slate-400">/ 16.0 GB</span>
<div id="vram-used-pct" class="text-xs text-cyan-400 font-bold">0% USED</div>
</div>
</div>
<!-- Segmented Bar -->
<div class="h-6 w-full bg-slate-950 rounded-lg overflow-hidden flex border border-slate-800 p-0.5">
<div id="bar-ollama" class="bg-gradient-to-r from-purple-600 to-indigo-500 h-full rounded-l transition-all duration-500 relative group" style="width: 0%">
<div class="opacity-0 group-hover:opacity-100 absolute -top-8 left-1/2 -translate-x-1/2 bg-slate-800 text-[10px] text-white px-2 py-0.5 rounded border border-slate-700 whitespace-nowrap z-20">
Ollama: <span id="tooltip-ollama">0 GB</span>
</div>
</div>
<div id="bar-comfy" class="bg-gradient-to-r from-cyan-500 to-sky-400 h-full transition-all duration-500 relative group" style="width: 0%">
<div class="opacity-0 group-hover:opacity-100 absolute -top-8 left-1/2 -translate-x-1/2 bg-slate-800 text-[10px] text-white px-2 py-0.5 rounded border border-slate-700 whitespace-nowrap z-20">
ComfyUI: <span id="tooltip-comfy">0 GB</span>
</div>
</div>
<div id="bar-system" class="bg-slate-600 h-full transition-all duration-500 relative group" style="width: 0%">
<div class="opacity-0 group-hover:opacity-100 absolute -top-8 left-1/2 -translate-x-1/2 bg-slate-800 text-[10px] text-white px-2 py-0.5 rounded border border-slate-700 whitespace-nowrap z-20">
System: <span id="tooltip-system">0 GB</span>
</div>
</div>
<div id="bar-free" class="bg-slate-900 h-full rounded-r transition-all duration-500" style="width: 100%"></div>
</div>
<!-- Legend -->
<div class="grid grid-cols-4 gap-2 mt-3 text-xs font-mono">
<div class="flex items-center space-x-1.5">
<span class="w-2.5 h-2.5 rounded-full bg-purple-500"></span>
<span class="text-slate-400">Ollama:</span>
<span id="legend-ollama" class="text-slate-200 font-bold">0 GB</span>
</div>
<div class="flex items-center space-x-1.5">
<span class="w-2.5 h-2.5 rounded-full bg-cyan-400"></span>
<span class="text-slate-400">Comfy:</span>
<span id="legend-comfy" class="text-slate-200 font-bold">0 GB</span>
</div>
<div class="flex items-center space-x-1.5">
<span class="w-2.5 h-2.5 rounded-full bg-slate-500"></span>
<span class="text-slate-400">System:</span>
<span id="legend-system" class="text-slate-200 font-bold">0 GB</span>
</div>
<div class="flex items-center space-x-1.5">
<span class="w-2.5 h-2.5 rounded-full bg-slate-800 border border-slate-700"></span>
<span class="text-slate-400">Free:</span>
<span id="legend-free" class="text-emerald-400 font-bold">0 GB</span>
</div>
</div>
</div>
<!-- SYSTEM RAM & PAGE CACHE GAUGE -->
<div class="bg-slate-900/70 border border-slate-800 rounded-2xl p-5 relative overflow-hidden backdrop-blur">
<div class="flex justify-between items-start mb-3">
<div>
<div class="flex items-center space-x-2">
<i class="fa-solid fa-memory text-amber-400"></i>
<h2 class="text-sm font-semibold text-slate-200 tracking-wide uppercase">Host RAM & Model Page Cache (64 GB)</h2>
</div>
<p class="text-xs text-slate-400 mt-0.5">Models stay resident in RAM for instant PCIe hot-swaps</p>
</div>
<div class="text-right font-mono">
<span id="ram-cached-gb" class="text-2xl font-bold text-amber-400">0.0</span>
<span class="text-xs text-slate-400">GB CACHED</span>
<div id="ram-total-text" class="text-xs text-slate-400">60.3 GB Total</div>
</div>
</div>
<!-- Segmented Bar -->
<div class="h-6 w-full bg-slate-950 rounded-lg overflow-hidden flex border border-slate-800 p-0.5">
<div id="bar-ram-used" class="bg-gradient-to-r from-rose-600 to-orange-500 h-full rounded-l transition-all duration-500" style="width: 10%"></div>
<div id="bar-ram-cache" class="bg-gradient-to-r from-amber-500 to-yellow-400 h-full transition-all duration-500" style="width: 50%"></div>
<div id="bar-ram-free" class="bg-slate-900 h-full rounded-r transition-all duration-500" style="width: 40%"></div>
</div>
<!-- Legend -->
<div class="grid grid-cols-3 gap-2 mt-3 text-xs font-mono">
<div class="flex items-center space-x-1.5">
<span class="w-2.5 h-2.5 rounded-full bg-rose-500"></span>
<span class="text-slate-400">Apps Used:</span>
<span id="legend-ram-used" class="text-slate-200 font-bold">0 GB</span>
</div>
<div class="flex items-center space-x-1.5">
<span class="w-2.5 h-2.5 rounded-full bg-amber-400"></span>
<span class="text-slate-400">Models in RAM:</span>
<span id="legend-ram-cached" class="text-amber-400 font-bold">0 GB</span>
</div>
<div class="flex items-center space-x-1.5">
<span class="w-2.5 h-2.5 rounded-full bg-slate-800 border border-slate-700"></span>
<span class="text-slate-400">Free RAM:</span>
<span id="legend-ram-free" class="text-emerald-400 font-bold">0 GB</span>
</div>
</div>
</div>
</div>
<!-- 4-CARD CONTROL & TELEMETRY GRID -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- CARD 1: OLLAMA LLM ENGINE -->
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 flex flex-col justify-between space-y-4">
<div>
<div class="flex items-center justify-between pb-3 border-b border-slate-800">
<div class="flex items-center space-x-2">
<div class="p-2 rounded-lg bg-purple-950/80 border border-purple-800 text-purple-400">
<i class="fa-solid fa-brain text-sm"></i>
</div>
<div>
<h3 class="font-bold text-slate-100 text-sm">Ollama LLM Engine</h3>
<p class="text-xs text-slate-400">Port :11434 // FlashAttention + Q4 KV Cache</p>
</div>
</div>
<button onclick="freeOllamaVRAM()" class="px-2.5 py-1 text-xs font-semibold rounded-lg bg-rose-950/70 border border-rose-800 text-rose-300 hover:bg-rose-900 transition flex items-center space-x-1">
<i class="fa-solid fa-arrow-down-to-bracket"></i>
<span>Soft-Yield VRAM</span>
</button>
</div>
<!-- Active Model Box -->
<div class="mt-4 p-4 rounded-xl bg-slate-950 border border-slate-800/80 space-y-3">
<div class="flex justify-between items-center">
<span class="text-xs text-slate-400 uppercase font-mono">Active Model in VRAM</span>
<span id="ollama-vram-badge" class="px-2 py-0.5 text-[10px] font-mono rounded bg-purple-900/60 text-purple-300 border border-purple-700">
0.0 GB VRAM
</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2 overflow-hidden">
<i class="fa-solid fa-cube text-purple-400"></i>
<span id="ollama-active-model" class="text-base font-bold font-mono text-slate-100 truncate">None Loaded</span>
</div>
<span id="ollama-context" class="text-xs font-mono text-slate-400">0 ctx</span>
</div>
</div>
<!-- Quick Hot-Swap Control -->
<div class="mt-4 space-y-2">
<label class="text-xs font-semibold text-slate-300 flex items-center justify-between">
<span>Instant Model Hot-Swap:</span>
<span class="text-[10px] text-cyan-400 font-mono">RAM Cache Optimized</span>
</label>
<div class="flex space-x-2">
<select id="ollama-model-select" class="flex-1 bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200 focus:outline-none focus:border-purple-500">
<option value="">Loading installed models...</option>
</select>
<button id="btn-switch-model" onclick="triggerModelSwitch()" class="px-4 py-2 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white text-xs font-bold rounded-lg shadow-md transition flex items-center space-x-1.5">
<i class="fa-solid fa-shuffle"></i>
<span>Hot Swap</span>
</button>
</div>
</div>
</div>
<!-- Ollama Stats Footer -->
<div class="pt-3 border-t border-slate-800 grid grid-cols-3 gap-2 text-center font-mono text-xs">
<div class="p-2 rounded bg-slate-950 border border-slate-800">
<span class="text-[10px] text-slate-500 block">LAST SWAP TIME</span>
<span id="ollama-last-swap" class="text-cyan-400 font-bold">-- ms</span>
</div>
<div class="p-2 rounded bg-slate-950 border border-slate-800">
<span class="text-[10px] text-slate-500 block">RAM HIT STATUS</span>
<span id="ollama-cache-hit" class="text-emerald-400 font-bold">--</span>
</div>
<div class="p-2 rounded bg-slate-950 border border-slate-800">
<span class="text-[10px] text-slate-500 block">TOTAL MODELS</span>
<span id="ollama-total-models" class="text-slate-300 font-bold">0</span>
</div>
</div>
</div>
<!-- CARD 2: COMFYUI DIFFUSION PIPELINE -->
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 flex flex-col justify-between space-y-4">
<div>
<div class="flex items-center justify-between pb-3 border-b border-slate-800">
<div class="flex items-center space-x-2">
<div class="p-2 rounded-lg bg-cyan-950/80 border border-cyan-800 text-cyan-400">
<i class="fa-solid fa-palette text-sm"></i>
</div>
<div>
<h3 class="font-bold text-slate-100 text-sm">ComfyUI Diffusion Engine</h3>
<p class="text-xs text-slate-400">Port :8188 // DynamicVRAM + Pinned Async Offload</p>
</div>
</div>
<button onclick="freeComfyVRAM()" class="px-2.5 py-1 text-xs font-semibold rounded-lg bg-rose-950/70 border border-rose-800 text-rose-300 hover:bg-rose-900 transition flex items-center space-x-1">
<i class="fa-solid fa-broom"></i>
<span>Purge VRAM</span>
</button>
</div>
<!-- Comfy Execution Box -->
<div class="mt-4 p-4 rounded-xl bg-slate-950 border border-slate-800/80 space-y-3">
<div class="flex justify-between items-center">
<span class="text-xs text-slate-400 uppercase font-mono">Pipeline Status</span>
<span id="comfy-exec-badge" class="px-2 py-0.5 text-[10px] font-mono rounded bg-emerald-950 text-emerald-300 border border-emerald-800">
IDLE / READY
</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<i class="fa-solid fa-layer-group text-cyan-400"></i>
<span id="comfy-model-info" class="text-sm font-mono text-slate-200">Dynamic Model Offloader</span>
</div>
<div class="text-xs font-mono text-slate-400">
Queue: <span id="comfy-queue-count" class="text-cyan-400 font-bold">0</span>
</div>
</div>
</div>
<!-- Comfy Feature Checklist -->
<div class="mt-4 space-y-2 text-xs">
<div class="p-2.5 rounded-lg bg-slate-950 border border-slate-800 space-y-1.5">
<div class="flex items-center justify-between text-slate-300">
<span class="flex items-center space-x-1.5">
<i class="fa-solid fa-check text-emerald-400 text-xs"></i>
<span>Host Pinned Memory:</span>
</span>
<span class="font-mono font-bold text-emerald-400">53.6 GB Staging Buffer</span>
</div>
<div class="flex items-center justify-between text-slate-300">
<span class="flex items-center space-x-1.5">
<i class="fa-solid fa-check text-emerald-400 text-xs"></i>
<span>Async PCIe Offloading:</span>
</span>
<span class="font-mono font-bold text-cyan-400">Enabled (2 Streams)</span>
</div>
<div class="flex items-center justify-between text-slate-300">
<span class="flex items-center space-x-1.5">
<i class="fa-solid fa-check text-emerald-400 text-xs"></i>
<span>Fast Disk RAM Mmap:</span>
</span>
<span class="font-mono font-bold text-amber-400">Active</span>
</div>
</div>
</div>
</div>
<!-- Comfy Stats Footer -->
<div class="pt-3 border-t border-slate-800 grid grid-cols-2 gap-2 text-center font-mono text-xs">
<div class="p-2 rounded bg-slate-950 border border-slate-800">
<span class="text-[10px] text-slate-500 block">DISCOVERED MODELS</span>
<span id="comfy-discovered-count" class="text-cyan-400 font-bold">0 Files</span>
</div>
<div class="p-2 rounded bg-slate-950 border border-slate-800">
<span class="text-[10px] text-slate-500 block">VRAM AVAILABLE</span>
<span id="comfy-vram-avail" class="text-emerald-400 font-bold">15.9 GB</span>
</div>
</div>
</div>
<!-- CARD 3: GPU HARDWARE TELEMETRY -->
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 space-y-4">
<div class="flex items-center justify-between pb-3 border-b border-slate-800">
<div class="flex items-center space-x-2">
<div class="p-2 rounded-lg bg-emerald-950/80 border border-emerald-800 text-emerald-400">
<i class="fa-solid fa-gauge-high text-sm"></i>
</div>
<div>
<h3 class="font-bold text-slate-100 text-sm">GPU Live Telemetry (NVML)</h3>
<p class="text-xs text-slate-400" id="gpu-chip-name">NVIDIA GeForce RTX 4080 SUPER</p>
</div>
</div>
</div>
<!-- Quick Gauges Grid -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 font-mono">
<div class="p-3 rounded-xl bg-slate-950 border border-slate-800 text-center">
<span class="text-[10px] text-slate-400 uppercase block">GPU Util</span>
<span id="gpu-util-val" class="text-xl font-bold text-cyan-400">0%</span>
</div>
<div class="p-3 rounded-xl bg-slate-950 border border-slate-800 text-center">
<span class="text-[10px] text-slate-400 uppercase block">Temperature</span>
<span id="gpu-temp-val" class="text-xl font-bold text-emerald-400">0°C</span>
</div>
<div class="p-3 rounded-xl bg-slate-950 border border-slate-800 text-center">
<span class="text-[10px] text-slate-400 uppercase block">Power Draw</span>
<span id="gpu-power-val" class="text-xl font-bold text-amber-400">0 W</span>
</div>
<div class="p-3 rounded-xl bg-slate-950 border border-slate-800 text-center">
<span class="text-[10px] text-slate-400 uppercase block">Fan Speed</span>
<span id="gpu-fan-val" class="text-xl font-bold text-slate-300">0%</span>
</div>
</div>
<!-- Process Table -->
<div class="space-y-2">
<span class="text-xs font-semibold text-slate-400 uppercase font-mono">Active Compute Processes</span>
<div class="rounded-xl bg-slate-950 border border-slate-800 p-2 max-h-36 overflow-y-auto font-mono text-xs">
<table class="w-full text-left">
<thead>
<tr class="text-slate-500 border-b border-slate-800/80 text-[10px]">
<th class="pb-1">PID</th>
<th class="pb-1">Process</th>
<th class="pb-1 text-right">VRAM</th>
</tr>
</thead>
<tbody id="gpu-proc-table" class="divide-y divide-slate-900">
<tr><td colspan="3" class="py-2 text-center text-slate-500">Scanning GPU processes...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- CARD 4: REAL-TIME SWITCH TIMELINE & OPTIMIZER -->
<div class="bg-slate-900/80 border border-slate-800 rounded-2xl p-5 flex flex-col justify-between space-y-4">
<div>
<div class="flex items-center justify-between pb-3 border-b border-slate-800">
<div class="flex items-center space-x-2">
<div class="p-2 rounded-lg bg-indigo-950/80 border border-indigo-800 text-indigo-400">
<i class="fa-solid fa-clock-rotate-left text-sm"></i>
</div>
<div>
<h3 class="font-bold text-slate-100 text-sm">Model Switch Timeline & Optimizer</h3>
<p class="text-xs text-slate-400">Real-time latency logger and memory warmer</p>
</div>
</div>
<button onclick="warmAllModels()" class="px-2.5 py-1 text-xs font-semibold rounded-lg bg-amber-950/70 border border-amber-800 text-amber-300 hover:bg-amber-900 transition flex items-center space-x-1">
<i class="fa-solid fa-fire text-amber-400"></i>
<span>Warm All to RAM</span>
</button>
</div>
<!-- Switch Events Log -->
<div class="mt-4 space-y-2">
<span class="text-xs font-semibold text-slate-400 uppercase font-mono">Recent Model Swaps</span>
<div id="switch-log-container" class="space-y-2 max-h-48 overflow-y-auto pr-1">
<div class="p-3 rounded-xl bg-slate-950 border border-slate-800 text-xs text-slate-400 text-center font-mono">
No recent model swaps recorded yet.
</div>
</div>
</div>
</div>
<!-- Optimizer Banner -->
<div class="p-3 rounded-xl bg-gradient-to-r from-cyan-950/50 via-slate-950 to-purple-950/50 border border-slate-800 text-xs flex items-center justify-between">
<div class="flex items-center space-x-2">
<i class="fa-solid fa-wand-magic-sparkles text-cyan-400"></i>
<span class="text-slate-300">64GB RAM Cache holds all models in memory</span>
</div>
<span class="font-mono text-cyan-400 font-bold">PCIe x16 (~31.5 GB/s)</span>
</div>
</div>
</div>
</main>
<script src="/static/app.js"></script>
</body>
</html>

28
static/styles.css Normal file
View File

@@ -0,0 +1,28 @@
/* Custom Scrollbars */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #020617;
}
::-webkit-scrollbar-thumb {
background: #1e293b;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #334155;
}
@keyframes pulseGlow {
0%, 100% {
box-shadow: 0 0 15px rgba(6, 182, 212, 0.2);
}
50% {
box-shadow: 0 0 25px rgba(6, 182, 212, 0.4);
}
}
.glow-card {
animation: pulseGlow 4s infinite ease-in-out;
}

322
vram_arbitrator.py Normal file
View File

@@ -0,0 +1,322 @@
"""VRAM Arbitrator and High-Speed Switch Manager for Ollama and ComfyUI."""
import time
import httpx
import psutil
import logging
from typing import Dict, List, Any, Optional
from collections import deque
try:
import pynvml
pynvml.nvmlInit()
NVML_AVAILABLE = True
except Exception as e:
NVML_AVAILABLE = False
logger = logging.getLogger("vram_arbitrator")
OLLAMA_API_BASE = "http://localhost:11434"
COMFY_API_BASE = "http://127.0.0.1:8188"
# Circular buffer for transition events
SWITCH_HISTORY = deque(maxlen=50)
def get_gpu_hardware_stats() -> Dict[str, Any]:
"""Retrieve comprehensive GPU hardware and process metrics via NVML."""
if not NVML_AVAILABLE:
return {"available": False, "error": "NVML not initialized"}
try:
handle = pynvml.nvmlDeviceGetHandleByIndex(0)
name = pynvml.nvmlDeviceGetName(handle)
if isinstance(name, bytes):
name = name.decode("utf-8")
mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
util_rates = pynvml.nvmlDeviceGetUtilizationRates(handle)
temp_c = pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU)
try:
power_mw = pynvml.nvmlDeviceGetPowerUsage(handle)
power_w = round(power_mw / 1000.0, 1)
except Exception:
power_w = 0.0
try:
fan_pct = pynvml.nvmlDeviceGetFanSpeed(handle)
except Exception:
fan_pct = 0
try:
clock_graphics = pynvml.nvmlDeviceGetClockInfo(handle, pynvml.NVML_CLOCK_GRAPHICS)
clock_mem = pynvml.nvmlDeviceGetClockInfo(handle, pynvml.NVML_CLOCK_MEM)
except Exception:
clock_graphics = 0
clock_mem = 0
# Discover processes on GPU
proc_breakdown = {
"ollama_bytes": 0,
"comfyui_bytes": 0,
"system_bytes": 0,
"processes": []
}
try:
procs = pynvml.nvmlDeviceGetComputeRunningProcesses(handle)
graphics_procs = pynvml.nvmlDeviceGetGraphicsRunningProcesses(handle)
all_procs = {p.pid: p.usedGpuMemory for p in procs}
for p in graphics_procs:
all_procs[p.pid] = max(all_procs.get(p.pid, 0), p.usedGpuMemory or 0)
for pid, used_mem in all_procs.items():
pname = "Unknown"
cmdline = ""
try:
proc = psutil.Process(pid)
pname = proc.name()
cmdline = " ".join(proc.cmdline())
except Exception:
pass
is_ollama = "ollama" in pname.lower() or "llama-server" in cmdline.lower()
is_comfy = "comfy" in cmdline.lower() or "main.py" in cmdline.lower()
if is_ollama:
proc_breakdown["ollama_bytes"] += used_mem
elif is_comfy:
proc_breakdown["comfyui_bytes"] += used_mem
else:
proc_breakdown["system_bytes"] += used_mem
proc_breakdown["processes"].append({
"pid": pid,
"name": pname,
"cmdline": cmdline[:60],
"vram_bytes": used_mem,
"vram_mb": round(used_mem / (1024**2), 1),
"is_ollama": is_ollama,
"is_comfy": is_comfy,
})
except Exception as e:
logger.error(f"Error enumerating GPU processes: {e}")
total_vram = mem_info.total
used_vram = mem_info.used
free_vram = mem_info.free
return {
"available": True,
"device_name": name,
"vram_total_bytes": total_vram,
"vram_total_gb": round(total_vram / (1024**3), 2),
"vram_used_bytes": used_vram,
"vram_used_gb": round(used_vram / (1024**3), 2),
"vram_free_bytes": free_vram,
"vram_free_gb": round(free_vram / (1024**3), 2),
"vram_used_pct": round((used_vram / total_vram * 100) if total_vram > 0 else 0, 1),
"gpu_util_pct": util_rates.gpu,
"mem_util_pct": util_rates.memory,
"temperature_c": temp_c,
"power_w": power_w,
"fan_pct": fan_pct,
"clock_graphics_mhz": clock_graphics,
"clock_mem_mhz": clock_mem,
"breakdown": {
"ollama_mb": round(proc_breakdown["ollama_bytes"] / (1024**2), 1),
"ollama_gb": round(proc_breakdown["ollama_bytes"] / (1024**3), 2),
"comfyui_mb": round(proc_breakdown["comfyui_bytes"] / (1024**2), 1),
"comfyui_gb": round(proc_breakdown["comfyui_bytes"] / (1024**3), 2),
"system_mb": round(proc_breakdown["system_bytes"] / (1024**2), 1),
"system_gb": round(proc_breakdown["system_bytes"] / (1024**3), 2),
"free_mb": round(free_vram / (1024**2), 1),
"free_gb": round(free_vram / (1024**3), 2),
"processes": proc_breakdown["processes"],
}
}
except Exception as e:
return {"available": False, "error": str(e)}
async def get_ollama_live_state() -> Dict[str, Any]:
"""Get active models, running status, and VRAM expiration from Ollama."""
state = {
"online": False,
"loaded_models": [],
"active_model_name": None,
"active_model_vram_gb": 0.0,
"active_context": 0,
"expires_at": None,
"installed_models": []
}
try:
async with httpx.AsyncClient(timeout=3.0) as client:
# Check running models (ps)
ps_resp = await client.get(f"{OLLAMA_API_BASE}/api/ps")
if ps_resp.status_code == 200:
state["online"] = True
models = ps_resp.json().get("models", [])
state["loaded_models"] = models
if models:
first = models[0]
state["active_model_name"] = first.get("name")
vram_bytes = first.get("size_vram", first.get("size", 0))
state["active_model_vram_gb"] = round(vram_bytes / (1024**3), 2)
state["active_context"] = first.get("context_length", 0)
state["expires_at"] = first.get("expires_at")
# Check all tags
tags_resp = await client.get(f"{OLLAMA_API_BASE}/api/tags")
if tags_resp.status_code == 200:
state["installed_models"] = tags_resp.json().get("models", [])
except Exception as e:
logger.debug(f"Ollama check error: {e}")
return state
async def get_comfyui_live_state() -> Dict[str, Any]:
"""Get prompt queue, device status, and active execution from ComfyUI."""
state = {
"online": False,
"executing": False,
"queue_remaining": 0,
"queue_running": 0,
"current_node": None,
"current_prompt_id": None,
"vram_free_mb": 0,
"vram_total_mb": 0,
}
try:
async with httpx.AsyncClient(timeout=3.0) as client:
# Check system stats
stats_resp = await client.get(f"{COMFY_API_BASE}/system_stats")
if stats_resp.status_code == 200:
state["online"] = True
data = stats_resp.json()
devices = data.get("devices", [])
if devices:
dev = devices[0]
state["vram_free_mb"] = round(dev.get("vram_free", 0) / (1024**2), 1)
state["vram_total_mb"] = round(dev.get("vram_total", 0) / (1024**2), 1)
# Check queue
queue_resp = await client.get(f"{COMFY_API_BASE}/queue")
if queue_resp.status_code == 200:
qdata = queue_resp.json()
running = qdata.get("queue_running", [])
pending = qdata.get("queue_pending", [])
state["queue_running"] = len(running)
state["queue_remaining"] = len(pending)
state["executing"] = len(running) > 0
if running:
state["current_prompt_id"] = running[0][1] if len(running[0]) > 1 else str(running[0])
except Exception as e:
logger.debug(f"ComfyUI check error: {e}")
return state
async def instant_free_ollama_vram(model_name: Optional[str] = None) -> Dict[str, Any]:
"""Tell Ollama to instantly yield VRAM without evicting from OS page cache."""
t0 = time.perf_counter()
if not model_name:
ollama_state = await get_ollama_live_state()
model_name = ollama_state.get("active_model_name")
if not model_name:
return {"success": True, "message": "No active Ollama model in VRAM", "duration_ms": 0}
try:
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.post(
f"{OLLAMA_API_BASE}/api/generate",
json={"model": model_name, "keep_alive": 0},
)
duration_ms = round((time.perf_counter() - t0) * 1000, 2)
event = {
"timestamp": time.strftime("%H:%M:%S"),
"event_type": "Ollama VRAM Yield",
"source": model_name,
"target": "VRAM 0MB (Kept in RAM)",
"duration_ms": duration_ms,
"cache_status": "RAM-Cached",
}
SWITCH_HISTORY.appendleft(event)
return {"success": True, "model": model_name, "duration_ms": duration_ms}
except Exception as e:
return {"success": False, "error": str(e), "duration_ms": round((time.perf_counter() - t0) * 1000, 2)}
async def instant_free_comfyui_vram() -> Dict[str, Any]:
"""Tell ComfyUI to purge loaded diffusion models from VRAM."""
t0 = time.perf_counter()
try:
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.post(
f"{COMFY_API_BASE}/free",
json={"unload_models": True, "free_memory": True},
)
duration_ms = round((time.perf_counter() - t0) * 1000, 2)
event = {
"timestamp": time.strftime("%H:%M:%S"),
"event_type": "ComfyUI VRAM Purge",
"source": "ComfyUI Pipeline",
"target": "VRAM Free",
"duration_ms": duration_ms,
"cache_status": "Cleaned",
}
SWITCH_HISTORY.appendleft(event)
return {"success": True, "duration_ms": duration_ms}
except Exception as e:
return {"success": False, "error": str(e), "duration_ms": round((time.perf_counter() - t0) * 1000, 2)}
async def switch_ollama_model(target_model: str, keep_alive: str = "30m") -> Dict[str, Any]:
"""High-speed hot-swap to target Ollama model, tracking swap metrics."""
t0 = time.perf_counter()
cur_state = await get_ollama_live_state()
prev_model = cur_state.get("active_model_name") or "None"
try:
async with httpx.AsyncClient(timeout=180.0) as client:
resp = await client.post(
f"{OLLAMA_API_BASE}/api/generate",
json={"model": target_model, "prompt": "Ready check", "stream": False, "keep_alive": keep_alive},
)
total_duration = time.perf_counter() - t0
total_duration_ms = round(total_duration * 1000, 2)
if resp.status_code == 200:
data = resp.json()
load_dur_ms = round(data.get("load_duration", 0) / 1e6, 2)
eval_dur_ms = round(data.get("eval_duration", 0) / 1e6, 2)
eval_count = data.get("eval_count", 0)
tokens_per_sec = round((eval_count / (eval_dur_ms / 1000)) if eval_dur_ms > 0 else 0, 1)
# Check if it was a RAM cache hit (load duration < 1500ms for large model indicates RAM hit)
is_ram_hit = load_dur_ms < 2500
event = {
"timestamp": time.strftime("%H:%M:%S"),
"event_type": "LLM Model Switch",
"source": prev_model,
"target": target_model,
"duration_ms": total_duration_ms,
"load_duration_ms": load_dur_ms,
"tokens_per_sec": tokens_per_sec,
"cache_status": "RAM Cache Hit ⚡" if is_ram_hit else "Cold Disk Load 💾",
}
SWITCH_HISTORY.appendleft(event)
return {
"success": True,
"prev_model": prev_model,
"target_model": target_model,
"total_duration_ms": total_duration_ms,
"load_duration_ms": load_dur_ms,
"tokens_per_sec": tokens_per_sec,
"is_ram_hit": is_ram_hit,
"response": data.get("response", ""),
}
else:
return {"success": False, "error": f"HTTP {resp.status_code}: {resp.text}", "duration_ms": total_duration_ms}
except Exception as e:
return {"success": False, "error": str(e), "duration_ms": round((time.perf_counter() - t0) * 1000, 2)}
def get_switch_history() -> List[Dict[str, Any]]:
return list(SWITCH_HISTORY)