#!/usr/bin/env python3 """ Kernel-level Lean vs Standard (SplitK) decode-attention sweep, both GQA head configs, full batch x context grid, at the shipped 1xCU persistent grid. Generalizes benchmark/lean_kernel_qwen_gqa.py (which is Qwen batch=1 only) to sweep Qwen2.5-7B (28Q/4KV) and Llama-3.1-8B (32Q/8KV) over batch in {1,2,4,8,16,32} x context in {8K,16K,32K,64K,128K}, reporting per-call kernel latency, speedup (std / lean), cosine parity vs SplitK, and whether the eager auto-gate would enable Lean. Writes a CSV for the PR tables. Grid is whatever SGLANG_FORCE_LEAN_GRID_CU_MULT resolves to (default 1.0 = one CTA per CU); set it to A/B other grids without a rebuild. Usage: python3 benchmark/lean_kernel_sweep.py [out.csv] """ import os import sys import time sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "python")) import torch from sglang.kernels.ops.attention.decode_attention import ( _LEAN_BLOCK_M, _lean_decode_launch_params, decode_attention_fwd, decode_attention_fwd_grouped, lean_decode_seqlen_gate, ) MODELS = [ ("qwen2.5-7b", 28, 4), ("llama3.1-8b", 32, 8), ] D = D_V = 128 MAX_KV_SPLITS = 8 BATCHES = [1, 2, 4, 8, 16, 32] CONTEXTS = [8192, 16384, 32768, 65536, 131072] def bench(fn, warmup=15, iters=100): 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 * 1000.0 # ms def run(H_Q, H_KV, B, S): dev, dt = "cuda", torch.float16 kvg = H_Q // H_KV sm = 1.0 / (D**0.5) tot = B * S total_programs, _, _ = _lean_decode_launch_params(H_KV, kvg) lean_Mp = torch.empty( (total_programs, _LEAN_BLOCK_M), dtype=torch.float32, device=dev ) lean_Lp = torch.empty( (total_programs, _LEAN_BLOCK_M), dtype=torch.float32, device=dev ) lean_Op = torch.empty( (total_programs, _LEAN_BLOCK_M, D_V), dtype=torch.float32, device=dev ) lean_locks = torch.zeros((total_programs,), dtype=torch.int32, device=dev) kvi = torch.arange(0, (B + 1) * S, step=S, device=dev, dtype=torch.int32) kvx = torch.arange(0, tot, device=dev, dtype=torch.int32) q = torch.randn(B, H_Q, D, dtype=dt, device=dev) k = torch.randn(tot, H_KV, D, dtype=dt, device=dev) v = torch.randn(tot, H_KV, D_V, dtype=dt, device=dev) attn_logits = torch.empty( (B, H_Q, MAX_KV_SPLITS, D_V), dtype=torch.float32, device=dev ) attn_lse = torch.empty((B, H_Q, MAX_KV_SPLITS), dtype=torch.float32, device=dev) nks = torch.full((B,), MAX_KV_SPLITS, dtype=torch.int32, device=dev) o_std = torch.zeros(B, H_Q, D_V, dtype=dt, device=dev) std_ms = bench( lambda: decode_attention_fwd_grouped( q, k, v, o_std, kvi, kvx, attn_logits, attn_lse, nks, MAX_KV_SPLITS, sm, 1.0 ) ) attn_logits2 = torch.empty_like(attn_logits) attn_lse2 = torch.empty_like(attn_lse) o_lean = torch.zeros(B, H_Q, D_V, dtype=dt, device=dev) lean_ms = bench( lambda: decode_attention_fwd( q, k, v, o_lean, kvi, kvx, attn_logits2, attn_lse2, nks, MAX_KV_SPLITS, sm, 1.0, 1.0, enable_lean=True, lean_Mp=lean_Mp, lean_Lp=lean_Lp, lean_Op=lean_Op, lean_locks=lean_locks, ) ) cos = torch.nn.functional.cosine_similarity( o_lean.flatten().float(), o_std.flatten().float(), dim=0 ).item() gate = lean_decode_seqlen_gate(H_Q, kvg, B, B * S, is_mla=False) return std_ms, lean_ms, cos, gate def main(): out = sys.argv[1] if len(sys.argv) > 1 else "grid_out/kernel_sweep_1xcu.csv" mult = float(os.environ.get("SGLANG_FORCE_LEAN_GRID_CU_MULT", "1.0")) print(f"\nKernel sweep GPU={torch.cuda.get_device_name(0)} grid_mult={mult}") print("=" * 78) rows = ["model,H_Q,H_KV,batch,context,std_ms,lean_ms,speedup,cos,gate"] for name, H_Q, H_KV in MODELS: print(f"\n{name} ({H_Q}Q/{H_KV}KV)") print( f"{'batch':>5} {'ctx':>6} {'std_ms':>9} {'lean_ms':>9} {'speedup':>8} {'gate':>5} {'cos':>7}" ) for B in BATCHES: for S in CONTEXTS: # b32 x 128K on the 8-KV-head config exceeds the microbench's single # contiguous KV tensor (faults the GPU); real serving uses a paged pool. if H_KV == 8 and B == 32 and S == 131072: print( f"{B:>5} {S // 1024:>5}K {'skipped (contiguous-KV limit)':>30}" ) rows.append(f"{name},{H_Q},{H_KV},{B},{S},,,,skip,") continue try: std, lean, cos, gate = run(H_Q, H_KV, B, S) except torch.cuda.OutOfMemoryError: torch.cuda.empty_cache() print(f"{B:>5} {S // 1024:>5}K {'OOM':>9}") rows.append(f"{name},{H_Q},{H_KV},{B},{S},,,,OOM,") continue sp = std / lean print( f"{B:>5} {S // 1024:>5}K {std:>9.3f} {lean:>9.3f} {sp:>7.2f}x {('ON' if gate else 'OFF'):>5} {cos:>7.4f}" ) rows.append( f"{name},{H_Q},{H_KV},{B},{S},{std:.4f},{lean:.4f},{sp:.4f},{cos:.4f},{int(gate)}" ) os.makedirs(os.path.dirname(out), exist_ok=True) open(out, "w").write("\n".join(rows) + "\n") print(f"\nwrote {out}") if __name__ == "__main__": main()