unslothai/unsloth · error · ValueError
Either num_epochs or max_steps must be > 0; both cannot be 0
Error message
Either num_epochs or max_steps must be > 0; both cannot be 0.
What it means
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.
Source
Thrown at studio/backend/models/training.py:603
# Streaming load_dataset does not accept HF slice syntax (probe-confirmed: ValueError: Bad
# split). Reject early with a clear message so the user knows to use a plain split name.
if self.dataset_streaming:
for field_name, split_val in (
("train_split", self.train_split),
("eval_split", self.eval_split),
):
if split_val is not None and not valid_hf_dataset_split_name(split_val):
raise ValueError(
f"dataset_streaming requires a plain split name in {field_name} "
f"(got {split_val!r}); use a name such as 'train' or 'validation'."
)
return self
@model_validator(mode = "after")
def _check_steps_or_epochs(self) -> "TrainingStartRequest":
# Each accepts 0 as "use the other"; both 0 means nothing to train.
if (self.max_steps is None or self.max_steps == 0) and self.num_epochs == 0:
raise ValueError("Either num_epochs or max_steps must be > 0; both cannot be 0.")
return self
@model_validator(mode = "after")
def _validate_lora_variant_flags(self) -> "TrainingStartRequest":
# The frontend only ever sends one of these and never under Full Finetuning, but a direct
# API/YAML/CLI caller can bypass that. Nothing downstream breaks, but reject early for a
# clear error instead of a silently-ignored flag.
active = [
name
for name, enabled in (
("use_rslora", self.use_rslora),
("use_loftq", self.use_loftq),
("use_dora", self.use_dora),
)
if enabled
]
if len(active) > 1:
raise ValueError(View on GitHub (pinned to 203007d190)
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.
Example fix
# before req = TrainingStartRequest(max_steps=0, num_epochs=0, ...) # after req = TrainingStartRequest(max_steps=500, num_epochs=0, ...)
Defensive patterns
Strategy: validation
Validate before calling
def has_training_duration(payload: dict) -> bool:
max_steps = payload.get("max_steps")
epochs_ok = (payload.get("num_epochs") or 0) > 0
steps_ok = max_steps is not None and max_steps > 0
return epochs_ok or steps_ok
assert has_training_duration({"num_epochs": 0, "max_steps": 0}) is False Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Provide either content_base64 or file_ids, not both
- Provide either content_base64 or file_ids
- file_ids must not be empty
- block_id is required when using file_ids
- file_names must be provided and same length as file_ids
AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15).
Data as JSON: /api/errors/a7cc27b01bd2e537.
Report an issue: GitHub.