Day 34

GPU Kernels Learning Journey

Description

### Files: `vec_add_cpu.c`, `vec_add_gpu.cu`, `benchmark.py` **Summary:** Today I compared the performance of vector addition implemented using two different approaches: CPU with MPI and Loop Unrolling - Parallelized across multiple CPU cores using MPI. GPU with CUDA - Executed on a GPU for massive parallelism. The script runs performance tests on different input sizes and generates a log-log scale plot comparing execution times.

Code Files

benchmark.py

import subprocess
import numpy as np
import matplotlib.pyplot as plt

# Define test sizes
sizes = [10**i for i in range(1, 10)]  # 10, 100, 1000, ..., 1 billion
cpu_times = []
gpu_times = []

# Compile CPU and GPU programs
subprocess.run("mpicc -o vec_add_cpu vec_add_cpu.c -O3", shell=True, check=True)
subprocess.run("nvcc -o vec_add_gpu vec_add_gpu.cu -O3", shell=True, check=True)

for size in sizes:
    print(f"Running for size: {size}")
    
    # Run MPI CPU version
    result_cpu = subprocess.run(
        f"mpirun --oversubscribe --allow-run-as-root -np 4 ./vec_add_cpu {size}",
        shell=True, capture_output=True, text=True
    )
    
    # Debugging output
    print("CPU Output:", result_cpu.stdout)
    print("Error (if any):", result_cpu.stderr)
    
    output = result_cpu.stdout.strip().split()
    if len(output) >= 2:
        cpu_time = float(output[-2])  # Extract time if available
    else:
        print("Error: Unexpected CPU output format ->", result_cpu.stdout)
        cpu_time = float('inf')  # Assign a large value to indicate failure
    cpu_times.append(cpu_time)
    
    # Run CUDA GPU version
    result_gpu = subprocess.run(
        f"./vec_add_gpu {size}", shell=True, capture_output=True, text=True
    )
    
    # Debugging output
    print("GPU Output:", result_gpu.stdout)
    print("Error (if any):", result_gpu.stderr)
    
    output = result_gpu.stdout.strip().split()
    if len(output) >= 2:
        gpu_time = float(output[-2])  # Extract time if available
    else:
        print("Error: Unexpected GPU output format ->", result_gpu.stdout)
        gpu_time = float('inf')  # Assign a large value to indicate failure
    gpu_times.append(gpu_time)

# Plot results
plt.figure(figsize=(10, 6))
plt.plot(sizes, cpu_times, marker='o', label='CPU (MPI + Unrolling)')
plt.plot(sizes, gpu_times, marker='s', label='GPU (CUDA)')
plt.xscale('log')
plt.yscale('log')
plt.xlabel("Input Size (log scale)")
plt.ylabel("Execution Time (ms, log scale)")
plt.legend()
plt.grid()
plt.title("Performance Comparison: CPU vs. GPU for Vector Addition")
plt.show()

vec_add_gpu.cu

#include <stdio.h>
#include <cuda.h>

#define UNROLL_FACTOR 4

__global__ void vector_add(double *A, double *B, double *C, int N) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    int limit = N - (N % UNROLL_FACTOR);

    for (; i < limit; i += blockDim.x * gridDim.x) {
        C[i] = A[i] + B[i];
        C[i+1] = A[i+1] + B[i+1];
        C[i+2] = A[i+2] + B[i+2];
        C[i+3] = A[i+3] + B[i+3];
    }

    for (; i < N; i++) {
        C[i] = A[i] + B[i];
    }
}

int main(int argc, char **argv) {
    int N = atoi(argv[1]);
    double *h_A, *h_B, *h_C;
    double *d_A, *d_B, *d_C;

    size_t size = N * sizeof(double);
    h_A = (double*) malloc(size);
    h_B = (double*) malloc(size);
    h_C = (double*) malloc(size);

    for (int i = 0; i < N; i++) {
        h_A[i] = i;
        h_B[i] = i * 2;
    }

    cudaMalloc((void**)&d_A, size);
    cudaMalloc((void**)&d_B, size);
    cudaMalloc((void**)&d_C, size);

    cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice);

    int threadsPerBlock = 256;
    int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;

    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);
    cudaEventRecord(start);

    vector_add<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N);
    
    cudaEventRecord(stop);
    cudaEventSynchronize(stop);

    cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);

    float elapsedTime;
    cudaEventElapsedTime(&elapsedTime, start, stop);
    printf("GPU (CUDA): %f ms\n", elapsedTime);

    cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
    free(h_A); free(h_B); free(h_C);

    return 0;
}

vec_add_cpu.c

#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>
#include <time.h>

#define UNROLL_FACTOR 4  // Loop unrolling factor

void vector_add(double *A, double *B, double *C, int size) {
    int i, limit = size - (size % UNROLL_FACTOR);
    for (i = 0; i < limit; i += UNROLL_FACTOR) {
        C[i]   = A[i]   + B[i];
        C[i+1] = A[i+1] + B[i+1];
        C[i+2] = A[i+2] + B[i+2];
        C[i+3] = A[i+3] + B[i+3];
    }
    for (; i < size; i++) {
        C[i] = A[i] + B[i];
    }
}

int main(int argc, char **argv) {
    int rank, size, N = atoi(argv[1]);  // Take vector size as argument
    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &size);

    int chunk_size = N / size;
    int start = rank * chunk_size;
    int end = (rank == size - 1) ? N : start + chunk_size;

    double *A = (double*) malloc(chunk_size * sizeof(double));
    double *B = (double*) malloc(chunk_size * sizeof(double));
    double *C = (double*) malloc(chunk_size * sizeof(double));

    for (int i = 0; i < chunk_size; i++) {
        A[i] = i + rank;
        B[i] = i - rank;
    }

    double t1 = MPI_Wtime();
    vector_add(A, B, C, chunk_size);
    double t2 = MPI_Wtime();

    if (rank == 0) {
        printf("CPU (MPI + Unrolling): %lf miliseconds\n", (t2 - t1)*1000.0);
    }

    free(A); free(B); free(C);
    MPI_Finalize();
    return 0;
}