| """Console logger utilities. |
| |
| Copied from https://github.com/HazyResearch/transformers/blob/master/src/utils/utils.py |
| Copied from https://docs.python.org/3/howto/logging-cookbook.html#using-a-context-manager-for-selective-logging |
| """ |
|
|
| import argparse |
| import logging |
| import os |
| import sys |
| import pickle |
| import time |
| import math |
| from typing import List |
|
|
| import fsspec |
| import lightning |
| import numpy as np |
| import torch |
| from scipy.integrate import quad |
| from scipy.stats import norm |
| from timm.scheduler import CosineLRScheduler |
|
|
|
|
| def _save_tensor(tensor, filename): |
| if os.path.exists(filename): |
| _logits_saved = torch.load(filename) |
| _logits_saved = torch.cat([_logits_saved, tensor], dim=0) |
| else: |
| _logits_saved = tensor |
| torch.save(_logits_saved, filename) |
|
|
|
|
| class WarmupCosineLR(torch.optim.lr_scheduler._LRScheduler): |
| """ |
| Global-step scheduler: linear warmup -> cosine decay. |
| |
| Args: |
| optimizer: torch optimizer |
| total_steps: total number of optimizer steps in the entire training run |
| warmup_steps: linear warmup steps from 0 -> base lr |
| min_lr: final learning rate at the end of decay (per group) |
| last_epoch: DO NOT pass directly; Lightning manages stepping. Left for state restoration. |
| |
| Notes: |
| - `last_epoch` here tracks *optimizer steps taken* (i.e., global steps within the scheduler). |
| - Works with Lightning when you set scheduler dict {"interval": "step"}. |
| """ |
| def __init__( |
| self, |
| optimizer: torch.optim.Optimizer, |
| total_steps: int, |
| warmup_steps: int = 2000, |
| min_lr: float = 0.0, |
| last_epoch: int = -1): |
| assert total_steps > 0, 'total_steps must be > 0' |
| assert 0 < warmup_steps < total_steps, 'warmup_steps must be in (0, total_steps)' |
| self.total_steps = total_steps |
| self.warmup_steps = int(warmup_steps) |
| self.min_lr = min_lr |
| super().__init__(optimizer, last_epoch) |
|
|
| def _lr_at(self, step: int, base_lr: float) -> float: |
| |
| |
| if step < self.warmup_steps: |
| return base_lr * (step + 1) / self.warmup_steps |
| progress = (step - self.warmup_steps) / ( |
| self.total_steps - self.warmup_steps) |
| cosine = 0.5 * (1 + math.cos(math.pi * min(1.0, progress))) |
| return self.min_lr + (base_lr - self.min_lr) * cosine |
|
|
| def get_lr(self) -> List[float]: |
| step = max(0, self.last_epoch) |
| return [self._lr_at(step, base_lr=group['initial_lr']) |
| for group in self.optimizer.param_groups] |
|
|
|
|
| def count_parameters(model): |
| return sum(p.numel() |
| for p in model.parameters() |
| if p.requires_grad) |
|
|
| def fsspec_exists(filename): |
| """Check if a file exists using fsspec.""" |
| fs, _ = fsspec.core.url_to_fs(filename) |
| return fs.exists(filename) |
|
|
|
|
| def fsspec_listdir(dirname): |
| """Listdir in manner compatible with fsspec.""" |
| fs, _ = fsspec.core.url_to_fs(dirname) |
| return fs.ls(dirname) |
|
|
|
|
| def fsspec_mkdirs(dirname, exist_ok=True): |
| """Mkdirs in manner compatible with fsspec.""" |
| fs, _ = fsspec.core.url_to_fs(dirname) |
| fs.makedirs(dirname, exist_ok=exist_ok) |
|
|
|
|
| def print_nans(tensor, name): |
| if torch.isnan(tensor).any(): |
| print(name, tensor) |
|
|
|
|
| def flops_to_max_steps(target_flops_1e18, global_batch_size, |
| json_path): |
| with fsspec.open(json_path) as f: |
| config = json.load(f) |
| return int(target_flops_1e18 * 1e18 |
| / config['flops'] / global_batch_size / 3) |
|
|
|
|
| class LRHalveScheduler: |
| def __init__(self, warmup_steps, n_halve_steps): |
| self.warmup_steps = warmup_steps |
| self.n_halve_steps = n_halve_steps |
| |
| def __call__(self, current_step): |
| if current_step < self.warmup_steps: |
| return current_step / self.warmup_steps |
| return 0.5 ** ((current_step - self.warmup_steps) |
| // self.n_halve_steps) |
|
|
|
|
| class CosineDecayWarmupLRScheduler( |
| CosineLRScheduler, |
| torch.optim.lr_scheduler._LRScheduler): |
| """Wrap timm.scheduler.CosineLRScheduler |
| Enables calling scheduler.step() without passing in epoch. |
| Supports resuming as well. |
| Adapted from: |
| https://github.com/HazyResearch/hyena-dna/blob/main/src/utils/optim/schedulers.py |
| """ |
|
|
| def __init__(self, *args, **kwargs): |
| super().__init__(*args, **kwargs) |
| self._last_epoch = -1 |
| self.step(epoch=0) |
|
|
| def step(self, epoch=None): |
| if epoch is None: |
| self._last_epoch += 1 |
| else: |
| self._last_epoch = epoch |
| |
| |
| |
| |
| |
| |
| |
| if self.t_in_epochs: |
| super().step(epoch=self._last_epoch) |
| else: |
| super().step_update(num_updates=self._last_epoch) |
|
|
|
|
| class LoggingContext: |
| """Context manager for selective logging.""" |
| def __init__(self, logger, level=None, handler=None, close=True): |
| self.logger = logger |
| self.level = level |
| self.handler = handler |
| self.close = close |
|
|
| def __enter__(self): |
| if self.level is not None: |
| self.old_level = self.logger.level |
| self.logger.setLevel(self.level) |
| if self.handler: |
| self.logger.addHandler(self.handler) |
|
|
| def __exit__(self, et, ev, tb): |
| if self.level is not None: |
| self.logger.setLevel(self.old_level) |
| if self.handler: |
| self.logger.removeHandler(self.handler) |
| if self.handler and self.close: |
| self.handler.close() |
|
|
|
|
| class GradientInspectionCallback(lightning.Callback): |
| def __init__(self, num_grads_log): |
| self.num_grads_log = 10 |
|
|
| def on_before_optimizer_step(self, trainer, pl_module, optimizer): |
| gradients = [] |
| for name, param in pl_module.backbone.blocks.named_parameters(): |
| gradients.append(param.grad.view(-1)) |
|
|
| if gradients: |
| grads = torch.cat((gradients)) |
| if not hasattr(pl_module, 'grad_accum_buffer'): |
| pl_module.grad_step = torch.tensor( |
| 0, device=pl_module.device) |
| pl_module.grad_accum_buffer = torch.zeros( |
| self.num_grads_log, |
| grads.shape[0], |
| device=pl_module.device) |
| pl_module.grad_accum_buffer[pl_module.grad_step] = grads |
| pl_module.grad_step += 1 |
|
|
| if (hasattr(pl_module, 'grad_accum_buffer') |
| and pl_module.grad_step == self.num_grads_log): |
| grads = pl_module.grad_accum_buffer |
| grad_var = grads.std(0).mean() |
| pl_module.log(name='trainer/grad_var', |
| value=grad_var.item(), |
| on_step=True, |
| on_epoch=False, |
| sync_dist=True) |
| |
| |
| |
| pl_module.grad_accum_buffer.zero_() |
| pl_module.grad_step = 0 |
|
|
|
|
| def get_logger(name=__name__, level=logging.INFO) -> logging.Logger: |
| """Initializes multi-GPU-friendly python logger.""" |
|
|
| logger = logging.getLogger(name) |
| logger.setLevel(level) |
|
|
| |
| |
| for level in ('debug', 'info', 'warning', 'error', |
| 'exception', 'fatal', 'critical'): |
| setattr(logger, |
| level, |
| lightning.pytorch.utilities.rank_zero_only( |
| getattr(logger, level))) |
|
|
| return logger |
|
|
|
|
| |
| def top_k_top_p_filtering( |
| logits, |
| top_k=0, |
| top_p=0.0, |
| filter_value=-float("Inf"), |
| dim=-1): |
| """Filter a distribution of logits using top-k/top-p (nucleus) filtering. |
| Adapted from https://gist.github.com/thomwolf/1a5a29f6962089e871b94cbd09daf317 |
| |
| Args: |
| logits (Tensor): Tensor of logits |
| top_k (int, optional): Number of top values to keep. |
| Deactivated if k is 0. Defaults to 0. |
| top_p (float, optional): Cumulative mass to retain. |
| Deactivated if p = 0. Defaults to 0.0. |
| filter_value (float, optional): Fill value to replace |
| the entries removed by top-k/top-p filtering. |
| Defaults to -float('Inf'). |
| dim (int, optional): Dimension of the filtering. Defaults to -1. |
| |
| Returns: |
| logits: Tensor whose axis `dim` was filtered. |
| """ |
| if dim != -1: |
| logits = torch.transpose(logits, dim, -1) |
|
|
| assert top_k < logits.size(dim) |
| if top_k > 0: |
| |
| |
| values, _ = torch.topk(logits, k=top_k, dim=-1) |
| to_remove_mask = ( |
| logits < torch.min(values, dim=-1, keepdim=True)[0] |
| ) |
| logits[to_remove_mask] = filter_value |
|
|
| if top_p > 0.0: |
| sorted_logits, sorted_indices = torch.sort( |
| logits, descending=True, dim=-1) |
| cum_probs = torch.cumsum( |
| torch.softmax(sorted_logits, dim=-1), dim=-1) |
|
|
| sorted_indices_to_remove = cum_probs > top_p |
| |
| sorted_indices_to_remove[..., 1:] = \ |
| sorted_indices_to_remove[..., :-1].clone() |
| sorted_indices_to_remove[..., 0] = 0 |
|
|
| mask_to_remove = torch.empty_like(sorted_indices_to_remove) |
| mask_to_remove.scatter_(dim=-1, |
| index=sorted_indices, |
| src=sorted_indices_to_remove) |
| logits[mask_to_remove] = filter_value |
|
|
| if dim != -1: |
| logits = torch.transpose(logits, dim, -1) |
|
|
| return logits |
|
|
|
|
| def _discrete_prob_map(gamma_t, N=10): |
| snr_sqrt = np.exp(-gamma_t / 2) |
| def value(x): |
| cdf = norm.cdf(x, scale=1) ** (N - 1) |
| pdf = norm.pdf(x, loc=snr_sqrt, scale=1) |
| return pdf * cdf |
| return value |
|
|
|
|
| def _discrete_prob_grad(gamma_t, N=10): |
| snr_sqrt = np.exp(-gamma_t / 2) |
| def value(x): |
| coef = -0.5 * snr_sqrt * (x - snr_sqrt) |
| cdf = norm.cdf(x, scale=1) ** (N - 1) |
| pdf = norm.pdf(x, loc=snr_sqrt, scale=1) |
| return coef * pdf * cdf |
| return value |
|
|
|
|
| def _cache_prob_usdm_in_partition( |
| vocab_size=30522, partition_index=0, num_partitions=1, |
| log10_num_points=5): |
| print(f'Caching partition:{partition_index} / {num_partitions}') |
| path = 'integral' |
| gamma_min = -5 |
| gamma_max = -1 |
| num_points = 10 ** log10_num_points |
| p_cache = [] |
| grad_p_cache = [] |
| start_time = time.time() |
| gammas = np.linspace(gamma_min, gamma_max, num_points) |
| n = num_points // num_partitions |
| for gamma in gammas[partition_index * n: |
| (partition_index + 1) * n]: |
| pt, _ = quad(_discrete_prob_map(gamma, vocab_size), |
| -np.inf, np.inf) |
| p_cache.append(pt) |
| grad_pt, _ = quad(_discrete_prob_grad(gamma, vocab_size), |
| -np.inf, np.inf) |
| grad_p_cache.append(grad_pt) |
| if len(p_cache) % 100 == 0: |
| print('{}% completed. Time elapsed:{:.2f} mins'.format( |
| int(100 * len(p_cache) / num_points), |
| (time.time() - start_time) / 60)) |
|
|
| filename = os.path.join( |
| path, '{}_{}_{}-{}.pkl'.format( |
| vocab_size, log10_num_points, partition_index, |
| num_partitions)) |
| with open(filename, 'wb') as f: |
| pickle.dump({ |
| 'vocab_size': vocab_size, |
| 'gamma_min': gamma_min, |
| 'gamma_max': gamma_max, |
| 'num_points': num_points, |
| 'pt': np.asarray(p_cache), |
| 'grad_pt': np.asarray(grad_p_cache)}, f) |
|
|
|
|
| def test_cache_prob_usdm_in_partition( |
| partition_index=0, num_partitions=1, vocab_size=30522, |
| log10_num_points=5): |
| path = 'integral/{}_{}_{}-{}.pkl'.format( |
| vocab_size, log10_num_points, partition_index, |
| num_partitions) |
| with open(path, 'rb') as f: |
| data = pickle.load(f) |
| num_points = data['num_points'] |
| def _get_index(x): |
| return round((num_points - 1) * (x - data['gamma_min']) / ( |
| data['gamma_max'] - data['gamma_min'])) |
|
|
| pt_errors = [] |
| grad_pt_errors = [] |
| gammas = np.linspace(data['gamma_min'], |
| data['gamma_max'], |
| num_points) |
| n = num_points // num_partitions |
| for gamma in gammas[partition_index * n: |
| (partition_index + 1) * n]: |
| pt, _ = quad( |
| _discrete_prob_map(gamma, data['vocab_size']), |
| -np.inf, np.inf) |
| grad_pt, _ = quad( |
| _discrete_prob_grad(gamma, data['vocab_size']), |
| -np.inf, np.inf) |
| idx = _get_index(gamma) |
| print(idx) |
| pt_errors.append((pt - data['pt'][idx]) ** 2) |
| grad_pt_errors.append((grad_pt - data['grad_pt'][idx]) ** 2) |
| print('Integral MSE:{} Integral Squared:{:.4f}'.format( |
| np.mean(pt_errors), np.mean(data['pt'] ** 2))) |
| print('Integral Grad MSE:{} Integral Grad Squared:{:.4f}'.format( |
| np.mean(grad_pt_errors), np.mean(data['grad_pt'] ** 2))) |
|
|
|
|
| if __name__ == "__main__": |
| |
| parser = argparse.ArgumentParser( |
| description='Caches the integral appearing in the ' |
| 'Diffusion Transformation operator.') |
| parser.add_argument( |
| '--vocab_size', |
| type=int, |
| default=50257, |
| help='Vocabulary size (default: 50257)') |
| parser.add_argument( |
| '--partition_index', |
| type=int, |
| default=0, |
| help='Helps parallelize caching') |
| parser.add_argument( |
| '--num_partitions', |
| type=int, |
| default=1, |
| help='Helps parallelize caching') |
| parser.add_argument( |
| '--log10_num_points', |
| type=int, |
| default=5, |
| help=('The integral is function that needs to be ' |
| 'evaluated for inputs with a range [-5, 1]. ' |
| 'This argument represents the logarithm base 10 ' |
| 'of number of bins of discretization.')) |
| args = parser.parse_args() |
|
|
| |
| |
| |
| _cache_prob_usdm_in_partition( |
| partition_index=args.partition_index, |
| num_partitions=args.num_partitions, |
| vocab_size=args.vocab_size, |
| log10_num_points=args.log10_num_points) |
| |
| test_cache_prob_usdm_in_partition( |
| partition_index=args.partition_index, |
| num_partitions=args.num_partitions, |
| vocab_size=args.vocab_size, |
| log10_num_points=args.log10_num_points) |
|
|
|
|
| def get_reverse_indices(indices): |
| """ |
| indices: LongTensor of shape [B, N] representing permutations |
| returns: LongTensor of shape [B, N] representing the inverse permutations |
| """ |
| B, N = indices.shape |
| reverse_indices = torch.empty_like(indices) |
| arange = torch.arange(N, device=indices.device).unsqueeze(0).expand(B, -1) |
| reverse_indices.scatter_(1, indices, arange) |
| return reverse_indices |