[FA4/B200] return_lse=True makes D64 forward about 19% faster

Author: drisspgCreated Jul 25, 2026Updated Sep 6, 2026

I had my agent create this reproducer while doing some heuristic tuning. This is quite strange, and I wanted to track it.

On B200, the FA4 CuTe forward kernel for dense BF16 attention with (B, S, H, D) = (2, 16384, 16, 64) is consistently faster when return_lse=True, even though that specialization calculates and writes the additional LSE output.

Agent notes

Environment

  • GPU: NVIDIA B200 (SM100)
  • PyTorch: 2.14.0.dev20260722+cu132
  • PyTorch CUDA runtime: 13.2
  • NVIDIA CuTe DSL: 4.6.0.dev0
  • FlashAttention commit: 00756db9d921da0846453283ddfbeb7457abd09b
  • Dtype: BF16
  • Causal: false
  • Measurement: fixed-pointer CUDA graph replay, 25 warmups and 100 measured replays per round, five alternating A/B rounds

Result

GPU: NVIDIA B200
Torch: 2.14.0.dev20260722+cu132 (CUDA 13.2)
CuTe DSL: 4.6.0.dev0
FA4 commit: 00756db9d921da0846453283ddfbeb7457abd09b
no-LSE: median=3.0018 ms rounds=[3.007457275390625, 3.0017739868164064, 3.001456604003906, 3.0028912353515627, 3.0008636474609376]
with-LSE: median=2.5281 ms rounds=[2.5091484069824217, 2.545796203613281, 2.512978210449219, 2.5281126403808596, 2.532301177978516]
with-LSE speedup: 1.187x

Both paths pass a correctness check against PyTorch SDPA.

A focused Nsight Compute comparison showed the same launch topology and resource limits for both specializations (148 blocks, 512 threads, 128 registers/thread, and approximately 232 KB dynamic shared memory). The no-LSE specialization had substantially fewer eligible/issued warps and more samples around the pipeline synchronization path. This looks more like a compiler scheduling/code-generation cliff than an inherent benefit from calculating LSE.

I have only established this behavior on B200; this report does not claim the same result on GB300.

Minimal reproducer

Run with:

bash
python repro_fa4_lse_b200.py /path/to/flash-attention
python
"""Reproduce the FA4 D64 B200 performance inversion caused by returning LSE."""

import argparse
import importlib
import importlib.metadata
import statistics
import subprocess
import sys
import time
from pathlib import Path
from types import ModuleType
from typing import Callable

import torch
import torch.nn.functional as F


def load_fa4_interface(fa4_root: Path) -> ModuleType:
    """Load the CuTe FA4 interface without requiring the FA2 extension."""
    package = ModuleType("flash_attn")
    package.__path__ = [str(fa4_root / "flash_attn")]
    sys.modules["flash_attn"] = package
    return importlib.import_module("flash_attn.cute.interface")


def make_call(
    interface: ModuleType,
    q: torch.Tensor,
    k: torch.Tensor,
    v: torch.Tensor,
    return_lse: bool,
) -> Callable[[], torch.Tensor]:
    """Create one allocation-free FA4 forward specialization."""
    out = torch.empty_like(q)
    lse = (
        torch.empty(q.shape[0], q.shape[2], q.shape[1], device=q.device)
        if return_lse
        else None
    )

    def run() -> torch.Tensor:
        return interface._flash_attn_fwd(
            q, k, v, out=out, lse=lse, return_lse=return_lse
        )[0]

    return run


def capture(function: Callable[[], torch.Tensor]) -> torch.cuda.CUDAGraph:
    """Capture a warmed FA4 call into a fixed-pointer CUDA graph."""
    stream = torch.cuda.Stream()
    stream.wait_stream(torch.cuda.current_stream())
    with torch.cuda.stream(stream):
        for _ in range(3):
            function()
    stream.synchronize()
    graph = torch.cuda.CUDAGraph()
    with torch.cuda.graph(graph, stream=stream):
        function()
    return graph


def warm_gpu(seconds: float = 1.0) -> None:
    """Warm the GPU long enough to avoid measuring initial idle clocks."""
    x = torch.randn(4096, 4096, device="cuda", dtype=torch.bfloat16)
    deadline = time.perf_counter() + seconds
    while time.perf_counter() < deadline:
        torch.mm(x, x)
    torch.cuda.synchronize()


def time_graph(graph: torch.cuda.CUDAGraph, iterations: int) -> float:
    """Return mean fixed-pointer graph replay latency in milliseconds."""
    for _ in range(25):
        graph.replay()
    start = torch.cuda.Event(enable_timing=True)
    end = torch.cuda.Event(enable_timing=True)
    start.record()
    for _ in range(iterations):
        graph.replay()
    end.record()
    end.synchronize()
    return start.elapsed_time(end) / iterations


def main() -> None:
    """Compile, validate, and compare no-LSE and with-LSE FA4 kernels."""
    parser = argparse.ArgumentParser()
    parser.add_argument("fa4_root", type=Path)
    parser.add_argument("--iterations", type=int, default=100)
    parser.add_argument("--rounds", type=int, default=5)
    args = parser.parse_args()

    torch.manual_seed(0)
    torch.cuda.manual_seed_all(0)
    interface = load_fa4_interface(args.fa4_root.resolve())
    shape = (2, 16384, 16, 64)
    q, k, v = (
        torch.randn(shape, device="cuda", dtype=torch.bfloat16) for _ in range(3)
    )
    no_lse = make_call(interface, q, k, v, False)
    with_lse = make_call(interface, q, k, v, True)

    expected = F.scaled_dot_product_attention(
        q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
    ).transpose(1, 2)
    torch.testing.assert_close(no_lse().float(), expected.float(), atol=5e-2, rtol=2e-2)
    torch.testing.assert_close(
        with_lse().float(), expected.float(), atol=5e-2, rtol=2e-2
    )
    torch.cuda.synchronize()

    graphs = {"no-LSE": capture(no_lse), "with-LSE": capture(with_lse)}
    samples = {name: [] for name in graphs}
    warm_gpu()
    for round_index in range(args.rounds):
        order = tuple(graphs) if round_index % 2 == 0 else tuple(reversed(graphs))
        for name in order:
            samples[name].append(time_graph(graphs[name], args.iterations))

    commit = subprocess.check_output(
        ["git", "-C", str(args.fa4_root), "rev-parse", "HEAD"], text=True
    ).strip()
    medians = {name: statistics.median(values) for name, values in samples.items()}
    print(f"GPU: {torch.cuda.get_device_name()}")
    print(f"Torch: {torch.__version__} (CUDA {torch.version.cuda})")
    print(f"CuTe DSL: {importlib.metadata.version('nvidia-cutlass-dsl')}")
    print(f"FA4 commit: {commit}")
    for name, values in samples.items():
        print(f"{name}: median={medians[name]:.4f} ms rounds={values}")
    print(f"with-LSE speedup: {medians['no-LSE'] / medians['with-LSE']:.3f}x")


if __name__ == "__main__":
    main()

Expected behavior: omitting the extra LSE output should be at least as fast as returning it, absent a documented specialization tradeoff.

Source: Dao-AILab/flash-attention