Skip to content

LLM Lab 实验手记 07:FlashAttention 为什么快——同机对比实验 ​

代码文件:llm_lab/05_flashattention/flash_compare.py

系列最后一篇,从"怎么并行"转到"怎么算得快"。长上下文是当前大模型的核心战场,而 Attention 的计算量随序列长度平方增长——FlashAttention 是打破这个瓶颈的关键内核。本篇在同一台 GPU 上直接对比标准 Attention 与 flash_attn,用数字说话。

1. 实验目标 ​

  1. 实现标准 Attention(QKᵀ → Softmax → AV)与 FlashAttention 的同条件对比;
  2. 实测两者的耗时差异(并观察显存行为的差异);
  3. 拆解 FlashAttention 快在哪:分块、IO 感知、在线 Softmax。

2. 代码逐段拆解 ​

完整代码(flash_compare.py,46 行):

python
import torch
import time
import torch.nn.functional as F
from flash_attn import flash_attn_func        # ① 引入 flash_attn

batch = 4
seq = 512
heads = 32
dim = 128

q = torch.randn(batch, seq, heads, dim, device="cuda", dtype=torch.bfloat16)   # ② 直接按 (b, s, h, d) 布局
k = torch.randn(batch, seq, heads, dim, device="cuda", dtype=torch.bfloat16)
v = torch.randn(batch, seq, heads, dim, device="cuda", dtype=torch.bfloat16)

for step in range(5):                        # ③ 5 轮:首轮预热,2-5 轮计时

    # ---- 标准 Attention ----
    torch.cuda.synchronize()
    t0 = time.time()

    scores = torch.matmul(
        q.transpose(1, 2),                   # (b, h, s, d)
        k.transpose(1, 2).transpose(-1, -2)  # (b, h, d, s)
    ) / (dim ** 0.5)                          # ④ S = QKᵀ / √d —— 完整 [b, h, s, s] 分数矩阵

    attn = F.softmax(scores, dim=-1)

    out = torch.matmul(attn, v.transpose(1, 2))

    torch.cuda.synchronize()
    t1 = time.time()

    # ---- FlashAttention ----
    torch.cuda.synchronize()
    t2 = time.time()

    out2 = flash_attn_func(q, k, v)          # ⑤ 一行调用

    torch.cuda.synchronize()
    t3 = time.time()

    if step > 0:                              # ⑥ 跳过预热轮
        print(f"Step {step}")
        print(f"Normal Attention : {(t1-t0)*1000:.2f} ms")
        print(f"FlashAttention   : {(t3-t2)*1000:.2f} ms")

① 环境要求 ​

flash_attn 需要与 PyTorch 版本严格匹配的预编译 wheel(或源码编译)。这是生产环境最常踩的坑之一——版本不匹配通常表现为导入即报错或非法内存访问,不是性能问题。

② 张量布局差异 ​

标准写法习惯 (b, h, s, d),而 flash_attn_func 直接吃 (b, s, h, d)——省掉了两次 transpose 的额外开销。这个小差异提醒我们:内核接口的布局约定,本身就是性能设计的一部分。

④ 关键:标准 Attention 要物化完整分数矩阵 ​

scores 的形状是 [4, 32, 512, 512],整个 S = QKᵀ 矩阵被完整写进显存,再 softmax,再乘 V。seq=512 时是 4×32×512×512×2B ≈ 67 MB;若 seq=8K,就是 16 GB 级——显存被分数矩阵吃光,这就是标准 Attention 的硬伤。

⑤ 一行调用的背后 ​

flash_attn_func(q, k, v) 内部做三件事(下节展开),外部表现是:不需要物化 S 矩阵,且跑得更快。

3. 原理讲解:FlashAttention 的三个设计 ​

3.1 分块计算(Tiling) ​

把 S 矩阵按块(tile)切成小片,逐块计算、逐块 softmax、逐块累加输出——任何时刻只保留一个 tile,完整 S 矩阵从不存在。显存占用从 O(s²) 降到 O(s)(实际是 O(s/tile))。

3.2 IO 感知(IO-Aware):瓶颈不是 FLOPs,是 HBM 读写 ​

GPU 算力(FLOPs)远快于显存带宽(HBM)。标准 Attention 每算一个算子就把中间结果写回 HBM 再读出来(QKᵀ → HBM → Softmax → HBM → AV),多次 HBM 往返。FlashAttention 把整块计算融合在一个 kernel 里,中间结果留在片上 SRAM,只回写最终输出——省的是内存搬运,不是数学运算。

3.3 在线 Softmax(Online Softmax) ​

分块后如何保证 softmax 结果与整块计算一致?FlashAttention 用"重缩放"技巧:每处理一个新块,用之前的 running max 重缩放已累加的输出,最后得到与标准 softmax 数值等价的结果。这就是为什么它快,却不是近似算法——结果与标准 Attention 一致(浮点误差范围内)。

4. 实验结果 ​

同机 4 轮(跳过预热)对比,预期输出形态:

Step 2
Normal Attention : 31.42 ms
FlashAttention   : 9.78 ms
Step 3
Normal Attention : 31.05 ms
FlashAttention   : 9.61 ms
Step 4
Normal Attention : 30.87 ms
FlashAttention   : 9.70 ms

(具体数值随 GPU 型号浮动;本实验在 H 系列 GPU 上典型加速比 3–5×,且 seq 越长差距越大——seq=512 时约 3×,seq=4K 以上可达 10× 量级。)

两个必看的实验动作:

  1. 把 seq 从 128 调到 512(代码里已注释备用值),观察标准 Attention 耗时按 s² 增长、FlashAttention 增速平缓得多;
  2. 观察显存(nvidia-smi):标准 Attention 的峰值显存随 s² 上涨,FlashAttention 几乎不随 seq 涨——这是它支撑长上下文的核心。

5. 工程启示 ​

  1. "快"的本质是省内存搬运:优化 AI 内核的第一原则不是减少 FLOPs,而是减少 HBM 往返。FlashAttention、fused kernel、算子融合全部遵循同一逻辑——这条经验适用于所有 GPU 性能优化。

  2. 长上下文配置的显存预算:规划 128K 上下文训练/推理时,用标准 Attention 的显存公式(b × h × s²)算一下就知道不可行;FlashAttention + 分页 KV Cache 是标配。这也是第 12 章推理引擎优化的前置认知。

  3. 与第 5 篇的组合:张量并行把 head 分到各卡(local_heads),FlashAttention 替换本地注意力计算——并行不冲突,分层优化:TP 解决"放不下",FlashAttention 解决"算不动"。

  4. 本系列收尾:从 18 行到生产——进程组 → DDP → 通信自动微分 → TP(FFN/Attention)→ Transformer Block → 高效内核,这条链路正是每个大模型训练集群上每天都在发生的事。用最小实验理解每一层,才能在万卡排障时知道问题出在哪一层。

系列完。下一主题建议:《LLM 训练稳定性:Loss Spike 排查》或《混合精度与断点续训》,或继续 分布式并行全景 的系统化视角。

AI-HPC 智算超算专家 · Contact: openaihpc@gmail.com