1、概念

OpenAI Triton是一个开源的、类似Python的编程语言和编译器。

(1)Program

在CUDA中,程序员通常需要管理Thread(线程)和Block(线程块),一个Block包含多个Threads。在Triton中,编程粒度是Block,Triton中的Program在概念上等价于CUDA中的Thread Block(线程块)。

这意味着你不需要在Triton代码中考虑一个Block内部有多少个Thread,也不需要管理Warp或线程同步的底层细节。Triton编译器会自动处理Block内部的线程调度和并行化。你只需要把注意力放在一个Program(Block)应该做什么上。

你可以把Triton中的Program理解为:

  • 执行单位:它是GPU上调度的一个工作单元
  • 逻辑块:它封装了一组相关的计算任务
  • 自动并行:你编写代码时,只需关注单个Program的逻辑。当你启动网格时,Triton会自动创建多个Program实例来并行处理大规模数据

一句话概括:Program是Triton中一个独立的、并行的执行单元(相当于CUDA线程块),它负责处理数据的一个分片。

pid = tl.program_id(axis=0)

pid(Program ID):这是当前正在执行这段代码的Program的唯一标识符。

分工逻辑:因为有多个Programs同时运行,你需要通过pid来区分它们。例如,pid=0的Program处理数据的前64个元素,pid=1的Program处理接下来的64个元素。

(2)1D Launch网格

Launch:在GPU计算中,Launch指的是启动(调度)一组并行执行的实例。

Grid(网格):指的是一组并行执行的Program(在CUDA术语中通常对应一个线程块Block)的集合。

1D:意味着这个集合被组织成一个一维数组。如果Grid的大小是N,那么它包含N个并行的Program。这些Program在逻辑上是线性排列的,因此只需要一个整数索引(即pid)就可以唯一标识其中的任意一个Program。

1D Grid:一个包含N个Program的线性队列。

(3)axis

axis参数用于定义索引的维度。

Triton的program_id函数支持三维网格(3D Grid),因此你需要指定你要获取的是那个轴上的ID。

axis=0:对应X轴,代表线性序列中的位置。

axis=1:对应Y轴,例如处理矩阵,这代表行号。

axis=2:对应Z轴,例如处理体积数据或复杂的滨兴层次,这代表深度或层号。

tl.program_id(axis=0)

返回当前正在执行该代码的Program在X轴上的ID。

数据映射:由于是一维映射,这个ID直接用于计算数据的偏移量(Offset)。公式:block_start = pid * BLOCK_SIZE,这意味着ID为0的Program处理数据的头部,ID为1的处理下一截,以此类推。

(4)利用arange生成块内偏移向量

在GPU并行计算中,为了让一个Program(处理单元)能够批量处理数据,我们需要快速生成该批次数据的内存索引。

全局偏移量 = 当前块起始位置 + 块内局部索引模板

例如:offsets = block_start + tl.arange(0, BLOCK_SIZE);

  • block_start:定位,这是当前Program负责的数据块在全局数据中的起始地址,由pid*BLOCK_SIZE计算得出。如果当前是第3个Program(pid=2),BLOCK_SIZE=4,那么block_start = 8。
  • tl.arange(0, BLOCK_SIZE):模板。它生成一个从0开始的,长度为BLOCK_SIZE的一维张量,这代表了块内的相对位置。例如,如果BLOCK_SIZE=4,这里生成[0, 1, 2, 3]。
  • offsets:结果。通过标量加向量(广播机制),将模板移动到正确的位置,得到实际要操作的全局索引。计算:8 + [0, 1, 2, 3],结果:[8, 9, 10, 11]。

为说明要这样写?

  • 向量化操作:GPU擅长同时处理大量数据,生成[8, 9, 10, 11]这个向量后,后续的tl.load指令可以一次性将这4个内存地址的数据加载到寄存器中,而不是循环4次单次加载。
  • 抽象层次:arange提供了一种简洁的方式来描述一段连续的内存,而不需要手动编写循环。

(5)边界检查掩码

作用:解决数据总量不是BLOCK_SIZE整数倍时的越界访问问题。

在并行计算中,我们通常将数据分成固定大小的块(BLOCK_SIZE)来处理。但如果总数据量N不能被块大小整除,最后一个块的后面就会出现空位。如果不加处理,程序可能会尝试去读取这些空位的数据(野指针),导致程序奔溃或结果错误。

逻辑原理:将计算出的全局偏移量数组offsets与总元素数量进行逐元素比较。True(1)代表该位置的索引是合法的,在数组范围内。False(0)代表该位置的索引超出了数组长度,是非法的。这个布尔数组随后会被传入tl.load或tl.store,告诉硬件:只对True的位置进行操作,False的位置忽略。

举例说明:

假设向量总共有n_elements=10个元素,设定块的大小为BLOCK_SIZE=4。理论上需要10/4=2.5个块,但在计算机中必须向上取整,所以需要启动3个Program来确保处理完所有的数据。在第3个Program(pid=2)中,

block_start = 2 * 4 = 8

offsets = [8, 9, 10, 11],

但是数组只有10个元素,但这个生成了索引10和11,这就是"越界"。

mask = offsets < n_elements,则mask = [True, True, False, False]

当执行tl.load(x_ptr + offsets, mask=mask]时,硬件会去读取索引8和9的数据,而忽略10和11的读取请求。结果:程序安全地读取了最后2个有效数据,同时完美避开了非法内存访问,程序不会奔溃。

(6)JIT即时编译

JIT(Just-In-Time Complilation)即时编译,是一种在程序运行时(Runtime)才将代码编译成机器码的技术。

与静态编译、解释执行的区别:

  • 静态编译:就像写C++程序,写完代码后必须先编译成一个.exe文件,然后再去运行它。编译和运行是两个分开的阶段。
  • 解释执行:就像传统的Python,代码写完后执行运行,解释器一边读代码一边执行,不需要编译步骤,但速度通常较慢。
  • JIT:结合了两者的有点。它看起来像是直接运行代码,但在后台,它会把那些频繁执行的"热点代码"抓出来,编译成高效的机器码,然后以后就直接运行这段编译好的机器码。

举例:

@triton.jit
def add_kernel(x_ptr, y_ptr, output_ptr, n_elements, BLOCK_SIZE: tl.constexpr):
    # ... (计算逻辑)

当第一次调用add_kernel时,Triton并不会直接用Python解释器去慢吞吞地执行它。

Triton会捕获这段Python代码,根据传入地参数(如BLOCK_SIZE)和当前地硬件环境(GPU架构),在运行时动态生成对应的GPU汇编代码。

这段生成的机器码会被缓存起来。

下次再调用这个函数时,如果参数没变,Triton直接执行编译好的GPU代码,速度极快。

(7)GPU核心概念

1)SM:流多处理器,Streaming Multiprocessor

SM是GPU的核心计算单元,也是硬件上的并行处理核心,它是一个包含多种资源的独立处理区块。

2)线程块(Thread Block)

线程块是软件编程模型中的逻辑组织单元,也是GPU调度的最小资源分配单位。

3)线程(Thread)

线程是GPU上最基础的执行单元,也是指令执行的最小实体。

4)Warp(线程束)

Warp是GPU硬件调度和执行的基本单位,包含一组被强制同步执行的线程。

5)寄存器(Register)

寄存器是位于SM内部、由每个线程私有的高速存储单元,属于片上存储资源。

总结:

GPU由多个SM组成,

每个SM同时运行一个或多个线程块

每个线程块由多个线程组成

硬件将线程块切分为多个Warp进行调度

每个线程拥有私有的寄存器,而线程块内的线程共享 共享内存。

(8)TMA内核

TMA 内核指的是利用 NVIDIA Hopper 架构中 Tensor Memory Accelerator(张量内存加速器,TMA)硬件单元的 Triton 内核版本。TMA 是一种专用硬件单元,用于在全局内存和共享内存之间进行高效、多维、异步的数据传输。

TMA 的主要特点:

  • 硬件加速的数据移动:TMA 可以独立于计算核心执行数据拷贝,支持复杂的多维张量布局(如不同步长、转置、广播等),无需软件逐元素搬运。
  • 描述符驱动:用户通过创建张量描述符(tl.make_tensor_descriptor)指定源/目标地址、形状、步长和块大小,TMA 硬件自动计算地址并执行传输。
  • 异步与流水线:TMA 操作可以异步执行,允许计算与数据移动完全重叠,提高利用率。
  • 简化代码:开发者只需定义描述符并调用 load/store,无需手动管理指针偏移和循环,降低编程复杂度。

在代码中的应用:

  • 内核 grouped_matmul_tma_kernel 使用 TMA 描述符加载 A、B 矩阵块,并存储 C 矩阵块,替代了手动指针计算和 tl.load/tl.store。
  • 主机端通过 triton.set_allocator 设置专用分配器,为 TMA 描述符分配元数据内存。
  • TMA 内核需要 GPU 支持(计算能力 ≥ 9),由 supports_tma() 检查。

tl.make_tensor_descriptor是Triton中用于创建TMA(Tensor Memory Accelerator)张量描述符的函数。它描述了全局内存中一个张量的布局信息,使得TMA硬件能够自动、高效地执行多维数据块地异步加载和存储。返回一个描述符对象,该对象具有:

-load(coord)

-store(coord, data)

工作原理:

  • 描述符创建:在编译时,Triton根据shape、strides和block_shape生成元数据,并在全局内存中分配TMA描述符所需地数据结构
  • TMA传输:执行load或store时,Triton会向TMA硬件发出异步传输请求。TMA硬件根据描述符中的信息自动计算每个块在内存中的地址,并执行数据移动,而无需占用计算核心。
  • 同步:TMA传输是异步的,通常需要与计算流水线配合。

(9)持久化内核

一种GPU内核设计模式,其中每个线程块在完成任务后不会立即退出,而是继续获取新的任务,直到所有工作完成。这样可以减少内核启动开销,提高资源利用率。

与普通内核每个线程块只处理一个输出块不同,持久化内核中每个线程块会处理多个输出块,直到完成所有工作。

【为什么需要持久化内核】

1)负载均衡

在普通内核中,如果某些块计算快、某些慢,快的线程块早早结束,SM资源闲置,而慢的块还在运行。持久化内核让快的线程块继续处理更多工作,实现负载均衡。

2)隐藏内存延迟

通过让一个线程块连续处理多个块,计算和内存访问可以重叠,更好地隐藏内存延迟。

3)减少启动开销

启动更快的线程块,减少GPU调度器的负担。

【核心思想】

-网格大小 = min(SM数量, 总块数)

-每个线程块通过循环处理多个tile

-步长=NUM_SMS,均匀分配tile

【关键机制-双计数器】

tile_id: prologue计数器(当前计算)
tile_id_c: epilogue计数器(当前存储)
初始值:tile_id_c = start_pid - NUM_SMS
作用:保持 tile_id - tile_id_c = NUM_SMS 的偏移
效果:计算当前块的同时存储上一个块 → 流水线并行

【代码模板】

# 启动时
grid = min(NUM_SMS, total_tiles)

# 内核中
tile_id_c = start_pid - NUM_SMS
for tile_id in range(start_pid, total_tiles, NUM_SMS):
# 计算当前块
result = compute(tile_id)

# 存储上一个块
store(result_prev, tile_id_c)
tile_id_c += NUM_SMS
result_prev = result

(10)流水线级数num_stages

在软件流水线中,num_stages表示同时进行的不同阶段的数量,比如:

-Stage 0:正在计算当前块

-Stage 1:正在预加载下一个需要的块

-Stage 2:正在预加载下下个块

...

每个stage都需要在共享内存中保留一份数据块。

通过增加流水线级数,比如从num_stages=2提升到num_stages=4:

-更好的隐藏内存延迟:更多的预加载可以完全掩盖内存访问延迟

-更高的计算密度:计算单元可以持续工作,减少等待数据的时间

-最终实现更高的TFLOPS

(11)TensorDescriptor

Triton 中用于创建主机端 TMA(张量内存加速器)描述符的工具类,让 GPU 硬件知道如何高效访问张量数据。

核心代码:

from triton.tools.tensor_descriptor import TensorDescriptor

# 创建描述符(初始块形状用占位符)
a_desc = TensorDescriptor.from_tensor(a, [1, 1])

# 更新块形状(在自动调优钩子中)
a_desc.block_shape = [BLOCK_M, BLOCK_K]

# 传递给内核并使用
a = a_desc.load([offs_am, offs_k])     # 加载数据块
c_desc.store([offs_cm, offs_cn], c)    # 存储数据块

要点:

-作用:封装张量的元数据(指针、形状、步长、数据类型)

-可修改:block_shape可在内核启动前动态更新

-传递:作为参数传入Triton内核

-内存:描述符本身需在设备端分配内存

2、常用函数

(1)内核编程抽象

这些函数用于编写GPU内核代码,在@triton.jit函数内部使用。

函数/类型 作用 关键参数说明
@triton.jit 即时编译装饰器:将Python函数编译成GPU指令,而非Python字节码
@triton.autotune 自动调优装饰器:它启用了JIT编译时的启发式搜索。运行时会根据key指定的参数变化,从configs列表中测量并选择性能最优的配置。

configs:triton.Config对象的列表,每个Config代表一种具体的资源分配策略;

key:缓存的索引键,它告诉Triton什么时候应该重新进行调优测试,该值会出现在所服务的函数的入参中

tl.program_id(axis) 获取程序ID:获取当前执行实例(Program),在指定轴上的ID

1:X轴

2:Y轴

3:Z轴

tl.num_programs(axis) 获取总线程块数量
tl.arange(start,end) 生成连续索引:生成一个从start到end-1的向量,用于向量化操作 通常用于计算内存偏移量offsets
tl.range(start, end, step, num_stages=1) 控制循环的特殊函数。 num_stages:流水线阶段数,用于优化内存访问的延迟,控制循环展开(Loop Unrolling)和流水线(Pipelining)。
tl.load(ptr, mask) 加载数据:从显存加载数据到寄存器

ptr:指针偏移量

mask:掩码,防止越界访问

tl.store(ptr, value, mask) 存储数据:将数据写回显存 同上,配合mask确保写入安全
tl.constexpr 编译时常量:告诉编译器该变量在编译时已知,用于优化 通常用于块大小,BLOCK_SIZE等元参数
tl.where(condition,x, y) 三元条件选择:根据condition的真假,从x或y中选择对应的值
tl.rand(seed, offsets) 随机数生成:基于种子和偏移量生成

tl.atomic_cas(ptr, cmp, val)

原子比较并交换:原子性地读取ptr指向的值,与cmp比较,如果相等则将val写入ptr,最后返回读取的原始值。整个过程不可中断,保证线程安全。

ptr:要操作的内存指针

cmp:比较值(compare)

val:交换值(value)

返回值:ptr指向的原始值

tl.debug_barrier()

线程块同步屏障:阻塞当前线程块中的所有线程,直到所有线程都到达此屏障点。确保之前的内存操作对所有线程可见,主要用于调试和强制内存一致性

tl.atomic_xchg(ptr, val)

原子交换:原子性地将val写入ptr指向的内存位置,并返回该位置原来的值。整个过程不可中断,保证线程安全。 ptr:要操作的内存指针
val:要写入的新值
返回值ptr指向的原始值
tl.multiple_of(ptr, dims) 向Triton编译器提供指针的对齐信息。编译器可利用此信息优化内存访问,并省略边界检查,从而提升带宽利用率。

ptr:要提示的指针

dims:每个维度的对齐元素数

tl.max_contiguous(ptr, dims) 提示编译器 offs_am 中的元素是连续递增的(即相邻元素差为1),这有助于向量化加载。
tl.dot(a,b) 执行两个小矩阵的乘法。tl.dot(a,b):其中a的形状为[M,K],b的形状为[K,N],结果形状为[M,N]。tl.dot会编译为使用Tensor Core的指令,以极高性能完成矩阵乘累加。
tl.dot(a,b, accumulator) 计算 a @ b 并加上 accumulator,然后返回新值,硬件优化过乘加。
tl.make_tensor_descriptor(base_ptr, shape, strides, block_shape)  创建TMA张量描述符,用于硬件加速的多维数据块传输。 base_ptr:全局内存基地址;shape:张量整体形状;strides:每个维度的步长;block_shape:每次传输的块形状。

(2)启动配置与辅助

这些函数用于在Python主机端配合和启动内核。

函数/类型 作用 关键参数说明
triton.cdiv(a,b) 向上取整除法:计算需要启动的块数量 常用于计算Grid Size:cdiv(n_elements, BLOCK_SIZE)
triton.next_power_of_2(n) 计算并返回大于或等于给定正整数n的最小的2的幂次方
triton.Config(meta, num_stages, num_warps) 定义一个具体“候选方案”的类。

meta:kernel代码里用到的自定义参数,通过tl.constexpr定义的

num_stages/num_warps:编译/硬件参数

triton.set_allocator(alloc_fn) 设置全局内存分配器,覆盖 Triton 默认分配行为,用于自定义内存分配(如与 PyTorch 兼容,支持 TMA 描述符等)。 alloc_fn:用户定义的分配函数,需符合签名 (size: int, alignment: int, stream: Optional[int]) -> Any,返回一个持有 GPU 内存的对象(如 torch.Tensor),Triton 将从该对象提取设备指针。

(3)性能测试与基准

用于量化内核性能,生成可视化报告。

函数/类型 作用 关键参数说明
@triton.testing.perf_report 性能报告装饰器:自动生成性能图表 benchmark:传入一个Benchmark对象,定义图表样式和维度
triton.testing.Benchmark 定义基准测试的维度和样式
triton.testing.do_bench 精准测量运行实践:用于获取内核执行的耗时

lambda:kernel,要测试的代码段

quantiles:性能波动范围(如[0.5, 0.2, 0.8])

(4)torch函数

函数/类型 作用
torch.allclose

判断两个张量是否"数值上近似相等"的函数

通过设定容差范围,来判断两个张量的差异是否在可接受范围内

3、核心知识点

(1)@triton.jit和@torch.jit.script的区别

【@triton.jit】

-概述:这个更偏底层

-操作对象:指针、线程ID(program_id)、内存偏移量、块(Block)、束(Warp)。

-语言:虽然是Python语法,但写的是并行算法的逻辑。

【@torch.jit.script】

-概述:高层模型

-操作对象:张量(Tensor)、神经网络层(nn.Module)、损失函数

-语言:纯Pytorch的张量操作

(2)warmup功能

-主要功能:触发内核的编译和代码生成,但并不真正执行计算逻辑。

(1)触发编译:Triton的代码是即时编译(JIT)的。当第一次调用内核时,它需要把Python代码编译成GPU能听懂的机器码。warmup就是手动触发这个过程。

(2)生成元数据:通过这次预演,编译器就能算出这个内核到底长什么样,比如它用了多少个寄存器、占用了多少共享内存。

(3)不干活:虽然它模拟了启动内核的过程,但并不会真的去计算,这样可以节省时间。

-参数:

它接收的参数和最终运行内核时需要的参数完全一样的,目的是为了让编译器提前知道“我要处理什么样的数据”。

硬件调度的配置参数:

1)num_stages:流水线阶段数:告诉编译器在循环中预取多少份数据。值越大,内存访问延迟隐藏得越好,但也可能消耗更多寄存器。

2)num_warps:线程束数量。每个线程块(Block)包含多少个Warp,通常一个Warp包含32个线程,当num_warps=8时意味着这个Block有256个线程。

3)grid:网格配置。这是一个临时的占位符。

(3)GPU寻址引擎

对于行优先(Row-major)存储的2D张量X,元素X[i,j]的地址为:

X[i,j]=X_{base} + i * stride_{i} + j * stride_{j}

关键变量定义:

-pid_m, pid_n:程序块ID,确定当前处理的是哪一块数据

-tl.arange(size):生成块内局部索引向量[0,1,..., size-1]

-[:, None]:维度提升,将向量变为列向量(用于广播)

-[None, :]:维度提升,将向量变为行向量(用于广播)

寻址三步法:

-计算块基址:base_idx = pid * BLOCK_SIZE

-生成局部网格:利用广播机制,将行向量和列向量相加,生成二维偏移矩阵

-处理边界:使用%运算符处理尾部块,配合后续的mask确保计算正确性

(4)L1/L2缓存

特性 L1缓存 L2缓存
归属 SM私有,每个SM独自拥有 GPU共享,所有SM共用
速度 最快 较快
容量 几十KB 几MB到即时MB
类比 部门经理的私人文档架 公司的中央档案室

当GPU需要一个数据时,它会先看SM的L1缓存里有没有,没有就去查共用的L2缓存,如果还没有,才不得不去速度最慢的显存中获取数据。这个过程层层递进,旨在用最小的延迟获取所需的信息。

(5)L2缓存优化

1)核心问题

在矩阵乘法C=A * B 中,每个程序实例计算输出矩阵C的一个小块,计算顺序直接影响L2缓存命中率。

简单行主序的缺陷:按行依次计算完再换行,会导致计算新块时,之前加载的输入矩阵数据(A或B)无法复用,必须重新从慢速显存(DRAM)加载,造成带宽浪费。

2)分组列主序

采用分组策略,将计算任务分组(每组包含GROUP_SIZE_M行),组内按列优先顺序计算。

策略:先算完第一组内的所有列(竖着算完一竖条),再算下一组

效果:在计算一组任务时,其中一个输入矩阵(B)的数据可以被钉在L2缓存里重复利用,大大减少了对DRAM的访问次数。

数据对比:在9*9块矩阵的例子中,行主序需加载90个块,而分组列主序仅需54个块。

3)矩阵乘法的特殊性

在矩阵乘法中,决定性能瓶颈的往往不是输出矩阵C的写入顺序,而是输入矩阵A和B的数据复用效率。

行主序分组的缺陷:如果按行分组计算,虽然A矩阵的数据可能是连续复用的,但B矩阵的数据访问模式会变得非常糟糕(跳跃式访问),导致B矩阵的数据无法在缓存中停留,必须频繁从显存重新加载。

列主序分组的优势:通过按列分组(或列主序调度),可以保证在计算这一组任务时,B矩阵的一小块数据可以被锁死在L2缓存中,被反复使用。

(6)meta变量

使用方法:

grid = lambda meta: (triton.cdiv(n_elements, meta['BLOCK_SIZE']), )

meta是一个字典,它包含了当前正在尝试的编译配置参数。

它是由Triton的运行时系统自动传进来的。

它里面装着这次运行所有的tl.constexpr参数,比如meta['BLOCK_SIZE']就是当前尝试的块大小。

上面这段代码的含义:工头(lambda),请根据当前配置单(meta)上写的饭盒大小(BLOCK_SIZE),帮我算一下要把这5000个活干完,需要派多少组工人(grid)。记得不够整除时要多派一组(向上取整)。

这是一种延迟计算的策略,为了适应Triton在编译时进行的自动参数优化(AutoTuning)。

grid = lambda META: (META['NUM_SM'], ) 可以写成:grid = (META['NUM_SM'], )吗?

不能。

  • META是Triton自动调优时动态传入的配置字典,它在grid函数被调用时才有值,而grid=(META['NUM_SM'],)在定义时就会尝试访问META,此时META尚未定义,会导致NameError。
  • grid参数需要是一个可调用对象(函数),它接收自动调优配置META,并返回网格大小的元组。这样Triton才能在每次测试不同配置时,根据该配置中的NUM_SM值动态计算网格大小
  • 如果写成固定元组,则网格大小被硬编码,无法随自动调优变化,失去了自动调优的意义。

(7)num_warps & num_ctas

1)num_warps

warp数量。

定义:每个线程块(CTA)中包含的warp数量,它是GPU调度的基本单位。

作用:

-控制线程块并行度

-影响寄存器/共享内存分配

-平衡资源利用与延迟隐藏

2)num_ctas

CTA数量。

定义:每个程序实例启动的CTA(Cooperative Thread Array)数量

何时使用:

-大模型并行

-资源受限场景

-特殊优化策略

(8)Pytorch的ctx

ctx是torch.autograd.Function上下文对象,用于在前向传播和反向传播之间传递数据。

核心功能:

-保存张量:ctx.save_for_backward(),取数方式:ctx.saved_tensors

-保存任意Python对象,比如:ctx.BLOCK_SIZE = 1024

示例:
 

import torch
import torch.nn as nn

class MyFunction(torch.autograd.Function):
    @staticmethod
    def forward(ctx, input):
        ctx.save_for_backward(input)
        ctx.multiplier = 2
        return input * 2

    @staticmethod
    def backward(ctx, grad_output):
        input, = ctx.saved_tensors
        multiplier = ctx.multiplier

        grad_input = grad_output * multiplier
        return grad_input

class SimpleModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.weight = nn.Parameter(torch.tensor([3.0], requires_grad=True))

    def forward(self, x):
        x = MyFunction.apply(x)
        x = x * self.weight
        return x

def test_model(x_value=5.0, target_value=100.0):
    model = SimpleModel()
    x = torch.tensor([x_value], requires_grad=True)

    output = model(x)
    target = torch.tensor([target_value])
    loss = (output - target) ** 2

    loss.backward()

    print(f"\n梯度结果:")
    print(f"  x.grad = {x.grad.item()}")
    print(f"  weight.grad = {model.weight.grad.item()}")

    return x.grad.item(), model.weight.grad.item()

test_model(5.0, 100.0)

运行得到:
 

梯度结果:
  x.grad = -840.0
  weight.grad = -1400.0
(-840.0, -1400.0)

loss=(2\cdot x \cdot w - t)^{2}

x.grad=\frac{dloss}{dx}=2(2xw-t)\cdot 2w=-840

model.weight.grad=\frac{dloss}{dw}=2(2xw -t)\cdot 2x=-1400

【调用流程】

MyFunction.apply(x) 被调用
         ↓
PyTorch创建ctx对象
         ↓
PyTorch调用 MyFunction.forward(ctx, x)  ← 自动调用forward
         ↓
forward 执行计算,保存数据到ctx
         ↓
forward 返回结果
         ↓
PyTorch在计算图中注册backward函数
         ↓
    [前向传播结束]

loss.backward() 被调用
         ↓
PyTorch找到之前注册的backward
         ↓
PyTorch调用 MyFunction.backward(ctx, grad_output)  ← 自动调用backward
         ↓
backward 从ctx取出数据,计算梯度
         ↓
backward 返回梯度

(9)libdevice

概念:它是NVIDIA GPU的数学函数库,提供了各种优化的数学计算函数,比如sin、asin、平方根等。

主要特点:

-数据类型适配:同一计算有不同版本:__nv_asin(double型)和__nv_asinf(float型)。类似自动挡汽车,Triton会根据输入数据自动选择合适版本。

-使用方式:result=tl.extra.libdevice.asin(data)

优势:

-方便:只需记住一个函数名

-安全:避免选错函数的风险

-简洁:代码更易读、易维护

(10)triton.jit的launch_metadata参数

它是一个非常有用的钩子函数,用于为每次内核启动附加自定义的元数据。

launch_metadata 是 @triton.jit 装饰器的一个可选参数。它接受一个函数,该函数在内核启动时被调用,并返回一个字典。这个字典的内容会与内核的启动事件关联,并被性能分析器记录下来。

def launch_metadata(grid, kernel, args) -> dict

launch_metadata 是 Triton 提供的一个强大工具,允许开发者将自定义的元数据与内核启动关联起来,从而在性能分析中获得更丰富、更有意义的信息。_matmul_launch_metadata 函数精心构造了这些元数据,包括内核名称、矩阵维度、理论 FLOPs 和内存字节数,使得后续的性能报告能够直接显示每个内核的 TFLOPS 和带宽效率,极大地便利了性能优化工作。

(11)智能优化内存访问

在GPU编程中,内存访问的合并对性能至关重要。

当多个线程访问连续的内存地址时,硬件可以将这些访问合并成少数几次大宽度的传输,从而大大提高带宽利用率。

Triton编译器需要知道线程访问的索引模式,才能生成合并的加载指令。

multiple_of提示:如果编译器知道索引都是某个数的倍数,它可以推断出地址的对齐方式,从而使用对齐的加载指令给,这通常比非对齐加载更快。

max_contiguous提示:如果编译器知道索引是连续递增的,它可以生成向量化的加载指令,一次加载多个连续元素,减少指令数量并提高吞吐量。

向量化的加载指令是 CPU 和 GPU 等处理器中的一种指令,它允许一条指令同时加载多个数据元素到寄存器中。这些数据元素在内存中通常是连续的,并且具有相同的数据类型。向量化加载的核心思想是利用硬件的并行数据通道,一次处理多个数据,从而提高内存访问的效率。

(12)后处理子块Epilogue Subtiling技术

1)什么是后处理子块技术
它是一种在 GPU 矩阵乘法内核中使用的优化技术。
它将计算完成的一个输出块(tile)在写回全局内存之前,逻辑上拆分成多个更小的子块分别处理,以利用硬件的异步传输能力实现流水线并行。

2)基本工作流程

【计算阶段】
线程块计算完整的 BLOCK_M * BLOCK_N 输出块,结果累积在寄存器的 accumulator 中(此时数据分散在各个线程的寄存器中)。

【拆分阶段】

acc = tl.reshape(accumulator, (BLOCK_SIZE_M, 2, BLOCK_SIZE_N // 2))
acc = tl.permute(acc, (0, 2, 1))
acc0, acc1 = tl.split(acc)
  • 重塑 (Reshape):将累加器逻辑视图重塑为 (BLOCK_SIZE_M, 2, BLOCK_SIZE_N // 2) 的形状,引入“子块索引”维度。
  • 置换 (Permute):调整维度顺序(这是视图操作,不移动物理数据),将子块维度移至末尾,便于分割。
  • 分割 (Split):沿最后一个维度切分,得到两个独立的逻辑子块 acc0 和 acc1
    • 注:此时数据在寄存器中依然保持原有的分散分布,但编译器已将它们标记为两个独立的任务单元。

【存储阶段】

c0 = acc0.to(dtype)
c_desc.store([offs_am_c, offs_bn_c], c0)  # 启动左半块写入

c1 = acc1.to(dtype)
c_desc.store([offs_am_c, offs_bn_c + BLOCK_SIZE_N // 2], c1)  # 启动右半块写入
  • 分别将两个子块进行数据类型转换(如 fp32 -> fp16)。
  • 调用 TMA (Tensor Memory Accelerator) 指令,将子块直接写入全局内存。
  • 关键机制:TMA 硬件具备 Gather(收集) 能力,它能自动从分散在各线程寄存器中的数据中提取元素,组装成连续的内存事务写入全局内存,无需软件手动整理数据布局。

3)与传统方式对比

【无 Subtiling 方式】
计算完整块 -> 线程协作将数据写入共享内存(手动整理布局,需占用共享内存缓冲区) -> 从共享内存读取连续块 -> 写入全局内存。

  • 特点:串行执行,且占用宝贵的共享内存资源。

【Subtiling 方式】
计算完整块 -> 逻辑拆分子块 -> 直接从寄存器经 TMA 写入全局内存(跳过共享内存暂存)。

  • 特点:利用 TMA 的 Gather 能力省去中间缓冲,并开启流水线。

4)为什么必须拆分子块

【适配 TMA 的任务调度模型】

  • TMA 要求写入全局内存的目标地址必须是连续的矩形块,但其源数据可以来自分散的寄存器(通过硬件 Gather 自动收集)。
  • 拆分的核心目的不是改变寄存器内的物理布局,而是创造独立的任务边界。如果不拆分,编译器会将整个大块视为一个单一任务,导致“类型转换”和“内存写入”只能串行执行。

【数据类型转换与流水线重叠】

  • 瓶颈accumulator.to(dtype)(类型转换)是计算操作,store(内存写入)是 IO 操作。若一次性处理整个大块,通常需等待转换全部完成后才能开始写入。
  • 拆分后的优势
    • 将大块拆分为 acc0 和 acc1 后,编译器可生成两串独立的指令流。
    • 流水线效果:当硬件正在异步写入 acc0(慢操作)时,计算单元可以同时转换 acc1 的数据(快操作)。
    • 这种“计算”与“通信”的重叠(Overlap)显著降低了整体延迟。

5)总结

【核心价值】

  • 流水线优化:通过任务拆分,实现类型转换(计算)与 TMA 写入(通信)的并行重叠,掩盖延迟。
  • 消除输出缓冲区:利用 TMA 的 Gather 能力,直接从分散的寄存器写回全局内存,无需共享内存中转。
  • 释放共享内存:典型配置下可释放约 32KB 共享内存,可用于增加流水线级数或扩大 Block 尺寸。
  • 资源重分配:将节省的内存资源转化为更高的吞吐量。

【为什么必须拆分】

  • 激活指令级并行:只有拆分为独立子块,编译器和硬件调度器才能识别出可并行的任务,从而重叠执行转换与写入。
  • 适配 TMA 任务模型:将一个大任务拆解为多个标准的小任务,便于生成独立的 TMA 描述符,发挥硬件异步传输效能。
  • 优化资源使用:避免使用共享内存作为中间缓冲,减少显存带宽压力和片上内存占用。

【为什么拆分两块最合适】

  • 拆一块(不拆):只有一个任务,无法实现转换与写入的重叠,退化为串行执行。
  • 拆两块:理想的平衡点。既能实现充分的流水线重叠(一边传一边算),又保持了代码逻辑简单,寄存器压力可控。
  • 拆多块:虽然理论上重叠更细,但会增加指令调度开销、寄存器管理复杂度,且收益递减,性价比不高。

4、调用nvidia库

(1)from triton._C.libtriton import nvidia

功能:让Python代码可以访问Triton内部封装的NVIDIA库接口。

triton._C.libtriton:这是Triton的一个内部模块,提供了从Python直接调用Triton底层C++功能的桥梁。

nvidia:从该内部模块中导入一个名为nvidia的对象,这个对象包含了Triton对NVIDIA相关库(如cuBLAS、cuDNN等)的封装接口

(2)device_blas = nvidia.cublas.CublasLt(device_workspace)

nvidia.cublas.CublasLt:代表cuBLASLt的Python绑定,该库是NVIDIA cuBLAS库的一个轻量级扩展。

device_workspace:这个是预先分配好的GPU内存张量,用作cuBLASLt操作的工作空间。cuBLASLt在执行某些操作时可能需要临时缓冲区(比如存储中间结果或调优数据),这个工作空间就是提供给它的。

CublasLt(device_workspace):创建了一个cuBLASLt的实例,并将工作空间绑定给它。之后通过这个实例调用matmul等方法执行矩阵乘法。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐