Back to all writing
GPU Compute
·5 min read

Writing Custom CUDA Kernels for Real-Time Parallel Video Filters

Deep dive into 2D shared memory tiling, thread divergence mitigation, and CUDA stream pipelining for high-throughput video processing.

CUDAC++GPUParallel Computing

Video processing is an inherently parallel domain: every frame comprises millions of independent pixels, and most spatial operations (such as Sobel edge detection, Gaussian blurs, and bilateral smoothing) exhibit strong 2D spatial locality.

However, naive CUDA implementations that read pixel neighborhoods directly from device global memory quickly saturate memory bandwidth, resulting in poor hardware utilization.

Shared Memory Tiling & Halo Regions

To maximize memory efficiency, our capstone processor partitions each frame into 2D tiles (e.g., 16×16 pixels) mapped to CUDA thread blocks.

Because filter kernels (like a 5×5 convolution) require pixels outside the primary tile—the **halo region**—we allocate shared memory with an apron:

__shared__ float s_Tile[TILE_H + 2 * RADIUS][TILE_W + 2 * RADIUS];

Each thread loads its primary pixel into shared memory, while edge threads cooperatively fetch halo boundary pixels. Once a `__syncthreads()` barrier completes, all filter computations operate purely on on-chip SRAM at negligible latency.

Overlapping Memory Transfers with Streams

PCIe bus transfer between host and device is the primary bottleneck for continuous video feeds. By allocating **page-locked (pinned) host memory** and multiplexing across multiple `cudaStream_t` queues, we pipeline:

  • Transferring Frame $N+1$ from Host to Device
  • Executing Convolution Kernel on Frame $N$
  • Transferring Frame $N-1$ from Device to Host

This concurrent pipeline sustains uninterrupted 60+ FPS processing on high-resolution streams.