mxflashattn 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchmarks/__init__.py +1 -0
- benchmarks/compare.py +105 -0
- benchmarks/configs/c500.yaml +10 -0
- benchmarks/fair_matrix.py +191 -0
- benchmarks/fair_report.py +71 -0
- benchmarks/model_fair_report.py +60 -0
- benchmarks/plot_summary.py +119 -0
- benchmarks/report.py +139 -0
- benchmarks/run.py +328 -0
- integrations/__init__.py +1 -0
- integrations/vllm/README.md +7 -0
- integrations/vllm/__init__.py +1 -0
- integrations/vllm/backend.py +158 -0
- integrations/vllm/compat.py +46 -0
- integrations/vllm/config.py +11 -0
- integrations/vllm/decoder_adapter.py +286 -0
- integrations/vllm/probe_compat.py +22 -0
- integrations/vllm/probe_decoder_layout.py +91 -0
- integrations/vllm/probe_registration.py +24 -0
- integrations/vllm/probe_v017.py +55 -0
- integrations/vllm/smoke_generate.py +190 -0
- integrations/vllm/vllm_plugin.py +511 -0
- mxflashattn/__init__.py +30 -0
- mxflashattn/accuracy.py +51 -0
- mxflashattn/api.py +286 -0
- mxflashattn/dispatch.py +172 -0
- mxflashattn/policy.py +111 -0
- mxflashattn/prefill.py +108 -0
- mxflashattn/reference.py +139 -0
- mxflashattn/validation.py +100 -0
- mxflashattn-1.4.0.dist-info/METADATA +242 -0
- mxflashattn-1.4.0.dist-info/RECORD +35 -0
- mxflashattn-1.4.0.dist-info/WHEEL +5 -0
- mxflashattn-1.4.0.dist-info/licenses/LICENSE +201 -0
- mxflashattn-1.4.0.dist-info/top_level.txt +3 -0
benchmarks/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Reproducible MXFlashAttn benchmark tools."""
|
benchmarks/compare.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
ENVIRONMENT_KEYS = (
|
|
9
|
+
"device",
|
|
10
|
+
"device_name",
|
|
11
|
+
"device_total_memory_bytes",
|
|
12
|
+
"torch",
|
|
13
|
+
"runtime",
|
|
14
|
+
"driver",
|
|
15
|
+
"flash_attn",
|
|
16
|
+
"vllm",
|
|
17
|
+
"warmup",
|
|
18
|
+
"repeats",
|
|
19
|
+
"seed",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def compare_runs(
|
|
24
|
+
baseline: dict,
|
|
25
|
+
candidate: dict,
|
|
26
|
+
*,
|
|
27
|
+
allow_mismatched_environment: bool = False,
|
|
28
|
+
) -> list[dict]:
|
|
29
|
+
baseline_metadata = baseline.get("metadata", {})
|
|
30
|
+
candidate_metadata = candidate.get("metadata", {})
|
|
31
|
+
mismatched = [
|
|
32
|
+
key
|
|
33
|
+
for key in ENVIRONMENT_KEYS
|
|
34
|
+
if (key in baseline_metadata or key in candidate_metadata)
|
|
35
|
+
and baseline_metadata.get(key) != candidate_metadata.get(key)
|
|
36
|
+
]
|
|
37
|
+
if mismatched and not allow_mismatched_environment:
|
|
38
|
+
raise ValueError(f"benchmark environments differ for: {', '.join(mismatched)}")
|
|
39
|
+
|
|
40
|
+
baseline_cases = {case["case_id"]: case for case in baseline.get("cases", [])}
|
|
41
|
+
candidate_cases = {case["case_id"]: case for case in candidate.get("cases", [])}
|
|
42
|
+
if not baseline_cases:
|
|
43
|
+
raise ValueError("baseline run contains no cases")
|
|
44
|
+
|
|
45
|
+
results = []
|
|
46
|
+
for case_id, base in baseline_cases.items():
|
|
47
|
+
if case_id not in candidate_cases:
|
|
48
|
+
raise ValueError(f"missing candidate case {case_id}")
|
|
49
|
+
candidate_case = candidate_cases[case_id]
|
|
50
|
+
base_latency = float(base["latency_ms"])
|
|
51
|
+
candidate_latency = float(candidate_case["latency_ms"])
|
|
52
|
+
if base_latency <= 0 or candidate_latency <= 0:
|
|
53
|
+
raise ValueError(f"latency must be positive for case {case_id}")
|
|
54
|
+
results.append(
|
|
55
|
+
{
|
|
56
|
+
"case_id": case_id,
|
|
57
|
+
"baseline_latency_ms": base_latency,
|
|
58
|
+
"latency_ms": candidate_latency,
|
|
59
|
+
"speedup": base_latency / candidate_latency,
|
|
60
|
+
"latency_reduction_pct": (base_latency - candidate_latency) / base_latency * 100,
|
|
61
|
+
"max_abs_error": candidate_case.get("max_abs_error"),
|
|
62
|
+
"backend": candidate_case.get("backend"),
|
|
63
|
+
"fallback_reason": candidate_case.get("fallback_reason"),
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
extra = candidate_cases.keys() - baseline_cases.keys()
|
|
67
|
+
if extra:
|
|
68
|
+
raise ValueError(f"candidate contains unmatched cases: {', '.join(sorted(extra))}")
|
|
69
|
+
return results
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main() -> None:
|
|
73
|
+
parser = argparse.ArgumentParser(description="Compare baseline and candidate benchmark JSON")
|
|
74
|
+
parser.add_argument("baseline", type=Path)
|
|
75
|
+
parser.add_argument("candidate", type=Path)
|
|
76
|
+
parser.add_argument("--output", type=Path, default=Path("artifacts/comparison.json"))
|
|
77
|
+
parser.add_argument("--allow-mismatched-environment", action="store_true")
|
|
78
|
+
args = parser.parse_args()
|
|
79
|
+
baseline = json.loads(args.baseline.read_text(encoding="utf-8"))
|
|
80
|
+
candidate = json.loads(args.candidate.read_text(encoding="utf-8"))
|
|
81
|
+
result = compare_runs(
|
|
82
|
+
baseline,
|
|
83
|
+
candidate,
|
|
84
|
+
allow_mismatched_environment=args.allow_mismatched_environment,
|
|
85
|
+
)
|
|
86
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
args.output.write_text(
|
|
88
|
+
json.dumps(
|
|
89
|
+
{
|
|
90
|
+
"metadata": {
|
|
91
|
+
"baseline": baseline.get("metadata", {}),
|
|
92
|
+
"candidate": candidate.get("metadata", {}),
|
|
93
|
+
"allow_mismatched_environment": args.allow_mismatched_environment,
|
|
94
|
+
},
|
|
95
|
+
"cases": result,
|
|
96
|
+
},
|
|
97
|
+
indent=2,
|
|
98
|
+
),
|
|
99
|
+
encoding="utf-8",
|
|
100
|
+
)
|
|
101
|
+
print(f"Wrote {len(result)} comparisons to {args.output}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Run a same-input six-column MXFlashAttn benchmark matrix.
|
|
2
|
+
|
|
3
|
+
The operator columns are measured in one process on one device. The vLLM
|
|
4
|
+
columns are imported from separately captured model runs and are never used to
|
|
5
|
+
inflate the operator speedup numbers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import importlib.metadata
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
import statistics
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import torch
|
|
20
|
+
import torch.nn.functional as F
|
|
21
|
+
|
|
22
|
+
from mxflashattn import flash_attn_func, flash_attn_varlen_func, flash_attn_with_kvcache
|
|
23
|
+
from mxflashattn import reference
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _sync(device: torch.device) -> None:
|
|
27
|
+
module = getattr(torch, device.type, None)
|
|
28
|
+
sync = getattr(module, "synchronize", None)
|
|
29
|
+
if sync:
|
|
30
|
+
sync(device)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _measure(fn, device: torch.device, repeats: int) -> tuple[float, torch.Tensor]:
|
|
34
|
+
result = fn()
|
|
35
|
+
_sync(device)
|
|
36
|
+
samples = []
|
|
37
|
+
for _ in range(repeats):
|
|
38
|
+
start = time.perf_counter()
|
|
39
|
+
result = fn()
|
|
40
|
+
_sync(device)
|
|
41
|
+
samples.append((time.perf_counter() - start) * 1000)
|
|
42
|
+
return statistics.median(samples), result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _make_cache(k: torch.Tensor, v: torch.Tensor, page_size: int = 16):
|
|
46
|
+
batch, kv_len, kv_heads, head_dim = k.shape
|
|
47
|
+
pages = (kv_len + page_size - 1) // page_size
|
|
48
|
+
cache_k = torch.zeros(batch * pages, page_size, kv_heads, head_dim, device=k.device, dtype=k.dtype)
|
|
49
|
+
cache_v = torch.zeros_like(cache_k)
|
|
50
|
+
table = torch.arange(batch * pages, device=k.device, dtype=torch.int32).reshape(batch, pages)
|
|
51
|
+
for b in range(batch):
|
|
52
|
+
for p in range(pages):
|
|
53
|
+
start = p * page_size
|
|
54
|
+
count = min(page_size, kv_len - start)
|
|
55
|
+
cache_k[int(table[b, p]), :count] = k[b, start : start + count]
|
|
56
|
+
cache_v[int(table[b, p]), :count] = v[b, start : start + count]
|
|
57
|
+
lengths = torch.full((batch,), kv_len, device=k.device, dtype=torch.int32)
|
|
58
|
+
return cache_k, cache_v, table, lengths
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _dense_sdpa(q, k, v, causal):
|
|
62
|
+
ratio = q.shape[2] // k.shape[2]
|
|
63
|
+
kh = k.transpose(1, 2).repeat_interleave(ratio, dim=1)
|
|
64
|
+
vh = v.transpose(1, 2).repeat_interleave(ratio, dim=1)
|
|
65
|
+
return F.scaled_dot_product_attention(
|
|
66
|
+
q.transpose(1, 2), kh, vh, is_causal=causal, scale=1.0 / math.sqrt(q.shape[-1])
|
|
67
|
+
).transpose(1, 2)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _case(index: int) -> dict:
|
|
71
|
+
# 36 deterministic combinations cover decode/prefill, both layouts,
|
|
72
|
+
# FP16/BF16, GQA/MQA, two head sizes, and causal/non-causal attention.
|
|
73
|
+
return {
|
|
74
|
+
"case_id": f"case-{index:02d}",
|
|
75
|
+
"batch_size": 1 + (index % 2),
|
|
76
|
+
"q_len": (1, 8)[(index // 2) % 2],
|
|
77
|
+
"kv_len": (64, 128)[(index // 4) % 2],
|
|
78
|
+
"head_dim": (64, 128)[(index // 8) % 2],
|
|
79
|
+
"gqa_ratio": (1, 4)[(index // 16) % 2],
|
|
80
|
+
"dtype": ("float16", "bfloat16")[(index // 32) % 2],
|
|
81
|
+
"causal": bool(index % 3),
|
|
82
|
+
"layout": ("dense", "varlen", "paged")[index % 3],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _run_case(case: dict, device: torch.device, repeats: int, vendor_dense, vendor_varlen, vendor_paged):
|
|
87
|
+
dtype = getattr(torch, case["dtype"])
|
|
88
|
+
b, q_len, kv_len, dim = case["batch_size"], case["q_len"], case["kv_len"], case["head_dim"]
|
|
89
|
+
kv_heads = 2
|
|
90
|
+
q_heads = kv_heads * case["gqa_ratio"]
|
|
91
|
+
generator = torch.Generator(device="cpu").manual_seed(20261005 + int(case["case_id"][-2:]))
|
|
92
|
+
q = torch.randn(b, q_len, q_heads, dim, dtype=dtype, generator=generator).to(device)
|
|
93
|
+
k = torch.randn(b, kv_len, kv_heads, dim, dtype=dtype, generator=generator).to(device)
|
|
94
|
+
v = torch.randn_like(k)
|
|
95
|
+
causal = case["causal"]
|
|
96
|
+
cache_k, cache_v, table, lengths = _make_cache(k, v)
|
|
97
|
+
|
|
98
|
+
def reference_run():
|
|
99
|
+
return reference.attention_batched(q, k, v, causal=causal)
|
|
100
|
+
|
|
101
|
+
def sdpa_run():
|
|
102
|
+
return _dense_sdpa(q, k, v, causal)
|
|
103
|
+
|
|
104
|
+
def mx_run():
|
|
105
|
+
if case["layout"] == "dense":
|
|
106
|
+
return flash_attn_func(q, k, v, causal=causal)
|
|
107
|
+
if case["layout"] == "varlen":
|
|
108
|
+
cu_q = torch.arange(0, (b + 1) * q_len, q_len, dtype=torch.int32, device=device)
|
|
109
|
+
cu_k = torch.arange(0, (b + 1) * kv_len, kv_len, dtype=torch.int32, device=device)
|
|
110
|
+
return flash_attn_varlen_func(q.reshape(-1, q_heads, dim), k.reshape(-1, kv_heads, dim), v.reshape(-1, kv_heads, dim), cu_q, cu_k, max_seqlen_q=q_len, max_seqlen_k=kv_len, causal=causal).reshape_as(q)
|
|
111
|
+
return flash_attn_with_kvcache(q, cache_k, cache_v, cache_seqlens=lengths, block_table=table, causal=causal)
|
|
112
|
+
|
|
113
|
+
def vendor_run():
|
|
114
|
+
if case["layout"] == "dense":
|
|
115
|
+
return vendor_dense(q, k, v, causal=causal)
|
|
116
|
+
if case["layout"] == "varlen":
|
|
117
|
+
cu_q = torch.arange(0, (b + 1) * q_len, q_len, dtype=torch.int32, device=device)
|
|
118
|
+
cu_k = torch.arange(0, (b + 1) * kv_len, kv_len, dtype=torch.int32, device=device)
|
|
119
|
+
return vendor_varlen(q.reshape(-1, q_heads, dim), k.reshape(-1, kv_heads, dim), v.reshape(-1, kv_heads, dim), cu_q, cu_k, max_seqlen_q=q_len, max_seqlen_k=kv_len, causal=causal).reshape_as(q)
|
|
120
|
+
return vendor_paged(q, cache_k, cache_v, cache_seqlens=lengths, block_table=table, causal=causal)
|
|
121
|
+
|
|
122
|
+
funcs = {"reference": reference_run, "torch_sdpa": sdpa_run, "mxflashattn_api": mx_run, "metax_vendor": vendor_run}
|
|
123
|
+
outputs, timings = {}, {}
|
|
124
|
+
for name, fn in funcs.items():
|
|
125
|
+
try:
|
|
126
|
+
elapsed, output = _measure(fn, device, repeats)
|
|
127
|
+
timings[name] = elapsed
|
|
128
|
+
outputs[name] = output.detach().float().cpu()
|
|
129
|
+
except Exception as exc:
|
|
130
|
+
timings[name] = None
|
|
131
|
+
outputs[name] = None
|
|
132
|
+
case.setdefault("errors", {})[name] = f"{type(exc).__name__}: {exc}"
|
|
133
|
+
base = outputs["reference"]
|
|
134
|
+
errors = {}
|
|
135
|
+
if base is not None:
|
|
136
|
+
for name, output in outputs.items():
|
|
137
|
+
if output is not None:
|
|
138
|
+
delta = (output - base).abs()
|
|
139
|
+
errors[name] = {"max_abs": float(delta.max()), "max_rel": float((delta / base.abs().clamp_min(1e-6)).max())}
|
|
140
|
+
case["timings_ms"] = timings
|
|
141
|
+
case["errors_vs_reference"] = errors
|
|
142
|
+
case["tokens"] = b * q_len
|
|
143
|
+
return case
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def run_matrix(device: torch.device, repeats: int, count: int) -> dict:
|
|
147
|
+
try:
|
|
148
|
+
vendor = __import__("flash_attn", fromlist=["flash_attn_func", "flash_attn_varlen_func", "flash_attn_with_kvcache"])
|
|
149
|
+
vendor_dense = vendor.flash_attn_func
|
|
150
|
+
vendor_varlen = vendor.flash_attn_varlen_func
|
|
151
|
+
vendor_paged = vendor.flash_attn_with_kvcache
|
|
152
|
+
except (ImportError, AttributeError) as exc:
|
|
153
|
+
vendor_dense = vendor_varlen = vendor_paged = None
|
|
154
|
+
vendor_error = f"{type(exc).__name__}: {exc}"
|
|
155
|
+
else:
|
|
156
|
+
vendor_error = None
|
|
157
|
+
cases = []
|
|
158
|
+
for index in range(count):
|
|
159
|
+
case = _case(index)
|
|
160
|
+
if vendor_dense is None:
|
|
161
|
+
case["errors"] = {"metax_vendor": vendor_error}
|
|
162
|
+
case["timings_ms"] = {name: None for name in ("reference", "torch_sdpa", "mxflashattn_api", "metax_vendor")}
|
|
163
|
+
cases.append(case)
|
|
164
|
+
else:
|
|
165
|
+
cases.append(_run_case(case, device, repeats, vendor_dense, vendor_varlen, vendor_paged))
|
|
166
|
+
metadata = {"device": str(device), "torch": torch.__version__, "vllm": _version("vllm"), "flash_attn": _version("flash-attn"), "vllm_metax": _version("vllm_metax"), "mxmaca": os.getenv("MXMACA_VERSION", "unknown"), "driver": os.getenv("MXMACA_DRIVER_VERSION", "unknown"), "seed": 20261005, "repeats": repeats, "case_count": len(cases), "vendor_error": vendor_error}
|
|
167
|
+
return {"schema_version": 1, "metadata": metadata, "cases": cases}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _version(name: str):
|
|
171
|
+
try:
|
|
172
|
+
return importlib.metadata.version(name)
|
|
173
|
+
except importlib.metadata.PackageNotFoundError:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def main():
|
|
178
|
+
parser = argparse.ArgumentParser()
|
|
179
|
+
parser.add_argument("--device", default="cpu")
|
|
180
|
+
parser.add_argument("--count", type=int, default=36)
|
|
181
|
+
parser.add_argument("--repeats", type=int, default=5)
|
|
182
|
+
parser.add_argument("--output", type=Path, default=Path("artifacts/fair-matrix.json"))
|
|
183
|
+
args = parser.parse_args()
|
|
184
|
+
result = run_matrix(torch.device(args.device), args.repeats, args.count)
|
|
185
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
args.output.write_text(json.dumps(result, indent=2), encoding="utf-8")
|
|
187
|
+
print(f"Wrote {len(result['cases'])} fair benchmark cases to {args.output}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import statistics
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _values(cases, method):
|
|
10
|
+
return [float(c["timings_ms"][method]) for c in cases if c.get("timings_ms", {}).get(method) is not None]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render(matrix: dict, vendor_model: dict | None, candidate_model: dict | None) -> str:
|
|
14
|
+
cases = matrix["cases"]
|
|
15
|
+
methods = ("reference", "torch_sdpa", "metax_vendor", "mxflashattn_api")
|
|
16
|
+
lines = ["# v0.9 Fair Benchmark Report", "", "The operator matrix uses the same generated tensors for every method.", "Reference speedup and vendor speedup are reported separately.", "", f"- Cases: {len(cases)}", f"- Device: {matrix['metadata'].get('device')} / {matrix['metadata'].get('torch')}", f"- MetaX flash-attn: {matrix['metadata'].get('flash_attn')}", ""]
|
|
17
|
+
for method in methods:
|
|
18
|
+
values = _values(cases, method)
|
|
19
|
+
lines.append(f"- Median {method}: {statistics.median(values):.6f} ms" if values else f"- Median {method}: unavailable")
|
|
20
|
+
ref = _values(cases, "reference")
|
|
21
|
+
mx = _values(cases, "mxflashattn_api")
|
|
22
|
+
vendor_times = _values(cases, "metax_vendor")
|
|
23
|
+
if ref and mx:
|
|
24
|
+
lines.append(f"- Median reduction vs PyTorch reference: {(1 - statistics.median(mx) / statistics.median(ref)) * 100:.2f}%")
|
|
25
|
+
if vendor_times and mx:
|
|
26
|
+
lines.append(f"- Median reduction vs MetaX vendor: {(1 - statistics.median(mx) / statistics.median(vendor_times)) * 100:.2f}%")
|
|
27
|
+
lines.extend(["", "## Operator Matrix", "", "| Case | Layout | Dtype | Batch | Q | KV | Dim | GQA | Causal | Reference ms | SDPA ms | MetaX vendor ms | MXFlashAttn ms | MX abs error | Vendor abs error |", "|---|---|---:|---:|---:|---:|---:|---:|:---:|---:|---:|---:|---:|---:|---:|---:|"])
|
|
28
|
+
for c in cases:
|
|
29
|
+
t = c.get("timings_ms", {})
|
|
30
|
+
e = c.get("errors_vs_reference", {})
|
|
31
|
+
lines.append("| {case_id} | {layout} | {dtype} | {batch_size} | {q_len} | {kv_len} | {head_dim} | {gqa_ratio} | {causal} | {reference} | {sdpa} | {vendor} | {mx} | {mxerr} | {verr} |".format(
|
|
32
|
+
**c, reference=_fmt(t.get("reference")), sdpa=_fmt(t.get("torch_sdpa")), vendor=_fmt(t.get("metax_vendor")), mx=_fmt(t.get("mxflashattn_api")), mxerr=_fmt((e.get("mxflashattn_api") or {}).get("max_abs")), verr=_fmt((e.get("metax_vendor") or {}).get("max_abs"))))
|
|
33
|
+
lines.extend(["", "## vLLM Model Evidence", "", "These rows are model-level runs and are intentionally not merged into the operator speedup medians.", "", "| Model evidence | vLLM vendor | vLLM MXFlashAttn candidate |", "|---|---:|---:|"])
|
|
34
|
+
vendor_speed = statistics.median([r.get("tokens_per_second") for r in (vendor_model or {}).get("runs", []) if r.get("tokens_per_second")]) if vendor_model else None
|
|
35
|
+
candidate_speed = statistics.median([r.get("tokens_per_second") for r in (candidate_model or {}).get("runs", []) if r.get("tokens_per_second")]) if candidate_model else None
|
|
36
|
+
lines.append(f"| status | {(vendor_model or {}).get('status', 'unavailable')} | {(candidate_model or {}).get('status', 'unavailable')} |")
|
|
37
|
+
lines.append(f"| median tokens/s | {_fmt(vendor_speed)} | {_fmt(candidate_speed)} |")
|
|
38
|
+
for label, record in (("vendor", vendor_model), ("candidate", candidate_model)):
|
|
39
|
+
if not record:
|
|
40
|
+
lines.append(f"- {label}: unavailable")
|
|
41
|
+
continue
|
|
42
|
+
runs = record.get("runs", [])
|
|
43
|
+
speeds = [r.get("tokens_per_second") for r in runs if r.get("tokens_per_second")]
|
|
44
|
+
speed_text = f"{statistics.median(speeds):.4f}" if speeds else "unavailable"
|
|
45
|
+
lines.append(f"- {label}: status={record.get('status')}, runs={len(runs)}, median tokens/s={speed_text}")
|
|
46
|
+
return "\n".join(lines) + "\n"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _fmt(value):
|
|
50
|
+
return "" if value is None else f"{float(value):.6g}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
parser = argparse.ArgumentParser()
|
|
55
|
+
parser.add_argument("matrix", type=Path)
|
|
56
|
+
parser.add_argument("--vendor-model", type=Path)
|
|
57
|
+
parser.add_argument("--candidate-model", type=Path)
|
|
58
|
+
parser.add_argument("--output", type=Path, default=Path("artifacts/fair-matrix.md"))
|
|
59
|
+
parser.add_argument("--summary", type=Path, default=Path("artifacts/fair-matrix-summary.json"))
|
|
60
|
+
args = parser.parse_args()
|
|
61
|
+
matrix = json.loads(args.matrix.read_text(encoding="utf-8"))
|
|
62
|
+
vendor = json.loads(args.vendor_model.read_text(encoding="utf-8")) if args.vendor_model else None
|
|
63
|
+
candidate = json.loads(args.candidate_model.read_text(encoding="utf-8")) if args.candidate_model else None
|
|
64
|
+
args.output.write_text(render(matrix, vendor, candidate), encoding="utf-8")
|
|
65
|
+
summary = {"case_count": len(matrix["cases"]), "methods": ["pytorch_reference", "torch_sdpa", "metax_vendor", "mxflashattn_api", "vllm_vendor_model", "vllm_candidate_model"], "operator_methods": ["reference", "torch_sdpa", "metax_vendor", "mxflashattn_api"], "vllm_vendor_status": vendor.get("status") if vendor else None, "vllm_candidate_status": candidate.get("status") if candidate else None, "claims": {"reference_speedup_separate": True, "vendor_speedup_verified": False}}
|
|
66
|
+
args.summary.write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
|
67
|
+
print(f"Wrote {len(matrix['cases'])} cases to {args.output}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Pair same-prompt vLLM vendor and candidate model runs."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import statistics
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build(vendor: dict, candidate: dict) -> dict:
|
|
11
|
+
if len(vendor.get("runs", [])) != len(candidate.get("runs", [])):
|
|
12
|
+
raise ValueError("vendor and candidate run counts differ")
|
|
13
|
+
rows = []
|
|
14
|
+
for left, right in zip(vendor["runs"], candidate["runs"]):
|
|
15
|
+
if left.get("prompt") != right.get("prompt"):
|
|
16
|
+
raise ValueError(f"prompt mismatch at index {left.get('index')}")
|
|
17
|
+
rows.append({
|
|
18
|
+
"index": left.get("index"),
|
|
19
|
+
"prompt": left.get("prompt"),
|
|
20
|
+
"prompt_tokens": left.get("prompt_tokens"),
|
|
21
|
+
"vendor_tokens_per_second": left.get("tokens_per_second"),
|
|
22
|
+
"candidate_tokens_per_second": right.get("tokens_per_second"),
|
|
23
|
+
"candidate_dispatch_verified": any(e.get("path") == "mxflashattn_decoder" for e in right.get("dispatch_events", [])),
|
|
24
|
+
"vendor_generated_text": left.get("generated_text"),
|
|
25
|
+
"candidate_generated_text": right.get("generated_text"),
|
|
26
|
+
})
|
|
27
|
+
vendor_speed = [r["vendor_tokens_per_second"] for r in rows if r["vendor_tokens_per_second"]]
|
|
28
|
+
candidate_speed = [r["candidate_tokens_per_second"] for r in rows if r["candidate_tokens_per_second"]]
|
|
29
|
+
return {
|
|
30
|
+
"schema_version": 1,
|
|
31
|
+
"kind": "vllm_same_prompt_model_matrix",
|
|
32
|
+
"case_count": len(rows),
|
|
33
|
+
"same_prompt": True,
|
|
34
|
+
"vendor_status": vendor.get("status"),
|
|
35
|
+
"candidate_status": candidate.get("status"),
|
|
36
|
+
"candidate_dispatch_verified": candidate.get("candidate_dispatch_verified", False),
|
|
37
|
+
"vendor_median_tokens_per_second": statistics.median(vendor_speed) if vendor_speed else None,
|
|
38
|
+
"candidate_median_tokens_per_second": statistics.median(candidate_speed) if candidate_speed else None,
|
|
39
|
+
"rows": rows,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main() -> None:
|
|
44
|
+
parser = argparse.ArgumentParser()
|
|
45
|
+
parser.add_argument("vendor", type=Path)
|
|
46
|
+
parser.add_argument("candidate", type=Path)
|
|
47
|
+
parser.add_argument("--json", type=Path, default=Path("artifacts/vllm-model-matrix-c500.json"))
|
|
48
|
+
parser.add_argument("--markdown", type=Path, default=Path("artifacts/vllm-model-matrix-c500.md"))
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
result = build(json.loads(args.vendor.read_text()), json.loads(args.candidate.read_text()))
|
|
51
|
+
args.json.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
|
|
52
|
+
lines = ["# vLLM Same-Prompt Model Matrix", "", "This is model-level evidence; it is not merged into operator medians.", "", f"- Cases: {result['case_count']}", f"- Candidate dispatch verified: {result['candidate_dispatch_verified']}", f"- Vendor median tokens/s: {result['vendor_median_tokens_per_second']:.4f}", f"- Candidate median tokens/s: {result['candidate_median_tokens_per_second']:.4f}", "", "| Case | Prompt tokens | Vendor tokens/s | Candidate tokens/s | Candidate dispatch |", "|---:|---:|---:|---:|:---:|"]
|
|
53
|
+
for row in result["rows"]:
|
|
54
|
+
lines.append(f"| {row['index']} | {row['prompt_tokens']} | {row['vendor_tokens_per_second']:.4f} | {row['candidate_tokens_per_second']:.4f} | {row['candidate_dispatch_verified']} |")
|
|
55
|
+
args.markdown.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
56
|
+
print(f"Wrote {result['case_count']} same-prompt model cases")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from statistics import median
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
LAYOUTS = ("dense", "varlen", "paged")
|
|
10
|
+
QUERY_LENGTHS = (1, 64)
|
|
11
|
+
COLORS = {"dense": "#0072B2", "varlen": "#009E73", "paged": "#D55E00"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def summarize_matrix(run: dict) -> list[dict]:
|
|
15
|
+
groups: dict[tuple[int, str], list[float]] = {}
|
|
16
|
+
for case in run.get("cases", []):
|
|
17
|
+
if case.get("fallback_reason") or case.get("backend") == "reference":
|
|
18
|
+
continue
|
|
19
|
+
baseline = case.get("baseline_latency_ms")
|
|
20
|
+
latency = case.get("latency_ms")
|
|
21
|
+
key = (case.get("q_len"), case.get("layout"))
|
|
22
|
+
if (
|
|
23
|
+
baseline is None
|
|
24
|
+
or latency is None
|
|
25
|
+
or baseline <= 0
|
|
26
|
+
or key[0] not in QUERY_LENGTHS
|
|
27
|
+
or key[1] not in LAYOUTS
|
|
28
|
+
):
|
|
29
|
+
continue
|
|
30
|
+
reduction = (float(baseline) - float(latency)) / float(baseline) * 100
|
|
31
|
+
groups.setdefault(key, []).append(reduction)
|
|
32
|
+
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
"q_len": q_len,
|
|
36
|
+
"layout": layout,
|
|
37
|
+
"median_reduction_pct": median(values),
|
|
38
|
+
"target_cases": sum(value >= 20 for value in values),
|
|
39
|
+
"case_count": len(values),
|
|
40
|
+
}
|
|
41
|
+
for q_len in QUERY_LENGTHS
|
|
42
|
+
for layout in LAYOUTS
|
|
43
|
+
if (values := groups.get((q_len, layout)))
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def render_plot(run: dict, output: Path) -> None:
|
|
48
|
+
try:
|
|
49
|
+
import matplotlib.pyplot as plt
|
|
50
|
+
except ImportError as error:
|
|
51
|
+
raise RuntimeError("install mxflashattn[reports] to generate the benchmark chart") from error
|
|
52
|
+
|
|
53
|
+
summary = summarize_matrix(run)
|
|
54
|
+
if not summary:
|
|
55
|
+
raise ValueError("benchmark run contains no comparable measurements")
|
|
56
|
+
|
|
57
|
+
metadata = run.get("metadata", {})
|
|
58
|
+
figure, axis = plt.subplots(figsize=(12, 6.5))
|
|
59
|
+
centers = (0.0, 1.7)
|
|
60
|
+
width = 0.28
|
|
61
|
+
offsets = {layout: (index - 1) * width for index, layout in enumerate(LAYOUTS)}
|
|
62
|
+
for layout in LAYOUTS:
|
|
63
|
+
for q_len, center in zip(QUERY_LENGTHS, centers):
|
|
64
|
+
item = next(
|
|
65
|
+
(row for row in summary if row["q_len"] == q_len and row["layout"] == layout),
|
|
66
|
+
None,
|
|
67
|
+
)
|
|
68
|
+
if item is None:
|
|
69
|
+
continue
|
|
70
|
+
x = center + offsets[layout]
|
|
71
|
+
label = layout if q_len == QUERY_LENGTHS[0] else "_nolegend_"
|
|
72
|
+
bar = axis.bar(x, item["median_reduction_pct"], width, color=COLORS[layout], label=label)
|
|
73
|
+
axis.annotate(
|
|
74
|
+
f"{item['median_reduction_pct']:.0f}%\n{item['target_cases']}/{item['case_count']}",
|
|
75
|
+
(bar[0].get_x() + bar[0].get_width() / 2, bar[0].get_height()),
|
|
76
|
+
xytext=(0, 5),
|
|
77
|
+
textcoords="offset points",
|
|
78
|
+
ha="center",
|
|
79
|
+
va="bottom",
|
|
80
|
+
fontsize=9,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
axis.axhline(20, color="#565D64", linestyle="--", linewidth=1.2, label="20% target")
|
|
84
|
+
axis.set_xticks(centers, ["Decode (q=1)", "Prefill (q=64)"])
|
|
85
|
+
axis.set_ylabel("Median latency reduction vs PyTorch reference (%)")
|
|
86
|
+
axis.set_title("MXFlashAttn C500 Operator Benchmark")
|
|
87
|
+
axis.grid(axis="y", color="#D9DEE3", linewidth=0.7)
|
|
88
|
+
axis.set_axisbelow(True)
|
|
89
|
+
axis.spines[["top", "right"]].set_visible(False)
|
|
90
|
+
axis.legend(ncols=4, loc="upper right", frameon=False)
|
|
91
|
+
upper = max(75, max(item["median_reduction_pct"] for item in summary) + 15)
|
|
92
|
+
axis.set_ylim(min(-10, min(item["median_reduction_pct"] for item in summary) - 15), upper)
|
|
93
|
+
|
|
94
|
+
run_details = (
|
|
95
|
+
f"{metadata.get('device_name', 'unknown')} | MXMACA {metadata.get('runtime', 'unknown')} | "
|
|
96
|
+
f"PyTorch {metadata.get('torch', 'unknown')} | {len(run.get('cases', []))} cases | "
|
|
97
|
+
f"seed {metadata.get('seed', 'unknown')}, warmup {metadata.get('warmup', 'unknown')}, "
|
|
98
|
+
f"repeats {metadata.get('repeats', 'unknown')}"
|
|
99
|
+
)
|
|
100
|
+
footer = "Labels show median reduction and cases at or above the 20% target.\n" + run_details
|
|
101
|
+
figure.text(0.5, 0.035, footer, ha="center", va="bottom", fontsize=9, color="#41484F")
|
|
102
|
+
figure.tight_layout(rect=(0.02, 0.13, 0.98, 0.98))
|
|
103
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
figure.savefig(output, dpi=180, facecolor="white")
|
|
105
|
+
plt.close(figure)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main() -> None:
|
|
109
|
+
parser = argparse.ArgumentParser(description="Plot summarized benchmark latency reductions")
|
|
110
|
+
parser.add_argument("input", type=Path)
|
|
111
|
+
parser.add_argument("--output", type=Path, default=Path("artifacts/benchmark-summary.png"))
|
|
112
|
+
args = parser.parse_args()
|
|
113
|
+
run = json.loads(args.input.read_text(encoding="utf-8"))
|
|
114
|
+
render_plot(run, args.output)
|
|
115
|
+
print(f"Wrote benchmark chart to {args.output}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
main()
|