Lightning-AI/pytorch-lightning · error · MisconfigurationException

In manual optimization, `training_step` must either return…

Error message

In manual optimization, `training_step` must either return a Tensor or have no return.

What it means

Raised by ClosureResult.from_training_step_output in the manual optimization path when training_step returns something that is neither a Tensor, a Mapping, nor None. Manual optimization tolerates dict returns (treated as extra logging values with an optional 'loss') but any other type (tuple, list, float) is rejected.

Solutions

  1. Return only the loss tensor, a dict, or nothing at all
  2. For multiple losses, return a dict: `return {'loss_g': loss_g, 'loss_d': loss_d}` and step optimizers manually inside training_step

Example fix

# before
self.automatic_optimization = False
def training_step(self, batch, batch_idx):
    ...
    return loss_d, loss_g  # tuple -> raises

# after
def training_step(self, batch, batch_idx):
    ...
    self.opt_d.step(); self.opt_g.step()
    return {'loss_d': loss_d, 'loss_g': loss_g}
Defensive patterns

Strategy: type-guard

Validate before calling

out = self.training_step(batch, batch_idx)
assert out is None or isinstance(out, (torch.Tensor, Mapping))

Type guard

def valid_manual_step_output(out) -> bool:
    return out is None or isinstance(out, (torch.Tensor, Mapping))

Prevention

When it happens

Trigger: `self.automatic_optimization = False` plus `training_step` returning `loss, logits` or `loss.item()`; GAN training loops returning tuples of discriminator/generator losses.

Common situations: Writing manual optimization for GANs or RL where multiple optimizers step manually; returning unpacked tuples by habit from vanilla PyTorch code.

Related errors


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

Appendix: source

Thrown at src/lightning/pytorch/loops/optimization/manual.py:54

    It is created from the output of :meth:`~lightning.pytorch.core.LightningModule.training_step`.

    Attributes:
        extra: Anything returned by the ``training_step``.

    """

    extra: dict[str, Any] = field(default_factory=dict)

    @classmethod
    def from_training_step_output(cls, training_step_output: STEP_OUTPUT) -> "ManualResult":
        extra = {}
        if isinstance(training_step_output, Mapping):
            extra = training_step_output.copy()
        elif isinstance(training_step_output, Tensor):
            extra = {"loss": training_step_output}
        elif training_step_output is not None:
            raise MisconfigurationException(
                "In manual optimization, `training_step` must either return a Tensor or have no return."
            )

        if "loss" in extra:
            # we detach manually as it's expected that it will have a `grad_fn`
            extra["loss"] = extra["loss"].detach()

        return cls(extra=extra)

    @override
    def asdict(self) -> dict[str, Any]:
        return self.extra


_OUTPUTS_TYPE = dict[str, Any]


class _ManualOptimization(_Loop):

View on GitHub (pinned to 9fed5c27d2)