Day 55

GPU Kernels Learning Journey

Description

### Files: `ddpm_benchmark.py` In this experiment, I implemented and benchmarked the DDPM (Denoising Diffusion Probabilistic Model) update step using both CUDA and PyTorch. The goal was to evaluate the speed difference between a custom CUDA kernel and PyTorch's native tensor operations. CUDA Kernel Time: 0.0628 ms vs PyTorch Time: 0.6710 ms Speedup: 10.69x

Code Files

ddpm_benchmark.py

import os
import subprocess
import time
import torch

CUDA_KERNEL_SOURCE = """
#include <cuda_runtime.h>
#include <stdio.h>
#include <math.h>

__global__ void ddpm_update(float *x, float *eps, float *out, float alpha, float beta, float alpha_bar, int n) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        float inv_sqrt_alpha = 1.0f / sqrtf(alpha);
        float scale_eps = beta / sqrtf(1.0f - alpha_bar);
        out[idx] = inv_sqrt_alpha * (x[idx] - scale_eps * eps[idx]);
    }
}

int main() {
    int n = 1024 * 1024 * 3; // Simulating shape (3, 1024, 1024)
    float alpha = 0.9f, beta = 0.1f, alpha_bar = 0.5f;
    
    float *h_x = (float*)malloc(n * sizeof(float));
    float *h_eps = (float*)malloc(n * sizeof(float));
    float *h_out = (float*)malloc(n * sizeof(float));
    
    for (int i = 0; i < n; i++) {
        h_x[i] = ((float)rand() / RAND_MAX) * 2 - 1;
        h_eps[i] = ((float)rand() / RAND_MAX) * 2 - 1;
    }
    
    float *d_x, *d_eps, *d_out;
    cudaMalloc(&d_x, n * sizeof(float));
    cudaMalloc(&d_eps, n * sizeof(float));
    cudaMalloc(&d_out, n * sizeof(float));

    cudaMemcpy(d_x, h_x, n * sizeof(float), cudaMemcpyHostToDevice);
    cudaMemcpy(d_eps, h_eps, n * sizeof(float), cudaMemcpyHostToDevice);

    dim3 blockSize(1024);
    dim3 gridSize((n + blockSize.x - 1) / blockSize.x);

    cudaDeviceSynchronize();
    float ms;
    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);
    
    cudaEventRecord(start);
    for (int i = 0; i < 1000; i++) {
        ddpm_update<<<gridSize, blockSize>>>(d_x, d_eps, d_out, alpha, beta, alpha_bar, n);
    }
    cudaEventRecord(stop);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&ms, start, stop);
    
    printf("CUDA Kernel Time: %f ms\\n", ms / 1000.0);
    
    cudaMemcpy(h_out, d_out, n * sizeof(float), cudaMemcpyDeviceToHost);
    
    free(h_x); free(h_eps); free(h_out);
    cudaFree(d_x); cudaFree(d_eps); cudaFree(d_out);

    return 0;
}
"""

CUDA_FILE = "ddpm_kernel.cu"
EXECUTABLE = "./ddpm_kernel"

# Step 1: Write CUDA code to file
with open(CUDA_FILE, "w") as f:
    f.write(CUDA_KERNEL_SOURCE)

# Step 2: Compile the CUDA code
print("Compiling CUDA Kernel...")
compile_cmd = f"nvcc -o {EXECUTABLE} {CUDA_FILE}"
os.system(compile_cmd)

# Step 3: Execute CUDA binary and extract time
print("Running CUDA Kernel...")
cuda_output = subprocess.check_output([EXECUTABLE]).decode("utf-8")
cuda_time = float(cuda_output.strip().split(":")[-1].strip().split()[0])
print(f"CUDA Kernel Time: {cuda_time:.4f} ms")

# Step 4: Run PyTorch implementation
def normal_update(x: torch.Tensor, epsilon_pred: torch.Tensor, alpha: float, beta: float, alpha_bar: float):
    inv_sqrt_alpha = 1 / torch.sqrt(torch.tensor(alpha, device=x.device))
    scale_eps = beta / torch.sqrt(torch.tensor(1 - alpha_bar, device=x.device))
    return inv_sqrt_alpha * (x - scale_eps * epsilon_pred)

device = "cuda" if torch.cuda.is_available() else "cpu"
shape = (3, 1024, 1024)
x = torch.randn(shape, device=device)
eps = torch.randn(shape, device=device)
alpha = 0.9
beta = 0.1
alpha_bar = 0.5

def benchmark_pytorch(iterations=1000):
    start_time = time.time()
    for _ in range(iterations):
        _ = normal_update(x, eps, alpha, beta, alpha_bar)
    elapsed = time.time() - start_time
    return (elapsed / iterations) * 1000.0

pytorch_time = benchmark_pytorch()
print(f"PyTorch Time: {pytorch_time:.4f} ms")

# Step 5: Compare results
print(f"CUDA Kernel Time: {cuda_time:.4f} ms vs PyTorch Time: {pytorch_time:.4f} ms")
print(f"Speedup: {pytorch_time / cuda_time:.2f}x")