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.
- {lmfuser-0.1.3 → lmfuser-0.3.0}/PKG-INFO +2 -2
- {lmfuser-0.1.3 → lmfuser-0.3.0}/pyproject.toml +2 -2
- {lmfuser-0.1.3 → lmfuser-0.3.0}/src/lmfuser/runners/ddp_runner.py +76 -14
- {lmfuser-0.1.3 → lmfuser-0.3.0}/src/lmfuser/task.py +38 -8
- {lmfuser-0.1.3 → lmfuser-0.3.0}/src/lmfuser/utils.py +5 -1
- lmfuser-0.3.0/tests/test_fsdp2_compile.py +72 -0
- {lmfuser-0.1.3 → lmfuser-0.3.0}/.github/workflows/python-publish.yml +0 -0
- {lmfuser-0.1.3 → lmfuser-0.3.0}/.gitignore +0 -0
- {lmfuser-0.1.3 → lmfuser-0.3.0}/.vscode/settings.json +0 -0
- {lmfuser-0.1.3 → lmfuser-0.3.0}/LICENSE +0 -0
- {lmfuser-0.1.3 → lmfuser-0.3.0}/README.md +0 -0
- {lmfuser-0.1.3 → lmfuser-0.3.0}/src/lmfuser/__init__.py +0 -0
- {lmfuser-0.1.3 → lmfuser-0.3.0}/src/lmfuser/model_loader.py +0 -0
- {lmfuser-0.1.3 → lmfuser-0.3.0}/src/lmfuser/optimizers.py +0 -0
- {lmfuser-0.1.3 → lmfuser-0.3.0}/src/lmfuser/runners/__init__.py +0 -0
- {lmfuser-0.1.3 → lmfuser-0.3.0}/src/lmfuser/runners/runner.py +0 -0
- {lmfuser-0.1.3 → lmfuser-0.3.0}/src/lmfuser/schedulers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: LMFuser
|
|
3
|
-
Version: 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
|
|
@@ -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.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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
model
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
-
|
|
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'
|
|
124
|
+
assert dataloader_type in ('sharded', 'single file', 'batch', 'empty'), \
|
|
125
|
+
f'Unknown dataloader type: {dataloader_type}'
|
|
122
126
|
|
|
123
|
-
if dataloader_type == '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|