1717
1818# Guard NVML imports
1919try :
20+ from cuda .core import Device as CudaDevice
2021 from cuda .core import system
2122
2223 CUDA_CORE_AVAILABLE = True
@@ -52,6 +53,7 @@ def __init__(
5253 thermal_wait : int | None = None ,
5354 thermal_cont : int | None = None ,
5455 thermal_timeout : int | None = None ,
56+ thermal_device : int | None = None ,
5557 verbose : bool = False ,
5658 ):
5759 """Initialize thermal controller.
@@ -71,10 +73,17 @@ def __init__(
7173 thermal_timeout: Maximum wait time in seconds for GPU to cool down.
7274 If None, uses default (180 seconds).
7375 Default: None
76+ thermal_device: CUDA device ordinal to monitor for thermal throttling.
77+ If set, monitor that CUDA device. If None, monitor the current
78+ CUDA device context. Device numbering follows CUDA runtime ordinals and
79+ honors ``CUDA_VISIBLE_DEVICES``.
80+ Default: None
7481 verbose: Whether to print thermal messages.
7582 Default: False
7683 """
7784 self .device : Any = None
85+ self .thermal_device = thermal_device
86+ self ._current_cuda_device_id : int | None = None
7887 self .thermal_mode = thermal_mode
7988
8089 # Set adaptive_mode based on thermal_mode
@@ -101,17 +110,111 @@ def __init__(
101110 def init (self ) -> bool :
102111 """Initialize NVML and get GPU handle.
103112
113+ Resolves the NVML device that corresponds to the profiled CUDA device
114+ (honoring ``CUDA_VISIBLE_DEVICES``).
115+
104116 Returns:
105117 True if temperature retrieval is supported, False otherwise.
106118 """
107119 if not CUDA_CORE_AVAILABLE :
108120 return False
109121
110122 if self .device is None :
111- self .device = system .Device (index = 0 )
123+ resolved_device = self ._resolve_device ()
124+ if resolved_device is None :
125+ return False
126+ self .device = resolved_device
112127
113128 return self ._is_temp_retrieval_supported ()
114129
130+ def _map_cuda_to_system_device (self , cuda_device : Any ) -> Any :
131+ """Map a CUDA device object to its corresponding NVML system device.
132+
133+ Args:
134+ cuda_device: CUDA device object to map.
135+
136+ Returns:
137+ The corresponding NVML ``system.Device``.
138+ """
139+ system_device = cuda_device .to_system_device ()
140+ self ._current_cuda_device_id = cuda_device .device_id
141+ return system_device
142+
143+ def _resolve_device (self ) -> Any :
144+ """Resolve the NVML device to monitor.
145+
146+ The CUDA device (explicitly requested via ``thermal_device`` or the
147+ current CUDA device context) is mapped to its underlying physical
148+ NVML device by UUID. This ensures the GPU actually running the profiled
149+ kernels is monitored, even when ``CUDA_VISIBLE_DEVICES`` remaps device
150+ ordinals.
151+
152+ Returns:
153+ The resolved NVML ``system.Device`` to monitor, or ``None`` if no
154+ device could be resolved.
155+ """
156+ cuda_ordinal = self .thermal_device
157+ try :
158+ # ``None`` selects the current CUDA device and already reflects
159+ # CUDA_VISIBLE_DEVICES. Mapping to system device is done by UUID.
160+ if self .verbose and cuda_ordinal is None :
161+ print (
162+ "[Thermovision] thermal_device not set; using current CUDA device context "
163+ "(set thermal_device to pin monitoring)."
164+ )
165+ cuda_device = CudaDevice (cuda_ordinal )
166+ system_device = self ._map_cuda_to_system_device (cuda_device )
167+ if self .verbose :
168+ print (
169+ f"[Thermovision] Monitoring CUDA device { self ._current_cuda_device_id } "
170+ f"(NVML index { system_device .index } , { system_device .name } , UUID { system_device .uuid } )"
171+ )
172+ return system_device
173+ except Exception as e :
174+ # Fall back to physical GPU 0 so thermal protection still works on
175+ # single-GPU systems and when CUDA cannot be initialized here.
176+ if cuda_ordinal is not None :
177+ print (
178+ f"Warning: Could not resolve thermal_device={ cuda_ordinal } "
179+ f"to an NVML device ({ e } ). Falling back to physical GPU 0."
180+ )
181+ self ._current_cuda_device_id = None
182+ try :
183+ return system .Device (index = 0 )
184+ except Exception :
185+ return None
186+
187+ def _refresh_device_for_current_cuda_context (self ) -> None :
188+ """Refresh monitored NVML device when current CUDA device changes.
189+
190+ This applies only when ``thermal_device`` is not explicitly set. It keeps
191+ Thermovision aligned with runtime CUDA context switches in user code
192+ (for example via ``torch.cuda.set_device``).
193+ """
194+ if self .thermal_device is not None :
195+ return
196+
197+ try :
198+ cuda_device = CudaDevice (None )
199+ current_cuda_id = cuda_device .device_id
200+ if (
201+ self .device is not None
202+ and self ._current_cuda_device_id == current_cuda_id
203+ ):
204+ return
205+
206+ self .device = self ._map_cuda_to_system_device (cuda_device )
207+ if self .verbose :
208+ print (
209+ f"[Thermovision] Switched monitoring to CUDA device { self ._current_cuda_device_id } "
210+ f"(NVML index { self .device .index } , { self .device .name } , UUID { self .device .uuid } )"
211+ )
212+ except Exception :
213+ # Keep existing device if refresh fails. If no device exists yet,
214+ # try the regular resolution path (including fallback).
215+ if self .device is None :
216+ self .device = self ._resolve_device ()
217+
115218 def throttle_guard (self ) -> None :
116219 """Check thermal state and pause if GPU is too hot.
117220
@@ -129,6 +232,7 @@ def throttle_guard(self) -> None:
129232 - Many iterations (>TARGET_MAX_ITERATIONS): GPU heats slowly → decrease thermal_cont (cool less)
130233 4. Wait until GPU cools back to thermal_cont, then repeat
131234 """
235+ self ._refresh_device_for_current_cuda_context ()
132236 tlimit = self ._get_gpu_tlimit ()
133237 if tlimit is None :
134238 return
0 commit comments