Source code for pimm.engines.hooks.checkpoint

"""Checkpoint hooks that delegate checkpoint semantics to CheckpointManager."""

from __future__ import annotations

from pimm.utils.checkpoints import CheckpointManager
from pimm.utils.comm import is_main_process

from .builder import HOOKS
from .default import HookBase


def _is_last_epoch_step(trainer):
    """Return whether the current optimizer step finishes its epoch."""
    current_iter = int(trainer.comm_info.get("iter", -1)) + 1
    iter_per_epoch = trainer.comm_info.get("iter_per_epoch")
    if iter_per_epoch is None:
        try:
            iter_per_epoch = len(trainer.train_loader)
        except TypeError:
            return False
    return current_iter >= int(iter_per_epoch)


[docs] @HOOKS.register_module() class CheckpointSaver(HookBase): """Save epoch/metric-oriented checkpoints during and after training. Maintains a running global-step counter (seeded from resumed trainer progress in ``before_train``) and, in ``after_step``, saves a checkpoint whenever either cadence fires: a periodic save every ``save_freq`` steps, or an evaluator step every ``evaluator_every_n_steps`` steps (only when ``cfg.evaluate`` is set). On an evaluator step it also reads the evaluator's ``current_metric_value`` from ``trainer.comm_info`` and, if it improves on ``trainer.best_metric_value``, writes a ``model_best`` snapshot. A final checkpoint is written in ``after_train``. All actual save logic is delegated to :class:`~pimm.utils.checkpoints.CheckpointManager`. Registered as ``CheckpointSaver``. Args: save_freq (int, optional): Save a rolling checkpoint every this many global steps. ``None`` disables periodic saving. Defaults to ``None``. evaluator_every_n_steps (int, optional): Treat every this many steps as an evaluation step, consulting the metric and updating ``model_best`` on improvement. Requires ``cfg.evaluate``. ``None`` disables. Defaults to ``None``. Note: Runs on **all** ranks because a standard-format (DCP) save is a collective operation; the save decision is gated only on rank-consistent step counters, never on the metric (which some evaluators publish on rank 0 only), so ranks cannot diverge and deadlock. Place any evaluator hook **before** this saver in the ``hooks`` list so the metric is available when the saver runs. Example: Add to ``cfg.hooks`` after an evaluator; it saves a rolling checkpoint every ``save_freq`` steps and, on evaluator steps, promotes ``model_best`` whenever the evaluator's metric improves: .. code-block:: python hooks = [ dict(type="SemSegEvaluator", every_n_steps=1000), dict(type="CheckpointSaver", save_freq=1000, evaluator_every_n_steps=1000), ] # → every 1000 steps writes the rolling model/last/ checkpoint; on each # eval step reads comm_info["current_metric_value"] and, if it beats # trainer.best_metric_value, writes model/model_best.pth; writes a # final checkpoint in after_train """ def __init__(self, save_freq=None, evaluator_every_n_steps=None): """Configure periodic saves and optional metric-driven best snapshots.""" self.save_freq = save_freq self.evaluator_every_n_steps = evaluator_every_n_steps self.step_count = 0
[docs] def before_train(self): """Seed internal step count from resumed trainer progress.""" self.checkpoint_manager = CheckpointManager(self.trainer) self._pending_epoch_save = None self.step_count = int(getattr(self.trainer, "global_step", 0) or ( self.trainer.start_epoch * len(self.trainer.train_loader) + self.trainer.start_iter ))
def _update_best(self, is_eval_step): """Return is_best from the metric, when this rank has one. Some evaluators publish ``current_metric_value`` on rank 0 only (they return early on non-main ranks), so the key may be absent here — in which case there is no "best" decision to make on this rank. The collective save itself is gated on rank-consistent state in ``after_step``, never on this metric, so ranks cannot diverge there. """ if not is_eval_step or "current_metric_value" not in self.trainer.comm_info: return False current_metric_value = self.trainer.comm_info["current_metric_value"] current_metric_name = self.trainer.comm_info["current_metric_name"] is_best = False if current_metric_value > self.trainer.best_metric_value: self.trainer.best_metric_value = current_metric_value is_best = True if is_main_process(): self.trainer.logger.info( "Best validation {} updated to: {:.4f}".format( current_metric_name, current_metric_value ) ) if is_main_process(): self.trainer.logger.info( "Currently Best {}: {:.4f}".format( current_metric_name, self.trainer.best_metric_value ) ) return is_best
[docs] def after_step(self): """Save after scheduled steps or after evaluator metric updates.""" self.step_count += 1 # The save decision MUST be identical on every rank: a standard-format # save is collective, so if some ranks save and others don't the run # deadlocks. Gate only on rank-consistent state (step counters) — never # on `"current_metric_value" in comm_info`, which gather-to-rank-0 # evaluators publish on rank 0 only. is_eval_step = bool( self.trainer.cfg.evaluate and self.evaluator_every_n_steps and self.step_count % self.evaluator_every_n_steps == 0 ) is_save_step = bool(self.save_freq and self.step_count % self.save_freq == 0) if not is_eval_step and not is_save_step: return is_best = self._update_best(is_eval_step) save_kwargs = dict( is_best=is_best, step_count=self.step_count, save_freq=self.save_freq, ) if _is_last_epoch_step(self.trainer): # Let after-epoch loggers add their metrics to the same W&B row # before checkpoint_state() commits it and records the rewind cursor. self._pending_epoch_save = save_kwargs return # Called on ALL ranks: a standard-format save is a collective op. self.checkpoint_manager.save_epoch_checkpoint(**save_kwargs)
[docs] def after_epoch(self): """Save a last-step checkpoint after epoch-end metrics are logged.""" if self._pending_epoch_save is None: return self.checkpoint_manager.save_epoch_checkpoint(**self._pending_epoch_save) self._pending_epoch_save = None
[docs] def after_train(self): """Persist a final checkpoint when training finishes.""" if is_main_process(): self.trainer.logger.info("Saving final checkpoint") self.checkpoint_manager.save_epoch_checkpoint( is_best=False, step_count=self.step_count, save_freq=self.save_freq, )
[docs] @HOOKS.register_module() class CheckpointLoader(HookBase): """Load model weights and, when requested, resume optimizer/train state. In ``before_train``, before the trainer enters the loop, delegates to :meth:`~pimm.utils.checkpoints.CheckpointManager.load_weight_and_resume`, which loads weights from ``cfg.weight`` (warm-start) or resumes a full run from ``cfg.resume``. State-dict keys are rewritten by the configured replacement rules before loading, which is how a checkpoint trained under one module layout is mapped onto another (e.g. wrapping a backbone under ``module.model.backbone``). Registered as ``CheckpointLoader``. Args: keywords (str): Single substring to match in checkpoint keys when using the scalar form. Defaults to ``""`` (matches everything / no-op). replacement (str, optional): String to substitute for ``keywords`` in matched keys. When ``None``, falls back to ``keywords`` (i.e. no rewrite). Defaults to ``None``. replacements (dict, optional): A ``{keyword: replacement}`` mapping to apply several key rewrites in a single ``load_state_dict`` call, so missing/unexpected keys are reported truthfully. Overlapping keywords are resolved by longest match, so dict order is irrelevant. Mutually exclusive in practice with the scalar form. Must be a dict or a ``TypeError`` is raised. Defaults to ``None``. strict (bool): Whether to require an exact key match when loading model weights (passed through to ``load_state_dict``). Defaults to ``False``. Note: Warm-starting (``cfg.weight``) loads model weights only; full resume (``cfg.resume``) additionally restores optimizer, scheduler, and step state. After loading, judge success by inspecting the reported missing / unexpected keys rather than assuming silence means success. Example: Add to ``cfg.hooks``; once in ``before_train`` it loads ``cfg.weight`` (warm-start) or ``cfg.resume`` (full resume), rewriting state-dict keys by the given rules before ``load_state_dict``: .. code-block:: python hooks = [ dict(type="CheckpointLoader", replacements={ "module.backbone": "module.model.backbone", "module.decoder": "module.model.decoder", }), ] # → before the loop, loads cfg.weight/cfg.resume after renaming # "module.backbone.*" -> "module.model.backbone.*" (etc.); inspect the # reported missing/unexpected keys to confirm the load took """ def __init__(self, keywords="", replacement=None, replacements=None, strict=False): """Configure key replacement and model weight strictness. Pass ``replacements`` as a ``{keyword: replacement}`` dict to apply several key rewrites in a single load (one ``load_state_dict``, so missing/unexpected keys are reported truthfully), e.g.:: dict(type="CheckpointLoader", replacements={ "module.backbone": "module.model.backbone", "module.decoder": "module.model.decoder", }) Overlapping keywords are resolved by longest match, so dict order does not matter. The scalar ``keywords``/``replacement`` form remains for the common single-rule case. """ if replacements is not None: if not isinstance(replacements, dict): raise TypeError( "CheckpointLoader.replacements must be a {keyword: replacement} " f"dict, got {type(replacements).__name__}" ) self.rules = list(replacements.items()) else: self.rules = [(keywords, replacement if replacement is not None else keywords)] self.strict = strict
[docs] def before_train(self): """Load configured weights before the trainer enters the train loop.""" CheckpointManager(self.trainer).load_weight_and_resume( rules=self.rules, strict=self.strict, )
[docs] @HOOKS.register_module() class CheckpointSaverIteration(HookBase): """Save iteration-oriented checkpoints on a pure global-step cadence. Like :class:`CheckpointSaver`, but the save decision depends only on the global step: in ``after_step`` it saves whenever ``step_count % save_freq == 0``. When ``cfg.evaluate`` is set and an evaluator has published ``current_metric_value`` in ``trainer.comm_info``, an improving metric also updates ``trainer.best_metric_value`` and writes ``model_best``. A final checkpoint is written in ``after_train``. Save logic is delegated to :class:`~pimm.utils.checkpoints.CheckpointManager`. Registered as ``CheckpointSaverIteration``. Args: save_freq (int, optional): Save every this many global steps. ``None`` disables periodic saving. Defaults to ``None``. save_iter_checkpoints (bool): If ``True``, keep a distinct per-step checkpoint copy at each save instead of only rolling the latest. Defaults to ``False``. backend (str, optional): Deprecated selector for the on-disk format, superseded by the top-level ``checkpoint_format`` config key. ``"dcp"`` maps to the standard (hybrid) format and ``"torch"`` to the legacy format; ``None`` resolves to standard. Defaults to ``None``. Note: Called on **all** ranks because a standard-format save is collective; because the cadence is purely step-based it is identical across ranks. Place any evaluator hook **before** this saver so the metric is available for the ``model_best`` decision. Example: Add to ``cfg.hooks``; in ``after_step`` it saves on a pure global-step cadence: .. code-block:: python hooks = [dict(type="CheckpointSaverIteration", save_freq=5000)] # → writes a rolling checkpoint every 5000 global steps (and a final # one in after_train); with cfg.evaluate set and an evaluator's # comm_info["current_metric_value"], an improving metric also updates # trainer.best_metric_value and writes model_best """ def __init__(self, save_freq=None, save_iter_checkpoints=False, backend=None): """Configure save cadence, iteration copies, and checkpoint format.""" self.save_freq = save_freq self.save_iter_checkpoints = save_iter_checkpoints self.backend = backend self.step_count = 0
[docs] def before_train(self): """Seed internal step count from resumed trainer progress.""" self.checkpoint_manager = CheckpointManager(self.trainer) self._pending_epoch_save = None self.step_count = int(getattr(self.trainer, "global_step", 0) or ( self.trainer.start_epoch * len(self.trainer.train_loader) + self.trainer.start_iter ))
def _update_best(self): """Return is_best, updating best_metric_value identically on every rank.""" if not ( self.trainer.cfg.evaluate and "current_metric_value" in self.trainer.comm_info ): return False current_metric_value = self.trainer.comm_info["current_metric_value"] current_metric_name = self.trainer.comm_info["current_metric_name"] is_best = False if current_metric_value > self.trainer.best_metric_value: self.trainer.best_metric_value = current_metric_value is_best = True if is_main_process(): self.trainer.logger.info( "Best validation {} updated to: {:.4f}".format( current_metric_name, current_metric_value ) ) if is_main_process(): self.trainer.logger.info( "Currently Best {}: {:.4f}".format( current_metric_name, self.trainer.best_metric_value ) ) return is_best
[docs] def after_step(self): """Save whenever the configured global-step cadence is reached.""" self.step_count += 1 if not self.save_freq or self.step_count % self.save_freq != 0: return if is_main_process(): self.trainer.logger.info(f"Saving checkpoint at global step {self.step_count}") is_best = self._update_best() save_kwargs = dict( backend=self.backend, is_best=is_best, step_count=self.step_count, save_freq=self.save_freq, save_iter_checkpoints=self.save_iter_checkpoints, ) if _is_last_epoch_step(self.trainer): self._pending_epoch_save = save_kwargs return self.checkpoint_manager.save_iteration_checkpoint(**save_kwargs)
[docs] def after_epoch(self): """Save a last-step checkpoint after epoch-end metrics are logged.""" if self._pending_epoch_save is None: return self.checkpoint_manager.save_iteration_checkpoint( **self._pending_epoch_save, ) self._pending_epoch_save = None
[docs] def after_train(self): """Persist a final checkpoint after the last training step.""" if is_main_process(): self.trainer.logger.info("Saving final checkpoint") self.checkpoint_manager.save_iteration_checkpoint( backend=self.backend, is_best=False, step_count=self.step_count, save_freq=self.save_freq, save_iter_checkpoints=self.save_iter_checkpoints, )