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.
- {lmfuser-0.2.0 → lmfuser-0.3.0}/PKG-INFO +1 -1
- {lmfuser-0.2.0 → lmfuser-0.3.0}/pyproject.toml +1 -1
- {lmfuser-0.2.0 → lmfuser-0.3.0}/src/lmfuser/runners/ddp_runner.py +56 -10
- lmfuser-0.3.0/tests/test_fsdp2_compile.py +72 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/.github/workflows/python-publish.yml +0 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/.gitignore +0 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/.vscode/settings.json +0 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/LICENSE +0 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/README.md +0 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/src/lmfuser/__init__.py +0 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/src/lmfuser/model_loader.py +0 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/src/lmfuser/optimizers.py +0 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/src/lmfuser/runners/__init__.py +0 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/src/lmfuser/runners/runner.py +0 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/src/lmfuser/schedulers.py +0 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/src/lmfuser/task.py +0 -0
- {lmfuser-0.2.0 → lmfuser-0.3.0}/src/lmfuser/utils.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
89
|
-
model
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|