lumen-app 0.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lumen_app/__init__.py +7 -0
- lumen_app/core/__init__.py +0 -0
- lumen_app/core/config.py +661 -0
- lumen_app/core/installer.py +274 -0
- lumen_app/core/loader.py +45 -0
- lumen_app/core/router.py +87 -0
- lumen_app/core/server.py +389 -0
- lumen_app/core/service.py +49 -0
- lumen_app/core/tests/__init__.py +1 -0
- lumen_app/core/tests/test_core_integration.py +561 -0
- lumen_app/core/tests/test_env_checker.py +487 -0
- lumen_app/proto/README.md +12 -0
- lumen_app/proto/ml_service.proto +88 -0
- lumen_app/proto/ml_service_pb2.py +66 -0
- lumen_app/proto/ml_service_pb2.pyi +136 -0
- lumen_app/proto/ml_service_pb2_grpc.py +251 -0
- lumen_app/server.py +362 -0
- lumen_app/utils/env_checker.py +752 -0
- lumen_app/utils/installation/__init__.py +25 -0
- lumen_app/utils/installation/env_manager.py +152 -0
- lumen_app/utils/installation/micromamba_installer.py +459 -0
- lumen_app/utils/installation/package_installer.py +149 -0
- lumen_app/utils/installation/verifier.py +95 -0
- lumen_app/utils/logger.py +181 -0
- lumen_app/utils/mamba/cuda.yaml +12 -0
- lumen_app/utils/mamba/default.yaml +6 -0
- lumen_app/utils/mamba/openvino.yaml +7 -0
- lumen_app/utils/mamba/tensorrt.yaml +13 -0
- lumen_app/utils/package_resolver.py +309 -0
- lumen_app/utils/preset_registry.py +219 -0
- lumen_app/web/__init__.py +3 -0
- lumen_app/web/api/__init__.py +1 -0
- lumen_app/web/api/config.py +229 -0
- lumen_app/web/api/hardware.py +201 -0
- lumen_app/web/api/install.py +608 -0
- lumen_app/web/api/server.py +253 -0
- lumen_app/web/core/__init__.py +1 -0
- lumen_app/web/core/server_manager.py +348 -0
- lumen_app/web/core/state.py +264 -0
- lumen_app/web/main.py +145 -0
- lumen_app/web/models/__init__.py +28 -0
- lumen_app/web/models/config.py +63 -0
- lumen_app/web/models/hardware.py +64 -0
- lumen_app/web/models/install.py +134 -0
- lumen_app/web/models/server.py +95 -0
- lumen_app/web/static/assets/index-CGuhGHC9.css +1 -0
- lumen_app/web/static/assets/index-DN6HmxWS.js +56 -0
- lumen_app/web/static/index.html +14 -0
- lumen_app/web/static/vite.svg +1 -0
- lumen_app/web/websockets/__init__.py +1 -0
- lumen_app/web/websockets/logs.py +159 -0
- lumen_app-0.4.2.dist-info/METADATA +23 -0
- lumen_app-0.4.2.dist-info/RECORD +56 -0
- lumen_app-0.4.2.dist-info/WHEEL +5 -0
- lumen_app-0.4.2.dist-info/entry_points.txt +3 -0
- lumen_app-0.4.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Environment checker for Lumen AI services.
|
|
3
|
+
|
|
4
|
+
Provides driver validation for different hardware platforms.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import platform
|
|
10
|
+
import subprocess
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from lumen_resources.lumen_config import Runtime
|
|
16
|
+
|
|
17
|
+
from lumen_app.core.config import DeviceConfig
|
|
18
|
+
from lumen_app.utils.logger import get_logger
|
|
19
|
+
from lumen_app.utils.preset_registry import PresetRegistry
|
|
20
|
+
|
|
21
|
+
logger = get_logger("lumen.env_checker")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DriverStatus(Enum):
|
|
25
|
+
"""Driver availability status."""
|
|
26
|
+
|
|
27
|
+
AVAILABLE = "available"
|
|
28
|
+
MISSING = "missing"
|
|
29
|
+
INCOMPATIBLE = "incompatible"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class DriverCheckResult:
|
|
34
|
+
"""Result of driver availability check."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
status: DriverStatus
|
|
38
|
+
details: str = ""
|
|
39
|
+
installable_via_mamba: bool = False
|
|
40
|
+
mamba_config_path: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class EnvironmentReport:
|
|
45
|
+
"""Complete environment status report."""
|
|
46
|
+
|
|
47
|
+
preset_name: str
|
|
48
|
+
drivers: list[DriverCheckResult]
|
|
49
|
+
ready: bool
|
|
50
|
+
missing_installable: list[str] = field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DriverChecker:
|
|
54
|
+
"""Checks driver availability for different hardware platforms."""
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def check_nvidia_gpu() -> DriverCheckResult:
|
|
58
|
+
"""Check NVIDIA GPU/CUDA driver by running nvidia-smi."""
|
|
59
|
+
logger.info("[DriverChecker] Checking NVIDIA GPU/CUDA driver")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
logger.debug("[DriverChecker] Executing: nvidia-smi")
|
|
63
|
+
result = subprocess.run(
|
|
64
|
+
["nvidia-smi"],
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
timeout=10,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if result.returncode == 0:
|
|
71
|
+
# Parse GPU info from output
|
|
72
|
+
lines = result.stdout.split("\n")
|
|
73
|
+
details = "NVIDIA GPU detected"
|
|
74
|
+
|
|
75
|
+
for line in lines:
|
|
76
|
+
if "CUDA Version" in line:
|
|
77
|
+
details = line.strip()
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
logger.info(f"[DriverChecker] NVIDIA GPU detected: {details}")
|
|
81
|
+
return DriverCheckResult(
|
|
82
|
+
name="CUDA",
|
|
83
|
+
status=DriverStatus.AVAILABLE,
|
|
84
|
+
details=details,
|
|
85
|
+
installable_via_mamba=True,
|
|
86
|
+
mamba_config_path="cuda.yaml",
|
|
87
|
+
)
|
|
88
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
89
|
+
logger.debug(
|
|
90
|
+
f"[DriverChecker] nvidia-smi check failed: {type(e).__name__}: {e}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
logger.info("[DriverChecker] NVIDIA GPU not detected")
|
|
94
|
+
return DriverCheckResult(
|
|
95
|
+
name="CUDA",
|
|
96
|
+
status=DriverStatus.MISSING,
|
|
97
|
+
details="nvidia-smi command not found or failed",
|
|
98
|
+
installable_via_mamba=True,
|
|
99
|
+
mamba_config_path="cuda.yaml",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def check_amd_ryzen_ai_npu() -> DriverCheckResult:
|
|
104
|
+
"""Check AMD Ryzen AI NPU driver on Windows."""
|
|
105
|
+
logger.info("[DriverChecker] Checking AMD Ryzen AI NPU")
|
|
106
|
+
|
|
107
|
+
if platform.system() != "Windows":
|
|
108
|
+
logger.debug(
|
|
109
|
+
"[DriverChecker] Not Windows platform, marking as incompatible"
|
|
110
|
+
)
|
|
111
|
+
return DriverCheckResult(
|
|
112
|
+
name="AMD Ryzen AI NPU",
|
|
113
|
+
status=DriverStatus.MISSING,
|
|
114
|
+
details="Only available on Windows",
|
|
115
|
+
installable_via_mamba=False,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Check if amdipu.dll exists
|
|
119
|
+
dll_path = Path(r"C:\Windows\System32\amdipu.dll")
|
|
120
|
+
logger.debug(f"[DriverChecker] Checking for {dll_path}")
|
|
121
|
+
|
|
122
|
+
if not dll_path.exists():
|
|
123
|
+
logger.debug("[DriverChecker] amdipu.dll not found")
|
|
124
|
+
return DriverCheckResult(
|
|
125
|
+
name="AMD Ryzen AI NPU",
|
|
126
|
+
status=DriverStatus.MISSING,
|
|
127
|
+
details="amdipu.dll not found in C:\\Windows\\System32\\",
|
|
128
|
+
installable_via_mamba=False,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Check if amdipu service is running
|
|
132
|
+
try:
|
|
133
|
+
logger.debug("[DriverChecker] Executing: sc query amdipu")
|
|
134
|
+
result = subprocess.run(
|
|
135
|
+
["sc", "query", "amdipu"],
|
|
136
|
+
capture_output=True,
|
|
137
|
+
text=True,
|
|
138
|
+
timeout=10,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if result.returncode == 0 and "RUNNING" in result.stdout:
|
|
142
|
+
logger.info("[DriverChecker] AMD Ryzen AI NPU detected")
|
|
143
|
+
return DriverCheckResult(
|
|
144
|
+
name="AMD Ryzen AI NPU",
|
|
145
|
+
status=DriverStatus.AVAILABLE,
|
|
146
|
+
details="amdipu service is running",
|
|
147
|
+
installable_via_mamba=False,
|
|
148
|
+
)
|
|
149
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
150
|
+
logger.debug(
|
|
151
|
+
f"[DriverChecker] amdipu service check failed: {type(e).__name__}: {e}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
logger.info("[DriverChecker] AMD Ryzen AI NPU not available")
|
|
155
|
+
return DriverCheckResult(
|
|
156
|
+
name="AMD Ryzen AI NPU",
|
|
157
|
+
status=DriverStatus.MISSING,
|
|
158
|
+
details="amdipu service is not running",
|
|
159
|
+
installable_via_mamba=False,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def check_intel_gpu_openvino() -> DriverCheckResult:
|
|
164
|
+
"""Check Intel GPU / OpenVINO compatibility."""
|
|
165
|
+
logger.info("[DriverChecker] Checking Intel GPU / OpenVINO")
|
|
166
|
+
is_intel_cpu = False
|
|
167
|
+
|
|
168
|
+
# Check CPU vendor based on platform
|
|
169
|
+
try:
|
|
170
|
+
system = platform.system()
|
|
171
|
+
logger.debug(f"[DriverChecker] Platform: {system}")
|
|
172
|
+
|
|
173
|
+
if system == "Linux":
|
|
174
|
+
logger.debug("[DriverChecker] Reading /proc/cpuinfo")
|
|
175
|
+
with open("/proc/cpuinfo", "r") as f:
|
|
176
|
+
cpuinfo = f.read()
|
|
177
|
+
is_intel_cpu = "GenuineIntel" in cpuinfo
|
|
178
|
+
logger.debug(f"[DriverChecker] Intel CPU detected: {is_intel_cpu}")
|
|
179
|
+
|
|
180
|
+
elif system == "Darwin":
|
|
181
|
+
# macOS - check sysctl
|
|
182
|
+
logger.debug(
|
|
183
|
+
"[DriverChecker] Executing: sysctl -n machdep.cpu.brand_string"
|
|
184
|
+
)
|
|
185
|
+
result = subprocess.run(
|
|
186
|
+
["sysctl", "-n", "machdep.cpu.brand_string"],
|
|
187
|
+
capture_output=True,
|
|
188
|
+
text=True,
|
|
189
|
+
timeout=5,
|
|
190
|
+
)
|
|
191
|
+
is_intel_cpu = "Intel" in result.stdout
|
|
192
|
+
logger.debug(
|
|
193
|
+
f"[DriverChecker] CPU: {result.stdout.strip()}, Intel: {is_intel_cpu}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
elif system == "Windows":
|
|
197
|
+
# Windows - use wmic to check CPU manufacturer
|
|
198
|
+
logger.debug("[DriverChecker] Executing: wmic cpu get manufacturer")
|
|
199
|
+
result = subprocess.run(
|
|
200
|
+
["wmic", "cpu", "get", "manufacturer"],
|
|
201
|
+
capture_output=True,
|
|
202
|
+
text=True,
|
|
203
|
+
timeout=10,
|
|
204
|
+
)
|
|
205
|
+
if result.returncode == 0:
|
|
206
|
+
# Output format: "Manufacturer\n GenuineIntel \n\n"
|
|
207
|
+
is_intel_cpu = (
|
|
208
|
+
"GenuineIntel" in result.stdout or "Intel" in result.stdout
|
|
209
|
+
)
|
|
210
|
+
logger.debug(f"[DriverChecker] Intel CPU detected: {is_intel_cpu}")
|
|
211
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
212
|
+
logger.debug(
|
|
213
|
+
f"[DriverChecker] CPU vendor check failed: {type(e).__name__}: {e}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# If not Intel CPU, return INCOMPATIBLE
|
|
217
|
+
if not is_intel_cpu:
|
|
218
|
+
logger.info("[DriverChecker] Not Intel CPU - OpenVINO incompatible")
|
|
219
|
+
return DriverCheckResult(
|
|
220
|
+
name="OpenVINO",
|
|
221
|
+
status=DriverStatus.INCOMPATIBLE,
|
|
222
|
+
details="CPU vendor is not Intel",
|
|
223
|
+
installable_via_mamba=True,
|
|
224
|
+
mamba_config_path="openvino.yaml",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Intel CPU detected - OpenVINO can be installed
|
|
228
|
+
# Note: We don't check if openvino package is installed since
|
|
229
|
+
# the runtime environment may not have it yet
|
|
230
|
+
logger.info("[DriverChecker] Intel CPU detected - OpenVINO installable")
|
|
231
|
+
return DriverCheckResult(
|
|
232
|
+
name="OpenVINO",
|
|
233
|
+
status=DriverStatus.MISSING,
|
|
234
|
+
details="Compatible Intel CPU detected, openvino package not installed",
|
|
235
|
+
installable_via_mamba=True,
|
|
236
|
+
mamba_config_path="openvino.yaml",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def check_apple_silicon() -> DriverCheckResult:
|
|
241
|
+
"""Check Apple Silicon CoreML support."""
|
|
242
|
+
logger.info("[DriverChecker] Checking Apple Silicon CoreML")
|
|
243
|
+
|
|
244
|
+
system = platform.system()
|
|
245
|
+
machine = platform.machine()
|
|
246
|
+
logger.debug(f"[DriverChecker] Platform: {system}, Architecture: {machine}")
|
|
247
|
+
|
|
248
|
+
if system != "Darwin":
|
|
249
|
+
logger.debug("[DriverChecker] Not macOS platform")
|
|
250
|
+
return DriverCheckResult(
|
|
251
|
+
name="CoreML",
|
|
252
|
+
status=DriverStatus.INCOMPATIBLE,
|
|
253
|
+
details="Only available on macOS (Darwin)",
|
|
254
|
+
installable_via_mamba=False,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if machine != "arm64":
|
|
258
|
+
logger.debug(f"[DriverChecker] Not arm64 architecture: {machine}")
|
|
259
|
+
return DriverCheckResult(
|
|
260
|
+
name="CoreML",
|
|
261
|
+
status=DriverStatus.INCOMPATIBLE,
|
|
262
|
+
details=f"Architecture is {machine}, not arm64",
|
|
263
|
+
installable_via_mamba=False,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Check for Apple chip
|
|
267
|
+
try:
|
|
268
|
+
logger.debug(
|
|
269
|
+
"[DriverChecker] Executing: sysctl -n machdep.cpu.brand_string"
|
|
270
|
+
)
|
|
271
|
+
result = subprocess.run(
|
|
272
|
+
["sysctl", "-n", "machdep.cpu.brand_string"],
|
|
273
|
+
capture_output=True,
|
|
274
|
+
text=True,
|
|
275
|
+
timeout=5,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if result.returncode == 0 and "Apple" in result.stdout:
|
|
279
|
+
logger.info(
|
|
280
|
+
f"[DriverChecker] Apple Silicon detected: {result.stdout.strip()}"
|
|
281
|
+
)
|
|
282
|
+
return DriverCheckResult(
|
|
283
|
+
name="CoreML",
|
|
284
|
+
status=DriverStatus.AVAILABLE,
|
|
285
|
+
details=result.stdout.strip(),
|
|
286
|
+
installable_via_mamba=False,
|
|
287
|
+
)
|
|
288
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
289
|
+
logger.debug(
|
|
290
|
+
f"[DriverChecker] Apple Silicon check failed: {type(e).__name__}: {e}"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
logger.info("[DriverChecker] Not an Apple Silicon chip")
|
|
294
|
+
return DriverCheckResult(
|
|
295
|
+
name="CoreML",
|
|
296
|
+
status=DriverStatus.MISSING,
|
|
297
|
+
details="Not an Apple Silicon chip",
|
|
298
|
+
installable_via_mamba=False,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
@staticmethod
|
|
302
|
+
def check_rockchip_rknn() -> DriverCheckResult:
|
|
303
|
+
"""Check Rockchip RKNN device node on Linux."""
|
|
304
|
+
logger.info("[DriverChecker] Checking Rockchip RKNN")
|
|
305
|
+
|
|
306
|
+
if platform.system() != "Linux":
|
|
307
|
+
logger.debug("[DriverChecker] Not Linux platform")
|
|
308
|
+
return DriverCheckResult(
|
|
309
|
+
name="RKNN",
|
|
310
|
+
status=DriverStatus.INCOMPATIBLE,
|
|
311
|
+
details="Only available on Linux",
|
|
312
|
+
installable_via_mamba=False,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Check for /dev/rknpu device file
|
|
316
|
+
rknpu_paths = [
|
|
317
|
+
Path("/dev/rknpu"),
|
|
318
|
+
Path("/dev/rknpu0"),
|
|
319
|
+
Path("/dev/rp1"),
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
logger.debug(
|
|
323
|
+
f"[DriverChecker] Checking for RKNN device nodes: {[str(p) for p in rknpu_paths]}"
|
|
324
|
+
)
|
|
325
|
+
for device_path in rknpu_paths:
|
|
326
|
+
if device_path.exists():
|
|
327
|
+
logger.info(f"[DriverChecker] RKNN device found: {device_path}")
|
|
328
|
+
return DriverCheckResult(
|
|
329
|
+
name="RKNN",
|
|
330
|
+
status=DriverStatus.AVAILABLE,
|
|
331
|
+
details=f"Device found: {device_path}",
|
|
332
|
+
installable_via_mamba=False,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Check for Rockchip in cpuinfo as fallback
|
|
336
|
+
try:
|
|
337
|
+
logger.debug("[DriverChecker] Reading /proc/cpuinfo for Rockchip CPU")
|
|
338
|
+
with open("/proc/cpuinfo", "r") as f:
|
|
339
|
+
cpuinfo = f.read().lower()
|
|
340
|
+
if "rockchip" in cpuinfo or "rk35" in cpuinfo:
|
|
341
|
+
logger.info(
|
|
342
|
+
"[DriverChecker] Rockchip CPU detected but no device node"
|
|
343
|
+
)
|
|
344
|
+
return DriverCheckResult(
|
|
345
|
+
name="RKNN",
|
|
346
|
+
status=DriverStatus.MISSING,
|
|
347
|
+
details="Rockchip CPU detected but device node not found",
|
|
348
|
+
installable_via_mamba=False,
|
|
349
|
+
)
|
|
350
|
+
except OSError as e:
|
|
351
|
+
logger.debug(
|
|
352
|
+
f"[DriverChecker] Failed to read cpuinfo: {type(e).__name__}: {e}"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
logger.info("[DriverChecker] RKNN not detected")
|
|
356
|
+
return DriverCheckResult(
|
|
357
|
+
name="RKNN",
|
|
358
|
+
status=DriverStatus.MISSING,
|
|
359
|
+
details="RKNN device node not found",
|
|
360
|
+
installable_via_mamba=False,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
@staticmethod
|
|
364
|
+
def check_amd_gpu_directml() -> DriverCheckResult:
|
|
365
|
+
"""Check AMD GPU DirectML support on Windows."""
|
|
366
|
+
logger.info("[DriverChecker] Checking AMD GPU DirectML")
|
|
367
|
+
|
|
368
|
+
if platform.system() != "Windows":
|
|
369
|
+
logger.debug("[DriverChecker] Not Windows platform")
|
|
370
|
+
return DriverCheckResult(
|
|
371
|
+
name="DirectML",
|
|
372
|
+
status=DriverStatus.INCOMPATIBLE,
|
|
373
|
+
details="Only available on Windows",
|
|
374
|
+
installable_via_mamba=False,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Check GPU name using wmic
|
|
378
|
+
try:
|
|
379
|
+
logger.debug(
|
|
380
|
+
"[DriverChecker] Executing: wmic path win32_videocontroller get name"
|
|
381
|
+
)
|
|
382
|
+
result = subprocess.run(
|
|
383
|
+
["wmic", "path", "win32_videocontroller", "get", "name"],
|
|
384
|
+
capture_output=True,
|
|
385
|
+
text=True,
|
|
386
|
+
timeout=10,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if result.returncode == 0:
|
|
390
|
+
output = result.stdout.lower()
|
|
391
|
+
logger.debug(f"[DriverChecker] GPU detection output: {output[:100]}...")
|
|
392
|
+
if "radeon" in output or "amd" in output:
|
|
393
|
+
logger.info("[DriverChecker] AMD Radeon GPU detected")
|
|
394
|
+
return DriverCheckResult(
|
|
395
|
+
name="DirectML",
|
|
396
|
+
status=DriverStatus.AVAILABLE,
|
|
397
|
+
details="AMD Radeon GPU detected",
|
|
398
|
+
installable_via_mamba=False,
|
|
399
|
+
)
|
|
400
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
401
|
+
logger.debug(
|
|
402
|
+
f"[DriverChecker] AMD GPU check failed: {type(e).__name__}: {e}"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
logger.info("[DriverChecker] No AMD GPU detected")
|
|
406
|
+
return DriverCheckResult(
|
|
407
|
+
name="DirectML",
|
|
408
|
+
status=DriverStatus.MISSING,
|
|
409
|
+
details="No AMD Radeon GPU found",
|
|
410
|
+
installable_via_mamba=False,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
@staticmethod
|
|
414
|
+
def check_for_preset(preset_name: str) -> list[DriverCheckResult]:
|
|
415
|
+
"""
|
|
416
|
+
Check drivers required for a specific DeviceConfig preset.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
preset_name: Name of the preset method (e.g., "nvidia_gpu", "apple_silicon")
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
List of DriverCheckResult for required drivers
|
|
423
|
+
"""
|
|
424
|
+
logger.info(f"[DriverChecker] Checking drivers for preset: {preset_name}")
|
|
425
|
+
results = []
|
|
426
|
+
|
|
427
|
+
# Validate preset exists
|
|
428
|
+
if not PresetRegistry.preset_exists(preset_name):
|
|
429
|
+
logger.warning(
|
|
430
|
+
f"[DriverChecker] Unknown preset '{preset_name}', returning empty results"
|
|
431
|
+
)
|
|
432
|
+
return results
|
|
433
|
+
|
|
434
|
+
# Create config from preset to determine required checks
|
|
435
|
+
try:
|
|
436
|
+
logger.debug(f"[DriverChecker] Creating config from preset '{preset_name}'")
|
|
437
|
+
config = PresetRegistry.create_config(preset_name)
|
|
438
|
+
logger.debug(
|
|
439
|
+
f"[DriverChecker] Config created: runtime={config.runtime}, providers={config.onnx_providers}"
|
|
440
|
+
)
|
|
441
|
+
# Delegate to device_config check
|
|
442
|
+
results = DriverChecker.check_for_device_config(config)
|
|
443
|
+
logger.info(
|
|
444
|
+
f"[DriverChecker] Completed preset check, found {len(results)} driver(s)"
|
|
445
|
+
)
|
|
446
|
+
return results
|
|
447
|
+
except Exception as e:
|
|
448
|
+
logger.error(
|
|
449
|
+
f"[DriverChecker] Failed to check preset '{preset_name}': {type(e).__name__}: {e}"
|
|
450
|
+
)
|
|
451
|
+
return results
|
|
452
|
+
|
|
453
|
+
@staticmethod
|
|
454
|
+
def check_for_device_config(device_config: DeviceConfig) -> list[DriverCheckResult]:
|
|
455
|
+
"""
|
|
456
|
+
Check drivers required for a DeviceConfig instance.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
device_config: DeviceConfig instance
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
List of DriverCheckResult for required drivers
|
|
463
|
+
"""
|
|
464
|
+
logger.info("[DriverChecker] Checking drivers for device config")
|
|
465
|
+
results = []
|
|
466
|
+
providers = device_config.onnx_providers or []
|
|
467
|
+
runtime = device_config.runtime
|
|
468
|
+
|
|
469
|
+
logger.debug(f"[DriverChecker] Config runtime={runtime}, providers={providers}")
|
|
470
|
+
|
|
471
|
+
# Check based on onnx providers
|
|
472
|
+
for provider in providers:
|
|
473
|
+
provider_name = (
|
|
474
|
+
provider
|
|
475
|
+
if isinstance(provider, str)
|
|
476
|
+
else (provider[0] if provider else "")
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
logger.debug(f"[DriverChecker] Checking provider: {provider_name}")
|
|
480
|
+
|
|
481
|
+
if "CUDA" in provider_name or "TensorRT" in provider_name:
|
|
482
|
+
logger.debug("[DriverChecker] Triggering NVIDIA GPU check")
|
|
483
|
+
results.append(DriverChecker.check_nvidia_gpu())
|
|
484
|
+
elif "CoreML" in provider_name:
|
|
485
|
+
logger.debug("[DriverChecker] Triggering Apple Silicon check")
|
|
486
|
+
results.append(DriverChecker.check_apple_silicon())
|
|
487
|
+
elif "OpenVINO" in provider_name:
|
|
488
|
+
logger.debug("[DriverChecker] Triggering Intel GPU/OpenVINO check")
|
|
489
|
+
results.append(DriverChecker.check_intel_gpu_openvino())
|
|
490
|
+
elif "DML" in provider_name:
|
|
491
|
+
logger.debug("[DriverChecker] Triggering AMD GPU DirectML check")
|
|
492
|
+
results.append(DriverChecker.check_amd_gpu_directml())
|
|
493
|
+
|
|
494
|
+
# Check RKNN runtime
|
|
495
|
+
if runtime == Runtime.rknn:
|
|
496
|
+
logger.debug("[DriverChecker] Triggering RKNN check")
|
|
497
|
+
results.append(DriverChecker.check_rockchip_rknn())
|
|
498
|
+
|
|
499
|
+
# Remove duplicates by name
|
|
500
|
+
seen = {}
|
|
501
|
+
for result in results:
|
|
502
|
+
if result.name not in seen:
|
|
503
|
+
seen[result.name] = result
|
|
504
|
+
|
|
505
|
+
unique_results = list(seen.values())
|
|
506
|
+
logger.info(
|
|
507
|
+
f"[DriverChecker] Device config check completed: {len(unique_results)} unique driver(s)"
|
|
508
|
+
)
|
|
509
|
+
return unique_results
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class DependencyInstaller:
|
|
513
|
+
"""Installs missing dependencies using micromamba."""
|
|
514
|
+
|
|
515
|
+
def __init__(
|
|
516
|
+
self,
|
|
517
|
+
mamba_configs_dir: str | Path | None = None,
|
|
518
|
+
micromamba_path: str | None = None,
|
|
519
|
+
root_prefix: str | Path | None = None,
|
|
520
|
+
):
|
|
521
|
+
"""
|
|
522
|
+
Initialize installer with mamba config directory and micromamba path.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
mamba_configs_dir: Path to directory containing *.yaml files.
|
|
526
|
+
Defaults to lumen_app/utils/mamba
|
|
527
|
+
micromamba_path: Optional path to micromamba executable.
|
|
528
|
+
If None, uses 'micromamba' from PATH
|
|
529
|
+
root_prefix: Optional micromamba root prefix (MAMBA_ROOT_PREFIX).
|
|
530
|
+
If None, micromamba default is used.
|
|
531
|
+
"""
|
|
532
|
+
if mamba_configs_dir is None:
|
|
533
|
+
current_file = Path(__file__)
|
|
534
|
+
mamba_configs_dir = current_file.parent / "mamba"
|
|
535
|
+
|
|
536
|
+
self.configs_dir = Path(mamba_configs_dir)
|
|
537
|
+
self.micromamba_path = micromamba_path or "micromamba"
|
|
538
|
+
self.root_prefix = Path(root_prefix).expanduser() if root_prefix else None
|
|
539
|
+
|
|
540
|
+
logger.info(
|
|
541
|
+
f"[DependencyInstaller] Initialized with configs_dir={self.configs_dir}, micromamba_path={self.micromamba_path}, root_prefix={self.root_prefix}"
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
def install_driver(
|
|
545
|
+
self, driver_name: str, env_name: str = "lumen_env", dry_run: bool = False
|
|
546
|
+
) -> tuple[bool, str]:
|
|
547
|
+
"""
|
|
548
|
+
Install driver using micromamba.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
driver_name: Name of driver (e.g., "cuda", "openvino")
|
|
552
|
+
env_name: Name of the conda environment to create/update
|
|
553
|
+
dry_run: If True, only print the command without executing
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
Tuple of (success: bool, message: str)
|
|
557
|
+
"""
|
|
558
|
+
logger.info(
|
|
559
|
+
f"[DependencyInstaller] Installing driver '{driver_name}' (dry_run={dry_run})"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# Map driver names to config files
|
|
563
|
+
config_map = {
|
|
564
|
+
"cuda": "cuda.yaml",
|
|
565
|
+
"openvino": "openvino.yaml",
|
|
566
|
+
"tensorrt": "tensorrt.yaml",
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
config_filename = config_map.get(driver_name)
|
|
570
|
+
if not config_filename:
|
|
571
|
+
logger.warning(
|
|
572
|
+
f"[DependencyInstaller] No mamba config available for {driver_name}"
|
|
573
|
+
)
|
|
574
|
+
return False, f"No mamba config available for {driver_name}"
|
|
575
|
+
|
|
576
|
+
config_file = self.configs_dir / config_filename
|
|
577
|
+
logger.debug(f"[DependencyInstaller] Config file path: {config_file}")
|
|
578
|
+
|
|
579
|
+
if not config_file.exists():
|
|
580
|
+
logger.error(f"[DependencyInstaller] Config file not found: {config_file}")
|
|
581
|
+
return False, f"Config file not found: {config_file}"
|
|
582
|
+
|
|
583
|
+
# Build micromamba command with specified path
|
|
584
|
+
cmd = [
|
|
585
|
+
self.micromamba_path,
|
|
586
|
+
"install",
|
|
587
|
+
"-y",
|
|
588
|
+
"-n",
|
|
589
|
+
env_name,
|
|
590
|
+
"-f",
|
|
591
|
+
str(config_file),
|
|
592
|
+
]
|
|
593
|
+
if self.root_prefix:
|
|
594
|
+
cmd.extend(["--root-prefix", str(self.root_prefix)])
|
|
595
|
+
|
|
596
|
+
logger.debug(f"[DependencyInstaller] Installation command: {' '.join(cmd)}")
|
|
597
|
+
|
|
598
|
+
if dry_run:
|
|
599
|
+
logger.info("[DependencyInstaller] Dry run mode - skipping execution")
|
|
600
|
+
return True, f"Would run: {' '.join(cmd)}"
|
|
601
|
+
|
|
602
|
+
try:
|
|
603
|
+
logger.info("[DependencyInstaller] Executing installation (timeout=600s)")
|
|
604
|
+
result = subprocess.run(
|
|
605
|
+
cmd,
|
|
606
|
+
capture_output=True,
|
|
607
|
+
text=True,
|
|
608
|
+
timeout=600, # 10 minutes timeout
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
if result.returncode == 0:
|
|
612
|
+
logger.info(
|
|
613
|
+
f"[DependencyInstaller] Successfully installed {driver_name}"
|
|
614
|
+
)
|
|
615
|
+
return True, f"Successfully installed {driver_name}"
|
|
616
|
+
else:
|
|
617
|
+
logger.error(
|
|
618
|
+
f"[DependencyInstaller] Installation failed: returncode={result.returncode}, stderr={result.stderr}"
|
|
619
|
+
)
|
|
620
|
+
return False, f"Installation failed: {result.stderr}"
|
|
621
|
+
|
|
622
|
+
except subprocess.TimeoutExpired:
|
|
623
|
+
logger.error("[DependencyInstaller] Installation timed out after 600s")
|
|
624
|
+
return False, "Installation timed out"
|
|
625
|
+
except FileNotFoundError:
|
|
626
|
+
logger.error(
|
|
627
|
+
f"[DependencyInstaller] micromamba not found at {self.micromamba_path}"
|
|
628
|
+
)
|
|
629
|
+
return False, f"micromamba not found at {self.micromamba_path}"
|
|
630
|
+
except Exception as e:
|
|
631
|
+
logger.error(
|
|
632
|
+
f"[DependencyInstaller] Installation error: {type(e).__name__}: {e}"
|
|
633
|
+
)
|
|
634
|
+
return False, f"Installation error: {str(e)}"
|
|
635
|
+
|
|
636
|
+
def get_install_command(self, driver_name: str, env_name: str = "lumen_env") -> str:
|
|
637
|
+
"""Get the installation command for manual execution."""
|
|
638
|
+
logger.debug(
|
|
639
|
+
f"[DependencyInstaller] Getting install command for '{driver_name}'"
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
config_map = {
|
|
643
|
+
"cuda": "cuda.yaml",
|
|
644
|
+
"openvino": "openvino.yaml",
|
|
645
|
+
"tensorrt": "tensorrt.yaml",
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
config_filename = config_map.get(driver_name)
|
|
649
|
+
if not config_filename:
|
|
650
|
+
logger.warning(
|
|
651
|
+
f"[DependencyInstaller] No mamba config available for {driver_name}"
|
|
652
|
+
)
|
|
653
|
+
return f"# No mamba config available for {driver_name}"
|
|
654
|
+
|
|
655
|
+
config_file = self.configs_dir / config_filename
|
|
656
|
+
if not config_file.exists():
|
|
657
|
+
logger.error(f"[DependencyInstaller] Config file not found: {config_file}")
|
|
658
|
+
return f"# Config file not found: {config_file}"
|
|
659
|
+
|
|
660
|
+
cmd = f"micromamba install -y -n {env_name} -f {config_file}"
|
|
661
|
+
logger.debug(f"[DependencyInstaller] Generated command: {cmd}")
|
|
662
|
+
return cmd
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class EnvironmentChecker:
|
|
666
|
+
"""Main entry point for environment checking."""
|
|
667
|
+
|
|
668
|
+
@staticmethod
|
|
669
|
+
def check_preset(preset_name: str) -> EnvironmentReport:
|
|
670
|
+
"""
|
|
671
|
+
Check environment for a specific preset.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
preset_name: Name of the DeviceConfig preset method
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
EnvironmentReport with full status
|
|
678
|
+
"""
|
|
679
|
+
logger.info(
|
|
680
|
+
f"[EnvironmentChecker] Checking environment for preset: {preset_name}"
|
|
681
|
+
)
|
|
682
|
+
drivers = DriverChecker.check_for_preset(preset_name)
|
|
683
|
+
|
|
684
|
+
all_available = all(d.status == DriverStatus.AVAILABLE for d in drivers)
|
|
685
|
+
missing_installable = [
|
|
686
|
+
d.name.lower()
|
|
687
|
+
for d in drivers
|
|
688
|
+
if d.status == DriverStatus.MISSING and d.installable_via_mamba
|
|
689
|
+
]
|
|
690
|
+
|
|
691
|
+
logger.info(
|
|
692
|
+
f"[EnvironmentChecker] Preset check result: ready={all_available}, drivers={len(drivers)}, missing_installable={len(missing_installable)}"
|
|
693
|
+
)
|
|
694
|
+
for driver in drivers:
|
|
695
|
+
logger.debug(
|
|
696
|
+
f"[EnvironmentChecker] - {driver.name}: {driver.status.value} ({driver.details})"
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
return EnvironmentReport(
|
|
700
|
+
preset_name=preset_name,
|
|
701
|
+
drivers=drivers,
|
|
702
|
+
ready=all_available,
|
|
703
|
+
missing_installable=missing_installable,
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
@staticmethod
|
|
707
|
+
def check_device_config(device_config: DeviceConfig) -> EnvironmentReport:
|
|
708
|
+
"""
|
|
709
|
+
Check environment for a DeviceConfig instance.
|
|
710
|
+
|
|
711
|
+
Args:
|
|
712
|
+
device_config: DeviceConfig to check
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
EnvironmentReport with full status
|
|
716
|
+
"""
|
|
717
|
+
logger.info("[EnvironmentChecker] Checking environment for device config")
|
|
718
|
+
drivers = DriverChecker.check_for_device_config(device_config)
|
|
719
|
+
|
|
720
|
+
all_available = all(d.status == DriverStatus.AVAILABLE for d in drivers)
|
|
721
|
+
missing_installable = [
|
|
722
|
+
d.name.lower()
|
|
723
|
+
for d in drivers
|
|
724
|
+
if d.status == DriverStatus.MISSING and d.installable_via_mamba
|
|
725
|
+
]
|
|
726
|
+
|
|
727
|
+
# Determine preset name from config using PresetRegistry
|
|
728
|
+
preset_name = "custom"
|
|
729
|
+
for name in PresetRegistry.get_preset_names():
|
|
730
|
+
preset_config = PresetRegistry.create_config(name)
|
|
731
|
+
if (
|
|
732
|
+
preset_config.runtime == device_config.runtime
|
|
733
|
+
and preset_config.onnx_providers == device_config.onnx_providers
|
|
734
|
+
):
|
|
735
|
+
preset_name = name
|
|
736
|
+
logger.debug(f"[EnvironmentChecker] Matched preset: {name}")
|
|
737
|
+
break
|
|
738
|
+
|
|
739
|
+
logger.info(
|
|
740
|
+
f"[EnvironmentChecker] Device config check result: preset={preset_name}, ready={all_available}, drivers={len(drivers)}, missing_installable={len(missing_installable)}"
|
|
741
|
+
)
|
|
742
|
+
for driver in drivers:
|
|
743
|
+
logger.debug(
|
|
744
|
+
f"[EnvironmentChecker] - {driver.name}: {driver.status.value} ({driver.details})"
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
return EnvironmentReport(
|
|
748
|
+
preset_name=preset_name,
|
|
749
|
+
drivers=drivers,
|
|
750
|
+
ready=all_available,
|
|
751
|
+
missing_installable=missing_installable,
|
|
752
|
+
)
|