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/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()
@@ -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
+ )