LMFuser 0.2.0__tar.gz → 0.4.0__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.
- {lmfuser-0.2.0 → lmfuser-0.4.0}/PKG-INFO +2 -2
- {lmfuser-0.2.0 → lmfuser-0.4.0}/pyproject.toml +2 -2
- {lmfuser-0.2.0 → lmfuser-0.4.0}/src/lmfuser/optimizers.py +5 -2
- {lmfuser-0.2.0 → lmfuser-0.4.0}/src/lmfuser/runners/ddp_runner.py +322 -47
- {lmfuser-0.2.0 → lmfuser-0.4.0}/src/lmfuser/task.py +7 -1
- {lmfuser-0.2.0 → lmfuser-0.4.0}/src/lmfuser/utils.py +0 -5
- lmfuser-0.4.0/tests/test_fsdp2_compile.py +72 -0
- {lmfuser-0.2.0 → lmfuser-0.4.0}/.github/workflows/python-publish.yml +0 -0
- {lmfuser-0.2.0 → lmfuser-0.4.0}/.gitignore +0 -0
- {lmfuser-0.2.0 → lmfuser-0.4.0}/.vscode/settings.json +0 -0
- {lmfuser-0.2.0 → lmfuser-0.4.0}/LICENSE +0 -0
- {lmfuser-0.2.0 → lmfuser-0.4.0}/README.md +0 -0
- {lmfuser-0.2.0 → lmfuser-0.4.0}/src/lmfuser/__init__.py +0 -0
- {lmfuser-0.2.0 → lmfuser-0.4.0}/src/lmfuser/model_loader.py +0 -0
- {lmfuser-0.2.0 → lmfuser-0.4.0}/src/lmfuser/runners/__init__.py +0 -0
- {lmfuser-0.2.0 → lmfuser-0.4.0}/src/lmfuser/runners/runner.py +0 -0
- {lmfuser-0.2.0 → lmfuser-0.4.0}/src/lmfuser/schedulers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: LMFuser
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: The LMFuser training framework.
|
|
5
5
|
Project-URL: Homepage, https://github.com/TYTTYTTYT/LMFuser
|
|
6
6
|
Project-URL: Documentation, https://github.com/TYTTYTTYT/LMFuser
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
17
17
|
Classifier: Topic :: Utilities
|
|
18
18
|
Requires-Python: >=3.11
|
|
19
19
|
Requires-Dist: hyperargs>=0.1.3
|
|
20
|
-
Requires-Dist: lmfuser-data>=0.
|
|
20
|
+
Requires-Dist: lmfuser-data>=0.3.0
|
|
21
21
|
Requires-Dist: numpy
|
|
22
22
|
Requires-Dist: pandas
|
|
23
23
|
Requires-Dist: pyarrow
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "LMFuser"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
requires-python = ">= 3.11"
|
|
9
9
|
description = "The LMFuser training framework."
|
|
10
10
|
readme = "README.md"
|
|
@@ -20,7 +20,7 @@ dependencies = [
|
|
|
20
20
|
"pandas",
|
|
21
21
|
"pyarrow",
|
|
22
22
|
"torch>=2.4.0",
|
|
23
|
-
"LMFuser-data>=0.
|
|
23
|
+
"LMFuser-data>=0.3.0",
|
|
24
24
|
"tqdm",
|
|
25
25
|
"wandb",
|
|
26
26
|
"HyperArgs>=0.1.3",
|
|
@@ -10,7 +10,7 @@ from torch.optim import (
|
|
|
10
10
|
Adadelta,
|
|
11
11
|
Adagrad
|
|
12
12
|
)
|
|
13
|
-
from hyperargs import Conf, OptionArg, FloatArg, BoolArg, add_dependency, monitor_on
|
|
13
|
+
from hyperargs import Conf, OptionArg, FloatArg, BoolArg, add_dependency, monitor_on, IntArg
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class OptimizerConfigBase(Conf):
|
|
@@ -32,6 +32,8 @@ class AdamWConfig(OptimizerConfigBase):
|
|
|
32
32
|
eps = FloatArg(1e-8, min_value=0.0, max_value=1.0)
|
|
33
33
|
weight_decay = FloatArg(0.01, min_value=0.0, max_value=1.0)
|
|
34
34
|
amsgrad = BoolArg(False)
|
|
35
|
+
# fused CUDA kernel (single multi-tensor launch); ignored on CPU
|
|
36
|
+
fused = BoolArg(default=False)
|
|
35
37
|
|
|
36
38
|
def init_optimzier(
|
|
37
39
|
self,
|
|
@@ -43,7 +45,8 @@ class AdamWConfig(OptimizerConfigBase):
|
|
|
43
45
|
betas=(self.beta1.value(), self.beta2.value()), # type: ignore
|
|
44
46
|
eps=self.eps.value(), # type: ignore
|
|
45
47
|
weight_decay=self.weight_decay.value(), # type: ignore
|
|
46
|
-
amsgrad=self.amsgrad.value() # type: ignore
|
|
48
|
+
amsgrad=self.amsgrad.value(), # type: ignore
|
|
49
|
+
fused=self.fused.value() or None, # type: ignore
|
|
47
50
|
)
|
|
48
51
|
|
|
49
52
|
|
|
@@ -59,12 +59,86 @@ logger = logging.getLogger(__name__)
|
|
|
59
59
|
T = TypeVar('T', bound=Conf)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def _compile_kwargs(compile_mode: str) -> dict:
|
|
63
|
+
return {} if compile_mode == 'default' else {'mode': compile_mode}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _TqdmLogHandler(logging.Handler):
|
|
67
|
+
"""Route logging through tqdm.write so log lines print above the progress
|
|
68
|
+
bar instead of tearing through it mid-refresh."""
|
|
69
|
+
|
|
70
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
71
|
+
try:
|
|
72
|
+
tqdm.write(self.format(record))
|
|
73
|
+
except Exception:
|
|
74
|
+
self.handleError(record)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _DevicePrefetcher:
|
|
78
|
+
"""Background fetch + side-stream H2D double buffering over a dataloader.
|
|
79
|
+
|
|
80
|
+
next() returns a device-resident batch whose copy has completed (event
|
|
81
|
+
sync on the compute stream, not the host)."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, loader, device, precision: str) -> None:
|
|
84
|
+
import threading, queue as _q
|
|
85
|
+
self._loader = loader
|
|
86
|
+
self._device = device
|
|
87
|
+
self._precision = precision
|
|
88
|
+
self._stream = torch.cuda.Stream(device=device)
|
|
89
|
+
self._queue: _q.Queue = _q.Queue(maxsize=2)
|
|
90
|
+
self._stop = False
|
|
91
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
92
|
+
self._thread.start()
|
|
93
|
+
|
|
94
|
+
def _cast(self, v: Tensor) -> Tensor:
|
|
95
|
+
if torch.is_floating_point(v):
|
|
96
|
+
dt = {'fp32': torch.float32, 'fp16': torch.float16, 'bf16': torch.bfloat16}[self._precision]
|
|
97
|
+
if v.dtype != dt:
|
|
98
|
+
v = v.to(dt)
|
|
99
|
+
return v
|
|
100
|
+
|
|
101
|
+
def _run(self) -> None:
|
|
102
|
+
it = iter(self._loader)
|
|
103
|
+
while not self._stop:
|
|
104
|
+
try:
|
|
105
|
+
batch = next(it)
|
|
106
|
+
except StopIteration:
|
|
107
|
+
it = iter(self._loader)
|
|
108
|
+
batch = next(it)
|
|
109
|
+
with torch.cuda.stream(self._stream):
|
|
110
|
+
dev = {}
|
|
111
|
+
ok = True
|
|
112
|
+
for k, v in batch.items():
|
|
113
|
+
if isinstance(v, Tensor):
|
|
114
|
+
v = self._cast(v)
|
|
115
|
+
dev[k] = v.pin_memory().to(self._device, non_blocking=True)
|
|
116
|
+
else:
|
|
117
|
+
dev[k] = v
|
|
118
|
+
ev = torch.cuda.Event()
|
|
119
|
+
ev.record(self._stream)
|
|
120
|
+
self._queue.put((dev, ev))
|
|
121
|
+
|
|
122
|
+
def next(self):
|
|
123
|
+
dev, ev = self._queue.get()
|
|
124
|
+
# make the COMPUTE stream wait for the copy — no host sync
|
|
125
|
+
torch.cuda.current_stream().wait_event(ev)
|
|
126
|
+
return dev
|
|
127
|
+
|
|
128
|
+
def close(self) -> None:
|
|
129
|
+
self._stop = True
|
|
130
|
+
|
|
131
|
+
|
|
62
132
|
class Wrapper(nn.Module):
|
|
63
133
|
|
|
64
|
-
def __init__(self, model: nn.Module) -> None:
|
|
134
|
+
def __init__(self, model: nn.Module, compile_mode: str = 'disable') -> None:
|
|
65
135
|
super().__init__()
|
|
66
136
|
self.module = model.to(get_default_device())
|
|
67
|
-
|
|
137
|
+
if compile_mode != 'disable':
|
|
138
|
+
logger.critical(f'torch.compile enabled (mode={compile_mode})')
|
|
139
|
+
self.forward = torch.compile(model.forward, **_compile_kwargs(compile_mode))
|
|
140
|
+
else:
|
|
141
|
+
self.forward = model.forward
|
|
68
142
|
|
|
69
143
|
def no_sync(self):
|
|
70
144
|
# duck-type DDP for the accumulation path: a single process has no
|
|
@@ -83,12 +157,28 @@ class Wrapper(nn.Module):
|
|
|
83
157
|
|
|
84
158
|
class DDPWraper(nn.Module):
|
|
85
159
|
|
|
86
|
-
def __init__(self, model: nn.Module) -> None:
|
|
160
|
+
def __init__(self, model: nn.Module, compile_mode: str = 'disable', find_unused: bool = True, bf16_grads: bool = False) -> None:
|
|
87
161
|
super().__init__()
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
162
|
+
if get_world_size() > 1:
|
|
163
|
+
logger.critical(f'DDP options: find_unused={find_unused} bf16_grads={bf16_grads}')
|
|
164
|
+
self.model = DDP(
|
|
165
|
+
model.to(get_default_device()),
|
|
166
|
+
find_unused_parameters=find_unused,
|
|
167
|
+
)
|
|
168
|
+
if bf16_grads:
|
|
169
|
+
from torch.distributed.algorithms.ddp_comm_hooks import default_hooks
|
|
170
|
+
self.model.register_comm_hook(None, default_hooks.bf16_compress_hook)
|
|
171
|
+
if compile_mode != 'disable':
|
|
172
|
+
# compile the DDP module so Dynamo's DDPOptimizer splits the
|
|
173
|
+
# graph at gradient-bucket boundaries (keeps comm overlap).
|
|
174
|
+
# OptimizedModule forwards attribute access to the DDP module,
|
|
175
|
+
# so .module / .no_sync() paths keep working.
|
|
176
|
+
logger.critical(f'torch.compile enabled (mode={compile_mode})')
|
|
177
|
+
self.model = torch.compile(self.model, **_compile_kwargs(compile_mode))
|
|
178
|
+
self.forward = self.model.forward
|
|
179
|
+
else:
|
|
180
|
+
self.model = Wrapper(model, compile_mode=compile_mode)
|
|
181
|
+
self.forward = self.model.forward
|
|
92
182
|
|
|
93
183
|
def __getattr__(self, name: str) -> Any:
|
|
94
184
|
try:
|
|
@@ -98,11 +188,28 @@ class DDPWraper(nn.Module):
|
|
|
98
188
|
|
|
99
189
|
|
|
100
190
|
class FSDP2Wrapper(nn.Module):
|
|
101
|
-
def __init__(self, model: FSDPModule) -> None:
|
|
191
|
+
def __init__(self, model: FSDPModule, compile_mode: str = 'disable') -> None:
|
|
102
192
|
super().__init__()
|
|
103
193
|
self.model = model
|
|
194
|
+
if compile_mode == 'reduce-overhead':
|
|
195
|
+
logger.critical(
|
|
196
|
+
'compile_mode=reduce-overhead is unsupported with fsdp2 '
|
|
197
|
+
'(cudagraphs need static addresses; FSDP all-gathers allocate '
|
|
198
|
+
'dynamically) — downgrading to "default".')
|
|
199
|
+
compile_mode = 'default'
|
|
200
|
+
if compile_mode != 'disable':
|
|
201
|
+
logger.critical(f'torch.compile enabled (mode={compile_mode})')
|
|
202
|
+
# compile the callable, NOT stored as a submodule: registering the
|
|
203
|
+
# OptimizedModule would duplicate every parameter under a second
|
|
204
|
+
# name and break optimizer construction / state_dict.
|
|
205
|
+
object.__setattr__(self, '_compiled_fwd',
|
|
206
|
+
torch.compile(model, **_compile_kwargs(compile_mode)))
|
|
207
|
+
else:
|
|
208
|
+
object.__setattr__(self, '_compiled_fwd', None)
|
|
104
209
|
|
|
105
210
|
def forward(self, *args: Any, **kwargs: Any) -> Any:
|
|
211
|
+
if self._compiled_fwd is not None:
|
|
212
|
+
return self._compiled_fwd(*args, **kwargs)
|
|
106
213
|
return self.model(*args, **kwargs) # type: ignore
|
|
107
214
|
|
|
108
215
|
def __getattr__(self, name: str) -> Any:
|
|
@@ -137,6 +244,37 @@ class DDPRunnerConfig(RunerConf):
|
|
|
137
244
|
lr_scheduler: LRSchedulerConfig = LRSchedulerConfig()
|
|
138
245
|
|
|
139
246
|
dp_type = OptionArg(default='ddp', options=['ddp', 'fsdp2'])
|
|
247
|
+
# torch.compile for the training model. 'default' = Inductor fusion;
|
|
248
|
+
# 'reduce-overhead' additionally wraps CUDA graphs (ddp only — with fsdp2 it
|
|
249
|
+
# is downgraded to 'default': cudagraphs' static-address requirement is
|
|
250
|
+
# incompatible with FSDP's dynamically allocated all-gather buffers).
|
|
251
|
+
# Explicit-by-config on purpose: anything that changes numerics or the
|
|
252
|
+
# execution path must be declared in the YAML, never auto-detected.
|
|
253
|
+
# NOTE 'disable' not 'off': YAML 1.1 parses bare off/on/yes/no as booleans
|
|
254
|
+
compile_mode = OptionArg(default='disable', options=['disable', 'default', 'reduce-overhead'])
|
|
255
|
+
# gather/log training metrics every N steps. At freq=1 the per-step cost is
|
|
256
|
+
# substantial for fast steps: a metrics all_gather + a dist_avg collective +
|
|
257
|
+
# ~6 wandb/logger calls + .item() syncs + a barrier — about half the step
|
|
258
|
+
# time for a 116M model. Logged values are point samples at the logged step.
|
|
259
|
+
metric_sync_freq = IntArg(1, min_value=1)
|
|
260
|
+
# DDP tuning. find_unused_parameters=1 (default, safest) supports tasks
|
|
261
|
+
# where a subset of parameters gets no grad on some steps (GAN/GRPO mode
|
|
262
|
+
# switching) at the cost of a per-step graph traversal and weaker comm
|
|
263
|
+
# overlap. Plain single-player training should set 0.
|
|
264
|
+
ddp_find_unused = BoolArg(default=True)
|
|
265
|
+
# compress gradient allreduce to bf16 (halves comm volume; fp32 master
|
|
266
|
+
# weights are untouched — only the wire format changes)
|
|
267
|
+
ddp_bf16_grads = BoolArg(default=False)
|
|
268
|
+
# double-buffered device prefetch: a background thread fetches batch N+1
|
|
269
|
+
# and copies it to the GPU on a side stream (pinned staging) while the GPU
|
|
270
|
+
# computes step N. Removes the serialized fetch+H2D segments from the step
|
|
271
|
+
# wall (measured ~40-120ms/step). Tensor-only batches.
|
|
272
|
+
device_prefetch = BoolArg(default=False)
|
|
273
|
+
# diagnostic: log per-step wall time split into fetch / h2d / traincall
|
|
274
|
+
# segments every metric_sync_freq steps. fetch = waiting on the loader,
|
|
275
|
+
# h2d = host-to-device copy (0 with device_prefetch), traincall = the
|
|
276
|
+
# task.train_step call including any GPU-queue wait it absorbs.
|
|
277
|
+
step_timing = BoolArg(default=False)
|
|
140
278
|
model_precision = OptionArg(options=['fp32', 'fp16', 'bf16'], default='fp32')
|
|
141
279
|
use_amp = BoolArg(default=False)
|
|
142
280
|
amp_precision = OptionArg(options=['fp16', 'bf16'], default='fp16')
|
|
@@ -204,6 +342,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
204
342
|
self.config.seed = self.config.seed.parse(hash(f'original_seed_{self.config.seed.value()}|step_{self.step}'))
|
|
205
343
|
|
|
206
344
|
self.train_data_loaders = config.task_conf.get_train_dataloaders(
|
|
345
|
+
resume_states=getattr(self, '_resume_data_states', None),
|
|
207
346
|
batch_size=config.sub_batch_size.value(), # type: ignore
|
|
208
347
|
seed=config.seed.value(), # type: ignore
|
|
209
348
|
shuffle=config.shuffle_dataset.value(), # type: ignore
|
|
@@ -321,6 +460,35 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
321
460
|
else:
|
|
322
461
|
raise ValueError(f'dp_type must be either "ddp" or "fsdp2", got "{self.dp_type}" instead.')
|
|
323
462
|
|
|
463
|
+
def _collect_data_states(self) -> list | None:
|
|
464
|
+
"""Per-task shard-cursor tables, merged across ranks (rank0 only;
|
|
465
|
+
other ranks and loaders without cursor support return/contribute
|
|
466
|
+
None). A COLLECTIVE call when world_size > 1 — every rank must
|
|
467
|
+
reach it."""
|
|
468
|
+
per_task = [
|
|
469
|
+
loader.state_dict()
|
|
470
|
+
if loader is not None and hasattr(loader, 'state_dict') else None
|
|
471
|
+
for loader in self.train_data_loaders
|
|
472
|
+
]
|
|
473
|
+
if not any(s is not None for s in per_task):
|
|
474
|
+
return None
|
|
475
|
+
if get_world_size() > 1:
|
|
476
|
+
gathered: list[Any] = [None] * get_world_size()
|
|
477
|
+
torch.distributed.all_gather_object(gathered, per_task)
|
|
478
|
+
if get_global_rank() != 0:
|
|
479
|
+
return None
|
|
480
|
+
merged: list = []
|
|
481
|
+
for task_idx in range(len(per_task)):
|
|
482
|
+
m: dict = {}
|
|
483
|
+
for rank_states in gathered:
|
|
484
|
+
state = rank_states[task_idx] if rank_states else None
|
|
485
|
+
if state:
|
|
486
|
+
for src, table in state.items():
|
|
487
|
+
m.setdefault(src, {}).update(table)
|
|
488
|
+
merged.append(m or None)
|
|
489
|
+
return merged
|
|
490
|
+
return per_task
|
|
491
|
+
|
|
324
492
|
def save(self, directory: str | os.PathLike, *args, **kwargs) -> None:
|
|
325
493
|
if isinstance(self.model, (DDPWraper, Wrapper)):
|
|
326
494
|
model = self.model.model.module
|
|
@@ -346,11 +514,19 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
346
514
|
else:
|
|
347
515
|
raise ValueError(f'Unknown model type {type(self.model)}')
|
|
348
516
|
|
|
517
|
+
data_states = self._collect_data_states()
|
|
518
|
+
|
|
349
519
|
if get_global_rank() == 0:
|
|
350
520
|
path = Path(directory) / str(self.step)
|
|
351
521
|
os.makedirs(path, exist_ok=True)
|
|
352
522
|
self.model_loader.save_model(model, path) # type: ignore
|
|
353
523
|
|
|
524
|
+
if data_states is not None:
|
|
525
|
+
tmp_path = path / 'data_state.json.tmp'
|
|
526
|
+
with open(tmp_path, 'w') as f:
|
|
527
|
+
json.dump(data_states, f)
|
|
528
|
+
os.replace(tmp_path, path / 'data_state.json')
|
|
529
|
+
|
|
354
530
|
optimizer_path = path / 'optimizer.pt'
|
|
355
531
|
torch.save(optim_states, optimizer_path)
|
|
356
532
|
|
|
@@ -378,12 +554,18 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
378
554
|
model = model.bfloat16() # type: ignore
|
|
379
555
|
elif self.config.model_precision.value() == 'fp32':
|
|
380
556
|
model = model.float() # type: ignore
|
|
557
|
+
compile_mode = self.config.compile_mode.value()
|
|
558
|
+
assert compile_mode is not None
|
|
381
559
|
if isinstance(model, FSDPModule):
|
|
382
560
|
logger.critical(f'wrapping model with FSDPModule')
|
|
383
|
-
self._model = FSDP2Wrapper(model)
|
|
561
|
+
self._model = FSDP2Wrapper(model, compile_mode=compile_mode)
|
|
384
562
|
else:
|
|
385
563
|
logger.critical(f'wrapping model with DDPWraper')
|
|
386
|
-
self._model = DDPWraper(
|
|
564
|
+
self._model = DDPWraper(
|
|
565
|
+
model, compile_mode=compile_mode,
|
|
566
|
+
find_unused=self.config.ddp_find_unused.value(),
|
|
567
|
+
bf16_grads=self.config.ddp_bf16_grads.value(),
|
|
568
|
+
)
|
|
387
569
|
assert self._model is not None
|
|
388
570
|
return self._model
|
|
389
571
|
|
|
@@ -513,11 +695,12 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
513
695
|
epochs = [loader.epoch for loader in self.train_data_loaders if loader is not None]
|
|
514
696
|
return max(epochs) + self.pre_epoch
|
|
515
697
|
|
|
516
|
-
def step_log(self, data: dict[str, Any]) -> None:
|
|
698
|
+
def step_log(self, data: dict[str, Any], console: bool = True) -> None:
|
|
517
699
|
self._wandb
|
|
518
700
|
if get_global_rank() != 0:
|
|
519
701
|
return
|
|
520
|
-
|
|
702
|
+
if console:
|
|
703
|
+
self.logger.critical(f'step:{self.step}\t{data}')
|
|
521
704
|
wandb.log(data, step=self.step)
|
|
522
705
|
|
|
523
706
|
def _one_train_step(self, **kwargs: Any) -> None:
|
|
@@ -531,6 +714,9 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
531
714
|
|
|
532
715
|
# calculate loss
|
|
533
716
|
running_loss: float = 0.0
|
|
717
|
+
running_loss_t = None
|
|
718
|
+
_msf = self.config.metric_sync_freq.value() or 1
|
|
719
|
+
log_this = (self.step % _msf == 0)
|
|
534
720
|
batch_datas: defaultdict[Hashable, List[float]] = defaultdict(list)
|
|
535
721
|
subbatch_result_lst: list[Batch] = []
|
|
536
722
|
for acc_idx in range(self.config._num_acc_steps):
|
|
@@ -558,13 +744,41 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
558
744
|
self.model.set_requires_gradient_sync(True, recurse=True)
|
|
559
745
|
|
|
560
746
|
# compute loss for each sub_batch
|
|
747
|
+
import time as _time
|
|
748
|
+
_tf0 = _time.perf_counter()
|
|
749
|
+
if self.config.device_prefetch.value():
|
|
750
|
+
if not hasattr(self, '_prefetchers'):
|
|
751
|
+
self._prefetchers = {}
|
|
752
|
+
pf = self._prefetchers.get(task_id)
|
|
753
|
+
if pf is None:
|
|
754
|
+
pf = _DevicePrefetcher(
|
|
755
|
+
self.train_data_loaders[task_id],
|
|
756
|
+
get_default_device(),
|
|
757
|
+
self.config.model_precision.value(),
|
|
758
|
+
)
|
|
759
|
+
self._prefetchers[task_id] = pf
|
|
760
|
+
_dev_batch = pf.next()
|
|
761
|
+
_tf1 = _tf2 = _time.perf_counter()
|
|
762
|
+
else:
|
|
763
|
+
_raw_batch = self._next_train_batch(task_id)
|
|
764
|
+
_tf1 = _time.perf_counter()
|
|
765
|
+
_dev_batch = self._batch_to_device(_raw_batch)
|
|
766
|
+
_tf2 = _time.perf_counter()
|
|
767
|
+
if self.config.step_timing.value():
|
|
768
|
+
if not hasattr(self, '_phase_acc'):
|
|
769
|
+
self._phase_acc = [0.0, 0.0, 0.0, 0]
|
|
770
|
+
self._phase_acc[0] += _tf1 - _tf0
|
|
771
|
+
self._phase_acc[1] += _tf2 - _tf1
|
|
772
|
+
_tf2b = _time.perf_counter()
|
|
561
773
|
subbatch_result = task.train_step(
|
|
562
774
|
model=self.model, # type: ignore
|
|
563
|
-
batch=
|
|
564
|
-
step=self.step,
|
|
775
|
+
batch=_dev_batch,
|
|
776
|
+
step=self.step,
|
|
565
777
|
device=get_local_rank(),
|
|
566
778
|
acc_step=acc_idx,
|
|
567
779
|
)
|
|
780
|
+
if self.config.step_timing.value():
|
|
781
|
+
self._phase_acc[2] += _time.perf_counter() - _tf2b
|
|
568
782
|
if isinstance(subbatch_result, torch.Tensor):
|
|
569
783
|
subbatch_result = {'loss': subbatch_result}
|
|
570
784
|
if 'loss' not in subbatch_result:
|
|
@@ -575,7 +789,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
575
789
|
subbatch_result_lst.append(subbatch_result) # type: ignore
|
|
576
790
|
assert isinstance(loss, Tensor)
|
|
577
791
|
loss = loss / self.config._num_acc_steps
|
|
578
|
-
|
|
792
|
+
running_loss_t = loss.detach() if running_loss_t is None else running_loss_t + loss.detach()
|
|
579
793
|
for k, v in subbatch_result.items():
|
|
580
794
|
if k == 'loss':
|
|
581
795
|
continue
|
|
@@ -589,30 +803,66 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
589
803
|
else:
|
|
590
804
|
loss.backward()
|
|
591
805
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
806
|
+
if log_this and getattr(self, '_phase_acc', None):
|
|
807
|
+
n = max(self._phase_acc[3], 1)
|
|
808
|
+
fetch_ms = self._phase_acc[0] / n * 1000
|
|
809
|
+
h2d_ms = self._phase_acc[1] / n * 1000
|
|
810
|
+
traincall_ms = self._phase_acc[2] / n * 1000
|
|
811
|
+
logger.critical(
|
|
812
|
+
f'[timing] fetch={fetch_ms:.1f}ms h2d={h2d_ms:.1f}ms '
|
|
813
|
+
f'traincall={traincall_ms:.1f}ms per-step (over {n} steps)')
|
|
814
|
+
# wandb: rank0's own segments plus the all-rank max of each — the
|
|
815
|
+
# max is the diagnostic one (in DDP the slowest rank paces the
|
|
816
|
+
# whole step, e.g. a data-starved rank shows up as fetch_ms_max)
|
|
817
|
+
t = torch.tensor([fetch_ms, h2d_ms, traincall_ms], device=get_default_device())
|
|
818
|
+
if torch.distributed.is_initialized():
|
|
819
|
+
torch.distributed.all_reduce(t, op=torch.distributed.ReduceOp.MAX)
|
|
820
|
+
mx = t.tolist()
|
|
821
|
+
self.step_log({
|
|
822
|
+
'timing/fetch_ms': fetch_ms,
|
|
823
|
+
'timing/h2d_ms': h2d_ms,
|
|
824
|
+
'timing/traincall_ms': traincall_ms,
|
|
825
|
+
'timing/fetch_ms_max': mx[0],
|
|
826
|
+
'timing/h2d_ms_max': mx[1],
|
|
827
|
+
'timing/traincall_ms_max': mx[2],
|
|
828
|
+
})
|
|
829
|
+
self._phase_acc = [0.0, 0.0, 0.0, 0]
|
|
830
|
+
if getattr(self, '_phase_acc', None) is not None:
|
|
831
|
+
self._phase_acc[3] += 1
|
|
832
|
+
if log_this:
|
|
833
|
+
running_loss = running_loss_t.item() if running_loss_t is not None else 0.0
|
|
834
|
+
running_loss = dist_avg(running_loss)
|
|
835
|
+
self.step_log({f'{task.__class__.__name__}/train/loss': running_loss})
|
|
836
|
+
self.step_log({'train/epoch': self.epoch})
|
|
837
|
+
if log_this:
|
|
838
|
+
for k, v in batch_datas.items():
|
|
839
|
+
try:
|
|
840
|
+
avg = sum(v) / len(v)
|
|
841
|
+
except:
|
|
842
|
+
continue
|
|
843
|
+
self.step_log({f'{task.__class__.__name__}/train/{k}': avg})
|
|
844
|
+
# the sampled step is part of the label: loss refreshes every
|
|
845
|
+
# metric_sync_freq steps while the bar ticks every step, and an
|
|
846
|
+
# unlabeled value reads as a frozen metric
|
|
847
|
+
self._pbar_train.set_postfix_str(
|
|
848
|
+
f'loss={running_loss:.3g}@{self.step}', refresh=True
|
|
849
|
+
)
|
|
604
850
|
|
|
605
851
|
grad_norm_clip_val = self.config.grad_norm_clip.value()
|
|
606
852
|
if grad_norm_clip_val is not None:
|
|
607
853
|
if self.scaler is not None:
|
|
608
854
|
self.scaler.unscale_(self.optimizer)
|
|
609
|
-
|
|
855
|
+
norm_t = clip_grad_norm_(
|
|
610
856
|
parameters=self.model.parameters(), # type: ignore
|
|
611
857
|
max_norm=grad_norm_clip_val
|
|
612
|
-
)
|
|
613
|
-
|
|
614
|
-
|
|
858
|
+
)
|
|
859
|
+
if log_this:
|
|
860
|
+
norm = dist_avg(norm_t.item())
|
|
861
|
+
self.step_log({f'{task.__class__.__name__}/train/grad_norm': norm})
|
|
615
862
|
|
|
863
|
+
# pure host-side work — no .item() sync, no collective — so it runs and
|
|
864
|
+
# reports every step (wandb enqueue is async; console line only on
|
|
865
|
+
# log steps to keep the terminal readable)
|
|
616
866
|
num_hot_params = 0
|
|
617
867
|
num_freeze_params = 0
|
|
618
868
|
for param in self.model.parameters(): # type: ignore
|
|
@@ -624,13 +874,12 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
624
874
|
num_total_params = num_hot_params + num_freeze_params
|
|
625
875
|
if num_total_params == 0:
|
|
626
876
|
raise RuntimeError('The model contains no parameters.')
|
|
627
|
-
|
|
628
877
|
self.step_log({
|
|
629
878
|
f'{task.__class__.__name__}/train/num_hot_params': num_hot_params,
|
|
630
879
|
f'{task.__class__.__name__}/train/num_freeze_params': num_freeze_params,
|
|
631
880
|
f'{task.__class__.__name__}/train/num_total_params': num_total_params,
|
|
632
881
|
f'{task.__class__.__name__}/train/hot_ratio': num_hot_params / num_total_params,
|
|
633
|
-
})
|
|
882
|
+
}, console=log_this)
|
|
634
883
|
|
|
635
884
|
if self.scaler is not None:
|
|
636
885
|
self.scaler.step(self.optimizer)
|
|
@@ -642,21 +891,22 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
642
891
|
current_lr = current_lr[0]
|
|
643
892
|
self.step_log(
|
|
644
893
|
{f'{task.__class__.__name__}/train/learning_rate': current_lr}
|
|
645
|
-
)
|
|
894
|
+
) if log_this else None
|
|
646
895
|
self.scheduler.step()
|
|
647
896
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
for
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
897
|
+
if log_this:
|
|
898
|
+
other_step_results = defaultdict(list)
|
|
899
|
+
for r in subbatch_result_lst:
|
|
900
|
+
for k, v in r.items():
|
|
901
|
+
if isinstance(v, (float, int)):
|
|
902
|
+
other_step_results[k].append(float(v))
|
|
903
|
+
all_other_step_results = batch_all_gather(other_step_results)
|
|
904
|
+
for k, v in all_other_step_results.items():
|
|
905
|
+
try:
|
|
906
|
+
avg = sum(v) / len(v)
|
|
907
|
+
except:
|
|
908
|
+
continue
|
|
909
|
+
self.step_log({f'{task.__class__.__name__}/train/{k}': avg})
|
|
660
910
|
|
|
661
911
|
save_step_freq = self.config.save_step_freq.value()
|
|
662
912
|
assert save_step_freq is not None
|
|
@@ -666,7 +916,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
666
916
|
assert ckpt_path is not None
|
|
667
917
|
self.save(ckpt_path)
|
|
668
918
|
self._pbar_train.set_description('model saved!', True)
|
|
669
|
-
|
|
919
|
+
torch.distributed.barrier() if get_world_size() > 1 else ...
|
|
670
920
|
|
|
671
921
|
eval_step_freq = self.config.eval_step_freq.value()
|
|
672
922
|
assert eval_step_freq is not None
|
|
@@ -680,6 +930,16 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
680
930
|
self.eval() # evaluate first
|
|
681
931
|
self._prepare_train()
|
|
682
932
|
|
|
933
|
+
# route log lines through tqdm.write for the training run so they
|
|
934
|
+
# print cleanly above the bar (plain StreamHandlers tear through it)
|
|
935
|
+
root_logger = logging.getLogger()
|
|
936
|
+
if not any(isinstance(h, _TqdmLogHandler) for h in root_logger.handlers):
|
|
937
|
+
tqdm_handler = _TqdmLogHandler()
|
|
938
|
+
tqdm_handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
|
|
939
|
+
for h in [x for x in root_logger.handlers if type(x) is logging.StreamHandler]:
|
|
940
|
+
root_logger.removeHandler(h)
|
|
941
|
+
root_logger.addHandler(tqdm_handler)
|
|
942
|
+
|
|
683
943
|
self._last_epoch = self.epoch
|
|
684
944
|
stop_metric = self.config.stop_by.value()
|
|
685
945
|
assert stop_metric in ('step', 'epoch')
|
|
@@ -707,6 +967,8 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
707
967
|
while not self._should_stop():
|
|
708
968
|
self._one_train_step()
|
|
709
969
|
if stop_metric == 'step':
|
|
970
|
+
# per-step update is fine: tqdm only renders every mininterval
|
|
971
|
+
# (0.1s); the call itself is just a counter bump
|
|
710
972
|
self._pbar_train.update(1)
|
|
711
973
|
elif stop_metric == 'epoch':
|
|
712
974
|
current_epoch = self.epoch
|
|
@@ -905,6 +1167,19 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
905
1167
|
else:
|
|
906
1168
|
logger.warning(f'scheduler.pt not found in {directory}, skip loading scheduler')
|
|
907
1169
|
|
|
1170
|
+
data_state_path = Path(directory) / 'data_state.json'
|
|
1171
|
+
if data_state_path.exists():
|
|
1172
|
+
with open(data_state_path, 'r') as f:
|
|
1173
|
+
self._resume_data_states = json.load(f)
|
|
1174
|
+
n = sum(
|
|
1175
|
+
len(tab)
|
|
1176
|
+
for task_state in self._resume_data_states if task_state
|
|
1177
|
+
for tab in task_state.values()
|
|
1178
|
+
)
|
|
1179
|
+
logger.info(f'loaded data stream state ({n} shard cursors)')
|
|
1180
|
+
else:
|
|
1181
|
+
logger.info('no data_state.json in checkpoint — data stream restarts from scratch')
|
|
1182
|
+
|
|
908
1183
|
runner_path = Path(directory) / 'runner.json'
|
|
909
1184
|
if runner_path.exists():
|
|
910
1185
|
with open(runner_path, 'r') as f:
|
|
@@ -110,6 +110,7 @@ class TaskBase(Conf, SubclassTracer):
|
|
|
110
110
|
num_batch_workers: int = 4,
|
|
111
111
|
batch_queue_depth: int = 4,
|
|
112
112
|
batch_slot_mb: int = 128,
|
|
113
|
+
resume_state: dict | None = None,
|
|
113
114
|
) -> None | DataLoader | PyTorchDataLoader | BatchDataLoader | EmptyDataLoader:
|
|
114
115
|
if self.num_train_data_path.value() == 0:
|
|
115
116
|
return None
|
|
@@ -143,6 +144,9 @@ class TaskBase(Conf, SubclassTracer):
|
|
|
143
144
|
num_ranks=world_size,
|
|
144
145
|
rank_idx=rank,
|
|
145
146
|
worker_timeout=worker_timeout,
|
|
147
|
+
# only forward when present: lmfuser-data < 0.3.0 has no
|
|
148
|
+
# resume_state parameter
|
|
149
|
+
**({'resume_state': resume_state} if resume_state else {}),
|
|
146
150
|
)
|
|
147
151
|
elif dataloader_type == 'sharded':
|
|
148
152
|
self._train_dataloader = DataLoader(
|
|
@@ -430,6 +434,7 @@ class Tasks(Conf):
|
|
|
430
434
|
num_batch_workers: int = 4,
|
|
431
435
|
batch_queue_depth: int = 4,
|
|
432
436
|
batch_slot_mb: int = 128,
|
|
437
|
+
resume_states: list[dict | None] | None = None,
|
|
433
438
|
) -> list[DataLoader | None | PyTorchDataLoader | BatchDataLoader | EmptyDataLoader]:
|
|
434
439
|
return [
|
|
435
440
|
task.conf._get_train_dataloader(
|
|
@@ -447,8 +452,9 @@ class Tasks(Conf):
|
|
|
447
452
|
num_batch_workers=num_batch_workers,
|
|
448
453
|
batch_queue_depth=batch_queue_depth,
|
|
449
454
|
batch_slot_mb=batch_slot_mb,
|
|
455
|
+
resume_state=(resume_states[i] if resume_states else None),
|
|
450
456
|
)
|
|
451
|
-
for task in self.tasks
|
|
457
|
+
for i, task in enumerate(self.tasks)
|
|
452
458
|
]
|
|
453
459
|
|
|
454
460
|
def get_eval_dataloaders(
|
|
@@ -207,16 +207,11 @@ def batch_all_gather(batch: dict[str, Any]) -> dict[str, Any]:
|
|
|
207
207
|
Returns:
|
|
208
208
|
Dict[Dict[str, Union[Tensor, List[Any]]]]: 汇总后的batch
|
|
209
209
|
"""
|
|
210
|
-
logger.info(f'Begin all gather on rank {get_global_rank()}')
|
|
211
210
|
if not dist.is_initialized():
|
|
212
211
|
return batch
|
|
213
212
|
|
|
214
213
|
gathered = {}
|
|
215
214
|
for k, v_list in batch.items():
|
|
216
|
-
logger.info(f'Begin gather key {k} on rank {get_global_rank()}')
|
|
217
|
-
if isinstance(v_list, list):
|
|
218
|
-
logger.info(f'Key {k} with length {len(v_list)} on rank {get_global_rank()}')
|
|
219
|
-
logger.info(f'after barrier on rank {get_global_rank()}')
|
|
220
215
|
if isinstance(v_list, Tensor):
|
|
221
216
|
gathered[k] = tensor_all_gather(v_list)
|
|
222
217
|
else:
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""FSDP2Wrapper + compile_mode coverage (synthetic, single GPU).
|
|
2
|
+
|
|
3
|
+
Run: torchrun --nproc_per_node=1 tests/test_fsdp2_compile.py
|
|
4
|
+
|
|
5
|
+
Covers, per compile_mode in {disable, default, reduce-overhead}:
|
|
6
|
+
* forward/backward/optimizer step run under fully_shard;
|
|
7
|
+
* parameters are NOT duplicated by compilation (the OptimizedModule must not
|
|
8
|
+
be registered as a submodule);
|
|
9
|
+
* 'reduce-overhead' downgrades to 'default' with a critical log.
|
|
10
|
+
"""
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import torch
|
|
16
|
+
from torch import nn
|
|
17
|
+
import torch.distributed as dist
|
|
18
|
+
from torch.distributed.fsdp import fully_shard
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
21
|
+
from lmfuser.runners.ddp_runner import FSDP2Wrapper # noqa: E402
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_model() -> nn.Module:
|
|
25
|
+
torch.manual_seed(0)
|
|
26
|
+
return nn.Sequential(
|
|
27
|
+
nn.Linear(64, 128), nn.GELU(), nn.Linear(128, 64),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_mode(mode: str) -> None:
|
|
32
|
+
model = build_model().cuda()
|
|
33
|
+
n_params_before = len(list(model.parameters()))
|
|
34
|
+
fully_shard(model)
|
|
35
|
+
|
|
36
|
+
records: list[str] = []
|
|
37
|
+
handler = logging.Handler()
|
|
38
|
+
handler.emit = lambda r: records.append(r.getMessage()) # type: ignore
|
|
39
|
+
logging.getLogger('lmfuser.runners.ddp_runner').addHandler(handler)
|
|
40
|
+
|
|
41
|
+
wrapper = FSDP2Wrapper(model, compile_mode=mode)
|
|
42
|
+
|
|
43
|
+
# ---- parameter registration must not be duplicated by compile ----
|
|
44
|
+
n_params_after = len(list(wrapper.parameters()))
|
|
45
|
+
assert n_params_after == n_params_before, \
|
|
46
|
+
f'[{mode}] parameter duplication: {n_params_before} -> {n_params_after}'
|
|
47
|
+
opt = torch.optim.AdamW(wrapper.parameters(), lr=1e-3) # raises on duplicates
|
|
48
|
+
|
|
49
|
+
# ---- train a few steps ----
|
|
50
|
+
for _ in range(3):
|
|
51
|
+
x = torch.randn(8, 64, device='cuda')
|
|
52
|
+
loss = wrapper(x).square().mean()
|
|
53
|
+
loss.backward()
|
|
54
|
+
opt.step()
|
|
55
|
+
opt.zero_grad()
|
|
56
|
+
assert torch.isfinite(loss).item(), f'[{mode}] non-finite loss'
|
|
57
|
+
|
|
58
|
+
# ---- downgrade warning ----
|
|
59
|
+
if mode == 'reduce-overhead':
|
|
60
|
+
assert any('downgrading' in m for m in records), \
|
|
61
|
+
'[reduce-overhead] downgrade warning not logged'
|
|
62
|
+
|
|
63
|
+
print(f'PASS fsdp2 compile_mode={mode} (loss={loss.item():.4f})')
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == '__main__':
|
|
67
|
+
dist.init_process_group(backend='nccl')
|
|
68
|
+
torch.cuda.set_device(0)
|
|
69
|
+
for mode in ('disable', 'default', 'reduce-overhead'):
|
|
70
|
+
run_mode(mode)
|
|
71
|
+
dist.destroy_process_group()
|
|
72
|
+
print('FSDP2_COMPILE_TESTS_PASS')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|