主机软件

CuTe DSLCuTe DSL

CuTe DSL 是一种基于 Python 的领域特定语言(Domain-Specific Language, DSL),用于在保持高开发效率的同时,高性能地编写和动态编译核函数 kernel

CuTe DSL 是 CUTLASS 的一部分,而 CUTLASS 是一个由 CUDA C++ 模板和 DSLs 组成的集合。与 cuBLAScuDNN 这种为常规操作提供现成可调用核函数的库不同,CUTLASS 技术栈提供的工具允许开发者以可组合的方式来定义高性能的核函数。

CuTe DSL 的核心抽象包括布局 (Layouts)、张量 (Tensors)、硬件原子 (Hardware Atoms) 以及分块操作 (Tiled Operations)。布局描述了数据在内存中以及在线程之间的组织方式;张量将数据指针或迭代器与布局元数据结合在一起;原子代表了底层的硬件操作,例如矩阵乘加(MMA)或内存拷贝;分块操作则描述了如何将这些原子应用到整个线程块 thread blocks 和 线程束 warps 中。关于其底层细节,请参阅 CuTe。

从 Python 启动 CuTe DSL 核函数时,Python 程序会调用一个带有 @cute.jit 修饰器的函数,而该函数会进一步启动一个带有 @cute.kernel 修饰器的函数。

@cute.jit 修饰器声明了一个即时编译(JIT)函数,该函数可以从 Python 或其他 CuTe DSL 函数中调用。@cute.kernel 修饰器则定义了一个 GPU 核函数,该函数可以从 @cute.jit 函数中启动。Python 代码无法直接调用 @cute.kernel 函数。

例如,让我们来看一个用于两个一维张量逐元素相加的朴素(未优化)CuTe DSL 核函数。 这是 GPU 编程的 “Hello World” 示例,其历史可以追溯到 Ian Buck 的 Brook 框架 (该框架早于并启发了 CUDA 编程模型)。您可以使用 Modal Notebook 来使用并编辑此核函数并在 B200 GPU 上运行它。

import cutlass.cute as cute
import torch

Tensor = cute.Tensor | torch.Tensor


@cute.kernel
def elem_add_kernel(a: cute.Tensor, b: cute.Tensor, out: cute.Tensor):
    block_x, _, _ = cute.arch.block_idx()
    block_dim_x, _, _ = cute.arch.block_dim()
    thread_x, _, _ = cute.arch.thread_idx()

    i = block_x * block_dim_x + thread_x

    if i < out.shape[0]:
        out[i] = a[i] + b[i]


@cute.jit
def elem_add(a: Tensor, b: Tensor, out: Tensor):
    n = out.shape[0]
    threads_per_block = 128
    blocks = (n + threads_per_block - 1) // threads_per_block

    elem_add_kernel(a, b, out).launch(
        grid=(blocks, 1, 1),
        block=(threads_per_block, 1, 1),
    )

其中 elem_add_kernel 函数就是核函数 (kernel)。每个线程 (thread) 计算一个输出元素。全局元素索引 i 是通过线程块索引、块内线程数以及块内线程索引计算得出的:

i = block_x * block_dim_x + thread_x

elem_add 函数计算出覆盖输出张量所需的线程块数量,并使用一维的线程块网格 (thread block grid) 启动该核函数。

这个例子仅用于教学,并未经过优化。即便如此,它也展示了一个良好的基础访问模式:相邻的线程读取 ab 的相邻元素,然后写入 out 的相邻元素。这是对全局内存 global memory 进行合并访问所需的模式;参见内存合并 memory coalescing

布局(Layout)问题是 CuTe DSL 在编写高性能核函数时大显身手的原因之一。针对性能 performance 进行工程设计非常困难,因为核函数必须与硬件紧密映射:哪些线程处理哪些数据、内存如何访问、工作如何分块,以及生成的代码应该使用哪些硬件操作。CuTe DSL 允许程序员显式地表达这些映射,同时可以在各种不同的形状(Shapes)和流式多处理器架构 Streaming Multiprocessor architectures 之间复用大部分相同的核函数代码。

对于其他领域关注性能的工程师来说,这可能会令人感到惊讶——一个用 Python 这种解释型语言编写的程序,怎么可能指望与用编译型语言编写的程序并驾齐驱?

答案是,CuTe DSL 核函数是即时(Just-In-Time, JIT)编译的。Python 源代码会被转换为抽象语法树(AST),通过代理参数进行追踪,然后再进行编译。请注意,JIT 编译的代码仅支持 Python 语法语义的一个子集。

在撰写本文时,在 CUTLASS 4.x 中,编译技术栈在执行前会先通过多级中间表示 Multi-Level Intermediate Representation (MLIR) 转换为 PTX IR,然后再转换为特定于设备的 SASS 汇编。

FlashAttention-4 核函数为例。我们 文章 中对该开源代码进行了拆解分析,详细介绍了它如何通过流水线化的序列线程束(Pipelined Warp Specialization)、张量核心 Tensor Core 操作,以及张量内存 Tensor Memory 和 张量内存加速器 Tensor Memory Accelerator 操作,直接利用 CuTe DSL 实现了顶级的尖端性能。

欲了解更多关于 CuTe DSL 的细节,请参阅 NVIDIA 的 CuTe DSL documentation 以及 CuTe DSL overview blog

英文原文(Modal GPU Glossary)

CuTe DSL is a Python-based Domain-Specific Language (DSL) for writing and dynamically compiling kernels at high performance and with high developer productivity.

CuTe DSL is part of CUTLASS, a collection of CUDA C++ templates and DSLs. Unlike cuBLAS or cuDNN, which provide ready-to-call kernels for common operations, the CUTLASS stack provides tools for composably defining high-performance kernels.

The core abstractions of CuTe DSL include layouts, tensors, hardware atoms, and tiled operations. Layouts describe how data is organized in memory and across threads. Tensors combine data pointers or iterators with layout metadata. Atoms represent fundamental hardware operations such as matrix multiply-accumulate (MMA) or memory copy. Tiled operations describe how atoms are applied across thread blocks and warps. For the underlying details, see CuTe.

When launching a CuTe DSL kernel from Python, the Python program calls a @cute.jit function, and that function launches a @cute.kernel function.

The @cute.jit decorator declares a JIT-compiled function that can be called from Python or from other CuTe DSL functions. The @cute.kernel decorator defines a GPU kernel function that can be launched from a @cute.jit function. Python code cannot call a @cute.kernel function directly.

For example, let's look at a naive (unoptimized) CuTe DSL kernel for elementwise addition of two one-dimensional tensors -- the "hello world" for GPU programming that goes back to Ian Buck's Brook framework that preceded and inspired CUDA. You can edit this kernel and execute it on a B200 GPU using this Modal Notebook.

import cutlass.cute as cute
import torch

Tensor = cute.Tensor | torch.Tensor


@cute.kernel
def elem_add_kernel(a: cute.Tensor, b: cute.Tensor, out: cute.Tensor):
    block_x, _, _ = cute.arch.block_idx()
    block_dim_x, _, _ = cute.arch.block_dim()
    thread_x, _, _ = cute.arch.thread_idx()

    i = block_x * block_dim_x + thread_x

    if i < out.shape[0]:
        out[i] = a[i] + b[i]


@cute.jit
def elem_add(a: Tensor, b: Tensor, out: Tensor):
    n = out.shape[0]
    threads_per_block = 128
    blocks = (n + threads_per_block - 1) // threads_per_block

    elem_add_kernel(a, b, out).launch(
        grid=(blocks, 1, 1),
        block=(threads_per_block, 1, 1),
    )

The elem_add_kernel function is the kernel. Each thread computes one output element. The global element index i is computed from the thread block index, the number of threads in the block, and the thread index inside the block:

i = block_x * block_dim_x + thread_x

The elem_add function computes the number of thread blocks needed to cover the output tensor and launches the kernel with a one-dimensional thread block grid.

This example is pedagogical, not optimized. Even so, it shows a good basic access pattern: adjacent threads read adjacent elements of a and b, then write adjacent elements of out. That is the pattern needed for coalesced accesses to global memory; see memory coalescing.

Layout concerns are one reason why CuTe DSL is useful for high-performance kernels. Engineering for performance is difficult because kernels must be closely mapped to hardware: which threads handle which data, how memory is accessed, how work is tiled, and which hardware operations the generated code should use. CuTe DSL allows programmers to express these mappings explicitly while reusing much of the same kernel code across a variety of shapes and Streaming Multiprocessor architectures.

This may be surprising to performance-focused engineers from other domains -- how can a program written in an interpreted language like Python hope to compete with programs written in compiled languages?

The answer is that CuTe DSL kernels are compiled, Just-In-Time (JIT). Python source code is converted to an abstract syntax tree (AST), traced with proxy arguments, and then compiled. Note that only a subset of Python semantics are supported in JIT-compiled code.

At time of writing, in CUTLASS 4.x, the compilation stack passes through Multi-Level Intermediate Representation (MLIR) to the PTX IR to device-specific SASS before being executed.

Consider the FlashAttention-4 kernels. Our writeup of the open source code walks through how it uses pipelined warp specialization, Tensor Core operations, and Tensor Memory & Tensor Memory Accelerator operations to achieve state-of-the-art performance directly from CuTe DSL.

For more details on CuTe DSL, see NVIDIA's CuTe DSL documentation and CuTe DSL overview blog.

相关词条

本词条改编自 Modal GPU Glossary(CC BY 4.0)· 中文翻译 miter6/gpu-glossary-zh,MAE 整理排版。