-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathdevice_manager.py
More file actions
309 lines (274 loc) · 11.1 KB
/
Copy pathdevice_manager.py
File metadata and controls
309 lines (274 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# Copyright 2026 FlagOS Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Device manager with lock-file based allocation. Supports CUDA, NPU, MUSA,
Iluvatar, Hygon DCU, and MetaX (MUXI) devices."""
import glob
import logging
import os
import shutil
import subprocess
import time
logger = logging.getLogger(__name__)
# Prevent torch from auto-loading chip-specific backends on domestic hardware.
# Must be set before any `import torch` — this module is imported early enough.
if os.environ.get("ASCEND_RT_VISIBLE_DEVICES") or \
os.environ.get("MUSA_VISIBLE_DEVICES") or \
os.environ.get("GEMS_VENDOR"):
os.environ.setdefault("TORCH_DEVICE_BACKEND_AUTOLOAD", "0")
def detect_device_type() -> str:
"""Detect current device type: 'cuda', 'npu', 'musa', 'iluvatar', 'hygon', or 'muxi'."""
vendor = os.environ.get("GEMS_VENDOR", "")
if vendor == "ascend" or os.environ.get("ASCEND_RT_VISIBLE_DEVICES"):
return "npu"
if vendor == "mthreads" or os.environ.get("MUSA_VISIBLE_DEVICES"):
return "musa"
if vendor == "iluvatar":
return "iluvatar"
if vendor == "hygon":
return "hygon"
if vendor == "muxi" or os.environ.get("MACA_VISIBLE_DEVICES"):
return "muxi"
# Auto-detect Ascend NPU
if not vendor:
try:
import torch_npu # noqa: F401
return "npu"
except ImportError:
pass
if os.path.isdir("/usr/local/Ascend/ascend-toolkit"):
return "npu"
# Auto-detect MUSA
if not vendor:
if os.path.isdir("/usr/local/musa") or shutil.which("mthreads-gmi"):
return "musa"
# Auto-detect Iluvatar GPU
if not vendor:
try:
result = subprocess.run(
["ixsmi", "-L"],
capture_output=True, text=True, timeout=10)
if result.returncode == 0 and "Iluvatar" in result.stdout:
return "iluvatar"
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
if glob.glob("/usr/local/corex*") or shutil.which("ixsmi"):
return "iluvatar"
try:
result = subprocess.run(
["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
capture_output=True, text=True, timeout=10)
if result.returncode == 0 and "Iluvatar" in result.stdout:
return "iluvatar"
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
# Auto-detect Hygon DCU
if not vendor:
try:
result = subprocess.run(
["rocm-smi", "--showproductname"],
capture_output=True, text=True, timeout=10)
if result.returncode == 0 and (
"Hygon" in result.stdout or "DCU" in result.stdout
or "BW" in result.stdout or "C-3000" in result.stdout
):
return "hygon"
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
# Auto-detect MetaX GPU
if not vendor:
try:
result = subprocess.run(
["mx-smi", "-L"],
capture_output=True, text=True, timeout=10)
if result.returncode == 0 and ("MetaX" in result.stdout or "MXC" in result.stdout):
return "muxi"
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
# Auto-detect Ascend NPU via npu-smi (fallback)
if not vendor:
try:
result = subprocess.run(
["npu-smi", "info"],
capture_output=True, text=True, timeout=10)
if result.returncode == 0:
return "npu"
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
return "cuda"
_VISIBLE_DEVICES_ENV = {
"cuda": "CUDA_VISIBLE_DEVICES",
"npu": "ASCEND_RT_VISIBLE_DEVICES",
"musa": "MUSA_VISIBLE_DEVICES",
"iluvatar": "CUDA_VISIBLE_DEVICES",
"hygon": "HIP_VISIBLE_DEVICES",
"muxi": "MACA_VISIBLE_DEVICES",
}
def get_device_env_var() -> str:
"""Get the environment variable name for device visibility."""
return _VISIBLE_DEVICES_ENV.get(detect_device_type(), "CUDA_VISIBLE_DEVICES")
class DeviceManager:
"""Manages device allocation using lock files to prevent conflicts."""
def __init__(self, lock_dir: str, gpu_ids: list[int] | None = None):
self.lock_dir = lock_dir
os.makedirs(lock_dir, exist_ok=True)
self.device_type = detect_device_type()
if gpu_ids is not None:
self.gpu_ids = gpu_ids
else:
self.gpu_ids = self._detect_devices()
logger.info(f"DeviceManager initialized: type={self.device_type}, devices={self.gpu_ids}")
def _detect_devices(self) -> list[int]:
"""Detect available devices based on device type."""
if self.device_type == "cuda":
return self._detect_via_cmd(
["nvidia-smi", "--query-gpu=index", "--format=csv,noheader"])
elif self.device_type == "npu":
return self._detect_npu()
elif self.device_type == "musa":
return self._detect_via_cmd(["musa-smi", "-L"])
elif self.device_type == "hygon":
return self._detect_hygon_dcu()
elif self.device_type == "muxi":
return self._detect_muxi()
return [0]
def _detect_via_cmd(self, cmd: list[str]) -> list[int]:
"""Detect devices by running a command that outputs device indices."""
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
ids = [int(line.strip()) for line in result.stdout.strip().split("\n")
if line.strip() and line.strip().isdigit()]
if ids:
return ids
except (subprocess.TimeoutExpired, FileNotFoundError, ValueError):
pass
logger.warning(f"Failed to detect devices via {cmd[0]}, defaulting to [0]")
return [0]
def _detect_npu(self) -> list[int]:
"""Detect Ascend NPU devices via npu-smi."""
try:
result = subprocess.run(
["npu-smi", "info", "-l"], capture_output=True, text=True, timeout=10)
if result.returncode == 0:
import re
ids = [int(m) for m in re.findall(r"NPU ID\s*:\s*(\d+)", result.stdout)]
if ids:
return ids
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
pass
# Fallback: detect via torch_npu
try:
import torch
import torch_npu # noqa: F401
count = torch.npu.device_count()
if count > 0:
return list(range(count))
except Exception:
pass
logger.warning("Failed to detect NPU devices, defaulting to [0]")
return [0]
def _detect_hygon_dcu(self) -> list[int]:
"""Detect Hygon DCU devices via rocm-smi."""
try:
result = subprocess.run(
["rocm-smi", "-l"], capture_output=True, text=True, timeout=10)
if result.returncode == 0:
import re
ids = [int(m) for m in re.findall(r"HCU\[(\d+)\]", result.stdout)]
if ids:
return sorted(set(ids))
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
# Fallback: detect via torch
try:
import torch
count = torch.cuda.device_count()
if count > 0:
return list(range(count))
except Exception:
pass
logger.warning("Failed to detect Hygon DCU devices, defaulting to [0]")
return [0]
def _detect_muxi(self) -> list[int]:
"""Detect MetaX (MUXI) GPU devices via mx-smi."""
try:
result = subprocess.run(
["mx-smi", "-L"], capture_output=True, text=True, timeout=10)
if result.returncode == 0:
import re
ids = [int(m) for m in re.findall(r"GPU#(\d+)", result.stdout)]
if ids:
return sorted(ids)
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
logger.warning("Failed to detect MetaX GPU devices, defaulting to [0]")
return [0]
def _lock_path(self, gpu_id: int) -> str:
return os.path.join(self.lock_dir, f"gpu_{gpu_id}.lock")
def acquire(self) -> int | None:
"""Acquire a free GPU. Returns gpu_id or None if all busy."""
for gpu_id in self.gpu_ids:
lock_path = self._lock_path(gpu_id)
if os.path.exists(lock_path):
if self._is_lock_stale(lock_path):
logger.info(f"Removing stale lock for GPU {gpu_id}")
os.remove(lock_path)
else:
continue
try:
with open(lock_path, "w") as f:
f.write(f"{os.getpid()}\n{time.time()}\n")
logger.info(f"Acquired GPU {gpu_id}")
return gpu_id
except OSError as e:
logger.warning(f"Failed to acquire GPU {gpu_id}: {e}")
continue
return None
def release(self, gpu_id: int):
"""Release a GPU lock."""
lock_path = self._lock_path(gpu_id)
if os.path.exists(lock_path):
os.remove(lock_path)
logger.info(f"Released GPU {gpu_id}")
def _is_lock_stale(self, lock_path: str) -> bool:
"""Check if a lock file's owning process is dead."""
try:
with open(lock_path) as f:
lines = f.read().strip().split("\n")
pid = int(lines[0])
os.kill(pid, 0)
return False
except (OSError, ValueError, IndexError):
return True
def release_all(self):
"""Release all locks owned by this process."""
for gpu_id in self.gpu_ids:
lock_path = self._lock_path(gpu_id)
if os.path.exists(lock_path):
try:
with open(lock_path) as f:
pid = int(f.read().strip().split("\n")[0])
if pid == os.getpid():
os.remove(lock_path)
logger.info(f"Released GPU {gpu_id}")
except (OSError, ValueError, IndexError):
pass
def available_count(self) -> int:
"""Return the number of currently available GPUs."""
count = 0
for gpu_id in self.gpu_ids:
lock_path = self._lock_path(gpu_id)
if not os.path.exists(lock_path) or self._is_lock_stale(lock_path):
count += 1
return count