Day 49

GPU Kernels Learning Journey

Description

### Files: `cgm.cu` I implemented a CUDA-based Conjugate Gradient Method (CGM) solver to efficiently solve linear systems Ax=b by iteratively updating search directions without computing Hessians, leveraging GPU parallelism. The implementation includes custom CUDA kernels for matrix–vector multiplication, vector arithmetic (addition, subtraction, scaling), and a shared memory reduction for dot products, ensuring high-performance execution. This approach optimizes memory access and computation, making it suitable for large-scale problems in machine learning and scientific computing while demonstrating how iterative solvers can benefit from GPU acceleration.

Code Files

CGM.cu

#include <iostream>
#include <cmath>
#include <cuda_runtime.h>

// Matrix size (for demonstration purposes)
#define N 4

// CUDA error-checking macro
#define CUDA_CHECK(call) \
    do { \
        cudaError_t err = call; \
        if(err != cudaSuccess) { \
            std::cerr << "CUDA error in " << __FILE__ << "@" << __LINE__ << ": " \
                      << cudaGetErrorString(err) << std::endl; \
            exit(EXIT_FAILURE); \
        } \
    } while(0)

// Kernel: Matrix-Vector multiplication: y = A*x
__global__ void matVecMul(const float *A, const float *x, float *y, int n) {
    int row = blockIdx.x * blockDim.x + threadIdx.x;
    if (row < n) {
        float sum = 0.0f;
        for (int j = 0; j < n; j++) {
            sum += A[row * n + j] * x[j];
        }
        y[row] = sum;
    }
}

// Kernel: Vector addition: y = y + alpha * x
__global__ void vecAdd(float *y, const float *x, float alpha, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        y[i] += alpha * x[i];
    }
}

// Kernel: Vector subtraction: y = y - alpha * x
__global__ void vecSub(float *y, const float *x, float alpha, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        y[i] -= alpha * x[i];
    }
}

// Kernel: Scale vector: x = beta * x
__global__ void vecScale(float *x, float beta, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        x[i] *= beta;
    }
}

// Kernel: Dot product using shared memory reduction
__global__ void dotProduct(const float *a, const float *b, float *result, int n) {
    __shared__ float cache[256];
    int tid = threadIdx.x;
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    float temp = 0.0f;
    while (i < n) {
        temp += a[i] * b[i];
        i += blockDim.x * gridDim.x;
    }
    cache[tid] = temp;
    __syncthreads();

    // Reduction in shared memory
    int blockSize = blockDim.x;
    while (blockSize > 1) {
        int halfPoint = blockSize / 2;
        if (tid < halfPoint) {
            cache[tid] += cache[tid + halfPoint];
        }
        __syncthreads();
        blockSize = halfPoint;
    }
    if (tid == 0) {
        atomicAdd(result, cache[0]);
    }
}

int main() {
    const int n = N;
    const int matrixSize = n * n * sizeof(float);
    const int vectorSize = n * sizeof(float);

    // Host data: Define a symmetric positive-definite matrix A and vector b.
    float h_A[N * N] = {
         4, 1, 0, 0,
         1, 3, 1, 0,
         0, 1, 2, 1,
         0, 0, 1, 1
    };
    float h_b[N] = {15, 10, 10, 10};
    float h_x[N] = {0}; // Initial guess x = 0

    // Device allocations
    float *d_A, *d_x, *d_b, *d_r, *d_p, *d_Ap;
    CUDA_CHECK(cudaMalloc(&d_A, matrixSize));
    CUDA_CHECK(cudaMalloc(&d_x, vectorSize));
    CUDA_CHECK(cudaMalloc(&d_b, vectorSize));
    CUDA_CHECK(cudaMalloc(&d_r, vectorSize));
    CUDA_CHECK(cudaMalloc(&d_p, vectorSize));
    CUDA_CHECK(cudaMalloc(&d_Ap, vectorSize));

    // Copy data to device
    CUDA_CHECK(cudaMemcpy(d_A, h_A, matrixSize, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_b, h_b, vectorSize, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_x, h_x, vectorSize, cudaMemcpyHostToDevice));

    // Initialize: r = b - A*x (x=0 => r=b), p = r
    CUDA_CHECK(cudaMemcpy(d_r, d_b, vectorSize, cudaMemcpyDeviceToDevice));
    CUDA_CHECK(cudaMemcpy(d_p, d_r, vectorSize, cudaMemcpyDeviceToDevice));

    // Allocate device memory for dot product result
    float *d_dot;
    CUDA_CHECK(cudaMalloc(&d_dot, sizeof(float)));

    float rdotr = 0.0f, new_rdotr = 0.0f;

    // Compute initial dot product: rdotr = r^T r
    CUDA_CHECK(cudaMemset(d_dot, 0, sizeof(float)));
    dotProduct<<<1, 256>>>(d_r, d_r, d_dot, n);
    CUDA_CHECK(cudaMemcpy(&rdotr, d_dot, sizeof(float), cudaMemcpyDeviceToHost));

    int max_iter = 1000;
    float tol = 1e-6f;
    int k = 0;

    while (sqrt(rdotr) > tol && k < max_iter) {
        // Ap = A * p
        matVecMul<<<(n + 255) / 256, 256>>>(d_A, d_p, d_Ap, n);

        // Compute p^T * A * p
        CUDA_CHECK(cudaMemset(d_dot, 0, sizeof(float)));
        dotProduct<<<1, 256>>>(d_p, d_Ap, d_dot, n);
        float pAp = 0.0f;
        CUDA_CHECK(cudaMemcpy(&pAp, d_dot, sizeof(float), cudaMemcpyDeviceToHost));

        float alpha = rdotr / pAp;

        // x = x + alpha * p
        vecAdd<<<(n + 255) / 256, 256>>>(d_x, d_p, alpha, n);

        // r = r - alpha * Ap
        vecSub<<<(n + 255) / 256, 256>>>(d_r, d_Ap, alpha, n);

        // Compute new dot product: new_rdotr = r^T * r
        CUDA_CHECK(cudaMemset(d_dot, 0, sizeof(float)));
        dotProduct<<<1, 256>>>(d_r, d_r, d_dot, n);
        CUDA_CHECK(cudaMemcpy(&new_rdotr, d_dot, sizeof(float), cudaMemcpyDeviceToHost));

        if (sqrt(new_rdotr) < tol) {
            break;
        }

        float beta = new_rdotr / rdotr;

        // p = r + beta * p
        // First scale p: p = beta * p
        vecScale<<<(n + 255) / 256, 256>>>(d_p, beta, n);
        // Then add r: p = p + r
        vecAdd<<<(n + 255) / 256, 256>>>(d_p, d_r, 1.0f, n);

        rdotr = new_rdotr;
        k++;
    }

    // Copy the solution back to host
    CUDA_CHECK(cudaMemcpy(h_x, d_x, vectorSize, cudaMemcpyDeviceToHost));
    std::cout << "Conjugate Gradient converged in " << k << " iterations." << std::endl;
    std::cout << "Solution x:" << std::endl;
    for (int i = 0; i < n; i++) {
        std::cout << h_x[i] << std::endl;
    }

    // Cleanup
    CUDA_CHECK(cudaFree(d_A));
    CUDA_CHECK(cudaFree(d_x));
    CUDA_CHECK(cudaFree(d_b));
    CUDA_CHECK(cudaFree(d_r));
    CUDA_CHECK(cudaFree(d_p));
    CUDA_CHECK(cudaFree(d_Ap));
    CUDA_CHECK(cudaFree(d_dot));

    return 0;
}