|
| 1 | +"""Benchmark for the SM80 dense MLA decode kernel. |
| 2 | +
|
| 3 | +Compares against a PyTorch eager (BMM-based) reference. The eager path is |
| 4 | +slow for long sequences -- iteration counts shrink accordingly. Reports |
| 5 | +latency, KV bandwidth, and speedup across a config sweep.""" |
| 6 | + |
| 7 | +import argparse |
| 8 | +import os |
| 9 | +import sys |
| 10 | +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| 11 | + |
| 12 | +import torch |
| 13 | +import flash_mla.cuda as cuda |
| 14 | + |
| 15 | + |
| 16 | +def torch_eager_mla(q, kcache, block_table, seqlens_k, softmax_scale, head_size_v, is_causal=False): |
| 17 | + """PyTorch BMM-based MLA decode. Returns the same shape as the kernel output.""" |
| 18 | + b, sq, hq, dk = q.shape |
| 19 | + _, pbs, hk, _ = kcache.shape |
| 20 | + nq_per_hk = hq // hk |
| 21 | + out = torch.zeros(b, sq, hq, head_size_v, dtype=q.dtype, device=q.device) |
| 22 | + for bi in range(b): |
| 23 | + sk = int(seqlens_k[bi].item()) |
| 24 | + bt = block_table[bi] |
| 25 | + nb = (sk + pbs - 1) // pbs |
| 26 | + ks = [kcache[bt[bl].item()] for bl in range(nb)] |
| 27 | + kc = torch.cat(ks, dim=0)[:sk] # (sk, hk, dk) |
| 28 | + k_full = kc.transpose(0, 1).contiguous() # (hk, sk, dk) |
| 29 | + v_full = k_full[:, :, :head_size_v] # (hk, sk, dv) |
| 30 | + q_b = q[bi] # (sq, hq, dk) |
| 31 | + q_rs = (q_b.view(sq, hk, nq_per_hk, dk) |
| 32 | + .permute(1, 0, 2, 3) |
| 33 | + .reshape(hk, sq * nq_per_hk, dk)) |
| 34 | + scores = torch.bmm(q_rs.float(), k_full.float().transpose(1, 2)) * softmax_scale |
| 35 | + if is_causal: |
| 36 | + for sq_idx in range(sq): |
| 37 | + rb = max(0, sk - (sq - sq_idx - 1)) |
| 38 | + if rb < sk: |
| 39 | + for nq_idx in range(nq_per_hk): |
| 40 | + scores[:, sq_idx * nq_per_hk + nq_idx, rb:] = float('-inf') |
| 41 | + probs = torch.softmax(scores, dim=-1) |
| 42 | + o = torch.bmm(probs, v_full.float()) # (hk, q_per_hk, dv) |
| 43 | + o_rs = (o.view(hk, sq, nq_per_hk, head_size_v) |
| 44 | + .permute(1, 2, 0, 3) |
| 45 | + .reshape(sq, hq, head_size_v)) |
| 46 | + out[bi] = o_rs.to(q.dtype) |
| 47 | + return out |
| 48 | + |
| 49 | + |
| 50 | +def bench(fn, iters, warmup): |
| 51 | + for _ in range(warmup): |
| 52 | + fn() |
| 53 | + torch.cuda.synchronize() |
| 54 | + s = torch.cuda.Event(enable_timing=True) |
| 55 | + e = torch.cuda.Event(enable_timing=True) |
| 56 | + s.record() |
| 57 | + for _ in range(iters): |
| 58 | + fn() |
| 59 | + e.record() |
| 60 | + torch.cuda.synchronize() |
| 61 | + return s.elapsed_time(e) / iters |
| 62 | + |
| 63 | + |
| 64 | +def make_inputs(batch, sq, hq, hk, sk, dtype, device): |
| 65 | + head_size_k = 576 |
| 66 | + page_block_size = 64 |
| 67 | + q = torch.randn(batch, sq, hq, head_size_k, dtype=dtype, device=device) * 0.1 |
| 68 | + nb = (sk + page_block_size - 1) // page_block_size |
| 69 | + kcache = torch.randn(nb * batch, page_block_size, hk, head_size_k, dtype=dtype, device=device) * 0.1 |
| 70 | + seqlens_k = torch.full((batch,), sk, dtype=torch.int32, device=device) |
| 71 | + block_table = torch.arange(nb * batch, dtype=torch.int32, device=device).view(batch, nb) |
| 72 | + return q, kcache, seqlens_k, block_table |
| 73 | + |
| 74 | + |
| 75 | +def main(): |
| 76 | + parser = argparse.ArgumentParser() |
| 77 | + parser.add_argument('--dtype', default='bfloat16', choices=['bfloat16', 'float16']) |
| 78 | + parser.add_argument('--no-torch-baseline', action='store_true', |
| 79 | + help='skip PyTorch eager reference (much faster sweep)') |
| 80 | + parser.add_argument('--check', action='store_true', |
| 81 | + help='also run a one-shot correctness check vs eager') |
| 82 | + args = parser.parse_args() |
| 83 | + |
| 84 | + torch.manual_seed(0) |
| 85 | + device = 'cuda' |
| 86 | + dtype = getattr(torch, args.dtype) |
| 87 | + head_size_k = 576 |
| 88 | + head_size_v = 512 |
| 89 | + softmax_scale = 1.0 / (head_size_k ** 0.5) |
| 90 | + |
| 91 | + configs = [ |
| 92 | + # (batch, sq, hq, hk, sk) |
| 93 | + (1, 1, 16, 1, 256), |
| 94 | + (1, 1, 16, 1, 1024), |
| 95 | + (1, 1, 16, 1, 4096), |
| 96 | + (1, 1, 16, 1, 16384), |
| 97 | + (1, 1, 16, 1, 65536), |
| 98 | + (4, 1, 16, 1, 1024), |
| 99 | + (4, 1, 16, 1, 4096), |
| 100 | + (16, 1, 16, 1, 1024), |
| 101 | + (16, 1, 16, 1, 4096), |
| 102 | + (64, 1, 16, 1, 1024), |
| 103 | + (64, 1, 16, 1, 4096), |
| 104 | + (1, 1, 64, 1, 4096), |
| 105 | + ] |
| 106 | + |
| 107 | + print(f'{"config":<32} {"ours(ms)":>9} {"torch(ms)":>10} {"speedup":>8} {"ours BW(GB/s)":>14}') |
| 108 | + print('-' * 75) |
| 109 | + for batch, sq, hq, hk, sk in configs: |
| 110 | + q, kcache, seqlens_k, block_table = make_inputs(batch, sq, hq, hk, sk, dtype, device) |
| 111 | + |
| 112 | + ours_fn = lambda: cuda.dense_decode_fwd( |
| 113 | + q, kcache, head_size_v, seqlens_k, block_table, softmax_scale, False, None, None |
| 114 | + ) |
| 115 | + ours_ms = bench(ours_fn, iters=200, warmup=20) |
| 116 | + kv_bytes = batch * hk * sk * head_size_k * 2 |
| 117 | + bw = kv_bytes / (ours_ms * 1e-3) / 1e9 |
| 118 | + |
| 119 | + if args.no_torch_baseline: |
| 120 | + torch_str = 'skip' |
| 121 | + speedup_str = '-' |
| 122 | + else: |
| 123 | + iters = 5 if sk * batch >= 8192 else (20 if sk * batch >= 1024 else 50) |
| 124 | + warmup = 2 |
| 125 | + torch_fn = lambda: torch_eager_mla(q, kcache, block_table, seqlens_k, softmax_scale, head_size_v) |
| 126 | + torch_ms = bench(torch_fn, iters=iters, warmup=warmup) |
| 127 | + torch_str = f'{torch_ms:.3f}' |
| 128 | + speedup_str = f'{torch_ms / ours_ms:.1f}x' |
| 129 | + |
| 130 | + if args.check: |
| 131 | + out, _, _, _ = ours_fn() |
| 132 | + ref = torch_eager_mla(q, kcache, block_table, seqlens_k, softmax_scale, head_size_v) |
| 133 | + diff = (out.float() - ref.float()).abs().max().item() |
| 134 | + tag = 'OK' if diff < 0.02 else f'FAIL diff={diff:.4f}' |
| 135 | + speedup_str = f'{speedup_str} ({tag})' |
| 136 | + |
| 137 | + cfg = f'b={batch} sq={sq} hq={hq} hk={hk} sk={sk}' |
| 138 | + print(f'{cfg:<32} {ours_ms:>9.3f} {torch_str:>10} {speedup_str:>8} {bw:>14.1f}') |
| 139 | + |
| 140 | + |
| 141 | +if __name__ == '__main__': |
| 142 | + main() |
0 commit comments