Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .buildkite/test_areas/engine.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ steps:
- label: ":nvidia: (H200 MIG 35GB) E2E Core"
device: h200_35gb
key: e2e-core-1-gpu
timeout_in_minutes: 40
timeout_in_minutes: 50
source_file_dependencies:
- vllm/v1/
- tests/v1/e2e/general/
Expand Down
16 changes: 15 additions & 1 deletion tests/v1/e2e/general/test_kv_sharing_fast_prefill.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,23 @@ def test_prompts():


@use_fork_for_test
@pytest.mark.parametrize("kv_sharing_fast_prefill", [False, True])
@pytest.mark.parametrize(
"kv_sharing_fast_prefill,use_v2_model_runner",
[
(False, False),
(True, False),
(True, True),
# (False, True) omitted: fast-prefill-off behavior is runner-generic,
# and skipping it saves ~9 min of mostly torch.compile time in CI. The
# no-silent-fallback assertion below is exercised by the V2 cases.
],
)
@pytest.mark.parametrize("enforce_eager", [True, False])
def test_kv_sharing_fast_prefill(
monkeypatch: pytest.MonkeyPatch,
kv_sharing_fast_prefill: bool,
enforce_eager: bool,
use_v2_model_runner: bool,
):
if not enforce_eager and current_platform.is_rocm():
# Relevant context: https://github.com/vllm-project/vllm/pull/29244
Expand All @@ -84,6 +95,7 @@ def test_kv_sharing_fast_prefill(
m.setenv("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
else:
m.setenv("VLLM_ENABLE_V1_MULTIPROCESSING", "0")
m.setenv("VLLM_USE_V2_MODEL_RUNNER", "1" if use_v2_model_runner else "0")

prompts, answer, indices = prep_prompts(batch_size)

Expand All @@ -95,6 +107,8 @@ def test_kv_sharing_fast_prefill(
kv_sharing_fast_prefill=kv_sharing_fast_prefill,
attention_backend="TRITON_ATTN",
)
# Guard against a silent fallback to the other model runner.
assert llm.llm_engine.vllm_config.use_v2_model_runner == use_v2_model_runner
responses = llm.generate(prompts, sampling_params)
check_answers(
indices,
Expand Down
64 changes: 64 additions & 0 deletions tests/v1/worker/test_attn_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
never addressed by the logical view.
"""

from types import SimpleNamespace

import pytest
import torch

Expand All @@ -22,6 +24,7 @@
MLAAttentionSpec,
compute_layout_strides,
)
from vllm.v1.worker.gpu import attn_utils
from vllm.v1.worker.gpu.attn_utils import (
get_attn_cg_support,
get_query_lens_mismatch_unsupported_backend,
Expand Down Expand Up @@ -118,6 +121,67 @@ def test_attention_checks_preserve_global_and_target_scoped_support():
)


def test_get_kv_sharing_fast_prefill_eligible_layers(monkeypatch: pytest.MonkeyPatch):
"""Fast prefill applies to the contiguous suffix of KV-sharing layers.

Draft-model layers register after the target model's and may share KV, so
they must not extend (or break) the target's eligible suffix.
"""

def check(
layer_names: list[str],
shared: dict[str, str],
draft_layer_names: set[str] | None = None,
) -> set[str]:
monkeypatch.setattr(
attn_utils,
"get_layers_from_vllm_config",
lambda *a, **k: {name: None for name in layer_names},
)
monkeypatch.setattr(attn_utils, "get_shared_kv_cache_layers", lambda *a: shared)
vllm_config = SimpleNamespace(
cache_config=SimpleNamespace(kv_sharing_fast_prefill=True)
)
return attn_utils.get_kv_sharing_fast_prefill_eligible_layers(
vllm_config, draft_layer_names
)

# No KV sharing: nothing is eligible.
assert check(["t0", "t1"], {}) == set()

# Trailing run of sharing layers (YOCO-style second half).
assert check(["t0", "t1", "t2", "t3"], {"t2": "t1", "t3": "t1"}) == {"t2", "t3"}

# A non-sharing layer after a sharing one breaks the suffix.
assert check(["t0", "t1", "t2", "t3"], {"t1": "t0", "t3": "t0"}) == {"t3"}

# KV-sharing draft layers at the end are collected without an exclusion...
assert check(
["t0", "t1", "t2", "t3", "d0", "d1"],
{"t2": "t1", "t3": "t1", "d0": "t1", "d1": "t1"},
) == {"t2", "t3", "d0", "d1"}

# ...so the runner excludes them: skipped, not collected, and they do not
# break the target's trailing run.
assert check(
["t0", "t1", "t2", "t3", "d0", "d1"],
{"t2": "t1", "t3": "t1", "d0": "t1", "d1": "t1"},
draft_layer_names={"d0", "d1"},
) == {"t2", "t3"}

# Feature flag off: nothing is eligible even with sharing layers.
monkeypatch.setattr(
attn_utils, "get_layers_from_vllm_config", lambda *a, **k: {"t0": None}
)
monkeypatch.setattr(
attn_utils, "get_shared_kv_cache_layers", lambda *a: {"t0": "t0"}
)
vllm_config = SimpleNamespace(
cache_config=SimpleNamespace(kv_sharing_fast_prefill=False)
)
assert attn_utils.get_kv_sharing_fast_prefill_eligible_layers(vllm_config) == set()


def test_reshape_padded_kv_cache_strides_by_padded_page():
num_blocks = 3
spec = FullAttentionSpec(
Expand Down
2 changes: 1 addition & 1 deletion tests/v1/worker/test_gpu_model_runner_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def narrow(self, *args):
monkeypatch.setattr(
model_runner_module,
"init_attn_backend",
lambda *args: ([], attn_cg_support, [8, 262144]),
lambda *args, **kwargs: ([], attn_cg_support, [8, 262144]),
)
monkeypatch.setattr(
model_runner_module,
Expand Down
1 change: 0 additions & 1 deletion vllm/config/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ class CacheConfig:
some layers can skip tokens corresponding to prefill. This flag enables
attention metadata for eligible layers to be overridden with metadata
necessary for implementing this optimization in some models (e.g. Gemma3n)
NOTE: KV cache sharing is not supported for MRv2 (v2 model runner).
"""

kv_cache_memory_bytes: int | None = None
Expand Down
4 changes: 0 additions & 4 deletions vllm/config/vllm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2638,10 +2638,6 @@ def _get_v2_model_runner_unsupported_features(self) -> list[str]:
):
unsupported.append("custom logits processors")

if self.cache_config.kv_sharing_fast_prefill:
# Will be added by https://github.com/vllm-project/vllm/pull/35045
unsupported.append("KV sharing fast prefill")

if self.cache_config.mamba_cache_mode == "all":
unsupported.append("mamba cache mode 'all'")

Expand Down
9 changes: 7 additions & 2 deletions vllm/v1/attention/backends/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,8 +626,13 @@ def make_kv_sharing_fast_prefill_common_attn_metadata(
# Skip computing fast prefill path
return common_attn_metadata

assert common_attn_metadata.logits_indices_padded is not None
assert common_attn_metadata.num_logits_indices is not None
if (
common_attn_metadata.logits_indices_padded is None
or common_attn_metadata.num_logits_indices is None
):
# Fast prefill not armed for this step (e.g. cudagraph capture, or a
# pure-decode step): run the KV-sharing layers on the full batch.
return common_attn_metadata

logits_indices_padded = common_attn_metadata.logits_indices_padded
num_logits_indices = common_attn_metadata.num_logits_indices
Expand Down
116 changes: 115 additions & 1 deletion vllm/v1/worker/gpu/attn_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@
from collections.abc import Mapping, Sequence
from contextlib import AbstractContextManager, nullcontext
from dataclasses import dataclass
from typing import Any, cast
from typing import TYPE_CHECKING, Any, cast

import numpy as np
import torch

from vllm.config import VllmConfig, get_layers_from_vllm_config
from vllm.config.compilation import CUDAGraphMode
from vllm.model_executor.layers.attention import Attention
from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
from vllm.multimodal.inputs import MultiModalFeatureSpec
from vllm.v1.attention.backend import (
AttentionCGSupport,
CommonAttentionMetadata,
)
from vllm.v1.attention.backends.utils import create_fast_prefill_custom_backend
from vllm.v1.kv_cache_interface import (
AttentionSpec,
KVCacheConfig,
Expand All @@ -31,6 +34,12 @@
prepare_kernel_block_sizes,
)

if TYPE_CHECKING:
from vllm.v1.worker.gpu.cudagraph_utils import (
BatchExecutionDescriptor,
CudaGraphManager,
)


@dataclass(frozen=True)
class AttentionCGSupportInfo:
Expand All @@ -50,6 +59,66 @@ def narrow(
return self


@dataclass(frozen=True)
class FastPrefillBatchMetadata:
"""Per-step inputs for the KV-sharing fast prefill path."""

logits_indices_padded: torch.Tensor
num_logits_indices: int
max_logits_per_req: int


class FastPrefillHelper:
"""Decides per step whether to arm the KV-sharing fast prefill path, and
stages the logits indices it runs on.
"""

def __init__(self, cudagraph_manager: "CudaGraphManager", max_num_tokens: int):
self.max_num_tokens = max_num_tokens
self.cudagraph_manager = cudagraph_manager
self.logits_indices_buf = torch.zeros(
max_num_tokens, dtype=torch.int32, device=cudagraph_manager.device
)

def prepare(
self,
logits_indices: torch.Tensor,
num_reqs: int,
cu_num_logits_np: np.ndarray,
has_prefill: bool,
batch_desc: "BatchExecutionDescriptor",
) -> FastPrefillBatchMetadata | None:
if (
not has_prefill
or batch_desc.cg_mode == CUDAGraphMode.FULL
or batch_desc.num_ubatches > 1
):
return None
buf = self.logits_indices_buf
num_logits = logits_indices.shape[0]
assert num_logits > 0
buf[:num_logits].copy_(logits_indices)
# There might be leftover indices in buf[num_logits:] from previous
# iterations. Broadcast the scalar GPU-side to keep them valid.
buf[num_logits:] = logits_indices[-1]
# Pad so the model's KV-sharing layers run their reduced (logits-only)
# batch at a captured piecewise cudagraph size. Pre-capture, or with
# cudagraphs off, dispatch returns the unpadded count.
desc = self.cudagraph_manager.dispatch(
num_reqs=num_reqs,
num_tokens=num_logits,
uniform_token_count=None,
num_active_loras=0,
)
num_logits_padded = min(desc.num_tokens, self.max_num_tokens)
return FastPrefillBatchMetadata(
logits_indices_padded=buf[:num_logits_padded],
num_logits_indices=num_logits,
# Largest per-request logits count, known on the host.
max_logits_per_req=int(np.diff(cu_num_logits_np).max()),
)


def get_kv_cache_spec(vllm_config: VllmConfig) -> dict[str, KVCacheSpec]:
kv_cache_spec: dict[str, KVCacheSpec] = {}
layer_type = cast(type[Any], AttentionLayerBase)
Expand All @@ -75,11 +144,42 @@ def get_shared_kv_cache_layers(vllm_config: VllmConfig):
}


def get_kv_sharing_fast_prefill_eligible_layers(
vllm_config: VllmConfig, draft_layer_names: set[str] | None = None
) -> set[str]:
"""Trailing run of KV-sharing layers, eligible for fast prefill.

In You Only Cache Once (https://arxiv.org/abs/2405.05254) or other similar
KV sharing setups, only the layers that generate KV caches are involved in
the prefill phase, enabling prefill to early exit. Layers are registered in
execution order, so the eligible layers are the contiguous suffix of
KV-sharing layers.

Speculator draft layers register after the target model's layers (and may
themselves share KV), so they are excluded from the walk.
"""
if not vllm_config.cache_config.kv_sharing_fast_prefill:
return set()
shared_kv_cache_layers = get_shared_kv_cache_layers(vllm_config)
if not shared_kv_cache_layers:
return set()
eligible_layers: set[str] = set()
attn_layers = get_layers_from_vllm_config(vllm_config, Attention)
for layer_name in reversed(attn_layers):
if draft_layer_names is not None and layer_name in draft_layer_names:
continue
if layer_name not in shared_kv_cache_layers:
break
eligible_layers.add(layer_name)
return eligible_layers


def init_attn_backend(
kv_cache_config: KVCacheConfig,
vllm_config: VllmConfig,
device: torch.device,
active_layer_names: set[str] | None = None,
draft_layer_names: set[str] | None = None,
) -> tuple[list[list[AttentionGroup]], AttentionCGSupportInfo, list[int]]:
# Phase 1: discover attention groups for each kv cache group.
attn_groups: list[list[AttentionGroup]] = []
Expand All @@ -89,6 +189,9 @@ def init_attn_backend(
add_kv_sharing_layers_to_kv_cache_groups(
get_shared_kv_cache_layers(vllm_config), kv_cache_config.kv_cache_groups
)
fast_prefill_eligible_layers = get_kv_sharing_fast_prefill_eligible_layers(
vllm_config, draft_layer_names
)

# Phase 1: discover attention groups for each kv cache group.
for kv_cache_group_id, kv_cache_group_spec in enumerate(
Expand All @@ -106,6 +209,10 @@ def init_attn_backend(

for layer_name in layer_names:
attn_backend = attn_layers[layer_name].get_attn_backend()
if layer_name in fast_prefill_eligible_layers:
attn_backend = create_fast_prefill_custom_backend(
"FastPrefill", attn_backend
)

layer_kv_cache_spec: KVCacheSpec = kv_cache_group_spec.kv_cache_spec
if isinstance(layer_kv_cache_spec, UniformTypeKVCacheSpecs):
Expand Down Expand Up @@ -278,6 +385,7 @@ def build_attn_metadata(
causal: bool | torch.Tensor | Mapping[int, bool] = True,
rswa_prefix_lens: torch.Tensor | None = None,
ubatch_idx: int = 0,
fast_prefill: FastPrefillBatchMetadata | None = None,
) -> dict[str, Any]:
seq_lens = seq_lens[:num_reqs]
if dcp_local_seq_lens is not None:
Expand Down Expand Up @@ -305,6 +413,12 @@ def build_attn_metadata(
group_is_prefilling = common_attn_metadata_extra_kwargs.pop(
"is_prefilling", is_prefilling
)
if fast_prefill is not None:
common_attn_metadata_extra_kwargs.update(
logits_indices_padded=fast_prefill.logits_indices_padded,
num_logits_indices=fast_prefill.num_logits_indices,
max_logits_per_req=fast_prefill.max_logits_per_req,
)
common_attn_metadata = CommonAttentionMetadata(
query_start_loc=query_start_loc_gpu,
query_start_loc_cpu=query_start_loc_cpu,
Expand Down
5 changes: 5 additions & 0 deletions vllm/v1/worker/gpu/input_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from vllm.utils.math_utils import cdiv

if TYPE_CHECKING:
from vllm.v1.worker.gpu.attn_utils import FastPrefillBatchMetadata
from vllm.v1.worker.gpu.block_table import BlockTables


Expand Down Expand Up @@ -109,6 +110,10 @@ class InputBatch:
# stays valid for every replay the graph serves.
max_query_len: int | None = None

# Arms the KV-sharing fast prefill path for this step. Absent for dummy
# (cudagraph capture) batches, which run the KV-sharing layers in full.
fast_prefill: "FastPrefillBatchMetadata | None" = None

@classmethod
def make_dummy(
cls,
Expand Down
Loading
Loading