TLX Block Attention:为 Blackwell 架构量身定制的块稀疏注意力内核

2026-05-26 25 预计阅读时间:1 分钟
来源:pytorch.org AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:14 分钟

大模型推理和训练的算力瓶颈,很大一部分卡在 self-attention 的 O(n²) 复杂度上。块对角稀疏(block-diagonal sparse)注意力是一种实用的压缩策略——只在固定大小的对角块内计算注意力,其余位置直接跳过。问题在于:现有内核多为通用稀疏注意力设计,没有针对固定块模式做编译期优化,更没有利用新一代 GPU 的硬件特性。

Meta 近期开源的 TLX Block Attention 正是填补这个空位的工作:一个用 Triton 编写、面向 NVIDIA Blackwell (SM100) 的 warp-specialized 内核,把块对角模式的编译期信息榨干,配合 Blackwell 的 TMA(Tensor Memory Accelerator)和更大共享内存,跑出远高于通用方案的吞吐。

块对角稀疏:为什么值得专门做一颗内核

块对角稀疏注意力的核心思想很简单——把序列按固定块大小 B 切段,每个段只和同段 token 做注意力:

注意力矩阵形状 (N, N),块大小 B
只有 (i*B ~ (i+1)*B, i*B ~ (i+1)*B) 的对角块非零
其余位置 mask 为 0,不参与计算

这种模式在多个场景下天然出现:

  • 分组注意力:文档内段落、代码内函数、多模态中同帧 patch,组内交互远强于跨组。
  • MoE 路由后的专家内注意力:同一专家处理的 token 构成自然分组。
  • 长序列推理的滑动窗口近似:窗口大小等于块大小时,块对角就是滑动窗口的离散化。

通用稀疏注意力内核(如 FlashAttention 的变长 mask 版本)可以处理块对角,但代价不小:运行时需要传入完整的 mask tensor,动态判断每个块是否跳过,分支开销和内存占用都不低。而块对角模式的块大小和偏移在编译期就完全确定——如果内核能吃进这些信息,就能消除运行时分支、预计算所有地址偏移,甚至让不同 warp 在编译期就分配好各自的任务。

这就是 TLX Block Attention 的切入点。

Warp-Specialized 设计:把分工写进编译期

Blackwell 架构引入了 TMA 单元,可以让 warp 级线程发起异步内存拷贝,而不需要整个 warp group 协同搬运数据。TLX 利用这一特性,把注意力计算拆成三类 warp 角色:

Warp 角色 职责 Blackwell 特性利用
Q-Loader warp 通过 TMA 异步加载 Q 块到共享内存 TMA 单 warp 发起拷贝,释放其他 warp
KV-Loader warp 通过 TMA 异步加载对应 K、V 块 同上,与 Q warp 并行发起
Compute warp(s) 在共享内存中完成 QK^T、softmax、与 V 的乘法 依赖已就绪的共享内存数据,纯计算

关键点在于:因为块对角模式编译期已知,每个 warp 处理哪些块、每个块的 SMEM 地址偏移,都可以在编译期算好并内联到指令中。运行时不再需要 if block_is_active 的判断——所有 warp 直接按预分配的块列表执行,零分支开销。

这种分工也解决了传统 FlashAttention 在 Blackwell 上的一个痛点:旧方案用 warp group(4 个 warp)协同做 TMA 拷贝,然后同一 group 再做计算,拷贝和计算无法重叠。TLX 让不同 warp 各司其职,Q/KV 拷贝和 MatMul 计算在不同 warp 上流水线化,吞吐显著提升。

编译期块信息的实际收益

具体来说,编译期已知块大小 B 和块数 N/B 带来三层优化:

1. 地址计算全内联。 每个 Q 块在全局内存中的起始地址 = Q_ptr + i * B * d,其中 i 是块序号。Bd 编译期常量意味着地址可以折叠成少数几个偏移常量,TMA 描述符(Tensor Descriptor)在内核启动前就构建好,运行时零开销。

2. SMEM 布局编译期确定。 Q、K、V 各占多少 SMEM、bank conflict 如何规避,都可以在 Triton 编译时固定。Blackwell 的 SMEM 容量提升到 227 KB/SM,足够容纳多个块的数据,让流水线深度增加。

3. 循环展开无动态判断。 对角块的数量 N/B 是编译期常量,Triton 编译器可以直接展开整个块循环,不存在 for i in range(dynamic_n_blocks) 的运行时循环控制。

下面用一个简化示例展示这种编译期信息如何在 Triton 内核中体现。

实践:用 Triton 写一个块对角稀疏注意力内核

以下是一个简化版 TLX Block Attention 的 Triton 内核,展示编译期常量和 warp 分工的核心写法。完整实现请参考 ads_model_kernel_library 仓库。

import triton
import triton.language as tl
import torch

# ---- 编译期常量:块大小和头维度 ----
BLOCK_SIZE = 64   # B,编译期固定
HEAD_DIM = 128    # d,编译期固定

@triton.jit
def tlx_block_attn_kernel(
    Q_ptr, K_ptr, V_ptr, O_ptr,
    seq_len,       # N,运行时传入
    stride_qm, stride_qh, stride_qd,
    stride_km, stride_kh, stride_kd,
    stride_vm, stride_vh, stride_vd,
    stride_om, stride_oh, stride_od,
    HEAD_DIM: tl.constexpr,     # 编译期常量
    BLOCK_SIZE: tl.constexpr,   # 编译期常量
    NUM_BLOCKS: tl.constexpr,   # N // B,编译期常量
):
    # 当前 warp 处理的对角块序号
    block_id = tl.program_id(0)
    head_id = tl.program_id(1)

    # ---- 编译期已知:block_id 对应的行范围 ----
    # 不需要运行时 mask 判断,直接计算偏移
    q_row_start = block_id * BLOCK_SIZE
    k_row_start = block_id * BLOCK_SIZE  # 对角块:K 和 Q 同一段

    # Q 块加载:[BLOCK_SIZE, HEAD_DIM]
    q_offsets = tl.arange(0, BLOCK_SIZE) * stride_qm + q_row_start * stride_qm
    d_offsets = tl.arange(0, HEAD_DIM) * stride_qd
    q_ptrs = Q_ptr + q_offsets[:, None] * 1 + d_offsets[None, :] * 1 \
             + head_id * stride_qh
    q_block = tl.load(q_ptrs)  # [BLOCK_SIZE, HEAD_DIM]

    # K 块加载:[BLOCK_SIZE, HEAD_DIM]
    k_offsets = tl.arange(0, BLOCK_SIZE) * stride_km + k_row_start * stride_km
    k_ptrs = K_ptr + k_offsets[:, None] * 1 + d_offsets[None, :] * 1 \
             + head_id * stride_kh
    k_block = tl.load(k_ptrs)  # [BLOCK_SIZE, HEAD_DIM]

    # ---- QK^T:[BLOCK_SIZE, BLOCK_SIZE] ----
    # 因为 BLOCK_SIZE 和 HEAD_DIM 是编译期常量,
    # Triton 编译器可以精确规划 SMEM 和寄存器分配
    attn = tl.dot(q_block, tl.trans(k_block))  # [B, B]
    attn = attn * (1.0 / (HEAD_DIM ** 0.5))

    # Softmax(行内)
    attn_max = tl.max(attn, axis=1, keep_dims=True)
    attn = attn - attn_max
    attn_exp = tl.exp(attn)
    attn_sum = tl.sum(attn_exp, axis=1, keep_dims=True)
    attn_norm = attn_exp / attn_sum  # [BLOCK_SIZE, BLOCK_SIZE]

    # V 块加载:[BLOCK_SIZE, HEAD_DIM]
    v_offsets = tl.arange(0, BLOCK_SIZE) * stride_vm + k_row_start * stride_vm
    v_ptrs = V_ptr + v_offsets[:, None] * 1 + d_offsets[None, :] * 1 \
             + head_id * stride_vh
    v_block = tl.load(v_ptrs)

    # ---- Attention × V ----
    o_block = tl.dot(attn_norm, v_block)  # [BLOCK_SIZE, HEAD_DIM]

    # 写回输出
    o_offsets = tl.arange(0, BLOCK_SIZE) * stride_om + q_row_start * stride_om
    o_ptrs = O_ptr + o_offsets[:, None] * 1 + d_offsets[None, :] * 1 \
             + head_id * stride_oh
    tl.store(o_ptrs, o_block)


def tlx_block_attn(q, k, v, block_size=64):
    """块对角稀疏注意力入口函数。

    q, k, v: [batch, heads, seq_len, head_dim]
    block_size: 必须能整除 seq_len
    """
    assert q.shape[2] % block_size == 0, \
        f"seq_len {q.shape[2]} must be divisible by block_size {block_size}"

    B, H, N, D = q.shape
    num_blocks = N // block_size

    o = torch.empty_like(q)

    # 编译期常量通过 constexpr 传入 Triton
    grid = (num_blocks, H, B)

    tlx_block_attn_kernel[grid](
        q, k, v, o,
        N,
        q.stride(2), q.stride(1), q.stride(3),
        k.stride(2), k.stride(1), k.stride(3),
        v.stride(2), v.stride(1), v.stride(3),
        o.stride(2), o.stride(1), o.stride(3),
        HEAD_DIM=D,
        BLOCK_SIZE=block_size,
        NUM_BLOCKS=num_blocks,
    )
    return o


# ---- 快速验证 ----
if __name__ == "__main__":
    torch.manual_seed(42)
    B, H, N, D = 1, 2, 256, 128
    q = torch.randn(B, H, N, D, device="cuda", dtype=torch.float16)
    k = torch.randn(B, H, N, D, device="cuda", dtype=torch.float16)
    v = torch.randn(B, H, N, D, device="cuda", dtype=torch.float16)

    # TLX 块对角注意力
    o_sparse = tlx_block_attn(q, k, v, block_size=64)

    # 对照:完整注意力(只取对角块部分验证数值一致性)
    q_full = q[0, 0, 0:64]   # 第 0 个块
    k_full = k[0, 0, 0:64]
    v_full = v[0, 0, 0:64]
    attn_full = torch.matmul(q_full, k_full.T) / (D ** 0.5)
    attn_full = torch.softmax(attn_full.float(), dim=-1).half()
    o_full = torch.matmul(attn_full, v_full)

    diff = (o_sparse[0, 0, 0:64].float() - o_full.float()).abs().max().item()
    print(f"块对角注意力输出与完整注意力(同块)最大差异: {diff:.6f}")
    print(f"稀疏输出形状: {o_sparse.shape}")
    # 预期差异在 float16 精度范围内(~1e-3)

运行前需要:

# 安装 Triton(需要 CUDA 12+ 环境)
pip install triton

# 验证 GPU 可用
python -c "import torch; print(torch.cuda.get_device_name(0))"

注意:上面的简化内核没有包含 warp-specialized 的 TMA 异步拷贝逻辑——那部分依赖 Blackwell 的 TMA 描述符 API,目前 Triton 对 SM100 的 TMA 支持仍在快速演进。完整 warp 分工实现请直接参考仓库中的 tlx_block_attn 目录。

与 FlashAttention 的对比:不是替代,是特化

维度 FlashAttention (dense) FlashAttention (varlen mask) TLX Block Attention
适用模式 全连接注意力 任意稀疏 mask 固定块对角
Mask 开销 传入完整 mask tensor,运行时判断 编译期消除,零开销
地址计算 运行时动态 运行时动态 编译期内联
Warp 组织 warp group 协同拷贝+计算 同上 warp-specialized,拷贝与计算流水线
目标架构 Hopper / 通用 Hopper / 通用 Blackwell (TMA + 227KB SMEM)
序列长度灵活性 任意 任意 必须整除块大小

TLX 不是 FlashAttention 的通用替代品。它的优势场景很明确:你的注意力模式确实是固定块对角的,且你在 Blackwell 上跑。满足这两个条件时,编译期优化 + warp 特化带来的吞吐提升可以非常显著——Meta 在博客中报告的数字显示,在 8K 序列、块大小 64 的设定下,TLX 比 FlashAttention 的 varlen mask 版本快约 2-3 倍

何时该考虑 TLX Block Attention

一个简单的决策清单:

  • 注意力模式是固定块对角(或可以近似为块对角且精度损失可接受)
  • 序列长度能整除块大小(或可以 padding 到整除)
  • 部署目标是 Blackwell GPU(B200 / GB200 等 SM100 架构)
  • 吞吐是首要优化目标,而非最大序列灵活性
  • ⚠️ 如果你的稀疏模式不是严格对角(比如有少量跨块连接),TLX 目前不支持——需要回退到通用稀疏内核
  • ⚠️ 如果你在 Hopper 或更早架构上运行,TMA 和 227KB SMEM 不可用,warp-specialized 的收益会打折

实操建议:

  1. 先用 FlashAttention varlen mask 版本跑通你的块对角模式,确认精度没问题。
  2. 在 Blackwell 环境下,用 ads_model_kernel_library 中的 TLX 内核替换,对比端到端推理吞吐。
  3. 如果块大小不是 64,修改 BLOCK_SIZE constexpr 重新编译 Triton 内核——编译期常量的意义就在于你可以为不同块大小生成专用内核,而不损失运行时性能。
  4. 关注 Triton 对 Blackwell TMA 的支持更新——warp-specialized 的完整收益依赖 TMA 异步拷贝的成熟度。

块对角稀疏注意力看起来是个小众模式,但在分组推理、MoE、长文档分段等场景中反复出现。为它做一颗编译期特化的内核,不是过度优化——是在正确的架构上,把确定性的模式变成确定性的性能。


相关推荐