InterviewPrepKit

Home / Learn / Generative AI System Design

How to design a face generator

In this lesson, we’ll design a photorealistic face generator end to end: the objective it optimizes, the network that implements it, the cost to serve one image, and the failure modes that make it a legal problem and not only an engineering one. By the end you’ll be able to say why the field converged on diffusion, a model that removes noise from an image a little at a time, over the three model families that came before it, and defend that choice against the one metric everyone quotes.

We assume no prior generative-modelling background. Every acronym (GAN, VAE, ELBO, FID, CFG, DiT) is defined the first time it appears.

Input and output. The system takes an optional description of the face you want (an age band, head pose, lighting, expression) or nothing at all, in which case it samples freely. It returns one 512 × 512 photograph of a person who does not exist. No reference photo goes in, and no real person’s identity is supposed to come out.

Why the architecture is not the hard part. Three facts about faces settle most of the design before any network is chosen:

  • A photograph of a face is biometric data, a measurement of a person’s body, regulated as such in most jurisdictions.
  • A generated face is raw material for a deepfake: a fabricated image passed off as a genuine recording of a real person.
  • A memorized face, an output that reproduces a training photograph closely enough to identify the person in it, is not a synthetic face at all. It is that person’s photograph, laundered.

Consent, misuse, and provenance therefore shape the design before the neural network does. We build the lesson in that order: constraints first, then the model choice they force.

Framing, and the constraint that comes before the architecture

We fix two things before choosing a single layer: the interface, and the legal and safety constraints that settle most of the design on their own.

The interface. The input is an optional attribute condition (a face described by age band, head pose, lighting setup, and expression) or nothing at all, in which case the system samples a face 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 single 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), compares two whole collections of images instead of scoring any single one, so it structurally cannot see the per-image failures users complain about.

Three obligations come 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 can tell that an image came from this system.

The three constraints, as mechanisms

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

Four pieces of vocabulary first:

  • An embedding is a list of numbers a network produces to summarize an input, arranged so that similar inputs get similar lists.
  • ArcFace is a face-recognition network whose embedding puts two photographs of the same person close together and two different people far apart.
  • Cosine similarity measures that closeness: 1.0 means the two vectors point in exactly the same direction, 0 means they are unrelated.
  • Provenance means a verifiable record of where a file came from. C2PA (Coalition for Content Provenance and Authenticity) is the industry standard for attaching such a record to an image, as a cryptographically signed block of metadata.

The last column keeps each mechanism honest about what it does not cover.

ConstraintMechanismWhere it runsWhat 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, before any tensor existsDoes not survive a scraped-data shortcut “just for v1” — the run becomes the checkpoint and the checkpoint is the product
Right to erasureThe provenance table is the join key. Erasure means removing the rows, removing the shards, and scheduling a retrain — you cannot subtract an image from trained weightsData plane + retrain cadenceDoes not clean the current checkpoint. Budget a retrain, or do not accept erasure requests
Minor exclusionAge classifier at ingest, tuned for high recall, 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. A hard exit criterion, not a quality dial
No identifiable real personArcFace embedding of every output, cosine against a public-figure index and the training index. Block above thresholdServing, post-decodeDoes not catch a person absent from both indexes. It catches the two cases you can be sued over
ProvenanceC2PA 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 in the decoder output; a paired detector recovers ~48 bitsServing, before the C2PA signDoes not prove absence (see below)

What an invisible watermark can and cannot prove

An invisible watermark is a tiny, deliberately imperceptible pattern added to every emitted image, carrying a few dozen bits of hidden payload. It is produced by nudging the output of the decoder, the final stage that turns the model’s internal representation into pixels. The mark is trained jointly with a paired detector network that reads the payload back. During that training the pair is exercised through a distortion layer: a stack of simulated attacks (compression, resizing, cropping) applied between them.

That setup is the whole story: the mark survives the distortions it was trained against and fails on the ones it was not. Everything below follows from that one sentence.

Bit accuracy is the fraction of the ~48 hidden bits the detector recovers correctly; 1.00 is perfect, 0.50 is a coin flip (the mark is gone). Bit accuracy is not a detection rate. Attribution makes a decision, “we generated this”, so the number that governs it is the false-positive rate (FPR): the chance an image the system never generated matches enough bits by luck.

That FPR is a binomial. Run the detector on an image with no mark and each of the 48 bits is an independent coin flip, so the count matching your payload follows 48 fair coins:

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

On the screenshot row (42 bits recovered) that is about 5.0e-8, or 50 false attributions per billion images. Attack names in the table: JPEG q=50 is aggressive lossy re-compression; img2img regeneration, strength 0.4 feeds the image back into a diffusion model and partly re-noises it; an autoencoder round trip compresses and re-expands it through two networks.

AttackBit accuracyBits of 48FPR/imageFalse hits per 1e9Reading
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 surviving area, still clears the bar
screenshot, re-encode, re-upload0.88425.0e-0850usable only if 50 false hits/billion is acceptable
img2img regeneration, strength 0.40.61299.7e-0296,706,326no attribution. A diffusion round trip resamples the high-frequency band the mark lives in
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

The bar has to be set first: to scan a large corpus at no more than one false attribution per billion images, you need FPR <= 1e-9. Reading that off the binomial: 44 of 48 bits gives 7.6e-10 (43 gives 6.8e-9), so 44 is the smallest count that clears it, and 44/48 is a bit accuracy of 0.917. Below 0.917 bit accuracy is not attribution. So crop to 50% clears it, screenshot does not (state that as “50 false hits per billion”, not “usable”), and the last three rows are not degrees of severity. They are one finding: no attribution.

A watermark is a positive-evidence channel, not a negative one. A detected mark says “we generated this.” An absent mark says nothing, because every other generator, camera, and re-encode is outside your control. Watermarking is an attribution tool for your own output, not a deepfake defence. The real 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 them that carries the real point. pHash is a perceptual hash, a short fingerprint that stays the same under re-compression or slight re-cropping. NSFW is 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

Ingest runs before any training tensor exists. Licensed and consented sources each get a provenance row. An age classifier screens every image, tuned for high recall over precision. It flags nearly every true minor even at the cost of flagging adults too. Survivors go through dedup: pHash catches exact and near-exact copies, an embedding-cluster step catches the rest, and every surviving cluster is capped at one copy.

Serving runs on every decoded image. An NSFW classifier and a second minor classifier screen the pixels. ArcFace cosine similarity matches the face against two indexes (searchable stores of embeddings) of known faces; anything above threshold is blocked and re-sampled, and a repeat triggers an alert. Clean images get the watermark, then a signed C2PA manifest, and only then are delivered.

The dotted edge running from the training set back into the serving check is the load-bearing one: the training-set embedding index is a serving dependency, because memorization (covered later) is detected at the output, not prevented at the input. The whole design also rests on one assumption that no engineering can rescue: that a lawful, consented corpus at the required scale can actually be assembled. A scraped corpus is not a cheaper variant. It becomes a permanent, unremovable property of the checkpoint.

The ML objective is a choice of divergence

With the constraints fixed, we ask what a generative model optimizes when there is nothing to compare an output against. The single decision here, which definition of “close” you pick, already determines the family of model you end up building.

There are no labels. The goal is to learn a sampler: a procedure that, when you turn the handle, emits a fresh face. Write p_data for the true distribution of real faces (the rule saying which arrangements of pixels are plausible faces and how often each occurs) and p_model for the distribution your sampler produces. Training means making p_model close to p_data.

“Close” between two distributions has to be defined, and the standard family of definitions is a divergence: a number that is zero when the distributions are identical and grows as they differ. Unlike an ordinary distance, a divergence need not be symmetric: the divergence from A to B can differ from B to A, and which direction you minimize is the model family choice.

The standard divergence is the Kullback-Leibler divergence (KL), which has two orderings. Notation: E_{x ~ p}[·] is an expectation, the average of the bracketed quantity when x is drawn from p; the subscript says which distribution supplies the samples, and that subscript is the whole subject of this section. Probability mass is how much of the model’s fixed output budget sits in a region of image space. A mode is a bump in the distribution where real examples cluster (for faces, think of each combination of skin tone, age, and pose).

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 distributions and differ only in which one supplies the average.

Forward KL averages over the data. If a kind of face exists in the data but the model never produces it (p_data(x) > 0 while p_model(x) → 0), the bracket is log(p_data/p_model) → ∞, and that point is in the average because the average runs over data. The model is infinitely punished for missing anything, so it spreads mass to cover everything, including the gaps between modes, where no real face lives. A sample from such a gap is a smeared average of two clusters. It is mode-covering, and the price is blur.

Reverse KL averages over the model. In the same situation the model never samples the missing region, so it contributes nothing. Missing a mode is free. The only penalty left is for putting mass where the data has none, which keeps the model inside regions the data covers. It is mode-seeking, and the price is dropped modes, whole kinds of face the model silently stops producing. Mode collapse is this failure by its usual name.

The four model families sort onto the two sides of that split:

  • Autoregressive: generates an image one piece at a time, each conditioned on the pieces already written. Optimizes forward KL exactly.
  • VAE (variational autoencoder): compresses each image to a compact code and expands it back. It cannot compute forward KL exactly, so it maximizes a lower bound called the ELBO (evidence lower bound), a quantity guaranteed to sit below the true objective, so raising the bound raises the objective.
  • Diffusion: optimizes a reweighted version of that same bound.
  • GAN (generative adversarial network): pits a generator against a critic. In theory it minimizes the Jensen-Shannon divergence (JS), a symmetrized cousin of KL; the loss used in practice (the non-saturating one) averages only over the generator’s own samples, which puts it on the reverse-KL side.
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, a bound on it<br/>Diffusion: a reweighted ELBO"]
    RK --> B["GAN: JS in theory, but the<br/>non-saturating generator loss<br/>averages 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 this asymmetry is legal, not aesthetic. 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 FID will not show you. That alone points toward the maximum-likelihood family, the families that optimize forward KL or a bound on it, before a single layer is drawn. The whole argument rests on one claim: dropping a mode is worse than blurring one. Invert it (say, a single decorative background face where only top quality matters) and the GAN’s cheapness wins outright.

Data and labels

Two corpus decisions 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. A trap also waits in the headline quality metric.

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

Alignment, and what it costs. Alignment means putting every face in the same place in the frame before training: detect the face, extract five landmarks (two eye centres, nose tip, two mouth corners), apply a similarity transform (rotation, uniform scale, shift, the only operations that move a picture without distorting its shape) so the eyes land on fixed coordinates, then crop and resize. Alignment removes a nuisance source of variation, so the same parameter budget buys more identity and texture detail. 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. Ask a model trained on an FFHQ-style aligned corpus (Flickr-Faces-HQ, the standard public aligned-face dataset) for an off-centre three-quarter profile and you get a warped centred face, because off-centre faces have probability zero. If the product needs varied framing, weaken the alignment during training and pay for it in resolution.

Deduplication is a safety control, not hygiene. Run a perceptual hash for exact and near-exact copies, then cluster embeddings at cosine 0.95 to catch re-cropped or re-compressed copies, and cap each cluster at one. The reason is derived later under memorization: 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 person is the direct consequence.

Attribute labels. Unconditional generation needs no labels. You need them anyway for two jobs. Conditioning steers the output toward a requested attribute, if the product exposes controls. Auditing measures demographic coverage. You cannot claim it without measuring it. Label a stratified sample (drawn to include enough of every group, not sampled uniformly and hoped over) by skin tone (a 10-point perceptual scale, not a race category), apparent age band, and pose. Label with a model first, then have humans adjudicate the ~15% lowest-confidence cases.

The trap in the metric. A marginal is the model’s overall breakdown across one attribute, the fraction of outputs in each skin-tone bin, ignoring everything else. If 68% of training faces sit in the lightest three bins, the model’s marginal reproduces that 68%. That much is expected. The trap is what FID does with it:

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 detect a problem your reference set has. The fix is a separate target marginal, the breakdown you decide the product should produce, set by a policy owner and not inferred from the corpus (infer it and you have written the bias down as the goal), and measure against that.

The measurement is total-variation distance (TV distance): half the sum of the absolute per-bin differences. For target [0.40, 0.35, 0.25] and output [0.52, 0.33, 0.15], the absolute differences are 0.12 + 0.02 + 0.10 = 0.24, so TV = 0.12. Halving lands it in [0, 1]: 0 means the breakdowns match, 1 means they share no bin. That 0.12 fails the 0.03 guardrail used later. Report TV distance alongside FID, never folded into one score.

The model family comparison, derived

All four families grow out of one shared difficulty, and each family’s famous weakness is the direct price of how it dodges that difficulty.

The shared difficulty. Training any of these models means pushing up the probability the model assigns to real images. But writing that probability down 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. This is intractable: correct in principle, impossible to evaluate. So each family is a different trick for extracting a usable gradient (the direction to nudge the weights) without evaluating that density, and each trick’s cost is that family’s signature failure.

GAN — a learned divergence

A GAN sidesteps the density by hiring a second network to judge the first. A generator G(z) turns random numbers z into an image; a discriminator D(x) (the critic) is a classifier trained to output the probability that x is real. They 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 maximizes this while the generator minimizes it”. Freeze the generator and the best discriminator is D*(x) = p_data(x) / (p_data(x) + p_g(x)), the share of local density coming from real data, which returns 0.5 where the two overlap perfectly. Substitute D* back in and what remains is 2·JSD(p_data || p_g) - log 4, so minimizing over the generator minimizes the Jensen-Shannon divergence. The discriminator estimates a divergence with no closed form, and the generator walks downhill on that estimate. Both failure modes fall out of this.

Failure 1, mode collapse. The generator’s loss averages over its own samples; no term sums over data it fails to produce. So “I never generate this kind of face” costs the generator nothing directly, only indirectly, if the discriminator notices, and the discriminator, trained on small minibatches, notices slowly.

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 the loop and notice what is missing: no single number decreases from start to finish. Ordinary training gives a loss curve that tells you when to stop; this is a two-player game whose dynamics can circle a saddle point (a minimum along one direction and a maximum along another) forever. On a face model, identity diversity can drop from 0.41 to 0.09 over 20,000 steps while the discriminator loss looks perfectly healthy, which 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 loss log(1 - D(G(z))) flattens, killing the gradient where the generator most needs it. The standard non-saturating fix max_G log D(G(z)) restores the gradient but abandons the clean JS interpretation, which is why GAN training is a pile of empirical stabilizers instead of one derivation:

  • Spectral normalization: rescale each weight matrix so it cannot amplify its input beyond a fixed factor, keeping the discriminator from becoming too sharp.
  • R1 penalty: penalize the size of the discriminator’s gradient at real images, with the same effect.
  • Two-timescale learning rates: update the discriminator and generator at different speeds to keep the game balanced.

Each is a knob with no principled setting, retuned per dataset. What you get in exchange is that sampling is a single forward pass, about 10-30 ms per image on a modern GPU, one to two orders of magnitude faster than anything else here.

VAE — a bound, and why the bound blurs

A VAE dodges the density by maximizing a quantity provably below it. An encoder q(z|x) compresses an image into a short vector z (the latent, a compact code the image can be rebuilt from) and a decoder p(x|z) expands it back. The ELBO 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; the second is a regularizer pulling the encoder’s output toward a simple fixed prior p(z) (usually a standard bell curve), which makes the latent space smooth enough to sample from at generation time.

Why the output blurs. With the usual Gaussian decoder of fixed variance, -log p(x|z) = ||x - mu(z)||^2 / (2 sigma^2) + const, so reconstruction is an L2 loss: squared error, pixel by pixel. The prediction that minimizes squared error under uncertainty is the conditional mean, the average of every outcome still consistent with what you know. If one latent z is consistent with several plausible fine-detail completions (exact stubble, exact hair strands), the loss-minimizing output is their average, and the average of several textures is smooth. Blur is not low capacity; it is the exact minimizer of the objective. Raising the regularizer weight beta makes it worse (a wider average per code); set beta too low and the latent space develops holes that decode to garbage, too high and you get posterior collapse, where the decoder ignores z and emits the dataset’s average face.

Autoregressive — exact likelihood, sequential sampling

An autoregressive model refuses to dodge the density. It splits the impossible whole-image probability into a chain of easy one-piece-at-a-time probabilities: p(x) = prod_i p(x_i | x_<i). Fix a raster order (left to right, top to bottom) and every factor becomes an ordinary classification problem. The result is one loss, one network, no adversary, stable training, and a genuine likelihood you can compare across models. Three costs, the third being the one that matters:

  1. One image takes n sequential forward passes, one per piece, each conditioned on the last. At 1,024 pieces that is 1,024 passes run strictly in series, the same memory-bandwidth-bound serial decoding described in the LLM-internals lesson. The steps within one image cannot be batched, because each depends on the previous.
  2. Raster order is a wrong prior. Nothing about an image says pixel (i, j) depends causally on the row above it. The model spends capacity undoing an ordering you imposed for convenience.
  3. Likelihood in pixel space is a bad proxy for how good an image looks. Most of a photograph’s information is fine detail nobody inspects; a model can cut its NLL (negative log-likelihood) substantially by modelling sensor noise better and look no different to a human. Exact likelihood is what you asked for and not what you wanted.

Diffusion — a fixed corruption, a learned reversal

Diffusion turns generation into a long sequence of tiny, easy denoising problems. A forward process destroys an image by adding noise on a fixed, non-learned schedule over T steps; a network learns to undo one step. 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

Notation: x_0 is the clean image, x_t the image after t steps of noising. N(mean, variance) is a Gaussian; I is the identity, so N(0, I) is independent standard noise per pixel. eps is the noise sample that was added, eps_hat the network’s prediction of it. abar_t (written alpha_bar) is the schedule value at step t: near 1 (almost no noise) early, near 0 (almost pure noise) late. The second line says the noised image is a fixed blend of clean image and noise, so you can jump to any step t in one shot. The third says training is squared error between the noise added and the noise predicted: draw an image, draw a step, draw noise, add it, ask the network what you added.

Why it trains where GANs do not: the target is fixed. The noise eps comes from a random number generator, not a second network that is itself learning. Every step is ordinary supervised regression against a known answer, so one number goes down and stays down and there is no game.

Why sampling is slow. The reverse of a noising step is only itself Gaussian when the step is small, so you cannot leap from noise to image in one move. You evaluate the network T times in sequence. That is the family’s entire weakness, and reducing it is the subject of the image-synthesis lesson.

The comparison

Three terms in the table: denoising score matching is another name for diffusion’s loss (a noise-predicting network also estimates the score, the direction to move an image to make it more probable); inpainting regenerates a masked region while leaving the rest untouched; ControlNet steers a trained diffusion model with an extra input such as a pose skeleton. The last two are post-hoc: applied to an already-trained model with no retraining.

The two rows that decide this are Mode coverage (the failure with legal exposure) and Sampling cost (what diffusion is bad at).

GANVAEAutoregressiveDiffusion
Objectivelearned JS via a criticELBO (bound on forward KL)exact forward KLreweighted ELBO / denoising score matching
Sample qualityhighlow, blurryhighhighest
Mode coveragepoor — nothing penalizes omissiongood but smearedbest — likelihood punishes missed massgood
Training stabilitypoor — two-player gamegoodbest — 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 — post-hoc
Signature failuremode collapseblur / posterior collapseslow, wrong ordering priorslow sampling

The pick

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. (Latent-versus-pixel is a cost decision, derived later and in full in the image-synthesis lesson; the family choice and the space it runs in are separate.) Three reasons:

  1. The intolerable failure is the GAN’s. Mode collapse in a face model means silently dropping demographics, a fairness incident with a legal surface, invisible to FID. Diffusion’s failure is slow sampling, a cost problem with known fixes.
  2. Retraining cadence. Erasure requests and licence expiry mean retraining on a changing corpus several times a year. A procedure that needs a per-dataset stabilizer 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 steering are all applied after training, to the sampling loop. On a GAN each is a separate research effort against a latent space you did not design.

The tradeoff being accepted: a model roughly 60× more expensive to sample than a GAN, because the GAN’s cheapness is paid for with a failure mode you cannot detect or fix at serving time. Sampling cost is fixable: distillation (training a cheaper student to reproduce in a few steps what the many-step teacher produces) takes 60 forwards to 4. That asymmetry (sampling cost is fixable later, mode coverage is not) is the whole basis of the pick.

Diffusion mechanics

We’ll build the machinery in three pieces: a training objective that predicts the noise instead of 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.

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

q is the fixed, parameter-free noising process; p_theta is the learned reversal (theta is the standard symbol for a network’s trainable parameters). Only the bottom path has learned parameters. The dotted line is the training signal: at every intermediate state, the answer the network is graded against is the noise q actually added.

Why predicting noise works

The network could equally be trained to output the clean image, so why does every real system predict the noise? Start from the blending formula x_t = sqrt(abar_t)·x_0 + sqrt(1 - abar_t)·eps, which is invertible: x_0 = (x_t - sqrt(1 - abar_t)·eps) / sqrt(abar_t). So predicting eps and predicting x_0 carry identical information: knowing either, plus the noised image, gives the other.

They are not identical objectives. If the noise prediction is off by e, the inversion formula magnifies that error into an x_0 error of e · sqrt(1 - abar_t)/sqrt(abar_t). Define the signal-to-noise ratio SNR_t = abar_t / (1 - abar_t) (how much clean image is present relative to noise); then the magnification is exactly e / sqrt(SNR_t).

Read backwards, that is a reweighting: a constant-weight squared-error loss on eps is a squared-error loss on x_0 scaled by SNR_t. High-noise steps (low SNR) get a small weight; low-noise steps (high SNR) get a large one. This down-weights the high-noise steps by exactly the amount the clean image is genuinely unpredictable there, and it is the difference between diffusion and the plain VAE bound. It is why diffusion does not blur. Capacity is spent on low-noise steps, where the conditional mean is nearly a single point, not on high-noise steps, where averaging over many faces is unavoidable.

Two more reasons noise is the better target: the noise is always standard bell-curve noise, so one network with one output range handles the whole schedule; and predicting noise is also estimating the score (grad_x log q(x_t) = -eps / sqrt(1 - abar_t)), which is what makes deterministic ODE sampling available, treating generation as solving an ordinary differential equation so better numerical solvers can take far fewer, larger steps.

The schedule below uses the cosine schedule, the standard modern choice for how abar_t falls from 1 to 0. Check one row: at t/T = 0.50, abar = 0.4938, so SNR = 0.4938/0.5062 = 0.976 and the error gain is 1/sqrt(0.976) = 1.01, signal and noise balanced.

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

The right-hand column reads as a budget. The high-noise steps decide what matters most (layout, pose, identity) while the t/T <= 0.25 band decides things a human never consciously inspects. That is why aggressive step reduction is possible at all, and why the steps you cut must come from the low-noise end.

Classifier-free guidance

Classifier-free guidance (CFG) makes a diffusion model obey its request more strongly than it naturally would. Train one network to handle both conditional inputs (request supplied) and unconditional ones (request deleted with probability 0.1 during training). At sampling time, ask it twice, with the request (eps_c) and without (eps_u), and extrapolate past the conditional prediction, away from the unconditional one:

eps_guided = eps_u + w · (eps_c - eps_u)

At w = 1 this is exactly eps_c; at w > 1 you overshoot. The name is “classifier-free” because earlier methods steered with a separately trained classifier; this needs none.

Where it comes from. Sampling from a sharpened p_w(x|c) ~ p(x|c) · [p(c|x)]^(w-1), raising the implied classifier p(c|x) (the probability that image x matches request c) to a power, gives, after taking gradients and applying Bayes’ rule:

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

The log p(c) term drops out (no x dependence), leaving everything in terms of two scores the network already produces. Convert scores back to noise with eps = -sqrt(1 - abar)·grad log p and the guidance formula falls out exactly.

So the guidance scale is an exponent on a classifier. Raising p(c|x) to the power w concentrates 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, not a heuristic but what an exponent does to a probability.

Precision and recall (defined fully in the metrics section) are, for now, the fraction of generated faces that look real (fidelity) and the fraction of real variety the model still produces (coverage). Posterized means smooth gradients have collapsed into flat bands; blown highlights means bright areas saturated to 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 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 while humans prefer w ≈ 3. Both are true, and together 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, cannot see. The saturated look at high w has its own cause: the guided update drifts the latent out of the decoder’s trained range and the final clamp to [-1, 1] clips it flat; rescaling the guided prediction’s spread to match the conditional prediction’s recovers most of the tonal range.

Because guidance is one global number and it concentrates on the majority demographic, a single global w couples image quality to demographic coverage. If the fairness guardrail cannot be met at any single w, the design needs a per-condition guidance schedule and the serving path changes to carry it.

Training

The training cost falls out of the network spec, so the spec comes first.

Model. The backbone is a DiT-XL/2, a diffusion transformer, where the denoising network is an ordinary transformer instead of the convolutional U-Net diffusion models originally used. XL names the size, /2 the patch size.

Token count. The model runs in the latent space of an f=8 autoencoder (f=8 shrinks the image by 8 along each side):

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

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

Parameter count. With width d = 1152 (the vector length per token), depth L = 28 (stacked blocks), and 16 heads: the standard per-token count 12·L·d^2 gives 445.9M, and the adaLN-zero conditioning MLPs add 6·d^2 per block but act on one conditioning vector per image, adding 223.0M, total ≈ 669M, quoted as “675M”. adaLN-zero (adaptive layer normalization, initialized to zero) produces each block’s normalization scale and shift from the conditioning information instead of learning them as constants. The split matters later: only the 445.9M is multiplied by the token count; the full 669M is the model size.

Five recipe choices worth defending:

  1. Keep an exponentially moving average (EMA) of the weights (decay 0.9999) and sample only from the averaged copy. This matters more than in supervised learning because a small systematic denoiser bias is reapplied at every one of the T sampling steps and accumulates. Averaging out SGD noise is worth 3-5 FID points routinely, more than most architecture changes.
  2. Force the noise schedule to end at zero signal. The older scaled-linear schedule leaves abar_T ≈ 0.0047, so sqrt(0.0047) ≈ 0.069 of the original signal, including average brightness, still leaks into the “pure noise” the model trained on. At generation you start from actual noise it has never seen, so it cannot produce very dark or bright images. Fix: set abar_T = 0 and switch to v-prediction, a blended target that stays well-behaved at the endpoint.
  3. Horizontal flip is the only augmentation. A classifier can recolour and crop freely because it is meant to be blind to those changes; a generative model is learning the distribution of images itself, so any augmentation that alters colour, crop, or contrast corrupts the very thing being modelled. A mirrored face is still a face.
  4. Drop the condition on 10% of training examples. Required for guidance, and it doubles as a memorization control.
  5. Use bf16, not fp16. Both are 16-bit floats that halve memory traffic; bf16 trades precision for a much wider range. Internal attention scores are poorly scaled, and a value overflowing to NaN at step 300,000 costs a day of the run.

Compute, order of magnitude. A forward pass is about 1.05 TFLOP per image (token-linear term 0.913 + attention term 0.135). A training step is about 3 forwards (2 FLOPs/param/token forward, roughly twice that for the backward). Over 1.8 billion images seen that is ~5.7e21 FLOP; on 64 H100s at 300 TFLOP/s effective, ~219 GPU-days, about 3.4 days wall-clock, ~$13,100 at $2.50/GPU-hour. With the autoencoder, ablations, and two failed runs, budget ~$50k end to end.

Attention is quadratic here and is not what you are paying for. The forward cost splits into a token-linear term (grows with tokens n) and an attention term (grows with n^2); the ratio here is about 6.75 : 1 in favour of the token-linear term. The two would be equal at n = 6d tokens (the same crossover derived in the neural-layers lesson), which at d = 1152 is 6,912 tokens, and this model runs at 1,024, well below it. Attention becomes the bill in the image-synthesis lesson, at four times the resolution.

Every dollar figure here rests on the 300 TFLOP/s effective rate, about 30% of the H100’s peak, absorbing normalizations, the conditioning path, and kernel gaps. Assume the sticker number instead and every cost is understated by roughly 3×.

Offline metrics

The single number the whole field quotes, FID, is the wrong one to ship on. We’ll run the argument from its definition, through six ways it misleads, to the metrics that actually catch this product’s failures.

FID, and what it measures

FID (Fréchet Inception Distance) is worth defining precisely, because every criticism is visible in the definition:

  1. Push real and generated images through Inception-v3, an image classifier trained on ImageNet.
  2. Take the 2048 numbers each produces at the pool3 layer as its feature vector. You now have two clouds of points in 2048-dimensional space.
  3. Fit a Gaussian to each cloud, keeping only its mean and covariance.
  4. Report the Fréchet distance (Wasserstein-2 distance) between the two Gaussians:
FID = || mu_r - mu_g ||^2  +  Tr( S_r + S_g - 2 (S_r · S_g)^(1/2) )

mu_r, mu_g are the mean feature vectors; S_r, S_g the covariance matrices; Tr the trace. Lower is better; zero means identical means and covariances. Six flaws:

  1. It sees only the first two moments (mean and covariance) of a 2048-dimensional distribution. Any structure beyond those is invisible.

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

  3. It compresses fidelity and coverage into one number. A sharp mode-collapsed model and a diverse mediocre one can score identically (see precision/recall below).

  4. It is biased on few samples, and the bias is large and one-directional. A 2048 × 2048 covariance has ~4.2M entries; estimating that stably needs samples on the same order, and a shortfall pushes the score systematically up. Never compare FIDs at different sample counts:

    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, not faces. ImageNet has essentially no face categories, so the extractor was never asked to represent consistent eyes, matching irises, or anatomically possible teeth. It is strongly sensitive to texture, so FID reacts more to JPEG quality and resize filter than to a third eyebrow.

  6. It measures distance to your reference set. A model that reproduces the reference set’s demographic skew scores well for doing so.

Inception Score

The Inception Score (IS), exp( E_x [ KL( p(y|x) || p(y) ) ] ), is high when each image gets a confident ImageNet category and categories vary across the set. On faces it has almost no dynamic range, because every face maps to the same two or three ImageNet categories. Report it only if asked, and say why it is uninformative.

Precision and recall

Replacing FID’s single number with two separates the things it adds together. Build a k-NN manifold estimate: for each set, draw a ball around every feature vector reaching to its k-th nearest neighbour; the union outlines the region that set occupies. Then:

  • Precision is the fraction of generated samples inside the real region, how many outputs look real. This is fidelity.
  • Recall is the fraction of real samples inside the generated region, how much real variety the model produces. This is coverage.
ModelFIDPrecisionRecallDiagnosis
A (GAN)7.40.780.31mode collapse — beautiful, 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

The two models score the same FID and are opposites. This converts “FID conflates two things” from a slogan into a measurement you can act on.

Face-specific metrics you must add

None of the above were designed for faces; these four were. SSCD (self-supervised copy detection) is a network trained to tell whether two images are copies, the right feature space for a memorization check, and better than ArcFace for it because it looks at the whole picture.

MetricHowCatches
Identity diversityArcFace-embed 10k samples, report the distribution of pairwise cosine, not the meanmode collapse, latent duplicates — a collapsing model grows a bump above cosine 0.5
Attribute marginal TV distanceClassify 10k samples into skin-tone / age / pose bins, TV distance 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
Symmetry defect ratePer-eye iris colour delta, landmark asymmetry residual after alignmentthe artifact class humans notice first

Report the distribution or the tail, never the mean, the same point as flaw 2. Rare catastrophic samples vanish in an average by construction, and they are the whole problem.

Human evaluation

Every automatic metric is a function of a feature extractor 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 a distribution metric cannot represent them.

Run a 2AFC study (two-alternative forced choice): show a rater one generated and one real image and have them pick the real one. Use 500 pairs, 5 raters each, and report three things: the fooling rate (share of pairs raters got wrong, 50% is the target, meaning they were guessing); a defect-taxonomy checklist (a fixed list of named defect classes with incidence of each); and Krippendorff’s alpha (rater agreement, 0 for chance to 1 for perfect, below 0.6 your taxonomy is ambiguous, not your model). Cost: 2,500 judgments at ~$0.36 each ≈ $900 and about two days per checkpoint, which is why this gates releases, not commits.

The one non-negotiable is that FID is never the only reported number. Coverage, fairness, and memorization are all invisible to FID by construction, so a process that gates on FID alone is not a weaker process. It is a process using the wrong instrument.

Online metrics and the A/B

Once real users are involved, the question shifts to whether a new checkpoint ships, decided by an A/B test: split users into two groups, give each a different version, compare.

MetricDefinitionRole
Accept-on-first-batchuser picks a face from the first 4 shownprimary
Regenerations per acceptbatches before a picksecondary, 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

A guardrail blocks a launch instead of informing one: you clear it or you do not ship. p50 is the median; p95 (used later) is the value 95% of observations fall below, giving the slow tail a number. The safety cascade is the chain of output checks in the serving diagram; its block rate is a guardrail in both directions, because a falling rate is as suspicious as a rising one.

The randomization unit is the user, not the request. Regeneration is the behaviour under test, so assigning per request would let one user straddle both arms and destroy the comparison.

Sizing. With statistical power (the chance of detecting a real effect) at 80%, 95% confidence, baseline accept rate 42%, and a minimum detectable effect of 2 pp (percentage points, absolute, 42% vs 44%), the standard formula n = 16·p(1-p)/delta^2 gives ~9,744 users per arm. At 20,000 sessions/day split 50/50, power is reached in about a day. Run 7 days anyway so the readout covers a full weekly cycle: accept rates differ weekday to weekend, and a one-day readout would attribute that to your change.

The fairness guardrail is a gate, not a reported number. 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 that drops modes under guidance. Demote it to a dashboard line and the launch process has nothing that can stop a coverage regression.

Serving architecture

The request path is what the metrics watch. A request carrying attributes and a count is authenticated and rate-limited against a per-account generation log. 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 (denoising diffusion implicit models, the deterministic sampler from the image-synthesis lesson) at guidance scale 3.0. The latent is decoded to a 512 × 512 image, the three-stage safety cascade runs, and a clean image is watermarked, signed, and put behind a CDN (content delivery network) as a signed URL with a 24-hour TTL (time to live).

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 choices, all about where a control runs:

  1. The policy check is before the queue and before any GPU. A control that runs in ordinary code cannot be argued out of running by the content it inspects, the way a control that is itself a model can, the same argument as deterministic escalation in the customer-support-agent lesson. Move it after generation and you have paid for every refused image and made an internal copy of exactly the content the policy exists to prevent.
  2. Guidance’s two forward passes are packed into one batch. The conditional and unconditional passes are the same network on the same shapes, so running them as a batch of two costs about the same as one pass.
  3. The memorization check needs the training index in memory. About 3M ArcFace vectors × 512 dims × 2 bytes ≈ 3.1 GB, small enough to hold an HNSW index (hierarchical navigable small world, the standard graph structure for fast approximate nearest-neighbour search) in-process on every replica instead of behind a network call. That is the only reason this check is affordable. At 30M images it would not fit, and the check would become a network hop on the critical path.
  4. Resample, don’t refuse, on the first hit. A memorization hit is a property of the random seed (the starting noise), not the request, so a fresh seed usually clears it. Three strikes means something is wrong with the request, and that is what the alert is for.

Scale and cost

The configuration is 512 × 512 output, 30 DDIM steps, and guidance on (which doubles network evaluations, one conditional and one unconditional per step).

Per image, order of magnitude: 60 forwards (30 steps × 2) × 1.05 TFLOP + decode + safety ≈ 63.4 TFLOP. At 300 TFLOP/s that is 0.21 s of GPU time per image, about $0.00015 at full utilization, and ~$0.00024 (= $0.24 per 1,000) at 60% fleet utilization, the divisor because you pay for GPUs by the hour whether or not a request is in flight, and bursty traffic wastes about 40%. Throughput is ~4.7 images/s/GPU; latency for a batch of 16 is ~3.4 s.

At 2M images/day: ~$489/day of GPU, ~4.9 GPUs of steady-state load, and ~8 replicas once you provision for the diurnal peak and p95 instead of the mean.

Which decision bought the savings

Remove 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, precision 0.76 → 0.58
250 steps instead of 30525.4$0.00203$4,054+$3,565/day for FID 7.57 → 7.30
4-step distilled, guidance distilled4.6$0.000018$36-$453/day, recall falls ~0.14 from 0.51
Pixel-space diffusion at 512546,000$2.11$4.2M+8,600×

Precision and recall are differenced against the w=3 row of the guidance table (0.76, 0.51), since the full design runs at w=3. The step-count FID change comes from the image-synthesis lesson’s second-order fit at both endpoints.

The last row: pixel-space at 512 means one token per pixel, n = 262,144 instead of 1,024, a 256× increase. The token-linear term grows 256× but the attention term grows with n^2, so ~65,536×, giving ~9,100 TFLOP per forward, ~30 s of H100 time per forward, and ~30 minutes per image against 0.21 s for the latent design. That single latent-space choice bought an 8,600× reduction (546,000 / 63.4); every other lever is under 15×, distillation 13.7×, step count 8.3×, guidance 2×. Latent-versus-pixel is not the model-family question; it is the same diffusion family run on a compressed representation.

Failure modes

One failure mode carries legal exposure, the model reproducing a real photograph, and it traces all the way to the objective. The other seven each come with a mechanism, a detector, and a guard.

Memorization, traced

Memorization is what the objective asks for in a specific regime. The trained denoiser converges to the posterior mean: the average of every training image, weighted by how consistent each is with the noisy picture.

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

x_0^(i) is the i-th training image and k_i how many times it appears. The distance term is between the noisy image and where training image i would sit after t steps; small distance means “consistent with what I see”, and the minus sign turns that into a large weight. Those weights are a softmax (exponentiate each score, divide by the total): the more the scores spread apart, the more completely the largest dominates, until one weight goes to 1 and the “average of every training image” collapses into one image. Two things drive it there:

  • Low noise. As abar_t → 1 the denominator (1 - abar_t) shrinks, blowing the exponents up and sharpening the softmax onto the nearest training image.
  • Duplicate count. k_i multiplies the weight before normalization, so an image appearing 30 times pulls 30 times as hard, which is why dedup is a safety control, not housekeeping.

The trace below runs the same seed against a corpus with duplicates left in and once deduplicated. NN is the nearest training image in the named feature space; a cosine of 0.981 in copy-detection space is not “similar to”, it is “the same photograph”.

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

  dedup off, condition = "age 30-40, frontal, studio lighting", seed 8812
  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

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 (removes the k_i multiplier); drop the condition on 10% of examples (a specific condition otherwise acts as an index pointing at the one matching image); and run the output-side nearest-neighbour check for whatever the first two missed. Set the detection threshold on the tail of the distance distribution, not the mean. A healthy model’s average nearest-neighbour cosine barely moves when 0.1% of samples are outright reproductions. Memorization can only be caught at the output; nothing drives it to zero, which is why the training index is a serving dependency.

The other failure modes

Vocabulary: FFT (fast Fourier transform) decomposes an image into repeating patterns, so a periodic defect nearly invisible to the eye shows up as a sharp spike at its frequency. A transposed convolution is the standard upsampling operation inside a network (a convolution run backwards to enlarge). Minibatch discrimination and an unrolled discriminator are GAN stabilizers: the first lets the critic see a whole batch at once, the second lets the generator see several of the critic’s future updates.

FailureMechanismDetectionGuard
Mode collapse (GAN only)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. If you must: minibatch discrimination, unrolled D, monitor recall not FID
Demographic dropout under guidanceGuidance is an exponent on p(c|x); it concentrates on the mode interior, which is the majority demographicTV distance of the skin-tone marginal at each wCap w where TV distance exceeds 0.03. At w=7 the two darkest bins drop 40% relative
Checkerboard artifactsTransposed conv with kernel not divisible by stride gives uneven output overlapFFT of the residual shows a spike at the stride frequencyNearest-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 for attention budget; nothing in the loss privileges itPer-eye colour delta on 10k samplesHigher token resolution, or a symmetry-aware discriminator; mostly, measure it and set a release bar
Mean-luminance collapseabar_T ≈ 0.0047 leaks ~7% of signal into the “pure noise” the model trained onOutput mean-luminance histogram 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

Alternatives rejected

Each rejection’s value is the reason attached to it, because the reason names the assumption that would have to change to reverse it.

AlternativeWhy it is temptingWhy rejected
StyleGAN-class GAN60× cheaper sampling, state of the art on aligned faces by FID, nice latent space for editingMode collapse is undetectable by FID and its consequence here is demographic dropout. Training needs per-dataset stabilizer tuning, and this model retrains several times a year
VAE aloneOne network, stable, fast, exact latent inferenceThe L2 term makes the conditional mean the optimum, and the conditional mean of several plausible 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 becomes a sequence of discrete codes from a learned codebook)Exact likelihood, one stable loss, same infrastructure as an LLM1,024 sequential decodes per image, an unjustified ordering prior, and a likelihood that rewards modelling sensor noise. Revisit if you need one model over text and images
Pixel-space diffusionNo autoencoder, no compression quality ceiling8,600× the serving cost at 512
U-Net backbone instead of DiT (the convolutional encoder-decoder diffusion originally used)Proven, strong locality prior, well-tuned public recipesCompute is spread across hand-designed resolution stages, so scaling is guesswork, and model FLOP utilization is lower on hardware that prefers uniform shapes
Scrape a public face dataset for v1Free, everyone did it, 10× the dataThe v1 checkpoint becomes the product. There is no subtraction on trained weights, so an unlawful-basis dataset is permanent
Watermark as the misuse controlCheap, invisible, sounds completeAbsence proves nothing, and a single img2img pass takes bit accuracy to 0.61. It attributes your 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 — carry criminal and civil exposure. Terms of service are not a control
Tune step count as the main cost leverVisible, easy, no quality risk if measured30 → 250 steps is 8× cost for 0.3 FID, inside the noise floor. The 8,600× lever was the latent space; the next 14× is distillation
Report FID aloneOne number, universally quotedMinimized at a guidance scale humans dislike, rewards reproducing your reference set’s bias, blind to a 1% catastrophic rate. Report it with precision, recall, and the attribute marginal, always as a set

Conclusion

  • Consent, misuse, and provenance decide the design before the network does. They fix the training data (licensed, consented, deduplicated, minor-excluded) and the serving path (identity check, watermark, signed manifest). A scraped corpus is a permanent property of the checkpoint, not a shortcut.
  • The model-family choice is a choice of divergence. Forward KL is mode-covering (blur); reverse KL is mode-seeking (dropped modes). For faces a dropped mode is a fairness incident with legal exposure, so the maximum-likelihood family wins over the GAN.
  • The pick is latent diffusion. Its only real weakness, slow sampling, is fixable at serving time (distillation, fewer steps); the GAN’s mode collapse is not. Running in latent space instead of on pixels is the single 8,600× cost decision; every other lever is under 15×.
  • Diffusion predicts noise because the SNR reweighting spends capacity where the image is predictable and avoids the VAE’s blur, and classifier-free guidance trades diversity for fidelity as an exponent on an implied classifier.
  • FID is never the only metric. It is blind to per-sample defects, to coverage, and to the demographic skew it actively rewards. Report precision, recall, attribute-marginal TV distance, a tail-based memorization rate, and a human 2AFC study alongside it.
  • Watermarking attributes your own output; it does not defend against deepfakes. The real misuse control is upstream: refuse identity-targeted requests, verify and rate-limit the API, log every generation.

One line to remember: for a face generator the model family is chosen by which failure you can survive, and a silently dropped demographic is the one you cannot.

Further reading

  • Goodfellow et al., “Generative Adversarial Networks” (2014), the original GAN.
  • Kingma & Welling, “Auto-Encoding Variational Bayes” (2013), the VAE and the ELBO.
  • Ho, Jain & Abbeel, “Denoising Diffusion Probabilistic Models” (2020), the noise-prediction objective used here.
  • Rombach et al., “High-Resolution Image Synthesis with Latent Diffusion Models” (2022), latent diffusion.
  • Peebles & Xie, “Scalable Diffusion Models with Transformers” (2023), the DiT backbone and adaLN-zero.
  • Ho & Salimans, “Classifier-Free Diffusion Guidance” (2022).
  • Salimans & Ho, “Progressive Distillation for Fast Sampling of Diffusion Models” (2022), v-prediction and step distillation.
  • Lin et al., “Common Diffusion Noise Schedules and Sample Steps Are Flawed” (2024), zero terminal SNR.
  • Heusel et al., “GANs Trained by a Two Time-Scale Update Rule Converge to a Local Nash Equilibrium” (2017), FID.
  • Kynkäänniemi et al., “Improved Precision and Recall Metric for Assessing Generative Models” (2019).
  • Karras, Laine & Aila, “A Style-Based Generator Architecture for GANs” (2019), StyleGAN and the FFHQ dataset.
  • Deng et al., “ArcFace: Additive Angular Margin Loss for Deep Face Recognition” (2019).
  • Coalition for Content Provenance and Authenticity (C2PA), the content provenance specification.
Report a bug