weight-only-ffn / benchmarks /benchmark.py
liangsu9988's picture
Promote latest kernel artifacts to main
fef614d verified
Raw
History Blame Contribute Delete
17.2 kB
#!/usr/bin/env python3
"""Tile/variant benchmark for the production M<=4 weight-only domain."""
from __future__ import annotations
import argparse
import importlib
import json
import os
import statistics
import sys
from pathlib import Path
import torch
import torch.nn.functional as F
SHAPES = {
"llm_m1": (1, 4096, 11008, 4096),
"llm_m2": (2, 4096, 11008, 4096),
"llm_m3": (3, 4096, 11008, 4096),
"llm_m4": (4, 4096, 11008, 4096),
"vla_m1": (1, 1024, 4096, 1024),
"vla_m2": (2, 1024, 4096, 1024),
"vla_m4": (4, 1024, 4096, 1024),
"vision_m1": (1, 1536, 6144, 1536),
"vision_m2": (2, 1536, 6144, 1536),
"vision_m4": (4, 1536, 6144, 1536),
}
LINEAR_SHAPES = {
"llm_square_m1": (1, 4096, 4096),
"llm_wide_m1": (1, 4096, 11008),
"vla_wide_m1": (1, 1024, 4096),
"vision_wide_m1": (1, 1536, 6144),
"llm_square_m2": (2, 4096, 4096),
"llm_wide_m2": (2, 4096, 11008),
"vla_wide_m2": (2, 1024, 4096),
"vision_wide_m2": (2, 1536, 6144),
}
def bench(fn, warmup: int, iterations: int, repeats: int = 3) -> float:
for _ in range(warmup):
fn()
torch.cuda.synchronize()
samples = []
for _ in range(repeats):
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
start.record()
for _ in range(iterations):
fn()
end.record()
torch.cuda.synchronize()
samples.append(float(start.elapsed_time(end) * 1000.0 / iterations))
return statistics.median(samples)
def sfb_bytes(rows: int, cols: int) -> int:
return ((rows + 127) // 128) * (((cols // 16) + 3) // 4) * 512
class SourceModule:
def __init__(self, ops) -> None:
self.ops = ops
def quantize_w4_weight_bf16(self, weight):
n, k = weight.shape
packed = torch.empty((n, k // 2), device="cuda", dtype=torch.uint8)
scale = torch.empty((sfb_bytes(n, k),), device="cuda", dtype=torch.uint8)
self.ops.quantize_w4_weight_bf16(weight, packed, scale)
return packed, scale
def dequantize_w4_weight_bf16(self, packed, scale, *, cols):
out = torch.empty((packed.shape[0], cols), device="cuda", dtype=torch.bfloat16)
self.ops.dequantize_w4_weight_bf16(packed, scale, out)
return out
def quantize_w8_weight_bf16(self, weight):
packed = torch.empty_like(weight, dtype=torch.int8)
scale = torch.empty((weight.shape[0],), device="cuda", dtype=torch.float32)
self.ops.quantize_w8_weight_bf16(weight, packed, scale)
return packed, scale
def dequantize_w8_weight_bf16(self, packed, scale):
out = torch.empty_like(packed, dtype=torch.bfloat16)
self.ops.dequantize_w8_weight_bf16(packed, scale, out)
return out
def w4a16_linear_bf16(self, x, weight, scale, *, variant=0, out=None):
if out is None:
out = torch.empty(
(x.shape[0], weight.shape[0]), device=x.device,
dtype=torch.bfloat16,
)
self.ops.w4a16_linear_bf16(
x, weight, scale, 1.0, variant, out,
)
return out
def w8a16_linear_bf16(self, x, weight, scale, *, variant=0, out=None):
if out is None:
out = torch.empty(
(x.shape[0], weight.shape[0]), device=x.device,
dtype=torch.bfloat16,
)
self.ops.w8a16_linear_bf16(x, weight, scale, variant, out)
return out
def _gated(self, bits, x, gu_w, gu_s, dn_w, dn_s, *, gelu,
gate_up_bias, down_bias, variant, workspace, out):
gu, hidden = workspace
if bits == 4:
self.ops.w4a16_gated_ffn_bf16(
x, gu_w, gu_s, dn_w, dn_s, gate_up_bias, down_bias,
gelu, 1.0, 1.0, variant, gu, hidden, out,
)
else:
self.ops.w8a16_gated_ffn_bf16(
x, gu_w, gu_s, dn_w, dn_s, gate_up_bias, down_bias,
gelu, variant, gu, hidden, out,
)
return out
def w4a16_swiglu_ffn_bf16(self, *args, **kwargs):
return self._gated(4, *args, gelu=False, **kwargs)
def w4a16_geglu_ffn_bf16(self, *args, **kwargs):
return self._gated(4, *args, gelu=True, **kwargs)
def w8a16_swiglu_ffn_bf16(self, *args, **kwargs):
return self._gated(8, *args, gelu=False, **kwargs)
def w8a16_geglu_ffn_bf16(self, *args, **kwargs):
return self._gated(8, *args, gelu=True, **kwargs)
def _gelu(self, bits, x, up_w, up_s, dn_w, dn_s, *, up_bias,
down_bias, variant, workspace, out):
up, hidden = workspace
if bits == 4:
self.ops.w4a16_gelu_ffn_bf16(
x, up_w, up_s, dn_w, dn_s, up_bias, down_bias,
1.0, 1.0, variant, up, hidden, out,
)
else:
self.ops.w8a16_gelu_ffn_bf16(
x, up_w, up_s, dn_w, dn_s, up_bias, down_bias,
variant, up, hidden, out,
)
return out
def w4a16_gelu_ffn_bf16(self, *args, **kwargs):
return self._gelu(4, *args, **kwargs)
def w8a16_gelu_ffn_bf16(self, *args, **kwargs):
return self._gelu(8, *args, **kwargs)
def load_source_module():
from torch.utils.cpp_extension import load
root = Path(__file__).resolve().parents[1]
registration = root.parent.parent / "kernels" / "kernel-builder" / "src" / "pyproject" / "templates" / "torch"
major, minor = torch.cuda.get_device_capability(0)
if major == 11 and minor == 0:
arch = "11.0a"
elif major == 12 and minor == 1:
arch = "12.1"
elif major >= 12:
arch = "12.0a"
else:
raise RuntimeError("source benchmark requires Blackwell SM110/SM120/SM121")
os.environ.setdefault("TORCH_CUDA_ARCH_LIST", arch)
namespace = "weight_only_ffn_benchmark_source"
load(
name=namespace,
sources=[str(root / path) for path in [
"torch-ext/torch_binding.cpp", "csrc/w4_weight_only.cu",
"csrc/w4a16_gemm_sm120.cu", "csrc/w4a16_matvec_sm120.cu",
"csrc/w8_weight_only.cu", "csrc/ffn_epilogues.cu",
]],
extra_include_paths=[str(root / "csrc"), str(registration)],
extra_cflags=["-O3", "-std=c++17", "-DCUDA_KERNEL"],
extra_cuda_cflags=["-O3", "-std=c++17", "-DCUDA_KERNEL", "--use_fast_math"],
is_python_module=False,
verbose=False,
)
return SourceModule(getattr(torch.ops, namespace))
def load_module(backend: str, artifact: str | None):
if backend == "source":
return load_source_module()
if artifact:
sys.path.insert(0, artifact)
try:
return importlib.import_module("weight_only_ffn")
finally:
if artifact:
sys.path.remove(artifact)
def quantize(module, bits: int, weight: torch.Tensor):
if bits == 4:
packed, scale = module.quantize_w4_weight_bf16(weight)
dequant = module.dequantize_w4_weight_bf16(packed, scale, cols=weight.shape[1])
else:
packed, scale = module.quantize_w8_weight_bf16(weight)
dequant = module.dequantize_w8_weight_bf16(packed, scale)
return packed, scale, dequant
def run_case(module, name: str, shape, bits: int, activation: str,
warmup: int, iterations: int):
m, k, h, n = shape
gated = activation in {"swiglu", "geglu"}
up_rows = 2 * h if gated else h
generator = torch.Generator(device="cuda").manual_seed(
91000 + m + k + h + n + bits + len(activation)
)
x = (torch.randn((m, k), generator=generator, device="cuda") * 0.1).bfloat16()
up_weight = (torch.randn((up_rows, k), generator=generator, device="cuda") * 0.02).bfloat16()
down_weight = (torch.randn((n, h), generator=generator, device="cuda") * 0.02).bfloat16()
up_bias = (torch.randn((up_rows,), generator=generator, device="cuda") * 0.01).bfloat16()
down_bias = (torch.randn((n,), generator=generator, device="cuda") * 0.01).bfloat16()
up_packed, up_scale, up_dequant = quantize(module, bits, up_weight)
down_packed, down_scale, down_dequant = quantize(module, bits, down_weight)
first = torch.empty((m, up_rows), device="cuda", dtype=torch.bfloat16)
hidden = torch.empty((m, h), device="cuda", dtype=torch.bfloat16)
out = torch.empty((m, n), device="cuda", dtype=torch.bfloat16)
if gated:
fn_name = f"w{bits}a16_{activation}_ffn_bf16"
kernel_fn = getattr(module, fn_name)
def kernel(variant: int):
return kernel_fn(
x, up_packed, up_scale, down_packed, down_scale,
gate_up_bias=up_bias, down_bias=down_bias, variant=variant,
workspace=(first, hidden), out=out,
)
def reference():
merged = F.linear(x, up_dequant, up_bias)
gate, up = merged.split(h, dim=-1)
act = F.silu(gate) if activation == "swiglu" else F.gelu(gate, approximate="tanh")
return F.linear(act * up, down_dequant, down_bias)
else:
fn_name = f"w{bits}a16_gelu_ffn_bf16"
kernel_fn = getattr(module, fn_name)
def kernel(variant: int):
return kernel_fn(
x, up_packed, up_scale, down_packed, down_scale,
up_bias=up_bias, down_bias=down_bias, variant=variant,
workspace=(first, hidden), out=out,
)
def reference():
return F.linear(
F.gelu(F.linear(x, up_dequant, up_bias), approximate="tanh"),
down_dequant, down_bias,
)
eager_us = bench(reference, warmup, iterations)
compiled = torch.compile(reference, fullgraph=True, mode="max-autotune-no-cudagraphs")
compiled()
torch.cuda.synchronize()
compiled_us = bench(compiled, warmup, iterations)
variants = {
str(variant): bench(
lambda variant=variant: kernel(variant), warmup, iterations
)
for variant in (1, 2, 3)
}
auto_error = None
try:
auto_us = bench(lambda: kernel(0), warmup, iterations)
kernel(0)
except RuntimeError as exc:
if "not qualified" not in str(exc) and "no qualified fast path" not in str(exc):
raise
auto_us = None
auto_error = str(exc)
best_diagnostic_variant = min(variants, key=variants.get)
kernel(int(best_diagnostic_variant))
ref = reference()
torch.cuda.synchronize()
diff = (out.float() - ref.float()).abs().flatten()
cosine = F.cosine_similarity(out.float().flatten(), ref.float().flatten(), dim=0)
best_diagnostic_variant = min(variants, key=variants.get)
best_diagnostic_us = variants[best_diagnostic_variant]
if auto_us is not None and auto_us > best_diagnostic_us * 1.05:
raise AssertionError(
f"{name} W{bits}A16 {activation}: auto {auto_us:.3f} us is more "
f"than 5% slower than diagnostic variant {best_diagnostic_variant} "
f"at {best_diagnostic_us:.3f} us"
)
if auto_us is not None and auto_us * 1.02 >= min(eager_us, compiled_us):
raise AssertionError(
f"{name} W{bits}A16 {activation}: accepted auto path must beat "
f"the strongest eager/compile baseline by at least 2%; "
f"auto={auto_us:.3f} us, eager={eager_us:.3f} us, "
f"compile={compiled_us:.3f} us"
)
return {
"region": "ffn",
"shape": name,
"M": m,
"K": k,
"H": h,
"N": n,
"precision": f"W{bits}A16",
"op": activation,
"eager_us": eager_us,
"compile_us": compiled_us,
"variant_us": variants,
"auto_status": "accepted" if auto_us is not None else "rejected",
"auto_us": auto_us,
"auto_error": auto_error,
"auto_speedup_vs_eager": eager_us / auto_us if auto_us is not None else None,
"auto_speedup_vs_compile": compiled_us / auto_us if auto_us is not None else None,
"best_diagnostic_variant": int(best_diagnostic_variant),
"best_diagnostic_us": best_diagnostic_us,
"max_abs": float(diff.max()),
"mean_abs": float(diff.mean()),
"p99_abs": float(torch.quantile(diff, 0.99)),
"cosine": float(cosine),
}
def run_linear_case(module, name: str, shape, bits: int, warmup: int,
iterations: int):
m, k, n = shape
generator = torch.Generator(device="cuda").manual_seed(
92000 + m + k + n + bits
)
x = (torch.randn((m, k), generator=generator, device="cuda") * 0.1).bfloat16()
weight = (
torch.randn((n, k), generator=generator, device="cuda") * 0.02
).bfloat16()
packed, scale, dequant = quantize(module, bits, weight)
out = torch.empty((m, n), device="cuda", dtype=torch.bfloat16)
linear = getattr(module, f"w{bits}a16_linear_bf16")
def kernel(variant: int):
return linear(x, packed, scale, variant=variant, out=out)
def reference():
return F.linear(x, dequant)
eager_us = bench(reference, warmup, iterations)
compiled = torch.compile(
reference, fullgraph=True, mode="max-autotune-no-cudagraphs"
)
compiled()
torch.cuda.synchronize()
compiled_us = bench(compiled, warmup, iterations)
variants = {
str(variant): bench(
lambda variant=variant: kernel(variant), warmup, iterations
)
for variant in (1, 2, 3)
}
auto_error = None
try:
auto_us = bench(lambda: kernel(0), warmup, iterations)
kernel(0)
except RuntimeError as exc:
if "no qualified fast path" not in str(exc):
raise
auto_us = None
auto_error = str(exc)
kernel(int(min(variants, key=variants.get)))
ref = reference()
torch.cuda.synchronize()
diff = (out.float() - ref.float()).abs().flatten()
cosine = F.cosine_similarity(
out.float().flatten(), ref.float().flatten(), dim=0
)
best_variant = min(variants, key=variants.get)
best_us = variants[best_variant]
if auto_us is not None and auto_us > best_us * 1.05:
raise AssertionError(
f"{name} W{bits}A16 linear: auto {auto_us:.3f} us is more than "
f"5% slower than diagnostic variant {best_variant} at {best_us:.3f} us"
)
if auto_us is not None and auto_us * 1.02 >= min(eager_us, compiled_us):
raise AssertionError(
f"{name} W{bits}A16 linear: accepted auto path must beat the "
f"strongest eager/compile baseline by at least 2%; "
f"auto={auto_us:.3f} us, eager={eager_us:.3f} us, "
f"compile={compiled_us:.3f} us"
)
return {
"region": "linear",
"shape": name,
"M": m,
"K": k,
"N": n,
"precision": f"W{bits}A16",
"op": "linear",
"eager_us": eager_us,
"compile_us": compiled_us,
"variant_us": variants,
"auto_status": "accepted" if auto_us is not None else "rejected",
"auto_us": auto_us,
"auto_error": auto_error,
"auto_speedup_vs_eager": eager_us / auto_us if auto_us else None,
"auto_speedup_vs_compile": compiled_us / auto_us if auto_us else None,
"best_diagnostic_variant": int(best_variant),
"best_diagnostic_us": best_us,
"max_abs": float(diff.max()),
"mean_abs": float(diff.mean()),
"p99_abs": float(torch.quantile(diff, 0.99)),
"cosine": float(cosine),
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--backend", choices=["source", "installed"], default="source")
parser.add_argument("--artifact")
parser.add_argument("--mode", choices=["smoke", "full"], default="smoke")
parser.add_argument("--warmup", type=int, default=20)
parser.add_argument("--iterations", type=int, default=100)
parser.add_argument("--json-out")
args = parser.parse_args()
module = load_module(args.backend, args.artifact)
names = ["llm_m1"] if args.mode == "smoke" else list(SHAPES)
rows = []
for name in names:
for bits in (4, 8):
for activation in ("swiglu", "geglu", "gelu"):
rows.append(run_case(module, name, SHAPES[name], bits, activation,
args.warmup, args.iterations))
linear_names = ["llm_square_m1"] if args.mode == "smoke" else list(LINEAR_SHAPES)
for name in linear_names:
for bits in (4, 8):
rows.append(run_linear_case(
module, name, LINEAR_SHAPES[name], bits,
args.warmup, args.iterations,
))
payload = {
"device": torch.cuda.get_device_name(),
"capability": list(torch.cuda.get_device_capability()),
"torch": torch.__version__,
"rows": rows,
}
print(json.dumps(payload, indent=2))
if args.json_out:
path = Path(args.json_out)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, indent=2) + "\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())