Skip to content

Commit 902af74

Browse files
committed
Add host-side validation for paged-KV cp.async load configs
The cp.async (non-TMA) paged-KV path derives page_entry_per_thread = n_block_size // <KV-load threads> in PagedKVManager.create. When tile_n is not an exact multiple of the loader thread count, the trailing KV rows of each n-block are silently dropped (or page_entry_per_thread hits 0), producing wrong output or an opaque failure deep inside JIT compilation. Validate this in _flash_attn_fwd before compilation: when page_table is set and page_size != tile_n, require tile_n to be divisible by the KV-load thread count (128 on SM90; 128/64 on SM100/SM110 depending on q_stage), and raise a ValueError naming the offending values otherwise. Scoped to the standard SM90/SM100/SM110 forward; the dedicated hd256 kernel and the MLA (qv) path use their own loaders and keep their existing checks. Add tests/cute/test_paged_kv_validation.py: a GPU-free test that runs under FakeTensorMode with a cute.compile sentinel, asserting invalid configs raise ValueError before compilation, valid configs clear the guard, and the TMA path (page_size == tile_n) is unaffected.
1 parent ddfec5d commit 902af74

2 files changed

Lines changed: 167 additions & 0 deletions

File tree

flash_attn/cute/interface.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,42 @@ def _flash_attn_fwd(
602602
use_dedicated_hd256_kernel = arch // 10 in [10, 11] and head_dim == 256 and head_dim_v == 256
603603
use_2cta_instrs = use_2cta_instrs or use_dedicated_hd256_kernel
604604

605+
# Host-side validation for the cp.async (non-TMA) paged-KV path. When
606+
# page_size != tile_n the kernel loads the page table through PagedKVManager,
607+
# which derives page_entry_per_thread = tile_n // <KV-load threads> via integer
608+
# division (see PagedKVManager.create in paged_kv.py). If tile_n is not an exact
609+
# multiple of the KV-load thread count, the trailing KV rows of every n-block are
610+
# never fetched (page_entry_per_thread rounds down, or hits 0), silently
611+
# corrupting the output. Fail fast here instead of deep inside JIT compilation.
612+
# The dedicated hd256 kernel and the MLA (qv) path use their own KV loaders, so
613+
# this guard is scoped to the standard SM90/SM100/SM110 forward.
614+
if (
615+
page_table is not None
616+
and qv is None
617+
and not use_dedicated_hd256_kernel
618+
and arch // 10 in [9, 10, 11]
619+
and page_size not in (None, tile_n)
620+
):
621+
if arch // 10 == 9:
622+
# SM90 loads KV with a single warp group (num_threads_per_warp_group).
623+
paged_kv_load_threads = 128
624+
else:
625+
# SM100/SM110 cp.async path: the load warp group is softmax1 (4 warps =
626+
# 128 threads) when q_stage == 1, otherwise the two trailing warps
627+
# (64 threads). See FlashAttentionForwardSm100 load_warp_ids selection.
628+
paged_kv_load_threads = 128 if q_stage == 1 else 64
629+
if tile_n % paged_kv_load_threads != 0:
630+
raise ValueError(
631+
f"Paged KV with page_size ({page_size}) != n_block_size ({tile_n}) "
632+
f"selects the cp.async KV-load path, which requires n_block_size "
633+
f"(tile_n={tile_n}) to be a multiple of the KV-load thread count "
634+
f"({paged_kv_load_threads}) so each thread loads a whole number of "
635+
f"page-table entries; got tile_n={tile_n} % {paged_kv_load_threads} = "
636+
f"{tile_n % paged_kv_load_threads}. Use page_size == n_block_size "
637+
f"(={tile_n}) to take the TMA path, or pick a configuration whose "
638+
f"tile_n is divisible by {paged_kv_load_threads}."
639+
)
640+
605641
if softcap is not None:
606642
assert score_mod is None, "softcap and score_mod cannot be used together"
607643
score_mod = utils.create_softcap_scoremod(softcap)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""Host-side validation tests for paged-KV configurations.
2+
3+
These tests do NOT require a GPU. They exercise the early input validation in
4+
``_flash_attn_fwd`` that rejects paged-KV configs whose ``n_block_size`` (tile_n)
5+
is not divisible by the cp.async KV-load thread count. Such configs would
6+
otherwise fail with an opaque error deep inside JIT compilation (or, worse,
7+
silently drop KV rows).
8+
9+
The pattern mirrors the fast two-pass test workflow: we run under
10+
``FakeTensorMode`` (no GPU memory) and replace ``cute.compile`` with a sentinel
11+
so that reaching compilation is observable without a CUDA device. The kernel
12+
arch is forced via the private ``_arch`` argument so the selected forward path
13+
is deterministic on any host.
14+
"""
15+
16+
import pytest
17+
18+
# CUDA / CuTeDSL are Linux+NVIDIA only; skip cleanly where they are unavailable
19+
# (e.g. macOS dev hosts) instead of erroring at collection time.
20+
torch = pytest.importorskip("torch")
21+
pytest.importorskip("cutlass")
22+
23+
from torch._subclasses.fake_tensor import FakeTensorMode # noqa: E402
24+
25+
from flash_attn.cute import interface # noqa: E402
26+
from flash_attn.cute.interface import _flash_attn_fwd # noqa: E402
27+
28+
29+
class _CompileReached(Exception):
30+
"""Raised by the stubbed ``cute.compile`` to mark that host validation passed."""
31+
32+
33+
def _install_compile_sentinel(monkeypatch):
34+
"""Replace cute.compile with a sentinel and reset the compile cache.
35+
36+
If host validation passes, control reaches ``cute.compile`` and the sentinel
37+
fires; we treat that as "the config was accepted".
38+
"""
39+
40+
def _sentinel(*args, **kwargs):
41+
raise _CompileReached
42+
43+
monkeypatch.setattr(interface.cute, "compile", _sentinel)
44+
# Ensure we never short-circuit on a previously cached kernel.
45+
monkeypatch.setattr(_flash_attn_fwd, "compile_cache", {})
46+
47+
48+
def _make_paged_inputs(
49+
*,
50+
batch_size=1,
51+
seqlen_q=128,
52+
num_head=4,
53+
num_head_kv=4,
54+
head_dim=128,
55+
head_dim_v=128,
56+
num_pages=4,
57+
page_size=64,
58+
max_num_pages_per_seq=4,
59+
dtype=torch.bfloat16,
60+
):
61+
"""Build fake (meta) tensors for a paged-KV forward call."""
62+
q = torch.empty(batch_size, seqlen_q, num_head, head_dim, dtype=dtype)
63+
k = torch.empty(num_pages, page_size, num_head_kv, head_dim, dtype=dtype)
64+
v = torch.empty(num_pages, page_size, num_head_kv, head_dim_v, dtype=dtype)
65+
page_table = torch.zeros(batch_size, max_num_pages_per_seq, dtype=torch.int32)
66+
return q, k, v, page_table
67+
68+
69+
@pytest.mark.parametrize("arch", [90, 100])
70+
def test_paged_kv_indivisible_tile_n_raises(monkeypatch, arch):
71+
"""tile_n not divisible by the KV-load thread count must raise ValueError.
72+
73+
tile_n is forced to 96 via ``tile_mn``. On SM90 the cp.async loader uses 128
74+
threads; on SM100 (q_stage == 1 here, since seqlen_q == tile_m) it also uses
75+
128. 96 % 128 != 0, so the config is rejected before compilation.
76+
"""
77+
_install_compile_sentinel(monkeypatch)
78+
with FakeTensorMode():
79+
q, k, v, page_table = _make_paged_inputs(page_size=64)
80+
with pytest.raises(ValueError, match=r"cp\.async KV-load path"):
81+
_flash_attn_fwd(
82+
q,
83+
k,
84+
v,
85+
page_table=page_table,
86+
tile_mn=(128, 96), # force n_block_size = 96
87+
_arch=arch,
88+
)
89+
90+
91+
@pytest.mark.parametrize("arch", [90, 100])
92+
def test_paged_kv_divisible_tile_n_passes_guard(monkeypatch, arch):
93+
"""A valid paged config (tile_n divisible by load threads) clears the guard.
94+
95+
tile_n is forced to 128 (divisible by both 128 and 64), with page_size=64 so
96+
the cp.async path is selected. The new validation must NOT raise; execution
97+
should reach the stubbed ``cute.compile`` (``_CompileReached``).
98+
"""
99+
_install_compile_sentinel(monkeypatch)
100+
with FakeTensorMode():
101+
q, k, v, page_table = _make_paged_inputs(page_size=64)
102+
with pytest.raises(_CompileReached):
103+
_flash_attn_fwd(
104+
q,
105+
k,
106+
v,
107+
page_table=page_table,
108+
tile_mn=(128, 128), # divisible by the KV-load thread count
109+
_arch=arch,
110+
)
111+
112+
113+
def test_paged_kv_tma_path_skips_guard(monkeypatch):
114+
"""When page_size == tile_n the TMA path is used and the guard is bypassed.
115+
116+
Even with tile_n=96 (not divisible by 128), page_size == tile_n means
117+
PagedKVManager's cp.async loader is never used, so the config is accepted and
118+
reaches compilation.
119+
"""
120+
_install_compile_sentinel(monkeypatch)
121+
with FakeTensorMode():
122+
q, k, v, page_table = _make_paged_inputs(page_size=96, num_pages=4)
123+
with pytest.raises(_CompileReached):
124+
_flash_attn_fwd(
125+
q,
126+
k,
127+
v,
128+
page_table=page_table,
129+
tile_mn=(128, 96), # page_size == tile_n -> TMA path
130+
_arch=90,
131+
)

0 commit comments

Comments
 (0)