Time conditioning: telling a network the time
Read these first if they are new to you
- Layer normalization Intermediate
- Fourier features: positions as waves Advanced
- The multilayer perceptron (MLP) Intermediate
The idea in one paragraph
A network that removes noise sees a blend of noise and data, and it must know how far along the blend is. That amount is the time t, from 0 (pure noise) to 1 (data). The same input can need a different output at a different time, so the time is an input too. A common way to feed it in has three parts. Turn t into many sines and cosines, pass those through a small network, and let the result set the scale and shift after each layer normalization. This last part is adaptive layer normalization, or adaLN.
The same point needs a different answer at a different time
Generative models such as flow matching blend a noise draw x0 and a data example x1: xt = (1 − t) x0 + t x1. At t = 0 the input is pure noise; at t = 1 it is data. The network sees xt and must predict how to move it toward the data. The right move depends on t, even for the same xt.
Let the data be a single number, x1 = 2, and look at the position 0. A point at 0 at time t must reach 2 by time 1 on a straight path, so its velocity is the distance over the time left: (2 − 0) / (1 − t).
- At t = 0: 2 / 1 = 2.
- At t = 0.5: 2 / 0.5 = 4.
- At t = 0.75: 2 / 0.25 = 8.
The input position is the same each time, but the right output changes by a factor of 4. A network that does not know t cannot give all three answers.
Your turn. What is the velocity at position 0 at t = 0.9?
Answer
2 / (1 − 0.9) = 2 / 0.1 = 20.
First, turn the time into many numbers
One number is a weak input for a network. Its first layer can only scale it and add to it, so small differences in t are hard to act on. The usual fix uses Fourier features: the sines and cosines of t at several frequencies. Nearby times then get similar lists of numbers, and the fast waves let the network tell close times apart.
A small MLP then maps those features to a vector e(t), the time embedding. It has as many numbers as each token has features. Other conditions can ride along. For example, a model that takes a class label can add the label's embedding to e(t) and use the sum wherever it would use e(t).
adaLN lets the time set each layer's scale and shift
Layer normalization takes each token's features, subtracts their mean and divides by their spread. It then multiplies by a learned scale and adds a learned shift. In plain layer norm, the scale and shift are fixed after training: the same for every input and every time.
Adaptive layer normalization computes the scale and shift from the time embedding instead. A linear layer maps e(t) to two vectors, γ(t) and β(t), one number per feature:
adaLN(h, t) = LN(h) ⊙ (1 + γ(t)) + β(t)
Here h is a token's feature vector, LN is layer normalization without its own scale and shift, and ⊙ multiplies feature by feature. The "1 +" means that γ = 0 leaves the scale at 1. Each block of the network has its own linear layer, so each block can respond to the time in its own way.
A token has the features h = (2, 4, 6, 8).
- Mean: 5. Spread: √((9 + 1 + 1 + 9) / 4) = √5 ≈ 2.236.
- LN(h) = ((2 − 5), (4 − 5), (6 − 5), (8 − 5)) / 2.236 = (−1.34, −0.45, 0.45, 1.34).
- Suppose that at t = 0.2 the time layer gives γ = (0.5, 0, 0, −0.5) and β = (0, 0, 1, 0). Then the output is (−1.34 × 1.5, −0.45, 0.45 + 1, 1.34 × 0.5) = (−2.01, −0.45, 1.45, 0.67).
- Suppose that at t = 0.9 it gives γ = 0 and β = 0. Then the output is just LN(h) = (−1.34, −0.45, 0.45, 1.34).
The same token leaves the layer differently at the two times. The rest of the block sees that difference.
Your turn. LN(h) = (−1, 0, 1), γ = (1, 0, 0) and β = (0, 0, 2). What does adaLN output?
Answer
(−1 × 2 + 0, 0 × 1 + 0, 1 × 1 + 2) = (−2, 0, 3).
adaLN-Zero starts every block switched off
In a diffusion transformer, the time layer also makes a gate α for each branch of a block. The branch's output is multiplied by α before it is added back to the input, through a residual connection. The layer that makes γ, β and α starts with all its weights set to zero. So at the start α = 0, every branch adds nothing, and every block passes its input through unchanged. Training then opens each gate as far as it helps.
This variant is called adaLN-Zero. The paper that introduced diffusion transformers compared it with three other ways to feed in the time and a class label: adding them as extra tokens, reading them with cross-attention, and plain adaLN. adaLN-Zero gave the best images in that comparison.
adaLN is common, not universal. Small networks often just append t to the input, as the flow-matching example does. Other models add the time embedding to every token. Which works best depends on the model and the data.
The code writes the layer as a Python class named AdaLN, built on PyTorch's
nn.Module. Its __init__ runs once, when AdaLN(D) makes the layer. It first
calls super().__init__(), which lets nn.Module set itself up. Then it stores the
layer's parts in self, the layer itself. Its forward runs each time the layer is called,
as in layer(h, t). The Python tutorial explains
classes.
adaln.py (PyTorch)
import torch
import torch.nn as nn
# fix the layer's random starting weights, so each run prints the same
torch.manual_seed(0)
D = 4 # features per token
def time_features(t):
# Sines and cosines of t at four frequencies: 8 numbers per time.
# torch.arange(4) is [0, 1, 2, 3], so freqs holds π × 1, 2, 4 and 8.
freqs = torch.pi * 2.0 ** torch.arange(4)
# torch.cat joins the 4 sines and the 4 cosines into one row of 8 numbers.
return torch.cat([torch.sin(t * freqs), torch.cos(t * freqs)], dim=-1)
class AdaLN(nn.Module):
def __init__(self, d, d_time=8):
super().__init__()
self.norm = nn.LayerNorm(d, elementwise_affine=False) # no fixed scale or shift
self.to_scale_shift = nn.Linear(d_time, 2 * d) # the time chooses them
def forward(self, h, t):
# .chunk(2, dim=-1) cuts the 8 outputs into two halves of 4: scale and shift.
scale, shift = self.to_scale_shift(time_features(t)).chunk(2, dim=-1)
return self.norm(h) * (1 + scale) + shift
layer = AdaLN(D)
h = torch.tensor([2.0, 4.0, 6.0, 8.0]) # one token's features
# Inside torch.no_grad(), PyTorch records no gradients, so .numpy() can turn
# each result into a NumPy array.
with torch.no_grad():
print("normalized ", layer.norm(h).numpy().round(2))
for t in [0.1, 0.9]:
print(f"adaLN at t={t}", layer(h, torch.tensor([t])).numpy().round(2))
normalized [-1.34 -0.45 0.45 1.34] adaLN at t=0.1 [-1.21 -0.52 0.95 0.34] adaLN at t=0.9 [-0.97 0.09 -0.08 1.25]
The layer here is untrained, so its outputs are arbitrary. The point is that one token leaves it differently at two times. For brevity, the code skips the small MLP and feeds the sines and cosines straight to the linear layer.
Check yourself
-
The data are the single number x1 = 4. On a straight path, what velocity does a point at position 0 need at t = 0.5? And at t = 0?
Answer
(4 − 0) / (1 − 0.5) = 8 at t = 0.5, and 4 / 1 = 4 at t = 0. The same position needs different answers, so the network must know t.
-
The time layer outputs γ = 0 and β = 0. What does adaLN do to a token?
Answer
Plain normalization: it subtracts the token's mean and divides by its spread, with no extra scale or shift.
-
Why does adaLN-Zero start every gate α at zero?
Answer
So every block starts by passing its input through unchanged. The untrained network is then simple and stable, and training opens each block's gate as far as it helps.
Go further
Optional background, for readers with more time.
-
The general idea behind adaLN, called FiLM: one input sets a scale and a shift for every feature of another. Clear diagrams and many examples.
-
Section 3.2 defines four ways to feed in the time and the class, including adaLN-Zero. Figure 5, in Section 5, compares them.
Where this shows up
Units that build on this one
- Diffusion transformers (DiT) Advanced