An educational image convolution library demonstrating naive and optimized implementations for 2D convolution operations on RGB images. The project compares sequential and parallel approaches in Python alongside a highly-optimized GPU implementation in CUDA.
This work presents a comparative analysis showing speedup from ~5Γ for naive CPU parallelization to over 380Γ for GPU acceleration on large images.
This project implements 2D image convolution with the following approaches:
- Sequential Naive Implementation (Python): Simple triple-loop baseline for educational purposes
- CPU Parallel Naive Implementation (Python): Block-based parallelization using joblib/multiprocessing with naive convolution
- GPU Implementation (CUDA): Massively parallel SIMT execution with shared memory tiling, constant memory, and template specialization
- Naive convolution with explicit loops (educational)
- Simple block-based parallelization for CPU
- Multiple convolution kernel presets (Gaussian blur, sharpen, edge detection, emboss)
- Support for custom kernel sizes (up to 7x7 for CUDA)
- Zero-padding boundary handling
- Highly optimized GPU implementation with shared memory and constant memory
- Performance profiling capabilities
.
βββ ConvolutionScripts/
β βββ ConvSeqDumb.py # Sequential naive implementation (loops)
β βββ ConvParallelAdvancedDumb.py # Parallel CPU with naive convolution
β βββ CudaConv.cu # CUDA GPU-accelerated implementation
βββ AnalyseResults/
β βββ benchmark_results.json # Raw benchmark data
β βββ speedup_results.json # Computed speedup metrics
β βββ compute_speedup.py # Speedup computation script
β βββ generate_all_plot.py # Plotting generation script
β βββ plots/ # Generated performance plots
βββ build/ # CMake build directory (generated)
β βββ CMakeCache.txt
β βββ CMakeFiles/
β βββ CudaConv # Compiled CUDA executable
βββ doc/ # Documentation resources
βββ CMakeLists.txt # Build configuration for CUDA
βββ benchmark.sh # Automated benchmark script
βββ check_occupancy.py # CUDA occupancy analysis tool
βββ README.md # This file
Simple triple-loop implementation over image rows, columns and kernel entries. Good for understanding convolution fundamentals; not optimized for performance.
Key characteristics:
- Explicit Python loops for clarity
- Direct pixel-by-pixel computation
- Minimal memory optimization
- Educational reference implementation
Divides the image into blocks and parallelizes across processes using multiprocessing/joblib. Each worker runs a naive convolution on its assigned block.
Key characteristics:
- Block-based spatial decomposition
- Shared memory to reduce data duplication
- Parallel processing across channels and spatial regions
- Naive convolution within each block (explicit loops)
- Automatic block size calculation based on available cores
Parallelization strategy:
- Simultaneous parallelism on both color channels (R, G, B) and spatial blocks
- Each process handles one block of one channel independently
- Load balancing across available cores
Highly optimized CUDA implementation with multiple performance features.
Key optimizations:
- Pinned memory: ~2x faster host-device transfers
- Constant memory: Fast kernel coefficient access with dedicated cache
- Shared memory: Cooperative tile loading with halo to reduce global memory accesses
- Template specialization: Compile-time kernel sizes (K=3, K=5, K=7) for loop unrolling
- Buffer reuse: Single GPU allocation for all 3 RGB channels
- Optimal block configuration: Configurable tile size for performance tuning (8Γ8 to 24Γ24)
Architecture:
conv2d_kernel_optimized_K<K>: Template version with compile-time unrollingconv2d_kernel_optimized: Runtime kernel size versionconvolveCUDA_RGB_Optimized: Host function managing memory and kernel launches- Spatial tiling with halo regions for independent block computation
Processing workflow:
- RGB channels processed separately but sequentially on GPU
- Each thread computes one output pixel
- Cooperative loading of input tiles into shared memory
- Synchronization before computation to ensure data availability
Usage:
# Build
mkdir build && cd build
cmake ..
make
# Run
./CudaConv [image_path]Start the benchmark
./benchmark.sh 2>/dev/null
or
./benchmark.sh 2>/dev/null --quickConfiguration parameters (in CudaConv.cu):
K: Kernel size (3, 5, or 7 for optimized template path)tileSize: Output tile size per block (default: 16, recommended: 8-24)MAX_KERNEL_SIZE: Maximum supported kernel dimension
pip install numpy pillow joblib- CUDA Toolkit (compute capability 6.0+)
- CMake 3.20+
- OpenCV 4.x
- C++20 compatible compiler
- CPU: 13th Gen Intel Core i9-13900H (14 cores / 20 threads, 4.7 GHz base, 5.3 GHz boost)
- RAM: 32 GB DDR5-6000
- GPU: NVIDIA GeForce RTX 4060 Laptop (8 GB VRAM, CUDA 13.0)
- OS: Linux
- Python: 3.13 with NumPy 1.24.3 (Intel MKL backend)
Performance measured on various image sizes (800Γ800 to 8000Γ8000 pixels) with different kernel sizes (3Γ3, 5Γ5, 7Γ7):
| Image Size | K=3Γ3 | K=5Γ5 | K=7Γ7 |
|---|---|---|---|
| 800Γ800 | 3.82s | 3.95s | 3.83s |
| 1600Γ1600 | 15.25s | 15.25s | 15.86s |
| 3200Γ3200 | 59.69s | 60.81s | 59.99s |
| 6400Γ6400 | 234.03s | 238.79s | 244.43s |
| 8000Γ8000 | 369.30s | 371.00s | 374.04s |
| Image Size | K=3Γ3 (20 jobs) | K=5Γ5 (20 jobs) | K=7Γ7 (20 jobs) |
|---|---|---|---|
| 800Γ800 | 6.09Γ | 6.05Γ | 5.69Γ |
| 1600Γ1600 | 4.99Γ | 4.89Γ | 5.00Γ |
| 3200Γ3200 | 4.84Γ | 4.89Γ | 4.80Γ |
| 6400Γ6400 | 4.78Γ | 4.74Γ | 4.83Γ |
| 8000Γ8000 | 4.83Γ | 4.81Γ | 4.77Γ |
Key findings for CPU Parallel:
- Maximum speedup: ~6Γ with 20 processes
- Efficiency: 24-72% (decreases with more processes due to memory bandwidth saturation)
- Best for images 800Γ800 to 3200Γ3200
- Speedup plateaus beyond 12 processes
| Image Size | K=3Γ3 (tile=8/16) | K=5Γ5 (tile=8/16) | K=7Γ7 (tile=24) |
|---|---|---|---|
| 800Γ800 | 424.9Γ (9ms) | 438.4Γ (9ms) | 425.7Γ (9ms) |
| 1600Γ1600 | 390.9Γ (39ms) | 390.9Γ (39ms) | 396.4Γ (40ms) |
| 3200Γ3200 | 373.1Γ (160ms) | 377.7Γ (161ms) | 370.3Γ (162ms) |
| 6400Γ6400 | 363.4Γ (644ms) | 369.6Γ (646ms) | 376.6Γ (649ms) |
| 8000Γ8000 | 367.8Γ (1004ms) | 369.1Γ (1005ms) | 370.0Γ (1011ms) |
Key findings for CUDA:
- Consistent speedup: 350-440Γ across all configurations
- Time per MAC: ~0.1-0.6 ns (vs 7-44 ns for CPU parallel)
- GPU is 70-80Γ more efficient per operation than CPU
- Tile sizes 8Γ8 to 24Γ24 perform similarly; 32Γ32 shows slight degradation
- Optimal for all image sizes, especially 3200Γ3200 and larger
For an RGB image of size HΓW with kernel KΓK:
- Complexity: O(3 Γ H Γ W Γ KΒ²) operations
- Per pixel: ~2KΒ² FLOPS (KΒ² multiplications + KΒ² additions)
- Total FLOPS: ~3 Γ H Γ W Γ KΒ² Γ 2
- Example (8000Γ8000, K=7): ~18.6 billion FLOPS
| Implementation | Speedup Range | Time per MAC | Best Use Case |
|---|---|---|---|
| Sequential | 1Γ (baseline) | 36-44 ns (K=3) | Prototyping, small images |
| CPU Parallel | 2.5-6.1Γ | 7-44 ns | Medium images without GPU |
| CUDA GPU | 336-439Γ | 0.1-0.6 ns | All production workloads |
Memory-bound regime: All implementations are limited by memory bandwidth rather than compute capacity for Kβ€7. Larger kernels would shift toward compute-bound behavior where GPU advantage increases further.
Efficiency = Speedup / Number of Processes
| Image Size | 4 jobs | 8 jobs | 12 jobs | 16 jobs | 20 jobs |
|---|---|---|---|---|---|
| 800Γ800 | 71.6% | 56.5% | 47.5% | 36.0% | 30.4% |
| 8000Γ8000 | 63.8% | 46.1% | 37.5% | 28.9% | 24.2% |
The sub-linear scaling indicates overhead from process management, memory contention, and GIL limitations despite using multiprocessing.
Direct comparison of absolute execution times for key configurations:
| Configuration | Sequential | CPU Parallel (best) | CUDA (best) | CPU vs Seq | GPU vs Seq |
|---|---|---|---|---|---|
| 800Γ800, K=3 | 3.82s | 0.63s | 0.009s | 6.09Γ | 424.9Γ |
| 3200Γ3200, K=5 | 60.80s | 12.43s | 0.161s | 4.89Γ | 377.7Γ |
| 8000Γ8000, K=7 | 373.99s | 78.41s | 1.011s | 4.77Γ | 370.0Γ |
Normalized performance metric showing efficiency per operation:
CPU Parallel (best n_jobs):
| Image Size | K=3 | K=5 | K=7 |
|---|---|---|---|
| 800Γ800 | 36.35 ns | 13.58 ns | 7.16 ns |
| 8000Γ8000 | 44.24 ns | 16.06 ns | 8.33 ns |
CUDA GPU (best tile size):
| Image Size | K=3 | K=5 | K=7 |
|---|---|---|---|
| 800Γ800 | 0.521 ns | 0.188 ns | 0.096 ns |
| 8000Γ8000 | 0.581 ns | 0.209 ns | 0.108 ns |
Key insights:
- GPU is 70-80Γ more efficient per operation than CPU parallel
- Time per MAC decreases with kernel size (overhead amortization)
- Performance stabilizes for larger images (compute-bound regime)
- All implementations show memory-bound behavior for Kβ€7
# Sequential (naive)
python ConvolutionScripts/ConvSeqDumb.py --input [image_path] --kernel gaussian
# Parallel CPU (naive)
python ConvolutionScripts/ConvParallelAdvancedDumb.py --input [image_path] --kernel gaussian --n_jobs 4mkdir build
cd build
cmake ..
make
./CudaConv ../[image_path]Available in all implementations:
KERNEL_BLUR: 3x3 box blurKERNEL_SHARPEN: 3x3 sharpening filterKERNEL_EDGE: 3x3 edge detection (Laplacian)KERNEL_EMBOSS: 3x3 embossing effectKERNEL_GAUSSIAN: 3x3 Gaussian blurKERNEL_GAUSSIAN_5x5: 5x5 Gaussian blurKERNEL_GAUSSIAN_7x7: 7x7 Gaussian blur
All implementations use planar RGB format internally:
- Sequential/Parallel: NumPy arrays with shape
(H, W, 3) - CUDA: Separate buffers for R, G, B channels processed sequentially
All implementations use zero-padding for pixels outside image boundaries.
Standard 2D discrete convolution with kernel flipping:
out[i,j] = Ξ£ Ξ£ img[i+m, j+n] * kernel[k_h-m-1, k_w-n-1]
m n
where m β [0, k_h-1], n β [0, k_w-1]
Key functions by file:
ConvolutionScripts/ConvSeqDumb.py-convolve_channel: Naive triple-loop convolutionConvolutionScripts/ConvParallelAdvancedDumb.py-process_block_channel_shm: Shared memory block processing with naive convolutionConvolutionScripts/CudaConv.cu-conv2d_kernel_optimized_K: Template CUDA kernelConvolutionScripts/CudaConv.cu-convolveCUDA_RGB_Optimized: Main GPU convolution function
This project is provided as-is for educational and research purposes.
For a detailed technical analysis including:
- Complete mathematical formulation of the convolution operation
- In-depth discussion of optimization strategies
- Comprehensive performance analysis with statistical metrics
- Architectural considerations for CPU and GPU implementations
Please refer to doc/Report_Conv_Filt_Chiostrini_Cappetti.pdf which contains the full academic report of this work.
- The benchmark results in this README are extracted from the comprehensive study documented in the report
- All speedup measurements use the sequential naive implementation as baseline
- CPU parallel efficiency accounts for the sub-linear scaling typical of memory-bound workloads
- CUDA measurements include host-device transfer times using pinned memory
- The naive ("Dumb") Python implementations serve as educational references showing the fundamental algorithms