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.
- syvain_training_utils-0.0.170/PKG-INFO +91 -0
- syvain_training_utils-0.0.170/README.md +77 -0
- syvain_training_utils-0.0.170/pyproject.toml +25 -0
- syvain_training_utils-0.0.170/pyproject.toml.orig +21 -0
- syvain_training_utils-0.0.170/src/syvain_training_utils/__init__.py +35 -0
- syvain_training_utils-0.0.170/src/syvain_training_utils/checkpoint.py +301 -0
- syvain_training_utils-0.0.170/src/syvain_training_utils/diagnostics.py +197 -0
- syvain_training_utils-0.0.170/src/syvain_training_utils/py.typed +0 -0
- syvain_training_utils-0.0.170/src/syvain_training_utils/runtime.py +54 -0
|
@@ -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
|
|
File without changes
|
|
@@ -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")
|