train.py 12.6 KB
Newer Older
Myle Ott's avatar
Myle Ott committed
1
#!/usr/bin/env python3 -u
2
# Copyright (c) Facebook, Inc. and its affiliates.
Sergey Edunov's avatar
Sergey Edunov committed
3
#
4
5
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
Myle Ott's avatar
Myle Ott committed
6
7
8
"""
Train a new model on one or across multiple GPUs.
"""
Sergey Edunov's avatar
Sergey Edunov committed
9

10
11
import collections
import math
12
13
import random

14
import numpy as np
15
import torch
Sergey Edunov's avatar
Sergey Edunov committed
16

Myle Ott's avatar
Myle Ott committed
17
from fairseq import checkpoint_utils, distributed_utils, options, progress_bar, tasks, utils
18
from fairseq.data import iterators
19
20
from fairseq.trainer import Trainer
from fairseq.meters import AverageMeter, StopwatchMeter
Sergey Edunov's avatar
Sergey Edunov committed
21

22
23
fb_pathmgr_registerd = False

Myle Ott's avatar
Myle Ott committed
24

25
def main(args, init_distributed=False):
Myle Ott's avatar
Myle Ott committed
26
    utils.import_user_module(args)
27

28
29
30
31
32
33
34
35
36
    try:
        from fairseq.fb_pathmgr import fb_pathmgr
        global fb_pathmgr_registerd
        if not fb_pathmgr_registerd:
            fb_pathmgr.register()
            fb_pathmgr_registerd = True
    except (ModuleNotFoundError, ImportError):
        pass

37
38
    assert args.max_tokens is not None or args.max_sentences is not None, \
        'Must specify batch size either with --max-tokens or --max-sentences'
39

40
    # Initialize CUDA and distributed training
Myle Ott's avatar
Myle Ott committed
41
42
    if torch.cuda.is_available() and not args.cpu:
        torch.cuda.set_device(args.device_id)
43
    np.random.seed(args.seed)
44
    torch.manual_seed(args.seed)
45
46
47
    if init_distributed:
        args.distributed_rank = distributed_utils.distributed_init(args)

48
49
50
    if distributed_utils.is_master(args):
        checkpoint_utils.verify_checkpoint_directory(args.save_dir)

51
52
    # Print args
    print(args)
53

Myle Ott's avatar
Myle Ott committed
54
55
    # Setup task, e.g., translation, language modeling, etc.
    task = tasks.setup_task(args)
56

Myle Ott's avatar
Myle Ott committed
57
    # Load valid dataset (we load training data below, based on the latest checkpoint)
Naman Goyal's avatar
Naman Goyal committed
58
    for valid_sub_split in args.valid_subset.split(','):
59
        task.load_dataset(valid_sub_split, combine=False, epoch=0)
60

Myle Ott's avatar
Myle Ott committed
61
62
63
    # Build model and criterion
    model = task.build_model(args)
    criterion = task.build_criterion(args)
64
    print(model)
65
    print('| model {}, criterion {}'.format(args.arch, criterion.__class__.__name__))
66
67
68
69
    print('| num. model params: {} (num. trained: {})'.format(
        sum(p.numel() for p in model.parameters()),
        sum(p.numel() for p in model.parameters() if p.requires_grad),
    ))
70
71

    # Build trainer
Myle Ott's avatar
Myle Ott committed
72
    trainer = Trainer(args, task, model, criterion)
73
74
75
76
77
78
    print('| training on {} GPUs'.format(args.distributed_world_size))
    print('| max tokens per GPU = {} and max sentences per GPU = {}'.format(
        args.max_tokens,
        args.max_sentences,
    ))

Myle Ott's avatar
Myle Ott committed
79
80
81
    # Load the latest checkpoint if one is available and restore the
    # corresponding train iterator
    extra_state, epoch_itr = checkpoint_utils.load_checkpoint(args, trainer)
82
83
84
85
86
87
88

    # Train until the learning rate gets too small
    max_epoch = args.max_epoch or math.inf
    max_update = args.max_update or math.inf
    lr = trainer.get_lr()
    train_meter = StopwatchMeter()
    train_meter.start()
89
    valid_subsets = args.valid_subset.split(',')
Myle Ott's avatar
Myle Ott committed
90
    while lr > args.min_lr and epoch_itr.epoch < max_epoch and trainer.get_num_updates() < max_update:
91
        # train for one epoch
Myle Ott's avatar
Myle Ott committed
92
        train(args, trainer, task, epoch_itr)
93

Myle Ott's avatar
Myle Ott committed
94
        if not args.disable_validation and epoch_itr.epoch % args.validate_interval == 0:
Myle Ott's avatar
Myle Ott committed
95
            valid_losses = validate(args, trainer, task, epoch_itr, valid_subsets)
Myle Ott's avatar
Myle Ott committed
96
97
        else:
            valid_losses = [None]
98
99

        # only use first validation loss to update the learning rate
Myle Ott's avatar
Myle Ott committed
100
        lr = trainer.lr_step(epoch_itr.epoch, valid_losses[0])
101
102

        # save checkpoint
Myle Ott's avatar
Myle Ott committed
103
        if epoch_itr.epoch % args.save_interval == 0:
Myle Ott's avatar
Myle Ott committed
104
            checkpoint_utils.save_checkpoint(args, trainer, epoch_itr, valid_losses[0])
Naman Goyal's avatar
Naman Goyal committed
105

106
107
108
        reload_dataset = ':' in getattr(args, 'data', '')
        # sharded data: get train iterator for next epoch
        epoch_itr = trainer.get_train_iterator(epoch_itr.epoch, load_dataset=reload_dataset)
109
110
111
112
    train_meter.stop()
    print('| done training in {:.1f} seconds'.format(train_meter.sum))


Myle Ott's avatar
Myle Ott committed
113
def train(args, trainer, task, epoch_itr):
114
    """Train the model for one epoch."""
115
    # Update parameters every N batches
Myle Ott's avatar
Myle Ott committed
116
    update_freq = args.update_freq[epoch_itr.epoch - 1] \
Myle Ott's avatar
Myle Ott committed
117
        if epoch_itr.epoch <= len(args.update_freq) else args.update_freq[-1]
Myle Ott's avatar
Myle Ott committed
118
119
120
121
122
123

    # Initialize data iterator
    itr = epoch_itr.next_epoch_itr(
        fix_batches_to_gpus=args.fix_batches_to_gpus,
        shuffle=(epoch_itr.epoch >= args.curriculum),
    )
124
125
126
127
128
    itr = iterators.GroupedIterator(itr, update_freq)
    progress = progress_bar.build_progress_bar(
        args, itr, epoch_itr.epoch, no_progress_bar='simple',
    )

129
    extra_meters = collections.defaultdict(lambda: AverageMeter())
130
    valid_subsets = args.valid_subset.split(',')
131
    max_update = args.max_update or math.inf
132
133
134
    for i, samples in enumerate(progress, start=epoch_itr.iterations_in_epoch):
        log_output = trainer.train_step(samples)
        if log_output is None:
135
136
137
138
139
            continue

        # log mid-epoch stats
        stats = get_training_stats(trainer)
        for k, v in log_output.items():
140
            if k in ['loss', 'nll_loss', 'ntokens', 'nsentences', 'sample_size']:
141
                continue  # these are already logged above
142
            if 'loss' in k or k == 'accuracy':
143
144
145
146
                extra_meters[k].update(v, log_output['sample_size'])
            else:
                extra_meters[k].update(v)
            stats[k] = extra_meters[k].avg
Myle Ott's avatar
Myle Ott committed
147
        progress.log(stats, tag='train', step=stats['num_updates'])
148

149
        # ignore the first mini-batch in words-per-second and updates-per-second calculation
150
151
        if i == 0:
            trainer.get_meter('wps').reset()
152
            trainer.get_meter('ups').reset()
153

154
        num_updates = trainer.get_num_updates()
Myle Ott's avatar
Myle Ott committed
155
156
157
158
159
160
        if (
            not args.disable_validation
            and args.save_interval_updates > 0
            and num_updates % args.save_interval_updates == 0
            and num_updates > 0
        ):
161
            valid_losses = validate(args, trainer, task, epoch_itr, valid_subsets)
162
            checkpoint_utils.save_checkpoint(args, trainer, epoch_itr, valid_losses[0])
163
164

        if num_updates >= max_update:
165
166
167
168
169
170
            break

    # log end-of-epoch stats
    stats = get_training_stats(trainer)
    for k, meter in extra_meters.items():
        stats[k] = meter.avg
Myle Ott's avatar
Myle Ott committed
171
    progress.print(stats, tag='train', step=stats['num_updates'])
172

Myle Ott's avatar
Myle Ott committed
173
    # reset training meters
174
175
176
    for k in [
        'train_loss', 'train_nll_loss', 'wps', 'ups', 'wpb', 'bsz', 'gnorm', 'clip',
    ]:
Myle Ott's avatar
Myle Ott committed
177
178
179
180
        meter = trainer.get_meter(k)
        if meter is not None:
            meter.reset()

181
182
183

def get_training_stats(trainer):
    stats = collections.OrderedDict()
Myle Ott's avatar
Myle Ott committed
184
    stats['loss'] = trainer.get_meter('train_loss')
185
    if trainer.get_meter('train_nll_loss').count > 0:
Myle Ott's avatar
Myle Ott committed
186
187
        nll_loss = trainer.get_meter('train_nll_loss')
        stats['nll_loss'] = nll_loss
188
    else:
Myle Ott's avatar
Myle Ott committed
189
        nll_loss = trainer.get_meter('train_loss')
190
    stats['ppl'] = utils.get_perplexity(nll_loss.avg)
Myle Ott's avatar
Myle Ott committed
191
192
193
194
    stats['wps'] = trainer.get_meter('wps')
    stats['ups'] = trainer.get_meter('ups')
    stats['wpb'] = trainer.get_meter('wpb')
    stats['bsz'] = trainer.get_meter('bsz')
195
196
    stats['num_updates'] = trainer.get_num_updates()
    stats['lr'] = trainer.get_lr()
Myle Ott's avatar
Myle Ott committed
197
198
199
    stats['gnorm'] = trainer.get_meter('gnorm')
    stats['clip'] = trainer.get_meter('clip')
    stats['oom'] = trainer.get_meter('oom')
200
    if trainer.get_meter('loss_scale') is not None:
Myle Ott's avatar
Myle Ott committed
201
        stats['loss_scale'] = trainer.get_meter('loss_scale')
202
    stats['wall'] = round(trainer.get_meter('wall').elapsed_time)
Myle Ott's avatar
Myle Ott committed
203
    stats['train_wall'] = trainer.get_meter('train_wall')
204
205
206
    return stats


Myle Ott's avatar
Myle Ott committed
207
def validate(args, trainer, task, epoch_itr, subsets):
208
    """Evaluate the model on the validation set(s) and return the losses."""
209
210
211
212
213

    if args.fixed_validation_seed is not None:
        # set fixed seed for every validation
        utils.set_torch_seed(args.fixed_validation_seed)

214
215
    valid_losses = []
    for subset in subsets:
Myle Ott's avatar
Myle Ott committed
216
        # Initialize data iterator
217
        itr = task.get_batch_iterator(
Myle Ott's avatar
Myle Ott committed
218
            dataset=task.dataset(subset),
219
            max_tokens=args.max_tokens_valid,
220
            max_sentences=args.max_sentences_valid,
221
222
223
224
            max_positions=utils.resolve_max_positions(
                task.max_positions(),
                trainer.get_model().max_positions(),
            ),
Myle Ott's avatar
Myle Ott committed
225
            ignore_invalid_inputs=args.skip_invalid_size_inputs_valid_test,
226
            required_batch_size_multiple=args.required_batch_size_multiple,
Myle Ott's avatar
Myle Ott committed
227
            seed=args.seed,
228
            num_shards=args.distributed_world_size,
Myle Ott's avatar
Myle Ott committed
229
            shard_id=args.distributed_rank,
Myle Ott's avatar
Myle Ott committed
230
            num_workers=args.num_workers,
Myle Ott's avatar
Myle Ott committed
231
        ).next_epoch_itr(shuffle=False)
232
        progress = progress_bar.build_progress_bar(
Myle Ott's avatar
Myle Ott committed
233
            args, itr, epoch_itr.epoch,
234
235
236
237
238
239
240
241
242
243
            prefix='valid on \'{}\' subset'.format(subset),
            no_progress_bar='simple'
        )

        # reset validation loss meters
        for k in ['valid_loss', 'valid_nll_loss']:
            meter = trainer.get_meter(k)
            if meter is not None:
                meter.reset()
        extra_meters = collections.defaultdict(lambda: AverageMeter())
Myle Ott's avatar
Myle Ott committed
244

245
246
247
248
        for sample in progress:
            log_output = trainer.valid_step(sample)

            for k, v in log_output.items():
249
                if k in ['loss', 'nll_loss', 'ntokens', 'nsentences', 'sample_size']:
250
251
                    continue
                extra_meters[k].update(v)
252

253
        # log validation stats
254
        stats = get_valid_stats(trainer, args, extra_meters)
255
256
        for k, meter in extra_meters.items():
            stats[k] = meter.avg
Myle Ott's avatar
Myle Ott committed
257
        progress.print(stats, tag=subset, step=trainer.get_num_updates())
258

259
260
261
262
263
        valid_losses.append(
            stats[args.best_checkpoint_metric].avg
            if args.best_checkpoint_metric == 'loss'
            else stats[args.best_checkpoint_metric]
        )
264
    return valid_losses
265
266


267
def get_valid_stats(trainer, args, extra_meters=None):
268
    stats = collections.OrderedDict()
Myle Ott's avatar
Myle Ott committed
269
    stats['loss'] = trainer.get_meter('valid_loss')
270
    if trainer.get_meter('valid_nll_loss').count > 0:
Myle Ott's avatar
Myle Ott committed
271
272
        nll_loss = trainer.get_meter('valid_nll_loss')
        stats['nll_loss'] = nll_loss
273
    else:
Myle Ott's avatar
Myle Ott committed
274
        nll_loss = stats['loss']
275
    stats['ppl'] = utils.get_perplexity(nll_loss.avg)
Myle Ott's avatar
Nits  
Myle Ott committed
276
    stats['num_updates'] = trainer.get_num_updates()
277
    if hasattr(checkpoint_utils.save_checkpoint, 'best'):
278
        key = 'best_{0}'.format(args.best_checkpoint_metric)
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
        best_function = max if args.maximize_best_checkpoint_metric else min

        current_metric = None
        if args.best_checkpoint_metric == 'loss':
            current_metric = stats['loss'].avg
        elif args.best_checkpoint_metric in extra_meters:
            current_metric = extra_meters[args.best_checkpoint_metric].avg
        elif args.best_checkpoint_metric in stats:
            current_metric = stats[args.best_checkpoint_metric]
        else:
            raise ValueError("best_checkpoint_metric not found in logs")

        stats[key] = best_function(
            checkpoint_utils.save_checkpoint.best,
            current_metric,
        )
295
296
297
    return stats


298
def distributed_main(i, args, start_rank=0):
Myle Ott's avatar
Myle Ott committed
299
300
    args.device_id = i
    if args.distributed_rank is None:  # torch.multiprocessing.spawn
301
302
        args.distributed_rank = start_rank + i
    main(args, init_distributed=True)
Myle Ott's avatar
Myle Ott committed
303
304


Myle Ott's avatar
Myle Ott committed
305
def cli_main():
Myle Ott's avatar
Myle Ott committed
306
307
    parser = options.get_training_parser()
    args = options.parse_args_and_arch(parser)
308

Myle Ott's avatar
Myle Ott committed
309
310
    if args.distributed_init_method is None:
        distributed_utils.infer_init_method(args)
311

Myle Ott's avatar
Myle Ott committed
312
313
    if args.distributed_init_method is not None:
        # distributed training
314
315
316
317
318
319
320
321
322
323
        if torch.cuda.device_count() > 1 and not args.distributed_no_spawn:
            start_rank = args.distributed_rank
            args.distributed_rank = None  # assign automatically
            torch.multiprocessing.spawn(
                fn=distributed_main,
                args=(args, start_rank),
                nprocs=torch.cuda.device_count(),
            )
        else:
            distributed_main(args.device_id, args)
324
    elif args.distributed_world_size > 1:
Myle Ott's avatar
Myle Ott committed
325
        # fallback for single node with multiple GPUs
326
        assert args.distributed_world_size <= torch.cuda.device_count()
327
328
        port = random.randint(10000, 20000)
        args.distributed_init_method = 'tcp://localhost:{port}'.format(port=port)
Myle Ott's avatar
Myle Ott committed
329
        args.distributed_rank = None  # set based on device id
Myle Ott's avatar
Myle Ott committed
330
331
        if max(args.update_freq) > 1 and args.ddp_backend != 'no_c10d':
            print('| NOTE: you may get better performance with: --ddp-backend=no_c10d')
Myle Ott's avatar
Myle Ott committed
332
333
334
335
336
        torch.multiprocessing.spawn(
            fn=distributed_main,
            args=(args, ),
            nprocs=args.distributed_world_size,
        )
337
    else:
Myle Ott's avatar
Myle Ott committed
338
        # single GPU training
339
        main(args)
Myle Ott's avatar
Myle Ott committed
340
341
342
343


if __name__ == '__main__':
    cli_main()