Download benchmarks/benchmark_linear_bias.py from flashrt/flashrt-fp8-ffn: direct link, hf CLI and curl.
- Browser
- Download file 12.1 kB
-
https://huggingface.co/flashrt/flashrt-fp8-ffn/resolve/main/benchmarks/benchmark_linear_bias.py
- Command line
-
hf download hf://flashrt/flashrt-fp8-ffn/benchmarks/benchmark_linear_bias.py
-
curl -L -o benchmark_linear_bias.py https://huggingface.co/flashrt/flashrt-fp8-ffn/resolve/main/benchmarks/benchmark_linear_bias.py
12.1 kB
| #!/usr/bin/env python3 | |
| """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() | |