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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: LMFuser
3
- Version: 0.3.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.2.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.3.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.2.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()), find_unused_parameters=True
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(model, compile_mode=compile_mode)
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
- self.logger.critical(f'step:{self.step}\t{data}')
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=self._batch_to_device(self._next_train_batch(task_id)),
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
- running_loss += loss.item()
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
- running_loss = dist_avg(running_loss)
639
- self.step_log({f'{task.__class__.__name__}/train/loss': running_loss})
640
- self.step_log({'train/epoch': self.epoch})
641
- for k, v in batch_datas.items():
642
- try:
643
- avg = sum(v) / len(v)
644
- except:
645
- continue
646
- self.step_log({f'{task.__class__.__name__}/train/{k}': avg})
647
- self._pbar_train.set_description(
648
- f'train loss: {running_loss:.3g}', refresh=True
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
- norm = clip_grad_norm_(
855
+ norm_t = clip_grad_norm_(
656
856
  parameters=self.model.parameters(), # type: ignore
657
857
  max_norm=grad_norm_clip_val
658
- ).item()
659
- norm = dist_avg(norm)
660
- self.step_log({f'{task.__class__.__name__}/train/grad_norm': norm})
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
- other_step_results = defaultdict(list)
695
- for r in subbatch_result_lst:
696
- for k, v in r.items():
697
- if isinstance(v, (float, int)):
698
- other_step_results[k].append(float(v))
699
- all_other_step_results = batch_all_gather(other_step_results)
700
- for k, v in all_other_step_results.items():
701
- try:
702
- avg = sum(v) / len(v)
703
- except:
704
- continue
705
- self.step_log({f'{task.__class__.__name__}/train/{k}': avg})
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
- torch.distributed.barrier() if get_world_size() > 1 else ...
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