File size: 3,157 Bytes
a358495
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from __future__ import annotations

import gc
import time
from collections import OrderedDict
from contextlib import contextmanager
from threading import RLock
from typing import Any, Callable, Iterator


class ModelRuntimeManager:
    """Lazy, bounded runtime for a single accelerator or CPU-only deployment."""

    def __init__(self, max_resident: int = 2, device: str = "auto") -> None:
        self.max_resident = max(1, int(max_resident))
        self.device = self._select_device(device)
        self._models: OrderedDict[str, Any] = OrderedDict()
        self._health: dict[str, dict[str, Any]] = {}
        self._lock = RLock()

    @staticmethod
    def _select_device(requested: str) -> str:
        if requested not in {"auto", "cuda", "cpu"}:
            raise ValueError(f"Unsupported device: {requested}")
        if requested == "cpu":
            return "cpu"
        try:
            import torch
            return "cuda" if torch.cuda.is_available() else "cpu"
        except ImportError:
            return "cpu"

    @contextmanager
    def acquire(self, model_id: str, loader: Callable[[], Any]) -> Iterator[Any]:
        started = time.perf_counter()
        with self._lock:
            try:
                if model_id not in self._models:
                    while len(self._models) >= self.max_resident:
                        oldest, _ = self._models.popitem(last=False)
                        self._health.setdefault(oldest, {})["resident"] = False
                        self._cleanup_device()
                    self._models[model_id] = loader()
                self._models.move_to_end(model_id)
                self._health[model_id] = {
                    "status": "READY",
                    "resident": True,
                    "device": self.device,
                    "load_ms": round((time.perf_counter() - started) * 1000, 2),
                }
                yield self._models[model_id]
            except Exception as exc:
                self._models.pop(model_id, None)
                self._health[model_id] = {"status": "LOAD_FAILED", "resident": False, "reason": str(exc)}
                self._cleanup_device()
                raise

    def unload(self, model_id: str) -> None:
        with self._lock:
            self._models.pop(model_id, None)
            self._health.setdefault(model_id, {})["resident"] = False
            self._cleanup_device()

    @staticmethod
    def _cleanup_device() -> None:
        gc.collect()
        try:
            import torch
            if torch.cuda.is_available():
                torch.cuda.empty_cache()
        except ImportError:
            pass

    def snapshot(self) -> dict[str, Any]:
        allocated = 0
        try:
            import torch
            if torch.cuda.is_available():
                allocated = int(torch.cuda.memory_allocated())
        except ImportError:
            pass
        return {
            "device": self.device,
            "max_resident": self.max_resident,
            "resident_models": list(self._models),
            "allocated_vram_bytes": allocated,
            "models": dict(self._health),
        }