Skip to content

← Generative Models step 7 of 11

Hard Primitives

DDPM Denoising Step

Implement ddpm_reverse_step(x_t, predicted_noise, t, betas, alpha_bar, seed) to take one reverse diffusion step. The model's noise prediction is already supplied; use it to compute the reverse mean, then add fresh noise unless this is the final step.

Inputs and output

  • x_t and predicted_noise have shape (N, d).
  • betas and alpha_bar have shape (T,). alpha_bar contains cumulative alphas, not the per-step values 1 - betas.
  • At the selected index, 0 < betas[t] < 1 and 0 <= alpha_bar[t] < 1.
  • All tensors share a floating-point dtype and device.
  • t is an integer with 0 <= t < T, shared by all batch rows.
  • seed is a nonnegative integer, used only when t > 0.

Return one tensor with the shape, dtype and device of x_t. Leave inputs unchanged. Use the supplied schedule values directly.

Reverse update

beta = betas[t]
alpha = 1 - beta
mean = (x_t - beta / sqrt(1 - alpha_bar[t]) * predicted_noise) / sqrt(alpha)

if t > 0:
    output = mean + sqrt(beta) * z
else:
    output = mean

Here z is a standard normal tensor with the full shape of x_t. Each element gets its own noise draw.

This exercise chooses beta as the reverse-step variance. Use sqrt(beta) as the noise multiplier; do not substitute the alternative posterior variance that appears in some DDPM samplers. The reverse mean depends on both the current beta and the cumulative alpha.

The final step

The array indices run backward from T-1 to 0. Index 0 is the last reverse step, undoing the first forward noising step. Return the mean at t=0 without drawing noise. Do not return x_t unchanged, and do not look up a schedule entry at index -1. The result estimates clean data but need not recover it exactly.

For example, at t=0 with beta=0.25, alpha_bar=0.75, x_t=[[1, -1]] and predicted_noise=[[2, 0]], the numerator is [[0, -1]]. Divide by sqrt(0.75) to get approximately [[0, -1.154701]], regardless of the seed.

Reproducible noise for t > 0

Draw one float32 noise tensor, then convert it to x_t's dtype and device. The frameworks produce different seeded samples and have separate expected values, compared with floating-point tolerance.

In PyTorch

Use a fresh CPU generator, torch.Generator().manual_seed(int(seed)). Draw torch.randn(x_t.shape, generator=gen, dtype=torch.float32, device="cpu"), then move the result to x_t's dtype and device. Leave the global generator state unchanged.

In JAX

Use jax.random.PRNGKey(int(seed)) and jax.random.normal(key, x_t.shape, dtype=jnp.float32), then convert to x_t.dtype. Draw the full tensor in one call without splitting the key. The runtime sets jax_threefry_partitionable=True for these fixtures.

See Denoising Diffusion Probabilistic Models, Algorithm 2, by Ho, Jain and Abbeel.