abinazebinoy commited on
Commit
22a359f
·
1 Parent(s): 69b5d6a

feat(config): add RuntimeProfile hardware detector

Browse files

Detects 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

Files changed (1) hide show
  1. 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()