[Feat][Perf] Add new warmup infrastructure for JITs (#47451)
Signed-off-by: LopezCastroRoberto <rocastro@redhat.com> Signed-off-by: Roberto L. Castro <38211239+LopezCastroRoberto@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Roberto L. Castro committed
Jul 20, 2026 at 22:21 UTC
7ca017778fc04cfb1a96080e7c484b1b44fc3a64
14 files changed
+1855
-693
.buildkite/test_areas/model_executor.yaml
+4
@@ -10,7 +10,9 @@ steps:
10
- vllm/engine/arg_utils.py
11
- vllm/config/model.py
12
- vllm/model_executor
13
+ - vllm/model_executor/warmup
14
- tests/model_executor
15
+ - tests/model_executor/test_jit_warmup.py
16
- tests/entrypoints/openai/completion/test_tensorizer_entrypoint.py
17
commands:
18
- apt-get update && apt-get install -y curl libsodium23
@@ -34,7 +36,9 @@ steps:
36
- vllm/engine/arg_utils.py
37
- vllm/config/model.py
38
- vllm/model_executor
39
+ - vllm/model_executor/warmup
40
- tests/model_executor
41
+ - tests/model_executor/test_jit_warmup.py
42
- tests/entrypoints/openai/completion/test_tensorizer_entrypoint.py
43
- vllm/_aiter_ops.py
44
- vllm/platforms/rocm.py
tests/model_executor/test_jit_warmup.py
new
+306
@@ -0,0 +1,306 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import ast
5
+from dataclasses import dataclass
6
+from types import SimpleNamespace
7
+from typing import Any, cast
8
+
9
+import pytest
10
+
11
+from vllm.model_executor.warmup.jit_warmup import (
12
+ VllmJitKernel,
13
+ WarmupIntRange,
14
+ get_ast_full_name,
15
+ zip_inputs,
16
+)
17
+
18
+
19
+def _next_power_of_2(value: int) -> int:
20
+ return 1 << max(0, value - 1).bit_length()
21
+
22
+
23
+def _round_up(value: int, *, multiple: int) -> int:
24
+ return ((value + multiple - 1) // multiple) * multiple
25
+
26
+
27
+def _config(
28
+ *,
29
+ bias: int = 0,
30
+ disabled: bool = False,
31
+ name: str = "base",
32
+ vectorized: bool = False,
33
+) -> SimpleNamespace:
34
+ return SimpleNamespace(
35
+ bias=bias,
36
+ disabled=disabled,
37
+ name=name,
38
+ vectorized=vectorized,
39
+ )
40
+
41
+
42
+class ToyKernel(VllmJitKernel["ToyKernel.CompileKey"]):
43
+ @dataclass(frozen=True)
44
+ class CompileKey:
45
+ block_size: int
46
+ work: int
47
+ vector_width: int
48
+ descriptor: tuple[object, ...]
49
+ enabled: bool
50
+
51
+ def dispatch( # type: ignore[override]
52
+ self,
53
+ *,
54
+ tokens: int,
55
+ cfg: Any,
56
+ lanes: int = 1,
57
+ mode: str = "default",
58
+ debug: int = 0,
59
+ ) -> CompileKey:
60
+ block_size = _next_power_of_2(tokens)
61
+ work: int = block_size * lanes + cfg.bias
62
+ return self.CompileKey(
63
+ block_size=block_size,
64
+ work=work,
65
+ vector_width=4 if cfg.vectorized and block_size >= 4 else 1,
66
+ descriptor=(
67
+ cfg.name,
68
+ mode,
69
+ -block_size,
70
+ block_size % 3,
71
+ block_size**2,
72
+ ),
73
+ enabled=not cfg.disabled,
74
+ )
75
+
76
+ def get_warmup_keys(self, max_tokens: int, cfg: Any) -> list[CompileKey]:
77
+ return self._trace_dispatch(self.dispatch)(
78
+ tokens=WarmupIntRange(1, max_tokens + 1),
79
+ cfg=cfg,
80
+ # This argument is intentionally unused by dispatch expressions.
81
+ debug=WarmupIntRange(0, 100),
82
+ )
83
+
84
+ def compile(self, compile_key: CompileKey) -> None:
85
+ pass
86
+
87
+
88
+class RecordingToyKernel(ToyKernel):
89
+ def __init__(self) -> None:
90
+ self.compiled: list[ToyKernel.CompileKey] = []
91
+ super().__init__()
92
+
93
+ def compile(self, compile_key: ToyKernel.CompileKey) -> None:
94
+ self.compiled.append(compile_key)
95
+
96
+
97
+def test_trace_dispatch_expands_ranges_dedupes_and_ignores_unused_inputs() -> None:
98
+ cfg = _config()
99
+
100
+ assert ToyKernel().get_warmup_keys(5, cfg) == [
101
+ ToyKernel.CompileKey(1, 1, 1, ("base", "default", -1, 1, 1), True),
102
+ ToyKernel.CompileKey(2, 2, 1, ("base", "default", -2, 2, 4), True),
103
+ ToyKernel.CompileKey(4, 4, 1, ("base", "default", -4, 1, 16), True),
104
+ ToyKernel.CompileKey(8, 8, 1, ("base", "default", -8, 2, 64), True),
105
+ ]
106
+
107
+
108
+def test_compile_key_uses_defaults_locals_attributes_and_expressions() -> None:
109
+ cfg = _config(bias=3, disabled=True, name="cfg", vectorized=True)
110
+
111
+ assert ToyKernel().compile_key(
112
+ {
113
+ "tokens": 4,
114
+ "cfg": cfg,
115
+ "lanes": 2,
116
+ }
117
+ ) == ToyKernel.CompileKey(
118
+ block_size=4,
119
+ work=11,
120
+ vector_width=4,
121
+ descriptor=("cfg", "default", -4, 1, 16),
122
+ enabled=False,
123
+ )
124
+
125
+
126
+def test_trace_dispatch_combines_zipped_rows_with_independent_values() -> None:
127
+ cfg = _config(vectorized=True)
128
+
129
+ keys = ToyKernel()._trace_dispatch(ToyKernel().dispatch)(
130
+ zip_inputs(
131
+ dict(tokens=1, mode="small"),
132
+ dict(tokens=4, mode="wide"),
133
+ ),
134
+ cfg=cfg,
135
+ lanes=(1, 2),
136
+ )
137
+
138
+ assert keys == [
139
+ ToyKernel.CompileKey(1, 1, 1, ("base", "small", -1, 1, 1), True),
140
+ ToyKernel.CompileKey(1, 2, 1, ("base", "small", -1, 1, 1), True),
141
+ ToyKernel.CompileKey(4, 4, 4, ("base", "wide", -4, 1, 16), True),
142
+ ToyKernel.CompileKey(4, 8, 4, ("base", "wide", -4, 1, 16), True),
143
+ ]
144
+
145
+
146
+def test_zip_inputs_validates_input_rows() -> None:
147
+ with pytest.raises(ValueError, match="requires at least one"):
148
+ zip_inputs()
149
+ with pytest.raises(ValueError, match="rows must be mappings"):
150
+ zip_inputs(cast(Any, ("tokens", 1)))
151
+ with pytest.raises(ValueError, match="at least one dispatch input name"):
152
+ zip_inputs({})
153
+ with pytest.raises(ValueError, match="dispatch input names must be strings"):
154
+ zip_inputs(cast(Any, {1: 2}))
155
+ with pytest.raises(ValueError, match="same dispatch input names"):
156
+ zip_inputs({"tokens": 1}, {"mode": "small"})
157
+
158
+
159
+def test_trace_dispatch_rejects_bad_positional_groups_and_duplicates() -> None:
160
+ kernel = ToyKernel()
161
+
162
+ with pytest.raises(TypeError, match="zip_inputs"):
163
+ kernel._trace_dispatch(kernel.dispatch)(
164
+ cast(Any, {"tokens": 1}),
165
+ cfg=_config(),
166
+ )
167
+
168
+ with pytest.raises(ValueError, match="specified more than once"):
169
+ kernel._trace_dispatch(kernel.dispatch)(
170
+ zip_inputs(dict(tokens=1, mode="small")),
171
+ tokens=2,
172
+ cfg=_config(),
173
+ )
174
+
175
+
176
+def test_helper_calls_support_keywords_and_reject_star_kwargs() -> None:
177
+ class HelperKernel(VllmJitKernel["HelperKernel.CompileKey"]):
178
+ @dataclass(frozen=True)
179
+ class CompileKey:
180
+ value: int
181
+
182
+ def dispatch( # type: ignore[override]
183
+ self,
184
+ *,
185
+ tokens: int,
186
+ block_size: int,
187
+ ) -> CompileKey:
188
+ return self.CompileKey(value=_round_up(tokens, multiple=block_size))
189
+
190
+ def get_warmup_keys(self) -> list[CompileKey]:
191
+ return []
192
+
193
+ def compile(self, compile_key: CompileKey) -> None:
194
+ pass
195
+
196
+ class StarKwargsKernel(VllmJitKernel["StarKwargsKernel.CompileKey"]):
197
+ @dataclass(frozen=True)
198
+ class CompileKey:
199
+ value: int
200
+
201
+ def dispatch( # type: ignore[override]
202
+ self,
203
+ *,
204
+ tokens: int,
205
+ block_size: int,
206
+ ) -> CompileKey:
207
+ return self.CompileKey(value=_round_up(tokens, **{"multiple": block_size}))
208
+
209
+ def get_warmup_keys(self) -> list[CompileKey]:
210
+ return []
211
+
212
+ def compile(self, compile_key: CompileKey) -> None:
213
+ pass
214
+
215
+ assert HelperKernel().compile_key(
216
+ {
217
+ "tokens": 5,
218
+ "block_size": 4,
219
+ }
220
+ ) == HelperKernel.CompileKey(value=8)
221
+ with pytest.raises(ValueError, match=r"cannot use \*\*kwargs"):
222
+ StarKwargsKernel().compile_key({"tokens": 5, "block_size": 4})
223
+
224
+
225
+def test_dispatch_body_must_be_local_assignments_then_compile_key_return() -> None:
226
+ class BranchKernel(VllmJitKernel["BranchKernel.CompileKey"]):
227
+ @dataclass(frozen=True)
228
+ class CompileKey:
229
+ value: int
230
+
231
+ def dispatch(self, *, value: int) -> CompileKey: # type: ignore[override]
232
+ if value > 0:
233
+ value = 1
234
+ return self.CompileKey(value=value)
235
+
236
+ def get_warmup_keys(self) -> list[CompileKey]:
237
+ return []
238
+
239
+ def compile(self, compile_key: CompileKey) -> None:
240
+ pass
241
+
242
+ class KwargsReturnKernel(VllmJitKernel["KwargsReturnKernel.CompileKey"]):
243
+ @dataclass(frozen=True)
244
+ class CompileKey:
245
+ value: int
246
+
247
+ def dispatch(self, *, value: int) -> CompileKey: # type: ignore[override]
248
+ return self.CompileKey(**{"value": value})
249
+
250
+ def get_warmup_keys(self) -> list[CompileKey]:
251
+ return []
252
+
253
+ def compile(self, compile_key: CompileKey) -> None:
254
+ pass
255
+
256
+ with pytest.raises(ValueError, match="local assignments"):
257
+ BranchKernel()
258
+ with pytest.raises(ValueError, match=r"cannot use \*\*kwargs in CompileKey"):
259
+ KwargsReturnKernel()
260
+
261
+
262
+def test_dispatch_reports_unsupported_expression_with_context() -> None:
263
+ class UnsupportedKernel(VllmJitKernel["UnsupportedKernel.CompileKey"]):
264
+ @dataclass(frozen=True)
265
+ class CompileKey:
266
+ value: object
267
+
268
+ def dispatch(self, *, value: int) -> CompileKey: # type: ignore[override]
269
+ return self.CompileKey(value={value})
270
+
271
+ def get_warmup_keys(self) -> list[CompileKey]:
272
+ return []
273
+
274
+ def compile(self, compile_key: CompileKey) -> None:
275
+ pass
276
+
277
+ with pytest.raises(ValueError) as exc_info:
278
+ UnsupportedKernel().compile_key({"value": 1})
279
+
280
+ message = str(exc_info.value)
281
+ assert "Unsupported dispatch expression" in message
282
+ assert "{value}" in message
283
+ assert "Supported dispatch expressions" in message
284
+
285
+
286
+def test_warmup_compiles_all_returned_keys_in_order() -> None:
287
+ kernel = RecordingToyKernel()
288
+ cfg = _config()
289
+
290
+ kernel.warmup(3, cfg)
291
+
292
+ assert kernel.compiled == [
293
+ ToyKernel.CompileKey(1, 1, 1, ("base", "default", -1, 1, 1), True),
294
+ ToyKernel.CompileKey(2, 2, 1, ("base", "default", -2, 2, 4), True),
295
+ ToyKernel.CompileKey(4, 4, 1, ("base", "default", -4, 1, 16), True),
296
+ ]
297
+
298
+
299
+def test_get_ast_full_name_handles_names_attributes_and_other_nodes() -> None:
300
+ dotted_expr = ast.parse("foo.bar.baz").body[0]
301
+ call_expr = ast.parse("foo()").body[0]
302
+ assert isinstance(dotted_expr, ast.Expr)
303
+ assert isinstance(call_expr, ast.Expr)
304
+
305
+ assert get_ast_full_name(dotted_expr.value) == "foo.bar.baz"
306
+ assert get_ast_full_name(call_expr.value) is None
vllm/config/kernel.py
+8
@@ -175,9 +175,15 @@ class KernelConfig:
175
enable_flashinfer_autotune: bool = None # type: ignore[assignment]
176
"""If True, run FlashInfer autotuning during kernel warmup."""
177
178
+ # TODO(roberto): Remove after registered CuTeDSL warmups are migrated
179
+ # to the shared JIT warmup infrastructure.
180
+ # https://github.com/vllm-project/vllm/pull/47451
181
enable_cutedsl_warmup: bool = True
182
"""If True, run CuTeDSL compile warmup during kernel warmup."""
183
184
+ enable_jit_warmup: bool = True
185
+ """If True, run JIT compile warmup during kernel warmup."""
186
+
187
enable_bf16x3_router_gemm: bool = False
188
"""If True, use the experimental SM100 BF16x3 CuteDSL router GEMM."""
189
@@ -250,6 +256,7 @@ class KernelConfig:
256
"""
257
ignored_factors = {
258
"enable_cutedsl_warmup",
259
+ "enable_jit_warmup",
260
"enable_flashinfer_autotune",
261
"ir_op_priority", # handled separately below
262
}
@@ -260,6 +267,7 @@ class KernelConfig:
267
@field_validator(
268
"enable_flashinfer_autotune",
269
"enable_cutedsl_warmup",
270
+ "enable_jit_warmup",
271
mode="wrap",
272
)
273
@classmethod
vllm/model_executor/warmup/cutedsl_warmup.py
+4
@@ -2,6 +2,10 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""Run registered CuTeDSL warmup compile units."""
4
5
+# TODO(roberto): Remove this compatibility registry after registered CuTeDSL
6
+# warmups are migrated to the shared JIT warmup infrastructure.
7
+# https://github.com/vllm-project/vllm/pull/47451
8
+
9
from __future__ import annotations
10
11
import time
vllm/model_executor/warmup/fa4_cutedsl_config.py
deleted
-204
@@ -1,204 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""FA4 MLA prefill CuTeDSL compile warmup config."""
4
-
5
-from __future__ import annotations
6
-
7
-from collections.abc import Hashable, Iterator
8
-from dataclasses import dataclass
9
-from typing import TYPE_CHECKING, Literal
10
-
11
-import torch
12
-
13
-if TYPE_CHECKING:
14
- from vllm.v1.attention.backends.fa_utils import (
15
- FlashAttentionCuTeDSLCompileSpec,
16
- )
17
-
18
-FA4ArchitectureFamily = Literal["sm90", "sm100f", "sm120"]
19
-
20
-FA4_STANDARD_DTYPES = (torch.bfloat16, torch.float16)
21
-
22
-# Current vLLM MLA prefill expands K/V to num_heads before FA4, so this plan
23
-# covers qhead_per_kvhead=1.
24
-# Batch is not a current FA4 MLA-prefill key field. Use b1 for compile-only
25
-# specs because it is the conservative case for Split-KV shape heuristics.
26
-# TODO(roberto): FA4 also has direct-GQA and qv/top-k absorbed-MLA paths, but vLLM
27
-# does not use them in this backend yet; they need a separate
28
-# num_kv_heads/qv/top-k-aware warmup plan if wired in later.
29
-FA4_MLA_PREFILL_COMPILE_BATCH_SIZE = 1
30
-FA4_MLA_PREFILL_Q_TILE = 128
31
-FA4_MLA_PREFILL_K_TILE = 128
32
-FA4_MLA_PREFILL_LONG_K_BLOCKS = 32
33
-FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS = 64
34
-FA4_MLA_PREFILL_CAUSAL_OPTIONS = (False, True)
35
-FA4_MLA_PREFILL_LSE_OPTIONS = (False, True)
36
-
37
-
38
-@dataclass(frozen=True)
39
-class FA4MLAPrefillCompileContext:
40
- dtype: torch.dtype
41
- num_heads: int
42
- qk_head_dim: int
43
- v_head_dim: int
44
- kv_nope_head_dim: int
45
- requires_v_padding: bool
46
- scale: float
47
- num_splits: int
48
- fa_version: int
49
-
50
- # Return the V head dim FA4 sees.
51
- @property
52
- def effective_v_head_dim(self) -> int:
53
- if self.requires_v_padding:
54
- return self.qk_head_dim
55
- return self.v_head_dim
56
-
57
-
58
-@dataclass(frozen=True)
59
-class FA4MLAPrefillCompileRequest:
60
- """One compile-only FA4 MLA prefill request."""
61
-
62
- key: Hashable
63
- compile_spec: FlashAttentionCuTeDSLCompileSpec
64
-
65
- # Compile this request.
66
- def compile(self) -> None:
67
- self.compile_spec.compile()
68
-
69
-
70
-# Yield deduped compile requests.
71
-def iter_fa4_mla_prefill_compile_requests(
72
- ctx: FA4MLAPrefillCompileContext,
73
-) -> Iterator[FA4MLAPrefillCompileRequest]:
74
- """Yield compile requests for this fixed MLA backend.
75
-
76
- FA4 dedupes duplicate atomic kernel selections in its own JIT cache.
77
- """
78
- seen: set[Hashable] = set()
79
- for compile_spec in iter_fa4_mla_prefill_compile_specs(ctx):
80
- key = compile_spec.request_key()
81
- if key in seen:
82
- continue
83
- seen.add(key)
84
- yield FA4MLAPrefillCompileRequest(
85
- key=key,
86
- compile_spec=compile_spec,
87
- )
88
-
89
-
90
-# Build compile specs for this setup.
91
-def iter_fa4_mla_prefill_compile_specs(
92
- ctx: FA4MLAPrefillCompileContext,
93
-) -> Iterator[FlashAttentionCuTeDSLCompileSpec]:
94
- """Yield compile-only FA4 MLA prefill requests for this fixed setup."""
95
-
96
- arch_family = _fa4_architecture_family_from_compute_capability(
97
- *torch.cuda.get_device_capability()
98
- )
99
- if not _supports_fa4_mla_prefill(ctx, arch_family):
100
- return
101
-
102
- from vllm.v1.attention.backends.fa_utils import (
103
- FlashAttentionCuTeDSLCompileSpec,
104
- )
105
-
106
- batch_size = FA4_MLA_PREFILL_COMPILE_BATCH_SIZE
107
- v_stride = None
108
- if not ctx.requires_v_padding:
109
- v_stride = (
110
- ctx.num_heads * ctx.kv_nope_head_dim,
111
- ctx.kv_nope_head_dim,
112
- 1,
113
- )
114
-
115
- for _, max_seqlen_q, max_seqlen_k in _shape_probes_for_context(ctx, arch_family):
116
- total_q_tokens = batch_size * max_seqlen_q
117
- total_kv_tokens = batch_size * max_seqlen_k
118
- for causal in FA4_MLA_PREFILL_CAUSAL_OPTIONS:
119
- for return_lse in FA4_MLA_PREFILL_LSE_OPTIONS:
120
- yield FlashAttentionCuTeDSLCompileSpec(
121
- q_shape=(total_q_tokens, ctx.num_heads, ctx.qk_head_dim),
122
- k_shape=(total_kv_tokens, ctx.num_heads, ctx.qk_head_dim),
123
- v_shape=(
124
- total_kv_tokens,
125
- ctx.num_heads,
126
- ctx.effective_v_head_dim,
127
- ),
128
- v_stride=v_stride,
129
- q_dtype=ctx.dtype,
130
- cu_seqlens_q_shape=(batch_size + 1,),
131
- cu_seqlens_k_shape=(batch_size + 1,),
132
- max_seqlen_q=max_seqlen_q,
133
- max_seqlen_k=max_seqlen_k,
134
- softmax_scale=ctx.scale,
135
- causal=causal,
136
- return_softmax_lse=return_lse,
137
- num_splits=ctx.num_splits,
138
- fa_version=ctx.fa_version,
139
- )
140
-
141
-
142
-# Pick one q/k point per current FA4 MLA-prefill shape regime.
143
-def _shape_probes_for_context(
144
- ctx: FA4MLAPrefillCompileContext,
145
- arch_family: FA4ArchitectureFamily,
146
-) -> tuple[tuple[str, int, int], ...]:
147
- q_stage1_q = 1
148
- q_stage2_q = FA4_MLA_PREFILL_Q_TILE + 1
149
- # FA4 never auto-splits when ceil(max_seqlen_k / tile_n) <= 4.
150
- no_split_k = 4 * FA4_MLA_PREFILL_K_TILE
151
- long_k = FA4_MLA_PREFILL_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE
152
- # Diff-head-dim Blackwell Split-KV switches tile_n at 64 K blocks.
153
- very_long_k = FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE
154
-
155
- base_probes = (
156
- ("q_stage1", q_stage1_q, FA4_MLA_PREFILL_K_TILE),
157
- ("q_stage2", q_stage2_q, no_split_k),
158
- )
159
- # SM120 currently rejects Split-KV in FA4; num_splits=1 also has no split
160
- # shape regimes on any architecture.
161
- if ctx.num_splits == 1 or arch_family == "sm120":
162
- return base_probes
163
-
164
- long_k_probes = (
165
- ("q_stage1_long_k", q_stage1_q, long_k),
166
- ("q_stage2_long_k", q_stage2_q, long_k),
167
- )
168
-
169
- # SM90 does not have the SM100 q_stage or diff-head-dim tile_n=64 branch.
170
- # Same-dim SM100-family MLA also does not need the very-long-K probe.
171
- if arch_family == "sm90" or ctx.qk_head_dim == ctx.effective_v_head_dim:
172
- return (*base_probes, *long_k_probes)
173
-
174
- very_long_k_probes = (
175
- ("q_stage1_very_long_k", q_stage1_q, very_long_k),
176
- ("q_stage2_very_long_k", q_stage2_q, very_long_k),
177
- )
178
- return (*base_probes, *long_k_probes, *very_long_k_probes)
179
-
180
-
181
-# Check whether this setup can use FA4 MLA prefill.
182
-def _supports_fa4_mla_prefill(
183
- ctx: FA4MLAPrefillCompileContext,
184
- arch_family: FA4ArchitectureFamily,
185
-) -> bool:
186
- return (
187
- ctx.dtype in FA4_STANDARD_DTYPES
188
- and ctx.num_heads > 0
189
- and (arch_family != "sm120" or ctx.num_splits == 1)
190
- )
191
-
192
-
193
-# Map CUDA capability to the FA4 arch family used by warmup checks.
194
-def _fa4_architecture_family_from_compute_capability(
195
- major: int,
196
- minor: int,
197
-) -> FA4ArchitectureFamily:
198
- if (major, minor) == (9, 0):
199
- return "sm90"
200
- if major == 10:
201
- return "sm100f"
202
- if (major, minor) == (12, 0):
203
- return "sm120"
204
- raise ValueError(f"FA4 warmup does not know CUDA capability {major}.{minor}")
vllm/model_executor/warmup/fa4_cutedsl_warmup.py
new
+30
@@ -0,0 +1,30 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Warm up FA4 CuTeDSL MLA prefill compile keys."""
4
+
5
+from __future__ import annotations
6
+
7
+from typing import TYPE_CHECKING
8
+
9
+from vllm.v1.attention.backends.mla.prefill import get_mla_prefill_backend
10
+
11
+if TYPE_CHECKING:
12
+ from vllm.v1.worker.gpu_worker import Worker
13
+
14
+
15
+def fa4_cutedsl_warmup(worker: Worker) -> None:
16
+ runner = worker.model_runner
17
+ if runner.is_pooling_model:
18
+ return
19
+
20
+ vllm_config = runner.vllm_config
21
+ if not vllm_config.model_config.use_mla:
22
+ return
23
+
24
+ backend_cls = get_mla_prefill_backend(vllm_config)
25
+ if backend_cls.get_name() != "FLASH_ATTN":
26
+ return
27
+
28
+ from vllm.v1.attention.backends.mla.prefill import flash_attn
29
+
30
+ flash_attn.FA4_MLA_PREFILL_KERNEL.warmup(vllm_config)
vllm/model_executor/warmup/jit_warmup.py
new
+474
@@ -0,0 +1,474 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Shared interfaces and tracing helpers for explicit JIT warmup keys."""
4
+
5
+from __future__ import annotations
6
+
7
+import ast
8
+import inspect
9
+import itertools
10
+import operator
11
+import textwrap
12
+from abc import ABC, abstractmethod
13
+from collections.abc import Callable, Iterable, Mapping
14
+from dataclasses import dataclass
15
+from typing import Any, Generic, TypeVar
16
+
17
+__all__ = [
18
+ "VllmJitKernel",
19
+ "WarmupIntRange",
20
+ "get_ast_full_name",
21
+ "get_function_source_node",
22
+ "zip_inputs",
23
+]
24
+
25
+
26
+CompileKeyT = TypeVar("CompileKeyT")
27
+
28
+
29
+@dataclass(frozen=True)
30
+class WarmupIntRange:
31
+ start: int
32
+ stop: int
33
+ step: int = 1
34
+
35
+
36
+WarmupValues = Any
37
+CompileKeyDispatchFn = Callable[..., CompileKeyT]
38
+
39
+
40
+@dataclass(frozen=True)
41
+class _WarmupInputRows:
42
+ """Warmup dispatch inputs expanded in lockstep."""
43
+
44
+ rows: tuple[Mapping[str, WarmupValues], ...]
45
+
46
+
47
+def _expand_warmup_values(values: WarmupValues) -> tuple[Any, ...]:
48
+ if isinstance(values, WarmupIntRange):
49
+ return tuple(range(values.start, values.stop, values.step))
50
+ if isinstance(values, (list, tuple)):
51
+ return tuple(values)
52
+ return (values,)
53
+
54
+
55
+def zip_inputs(*rows: Mapping[str, WarmupValues]) -> _WarmupInputRows:
56
+ """Group row-wise dispatch inputs that should be expanded in lockstep."""
57
+ if not rows:
58
+ raise ValueError("zip_inputs requires at least one dispatch input row")
59
+ if not all(isinstance(row, Mapping) for row in rows):
60
+ raise ValueError("zip_inputs rows must be mappings")
61
+
62
+ first_names = frozenset(rows[0])
63
+ if not first_names:
64
+ raise ValueError("zip_inputs rows require at least one dispatch input name")
65
+ if not all(isinstance(name, str) for name in first_names):
66
+ raise ValueError("zip_inputs dispatch input names must be strings")
67
+
68
+ input_rows: list[Mapping[str, WarmupValues]] = []
69
+ for row in rows:
70
+ names = frozenset(row)
71
+ if names != first_names:
72
+ raise ValueError("zip_inputs rows must use the same dispatch input names")
73
+ input_rows.append(dict(row))
74
+
75
+ return _WarmupInputRows(rows=tuple(input_rows))
76
+
77
+
78
+def _expand_warmup_value_grid(
79
+ values: Mapping[str, WarmupValues],
80
+ input_names: frozenset[str],
81
+) -> tuple[dict[str, Any], ...]:
82
+ names = tuple(name for name in values if name in input_names)
83
+ if not names:
84
+ return ({},)
85
+
86
+ expanded_values = tuple(_expand_warmup_values(values[name]) for name in names)
87
+ return tuple(
88
+ dict(zip(names, value_set)) for value_set in itertools.product(*expanded_values)
89
+ )
90
+
91
+
92
+def _expand_warmup_input_rows(
93
+ rows: tuple[Mapping[str, WarmupValues], ...],
94
+ input_names: frozenset[str],
95
+) -> tuple[dict[str, Any], ...]:
96
+ active_names = frozenset(name for name in rows[0] if name in input_names)
97
+ if not active_names:
98
+ return ({},)
99
+
100
+ return tuple(
101
+ {name: value for name, value in row.items() if name in active_names}
102
+ for row in rows
103
+ )
104
+
105
+
106
+def _merge_warmup_kwargs(parts: Iterable[Mapping[str, Any]]) -> dict[str, Any]:
107
+ merged: dict[str, Any] = {}
108
+ for part in parts:
109
+ for name, value in part.items():
110
+ if name in merged:
111
+ raise ValueError(
112
+ f"Warmup dispatch input '{name}' is specified more than once"
113
+ )
114
+ merged[name] = value
115
+ return merged
116
+
117
+
118
+@dataclass(frozen=True)
119
+class _CompileKeyDispatchTrace:
120
+ local_exprs: tuple[tuple[str, ast.AST], ...]
121
+ field_exprs: tuple[tuple[str, ast.AST], ...]
122
+ globals: Mapping[str, Any]
123
+ input_names: frozenset[str]
124
+ defaults: Mapping[str, Any]
125
+
126
+ def compile_key(
127
+ self,
128
+ compile_key_type: type[CompileKeyT],
129
+ kwargs: Mapping[str, Any],
130
+ ) -> CompileKeyT:
131
+ dispatch_values = {**self.defaults, **kwargs}
132
+ for name, expr in self.local_exprs:
133
+ dispatch_values[name] = _eval_dispatch_expr(
134
+ expr, dispatch_values, self.globals
135
+ )
136
+ return compile_key_type(
137
+ **{
138
+ field: _eval_dispatch_expr(expr, dispatch_values, self.globals)
139
+ for field, expr in self.field_exprs
140
+ }
141
+ )
142
+
143
+
144
+_BIN_OPS: dict[type[ast.operator], Callable[[Any, Any], Any]] = {
145
+ ast.Add: operator.add,
146
+ ast.Sub: operator.sub,
147
+ ast.Mult: operator.mul,
148
+ ast.FloorDiv: operator.floordiv,
149
+ ast.Mod: operator.mod,
150
+ ast.Pow: operator.pow,
151
+}
152
+_CMP_OPS: dict[type[ast.cmpop], Callable[[Any, Any], bool]] = {
153
+ ast.Eq: operator.eq,
154
+ ast.NotEq: operator.ne,
155
+ ast.Lt: operator.lt,
156
+ ast.LtE: operator.le,
157
+ ast.Gt: operator.gt,
158
+ ast.GtE: operator.ge,
159
+}
160
+
161
+
162
+def _dispatch_expr_source(node: ast.AST) -> str:
163
+ try:
164
+ return ast.unparse(node)
165
+ except Exception:
166
+ return ast.dump(node)
167
+
168
+
169
+def _dispatch_expr_error(node: ast.AST, reason: str) -> ValueError:
170
+ return ValueError(
171
+ f"{reason}: {_dispatch_expr_source(node)}. "
172
+ "Supported dispatch expressions are names, constants, attributes, "
173
+ "tuple/list literals, conditional expressions, comparisons, boolean "
174
+ "operators, unary not/minus, arithmetic, and calls without **kwargs."
175
+ )
176
+
177
+
178
+class _DispatchExprEvaluator(ast.NodeVisitor):
179
+ def __init__(
180
+ self,
181
+ values: Mapping[str, Any],
182
+ globals_: Mapping[str, Any],
183
+ ) -> None:
184
+ self.values = values
185
+ self.globals = globals_
186
+
187
+ def eval(self, node: ast.AST) -> Any:
188
+ return self.visit(node)
189
+
190
+ def generic_visit(self, node: ast.AST) -> Any:
191
+ raise _dispatch_expr_error(node, "Unsupported dispatch expression")
192
+
193
+ def visit_Name(self, node: ast.Name) -> Any:
194
+ if node.id in self.values:
195
+ return self.values[node.id]
196
+ if node.id in self.globals:
197
+ return self.globals[node.id]
198
+ raise _dispatch_expr_error(node, f"Unknown dispatch name '{node.id}'")
199
+
200
+ def visit_Constant(self, node: ast.Constant) -> Any:
201
+ return node.value
202
+
203
+ def visit_IfExp(self, node: ast.IfExp) -> Any:
204
+ return self.visit(node.body if self.visit(node.test) else node.orelse)
205
+
206
+ def visit_Tuple(self, node: ast.Tuple) -> tuple[Any, ...]:
207
+ return tuple(self.visit(elt) for elt in node.elts)
208
+
209
+ def visit_List(self, node: ast.List) -> list[Any]:
210
+ return [self.visit(elt) for elt in node.elts]
211
+
212
+ def visit_BoolOp(self, node: ast.BoolOp) -> Any:
213
+ if isinstance(node.op, ast.And):
214
+ result = None
215
+ for value in node.values:
216
+ result = self.visit(value)
217
+ if not result:
218
+ return result
219
+ return result
220
+ if isinstance(node.op, ast.Or):
221
+ result = None
222
+ for value in node.values:
223
+ result = self.visit(value)
224
+ if result:
225
+ return result
226
+ return result
227
+ raise _dispatch_expr_error(node, "Unsupported dispatch boolean operator")
228
+
229
+ def visit_Compare(self, node: ast.Compare) -> bool:
230
+ left = self.visit(node.left)
231
+ for op_node, comparator in zip(node.ops, node.comparators):
232
+ right = self.visit(comparator)
233
+ op = _CMP_OPS.get(type(op_node))
234
+ if op is None:
235
+ raise _dispatch_expr_error(
236
+ node, "Unsupported dispatch comparison operator"
237
+ )
238
+ if not op(left, right):
239
+ return False
240
+ left = right
241
+ return True
242
+
243
+ def visit_UnaryOp(self, node: ast.UnaryOp) -> Any:
244
+ operand = self.visit(node.operand)
245
+ if isinstance(node.op, ast.Not):
246
+ return not operand
247
+ if isinstance(node.op, ast.USub):
248
+ return -operand
249
+ raise _dispatch_expr_error(node, "Unsupported dispatch unary operator")
250
+
251
+ def visit_BinOp(self, node: ast.BinOp) -> Any:
252
+ op = _BIN_OPS.get(type(node.op))
253
+ if op is None:
254
+ raise _dispatch_expr_error(node, "Unsupported dispatch binary operator")
255
+ return op(self.visit(node.left), self.visit(node.right))
256
+
257
+ def visit_Call(self, node: ast.Call) -> Any:
258
+ args = [self.visit(arg) for arg in node.args]
259
+ fn = self.visit(node.func)
260
+ call_kwargs: dict[str, Any] = {}
261
+ for keyword in node.keywords:
262
+ if keyword.arg is None:
263
+ raise _dispatch_expr_error(
264
+ node, "Dispatch helper calls cannot use **kwargs"
265
+ )
266
+ call_kwargs[keyword.arg] = self.visit(keyword.value)
267
+ return fn(*args, **call_kwargs)
268
+
269
+ def visit_Attribute(self, node: ast.Attribute) -> Any:
270
+ return getattr(self.visit(node.value), node.attr)
271
+
272
+
273
+def get_ast_full_name(node: ast.AST) -> str | None:
274
+ if isinstance(node, ast.Name):
275
+ return node.id
276
+ if isinstance(node, ast.Attribute):
277
+ parent = get_ast_full_name(node.value)
278
+ if parent is not None:
279
+ return f"{parent}.{node.attr}"
280
+ return None
281
+
282
+
283
+def get_function_source_node(fn: Callable[..., Any]) -> ast.FunctionDef:
284
+ source_fn = getattr(fn, "fn", fn)
285
+ source = textwrap.dedent(inspect.getsource(source_fn))
286
+ tree = ast.parse(source)
287
+ function_defs = [node for node in tree.body if isinstance(node, ast.FunctionDef)]
288
+ if len(function_defs) != 1:
289
+ name = getattr(source_fn, "__name__", type(source_fn).__name__)
290
+ raise ValueError(f"Expected one function in {name}, found {len(function_defs)}")
291
+ return function_defs[0]
292
+
293
+
294
+def _eval_dispatch_expr(
295
+ node: ast.AST,
296
+ kwargs: Mapping[str, Any],
297
+ globals_: Mapping[str, Any],
298
+) -> Any:
299
+ return _DispatchExprEvaluator(kwargs, globals_).eval(node)
300
+
301
+
302
+def _collect_input_names(
303
+ node: ast.AST,
304
+ candidate_names: set[str],
305
+ local_names: set[str] | None = None,
306
+) -> set[str]:
307
+ if local_names is None:
308
+ local_names = set()
309
+ return {
310
+ child.id
311
+ for child in ast.walk(node)
312
+ if (
313
+ isinstance(child, ast.Name)
314
+ and child.id in candidate_names
315
+ and child.id not in local_names
316
+ )
317
+ }
318
+
319
+
320
+def _collect_dispatch_body(
321
+ fn: CompileKeyDispatchFn[Any],
322
+ function_def: ast.FunctionDef,
323
+) -> tuple[list[tuple[str, ast.AST]], ast.Call]:
324
+ local_exprs: list[tuple[str, ast.AST]] = []
325
+ for statement in function_def.body:
326
+ if (
327
+ isinstance(statement, ast.Expr)
328
+ and isinstance(statement.value, ast.Constant)
329
+ and isinstance(statement.value.value, str)
330
+ ):
331
+ continue
332
+
333
+ if isinstance(statement, ast.Assign):
334
+ if len(statement.targets) != 1 or not isinstance(
335
+ statement.targets[0], ast.Name
336
+ ):
337
+ raise _dispatch_expr_error(
338
+ statement, "Dispatch assignments must target one local name"
339
+ )
340
+ local_exprs.append((statement.targets[0].id, statement.value))
341
+ continue
342
+
343
+ if isinstance(statement, ast.AnnAssign):
344
+ if statement.value is None:
345
+ raise _dispatch_expr_error(
346
+ statement, "Dispatch annotations must assign a value"
347
+ )
348
+ if not isinstance(statement.target, ast.Name):
349
+ raise _dispatch_expr_error(
350
+ statement, "Dispatch assignments must target one local name"
351
+ )
352
+ local_exprs.append((statement.target.id, statement.value))
353
+ continue
354
+
355
+ if isinstance(statement, ast.Return) and isinstance(statement.value, ast.Call):
356
+ return local_exprs, statement.value
357
+
358
+ if isinstance(statement, ast.Return):
359
+ raise _dispatch_expr_error(
360
+ statement, "Dispatch must return one CompileKey(...) call"
361
+ )
362
+
363
+ raise _dispatch_expr_error(
364
+ statement,
365
+ "Dispatch may only contain local assignments before CompileKey return",
366
+ )
367
+
368
+ raise ValueError(f"Expected {fn.__name__} to return one CompileKey(...) call")
369
+
370
+
371
+def _trace_compile_key_dispatch(
372
+ fn: CompileKeyDispatchFn[Any],
373
+) -> _CompileKeyDispatchTrace:
374
+ source_fn = getattr(fn, "__func__", fn)
375
+ globals_ = source_fn.__globals__
376
+ function_def = get_function_source_node(fn)
377
+
378
+ local_exprs, return_call = _collect_dispatch_body(fn, function_def)
379
+
380
+ field_exprs: list[tuple[str, ast.AST]] = []
381
+ signature = inspect.signature(fn)
382
+ defaults = {
383
+ name: parameter.default
384
+ for name, parameter in signature.parameters.items()
385
+ if parameter.default is not inspect.Parameter.empty
386
+ }
387
+ candidate_names = set(signature.parameters)
388
+ input_names: set[str] = set()
389
+ local_names = {name for name, _ in local_exprs}
390
+ for _, expr in local_exprs:
391
+ input_names.update(_collect_input_names(expr, candidate_names))
392
+ for keyword in return_call.keywords:
393
+ if keyword.arg is None:
394
+ raise ValueError(f"{fn.__name__} cannot use **kwargs in CompileKey")
395
+ field_exprs.append((keyword.arg, keyword.value))
396
+ input_names.update(
397
+ _collect_input_names(keyword.value, candidate_names, local_names)
398
+ )
399
+
400
+ return _CompileKeyDispatchTrace(
401
+ tuple(local_exprs),
402
+ tuple(field_exprs),
403
+ globals_,
404
+ frozenset(input_names),
405
+ defaults,
406
+ )
407
+
408
+
409
+class VllmJitKernel(Generic[CompileKeyT], ABC):
410
+ """Kernel wrapper that owns dispatch, warmup keys, and compilation."""
411
+
412
+ CompileKey: type[CompileKeyT]
413
+
414
+ def __init__(self) -> None:
415
+ self.compile_key_dispatch_trace = _trace_compile_key_dispatch(self.dispatch)
416
+
417
+ def compile_key(self, kwargs: Mapping[str, Any]) -> CompileKeyT:
418
+ return self.compile_key_dispatch_trace.compile_key(self.CompileKey, kwargs)
419
+
420
+ def _trace_dispatch(
421
+ self, dispatch: CompileKeyDispatchFn[CompileKeyT]
422
+ ) -> Callable[..., list[CompileKeyT]]:
423
+ compile_key_dispatch_trace = _trace_compile_key_dispatch(dispatch)
424
+
425
+ def traced(
426
+ *input_groups: _WarmupInputRows,
427
+ **kwargs: WarmupValues,
428
+ ) -> list[CompileKeyT]:
429
+ for group in input_groups:
430
+ if not isinstance(group, _WarmupInputRows):
431
+ raise TypeError(
432
+ "_trace_dispatch positional arguments must be "
433
+ "zip_inputs(...) groups"
434
+ )
435
+ expanded_input_groups = tuple(
436
+ _expand_warmup_input_rows(
437
+ group.rows, compile_key_dispatch_trace.input_names
438
+ )
439
+ for group in input_groups
440
+ )
441
+ expanded_kwargs = _expand_warmup_value_grid(
442
+ kwargs, compile_key_dispatch_trace.input_names
443
+ )
444
+ dispatch_value_groups = (*expanded_input_groups, expanded_kwargs)
445
+ return list(
446
+ dict.fromkeys(
447
+ compile_key_dispatch_trace.compile_key(
448
+ self.CompileKey, _merge_warmup_kwargs(dispatch_values)
449
+ )
450
+ for dispatch_values in itertools.product(*dispatch_value_groups)
451
+ )
452
+ )
453
+
454
+ return traced
455
+
456
+ @abstractmethod
457
+ def dispatch(self, **kwargs: Any) -> CompileKeyT:
458
+ """Build one compile key from one concrete dispatch point."""
459
+ raise NotImplementedError
460
+
461
+ @abstractmethod
462
+ def get_warmup_keys(self, *args: Any, **kwargs: Any) -> list[CompileKeyT]:
463
+ """Return compile keys that should be warmed for this kernel."""
464
+ raise NotImplementedError
465
+
466
+ @abstractmethod
467
+ def compile(self, compile_key: CompileKeyT) -> None:
468
+ """Compile one warmup key."""
469
+ raise NotImplementedError
470
+
471
+ def warmup(self, *args: Any, **kwargs: Any) -> None:
472
+ """Compile this kernel's warmup keys."""
473
+ for compile_key in self.get_warmup_keys(*args, **kwargs):
474
+ self.compile(compile_key)
vllm/model_executor/warmup/jit_warmup_triton_helper.py
new
+183
@@ -0,0 +1,183 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import ast
4
+import inspect
5
+from collections.abc import Callable
6
+from dataclasses import dataclass
7
+from typing import Any
8
+
9
+from vllm.model_executor.warmup.jit_warmup import (
10
+ get_ast_full_name,
11
+ get_function_source_node,
12
+)
13
+
14
+
15
+@dataclass(frozen=True)
16
+class TritonWarmupTensor:
17
+ # Compile-only tensor descriptor for Triton pointer specialization.
18
+ dtype: Any
19
+ aligned: bool = True
20
+ shape: tuple[int, ...] = (1,)
21
+
22
+ def data_ptr(self) -> int:
23
+ return 0 if self.aligned else 1
24
+
25
+ def ptr_range(self) -> int:
26
+ return 0
27
+
28
+ def stride(self) -> tuple[int, ...]:
29
+ strides: list[int] = []
30
+ stride = 1
31
+ for size in reversed(self.shape):
32
+ strides.append(stride)
33
+ stride *= size
34
+ return tuple(reversed(strides))
35
+
36
+
37
+@dataclass(frozen=True)
38
+class TritonPointerInputVariant:
39
+ # Named pointer-alignment variant for compile-only Triton warmup.
40
+ alignments: tuple[tuple[str, bool], ...]
41
+
42
+ @classmethod
43
+ def from_alignment(cls, **aligned: bool) -> "TritonPointerInputVariant":
44
+ return cls(tuple(aligned.items()))
45
+
46
+ def is_aligned(self, name: str) -> bool:
47
+ for alignment_name, aligned in self.alignments:
48
+ if alignment_name == name:
49
+ return aligned
50
+ raise KeyError(f"Unknown Triton pointer input variant: {name}")
51
+
52
+ def pointer(
53
+ self,
54
+ name: str,
55
+ dtype: Any,
56
+ shape: tuple[int, ...] = (1,),
57
+ ) -> TritonWarmupTensor:
58
+ return TritonWarmupTensor(dtype, aligned=self.is_aligned(name), shape=shape)
59
+
60
+
61
+def _literal_str_refs(node: ast.AST) -> tuple[str | int, ...]:
62
+ if isinstance(node, ast.Constant) and isinstance(node.value, str | int):
63
+ return (node.value,)
64
+ if isinstance(node, ast.List | ast.Tuple):
65
+ refs: list[str | int] = []
66
+ for elt in node.elts:
67
+ if isinstance(elt, ast.Constant) and isinstance(elt.value, str | int):
68
+ refs.append(elt.value)
69
+ else:
70
+ raise ValueError(
71
+ f"Unsupported Triton specialization ref: {ast.dump(elt)}"
72
+ )
73
+ return tuple(refs)
74
+ raise ValueError(f"Unsupported Triton specialization refs: {ast.dump(node)}")
75
+
76
+
77
+def _normalize_arg_refs(
78
+ refs: tuple[str | int, ...],
79
+ arg_names: tuple[str, ...],
80
+) -> frozenset[str]:
81
+ names: set[str] = set()
82
+ for ref in refs:
83
+ if isinstance(ref, int):
84
+ names.add(arg_names[ref])
85
+ else:
86
+ names.add(ref)
87
+ return frozenset(names)
88
+
89
+
90
+def _decorator_keyword_refs(
91
+ function_def: ast.FunctionDef,
92
+ keyword_name: str,
93
+) -> tuple[str | int, ...]:
94
+ for decorator in function_def.decorator_list:
95
+ if not isinstance(decorator, ast.Call):
96
+ continue
97
+ decorator_name = get_ast_full_name(decorator.func)
98
+ if decorator_name not in ("triton.jit", "jit"):
99
+ continue
100
+ for keyword in decorator.keywords:
101
+ if keyword.arg == keyword_name:
102
+ return _literal_str_refs(keyword.value)
103
+ return ()
104
+
105
+
106
+def _triton_do_not_specialize_args(
107
+ kernel: Callable[..., Any],
108
+ function_def: ast.FunctionDef,
109
+ arg_names: tuple[str, ...],
110
+) -> frozenset[str]:
111
+ refs = getattr(kernel, "do_not_specialize", None)
112
+ if refs is not None:
113
+ return _normalize_arg_refs(tuple(refs), arg_names)
114
+ return _normalize_arg_refs(
115
+ _decorator_keyword_refs(function_def, "do_not_specialize"),
116
+ arg_names,
117
+ )
118
+
119
+
120
+def _triton_constexpr_arg_names(
121
+ kernel: Callable[..., Any],
122
+ function_def: ast.FunctionDef,
123
+ arg_names: tuple[str, ...],
124
+) -> frozenset[str]:
125
+ constexprs = getattr(kernel, "constexprs", None)
126
+ if constexprs is not None:
127
+ return frozenset(arg_names[index] for index in constexprs)
128
+
129
+ names: set[str] = set()
130
+ for arg in function_def.args.args + function_def.args.kwonlyargs:
131
+ if arg.annotation is None:
132
+ continue
133
+ annotation = get_ast_full_name(arg.annotation)
134
+ if annotation in ("tl.constexpr", "triton.language.constexpr", "constexpr"):
135
+ names.add(arg.arg)
136
+ return frozenset(names)
137
+
138
+
139
+def _leftmost_name(node: ast.AST) -> str | None:
140
+ if isinstance(node, ast.Name):
141
+ return node.id
142
+ if isinstance(node, ast.BinOp):
143
+ return _leftmost_name(node.left)
144
+ return None
145
+
146
+
147
+def _pointer_arg_names(
148
+ function_def: ast.FunctionDef,
149
+ arg_names: tuple[str, ...],
150
+) -> frozenset[str]:
151
+ candidate_names = set(arg_names)
152
+ pointer_names = {name for name in arg_names if name.endswith("_ptr")}
153
+ for node in ast.walk(function_def):
154
+ if not isinstance(node, ast.Call):
155
+ continue
156
+ if get_ast_full_name(node.func) not in ("tl.load", "tl.store"):
157
+ continue
158
+ if not node.args:
159
+ continue
160
+ name = _leftmost_name(node.args[0])
161
+ if name in candidate_names:
162
+ pointer_names.add(name)
163
+ return frozenset(pointer_names)
164
+
165
+
166
+def trace_triton_kernel_specialization_args(
167
+ kernel: Callable[..., Any],
168
+) -> tuple[str, ...]:
169
+ function_def = get_function_source_node(kernel)
170
+ source_fn = getattr(kernel, "fn", kernel)
171
+ arg_names = tuple(inspect.signature(source_fn).parameters)
172
+ constexpr_args = _triton_constexpr_arg_names(kernel, function_def, arg_names)
173
+ do_not_specialize_args = _triton_do_not_specialize_args(
174
+ kernel, function_def, arg_names
175
+ )
176
+ pointer_args = _pointer_arg_names(function_def, arg_names)
177
+
178
+ return tuple(
179
+ name
180
+ for name in arg_names
181
+ if name in constexpr_args
182
+ or (name not in pointer_args and name not in do_not_specialize_args)
183
+ )
vllm/model_executor/warmup/kernel_warmup.py
+11
-2
@@ -17,6 +17,9 @@ from vllm.model_executor.warmup.deep_gemm_warmup import deep_gemm_warmup
17
from vllm.model_executor.warmup.deepseek_v4_mhc_warmup import (
18
deepseek_v4_mhc_warmup,
19
)
20
+from vllm.model_executor.warmup.fa4_cutedsl_warmup import (
21
+ fa4_cutedsl_warmup,
22
+)
23
from vllm.model_executor.warmup.flashinfer_autotune_cache import (
24
resolve_flashinfer_autotune_file,
25
write_flashinfer_autotune_cache,
@@ -27,7 +30,7 @@ from vllm.model_executor.warmup.flashinfer_sparse_mla_warmup import (
30
)
31
from vllm.model_executor.warmup.qwen_triton_warmup import qwen_triton_warmup
32
from vllm.model_executor.warmup.sparse_mla_triton_warmup import (
30
- sparse_mla_triton_warmup_if_needed,
33
+ sparse_mla_triton_warmup,
34
)
35
from vllm.model_executor.warmup.v1_block_table_warmup import (
36
warm_v1_block_table_kernels,
@@ -94,7 +97,6 @@ def kernel_warmup(worker: "Worker"):
97
)
98
99
# Run next so input-prep kernels JIT against pristine runner state.
97
- sparse_mla_triton_warmup_if_needed(worker)
100
flashinfer_sparse_mla_decode_autotune_warmup(worker)
101
deepseek_v4_sparse_mla_attention_warmup(worker)
102
@@ -156,8 +158,15 @@ def kernel_warmup(worker: "Worker"):
158
)
159
160
if worker.vllm_config.kernel_config.enable_cutedsl_warmup:
161
+ # TODO(roberto): Remove after registered CuTeDSL warmups are migrated
162
+ # to the shared JIT warmup infrastructure.
163
+ # https://github.com/vllm-project/vllm/pull/47451
164
cutedsl_warmup()
165
166
+ if worker.vllm_config.kernel_config.enable_jit_warmup:
167
+ fa4_cutedsl_warmup(worker)
168
+ sparse_mla_triton_warmup(worker)
169
+
170
171
def _flashinfer_autotune_skip_ops(runner: "GPUModelRunner") -> set[str] | None:
172
if envs.VLLM_FLASHINFER_AUTOTUNE_SKIP_OPS is not None:
vllm/model_executor/warmup/sparse_mla_triton_warmup.py
+27
-253
@@ -4,11 +4,10 @@
4
5
from typing import TYPE_CHECKING
6
7
-import torch
8
-
7
from vllm.logger import init_logger
8
9
if TYPE_CHECKING:
10
+ from vllm.config import VllmConfig
11
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
12
from vllm.v1.worker.gpu_worker import Worker
13
@@ -31,47 +30,7 @@ _GENERIC_SPARSE_MLA_BACKENDS = frozenset(
30
)
31
_INDEXER_PREFILL_CHUNK_METADATA_BACKENDS = frozenset({"DEEPSEEK_V32_INDEXER"})
32
34
-_SPARSE_PREFILL_METADATA_NUM_PREFILLS = (1, 2, 4, 8)
35
-_SPARSE_PREFILL_METADATA_NUM_DECODES = (0, 1, 2)
36
-_DSV4_PREFILL_CHUNK_METADATA_COMPRESS_RATIOS = (4, 128)
37
-_PREFILL_CHUNK_METADATA_SEQ_LEN_MULTIPLIERS = (2, 3)
38
-_PREFILL_CHUNK_METADATA_QUERY_SLICE_OFFSETS = (
39
- # query_slice_start offset, query_slice_stop offset
40
- (0, 0),
41
- (0, -1),
42
- (1, 0),
43
- (1, -1),
44
-)
45
-_COMBINE_TOPK_SWA_INPUT_VARIANTS = (
46
- # offset_topk, offset_query_and_seq, offset_gather
47
- (False, False, False),
48
- (False, True, False),
49
- (True, True, True),
50
-)
51
-_DSV4_COMBINE_TOPK_SWA_WARMUP_CASES = (
52
- # compress_ratio, topk, topk_width, N
53
- (1, 0, 512, 512),
54
- (4, 512, 512, 512 * 4),
55
- # DSv4-Pro C4A traffic uses top-k 1024 with N=1024.
56
- (4, 1024, 1024, 1024),
57
- (128, 8192, 8192, 8192 * 128),
58
- # Real C128A traffic also specializes N=1 in one call path.
59
- (128, 8192, 8192, 1),
60
-)
61
-
62
-
63
-def _clamp_warmup_tokens(num_tokens: int, max_tokens: int) -> int:
64
- return max(0, min(num_tokens, max_tokens))
65
-
66
-
67
-def _next_power_of_2(x: int) -> int:
68
- return 1 << (x - 1).bit_length()
69
-
70
-
71
-def _hf_config_int(runner: "GPUModelRunner", name: str, default: int) -> int:
72
- model_config = getattr(runner.vllm_config, "model_config", None)
73
- hf_config = getattr(model_config, "hf_config", None)
74
- return int(getattr(hf_config, name, default) or default)
33
+_INDEXER_PREFILL_CHUNK_METADATA_BACKENDS = frozenset({"DEEPSEEK_V32_INDEXER"})
34
35
36
def _attention_backend_name(backend: object) -> str | None:
@@ -96,242 +55,57 @@ def _has_attention_backend(
55
return False
56
57
99
-def _warm_sparse_swa_prefill_metadata_kernel(
100
- device: torch.device,
101
- window_size: int,
102
- prefill_tokens: int,
58
+def _compile_sparse_swa_prefill_metadata_kernel(
59
+ vllm_config: "VllmConfig",
60
) -> None:
61
from vllm.v1.attention.backends.mla.sparse_swa import (
105
- _compute_prefill_metadata_kernel,
62
+ _COMPUTE_PREFILL_METADATA_KERNEL,
63
)
64
108
- for num_prefills in _SPARSE_PREFILL_METADATA_NUM_PREFILLS:
109
- for num_decodes in _SPARSE_PREFILL_METADATA_NUM_DECODES:
110
- query_lens = [1] * num_decodes
111
- query_lens += [prefill_tokens] * num_prefills
112
- query_start_locs = [0]
113
- for query_len in query_lens:
114
- query_start_locs.append(query_start_locs[-1] + query_len)
115
- query_start_loc = torch.tensor(
116
- query_start_locs,
117
- dtype=torch.int32,
118
- device=device,
119
- )
120
- seq_lens = torch.tensor(
121
- [1] * num_decodes + [window_size + q for q in query_lens[num_decodes:]],
122
- dtype=torch.int32,
123
- device=device,
124
- )
125
- prefill_gather_lens = torch.empty(
126
- num_prefills, dtype=torch.int32, device=device
127
- )
128
- _compute_prefill_metadata_kernel[(1,)](
129
- prefill_gather_lens,
130
- seq_lens,
131
- query_start_loc,
132
- num_prefills,
133
- num_decodes,
134
- window_size,
135
- BLOCK_SIZE=_next_power_of_2(num_prefills),
136
- )
65
+ _COMPUTE_PREFILL_METADATA_KERNEL.warmup(vllm_config)
66
67
139
-def _warm_prefill_chunk_metadata_kernel(
140
- device: torch.device,
141
- compress_ratio: int,
142
- query_len: int,
68
+def _compile_prefill_chunk_metadata_kernel(
69
+ vllm_config: "VllmConfig",
70
) -> None:
144
- from vllm.v1.attention.backends.mla.indexer import build_prefill_chunk_metadata
145
-
146
- num_reqs = 2
147
- query_start_loc_cpu = torch.arange(
148
- 0, (num_reqs + 1) * query_len, query_len, dtype=torch.int32
71
+ from vllm.v1.attention.backends.mla.indexer import (
72
+ _BUILD_PREFILL_CHUNK_METADATA_KERNEL,
73
)
150
- query_start_loc = query_start_loc_cpu.to(device=device)
74
152
- uncompressed_seq_lens_cpu = torch.tensor(
153
- [
154
- compress_ratio * multiplier + query_len
155
- for multiplier in _PREFILL_CHUNK_METADATA_SEQ_LEN_MULTIPLIERS
156
- ],
157
- dtype=torch.int32,
158
- )
159
- compressed_seq_lens_cpu = uncompressed_seq_lens_cpu // compress_ratio
160
- uncompressed_seq_lens = uncompressed_seq_lens_cpu.to(device=device)
161
- compressed_seq_lens = compressed_seq_lens_cpu.to(device=device)
162
- block_table = torch.zeros(
163
- (num_reqs, int(compressed_seq_lens_cpu.max().item())),
164
- dtype=torch.int32,
165
- device=device,
166
- )
167
-
168
- offset_uncompressed_seq_lens = torch.empty(
169
- num_reqs + 1, dtype=torch.int32, device=device
170
- )[1:]
171
- offset_uncompressed_seq_lens.copy_(uncompressed_seq_lens)
172
- query_slices = tuple(
173
- slice(start, num_reqs * query_len + stop)
174
- for start, stop in _PREFILL_CHUNK_METADATA_QUERY_SLICE_OFFSETS
175
- )
176
- for warmup_uncompressed_seq_lens in (
177
- uncompressed_seq_lens,
178
- offset_uncompressed_seq_lens,
179
- ):
180
- for query_slice in query_slices:
181
- build_prefill_chunk_metadata(
182
- 0,
183
- num_reqs,
184
- query_start_loc,
185
- query_start_loc_cpu,
186
- warmup_uncompressed_seq_lens,
187
- compressed_seq_lens,
188
- compressed_seq_lens_cpu,
189
- block_table,
190
- compress_ratio,
191
- query_slice=query_slice,
192
- )
75
+ _BUILD_PREFILL_CHUNK_METADATA_KERNEL.warmup(vllm_config)
76
77
195
-def _warm_combine_topk_swa_indices_kernel(
196
- device: torch.device,
197
- num_tokens: int,
198
- window_size: int,
199
- compress_ratio: int,
200
- topk: int,
201
- topk_width: int,
202
- n: int,
78
+def _compile_combine_topk_swa_indices_kernel(
79
+ vllm_config: "VllmConfig",
80
) -> None:
204
- from vllm.models.deepseek_v4.common.ops.cache_utils import combine_topk_swa_indices
205
-
206
- if num_tokens <= 0:
207
- return
208
-
209
- def _make_topk_indices(*, offset: bool) -> torch.Tensor:
210
- if offset:
211
- topk_storage = torch.full(
212
- (num_tokens * topk_width + 1,),
213
- -1,
214
- dtype=torch.int32,
215
- device=device,
216
- )
217
- topk_indices = topk_storage[1:].reshape(num_tokens, topk_width)
218
- else:
219
- topk_indices = torch.full(
220
- (num_tokens, topk_width), -1, dtype=torch.int32, device=device
221
- )
222
- if topk > 0:
223
- topk_indices.copy_(
224
- torch.arange(num_tokens * topk_width, dtype=torch.int32, device=device)
225
- .reshape(num_tokens, topk_width)
226
- .remainder(topk_width)
227
- )
228
- return topk_indices
229
-
230
- query_start_loc = torch.tensor([0, num_tokens], dtype=torch.int32, device=device)
231
- seq_lens = torch.tensor(
232
- [window_size + num_tokens], dtype=torch.int32, device=device
81
+ from vllm.models.deepseek_v4.common.ops.cache_utils import (
82
+ _COMBINE_TOPK_SWA_INDICES_KERNEL,
83
)
234
- gather_lens = torch.tensor(
235
- [min(window_size + num_tokens, window_size + num_tokens - 1)],
236
- dtype=torch.int32,
237
- device=device,
238
- )
239
- offset_query_start_loc = torch.empty(3, dtype=torch.int32, device=device)[1:]
240
- offset_query_start_loc.copy_(query_start_loc)
241
- offset_seq_lens = torch.empty(2, dtype=torch.int32, device=device)[1:]
242
- offset_seq_lens.copy_(seq_lens)
243
- offset_gather_lens = torch.empty(2, dtype=torch.int32, device=device)[1:]
244
- offset_gather_lens.copy_(gather_lens)
245
-
246
- for (
247
- offset_topk,
248
- offset_query_and_seq,
249
- offset_gather,
250
- ) in _COMBINE_TOPK_SWA_INPUT_VARIANTS:
251
- warmup_topk_indices = _make_topk_indices(offset=offset_topk)
252
- warmup_query_start_loc = (
253
- offset_query_start_loc if offset_query_and_seq else query_start_loc
254
- )
255
- warmup_seq_lens = offset_seq_lens if offset_query_and_seq else seq_lens
256
- warmup_gather_lens = offset_gather_lens if offset_gather else gather_lens
257
- n_values = (n,) if n == 1 else (n, n + 1)
258
- for m in (window_size + num_tokens, topk_width):
259
- for n_value in n_values:
260
- combine_topk_swa_indices(
261
- warmup_topk_indices,
262
- warmup_query_start_loc,
263
- warmup_seq_lens,
264
- warmup_gather_lens,
265
- window_size,
266
- compress_ratio,
267
- topk,
268
- M=m,
269
- N=n_value,
270
- )
271
-
272
-
273
-@torch.inference_mode()
274
-def sparse_mla_triton_warmup(
275
- runner: "GPUModelRunner",
276
- num_tokens: int,
277
- *,
278
- compress_ratios: tuple[int, ...],
279
- combine_topk_swa_cases: tuple[tuple[int, int, int, int], ...] = (),
280
-) -> None:
281
- device = getattr(runner, "device", torch.device("cuda"))
282
- window_size = _hf_config_int(runner, "sliding_window", 128)
283
-
284
- _warm_sparse_swa_prefill_metadata_kernel(device, window_size, num_tokens)
285
- for compress_ratio in compress_ratios:
286
- _warm_prefill_chunk_metadata_kernel(device, compress_ratio, num_tokens)
287
- for compress_ratio, topk, topk_width, n in combine_topk_swa_cases:
288
- _warm_combine_topk_swa_indices_kernel(
289
- device,
290
- num_tokens,
291
- window_size,
292
- compress_ratio,
293
- topk,
294
- topk_width,
295
- n,
296
- )
297
-
84
299
-def deepseek_v4_sparse_triton_warmup(
300
- runner: "GPUModelRunner",
301
- num_tokens: int,
302
-) -> None:
303
- sparse_mla_triton_warmup(
304
- runner,
305
- num_tokens,
306
- compress_ratios=_DSV4_PREFILL_CHUNK_METADATA_COMPRESS_RATIOS,
307
- combine_topk_swa_cases=_DSV4_COMBINE_TOPK_SWA_WARMUP_CASES,
308
- )
85
+ _COMBINE_TOPK_SWA_INDICES_KERNEL.warmup(vllm_config)
86
87
311
-def sparse_mla_triton_warmup_if_needed(worker: "Worker") -> None:
88
+def sparse_mla_triton_warmup(worker: "Worker") -> None:
89
runner = worker.model_runner
90
if runner.is_pooling_model:
91
return
92
93
max_tokens = worker.scheduler_config.max_num_batched_tokens
317
- num_tokens = _clamp_warmup_tokens(8, max_tokens)
318
- if num_tokens <= 0:
94
+ max_num_prefills = min(worker.scheduler_config.max_num_seqs, max_tokens)
95
+ if max_tokens <= 0 or max_num_prefills <= 0:
96
return
97
98
+ vllm_config = runner.vllm_config
99
try:
100
if _has_attention_backend(runner, _DEEPSEEK_V4_SPARSE_MLA_BACKENDS):
323
- deepseek_v4_sparse_triton_warmup(runner, num_tokens)
101
+ _compile_sparse_swa_prefill_metadata_kernel(vllm_config)
102
+ _compile_prefill_chunk_metadata_kernel(vllm_config)
103
+ _compile_combine_topk_swa_indices_kernel(vllm_config)
104
elif _has_attention_backend(runner, _GENERIC_SPARSE_MLA_BACKENDS):
325
- sparse_mla_triton_warmup(
326
- runner,
327
- num_tokens,
328
- compress_ratios=(1,),
329
- )
105
+ _compile_sparse_swa_prefill_metadata_kernel(vllm_config)
106
+ _compile_prefill_chunk_metadata_kernel(vllm_config)
107
elif _has_attention_backend(runner, _INDEXER_PREFILL_CHUNK_METADATA_BACKENDS):
331
- _warm_prefill_chunk_metadata_kernel(
332
- getattr(runner, "device", torch.device("cuda")),
333
- compress_ratio=1,
334
- query_len=num_tokens,
335
- )
108
+ _compile_prefill_chunk_metadata_kernel(vllm_config)
109
+
110
except Exception:
111
logger.warning("Skipping sparse MLA Triton warmup.", exc_info=True)
vllm/models/deepseek_v4/common/ops/cache_utils.py
+239
-71
@@ -14,14 +14,23 @@ preparation.
14
window indices for sparse prefill.
15
"""
16
17
+from dataclasses import dataclass
18
+from typing import Any
19
+
20
import torch
21
22
from vllm.model_executor.layers.quantization.utils.quant_utils import (
23
get_fp8_min_max,
24
)
25
+from vllm.model_executor.warmup.jit_warmup import VllmJitKernel, zip_inputs
26
+from vllm.model_executor.warmup.jit_warmup_triton_helper import (
27
+ TritonPointerInputVariant,
28
+ TritonWarmupTensor,
29
+)
30
from vllm.platforms import current_platform
31
from vllm.triton_utils import tl, triton
32
from vllm.utils.import_utils import has_cutedsl
33
+from vllm.utils.math_utils import next_power_of_2
34
35
36
@triton.jit
@@ -528,7 +537,6 @@ def combine_topk_swa_indices(
537
N: int,
538
) -> tuple[torch.Tensor, torch.Tensor]:
539
num_tokens = topk_indices.shape[0]
531
- num_reqs = seq_lens.shape[0]
540
combined_topk = (
541
(topk + window_size + _SPARSE_PREFILL_TOPK_ALIGNMENT - 1)
542
// _SPARSE_PREFILL_TOPK_ALIGNMENT
@@ -544,13 +552,10 @@ def combine_topk_swa_indices(
552
num_tokens, dtype=torch.int32, device=topk_indices.device
553
)
554
547
- NUM_WORKERS = 128
548
- _combine_topk_swa_indices_kernel[(num_reqs, NUM_WORKERS)](
555
+ _COMBINE_TOPK_SWA_INDICES_KERNEL(
556
combined_indices,
550
- combined_indices.stride(0),
557
combined_lens,
558
topk_indices,
553
- topk_indices.stride(0),
559
query_start_loc,
560
seq_lens,
561
gather_lens,
@@ -559,82 +564,245 @@ def combine_topk_swa_indices(
564
TOP_K=topk,
565
COMPRESS_RATIO=compress_ratio,
566
WINDOW_SIZE=window_size,
562
- PADDED_TOP_K=triton.next_power_of_2(topk_indices.shape[-1]),
567
)
568
return combined_indices, combined_lens
569
570
567
-@triton.jit
568
-def _combine_topk_swa_indices_kernel(
569
- combined_indices_ptr,
570
- combined_indices_stride,
571
- combined_lens_ptr,
572
- topk_indices_ptr,
573
- topk_indices_stride,
574
- query_start_loc_ptr,
575
- seq_lens_ptr,
576
- gather_lens_ptr,
577
- M,
578
- N,
579
- TOP_K: tl.constexpr,
580
- COMPRESS_RATIO: tl.constexpr,
581
- WINDOW_SIZE: tl.constexpr,
582
- PADDED_TOP_K: tl.constexpr,
571
+_COMBINE_TOPK_SWA_NUM_WORKERS = 128
572
+
573
+
574
+# Representative pointer alignment variants for Triton pointer specialization.
575
+_COMBINE_TOPK_SWA_POINTER_INPUTS = zip_inputs(
576
+ dict(
577
+ topk_indices=True,
578
+ query_start_loc=True,
579
+ seq_lens=True,
580
+ gather_lens=True,
581
+ ),
582
+ dict(
583
+ topk_indices=True,
584
+ query_start_loc=False,
585
+ seq_lens=False,
586
+ gather_lens=True,
587
+ ),
588
+ dict(
589
+ topk_indices=False,
590
+ query_start_loc=False,
591
+ seq_lens=False,
592
+ gather_lens=False,
593
+ ),
594
+)
595
+
596
+
597
+_DSV4_COMBINE_TOPK_SWA_WARMUP_INPUTS = zip_inputs(
598
+ # DSv4-Flash / SWA-only and C4A.
599
+ dict(compress_ratio=1, topk=0, topk_width=512),
600
+ dict(compress_ratio=4, topk=512, topk_width=512),
601
+ # DSv4-Pro C4A.
602
+ dict(compress_ratio=4, topk=1024, topk_width=1024),
603
+ # DSv4-Pro C128A.
604
+ dict(compress_ratio=128, topk=8192, topk_width=8192),
605
+)
606
+
607
+
608
+def _hf_config_int(vllm_config: Any, name: str, default: int) -> int:
609
+ model_config = getattr(vllm_config, "model_config", None)
610
+ hf_config = getattr(model_config, "hf_config", None)
611
+ return int(getattr(hf_config, name, default) or default)
612
+
613
+
614
+def _scheduler_config_int(vllm_config: Any, name: str, default: int) -> int:
615
+ scheduler_config = getattr(vllm_config, "scheduler_config", None)
616
+ return int(getattr(scheduler_config, name, default) or default)
617
+
618
+
619
+class CombineTopkSwaIndicesKernel(
620
+ VllmJitKernel["CombineTopkSwaIndicesKernel.CompileKey"]
621
):
584
- batch_idx = tl.program_id(0)
585
- worker_id = tl.program_id(1)
586
- num_workers = tl.num_programs(1)
622
+ @dataclass(frozen=True)
623
+ class CompileKey:
624
+ TOP_K: int
625
+ COMPRESS_RATIO: int
626
+ WINDOW_SIZE: int
627
+ PADDED_TOP_K: int
628
+ input_variant: TritonPointerInputVariant
629
+
630
+ @staticmethod
631
+ @triton.jit(
632
+ do_not_specialize=[
633
+ "combined_indices_stride",
634
+ "topk_indices_stride",
635
+ "M",
636
+ "N",
637
+ ]
638
+ )
639
+ def kernel(
640
+ combined_indices_ptr,
641
+ combined_indices_stride,
642
+ combined_lens_ptr,
643
+ topk_indices_ptr,
644
+ topk_indices_stride,
645
+ query_start_loc_ptr,
646
+ seq_lens_ptr,
647
+ gather_lens_ptr,
648
+ M,
649
+ N,
650
+ TOP_K: tl.constexpr,
651
+ COMPRESS_RATIO: tl.constexpr,
652
+ WINDOW_SIZE: tl.constexpr,
653
+ PADDED_TOP_K: tl.constexpr,
654
+ ):
655
+ batch_idx = tl.program_id(0)
656
+ worker_id = tl.program_id(1)
657
+ num_workers = tl.num_programs(1)
658
+
659
+ # query_start_loc is a global tensor; rebase to chunk-local offsets
660
+ # by subtracting the chunk's starting value.
661
+ base = tl.load(query_start_loc_ptr)
662
+ query_start = tl.load(query_start_loc_ptr + batch_idx) - base
663
+ query_end = tl.load(query_start_loc_ptr + batch_idx + 1) - base
664
+ query_len = query_end - query_start
665
+ seq_len = tl.load(seq_lens_ptr + batch_idx)
666
+ gather_len = tl.load(gather_lens_ptr + batch_idx)
667
+ start_pos = seq_len - query_len
668
+ # The SWA portion of the gathered buffer starts from position
669
+ # (seq_len - gather_len), not position 0. We need this offset
670
+ # to correctly index into the gathered buffer.
671
+ gather_start = seq_len - gather_len
672
+
673
+ for token_idx in range(query_start + worker_id, query_end, num_workers):
674
+ # topk_len is fully determined by the query token's absolute position:
675
+ # both the C4A indexer and the C128A metadata builder emit
676
+ # min((pos + 1) // compress_ratio, topk_tokens) valid entries.
677
+ # Caller passes TOP_K=0 for SWA-only layers to zero this out.
678
+ token_idx_in_query = token_idx - query_start
679
+ pos = start_pos + token_idx_in_query
680
+ topk_len = tl.minimum((pos + 1) // COMPRESS_RATIO, TOP_K)
681
+ swa_len = tl.minimum(pos + 1, WINDOW_SIZE)
682
+
683
+ offset = tl.arange(0, PADDED_TOP_K)
684
+ mask = offset < topk_len
685
+ topk_indices = tl.load(
686
+ topk_indices_ptr + token_idx * topk_indices_stride + offset,
687
+ mask=mask,
688
+ )
689
+ tl.store(
690
+ combined_indices_ptr + token_idx * combined_indices_stride + offset,
691
+ topk_indices + M * batch_idx,
692
+ mask=mask,
693
+ )
694
+ offset = tl.arange(0, WINDOW_SIZE)
695
+ # Index into gathered buffer: N + (position - gather_start)
696
+ # For positions [pos - swa_len + 1, pos], the buffer indices are:
697
+ # [N + pos - swa_len + 1 - gather_start, N + pos - gather_start]
698
+ tl.store(
699
+ combined_indices_ptr
700
+ + token_idx * combined_indices_stride
701
+ + topk_len
702
+ + offset,
703
+ M * batch_idx + N + offset + pos - swa_len + 1 - gather_start,
704
+ mask=offset < swa_len,
705
+ )
706
588
- # query_start_loc is a global tensor; rebase to chunk-local offsets
589
- # by subtracting the chunk's starting value.
590
- base = tl.load(query_start_loc_ptr)
591
- query_start = tl.load(query_start_loc_ptr + batch_idx) - base
592
- query_end = tl.load(query_start_loc_ptr + batch_idx + 1) - base
593
- query_len = query_end - query_start
594
- seq_len = tl.load(seq_lens_ptr + batch_idx)
595
- gather_len = tl.load(gather_lens_ptr + batch_idx)
596
- start_pos = seq_len - query_len
597
- # The SWA portion of the gathered buffer starts from position
598
- # (seq_len - gather_len), not position 0. We need this offset
599
- # to correctly index into the gathered buffer.
600
- gather_start = seq_len - gather_len
601
-
602
- for token_idx in range(query_start + worker_id, query_end, num_workers):
603
- # topk_len is fully determined by the query token's absolute position:
604
- # both the C4A indexer and the C128A metadata builder emit
605
- # min((pos + 1) // compress_ratio, topk_tokens) valid entries.
606
- # Caller passes TOP_K=0 for SWA-only layers to zero this out.
607
- token_idx_in_query = token_idx - query_start
608
- pos = start_pos + token_idx_in_query
609
- topk_len = tl.minimum((pos + 1) // COMPRESS_RATIO, TOP_K)
610
- swa_len = tl.minimum(pos + 1, WINDOW_SIZE)
611
-
612
- offset = tl.arange(0, PADDED_TOP_K)
613
- mask = offset < topk_len
614
- topk_indices = tl.load(
615
- topk_indices_ptr + token_idx * topk_indices_stride + offset,
616
- mask=mask,
707
+ combined_len = topk_len + swa_len
708
+ tl.store(combined_lens_ptr + token_idx, combined_len)
709
+
710
+ def dispatch( # type: ignore[override]
711
+ self,
712
+ *,
713
+ topk_width: int,
714
+ topk_indices: bool,
715
+ query_start_loc: bool,
716
+ seq_lens: bool,
717
+ gather_lens: bool,
718
+ topk: int,
719
+ compress_ratio: int,
720
+ WINDOW_SIZE: int,
721
+ ) -> CompileKey:
722
+ padded_topk = next_power_of_2(topk_width)
723
+ input_variant = TritonPointerInputVariant.from_alignment(
724
+ topk_indices=topk_indices,
725
+ query_start_loc=query_start_loc,
726
+ seq_lens=seq_lens,
727
+ gather_lens=gather_lens,
728
)
618
- tl.store(
619
- combined_indices_ptr + token_idx * combined_indices_stride + offset,
620
- topk_indices + M * batch_idx,
621
- mask=mask,
729
+ return self.CompileKey(
730
+ TOP_K=topk,
731
+ COMPRESS_RATIO=compress_ratio,
732
+ WINDOW_SIZE=WINDOW_SIZE,
733
+ PADDED_TOP_K=padded_topk,
734
+ input_variant=input_variant,
735
)
623
- offset = tl.arange(0, WINDOW_SIZE)
624
- # Index into gathered buffer: N + (position - gather_start)
625
- # For positions [pos - swa_len + 1, pos], the buffer indices are:
626
- # [N + pos - swa_len + 1 - gather_start, N + pos - gather_start]
627
- tl.store(
628
- combined_indices_ptr
629
- + token_idx * combined_indices_stride
630
- + topk_len
631
- + offset,
632
- M * batch_idx + N + offset + pos - swa_len + 1 - gather_start,
633
- mask=offset < swa_len,
736
+
737
+ def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]:
738
+ if _scheduler_config_int(vllm_config, "max_num_batched_tokens", 0) <= 0:
739
+ return []
740
+
741
+ window_size = _hf_config_int(vllm_config, "sliding_window", 128)
742
+ return self._trace_dispatch(self.dispatch)(
743
+ _DSV4_COMBINE_TOPK_SWA_WARMUP_INPUTS,
744
+ _COMBINE_TOPK_SWA_POINTER_INPUTS,
745
+ WINDOW_SIZE=window_size,
746
+ )
747
+
748
+ def compile(self, compile_key: CompileKey) -> None:
749
+ warmup = getattr(self.kernel, "warmup", None)
750
+ assert warmup is not None
751
+ int32_ptr = TritonWarmupTensor(torch.int32)
752
+ input_variant = compile_key.input_variant
753
+ warmup(
754
+ int32_ptr,
755
+ 1, # do not specialize combined_indices_stride
756
+ int32_ptr,
757
+ input_variant.pointer("topk_indices", torch.int32),
758
+ 1, # do not specialize topk_indices_stride
759
+ input_variant.pointer("query_start_loc", torch.int32),
760
+ input_variant.pointer("seq_lens", torch.int32),
761
+ input_variant.pointer("gather_lens", torch.int32),
762
+ 1, # do not specialize M
763
+ 1, # do not specialize N
764
+ TOP_K=compile_key.TOP_K,
765
+ COMPRESS_RATIO=compile_key.COMPRESS_RATIO,
766
+ WINDOW_SIZE=compile_key.WINDOW_SIZE,
767
+ PADDED_TOP_K=compile_key.PADDED_TOP_K,
768
+ grid=(1, _COMBINE_TOPK_SWA_NUM_WORKERS),
769
)
770
636
- combined_len = topk_len + swa_len
637
- tl.store(combined_lens_ptr + token_idx, combined_len)
771
+ def __call__(
772
+ self,
773
+ combined_indices: torch.Tensor,
774
+ combined_lens: torch.Tensor,
775
+ topk_indices: torch.Tensor,
776
+ query_start_loc: torch.Tensor,
777
+ seq_lens: torch.Tensor,
778
+ gather_lens: torch.Tensor,
779
+ M: int,
780
+ N: int,
781
+ *,
782
+ TOP_K: int,
783
+ COMPRESS_RATIO: int,
784
+ WINDOW_SIZE: int,
785
+ ) -> None:
786
+ num_reqs = seq_lens.shape[0]
787
+ self.kernel[(num_reqs, _COMBINE_TOPK_SWA_NUM_WORKERS)](
788
+ combined_indices,
789
+ combined_indices.stride(0),
790
+ combined_lens,
791
+ topk_indices,
792
+ topk_indices.stride(0),
793
+ query_start_loc,
794
+ seq_lens,
795
+ gather_lens,
796
+ M,
797
+ N,
798
+ TOP_K=TOP_K,
799
+ COMPRESS_RATIO=COMPRESS_RATIO,
800
+ WINDOW_SIZE=WINDOW_SIZE,
801
+ PADDED_TOP_K=next_power_of_2(topk_indices.shape[-1]),
802
+ )
803
+
804
+
805
+_COMBINE_TOPK_SWA_INDICES_KERNEL = CombineTopkSwaIndicesKernel()
806
807
808
def build_flashinfer_mixed_sparse_indices(
vllm/v1/attention/backends/mla/indexer.py
+215
-79
@@ -8,6 +8,14 @@ import vllm.envs as envs
8
from vllm.config import VllmConfig
9
from vllm.distributed import get_dcp_group, get_pcp_group
10
from vllm.logger import init_logger
11
+from vllm.model_executor.warmup.jit_warmup import (
12
+ VllmJitKernel,
13
+ WarmupIntRange,
14
+)
15
+from vllm.model_executor.warmup.jit_warmup_triton_helper import (
16
+ TritonPointerInputVariant,
17
+ TritonWarmupTensor,
18
+)
19
from vllm.platforms import current_platform
20
from vllm.triton_utils import tl, triton
21
from vllm.utils.deep_gemm import (
@@ -190,6 +198,211 @@ class DeepseekV32IndexerPrefillChunkMetadata:
198
max_local_total_seq_lens: int = 0
199
200
201
+_BUILD_PREFILL_CHUNK_METADATA_INPUT_VARIANTS = (
202
+ TritonPointerInputVariant.from_alignment(uncompressed_seq_lens=True),
203
+ TritonPointerInputVariant.from_alignment(uncompressed_seq_lens=False),
204
+)
205
+
206
+
207
+class BuildPrefillChunkMetadataKernel(
208
+ VllmJitKernel["BuildPrefillChunkMetadataKernel.CompileKey"]
209
+):
210
+ BLOCK_SIZE = 1024
211
+
212
+ @dataclass(frozen=True)
213
+ class CompileKey:
214
+ query_slice_start: int
215
+ query_slice_stop: int
216
+ DCP_RANK: int
217
+ DCP_WORLD: int
218
+ DCP_INTERLEAVE: int
219
+ BLOCK_SIZE: int
220
+ COMPRESS_RATIO: int
221
+ input_variant: TritonPointerInputVariant
222
+
223
+ @staticmethod
224
+ @triton.jit
225
+ def kernel(
226
+ # Inputs
227
+ query_start_loc_ptr,
228
+ uncompressed_seq_lens_ptr,
229
+ cu_compressed_seq_lens_ptr,
230
+ # Row-start base for cu_seq_len_ks/ke: local cumulative lens under DCP,
231
+ # aliases cu_compressed_seq_lens_ptr otherwise.
232
+ row_start_cu_compressed_seq_lens_ptr,
233
+ # Outputs
234
+ token_to_seq_ptr,
235
+ cu_compressed_seq_len_ks_ptr,
236
+ cu_compressed_seq_len_ke_ptr,
237
+ query_slice_start,
238
+ query_slice_stop,
239
+ DCP_RANK,
240
+ DCP_WORLD,
241
+ DCP_INTERLEAVE,
242
+ BLOCK_SIZE: tl.constexpr,
243
+ COMPRESS_RATIO: tl.constexpr,
244
+ ):
245
+ batch_idx = tl.program_id(0)
246
+
247
+ query_start = tl.load(query_start_loc_ptr + batch_idx)
248
+ query_end = tl.load(query_start_loc_ptr + batch_idx + 1)
249
+ query_len = query_end - query_start
250
+
251
+ seq_start = tl.load(cu_compressed_seq_lens_ptr + batch_idx)
252
+ seq_end = tl.load(cu_compressed_seq_lens_ptr + batch_idx + 1)
253
+ compressed_seq_len = seq_end - seq_start
254
+
255
+ # Row start for the (possibly localized) cu_seq_len_ks/ke. Equals seq_start
256
+ # when DCP is disabled (the pointer aliases cu_compressed_seq_lens_ptr).
257
+ row_start = tl.load(row_start_cu_compressed_seq_lens_ptr + batch_idx)
258
+
259
+ uncompressed_seq_len = tl.load(uncompressed_seq_lens_ptr + batch_idx)
260
+ start_pos = uncompressed_seq_len - query_len
261
+
262
+ for i in range(0, query_len, BLOCK_SIZE):
263
+ offset = i + tl.arange(0, BLOCK_SIZE)
264
+ abs_pos = query_start + offset
265
+ mask = (
266
+ (offset < query_len)
267
+ & (abs_pos >= query_slice_start)
268
+ & (abs_pos < query_slice_stop)
269
+ )
270
+ out_pos = abs_pos - query_slice_start
271
+
272
+ # cu_seq_len_ks: row start in the gathered K buffer.
273
+ tl.store(cu_compressed_seq_len_ks_ptr + out_pos, row_start, mask=mask)
274
+
275
+ # cu_seq_len_ke: row start + per-token context length. Under DCP the
276
+ # global per-token length is sharded across ranks.
277
+ global_ctx = start_pos + 1 + offset
278
+ len_per_token = global_ctx // COMPRESS_RATIO
279
+ if DCP_WORLD > 1:
280
+ # Per-rank local context length under interleave-aware DCP, matching
281
+ # get_dcp_local_seq_lens. K == 1 reduces to (len + world-1-rank)//world.
282
+ base = (len_per_token // DCP_INTERLEAVE // DCP_WORLD) * DCP_INTERLEAVE
283
+ remainder = len_per_token - base * DCP_WORLD
284
+ remainder = tl.minimum(
285
+ tl.maximum(remainder - DCP_RANK * DCP_INTERLEAVE, 0), DCP_INTERLEAVE
286
+ )
287
+ len_per_token = base + remainder
288
+ tl.store(
289
+ cu_compressed_seq_len_ke_ptr + out_pos,
290
+ row_start + len_per_token,
291
+ mask=mask,
292
+ )
293
+
294
+ # Compute token_to_seq
295
+ for i in range(0, compressed_seq_len, BLOCK_SIZE):
296
+ offset = i + tl.arange(0, BLOCK_SIZE)
297
+ mask = offset < compressed_seq_len
298
+ tl.store(token_to_seq_ptr + seq_start + offset, batch_idx, mask=mask)
299
+
300
+ def dispatch( # type: ignore[override]
301
+ self,
302
+ *,
303
+ query_slice_start: int,
304
+ query_slice_stop: int,
305
+ DCP_RANK: int,
306
+ DCP_WORLD: int,
307
+ DCP_INTERLEAVE: int,
308
+ BLOCK_SIZE: int,
309
+ COMPRESS_RATIO: int,
310
+ input_variant: TritonPointerInputVariant,
311
+ ) -> CompileKey:
312
+ return self.CompileKey(
313
+ query_slice_start=query_slice_start,
314
+ query_slice_stop=query_slice_stop,
315
+ DCP_RANK=DCP_RANK,
316
+ DCP_WORLD=DCP_WORLD,
317
+ DCP_INTERLEAVE=DCP_INTERLEAVE,
318
+ BLOCK_SIZE=BLOCK_SIZE,
319
+ COMPRESS_RATIO=COMPRESS_RATIO,
320
+ input_variant=input_variant,
321
+ )
322
+
323
+ def get_warmup_keys(self, vllm_config: VllmConfig) -> list[CompileKey]:
324
+ max_tokens = max(1, min(vllm_config.scheduler_config.max_num_batched_tokens, 8))
325
+ hf_config = vllm_config.model_config.hf_config
326
+ parallel_config = vllm_config.parallel_config
327
+ dcp_world = parallel_config.decode_context_parallel_size
328
+ dcp_interleave = parallel_config.cp_kv_cache_interleave_size
329
+ dcp_rank = get_dcp_group().rank_in_group if dcp_world > 1 else 0
330
+ compress_ratios = tuple(
331
+ int(ratio)
332
+ for ratio in (getattr(hf_config, "compress_ratios", None) or (1,))
333
+ )
334
+ return self._trace_dispatch(self.dispatch)(
335
+ query_slice_start=WarmupIntRange(0, 2),
336
+ query_slice_stop=(1, 2 * max_tokens - 1, 2 * max_tokens),
337
+ DCP_RANK=dcp_rank,
338
+ DCP_WORLD=dcp_world,
339
+ DCP_INTERLEAVE=dcp_interleave,
340
+ BLOCK_SIZE=self.BLOCK_SIZE,
341
+ COMPRESS_RATIO=list(compress_ratios),
342
+ input_variant=_BUILD_PREFILL_CHUNK_METADATA_INPUT_VARIANTS,
343
+ )
344
+
345
+ def compile(self, compile_key: CompileKey) -> None:
346
+ warmup = getattr(self.kernel, "warmup", None)
347
+ assert warmup is not None
348
+ int32_ptr = TritonWarmupTensor(torch.int32)
349
+ warmup(
350
+ int32_ptr,
351
+ compile_key.input_variant.pointer("uncompressed_seq_lens", torch.int32),
352
+ int32_ptr,
353
+ int32_ptr,
354
+ int32_ptr,
355
+ int32_ptr,
356
+ int32_ptr,
357
+ compile_key.query_slice_start,
358
+ compile_key.query_slice_stop,
359
+ compile_key.DCP_RANK,
360
+ compile_key.DCP_WORLD,
361
+ compile_key.DCP_INTERLEAVE,
362
+ BLOCK_SIZE=compile_key.BLOCK_SIZE,
363
+ COMPRESS_RATIO=compile_key.COMPRESS_RATIO,
364
+ grid=(1,),
365
+ )
366
+
367
+ def __call__(
368
+ self,
369
+ query_start_loc: torch.Tensor,
370
+ uncompressed_seq_lens: torch.Tensor,
371
+ cu_compressed_seq_lens: torch.Tensor,
372
+ row_start_cu_compressed_seq_lens: torch.Tensor,
373
+ token_to_seq: torch.Tensor,
374
+ cu_compressed_seq_len_ks: torch.Tensor,
375
+ cu_compressed_seq_len_ke: torch.Tensor,
376
+ query_slice_start: int,
377
+ query_slice_stop: int,
378
+ DCP_RANK: int,
379
+ DCP_WORLD: int,
380
+ DCP_INTERLEAVE: int,
381
+ *,
382
+ num_reqs: int,
383
+ COMPRESS_RATIO: int,
384
+ ) -> None:
385
+ self.kernel[(num_reqs,)](
386
+ query_start_loc,
387
+ uncompressed_seq_lens,
388
+ cu_compressed_seq_lens,
389
+ row_start_cu_compressed_seq_lens,
390
+ token_to_seq,
391
+ cu_compressed_seq_len_ks,
392
+ cu_compressed_seq_len_ke,
393
+ query_slice_start,
394
+ query_slice_stop,
395
+ DCP_RANK,
396
+ DCP_WORLD,
397
+ DCP_INTERLEAVE,
398
+ BLOCK_SIZE=self.BLOCK_SIZE,
399
+ COMPRESS_RATIO=COMPRESS_RATIO,
400
+ )
401
+
402
+
403
+_BUILD_PREFILL_CHUNK_METADATA_KERNEL = BuildPrefillChunkMetadataKernel()
404
+
405
+
406
@dataclass
407
class DeepseekV32IndexerPrefillMetadata:
408
chunks: list[DeepseekV32IndexerPrefillChunkMetadata]
@@ -841,7 +1054,7 @@ def build_prefill_chunk_metadata(
1054
1055
# Under DCP the kernel writes this rank's local row bounds into
1056
# cu_seq_len_ks/ke; otherwise local_cu_seq_lens aliases cu_seq_lens.
844
- _build_prefill_chunk_metadata_kernel[(num_reqs,)](
1057
+ _BUILD_PREFILL_CHUNK_METADATA_KERNEL(
1058
query_start_loc,
1059
uncompressed_seq_lens[start_idx:end_idx],
1060
cu_seq_lens,
@@ -854,7 +1067,7 @@ def build_prefill_chunk_metadata(
1067
dcp_rank,
1068
dcp_world_size,
1069
cp_kv_cache_interleave_size,
857
- BLOCK_SIZE=1024,
1070
+ num_reqs=num_reqs,
1071
COMPRESS_RATIO=compress_ratio,
1072
)
1073
@@ -881,80 +1094,3 @@ def build_prefill_chunk_metadata(
1094
local_total_seq_lens=local_total_seq_lens,
1095
max_local_total_seq_lens=max_local_total_seq_lens,
1096
)
884
-
885
-
886
-@triton.jit
887
-def _build_prefill_chunk_metadata_kernel(
888
- # Inputs
889
- query_start_loc_ptr,
890
- uncompressed_seq_lens_ptr,
891
- cu_compressed_seq_lens_ptr,
892
- # Row-start base for cu_seq_len_ks/ke: local cumulative lens under DCP,
893
- # aliases cu_compressed_seq_lens_ptr otherwise.
894
- row_start_cu_compressed_seq_lens_ptr,
895
- # Outputs
896
- token_to_seq_ptr,
897
- cu_compressed_seq_len_ks_ptr,
898
- cu_compressed_seq_len_ke_ptr,
899
- query_slice_start,
900
- query_slice_stop,
901
- DCP_RANK,
902
- DCP_WORLD,
903
- DCP_INTERLEAVE,
904
- BLOCK_SIZE: tl.constexpr,
905
- COMPRESS_RATIO: tl.constexpr,
906
-):
907
- batch_idx = tl.program_id(0)
908
-
909
- query_start = tl.load(query_start_loc_ptr + batch_idx)
910
- query_end = tl.load(query_start_loc_ptr + batch_idx + 1)
911
- query_len = query_end - query_start
912
-
913
- seq_start = tl.load(cu_compressed_seq_lens_ptr + batch_idx)
914
- seq_end = tl.load(cu_compressed_seq_lens_ptr + batch_idx + 1)
915
- compressed_seq_len = seq_end - seq_start
916
-
917
- # Row start for the (possibly localized) cu_seq_len_ks/ke. Equals seq_start
918
- # when DCP is disabled (the pointer aliases cu_compressed_seq_lens_ptr).
919
- row_start = tl.load(row_start_cu_compressed_seq_lens_ptr + batch_idx)
920
-
921
- uncompressed_seq_len = tl.load(uncompressed_seq_lens_ptr + batch_idx)
922
- start_pos = uncompressed_seq_len - query_len
923
-
924
- for i in range(0, query_len, BLOCK_SIZE):
925
- offset = i + tl.arange(0, BLOCK_SIZE)
926
- abs_pos = query_start + offset
927
- mask = (
928
- (offset < query_len)
929
- & (abs_pos >= query_slice_start)
930
- & (abs_pos < query_slice_stop)
931
- )
932
- out_pos = abs_pos - query_slice_start
933
-
934
- # cu_seq_len_ks: row start in the gathered K buffer.
935
- tl.store(cu_compressed_seq_len_ks_ptr + out_pos, row_start, mask=mask)
936
-
937
- # cu_seq_len_ke: row start + per-token context length. Under DCP the
938
- # global per-token length is sharded across ranks.
939
- global_ctx = start_pos + 1 + offset
940
- len_per_token = global_ctx // COMPRESS_RATIO
941
- if DCP_WORLD > 1:
942
- # Per-rank local context length under interleave-aware DCP, matching
943
- # get_dcp_local_seq_lens. K == 1 reduces to (len + world-1-rank)//world.
944
- base = (len_per_token // DCP_INTERLEAVE // DCP_WORLD) * DCP_INTERLEAVE
945
- remainder = len_per_token - base * DCP_WORLD
946
- remainder = tl.minimum(
947
- tl.maximum(remainder - DCP_RANK * DCP_INTERLEAVE, 0), DCP_INTERLEAVE
948
- )
949
- len_per_token = base + remainder
950
- tl.store(
951
- cu_compressed_seq_len_ke_ptr + out_pos,
952
- row_start + len_per_token,
953
- mask=mask,
954
- )
955
-
956
- # Compute token_to_seq
957
- for i in range(0, compressed_seq_len, BLOCK_SIZE):
958
- offset = i + tl.arange(0, BLOCK_SIZE)
959
- mask = offset < compressed_seq_len
960
- tl.store(token_to_seq_ptr + seq_start + offset, batch_idx, mask=mask)
vllm/v1/attention/backends/mla/prefill/flash_attn.py
+246
-50
@@ -3,7 +3,9 @@
3
"""FlashAttention backend for MLA prefill."""
4
5
import functools
6
-from typing import TYPE_CHECKING
6
+from collections.abc import Callable
7
+from dataclasses import dataclass
8
+from typing import TYPE_CHECKING, Any
9
10
import torch
11
@@ -11,14 +13,7 @@ import vllm.envs as envs
13
from vllm.model_executor.layers.quantization.utils.quant_utils import (
14
kFp8StaticTensorSym,
15
)
14
-from vllm.model_executor.warmup.cutedsl_warmup import (
15
- CuTeDSLCompileUnit,
16
- register_cutedsl_warmup_provider,
17
-)
18
-from vllm.model_executor.warmup.fa4_cutedsl_config import (
19
- FA4MLAPrefillCompileContext,
20
- iter_fa4_mla_prefill_compile_requests,
21
-)
16
+from vllm.model_executor.warmup.jit_warmup import VllmJitKernel
17
from vllm.platforms import current_platform
18
from vllm.v1.attention.backends.fa_utils import (
19
compile_flash_attn_varlen_func_from_specs,
@@ -32,6 +27,7 @@ from vllm.v1.attention.backends.mla.prefill.base import (
27
28
if TYPE_CHECKING:
29
from vllm.config import VllmConfig
30
+ from vllm.model_executor.layers.attention.mla_attention import MLADims
31
from vllm.model_executor.layers.quantization.utils.quant_utils import QuantKey
32
33
if is_flash_attn_varlen_func_available():
@@ -40,6 +36,245 @@ else:
36
flash_attn_varlen_func = None # type: ignore[assignment]
37
38
39
+FA4_STANDARD_DTYPES = (torch.bfloat16, torch.float16)
40
+
41
+# Current vLLM MLA prefill expands K/V to num_heads before FA4, so this plan
42
+# covers qhead_per_kvhead=1. Batch is not a current FA4 MLA-prefill key field.
43
+# Use b1 for compile-only specs because it is the conservative case for
44
+# Split-KV shape heuristics.
45
+# TODO(roberto): FA4 also has direct-GQA and qv/top-k absorbed-MLA paths, but
46
+# vLLM does not use them in this backend yet; they need a separate
47
+# num_kv_heads/qv/top-k-aware warmup plan if wired in later.
48
+FA4_MLA_PREFILL_COMPILE_BATCH_SIZE = 1
49
+FA4_MLA_PREFILL_Q_TILE = 128
50
+FA4_MLA_PREFILL_K_TILE = 128
51
+FA4_MLA_PREFILL_LONG_K_BLOCKS = 32
52
+FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS = 64
53
+FA4_MLA_PREFILL_CAUSAL_OPTIONS = (False, True)
54
+FA4_MLA_PREFILL_LSE_OPTIONS = (False, True)
55
+
56
+
57
+@dataclass(frozen=True)
58
+class _FA4MLAPrefillShapeProbe:
59
+ max_seqlen_q: int
60
+ max_seqlen_k: int
61
+
62
+
63
+class FA4MLAPrefillKernel(VllmJitKernel["FA4MLAPrefillKernel.CompileKey"]):
64
+ """FA4 MLA prefill compile-key wrapper used by generic JIT warmup."""
65
+
66
+ @dataclass(frozen=True)
67
+ class CompileKey:
68
+ """High-level FA4 compile-only key used by vLLM warmup.
69
+
70
+ This is not the CuTeDSL cache key. FA4 owns the selector that maps these
71
+ serving inputs to the actual compile-static fields: tile sizes, q_stage,
72
+ Split-KV, scheduler choice, layout-presence booleans, dtype/head dims,
73
+ arch, and related fields.
74
+ """
75
+
76
+ q_shape: tuple[int, ...]
77
+ k_shape: tuple[int, ...]
78
+ v_shape: tuple[int, ...]
79
+ q_dtype: torch.dtype
80
+ max_seqlen_q: int
81
+ max_seqlen_k: int
82
+ softmax_scale: float
83
+ causal: bool
84
+ fa_version: int
85
+ v_stride: tuple[int, ...] | None = None
86
+ cu_seqlens_q_shape: tuple[int, ...] | None = None
87
+ cu_seqlens_k_shape: tuple[int, ...] | None = None
88
+ window_size: tuple[int, int] | None = None
89
+ return_softmax_lse: bool = False
90
+ num_splits: int = 0
91
+
92
+ def dispatch( # type: ignore[override]
93
+ self,
94
+ *,
95
+ batch_size: int,
96
+ dtype: torch.dtype,
97
+ num_heads: int,
98
+ mla_dims: "MLADims",
99
+ shape_probe: _FA4MLAPrefillShapeProbe,
100
+ requires_v_padding: bool,
101
+ causal: bool,
102
+ fa_version: int,
103
+ window_size: tuple[int, int] | None = None,
104
+ return_lse: bool = False,
105
+ num_splits: int = 0,
106
+ ) -> CompileKey:
107
+ return self.CompileKey(
108
+ q_shape=(
109
+ batch_size * shape_probe.max_seqlen_q,
110
+ num_heads,
111
+ mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim,
112
+ ),
113
+ k_shape=(
114
+ batch_size * shape_probe.max_seqlen_k,
115
+ num_heads,
116
+ mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim,
117
+ ),
118
+ v_shape=(
119
+ batch_size * shape_probe.max_seqlen_k,
120
+ num_heads,
121
+ (
122
+ mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim
123
+ if requires_v_padding
124
+ else mla_dims.v_head_dim
125
+ ),
126
+ ),
127
+ q_dtype=dtype,
128
+ max_seqlen_q=shape_probe.max_seqlen_q,
129
+ max_seqlen_k=shape_probe.max_seqlen_k,
130
+ softmax_scale=(mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim)
131
+ ** -0.5,
132
+ causal=causal,
133
+ fa_version=fa_version,
134
+ v_stride=(
135
+ None
136
+ if requires_v_padding
137
+ else (
138
+ num_heads * (mla_dims.qk_nope_head_dim + mla_dims.v_head_dim),
139
+ mla_dims.qk_nope_head_dim + mla_dims.v_head_dim,
140
+ 1,
141
+ )
142
+ ),
143
+ cu_seqlens_q_shape=(batch_size + 1,),
144
+ cu_seqlens_k_shape=(batch_size + 1,),
145
+ window_size=window_size,
146
+ return_softmax_lse=return_lse,
147
+ num_splits=num_splits,
148
+ )
149
+
150
+ def get_warmup_keys(self, vllm_config: "VllmConfig") -> list[CompileKey]:
151
+ from vllm.model_executor.layers.attention.mla_attention import (
152
+ get_mla_dims,
153
+ )
154
+
155
+ mla_dims = get_mla_dims(vllm_config.model_config)
156
+ qk_head_dim = mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim
157
+ fa_version = get_flash_attn_version(head_size=qk_head_dim)
158
+ if fa_version != 4:
159
+ return []
160
+
161
+ dtype = vllm_config.model_config.dtype
162
+ if dtype not in FA4_STANDARD_DTYPES:
163
+ dtype = torch.bfloat16
164
+
165
+ is_sm90 = current_platform.is_device_capability(90)
166
+ is_sm100_family = current_platform.is_device_capability_family(100)
167
+ is_sm120 = current_platform.is_device_capability_family(120)
168
+ if not (is_sm90 or is_sm100_family or is_sm120):
169
+ return []
170
+
171
+ num_splits = 1 if envs.VLLM_BATCH_INVARIANT else 0
172
+ if is_sm120 and num_splits != 1:
173
+ return []
174
+
175
+ num_heads = vllm_config.model_config.get_num_attention_heads(
176
+ vllm_config.parallel_config
177
+ )
178
+ if num_heads <= 0:
179
+ return []
180
+
181
+ requires_v_padding = False
182
+ effective_v_head_dim = (
183
+ qk_head_dim if requires_v_padding else mla_dims.v_head_dim
184
+ )
185
+ shape_probes = [
186
+ _FA4MLAPrefillShapeProbe(1, FA4_MLA_PREFILL_K_TILE),
187
+ _FA4MLAPrefillShapeProbe(
188
+ FA4_MLA_PREFILL_Q_TILE + 1,
189
+ 4 * FA4_MLA_PREFILL_K_TILE,
190
+ ),
191
+ ]
192
+ if num_splits != 1:
193
+ shape_probes.extend(
194
+ (
195
+ _FA4MLAPrefillShapeProbe(
196
+ 1,
197
+ FA4_MLA_PREFILL_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE,
198
+ ),
199
+ _FA4MLAPrefillShapeProbe(
200
+ FA4_MLA_PREFILL_Q_TILE + 1,
201
+ FA4_MLA_PREFILL_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE,
202
+ ),
203
+ )
204
+ )
205
+ if not is_sm90 and qk_head_dim != effective_v_head_dim:
206
+ shape_probes.extend(
207
+ (
208
+ _FA4MLAPrefillShapeProbe(
209
+ 1,
210
+ FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE,
211
+ ),
212
+ _FA4MLAPrefillShapeProbe(
213
+ FA4_MLA_PREFILL_Q_TILE + 1,
214
+ FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE,
215
+ ),
216
+ )
217
+ )
218
+
219
+ return self._trace_dispatch(self.dispatch)(
220
+ batch_size=FA4_MLA_PREFILL_COMPILE_BATCH_SIZE,
221
+ dtype=dtype,
222
+ num_heads=num_heads,
223
+ mla_dims=mla_dims,
224
+ shape_probe=shape_probes,
225
+ requires_v_padding=requires_v_padding,
226
+ causal=FA4_MLA_PREFILL_CAUSAL_OPTIONS,
227
+ return_lse=FA4_MLA_PREFILL_LSE_OPTIONS,
228
+ num_splits=num_splits,
229
+ fa_version=fa_version,
230
+ )
231
+
232
+ @staticmethod
233
+ def kernel(*args: Any, **kwargs: Any) -> Any:
234
+ assert flash_attn_varlen_func is not None
235
+ return flash_attn_varlen_func(*args, **kwargs)
236
+
237
+ def compile(self, compile_key: CompileKey) -> None:
238
+ assert compile_flash_attn_varlen_func_from_specs is not None
239
+ window_size = (
240
+ list(compile_key.window_size)
241
+ if compile_key.window_size is not None
242
+ else None
243
+ )
244
+ compile_flash_attn_varlen_func_from_specs(
245
+ q_shape=compile_key.q_shape,
246
+ k_shape=compile_key.k_shape,
247
+ v_shape=compile_key.v_shape,
248
+ q_dtype=compile_key.q_dtype,
249
+ v_stride=compile_key.v_stride,
250
+ cu_seqlens_q_shape=compile_key.cu_seqlens_q_shape,
251
+ cu_seqlens_k_shape=compile_key.cu_seqlens_k_shape,
252
+ max_seqlen_q=compile_key.max_seqlen_q,
253
+ max_seqlen_k=compile_key.max_seqlen_k,
254
+ softmax_scale=compile_key.softmax_scale,
255
+ causal=compile_key.causal,
256
+ window_size=window_size,
257
+ return_softmax_lse=compile_key.return_softmax_lse,
258
+ fa_version=compile_key.fa_version,
259
+ num_splits=compile_key.num_splits,
260
+ )
261
+
262
+ def __call__(
263
+ self,
264
+ *,
265
+ q: torch.Tensor,
266
+ k: torch.Tensor,
267
+ v: torch.Tensor,
268
+ runtime_kernel: Callable[..., Any] | None = None,
269
+ **kwargs: Any,
270
+ ) -> Any:
271
+ kernel = self.kernel if runtime_kernel is None else runtime_kernel
272
+ return kernel(q=q, k=k, v=v, **kwargs)
273
+
274
+
275
+FA4_MLA_PREFILL_KERNEL = FA4MLAPrefillKernel()
276
+
277
+
278
class FlashAttnPrefillBackend(MLAPrefillBackend):
279
"""FlashAttention backend for MLA prefill."""
280
@@ -127,46 +362,6 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
362
363
# Track whether we're using vllm's FA or upstream (for ROCm)
364
self._is_vllm_fa = current_platform.is_cuda() or current_platform.is_xpu()
130
- if self.vllm_flash_attn_version == 4:
131
- register_cutedsl_warmup_provider(self)
132
-
133
- def get_cutedsl_warmup_compile_units(self) -> tuple[CuTeDSLCompileUnit, ...]:
134
- if self.vllm_flash_attn_version != 4:
135
- return ()
136
- if compile_flash_attn_varlen_func_from_specs is None:
137
- raise RuntimeError(
138
- "FA4 compile-only API is unavailable; CuTeDSL warmup does not "
139
- "fall back to synthetic forward passes."
140
- )
141
-
142
- dtype = self.vllm_config.model_config.dtype
143
- if dtype not in self.supported_dtypes:
144
- dtype = torch.bfloat16
145
-
146
- qk_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
147
- ctx = FA4MLAPrefillCompileContext(
148
- dtype=dtype,
149
- num_heads=self.num_heads,
150
- qk_head_dim=qk_head_dim,
151
- v_head_dim=self.v_head_dim,
152
- kv_nope_head_dim=self.qk_nope_head_dim + self.v_head_dim,
153
- requires_v_padding=self.requires_v_padding,
154
- scale=self.scale,
155
- num_splits=1 if envs.VLLM_BATCH_INVARIANT else 0,
156
- fa_version=self.vllm_flash_attn_version,
157
- )
158
- compile_requests = tuple(iter_fa4_mla_prefill_compile_requests(ctx))
159
- if not compile_requests:
160
- return ()
161
-
162
- return tuple(
163
- CuTeDSLCompileUnit(
164
- name="fa4_mla_prefill",
165
- key=request.key,
166
- compile=request.compile,
167
- )
168
- for request in compile_requests
169
- )
365
366
def supports_quant_output(self, quant_key: "QuantKey") -> bool:
367
device_capability = current_platform.get_device_capability()
@@ -207,10 +402,11 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
402
if envs.VLLM_BATCH_INVARIANT:
403
kwargs["num_splits"] = 1
404
210
- attn_out = self.flash_attn_varlen_func(
405
+ attn_out = FA4_MLA_PREFILL_KERNEL(
406
q=q,
407
k=k,
408
v=maybe_padded_v,
409
+ runtime_kernel=self.flash_attn_varlen_func,
410
softmax_scale=softmax_scale,
411
**kwargs,
412
)
vllm/v1/attention/backends/mla/sparse_swa.py
+108
-34
@@ -7,9 +7,14 @@ import torch
7
8
from vllm.config import CacheConfig, VllmConfig, get_current_vllm_config
9
from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
10
+from vllm.model_executor.warmup.jit_warmup import (
11
+ VllmJitKernel,
12
+ WarmupIntRange,
13
+)
14
+from vllm.model_executor.warmup.jit_warmup_triton_helper import TritonWarmupTensor
15
from vllm.platforms import current_platform
16
from vllm.triton_utils import tl, triton
12
-from vllm.utils.math_utils import cdiv
17
+from vllm.utils.math_utils import cdiv, next_power_of_2
18
from vllm.v1.attention.backend import (
19
AttentionBackend,
20
AttentionCGSupport,
@@ -279,6 +284,107 @@ class DeepseekSparseSWAMetadata:
284
return chunk_plan
285
286
287
+class ComputePrefillMetadataKernel(
288
+ VllmJitKernel["ComputePrefillMetadataKernel.CompileKey"]
289
+):
290
+ @dataclass(frozen=True)
291
+ class CompileKey:
292
+ BLOCK_SIZE: int
293
+
294
+ @staticmethod
295
+ @triton.jit(do_not_specialize=["num_prefills", "num_decodes", "window_size"])
296
+ def kernel(
297
+ # Outputs
298
+ prefill_gather_lens_ptr,
299
+ # Inputs
300
+ seq_lens_ptr,
301
+ query_start_loc_ptr,
302
+ num_prefills,
303
+ num_decodes,
304
+ window_size,
305
+ BLOCK_SIZE: tl.constexpr,
306
+ ):
307
+ """Compute prefill gather_lens in a single pass."""
308
+ offset = tl.arange(0, BLOCK_SIZE)
309
+ mask = offset < num_prefills
310
+ # SM12x + Triton 3.6 raises IMA on out-of-bounds address arithmetic for
311
+ # masked-off lanes even though the load mask gates the actual read, so
312
+ # clamp the offset. Caller guarantees num_prefills > 0.
313
+ safe_offset = tl.minimum(offset, num_prefills - 1)
314
+
315
+ seq_len = tl.load(seq_lens_ptr + num_decodes + safe_offset, mask=mask)
316
+ qsl_start = tl.load(query_start_loc_ptr + num_decodes + safe_offset, mask=mask)
317
+ qsl_end = tl.load(
318
+ query_start_loc_ptr + num_decodes + safe_offset + 1, mask=mask
319
+ )
320
+
321
+ query_len = qsl_end - qsl_start
322
+ prefix_len = seq_len - query_len
323
+ gather_len = query_len + tl.minimum(prefix_len, window_size - 1)
324
+
325
+ tl.store(prefill_gather_lens_ptr + offset, gather_len, mask=mask)
326
+
327
+ def dispatch( # type: ignore[override]
328
+ self,
329
+ *,
330
+ num_prefills: int,
331
+ ) -> CompileKey:
332
+ return self.CompileKey(
333
+ BLOCK_SIZE=next_power_of_2(num_prefills),
334
+ )
335
+
336
+ def get_warmup_keys(self, vllm_config: VllmConfig) -> list[CompileKey]:
337
+ scheduler_config = vllm_config.scheduler_config
338
+ max_prefills = max(
339
+ 1,
340
+ min(
341
+ scheduler_config.max_num_seqs,
342
+ scheduler_config.max_num_batched_tokens,
343
+ ),
344
+ )
345
+ return self._trace_dispatch(self.dispatch)(
346
+ num_prefills=WarmupIntRange(1, max_prefills + 1),
347
+ )
348
+
349
+ def compile(self, compile_key: CompileKey) -> None:
350
+ warmup = getattr(self.kernel, "warmup", None)
351
+ assert warmup is not None
352
+ int32_ptr = TritonWarmupTensor(torch.int32)
353
+ warmup(
354
+ int32_ptr,
355
+ int32_ptr,
356
+ int32_ptr,
357
+ compile_key.BLOCK_SIZE,
358
+ 0,
359
+ 1,
360
+ BLOCK_SIZE=compile_key.BLOCK_SIZE,
361
+ grid=(1,),
362
+ )
363
+
364
+ def __call__(
365
+ self,
366
+ prefill_gather_lens: torch.Tensor,
367
+ seq_lens: torch.Tensor,
368
+ query_start_loc: torch.Tensor,
369
+ num_prefills: int,
370
+ num_decodes: int,
371
+ window_size: int,
372
+ ) -> None:
373
+ compile_key = self.dispatch(num_prefills=num_prefills)
374
+ self.kernel[(1,)](
375
+ prefill_gather_lens,
376
+ seq_lens,
377
+ query_start_loc,
378
+ num_prefills,
379
+ num_decodes,
380
+ window_size,
381
+ BLOCK_SIZE=compile_key.BLOCK_SIZE,
382
+ )
383
+
384
+
385
+_COMPUTE_PREFILL_METADATA_KERNEL = ComputePrefillMetadataKernel()
386
+
387
+
388
class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
389
"""Builds metadata for DeepseekV4 SWA cache.
390
@@ -602,14 +708,13 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
708
pfx_gather_lens = torch.empty(
709
num_prefills, dtype=torch.int32, device=seq_lens.device
710
)
605
- _compute_prefill_metadata_kernel[(1,)](
711
+ _COMPUTE_PREFILL_METADATA_KERNEL(
712
pfx_gather_lens,
713
seq_lens,
714
query_start_loc,
715
num_prefills,
716
num_decodes,
717
self.window_size,
612
- BLOCK_SIZE=triton.next_power_of_2(num_prefills),
718
)
719
720
result["prefill_seq_lens"] = seq_lens[num_decodes:]
@@ -626,37 +731,6 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
731
return result
732
733
629
-@triton.jit
630
-def _compute_prefill_metadata_kernel(
631
- # Outputs
632
- prefill_gather_lens_ptr,
633
- # Inputs
634
- seq_lens_ptr,
635
- query_start_loc_ptr,
636
- num_prefills,
637
- num_decodes,
638
- window_size,
639
- BLOCK_SIZE: tl.constexpr,
640
-):
641
- """Compute prefill gather_lens in a single pass."""
642
- offset = tl.arange(0, BLOCK_SIZE)
643
- mask = offset < num_prefills
644
- # SM12x + Triton 3.6 raises IMA on out-of-bounds address arithmetic for
645
- # masked-off lanes even though the load mask gates the actual read, so
646
- # clamp the offset. Caller guarantees num_prefills > 0.
647
- safe_offset = tl.minimum(offset, num_prefills - 1)
648
-
649
- seq_len = tl.load(seq_lens_ptr + num_decodes + safe_offset, mask=mask)
650
- qsl_start = tl.load(query_start_loc_ptr + num_decodes + safe_offset, mask=mask)
651
- qsl_end = tl.load(query_start_loc_ptr + num_decodes + safe_offset + 1, mask=mask)
652
-
653
- query_len = qsl_end - qsl_start
654
- prefix_len = seq_len - query_len
655
- gather_len = query_len + tl.minimum(prefix_len, window_size - 1)
656
-
657
- tl.store(prefill_gather_lens_ptr + offset, gather_len, mask=mask)
658
-
659
-
734
@triton.jit(do_not_specialize=["token_offset"])
735
def _compute_swa_indices_and_lens_kernel(
736
swa_indices_ptr,