| |
| """Benchmark generic FP8 linear+bias projection paths.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import importlib |
| import importlib.util |
| import json |
| import statistics |
| import sys |
| from pathlib import Path |
|
|
| import torch |
|
|
|
|
| ROOT = Path(__file__).resolve().parents[2] |
| TEST_FILE = ROOT / "flashrt-fp8-ffn" / "tests" / "test_fp8_ffn.py" |
|
|
| SHAPES = { |
| "decode_m1_d2048_o": (1, 2048, 2048), |
| "small_m8_d1536_o": (8, 1536, 1536), |
| "groot_dit_m51_qkv": (51, 1536, 4608), |
| "groot_dit_m51_o": (51, 1536, 1536), |
| "mid_m64_d2048_o": (64, 2048, 2048), |
| "groot_backbone_m105_qkv": (105, 2048, 4096), |
| "groot_backbone_m105_o": (105, 2048, 2048), |
| "mid_m128_siglip_qkv": (128, 1152, 3456), |
| "prefill_m256_d1536_o": (256, 1536, 1536), |
| "prefill_m512_d2048_o": (512, 2048, 2048), |
| } |
|
|
|
|
| def load_test_helpers(): |
| spec = importlib.util.spec_from_file_location("flashrt_fp8_ffn_test_helpers", TEST_FILE) |
| if spec is None or spec.loader is None: |
| raise RuntimeError(f"cannot load {TEST_FILE}") |
| module = importlib.util.module_from_spec(spec) |
| spec.loader.exec_module(module) |
| return module |
|
|
|
|
| def load_ops(backend: str, artifact: str | None): |
| helpers = load_test_helpers() |
| if backend == "source": |
| return helpers.load_source_ops(), helpers |
| if artifact: |
| sys.path.insert(0, artifact) |
| try: |
| return importlib.import_module("flashrt_fp8_ffn"), helpers |
| finally: |
| if artifact: |
| sys.path.remove(artifact) |
|
|
|
|
| def cuda_time_us(fn, warmup: int, iters: int, rounds: int) -> float: |
| samples = [] |
| for _ in range(rounds): |
| 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() |
| end.synchronize() |
| samples.append(start.elapsed_time(end) * 1000.0 / iters) |
| return statistics.median(samples) |
|
|
|
|
| def graph_runner(fn): |
| fn() |
| torch.cuda.synchronize() |
| graph = torch.cuda.CUDAGraph() |
| with torch.cuda.graph(graph): |
| fn() |
| graph.replay() |
| torch.cuda.synchronize() |
| return graph.replay |
|
|
|
|
| def maybe_fvk_runner(x_fp8, w_fp8, bias, out, x_scale, w_scale, M, N, K): |
| try: |
| from flash_rt import flash_rt_kernels as fvk |
| except ImportError: |
| return None, "flash_rt.flash_rt_kernels is unavailable" |
| gemm = fvk.GemmRunner() |
| alpha = float((x_scale * w_scale).item()) |
| stream = torch.cuda.current_stream().cuda_stream |
|
|
| def run(): |
| gemm.fp8_nn_bias_bf16( |
| x_fp8.data_ptr(), w_fp8.data_ptr(), out.data_ptr(), bias.data_ptr(), |
| M, N, K, alpha, stream, |
| ) |
|
|
| try: |
| run() |
| except RuntimeError as error: |
| return None, str(error) |
| return run, None |
|
|
|
|
| def maybe_fvk_fp16_runners( |
| x_fp8, w_fp8, bias_fp16, out_fp16, x_scale, w_scale, M, N, K |
| ): |
| try: |
| from flash_rt import flash_rt_kernels as fvk |
| except ImportError: |
| return None, None, "flash_rt.flash_rt_kernels is unavailable" |
| gemm = fvk.GemmRunner() |
| alpha = float((x_scale * w_scale).item()) |
| stream = torch.cuda.current_stream().cuda_stream |
|
|
| def fused(): |
| gemm.fp8_nn_bias( |
| x_fp8.data_ptr(), w_fp8.data_ptr(), out_fp16.data_ptr(), |
| bias_fp16.data_ptr(), M, N, K, alpha, stream, |
| ) |
|
|
| def decomposed(): |
| gemm.fp8_descale_fp16( |
| x_fp8.data_ptr(), w_fp8.data_ptr(), out_fp16.data_ptr(), M, N, K, |
| x_scale.data_ptr(), w_scale.data_ptr(), stream, |
| ) |
| fvk.add_bias_fp16( |
| out_fp16.data_ptr(), bias_fp16.data_ptr(), M, N, stream |
| ) |
|
|
| try: |
| fused() |
| fused_runner = fused |
| fused_error = None |
| except RuntimeError as error: |
| fused_runner = None |
| fused_error = str(error) |
| decomposed() |
| return fused_runner, decomposed, fused_error |
|
|
|
|
| def metrics(got: torch.Tensor, expected: torch.Tensor) -> dict[str, float]: |
| got_f = got.float().flatten() |
| exp_f = expected.float().flatten() |
| diff = (got_f - exp_f).abs() |
| return { |
| "max_abs": float(diff.max().item()), |
| "mean_abs": float(diff.mean().item()), |
| "p99_abs": float(torch.quantile(diff, 0.99).item()), |
| "cosine": float(torch.nn.functional.cosine_similarity(got_f, exp_f, dim=0).item()), |
| } |
|
|
|
|
| def run(args) -> dict: |
| if not torch.cuda.is_available(): |
| raise SystemExit("CUDA is required") |
| torch.manual_seed(20260718) |
| ops, helpers = load_ops(args.backend, args.artifact) |
| labels = list(SHAPES) if args.shapes == "all" else args.shapes.split(",") |
| results = [] |
|
|
| for label in labels: |
| if label not in SHAPES: |
| raise ValueError(f"unknown shape {label!r}") |
| M, K, N = SHAPES[label] |
| x_bf16 = torch.randn((M, K), device="cuda", dtype=torch.bfloat16) * 0.25 |
| w_bf16 = torch.randn((N, K), device="cuda", dtype=torch.bfloat16) * (K**-0.5) |
| bias = torch.randn((N,), device="cuda", dtype=torch.bfloat16) * 0.01 |
| x_scale = ( |
| x_bf16.float().abs().max() / (0.9 * helpers.fp8_max()) |
| ).clamp_min(1e-6).reshape(1) |
| w_scale = ( |
| w_bf16.float().abs().max() / (0.9 * helpers.fp8_max()) |
| ).clamp_min(1e-6).reshape(1) |
| x_fp8 = helpers.quantize_fp8_reciprocal(x_bf16, x_scale) |
| w_fp8 = helpers.quantize_fp8(w_bf16, w_scale) |
| out = torch.empty((M, N), device="cuda", dtype=torch.bfloat16) |
| region_out = torch.empty_like(out) |
| input_fp8 = torch.empty_like(x_fp8) |
| fvk_out = torch.empty_like(out) |
| fvk_fp16_out = torch.empty((M, N), device="cuda", dtype=torch.float16) |
| bias_fp16 = bias.to(torch.float16) |
|
|
| def package_fp8(): |
| ops.fp8_linear_bias_bf16( |
| x_fp8, w_fp8, bias, x_scale, w_scale, out=out |
| ) |
|
|
| def package_region(): |
| ops.bf16_fp8_linear_bias_bf16( |
| x_bf16, w_fp8, bias, x_scale, w_scale, |
| input_fp8=input_fp8, out=region_out, pad_to=M, |
| ) |
|
|
| reference = helpers.ref_linear_bias( |
| x_fp8, w_fp8, bias, x_scale, w_scale |
| ) |
| package_fp8() |
| package_metrics = metrics(out, reference) |
| if package_metrics["p99_abs"] > 0.015625 or package_metrics["cosine"] < 0.9999: |
| raise AssertionError(f"{label} package correctness failed: {package_metrics}") |
| package_region() |
| if not torch.equal(region_out, out) or not torch.equal(input_fp8, x_fp8): |
| raise AssertionError(f"{label} BF16 region does not match FP8 entry") |
|
|
| graph = graph_runner(package_region) |
| if args.compare_fvk: |
| fvk, fvk_error = maybe_fvk_runner( |
| x_fp8, w_fp8, bias, fvk_out, x_scale, w_scale, M, N, K |
| ) |
| else: |
| fvk, fvk_error = None, "not requested" |
| if args.compare_fvk: |
| fvk_fp16_fused, fvk_fp16_decomposed, fvk_fp16_error = ( |
| maybe_fvk_fp16_runners( |
| x_fp8, w_fp8, bias_fp16, fvk_fp16_out, x_scale, w_scale, |
| M, N, K, |
| ) |
| ) |
| else: |
| fvk_fp16_fused = None |
| fvk_fp16_decomposed = None |
| fvk_fp16_error = "not requested" |
| if fvk is not None: |
| fvk() |
| fvk_metrics = metrics(fvk_out, reference) |
| if fvk_metrics["p99_abs"] > 0.015625 or fvk_metrics["cosine"] < 0.9999: |
| raise AssertionError(f"{label} FVK correctness failed: {fvk_metrics}") |
| else: |
| fvk_metrics = None |
|
|
| def eager(value): |
| return torch.addmm(bias, value, w_bf16.T) |
|
|
| compiled = torch.compile(eager, fullgraph=True) if args.compile_baseline else None |
| if compiled is not None: |
| compiled_out = compiled(x_bf16) |
| changed_out = compiled(x_bf16 + torch.ones_like(x_bf16) * 0.125) |
| torch.cuda.synchronize() |
| if torch.equal(compiled_out, changed_out): |
| raise AssertionError( |
| f"{label} compiled baseline did not respond to changed input" |
| ) |
|
|
| package_us = cuda_time_us(package_fp8, args.warmup, args.iters, args.rounds) |
| region_us = cuda_time_us(package_region, args.warmup, args.iters, args.rounds) |
| graph_us = cuda_time_us(graph, args.warmup, args.iters, args.rounds) |
| eager_call = lambda: eager(x_bf16) |
| compiled_call = (lambda: compiled(x_bf16)) if compiled is not None else None |
| eager_us = cuda_time_us(eager_call, args.warmup, args.iters, args.rounds) |
| compiled_us = ( |
| cuda_time_us(compiled_call, args.warmup, args.iters, args.rounds) |
| if compiled_call is not None else None |
| ) |
| fvk_us = ( |
| cuda_time_us(fvk, args.warmup, args.iters, args.rounds) |
| if fvk is not None else None |
| ) |
| fvk_fp16_fused_us = ( |
| cuda_time_us( |
| fvk_fp16_fused, args.warmup, args.iters, args.rounds |
| ) |
| if fvk_fp16_fused is not None else None |
| ) |
| fvk_fp16_decomposed_us = ( |
| cuda_time_us( |
| fvk_fp16_decomposed, args.warmup, args.iters, args.rounds |
| ) |
| if fvk_fp16_decomposed is not None else None |
| ) |
| row = { |
| "shape": label, |
| "M": M, |
| "K": K, |
| "N": N, |
| "fp8_linear_bias_us": package_us, |
| "bf16_region_us": region_us, |
| "bf16_region_graph_us": graph_us, |
| "torch_bf16_eager_us": eager_us, |
| "torch_bf16_compiled_us": compiled_us, |
| "fvk_fp8_nn_bias_bf16_us": fvk_us, |
| "fvk_status": "ok" if fvk is not None else "unsupported", |
| "fvk_error": fvk_error, |
| "fvk_fp16_fused_us": fvk_fp16_fused_us, |
| "fvk_fp16_decomposed_us": fvk_fp16_decomposed_us, |
| "fvk_fp16_fused_error": fvk_fp16_error, |
| "package_vs_eager": eager_us / package_us, |
| "region_vs_eager": eager_us / region_us, |
| "graph_vs_eager": eager_us / graph_us, |
| "package_vs_fvk": fvk_us / package_us if fvk_us is not None else None, |
| "package_vs_fvk_fp16_fused": ( |
| fvk_fp16_fused_us / package_us |
| if fvk_fp16_fused_us is not None else None |
| ), |
| "package_vs_fvk_fp16_decomposed": ( |
| fvk_fp16_decomposed_us / package_us |
| if fvk_fp16_decomposed_us is not None else None |
| ), |
| "package_correctness": package_metrics, |
| "fvk_correctness": fvk_metrics, |
| } |
| results.append(row) |
| print(json.dumps(row, sort_keys=True)) |
|
|
| return { |
| "backend": args.backend, |
| "device": torch.cuda.get_device_name(0), |
| "capability": list(torch.cuda.get_device_capability(0)), |
| "torch": torch.__version__, |
| "cuda": torch.version.cuda, |
| "warmup": args.warmup, |
| "iters": args.iters, |
| "rounds": args.rounds, |
| "results": results, |
| } |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--backend", choices=["source", "installed"], default="source") |
| parser.add_argument("--artifact", default=None) |
| parser.add_argument("--shapes", default="all") |
| parser.add_argument("--warmup", type=int, default=20) |
| parser.add_argument("--iters", type=int, default=100) |
| parser.add_argument("--rounds", type=int, default=5) |
| parser.add_argument("--compile-baseline", action="store_true") |
| parser.add_argument("--compare-fvk", action="store_true") |
| parser.add_argument("--output", type=Path, default=None) |
| args = parser.parse_args() |
| payload = run(args) |
| if args.output is not None: |
| args.output.parent.mkdir(parents=True, exist_ok=True) |
| args.output.write_text(json.dumps(payload, indent=2) + "\n") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|