AQIT
0.1.0
Toggle main menu visibility
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
4
from
__future__
import
annotations
5
6
import
json
7
import
os
8
import
platform
9
import
shutil
10
import
subprocess
11
from
typing
import
Any
12
13
_VALID_OVERRIDES = frozenset({
"cuda"
,
"mps"
,
"cpu"
,
"auto"
})
14
15
16
def
_import_torch
():
17
import
torch
18
19
return
torch
20
21
22
def
cuda_available
() -> bool:
23
try
:
24
return
bool(
_import_torch
().cuda.is_available())
25
except
Exception:
26
return
False
27
28
29
def
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
38
def
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
46
def
cuda_backend_label
() -> str:
47
"""PyTorch still uses device type 'cuda' for ROCm builds."""
48
return
"rocm"
if
is_rocm_build
()
else
"cuda"
49
50
51
def
_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
60
def
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
89
def
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
99
def
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)
103
104
105
def
accelerator_available
() -> bool:
106
return
resolve_compute_device
() !=
"cpu"
107
108
109
def
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
118
def
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
132
def
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
146
def
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
159
def
_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
177
def
_is_windows
() -> bool:
178
return
platform.system().lower()
in
(
"windows"
,
"win32"
)
or
os.name ==
"nt"
179
180
181
def
_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
192
def
torch_version_label
() -> str | None:
193
try
:
194
return
str(
_import_torch
().__version__)
195
except
Exception:
196
return
None
197
198
199
def
torch_is_cpu_wheel
() -> bool:
200
label = (
torch_version_label
()
or
""
).lower()
201
return
"+cpu"
in
label
202
203
204
def
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
222
def
_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"
227
if
torch_is_cpu_wheel
()
or
not
cuda_available
():
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
238
def
_best_fix_hint
() -> str | None:
239
return
(
240
_nvidia_cpu_torch_hint
()
241
or
_darwin_cpu_hint
()
242
or
_rocm_install_hint
()
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
248
def
_windows_amd_hint
() -> str | None:
249
if
not
_is_windows
():
250
return
None
251
if
cuda_available
()
or
mps_available
():
252
return
None
253
return
(
254
"Windows AMD GPU: use WSL2 + Linux ROCm PyTorch, or CPU with AQUIN_ALLOW_CPU=1."
255
)
256
257
258
def
_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
266
def
load_blocked_message
(model_id: str) -> str:
267
dev =
resolve_compute_device
()
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
284
def
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
"""
288
device =
resolve_compute_device
()
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
296
def
probe_backend
() -> dict[str, Any]:
297
"""JSON-friendly backend snapshot for status / diagnostics."""
298
selected =
resolve_compute_device
()
299
cuda =
cuda_available
()
300
mps =
mps_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
348
def
format_backend_summary
() -> str:
349
return
probe_backend
()[
"summary"
]
350
351
352
def
probe_backend_json
() -> str:
353
return
json.dumps(
probe_backend
(), ensure_ascii=
False
)
354
aquin.compute.device.cuda_available
bool cuda_available()
Definition
device.py:26
aquin.compute.device.is_rocm_build
bool is_rocm_build()
Definition
device.py:42
aquin.compute.device.mps_available
bool mps_available()
Definition
device.py:33
aquin.compute.device.empty_device_cache
None empty_device_cache(str|None device=None)
Definition
device.py:122
aquin.compute.device._nvidia_cpu_torch_hint
str|None _nvidia_cpu_torch_hint()
Definition
device.py:226
aquin.compute.device.require_load_device
str require_load_device(str model_id, dict[str, Any]|None cfg=None)
Definition
device.py:288
aquin.compute.device.device_and_dtype
tuple[str, Any] device_and_dtype(*, str|None prefer=None)
Definition
device.py:103
aquin.compute.device.torch_version_label
str|None torch_version_label()
Definition
device.py:196
aquin.compute.device.synchronize_device
None synchronize_device(str|None device=None)
Definition
device.py:136
aquin.compute.device._import_torch
_import_torch()
Definition
device.py:20
aquin.compute.device.cuda_backend_label
str cuda_backend_label()
Definition
device.py:50
aquin.compute.device.is_oom_error
bool is_oom_error(BaseException exc)
Definition
device.py:150
aquin.compute.device.torch_is_cpu_wheel
bool torch_is_cpu_wheel()
Definition
device.py:203
aquin.compute.device._is_windows
bool _is_windows()
Definition
device.py:181
aquin.compute.device.default_dtype_for_device
Any default_dtype_for_device(str|None device=None)
Definition
device.py:93
aquin.compute.device.accelerator_available
bool accelerator_available()
Definition
device.py:109
aquin.compute.device.resolve_compute_device
str resolve_compute_device(*, str|None prefer=None)
Definition
device.py:64
aquin.compute.device.probe_backend_json
str probe_backend_json()
Definition
device.py:356
aquin.compute.device.allow_cpu_load
bool allow_cpu_load()
Definition
device.py:113
aquin.compute.device._windows_amd_hint
str|None _windows_amd_hint()
Definition
device.py:252
aquin.compute.device._cuda_gpu_entries
list[dict[str, Any]] _cuda_gpu_entries()
Definition
device.py:163
aquin.compute.device.nvidia_smi_gpu_names
list[str] nvidia_smi_gpu_names()
Definition
device.py:208
aquin.compute.device._darwin_cpu_hint
str|None _darwin_cpu_hint()
Definition
device.py:262
aquin.compute.device.format_backend_summary
str format_backend_summary()
Definition
device.py:352
aquin.compute.device.probe_backend
dict[str, Any] probe_backend()
Definition
device.py:300
aquin.compute.device._best_fix_hint
str|None _best_fix_hint()
Definition
device.py:242
aquin.compute.device._env_override
str|None _env_override()
Definition
device.py:55
aquin.compute.device.load_blocked_message
str load_blocked_message(str model_id)
Definition
device.py:270
aquin.compute.device._rocm_install_hint
str|None _rocm_install_hint()
Definition
device.py:185
aquin
compute
device.py
AQIT · Aquin Labs Private Limited · Apache 2.0 · Generated by
1.18.0