{"record":{"id":"139d87f5588abc8a","repo":"Lightning-AI/pytorch-lightning","slug":"in-manual-optimization-training-step-must-eithe","errorCode":null,"errorMessage":"In manual optimization, `training_step` must either return a Tensor or have no return.","messagePattern":"In manual optimization, `training_step` must either return a Tensor or have no return\\.","errorType":"exception","errorClass":"MisconfigurationException","httpStatus":null,"severity":"error","filePath":"src/lightning/pytorch/loops/optimization/manual.py","lineNumber":54,"sourceCode":"\n    It is created from the output of :meth:`~lightning.pytorch.core.LightningModule.training_step`.\n\n    Attributes:\n        extra: Anything returned by the ``training_step``.\n\n    \"\"\"\n\n    extra: dict[str, Any] = field(default_factory=dict)\n\n    @classmethod\n    def from_training_step_output(cls, training_step_output: STEP_OUTPUT) -> \"ManualResult\":\n        extra = {}\n        if isinstance(training_step_output, Mapping):\n            extra = training_step_output.copy()\n        elif isinstance(training_step_output, Tensor):\n            extra = {\"loss\": training_step_output}\n        elif training_step_output is not None:\n            raise MisconfigurationException(\n                \"In manual optimization, `training_step` must either return a Tensor or have no return.\"\n            )\n\n        if \"loss\" in extra:\n            # we detach manually as it's expected that it will have a `grad_fn`\n            extra[\"loss\"] = extra[\"loss\"].detach()\n\n        return cls(extra=extra)\n\n    @override\n    def asdict(self) -> dict[str, Any]:\n        return self.extra\n\n\n_OUTPUTS_TYPE = dict[str, Any]\n\n\nclass _ManualOptimization(_Loop):","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/Lightning-AI/pytorch-lightning/blob/9fed5c27d2a62ff0efd6c3573599921d6ff67c14/src/lightning/pytorch/loops/optimization/manual.py#L36-L72","documentation":"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.","triggerScenarios":"`self.automatic_optimization = False` plus `training_step` returning `loss, logits` or `loss.item()`; GAN training loops returning tuples of discriminator/generator losses.","commonSituations":"Writing manual optimization for GANs or RL where multiple optimizers step manually; returning unpacked tuples by habit from vanilla PyTorch code.","solutions":["Return only the loss tensor, a dict, or nothing at all","For multiple losses, return a dict: `return {'loss_g': loss_g, 'loss_d': loss_d}` and step optimizers manually inside training_step"],"exampleFix":"# before\nself.automatic_optimization = False\ndef training_step(self, batch, batch_idx):\n    ...\n    return loss_d, loss_g  # tuple -> raises\n\n# after\ndef training_step(self, batch, batch_idx):\n    ...\n    self.opt_d.step(); self.opt_g.step()\n    return {'loss_d': loss_d, 'loss_g': loss_g}","handlingStrategy":"type-guard","validationCode":"out = self.training_step(batch, batch_idx)\nassert out is None or isinstance(out, (torch.Tensor, Mapping))","typeGuard":"def valid_manual_step_output(out) -> bool:\n    return out is None or isinstance(out, (torch.Tensor, Mapping))","tryCatchPattern":null,"preventionTips":["In manual optimization return only Tensor, dict, or None; step optimizers inside training_step","Return a dict of named losses for GAN-style multi-optimizer loops"],"tags":["pytorch-lightning","manual-optimization","training-step","return-type"],"backgroundTag":"invalid-training-step-return-type","analyzedSha":"9fed5c27d2a62ff0efd6c3573599921d6ff67c14","analyzedAt":"2026-08-28T11:52:41.083Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}