GradScaler
GradScaler(init_scale: float = 2.0 ** 16, growth_factor: float = 2.0, backoff_factor: float = 0.5, growth_interval: int = 2000, enabled: bool = True)Dynamic loss-scaling helper for mixed-precision training.
Mixed-precision training keeps activations and weights in fp16 to
halve memory bandwidth and exploit fp16-fast hardware paths, but
fp16's narrow dynamic range causes small gradients to underflow to
zero — the network stops learning. GradScaler works
around this by multiplying the loss by a large constant
before backpropagation:
The scaled gradients sit comfortably inside fp16's representable range; before the optimizer step they are unscaled by in fp32 so the update is mathematically equivalent to ordinary training.
The scale itself is adapted dynamically. After every step the
unscaled gradients are checked for inf / NaN:
- Overflow detected — the step is skipped and is
multiplied by
backoff_factor(typically0.5). - No overflow for
growth_intervalconsecutive steps — is multiplied bygrowth_factor(typically2.0).
This produces a sawtooth schedule that tracks the largest scale the current gradient distribution can tolerate.
Parameters
init_scalefloat= 2**16scale.growth_factorfloat= 2.0growth_interval
consecutive non-overflowing steps. Must be > 1.0.backoff_factorfloat= 0.5inf / NaN gradient is
detected. Must be in (0, 1).growth_intervalint= 2000enabledbool= TrueFalse the scaler degenerates into a transparent
pass-through — scale returns its input unchanged,
step calls the optimizer directly, and update
is a no-op.Notes
The canonical training-loop pattern is scale-loss, then step, then update:
scalemultiplies the loss by beforebackward()so the gradients land safely inside fp16 range.stepunscales the gradients, checks forinf/NaN, and either runsoptimizer.step()or skips the update.updateadjusts according to the growth / backoff schedule for the next iteration.
Examples
>>> scaler = GradScaler()
>>> for x, y in dataloader:
... with autocast():
... out = model(x)
... loss = loss_fn(out, y)
... scaler.scale(loss).backward()
... scaler.step(optimizer)
... scaler.update()Used by 2
Constructors
1__init__
→None__init__(init_scale: float = 2.0 ** 16, growth_factor: float = 2.0, backoff_factor: float = 0.5, growth_interval: int = 2000, enabled: bool = True)Initialize the scaler state.
Parameters
init_scalefloat= 2**16scale.growth_factorfloat= 2.0growth_interval
consecutive non-overflowing steps.backoff_factorfloat= 0.5growth_intervalint= 2000enabledbool= TrueFalse the scaler is a transparent pass-through.Instance methods
6Return the current scale factor.
Load state from a dict.
Return serializable state dict.
Update the scale factor.
If a scale is provided, it is set directly. Otherwise, the scale is grown if no overflow was found for growth_interval steps, or reduced if overflow was found.
Parameters
new_scalefloat | None= NoneIn-place ops
1Divide gradients by the current scale in-place.
Should be called before gradient clipping.
Args: optimizer: The optimizer whose parameters' grads will be unscaled.
Notes
The inverse-scale coefficient is always built in float32 even
when the gradient is float16. At init_scale=2**16=65536 the
unscale factor is 1/65536 ≈ 1.526e-5, which is subnormal
in float16 (the smallest normal F16 value is 6.1e-5). Apple
Silicon's Metal backend flushes F16 subnormals to zero, so a
naive full(shape, inv_scale, F16, ...) coefficient becomes the
zero tensor and every unscaled gradient collapses to 0 → the
model stops learning even though the wall-clock looks great.
Casting the gradient to F32 first (via MLX's automatic
promotion on mixed-dtype multiply) keeps the unscale exact and
also gives the optimizer F32 gradients to update the F32
parameter slots with — matching the reference framework's AMP
path.