CS 259 · Learning Machines · MQST Spring 2023

CUDA Convolutional Neural Network Kernels

Hand-written CUDA kernels for a VGG-style CNN's convolution and fully-connected layers — profiled on an NVIDIA Titan V, benchmarked against CuDNN, and analyzed with a mechanistic roofline performance model.

NVIDIA Titan V · Volta · sm_70 Block-strided + register tiling ReLU activation Roofline & CuDNN comparison

Overview

Four kernels implemented from scratch in a single CUDA translation unit.

Each kernel launches with a block-strided pattern — threads stride across the problem in blockDim.x * gridDim.x steps so any problem size maps onto a fixed grid. Data reuse is squeezed out with register tiling (Tn, Ti, Tx, Ty) sized to keep working sets inside the L1/L2 caches, and every output passes through a ReLU. The two convolution kernels differ in which axis they stride over — the key idea behind their very different roofline placement.

The kernels

Two convolutional layers and two fully-connected classifiers.

Conv1 3×3, 224², 64→64

Strides over the spatial (y, x) dimensions. Large feature maps, modest channel count. Non-contiguous access hurts reuse → low arithmetic intensity, memory-bound.

Conv2 3×3, 14², 512→512

Strides over the channel (n, i) dimensions instead. With many feature maps this yields far better reuse → much higher arithmetic intensity on the roofline.

Classifier1 25088 → 4096

Fully-connected layer as a tiled matrix–vector product with ReLU. Threads strided over the output dimension, input tiled by Ti.

Classifier2 4096 → 1024

The smaller fully-connected layer, same structure — the cheapest of the four kernels to evaluate.

Results

Batch size 1 on the NVIDIA Titan V — our naive kernels vs. CuDNN.

Execution time and DRAM read throughput. CuDNN's Conv2 uses Winograd, for which a direct throughput comparison isn't meaningful.
KernelOurs (ms)Ours BW (GB/s)CuDNN (ms)CuDNN BW (GB/s)Slowdown
Conv1121.3452.340.342406.34~355×
Conv243.6770.0970.186N/A~235×
Classifier1134.513.430.746542.32~180×
Classifier217.2680.770.040416.23~430×

What the numbers say

  • All four kernels are memory-bandwidth bound — they sit well below both roofline ceilings, with too little data reuse (no shared-memory scratchpad).
  • We trail CuDNN by roughly 100×: it uses the Winograd algorithm on tensor-core matrix multiply, which a naive direct convolution can't match.
  • The large block size left only 49 of 80 SMs worth of blocks occupied, leaving performance on the table.

Roofline model

Where each kernel lands relative to the Titan V's DRAM and L2 ceilings.

Log-log roofline plot for the Titan V. DRAM and L2 ceilings rise to a 14.9 TFLOP/s FP32 peak; the four kernel operating points sit far below the ceilings, indicating a memory-bound implementation.
FP32 peak ≈ 14.9 TFLOP/s · DRAM BW 651.3 GB/s · L2 BW 2155 GB/s. Regenerate with python perf/roofline.py.

Mechanistic performance model

A separate model (Mini-Project 2) predicts operational intensity from the tiling parameters at each level of the memory hierarchy. Sizing tiles so the working set fits the L2 (4.5 MB) and L1 (65 KB/SM) caches gives target intensities of 14.05 FLOP/byte for Conv1 (L1 set) and 34.37 FLOP/byte for Conv2 (L2 set).

Build & run

Needs the CUDA Toolkit (nvcc). Makefile defaults to SMS=70.

cd src
make                             # builds ./convolution
./convolution -k Conv1 -b 16     # kernel + batch count

# kernels: Conv1 | Conv2 | Classifier1 | Classifier2
make profilePerformance          # nvprof timing
make profileMemory               # nvprof -m dram_read_throughput