train.py 11.3 KB
Newer Older
Myle Ott's avatar
Myle Ott committed
1
#!/usr/bin/env python3 -u
Sergey Edunov's avatar
Sergey Edunov committed
2
3
4
5
6
7
# Copyright (c) 2017-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the license found in the LICENSE file in
# the root directory of this source tree. An additional grant of patent rights
# can be found in the PATENTS file in the same directory.
Myle Ott's avatar
Myle Ott committed
8
9
10
"""
Train a new model on one or across multiple GPUs.
"""
Sergey Edunov's avatar
Sergey Edunov committed
11

12
13
import collections
import math
Myle Ott's avatar
Myle Ott committed
14
import os
15
16
import random

17
import torch
Sergey Edunov's avatar
Sergey Edunov committed
18

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

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
    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'
30

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

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

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

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

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

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


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

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

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

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

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

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

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

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

168
169
170

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


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

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

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

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

241
        valid_losses.append(stats[args.best_checkpoint_metric].avg)
242
    return valid_losses
243
244
245
246


def get_valid_stats(trainer):
    stats = collections.OrderedDict()
Myle Ott's avatar
Myle Ott committed
247
    stats['loss'] = trainer.get_meter('valid_loss')
248
    if trainer.get_meter('valid_nll_loss').count > 0:
Myle Ott's avatar
Myle Ott committed
249
250
        nll_loss = trainer.get_meter('valid_nll_loss')
        stats['nll_loss'] = nll_loss
251
    else:
Myle Ott's avatar
Myle Ott committed
252
        nll_loss = stats['loss']
253
    stats['ppl'] = utils.get_perplexity(nll_loss.avg)
Myle Ott's avatar
Nits  
Myle Ott committed
254
    stats['num_updates'] = trainer.get_num_updates()
255
256
257
    if hasattr(checkpoint_utils.save_checkpoint, 'best'):
        stats['best_loss'] = min(
            checkpoint_utils.save_checkpoint.best, stats['loss'].avg)
258
259
260
    return stats


261
def distributed_main(i, args, start_rank=0):
Myle Ott's avatar
Myle Ott committed
262
263
    args.device_id = i
    if args.distributed_rank is None:  # torch.multiprocessing.spawn
264
265
        args.distributed_rank = start_rank + i
    main(args, init_distributed=True)
Myle Ott's avatar
Myle Ott committed
266
267


Myle Ott's avatar
Myle Ott committed
268
def cli_main():
Myle Ott's avatar
Myle Ott committed
269
270
    parser = options.get_training_parser()
    args = options.parse_args_and_arch(parser)
271

Myle Ott's avatar
Myle Ott committed
272
273
    if args.distributed_init_method is None:
        distributed_utils.infer_init_method(args)
274

Myle Ott's avatar
Myle Ott committed
275
276
    if args.distributed_init_method is not None:
        # distributed training
277
278
279
280
281
282
283
284
285
286
        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)
287
    elif args.distributed_world_size > 1:
Myle Ott's avatar
Myle Ott committed
288
        # fallback for single node with multiple GPUs
289
        assert args.distributed_world_size <= torch.cuda.device_count()
290
291
        port = random.randint(10000, 20000)
        args.distributed_init_method = 'tcp://localhost:{port}'.format(port=port)
Myle Ott's avatar
Myle Ott committed
292
        args.distributed_rank = None  # set based on device id
Myle Ott's avatar
Myle Ott committed
293
294
        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
295
296
297
298
299
        torch.multiprocessing.spawn(
            fn=distributed_main,
            args=(args, ),
            nprocs=args.distributed_world_size,
        )
300
    else:
Myle Ott's avatar
Myle Ott committed
301
        # single GPU training
302
        main(args)
Myle Ott's avatar
Myle Ott committed
303
304
305
306


if __name__ == '__main__':
    cli_main()