LMFuser 0.2.0__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.2.0
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "LMFuser"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  requires-python = ">= 3.11"
9
9
  description = "The LMFuser training framework."
10
10
  readme = "README.md"
@@ -59,12 +59,20 @@ 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
68
76
 
69
77
  def no_sync(self):
70
78
  # duck-type DDP for the accumulation path: a single process has no
@@ -83,12 +91,23 @@ class Wrapper(nn.Module):
83
91
 
84
92
  class DDPWraper(nn.Module):
85
93
 
86
- def __init__(self, model: nn.Module) -> None:
94
+ def __init__(self, model: nn.Module, compile_mode: str = 'disable') -> None:
87
95
  super().__init__()
88
- self.model = DDP(
89
- model.to(get_default_device()), find_unused_parameters=True
90
- ) if get_world_size() > 1 else Wrapper(model)
91
- 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
92
111
 
93
112
  def __getattr__(self, name: str) -> Any:
94
113
  try:
@@ -98,11 +117,28 @@ class DDPWraper(nn.Module):
98
117
 
99
118
 
100
119
  class FSDP2Wrapper(nn.Module):
101
- def __init__(self, model: FSDPModule) -> None:
120
+ def __init__(self, model: FSDPModule, compile_mode: str = 'disable') -> None:
102
121
  super().__init__()
103
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)
104
138
 
105
139
  def forward(self, *args: Any, **kwargs: Any) -> Any:
140
+ if self._compiled_fwd is not None:
141
+ return self._compiled_fwd(*args, **kwargs)
106
142
  return self.model(*args, **kwargs) # type: ignore
107
143
 
108
144
  def __getattr__(self, name: str) -> Any:
@@ -137,6 +173,14 @@ class DDPRunnerConfig(RunerConf):
137
173
  lr_scheduler: LRSchedulerConfig = LRSchedulerConfig()
138
174
 
139
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'])
140
184
  model_precision = OptionArg(options=['fp32', 'fp16', 'bf16'], default='fp32')
141
185
  use_amp = BoolArg(default=False)
142
186
  amp_precision = OptionArg(options=['fp16', 'bf16'], default='fp16')
@@ -378,12 +422,14 @@ class DDPRunner(Runner[DDPRunnerConfig]):
378
422
  model = model.bfloat16() # type: ignore
379
423
  elif self.config.model_precision.value() == 'fp32':
380
424
  model = model.float() # type: ignore
425
+ compile_mode = self.config.compile_mode.value()
426
+ assert compile_mode is not None
381
427
  if isinstance(model, FSDPModule):
382
428
  logger.critical(f'wrapping model with FSDPModule')
383
- self._model = FSDP2Wrapper(model)
429
+ self._model = FSDP2Wrapper(model, compile_mode=compile_mode)
384
430
  else:
385
431
  logger.critical(f'wrapping model with DDPWraper')
386
- self._model = DDPWraper(model)
432
+ self._model = DDPWraper(model, compile_mode=compile_mode)
387
433
  assert self._model is not None
388
434
  return self._model
389
435
 
@@ -0,0 +1,72 @@
1
+ """FSDP2Wrapper + compile_mode coverage (synthetic, single GPU).
2
+
3
+ Run: torchrun --nproc_per_node=1 tests/test_fsdp2_compile.py
4
+
5
+ Covers, per compile_mode in {disable, default, reduce-overhead}:
6
+ * forward/backward/optimizer step run under fully_shard;
7
+ * parameters are NOT duplicated by compilation (the OptimizedModule must not
8
+ be registered as a submodule);
9
+ * 'reduce-overhead' downgrades to 'default' with a critical log.
10
+ """
11
+ import logging
12
+ import os
13
+ import sys
14
+
15
+ import torch
16
+ from torch import nn
17
+ import torch.distributed as dist
18
+ from torch.distributed.fsdp import fully_shard
19
+
20
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
21
+ from lmfuser.runners.ddp_runner import FSDP2Wrapper # noqa: E402
22
+
23
+
24
+ def build_model() -> nn.Module:
25
+ torch.manual_seed(0)
26
+ return nn.Sequential(
27
+ nn.Linear(64, 128), nn.GELU(), nn.Linear(128, 64),
28
+ )
29
+
30
+
31
+ def run_mode(mode: str) -> None:
32
+ model = build_model().cuda()
33
+ n_params_before = len(list(model.parameters()))
34
+ fully_shard(model)
35
+
36
+ records: list[str] = []
37
+ handler = logging.Handler()
38
+ handler.emit = lambda r: records.append(r.getMessage()) # type: ignore
39
+ logging.getLogger('lmfuser.runners.ddp_runner').addHandler(handler)
40
+
41
+ wrapper = FSDP2Wrapper(model, compile_mode=mode)
42
+
43
+ # ---- parameter registration must not be duplicated by compile ----
44
+ n_params_after = len(list(wrapper.parameters()))
45
+ assert n_params_after == n_params_before, \
46
+ f'[{mode}] parameter duplication: {n_params_before} -> {n_params_after}'
47
+ opt = torch.optim.AdamW(wrapper.parameters(), lr=1e-3) # raises on duplicates
48
+
49
+ # ---- train a few steps ----
50
+ for _ in range(3):
51
+ x = torch.randn(8, 64, device='cuda')
52
+ loss = wrapper(x).square().mean()
53
+ loss.backward()
54
+ opt.step()
55
+ opt.zero_grad()
56
+ assert torch.isfinite(loss).item(), f'[{mode}] non-finite loss'
57
+
58
+ # ---- downgrade warning ----
59
+ if mode == 'reduce-overhead':
60
+ assert any('downgrading' in m for m in records), \
61
+ '[reduce-overhead] downgrade warning not logged'
62
+
63
+ print(f'PASS fsdp2 compile_mode={mode} (loss={loss.item():.4f})')
64
+
65
+
66
+ if __name__ == '__main__':
67
+ dist.init_process_group(backend='nccl')
68
+ torch.cuda.set_device(0)
69
+ for mode in ('disable', 'default', 'reduce-overhead'):
70
+ run_mode(mode)
71
+ dist.destroy_process_group()
72
+ print('FSDP2_COMPILE_TESTS_PASS')
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes