This section is about the neural network inside an image generator, the denoiser. A diffusion or flow sampler makes a picture by starting from pure noise and calling this one network dozens of times. Each call looks at the current noisy picture and predicts the noise to remove (§33.2). So the denoiser decides what the generator can learn, how much it improves with more compute, and what every one of those calls costs.
Here is what the network must cope with. The denoiser of §33.2 was a small MLP that read a point in the plane. A real system denoises a latent instead of pixels (§34.1). An autoencoder compresses the image into a small grid of numbers, and the sampler works on that grid. For a 512 by 512 photograph the grid is 64×64×4, which is 16,384 numbers. An MLP on that grid would not know which entries are neighbors in the picture, and one layer from 16,384 numbers to 16,384 numbers already holds 16,3842≈268 million weights. The same network must also work at every noise level. At high noise it has to invent a global layout from almost nothing. At low noise it has to fix fine detail without disturbing the rest. And it must follow a class label or a prompt.
So this section answers three concrete questions. What network should predict the noise? How does the noise level reach every one of its layers? How does a label or a prompt get in? Three designs have answered them, one after another.
A convolutional U-Net processes the latent at several resolutions with small filters. It feeds the noise level into every block, by adding a learned vector to its features or by rescaling them. The first strong image diffusion models used it, and so did Stable Diffusion 1 and SDXL.
A diffusion transformer, DiT, cuts the latent into patches and treats each patch as a token of a Transformer. It keeps the rescaling idea and adds one initialization detail, called adaLN-Zero, that makes every block start by passing its input through unchanged.
A multimodal diffusion transformer, MMDiT, lets image tokens and prompt tokens attend to each other in one attention operation. Stable Diffusion 3 uses it, and FLUX uses a variant. We will prove that this joint attention is exactly self-attention over the image and cross-attention to the text, blended by one softmax.
Along the way we count every parameter and multiply-add of the standard DiT model from its configuration and reproduce the published numbers. We close with what is known about scaling these networks. By the end you should be able to build each design, cost it, and defend its choices in an interview.
Notation. A latent is z∈Rc×h×w, with c channels, height h, and width w, written channels first as code stores it. The denoiser sees the noisy latent zt at time t∈[0,1]. Clean data sits at t=0 and pure noise at t=1, as in Chapter 33. The bold c is a condition vector, never the channel count c. A transformer has width d, depth L, and T tokens. Here T always counts tokens, never sampling steps. The patch size is p. Feature maps inside a U-Net are h∈RC×Hf×Wf, with C channels.
What the denoiser has to do
Every denoiser has the same inputs and the same output, whatever is inside it. It reads a noisy latent, the time, and a condition, and it returns an array with the latent's shape,
ϵθ:Rc×h×w×[0,1]×C→Rc×h×w,(zt,t,c)↦ϵ^.
Here C is the set of possible conditions, such as class labels, prompts, or nothing at all, and θ stands for the network's weights. In plain words, the network looks at a noisy picture, is told how noisy it is and what it should show, and returns one number for every number of the picture.
That output can mean three things. It can be the noise ϵ that was added, the clean latent, or the velocityv=ϵ−z0 that flow matching learns, which points from the data toward the noise. The choice changes the loss and the sampler (§33.4 and §33.6), not the network. Three demands on the network follow.
One output per input entry. Every output entry is a prediction about the matching input entry. So the network maps an image to an image, like a segmentation network. It never pools everything into one vector the way a classifier does.
Every scale at once. At large t the input is mostly noise, and the best prediction depends on the global layout the network guesses. At small t the input is almost clean, and the leftover noise is a fine pattern that must be found entry by entry. One network has to do both. So it must see the whole image and also keep a short path for fine detail.
A global input for the noise level. The time t changes what a good feature is, everywhere in the image at once. So it should reach every layer, not enter as one more pixel. A class label is global in the same way, and so is a single vector that summarizes a prompt. A full prompt is a sequence of tokens and needs attention, which is the subject of §34.3.
The U-Net meets the first two demands with several resolutions, and the transformer meets them with attention. Both meet the third with the same idea, a rescaling of normalized features, which we build after the U-Net. If an interviewer asks why a classifier backbone cannot serve as a denoiser, these three demands are the answer.
The diffusion U-Net
A U-Net is an image-to-image network shaped like the letter U (Ronneberger et al. 2015). Its left half applies convolutions and halves the resolution, again and again. Its right half doubles the resolution and applies convolutions, again and again. A skip connection carries the features of each level on the left straight across to the level of the same resolution on the right. The U-Net was built to segment biomedical images, another task with one output per pixel. The first strong diffusion model for images, DDPM (Ho et al. 2020), used it, and almost every pixel and latent diffusion model until 2023 kept its shape.
Each part has a reason you can count. A 3×3 convolution computes each output from one position and its eight neighbors (§0.8). The receptive field of a feature is the patch of the input it depends on. Each 3×3 layer widens it by two positions, so at full resolution a feature would need about 32 layers to see across a 64×64 latent. Halve the resolution three times instead. The map is then 8×8, and each of its positions summarizes an 8×8 block of the latent. One 3×3 convolution there spans 24 of the 64 latent positions on each side. That low-resolution middle, the bottleneck, is where the global layout is decided.
An 8×8 map cannot hold all the detail of a 64×64 input, and at small t the noise to predict is exactly that detail. The skip connections give the detail a short path. Each level on the way up receives the features of the matching level on the way down, stacked along the channels. So the network only has to learn what to change at each scale.
DDPM's U-Net was later refined, one tested change at a time, into a stronger network that its authors called the ablated diffusion model, ADM (Dhariwal and Nichol 2021). It is built from four parts.
Residual blocks. A residual block computes a correction and adds it to its own input. Here the correction is GroupNorm (defined below), the activation SiLU(x)=x⋅sigmoid(x), a 3×3 convolution, a second GroupNorm where the time enters (next section), SiLU, dropout, and a second 3×3 convolution. When the channel count changes, a 1×1 convolution resizes the input before the addition. The last convolution starts at zero, so every block starts as the identity, an idea DiT will reuse.
Down and up sampling. Each level has two or three residual blocks. A stride-2 convolution or average pooling then goes down a level, and a nearest-neighbor resize followed by a convolution goes up. The channel count grows as the resolution falls, for example C,2C,4C,4C.
Self-attention at low resolution. At chosen levels a residual block is followed by multi-head self-attention (§4.5), in which every position of the feature map is a token. DDPM used single-head attention at 16×16 and once more in the middle of the bottleneck. ADM used heads of 64 channels at 32×32, 16×16, and 8×8. Its ablations found that more heads, attention at several resolutions, and residual blocks for the down and up sampling each improved FID. FID measures how far the statistics of generated images are from those of real images, and lower is better (§34.6 derives it).
Skip concatenation. On the way up, each block's input is the upsampled path stacked with the matching features from the way down. That is why blocks on the right side have more input channels than those on the left.
Why put attention only at low resolution? Self-attention compares every position with every other, so its cost grows with the square of the number of positions. The number of positions is the square of the side. So the number of query-key pairs grows with the fourth power of the side. A U-Net halves its channels each time it doubles the side, which only halves the cost of each pair. The next example puts real numbers on it.
Python · cell 1
In practice, the first question to ask of any image backbone is where it spends attention and how that cost grows with resolution. An interviewer who asks why U-Nets put attention only at low resolution wants the fourth-power argument.
Group normalization
A normalization layer rescales features so that each layer sees inputs of a steady size. BatchNorm computes each channel's mean and variance over all the examples of a batch (§0.7). That fails in a diffusion model for three reasons. Images are large, so each device holds a small batch and the statistics are noisy. Each example has its own noise level, so the statistics mix an almost clean image with pure noise, and each output depends on the other examples. And at sampling time the batch is whatever the user asked for. LayerNorm normalizes each example by itself (§5.2), but over a whole feature map it makes every channel share one mean and one scale. Group normalization (Wu and He 2018) sits between the two.
In plain words, split the channels into groups. Give each group of each example its own mean and variance, computed over all its channels and positions together. The gain and the shift stay per channel.
Why pool the statistics over several channels? Because it keeps information that normalizing each channel alone would destroy. A channel that is constant across the image has zero variance on its own, so InstanceNorm maps it to 0. Inside a group, its offset from the other channels survives. The example shows this, and then previews how the time will act on the normalized features.
Python · cell 2
Warm-up
GroupNorm for a batch of feature maps
Implement group_norm(h, G, gamma, beta, eps=1e-5). The input h has shape (B, C, H, W), and the output has the same shape. For each example and each group, compute one mean and one biased variance over the group's C/G channels and all H×W positions, and normalize with them. Then apply a per-channel gain and shift from gamma and beta, each of shape (C,). If G does not divide C, raise a ValueError. Use no loops over the batch or the groups.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Telling every layer the noise level
The time reaches every layer in two steps. First it becomes a vector the network can use. Then that vector rescales and shifts the normalized features inside every block.
The embedding. One number is a poor input. A network finds it hard to respond sharply to small changes of a single scalar, and DDPM's time runs over a thousandfold range of steps. So we expand t into sines and cosines at many frequencies, from fast to slow, the same features that §5.4 uses for token positions. Here is a tiny case. With 4 features there are two frequencies, 1 and 0.01. At step 50 the features are (cos50,cos0.5,sin50,sin0.5)=(0.96,0.88,−0.26,0.48). At step 51 the fast cosine has fallen to 0.74 while the slow one has barely moved, to 0.87. The fast features tell neighboring times apart, and the slow ones tell distant times apart.
In general, for an embedding of width df (256 in DiT) with k=df/2 frequencies ωi=10000−i/k, i=0,…,k−1, the features are
In plain words, all the cosines come first and then all the sines, the layout of ADM and DiT. At t=0 every cosine is 1 and every sine is 0. In DDPM and DiT the number fed to these features is the step index, from 0 to 999. Flow models such as SD3, whose time runs over [0,1], multiply it by 1,000 first, so the fast frequencies still turn many times over the range. A two-layer MLP then maps the features to the model width,
emb(t)=SiLU(ψ(t)W1+b1)W2+b2∈Rd.
Libraries differ in the details. The diffusers function get_timestep_embedding can put the sines first, and it can divide the exponent by k−1 instead of k. A checkpoint only works with the convention it was trained with.
The class. A class label y looks up a row of a learned table, emb(y)∈Rd. The table has one extra row that means "no label". Classifier-free guidance (§33.5) needs that row. It sharpens samples by comparing the prediction made with the label against the prediction made without it, so one network must be able to make both. DiT's code replaces the label with the extra row 10 percent of the time during training. The condition vector is the sum,
c=emb(t)+emb(y)∈Rd.
The modulation. How should c act on a feature map? DDPM projected the time embedding to one number per channel and added it to the activations of each residual block. ADM instead rescaled and shifted the normalized activations. In its ablation on 128×128 images from ImageNet, the standard benchmark of 1,000 object classes, rescaling reached FID 13.06 against 15.08 for adding, at the same training length. The U-Nets of Stable Diffusion 1 and SDXL kept DDPM's addition. The general form of rescaling has a name.
In plain words, the condition is not one more input. It turns each channel up or down and moves its center, the same way at every position. One number per channel can change what the whole layer does.
Why does rescaling beat adding? A bias added before a normalization is partly undone by it. The next GroupNorm subtracts the group mean, and divides what survives by the activations' own spread. A scale applied after the normalization acts directly. It can also do what a bias cannot, which is to switch a channel off (1+γk=0) or amplify it. A network that should rely on texture detectors at low noise and layout detectors at high noise needs exactly that kind of switch. Interviewers often ask why the scale is written as 1+γ. The answer is that a modulation layer that outputs zero then leaves the features unchanged.
Core
Timestep embedding and modulation
Implement two functions.
timestep_embedding(t, d, max_period=10000.0) takes times t of shape (B,) and an even width d. It returns the features ψ(t) above, of shape (B, d), with frequencies max_period−i/(d/2), all cosines first and then all sines.
modulate(x, shift, scale) takes tokens x of shape (B, T, d) and one shift and one scale per image, each of shape (B, d). It returns x⊙(1+scale)+shift, so that every token of image b gets image b's modulation.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
The diffusion transformer
Peebles and Xie 2023 asked whether a denoiser needs convolutions at all. Their diffusion transformer, DiT, is a Vision Transformer with the fewest changes that make it a denoiser. A Vision Transformer cuts an image into square patches, flattens each patch into a vector, and maps every vector to a token with one shared linear layer (§27.1, §27.2). DiT does the same to the latent.
Here is the input in numbers. A 256×256 ImageNet image becomes a 32×32×4 latent in Stable Diffusion's autoencoder. With patch size p=2, the latent is cut into 16×16=256 patches of 2×2×4=16 numbers each. In general there are
T=ph⋅pw
tokens, each made from p2c numbers. Fixed two-dimensional sine-cosine position embeddings are added, and there is no class token. DiT comes in four sizes, three of them taken from ViT. They are S (depth 12, width 384, 6 heads), B (12, 768, 12), L (24, 1024, 16), and a new XL (28, 1152, 16), each with patch sizes 2, 4, and 8. The name DiT-XL/2 means size XL with p=2.
The open question was how the condition c should enter. Peebles and Xie trained four DiT-XL/2 variants for 400,000 steps and compared them.
In context. Append emb(t) and emb(y) as two extra tokens and use unchanged ViT blocks, the way ViT uses a class token.
Cross-attention. Keep the two embeddings as a separate sequence of length two, and add a cross-attention layer after self-attention in every block (§4.6).
adaLN. Replace each LayerNorm with adaptive LayerNorm, with the shift and scale computed from c.
adaLN-Zero. adaLN plus a per-channel gate that multiplies each residual branch, also computed from c. The layer that computes all of them starts at zero.
block
G multiply-adds
parameters
FID-50K at 400K steps
in-context
119.37
449M
35.24
cross-attention
137.62
598M
26.14
adaLN
118.56
600M
25.21
adaLN-Zero
118.64
675M
19.47
The parameter column holds a surprise that the counting section will explain. adaLN-Zero has the most parameters of the four, because its modulation layers are large. Yet it costs almost no extra compute, because those layers run once per image instead of once per token.
In plain words, a DiT block is a pre-norm Transformer block (§5.6) with two changes. Its two LayerNorms get their shift and scale from the condition. Its two residual branches are multiplied by a gate, also computed from the condition, before being added back. The paper calls the gate α. We write g to keep αt for the noise schedule.
The output head. After the last block come one more adaptive LayerNorm and a linear layer, both initialized to zero. The linear layer maps each token to p2⋅2c numbers, twice what a patch holds. The first c channels of each output position are the noise prediction ϵ^. The other c set the variance of each reverse sampling step, because DiT is trained like ADM, which also learns that variance. Each such number ρ picks a variance between the two fixed choices of §33.2, the step's noise variance βt and the posterior variance β~t, as exp(ρlogβt+(1−ρ)logβ~t) (Nichol and Dhariwal 2021). A flow-matching DiT needs only p2c numbers per token, the velocity.
Finally the tokens are put back into their spatial layout. This step, unpatchify, is the inverse of the patch flattening. It is a place where a layout bug scrambles the output silently, because the loss still goes down a little. So it deserves a test.
Core
Unpatchify the output head
The setup defines patchify(z, p). It turns a latent of shape (B, C, H, W) into tokens of shape (B, T, p*p*C). The patches are in raster order, and each patch is flattened as (row in patch, column in patch, channel), with channels last. DiT's head emits tokens of width p2⋅2c in that layout. The first c channels are the noise prediction and the next c the variance output.
Implement unpatchify(tokens, h, w, p, c). It returns the pair (eps_pred, var_pred), each of shape (B, c, h, w). It must invert patchify exactly, including for latents that are not square.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Interview
A DiT block with adaLN-Zero, batched, in NumPy
Implement dit_block(X, c, P, n_heads) exactly as in the definition box. The tokens X have shape (B, T, d) and the conditions c have shape (B, d). The dict P holds these weights.
W_mod(d, 6d) and b_mod(6d,). Their output splits into six consecutive chunks of width d, in the order (shift, scale, gate) for attention, then (shift, scale, gate) for the MLP.
W_qkv(d, 3d) and b_qkv. The output columns are Q, K, V in that order, and head j uses columns j*d_h:(j+1)*d_h of each.
W_o(d, d) and b_o for the attention output.
The MLP's W_1(d, 4d), b_1, W_2(4d, d), and b_2.
The setup provides ln_noaffine (LayerNorm over the last axis with ε=10−6 and no gain or shift), gelu_tanh, silu, softmax_last, and make_block_params. Use no loops over the batch, the tokens, or the heads.
import numpy as np
def dit_block(X, c, P, n_heads):
# X: (B, T, d) tokens, c: (B, d) condition, P: dict of weights (see the task)
raise NotImplementedError
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Why adaLN-Zero starts as the identity and still learns
Starting at zero sounds like a way to kill a network. If every weight were zero, every hidden unit would compute the same thing and receive the same gradient, so the units would stay identical forever. That is the symmetry problem of §0.7. adaLN-Zero zeros something much narrower, the one layer that produces the shifts, scales, and gates. The attention and MLP weights stay random.
So each branch still computes a rich random function. The zero gate in front of it hides that function from the residual stream, the running sum of branch outputs that flows through the network (§5.1). The question is whether the gate can open.
A tiny example shows why it can. Take one entry where the branch outputs f=0.7, the gate is g=0, and the gradient arriving from above is 2. The entry adds gf=0 to the stream, so the block changes nothing. But the gradient with respect to the gate is 2⋅f=1.4, which is not zero. The gradient with respect to the branch output is 2⋅g=0. So the gate learns at once and the branch waits. In general, a product of a zero factor and a random factor is zero, yet its gradient with respect to the zero factor is not. The theorem states this for the whole block.
Write dZ=∂L/∂Z for the gradient of the loss with respect to any array Z, as in §4.3. Let A=MHSA(U1) and M=MLP(U2) be the two branch outputs, each T×d.
In plain words, at the start each block adds nothing to its input, yet each gate already receives a gradient. That gradient measures how well the branch's random output lines up with the gradient from above, summed over the tokens. Nothing inside the branch learns until its gate is no longer zero.
∂ Proof that adaLN-Zero starts as the identity and still learns
At initialization all six modulation vectors are zero. Then X1=X+Adiag(0)=X, and likewise Xout=X1=X, which is item 1.
Go backward through the MLP sublayer, Xout=X1+Mdiag(g2). Entry (i,j) is X1,ij+Mijg2,j, so ∂L/∂g2,j=∑idXout,ijMij, which is item 2 for g2. Also dM=dXoutdiag(g2)=0. Every weight inside the MLP, and its input U2, receive gradient only through M, so they get zero. From U2=LN(X1)diag(1+γ2)+1Tβ2, the gradients dγ2=1T⊤(dU2⊙LN(X1)) and dβ2=1T⊤dU2 are zero too. The path from X1 through the LayerNorm carries nothing, so dX1=dXout.
The attention sublayer has the same form, with dX1=dXout arriving from above. That gives item 2 for g1, zero gradient for the attention weights, γ1, and β1, and dX=dX1=dXout, which is item 5.
The six vectors are one linear function of s=SiLU(c), namely m=sWmod+bmod, and dm is nonzero only in the gate slots. The linear-layer gradients of §0.2 give dWmod=s⊤dm and dbmod=dm, which is item 3. They also give ds=dmWmod⊤=0, because Wmod=0. So nothing upstream of the modulation layer learns from this block, which completes item 4. ∎
The same move appears all over the book. Each time, a new path starts with zero output but is not starved of gradient. ResNets often start the last normalization gain of each residual branch at zero (Goyal et al. 2017), as §0.8 notes, which is the closest cousin of this trick. LoRA starts one of its two matrices at zero (§14.5). Flamingo puts tanh gates that start at zero on its new layers (§31.2). ControlNet adds convolutions that start at zero (§34.7).
The whole network starts learning at its output. DiT also initializes its final adaptive LayerNorm and its output layer to zero. Follow the first three training steps.
Step 0. The output is identically zero. The output layer gets a gradient, because its input is not zero. The gradient that reaches the last block, dXL=dOWout⊤ through the final norm, is zero because Wout=0. So only the output layer learns.
Step 1. The output layer is now nonzero, so a gradient reaches every block and passes through each one unchanged (item 5). It moves every gate, the final modulation, and the patch embedding.
Step 2. The gates are now nonzero, so the attention and MLP weights, the shifts and scales, and the timestep and class embeddings receive gradient at last.
In practice, a deep stack starts as an identity map, which is trivially stable, and grows into a useful function one set of zeros at a time. The NumPy cell checks the theorem with finite differences on one sublayer. The PyTorch block further down checks the order of the three steps on a small DiT.
Python · cell 3
The widget runs a toy block of width 12 over 5 tokens, with random attention and MLP weights. Press Train to move its modulation layer from the zero initialization toward a fixed set of stand-in trained values. The gates fill with color first, then the shifts and scales. The grid on the right shows how much the block changes each token, and the curve below shows that change at every noise level t.
What does an adaLN-Zero block do to its tokens as training opens its gates?
A toy DiT block of width 12 over five tokens. Its modulation moves from zero toward stand-in trained values.
Loading interactive figure…
The PyTorch block builds a small DiT the way the official code does. It has adaLN-Zero blocks, a final modulation and output layer that start at zero, a timestep MLP on 32 frequency features, and a class table with a null row. It takes three Adam steps and reports which groups of parameters received a nonzero gradient at each step.
🔥 In PyTorchfor a machine with PyTorch
MMDiT, two streams and one attention
A class label fits in one vector. A prompt does not. "A red cube on a blue sphere" and "a blue cube on a red sphere" contain the same words. So any summary that only records which words occur gives them the same vector, and only a model that reads the tokens in order can tell them apart (§30.6 showed the same failure in CLIP). A text-to-image denoiser therefore needs the whole sequence of text-token vectors, Cctx∈RTc×dc for Tc tokens of width dc, not just one pooled vector. By 2023 there were three ways to read that sequence.
Cross-attention. Image tokens query the text tokens in an extra sublayer (§4.6). Stable Diffusion 1, SDXL, and PixArt-α work this way. The text is read, but the image never changes it, so the same fixed representation of "red" serves every image and every noise level.
One sequence with shared weights. Text tokens and image tokens enter one sequence and pass through the same weights, as in U-ViT (Bao et al. 2022) and DiT's in-context variant. Information flows both ways, but one set of projections must serve two kinds of token with very different statistics.
Joint attention with separate weights. Each modality keeps its own weights for everything except the attention operation itself, which runs over the two sequences stacked together. This is MMDiT (Esser et al. 2024).
In plain words, an MMDiT block is two transformers, one for the picture and one for the prompt, that share a single attention. Every token can read every other token, while each modality keeps its own way of computing queries, keys, values, and features. SD3 compared this design on the CC12M dataset of image-caption pairs. It beat a shared-weight DiT, a DiT with cross-attention, and a U-Net with transformer blocks at its lowest levels, in validation loss, in FID, and in CLIP score, the similarity of each image to its prompt under CLIP (§34.6). A third set of weights, with separate streams for the tokens of SD3's two kinds of text encoder, added little (Esser et al. 2024, Section 5.2.3).
What exactly does an image token get from joint attention? A tiny example answers it before the general statement.
That is no coincidence, and the proof is two lines.
Proof. The softmax over all T+Tc keys has denominator Zx+Zy. Multiply and divide each partial sum by its own normalizer,
The two fractions on the right are ordinary softmax attentions over their own keys, and their weights are λ and 1−λ. Writing λ=1/(1+Zy/Zx)=1/(1+e−(logZx−logZy)) gives the logistic form. ∎
In plain words, each image token runs self-attention and cross-attention side by side and blends the two results. It chooses the blend itself, through how strongly its query matches the image keys compared with the text keys. This is the same merge of partial softmaxes that FlashAttention uses (§12.2), with the pieces split by modality instead of by memory tile.
Two consequences matter in practice.
The text tokens are updated. The text queries produce Oy. So the prompt's representation changes from block to block in response to the image, and through its own modulation in response to the noise level. Cross-attention never does this.
The prompt's share starts small. At initialization the scores are roughly alike, so λ is close to the share of keys that are image keys, T/(T+Tc). The next example puts SD3's numbers on it, and on the cost.
The cell checks the theorem for random image and text streams. It also prints the image share next to the token share.
Python · cell 4
QK-norm. Large transformers trained in 16-bit precision can diverge when their attention scores grow without limit. SD3 hit this problem. It pretrained at 256×256. When it fine-tuned at higher resolution in mixed precision, which computes in a 16-bit format and keeps a 32-bit (fp32) copy of the weights (§8.5), the loss diverged. The cause was attention logit growth (§5.7). The largest attention scores grow until each softmax row puts nearly all its weight on one key, so the entropy of the attention collapses. SD3 saw it in the last transformer blocks.
Training fully in fp32 fixed it, at about half the speed. RMSNorm with a learned gain on the queries and keys of both streams fixed it in mixed precision with the bfloat16 format (bf16), together with AdamW at ϵAdam=10−15. It even worked when added to a model pretrained without it (Esser et al. 2024, Section 5.3.2). The norm acts on each head's dk numbers, so each head's scores are bounded by its own gains. In an interview about a run that diverges at high resolution, QK-norm is the first fix to name.
Single-stream blocks. FLUX.1 runs 19 MMDiT blocks and then 38 single-stream blocks, in which the text and image tokens form one sequence and share all weights. The box below describes them, and the counting section uses them.
Interview
MMDiT joint attention with QK-norm
Implement joint_attention(X, Y, Px, Py, n_heads, eps=1e-6). The image tokens X have shape (B, T, d) and the text tokens Y have shape (B, T_c, d), both already normalized and modulated. Each stream's dict holds W_qkv of shape (d, 3d), W_o of shape (d, d), and per-head RMSNorm gains g_q and g_k of shape (d_h,). The columns of W_qkv are Q, K, V, with heads contiguous, as in Exercise 34.2d. Follow these steps.
Project each stream with its own weights and split the heads.
Apply RMSNorm to each head's query and key over its dh entries.
Concatenate image tokens first, then text tokens, and attend with the 1/dh scale.
Split the output back into the two streams and project each with its own W_o.
Return (out_x, out_y, lam). Here lam has shape (B, n_heads, T) and holds the λ of the theorem for every image query and head. Use no biases and no masks.
import numpy as np
def joint_attention(X, Y, Px, Py, n_heads, eps=1e-6):
# X: (B, T, d) image tokens, Y: (B, T_c, d) text tokens, Px and Py: each stream's weights
# returns (out_x of shape (B, T, d), out_y of shape (B, T_c, d), lam of shape (B, n_heads, T))
raise NotImplementedError
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
The order of concatenation is a convention. The diffusers implementation of SD3 puts image tokens first, and FLUX puts text first. Attention without a mask treats its tokens as a set, so either order works. The split must undo the concatenation, and each token's position encoding must travel with it.
Counting parameters and multiply-adds
Counting a model's parameters and compute from its configuration is a standard interview test, and it is how you check any description of an architecture. We count multiply-adds (MACs). One MAC is one multiplication followed by one addition, the basic step of a matrix product. Vision papers call this unit "FLOPs", while this book counts two FLOPs per MAC (§27.2 made the distinction). §33.4 priced a sample with the cost of one DiT-XL/2 forward pass. Here we derive that cost term by term.
That last line explains the parameter column of the conditioning table, and PixArt-α acted on it. PixArt-α is a text-to-image DiT that reads the prompt through cross-attention (Chen et al. 2023), so its modulation only has to carry the time. Its adaLN-single computes one set of six modulation vectors from the time embedding, shared by all blocks, and adds a small learned offset in each block. That cut the model from 833M to 611M parameters, 26 percent fewer, and its GPU memory by 21 percent. Full adaLN kept a clearly lower FID in their ablation, 18.20 against 22.37, though the authors judged its samples visually on par.
The same arithmetic constrains every large model. The next cell counts the weights in the blocks of SD3, FLUX.1, and HunyuanVideo from their published configurations. SD3 sets its width to 64 times its depth. It also counts Movie Gen, Meta's 30-billion-parameter video generator (Polyak et al. 2024). Its blocks follow the layout of Meta's Llama language models, with self-attention, cross-attention to the text, and a gated SwiGLU MLP (§5.3), which has three weight matrices instead of two.
Python · cell 5
The formulas land within a few percent of every published size. SD3's depth-38 model has 8.09B weights in its blocks, matching the "8B" of its paper, and depth 24 gives the 2B model. FLUX.1's 11.83B plus its input layers is the advertised 12B, and HunyuanVideo's 12.46B plus its input layers is its 13B. The formula leaves out only small pieces. They are the text and pooled-vector projections, the timestep MLP, the final layer, the biases, and the norm gains.
The widget plots DiT's twelve models by compute and FID. Pick a size and a patch size, and read the computed cost next to the published one.
How do patch size, width, and depth trade compute for quality in a diffusion transformer?
DiT's twelve ImageNet 256 × 256 models after 400K training steps, without guidance.
Loading interactive figure…
Research
The cost model of a diffusion transformer
Implement dit_cost(d, L, T, p, c=4, n_classes=1000, freq_dim=256). It returns (parameters, multiply_adds) for one forward pass of a class-conditional DiT with learned variance, whose output has p2⋅2c numbers per token. Return both counts as Python ints.
Count every trainable parameter. Every linear layer has a bias, and the LayerNorms have no gain or shift. Count the blocks of the definition box (MLP width 4d), the patch embedding (a linear map from p2c numbers to d), the timestep MLP from freq_dim to d to d, the class table with n_classes + 1 rows (the extra one is the null label), the final modulation from d to 2d, and the output layer. Leave out the frozen position table.
Count the multiply-adds of every matrix product and nothing else. That is the per-token weights, the two attention products, the per-image modulations and timestep MLP, the patch embedding, and the output layer. Biases, norms, activations, the softmax, and the class lookup cost nothing here.
The tests check both counts exactly, then compare your multiply-adds with all twelve rows of DiT's Table 4 and with the 512×512 model.
def dit_cost(d, L, T, p, c=4, n_classes=1000, freq_dim=256):
# returns (parameters, multiply_adds) for one forward pass, both Python ints
raise NotImplementedError
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
What scaling diffusion transformers has shown
Compute, not parameters, predicts quality. Across DiT's twelve models, FID after 400K steps fell steadily as the forward MACs rose, with a correlation of −0.93 between FID and compute, plotted on a log scale. Models of different shapes but similar compute mostly reached similar FID. DiT-S/2 at 6.06 G and DiT-B/4 at 5.56 G both reached 68.4. The exception is patch size 8, whose tokens are too coarse. DiT-B/8 and DiT-S/4 both cost 1.4 G, yet B/8 reached only 122.74 against 100.41. Halving the patch size at a fixed model size quadruples the tokens and the compute while leaving the parameters almost unchanged, and it improved FID at every size. For XL, FID went from 106.41 at p=8 to 43.01 at p=4 and 19.47 at p=2. Larger models also used training compute better, overtaking smaller models trained for longer (Peebles and Xie 2023, Figures 8 and 9).
Validation loss falls smoothly and predicts what people see. SD3 trained MMDiTs from depth 15 to 38 for 500,000 steps at 256×256. Validation loss fell smoothly with model size and training steps, for images and for videos, without saturating. It correlated strongly with the GenEval and T2I-CompBench benchmarks of prompt following and with human preference. The largest model needed a learning-rate adjustment at 300,000 steps to avoid divergence. Larger models also needed fewer sampling steps to reach their best quality (Esser et al. 2024, Section 5.3.3).
U-Nets against transformers. U-ViT (Bao et al. 2022) is a plain ViT that treats the time and the condition as tokens and adds long skip connections between shallow and deep blocks. It matched a U-Net of similar size and reached FID 2.29 on ImageNet 256×256. Its ablations showed that the long skips mattered while the down and up sampling did not. SDXL moved its U-Net's transformer blocks to the lowest resolutions, a step toward a transformer. In SD3's comparison, a U-Net with transformer blocks at its lowest levels (Hoogeboom et al. 2023) learned faster at first than the DiT with cross-attention. The cross-attention DiT overtook it, and MMDiT beat both.
Every frontier image and video model since 2024 whose architecture is published is a transformer, including SD3, FLUX, Movie Gen, Wan, and HunyuanVideo. That is evidence that transformers scale better, though not a controlled experiment. At small compute a well-tuned U-Net remains a strong baseline. EDM2 redesigned a convolutional U-Net's layers so that the sizes of activations and weights stay steady during training, and it reached FID 1.81 on ImageNet 512×512 (Karras et al. 2024).
A narrow denoiser has a loss floor. One more result sets a hard rule for the width. A representation autoencoder uses a frozen DINOv2 encoder as its encoder, so each latent token is a 768-dimensional feature vector (§32.6, §34.1). Zheng et al. 2025 found that a DiT narrower than 768 could not even fit a single image. The reason is a short theorem. Each output token is a linear map of a d-dimensional hidden state, so it can only move in d directions, while the target varies in all n.
In plain words, a token's output can only point in d directions, and the noise alone puts unit variance in all n. So at least n−d units of loss per token can never be removed. For a DiT on Stable Diffusion's latents, n=p2c=16, or 64 for FLUX's 16 channels, far below any width, and the bound never bites. For 768-dimensional representation tokens, a DiT-S of width 384 is stuck at a loss of at least 384 per token, half of the 768 units of variance that the noise alone puts in each token. Zheng et al. widened the denoiser, or added a wide shallow head, to fix it. The interview lesson is that the width must exceed the number of values each token predicts.
∂ Proof of the width bound
The output always lies in the row space S of Wout, of dimension at most d. Let P be the orthogonal projector onto S, so I−P projects onto a subspace of dimension m≥n−d. Since w^∈S, Pythagoras gives ∥w−w^∥2=∥(I−P)w∥2+∥Pw−w^∥2≥∥(I−P)w∥2.
Treat w as a column for the moment. Then E∥(I−P)w∥2=tr((I−P)E[ww⊤])≥tr((I−P)Σw). The inequality holds because E[ww⊤]=Σw+μμ⊤ and tr((I−P)μμ⊤)=∥(I−P)μ∥2≥0.
Now expand Σw=∑kνkekek⊤ in its orthonormal eigenvectors (the spectral theorem of §0.10). Then tr((I−P)Σw)=∑kνkak with ak=∥(I−P)ek∥2∈[0,1] and ∑kak=tr(I−P)=m. A weighted sum of the νk with weights in [0,1] that add up to m is smallest when weight 1 sits on the m smallest eigenvalues. So it is at least ∑i>n−mνi≥∑i>dνi.
For the last inequality, independence gives Σw=I+Cov(z0), so every νi≥1 and the sum of n−d of them is at least n−d. An output bias changes nothing, since it moves w^ within a shifted copy of S and only the mean term above absorbs it. ∎
That closes the theory of the backbone. What remains is practice, how these networks are trained at scale and where implementations of them go wrong.
The table puts the five backbones of this section side by side, with what each one does well and badly.
backbone
how t and c enter
how text enters
strengths
weaknesses
used by
convolutional U-Net
in every residual block, added (DDPM, SD) or by AdaGN (ADM)
cross-attention at low resolutions
strong built-in assumptions about images, efficient at small compute, any resolution
hard to scale cleanly, many design choices to tune
DDPM, ADM, SD 1 and XL, EDM2
U-ViT
as tokens
as tokens, shared weights
simple, long skips help early training
shared weights for unlike tokens
U-ViT
DiT
adaLN-Zero
none (class labels) or added cross-attention
scales predictably with compute, identity start
quadratic in T, sequence conditioning needs extra layers
DiT, SiT, PixArt-α (with cross-attention)
MMDiT
adaLN-Zero per stream
joint attention, separate weights
text and image update each other, best in SD3's comparison
Dhariwal & Nichol (2021). Diffusion Models Beat GANs on Image Synthesis. Section 3 is the ablation that built ADM, with attention at several resolutions, 64-channel heads, residual blocks for resampling, and adaptive group normalization.
Wu & He (2018). Group Normalization. The definition, its relation to LayerNorm and InstanceNorm, and why it does not depend on the batch size.
🎬 Diffusion Transformers (ViT, DiT, MMDiT) · Julia TurcA clear walk from ViT patches to DiT's adaLN conditioning and SD3's joint-attention MMDiT, the same path as this section.🎬 Scalable Diffusion Models with Transformers | DiT Explanation and Implementation · ExplainingAIExplains the DiT paper and then implements patchify, adaLN-Zero blocks, and unpatchify in PyTorch, a good companion to Exercises 34.2c and 34.2d.🎬 Stanford CS25: V5 I Transformers in Diffusion Models for Image Generation and Beyond · Stanford OnlineA Stanford CS25 lecture on how transformers replaced U-Nets in diffusion, through DiT, MMDiT-style models, and current open systems.