Skip to content

← Generative Models step 10 of 11

Medium End-to-End

DDPM Training Step End-to-End

Implement ddpm_train_step(x_clean, w_model, betas, alpha_bar, lr, seed). Train a small linear noise predictor for exactly one batch and one SGD update. Compute the gradient yourself using tensor operations, without automatic differentiation in either framework.

Inputs and output

  • x_clean has shape (N, d), with N and d positive.
  • w_model has shape (d + 1, d). Its last row weights the time feature.
  • betas and alpha_bar have shape (T,), with T >= 1. Read T from betas, but use the supplied alpha_bar values for noising; do not recompute them. Each cumulative value lies between 0 and 1 inclusive.
  • All tensors share a floating-point dtype and device.
  • lr is a finite, nonnegative Python float; seed is a nonnegative integer.

Return the updated weights flattened in row-major order, with shape ((d + 1) * d,), preserving the weights' dtype and device. Return neither the loss nor a prediction. Leave every input unchanged.

One training update

  1. Draw one timestep t uniformly from 0 through T-1. Use this same t for every example in the batch.
  2. Draw a standard normal tensor eps of shape (N, d), with one draw per element. The sampling recipes below fix the generators and draw order.
  3. Construct the noisy input: x_t = sqrt(alpha_bar[t]) * x_clean + sqrt(1 - alpha_bar[t]) * eps.
  4. Append a column containing t / T to x_t. This gives inp with shape (N, d + 1). Use T, not T - 1, as the denominator.
  5. Compute predicted = inp @ w_model. There is no bias or activation.
  6. Compute the gradient of the mean squared error between predicted and eps, then update the weights once.

The target is the sampled eps, not x_clean. Treat inp and eps as fixed when differentiating with respect to w_model:

loss = mean((predicted - eps)**2)
d_predicted = 2 * (predicted - eps) / (N * d)
grad_w = inp.T @ d_predicted
updated_weights = w_model - lr * grad_w

The mean includes every batch and feature element, so the divisor is N*d, not N alone. Compute the whole gradient from the original weights before updating. With lr=0, the returned values are the original weights flattened.

For T=1, the sampled timestep is always 0 and the time column is zero. The last row of weights then has zero gradient, while the other rows can still change.

Reproducible sampling

Use seed for the timestep and seed+1 for the noise, with separate fresh generators or keys. Draw the noise in float32, then convert it to the input dtype and device. Each framework has its own expected outputs, compared with floating-point tolerance.

In PyTorch

Create gen_t = torch.Generator().manual_seed(int(seed)) and sample torch.randint(0, T, (1,), generator=gen_t, dtype=torch.int64, device="cpu"). Convert its single value to a Python integer.

Create a second CPU generator seeded with int(seed)+1. Draw torch.randn(N, d, generator=gen_eps, dtype=torch.float32, device="cpu"), then move eps to x_clean's dtype and device. Leave the global RNG unchanged.

In JAX

Sample t from jax.random.randint(jax.random.PRNGKey(int(seed)), (1,), 0, T, dtype=jnp.int32), then convert its single value to a Python integer.

Draw eps with jax.random.normal(jax.random.PRNGKey(int(seed)+1), (N, d), dtype=jnp.float32) and convert to x_clean.dtype. Do not split keys. The runtime sets jax_threefry_partitionable=True for these fixtures.

The noising and noise-prediction objective follow Denoising Diffusion Probabilistic Models, using this exercise's linear model.