在 AMD GPU 上开发 Triton Kernel

Developing Triton Kernels on AMD GPUs — ROCm Blogs

2024 年 4 月 15 日,作者: Clint Greene.

简介

OpenAI 开发了一种强大的、专注于 GPU 的编程语言和编译器,名为 Triton,它可以无缝地在 AMD GPU 上运行。Triton 的目标是使 AI 工程师和科学家能够用最少的专业知识编写高性能的 GPU 代码。Triton 内核之所以性能优越,是因为其块状程序表示,使其能够被编译成高度优化的二进制代码。Triton 还利用 Python 来进行内核开发,使其既熟悉又容易上手。并且,内核只需在定义前简单声明 triton.jit python 装饰器即可轻松编译。

在这篇博客中,我们将深入探讨如何开发 GELU(高斯误差线性单元)内核,并将其性能与 PyTorch 的类似实现进行基准测试。

先决条件

要运行本博文中的内容,你需要以下条件:

  • 一块 AMD GPU: 请参考兼容 GPU 列表

  • Linux: 请参考支持的 Linux 发行版

  • ROCm 5.7+: 请参考安装说明

开始

让我们先安装需要用到的库。如果你已经安装了 ROCm 6.0 和最新版本的 PyTorch,可以跳过这一步。然而,如果你在运行任何命令时遇到问题,我们建议使用 nightly wheels 进行更新。这也会安装与 ROCm 的 PyTorch 兼容的 Triton 版本。

pip install matplotlib pandas -q
pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/rocm6.0/ -q

并导入它们

import torch
import triton
import triton.language as tl

现在必要的库已经安装和导入,我们可以开始开发一个使用_tanh_近似 GELU 的 Triton 内核。但让我们首先讨论什么是 GELU 以及为什么我们需要为它开发一个内核。

GELU

在神经网络中,GELU(Gaussian Error Linear Unit)是最广泛使用的激活函数之一,因为它能够引入网络的非线性特性,其梯度在训练过程中平滑且稳定,具有计算效率且在深度学习应用中表现良好。GELU通常用于Transformer模型如GPT和BERT中,并且已经在各种计算机视觉任务中的卷积神经网络(CNN)中取得了优异的性能。GELU接受一个任意实值的向量_x_作为输入,并输出一个相同大小的向量。通过tanh函数近似的GELU定义如下:

GELU(x)≈x²[1+tanh(√(2/π)⋅(x+0.044715x³))]

我们可以很容易地在Triton中从头编写这个代码。

# GELU计算的常数
k = (2.0 / torch.pi) ** 0.5

@triton.jit  # Triton即时编译的装饰器
def gelu_kernel(
    output_buffer,
    input_buffer,
    input_row_stride,
    output_row_stride,
    n_cols,
    block_size: tl.constexpr,
):
    """
    使用Triton应用GELU的函数。

    参数
    ----------
    output_buffer (pointer): 存储输出张量数据的缓冲区指针。
    input_buffer (pointer): 存储输入张量数据的缓冲区指针。
    input_row_stride (int): 输入张量中行之间的步幅(跳过的元素数量)。
    output_row_stride (int): 输出张量中行之间的步幅(跳过的元素数量)。
    n_cols (int): 输入和输出张量中的列数。
    block_size (tl.constexpr int): 在编译时已知的用于高效处理张量的块大小。

    返回值:无,输出张量缓冲区就地修改。
    """
    # 根据程序ID确定起始行指针
    row_idx = tl.program_id(0)
    row_start_ptr = input_buffer + row_idx * input_row_stride

    # 为块处理生成一系列列偏移
    col_offsets = tl.arange(0, block_size)
    input_ptrs = row_start_ptr + col_offsets

    # 加载当前行的数据,处理潜在的越界元素
    row = tl.load(input_ptrs, mask=col_offsets < n_cols, other=-float('inf'))

    # 使用tanh近似值应用GELU激活函数
    t = (row + 0.044715 * row * row * row) * k
    numerator = 2
    denominator = (1 + tl.exp(-2 * t))
    gelu_output = 0.5 * row * (1 + (numerator / denominator) - 1)

    # 计算输出指针并存储计算出的GELU值
    output_row_start_ptr = output_buffer + row_idx * output_row_stride
    output_ptrs = output_row_start_ptr + col_offsets
    tl.store(output_ptrs, gelu_output, mask=col_offsets < n_cols)

在Triton中,还需要创建一个辅助函数,该函数使用所需的参数排队(kernel)。如果您需要有关参数和Triton中GPU编程的更多信息,请参阅 official docs。现在让我们开发我们的排队函数。

def gelu(x: torch.Tensor):
    n_rows, n_cols = x.shape

    # 确定用于并行处理元素的有效块大小
    block_size = triton.next_power_of_2(n_cols)

    # 根据块大小调整warp(线程组)的数量以获得最佳性能
    # 对于较小的块大小使用4个warp
    num_warps = 4
    
    # 对于较大的块,将warp 增加到8个
    if block_size >= 2048:
        num_warps = 8
    
    # 对于更大的块,将warp 增加到16个
    elif block_size >= 4096:
        num_warps = 16

    y = torch.empty_like(x)

    gelu_kernel[(n_rows,)](# Launch with a one-element tuple for grid size (number of rows)
      y,  # 输出张量
      x,  # 输入张量
      x.stride(0),  # 输入张量中行之间的步幅
      y.stride(0),  # 输出张量中行之间的步幅
      n_cols,  # 张量中的列数
      num_warps=num_warps,  # 并行执行的warp数量
      block_size=block_size,  # 高效处理的块大小
    )

    return y

在开发了Triton内核后,进行单元测试总是一个好主意。这将确保我们正确地编码了gelu近似,并且我们的内核能够处理不规则数量的行和列。

torch.manual_seed(0)
x = torch.randn(4097, 311, device='cuda')
y_triton = gelu(x)
y_torch = torch.nn.functional.gelu(x, approximate='tanh')
assert torch.allclose(y_triton, y_torch, rtol=1e-04,atol=1e-04), (y_triton, y_torch)

单元测试确认我们的内核正常工作。现在我们可以对内核进行基准测试并评估其性能。

基准测试

我们使用Triton的基准测试工具对Triton内核在不同大小的张量上的性能进行了基准测试,并将其与PyTorch内部的`gelu`函数的性能进行比较。

@triton.testing.perf_report(
    triton.testing.Benchmark(
        x_names=['N'],  # 用作x轴的参数名
        x_vals=[128 * i for i in range(2, 100)],  # d`x_name`的不同可能值
        line_arg='provider',  # 参数名,其值对应于图中不同的线
        line_vals=[
            'triton',
            'pytorch',
        ],  # `line_arg`的可能值
        line_names=[
            "Triton",
            "PyTorch",
        ],  # 线条标签名
        styles=[('blue', '-'), ('green', '-'), ('green', '--')],  # 线条样式
        ylabel="GB/s",  # y轴标签名
        plot_name="gelu-performance",  # 图的名称。也用作保存图像文件的文件名
        args={'M': 4096},  #函数参数`x_names`和`y_name`之外的参数值
    ))
def benchmark(M, N, provider):
    x = torch.randn(M, N, device='cuda', dtype=torch.float32)
    quantiles = [0.5, 0.2, 0.8]
    if provider == 'pytorch':
        ms, min_ms, max_ms = triton.testing.do_bench(lambda: torch.nn.functional.gelu(x), quantiles=quantiles)
    if provider == 'triton':
        ms, min_ms, max_ms = triton.testing.do_bench(lambda: gelu(x), quantiles=quantiles)
    gbps = lambda ms: 2 * x.nelement() * x.element_size() * 1e-9 / (ms * 1e-3)
    return gbps(ms), gbps(max_ms), gbps(min_ms)

benchmark.run(show_plots=True, print_data=True)

现在我们运行基准测试,看看性能表现。

benchmark.run(print_data=True, show_plots=True)

我们可以看到,我们的GELU内核(蓝色)在带宽上比PyTorch的GELU实现(绿色)领先多达10%。性能提升归功于在片上SRAM中手动融合操作,而不是从HBM DRAM多次读写。

总结

在这篇博客中,我们演示了如何开发一个Triton内核并对其性能进行基准测试。有关理解GPU编程更多详细信息和开发更高级融合内核的其他示例,请访问Triton的官方教程.

上一篇:vue3实现一个无缝衔接、滚动平滑的列表自动滚屏效果,支持鼠标移入停止移出滚动


下一篇:跨域问题以及使用vscode的LiveServer插件跨域访问