flashrt-spatiotemporal-layout / benchmarks /benchmark_native_parity.py
liangsu9988's picture
Promote latest kernel artifacts to main
0e2da66 verified
Raw
History Blame Contribute Delete
13.5 kB
#!/usr/bin/env python3
"""Benchmark Tensor wrappers against native FlashRT and torch baselines."""
from __future__ import annotations
import argparse
import importlib
import json
import os
import sys
from dataclasses import asdict, dataclass
from pathlib import Path
import torch
ROOT = Path(__file__).resolve().parents[2]
PACKAGE = ROOT / "flashrt-spatiotemporal-layout"
REGISTRATION = (
ROOT.parent / "kernels/kernel-builder/src/pyproject/templates/torch"
)
SHAPES = {
"latent-small": (1, 64, 4, 32, 32),
"vae-channel320": (1, 320, 17, 32, 32),
"vae-channel512": (1, 512, 4, 64, 64),
}
@dataclass
class Result:
workload: str
shape: str
dtype: str
native_us: float
wrapper_us: float
native_parity: float
graph_native_us: float
graph_wrapper_us: float
graph_native_parity: float
eager_us: float
compile_us: float
strong_library_us: str
max_abs: float
mean_abs: float
p99_abs: float
cosine: float
accepted: bool
def time_us(fn, warmup: int, iters: int) -> float:
for _ in range(warmup):
fn()
torch.cuda.synchronize()
begin = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
begin.record()
for _ in range(iters):
fn()
end.record()
torch.cuda.synchronize()
return begin.elapsed_time(end) * 1000.0 / iters
def graph_time_us(fn, warmup: int, iters: int) -> float:
side = torch.cuda.Stream()
side.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(side):
fn()
torch.cuda.current_stream().wait_stream(side)
torch.cuda.synchronize()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
fn()
return time_us(graph.replay, warmup, iters)
def build_source():
from torch.utils.cpp_extension import load
major, minor = torch.cuda.get_device_capability()
os.environ.setdefault("TORCH_CUDA_ARCH_LIST", f"{major}.{minor}")
namespace = "flashrt_spatiotemporal_layout_native_parity"
load(
name=namespace,
sources=[
str(PACKAGE / "torch-ext/torch_binding.cpp"),
str(PACKAGE / "csrc/spatiotemporal_layout.cu"),
str(PACKAGE / "csrc/bf16_ndhwc_to_ncdhw_transpose.cu"),
str(PACKAGE / "csrc/bf16_quant_fp8_ncdhw_to_ndhwc.cu"),
],
extra_include_paths=[str(PACKAGE / "csrc"), str(REGISTRATION)],
extra_cflags=["-O3", "-DCUDA_KERNEL"],
extra_cuda_cflags=["-O3", "-DCUDA_KERNEL"],
is_python_module=False,
verbose=False,
)
return getattr(torch.ops, namespace)
def load_wrapper(backend: str, artifact: str | None):
if backend == "source":
return build_source()
if artifact:
sys.path.insert(0, artifact)
try:
return importlib.import_module("flashrt_spatiotemporal_layout")
finally:
if artifact:
sys.path.remove(artifact)
def build_native():
from torch.utils.cpp_extension import load
return load(
name="flashrt_spatiotemporal_layout_raw_native",
sources=[
str(PACKAGE / "benchmarks/native_binding.cpp"),
str(PACKAGE / "csrc/spatiotemporal_layout.cu"),
str(PACKAGE / "csrc/bf16_ndhwc_to_ncdhw_transpose.cu"),
str(PACKAGE / "csrc/bf16_quant_fp8_ncdhw_to_ndhwc.cu"),
],
extra_include_paths=[str(PACKAGE / "csrc")],
extra_cflags=["-O3"],
extra_cuda_cflags=["-O3"],
verbose=False,
)
def metrics(got: torch.Tensor, ref: torch.Tensor):
if got.dtype == torch.float8_e4m3fn:
exact = torch.equal(got.view(torch.uint8), ref.view(torch.uint8))
return (0.0, 0.0, 0.0, 1.0) if exact else (float("inf"),) * 4
diff = (got.float() - ref.float()).abs().flatten()
cosine = torch.nn.functional.cosine_similarity(
got.float().flatten(), ref.float().flatten(), dim=0
).item()
return (
diff.max().item(),
diff.mean().item(),
torch.quantile(diff, 0.99).item(),
cosine,
)
def add_result(
rows,
workload,
shape,
wrapper_fn,
native_fn,
eager_fn,
compiled_fn,
got,
ref,
args,
):
native_us = time_us(native_fn, args.warmup, args.iters)
wrapper_us = time_us(wrapper_fn, args.warmup, args.iters)
graph_native_us = graph_time_us(native_fn, args.warmup, args.iters)
graph_wrapper_us = graph_time_us(wrapper_fn, args.warmup, args.iters)
eager_us = time_us(eager_fn, args.warmup, args.iters)
compile_us = time_us(compiled_fn, args.warmup, args.iters)
max_abs, mean_abs, p99_abs, cosine = metrics(got, ref)
parity = wrapper_us / native_us
rows.append(
Result(
workload,
str(shape),
str(got.dtype),
native_us,
wrapper_us,
parity,
graph_native_us,
graph_wrapper_us,
graph_wrapper_us / graph_native_us,
eager_us,
compile_us,
"N/A (no equivalent single library op)",
max_abs,
mean_abs,
p99_abs,
cosine,
(
wrapper_us - native_us <= max(0.75, native_us * 0.05)
or graph_wrapper_us - graph_native_us
<= max(0.5, graph_native_us * 0.05)
)
and min(wrapper_us, graph_wrapper_us)
<= min(eager_us, compile_us) * 0.98
and max_abs == 0.0,
)
)
def run_shape(wrapper, native, name, shape, args):
b, c, t, h, w = shape
x = torch.randn(shape, device="cuda", dtype=torch.bfloat16)
ndhwc = x.permute(0, 2, 3, 4, 1).contiguous()
bias = torch.randn(c, device="cuda", dtype=torch.bfloat16)
residual = torch.randn_like(x)
scale = 0.03125
rows = []
def wrapper_layout():
wrapper.ndhwc_to_ncdhw_bf16(ndhwc, out_layout)
def eager_layout():
out_layout.copy_(ndhwc.permute(0, 4, 1, 2, 3))
compiled_layout = torch.compile(eager_layout, fullgraph=True)
out_layout = torch.empty_like(x)
native_layout = lambda: native.ndhwc_to_ncdhw(ndhwc, out_layout)
wrapper_layout()
ref = ndhwc.permute(0, 4, 1, 2, 3).contiguous()
add_result(
rows, f"{name}/ndhwc_to_ncdhw", shape, wrapper_layout, native_layout,
eager_layout, compiled_layout, out_layout, ref, args
)
out_bias = torch.empty_like(x)
wrapper_bias = lambda: wrapper.ndhwc_to_ncdhw_bias_bf16(
ndhwc, bias, out_bias
)
native_bias = lambda: native.ndhwc_to_ncdhw_bias(ndhwc, bias, out_bias)
def eager_bias():
out_bias.copy_(
(ndhwc.permute(0, 4, 1, 2, 3).float()
+ bias.float().view(1, c, 1, 1, 1)).to(torch.bfloat16)
)
compiled_bias = torch.compile(eager_bias, fullgraph=True)
wrapper_bias()
ref_bias = (
x.float() + bias.float().view(1, c, 1, 1, 1)
).to(torch.bfloat16)
add_result(
rows, f"{name}/ndhwc_to_ncdhw_bias", shape, wrapper_bias, native_bias,
eager_bias, compiled_bias, out_bias, ref_bias, args
)
out_add = torch.empty_like(x)
wrapper_add = lambda: wrapper.ndhwc_to_ncdhw_add_bf16(
ndhwc, residual, out_add
)
native_add = lambda: native.ndhwc_to_ncdhw_add(
ndhwc, residual, out_add
)
def eager_add():
out_add.copy_(
(ndhwc.permute(0, 4, 1, 2, 3).float()
+ residual.float()).to(torch.bfloat16)
)
compiled_add = torch.compile(eager_add, fullgraph=True)
wrapper_add()
ref_add = (x.float() + residual.float()).to(torch.bfloat16)
add_result(
rows, f"{name}/ndhwc_to_ncdhw_add", shape, wrapper_add, native_add,
eager_add, compiled_add, out_add, ref_add, args
)
out_fp8 = torch.empty(
(b, t, h, w, c), device="cuda", dtype=torch.float8_e4m3fn
)
wrapper_quant = lambda: wrapper.ncdhw_quantize_fp8_static_ndhwc_bf16(
x, scale, out_fp8
)
native_quant = lambda: native.ncdhw_quantize(x, scale, out_fp8)
def eager_quant():
out_fp8.copy_(
(x.float() / scale).clamp(-448.0, 448.0)
.to(torch.float8_e4m3fn).permute(0, 2, 3, 4, 1)
)
compiled_quant = torch.compile(eager_quant, fullgraph=True)
wrapper_quant()
ref_fp8 = (
(x.float() / scale).clamp(-448.0, 448.0)
.to(torch.float8_e4m3fn).permute(0, 2, 3, 4, 1).contiguous()
)
add_result(
rows, f"{name}/ncdhw_quantize_fp8_ndhwc", shape, wrapper_quant,
native_quant, eager_quant, compiled_quant, out_fp8, ref_fp8, args
)
return rows
def run_cache_and_upsample(wrapper, native, args):
rows = []
previous = torch.randn(
(1, 64, 2, 32, 32), device="cuda", dtype=torch.bfloat16
)
current = torch.randn(
(1, 64, 1, 32, 32), device="cuda", dtype=torch.bfloat16
)
packed = torch.empty(
(1, 32, 32, 192), device="cuda", dtype=torch.bfloat16
)
wrapper_pack = lambda: wrapper.pack_causal_cache3_nhwc_bf16(
previous, current, packed
)
native_pack = lambda: native.pack_causal_cache3_nhwc(
previous, current, packed
)
def eager_pack():
packed.copy_(
torch.cat(
(previous[:, :, 0], previous[:, :, 1], current[:, :, 0]),
dim=1,
).permute(0, 2, 3, 1)
)
compiled_pack = torch.compile(eager_pack, fullgraph=True)
wrapper_pack()
got_pack = packed.clone()
ref_pack = torch.cat(
(previous[:, :, 0], previous[:, :, 1], current[:, :, 0]), dim=1
).permute(0, 2, 3, 1).contiguous()
add_result(
rows,
"vae-t1/pack_causal_cache3_nhwc",
tuple(current.shape),
wrapper_pack,
native_pack,
eager_pack,
compiled_pack,
got_pack,
ref_pack,
args,
)
for temporal_factor, first_chunk in ((1, False), (2, True)):
spatial_factor = 2
out_channels = 16 if temporal_factor == 1 else 8
input = current if temporal_factor == 1 else torch.randn(
(1, 64, 4, 16, 16), device="cuda", dtype=torch.bfloat16
)
out_t = input.shape[2] * temporal_factor - (
temporal_factor - 1 if first_chunk else 0
)
out = torch.empty(
(
input.shape[0], out_channels, out_t,
input.shape[3] * spatial_factor,
input.shape[4] * spatial_factor,
),
device="cuda",
dtype=torch.bfloat16,
)
wrapper_up = lambda: wrapper.channel_to_space3d_bf16(
input, out_channels, temporal_factor, spatial_factor, 1,
first_chunk, out
)
native_up = lambda: native.channel_to_space3d(
input, out_channels, temporal_factor, spatial_factor, 1,
first_chunk, out
)
def eager_up():
expanded = input[:, : out_channels * temporal_factor * 4]
expanded = expanded.view(
input.shape[0], out_channels, temporal_factor, 2, 2,
input.shape[2], input.shape[3], input.shape[4],
).permute(0, 1, 5, 2, 6, 3, 7, 4)
value = expanded.reshape(
input.shape[0], out_channels,
input.shape[2] * temporal_factor,
input.shape[3] * 2, input.shape[4] * 2,
)
if first_chunk:
value = value[:, :, temporal_factor - 1 :]
out.copy_(value)
compiled_up = torch.compile(eager_up, fullgraph=True)
wrapper_up()
got = out.clone()
eager_up()
ref = out.clone()
out.copy_(got)
add_result(
rows,
f"vae/channel_to_space3d-ft{temporal_factor}"
f"-first{int(first_chunk)}",
tuple(input.shape),
wrapper_up,
native_up,
eager_up,
compiled_up,
got,
ref,
args,
)
return rows
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--backend", choices=["source", "installed"], default="source")
parser.add_argument("--artifact")
parser.add_argument("--warmup", type=int, default=20)
parser.add_argument("--iters", type=int, default=100)
parser.add_argument("--output")
args = parser.parse_args()
wrapper = load_wrapper(args.backend, args.artifact)
native = build_native()
rows = []
for name, shape in SHAPES.items():
rows.extend(run_shape(wrapper, native, name, shape, args))
rows.extend(run_cache_and_upsample(wrapper, native, args))
for row in rows:
print(
f"{row.workload}: native={row.native_us:.3f}us "
f"wrapper={row.wrapper_us:.3f}us parity={row.native_parity:.3f} "
f"graph={row.graph_wrapper_us:.3f}/"
f"{row.graph_native_us:.3f}us "
f"eager={row.eager_us:.3f}us compile={row.compile_us:.3f}us "
f"accepted={row.accepted}"
)
if args.output:
path = Path(args.output)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps([asdict(row) for row in rows], indent=2) + "\n")
if not all(row.accepted for row in rows):
raise SystemExit("performance/correctness acceptance failed")
if __name__ == "__main__":
main()