LMFuser 0.1.3__tar.gz → 0.3.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.1.3
3
+ Version: 0.3.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.1.1
20
+ Requires-Dist: lmfuser-data>=0.2.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.1.3"
7
+ version = "0.3.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.1.1",
23
+ "LMFuser-data>=0.2.0",
24
24
  "tqdm",
25
25
  "wandb",
26
26
  "HyperArgs>=0.1.3",
@@ -59,42 +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
+
62
66
  class Wrapper(nn.Module):
63
67
 
64
- def __init__(self, model: nn.Module) -> None:
68
+ def __init__(self, model: nn.Module, compile_mode: str = 'disable') -> None:
65
69
  super().__init__()
66
70
  self.module = model.to(get_default_device())
67
- self.forward = model.forward
71
+ if compile_mode != 'disable':
72
+ logger.critical(f'torch.compile enabled (mode={compile_mode})')
73
+ self.forward = torch.compile(model.forward, **_compile_kwargs(compile_mode))
74
+ else:
75
+ self.forward = model.forward
76
+
77
+ def no_sync(self):
78
+ # duck-type DDP for the accumulation path: a single process has no
79
+ # gradient sync to skip (latent crash: single GPU + grad accumulation)
80
+ from contextlib import nullcontext
81
+ return nullcontext()
68
82
 
69
83
  def __getattr__(self, name: str) -> Any:
70
84
  try:
71
85
  return super().__getattr__(name)
72
- except:
73
- return self.module.__getattr__(name)
86
+ except AttributeError:
87
+ # full lookup (instance dict included) — nn.Module.__getattr__ alone
88
+ # misses plain attributes like `.config`
89
+ return getattr(self.module, name)
74
90
 
75
91
 
76
92
  class DDPWraper(nn.Module):
77
93
 
78
- def __init__(self, model: nn.Module) -> None:
94
+ def __init__(self, model: nn.Module, compile_mode: str = 'disable') -> None:
79
95
  super().__init__()
80
- self.model = DDP(
81
- model.to(get_default_device()), find_unused_parameters=True
82
- ) if get_world_size() > 1 else Wrapper(model)
83
- self.forward = self.model.forward
96
+ if get_world_size() > 1:
97
+ self.model = DDP(
98
+ model.to(get_default_device()), find_unused_parameters=True
99
+ )
100
+ if compile_mode != 'disable':
101
+ # compile the DDP module so Dynamo's DDPOptimizer splits the
102
+ # graph at gradient-bucket boundaries (keeps comm overlap).
103
+ # OptimizedModule forwards attribute access to the DDP module,
104
+ # so .module / .no_sync() paths keep working.
105
+ logger.critical(f'torch.compile enabled (mode={compile_mode})')
106
+ self.model = torch.compile(self.model, **_compile_kwargs(compile_mode))
107
+ self.forward = self.model.forward
108
+ else:
109
+ self.model = Wrapper(model, compile_mode=compile_mode)
110
+ self.forward = self.model.forward
84
111
 
85
112
  def __getattr__(self, name: str) -> Any:
86
113
  try:
87
114
  return super().__getattr__(name)
88
- except:
89
- return self.model.module.__getattr__(name)
115
+ except AttributeError:
116
+ return getattr(self.model.module, name)
90
117
 
91
118
 
92
119
  class FSDP2Wrapper(nn.Module):
93
- def __init__(self, model: FSDPModule) -> None:
120
+ def __init__(self, model: FSDPModule, compile_mode: str = 'disable') -> None:
94
121
  super().__init__()
95
122
  self.model = model
123
+ if compile_mode == 'reduce-overhead':
124
+ logger.critical(
125
+ 'compile_mode=reduce-overhead is unsupported with fsdp2 '
126
+ '(cudagraphs need static addresses; FSDP all-gathers allocate '
127
+ 'dynamically) — downgrading to "default".')
128
+ compile_mode = 'default'
129
+ if compile_mode != 'disable':
130
+ logger.critical(f'torch.compile enabled (mode={compile_mode})')
131
+ # compile the callable, NOT stored as a submodule: registering the
132
+ # OptimizedModule would duplicate every parameter under a second
133
+ # name and break optimizer construction / state_dict.
134
+ object.__setattr__(self, '_compiled_fwd',
135
+ torch.compile(model, **_compile_kwargs(compile_mode)))
136
+ else:
137
+ object.__setattr__(self, '_compiled_fwd', None)
96
138
 
97
139
  def forward(self, *args: Any, **kwargs: Any) -> Any:
140
+ if self._compiled_fwd is not None:
141
+ return self._compiled_fwd(*args, **kwargs)
98
142
  return self.model(*args, **kwargs) # type: ignore
99
143
 
100
144
  def __getattr__(self, name: str) -> Any:
@@ -129,6 +173,14 @@ class DDPRunnerConfig(RunerConf):
129
173
  lr_scheduler: LRSchedulerConfig = LRSchedulerConfig()
130
174
 
131
175
  dp_type = OptionArg(default='ddp', options=['ddp', 'fsdp2'])
176
+ # torch.compile for the training model. 'default' = Inductor fusion;
177
+ # 'reduce-overhead' additionally wraps CUDA graphs (ddp only — with fsdp2 it
178
+ # is downgraded to 'default': cudagraphs' static-address requirement is
179
+ # incompatible with FSDP's dynamically allocated all-gather buffers).
180
+ # Explicit-by-config on purpose: anything that changes numerics or the
181
+ # execution path must be declared in the YAML, never auto-detected.
182
+ # NOTE 'disable' not 'off': YAML 1.1 parses bare off/on/yes/no as booleans
183
+ compile_mode = OptionArg(default='disable', options=['disable', 'default', 'reduce-overhead'])
132
184
  model_precision = OptionArg(options=['fp32', 'fp16', 'bf16'], default='fp32')
133
185
  use_amp = BoolArg(default=False)
134
186
  amp_precision = OptionArg(options=['fp16', 'bf16'], default='fp16')
@@ -141,6 +193,11 @@ class DDPRunnerConfig(RunerConf):
141
193
  shuffle_dataset = BoolArg(default=True)
142
194
  row_prefetch = IntArg(0, min_value=0)
143
195
  num_row_workers = IntArg(1, min_value=1)
196
+ # batch-mode loader (train_dataloader_type: batch): TOTAL workers per rank,
197
+ # shared-memory ring depth, and per-slot capacity
198
+ num_batch_workers = IntArg(4, min_value=1)
199
+ batch_queue_depth = IntArg(4, min_value=2)
200
+ batch_slot_mb = IntArg(128, min_value=1)
144
201
 
145
202
  resume_training = BoolArg(default=False)
146
203
  resume_path = StrArg(default=None, allow_none=True)
@@ -202,6 +259,9 @@ class DDPRunner(Runner[DDPRunnerConfig]):
202
259
  worker_timeout=config.worker_timeout.value(), # type: ignore
203
260
  world_size=get_world_size(),
204
261
  rank=get_global_rank(),
262
+ num_batch_workers=config.num_batch_workers.value(), # type: ignore
263
+ batch_queue_depth=config.batch_queue_depth.value(), # type: ignore
264
+ batch_slot_mb=config.batch_slot_mb.value(), # type: ignore
205
265
  )
206
266
 
207
267
  self.eval_data_loaders = config.task_conf.get_eval_dataloaders(
@@ -362,12 +422,14 @@ class DDPRunner(Runner[DDPRunnerConfig]):
362
422
  model = model.bfloat16() # type: ignore
363
423
  elif self.config.model_precision.value() == 'fp32':
364
424
  model = model.float() # type: ignore
425
+ compile_mode = self.config.compile_mode.value()
426
+ assert compile_mode is not None
365
427
  if isinstance(model, FSDPModule):
366
428
  logger.critical(f'wrapping model with FSDPModule')
367
- self._model = FSDP2Wrapper(model)
429
+ self._model = FSDP2Wrapper(model, compile_mode=compile_mode)
368
430
  else:
369
431
  logger.critical(f'wrapping model with DDPWraper')
370
- self._model = DDPWraper(model)
432
+ self._model = DDPWraper(model, compile_mode=compile_mode)
371
433
  assert self._model is not None
372
434
  return self._model
373
435
 
@@ -5,7 +5,7 @@ import torch
5
5
  from torch import nn
6
6
  from lmfuser_data.interfaces import Batch, Row
7
7
  from lmfuser_data.scanners import Scanner
8
- from lmfuser_data import DataLoader, PyTorchDataLoader
8
+ from lmfuser_data import DataLoader, PyTorchDataLoader, BatchDataLoader
9
9
  from lmfuser_data.interfaces import SubclassTracer
10
10
  from hyperargs import Conf, StrArg, FloatArg, IntArg, OptionArg, add_dependency, monitor_on
11
11
 
@@ -53,7 +53,7 @@ class TaskBase(Conf, SubclassTracer):
53
53
 
54
54
  scanner_type = OptionArg(default='C4Scanner', option_fn=scanner_type_list)
55
55
 
56
- train_dataloader_type = OptionArg(default='single file', options=['single file', 'sharded', 'empty'])
56
+ train_dataloader_type = OptionArg(default='single file', options=['single file', 'sharded', 'batch', 'empty'])
57
57
  eval_dataloader_type = OptionArg(default='single file', options=['single file', 'sharded', 'empty'])
58
58
  test_dataloader_type = OptionArg(default='single file', options=['single file', 'sharded', 'empty'])
59
59
 
@@ -106,8 +106,11 @@ class TaskBase(Conf, SubclassTracer):
106
106
  worker_timeout: float,
107
107
  num_workers: int,
108
108
  rank: int,
109
- world_size: int
110
- ) -> None | DataLoader | PyTorchDataLoader | EmptyDataLoader:
109
+ world_size: int,
110
+ num_batch_workers: int = 4,
111
+ batch_queue_depth: int = 4,
112
+ batch_slot_mb: int = 128,
113
+ ) -> None | DataLoader | PyTorchDataLoader | BatchDataLoader | EmptyDataLoader:
111
114
  if self.num_train_data_path.value() == 0:
112
115
  return None
113
116
  if self._train_dataloader is not None:
@@ -118,9 +121,30 @@ class TaskBase(Conf, SubclassTracer):
118
121
  assert scanner_type is not None, 'scanner_type is None'
119
122
 
120
123
  dataloader_type = self.train_dataloader_type.value()
121
- assert dataloader_type in ('sharded', 'single file'), f'Unknown dataloader type: {dataloader_type}'
124
+ assert dataloader_type in ('sharded', 'single file', 'batch', 'empty'), \
125
+ f'Unknown dataloader type: {dataloader_type}'
122
126
 
123
- if dataloader_type == 'sharded':
127
+ if dataloader_type == 'batch':
128
+ self._train_dataloader = BatchDataLoader(
129
+ batch_size=batch_size,
130
+ path_list=path_list, # type: ignore
131
+ distributor_weights=weight_list, # type: ignore
132
+ scanner_type=Scanner.get_subclass(scanner_type),
133
+ seed=seed,
134
+ shuffle=shuffle,
135
+ map_fn=self.get_row_processor(),
136
+ flow_fn=self.get_flow_processor(),
137
+ collate_fn=self.get_collate_fn(),
138
+ batch_map_fn=self.get_batch_processor(),
139
+ ignore_error=ignore_error,
140
+ num_workers=num_batch_workers,
141
+ queue_depth=batch_queue_depth,
142
+ slot_mb=batch_slot_mb,
143
+ num_ranks=world_size,
144
+ rank_idx=rank,
145
+ worker_timeout=worker_timeout,
146
+ )
147
+ elif dataloader_type == 'sharded':
124
148
  self._train_dataloader = DataLoader(
125
149
  batch_size=batch_size,
126
150
  path_list=path_list, # type: ignore
@@ -402,8 +426,11 @@ class Tasks(Conf):
402
426
  worker_timeout: float,
403
427
  num_workers: int,
404
428
  rank: int,
405
- world_size: int
406
- ) -> list[DataLoader | None | PyTorchDataLoader | EmptyDataLoader]:
429
+ world_size: int,
430
+ num_batch_workers: int = 4,
431
+ batch_queue_depth: int = 4,
432
+ batch_slot_mb: int = 128,
433
+ ) -> list[DataLoader | None | PyTorchDataLoader | BatchDataLoader | EmptyDataLoader]:
407
434
  return [
408
435
  task.conf._get_train_dataloader(
409
436
  batch_size=batch_size,
@@ -417,6 +444,9 @@ class Tasks(Conf):
417
444
  num_workers=num_workers,
418
445
  rank=rank,
419
446
  world_size=world_size,
447
+ num_batch_workers=num_batch_workers,
448
+ batch_queue_depth=batch_queue_depth,
449
+ batch_slot_mb=batch_slot_mb,
420
450
  )
421
451
  for task in self.tasks
422
452
  ]
@@ -42,7 +42,11 @@ def dist_init() -> None:
42
42
  device_type = get_default_device_type()
43
43
 
44
44
  if device_type == 'cuda':
45
- dist.init_process_group(backend='nccl')
45
+ import datetime as _dt
46
+ # 1h timeout: a transient data stall on one rank (e.g. a shard
47
+ # download hang) must not nuke the whole DDP group (default 10min
48
+ # killed a pretrain run mid-flight)
49
+ dist.init_process_group(backend='nccl', timeout=_dt.timedelta(seconds=3600))
46
50
  torch.cuda.set_device(get_local_rank())
47
51
  elif device_type == 'npu':
48
52
  dist.init_process_group(backend='hccl')
@@ -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