Lightning-AI/pytorch-lightning · error · NotImplementedError

Support for `{epoch_end_name}` has been removed in v2.0.0. `

Error message

Support for `{epoch_end_name}` has been removed in v2.0.0. `{type(model).__name__}` implements this method. You can use the `on_{epoch_end_name}` hook instead. To access outputs, save them in-memory as instance attributes. You can find migration examples in https://github.com/Lightning-AI/pytorch-lightning/pull/16520.

What it means

Lightning 2.0 removed the `validation_epoch_end` / `test_epoch_end` hooks. If your module still defines them as callable methods, Lightning raises NotImplementedError at run start and points you to the `on_validation_epoch_end` / `on_test_epoch_end` hooks instead, with outputs stored as instance attributes.

Source

Thrown at src/lightning/pytorch/trainer/configuration_validator.py:111

    step_name = "validation_step" if stage == "val" else f"{stage}_step"
    has_step = is_overridden(step_name, model)

    # predict_step is not required to be overridden
    if stage == "predict":
        if model.predict_step is None:
            raise MisconfigurationException("`predict_step` cannot be None to run `Trainer.predict`")
        if not has_step and not is_overridden("forward", model):
            raise MisconfigurationException("`Trainer.predict` requires `forward` method to run.")
    else:
        # verify minimum evaluation requirements
        if not has_step:
            trainer_method = "validate" if stage == "val" else stage
            raise MisconfigurationException(f"No `{step_name}()` method defined to run `Trainer.{trainer_method}`.")

        # check legacy hooks are not present
        epoch_end_name = "validation_epoch_end" if stage == "val" else "test_epoch_end"
        if callable(getattr(model, epoch_end_name, None)):
            raise NotImplementedError(
                f"Support for `{epoch_end_name}` has been removed in v2.0.0. `{type(model).__name__}` implements this"
                f" method. You can use the `on_{epoch_end_name}` hook instead. To access outputs, save them in-memory"
                " as instance attributes."
                " You can find migration examples in https://github.com/Lightning-AI/pytorch-lightning/pull/16520."
            )


def __verify_manual_optimization_support(trainer: "pl.Trainer", model: "pl.LightningModule") -> None:
    if model.automatic_optimization:
        return
    if trainer.gradient_clip_val is not None and trainer.gradient_clip_val > 0:
        raise MisconfigurationException(
            "Automatic gradient clipping is not supported for manual optimization."
            f" Remove `Trainer(gradient_clip_val={trainer.gradient_clip_val})`"
            " or switch to automatic optimization."
        )
    if trainer.accumulate_grad_batches != 1:
        raise MisconfigurationException(

View on GitHub (pinned to 9fed5c27d2)

Solutions

  1. Delete `validation_epoch_end`/`test_epoch_end` and move aggregation logic into `on_validation_epoch_end`/`on_test_epoch_end`.
  2. Collect step outputs manually: append them to a list in `validation_step` and compute metrics in the epoch-end hook.
  3. If a dependency ships the legacy hook, upgrade that dependency or override the method with `pass` in your subclass.
  4. See migration examples in PR #16520 linked in the message.

Example fix

# before
class Model(L.LightningModule):
    def validation_step(self, batch, idx):
        return self(loss)
    def validation_epoch_end(self, outputs):
        self.log('val_loss', torch.stack(outputs).mean())
# after
class Model(L.LightningModule):
    def __init__(self):
        super().__init__()
        self.val_outputs = []
    def validation_step(self, batch, idx):
        loss = self.step(batch)
        self.val_outputs.append(loss)
        return loss
    def on_validation_epoch_end(self):
        self.log('val_loss', torch.stack(self.val_outputs).mean())
        self.val_outputs.clear()
Defensive patterns

Strategy: validation

Validate before calling

import inspect

def has_legacy_epoch_end(model):
    return any(callable(getattr(model, n, None)) and getattr(model, n, None) is not getattr(object, n, None)
               for n in ('validation_epoch_end', 'test_epoch_end'))

if has_legacy_epoch_end(model):
    raise RuntimeError('Migrate *_epoch_end hooks to on_*_epoch_end (Lightning 2.0)')

Type guard

def is_lightning2_compatible(model) -> bool:
    return not any(callable(getattr(model, n, None)) for n in ('validation_epoch_end', 'test_epoch_end'))

Try / catch

try:
    trainer.fit(model)
except NotImplementedError as e:
    if 'removed in v2.0.0' in str(e):
        # strip legacy hooks and re-run
        ...
    raise

Prevention

When it happens

Trigger: A LightningModule (including inherited base classes) defines `validation_epoch_end` or `test_epoch_end` and you call `trainer.fit/validate/test`. This includes code migrated from Lightning 1.x without updating the hooks.

Common situations: Upgrading a project from lightning <2.0 to >=2.0; old tutorials/examples; a shared corporate base model class still carrying the legacy hook.

Related errors


AI-assisted analysis of Lightning-AI/pytorch-lightning@9fed5c27d2 (2026-08-28). Data as JSON: /api/errors/8adb4ac87ee2bbf4. Report an issue: GitHub.