masked-mha-runtime / tests /test_masked_mha_runtime.py
liangsu9988's picture
Promote latest kernel artifacts to main
b9ecbf8 verified
Raw
History Blame Contribute Delete
5.94 kB
#!/usr/bin/env python3
"""Strict source and installed-artifact tests for masked-mha-runtime."""
from __future__ import annotations
import argparse
import importlib
import os
import re
import subprocess
import sys
from pathlib import Path
import torch
ROOT = Path(__file__).resolve().parents[2]
PACKAGE = ROOT / "masked-mha-runtime"
REGISTRATION_INCLUDE = (
ROOT.parent / "kernels" / "kernel-builder" / "src" / "pyproject"
/ "templates" / "torch"
)
class SourceOps:
def __init__(self, namespace: str):
self.ops = getattr(torch.ops, namespace)
@staticmethod
def allocate_workspace(q, k):
stride = (k.shape[0] + 7) // 8 * 8
return torch.empty(
(q.shape[1], q.shape[0], stride), device=q.device, dtype=q.dtype
)
def forward_static(self, q, k, v, *, logits, out, scale=None):
scale = q.shape[-1] ** -0.5 if scale is None else scale
self.ops.forward_static(q, k, v, logits, out, float(scale))
return out
def load_source_ops():
from torch.utils.cpp_extension import load
nvcc = subprocess.check_output(
["nvcc", "--version"], text=True
)
match = re.search(r"release\s+(\d+)\.", nvcc)
torch_cuda_major = int(torch.version.cuda.split(".", 1)[0])
if match and int(match.group(1)) != torch_cuda_major:
raise RuntimeError(
"source test requires PyTorch and nvcc from the same CUDA major; "
f"torch={torch.version.cuda}, nvcc={match.group(1)}.x. Use the "
"installed artifact or a matching isolated build environment."
)
major, minor = torch.cuda.get_device_capability(0)
os.environ.setdefault("TORCH_CUDA_ARCH_LIST", f"{major}.{minor}")
namespace = "masked_mha_runtime_source_test"
load(
name=namespace,
sources=[
str(PACKAGE / "torch-ext" / "torch_binding.cpp"),
str(PACKAGE / "csrc" / "attention_mha_masked.cu"),
],
extra_include_paths=[str(PACKAGE / "csrc"), str(REGISTRATION_INCLUDE)],
extra_cflags=["-O3", "-DCUDA_KERNEL"],
extra_cuda_cflags=["-O3", "--use_fast_math", "-DCUDA_KERNEL"],
extra_ldflags=["-lcublas"],
is_python_module=False,
verbose=False,
)
return SourceOps(namespace)
def load_installed_ops(artifact):
if artifact:
sys.path.insert(0, artifact)
try:
return importlib.import_module("masked_mha_runtime")
finally:
if artifact:
sys.path.remove(artifact)
def metrics(got, ref):
diff = (got.float() - ref.float()).abs()
cosine = torch.nn.functional.cosine_similarity(
got.float().flatten(), ref.float().flatten(), dim=0
).item()
return float(diff.max()), float(torch.quantile(diff.flatten(), 0.99)), float(cosine)
def run_case(ops, dtype, sq, sk, heads, dim, fused_stride=False):
torch.manual_seed(1000 + sq + sk + dim)
if fused_stride:
packed = torch.randn((sk, 3, heads, dim), device="cuda", dtype=dtype)
q = packed[:sq, 0]
k = packed[:, 1]
v = packed[:, 2]
else:
q = torch.randn((sq, heads, dim), device="cuda", dtype=dtype)
k = torch.randn((sk, heads, dim), device="cuda", dtype=dtype)
v = torch.randn_like(k)
logits = ops.allocate_workspace(q, k)
logits.fill_(float("nan"))
out = torch.empty_like(q, memory_format=torch.contiguous_format)
got = ops.forward_static(q, k, v, logits=logits, out=out)
torch.cuda.synchronize()
ref = torch.nn.functional.scaled_dot_product_attention(
q.permute(1, 0, 2).unsqueeze(0).float(),
k.permute(1, 0, 2).unsqueeze(0).float(),
v.permute(1, 0, 2).unsqueeze(0).float(),
).squeeze(0).permute(1, 0, 2).to(dtype)
max_abs, p99_abs, cosine = metrics(got, ref)
atol = 0.00390625 if dtype is torch.float16 else 0.015625
if not torch.isfinite(got.float()).all() or cosine < 0.999 or p99_abs > atol:
raise AssertionError(
f"dtype={dtype} sq={sq} sk={sk} h={heads} d={dim}: "
f"max={max_abs} p99={p99_abs} cos={cosine}"
)
# Preserve fused-QKV token strides. Cloning each view separately can
# normalize a size-one query dimension and destroy the shared stride.
static_q = q
static_k = k
static_v = v
graph = torch.cuda.CUDAGraph()
torch.cuda.synchronize()
with torch.cuda.graph(graph):
ops.forward_static(
static_q, static_k, static_v, logits=logits, out=out
)
graph.replay()
first = out.clone()
graph.replay()
torch.cuda.synchronize()
if not torch.equal(out, first):
raise AssertionError("CUDA Graph replay is not bitwise deterministic")
print(
f"PASS {dtype} sq={sq} sk={sk} h={heads} d={dim} "
f"fused_stride={fused_stride} max={max_abs:.6f} "
f"p99={p99_abs:.6f} cos={cosine:.8f}"
)
def 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()
if not torch.cuda.is_available():
raise SystemExit("CUDA is required")
ops = load_source_ops() if args.backend == "source" else load_installed_ops(args.artifact)
cases = [
(torch.float16, 41, 41, 32, 48, False),
(torch.bfloat16, 41, 41, 32, 48, True),
]
if args.mode == "full":
cases.extend([
(torch.float16, 1, 277, 16, 128, False),
(torch.bfloat16, 1, 1024, 1, 16, True),
(torch.bfloat16, 1, 1025, 1, 16, True),
(torch.bfloat16, 1, 2048, 1, 16, True),
])
for case in cases:
run_case(ops, *case)
print(f"masked-mha-runtime {args.backend} {args.mode}: passed {len(cases)}/{len(cases)}")
if __name__ == "__main__":
main()