Skip to content
AILinkDeepTech
Go back
Deep Learning Medium

Stable Diffusion (DDPM): Minimal PyTorch Implementation

Abstract

A minimal PyTorch implementation of a Denoising Diffusion Probabilistic Model (DDPM): sinusoidal time embeddings, U-Net with skip connections, forward noising, reverse denoising, and ancestral sampling.

Stable Diffusion (DDPM): Minimal PyTorch Implementation

This implementation builds a minimal Denoising Diffusion Probabilistic Model (DDPM), the core of the Stable Diffusion family. It includes sinusoidal time embeddings, a U-Net with skip connections and time-conditioned features, a forward noising process, and an ancestral reverse denoising process for sampling.

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import math

class SinusoidalPositionEmbeddings(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.dim = dim

    def forward(self, time):
        device = time.device
        half_dim = self.dim // 2
        embeddings = math.log(10000) / (half_dim - 1)
        embeddings = torch.exp(torch.arange(half_dim, device=device) * -embeddings)
        embeddings = time[:, None] * embeddings[None, :]
        embeddings = torch.cat((embeddings.sin(), embeddings.cos()), dim=-1)
        return embeddings

class UNet(nn.Module):
    def __init__(self, in_channels=3, time_dim=256):
        super().__init__()

        self.time_mlp = nn.Sequential(
            SinusoidalPositionEmbeddings(time_dim),
            nn.Linear(time_dim, time_dim),
            nn.GELU(),
            nn.Linear(time_dim, time_dim)
        )
        
        self.conv_in = nn.Conv2d(in_channels, 64, kernel_size=3, padding=1)

        # Downsampling path
        self.down1 = nn.Sequential(
            nn.Conv2d(64, 128, 3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )
        self.down2 = nn.Sequential(
            nn.Conv2d(128, 256, 3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )

        # Middle blocks
        self.mid_block1 = nn.Sequential(
            nn.Conv2d(256, 512, 3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
        )

        # Time embedding projection
        self.time_proj = nn.Conv2d(time_dim, 512, 1)
        
        self.mid_block2 = nn.Sequential(
            nn.Conv2d(512, 256, 3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
        )

        # Upsampling path
        self.up1 = nn.Sequential(
            nn.ConvTranspose2d(256, 128, 2, stride=2),
            nn.BatchNorm2d(128),
            nn.ReLU()
        )
        self.up2 = nn.Sequential(
            nn.ConvTranspose2d(256, 64, 2, stride=2),
            nn.BatchNorm2d(64),
            nn.ReLU()
        )

        self.conv_out = nn.Conv2d(128, in_channels, kernel_size=3, padding=1)

    def forward(self, x, t):
        t_emb = self.time_mlp(t)  
        
        x1 = self.conv_in(x)

        # Downsample
        x2 = self.down1(x1)
        x3 = self.down2(x2)
        
        # Middle block
        x3 = self.mid_block1(x3)
        
        # Project time embedding and add to features
        t_emb = self.time_proj(t_emb.unsqueeze(-1).unsqueeze(-1))
        x3 = x3 + t_emb
        
        x3 = self.mid_block2(x3)
        
        # Upsample
        x = self.up1(x3)
        x = torch.cat([x, x2], dim=1)
        x = self.up2(x)
        x = torch.cat([x, x1], dim=1)

        return self.conv_out(x)
    
class DiffusionModel:
    def __init__(self, timesteps=1000, beta_start=1e-4, beta_end=0.02):
        self.timesteps = timesteps
        
        self.beta = torch.linspace(beta_start, beta_end, timesteps)
        self.alpha = 1 - self.beta
        self.alpha_bar = torch.cumprod(self.alpha, dim=0)
        
        self.unet = UNet()

    def add_noise(self, x, t):
        sqrt_alpha_bar = torch.sqrt(self.alpha_bar[t])[:, None, None, None]
        sqrt_one_minus_alpha_bar = torch.sqrt(1 - self.alpha_bar[t])[:, None, None, None]
        epsilon = torch.randn_like(x)
        
        return sqrt_alpha_bar * x + sqrt_one_minus_alpha_bar * epsilon, epsilon
    
    def remove_noise(self, x, t, noise):
        alpha_t = self.alpha[t][:, None, None, None]
        alpha_bar_t = self.alpha_bar[t][:, None, None, None]
        beta_t = self.beta[t][:, None, None, None]
        
        factor = (1 - alpha_t) / torch.sqrt(1 - alpha_bar_t)
        noise_pred = self.unet(x, t.float())
        mean = (1 / torch.sqrt(alpha_t)) * (x - factor * noise_pred)
        
        if t[0] > 0:  # Check first timestep value
            variance = torch.sqrt(beta_t) * torch.randn_like(x)
            return mean + variance
        return mean
    
    def sample(self, batch_size=1, img_size=32):
        device = next(self.unet.parameters()).device
        
        # random noise
        x = torch.randn(batch_size, 3, img_size, img_size).to(device)
        
        for t in reversed(range(self.timesteps)):
            t_batch = torch.full((batch_size,), t, device=device)
            x = self.remove_noise(x, t_batch, None)
        
        return x
    
def test_diffusion_model():
    import math  
    
    model = DiffusionModel(timesteps=100)

    print("Testing noise addition")
    x = torch.randn(1, 3, 32, 32)
    t = torch.tensor([50])
    noised_x, noise = model.add_noise(x, t)
    assert noised_x.shape == x.shape, f"Expected shape {x.shape}, got {noised_x.shape}"
    print("✓ Noise addition shape test passed")

    print("\nTesting U-Net forward pass")
    noise_pred = model.unet(noised_x, t.float())
    assert noise_pred.shape == x.shape, f"Expected shape {x.shape}, got {noise_pred.shape}"
    print("✓ U-Net forward pass shape test passed")

    print("\nTesting sampling process")
    samples = model.sample(batch_size=2, img_size=32)
    assert samples.shape == (2, 3, 32, 32), f"Expected shape (2, 3, 32, 32), got {samples.shape}"
    assert torch.isfinite(samples).all(), "Generated samples contain NaN or infinite values"
    print("✓ Sampling test passed")

if __name__ == "__main__":
    test_diffusion_model()


Cite this Explanation

@article{ailinkdeeptech2026stablediffusionalgo,
  title={Stable Diffusion (DDPM): Minimal PyTorch Implementation},
  author={AILinkDeepTech},
  journal={AILinkDeepTech Algorithm Explanations},
  year={2026},
  url={https://ailinkdeeptech.com/research/stablediffusion_algo}
}

Related Explanations