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/report.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from statistics import median
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def render_markdown(run: dict) -> str:
|
|
10
|
+
cases = run.get("cases", [])
|
|
11
|
+
metadata = run.get("metadata", {})
|
|
12
|
+
fallbacks = [case for case in cases if case.get("fallback_reason")]
|
|
13
|
+
latencies = [float(case["latency_ms"]) for case in cases if case.get("latency_ms") is not None]
|
|
14
|
+
errors = [float(case["max_abs_error"]) for case in cases if case.get("max_abs_error") is not None]
|
|
15
|
+
relative_errors = [
|
|
16
|
+
float(case["max_relative_error"]) for case in cases if case.get("max_relative_error") is not None
|
|
17
|
+
]
|
|
18
|
+
comparable = [
|
|
19
|
+
case
|
|
20
|
+
for case in cases
|
|
21
|
+
if case.get("latency_ms") is not None and case.get("baseline_latency_ms") is not None
|
|
22
|
+
]
|
|
23
|
+
reductions = [
|
|
24
|
+
(float(case["baseline_latency_ms"]) - float(case["latency_ms"]))
|
|
25
|
+
/ float(case["baseline_latency_ms"])
|
|
26
|
+
* 100
|
|
27
|
+
for case in comparable
|
|
28
|
+
if float(case["baseline_latency_ms"]) > 0
|
|
29
|
+
]
|
|
30
|
+
decode_cases = [case for case in comparable if case.get("q_len") == 1]
|
|
31
|
+
decode_reductions = [
|
|
32
|
+
(float(case["baseline_latency_ms"]) - float(case["latency_ms"]))
|
|
33
|
+
/ float(case["baseline_latency_ms"])
|
|
34
|
+
* 100
|
|
35
|
+
for case in decode_cases
|
|
36
|
+
if float(case["baseline_latency_ms"]) > 0
|
|
37
|
+
]
|
|
38
|
+
decode_target_cases = sum(value >= 20 for value in decode_reductions)
|
|
39
|
+
baseline_memory = [
|
|
40
|
+
int(case["baseline_peak_memory_bytes"])
|
|
41
|
+
for case in cases
|
|
42
|
+
if case.get("baseline_peak_memory_bytes") is not None
|
|
43
|
+
]
|
|
44
|
+
candidate_memory = [
|
|
45
|
+
int(case["peak_memory_bytes"])
|
|
46
|
+
for case in cases
|
|
47
|
+
if case.get("peak_memory_bytes") is not None
|
|
48
|
+
]
|
|
49
|
+
lines = [
|
|
50
|
+
"# MXFlashAttn Benchmark Report",
|
|
51
|
+
"",
|
|
52
|
+
f"- Device: {metadata.get('device_name', metadata.get('device', 'unknown'))}",
|
|
53
|
+
f"- Device total memory: {_memory(metadata.get('device_total_memory_bytes'))}",
|
|
54
|
+
f"- PyTorch: {metadata.get('torch', 'unknown')}",
|
|
55
|
+
f"- MXMACA runtime / driver: {metadata.get('runtime', 'unknown')} / {metadata.get('driver', 'unknown')}",
|
|
56
|
+
f"- FlashAttention / vLLM: {metadata.get('flash_attn', 'unknown')} / {metadata.get('vllm', 'unknown')}",
|
|
57
|
+
f"- Seed / warmup / repeats: {metadata.get('seed', 'unknown')} / {metadata.get('warmup', 'unknown')} / {metadata.get('repeats', 'unknown')}",
|
|
58
|
+
f"- Cases: {len(cases)}",
|
|
59
|
+
f"- Reference fallback cases: {len(fallbacks)}",
|
|
60
|
+
]
|
|
61
|
+
if latencies:
|
|
62
|
+
lines.extend(["", f"- Median attention latency: {median(latencies):.4f} ms"])
|
|
63
|
+
if reductions:
|
|
64
|
+
lines.append(f"- Median latency reduction vs reference: {median(reductions):.2f}%")
|
|
65
|
+
if decode_reductions:
|
|
66
|
+
lines.append(
|
|
67
|
+
f"- Decode (q_len=1) median reduction: {median(decode_reductions):.2f}% "
|
|
68
|
+
f"({decode_target_cases}/{len(decode_reductions)} cases at or above 20%)"
|
|
69
|
+
)
|
|
70
|
+
if errors:
|
|
71
|
+
lines.append(f"- Maximum absolute error: {max(errors):.6g}")
|
|
72
|
+
if relative_errors:
|
|
73
|
+
lines.append(
|
|
74
|
+
f"- Maximum relative error (reference denominator floor 1e-6): {max(relative_errors):.6g}"
|
|
75
|
+
)
|
|
76
|
+
if baseline_memory and candidate_memory:
|
|
77
|
+
lines.append(
|
|
78
|
+
f"- Maximum measured peak memory: {_memory(max(baseline_memory))} reference, "
|
|
79
|
+
f"{_memory(max(candidate_memory))} candidate"
|
|
80
|
+
)
|
|
81
|
+
if not cases or len(fallbacks) == len(cases):
|
|
82
|
+
lines.extend(["", "**Performance target is not verified.** Every measured candidate used the reference fallback."])
|
|
83
|
+
else:
|
|
84
|
+
lines.extend(["", "Native backend measurements are present; per-case results include regressions and improvements."])
|
|
85
|
+
if decode_reductions and decode_target_cases != len(decode_reductions):
|
|
86
|
+
lines.append("The 20% decode target is not met by every measured decode case.")
|
|
87
|
+
lines.extend(
|
|
88
|
+
[
|
|
89
|
+
"",
|
|
90
|
+
"| Case | Layout | Dtype | Backend | Reference (ms) | Candidate (ms) | Reduction | Decode (ms) | Max abs error | Max rel error | Peak memory ref/candidate | Fallback reason |",
|
|
91
|
+
"|---|---|---|---|---:|---:|---:|---:|---:|---:|---|---|",
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
for case in cases:
|
|
95
|
+
baseline_latency = case.get("baseline_latency_ms")
|
|
96
|
+
latency = case.get("latency_ms")
|
|
97
|
+
reduction = None
|
|
98
|
+
if baseline_latency is not None and latency is not None and float(baseline_latency) > 0:
|
|
99
|
+
reduction = (float(baseline_latency) - float(latency)) / float(baseline_latency) * 100
|
|
100
|
+
lines.append(
|
|
101
|
+
"| {case_id} | {layout} | {dtype} | {backend} | {baseline} | {latency} | {reduction} | {decode} | {error} | {relative_error} | {memory} | {fallback} |".format(
|
|
102
|
+
case_id=case.get("case_id", ""),
|
|
103
|
+
layout=case.get("layout", ""),
|
|
104
|
+
dtype=case.get("dtype", ""),
|
|
105
|
+
backend=case.get("backend", ""),
|
|
106
|
+
baseline=_format(baseline_latency),
|
|
107
|
+
latency=_format(case.get("latency_ms")),
|
|
108
|
+
reduction="" if reduction is None else f"{reduction:.2f}%",
|
|
109
|
+
decode=_format(case.get("decode_latency_ms")),
|
|
110
|
+
error=_format(case.get("max_abs_error")),
|
|
111
|
+
relative_error=_format(case.get("max_relative_error")),
|
|
112
|
+
memory=f"{_memory(case.get('baseline_peak_memory_bytes'))} / {_memory(case.get('peak_memory_bytes'))}",
|
|
113
|
+
fallback=case.get("fallback_reason") or "",
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
return "\n".join(lines) + "\n"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _format(value: object) -> str:
|
|
120
|
+
return "" if value is None else f"{float(value):.6g}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _memory(value: object) -> str:
|
|
124
|
+
return "" if value is None else f"{int(value) / (1024**2):.2f} MiB"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main() -> None:
|
|
128
|
+
parser = argparse.ArgumentParser(description="Render a benchmark JSON report as Markdown")
|
|
129
|
+
parser.add_argument("input", type=Path)
|
|
130
|
+
parser.add_argument("--output", type=Path, default=Path("artifacts/benchmark.md"))
|
|
131
|
+
args = parser.parse_args()
|
|
132
|
+
data = json.loads(args.input.read_text(encoding="utf-8"))
|
|
133
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
args.output.write_text(render_markdown(data), encoding="utf-8")
|
|
135
|
+
print(f"Wrote benchmark report to {args.output}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
main()
|
benchmarks/run.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import importlib.metadata
|
|
5
|
+
import itertools
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import statistics
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import torch
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
from mxflashattn import flash_attn_func, flash_attn_varlen_func, flash_attn_with_kvcache, get_last_dispatch_info
|
|
18
|
+
from mxflashattn import reference
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def expand_cases(config: dict) -> list[dict]:
|
|
22
|
+
matrix = config["matrix"]
|
|
23
|
+
keys = list(matrix)
|
|
24
|
+
return [
|
|
25
|
+
dict(zip(keys, values))
|
|
26
|
+
for values in itertools.product(*(matrix[key] for key in keys))
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _case_id(case: dict) -> str:
|
|
31
|
+
return "_".join(f"{key}-{value}" for key, value in case.items())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _sync(device: torch.device) -> None:
|
|
35
|
+
module_names = {"maca": "maca", "musa": "musa", "cuda": "cuda"}
|
|
36
|
+
module = getattr(torch, module_names.get(device.type, ""), None)
|
|
37
|
+
synchronize = getattr(module, "synchronize", None)
|
|
38
|
+
if synchronize is not None:
|
|
39
|
+
synchronize(device)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _peak_memory(device: torch.device) -> int | None:
|
|
43
|
+
module = getattr(torch, {"maca": "maca", "musa": "musa", "cuda": "cuda"}.get(device.type, ""), None)
|
|
44
|
+
getter = getattr(module, "max_memory_allocated", None)
|
|
45
|
+
if getter is None:
|
|
46
|
+
return None
|
|
47
|
+
try:
|
|
48
|
+
return int(getter(device))
|
|
49
|
+
except (RuntimeError, TypeError):
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _reset_peak_memory(device: torch.device) -> None:
|
|
54
|
+
module = getattr(torch, {"maca": "maca", "musa": "musa", "cuda": "cuda"}.get(device.type, ""), None)
|
|
55
|
+
reset = getattr(module, "reset_peak_memory_stats", None)
|
|
56
|
+
if reset is not None:
|
|
57
|
+
try:
|
|
58
|
+
reset(device)
|
|
59
|
+
except (RuntimeError, TypeError):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _make_inputs(case: dict, device: torch.device):
|
|
64
|
+
dtype = getattr(torch, case["dtype"])
|
|
65
|
+
batch = int(case["batch_size"])
|
|
66
|
+
query_len = int(case["q_len"])
|
|
67
|
+
kv_len = int(case["kv_len"])
|
|
68
|
+
head_dim = int(case["head_dim"])
|
|
69
|
+
kv_heads = 4
|
|
70
|
+
query_heads = kv_heads * int(case["gqa_ratio"])
|
|
71
|
+
q = torch.randn(batch, query_len, query_heads, head_dim, device=device, dtype=dtype)
|
|
72
|
+
k = torch.randn(batch, kv_len, kv_heads, head_dim, device=device, dtype=dtype)
|
|
73
|
+
v = torch.randn_like(k)
|
|
74
|
+
return q, k, v
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _prepare_candidate(case: dict, q, k, v):
|
|
78
|
+
layout = case["layout"]
|
|
79
|
+
causal = bool(case.get("causal", True))
|
|
80
|
+
if layout == "dense":
|
|
81
|
+
return lambda: flash_attn_func(q, k, v, causal=causal)
|
|
82
|
+
if layout == "varlen":
|
|
83
|
+
batch, q_len = q.shape[:2]
|
|
84
|
+
kv_len = k.shape[1]
|
|
85
|
+
cu_q = torch.arange(0, (batch + 1) * q_len, q_len, dtype=torch.int32, device=q.device)
|
|
86
|
+
cu_k = torch.arange(0, (batch + 1) * kv_len, kv_len, dtype=torch.int32, device=q.device)
|
|
87
|
+
flat_q, flat_k, flat_v = (
|
|
88
|
+
q.reshape(-1, *q.shape[2:]),
|
|
89
|
+
k.reshape(-1, *k.shape[2:]),
|
|
90
|
+
v.reshape(-1, *v.shape[2:]),
|
|
91
|
+
)
|
|
92
|
+
return lambda: flash_attn_varlen_func(
|
|
93
|
+
flat_q,
|
|
94
|
+
flat_k,
|
|
95
|
+
flat_v,
|
|
96
|
+
cu_q,
|
|
97
|
+
cu_k,
|
|
98
|
+
max_seqlen_q=q_len,
|
|
99
|
+
max_seqlen_k=kv_len,
|
|
100
|
+
causal=causal,
|
|
101
|
+
)
|
|
102
|
+
if layout == "paged":
|
|
103
|
+
batch, kv_len = k.shape[:2]
|
|
104
|
+
page_size = int(case.get("page_size", 16))
|
|
105
|
+
pages_per_batch = (kv_len + page_size - 1) // page_size
|
|
106
|
+
cache_k = torch.zeros(batch * pages_per_batch, page_size, *k.shape[2:], dtype=k.dtype, device=k.device)
|
|
107
|
+
cache_v = torch.zeros_like(cache_k)
|
|
108
|
+
table = torch.arange(batch * pages_per_batch, dtype=torch.int32, device=k.device).reshape(batch, pages_per_batch)
|
|
109
|
+
for batch_index in range(batch):
|
|
110
|
+
start = batch_index * pages_per_batch
|
|
111
|
+
flat_k = k[batch_index]
|
|
112
|
+
flat_v = v[batch_index]
|
|
113
|
+
for page_index in range(pages_per_batch):
|
|
114
|
+
source_start = page_index * page_size
|
|
115
|
+
count = min(page_size, kv_len - source_start)
|
|
116
|
+
cache_k[start + page_index, :count] = flat_k[source_start : source_start + count]
|
|
117
|
+
cache_v[start + page_index, :count] = flat_v[source_start : source_start + count]
|
|
118
|
+
lengths = torch.full((batch,), kv_len, dtype=torch.int32, device=k.device)
|
|
119
|
+
return lambda: flash_attn_with_kvcache(
|
|
120
|
+
q,
|
|
121
|
+
cache_k,
|
|
122
|
+
cache_v,
|
|
123
|
+
cache_seqlens=lengths,
|
|
124
|
+
block_table=table,
|
|
125
|
+
causal=causal,
|
|
126
|
+
)
|
|
127
|
+
raise ValueError(f"unknown layout: {layout}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _reference(case: dict, q, k, v):
|
|
131
|
+
causal = bool(case.get("causal", True))
|
|
132
|
+
if case["layout"] in ("dense", "paged"):
|
|
133
|
+
return reference.attention_batched(q, k, v, causal=causal)
|
|
134
|
+
batch, q_len = q.shape[:2]
|
|
135
|
+
kv_len = k.shape[1]
|
|
136
|
+
cu_q = [index * q_len for index in range(batch + 1)]
|
|
137
|
+
cu_k = [index * kv_len for index in range(batch + 1)]
|
|
138
|
+
return reference.attention_varlen(q.reshape(-1, *q.shape[2:]), k.reshape(-1, *k.shape[2:]), v.reshape(-1, *v.shape[2:]), cu_q, cu_k, causal=causal)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _measure(fn, warmup: int, repeats: int, device: torch.device) -> tuple[float, object]:
|
|
142
|
+
result = None
|
|
143
|
+
for _ in range(warmup):
|
|
144
|
+
result = fn()
|
|
145
|
+
_sync(device)
|
|
146
|
+
samples = []
|
|
147
|
+
for _ in range(repeats):
|
|
148
|
+
start = time.perf_counter()
|
|
149
|
+
result = fn()
|
|
150
|
+
_sync(device)
|
|
151
|
+
samples.append((time.perf_counter() - start) * 1000)
|
|
152
|
+
return statistics.median(samples), result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def run_benchmarks(
|
|
156
|
+
config: dict,
|
|
157
|
+
*,
|
|
158
|
+
device: torch.device,
|
|
159
|
+
warmup: int,
|
|
160
|
+
repeats: int,
|
|
161
|
+
limit: int | None = None,
|
|
162
|
+
seed: int | None = None,
|
|
163
|
+
) -> dict:
|
|
164
|
+
seed = int(config.get("seed", 0) if seed is None else seed)
|
|
165
|
+
if seed < 0:
|
|
166
|
+
raise ValueError("seed must be non-negative")
|
|
167
|
+
torch.manual_seed(seed)
|
|
168
|
+
cases = expand_cases(config)
|
|
169
|
+
if limit is not None:
|
|
170
|
+
cases = cases[:limit]
|
|
171
|
+
output_cases = []
|
|
172
|
+
for case in cases:
|
|
173
|
+
q, k, v = _make_inputs(case, device)
|
|
174
|
+
_reset_peak_memory(device)
|
|
175
|
+
baseline_latency, baseline_output = _measure(lambda: _reference(case, q, k, v), warmup, repeats, device)
|
|
176
|
+
baseline_peak_memory = _peak_memory(device)
|
|
177
|
+
baseline_output = baseline_output.detach().cpu()
|
|
178
|
+
candidate = _prepare_candidate(case, q, k, v)
|
|
179
|
+
_reset_peak_memory(device)
|
|
180
|
+
candidate_latency, candidate_output = _measure(candidate, warmup, repeats, device)
|
|
181
|
+
dispatch_info = get_last_dispatch_info()
|
|
182
|
+
candidate_output = candidate_output.detach().cpu()
|
|
183
|
+
difference = (candidate_output.float() - baseline_output.float()).abs()
|
|
184
|
+
max_abs_error = float(difference.max().item()) if difference.numel() else 0.0
|
|
185
|
+
denom = baseline_output.float().abs().clamp_min(1e-6)
|
|
186
|
+
max_relative_error = float((difference / denom).max().item()) if difference.numel() else 0.0
|
|
187
|
+
query_tokens = int(q.shape[0] * q.shape[1])
|
|
188
|
+
output_cases.append(
|
|
189
|
+
{
|
|
190
|
+
**case,
|
|
191
|
+
"case_id": _case_id(case),
|
|
192
|
+
"backend": dispatch_info.backend if dispatch_info else "unknown",
|
|
193
|
+
"fallback_reason": dispatch_info.fallback_reason if dispatch_info else None,
|
|
194
|
+
"max_abs_error": max_abs_error,
|
|
195
|
+
"max_relative_error": max_relative_error,
|
|
196
|
+
"baseline_latency_ms": baseline_latency,
|
|
197
|
+
"latency_ms": candidate_latency,
|
|
198
|
+
"attention_tokens_per_second": query_tokens / (candidate_latency / 1000),
|
|
199
|
+
"decode_latency_ms": candidate_latency if q.shape[1] == 1 else None,
|
|
200
|
+
"first_token_latency_ms": None,
|
|
201
|
+
"baseline_peak_memory_bytes": baseline_peak_memory,
|
|
202
|
+
"peak_memory_bytes": _peak_memory(device),
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
return {
|
|
206
|
+
"schema_version": 1,
|
|
207
|
+
"metadata": {
|
|
208
|
+
"device": str(device),
|
|
209
|
+
"device_name": _device_name(device),
|
|
210
|
+
"device_total_memory_bytes": _device_total_memory(device),
|
|
211
|
+
"torch": torch.__version__,
|
|
212
|
+
"runtime": _runtime_version(),
|
|
213
|
+
"driver": _driver_version(),
|
|
214
|
+
"flash_attn": _optional_distribution_version("flash-attn"),
|
|
215
|
+
"vllm": _optional_version("vllm"),
|
|
216
|
+
"warmup": warmup,
|
|
217
|
+
"repeats": repeats,
|
|
218
|
+
"seed": seed,
|
|
219
|
+
"fallback_enabled": os.getenv("MXFLASHATTN_ALLOW_FALLBACK") == "1",
|
|
220
|
+
},
|
|
221
|
+
"cases": output_cases,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _device_name(device: torch.device) -> str:
|
|
226
|
+
if device.type == "cpu":
|
|
227
|
+
return "CPU reference"
|
|
228
|
+
for module_name in (device.type, "cuda"):
|
|
229
|
+
module = getattr(torch, module_name, None)
|
|
230
|
+
getter = getattr(module, "get_device_name", None)
|
|
231
|
+
if getter is not None:
|
|
232
|
+
try:
|
|
233
|
+
return str(getter(device))
|
|
234
|
+
except (RuntimeError, TypeError):
|
|
235
|
+
pass
|
|
236
|
+
return str(device)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _device_total_memory(device: torch.device) -> int | None:
|
|
240
|
+
if device.type == "cpu":
|
|
241
|
+
return None
|
|
242
|
+
for module_name in (device.type, "cuda"):
|
|
243
|
+
module = getattr(torch, module_name, None)
|
|
244
|
+
getter = getattr(module, "get_device_properties", None)
|
|
245
|
+
if getter is not None:
|
|
246
|
+
try:
|
|
247
|
+
return int(getter(device).total_memory)
|
|
248
|
+
except (RuntimeError, TypeError, AttributeError, AssertionError):
|
|
249
|
+
pass
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _runtime_version() -> str | None:
|
|
254
|
+
for name in ("MXMACA_VERSION", "MACA_VERSION", "MUSA_VERSION"):
|
|
255
|
+
if os.getenv(name):
|
|
256
|
+
return os.environ[name]
|
|
257
|
+
return _mx_smi_version(r"MACA Version:\s*([^\s]+)")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _driver_version() -> str | None:
|
|
261
|
+
for name in ("MXMACA_DRIVER_VERSION", "MACA_DRIVER_VERSION", "MUSA_DRIVER_VERSION"):
|
|
262
|
+
if os.getenv(name):
|
|
263
|
+
return os.environ[name]
|
|
264
|
+
return _mx_smi_version(r"Kernel Mode Driver Version:\s*([^\s]+)")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _mx_smi_version(pattern: str) -> str | None:
|
|
268
|
+
try:
|
|
269
|
+
result = subprocess.run(
|
|
270
|
+
["mx-smi"],
|
|
271
|
+
capture_output=True,
|
|
272
|
+
text=True,
|
|
273
|
+
timeout=5,
|
|
274
|
+
check=False,
|
|
275
|
+
)
|
|
276
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
277
|
+
return None
|
|
278
|
+
match = re.search(pattern, result.stdout)
|
|
279
|
+
return match.group(1) if match else None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _optional_version(module_name: str) -> str | None:
|
|
283
|
+
try:
|
|
284
|
+
return importlib.metadata.version(module_name.replace("_", "-"))
|
|
285
|
+
except importlib.metadata.PackageNotFoundError:
|
|
286
|
+
try:
|
|
287
|
+
module = importlib.import_module(module_name)
|
|
288
|
+
except (ImportError, OSError):
|
|
289
|
+
return None
|
|
290
|
+
return str(getattr(module, "__version__", "unknown"))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _optional_distribution_version(distribution: str) -> str | None:
|
|
294
|
+
try:
|
|
295
|
+
return importlib.metadata.version(distribution)
|
|
296
|
+
except importlib.metadata.PackageNotFoundError:
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def main() -> None:
|
|
301
|
+
parser = argparse.ArgumentParser(description="Run MXFlashAttn correctness and latency cases")
|
|
302
|
+
parser.add_argument("--config", type=Path, default=Path("benchmarks/configs/c500.yaml"))
|
|
303
|
+
parser.add_argument("--output", type=Path, default=Path("artifacts/benchmark.json"))
|
|
304
|
+
parser.add_argument("--device", default="cpu")
|
|
305
|
+
parser.add_argument("--warmup", type=int, default=3)
|
|
306
|
+
parser.add_argument("--repeats", type=int, default=10)
|
|
307
|
+
parser.add_argument("--limit", type=int, default=36)
|
|
308
|
+
parser.add_argument("--seed", type=int)
|
|
309
|
+
args = parser.parse_args()
|
|
310
|
+
if args.warmup < 0 or args.repeats < 1 or args.limit < 1:
|
|
311
|
+
parser.error("warmup must be non-negative; repeats and limit must be positive")
|
|
312
|
+
with args.config.open(encoding="utf-8") as stream:
|
|
313
|
+
config = yaml.safe_load(stream)
|
|
314
|
+
result = run_benchmarks(
|
|
315
|
+
config,
|
|
316
|
+
device=torch.device(args.device),
|
|
317
|
+
warmup=args.warmup,
|
|
318
|
+
repeats=args.repeats,
|
|
319
|
+
limit=args.limit,
|
|
320
|
+
seed=args.seed,
|
|
321
|
+
)
|
|
322
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
323
|
+
args.output.write_text(json.dumps(result, indent=2), encoding="utf-8")
|
|
324
|
+
print(f"Wrote {len(result['cases'])} benchmark cases to {args.output}")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
if __name__ == "__main__":
|
|
328
|
+
main()
|
integrations/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Optional upstream framework adapters."""
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# vLLM Integration Status
|
|
2
|
+
|
|
3
|
+
The release candidate targets the checked C500 environment: `vllm 0.17.0` and `vllm_metax 0.17.0+gd10261.d20260409.maca3.5.3.20.torch2.8`. SGLang is not installed in the image, so the alternate integration path remains unevaluated. [`backend.py`](backend.py) contains a callable bridge and an isolated vLLM v1 registration probe for dense `[tokens, heads, head_dim]` Q/K/V tensors.
|
|
4
|
+
|
|
5
|
+
A full integration must target one explicitly pinned and C500-compatible vLLM version. The local v0.5.0 probe reached the vLLM 0.17.0 v1 registration hook, and the candidate encoder-shaped path can call the public MXFlashAttn API. Decoder paged-cache execution remains delegated to vLLM because metadata construction, slot-based KV writes, block-table reads, cascade handling, and the version's `AttentionImpl` contract still require validation. No end-to-end MXFlashAttn acceleration claim is made.
|
|
6
|
+
|
|
7
|
+
The generation script only collects a baseline by default when `--baseline-only` is passed. It refuses to run without that flag because decoder paged-cache execution has not been switched to MXFlashAttn; baseline output explicitly says that it did not exercise this project. The configured quick model is Qwen3-0.6B for the tested vLLM 0.17.0 environment; weights remain outside this repository.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Experimental vLLM integration helpers."""
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""vLLM 0.17.0 registration and dense bridge for the local E2E candidate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
|
|
10
|
+
from mxflashattn import flash_attn_func
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DISPATCH_EVENTS: list[dict[str, str]] = []
|
|
14
|
+
|
|
15
|
+
# Recording runs once per attention call (28 layers x N steps x prompts). The
|
|
16
|
+
# event payloads repeat heavily, so serialisation is cached. Set
|
|
17
|
+
# MXFLASHATTN_DISPATCH_SAMPLE=N to write only every Nth event: the log still
|
|
18
|
+
# shows which backend served the run, but the write cost leaves the hot path.
|
|
19
|
+
_JSON_CACHE: dict[tuple[tuple[str, str], ...], str] = {}
|
|
20
|
+
_SAMPLE_EVERY = int(os.getenv("MXFLASHATTN_DISPATCH_SAMPLE", "0") or 0)
|
|
21
|
+
_event_count = 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_dispatch_events() -> list[dict[str, str]]:
|
|
25
|
+
return list(DISPATCH_EVENTS)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def clear_dispatch_events(truncate_log: bool = True) -> None:
|
|
29
|
+
"""Drop buffered events.
|
|
30
|
+
|
|
31
|
+
``truncate_log=False`` keeps previously written lines, which is what
|
|
32
|
+
multi-prompt suites need: the in-memory window resets per prompt, but the
|
|
33
|
+
on-disk evidence must accumulate across the whole run.
|
|
34
|
+
"""
|
|
35
|
+
DISPATCH_EVENTS.clear()
|
|
36
|
+
if not truncate_log:
|
|
37
|
+
return
|
|
38
|
+
path = os.getenv("MXFLASHATTN_DISPATCH_LOG")
|
|
39
|
+
if path:
|
|
40
|
+
try:
|
|
41
|
+
open(path, "w", encoding="utf-8").close()
|
|
42
|
+
except OSError:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _record_event(event: dict[str, str]) -> None:
|
|
47
|
+
DISPATCH_EVENTS.append(event)
|
|
48
|
+
path = os.getenv("MXFLASHATTN_DISPATCH_LOG")
|
|
49
|
+
# EngineCore workers may import the plugin before the parent-side smoke
|
|
50
|
+
# runner's environment mutation is visible. The plugin log is already
|
|
51
|
+
# worker-visible; derive a sibling dispatch path as a durable fallback.
|
|
52
|
+
if not path:
|
|
53
|
+
plugin_log = os.getenv("MXFLASHATTN_PLUGIN_IMPORT_LOG")
|
|
54
|
+
if plugin_log:
|
|
55
|
+
path = plugin_log.removesuffix(".plugin.log") + ".dispatch.jsonl"
|
|
56
|
+
if not path:
|
|
57
|
+
return
|
|
58
|
+
if _SAMPLE_EVERY > 0:
|
|
59
|
+
# Sampling keeps the evidence trail (which backend served which call)
|
|
60
|
+
# while removing the write cost from the measured hot path.
|
|
61
|
+
global _event_count
|
|
62
|
+
_event_count += 1
|
|
63
|
+
if _event_count % _SAMPLE_EVERY != 1:
|
|
64
|
+
return
|
|
65
|
+
try:
|
|
66
|
+
key = tuple(sorted(event.items()))
|
|
67
|
+
rendered = _JSON_CACHE.get(key)
|
|
68
|
+
if rendered is None:
|
|
69
|
+
rendered = json.dumps(event, sort_keys=True) + "\n"
|
|
70
|
+
_JSON_CACHE[key] = rendered
|
|
71
|
+
with open(path, "a", encoding="utf-8") as stream:
|
|
72
|
+
stream.write(rendered)
|
|
73
|
+
except OSError:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def attention_forward(
|
|
78
|
+
query: torch.Tensor,
|
|
79
|
+
key: torch.Tensor,
|
|
80
|
+
value: torch.Tensor,
|
|
81
|
+
*,
|
|
82
|
+
causal: bool,
|
|
83
|
+
scale: float,
|
|
84
|
+
) -> torch.Tensor:
|
|
85
|
+
"""Adapt dense vLLM token/head tensors to the MXFlashAttn public API."""
|
|
86
|
+
if query.ndim != 3 or key.ndim != 3 or value.shape != key.shape:
|
|
87
|
+
raise ValueError("vLLM Q/K/V must use [tokens, heads, head_dim] layout")
|
|
88
|
+
output = flash_attn_func(
|
|
89
|
+
query.unsqueeze(0), key.unsqueeze(0), value.unsqueeze(0),
|
|
90
|
+
causal=causal, softmax_scale=scale,
|
|
91
|
+
)
|
|
92
|
+
return output.squeeze(0)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def register_vllm_v1_backend() -> dict[str, str]:
|
|
96
|
+
"""Register an importable backend path for vLLM 0.17.0 workers."""
|
|
97
|
+
try:
|
|
98
|
+
from vllm.v1.attention.backends.registry import AttentionBackendEnum, register_backend
|
|
99
|
+
except Exception as exc: # pragma: no cover - depends on installed vLLM
|
|
100
|
+
return {"registered": "false", "reason": f"vllm_v1_api_unavailable: {type(exc).__name__}: {exc}"}
|
|
101
|
+
class_path = "integrations.vllm.vllm_plugin.MXFlashAttnBackend"
|
|
102
|
+
|
|
103
|
+
def apply_override() -> None:
|
|
104
|
+
register_backend(AttentionBackendEnum.FLASH_ATTN, class_path=class_path)
|
|
105
|
+
|
|
106
|
+
apply_override()
|
|
107
|
+
# The MetaX selector may intentionally return its own backend class even
|
|
108
|
+
# after registry overrides. Replace only its implementation factory so
|
|
109
|
+
# the vendor metadata builder and constructor remain intact while the
|
|
110
|
+
# decoder implementation is ours.
|
|
111
|
+
try:
|
|
112
|
+
from integrations.vllm.vllm_plugin import MXFlashAttnImpl
|
|
113
|
+
import vllm_metax.v1.attention.backends.flash_attn as metax_flash_attn
|
|
114
|
+
|
|
115
|
+
metax_flash_attn.MacaFlashAttentionBackend.get_impl_cls = staticmethod(
|
|
116
|
+
lambda: MXFlashAttnImpl
|
|
117
|
+
)
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
# The MetaX vLLM plugin re-registers FLASH_ATTN during backend selection.
|
|
121
|
+
# Reapply our importable path after that platform hook runs in each worker.
|
|
122
|
+
try:
|
|
123
|
+
import vllm_metax.platform as metax_platform
|
|
124
|
+
|
|
125
|
+
original = metax_platform.register_attention_backends
|
|
126
|
+
if not getattr(original, "_mxflashattn_wrapped", False):
|
|
127
|
+
def wrapped_register_attention_backends() -> None:
|
|
128
|
+
original()
|
|
129
|
+
apply_override()
|
|
130
|
+
|
|
131
|
+
wrapped_register_attention_backends._mxflashattn_wrapped = True
|
|
132
|
+
metax_platform.register_attention_backends = wrapped_register_attention_backends
|
|
133
|
+
|
|
134
|
+
# MetaX's selector calls its registration hook immediately before
|
|
135
|
+
# resolving the class path. In some worker start modes that hook is
|
|
136
|
+
# bound before the wrapper above is installed, so enforce the path at
|
|
137
|
+
# the final selector boundary as well.
|
|
138
|
+
platform_base = getattr(metax_platform, "MacaPlatformBase", None)
|
|
139
|
+
descriptor = getattr(platform_base, "__dict__", {}).get("get_attn_backend_cls")
|
|
140
|
+
selector = descriptor.__func__ if isinstance(descriptor, classmethod) else descriptor
|
|
141
|
+
if selector is not None and not getattr(selector, "_mxflashattn_wrapped", False):
|
|
142
|
+
def wrapped_selector(cls, selected_backend, attn_selector_config, num_heads=None):
|
|
143
|
+
if getattr(selected_backend, "name", None) == "FLASH_ATTN":
|
|
144
|
+
apply_override()
|
|
145
|
+
return class_path
|
|
146
|
+
return selector(cls, selected_backend, attn_selector_config, num_heads)
|
|
147
|
+
|
|
148
|
+
wrapped_selector._mxflashattn_wrapped = True
|
|
149
|
+
platform_base.get_attn_backend_cls = classmethod(wrapped_selector)
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
return {
|
|
153
|
+
"registered": "true",
|
|
154
|
+
"backend_name": "MXFLASHATTN_V017",
|
|
155
|
+
"backend_path": class_path,
|
|
156
|
+
"decoder_path": "vendor_direct_with_aten_correctness_fallback",
|
|
157
|
+
"encoder_path": "mxflashattn_public_api",
|
|
158
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class VllmCompatibility:
|
|
9
|
+
installed_version: str | None
|
|
10
|
+
status: str
|
|
11
|
+
reason: str
|
|
12
|
+
supported: bool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def detect_vllm_version() -> str | None:
|
|
16
|
+
try:
|
|
17
|
+
return importlib.metadata.version("vllm")
|
|
18
|
+
except importlib.metadata.PackageNotFoundError:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def classify_vllm(version: str | None) -> VllmCompatibility:
|
|
23
|
+
if version is None:
|
|
24
|
+
return VllmCompatibility(None, "not_installed", "vLLM is not installed", False)
|
|
25
|
+
normalized = version.split("+", 1)[0]
|
|
26
|
+
parts = normalized.split(".")
|
|
27
|
+
try:
|
|
28
|
+
major, minor = int(parts[0]), int(parts[1])
|
|
29
|
+
except (IndexError, ValueError):
|
|
30
|
+
return VllmCompatibility(version, "unknown", "cannot parse vLLM version", False)
|
|
31
|
+
|
|
32
|
+
if (major, minor) == (0, 17):
|
|
33
|
+
return VllmCompatibility(version, "supported", "v1.3 backend contract", True)
|
|
34
|
+
if (major, minor) == (0, 30):
|
|
35
|
+
return VllmCompatibility(
|
|
36
|
+
version,
|
|
37
|
+
"probe_required",
|
|
38
|
+
"vLLM 0.30 API must be inspected and tested in the target environment",
|
|
39
|
+
False,
|
|
40
|
+
)
|
|
41
|
+
return VllmCompatibility(
|
|
42
|
+
version,
|
|
43
|
+
"unsupported",
|
|
44
|
+
"no validated MXFlashAttn integration contract for this vLLM version",
|
|
45
|
+
False,
|
|
46
|
+
)
|