Skip to content

Commit ed9885e

Browse files
authored
[Bugfix][PA] Fix D192 query staging and 64-bit cache offsets (#1064)
* fix(pa): correct D192 query loads and cache offsets Signed-off-by: Xiake Sun <xiake.sun@amd.com> * test(pa): merge tile decode regressions into test_pa.py Signed-off-by: Xiake Sun <xiake.sun@amd.com> --------- Signed-off-by: Xiake Sun <xiake.sun@amd.com>
1 parent bb1a1ff commit ed9885e

2 files changed

Lines changed: 238 additions & 4 deletions

File tree

‎kernels/attention/pa_decode_tile.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def compile_pa_decode_tile(
112112
# as the absmax butterfly width); QCHUNK scales with head_dim instead.
113113
NQCHUNK = 16
114114
QCHUNK = head_dim // NQCHUNK # f16 elements per lane's load chunk (8 for head_dim=128, 4 for head_dim=64)
115+
assert QCHUNK % 4 == 0, f"head_dim {head_dim} must provide a whole number of packed FP8 query words"
115116

116117
VHE_CHUNKS = head_dim // (NWARP * MFMA_MNK) # 2 for head_dim=128, 1 for head_dim=64
117118

@@ -211,8 +212,11 @@ def _load(elem_idx):
211212
_k_load_fp8x16 = _make_flat_loader(key_cache_ptr, FP8, 16, fx.UniversalCopy128b())
212213
_v_load_fp8x16 = _make_flat_loader(value_cache_ptr, FP8, 16, fx.UniversalCopy128b())
213214
# Per-lane Q chunk (QCHUNK 16-bit elems) fetched in QLOAD_UNIT-wide
214-
# pieces (128b max per buffer load): head_dim=256 needs 2 pieces.
215-
QLOAD_UNIT = QCHUNK if QCHUNK < 8 else 8
215+
# pieces (128b max per buffer load). Use 64-bit loads when QCHUNK is
216+
# not divisible by 8: head_dim=192 gives QCHUNK=12 and therefore needs
217+
# three 4-element loads. Rounding it down to one 8-element load leaves
218+
# one third of every query row unstaged in LDS.
219+
QLOAD_UNIT = 8 if QCHUNK % 8 == 0 else 4
216220
N_QLOADS = QCHUNK // QLOAD_UNIT
217221
_q_copy_op = fx.rocdl.BufferCopy128b() if QLOAD_UNIT == 8 else fx.rocdl.BufferCopy64b()
218222
_q_load_chunk = _make_flat_loader(query_ptr, Q_DTYPE, QLOAD_UNIT, _q_copy_op)
@@ -348,6 +352,9 @@ def _load_kv_scale_vecs(a, buf_off=0):
348352
# token = warp*TOK_CHUNK + a*c16 + lane16 (the softmax mask and P-pack
349353
# write position below must encode this same formula).
350354
def _k_ops(phys, a):
355+
# Physical page ids fit in i32, but their element offsets do not
356+
# once an individual KV cache grows beyond 2 GiB.
357+
phys = fx.Int64(phys)
351358
within_page_tok = (a * c16 + lane16) % block_size
352359
ops = []
353360
for qkhe in range_constexpr(QKHE_LOOP):
@@ -536,11 +543,12 @@ def _v_ops(phys_row, vh):
536543
head_element = head_group * 16 + lane16
537544
ops = []
538545
for sub in range_constexpr(PAGES_PER_CHUNK):
546+
phys = fx.Int64(phys_row[sub])
539547
for step in range_constexpr(STEPS_PER_PAGE):
540548
if const_expr(trans_v):
541-
base = (((phys_row[sub] * n_kv + kv_h) * STEPS_PER_PAGE + step) * head_dim + head_element) * 16
549+
base = (((phys * n_kv + kv_h) * STEPS_PER_PAGE + step) * head_dim + head_element) * 16
542550
else:
543-
base = ((phys_row[sub] * n_kv + kv_h) * head_dim + head_element) * block_size + step * 16
551+
base = ((phys * n_kv + kv_h) * head_dim + head_element) * block_size + step * 16
544552
w = _v_load16(base)
545553
if const_expr(block_size == 16):
546554
# help the scheduler overlap the per-page gathered loads (see _k_ops)

‎tests/kernels/test_pa.py‎

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import torch
1717
import triton
1818

19+
from flydsl.runtime.device import get_rocm_arch
20+
1921
try:
2022
import aiter
2123
from aiter import dtypes, per_tensor_quant, pertoken_quant
@@ -80,6 +82,12 @@
8082
BATCH_SIZE_OPTIONS = [3, 81]
8183
SLIDING_WINDOW_OPTIONS = [0]
8284

85+
_ARCH = get_rocm_arch()
86+
_requires_tile_pa = pytest.mark.skipif(
87+
not (_ARCH == "gfx942" or _ARCH.startswith("gfx95")),
88+
reason=f"tile PA decode requires gfx942 or gfx95*, got {_ARCH}",
89+
)
90+
8391

8492
def setup_seed(seed: int) -> None:
8593
random.seed(seed)
@@ -1117,6 +1125,224 @@ def test_normal_accuracy(
11171125
)
11181126

11191127

1128+
@pytest.mark.l2_device
1129+
@pytest.mark.rocm_lower
1130+
@_requires_tile_pa
1131+
@pytest.mark.parametrize("compute_type", ["fp8"])
1132+
@pytest.mark.parametrize("num_partitions", [4])
1133+
@pytest.mark.parametrize("head_size", [192])
1134+
@pytest.mark.parametrize("num_heads", [(16, 1)])
1135+
@pytest.mark.parametrize("query_length", [4])
1136+
@pytest.mark.parametrize("quant_mode", ["per_tensor"])
1137+
@pytest.mark.parametrize("context_length", [1027])
1138+
@pytest.mark.parametrize("batch_size", [2])
1139+
@pytest.mark.parametrize("trans_v", [True])
1140+
@pytest.mark.parametrize("kv_varlen", [False])
1141+
@pytest.mark.parametrize("block_size", [64])
1142+
@pytest.mark.parametrize("sliding_window", [0])
1143+
def test_fp8_head_dim_192_matches_torch(
1144+
compute_type: str,
1145+
num_partitions: int,
1146+
head_size: int,
1147+
num_heads: Tuple[int, int],
1148+
query_length: int,
1149+
quant_mode: str,
1150+
context_length: int,
1151+
batch_size: int,
1152+
trans_v: bool,
1153+
kv_varlen: bool,
1154+
block_size: int,
1155+
sliding_window: int,
1156+
) -> None:
1157+
from kernels.attention.pa_decode_tile import pa_decode_tile
1158+
1159+
assert quant_mode == "per_tensor"
1160+
assert not kv_varlen
1161+
num_query_heads, num_kv_heads = num_heads
1162+
blocks_per_sequence = triton.cdiv(context_length, block_size)
1163+
total_blocks = batch_size * blocks_per_sequence
1164+
device = torch.device("cuda:0")
1165+
cache_dtype = dtypes.d_dtypes[compute_type]
1166+
1167+
setup_seed(20260821)
1168+
query = torch.empty(
1169+
batch_size * query_length,
1170+
num_query_heads,
1171+
head_size,
1172+
dtype=torch.bfloat16,
1173+
device=device,
1174+
).uniform_(-0.5, 0.5)
1175+
key_caches, value_caches = create_kv_cache(
1176+
total_blocks,
1177+
block_size,
1178+
1,
1179+
num_kv_heads,
1180+
head_size,
1181+
"auto",
1182+
torch.bfloat16,
1183+
seed=20260821,
1184+
device=str(device),
1185+
itemsize=1,
1186+
)
1187+
(
1188+
key_cache,
1189+
key_scale_flat,
1190+
value_cache,
1191+
value_scale_flat,
1192+
key_scale,
1193+
value_scale,
1194+
) = quantize_kv_cache_per_tensor(key_caches[0], value_caches[0], quant_dtype=cache_dtype)
1195+
block_tables = torch.arange(total_blocks - 1, -1, -1, dtype=torch.int32, device=device).reshape(
1196+
batch_size, blocks_per_sequence
1197+
)
1198+
context_lengths = torch.full((batch_size,), context_length, dtype=torch.int32, device=device)
1199+
query_output_indptr = torch.arange(
1200+
0,
1201+
(batch_size + 1) * query_length,
1202+
query_length,
1203+
dtype=torch.int32,
1204+
device=device,
1205+
)
1206+
expected = torch_mha_extend(
1207+
query,
1208+
key_cache,
1209+
value_cache,
1210+
block_tables,
1211+
context_lengths,
1212+
query_output_indptr,
1213+
key_scale_flat,
1214+
value_scale_flat,
1215+
sliding_window=sliding_window,
1216+
)
1217+
actual = torch.full_like(query, float("nan"))
1218+
pa_decode_tile(
1219+
output=actual,
1220+
query=query,
1221+
key_cache=key_cache,
1222+
value_cache=shuffle_value_cache_layout(value_cache) if trans_v else value_cache,
1223+
block_tables=block_tables,
1224+
context_lengths=context_lengths,
1225+
key_scale=key_scale,
1226+
value_scale=value_scale,
1227+
softmax_scale=head_size**-0.5,
1228+
num_partitions=num_partitions,
1229+
)
1230+
torch.cuda.synchronize()
1231+
1232+
assert bool(torch.isfinite(actual).all().item())
1233+
torch.testing.assert_close(actual, expected, rtol=2.0e-2, atol=2.0e-2)
1234+
1235+
1236+
@pytest.mark.large_shape
1237+
@pytest.mark.l2_device
1238+
@pytest.mark.rocm_lower
1239+
@_requires_tile_pa
1240+
@pytest.mark.parametrize("compute_type", ["fp8"])
1241+
@pytest.mark.parametrize("num_partitions", [1])
1242+
@pytest.mark.parametrize("head_size", [192])
1243+
@pytest.mark.parametrize("num_heads", [(16, 1)])
1244+
@pytest.mark.parametrize("query_length", [1])
1245+
@pytest.mark.parametrize("context_length", [2])
1246+
@pytest.mark.parametrize("batch_size", [1])
1247+
@pytest.mark.parametrize("trans_v", [True])
1248+
@pytest.mark.parametrize("block_size", [64])
1249+
def test_fp8_cache_offset_above_2gib(
1250+
compute_type: str,
1251+
num_partitions: int,
1252+
head_size: int,
1253+
num_heads: Tuple[int, int],
1254+
query_length: int,
1255+
context_length: int,
1256+
batch_size: int,
1257+
trans_v: bool,
1258+
block_size: int,
1259+
) -> None:
1260+
assert trans_v
1261+
num_query_heads, num_kv_heads = num_heads
1262+
cache_dtype = dtypes.d_dtypes[compute_type]
1263+
page_bytes = num_kv_heads * head_size * block_size * cache_dtype.itemsize
1264+
high_page = triton.cdiv(2**31, page_bytes)
1265+
total_blocks = high_page + 1
1266+
device = torch.device("cuda:0")
1267+
1268+
query = torch.ones(
1269+
batch_size * query_length,
1270+
num_query_heads,
1271+
head_size,
1272+
dtype=torch.bfloat16,
1273+
device=device,
1274+
)
1275+
key_cache = torch.empty(
1276+
total_blocks,
1277+
num_kv_heads,
1278+
head_size // 16,
1279+
block_size,
1280+
16,
1281+
dtype=cache_dtype,
1282+
device=device,
1283+
)
1284+
value_cache = torch.empty(
1285+
total_blocks,
1286+
num_kv_heads,
1287+
block_size // 16,
1288+
head_size,
1289+
16,
1290+
dtype=cache_dtype,
1291+
device=device,
1292+
)
1293+
# Rounded-up tiles use page 0 for bounded fallback loads. Keep both that
1294+
# page and the selected high page finite so masked PV lanes cannot see NaNs.
1295+
key_cache[0].zero_()
1296+
value_cache[0].zero_()
1297+
key_cache[high_page].zero_()
1298+
value_cache[high_page].zero_()
1299+
1300+
# Make the output sensitive to both selected K/V token addresses. If K
1301+
# wraps to page 0, the opposing values average to zero.
1302+
key_cache[high_page, 0, :, 0, :].fill_(-0.25)
1303+
key_cache[high_page, 0, :, 1, :].fill_(0.25)
1304+
value_cache[high_page, 0, 0, :, 0].fill_(-1.0)
1305+
value_cache[high_page, 0, 0, :, 1].fill_(1.0)
1306+
1307+
selected_keys = (
1308+
key_cache[high_page].permute(2, 0, 1, 3).reshape(block_size, num_kv_heads, head_size)[:context_length]
1309+
)
1310+
selected_values = (
1311+
value_cache[high_page].permute(1, 3, 0, 2).reshape(block_size, num_kv_heads, head_size)[:context_length]
1312+
)
1313+
expected = reference_masked_attention(
1314+
query,
1315+
selected_keys,
1316+
selected_values,
1317+
head_size**-0.5,
1318+
query.dtype,
1319+
)
1320+
assert expected.float().abs().min().item() > 0.5
1321+
1322+
block_tables = torch.full((batch_size, 1), high_page, dtype=torch.int32, device=device)
1323+
context_lengths = torch.full((batch_size,), context_length, dtype=torch.int32, device=device)
1324+
scale = torch.ones(1, dtype=torch.float32, device=device)
1325+
actual = torch.full_like(query, float("nan"))
1326+
from kernels.attention.pa_decode_tile import pa_decode_tile
1327+
1328+
pa_decode_tile(
1329+
output=actual,
1330+
query=query,
1331+
key_cache=key_cache,
1332+
value_cache=value_cache,
1333+
block_tables=block_tables,
1334+
context_lengths=context_lengths,
1335+
key_scale=scale,
1336+
value_scale=scale,
1337+
softmax_scale=head_size**-0.5,
1338+
num_partitions=num_partitions,
1339+
)
1340+
torch.cuda.synchronize()
1341+
1342+
assert bool(torch.isfinite(actual).all().item())
1343+
torch.testing.assert_close(actual, expected, rtol=2.0e-2, atol=2.0e-2)
1344+
1345+
11201346
@pytest.mark.parametrize(("query_length", "quant_mode"), [(1, "per_tensor"), (4, "per_token")])
11211347
def test_metadata_accuracy(query_length: int, quant_mode: str) -> None:
11221348
"""Exercise the block-1024 persistent worklist decode and split reducer."""

0 commit comments

Comments
 (0)