Replaces LayerNorm’s learned with values predicted from a conditioning vector (timestep, class, text embedding), the FiLM-style mechanism that DiT and most diffusion transformers use to inject conditioning.
Equation
A linear head maps the condition to shift, scale, and gate, :
In adaLN-Zero, are initialized to zero so and every block starts as the identity.
From scratch
import torch
import torch.nn as nn
from einops import rearrange, reduce
class AdaLNZero(nn.Module):
def __init__(self, d, d_cond):
super().__init__()
self.proj = nn.Linear(d_cond, 3 * d)
nn.init.zeros_(self.proj.weight)
nn.init.zeros_(self.proj.bias)
def forward(self, x, c, block, eps=1e-6):
# x: (b, n, d); c: (b, d_cond)
shift, scale, gate = rearrange(self.proj(c), "b (p d) -> p b 1 d", p=3)
mu, var = x.mean(-1, keepdim=True), x.var(-1, keepdim=True, unbiased=False)
h = (x - mu) * torch.rsqrt(var + eps) * (1 + scale) + shift
return x + gate * block(h)Notes
- Predict
1 + scalerather thanscaleso zero-init gives an identity modulation. - Zero-init of
gatemakes deep stacks trainable at high LR: the main reason adaLN-Zero beats plain adaLN. - Costs params per block; DiT spends ~30% of params here.
- FiLM is the same idea without the normalization step.