Introduction
In our last lesson, we established the fundamental workflow for programming Tensor Cores using the nvcuda::wmma C++ API. You learned how to declare matrix fragments, load them with data, execute a warp-level matrix-multiply-accumulate operation with mma_sync, and store the results. However, the example we studied used a simplified data path, loading matrix tiles directly from global memory into registers for each mma_sync operation. For your goal of mastering high-performance computing, you know this approach is suboptimal, as it repeatedly accesses high-latency global memory for data that could be reused.
This lesson brings together your knowledge of memory hierarchy optimization from Module 9 with your new understanding of Tensor Cores. We will construct a complete, high-performance General Matrix Multiplication (GEMM) kernel that implements the canonical data pipeline for modern GPU computing: staging data from global memory into shared memory, and then efficiently feeding the Tensor Cores by loading fragments from shared memory into registers. By mastering this pattern, you will be able to write kernels that can truly saturate the GPU's computational resources.
The Complete GEMM Data Pipeline
To achieve maximum performance, we must choreograph a flow of data through the GPU's memory hierarchy that maximizes reuse at each level. The goal is to keep the powerful Tensor Cores constantly supplied with data, minimizing stalls caused by waiting for memory. This creates a hierarchical tiling strategy, where the problem is progressively broken down to match the GPU's architecture.
The image below illustrates this complete hierarchy. We will spend this lesson building a kernel that implements this structure.

Let's break down the responsibilities at each level:
-
Grid Level (Global Memory): The entire GEMM operation, , involves matrices residing in global memory. The grid of thread blocks is overlaid on the output matrix C.
-
Thread Block Level (Shared Memory): A single thread block is responsible for computing a tile of the output matrix C (e.g., 128x128). To do this efficiently, the block's threads collaboratively load tiles of A and B from global memory into shared memory. This acts as a fast, programmer-managed cache. The block iterates through tiles along the K dimension, loading a new pair of A and B tiles into shared memory at each step.
-
Warp Level (Register Fragments): Within a thread block, each warp computes a smaller sub-tile of the block's output tile (e.g., a 64x32 tile). The warp loads even smaller matrix tiles—the
wmmafragments (e.g., 16x16)—from shared memory into its registers. It performs themma_syncoperation and accumulates the result in its private accumulator fragments.
This layered approach ensures that once a tile of data is fetched from slow global memory into fast shared memory, it is reused many times by the warps to perform numerous MMA operations, dramatically increasing the arithmetic intensity of the kernel.
Building the High-Performance wmma Kernel
We will now walk through the structure of a kernel that implements this pipeline. The logic builds directly upon the optimizations you learned in Module 9, but the compute step is replaced with wmma intrinsics.
Step 1: Data Staging from Global to Shared Memory
The first step inside the kernel's main loop (which iterates over tiles in the K dimension) is to have all threads in the block participate in loading one tile of A and one tile of B from global memory.
__shared__Memory Declaration: You declare 2D arrays in shared memory to hold these tiles. A common size might be__shared__ half A_tile[128][16];and__shared__ half B_tile[16][128];.- Cooperative Loading: Each thread calculates a source address in global memory and a destination address in shared memory, then loads several elements. To maximize bandwidth, this is done using vectorized loads (e.g., loading data as
int4orfloat4), a technique you're familiar with from Module 9. - Synchronization: After all threads have finished loading their portion of the tiles, a
__syncthreads();is absolutely essential. This guarantees that the entire A and B tiles are visible to all threads in the block before any thread attempts to use them for computation.
Step 2: Feeding Fragments from Shared Memory
Once the data is staged in shared memory, the warps can begin their work. Inside a second, nested loop that iterates over the K-dimension of the shared memory tile, each warp executes the core wmma workflow. The key difference from our last lesson is the source of the data.
The nvcuda::wmma::load_matrix_sync function is now called with a pointer to an address in shared memory.
// Within a loop over the K-dimension of the shared memory tile...
// Pointer to the start of the current A fragment in shared memory
const half* a_tile_ptr = &A_tile[/*row*/][/*col*/];
// Pointer to the start of the current B fragment in shared memory
const half* b_tile_ptr = &B_tile[/*row*/][/*col*/];
// Load fragments from SHARED memory
nvcuda::wmma::load_matrix_sync(a_frag, a_tile_ptr, A_tile_leading_dim);
nvcuda::wmma::load_matrix_sync(b_frag, b_tile_ptr, B_tile_leading_dim);
// Perform the MMA operation
nvcuda::wmma::mma_sync(acc_frag, a_frag, b_frag, acc_frag);
By repeatedly loading from shared memory, we avoid going back to global memory for each MMA operation, which is the cornerstone of this optimization.
Step 3: Bank Conflicts and Layouts
As a systems programmer, you know that shared memory is organized into banks. If multiple threads in a warp access the same bank simultaneously, a bank conflict occurs, serializing the memory accesses and reducing performance.
When warps load fragments from shared memory, the access patterns can easily lead to such conflicts. A common technique to mitigate this is to pad the leading dimension of the shared memory array. For example, instead of declaring __shared__ half B_tile[16][128], one might use __shared__ half B_tile[16][128 + 8]. This "skewing" offsets the starting address for each row, ensuring that threads from a warp access different banks.
Another advanced technique is to transpose one of the matrices (e.g., matrix A) on-the-fly during the load from global to shared memory. This can arrange the data in shared memory such that subsequent fragment loads by the warp are perfectly conflict-free for both A and B.
A Complete Implementation
A comprehensive, production-quality implementation of this pattern involves intricate indexing and careful tuning of tile sizes. Let's study an expertly written example that puts all these pieces together.
CUDA Matrix Multiplication Optimization - Lei Mao's Log Book
This article by Lei Mao presents a fantastic evolution of a GEMM kernel, culminating in a version that uses the full data pipeline with Tensor Cores. It will serve as our reference implementation.
Please read the section "Implementation with Tensor Core". As you study the gemm_v07_vectorized kernel, focus on identifying the following components: Shared Memory: Locate the declarations for A_thread_block_tile_transposed and B_thread_block_tile. Note the use of BLOCK_TILE_SKEW_SIZE for padding to avoid bank conflicts. Global to Shared Load: Examine the load_data_to_shared_memory_transposed_vectorized function. See how it uses vectorized types (int4) for high-bandwidth transfers. Synchronization: Find the __syncthreads() calls. The first one is after the load into shared memory, and the second is after the computation on the shared memory tile is complete, before loading the next tile. Shared to Fragment Load: Inside the inner k_i loop, find the nvcuda::wmma::load_matrix_sync calls. Verify that their source pointers are addressing the shared memory arrays. MMA and Store: Observe how nvcuda::wmma::mma_sync is used to accumulate results, and how nvcuda::wmma::store_matrix_sync writes the final acc_frags back to global memory.
The example uses __half precision for the inputs and accumulator, as this is the native format for many Tensor Core operations, delivering the highest throughput. The WMMA API also supports other configurations, such as FP16 inputs with FP32 accumulation.

Conclusion
In this lesson, you have assembled the concepts from previous modules into a cohesive, high-performance kernel. By implementing a three-stage data pipeline—Global Memory -> Shared Memory -> Registers (Fragments)—you now understand the standard pattern for writing compute-bound kernels that can effectively leverage NVIDIA's Tensor Cores.
Key Takeaways:
- High-performance
wmmakernels use shared memory as a programmer-managed cache to minimize redundant global memory traffic. - The overall kernel structure involves a block-level loop for staging data into shared memory and a nested warp-level loop for consuming that data via fragments.
__syncthreads()is critical for ensuring correctness, synchronizing the block's threads after loading data into shared memory and before beginning computation on it.- Advanced optimizations like vectorized loads, shared memory padding (skewing), and on-the-fly transposition are used to maximize memory bandwidth and avoid bank conflicts.
- The pointer passed to
nvcuda::wmma::load_matrix_syncdetermines the data source; for high performance, this should point to shared memory, not global memory.
You have now reached the pinnacle of classical GEMM optimization. You have the theoretical knowledge and practical code reference to build kernels that approach the performance of vendor-tuned libraries like cuBLAS.
In our next lesson, we will move beyond GEMM and apply these powerful tiling and data movement principles to a more complex and cutting-edge algorithm. We will investigate FlashAttention, the algorithm that revolutionized Transformer models by reformulating the attention mechanism to be memory-aware, enabling the training of models with much longer sequence lengths.