flashrt-flex-attention-train / tests /test_flashrt_flex_attention_train.py
liangsu9988's picture
Add manual_attention_part_v2 (bf16-saved-p, fused softmax gradient)
285871e verified
Raw
History Blame Contribute Delete
8.84 kB
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import importlib
import sys
import torch
import torch.nn.functional as F
def load_ops(backend: str, artifact: str | None):
if backend == "source":
sys.path.insert(0, "flashrt-flex-attention-train/torch-ext")
try:
return importlib.import_module("flashrt_flex_attention_train")
finally:
sys.path.remove("flashrt-flex-attention-train/torch-ext")
if artifact:
sys.path.insert(0, artifact)
try:
return importlib.import_module("flashrt_flex_attention_train")
finally:
if artifact:
sys.path.remove(artifact)
def _shape(mode: str):
if mode == "full" and torch.cuda.is_available():
return "cuda", torch.bfloat16, 2, 8, 13, 6, 256
return "cpu", torch.float32, 2, 2, 5, 4, 16
def test_matches_explicit_sdpa(flex_ops, mode: str) -> None:
device, dtype, bsz, heads, prefix, action, dim = _shape(mode)
torch.manual_seed(11)
q = torch.randn(bsz, heads, prefix + action, dim, device=device, dtype=dtype, requires_grad=True)
k = torch.randn_like(q, requires_grad=True)
v = torch.randn_like(q, requires_grad=True)
prefix_valid = torch.ones(bsz, prefix, device=device, dtype=torch.bool)
prefix_valid[0, -1] = False
prefix_att = torch.zeros(bsz, prefix, device=device, dtype=torch.bool)
prefix_att[:, prefix // 2 :] = True
action_valid = torch.ones(bsz, action, device=device, dtype=torch.bool)
action_valid[1, -1] = False
out = flex_ops.flex_attention(
q,
k,
v,
prefix_len=prefix,
action_block_size=2,
prefix_valid=prefix_valid,
prefix_att=prefix_att,
action_valid=action_valid,
non_fast_prefix_len=prefix - 1,
)
pm, am = flex_ops.build_block_sparse_bool_masks(
prefix_valid,
prefix_att,
batch=bsz,
prefix_len=prefix,
action_len=action,
action_block_size=2,
non_fast_prefix_len=prefix - 1,
action_valid=action_valid,
device=q.device,
)
pm = torch.where(
pm[:, None],
torch.zeros((), device=q.device, dtype=q.dtype),
torch.full((), flex_ops.MASK_VALUE_F32, device=q.device, dtype=q.dtype),
)
am = torch.where(
am[:, None],
torch.zeros((), device=q.device, dtype=q.dtype),
torch.full((), flex_ops.MASK_VALUE_F32, device=q.device, dtype=q.dtype),
)
expected_p = F.scaled_dot_product_attention(q[:, :, :prefix], k, v, attn_mask=pm, scale=dim**-0.5)
kd = torch.cat([k[:, :, :prefix].detach(), k[:, :, prefix:]], dim=2)
vd = torch.cat([v[:, :, :prefix].detach(), v[:, :, prefix:]], dim=2)
expected_a = F.scaled_dot_product_attention(q[:, :, prefix:], kd, vd, attn_mask=am, scale=dim**-0.5)
expected = torch.cat([expected_p, expected_a], dim=2)
tol = 2e-3 if dtype == torch.bfloat16 else 1e-5
torch.testing.assert_close(out, expected, atol=tol, rtol=tol)
def test_detached_prefix_semantics(flex_ops, mode: str) -> None:
device, dtype, bsz, heads, prefix, action, dim = _shape(mode)
torch.manual_seed(17)
q = torch.randn(bsz, heads, prefix + action, dim, device=device, dtype=dtype, requires_grad=True)
k = torch.randn_like(q, requires_grad=True)
v = torch.randn_like(q, requires_grad=True)
out = flex_ops.flex_attention(q, k, v, prefix_len=prefix, action_block_size=2)
out[:, :, prefix:].float().square().mean().backward()
assert torch.count_nonzero(k.grad[:, :, :prefix]) == 0
assert torch.count_nonzero(v.grad[:, :, :prefix]) == 0
assert torch.count_nonzero(k.grad[:, :, prefix:]) > 0
assert torch.count_nonzero(v.grad[:, :, prefix:]) > 0
def test_dense_attention_mask_path(flex_ops, mode: str) -> None:
device, dtype, bsz, heads, prefix, action, dim = _shape(mode)
torch.manual_seed(23)
q = torch.randn(bsz, heads, prefix + action, dim, device=device, dtype=dtype)
k = torch.randn_like(q)
v = torch.randn_like(q)
pm, am = flex_ops.build_block_sparse_bool_masks(
None,
None,
batch=bsz,
prefix_len=prefix,
action_len=action,
action_block_size=2,
device=q.device,
)
full = torch.cat([pm, am], dim=1)
mask = torch.where(
full[:, None],
torch.zeros((), device=q.device, dtype=q.dtype),
torch.full((), flex_ops.MASK_VALUE_F32, device=q.device, dtype=q.dtype),
)
out_from_dense = flex_ops.flex_attention(q, k, v, prefix_len=prefix, action_block_size=2, attention_mask=mask)
out_from_parts = flex_ops.flex_attention(q, k, v, prefix_len=prefix, action_block_size=2)
tol = 2e-3 if dtype == torch.bfloat16 else 1e-5
torch.testing.assert_close(out_from_dense, out_from_parts, atol=tol, rtol=tol)
def test_manual_matches_reference(flex_ops, mode: str) -> None:
device, dtype, bsz, heads, prefix, action, dim = _shape(mode)
kv_heads = 1 if mode == "full" else heads # GQA on the real-shape run
torch.manual_seed(29)
total = prefix + action
q1 = torch.randn(bsz, heads, total, dim, device=device, dtype=dtype, requires_grad=True)
k1 = torch.randn(bsz, kv_heads, total, dim, device=device, dtype=dtype, requires_grad=True)
v1 = torch.randn_like(k1, requires_grad=True)
q2 = q1.detach().clone().requires_grad_(True)
k2 = k1.detach().clone().requires_grad_(True)
v2 = v1.detach().clone().requires_grad_(True)
prefix_att = torch.zeros(bsz, prefix, device=device, dtype=torch.bool)
prefix_att[:, prefix // 2 :] = True
kwargs = dict(prefix_len=prefix, action_block_size=2, prefix_att=prefix_att)
ref = flex_ops.flex_attention(q1, k1, v1, **kwargs)
got = flex_ops.manual_attention(q2, k2, v2, compile_part=device != "cpu", **kwargs)
# bf16-logits class: the manual path stores logits in the io dtype
# between the GEMM and the fp32 softmax.
tol = 2e-2 if dtype == torch.bfloat16 else 1e-5
torch.testing.assert_close(got, ref, atol=tol, rtol=tol)
ref.float().square().mean().backward()
got.float().square().mean().backward()
for a, b in ((q1, q2), (k1, k2), (v1, v2)):
denom = torch.linalg.vector_norm(a.grad.float()).clamp_min(1e-12)
rel = torch.linalg.vector_norm((a.grad - b.grad).float()) / denom
assert float(rel) <= 2e-2, f"grad rel diff {float(rel)}"
# impl dispatch reaches the same path
via_impl = flex_ops.flex_attention(
q2.detach(), k2.detach(), v2.detach(), impl="manual", **kwargs
)
torch.testing.assert_close(via_impl, got.detach(), atol=tol, rtol=tol)
def test_part_v2_matches_v1(flex_ops, mode: str) -> None:
device, dtype, bsz, heads, prefix, action, dim = _shape(mode)
kv_heads = 1 if mode == "full" else heads
torch.manual_seed(31)
total = prefix + action
q1 = torch.randn(bsz, heads, total, dim, device=device, dtype=dtype, requires_grad=True)
k1 = torch.randn(bsz, kv_heads, total, dim, device=device, dtype=dtype, requires_grad=True)
v1 = torch.randn_like(k1, requires_grad=True)
q2 = q1.detach().clone().requires_grad_(True)
k2 = k1.detach().clone().requires_grad_(True)
v2 = v1.detach().clone().requires_grad_(True)
mask = torch.zeros(bsz, 1, total, total, device=device, dtype=dtype)
mask[:, :, :, total // 2 :] = float(torch.finfo(dtype).min if dtype.is_floating_point else -1e9)
scale = dim**-0.5
ref = flex_ops.manual_attention_part(q1, k1, v1, mask, scale)
got = flex_ops.manual_attention_part_v2(q2, k2, v2, mask, scale)
tol = 2e-2 if dtype == torch.bfloat16 else 1e-5
torch.testing.assert_close(got, ref, atol=tol, rtol=tol)
ref.float().square().mean().backward()
got.float().square().mean().backward()
for a, b in ((q1, q2), (k1, k2), (v1, v2)):
denom = torch.linalg.vector_norm(a.grad.float()).clamp_min(1e-12)
rel = torch.linalg.vector_norm((a.grad - b.grad).float()) / denom
assert float(rel) <= 2e-2, f"v2 grad rel diff {float(rel)}"
def run(flex_ops, mode: str) -> None:
test_matches_explicit_sdpa(flex_ops, mode)
test_detached_prefix_semantics(flex_ops, mode)
test_dense_attention_mask_path(flex_ops, mode)
test_manual_matches_reference(flex_ops, mode)
test_part_v2_matches_v1(flex_ops, mode)
x = torch.ones(1)
torch.testing.assert_close(flex_ops.backend_marker(x), x)
print(f"flashrt-flex-attention-train {mode}: passed")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--backend", choices=["source", "installed"], default="source")
parser.add_argument("--artifact")
parser.add_argument("--mode", choices=["smoke", "full"], default="smoke")
args = parser.parse_args()
run(load_ops(args.backend, args.artifact), args.mode)