#!/usr/bin/env python3 """Benchmark flashrt-fp8-swiglu-ffn against PyTorch references.""" from __future__ import annotations import argparse import ctypes import ctypes.util import importlib import json import math import os import sys from dataclasses import asdict, dataclass from pathlib import Path import torch ROOT = Path(__file__).resolve().parents[2] PACKAGE = ROOT / "flashrt-fp8-swiglu-ffn" REGISTRATION_INCLUDE = ( ROOT.parent / "kernels" / "kernel-builder" / "src" / "pyproject" / "templates" / "torch" ) SHAPES = { "pi05_decoder_m1": (1, 1024, 4096, 1024), "pi05_decoder_m8": (8, 1024, 4096, 1024), "pi05_decoder_m10": (10, 1024, 4096, 1024), "pi05_decoder_m16": (16, 1024, 4096, 1024), "pi05_vision_1view": (256, 1152, 4304, 1152), "pi05_vision_2view": (512, 1152, 4304, 1152), "pi05_vision_3view": (768, 1152, 4304, 1152), "groot_vl_seq512": (512, 2048, 8192, 2048), "groot_vl_seq1024": (1024, 2048, 8192, 2048), "groot_vl_seq2520": (2520, 2048, 8192, 2048), "action_dit": (41, 1536, 6144, 1536), } SHAPE_GROUPS = { "smoke": ["pi05_decoder_m10"], "headline": ["pi05_decoder_m10", "pi05_vision_2view", "groot_vl_seq1024"], "pi05": [ "pi05_decoder_m1", "pi05_decoder_m8", "pi05_decoder_m10", "pi05_decoder_m16", "pi05_vision_1view", "pi05_vision_2view", "pi05_vision_3view", ], "all": list(SHAPES.keys()), } @dataclass class Result: shape: str M: int K: int H: int N: int flashrt_us: float torch_eager_us: float torch_compile_us: float | None speedup_vs_eager: float speedup_vs_compile: float | None compile_status: str max_abs: float mean_abs: float p99_abs: float cosine: float p99_rel_floor1: float torch_ref_max_abs: float torch_ref_mean_abs: float torch_ref_p99_abs: float torch_ref_cosine: float torch_ref_p99_rel_floor1: float status: str class SourceOps: def __init__(self, namespace: str) -> None: self._ops = getattr(torch.ops, namespace) def fp8_gemm_bf16(self, x, w, x_scale, w_scale, out=None): if out is None: out = torch.empty((x.shape[0], w.shape[0]), device=x.device, dtype=torch.bfloat16) self._ops.fp8_gemm_bf16(x, w, x_scale, w_scale, out) return out def silu_mul_merged_quantize_fp8_static_bf16(self, gate_up, scale, out=None): if out is None: out = torch.empty( (gate_up.shape[0], gate_up.shape[1] // 2), device=gate_up.device, dtype=fp8_dtype(), ) self._ops.silu_mul_merged_quantize_fp8_static_bf16(gate_up, scale, out) return out def fp8_swiglu_mlp_bf16( self, x, gate_up_w, down_w, x_scale, gate_up_w_scale, hidden_scale, down_w_scale, gate_up_bf16=None, hidden_fp8=None, out=None, ): if gate_up_bf16 is None: gate_up_bf16 = torch.empty( (x.shape[0], gate_up_w.shape[0]), device=x.device, dtype=torch.bfloat16, ) if hidden_fp8 is None: hidden_fp8 = torch.empty( (x.shape[0], gate_up_w.shape[0] // 2), device=x.device, dtype=fp8_dtype(), ) if out is None: out = torch.empty((x.shape[0], down_w.shape[0]), device=x.device, dtype=torch.bfloat16) self._ops.fp8_swiglu_mlp_bf16( x, gate_up_w, down_w, x_scale, gate_up_w_scale, hidden_scale, down_w_scale, gate_up_bf16, hidden_fp8, out, ) return out def _preload_cublaslt() -> None: for parent in Path(torch.__file__).resolve().parents: candidate = parent / "nvidia" / "cublas" / "lib" / "libcublasLt.so.12" if candidate.exists(): ctypes.CDLL(str(candidate), mode=ctypes.RTLD_GLOBAL) return library = ctypes.util.find_library("cublasLt") if library: ctypes.CDLL(library, mode=ctypes.RTLD_GLOBAL) def _current_arch_list() -> str: major, minor = torch.cuda.get_device_capability(0) return f"{major}.{minor}" def load_source_ops() -> SourceOps: from torch.utils.cpp_extension import load if not REGISTRATION_INCLUDE.is_dir(): raise RuntimeError(f"missing kernel-builder registration include: {REGISTRATION_INCLUDE}") _preload_cublaslt() os.environ.setdefault("TORCH_CUDA_ARCH_LIST", _current_arch_list()) namespace = "flashrt_fp8_swiglu_ffn_benchmark" load( name=namespace, sources=[ str(PACKAGE / "torch-ext" / "torch_binding.cpp"), str(PACKAGE / "csrc" / "fp8_swiglu_ffn.cu"), ], extra_include_paths=[str(PACKAGE / "csrc"), str(REGISTRATION_INCLUDE)], extra_cflags=["-O3", "-DCUDA_KERNEL"], extra_cuda_cflags=["-O3", "--expt-relaxed-constexpr", "-DCUDA_KERNEL"], verbose=False, ) return SourceOps(namespace) def load_installed_ops(artifact: str | None): if artifact: sys.path.insert(0, artifact) try: return importlib.import_module("flashrt_fp8_swiglu_ffn") finally: if artifact: sys.path.remove(artifact) def load_hub_ops(repo_id: str, version: int): from kernels import get_kernel return get_kernel(repo_id, version=version) def fp8_dtype() -> torch.dtype: if torch.version.hip is not None and hasattr(torch, "float8_e4m3fnuz"): return torch.float8_e4m3fnuz return torch.float8_e4m3fn def fp8_max() -> float: return 240.0 if torch.version.hip is not None else 448.0 def quantize_fp8(x: torch.Tensor, scale: torch.Tensor) -> torch.Tensor: limit = fp8_max() return torch.clamp(x.float() / scale.float(), -limit, limit).to(fp8_dtype()) def dequant_fp8(x: torch.Tensor, scale: torch.Tensor) -> torch.Tensor: return x.float() * scale.float() def torch_ref(x, gate_up_w, down_w, x_s, gu_s, hid_s, dn_s): gate_up = (dequant_fp8(x, x_s) @ dequant_fp8(gate_up_w, gu_s).T).to(torch.bfloat16) gate, up = gate_up.float().chunk(2, dim=1) hidden_fp8 = quantize_fp8(torch.nn.functional.silu(gate) * up, hid_s) return (dequant_fp8(hidden_fp8, hid_s) @ dequant_fp8(down_w, dn_s).T).to(torch.bfloat16) def compiler_disable(fn): compiler = getattr(torch, "compiler", None) if compiler is not None and hasattr(compiler, "disable"): return compiler.disable(fn) return torch._dynamo.disable(fn) def swiglu_quant_boundary(gate_up: torch.Tensor, scale: torch.Tensor) -> torch.Tensor: gate, up = gate_up.float().chunk(2, dim=1) return quantize_fp8(torch.nn.functional.silu(gate) * up, scale) stable_swiglu_quant_boundary = compiler_disable(swiglu_quant_boundary) def ref_fp8_gemm(x, w, x_scale, w_scale): return (dequant_fp8(x, x_scale) @ dequant_fp8(w, w_scale).T).to(torch.bfloat16) def make_case(M: int, K: int, H: int, N: int): x_s = torch.tensor([0.05], device="cuda", dtype=torch.float32) gu_s = torch.tensor([0.04], device="cuda", dtype=torch.float32) hid_s = torch.tensor([0.25], device="cuda", dtype=torch.float32) dn_s = torch.tensor([0.04], device="cuda", dtype=torch.float32) x = quantize_fp8(torch.randn((M, K), device="cuda", dtype=torch.bfloat16), x_s) gate_up_w = quantize_fp8( torch.randn((2 * H, K), device="cuda", dtype=torch.bfloat16), gu_s, ) down_w = quantize_fp8(torch.randn((N, H), device="cuda", dtype=torch.bfloat16), dn_s) scratch_gate_up = torch.empty((M, 2 * H), device="cuda", dtype=torch.bfloat16) scratch_hidden = torch.empty((M, H), device="cuda", dtype=fp8_dtype()) out = torch.empty((M, N), device="cuda", dtype=torch.bfloat16) return x, gate_up_w, down_w, x_s, gu_s, hid_s, dn_s, scratch_gate_up, scratch_hidden, out def time_us(fn, warmup: int, iters: int) -> float: 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() torch.cuda.synchronize() return start.elapsed_time(end) * 1000.0 / iters def percentile(x: torch.Tensor, q: float) -> torch.Tensor: flat = x.flatten() k = max(1, min(flat.numel(), math.ceil(q * flat.numel()))) return flat.kthvalue(k).values def metrics(got: torch.Tensor, expected: torch.Tensor): diff = (got.float() - expected.float()).abs().flatten() rel = diff / expected.float().abs().flatten().clamp_min(1.0) cosine = torch.nn.functional.cosine_similarity( got.float().flatten(), expected.float().flatten(), dim=0 ) return { "max_abs": float(diff.max().item()), "mean_abs": float(diff.mean().item()), "p99_abs": float(percentile(diff, 0.99).item()), "cosine": float(cosine.item()), "p99_rel_floor1": float(percentile(rel, 0.99).item()), } def run_one(ops, name: str, shape: tuple[int, int, int, int], args) -> Result: M, K, H, N = shape x, gu_w, dn_w, x_s, gu_s, hid_s, dn_s, gate_up, hidden, out = make_case(M, K, H, N) def flashrt_call(): return ops.fp8_swiglu_mlp_bf16( x, gu_w, dn_w, x_s, gu_s, hid_s, dn_s, gate_up, hidden, out ) eager_out = torch_ref(x, gu_w, dn_w, x_s, gu_s, hid_s, dn_s) flash_out = flashrt_call() staged_gate_up = ops.fp8_gemm_bf16(x, gu_w, x_s, gu_s) staged_hidden = ops.silu_mul_merged_quantize_fp8_static_bf16(staged_gate_up, hid_s) staged_out = ops.fp8_gemm_bf16(staged_hidden, dn_w, hid_s, dn_s) torch.cuda.synchronize() m = metrics(flash_out, staged_out) torch_m = metrics(flash_out, eager_out) status = "PASS" if m["p99_abs"] <= args.p99_abs_limit and m["p99_rel_floor1"] <= args.p99_rel_limit else "FAIL" flashrt_us = time_us(flashrt_call, args.warmup, args.iters) eager_us = time_us(lambda: torch_ref(x, gu_w, dn_w, x_s, gu_s, hid_s, dn_s), args.warmup, args.iters) compile_us = None compile_status = "skipped" if args.compile_baseline: try: compiled_gemm = torch.compile(ref_fp8_gemm, fullgraph=True, mode="reduce-overhead") def compiled_ref(): gate_up_ref = compiled_gemm(x, gu_w, x_s, gu_s) hidden_ref = stable_swiglu_quant_boundary(gate_up_ref, hid_s) return compiled_gemm(hidden_ref, dn_w, hid_s, dn_s) compiled_out = compiled_ref() torch.cuda.synchronize() cm = metrics(compiled_out, eager_out) if cm["p99_abs"] <= args.p99_abs_limit and cm["p99_rel_floor1"] <= args.p99_rel_limit: compile_us = time_us( compiled_ref, args.warmup, args.iters, ) compile_status = "segmented-ok" else: compile_status = ( f"mismatch p99_abs={cm['p99_abs']:.6f} " f"p99_rel={cm['p99_rel_floor1']:.6f}" ) except Exception as exc: compile_status = f"failed: {type(exc).__name__}: {exc}" return Result( shape=name, M=M, K=K, H=H, N=N, flashrt_us=flashrt_us, torch_eager_us=eager_us, torch_compile_us=compile_us, speedup_vs_eager=eager_us / flashrt_us, speedup_vs_compile=(compile_us / flashrt_us if compile_us else None), compile_status=compile_status, status=status, torch_ref_max_abs=torch_m["max_abs"], torch_ref_mean_abs=torch_m["mean_abs"], torch_ref_p99_abs=torch_m["p99_abs"], torch_ref_cosine=torch_m["cosine"], torch_ref_p99_rel_floor1=torch_m["p99_rel_floor1"], **m, ) def write_markdown(path: Path, results: list[Result]) -> None: lines = [ "| Shape | M,K,H,N | FlashRT us | Eager us | vs eager | Compile us | vs compile | Compile status | Staged p99 | Staged cosine | Torch-ref p99 | Torch-ref cosine | Status |", "|---|---:|---:|---:|---:|---:|---:|---|---:|---:|---:|---:|---|", ] for r in results: compile_us = "n/a" if r.torch_compile_us is None else f"{r.torch_compile_us:.3f}" speedup_compile = "n/a" if r.speedup_vs_compile is None else f"{r.speedup_vs_compile:.2f}x" lines.append( f"| {r.shape} | {r.M},{r.K},{r.H},{r.N} | {r.flashrt_us:.3f} | " f"{r.torch_eager_us:.3f} | {r.speedup_vs_eager:.2f}x | {compile_us} | " f"{speedup_compile} | {r.compile_status} | {r.p99_abs:.6f} | " f"{r.cosine:.8f} | {r.torch_ref_p99_abs:.6f} | " f"{r.torch_ref_cosine:.8f} | {r.status} |" ) path.write_text("\n".join(lines) + "\n") def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--backend", choices=["source", "installed", "hub"], default="source") parser.add_argument("--artifact", default=None) parser.add_argument("--repo-id", default="flashrt/flashrt-fp8-swiglu-ffn") parser.add_argument("--version", type=int, default=1) parser.add_argument("--shapes", choices=sorted(SHAPE_GROUPS), default="smoke") parser.add_argument("--warmup", type=int, default=5) parser.add_argument("--iters", type=int, default=20) parser.add_argument("--compile-baseline", action="store_true") parser.add_argument("--p99-abs-limit", type=float, default=1.0) parser.add_argument("--p99-rel-limit", type=float, default=0.05) parser.add_argument("--output", default=None) parser.add_argument("--markdown", default=None) args = parser.parse_args() if not torch.cuda.is_available(): raise SystemExit("CUDA is required") torch.manual_seed(17) ops = { "source": load_source_ops, "installed": lambda: load_installed_ops(args.artifact), "hub": lambda: load_hub_ops(args.repo_id, args.version), }[args.backend]() results = [run_one(ops, name, SHAPES[name], args) for name in SHAPE_GROUPS[args.shapes]] for r in results: print( f"{r.status} {r.shape}: flashrt={r.flashrt_us:.3f}us " f"eager={r.torch_eager_us:.3f}us speedup={r.speedup_vs_eager:.2f}x " f"staged_p99={r.p99_abs:.6f} staged_cosine={r.cosine:.8f} " f"torch_ref_p99={r.torch_ref_p99_abs:.6f} " f"torch_ref_cosine={r.torch_ref_cosine:.8f}" ) if args.output: Path(args.output).parent.mkdir(parents=True, exist_ok=True) Path(args.output).write_text(json.dumps([asdict(r) for r in results], indent=2) + "\n") if args.markdown: Path(args.markdown).parent.mkdir(parents=True, exist_ok=True) write_markdown(Path(args.markdown), results) if any(r.status != "PASS" for r in results): raise SystemExit(1) if __name__ == "__main__": main()