{"record":{"id":"e95bbdc55a6ecd1a","repo":"hiyouga/LlamaFactory","slug":"dynamic-batching-requires-max-steps-because-it","errorCode":null,"errorMessage":"`dynamic_batching` requires `max_steps` because it is step-driven.","messagePattern":"`dynamic_batching` requires `max_steps` because it is step-driven\\.","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/llamafactory/v1/config/training_args.py","lineNumber":196,"sourceCode":"\n            register_deepspeed_dist_config(self.dist_config)\n        except ImportError:\n            pass\n\n        # The optimizer learning rate has a single source of truth: ``learning_rate``.\n        # Propagate it into ``optim_config[\"lr\"]`` so optimizer plugins (e.g. Muon) pick it up\n        # via ``optim_config.get(\"lr\")`` without each plugin needing a separate ``learning_rate`` arg.\n        if self.optim_config is not None:\n            if \"lr\" in self.optim_config:\n                logger.warning_rank0(\n                    \"`optim_config.lr` is overridden by `learning_rate`; set the learning rate via \"\n                    \"`learning_rate` instead and remove `lr` from `optim_config`.\"\n                )\n            self.optim_config[\"lr\"] = self.learning_rate\n\n        if str(self.batching_strategy) == str(BatchingStrategy.DYNAMIC_BATCHING):\n            if self.max_steps is None or self.max_steps <= 0:\n                raise ValueError(\"`dynamic_batching` requires `max_steps` because it is step-driven.\")\n            if self.save_epochs is not None:\n                raise ValueError(\"`save_epochs` is not supported with `dynamic_batching`; use `save_steps` instead.\")\n","sourceCodeStart":178,"sourceCodeEnd":199,"githubUrl":"https://github.com/hiyouga/LlamaFactory/blob/f28afaf6355af515454dfb16c97d728307c93897/src/llamafactory/v1/config/training_args.py#L178-L199","documentation":"In v1 `TrainingArguments.__post_init__`, choosing `batching_strategy: dynamic_batching` requires `max_steps` to be a positive integer because dynamic batching has no fixed epoch boundary — training length is driven purely by optimizer steps. Without `max_steps` the trainer could not know when to stop or schedule.","triggerScenarios":"Setting `batching_strategy: dynamic_batching` in v1 training args while `max_steps` is None (default) or <= 0, typically because the config relies on `epochs` instead.","commonSituations":"Switching an epoch-based config to dynamic batching without adding a step budget; setting `max_steps: -1` (HF convention for 'unset') which fails the <= 0 check.","solutions":["Set an explicit positive `max_steps` appropriate for your token budget (e.g. `max_steps: 1000`)","Replace `epochs`-based stopping with `max_steps` when using dynamic batching","If you need epoch semantics, keep a static/padding strategy instead of `dynamic_batching`"],"exampleFix":"# before (yaml)\ntraining:\n  batching_strategy: dynamic_batching\n  epochs: 3\n\n# after (yaml)\ntraining:\n  batching_strategy: dynamic_batching\n  max_steps: 1000","handlingStrategy":"validation","validationCode":"def validate_dynamic_batching(cfg: dict) -> None:\n    if str(cfg.get(\"batching_strategy\", \"\")) == \"dynamic_batching\":\n        steps = cfg.get(\"max_steps\")\n        if steps is None or steps <= 0:\n            raise SystemExit(\"dynamic_batching requires positive max_steps\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat dynamic batching as step-driven: always pair it with max_steps","Lint configs for (dynamic_batching, epochs) and (dynamic_batching, missing max_steps) combos","Document the step budget next to the batching strategy in team templates"],"tags":["v1","configuration","batching","training-args"],"backgroundTag":null,"analyzedSha":"f28afaf6355af515454dfb16c97d728307c93897","analyzedAt":"2026-08-14T21:57:28.298Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}