-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathengine.py
More file actions
319 lines (266 loc) · 12.8 KB
/
Copy pathengine.py
File metadata and controls
319 lines (266 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
import os
import sys
import logging
from itertools import islice
import torch
from torch import distributed as dist
from torch import nn
from torch.utils import data as torch_data
from torchdrug import data, core, utils
from torchdrug.core import Registry as R
from torchdrug.utils import comm, pretty
module = sys.modules[__name__]
logger = logging.getLogger(__name__)
@R.register("core.Engine")
class Engine(core.Configurable):
"""
General class that handles everything about training and test of a task.
This class can perform synchronous distributed parallel training over multiple CPUs or GPUs.
To invoke parallel training, launch with one of the following commands.
1. Single-node multi-process case.
.. code-block:: bash
python -m torch.distributed.launch --nproc_per_node={number_of_gpus} {your_script.py} {your_arguments...}
2. Multi-node multi-process case.
.. code-block:: bash
python -m torch.distributed.launch --nnodes={number_of_nodes} --node_rank={rank_of_this_node}
--nproc_per_node={number_of_gpus} {your_script.py} {your_arguments...}
If :meth:`preprocess` is defined by the task, it will be applied to ``train_set``, ``valid_set`` and ``test_set``.
Parameters:
task (nn.Module): task
train_set (data.Dataset): training set
valid_set (data.Dataset): validation set
test_set (data.Dataset): test set
optimizer (optim.Optimizer): optimizer
scheduler (lr_scheduler._LRScheduler, optional): scheduler
gpus (list of int, optional): GPU ids. By default, CPUs will be used.
For multi-node multi-process case, repeat the GPU ids for each node.
batch_size (int, optional): batch size of a single CPU / GPU
gradient_interval (int, optional): perform a gradient update every n batches.
This creates an equivalent batch size of ``batch_size * gradient_interval`` for optimization.
num_worker (int, optional): number of CPU workers per GPU
logger (str or core.LoggerBase, optional): logger type or logger instance.
Available types are ``logging`` and ``wandb``.
log_interval (int, optional): log every n gradient updates
"""
def __init__(self, task, train1_set, train2_set, train3_set, valid_set, test_set, optimizer, scheduler=None, gpus=None, batch_size=1,
gradient_interval=1, num_worker=0, logger="logging", log_interval=100):
self.rank = comm.get_rank()
self.world_size = comm.get_world_size()
self.gpus = gpus
self.batch_size = batch_size
self.gradient_interval = gradient_interval
self.num_worker = num_worker
if gpus is None:
self.device = torch.device("cpu")
else:
if len(gpus) != self.world_size:
error_msg = "World size is %d but found %d GPUs in the argument"
if self.world_size == 1:
error_msg += ". Did you launch with `python -m torch.distributed.launch`?"
raise ValueError(error_msg % (self.world_size, len(gpus)))
self.device = torch.device(gpus[self.rank % len(gpus)])
if self.world_size > 1 and not dist.is_initialized():
if self.rank == 0:
module.logger.info("Initializing distributed process group")
backend = "gloo" if gpus is None else "nccl"
comm.init_process_group(backend, init_method="env://")
if hasattr(task, "preprocess"):
if self.rank == 0:
module.logger.warning("Preprocess training set")
# TODO: more elegant implementation
# handle dynamic parameters in optimizer
old_params = list(task.parameters())
result = task.preprocess(train1_set, train2_set, train3_set, valid_set, test_set)
if result is not None:
train1_set, train2_set, train3_set, valid_set, test_set = result
new_params = list(task.parameters())
if len(new_params) != len(old_params):
optimizer.add_param_group({"params": new_params[len(old_params):]})
if self.world_size > 1:
task = nn.SyncBatchNorm.convert_sync_batchnorm(task)
buffers_to_ignore = []
for name, buffer in task.named_buffers():
if not isinstance(buffer, torch.Tensor):
buffers_to_ignore.append(name)
task._ddp_params_and_buffers_to_ignore = set(buffers_to_ignore)
if self.device.type == "cuda":
task = task.cuda(self.device)
self.model = task
self.train1_set = train1_set
self.train2_set = train2_set
self.train3_set = train3_set
self.valid_set = valid_set
self.test_set = test_set
self.optimizer = optimizer
self.scheduler = scheduler
if isinstance(logger, str):
if logger == "logging":
logger = core.LoggingLogger()
elif logger == "wandb":
logger = core.WandbLogger(project=task.__class__.__name__)
else:
raise ValueError("Unknown logger `%s`" % logger)
self.meter = core.Meter(log_interval=log_interval, silent=self.rank > 0, logger=logger)
self.meter.log_config(self.config_dict())
def train(self, num_epoch=1, batch_per_epoch=None):
"""
Train the model.
If ``batch_per_epoch`` is specified, randomly draw a subset of the training set for each epoch.
Otherwise, the whole training set is used for each epoch.
Parameters:
num_epoch (int, optional): number of epochs
batch_per_epoch (int, optional): number of batches per epoch
"""
sampler1 = torch_data.DistributedSampler(self.train1_set, self.world_size, self.rank)
sampler2 = torch_data.DistributedSampler(self.train2_set, self.world_size, self.rank)
sampler3 = torch_data.DistributedSampler(self.train3_set, self.world_size, self.rank)
if self.rank==0:
dataloader1 = data.DataLoader(self.train1_set, self.batch_size-1, sampler=sampler1, num_workers=self.num_worker)
dataloader2 = data.DataLoader(self.train2_set, self.batch_size-1, sampler=sampler2, num_workers=self.num_worker)
dataloader3 = data.DataLoader(self.train3_set, self.batch_size-1, sampler=sampler3, num_workers=self.num_worker)
else:
dataloader1 = data.DataLoader(self.train1_set, self.batch_size, sampler=sampler1, num_workers=self.num_worker)
dataloader2 = data.DataLoader(self.train2_set, self.batch_size, sampler=sampler2, num_workers=self.num_worker)
dataloader3 = data.DataLoader(self.train3_set, self.batch_size, sampler=sampler3, num_workers=self.num_worker)
batch_per_epoch = batch_per_epoch or len(dataloader1)
model = self.model
model.split = "train"
if self.world_size > 1:
if self.device.type == "cuda":
model = nn.parallel.DistributedDataParallel(model, device_ids=[self.device],
find_unused_parameters=True)
else:
model = nn.parallel.DistributedDataParallel(model, find_unused_parameters=True)
model.train()
for epoch in self.meter(num_epoch):
if epoch%3==0:
sampler1.set_epoch(epoch)
dataloader = dataloader1
eph = 0
elif epoch%3==1:
sampler2.set_epoch(epoch)
dataloader = dataloader2
eph = 1
else:
sampler3.set_epoch(epoch)
dataloader = dataloader3
eph = 2
metrics = []
start_id = 0
# the last gradient update may contain less than gradient_interval batches
gradient_interval = min(batch_per_epoch - start_id, self.gradient_interval)
for batch_id, batch in enumerate(islice(dataloader, batch_per_epoch)):
if self.device.type == "cuda":
batch = utils.cuda(batch, device=self.device)
loss, metric = model(batch, eph=eph)
if not loss.requires_grad:
raise RuntimeError("Loss doesn't require grad. Did you define any loss in the task?")
loss = loss / gradient_interval
loss.backward()
metrics.append(metric)
if batch_id - start_id + 1 == gradient_interval:
self.optimizer.step()
self.optimizer.zero_grad()
metric = utils.stack(metrics, dim=0)
metric = utils.mean(metric, dim=0)
if self.world_size > 1:
metric = comm.reduce(metric, op="mean")
self.meter.update(metric)
metrics = []
start_id = batch_id + 1
gradient_interval = min(batch_per_epoch - start_id, self.gradient_interval)
if self.scheduler:
self.scheduler.step()
@torch.no_grad()
def evaluate(self, split, log=True):
"""
Evaluate the model.
Parameters:
split (str): split to evaluate. Can be ``train``, ``valid`` or ``test``.
log (bool, optional): log metrics or not
Returns:
dict: metrics
"""
if comm.get_rank() == 0:
logger.warning(pretty.separator)
logger.warning("Evaluate on %s" % split)
test_set = getattr(self, "%s_set" % split)
sampler = torch_data.DistributedSampler(test_set, self.world_size, self.rank)
dataloader = data.DataLoader(test_set, self.batch_size, sampler=sampler, num_workers=self.num_worker)
model = self.model
model.split = split
model.eval()
preds = []
targets = []
for batch in dataloader:
if self.device.type == "cuda":
batch = utils.cuda(batch, device=self.device)
pred, target = model.predict_and_target(batch)
preds.append(pred)
targets.append(target)
pred = utils.cat(preds)
target = utils.cat(targets)
if self.world_size > 1:
pred = comm.cat(pred)
target = comm.cat(target)
metric = model.evaluate(pred, target)
if log:
self.meter.log(metric, category="%s/epoch" % split)
return metric
def load(self, checkpoint, load_optimizer=True, strict=True):
"""
Load a checkpoint from file.
Parameters:
checkpoint (file-like): checkpoint file
load_optimizer (bool, optional): load optimizer state or not
strict (bool, optional): whether to strictly check the checkpoint matches the model parameters
"""
if comm.get_rank() == 0:
logger.warning("Load checkpoint from %s" % checkpoint)
checkpoint = os.path.expanduser(checkpoint)
state = torch.load(checkpoint, map_location=self.device)
self.model.load_state_dict(state["model"], strict=strict)
if load_optimizer:
self.optimizer.load_state_dict(state["optimizer"])
for state in self.optimizer.state.values():
for k, v in state.items():
if isinstance(v, torch.Tensor):
state[k] = v.to(self.device)
comm.synchronize()
def save(self, checkpoint):
"""
Save checkpoint to file.
Parameters:
checkpoint (file-like): checkpoint file
"""
if comm.get_rank() == 0:
logger.warning("Save checkpoint to %s" % checkpoint)
checkpoint = os.path.expanduser(checkpoint)
if self.rank == 0:
state = {
"model": self.model.state_dict(),
"optimizer": self.optimizer.state_dict()
}
torch.save(state, checkpoint)
comm.synchronize()
@classmethod
def load_config_dict(cls, config):
"""
Construct an instance from the configuration dict.
"""
if getattr(cls, "_registry_key", cls.__name__) != config["class"]:
raise ValueError("Expect config class to be `%s`, but found `%s`" % (cls.__name__, config["class"]))
optimizer_config = config.pop("optimizer")
new_config = {}
for k, v in config.items():
if isinstance(v, dict) and "class" in v:
v = core.Configurable.load_config_dict(v)
if k != "class":
new_config[k] = v
optimizer_config["params"] = new_config["task"].parameters()
new_config["optimizer"] = core.Configurable.load_config_dict(optimizer_config)
return cls(**new_config)
@property
def epoch(self):
"""Current epoch."""
return self.meter.epoch_id