train.py 12 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 torch
Sergey Edunov's avatar
Sergey Edunov committed
15

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

Myle Ott's avatar
Myle Ott committed
21

22
def main(args, init_distributed=False):
Myle Ott's avatar
Myle Ott committed
23
    utils.import_user_module(args)
24

25
26
    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'
27

28
    # Initialize CUDA and distributed training
Myle Ott's avatar
Myle Ott committed
29
30
    if torch.cuda.is_available() and not args.cpu:
        torch.cuda.set_device(args.device_id)
31
    torch.manual_seed(args.seed)
32
33
34
    if init_distributed:
        args.distributed_rank = distributed_utils.distributed_init(args)

35
36
37
    if distributed_utils.is_master(args):
        checkpoint_utils.verify_checkpoint_directory(args.save_dir)

38
39
    # Print args
    print(args)
40

Myle Ott's avatar
Myle Ott committed
41
42
    # Setup task, e.g., translation, language modeling, etc.
    task = tasks.setup_task(args)
43

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

Myle Ott's avatar
Myle Ott committed
48
49
50
    # Build model and criterion
    model = task.build_model(args)
    criterion = task.build_criterion(args)
51
    print(model)
52
    print('| model {}, criterion {}'.format(args.arch, criterion.__class__.__name__))
53
54
55
56
    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),
    ))
57
58

    # Build trainer
Myle Ott's avatar
Myle Ott committed
59
    trainer = Trainer(args, task, model, criterion)
60
61
62
63
64
65
    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
66
67
68
    # Load the latest checkpoint if one is available and restore the
    # corresponding train iterator
    extra_state, epoch_itr = checkpoint_utils.load_checkpoint(args, trainer)
69
70
71
72
73
74
75

    # 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()
76
    valid_subsets = args.valid_subset.split(',')
Myle Ott's avatar
Myle Ott committed
77
    while lr > args.min_lr and epoch_itr.epoch < max_epoch and trainer.get_num_updates() < max_update:
78
        # train for one epoch
Myle Ott's avatar
Myle Ott committed
79
        train(args, trainer, task, epoch_itr)
80

Myle Ott's avatar
Myle Ott committed
81
        if not args.disable_validation and epoch_itr.epoch % args.validate_interval == 0:
Myle Ott's avatar
Myle Ott committed
82
            valid_losses = validate(args, trainer, task, epoch_itr, valid_subsets)
Myle Ott's avatar
Myle Ott committed
83
84
        else:
            valid_losses = [None]
85
86

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

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

93
        if ':' in getattr(args, 'data', ''):
Myle Ott's avatar
Myle Ott committed
94
95
            # sharded data: get train iterator for next epoch
            epoch_itr = trainer.get_train_iterator(epoch_itr.epoch)
96
97
98
99
    train_meter.stop()
    print('| done training in {:.1f} seconds'.format(train_meter.sum))


Myle Ott's avatar
Myle Ott committed
100
def train(args, trainer, task, epoch_itr):
101
    """Train the model for one epoch."""
102
    # Update parameters every N batches
Myle Ott's avatar
Myle Ott committed
103
    update_freq = args.update_freq[epoch_itr.epoch - 1] \
Myle Ott's avatar
Myle Ott committed
104
        if epoch_itr.epoch <= len(args.update_freq) else args.update_freq[-1]
Myle Ott's avatar
Myle Ott committed
105
106
107
108
109
110

    # Initialize data iterator
    itr = epoch_itr.next_epoch_itr(
        fix_batches_to_gpus=args.fix_batches_to_gpus,
        shuffle=(epoch_itr.epoch >= args.curriculum),
    )
111
112
113
114
115
    itr = iterators.GroupedIterator(itr, update_freq)
    progress = progress_bar.build_progress_bar(
        args, itr, epoch_itr.epoch, no_progress_bar='simple',
    )

116
    extra_meters = collections.defaultdict(lambda: AverageMeter())
117
    valid_subsets = args.valid_subset.split(',')
118
    max_update = args.max_update or math.inf
119
120
121
    for i, samples in enumerate(progress, start=epoch_itr.iterations_in_epoch):
        log_output = trainer.train_step(samples)
        if log_output is None:
122
123
124
125
126
            continue

        # log mid-epoch stats
        stats = get_training_stats(trainer)
        for k, v in log_output.items():
127
            if k in ['loss', 'nll_loss', 'ntokens', 'nsentences', 'sample_size']:
128
                continue  # these are already logged above
129
            if 'loss' in k or k == 'accuracy':
130
131
132
133
                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
134
        progress.log(stats, tag='train', step=stats['num_updates'])
135
136
137
138
139

        # ignore the first mini-batch in words-per-second calculation
        if i == 0:
            trainer.get_meter('wps').reset()

140
        num_updates = trainer.get_num_updates()
Myle Ott's avatar
Myle Ott committed
141
142
143
144
145
146
        if (
            not args.disable_validation
            and args.save_interval_updates > 0
            and num_updates % args.save_interval_updates == 0
            and num_updates > 0
        ):
147
            valid_losses = validate(args, trainer, task, epoch_itr, valid_subsets)
148
            checkpoint_utils.save_checkpoint(args, trainer, epoch_itr, valid_losses[0])
149
150

        if num_updates >= max_update:
151
152
153
154
155
156
            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
157
    progress.print(stats, tag='train', step=stats['num_updates'])
158

Myle Ott's avatar
Myle Ott committed
159
    # reset training meters
160
161
162
    for k in [
        'train_loss', 'train_nll_loss', 'wps', 'ups', 'wpb', 'bsz', 'gnorm', 'clip',
    ]:
Myle Ott's avatar
Myle Ott committed
163
164
165
166
        meter = trainer.get_meter(k)
        if meter is not None:
            meter.reset()

167
168
169

def get_training_stats(trainer):
    stats = collections.OrderedDict()
Myle Ott's avatar
Myle Ott committed
170
    stats['loss'] = trainer.get_meter('train_loss')
171
    if trainer.get_meter('train_nll_loss').count > 0:
Myle Ott's avatar
Myle Ott committed
172
173
        nll_loss = trainer.get_meter('train_nll_loss')
        stats['nll_loss'] = nll_loss
174
    else:
Myle Ott's avatar
Myle Ott committed
175
        nll_loss = trainer.get_meter('train_loss')
176
    stats['ppl'] = utils.get_perplexity(nll_loss.avg)
Myle Ott's avatar
Myle Ott committed
177
178
179
180
    stats['wps'] = trainer.get_meter('wps')
    stats['ups'] = trainer.get_meter('ups')
    stats['wpb'] = trainer.get_meter('wpb')
    stats['bsz'] = trainer.get_meter('bsz')
181
182
    stats['num_updates'] = trainer.get_num_updates()
    stats['lr'] = trainer.get_lr()
Myle Ott's avatar
Myle Ott committed
183
184
185
    stats['gnorm'] = trainer.get_meter('gnorm')
    stats['clip'] = trainer.get_meter('clip')
    stats['oom'] = trainer.get_meter('oom')
186
    if trainer.get_meter('loss_scale') is not None:
Myle Ott's avatar
Myle Ott committed
187
        stats['loss_scale'] = trainer.get_meter('loss_scale')
188
    stats['wall'] = round(trainer.get_meter('wall').elapsed_time)
Myle Ott's avatar
Myle Ott committed
189
    stats['train_wall'] = trainer.get_meter('train_wall')
190
191
192
    return stats


Myle Ott's avatar
Myle Ott committed
193
def validate(args, trainer, task, epoch_itr, subsets):
194
195
196
    """Evaluate the model on the validation set(s) and return the losses."""
    valid_losses = []
    for subset in subsets:
Myle Ott's avatar
Myle Ott committed
197
        # Initialize data iterator
198
        itr = task.get_batch_iterator(
Myle Ott's avatar
Myle Ott committed
199
            dataset=task.dataset(subset),
200
            max_tokens=args.max_tokens_valid,
201
            max_sentences=args.max_sentences_valid,
202
203
204
205
            max_positions=utils.resolve_max_positions(
                task.max_positions(),
                trainer.get_model().max_positions(),
            ),
Myle Ott's avatar
Myle Ott committed
206
            ignore_invalid_inputs=args.skip_invalid_size_inputs_valid_test,
207
            required_batch_size_multiple=args.required_batch_size_multiple,
Myle Ott's avatar
Myle Ott committed
208
            seed=args.seed,
209
            num_shards=args.distributed_world_size,
Myle Ott's avatar
Myle Ott committed
210
            shard_id=args.distributed_rank,
Myle Ott's avatar
Myle Ott committed
211
            num_workers=args.num_workers,
Myle Ott's avatar
Myle Ott committed
212
        ).next_epoch_itr(shuffle=False)
213
        progress = progress_bar.build_progress_bar(
Myle Ott's avatar
Myle Ott committed
214
            args, itr, epoch_itr.epoch,
215
216
217
218
219
220
221
222
223
224
            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
225

226
227
228
229
        for sample in progress:
            log_output = trainer.valid_step(sample)

            for k, v in log_output.items():
230
                if k in ['loss', 'nll_loss', 'ntokens', 'nsentences', 'sample_size']:
231
232
                    continue
                extra_meters[k].update(v)
233

234
        # log validation stats
235
        stats = get_valid_stats(trainer, args, extra_meters)
236
237
        for k, meter in extra_meters.items():
            stats[k] = meter.avg
Myle Ott's avatar
Myle Ott committed
238
        progress.print(stats, tag=subset, step=trainer.get_num_updates())
239

240
241
242
243
244
        valid_losses.append(
            stats[args.best_checkpoint_metric].avg
            if args.best_checkpoint_metric == 'loss'
            else stats[args.best_checkpoint_metric]
        )
245
    return valid_losses
246
247


248
def get_valid_stats(trainer, args, extra_meters=None):
249
    stats = collections.OrderedDict()
Myle Ott's avatar
Myle Ott committed
250
    stats['loss'] = trainer.get_meter('valid_loss')
251
    if trainer.get_meter('valid_nll_loss').count > 0:
Myle Ott's avatar
Myle Ott committed
252
253
        nll_loss = trainer.get_meter('valid_nll_loss')
        stats['nll_loss'] = nll_loss
254
    else:
Myle Ott's avatar
Myle Ott committed
255
        nll_loss = stats['loss']
256
    stats['ppl'] = utils.get_perplexity(nll_loss.avg)
Myle Ott's avatar
Nits  
Myle Ott committed
257
    stats['num_updates'] = trainer.get_num_updates()
258
    if hasattr(checkpoint_utils.save_checkpoint, 'best'):
259
        key = 'best_{0}'.format(args.best_checkpoint_metric)
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
        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,
        )
276
277
278
    return stats


279
def distributed_main(i, args, start_rank=0):
Myle Ott's avatar
Myle Ott committed
280
281
    args.device_id = i
    if args.distributed_rank is None:  # torch.multiprocessing.spawn
282
283
        args.distributed_rank = start_rank + i
    main(args, init_distributed=True)
Myle Ott's avatar
Myle Ott committed
284
285


Myle Ott's avatar
Myle Ott committed
286
def cli_main():
Myle Ott's avatar
Myle Ott committed
287
288
    parser = options.get_training_parser()
    args = options.parse_args_and_arch(parser)
289

Myle Ott's avatar
Myle Ott committed
290
291
    if args.distributed_init_method is None:
        distributed_utils.infer_init_method(args)
292

Myle Ott's avatar
Myle Ott committed
293
294
    if args.distributed_init_method is not None:
        # distributed training
295
296
297
298
299
300
301
302
303
304
        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)
305
    elif args.distributed_world_size > 1:
Myle Ott's avatar
Myle Ott committed
306
        # fallback for single node with multiple GPUs
307
        assert args.distributed_world_size <= torch.cuda.device_count()
308
309
        port = random.randint(10000, 20000)
        args.distributed_init_method = 'tcp://localhost:{port}'.format(port=port)
Myle Ott's avatar
Myle Ott committed
310
        args.distributed_rank = None  # set based on device id
Myle Ott's avatar
Myle Ott committed
311
312
        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
313
314
315
316
317
        torch.multiprocessing.spawn(
            fn=distributed_main,
            args=(args, ),
            nprocs=args.distributed_world_size,
        )
318
    else:
Myle Ott's avatar
Myle Ott committed
319
        # single GPU training
320
        main(args)
Myle Ott's avatar
Myle Ott committed
321
322
323
324


if __name__ == '__main__':
    cli_main()