mrnautilus / src /mrna /utils /callbacks.py
sawanp813's picture
Initial commit
e781e66
Raw History Blame Contribute Delete
11.1 kB
# Copyright (c) 2024 Bytedance Ltd. and/or its affiliates
# SPDX-License-Identifier: Apache-2.0
# Modifications copyright (C) 2024 Atom Bioworks
import os
from typing import Dict
import pytorch_lightning as pl
import torch
from pytorch_lightning import callbacks
#from pytorch_lightning.utilities.imports import _RICH_AVAILABLE
from importlib.util import find_spec
from pytorch_lightning.utilities.rank_zero import (
rank_zero_deprecation,
rank_zero_info,
rank_zero_warn,
)
from rich import reconfigure
from torch import Tensor
from omegaconf import OmegaConf
from packaging.version import Version
import importlib
from typing import Callable
from pkg_resources import DistributionNotFound
import pkg_resources
import operator
def _package_available(package_name: str) -> bool:
"""Check if a package is available in your environment.
>>> _package_available('os')
True
>>> _package_available('bla')
False
"""
try:
return find_spec(package_name) is not None
except ModuleNotFoundError:
return False
def _compare_version(package: str, op: Callable, version: str, use_base_version: bool = False) -> bool:
"""Compare package version with some requirements.
>>> _compare_version("torch", operator.ge, "0.1")
True
>>> _compare_version("does_not_exist", operator.ge, "0.0")
False
"""
try:
pkg = importlib.import_module(package)
except (ImportError, DistributionNotFound):
return False
try:
if hasattr(pkg, "__version__"):
pkg_version = Version(pkg.__version__)
else:
# try pkg_resources to infer version
pkg_version = Version(pkg_resources.get_distribution(package).version)
except TypeError:
# this is mocked by Sphinx, so it should return True to generate all summaries
return True
if use_base_version:
pkg_version = Version(pkg_version.base_version)
return op(pkg_version, Version(version))
_RICH_AVAILABLE = _package_available("rich") and _compare_version("rich", operator.ge, "10.2.2")
if _RICH_AVAILABLE:
from pytorch_lightning.callbacks.progress.rich_progress import (
CustomProgress,
MetricsTextColumn,
RichProgressBar,
)
from rich import get_console, reconfigure
from rich.text import Text
# NOTE[zzx]: modify here to display float in e-format when lower than 1e-3
def float_fmt(float_value):
if float_value.is_integer():
return round(float_value)
elif float_value < 1e-3:
return f'{float_value:.2e}'
else:
return round(float_value, 3)
class BetterMetricsTextColumn(MetricsTextColumn):
"""A column containing text."""
def render(self, task) -> Text:
if (
self._trainer.state.fn != "fit"
or self._trainer.sanity_checking
or self._trainer.progress_bar_callback.train_progress_bar_id != task.id
):
return Text()
if self._trainer.training and task.id not in self._tasks:
self._tasks[task.id] = "None"
if self._renderable_cache:
self._tasks[self._current_task_id] = self._renderable_cache[self._current_task_id][1]
self._current_task_id = task.id
if self._trainer.training and task.id != self._current_task_id:
return self._tasks[task.id]
text = ""
for k, v in self._metrics.items():
text += f"{k}: {float_fmt(v) if isinstance(v, float) else v} "
return Text(text, justify="left", style=self._style)
class BetterRichProgressBar(RichProgressBar):
def _init_progress(self, trainer):
if self.is_enabled and (self.progress is None or self._progress_stopped):
self._reset_progress_bar_ids()
reconfigure(**self._console_kwargs)
self._console = get_console()
self._console.clear_live()
self._metric_component = BetterMetricsTextColumn(trainer, self.theme.metrics, text_delimiter=',', metrics_format='.2f')
self.progress = CustomProgress(
*self.configure_columns(trainer),
self._metric_component,
auto_refresh=False,
disable=self.is_disabled,
console=self._console,
)
self.progress.start()
# progress has started
self._progress_stopped = False
class ValEveryNSteps(pl.Callback):
def __init__(self, every_n_step):
self.every_n_step = every_n_step
def on_batch_end(self, trainer, pl_module):
if trainer.global_step % self.every_n_step == 0 and trainer.global_step != 0:
trainer.validate()
class CheckpointEveryNSteps(pl.Callback):
"""
Save a checkpoint every N steps, instead of Lightning's default that checkpoints
based on validation loss.
"""
def __init__(
self,
save_step_frequency,
prefix="N-Step-Checkpoint",
use_modelcheckpoint_filename=False,
):
"""
Args:
save_step_frequency: how often to save in steps
prefix: add a prefix to the name, only used if
use_modelcheckpoint_filename=False
use_modelcheckpoint_filename: just use the ModelCheckpoint callback's
default filename, don't use ours.
"""
self.save_step_frequency = save_step_frequency
self.prefix = prefix
self.use_modelcheckpoint_filename = use_modelcheckpoint_filename
def on_batch_end(self, trainer: pl.Trainer, _):
""" Check if we should save a checkpoint after every train batch """
epoch = trainer.current_epoch
global_step = trainer.global_step
if global_step % self.save_step_frequency == 0:
if self.use_modelcheckpoint_filename:
filename = trainer.checkpoint_callback.filename
else:
filename = f"{self.prefix}_epoch={epoch}_step={global_step}.ckpt"
ckpt_path = os.path.join(trainer.checkpoint_callback.dirpath, filename)
trainer.save_checkpoint(ckpt_path)
class ModelCheckpoint(callbacks.ModelCheckpoint):
CHECKPOINT_NAME_BEST = "best"
# @classmethod
def _format_checkpoint_name(
self,
filename,
metrics: Dict[str, Tensor],
prefix: str = "",
auto_insert_metric_name: bool = True,
) -> str:
filename = super()._format_checkpoint_name(filename, metrics, prefix, auto_insert_metric_name)
filename = filename.replace('/', '_') # avoid '/' in filename unexpectedly creates folder
return filename
def on_train_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None:
super().on_train_start(trainer, pl_module)
trainer.callback_metrics[self.monitor] = self.best_model_score
def _update_best_and_save(
self, current: Tensor, trainer: "pl.Trainer", monitor_candidates: Dict[str, Tensor]
) -> None:
k = len(self.best_k_models) + 1 if self.save_top_k == -1 else self.save_top_k
del_filepath = None
if len(self.best_k_models) == k and k > 0:
del_filepath = self.kth_best_model_path
self.best_k_models.pop(del_filepath)
# do not save nan, replace with +/- inf
if isinstance(current, Tensor) and torch.isnan(current):
current = torch.tensor(float("inf" if self.mode == "min" else "-inf"), device=current.device)
filepath = self._get_metric_interpolated_filepath_name(monitor_candidates, trainer, del_filepath)
# save the current score
self.current_score = current
self.best_k_models[filepath] = current
if len(self.best_k_models) == k:
# monitor dict has reached k elements
_op = max if self.mode == "min" else min
self.kth_best_model_path = _op(self.best_k_models, key=self.best_k_models.get) # type: ignore[arg-type]
self.kth_value = self.best_k_models[self.kth_best_model_path]
_op = min if self.mode == "min" else max
self.best_model_path = _op(self.best_k_models, key=self.best_k_models.get) # type: ignore[arg-type]
self.best_model_score = self.best_k_models[self.best_model_path]
if self.verbose:
epoch = monitor_candidates["epoch"]
step = monitor_candidates["step"]
rank_zero_info(
f"Epoch {epoch:d}, global step {step:d}: {self.monitor!r} reached {current:0.5f}"
f" (best {self.best_model_score:0.5f}), saving model to {filepath!r} as top {k}"
)
self._save_checkpoint(trainer, filepath)
# update best checkpoint
if self.best_model_path == filepath:
self._save_checkpoint(
trainer,
self.format_checkpoint_name(monitor_candidates, self.CHECKPOINT_NAME_BEST)
)
if del_filepath is not None and filepath != del_filepath:
trainer.strategy.remove_checkpoint(del_filepath)
def _save_last_checkpoint(self, trainer: "pl.Trainer", monitor_candidates: Dict[str, Tensor]) -> None:
if not self.save_last:
return
filepath = self.format_checkpoint_name(monitor_candidates, self.CHECKPOINT_NAME_LAST)
# set the last model path before saving because it will be part of the state.
previous, self.last_model_path = self.last_model_path, filepath
self._save_checkpoint(trainer, filepath)
if previous and previous != filepath:
trainer.strategy.remove_checkpoint(previous)
class TrackNorms(pl.Callback):
# TODO do callbacks happen before or after the method in the main LightningModule?
# @rank_zero_only # needed?
def on_after_training_step(self, batch, batch_idx, trainer: pl.Trainer, pl_module: pl.LightningModule):
# Log extra metrics
metrics = {}
if hasattr(pl_module, "_grad_norms"):
metrics.update(pl_module._grad_norms)
self.log_dict(
metrics,
on_step=True,
on_epoch=False,
prog_bar=False,
add_dataloader_idx=False,
sync_dist=True,
)
def on_after_backward(self, trainer: pl.Trainer, pl_module: pl.LightningModule):
# example to inspect gradient information in tensorboard
if OmegaConf.select(trainer.hparams, 'trainer.track_grad_norms'): # TODO dot notation should work with omegaconf?
norms = {}
for name, p in pl_module.named_parameters():
if p.grad is None:
continue
# param_norm = float(p.grad.data.norm(norm_type))
param_norm = torch.mean(p.grad.data ** 2)
norms[f"grad_norm.{name}"] = param_norm
pl_module._grad_norms = norms