A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 4, 02:30 UTC
[Attention][Cleanup] Remove tree attention (#42121)
Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
Lucas Wilkinson committed
May 8, 2026 at 21:36 UTC
b1728c1e660d35f0443605c779cf0643e7737cec
11 files changed
+6
-1399
.buildkite/intel_jobs/test-intel.yaml
+1
-1
@@ -61,5 +61,5 @@ steps:
61
pytest -v -s v1/worker --ignore=v1/worker/test_gpu_model_runner.py --ignore=v1/worker/test_worker_memory_snapshot.py &&
62
pytest -v -s v1/structured_output &&
63
pytest -v -s v1/test_serial_utils.py &&
64
- pytest -v -s v1/spec_decode --ignore=v1/spec_decode/test_max_len.py --ignore=v1/spec_decode/test_tree_attention.py --ignore=v1/spec_decode/test_speculators_eagle3.py --ignore=v1/spec_decode/test_acceptance_length.py &&
64
+ pytest -v -s v1/spec_decode --ignore=v1/spec_decode/test_max_len.py --ignore=v1/spec_decode/test_speculators_eagle3.py --ignore=v1/spec_decode/test_acceptance_length.py &&
65
pytest -v -s v1/kv_connector/unit --ignore=v1/kv_connector/unit/test_multi_connector.py --ignore=v1/kv_connector/unit/test_example_connector.py --ignore=v1/kv_connector/unit/test_lmcache_integration.py --ignore=v1/kv_connector/unit/test_hf3fs_client.py --ignore=v1/kv_connector/unit/test_hf3fs_connector.py --ignore=v1/kv_connector/unit/test_hf3fs_metadata_server.py'
docs/design/attention_backends.md
-1
@@ -179,7 +179,6 @@ Priority is **1 = highest** (tried first).
179
| `ROCM_AITER_FA` | | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32 | 64, 128, 256 | ❌ | ✅ | ❌ | ❌ | Decoder | N/A |
180
| `ROCM_AITER_UNIFIED_ATTN` | | fp16, bf16 | `auto` | %16 | Any | ✅ | ❌ | ✅ | ❌ | All | N/A |
181
| `ROCM_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | 32, 64, 80, 96, 128, 160, 192, 224, 256 | ❌ | ✅ | ✅ | ❌ | Decoder, Encoder, Encoder Only | N/A |
182
-| `TREE_ATTN` | | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | 32, 64, 96, 128, 160, 192, 224, 256 | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
182
| `TRITON_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `int8_per_token_head`, `fp8_per_token_head` | %16 | Any | ✅ | ❌ | ✅ | ❌ | All | Any |
183
| `TURBOQUANT` | | fp16, bf16 | `turboquant_k8v4`, `turboquant_4bit_nc`, `turboquant_k3v4_nc`, `turboquant_3bit_nc` | 16, 32, 64, 128 | Any | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
184
tests/utils.py
+1
-1
@@ -1865,7 +1865,7 @@ def has_module_attribute(module_name, attribute_name):
1865
1866
def get_attn_backend_list_based_on_platform() -> list[str]:
1867
if current_platform.is_cuda():
1868
- return ["FLASH_ATTN", "TRITON_ATTN", "TREE_ATTN"]
1868
+ return ["FLASH_ATTN", "TRITON_ATTN"]
1869
elif current_platform.is_rocm():
1870
attn_backend_list = ["TRITON_ATTN"]
1871
try:
tests/v1/attention/test_attention_backends.py
-1
@@ -36,7 +36,6 @@ BACKENDS_TO_TEST = [
36
AttentionBackendEnum.FLASHINFER,
37
AttentionBackendEnum.FLEX_ATTENTION,
38
AttentionBackendEnum.TRITON_ATTN,
39
- AttentionBackendEnum.TREE_ATTN,
39
"FLEX_ATTENTION_SLOW",
40
]
41
tests/v1/e2e/spec_decode/test_spec_decode.py
-5
@@ -386,11 +386,6 @@ def _run_eagle_correctness(
386
Compare the outputs of an original LLM and a speculative LLM
387
which should be the same when using eagle speculative decoding.
388
"""
389
- if attn_backend == "TREE_ATTN":
390
- pytest.skip(
391
- "TREE_ATTN is flaky in the test disable for now until it can be "
392
- "resolved (see https://github.com/vllm-project/vllm/issues/22922)"
393
- )
389
if model_impl == "transformers":
390
import transformers
391
from packaging.version import Version
tests/v1/spec_decode/test_eagle.py
-160
@@ -49,7 +49,6 @@ def _create_proposer(
49
method: str,
50
num_speculative_tokens: int,
51
attention_backend: str | None = None,
52
- speculative_token_tree: list[tuple[int, ...]] | None = None,
52
parallel_drafting: bool = False,
53
) -> EagleProposer:
54
# Method-dependent setup
@@ -75,18 +74,12 @@ def _create_proposer(
74
trust_remote_code=(method == "dflash"),
75
)
76
78
- spec_token_tree_str = None
79
- if speculative_token_tree is not None:
80
- assert num_speculative_tokens == len(speculative_token_tree)
81
- spec_token_tree_str = str(speculative_token_tree)
82
-
77
speculative_config = SpeculativeConfig(
78
target_model_config=model_config,
79
target_parallel_config=ParallelConfig(),
80
model=draft_model_dir,
81
method=method,
82
num_speculative_tokens=num_speculative_tokens,
89
- speculative_token_tree=spec_token_tree_str,
83
parallel_drafting=parallel_drafting,
84
)
85
if parallel_drafting:
@@ -844,12 +837,6 @@ def test_propose(method, attn_backend, num_speculative_tokens, monkeypatch):
837
"multi-token eagle spec decode on current platform"
838
)
839
847
- if attn_backend == "TREE_ATTN":
848
- pytest.skip(
849
- "TREE_ATTN is tested separately in test_propose_tree"
850
- "because it requires special input mocking."
851
- )
852
-
840
if attn_backend == "ROCM_AITER_FA" and current_platform.is_rocm():
841
monkeypatch.setenv("VLLM_ROCM_USE_AITER", "1")
842
@@ -955,10 +942,6 @@ def test_propose(method, attn_backend, num_speculative_tokens, monkeypatch):
942
attn_metadata_builder_cls, _ = try_get_attention_backend(
943
AttentionBackendEnum.TRITON_ATTN
944
)
958
- elif attn_backend == "TREE_ATTN":
959
- attn_metadata_builder_cls, _ = try_get_attention_backend(
960
- AttentionBackendEnum.TREE_ATTN
961
- )
945
elif attn_backend == "ROCM_AITER_FA":
946
attn_metadata_builder_cls, _ = try_get_attention_backend(
947
AttentionBackendEnum.ROCM_AITER_FA
@@ -1014,149 +997,6 @@ def test_propose(method, attn_backend, num_speculative_tokens, monkeypatch):
997
assert torch.equal(result, expected_tokens)
998
999
1017
-@pytest.mark.parametrize(
1018
- "spec_token_tree",
1019
- [
1020
- [(0,)], # A single token
1021
- [(0,), (0, 0), (0, 0, 0)], # Chain
1022
- [(0,), (1,), (2,)], # Parallel
1023
- [(0,), (1,), (2,), (0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)], # Tree
1024
- ],
1025
-)
1026
-def test_propose_tree(spec_token_tree):
1027
- # Get GPU device.
1028
- device = torch.device(DEVICE_TYPE)
1029
-
1030
- # Setup test parameters.
1031
- batch_size = 2
1032
- seq_len_1 = 5
1033
- seq_len_2 = 3
1034
- total_tokens = seq_len_1 + seq_len_2
1035
- vocab_size = 100
1036
- seq_lens = [seq_len_1, seq_len_2]
1037
- num_speculative_tokens = len(spec_token_tree)
1038
-
1039
- # Create proposer first so we can use its actual hidden_size.
1040
- proposer = _create_proposer(
1041
- "eagle",
1042
- num_speculative_tokens,
1043
- speculative_token_tree=spec_token_tree,
1044
- )
1045
- # Get the hidden_size from the proposer to ensure consistency.
1046
- hidden_size = proposer.hidden_size
1047
-
1048
- # Helper to create deterministic logits that will produce specific tokens
1049
- def create_deterministic_logits(token_ids, k: int):
1050
- logits = torch.full((batch_size, vocab_size), -100.0, device=device)
1051
- for i, token_id in enumerate(token_ids):
1052
- # Assign decreasing values to the k, consecutive, tokens.
1053
- for j in range(k):
1054
- logits[i, token_id + j] = 100.0 - j
1055
- return logits
1056
-
1057
- # Mock a model that returns deterministic logits.
1058
- base_token_ids = torch.tensor([42, 60], dtype=torch.int64, device=device)
1059
-
1060
- # Skip loading the model and replace it with a mock that returns
1061
- # deterministic outputs.
1062
- model_mock = mock.MagicMock()
1063
-
1064
- # Mock the model forward calls.
1065
- forward_returns = [
1066
- (
1067
- torch.zeros(total_tokens, hidden_size, device=device),
1068
- torch.zeros(total_tokens, hidden_size, device=device),
1069
- )
1070
- ]
1071
- for cu_num_drafts in proposer.cu_drafts_per_level:
1072
- h_logits = torch.zeros(batch_size * cu_num_drafts, hidden_size, device=device)
1073
- h_states = torch.zeros(batch_size * cu_num_drafts, hidden_size, device=device)
1074
- forward_returns.append((h_logits, h_states))
1075
- model_mock.side_effect = forward_returns
1076
-
1077
- # Mock the compute_logits calls.
1078
- cu_num_drafts_tensor = torch.tensor(
1079
- [0] + proposer.cu_drafts_per_level, dtype=torch.int32, device=device
1080
- )
1081
- logits_returns = []
1082
- for level, num_children in enumerate(proposer.child_drafts_per_level):
1083
- token_ids = base_token_ids + cu_num_drafts_tensor[level]
1084
- level_num_drafts = cu_num_drafts_tensor[level + 1] - cu_num_drafts_tensor[level]
1085
- level_logits = []
1086
- for i in range(level_num_drafts // num_children):
1087
- level_logits.append(
1088
- create_deterministic_logits(token_ids + i * num_children, num_children)
1089
- )
1090
- logits_returns.append(torch.stack(level_logits, dim=1))
1091
- model_mock.compute_logits.side_effect = logits_returns
1092
-
1093
- # Assign the mock to the proposer
1094
- proposer.model = model_mock
1095
-
1096
- # Assign draft attn_layer_names since load_model is not invoked
1097
- proposer._draft_attn_layer_names = {"layer.0"}
1098
-
1099
- # Get the tree attention metadata builder.
1100
- attn_metadata_builder_cls, _ = try_get_attention_backend(
1101
- AttentionBackendEnum.TREE_ATTN
1102
- )
1103
- attn_metadata_builder = attn_metadata_builder_cls(
1104
- kv_cache_spec=create_standard_kv_cache_spec(proposer.vllm_config),
1105
- layer_names=proposer._draft_attn_layer_names,
1106
- vllm_config=proposer.vllm_config,
1107
- device=device,
1108
- )
1109
-
1110
- # Mock runner and draft_attn_groups for attention metadata building.
1111
- proposer.runner = mock.MagicMock()
1112
- mock_attn_group = mock.MagicMock()
1113
- mock_attn_group.get_metadata_builder.return_value = attn_metadata_builder
1114
- mock_attn_group.layer_names = list(proposer._draft_attn_layer_names)
1115
- mock_attn_group.kv_cache_spec = attn_metadata_builder.kv_cache_spec
1116
- proposer.draft_attn_groups = [mock_attn_group]
1117
-
1118
- # Setup inputs for the proposer.
1119
- target_token_ids = torch.randint(0, vocab_size, (total_tokens,), device=device)
1120
- target_positions = torch.cat(
1121
- [torch.arange(seq_len_1, device=device), torch.arange(seq_len_2, device=device)]
1122
- )
1123
- target_hidden_states = torch.randn(total_tokens, hidden_size, device=device)
1124
- next_token_ids = torch.randint(
1125
- 0, vocab_size, (batch_size,), dtype=torch.int32, device=device
1126
- )
1127
- batch_spec = BatchSpec(
1128
- seq_lens=seq_lens,
1129
- query_lens=seq_lens,
1130
- )
1131
- common_attn_metadata = create_common_attn_metadata(
1132
- batch_spec,
1133
- block_size=BLOCK_SIZE,
1134
- device=device,
1135
- )
1136
- sampling_metadata = mock.MagicMock()
1137
-
1138
- # Propose draft tokens.
1139
- result = proposer.propose(
1140
- target_token_ids=target_token_ids,
1141
- target_positions=target_positions,
1142
- target_hidden_states=target_hidden_states,
1143
- next_token_ids=next_token_ids,
1144
- token_indices_to_sample=None,
1145
- common_attn_metadata=common_attn_metadata,
1146
- sampling_metadata=sampling_metadata,
1147
- )
1148
- assert result.shape == (batch_size, num_speculative_tokens)
1149
-
1150
- # The tokens are expected to be consecutive integers starting
1151
- # from the base token IDs.
1152
- expected_tokens = base_token_ids[:, None] + torch.arange(
1153
- num_speculative_tokens, dtype=torch.int64, device=device
1154
- )
1155
-
1156
- # Verify that the draft tokens match our expectations.
1157
- assert torch.equal(result, expected_tokens)
1158
-
1159
-
1000
def test_set_inputs_first_pass_dflash():
1001
"""
1002
Test for DFlash set_inputs_first_pass.
tests/v1/spec_decode/test_tree_attention.py
deleted
-506
@@ -1,506 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-
4
-import math
5
-
6
-import pytest
7
-import torch
8
-
9
-from tests.v1.attention.utils import (
10
- create_standard_kv_cache_spec,
11
- create_vllm_config,
12
- try_backend_includes_kv_cache_update,
13
- try_get_attention_backend,
14
-)
15
-from vllm.config import ParallelConfig, SpeculativeConfig
16
-from vllm.platforms import current_platform
17
-from vllm.utils.torch_utils import set_random_seed
18
-from vllm.v1.attention.backend import CommonAttentionMetadata
19
-from vllm.v1.attention.backends.fa_utils import is_flash_attn_varlen_func_available
20
-from vllm.v1.attention.backends.registry import AttentionBackendEnum
21
-
22
-DEVICE_TYPE = current_platform.device_type
23
-
24
-if not is_flash_attn_varlen_func_available():
25
- pytest.skip(
26
- "This test requires flash_attn_varlen_func, but it's not available.",
27
- allow_module_level=True,
28
- )
29
-
30
-# --------------------------------------------------------------------------- #
31
-# KV cache layout adaptation
32
-# --------------------------------------------------------------------------- #
33
-# Two KV cache layouts exist across backends:
34
-#
35
-# Flash layout: (2, num_blocks, block_size, num_kv_heads, head_size)
36
-# - dim 0 separates key (index 0) and value (index 1)
37
-# - Used by: FLASH_ATTN, TREE_ATTN, ROCM_AITER_FA, ROCM_ATTN
38
-#
39
-# Block layout: (num_blocks, 2, block_size, num_kv_heads, head_size)
40
-# - dim 1 separates key (index 0) and value (index 1)
41
-# - Used by: TRITON_ATTN
42
-#
43
-# The test creates KV caches in flash layout (the canonical format used by
44
-# tree attention). When a reference backend needs block layout we transpose
45
-# dims 0 and 1.
46
-#
47
-# Note: ROCM_ATTN uses flash layout for storage but its forward path calls
48
-# PagedAttention.split_kv_cache which reinterprets the raw memory as paged
49
-# layout (num_blocks, num_kv_heads, head_size//x, block_size, x). This is
50
-# a view-level incompatibility, not a transpose - see the TODO in
51
-# _get_available_reference_backends for details.
52
-#
53
-# TODO: Replace this mapping with a `KV_CACHE_LAYOUT` class attribute on each
54
-# AttentionImpl so the layout is self-documented by the backend itself, e.g.:
55
-# class TritonAttentionImpl(AttentionImpl):
56
-# KV_CACHE_LAYOUT = "block"
57
-# --------------------------------------------------------------------------- #
58
-
59
-_BLOCK_KV_LAYOUT_BACKENDS = frozenset(
60
- {
61
- AttentionBackendEnum.TRITON_ATTN,
62
- }
63
-)
64
-
65
-# Backends whose do_kv_cache_update requires engine-level state (e.g.
66
-# ForwardContext) that is not available in this test harness, but whose
67
-# KV cache is flash layout and can be written with reshape_and_cache_flash.
68
-# When a backend is listed here, forward_attention() bypasses
69
-# do_kv_cache_update and writes directly to the cache.
70
-_NEEDS_DIRECT_CACHE_UPDATE = frozenset(
71
- {
72
- AttentionBackendEnum.ROCM_AITER_FA,
73
- }
74
-)
75
-
76
-# Backends with known test-harness incompatibilities - see the TODOs
77
-# inside _get_available_reference_backends for details.
78
-_INCOMPATIBLE_REFERENCE_BACKENDS = frozenset(
79
- {
80
- AttentionBackendEnum.ROCM_AITER_FA,
81
- AttentionBackendEnum.ROCM_ATTN,
82
- }
83
-)
84
-
85
-
86
-def _adapt_kv_cache_for_backend(
87
- kv_cache: torch.Tensor,
88
- backend: AttentionBackendEnum,
89
-) -> torch.Tensor:
90
- """Convert kv_cache from flash layout ``(2, num_blocks, ...)`` to block
91
- layout ``(num_blocks, 2, ...)`` if the backend requires it. Returns the
92
- original tensor unchanged when no conversion is needed."""
93
- if backend in _BLOCK_KV_LAYOUT_BACKENDS:
94
- return kv_cache.transpose(0, 1).contiguous()
95
- return kv_cache
96
-
97
-
98
-def _get_platform_default_backend() -> AttentionBackendEnum:
99
- """Ask the platform what backend it would auto-select at runtime."""
100
- from vllm.v1.attention.selector import AttentionSelectorConfig
101
-
102
- config = AttentionSelectorConfig(
103
- block_size=32,
104
- kv_cache_dtype="auto",
105
- use_mla=False,
106
- use_sparse=False,
107
- head_size=128,
108
- dtype=torch.bfloat16,
109
- )
110
- backend_path = current_platform.get_attn_backend_cls(
111
- selected_backend=None,
112
- attn_selector_config=config,
113
- )
114
- for backend in AttentionBackendEnum:
115
- try:
116
- if backend.get_path() == backend_path:
117
- return backend
118
- except ValueError:
119
- continue
120
- raise RuntimeError(
121
- f"Platform returned backend path '{backend_path}' "
122
- f"that doesn't match any AttentionBackendEnum member."
123
- )
124
-
125
-
126
-def _get_available_reference_backends() -> list[AttentionBackendEnum]:
127
- """Collect all reference backends the current platform can run.
128
-
129
- On CUDA this is just FLASH_ATTN. On ROCm this includes the platform
130
- default plus every backend the hardware supports, so the test validates
131
- tree attention against all of them.
132
- """
133
- if current_platform.is_rocm():
134
- backends: list[AttentionBackendEnum] = []
135
-
136
- # 1. Whatever the platform would auto-select at runtime.
137
- default_backend = _get_platform_default_backend()
138
- if default_backend not in _INCOMPATIBLE_REFERENCE_BACKENDS:
139
- backends.append(default_backend)
140
-
141
- # 2. TRITON_ATTN - always available on ROCm.
142
- if AttentionBackendEnum.TRITON_ATTN not in backends:
143
- backends.append(AttentionBackendEnum.TRITON_ATTN)
144
-
145
- # TODO: Enable ROCM_ATTN. Its forward path uses
146
- # PagedAttention.split_kv_cache which reinterprets the raw
147
- # cache memory as paged layout:
148
- # key: (num_blocks, num_kv_heads, head_size//x, block_size, x)
149
- # value: (num_blocks, num_kv_heads, head_size, block_size)
150
- # Tree attention writes prefix data in NHD flash layout, so the
151
- # same bytes produce completely different values when read in
152
- # paged format. Supporting ROCM_ATTN would require writing
153
- # prefix data via PagedAttention.write_to_paged_cache into a
154
- # separate paged-format KV cache.
155
-
156
- # TODO: Enable ROCM_AITER_FA. Its metadata builder reads head
157
- # counts from the model config at construction time and
158
- # allocates extend_workspace with those dimensions. The test
159
- # uses independent head count parameters (num_heads=2/4,
160
- # num_kv_heads=2) that don't match the model config
161
- # (Llama-3-8B: 32 q heads, 8 kv heads), causing a head count
162
- # mismatch in flash_attn_varlen_func during extend_forward.
163
- # Fixing this requires either matching test head counts to the
164
- # model config or decoupling the builder from model config
165
- # head geometry. The direct cache update path
166
- # (_NEEDS_DIRECT_CACHE_UPDATE) is already in place for when
167
- # this is resolved.
168
-
169
- return backends
170
-
171
- # CUDA: flash attention.
172
- return [AttentionBackendEnum.FLASH_ATTN]
173
-
174
-
175
-class MockAttentionLayer(torch.nn.Module):
176
- _q_scale = torch.tensor(1.0, dtype=torch.float32, device=DEVICE_TYPE)
177
- _k_scale = torch.tensor(1.0, dtype=torch.float32, device=DEVICE_TYPE)
178
- _v_scale = torch.tensor(1.0, dtype=torch.float32, device=DEVICE_TYPE)
179
- layer_name = "mock_layer"
180
-
181
- def __init__(self):
182
- super().__init__()
183
-
184
- def forward(self, x):
185
- return x
186
-
187
-
188
-def forward_attention(
189
- q: torch.Tensor,
190
- k: torch.Tensor,
191
- v: torch.Tensor,
192
- kv_cache: torch.Tensor,
193
- block_table: torch.Tensor,
194
- slot_mapping: torch.Tensor,
195
- seqlen_k: int,
196
- backend: AttentionBackendEnum,
197
- spec_token_tree: str | None = None,
198
- num_spec_tokens: int = 0,
199
-) -> torch.Tensor:
200
- """Run a single attention forward pass through the given backend.
201
-
202
- ``kv_cache`` is expected in **flash layout**
203
- ``(2, num_blocks, block_size, num_kv_heads, head_size)``.
204
- It is automatically converted when the target backend needs a
205
- different layout.
206
- """
207
- batch_size, q_len, num_heads, dim_per_head = q.shape
208
- num_kv_heads = k.shape[-2]
209
- # Initialize the query and KV sequence lengths.
210
- query_start_loc = q_len * torch.arange(
211
- batch_size + 1, device=q.device, dtype=torch.int32
212
- )
213
- query_lens = torch.diff(query_start_loc)
214
- seq_lens = torch.full(
215
- (batch_size,),
216
- seqlen_k,
217
- device=q.device,
218
- dtype=torch.int32,
219
- )
220
- context_lens = seq_lens - query_lens
221
- max_seq_len = int(seq_lens.max())
222
- max_query_len = q_len
223
- num_actual_tokens = query_start_loc[-1]
224
-
225
- softmax_scale = q.shape[-1] ** (-0.5)
226
- layer = MockAttentionLayer()
227
-
228
- # Build common metadata.
229
- model_name = "meta-llama/Meta-Llama-3-8B"
230
- builder_cls, impl_cls = try_get_attention_backend(backend)
231
- vllm_config = create_vllm_config(model_name=model_name, max_model_len=max(seq_lens))
232
- if spec_token_tree is not None:
233
- # Create speculative config if token tree is specified.
234
- vllm_config.speculative_config = SpeculativeConfig(
235
- target_model_config=vllm_config.model_config,
236
- target_parallel_config=ParallelConfig(),
237
- model=model_name,
238
- method="eagle",
239
- num_speculative_tokens=num_spec_tokens,
240
- speculative_token_tree=spec_token_tree,
241
- )
242
- kv_cache_spec = create_standard_kv_cache_spec(vllm_config)
243
- builder = builder_cls(kv_cache_spec, [], vllm_config, q.device)
244
- seq_lens_cpu = seq_lens.cpu()
245
- common_attn_metadata = CommonAttentionMetadata(
246
- query_start_loc=query_start_loc,
247
- query_start_loc_cpu=query_start_loc.cpu(),
248
- seq_lens=seq_lens,
249
- seq_lens_cpu_upper_bound=seq_lens_cpu,
250
- _seq_lens_cpu=seq_lens_cpu,
251
- _num_computed_tokens_cpu=context_lens.cpu(),
252
- num_reqs=batch_size,
253
- num_actual_tokens=num_actual_tokens,
254
- max_query_len=max_query_len,
255
- max_seq_len=max_seq_len,
256
- block_table_tensor=block_table,
257
- slot_mapping=slot_mapping,
258
- )
259
-
260
- # Build attention metadata.
261
- attn_metadata = builder.build(
262
- common_prefix_len=0,
263
- common_attn_metadata=common_attn_metadata,
264
- )
265
-
266
- # Initialize the backend implementation.
267
- instance = impl_cls(
268
- num_heads=num_heads,
269
- head_size=dim_per_head,
270
- scale=softmax_scale,
271
- num_kv_heads=num_kv_heads,
272
- alibi_slopes=None,
273
- sliding_window=None,
274
- kv_cache_dtype="auto",
275
- )
276
-
277
- # Adapt KV cache layout for this backend.
278
- adapted_kv_cache = _adapt_kv_cache_for_backend(kv_cache, backend)
279
-
280
- # Run forward pass and return output.
281
- query = q.view(-1, num_heads, dim_per_head)
282
- key = k.view(-1, num_kv_heads, dim_per_head)
283
- value = v.view(-1, num_kv_heads, dim_per_head)
284
- output = torch.empty_like(query)
285
- if not try_backend_includes_kv_cache_update(backend):
286
- if backend in _NEEDS_DIRECT_CACHE_UPDATE:
287
- # This backend's do_kv_cache_update requires engine-level
288
- # ForwardContext that isn't available in this test harness.
289
- # Write directly using reshape_and_cache_flash since the
290
- # KV cache layout is identical (flash layout, unbind on dim 0).
291
- key_cache, value_cache = adapted_kv_cache.unbind(0)
292
- torch.ops._C_cache_ops.reshape_and_cache_flash(
293
- key,
294
- value,
295
- key_cache,
296
- value_cache,
297
- attn_metadata.slot_mapping,
298
- "auto",
299
- layer._k_scale,
300
- layer._v_scale,
301
- )
302
- else:
303
- instance.do_kv_cache_update(
304
- layer=layer,
305
- key=key,
306
- value=value,
307
- kv_cache=adapted_kv_cache,
308
- slot_mapping=attn_metadata.slot_mapping,
309
- )
310
- return instance.forward(
311
- layer=layer,
312
- query=query,
313
- key=key,
314
- value=value,
315
- kv_cache=adapted_kv_cache.clone(),
316
- attn_metadata=attn_metadata,
317
- output=output,
318
- )
319
-
320
-
321
-@pytest.mark.parametrize(
322
- "reference_backend",
323
- _get_available_reference_backends(),
324
- ids=lambda b: b.name,
325
-)
326
-def test_tree_attn_correctness(
327
- reference_backend: AttentionBackendEnum,
328
-) -> None:
329
- set_random_seed(42)
330
-
331
- device = "cuda"
332
- tree_attn_masks = {
333
- # Chain.
334
- "[(0,), (0, 0), (0, 0, 0)]": torch.tensor(
335
- [
336
- [1, 0, 0, 0],
337
- [1, 1, 0, 0],
338
- [1, 1, 1, 0],
339
- [1, 1, 1, 1],
340
- ],
341
- device=device,
342
- dtype=torch.int32,
343
- ),
344
- # Tree.
345
- "[(0,), (1,), (0, 0), (0, 1), (1, 0), (1, 1)]": torch.tensor(
346
- [
347
- [1, 0, 0, 0, 0, 0, 0],
348
- [1, 1, 0, 0, 0, 0, 0],
349
- [1, 0, 1, 0, 0, 0, 0],
350
- [1, 1, 0, 1, 0, 0, 0],
351
- [1, 1, 0, 0, 1, 0, 0],
352
- [1, 0, 1, 0, 0, 1, 0],
353
- [1, 0, 1, 0, 0, 0, 1],
354
- ],
355
- device=device,
356
- dtype=torch.int32,
357
- ),
358
- }
359
-
360
- dim_per_head = 128
361
- num_kv_heads = 2
362
- block_size = 32
363
- max_sequence_length = 8192
364
- randomize_blocks = True
365
- for batch_size in [1, 16, 32]:
366
- for num_heads in [2, 4]:
367
- for sequence_position in [16, 1024, 2048]:
368
- for spec_token_tree, tree_attn_mask in tree_attn_masks.items():
369
- # Assert that the number of heads is divisible
370
- # by the number of KV heads.
371
- assert num_heads % num_kv_heads == 0
372
-
373
- # Initialize q, k, and v.
374
- tree_size_q = tree_attn_mask.shape[0]
375
- seqlen_k = sequence_position + tree_size_q
376
- q = torch.randn(
377
- (batch_size, tree_size_q, num_heads, dim_per_head),
378
- device=device,
379
- dtype=torch.bfloat16,
380
- )
381
- k = torch.randn(
382
- (batch_size, tree_size_q, num_kv_heads, dim_per_head),
383
- device=device,
384
- dtype=torch.bfloat16,
385
- )
386
- v = torch.randn(
387
- (batch_size, tree_size_q, num_kv_heads, dim_per_head),
388
- device=device,
389
- dtype=torch.bfloat16,
390
- )
391
-
392
- # KV cache in flash layout - the canonical format for
393
- # tree attention. forward_attention() handles conversion
394
- # when needed.
395
- assert max_sequence_length % block_size == 0
396
- max_blocks_per_batch = max_sequence_length // block_size
397
- kv_cache = torch.randn(
398
- (
399
- 2,
400
- batch_size * max_blocks_per_batch,
401
- block_size,
402
- num_kv_heads,
403
- dim_per_head,
404
- ),
405
- device=q.device,
406
- dtype=torch.bfloat16,
407
- )
408
- num_alloc_blocks_per_batch = math.ceil(seqlen_k / block_size)
409
- block_table = torch.zeros(
410
- (batch_size, max_blocks_per_batch),
411
- device=q.device,
412
- dtype=torch.int32,
413
- )
414
- block_ids = torch.arange(
415
- 0,
416
- batch_size * num_alloc_blocks_per_batch,
417
- device=q.device,
418
- dtype=torch.int32,
419
- )
420
- if randomize_blocks:
421
- # Randomize the block ids.
422
- block_ids = block_ids[torch.randperm(block_ids.numel())]
423
- block_table[:, :num_alloc_blocks_per_batch] = block_ids.view(
424
- -1, num_alloc_blocks_per_batch
425
- )
426
-
427
- # Set up the slot mapping for the input KVs.
428
- tree_positions = sequence_position + torch.arange(
429
- 0,
430
- tree_size_q,
431
- device=q.device,
432
- dtype=torch.int64,
433
- ).repeat(batch_size, 1)
434
- tree_slot_mapping = _gen_slot_mapping(
435
- tree_positions, block_table, block_size
436
- )
437
-
438
- # Compute attention for the tree.
439
- tree_attn_output = forward_attention(
440
- q=q,
441
- k=k,
442
- v=v,
443
- kv_cache=kv_cache,
444
- block_table=block_table,
445
- slot_mapping=tree_slot_mapping,
446
- seqlen_k=seqlen_k,
447
- backend=AttentionBackendEnum.TREE_ATTN,
448
- spec_token_tree=spec_token_tree,
449
- num_spec_tokens=tree_size_q - 1,
450
- ).view(batch_size, -1, num_heads, dim_per_head)
451
-
452
- # Verify each branch against the reference backend.
453
- for q_index in range(tree_size_q):
454
- # Get the q, k, and v for the branch.
455
- branch_mask = tree_attn_mask[q_index, :]
456
- branch_indices = torch.nonzero(branch_mask, as_tuple=True)[0]
457
- q_len = branch_indices.shape[0]
458
- q_branch = q[:, branch_indices]
459
- k_branch = k[:, branch_indices]
460
- v_branch = v[:, branch_indices]
461
-
462
- # Setup slot mapping for the branch.
463
- branch_positions = sequence_position + torch.arange(
464
- 0,
465
- q_len,
466
- device=q.device,
467
- dtype=torch.int64,
468
- ).repeat(batch_size, 1)
469
- branch_slot_mapping = _gen_slot_mapping(
470
- branch_positions, block_table, block_size
471
- )
472
-
473
- # Reference attention for this branch.
474
- ref_output = forward_attention(
475
- q=q_branch,
476
- k=k_branch,
477
- v=v_branch,
478
- kv_cache=kv_cache,
479
- block_table=block_table,
480
- slot_mapping=branch_slot_mapping,
481
- seqlen_k=sequence_position + q_len,
482
- backend=reference_backend,
483
- ).view(batch_size, -1, num_heads, dim_per_head)
484
-
485
- # Compare the outputs.
486
- assert torch.allclose(
487
- tree_attn_output[:, branch_indices],
488
- ref_output,
489
- atol=7.81e-3,
490
- ), (
491
- f"outputs are not close for "
492
- f"reference_backend: {reference_backend.name}, "
493
- f"batch_size: {batch_size}, "
494
- f"num_heads: {num_heads}, "
495
- f"sequence_position: {sequence_position}, "
496
- f"tree_attn_mask: {tree_attn_mask}, "
497
- f"q_index: {q_index}."
498
- )
499
-
500
-
501
-def _gen_slot_mapping(
502
- positions: torch.Tensor, block_table: torch.Tensor, block_size: int
503
-):
504
- block_indices = positions // block_size
505
- blocks = block_table.gather(dim=1, index=block_indices)
506
- return (blocks * block_size + positions % block_size).view(-1)
vllm/config/speculative.py
+4
-21
@@ -1,7 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-import ast
4
import copy
5
from typing import TYPE_CHECKING, Any, Literal, get_args
6
@@ -145,9 +144,6 @@ class SpeculativeConfig:
144
provided. Defaults to 1."""
145
146
# Alternative drafting strategies
148
- speculative_token_tree: str | None = None
149
- """Specifies the tree structure for speculative token generation.
150
- """
147
parallel_drafting: bool = False
148
"""Enable parallel drafting, where all speculative tokens are generated
149
in parallel rather than sequentially. This can improve performance but
@@ -739,23 +735,10 @@ class SpeculativeConfig:
735
f" must be divisible by {n_predict=}"
736
)
737
742
- if self.speculative_token_tree is None:
743
- if self.num_speculative_tokens is None:
744
- raise ValueError(
745
- "A speculative model was provided, but neither "
746
- "`speculative_token_tree` nor `num_speculative_tokens` "
747
- "was provided"
748
- )
749
-
750
- # Generate chain of tokens.
751
- self.speculative_token_tree = str(
752
- [(i + 1) * (0,) for i in range(self.num_speculative_tokens)]
753
- )
754
- else:
755
- # Sort the token tree breadth-first.
756
- tree_choices = ast.literal_eval(self.speculative_token_tree)
757
- self.speculative_token_tree = str(
758
- sorted(tree_choices, key=lambda t: (len(t), t))
738
+ if self.num_speculative_tokens is None:
739
+ raise ValueError(
740
+ "A speculative model was provided, but "
741
+ "`num_speculative_tokens` was not provided"
742
)
743
744
self.draft_tensor_parallel_size = (
vllm/v1/attention/backends/registry.py
-1
@@ -76,7 +76,6 @@ class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
76
FLASH_ATTN_MLA = "vllm.v1.attention.backends.mla.flashattn_mla.FlashAttnMLABackend"
77
NO_ATTENTION = "vllm.v1.attention.backends.no_attention.NoAttentionBackend"
78
FLEX_ATTENTION = "vllm.v1.attention.backends.flex_attention.FlexAttentionBackend"
79
- TREE_ATTN = "vllm.v1.attention.backends.tree_attn.TreeAttentionBackend"
79
ROCM_AITER_UNIFIED_ATTN = (
80
"vllm.v1.attention.backends.rocm_aiter_unified_attn."
81
"RocmAiterUnifiedAttentionBackend"
vllm/v1/attention/backends/tree_attn.py
deleted
-488
@@ -1,488 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""Attention layer with TreeAttention."""
4
-
5
-import ast
6
-from dataclasses import dataclass
7
-from typing import ClassVar
8
-
9
-import torch
10
-
11
-from vllm import _custom_ops as ops
12
-from vllm.config import VllmConfig
13
-from vllm.config.cache import CacheDType
14
-from vllm.logger import init_logger
15
-from vllm.v1.attention.backend import (
16
- AttentionBackend,
17
- AttentionImpl,
18
- AttentionMetadataBuilder,
19
- AttentionType,
20
- CommonAttentionMetadata,
21
- MultipleOf,
22
-)
23
-from vllm.v1.attention.backends.utils import (
24
- split_decodes_and_prefills,
25
-)
26
-from vllm.v1.attention.ops.triton_unified_attention import unified_attention
27
-from vllm.v1.kv_cache_interface import AttentionSpec
28
-
29
-logger = init_logger(__name__)
30
-
31
-
32
-class TreeAttentionBackend(AttentionBackend):
33
- supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16]
34
- supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
35
- "auto",
36
- "float16",
37
- "bfloat16",
38
- ]
39
- forward_includes_kv_cache_update: bool = False
40
-
41
- @staticmethod
42
- def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
43
- return [MultipleOf(16)]
44
-
45
- @classmethod
46
- def get_supported_head_sizes(cls) -> list[int]:
47
- return [32, 64, 96, 128, 160, 192, 224, 256]
48
-
49
- @staticmethod
50
- def get_name() -> str:
51
- return "TREE_ATTN"
52
-
53
- @staticmethod
54
- def get_impl_cls() -> type["TreeAttentionImpl"]:
55
- return TreeAttentionImpl
56
-
57
- @staticmethod
58
- def get_kv_cache_shape(
59
- num_blocks: int,
60
- block_size: int,
61
- num_kv_heads: int,
62
- head_size: int,
63
- cache_dtype_str: str = "auto",
64
- ) -> tuple[int, ...]:
65
- if block_size % 16 != 0:
66
- raise ValueError("Block size must be a multiple of 16.")
67
- return (2, num_blocks, block_size, num_kv_heads, head_size)
68
-
69
- @staticmethod
70
- def get_builder_cls() -> type["TreeAttentionMetadataBuilder"]:
71
- return TreeAttentionMetadataBuilder
72
-
73
- @staticmethod
74
- def use_cascade_attention(*args, **kwargs) -> bool:
75
- return False
76
-
77
-
78
-@dataclass
79
-class TreeAttentionMetadata:
80
- num_actual_tokens: int # Number of tokens excluding padding.
81
- max_query_len: int
82
- query_start_loc: torch.Tensor
83
- max_seq_len: int
84
- seq_lens: torch.Tensor
85
- block_table: torch.Tensor
86
- slot_mapping: torch.Tensor
87
-
88
- num_prefill_tokens: int = 0
89
- num_decode_tokens: int = 0
90
- num_prefills: int = 0
91
- num_decodes: int = 0
92
-
93
- # Precomputed (on CPU in the builder) max_query_len and max_seq_len for
94
- # the prefill-only and decode-only sub-batches. Used by the properties
95
- # below to avoid a GPU->CPU sync via `.max().item()` on every forward.
96
- max_query_len_prefill: int = 0
97
- max_seq_len_prefill: int = 0
98
- max_query_len_decode: int = 0
99
- max_seq_len_decode: int = 0
100
-
101
- tree_attn_bias: torch.Tensor | None = None
102
-
103
- # Cached Prefill/decode metadata.
104
- _cached_prefill_metadata: "TreeAttentionMetadata | None" = None
105
- _cached_decode_metadata: "TreeAttentionMetadata | None" = None
106
-
107
- @property
108
- def prefill_metadata(self) -> "TreeAttentionMetadata | None":
109
- if self.num_prefills == 0:
110
- return None
111
-
112
- if self._cached_prefill_metadata is not None:
113
- # Recover cached prefill-phase attention
114
- # metadata structure
115
- return self._cached_prefill_metadata
116
-
117
- q_start_loc = self.query_start_loc[self.num_decodes :]
118
- kv_seqlens = self.seq_lens[self.num_decodes :]
119
- # Construct & cache prefill-phase attention metadata structure
120
- self._cached_prefill_metadata = TreeAttentionMetadata(
121
- num_actual_tokens=self.num_prefill_tokens,
122
- max_query_len=self.max_query_len_prefill,
123
- query_start_loc=q_start_loc - q_start_loc[0],
124
- max_seq_len=self.max_seq_len_prefill,
125
- seq_lens=kv_seqlens,
126
- block_table=self.block_table[self.num_decodes :],
127
- slot_mapping=self.slot_mapping[self.num_decode_tokens :],
128
- )
129
- return self._cached_prefill_metadata
130
-
131
- @property
132
- def decode_metadata(self) -> "TreeAttentionMetadata | None":
133
- if self.num_decode_tokens == 0:
134
- return None
135
-
136
- if self._cached_decode_metadata is not None:
137
- # Recover cached decode-phase attention
138
- # metadata structure
139
- return self._cached_decode_metadata
140
-
141
- q_start_loc = self.query_start_loc[: self.num_decodes + 1]
142
- kv_seqlens = self.seq_lens[: self.num_decodes]
143
- # Construct & cache decode-phase attention metadata structure
144
- self._cached_decode_metadata = TreeAttentionMetadata(
145
- num_actual_tokens=self.num_decode_tokens,
146
- max_query_len=self.max_query_len_decode,
147
- query_start_loc=q_start_loc,
148
- max_seq_len=self.max_seq_len_decode,
149
- seq_lens=kv_seqlens,
150
- block_table=self.block_table[: self.num_decodes],
151
- slot_mapping=self.slot_mapping[: self.num_decode_tokens],
152
- tree_attn_bias=self.tree_attn_bias,
153
- )
154
- return self._cached_decode_metadata
155
-
156
-
157
-class TreeAttentionMetadataBuilder(AttentionMetadataBuilder[TreeAttentionMetadata]):
158
- def __init__(
159
- self,
160
- kv_cache_spec: AttentionSpec,
161
- layer_names: list[str],
162
- vllm_config: VllmConfig,
163
- device: torch.device,
164
- ):
165
- super().__init__(kv_cache_spec, layer_names, vllm_config, device)
166
-
167
- self.block_size = kv_cache_spec.block_size
168
-
169
- spec_config = vllm_config.speculative_config
170
- spec_token_tree: str | None = None
171
- if spec := spec_config:
172
- spec_token_tree = spec.speculative_token_tree
173
- tree_choices: list[tuple[int, ...]] = (
174
- ast.literal_eval(spec_token_tree) if spec_token_tree is not None else [(0,)]
175
- )
176
- # Construct the tree attention bias.
177
- depth_counts = _get_depth_counts(tree_choices)
178
- self.tree_attn_bias = _prepare_tree_attn_bias(
179
- tree_choices,
180
- depth_counts,
181
- dtype=torch.float32,
182
- device=device,
183
- )
184
-
185
- self.reorder_batch_threshold = self.tree_attn_bias.shape[0]
186
-
187
- def build(
188
- self,
189
- common_prefix_len: int,
190
- common_attn_metadata: CommonAttentionMetadata,
191
- fast_build: bool = False,
192
- ) -> TreeAttentionMetadata:
193
- decode_threshold = self.tree_attn_bias.shape[0]
194
- num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
195
- split_decodes_and_prefills(
196
- common_attn_metadata, decode_threshold=decode_threshold
197
- )
198
- )
199
-
200
- num_actual_tokens = common_attn_metadata.num_actual_tokens
201
- q_start_loc = common_attn_metadata.query_start_loc
202
- max_query_len = common_attn_metadata.max_query_len
203
- kv_seqlens = common_attn_metadata.seq_lens
204
- max_seq_len = common_attn_metadata.max_seq_len
205
- block_table = common_attn_metadata.block_table_tensor
206
- slot_mapping = common_attn_metadata.slot_mapping
207
-
208
- # Precompute prefill/decode sub-batch max_query_len / max_seq_len on
209
- # CPU so the prefill_metadata / decode_metadata properties don't need
210
- # a GPU->CPU sync via `.max().item()` on every forward.
211
- # Prefer `seq_lens_cpu_upper_bound` over the (deprecated)
212
- # `seq_lens_cpu` property: the upper bound is precise for prefill
213
- # rows and optimistic-but-safe for decode rows (workspace sizing
214
- # from `max()` is fine with an over-estimate), and avoids the
215
- # `seq_lens.to("cpu")` sync the property would fall through to in
216
- # async-spec-decode mode. The draft-attention path (eagle
217
- # speculator) doesn't populate it; fall back to the batch-wide
218
- # `max_seq_len` as a safe upper bound for both sub-batches.
219
- q_start_loc_cpu = common_attn_metadata.query_start_loc_cpu
220
- seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
221
- if num_prefills > 0:
222
- q_seqlens_p = torch.diff(q_start_loc_cpu[num_decodes:])
223
- max_query_len_prefill = int(q_seqlens_p.max())
224
- max_seq_len_prefill = (
225
- int(seq_lens_cpu[num_decodes:].max())
226
- if seq_lens_cpu is not None
227
- else max_seq_len
228
- )
229
- else:
230
- max_query_len_prefill = 0
231
- max_seq_len_prefill = 0
232
- if num_decodes > 0:
233
- q_seqlens_d = torch.diff(q_start_loc_cpu[: num_decodes + 1])
234
- max_query_len_decode = int(q_seqlens_d.max())
235
- max_seq_len_decode = (
236
- int(seq_lens_cpu[:num_decodes].max())
237
- if seq_lens_cpu is not None
238
- else max_seq_len
239
- )
240
- else:
241
- max_query_len_decode = 0
242
- max_seq_len_decode = 0
243
-
244
- return TreeAttentionMetadata(
245
- num_actual_tokens=num_actual_tokens,
246
- num_prefill_tokens=num_prefill_tokens,
247
- num_decode_tokens=num_decode_tokens,
248
- num_prefills=num_prefills,
249
- num_decodes=num_decodes,
250
- max_query_len=max_query_len,
251
- query_start_loc=q_start_loc,
252
- max_seq_len=max_seq_len,
253
- seq_lens=kv_seqlens,
254
- block_table=block_table,
255
- slot_mapping=slot_mapping,
256
- max_query_len_prefill=max_query_len_prefill,
257
- max_seq_len_prefill=max_seq_len_prefill,
258
- max_query_len_decode=max_query_len_decode,
259
- max_seq_len_decode=max_seq_len_decode,
260
- tree_attn_bias=self.tree_attn_bias,
261
- )
262
-
263
- def build_for_drafting(
264
- self,
265
- common_attn_metadata: CommonAttentionMetadata,
266
- draft_index: int,
267
- ) -> TreeAttentionMetadata:
268
- # Cache the original tree attention bias.
269
- orig_tree_attn_bias = self.tree_attn_bias
270
-
271
- if draft_index == 0:
272
- # Use prefill for drafting at the root level.
273
- self.tree_attn_bias = torch.empty(0)
274
- else:
275
- # Slice the tree attention bias for drafting. Exclude
276
- # the root level.
277
- start, end = 1, 1 + common_attn_metadata.max_query_len
278
- self.tree_attn_bias = self.tree_attn_bias[start:end, start:end].contiguous()
279
-
280
- # Build attention bias.
281
- attn_metadata = self.build(0, common_attn_metadata, fast_build=True)
282
-
283
- # Reset the tree attention bias to the original value.
284
- self.tree_attn_bias = orig_tree_attn_bias
285
- return attn_metadata
286
-
287
-
288
-def _get_depth_counts(sorted_tree_choices: list[tuple[int, ...]]) -> list[int]:
289
- # Count the number of choices at each depth of the tree.
290
- depth_counts = []
291
- prev_depth = 0
292
- for path in sorted_tree_choices:
293
- depth = len(path)
294
- if depth != prev_depth:
295
- depth_counts.append(0)
296
- depth_counts[depth - 1] += 1
297
- prev_depth = depth
298
- return depth_counts
299
-
300
-
301
-def _prepare_tree_attn_bias(
302
- sorted_tree_choices: list[tuple[int, ...]],
303
- depth_counts: list[int],
304
- dtype: torch.dtype | None,
305
- device: torch.device | None,
306
-) -> torch.Tensor:
307
- # +1 comes from the additional root node.
308
- tree_len = len(sorted_tree_choices) + 1
309
- tree_attn_mask = torch.full(
310
- (tree_len, tree_len), -torch.inf, device=device, dtype=dtype
311
- )
312
-
313
- # Set diagonal to all zeros. Each token should
314
- # attend to itself.
315
- mask_val = 0
316
- for i in range(tree_len):
317
- tree_attn_mask[i, i] = mask_val
318
-
319
- # Set root to all zeros. All tokens attend to it.
320
- tree_attn_mask[:, 0] = mask_val
321
-
322
- # Set all ancestors to zeros.
323
- start = 0
324
- for i in range(len(depth_counts)):
325
- for j in range(depth_counts[i]):
326
- cur_tree_choice = sorted_tree_choices[start + j]
327
- # Retrieve ancestor position.
328
- if len(cur_tree_choice) == 1:
329
- continue
330
- ancestor_idx = []
331
- for c in range(len(cur_tree_choice) - 1):
332
- ancestor_idx.append(
333
- sorted_tree_choices.index(cur_tree_choice[: c + 1]) + 1
334
- )
335
- tree_attn_mask[j + start + 1, ancestor_idx] = mask_val
336
- start += depth_counts[i]
337
- return tree_attn_mask
338
-
339
-
340
-class TreeAttentionImpl(AttentionImpl):
341
- def __init__(
342
- self,
343
- num_heads: int,
344
- head_size: int,
345
- scale: float,
346
- num_kv_heads: int,
347
- alibi_slopes: list[float] | None,
348
- sliding_window: int | None,
349
- kv_cache_dtype: str,
350
- logits_soft_cap: float | None = None,
351
- attn_type: AttentionType = AttentionType.DECODER,
352
- kv_sharing_target_layer_name: str | None = None,
353
- ) -> None:
354
- self.num_heads = num_heads
355
- self.head_size = head_size
356
- self.scale = float(scale)
357
- self.num_kv_heads = num_kv_heads
358
- self.num_queries_per_kv = self.num_heads // self.num_kv_heads
359
- self.kv_cache_dtype = kv_cache_dtype
360
- self.kv_sharing_target_layer_name = kv_sharing_target_layer_name
361
- if alibi_slopes is not None:
362
- alibi_slopes = torch.tensor(alibi_slopes, dtype=torch.float32)
363
- self.alibi_slopes = alibi_slopes
364
- if logits_soft_cap is None:
365
- # Setting logits_soft_cap to 0 means no soft cap.
366
- logits_soft_cap = 0
367
- self.logits_soft_cap = logits_soft_cap
368
- if sliding_window is None:
369
- self.sliding_window = (-1, -1)
370
- else:
371
- self.sliding_window = (sliding_window - 1, 0)
372
-
373
- if attn_type != AttentionType.DECODER:
374
- raise NotImplementedError(
375
- "Encoder self-attention and "
376
- "encoder/decoder cross-attention "
377
- "are not implemented for "
378
- "TreeAttentionImpl."
379
- )
380
-
381
- def do_kv_cache_update(
382
- self,
383
- layer: torch.nn.Module,
384
- key: torch.Tensor,
385
- value: torch.Tensor,
386
- kv_cache: torch.Tensor,
387
- slot_mapping: torch.Tensor,
388
- ) -> None:
389
- key_cache, value_cache = kv_cache.unbind(0)
390
-
391
- # Reshape the input keys and values and store them in the cache.
392
- # NOTE(woosuk): Here, key and value are padded while slot_mapping is
393
- # not padded. However, we don't need to do key[:num_actual_tokens]
394
- # and value[:num_actual_tokens] because the reshape_and_cache_flash
395
- # op uses the slot_mapping's shape to determine the number of
396
- # actual tokens.
397
- ops.reshape_and_cache_flash(
398
- key,
399
- value,
400
- key_cache,
401
- value_cache,
402
- slot_mapping,
403
- self.kv_cache_dtype,
404
- layer._k_scale,
405
- layer._v_scale,
406
- )
407
-
408
- def forward(
409
- self,
410
- layer: torch.nn.Module,
411
- query: torch.Tensor,
412
- key: torch.Tensor,
413
- value: torch.Tensor,
414
- kv_cache: torch.Tensor,
415
- attn_metadata: TreeAttentionMetadata,
416
- output: torch.Tensor,
417
- output_scale: torch.Tensor | None = None,
418
- output_block_scale: torch.Tensor | None = None,
419
- ) -> torch.Tensor:
420
- """Forward pass with TreeAttention.
421
-
422
- Args:
423
- query: shape = [num_tokens, num_heads, head_size]
424
- key: shape = [num_tokens, num_kv_heads, head_size]
425
- value: shape = [num_tokens, num_kv_heads, head_size]
426
- kv_cache: shape =
427
- [2, num_blocks, block_size, num_kv_heads, head_size]
428
- attn_metadata: Metadata for attention.
429
- Returns:
430
- shape = [num_tokens, num_heads * head_size]
431
- """
432
- if output_scale is not None or output_block_scale is not None:
433
- raise NotImplementedError(
434
- "fused output quantization is not yet supported for TreeAttentionImpl"
435
- )
436
-
437
- if attn_metadata is None:
438
- # Profiling run.
439
- return output.fill_(0)
440
-
441
- key_cache, value_cache = kv_cache.unbind(0)
442
-
443
- num_actual_tokens = attn_metadata.num_actual_tokens
444
- num_decode_tokens = attn_metadata.num_decode_tokens
445
- descale_shape = (attn_metadata.query_start_loc.shape[0] - 1, key.shape[1])
446
- if prefill_meta := attn_metadata.prefill_metadata:
447
- unified_attention(
448
- q=query[num_decode_tokens:num_actual_tokens],
449
- k=key_cache,
450
- v=value_cache,
451
- out=output[num_decode_tokens:num_actual_tokens],
452
- cu_seqlens_q=prefill_meta.query_start_loc,
453
- max_seqlen_q=prefill_meta.max_query_len,
454
- seqused_k=prefill_meta.seq_lens,
455
- max_seqlen_k=prefill_meta.max_seq_len,
456
- softmax_scale=self.scale,
457
- causal=True,
458
- alibi_slopes=self.alibi_slopes,
459
- window_size=self.sliding_window,
460
- block_table=prefill_meta.block_table,
461
- softcap=self.logits_soft_cap,
462
- q_descale=None, # Not supported
463
- k_descale=layer._k_scale.expand(descale_shape),
464
- v_descale=layer._v_scale.expand(descale_shape),
465
- )
466
-
467
- if decode_meta := attn_metadata.decode_metadata:
468
- unified_attention(
469
- q=query[:num_decode_tokens],
470
- k=key_cache,
471
- v=value_cache,
472
- out=output[:num_decode_tokens],
473
- cu_seqlens_q=decode_meta.query_start_loc,
474
- max_seqlen_q=decode_meta.max_query_len,
475
- seqused_k=decode_meta.seq_lens,
476
- max_seqlen_k=decode_meta.max_seq_len,
477
- softmax_scale=self.scale,
478
- causal=True,
479
- alibi_slopes=self.alibi_slopes,
480
- qq_bias=decode_meta.tree_attn_bias,
481
- window_size=self.sliding_window,
482
- block_table=decode_meta.block_table,
483
- softcap=self.logits_soft_cap,
484
- q_descale=None, # Not supported
485
- k_descale=layer._k_scale.expand(descale_shape),
486
- v_descale=layer._v_scale.expand(descale_shape),
487
- )
488
- return output
vllm/v1/spec_decode/llm_base_proposer.py
-214
@@ -1,6 +1,5 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-import ast
3
from importlib.util import find_spec
4
from typing import Any, cast
5
@@ -29,10 +28,6 @@ from vllm.platforms import current_platform
28
from vllm.utils.platform_utils import is_pin_memory_available
29
from vllm.v1.attention.backend import CommonAttentionMetadata
30
from vllm.v1.attention.backends.registry import AttentionBackendEnum
32
-from vllm.v1.attention.backends.tree_attn import (
33
- TreeAttentionMetadata,
34
- TreeAttentionMetadataBuilder,
35
-)
31
from vllm.v1.attention.backends.triton_attn import TritonAttentionMetadata
32
from vllm.v1.cudagraph_dispatcher import CudagraphDispatcher
33
from vllm.v1.kv_cache_interface import KVCacheConfig, UniformTypeKVCacheSpecs
@@ -284,29 +279,6 @@ class SpecDecodeBaseProposer:
279
280
self.allowed_attn_types = tuple(rocm_types)
281
287
- # Parse the speculative token tree.
288
- spec_token_tree = self.speculative_config.speculative_token_tree
289
- assert spec_token_tree is not None
290
- self.tree_choices: list[tuple[int, ...]] = ast.literal_eval(spec_token_tree)
291
- tree_depth = len(self.tree_choices[-1])
292
- # Precompute per-level properties of the tree.
293
- num_drafts_per_level = [0] * tree_depth
294
- for node in self.tree_choices:
295
- num_drafts_per_level[len(node) - 1] += 1
296
- self.cu_drafts_per_level = [num_drafts_per_level[0]]
297
- self.child_drafts_per_level = [num_drafts_per_level[0]]
298
- for level in range(1, tree_depth):
299
- self.cu_drafts_per_level.append(
300
- self.cu_drafts_per_level[-1] + num_drafts_per_level[level]
301
- )
302
- self.child_drafts_per_level.append(
303
- num_drafts_per_level[level] // num_drafts_per_level[level - 1]
304
- )
305
- # Precompute draft position offsets in flattened tree.
306
- self.tree_draft_pos_offsets = torch.arange(
307
- 1, len(self.tree_choices) + 1, device=device, dtype=torch.int32
308
- ).repeat(self.max_batch_size, 1)
309
-
282
def _raise_if_padded_drafter_batch_disabled(self):
283
if self.speculative_config.disable_padded_drafter_batch:
284
raise NotImplementedError(
@@ -512,20 +484,6 @@ class SpecDecodeBaseProposer:
484
# (which read via _get_positions) use the correct values.
485
self.positions[:batch_size] = positions
486
515
- if any(isinstance(md, TreeAttentionMetadata) for md in per_group_attn_metadata):
516
- # Draft using tree attention - requires full logits for top-k
517
- logits = self.model.compute_logits(sample_hidden_states)
518
- draft_token_ids_list = self.propose_tree(
519
- batch_size=batch_size,
520
- logits=logits,
521
- positions=positions,
522
- hidden_states=hidden_states,
523
- common_attn_metadata=common_attn_metadata,
524
- slot_mappings=slot_mappings,
525
- )
526
- # [batch_size, num_tree_tokens]
527
- return torch.cat(draft_token_ids_list, dim=1)
528
-
487
draft_token_ids = self._greedy_sample(sample_hidden_states)
488
489
if self.allowed_attn_types is not None:
@@ -1014,178 +972,6 @@ class SpecDecodeBaseProposer:
972
num_rejected_tokens_gpu,
973
)
974
1017
- def propose_tree(
1018
- self,
1019
- batch_size: int,
1020
- # [num_tokens, vocab_size]
1021
- logits: torch.Tensor,
1022
- # [num_tokens]
1023
- positions: torch.Tensor,
1024
- # [num_tokens, hidden_size]
1025
- hidden_states: torch.Tensor,
1026
- common_attn_metadata: CommonAttentionMetadata,
1027
- slot_mappings: dict[str, torch.Tensor]
1028
- | list[dict[str, torch.Tensor]]
1029
- | None = None,
1030
- ) -> list[torch.Tensor]:
1031
- tree_attn_metadata_builder = self.draft_attn_groups[0].get_metadata_builder()
1032
- assert isinstance(tree_attn_metadata_builder, TreeAttentionMetadataBuilder)
1033
-
1034
- total_num_drafts = self.cu_drafts_per_level[0]
1035
- level_num_drafts = total_num_drafts
1036
- # Sample a draft token for each child at the tree root level.
1037
- num_children = self.child_drafts_per_level[0]
1038
- if num_children == 1:
1039
- draft_token_ids = logits.argmax(dim=-1).view(batch_size, -1)
1040
- else:
1041
- draft_token_ids = torch.topk(logits, num_children, dim=-1).indices.view(
1042
- batch_size, -1
1043
- )
1044
- draft_token_ids_list = [draft_token_ids]
1045
- draft_hidden_states = hidden_states.view(batch_size, 1, -1)
1046
-
1047
- # Initialize empty tensors for concatenation with the level outputs.
1048
- tree_input_ids = torch.empty(
1049
- 0, device=self.input_ids.device, dtype=self.input_ids.dtype
1050
- )
1051
- tree_positions = torch.empty(
1052
- 0, device=self.positions.device, dtype=self.positions.dtype
1053
- )
1054
- tree_hidden_states = torch.empty(
1055
- 0, device=self.hidden_states.device, dtype=self.hidden_states.dtype
1056
- )
1057
- # Precompute the draft token positions.
1058
- flattened_draft_positions = (
1059
- positions.view(batch_size, -1) + self.tree_draft_pos_offsets[:batch_size, :]
1060
- )
1061
- tree_depth = len(self.cu_drafts_per_level)
1062
- for level in range(tree_depth - 1):
1063
- # Get draft positions for RoPE.
1064
- draft_positions = positions + (level + 1)
1065
- exceeds_max_model_len = (positions + total_num_drafts) >= self.max_model_len
1066
- # Mask out the position ids that exceed the max model length.
1067
- # Otherwise, we may get out-of-range error in RoPE.
1068
- draft_positions = torch.where(
1069
- exceeds_max_model_len,
1070
- 0,
1071
- draft_positions,
1072
- ).view(batch_size, -1)
1073
-
1074
- if level_num_drafts > 1:
1075
- # Repeat the positions for each draft at this level.
1076
- draft_positions = draft_positions.repeat_interleave(
1077
- level_num_drafts, dim=1
1078
- )
1079
-
1080
- if num_children > 1:
1081
- # Repeat draft hidden states for each child.
1082
- draft_hidden_states = draft_hidden_states.repeat_interleave(
1083
- num_children, dim=1
1084
- )
1085
-
1086
- # Concatenate the draft tokens, positions, and hidden states.
1087
- tree_input_ids = torch.cat([tree_input_ids, draft_token_ids], dim=1)
1088
- tree_positions = torch.cat([tree_positions, draft_positions], dim=1)
1089
- tree_hidden_states = torch.cat(
1090
- [tree_hidden_states, draft_hidden_states], dim=1
1091
- )
1092
-
1093
- # Build new attention metadata for the next level of drafts.
1094
- # This is necessary to support tree attention.
1095
- query_len = total_num_drafts
1096
- common_attn_metadata = replace(
1097
- common_attn_metadata,
1098
- query_start_loc=query_len * self.arange[: batch_size + 1],
1099
- seq_lens=common_attn_metadata.seq_lens + level_num_drafts,
1100
- num_actual_tokens=batch_size * query_len,
1101
- max_query_len=query_len,
1102
- )
1103
- attn_metadata = tree_attn_metadata_builder.build_for_drafting(
1104
- common_attn_metadata=common_attn_metadata, draft_index=level + 1
1105
- )
1106
-
1107
- # Apply new attention metadata to all draft layers.
1108
- per_layer_attn_metadata = {}
1109
- for attn_group in self.draft_attn_groups:
1110
- for layer_name in attn_group.layer_names:
1111
- per_layer_attn_metadata[layer_name] = attn_metadata
1112
-
1113
- # Consider max model length.
1114
- attn_metadata.max_seq_len = min(
1115
- attn_metadata.max_seq_len, self.max_model_len
1116
- )
1117
- # For the requests that exceed the max model length, we set the
1118
- # sequence length to 1 to minimize their overheads in attention.
1119
- attn_metadata.seq_lens.masked_fill_(exceeds_max_model_len, 1)
1120
-
1121
- # Compute the slot mapping.
1122
- block_size = tree_attn_metadata_builder.kv_cache_spec.block_size
1123
- query_positions = flattened_draft_positions[:, level : level + query_len]
1124
- block_numbers = query_positions // block_size
1125
- block_ids = attn_metadata.block_table.gather(dim=1, index=block_numbers)
1126
- slot_mapping = block_ids * block_size + query_positions % block_size
1127
- # Mask out the slot mappings that exceed the max model length.
1128
- # Otherwise, the KV cache will be inadvertently updated with the
1129
- # padding tokens.
1130
- slot_mapping[exceeds_max_model_len] = PADDING_SLOT_ID
1131
- attn_metadata.slot_mapping = slot_mapping.view(-1)
1132
-
1133
- # Copy inputs to buffer for cudagraph.
1134
- num_tokens = attn_metadata.num_actual_tokens
1135
- input_ids = tree_input_ids.view(-1)
1136
- self.input_ids[:num_tokens] = input_ids
1137
- self.positions[:num_tokens] = tree_positions.view(-1)
1138
- self.hidden_states[:num_tokens] = tree_hidden_states.view(num_tokens, -1)
1139
-
1140
- cudagraph_runtime_mode, batch_desc = self.cudagraph_dispatcher.dispatch(
1141
- num_tokens
1142
- )
1143
- num_input_tokens = batch_desc.num_tokens
1144
- # Run the model.
1145
- with set_forward_context(
1146
- per_layer_attn_metadata,
1147
- self.vllm_config,
1148
- num_tokens=num_input_tokens,
1149
- cudagraph_runtime_mode=cudagraph_runtime_mode,
1150
- slot_mapping=self._get_slot_mapping(
1151
- num_input_tokens, attn_metadata.slot_mapping
1152
- ),
1153
- ):
1154
- last_hidden_states, hidden_states = self.model(
1155
- input_ids=self.input_ids[:num_input_tokens],
1156
- positions=self.positions[:num_input_tokens],
1157
- hidden_states=self.hidden_states[:num_input_tokens],
1158
- inputs_embeds=None,
1159
- )
1160
-
1161
- # Get the output hidden states for the draft tokens.
1162
- draft_hidden_states = hidden_states[:num_tokens].view(
1163
- batch_size, query_len, -1
1164
- )[:, -level_num_drafts:]
1165
- draft_last_hidden_states = last_hidden_states[:num_tokens].view(
1166
- batch_size, query_len, -1
1167
- )[:, -level_num_drafts:]
1168
-
1169
- # Get the output logits for the draft tokens.
1170
- logits = self.model.compute_logits(
1171
- draft_last_hidden_states.reshape(batch_size * level_num_drafts, -1)
1172
- )
1173
-
1174
- # Sample a draft token for each child at the next tree level.
1175
- num_children = self.child_drafts_per_level[level + 1]
1176
- if num_children == 1:
1177
- draft_token_ids = logits.argmax(dim=-1).view(batch_size, -1)
1178
- else:
1179
- draft_token_ids = torch.topk(logits, num_children, dim=-1).indices.view(
1180
- batch_size, -1
1181
- )
1182
- draft_token_ids_list.append(draft_token_ids)
1183
-
1184
- # Update the # drafts counters for the next tree level.
1185
- level_num_drafts = self.cu_drafts_per_level[level + 1] - total_num_drafts
1186
- total_num_drafts = self.cu_drafts_per_level[level + 1]
1187
- return draft_token_ids_list
1188
-
975
def prepare_inputs(
976
self,
977
common_attn_metadata: CommonAttentionMetadata,