unslothai/unsloth · error · ValueError
save_steps must be >= 0 (0 disables periodic checkpoints)
Error message
save_steps must be >= 0 (0 disables periodic checkpoints)
What it means
The validator rejected a negative save_steps. save_steps controls how often periodic checkpoints are written during the loop; 0 is the documented 'disable periodic checkpoints' value, and negatives are meaningless. Like the other checkpoint knobs, it is checked before the route evicts resident GPU models.
Source
Thrown at studio/backend/core/training/diffusion_train_common.py:1062
)
if str(self.lr_scheduler) not in _LR_SCHEDULERS:
raise ValueError(
f"lr_scheduler must be one of {', '.join(sorted(_LR_SCHEDULERS))}; "
f"got {self.lr_scheduler!r}"
)
if not 1 <= int(self.cache_variants) <= 16:
raise ValueError("cache_variants must be between 1 and 16")
# Checkpointing knobs. Rejected here, before the route evicts resident GPU models, rather than deep in the loop.
try:
save_steps = int(self.save_steps or 0)
save_total_limit = int(self.save_total_limit or 0)
except (TypeError, ValueError) as exc:
raise ValueError(
f"save_steps / save_total_limit must be whole numbers, got "
f"{self.save_steps!r} / {self.save_total_limit!r}"
) from exc
if save_steps < 0:
raise ValueError("save_steps must be >= 0 (0 disables periodic checkpoints)")
if save_total_limit < 0:
raise ValueError("save_total_limit must be >= 0 (0 keeps every checkpoint)")
# A blank resume path (the Studio default when the field is present but unset) means "fresh run", not the outputs root.
resume_from_checkpoint = (
str(self.resume_from_checkpoint).strip()
if self.resume_from_checkpoint is not None
else ""
) or None
# The H3 loop does not checkpoint: it neither writes a resume bundle nor restores one.
# Accepting these two silently was the dangerous part -- a caller handing over a resume
# bundle got a FRESH optimization that then overwrote the outputs it was meant to
# continue, and one asking for periodic saves got none, both discovered only after an
# expensive run. Refuse in validation, where it costs nothing, until the loop supports it.
if resolved_family in CHECKPOINTLESS_FAMILIES:
if resume_from_checkpoint:
raise ValueError(
f"resume_from_checkpoint is not supported for {resolved_family}: its trainer "
f"writes no checkpoint bundle, so there is nothing to continue from and the "View on GitHub (pinned to 203007d190)
Solutions
- Use 0 to disable periodic checkpointing — not a negative number.
- Set a positive step count (e.g. 500) for periodic saves.
- Clamp derived values: save_steps = max(0, computed).
Example fix
# before config = TrainConfig(save_steps=-1) # after config = TrainConfig(save_steps=0)
Defensive patterns
Strategy: validation
Validate before calling
def check_save_steps(v) -> int:
n = int(v or 0)
if n < 0:
raise ValueError(f"save_steps must be >= 0 (0 disables periodic checkpoints), got {v!r}")
return n Type guard
def is_valid_save_steps(v) -> bool:
try:
return int(v or 0) >= 0
except (TypeError, ValueError):
return False Try / catch
try:
session.submit_training(config)
except ValueError as e:
if "save_steps must be >= 0" in str(e):
config.save_steps = 0
session.submit_training(config)
else:
raise Prevention
- Use 0 (not -1) as the 'periodic checkpoints off' sentinel here.
- Clamp derived cadences: save_steps = max(0, total_steps // n).
- Purge -1 sentinels when porting configs from other trainers.
When it happens
Trigger: save_steps=-100, or a string like '-1' that int() happily parses. Often from code computing save_steps = total_steps // n where n overshoots and yields a negative remainder, or from configs using -1 as a 'disabled' sentinel from another tool's convention.
Common situations: Migrating configs from frameworks where -1 means 'off'; arithmetic that derives save cadence from run length and can go negative for short runs.
Related errors
- save_steps / save_total_limit must be whole numbers, got {se
- save_total_limit must be >= 0 (0 keeps every checkpoint)
- save_steps is not supported for {resolved_family}: its train
- gradient_accumulation_steps must be >= 1
- lora_rank must be >= 1
AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15).
Data as JSON: /api/errors/f21d4365e7a65342.
Report an issue: GitHub.