eugen-sparse / speed_check.py
JakCwi's picture
Model card summary; English only; scripts renamed (speed_check / demo_collapse)
0c0ac02
Raw History Blame Contribute Delete
3.5 kB
# SPDX-License-Identifier: CC-BY-NC-4.0
# Copyright (c) 2026 Jakub Ćwirlej
"""FFN block micro-benchmark (CUDA): dense SwiGLU vs eugen_sparse (selection + kernel), forward and
forward+backward, bf16, optionally under torch.compile.
python speed_check.py --tokens 65536 --d-model 512 --d-ff 4096 --group-size 32 \
--active-groups 8 --samples-per-group 8 [--compile]
This times one FFN block on random inputs. It is not a prediction of end-to-end training speed:
measure your own training loop. Random inputs spread tokens evenly over the groups; a trained model
is more skewed (popular groups), which the load-balancing bias counteracts.
"""
import argparse
import statistics
import time
import torch
import torch.nn.functional as F
import eugen_sparse as es
def timer(fn, warmup=5, iters=20):
for _ in range(warmup):
fn()
torch.cuda.synchronize()
ts = []
for _ in range(iters):
t0 = time.perf_counter()
fn()
torch.cuda.synchronize()
ts.append((time.perf_counter() - t0) * 1000)
return statistics.median(ts)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--tokens', type=int, default=65536)
ap.add_argument('--d-model', type=int, default=512)
ap.add_argument('--d-ff', type=int, default=4096)
ap.add_argument('--group-size', type=int, default=32)
ap.add_argument('--active-groups', type=int, default=8)
ap.add_argument('--samples-per-group', type=int, default=8)
ap.add_argument('--compile', action='store_true')
a = ap.parse_args()
assert torch.cuda.is_available(), 'CUDA required'
G, kg = a.group_size, a.active_groups
if not es.kernel_supported(True, a.d_model, a.d_ff, G, kg):
raise SystemExit('shape not supported by the kernel (see kernel_supported)')
dt = torch.bfloat16
torch.manual_seed(1)
x = torch.randn(a.tokens, a.d_model, device='cuda', dtype=dt)
gw, uw = (torch.randn(a.d_ff, a.d_model, device='cuda', dtype=dt) * a.d_model ** -0.5 for _ in range(2))
dw = torch.randn(a.d_model, a.d_ff, device='cuda', dtype=dt) * a.d_ff ** -0.5
dy = torch.randn_like(x)
sub, spg = es.make_subsample_index(a.d_ff, G, a.samples_per_group)
sub = sub.cuda()
def dense(x_, g_, u_, d_):
return F.linear(F.silu(F.linear(x_, g_)) * F.linear(x_, u_), d_)
def sparse(x_, g_, u_, d_):
idx = es.grouped_topk_select(x_, g_, G, kg, sub, spg)
return es.sparse_swiglu_ffn(x_, g_, u_, d_, idx, G)
print(f'tokens={a.tokens} d_model={a.d_model} d_ff={a.d_ff} group_size={G} active_groups={kg} '
f'(density {kg * G / a.d_ff:.2%}) compile={a.compile} | {torch.cuda.get_device_name(0)} | '
f'torch {torch.__version__}')
print('plan:', es.plan(a.tokens, a.d_model, a.d_ff, G, kg))
res = {}
for name, fn in (('dense', dense), ('sparse', sparse)):
if a.compile:
fn = torch.compile(fn, fullgraph=True, dynamic=False)
t_f = timer(lambda: fn(x, gw, uw, dw))
leaves = tuple(t.clone().requires_grad_() for t in (x, gw, uw, dw))
def fb(fn=fn, l=leaves):
torch.autograd.grad(fn(*l), l, dy)
t_fb = timer(fb)
res[name] = (t_f, t_fb)
print(f' {name:6s} fwd {t_f:8.2f} ms fwd+bwd {t_fb:8.2f} ms')
print(f' sparse vs dense: fwd x{res["dense"][0] / res["sparse"][0]:.2f} '
f'fwd+bwd x{res["dense"][1] / res["sparse"][1]:.2f} (>1 = sparse faster)')
if __name__ == '__main__':
main()