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.
@@ -0,0 +1,11 @@
1
+ VLLM_TARGET_VERSION = "0.17.0"
2
+ QUICK_MODEL = "/mnt/moark-models/Qwen3-0.6B"
3
+ SHOWCASE_MODEL = "Qwen/Qwen2.5-7B-Instruct"
4
+
5
+
6
+ def validate_vllm_version(version: str) -> None:
7
+ if version != VLLM_TARGET_VERSION:
8
+ raise RuntimeError(
9
+ f"MXFlashAttn currently targets vLLM {VLLM_TARGET_VERSION}; found {version}. "
10
+ "Confirm compatibility before selecting an attention backend."
11
+ )
@@ -0,0 +1,286 @@
1
+ """Small, explicit bridge for vLLM 0.17 paged decoder metadata.
2
+
3
+ This adapter intentionally supports one-token decode only. It converts the
4
+ packed vLLM tensors and the ``[2, blocks, block_size, kv_heads, dim]`` cache
5
+ layout into ``flash_attn_with_kvcache``. Prefill, cascade attention, FP8,
6
+ and scheduler-specific metadata are rejected until they have dedicated tests.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ import torch
16
+
17
+ from mxflashattn import flash_attn_with_kvcache
18
+
19
+ # inspect_layout() reads query_start_loc / seq_lens back to the host, which is a
20
+ # device synchronisation. Inside vLLM this runs once per layer per decode step
21
+ # (28 layers x N steps x prompts), so it dominates end-to-end latency. The cheap
22
+ # shape/device/dtype checks always run; the host-syncing inspection is gated and
23
+ # enabled by the test suite via MXFLASHATTN_STRICT_LAYOUT=1.
24
+ STRICT_LAYOUT = os.getenv("MXFLASHATTN_STRICT_LAYOUT", "0") == "1"
25
+ def _transparent_enabled() -> bool:
26
+ # Resolve at call time so vLLM workers and tests can configure the mode
27
+ # after importing this module.
28
+ return os.getenv("MXFLASHATTN_TRANSPARENT", "0") == "1"
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class DecoderLayout:
33
+ num_actual_tokens: int
34
+ batch_size: int
35
+ query_lens: tuple[int, ...]
36
+ max_query_len: int
37
+ seq_lens: tuple[int, ...]
38
+ query_start_loc: tuple[int, ...]
39
+ block_table_shape: tuple[int, int]
40
+ kv_cache_shape: tuple[int, int, int, int, int]
41
+ scheduler_metadata_present: bool
42
+
43
+
44
+ class DecoderAdapterBlocked(RuntimeError):
45
+ """Raised when vLLM metadata is outside the verified adapter contract."""
46
+
47
+
48
+ def inspect_layout(query: torch.Tensor, kv_cache: torch.Tensor, attn_metadata: Any) -> DecoderLayout:
49
+ """Validate and record the vLLM 0.17 decoder tensor layout."""
50
+ required = (
51
+ "num_actual_tokens", "max_query_len", "query_start_loc", "max_seq_len",
52
+ "seq_lens", "block_table", "scheduler_metadata", "causal", "use_cascade",
53
+ )
54
+ missing = [name for name in required if not hasattr(attn_metadata, name)]
55
+ if missing:
56
+ raise DecoderAdapterBlocked(f"metadata_missing_fields:{','.join(missing)}")
57
+ if query.ndim != 3:
58
+ raise DecoderAdapterBlocked(f"query_rank:{query.ndim},expected:3")
59
+ if kv_cache.ndim != 5 or kv_cache.shape[0] != 2:
60
+ raise DecoderAdapterBlocked(
61
+ f"kv_cache_shape:{tuple(kv_cache.shape)},expected:[2,blocks,block_size,kv_heads,head_dim]"
62
+ )
63
+ starts = tuple(int(x) for x in attn_metadata.query_start_loc.detach().cpu().tolist())
64
+ seq_lens = tuple(int(x) for x in attn_metadata.seq_lens.detach().cpu().tolist())
65
+ if len(starts) != len(seq_lens) + 1 or starts[0] != 0 or starts[-1] != int(attn_metadata.num_actual_tokens):
66
+ raise DecoderAdapterBlocked("query_start_loc_does_not_cover_num_actual_tokens")
67
+ query_lens = tuple(right - left for left, right in zip(starts, starts[1:]))
68
+ block_table = attn_metadata.block_table
69
+ if block_table.ndim != 2 or block_table.shape[0] != len(seq_lens):
70
+ raise DecoderAdapterBlocked("block_table_rows_do_not_match_batch")
71
+ return DecoderLayout(
72
+ num_actual_tokens=int(attn_metadata.num_actual_tokens),
73
+ batch_size=len(seq_lens),
74
+ query_lens=query_lens,
75
+ max_query_len=int(attn_metadata.max_query_len),
76
+ seq_lens=seq_lens,
77
+ query_start_loc=starts,
78
+ block_table_shape=(int(block_table.shape[0]), int(block_table.shape[1])),
79
+ kv_cache_shape=tuple(int(x) for x in kv_cache.shape),
80
+ scheduler_metadata_present=attn_metadata.scheduler_metadata is not None,
81
+ )
82
+
83
+
84
+ def fast_layout(query: torch.Tensor, kv_cache: torch.Tensor, attn_metadata: Any) -> DecoderLayout:
85
+ """Cheap layout capture that performs no device-to-host synchronisation."""
86
+ required = (
87
+ "num_actual_tokens", "max_query_len", "query_start_loc", "max_seq_len",
88
+ "seq_lens", "block_table", "scheduler_metadata", "causal", "use_cascade",
89
+ )
90
+ missing = [name for name in required if not hasattr(attn_metadata, name)]
91
+ if missing:
92
+ raise DecoderAdapterBlocked(f"metadata_missing_fields:{','.join(missing)}")
93
+ if query.ndim != 3:
94
+ raise DecoderAdapterBlocked(f"query_rank:{query.ndim},expected:3")
95
+ if kv_cache.ndim != 5 or kv_cache.shape[0] != 2:
96
+ raise DecoderAdapterBlocked(
97
+ f"kv_cache_shape:{tuple(kv_cache.shape)},expected:[2,blocks,block_size,kv_heads,head_dim]"
98
+ )
99
+ block_table = attn_metadata.block_table
100
+ if block_table.ndim != 2:
101
+ raise DecoderAdapterBlocked("block_table_rows_do_not_match_batch")
102
+ batch = int(block_table.shape[0])
103
+ max_query_len = int(attn_metadata.max_query_len)
104
+ return DecoderLayout(
105
+ num_actual_tokens=int(attn_metadata.num_actual_tokens),
106
+ batch_size=batch,
107
+ query_lens=tuple([max_query_len] * batch),
108
+ max_query_len=max_query_len,
109
+ seq_lens=(),
110
+ query_start_loc=(),
111
+ block_table_shape=(batch, int(block_table.shape[1])),
112
+ kv_cache_shape=tuple(int(x) for x in kv_cache.shape),
113
+ scheduler_metadata_present=attn_metadata.scheduler_metadata is not None,
114
+ )
115
+
116
+
117
+ def decode_forward(
118
+ query: torch.Tensor,
119
+ kv_cache: torch.Tensor,
120
+ attn_metadata: Any,
121
+ *,
122
+ softmax_scale: float,
123
+ causal: bool = True,
124
+ ) -> tuple[torch.Tensor, DecoderLayout]:
125
+ """Run the verified one-token paged decode path through MXFlashAttn.
126
+
127
+ vLLM updates the cache before calling attention. Consequently ``k`` and
128
+ ``v`` are omitted here and ``seq_lens`` describes the already populated
129
+ cache. This is stricter than vLLM's general FlashAttention implementation
130
+ so an unsupported prefill path cannot silently delegate.
131
+ """
132
+ if os.getenv("MXFLASHATTN_GRAPH_DECODE", "0") == "1":
133
+ # Graph-safe path is decoder-only. Prefill may have a static padded
134
+ # query shape, but its metadata is not one cache sequence per query
135
+ # row; sending it to the paged KV decoder corrupts outputs.
136
+ # In full CUDA-graph mode vLLM pads q to a capture size while keeping
137
+ # one metadata row per logical request. A singleton seq_lens/block row
138
+ # therefore identifies a padded one-token decode, even when
139
+ # max_query_len contains the capture/prefill value (for example 15).
140
+ has_padded_decode = (
141
+ attn_metadata.seq_lens.ndim == 1
142
+ and attn_metadata.block_table.ndim == 2
143
+ and attn_metadata.seq_lens.numel() == attn_metadata.block_table.shape[0]
144
+ and attn_metadata.seq_lens.numel() < query.shape[0]
145
+ )
146
+ has_split_decode = (
147
+ int(getattr(attn_metadata, "num_decode_tokens", 0) or 0) > 0
148
+ and int(getattr(attn_metadata, "num_decodes", 0) or 0) > 0
149
+ and getattr(attn_metadata, "decode_seq_lens", None) is not None
150
+ and getattr(attn_metadata, "decode_block_table", None) is not None
151
+ )
152
+ if int(getattr(attn_metadata, "max_query_len", 0)) != 1 and not has_padded_decode and not has_split_decode:
153
+ raise DecoderAdapterBlocked(
154
+ f"only_single_token_decode_supported:max_query_len={int(getattr(attn_metadata, 'max_query_len', 0))}"
155
+ )
156
+ return _decode_forward_graph(query, kv_cache, attn_metadata,
157
+ softmax_scale=softmax_scale, causal=causal)
158
+ if _transparent_enabled():
159
+ # Transparent mode still only owns the verified one-token decoder
160
+ # contract. Prefill has one seq_lens entry but many query tokens, so
161
+ # forwarding it to the paged KV API produces a batch-size mismatch;
162
+ # let vLLM's native implementation handle that path.
163
+ if int(attn_metadata.max_query_len) != 1:
164
+ raise DecoderAdapterBlocked(
165
+ f"only_single_token_decode_supported:max_query_len={int(attn_metadata.max_query_len)}"
166
+ )
167
+ key_cache, value_cache = kv_cache.unbind(0)
168
+ n = int(attn_metadata.num_actual_tokens)
169
+ q = query[:n].reshape(n, 1, query.shape[1], query.shape[2])
170
+ # vLLM capture/replay metadata may expose seq_lens as a singleton
171
+ # leading-dimension view (for example [1, batch]). The public
172
+ # flash-attn contract is strictly one-dimensional, so normalize the
173
+ # view without reading it back to the host.
174
+ seq_lens = attn_metadata.seq_lens.reshape(-1)
175
+ cache_seqlens = (
176
+ seq_lens
177
+ if (seq_lens.device == query.device and seq_lens.dtype == torch.int32)
178
+ else seq_lens.to(device=query.device, dtype=torch.int32)
179
+ )
180
+ bt_raw = attn_metadata.block_table
181
+ block_table = bt_raw if (bt_raw.device == query.device and bt_raw.dtype == torch.int32) else bt_raw.to(device=query.device, dtype=torch.int32)
182
+ result = flash_attn_with_kvcache(q, key_cache, value_cache, cache_seqlens=cache_seqlens, block_table=block_table, softmax_scale=softmax_scale, causal=causal)
183
+ return result.reshape(n, -1), None
184
+ layout = inspect_layout(query, kv_cache, attn_metadata) if STRICT_LAYOUT else fast_layout(query, kv_cache, attn_metadata)
185
+ if not bool(attn_metadata.causal) and causal:
186
+ raise DecoderAdapterBlocked("metadata_causal_flag_is_false")
187
+ if bool(attn_metadata.use_cascade):
188
+ raise DecoderAdapterBlocked("cascade_attention_not_supported_by_adapter")
189
+ if layout.max_query_len != 1:
190
+ import os as _os
191
+ if _os.getenv("MXFLASHATTN_GRAPH_PROBE") == "1":
192
+ try:
193
+ with open("/tmp/graph_probe.log", "a") as _lf:
194
+ _lf.write(
195
+ f"max_query_len={layout.max_query_len} "
196
+ f"query_shape={tuple(query.shape)} "
197
+ f"n_actual={int(attn_metadata.num_actual_tokens)} "
198
+ f"meta_type={type(attn_metadata).__name__} "
199
+ f"has_capture_meta={hasattr(attn_metadata,'attention_metadata_for_capture')} "
200
+ f"q_start_loc_present={hasattr(attn_metadata,'query_start_loc')} "
201
+ f"seq_lens_shape={tuple(getattr(attn_metadata,'seq_lens',__import__('torch').empty(0)).shape) if hasattr(attn_metadata,'seq_lens') else 'NA'}\n"
202
+ )
203
+ except Exception:
204
+ pass
205
+ raise DecoderAdapterBlocked(
206
+ f"only_single_token_decode_supported:max_query_len={layout.max_query_len}"
207
+ )
208
+ if kv_cache.dtype not in (torch.float16, torch.bfloat16):
209
+ raise DecoderAdapterBlocked(f"kv_cache_dtype_not_supported:{kv_cache.dtype}")
210
+ if query.shape[1] % kv_cache.shape[3]:
211
+ raise DecoderAdapterBlocked("query_and_kv_head_counts_are_not_gqa_compatible")
212
+ key_cache, value_cache = kv_cache.unbind(0)
213
+ batch = layout.batch_size
214
+ q = query[: layout.num_actual_tokens].reshape(batch, 1, query.shape[1], query.shape[2])
215
+ # vLLM 0.17 already keeps these on the device in int32. Calling .to()
216
+ # unconditionally costs a Python/dispatch round trip per layer per step, and
217
+ # .clone() cost even more. Only convert when the contract is actually broken.
218
+ seq_lens = attn_metadata.seq_lens
219
+ cache_seqlens = (
220
+ seq_lens if seq_lens.device == query.device and seq_lens.dtype == torch.int32
221
+ else seq_lens.to(device=query.device, dtype=torch.int32)
222
+ )
223
+ block_table_raw = attn_metadata.block_table
224
+ block_table = (
225
+ block_table_raw
226
+ if block_table_raw.device == query.device and block_table_raw.dtype == torch.int32
227
+ else block_table_raw.to(device=query.device, dtype=torch.int32)
228
+ )
229
+ result = flash_attn_with_kvcache(
230
+ q, key_cache, value_cache, cache_seqlens=cache_seqlens,
231
+ block_table=block_table, softmax_scale=softmax_scale, causal=causal,
232
+ )
233
+ return result.reshape(layout.num_actual_tokens, -1), layout
234
+
235
+
236
+ def _decode_forward_graph(
237
+ query: torch.Tensor,
238
+ kv_cache: torch.Tensor,
239
+ attn_metadata: Any,
240
+ *,
241
+ softmax_scale: float,
242
+ causal: bool = True,
243
+ ):
244
+ """Run the static-shape graph decode using MetaX's split decode metadata.
245
+
246
+ vLLM/MetaX keeps padded prefill metadata in ``seq_lens``/``block_table``
247
+ and exposes the actual decode subset separately. Using the former and
248
+ broadcasting it to ``query.shape[0]`` makes a graph-shaped call execute
249
+ with the wrong KV pages.
250
+ """
251
+ key_cache, value_cache = kv_cache.unbind(0)
252
+ decode_n = int(getattr(attn_metadata, "num_decode_tokens", 0) or 0)
253
+ decode_batch = int(getattr(attn_metadata, "num_decodes", 0) or 0)
254
+ decode_seq = getattr(attn_metadata, "decode_seq_lens", None)
255
+ decode_bt = getattr(attn_metadata, "decode_block_table", None)
256
+ if decode_n > 0 and decode_batch > 0 and decode_seq is not None and decode_bt is not None:
257
+ q_tokens = query[:decode_n]
258
+ if decode_n % decode_batch:
259
+ raise DecoderAdapterBlocked("decode_tokens_not_divisible_by_num_decodes")
260
+ q = q_tokens.reshape(decode_batch, decode_n // decode_batch, query.shape[1], query.shape[2]).contiguous()
261
+ cache_seqlens = decode_seq.reshape(-1)
262
+ block_table = decode_bt
263
+ batch = decode_batch
264
+ else:
265
+ raw_seq = attn_metadata.seq_lens.reshape(-1)
266
+ raw_bt = attn_metadata.block_table
267
+ # Capture padding belongs to the graph buffer, not to the logical
268
+ # attention batch. Use one q row per metadata row and leave padded q
269
+ # rows out of the vendor call.
270
+ batch = int(raw_seq.numel()) if raw_seq.numel() < query.shape[0] else query.shape[0]
271
+ q = query[:batch].reshape(batch, 1, query.shape[1], query.shape[2]).contiguous()
272
+ cache_seqlens = raw_seq
273
+ block_table = raw_bt
274
+ if cache_seqlens.device != query.device or cache_seqlens.dtype != torch.int32:
275
+ cache_seqlens = cache_seqlens.to(device=query.device, dtype=torch.int32)
276
+ if block_table.device != query.device or block_table.dtype != torch.int32:
277
+ block_table = block_table.to(device=query.device, dtype=torch.int32)
278
+ if cache_seqlens.numel() != batch or block_table.shape[0] != batch:
279
+ raise DecoderAdapterBlocked("decode_metadata_batch_mismatch")
280
+ cache_seqlens = cache_seqlens.contiguous()
281
+ block_table = block_table.contiguous()
282
+ result = flash_attn_with_kvcache(
283
+ q, key_cache, value_cache, cache_seqlens=cache_seqlens,
284
+ block_table=block_table, softmax_scale=softmax_scale, causal=causal,
285
+ )
286
+ return result.reshape(q.shape[0] * q.shape[1], -1), None
@@ -0,0 +1,22 @@
1
+ """Write a non-invasive vLLM compatibility report for the current environment."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from integrations.vllm.compat import classify_vllm, detect_vllm_version
9
+
10
+
11
+ def main() -> None:
12
+ parser = argparse.ArgumentParser()
13
+ parser.add_argument("--output", type=Path, default=Path("artifacts/vllm-compatibility.json"))
14
+ args = parser.parse_args()
15
+ result = classify_vllm(detect_vllm_version())
16
+ args.output.parent.mkdir(parents=True, exist_ok=True)
17
+ args.output.write_text(json.dumps(result.__dict__, indent=2) + "\n", encoding="utf-8")
18
+ print(json.dumps(result.__dict__, indent=2))
19
+
20
+
21
+ if __name__ == "__main__":
22
+ main()
@@ -0,0 +1,91 @@
1
+ """Capture vLLM 0.17 decoder layout evidence and exercise the strict adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import time
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ import torch
13
+
14
+ from integrations.vllm.decoder_adapter import DecoderAdapterBlocked, decode_forward, inspect_layout
15
+ from mxflashattn.dispatch import get_last_dispatch_info
16
+
17
+
18
+ @dataclass
19
+ class Metadata:
20
+ num_actual_tokens: int
21
+ max_query_len: int
22
+ query_start_loc: torch.Tensor
23
+ max_seq_len: int
24
+ seq_lens: torch.Tensor
25
+ block_table: torch.Tensor
26
+ slot_mapping: torch.Tensor
27
+ use_cascade: bool = False
28
+ common_prefix_len: int = 0
29
+ cu_prefix_query_lens: torch.Tensor | None = None
30
+ prefix_kv_lens: torch.Tensor | None = None
31
+ suffix_kv_lens: torch.Tensor | None = None
32
+ scheduler_metadata: torch.Tensor | None = None
33
+ causal: bool = True
34
+
35
+
36
+ def main() -> None:
37
+ parser = argparse.ArgumentParser()
38
+ parser.add_argument("--output", type=Path, default=Path("artifacts/decoder-layout.json"))
39
+ parser.add_argument("--log", type=Path, default=Path("artifacts/decoder-layout.log"))
40
+ args = parser.parse_args()
41
+ os.environ.setdefault("MXFLASHATTN_ALLOW_FALLBACK", "1")
42
+ torch.manual_seed(7)
43
+ # MetaX PyTorch exposes C500 through the CUDA-compatible device namespace.
44
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
45
+ dtype = torch.float16
46
+ batch, q_heads, kv_heads, dim = 2, 4, 2, 64
47
+ block_size, blocks, seq_len = 16, 4, 17
48
+ query = torch.randn(batch, q_heads, dim, device=device, dtype=dtype)
49
+ kv_cache = torch.randn(2, blocks, block_size, kv_heads, dim, device=device, dtype=dtype)
50
+ metadata = Metadata(
51
+ num_actual_tokens=batch, max_query_len=1,
52
+ query_start_loc=torch.tensor([0, 1, 2], device=device, dtype=torch.int32),
53
+ max_seq_len=seq_len,
54
+ seq_lens=torch.tensor([seq_len, seq_len - 1], device=device, dtype=torch.int32),
55
+ block_table=torch.tensor([[0, 1], [2, 3]], device=device, dtype=torch.int32),
56
+ slot_mapping=torch.tensor([0, 1], device=device, dtype=torch.int32),
57
+ )
58
+ started = time.perf_counter()
59
+ record: dict[str, object] = {
60
+ "schema_version": 1, "kind": "vllm_decoder_layout_probe", "target_vllm": "0.17.0",
61
+ "device": str(device), "dtype": str(dtype), "contract": "single_token_decode_paged_kv",
62
+ "source_layout": {
63
+ "query": "[num_tokens,num_heads,head_size]",
64
+ "kv_cache": "[2,num_blocks,block_size,num_kv_heads,head_size]",
65
+ "metadata": ["num_actual_tokens", "max_query_len", "query_start_loc", "max_seq_len",
66
+ "seq_lens", "block_table", "slot_mapping", "scheduler_metadata", "use_cascade"],
67
+ },
68
+ }
69
+ try:
70
+ layout = inspect_layout(query, kv_cache, metadata)
71
+ output, layout = decode_forward(query, kv_cache, metadata, softmax_scale=dim ** -0.5)
72
+ info = get_last_dispatch_info()
73
+ record.update({"status": "adapter_exercised", "layout": layout.__dict__,
74
+ "output_shape": list(output.shape),
75
+ "dispatch": info.__dict__ if info else None,
76
+ "elapsed_ms": (time.perf_counter() - started) * 1000,
77
+ "fallback_allowed": True})
78
+ except DecoderAdapterBlocked as exc:
79
+ record.update({"status": "blocked", "blocker": str(exc), "dispatch": None})
80
+ except Exception as exc:
81
+ info = get_last_dispatch_info()
82
+ record.update({"status": "adapter_runtime_error", "error": f"{type(exc).__name__}: {exc}",
83
+ "dispatch": info.__dict__ if info else None})
84
+ args.output.parent.mkdir(parents=True, exist_ok=True)
85
+ args.output.write_text(json.dumps(record, indent=2, default=str), encoding="utf-8")
86
+ args.log.write_text(json.dumps(record, indent=2, default=str) + "\n", encoding="utf-8")
87
+ print(json.dumps(record, indent=2, default=str))
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -0,0 +1,24 @@
1
+ """Validate the vLLM 0.17 v1 registration hook in an isolated process."""
2
+ from __future__ import annotations
3
+
4
+ import importlib.metadata
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from integrations.vllm.backend import register_vllm_v1_backend
9
+
10
+
11
+ def main() -> None:
12
+ try:
13
+ version = importlib.metadata.version("vllm")
14
+ except importlib.metadata.PackageNotFoundError:
15
+ version = None
16
+ result = {"vllm_version": version, "registration": register_vllm_v1_backend()}
17
+ output = Path("artifacts/vllm-0.17-registration.json")
18
+ output.parent.mkdir(parents=True, exist_ok=True)
19
+ output.write_text(json.dumps(result, indent=2), encoding="utf-8")
20
+ print(f"Wrote registration probe to {output}")
21
+
22
+
23
+ if __name__ == "__main__":
24
+ main()
@@ -0,0 +1,55 @@
1
+ """Inspect the installed vLLM attention extension points without mutating vLLM."""
2
+ from __future__ import annotations
3
+
4
+ import importlib
5
+ import importlib.metadata
6
+ import inspect
7
+ import json
8
+ from pathlib import Path
9
+
10
+
11
+ def _describe(module_name: str, names: list[str]) -> dict:
12
+ result = {"module": module_name, "imports": {}}
13
+ try:
14
+ module = importlib.import_module(module_name)
15
+ except Exception as exc: # pragma: no cover - depends on installed vLLM
16
+ result["import_error"] = f"{type(exc).__name__}: {exc}"
17
+ return result
18
+ for name in names:
19
+ try:
20
+ value = getattr(module, name)
21
+ result["imports"][name] = {
22
+ "type": type(value).__name__,
23
+ "signature": str(inspect.signature(value)) if callable(value) else None,
24
+ "public_attributes": sorted(name for name in dir(value) if not name.startswith("_"))[:80],
25
+ }
26
+ except Exception as exc:
27
+ result["imports"][name] = {"error": f"{type(exc).__name__}: {exc}"}
28
+ return result
29
+
30
+
31
+ def main() -> None:
32
+ try:
33
+ version = importlib.metadata.version("vllm")
34
+ except importlib.metadata.PackageNotFoundError:
35
+ version = None
36
+ result = {
37
+ "vllm_version": version,
38
+ "modules": [
39
+ _describe("vllm.attention.backends.abstract", ["AttentionBackend", "AttentionMetadata", "AttentionType"]),
40
+ _describe("vllm.attention.backends.registry", ["AttentionBackendEnum", "register_backend", "get_backend" ]),
41
+ _describe("vllm.attention.backends.flash_attn", ["FlashAttentionBackend", "FlashAttentionMetadata"]),
42
+ _describe("vllm.attention.layer", ["Attention" ]),
43
+ _describe("vllm.v1.attention.backend", ["AttentionBackend", "AttentionMetadata", "AttentionImpl"]),
44
+ _describe("vllm.v1.attention.backends.registry", ["AttentionBackendEnum", "register_backend", "get_attn_backend"]),
45
+ _describe("vllm.v1.attention.backends.flash_attn", ["FlashAttentionBackend", "FlashAttentionImpl", "FlashAttentionMetadata"]),
46
+ ],
47
+ }
48
+ output = Path("artifacts/vllm-0.17-api-probe.json")
49
+ output.parent.mkdir(parents=True, exist_ok=True)
50
+ output.write_text(json.dumps(result, indent=2), encoding="utf-8")
51
+ print(f"Wrote vLLM API probe to {output}")
52
+
53
+
54
+ if __name__ == "__main__":
55
+ main()