InterviewPrepKit

Home / Learn / GenAI System Design

07 — Realistic Face Generation

“Build a system that generates photorealistic human faces that do not belong to anyone.”

A modern image generator can be understood end to end — from the mathematical objective it optimizes, through the network that implements it, to the cents it costs to serve one picture — and face generation is the worked example that carries the whole story.

The centrepiece is a derivation you should be able to reproduce on a whiteboard: why the field converged on diffusion, a model that learns to remove noise from a picture a little at a time, and rejected the three families that came before it.

When you finish, you should be able to do five things:

  1. Say exactly what goes into the system and what comes out.
  2. Derive the model-family choice from first principles rather than recite it.
  3. Explain why the industry-standard quality score is misleading, and what to report instead.
  4. Price a single generated image to four decimal places.
  5. Name the failure modes that make this a legal problem and not only an engineering one.

No prior generative-modelling background is assumed. Every acronym — GAN, VAE, ELBO, FID, CFG, DiT — is defined the first time it appears.

Input and output, before any mechanism. The system takes an optional description of the face you want — an age band, a head pose, a lighting style, an expression — or nothing at all. It returns one 512 × 512 photograph of a human being who does not exist. Nothing else crosses the boundary: no reference photo goes in, and no real person’s identity is supposed to come out.

Why the architecture is not the hardest part. Three facts about faces, before any network:

Anyone who reaches for the neural network before raising consent, misuse, and provenance has failed the round with a technically correct answer.

So the constraints come first, in Framing and the constraint that comes before the architecture. The model-family comparison that every later chapter builds on is The model family comparison derived.


1. Framing, and the constraint that comes before the architecture

Before a single layer is chosen, two things need fixing: the interface — what the system takes in and hands back — and the three legal and safety constraints that settle most of the design on their own.

The interface. The input is an optional attribute condition — a description of the face you want, expressed as an age band, a head pose, a lighting setup, and an expression — or nothing at all, in which case the system samples a face freely from everything it has learned. The output is a single 512 × 512 photorealistic face of a person who does not exist. Three products pay for it: synthetic avatars, stock imagery for design mockups, and privacy-preserving test data for face-recognition systems that cannot lawfully hold real faces.

Why it is hard. There are no labels and no ground truth for any individual sample. Nobody can write down “the correct face” for the request age 30-40, frontal, studio lighting — there is only a space of acceptable ones, and the model has to learn the shape of that space. Worse, the metric everyone reaches for, the Fréchet Inception Distance (FID, defined in full later), is a statistic comparing two whole collections of images rather than scoring any single one, so it structurally cannot see the per-image failures users complain about.

Say this first, before any architecture: “Three things have to be settled before the model choice, because two of them constrain the training data and one constrains the serving path. Where the faces came from and whether those people consented. What stops the output being used to impersonate someone. And how a downstream party tells that this image came from us.”

The three constraints, as mechanisms

Those three obligations — consent, misuse, provenance — resolve into six concrete mechanisms, each with a specific place in the pipeline where it runs and a specific thing it cannot do.

Four pieces of vocabulary first, because the table leans on all of them:

The last column is the one that keeps each mechanism honest about what it does not cover.

ConstraintMechanismWhere it is enforcedWhat it does not do
Consent / lawful basisLicensed portrait sets with model releases, plus consented capture. Per-image provenance row: source, licence id, consent scope, ingest dateIngest pipeline, before any tensor existsDoes not survive a scraped-data shortcut “just for the first run” — the run becomes the checkpoint and the checkpoint is the product
Right to erasureThe provenance table is the join key. Erasure means: remove the rows, remove the shards, and schedule a retrain — you cannot subtract an image from trained weightsData plane + retrain cadenceDoes not make the current checkpoint clean. Budget a retrain, or do not accept erasure requests
Minor exclusionAge classifier at ingest, threshold set for high recall not high precision, plus human audit of the borderline bandIngest, hard rejectDoes not need to be accurate on the majority — it needs to be paranoid at the boundary. This is a hard exit criterion, not a quality dial
No identifiable real personArcFace embedding of every output, cosine against a public-figure index and against the training index. Block above thresholdServing, on the output, post-decodeDoes not catch a person absent from either index. It catches the two cases you can be sued over
ProvenanceC2PA-style signed manifest attached at generation: model id, checkpoint hash, timestamp, “AI-generated” assertion, signed with the service keyServing, at the response boundaryDoes not survive a re-encode that drops metadata — which is every social upload
Invisible watermarkA learned pattern embedded in the decoder output; a paired detector recovers ~48 bitsServing, before the C2PA signDoes not prove absence. See below

Watermark robustness, measured

How much abuse does an invisible watermark actually survive? The measured answer settles the one question that matters: what a watermark can and cannot be used to prove.

An invisible watermark is a tiny, deliberately imperceptible pattern added to the pixels of every image the system emits, carrying a few dozen bits of hidden payload. It is produced by nudging the output of the decoder — the final network stage that turns the model’s internal representation into pixels.

The mark is trained jointly with a paired detector network whose job is to read the payload back. During that joint training the pair is exercised through a distortion layer: a stack of simulated attacks (compression, resizing, cropping) applied between the encoder and the detector, so the mark learns to survive them.

That training setup is the whole story. The mark survives the distortions it was trained against and fails on the ones it was not. Everything in the table below is a consequence of that one sentence.

Bit accuracy is the fraction of the roughly 48 hidden bits the detector recovers correctly. 1.00 is perfect recovery. 0.50 is a coin flip, meaning the mark is gone.

The attack names: JPEG q=50 is a fairly aggressive lossy re-compression. img2img regeneration means feeding the watermarked picture back into a diffusion model and letting it partly re-noise and re-generate the image — “strength 0.4” says how far back toward noise it was pushed. An autoencoder round trip is the same idea, cheaper: compress the image to a compact internal representation with one network and expand it back with another.

The last two columns are the ones that decide anything. Bits of 48 is just round(48 x bit accuracy). FPR — false-positive rate — is the chance that an image the system never generated matches at least that many of the 48 bits by luck.

Where does FPR come from? Run the detector on an image you did not generate and there is no mark to read, so each of the 48 bits it reports is an independent coin flip. The count of bits that happen to match your payload is therefore a binomial: 48 fair coins. The chance of getting k or more heads out of 48 is

FPR(k) = ( sum_{i=k}^{48} C(48, i) ) / 2^48

where C(48, i) is the number of ways to choose i items from 48. Worked on the screenshot row, which recovers 42 bits: summing C(48, 42) ... C(48, 48) gives about 1.4e7 favourable outcomes out of 2^48 = 2.8e14 total, which is 5.0e-08. Multiply by a billion images and you get the last column: 50 false attributions per 1e9 images.

AttackBit accuracyBits of 48FPR per imageFalse hits per 1e9 imagesReading
none1.00483.6e-150
JPEG q=500.99483.6e-150in the training distortion set
resize 512 -> 256 -> 5120.98471.7e-130in the set
crop to 50% area0.93456.6e-110degrades with the surviving area, and still clears the 1e-9 bar
screenshot, re-encode, re-upload0.88425.0e-0850usable only if 50 false attributions per billion images is acceptable — say the number, do not say “usable”
img2img regeneration, strength 0.40.61299.7e-0296,706,326no attribution. The mark is a high-frequency signal; a diffusion round trip resamples exactly that band
round trip through a different autoencoder0.55263.3e-01332,732,906no attribution. Same mechanism, cheaper
adversarial optimization with detector access0.50245.6e-01557,283,251no attribution. Detector access is game over

Bit accuracy is not a detection rate, and reading conclusions off the accuracy column alone is the standard mistake. Attribution makes a decision — “we generated this” — so the number that governs it is the false-positive rate, not the accuracy that feeds it.

Set the bar first. Scanning a large corpus, you want at most about one false attribution per billion images, so the target is FPR <= 1e-9. Read that back off the binomial: 44 matched bits gives 7.6e-10 and 43 gives 6.8e-09, so 44 is the smallest count that clears it. And 44 out of 48 is a bit accuracy of 44 / 48 = 0.917. Anything below 0.917 bit accuracy is not attribution.

Now three consequences follow.

The code block below reproduces every row of the table from the binomial formula and asserts the printed figures match.

from math import comb

BITS = 48

def fpr(bits_matched: int, n: int = BITS) -> float:
    """Chance that an image we did not generate matches >= bits_matched of n bits."""
    return sum(comb(n, i) for i in range(bits_matched, n + 1)) / 2 ** n

# (attack, bit accuracy, bits, FPR as printed, false hits per 1e9 as printed)
WATERMARK_TABLE = [
    ("none",                                          1.00, 48, 3.6e-15,          0),
    ("JPEG q=50",                                     0.99, 48, 3.6e-15,          0),
    ("resize 512->256->512",                          0.98, 47, 1.7e-13,          0),
    ("crop to 50% area",                              0.93, 45, 6.6e-11,          0),
    ("screenshot, re-encode, re-upload",              0.88, 42, 5.0e-08,         50),
    ("img2img regeneration, strength 0.4",            0.61, 29, 9.7e-02,  96706326),
    ("round trip through a different autoencoder",    0.55, 26, 3.3e-01, 332732906),
    ("adversarial optimization with detector access", 0.50, 24, 5.6e-01, 557283251),
]

for attack, acc, bits, fpr_want, hits_want in WATERMARK_TABLE:
    p = fpr(round(acc * BITS))
    print(f"{attack:46s} acc={acc:.2f} bits={bits:2d} FPR={p:.3g} "
          f"per 1e9={round(p * 1e9):,}")
    assert round(acc * BITS) == bits, (attack, acc * BITS)
    assert float(f"{p:.2g}") == fpr_want, (attack, p, fpr_want)
    assert round(p * 1e9) == hits_want, (attack, p * 1e9, hits_want)

# The bar the prose quotes: 1e-9 needs 44 of 48, i.e. 0.917 bit accuracy.
assert fpr(44) < 1e-9 <= fpr(43), (fpr(44), fpr(43))
assert round(44 / BITS, 3) == 0.917
print(f"\nFPR <= 1e-9 needs {44}/{BITS} bits = {44 / BITS:.3f} bit accuracy "
      f"(44 -> {fpr(44):.2g}, 43 -> {fpr(43):.2g})")

A watermark is a positive-evidence channel, not a negative one. A detected mark says “we generated this.” An absent mark says nothing at all, because every other generator, every camera, and every re-encode is outside your control. Anyone who proposes watermarking as a deepfake defence has inverted the logic — it is an attribution tool for your own output, and the actual misuse control is upstream: refuse identity-targeted generation, rate-limit and identity-verify the API, and log every generation against an account.

The diagram below puts all six mechanisms on one picture: ingest at the top, serving at the bottom, and one dotted line joining the two halves that carries the diagram’s real point.

Two abbreviations appear in its boxes: pHash is a perceptual hash, a short fingerprint of an image that stays the same when the image is re-compressed or slightly re-cropped. NSFW stands for not safe for work — sexual or graphic content.

flowchart TD
    subgraph ING["Ingest — before any training"]
        SRC["Licensed / consented sources"] --> PROV["Provenance row<br/>source, licence, consent scope"]
        PROV --> AGE{"Age classifier<br/>high recall"}
        AGE -->|"reject"| DROP["Excluded, logged"]
        AGE -->|"pass"| DEDUP["Dedup: pHash + embedding cluster<br/>cap duplicate count at 1"]
    end
    subgraph SRV["Serving — on every output"]
        DEC["Decoded image"] --> NSFW["NSFW + minor classifier"]
        NSFW --> ID{"ArcFace cosine vs<br/>public-figure index<br/>and training index"}
        ID -->|"above threshold"| BLK["Block, sample again,<br/>alert on repeat"]
        ID -->|"below"| WM["Embed invisible watermark"]
        WM --> C2PA["Sign C2PA manifest"]
        C2PA --> OUT(["Deliver"])
    end
    DEDUP --> TRAIN["Training set"]
    TRAIN -.->|"index of every<br/>training embedding"| ID

    style AGE fill:#9d0208,color:#fff
    style ID fill:#9d0208,color:#fff
    style DEDUP fill:#2d6a4f,color:#fff
    style OUT fill:#2d6a4f,color:#fff

The ingest half is everything that happens before any training tensor exists. Three stages, in order:

  1. Licensed and consented sources each get a provenance row: a database record naming the source, the licence identifier and the scope of consent.
  2. An age classifier screens every image. It is tuned for high recall rather than high precision — meaning it flags nearly every true minor even at the cost of flagging adults too, which is the paranoid direction. Anything it rejects is excluded and logged.
  3. What survives goes through dedup (deduplication). pHash catches exact and near-exact copies; the embedding-cluster step catches the near-duplicates the hash misses. Every surviving cluster is capped at one copy.

The serving half runs on every decoded image, before it reaches the user:

  1. An NSFW classifier and a second minor classifier screen the pixels.
  2. ArcFace cosine similarity matches the face against two indexes of known faces — an index here being a searchable store of embeddings. Anything above threshold is blocked and re-sampled; a repeat triggers an alert.
  3. Clean images get the invisible watermark, then a signed C2PA manifest — the small block of provenance metadata described above — and only then are delivered.

Now note the dotted edge running from the training set back into the serving-time check. The training-set embedding index is a serving dependency, because memorization (Failure modes) is detected at the output, not prevented at the input.

Assumptions in this section.


2. The ML objective, and why it is a choice of divergence

With the constraints fixed, the next question is what a generative model is actually optimizing when there is nothing to compare an output against — and the single decision made here, which definition of “close” you pick, already determines which family of model you will end up building.

Start from the fact that there are no labels. The goal is to learn a sampler: a procedure that, when you turn the handle, emits a fresh face.

Two symbols carry the rest of the section. Write p_data for the true distribution of real faces — the invisible rule that says which arrangements of pixels are plausible faces and how often each occurs. Write p_model for the distribution your sampler actually produces. Training means making p_model close to p_data.

But “close” between two probability distributions has to be defined, and the standard family of definitions is called a divergence: a number that is zero when the two distributions are identical and grows as they differ.

Here is the detail that does all the work. Unlike an ordinary distance, a divergence need not be symmetric — the divergence from A to B can differ from the divergence from B to A. Everything downstream follows from which of the two you minimize, and that choice is the model family choice.

The standard divergence is the Kullback-Leibler divergence (KL). It has two orderings, and they are not the same quantity.

Three pieces of notation before the formulas:

forward KL   KL(p_data || p_model) = E_{x ~ p_data} [ log p_data(x) - log p_model(x) ]
reverse KL   KL(p_model || p_data) = E_{x ~ p_model} [ log p_model(x) - log p_data(x) ]

The two lines contain the same two distributions and differ only in which one the average is taken over. Now read each one and watch what that does.

Forward KL takes its average over the data. Suppose there is a kind of face that exists in the data but that the model never produces: p_data(x) > 0 while p_model(x) -> 0. The bracketed quantity is log p_data(x) - log p_model(x), which is log(p_data/p_model), and as p_model goes to zero that ratio goes to infinity, so the log does too. That point is in the average, because the average runs over data.

So the model is infinitely punished for missing anything. It responds by spreading mass to cover everything — including the space between modes, where no real face actually lives. An image sampled from the gap between two clusters of real faces is a smeared average of both. Mode-covering, and the price is blur.

Reverse KL takes its average over the model. Take the same situation: p_data(x) > 0 but p_model(x) = 0. This time the average runs over samples from the model, and the model never samples there, so that point contributes nothing at all. Missing a mode is free.

The only penalty left is for putting mass where the data has none, which pushes the model to stay inside regions the data covers. Mode-seeking, and the price is dropped modes — whole kinds of face the model silently stops producing.

The diagram below sorts the four model families onto the two sides of that split. The model family comparison derived derives them properly; here is the one-line version of each, so the boxes mean something when you get to them.

Mode collapse is the reverse-KL failure by its usual name: the generator quietly concentrates on a few kinds of output and abandons the rest.

flowchart TD
    G{"Which divergence<br/>do you minimize?"}
    G -->|"forward KL<br/>expectation over DATA"| FK["MODE-COVERING<br/>infinite penalty for missing mass<br/>covers everything, including<br/>the gaps between modes"]
    G -->|"reverse KL<br/>expectation over MODEL"| RK["MODE-SEEKING<br/>no penalty for missing a mode<br/>sharp, and silently incomplete"]
    FK --> A["Autoregressive: exact forward KL<br/>VAE: an ELBO, so a bound on it<br/>Diffusion: a reweighted ELBO"]
    RK --> B["GAN: JS in theory, but the<br/>non-saturating generator loss<br/>is an expectation over G samples only"]
    A --> C1["Symptom: blur, over-dispersion,<br/>capacity spent on invisible detail"]
    B --> C2["Symptom: mode collapse,<br/>demographic dropout, sharp output"]

    style FK fill:#1d3557,color:#fff
    style RK fill:#9d0208,color:#fff
    style C1 fill:#bc6c25,color:#fff
    style C2 fill:#9d0208,color:#fff

For a face generator the asymmetry is not aesthetic, it is legal. A mode-covering model that produces slightly soft faces is a quality bug. A mode-seeking model that quietly stops generating an entire demographic is a fairness incident that FID will not show you (Offline metrics). That sentence alone should push you toward the maximum-likelihood family — the families that optimize forward KL or a bound on it — before you have drawn a single layer.

Assumptions in this section.


3. Data and labels

The corpus decisions come next, and two of them are not the housekeeping they look like: one preprocessing step quietly redefines what the model can ever produce, and removing duplicate photographs is a legal control rather than tidiness. A trap also waits in the headline quality metric, which rewards exactly the bias you are trying to remove.

Scale. Between 2 and 5 million aligned faces is enough for 512 × 512 output. Beyond that, additional quality comes from curating what you already have rather than from adding volume.

The alignment pipeline, and what it costs. Alignment means putting every face in the same place in the frame before training. Detect the face, extract five landmarks (the two eye centres, the nose tip and the two mouth corners), apply a similarity transform — a rotation, uniform scale and shift, the only operations that move a picture without distorting its shape — so that the eye centres land on fixed coordinates, then crop and resize.

Alignment removes a nuisance factor of variation, a source of variability in the data that carries no information you want. The model no longer spends capacity learning that a face can appear anywhere in the frame, so the same parameter budget buys more identity and texture detail instead. The cost is that the model’s support shrinks to exactly the alignment you imposed — its support being the set of images it assigns any probability to at all. Ask a model trained on an FFHQ-style aligned corpus (Flickr-Faces-HQ, the standard public aligned-face dataset) for a three-quarter profile at the edge of frame and you get a warped centred face, because off-centre faces have probability zero under the training distribution. If the product needs varied framing, weaken the alignment during training and pay for it in the capacity that would otherwise have bought resolution.

Deduplication, and why it is a safety control rather than a hygiene step. Run a perceptual hash to catch exact and near-exact copies, then cluster embeddings at cosine similarity 0.95 to catch the same photograph re-cropped or re-compressed. Cap the surviving count of any cluster at one copy. The reason this matters is derived in Memorization traced: the number of times an image appears enters the model’s effective loss linearly, so a photograph present thirty times pulls thirty times as hard, and memorizing that specific person is the direct consequence.

Attribute labels. Unconditional generation — sampling a face with no request attached — needs no labels at all. You need them anyway, for two jobs:

  1. Conditioning, meaning steering the output toward a requested attribute, if the product exposes controls at all.
  2. Auditing. You cannot claim demographic coverage without measuring it. Label a stratified sample — one deliberately drawn to include enough of every group, rather than sampled uniformly and hoping — by skin tone (a 10-point perceptual scale, not a race category), apparent age band, and pose. Label it with a model first and have humans adjudicate the 15% of cases where the model was least confident.

The bias mechanism, and the trap in the metric. A marginal here is the model’s overall breakdown across one attribute — what fraction of its outputs land in each skin-tone bin, ignoring everything else. If 68% of training faces sit in the lightest three skin-tone bins, the model’s marginal reproduces that 68%. That much is expected and unsurprising. The trap is what happens next:

FID is a distance to a REFERENCE SET.
Reference set = a held-out split of the same skewed corpus.
=> a model that faithfully reproduces the skew scores WELL.
=> the better your FID, the more exactly you have reproduced the bias.

FID rewards matching your reference set, so it cannot be the detector for a problem your reference set has.

The fix is to write down a separate target marginal — the demographic breakdown you have decided the product should produce, constructed deliberately rather than inherited from the corpus — and measure the model against that instead.

The measurement is total-variation distance (TV distance): half the sum of the absolute differences between the two breakdowns, bin by bin. Worked on a three-bin toy example, with target [0.40, 0.35, 0.25] and measured output [0.52, 0.33, 0.15]:

|0.40 - 0.52| + |0.35 - 0.33| + |0.25 - 0.15|   =  0.12 + 0.02 + 0.10  =  0.24
TV = 0.24 / 2 = 0.12

The halving is what makes the answer land in [0, 1]: 0 means the two breakdowns match exactly, 1 means they share no bin at all. The 0.12 above would fail the 0.03 cap this chapter uses as a guardrail in The other failure modes. Report TV distance alongside FID and never fold the two into a single score.

Assumptions in this section.


4. The model family comparison, derived

All four candidate model families grow out of a single shared difficulty, and each family’s famous weakness is the direct, unavoidable price of the way it dodges that difficulty. That derivation is the thing to be able to reconstruct rather than recite.

Here is the shared difficulty. Training any of these models means pushing up the probability the model assigns to real images.

But writing down that probability requires a density: a formula giving the probability of any particular arrangement of pixels. For a 512 × 512 image no such formula is computable — normalizing it means summing over every possible image, and there are more of those than there are atoms in the universe. The word for this is intractable: correct in principle, impossible to evaluate in practice.

So every family below is a different trick for extracting a usable gradient — the direction in which to nudge the network’s weights to improve — without ever evaluating that density. Each trick has a cost, and each cost shows up as that family’s signature failure. That is the pattern to hold onto: the famous weakness is not a bug in the family, it is the receipt for the dodge.

GAN — a learned divergence

A GAN (generative adversarial network) sidesteps the density entirely by hiring a second network to judge the first. A generator G(z) turns a vector of random numbers z into an image; a discriminator D(x), also called the critic, is a classifier trained to output the probability that the image x it is shown is real rather than generated. The two play a game:

min_G max_D   E_{x ~ p_data}[log D(x)]  +  E_{z}[log(1 - D(G(z)))]

Read min_G max_D as “the discriminator tries to maximize this quantity while the generator tries to minimize it”. The first term rewards D for calling real images real; the second rewards it for calling generated images fake.

Where the divergence comes from, in two steps.

Step one: freeze the generator and ask what the best possible discriminator looks like. At each point x the objective is a simple expression in D(x) alone, and maximizing it gives

D*(x) = p_data(x) / (p_data(x) + p_g(x))

where p_g is the distribution the generator currently produces. That formula is worth reading in plain English: the ideal critic reports the share of the local density that comes from real data. Where the two distributions overlap perfectly it returns 0.5 — a shrug.

Step two: substitute D* back into the game. What is left is a function of p_data and p_g only, and it works out to

2 · JSD(p_data || p_g)  -  log 4

where JSD is the Jensen-Shannon divergence introduced in The ml objective and why it is a choice of divergence. The - log 4 is a constant, so minimizing this over the generator is minimizing JSD. (Sanity check: when p_g = p_data, D* is 0.5 everywhere, the objective is log 0.5 + log 0.5 = -log 4, and JSD is 0. The two agree.)

So the discriminator’s job is to estimate a divergence that has no closed form, and the generator walks downhill on that estimate. That is the whole idea, and both failure modes fall out of it.

Failure 1 — mode collapse, traced. The generator’s loss is an average over the generator’s own samples. There is no term anywhere that sums over data points the generator fails to produce. So “I never generate this kind of face” costs the generator nothing directly — it costs only indirectly, if the discriminator notices the imbalance. And the discriminator is a finite network trained on minibatches, small random subsets of the data seen a few dozen or hundred images at a time, so it notices slowly.

The diagram below is that dynamic as a cycle. Follow the arrows and note that the last one loops back to the second box — the system returns to a state it was already in, having improved nothing.

flowchart LR
    S1["G covers modes A and B"] --> S2["D learns: mode A samples<br/>have a tell"]
    S2 --> S3["G's cheapest descent:<br/>move ALL mass to B<br/>loss drops immediately"]
    S3 --> S4["D re-fits: now B<br/>is the giveaway"]
    S4 --> S5["G moves all mass back to A"]
    S5 --> S2

    style S3 fill:#9d0208,color:#fff
    style S5 fill:#9d0208,color:#fff

Trace that loop and notice what is missing: there is no single number that decreases from start to finish. Ordinary training gives you a loss curve that goes down and tells you when to stop; this is a two-player game whose dynamics can circle a saddle point — a configuration that is a minimum along one direction and a maximum along another — forever. Concretely, on a face model: identity diversity (Face specific metrics you must add) drops from 0.41 to 0.09 over 20,000 training steps while the discriminator loss looks perfectly healthy, because a healthy-looking discriminator loss is exactly what a well-matched adversary produces.

Failure 2 — vanishing generator gradient. If the discriminator gets too good, D(G(z)) -> 0, and the original saturating generator loss log(1 - D(G(z))) flattens out. Its gradient goes to near zero exactly where the generator most needs a push.

The standard non-saturating fix, max_G log D(G(z)), restores the gradient but abandons the clean Jensen-Shannon interpretation derived above. That is why GAN training is a pile of empirical stabilizers rather than one derivation. The three you should be able to name:

Every one of them is a knob with no principled setting, retuned per dataset. Hold onto that: it is reason 2 in the pick below.

What you get in exchange: sampling is a single forward pass — one sweep of data through the network, with no loop. That is 10-30 milliseconds per image on a modern GPU, one to two orders of magnitude faster than anything else in this comparison.

VAE — a bound, and why the bound blurs

A VAE (variational autoencoder) dodges the intractable density a second way: instead of computing it, maximize a quantity provably below it. An encoder q(z|x) compresses an image x into a short vector z called the latent — a compact internal code, far smaller than the image, from which the image can be approximately rebuilt — and a decoder p(x|z) expands the code back into pixels. The quantity being maximized is the ELBO (evidence lower bound), and it splits into two readable pieces:

log p(x)  >=  E_{q(z|x)}[ log p(x|z) ]  -  KL( q(z|x) || p(z) )
              \_____ reconstruction _____/    \____ regularizer ____/

The first term rewards rebuilding the image accurately. The second is a regularizer — a term that exists to constrain the solution rather than to fit the data — pulling the encoder’s output toward a simple fixed reference distribution p(z), the prior, usually a standard bell curve. That pull is what makes the latent space smooth enough that you can draw a random z from the prior at generation time and get a sensible face out.

Why the output is blurry, derived. Take the usual Gaussian decoder with fixed variance sigma^2 — that is, assume the decoder predicts a mean image mu(z) and that the true image scatters around it like a bell curve of fixed width:

-log p(x|z)  =  || x - mu(z) ||^2 / (2 sigma^2)  +  const

The reconstruction term is therefore an L2 loss: the sum of squared differences, pixel by pixel. And the prediction that minimizes squared error when the answer is uncertain is the conditional mean — the average of every outcome still consistent with what you know. If a given latent z is consistent with several plausible fine-detail completions — the exact placement of stubble, the exact strand pattern of hair — then the loss-minimizing output is their average, and the average of several plausible textures is smooth. Blur is not an artifact of insufficient capacity; it is the exact minimizer of the objective.

The regularizer term makes it worse in a controllable way. Raise its weight (conventionally called beta) and the encoder is pulled harder toward the prior, so its output carries less information about the specific image, which increases how much is left uncertain per latent code, which makes the conditional mean an average over a wider set of possibilities. The reconstruction/regularizer balance is a dial between sharpness and a well-behaved latent space, and both ends are bad. Set beta too low and the latent space develops holes — regions you can draw a sample from that the decoder has never seen and turns into garbage. Set it too high and you get posterior collapse: the decoder learns to ignore z entirely and emits the average face of the whole dataset regardless of input.

Autoregressive — exact likelihood, sequential sampling

An autoregressive model dodges the intractable density by refusing to dodge it — it splits the impossible whole-image probability into a chain of easy one-piece-at-a-time probabilities. Write p(x) = prod_i p(x_i | x_<i): the probability of the image is the probability of the first piece, times the probability of the second given the first, and so on. Fix a raster order — left to right, top to bottom, the order a printer would use — and every factor becomes an ordinary classification problem. The result is one loss, one network, no adversary, textbook-stable training, and a genuine likelihood number (how much probability the model assigns to real data) that you can compare across models.

Three costs, and the third is the one that matters here:

  1. Generating one image takes n sequential forward passes, one per piece, because each piece has to be produced before the next can be conditioned on it. At 1,024 pieces that is 1,024 passes run strictly one after another, with the same asymmetry between the parallel first pass and the serial per-step passes described in The kv cache the most important mechanism in this chapter: each step moves the whole model’s weights from memory to do very little arithmetic, so it is limited by memory bandwidth rather than compute, and the steps within a single image cannot be batched together because each depends on the last.
  2. Raster order is a wrong prior — a prior being an assumption baked into the model before it sees any data. Nothing about an image says the pixel at position (i, j) depends causally on the row above it. The model spends capacity learning to undo an ordering you imposed for your own convenience.
  3. Likelihood in pixel space is a bad stand-in for how good an image looks. Most of the information content of a photograph is fine-grained detail nobody looks at. A model can substantially cut its NLL (negative log-likelihood, the loss being minimized — lower is better) by modelling camera sensor noise more precisely and look no better at all to a human. Exact likelihood is exactly what you asked for and not what you wanted.

Diffusion — a fixed corruption, a learned reversal

Diffusion dodges the intractable density by turning generation into a long sequence of tiny, easy denoising problems. Define a forward process that destroys an image by adding noise on a fixed, non-learned schedule — a pre-decided recipe saying how much noise to add at each of T steps, with no parameters to train — and then learn a network that undoes one step of it. Generation runs the learned reversal from pure noise back to an image.

forward   q(x_t | x_0) = N( sqrt(abar_t) · x_0 ,  (1 - abar_t) · I )
          x_t = sqrt(abar_t)·x_0 + sqrt(1 - abar_t)·eps,   eps ~ N(0, I)

training  L = E_{x0, t, eps} || eps - eps_hat(x_t, t) ||^2

Read the notation once and the rest of the chapter opens up:

With those in hand, the two lines say something simple. The second line says the noised image is a fixed blend of the clean image and pure noise, with the blend weights sqrt(abar_t) and sqrt(1 - abar_t) set by the schedule — so you can jump straight to any step t in one shot, without simulating the ones before it. The third line says training is squared error between the noise that was actually added and the noise the network guessed. That is all: draw an image, draw a step, draw some noise, add it, ask the network what you added.

Why it trains where GANs do not, in one sentence: the target is fixed. The noise eps is drawn from a random number generator, not predicted by a second network that is itself learning. Every training step is therefore ordinary supervised regression against a known answer, so there is one number that goes down and stays down, and there is no game. The adversary has been replaced by a corruption process with a closed-form formula, and everything that made GAN training a research project disappears with it.

Why sampling is slow. The reverse of a noising step is only itself a clean Gaussian when the step is small, so you cannot leap from pure noise to an image in one move — you evaluate the network T times in sequence. That is the entire weakness of the family, and Sampling ddpm ddim and the step count curve is about removing it.

The comparison

Here are the four families side by side. Three entries in the table need unpacking first:

The last two are post-hoc: applied to an already-trained model, with no retraining. That is the whole content of the “Controllability” row.

Two rows decide this table, and it is worth reading them before the others: Mode coverage, because that is the failure with legal exposure, and Sampling cost, because that is what diffusion is bad at. Everything after the table is an argument about which of those two you would rather be wrong about.

GANVAEAutoregressiveDiffusion
Objectivelearned JS via a criticELBO (bound on forward KL)exact forward KLreweighted ELBO / denoising score matching
Sample qualityhighlow, blurryhighhighest
Mode coveragepoor — no term penalizes omissiongood but smearedbest — likelihood punishes any missed massgood
Training stabilitypoor — two-player game, no single decreasing lossgoodbest — one convex-ish lossgood
Sampling cost1 forward pass1 forward passn sequential passesT sequential passes
Exact likelihoodnolower bound onlyyeslower bound only
Controllabilitylatent arithmetic, awkwardlatent arithmeticprefix conditioningguidance, inpainting, ControlNet — all post-hoc
Signature failuremode collapseblur / posterior collapseslow, wrong ordering priorslow sampling

The pick, and the argument

The pick is latent diffusion: run the diffusion process not on pixels but on the compact code produced by a separately trained autoencoder, and decode to pixels once at the end. (Why the latent rather than pixels is a cost argument, derived in Scale and cost and in full in chapter 08; the family choice and the space it runs in are two separate decisions.) Three reasons, in the order that survives pushback:

  1. The failure I cannot tolerate is the one GANs have. Mode collapse in a face model means silently dropping demographics. That is a fairness incident with a legal surface, and it is invisible to the headline metric (Offline metrics). Diffusion’s failure is slow sampling — a cost problem with four known engineering fixes.
  2. Retraining cadence. Erasure requests and licence expiry mean this model gets retrained on a changing corpus several times a year. A training procedure that needs a stabilizer tuned per dataset is an operational liability. Diffusion’s loss curve looks the same every time.
  3. Controllability arrives for free. Attribute conditioning, inpainting, identity-preserving edits, and safety-relevant steering are all applied after training, to the sampling loop. On a GAN each of them is a separate research effort against a latent space you did not design.

State the tradeoff you are accepting, out loud: “I am choosing a model that is 60× more expensive to sample than a GAN, because the GAN’s cheapness is paid for with a failure mode I cannot detect and cannot fix at serving time. Sampling cost is fixable at serving time — distillation takes 60 forwards to 4.”

Distillation, used in that sentence and throughout the rest of the chapter, means training a second, cheaper model — the student — to reproduce in a few steps what the trained model — the teacher — takes many steps to produce.

Assumptions in this section.


5. Diffusion mechanics

The family choice is made; now open the model up. The machinery comes in three pieces: a training objective that predicts the noise rather than the clean image, a noise schedule in which each stage decides something specific about the picture, and classifier-free guidance — the single knob that trades diversity for fidelity at generation time.

Start with the shape of the whole process: a top path that destroys an image and a bottom path that rebuilds one — and only one of the two has learned parameters in it.

flowchart LR
    X0(["x_0<br/>real face"]) -->|"q: add noise<br/>fixed, no parameters"| XT1["x_t<br/>partly noised"]
    XT1 -->|"q"| XT["x_T<br/>~ N 0, I"]
    XT -->|"p_theta: predict eps<br/>learned"| RT1["x_t-1"]
    RT1 -->|"p_theta"| RX["x_0 hat<br/>generated face"]
    XT1 -.->|"training target is<br/>the eps that was added"| LOSS["L = || eps - eps_hat ||^2"]

    style X0 fill:#2d6a4f,color:#fff
    style XT fill:#1d3557,color:#fff
    style RX fill:#2d6a4f,color:#fff
    style LOSS fill:#bc6c25,color:#fff

Along the top, q is the fixed, parameter-free noising process that walks a real face x_0 through partly-noised states x_t to pure noise x_T. p_theta is the learned reversal — theta is the conventional symbol for a network’s trainable parameters — which walks back down to a generated face x_0 hat, the hat meaning “the model’s estimate of”. The dotted line is the training signal: at every intermediate state, the answer the network is graded against is the noise that q actually added.

Why predicting noise works

Here is a question that looks like a technicality and is not: given that the network could equally be trained to output the clean image, why is every real system trained to output the noise instead?

Begin with the blending formula, x_t = sqrt(abar_t)·x_0 + sqrt(1 - abar_t)·eps. It has three quantities in it and you can always solve for the third given the other two — it is invertible in either unknown:

x_0 = ( x_t - sqrt(1 - abar_t)·eps ) / sqrt(abar_t)

So predicting the noise eps and predicting the clean image x_0 carry identical information: knowing either one, plus the noised image you were handed, gives you the other for free.

They are nevertheless not identical objectives, and the reason is visible in that inversion formula. Suppose the network’s noise prediction is off by an amount e. Look at where eps sits in the formula for x_0: it is multiplied by sqrt(1 - abar_t) and then the whole thing is divided by sqrt(abar_t). So the error is magnified by that same factor:

an eps error of size e  ->  an x_0 error of  e · sqrt(1 - abar_t) / sqrt(abar_t)

Define the signal-to-noise ratio at step t as SNR_t = abar_t / (1 - abar_t) — how much of the clean image is still present relative to how much noise is drowning it. High SNR means an almost-clean picture; low SNR means almost pure noise. Then sqrt(1 - abar_t)/sqrt(abar_t) is exactly 1/sqrt(SNR_t), and the magnification factor is

an eps error of size e  ->  an x_0 error of  e / sqrt(SNR_t)

Read that backwards and you have the reweighting. If an eps error of e costs e / sqrt(SNR_t) in x_0, then squaring both sides says a squared-error loss on eps with equal weight at every step is a squared-error loss on x_0 scaled by SNR_t. High-noise steps have low SNR, so they get a small weight. Low-noise steps have high SNR, so they get a large one.

That weighting is not arbitrary — it down-weights the high-noise steps by exactly the amount that the clean image is genuinely unpredictable there. That reweighting is the difference between diffusion and the plain VAE-style bound in Vae a bound and why the bound blurs, and it is why diffusion does not blur.

Model capacity is spent on the low-noise steps, where the conditional mean is nearly a single point and averaging costs nothing. It is not spent on the high-noise steps, where averaging over many possible faces is unavoidable and would smear them together.

Two more reasons the noise is the better thing to predict:

The code below evaluates the schedule at five points. It uses the cosine schedule, the standard modern choice for how abar_t falls from 1 to 0 across the T steps.

import math

def cosine_abar(u: float, s: float = 0.008) -> float:
    """alpha_bar at t/T = u for the cosine schedule. abar(0) ~ 1, abar(1) = 0."""
    f = lambda v: math.cos((v + s) / (1.0 + s) * math.pi / 2.0) ** 2
    return f(u) / f(0.0)

def snr(abar: float) -> float:
    return abar / (1.0 - abar)

def x0_error_gain(abar: float) -> float:
    """An eps error of size e becomes an x_0 error of e / sqrt(SNR)."""
    return math.sqrt((1.0 - abar) / abar)

# Every row of the table printed below this block, so the prose and the code
# cannot drift apart: (t/T, abar to 4 dp, SNR, dp the table shows SNR to, gain to 2 dp).
SCHEDULE_TABLE = [
    (0.10, 0.9721, 34.83,  2, 0.17),
    (0.25, 0.8470,  5.54,  2, 0.42),
    (0.50, 0.4938,  0.976, 3, 1.01),
    (0.75, 0.1443,  0.169, 3, 2.44),
    (0.90, 0.0241,  0.0247, 4, 6.36),
]

for u, abar_want, snr_want, snr_dp, gain_want in SCHEDULE_TABLE:
    a = cosine_abar(u)
    print(f"t/T={u:.2f}  abar={a:.4f}  SNR={snr(a):8.3f}  x0 gain={x0_error_gain(a):6.2f}")
    assert round(a, 4) == abar_want, (u, a)
    assert round(snr(a), snr_dp) == snr_want, (u, snr(a))
    assert round(x0_error_gain(a), 2) == gain_want, (u, x0_error_gain(a))

# The claim the section is built on: an eps error costs more in x_0 the noisier
# the step, and the gain crosses 1 exactly where SNR does.
assert all(x0_error_gain(cosine_abar(u)) < x0_error_gain(cosine_abar(v))
           for u, v in zip((0.10, 0.25, 0.50, 0.75), (0.25, 0.50, 0.75, 0.90)))
assert x0_error_gain(cosine_abar(0.50)) > 1.0 > x0_error_gain(cosine_abar(0.25))

The table below is what that code prints, plus a last column naming what each part of the schedule is deciding about the picture. Check one row by hand before reading the rest. At the halfway point t/T = 0.50 the schedule gives abar = 0.4938, so SNR = 0.4938 / (1 - 0.4938) = 0.4938 / 0.5062 = 0.976, and the error gain is 1 / sqrt(0.976) = 1.01. Signal and noise are balanced, and an eps error passes through to x_0 roughly unchanged.

t/Tabar_tSNR_tx_0 error gainWhat this step decides
0.100.972134.830.17pore-level texture, sharpening
0.250.84705.540.42skin detail, hair strands
0.500.49380.9761.01identity, features
0.750.14430.1692.44pose, lighting direction
0.900.02410.02476.36global layout, background split

Read the right-hand column as a budget. Each step of the sampling loop is deciding something specific about the picture, and the high-noise steps decide the things that matter most: the layout, the pose, the identity. The t/T <= 0.25 band — roughly a quarter of the steps, at the low-noise end — decides things a human never consciously inspects. That is why aggressive step reduction (Sampling ddpm ddim and the step count curve) is possible at all, and why the steps you cut must come from the low-noise end.

Classifier-free guidance

Classifier-free guidance (CFG) is the standard way to make a diffusion model obey its request more strongly than it naturally would. It deserves a derivation rather than a recipe, because the derivation is what tells you why turning it up costs you diversity.

The training change is one line: train a single network to handle both conditional inputs (the request is supplied) and unconditional ones (no request), by simply deleting the condition with probability 0.1 during training. That gives you one network that can be asked twice — once with the request, producing a noise prediction eps_c, once without, producing eps_u. At sampling time you do not use either directly. You extrapolate past the conditional prediction, in the direction that leads away from the unconditional one:

eps_guided = eps_u + w · (eps_c - eps_u)

At w = 1 this is exactly eps_c and nothing has changed. At w > 1 you have overshot past the conditional prediction. The name is “classifier-free” because earlier methods steered a diffusion model by attaching a separately trained image classifier; this achieves the same steering with no classifier at all.

Where that comes from. Consider sampling not from p(x|c) but from a sharpened version of it, one that raises the implied classifier p(c|x) — the probability that an image x matches request c — to a power:

p_w(x | c)  ~  p(x | c) · [ p(c | x) ]^(w - 1)

grad log p_w(x|c) = grad log p(x|c) + (w-1)·grad log p(c|x)
                  = grad log p(x|c) + (w-1)·[ grad log p(x|c) - grad log p(x) ]
                  = grad log p(x)   +  w  ·[ grad log p(x|c) - grad log p(x) ]

Take those three lines one at a time. grad here means the gradient with respect to the image x, so every term is a score in the sense defined above.

The result is written entirely in terms of two scores the network already produces: the conditional one and the unconditional one. Convert scores back into noise predictions with eps = -sqrt(1 - abar)·grad log p — the same score/noise identity established above — and the guidance formula eps_u + w·(eps_c - eps_u) falls out exactly.

So the guidance scale is an exponent on a classifier. Raising p(c|x) to the power w concentrates probability mass where the implied classifier is most confident — the interior of the conditional mode, the typical, unambiguous examples — and suppresses the rare-but-valid faces at its edges. That is the fidelity/diversity tradeoff, and it is not a heuristic bolted on by practitioners; it is what an exponent does to a probability.

The table below measures it. Precision and recall are defined properly in Precision and recall for generative models; for now, precision is the fraction of generated faces that look like real faces (fidelity) and recall is the fraction of the real variety the model still produces (coverage). Posterized means the smooth tonal gradients have collapsed into visible flat bands, and blown highlights means the bright areas have saturated to featureless white.

wFIDPrecision (fidelity)Recall (coverage)Symptom
1.08.10.580.68no guidance, soft, maximally diverse
1.55.90.680.62FID optimum
3.06.80.760.51FID already worse, humans prefer it
7.011.40.830.36saturated skin, hard shadows
12.019.70.850.24posterized, blown highlights

FID is minimized at w ≈ 1.5 and humans prefer w ≈ 3. Both statements are true and they are the clearest evidence that FID is not the objective — it penalizes the diversity loss that human raters, judging one image at a time, structurally cannot see.

The saturated, posterized look at high w has a mechanism too. The guided update extrapolates past the conditional prediction, so the sample drifts out of the region of latent space the decoder was trained on. The statistics of the latent code drift with it, the decoder renders values outside the range it knows how to produce, and the final clamp that forces every pixel into the valid range [-1, 1] clips them flat. The standard fix is to rescale the guided prediction so its spread matches the conditional prediction’s, which recovers most of the tonal range at high w.

Assumptions in this section.


6. Training

Mechanics settled, the next bill is training. The number to walk away with is the compute budget: roughly $13,100 of GPU time for the model itself and about $50,000 all in — and every figure in it falls out of the network spec, so the spec comes first.

Model. The backbone is a DiT-XL/2 — a diffusion transformer, meaning the denoising network is an ordinary transformer of the kind used for language rather than the convolutional U-Net that diffusion models originally used. XL names the size and /2 names the patch size.

Where the token count comes from. The model runs in the latent space of an f=8 autoencoder, where f=8 means the autoencoder shrinks the image by a factor of 8 along each side. Follow the arithmetic:

image                512 × 512 pixels
÷ 8 (the f=8 autoencoder)   ->   64 × 64 latent grid, 4 numbers per cell
group into 2 × 2 patches    ->   32 × 32 patches
                                 32 × 32  =  1,024 tokens

A token is one element of the sequence a transformer attends over. 1,024 of them is the number every FLOP count in this chapter runs on.

Where the parameter count comes from. d is the model’s width, the length of the vector each token is represented by, and L is its depth, the number of stacked transformer blocks.

d = 1152,  L = 28,  heads = 16
d^2 = 1152 · 1152 = 1,327,104

per-token parameters   12·L·d^2         = 12 · 28 · 1,327,104   = 445.9M
adaLN-zero conditioning MLPs  6·d^2 per block, applied to ONE vector, not per token
                              28 · 6 · 1,327,104                = 223.0M
                                                                  --------
total                                                             668.9M  ->  "675M"

12·L·d^2 is the standard count of the parameters that act on every token: roughly 4·d^2 for the attention projections plus 8·d^2 for the feed-forward network at the usual 4× width, per block, times L blocks.

adaLN-zero is how the request and the timestep are fed in. It stands for adaptive layer normalization: the per-block normalization step has its scale and shift produced by a small network from the conditioning information, rather than learned as fixed constants. Those small networks are MLPs (multi-layer perceptrons — plain stacks of fully connected layers), initialized to zero so each block starts as a pass-through.

The crucial detail is that adaLN acts on one conditioning vector per image, not on every token. That per-token/per-vector split matters for the arithmetic in Scale and cost: only the 445.9M is multiplied by the token count. The full 668.9M is what you quote as the model size; the 445.9M is what you multiply by 1,024.

Five recipe choices worth defending:

  1. Keep an exponentially moving average (EMA) of the weights, with decay 0.9999, and sample from the averaged copy only. An EMA is a slowly updated running average of the weights — each step it moves 0.0001 of the way toward the current weights — so it smooths out the jitter of training. This matters far more here than in ordinary supervised learning because a small systematic bias in the denoiser is applied again at every one of the T sampling steps. The sampler feeds its own output back in, so a consistent error accumulates rather than cancelling. Averaging out the noise of stochastic gradient descent (SGD, the standard weight-update procedure, which is noisy because each step sees only a random minibatch) is worth 3-5 FID points routinely, which is a larger effect than most architecture changes.
  2. Force the noise schedule to end at zero signal. The older scaled-linear schedule leaves abar_T ≈ 0.0047 at the final step. Recall from the blending formula that the clean image enters with weight sqrt(abar_t), and sqrt(0.0047) ≈ 0.069 — so the “pure noise” the model trained on still carries about 7% of the original image’s signal, including its average brightness. At generation time you start from actual pure noise, which the model has never seen. The result is a model that cannot produce very dark or very bright images: it always reverts to the training set’s average brightness, because during training the leaked signal always told it what brightness to expect. The fix is to set the schedule so abar_T = 0 exactly, and switch the network’s output from noise to v-prediction — a blended target that stays numerically well-behaved at the endpoint where predicting pure noise becomes meaningless.
  3. Horizontal flip is the only data augmentation. Augmentation means synthetically varying training images — recolouring, cropping, adjusting contrast — to get more mileage out of a corpus. A classifier can do that freely because it is supposed to be blind to those changes. A generative model has no such blindness to exploit: it is learning the distribution of images itself, so any augmentation that alters colour, crop or contrast is a deliberate corruption of the very thing being modelled. A mirror flip is safe only because a mirrored face is a face.
  4. Drop the condition on 10% of training examples. This is required for guidance, as derived in Classifier free guidance, and it doubles as a memorization control (Memorization traced).
  5. Use bf16 arithmetic throughout, not fp16. Both are 16-bit floating-point number formats, half the size of the usual 32-bit one, and both halve memory traffic; bf16 trades precision for a much wider range of representable magnitudes. The noise targets are well-scaled but the internal attention scores are not, and a value overflowing to NaN (“not a number”, which then poisons every subsequent computation) at training step 300,000 costs a day of the run.

Compute, derived. A FLOP is one floating-point operation and a TFLOP is a trillion of them (1e12). The standard accounting is two FLOPs per parameter per token for the forward pass, and roughly twice that again for the backward pass that computes gradients — so a full training step costs about three forward passes.

The block below runs that accounting from the model spec down to a dollar figure. Every line is one substitution.

forward FLOPs per image  =  2 · 445.9e6 · 1024        (token-linear term)
                          + 4 · 1024^2 · 1152 · 28    (attention term)
                         =  0.913e12 + 0.135e12  =  1.05 TFLOP

training step   = 3 × forward (fwd 2N, bwd 4N)         = 3.15 TFLOP / image seen
images seen     = 1.8e9
total           = 3.15e12 × 1.8e9                      = 5.67e21 FLOP
H100 at 300 TFLOP/s effective end to end               -> 5.67e21 / 300e12
                                                        = 1.89e7 s
1.89e7 s / 86,400 s per day                            = 219 GPU-days
219 / 64 H100s                                         = 3.4 days
1.89e7 s / 3,600 × $2.50/GPU-hour                      = $13,100

with the autoencoder, ablations, and two failed runs: budget ~$50k end to end.

The forward-pass cost splits into two terms, and it is worth seeing which one dominates. The token-linear term is the cost of the ordinary matrix multiplications, which grows in proportion to the number of tokens n. The attention term is the cost of every token comparing itself against every other token, which grows with n^2.

Here the ratio is 0.913 : 0.135, about 6.75 : 1 in favour of the token-linear term. The two terms would be equal at n = 6d tokens — set 2·(12·L·d^2)·n = 4·n^2·d·L, cancel 4·L·d·n from both sides, and you get 6d = n; the same result is derived in Parameter arithmetic you should be able to do in your head. At d = 1152 that crossover is 6 × 1152 = 6,912 tokens, and this model runs at 1,024, well below it.

“Attention is quadratic” is a true statement here and is not what you are paying for. It becomes what you are paying for in chapter 08, at four times the resolution.

Assumptions in this section.


7. Offline metrics

A trained checkpoint now has to be judged, and the single number the whole field quotes for that job is the wrong one to ship on. The argument runs from FID’s exact definition, through the six specific ways it misleads, to the set of metrics that actually catches the failures this product has.

FID, and what it actually measures

FID stands for Fréchet Inception Distance, and it is worth defining precisely, because every criticism of it is visible in the definition.

The recipe is four steps:

  1. Push both the real images and the generated images through Inception-v3, a well-known image classifier trained on the ImageNet dataset.
  2. Take the 2048 numbers each image produces at the network’s pool3 layer — one layer before the classification head — as that image’s feature vector. You now have two clouds of points in 2048-dimensional space, one per set.
  3. Fit a Gaussian to each cloud. A multi-dimensional Gaussian is summarized entirely by a mean vector and a covariance matrix, so this step throws away everything about the cloud except those two.
  4. Report the Fréchet distance between the two Gaussians, also known as the Wasserstein-2 distance, which is the standard way of measuring how far apart two such bell curves are:
FID = || mu_r - mu_g ||^2  +  Tr( S_r + S_g - 2 (S_r · S_g)^(1/2) )

mu_r and mu_g are the mean feature vectors of the real and generated sets, S_r and S_g are their covariance matrices — the tables recording how each of the 2048 features varies with each other — and Tr is the trace, the sum down the diagonal of a matrix. Lower is better; zero means the two feature clouds have identical means and identical covariances.

That is the whole definition, and everything wrong with FID is visible in it. Six flaws, in the order an interviewer will probe them:

  1. It sees only the first two moments of a 2048-dimensional distribution — the moments being the summary statistics of a distribution, of which the first is the mean and the second the covariance. Any structure beyond those two is invisible to the score.

  2. It is a distribution metric, so per-sample failures do not register. A model emitting 1% grotesque images and 99% excellent ones moves FID by a fraction of a point. Users notice the 1%.

  3. It compresses fidelity and coverage into a single number. A sharp but mode-collapsed model and a diverse but mediocre one can score identically — see the precision/recall table below.

  4. It is biased when computed on few samples, and the bias is large. A 2048 × 2048 covariance matrix has about 4.2 million entries (2048^2), and estimating that many quantities stably needs samples on the same order. A FID computed on 1,000 images is nowhere near that, and the shortfall does not average out — it pushes the score systematically up, so a small-sample FID always looks worse than the model is. Never compare FIDs computed at different sample counts. Same model, four sample sizes:

    N samplesFID of the same model
    1,00021.4
    5,00011.2
    10,0009.1
    50,0007.6
  5. The features were trained for ImageNet classification, not for faces. ImageNet has essentially no face categories, so the feature extractor was never asked to represent the things that make a face right — that the two eyes are consistent with each other, that both irises are the same colour, that the teeth are anatomically possible. It is instead strongly sensitive to texture, which is why FID reacts more to JPEG compression quality and to which resizing filter you used than to a third eyebrow. Differences in the resize implementation alone move FID by whole points.

  6. It measures distance to your reference set. As derived in Data and labels, a model that faithfully reproduces the reference set’s demographic skew scores well for doing so.

Inception Score

The Inception Score (IS) is the other number papers quote, and on faces it earns a one-line verdict: do not use it.

Its formula is IS = exp( E_x [ KL( p(y|x) || p(y) ) ] ), where p(y|x) is the ImageNet classifier’s category distribution for a single generated image and p(y) is the average of those distributions across the whole generated set. It is high when each individual image gets a confident category and the categories are varied across the set — a rough joint proxy for quality and diversity. On faces it has almost no dynamic range, because every face, good or bad, maps to the same two or three ImageNet categories. Report it only if someone asks, and say why it is uninformative here.

Precision and recall for generative models

Replace FID’s single number with two and you get the most useful measurement in the chapter, because the pair separates the two things FID adds together.

The construction is a k-NN manifold estimate. For each set of images, take the feature vectors, and around each one draw a ball reaching to its k-th nearest neighbour. The union of those balls is a rough outline — a manifold — of the region of feature space that set occupies. Do this twice, once for the real set and once for the generated set, and you have two regions you can test membership against.

Then:

The table below is the payoff. Look at the FID column first — models A and B tie exactly — then look at what precision and recall say about them.

ModelFIDPrecisionRecallDiagnosis
A (GAN)7.40.780.31mode collapse — beautiful, and missing two-thirds of the data
B (diffusion)7.40.610.66balanced
C (diffusion, w=7)11.40.830.36over-guided into the mode interior

Same FID, opposite models. This is the single most useful table in the chapter: it converts “FID conflates two things” from a slogan into a measurement you can act on.

Face-specific metrics you must add

None of the metrics above were designed for faces; the four below were. TV distance in the second row is the total-variation distance defined in Data and labels. SSCD in the third row (self-supervised copy detection) is a network trained specifically to tell whether two images are copies of each other, which is the right feature space for a memorization check and a better one than ArcFace for that purpose because it looks at the whole picture rather than only the face.

MetricHowCatches
Identity diversityArcFace-embed 10k samples, report the distribution of pairwise cosine, not the meanmode collapse, latent duplicates. A healthy model has a near-zero mass above cosine 0.5; a collapsing one grows a bump
Attribute marginal TV distanceClassify 10k samples into skin-tone / age / pose bins, total-variation against the target marginaldemographic dropout, which FID rewards
Memorization rateNearest neighbour of every sample against the training index in SSCD space; report the tail, not the meanreproduction of a real person (Memorization traced)
Symmetry defect ratePer-eye iris colour delta, landmark asymmetry residual after alignmentthe artifact class humans notice first

The recurring instruction in that table — report the distribution or the tail, never the mean — is the same point as flaw 2 above. Rare catastrophic samples are invisible in an average by construction, and rare catastrophic samples are the whole problem.

Why human evaluation stays in the loop

Humans stay in the loop because they see what no automatic metric can — and they have a price. One piece of vocabulary first: an eval, used as a noun, is a fixed measurement procedure — a frozen dataset, a written protocol, and a number it reports — that you re-run unchanged on every checkpoint, as opposed to a one-off analysis.

Every automatic metric above is a function of a feature extractor that was never trained to notice what humans notice. The defects that generate support tickets — three earrings, mismatched irises, a tooth in the wrong plane — occupy tiny pixel areas and near-zero feature-space distance, so they are precisely the class of failure a distribution metric cannot represent.

The protocol. Run a 2AFC study — two-alternative forced choice, meaning a rater is shown one generated and one real image and must pick which is real. Use 500 pairs with 5 raters each.

Report three things.

  1. The fooling rate: the share of pairs where raters picked wrong. 50% is the target, because 50% means they were guessing, which means the generated images are indistinguishable from real ones.
  2. A defect-taxonomy checklist: a fixed list of named defect classes with the incidence of each. This is what changes between checkpoints.
  3. Krippendorff’s alpha: a standard measure of how much independent raters agree with one another, running from 0 for chance agreement to 1 for perfect agreement. Below 0.6, your taxonomy is ambiguous, not your model — the raters are not disagreeing about the images, they are disagreeing about what the categories mean.

What it costs. 500 pairs × 5 raters = 2,500 judgments, at roughly $0.36 each, so 2,500 × 0.36 = $900 and about two days of turnaround per checkpoint. That price is why this gates releases and not individual commits.

Assumptions in this section.


8. Online metrics and the A/B

Once real users are involved, the question shifts from how good the images are to whether a new checkpoint ships, and the instrument that decides it is an A/B test: split users into two groups, give each a different version, and compare.

If this ships as an avatar product, the offline metrics stop being the decision.

MetricDefinitionRole
Accept-on-first-batchuser picks a face from the first 4 shownprimary
Regenerations per accepthow many batches before a picksecondary, and the cost driver
Time to acceptp50 secondsUX
Report rateuser-flagged outputs per 10ksafety guardrail
Block ratesafety cascade rejections per 10ksafety guardrail, both directions
Accept rate by requested demographicaccept-on-first-batch, splitfairness guardrail — ship-blocking
Cost per acceptGPU seconds × regenerationseconomics

Three terms in that table. A guardrail is a metric that blocks a launch rather than informing one — you do not trade it off against the primary metric, you either clear it or you do not ship. p50 means the median — the value half of all observations fall below — and is the standard way to report a typical latency; p95, used later in Scale and cost, is the value 95% of observations fall below, which is how the slow tail gets a number. The safety cascade is the chain of output checks drawn in Serving architecture; its block rate is a guardrail in both directions, because a rate that falls is as suspicious as one that rises.

The randomization unit is the user, not the individual request. Regeneration — a user rejecting a batch and asking for another — is the behaviour under test, so assigning per request would let a single user straddle both arms of the experiment and destroy the comparison.

Now size the experiment. Statistical power is the probability that the test detects a real effect if there is one, and the MDE (minimum detectable effect) is the smallest difference you have chosen to be able to detect. Here the baseline accept rate is 42% and the MDE is 2 pp — percentage points, an absolute difference, so 42% versus 44%, as opposed to a 2% relative change. The factor of 16 is the standard constant for 80% power at 95% confidence:

n per arm  =  16 · p(1-p) / delta^2            p = 0.42,  delta = 0.02
           =  16 · 0.42 · 0.58 / 0.02^2
           =  16 · 0.2436 / 0.0004             0.42 · 0.58 = 0.2436
           =  3.8976 / 0.0004                  16 · 0.2436 = 3.8976
           =  9,744 users per arm

traffic    20,000 sessions/day, split 50/50    ->  10,000 users per arm per day
           9,744 / 10,000                      ->  ~1 day to reach power
run 7 days anyway, so the readout covers a full weekly cycle

The seven-day run is not a power requirement — power is reached on day one. It is there because accept rates differ between weekdays and weekends, and a one-day readout would attribute that difference to your change.

The fairness guardrail is not a metric you report, it is a gate you fail on. A 3 pp accept-rate gap between demographic slices blocks the ship even if the primary metric moved, because the mechanism producing that gap is the same one in The other failure modes that drops modes under guidance.

Assumptions in this section.


9. Serving architecture

The metrics say what to watch; the request path below is the thing they watch. Four choices in it need defending, and all four are about where a control runs rather than what it does.

Three abbreviations appear in the diagram’s boxes:

The path, top to bottom: a request carrying attributes and a count is authenticated and rate-limited against a per-account log of every generation. A policy check refuses anything aimed at a specific real person. Survivors join a priority queue, are batched, and run through the sampler pool for 30 DDIM steps at guidance scale 3.0. The resulting latent is decoded from a 64 × 64 × 4 grid to a 512 × 512 × 3 image. The three-stage safety cascade runs. A clean image is then watermarked, signed, and put behind a CDN as a signed URL with a 24-hour TTL.

flowchart TD
    REQ(["Request<br/>attributes, count"]) --> AUTH["Auth + rate limit<br/>per-account generation log"]
    AUTH --> POL{"Policy check<br/>identity-targeted?<br/>named person?"}
    POL -->|"reject"| DENY(["Refused, logged"])
    POL -->|"pass"| Q["Queue<br/>priority by tier"]
    Q --> BATCH["Batcher<br/>pack cond + uncond<br/>into one forward"]
    BATCH --> GPU["Sampler pool<br/>DiT-XL · 30 DDIM steps<br/>CFG w=3.0"]
    GPU --> VAE["VAE decode<br/>64x64x4 -> 512x512x3"]
    VAE --> CAS["Safety cascade"]
    CAS --> C1["1. NSFW + minor classifier"]
    C1 --> C2["2. ArcFace vs public-figure index"]
    C2 --> C3["3. ArcFace vs training index<br/>memorization check"]
    C3 -->|"any hit"| RESAMP["Resample with a new seed<br/>3 strikes -> refuse + alert"]
    C3 -->|"clean"| WM["Invisible watermark"]
    WM --> SIGN["C2PA manifest, signed"]
    SIGN --> CDN(["CDN, signed URL, 24h TTL"])
    RESAMP --> Q

    style POL fill:#9d0208,color:#fff
    style CAS fill:#bc6c25,color:#fff
    style C3 fill:#9d0208,color:#fff
    style CDN fill:#2d6a4f,color:#fff

Four things to narrate:

  1. The policy check is before the queue and before any GPU. Same argument as deterministic escalation in case study 06: a control that runs in ordinary code cannot be argued out of running by the content it inspects, whereas a control that is itself a model can be.
  2. Guidance needs two forward passes, and they are packed into one batch. The conditional and unconditional passes are the same network on the same input shapes, so running them together as a batch of two costs essentially the same as one pass and much less than two separate calls.
  3. The memorization check needs the training index at serving time. Size it before you argue about it: roughly 3 million ArcFace vectors, 512 dimensions each, stored as 16-bit numbers, is 3e6 × 512 × 2 bytes = 3.07e9 bytes, so about 3.1 GB. That is small enough to hold an HNSW index (hierarchical navigable small world, the standard graph-based structure for fast approximate nearest-neighbour search) inside the process on every replica, rather than behind a network call. That is the only reason this check is affordable at all.
  4. Resample rather than refuse on the first hit. A memorization hit is a property of the random seed — the starting noise that determined this particular sample — not of the request, so a fresh seed usually clears it. Three strikes in a row means something is wrong with the request, and that is what the alert is for.

Assumptions in this section.


10. Scale and cost

With the serving path fixed, price one image — and then ask which design decision actually bought the savings. The answer is a single decision worth 8,600×, with every other lever under 15×.

The configuration is 512 × 512 output, 30 DDIM sampling steps, and guidance on — which doubles the number of network evaluations, because each step needs both a conditional and an unconditional prediction. ANN below is approximate nearest neighbour, the index search from Serving architecture.

forwards per image      30 steps × 2 (cond + uncond)          =  60
FLOPs per forward       (from §6)                             =  1.05 TFLOP
diffusion               60 × 1.05                             =  63.0 TFLOP
VAE decode              conv stack at 512^2                   =   0.4 TFLOP
safety cascade          3 small nets + 3M-vector ANN search   =  ~0.02 TFLOP
                                                                 -----------
                                                                 63.4 TFLOP

H100 at 300 TFLOP/s effective   ->  63.4 / 300        =  0.211 s of GPU time per image
$2.50/GPU-hour / 3,600 s        ->  $0.000694 per GPU-second
                                    0.211 × 0.000694  =  $0.00015 per image at 100% utilization
at 60% fleet utilization        ->  0.00015 / 0.6     =  $0.00024 per image   =   $0.24 per 1,000

throughput   1 / 0.211  =  4.7 images/s/GPU
latency      batch of 16: 16 × 63.4 / 300  =  3.4 s for the batch

Two things about that block. The 60% fleet utilization divisor is there because you pay for GPUs by the hour whether or not a request is in flight; real traffic is bursty, so a GPU-second of useful work costs you about 1.7 GPU-seconds of billed time. And the last two lines answer different questions: throughput sets how many GPUs you rent, latency sets what the user waits for.

Now scale it to traffic. At 2 million images a day:

cost      2e6 × $0.00024                        =  $489/day of GPU
load      2e6 × 0.211 s = 422,000 GPU-seconds
          422,000 / 86,400 s per day            =  4.9 GPUs of steady-state load
provision headroom + diurnal peak + p95         =  ~8 replicas

$489/day, 4.9 GPUs of steady-state load, ~8 replicas. The gap between 4.9 and 8 is the peak-versus-mean problem: you provision for the busiest hour and you pay for the average.

Leave-one-out attribution

Of all the decisions in the design, which one bought the savings? That is the question that decides where engineering time goes next, and the way to answer it is to remove exactly one decision at a time from the finished design and re-price it.

DesignTFLOP/imageCost/imageAt 2M/dayDelta
Full design (latent, 30 steps, CFG w=3)63.4$0.00024$489
Guidance off (w=1)31.9$0.00012$246-$243/day, and precision 0.76 -> 0.58
250 steps instead of 30525.4$0.00203$4,054+$3,565/day for FID 7.57 -> 7.30 (Sampling ddpm ddim and the step count curve, 2nd-order curve)
4-step distilled, guidance distilled4.6$0.000018$36-$453/day, and recall falls ~0.14 from this design’s 0.51 (ch 08 §6)
Pixel-space diffusion at 512546,000$2.11$4.2M+8,600×

Two conventions to keep straight while reading that table. The precision and recall figures are differenced against the w=3 row of the guidance table in Classifier free guidance (precision 0.76, recall 0.51), not against the FID-optimal w=1.5 row, because the full design runs at w=3. And the FID change for the step count comes from chapter 08’s second-order fit evaluated at both endpoints; mixing a second-order reading at 30 steps with a first-order reading at 250 is how you talk yourself into a difference of “0.06 FID” that is not real.

The last row is the entire subject of chapter 08, and it is worth doing the arithmetic once here so the 8,600× is not a number you are asked to take on trust.

Running diffusion directly on pixels at 512 × 512 means one token per pixel, so n = 512 × 512 = 262,144 tokens instead of 1,024 — a 256× increase. The token-linear term grows 256× with it, but the attention term grows with n^2, so it grows 65,536×:

attention term   4 · n^2 · d · L  =  4 · 262,144^2 · 1152 · 28  =  8.87e15 FLOP per forward
token-linear     2 · 445.9e6 · 262,144                          =  0.23e15 FLOP per forward
total                                                            =  9,100 TFLOP per forward
                 9,100 / 300 TFLOP/s                             =  30 s of H100 time per forward
                 × 60 forwards per image                         =  ~30 minutes per image

Compare that to 0.211 seconds for the latent design. That is the 8,600×: 546,000 TFLOP / 63.4 TFLOP ≈ 8,600.

The honest reading of this table: the latent-space choice bought an 8,600× cost reduction, and every other lever on the list is under 15× — distillation is the largest of them at 13.7× (63.4 / 4.6), step count 8.3×, guidance 2.0×. Note that “latent versus pixel” is not the The model family comparison derived family question; it is the same diffusion family run on a compressed representation, which is why chapter 07 picks the family and chapter 08 picks the space. Anyone who spends the interview tuning step counts has found the second-order term.

Assumptions in this section.


11. Failure modes

One failure mode carries legal exposure — the model reproducing a real photograph — and it can be traced all the way down to the objective that causes it. The other seven each come with a mechanism, a detector and a guard.

Memorization, traced

Memorization is not a bug bolted onto diffusion. It is what the objective asks for, in a specific regime, and the two lines below show which regime.

Start from what the trained denoiser actually converges to. Given a noised image x_t, the best possible guess at the clean image is the posterior mean: the average of every training image, weighted by how consistent each one is with the noisy picture you were handed.

E[x_0 | x_t]  =  sum_i  w_i · x_0^(i)
w_i  ~  k_i · exp( -|| x_t - sqrt(abar_t)·x_0^(i) ||^2 / (2(1 - abar_t)) )

Reading the second line: x_0^(i) is the i-th training image and k_i is how many times it appears in the corpus. The || ... ||^2 term is the squared distance between the noisy image you have and where training image i would sit after t steps of noising. Small distance means “this training image is consistent with what I see”, and the minus sign in the exponent turns small distance into a large weight.

Those weights are a softmax: the standard construction that turns a list of scores into probabilities summing to one, by exponentiating each score and dividing by the total. Its behaviour is the whole point — the more the scores spread apart, the more completely the largest one dominates. At the extreme, one weight goes to 1 and the rest to 0, and the “average of every training image” collapses into one training image.

Two things drive it to that extreme:

The trace below shows both halves of that on one seed: the same request and the same starting noise, run once against a corpus with the duplicates left in and once against a deduplicated one.

TRACE — a licensed portrait appearing 34 times across three vendor packs

  dedup off, condition = "age 30-40, frontal, studio lighting"
  seed 8812 -> sample
  SSCD cosine to training NN                 0.981
  ArcFace cosine to training NN              0.974
  pixel L2 after alignment                   0.03
  -> this is that photograph, not a face like it

  same seed, dedup on (k capped at 1), condition dropout 0.1
  SSCD cosine to training NN                 0.412
  ArcFace cosine to training NN              0.238
  -> the NN is the nearest look-alike, which is what "learned the distribution" means

In that trace, NN means nearest neighbour — the closest training image to the generated sample, measured in the named feature space. A cosine of 0.981 in copy-detection space is not “similar to”; it is “the same photograph”.

The legal exposure is that the output is simultaneously a licensing breach and a biometric disclosure about a real, identifiable person. Three controls, in order of effect. Deduplicate at ingest, which removes the duplicate-count multiplier k_i entirely. Drop the condition on 10% of training examples, because a highly specific condition otherwise acts as an index pointing at the one image that matches it. And run the output-side nearest-neighbour check, which catches whatever the first two missed. Set the detection threshold on the tail of the nearest-neighbour distance distribution rather than its mean, because the mean is uninformative — a healthy model’s average nearest-neighbour cosine barely moves when 0.1% of its samples are outright reproductions.

The other failure modes

Four terms in the table need defining first:

The mechanisms are the part to know cold, because every guard in the last column exists only on account of the mechanism named next to it.

FailureMechanismDetectionGuard
Mode collapse (if you had picked a GAN)Generator loss integrates over generator samples only; omission is unpenalizedIdentity-diversity histogram grows a mass above cosine 0.5; recall falls with FID flatDo not pick a GAN for this. If you must: minibatch discrimination, unrolled D, and monitor recall not FID
Demographic dropout under guidanceGuidance is an exponent on p(c|x); it concentrates on the mode interior, and the mode interior is the majority demographicTV distance of the skin-tone marginal, computed at each wCap w at the value where TV distance exceeds 0.03. Measured: unguided marginal matches target within 2 pp; at w=7 the two darkest bins drop 40% relative
Checkerboard artifactsTransposed convolution with kernel size not divisible by stride produces uneven output overlap, a fixed periodic patternFFT of the residual shows a spike at the stride frequencyReplace transposed conv with nearest-neighbour upsample + 3×3 conv in the decoder
Patch-boundary gridDiT patch size 2 with a weak decoder leaves a visible 16-px latticeSame FFT test at the patch frequencyOverlapping patch embedding, or more decoder capacity
Asymmetric irises / earringsLeft-right consistency is a single long-range dependency at 32×32 token resolution, competing with every other dependency for the same attention budget; nothing in the loss privileges itPer-eye colour delta on 10k samplesHigher token resolution, or an explicit symmetry-aware discriminator on the decoder. Mostly: measure it and set a release bar
Mean-luminance collapseabar_T ≈ 0.0047 leaks about 7% of signal into the “pure noise” the model trained on (Training)Histogram of output mean luminance is narrower than the training set’sZero terminal SNR + v-prediction
Oversaturation at high wGuided extrapolation leaves the decoder’s trained range; the clamp clipsFraction of clipped channels per imageRescale guided prediction std to the conditional prediction std
Off-distribution framingAlignment made off-centre faces probability-zeroAny request for unusual framingWeaken alignment at training time, or refuse the control

Assumptions in this section.


12. Alternatives rejected

Every defensible design carries a list of what was considered and dropped. The value is not the rejections themselves but the reason attached to each, because every reason names the assumption that would have to change to reverse it.

AlternativeWhy it is temptingWhy rejected
StyleGAN-class GAN60× cheaper sampling, still state of the art on aligned faces by FID, and a genuinely nice latent space for editingMode collapse is undetectable by the headline metric and its consequence here is demographic dropout. Training needs per-dataset stabilizer tuning, and this model retrains several times a year on a changing corpus
VAE aloneOne network, stable, fast, exact latent inferenceThe L2 reconstruction term makes the conditional mean the optimum, and the conditional mean of several plausible skin textures is blur. Kept as the autoencoder, where reconstruction is conditioned on the input and blur is bounded
Autoregressive over pixels or VQ tokens (VQ = vector-quantized: the image is first turned into a sequence of discrete codes drawn from a learned codebook, so it can be modelled exactly like text)Exact likelihood, one stable loss, and the same infrastructure as a large language model (LLM)1024 sequential decodes per image, an ordering prior with no justification, and a likelihood that rewards modelling sensor noise. Revisit if you need a single model over text and images
Pixel-space diffusionNo autoencoder, no quality ceiling from compression8,600× the serving cost at 512 (Scale and cost). Derived in Why pixel space diffusion at 1024 is hopeless
U-Net backbone instead of DiT (a U-Net is the convolutional encoder-decoder that diffusion models originally used, shaped like a U: it downsamples the image through several stages, then upsamples back, with shortcut links across)Proven, strong locality prior, well-tuned public recipesCompute is spread across hand-designed resolution stages, so scaling is guesswork; and MFU — model FLOP utilization, the fraction of the hardware’s peak arithmetic rate you actually achieve — is materially lower on hardware that prefers uniform shapes (Architecture u net vs dit)
Scrape a public face dataset for v1Everyone did it, it is free, and it is 10× the dataThe v1 checkpoint becomes the product. There is no subtraction operation on trained weights, so an unlawful-basis dataset is a permanent property of the model, not a temporary shortcut
Watermark as the misuse controlCheap, invisible, and it sounds like a complete answerAbsence of a watermark proves nothing, and a single img2img pass takes bit accuracy to 0.61. It attributes your own output; it does not defend against deepfakes
Refuse nothing, rely on terms of serviceFewer false blocks, better product feelThe two blocking cases — minors and identifiable real people — have criminal and civil exposure respectively. Terms of service are not a control
Tune the step count as the main cost leverVisible, easy, no quality risk if you measure30 -> 250 steps is 8× cost for 0.3 FID, inside the metric’s noise floor. The 8,600× lever was the latent space, and the next 14× is distillation, not scheduling
Report FID aloneOne number, universally quoted, comparable to papersIt is minimized at a guidance scale humans dislike, it rewards reproducing your reference set’s bias, and it is blind to a 1% catastrophic-output rate. Report FID with precision, recall, and the attribute marginal, always as a set

13. Interviewer pushback

These are the ten questions this design is actually asked, what each one is testing, and an answer that survives the follow-up. Almost every one is an attack on an assumption rather than on a fact, which is why each answer names the mechanism rather than restating the conclusion.

“Why diffusion and not a GAN? GANs are much cheaper to serve.” Testing: whether the model choice is derived or fashionable. Because the two failures are not symmetric. A GAN’s generator loss is an expectation over its own samples, so failing to cover part of the data distribution costs it nothing directly — omission is unpenalized by construction. For a face model that means silently dropping demographics, which FID actively rewards because FID measures distance to a reference set that has the same skew. Diffusion’s weakness is sampling cost, which I can fix at serving time: distillation takes 60 forwards to 4, a 15× win. There is no serving-time fix for mode collapse. I am trading a cost problem I can solve for a correctness problem I cannot detect.

“Walk me through why predicting the noise is better than predicting the clean image.” Testing: whether you understand the objective or just the interface you call. They carry identical information — x_0 and eps determine each other given x_t. What differs is the implied loss weighting. An eps error of e becomes an x_0 error of e / sqrt(SNR_t), so a uniformly weighted eps loss is an x_0 loss weighted by SNR. That down-weights exactly the high-noise steps where x_0 is genuinely ambiguous, which is why diffusion does not converge to the conditional mean the way a VAE does. Second reason: eps has unit variance at every t, so one network with one output scale covers the whole schedule. Third: eps is a rescaled score estimate, which is what makes deterministic ODE sampling available.

“Your FID is 5.9 at guidance 1.5 and 6.8 at 3.0. Which do you ship?” Testing: whether you take a metric at face value. It is a trap. 3.0, and the discrepancy is the interesting part. FID punishes the diversity loss that guidance causes, and human raters judging one image at a time structurally cannot see diversity — so FID and preference disagree in a predictable direction. I would ship at the guidance scale where the fairness guardrail binds, not where FID or preference peaks: measure the skin-tone marginal’s TV distance at each w and cap at 0.03. On our numbers that is around w = 3, and it happens to coincide with the preference optimum, but the binding constraint is the marginal.

“What is FID actually computing, and where does it lie to you?” Testing: whether you can define your own metric. Frechet distance between two Gaussians fitted to Inception-v3 pool3 features — mean difference squared plus a covariance trace term. So it sees only the first two moments of a 2048-d distribution. It lies in five places: it is blind to per-sample catastrophes because it is a distribution statistic; it conflates fidelity and coverage into one scalar, so a mode-collapsed model and a balanced one can tie at 7.4; it is biased at small N, moving from 21.4 at 1k samples to 7.6 at 50k for the same model; the features come from an ImageNet classifier that has no face classes, so it is texture-sensitive and anatomy-blind; and it measures distance to your reference set, which means reproducing that set’s demographic skew scores well.

“Prove the model has not memorized a training image.” Testing: whether you know this is a legal question with a technical answer. You cannot prove it, so you measure the tail. Every generated sample gets a nearest-neighbour lookup in SSCD and ArcFace space against a 3M-vector index of the training set, in-process on every replica at 3.1 GB. Report the tail of the distance distribution, never the mean — a healthy mean is completely consistent with 0.1% reproductions. Upstream: dedup at ingest with a hard cap of one copy per cluster, because duplicate count multiplies an image’s weight in the posterior-mean that the optimal denoiser converges to, and condition dropout, because a highly specific condition acts as an index into a single image.

“Someone uses this to make a deepfake. What did your design do about it?” Testing: whether your safety story is a control or a disclaimer. Four things, and only two of them are controls. Controls: the policy check refuses identity-targeted requests before any GPU work, and the output-side ArcFace match against a public-figure index blocks anything that resembles a real person above threshold. Attribution, not control: a C2PA manifest signed at generation, and an invisible watermark. I would not oversell the last two — the manifest dies on any metadata-stripping re-encode, and the watermark drops to 0.61 bit accuracy after a single img2img pass and to chance against an adversary with detector access. A watermark tells you that you made something. It never tells you that someone else did not.

“Your fairness guardrail fails a launch that improved the primary metric by 3 points. What do you do?” Testing: whether the guardrail is real. Do not ship, and go find the mechanism, because there is almost always one. The usual culprit is guidance scale: the guided score is an exponent on the implied classifier, which concentrates mass on the mode interior, and the mode interior is the majority demographic. So the accept-rate lift and the coverage loss are frequently the same change viewed twice. Concretely I would re-run the marginal at several w, and if the gap tracks w then the fix is a per-condition guidance schedule, not a rollback of the whole launch.

“You have no human eval budget. What is the minimum you keep?” Testing: whether you know which metrics are load-bearing. The defect-taxonomy checklist on 200 images, dropping the 2AFC study. The 2AFC number is a nice headline that moves slowly; the defect incidence rates are what change between checkpoints and what generate support tickets. Every automatic metric I have is a function of a feature extractor that was never trained to notice mismatched irises, so no amount of FID substitutes for someone counting them. If I lose that too, I keep the identity-diversity histogram and the attribute marginal, because those are free and they catch the two failures with a legal surface.

“Why is your training-set index a serving dependency? That seems like bad hygiene.” Testing: whether you understand where memorization is catchable. Because memorization is a property of the sample, not of the request, so it cannot be prevented at the input. Dedup and condition dropout reduce the rate; they do not drive it to zero, and the residual rate matters because the failure is a real person’s face. The index is 3.1 GB of fp16 vectors, small enough to sit in-process, and the lookup is 20 ms against the 3.4 seconds a batch of 16 spends generating (Scale and cost) — under 1% of the user-visible time. The hygiene cost is real — the index has to be versioned with the checkpoint — and it is much cheaper than the alternative, which is finding out from the person in the photograph.

“Costs are fine. Where would you actually spend the next month?” Testing: whether you can rank. Not on the model. The cost table says the latent-space choice was 8,600× and everything left is under 15×, so serving is solved. I would spend it on the data plane — everything that moves and records images before training, as opposed to the model itself: provenance completeness, the erasure-to-retrain pipeline, and dedup quality — because those three determine both the memorization rate and whether the checkpoint is defensible, and unlike a step-count change they cannot be retrofitted. Second priority is the attribute-marginal measurement, because it is the only metric in the set that detects the failure the headline metric rewards.


The assumption ledger

Here, in one place, is every assumption the design has leaned on, so that you can state its foundations in twenty seconds and say what replaces the design when each one fails. Sort each into one of three bins: state it (you are free to pick, and being wrong costs a re-derivation), ask it (the answer changes the architecture, so it is worth an interviewer’s time), and load-bearing (if it is wrong the design is not suboptimal, it is invalid) — the same three bins used in ch 01.

AssumptionBinWhat it holds upWhat replaces the design if it is false
A lawful, consented corpus of 2-5M portraits can be assembledLoad-bearingEvery number in the chapter, and whether the checkpoint may exist at allNothing replaces it. A scraped corpus is not a cheaper variant — there is no subtraction operation on trained weights, so an unlawful basis is a permanent property of the model (Alternatives rejected)
Dropping a mode is worse than blurring oneLoad-bearingThe entire model-family argument in The model family comparison derivedInvert it — a product where only peak quality matters and coverage genuinely does not — and the GAN’s 60× cheaper sampling wins outright
Sampling cost is fixable after training and mode coverage is notLoad-bearingThe choice of diffusion despite 60 forward passes per imageIf step counts could not be reduced, this is a 60× more expensive product and the family choice must be re-argued against the real budget
Memorization is only catchable at the outputLoad-bearingThe training index as a serving dependency (Serving architecture), and the whole memorization control stack in Memorization tracedIf ingest controls were sufficient, drop 3.1 GB per replica and a 20 ms lookup from the serving path
3M ArcFace vectors (3.1 GB) fit in process on every replicaLoad-bearingThat the output-side memorization check is affordable at allAt 30M images the index becomes a network hop on the critical path, changing both the latency budget and the failure semantics of the serving tier
FID is never the only reported numberLoad-bearingEvery coverage, fairness and memorization argument in Offline metricsA process gating on FID alone has no guard against any of them — it is not weaker, it has the wrong instrument
The target demographic marginal is set by a policy owner, not inferred from the corpusLoad-bearingThe total-variation fairness metric and the ship-blocking guardrail in Online metrics and the abInfer it from the corpus and you have written the bias down as the goal, and a better FID means you achieved it more exactly
The watermark is attribution, never a defenceLoad-bearingThe honesty of the whole safety storyTreat it as a deepfake control and the design has no misuse control, because a single img2img pass takes bit accuracy to 0.61 and absence proves nothing
The policy check runs before any GPU workLoad-bearingThe one control that cannot be argued out of running by its inputRun it after generation and you pay for every refusal and hold an internal copy of exactly the content the policy exists to prevent
User-level randomization is possible, so users have stable identity across sessionsLoad-bearingThe A/B design in Online metrics and the abRegeneration behaviour leaks between arms and the readout looks perfectly valid while measuring nothing; fall back to interleaving
Deduplication reliably catches re-encoded and re-cropped copiesLoad-bearingThe removal of the duplicate-count multiplier that drives memorizationA leaky dedup leaves the multiplier intact while supplying the confidence of having handled it — worse than skipping the step knowingly
A single global guidance scale can satisfy the fairness guardrailLoad-bearingThe w = 3 operating point and the serving path that assumes one valueIf no single w clears the 0.03 total-variation cap, the sampler needs a per-condition guidance schedule and the serving path grows a lookup
Whether the product may refuse a requestAsk itThe policy check, which is the only genuine misuse controlA product that must never refuse has no misuse control; the honest answer is that it should not ship for face generation
Whether erasure requests must be honouredAsk itThe retrain cadence, and therefore reason 2 of the model pick in The pick and the argumentNo erasure obligation removes the retrain cadence argument, and the GAN’s stability objection weakens with it
How often the model is retrainedAsk itThe operational argument against per-dataset stabilizer tuningAt one training run ever, stability is worth nothing; at four a year it is decisive
Whether the product needs varied framingAsk itHow hard you align in Data and labels, which cannot be changed after trainingVaried framing means weaker alignment and paying for it in resolution-equivalent capacity
Whether a rater pool exists, and on what turnaroundAsk itThe human evaluation loop in Why human evaluation stays in the loopWithout it FID quietly becomes the ship gate again, which the chapter has just spent a section arguing against
DiT-XL/2 at d = 1152, L = 28, f = 8 autoencoder, 1024 tokens, 1.8e9 images seenState itEvery FLOP, day and dollar figure in TrainingA re-derivation; the ratios and the ordering of the levers are unchanged
H100 at 300 TFLOP/s effective, $2.50/GPU-hour, 60% fleet utilizationState itTraining cost, cost per image, fleet sizeDifferent hardware moves every row of the cost tables together
2M images/day, 30 DDIM steps, guidance w = 3.0State itThe $489/day figure and the 4.9-GPU steady-state loadRe-derive; provisioning is set by the peak and cost by the mean, so both need the real numbers
42% baseline accept rate, 2 pp minimum detectable effect, 20k sessions/dayState itThe experiment’s duration, and nothing elseRe-derive the power calculation; the metric set does not change
2-5M aligned faces, 0.95 dedup cosine, 10-point skin-tone scale, 15% human adjudicationState itThe data pipeline’s shape and labelling costTune against measured label quality; the mechanism is unchanged

The sentence that makes this visible to an interviewer: “This design rests on three things. One, that a lawful consented corpus at this scale exists — without it there is no version of this system, because you cannot subtract an image from trained weights. Two, that silently dropping a demographic is worse than producing slightly soft faces, which is the entire reason I picked diffusion over a GAN that is sixty times cheaper to serve. Three, that memorization can only be caught at the output, which is why the training-set index is a serving dependency on every replica and why I am willing to pay 3.1 GB and 20 milliseconds for it.”


Next: 08 — High-Resolution Image Synthesis.