[1/N][warmup][DSv4] Migrate generic MLA metadata and indexing kernels (#50175)

Signed-off-by: LopezCastroRoberto <rocastro@redhat.com> Co-authored-by: Codex <codex@openai.com>

Roberto L. Castro committed Sep 1, 2026 at 05:26 UTC e16b5e518db8ed54a9c82dd825d3a3d5502ef13e
17 files changed +1269 -625
docs/contributing/jit_kernel_warmup.md
+10 -8
@@ -17,7 +17,7 @@ Each warmable kernel defines its compile-key mapping and compile-only entry poin
17
18 Here, a **kernel wrapper** (or just **wrapper**) is an instance of a concrete `VllmJitKernel` subclass.
19
20 -Expose one wrapper near the kernel's normal runtime entry point. Prefer this shape:
20 +Expose one wrapper near the kernel's normal runtime entry point. Prefer this backend-agnostic shape:
21
22 ```python
23 class MyKernel(VllmJitKernel["MyKernel.CompileKey"]):
@@ -43,14 +43,14 @@ class MyKernel(VllmJitKernel["MyKernel.CompileKey"]):
43 return self.kernel(...)
44
45
46 -MY_KERNEL = MyKernel()
46 +_MY_KERNEL = MyKernel()
47 ```
48
49 `CompileKey`, `dispatch(...)`, and `get_warmup_keys(...)` are backend-agnostic. Backend-specific behavior belongs in `kernel(...)`, `compile(...)`, and `__call__(...)`.
50
51 The module-level singleton should be used by warmup and by the runtime call path. This keeps dispatch behavior shared instead of duplicated.
52
53 -`VllmJitKernel.warmup(...)` compiles every key returned by `get_warmup_keys(...)`; wrappers should not reimplement it.
53 +`VllmJitKernel.warmup(...)` materializes every key returned by `get_warmup_keys(...)`; wrappers should not reimplement it.
54
55 ### Choose Compile-Key Fields
56
@@ -70,18 +70,20 @@ def get_warmup_keys(self, vllm_config: VllmConfig) -> list[CompileKey]:
70
71 Use independent ranges or alternatives for cartesian products, `zip_inputs(...)` for coupled rows, and `_when` for validity constraints. The complete syntax is documented in [Search-Space Reference](#2-search-space-reference).
72
73 -### Compile Without Launching
73 +### Materialize Without Launching
74
75 -`compile(compile_key)` means "make this specialization available". Depending on the backend, that may compile from source, call a compile-only API, load an already-built artifact, or compile on cache miss.
75 +The base contract uses `compile(compile_key)` internally to mean "make this specialization available". Depending on the backend, that may compile from source, call a compile-only API, load an already-built artifact, or compile on cache miss. Contributors only implement this hook when their backend helper does not provide it.
76
77 -`compile(...)` should not launch a real inference workload or allocate real tensors. Each DSL should expose fake tensor/spec descriptors suitable for compilation only.
77 +For Triton, implement `warmup_inputs(...)`. `@kernel_launcher` binds those inputs to `__call__(...)`, whose body returns the runtime grid and derived launch arguments as a `LaunchSpec`. The shared helper then routes that specification to the normal kernel launch or to `kernel.warmup(...)` during startup.
78 +
79 +Materialization should not launch a real inference workload or allocate real tensors. `TritonWarmupTensor(..., strides=None)` represents compact row-major storage; provide explicit representative strides for padded, transposed, or otherwise strided runtime inputs.
80
81 ### Register the Selected Wrapper
82
83 Register the wrapper where the runtime implementation is selected:
84
85 ```python
84 -MY_KERNEL.register_warmup()
86 +_MY_KERNEL.register_warmup()
87 ```
88
89 Registration records metadata only. It does not compile or launch the kernel. Repeated registrations from equivalent layers are allowed and deduplicated later.
@@ -92,7 +94,7 @@ Registration records metadata only. It does not compile or launch the kernel. Re
94 - Keep specialization mapping in `dispatch(...)` instead of duplicating it in warmup code.
95 - Use fake tensors or backend compile-only descriptors; never perform a dummy runtime launch.
96 - Keep registration metadata-only so model construction remains cheap and side-effect free.
95 -- Compile registered kernels only through `kernel_warmup()`.
97 +- Materialize registered kernels only through `kernel_warmup()`.
98 - Keep runtime execution and startup compilation separate and easy to review.
99 - Use one module-level wrapper instance for registration and runtime calls.
100
tests/model_executor/layers/test_fused_shared_expert.py
+1
@@ -553,6 +553,7 @@ def test_models_fse_init(
553 method="mtp",
554 parallel_drafting=False,
555 enable_adaptive_verification=False,
556 + use_dspark=lambda: False,
557 )
558 mtp = DeepSeekV4MTP(vllm_config=vllm_config)
559 assert model.is_fused_shared_expert_enabled is (fse_enabled and not exclude)
tests/model_executor/test_jit_warmup.py
+29
@@ -590,6 +590,35 @@ def test_registry_passes_vllm_config_to_default_requests() -> None:
590 assert kernel.compiled == [ConfigKernel.CompileKey(value=7)]
591
592
593 +def test_registry_default_request_supports_config_free_kernel() -> None:
594 + class ConfigFreeKernel(VllmJitKernel["ConfigFreeKernel.CompileKey"]):
595 + @dataclass(frozen=True)
596 + class CompileKey:
597 + value: int
598 +
599 + def __init__(self) -> None:
600 + self.compiled: list[ConfigFreeKernel.CompileKey] = []
601 + super().__init__()
602 +
603 + def dispatch(self, *, value: int) -> CompileKey: # type: ignore[override]
604 + return self.CompileKey(value=value)
605 +
606 + def get_warmup_keys(self) -> list[CompileKey]:
607 + return [self.dispatch(value=7)]
608 +
609 + def compile(self, compile_key: CompileKey) -> None:
610 + self.compiled.append(compile_key)
611 +
612 + registry = JitWarmupRegistry(_config())
613 + kernel = ConfigFreeKernel()
614 +
615 + with registry.activate():
616 + kernel.register_warmup()
617 + registry.warmup()
618 +
619 + assert kernel.compiled == [ConfigFreeKernel.CompileKey(value=7)]
620 +
621 +
622 def test_get_ast_full_name_handles_names_attributes_and_other_nodes() -> None:
623 dotted_expr = ast.parse("foo.bar.baz").body[0]
624 call_expr = ast.parse("foo()").body[0]
tests/v1/attention/test_indexer_deepseek_v4_slot_mapping.py
+43 -2
@@ -8,10 +8,16 @@ import torch
8
9 from tests.v1.attention.utils import create_vllm_config
10 from vllm.v1.attention.backend import CommonAttentionMetadata
11 +from vllm.v1.attention.backends.mla.compressor_utils import (
12 + CompressedSlotMappingKernel,
13 +)
14 from vllm.v1.attention.backends.mla.indexer import (
15 BuildPrefillChunkMetadataKernel,
16 DeepseekV32IndexerMetadataBuilder,
17 )
18 +from vllm.v1.attention.backends.mla.sparse_utils import (
19 + ConvertReqIndexToGlobalIndexKernel,
20 +)
21 from vllm.v1.kv_cache_interface import MLAAttentionSpec
22 from vllm.v1.worker.block_table import get_block_table_width
23
@@ -20,7 +26,7 @@ def test_indexer_warmup_normalizes_zero_compress_ratios():
26 config = SimpleNamespace(
27 scheduler_config=SimpleNamespace(max_num_batched_tokens=8),
28 model_config=SimpleNamespace(
23 - hf_config=SimpleNamespace(compress_ratios=[0, 0, 4, 128, 0])
29 + hf_config=SimpleNamespace(compress_ratios=[0, 0, 4, 128, 0], index_kpool=32)
30 ),
31 parallel_config=SimpleNamespace(
32 decode_context_parallel_size=1,
@@ -30,7 +36,42 @@ def test_indexer_warmup_normalizes_zero_compress_ratios():
36
37 keys = BuildPrefillChunkMetadataKernel().get_warmup_keys(config)
38
33 - assert {key.COMPRESS_RATIO for key in keys} == {1, 4, 128}
39 + assert {key.compress_ratio for key in keys} == {1, 4, 32, 128}
40 + assert {(key.query_slice_start, key.query_slice_stop) for key in keys} == {
41 + (query_slice_start, query_slice_stop)
42 + for query_slice_start in (1, 2, 16)
43 + for query_slice_stop in (1, 2, 16)
44 + }
45 +
46 +
47 +def test_compressed_slot_mapping_warmup_includes_index_kpool():
48 + config = SimpleNamespace(
49 + cache_config=SimpleNamespace(block_size=256),
50 + model_config=SimpleNamespace(hf_config=SimpleNamespace(index_kpool=32)),
51 + )
52 +
53 + keys = CompressedSlotMappingKernel().get_warmup_keys(config)
54 + assert {(key.compress_ratio, key.block_size) for key in keys} == {(32, 2)}
55 +
56 +
57 +def test_index_conversion_warmup_uses_physical_block_stride():
58 + config = SimpleNamespace(
59 + cache_config=SimpleNamespace(block_size=64),
60 + model_config=SimpleNamespace(
61 + max_model_len=1024,
62 + hf_config=SimpleNamespace(index_topk=2048),
63 + ),
64 + parallel_config=SimpleNamespace(
65 + decode_context_parallel_size=1,
66 + cp_kv_cache_interleave_size=1,
67 + ),
68 + )
69 +
70 + keys = ConvertReqIndexToGlobalIndexKernel().get_warmup_keys(
71 + config,
72 + block_stride_rows=4096,
73 + )
74 + assert {key.block_stride_rows for key in keys} == {4096}
75
76
77 @pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
tests/v1/worker/test_gpu_model_runner_v2_eplb.py
+2
@@ -7,6 +7,7 @@ from typing import Any
7
8 import torch
9
10 +from vllm.model_executor.warmup.jit_warmup import JitWarmupRegistry
11 from vllm.v1.outputs import EMPTY_MODEL_RUNNER_OUTPUT
12 from vllm.v1.worker.gpu import eplb_utils as eplb
13 from vllm.v1.worker.gpu import model_runner as mrv2
@@ -84,6 +85,7 @@ def _make_runner(**overrides: Any) -> Any:
85 post_forward=lambda *_, **__: None,
86 )
87 runner.eplb = eplb.EPLBController(runner.parallel_config, runner.device)
88 + runner.jit_warmup_registry = JitWarmupRegistry(runner.vllm_config)
89 runner.pooling_runner = None
90 runner.execute_model_state = None
91 for key, value in overrides.items():
vllm/model_executor/layers/attention/mla_attention.py
+47
@@ -604,6 +604,33 @@ class MLAAttention(nn.Module, AttentionLayerBase):
604
605 self.use_sparse = use_sparse
606
607 + if vllm_config.kernel_config.enable_jit_warmup:
608 + backend_name = self.attn_backend.get_name()
609 + if backend_name in (
610 + "FLASHMLA_SPARSE",
611 + "FLASHINFER_MLA_SPARSE",
612 + "FLASHINFER_MLA_SPARSE_SM120",
613 + "DEEPSEEK_V32_INDEXER",
614 + ):
615 + from vllm.v1.attention.backends.mla.compressor_utils import (
616 + _COMPRESSED_SLOT_MAPPING_KERNEL,
617 + )
618 + from vllm.v1.attention.backends.mla.indexer import (
619 + _BUILD_PREFILL_CHUNK_METADATA_KERNEL,
620 + _PREPARE_UNIFORM_DECODE_KERNEL,
621 + )
622 +
623 + _COMPRESSED_SLOT_MAPPING_KERNEL.register_warmup()
624 + _PREPARE_UNIFORM_DECODE_KERNEL.register_warmup()
625 + _BUILD_PREFILL_CHUNK_METADATA_KERNEL.register_warmup()
626 +
627 + if backend_name != "DEEPSEEK_V32_INDEXER":
628 + from vllm.v1.attention.backends.mla.sparse_swa import (
629 + _COMPUTE_PREFILL_METADATA_KERNEL,
630 + )
631 +
632 + _COMPUTE_PREFILL_METADATA_KERNEL.register_warmup()
633 +
634 self.dcp_manager: MLADCPManager | None = None
635 if self.impl.dcp_world_size > 1:
636 query_dtype = (
@@ -652,6 +679,26 @@ class MLAAttention(nn.Module, AttentionLayerBase):
679 def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
680 # [B, H=1, N, C] -> [B, N, C]
681 self.kv_cache = kv_cache.squeeze(1)
682 + if (
683 + self._vllm_config.kernel_config.enable_jit_warmup
684 + and self.attn_backend.get_name()
685 + in (
686 + "FLASHMLA_SPARSE",
687 + "FLASHINFER_MLA_SPARSE",
688 + "FLASHINFER_MLA_SPARSE_SM120",
689 + "DEEPSEEK_V32_INDEXER",
690 + )
691 + ):
692 + from vllm.v1.attention.backends.mla.sparse_utils import (
693 + _CONVERT_REQ_INDEX_TO_GLOBAL_INDEX_KERNEL,
694 + )
695 +
696 + row_width = self.kv_cache.shape[-1]
697 + assert self.kv_cache.stride(0) % row_width == 0
698 + _CONVERT_REQ_INDEX_TO_GLOBAL_INDEX_KERNEL.register_warmup(
699 + self._vllm_config,
700 + block_stride_rows=self.kv_cache.stride(0) // row_width,
701 + )
702
703 @property
704 def chunked_prefill_workspace_size(self) -> int:
vllm/model_executor/warmup/jit_warmup.py
+5 -1
@@ -737,7 +737,11 @@ class JitWarmupRegistry:
737 for kernel, registrations in self._registrations.items():
738 compile_keys: dict[Any, None] = {}
739 for args, kwargs in registrations:
740 - if not args and not kwargs:
740 + if (
741 + not args
742 + and not kwargs
743 + and inspect.signature(kernel.get_warmup_keys).parameters
744 + ):
745 args = (self.vllm_config,)
746 for compile_key in kernel.get_warmup_keys(*args, **kwargs):
747 compile_keys[compile_key] = None
vllm/model_executor/warmup/jit_warmup_triton_helper.py
+90 -10
@@ -2,15 +2,21 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import ast
4 import inspect
5 -from collections.abc import Callable
5 +from abc import abstractmethod
6 +from collections.abc import Callable, Mapping
7 from dataclasses import dataclass
7 -from typing import Any
8 +from functools import cached_property, wraps
9 +from typing import Any, ClassVar, Generic, TypeVar, cast
10
11 from vllm.model_executor.warmup.jit_warmup import (
12 + VllmJitKernel,
13 get_ast_full_name,
14 get_function_source_node,
15 )
16
17 +CompileKeyT = TypeVar("CompileKeyT")
18 +LaunchSpec = tuple[tuple[int, ...], dict[str, Any]]
19 +
20
21 def triton_scalar_specialization_rep(value: int) -> int:
22 """Return an integer with the same default Triton JIT specialization.
@@ -50,10 +56,16 @@ def triton_scalar_specialization_rep(value: int) -> int:
56
57 @dataclass(frozen=True)
58 class TritonWarmupTensor:
53 - # Compile-only tensor descriptor for Triton pointer specialization.
59 + """Compile-only tensor metadata used by Triton warmup.
60 +
61 + ``strides=None`` represents compact row-major storage. Pass explicit strides
62 + whenever the runtime tensor can be padded, transposed, or otherwise strided.
63 + """
64 +
65 dtype: Any
66 aligned: bool = True
67 shape: tuple[int, ...] = (1,)
68 + strides: tuple[int, ...] | None = None
69
70 def data_ptr(self) -> int:
71 return 0 if self.aligned else 1
@@ -61,13 +73,81 @@ class TritonWarmupTensor:
73 def ptr_range(self) -> int:
74 return 0
75
64 - def stride(self) -> tuple[int, ...]:
65 - strides: list[int] = []
66 - stride = 1
67 - for size in reversed(self.shape):
68 - strides.append(stride)
69 - stride *= size
70 - return tuple(reversed(strides))
76 + def stride(self, dim: int | None = None) -> int | tuple[int, ...]:
77 + if self.strides is None:
78 + strides: list[int] = []
79 + stride = 1
80 + for size in reversed(self.shape):
81 + strides.append(stride)
82 + stride *= size
83 + result = tuple(reversed(strides))
84 + else:
85 + result = self.strides
86 + return result if dim is None else result[dim]
87 +
88 +
89 +class VllmTritonJitKernel(VllmJitKernel[CompileKeyT], Generic[CompileKeyT]):
90 + """Triton owner whose runtime launch specification is reused for warmup."""
91 +
92 + kernel: ClassVar[Any]
93 + _warming = False
94 +
95 + @abstractmethod
96 + def warmup_inputs(self, compile_key: CompileKeyT) -> dict[str, Any]:
97 + """Return runtime-shaped inputs that reproduce one compile key."""
98 + raise NotImplementedError
99 +
100 + def compile(self, compile_key: CompileKeyT) -> None:
101 + inputs = self.warmup_inputs(compile_key)
102 + self._warming = True
103 + try:
104 + cast(Callable[..., None], self)(**inputs)
105 + finally:
106 + self._warming = False
107 +
108 + @cached_property
109 + def _kernel_param_names(self) -> frozenset[str]:
110 + return frozenset(self.kernel.arg_names)
111 +
112 + def launch(
113 + self,
114 + grid: tuple[int, ...],
115 + inputs: Mapping[str, Any],
116 + /,
117 + **kwargs: Any,
118 + ) -> Any:
119 + for name, value in inputs.items():
120 + target = name if name in self._kernel_param_names else f"{name}_ptr"
121 + if target in self._kernel_param_names and target not in kwargs:
122 + kwargs[target] = value
123 + if self._warming:
124 + warmup = getattr(self.kernel, "warmup", None)
125 + assert warmup is not None
126 + return warmup(grid=(1,), **kwargs)
127 + return self.kernel[grid](**kwargs)
128 +
129 +
130 +def kernel_launcher(
131 + call_fn: Callable[..., LaunchSpec],
132 +) -> Callable[..., None]:
133 + """Launch a Triton kernel from a declarative ``__call__`` specification."""
134 + signature = inspect.signature(call_fn)
135 +
136 + @wraps(call_fn)
137 + def wrapper(
138 + self: VllmTritonJitKernel[Any],
139 + *args: Any,
140 + **kwargs: Any,
141 + ) -> None:
142 + grid, launch_kwargs = call_fn(self, *args, **kwargs)
143 + bound = signature.bind(self, *args, **kwargs)
144 + bound.apply_defaults()
145 + inputs = {
146 + name: value for name, value in bound.arguments.items() if name != "self"
147 + }
148 + self.launch(grid, inputs, **launch_kwargs)
149 +
150 + return wrapper
151
152
153 @dataclass(frozen=True)
vllm/model_executor/warmup/kernel_warmup.py
-4
@@ -35,9 +35,6 @@ from vllm.model_executor.warmup.kimi_k3_triton_warmup import (
35 kimi_k3_triton_warmup,
36 )
37 from vllm.model_executor.warmup.qwen_triton_warmup import qwen_triton_warmup
38 -from vllm.model_executor.warmup.sparse_mla_triton_warmup import (
39 - sparse_mla_triton_warmup,
40 -)
38 from vllm.platforms import current_platform
39 from vllm.utils.deep_gemm import is_deep_gemm_supported
40 from vllm.utils.flashinfer import has_flashinfer
@@ -154,7 +151,6 @@ def kernel_warmup(worker: "Worker", *, process_local_only: bool = False):
151 if worker.vllm_config.kernel_config.enable_jit_warmup:
152 kimi_k3_triton_warmup(worker)
153 fa4_cutedsl_warmup(worker)
157 - sparse_mla_triton_warmup(worker)
154
155 if current_platform.has_device_capability(90):
156 _warmup_ll_bf16_router_gemm(worker.get_model())
vllm/model_executor/warmup/sparse_mla_triton_warmup.py deleted
-111
@@ -1,111 +0,0 @@
1 -# SPDX-License-Identifier: Apache-2.0
2 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -"""Warm up sparse-MLA Triton metadata kernels."""
4 -
5 -from typing import TYPE_CHECKING
6 -
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 -
14 -logger = init_logger(__name__)
15 -
16 -_DEEPSEEK_V4_SPARSE_MLA_BACKENDS = frozenset(
17 - {
18 - "FLASHMLA_SPARSE_DSV4",
19 - "FLASHINFER_MLA_SPARSE_DSV4",
20 - "ROCM_FLASHMLA_SPARSE_DSV4",
21 - "DEEPSEEK_SPARSE_SWA",
22 - }
23 -)
24 -_GENERIC_SPARSE_MLA_BACKENDS = frozenset(
25 - {
26 - "FLASHMLA_SPARSE",
27 - "FLASHINFER_MLA_SPARSE",
28 - "FLASHINFER_MLA_SPARSE_SM120",
29 - }
30 -)
31 -_INDEXER_PREFILL_CHUNK_METADATA_BACKENDS = frozenset({"DEEPSEEK_V32_INDEXER"})
32 -
33 -_INDEXER_PREFILL_CHUNK_METADATA_BACKENDS = frozenset({"DEEPSEEK_V32_INDEXER"})
34 -
35 -
36 -def _attention_backend_name(backend: object) -> str | None:
37 - get_name = getattr(backend, "get_name", None)
38 - if get_name is None:
39 - return None
40 - try:
41 - return get_name()
42 - except NotImplementedError:
43 - return None
44 -
45 -
46 -def _has_attention_backend(
47 - runner: "GPUModelRunner",
48 - backend_names: frozenset[str],
49 -) -> bool:
50 - for groups in getattr(runner, "attn_groups", []) or ():
51 - for group in groups:
52 - name = _attention_backend_name(getattr(group, "backend", None))
53 - if name in backend_names:
54 - return True
55 - return False
56 -
57 -
58 -def _compile_sparse_swa_prefill_metadata_kernel(
59 - vllm_config: "VllmConfig",
60 -) -> None:
61 - from vllm.v1.attention.backends.mla.sparse_swa import (
62 - _COMPUTE_PREFILL_METADATA_KERNEL,
63 - )
64 -
65 - _COMPUTE_PREFILL_METADATA_KERNEL.warmup(vllm_config)
66 -
67 -
68 -def _compile_prefill_chunk_metadata_kernel(
69 - vllm_config: "VllmConfig",
70 -) -> None:
71 - from vllm.v1.attention.backends.mla.indexer import (
72 - _BUILD_PREFILL_CHUNK_METADATA_KERNEL,
73 - )
74 -
75 - _BUILD_PREFILL_CHUNK_METADATA_KERNEL.warmup(vllm_config)
76 -
77 -
78 -def _compile_combine_topk_swa_indices_kernel(
79 - vllm_config: "VllmConfig",
80 -) -> None:
81 - from vllm.models.deepseek_v4.common.ops.cache_utils import (
82 - _COMBINE_TOPK_SWA_INDICES_KERNEL,
83 - )
84 -
85 - _COMBINE_TOPK_SWA_INDICES_KERNEL.warmup(vllm_config)
86 -
87 -
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
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):
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):
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):
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/attention.py
+51
@@ -353,6 +353,57 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
353 k_cache_prefix=self.prefix,
354 )
355
356 + if vllm_config.kernel_config.enable_jit_warmup:
357 + from vllm.v1.attention.backends.mla.sparse_swa import (
358 + _COMPUTE_PREFILL_METADATA_KERNEL,
359 + _COMPUTE_SWA_INDICES_AND_LENS_KERNEL,
360 + )
361 +
362 + _COMPUTE_PREFILL_METADATA_KERNEL.register_warmup()
363 + _COMPUTE_SWA_INDICES_AND_LENS_KERNEL.register_warmup(
364 + window_size=self.window_size,
365 + block_size=self.swa_cache_layer.block_size,
366 + )
367 +
368 + if self.compress_ratio > 1:
369 + from vllm.v1.attention.backends.mla.compressor_utils import (
370 + _COMPRESSED_SLOT_MAPPING_KERNEL,
371 + )
372 +
373 + _COMPRESSED_SLOT_MAPPING_KERNEL.register_warmup()
374 +
375 + if self.indexer is not None:
376 + from vllm.v1.attention.backends.mla.indexer import (
377 + _BUILD_PREFILL_CHUNK_METADATA_KERNEL,
378 + _PREPARE_UNIFORM_DECODE_KERNEL,
379 + )
380 +
381 + _PREPARE_UNIFORM_DECODE_KERNEL.register_warmup()
382 + _BUILD_PREFILL_CHUNK_METADATA_KERNEL.register_warmup()
383 +
384 + spec_config = vllm_config.speculative_config
385 + if spec_config is not None and spec_config.use_dspark():
386 + from vllm.v1.attention.backends.mla.sparse_swa import (
387 + _COMPUTE_DSPARK_NONCAUSAL_SWA_INDICES_KERNEL,
388 + )
389 +
390 + _COMPUTE_DSPARK_NONCAUSAL_SWA_INDICES_KERNEL.register_warmup(
391 + window_size=self.window_size,
392 + num_speculative_tokens=spec_config.num_speculative_tokens,
393 + block_size=self.swa_cache_layer.block_size,
394 + )
395 +
396 + if self.backend_cls.get_name() in (
397 + "FLASHMLA_SPARSE_DSV4",
398 + "ROCM_FLASHMLA_SPARSE_DSV4",
399 + "XPU_V4_MLA_SPARSE",
400 + ):
401 + from vllm.models.deepseek_v4.common.ops.cache_utils import (
402 + _COMBINE_TOPK_SWA_INDICES_KERNEL,
403 + )
404 +
405 + _COMBINE_TOPK_SWA_INDICES_KERNEL.register_warmup()
406 +
407 def forward(
408 self,
409 positions: torch.Tensor,
vllm/v1/attention/backends/mla/compressor_utils.py
+125 -40
@@ -1,7 +1,18 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from dataclasses import dataclass
4 +from typing import Any
5 +
6 import torch
7
8 +from vllm.model_executor.warmup.jit_warmup import zip_inputs
9 +from vllm.model_executor.warmup.jit_warmup_triton_helper import (
10 + LaunchSpec,
11 + TritonWarmupTensor,
12 + VllmTritonJitKernel,
13 + kernel_launcher,
14 + triton_scalar_specialization_rep,
15 +)
16 from vllm.triton_utils import tl, triton
17 from vllm.utils.math_utils import cdiv
18
@@ -17,49 +28,124 @@ def get_dspark_swa_index_width(
28 return cdiv(width, _DSPARK_SWA_INDEX_ALIGNMENT) * _DSPARK_SWA_INDEX_ALIGNMENT
29
30
20 -@triton.jit
21 -def _compressed_slot_mapping_kernel(
22 - # [num_tokens]
23 - slot_mapping_ptr,
24 - # [num_reqs + 1]
25 - query_start_loc_ptr,
26 - # [num_reqs]
27 - seq_lens_ptr,
28 - # [num_reqs, max_num_blocks]
29 - block_table_ptr,
30 - block_table_stride,
31 - block_size,
32 - COMPRESS_RATIO: tl.constexpr,
33 - PAD_ID: tl.constexpr,
34 - TRITON_BLOCK_SIZE: tl.constexpr,
31 +class CompressedSlotMappingKernel(
32 + VllmTritonJitKernel["CompressedSlotMappingKernel.CompileKey"]
33 ):
36 - batch_idx = tl.program_id(0)
34 + TRITON_BLOCK_SIZE = 1024
35 +
36 + @dataclass(frozen=True)
37 + class CompileKey:
38 + compress_ratio: int
39 + triton_block_size: int
40 + block_size: int
41 +
42 + @staticmethod
43 + @triton.jit(do_not_specialize=["block_table_stride"])
44 + def kernel(
45 + # [num_tokens]
46 + slot_mapping_ptr,
47 + # [num_reqs + 1]
48 + query_start_loc_ptr,
49 + # [num_reqs]
50 + seq_lens_ptr,
51 + # [num_reqs, max_num_blocks]
52 + block_table_ptr,
53 + block_table_stride,
54 + block_size,
55 + COMPRESS_RATIO: tl.constexpr,
56 + PAD_ID: tl.constexpr,
57 + TRITON_BLOCK_SIZE: tl.constexpr,
58 + ):
59 + batch_idx = tl.program_id(0)
60
38 - query_start = tl.load(query_start_loc_ptr + batch_idx)
39 - query_end = tl.load(query_start_loc_ptr + batch_idx + 1)
40 - query_len = query_end - query_start
61 + query_start = tl.load(query_start_loc_ptr + batch_idx)
62 + query_end = tl.load(query_start_loc_ptr + batch_idx + 1)
63 + query_len = query_end - query_start
64
42 - seq_len = tl.load(seq_lens_ptr + batch_idx)
43 - start_pos = seq_len - query_len
65 + seq_len = tl.load(seq_lens_ptr + batch_idx)
66 + start_pos = seq_len - query_len
67
45 - for i in range(0, query_len, TRITON_BLOCK_SIZE):
46 - offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
47 - mask = offset < query_len
68 + for i in range(0, query_len, TRITON_BLOCK_SIZE):
69 + offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
70 + mask = offset < query_len
71
49 - pos = start_pos + i + tl.arange(0, TRITON_BLOCK_SIZE)
50 - is_valid = (pos + 1) % COMPRESS_RATIO == 0
51 - pos_after_compress = pos // COMPRESS_RATIO
72 + pos = start_pos + i + tl.arange(0, TRITON_BLOCK_SIZE)
73 + is_valid = (pos + 1) % COMPRESS_RATIO == 0
74 + pos_after_compress = pos // COMPRESS_RATIO
75
53 - block_ids = pos_after_compress // block_size
54 - block_numbers = tl.load(
55 - block_table_ptr + batch_idx * block_table_stride + block_ids,
56 - mask=mask & is_valid,
76 + block_ids = pos_after_compress // block_size
77 + block_numbers = tl.load(
78 + block_table_ptr + batch_idx * block_table_stride + block_ids,
79 + mask=mask & is_valid,
80 + )
81 + slot_ids = block_numbers * block_size + pos_after_compress % block_size
82 +
83 + # NOTE
84 + slot_ids = tl.where(is_valid, slot_ids, PAD_ID)
85 + tl.store(slot_mapping_ptr + query_start + offset, slot_ids, mask=mask)
86 +
87 + def dispatch( # type: ignore[override]
88 + self,
89 + *,
90 + compress_ratio: int,
91 + block_size: int,
92 + ) -> CompileKey:
93 + return self.CompileKey(
94 + compress_ratio=compress_ratio,
95 + triton_block_size=self.TRITON_BLOCK_SIZE,
96 + block_size=triton_scalar_specialization_rep(block_size),
97 )
58 - slot_ids = block_numbers * block_size + pos_after_compress % block_size
98
60 - # NOTE
61 - slot_ids = tl.where(is_valid, slot_ids, PAD_ID)
62 - tl.store(slot_mapping_ptr + query_start + offset, slot_ids, mask=mask)
99 + def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]:
100 + hf_config = vllm_config.model_config.hf_config
101 + configured_ratios = (
102 + *(getattr(hf_config, "compress_ratios", None) or ()),
103 + getattr(hf_config, "index_kpool", 1) or 1,
104 + )
105 + compress_ratios = tuple(
106 + dict.fromkeys(int(ratio) for ratio in configured_ratios if int(ratio) > 1)
107 + )
108 + if not compress_ratios:
109 + return []
110 + return self._trace_dispatch(self.dispatch)(
111 + zip_inputs(
112 + *(
113 + dict(
114 + compress_ratio=ratio,
115 + block_size=vllm_config.cache_config.block_size // ratio,
116 + )
117 + for ratio in compress_ratios
118 + )
119 + )
120 + )
121 +
122 + def warmup_inputs(self, compile_key: CompileKey) -> dict[str, Any]:
123 + int32_ptr = TritonWarmupTensor(torch.int32)
124 + return dict(
125 + slot_mapping=TritonWarmupTensor(torch.int64),
126 + query_start_loc=int32_ptr,
127 + seq_lens=int32_ptr,
128 + block_table=int32_ptr,
129 + block_size=compile_key.block_size,
130 + compress_ratio=compile_key.compress_ratio,
131 + )
132 +
133 + @kernel_launcher
134 + def __call__(
135 + self,
136 + slot_mapping: torch.Tensor,
137 + query_start_loc: torch.Tensor,
138 + seq_lens: torch.Tensor,
139 + block_table: torch.Tensor,
140 + block_size: int,
141 + compress_ratio: int,
142 + ) -> LaunchSpec:
143 + return (block_table.shape[0],), dict(
144 + block_table_stride=block_table.stride(0),
145 + COMPRESS_RATIO=compress_ratio,
146 + PAD_ID=-1,
147 + TRITON_BLOCK_SIZE=self.TRITON_BLOCK_SIZE,
148 + )
149
150
151 def get_compressed_slot_mapping(
@@ -83,16 +169,15 @@ def get_compressed_slot_mapping(
169 (num_tokens,), -1, dtype=torch.int64, device=query_start_loc.device
170 )
171
86 - num_reqs = block_table.shape[0]
87 - _compressed_slot_mapping_kernel[(num_reqs,)](
172 + _COMPRESSED_SLOT_MAPPING_KERNEL(
173 slot_mapping,
174 query_start_loc,
175 seq_lens,
176 block_table,
92 - block_table.stride(0),
177 block_size,
178 compress_ratio,
95 - PAD_ID=-1,
96 - TRITON_BLOCK_SIZE=1024,
179 )
180 return slot_mapping
181 +
182 +
183 +_COMPRESSED_SLOT_MAPPING_KERNEL = CompressedSlotMappingKernel()
vllm/v1/attention/backends/mla/indexer.py
+163 -101
@@ -1,6 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from dataclasses import dataclass
4 +from typing import Any
5
6 import torch
7
@@ -8,13 +9,13 @@ import vllm.envs as envs
9 from vllm.config import VllmConfig
10 from vllm.distributed import get_dcp_group, get_pcp_group
11 from vllm.logger import init_logger
11 -from vllm.model_executor.warmup.jit_warmup import (
12 - VllmJitKernel,
13 - WarmupIntRange,
14 -)
12 from vllm.model_executor.warmup.jit_warmup_triton_helper import (
13 + LaunchSpec,
14 TritonPointerInputVariant,
15 TritonWarmupTensor,
16 + VllmTritonJitKernel,
17 + kernel_launcher,
18 + triton_scalar_specialization_rep,
19 )
20 from vllm.platforms import current_platform
21 from vllm.triton_utils import tl, triton
@@ -63,41 +64,112 @@ def dsa_indexer_uses_fp4(vllm_config: VllmConfig) -> bool:
64 return use_fp4
65
66
66 -@triton.jit
67 -def _prepare_uniform_decode_kernel(
68 - seq_lens_ptr,
69 - decode_seq_lens_ptr,
70 - block_table_ptr,
71 - block_table_stride,
72 - expanded_block_table_ptr,
73 - expanded_bt_stride,
74 - decode_lens_ptr,
75 - max_decode_len,
76 - BLOCK_SIZE: tl.constexpr,
67 +class PrepareUniformDecodeKernel(
68 + VllmTritonJitKernel["PrepareUniformDecodeKernel.CompileKey"]
69 ):
78 - idx = tl.program_id(0)
79 - req_id = idx // max_decode_len
80 - local_idx = idx % max_decode_len
81 -
82 - # Compute number of KVs attended to by this token. Padding requests have
83 - # seq_len == 0, which would otherwise make the first token of each padded
84 - # request negative (e.g. next_n=2 gives 0-2+0+1 = -1). Downstream kernels
85 - # read these as uint32, turning -1 into ~4e9.
86 - seq_len = tl.load(seq_lens_ptr + req_id)
87 - per_token_seq_len = tl.maximum(seq_len - max_decode_len + local_idx + 1, 0)
88 - tl.store(decode_seq_lens_ptr + idx, per_token_seq_len)
89 -
90 - # Copy block table row.
91 - src = block_table_ptr + req_id * block_table_stride
92 - dst = expanded_block_table_ptr + idx * expanded_bt_stride
93 - for i in tl.range(0, expanded_bt_stride, BLOCK_SIZE):
94 - off = i + tl.arange(0, BLOCK_SIZE)
95 - mask = off < expanded_bt_stride
96 - src_block = tl.load(src + off, mask=mask)
97 - tl.store(dst + off, src_block, mask=mask)
98 -
99 - # All reqs now have decode_len = 1.
100 - tl.store(decode_lens_ptr + idx, 1)
70 + BLOCK_SIZE = 1024
71 +
72 + @dataclass(frozen=True)
73 + class CompileKey:
74 + block_size: int
75 + block_table_stride: int
76 + expanded_bt_stride: int
77 + max_decode_len: int
78 +
79 + @staticmethod
80 + @triton.jit
81 + def kernel(
82 + seq_lens_ptr,
83 + decode_seq_lens_ptr,
84 + block_table_ptr,
85 + block_table_stride,
86 + expanded_block_table_ptr,
87 + expanded_bt_stride,
88 + decode_lens_ptr,
89 + max_decode_len,
90 + BLOCK_SIZE: tl.constexpr,
91 + ):
92 + idx = tl.program_id(0)
93 + req_id = idx // max_decode_len
94 + local_idx = idx % max_decode_len
95 +
96 + # Compute number of KVs attended to by this token. Padding requests have
97 + # seq_len == 0, which would otherwise make the first token of each padded
98 + # request negative (e.g. next_n=2 gives 0-2+0+1 = -1). Downstream kernels
99 + # read these as uint32, turning -1 into ~4e9.
100 + seq_len = tl.load(seq_lens_ptr + req_id)
101 + per_token_seq_len = tl.maximum(seq_len - max_decode_len + local_idx + 1, 0)
102 + tl.store(decode_seq_lens_ptr + idx, per_token_seq_len)
103 +
104 + # Copy block table row.
105 + src = block_table_ptr + req_id * block_table_stride
106 + dst = expanded_block_table_ptr + idx * expanded_bt_stride
107 + for i in tl.range(0, expanded_bt_stride, BLOCK_SIZE):
108 + off = i + tl.arange(0, BLOCK_SIZE)
109 + mask = off < expanded_bt_stride
110 + src_block = tl.load(src + off, mask=mask)
111 + tl.store(dst + off, src_block, mask=mask)
112 +
113 + # All reqs now have decode_len = 1.
114 + tl.store(decode_lens_ptr + idx, 1)
115 +
116 + def dispatch( # type: ignore[override]
117 + self,
118 + *,
119 + block_size: int,
120 + block_table_stride: int,
121 + expanded_bt_stride: int,
122 + max_decode_len: int,
123 + ) -> CompileKey:
124 + return self.CompileKey(
125 + block_size=block_size,
126 + block_table_stride=triton_scalar_specialization_rep(block_table_stride),
127 + expanded_bt_stride=triton_scalar_specialization_rep(expanded_bt_stride),
128 + max_decode_len=triton_scalar_specialization_rep(max_decode_len),
129 + )
130 +
131 + def get_warmup_keys(self) -> list[CompileKey]:
132 + return self._trace_dispatch(self.dispatch)(
133 + block_size=self.BLOCK_SIZE,
134 + block_table_stride=(1, 2, 16),
135 + expanded_bt_stride=(1, 2, 16),
136 + max_decode_len=(1, 2, 16),
137 + )
138 +
139 + def warmup_inputs(self, compile_key: CompileKey) -> dict[str, Any]:
140 + int32_ptr = TritonWarmupTensor(torch.int32)
141 + return dict(
142 + seq_lens=int32_ptr,
143 + decode_seq_lens=int32_ptr,
144 + block_table=TritonWarmupTensor(
145 + torch.int32,
146 + shape=(1, compile_key.block_table_stride),
147 + ),
148 + expanded_block_table=TritonWarmupTensor(
149 + torch.int32,
150 + shape=(1, compile_key.expanded_bt_stride),
151 + ),
152 + decode_lens=int32_ptr,
153 + num_decode_tokens=1,
154 + max_decode_len=compile_key.max_decode_len,
155 + )
156 +
157 + @kernel_launcher
158 + def __call__(
159 + self,
160 + seq_lens: torch.Tensor,
161 + decode_seq_lens: torch.Tensor,
162 + block_table: torch.Tensor,
163 + expanded_block_table: torch.Tensor,
164 + decode_lens: torch.Tensor,
165 + num_decode_tokens: int,
166 + max_decode_len: int,
167 + ) -> LaunchSpec:
168 + return (num_decode_tokens,), dict(
169 + block_table_stride=block_table.stride(0),
170 + expanded_bt_stride=expanded_block_table.stride(0),
171 + BLOCK_SIZE=self.BLOCK_SIZE,
172 + )
173
174
175 def split_indexer_prefill_chunks(
@@ -215,14 +287,8 @@ class DeepseekV32IndexerPrefillChunkMetadata:
287 max_local_total_seq_lens: int = 0
288
289
218 -_BUILD_PREFILL_CHUNK_METADATA_INPUT_VARIANTS = (
219 - TritonPointerInputVariant.from_alignment(uncompressed_seq_lens=True),
220 - TritonPointerInputVariant.from_alignment(uncompressed_seq_lens=False),
221 -)
222 -
223 -
290 class BuildPrefillChunkMetadataKernel(
225 - VllmJitKernel["BuildPrefillChunkMetadataKernel.CompileKey"]
291 + VllmTritonJitKernel["BuildPrefillChunkMetadataKernel.CompileKey"]
292 ):
293 BLOCK_SIZE = 1024
294
@@ -230,11 +296,11 @@ class BuildPrefillChunkMetadataKernel(
296 class CompileKey:
297 query_slice_start: int
298 query_slice_stop: int
233 - DCP_RANK: int
234 - DCP_WORLD: int
235 - DCP_INTERLEAVE: int
236 - BLOCK_SIZE: int
237 - COMPRESS_RATIO: int
299 + dcp_rank: int
300 + dcp_world: int
301 + dcp_interleave: int
302 + block_size: int
303 + compress_ratio: int
304 input_variant: TritonPointerInputVariant
305
306 @staticmethod
@@ -327,13 +393,13 @@ class BuildPrefillChunkMetadataKernel(
393 input_variant: TritonPointerInputVariant,
394 ) -> CompileKey:
395 return self.CompileKey(
330 - query_slice_start=query_slice_start,
331 - query_slice_stop=query_slice_stop,
332 - DCP_RANK=DCP_RANK,
333 - DCP_WORLD=DCP_WORLD,
334 - DCP_INTERLEAVE=DCP_INTERLEAVE,
335 - BLOCK_SIZE=BLOCK_SIZE,
336 - COMPRESS_RATIO=COMPRESS_RATIO,
396 + query_slice_start=triton_scalar_specialization_rep(query_slice_start),
397 + query_slice_stop=triton_scalar_specialization_rep(query_slice_stop),
398 + dcp_rank=triton_scalar_specialization_rep(DCP_RANK),
399 + dcp_world=triton_scalar_specialization_rep(DCP_WORLD),
400 + dcp_interleave=triton_scalar_specialization_rep(DCP_INTERLEAVE),
401 + block_size=BLOCK_SIZE,
402 + compress_ratio=COMPRESS_RATIO,
403 input_variant=input_variant,
404 )
405
@@ -345,42 +411,51 @@ class BuildPrefillChunkMetadataKernel(
411 dcp_interleave = parallel_config.cp_kv_cache_interleave_size
412 dcp_rank = get_dcp_group().rank_in_group if dcp_world > 1 else 0
413 compress_ratios = tuple(
348 - max(1, int(ratio))
349 - for ratio in (getattr(hf_config, "compress_ratios", None) or (1,))
414 + dict.fromkeys(
415 + max(1, int(ratio))
416 + for ratio in (
417 + *(getattr(hf_config, "compress_ratios", None) or (1,)),
418 + getattr(hf_config, "index_kpool", 1) or 1,
419 + )
420 + )
421 )
422 return self._trace_dispatch(self.dispatch)(
352 - query_slice_start=WarmupIntRange(0, 2),
423 + # Cover Triton's divisible, exact-one, and generic i32 classes.
424 + query_slice_start=(0, 1, 2),
425 query_slice_stop=(1, 2 * max_tokens - 1, 2 * max_tokens),
426 DCP_RANK=dcp_rank,
427 DCP_WORLD=dcp_world,
428 DCP_INTERLEAVE=dcp_interleave,
429 BLOCK_SIZE=self.BLOCK_SIZE,
430 COMPRESS_RATIO=list(compress_ratios),
359 - input_variant=_BUILD_PREFILL_CHUNK_METADATA_INPUT_VARIANTS,
431 + input_variant=(
432 + TritonPointerInputVariant.from_alignment(uncompressed_seq_lens=True),
433 + TritonPointerInputVariant.from_alignment(uncompressed_seq_lens=False),
434 + ),
435 )
436
362 - def compile(self, compile_key: CompileKey) -> None:
363 - warmup = getattr(self.kernel, "warmup", None)
364 - assert warmup is not None
437 + def warmup_inputs(self, compile_key: CompileKey) -> dict[str, Any]:
438 int32_ptr = TritonWarmupTensor(torch.int32)
366 - warmup(
367 - int32_ptr,
368 - compile_key.input_variant.pointer("uncompressed_seq_lens", torch.int32),
369 - int32_ptr,
370 - int32_ptr,
371 - int32_ptr,
372 - int32_ptr,
373 - int32_ptr,
374 - compile_key.query_slice_start,
375 - compile_key.query_slice_stop,
376 - compile_key.DCP_RANK,
377 - compile_key.DCP_WORLD,
378 - compile_key.DCP_INTERLEAVE,
379 - BLOCK_SIZE=compile_key.BLOCK_SIZE,
380 - COMPRESS_RATIO=compile_key.COMPRESS_RATIO,
381 - grid=(1,),
439 + return dict(
440 + query_start_loc=int32_ptr,
441 + uncompressed_seq_lens=compile_key.input_variant.pointer(
442 + "uncompressed_seq_lens", torch.int32
443 + ),
444 + cu_compressed_seq_lens=int32_ptr,
445 + row_start_cu_compressed_seq_lens=int32_ptr,
446 + token_to_seq=int32_ptr,
447 + cu_compressed_seq_len_ks=int32_ptr,
448 + cu_compressed_seq_len_ke=int32_ptr,
449 + query_slice_start=compile_key.query_slice_start,
450 + query_slice_stop=compile_key.query_slice_stop,
451 + DCP_RANK=compile_key.dcp_rank,
452 + DCP_WORLD=compile_key.dcp_world,
453 + DCP_INTERLEAVE=compile_key.dcp_interleave,
454 + num_reqs=1,
455 + COMPRESS_RATIO=compile_key.compress_ratio,
456 )
457
458 + @kernel_launcher
459 def __call__(
460 self,
461 query_start_loc: torch.Tensor,
@@ -398,28 +473,13 @@ class BuildPrefillChunkMetadataKernel(
473 *,
474 num_reqs: int,
475 COMPRESS_RATIO: int,
401 - ) -> None:
402 - self.kernel[(num_reqs,)](
403 - query_start_loc,
404 - uncompressed_seq_lens,
405 - cu_compressed_seq_lens,
406 - row_start_cu_compressed_seq_lens,
407 - token_to_seq,
408 - cu_compressed_seq_len_ks,
409 - cu_compressed_seq_len_ke,
410 - query_slice_start,
411 - query_slice_stop,
412 - DCP_RANK,
413 - DCP_WORLD,
414 - DCP_INTERLEAVE,
476 + ) -> LaunchSpec:
477 + return (num_reqs,), dict(
478 BLOCK_SIZE=self.BLOCK_SIZE,
479 COMPRESS_RATIO=COMPRESS_RATIO,
480 )
481
482
420 -_BUILD_PREFILL_CHUNK_METADATA_KERNEL = BuildPrefillChunkMetadataKernel()
421 -
422 -
483 @dataclass
484 class DeepseekV32IndexerPrefillMetadata:
485 chunks: list[DeepseekV32IndexerPrefillChunkMetadata]
@@ -694,16 +754,14 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
754 and num_decodes * max_decode_len == num_decode_tokens
755 ):
756 # Uniform decode lengths with no cudagraph token padding.
697 - _prepare_uniform_decode_kernel[(num_decode_tokens,)](
757 + _PREPARE_UNIFORM_DECODE_KERNEL(
758 seq_lens,
759 self.decode_seq_lens_buffer,
760 block_table,
701 - block_table.stride(0),
761 self.expanded_block_table_buffer,
703 - self.expanded_block_table_buffer.stride(0),
762 self.decode_lens_buffer,
763 + num_decode_tokens,
764 max_decode_len,
706 - BLOCK_SIZE=1024,
765 )
766 self.decode_seq_lens_buffer[num_decode_tokens:] = 0
767 seq_lens = self.decode_seq_lens_buffer[:num_decode_tokens]
@@ -1180,3 +1238,7 @@ def build_prefill_chunk_metadata(
1238 local_total_seq_lens=local_total_seq_lens,
1239 max_local_total_seq_lens=max_local_total_seq_lens,
1240 )
1241 +
1242 +
1243 +_PREPARE_UNIFORM_DECODE_KERNEL = PrepareUniformDecodeKernel()
1244 +_BUILD_PREFILL_CHUNK_METADATA_KERNEL = BuildPrefillChunkMetadataKernel()
vllm/v1/attention/backends/mla/sparse_swa.py
+304 -155
@@ -1,17 +1,21 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from dataclasses import dataclass, field
4 -from typing import ClassVar, cast
4 +from typing import Any, ClassVar, cast
5
6 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,
11 WarmupIntRange,
12 )
14 -from vllm.model_executor.warmup.jit_warmup_triton_helper import TritonWarmupTensor
13 +from vllm.model_executor.warmup.jit_warmup_triton_helper import (
14 + LaunchSpec,
15 + TritonWarmupTensor,
16 + VllmTritonJitKernel,
17 + kernel_launcher,
18 +)
19 from vllm.platforms import current_platform
20 from vllm.triton_utils import tl, triton
21 from vllm.utils.math_utils import cdiv, next_power_of_2
@@ -275,11 +279,11 @@ class DeepseekSparseSWAMetadata:
279
280
281 class ComputePrefillMetadataKernel(
278 - VllmJitKernel["ComputePrefillMetadataKernel.CompileKey"]
282 + VllmTritonJitKernel["ComputePrefillMetadataKernel.CompileKey"]
283 ):
284 @dataclass(frozen=True)
285 class CompileKey:
282 - BLOCK_SIZE: int
286 + block_size: int
287
288 @staticmethod
289 @triton.jit(do_not_specialize=["num_prefills", "num_decodes", "window_size"])
@@ -320,7 +324,7 @@ class ComputePrefillMetadataKernel(
324 num_prefills: int,
325 ) -> CompileKey:
326 return self.CompileKey(
323 - BLOCK_SIZE=next_power_of_2(num_prefills),
327 + block_size=next_power_of_2(num_prefills),
328 )
329
330 def get_warmup_keys(self, vllm_config: VllmConfig) -> list[CompileKey]:
@@ -336,21 +340,18 @@ class ComputePrefillMetadataKernel(
340 num_prefills=WarmupIntRange(1, max_prefills + 1),
341 )
342
339 - def compile(self, compile_key: CompileKey) -> None:
340 - warmup = getattr(self.kernel, "warmup", None)
341 - assert warmup is not None
343 + def warmup_inputs(self, compile_key: CompileKey) -> dict[str, Any]:
344 int32_ptr = TritonWarmupTensor(torch.int32)
343 - warmup(
344 - int32_ptr,
345 - int32_ptr,
346 - int32_ptr,
347 - compile_key.BLOCK_SIZE,
348 - 0,
349 - 1,
350 - BLOCK_SIZE=compile_key.BLOCK_SIZE,
351 - grid=(1,),
345 + return dict(
346 + prefill_gather_lens=int32_ptr,
347 + seq_lens=int32_ptr,
348 + query_start_loc=int32_ptr,
349 + num_prefills=compile_key.block_size,
350 + num_decodes=0,
351 + window_size=1,
352 )
353
354 + @kernel_launcher
355 def __call__(
356 self,
357 prefill_gather_lens: torch.Tensor,
@@ -359,22 +360,13 @@ class ComputePrefillMetadataKernel(
360 num_prefills: int,
361 num_decodes: int,
362 window_size: int,
362 - ) -> None:
363 + ) -> LaunchSpec:
364 compile_key = self.dispatch(num_prefills=num_prefills)
364 - self.kernel[(1,)](
365 - prefill_gather_lens,
366 - seq_lens,
367 - query_start_loc,
368 - num_prefills,
369 - num_decodes,
370 - window_size,
371 - BLOCK_SIZE=compile_key.BLOCK_SIZE,
365 + return (1,), dict(
366 + BLOCK_SIZE=compile_key.block_size,
367 )
368
369
375 -_COMPUTE_PREFILL_METADATA_KERNEL = ComputePrefillMetadataKernel()
376 -
377 -
370 class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
371 """Builds metadata for DeepseekV4 SWA cache.
372
@@ -545,9 +537,8 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
537 device=self.device,
538 )
539 decode_swa_indices = self.decode_swa_indices_noncausal
548 - _compute_dspark_noncausal_swa_indices_kernel[(num_decode_tokens,)](
540 + _COMPUTE_DSPARK_NONCAUSAL_SWA_INDICES_KERNEL(
541 decode_swa_indices,
550 - decode_swa_indices.stride(0),
542 self.decode_swa_lens,
543 self.window_size,
544 self.noncausal_index_width,
@@ -556,15 +547,13 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
547 token_to_req_indices,
548 is_valid_token,
549 block_table,
559 - block_table.stride(0),
550 self.block_size,
551 + num_tokens=num_decode_tokens,
552 token_offset=0,
562 - TRITON_BLOCK_SIZE=1024,
553 )
554 else:
565 - _compute_swa_indices_and_lens_kernel[(num_decode_tokens,)](
555 + _COMPUTE_SWA_INDICES_AND_LENS_KERNEL(
556 decode_swa_indices,
567 - decode_swa_indices.stride(0),
557 self.decode_swa_lens,
558 self.window_size,
559 query_start_loc,
@@ -572,10 +561,9 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
561 token_to_req_indices,
562 is_valid_token,
563 block_table,
575 - block_table.stride(0),
564 self.block_size,
565 + num_tokens=num_decode_tokens,
566 token_offset=0,
578 - TRITON_BLOCK_SIZE=1024,
567 )
568
569 # Prefill SWA indices live in paged coordinates. `token_offset` lets
@@ -584,9 +572,8 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
572 if num_prefill_tokens > 0:
573 prefill_swa_indices = self.prefill_swa_indices[:num_prefill_tokens]
574 prefill_swa_lens = self.prefill_swa_lens[:num_prefill_tokens]
587 - _compute_swa_indices_and_lens_kernel[(num_prefill_tokens,)](
575 + _COMPUTE_SWA_INDICES_AND_LENS_KERNEL(
576 prefill_swa_indices,
589 - prefill_swa_indices.stride(0),
577 prefill_swa_lens,
578 self.window_size,
579 query_start_loc,
@@ -594,10 +581,9 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
581 token_to_req_indices,
582 is_valid_token,
583 block_table,
597 - block_table.stride(0),
584 self.block_size,
585 + num_tokens=num_prefill_tokens,
586 token_offset=num_decode_tokens,
600 - TRITON_BLOCK_SIZE=1024,
587 )
588
589 # Pre-compute DeepseekV4 prefill metadata shared across all attention layers.
@@ -668,9 +654,8 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
654 assert metadata.decode_swa_indices is not None
655 assert metadata.decode_swa_lens is not None
656
671 - _compute_swa_indices_and_lens_kernel[(metadata.num_decode_tokens,)](
657 + _COMPUTE_SWA_INDICES_AND_LENS_KERNEL(
658 metadata.decode_swa_indices,
673 - metadata.decode_swa_indices.stride(0),
659 metadata.decode_swa_lens,
660 metadata.decode_swa_indices.shape[-1],
661 metadata.query_start_loc,
@@ -678,10 +663,9 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
663 metadata.token_to_req_indices,
664 metadata.is_valid_token,
665 metadata.block_table,
681 - metadata.block_table.stride(0),
666 self.block_size,
667 + num_tokens=metadata.num_decode_tokens,
668 token_offset=0,
684 - TRITON_BLOCK_SIZE=1024,
669 )
670 tile_sched = self.build_tile_scheduler(metadata.num_decode_tokens)
671 metadata.tile_sched_swaonly = tile_sched[_LAYER_TYPE_SWAONLY]
@@ -771,142 +755,307 @@ class DeepseekSparseSWAMetadataBuilder(AttentionMetadataBuilder):
755 return result
756
757
774 -@triton.jit(do_not_specialize=["token_offset"])
775 -def _compute_swa_indices_and_lens_kernel(
776 - swa_indices_ptr,
777 - swa_indices_stride,
778 - swa_lens_ptr,
779 - window_size,
780 - query_start_loc_ptr,
781 - seq_lens_ptr,
782 - token_to_req_indices_ptr,
783 - is_valid_token_ptr,
784 - block_table_ptr,
785 - block_table_stride,
786 - block_size,
787 - token_offset,
788 - TRITON_BLOCK_SIZE: tl.constexpr,
758 +# TODO(ben): unify this kernel to reduce duplication
759 +
760 +
761 +class ComputeSWAIndicesAndLensKernel(
762 + VllmTritonJitKernel["ComputeSWAIndicesAndLensKernel.CompileKey"]
763 ):
790 - pid = tl.program_id(0)
791 - token_idx = pid + token_offset
792 - is_valid = tl.load(is_valid_token_ptr + token_idx)
793 - if not is_valid:
794 - tl.store(swa_lens_ptr + pid, 0)
795 - # Clear the row so a padded token cannot gather through stale indices.
764 + @dataclass(frozen=True)
765 + class CompileKey:
766 + window_size: int
767 + block_size: int
768 + triton_block_size: int
769 +
770 + @staticmethod
771 + @triton.jit(
772 + do_not_specialize=[
773 + "swa_indices_stride",
774 + "block_table_stride",
775 + "token_offset",
776 + ]
777 + )
778 + def kernel(
779 + swa_indices_ptr,
780 + swa_indices_stride,
781 + swa_lens_ptr,
782 + window_size,
783 + query_start_loc_ptr,
784 + seq_lens_ptr,
785 + token_to_req_indices_ptr,
786 + is_valid_token_ptr,
787 + block_table_ptr,
788 + block_table_stride,
789 + block_size,
790 + token_offset,
791 + TRITON_BLOCK_SIZE: tl.constexpr,
792 + ):
793 + pid = tl.program_id(0)
794 + token_idx = pid + token_offset
795 + is_valid = tl.load(is_valid_token_ptr + token_idx)
796 + if not is_valid:
797 + tl.store(swa_lens_ptr + pid, 0)
798 + # Clear the row so a padded token cannot gather through stale indices.
799 + for i in range(0, window_size, TRITON_BLOCK_SIZE):
800 + offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
801 + tl.store(
802 + swa_indices_ptr + pid * swa_indices_stride + offset,
803 + -1,
804 + mask=offset < window_size,
805 + )
806 + return
807 +
808 + req_idx = tl.load(token_to_req_indices_ptr + token_idx)
809 +
810 + query_start = tl.load(query_start_loc_ptr + req_idx)
811 + query_end = tl.load(query_start_loc_ptr + req_idx + 1)
812 + query_len = query_end - query_start
813 +
814 + seq_len = tl.load(seq_lens_ptr + req_idx)
815 + prefix_len = seq_len - query_len
816 +
817 + pos = prefix_len + token_idx - query_start
818 + start_pos = tl.maximum(pos - window_size + 1, 0)
819 + end_pos = pos + 1
820 +
821 + swa_len = end_pos - start_pos
822 + tl.store(swa_lens_ptr + pid, swa_len)
823 +
824 for i in range(0, window_size, TRITON_BLOCK_SIZE):
825 offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
826 +
827 + pos_offset = start_pos + offset
828 + block_indices = pos_offset // block_size
829 + block_numbers = tl.load(
830 + block_table_ptr + req_idx * block_table_stride + block_indices,
831 + mask=pos_offset < end_pos,
832 + )
833 + block_offsets = pos_offset % block_size
834 + slot_ids = block_numbers * block_size + block_offsets
835 +
836 + slot_ids = tl.where(offset < swa_len, slot_ids, -1)
837 tl.store(
838 swa_indices_ptr + pid * swa_indices_stride + offset,
800 - -1,
839 + slot_ids,
840 mask=offset < window_size,
841 )
803 - return
842
805 - req_idx = tl.load(token_to_req_indices_ptr + token_idx)
843 + def dispatch( # type: ignore[override]
844 + self,
845 + **compile_key_fields: int,
846 + ) -> CompileKey:
847 + return self.CompileKey(
848 + **compile_key_fields,
849 + triton_block_size=1024,
850 + )
851 +
852 + def get_warmup_keys(
853 + self,
854 + *,
855 + window_size: int,
856 + block_size: int,
857 + ) -> list[CompileKey]:
858 + return self._trace_dispatch(self.dispatch)(
859 + window_size=window_size,
860 + block_size=block_size,
861 + )
862 +
863 + def warmup_inputs(self, compile_key: CompileKey) -> dict[str, Any]:
864 + int32_ptr = TritonWarmupTensor(torch.int32)
865 + return dict(
866 + swa_indices=TritonWarmupTensor(torch.int32, shape=(1, 1), strides=(1, 1)),
867 + swa_lens=int32_ptr,
868 + window_size=compile_key.window_size,
869 + query_start_loc=int32_ptr,
870 + seq_lens=int32_ptr,
871 + token_to_req_indices=int32_ptr,
872 + is_valid_token=TritonWarmupTensor(torch.bool),
873 + block_table=TritonWarmupTensor(torch.int32, shape=(1, 1), strides=(1, 1)),
874 + block_size=compile_key.block_size,
875 + num_tokens=1,
876 + token_offset=0,
877 + )
878 +
879 + @kernel_launcher
880 + def __call__(
881 + self,
882 + swa_indices: torch.Tensor,
883 + swa_lens: torch.Tensor,
884 + window_size: int,
885 + query_start_loc: torch.Tensor,
886 + seq_lens: torch.Tensor,
887 + token_to_req_indices: torch.Tensor,
888 + is_valid_token: torch.Tensor,
889 + block_table: torch.Tensor,
890 + block_size: int,
891 + *,
892 + num_tokens: int,
893 + token_offset: int,
894 + ) -> LaunchSpec:
895 + return (num_tokens,), dict(
896 + swa_indices_stride=swa_indices.stride(0),
897 + block_table_stride=block_table.stride(0),
898 + TRITON_BLOCK_SIZE=1024,
899 + )
900
807 - query_start = tl.load(query_start_loc_ptr + req_idx)
808 - query_end = tl.load(query_start_loc_ptr + req_idx + 1)
809 - query_len = query_end - query_start
901
811 - seq_len = tl.load(seq_lens_ptr + req_idx)
812 - prefix_len = seq_len - query_len
902 +class ComputeDSparkNoncausalSWAIndicesKernel(
903 + VllmTritonJitKernel["ComputeDSparkNoncausalSWAIndicesKernel.CompileKey"]
904 +):
905 + @dataclass(frozen=True)
906 + class CompileKey:
907 + window_size: int
908 + index_width: int
909 + block_size: int
910 + triton_block_size: int
911
814 - pos = prefix_len + token_idx - query_start
815 - start_pos = tl.maximum(pos - window_size + 1, 0)
816 - end_pos = pos + 1
912 + @staticmethod
913 + @triton.jit(
914 + do_not_specialize=[
915 + "swa_indices_stride",
916 + "block_table_stride",
917 + "token_offset",
918 + ]
919 + )
920 + def kernel(
921 + swa_indices_ptr,
922 + swa_indices_stride,
923 + swa_lens_ptr,
924 + window_size,
925 + index_width,
926 + query_start_loc_ptr,
927 + seq_lens_ptr,
928 + token_to_req_indices_ptr,
929 + is_valid_token_ptr,
930 + block_table_ptr,
931 + block_table_stride,
932 + block_size,
933 + token_offset,
934 + TRITON_BLOCK_SIZE: tl.constexpr,
935 + ):
936 + """Non-causal per-token indices for the DSpark draft block.
937
818 - swa_len = end_pos - start_pos
819 - tl.store(swa_lens_ptr + pid, swa_len)
938 + Here, we populate the topk indices with the trailing window of context tokens,
939 + plus all query tokens (including future ones).
940 + """
941 + pid = tl.program_id(0)
942 + token_idx = pid + token_offset
943 + is_valid = tl.load(is_valid_token_ptr + token_idx)
944 + if not is_valid:
945 + tl.store(swa_lens_ptr + pid, 0)
946 + # Clear the row so a padded token cannot gather through stale indices.
947 + for i in range(0, index_width, TRITON_BLOCK_SIZE):
948 + offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
949 + tl.store(
950 + swa_indices_ptr + pid * swa_indices_stride + offset,
951 + -1,
952 + mask=offset < index_width,
953 + )
954 + return
955
821 - for i in range(0, window_size, TRITON_BLOCK_SIZE):
822 - offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
956 + req_idx = tl.load(token_to_req_indices_ptr + token_idx)
957
824 - pos_offset = start_pos + offset
825 - block_indices = pos_offset // block_size
826 - block_numbers = tl.load(
827 - block_table_ptr + req_idx * block_table_stride + block_indices,
828 - mask=pos_offset < end_pos,
829 - )
830 - block_offsets = pos_offset % block_size
831 - slot_ids = block_numbers * block_size + block_offsets
958 + query_start = tl.load(query_start_loc_ptr + req_idx)
959 + query_end = tl.load(query_start_loc_ptr + req_idx + 1)
960 + query_len = query_end - query_start
961
833 - slot_ids = tl.where(offset < swa_len, slot_ids, -1)
834 - tl.store(
835 - swa_indices_ptr + pid * swa_indices_stride + offset,
836 - slot_ids,
837 - mask=offset < window_size,
838 - )
962 + seq_len = tl.load(seq_lens_ptr + req_idx)
963 + prefix_len = seq_len - query_len
964
965 + # Block-anchored window (shared by every token in the block) + full block.
966 + start_pos = tl.maximum(prefix_len - window_size, 0)
967 + end_pos = seq_len
968
841 -# TODO(ben): unify this kernel to reduce duplication
842 -@triton.jit(do_not_specialize=["token_offset"])
843 -def _compute_dspark_noncausal_swa_indices_kernel(
844 - swa_indices_ptr,
845 - swa_indices_stride,
846 - swa_lens_ptr,
847 - window_size,
848 - index_width,
849 - query_start_loc_ptr,
850 - seq_lens_ptr,
851 - token_to_req_indices_ptr,
852 - is_valid_token_ptr,
853 - block_table_ptr,
854 - block_table_stride,
855 - block_size,
856 - token_offset,
857 - TRITON_BLOCK_SIZE: tl.constexpr,
858 -):
859 - """Non-causal per-token indices for the DSpark draft block.
969 + swa_len = end_pos - start_pos
970 + tl.store(swa_lens_ptr + pid, swa_len)
971
861 - Here, we populate the topk indices with the trailing window of context tokens,
862 - plus all query tokens (including future ones).
863 - """
864 - pid = tl.program_id(0)
865 - token_idx = pid + token_offset
866 - is_valid = tl.load(is_valid_token_ptr + token_idx)
867 - if not is_valid:
868 - tl.store(swa_lens_ptr + pid, 0)
869 - # Clear the row so a padded token cannot gather through stale indices.
972 for i in range(0, index_width, TRITON_BLOCK_SIZE):
973 offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
974 +
975 + pos_offset = start_pos + offset
976 + block_indices = pos_offset // block_size
977 + block_numbers = tl.load(
978 + block_table_ptr + req_idx * block_table_stride + block_indices,
979 + mask=pos_offset < end_pos,
980 + )
981 + block_offsets = pos_offset % block_size
982 + slot_ids = block_numbers * block_size + block_offsets
983 +
984 + slot_ids = tl.where(offset < swa_len, slot_ids, -1)
985 tl.store(
986 swa_indices_ptr + pid * swa_indices_stride + offset,
874 - -1,
987 + slot_ids,
988 mask=offset < index_width,
989 )
877 - return
878 -
879 - req_idx = tl.load(token_to_req_indices_ptr + token_idx)
990
881 - query_start = tl.load(query_start_loc_ptr + req_idx)
882 - query_end = tl.load(query_start_loc_ptr + req_idx + 1)
883 - query_len = query_end - query_start
884 -
885 - seq_len = tl.load(seq_lens_ptr + req_idx)
886 - prefix_len = seq_len - query_len
887 -
888 - # Block-anchored window (shared by every token in the block) + full block.
889 - start_pos = tl.maximum(prefix_len - window_size, 0)
890 - end_pos = seq_len
891 -
892 - swa_len = end_pos - start_pos
893 - tl.store(swa_lens_ptr + pid, swa_len)
991 + def dispatch( # type: ignore[override]
992 + self,
993 + *,
994 + window_size: int,
995 + num_speculative_tokens: int,
996 + block_size: int,
997 + ) -> CompileKey:
998 + return self.CompileKey(
999 + window_size=window_size,
1000 + index_width=get_dspark_swa_index_width(window_size, num_speculative_tokens),
1001 + block_size=block_size,
1002 + triton_block_size=1024,
1003 + )
1004
895 - for i in range(0, index_width, TRITON_BLOCK_SIZE):
896 - offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
1005 + def get_warmup_keys(
1006 + self,
1007 + *,
1008 + window_size: int,
1009 + num_speculative_tokens: int,
1010 + block_size: int,
1011 + ) -> list[CompileKey]:
1012 + return self._trace_dispatch(self.dispatch)(
1013 + window_size=window_size,
1014 + num_speculative_tokens=num_speculative_tokens,
1015 + block_size=block_size,
1016 + )
1017
898 - pos_offset = start_pos + offset
899 - block_indices = pos_offset // block_size
900 - block_numbers = tl.load(
901 - block_table_ptr + req_idx * block_table_stride + block_indices,
902 - mask=pos_offset < end_pos,
1018 + def warmup_inputs(self, compile_key: CompileKey) -> dict[str, Any]:
1019 + int32_ptr = TritonWarmupTensor(torch.int32)
1020 + return dict(
1021 + swa_indices=TritonWarmupTensor(torch.int32, shape=(1, 1), strides=(1, 1)),
1022 + swa_lens=int32_ptr,
1023 + window_size=compile_key.window_size,
1024 + index_width=compile_key.index_width,
1025 + query_start_loc=int32_ptr,
1026 + seq_lens=int32_ptr,
1027 + token_to_req_indices=int32_ptr,
1028 + is_valid_token=TritonWarmupTensor(torch.bool),
1029 + block_table=TritonWarmupTensor(torch.int32, shape=(1, 1), strides=(1, 1)),
1030 + block_size=compile_key.block_size,
1031 + num_tokens=1,
1032 + token_offset=0,
1033 )
904 - block_offsets = pos_offset % block_size
905 - slot_ids = block_numbers * block_size + block_offsets
1034
907 - slot_ids = tl.where(offset < swa_len, slot_ids, -1)
908 - tl.store(
909 - swa_indices_ptr + pid * swa_indices_stride + offset,
910 - slot_ids,
911 - mask=offset < index_width,
1035 + @kernel_launcher
1036 + def __call__(
1037 + self,
1038 + swa_indices: torch.Tensor,
1039 + swa_lens: torch.Tensor,
1040 + window_size: int,
1041 + index_width: int,
1042 + query_start_loc: torch.Tensor,
1043 + seq_lens: torch.Tensor,
1044 + token_to_req_indices: torch.Tensor,
1045 + is_valid_token: torch.Tensor,
1046 + block_table: torch.Tensor,
1047 + block_size: int,
1048 + *,
1049 + num_tokens: int,
1050 + token_offset: int,
1051 + ) -> LaunchSpec:
1052 + return (num_tokens,), dict(
1053 + swa_indices_stride=swa_indices.stride(0),
1054 + block_table_stride=block_table.stride(0),
1055 + TRITON_BLOCK_SIZE=1024,
1056 )
1057 +
1058 +
1059 +_COMPUTE_PREFILL_METADATA_KERNEL = ComputePrefillMetadataKernel()
1060 +_COMPUTE_SWA_INDICES_AND_LENS_KERNEL = ComputeSWAIndicesAndLensKernel()
1061 +_COMPUTE_DSPARK_NONCAUSAL_SWA_INDICES_KERNEL = ComputeDSparkNoncausalSWAIndicesKernel()
vllm/v1/attention/backends/mla/sparse_utils.py
+370 -173
@@ -2,9 +2,26 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Utility functions for sparse MLA backends."""
4
5 +from dataclasses import dataclass
6 +from typing import Any
7 +
8 import torch
9
10 +from vllm.config import VllmConfig
11 +from vllm.distributed import get_dcp_group
12 +from vllm.model_executor.warmup.jit_warmup import (
13 + zip_inputs,
14 +)
15 +from vllm.model_executor.warmup.jit_warmup_triton_helper import (
16 + LaunchSpec,
17 + TritonWarmupTensor,
18 + VllmTritonJitKernel,
19 + kernel_launcher,
20 + triton_scalar_specialization_rep,
21 +)
22 from vllm.triton_utils import tl, triton
23 +from vllm.utils.math_utils import cdiv
24 +from vllm.v1.worker.block_table import get_block_table_width
25
26
27 def flat_kv_row_view(
@@ -30,126 +47,330 @@ def flat_kv_row_view(
47
48
49 # Kernel with prefill workspace support and valid count tracking
33 -@triton.jit
34 -def _convert_req_index_to_global_index_kernel(
35 - req_id_ptr, # int32 [num_tokens]
36 - block_table_ptr, # int32 [num_requests, max_num_blocks_per_req]
37 - token_indices_ptr, # int32 [num_tokens, NUM_TOPK_TOKENS]
38 - out_ptr, # int32 [num_tokens, NUM_TOPK_TOKENS]
39 - valid_count_ptr, # int32 [num_tokens] - output valid count per row
40 - prefill_request_id_ptr, # int32 [num_tokens], -1 for decode, >=0 for prefill
41 - workspace_starts_ptr, # int32 [num_prefill_reqs+1] or nullptr
42 - # shapes (compile-time where possible)
43 - max_num_blocks_per_req: tl.constexpr,
44 - BLOCK_SIZE: tl.constexpr,
45 - BLOCK_STRIDE_ROWS: tl.constexpr,
46 - BLOCK_N: tl.constexpr, # tile width along columns
47 - HAS_PREFILL: tl.constexpr,
48 - COUNT_VALID: tl.constexpr, # whether to count valid indices
49 - # BLOCK_N == NUM_TOPK_TOKENS: one program owns the row, so the valid count
50 - # is an in-register reduction and needs no atomic.
51 - SINGLE_TILE: tl.constexpr,
52 - # When set, scatter valid slots to a contiguous prefix [0, valid_count) using
53 - # valid_count_ptr as an atomic slot allocator (DCP filtering leaves interior
54 - # -1 gaps; the trtllm-gen sparse kernel reads the first valid_count entries).
55 - # Requires COUNT_VALID and an out buffer pre-filled with -1. Order within the
56 - # prefix is unspecified (only the selected set matters).
57 - COMPACT_TO_FRONT: tl.constexpr,
58 - # DCP de-interleave: with DCP_SIZE == 1 these are an exact no-op
59 - DCP_SIZE: tl.constexpr,
60 - DCP_RANK: tl.constexpr,
61 - DCP_INTERLEAVE: tl.constexpr,
62 - # strides (in elements)
63 - bt_stride0,
64 - bt_stride1,
65 - ti_stride0,
66 - ti_stride1,
67 - out_stride0,
68 - out_stride1,
50 +
51 +
52 +class ConvertReqIndexToGlobalIndexKernel(
53 + VllmTritonJitKernel["ConvertReqIndexToGlobalIndexKernel.CompileKey"]
54 ):
70 - # program_id(0) -> token_id (row)
71 - # program_id(1) -> tile index along columns
72 - token_id = tl.program_id(0)
73 - tile_id = tl.program_id(1)
74 -
75 - # Each program covers BLOCK_N consecutive columns
76 - indice_id = tile_id * BLOCK_N + tl.arange(0, BLOCK_N)
77 -
78 - # Load request id for this token (no mask: grid is exact)
79 - req = tl.load(req_id_ptr + token_id)
80 -
81 - # Load token indices for this tile
82 - ti_ptr = token_indices_ptr + token_id * ti_stride0 + indice_id * ti_stride1
83 - tok = tl.load(ti_ptr) # int32
84 -
85 - # Only token == -1 should propagate as -1
86 - is_invalid_tok = tok < 0
87 - is_prefill = False
88 - if HAS_PREFILL:
89 - prefill_req_id = tl.load(prefill_request_id_ptr + token_id)
90 - is_prefill = prefill_req_id >= 0
91 -
92 - # DCP de-interleave the global token id into this rank's local slot.
93 - # Tokens are interleaved in groups of DCP_INTERLEAVE across ranks. With
94 - # DCP_SIZE == 1 (and any interleave) owning_rank == 0 == DCP_RANK (never
95 - # remote) and local_idx == tok, so this reduces to the non-DCP path; with
96 - # DCP_INTERLEAVE == 1 it reduces to plain round-robin (tok % / // DCP_SIZE).
97 - owning_rank = (tok // DCP_INTERLEAVE) % DCP_SIZE
98 - is_remote = owning_rank != DCP_RANK
99 - local_idx = (
100 - tok // (DCP_SIZE * DCP_INTERLEAVE)
101 - ) * DCP_INTERLEAVE + tok % DCP_INTERLEAVE
102 -
103 - # Compute block id and in-block offset
104 - block_id = local_idx // BLOCK_SIZE
105 - inblock_off = local_idx % BLOCK_SIZE
106 -
107 - # Guard block_table access
108 - valid_block = (block_id < max_num_blocks_per_req) & (block_id >= 0)
109 - bt_ptr = block_table_ptr + req * bt_stride0 + block_id * bt_stride1
110 - is_invalid_tok |= ~valid_block | is_remote
111 - base = tl.load(bt_ptr, mask=valid_block & ~is_prefill & ~is_remote, other=0)
112 - out_val = base * BLOCK_STRIDE_ROWS + inblock_off
113 -
114 - # Override with prefill output if prefill is enabled
115 - if HAS_PREFILL:
116 - workspace_start = tl.load(
117 - workspace_starts_ptr + prefill_req_id, mask=is_prefill, other=0
118 - )
119 - prefill_out = workspace_start + tok
120 - out_val = tl.where(is_prefill, prefill_out, out_val)
121 - out_val = tl.where(is_invalid_tok, -1, out_val)
122 -
123 - if COMPACT_TO_FRONT:
124 - # Scatter valid slots to a contiguous prefix. A per-tile exclusive prefix
125 - # sum gives each valid lane a distinct local offset; one atomic add of the
126 - # tile's valid count reserves a contiguous base across racing tiles. The
127 - # out buffer is pre-filled with -1, so unwritten tail slots stay -1.
128 - # With no racing tiles the base is 0 and the allocator becomes a store.
129 - is_valid = (~is_invalid_tok).to(tl.int32)
130 - local_offset = tl.cumsum(is_valid) - is_valid
131 - tile_valid_count = tl.sum(is_valid)
132 - if SINGLE_TILE:
133 - base = 0
134 - tl.store(valid_count_ptr + token_id, tile_valid_count)
135 - else:
136 - base = tl.atomic_add(valid_count_ptr + token_id, tile_valid_count)
137 - dest = base + local_offset
138 - out_ptr_dest = out_ptr + token_id * out_stride0 + dest * out_stride1
139 - tl.store(out_ptr_dest, out_val, mask=is_valid == 1)
140 - else:
141 - # Store results in place (input column == output column).
142 - out_ptr_ij = out_ptr + token_id * out_stride0 + indice_id * out_stride1
143 - tl.store(out_ptr_ij, out_val)
144 -
145 - # Accumulate the tile's valid count into the row total; a single tile's
146 - # reduction *is* the total.
147 - if COUNT_VALID:
148 - tile_valid_count = tl.sum((~is_invalid_tok).to(tl.int32))
55 + @dataclass(frozen=True)
56 + class CompileKey:
57 + block_size: int
58 + block_stride_rows: int
59 + block_n: int
60 + has_prefill_workspace: bool
61 + count_valid: bool
62 + single_tile: bool
63 + compact_to_front: bool
64 + dcp_size: int
65 + dcp_rank: int
66 + dcp_interleave: int
67 + num_warps: int
68 + num_topk_tokens: int
69 + block_table_stride: int
70 +
71 + @staticmethod
72 + @triton.jit(do_not_specialize=["max_num_blocks_per_req"])
73 + def kernel(
74 + req_id_ptr, # int32 [num_tokens]
75 + block_table_ptr, # int32 [num_requests, max_num_blocks_per_req]
76 + token_indices_ptr, # int32 [num_tokens, NUM_TOPK_TOKENS]
77 + out_ptr, # int32 [num_tokens, NUM_TOPK_TOKENS]
78 + valid_count_ptr, # int32 [num_tokens] - output valid count per row
79 + prefill_request_id_ptr, # int32 [num_tokens], -1 for decode, >=0 for prefill
80 + workspace_starts_ptr, # int32 [num_prefill_reqs+1] or nullptr
81 + # shapes (compile-time where possible)
82 + max_num_blocks_per_req,
83 + BLOCK_SIZE: tl.constexpr,
84 + BLOCK_STRIDE_ROWS: tl.constexpr,
85 + BLOCK_N: tl.constexpr, # tile width along columns
86 + HAS_PREFILL: tl.constexpr,
87 + COUNT_VALID: tl.constexpr, # whether to count valid indices
88 + # BLOCK_N == NUM_TOPK_TOKENS: one program owns the row, so the valid count
89 + # is an in-register reduction and needs no atomic.
90 + SINGLE_TILE: tl.constexpr,
91 + # When set, scatter valid slots to a contiguous prefix [0, valid_count) using
92 + # valid_count_ptr as an atomic slot allocator (DCP filtering leaves interior
93 + # -1 gaps; the trtllm-gen sparse kernel reads the first valid_count entries).
94 + # Requires COUNT_VALID and an out buffer pre-filled with -1. Order within the
95 + # prefix is unspecified (only the selected set matters).
96 + COMPACT_TO_FRONT: tl.constexpr,
97 + # DCP de-interleave: with DCP_SIZE == 1 these are an exact no-op
98 + DCP_SIZE: tl.constexpr,
99 + DCP_RANK: tl.constexpr,
100 + DCP_INTERLEAVE: tl.constexpr,
101 + # strides (in elements)
102 + bt_stride0,
103 + bt_stride1,
104 + ti_stride0,
105 + ti_stride1,
106 + out_stride0,
107 + out_stride1,
108 + ):
109 + # program_id(0) -> token_id (row)
110 + # program_id(1) -> tile index along columns
111 + token_id = tl.program_id(0)
112 + tile_id = tl.program_id(1)
113 +
114 + # Each program covers BLOCK_N consecutive columns
115 + indice_id = tile_id * BLOCK_N + tl.arange(0, BLOCK_N)
116 +
117 + # Load request id for this token (no mask: grid is exact)
118 + req = tl.load(req_id_ptr + token_id)
119 +
120 + # Load token indices for this tile
121 + ti_ptr = token_indices_ptr + token_id * ti_stride0 + indice_id * ti_stride1
122 + tok = tl.load(ti_ptr) # int32
123 +
124 + # Only token == -1 should propagate as -1
125 + is_invalid_tok = tok < 0
126 + is_prefill = False
127 + if HAS_PREFILL:
128 + prefill_req_id = tl.load(prefill_request_id_ptr + token_id)
129 + is_prefill = prefill_req_id >= 0
130 +
131 + # DCP de-interleave the global token id into this rank's local slot.
132 + # Tokens are interleaved in groups of DCP_INTERLEAVE across ranks. With
133 + # DCP_SIZE == 1 (and any interleave) owning_rank == 0 == DCP_RANK (never
134 + # remote) and local_idx == tok, so this reduces to the non-DCP path; with
135 + # DCP_INTERLEAVE == 1 it reduces to plain round-robin (tok % / // DCP_SIZE).
136 + owning_rank = (tok // DCP_INTERLEAVE) % DCP_SIZE
137 + is_remote = owning_rank != DCP_RANK
138 + local_idx = (
139 + tok // (DCP_SIZE * DCP_INTERLEAVE)
140 + ) * DCP_INTERLEAVE + tok % DCP_INTERLEAVE
141 +
142 + # Compute block id and in-block offset
143 + block_id = local_idx // BLOCK_SIZE
144 + inblock_off = local_idx % BLOCK_SIZE
145 +
146 + # Guard block_table access
147 + valid_block = (block_id < max_num_blocks_per_req) & (block_id >= 0)
148 + bt_ptr = block_table_ptr + req * bt_stride0 + block_id * bt_stride1
149 + is_invalid_tok |= ~valid_block | is_remote
150 + base = tl.load(bt_ptr, mask=valid_block & ~is_prefill & ~is_remote, other=0)
151 + out_val = base * BLOCK_STRIDE_ROWS + inblock_off
152 +
153 + # Override with prefill output if prefill is enabled
154 + if HAS_PREFILL:
155 + workspace_start = tl.load(
156 + workspace_starts_ptr + prefill_req_id, mask=is_prefill, other=0
157 + )
158 + prefill_out = workspace_start + tok
159 + out_val = tl.where(is_prefill, prefill_out, out_val)
160 + out_val = tl.where(is_invalid_tok, -1, out_val)
161 +
162 + if COMPACT_TO_FRONT:
163 + # Scatter valid slots to a contiguous prefix. A per-tile exclusive prefix
164 + # sum gives each valid lane a distinct local offset; one atomic add of the
165 + # tile's valid count reserves a contiguous base across racing tiles. The
166 + # out buffer is pre-filled with -1, so unwritten tail slots stay -1.
167 + # With no racing tiles the base is 0 and the allocator becomes a store.
168 + is_valid = (~is_invalid_tok).to(tl.int32)
169 + local_offset = tl.cumsum(is_valid) - is_valid
170 + tile_valid_count = tl.sum(is_valid)
171 if SINGLE_TILE:
172 + base = 0
173 tl.store(valid_count_ptr + token_id, tile_valid_count)
174 else:
152 - tl.atomic_add(valid_count_ptr + token_id, tile_valid_count)
175 + base = tl.atomic_add(valid_count_ptr + token_id, tile_valid_count)
176 + dest = base + local_offset
177 + out_ptr_dest = out_ptr + token_id * out_stride0 + dest * out_stride1
178 + tl.store(out_ptr_dest, out_val, mask=is_valid == 1)
179 + else:
180 + # Store results in place (input column == output column).
181 + out_ptr_ij = out_ptr + token_id * out_stride0 + indice_id * out_stride1
182 + tl.store(out_ptr_ij, out_val)
183 +
184 + # Accumulate the tile's valid count into the row total; a single tile's
185 + # reduction *is* the total.
186 + if COUNT_VALID:
187 + tile_valid_count = tl.sum((~is_invalid_tok).to(tl.int32))
188 + if SINGLE_TILE:
189 + tl.store(valid_count_ptr + token_id, tile_valid_count)
190 + else:
191 + tl.atomic_add(valid_count_ptr + token_id, tile_valid_count)
192 +
193 + def dispatch( # type: ignore[override]
194 + self,
195 + *,
196 + BLOCK_SIZE: int,
197 + BLOCK_STRIDE_ROWS: int,
198 + BLOCK_N: int,
199 + NUM_TOPK_TOKENS: int,
200 + HAS_PREFILL_WORKSPACE: bool,
201 + COUNT_VALID: bool,
202 + COMPACT_TO_FRONT: bool,
203 + DCP_SIZE: int,
204 + DCP_RANK: int,
205 + DCP_INTERLEAVE: int,
206 + max_num_blocks_per_req: int,
207 + ) -> CompileKey:
208 + tiling = _remap_tiling(NUM_TOPK_TOKENS, BLOCK_N, COUNT_VALID)
209 + return self.CompileKey(
210 + block_size=BLOCK_SIZE,
211 + block_stride_rows=BLOCK_STRIDE_ROWS,
212 + block_n=tiling[1],
213 + has_prefill_workspace=HAS_PREFILL_WORKSPACE,
214 + count_valid=COUNT_VALID,
215 + single_tile=tiling[0],
216 + compact_to_front=COMPACT_TO_FRONT,
217 + dcp_size=DCP_SIZE,
218 + dcp_rank=DCP_RANK,
219 + dcp_interleave=DCP_INTERLEAVE,
220 + num_warps=tiling[3],
221 + num_topk_tokens=NUM_TOPK_TOKENS,
222 + block_table_stride=triton_scalar_specialization_rep(max_num_blocks_per_req),
223 + )
224 +
225 + def get_warmup_keys(
226 + self,
227 + vllm_config: VllmConfig,
228 + *,
229 + block_stride_rows: int,
230 + ) -> list[CompileKey]:
231 + block_size = vllm_config.cache_config.block_size
232 + dcp_size = vllm_config.parallel_config.decode_context_parallel_size
233 + dcp_interleave = vllm_config.parallel_config.cp_kv_cache_interleave_size
234 + dcp_rank = get_dcp_group().rank_in_group if dcp_size > 1 else 0
235 + num_topk_tokens = vllm_config.model_config.hf_config.index_topk
236 + max_num_blocks = cdiv(
237 + vllm_config.model_config.max_model_len,
238 + block_size * dcp_size,
239 + )
240 + max_num_blocks_per_req = get_block_table_width(max_num_blocks, block_size)
241 + return self._trace_dispatch(self.dispatch)(
242 + zip_inputs(
243 + dict(
244 + HAS_PREFILL_WORKSPACE=False,
245 + COUNT_VALID=False,
246 + COMPACT_TO_FRONT=False,
247 + DCP_SIZE=1,
248 + DCP_RANK=0,
249 + DCP_INTERLEAVE=1,
250 + ),
251 + dict(
252 + HAS_PREFILL_WORKSPACE=False,
253 + COUNT_VALID=True,
254 + COMPACT_TO_FRONT=False,
255 + DCP_SIZE=1,
256 + DCP_RANK=0,
257 + DCP_INTERLEAVE=1,
258 + ),
259 + dict(
260 + HAS_PREFILL_WORKSPACE=True,
261 + COUNT_VALID=True,
262 + COMPACT_TO_FRONT=False,
263 + DCP_SIZE=1,
264 + DCP_RANK=0,
265 + DCP_INTERLEAVE=1,
266 + ),
267 + dict(
268 + HAS_PREFILL_WORKSPACE=False,
269 + COUNT_VALID=True,
270 + COMPACT_TO_FRONT=False,
271 + DCP_SIZE=dcp_size,
272 + DCP_RANK=dcp_rank,
273 + DCP_INTERLEAVE=dcp_interleave,
274 + ),
275 + dict(
276 + HAS_PREFILL_WORKSPACE=False,
277 + COUNT_VALID=True,
278 + COMPACT_TO_FRONT=dcp_size > 1,
279 + DCP_SIZE=dcp_size,
280 + DCP_RANK=dcp_rank,
281 + DCP_INTERLEAVE=dcp_interleave,
282 + ),
283 + ),
284 + BLOCK_SIZE=block_size,
285 + BLOCK_STRIDE_ROWS=block_stride_rows,
286 + BLOCK_N=128,
287 + NUM_TOPK_TOKENS=num_topk_tokens,
288 + max_num_blocks_per_req=max_num_blocks_per_req,
289 + )
290 +
291 + def warmup_inputs(self, compile_key: CompileKey) -> dict[str, Any]:
292 + int32_ptr = TritonWarmupTensor(torch.int32)
293 + token_shape = (1, compile_key.num_topk_tokens)
294 + token_strides = (16, 1)
295 + return dict(
296 + req_id=int32_ptr,
297 + block_table=TritonWarmupTensor(
298 + torch.int32,
299 + shape=(1, compile_key.block_table_stride),
300 + ),
301 + token_indices=TritonWarmupTensor(
302 + torch.int32,
303 + shape=token_shape,
304 + strides=token_strides,
305 + ),
306 + out=TritonWarmupTensor(
307 + torch.int32,
308 + shape=token_shape,
309 + strides=token_strides,
310 + ),
311 + valid_counts=int32_ptr if compile_key.count_valid else None,
312 + prefill_workspace_request_ids=(
313 + int32_ptr if compile_key.has_prefill_workspace else None
314 + ),
315 + prefill_workspace_starts=(
316 + int32_ptr if compile_key.has_prefill_workspace else None
317 + ),
318 + max_num_blocks_per_req=1,
319 + BLOCK_SIZE=compile_key.block_size,
320 + BLOCK_STRIDE_ROWS=compile_key.block_stride_rows,
321 + BLOCK_N=compile_key.block_n,
322 + NUM_TOPK_TOKENS=compile_key.num_topk_tokens,
323 + HAS_PREFILL_WORKSPACE=compile_key.has_prefill_workspace,
324 + COUNT_VALID=compile_key.count_valid,
325 + COMPACT_TO_FRONT=compile_key.compact_to_front,
326 + DCP_SIZE=compile_key.dcp_size,
327 + DCP_RANK=compile_key.dcp_rank,
328 + DCP_INTERLEAVE=compile_key.dcp_interleave,
329 + )
330 +
331 + @kernel_launcher
332 + def __call__(
333 + self,
334 + req_id: torch.Tensor,
335 + block_table: torch.Tensor,
336 + token_indices: torch.Tensor,
337 + out: torch.Tensor,
338 + valid_counts: torch.Tensor | None,
339 + prefill_workspace_request_ids: torch.Tensor | None,
340 + prefill_workspace_starts: torch.Tensor | None,
341 + *,
342 + max_num_blocks_per_req: int,
343 + BLOCK_SIZE: int,
344 + BLOCK_STRIDE_ROWS: int,
345 + BLOCK_N: int,
346 + NUM_TOPK_TOKENS: int,
347 + HAS_PREFILL_WORKSPACE: bool,
348 + COUNT_VALID: bool,
349 + COMPACT_TO_FRONT: bool,
350 + DCP_SIZE: int,
351 + DCP_RANK: int,
352 + DCP_INTERLEAVE: int,
353 + ) -> LaunchSpec:
354 + single_tile, block_n, tiles_per_row, num_warps = _remap_tiling(
355 + NUM_TOPK_TOKENS, BLOCK_N, COUNT_VALID
356 + )
357 + # Exact 2D grid: tokens × column tiles
358 + return (req_id.shape[0], tiles_per_row), dict(
359 + valid_count_ptr=valid_counts,
360 + prefill_request_id_ptr=prefill_workspace_request_ids,
361 + workspace_starts_ptr=prefill_workspace_starts,
362 + BLOCK_N=block_n,
363 + HAS_PREFILL=HAS_PREFILL_WORKSPACE,
364 + SINGLE_TILE=single_tile,
365 + # Strides in elements
366 + bt_stride0=block_table.stride(0),
367 + bt_stride1=block_table.stride(1),
368 + ti_stride0=token_indices.stride(0),
369 + ti_stride1=token_indices.stride(1),
370 + out_stride0=out.stride(0),
371 + out_stride1=out.stride(1),
372 + num_warps=num_warps,
373 + )
374
375
376 def _remap_tiling(
@@ -231,9 +452,7 @@ def triton_convert_req_index_to_global_index(
452 num_tokens = req_id.shape[0]
453 max_num_blocks_per_req = block_table.shape[1]
454
234 - single_tile, block_n, tiles_per_row, num_warps = _remap_tiling(
235 - NUM_TOPK_TOKENS, BLOCK_N, return_valid_counts
236 - )
455 + single_tile, _, _, _ = _remap_tiling(NUM_TOPK_TOKENS, BLOCK_N, return_valid_counts)
456
457 # Ensure contiguous tensors on the same device
458 req_id_c = req_id.contiguous()
@@ -247,11 +466,6 @@ def triton_convert_req_index_to_global_index(
466 alloc = torch.empty if single_tile else torch.zeros
467 valid_counts = alloc(num_tokens, dtype=torch.int32, device=token_indices.device)
468
250 - # Strides in elements
251 - bt_stride0, bt_stride1 = block_table_c.stride()
252 - ti_stride0, ti_stride1 = token_indices_c.stride()
253 - out_stride0, out_stride1 = out.stride()
254 -
469 # Prepare prefill pointers
470 if HAS_PREFILL_WORKSPACE:
471 assert prefill_workspace_request_ids is not None # for mypy
@@ -259,10 +473,7 @@ def triton_convert_req_index_to_global_index(
473 assert prefill_workspace_request_ids.is_contiguous()
474 assert prefill_workspace_starts.is_contiguous()
475
262 - # Exact 2D grid: tokens × column tiles
263 - grid = (num_tokens, tiles_per_row)
264 -
265 - _convert_req_index_to_global_index_kernel[grid](
476 + _CONVERT_REQ_INDEX_TO_GLOBAL_INDEX_KERNEL(
477 req_id_c,
478 block_table_c,
479 token_indices_c,
@@ -271,26 +482,20 @@ def triton_convert_req_index_to_global_index(
482 prefill_workspace_request_ids,
483 prefill_workspace_starts,
484 # shapes / constexprs
274 - max_num_blocks_per_req,
275 - BLOCK_SIZE,
276 - BLOCK_STRIDE_ROWS if BLOCK_STRIDE_ROWS is not None else BLOCK_SIZE,
277 - block_n,
278 - HAS_PREFILL_WORKSPACE,
279 - return_valid_counts,
280 - single_tile,
281 - False, # COMPACT_TO_FRONT: keep input column == output column
485 + max_num_blocks_per_req=max_num_blocks_per_req,
486 + BLOCK_SIZE=BLOCK_SIZE,
487 + BLOCK_STRIDE_ROWS=(
488 + BLOCK_STRIDE_ROWS if BLOCK_STRIDE_ROWS is not None else BLOCK_SIZE
489 + ),
490 + BLOCK_N=BLOCK_N,
491 + NUM_TOPK_TOKENS=NUM_TOPK_TOKENS,
492 + HAS_PREFILL_WORKSPACE=HAS_PREFILL_WORKSPACE,
493 + COUNT_VALID=return_valid_counts,
494 + COMPACT_TO_FRONT=False,
495 # DCP disabled (no-op de-interleave)
283 - 1,
284 - 0,
285 - 1,
286 - # strides
287 - bt_stride0,
288 - bt_stride1,
289 - ti_stride0,
290 - ti_stride1,
291 - out_stride0,
292 - out_stride1,
293 - num_warps=num_warps,
496 + DCP_SIZE=1,
497 + DCP_RANK=0,
498 + DCP_INTERLEAVE=1,
499 )
500
501 if return_valid_counts:
@@ -362,9 +567,7 @@ def triton_filter_and_convert_dcp_index(
567 count_valid = return_valid_counts or compact_valid_to_front
568
569 # The compaction builds on the counting, so it shares the tiling.
365 - single_tile, block_n, tiles_per_row, num_warps = _remap_tiling(
366 - NUM_TOPK_TOKENS, BLOCK_N, count_valid
367 - )
570 + single_tile, _, _, _ = _remap_tiling(NUM_TOPK_TOKENS, BLOCK_N, count_valid)
571
572 if compact_valid_to_front:
573 out = torch.full_like(token_indices_c, -1)
@@ -377,11 +580,7 @@ def triton_filter_and_convert_dcp_index(
580 alloc = torch.empty if single_tile else torch.zeros
581 valid_counts = alloc(num_tokens, dtype=torch.int32, device=token_indices.device)
582
380 - bt_stride0, bt_stride1 = block_table_c.stride()
381 - ti_stride0, ti_stride1 = token_indices_c.stride()
382 - out_stride0, out_stride1 = out.stride()
383 -
384 - _convert_req_index_to_global_index_kernel[(num_tokens, tiles_per_row)](
583 + _CONVERT_REQ_INDEX_TO_GLOBAL_INDEX_KERNEL(
584 req_id_c,
585 block_table_c,
586 token_indices_c,
@@ -390,27 +589,25 @@ def triton_filter_and_convert_dcp_index(
589 # No prefill workspace on the DCP decode path.
590 None,
591 None,
393 - max_num_blocks_per_req,
394 - BLOCK_SIZE,
395 - BLOCK_STRIDE_ROWS if BLOCK_STRIDE_ROWS is not None else BLOCK_SIZE,
396 - block_n,
397 - False, # HAS_PREFILL
398 - count_valid,
399 - single_tile,
400 - compact_valid_to_front,
401 - dcp_size,
402 - dcp_rank,
403 - cp_kv_cache_interleave_size,
404 - bt_stride0,
405 - bt_stride1,
406 - ti_stride0,
407 - ti_stride1,
408 - out_stride0,
409 - out_stride1,
410 - num_warps=num_warps,
592 + max_num_blocks_per_req=max_num_blocks_per_req,
593 + BLOCK_SIZE=BLOCK_SIZE,
594 + BLOCK_STRIDE_ROWS=(
595 + BLOCK_STRIDE_ROWS if BLOCK_STRIDE_ROWS is not None else BLOCK_SIZE
596 + ),
597 + BLOCK_N=BLOCK_N,
598 + NUM_TOPK_TOKENS=NUM_TOPK_TOKENS,
599 + HAS_PREFILL_WORKSPACE=False,
600 + COUNT_VALID=count_valid,
601 + COMPACT_TO_FRONT=compact_valid_to_front,
602 + DCP_SIZE=dcp_size,
603 + DCP_RANK=dcp_rank,
604 + DCP_INTERLEAVE=cp_kv_cache_interleave_size,
605 )
606
607 if return_valid_counts:
608 assert valid_counts is not None
609 return out, valid_counts
610 return out
611 +
612 +
613 +_CONVERT_REQ_INDEX_TO_GLOBAL_INDEX_KERNEL = ConvertReqIndexToGlobalIndexKernel()
vllm/v1/worker/gpu/model_runner.py
+17 -12
@@ -382,9 +382,12 @@ class GPUModelRunner(LoRAModelRunnerMixin):
382 model_loader = get_model_loader(self.vllm_config.load_config)
383 logger.info_once("Loading model from scratch...")
384
385 - self.model = model_loader.load_model(
386 - vllm_config=self.vllm_config, model_config=self.vllm_config.model_config
387 - )
385 + # Capture warmup providers selected while constructing the model.
386 + with self.jit_warmup_registry.activate():
387 + self.model = model_loader.load_model(
388 + vllm_config=self.vllm_config,
389 + model_config=self.vllm_config.model_config,
390 + )
391 if self.lora_config:
392 self.model = self.load_lora_model(
393 self.model, self.vllm_config, self.device
@@ -668,15 +671,17 @@ class GPUModelRunner(LoRAModelRunnerMixin):
671 self.speculator.init_cudagraph_manager(cudagraph_mode)
672
673 self.kv_caches: list[torch.Tensor] = []
671 - kv_caches_dict = init_kv_cache(
672 - self.kv_caches,
673 - self.compilation_config.static_forward_context,
674 - self.kv_cache_config,
675 - self.device,
676 - self.kernel_block_sizes,
677 - self.vllm_config,
678 - kv_cache_allocation_context=kv_cache_allocation_context,
679 - )
674 + # Capture warmup providers that depend on allocated KV-cache strides.
675 + with self.jit_warmup_registry.activate():
676 + kv_caches_dict = init_kv_cache(
677 + self.kv_caches,
678 + self.compilation_config.static_forward_context,
679 + self.kv_cache_config,
680 + self.device,
681 + self.kernel_block_sizes,
682 + self.vllm_config,
683 + kv_cache_allocation_context=kv_cache_allocation_context,
684 + )
685 if is_profiling:
686 self.kv_connector = NO_OP_KV_CONNECTOR
687 else:
vllm/v1/worker/gpu_model_runner.py
+12 -8
@@ -5393,9 +5393,11 @@ class GPUModelRunner(
5393 if load_dummy_weights:
5394 self.load_config.load_format = "dummy"
5395 model_loader = get_model_loader(self.load_config)
5396 - self.model = model_loader.load_model(
5397 - vllm_config=self.vllm_config, model_config=self.model_config
5398 - )
5396 + # Capture warmup providers selected while constructing the model.
5397 + with self.jit_warmup_registry.activate():
5398 + self.model = model_loader.load_model(
5399 + vllm_config=self.vllm_config, model_config=self.model_config
5400 + )
5401 if self.lora_config:
5402 self.model = self.load_lora_model(
5403 self.model, self.vllm_config, self.device
@@ -7500,11 +7502,13 @@ class GPUModelRunner(
7502
7503 # Reinitialize need to after initialize_attn_backend
7504 self.may_reinitialize_input_batch(kv_cache_config, kernel_block_sizes)
7503 - kv_caches = self.initialize_kv_cache_tensors(
7504 - kv_cache_config,
7505 - kernel_block_sizes,
7506 - kv_cache_allocation_context=kv_cache_allocation_context,
7507 - )
7505 + # Capture warmup providers that depend on allocated KV-cache strides.
7506 + with self.jit_warmup_registry.activate():
7507 + kv_caches = self.initialize_kv_cache_tensors(
7508 + kv_cache_config,
7509 + kernel_block_sizes,
7510 + kv_cache_allocation_context=kv_cache_allocation_context,
7511 + )
7512
7513 if (
7514 self.speculative_config