Implement the latent normalisation that sits between Stable Diffusion's VAE and its U-Net, including the empirical derivation of the famous 0.18215 constant.
A latent diffusion model runs the diffusion process on VAE latents, not pixels. But the forward process assumes unit-variance data: the schedule is built so that xt has variance αˉt⋅Var(x0)+(1−αˉt), which only lands on 1 at t=T if Var(x0)=1. A raw VAE latent has a standard deviation of roughly 5.5, so it must be normalised first:
zscaled=(z−shift)⋅s,z=szscaled+shift
The scaling factor is estimated once from the training set as s=1/std(z) -- which for SD 1.x came out at 0.18215, i.e. a latent std of about 5.49. SD 3 and Flux add a non-zero shift_factor to centre the latents too.
The pipeline is: encode, scale, diffuse; then at the end unscale and decode. Forgetting either half is a classic bug -- skip the scale and the U-Net sees data at 5x the variance it was trained on; skip the unscale and the VAE decodes a washed-out grey image.
Implement three functions:
def compute_scaling_factor(z):
def scale_latents(z, scaling_factor, shift_factor=0.0):
def unscale_latents(z_scaled, scaling_factor, shift_factor=0.0):
compute_scaling_factor returns a Python float, the other two return arrays shaped like their input.
A float, and two arrays.
z = np.arange(-4.0, 5.0)
f = compute_scaling_factor(z)
print(round(f, 4))
print(round(float(np.std(scale_latents(z, f))), 4))
The std of [-4 ... 4] is 2.582, so the factor is 1/2.582 = 0.3873, and scaling by it makes the std exactly 1.
z = np.arange(-4.0, 5.0) f = compute_scaling_factor(z) print(round(f, 4)) print(round(float(np.std(scale_latents(z, f))), 4))
0.3873 1.0
np.arange(-4.0, 5.0) has mean 0 and population variance 60/9 = 6.6667, so its std is 2.582 and the scaling factor is 1/2.582 = 0.3873. Multiplying the (already centred) latents by that factor makes their std exactly 1.0 -- which is the whole reason Stable Diffusion carries the constant 0.18215.
compute_scaling_factor uses the population standard deviation over all elements (np.std with its default ddof=0) and returns 1 / std as a plain Python float.unscale_latents(scale_latents(z, s, h), s, h) must round-trip to z.