unslothai/unsloth · error · ValueError
ema_decay must be a number, got {self.ema_decay!r}
Error message
ema_decay must be a number, got {self.ema_decay!r} What it means
Raised when ema_decay cannot be converted with float(): the validator wraps the conversion in try/except (TypeError, ValueError) and echoes the offending value. EMA (exponential moving average of adapter weights) needs a numeric decay; values like a list, dict, or a non-numeric string fail here. Note float('') also raises ValueError, but a falsy value hits the `or 0.0` default first — only truthy non-numeric values land here.
Source
Thrown at studio/backend/core/training/diffusion_train_common.py:1091
# 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 "
f"run would silently start over and overwrite its output. Start a fresh run."
)
if save_steps:
raise ValueError(
f"save_steps is not supported for {resolved_family}: its trainer writes no "
f"checkpoint bundle. Leave it at 0; the adapter is still saved at the end."
)
try:
ema_decay = float(self.ema_decay or 0.0)
except (TypeError, ValueError) as exc:
raise ValueError(f"ema_decay must be a number, got {self.ema_decay!r}") from exc
# decay = 1.0 would freeze the shadow at its init forever; the update is shadow * decay + param * (1 - decay), so valid decays live in [0, 1).
if not 0.0 <= ema_decay < 1.0:
raise ValueError("ema_decay must be in [0, 1); 0 disables the EMA adapter")
# A blank cond_cache_dir (the Studio default when unset) means "off", not cwd.
cond_cache_dir = (
str(self.cond_cache_dir).strip() if self.cond_cache_dir is not None else ""
) or None
compile_transformer = str(self.compile_transformer or "auto").strip().lower()
if compile_transformer not in ("off", "on", "auto"):
raise ValueError("compile_transformer must be one of off / on / auto")
base_precision = str(self.base_precision or "nf4").strip().lower()
if base_precision not in ("nf4", "bf16", "int8", "fp8", "mxfp8", "auto"):
raise ValueError("base_precision must be one of nf4 / bf16 / int8 / fp8 / mxfp8 / auto")
# base_precision is a DiT-only lever, so the dense-mode gates apply only to the DiT families. The mode-name check above still runs for every family.
if resolved_family != "sdxl" and base_precision in ("bf16", "int8", "fp8", "mxfp8"):
if repo_is_prequantized(self.base_model):
raise ValueError(
f"base_precision={base_precision!r} needs a dense base repo, but "View on GitHub (pinned to 203007d190)
Solutions
- Pass a plain number, e.g. ema_decay=0.999, or None/0 to disable EMA.
- If your config nests EMA options, unwrap the scalar: ema_decay = cfg['ema']['decay'].
- The error echoes the value — check its repr to see what actually arrived.
Example fix
# before
config = TrainConfig(ema_decay={'decay': 0.999})
# after
config = TrainConfig(ema_decay=0.999) Defensive patterns
Strategy: validation
Validate before calling
def check_ema_decay(v) -> float:
if v in (None, "", 0, 0.0):
return 0.0 # disabled
if isinstance(v, (list, tuple, dict, bool)):
raise ValueError(f"ema_decay must be a number, got {v!r}")
return float(v) # raises for junk strings like 'auto' Type guard
def is_valid_ema_decay(v) -> bool:
if v in (None, ""):
return True
if isinstance(v, bool) or not isinstance(v, (int, float, str)):
return False
try:
float(v)
return True
except ValueError:
return False Try / catch
try:
session.submit_training(config)
except ValueError as e:
if "ema_decay must be a number" in str(e):
config.ema_decay = None # disable EMA rather than guess the intended value
session.submit_training(config)
else:
raise Prevention
- Pass a scalar number (e.g. 0.999) or None; never forward a nested EMA settings object into this one field.
- Unwrap structured configs at your boundary: cfg['ema']['decay'], not cfg['ema'].
- The error echoes the repr — log it to see what shape actually arrived.
When it happens
Trigger: Passing ema_decay='auto', '0.999 ' is fine but 'high' fails, [0.99] (list), {'decay': 0.99} (dict), or an object without __float__. Typically a deserialization mismatch where the field arrives as a nested JSON structure instead of a scalar.
Common situations: JSON config schemas that model ema as an object ({'enabled': true, 'decay': 0.99}) forwarded whole; string placeholders from UI; version upgrades that changed the field's expected shape.
Related errors
- save_steps / save_total_limit must be whole numbers, got {se
- ema_decay must be in [0, 1); 0 disables the EMA adapter
- gradient_accumulation_steps must be >= 1
- lora_rank must be >= 1
- lora_alpha must be >= 1 (a zero/negative alpha scales the ad
AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15).
Data as JSON: /api/errors/2e94ed357f03e7a5.
Report an issue: GitHub.