LMFuser 0.4.1__tar.gz → 0.4.2__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.4.1 → lmfuser-0.4.2}/PKG-INFO +2 -2
- {lmfuser-0.4.1 → lmfuser-0.4.2}/pyproject.toml +2 -2
- {lmfuser-0.4.1 → lmfuser-0.4.2}/src/lmfuser/runners/ddp_runner.py +237 -58
- {lmfuser-0.4.1 → lmfuser-0.4.2}/src/lmfuser/task.py +16 -3
- lmfuser-0.4.2/src/lmfuser/utils.py +437 -0
- lmfuser-0.4.2/tests/_gather_worker.py +25 -0
- lmfuser-0.4.2/tests/_gather_worker_conflict.py +17 -0
- lmfuser-0.4.2/tests/_gather_worker_cpu.py +12 -0
- lmfuser-0.4.2/tests/_gather_worker_multi.py +37 -0
- lmfuser-0.4.2/tests/_gather_worker_spec.py +25 -0
- lmfuser-0.4.2/tests/_gather_worker_symmetry.py +155 -0
- lmfuser-0.4.2/tests/test_gather.py +106 -0
- lmfuser-0.4.2/tests/test_runner_robustness.py +245 -0
- lmfuser-0.4.1/src/lmfuser/utils.py +0 -248
- lmfuser-0.4.1/tests/test_runner_robustness.py +0 -114
- {lmfuser-0.4.1 → lmfuser-0.4.2}/.github/workflows/python-publish.yml +0 -0
- {lmfuser-0.4.1 → lmfuser-0.4.2}/.gitignore +0 -0
- {lmfuser-0.4.1 → lmfuser-0.4.2}/.vscode/settings.json +0 -0
- {lmfuser-0.4.1 → lmfuser-0.4.2}/LICENSE +0 -0
- {lmfuser-0.4.1 → lmfuser-0.4.2}/README.md +0 -0
- {lmfuser-0.4.1 → lmfuser-0.4.2}/src/lmfuser/__init__.py +0 -0
- {lmfuser-0.4.1 → lmfuser-0.4.2}/src/lmfuser/model_loader.py +0 -0
- {lmfuser-0.4.1 → lmfuser-0.4.2}/src/lmfuser/optimizers.py +0 -0
- {lmfuser-0.4.1 → lmfuser-0.4.2}/src/lmfuser/runners/__init__.py +0 -0
- {lmfuser-0.4.1 → lmfuser-0.4.2}/src/lmfuser/runners/runner.py +0 -0
- {lmfuser-0.4.1 → lmfuser-0.4.2}/src/lmfuser/schedulers.py +0 -0
- {lmfuser-0.4.1 → lmfuser-0.4.2}/tests/test_fsdp2_compile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: LMFuser
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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.3.
|
|
20
|
+
Requires-Dist: lmfuser-data>=0.3.2
|
|
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.4.
|
|
7
|
+
version = "0.4.2"
|
|
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.3.
|
|
23
|
+
"LMFuser-data>=0.3.2",
|
|
24
24
|
"tqdm",
|
|
25
25
|
"wandb",
|
|
26
26
|
"HyperArgs>=0.1.3",
|
|
@@ -36,6 +36,12 @@ from torch.distributed.fsdp import FSDPModule, MixedPrecisionPolicy
|
|
|
36
36
|
from tqdm import tqdm
|
|
37
37
|
from hyperargs import Conf, StrArg, IntArg, FloatArg, OptionArg, BoolArg
|
|
38
38
|
from lmfuser_data.interfaces import Batch
|
|
39
|
+
from lmfuser_data.utils import slowest_epoch
|
|
40
|
+
try:
|
|
41
|
+
from lmfuser_data import merge_cursors
|
|
42
|
+
except ImportError: # lmfuser-data < 0.3.7
|
|
43
|
+
def merge_cursors(into: dict, cursors: dict) -> None: # type: ignore[misc]
|
|
44
|
+
into.update(cursors)
|
|
39
45
|
import wandb
|
|
40
46
|
from wandb.wandb_run import Run
|
|
41
47
|
|
|
@@ -45,6 +51,7 @@ from ..utils import (
|
|
|
45
51
|
get_local_rank,
|
|
46
52
|
get_world_size,
|
|
47
53
|
get_default_device,
|
|
54
|
+
torch_device,
|
|
48
55
|
dist_init,
|
|
49
56
|
dist_avg,
|
|
50
57
|
batch_all_gather,
|
|
@@ -126,6 +133,8 @@ class _DevicePrefetcher:
|
|
|
126
133
|
self._queue.put(None)
|
|
127
134
|
|
|
128
135
|
def next(self):
|
|
136
|
+
if self._error is not None: # every later call, not just the first
|
|
137
|
+
raise RuntimeError('device prefetch thread died') from self._error
|
|
129
138
|
item = self._queue.get()
|
|
130
139
|
if item is None:
|
|
131
140
|
raise RuntimeError('device prefetch thread died') from self._error
|
|
@@ -144,14 +153,42 @@ class _DevicePrefetcher:
|
|
|
144
153
|
return dev
|
|
145
154
|
|
|
146
155
|
def close(self) -> None:
|
|
156
|
+
# the thread parks in queue.put() on a full queue, so it never reaches
|
|
157
|
+
# the _stop check on its own — drain a slot to let it wake and exit
|
|
147
158
|
self._stop = True
|
|
159
|
+
try:
|
|
160
|
+
while True:
|
|
161
|
+
self._queue.get_nowait()
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
self._thread.join(timeout=5.0)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _as_log_scalar(v: Any) -> float | None:
|
|
168
|
+
"""A metric value reduced to a float for logging, or None if it is not a
|
|
169
|
+
scalar.
|
|
170
|
+
|
|
171
|
+
float() is what actually decides — it accepts python and numpy scalars,
|
|
172
|
+
0-dim and single-element tensors, and rejects lists and multi-element
|
|
173
|
+
tensors — so attempt it rather than enumerate accepted types. The old
|
|
174
|
+
`isinstance(v, (float, int))` test silently dropped numpy.float32, numpy
|
|
175
|
+
ints, and 0-dim/1-element tensors from wandb, while numpy.float64 slipped
|
|
176
|
+
through only because it subclasses float. str/bytes are excluded so a
|
|
177
|
+
numeric-looking string is not quietly parsed into a metric.
|
|
178
|
+
"""
|
|
179
|
+
if isinstance(v, (str, bytes)):
|
|
180
|
+
return None
|
|
181
|
+
try:
|
|
182
|
+
return float(v)
|
|
183
|
+
except (TypeError, ValueError):
|
|
184
|
+
return None
|
|
148
185
|
|
|
149
186
|
|
|
150
187
|
class Wrapper(nn.Module):
|
|
151
188
|
|
|
152
189
|
def __init__(self, model: nn.Module, compile_mode: str = 'disable') -> None:
|
|
153
190
|
super().__init__()
|
|
154
|
-
self.module = model.to(
|
|
191
|
+
self.module = model.to(torch_device())
|
|
155
192
|
if compile_mode != 'disable':
|
|
156
193
|
logger.critical(f'torch.compile enabled (mode={compile_mode})')
|
|
157
194
|
self.forward = torch.compile(model.forward, **_compile_kwargs(compile_mode))
|
|
@@ -180,7 +217,7 @@ class DDPWraper(nn.Module):
|
|
|
180
217
|
if get_world_size() > 1:
|
|
181
218
|
logger.critical(f'DDP options: find_unused={find_unused} bf16_grads={bf16_grads}')
|
|
182
219
|
self.model = DDP(
|
|
183
|
-
model.to(
|
|
220
|
+
model.to(torch_device()),
|
|
184
221
|
find_unused_parameters=find_unused,
|
|
185
222
|
)
|
|
186
223
|
if bf16_grads:
|
|
@@ -236,8 +273,12 @@ class FSDP2Wrapper(nn.Module):
|
|
|
236
273
|
except:
|
|
237
274
|
return self.model.__getattribute__(name)
|
|
238
275
|
|
|
239
|
-
|
|
240
|
-
|
|
276
|
+
# NOTE: no __call__ override. It used to return self.model(...), which
|
|
277
|
+
# bypassed forward() — so the compiled callable built above was never
|
|
278
|
+
# invoked (compile_mode was a silent no-op under fsdp2, while still
|
|
279
|
+
# logging "torch.compile enabled") and any hook registered on the wrapper
|
|
280
|
+
# never fired. nn.Module.__call__ dispatches to forward() and is what the
|
|
281
|
+
# DDP wrapper already relies on.
|
|
241
282
|
|
|
242
283
|
|
|
243
284
|
class DDPRunnerConfig(RunerConf):
|
|
@@ -316,14 +357,18 @@ class DDPRunnerConfig(RunerConf):
|
|
|
316
357
|
|
|
317
358
|
@property
|
|
318
359
|
def _default_precision(self) -> torch.dtype:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
return
|
|
325
|
-
|
|
326
|
-
|
|
360
|
+
# `model_precision` is an OptionArg, and Arg defines no __eq__, so
|
|
361
|
+
# comparing it to a str was False on every branch and this property
|
|
362
|
+
# raised for every valid value. Read the value, as _num_acc_steps does.
|
|
363
|
+
precision = self.model_precision.value()
|
|
364
|
+
try:
|
|
365
|
+
return {
|
|
366
|
+
'fp32': torch.float32,
|
|
367
|
+
'fp16': torch.float16,
|
|
368
|
+
'bf16': torch.bfloat16,
|
|
369
|
+
}[precision]
|
|
370
|
+
except KeyError:
|
|
371
|
+
raise ValueError(f'Unknown model precision "{precision}"')
|
|
327
372
|
|
|
328
373
|
@property
|
|
329
374
|
def _num_acc_steps(self) -> int:
|
|
@@ -357,24 +402,29 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
357
402
|
resume_path = config.resume_path.value()
|
|
358
403
|
assert resume_path is not None, 'resume_path is None'
|
|
359
404
|
self.load(resume_path)
|
|
360
|
-
#
|
|
361
|
-
#
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
#
|
|
365
|
-
#
|
|
366
|
-
#
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
405
|
+
# Data seed, derived deterministically and ONCE per configured seed.
|
|
406
|
+
#
|
|
407
|
+
# Two properties this must have, both learned the hard way:
|
|
408
|
+
# * identical on every rank — `hash()` on a str is PYTHONHASHSEED-
|
|
409
|
+
# randomized, so it gave each rank a different seed (ranks then
|
|
410
|
+
# disagreed on task sampling) and made runs irreproducible;
|
|
411
|
+
# * identical across a resume — shard cursors store a row index into
|
|
412
|
+
# a permutation seeded by this value, so a seed that moves between
|
|
413
|
+
# a run and its own restart silently invalidates every cursor.
|
|
414
|
+
# It is therefore a pure function of config.seed: no step, no rank.
|
|
415
|
+
# Kept separate from config.seed so what lands in runner.json stays the
|
|
416
|
+
# value the user configured.
|
|
417
|
+
self.data_seed = (
|
|
418
|
+
int.from_bytes(
|
|
419
|
+
hashlib.sha256(f'original_seed_{self.config.seed.value()}'.encode()).digest()[:4],
|
|
420
|
+
'big',
|
|
421
|
+
) & 0x7FFFFFFF
|
|
372
422
|
)
|
|
373
423
|
|
|
374
424
|
self.train_data_loaders = config.task_conf.get_train_dataloaders(
|
|
375
425
|
resume_states=getattr(self, '_resume_data_states', None),
|
|
376
426
|
batch_size=config.sub_batch_size.value(), # type: ignore
|
|
377
|
-
seed=
|
|
427
|
+
seed=self.data_seed, # type: ignore
|
|
378
428
|
shuffle=config.shuffle_dataset.value(), # type: ignore
|
|
379
429
|
prefetch_factor=config.row_prefetch.value(), # type: ignore
|
|
380
430
|
num_workers=config.num_row_workers.value(), # type: ignore
|
|
@@ -391,7 +441,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
391
441
|
|
|
392
442
|
self.eval_data_loaders = config.task_conf.get_eval_dataloaders(
|
|
393
443
|
batch_size=config.sub_batch_size.value(), # type: ignore
|
|
394
|
-
seed=
|
|
444
|
+
seed=self.data_seed, # type: ignore
|
|
395
445
|
shuffle=config.shuffle_dataset.value(), # type: ignore
|
|
396
446
|
prefetch_factor=config.row_prefetch.value(), # type: ignore
|
|
397
447
|
num_workers=config.num_row_workers.value(), # type: ignore
|
|
@@ -405,7 +455,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
405
455
|
|
|
406
456
|
self.test_data_loaders = config.task_conf.get_test_dataloaders(
|
|
407
457
|
batch_size=config.sub_batch_size.value(), # type: ignore
|
|
408
|
-
seed=
|
|
458
|
+
seed=self.data_seed, # type: ignore
|
|
409
459
|
shuffle=config.shuffle_dataset.value(), # type: ignore
|
|
410
460
|
prefetch_factor=config.row_prefetch.value(), # type: ignore
|
|
411
461
|
num_workers=config.num_row_workers.value(), # type: ignore
|
|
@@ -451,9 +501,16 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
451
501
|
You can modify this fuction to control the task scheduler.
|
|
452
502
|
Be sure that all ranks are selecting the same task at the same time.
|
|
453
503
|
'''
|
|
454
|
-
|
|
504
|
+
# Return the task's own index, not its position in train_task_idxs:
|
|
505
|
+
# callers use this to address self.tasks / self.train_data_loaders,
|
|
506
|
+
# which are indexed globally. The two coincide only when the trainable
|
|
507
|
+
# tasks happen to come first — with an eval-only task before a
|
|
508
|
+
# trainable one this trained the wrong task and then crashed on its
|
|
509
|
+
# missing dataloader.
|
|
510
|
+
position = self.task_rand_g.choices(
|
|
455
511
|
list(range(len(self.train_task_idxs))), weights=self.train_task_weights, k=1
|
|
456
512
|
)[0]
|
|
513
|
+
return self.train_task_idxs[position]
|
|
457
514
|
|
|
458
515
|
def sample_eval_task_id(self) -> int:
|
|
459
516
|
'''
|
|
@@ -461,9 +518,10 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
461
518
|
You can modify this fuction to control the task scheduler.
|
|
462
519
|
Be sure that all ranks are selecting the same task at the same time.
|
|
463
520
|
'''
|
|
464
|
-
|
|
521
|
+
position = self.task_rand_g.choices(
|
|
465
522
|
list(range(len(self.eval_task_idxs))), weights=self.eval_task_weights, k=1
|
|
466
523
|
)[0]
|
|
524
|
+
return self.eval_task_idxs[position]
|
|
467
525
|
|
|
468
526
|
@property
|
|
469
527
|
def fsdp_kwargs(self) -> dict:
|
|
@@ -520,7 +578,12 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
520
578
|
state = rank_states[task_idx] if rank_states else None
|
|
521
579
|
if state:
|
|
522
580
|
for src, table in state.items():
|
|
523
|
-
|
|
581
|
+
# the loader's own rule, not dict.update: ranks own
|
|
582
|
+
# disjoint shards except on the round-robin
|
|
583
|
+
# fallback, where several read the same one and
|
|
584
|
+
# last-writer-wins silently rewinds whichever of
|
|
585
|
+
# them was enumerated first
|
|
586
|
+
merge_cursors(m.setdefault(src, {}), table)
|
|
524
587
|
merged.append(m or None)
|
|
525
588
|
return merged
|
|
526
589
|
return per_task
|
|
@@ -539,8 +602,13 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
539
602
|
model = self.model_loader.load_model()
|
|
540
603
|
model.load_state_dict(full_states)
|
|
541
604
|
|
|
605
|
+
# the INNER model, matching set_optimizer_state_dict on the resume
|
|
606
|
+
# path: passing the wrapper prefixes every parameter FQN with
|
|
607
|
+
# "model.", and the two sides then disagree — every fsdp2 resume
|
|
608
|
+
# died with "Missing optimizer state for parameter ...", and
|
|
609
|
+
# strict=False silently restored nothing at all
|
|
542
610
|
optim_states = get_optimizer_state_dict(
|
|
543
|
-
model=self.model, # type: ignore
|
|
611
|
+
model=self.model.model, # type: ignore
|
|
544
612
|
optimizers=self.optimizer,
|
|
545
613
|
options=StateDictOptions(
|
|
546
614
|
full_state_dict=True,
|
|
@@ -639,7 +707,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
639
707
|
else:
|
|
640
708
|
raise ValueError(f'Unknown model precision "{precision}"')
|
|
641
709
|
assert isinstance(batch, dict)
|
|
642
|
-
batch[key] = v.to(
|
|
710
|
+
batch[key] = v.to(torch_device()) if v.get_device() != get_default_device() else v
|
|
643
711
|
return batch
|
|
644
712
|
|
|
645
713
|
def _prepare_train(
|
|
@@ -661,7 +729,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
661
729
|
for state in self.optimizer.state.values():
|
|
662
730
|
for k, v in state.items():
|
|
663
731
|
if torch.is_tensor(v):
|
|
664
|
-
state[k] = v.to(
|
|
732
|
+
state[k] = v.to(torch_device(), non_blocking=True)
|
|
665
733
|
elif isinstance(self.model, FSDP2Wrapper):
|
|
666
734
|
set_optimizer_state_dict(
|
|
667
735
|
model=self.model.model, # type: ignore
|
|
@@ -733,8 +801,47 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
733
801
|
|
|
734
802
|
@property
|
|
735
803
|
def epoch(self) -> int:
|
|
736
|
-
epochs =
|
|
737
|
-
|
|
804
|
+
"""Completed epochs = the SLOWEST task's. See `slowest_epoch`.
|
|
805
|
+
|
|
806
|
+
`max` let the fastest task end the epoch for the whole run, so with
|
|
807
|
+
tasks of different sizes `stop_by: epoch` stopped while the largest
|
|
808
|
+
task had been round only partway. A task with weight 0.0 is never
|
|
809
|
+
sampled and is excluded so it cannot stall the run instead.
|
|
810
|
+
"""
|
|
811
|
+
# Index by TASK id, not by position: train_data_loaders is per task
|
|
812
|
+
# and holds None for eval-only tasks, while train_task_weights is
|
|
813
|
+
# packed to the train tasks only. Zipping them pairs a loader with
|
|
814
|
+
# another task's weight as soon as any task is eval-only.
|
|
815
|
+
pairs = [
|
|
816
|
+
(self.train_data_loaders[idx].epoch, self.task_weights[idx]) # type: ignore
|
|
817
|
+
for idx in self.train_task_idxs
|
|
818
|
+
]
|
|
819
|
+
if not pairs:
|
|
820
|
+
return self.pre_epoch
|
|
821
|
+
epochs, weights = zip(*pairs)
|
|
822
|
+
return slowest_epoch(list(epochs), list(weights)) + self.pre_epoch
|
|
823
|
+
|
|
824
|
+
def _pbar_status(self, **fields: Any) -> None:
|
|
825
|
+
"""Update named markers shown after the progress bar.
|
|
826
|
+
|
|
827
|
+
Everything here is a point sample of something that does not change
|
|
828
|
+
every step, so each marker carries the step it refers to. Pass None to
|
|
829
|
+
drop a marker. (set_description would put the text in front of the bar
|
|
830
|
+
and leave it there — "model saved!" fronted the bar for the 25k steps
|
|
831
|
+
until the next save.)
|
|
832
|
+
"""
|
|
833
|
+
if not hasattr(self, '_pbar_fields'):
|
|
834
|
+
self._pbar_fields: dict[str, str] = {}
|
|
835
|
+
for key, value in fields.items():
|
|
836
|
+
if value is None:
|
|
837
|
+
self._pbar_fields.pop(key, None)
|
|
838
|
+
else:
|
|
839
|
+
self._pbar_fields[key] = value
|
|
840
|
+
if getattr(self, '_pbar_train', None) is not None:
|
|
841
|
+
self._pbar_train.set_postfix_str(
|
|
842
|
+
' '.join(f'{k}={v}' for k, v in self._pbar_fields.items()),
|
|
843
|
+
refresh=True,
|
|
844
|
+
)
|
|
738
845
|
|
|
739
846
|
def step_log(self, data: dict[str, Any], console: bool = True) -> None:
|
|
740
847
|
self._wandb
|
|
@@ -834,10 +941,17 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
834
941
|
for k, v in subbatch_result.items():
|
|
835
942
|
if k == 'loss':
|
|
836
943
|
continue
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
944
|
+
scalar = _as_log_scalar(v)
|
|
945
|
+
if scalar is not None:
|
|
946
|
+
batch_datas[k].append(scalar)
|
|
947
|
+
elif isinstance(v, (list, tuple)) and len(v) > 0:
|
|
948
|
+
floats = [s for s in (_as_log_scalar(i) for i in v)
|
|
949
|
+
if s is not None]
|
|
950
|
+
# all-or-nothing: a partially-convertible list is a type
|
|
951
|
+
# confusion, not a metric — logging half of it silently
|
|
952
|
+
# would be worse than dropping it.
|
|
953
|
+
if len(floats) == len(v):
|
|
954
|
+
batch_datas[k].extend(floats)
|
|
841
955
|
|
|
842
956
|
if self.scaler is not None:
|
|
843
957
|
self.scaler.scale(loss).backward()
|
|
@@ -855,7 +969,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
855
969
|
# wandb: rank0's own segments plus the all-rank max of each — the
|
|
856
970
|
# max is the diagnostic one (in DDP the slowest rank paces the
|
|
857
971
|
# whole step, e.g. a data-starved rank shows up as fetch_ms_max)
|
|
858
|
-
t = torch.tensor([fetch_ms, h2d_ms, traincall_ms], device=
|
|
972
|
+
t = torch.tensor([fetch_ms, h2d_ms, traincall_ms], device=torch_device())
|
|
859
973
|
if torch.distributed.is_initialized():
|
|
860
974
|
torch.distributed.all_reduce(t, op=torch.distributed.ReduceOp.MAX)
|
|
861
975
|
mx = t.tolist()
|
|
@@ -885,9 +999,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
885
999
|
# the sampled step is part of the label: loss refreshes every
|
|
886
1000
|
# metric_sync_freq steps while the bar ticks every step, and an
|
|
887
1001
|
# unlabeled value reads as a frozen metric
|
|
888
|
-
self.
|
|
889
|
-
f'loss={running_loss:.3g}@{self.step}', refresh=True
|
|
890
|
-
)
|
|
1002
|
+
self._pbar_status(loss=f'{running_loss:.3g}@{self.step}')
|
|
891
1003
|
|
|
892
1004
|
grad_norm_clip_val = self.config.grad_norm_clip.value()
|
|
893
1005
|
if grad_norm_clip_val is not None:
|
|
@@ -939,8 +1051,9 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
939
1051
|
other_step_results = defaultdict(list)
|
|
940
1052
|
for r in subbatch_result_lst:
|
|
941
1053
|
for k, v in r.items():
|
|
942
|
-
|
|
943
|
-
|
|
1054
|
+
scalar = _as_log_scalar(v)
|
|
1055
|
+
if scalar is not None:
|
|
1056
|
+
other_step_results[k].append(scalar)
|
|
944
1057
|
all_other_step_results = batch_all_gather(other_step_results)
|
|
945
1058
|
for k, v in all_other_step_results.items():
|
|
946
1059
|
try:
|
|
@@ -952,11 +1065,14 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
952
1065
|
save_step_freq = self.config.save_step_freq.value()
|
|
953
1066
|
assert save_step_freq is not None
|
|
954
1067
|
if self.step % save_step_freq == 0:
|
|
955
|
-
self.
|
|
1068
|
+
self._pbar_status(saving=str(self.step))
|
|
956
1069
|
ckpt_path = self.config.checkpoint_directory.value()
|
|
957
1070
|
assert ckpt_path is not None
|
|
958
1071
|
self.save(ckpt_path)
|
|
959
|
-
|
|
1072
|
+
# replaces the "saving" marker rather than adding to it, and stays
|
|
1073
|
+
# labelled with the step: set_description used to leave "model
|
|
1074
|
+
# saved!" fronting the bar for the next 25k steps
|
|
1075
|
+
self._pbar_status(saving=None, saved=str(self.step))
|
|
960
1076
|
torch.distributed.barrier() if get_world_size() > 1 else ...
|
|
961
1077
|
|
|
962
1078
|
eval_step_freq = self.config.eval_step_freq.value()
|
|
@@ -1035,7 +1151,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
1035
1151
|
if get_world_size() > 1:
|
|
1036
1152
|
if isinstance(self.model, (DDPWraper, Wrapper)):
|
|
1037
1153
|
stack.enter_context(self.model.model.no_sync())
|
|
1038
|
-
elif isinstance(self.model,
|
|
1154
|
+
elif isinstance(self.model, FSDP2Wrapper):
|
|
1039
1155
|
self.model.set_requires_gradient_sync(False, recurse=True)
|
|
1040
1156
|
for batch in tqdm(
|
|
1041
1157
|
dataloader,
|
|
@@ -1055,8 +1171,17 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
1055
1171
|
batch_list.append(result)
|
|
1056
1172
|
|
|
1057
1173
|
if len(batch_list) == 0:
|
|
1058
|
-
|
|
1059
|
-
|
|
1174
|
+
# A rank with an empty slice must NOT raise here: every other
|
|
1175
|
+
# rank is heading into batch_all_gather below, and a rank that
|
|
1176
|
+
# leaves instead of entering it hangs them all — under NCCL
|
|
1177
|
+
# until the watchdog fires, or forever. Contribute nothing and
|
|
1178
|
+
# let the collective complete; an empty result surfaces as a
|
|
1179
|
+
# metric error afterwards, on every rank at once.
|
|
1180
|
+
logger.warning(
|
|
1181
|
+
f'no eval data in rank {get_global_rank()} for task '
|
|
1182
|
+
f'{task.__class__.__name__}; contributing nothing to the gather'
|
|
1183
|
+
)
|
|
1184
|
+
batch_list = [{}]
|
|
1060
1185
|
cat_result = batch_list[0]
|
|
1061
1186
|
for batch in batch_list[1:]:
|
|
1062
1187
|
for k, v in batch.items():
|
|
@@ -1070,8 +1195,14 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
1070
1195
|
|
|
1071
1196
|
if task.__class__.__name__ not in self._all_eval_results:
|
|
1072
1197
|
self._all_eval_results[task.__class__.__name__] = []
|
|
1073
|
-
|
|
1074
|
-
|
|
1198
|
+
# `saved` lets checkpoint selection tell an eval that can be reloaded
|
|
1199
|
+
# from one that cannot: eval_step_freq and save_step_freq are
|
|
1200
|
+
# independent, so most evals happen at steps with nothing on disk.
|
|
1201
|
+
ckpt_dir = self.config.checkpoint_directory.value()
|
|
1202
|
+
saved = bool(ckpt_dir) and (Path(str(ckpt_dir)) / str(self.step)).is_dir()
|
|
1203
|
+
self._all_eval_results[task.__class__.__name__].append(
|
|
1204
|
+
{'step': self.step, 'metrics': metrics, 'saved': saved})
|
|
1205
|
+
if isinstance(self.model, FSDP2Wrapper):
|
|
1075
1206
|
self.model.set_requires_gradient_sync(True, recurse=True)
|
|
1076
1207
|
|
|
1077
1208
|
def _test_one_task(self, task_id: int, **kwargs: Any) -> None:
|
|
@@ -1087,7 +1218,35 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
1087
1218
|
if test_model_path is not None:
|
|
1088
1219
|
if isinstance(test_model_path, int):
|
|
1089
1220
|
test_model_path = Path(ckpt_path) / str(test_model_path)
|
|
1090
|
-
|
|
1221
|
+
|
|
1222
|
+
# A task picks the best step by DEV METRIC, but eval runs on its own
|
|
1223
|
+
# schedule and most eval steps have no checkpoint on disk. Loading
|
|
1224
|
+
# a path that isn't there kills the run after all the training is
|
|
1225
|
+
# already paid for, so fall back to the best step that was saved.
|
|
1226
|
+
if not Path(test_model_path).is_dir():
|
|
1227
|
+
results = self._all_eval_results.get(task.__class__.__name__, [])
|
|
1228
|
+
saved_only = [r for r in results if r.get('saved')]
|
|
1229
|
+
fallback = task.set_test_model_path(ckpt_path, {
|
|
1230
|
+
task.__class__.__name__: saved_only}) if saved_only else None
|
|
1231
|
+
if isinstance(fallback, int):
|
|
1232
|
+
fallback = Path(ckpt_path) / str(fallback)
|
|
1233
|
+
if fallback is not None and Path(fallback).is_dir():
|
|
1234
|
+
logger.warning(
|
|
1235
|
+
f'best dev step has no checkpoint on disk '
|
|
1236
|
+
f'({test_model_path}) — eval_step_freq and '
|
|
1237
|
+
f'save_step_freq disagree. Testing the best SAVED step '
|
|
1238
|
+
f'instead: {fallback}'
|
|
1239
|
+
)
|
|
1240
|
+
test_model_path = fallback
|
|
1241
|
+
else:
|
|
1242
|
+
logger.warning(
|
|
1243
|
+
f'best dev step has no checkpoint on disk '
|
|
1244
|
+
f'({test_model_path}) and no evaluated step was saved; '
|
|
1245
|
+
f'testing the in-memory model instead'
|
|
1246
|
+
)
|
|
1247
|
+
test_model_path = None
|
|
1248
|
+
|
|
1249
|
+
if test_model_path is not None:
|
|
1091
1250
|
self.model_loader.model_path = test_model_path
|
|
1092
1251
|
self._model = None
|
|
1093
1252
|
|
|
@@ -1098,7 +1257,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
1098
1257
|
if get_world_size() > 1:
|
|
1099
1258
|
if isinstance(self.model, (DDPWraper, Wrapper)):
|
|
1100
1259
|
stack.enter_context(self.model.model.no_sync())
|
|
1101
|
-
elif isinstance(self.model,
|
|
1260
|
+
elif isinstance(self.model, FSDP2Wrapper):
|
|
1102
1261
|
self.model.set_requires_gradient_sync(False, recurse=True)
|
|
1103
1262
|
for batch in tqdm(
|
|
1104
1263
|
dataloader,
|
|
@@ -1118,8 +1277,17 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
1118
1277
|
batch_list.append(result)
|
|
1119
1278
|
|
|
1120
1279
|
if len(batch_list) == 0:
|
|
1121
|
-
|
|
1122
|
-
|
|
1280
|
+
# A rank with an empty slice must NOT raise here: every other
|
|
1281
|
+
# rank is heading into batch_all_gather below, and a rank that
|
|
1282
|
+
# leaves instead of entering it hangs them all — under NCCL
|
|
1283
|
+
# until the watchdog fires, or forever. Contribute nothing and
|
|
1284
|
+
# let the collective complete; an empty result surfaces as a
|
|
1285
|
+
# metric error afterwards, on every rank at once.
|
|
1286
|
+
logger.warning(
|
|
1287
|
+
f'no test data in rank {get_global_rank()} for task '
|
|
1288
|
+
f'{task.__class__.__name__}; contributing nothing to the gather'
|
|
1289
|
+
)
|
|
1290
|
+
batch_list = [{}]
|
|
1123
1291
|
cat_result = batch_list[0]
|
|
1124
1292
|
for batch in batch_list[1:]:
|
|
1125
1293
|
for k, v in batch.items():
|
|
@@ -1132,7 +1300,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
1132
1300
|
self.step_log({f'{task.__class__.__name__}/test/{k}': v})
|
|
1133
1301
|
|
|
1134
1302
|
self._test_results[task.__class__.__name__] = {'step': self.step, 'metrics': metrics}
|
|
1135
|
-
if isinstance(self.model,
|
|
1303
|
+
if isinstance(self.model, FSDP2Wrapper):
|
|
1136
1304
|
self.model.set_requires_gradient_sync(True, recurse=True)
|
|
1137
1305
|
|
|
1138
1306
|
def eval(self, *args: Any, **kwargs: Any) -> None:
|
|
@@ -1226,6 +1394,17 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
1226
1394
|
with open(runner_path, 'r') as f:
|
|
1227
1395
|
runner_state = json.load(f)
|
|
1228
1396
|
self.step = int(runner_state['step'])
|
|
1229
|
-
|
|
1397
|
+
# runner.json's `epoch` is the FULL count — it was written as
|
|
1398
|
+
# self.epoch, which already includes the loaders' progress.
|
|
1399
|
+
# When the data stream is restored too, the loaders replay that
|
|
1400
|
+
# progress and report it again, so adding it here counted every
|
|
1401
|
+
# completed epoch twice, and once more for every further
|
|
1402
|
+
# resume (2 + 2 = 4 after one). pre_epoch exists for the case
|
|
1403
|
+
# where the stream restarts from scratch and that history would
|
|
1404
|
+
# otherwise be lost; carry it only then.
|
|
1405
|
+
self.pre_epoch = (
|
|
1406
|
+
0 if getattr(self, '_resume_data_states', None)
|
|
1407
|
+
else int(runner_state['epoch'])
|
|
1408
|
+
)
|
|
1230
1409
|
else:
|
|
1231
1410
|
logger.warning(f'runner.json not found in {directory}, skip loading runner state')
|
|
@@ -89,7 +89,10 @@ class TaskBase(Conf, SubclassTracer):
|
|
|
89
89
|
assert isinstance(num, int)
|
|
90
90
|
if len(self.test_data_path_list) > num:
|
|
91
91
|
self.test_data_path_list = self.test_data_path_list[:num]
|
|
92
|
-
|
|
92
|
+
# was a duplicate of the line above, so shrinking the test list
|
|
93
|
+
# left the weights at their old length — paths and weights then
|
|
94
|
+
# disagreed. The train and eval monitors alongside get this right.
|
|
95
|
+
self.test_data_weights = self.test_data_weights[:num]
|
|
93
96
|
elif len(self.test_data_path_list) < num:
|
|
94
97
|
self.test_data_path_list += [StrArg('Enther the path to the data file.')] * (num - len(self.test_data_path_list))
|
|
95
98
|
self.test_data_weights += [FloatArg(1.0, min_value=0.0, max_value=1.0)] * (num - len(self.test_data_weights))
|
|
@@ -250,7 +253,12 @@ class TaskBase(Conf, SubclassTracer):
|
|
|
250
253
|
num_ranks=world_size,
|
|
251
254
|
rank_idx=rank,
|
|
252
255
|
collate_fn=self.get_collate_fn(),
|
|
253
|
-
drop_last=False
|
|
256
|
+
drop_last=False,
|
|
257
|
+
# score every row exactly once: a padded sampler
|
|
258
|
+
# repeats rows to equalise rank counts, and those
|
|
259
|
+
# duplicates are gathered into the metric, so the
|
|
260
|
+
# same checkpoint scores differently on 2 vs 4 GPUs
|
|
261
|
+
exact_pass=True
|
|
254
262
|
)
|
|
255
263
|
elif dataloader_type == 'empty':
|
|
256
264
|
self._eval_dataloader = EmptyDataLoader(init_step=0)
|
|
@@ -320,7 +328,12 @@ class TaskBase(Conf, SubclassTracer):
|
|
|
320
328
|
num_ranks=world_size,
|
|
321
329
|
rank_idx=rank,
|
|
322
330
|
collate_fn=self.get_collate_fn(),
|
|
323
|
-
drop_last=False
|
|
331
|
+
drop_last=False,
|
|
332
|
+
# score every row exactly once: a padded sampler
|
|
333
|
+
# repeats rows to equalise rank counts, and those
|
|
334
|
+
# duplicates are gathered into the metric, so the
|
|
335
|
+
# same checkpoint scores differently on 2 vs 4 GPUs
|
|
336
|
+
exact_pass=True
|
|
324
337
|
)
|
|
325
338
|
elif dataloader_type == 'empty':
|
|
326
339
|
self._test_dataloader = EmptyDataLoader(init_step=0)
|