88"""
99
1010try :
11- import nvidia_ml_py3 as pynvml # Preferred package
12- except ImportError :
13- import pynvml # Fallback for environments that still ship pynvml
11+ import pynvml # Provided by the nvidia-ml-py package
12+ except ImportError as exc :
13+ raise ImportError (
14+ "pynvml module not available. Install the 'nvidia-ml-py' package to enable GPU detection."
15+ ) from exc
16+
1417import subprocess
1518import json
1619import os
@@ -71,23 +74,49 @@ def _check_metal() -> bool:
7174# NVIDIA GPU Detection
7275# ============================================================================
7376
77+ def _query_nvml_cuda_version (initialized : bool = False ) -> Optional [str ]:
78+ """
79+ Retrieve CUDA driver version via NVML. If NVML is not initialized, this
80+ function will initialize and shut it down automatically.
81+ """
82+ try :
83+ if not initialized :
84+ pynvml .nvmlInit ()
85+ version = pynvml .nvmlSystemGetCudaDriverVersion ()
86+ major = version // 1000
87+ minor = (version % 1000 ) // 10
88+ return f"{ major } .{ minor } "
89+ except Exception :
90+ return None
91+ finally :
92+ if not initialized :
93+ try :
94+ pynvml .nvmlShutdown ()
95+ except Exception :
96+ pass
97+
98+
7499async def detect_nvidia_gpu () -> Optional [Dict ]:
75100 """Detect NVIDIA GPUs using pynvml and nvidia-smi"""
101+ cuda_version_hint : Optional [str ] = None
102+
76103 try :
77104 pynvml .nvmlInit ()
78-
105+ cuda_version_hint = _query_nvml_cuda_version (initialized = True )
106+
79107 device_count = pynvml .nvmlDeviceGetCount ()
80108 if device_count == 0 :
81109 logger .debug ("NVML reported zero NVIDIA devices; falling back to nvidia-smi detection" )
82- raise RuntimeError ( "NVML reported zero devices" )
110+ return await _detect_nvidia_via_smi ( cuda_version_hint )
83111
84112 gpus = []
85113
86114 for i in range (device_count ):
87115 handle = pynvml .nvmlDeviceGetHandleByIndex (i )
88116
89117 # Get basic info
90- name = pynvml .nvmlDeviceGetName (handle ).decode ('utf-8' )
118+ raw_name = pynvml .nvmlDeviceGetName (handle )
119+ name = raw_name .decode ('utf-8' ) if isinstance (raw_name , bytes ) else str (raw_name )
91120 memory_info = pynvml .nvmlDeviceGetMemoryInfo (handle )
92121
93122 # Get compute capability
@@ -146,30 +175,45 @@ async def detect_nvidia_gpu() -> Optional[Dict]:
146175 except Exception as exc :
147176 logger .debug (f"Failed to detect NVIDIA GPUs via NVML: { exc } " )
148177 # Fallback to nvidia-smi
149- return await _detect_nvidia_via_smi ()
178+ return await _detect_nvidia_via_smi (cuda_version_hint )
179+ finally :
180+ try :
181+ pynvml .nvmlShutdown ()
182+ except Exception :
183+ pass
150184
151185
152- async def _detect_nvidia_via_smi () -> Optional [Dict ]:
186+ async def _detect_nvidia_via_smi (cuda_version_hint : Optional [ str ] = None ) -> Optional [Dict ]:
153187 """Fallback NVIDIA detection using nvidia-smi"""
154188 try :
189+ query_fields = [
190+ "index" ,
191+ "name" ,
192+ "memory.total" ,
193+ "memory.free" ,
194+ "memory.used" ,
195+ "compute_cap" ,
196+ "driver_version" ,
197+ ]
198+
155199 result = subprocess .run (
156200 [
157201 "nvidia-smi" ,
158- "--query-gpu=index,name,memory.total,memory.free,memory.used,compute_cap,driver_version,cuda_version " ,
202+ f "--query-gpu={ ',' . join ( query_fields ) } " ,
159203 "--format=csv,noheader,nounits" ,
160204 ],
161205 capture_output = True ,
162206 text = True ,
163- check = True
207+ check = True ,
164208 )
165-
209+
166210 gpus = []
167211 lines = result .stdout .strip ().split ('\n ' )
168-
169- reported_cuda_version = None
212+
213+ reported_cuda_version = cuda_version_hint
170214 for line in lines :
171215 parts = [p .strip () for p in line .split (',' )]
172- if len (parts ) >= 8 :
216+ if len (parts ) >= len ( query_fields ) :
173217 # nvidia-smi reports memory in MiB when using nounits
174218 try :
175219 total_bytes = int (parts [2 ]) * 1024 * 1024
@@ -180,8 +224,11 @@ async def _detect_nvidia_via_smi() -> Optional[Dict]:
180224
181225 compute_capability = parts [5 ]
182226 driver_version = parts [6 ]
183- cuda_version = parts [7 ]
184- reported_cuda_version = cuda_version or reported_cuda_version
227+ cuda_version = None
228+ if len (parts ) > 7 :
229+ cuda_version = parts [7 ].strip () or cuda_version
230+ if cuda_version and not reported_cuda_version :
231+ reported_cuda_version = cuda_version
185232
186233 gpu_info = {
187234 "index" : int (parts [0 ]),
@@ -196,7 +243,22 @@ async def _detect_nvidia_via_smi() -> Optional[Dict]:
196243 "cuda_version" : cuda_version ,
197244 }
198245 gpus .append (gpu_info )
199-
246+
247+ if reported_cuda_version in (None , "" , "Unknown" ):
248+ try :
249+ detail_output = subprocess .run (
250+ ["nvidia-smi" , "-q" ],
251+ capture_output = True ,
252+ text = True ,
253+ check = True ,
254+ ).stdout
255+ for line in detail_output .splitlines ():
256+ if "CUDA Version" in line :
257+ reported_cuda_version = line .split (":" , 1 )[1 ].strip () or reported_cuda_version
258+ break
259+ except subprocess .CalledProcessError :
260+ pass
261+
200262 return {
201263 "vendor" : "nvidia" ,
202264 "cuda_version" : reported_cuda_version or "Unknown" ,
@@ -205,7 +267,7 @@ async def _detect_nvidia_via_smi() -> Optional[Dict]:
205267 "total_vram" : sum (gpu ["memory" ]["total" ] for gpu in gpus ),
206268 "available_vram" : sum (gpu ["memory" ]["free" ] for gpu in gpus )
207269 }
208-
270+
209271 except (subprocess .CalledProcessError , FileNotFoundError ):
210272 return None
211273
0 commit comments