Spaces:
Running
Running
Commit ·
22a359f
1
Parent(s): 69b5d6a
feat(config): add RuntimeProfile hardware detector
Browse filesDetects CPU cores, RAM GB, GPU, free disk, Docker environment,
and Neo4j URI location at startup.
Scoring: cpu*2 + ram*2 + gpu*2 + disk + docker + db_local (max 9)
Score 0-3 -> LOW (2 workers, batch 25, depth 2)
Score 4-7 -> MEDIUM (4 workers, batch 100, depth 3)
Score 8+ -> HIGH (8 workers, batch 500, depth 5)
BHARATGRAPH_PROFILE env var overrides auto-detection.
Module-level PROFILE singleton imported by all modules.
Part of: #56
- config/runtime_profile.py +209 -0
config/runtime_profile.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BharatGraph - Phase 31: Runtime Profile Auto-Detector
|
| 3 |
+
Detects hardware at startup and assigns LOW / MEDIUM / HIGH profile.
|
| 4 |
+
All downstream modules read from PROFILE rather than hardcoding limits.
|
| 5 |
+
Pure ASCII - no Unicode characters.
|
| 6 |
+
"""
|
| 7 |
+
import os
|
| 8 |
+
import multiprocessing
|
| 9 |
+
import platform
|
| 10 |
+
import shutil
|
| 11 |
+
from loguru import logger
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# ---- Profile presets --------------------------------------------------
|
| 15 |
+
|
| 16 |
+
PROFILES = {
|
| 17 |
+
"low": {
|
| 18 |
+
"max_workers": 2,
|
| 19 |
+
"batch_size": 25,
|
| 20 |
+
"graph_depth": 2,
|
| 21 |
+
"investigation_layers": 3,
|
| 22 |
+
"cache_ttl_seconds": 300,
|
| 23 |
+
"enable_gpu": False,
|
| 24 |
+
"description": "Minimal footprint - laptop or free-tier cloud",
|
| 25 |
+
},
|
| 26 |
+
"medium": {
|
| 27 |
+
"max_workers": 4,
|
| 28 |
+
"batch_size": 100,
|
| 29 |
+
"graph_depth": 3,
|
| 30 |
+
"investigation_layers": 4,
|
| 31 |
+
"cache_ttl_seconds": 120,
|
| 32 |
+
"enable_gpu": False,
|
| 33 |
+
"description": "Standard server - 4 CPU / 8 GB RAM",
|
| 34 |
+
},
|
| 35 |
+
"high": {
|
| 36 |
+
"max_workers": 8,
|
| 37 |
+
"batch_size": 500,
|
| 38 |
+
"graph_depth": 5,
|
| 39 |
+
"investigation_layers": 6,
|
| 40 |
+
"cache_ttl_seconds": 60,
|
| 41 |
+
"enable_gpu": True,
|
| 42 |
+
"description": "High-performance server - 8+ CPU / 16+ GB RAM",
|
| 43 |
+
},
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# ---- Hardware detection -----------------------------------------------
|
| 48 |
+
|
| 49 |
+
def _cpu_cores() -> int:
|
| 50 |
+
try:
|
| 51 |
+
return multiprocessing.cpu_count()
|
| 52 |
+
except Exception:
|
| 53 |
+
return 1
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _ram_gb() -> float:
|
| 57 |
+
try:
|
| 58 |
+
import psutil
|
| 59 |
+
return psutil.virtual_memory().total / (1024 ** 3)
|
| 60 |
+
except ImportError:
|
| 61 |
+
try:
|
| 62 |
+
with open("/proc/meminfo") as f:
|
| 63 |
+
for line in f:
|
| 64 |
+
if line.startswith("MemTotal"):
|
| 65 |
+
kb = int(line.split()[1])
|
| 66 |
+
return kb / (1024 ** 2)
|
| 67 |
+
except Exception:
|
| 68 |
+
pass
|
| 69 |
+
return 2.0
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _gpu_available() -> bool:
|
| 73 |
+
try:
|
| 74 |
+
import torch
|
| 75 |
+
return torch.cuda.is_available()
|
| 76 |
+
except ImportError:
|
| 77 |
+
pass
|
| 78 |
+
try:
|
| 79 |
+
result = shutil.which("nvidia-smi")
|
| 80 |
+
if result:
|
| 81 |
+
import subprocess
|
| 82 |
+
r = subprocess.run(
|
| 83 |
+
["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
|
| 84 |
+
capture_output=True, text=True, timeout=5
|
| 85 |
+
)
|
| 86 |
+
return r.returncode == 0 and bool(r.stdout.strip())
|
| 87 |
+
except Exception:
|
| 88 |
+
pass
|
| 89 |
+
return False
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _free_disk_gb() -> float:
|
| 93 |
+
try:
|
| 94 |
+
usage = shutil.disk_usage(".")
|
| 95 |
+
return usage.free / (1024 ** 3)
|
| 96 |
+
except Exception:
|
| 97 |
+
return 10.0
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _in_docker() -> bool:
|
| 101 |
+
try:
|
| 102 |
+
with open("/proc/1/cgroup") as f:
|
| 103 |
+
return "docker" in f.read() or "kubepods" in f.read()
|
| 104 |
+
except Exception:
|
| 105 |
+
pass
|
| 106 |
+
return os.path.exists("/.dockerenv")
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _db_local() -> bool:
|
| 110 |
+
"""True when Neo4j URI points to localhost (low-latency)."""
|
| 111 |
+
uri = os.getenv("NEO4J_URI", "")
|
| 112 |
+
return "localhost" in uri or "127.0.0.1" in uri or "bolt://" in uri
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# ---- Profile scoring --------------------------------------------------
|
| 116 |
+
# Score >= 8 -> high, >= 4 -> medium, else low
|
| 117 |
+
|
| 118 |
+
def _compute_score(cpu: int, ram: float, gpu: bool,
|
| 119 |
+
disk: float, docker: bool, db_local: bool) -> int:
|
| 120 |
+
score = 0
|
| 121 |
+
score += 2 if cpu >= 8 else (1 if cpu >= 4 else 0)
|
| 122 |
+
score += 2 if ram >= 16 else (1 if ram >= 8 else 0)
|
| 123 |
+
score += 2 if gpu else 0
|
| 124 |
+
score += 1 if disk >= 20 else 0
|
| 125 |
+
score += 1 if docker else 0
|
| 126 |
+
score += 1 if db_local else 0
|
| 127 |
+
return score
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _score_to_profile(score: int) -> str:
|
| 131 |
+
if score >= 8:
|
| 132 |
+
return "high"
|
| 133 |
+
if score >= 4:
|
| 134 |
+
return "medium"
|
| 135 |
+
return "low"
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
# ---- Public API -------------------------------------------------------
|
| 139 |
+
|
| 140 |
+
class RuntimeProfile:
|
| 141 |
+
"""
|
| 142 |
+
Singleton - call RuntimeProfile.get() anywhere to read settings.
|
| 143 |
+
|
| 144 |
+
Usage:
|
| 145 |
+
from config.runtime_profile import PROFILE
|
| 146 |
+
workers = PROFILE["max_workers"]
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
_instance = None
|
| 150 |
+
|
| 151 |
+
def __init__(self):
|
| 152 |
+
self.cpu = _cpu_cores()
|
| 153 |
+
self.ram_gb = _ram_gb()
|
| 154 |
+
self.gpu = _gpu_available()
|
| 155 |
+
self.disk_gb = _free_disk_gb()
|
| 156 |
+
self.docker = _in_docker()
|
| 157 |
+
self.db_loc = _db_local()
|
| 158 |
+
self.os = platform.system()
|
| 159 |
+
|
| 160 |
+
self.score = _compute_score(
|
| 161 |
+
self.cpu, self.ram_gb, self.gpu,
|
| 162 |
+
self.disk_gb, self.docker, self.db_loc
|
| 163 |
+
)
|
| 164 |
+
self.name = os.getenv("BHARATGRAPH_PROFILE", "").lower()
|
| 165 |
+
if self.name not in PROFILES:
|
| 166 |
+
self.name = _score_to_profile(self.score)
|
| 167 |
+
|
| 168 |
+
self.settings = dict(PROFILES[self.name])
|
| 169 |
+
|
| 170 |
+
logger.info(
|
| 171 |
+
f"[RuntimeProfile] Detected: CPU={self.cpu} cores, "
|
| 172 |
+
f"RAM={self.ram_gb:.1f}GB, GPU={self.gpu}, "
|
| 173 |
+
f"Disk={self.disk_gb:.1f}GB, Docker={self.docker}, "
|
| 174 |
+
f"DB-local={self.db_loc}, OS={self.os}"
|
| 175 |
+
)
|
| 176 |
+
logger.success(
|
| 177 |
+
f"[RuntimeProfile] Score={self.score} -> Profile: {self.name.upper()} "
|
| 178 |
+
f"({self.settings['description']})"
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
@classmethod
|
| 182 |
+
def get(cls) -> "RuntimeProfile":
|
| 183 |
+
if cls._instance is None:
|
| 184 |
+
cls._instance = RuntimeProfile()
|
| 185 |
+
return cls._instance
|
| 186 |
+
|
| 187 |
+
def __getitem__(self, key):
|
| 188 |
+
return self.settings[key]
|
| 189 |
+
|
| 190 |
+
def to_dict(self) -> dict:
|
| 191 |
+
return {
|
| 192 |
+
"profile_name": self.name,
|
| 193 |
+
"score": self.score,
|
| 194 |
+
"hardware": {
|
| 195 |
+
"cpu_cores": self.cpu,
|
| 196 |
+
"ram_gb": round(self.ram_gb, 1),
|
| 197 |
+
"gpu": self.gpu,
|
| 198 |
+
"disk_gb": round(self.disk_gb, 1),
|
| 199 |
+
"in_docker": self.docker,
|
| 200 |
+
"db_local": self.db_loc,
|
| 201 |
+
"os": self.os,
|
| 202 |
+
},
|
| 203 |
+
"settings": self.settings,
|
| 204 |
+
"overridable": "Set BHARATGRAPH_PROFILE=low|medium|high to force a profile",
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# Module-level singleton - import this in all modules
|
| 209 |
+
PROFILE = RuntimeProfile.get()
|