Skip to main content

Welcome to a Grasp lesson

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

Lesson 70

Tensor Cores and `mma.sync` for Matrix Operations

Recommended resources

Introduction

In our previous lesson, we explored the "what" and "why" of mixed-precision computing. You learned how formats like FP16 and BFloat16 enable massive performance gains by reducing memory traffic and, crucially, by unlocking the power of specialized hardware. We concluded by identifying Tensor Cores as the key architectural innovation that makes this all possible.

Today, we transition from the high-level concept to the low-level reality. This lesson delves into the architecture of NVIDIA's Tensor Cores, aligning perfectly with your interest in systems programming and hardware specifics. We will peel back the abstraction of CUDA C++ to understand what a Tensor Core truly is, how it fits into the Streaming Multiprocessor (SM), and why programming it presents unique challenges. Our main objective is to dissect the fundamental PTX (Parallel Thread Execution) instruction that commands these units: mma.sync, the warp-level matrix-multiply-accumulate. This is your first step toward commanding the GPU at a level where true peak performance is achieved.


What is a Tensor Core?

First, it is essential to clarify a common misconception. Despite the name, a "Tensor Core" is not a programmable core in the sense of a CPU core, with its own instruction fetch/decode units and program counter.

Lab 6: Matrix Multiply – Tensor Cores - Accelerated Computing

This reading from the Accelerated Computing course at CMU provides an excellent and concise clarification on this terminology.

Please read the introduction, paying close attention to the section titled Note on Terminology.

As the text explains, a Tensor Core is a specialized functional unit within the SM, much like an FPU (Floating-Point Unit) or ALU (Arithmetic Logic Unit). Its purpose is singular: to execute matrix-multiply-accumulate (MMA) operations, of the form , at an incredible speed.

This diagram shows the data path for a warp-level MMA operation. Data for matrix A is loaded from Shared Memory into the warp's Register File. These registers then feed the Tensor Cores, which perform the computation. This illustrates the final stage of the memory hierarchy before computation.

To get a better feel for the hardware-level operation and its significance, let's watch a short analysis.

Analysis of a Tensor Core

The video "Analysis of a Tensor Core" by RTL Engineering offers a VLSI architecture perspective on the Tensor Core's function.

Watch the following three segments: The introduction explains the core D = A \times B + C operation and highlights why the accumulation step (matrix C) is vital for building larger matrix multiplications from smaller 4x4 hardware chunks. This segment shows a functional block diagram, emphasizing the mixed-precision nature (e.g., FP16 inputs, FP32 accumulator) we discussed in the last lesson. This short clip shows where Tensor Cores are physically located within a Volta-generation SM, giving you a sense of their physical size and integration.

Tensor Cores have evolved significantly across NVIDIA architectures. What began with Volta as a cooperative operation among 8 threads has expanded dramatically. In Ampere, it became a warp-level operation, and in the latest Hopper architecture, it's a "warp group" operation involving four warps. This evolution reflects a continuous push for higher throughput, which also increases the complexity of programming them effectively.


The Challenge: Feeding the Beast

The immense computational throughput of Tensor Cores creates a new bottleneck: data. To keep them busy, you must feed them data at an unprecedented rate. This is non-trivial and is the core challenge of high-performance kernel engineering.

Lecture 23: Tensor Cores

In "Lecture 23: Tensor Cores," the speaker from NVIDIA provides an excellent summary of why programming Tensor Cores is so difficult.

Watch the section from this timestamp. The speaker outlines two fundamental problems: Hierarchical Parallelism & Data Layouts: You can't just write a simple for loop. The problem must be tiled across the GPU's memory and thread hierarchies, and Tensor Cores require data to be arranged in specific, complex layouts within registers. Asynchrony & Latency Hiding: The programmer must explicitly manage a software pipeline to stream data from slower global memory to fast shared memory and registers, ensuring the Tensor Cores are never waiting for data.

The speaker's point about the "insanity of index bookkeeping" is key. The rest of this lesson is dedicated to understanding the precise nature of that insanity.


The mma.sync Instruction: A Warp-Level Command

To command the Tensor Core, we must go below CUDA C++ to PTX (Parallel Thread Execution), an assembly-like intermediate language for NVIDIA GPUs. We can embed PTX instructions directly into our CUDA C++ code using inline assembly.

The fundamental instruction is mma, for Matrix-Multiply-Accumulate. For modern architectures (Ampere and later), this is a warp-level instruction.

This is the single most important concept in this lesson: when a warp executes an mma instruction, it is not 32 threads performing 32 independent matrix multiplications. Instead, all 32 threads of the warp cooperate on a single, larger matrix multiplication. The input and output matrices (A, B, C, D) are distributed, or "fragmented," across the private registers of all 32 threads.

Deconstructing the Instruction

Let's examine a specific mma instruction available on Ada-generation GPUs, which is also compatible with our previous discussion on mixed precision.

mma.sync.aligned.m16n8k8.row.col.f32.tf32.tf32.f32

Let's break this down piece by piece, using the insights from our resources.

  • mma: Specifies a Matrix-Multiply-Accumulate operation.
  • sync: Indicates that all 32 threads in the warp synchronize after this instruction completes. Execution for the warp will not proceed until the MMA is finished.
  • aligned: A requirement that all 32 threads in the warp must execute this instruction. If divergence causes some threads to skip it, the behavior is undefined.
  • m16n8k8: This defines the shape of the matrix multiplication. The operation is .
    • m16: M = 16 rows.
    • n8: N = 8 columns.
    • k8: K = 8 is the inner "reduction" dimension.
    • So, this instruction multiplies a 16x8 matrix (A) by an 8x8 matrix (B) and adds the 16x8 result to a 16x8 matrix (C).
  • row.col: Specifies the memory layout of the input matrices. Matrix A is assumed to be in row-major layout in the registers, while matrix B is in column-major layout.
  • .f32.tf32.tf32.f32: These specify the data types for the matrices D, A, B, and C, respectively. Here, the input matrices A and B are in tf32 (TensorFloat-32) format, but the accumulation (+ C) and the final output D are done in full f32 precision. This is a direct hardware implementation of the mixed-precision principle.

Data Layouts: Matrix Fragments

Now for the "insanity". How are these 16x8 and 8x8 matrices physically stored across the registers of 32 threads? This is where the concept of a matrix fragment becomes critical. The hardware dictates a specific, non-intuitive mapping.

Lab 6: Matrix Multiply – Tensor Cores - Accelerated Computing

This lab guide provides the clearest available documentation on the m16n8k8 fragment layouts.

Read the sections Warp-Level Semantics to solidify your understanding of the cooperative nature of the warp. Then, carefully study the text in the sections that follow, describing the layouts for the A, B, and C/D matrices, and finally read the Note on Register Types. The PTX docs for these layouts are notoriously dense, so this description is invaluable.

Let's summarize the key points from the reading:

  • Vector Registers: It's helpful to think of a single C++ register variable (e.g., float reg_a;) not as a scalar, but as a 32-wide vector register, with each thread holding one lane.
  • Total Registers Needed:
    • A (16x8 = 128 elements): 128 / 32 threads = 4 registers per thread.
    • B (8x8 = 64 elements): 64 / 32 threads = 2 registers per thread.
    • C/D (16x8 = 128 elements): 128 / 32 threads = 4 registers per thread.
    • This is why the PTX instruction has placeholders for 4 registers for D, 4 for A, 2 for B, and 4 for C.
  • Layout Description:
    • Matrix A (row-major): The 16x8 matrix is divided into four 8x4 quadrants. Each thread in the warp holds four values, with each value coming from a specific location determined by the hardware mapping.
    • Matrix B (col-major): The 8x8 matrix is split into two 8x4 halves. Each thread holds two values.
    • Matrix C/D: The 16x8 matrix layout is the most complex, involving interleaved vertical stripes.
  • Register Type Quirk: The PTX instruction expects the operands to be 32-bit integer registers, even though they contain bit-patterns for floats. You must use compiler intrinsics like __float_as_uint() to handle this type reinterpretation.

The image below gives a conceptual illustration of how a warp of 32 threads (T0 to T31) might be organized to collectively work on a matrix problem. While the specific layout shown is for a different mma shape, it powerfully visualizes the core idea of threads being mapped to different parts of the input and output matrices.

Conceptual mapping of a 32-thread warp to a matrix operation. The warp (represented by T0-T31) computes an 8x8 output tile (green) by accessing elements from an 8xK input matrix A (blue) and a Kx8 input matrix B (yellow).

You are not expected to memorize these layouts. In practice, library functions (like those in nvcuda::wmma or CUTLASS) handle the loading and arranging of data for you. However, understanding that these complex, rigid layouts exist is fundamental to appreciating why those libraries are necessary and how they are designed.


Conclusion

This lesson has taken you deep into the GPU's silicon to uncover the nature of Tensor Cores. You've moved beyond the abstract idea of a "performance accelerator" to a concrete understanding of a specialized hardware unit with a specific command set and rigid operational requirements.

Key Takeaways:

  • Tensor Cores are specialized MMA functional units, not general-purpose cores. They are the engine of modern AI computation.
  • The primary challenge in using them is feeding them data fast enough, which requires sophisticated data movement and pipelining.
  • Tensor Cores are programmed at a low level using warp-level PTX instructions like mma.sync.
  • A single mma.sync instruction directs an entire 32-thread warp to cooperate on one matrix multiplication.
  • The input and output matrices are fragmented across the registers of the warp's threads in a specific, non-trivial data layout dictated by the hardware.
  • The mma.sync instruction's name encodes its shape, data types, and layout requirements, directly exposing the hardware's functionality.

We have now established the fundamental hardware principles. In our next lesson, we will climb one level of abstraction higher and learn how to use the nvcuda::wmma C++ template API. This API provides a more user-friendly interface to the raw PTX instructions, allowing us to load data into fragments, invoke the MMA operation, and begin constructing our first Tensor Core-powered GEMM kernel without writing inline assembly by hand.