为什么需要 Flash Attention?

标准 Attention 的瓶颈不在计算,而在显存 IO。考虑 $n = 8192$, $d = 128$ 的注意力计算:

操作HBM 读写量SRAM 计算
$S = QK^T$读 Q(8MB) + 读 K(8MB) + 写 S(512MB)~16 GFLOPs
$P = \text{softmax}(S)$读 S(512MB) + 写 P(512MB)~8 MFLOPs
$O = PV$读 P(512MB) + 读 V(8MB) + 写 O(8MB)~16 GFLOPs
总计~1.6 GB~32 GFLOPs

A100 的 HBM 带宽 2 TB/s,SRAM 带宽 19 TB/s。计算只需 ~0.02ms,但 IO 需要 ~0.8ms——IO 是瓶颈的 40 倍

Flash Attention v1:Tiling + 在线 Softmax

核心思想

不分块就不写回 HBM——整个注意力计算在 SRAM 中完成。

Tiling 策略

将 Q、K、V 分块加载到 SRAM,逐块计算:

SRAM 容量: ~164 KB (A100)
Block 大小: B_r × B_c (需要满足 B_r × d + B_c × d + B_r × B_c ≤ SRAM)
典型值: B_r = 128, B_c = 128, d = 128 → 128×128 + 128×128 + 128×128 = 48KB ✓
def flash_attention_v1(Q, K, V, block_size=128):
    """
    Q: (n, d), K: (n, d), V: (n, d)
    """
    n, d = Q.shape
    O = torch.zeros_like(Q)
    l = torch.zeros(n)
    m = torch.full((n,), -float('inf'))

    for i in range(0, n, block_size):
        Qi = Q[i:i+block_size]  # 加载到 SRAM
        Oi = torch.zeros_like(Qi)
        li = torch.zeros(block_size)
        mi = torch.full((block_size,), -float('inf'))

        for j in range(0, n, block_size):
            Kj = K[j:j+block_size]  # 加载到 SRAM
            Vj = V[j:j+block_size]

            # 在 SRAM 中计算
            Sij = Qi @ Kj.T / math.sqrt(d)  # (B_r, B_c)
            
            # 在线 softmax 更新
            mij_new = torch.maximum(mi, Sij.max(dim=-1).values)
            Pij = torch.exp(Sij - mij_new.unsqueeze(-1))
            
            # 重缩放
            alpha = torch.exp(mi - mij_new)
            Oi = Oi * alpha.unsqueeze(-1)
            li = li * alpha
            
            Oi += Pij @ Vj
            li += Pij.sum(dim=-1)
            mi = mij_new

        O[i:i+block_size] = Oi / li.unsqueeze(-1)
    
    return O

IO 复杂度分析

标准 Attention IO: $O(n^2)$(读写 $n \times n$ 矩阵)

Flash Attention IO: $O\left(\frac{n^2 d^2}{M}\right)$,其中 $M$ 为 SRAM 大小。

当 $M \geq n \cdot d$ 时,退化为 $O(n \cdot d)$——每个元素只读写一次。

方法HBM IO (n=8192, d=128)加速比
标准 Attention1.6 GB
Flash Attn v1~50 MB32× IO 减少

Flash Attention v2:减少非矩阵乘法

v1 的问题

  1. 非矩阵乘法操作过多:Softmax 的 max/exp/sum 占总时间 ~30-40%
  2. 循环结构低效:外层遍历 Q 块,内层遍历 KV 块,GPU 并行度不足

v2 改进

改进 1:重排循环——外层 KV,内层 Q

def flash_attention_v2(Q, K, V, block_size=128):
    n, d = Q.shape
    O = torch.zeros_like(Q)
    
    for j in range(0, n, block_size):
        Kj = K[j:j+block_size]
        Vj = V[j:j+block_size]
        
        for i in range(0, n, block_size):
            Qi = Q[i:i+block_size]
            Sij = Qi @ Kj.T / math.sqrt(d)
            
            # 只需要当前块的 max
            mij = Sij.max(dim=-1).values
            Pij = torch.exp(Sij - mij.unsqueeze(-1))
            
            # 累积更新 (需要保存 mi, li 用于全局归一化)
            if j == 0:
                O[i:i+block_size] = Pij @ Vj
                m_global[i] = mij
                l_global[i] = Pij.sum(dim=-1)
            else:
                m_new = torch.maximum(m_global[i], mij)
                alpha = torch.exp(m_global[i] - m_new)
                O[i:i+block_size] = O[i:i+block_size] * alpha.unsqueeze(-1) \
                    + Pij @ Vj * torch.exp(mij - m_new).unsqueeze(-1)
                # ... 更新 m, l
    
    return O / l_global.unsqueeze(-1)

改进 2:减少 rescale 操作——将 rescale 推迟到最后,中间使用 log-space 累积。

改进 3:更好的并行度——在 Q 块维度和 head 维度同时并行。

性能对比 (A100, FP16)

模型规模标准 AttnFA v1FA v2FA v2 加速
GPT-2 (1.5B)1.0×2.0×2.3×vs 标准
LLaMA-7B1.0×2.1×2.5×vs 标准
LLaMA-70B1.0×2.0×2.4×vs 标准

Flash Attention v3:异步与 FP8

针对 H100 的优化

FA v3 (2024) 深度利用 H100 的硬件特性:

特性 1:WGMMA (Warp Group Matrix Multiply-Accumulate)

H100 的 TMA (Tensor Memory Accelerator) + WGMMA 指令实现异步 GEMM

// FA v3 伪代码: 异步 GEMM + softmax 重叠
__global__ void flash_attn_v3_kernel(/* ... */) {
    // 1. TMA 异步加载 K, V 块到共享内存
    cp_async_bulk(shared_k, global_k);
    cp_async_bulk(shared_v, global_v);
    
    // 2. WGMMA 异步计算 Q @ K^T
    wgmma_async(C_shared, Q_shared, K_shared);
    
    // 3. 等待 GEMM 完成
    wgmma_wait_group(0);
    
    // 4. 在 GEMM 结果到达时,同时做 softmax
    softmax_in_place(C_shared, m, l);
    
    // 5. 异步启动 P @ V 的 GEMM
    wgmma_async(O_shared, P_shared, V_shared);
    
    // 6. 同时 TMA 加载下一块 K, V
    cp_async_bulk(shared_k_next, global_k_next);
}

特性 2:FP8 支持

H100 支持 FP8 (E4M3/E5M2) GEMM,吞吐量是 FP16 的 2 倍:

# FA v3 FP8 路径
def flash_attn_v3_fp8(Q, K, V):
    # Q, K, V: FP16 输入
    Q_fp8 = Q.to(torch.float8_e4m3fn)
    K_fp8 = K.to(torch.float8_e4m3fn)
    V_fp8 = V.to(torch.float8_e4m3fn)
    
    # FP8 GEMM (2x 吞吐)
    S = fp8_gemm(Q_fp8, K_fp8.T)  # FP32 累积
    S = S / math.sqrt(d)
    P = softmax(S)
    P_fp8 = P.to(torch.float8_e4m3fn)
    O = fp8_gemm(P_fp8, V_fp8)    # FP32 累积
    
    return O.to(torch.float16)

三代对比

指标FA v1FA v2FA v3
目标 GPUA100A100H100
异步 GEMM部分✅ (WGMMA)
TMA
FP8
Softmax 重叠部分✅ (完全重叠)
理论 MFU~50%~62%~75%

实测性能 (H100, FP16)

序列长度FA v2 (TFLOPS)FA v3 (TFLOPS)提升
4K16019522%
8K17523031%
16K18026044%
32K18229059%

FA v3 在长序列上优势更明显——因为异步重叠在长序列中能隐藏更多延迟。

FP8 性能 (H100)

序列长度FA v3 FP16FA v3 FP8加速比
8K230 TFLOPS420 TFLOPS1.83×
16K260 TFLOPS510 TFLOPS1.96×
32K290 TFLOPS580 TFLOPS2.00×

Tiling 参数选择

FA v3 的 tiling 需要综合考虑 SRAM 容量和 GEMM 效率:

def compute_block_size(sram_bytes, d, dtype_bytes=2):
    """计算最优 block size"""
    # SRAM 需要容纳: Q_block + K_block + V_block + P_block + O_block
    # B_r * d + B_c * d + B_c * d + B_r * B_c + B_r * d <= SRAM
    # 典型 SRAM: 228 KB (H100)
    
    available = sram_bytes - 3 * d * dtype_bytes  # 留余量
    # 最大化 B_r * B_c 同时满足约束
    # 启发式: B_r = B_c = sqrt(available / (2*d + 1))
    block = int(math.sqrt(available / (2 * d * dtype_bytes + 1)))
    # 对齐到 64 (GEMM 对齐要求)
    block = (block // 64) * 64
    return min(block, 256)  # 上限 256

总结

Flash Attention 的三代演进完美诠释了IO-aware 算法设计的价值:

  • v1:Tiling + 在线 Softmax,将 IO 从 $O(n^2)$ 降为 $O(n)$
  • v2:减少非 GEMM 操作,提升并行度
  • v3:异步 GEMM + Softmax 重叠 + FP8,逼近硬件理论峰值

FA v3 在 H100 上达到 580 TFLOPS (FP8),理论 MFU 75%,是目前最快的 Attention 实现。对于长序列训练和推理,Flash Attention 已是不可替代的基础设施。


本文由硅基 AGI 技术团队撰写,转载请注明出处。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。