LMFuser 0.3.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.3.0 → lmfuser-0.4.0}/PKG-INFO +2 -2
- {lmfuser-0.3.0 → lmfuser-0.4.0}/pyproject.toml +2 -2
- {lmfuser-0.3.0 → lmfuser-0.4.0}/src/lmfuser/optimizers.py +5 -2
- {lmfuser-0.3.0 → lmfuser-0.4.0}/src/lmfuser/runners/ddp_runner.py +269 -40
- {lmfuser-0.3.0 → lmfuser-0.4.0}/src/lmfuser/task.py +7 -1
- {lmfuser-0.3.0 → lmfuser-0.4.0}/src/lmfuser/utils.py +0 -5
- {lmfuser-0.3.0 → lmfuser-0.4.0}/.github/workflows/python-publish.yml +0 -0
- {lmfuser-0.3.0 → lmfuser-0.4.0}/.gitignore +0 -0
- {lmfuser-0.3.0 → lmfuser-0.4.0}/.vscode/settings.json +0 -0
- {lmfuser-0.3.0 → lmfuser-0.4.0}/LICENSE +0 -0
- {lmfuser-0.3.0 → lmfuser-0.4.0}/README.md +0 -0
- {lmfuser-0.3.0 → lmfuser-0.4.0}/src/lmfuser/__init__.py +0 -0
- {lmfuser-0.3.0 → lmfuser-0.4.0}/src/lmfuser/model_loader.py +0 -0
- {lmfuser-0.3.0 → lmfuser-0.4.0}/src/lmfuser/runners/__init__.py +0 -0
- {lmfuser-0.3.0 → lmfuser-0.4.0}/src/lmfuser/runners/runner.py +0 -0
- {lmfuser-0.3.0 → lmfuser-0.4.0}/src/lmfuser/schedulers.py +0 -0
- {lmfuser-0.3.0 → lmfuser-0.4.0}/tests/test_fsdp2_compile.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
|
|
|
@@ -63,6 +63,72 @@ def _compile_kwargs(compile_mode: str) -> dict:
|
|
|
63
63
|
return {} if compile_mode == 'default' else {'mode': compile_mode}
|
|
64
64
|
|
|
65
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
|
+
|
|
66
132
|
class Wrapper(nn.Module):
|
|
67
133
|
|
|
68
134
|
def __init__(self, model: nn.Module, compile_mode: str = 'disable') -> None:
|
|
@@ -91,12 +157,17 @@ class Wrapper(nn.Module):
|
|
|
91
157
|
|
|
92
158
|
class DDPWraper(nn.Module):
|
|
93
159
|
|
|
94
|
-
def __init__(self, model: nn.Module, compile_mode: str = 'disable') -> None:
|
|
160
|
+
def __init__(self, model: nn.Module, compile_mode: str = 'disable', find_unused: bool = True, bf16_grads: bool = False) -> None:
|
|
95
161
|
super().__init__()
|
|
96
162
|
if get_world_size() > 1:
|
|
163
|
+
logger.critical(f'DDP options: find_unused={find_unused} bf16_grads={bf16_grads}')
|
|
97
164
|
self.model = DDP(
|
|
98
|
-
model.to(get_default_device()),
|
|
165
|
+
model.to(get_default_device()),
|
|
166
|
+
find_unused_parameters=find_unused,
|
|
99
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)
|
|
100
171
|
if compile_mode != 'disable':
|
|
101
172
|
# compile the DDP module so Dynamo's DDPOptimizer splits the
|
|
102
173
|
# graph at gradient-bucket boundaries (keeps comm overlap).
|
|
@@ -181,6 +252,29 @@ class DDPRunnerConfig(RunerConf):
|
|
|
181
252
|
# execution path must be declared in the YAML, never auto-detected.
|
|
182
253
|
# NOTE 'disable' not 'off': YAML 1.1 parses bare off/on/yes/no as booleans
|
|
183
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)
|
|
184
278
|
model_precision = OptionArg(options=['fp32', 'fp16', 'bf16'], default='fp32')
|
|
185
279
|
use_amp = BoolArg(default=False)
|
|
186
280
|
amp_precision = OptionArg(options=['fp16', 'bf16'], default='fp16')
|
|
@@ -248,6 +342,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
248
342
|
self.config.seed = self.config.seed.parse(hash(f'original_seed_{self.config.seed.value()}|step_{self.step}'))
|
|
249
343
|
|
|
250
344
|
self.train_data_loaders = config.task_conf.get_train_dataloaders(
|
|
345
|
+
resume_states=getattr(self, '_resume_data_states', None),
|
|
251
346
|
batch_size=config.sub_batch_size.value(), # type: ignore
|
|
252
347
|
seed=config.seed.value(), # type: ignore
|
|
253
348
|
shuffle=config.shuffle_dataset.value(), # type: ignore
|
|
@@ -365,6 +460,35 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
365
460
|
else:
|
|
366
461
|
raise ValueError(f'dp_type must be either "ddp" or "fsdp2", got "{self.dp_type}" instead.')
|
|
367
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
|
+
|
|
368
492
|
def save(self, directory: str | os.PathLike, *args, **kwargs) -> None:
|
|
369
493
|
if isinstance(self.model, (DDPWraper, Wrapper)):
|
|
370
494
|
model = self.model.model.module
|
|
@@ -390,11 +514,19 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
390
514
|
else:
|
|
391
515
|
raise ValueError(f'Unknown model type {type(self.model)}')
|
|
392
516
|
|
|
517
|
+
data_states = self._collect_data_states()
|
|
518
|
+
|
|
393
519
|
if get_global_rank() == 0:
|
|
394
520
|
path = Path(directory) / str(self.step)
|
|
395
521
|
os.makedirs(path, exist_ok=True)
|
|
396
522
|
self.model_loader.save_model(model, path) # type: ignore
|
|
397
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
|
+
|
|
398
530
|
optimizer_path = path / 'optimizer.pt'
|
|
399
531
|
torch.save(optim_states, optimizer_path)
|
|
400
532
|
|
|
@@ -429,7 +561,11 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
429
561
|
self._model = FSDP2Wrapper(model, compile_mode=compile_mode)
|
|
430
562
|
else:
|
|
431
563
|
logger.critical(f'wrapping model with DDPWraper')
|
|
432
|
-
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
|
+
)
|
|
433
569
|
assert self._model is not None
|
|
434
570
|
return self._model
|
|
435
571
|
|
|
@@ -559,11 +695,12 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
559
695
|
epochs = [loader.epoch for loader in self.train_data_loaders if loader is not None]
|
|
560
696
|
return max(epochs) + self.pre_epoch
|
|
561
697
|
|
|
562
|
-
def step_log(self, data: dict[str, Any]) -> None:
|
|
698
|
+
def step_log(self, data: dict[str, Any], console: bool = True) -> None:
|
|
563
699
|
self._wandb
|
|
564
700
|
if get_global_rank() != 0:
|
|
565
701
|
return
|
|
566
|
-
|
|
702
|
+
if console:
|
|
703
|
+
self.logger.critical(f'step:{self.step}\t{data}')
|
|
567
704
|
wandb.log(data, step=self.step)
|
|
568
705
|
|
|
569
706
|
def _one_train_step(self, **kwargs: Any) -> None:
|
|
@@ -577,6 +714,9 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
577
714
|
|
|
578
715
|
# calculate loss
|
|
579
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)
|
|
580
720
|
batch_datas: defaultdict[Hashable, List[float]] = defaultdict(list)
|
|
581
721
|
subbatch_result_lst: list[Batch] = []
|
|
582
722
|
for acc_idx in range(self.config._num_acc_steps):
|
|
@@ -604,13 +744,41 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
604
744
|
self.model.set_requires_gradient_sync(True, recurse=True)
|
|
605
745
|
|
|
606
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()
|
|
607
773
|
subbatch_result = task.train_step(
|
|
608
774
|
model=self.model, # type: ignore
|
|
609
|
-
batch=
|
|
610
|
-
step=self.step,
|
|
775
|
+
batch=_dev_batch,
|
|
776
|
+
step=self.step,
|
|
611
777
|
device=get_local_rank(),
|
|
612
778
|
acc_step=acc_idx,
|
|
613
779
|
)
|
|
780
|
+
if self.config.step_timing.value():
|
|
781
|
+
self._phase_acc[2] += _time.perf_counter() - _tf2b
|
|
614
782
|
if isinstance(subbatch_result, torch.Tensor):
|
|
615
783
|
subbatch_result = {'loss': subbatch_result}
|
|
616
784
|
if 'loss' not in subbatch_result:
|
|
@@ -621,7 +789,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
621
789
|
subbatch_result_lst.append(subbatch_result) # type: ignore
|
|
622
790
|
assert isinstance(loss, Tensor)
|
|
623
791
|
loss = loss / self.config._num_acc_steps
|
|
624
|
-
|
|
792
|
+
running_loss_t = loss.detach() if running_loss_t is None else running_loss_t + loss.detach()
|
|
625
793
|
for k, v in subbatch_result.items():
|
|
626
794
|
if k == 'loss':
|
|
627
795
|
continue
|
|
@@ -635,30 +803,66 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
635
803
|
else:
|
|
636
804
|
loss.backward()
|
|
637
805
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
+
)
|
|
650
850
|
|
|
651
851
|
grad_norm_clip_val = self.config.grad_norm_clip.value()
|
|
652
852
|
if grad_norm_clip_val is not None:
|
|
653
853
|
if self.scaler is not None:
|
|
654
854
|
self.scaler.unscale_(self.optimizer)
|
|
655
|
-
|
|
855
|
+
norm_t = clip_grad_norm_(
|
|
656
856
|
parameters=self.model.parameters(), # type: ignore
|
|
657
857
|
max_norm=grad_norm_clip_val
|
|
658
|
-
)
|
|
659
|
-
|
|
660
|
-
|
|
858
|
+
)
|
|
859
|
+
if log_this:
|
|
860
|
+
norm = dist_avg(norm_t.item())
|
|
861
|
+
self.step_log({f'{task.__class__.__name__}/train/grad_norm': norm})
|
|
661
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)
|
|
662
866
|
num_hot_params = 0
|
|
663
867
|
num_freeze_params = 0
|
|
664
868
|
for param in self.model.parameters(): # type: ignore
|
|
@@ -670,13 +874,12 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
670
874
|
num_total_params = num_hot_params + num_freeze_params
|
|
671
875
|
if num_total_params == 0:
|
|
672
876
|
raise RuntimeError('The model contains no parameters.')
|
|
673
|
-
|
|
674
877
|
self.step_log({
|
|
675
878
|
f'{task.__class__.__name__}/train/num_hot_params': num_hot_params,
|
|
676
879
|
f'{task.__class__.__name__}/train/num_freeze_params': num_freeze_params,
|
|
677
880
|
f'{task.__class__.__name__}/train/num_total_params': num_total_params,
|
|
678
881
|
f'{task.__class__.__name__}/train/hot_ratio': num_hot_params / num_total_params,
|
|
679
|
-
})
|
|
882
|
+
}, console=log_this)
|
|
680
883
|
|
|
681
884
|
if self.scaler is not None:
|
|
682
885
|
self.scaler.step(self.optimizer)
|
|
@@ -688,21 +891,22 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
688
891
|
current_lr = current_lr[0]
|
|
689
892
|
self.step_log(
|
|
690
893
|
{f'{task.__class__.__name__}/train/learning_rate': current_lr}
|
|
691
|
-
)
|
|
894
|
+
) if log_this else None
|
|
692
895
|
self.scheduler.step()
|
|
693
896
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
for
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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})
|
|
706
910
|
|
|
707
911
|
save_step_freq = self.config.save_step_freq.value()
|
|
708
912
|
assert save_step_freq is not None
|
|
@@ -712,7 +916,7 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
712
916
|
assert ckpt_path is not None
|
|
713
917
|
self.save(ckpt_path)
|
|
714
918
|
self._pbar_train.set_description('model saved!', True)
|
|
715
|
-
|
|
919
|
+
torch.distributed.barrier() if get_world_size() > 1 else ...
|
|
716
920
|
|
|
717
921
|
eval_step_freq = self.config.eval_step_freq.value()
|
|
718
922
|
assert eval_step_freq is not None
|
|
@@ -726,6 +930,16 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
726
930
|
self.eval() # evaluate first
|
|
727
931
|
self._prepare_train()
|
|
728
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
|
+
|
|
729
943
|
self._last_epoch = self.epoch
|
|
730
944
|
stop_metric = self.config.stop_by.value()
|
|
731
945
|
assert stop_metric in ('step', 'epoch')
|
|
@@ -753,6 +967,8 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
753
967
|
while not self._should_stop():
|
|
754
968
|
self._one_train_step()
|
|
755
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
|
|
756
972
|
self._pbar_train.update(1)
|
|
757
973
|
elif stop_metric == 'epoch':
|
|
758
974
|
current_epoch = self.epoch
|
|
@@ -951,6 +1167,19 @@ class DDPRunner(Runner[DDPRunnerConfig]):
|
|
|
951
1167
|
else:
|
|
952
1168
|
logger.warning(f'scheduler.pt not found in {directory}, skip loading scheduler')
|
|
953
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
|
+
|
|
954
1183
|
runner_path = Path(directory) / 'runner.json'
|
|
955
1184
|
if runner_path.exists():
|
|
956
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:
|
|
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
|
|
File without changes
|