Signed-off-by: levius <2114377220@qq.com> Co-authored-by: OpenAI Codex <noreply@openai.com>
263 lines
9.4 KiB
Python
263 lines
9.4 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
|
|
"""Benchmark MammothModa2's Q/K RMSNorm + adjacent-pair real RoPE.
|
|
|
|
Example:
|
|
|
|
CUDA_VISIBLE_DEVICES=0 python \
|
|
benchmarks/diffusion/benchmark_mammoth_moda2_qk_norm_rope.py \
|
|
--tokens 512 4173 8346 --warmup 50 --iters 200
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.metadata
|
|
import json
|
|
import os
|
|
import statistics
|
|
from collections.abc import Callable
|
|
|
|
import torch
|
|
|
|
from vllm_omni.diffusion.layers.fused_qk_norm_rope import (
|
|
_fused_cuda_supported,
|
|
fused_qk_norm_rope,
|
|
)
|
|
from vllm_omni.diffusion.layers.norm import RMSNorm
|
|
from vllm_omni.diffusion.models.mammoth_moda2.rope_real import (
|
|
apply_real_rotary_emb,
|
|
)
|
|
|
|
_HEAD_DIM = 120
|
|
_Q_HEADS = 21
|
|
_KV_HEADS = 7
|
|
_EPS = 1e-5
|
|
|
|
|
|
def _parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--tokens", nargs="+", type=int, default=[512, 4173, 8346])
|
|
parser.add_argument("--warmup", type=int, default=50)
|
|
parser.add_argument("--iters", type=int, default=200)
|
|
parser.add_argument("--rounds", type=int, default=3)
|
|
parser.add_argument("--attention-sequences", nargs="*", type=int, default=[])
|
|
return parser.parse_args()
|
|
|
|
|
|
def _version(distribution: str) -> str | None:
|
|
try:
|
|
return importlib.metadata.version(distribution)
|
|
except importlib.metadata.PackageNotFoundError:
|
|
return None
|
|
|
|
|
|
def _measure(
|
|
fn: Callable[[], object],
|
|
warmup: int,
|
|
iters: int,
|
|
) -> dict[str, float]:
|
|
for _ in range(warmup):
|
|
fn()
|
|
torch.accelerator.synchronize()
|
|
|
|
torch.accelerator.reset_peak_memory_stats()
|
|
baseline_bytes = torch.accelerator.memory_allocated()
|
|
samples = []
|
|
for _ in range(iters):
|
|
start = torch.cuda.Event(enable_timing=True)
|
|
end = torch.cuda.Event(enable_timing=True)
|
|
start.record()
|
|
fn()
|
|
end.record()
|
|
end.synchronize()
|
|
samples.append(float(start.elapsed_time(end)))
|
|
|
|
ordered = sorted(samples)
|
|
return {
|
|
"median_ms": statistics.median(ordered),
|
|
"mean_ms": statistics.mean(ordered),
|
|
"stddev_ms": statistics.pstdev(ordered),
|
|
"min_ms": ordered[0],
|
|
"max_ms": ordered[-1],
|
|
"p90_ms": ordered[min(len(ordered) - 1, int(len(ordered) * 0.9))],
|
|
"extra_peak_mib": (torch.accelerator.max_memory_allocated() - baseline_bytes) / 2**20,
|
|
}
|
|
|
|
|
|
@torch.inference_mode()
|
|
def _run_shape(tokens: int, warmup: int, iters: int, *, fused_first: bool = False) -> dict[str, object]:
|
|
dtype = torch.bfloat16
|
|
device = torch.device("cuda")
|
|
torch.manual_seed(42)
|
|
query = torch.randn(tokens, _Q_HEADS, _HEAD_DIM, device=device, dtype=dtype)
|
|
key = torch.randn(tokens, _KV_HEADS, _HEAD_DIM, device=device, dtype=dtype)
|
|
norm_q = RMSNorm(_HEAD_DIM, eps=_EPS).to(device=device, dtype=dtype)
|
|
norm_k = RMSNorm(_HEAD_DIM, eps=_EPS).to(device=device, dtype=dtype)
|
|
norm_q.weight.normal_(mean=1.0, std=0.2)
|
|
norm_k.weight.normal_(mean=1.0, std=0.2)
|
|
angles = torch.rand(tokens, _HEAD_DIM // 2, device=device) * (2 * torch.pi)
|
|
cos = angles.cos().repeat_interleave(2, dim=-1).to(dtype)
|
|
sin = angles.sin().repeat_interleave(2, dim=-1).to(dtype)
|
|
|
|
def native() -> tuple[torch.Tensor, torch.Tensor]:
|
|
q_out = apply_real_rotary_emb(norm_q(query).unsqueeze(0), cos.unsqueeze(0), sin.unsqueeze(0)).squeeze(0)
|
|
k_out = apply_real_rotary_emb(norm_k(key).unsqueeze(0), cos.unsqueeze(0), sin.unsqueeze(0)).squeeze(0)
|
|
return q_out, k_out
|
|
|
|
def fused() -> tuple[torch.Tensor, torch.Tensor]:
|
|
rope_table = torch.cat((cos[..., 0::2], sin[..., 0::2]), dim=-1)
|
|
return fused_qk_norm_rope(
|
|
query,
|
|
key,
|
|
norm_q.weight,
|
|
norm_k.weight,
|
|
rope_table,
|
|
_EPS,
|
|
head_dim=_HEAD_DIM,
|
|
rotary_dim=_HEAD_DIM,
|
|
interleaved=True,
|
|
)
|
|
|
|
expected_q, expected_k = native()
|
|
actual_q, actual_k = fused()
|
|
errors = {
|
|
"q_max_abs": (actual_q.float() - expected_q.float()).abs().max().item(),
|
|
"q_mean_abs": (actual_q.float() - expected_q.float()).abs().mean().item(),
|
|
"k_max_abs": (actual_k.float() - expected_k.float()).abs().max().item(),
|
|
"k_mean_abs": (actual_k.float() - expected_k.float()).abs().mean().item(),
|
|
}
|
|
torch.testing.assert_close(actual_q, expected_q, atol=0.0625, rtol=0.02)
|
|
torch.testing.assert_close(actual_k, expected_k, atol=0.0625, rtol=0.02)
|
|
|
|
functions = {"native": native, "fused": fused}
|
|
order = ("fused", "native") if fused_first else ("native", "fused")
|
|
measured = {name: _measure(functions[name], warmup, iters) for name in order}
|
|
native_stats, fused_stats = measured["native"], measured["fused"]
|
|
return {
|
|
"tokens": tokens,
|
|
"native": native_stats,
|
|
"fused_including_rope_pack": fused_stats,
|
|
"median_speedup": native_stats["median_ms"] / fused_stats["median_ms"],
|
|
"errors": errors,
|
|
}
|
|
|
|
|
|
@torch.inference_mode()
|
|
def _run_attention(seq: int, warmup: int, iters: int, *, fused_first: bool = False) -> dict[str, object]:
|
|
from vllm_omni.diffusion.models.mammoth_moda2.mammothmoda2_dit_model import TransformerBlock
|
|
|
|
torch.manual_seed(42)
|
|
block = (
|
|
TransformerBlock(
|
|
2520,
|
|
_Q_HEADS,
|
|
_KV_HEADS,
|
|
multiple_of=256,
|
|
ffn_dim_multiplier=1.0,
|
|
norm_eps=_EPS,
|
|
rope_repeats_pairs=True,
|
|
)
|
|
.to(device="cuda", dtype=torch.bfloat16)
|
|
.eval()
|
|
)
|
|
block.attn.norm_q.weight.normal_(1.0, 0.2)
|
|
block.attn.norm_k.weight.normal_(1.0, 0.2)
|
|
hidden = torch.randn(2, seq, 2520, device="cuda", dtype=torch.bfloat16)
|
|
mask = torch.ones(2, seq, device="cuda", dtype=torch.bool)
|
|
mask[0, seq - min(300, seq // 2) :] = False
|
|
mask[1, seq - min(17, seq // 2) :] = False
|
|
angles = torch.rand(2, seq, _HEAD_DIM // 2, device="cuda") * (2 * torch.pi)
|
|
# The production producer supplies FP32 tables; each example has its own positions.
|
|
rotary = angles.cos().repeat_interleave(2, dim=-1), angles.sin().repeat_interleave(2, dim=-1)
|
|
env = "VLLM_OMNI_FUSED_QK_NORM_ROPE_MIN_TOKENS"
|
|
previous = os.environ.get(env)
|
|
|
|
def run():
|
|
return block.attn(hidden, hidden, attention_mask=mask, image_rotary_emb=rotary)
|
|
|
|
try:
|
|
outputs = {}
|
|
timings = {}
|
|
order = ("fused", "native") if fused_first else ("native", "fused")
|
|
for name in order:
|
|
os.environ[env] = "0" if name == "fused" else str(2 * seq + 1)
|
|
outputs[name] = run()
|
|
timings[name] = _measure(run, warmup, iters)
|
|
diff = (outputs["native"].float() - outputs["fused"].float()).abs()[mask]
|
|
max_diff, mean_diff = diff.max().item(), diff.mean().item()
|
|
assert max_diff < 2e-2 and mean_diff < 1e-3, (max_diff, mean_diff)
|
|
assert torch.count_nonzero(outputs["fused"][~mask]) == 0
|
|
return {
|
|
"sequence": seq,
|
|
"batch": 2,
|
|
"backend": block.attn.omni_attn.attn_backend.__name__,
|
|
"native": timings["native"],
|
|
"fused": timings["fused"],
|
|
"median_speedup": timings["native"]["median_ms"] / timings["fused"]["median_ms"],
|
|
"max_abs_diff": max_diff,
|
|
"mean_abs_diff": mean_diff,
|
|
}
|
|
finally:
|
|
if previous is None:
|
|
os.environ.pop(env, None)
|
|
else:
|
|
os.environ[env] = previous
|
|
|
|
|
|
def main() -> None:
|
|
args = _parse_args()
|
|
if (
|
|
args.rounds < 1
|
|
or args.iters < 1
|
|
or args.warmup < 0
|
|
or any(value < 1 for value in [*args.tokens, *args.attention_sequences])
|
|
):
|
|
raise ValueError("rounds, iters and sequence lengths must be positive; warmup must be nonnegative")
|
|
if not torch.cuda.is_available():
|
|
raise RuntimeError("This benchmark requires CUDA")
|
|
|
|
probe_q = torch.empty(1, _Q_HEADS, _HEAD_DIM, device="cuda", dtype=torch.bfloat16)
|
|
probe_k = torch.empty(1, _KV_HEADS, _HEAD_DIM, device="cuda", dtype=torch.bfloat16)
|
|
if not _fused_cuda_supported(probe_q, probe_k, _HEAD_DIM, _HEAD_DIM, interleaved=True):
|
|
raise RuntimeError("The fused CUDA QK norm/RoPE path is unavailable")
|
|
|
|
result = {
|
|
"environment": {
|
|
"gpu": torch.cuda.get_device_name(),
|
|
"torch": torch.__version__,
|
|
"cuda": torch.version.cuda,
|
|
"triton": _version("triton"),
|
|
"vllm": _version("vllm"),
|
|
"vllm_omni": _version("vllm-omni"),
|
|
},
|
|
"config": {
|
|
"dtype": "bfloat16",
|
|
"q_heads": _Q_HEADS,
|
|
"kv_heads": _KV_HEADS,
|
|
"head_dim": _HEAD_DIM,
|
|
"warmup": args.warmup,
|
|
"iters": args.iters,
|
|
"rounds": args.rounds,
|
|
"inference_mode": True,
|
|
"alternating_order": True,
|
|
"norm_weights": "normal(mean=1.0, std=0.2)",
|
|
},
|
|
"results": [
|
|
{"round": round_id, **_run_shape(tokens, args.warmup, args.iters, fused_first=bool(round_id % 2))}
|
|
for round_id in range(args.rounds)
|
|
for tokens in args.tokens
|
|
],
|
|
"attention_results": [
|
|
{"round": round_id, **_run_attention(seq, args.warmup, args.iters, fused_first=bool(round_id % 2))}
|
|
for round_id in range(args.rounds)
|
|
for seq in args.attention_sequences
|
|
],
|
|
}
|
|
print(json.dumps(result, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|