AQIT 0.1.0
Loading...
Searching...
No Matches
device.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""Resolve compute device: CUDA (incl. ROCm), Metal (MPS), or CPU."""
3
4from __future__ import annotations
5
6import json
7import os
8import platform
9import shutil
10import subprocess
11from typing import Any
12
13_VALID_OVERRIDES = frozenset({"cuda", "mps", "cpu", "auto"})
14
15
16def _import_torch():
17 import torch
18
19 return torch
21
22def cuda_available() -> bool:
23 try:
24 return bool(_import_torch().cuda.is_available())
25 except Exception:
26 return False
27
28
29def mps_available() -> bool:
30 try:
31 torch = _import_torch()
32 mps = getattr(torch.backends, "mps", None)
33 return bool(mps and mps.is_available())
34 except Exception:
35 return False
36
37
38def is_rocm_build() -> bool:
39 try:
40 torch = _import_torch()
41 return getattr(torch.version, "hip", None) is not None
42 except Exception:
43 return False
44
45
46def cuda_backend_label() -> str:
47 """PyTorch still uses device type 'cuda' for ROCm builds."""
48 return "rocm" if is_rocm_build() else "cuda"
49
51def _env_override() -> str | None:
52 raw = (os.environ.get("AQUIN_DEVICE") or "").strip().lower()
53 if not raw or raw == "auto":
54 return None
55 if raw not in _VALID_OVERRIDES:
56 return None
57 return raw
58
59
60def resolve_compute_device(*, prefer: str | None = None) -> str:
61 """
62 Pick torch device string: cuda → mps → cpu.
63 Override with AQUIN_DEVICE=cuda|mps|cpu|auto or `prefer`.
64 """
65 override = (prefer or _env_override() or "").strip().lower() or None
66
67 if override == "cpu":
68 return "cpu"
69 if override == "cuda":
70 if cuda_available():
71 return "cuda"
72 if mps_available():
73 return "mps"
74 return "cpu"
75 if override == "mps":
76 if mps_available():
77 return "mps"
78 if cuda_available():
79 return "cuda"
80 return "cpu"
81
82 if cuda_available():
83 return "cuda"
84 if mps_available():
85 return "mps"
86 return "cpu"
87
88
89def default_dtype_for_device(device: str | None = None) -> Any:
90 torch = _import_torch()
91 dev = device or resolve_compute_device()
92 if dev == "cuda":
93 return torch.bfloat16
94 if dev == "mps":
95 return torch.float16
96 return torch.float32
97
98
99def device_and_dtype(*, prefer: str | None = None) -> tuple[str, Any]:
100 """Convenience for train/eval modules that need both together."""
101 dev = resolve_compute_device(prefer=prefer)
102 return dev, default_dtype_for_device(dev)
104
105def accelerator_available() -> bool:
106 return resolve_compute_device() != "cpu"
107
108
109def allow_cpu_load() -> bool:
110 return os.environ.get("AQUIN_ALLOW_CPU", "").strip().lower() in (
111 "1",
112 "true",
113 "yes",
114 "on",
115 )
116
117
118def empty_device_cache(device: str | None = None) -> None:
119 dev = device or resolve_compute_device()
120 try:
121 torch = _import_torch()
122 if dev == "cuda" and torch.cuda.is_available():
123 torch.cuda.empty_cache()
124 elif dev == "mps" and mps_available() and hasattr(torch, "mps"):
125 empty = getattr(torch.mps, "empty_cache", None)
126 if callable(empty):
127 empty()
128 except Exception:
129 pass
130
131
132def synchronize_device(device: str | None = None) -> None:
133 dev = device or resolve_compute_device()
134 try:
135 torch = _import_torch()
136 if dev == "cuda" and torch.cuda.is_available():
137 torch.cuda.synchronize()
138 elif dev == "mps" and mps_available() and hasattr(torch, "mps"):
139 sync = getattr(torch.mps, "synchronize", None)
140 if callable(sync):
141 sync()
142 except Exception:
143 pass
144
145
146def is_oom_error(exc: BaseException) -> bool:
147 if exc.__class__.__name__ in ("OutOfMemoryError", "CUDAOutOfMemoryError"):
148 return True
149 msg = str(exc).lower()
150 if "out of memory" in msg:
151 return True
152 if "cuda out of memory" in msg:
153 return True
154 if "mps" in msg and "memory" in msg:
155 return True
156 return False
157
158
159def _cuda_gpu_entries() -> list[dict[str, Any]]:
160 entries: list[dict[str, Any]] = []
161 try:
162 torch = _import_torch()
163 if not torch.cuda.is_available():
164 return entries
165 for idx in range(torch.cuda.device_count()):
166 props = torch.cuda.get_device_properties(idx)
167 entries.append({
168 "index": idx,
169 "name": props.name,
170 "vram_gb": round(props.total_memory / (1024 ** 3), 1),
171 })
172 except Exception:
173 pass
174 return entries
175
176
177def _is_windows() -> bool:
178 return platform.system().lower() in ("windows", "win32") or os.name == "nt"
179
180
181def _rocm_install_hint() -> str | None:
182 if platform.system().lower() != "linux":
183 return None
184 if cuda_available():
185 return None
186 return (
187 "Linux AMD GPU: install PyTorch with ROCm "
188 "(https://pytorch.org/get-started/locally/) — pip's default torch is often CPU-only."
189 )
190
191
192def torch_version_label() -> str | None:
193 try:
194 return str(_import_torch().__version__)
195 except Exception:
196 return None
197
198
199def torch_is_cpu_wheel() -> bool:
200 label = (torch_version_label() or "").lower()
201 return "+cpu" in label
202
204def nvidia_smi_gpu_names() -> list[str]:
205 if not shutil.which("nvidia-smi"):
206 return []
207 try:
208 proc = subprocess.run(
209 ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
210 capture_output=True,
211 text=True,
212 timeout=8,
213 check=False,
214 )
215 if proc.returncode != 0:
216 return []
217 return [line.strip() for line in proc.stdout.splitlines() if line.strip()]
218 except Exception:
219 return []
220
221
222def _nvidia_cpu_torch_hint() -> str | None:
223 names = nvidia_smi_gpu_names()
224 if not names or cuda_available():
225 return None
226 ver = torch_version_label() or "unknown"
228 return (
229 f"NVIDIA GPU detected ({names[0]}) but PyTorch is CPU-only ({ver}). "
230 "Reinstall the CUDA build of torch/torchvision from pytorch.org."
231 )
232 return (
233 f"NVIDIA GPU detected ({names[0]}) but torch.cuda.is_available() is false — "
234 "check NVIDIA driver / CUDA runtime."
235 )
236
237
238def _best_fix_hint() -> str | None:
239 return (
243 or (_windows_amd_hint() if not nvidia_smi_gpu_names() else None)
244 or "Set AQUIN_ALLOW_CPU=1 for CPU-only smoke tests."
245 )
246
247
248def _windows_amd_hint() -> str | None:
249 if not _is_windows():
250 return None
252 return None
253 return (
254 "Windows AMD GPU: use WSL2 + Linux ROCm PyTorch, or CPU with AQUIN_ALLOW_CPU=1."
255 )
256
257
258def _darwin_cpu_hint() -> str | None:
259 if platform.system().lower() != "darwin":
260 return None
261 if mps_available():
262 return None
263 return "Apple Silicon needs PyTorch with MPS (Metal); reinstall compute extras in ~/.aquin/venv."
264
265
266def load_blocked_message(model_id: str) -> str:
268 if dev != "cpu":
269 return f"Cannot load '{model_id}' on device {dev}."
270 hints = []
271 primary = _best_fix_hint()
272 if primary:
273 hints.append(primary)
274 if not primary or "AQUIN_ALLOW_CPU" not in primary:
275 hints.append("For slow CPU smoke tests: AQUIN_ALLOW_CPU=1")
276 hint_lines = [h for h in hints if h]
277 body = (
278 f"Cannot load '{model_id}': no GPU backend (CUDA/ROCm or Metal MPS).\n"
279 + "\n".join(f" · {h}" for h in hint_lines)
280 )
281 return body
282
283
284def require_load_device(model_id: str, cfg: dict[str, Any] | None = None) -> str:
285 """
286 Device for aquin load model. Raises RuntimeError when only CPU and not allowed.
287 """
289 if device != "cpu":
290 return device
291 if allow_cpu_load():
292 return "cpu"
293 raise RuntimeError(load_blocked_message(model_id))
294
295
296def probe_backend() -> dict[str, Any]:
297 """JSON-friendly backend snapshot for status / diagnostics."""
298 selected = resolve_compute_device()
299 cuda = cuda_available()
301 gpus = _cuda_gpu_entries()
302 backend = "cpu"
303 if selected == "cuda":
304 backend = cuda_backend_label()
305 elif selected == "mps":
306 backend = "mps"
307
308 nvidia_hw = nvidia_smi_gpu_names()
309 torch_ver = torch_version_label()
310
311 summary_parts = [f"device={selected}", f"backend={backend}"]
312 if gpus:
313 g = gpus[0]
314 summary_parts.append(f"{g.get('name', 'GPU')} · {g.get('vram_gb', '?')} GB")
315 if len(gpus) > 1:
316 summary_parts.append(f"+{len(gpus) - 1} more")
317 elif nvidia_hw and not cuda:
318 summary_parts.append(
319 f"{nvidia_hw[0]} visible to nvidia-smi · torch={torch_ver or '?'}"
320 )
321 elif selected == "mps":
322 summary_parts.append("Apple Metal (MPS)")
323 else:
324 summary_parts.append("no GPU accelerator")
325
326 fix = None
327 if not accelerator_available():
328 fix = _best_fix_hint()
329
330 return {
331 "selected": selected,
332 "backend": backend,
333 "cuda_available": cuda,
334 "mps_available": mps,
335 "rocm": is_rocm_build(),
336 "accelerator_available": accelerator_available(),
337 "allow_cpu": allow_cpu_load(),
338 "gpus": gpus,
339 "nvidia_smi_gpus": nvidia_hw,
340 "torch_version": torch_ver,
341 "torch_cpu_wheel": torch_is_cpu_wheel(),
342 "platform": platform.system(),
343 "summary": " · ".join(summary_parts),
344 "fix": fix,
345 }
346
347
348def format_backend_summary() -> str:
349 return probe_backend()["summary"]
350
351
353 return json.dumps(probe_backend(), ensure_ascii=False)
354
bool cuda_available()
Definition device.py:26
bool is_rocm_build()
Definition device.py:42
bool mps_available()
Definition device.py:33
None empty_device_cache(str|None device=None)
Definition device.py:122
str|None _nvidia_cpu_torch_hint()
Definition device.py:226
str require_load_device(str model_id, dict[str, Any]|None cfg=None)
Definition device.py:288
tuple[str, Any] device_and_dtype(*, str|None prefer=None)
Definition device.py:103
str|None torch_version_label()
Definition device.py:196
None synchronize_device(str|None device=None)
Definition device.py:136
str cuda_backend_label()
Definition device.py:50
bool is_oom_error(BaseException exc)
Definition device.py:150
bool torch_is_cpu_wheel()
Definition device.py:203
Any default_dtype_for_device(str|None device=None)
Definition device.py:93
bool accelerator_available()
Definition device.py:109
str resolve_compute_device(*, str|None prefer=None)
Definition device.py:64
str probe_backend_json()
Definition device.py:356
bool allow_cpu_load()
Definition device.py:113
str|None _windows_amd_hint()
Definition device.py:252
list[dict[str, Any]] _cuda_gpu_entries()
Definition device.py:163
list[str] nvidia_smi_gpu_names()
Definition device.py:208
str|None _darwin_cpu_hint()
Definition device.py:262
str format_backend_summary()
Definition device.py:352
dict[str, Any] probe_backend()
Definition device.py:300
str|None _best_fix_hint()
Definition device.py:242
str|None _env_override()
Definition device.py:55
str load_blocked_message(str model_id)
Definition device.py:270
str|None _rocm_install_hint()
Definition device.py:185