8.8 KiB
8.8 KiB
Triton与CUDA算子
Triton概述
Triton是OpenAI开发的开源GPU编程框架,旨在简化高效GPU算子的开发,无需编写CUDA C/C++代码。
核心特点
| 特性 | 说明 |
|---|---|
| Python前端 | 使用Python编写kernel |
| 自动优化 | 自动生成高效CUDA代码 |
| Torch集成 | 与PyTorch无缝结合 |
| 自动融合 | 算子融合策略 |
与CUDA C/C++对比
CUDA C/C++:
- 完全控制,但开发周期长
- 手动管理shared memory、register allocation
- 需要处理硬件细节
Triton:
- 抽象掉硬件细节
- 自动优化内存访问模式
- 开发效率高,但灵活性略低
自动Kernel生成
Triton Kernel结构
import triton
import triton.language as tl
@triton.jit
def matmul_kernel(
A_ptr, B_ptr, C_ptr,
M, N, K,
stride_am, stride_ak,
stride_bk, stride_bn,
stride_cm, stride_cn,
BLOCK_SIZE: tl.constexpr
):
# 块级别的程序化内存访问
pid = tl.program_id(axis=0)
num_pid_m = tl.cdiv(M, BLOCK_SIZE)
pid_m = pid // num_pid_m
pid_n = pid % num_pid_m
# 初始化累加器
acc = tl.zeros((BLOCK_SIZE, BLOCK_SIZE), dtype=tl.float32)
# 计算指针偏移
offs_m = (pid_m * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)) % M
offs_n = (pid_n * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)) % N
offs_k = tl.arange(0, BLOCK_SIZE)
# 分块计算
for k in range(0, tl.cdiv(K, BLOCK_SIZE)):
# 加载数据到SRAM
a_ptrs = A_ptr + offs_m[:, None] * stride_am + (offs_k[None, :] * stride_ak)
b_ptrs = B_ptr + (offs_k[:, None] * stride_bk) + offs_n[None, :] * stride_bn
a = tl.load(a_ptrs)
b = tl.load(b_ptrs)
# 矩阵乘法累加
acc += tl.dot(a, b)
# 更新k指针
offs_k += BLOCK_SIZE
# 写回结果
c_ptrs = C_ptr + stride_cm * offs_m[:, None] + stride_cn * offs_n[None, :]
tl.store(c_ptrs, acc.to(tl.float16))
自动编译优化
# Triton自动编译配置
config = triton.Config(
num_warps=4, # 控制并行度
num_stages=2, # 流水线阶段数
block_size=128 # 块大小
)
# JIT编译
grid = (M * N // 1024,)
matmul_kernel[grid](A, B, C, M, N, K, **config)
算子融合策略
融合原则
融合收益 = 内存访问节省 - 计算额外开销
常见可融合模式:
1. MatMul + Softmax → Flash Attention
2. MatMul + Bias → Fused MatMul
3. Activation + Pointwise → 融合Activation
4. LayerNorm组件 → 单Kernel实现
融合示例:LayerNorm
@triton.jit
def layer_norm_kernel(
x_ptr, y_ptr, weight_ptr, bias_ptr,
N, eps,
BLOCK_SIZE: tl.constexpr
):
# 计算均值
x = tl.load(x_ptr + tl.arange(0, BLOCK_SIZE))
mean = tl.sum(x) / N
# 计算方差
x_mean = x - mean
var = tl.sum(x_mean * x_mean) / N
# 归一化 + 仿射
y = (x_mean / tl.sqrt(var + eps))
if weight_ptr is not None:
w = tl.load(weight_ptr + tl.arange(0, BLOCK_SIZE))
y = y * w
if bias_ptr is not None:
b = tl.load(bias_ptr + tl.arange(0, BLOCK_SIZE))
y = y + b
tl.store(y_ptr + tl.arange(0, BLOCK_SIZE), y)
融合收益分析
LayerNorm融合前(3个独立kernel):
- Mean计算: N次读 + N次写 + reduce
- Var计算: N次读 + N次写 + reduce
- Normalize: 读 + 算 + 写
融合后(单kernel):
- 读一次,计算全在SRAM完成
- 内存访问减少 ~66%
Flash Attention的Triton实现
Flash Attention Kernel
@triton.jit
def flash_attention_kernel(
Q, K, V, Out,
stride_qm, stride_qk,
stride_km, stride_kk,
stride_vm, stride_vk,
M, N, # M=seq_len_q, N=seq_len_k
HEAD_DIM: tl.constexpr,
BLOCK_M: tl.constexpr = 64,
BLOCK_N: tl.constexpr = 64,
BLOCK_DMODEL: tl.constexpr = 64
):
# 程序ID(行号)
row_id = tl.program_id(0)
# 加载Q块
q_offset = row_id * BLOCK_M * stride_qm
q = tl.load(Q + q_offset + tl.arange(0, BLOCK_M)[:, None] * stride_qm
+ tl.arange(0, BLOCK_DMODEL)[None, :])
# 初始化累加器
m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf") # 行最大值
l_i = tl.zeros([BLOCK_M], dtype=tl.float32) # 行归一化因子
acc = tl.zeros([BLOCK_M, BLOCK_DMODEL], dtype=tl.float32)
# 遍历K, V块
for start_n in range(0, N, BLOCK_N):
k = tl.load(K + start_n * stride_km +
tl.arange(0, BLOCK_N)[:, None] * stride_km +
tl.arange(0, BLOCK_DMODEL)[None, :])
v = tl.load(V + start_n * stride_vm +
tl.arange(0, BLOCK_N)[:, None] * stride_vm +
tl.arange(0, BLOCK_DMODEL)[None, :])
# 计算S = Q @ K^T
s = tl.dot(q, tl.trans(k))
# 更新m(行最大值)
m_new = tl.maximum(m_i, tl.max(s, axis=1))
# 计算P = exp(S - m_new)
p = tl.exp(s - m_new[:, None])
# 更新l(归一化因子)
l_new = l_i * tl.exp(m_i - m_new) + tl.sum(p, axis=1)
# 计算acc
acc_scale = l_i * tl.exp(m_i - m_new) / l_new
acc = acc * acc_scale[:, None] + tl.dot(p, v) / l_new[:, None]
m_i = m_new
l_i = l_new
# 写回
out_offset = row_id * BLOCK_M * stride_qm
tl.store(Out + out_offset + tl.arange(0, BLOCK_M)[:, None] * stride_qm
+ tl.arange(0, BLOCK_DMODEL)[None, :], acc)
性能对比
Flash Attention Triton vs CUDA原生:
硬件: A100
序列长度: 4096, heads=32, head_dim=128
| 实现 | TFLOPS | 显存占用 |
|------|--------|---------|
| CUDA C (手写) | 350 | 基准 |
| Triton | 340 | 基准 |
| 差距 | ~3% | 相当 |
Triton最佳实践
内存访问优化
@triton.jit
def optimized_matmul(
A, B, C,
M, N, K,
BLOCK_SIZE: tl.constexpr
):
# 预取策略:重叠加载和计算
# 使用多个stage的pipeline
# 向量化加载(提高带宽利用率)
# 假设BLOCK_SIZE=64, head_dim=64
# 使用 (64, 4) 的向量化访问
a = tl.load(A + offs, mask=offs < M * K, other=0.0, dtype=tl.float16)
算子融合判断
# 可融合的组合
fusable_patterns = [
("linear", "relu"), # Linear + Activation
("matmul", "add"), # Fused MatMul + Bias
("softmax", "matmul"), # Flash Attention基础
("layernorm", "add"), # LayerNorm + Residual
("rmsnorm", "matmul"), # RMSNorm融合
]
# 不可融合(需拆分)
non_fusable = [
"matmul + convolution", # 不同的内存访问模式
"reduce + elementwise", # 依赖关系复杂
]
配置选择
# 性能调优参数
config = {
"num_warps": 4, # 2/4/8,影响并行度和shared memory使用
"num_stages": 2, # 流水线深度,2适合short seq,3适合long seq
"BLOCK_SIZE": 128, # 影响occupancy
}
# 不同场景推荐
scenarios = {
"short_seq (<1024)": {"num_warps": 4, "num_stages": 3, "BLOCK": 64},
"medium_seq": {"num_warps": 4, "num_stages": 2, "BLOCK": 128},
"long_seq (>4096)": {"num_warps": 8, "num_stages": 2, "BLOCK": 64},
}
与其他框架的对比
| 框架 | 开发语言 | 学习曲线 | 性能 | 适用场景 |
|---|---|---|---|---|
| CUDA C/C++ | C++/CUDA | 高 | 最优 | 极致优化 |
| Triton | Python | 中 | 接近CUDA | 快速开发 |
| cuBLAS | - | 低 | 最优 | 通用MatMul |
| Cutlass | C++/CUDA | 高 | 最优 | 定制MatMul |
| TVM | Python/Relay | 中 | 接近CUDA | 自动优化 |
PyTorch集成
算子注册
import torch
from triton.ops import autotune
@autotune(
configs=[
triton.Config({'BLOCK_M': 64, 'BLOCK_N': 64, 'BLOCK_K': 32}),
triton.Config({'BLOCK_M': 128, 'BLOCK_N': 64, 'BLOCK_K': 32}),
],
key=['M', 'N', 'K']
)
@triton.jit
def matmul_kernel(...):
...
class MatMul(torch.nn.Module):
def forward(self, x, y):
return matmul_kernel(x, y)
Torch.compile集成
# PyTorch 2.0 torch.compile
model = model.compile(mode="reduce-overhead")
# Triton kernel自动使用torch.compile后端
# 无需额外配置
常见优化场景
| 场景 | 优化方法 | 效果 |
|---|---|---|
| Element-wise融合 | 编译时合并 | 减少kernel launch开销 |
| 矩阵分块 | 自动tile | 提高cache命中率 |
| 向量化访问 | 使用tl.load(vectorized) | 提高带宽利用率 |
| 算子重排 | 调整计算顺序 | 减少中间内存访问 |
注意事项
- Debug困难:Triton编译错误信息不直观
- 不支持所有操作:复杂控制流、某些特殊操作需回退到CUDA
- Occupancy限制:BLOCK_SIZE过大导致occupancy下降
- 数值稳定性:FP16计算需注意溢出问题
- Memory Alignment:数据对齐影响加载效率