Initial commit

This commit is contained in:
王新升
2026-02-06 20:31:14 +08:00
parent a0b51be095
commit c589bcb837
145 changed files with 28773 additions and 0 deletions
@@ -0,0 +1,235 @@
import logging
import os
import random
import subprocess
import sys
from datetime import datetime
import numpy as np
import torch.utils.data
from torch import nn
from torch.utils.tensorboard import SummaryWriter
from .dataset_utils import data_loader
from .hparams import hparams
from .meters import AvgrageMeter
from .tensor_utils import tensors_to_scalars
from .trainer import Trainer
torch.multiprocessing.set_sharing_strategy(os.getenv('TORCH_SHARE_STRATEGY', 'file_system'))
log_format = '%(asctime)s %(message)s'
logging.basicConfig(stream=sys.stdout, level=logging.INFO,
format=log_format, datefmt='%m/%d %I:%M:%S %p')
class BaseTask(nn.Module):
def __init__(self, *args, **kwargs):
super(BaseTask, self).__init__()
self.current_epoch = 0
self.global_step = 0
self.trainer = None
self.use_ddp = False
self.gradient_clip_norm = hparams['clip_grad_norm']
self.gradient_clip_val = hparams.get('clip_grad_value', 0)
self.model = None
self.training_losses_meter = None
self.logger: SummaryWriter = None
######################
# build model, dataloaders, optimizer, scheduler and tensorboard
######################
def build_model(self):
raise NotImplementedError
@data_loader
def train_dataloader(self):
raise NotImplementedError
@data_loader
def test_dataloader(self):
raise NotImplementedError
@data_loader
def val_dataloader(self):
raise NotImplementedError
def build_scheduler(self, optimizer):
return None
def build_optimizer(self, model):
raise NotImplementedError
def configure_optimizers(self):
optm = self.build_optimizer(self.model)
self.scheduler = self.build_scheduler(optm)
if isinstance(optm, (list, tuple)):
return optm
return [optm]
def build_tensorboard(self, save_dir, name, **kwargs):
log_dir = os.path.join(save_dir, name)
os.makedirs(log_dir, exist_ok=True)
self.logger = SummaryWriter(log_dir=log_dir, **kwargs)
######################
# training
######################
def on_train_start(self):
pass
def on_train_end(self):
pass
def on_epoch_start(self):
self.training_losses_meter = {'total_loss': AvgrageMeter()}
def on_epoch_end(self):
loss_outputs = {k: round(v.avg, 4) for k, v in self.training_losses_meter.items()}
print(f"Epoch {self.current_epoch} ended. Steps: {self.global_step}. {loss_outputs}")
def _training_step(self, sample, batch_idx, optimizer_idx):
"""
:param sample:
:param batch_idx:
:return: total loss: torch.Tensor, loss_log: dict
"""
raise NotImplementedError
def training_step(self, sample, batch_idx, optimizer_idx=-1):
"""
:param sample:
:param batch_idx:
:param optimizer_idx:
:return: {'loss': torch.Tensor, 'progress_bar': dict, 'tb_log': dict}
"""
loss_ret = self._training_step(sample, batch_idx, optimizer_idx)
if loss_ret is None:
return {'loss': None}
total_loss, log_outputs = loss_ret
log_outputs = tensors_to_scalars(log_outputs)
for k, v in log_outputs.items():
if k not in self.training_losses_meter:
self.training_losses_meter[k] = AvgrageMeter()
if not np.isnan(v):
self.training_losses_meter[k].update(v)
self.training_losses_meter['total_loss'].update(total_loss.item())
if optimizer_idx >= 0:
log_outputs[f'lr_{optimizer_idx}'] = self.trainer.optimizers[optimizer_idx].param_groups[0]['lr']
progress_bar_log = log_outputs
tb_log = {f'tr/{k}': v for k, v in log_outputs.items()}
return {
'loss': total_loss,
'progress_bar': progress_bar_log,
'tb_log': tb_log
}
def on_before_optimization(self, opt_idx):
if self.gradient_clip_norm > 0:
torch.nn.utils.clip_grad_norm_(self.parameters(), self.gradient_clip_norm)
if self.gradient_clip_val > 0:
torch.nn.utils.clip_grad_value_(self.parameters(), self.gradient_clip_val)
def on_after_optimization(self, epoch, batch_idx, optimizer, optimizer_idx):
if self.scheduler is not None:
# self.scheduler.step(self.global_step // hparams['accumulate_grad_batches'])
# the code above causes EPOCH_DEPRECATION_WARNING, changed it and changed the optimizer init with
# step_size divided by accumulate_grad_batches
self.scheduler.step()
######################
# validation
######################
def validation_start(self):
pass
def validation_step(self, sample, batch_idx):
"""
:param sample:
:param batch_idx:
:return: output: {"losses": {...}, "total_loss": float, ...} or (total loss: torch.Tensor, loss_log: dict)
"""
raise NotImplementedError
def validation_end(self, outputs):
"""
:param outputs:
:return: loss_output: dict
"""
all_losses_meter = {'total_loss': AvgrageMeter()}
for output in outputs:
if len(output) == 0 or output is None:
continue
if isinstance(output, dict):
assert 'losses' in output, 'Key "losses" should exist in validation output.'
n = output.pop('nsamples', 1)
losses = tensors_to_scalars(output['losses'])
total_loss = output.get('total_loss', sum(losses.values()))
else:
assert len(output) == 2, 'Validation output should only consist of two elements: (total_loss, losses)'
n = 1
total_loss, losses = output
losses = tensors_to_scalars(losses)
if isinstance(total_loss, torch.Tensor):
total_loss = total_loss.item()
for k, v in losses.items():
if k not in all_losses_meter:
all_losses_meter[k] = AvgrageMeter()
all_losses_meter[k].update(v, n)
all_losses_meter['total_loss'].update(total_loss, n)
loss_output = {k: round(v.avg, 4) for k, v in all_losses_meter.items()}
print(f"| Validation results@{self.global_step}: {loss_output}")
return {
'tb_log': {f'val/{k}': v for k, v in loss_output.items()},
'val_loss': loss_output['total_loss']
}
######################
# testing
######################
def test_start(self):
pass
def test_step(self, sample, batch_idx):
return self.validation_step(sample, batch_idx)
def test_end(self, outputs):
return self.validation_end(outputs)
######################
# start training/testing
######################
@classmethod
def start(cls):
os.environ['MASTER_PORT'] = str(random.randint(15000, 30000))
random.seed(hparams['seed'])
np.random.seed(hparams['seed'])
work_dir = hparams['work_dir']
trainer = Trainer(
work_dir=work_dir,
val_check_interval=hparams['val_check_interval'],
tb_log_interval=hparams['tb_log_interval'],
max_updates=hparams['max_updates'],
num_sanity_val_steps=hparams['num_sanity_val_steps'] if not hparams['validate'] else 10000,
accumulate_grad_batches=hparams['accumulate_grad_batches'],
print_nan_grads=hparams['print_nan_grads'],
resume_from_checkpoint=hparams.get('resume_from_checkpoint', 0),
amp=hparams['amp'],
monitor_key=hparams['valid_monitor_key'],
monitor_mode=hparams['valid_monitor_mode'],
num_ckpt_keep=hparams['num_ckpt_keep'],
save_best=hparams['save_best'],
seed=hparams['seed'],
debug=hparams['debug']
)
if not hparams['infer']: # train
trainer.fit(cls)
else:
trainer.test(cls)
def on_keyboard_interrupt(self):
pass
@@ -0,0 +1,68 @@
import glob
import os
import re
import torch
def get_last_checkpoint(work_dir, steps=None):
checkpoint = None
last_ckpt_path = None
ckpt_paths = get_all_ckpts(work_dir, steps)
if len(ckpt_paths) > 0:
last_ckpt_path = ckpt_paths[0]
checkpoint = torch.load(last_ckpt_path, map_location='cpu')
return checkpoint, last_ckpt_path
def get_all_ckpts(work_dir, steps=None):
if steps is None:
ckpt_path_pattern = f'{work_dir}/model_ckpt_steps_*.ckpt'
else:
ckpt_path_pattern = f'{work_dir}/model_ckpt_steps_{steps}.ckpt'
return sorted(glob.glob(ckpt_path_pattern),
key=lambda x: -int(re.findall('.*steps\_(\d+)\.ckpt', x)[0]))
def load_ckpt(cur_model, ckpt_base_dir, model_name='model', force=True, strict=True, verbose=True):
if os.path.isfile(ckpt_base_dir):
base_dir = os.path.dirname(ckpt_base_dir)
ckpt_path = ckpt_base_dir
checkpoint = torch.load(ckpt_base_dir, map_location='cpu')
else:
base_dir = ckpt_base_dir
checkpoint, ckpt_path = get_last_checkpoint(ckpt_base_dir)
if checkpoint is not None:
state_dict = checkpoint["state_dict"]
if len([k for k in state_dict.keys() if '.' in k]) > 0:
state_dict = {k[len(model_name) + 1:]: v for k, v in state_dict.items()
if k.startswith(f'{model_name}.')}
else:
if '.' not in model_name:
state_dict = state_dict[model_name]
else:
base_model_name = model_name.split('.')[0]
rest_model_name = model_name[len(base_model_name) + 1:]
state_dict = {
k[len(rest_model_name) + 1:]: v for k, v in state_dict[base_model_name].items()
if k.startswith(f'{rest_model_name}.')}
if not strict:
cur_model_state_dict = cur_model.state_dict()
unmatched_keys = []
for key, param in state_dict.items():
if key in cur_model_state_dict:
new_param = cur_model_state_dict[key]
if new_param.shape != param.shape:
unmatched_keys.append(key)
print("| Unmatched keys: ", key, new_param.shape, param.shape)
for key in unmatched_keys:
del state_dict[key]
# print(state_dict)
cur_model.load_state_dict(state_dict, strict=strict)
if verbose:
print(f"| load '{model_name}' from '{ckpt_path}'.")
else:
e_msg = f"| ckpt not found in {base_dir}."
if force:
assert False, e_msg
else:
print(e_msg)
@@ -0,0 +1,372 @@
import os
import sys
import traceback
import types
from functools import wraps
from itertools import chain
import numpy as np
import torch.utils.data
import torch.nn.functional as F
from torch.utils.data import ConcatDataset
from .hparams import hparams
def collate_1d_or_2d(values, pad_idx=0, left_pad=False, shift_right=False, max_len=None, shift_id=1):
if len(values[0].shape) == 1:
return collate_1d(values, pad_idx, left_pad, shift_right, max_len, shift_id)
else:
return collate_2d(values, pad_idx, left_pad, shift_right, max_len)
def collate_1d(values, pad_idx=0, left_pad=False, shift_right=False, max_len=None, shift_id=1):
"""Convert a list of 1d tensors into a padded 2d tensor."""
size = max(v.size(0) for v in values) if max_len is None else max_len
res = values[0].new(len(values), size).fill_(pad_idx)
def copy_tensor(src, dst):
assert dst.numel() == src.numel()
if shift_right:
dst[1:] = src[:-1]
dst[0] = shift_id
else:
dst.copy_(src)
for i, v in enumerate(values):
copy_tensor(v, res[i][size - len(v):] if left_pad else res[i][:len(v)])
return res
def collate_2d(values, pad_idx=0, left_pad=False, shift_right=False, max_len=None):
"""Convert a list of 2d tensors into a padded 3d tensor."""
size = max(v.size(0) for v in values) if max_len is None else max_len
res = values[0].new(len(values), size, values[0].shape[1]).fill_(pad_idx)
def copy_tensor(src, dst):
assert dst.numel() == src.numel()
if shift_right:
dst[1:] = src[:-1]
else:
dst.copy_(src)
for i, v in enumerate(values):
copy_tensor(v, res[i][size - len(v):] if left_pad else res[i][:len(v)])
return res
def collate_xd(values, pad_value=0, max_len=None):
size = ((max(v.size(0) for v in values) if max_len is None else max_len), *values[0].shape[1:])
res = torch.full((len(values), *size), fill_value=pad_value, dtype=values[0].dtype, device=values[0].device)
for i, v in enumerate(values):
res[i, :len(v), ...] = v
return res
def pad_or_cut_1d(values: torch.tensor, tgt_len, pad_value=0):
src_len = values.shape[0]
if src_len < tgt_len:
res = F.pad(values, [0, tgt_len - src_len], value=pad_value)
else:
res = values[:tgt_len]
return res
def pad_or_cut_2d(values: torch.tensor, tgt_len, dim=-1, pad_value=0):
if dim == 0 or dim == -2:
src_len = values.shape[0]
if src_len < tgt_len:
res = F.pad(values, [0, 0, 0, tgt_len - src_len], value=pad_value)
else:
res = values[:tgt_len]
elif dim == 1 or dim == -1:
src_len = values.shape[1]
if src_len < tgt_len:
res = F.pad(values, [0, tgt_len - src_len], value=pad_value)
else:
res = values[:, :tgt_len]
else:
raise RuntimeError(f"Wrong dim number {dim} while the tensor only has {len(values.shape)} dimensions.")
return res
def pad_or_cut_3d(values: torch.tensor, tgt_len, dim=-1, pad_value=0):
if dim == 0 or dim == -3:
src_len = values.shape[0]
if src_len < tgt_len:
res = F.pad(values, [0, 0, 0, 0, 0, tgt_len - src_len], value=pad_value)
else:
res = values[:tgt_len]
elif dim == 1 or dim == -2:
src_len = values.shape[1]
if src_len < tgt_len:
res = F.pad(values, [0, 0, 0, tgt_len - src_len], value=pad_value)
else:
res = values[:, :tgt_len]
elif dim == 2 or dim == -1:
src_len = values.shape[2]
if src_len < tgt_len:
res = F.pad(values, [0, tgt_len - src_len], value=pad_value)
else:
res = values[:, :, :tgt_len]
else:
raise RuntimeError(f"Wrong dim number {dim} while the tensor only has {len(values.shape)} dimensions.")
return res
def pad_or_cut_xd(values, tgt_len, dim=-1, pad_value=0):
if len(values.shape) == 1:
return pad_or_cut_1d(values, tgt_len, pad_value)
elif len(values.shape) == 2:
return pad_or_cut_2d(values, tgt_len, dim, pad_value)
elif len(values.shape) == 3:
return pad_or_cut_3d(values, tgt_len, dim, pad_value)
else:
raise NotImplementedError
def _is_batch_full(batch, num_tokens, max_tokens, max_sentences):
if len(batch) == 0:
return 0
if len(batch) == max_sentences:
return 1
if num_tokens > max_tokens:
return 1
return 0
def batch_by_size(
indices, num_tokens_fn, max_tokens=None, max_sentences=None,
required_batch_size_multiple=1, distributed=False
):
"""
Yield mini-batches of indices bucketed by size. Batches may contain
sequences of different lengths.
Args:
indices (List[int]): ordered list of dataset indices
num_tokens_fn (callable): function that returns the number of tokens at
a given index
max_tokens (int, optional): max number of tokens in each batch
(default: None).
max_sentences (int, optional): max number of sentences in each
batch (default: None).
required_batch_size_multiple (int, optional): require batch size to
be a multiple of N (default: 1).
"""
max_tokens = max_tokens if max_tokens is not None else sys.maxsize
max_sentences = max_sentences if max_sentences is not None else sys.maxsize
bsz_mult = required_batch_size_multiple
if isinstance(indices, types.GeneratorType):
indices = np.fromiter(indices, dtype=np.int64, count=-1)
sample_len = 0
sample_lens = []
batch = []
batches = []
for i in range(len(indices)):
idx = indices[i]
num_tokens = num_tokens_fn(idx)
sample_lens.append(num_tokens)
sample_len = max(sample_len, num_tokens)
assert sample_len <= max_tokens, (
"sentence at index {} of size {} exceeds max_tokens "
"limit of {}!".format(idx, sample_len, max_tokens)
)
num_tokens = (len(batch) + 1) * sample_len
if _is_batch_full(batch, num_tokens, max_tokens, max_sentences):
mod_len = max(
bsz_mult * (len(batch) // bsz_mult),
len(batch) % bsz_mult,
)
batches.append(batch[:mod_len])
batch = batch[mod_len:]
sample_lens = sample_lens[mod_len:]
sample_len = max(sample_lens) if len(sample_lens) > 0 else 0
batch.append(idx)
if len(batch) > 0:
batches.append(batch)
return batches
def build_dataloader(dataset, shuffle, max_tokens=None, max_sentences=None,
required_batch_size_multiple=-1, endless=False, apply_batch_by_size=True, pin_memory=False, use_ddp=False):
import torch.distributed as dist
devices_cnt = torch.cuda.device_count()
if devices_cnt == 0:
devices_cnt = 1
if not use_ddp:
devices_cnt = 1
if required_batch_size_multiple == -1:
required_batch_size_multiple = devices_cnt
def shuffle_batches(batches):
np.random.shuffle(batches)
return batches
if max_tokens is not None:
max_tokens *= devices_cnt
if max_sentences is not None:
max_sentences *= devices_cnt
indices = dataset.ordered_indices()
if apply_batch_by_size:
batch_sampler = batch_by_size(
indices, dataset.num_tokens, max_tokens=max_tokens, max_sentences=max_sentences,
required_batch_size_multiple=required_batch_size_multiple,
)
else:
batch_sampler = []
for i in range(0, len(indices), max_sentences):
batch_sampler.append(indices[i:i + max_sentences])
if shuffle:
batches = shuffle_batches(list(batch_sampler))
if endless:
batches = [b for _ in range(1000) for b in shuffle_batches(list(batch_sampler))]
else:
batches = batch_sampler
if endless:
batches = [b for _ in range(1000) for b in batches]
num_workers = dataset.num_workers
if use_ddp:
num_replicas = dist.get_world_size()
rank = dist.get_rank()
# batches = [x[rank::num_replicas] for x in batches if len(x) % num_replicas == 0]
# ensure that every sample in the dataset is covered
batches_ = []
for x in batches:
if len(x) % num_replicas == 0:
batches_.append(x[rank::num_replicas])
else:
x_ = x + [x[-1]] * (len(x) - len(x) // num_replicas * num_replicas)
batches_.append(x_[rank::num_replicas])
batches = batches_
return torch.utils.data.DataLoader(dataset,
collate_fn=dataset.collater,
batch_sampler=batches,
num_workers=num_workers,
pin_memory=pin_memory)
def unpack_dict_to_list(samples):
samples_ = []
bsz = samples.get('outputs').size(0)
for i in range(bsz):
res = {}
for k, v in samples.items():
try:
res[k] = v[i]
except:
pass
samples_.append(res)
return samples_
def remove_padding(x, padding_idx=0):
if x is None:
return None
assert len(x.shape) in [1, 2]
if len(x.shape) == 2: # [T, H]
return x[np.abs(x).sum(-1) != padding_idx]
elif len(x.shape) == 1: # [T]
return x[x != padding_idx]
def data_loader(fn):
"""
Decorator to make any fx with this use the lazy property
:param fn:
:return:
"""
wraps(fn)
attr_name = '_lazy_' + fn.__name__
def _get_data_loader(self):
try:
value = getattr(self, attr_name)
except AttributeError:
try:
value = fn(self) # Lazy evaluation, done only once.
except AttributeError as e:
# Guard against AttributeError suppression. (Issue #142)
traceback.print_exc()
error = f'{fn.__name__}: An AttributeError was encountered: ' + str(e)
raise RuntimeError(error) from e
setattr(self, attr_name, value) # Memoize evaluation.
return value
return _get_data_loader
class BaseDataset(torch.utils.data.Dataset):
def __init__(self, shuffle):
super().__init__()
self.hparams = hparams
self.shuffle = shuffle
self.sort_by_len = hparams['sort_by_len']
self.sizes = None
@property
def _sizes(self):
return self.sizes
def __getitem__(self, index):
raise NotImplementedError
def collater(self, samples):
raise NotImplementedError
def __len__(self):
return len(self._sizes)
def num_tokens(self, index):
return self.size(index)
def size(self, index):
"""Return an example's size as a float or tuple. This value is used when
filtering a dataset with ``--max-positions``."""
return min(self._sizes[index], hparams['max_frames'])
def ordered_indices(self):
"""Return an ordered list of indices. Batches will be constructed based
on this order."""
if self.shuffle:
indices = np.random.permutation(len(self))
if self.sort_by_len:
indices = indices[np.argsort(np.array(self._sizes)[indices], kind='mergesort')]
else:
indices = np.arange(len(self))
return indices.tolist()
@property
def num_workers(self):
return int(os.getenv('NUM_WORKERS', hparams['ds_workers']))
class BaseConcatDataset(ConcatDataset):
def collater(self, samples):
return self.datasets[0].collater(samples)
@property
def _sizes(self):
if not hasattr(self, 'sizes'):
self.sizes = list(chain.from_iterable([d._sizes for d in self.datasets]))
return self.sizes
def size(self, index):
return min(self._sizes[index], hparams['max_frames'])
def num_tokens(self, index):
return self.size(index)
def ordered_indices(self):
"""Return an ordered list of indices. Batches will be constructed based
on this order."""
if self.datasets[0].shuffle:
indices = np.random.permutation(len(self))
if self.datasets[0].sort_by_len:
indices = indices[np.argsort(np.array(self._sizes)[indices], kind='mergesort')]
else:
indices = np.arange(len(self))
return indices
@property
def num_workers(self):
return self.datasets[0].num_workers
@@ -0,0 +1,164 @@
from torch.nn.parallel import DistributedDataParallel
from torch.nn.parallel.distributed import _find_tensors
import torch.optim
import torch.utils.data
import torch
from packaging import version
class DDP(DistributedDataParallel):
"""
Override the forward call in lightning so it goes to training and validation step respectively
"""
def forward(self, *inputs, **kwargs): # pragma: no cover
# if version.parse(torch.__version__[:6]) < version.parse("1.11"):
if version.parse(torch.__version__) < version.parse("1.11"): # fix the hard [:6] problem
self._sync_params()
inputs, kwargs = self.scatter(inputs, kwargs, self.device_ids)
assert len(self.device_ids) == 1
if self.module.training:
output = self.module.training_step(*inputs[0], **kwargs[0])
elif self.module.testing:
output = self.module.test_step(*inputs[0], **kwargs[0])
else:
output = self.module.validation_step(*inputs[0], **kwargs[0])
if torch.is_grad_enabled():
# We'll return the output object verbatim since it is a freeform
# object. We need to find any tensors in this object, though,
# because we need to figure out which parameters were used during
# this forward pass, to ensure we short circuit reduction for any
# unused parameters. Only if `find_unused_parameters` is set.
if self.find_unused_parameters:
self.reducer.prepare_for_backward(list(_find_tensors(output)))
else:
self.reducer.prepare_for_backward([])
elif version.parse("1.11") <= version.parse(torch.__version__) < version.parse("2.0"):
from torch.nn.parallel.distributed import \
logging, Join, _DDPSink, _tree_flatten_with_rref, _tree_unflatten_with_rref
with torch.autograd.profiler.record_function("DistributedDataParallel.forward"):
if torch.is_grad_enabled() and self.require_backward_grad_sync:
self.logger.set_runtime_stats_and_log()
self.num_iterations += 1
self.reducer.prepare_for_forward()
# Notify the join context that this process has not joined, if
# needed
work = Join.notify_join_context(self)
if work:
self.reducer._set_forward_pass_work_handle(
work, self._divide_by_initial_world_size
)
# Calling _rebuild_buckets before forward compuation,
# It may allocate new buckets before deallocating old buckets
# inside _rebuild_buckets. To save peak memory usage,
# call _rebuild_buckets before the peak memory usage increases
# during forward computation.
# This should be called only once during whole training period.
if torch.is_grad_enabled() and self.reducer._rebuild_buckets():
logging.info("Reducer buckets have been rebuilt in this iteration.")
self._has_rebuilt_buckets = True
# sync params according to location (before/after forward) user
# specified as part of hook, if hook was specified.
buffer_hook_registered = hasattr(self, 'buffer_hook')
if self._check_sync_bufs_pre_fwd():
self._sync_buffers()
if self._join_config.enable:
# Notify joined ranks whether they should sync in backwards pass or not.
self._check_global_requires_backward_grad_sync(is_joined_rank=False)
# modified part
inputs, kwargs = self.scatter(inputs, kwargs, self.device_ids)
if self.module.training:
output = self.module.training_step(*inputs[0], **kwargs[0])
elif self.module.testing:
output = self.module.test_step(*inputs[0], **kwargs[0])
else:
output = self.module.validation_step(*inputs[0], **kwargs[0])
# sync params according to location (before/after forward) user
# specified as part of hook, if hook was specified.
if self._check_sync_bufs_post_fwd():
self._sync_buffers()
if torch.is_grad_enabled() and self.require_backward_grad_sync:
self.require_forward_param_sync = True
# We'll return the output object verbatim since it is a freeform
# object. We need to find any tensors in this object, though,
# because we need to figure out which parameters were used during
# this forward pass, to ensure we short circuit reduction for any
# unused parameters. Only if `find_unused_parameters` is set.
if self.find_unused_parameters and not self.static_graph:
# Do not need to populate this for static graph.
self.reducer.prepare_for_backward(list(_find_tensors(output)))
else:
self.reducer.prepare_for_backward([])
else:
self.require_forward_param_sync = False
# TODO: DDPSink is currently enabled for unused parameter detection and
# static graph training for first iteration.
if (self.find_unused_parameters and not self.static_graph) or (
self.static_graph and self.num_iterations == 1
):
state_dict = {
'static_graph': self.static_graph,
'num_iterations': self.num_iterations,
}
output_tensor_list, treespec, output_is_rref = _tree_flatten_with_rref(
output
)
output_placeholders = [None for _ in range(len(output_tensor_list))]
# Do not touch tensors that have no grad_fn, which can cause issues
# such as https://github.com/pytorch/pytorch/issues/60733
for i, output in enumerate(output_tensor_list):
if torch.is_tensor(output) and output.grad_fn is None:
output_placeholders[i] = output
# When find_unused_parameters=True, makes tensors which require grad
# run through the DDPSink backward pass. When not all outputs are
# used in loss, this makes those corresponding tensors receive
# undefined gradient which the reducer then handles to ensure
# param.grad field is not touched and we don't error out.
passthrough_tensor_list = _DDPSink.apply(
self.reducer,
state_dict,
*output_tensor_list,
)
for i in range(len(output_placeholders)):
if output_placeholders[i] is None:
output_placeholders[i] = passthrough_tensor_list[i]
# Reconstruct output data structure.
output = _tree_unflatten_with_rref(
output_placeholders, treespec, output_is_rref
)
else:
# now pytorch version >= 2.0
with torch.autograd.profiler.record_function("DistributedDataParallel.forward"):
inputs, kwargs = self._pre_forward(*inputs, **kwargs)
output = (
# self.module.forward(*inputs, **kwargs)
# if self._delay_all_reduce_all_params
# else self._run_ddp_forward(*inputs, **kwargs)
# modified: delete 'delay_all_reduce_named_params' function
self._run_ddp_forward(*inputs, **kwargs)
)
return self._post_forward(output)
return output
def _run_ddp_forward(self, *inputs, **kwargs):
if version.parse(torch.__version__) >= version.parse("2.0"):
with self._inside_ddp_forward():
if self.module.training:
output = self.module.training_step(*inputs, **kwargs)
elif self.module.testing:
output = self.module.test_step(*inputs, **kwargs)
else:
output = self.module.validation_step(*inputs, **kwargs)
return output # type: ignore[index]
else:
return super(DDP, self)._run_ddp_forward(*inputs, **kwargs)
@@ -0,0 +1,113 @@
import gc
import datetime
import inspect
import torch
import numpy as np
dtype_memory_size_dict = {
torch.float64: 64/8,
torch.double: 64/8,
torch.float32: 32/8,
torch.float: 32/8,
torch.float16: 16/8,
torch.half: 16/8,
torch.int64: 64/8,
torch.long: 64/8,
torch.int32: 32/8,
torch.int: 32/8,
torch.int16: 16/8,
torch.short: 16/6,
torch.uint8: 8/8,
torch.int8: 8/8,
}
# compatibility of torch1.0
if getattr(torch, "bfloat16", None) is not None:
dtype_memory_size_dict[torch.bfloat16] = 16/8
if getattr(torch, "bool", None) is not None:
dtype_memory_size_dict[torch.bool] = 8/8 # pytorch use 1 byte for a bool, see https://github.com/pytorch/pytorch/issues/41571
def get_mem_space(x):
try:
ret = dtype_memory_size_dict[x]
except KeyError:
print(f"dtype {x} is not supported!")
return ret
class MemTracker(object):
"""
Class used to track pytorch memory usage
Arguments:
detail(bool, default True): whether the function shows the detail gpu memory usage
path(str): where to save log file
verbose(bool, default False): whether show the trivial exception
device(int): GPU number, default is 0
"""
def __init__(self, detail=True, path='', verbose=False, device=0):
self.print_detail = detail
self.last_tensor_sizes = set()
self.gpu_profile_fn = path + f'{datetime.datetime.now():%d-%b-%y-%H:%M:%S}-gpu_mem_track.txt'
self.verbose = verbose
self.begin = True
self.device = device
def get_tensors(self):
for obj in gc.get_objects():
try:
if torch.is_tensor(obj) or (hasattr(obj, 'data') and torch.is_tensor(obj.data)):
tensor = obj
else:
continue
if tensor.is_cuda:
yield tensor
except Exception as e:
if self.verbose:
print('A trivial exception occured: {}'.format(e))
def get_tensor_usage(self):
sizes = [np.prod(np.array(tensor.size())) * get_mem_space(tensor.dtype) for tensor in self.get_tensors()]
return np.sum(sizes) / 1024**2
def get_allocate_usage(self):
return torch.cuda.memory_allocated() / 1024**2
def clear_cache(self):
gc.collect()
torch.cuda.empty_cache()
def print_all_gpu_tensor(self, file=None):
for x in self.get_tensors():
print(x.size(), x.dtype, np.prod(np.array(x.size()))*get_mem_space(x.dtype)/1024**2, file=file)
def track(self):
"""
Track the GPU memory usage
"""
frameinfo = inspect.stack()[1]
where_str = frameinfo.filename + ' line ' + str(frameinfo.lineno) + ': ' + frameinfo.function
with open(self.gpu_profile_fn, 'a+') as f:
if self.begin:
f.write(f"GPU Memory Track | {datetime.datetime.now():%d-%b-%y-%H:%M:%S} |"
f" Total Tensor Used Memory:{self.get_tensor_usage():<7.1f}Mb"
f" Total Allocated Memory:{self.get_allocate_usage():<7.1f}Mb\n\n")
self.begin = False
if self.print_detail is True:
ts_list = [(tensor.size(), tensor.dtype) for tensor in self.get_tensors()]
new_tensor_sizes = {(type(x),
tuple(x.size()),
ts_list.count((x.size(), x.dtype)),
np.prod(np.array(x.size()))*get_mem_space(x.dtype)/1024**2,
x.dtype) for x in self.get_tensors()}
for t, s, n, m, data_type in new_tensor_sizes - self.last_tensor_sizes:
f.write(f'+ | {str(n)} * Size:{str(s):<20} | Memory: {str(m*n)[:6]} M | {str(t):<20} | {data_type}\n')
for t, s, n, m, data_type in self.last_tensor_sizes - new_tensor_sizes:
f.write(f'- | {str(n)} * Size:{str(s):<20} | Memory: {str(m*n)[:6]} M | {str(t):<20} | {data_type}\n')
self.last_tensor_sizes = new_tensor_sizes
f.write(f"\nAt {where_str:<50}"
f" Total Tensor Used Memory:{self.get_tensor_usage():<7.1f}Mb"
f" Total Allocated Memory:{self.get_allocate_usage():<7.1f}Mb\n\n")
@@ -0,0 +1,131 @@
import argparse
import os
import yaml
global_print_hparams = True
hparams = {}
class Args:
def __init__(self, **kwargs):
for k, v in kwargs.items():
self.__setattr__(k, v)
def override_config(old_config: dict, new_config: dict):
for k, v in new_config.items():
if isinstance(v, dict) and k in old_config:
override_config(old_config[k], new_config[k])
else:
old_config[k] = v
def set_hparams(config='', exp_name='', hparams_str='', print_hparams=True, global_hparams=True, root_dir=''):
if config == '' and exp_name == '':
parser = argparse.ArgumentParser(description='')
parser.add_argument('--config', type=str, default='',
help='location of the data corpus')
parser.add_argument('--exp_name', type=str, default='', help='exp_name')
parser.add_argument('-hp', '--hparams', type=str, default='',
help='location of the data corpus')
parser.add_argument('--infer', action='store_true', help='infer')
parser.add_argument('--validate', action='store_true', help='validate')
parser.add_argument('--reset', action='store_true', help='reset hparams')
parser.add_argument('--remove', action='store_true', help='remove old ckpt')
parser.add_argument('--debug', action='store_true', help='debug')
parser.add_argument('--root_dir', type=str, default='', help='root directory of the project.')
args, unknown = parser.parse_known_args()
print("| Unknow hparams: ", unknown)
else:
args = Args(config=config, exp_name=exp_name, hparams=hparams_str,
infer=False, validate=False, reset=False, debug=False, remove=False, root_dir=root_dir)
global hparams
assert args.config != '' or args.exp_name != ''
root_dir = args.root_dir
if args.config != '':
assert os.path.exists(os.path.join(root_dir, args.config)), f'| Wrong config path! root_dir: {root_dir}, config_path: {args.config}'
config_chains = []
loaded_config = set()
def load_config(config_fn):
# deep first inheritance and avoid the second visit of one node
if not os.path.exists(os.path.join(root_dir, config_fn)):
return {}
with open(os.path.join(root_dir, config_fn)) as f:
hparams_ = yaml.safe_load(f)
loaded_config.add(config_fn)
if 'base_config' in hparams_:
ret_hparams = {}
if not isinstance(hparams_['base_config'], list):
hparams_['base_config'] = [hparams_['base_config']]
for c in hparams_['base_config']:
if c.startswith('.'):
c = f'{os.path.dirname(config_fn)}/{c}'
c = os.path.normpath(c)
if c not in loaded_config:
override_config(ret_hparams, load_config(c))
override_config(ret_hparams, hparams_)
else:
ret_hparams = hparams_
config_chains.append(config_fn)
return ret_hparams
saved_hparams = {}
args_work_dir = ''
if args.exp_name != '':
args_work_dir = os.path.join(root_dir, f'checkpoints/{args.exp_name}')
ckpt_config_path = f'{args_work_dir}/config.yaml'
if os.path.exists(ckpt_config_path):
with open(ckpt_config_path) as f:
saved_hparams_ = yaml.safe_load(f)
if saved_hparams_ is not None:
saved_hparams.update(saved_hparams_)
hparams_ = {}
if args.config != '':
hparams_.update(load_config(args.config))
if not args.reset:
hparams_.update(saved_hparams)
hparams_['work_dir'] = args_work_dir
# Support config overriding in command line. Support list type config overriding.
# Examples: --hparams="a=1,b.c=2,d=[1 1 1]"
if args.hparams != "":
for new_hparam in args.hparams.split(","):
k, v = new_hparam.split("=")
v = v.strip("\'\" ")
config_node = hparams_
for k_ in k.split(".")[:-1]:
config_node = config_node[k_]
k = k.split(".")[-1]
if v in ['True', 'False'] or type(config_node[k]) in [bool, list, dict]:
if type(config_node[k]) == list:
v = v.replace(" ", ",")
config_node[k] = eval(v)
else:
config_node[k] = type(config_node[k])(v)
if args_work_dir != '' and args.remove:
answer = input("REMOVE old checkpoint? Y/N [Default: N]: ")
if answer.lower() == "y":
pass
if args_work_dir != '' and (not os.path.exists(ckpt_config_path) or args.reset) and not args.infer:
os.makedirs(hparams_['work_dir'], exist_ok=True)
with open(ckpt_config_path, 'w') as f:
yaml.safe_dump(hparams_, f)
hparams_['infer'] = args.infer
hparams_['debug'] = args.debug
hparams_['validate'] = args.validate
hparams_['exp_name'] = args.exp_name
global global_print_hparams
if global_hparams:
hparams.clear()
hparams.update(hparams_)
if print_hparams and global_print_hparams and global_hparams:
# print('| Hparams chains: ', config_chains)
# print('| Hparams: ')
# for i, (k, v) in enumerate(sorted(hparams_.items())):
# print(f"\033[;33;m{k}\033[0m: {v}, ", end="\n" if i % 5 == 4 else "")
# print("")
global_print_hparams = False
return hparams_
@@ -0,0 +1,71 @@
import pickle
from copy import deepcopy
import numpy as np
class IndexedDataset:
def __init__(self, path, num_cache=1):
super().__init__()
self.path = path
self.data_file = None
self.data_offsets = np.load(f"{path}.idx", allow_pickle=True).item()['offsets']
self.data_file = open(f"{path}.data", 'rb', buffering=-1)
self.cache = []
self.num_cache = num_cache
def check_index(self, i):
if i < 0 or i >= len(self.data_offsets) - 1:
raise IndexError('index out of range')
def __del__(self):
if self.data_file:
self.data_file.close()
def __getitem__(self, i):
self.check_index(i)
if self.num_cache > 0:
for c in self.cache:
if c[0] == i:
return c[1]
self.data_file.seek(self.data_offsets[i])
b = self.data_file.read(self.data_offsets[i + 1] - self.data_offsets[i])
item = pickle.loads(b)
if self.num_cache > 0:
self.cache = [(i, deepcopy(item))] + self.cache[:-1]
return item
def __len__(self):
return len(self.data_offsets) - 1
class IndexedDatasetBuilder:
def __init__(self, path):
self.path = path
self.out_file = open(f"{path}.data", 'wb')
self.byte_offsets = [0]
def add_item(self, item):
s = pickle.dumps(item)
bytes = self.out_file.write(s)
self.byte_offsets.append(self.byte_offsets[-1] + bytes)
def finalize(self):
self.out_file.close()
np.save(open(f"{self.path}.idx", 'wb'), {'offsets': self.byte_offsets})
if __name__ == "__main__":
import random
from tqdm import tqdm
ds_path = '/tmp/indexed_ds_example'
size = 100
items = [{"a": np.random.normal(size=[10000, 10]),
"b": np.random.normal(size=[10000, 10])} for i in range(size)]
builder = IndexedDatasetBuilder(ds_path)
for i in tqdm(range(size)):
builder.add_item(items[i])
builder.finalize()
ds = IndexedDataset(ds_path)
for i in tqdm(range(10000)):
idx = random.randint(0, size - 1)
assert (ds[idx]['a'] == items[idx]['a']).all()
@@ -0,0 +1,53 @@
import torch
import torch.nn.functional as F
def sigmoid_focal_loss(
inputs: torch.Tensor,
targets: torch.Tensor,
alpha: float = 0.25,
gamma: float = 2,
reduction: str = "none",
) -> torch.Tensor:
"""
Loss used in RetinaNet for dense detection: https://arxiv.org/abs/1708.02002.
Args:
inputs (Tensor): A float tensor of arbitrary shape.
The predictions for each example.
targets (Tensor): A float tensor with the same shape as inputs. Stores the binary
classification label for each element in inputs
(0 for the negative class and 1 for the positive class).
alpha (float): Weighting factor in range (0,1) to balance
positive vs negative examples or -1 for ignore. Default: ``0.25``.
gamma (float): Exponent of the modulating factor (1 - p_t) to
balance easy vs hard examples. Default: ``2``.
reduction (string): ``'none'`` | ``'mean'`` | ``'sum'``
``'none'``: No reduction will be applied to the output.
``'mean'``: The output will be averaged.
``'sum'``: The output will be summed. Default: ``'none'``.
Returns:
Loss tensor with the reduction option applied.
"""
# Original implementation from https://github.com/facebookresearch/fvcore/blob/master/fvcore/nn/focal_loss.py
p = torch.sigmoid(inputs)
ce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction="none")
p_t = p * targets + (1 - p) * (1 - targets)
loss = ce_loss * ((1 - p_t) ** gamma)
if alpha >= 0: # decrease the importance of negative samples
alpha_t = alpha * targets + (1 - alpha) * (1 - targets)
loss = alpha_t * loss
# Check reduction option and return loss accordingly
if reduction == "none":
pass
elif reduction == "mean":
loss = loss.mean()
elif reduction == "sum":
loss = loss.sum()
else:
raise ValueError(
f"Invalid Value for arg 'reduction': '{reduction} \n Supported reduction modes: 'none', 'mean', 'sum'"
)
return loss
@@ -0,0 +1,42 @@
import time
import torch
class AvgrageMeter(object):
def __init__(self):
self.reset()
def reset(self):
self.avg = 0
self.sum = 0
self.cnt = 0
def update(self, val, n=1):
self.sum += val * n
self.cnt += n
self.avg = self.sum / self.cnt
class Timer:
timer_map = {}
def __init__(self, name, enable=False):
if name not in Timer.timer_map:
Timer.timer_map[name] = 0
self.name = name
self.enable = enable
def __enter__(self):
if self.enable:
if torch.cuda.is_available():
torch.cuda.synchronize()
self.t = time.time()
def __exit__(self, exc_type, exc_val, exc_tb):
if self.enable:
if torch.cuda.is_available():
torch.cuda.synchronize()
Timer.timer_map[self.name] += time.time() - self.t
if self.enable:
print(f'[Timer] {self.name}: {Timer.timer_map[self.name]}')
@@ -0,0 +1,180 @@
import os
import traceback
from functools import partial
from tqdm import tqdm
import torch
def chunked_worker(worker_id, args_queue=None, results_queue=None, init_ctx_func=None):
ctx = init_ctx_func(worker_id) if init_ctx_func is not None else None
while True:
args = args_queue.get()
if args == '<KILL>':
return
job_idx, map_func, arg = args
try:
map_func_ = partial(map_func, ctx=ctx) if ctx is not None else map_func
if isinstance(arg, dict):
res = map_func_(**arg)
elif isinstance(arg, (list, tuple)):
res = map_func_(*arg)
else:
res = map_func_(arg)
results_queue.put((job_idx, res))
except:
traceback.print_exc()
results_queue.put((job_idx, None))
class MultiprocessManager:
def __init__(self, num_workers=None, init_ctx_func=None, multithread=False, queue_max=-1):
if multithread:
from multiprocessing.dummy import Queue, Process
else:
from multiprocessing import Queue, Process
if num_workers is None:
num_workers = int(os.getenv('N_PROC', os.cpu_count()))
self.num_workers = num_workers
self.results_queue = Queue(maxsize=-1)
self.jobs_pending = []
self.args_queue = Queue(maxsize=queue_max)
self.workers = []
self.total_jobs = 0
self.multithread = multithread
for i in range(num_workers):
if multithread:
p = Process(target=chunked_worker,
args=(i, self.args_queue, self.results_queue, init_ctx_func))
else:
p = Process(target=chunked_worker,
args=(i, self.args_queue, self.results_queue, init_ctx_func),
daemon=True)
self.workers.append(p)
p.start()
def add_job(self, func, args):
if not self.args_queue.full():
self.args_queue.put((self.total_jobs, func, args))
else:
self.jobs_pending.append((self.total_jobs, func, args))
self.total_jobs += 1
def get_results(self):
self.n_finished = 0
while self.n_finished < self.total_jobs:
while len(self.jobs_pending) > 0 and not self.args_queue.full():
self.args_queue.put(self.jobs_pending[0])
self.jobs_pending = self.jobs_pending[1:]
job_id, res = self.results_queue.get()
yield job_id, res
self.n_finished += 1
for w in range(self.num_workers):
self.args_queue.put("<KILL>")
for w in self.workers:
w.join()
def close(self):
if not self.multithread:
for w in self.workers:
w.terminate()
def __len__(self):
return self.total_jobs
def multiprocess_run_tqdm(map_func, args, num_workers=None, ordered=True, init_ctx_func=None,
multithread=False, queue_max=-1, desc=None):
for i, res in tqdm(
multiprocess_run(map_func, args, num_workers, ordered, init_ctx_func, multithread,
queue_max=queue_max),
total=len(args), desc=desc):
yield i, res
def multiprocess_run(map_func, args, num_workers=None, ordered=True, init_ctx_func=None, multithread=False,
queue_max=-1):
"""
Multiprocessing running chunked jobs.
Examples:
>>> for res in tqdm(multiprocess_run(job_func, args):
>>> print(res)
:param map_func:
:param args:
:param num_workers:
:param ordered:
:param init_ctx_func:
:param q_max_size:
:param multithread:
:return:
"""
if num_workers is None:
num_workers = int(os.getenv('N_PROC', os.cpu_count()))
manager = MultiprocessManager(num_workers, init_ctx_func, multithread, queue_max=queue_max)
for arg in args:
manager.add_job(map_func, arg)
if ordered:
n_jobs = len(args)
results = ['<WAIT>' for _ in range(n_jobs)]
i_now = 0
for job_i, res in manager.get_results():
results[job_i] = res
while i_now < n_jobs and (not isinstance(results[i_now], str) or results[i_now] != '<WAIT>'):
yield i_now, results[i_now]
results[i_now] = None
i_now += 1
else:
for job_i, res in manager.get_results():
yield job_i, res
manager.close()
# #### this is the old version of chunked_multiprocess_run
def chunked_worker_old(worker_id, map_func, args, results_queue=None, init_ctx_func=None):
ctx = init_ctx_func(worker_id) if init_ctx_func is not None else None
for job_idx, arg in args:
try:
if not isinstance(arg, tuple) and not isinstance(arg, list):
arg = [arg]
if ctx is not None:
res = map_func(*arg, ctx=ctx)
else:
res = map_func(*arg)
results_queue.put((job_idx, res))
except:
traceback.print_exc()
results_queue.put((job_idx, None))
def chunked_multiprocess_run(
map_func, args, num_workers=None, ordered=True,
init_ctx_func=None, q_max_size=1000, multithread=False):
if multithread:
from multiprocessing.dummy import Queue, Process
else:
from multiprocessing import Queue, Process
args = zip(range(len(args)), args)
args = list(args)
n_jobs = len(args)
if num_workers is None:
num_workers = int(os.getenv('N_PROC', os.cpu_count()))
results_queues = []
if ordered:
for i in range(num_workers):
results_queues.append(Queue(maxsize=q_max_size // num_workers))
else:
results_queue = Queue(maxsize=q_max_size)
for i in range(num_workers):
results_queues.append(results_queue)
workers = []
for i in range(num_workers):
args_worker = args[i::num_workers]
p = Process(target=chunked_worker_old, args=(
i, map_func, args_worker, results_queues[i], init_ctx_func), daemon=True)
workers.append(p)
p.start()
for n_finished in range(n_jobs):
results_queue = results_queues[n_finished % num_workers]
job_idx, res = results_queue.get()
assert job_idx == n_finished or not ordered, (job_idx, n_finished)
yield res
for w in workers:
w.join()
@@ -0,0 +1,74 @@
import numpy as np
import torch
import torch.nn as nn
def get_filter_2d(kernel, kernel_size, channels, no_grad=True):
# Reshape to 2d depthwise convolutional weight
kernel = kernel.view(1, 1, kernel_size, kernel_size)
kernel = kernel.repeat(channels, 1, 1, 1)
filter = nn.Conv2d(in_channels=channels, out_channels=channels, kernel_size=kernel_size, groups=channels,
bias=False, padding=kernel_size // 2)
filter.weight.data = kernel
if no_grad:
filter.weight.requires_grad = False
return filter
def get_filter_1d(kernel, kernel_size, channels, no_grad=True):
kernel = kernel.view(1, 1, kernel_size)
kernel = kernel.repeat(channels, 1, 1)
filter = nn.Conv1d(in_channels=channels, out_channels=channels, kernel_size=kernel_size, groups=channels,
bias=False, padding=kernel_size // 2)
filter.weight.data = kernel
if no_grad:
filter.weight.requires_grad = False
return filter
def get_gaussian_kernel_2d(kernel_size, sigma):
# Create a x, y coordinate grid of shape (kernel_size, kernel_size, 2)
x_coord = torch.arange(kernel_size)
x_grid = x_coord.repeat(kernel_size).view(kernel_size, kernel_size)
y_grid = x_grid.t()
xy_grid = torch.stack([x_grid, y_grid], dim=-1).float()
mean = (kernel_size - 1) / 2.
variance = sigma ** 2.
# Calculate the 2-dimensional gaussian kernel which is
# the product of two gaussian distributions for two different
# variables (in this case called x and y)
gaussian_kernel = (1. / (2. * np.pi * variance)) * torch.exp(
-torch.sum((xy_grid - mean) ** 2., dim=-1) / (2 * variance))
# Make sure sum of values in gaussian kernel equals 1.
gaussian_kernel = gaussian_kernel / torch.sum(gaussian_kernel)
return gaussian_kernel
def get_gaussian_kernel_1d(kernel_size, sigma):
x_grid = torch.arange(kernel_size)
mean = (kernel_size - 1) / 2.
variance = sigma ** 2.
gaussian_kernel = (1. / ((2. * np.pi) ** 0.5 * sigma)) * torch.exp(-(x_grid - mean) ** 2. / (2 * variance))
gaussian_kernel = gaussian_kernel / torch.sum(gaussian_kernel)
return gaussian_kernel
def get_hann_kernel_1d(kernel_size, periodic=False):
# periodic=False gives symmetric kernel, otherwise equivalent to hann(kernel_size + 1)
return torch.hann_window(kernel_size, periodic)
def get_triangle_kernel_1d(kernel_size):
kernel = torch.zeros(kernel_size)
for idx in range(kernel_size):
kernel[idx] = 1 - abs((idx - (kernel_size - 1) / 2) / ((kernel_size - 1) / 2))
return kernel
def add_gaussian_noise(tensor, mean=0, std=1):
noise = torch.randn(tensor.size()) * std + mean
noisy_tensor = tensor + noise
return noisy_tensor
@@ -0,0 +1,5 @@
import os
os.environ["OMP_NUM_THREADS"] = "1"
os.environ['TF_NUM_INTEROP_THREADS'] = '1'
os.environ['TF_NUM_INTRAOP_THREADS'] = '1'
@@ -0,0 +1,92 @@
import torch
import torch.distributed as dist
def reduce_tensors(metrics):
new_metrics = {}
for k, v in metrics.items():
if isinstance(v, torch.Tensor):
dist.all_reduce(v)
v = v / dist.get_world_size()
if type(v) is dict:
v = reduce_tensors(v)
new_metrics[k] = v
return new_metrics
def tensors_to_scalars(tensors):
if isinstance(tensors, torch.Tensor):
tensors = tensors.item()
return tensors
elif isinstance(tensors, dict):
new_tensors = {}
for k, v in tensors.items():
v = tensors_to_scalars(v)
new_tensors[k] = v
return new_tensors
elif isinstance(tensors, list):
return [tensors_to_scalars(v) for v in tensors]
else:
return tensors
def tensors_to_np(tensors):
if isinstance(tensors, dict):
new_np = {}
for k, v in tensors.items():
if isinstance(v, torch.Tensor):
v = v.cpu().numpy()
if type(v) is dict:
v = tensors_to_np(v)
new_np[k] = v
elif isinstance(tensors, list):
new_np = []
for v in tensors:
if isinstance(v, torch.Tensor):
v = v.cpu().numpy()
if type(v) is dict:
v = tensors_to_np(v)
new_np.append(v)
elif isinstance(tensors, torch.Tensor):
v = tensors
if isinstance(v, torch.Tensor):
v = v.cpu().numpy()
if type(v) is dict:
v = tensors_to_np(v)
new_np = v
else:
raise Exception(f'tensors_to_np does not support type {type(tensors)}.')
return new_np
def move_to_cpu(tensors):
ret = {}
for k, v in tensors.items():
if isinstance(v, torch.Tensor):
v = v.cpu()
if type(v) is dict:
v = move_to_cpu(v)
ret[k] = v
return ret
def move_to_cuda(batch, gpu_id=0):
# base case: object can be directly moved using `cuda` or `to`
if callable(getattr(batch, 'cuda', None)):
return batch.cuda(gpu_id, non_blocking=True)
elif callable(getattr(batch, 'to', None)):
return batch.to(torch.device('cuda', gpu_id), non_blocking=True)
elif isinstance(batch, list):
for i, x in enumerate(batch):
batch[i] = move_to_cuda(x, gpu_id)
return batch
elif isinstance(batch, tuple):
batch = list(batch)
for i, x in enumerate(batch):
batch[i] = move_to_cuda(x, gpu_id)
return tuple(batch)
elif isinstance(batch, dict):
for k, v in batch.items():
batch[k] = move_to_cuda(v, gpu_id)
return batch
return batch
@@ -0,0 +1,557 @@
import random
import subprocess
import traceback
from datetime import datetime
from torch.cuda.amp import GradScaler, autocast
import numpy as np
import torch.optim
import torch.utils.data
import copy
import logging
import os
import re
import sys
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
import tqdm
from .ckpt_utils import get_last_checkpoint, get_all_ckpts
from .ddp_utils import DDP
from .hparams import hparams
from .tensor_utils import move_to_cuda
class Tee(object):
def __init__(self, name, mode):
self.file = open(name, mode)
self.stdout = sys.stdout
sys.stdout = self
def __del__(self):
sys.stdout = self.stdout
self.file.close()
def write(self, data):
self.file.write(data)
self.stdout.write(data)
def flush(self):
self.file.flush()
class Trainer:
def __init__(
self,
work_dir,
default_save_path=None,
accumulate_grad_batches=1,
max_updates=160000,
print_nan_grads=False,
val_check_interval=2000,
num_sanity_val_steps=5,
amp=False,
# tb logger
log_save_interval=100,
tb_log_interval=10,
# checkpoint
monitor_key='val_loss',
monitor_mode='min',
num_ckpt_keep=5,
save_best=True,
resume_from_checkpoint=0,
seed=1234,
debug=False,
):
os.makedirs(work_dir, exist_ok=True)
self.work_dir = work_dir
self.accumulate_grad_batches = accumulate_grad_batches
self.max_updates = max_updates
self.num_sanity_val_steps = num_sanity_val_steps
self.print_nan_grads = print_nan_grads
self.default_save_path = default_save_path
self.resume_from_checkpoint = resume_from_checkpoint if resume_from_checkpoint > 0 else None
self.seed = seed
self.debug = debug
# model and optm
self.task = None
self.optimizers = []
# trainer state
self.testing = False
self.global_step = 0
self.current_epoch = 0
self.total_batches = 0
# configure checkpoint
self.monitor_key = monitor_key
self.num_ckpt_keep = num_ckpt_keep
self.save_best = save_best
self.monitor_op = np.less if monitor_mode == 'min' else np.greater
self.best_val_results = np.Inf if monitor_mode == 'min' else -np.Inf
self.mode = 'min'
# allow int, string and gpu list
self.all_gpu_ids = [
int(x) for x in os.environ.get("CUDA_VISIBLE_DEVICES", "").split(",") if x != '']
self.num_gpus = len(self.all_gpu_ids)
self.on_gpu = self.num_gpus > 0
self.root_gpu = 0
logging.info(f'GPU available: {torch.cuda.is_available()}, GPU used: {self.all_gpu_ids}')
self.use_ddp = self.num_gpus > 1
self.proc_rank = 0
# Tensorboard logging
self.log_save_interval = log_save_interval
self.val_check_interval = val_check_interval
self.tb_log_interval = tb_log_interval
self.amp = amp
self.amp_scalar = GradScaler()
def test(self, task_cls):
self.testing = True
self.fit(task_cls)
def fit(self, task_cls):
if len(self.all_gpu_ids) > 1:
mp.spawn(self.ddp_run, nprocs=self.num_gpus, args=(task_cls, copy.deepcopy(hparams)))
else:
self.task = task_cls()
self.task.trainer = self
self.run_single_process(self.task)
return 1
def ddp_run(self, gpu_idx, task_cls, hparams_):
hparams.update(hparams_)
self.proc_rank = gpu_idx
self.init_ddp_connection(self.proc_rank, self.num_gpus)
if dist.get_rank() != 0 and not self.debug:
sys.stdout = open(os.devnull, "w")
sys.stderr = open(os.devnull, "w")
task = task_cls()
task.trainer = self
torch.cuda.set_device(gpu_idx)
self.root_gpu = gpu_idx
self.task = task
self.run_single_process(task)
def run_single_process(self, task):
"""Sanity check a few things before starting actual training.
:param task:
"""
# build model, optm and load checkpoint
if self.proc_rank == 0:
self.save_terminal_logs()
if not self.testing:
self.save_codes()
model = task.build_model()
if model is not None:
task.model = model
checkpoint, _ = get_last_checkpoint(self.work_dir, self.resume_from_checkpoint)
if checkpoint is not None:
self.restore_weights(checkpoint)
elif self.on_gpu:
task.cuda(self.root_gpu)
if not self.testing:
self.optimizers = task.configure_optimizers()
self.fisrt_epoch = True
if checkpoint is not None:
self.restore_opt_state(checkpoint)
del checkpoint
# clear cache after restore
if self.on_gpu:
torch.cuda.empty_cache()
if self.use_ddp:
self.task = self.configure_ddp(self.task)
dist.barrier()
task_ref = self.get_task_ref()
task_ref.trainer = self
task_ref.testing = self.testing
# link up experiment object
if self.proc_rank == 0:
task_ref.build_tensorboard(save_dir=self.work_dir, name='tb_logs')
else:
os.makedirs('tmp', exist_ok=True)
task_ref.build_tensorboard(save_dir='tmp', name='tb_tmp')
self.logger = task_ref.logger
try:
if self.testing:
self.run_evaluation(test=True)
else:
self.train()
except KeyboardInterrupt as e:
traceback.print_exc()
task_ref.on_keyboard_interrupt()
####################
# valid and test
####################
def run_evaluation(self, test=False):
eval_results = self.evaluate(self.task, test, tqdm_desc='Valid' if not test else 'test',
max_batches=hparams['eval_max_batches'])
if eval_results is not None and 'tb_log' in eval_results:
tb_log_output = eval_results['tb_log']
self.log_metrics_to_tb(tb_log_output)
if self.proc_rank == 0 and not test:
self.save_checkpoint(epoch=self.current_epoch, logs=eval_results)
def evaluate(self, task, test=False, tqdm_desc='Valid', max_batches=None):
if max_batches == -1:
max_batches = None
# enable eval mode
task.zero_grad()
task.eval()
torch.set_grad_enabled(False)
task_ref = self.get_task_ref()
if test:
ret = task_ref.test_start()
if ret == 'EXIT':
return
else:
task_ref.validation_start()
outputs = []
dataloader = task_ref.test_dataloader() if test else task_ref.val_dataloader()
pbar = tqdm.tqdm(dataloader, desc=tqdm_desc, total=max_batches, dynamic_ncols=True, unit='step',
disable=self.root_gpu > 0)
# give model a chance to do something with the outputs (and method defined)
for batch_idx, batch in enumerate(pbar):
if batch is None: # pragma: no cover
continue
# stop short when on fast_dev_run (sets max_batch=1)
if max_batches is not None and batch_idx >= max_batches:
break
# make dataloader_idx arg in validation_step optional
if self.on_gpu:
batch = move_to_cuda(batch, self.root_gpu)
args = [batch, batch_idx]
if self.use_ddp:
output = task(*args)
else:
if test:
output = task_ref.test_step(*args)
else:
output = task_ref.validation_step(*args)
# track outputs for collation
outputs.append(output)
# give model a chance to do something with the outputs (and method defined)
if test:
eval_results = task_ref.test_end(outputs)
else:
eval_results = task_ref.validation_end(outputs)
# enable train mode again
task.train()
torch.set_grad_enabled(True)
return eval_results
####################
# train
####################
def train(self):
task_ref = self.get_task_ref()
task_ref.on_train_start()
if self.num_sanity_val_steps > 0:
# run tiny validation (if validation defined) to make sure program won't crash during val
self.evaluate(self.task, False, 'Sanity Val', max_batches=self.num_sanity_val_steps)
# clear cache before training
if self.on_gpu:
torch.cuda.empty_cache()
dataloader = task_ref.train_dataloader()
epoch = self.current_epoch
# run all epochs
while True:
# set seed for distributed sampler (enables shuffling for each epoch)
if self.use_ddp and hasattr(dataloader.sampler, 'set_epoch'):
dataloader.sampler.set_epoch(epoch)
# update training progress in trainer and model
task_ref.current_epoch = epoch
self.current_epoch = epoch
# total batches includes multiple val checks
self.batch_loss_value = 0 # accumulated grads
# before epoch hook
task_ref.on_epoch_start()
# run epoch
train_pbar = tqdm.tqdm(dataloader, initial=self.global_step, total=float('inf'),
dynamic_ncols=True, unit='step', disable=self.root_gpu > 0)
for batch_idx, batch in enumerate(train_pbar):
if self.global_step % self.val_check_interval == 0 and not self.fisrt_epoch:
self.run_evaluation()
pbar_metrics, tb_metrics = self.run_training_batch(batch_idx, batch)
train_pbar.set_postfix(**pbar_metrics)
self.fisrt_epoch = False
# when metrics should be logged
if (self.global_step + 1) % self.tb_log_interval == 0:
# logs user requested information to logger
self.log_metrics_to_tb(tb_metrics)
self.global_step += 1
task_ref.global_step = self.global_step
if self.global_step > self.max_updates:
print("| Training end..")
break
# epoch end hook
task_ref.on_epoch_end()
epoch += 1
if self.global_step > self.max_updates:
break
task_ref.on_train_end()
def run_training_batch(self, batch_idx, batch):
if batch is None:
return {}
all_progress_bar_metrics = []
all_log_metrics = []
task_ref = self.get_task_ref()
for opt_idx, optimizer in enumerate(self.optimizers):
if optimizer is None:
continue
# make sure only the gradients of the current optimizer's paramaters are calculated
# in the training step to prevent dangling gradients in multiple-optimizer setup.
if len(self.optimizers) > 1:
for param in task_ref.parameters():
param.requires_grad = False
for group in optimizer.param_groups:
for param in group['params']:
param.requires_grad = True
# forward pass
with autocast(enabled=self.amp):
if self.on_gpu:
batch = move_to_cuda(copy.copy(batch), self.root_gpu)
args = [batch, batch_idx, opt_idx]
if self.use_ddp:
output = self.task(*args)
else:
output = task_ref.training_step(*args)
loss = output['loss']
if loss is None:
continue
progress_bar_metrics = output['progress_bar']
log_metrics = output['tb_log']
# accumulate loss
loss = loss / self.accumulate_grad_batches
# backward pass
if loss.requires_grad:
if self.amp:
self.amp_scalar.scale(loss).backward()
else:
loss.backward()
# track progress bar metrics
all_log_metrics.append(log_metrics)
all_progress_bar_metrics.append(progress_bar_metrics)
if loss is None:
continue
# nan grads
if self.print_nan_grads:
has_nan_grad = False
for name, param in task_ref.named_parameters():
if (param.grad is not None) and torch.isnan(param.grad.float()).any():
print("| NaN params: ", name, param, param.grad)
has_nan_grad = True
if has_nan_grad:
exit(0)
# gradient update with accumulated gradients
if (self.global_step + 1) % self.accumulate_grad_batches == 0:
task_ref.on_before_optimization(opt_idx)
if self.amp:
self.amp_scalar.step(optimizer)
self.amp_scalar.update()
else:
optimizer.step()
optimizer.zero_grad()
task_ref.on_after_optimization(self.current_epoch, batch_idx, optimizer, opt_idx)
# collapse all metrics into one dict
all_progress_bar_metrics = {k: v for d in all_progress_bar_metrics for k, v in d.items()}
all_log_metrics = {k: v for d in all_log_metrics for k, v in d.items()}
return all_progress_bar_metrics, all_log_metrics
####################
# load and save checkpoint
####################
def restore_weights(self, checkpoint):
# load model state
task_ref = self.get_task_ref()
for k, v in checkpoint['state_dict'].items():
getattr(task_ref, k).load_state_dict(v)
if self.on_gpu:
task_ref.cuda(self.root_gpu)
# load training state (affects trainer only)
self.best_val_results = checkpoint['checkpoint_callback_best']
self.global_step = checkpoint['global_step']
self.current_epoch = checkpoint['epoch']
task_ref.global_step = self.global_step
# wait for all models to restore weights
if self.use_ddp:
# wait for all processes to catch up
dist.barrier()
def restore_opt_state(self, checkpoint):
if self.testing:
return
# restore the optimizers
optimizer_states = checkpoint['optimizer_states']
for optimizer, opt_state in zip(self.optimizers, optimizer_states):
if optimizer is None:
return
try:
optimizer.load_state_dict(opt_state)
# move optimizer to GPU 1 weight at a time
if self.on_gpu:
for state in optimizer.state.values():
for k, v in state.items():
if isinstance(v, torch.Tensor):
state[k] = v.cuda(self.root_gpu)
except ValueError:
print("| WARMING: optimizer parameters not match !!!")
try:
if dist.is_initialized() and dist.get_rank() > 0:
return
except Exception as e:
print(e)
return
did_restore = True
return did_restore
def save_checkpoint(self, epoch, logs=None):
monitor_op = np.less
ckpt_path = f'{self.work_dir}/model_ckpt_steps_{self.global_step}.ckpt'
logging.info(f'Epoch {epoch:05d}@{self.global_step}: saving model to {ckpt_path}')
self._atomic_save(ckpt_path)
for old_ckpt in get_all_ckpts(self.work_dir)[self.num_ckpt_keep:]:
pass
current = None
if logs is not None and self.monitor_key in logs:
current = logs[self.monitor_key]
if current is not None and self.save_best:
if monitor_op(current, self.best_val_results):
best_filepath = f'{self.work_dir}/model_ckpt_best.pt'
self.best_val_results = current
logging.info(
f'Epoch {epoch:05d}@{self.global_step}: {self.monitor_key} reached {current:0.5f}. '
f'Saving model to {best_filepath}')
self._atomic_save(best_filepath)
def _atomic_save(self, filepath):
checkpoint = self.dump_checkpoint()
tmp_path = str(filepath) + ".part"
torch.save(checkpoint, tmp_path, _use_new_zipfile_serialization=False)
os.replace(tmp_path, filepath)
def dump_checkpoint(self):
checkpoint = {'epoch': self.current_epoch, 'global_step': self.global_step,
'checkpoint_callback_best': self.best_val_results}
# save optimizers
optimizer_states = []
for i, optimizer in enumerate(self.optimizers):
if optimizer is not None:
optimizer_states.append(optimizer.state_dict())
checkpoint['optimizer_states'] = optimizer_states
task_ref = self.get_task_ref()
checkpoint['state_dict'] = {
k: v.state_dict() for k, v in task_ref.named_children() if len(list(v.parameters())) > 0}
return checkpoint
####################
# DDP
####################
def configure_ddp(self, task):
task = DDP(task, device_ids=[self.root_gpu], find_unused_parameters=hparams.get('find_unused_parameters', True))
random.seed(self.seed)
np.random.seed(self.seed)
return task
def init_ddp_connection(self, proc_rank, world_size):
root_node = '127.0.0.1'
root_node = self.resolve_root_node_address(root_node)
os.environ['MASTER_ADDR'] = root_node
dist.init_process_group('nccl', rank=proc_rank, world_size=world_size)
def resolve_root_node_address(self, root_node):
if '[' in root_node:
name = root_node.split('[')[0]
number = root_node.split(',')[0]
if '-' in number:
number = number.split('-')[0]
number = re.sub('[^0-9]', '', number)
root_node = name + number
return root_node
####################
# utils
####################
def get_task_ref(self):
from .base_task import BaseTask
task: BaseTask = self.task.module if isinstance(self.task, DDP) else self.task
return task
def log_metrics_to_tb(self, metrics, step=None):
"""Logs the metric dict passed in.
:param metrics:
"""
# turn all tensors to scalars
scalar_metrics = self.metrics_to_scalars(metrics)
step = step if step is not None else self.global_step
# log actual metrics
if self.proc_rank == 0:
self.log_metrics(self.logger, scalar_metrics, step=step)
@staticmethod
def log_metrics(logger, metrics, step=None):
for k, v in metrics.items():
if isinstance(v, torch.Tensor):
v = v.item()
logger.add_scalar(k, v, step)
def metrics_to_scalars(self, metrics):
new_metrics = {}
for k, v in metrics.items():
if isinstance(v, torch.Tensor):
v = v.item()
if type(v) is dict:
v = self.metrics_to_scalars(v)
new_metrics[k] = v
return new_metrics
def save_terminal_logs(self):
t = datetime.now().strftime('%Y%m%d%H%M%S')
os.makedirs(f'{self.work_dir}/terminal_logs', exist_ok=True)
Tee(f'{self.work_dir}/terminal_logs/log_{t}.txt', 'w')
def save_codes(self):
if len(hparams['save_codes']) > 0:
t = datetime.now().strftime('%Y%m%d%H%M%S')
code_dir = f'{self.work_dir}/codes/{t}'
subprocess.check_call(f'mkdir -p "{code_dir}"', shell=True)
for c in hparams['save_codes']:
if os.path.exists(c):
subprocess.check_call(
f'rsync -aR '
f'--include="*.py" '
f'--include="*.yaml" '
f'--exclude="__pycache__" '
f'--include="*/" '
f'--exclude="*" '
f'"./{c}" "{code_dir}/"',
shell=True)
print(f"| Copied codes to {code_dir}.")