Skip to content

Commit bf1e763

Browse files
committed
add attention benchmark
Signed-off-by: Vladimir Mandic <mandic00@live.com>
1 parent 3aac1f6 commit bf1e763

1 file changed

Lines changed: 241 additions & 0 deletions

File tree

cli/benchmark_attention.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import time
2+
import warnings
3+
import torch
4+
import torch.nn.functional as F
5+
from typing import Dict, Any
6+
7+
warnings.filterwarnings("ignore", category=UserWarning)
8+
9+
warmup = 2
10+
repeats = 50
11+
dtypes = [torch.bfloat16] # , torch.float16]
12+
# if hasattr(torch, "float8_e4m3fn"):
13+
# dtypes.append(torch.float8_e4m3fn)
14+
15+
PROFILES = {
16+
"sdxl": {"l_q": 4096, "l_k": 4096, "h": 32, "d": 128},
17+
"flux.1": {"l_q": 16717, "l_k": 16717, "h": 24, "d": 128},
18+
"sd35": {"l_q": 16538, "l_k": 16538, "h": 24, "d": 128},
19+
"qwen-image": {"l_q": 16384, "l_k": 16384, "h": 24, "d": 128},
20+
"z-image": {"l_q": 4096, "l_k": 4096, "h": 32, "d": 120},
21+
"wan2.1": {"l_q": 16384, "l_k": 16384, "h": 40, "d": 128},
22+
}
23+
24+
def get_stats(reset: bool = False):
25+
torch.cuda.synchronize()
26+
if reset:
27+
with torch.no_grad():
28+
torch.cuda.empty_cache()
29+
torch.cuda.reset_peak_memory_stats()
30+
m = torch.cuda.max_memory_allocated()
31+
t = time.perf_counter()
32+
return m / (1024 ** 2), t
33+
34+
def print_gpu_info():
35+
if not torch.cuda.is_available():
36+
print("GPU: Not available")
37+
return
38+
39+
device = torch.cuda.current_device()
40+
props = torch.cuda.get_device_properties(device)
41+
total_mem = props.total_memory / (1024**3)
42+
free_mem, _ = torch.cuda.mem_get_info(device)
43+
free_mem = free_mem / (1024**3)
44+
major, minor = torch.cuda.get_device_capability(device)
45+
46+
print(f"gpu: {torch.cuda.get_device_name(device)}")
47+
print(f"vram: total={total_mem:.2f}GB free={free_mem:.2f}GB")
48+
print(f"cuda: capability={major}.{minor} version={torch.version.cuda}")
49+
print(f"torch: {torch.__version__}")
50+
51+
def benchmark_attention(
52+
backend: str,
53+
dtype: torch.dtype,
54+
b: int = 1,
55+
l_q: int = 4096,
56+
l_k: int = 4096,
57+
h: int = 32,
58+
d: int = 128,
59+
warmup: int = 10,
60+
repeats: int = 100
61+
) -> Dict[str, Any]:
62+
device = "cuda" if torch.cuda.is_available() else "cpu"
63+
64+
# Initialize tensors
65+
q = torch.randn(b, h, l_q, d, device=device, dtype=torch.float16 if dtype.is_floating_point and dtype.itemsize == 1 else dtype, requires_grad=False).to(dtype)
66+
k = torch.randn(b, h, l_k, d, device=device, dtype=torch.float16 if dtype.is_floating_point and dtype.itemsize == 1 else dtype, requires_grad=False).to(dtype)
67+
v = torch.randn(b, h, l_k, d, device=device, dtype=torch.float16 if dtype.is_floating_point and dtype.itemsize == 1 else dtype, requires_grad=False).to(dtype)
68+
69+
results = {
70+
"backend": backend,
71+
"dtype": str(dtype),
72+
"status": "pass",
73+
"latency_ms": 0.0,
74+
"memory_mb": 0.0,
75+
"version": "N/A",
76+
"error": ""
77+
}
78+
try:
79+
if backend.startswith("sdpa_"):
80+
from torch.nn.attention import sdpa_kernel, SDPBackend
81+
sdp_type = backend[len("sdpa_"):]
82+
# Map friendly names to new SDPA backends
83+
backend_map = {
84+
"math": [SDPBackend.MATH],
85+
"flash": [SDPBackend.FLASH_ATTENTION],
86+
"mem_efficient": [SDPBackend.EFFICIENT_ATTENTION],
87+
"all": [SDPBackend.FLASH_ATTENTION, SDPBackend.EFFICIENT_ATTENTION, SDPBackend.MATH]
88+
}
89+
if sdp_type not in backend_map:
90+
raise ValueError(f"Unknown SDPA type: {sdp_type}")
91+
92+
results["version"] = torch.__version__
93+
94+
with sdpa_kernel(backend_map[sdp_type]):
95+
# Warmup
96+
for _ in range(warmup):
97+
_ = F.scaled_dot_product_attention(q, k, v)
98+
99+
start_mem, start_time = get_stats(True)
100+
101+
for _ in range(repeats):
102+
_ = F.scaled_dot_product_attention(q, k, v)
103+
104+
end_mem, end_time = get_stats()
105+
106+
results["latency_ms"] = (end_time - start_time) / repeats * 1000
107+
results["memory_mb"] = end_mem - start_mem
108+
109+
elif backend == "flash_attn":
110+
from flash_attn import flash_attn_func, __version__ as fa_version
111+
results["version"] = fa_version
112+
# Flash attention usually expects (B, L, H, D)
113+
q_fa = q.transpose(1, 2)
114+
k_fa = k.transpose(1, 2)
115+
v_fa = v.transpose(1, 2)
116+
117+
for _ in range(warmup):
118+
_ = flash_attn_func(q_fa, k_fa, v_fa)
119+
120+
start_mem, start_time = get_stats(True)
121+
122+
for _ in range(repeats):
123+
_ = flash_attn_func(q_fa, k_fa, v_fa)
124+
125+
end_mem, end_time = get_stats()
126+
127+
results["latency_ms"] = (end_time - start_time) / repeats * 1000
128+
results["memory_mb"] = end_mem - start_mem
129+
130+
elif backend == "xformers":
131+
from xformers.ops import memory_efficient_attention
132+
from xformers import __version__ as xf_version
133+
results["version"] = xf_version
134+
# xformers also usually prefers (B, L, H, D)
135+
q_xf = q.transpose(1, 2)
136+
k_xf = k.transpose(1, 2)
137+
v_xf = v.transpose(1, 2)
138+
139+
for _ in range(warmup):
140+
_ = memory_efficient_attention(q_xf, k_xf, v_xf)
141+
142+
start_mem, start_time = get_stats(True)
143+
144+
for _ in range(repeats):
145+
_ = memory_efficient_attention(q_xf, k_xf, v_xf)
146+
147+
end_mem, end_time = get_stats()
148+
149+
results["latency_ms"] = (end_time - start_time) / repeats * 1000
150+
results["memory_mb"] = end_mem - start_mem
151+
152+
elif backend == "sage_attn":
153+
from sageattention import sageattn
154+
import sageattention
155+
# Attempt to get version from package metadata or a common attribute
156+
try:
157+
import importlib.metadata
158+
results["version"] = importlib.metadata.version("sageattention")
159+
except Exception:
160+
results["version"] = getattr(sageattention, "__version__", "N/A")
161+
162+
# SageAttention expects (B, H, L, D) logic
163+
for _ in range(warmup):
164+
_ = sageattn(q, k, v)
165+
166+
start_mem, start_time = get_stats(True)
167+
168+
for _ in range(repeats):
169+
_ = sageattn(q, k, v)
170+
171+
end_mem, end_time = get_stats()
172+
173+
results["latency_ms"] = (end_time - start_time) / repeats * 1000
174+
results["memory_mb"] = end_mem - start_mem
175+
176+
elif backend == "flex_attention":
177+
from torch.nn.attention.flex_attention import flex_attention
178+
results["version"] = torch.__version__
179+
180+
# flex_attention requires torch.compile for performance
181+
flex_attention_compiled = torch.compile(flex_attention, dynamic=False)
182+
183+
# Warmup (important to trigger compilation)
184+
for _ in range(warmup):
185+
_ = flex_attention_compiled(q, k, v)
186+
187+
start_mem, start_time = get_stats(True)
188+
189+
for _ in range(repeats):
190+
_ = flex_attention_compiled(q, k, v)
191+
192+
end_mem, end_time = get_stats()
193+
194+
results["latency_ms"] = (end_time - start_time) / repeats * 1000
195+
results["memory_mb"] = end_mem - start_mem
196+
except Exception as e:
197+
results["status"] = "fail"
198+
results["error"] = str(e)[:49]
199+
200+
return results
201+
202+
def main():
203+
backends = [
204+
"sdpa_math",
205+
"sdpa_mem_efficient",
206+
"sdpa_flash",
207+
"flex_attention",
208+
"xformers",
209+
"flash_attn",
210+
"sage_attn",
211+
]
212+
213+
all_results = []
214+
215+
print_gpu_info()
216+
print(f'config: warmup={warmup} repeats={repeats} dtypes={dtypes}')
217+
for name, config in PROFILES.items():
218+
print(f"profile: {name} (L_q={config['l_q']}, L_k={config['l_k']}, H={config['h']}, D={config['d']})")
219+
for dtype in dtypes:
220+
print(f" dtype: {dtype}")
221+
print(f" {'backend':<20} | {'version':<12} | {'status':<8} | {'latency':<10} | {'memory':<12} | ")
222+
for backend in backends:
223+
res = benchmark_attention(
224+
backend,
225+
dtype,
226+
l_q=config["l_q"],
227+
l_k=config["l_k"],
228+
h=config["h"],
229+
d=config["d"],
230+
warmup=warmup,
231+
repeats=repeats
232+
)
233+
all_results.append(res)
234+
235+
latency = f"{res['latency_ms']:.4f} ms"
236+
memory = f"{res['memory_mb']:.2f} MB"
237+
238+
print(f" {res['backend']:<20} | {res['version']:<12} | {res['status']:<8} | {latency:<10} | {memory:<12} | {res['error']}")
239+
240+
if __name__ == "__main__":
241+
main()

0 commit comments

Comments
 (0)