Hey Dev Community!
Welcome to a deep, practical, and example-driven guide to programming NVIDIA Tensor Cores for maximum throughput and real-world accuracy. This article is written for engineers who want actionable, production-ready techniques: from WMMA usage to PTX-level MMA instructions, tile-shape selection, mixed-precision strategies, occupancy tuning, and memory layout trade-offs. Expect full CUDA examples, hybrid patterns (CPU+GPU, multi-GPU, NPU+GPU), and micro-optimizations you can apply immediately.
Executive Summary
This article explains how to harness NVIDIA Tensor Cores effectively. You will learn:
- The WMMA API and how to use it in CUDA kernels for matrix multiply-accumulate on Tensor Cores.
- How MMA PTX instructions map to Tensor Core operations and how to inspect them.
- Tile shapes supported by Tensor Cores and how to choose them for different precisions and matrix sizes.
- Mixed-precision strategies (FP16/TF32/FP32/INT8) and numerical stability techniques.
- Occupancy tuning specifically for Tensor Core workloads, including register pressure and shared memory trade-offs.
- Memory layout considerations (row-major vs column-major), alignment, and coalescing patterns.
- Hybrid examples: CPU+GPU orchestration, multi-GPU tiling, GPU+NPU offload patterns.
Table of Contents
- Introduction: Why Tensor Cores Matter
- WMMA API Full Tutorial
- MMA PTX Instructions and Mapping
- Tensor Core Tile Shapes and Performance Implications
- Mixed Precision Strategies and Numerical Stability
- Tensor Core Occupancy Tuning
- Memory Layouts: Row-major vs Column-major and Alignment
- Hybrid Patterns and End-to-End Examples
- CPU + GPU hybrid
- Multi-GPU tiling with NVLink
- GPU + NPU (TensorRT / DLA) hybrid
- GPU + FPGA / ASIC offload patterns (conceptual)
- Practical Checklist and Micro-optimizations
- Conclusion and Call to Action
- Introduction: Why Tensor Cores Matter Tensor Cores are specialized matrix-multiply-accumulate units inside NVIDIA GPUs designed to accelerate dense linear algebra operations. They provide orders-of-magnitude higher throughput for matrix multiplies compared to scalar CUDA cores when workloads are structured to exploit their tile-based, mixed-precision execution model. Modern deep learning, HPC linear algebra kernels, and many inference workloads benefit dramatically from Tensor Cores when programmed correctly.
Key takeaways:
- Tensor Cores operate on small tiles (e.g., 16x16x16, 8x8x32 depending on architecture and precision).
- They are optimized for mixed precision: FP16 inputs with FP32 accumulation, TF32, INT8, and newer formats.
- Achieving peak performance requires careful tiling, memory layout, and occupancy tuning.
- WMMA API Full Tutorial
2.1 WMMA Concepts
WMMA (Warp Matrix Multiply-Accumulate) is a CUDA API that exposes Tensor Core functionality at a high level. It provides fragments for matrix tiles and operations to load, compute, and store results.
Core concepts:
- Fragments: small tile buffers representing submatrices in registers.
- loadmatrixsync / storematrixsync: move data between global/shared memory and fragments.
- mma_sync: perform matrix multiply-accumulate on fragments (maps to Tensor Core MMA).
- tile sizes: depend on compute capability and precision (e.g., 16x16x16 for FP16 on many architectures).
2.2 Basic WMMA Example (FP16 inputs, FP32 accumulation)
A minimal example showing WMMA usage in CUDA. This example demonstrates a single-warp tile multiply and store.
// Compile with: nvcc -arch=sm80 -O3 wmmaexample.cu -o wmma_example
include <cuda.h>
include <cuda_runtime.h>
include <mma.h>
include <stdio.h>
using namespace nvcuda;
define M 16
define N 16
define K 16
// Kernel: single-warp WMMA multiply: C = A * B + C
global void wmmagemmfp16(const half A, const half B, float *C, int lda, int ldb, int ldc) {
// Each warp computes one tile MxN
int warpId = (blockIdx.x * blockDim.x + threadIdx.x) / 32;
// For simplicity assume one warp per tile and contiguous tiles
int tileRow = warpId; // mapping simplified
// Fragments
wmma::fragment<wmma::matrixa, M, N, K, half, wmma::rowmajor> a_frag;
wmma::fragment<wmma::matrixb, M, N, K, half, wmma::colmajor> b_frag;
wmma::fragment<wmma::accumulator, M, N, K, float> c_frag;
// Initialize c_frag to zero
wmma::fillfragment(cfrag, 0.0f);
// Load A and B tiles from global memory (example assumes contiguous layout)
const half tileA = A + tileRow M * lda;
const half *tileB = B; // simplified: single tile B
wmma::loadmatrixsync(a_frag, tileA, lda);
wmma::loadmatrixsync(b_frag, tileB, ldb);
// MMA
wmma::mmasync(cfrag, afrag, bfrag, c_frag);
// Store result
float tileC = C + tileRow M * ldc;
wmma::storematrixsync(tileC, cfrag, ldc, wmma::memrow_major);
}
Notes:
- Real kernels must map tiles to warps/blocks carefully, handle boundaries, and use shared memory for staging.
- Use -arch=sm70 or higher depending on target GPU (sm70 for Volta, sm75 for Turing, sm80 for Ampere, sm_89+ for Hopper).
2.3 WMMA Best Practices
- Use shared memory to stage tiles when A and B are larger than a single tile; this reduces global memory traffic.
- Align loads to 128-bit or 256-bit boundaries for coalescing.
- Batch multiple mma_sync calls per warp to amortize overhead.
- Avoid warp divergence inside warps that execute WMMA.
- MMA PTX Instructions and Mapping
3.1 PTX MMA Overview
At the PTX level, Tensor Core operations are exposed as MMA instructions (e.g., mma.sync.aligned.m16n8k16.row.col.f32.f16.f16.f32 on some architectures). These PTX instructions map to SASS (machine code) that triggers Tensor Core hardware.
3.2 Inspecting PTX and SASS
- Compile CUDA with -lineinfo -G for debug or -arch=sm_80 for release.
- Use nvcc --ptxas-options=-v to see register usage.
- Use cuobjdump --dump-sass or nvdisasm to inspect SASS and confirm MMA instructions.
3.3 Example: Inline PTX MMA (conceptual)
You can emit PTX inline in CUDA to control MMA instruction selection. This is advanced and architecture-specific.
// Conceptual snippet: inline PTX to issue MMA (actual syntax varies by CUDA version)
asm volatile(
"mma.sync.aligned.m16n8k16.row.col.f32.f16.f16.f32 %0, %1, %2, %3;\n"
: "=r"(dst)
: "r"(afragreg), "r"(bfragreg), "r"(cfragreg)
);
Caveats:
- Inline PTX is fragile across toolchain versions.
- Use only when you need exact instruction control and have validated SASS output.
- Tensor Core Tile Shapes and Performance Implications
4.1 Supported Tile Shapes
Tensor Core tile shapes depend on architecture and precision:
- Volta/Turing/Ampere: common tiles include 16x16x16 for FP16, 8x8x32 for INT8 variants, and specialized shapes for TensorFloat-32 (TF32).
- Hopper: introduces more flexible shapes and DP4A-like integer operations.
4.2 Choosing Tile Shapes
- Match algorithmic blocking: choose tile sizes that divide your matrix dimensions to minimize edge handling.
- Balance compute and memory: larger tiles increase arithmetic intensity but require more registers/shared memory.
- Consider accumulation precision: FP32 accumulation reduces overflow risk but increases register pressure.
4.3 Example: Tiling Strategy
- Use 2D tiling: block-level tiling (shared memory) + warp-level WMMA tiles.
- Example: blockDim = (128, 1) threads, each block computes a 128x128 output tile composed of 8x8 WMMA tiles.
- Mixed Precision Strategies and Numerical Stability
5.1 Precision Modes
- FP16 inputs, FP32 accumulation: common for training/inference; good throughput with reduced dynamic range.
- TF32: NVIDIA introduced TF32 to accelerate FP32-like workloads with Tensor Cores (hardware maps TF32 to Tensor Core operations).
- INT8/INT4: quantized inference; requires calibration and careful scaling.
- BFloat16: wider exponent than FP16; good for training stability.
5.2 Numerical Stability Techniques
- Loss scaling (for training with FP16): scale gradients to avoid underflow, then unscale before optimizer step.
- Kahan-style accumulation: when summing many FP16 products, use compensated summation in FP32.
- Block-wise accumulation: accumulate partial sums in FP32 per tile, then reduce.
5.3 Example: Mixed-Precision GEMM with Accumulation in FP32
// Pseudocode: load FP16 tiles, perform WMMA with FP32 accumulation, store FP32 result
wmma::fragment<wmma::matrixa, 16, 16, 16, half, wmma::rowmajor> a;
wmma::fragment<wmma::matrixb, 16, 16, 16, half, wmma::colmajor> b;
wmma::fragment<wmma::accumulator, 16, 16, 16, float> c;
wmma::fill_fragment(c, 0.0f);
wmma::loadmatrixsync(a, A_tile, lda);
wmma::loadmatrixsync(b, B_tile, ldb);
wmma::mma_sync(c, a, b, c); // FP16 inputs, FP32 accumulation
wmma::storematrixsync(Ctile, c, ldc, wmma::memrow_major);
5.4 Hybrid Mixed-Precision Patterns
- FP16 compute + FP32 final reduction: compute many partials in FP16, convert to FP32 for final accumulation.
- Quantize for inference: run INT8 GEMM on Tensor Cores where supported, but validate accuracy with calibration.
- Tensor Core Occupancy Tuning
6.1 Occupancy vs Throughput
Occupancy (active warps per SM) is necessary but not sufficient for throughput. Tensor Core workloads often need:
- Enough warps to hide memory latency.
- Low register pressure to allow many warps.
- Sufficient shared memory for staging tiles.
6.2 Register Pressure and Shared Memory Trade-offs
- WMMA fragments use registers; large fragments increase register usage and reduce occupancy.
- Shared memory reduces global memory traffic but consumes resources that limit blocks per SM.
6.3 Practical Tuning Steps
- Measure baseline: use nvprof / nsys / nv-nsight to get SM utilization, achieved occupancy, and memory throughput.
- Tune block size: vary threads-per-block and tiles-per-block to find sweet spot.
- Reduce registers: compile with -maxrregcount to force spilling if necessary, but prefer algorithmic changes to reduce register usage.
- Use shared memory carefully: prefer double-buffering to overlap loads and compute.
- Use launchbounds_ to guide compiler register allocation.
6.4 Example: Occupancy-aware Kernel Launch
// Example: choose block size to balance occupancy and per-block shared memory
int blockSize = 256; // threads per block
int tilesPerBlock = 2; // number of WMMA tiles computed per block
dim3 grid((N + TILENtilesPerBlock - 1) / (TILENtilesPerBlock));
mywmmakernel<<<grid, blockSize, sharedMemBytes>>>(...);
- Tensor Core Memory Layouts (Row-major VS Col-major)
7.1 Layout Impact
- Tensor Cores expect tiles in specific memory layouts for efficient loads.
- Row-major vs column-major affects coalescing and alignment.
- For WMMA, loadmatrixsync supports both row-major and column-major fragments; choose based on upstream data layout to avoid transposes.
7.2 Alignment and Coalescing
- Align tile base addresses to 128-bit or 256-bit boundaries.
- Coalesce loads by having consecutive threads read consecutive memory addresses.
- Use vectorized loads (float4, half2) when possible.
7.3 Example: Row-major staging into shared memory
// Stage a block of A into shared memory in row-major order
shared half shA[BLOCKM * BLOCKK];
int lane = threadIdx.x;
for (int i = lane; i < BLOCKM * BLOCKK; i += blockDim.x) {
shA[i] = A[blockRow BLOCK_M lda + i];
}
syncthreads();
// Now load from shA into WMMA fragment using row-major load
wmma::loadmatrixsync(afrag, shA + tileOffset, BLOCKK);
- Hybrid Patterns and End-to-End Examples
Goal: show real-world hybrid patterns combining CPU orchestration, multi-GPU tiling, and NPU/GPU offload.
8.1 CPU + GPU Hybrid: Overlap, Pipelining, and Prefetch
Pattern: CPU prepares data, enqueues asynchronous GPU kernels, overlaps host computation and device execution, and pipelines data transfers.
Key techniques:
- Use multiple CUDA streams for overlap.
- Use pinned host memory for fast H2D transfers.
- Double-buffering: while GPU computes on buffer A, CPU fills buffer B.
- Use CUDA events for synchronization.
Example: CPU-GPU pipeline for batched GEMM
// Pseudocode
cudaStream_t streams[2];
cudaStreamCreate(&streams[0]);
cudaStreamCreate(&streams[1]);
for (int batch = 0; batch < batches; batch += 2) {
// Fill host buffers for batch+0 on CPU
prepare_batch(hostA0, hostB0);
cudaMemcpyAsync(devA0, hostA0, size, cudaMemcpyHostToDevice, streams[0]);
cudaMemcpyAsync(devB0, hostB0, size, cudaMemcpyHostToDevice, streams[0]);
launchwmmakernel<<<grid, block, 0, streams[0]>>>(devA0, devB0, devC0);
// Meanwhile prepare batch+1
prepare_batch(hostA1, hostB1);
cudaMemcpyAsync(devA1, hostA1, size, cudaMemcpyHostToDevice, streams[1]);
cudaMemcpyAsync(devB1, hostB1, size, cudaMemcpyHostToDevice, streams[1]);
launchwmmakernel<<<grid, block, 0, streams[1]>>>(devA1, devB1, devC1);
// Copy results back asynchronously
cudaMemcpyAsync(hostC0, devC0, size, cudaMemcpyDeviceToHost, streams[0]);
cudaMemcpyAsync(hostC1, devC1, size, cudaMemcpyDeviceToHost, streams[1]);
}
8.2 Multi-GPU Tiling with NVLink Peer-to-Peer
Pattern: Partition large matrices across GPUs; use NVLink for fast peer-to-peer transfers; overlap inter-GPU comms with compute.
Key techniques:
- Use cudaSetDevice and cudaMemcpyPeerAsync for P2P transfers.
- Use NCCL for collective operations (all-reduce) when synchronizing gradients or partial results.
- Partition by rows or columns depending on memory layout and communication pattern.
Example: 2-GPU row-slab partition
- GPU0 holds rows [0..N/2), GPU1 holds rows [N/2..N).
- Each GPU computes local GEMM for its slab.
- If final reduction needed, use cudaMemcpyPeerAsync to gather or NCCL AllGather.
8.3 GPU + NPU (TensorRT / DLA) Hybrid
Pattern: Use GPU Tensor Cores for heavy matrix ops and NPU (or DLA) for inference kernels that are more efficient on specialized hardware.
Key techniques:
- Partition model: run convolution-heavy layers on GPU, run small fully-connected or quantized layers on NPU.
- Use TensorRT to compile subgraphs for GPU and DLA; orchestrate execution via host.
- Ensure consistent quantization/scaling between devices.
Example flow:
- Preprocess on CPU.
- Run conv blocks on GPU (Tensor Cores).
- Transfer intermediate activations to NPU (if supported) for quantized FC layers.
- Collect outputs and postprocess on CPU.
8.4 GPU + FPGA / ASIC Offload (Conceptual)
Pattern: Offload specific kernels (e.g., custom convolution, low-latency inference) to FPGA/ASIC while using GPU for general dense compute.
Key techniques:
- Define clear data exchange protocol (DMA, PCIe).
- Use streaming interfaces to minimize latency.
- Partition workload by kernel characteristics: FPGA for low-latency, GPU for throughput.
- Practical Checklist and Micro-optimizations
9.1 Pre-kernel Checklist
- Ensure data alignment (128-bit) for loads.
- Choose tile sizes that divide matrix dims or handle edges explicitly.
- Precompute strides and offsets on host to reduce device arithmetic.
- Use pinned host memory for transfers.
9.2 Kernel Micro-optimizations
- Use restrict pointers to help compiler optimize.
- Minimize divergent branches inside warps.
- Use ldg() for read-only global memory where beneficial.
- Use syncwarp() and shfl_sync() for warp-level reductions.
- Double-buffer shared memory to overlap loads and compute.
9.3 Profiling and Validation
- Use Nsight Systems and Nsight Compute to profile:
- SM utilization
- Tensor Core utilization
- Memory throughput and L2 hit rates
- Warp efficiency and divergence
- Validate numerical accuracy with unit tests comparing FP32 baseline to mixed-precision outputs.
- Conclusion and Call to Action (in-article) Tensor Cores unlock massive throughput for matrix-heavy workloads, but only when code is structured to match their tile-based, mixed-precision model. The path to peak performance is iterative: design tiling, stage with shared memory, tune occupancy, and validate numerics. Use the hybrid patterns shown here to scale from single-GPU to multi-GPU and heterogeneous systems.
Powerful next steps:
- Try the WMMA example on a small matrix and inspect the generated SASS to confirm MMA instructions.
- Replace FP32 GEMM with mixed-precision WMMA and measure throughput and accuracy trade-offs.
- Implement a CPU-GPU pipeline with double-buffering and measure end-to-end latency.
Appendix: Advanced CUDA Examples (Hybrid-focused)
A.1 Full WMMA + Shared Memory Tiled GEMM (simplified)
// Highly simplified skeleton: real implementation requires boundary checks and optimizations
global void wmmatiledgemm(const half A, const half B, float *C, int M, int N, int K) {
extern shared half shmem[]; // shared memory for A and B tiles
// compute block and warp indices, load tiles into shared memory, sync
// use wmma::loadmatrixsync to load from shared memory into fragments
// perform multiple mma_sync per warp to compute block result
// store final results to global memory
}
A.2 Hybrid CPU+GPU: Host-side autotuner
- Implement a small autotuner on CPU that runs microbenchmarks for tile sizes, block sizes, and shared memory usage, then picks the best configuration for the target GPU and matrix sizes.
A.3 Multi-GPU: Partition + NCCL AllReduce for distributed GEMM
- Partition matrices across GPUs, compute local partials, then use NCCL AllReduce to sum partial results when needed (e.g., distributed training gradient aggregation).
Final Notes
- Always test on the target GPU architecture; tile shapes and instruction encodings differ across Volta/Turing/Ampere/Hopper.
- Keep an eye on new CUDA toolkit releases; NVIDIA often adds new WMMA primitives and improved compiler support.
- Use mixed-precision carefully: validate accuracy for your workload and consider block-wise FP32 accumulation when necessary.
Goodbye and Call to Action (in-article)
If this guide helped you squeeze more performance from Tensor Cores, do the following:
- React to this post — reactions help us prioritize deeper tutorials and real-world kernel walkthroughs.
- Follow us for weekly deep dives into GPU internals and production-ready optimization patterns.
- Comment the section you want next (WMMA microbenchmarks, PTX→SASS walkthrough, multi-GPU orchestration, or TensorRT integration) and we’ll produce a focused, hands-on follow-up.
- Share your results and benchmarks in the comments — we’ll feature the best community submissions in the next post.
- Check our previous blogs in the Advanced GPU Optimization series for complementary topics and code samples.
Thank you for reading — now go benchmark, iterate, and push those Tensor Cores to their limits.
Top comments (0)