Concept units

Time conditioning: telling a network the time

Advanced

Read these first if they are new to you

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.

Worked example: one place, three times, three answers

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).

  1. At t = 0: 2 / 1 = 2.
  2. At t = 0.5: 2 / 0.5 = 4.
  3. 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.

At position 0, the velocity toward the data point 2 grows as time runs out
0 2 (data) t = 0t = 0.5t = 0.75 248
What to notice. Each arrow's length is the speed. With less time left, the point must move faster to arrive on time. The scale is 35 pixels per unit of speed.

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.

Worked example: one token at two times

A token has the features h = (2, 4, 6, 8).

  1. Mean: 5. Spread: √((9 + 1 + 1 + 9) / 4) = √5 ≈ 2.236.
  2. LN(h) = ((2 − 5), (4 − 5), (6 − 5), (8 − 5)) / 2.236 = (−1.34, −0.45, 0.45, 1.34).
  3. 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).
  4. 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).

Where the time enters one block
time t sines and cosines small MLP time embedding e(t) block's linear layer token features h layer norm × (1 + γ), then + β attention or MLP × α add to h one block γ, β α
What to notice. In this design, the time never enters the token features directly. It changes how each block scales, shifts and gates them (orange boxes). The line on the right is the residual path, which adds the block's input back to its output. The gate α multiplies the output of the attention or MLP step. The next section explains it.

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.

Other designs work too

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

  1. 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.

  2. 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.

  3. 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.

Where this shows up

Units that build on this one