No history yet

CuTe Layout Algebra

The Essence of CuTe Layouts

CuTe, standing for CUDA Templates, is a core component of NVIDIA's CUTLASS 3.x library. It provides a powerful, compile-time abstraction for describing and manipulating the layout of data in memory. This abstraction allows for the decoupling of the logical arrangement of threads from the physical layout of data in memory, enabling flexible and high-performance kernel development.

At its heart, a cute::Layout is a function that maps a logical coordinate (a multidimensional index) to an offset in physical memory. This function is defined by two key components: a Shape and a Stride.

// Conceptual representation
struct Layout {
  Shape shape;    // The dimensions of the logical tensor
  Stride stride;  // The distance in memory between elements along each dimension
};

// To find the memory offset for a coordinate (i, j, k):
// offset = i * stride_i + j * stride_j + k * stride_k;

Let's break down the Shape and Stride components. A Shape is a tuple of integers representing the dimensions of a tensor. For example, make_shape(64, 128) represents a 2D matrix of 64 rows and 128 columns. A Stride is also a tuple of integers, where each element defines the number of linear memory elements to traverse to move one step along the corresponding dimension of the Shape. For a standard row-major layout of a 64x128 matrix, the strides would be (128, 1). To move to the next row (dimension 0), you step 128 elements. To move to the next column (dimension 1), you step 1 element.

#include <cute/tensor.hpp>

// A 4x8 matrix shape
auto shape = cute::make_shape(4, 8); // Shape<_4, _8>

// Standard row-major layout for this shape
auto layout = make_layout(shape); // Stride<_8, _1>

// Column-major layout for the same shape
auto layout_col = make_layout(shape, make_stride(1, 4)); // Stride<_1, _4>

Notice the use of _4 and _8. In CuTe, integers prefixed with an underscore are treated as compile-time constants. This allows the compiler to perform extensive optimizations, unrolling loops and pre-calculating offsets, which is critical for performance. You can also use runtime variables for dimensions, which provides flexibility but may come at a slight performance cost. This is the difference between a static and a dynamic layout.

Composing and Tiling Layouts

The real power of CuTe lies in its ability to compose and manipulate layouts. This allows you to build complex memory access patterns from simpler, reusable components. The two primary operations for this are composition and tile.

  • composition(layoutA, layoutB): Creates a new layout by applying layout B to the co-domain (the memory locations) of layout A. This is useful for describing layouts within layouts, such as a layout of tiles within a larger matrix.
  • tile(layout, tile_shape): This is a specialized form of composition that partitions a larger layout into smaller, identical tiles. It's the cornerstone of how CUTLASS organizes data for processing by warps and thread blocks.
using namespace cute;

// A 128x128 matrix in global memory
Layout gmem_layout = make_layout(make_shape(Int<128>{}, Int<128>{}));

// Tiling it into 8x8 thread blocks
// This produces a layout of shape (16, 16, 8, 8)
// The first two modes correspond to the grid of thread blocks
// The second two modes correspond to the layout within a block
auto tiled_layout = tile_to_shape(gmem_layout, make_shape(Int<8>{}, Int<8>{}));

// A thread's view of the data (e.g., thread 0 in block (0,0))
auto thr_idx = 0;
auto blk_idx = make_coord(0, 0);
auto thr_layout = local_tile(tiled_layout, make_shape(Int<8>{}, Int<8>{}), make_coord(thr_idx, 0), Step<_1, _0>{});

// The logical_divide operation is a powerful composition variant.
// It splits one logical dimension into two. For example, splitting a vector of 16 elements
// into a 4x4 tiled layout.
auto vec_layout = make_layout(Int<16>{});
auto tiled_vec_layout = logical_divide(vec_layout, Shape<_4, _4>{}); // Shape is now <_4, _4>, Stride is <_4, _1>

The tile_to_shape function is a key helper that combines composition and product operations to create a tiled layout. The result is a hierarchical layout that maps a multidimensional thread and block index to a linear memory offset. Another powerful tool is the Z-order curve|https://en.wikipedia.org/wiki/Z-order_curve layout, often used for improving cache locality in matrix multiplication. In CuTe, this is achieved using zipped_divide, which interleaves the bits of the coordinates.

Mapping Threads to Data

Once you have a tiled layout, you need to map each thread in a block to a specific portion of that tile. This is where thread identifiers like threadIdx.x come into play. CuTe provides functions like thread_idx(0) (equivalent to threadIdx.x) and block_idx(0) (equivalent to blockIdx.x) to get these hardware identifiers in a rank-agnostic way.

A common pattern is to partition the tile layout by the number of threads in the block. The local_tile function is a convenient way to do this. It takes the tiled layout and a thread's unique ID and returns a new layout representing only the data that specific thread is responsible for. This makes the code within the kernel much cleaner, as each thread can operate in its own logical coordinate system without complex indexing arithmetic.

// Assume a 1D thread block of 128 threads
// and a tile of 8x16 elements.
// Total elements per block = 8 * 16 = 128

// Layout for the shared memory tile
auto smem_layout = make_layout(make_shape(Int<8>{}, Int<16>{}));

// Decompose the layout for each thread
// This gives each thread a unique, disjoint part of the tile
auto thr_layout = local_tile(smem_layout, make_shape(Int<1>{}, Int<1>{}), make_coord(threadIdx.x, 0));

// Now, each thread can find its element's memory offset like this:
int offset = thr_layout(make_coord(0,0)); // Offset for this thread's element

This powerful abstraction is what allows CUTLASS to be so flexible. The core computational part of a kernel (like a matrix multiplication) can be written generically against these logical layouts. Then, by simply changing the Layout objects passed into the kernel, the same code can be adapted to handle different data arrangements in memory (row-major, column-major, interleaved, etc.) without modification. This is the essence of building kernels that are 'correct-by-construction'.

Quiz Questions 1/6

What are the two primary components that define a cute::Layout?

Quiz Questions 2/6

For a make_shape(32, 64) tensor stored in a standard column-major format, what would the corresponding Stride tuple be?