The problem
Training in FP16 (16-bit floating point) halves the memory and is much faster, but FP16 has a small range: its largest number is 65504, and numbers below about round to exactly 0. Many gradients are that small, and they would simply vanish.
Loss scaling fixes it: multiply the loss, and therefore every gradient, by a large factor before the backward pass, so that small gradients stay representable; then divide the gradients by , in FP32 (32-bit floating point), before the update. You will simulate this, and then write the dynamic scaler that chooses during training, as PyTorch does.
Part 1: a round trip through FP16
Simulate a backward pass in FP16: multiply by the scale, store in FP16, read back in FP32, divide by the scale.
Example. Three gradients, , and .
- With
scale = 1: comes back as 0 (lost), as (slightly rounded), as .- With
scale = 65536(): comes back as (saved), as , and asinf: is above 65504, so it overflows.The scale has to be large enough for the small gradients and small enough for the large ones, and the right value changes during training. Hence the dynamic scaler.
Part 2: the dynamic scaler
The scaler keeps the current scale and a count of good steps in a row. At every step it receives the gradients of the scaled loss, in FP16:
- If any value is
infornan, the scale was too large: multiplyscalebybackoff_factor(0.5 by default), reset the count, and returnNone, meaning "skip this step". - Otherwise, return the gradients unscaled: cast to FP32 and divided by the scale used for this step. Count one good step; after
growth_intervalgood steps in a row, multiplyscalebygrowth_factor(2 by default) and reset the count, to try a larger scale again.
Example.
init_scale = 8andgrowth_interval = 2:
step gradients received returned scale after the step 1 [8, 16][1, 2]8 (1 good step) 2 [inf]None4 (halved, count reset) 3 [4][1]4 (1 good step) 4 [4][1]8 (2 good steps: doubled) In step 4 the gradient is divided by 4, the scale used to produce it, and only then is the scale doubled.
What to implement
fp16_roundtrip(g, scale): multiply the FP32 arraygbyscale, cast tonp.float16, cast back tonp.float32and divide byscale.class DynamicLossScaler(init_scale=2.0**16, growth_factor=2.0, backoff_factor=0.5, growth_interval=2000), with an attributescale(starting atinit_scale) and a methodstep(scaled_grads), wherescaled_gradsis a list of FP16 arrays. It returnsNoneor the list of unscaled FP32 arrays, as in part 2.
Requirements
- Unscale with the scale that was used for this step, before changing it.
- The defaults are those of PyTorch's
torch.amp.GradScaler.
Hints
np.isfinite(g).all()checks one array forinfandnan.- Do the division in FP32: dividing in FP16 can underflow to 0 again.