gallery unit · generative core · bottleneck class I
Latent diffusion mechanics
A diffusion model learns to undo noise. Do that one pixel at a time and a single 512x512 image is 786,432 numbers to clean up, a thousand times over. The word “latent” is the whole trick: squeeze the image into a small code first, denoise the code, expand it back at the end. The loop then runs on 48x fewer numbers. That is the difference between a datacenter and a laptop.
scenario sandbox
The schedule is the part of a diffusion model that carries no weights. Pick one of the repo's configs, drag the timestep, and watch a 24x24 toy field dissolve into noise and come back. This is the repo's arithmetic running in your browser, not its trained model.
pick a schedule, drag the timestep, then break the predictor
t = 500 of 1000 · txt2img 1.4B
The field is carrying 52.6% of its original amplitude and 85.1% noise. Hand the reverse step the exact noise that went in and the original field comes back whole, RMS error 0.000.
alphas_cumprod[500] = 0.2763 · beta[500] = 0.004815 · endpoints 0.00085 to 0.012 from configs/latent-diffusion/txt2img-1p4B-eval.yaml
x_t · forward step at t = 500
q_sample: sqrt(abar_t) * x_0 + sqrt(1 - abar_t) * eps. Signal 52.6%, noise 85.1%.
x0_hat · one reverse step
predict_start_from_noise: sqrt(1/abar_t) * x_t - sqrt(1/abar_t - 1) * eps_hat. Error gain at this step 1.6x.
The right panel is handed the true noise, optionally spoiled. A trained U-Net’s entire job is estimating that noise from the left panel alone. Nothing here estimates anything, so read the right panel as the schedule’s arithmetic, never as model output.
The toy field is a fixed 24x24 pattern, not an image and not a latent. What comes from the repo is the schedule: 0.00085 to 0.012 over 1000 steps, configs/latent-diffusion/txt2img-1p4B-eval.yaml, run through make_beta_schedule in ldm/modules/diffusionmodules/util.py and the two formulas in ldm/models/diffusion/ddpm.py.
the squeeze, in arithmetic
The autoencoder in autoencoder_kl_32x32x4.yaml lists ch_mult: [1,2,4,4] and embed_dim: 4. Class Encoder in ldm/modules/diffusionmodules/model.py attaches a downsample at every level except the last, halving the side each time, so four entries means three halvings and a spatial factor of 8. Run a 512x512 RGB image through it:
image 512 x 512 x 3 = 786,432 numbers
latent 64 x 64 x 4 (512/8 = 64) = 16,384 numbers
---------
the U-Net denoises the second one, 48x smaller, 1000 times| autoencoder config | f | latent, 512x512 in | values | vs pixels |
|---|---|---|---|---|
| autoencoder_kl_64x64x3 | 4 | 128x128x3 | 49,152 | 16x |
| autoencoder_kl_32x32x4 | 8 | 64x64x4 | 16,384 | 48x |
| autoencoder_kl_16x16x16 | 16 | 32x32x16 | 16,384 | 48x |
| autoencoder_kl_8x8x64 | 32 | 16x16x64 | 16,384 | 48x |
Two things fall out of the table. The three configs at f = 8 and above all land on 16,384 values: each doubling of the spatial factor is paid back by quadrupling the channels, so the count holds and only the shape moves. And the derivation checks itself against the filenames. At the configs’ own resolution of 256, 256/f gives 64, 32, 16, 8, which is exactly what 64x64x3, 32x32x4, 16x16x16, 8x8x64 claim.
what you are looking at
01 · signal in
A fixed 24x24 field stands in for a latent. It is a pattern, not an image: small enough to watch cell by cell, which is the only reason it is here.
02 · schedule runs
The forward step mixes in noise using the beta endpoints from the config you picked, over the repo's 1000 timesteps. Drag t and the mix moves.
03 · one step back
The reverse step is handed the true noise and returns the original. Spoil that noise by 5 percent and watch the same step multiply the mistake.
the pipeline, from the code
Three models, trained in two stages. The autoencoder learns the squeeze first and is frozen. The U-Net then learns to predict noise inside that squeezed space, with a text or class encoder feeding it context through cross-attention. Sampling walks the schedule backwards and decodes once at the end.
stage 1 AutoencoderKL ldm/models/autoencoder.py:278
Encoder / Decoder ldm/modules/diffusionmodules/model.py:344
image 256x256x3 -> z 32x32x4 -> image f = 8 from ch_mult [1,2,4,4]
frozen after this stage; scale_factor 0.18215 rescales z
stage 2 LatentDiffusion ldm/models/diffusion/ddpm.py:401
register_schedule builds the beta tables ddpm.py:106
q_sample adds noise to z ddpm.py:252
prompt -> BERTEmbedder ldm/modules/encoders/modules.py:76
n_embed 1280, n_layer 32
| context, 1280-d
v
+-- UNetModel -----------------------------+ ldm/modules/diffusionmodules/openaimodel.py:346
| in 4ch, out 4ch, model_channels 320 |
| channel_mult [1,2,4,4], 2 res blocks |
| attention_resolutions [4,2,1] |
| SpatialTransformer depth 1, 8 heads | ldm/modules/attention.py:216
| CrossAttention q=z, k/v=context | ldm/modules/attention.py:150
+------------------------------------------+
| eps_hat
v
sampling DDIMSampler / PLMSSampler loop over t ddim.py:11 / plms.py:11
predict_start_from_noise closes each step ddpm.py:195
z_0 -> AutoencoderKL.decode -> image
all shapes above are read from configs/latent-diffusion/txt2img-1p4B-eval.yamlOne correction, because it is the thing everyone assumes. Neither text path above conditions on CLIP. txt2img-1p4B-eval.yaml wires BERTEmbedder, cin256-v2.yaml wires ClassEmbedder, and no config in the tree, under configs/ or models/, names a CLIP class at all.
CLIP is still here, just not through a config. environment.yaml pins openai/CLIP as an editable install, ldm/modules/encoders/modules.py defines FrozenCLIPTextEmbedder at line 133 and FrozenClipImageEmbedder at line 162, and scripts/knn2img.py imports both directly for the retrieval-augmented path. That path’s config, configs/retrieval-augmented-diffusion/768x768.yaml, sets cond_stage_config to torch.nn.Identity and takes its 768-wide context precomputed from outside. So the conditioning encoder is chosen by the script there, not by the config.
the claims with no artifact
Three claims travel with this project in its resume form: a training run on 10 H100 nodes over pooled ArtBench and OpenImages, a 20 percent FID improvement, and 1.84 second inference. None of them are presented as results here. They were claimed during development and the artifacts were not retained in the repository.
The repository was read end to end twice, most recently on 12 August 2026: 65 tracked files, 2 commits, both dated 4 December 2024. It holds no training log, no eval output, no checkpoint, and no generated sample beyond the eight that ship with the upstream source. So none of the three numbers appear anywhere else on this page, in the gallery card, or in the page metadata.
| claimed, unverified | what the repo actually holds | what would verify it |
|---|---|---|
| Trained on 10 H100 nodes over pooled ArtBench and OpenImages | Two commits, under two minutes apart. The second is titled Saving Model Weights and contains no weights: it adds the CompVis source tree, its configs, and eight stock sample images. No checkpoint, no run directory, no lightning_logs, no dataset manifest. | A training log carrying step count, loss curve, wall clock, and the config that ran, next to the checkpoint or its hash. |
| FID reduced by 20 percent | No eval output, no metrics file, no reference statistics, and no baseline number for the 20 percent to be measured against. | An eval output naming the FID implementation, the reference statistics, the sample count, and the eval config, published beside the baseline it improves on. |
| 1.84 second inference latency | No timing harness, no generated samples beyond the eight stock CompVis assets, no hardware named anywhere in the tree. | A generation run recording sampler, step count, guidance scale, resolution, batch size, device, and wall clock, published with the samples it produced. |
Be exact about the two dataset names in the repository title. Both appear in the tree, and neither is evidence of a training run. ArtBench appears as ten entries in the DATABASES list in scripts/knn2img.py, which is the upstream CompVis list of retrieval databases for retrieval-augmented diffusion. OpenImages appears in models/ldm/layout2img-openimages256/config.yaml, an upstream config whose data module is ldm.data.openimages, a file this tree does not contain: ldm/data/ holds base.py, imagenet.py, and lsun.py only. That config cannot load the dataset it names.
The rule this page follows is the same one every unit in the gallery follows. A claim with an artifact ships with its regime attached. A claim without one ships here, in this panel, labelled as unverified, or it does not ship. If the logs and the eval output turn up, this unit gets rebuilt around them and this panel gets shorter. Until then the demonstration above stands on the only thing the repository can back: its configs and its code, both of which run in your browser a screen up.
Go deeper
The schedule, exactly as the repo computes it
Neither config sets beta_schedule, so both take the "linear" default from the DDPM.__init__ signature at ldm/models/diffusion/ddpm.py line 38. That name is misleading and worth flagging: the function it selects is linear in the square root of beta, not in beta.
# ldm/modules/diffusionmodules/util.py:14
if schedule == "linear":
betas = (
torch.linspace(linear_start ** 0.5, linear_end ** 0.5,
n_timestep, dtype=torch.float64) ** 2
)
# ldm/models/diffusion/ddpm.py:106 register_schedule
alphas = 1. - betas
alphas_cumprod = np.cumprod(alphas, axis=0)
sqrt_alphas_cumprod = sqrt(alphas_cumprod)
sqrt_one_minus_alphas_cumprod = sqrt(1. - alphas_cumprod)
sqrt_recip_alphas_cumprod = sqrt(1. / alphas_cumprod)
sqrt_recipm1_alphas_cumprod = sqrt(1. / alphas_cumprod - 1)Squaring a linear ramp puts most of the schedule’s small betas early, so signal survives much longer than a plain linear ramp would let it. The sandbox above ports those seven lines to TypeScript, endpoint for endpoint, and the ported betas land on the config values exactly at both ends. The three preset chips differ only in those two endpoints, and they behave visibly differently: at t = 999 the txt2img schedule still holds a measurable trace of the signal, and cin256 has effectively none.
Why the reverse panel is handed the answer
A reverse step needs two things: the noisy field, and an estimate of the noise inside it. The trained U-Net supplies the second one, and that is the only part of a diffusion model the weights are for. Everything around it is fixed arithmetic:
# ldm/models/diffusion/ddpm.py:195
def predict_start_from_noise(self, x_t, t, noise):
return (sqrt_recip_alphas_cumprod[t] * x_t -
sqrt_recipm1_alphas_cumprod[t] * noise)There are no weights on this page, so the demo hands that step the noise it added itself. Read the right panel as the arithmetic, never as model output. The interesting part is the second coefficient. It is the error gain: whatever the predictor gets wrong, this step multiplies it by that number, and on the txt2img schedule it climbs from 0.57 at t = 200 to 14.6 at t = 999. That is why sampling walks back in many small steps instead of jumping. Late in the trajectory almost nothing is left of the signal, so a small mistake about the noise becomes a large mistake about the image. Switch the predictor to 5 percent off and drag t: the RMS error in the banner tracks the gain, because that is what the multiplication does.
Where this repository came from
The source is the CompVis latent-diffusion tree. It was copied in rather than forked with history: the repository holds two commits, 1 minute and 59 seconds apart on 4 December 2024. The first carries a LICENSE and a two-line README. The second adds all 12,646 lines of the upstream tree in one move, under the title Saving Model Weights, and adds no weights.
The lineage is not in doubt: environment.yaml pins CompVis/taming-transformers and openai/CLIP as editable installs, which is upstream’s own dependency list, setup.py declares the package as latent-diffusion 0.0.1, and the eight images in assets/ are the upstream fire prompts. Credit for the architecture, the configs, and the code on this page belongs to the CompVis authors of the latent diffusion paper. The work in this unit is the reading of it.
What this demo does not do
- It runs no model. There are no weights on this page and no inference of any kind. The 24x24 field is a toy pattern, not a latent produced by the autoencoder.
- It shows one reverse step, not a sampling loop. DDIM and PLMS chain hundreds of these with a guidance term; none of that is modeled here.
- The compression table is exact arithmetic on config values. It says nothing about reconstruction quality, which is what the KL and LPIPS terms in the autoencoder configs actually trade against.
- No generated images appear anywhere on this page. The eight in the repo’s assets/ folder are upstream CompVis samples and are not this project’s output.