为 FlashInfer 基准脚本增加统一运行参数控制 #57

Open
Mengz wants to merge 1 commits from Mengz/op_optimization:mengz/flashinfer-benchmark-controls into master
5 changed files with 318 additions and 110 deletions

View File

@ -4,11 +4,22 @@ seq_len_q=1 (decode mode), seq_len_kv from 1K to 16K
"""
import itertools
import pandas as pd
import torch
import flashinfer
from bench_common import dtype, page_block_size, setup_workspace, setup_paged_kv_indptr, run_with_profiler, get_csv_path, compute_reps
from bench_common import (
compute_reps,
dtype,
get_device_info,
limited_cases,
page_block_size,
parse_benchmark_args,
record_failed_case,
run_with_profiler,
setup_paged_kv_indptr,
setup_workspace,
write_csv,
)
target_kernels = ["BatchPrefillWithPagedKVCacheKernel"]
@ -20,6 +31,8 @@ def bench_batch_decode(
num_kv_heads,
head_dim,
page_block_size,
warmup,
base_reps,
):
"""Benchmark BatchDecodeWithPagedKVCacheWrapper"""
seq_lens = [seq_len_kv] * batch_size
@ -45,15 +58,20 @@ def bench_batch_decode(
q_data_type=dtype,
)
reps = compute_reps(batch_size, seq_len_kv, head_dim, base_reps=100)
ms = run_with_profiler(lambda: wrapper.run(q, kv_data), target_kernels=target_kernels, reps=reps)
reps = compute_reps(batch_size, seq_len_kv, head_dim, base_reps=base_reps)
ms = run_with_profiler(
lambda: wrapper.run(q, kv_data),
target_kernels=target_kernels,
warmup=warmup,
reps=reps,
)
io = q.numel() * q.element_size() + kv_data.numel() * kv_data.element_size()
flops = 2 * batch_size * seq_len_kv * num_qo_heads * num_kv_heads * head_dim
return ms, io, flops
return ms, io, flops, reps
def run_benchmark():
def run_benchmark(args):
records = []
batch_sizes = [1, 2, 4, 8, 16, 32, 64, 128]
@ -61,7 +79,9 @@ def run_benchmark():
seq_lens_kv = [512, 1024, 2048, 4096, 8192, 16384]
api_name = "BatchDecodeWithPagedKVCacheWrapper"
test_cases = list(itertools.product(batch_sizes, seq_lens_kv, head_dims))
test_cases = limited_cases(
list(itertools.product(batch_sizes, seq_lens_kv, head_dims)), args.max_cases
)
total_cases = len(test_cases)
print(f"[{api_name}] Starting benchmark, total cases: {total_cases}")
@ -69,22 +89,45 @@ def run_benchmark():
for idx, (bs, sl_kv, hd) in enumerate(test_cases, 1):
num_qo_heads = 32
num_kv_heads = 8 if hd == 64 else 4
ms, io, flops = bench_batch_decode(bs, sl_kv, num_qo_heads, num_kv_heads, hd, page_block_size)
bw = io / ms / 1e6
tflops = flops / ms / 1e9
records.append({
"api": api_name,
case = {
"batch_size": bs,
"seq_len_q": 1,
"seq_len_kv": sl_kv,
"num_qo_heads": num_qo_heads,
"num_kv_heads": num_kv_heads,
"head_dim": hd,
"time_ms": ms,
"bandwidth_GB_s": bw,
"tflops": tflops,
})
print(f" [{idx}/{total_cases}] bs={bs}, kv_len={sl_kv}, hd={hd}: {ms:.3f}ms, {bw:.2f} GB/s, {tflops:.2f} TFLOPs")
}
try:
ms, io, flops, reps = bench_batch_decode(
bs,
sl_kv,
num_qo_heads,
num_kv_heads,
hd,
page_block_size,
args.warmup,
args.base_reps,
)
bw = io / ms / 1e6
tflops = flops / ms / 1e9
record = {
"api": api_name,
"status": "ok",
"time_ms": ms,
"bandwidth_GB_s": bw,
"tflops": tflops,
"warmup": args.warmup,
"reps": reps,
}
record.update(case)
record.update(get_device_info())
records.append(record)
print(f" [{idx}/{total_cases}] bs={bs}, kv_len={sl_kv}, hd={hd}: {ms:.3f}ms, {bw:.2f} GB/s, {tflops:.2f} TFLOPs")
except Exception as exc:
if not args.continue_on_error:
raise
records.append(record_failed_case(api_name, exc, **case))
print(f" [{idx}/{total_cases}] bs={bs}, kv_len={sl_kv}, hd={hd}: failed: {exc}")
return records
@ -94,8 +137,7 @@ if __name__ == "__main__":
np.random.seed(42)
torch.random.manual_seed(42)
records = run_benchmark()
df = pd.DataFrame(records)
csv_path = get_csv_path("BatchDecodeWithPagedKVCacheWrapper")
df.to_csv(csv_path, index=False)
print(f"\nResults saved to {csv_path}")
args = parse_benchmark_args(__doc__)
records = run_benchmark(args)
csv_path = write_csv("BatchDecodeWithPagedKVCacheWrapper", records, args.output_dir)
print(f"\nResults saved to {csv_path}")

View File

@ -4,11 +4,20 @@ headdim: ckv=512, kpe=64 (DeepSeek MLA configuration)
"""
import itertools
import pandas as pd
import torch
import flashinfer
from bench_common import dtype, page_block_size, setup_workspace, run_with_profiler, get_csv_path, compute_reps
from bench_common import (
compute_reps,
dtype,
get_device_info,
limited_cases,
parse_benchmark_args,
record_failed_case,
run_with_profiler,
setup_workspace,
write_csv,
)
target_kernels = ["BatchMLAPagedAttentionKernel"]
@ -19,6 +28,8 @@ def bench_batch_mla_paged_attention(
num_heads,
head_dim_ckv,
head_dim_kpe,
warmup,
base_reps,
):
"""Benchmark BatchMLAPagedAttentionWrapper for DeepSeek MLA"""
# MLA decode mode: q has length 1, not seq_len
@ -54,16 +65,21 @@ def bench_batch_mla_paged_attention(
ckv.dtype,
)
reps = compute_reps(batch_size, seq_len, head_dim_ckv + head_dim_kpe, base_reps=100)
ms = run_with_profiler(lambda: wrapper.run(q_nope, q_pe, ckv, kpe, return_lse=False), target_kernels=target_kernels, reps=reps)
reps = compute_reps(batch_size, seq_len, head_dim_ckv + head_dim_kpe, base_reps=base_reps)
ms = run_with_profiler(
lambda: wrapper.run(q_nope, q_pe, ckv, kpe, return_lse=False),
target_kernels=target_kernels,
warmup=warmup,
reps=reps,
)
io = sum([t.numel() * t.element_size() for t in [q_nope, q_pe, ckv, kpe]])
# MLA FLOPs: 2 * batch_size * num_heads * (2 * head_dim_ckv + head_dim_kpe) * seq_len
flops = 2 * batch_size * num_heads * (2 * head_dim_ckv + head_dim_kpe) * seq_len
return ms, io, flops
return ms, io, flops, reps
def run_benchmark():
def run_benchmark(args):
records = []
# MLA configuration - same as DeepSeek
@ -74,26 +90,50 @@ def run_benchmark():
num_heads_list = [64, 128]
api_name = "BatchMLAPagedAttentionWrapper"
test_cases = list(itertools.product(num_heads_list, batch_sizes, seq_lens))
test_cases = limited_cases(
list(itertools.product(num_heads_list, batch_sizes, seq_lens)), args.max_cases
)
total_cases = len(test_cases)
print(f"[{api_name}] Starting benchmark, total cases: {total_cases}")
for idx, (num_heads, bs, sl) in enumerate(test_cases, 1):
ms, io, flops = bench_batch_mla_paged_attention(bs, sl, num_heads, head_dim_ckv, head_dim_kpe)
bw = io / ms / 1e6
tflops = flops / ms / 1e9
records.append({
"api": api_name,
case = {
"batch_size": bs,
"seq_len": sl,
"num_heads": num_heads,
"head_dim_ckv": head_dim_ckv,
"head_dim_kpe": head_dim_kpe,
"time_ms": ms,
"bandwidth_GB_s": bw,
"tflops": tflops,
})
print(f" [{idx}/{total_cases}] bs={bs}, sl={sl}, num_heads={num_heads}: {ms:.3f}ms, {bw:.2f} GB/s, {tflops:.2f} TFLOPs")
}
try:
ms, io, flops, reps = bench_batch_mla_paged_attention(
bs,
sl,
num_heads,
head_dim_ckv,
head_dim_kpe,
args.warmup,
args.base_reps,
)
bw = io / ms / 1e6
tflops = flops / ms / 1e9
record = {
"api": api_name,
"status": "ok",
"time_ms": ms,
"bandwidth_GB_s": bw,
"tflops": tflops,
"warmup": args.warmup,
"reps": reps,
}
record.update(case)
record.update(get_device_info())
records.append(record)
print(f" [{idx}/{total_cases}] bs={bs}, sl={sl}, num_heads={num_heads}: {ms:.3f}ms, {bw:.2f} GB/s, {tflops:.2f} TFLOPs")
except Exception as exc:
if not args.continue_on_error:
raise
records.append(record_failed_case(api_name, exc, **case))
print(f" [{idx}/{total_cases}] bs={bs}, sl={sl}, num_heads={num_heads}: failed: {exc}")
return records
@ -103,8 +143,7 @@ if __name__ == "__main__":
np.random.seed(42)
torch.random.manual_seed(42)
records = run_benchmark()
df = pd.DataFrame(records)
csv_path = get_csv_path("BatchMLAPagedAttentionWrapper")
df.to_csv(csv_path, index=False)
print(f"\nResults saved to {csv_path}")
args = parse_benchmark_args(__doc__)
records = run_benchmark(args)
csv_path = write_csv("BatchMLAPagedAttentionWrapper", records, args.output_dir)
print(f"\nResults saved to {csv_path}")

View File

@ -4,17 +4,20 @@ headdim: 64/128/256
"""
import itertools
import pandas as pd
import torch
import flashinfer
from bench_common import (
dtype,
setup_workspace,
setup_paged_kv_indptr,
run_with_profiler,
get_csv_path,
compute_reps,
dtype,
get_device_info,
limited_cases,
parse_benchmark_args,
record_failed_case,
run_with_profiler,
setup_paged_kv_indptr,
setup_workspace,
write_csv,
)
target_kernels = ["BatchPrefillWithPagedKVCacheKernel"]
@ -26,6 +29,8 @@ def bench_batch_prefill_with_paged_kv_cache(
num_qo_heads,
num_kv_heads,
head_dim,
warmup,
base_reps,
causal=True,
):
"""Benchmark BatchPrefillWithPagedKVCacheWrapper"""
@ -59,9 +64,12 @@ def bench_batch_prefill_with_paged_kv_cache(
kv_data_type=dtype,
)
reps = compute_reps(batch_size, seq_len, head_dim, base_reps=100)
reps = compute_reps(batch_size, seq_len, head_dim, base_reps=base_reps)
ms = run_with_profiler(
lambda: wrapper.run(q, kv_data), target_kernels=target_kernels, reps=reps
lambda: wrapper.run(q, kv_data),
target_kernels=target_kernels,
warmup=warmup,
reps=reps,
)
io = q.numel() * q.element_size() + kv_data.numel() * kv_data.element_size()
@ -77,10 +85,10 @@ def bench_batch_prefill_with_paged_kv_cache(
* head_dim
* (1 if causal else 2)
)
return ms, io, flops
return ms, io, flops, reps
def run_benchmark():
def run_benchmark(args):
records = []
batch_sizes = [1, 4, 16, 64]
@ -88,34 +96,48 @@ def run_benchmark():
head_dims = [128, 256]
api_name = "BatchPrefillWithPagedKVCacheWrapper"
test_cases = list(itertools.product(head_dims, batch_sizes, seq_lens))
test_cases = limited_cases(
list(itertools.product(head_dims, batch_sizes, seq_lens)), args.max_cases
)
total_cases = len(test_cases)
print(f"[{api_name}] Starting benchmark, total cases: {total_cases}")
for idx, (head_dim, bs, sl) in enumerate(test_cases, 1):
num_qo_heads = 32
num_kv_heads = 8 if head_dim == 64 else 4
ms, io, flops = bench_batch_prefill_with_paged_kv_cache(
bs, sl, num_qo_heads, num_kv_heads, head_dim
)
bw = io / ms / 1e6
tflops = flops / ms / 1e9
records.append(
{
case = {
"batch_size": bs,
"seq_len": sl,
"num_qo_heads": num_qo_heads,
"num_kv_heads": num_kv_heads,
"head_dim": head_dim,
}
try:
ms, io, flops, reps = bench_batch_prefill_with_paged_kv_cache(
bs, sl, num_qo_heads, num_kv_heads, head_dim, args.warmup, args.base_reps
)
bw = io / ms / 1e6
tflops = flops / ms / 1e9
record = {
"api": api_name,
"batch_size": bs,
"seq_len": sl,
"num_qo_heads": num_qo_heads,
"num_kv_heads": num_kv_heads,
"head_dim": head_dim,
"status": "ok",
"time_ms": ms,
"bandwidth_GB_s": bw,
"tflops": tflops,
"warmup": args.warmup,
"reps": reps,
}
)
print(
f" [{idx}/{total_cases}] bs={bs}, sl={sl}, hd={head_dim}: {ms:.3f}ms, {bw:.2f} GB/s, {tflops:.2f} TFLOPs"
)
record.update(case)
record.update(get_device_info())
records.append(record)
print(
f" [{idx}/{total_cases}] bs={bs}, sl={sl}, hd={head_dim}: {ms:.3f}ms, {bw:.2f} GB/s, {tflops:.2f} TFLOPs"
)
except Exception as exc:
if not args.continue_on_error:
raise
records.append(record_failed_case(api_name, exc, **case))
print(f" [{idx}/{total_cases}] bs={bs}, sl={sl}, hd={head_dim}: failed: {exc}")
return records
@ -126,8 +148,7 @@ if __name__ == "__main__":
np.random.seed(42)
torch.random.manual_seed(42)
records = run_benchmark()
df = pd.DataFrame(records)
csv_path = get_csv_path("BatchPrefillWithPagedKVCacheWrapper")
df.to_csv(csv_path, index=False)
args = parse_benchmark_args(__doc__)
records = run_benchmark(args)
csv_path = write_csv("BatchPrefillWithPagedKVCacheWrapper", records, args.output_dir)
print(f"\nResults saved to {csv_path}")

View File

@ -4,16 +4,19 @@ headdim configurations: [64,64], [128,128], [192,128], [256,256]
"""
import itertools
import pandas as pd
import torch
import flashinfer
from bench_common import (
dtype,
setup_workspace,
run_with_profiler,
get_csv_path,
compute_reps,
dtype,
get_device_info,
limited_cases,
parse_benchmark_args,
record_failed_case,
run_with_profiler,
setup_workspace,
write_csv,
)
target_kernels = [
@ -29,6 +32,8 @@ def bench_batch_prefill_with_ragged_kv_cache(
num_kv_heads,
head_dim_qk,
head_dim_vo,
warmup,
base_reps,
causal=True,
):
"""Benchmark BatchPrefillWithRaggedKVCacheWrapper for MLA"""
@ -58,9 +63,12 @@ def bench_batch_prefill_with_ragged_kv_cache(
kv_data_type=dtype,
)
reps = compute_reps(batch_size, seq_len, head_dim_qk + head_dim_vo, base_reps=100)
reps = compute_reps(batch_size, seq_len, head_dim_qk + head_dim_vo, base_reps=base_reps)
ms = run_with_profiler(
lambda: wrapper.run(q, k, v), target_kernels=target_kernels, reps=reps
lambda: wrapper.run(q, k, v),
target_kernels=target_kernels,
warmup=warmup,
reps=reps,
)
io = (
@ -78,10 +86,10 @@ def bench_batch_prefill_with_ragged_kv_cache(
* (1 if causal else 2)
)
return ms, io, flops
return ms, io, flops, reps
def run_benchmark():
def run_benchmark(args):
records = []
# headdim combinations: [qk, vo]
@ -90,35 +98,58 @@ def run_benchmark():
seq_lens = [1024, 4096, 8192, 16384]
api_name = "BatchPrefillWithRaggedKVCacheWrapper"
test_cases = list(itertools.product(head_dim_configs, batch_sizes, seq_lens))
test_cases = limited_cases(
list(itertools.product(head_dim_configs, batch_sizes, seq_lens)), args.max_cases
)
total_cases = len(test_cases)
print(f"[{api_name}] Starting benchmark, total cases: {total_cases}")
for idx, ((head_dim_qk, head_dim_vo), bs, sl) in enumerate(test_cases, 1):
num_qo_heads = 32
num_kv_heads = 4
ms, io, flops = bench_batch_prefill_with_ragged_kv_cache(
bs, sl, num_qo_heads, num_kv_heads, head_dim_qk, head_dim_vo
)
bw = io / ms / 1e6
tflops = flops / ms / 1e9
records.append(
{
case = {
"batch_size": bs,
"seq_len": sl,
"num_qo_heads": num_qo_heads,
"num_kv_heads": num_kv_heads,
"head_dim_qk": head_dim_qk,
"head_dim_vo": head_dim_vo,
}
try:
ms, io, flops, reps = bench_batch_prefill_with_ragged_kv_cache(
bs,
sl,
num_qo_heads,
num_kv_heads,
head_dim_qk,
head_dim_vo,
args.warmup,
args.base_reps,
)
bw = io / ms / 1e6
tflops = flops / ms / 1e9
record = {
"api": api_name,
"batch_size": bs,
"seq_len": sl,
"num_qo_heads": num_qo_heads,
"num_kv_heads": num_kv_heads,
"head_dim_qk": head_dim_qk,
"head_dim_vo": head_dim_vo,
"status": "ok",
"time_ms": ms,
"bandwidth_GB_s": bw,
"tflops": tflops,
"warmup": args.warmup,
"reps": reps,
}
)
print(
f" [{idx}/{total_cases}] bs={bs}, sl={sl}, hd=[{head_dim_qk},{head_dim_vo}]: {ms:.3f}ms, {bw:.2f} GB/s, {tflops:.2f} TFLOPs"
)
record.update(case)
record.update(get_device_info())
records.append(record)
print(
f" [{idx}/{total_cases}] bs={bs}, sl={sl}, hd=[{head_dim_qk},{head_dim_vo}]: {ms:.3f}ms, {bw:.2f} GB/s, {tflops:.2f} TFLOPs"
)
except Exception as exc:
if not args.continue_on_error:
raise
records.append(record_failed_case(api_name, exc, **case))
print(
f" [{idx}/{total_cases}] bs={bs}, sl={sl}, hd=[{head_dim_qk},{head_dim_vo}]: failed: {exc}"
)
return records
@ -129,8 +160,7 @@ if __name__ == "__main__":
np.random.seed(42)
torch.random.manual_seed(42)
records = run_benchmark()
df = pd.DataFrame(records)
csv_path = get_csv_path("BatchPrefillWithRaggedKVCacheWrapper")
df.to_csv(csv_path, index=False)
args = parse_benchmark_args(__doc__)
records = run_benchmark(args)
csv_path = write_csv("BatchPrefillWithRaggedKVCacheWrapper", records, args.output_dir)
print(f"\nResults saved to {csv_path}")

View File

@ -1,10 +1,11 @@
"""
Common utilities for FlashInfer benchmarks
"""
"""Common utilities for FlashInfer benchmarks."""
import argparse
import csv
import os
import random
from datetime import datetime
from pathlib import Path
import numpy as np
import torch
@ -21,6 +22,81 @@ def get_csv_path(prefix):
return f"{prefix}_{get_timestamp()}.csv"
def parse_benchmark_args(description):
parser = argparse.ArgumentParser(description=description)
parser.add_argument(
"--output-dir",
default=".",
help="Directory used for CSV benchmark output.",
)
parser.add_argument(
"--max-cases",
type=int,
default=0,
help="Run at most this many cases. 0 means run all cases.",
)
parser.add_argument(
"--warmup",
type=int,
default=10,
help="Profiler warmup iterations for each case.",
)
parser.add_argument(
"--base-reps",
type=int,
default=100,
help="Base profiler repetitions before workload-based scaling.",
)
parser.add_argument(
"--continue-on-error",
action="store_true",
help="Record failed cases and continue instead of aborting.",
)
return parser.parse_args()
def limited_cases(test_cases, max_cases):
if max_cases and max_cases > 0:
return test_cases[:max_cases]
return test_cases
def write_csv(prefix, records, output_dir):
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
csv_path = output_path / get_csv_path(prefix)
fieldnames = sorted({key for record in records for key in record.keys()})
with csv_path.open("w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(records)
return csv_path
def get_device_info():
if not torch.cuda.is_available():
return {"device_name": "unavailable", "device_memory_gib": 0.0}
device = torch.cuda.current_device()
props = torch.cuda.get_device_properties(device)
return {
"device_name": props.name,
"device_memory_gib": round(props.total_memory / 1024**3, 2),
}
def record_failed_case(api_name, error, **case):
row = {
"api": api_name,
"status": "failed",
"error_type": type(error).__name__,
"error": str(error),
}
row.update(case)
row.update(get_device_info())
return row
def generate_random_seqlens(batch_size, min_len=1024, max_len=16384):
"""Generate random sequence lengths simulating real LLM workloads"""
return [random.randint(min_len, max_len) for _ in range(batch_size)]
@ -88,4 +164,4 @@ def compute_reps(batch_size, seq_len, head_dim, base_reps=100):
elif workload < 1e9: # very large workload
return base_reps // 16
else: # huge workload
return base_reps // 32
return max(1, base_reps // 32)