{"record":{"id":"a7cc27b01bd2e537","repo":"unslothai/unsloth","slug":"either-num-epochs-or-max-steps-must-be-0-both-c","errorCode":null,"errorMessage":"Either num_epochs or max_steps must be > 0; both cannot be 0.","messagePattern":"Either num_epochs or max_steps must be > 0; both cannot be 0\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"studio/backend/models/training.py","lineNumber":603,"sourceCode":"        # Streaming load_dataset does not accept HF slice syntax (probe-confirmed: ValueError: Bad\n        # split). Reject early with a clear message so the user knows to use a plain split name.\n        if self.dataset_streaming:\n            for field_name, split_val in (\n                (\"train_split\", self.train_split),\n                (\"eval_split\", self.eval_split),\n            ):\n                if split_val is not None and not valid_hf_dataset_split_name(split_val):\n                    raise ValueError(\n                        f\"dataset_streaming requires a plain split name in {field_name} \"\n                        f\"(got {split_val!r}); use a name such as 'train' or 'validation'.\"\n                    )\n        return self\n\n    @model_validator(mode = \"after\")\n    def _check_steps_or_epochs(self) -> \"TrainingStartRequest\":\n        # Each accepts 0 as \"use the other\"; both 0 means nothing to train.\n        if (self.max_steps is None or self.max_steps == 0) and self.num_epochs == 0:\n            raise ValueError(\"Either num_epochs or max_steps must be > 0; both cannot be 0.\")\n        return self\n\n    @model_validator(mode = \"after\")\n    def _validate_lora_variant_flags(self) -> \"TrainingStartRequest\":\n        # The frontend only ever sends one of these and never under Full Finetuning, but a direct\n        # API/YAML/CLI caller can bypass that. Nothing downstream breaks, but reject early for a\n        # clear error instead of a silently-ignored flag.\n        active = [\n            name\n            for name, enabled in (\n                (\"use_rslora\", self.use_rslora),\n                (\"use_loftq\", self.use_loftq),\n                (\"use_dora\", self.use_dora),\n            )\n            if enabled\n        ]\n        if len(active) > 1:\n            raise ValueError(","sourceCodeStart":585,"sourceCodeEnd":621,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/models/training.py#L585-L621","documentation":"Pydantic model validator _check_steps_or_epochs on TrainingStartRequest enforcing that a training run has a duration. max_steps of 0 or None is treated as 'use num_epochs' (and vice versa), but when both resolve to nothing to train (max_steps None or 0 AND num_epochs 0) the request is rejected immediately rather than starting a job that would exit after zero updates.","triggerScenarios":"POSTing a TrainingStartRequest with max_steps=0 (or omitting it) and num_epochs=0; a YAML/CLI config that relies on a default of 0 for both fields; unsetting max_steps while explicitly zeroing epochs.","commonSituations":"Config templates that default num_epochs to 0 expecting max_steps to be set, but the steps value is dropped or named wrong so it stays unset; UI forms where the user cleared both inputs.","solutions":["Set num_epochs > 0 (e.g. 1) or max_steps > 0 so at least one duration is non-zero.","If max_steps was omitted unintentionally, check the field name/type — a misspelled key leaves max_steps None, which combined with num_epochs=0 triggers this error.","Audit YAML/JSON configs for a global default of num_epochs: 0 that leaks into every request."],"exampleFix":"# before\nreq = TrainingStartRequest(max_steps=0, num_epochs=0, ...)\n\n# after\nreq = TrainingStartRequest(max_steps=500, num_epochs=0, ...)","handlingStrategy":"validation","validationCode":"def has_training_duration(payload: dict) -> bool:\n    max_steps = payload.get(\"max_steps\")\n    epochs_ok = (payload.get(\"num_epochs\") or 0) > 0\n    steps_ok = max_steps is not None and max_steps > 0\n    return epochs_ok or steps_ok\n\nassert has_training_duration({\"num_epochs\": 0, \"max_steps\": 0}) is False","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Make your UI/CLI require at least one non-zero duration before enabling submit.","Treat 0 as 'unset' consistently across your config tooling, mirroring the backend semantics."],"tags":["pydantic","training-config","validation"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}