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

  1. Set num_epochs > 0 (e.g. 1) or max_steps > 0 so at least one duration is non-zero.
  2. 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.
  3. 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

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


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/a7cc27b01bd2e537. Report an issue: GitHub.