syvain-training-utils 0.0.170__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.3
2
+ Name: syvain-training-utils
3
+ Version: 0.0.170
4
+ Summary: Shared runtime, diagnostics, and checkpoint utilities for Syvain training runs
5
+ Requires-Dist: obstore>=0.11.0,<0.12.0
6
+ Requires-Dist: pydantic>=2.13.4
7
+ Requires-Dist: torch>=2.13.0
8
+ Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
9
+ Requires-Dist: ruff>=0.15.12 ; extra == 'dev'
10
+ Requires-Dist: ty>=0.0.34 ; extra == 'dev'
11
+ Requires-Python: >=3.14, <3.15
12
+ Provides-Extra: dev
13
+ Description-Content-Type: text/markdown
14
+
15
+ # syvain-training-utils
16
+
17
+ Internal [Syvain](https://syvain.com/) helpers for small, explicit ML training
18
+ runs. No secret sauce here, just shared runtime, device-diagnostic, and
19
+ checkpoint patterns.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ uv add syvain-training-utils
25
+ ```
26
+
27
+ ## Runtime setup
28
+
29
+ ```python
30
+ from syvain_training_utils import (
31
+ generate_run_id,
32
+ require_torch_compile_toolchain,
33
+ select_device,
34
+ )
35
+
36
+ run_id = generate_run_id()
37
+ device = select_device()
38
+ require_torch_compile_toolchain()
39
+ ```
40
+
41
+ ## Device smoke test
42
+
43
+ ```python
44
+ import json
45
+
46
+ from syvain_training_utils import run_device_smoke_test
47
+
48
+ report = run_device_smoke_test(require_cuda=True)
49
+ print(json.dumps({"smoke": report}, indent=2, sort_keys=True))
50
+ ```
51
+
52
+ ## Checkpoint a training run
53
+
54
+ ```python
55
+ from syvain_training_utils import (
56
+ TrainingLoopState,
57
+ load_training_checkpoint_if_available,
58
+ save_model_checkpoint,
59
+ )
60
+
61
+ loop_state = TrainingLoopState(
62
+ global_step=global_step,
63
+ curriculum_stage=curriculum_stage,
64
+ curriculum_step=curriculum_step,
65
+ )
66
+
67
+ save_model_checkpoint(
68
+ object_store=object_store,
69
+ base_path=base_model_path,
70
+ experiment_slug=experiment_slug,
71
+ run_id=run_id,
72
+ model=model,
73
+ optimizer=optimizer,
74
+ scheduler=scheduler,
75
+ loop_state=loop_state,
76
+ checkpoint_label=f"step-{global_step:012d}",
77
+ )
78
+
79
+ resume = load_training_checkpoint_if_available(
80
+ object_store=object_store,
81
+ base_path=base_model_path,
82
+ model=model,
83
+ optimizer=optimizer,
84
+ scheduler=scheduler,
85
+ device=device,
86
+ )
87
+ ```
88
+
89
+ The manifest remains a small pointer to the current checkpoint. Each checkpoint
90
+ contains the model, optimizer, optional scheduler, and PyTorch RNG state needed
91
+ by the common single-file training-run contract.
@@ -0,0 +1,77 @@
1
+ # syvain-training-utils
2
+
3
+ Internal [Syvain](https://syvain.com/) helpers for small, explicit ML training
4
+ runs. No secret sauce here, just shared runtime, device-diagnostic, and
5
+ checkpoint patterns.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ uv add syvain-training-utils
11
+ ```
12
+
13
+ ## Runtime setup
14
+
15
+ ```python
16
+ from syvain_training_utils import (
17
+ generate_run_id,
18
+ require_torch_compile_toolchain,
19
+ select_device,
20
+ )
21
+
22
+ run_id = generate_run_id()
23
+ device = select_device()
24
+ require_torch_compile_toolchain()
25
+ ```
26
+
27
+ ## Device smoke test
28
+
29
+ ```python
30
+ import json
31
+
32
+ from syvain_training_utils import run_device_smoke_test
33
+
34
+ report = run_device_smoke_test(require_cuda=True)
35
+ print(json.dumps({"smoke": report}, indent=2, sort_keys=True))
36
+ ```
37
+
38
+ ## Checkpoint a training run
39
+
40
+ ```python
41
+ from syvain_training_utils import (
42
+ TrainingLoopState,
43
+ load_training_checkpoint_if_available,
44
+ save_model_checkpoint,
45
+ )
46
+
47
+ loop_state = TrainingLoopState(
48
+ global_step=global_step,
49
+ curriculum_stage=curriculum_stage,
50
+ curriculum_step=curriculum_step,
51
+ )
52
+
53
+ save_model_checkpoint(
54
+ object_store=object_store,
55
+ base_path=base_model_path,
56
+ experiment_slug=experiment_slug,
57
+ run_id=run_id,
58
+ model=model,
59
+ optimizer=optimizer,
60
+ scheduler=scheduler,
61
+ loop_state=loop_state,
62
+ checkpoint_label=f"step-{global_step:012d}",
63
+ )
64
+
65
+ resume = load_training_checkpoint_if_available(
66
+ object_store=object_store,
67
+ base_path=base_model_path,
68
+ model=model,
69
+ optimizer=optimizer,
70
+ scheduler=scheduler,
71
+ device=device,
72
+ )
73
+ ```
74
+
75
+ The manifest remains a small pointer to the current checkpoint. Each checkpoint
76
+ contains the model, optimizer, optional scheduler, and PyTorch RNG state needed
77
+ by the common single-file training-run contract.
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "syvain-training-utils"
3
+ version = "0.0.170"
4
+ description = "Shared runtime, diagnostics, and checkpoint utilities for Syvain training runs"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14,<3.15"
7
+ dependencies = [
8
+ "obstore>=0.11.0,<0.12.0",
9
+ "pydantic>=2.13.4",
10
+ "torch>=2.13.0",
11
+ ]
12
+
13
+ [project.optional-dependencies]
14
+ dev = [
15
+ "pytest>=8.0.0",
16
+ "ruff>=0.15.12",
17
+ "ty>=0.0.34",
18
+ ]
19
+
20
+ [tool.pytest.ini_options]
21
+ testpaths = ["tests"]
22
+
23
+ [build-system]
24
+ requires = ["uv_build>=0.11.9,<0.12"]
25
+ build-backend = "uv_build"
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "syvain-training-utils"
3
+ version = "0.0.170"
4
+ description = "Shared runtime, diagnostics, and checkpoint utilities for Syvain training runs"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14,<3.15"
7
+ dependencies = [
8
+ "obstore>=0.11.0,<0.12.0",
9
+ "pydantic>=2.13.4",
10
+ "torch>=2.13.0",
11
+ ]
12
+
13
+ [project.optional-dependencies]
14
+ dev = ["pytest>=8.0.0", "ruff>=0.15.12", "ty>=0.0.34"]
15
+
16
+ [tool.pytest.ini_options]
17
+ testpaths = ["tests"]
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.11.9,<0.12"]
21
+ build-backend = "uv_build"
@@ -0,0 +1,35 @@
1
+ from syvain_training_utils.checkpoint import (
2
+ CheckpointManifest,
3
+ ResumeCheckpoint,
4
+ TrainingLoopState,
5
+ load_training_checkpoint_if_available,
6
+ save_model_checkpoint,
7
+ )
8
+ from syvain_training_utils.diagnostics import (
9
+ DeviceSmokeError,
10
+ collect_device_diagnostics,
11
+ nvidia_smi_diagnostics,
12
+ run_device_smoke_test,
13
+ )
14
+ from syvain_training_utils.runtime import (
15
+ generate_run_id,
16
+ require_torch_compile_toolchain,
17
+ runtime_c_compiler,
18
+ select_device,
19
+ )
20
+
21
+ __all__ = [
22
+ "CheckpointManifest",
23
+ "DeviceSmokeError",
24
+ "ResumeCheckpoint",
25
+ "TrainingLoopState",
26
+ "collect_device_diagnostics",
27
+ "generate_run_id",
28
+ "load_training_checkpoint_if_available",
29
+ "nvidia_smi_diagnostics",
30
+ "require_torch_compile_toolchain",
31
+ "run_device_smoke_test",
32
+ "runtime_c_compiler",
33
+ "save_model_checkpoint",
34
+ "select_device",
35
+ ]
@@ -0,0 +1,301 @@
1
+ import json
2
+ from collections.abc import Mapping
3
+ from dataclasses import dataclass
4
+ from io import BytesIO
5
+ from typing import Any, Literal
6
+
7
+ import torch
8
+ from obstore.store import S3Store
9
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
10
+ from torch.torch_version import TorchVersion
11
+
12
+
13
+ class ResumeCheckpoint(BaseModel):
14
+ model_config = ConfigDict(extra="forbid", frozen=True, strict=True)
15
+
16
+ experiment_slug: str = Field(min_length=1)
17
+ run_id: str = Field(min_length=1)
18
+ checkpoint_label: str = Field(min_length=1)
19
+ global_step: int = Field(ge=0)
20
+ curriculum_stage: int = Field(ge=0)
21
+ curriculum_step: int = Field(ge=0)
22
+ torch_version: str = Field(min_length=1)
23
+
24
+
25
+ class CheckpointManifest(BaseModel):
26
+ model_config = ConfigDict(extra="forbid", frozen=True, strict=True)
27
+
28
+ version: Literal[1]
29
+ checkpoint_key: str = Field(min_length=1)
30
+
31
+
32
+ class _CheckpointPayload(BaseModel):
33
+ model_config = ConfigDict(extra="forbid", frozen=True, strict=True)
34
+
35
+ version: Literal[1]
36
+ checkpoint: ResumeCheckpoint
37
+ checkpoint_data: dict[str, Any]
38
+
39
+
40
+ @dataclass
41
+ class TrainingLoopState:
42
+ global_step: int
43
+ curriculum_stage: int
44
+ curriculum_step: int
45
+
46
+
47
+ def _cpu_checkpoint_value(value: Any) -> Any:
48
+ if isinstance(value, torch.Tensor):
49
+ return value.detach().cpu()
50
+ if isinstance(value, dict):
51
+ return {key: _cpu_checkpoint_value(inner) for key, inner in value.items()}
52
+ if isinstance(value, list):
53
+ return [_cpu_checkpoint_value(inner) for inner in value]
54
+ if isinstance(value, tuple):
55
+ return tuple(_cpu_checkpoint_value(inner) for inner in value)
56
+ return value
57
+
58
+
59
+ def _checkpoint_module(model: torch.nn.Module) -> torch.nn.Module:
60
+ original_module = getattr(model, "_orig_mod", None)
61
+ if isinstance(original_module, torch.nn.Module):
62
+ return original_module
63
+ return model
64
+
65
+
66
+ def _capture_torch_rng_state() -> dict[str, Any]:
67
+ state: dict[str, Any] = {
68
+ "version": 1,
69
+ "cpu": _cpu_checkpoint_value(torch.random.get_rng_state()),
70
+ }
71
+
72
+ if torch.cuda.is_available():
73
+ state["cuda"] = _cpu_checkpoint_value(torch.cuda.get_rng_state_all())
74
+
75
+ xpu_backend = getattr(torch, "xpu", None)
76
+ if xpu_backend is not None and bool(xpu_backend.is_available()):
77
+ state["xpu"] = _cpu_checkpoint_value(xpu_backend.get_rng_state_all())
78
+
79
+ mps_backend = getattr(torch.backends, "mps", None)
80
+ torch_mps = getattr(torch, "mps", None)
81
+ if (
82
+ mps_backend is not None
83
+ and torch_mps is not None
84
+ and bool(mps_backend.is_available())
85
+ ):
86
+ state["mps"] = _cpu_checkpoint_value(torch_mps.get_rng_state())
87
+
88
+ return state
89
+
90
+
91
+ def _validate_accelerator_rng_states(value: Any, *, backend: str) -> list[torch.Tensor]:
92
+ if not isinstance(value, list):
93
+ raise TypeError(f"Checkpoint {backend} RNG state must be a list")
94
+
95
+ states: list[torch.Tensor] = []
96
+ for rng_state in value:
97
+ if not isinstance(rng_state, torch.Tensor):
98
+ raise TypeError(f"Checkpoint {backend} RNG states must be tensors")
99
+ states.append(rng_state.detach().cpu())
100
+ return states
101
+
102
+
103
+ def _restore_torch_rng_state(state: Mapping[str, Any], *, device: torch.device) -> None:
104
+ if state.get("version") != 1:
105
+ raise RuntimeError("Checkpoint torch RNG state has unsupported version")
106
+
107
+ cpu_state = state.get("cpu")
108
+ if not isinstance(cpu_state, torch.Tensor):
109
+ raise TypeError("Checkpoint torch RNG state has no CPU state")
110
+ torch.random.set_rng_state(cpu_state.detach().cpu())
111
+
112
+ if device.type == "cuda":
113
+ if not torch.cuda.is_available():
114
+ raise RuntimeError("CUDA resume requested but CUDA is not available")
115
+ if "cuda" not in state:
116
+ raise RuntimeError(
117
+ "CUDA resume requested but checkpoint has no CUDA RNG state"
118
+ )
119
+ cuda_states = _validate_accelerator_rng_states(state["cuda"], backend="CUDA")
120
+ if len(cuda_states) != torch.cuda.device_count():
121
+ raise RuntimeError(
122
+ "Checkpoint CUDA RNG state device count does not match runtime"
123
+ )
124
+ torch.cuda.set_rng_state_all(cuda_states)
125
+
126
+ xpu_backend = getattr(torch, "xpu", None)
127
+ xpu_available = xpu_backend is not None and bool(xpu_backend.is_available())
128
+ if device.type == "xpu":
129
+ if not xpu_available:
130
+ raise RuntimeError("XPU resume requested but XPU is not available")
131
+ if "xpu" not in state:
132
+ raise RuntimeError(
133
+ "XPU resume requested but checkpoint has no XPU RNG state"
134
+ )
135
+ xpu_states = _validate_accelerator_rng_states(state["xpu"], backend="XPU")
136
+ if len(xpu_states) != xpu_backend.device_count():
137
+ raise RuntimeError(
138
+ "Checkpoint XPU RNG state device count does not match runtime"
139
+ )
140
+ xpu_backend.set_rng_state_all(xpu_states)
141
+
142
+ mps_backend = getattr(torch.backends, "mps", None)
143
+ torch_mps = getattr(torch, "mps", None)
144
+ mps_available = (
145
+ mps_backend is not None
146
+ and torch_mps is not None
147
+ and bool(mps_backend.is_available())
148
+ )
149
+ if device.type == "mps":
150
+ if not mps_available:
151
+ raise RuntimeError("MPS resume requested but MPS is not available")
152
+ if "mps" not in state:
153
+ raise RuntimeError(
154
+ "MPS resume requested but checkpoint has no MPS RNG state"
155
+ )
156
+ mps_state = state["mps"]
157
+ if not isinstance(mps_state, torch.Tensor):
158
+ raise RuntimeError("Checkpoint MPS RNG state must be a tensor")
159
+ torch_mps.set_rng_state(mps_state.detach().cpu())
160
+
161
+
162
+ _CHECKPOINT_MANIFEST_FILENAME = "checkpoint-manifest-v1.json"
163
+
164
+
165
+ def save_model_checkpoint(
166
+ *,
167
+ object_store: S3Store,
168
+ base_path: str,
169
+ experiment_slug: str,
170
+ run_id: str,
171
+ model: torch.nn.Module,
172
+ optimizer: torch.optim.Optimizer,
173
+ scheduler: torch.optim.lr_scheduler.LRScheduler | None,
174
+ loop_state: TrainingLoopState,
175
+ checkpoint_label: str,
176
+ ) -> str:
177
+ """Save model/training state and update the base-path manifest pointer."""
178
+ checkpoint_filename = f"{checkpoint_label}.pt"
179
+ checkpoint_key = (
180
+ f"{base_path.lstrip('/').rstrip('/')}/runs/{run_id}/checkpoints/"
181
+ f"{checkpoint_filename}"
182
+ )
183
+
184
+ resume_checkpoint = ResumeCheckpoint(
185
+ experiment_slug=experiment_slug,
186
+ run_id=run_id,
187
+ checkpoint_label=checkpoint_label,
188
+ global_step=loop_state.global_step,
189
+ curriculum_stage=loop_state.curriculum_stage,
190
+ curriculum_step=loop_state.curriculum_step,
191
+ torch_version=str(torch.__version__),
192
+ )
193
+ checkpoint = {
194
+ "version": 1,
195
+ "checkpoint": resume_checkpoint.model_dump(mode="json"),
196
+ "checkpoint_data": {
197
+ "model_state_dict": _cpu_checkpoint_value(
198
+ _checkpoint_module(model).state_dict()
199
+ ),
200
+ "optimizer_state_dict": _cpu_checkpoint_value(optimizer.state_dict()),
201
+ "scheduler_state_dict": (
202
+ None
203
+ if scheduler is None
204
+ else _cpu_checkpoint_value(scheduler.state_dict())
205
+ ),
206
+ "torch_rng_state": _capture_torch_rng_state(),
207
+ },
208
+ }
209
+
210
+ checkpoint_body = BytesIO()
211
+ torch.save(checkpoint, checkpoint_body)
212
+ object_store.put(
213
+ checkpoint_key,
214
+ checkpoint_body.getvalue(),
215
+ attributes={"Content-Type": "application/octet-stream"},
216
+ )
217
+
218
+ manifest_key = (
219
+ f"{base_path.lstrip('/').rstrip('/')}/{_CHECKPOINT_MANIFEST_FILENAME}"
220
+ )
221
+ manifest = CheckpointManifest(version=1, checkpoint_key=checkpoint_key)
222
+ manifest_body = (manifest.model_dump_json(indent=2) + "\n").encode("utf-8")
223
+ object_store.put(
224
+ manifest_key,
225
+ manifest_body,
226
+ attributes={"Content-Type": "application/json"},
227
+ )
228
+ return checkpoint_key
229
+
230
+
231
+ def load_training_checkpoint_if_available(
232
+ *,
233
+ object_store: S3Store,
234
+ base_path: str,
235
+ model: torch.nn.Module,
236
+ optimizer: torch.optim.Optimizer,
237
+ scheduler: torch.optim.lr_scheduler.LRScheduler | None,
238
+ device: torch.device,
239
+ ) -> ResumeCheckpoint | None:
240
+ """Restore the manifest-selected checkpoint, or return None when absent."""
241
+ manifest_key = (
242
+ f"{base_path.lstrip('/').rstrip('/')}/{_CHECKPOINT_MANIFEST_FILENAME}"
243
+ )
244
+ try:
245
+ manifest_body = bytes(object_store.get(manifest_key).bytes())
246
+ raw_manifest = json.loads(manifest_body.decode("utf-8"))
247
+ manifest = CheckpointManifest.model_validate(raw_manifest)
248
+ except json.JSONDecodeError as exc:
249
+ raise RuntimeError(
250
+ f"Checkpoint manifest is not valid JSON: {manifest_key}"
251
+ ) from exc
252
+ except ValidationError as exc:
253
+ raise RuntimeError(
254
+ f"Checkpoint manifest has invalid shape: {manifest_key}"
255
+ ) from exc
256
+ except FileNotFoundError:
257
+ return None
258
+
259
+ checkpoint_key = manifest.checkpoint_key
260
+ checkpoint_body = BytesIO(bytes(object_store.get(checkpoint_key).bytes()))
261
+ with torch.serialization.safe_globals([TorchVersion]):
262
+ checkpoint_object = torch.load(
263
+ checkpoint_body,
264
+ map_location=device,
265
+ weights_only=True,
266
+ )
267
+ if not isinstance(checkpoint_object, dict):
268
+ raise TypeError(f"Checkpoint {checkpoint_key} must contain a mapping")
269
+
270
+ try:
271
+ checkpoint_payload = _CheckpointPayload.model_validate(checkpoint_object)
272
+ except ValidationError as exc:
273
+ raise RuntimeError(f"Checkpoint {checkpoint_key} has invalid payload") from exc
274
+
275
+ checkpoint_data = checkpoint_payload.checkpoint_data
276
+ model_state = checkpoint_data.get("model_state_dict")
277
+ optimizer_state = checkpoint_data.get("optimizer_state_dict")
278
+ scheduler_state = checkpoint_data.get("scheduler_state_dict")
279
+ torch_rng_state = checkpoint_data.get("torch_rng_state")
280
+ if not isinstance(model_state, Mapping):
281
+ raise TypeError(f"Checkpoint {checkpoint_key} has no model state")
282
+ if not isinstance(optimizer_state, dict):
283
+ raise TypeError(f"Checkpoint {checkpoint_key} has no optimizer state")
284
+ if not isinstance(torch_rng_state, Mapping):
285
+ raise TypeError(f"Checkpoint {checkpoint_key} has no torch RNG state")
286
+
287
+ _checkpoint_module(model).load_state_dict(model_state)
288
+ optimizer.load_state_dict(optimizer_state)
289
+ if scheduler is None:
290
+ if scheduler_state is not None:
291
+ raise RuntimeError(
292
+ f"Checkpoint {checkpoint_key} has scheduler state "
293
+ "but current config disables the scheduler"
294
+ )
295
+ else:
296
+ if not isinstance(scheduler_state, dict):
297
+ raise RuntimeError(f"Checkpoint {checkpoint_key} has no scheduler state")
298
+ scheduler.load_state_dict(scheduler_state)
299
+
300
+ _restore_torch_rng_state(dict(torch_rng_state), device=device)
301
+ return checkpoint_payload.checkpoint
@@ -0,0 +1,197 @@
1
+ import os
2
+ import subprocess
3
+ import time
4
+ from typing import Any
5
+
6
+ import torch
7
+
8
+ from syvain_training_utils.runtime import select_device
9
+
10
+
11
+ class DeviceSmokeError(RuntimeError):
12
+ """A device smoke failure with the diagnostics collected before the failure."""
13
+
14
+ def __init__(self, message: str, diagnostics: dict[str, Any]) -> None:
15
+ super().__init__(message)
16
+ self.diagnostics = diagnostics
17
+
18
+
19
+ def nvidia_smi_diagnostics() -> dict[str, object]:
20
+ """Collect a small, non-failing nvidia-smi report."""
21
+ try:
22
+ result = subprocess.run(
23
+ (
24
+ "nvidia-smi",
25
+ "--query-gpu=index,name,driver_version,memory.total",
26
+ "--format=csv,noheader",
27
+ ),
28
+ text=True,
29
+ stdout=subprocess.PIPE,
30
+ stderr=subprocess.STDOUT,
31
+ check=False,
32
+ )
33
+ except FileNotFoundError:
34
+ return {
35
+ "available": False,
36
+ "output": "nvidia-smi executable not found",
37
+ }
38
+ return {
39
+ "available": result.returncode == 0,
40
+ "returncode": result.returncode,
41
+ "output": result.stdout.strip(),
42
+ }
43
+
44
+
45
+ def collect_device_diagnostics() -> dict[str, Any]:
46
+ """Describe visible CUDA, XPU, and MPS devices without selecting one."""
47
+ cuda_available = torch.cuda.is_available()
48
+ diagnostics: dict[str, Any] = {
49
+ "torch_version": torch.__version__,
50
+ "cuda_version": torch.version.cuda,
51
+ "environment": {
52
+ "CUDA_VISIBLE_DEVICES": os.environ.get("CUDA_VISIBLE_DEVICES"),
53
+ "NVIDIA_VISIBLE_DEVICES": os.environ.get("NVIDIA_VISIBLE_DEVICES"),
54
+ "SKYPILOT_NUM_GPUS_PER_NODE": os.environ.get("SKYPILOT_NUM_GPUS_PER_NODE"),
55
+ },
56
+ "nvidia_smi": nvidia_smi_diagnostics(),
57
+ "cuda": {
58
+ "available": cuda_available,
59
+ "device_count": torch.cuda.device_count(),
60
+ "devices": [],
61
+ },
62
+ "mps": {
63
+ "built": False,
64
+ "available": False,
65
+ },
66
+ "xpu": {
67
+ "available": False,
68
+ "device_count": 0,
69
+ "devices": [],
70
+ },
71
+ }
72
+
73
+ if cuda_available:
74
+ for index in range(torch.cuda.device_count()):
75
+ properties = torch.cuda.get_device_properties(index)
76
+ cuda_device: dict[str, Any] = {
77
+ "index": index,
78
+ "name": torch.cuda.get_device_name(index),
79
+ "compute_capability": f"{properties.major}.{properties.minor}",
80
+ "total_memory_gb": round(properties.total_memory / (1024**3), 3),
81
+ }
82
+ try:
83
+ free_bytes, total_bytes = torch.cuda.mem_get_info(index)
84
+ cuda_device["free_memory_gb"] = round(free_bytes / (1024**3), 3)
85
+ cuda_device["visible_memory_gb"] = round(total_bytes / (1024**3), 3)
86
+ except RuntimeError as exc:
87
+ cuda_device["memory_info_error"] = str(exc)
88
+ diagnostics["cuda"]["devices"].append(cuda_device)
89
+
90
+ mps_backend = getattr(torch.backends, "mps", None)
91
+ if mps_backend is not None:
92
+ diagnostics["mps"]["built"] = bool(mps_backend.is_built())
93
+ diagnostics["mps"]["available"] = bool(mps_backend.is_available())
94
+
95
+ xpu_backend = getattr(torch, "xpu", None)
96
+ if xpu_backend is not None:
97
+ diagnostics["xpu"]["available"] = bool(xpu_backend.is_available())
98
+ if diagnostics["xpu"]["available"]:
99
+ device_count = int(xpu_backend.device_count())
100
+ diagnostics["xpu"]["device_count"] = device_count
101
+ for index in range(device_count):
102
+ xpu_device: dict[str, Any] = {"index": index}
103
+ try:
104
+ xpu_device["name"] = xpu_backend.get_device_name(index)
105
+ except RuntimeError as exc:
106
+ xpu_device["name_error"] = str(exc)
107
+ diagnostics["xpu"]["devices"].append(xpu_device)
108
+
109
+ return diagnostics
110
+
111
+
112
+ def _synchronize_device(device: torch.device) -> None:
113
+ if device.type == "cuda":
114
+ torch.cuda.synchronize(device)
115
+ return
116
+ if device.type == "xpu":
117
+ xpu_backend = getattr(torch, "xpu", None)
118
+ if xpu_backend is not None:
119
+ xpu_backend.synchronize(device)
120
+
121
+
122
+ def run_device_smoke_test(
123
+ *,
124
+ require_cuda: bool = False,
125
+ matrix_size: int = 1024,
126
+ repeats: int = 3,
127
+ ) -> dict[str, Any]:
128
+ """Run a deterministic matrix multiply on the selected device."""
129
+ if matrix_size < 1:
130
+ raise ValueError("matrix_size must be at least 1")
131
+ if repeats < 1:
132
+ raise ValueError("repeats must be at least 1")
133
+
134
+ diagnostics = collect_device_diagnostics()
135
+ diagnostics["require_cuda"] = require_cuda
136
+ if require_cuda and not diagnostics["cuda"]["available"]:
137
+ raise DeviceSmokeError(
138
+ "CUDA is required for this smoke run but PyTorch could not initialize it",
139
+ diagnostics,
140
+ )
141
+
142
+ device = select_device()
143
+ dtype = torch.float32
144
+ generator = torch.Generator(device="cpu")
145
+ generator.manual_seed(0)
146
+ left = torch.randn(
147
+ matrix_size,
148
+ matrix_size,
149
+ generator=generator,
150
+ dtype=dtype,
151
+ device="cpu",
152
+ ).to(device)
153
+ right = torch.randn(
154
+ matrix_size,
155
+ matrix_size,
156
+ generator=generator,
157
+ dtype=dtype,
158
+ device="cpu",
159
+ ).to(device)
160
+
161
+ _synchronize_device(device)
162
+ start = time.perf_counter()
163
+ result = left @ right
164
+ for _ in range(repeats - 1):
165
+ result = left @ right
166
+ _synchronize_device(device)
167
+ elapsed_seconds = time.perf_counter() - start
168
+
169
+ is_finite = bool(torch.isfinite(result).all().cpu().item())
170
+ if not is_finite:
171
+ raise DeviceSmokeError(
172
+ f"Smoke matmul produced non-finite values on {device}", diagnostics
173
+ )
174
+
175
+ selected_device: dict[str, Any] = {
176
+ "accelerator": device.type,
177
+ "device": str(device),
178
+ "dtype": str(dtype),
179
+ }
180
+ diagnostics["selected_device"] = selected_device
181
+ diagnostics["matmul_smoke"] = {
182
+ "matrix_size": matrix_size,
183
+ "repeats": repeats,
184
+ "elapsed_seconds": round(elapsed_seconds, 6),
185
+ "mean_checksum": float(result.detach().float().mean().cpu().item()),
186
+ "finite": True,
187
+ }
188
+
189
+ if device.type == "cuda":
190
+ selected_device["memory_allocated_gb"] = round(
191
+ torch.cuda.memory_allocated(device) / (1024**3), 3
192
+ )
193
+ selected_device["max_memory_allocated_gb"] = round(
194
+ torch.cuda.max_memory_allocated(device) / (1024**3), 3
195
+ )
196
+
197
+ return diagnostics
@@ -0,0 +1,54 @@
1
+ import datetime as dt
2
+ import logging
3
+ import os
4
+ import shutil
5
+
6
+ import torch
7
+
8
+
9
+ def generate_run_id() -> str:
10
+ """Return a compact UTC timestamp suitable for a training-run identifier."""
11
+ return dt.datetime.now(dt.UTC).strftime("%Y%m%d-%H%M%S")
12
+
13
+
14
+ def runtime_c_compiler() -> str | None:
15
+ """Find the configured or first conventional C compiler on PATH."""
16
+ configured_cc = os.getenv("CC")
17
+ if configured_cc:
18
+ return shutil.which(configured_cc)
19
+
20
+ for compiler in ("cc", "gcc", "clang"):
21
+ resolved = shutil.which(compiler)
22
+ if resolved is not None:
23
+ return resolved
24
+ return None
25
+
26
+
27
+ def require_torch_compile_toolchain(*, logger: logging.Logger | None = None) -> str:
28
+ """Require the runtime compiler used by TorchInductor and return its path."""
29
+ compiler = runtime_c_compiler()
30
+ if compiler is None:
31
+ raise RuntimeError(
32
+ "torch.compile uses TorchInductor/Triton and requires a C compiler "
33
+ "at runtime. Use an image with cc/gcc/clang installed or "
34
+ "set CC to a valid compiler path."
35
+ )
36
+ if logger is not None:
37
+ logger.info("Using C compiler for torch.compile: %s", compiler)
38
+ return compiler
39
+
40
+
41
+ def select_device() -> torch.device:
42
+ """Select the best available PyTorch device in CUDA, XPU, MPS, CPU order."""
43
+ if torch.cuda.is_available():
44
+ return torch.device("cuda")
45
+
46
+ xpu_backend = getattr(torch, "xpu", None)
47
+ if xpu_backend is not None and bool(xpu_backend.is_available()):
48
+ return torch.device("xpu")
49
+
50
+ mps_backend = getattr(torch.backends, "mps", None)
51
+ if mps_backend is not None and bool(mps_backend.is_available()):
52
+ return torch.device("mps")
53
+
54
+ return torch.device("cpu")