Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 62

Benchmarking GEMM Optimizations Against cuBLAS

Recommended resources

Introduction

Over the last several lessons, we have systematically constructed a high-performance General Matrix Multiplication (GEMM) kernel. Starting from a naive implementation, you've applied a series of critical optimizations: coalescing global memory accesses, tiling with shared memory, maximizing data reuse through register blocking, vectorizing memory loads, and finally, adding boundary checks to create a robust, general-purpose kernel.

This lesson is the culmination of that effort. We will now turn from implementation to evaluation, which is a core discipline in high-performance computing. Our goal is to rigorously benchmark each stage of our kernel's development against NVIDIA's cuBLAS library. This process will not only quantify the impact of each optimization but also illuminate the remaining gap to a production-grade library, revealing the advanced techniques that define the state of the art in GPU kernel engineering.


The Gold Standard: NVIDIA cuBLAS

Before we measure our own work, we must define our yardstick. The NVIDIA CUDA Basic Linear Algebra Subprograms (cuBLAS) library is the industry standard for dense linear algebra on GPUs. It is not a single GEMM kernel but a vast collection of hundreds of specialized kernels. At runtime, when you call cublasSgemm, a sophisticated heuristic engine selects the optimal kernel based on your GPU architecture, matrix dimensions (M, N, K), data types, and other parameters.

A conceptual illustration of the auto-tuning process used by libraries like cuBLAS. For a given problem, the library can retrieve a set of candidate algorithms (kernels) and empirically measure their performance to select the fastest one.

This expert system is the result of thousands of engineering hours and is precisely tuned for each generation of hardware. Our goal is not necessarily to beat cuBLAS—a monumental task—but to understand the principles that make it so fast and to see how close we can get with our own hand-rolled optimizations.


The Performance Ladder: Quantifying Our Optimizations

To measure performance, we use the metric of GFLOP/s (Giga Floating-Point Operations Per Second). For a GEMM operation , the total number of floating-point operations is approximately (one multiplication and one addition for each element of the inner product). The performance is then:

Let's analyze the performance jump we achieve at each optimization stage. The blog post "How to Optimize a CUDA Matmul Kernel for cuBLAS-like Performance" provides an excellent worklog that mirrors our own journey. We will use its findings to structure our analysis.

How to Optimize a CUDA Matmul Kernel for cuBLAS-like ...

This article provides a step-by-step log of optimizing a CUDA GEMM kernel, complete with performance benchmarks for each version against cuBLAS. It serves as a perfect case study for our current learning objective.

Please read the introduction and the sections for "Kernel 1: Naive" through "Kernel 6: Vectorize SMEM and GMEM Accesses". Focus on the performance table in the introduction, which we will analyze in detail. As you read, connect each kernel version to the optimizations we've implemented in our course.

Based on the reading, here is a summary of the performance progression for a 4096x4096 matrix multiplication, which we can treat as our "performance ladder":

Kernel Optimization Stage GFLOP/s Performance relative to cuBLAS
1: Naive Implementation 309.0 1.3%
2: Global Memory Coalescing 1986.5 8.5%
3: Shared Memory Tiling 2980.3 12.8%
4: 1D Register Tiling 8474.7 36.5%
5: 2D Register Tiling 15971.7 68.7%
6: Vectorized Memory Access 18237.3 78.4%
Reference: cuBLAS (FP32) 23249.6 100.0%

Let's break down these performance jumps:

  1. Naive to Coalescing (1.3% -> 8.5%): This is a staggering ~6.4x speedup. It underscores the single most important rule of global memory access: ensure threads in a warp access contiguous memory locations. Failing to do so thrashes the memory bus with unoptimized, small transactions, destroying performance.
  2. Coalescing to Shared Memory (8.5% -> 12.8%): A respectable ~1.5x speedup. By explicitly caching tiles in the fast, on-chip shared memory, we drastically reduce the number of expensive trips to global memory, increasing our arithmetic intensity.
  3. Shared Memory to Register Tiling (12.8% -> 68.7%): This is where we see another massive leap. The 1D tiling gives a ~2.8x speedup, and the 2D tiling provides another ~1.9x on top of that. This highlights the paramount importance of data reuse within the registers—the fastest memory available to a thread. By having each thread compute a 2D patch of the output, we maximize the work done for each value loaded from shared memory.
  4. Register Tiling to Vectorization (68.7% -> 78.4%): A solid ~1.14x speedup. At this stage, we are nearing the limits of the hardware. Vectorized loads (e.g., using float4) allow us to fetch more data per instruction, reducing instruction overhead and more effectively saturating memory bandwidth.

The following video provides a great visual recap of this entire optimization journey, from the "embarrassing" performance of a naive kernel to one that approaches the speed of cuBLAS.

Mini Project: How to program a GPU? | CUDA C/C++

This video by Tushar Gautam provides a practical demonstration of iteratively optimizing a GEMM kernel, mirroring the steps we've taken.

Watch the video from the initial benchmark at the start through the successive optimizations. Pay attention to the performance graph shown at the end (final results), which visualizes the impact of coalescing, shared memory, register reuse, and vectorization.


Analyzing the Gap: What Makes cuBLAS Faster?

Even after all our hard work, there's still a ~22% performance gap to cuBLAS. This gap isn't due to a single missing trick, but rather a collection of more advanced techniques and a level of hardware-specific tuning that goes beyond a general-purpose approach.

1. Autotuning

Our kernel uses fixed, compile-time constants for tile sizes (BM, BN, BK) and thread block geometry. The optimal values for these parameters depend heavily on the specific GPU, matrix dimensions, and even the data type. High-performance libraries like cuBLAS don't guess; they autotune. They either run a search at installation time or use a pre-computed table to find the best configuration for a given problem.

How to Optimize a CUDA Matmul Kernel for cuBLAS-like ...

This section explains the concept and impact of autotuning kernel parameters.

Please read the section titled Kernel 9: Autotuning. Notice that simply finding a better set of template parameters through benchmarking can yield a significant performance boost (5-6% in this case).

2. Multiple Kernel Specializations

cuBLAS is not one kernel; it's an army of them. For small matrices, the overhead of our tiling strategy might make it slower than a simpler kernel. For certain shapes, a "split-K" strategy (where the K dimension is split across thread blocks, requiring a final reduction step) can be more effective.

The warptiling article demonstrates this by profiling cuBLAS calls for different matrix sizes and discovering that different underlying kernels are dispatched.

How to Optimize a CUDA Matmul Kernel for cuBLAS-like ...

This section introduces a more advanced tiling strategy and, more importantly, analyzes how cuBLAS uses different kernels for different matrix sizes.

Read the section on Warptiling. The key takeaway here is in the table that shows which sgemm kernel NVIDIA's profiler reports for matrix sizes from 128 to 4096. This reveals the library's "many-kernels" strategy.

3. Advanced Tiling and Scheduling (Warp Tiling)

Our tiling strategy operates at two levels: block-level (SMEM) and thread-level (registers). Production kernels often introduce an intermediate level: warp-level tiling. Here, a block's work is subdivided among its warps. This aligns the software design more closely with the hardware's fundamental scheduling unit, which can improve shared memory access patterns and register cache locality.

4. Specialized Hardware: Tensor Cores

The biggest leap in modern GPU performance for AI comes from Tensor Cores. These are highly specialized processing units, first introduced in the Volta architecture, that can execute an entire matrix-multiply-accumulate (MMA) operation in a single instruction. Our kernels have used standard CUDA cores, which perform scalar FMA instructions.

For FP32 operations, the difference is significant. For mixed-precision (e.g., multiplying BF16 inputs to produce an FP32 result), the difference is astronomical. This is the primary reason why cuBLAS performance on modern GPUs is so far ahead for mixed-precision workloads.

The following resource provides an excellent deep-dive into a GEMM implementation on the H100 GPU, focusing on the use of Tensor Cores via the Tensor Memory Accelerator (TMA) and Warp Group Matrix Multiply and Accumulate (WGMMA) instructions.

Worklog: Optimising GEMM on NVIDIA H100 for cuBLAS-like ...

This section from a blog post on H100 optimization details how to program Tensor Cores, the specialized hardware that provides massive speedups for matrix multiplication.

Please read the section titled Kernel 7: Tensor Cores. You don't need to follow every line of PTX assembly, but focus on the core concepts: Tensor Cores execute a full matrix operation (D = A @ B + C) in a single instruction (CISC-like). The TMA is a dedicated engine for asynchronously moving tiles from global to shared memory. The WGMMA instruction performs the MMA operation on tiles prepared by the TMA. Notice the performance jump: a 9x improvement over the previous mixed-precision kernel, reaching 37.8% of the Tensor Core-enabled cuBLAS. This demonstrates the sheer power of this specialized hardware.


Visualizing Performance with the Roofline Model

The Roofline model provides a powerful way to visualize our optimization journey. It plots a kernel's performance (GFLOP/s) against its arithmetic intensity (FLOPs/Byte).

An example of a GPU Roofline plot. The y-axis is performance (% of peak), and the x-axis is arithmetic intensity. The dashed line represents the theoretical performance limit, determined by memory bandwidth (the slanted part) and peak compute (the flat part).

Our kernels progress across this chart as follows:

  • Kernel 1 (Naive): Low arithmetic intensity. It's stuck in the memory-bound region, far below the performance ceiling.
  • Kernels 2-5 (Tiling): Each tiling optimization (SMEM, registers) dramatically increases arithmetic intensity by reducing memory traffic (the denominator of FLOPs/Byte). This moves our kernel's point to the right on the plot, allowing it to climb higher up the roofline.
  • Final Kernel: Our most optimized version sits much further to the right. It is likely still limited by a combination of memory bandwidth and compute, but it is much closer to the "corner" of the roofline, which represents the point of optimal balance.
  • cuBLAS: A highly tuned cuBLAS kernel would sit right at the apex of the roofline, achieving performance very close to the hardware's theoretical peak compute.

Conclusion

You have now completed the full lifecycle of kernel optimization for one of the most fundamental operations in computing. By benchmarking your work, you've seen firsthand how systematic optimization translates into concrete performance gains and have gained a deep appreciation for the sophisticated engineering that goes into a production library like cuBLAS.

Key Takeaways:

  • Benchmarking is essential: It provides the quantitative feedback needed to guide the optimization process.
  • The path to performance is layered: Coalescing, shared memory tiling, and register tiling are the foundational steps that deliver the largest gains by increasing arithmetic intensity.
  • Production libraries are highly specialized: The final performance gap is closed by techniques like autotuning, using multiple kernel implementations for different problem sizes, and exploiting specialized hardware like Tensor Cores.
  • The Roofline model visualizes optimization: It clearly shows how increasing arithmetic intensity allows a kernel to move from being memory-bound towards being compute-bound, unlocking higher performance.

In our next module, we will leverage this deep understanding of GEMM and GPU architecture. You will learn to apply these optimization principles to other critical AI kernels, such as Layer Normalization and Softmax, and explore powerful techniques like kernel fusion.