Skip to content

Commit c2cd1a3

Browse files
committed
Improve torch amp support and add channels_last support for train/validate scripts
1 parent 1d34a0a commit c2cd1a3

File tree

3 files changed

+165
-87
lines changed

3 files changed

+165
-87
lines changed

timm/utils.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ def __init__(
4949
checkpoint_dir='',
5050
recovery_dir='',
5151
decreasing=False,
52-
max_history=10):
52+
max_history=10,
53+
save_amp=False):
5354

5455
# state
5556
self.checkpoint_files = [] # (filename, metric) tuples in order of decreasing betterness
@@ -67,13 +68,14 @@ def __init__(
6768
self.decreasing = decreasing # a lower metric is better if True
6869
self.cmp = operator.lt if decreasing else operator.gt # True if lhs better than rhs
6970
self.max_history = max_history
71+
self.save_apex_amp = save_amp # save APEX amp state
7072
assert self.max_history >= 1
7173

72-
def save_checkpoint(self, model, optimizer, args, epoch, model_ema=None, metric=None, use_amp=False):
74+
def save_checkpoint(self, model, optimizer, args, epoch, model_ema=None, metric=None):
7375
assert epoch >= 0
7476
tmp_save_path = os.path.join(self.checkpoint_dir, 'tmp' + self.extension)
7577
last_save_path = os.path.join(self.checkpoint_dir, 'last' + self.extension)
76-
self._save(tmp_save_path, model, optimizer, args, epoch, model_ema, metric, use_amp)
78+
self._save(tmp_save_path, model, optimizer, args, epoch, model_ema, metric)
7779
if os.path.exists(last_save_path):
7880
os.unlink(last_save_path) # required for Windows support.
7981
os.rename(tmp_save_path, last_save_path)
@@ -105,7 +107,7 @@ def save_checkpoint(self, model, optimizer, args, epoch, model_ema=None, metric=
105107

106108
return (None, None) if self.best_metric is None else (self.best_metric, self.best_epoch)
107109

108-
def _save(self, save_path, model, optimizer, args, epoch, model_ema=None, metric=None, use_amp=False):
110+
def _save(self, save_path, model, optimizer, args, epoch, model_ema=None, metric=None):
109111
save_state = {
110112
'epoch': epoch,
111113
'arch': args.model,
@@ -114,7 +116,7 @@ def _save(self, save_path, model, optimizer, args, epoch, model_ema=None, metric
114116
'args': args,
115117
'version': 2, # version < 2 increments epoch before save
116118
}
117-
if use_amp and 'state_dict' in amp.__dict__:
119+
if self.save_apex_amp and 'state_dict' in amp.__dict__:
118120
save_state['amp'] = amp.state_dict()
119121
if model_ema is not None:
120122
save_state['state_dict_ema'] = get_state_dict(model_ema)
@@ -136,11 +138,11 @@ def _cleanup_checkpoints(self, trim=0):
136138
_logger.error("Exception '{}' while deleting checkpoint".format(e))
137139
self.checkpoint_files = self.checkpoint_files[:delete_index]
138140

139-
def save_recovery(self, model, optimizer, args, epoch, model_ema=None, use_amp=False, batch_idx=0):
141+
def save_recovery(self, model, optimizer, args, epoch, model_ema=None, batch_idx=0):
140142
assert epoch >= 0
141143
filename = '-'.join([self.recovery_prefix, str(epoch), str(batch_idx)]) + self.extension
142144
save_path = os.path.join(self.recovery_dir, filename)
143-
self._save(save_path, model, optimizer, args, epoch, model_ema, use_amp=use_amp)
145+
self._save(save_path, model, optimizer, args, epoch, model_ema)
144146
if os.path.exists(self.last_recovery_file):
145147
try:
146148
_logger.debug("Cleaning recovery: {}".format(self.last_recovery_file))

train.py

Lines changed: 110 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,12 @@
1818
import time
1919
import yaml
2020
from datetime import datetime
21+
from contextlib import suppress
2122

22-
try:
23-
from apex import amp
24-
from apex.parallel import DistributedDataParallel as DDP
25-
from apex.parallel import convert_syncbn_model
26-
has_apex = True
27-
except ImportError:
28-
from torch.cuda import amp
29-
from torch.nn.parallel import DistributedDataParallel as DDP
30-
has_apex = False
31-
32-
23+
import torch
24+
import torch.nn as nn
25+
import torchvision.utils
26+
from torch.nn.parallel import DistributedDataParallel as NativeDDP
3327

3428
from timm.data import Dataset, create_loader, resolve_data_config, Mixup, FastCollateMixup, AugMixDataset
3529
from timm.models import create_model, resume_checkpoint, convert_splitbn_model
@@ -38,14 +32,24 @@
3832
from timm.optim import create_optimizer
3933
from timm.scheduler import create_scheduler
4034

41-
import torch
42-
import torch.nn as nn
43-
import torchvision.utils
35+
try:
36+
from apex import amp
37+
from apex.parallel import DistributedDataParallel as ApexDDP
38+
from apex.parallel import convert_syncbn_model
39+
has_apex = True
40+
except ImportError:
41+
has_apex = False
42+
43+
has_native_amp = False
44+
try:
45+
if getattr(torch.cuda.amp, 'autocast') is not None:
46+
has_native_amp = True
47+
except AttributeError:
48+
pass
4449

4550
torch.backends.cudnn.benchmark = True
4651
_logger = logging.getLogger('train')
4752

48-
4953
# The first arg parser parses out only the --config argument, this argument is used to
5054
# load a yaml file containing key-values that override the defaults for the main parser below
5155
config_parser = parser = argparse.ArgumentParser(description='Training Config', add_help=False)
@@ -221,7 +225,13 @@
221225
parser.add_argument('--save-images', action='store_true', default=False,
222226
help='save images of input bathes every log interval for debugging')
223227
parser.add_argument('--amp', action='store_true', default=False,
224-
help='use NVIDIA amp for mixed precision training')
228+
help='use NVIDIA Apex AMP or Native AMP for mixed precision training')
229+
parser.add_argument('--apex-amp', action='store_true', default=False,
230+
help='Use NVIDIA Apex AMP mixed precision')
231+
parser.add_argument('--native-amp', action='store_true', default=False,
232+
help='Use Native Torch AMP mixed precision')
233+
parser.add_argument('--channels-last', action='store_true', default=False,
234+
help='Use channels_last memory layout')
225235
parser.add_argument('--pin-mem', action='store_true', default=False,
226236
help='Pin CPU memory in DataLoader for more efficient (sometimes) transfer to GPU.')
227237
parser.add_argument('--no-prefetcher', action='store_true', default=False,
@@ -254,6 +264,23 @@ def _parse_args():
254264
return args, args_text
255265

256266

267+
class ApexScaler:
268+
def __call__(self, loss, optimizer):
269+
with amp.scale_loss(loss, optimizer) as scaled_loss:
270+
scaled_loss.backward()
271+
optimizer.step()
272+
273+
274+
class NativeScaler:
275+
def __init__(self):
276+
self._scaler = torch.cuda.amp.GradScaler()
277+
278+
def __call__(self, loss, optimizer):
279+
self._scaler.scale(loss).backward()
280+
self._scaler.step(optimizer)
281+
self._scaler.update()
282+
283+
257284
def main():
258285
setup_default_logging()
259286
args, args_text = _parse_args()
@@ -263,7 +290,8 @@ def main():
263290
if 'WORLD_SIZE' in os.environ:
264291
args.distributed = int(os.environ['WORLD_SIZE']) > 1
265292
if args.distributed and args.num_gpu > 1:
266-
_logger.warning('Using more than one GPU per process in distributed mode is not allowed. Setting num_gpu to 1.')
293+
_logger.warning(
294+
'Using more than one GPU per process in distributed mode is not allowed.Setting num_gpu to 1.')
267295
args.num_gpu = 1
268296

269297
args.device = 'cuda:0'
@@ -315,28 +343,50 @@ def main():
315343
assert num_aug_splits > 1 or args.resplit
316344
model = convert_splitbn_model(model, max(num_aug_splits, 2))
317345

346+
use_amp = None
347+
if args.amp:
348+
# for backwards compat, `--amp` arg tries apex before native amp
349+
if has_apex:
350+
args.apex_amp = True
351+
elif has_native_amp:
352+
args.native_amp = True
353+
if args.apex_amp and has_apex:
354+
use_amp = 'apex'
355+
elif args.native_amp and has_native_amp:
356+
use_amp = 'native'
357+
elif args.apex_amp or args.native_amp:
358+
_logger.warning("Neither APEX or native Torch AMP is available, using float32. "
359+
"Install NVIDA apex or upgrade to PyTorch 1.6")
360+
318361
if args.num_gpu > 1:
319-
if args.amp:
362+
if use_amp == 'apex':
320363
_logger.warning(
321-
'AMP does not work well with nn.DataParallel, disabling. Use distributed mode for multi-GPU AMP.')
322-
args.amp = False
364+
'Apex AMP does not work well with nn.DataParallel, disabling. Use DDP or Torch AMP.')
365+
use_amp = None
323366
model = nn.DataParallel(model, device_ids=list(range(args.num_gpu))).cuda()
367+
assert not args.channels_last, "Channels last not supported with DP, use DDP."
324368
else:
325369
model.cuda()
370+
if args.channels_last:
371+
model = model.to(memory_format=torch.channels_last)
326372

327373
optimizer = create_optimizer(args, model)
328374

329-
use_amp = False
330-
if has_apex and args.amp:
375+
amp_autocast = suppress # do nothing
376+
loss_scaler = None
377+
if use_amp == 'apex':
331378
model, optimizer = amp.initialize(model, optimizer, opt_level='O1')
332-
use_amp = True
333-
elif args.amp:
334-
_logger.info('Using torch AMP. Install NVIDIA Apex for Apex AMP.')
335-
scaler = torch.cuda.amp.GradScaler()
336-
use_amp = True
337-
if args.local_rank == 0:
338-
_logger.info('NVIDIA APEX {}. AMP {}.'.format(
339-
'installed' if has_apex else 'not installed', 'on' if use_amp else 'off'))
379+
loss_scaler = ApexScaler()
380+
if args.local_rank == 0:
381+
_logger.info('Using NVIDIA APEX AMP. Training in mixed precision.')
382+
elif use_amp == 'native':
383+
amp_autocast = torch.cuda.amp.autocast
384+
loss_scaler = NativeScaler()
385+
if args.local_rank == 0:
386+
_logger.info('Using native Torch AMP. Training in mixed precision.')
387+
else:
388+
if args.local_rank == 0:
389+
_logger.info('AMP not enabled. Training in float32.')
340390

341391
# optionally resume from a checkpoint
342392
resume_state = {}
@@ -346,7 +396,7 @@ def main():
346396
if resume_state and not args.no_resume_opt:
347397
if 'optimizer' in resume_state:
348398
if args.local_rank == 0:
349-
_logger.info('Restoring Optimizer state from checkpoint')
399+
_logger.info('Restoring optimizer state from checkpoint')
350400
optimizer.load_state_dict(resume_state['optimizer'])
351401
if use_amp and 'amp' in resume_state and 'load_state_dict' in amp.__dict__:
352402
if args.local_rank == 0:
@@ -367,7 +417,8 @@ def main():
367417
if args.sync_bn:
368418
assert not args.split_bn
369419
try:
370-
if has_apex:
420+
if has_apex and use_amp != 'native':
421+
# Apex SyncBN preferred unless native amp is activated
371422
model = convert_syncbn_model(model)
372423
else:
373424
model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model)
@@ -377,12 +428,15 @@ def main():
377428
'zero initialized BN layers (enabled by default for ResNets) while sync-bn enabled.')
378429
except Exception as e:
379430
_logger.error('Failed to enable Synchronized BatchNorm. Install Apex or Torch >= 1.1')
380-
if has_apex:
381-
model = DDP(model, delay_allreduce=True)
431+
if has_apex and use_amp != 'native':
432+
# Apex DDP preferred unless native amp is activated
433+
if args.local_rank == 0:
434+
_logger.info("Using NVIDIA APEX DistributedDataParallel.")
435+
model = ApexDDP(model, delay_allreduce=True)
382436
else:
383437
if args.local_rank == 0:
384-
_logger.info("Using torch DistributedDataParallel. Install NVIDIA Apex for Apex DDP.")
385-
model = DDP(model, device_ids=[args.local_rank]) # can use device str in Torch >= 1.1
438+
_logger.info("Using native Torch DistributedDataParallel.")
439+
model = NativeDDP(model, device_ids=[args.local_rank]) # can use device str in Torch >= 1.1
386440
# NOTE: EMA model does not need to be wrapped by DDP
387441

388442
lr_scheduler, num_epochs = create_scheduler(args, optimizer)
@@ -501,7 +555,7 @@ def main():
501555
])
502556
output_dir = get_outdir(output_base, 'train', exp_name)
503557
decreasing = True if eval_metric == 'loss' else False
504-
saver = CheckpointSaver(checkpoint_dir=output_dir, decreasing=decreasing)
558+
saver = CheckpointSaver(checkpoint_dir=output_dir, decreasing=decreasing, save_amp=use_amp == 'apex')
505559
with open(os.path.join(output_dir, 'args.yaml'), 'w') as f:
506560
f.write(args_text)
507561

@@ -513,22 +567,20 @@ def main():
513567
train_metrics = train_epoch(
514568
epoch, model, loader_train, optimizer, train_loss_fn, args,
515569
lr_scheduler=lr_scheduler, saver=saver, output_dir=output_dir,
516-
use_amp=use_amp, has_apex=has_apex, scaler = scaler,
517-
model_ema=model_ema, mixup_fn=mixup_fn)
570+
amp_autocast=amp_autocast, loss_scaler=loss_scaler, model_ema=model_ema, mixup_fn=mixup_fn)
518571

519572
if args.distributed and args.dist_bn in ('broadcast', 'reduce'):
520573
if args.local_rank == 0:
521574
_logger.info("Distributing BatchNorm running means and vars")
522575
distribute_bn(model, args.world_size, args.dist_bn == 'reduce')
523576

524-
eval_metrics = validate(model, loader_eval, validate_loss_fn, args)
577+
eval_metrics = validate(model, loader_eval, validate_loss_fn, args, amp_autocast=amp_autocast)
525578

526579
if model_ema is not None and not args.model_ema_force_cpu:
527580
if args.distributed and args.dist_bn in ('broadcast', 'reduce'):
528581
distribute_bn(model_ema, args.world_size, args.dist_bn == 'reduce')
529-
530582
ema_eval_metrics = validate(
531-
model_ema.ema, loader_eval, validate_loss_fn, args, log_suffix=' (EMA)')
583+
model_ema.ema, loader_eval, validate_loss_fn, args, amp_autocast=amp_autocast, log_suffix=' (EMA)')
532584
eval_metrics = ema_eval_metrics
533585

534586
if lr_scheduler is not None:
@@ -543,8 +595,7 @@ def main():
543595
# save proper checkpoint with eval metric
544596
save_metric = eval_metrics[eval_metric]
545597
best_metric, best_epoch = saver.save_checkpoint(
546-
model, optimizer, args,
547-
epoch=epoch, model_ema=model_ema, metric=save_metric, use_amp=has_apex&use_amp)
598+
model, optimizer, args, epoch=epoch, model_ema=model_ema, metric=save_metric)
548599

549600
except KeyboardInterrupt:
550601
pass
@@ -554,8 +605,8 @@ def main():
554605

555606
def train_epoch(
556607
epoch, model, loader, optimizer, loss_fn, args,
557-
lr_scheduler=None, saver=None, output_dir='', use_amp=False,
558-
has_apex=False, scaler = None, model_ema=None, mixup_fn=None):
608+
lr_scheduler=None, saver=None, output_dir='', amp_autocast=suppress,
609+
loss_scaler=None, model_ema=None, mixup_fn=None):
559610

560611
if args.mixup_off_epoch and epoch >= args.mixup_off_epoch:
561612
if args.prefetcher and loader.mixup_enabled:
@@ -579,31 +630,21 @@ def train_epoch(
579630
input, target = input.cuda(), target.cuda()
580631
if mixup_fn is not None:
581632
input, target = mixup_fn(input, target)
582-
if not has_apex and use_amp:
583-
with torch.cuda.amp.autocast():
584-
output = model(input)
585-
loss = loss_fn(output, target)
586-
else:
633+
if args.channels_last:
634+
input = input.contiguous(memory_format=torch.channels_last)
635+
636+
with amp_autocast():
587637
output = model(input)
588638
loss = loss_fn(output, target)
589-
639+
590640
if not args.distributed:
591641
losses_m.update(loss.item(), input.size(0))
592642

593643
optimizer.zero_grad()
594-
if use_amp:
595-
if has_apex:
596-
with amp.scale_loss(loss, optimizer) as scaled_loss:
597-
scaled_loss.backward()
598-
else:
599-
scaler.scale(loss).backward()
600-
644+
if loss_scaler is not None:
645+
loss_scaler(loss, optimizer)
601646
else:
602647
loss.backward()
603-
if not has_apex and use_amp:
604-
scaler.step(optimizer)
605-
scaler.update()
606-
else:
607648
optimizer.step()
608649

609650
torch.cuda.synchronize()
@@ -648,8 +689,7 @@ def train_epoch(
648689
if saver is not None and args.recovery_interval and (
649690
last_batch or (batch_idx + 1) % args.recovery_interval == 0):
650691

651-
saver.save_recovery(
652-
model, optimizer, args, epoch, model_ema=model_ema, use_amp=has_apex&use_amp, batch_idx=batch_idx)
692+
saver.save_recovery(model, optimizer, args, epoch, model_ema=model_ema, batch_idx=batch_idx)
653693

654694
if lr_scheduler is not None:
655695
lr_scheduler.step_update(num_updates=num_updates, metric=losses_m.avg)
@@ -663,7 +703,7 @@ def train_epoch(
663703
return OrderedDict([('loss', losses_m.avg)])
664704

665705

666-
def validate(model, loader, loss_fn, args, log_suffix=''):
706+
def validate(model, loader, loss_fn, args, amp_autocast=suppress, log_suffix=''):
667707
batch_time_m = AverageMeter()
668708
losses_m = AverageMeter()
669709
top1_m = AverageMeter()
@@ -679,8 +719,11 @@ def validate(model, loader, loss_fn, args, log_suffix=''):
679719
if not args.prefetcher:
680720
input = input.cuda()
681721
target = target.cuda()
722+
if args.channels_last:
723+
input = input.contiguous(memory_format=torch.channels_last)
682724

683-
output = model(input)
725+
with amp_autocast():
726+
output = model(input)
684727
if isinstance(output, (tuple, list)):
685728
output = output[0]
686729

0 commit comments

Comments
 (0)