[DSA] Q8KV8 FP8 Sparse Prefill on GLM-5.2 & DeepSeek-V3.2: Q8-Path & Shared-Path Optimizations (#31888)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ho-Ren (Jack) Chuang
2026-07-30 15:15:11 +08:00
committed by GitHub
co-authored by Claude Fable 5
parent 4f51dad1da
commit e4a40a71f8
30 changed files with 3837 additions and 52 deletions
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""Microbenchmark: Q8KV8 sparse-prefill KV gather overhaul.
Compares the legacy gather (``gather_dequant_requant_fp8_paged_legacy``:
fresh ``torch.zeros`` destination + one program per (token, 128-elem
slice)) against the new gather (``gather_dequant_requant_fp8_paged``:
no pre-zeroing needed, fused pad-row zero-fill, TOKENS_PER_PROG tokens
per program with 16B-vectorized access), in three flavors:
legacy torch.zeros alloc + legacy kernel (baseline)
new_alloc torch.empty alloc + vectorized kernel (vectorized copy only)
new_cached persistent grow-only buffer + vec kernel (changes 1 + 2,
= production path)
For each scenario it checks BIT-EXACT equality of the fp8 output bytes
(``torch.equal`` on ``uint8`` views; the requant scale is the identity
scalar 1.0 on this path, so the buffer is the entire output), including
the `topk` zero landing-pad rows, then reports us/call and effective
TB/s.
Shapes model GLM / DeepSeek-V3.2 DSA prefill on one rank: d = 576 fp8
out (512 nope + 64 rope), 656 B/token paged cache rows (512 nope fp8 +
16 B f32 group scales + 128 B bf16 rope), page_size 64, topk 2048.
NOTE: gather traffic scales with kv_len (= number of gathered KV rows
= len(page_table_1_flattened)), NOT with s_q; s_q below only labels the
chunk that a scenario represents. The GLM-5.2 il=64k profile point
(112.0 us/call, ~0.7 TB/s effective) corresponds to the
(s_q=4096, kv_len=65536) row.
Usage (single GPU, < 2 min):
python benchmark/kernels/deepseek/benchmark_q8kv8_kv_gather.py [--device cuda:0]
[--iters 200] [--warmup 20]
Also runs correctness-only edge cases: ragged tail (kv_len % 4 != 0),
extra_rows=0, num_tokens=0, and a cached-buffer SHRINK reuse (big call
then small call) that proves stale bytes from the earlier, larger call
cannot leak into the smaller call's pad rows.
"""
import argparse
import importlib.util
import sys
from pathlib import Path
import torch
_REPO_ROOT = Path(__file__).resolve().parents[3]
_MOD_PATH = _REPO_ROOT / "python/sglang/kernels/ops/attention/dsa/dequant_k_cache.py"
# Import the module straight from its file so the benchmark stays
# standalone (no sglang package import side effects; needs only
# torch + triton).
_spec = importlib.util.spec_from_file_location("dequant_k_cache", _MOD_PATH)
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
gather_new = _mod.gather_dequant_requant_fp8_paged
gather_legacy = _mod.gather_dequant_requant_fp8_paged_legacy
PAGE_SIZE = 64
DIM_QUANT = 656 # 512 nope fp8 + 16 scale bytes + 128 rope bytes
OUT_DIM = 576 # 512 nope + 64 rope, fp8
TOPK = 2048
# (s_q label, kv_len = gathered rows). kv_len drives the bytes moved.
SCENARIOS = [
(512, 8192),
(2048, 32768),
(4096, 65536),
]
def build_paged_kv_pool(pool_tokens: int, device: str) -> torch.Tensor:
"""Synthetic paged fp8 KV cache: [pool_tokens, 1, 656] fp8_e4m3fn."""
g = torch.Generator(device=device).manual_seed(0)
nope = (torch.randn(pool_tokens, 512, generator=g, device=device) * 2.0).to(
torch.float8_e4m3fn
)
# Positive, realistically small per-group dequant scales.
scales = (torch.rand(pool_tokens, 4, generator=g, device=device) * 0.05 + 1e-3).to(
torch.float32
)
rope = torch.randn(pool_tokens, 64, generator=g, device=device).to(torch.bfloat16)
raw = torch.empty(pool_tokens, DIM_QUANT, dtype=torch.uint8, device=device)
raw[:, :512] = nope.view(torch.uint8)
raw[:, 512:528] = scales.view(torch.uint8)
raw[:, 528:] = rope.view(torch.uint8)
return raw.view(torch.float8_e4m3fn).view(pool_tokens, 1, DIM_QUANT)
def build_page_table_flattened(
kv_lens, pool_tokens: int, device: str, seed: int = 1
) -> torch.Tensor:
"""Realistic page_table_1_flattened: per request, random distinct
64-token pages, tokens contiguous within a page (production paged
layout), requests concatenated."""
n_pool_pages = pool_tokens // PAGE_SIZE
g = torch.Generator(device="cpu").manual_seed(seed)
parts = []
for kv_len in kv_lens:
n_pages = (kv_len + PAGE_SIZE - 1) // PAGE_SIZE
assert n_pages <= n_pool_pages, "pool too small for scenario"
pages = torch.randperm(n_pool_pages, generator=g)[:n_pages]
toks = (pages[:, None] * PAGE_SIZE + torch.arange(PAGE_SIZE)[None, :]).reshape(
-1
)[:kv_len]
parts.append(toks)
return torch.cat(parts).to(torch.int32).to(device)
class CachedGather:
"""Mimics the dsa_backend production path: persistent grow-only fp8
destination buffer, gather zero-fills only the pad tail in-kernel."""
def __init__(self):
self.buf = None
def __call__(self, pool, pt, extra_rows):
total = pt.shape[0] + extra_rows
if self.buf is None or self.buf.shape[0] < total:
self.buf = torch.empty(
(total, OUT_DIM), dtype=torch.float8_e4m3fn, device=pool.device
)
return gather_new(pool, pt, extra_rows=extra_rows, out=self.buf[:total])
def assert_bit_exact(ref: torch.Tensor, got: torch.Tensor, what: str):
assert ref.shape == got.shape, f"{what}: shape {got.shape} != {ref.shape}"
ok = torch.equal(
ref.contiguous().view(torch.uint8), got.contiguous().view(torch.uint8)
)
assert ok, f"{what}: fp8 bytes NOT bit-exact"
def bench_us(fn, warmup: int, iters: int) -> float:
for _ in range(warmup):
fn()
torch.cuda.synchronize()
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
start.record()
for _ in range(iters):
fn()
end.record()
torch.cuda.synchronize()
return start.elapsed_time(end) * 1000.0 / iters # ms -> us
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--device", default="cuda:0")
ap.add_argument("--iters", type=int, default=200)
ap.add_argument("--warmup", type=int, default=20)
args = ap.parse_args()
if not torch.cuda.is_available():
print("CUDA not available; this microbench needs 1 GPU.", file=sys.stderr)
sys.exit(1)
torch.cuda.set_device(args.device)
dev = args.device
pool_tokens = 131072 # 2x the largest kv_len; 131072*656 B ~ 86 MB
pool = build_paged_kv_pool(pool_tokens, dev)
print(f"device={dev} ({torch.cuda.get_device_name(dev)})")
print(f"pool: {pool_tokens} tokens x {DIM_QUANT} B, topk={TOPK}")
print()
# ------------------------------------------------------------------
# Correctness edge cases (not timed)
# ------------------------------------------------------------------
print("== correctness edge cases ==")
cached_edge = CachedGather()
for kv_len, extra in [(1234, TOPK), (8192, 0), (0, TOPK), (63, 17)]:
pt = build_page_table_flattened([kv_len], pool_tokens, dev, seed=7)
ref = gather_legacy(pool, pt, extra_rows=extra)
got_alloc = gather_new(pool, pt, extra_rows=extra)
got_cached = cached_edge(pool, pt, extra_rows=extra)
assert_bit_exact(ref, got_alloc, f"kv_len={kv_len},extra={extra} new_alloc")
assert_bit_exact(ref, got_cached, f"kv_len={kv_len},extra={extra} new_cached")
# Pad rows must be exactly zero bytes.
if extra > 0:
pad = got_cached[kv_len:].view(torch.uint8)
assert int(pad.max()) == 0 if pad.numel() else True
print(f" kv_len={kv_len:6d} extra_rows={extra:5d}: bit-exact OK")
# Cached-buffer SHRINK reuse: big call dirties the buffer, then a
# smaller call must still produce zero pad rows (stale-data test for
# the grow-only buffer + tail-only zeroing invariant).
cached_shrink = CachedGather()
pt_big = build_page_table_flattened([65536], pool_tokens, dev, seed=11)
cached_shrink(pool, pt_big, TOPK)
pt_small = build_page_table_flattened([4096], pool_tokens, dev, seed=13)
ref_small = gather_legacy(pool, pt_small, extra_rows=TOPK)
got_small = cached_shrink(pool, pt_small, TOPK)
assert_bit_exact(ref_small, got_small, "shrink-reuse (65536 -> 4096)")
print(" shrink-reuse 65536 -> 4096 rows: pad rows clean, bit-exact OK")
print()
# ------------------------------------------------------------------
# Timed scenarios
# ------------------------------------------------------------------
print("== timing ==")
print(
"metrics: us/call = mean wall time per gather call incl. any alloc/"
"zero-fill (LOWER = faster); eff TB/s = payload (656+4 B/token read"
" + 576 B/row written incl. pad) / time (HIGHER = faster);"
" speedup = legacy_us / variant_us (>1 = faster than legacy)."
)
header = (
f"{'s_q':>5} {'kv_len':>7} {'variant':>10} {'us/call':>9} "
f"{'eff TB/s':>9} {'speedup':>8}"
)
print(header)
print("-" * len(header))
for s_q, kv_len in SCENARIOS:
pt = build_page_table_flattened([kv_len], pool_tokens, dev, seed=s_q)
total_rows = kv_len + TOPK
payload_bytes = kv_len * (DIM_QUANT + 4) + total_rows * OUT_DIM
cached = CachedGather()
variants = [
("legacy", lambda: gather_legacy(pool, pt, extra_rows=TOPK)),
("new_alloc", lambda: gather_new(pool, pt, extra_rows=TOPK)),
("new_cached", lambda: cached(pool, pt, TOPK)),
]
# Bit-exactness at the benchmarked shape before timing.
ref = gather_legacy(pool, pt, extra_rows=TOPK)
for name, fn in variants[1:]:
assert_bit_exact(ref, fn(), f"s_q={s_q} {name}")
del ref
legacy_us = None
for name, fn in variants:
us = bench_us(fn, args.warmup, args.iters)
tbps = payload_bytes / (us * 1e-6) / 1e12
if name == "legacy":
legacy_us = us
speedup = "1.00x"
else:
speedup = f"{legacy_us / us:.2f}x"
print(
f"{s_q:>5} {kv_len:>7} {name:>10} {us:>9.1f} "
f"{tbps:>9.3f} {speedup:>8}"
)
print()
print(
"note: legacy additionally writes a full-buffer zero fill "
f"({total_rows * OUT_DIM / 1e6:.1f} MB at the largest shape) that is "
"NOT counted in its payload bytes; its true HW bandwidth is higher "
"than the eff TB/s shown, which is exactly why us/call is the "
"decision metric."
)
print("ALL CHECKS PASSED")
if __name__ == "__main__":
main()
+486
View File
@@ -0,0 +1,486 @@
#!/usr/bin/env python3
"""Microbenchmark: Q8KV8 sparse-prefill q-prep — old path vs born-fp8 fused path.
Old path (production default):
1. q_nope_out = torch.bmm(q_nope.transpose(0, 1), w_kc).transpose(0, 1)
(cublas bf16 bmm, writes bf16 [H, T, N] to DRAM)
2. concat_and_cast_q_fp8_pad(q_fp8, q_nope_out, q_rope, H)
(Triton: re-reads the bf16 bmm output + q_rope, writes fp8 [T, H, N+R])
New path (SGLANG_ENABLE_DSA_Q8KV8_BORN_FP8_Q):
absorbed_bmm_concat_cast_q_fp8(q_fp8, q_nope, w_kc, q_rope, H)
(one Triton kernel: bmm + concat + fp8 cast; the bf16 q_nope_out and the
standalone concat-cast launch disappear)
Non-power-of-2 K (GLM 192) K-dimension codegen variants (A/B'd here; all keep
the identical fp32 -> bf16 -> fp8 epilogue, see cache_ops.py):
loop : split-K loop, BLOCK_K=64 x 3 (the original K=192 path)
two_dot : preload a once as 128+64 tiles, two chained tl.dot, no K-loop
three_dot : preload a once as 3 x 64 tiles, three chained tl.dot
(same fp32 add order as `loop`, loads hoisted)
pad : single tl.dot at BLOCK_K=256 with zero-masked k tail
single_k : single tl.dot at BLOCK_K=192 -- documents the Triton
non-power-of-2 tl.arange limitation (compile fails <= 3.5.x)
Power-of-2 K (DeepSeek 128) collapses every variant to the same single-dot
fast path, so only one NEW row is shown there.
Shapes (both models: N = kv_lora_rank = 512, R = qk_rope_head_dim = 64;
K = qk_nope_head_dim differs per model):
GLM-5.2: heads = 64, K = 192 (w_kc [64, 192, 512]; DP attention,
per-rank full heads; K=192 exercises the kernel's split-K path)
DS-V3.2: heads = 128, K = 128 (power-of-2 K, preload-once fast path)
Metric conventions:
* time is reported in microseconds per call (us/call) — LOWER = FASTER.
* bandwidth is analytic-bytes / time in GB/s — HIGHER = BETTER.
* "speedup x" = old_time / new_time — >1.0 means the NEW path is faster.
Correctness:
* rope half must be BIT-EXACT (same bf16 source, same Triton conversion).
* nope half: same rounding stages (fp32 accum -> bf16 -> fp8) but a
different GEMM accumulation order than cublas -> near- but not
guaranteed bit-exact. We report the bitwise-match fraction, the max
dequantized |diff|, and which path lands closer to an fp64 reference.
Usage (single GPU):
python benchmark/kernels/deepseek/benchmark_q8kv8_q_prep.py # both model shapes
python benchmark/kernels/deepseek/benchmark_q8kv8_q_prep.py --tokens 8192 --iters 300
python benchmark/kernels/deepseek/benchmark_q8kv8_q_prep.py --variants two_dot,pad
python benchmark/kernels/deepseek/benchmark_q8kv8_q_prep.py --sweep # + tile/warp sweep
python benchmark/kernels/deepseek/benchmark_q8kv8_q_prep.py --rounding-study
"""
import argparse
import torch
from sglang.kernels.ops.kvcache.cache_ops import (
absorbed_bmm_concat_cast_q_fp8,
concat_and_cast_q_fp8_pad,
)
N_LORA = 512 # kv_lora_rank (post-absorb q_nope dim; "d_nope" at the kernel)
R_ROPE = 64 # qk_rope_head_dim
def make_inputs(
num_tokens: int,
num_heads: int,
k_nope: int,
device,
seed: int,
magnitude: float,
):
g = torch.Generator(device=device).manual_seed(seed)
# Production layout: q = q_b_proj output [T, H, K+R] bf16; q_nope/q_rope are
# strided views of it (rope applied in-place on the q_rope slice).
q = (
torch.randn(
(num_tokens, num_heads, k_nope + R_ROPE),
generator=g,
device=device,
dtype=torch.float32,
)
* magnitude
).to(torch.bfloat16)
q_nope = q[..., :k_nope]
q_rope = q[..., k_nope:]
# Production w_kc layout: [H, K, N] with strides (K*N, 1, K) (N-major), the
# result of w_kc.transpose(1, 2).contiguous().transpose(1, 2) at load.
w_base = (
torch.randn(
(num_heads, N_LORA, k_nope),
generator=g,
device=device,
dtype=torch.float32,
)
/ (k_nope**0.5)
).to(torch.bfloat16)
w_kc = w_base.transpose(1, 2)
return q, q_nope, q_rope, w_kc
def old_path(q_fp8, q_nope, w_kc, q_rope, num_heads):
q_nope_out = torch.bmm(q_nope.transpose(0, 1), w_kc).transpose(0, 1)
concat_and_cast_q_fp8_pad(q_fp8, q_nope_out, q_rope, num_heads)
def old_path_bmm_only(q_nope, w_kc):
return torch.bmm(q_nope.transpose(0, 1), w_kc)
def new_path(q_fp8, q_nope, w_kc, q_rope, num_heads, **kw):
absorbed_bmm_concat_cast_q_fp8(q_fp8, q_nope, w_kc, q_rope, num_heads, **kw)
# Non-power-of-2-K variants, in bench order (power-of-2 K collapses to "auto").
ALL_VARIANTS = ["loop", "two_dot", "three_dot", "pad", "single_k"]
# (block_m, block_n, num_warps, num_stages) sweep grid; num_stages 0 = Triton
# default. N=512 is a multiple of every block_n here; block_m stays power of 2.
SWEEP_TILES = [
(64, 128, 4, 0), # kernel default
(64, 128, 8, 0),
(64, 128, 4, 2),
(64, 128, 4, 4),
(128, 128, 4, 0),
(128, 128, 8, 0),
(64, 256, 8, 0),
(128, 256, 8, 0),
(32, 128, 4, 0),
(64, 64, 4, 0),
(128, 64, 8, 0),
]
def time_fn(fn, iters: int, warmup: int) -> float:
"""Median wall time of fn() in microseconds per call (lower = faster)."""
for _ in range(warmup):
fn()
torch.cuda.synchronize()
times = []
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
chunk = 10
for _ in range(max(1, iters // chunk)):
start.record()
for _ in range(chunk):
fn()
end.record()
torch.cuda.synchronize()
times.append(start.elapsed_time(end) * 1e3 / chunk) # ms -> us
times.sort()
return times[len(times) // 2]
def analytic_bytes(num_tokens: int, num_heads: int, k_nope: int):
"""(old_bytes, new_bytes) of DRAM traffic per call, analytic lower bound."""
t, h = num_tokens, num_heads
a_read = t * h * k_nope * 2 # q_nope bf16
w_read = h * k_nope * N_LORA * 2 # w_kc bf16
nope_bf16 = t * h * N_LORA * 2 # bmm bf16 out (written then re-read)
rope_read = t * h * R_ROPE * 2 # q_rope bf16
fp8_write = t * h * (N_LORA + R_ROPE) # q_fp8 out
old = (a_read + w_read + nope_bf16) + (nope_bf16 + rope_read + fp8_write)
new = a_read + w_read + rope_read + fp8_write
return old, new
def make_check_ctx(num_tokens, num_heads, k_nope, device, seed, magnitude):
"""Fresh inputs + old-path fp8 output + fp64 bmm reference (once/config)."""
q, q_nope, q_rope, w_kc = make_inputs(
num_tokens, num_heads, k_nope, device, seed, magnitude
)
q_fp8_old = torch.zeros(
(num_tokens, num_heads, N_LORA + R_ROPE),
dtype=torch.float8_e4m3fn,
device=device,
)
old_path(q_fp8_old, q_nope, w_kc, q_rope, num_heads)
torch.cuda.synchronize()
# fp64 reference: which path's fp8 lands closer to the exact bmm?
ref = torch.bmm(
q_nope.transpose(0, 1).to(torch.float64), w_kc.to(torch.float64)
).transpose(0, 1)
err_old = (q_fp8_old[..., :N_LORA].to(torch.float64) - ref).abs()
return {
"q_nope": q_nope,
"q_rope": q_rope,
"w_kc": w_kc,
"num_heads": num_heads,
"q_fp8_old": q_fp8_old,
"ref": ref,
"meanerr_old": err_old.mean().item(),
"maxerr_old": err_old.max().item(),
}
def check_variant(ctx, **new_kwargs):
"""Correctness of one new-path variant vs the old path + fp64 reference."""
q_fp8_old = ctx["q_fp8_old"]
q_fp8_new = torch.zeros_like(q_fp8_old)
new_path(
q_fp8_new,
ctx["q_nope"],
ctx["w_kc"],
ctx["q_rope"],
ctx["num_heads"],
**new_kwargs,
)
torch.cuda.synchronize()
rope_old = q_fp8_old[..., N_LORA:].view(torch.uint8)
rope_new = q_fp8_new[..., N_LORA:].view(torch.uint8)
rope_bitexact = bool(torch.equal(rope_old, rope_new))
nope_old = q_fp8_old[..., :N_LORA]
nope_new = q_fp8_new[..., :N_LORA]
match = (
(nope_old.view(torch.uint8) == nope_new.view(torch.uint8)).float().mean().item()
)
diff = (nope_old.to(torch.float32) - nope_new.to(torch.float32)).abs()
max_diff = diff.max().item()
err_new = (nope_new.to(torch.float64) - ctx["ref"]).abs()
return {
"rope_bitexact": rope_bitexact,
"nope_bitwise_match_frac": match,
"nope_max_dequant_absdiff": max_diff,
"nope_meanerr_old_vs_fp64": ctx["meanerr_old"],
"nope_meanerr_new_vs_fp64": err_new.mean().item(),
"nope_maxerr_old_vs_fp64": ctx["maxerr_old"],
"nope_maxerr_new_vs_fp64": err_new.max().item(),
}
def rounding_study(device, seed):
"""sweep summary: fp8(bf16(x)) double round vs fp8(x) single round.
(Informational only — the born-fp8 kernel deliberately keeps the
fp32->bf16->fp8 double round to match the default path's rounding stages.)
"""
g = torch.Generator(device=device).manual_seed(seed)
x = torch.randn((1 << 22,), generator=g, device=device, dtype=torch.float32) * 8.0
double = x.to(torch.bfloat16).to(torch.float8_e4m3fn)
single = x.to(torch.float8_e4m3fn)
mismatch = (
(double.view(torch.uint8) != single.view(torch.uint8)).float().mean().item()
)
err_double = (double.to(torch.float32) - x).abs()
err_single = (single.to(torch.float32) - x).abs()
print("\n=== rounding study: fp32->bf16->fp8 (double) vs fp32->fp8 (single) ===")
print(f"elements : {x.numel()}")
print(f"byte-mismatch fraction : {mismatch:.3e} (fraction, lower = closer)")
print(
f"mean |err| vs fp32 (double) : {err_double.mean().item():.6e} (lower = more accurate)"
)
print(
f"mean |err| vs fp32 (single) : {err_single.mean().item():.6e} (lower = more accurate)"
)
winner = (
"single (direct fp32->fp8)"
if err_single.mean() <= err_double.mean()
else "double (via bf16)"
)
print(f"more accurate on average : {winner}")
print(
"NOTE: the born-fp8 kernel keeps the DOUBLE round on purpose to match "
"the default path's rounding stages."
)
def run_config(
name,
num_tokens,
num_heads,
k_nope,
iters,
warmup,
device,
seed,
magnitude,
variants,
sweep,
):
print(f"\n=== {name}: tokens={num_tokens} heads={num_heads} K={k_nope} ===")
print(
f" (K={k_nope} nope-in, N={N_LORA} nope-out, R={R_ROPE} rope; "
"us/call LOWER = FASTER; GB/s HIGHER = BETTER; speedup >1 = new faster)"
)
q, q_nope, q_rope, w_kc = make_inputs(
num_tokens, num_heads, k_nope, device, seed, magnitude
)
q_fp8 = torch.zeros(
(num_tokens, num_heads, N_LORA + R_ROPE),
dtype=torch.float8_e4m3fn,
device=device,
)
t_old = time_fn(
lambda: old_path(q_fp8, q_nope, w_kc, q_rope, num_heads), iters, warmup
)
t_bmm = time_fn(lambda: old_path_bmm_only(q_nope, w_kc), iters, warmup)
# standalone concat-cast (reads a fresh bf16 bmm out, like production)
q_nope_out = torch.bmm(q_nope.transpose(0, 1), w_kc).transpose(0, 1)
t_cast = time_fn(
lambda: concat_and_cast_q_fp8_pad(q_fp8, q_nope_out, q_rope, num_heads),
iters,
warmup,
)
b_old, b_new = analytic_bytes(num_tokens, num_heads, k_nope)
print(
f"OLD bmm (cublas bf16) : {t_bmm:10.1f} us/call"
f" (component of OLD total)"
)
print(f"OLD concat_and_cast_q_fp8_pad: {t_cast:10.1f} us/call (component)")
print(
f"OLD total (bmm + concat-cast): {t_old:10.1f} us/call"
f" ({b_old / 1e6:8.1f} MB analytic, {b_old / t_old / 1e3:7.0f} GB/s)"
)
# Power-of-2 K collapses every variant to the same single-dot codegen.
pow2 = k_nope & (k_nope - 1) == 0
run_variants = ["auto"] if pow2 else variants
ctx = make_check_ctx(num_tokens, num_heads, k_nope, device, seed + 1, magnitude)
results = {}
for v in run_variants:
kw = {"variant": v}
try:
t_new = time_fn(
lambda: new_path(q_fp8, q_nope, w_kc, q_rope, num_heads, **kw),
iters,
warmup,
)
except Exception as e:
msg = (str(e).splitlines() or [type(e).__name__])[0]
print(f"NEW {v:<24}: COMPILE/RUN FAIL — {msg[:100]}")
continue
c = check_variant(ctx, **kw)
results[v] = t_new
faster = "NEW FASTER" if t_new < t_old else "OLD FASTER"
print(
f"NEW {v:<24}: {t_new:10.1f} us/call"
f" ({b_new / 1e6:8.1f} MB analytic, {b_new / t_new / 1e3:7.0f} GB/s,"
f" speedup {t_old / t_new:5.2f}x vs OLD, {faster})"
)
rope = "PASS (bitwise identical)" if c["rope_bitexact"] else "FAIL (BUG)"
print(
f" rope bit-exact: {rope}; nope bitwise match vs OLD"
f" {c['nope_bitwise_match_frac'] * 100:9.4f}% (100% = bit-exact);"
f" max |dequant diff| {c['nope_max_dequant_absdiff']:.4f}"
)
print(
f" nope |err| vs fp64 ref: mean old {c['nope_meanerr_old_vs_fp64']:.3e}"
f" / new {c['nope_meanerr_new_vs_fp64']:.3e}; max old"
f" {c['nope_maxerr_old_vs_fp64']:.3e} / new"
f" {c['nope_maxerr_new_vs_fp64']:.3e} (lower = more accurate)"
)
if results:
best = min(results, key=results.get)
print(
f"BEST variant : {best} @ {results[best]:.1f} us/call"
f" (speedup {t_old / results[best]:.2f}x vs OLD total)"
)
if sweep and results:
print(
f"\n--- tile sweep: {name} (us/call LOWER = FASTER;"
" stages=0 -> Triton default) ---"
)
rows = []
for v in results:
for bm, bn, nw, ns in SWEEP_TILES:
kw = dict(
variant=v, block_m=bm, block_n=bn, num_warps=nw, num_stages=ns
)
try:
t = time_fn(
lambda: new_path(q_fp8, q_nope, w_kc, q_rope, num_heads, **kw),
max(iters // 2, 20),
warmup,
)
except Exception as e:
msg = (str(e).splitlines() or [type(e).__name__])[0]
print(
f" {v:<10} bm={bm:<3} bn={bn:<3} warps={nw} stages={ns}:"
f" FAIL — {msg[:70]}"
)
continue
rows.append((t, v, bm, bn, nw, ns))
print(
f" {v:<10} bm={bm:<3} bn={bn:<3} warps={nw} stages={ns}:"
f" {t:8.1f} us/call ({b_new / t / 1e3:5.0f} GB/s,"
f" {t_old / t:5.2f}x vs OLD)"
)
rows.sort()
print(" -- top 5 (fastest first) --")
for t, v, bm, bn, nw, ns in rows[:5]:
print(
f" {v:<10} bm={bm:<3} bn={bn:<3} warps={nw} stages={ns}:"
f" {t:8.1f} us/call ({t_old / t:5.2f}x vs OLD)"
)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--tokens", type=int, default=4096, help="s_q per call")
parser.add_argument("--iters", type=int, default=200)
parser.add_argument("--warmup", type=int, default=20)
parser.add_argument("--seed", type=int, default=1234)
parser.add_argument(
"--magnitude",
type=float,
default=1.0,
help="input scale multiplier (q amax stress)",
)
parser.add_argument(
"--heads",
type=int,
default=None,
help="run a single head count instead of the GLM(64,K192)+DS(128,K128) pair",
)
parser.add_argument(
"--k-nope",
type=int,
default=128,
help="qk_nope_head_dim for --heads runs (ignored for the default pair)",
)
parser.add_argument(
"--variants",
type=str,
default="all",
help=(
"comma list of non-power-of-2-K variants to bench "
f"(default: all = {','.join(ALL_VARIANTS)}); power-of-2-K configs "
"always run the single collapsed 'auto' variant"
),
)
parser.add_argument(
"--sweep",
action="store_true",
help="also sweep (block_m, block_n, num_warps, num_stages) per variant",
)
parser.add_argument("--rounding-study", action="store_true")
args = parser.parse_args()
if args.variants == "all":
variants = ALL_VARIANTS
else:
variants = [v.strip() for v in args.variants.split(",") if v.strip()]
unknown = set(variants) - set(ALL_VARIANTS) - {"auto"}
assert not unknown, f"unknown variants: {sorted(unknown)}"
assert torch.cuda.is_available(), "CUDA GPU required"
device = torch.device("cuda")
name = torch.cuda.get_device_name(device)
print(f"device: {name}; torch {torch.__version__}")
if args.heads is not None:
configs = [(f"custom h={args.heads} K={args.k_nope}", args.heads, args.k_nope)]
else:
configs = [
("GLM-5.2 (h=64, K=192)", 64, 192),
("DS-V3.2 (h=128, K=128)", 128, 128),
]
for cfg_name, heads, k_nope in configs:
run_config(
cfg_name,
args.tokens,
heads,
k_nope,
args.iters,
args.warmup,
device,
args.seed,
args.magnitude,
variants,
args.sweep,
)
if args.rounding_study:
rounding_study(device, args.seed)
if __name__ == "__main__":
main()