Advanced Matrix Multiplication Optimization on Nvidia GPUs A new open-source CUDA project, sgemm.cu, provides an educational FP32 matrix multiplication (SGEMM) implementation that the author says was fine-tuned for and tested on an NVIDIA RTX 3090 (GA102 chip) and is expected to perform well on Ada, Ampere, Volta and Turing GPUs. The author states that existing educational CUDA matmul tutorials achieve only 50-70% of cuBLAS performance at best, and that the project's goal is to bridge the gap between book and blog SGEMM explanations and NVIDIA's BLAS libraries rather than to beat cuBLAS, whose algorithm design the author credits to NVIDIA engineers and prior cuBLAS and CUTLASS work. The implementation uses inlined PTX, asynchronous memory copies, double-buffering, avoidance of shared memory bank conflicts and coalesced storage through shared memory, and is benchmarked against cuBLAS and Simon Boehm's tinyBLAS. Advanced Matrix Multiplication Optimization on NVIDIA GPUs This project is inspired by the outstanding works of Andrej Karpathy, George Hotz, Scott Gray, Horace He, Philippe Tillet, Jeremy Howard, Lei Mao and the best CUDA hackers from the GPU MODE https://github.com/gpu-mode community Discord server https://discord.gg/gpumode . A special thanks to Mark Saroufim and Andreas Köpf for running GPU MODE and all you’ve done for the community. The code is available at sgemm.cu https://github.com/salykova/sgemm.cu . This article complements my blog post https://salykova.github.io/gemm-cpu , which covers the implementation of FP32 matrix multiplication that outperforms BLAS libraries on modern Intel and AMD CPUs. Today we’ll walk through a GPU implementation of SGEMM Single-precision GEneral Matrix Multiply operation defined as C := alpha A B + beta C . The blog delves into benchmarking code on CUDA devices and explains the algorithm’s design along with optimization techniques. These include inlined PTX, asynchronous memory copies, double-buffering, avoiding shared memory bank conflicts, and efficient coalesced storage through shared memory. I’d also like to mention that the high-level algorithm design used in this project was developed by the excellent engineers at NVIDIA and has been extensively studied in prior works on cuBLAS and CUTLASS. My main contribution was translating it into efficient CUDA/PTX code. The goal of this project wasn’t to build an SGEMM that would magically outperform cuBLAS on all GPUs and all matrix sizes. This is especially pointless, given the open-sourced, lightweight CUTLASS library. Instead, the project primarily targets CUDA learners and aims to bridge the gap between the SGEMM implementations explained in books/blogs and those used in NVIDIA’s BLAS libraries. While the implementation is expected to deliver high performance on Ada/Ampere/Volta/Turing devices, it was specifically fine-tuned for and tested on a local NVIDIA RTX 3090 =GA102 chip: RTX 3080, A10, A40, A6000 . The achieved performance is shown below, comparing results with locked and unlocked GPU core frequencies against cuBLAS and Simon Boehm’s highly cited work used in llamafile, aka tinyBLAS . I plan to continue publishing educational content on high-performance kernels used in AI/ML. Let me know what topics you’d like to see next Projects currently in development: beating NVIDIA on Tensor Cores, Stream-K GEMM, FlashAttention, xLSTM. If you enjoy educational content like this and would like to see more, please share this article. Your feedback would be greatly appreciated P.S. Please feel free to get in touch if you are interested in collaborating. My contact information is available on the homepage. 1. Introduction I clearly remember Andrej’s post on the current state of the existing cuda learning materials vs. cuda code used in high-performance libraries: Indeed, when it comes to SGEMM implementations, there are some excellent educational blog posts, such as 1. How to Optimize a CUDA Matmul Kernel for cuBLAS-like Performance https://siboehm.com/articles/22/CUDA-MMM mentioned by Andrej 2. CUDA Matrix Multiplication Optimization https://leimao.github.io/article/CUDA-Matrix-Multiplication-Optimization/ that break down, step by step, how to optimize a CUDA matmul kernel. However, in terms of achieved performance, none of them come close to matching the speed of cuBLAS or CUTLASS, especially when using recent CUDA versions and if benchmarked properly. From my experiments, these implementations achieve 50-70% of cuBLAS’ performance at best. Additionally, I found the explanations in both blog posts a bit overcomplicated in the final optimization steps. Nevertheless, I still think these resources are great for anyone starting with CUDA programming since they provide good foundational knowledge. On the other hand, I’ve seen some really fast SGEMM implementations with cuBLAS-level performance: The problem is that they are undocumented, difficult to find and understand, especially for a CUDA beginner. A similar problem exists with CUTLASS. While it is highly performant, there is a lack of introductory or educational materials explaining how it is internally designed and implemented in efficient CUDA/PTX. Another notable project is MaxAs https://github.com/NervanaSystems/maxas , an assembler for the Maxwell architecture developed over a decade ago by Scott Gray. This tool enables programming directly in SASS the assembly language for NVIDIA GPUs , allowing direct communication with the hardware instead of relying on the hardware-agnostic CUDA/PTX. Using MaxAs, Scott wrote an SGEMM implementation that achieved around 98% of the GM204 chip’s theoretical maximum FLOPS, surpassing cuBLAS by an average of 5%. While the results are impressive, programming in SASS is inflexible and requires deep understanding of the underlying hardware. Furthermore, with significant advancements in the compiler since then, programming directly in SASS is only advantageous in exceptional cases for example, if you build tinygrad https://github.com/tinygrad/tinygrad . CUTLASS achieves performance on par with cuBLAS across various GPU architectures and matrix sizes using only CUDA/PTX code. But can we actually exceed the cuBLAS barrier? In the following chapters, we will briefly review the high-level SGEMM design used in CUTLASS, and discuss how to translate this design into efficient CUDA/PTX. This guide assumes only a basic knowledge of the CUDA programming model and linear algebra. If you are new to CUDA programming, I strongly recommend starting with these short introductory articles: 1. An Easy Introduction to CUDA C and C++ https://developer.nvidia.com/blog/easy-introduction-cuda-c-and-c/ 2. How to Access Global Memory Efficiently in CUDA C/C++ Kernels https://developer.nvidia.com/blog/how-access-global-memory-efficiently-cuda-c-kernels/ 3. Using Shared Memory in CUDA C/C++ https://developer.nvidia.com/blog/using-shared-memory-cuda-cc/ 4. Increase Performance with Vectorized Memory Access https://developer.nvidia.com/blog/cuda-pro-tip-increase-performance-with-vectorized-memory-access/ Before we proceed with implementation, let’s talk about benchmarking code on NVIDIA GPUs - a topic often overlooked. Properly benchmarking code is just as important as the code itself, particularly when comparing different implementations. 2. How to Benchmark Code on CUDA Devices? The most reliable way to measure kernel duration is by profiling with NVIDIA Nsight Compute and manually extracting performance data. To obtain deterministic and reproducible results, Nsight Compute automatically applies the following settings: 1. Clock Control: locks GPU clock frequencies to their base values 2. Cache Control: flushes all GPU caches before each replay pass 3. Persistence mode Alternatively, you can apply these settings manually and measure kernel duration at runtime without relying on external profilers. On Ubuntu, you can retrieve the base core clock frequency using: nvidia-smi base-clocks For instance, on an RTX 3090, the base core clock frequency is 1395 MHz. Next, you’ll need the memory clock frequencies, which work in combination with the base core clock: nvidia-smi -q -d supported clocks From the list of supported frequencies, choose the fastest memory clock compatible with the base core frequency. Memory clock speeds are generally more stable than core clock speeds. To lock the clock frequencies and enable persistence mode, run the following commands: sudo nvidia-smi --persistence-mode=1 NVIDIA RTX 3090 sudo nvidia-smi --lock-gpu-clocks=1395 sudo nvidia-smi --lock-memory-clocks=9501 To reset the core and memory clock frequencies, you can use: sudo nvidia-smi --reset-gpu-clocks sudo nvidia-smi --reset-memory-clocks sudo nvidia-smi --persistence-mode=0 GPU clock frequencies may drop due to the GPU’s thermal state, but for high-performance applications, throttling is often caused by power limits. Faulty hardware can also lead to throttling. It’s a good idea to monitor the GPU’s state at least during a test run. Use the following command to keep track of power draw, clock speeds, and throttling reasons in real time: watch -n 0.1 nvidia-smi --query-gpu=power.draw,clocks.sm,clocks.mem,clocks throttle reasons.active --format=csv A sample output might look like this: 308.50 W, 1395 MHz, 9501 MHz, 0x0000000000000000 The bit mask 0x0000000000000000 indicates no throttling, and the clocks are running at their maximum speeds. A value of 0x0000000000000001 indicates an idle state. Any other values suggest throttling is occurring. For a full list of bit mask values and their meanings, refer to the NvmlClocksThrottleReasons documentation https://docs.nvidia.com/deploy/nvml-api/group nvmlClocksThrottleReasons.html . Once you’ve locked the clock frequencies, you can measure the kernel duration directly in CUDA using CUDA events. Here’s an example: cudaEvent t start, stop; cudaEventCreate &start ; cudaEventCreate &stop ; float elapsed time ms = 0.0; cudaEventRecord start ; kernel<<<... ... ; cudaEventRecord stop ; cudaEventSynchronize stop ; cudaEventElapsedTime &elapsed time ms, start, stop ; For reliable measurements, multiple replay passes are typically used. In such cases, the GPU cache should be flushed before each kernel replay. This can be done using cudaMemsetAsync as shown in nvbench https://github.com/NVIDIA/nvbench/blob/main/nvbench/detail/l2flush.cuh : // Flush L2 cache int dev id{}; int m l2 size{}; void buffer; checkCudaErrors cudaGetDevice &dev id ; checkCudaErrors cudaDeviceGetAttribute &m l2 size, cudaDevAttrL2CacheSize, dev id ; if m l2 size 0 { checkCudaErrors cudaMalloc &buffer, static cast