forked from MakazhanAlpamys/Soup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathactivation_offload.py
More file actions
221 lines (183 loc) · 7 KB
/
Copy pathactivation_offload.py
File metadata and controls
221 lines (183 loc) · 7 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
"""Activation offloading — move stored activations to CPU or disk.
During the backward pass, cached activations are the main VRAM cost. Offloading
them to RAM (CPU) or a scratch file (disk) trades IO/PCIe bandwidth for VRAM
headroom — useful for single-GPU large-batch training on small VRAM.
Implemented as a context manager that installs + removes the offload hooks
for the duration of ``trainer.train()``. The hooks wrap ``torch.utils.hooks``
/ saved-tensor hooks and move saved tensors to the target device.
Unlike DeepSpeed ZeRO offload (which is partitioning-aware), this is a
*per-tensor* offload that composes with any backend (DDP, FSDP, or single-GPU).
"""
from __future__ import annotations
import contextlib
from types import ModuleType
from typing import Any, Callable, Generator, Optional, Tuple
HookPair = Tuple[Callable[[Any], Any], Callable[[Any], Any]]
def validate_offload_config(
target: Optional[str],
backend: str,
device: str,
save_dir: Optional[str] = None,
) -> list[str]:
"""Validate ``activation_offloading`` config.
Args:
target: None / 'cpu' / 'disk'.
backend: transformers / unsloth / mlx.
device: cuda / cpu / mps.
save_dir: Optional. Required when target='disk' — caller must supply
a containment-checked scratch directory.
Returns:
List of error messages. Empty if valid or disabled.
"""
errors: list[str] = []
if target is None:
return errors
if backend == "unsloth":
errors.append(
"Activation offloading is not compatible with the unsloth backend. "
"Unsloth manages its own memory. Use backend: transformers."
)
return errors
if backend == "mlx":
errors.append(
"Activation offloading is not supported on the mlx backend. "
"Use backend: transformers."
)
return errors
if device != "cuda":
errors.append(
"Activation offloading requires CUDA training. "
f"Current device: {device}."
)
if target == "disk" and not save_dir:
errors.append(
"activation_offloading='disk' requires a save_dir "
"(scratch directory for offloaded activation tensors)."
)
return errors
@contextlib.contextmanager
def offload_context(
target: Optional[str], save_dir: Optional[str] = None,
) -> Generator[None, None, None]:
"""Install saved-tensor hooks for activation offloading; remove on exit.
Args:
target: 'cpu' → offload to RAM; 'disk' → offload to scratch files;
None → no-op.
save_dir: Directory for disk-mode scratch files. Required for 'disk'.
Yields:
Nothing — use as a ``with`` block around ``trainer.train()``.
Note:
When ``torch`` is not installed the hooks are silently skipped; this
keeps the context manager safe to enter from CI / CLI --help paths.
"""
if target is None:
yield
return
try:
import torch
except ImportError:
yield
return
created_files: list[str] = []
if target == "cpu":
pack_hook, unpack_hook = _make_cpu_hooks(torch)
elif target == "disk":
if not save_dir:
raise ValueError(
"activation_offloading='disk' requires save_dir"
)
pack_hook, unpack_hook = _make_disk_hooks(torch, save_dir, created_files)
else:
raise ValueError(
f"Unknown activation_offloading target: {target!r}. "
"Expected None, 'cpu', or 'disk'."
)
# saved_tensors_hooks is the public API (torch>=1.11). Older torch: no-op.
if not hasattr(torch.autograd.graph, "saved_tensors_hooks"):
yield
return
try:
with torch.autograd.graph.saved_tensors_hooks(pack_hook, unpack_hook):
yield
finally:
# Best-effort cleanup for disk mode: remove any leftover scratch files
# (e.g. from a crashed backward pass). Per-file OSErrors are swallowed
# so cleanup is never itself a crash source.
if target == "disk":
import os
for path in created_files:
try:
os.unlink(path)
except OSError:
pass
def _make_cpu_hooks(torch_module: ModuleType) -> HookPair:
"""Saved-tensor hooks: pack → move to CPU, unpack → move back."""
def pack(tensor: Any) -> Any:
if tensor.device.type == "cuda":
return ("cuda", tensor.device, tensor.detach().cpu())
return ("keep", None, tensor)
def unpack(payload: Any) -> Any:
kind, original_device, stored = payload
if kind == "cuda":
return stored.to(original_device, non_blocking=True)
return stored
return pack, unpack
def _make_disk_hooks(
torch_module: ModuleType,
save_dir: str,
created_files: list[str],
) -> HookPair:
"""Saved-tensor hooks: pack → save to disk, unpack → reload.
``created_files`` accumulates paths for best-effort cleanup at context exit.
The scratch fd is held open until ``torch.save`` returns to close the
TOCTOU window between ``mkstemp`` and ``save``.
"""
import os
import tempfile
os.makedirs(save_dir, exist_ok=True)
def pack(tensor: Any) -> Any:
if tensor.device.type != "cuda":
return ("keep", None, None, tensor)
fd, path = tempfile.mkstemp(dir=save_dir, suffix=".pt")
created_files.append(path)
# Keep fd open until torch.save flushes to close the TOCTOU gap
# between mkstemp and a subsequent open-by-path.
try:
with os.fdopen(fd, "wb") as file_obj:
torch_module.save(tensor.detach().cpu(), file_obj)
except Exception:
# Hook must be best-effort; fall back to keeping tensor in VRAM.
try:
os.unlink(path)
except OSError:
pass
try:
created_files.remove(path)
except ValueError:
pass
return ("keep", None, None, tensor)
return ("disk", tensor.device, path, None)
def unpack(payload: Any) -> Any:
kind, original_device, path, stored = payload
if kind == "disk":
try:
loaded = torch_module.load(
path, map_location="cpu", weights_only=True,
)
except FileNotFoundError:
# File already gone (e.g. GC'd by crash + cleanup); return a
# sentinel None so autograd surfaces a clear error. This
# should not happen in a healthy run.
raise
finally:
try:
os.unlink(path)
except OSError:
pass
try:
created_files.remove(path)
except ValueError:
pass
return loaded.to(original_device, non_blocking=True)
return stored
return pack, unpack