train.py 12.3 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

Myle Ott's avatar
Myle Ott committed
22

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

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

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

37
38
39
    if distributed_utils.is_master(args):
        checkpoint_utils.verify_checkpoint_directory(args.save_dir)

40
41
    # Print args
    print(args)
42

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

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

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

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

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

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

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

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

95
96
97
        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)
98
99
100
101
    train_meter.stop()
    print('| done training in {:.1f} seconds'.format(train_meter.sum))


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

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

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

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

138
        # ignore the first mini-batch in words-per-second and updates-per-second calculation
139
140
        if i == 0:
            trainer.get_meter('wps').reset()
141
            trainer.get_meter('ups').reset()
142

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

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

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

170
171
172

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


Myle Ott's avatar
Myle Ott committed
196
def validate(args, trainer, task, epoch_itr, subsets):
197
    """Evaluate the model on the validation set(s) and return the losses."""
198
199
200
201
202

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

203
204
    valid_losses = []
    for subset in subsets:
Myle Ott's avatar
Myle Ott committed
205
        # Initialize data iterator
206
        itr = task.get_batch_iterator(
Myle Ott's avatar
Myle Ott committed
207
            dataset=task.dataset(subset),
208
            max_tokens=args.max_tokens_valid,
209
            max_sentences=args.max_sentences_valid,
210
211
212
213
            max_positions=utils.resolve_max_positions(
                task.max_positions(),
                trainer.get_model().max_positions(),
            ),
Myle Ott's avatar
Myle Ott committed
214
            ignore_invalid_inputs=args.skip_invalid_size_inputs_valid_test,
215
            required_batch_size_multiple=args.required_batch_size_multiple,
Myle Ott's avatar
Myle Ott committed
216
            seed=args.seed,
217
            num_shards=args.distributed_world_size,
Myle Ott's avatar
Myle Ott committed
218
            shard_id=args.distributed_rank,
Myle Ott's avatar
Myle Ott committed
219
            num_workers=args.num_workers,
Myle Ott's avatar
Myle Ott committed
220
        ).next_epoch_itr(shuffle=False)
221
        progress = progress_bar.build_progress_bar(
Myle Ott's avatar
Myle Ott committed
222
            args, itr, epoch_itr.epoch,
223
224
225
226
227
228
229
230
231
232
            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
233

234
235
236
237
        for sample in progress:
            log_output = trainer.valid_step(sample)

            for k, v in log_output.items():
238
                if k in ['loss', 'nll_loss', 'ntokens', 'nsentences', 'sample_size']:
239
240
                    continue
                extra_meters[k].update(v)
241

242
        # log validation stats
243
        stats = get_valid_stats(trainer, args, extra_meters)
244
245
        for k, meter in extra_meters.items():
            stats[k] = meter.avg
Myle Ott's avatar
Myle Ott committed
246
        progress.print(stats, tag=subset, step=trainer.get_num_updates())
247

248
249
250
251
252
        valid_losses.append(
            stats[args.best_checkpoint_metric].avg
            if args.best_checkpoint_metric == 'loss'
            else stats[args.best_checkpoint_metric]
        )
253
    return valid_losses
254
255


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


287
def distributed_main(i, args, start_rank=0):
Myle Ott's avatar
Myle Ott committed
288
289
    args.device_id = i
    if args.distributed_rank is None:  # torch.multiprocessing.spawn
290
291
        args.distributed_rank = start_rank + i
    main(args, init_distributed=True)
Myle Ott's avatar
Myle Ott committed
292
293


Myle Ott's avatar
Myle Ott committed
294
def cli_main():
Myle Ott's avatar
Myle Ott committed
295
296
    parser = options.get_training_parser()
    args = options.parse_args_and_arch(parser)
297

Myle Ott's avatar
Myle Ott committed
298
299
    if args.distributed_init_method is None:
        distributed_utils.infer_init_method(args)
300

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


if __name__ == '__main__':
    cli_main()