File size: 7,811 Bytes
1d30035 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | # SPDX-License-Identifier: Apache-2.0
"""mb1_sdpa — ``ttnn.transformer.scaled_dot_product_attention`` timings for the GR00T attention shapes.
Shape classes (arch synthesis §1 / perf-model-v2 §2.3):
* ViT (SigLIP / Qwen3-VL vision): non-causal, B = images as batch (1, 2, 4), 16 heads, S in {256, 352, 448},
head_dim 96 (SigLIP 72 padded to 96, ttnn-blocks-vlm.md line 114) and 64 (Qwen3-VL ViT).
* LLM (Qwen3 decoder prefill): causal GQA, 16 q / 8 kv heads, S in {128, 256, 512, 1024}, head_dim 128.
* DiT (action head): 32 heads, Sq = 64 (49/51/41 padded), Sk in {64, 128, 320, 512}, head_dim 64 (48 padded), non-causal
(self-attention Sk = 64, cross-attention Sk = VL tokens).
For each shape the default program config and a sweep of ``SDPAProgramConfig(q_chunk_size, k_chunk_size)`` over
{32, 64, 128, 256, 512} are timed (trace replay of ``--iters`` calls); FLOP = 4 * B * H * Sq * Sk * d (halved for causal).
Usage:
with-device.sh python models/experimental/gr00t/benchmarks/mb1_sdpa.py --out results/mb1_sdpa.json
"""
from __future__ import annotations
import argparse
import itertools
import math
import os
import sys
import time
from typing import Any, Dict, List, Optional
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import mb1_common as C # noqa: E402
CHUNKS = [32, 64, 128, 256, 512]
def cases(args) -> List[Dict[str, Any]]:
out = []
if "vit" in args.groups:
for B, S, d in itertools.product([1, 2, 4], [256, 352, 448], [96, 64]):
out.append(dict(group="vit", B=B, H=16, Hkv=16, Sq=S, Sk=S, d=d, causal=False))
if "llm" in args.groups:
for S in [128, 256, 512, 1024]:
out.append(dict(group="llm", B=1, H=16, Hkv=8, Sq=S, Sk=S, d=128, causal=True))
if "dit" in args.groups:
for Sk in [64, 128, 320, 512]:
out.append(dict(group="dit", B=1, H=32, Hkv=32, Sq=64, Sk=Sk, d=64, causal=False))
if args.quick:
out = [c for c in out if c["B"] == 1][:6]
return out
def run_case(ttnn, device, args, case: Dict[str, Any]) -> List[Dict[str, Any]]:
import torch
B, H, Hkv, Sq, Sk, d = case["B"], case["H"], case["Hkv"], case["Sq"], case["Sk"], case["d"]
g = torch.Generator().manual_seed(0)
q_t = torch.randn((B, H, Sq, d), generator=g)
k_t = torch.randn((B, Hkv, Sk, d), generator=g)
v_t = torch.randn((B, Hkv, Sk, d), generator=g)
q = ttnn.from_torch(q_t, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT, device=device, pad_value=0.0)
k = ttnn.from_torch(k_t, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT, device=device, pad_value=0.0)
v = ttnn.from_torch(v_t, dtype=ttnn.bfloat16, layout=ttnn.TILE_LAYOUT, device=device, pad_value=0.0)
ck = C.compute_config(ttnn, args.fidelity, args.fp32_acc)
flop = 4.0 * B * H * Sq * Sk * d * (0.5 if case["causal"] else 1.0)
# reference
if Hkv != H:
rep = H // Hkv
k_ref = k_t.reshape(B, Hkv, 1, Sk, d).repeat(1, 1, rep, 1, 1).reshape(B, H, Sk, d)
v_ref = v_t.reshape(B, Hkv, 1, Sk, d).repeat(1, 1, rep, 1, 1).reshape(B, H, Sk, d)
else:
k_ref, v_ref = k_t, v_t
ref = torch.nn.functional.scaled_dot_product_attention(q_t, k_ref, v_ref, is_causal=case["causal"])
rows = []
grid = device.compute_with_storage_grid_size()
combos: List[Optional[Dict[str, int]]] = [None]
q_opts = [c for c in CHUNKS if c <= max(32, Sq)]
k_opts = [c for c in CHUNKS if c <= max(32, Sk)]
if args.quick:
q_opts, k_opts = q_opts[-2:], k_opts[-2:]
combos += [dict(q=qc, k=kc) for qc in q_opts for kc in k_opts]
try:
for combo in combos:
row = dict(case)
row.update(fidelity=args.fidelity, fp32_acc=args.fp32_acc, gflop=round(flop / 1e9, 3))
try:
pc = None
if combo is not None:
pc = ttnn.SDPAProgramConfig(
compute_with_storage_grid_size=grid,
q_chunk_size=combo["q"],
k_chunk_size=combo["k"],
exp_approx_mode=args.exp_approx,
)
row["q_chunk"], row["k_chunk"] = combo["q"], combo["k"]
else:
row["q_chunk"], row["k_chunk"] = "default", "default"
def fn():
return ttnn.transformer.scaled_dot_product_attention(
q, k, v, is_causal=case["causal"], program_config=pc, compute_kernel_config=ck
)
if not args.no_check:
out = fn()
got = ttnn.to_torch(out).float()[:, :, :Sq, :]
row["pcc"] = round(pcc(got, ref), 5)
ttnn.deallocate(out)
t = C.measure(ttnn, device, fn, iters=args.iters, warmup=2, reps=args.reps)
row.update(
eager_us=C.r(t.eager_us),
trace_us=C.r(t.trace_us),
tflops_trace=C.r(C.tflops(flop, t.trace_us)),
error=t.error,
)
except Exception as e: # noqa: BLE001
row["error"] = C.short_error(e)
rows.append(row)
print(
f" {case['group']} B={B} H={H}/{Hkv} Sq={Sq} Sk={Sk} d={d} q={row['q_chunk']} k={row['k_chunk']}: trace {row.get('trace_us')} us eager {row.get('eager_us')} pcc={row.get('pcc')} {row.get('error') or ''}"
)
finally:
for t_ in (q, k, v):
ttnn.deallocate(t_)
return rows
def pcc(a, b) -> float:
import torch
a = a.flatten().double()
b = b.flatten().double()
return float(torch.corrcoef(torch.stack([a, b]))[0, 1])
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--out", default=os.path.join(C.default_out_dir(), "mb1_sdpa.json"))
ap.add_argument("--groups", default="vit,llm,dit")
ap.add_argument("--fidelity", default="HiFi2")
ap.add_argument("--fp32-acc", action="store_true")
ap.add_argument("--exp-approx", action="store_true", default=False)
ap.add_argument("--iters", type=int, default=16)
ap.add_argument("--reps", type=int, default=5)
ap.add_argument("--quick", action="store_true")
ap.add_argument("--no-check", action="store_true")
args = ap.parse_args()
import ttnn
device = C.open_device(ttnn)
result: Dict[str, Any] = {
"env": C.env_report(),
"device": C.device_report(ttnn, device),
"args": vars(args),
"rows": [],
}
t0 = time.perf_counter()
try:
for case in cases(args):
result["rows"].extend(run_case(ttnn, device, args, case))
C.write_json(args.out, result)
finally:
result["wall_s"] = round(time.perf_counter() - t0, 1)
C.write_json(args.out, result)
ttnn.close_device(device)
bests = []
seen = set()
for rw in result["rows"]:
key = (rw["group"], rw["B"], rw["Sq"], rw["Sk"], rw["d"])
if key in seen:
continue
seen.add(key)
cands = [
x for x in result["rows"] if (x["group"], x["B"], x["Sq"], x["Sk"], x["d"]) == key and x.get("trace_us")
]
if cands:
bests.append(min(cands, key=lambda x: x["trace_us"]))
C.print_table(
bests,
[
"group",
"B",
"H",
"Hkv",
"Sq",
"Sk",
"d",
"q_chunk",
"k_chunk",
"trace_us",
"eager_us",
"tflops_trace",
"pcc",
],
"best chunk config per SDPA shape",
)
return 0
if __name__ == "__main__":
sys.exit(main())
|