Day 63

GPU Kernels Learning Journey

Description

### Files: `group_norm.cu` I tackled the implementation of a Group Normalization forward pass using CUDA.

Code Files

group_norm.cu

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

#define CUDA_CHECK(err) \
    if(err != cudaSuccess) {\
        std::cerr << "CUDA error: " << cudaGetErrorString(err) \
                  << " at " << __FILE__ << ":" << __LINE__ << std::endl;\
        exit(EXIT_FAILURE);\
    }

// Kernel that performs group normalization forward pass using parallel reduction.
__global__ void groupNormForward(const float *x, float *y, 
                                 const float *gamma, const float *beta,
                                 int N, int C, int H, int W, int G, float eps) {
    // Each block handles one sample (n) and one group (g)
    int n = blockIdx.x;
    int g = blockIdx.y;
    
    int group_channels = C / G;
    int spatial_size = H * W;
    int group_size = group_channels * spatial_size;
    int start_c = g * group_channels;
    
    // Use threadIdx.x to index within the group.
    int tid = threadIdx.x;
    
    // Each thread may process multiple elements.
    float local_sum = 0.0f;
    float local_sum_sq = 0.0f;
    for (int i = tid; i < group_size; i += blockDim.x) {
        int c_offset = i / spatial_size;   // index within the group channels
        int s = i % spatial_size;            // spatial index
        int channel = start_c + c_offset;
        int idx = n * C * spatial_size + channel * spatial_size + s;
        float val = x[idx];
        local_sum += val;
        local_sum_sq += val * val;
    }
    
    // Allocate shared memory for reduction (two arrays in one block).
    extern __shared__ float shared[];
    float* s_sum = shared;              // size: blockDim.x
    float* s_sum_sq = &shared[blockDim.x]; // size: blockDim.x

    s_sum[tid] = local_sum;
    s_sum_sq[tid] = local_sum_sq;
    __syncthreads();
    
    // Parallel reduction for sum and sum of squares.
    for (int stride = blockDim.x / 2; stride > 0; stride /= 2) {
        if (tid < stride) {
            s_sum[tid] += s_sum[tid + stride];
            s_sum_sq[tid] += s_sum_sq[tid + stride];
        }
        __syncthreads();
    }
    
    // Compute mean and variance (only thread 0 does it, then broadcast).
    float mean = s_sum[0] / group_size;
    float var = s_sum_sq[0] / group_size - mean * mean;
    float inv_std = rsqrtf(var + eps);
    __syncthreads(); // ensure all threads see the computed mean and var

    // Normalize each element in the group.
    for (int i = tid; i < group_size; i += blockDim.x) {
        int c_offset = i / spatial_size;
        int s = i % spatial_size;
        int channel = start_c + c_offset;
        int idx = n * C * spatial_size + channel * spatial_size + s;
        float val = x[idx];
        float norm = (val - mean) * inv_std;
        y[idx] = gamma[channel] * norm + beta[channel];
    }
}

int main() {
    // Tensor dimensions.
    int N = 1;   // Batch size.
    int C = 4;   // Number of channels.
    int H = 2;   // Height.
    int W = 2;   // Width.
    int G = 2;   // Number of groups (C must be divisible by G).
    float eps = 1e-5;
    int tensor_size = N * C * H * W;
    
    // Host memory allocation.
    float h_x[tensor_size];
    float h_y[tensor_size];
    float h_gamma[C];
    float h_beta[C];
    
    // Initialize input tensor and parameters.
    for (int i = 0; i < tensor_size; i++) {
        h_x[i] = static_cast<float>(i);
    }
    for (int i = 0; i < C; i++) {
        h_gamma[i] = 1.0f;  // scaling factors.
        h_beta[i] = 0.0f;   // shifting factors.
    }
    
    // Device memory allocation.
    float *d_x, *d_y, *d_gamma, *d_beta;
    CUDA_CHECK(cudaMalloc(&d_x, tensor_size * sizeof(float)));
    CUDA_CHECK(cudaMalloc(&d_y, tensor_size * sizeof(float)));
    CUDA_CHECK(cudaMalloc(&d_gamma, C * sizeof(float)));
    CUDA_CHECK(cudaMalloc(&d_beta, C * sizeof(float)));
    
    // Copy data to device.
    CUDA_CHECK(cudaMemcpy(d_x, h_x, tensor_size * sizeof(float), cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_gamma, h_gamma, C * sizeof(float), cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_beta, h_beta, C * sizeof(float), cudaMemcpyHostToDevice));
    
    // Set grid and block dimensions.
    int group_channels = C / G;
    int spatial_size = H * W;
    int group_size = group_channels * spatial_size;
    int threadsPerBlock = (group_size < 128) ? group_size : 128;
    // Shared memory size: two arrays of threadsPerBlock floats.
    size_t sharedMemSize = threadsPerBlock * 2 * sizeof(float);
    dim3 grid(N, G);
    
    // Launch the kernel.
    groupNormForward<<<grid, threadsPerBlock, sharedMemSize>>>(d_x, d_y, d_gamma, d_beta,
                                                                N, C, H, W, G, eps);
    CUDA_CHECK(cudaDeviceSynchronize());
    
    // Copy results back to host.
    CUDA_CHECK(cudaMemcpy(h_y, d_y, tensor_size * sizeof(float), cudaMemcpyDeviceToHost));
    
    // Print the normalized output.
    std::cout << "Group Norm Forward Output:" << std::endl;
    for (int n = 0; n < N; n++) {
        for (int c = 0; c < C; c++) {
            for (int h = 0; h < H; h++) {
                for (int w = 0; w < W; w++) {
                    int idx = n * C * H * W + c * H * W + h * W + w;
                    std::cout << h_y[idx] << " ";
                }
                std::cout << std::endl;
            }
            std::cout << std::endl;
        }
    }
    
    // Free device memory.
    CUDA_CHECK(cudaFree(d_x));
    CUDA_CHECK(cudaFree(d_y));
    CUDA_CHECK(cudaFree(d_gamma));
    CUDA_CHECK(cudaFree(d_beta));
    
    return 0;
}