Skip to content

Commit 34488dc

Browse files
committed
Refactor GPU detection and model metadata handling
- Changed asynchronous GPU detection functions to synchronous counterparts for improved performance and compatibility with blocking calls. - Updated GPU information retrieval to utilize a caching mechanism, reducing redundant NVML calls and enhancing response times. - Refactored model details fetching to support blocking operations, allowing for smoother integration with the frontend. - Adjusted API routes to reflect changes in GPU detection and model metadata retrieval, ensuring consistent behavior across the application.
1 parent 43d8e03 commit 34488dc

7 files changed

Lines changed: 291 additions & 155 deletions

File tree

backend/gpu_detector.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- CPU acceleration (OpenBLAS)
77
"""
88

9+
import asyncio
910
import subprocess
1011
import json
1112
import os
@@ -149,13 +150,13 @@ def _query_nvml_cuda_version(initialized: bool = False) -> Optional[str]:
149150
pass
150151

151152

152-
async def detect_nvidia_gpu() -> Optional[Dict]:
153+
def _detect_nvidia_gpu() -> Optional[Dict]:
153154
"""Detect NVIDIA GPUs using pynvml and nvidia-smi"""
154155
cuda_version_hint: Optional[str] = None
155156

156157
if pynvml is None:
157158
logger.debug("pynvml not available; attempting detection via nvidia-smi")
158-
return await _detect_nvidia_via_smi()
159+
return _detect_nvidia_via_smi()
159160

160161
try:
161162
pynvml.nvmlInit()
@@ -166,7 +167,7 @@ async def detect_nvidia_gpu() -> Optional[Dict]:
166167
logger.debug(
167168
"NVML reported zero NVIDIA devices; falling back to nvidia-smi detection"
168169
)
169-
return await _detect_nvidia_via_smi(cuda_version_hint)
170+
return _detect_nvidia_via_smi(cuda_version_hint)
170171

171172
gpus = []
172173

@@ -241,7 +242,7 @@ async def detect_nvidia_gpu() -> Optional[Dict]:
241242
except Exception as exc:
242243
logger.debug(f"Failed to detect NVIDIA GPUs via NVML: {exc}")
243244
# Fallback to nvidia-smi
244-
return await _detect_nvidia_via_smi(cuda_version_hint)
245+
return _detect_nvidia_via_smi(cuda_version_hint)
245246
finally:
246247
if pynvml is not None:
247248
try:
@@ -250,7 +251,7 @@ async def detect_nvidia_gpu() -> Optional[Dict]:
250251
pass
251252

252253

253-
async def _detect_nvidia_via_smi(
254+
def _detect_nvidia_via_smi(
254255
cuda_version_hint: Optional[str] = None,
255256
) -> Optional[Dict]:
256257
"""Fallback NVIDIA detection using nvidia-smi"""
@@ -358,16 +359,16 @@ async def _detect_nvidia_via_smi(
358359
# ============================================================================
359360

360361

361-
async def detect_amd_gpu() -> Optional[Dict]:
362+
def _detect_amd_gpu() -> Optional[Dict]:
362363
"""Detect AMD GPUs using rocm-smi or lspci"""
363364
try:
364365
# Try using rocm-smi first
365-
amd_info = await _detect_amd_via_rocm()
366+
amd_info = _detect_amd_via_rocm()
366367
if amd_info:
367368
return amd_info
368369

369370
# Fallback to lspci
370-
amd_info = await _detect_amd_via_lspci()
371+
amd_info = _detect_amd_via_lspci()
371372
if amd_info:
372373
return amd_info
373374

@@ -377,7 +378,7 @@ async def detect_amd_gpu() -> Optional[Dict]:
377378
return None
378379

379380

380-
async def _detect_amd_via_rocm() -> Optional[Dict]:
381+
def _detect_amd_via_rocm() -> Optional[Dict]:
381382
"""Detect AMD GPUs using rocm-smi"""
382383
try:
383384
result = subprocess.run(
@@ -432,7 +433,7 @@ async def _detect_amd_via_rocm() -> Optional[Dict]:
432433
return None
433434

434435

435-
async def _detect_amd_via_lspci() -> Optional[Dict]:
436+
def _detect_amd_via_lspci() -> Optional[Dict]:
436437
"""Detect AMD GPUs using lspci"""
437438
try:
438439
result = subprocess.run(
@@ -486,22 +487,19 @@ async def _detect_amd_via_lspci() -> Optional[Dict]:
486487
# ============================================================================
487488

488489

489-
async def get_gpu_info() -> Dict[str, any]:
490-
"""Get comprehensive GPU information (tries all vendors)"""
490+
def _collect_gpu_info() -> Dict[str, any]:
491+
"""Blocking GPU probe (NVML, nvidia-smi, rocm-smi, lspci). Run via asyncio.to_thread."""
491492
if _gpu_detection_disabled:
492493
return _cpu_only_response()
493494

494-
# Try NVIDIA first
495-
nvidia_info = await detect_nvidia_gpu()
495+
nvidia_info = _detect_nvidia_gpu()
496496
if nvidia_info:
497497
return nvidia_info
498498

499-
# Try AMD
500-
amd_info = await detect_amd_gpu()
499+
amd_info = _detect_amd_gpu()
501500
if amd_info:
502501
return amd_info
503502

504-
# No GPU detected
505503
return {
506504
"vendor": None,
507505
"cuda_version": "Unknown",
@@ -513,6 +511,13 @@ async def get_gpu_info() -> Dict[str, any]:
513511
}
514512

515513

514+
async def get_gpu_info() -> Dict[str, any]:
515+
"""Get comprehensive GPU information without blocking the event loop."""
516+
if _gpu_detection_disabled:
517+
return _cpu_only_response()
518+
return await asyncio.to_thread(_collect_gpu_info)
519+
520+
516521
# ============================================================================
517522
# Backend Capability Detection
518523
# ============================================================================

backend/huggingface.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1717,8 +1717,8 @@ def _get_attr_or_key(obj, key, default=None):
17171717
return summary
17181718

17191719

1720-
async def get_model_details(model_id: str) -> Dict:
1721-
"""Get detailed model information including config and README"""
1720+
def _get_model_details_blocking(model_id: str) -> Dict:
1721+
"""Blocking Hugging Face API + config.json fetch (run via asyncio.to_thread)."""
17221722
try:
17231723
# Get model info with expanded data
17241724
model_info = hf_api.model_info(model_id, expand=["cardData", "siblings"])
@@ -1773,6 +1773,11 @@ async def get_model_details(model_id: str) -> Dict:
17731773
raise Exception(f"Failed to get model details: {e}")
17741774

17751775

1776+
async def get_model_details(model_id: str) -> Dict:
1777+
"""Get detailed model information including config and README."""
1778+
return await asyncio.to_thread(_get_model_details_blocking, model_id)
1779+
1780+
17761781
async def download_model(
17771782
huggingface_id: str, filename: str, model_format: str = "gguf"
17781783
) -> tuple[str, int]:

backend/routes/gpu_info.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastapi import APIRouter
22
import multiprocessing
3-
from backend.gpu_detector import get_gpu_info as detect_gpu_info
3+
from backend.services.model_metadata import get_cached_gpu_info
44
from backend.logging_config import get_logger
55

66
router = APIRouter()
@@ -11,7 +11,7 @@
1111
async def get_gpu_info():
1212
"""Detect GPU information and CPU capabilities via unified detector"""
1313
try:
14-
info = await detect_gpu_info()
14+
info = await get_cached_gpu_info()
1515
except Exception as exc:
1616
logger.exception("GPU detection failed: %s", exc)
1717
info = {

backend/routes/models.py

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
search_models,
3131
set_huggingface_token,
3232
get_huggingface_token,
33-
get_model_details,
3433
extract_quantization,
3534
list_grouped_safetensors_downloads,
3635
get_safetensors_manifest_entries,
@@ -213,6 +212,27 @@ class SafetensorsBundleRequest(BaseModel):
213212
files: List[Dict[str, Any]]
214213

215214

215+
_param_registry_cache: Dict[str, tuple] = {}
216+
_PARAM_REGISTRY_CACHE_TTL = 30.0
217+
218+
219+
def _param_registry_cache_key(store, engine: str) -> str:
220+
active = store.get_active_engine_version(engine) if engine in (
221+
"llama_cpp",
222+
"ik_llama",
223+
"lmdeploy",
224+
) else None
225+
version = (active or {}).get("version") or ""
226+
catalog_mtime = 0.0
227+
try:
228+
catalog_mtime = os.path.getmtime(
229+
os.path.join(store._config_dir, "engine_params_catalog.yaml")
230+
)
231+
except OSError:
232+
pass
233+
return f"{engine}:{version}:{catalog_mtime}"
234+
235+
216236
@router.get("/param-registry")
217237
async def get_param_registry_endpoint(engine: str = "llama_cpp"):
218238
"""Return param definitions from ``engine_params_catalog.yaml`` plus studio-only fields (read-only)."""
@@ -223,8 +243,16 @@ async def get_param_registry_endpoint(engine: str = "llama_cpp"):
223243
from backend.studio_engine_fields import studio_sections_for_engine
224244

225245
store = get_store()
246+
cache_key = _param_registry_cache_key(store, engine)
247+
now = time.monotonic()
248+
cached = _param_registry_cache.get(cache_key)
249+
if cached and now - cached[1] < _PARAM_REGISTRY_CACHE_TTL:
250+
return cached[0]
251+
226252
if engine not in ("llama_cpp", "ik_llama", "lmdeploy"):
227-
return registry_payload_from_entry(engine, None, [], has_active_engine=False)
253+
payload = registry_payload_from_entry(engine, None, [], has_active_engine=False)
254+
_param_registry_cache[cache_key] = (payload, now)
255+
return payload
228256

229257
studio = studio_sections_for_engine(engine)
230258
active = store.get_active_engine_version(engine)
@@ -239,9 +267,11 @@ async def get_param_registry_endpoint(engine: str = "llama_cpp"):
239267
if active and active.get("version"):
240268
entry = get_version_entry(store, engine, active["version"])
241269

242-
return registry_payload_from_entry(
270+
payload = registry_payload_from_entry(
243271
engine, entry, studio, has_active_engine=has_active
244272
)
273+
_param_registry_cache[cache_key] = (payload, now)
274+
return payload
245275

246276

247277
@router.get("")
@@ -803,15 +833,16 @@ async def update_model_projector(
803833
}
804834

805835

806-
@router.get("/{model_id:path}/limits")
807-
async def get_model_limits(model_id: str):
836+
_limits_cache: Dict[str, tuple] = {}
837+
_LIMITS_CACHE_TTL = 300.0
838+
839+
840+
def _compute_model_limits(model: Dict[str, Any]) -> Dict[str, Optional[int]]:
808841
"""
809842
Return model limits in an engine-agnostic way. For GGUF models with a manifest
810843
entry, uses the GGUF manifest; for safetensors, uses the safetensors manifest.
811844
Otherwise falls back to the Hugging Face model card (config.json / model info).
812845
"""
813-
store = get_store()
814-
model = _get_model_or_404(store, model_id)
815846
hf_id = model.get("huggingface_id")
816847
if not hf_id:
817848
return {"max_context_length": None, "layer_count": None}
@@ -828,7 +859,9 @@ async def get_model_limits(model_id: str):
828859

829860
if max_ctx is None or layer_count is None:
830861
try:
831-
details = await get_model_details(hf_id)
862+
from backend.huggingface import _get_model_details_blocking
863+
864+
details = _get_model_details_blocking(hf_id)
832865
config = details.get("config") or {}
833866
if max_ctx is None:
834867
hf_max = details.get("model_max_length") or config.get(
@@ -840,15 +873,27 @@ async def get_model_limits(model_id: str):
840873
for key in ("num_hidden_layers", "n_layer", "num_layers"):
841874
val = config.get(key)
842875
if isinstance(val, (int, float)) and val > 0:
843-
layer_count = (
844-
int(val) + 1
845-
) # + output head for n_gpu_layers hint
876+
layer_count = int(val) + 1
846877
break
847878
except Exception:
848879
pass
849880
return {"max_context_length": max_ctx, "layer_count": layer_count}
850881

851882

883+
@router.get("/{model_id:path}/limits")
884+
async def get_model_limits(model_id: str):
885+
now = time.monotonic()
886+
cached = _limits_cache.get(model_id)
887+
if cached and now - cached[1] < _LIMITS_CACHE_TTL:
888+
return cached[0]
889+
890+
store = get_store()
891+
model = _get_model_or_404(store, model_id)
892+
payload = await asyncio.to_thread(_compute_model_limits, model)
893+
_limits_cache[model_id] = (payload, now)
894+
return payload
895+
896+
852897
@router.get("/{model_id:path}/config")
853898
async def get_model_config(model_id: str):
854899
"""Get model's llama.cpp configuration"""

backend/services/model_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
# Lightweight cache for GPU info to avoid repeated NVML calls during rapid estimate requests
1818
_gpu_info_cache: Dict[str, Any] = {"data": None, "timestamp": 0.0}
19-
GPU_INFO_CACHE_TTL = 2.0 # seconds
19+
GPU_INFO_CACHE_TTL = 30.0 # seconds — shared by /api/gpu-info and VRAM estimates
2020

2121

2222
async def get_cached_gpu_info() -> Dict[str, Any]:

frontend/src/views/ModelConfig.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ function mountView() {
125125
LoadingState: { template: '<div>loading</div>' },
126126
EmptyState: { template: '<div><slot /></div>' },
127127
PageHeader: { template: '<div><slot name="start" /><slot name="title" /><slot name="actions" /></div>' },
128+
Dialog: {
129+
props: ['visible', 'header'],
130+
emits: ['update:visible', 'show'],
131+
template:
132+
'<div v-if="visible" class="dialog-stub"><slot /><slot name="footer" /></div>',
133+
},
128134
},
129135
},
130136
})
@@ -232,6 +238,12 @@ describe('ModelConfig', () => {
232238
await flushPromises()
233239

234240
expect(wrapper.text()).toContain('legacy_temp')
241+
242+
await wrapper.get('button[data-label="Live preview"]').trigger('click')
243+
await flushPromises()
244+
await vi.runAllTimersAsync()
245+
await flushPromises()
246+
235247
expect(axios.post).toHaveBeenLastCalledWith(
236248
'/api/models/model-1/preview-llama-swap-cmd',
237249
{

0 commit comments

Comments
 (0)