mamba3 / benchmarks /bench_vs_upstream.py
phanerozoic's picture
Sync v1 sources and card to main
541791f verified
Raw
History Blame
5.54 kB
"""Head-to-head against upstream's TileLang Mamba-3 MIMO forward.
The two kernels take different dtype contracts at reduced precision. Upstream
keeps the biases, the rank weights and D in float32 and casts trap to the value
dtype; this kernel does the reverse, carrying the biases and rank weights in the
value dtype and the whole schedule in float32. So one float32 master set is
built and each side is handed the cast it requires, which keeps the underlying
values identical.
In float32 the two contracts coincide, so that mode doubles as a correctness
anchor. Both paths run in one process on one device, and TileLang autotunes per
shape, so everything is warmed before it is timed.
PYTHONPATH=~/k/mamba-upstream ~/m3bench/bin/python bench_vs_upstream.py
"""
import argparse
import sys
import time
from pathlib import Path
import torch
import torch.nn.functional as F
M3 = Path.home() / "k" / "mamba3"
UP = Path.home() / "k" / "mamba-upstream"
sys.path.insert(0, str(M3))
sys.path.insert(0, str(UP))
import load_local as mamba3 # noqa: E402
from mamba_ssm.ops.tilelang.mamba3.mamba3_mimo import mamba3_mimo # noqa: E402
# ours: value dtype on the activations, the biases and the rank weights
OURS_CAST = ["q", "k", "v", "z", "q_bias", "k_bias", "mimo_v", "mimo_o", "mimo_z", "D"]
# upstream: value dtype on the activations and trap only
THEIRS_CAST = ["q", "k", "v", "z", "trap"]
def master(B, S, H, G, P, N, R, Na, dev, seed):
"""One float32 set of values; each side casts what it needs."""
torch.manual_seed(seed)
f = lambda *s: torch.randn(*s, device=dev)
dt = F.softplus(-3.0 + f(B, H, S))
return {
"q": f(B, S, R, G, N), "k": f(B, S, R, G, N),
"v": f(B, S, H, P), "z": f(B, S, H, P),
"q_bias": f(H, R, N), "k_bias": f(H, R, N),
"mimo_v": torch.rand(H, R, P, device=dev) / R,
"mimo_o": torch.rand(H, R, P, device=dev) / R,
"mimo_z": torch.rand(H, R, P, device=dev) / R,
"D": f(H), "angles": torch.rand(B, S, H, Na, device=dev),
"dt": dt, "adt": -F.softplus(f(B, H, S)).clamp(max=-1e-4) * dt,
"trap": torch.rand(B, H, S, device=dev) * 0.5,
}
def cast(c, names, dtype):
if dtype is torch.float32:
return dict(c)
return {k: (t.to(dtype).contiguous() if k in names else t) for k, t in c.items()}
def to_split_half(c, N):
"""Reindex q, k and their biases from pairwise rotary layout to split-half.
This kernel rotates the pair (2i, 2i+1); the TileLang kernel rotates
(n, N/2 + n). The two are a permutation of the head dimension, and it is
applied to q and k alike, so every inner product is preserved and the two
compute the same operator on the same values.
"""
idx = torch.cat([torch.arange(0, N, 2), torch.arange(1, N, 2)]).to(c["q"].device)
out = dict(c)
for k in ("q", "k", "q_bias", "k_bias"):
out[k] = c[k].index_select(-1, idx).contiguous()
return out
def ours(c, C):
return mamba3.forward(
c["q"], c["k"], c["v"], c["q_bias"], c["k_bias"], c["mimo_v"], c["mimo_o"],
c["angles"], c["adt"], c["dt"], c["trap"], z=c["z"], mimo_z=c["mimo_z"],
D=c["D"], chunk_size=C)
def theirs(c, C, dtype):
return mamba3_mimo(
c["q"], c["k"], c["v"], c["adt"], c["dt"], c["trap"],
c["q_bias"], c["k_bias"], c["mimo_v"], c["mimo_z"], c["mimo_o"],
c["angles"], c["D"], c["z"],
chunk_size=C, rotary_dim_divisor=2, dtype=dtype)
def timeit(fn, iters, warmup=5):
for _ in range(warmup):
fn()
torch.cuda.synchronize()
t0 = time.perf_counter()
for _ in range(iters):
fn()
torch.cuda.synchronize()
return (time.perf_counter() - t0) / iters * 1e3
def rel(a, b):
a, b = a.float(), b.float()
return float((a - b).abs().max() / b.abs().max().clamp_min(1e-5))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--dtype", default="bfloat16", choices=["bfloat16", "float32"])
ap.add_argument("--iters", type=int, default=20)
args = ap.parse_args()
dtype = getattr(torch, args.dtype)
dev = "cuda"
cap = "".join(map(str, torch.cuda.get_device_capability()))
print(f"torch {torch.__version__} {torch.cuda.get_device_name(0)} "
f"sm_{cap} dtype={args.dtype}\n")
print(f"{'S':>6} {'H':>4} {'P':>4} {'N':>4} {'R':>3} {'C':>4} "
f"{'ours ms':>10} {'tilelang ms':>12} {'ratio':>8} {'max rel':>9}")
B, G = 1, 1
grid = [(512, 8, 64, 128, 4, 16), (1024, 8, 64, 128, 4, 16),
(2048, 8, 64, 128, 4, 16), (4096, 8, 64, 128, 4, 16),
(2048, 32, 64, 128, 4, 16), (2048, 8, 64, 128, 1, 64)]
for S, H, P, N, R, C in grid:
Na = N // 2
m = master(B, S, H, G, P, N, R, Na, dev, seed=0)
co = cast(m, OURS_CAST, dtype)
ct = cast(to_split_half(m, N), THEIRS_CAST, dtype)
try:
yo, yt = ours(co, C), theirs(ct, C, dtype)
r = rel(yo, yt)
to = timeit(lambda: ours(co, C), args.iters)
tt = timeit(lambda: theirs(ct, C, dtype), args.iters)
print(f"{S:6d} {H:4d} {P:4d} {N:4d} {R:3d} {C:4d} "
f"{to:10.3f} {tt:12.3f} {tt / to:7.2f}x {r:9.2e}")
except Exception as exc:
msg = f"{type(exc).__name__}: {exc}".replace("\n", " ")
print(f"{S:6d} {H:4d} {P:4d} {N:4d} {R:3d} {C:4d} {msg[:88]}")
finally:
del m, co, ct
torch.cuda.empty_cache()
return 0
if __name__ == "__main__":
sys.exit(main())