得物技术 01月13日
Triton-Lang在Transformer优化加速中的实践 | 得物技术
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

Triton是由OpenAI推出的,以Python为基础的深度学习高性能计算编程语言和编译器。它旨在简化GPU编程,降低优化门槛。Triton起初基于CUDA开发,为不熟悉CUDA的开发者提供了快速编写高效GPU内核的方案。随着发展,Triton开始支持AMD的ROCm等其他芯片和工具,力求构建一个“CUDA-free”的内核编写方案。Triton的关键在于SM层级的并行处理设计,用户只需关注顶层逻辑,而底层的调度细节则由编译器自动优化。与CUDA相比,Triton降低了编程难度,同时保持了较高的性能,使得高性能计算更加普及。

🚀Triton是OpenAI推出的,以Python为基础的深度学习高性能计算编程语言和编译器,旨在简化GPU编程,降低高性能优化的门槛。

⚙️Triton的编程关键在于SM层级的并行处理设计,用户只需关注顶层逻辑,如矩阵分片、SM间数据同步等,底层的调度细节则由编译器自动优化。

📈Triton的性能通常优于普通CUDA内核,并且在某些情况下能与顶级CUDA算子媲美,同时显著降低了编程门槛,使得高性能计算更加普及。

💡Triton不仅支持Nvidia的CUDA,还逐渐支持AMD的ROCm等其他芯片和编程工具,力求构建一个“CUDA-free”的内核编写方案,打破CUDA的垄断。

原创 xujiong 2025-01-13 18:31 上海

Triton是OpenAI 推出的以python为编程语言基础,专门为深度学习研发和高性能计算而设计的编程语言和编译器,旨在简化和优化GPU编程的复杂操作,降低高性能优化的门槛。

目录

一、前言

二、GPU 基础

三、Triton 编程实例

    1. 向量求和

    2. 矩阵乘法

    3. 旋转位置编码

四、总结



前 言

众所周知,英伟达(Nvidia)自2006年推出CUDA以来,经过近20年的发展,尤其是经历了以卷积为代表的深度学习和近两年以Transformer为基础的LLM的推动,CUDA编程基本上成为了GPU编程的代名词。CUDA作为GPU的编程语言,不仅使用户能充分发挥Nvidia GPU的高性能的并行计算能力,也逐渐构筑了一个包括硬件、驱动、开发库和编程技巧的完备生态链,从而使CUDA成为了人工智能、高性能计算和云计算中的核心依赖。 

(图片来源:Triton-lang documentation )


Triton是OpenAI 推出的以python为编程语言基础,专门为深度学习研发和高性能计算而设计的编程语言和编译器,旨在简化和优化GPU编程的复杂操作,降低高性能优化的门槛。


在大模型推理优化领域,已有很多优秀的工作开始应用Triton编写高效算子,例如近期被众多大模型推理框架集成的Attention算子FlashAttention、推理加速框架lightllm、训练加速框架的Unsloth等。


Triton的初期版本以CUDA为起点而开发,为没有CUDA基础的编程者提供快速编写高效CUDA kernel的方案,而随着迭代已逐渐支持其他芯片和编程工具,如AMD的ROCm,并在继续支持其他的芯片,如Intel的CPU。因而,除了简化高性能计算,同时Triton也在试图构建一个“CUDA-free”的更高层的kernel编写方案,打破“天下苦CUDA久矣”的局面,把复杂的对底层芯片的交互,交给其IR和底层的编译器。


综上,可以说Triton是起于CUDA,又不止于CUDA。几个词可以简单总结Triton的特点和发展方向:



GPU基础

在学习Triton的编程设计前,还是需要了解GPU一些简单的基础架构知识和GPU编程的基础概念。


以下左图是引自NVIDIA经典Ampere架构的GA100(A100)的datasheet的整体架构示意图,展现其所有128个SMs(Streaming Multiprocessors)和各级缓存、HBM(高性能内存)和NvLink(Nvidia卡间互联)等;而右图是A100的单个SM(Streaming MultiProcessor, 多核流处理器) 的结构。 


(图片来源:Nvidia-ampere-architecture-whitepaper )


从硬件的角度来讲,



从软件的角度来讲,



对应关系:


 (图片来源:OpenAI official introduction )


将上述结构大致抽象成3个组成部分DRAM, SRAM和ALU, 其中DRAM即各个HBMs(即俗称的显存),SRAM指各级缓存,ALU即计算单元(GPU中的SM),而当用户优化CUDA代码时需要考虑:



而在编程设计时充分考虑以上,即使是对于富有经验的CUDA编程者也颇具挑战,因而Triton希望底层编译器对多数的调度细节能自动优化,而用户只需要考虑一些顶层的逻辑设计,即SMs层级的,例如矩阵分片,SM之间数据同步等问题。


其官网介绍给出了一个对比,

(表格来源:OpenAI official introduction)


通俗而言,相比于CUDA,使用Triton,你不必控制所有内容,因为有些事情可以留给工具自动优化;用Triton编写的模块可能不一定优于顶级的CUDA算子,但是性能通常能优于普通的CUDA kernel;而前者的门槛大大低于后者。


因而Triton的编程设计过程,其关键在于SM层级的并行处理过程的设计,即画好SM层级的网格图以表示算子的计算过程。 


Triton 编程实例

向量求和

内核函数

向量求和对于Triton是一个"Hello World"式的示例。使用Pytorch,对于两个同长度的vector,直接相加,非常简单。

size = 1024x = torch.rand(size, device='cuda')y = torch.rand(size, device='cuda')output_torch = x + y

而对于Triton,需要编写一个内核函数(kernel)和一个调用函数(wrapper),调用时的并行网格图如下:

kernel 函数代码如下:

import triton.language as tl
@triton.jitdef add_kernel(x_ptr, # 第一个输入向量的指针 y_ptr, # 第二个输入向量的指针 output_ptr, # 输出向量的指针 n_elements, # 向量长度 BLOCK_SIZE: tl.constexpr, # 每个线程块处理的元素数量 ): # 有多个'程序'处理不同的数据, 用pid标识当前是哪个程序 pid = tl.program_id(axis=0) # 计算当前程序所需要的数据的偏置 block_start = pid * BLOCK_SIZE offsets = block_start + tl.arange(0, BLOCK_SIZE) # 创建一个掩码以防止内存操作超出范围 mask = offsets < n_elements # 从 DRAM 加载 x 和 y x = tl.load(x_ptr + offsets, mask=mask) y = tl.load(y_ptr + offsets, mask=mask) output = x + y # 将计算结果output写回 DRAM tl.store(output_ptr + offsets, output, mask=mask)


线程块在GPU的计算模型里又被称为CTA(Cooperative Thread Array),以上的计算过程相当于一个CTA处理单个block。


而当缓存受限时,我们也可以在单个CTA中处理多个blocks, 如下图和相应的写法:

@triton.jitdef add_kernel(x_ptr, y_ptr, o_ptr, n_elements, num_blocks_per_CTA, BLOCK_SIZE: tl.constexpr,):    pid = tl.program_id(axis=0)      program_offsets = pid * num_blocks_per_CTA * BLOCK_SIZE     offsets = program_offsets + tl.arange(0, BLOCK_SIZE)        for i in range(num_blocks_per_CTA):        mask = offsets < n_elements        x = tl.load(x_ptr + offsets, mask=mask)        y = tl.load(y_ptr + offsets, mask=mask)        output = x + y        tl.store(o_ptr + offsets, output, mask=mask)        offsets += BLOCK_SIZE


接口函数

有了内核函数,我们需要再写一个wrapper,就可以调用内核(好比Pytorch的torch.Add api, 即加号"+")。 

def add(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:    output = torch.empty_like(x)    assert x.is_cuda and y.is_cuda and output.is_cuda    n_elements = output.numel()    # SPMD启动网格,表示并行运行的内核实例数。    grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE']), )    # 调用内核函数    add_kernel[grid](x, y, output, n_elements, BLOCK_SIZE=1024)    # 我们返回一个指向z的句柄,但是,由于`torch.cuda.synchronize()`尚未被调用,内核此时仍在异步运行。    return output
torch.manual_seed(0)size = 98432x = torch.rand(size, device='cuda')y = torch.rand(size, device='cuda')output_triton = add(x, y)

这里需要注意两点:


性能测试

Triton自带性能测试函数,可以帮助衡量自己设计的算子与baseline之间的差距。装饰器@triton.testing.perf_report用于装饰benchmark函数,而triton.testing.Benchmark函数定义了plot折线图的属性,在benchmark函数里面我们可以定义指标来比较不同算子之间的性能,如Triton和Pytorch算子之间在不同size计算下的吞吐差距。 

@triton.testing.perf_report(    triton.testing.Benchmark(        x_names=['size'],  # Argument names to use as an x-axis for the plot.        x_vals=[2**i for i in range(12, 28, 1)],  # Different possible values for `x_name`.        x_log=True,  # x axis is logarithmic.        line_arg='provider',  # Argument name whose value corresponds to a different line in the plot.        line_vals=['triton', 'torch'],  # Possible values for `line_arg`.        line_names=['Triton', 'Torch'],  # Label name for the lines.        styles=[('blue', '-'), ('green', '-')],  # Line styles.        ylabel='GB/s',  # Label name for the y-axis.        plot_name='vector-add-performance',  # Name for the plot. Used also as a file name for saving the plot.        args={},  # Values for function arguments not in `x_names` and `y_name`.    ))def benchmark(size, provider):    x = torch.rand(size, device='cuda', dtype=torch.float32)    y = torch.rand(size, device='cuda', dtype=torch.float32)    quantiles = [0.5, 0.2, 0.8]    if provider == 'torch':        ms, min_ms, max_ms = triton.testing.do_bench(lambda: x + y, quantiles=quantiles)    if provider == 'triton':        ms, min_ms, max_ms = triton.testing.do_bench(lambda: add(x, y), quantiles=quantiles)    gbps = lambda ms: 3 * x.numel() * x.element_size() * 1e-9 / (ms * 1e-3)    return gbps(ms), gbps(max_ms), gbps(min_ms)
benchmark.run(print_data=True, show_plots=True, save_path="./")

在左图,我们可以看到在向量维度较小时,torch的计算更快,而当维度较大时,Triton算子的计算更快一些。


相比向量的计算可能无法体现自定义算子的优势,右图展示了Triton 官方教程中定义的矩阵乘法算子的性能,可以看到其和cuBLAS编写的算子相比已能够达到持平的性能。

Vector Add Triton vs. Pytorch


Matrix multiplication  Triton vs cuBLAS


调试

早期的Triton核函数开发不支持调试,目前已经支持pdb和各种python ide的断点调试,只需设置环境变量即可。

os.environ["TRITON_INTERPRET"]=1


矩阵乘法

两个矩阵相乘,


在Pytorch中,两个矩阵相乘可以直接以torch.matmul(A, B)计算得到。而进一步对其稍作优化,我们立刻能想到的通常是分块矩阵。用Pytorch表示具体的流程:

# Pytorchimport torchfrom typing import Tuple
@torch.jit.scriptdef block_matrix_multiplication(A: torch.Tensor, B:torch.Tensor, M: int, N: int, K: int, BLOCK_SIZE_M: int, BLOCK_SIZE_N: int, BLOCK_SIZE_K: int) -> torch.Tensor: C = torch.zeros((M, N), dtype=torch.float32) for m in range(0, M, BLOCK_SIZE_M): for n in range(0, N, BLOCK_SIZE_N): acc = torch.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=torch.float32) for k in range(0, K, BLOCK_SIZE_K): a = A[m : m+BLOCK_SIZE_M, k : k+BLOCK_SIZE_K] b = B[k : k+BLOCK_SIZE_K, n : n+BLOCK_SIZE_N] acc += torch.matmul(a, b) C[m : m+BLOCK_SIZE_M, n : n+BLOCK_SIZE_N] = acc return C
# 用法示例A = torch.rand(100, 100)B = torch.rand(100, 100)result = block_matrix_multiplication(A, B, 100, 100, 100, 16, 16, 16)


从以上逻辑可以看出,这是一个行主序(Row Major)的分块矩阵乘法顺序。


我们可以画出其网格图,以下橙色的blocks 表示单个CTA的计算过程。

按照网格图,将上述的计算过程改写为Triton的内核函数:

# Triton kernel@triton.jitdet matmul_kernel(A, B, C, M, N, K,     BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr):    # 2d grid    pid_m = tl.program_id(0)    pid_n = tl.program_id(1)    offsets_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)    mask_m = offsets_m < M    offsets_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)    mask_n = offsets_n < N        # 2d tile    acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)    for start_k in range(0, K, BLOCK_K):        offsets_k = start_k + tl.arange(0, BLOCK_K)        mask_k = offsets_k < K                a_ptrs = A + offsets_m[:, None]*K + offsets_k[None, :]        mask_a = mask_m[:, None] & mask_k[None, :]        b_ptrs = B + offsets_k[:, None]*N + offsets_n[None, :]        mask_b = mask_k[:, None] & mask_n[None, :]            a = tl.load(a_ptrs, mask=mask_a, other=0)        b = tl.load(b_ptrs, mask=mask_b, other=0)        acc += tl.dot(a, b)    c_ptrs = C + offsets_m[:, None]*N + offsets_n    mask_c = mask_m[:, None] & mask_n[None, :]    tl.store(c_ptrs, acc, mask = mask_c)     # grid = (tl.cdiv(M, BLOCK_M), tl.cdiv(N, BLOCK_N), 1)

以上代码,我们关注几点:



行主序和列主序的代码和计算顺序如下,虽说CUDA是并行计算的程序,但是当我们将矩阵分为很多的程序执行时,如果我们的GPU并没有足够的SM来同时执行所有程序,因而这些程序是先后被加载入SM计算的。而CUDA默认是以列主序存储数据的,所以有时候列主序的程序性能要优于行主序。


for m in range(0, m, BLOCK_M):    for n in range(0, n, BLOCK_N):         CTA(m, n) ....

for n in range(0, N, BLOCK_N):    for m in range(0, M, BLOCK_M):         CTA(m, n) ....


而Triton的官网给出了一个基于L2 cache 优化的方案。其思路是以减少访存次数来提高cache的命中率。我们可以从下图比较其与通常的乘法算子的区别。

通常的列主序(column-major-ordering)

分组后的列主序 (grouped-column-major-ordering)


从上可以看出,左边计算4个CTA时,需要读取1列和4行,总共要进行5次读取;而右边的操作,只需要读取2列和2行,共4次读取。实际计算中,矩阵的行列维度数值都较大,分组后的计算在访存上会有一定的优化,而实际中在例如A100的硬件上这样的优化也能有10%的提升。


以下是官网优化示例给出的核心代码,相比于上述的二维索引,引入group之后采用一维索引,而代码的本质则是将这个一维索引pid转化为二维索引(pid_m, pid_n),而在这个变化中,我们重新定义了结果矩阵的计算顺序(即上图,右图中区别于左图的元素计算顺序)。

pid = tl.program_id(axis=0)num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)num_pid_in_group = GROUP_SIZE_M * num_pid_ngroup_id = pid // num_pid_in_groupfirst_pid_m = group_id * GROUP_SIZE_Mgroup_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)pid_m = first_pid_m + (pid % group_size_m)pid_n = (pid % num_pid_in_group) // group_size_m

我们用两个9x9的矩阵相乘来说明这个索引的过程:



旋转位置编码

旋转位置编码(RoPE, Rotate Position Encoding)是Transformer 进入大模型应用时代后的重要算子,在Llama,ChatGLM 等主流的大模型中都有应用。关于旋转位置编码的原理和作用可以参考原论文和作者博客。其计算过程可以简要表示成以下的旋转变换,


以下是一个Huggingface中Llama RoPE的前向计算流程。



以上是计算cos和sin两个旋转变换矩阵的过程,而矩阵q和k在做注意力乘法前先做简单处理。

def rotate_half(x):    """Rotates half the hidden dims of the input."""    x1 = x[..., : x.shape[-1] // 2]    x2 = x[..., x.shape[-1] // 2 :]    return torch.cat((-x2, x1), dim=-1)

最后是旋转变换:

q_embed = (q*cos) + (rotate_half(q)*sin)k_embed = (k*cos) + (rotate_half(k)*sin)

我们来看Triton kernel对前向过程的实现:


给定矩阵Q,Cos,Sin, 它们的维度分别为

是注意力模块中的head数,为方便说明,可令b=1;需要实现的计算过程是

为计算方便,现将reshape为(n,hd)。


考虑并行的维度时,按 token 并行是首先能想到的,n_rows = n,再考虑到有限的缓存,可以将另一个维度按group分,可以定义一个常量GROUP_SIZE=4(当然这里也可以设计成autotune模式,自动选择合适值),然后可以计算得到embedding维度的group数量n_groups ,并定义我们的网格 grid

div, mod = divmod(n, GROUP_SIZE)n_groups = div + (mod != 0)grid = (n_rows, n_groups)

下图阐释了整体和单元的计算过程。并行程序按两个维度计算,每个CTA的计算过程中有个次数为GROUP_SIZE的循环累加,各自累加计算得到q*cosrotate_half(q)*sin,再相加。

def _rope_embedding(    Q,     Q_row_stride,    cos, cos_row_stride,    sin, sin_row_stride,    seqlen,    head_dim      : tl.constexpr,    n_heads       : tl.constexpr,    BLOCK_SIZE    : tl.constexpr,):    """        Calculates the RoPE Embedding quickly        RoPE is Q * cos + rotate_half(Q) * sin        See our blog post for more info    """    GROUP_SIZE = 4    row_position  = tl.program_id(0)    group_head_position = tl.program_id(1)    col_offsets  = tl.arange(0, BLOCK_SIZE)    half_head_dim = head_dim // 2    mask = col_offsets < half_head_dim
sin1 = tl.load(sin + (row_position % seqlen)*sin_row_stride + \ half_head_dim*0 + col_offsets, mask = mask, other = 0) cos1 = tl.load(cos + (row_position % seqlen)*cos_row_stride + \ half_head_dim*0 + col_offsets, mask = mask, other = 0)
head_start = group_head_position * GROUP_SIZE head_end = min((head_start + GROUP_SIZE), n_heads)
for k in range(head_start, head_end): offs_q1 = row_position * Q_row_stride + k * head_dim + col_offsets offs_q2 = row_position * Q_row_stride + k * head_dim + col_offsets + half_head_dim
Q1 = tl.load(Q + offs_q1, mask = mask, other = 0).to(sin1.dtype) Q2 = tl.load(Q + offs_q2, mask = mask, other = 0).to(sin1.dtype)
tl.store(Q + offs_q1, Q1*cos1 - Q2*sin1, mask = mask) tl.store(Q + offs_q2, Q2*cos1 + Q1*sin1, mask = mask)


总 结

参考Triton的官方示例、以及其他社区的开源加速工具,我们还能看到其他许多算子,诸如rmsNorm, Softmax, Flash-Attention 等的具体加速方案和并行思路,以及他们的反向传导过程。而这些算子则组成了Transformer 的注意力模块的整体结构。通过对各个模块的并行优化,可以实现对注意力计算的推理和训练加速。

Transformer 的注意力结构及相关算子


在实际应用中,我们可以自己编写和优化自定义算子,也可以引用社区优秀的开源加速算子库,而且Pytorch在2.0+版本后已将Triton集成到其编译器,用torch.compile()可以直接对已加载模型编译,帮助自动优化可优化的算子。


参考:


往期回顾


1. 2024 Top 10文章推荐|得物技术

2. RAG应用在得物开放平台的智能答疑的探索  

3. 得物基于AIGC生成测试用例的探索与实践

4. 软件开发领域变革前夕-AI智能编码的发展|得物技术

5. Java程序中的潜在危机: 深入探讨NullPointerException|得物技术


文 / xujiong


关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

扫码添加小助手微信

如有任何疑问,或想要了解更多技术资讯,请添加小助手微信:


阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Triton GPU编程 深度学习 高性能计算 CUDA
相关文章