[2/N][Feat][Perf] Add new warmup infrastructure for JITs. Add predicate filtering for JIT warmup, and migrate Inkling FA4 (#49315)
Signed-off-by: LopezCastroRoberto <rocastro@redhat.com> Signed-off-by: Roberto L. Castro <38211239+LopezCastroRoberto@users.noreply.github.com> Co-authored-by: Lucas Wilkinson <LucasWilkinson@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Roberto L. Castro committed
Aug 11, 2026 at 13:46 UTC
6c95a641e95c0faa6f3aa802d1fd3cce3f3bc3ce
12 files changed
+743
-390
tests/model_executor/test_jit_warmup.py
+65
@@ -105,6 +105,39 @@ def test_trace_dispatch_expands_ranges_dedupes_and_ignores_unused_inputs() -> No
105
]
106
107
108
+def test_warmup_range_uses_custom_advancement() -> None:
109
+ keys = ToyKernel()._trace_dispatch(ToyKernel().dispatch)(
110
+ tokens=WarmupIntRange(
111
+ 1,
112
+ 10,
113
+ advance=lambda value: _next_power_of_2(value) + 1,
114
+ ),
115
+ cfg=_config(),
116
+ )
117
+
118
+ assert [key.block_size for key in keys] == [1, 2, 4, 8, 16]
119
+
120
+
121
+def test_warmup_range_validates_custom_advancement() -> None:
122
+ trace_dispatch = ToyKernel()._trace_dispatch(ToyKernel().dispatch)
123
+
124
+ with pytest.raises(ValueError, match="both step and advance"):
125
+ trace_dispatch(
126
+ tokens=WarmupIntRange(
127
+ 1,
128
+ 10,
129
+ step=2,
130
+ advance=lambda value: value + 1,
131
+ ),
132
+ cfg=_config(),
133
+ )
134
+ with pytest.raises(ValueError, match="must return a greater value"):
135
+ trace_dispatch(
136
+ tokens=WarmupIntRange(1, 10, advance=lambda value: value),
137
+ cfg=_config(),
138
+ )
139
+
140
+
141
def test_compile_key_uses_defaults_locals_attributes_and_expressions() -> None:
142
cfg = _config(bias=3, disabled=True, name="cfg", vectorized=True)
143
@@ -143,6 +176,38 @@ def test_trace_dispatch_combines_zipped_rows_with_independent_values() -> None:
176
]
177
178
179
+def test_trace_dispatch_filters_with_traced_predicate() -> None:
180
+ class PredicateKernel(ToyKernel):
181
+ def _is_valid_warmup_dispatch(
182
+ self,
183
+ *,
184
+ tokens: int,
185
+ lanes: int,
186
+ max_work: int,
187
+ ) -> bool:
188
+ block_size = _next_power_of_2(tokens)
189
+ return block_size * lanes <= max_work
190
+
191
+ def get_warmup_keys(
192
+ self, max_tokens: int, cfg: Any
193
+ ) -> list[ToyKernel.CompileKey]:
194
+ return self._trace_dispatch(self.dispatch)(
195
+ tokens=WarmupIntRange(1, max_tokens + 1),
196
+ lanes=(1, 2),
197
+ cfg=cfg,
198
+ max_work=4,
199
+ _when=self._is_valid_warmup_dispatch,
200
+ )
201
+
202
+ assert PredicateKernel().get_warmup_keys(5, _config()) == [
203
+ ToyKernel.CompileKey(1, 1, 1, ("base", "default", -1, 1, 1), True),
204
+ ToyKernel.CompileKey(1, 2, 1, ("base", "default", -1, 1, 1), True),
205
+ ToyKernel.CompileKey(2, 2, 1, ("base", "default", -2, 2, 4), True),
206
+ ToyKernel.CompileKey(2, 4, 1, ("base", "default", -2, 2, 4), True),
207
+ ToyKernel.CompileKey(4, 4, 1, ("base", "default", -4, 1, 16), True),
208
+ ]
209
+
210
+
211
def test_zip_inputs_validates_input_rows() -> None:
212
with pytest.raises(ValueError, match="requires at least one"):
213
zip_inputs()
tests/models/inkling/test_fa4_rel_attention.py
+4
-4
@@ -2,8 +2,8 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""Correctness test for the Inkling FA4 relative-attention score-mod kernel.
4
5
-Checks ``inkling_fa4_rel_attention`` against a pure-PyTorch reference that
6
-implements the relative bias exactly as documented in the Inkling architecture
5
+Checks ``INKLING_FA4_REL_ATTENTION_KERNEL`` against a pure-PyTorch reference
6
+that implements the relative bias exactly as documented in the Inkling architecture
7
guide::
8
9
logit(i, j, h) = (1 / head_dim) * dot(q[i, h], k[j, h]) + rel_bias(i, j, h)
@@ -23,9 +23,9 @@ from vllm.models.inkling.nvidia.attention import (
23
compute_log_scaling_tau,
24
)
25
from vllm.models.inkling.nvidia.ops.fa4_rel_attention import (
26
+ INKLING_FA4_REL_ATTENTION_KERNEL,
27
_use_sheared_bias,
28
inkling_fa4_num_splits,
28
- inkling_fa4_rel_attention,
29
)
30
from vllm.platforms import current_platform
31
from vllm.platforms.interface import DeviceCapability
@@ -279,7 +279,7 @@ def _run_case(seq_lens, num_heads, num_kv_heads, rel_extent, window_left, seed=0
279
num_kv_heads=num_kv_heads,
280
max_kv_len=max(kv_lens),
281
)
282
- out = inkling_fa4_rel_attention(
282
+ out = INKLING_FA4_REL_ATTENTION_KERNEL(
283
q,
284
key_cache,
285
value_cache,
tests/models/inkling/test_fa4_warmup.py
+84
-29
@@ -1,17 +1,47 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+from types import SimpleNamespace
5
+
6
import torch
7
8
+import vllm.models.inkling.nvidia.ops.fa4_rel_attention as fa4_rel_attention
9
+from vllm.models.inkling.configs import InklingMMConfig, InklingModelConfig
10
from vllm.models.inkling.nvidia.ops.fa4_rel_attention import (
11
+ InklingFA4RelAttentionKernel,
12
+ _num_warps_bucket,
13
bucket_max_seqlen_q,
14
inkling_fa4_num_splits,
15
)
10
-from vllm.models.inkling.nvidia.ops.fa4_warmup import (
11
- InklingFA4WarmupConfig,
12
- _iter_compile_units,
13
- _num_warps_bucket,
14
-)
16
+
17
+
18
+def _vllm_config_from_reference_config(config: SimpleNamespace) -> SimpleNamespace:
19
+ return SimpleNamespace(
20
+ model_config=SimpleNamespace(
21
+ hf_config=InklingMMConfig(
22
+ text_config=InklingModelConfig(
23
+ num_attention_heads=config.num_heads,
24
+ num_key_value_heads=config.num_kv_heads,
25
+ head_dim=config.head_dim,
26
+ rel_extent=config.rel_extent,
27
+ swa_num_attention_heads=config.num_heads,
28
+ swa_num_key_value_heads=config.num_kv_heads,
29
+ swa_head_dim=config.head_dim,
30
+ sliding_window_size=config.rel_extent,
31
+ )
32
+ ),
33
+ max_model_len=config.max_kv_len,
34
+ dtype=config.dtype,
35
+ ),
36
+ cache_config=SimpleNamespace(
37
+ cache_dtype="auto",
38
+ block_size=config.block_size,
39
+ ),
40
+ scheduler_config=SimpleNamespace(
41
+ max_num_seqs=config.max_num_reqs,
42
+ max_num_batched_tokens=config.max_num_batched_tokens,
43
+ ),
44
+ )
45
46
47
def test_bucket_max_seqlen_q():
@@ -28,8 +58,13 @@ def test_bucket_max_seqlen_q():
58
]
59
60
31
-def test_warmup_enumerates_every_runtime_compile_class():
32
- config = InklingFA4WarmupConfig(
61
+def test_warmup_enumerates_every_runtime_compile_class(monkeypatch):
62
+ monkeypatch.setattr(
63
+ fa4_rel_attention,
64
+ "get_tensor_model_parallel_world_size",
65
+ lambda: 1,
66
+ )
67
+ config = SimpleNamespace(
68
num_heads=16,
69
num_kv_heads=2,
70
head_dim=128,
@@ -43,27 +78,47 @@ def test_warmup_enumerates_every_runtime_compile_class():
78
max_num_reqs=64,
79
max_num_batched_tokens=192,
80
)
46
- warmed_keys = {unit.key[-1] for unit in _iter_compile_units(config)}
81
+ vllm_config = _vllm_config_from_reference_config(config)
82
+ kernel = InklingFA4RelAttentionKernel()
83
+ warmed_keys = set(kernel.get_warmup_keys(vllm_config))
84
+ expected_keys: set[InklingFA4RelAttentionKernel.CompileKey] = set()
85
48
- for query_len in range(1, config.max_num_batched_tokens + 1):
49
- max_seqlen_q = bucket_max_seqlen_q(query_len)
50
- max_num_reqs = min(
51
- config.max_num_reqs,
52
- config.max_num_batched_tokens - query_len + 1,
53
- )
54
- for num_reqs in range(1, max_num_reqs + 1):
55
- num_splits = inkling_fa4_num_splits(
56
- is_local=config.is_local,
57
- batch_size=num_reqs,
58
- max_query_len=max_seqlen_q,
59
- num_heads=config.num_heads,
60
- num_kv_heads=config.num_kv_heads,
61
- max_kv_len=config.max_kv_len,
86
+ for is_local in (False, True):
87
+ max_kv_len = config.rel_extent if is_local else config.max_kv_len
88
+ window_size = (config.rel_extent - 1, 0) if is_local else (-1, -1)
89
+ for query_len in range(1, config.max_num_batched_tokens + 1):
90
+ max_seqlen_q = bucket_max_seqlen_q(query_len)
91
+ max_num_reqs = min(
92
+ config.max_num_reqs,
93
+ config.max_num_batched_tokens - query_len + 1,
94
)
63
- runtime_key = (
64
- max_seqlen_q,
65
- num_splits,
66
- _num_warps_bucket(num_reqs) if num_splits > 1 else None,
67
- num_reqs > 1024,
68
- )
69
- assert runtime_key in warmed_keys
95
+ for num_reqs in range(1, max_num_reqs + 1):
96
+ num_splits = inkling_fa4_num_splits(
97
+ is_local=is_local,
98
+ batch_size=num_reqs,
99
+ max_query_len=max_seqlen_q,
100
+ num_heads=config.num_heads,
101
+ num_kv_heads=config.num_kv_heads,
102
+ max_kv_len=max_kv_len,
103
+ )
104
+ expected_keys.add(
105
+ InklingFA4RelAttentionKernel.CompileKey(
106
+ is_local=is_local,
107
+ num_heads=config.num_heads,
108
+ num_kv_heads=config.num_kv_heads,
109
+ head_dim=config.head_dim,
110
+ rel_extent=config.rel_extent,
111
+ dtype=config.dtype,
112
+ kv_dtype=config.kv_dtype,
113
+ block_size=config.block_size,
114
+ window_size=window_size,
115
+ max_seqlen_q=max_seqlen_q,
116
+ num_splits=num_splits,
117
+ num_warps_bucket=(
118
+ _num_warps_bucket(num_reqs) if num_splits > 1 else None
119
+ ),
120
+ large_num_reqs=num_reqs > 1024,
121
+ )
122
+ )
123
+
124
+ assert warmed_keys == expected_keys
vllm/config/kernel.py
+1
-1
@@ -180,7 +180,7 @@ class KernelConfig:
180
# to the shared JIT warmup infrastructure.
181
# https://github.com/vllm-project/vllm/pull/47451
182
enable_cutedsl_warmup: bool = True
183
- """If True, run CuTeDSL compile warmup during kernel warmup."""
183
+ """Deprecated: run legacy CuTeDSL warmup providers."""
184
185
enable_jit_warmup: bool = True
186
"""If True, run JIT compile warmup during kernel warmup."""
vllm/model_executor/warmup/cutedsl_warmup.py
+1
-1
@@ -1,6 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""Run registered CuTeDSL warmup compile units."""
3
+"""Deprecated compatibility registry for legacy CuTeDSL warmups."""
4
5
# TODO(roberto): Remove this compatibility registry after registered CuTeDSL
6
# warmups are migrated to the shared JIT warmup infrastructure.
vllm/model_executor/warmup/fa4_cutedsl_warmup.py
+21
-2
@@ -1,6 +1,6 @@
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."""
3
+"""Warm up FA4 CuTeDSL kernels."""
4
5
from __future__ import annotations
6
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
12
from vllm.v1.worker.gpu_worker import Worker
13
14
15
-def fa4_cutedsl_warmup(worker: Worker) -> None:
15
+def _warm_fa4_mla_prefill(worker: Worker) -> None:
16
runner = worker.model_runner
17
if runner.is_pooling_model:
18
return
@@ -32,3 +32,22 @@ def fa4_cutedsl_warmup(worker: Worker) -> None:
32
from vllm.v1.attention.backends.mla.prefill import flash_attn
33
34
flash_attn.FA4_MLA_PREFILL_KERNEL.warmup(vllm_config)
35
+
36
+
37
+def _warm_inkling_fa4_rel_attention(worker: Worker) -> None:
38
+ from vllm.models.inkling.configs import InklingMMConfig, InklingModelConfig
39
+ from vllm.models.inkling.nvidia.ops.fa4_rel_attention import (
40
+ INKLING_FA4_REL_ATTENTION_KERNEL,
41
+ )
42
+
43
+ vllm_config = worker.vllm_config
44
+ hf_config = vllm_config.model_config.hf_config
45
+ if not isinstance(hf_config, (InklingMMConfig, InklingModelConfig)):
46
+ return
47
+
48
+ INKLING_FA4_REL_ATTENTION_KERNEL.warmup(vllm_config)
49
+
50
+
51
+def fa4_cutedsl_warmup(worker: Worker) -> None:
52
+ _warm_fa4_mla_prefill(worker)
53
+ _warm_inkling_fa4_rel_attention(worker)
vllm/model_executor/warmup/jit_warmup.py
+149
-52
@@ -28,13 +28,29 @@ CompileKeyT = TypeVar("CompileKeyT")
28
29
@dataclass(frozen=True)
30
class WarmupIntRange:
31
+ """Expand integers with range semantics or a custom monotonic progression."""
32
+
33
start: int
34
stop: int
35
step: int = 1
36
+ advance: Callable[[int], int] | None = None
37
38
39
WarmupValues = Any
40
CompileKeyDispatchFn = Callable[..., CompileKeyT]
41
+WarmupPredicateFn = Callable[..., bool]
42
+_LocalExprs = tuple[tuple[str, ast.AST], ...]
43
+
44
+
45
+def _eval_local_exprs(
46
+ local_exprs: _LocalExprs,
47
+ values: Mapping[str, Any],
48
+ globals_: Mapping[str, Any],
49
+) -> dict[str, Any]:
50
+ evaluated_values = dict(values)
51
+ for name, expr in local_exprs:
52
+ evaluated_values[name] = _eval_dispatch_expr(expr, evaluated_values, globals_)
53
+ return evaluated_values
54
55
56
@dataclass(frozen=True)
@@ -46,7 +62,20 @@ class _WarmupInputRows:
62
63
def _expand_warmup_values(values: WarmupValues) -> tuple[Any, ...]:
64
if isinstance(values, WarmupIntRange):
49
- return tuple(range(values.start, values.stop, values.step))
65
+ if values.advance is None:
66
+ return tuple(range(values.start, values.stop, values.step))
67
+ if values.step != 1:
68
+ raise ValueError("WarmupIntRange cannot set both step and advance")
69
+
70
+ expanded: list[int] = []
71
+ value = values.start
72
+ while value < values.stop:
73
+ expanded.append(value)
74
+ next_value = values.advance(value)
75
+ if next_value <= value:
76
+ raise ValueError("WarmupIntRange.advance must return a greater value")
77
+ value = next_value
78
+ return tuple(expanded)
79
if isinstance(values, (list, tuple)):
80
return tuple(values)
81
return (values,)
@@ -75,20 +104,6 @@ def zip_inputs(*rows: Mapping[str, WarmupValues]) -> _WarmupInputRows:
104
return _WarmupInputRows(rows=tuple(input_rows))
105
106
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
-
107
def _expand_warmup_input_rows(
108
rows: tuple[Mapping[str, WarmupValues], ...],
109
input_names: frozenset[str],
@@ -117,7 +132,7 @@ def _merge_warmup_kwargs(parts: Iterable[Mapping[str, Any]]) -> dict[str, Any]:
132
133
@dataclass(frozen=True)
134
class _CompileKeyDispatchTrace:
120
- local_exprs: tuple[tuple[str, ast.AST], ...]
135
+ local_exprs: _LocalExprs
136
field_exprs: tuple[tuple[str, ast.AST], ...]
137
globals: Mapping[str, Any]
138
input_names: frozenset[str]
@@ -128,11 +143,9 @@ class _CompileKeyDispatchTrace:
143
compile_key_type: type[CompileKeyT],
144
kwargs: Mapping[str, Any],
145
) -> 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
- )
146
+ dispatch_values = _eval_local_exprs(
147
+ self.local_exprs, {**self.defaults, **kwargs}, self.globals
148
+ )
149
return compile_key_type(
150
**{
151
field: _eval_dispatch_expr(expr, dispatch_values, self.globals)
@@ -141,6 +154,23 @@ class _CompileKeyDispatchTrace:
154
)
155
156
157
+@dataclass(frozen=True)
158
+class _WarmupPredicateTrace:
159
+ local_exprs: _LocalExprs
160
+ return_expr: ast.AST
161
+ globals: Mapping[str, Any]
162
+ input_names: frozenset[str]
163
+ defaults: Mapping[str, Any]
164
+
165
+ def matches(self, kwargs: Mapping[str, Any]) -> bool:
166
+ dispatch_values = _eval_local_exprs(
167
+ self.local_exprs, {**self.defaults, **kwargs}, self.globals
168
+ )
169
+ return bool(
170
+ _eval_dispatch_expr(self.return_expr, dispatch_values, self.globals)
171
+ )
172
+
173
+
174
_BIN_OPS: dict[type[ast.operator], Callable[[Any, Any], Any]] = {
175
ast.Add: operator.add,
176
ast.Sub: operator.sub,
@@ -317,10 +347,10 @@ def _collect_input_names(
347
}
348
349
320
-def _collect_dispatch_body(
321
- fn: CompileKeyDispatchFn[Any],
350
+def _collect_expression_body(
351
+ fn: Callable[..., Any],
352
function_def: ast.FunctionDef,
323
-) -> tuple[list[tuple[str, ast.AST]], ast.Call]:
353
+) -> tuple[list[tuple[str, ast.AST]], ast.AST]:
354
local_exprs: list[tuple[str, ast.AST]] = []
355
for statement in function_def.body:
356
if (
@@ -335,7 +365,8 @@ def _collect_dispatch_body(
365
statement.targets[0], ast.Name
366
):
367
raise _dispatch_expr_error(
338
- statement, "Dispatch assignments must target one local name"
368
+ statement,
369
+ "Traced warmup helper assignments must target one local name",
370
)
371
local_exprs.append((statement.targets[0].id, statement.value))
372
continue
@@ -343,29 +374,45 @@ def _collect_dispatch_body(
374
if isinstance(statement, ast.AnnAssign):
375
if statement.value is None:
376
raise _dispatch_expr_error(
346
- statement, "Dispatch annotations must assign a value"
377
+ statement, "Traced warmup helper annotations must assign a value"
378
)
379
if not isinstance(statement.target, ast.Name):
380
raise _dispatch_expr_error(
350
- statement, "Dispatch assignments must target one local name"
381
+ statement,
382
+ "Traced warmup helper assignments must target one local name",
383
)
384
local_exprs.append((statement.target.id, statement.value))
385
continue
386
355
- if isinstance(statement, ast.Return) and isinstance(statement.value, ast.Call):
387
+ if isinstance(statement, ast.Return) and statement.value is not None:
388
return local_exprs, statement.value
389
390
if isinstance(statement, ast.Return):
391
raise _dispatch_expr_error(
360
- statement, "Dispatch must return one CompileKey(...) call"
392
+ statement, "Traced warmup helper must return an expression"
393
)
394
395
raise _dispatch_expr_error(
396
statement,
365
- "Dispatch may only contain local assignments before CompileKey return",
397
+ "Traced warmup helper may only contain local assignments before return",
398
)
399
368
- raise ValueError(f"Expected {fn.__name__} to return one CompileKey(...) call")
400
+ raise ValueError(f"Expected {fn.__name__} to return an expression")
401
+
402
+
403
+def _function_trace_inputs(
404
+ fn: Callable[..., Any],
405
+) -> tuple[dict[str, Any], set[str]]:
406
+ signature = inspect.signature(fn)
407
+ defaults = {
408
+ name: parameter.default
409
+ for name, parameter in signature.parameters.items()
410
+ if parameter.default is not inspect.Parameter.empty
411
+ }
412
+ bound_self = getattr(fn, "__self__", None)
413
+ if bound_self is not None:
414
+ defaults["self"] = bound_self
415
+ return defaults, set(signature.parameters)
416
417
418
def _trace_compile_key_dispatch(
@@ -375,21 +422,20 @@ def _trace_compile_key_dispatch(
422
globals_ = source_fn.__globals__
423
function_def = get_function_source_node(fn)
424
378
- local_exprs, return_call = _collect_dispatch_body(fn, function_def)
425
+ local_exprs, return_expr = _collect_expression_body(fn, function_def)
426
+ if not isinstance(return_expr, ast.Call):
427
+ raise _dispatch_expr_error(
428
+ return_expr,
429
+ "Dispatch must return one CompileKey(...) call",
430
+ )
431
432
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)
433
+ defaults, candidate_names = _function_trace_inputs(fn)
434
input_names: set[str] = set()
435
local_names = {name for name, _ in local_exprs}
436
for _, expr in local_exprs:
437
input_names.update(_collect_input_names(expr, candidate_names))
392
- for keyword in return_call.keywords:
438
+ for keyword in return_expr.keywords:
439
if keyword.arg is None:
440
raise ValueError(f"{fn.__name__} cannot use **kwargs in CompileKey")
441
field_exprs.append((keyword.arg, keyword.value))
@@ -406,6 +452,30 @@ def _trace_compile_key_dispatch(
452
)
453
454
455
+def _trace_warmup_predicate(
456
+ fn: WarmupPredicateFn,
457
+) -> _WarmupPredicateTrace:
458
+ source_fn = getattr(fn, "__func__", fn)
459
+ globals_ = source_fn.__globals__
460
+ function_def = get_function_source_node(fn)
461
+
462
+ local_exprs, return_expr = _collect_expression_body(fn, function_def)
463
+ defaults, candidate_names = _function_trace_inputs(fn)
464
+ input_names: set[str] = set()
465
+ local_names = {name for name, _ in local_exprs}
466
+ for _, expr in local_exprs:
467
+ input_names.update(_collect_input_names(expr, candidate_names))
468
+ input_names.update(_collect_input_names(return_expr, candidate_names, local_names))
469
+
470
+ return _WarmupPredicateTrace(
471
+ tuple(local_exprs),
472
+ return_expr,
473
+ globals_,
474
+ frozenset(input_names),
475
+ defaults,
476
+ )
477
+
478
+
479
class VllmJitKernel(Generic[CompileKeyT], ABC):
480
"""Kernel wrapper that owns dispatch, warmup keys, and compilation."""
481
@@ -424,6 +494,7 @@ class VllmJitKernel(Generic[CompileKeyT], ABC):
494
495
def traced(
496
*input_groups: _WarmupInputRows,
497
+ _when: WarmupPredicateFn | None = None,
498
**kwargs: WarmupValues,
499
) -> list[CompileKeyT]:
500
for group in input_groups:
@@ -432,24 +503,50 @@ class VllmJitKernel(Generic[CompileKeyT], ABC):
503
"_trace_dispatch positional arguments must be "
504
"zip_inputs(...) groups"
505
)
506
+ predicate_trace = (
507
+ _trace_warmup_predicate(_when) if _when is not None else None
508
+ )
509
+ input_names = compile_key_dispatch_trace.input_names
510
+ if predicate_trace is not None:
511
+ input_names = input_names | predicate_trace.input_names
512
expanded_input_groups = tuple(
436
- _expand_warmup_input_rows(
437
- group.rows, compile_key_dispatch_trace.input_names
438
- )
513
+ _expand_warmup_input_rows(group.rows, input_names)
514
for group in input_groups
515
)
441
- expanded_kwargs = _expand_warmup_value_grid(
442
- kwargs, compile_key_dispatch_trace.input_names
516
+ # Expand independent keyword inputs into cartesian-product axes.
517
+ expanded_kwarg_axes = tuple(
518
+ (name, _expand_warmup_values(value))
519
+ for name, value in kwargs.items()
520
+ if name in input_names
521
)
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)
522
+ dispatch_value_axes = (
523
+ *expanded_input_groups,
524
+ *(values for _, values in expanded_kwarg_axes),
525
+ )
526
+ input_group_count = len(expanded_input_groups)
527
+ kwarg_names = tuple(name for name, _ in expanded_kwarg_axes)
528
+ compile_keys: dict[CompileKeyT, None] = {}
529
+ for dispatch_value_set in itertools.product(*dispatch_value_axes):
530
+ dispatch_values = _merge_warmup_kwargs(
531
+ (
532
+ *dispatch_value_set[:input_group_count],
533
+ dict(
534
+ zip(
535
+ kwarg_names,
536
+ dispatch_value_set[input_group_count:],
537
+ )
538
+ ),
539
)
450
- for dispatch_values in itertools.product(*dispatch_value_groups)
540
)
452
- )
541
+ if predicate_trace is not None and not predicate_trace.matches(
542
+ dispatch_values
543
+ ):
544
+ continue
545
+ compile_key = compile_key_dispatch_trace.compile_key(
546
+ self.CompileKey, dispatch_values
547
+ )
548
+ compile_keys[compile_key] = None
549
+ return list(compile_keys)
550
551
return traced
552
vllm/models/inkling/nvidia/attention.py
+2
-22
@@ -35,11 +35,10 @@ from vllm.v1.kv_cache_interface import (
35
from ..configs import InklingModelConfig
36
from .layernorm import InklingRMSNorm
37
from .ops.fa4_rel_attention import (
38
+ INKLING_FA4_REL_ATTENTION_KERNEL,
39
bucket_max_seqlen_q,
40
inkling_fa4_num_splits,
40
- inkling_fa4_rel_attention,
41
)
42
-from .ops.fa4_warmup import InklingFA4WarmupConfig, register_fa4_warmup
42
from .ops.qkvr_prep import fused_qkvr_prep
43
from .sconv_swa_attn import _K, _V, InklingConvState, InklingSconvMetadata
44
from .short_conv import InklingShortConv
@@ -175,25 +174,6 @@ class InklingAttention(nn.Module, AttentionLayerBase):
174
compilation_config.static_forward_context[prefix] = self
175
self.kv_cache = torch.tensor([]) # replaced by bind_kv_cache
176
178
- register_fa4_warmup(
179
- InklingFA4WarmupConfig(
180
- num_heads=self.num_heads,
181
- num_kv_heads=self.num_kv_heads,
182
- head_dim=self.head_dim,
183
- rel_extent=self.rel_extent,
184
- window_size=self.window_size,
185
- is_local=self.is_local,
186
- max_kv_len=self._max_kv_len,
187
- dtype=vllm_config.model_config.dtype,
188
- kv_dtype=self.kv_cache_torch_dtype,
189
- block_size=vllm_config.cache_config.block_size,
190
- max_num_reqs=vllm_config.scheduler_config.max_num_seqs,
191
- max_num_batched_tokens=(
192
- vllm_config.scheduler_config.max_num_batched_tokens
193
- ),
194
- )
195
- )
196
-
177
def get_attn_backend(self) -> type[AttentionBackend]:
178
return FlashAttentionBackend
179
@@ -309,7 +289,7 @@ class InklingAttention(nn.Module, AttentionLayerBase):
289
num_kv_heads=self.num_kv_heads,
290
max_kv_len=self._max_kv_len,
291
)
312
- inkling_fa4_rel_attention(
292
+ INKLING_FA4_REL_ATTENTION_KERNEL(
293
q[:nt],
294
key_cache,
295
value_cache,
vllm/models/inkling/nvidia/ops/__init__.py
+4
-2
@@ -15,11 +15,13 @@ from .sconv import fused_sconv
15
_LAZY_EXPORTS = {
16
"silu_and_mul_triton": "silu_and_mul",
17
"sink_silu_mul_epilogue": "silu_and_mul",
18
- "inkling_fa4_rel_attention": "fa4_rel_attention",
18
+ "INKLING_FA4_REL_ATTENTION_KERNEL": "fa4_rel_attention",
19
}
20
21
if TYPE_CHECKING:
22
- from .fa4_rel_attention import inkling_fa4_rel_attention # noqa: F401
22
+ from .fa4_rel_attention import ( # noqa: F401
23
+ INKLING_FA4_REL_ATTENTION_KERNEL,
24
+ )
25
from .silu_and_mul import ( # noqa: F401
26
silu_and_mul_triton,
27
sink_silu_mul_epilogue,
vllm/models/inkling/nvidia/ops/fa4_rel_attention.py
+333
-67
@@ -3,12 +3,23 @@
3
from __future__ import annotations
4
5
from collections.abc import Callable
6
+from dataclasses import dataclass
7
from functools import cache
7
-from typing import Any
8
+from typing import TYPE_CHECKING, Any
9
10
import torch
11
12
+from vllm.distributed import get_tensor_model_parallel_world_size
13
+from vllm.model_executor.warmup.jit_warmup import (
14
+ VllmJitKernel,
15
+ WarmupIntRange,
16
+ zip_inputs,
17
+)
18
from vllm.platforms import current_platform
19
+from vllm.utils.torch_utils import kv_cache_dtype_str_to_dtype
20
+
21
+if TYPE_CHECKING:
22
+ from vllm.config import VllmConfig
23
24
25
def bucket_max_seqlen_q(max_seqlen_q: int) -> int:
@@ -98,73 +109,328 @@ def inkling_fa4_num_splits(
109
)
110
111
101
-def inkling_fa4_rel_attention(
102
- q: torch.Tensor,
103
- key_cache: torch.Tensor,
104
- value_cache: torch.Tensor,
105
- *,
106
- block_table: torch.Tensor,
107
- cache_seqlens: torch.Tensor,
108
- cu_seqlens_q: torch.Tensor,
109
- max_seqlen_q: int,
110
- softmax_scale: float,
111
- causal: bool,
112
- window_size: tuple[int, int],
113
- rel_extent: int,
114
- rel_logits: torch.Tensor,
115
- num_splits: int = 32,
116
- out: torch.Tensor | None = None,
117
-) -> torch.Tensor:
118
- """Paged varlen FA4 over the bound K/V cache with the Inkling relative bias.
119
-
120
- ``q`` is ``(num_tokens, num_heads, head_dim)``; ``key_cache`` / ``value_cache``
121
- are the paged caches ``(num_blocks, block_size, num_kv_heads, head_dim)``;
122
- ``block_table`` is the per-request page table and ``cache_seqlens`` the
123
- per-request KV lengths (``seqused_k``). ``rel_logits`` is
124
- ``(num_tokens, num_heads, rel_extent)``.
125
-
126
- Hopper uses standard FA4's score-mod gather. Blackwell uses tml-fa4's
127
- sheared relative-bias layout.
128
- """
129
- # cute uses (None, None) to mean "no window".
130
- cute_window = (None, None) if window_size == (-1, -1) else window_size
131
-
132
- rel_logits = rel_logits.contiguous()
133
- flash_attn_varlen_func: Callable[..., Any]
134
- if _use_sheared_bias():
135
- from vllm.third_party.tml_fa4 import (
136
- flash_attn_varlen_func as tml_flash_attn_varlen_func,
112
+def _num_warps_bucket(num_reqs: int) -> int:
113
+ num_warps = min((num_reqs + 30) // 31, 32)
114
+ return 1 << (num_warps - 1).bit_length()
115
+
116
+
117
+class InklingFA4RelAttentionKernel(
118
+ VllmJitKernel["InklingFA4RelAttentionKernel.CompileKey"]
119
+):
120
+ @dataclass(frozen=True)
121
+ class CompileKey:
122
+ is_local: bool
123
+ num_heads: int
124
+ num_kv_heads: int
125
+ head_dim: int
126
+ rel_extent: int
127
+ dtype: torch.dtype
128
+ kv_dtype: torch.dtype
129
+ block_size: int
130
+ window_size: tuple[int, int]
131
+ max_seqlen_q: int
132
+ num_splits: int
133
+ num_warps_bucket: int | None
134
+ large_num_reqs: bool
135
+
136
+ @staticmethod
137
+ def kernel(
138
+ q: torch.Tensor,
139
+ key_cache: torch.Tensor,
140
+ value_cache: torch.Tensor,
141
+ *,
142
+ block_table: torch.Tensor,
143
+ cache_seqlens: torch.Tensor,
144
+ cu_seqlens_q: torch.Tensor,
145
+ max_seqlen_q: int,
146
+ softmax_scale: float,
147
+ causal: bool,
148
+ window_size: tuple[int, int],
149
+ rel_extent: int,
150
+ rel_logits: torch.Tensor,
151
+ num_splits: int = 32,
152
+ out: torch.Tensor | None = None,
153
+ ) -> torch.Tensor:
154
+ """Paged varlen FA4 over the bound K/V cache with the Inkling relative bias.
155
+
156
+ ``q`` is ``(num_tokens, num_heads, head_dim)``; ``key_cache`` / ``value_cache``
157
+ are the paged caches ``(num_blocks, block_size, num_kv_heads, head_dim)``;
158
+ ``block_table`` is the per-request page table and ``cache_seqlens`` the
159
+ per-request KV lengths (``seqused_k``). ``rel_logits`` is
160
+ ``(num_tokens, num_heads, rel_extent)``.
161
+
162
+ Hopper uses standard FA4's score-mod gather. Blackwell uses tml-fa4's
163
+ sheared relative-bias layout.
164
+ """
165
+ # cute uses (None, None) to mean "no window".
166
+ cute_window = (None, None) if window_size == (-1, -1) else window_size
167
+
168
+ rel_logits = rel_logits.contiguous()
169
+ flash_attn_varlen_func: Callable[..., Any]
170
+ if _use_sheared_bias():
171
+ from vllm.third_party.tml_fa4 import (
172
+ flash_attn_varlen_func as tml_flash_attn_varlen_func,
173
+ )
174
+
175
+ flash_attn_varlen_func = tml_flash_attn_varlen_func
176
+ bias_kwargs: dict[str, Any] = {"rel_bias": rel_logits}
177
+ else:
178
+ from vllm.vllm_flash_attn.cute import (
179
+ flash_attn_varlen_func as cute_flash_attn_varlen_func,
180
+ )
181
+
182
+ flash_attn_varlen_func = cute_flash_attn_varlen_func
183
+ bias_kwargs = {
184
+ "score_mod": _get_score_mod(rel_extent),
185
+ "aux_tensors": [rel_logits],
186
+ }
187
+
188
+ ret = flash_attn_varlen_func(
189
+ q=q,
190
+ k=key_cache,
191
+ v=value_cache,
192
+ cu_seqlens_q=cu_seqlens_q,
193
+ seqused_k=cache_seqlens,
194
+ max_seqlen_q=max_seqlen_q,
195
+ page_table=block_table,
196
+ softmax_scale=softmax_scale,
197
+ causal=causal,
198
+ window_size=cute_window,
199
+ num_splits=num_splits,
200
+ return_lse=False,
201
+ out=out,
202
+ **bias_kwargs,
203
)
204
+ if isinstance(ret, tuple):
205
+ return ret[0]
206
+ return ret
207
139
- flash_attn_varlen_func = tml_flash_attn_varlen_func
140
- bias_kwargs: dict[str, Any] = {"rel_bias": rel_logits}
141
- else:
142
- from vllm.vllm_flash_attn.cute import (
143
- flash_attn_varlen_func as cute_flash_attn_varlen_func,
208
+ def dispatch( # type: ignore[override]
209
+ self,
210
+ *,
211
+ is_local: bool,
212
+ num_heads: int,
213
+ num_kv_heads: int,
214
+ head_dim: int,
215
+ rel_extent: int,
216
+ dtype: torch.dtype,
217
+ kv_dtype: torch.dtype,
218
+ block_size: int,
219
+ window_size: tuple[int, int],
220
+ max_kv_len: int,
221
+ query_len: int,
222
+ num_reqs: int,
223
+ ) -> CompileKey:
224
+ max_seqlen_q = bucket_max_seqlen_q(query_len)
225
+ num_splits = inkling_fa4_num_splits(
226
+ is_local=is_local,
227
+ batch_size=num_reqs,
228
+ max_query_len=max_seqlen_q,
229
+ num_heads=num_heads,
230
+ num_kv_heads=num_kv_heads,
231
+ max_kv_len=max_kv_len,
232
+ )
233
+ return self.CompileKey(
234
+ is_local=is_local,
235
+ num_heads=num_heads,
236
+ num_kv_heads=num_kv_heads,
237
+ head_dim=head_dim,
238
+ rel_extent=rel_extent,
239
+ dtype=dtype,
240
+ kv_dtype=kv_dtype,
241
+ block_size=block_size,
242
+ window_size=window_size,
243
+ max_seqlen_q=max_seqlen_q,
244
+ num_splits=num_splits,
245
+ num_warps_bucket=(_num_warps_bucket(num_reqs) if num_splits > 1 else None),
246
+ large_num_reqs=num_reqs > 1024,
247
)
248
146
- flash_attn_varlen_func = cute_flash_attn_varlen_func
147
- bias_kwargs = {
148
- "score_mod": _get_score_mod(rel_extent),
149
- "aux_tensors": [rel_logits],
150
- }
151
-
152
- ret = flash_attn_varlen_func(
153
- q=q,
154
- k=key_cache,
155
- v=value_cache,
156
- cu_seqlens_q=cu_seqlens_q,
157
- seqused_k=cache_seqlens,
158
- max_seqlen_q=max_seqlen_q,
159
- page_table=block_table,
160
- softmax_scale=softmax_scale,
161
- causal=causal,
162
- window_size=cute_window,
163
- num_splits=num_splits,
164
- return_lse=False,
165
- out=out,
166
- **bias_kwargs,
167
- )
168
- if isinstance(ret, tuple):
169
- return ret[0]
170
- return ret
249
+ def _is_valid_warmup_dispatch(
250
+ self,
251
+ *,
252
+ query_len: int,
253
+ num_reqs: int,
254
+ max_num_batched_tokens: int,
255
+ ) -> bool:
256
+ return query_len + num_reqs <= max_num_batched_tokens + 1
257
+
258
+ def get_warmup_keys(self, vllm_config: VllmConfig) -> list[CompileKey]:
259
+ max_num_reqs = vllm_config.scheduler_config.max_num_seqs
260
+ max_num_batched_tokens = vllm_config.scheduler_config.max_num_batched_tokens
261
+ if max_num_reqs <= 0 or max_num_batched_tokens <= 0:
262
+ return []
263
+
264
+ hf_config = vllm_config.model_config.hf_config
265
+ get_text_config = getattr(hf_config, "get_text_config", None)
266
+ config = get_text_config() if callable(get_text_config) else hf_config
267
+
268
+ tp_size = get_tensor_model_parallel_world_size()
269
+ dtype = vllm_config.model_config.dtype
270
+ kv_dtype = kv_cache_dtype_str_to_dtype(
271
+ vllm_config.cache_config.cache_dtype,
272
+ vllm_config.model_config,
273
+ )
274
+ block_size = vllm_config.cache_config.block_size
275
+ local_extent = config.sliding_window_size
276
+
277
+ global_num_kv_heads = config.num_key_value_heads
278
+ local_num_kv_heads = config.swa_num_key_value_heads
279
+ assert config.num_attention_heads % tp_size == 0
280
+ assert config.swa_num_attention_heads % tp_size == 0
281
+ if global_num_kv_heads >= tp_size:
282
+ assert global_num_kv_heads % tp_size == 0
283
+ else:
284
+ assert tp_size % global_num_kv_heads == 0
285
+ if local_num_kv_heads >= tp_size:
286
+ assert local_num_kv_heads % tp_size == 0
287
+ else:
288
+ assert tp_size % local_num_kv_heads == 0
289
+
290
+ return self._trace_dispatch(self.dispatch)(
291
+ zip_inputs(
292
+ dict(
293
+ is_local=False,
294
+ num_heads=config.num_attention_heads // tp_size,
295
+ num_kv_heads=max(1, global_num_kv_heads // tp_size),
296
+ head_dim=config.head_dim,
297
+ rel_extent=config.rel_extent,
298
+ dtype=dtype,
299
+ kv_dtype=kv_dtype,
300
+ block_size=block_size,
301
+ window_size=(-1, -1),
302
+ max_kv_len=vllm_config.model_config.max_model_len,
303
+ ),
304
+ dict(
305
+ is_local=True,
306
+ num_heads=config.swa_num_attention_heads // tp_size,
307
+ num_kv_heads=max(1, local_num_kv_heads // tp_size),
308
+ head_dim=config.swa_head_dim,
309
+ rel_extent=local_extent,
310
+ dtype=dtype,
311
+ kv_dtype=kv_dtype,
312
+ block_size=block_size,
313
+ window_size=(local_extent - 1, 0),
314
+ max_kv_len=local_extent,
315
+ ),
316
+ ),
317
+ query_len=WarmupIntRange(
318
+ 1,
319
+ max_num_batched_tokens + 1,
320
+ advance=lambda value: bucket_max_seqlen_q(value) + 1,
321
+ ),
322
+ num_reqs=WarmupIntRange(1, max_num_reqs + 1),
323
+ max_num_batched_tokens=max_num_batched_tokens,
324
+ _when=self._is_valid_warmup_dispatch,
325
+ )
326
+
327
+ def compile(self, compile_key: CompileKey) -> None:
328
+ from torch._subclasses.fake_tensor import FakeTensorMode
329
+
330
+ if compile_key.num_splits == 1 or compile_key.num_warps_bucket is None:
331
+ num_reqs = 1025 if compile_key.large_num_reqs else 1
332
+ else:
333
+ min_num_warps = (
334
+ 1
335
+ if compile_key.num_warps_bucket == 1
336
+ else compile_key.num_warps_bucket // 2 + 1
337
+ )
338
+ min_num_reqs = (min_num_warps - 1) * 31 + 1
339
+ num_reqs = max(
340
+ min_num_reqs,
341
+ 1025 if compile_key.large_num_reqs else 1,
342
+ )
343
+
344
+ with FakeTensorMode():
345
+ device = torch.accelerator.current_accelerator()
346
+ total_q = compile_key.max_seqlen_q + num_reqs - 1
347
+ q = torch.empty(
348
+ total_q,
349
+ compile_key.num_heads,
350
+ compile_key.head_dim,
351
+ dtype=compile_key.dtype,
352
+ device=device,
353
+ )
354
+ kv = torch.empty(
355
+ 1,
356
+ 2,
357
+ compile_key.block_size,
358
+ compile_key.num_kv_heads,
359
+ compile_key.head_dim,
360
+ dtype=compile_key.kv_dtype,
361
+ device=device,
362
+ )
363
+ key_cache, value_cache = kv.unbind(1)
364
+ self.kernel(
365
+ q,
366
+ key_cache,
367
+ value_cache,
368
+ block_table=torch.empty(
369
+ num_reqs,
370
+ 1,
371
+ dtype=torch.int32,
372
+ device=device,
373
+ ),
374
+ cache_seqlens=torch.empty(
375
+ num_reqs,
376
+ dtype=torch.int32,
377
+ device=device,
378
+ ),
379
+ cu_seqlens_q=torch.empty(
380
+ num_reqs + 1,
381
+ dtype=torch.int32,
382
+ device=device,
383
+ ),
384
+ max_seqlen_q=compile_key.max_seqlen_q,
385
+ softmax_scale=compile_key.head_dim**-1,
386
+ causal=True,
387
+ window_size=compile_key.window_size,
388
+ rel_extent=compile_key.rel_extent,
389
+ rel_logits=torch.empty(
390
+ total_q,
391
+ compile_key.num_heads,
392
+ compile_key.rel_extent,
393
+ dtype=compile_key.dtype,
394
+ device=device,
395
+ ),
396
+ num_splits=compile_key.num_splits,
397
+ out=torch.empty_like(q),
398
+ )
399
+
400
+ def __call__(
401
+ self,
402
+ q: torch.Tensor,
403
+ key_cache: torch.Tensor,
404
+ value_cache: torch.Tensor,
405
+ *,
406
+ block_table: torch.Tensor,
407
+ cache_seqlens: torch.Tensor,
408
+ cu_seqlens_q: torch.Tensor,
409
+ max_seqlen_q: int,
410
+ softmax_scale: float,
411
+ causal: bool,
412
+ window_size: tuple[int, int],
413
+ rel_extent: int,
414
+ rel_logits: torch.Tensor,
415
+ num_splits: int = 32,
416
+ out: torch.Tensor | None = None,
417
+ ) -> torch.Tensor:
418
+ return self.kernel(
419
+ q,
420
+ key_cache,
421
+ value_cache,
422
+ block_table=block_table,
423
+ cache_seqlens=cache_seqlens,
424
+ cu_seqlens_q=cu_seqlens_q,
425
+ max_seqlen_q=max_seqlen_q,
426
+ softmax_scale=softmax_scale,
427
+ causal=causal,
428
+ window_size=window_size,
429
+ rel_extent=rel_extent,
430
+ rel_logits=rel_logits,
431
+ num_splits=num_splits,
432
+ out=out,
433
+ )
434
+
435
+
436
+INKLING_FA4_REL_ATTENTION_KERNEL = InklingFA4RelAttentionKernel()
vllm/models/inkling/nvidia/ops/fa4_warmup.py
deleted
-154
@@ -1,154 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""Startup compilation of the FA4 kernels used by Inkling."""
4
-
5
-from __future__ import annotations
6
-
7
-from collections.abc import Iterator
8
-from dataclasses import dataclass
9
-from functools import partial
10
-
11
-import torch
12
-
13
-from vllm.model_executor.warmup.cutedsl_warmup import (
14
- CuTeDSLCompileUnit,
15
- register_cutedsl_warmup_provider,
16
-)
17
-
18
-from .fa4_rel_attention import (
19
- bucket_max_seqlen_q,
20
- inkling_fa4_num_splits,
21
- inkling_fa4_rel_attention,
22
-)
23
-
24
-
25
-@dataclass(frozen=True)
26
-class InklingFA4WarmupConfig:
27
- num_heads: int
28
- num_kv_heads: int
29
- head_dim: int
30
- rel_extent: int
31
- window_size: tuple[int, int]
32
- is_local: bool
33
- max_kv_len: int
34
- dtype: torch.dtype
35
- kv_dtype: torch.dtype
36
- block_size: int
37
- max_num_reqs: int
38
- max_num_batched_tokens: int
39
-
40
-
41
-def _num_warps_bucket(num_reqs: int) -> int:
42
- num_warps = min((num_reqs + 30) // 31, 32)
43
- return 1 << (num_warps - 1).bit_length()
44
-
45
-
46
-def _compile(config: InklingFA4WarmupConfig, max_seqlen_q: int, num_reqs: int) -> None:
47
- from torch._subclasses.fake_tensor import FakeTensorMode
48
-
49
- num_splits = inkling_fa4_num_splits(
50
- is_local=config.is_local,
51
- batch_size=num_reqs,
52
- max_query_len=max_seqlen_q,
53
- num_heads=config.num_heads,
54
- num_kv_heads=config.num_kv_heads,
55
- max_kv_len=config.max_kv_len,
56
- )
57
- with FakeTensorMode():
58
- device = torch.accelerator.current_accelerator()
59
- total_q = max_seqlen_q + num_reqs - 1
60
- q = torch.empty(
61
- total_q,
62
- config.num_heads,
63
- config.head_dim,
64
- dtype=config.dtype,
65
- device=device,
66
- )
67
- kv = torch.empty(
68
- 1,
69
- 2,
70
- config.block_size,
71
- config.num_kv_heads,
72
- config.head_dim,
73
- dtype=config.kv_dtype,
74
- device=device,
75
- )
76
- key_cache, value_cache = kv.unbind(1)
77
- inkling_fa4_rel_attention(
78
- q,
79
- key_cache,
80
- value_cache,
81
- block_table=torch.empty(num_reqs, 1, dtype=torch.int32, device=device),
82
- cache_seqlens=torch.empty(num_reqs, dtype=torch.int32, device=device),
83
- cu_seqlens_q=torch.empty(num_reqs + 1, dtype=torch.int32, device=device),
84
- max_seqlen_q=max_seqlen_q,
85
- softmax_scale=1.0 / config.head_dim,
86
- causal=True,
87
- window_size=config.window_size,
88
- rel_extent=config.rel_extent,
89
- rel_logits=torch.empty(
90
- total_q,
91
- config.num_heads,
92
- config.rel_extent,
93
- dtype=config.dtype,
94
- device=device,
95
- ),
96
- num_splits=num_splits,
97
- out=torch.empty_like(q),
98
- )
99
-
100
-
101
-def _iter_compile_units(
102
- config: InklingFA4WarmupConfig,
103
-) -> Iterator[CuTeDSLCompileUnit]:
104
- max_bucket = bucket_max_seqlen_q(config.max_num_batched_tokens)
105
- max_seqlen_q = 1
106
- while max_seqlen_q <= max_bucket:
107
- min_query_len = max_seqlen_q // 2 + 1
108
- max_num_reqs = min(
109
- config.max_num_reqs,
110
- config.max_num_batched_tokens - min_query_len + 1,
111
- )
112
- seen: set[tuple[int, int, int | None, bool]] = set()
113
- for num_reqs in range(1, max_num_reqs + 1):
114
- num_splits = inkling_fa4_num_splits(
115
- is_local=config.is_local,
116
- batch_size=num_reqs,
117
- max_query_len=max_seqlen_q,
118
- num_heads=config.num_heads,
119
- num_kv_heads=config.num_kv_heads,
120
- max_kv_len=config.max_kv_len,
121
- )
122
- key = (
123
- max_seqlen_q,
124
- num_splits,
125
- _num_warps_bucket(num_reqs) if num_splits > 1 else None,
126
- num_reqs > 1024,
127
- )
128
- if key in seen:
129
- continue
130
- seen.add(key)
131
- yield CuTeDSLCompileUnit(
132
- name="inkling_fa4",
133
- key=("inkling_fa4", config, key),
134
- compile=partial(_compile, config, max_seqlen_q, num_reqs),
135
- )
136
- max_seqlen_q *= 2
137
-
138
-
139
-class _WarmupProvider:
140
- def __init__(self) -> None:
141
- self.configs: set[InklingFA4WarmupConfig] = set()
142
-
143
- def get_cutedsl_warmup_compile_units(self) -> tuple[CuTeDSLCompileUnit, ...]:
144
- return tuple(
145
- unit for config in self.configs for unit in _iter_compile_units(config)
146
- )
147
-
148
-
149
-_PROVIDER = _WarmupProvider()
150
-
151
-
152
-def register_fa4_warmup(config: InklingFA4WarmupConfig) -> None:
153
- _PROVIDER.configs.add(config)
154
- register_cutedsl_warmup_provider(_PROVIDER)
vllm/v1/attention/backends/mla/prefill/flash_attn.py
+79
-56
@@ -13,7 +13,7 @@ import vllm.envs as envs
13
from vllm.model_executor.layers.quantization.utils.quant_utils import (
14
kFp8StaticTensorSym,
15
)
16
-from vllm.model_executor.warmup.jit_warmup import VllmJitKernel
16
+from vllm.model_executor.warmup.jit_warmup import VllmJitKernel, zip_inputs
17
from vllm.platforms import current_platform
18
from vllm.v1.attention.backends.fa_utils import (
19
compile_flash_attn_varlen_func_from_specs,
@@ -57,12 +57,6 @@ FA4_MLA_PREFILL_CAUSAL_OPTIONS = (False, True)
57
FA4_MLA_PREFILL_LSE_OPTIONS = (False, True)
58
59
60
-@dataclass(frozen=True)
61
-class _FA4MLAPrefillShapeProbe:
62
- max_seqlen_q: int
63
- max_seqlen_k: int
64
-
65
-
60
class FA4MLAPrefillKernel(VllmJitKernel["FA4MLAPrefillKernel.CompileKey"]):
61
"""FA4 MLA prefill compile-key wrapper used by generic JIT warmup."""
62
@@ -92,6 +86,16 @@ class FA4MLAPrefillKernel(VllmJitKernel["FA4MLAPrefillKernel.CompileKey"]):
86
return_softmax_lse: bool = False
87
num_splits: int = 0
88
89
+ @staticmethod
90
+ def kernel(
91
+ *args: Any,
92
+ runtime_kernel: Callable[..., Any] | None = None,
93
+ **kwargs: Any,
94
+ ) -> Any:
95
+ kernel = flash_attn_varlen_func if runtime_kernel is None else runtime_kernel
96
+ assert kernel is not None
97
+ return kernel(*args, **kwargs)
98
+
99
def dispatch( # type: ignore[override]
100
self,
101
*,
@@ -99,7 +103,8 @@ class FA4MLAPrefillKernel(VllmJitKernel["FA4MLAPrefillKernel.CompileKey"]):
103
dtype: torch.dtype,
104
num_heads: int,
105
mla_dims: "MLADims",
102
- shape_probe: _FA4MLAPrefillShapeProbe,
106
+ max_seqlen_q: int,
107
+ max_seqlen_k: int,
108
requires_v_padding: bool,
109
causal: bool,
110
fa_version: int,
@@ -109,17 +114,17 @@ class FA4MLAPrefillKernel(VllmJitKernel["FA4MLAPrefillKernel.CompileKey"]):
114
) -> CompileKey:
115
return self.CompileKey(
116
q_shape=(
112
- batch_size * shape_probe.max_seqlen_q,
117
+ batch_size * max_seqlen_q,
118
num_heads,
119
mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim,
120
),
121
k_shape=(
117
- batch_size * shape_probe.max_seqlen_k,
122
+ batch_size * max_seqlen_k,
123
num_heads,
124
mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim,
125
),
126
v_shape=(
122
- batch_size * shape_probe.max_seqlen_k,
127
+ batch_size * max_seqlen_k,
128
num_heads,
129
(
130
mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim
@@ -128,8 +133,8 @@ class FA4MLAPrefillKernel(VllmJitKernel["FA4MLAPrefillKernel.CompileKey"]):
133
),
134
),
135
q_dtype=dtype,
131
- max_seqlen_q=shape_probe.max_seqlen_q,
132
- max_seqlen_k=shape_probe.max_seqlen_k,
136
+ max_seqlen_q=max_seqlen_q,
137
+ max_seqlen_k=max_seqlen_k,
138
softmax_scale=(mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim)
139
** -0.5,
140
causal=causal,
@@ -150,6 +155,28 @@ class FA4MLAPrefillKernel(VllmJitKernel["FA4MLAPrefillKernel.CompileKey"]):
155
num_splits=num_splits,
156
)
157
158
+ def _is_valid_warmup_shape_probe(
159
+ self,
160
+ *,
161
+ max_seqlen_k: int,
162
+ num_splits: int,
163
+ is_sm90: bool,
164
+ qk_head_dim: int,
165
+ effective_v_head_dim: int,
166
+ ) -> bool:
167
+ long_k = FA4_MLA_PREFILL_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE
168
+ very_long_k = FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE
169
+ return (
170
+ max_seqlen_k < long_k
171
+ or (num_splits != 1 and max_seqlen_k == long_k)
172
+ or (
173
+ num_splits != 1
174
+ and max_seqlen_k == very_long_k
175
+ and not is_sm90
176
+ and qk_head_dim != effective_v_head_dim
177
+ )
178
+ )
179
+
180
def get_warmup_keys(self, vllm_config: "VllmConfig") -> list[CompileKey]:
181
from vllm.model_executor.layers.attention.mla_attention import (
182
get_mla_dims,
@@ -185,58 +212,49 @@ class FA4MLAPrefillKernel(VllmJitKernel["FA4MLAPrefillKernel.CompileKey"]):
212
effective_v_head_dim = (
213
qk_head_dim if requires_v_padding else mla_dims.v_head_dim
214
)
188
- shape_probes = [
189
- _FA4MLAPrefillShapeProbe(1, FA4_MLA_PREFILL_K_TILE),
190
- _FA4MLAPrefillShapeProbe(
191
- FA4_MLA_PREFILL_Q_TILE + 1,
192
- 4 * FA4_MLA_PREFILL_K_TILE,
215
+ shape_probes = (
216
+ dict(max_seqlen_q=1, max_seqlen_k=FA4_MLA_PREFILL_K_TILE),
217
+ dict(
218
+ max_seqlen_q=FA4_MLA_PREFILL_Q_TILE + 1,
219
+ max_seqlen_k=4 * FA4_MLA_PREFILL_K_TILE,
220
),
194
- ]
195
- if num_splits != 1:
196
- shape_probes.extend(
197
- (
198
- _FA4MLAPrefillShapeProbe(
199
- 1,
200
- FA4_MLA_PREFILL_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE,
201
- ),
202
- _FA4MLAPrefillShapeProbe(
203
- FA4_MLA_PREFILL_Q_TILE + 1,
204
- FA4_MLA_PREFILL_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE,
205
- ),
206
- )
207
- )
208
- if not is_sm90 and qk_head_dim != effective_v_head_dim:
209
- shape_probes.extend(
210
- (
211
- _FA4MLAPrefillShapeProbe(
212
- 1,
213
- FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE,
214
- ),
215
- _FA4MLAPrefillShapeProbe(
216
- FA4_MLA_PREFILL_Q_TILE + 1,
217
- FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE,
218
- ),
219
- )
220
- )
221
+ dict(
222
+ max_seqlen_q=1,
223
+ max_seqlen_k=FA4_MLA_PREFILL_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE,
224
+ ),
225
+ dict(
226
+ max_seqlen_q=FA4_MLA_PREFILL_Q_TILE + 1,
227
+ max_seqlen_k=FA4_MLA_PREFILL_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE,
228
+ ),
229
+ dict(
230
+ max_seqlen_q=1,
231
+ max_seqlen_k=FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS
232
+ * FA4_MLA_PREFILL_K_TILE,
233
+ ),
234
+ dict(
235
+ max_seqlen_q=FA4_MLA_PREFILL_Q_TILE + 1,
236
+ max_seqlen_k=FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS
237
+ * FA4_MLA_PREFILL_K_TILE,
238
+ ),
239
+ )
240
241
return self._trace_dispatch(self.dispatch)(
242
+ zip_inputs(*shape_probes),
243
batch_size=FA4_MLA_PREFILL_COMPILE_BATCH_SIZE,
244
dtype=dtype,
245
num_heads=num_heads,
246
mla_dims=mla_dims,
227
- shape_probe=shape_probes,
247
requires_v_padding=requires_v_padding,
248
+ is_sm90=is_sm90,
249
+ qk_head_dim=qk_head_dim,
250
+ effective_v_head_dim=effective_v_head_dim,
251
causal=FA4_MLA_PREFILL_CAUSAL_OPTIONS,
252
return_lse=FA4_MLA_PREFILL_LSE_OPTIONS,
253
num_splits=num_splits,
254
fa_version=fa_version,
255
+ _when=self._is_valid_warmup_shape_probe,
256
)
257
235
- @staticmethod
236
- def kernel(*args: Any, **kwargs: Any) -> Any:
237
- assert flash_attn_varlen_func is not None
238
- return flash_attn_varlen_func(*args, **kwargs)
239
-
258
def compile(self, compile_key: CompileKey) -> None:
259
assert compile_flash_attn_varlen_func_from_specs is not None
260
window_size = (
@@ -271,11 +289,13 @@ class FA4MLAPrefillKernel(VllmJitKernel["FA4MLAPrefillKernel.CompileKey"]):
289
runtime_kernel: Callable[..., Any] | None = None,
290
**kwargs: Any,
291
) -> Any:
274
- kernel = self.kernel if runtime_kernel is None else runtime_kernel
275
- return kernel(q=q, k=k, v=v, **kwargs)
276
-
277
-
278
-FA4_MLA_PREFILL_KERNEL = FA4MLAPrefillKernel()
292
+ return self.kernel(
293
+ q=q,
294
+ k=k,
295
+ v=v,
296
+ runtime_kernel=runtime_kernel,
297
+ **kwargs,
298
+ )
299
300
301
class FlashAttnPrefillBackend(MLAPrefillBackend):
@@ -470,3 +490,6 @@ class FlashAttnPrefillBackend(MLAPrefillBackend):
490
causal=False, # Context is unmasked
491
return_softmax_lse=True,
492
)
493
+
494
+
495
+FA4_MLA_PREFILL_KERNEL = FA4MLAPrefillKernel()