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 15, 06:00 UTC
[Feature][ROCm] Add full graph capture support for TritonAttentionBackend (#19158)
Signed-off-by: charlifu <charlifu@amd.com>
Charlie Fu committed
Jun 17, 2025 at 16:03 UTC
a44b1c951df919bd6a42cb7e13104106619723ff
5 files changed
+334
-178
tests/compile/piecewise/test_full_cudagraph.py
+1
@@ -147,6 +147,7 @@ def test_lower_max_num_seqs(model, supported):
147
llm.generate(["Hello, my name is"] * 10)
148
149
150
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="Skip if not cuda")
151
def test_full_cudagraph_with_invalid_backend():
152
with temporary_environ({
153
"VLLM_USE_V1": "1",
vllm/platforms/rocm.py
+3
-2
@@ -141,7 +141,8 @@ def use_rocm_custom_paged_attention(
141
and (head_size == 64 or head_size == 128)
142
and (block_size == 16 or block_size == 32)
143
and (gqa_ratio >= 1 and gqa_ratio <= 16)
144
- and max_seq_len <= 32768 and (envs.VLLM_ROCM_CUSTOM_PAGED_ATTN)
144
+ and max_seq_len <= 128 * 1024
145
+ and (envs.VLLM_ROCM_CUSTOM_PAGED_ATTN)
146
and not (envs.VLLM_ROCM_USE_AITER_PAGED_ATTN
147
and envs.VLLM_ROCM_USE_AITER))
148
@@ -151,7 +152,7 @@ def use_rocm_custom_paged_attention(
152
and (qtype == torch.half or qtype == torch.bfloat16)
153
and head_size == 128 and block_size == 16
154
and (gqa_ratio >= 3 and gqa_ratio <= 16)
154
- and max_seq_len <= 32768 and alibi_slopes is None
155
+ and max_seq_len <= 128 * 1024 and alibi_slopes is None
156
and kv_cache_dtype == "auto"
157
and envs.VLLM_ROCM_CUSTOM_PAGED_ATTN)
158
vllm/v1/attention/backends/flash_attn.py
+3
-169
@@ -19,9 +19,9 @@ from vllm.config import VllmConfig, get_layers_from_vllm_config
19
from vllm.logger import init_logger
20
from vllm.platforms import current_platform
21
from vllm.utils import cdiv
22
-from vllm.v1.attention.backends.utils import (AttentionMetadataBuilder,
23
- CommonAttentionMetadata,
24
- get_kv_cache_layout)
22
+from vllm.v1.attention.backends.utils import (
23
+ AttentionMetadataBuilder, CommonAttentionMetadata, get_kv_cache_layout,
24
+ make_local_attention_virtual_batches)
25
from vllm.v1.kv_cache_interface import AttentionSpec
26
from vllm.v1.worker.block_table import BlockTable
27
@@ -126,172 +126,6 @@ class FlashAttentionMetadata:
126
local_attn_metadata: Optional[LocalAttentionMetadata] = None
127
128
129
-#
130
-# Take in `query_start_loc_np` and `seq_lens_np` and break the sequences into
131
-# local attention blocks, where each block is passed to the attention kernel
132
-# as an independent local ("virtual") batch item.
133
-#
134
-# For example, if are performing a chunked prefill a batch of 3 sequences:
135
-# q_seqlens = [4, 10, 5]
136
-# kv_seqlens = [6, 17, 9]
137
-# Then normally for regular attention we would compute with an attention mask
138
-# for batch idx 0 (q_seqlens = 4, kv_seqlens = 6) like:
139
-# batch idx: 0 (q_seqlens = 4, kv_seqlens = 6)
140
-# k_toks > 0 1 2 3 4 5
141
-# q_toks v _____________
142
-# 0 | 1 1 1
143
-# 1 | 1 1 1 1
144
-# 2 | 1 1 1 1 1
145
-# 3 | 1 1 1 1 1 1
146
-#
147
-# for local attention (with attn_chunk_size = 4) we would compute with an
148
-# attention mask like:
149
-# batch idx: 0 (q_seqlens = 4, kv_seqlens = 6, attn_chunk_size = 4)
150
-# k_toks > 0 1 2 3 4 5
151
-# q_toks v _____________
152
-# 0 | 1 1 1
153
-# 1 | 1 1 1 1
154
-# 2 | 1
155
-# 3 | 1 1
156
-#
157
-# We can simulate this mask using standard flash-attention by breaking the
158
-# sequences into local ("virtual") batches, where each local batch item is a
159
-# local attention block, so in this case batch idx 0 would be broken up into:
160
-#
161
-# local-batch idx: 0 (q_seqlens = 2, kv_seqlens = 4) (batch 0)
162
-# k_toks > 0 1 2 3
163
-# q_toks v _____________
164
-# 0 | 1 1 1
165
-# 1 | 1 1 1 1
166
-# local-batch idx: 1 (q_seqlens = 2, kv_seqlens = 2) (batch 0)
167
-# k_toks > 4 5
168
-# q_toks v _____________
169
-# 2 | 1
170
-# 3 | 1 1
171
-#
172
-# e.g. if we have:
173
-# attn_chunk_size = 4
174
-# query_start_loc_np = [0, 4, 14, 19] (q_seqlens = [4, 10, 5])
175
-# Then this function would return:
176
-# __b0__ ______b1______ __b2__ < orig batch indices
177
-# q_seqlens_local = [ 2, 2, 1, 4, 4, 1, 4, 1]
178
-# cu_seqlens_q_local = [0, 4, 6, 10, 14, 18, 19, 23, 24]
179
-# seqlens_k_local = [ 4, 2, 4, 4, 4, 1, 4, 1]
180
-# block_table_local : shape[local_virtual_batches, pages_per_local_batch]
181
-def make_local_attention_virtual_batches(
182
- attn_chunk_size: int,
183
- query_start_loc_np: np.ndarray,
184
- seq_lens_np: np.ndarray,
185
- block_table: torch.Tensor,
186
- block_size: int = 0,
187
-) -> tuple[np.ndarray, np.ndarray, np.ndarray, torch.Tensor]:
188
- q_seqlens = query_start_loc_np[1:] - query_start_loc_np[:-1]
189
- actual_batch_size = seq_lens_np.shape[0]
190
-
191
- # Handle if we are starting in the middle of a local attention block,
192
- # we assume q_seqlens > 0 (for all elements), for each batch idx we compute
193
- # the number of tokens that are not in the first local attention block and
194
- # then we can simply use a cdiv for the rest.
195
- # For example if we have:
196
- # attn_chunk_size = 4
197
- # q_seqlens = [4, 10, 5]
198
- # k_seqlens = [6, 17, 9]
199
- # Then we would get:
200
- # new_tokens_in_first_block = [2, 1, 4]
201
- # local_blocks = [2, 4, 2]
202
- q_tokens_in_first_block = np.minimum(
203
- attn_chunk_size - ((seq_lens_np - q_seqlens) % attn_chunk_size),
204
- q_seqlens).astype(np.int32)
205
- tokens_in_last_block = attn_chunk_size + (seq_lens_np % -attn_chunk_size)
206
- local_blocks = 1 + cdiv(q_seqlens - q_tokens_in_first_block,
207
- attn_chunk_size)
208
-
209
- # Once we know the number of local blocks we can compute the request spans
210
- # for each batch idx, we can figure out the number of "virtual" requests we
211
- # have to make,
212
- # For the above example we would get:
213
- # seqlens_q_local = [2, 2, 1, 4, 4, 1, 4, 1]
214
- #
215
- # First Get batched arange. (E.g., [2, 4, 2] -> [0, 1, 0, 1, 2, 3, 0, 1])
216
- # (TODO: max a utility to share this code with _prepare_inputs)
217
- # arange step 1. [2, 4, 2] -> [2, 6, 8]
218
- cu_num_blocks = np.cumsum(local_blocks)
219
- virtual_batches = cu_num_blocks[-1]
220
- # arange step 2. [2, 6, 8] -> [0, 0, 2, 2, 2, 2, 6, 6]
221
- block_offsets = np.repeat(cu_num_blocks - local_blocks, local_blocks)
222
- # arange step 3. [0, 1, 0, 1, 2, 3, 0, 1]
223
- arange = np.arange(virtual_batches, dtype=np.int32) - block_offsets
224
- # also compute reverse arange (i.e. [1, 0, 3, 2, 1, 0, 1, 0])
225
- rarange = np.repeat(local_blocks, local_blocks) - arange - 1
226
- # Then we can compute the seqlens_q_local, handling the fact that the
227
- # first and last blocks could be partial
228
- seqlens_q_local = \
229
- np.repeat(q_seqlens - q_tokens_in_first_block, local_blocks)
230
- # set the first block since this may be a partial block
231
- seqlens_q_local[arange == 0] = q_tokens_in_first_block
232
- # set the remaining blocks
233
- seqlens_q_local[arange > 0] = np.minimum(
234
- seqlens_q_local - attn_chunk_size * (arange - 1),
235
- attn_chunk_size)[arange > 0]
236
-
237
- # convert from q_seqlens to cu_seqlens_q
238
- cu_seqlens_q_local = np.pad(np.cumsum(seqlens_q_local), (1, 0))\
239
- .astype(np.int32)
240
-
241
- # compute the seqlens_k_local,
242
- # basically a full local attention block for all but the last block in each
243
- # batch
244
- # For our example this will be:
245
- # seqlens_k_local = [4, 2, 4, 4, 4, 1, 4, 1]
246
- seqlens_k_local = np.full(cu_num_blocks[-1],
247
- attn_chunk_size,
248
- dtype=np.int32)
249
- seqlens_k_local[cu_num_blocks - 1] = tokens_in_last_block
250
-
251
- k_seqstarts_absolute = np.repeat(seq_lens_np, local_blocks) - \
252
- (rarange * attn_chunk_size + \
253
- np.repeat(tokens_in_last_block, local_blocks))
254
- # For the example the local attention blocks start at:
255
- # _b0_ _____b1_____ _b2_
256
- # k_seqstarts_absolute = [0, 4, 4, 8, 12, 16, 4, 8]
257
- block_starts = k_seqstarts_absolute // block_size
258
- assert attn_chunk_size % block_size == 0, \
259
- f"attn_chunk_size {attn_chunk_size} is not " \
260
- f"divisible by block_size {block_size}"
261
- pages_per_local_batch = attn_chunk_size // block_size
262
-
263
- # Create a block_table for the local attention blocks
264
- # For out example if we have a block-table like (assuming block_size=2):
265
- # block_table = [
266
- # [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], < batch 0
267
- # [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], < batch 1
268
- # [20, 21, 22, 23, 24, 25, 26, 27, 28, 29], < batch 2
269
- # ]
270
- # Then for the local batches we would want a block-table like
271
- # block_table_local = [
272
- # [ 0, 1 ], < local-batch 0, (batch 0, starting from k[0])
273
- # [ 2, 3 ], < local-batch 1, (batch 0, starting from k[4])
274
- # [ 12, 13 ], < local-batch 2, (batch 1, starting from k[4])
275
- # [ 14, 15 ], < local-batch 3, (batch 1, starting from k[8])
276
- # [ 16, 17 ], < local-batch 4, (batch 1, starting from k[12])
277
- # [ 18, 19 ], < local-batch 5, (batch 1, starting from k[16])
278
- # [ 22, 23 ], < local-batch 6, (batch 2, starting from k[4])
279
- # [ 24, 25 ], < local-batch 7, (batch 2, starting from k[8])
280
- # ]
281
- block_indices= np.broadcast_to(
282
- np.arange(pages_per_local_batch, dtype=np.int32),
283
- (virtual_batches, pages_per_local_batch)) \
284
- + np.expand_dims(block_starts, axis=1)
285
- block_indices = block_indices.flatten().clip(max=block_table.shape[1] - 1)
286
- batch_indices = np.repeat(np.arange(actual_batch_size, dtype=np.int32),
287
- local_blocks * pages_per_local_batch)
288
- block_table_local = block_table[batch_indices, block_indices]\
289
- .view(virtual_batches, -1)
290
-
291
- return seqlens_q_local, cu_seqlens_q_local, seqlens_k_local, \
292
- block_table_local
293
-
294
-
129
def _get_sliding_window_configs(
130
vllm_config: VllmConfig) -> set[Optional[tuple[int, int]]]:
131
"""Get the set of all sliding window configs used in the model."""
vllm/v1/attention/backends/triton_attn.py
+159
-7
@@ -1,7 +1,8 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""Attention layer with PagedAttention and Triton prefix prefill."""
4
-from typing import TYPE_CHECKING, Any, Optional
4
+from dataclasses import dataclass
5
+from typing import TYPE_CHECKING, Any, ClassVar, Optional
6
7
import torch
8
@@ -15,8 +16,10 @@ from vllm.attention.ops.paged_attn import PagedAttention
16
from vllm.attention.ops.triton_unified_attention import unified_attention
17
from vllm.logger import init_logger
18
from vllm.platforms import current_platform
18
-from vllm.v1.attention.backends.flash_attn import (
19
- FlashAttentionMetadata, FlashAttentionMetadataBuilder)
19
+from vllm.v1.attention.backends.flash_attn import FlashAttentionMetadata
20
+from vllm.v1.attention.backends.utils import (
21
+ AttentionMetadataBuilder, CommonAttentionMetadata,
22
+ make_local_attention_virtual_batches)
23
from vllm.v1.kv_cache_interface import AttentionSpec
24
from vllm.v1.worker.block_table import BlockTable
25
@@ -26,12 +29,161 @@ if TYPE_CHECKING:
29
logger = init_logger(__name__)
30
31
29
-class TritonAttentionMetadataBuilder(FlashAttentionMetadataBuilder):
32
+@dataclass
33
+class TritonAttentionMetadata:
34
+ # NOTE(sang): Definition of context_len, query_len, and seq_len.
35
+ # |---------- N-1 iteration --------|
36
+ # |---------------- N iteration ---------------------|
37
+ # |- tokenA -|......................|-- newTokens ---|
38
+ # |---------- context_len ----------|
39
+ # |-------------------- seq_len ---------------------|
40
+ # |-- query_len ---|
41
+
42
+ num_actual_tokens: int # Number of tokens excluding padding.
43
+ max_query_len: int
44
+ query_start_loc: torch.Tensor
45
+ max_seq_len: int
46
+ seq_lens: torch.Tensor
47
+ block_table: torch.Tensor
48
+ slot_mapping: torch.Tensor
49
+
50
+ # For cascade attention.
51
+ use_cascade: bool
52
+ common_prefix_len: int
53
+ cu_prefix_query_lens: Optional[torch.Tensor]
54
+ prefix_kv_lens: Optional[torch.Tensor]
55
+ suffix_kv_lens: Optional[torch.Tensor]
56
+
57
+ # Optional aot scheduling
58
+ scheduler_metadata: Optional[torch.Tensor] = None
59
+ prefix_scheduler_metadata: Optional[torch.Tensor] = None
60
+
61
+ # for local attention
62
+ @dataclass
63
+ class LocalAttentionMetadata:
64
+ local_query_start_loc: torch.Tensor
65
+ local_seqused_k: torch.Tensor
66
+ local_block_table: torch.Tensor
67
+ local_max_query_len: int
68
+ local_max_seq_len: int
69
+ local_scheduler_metadata: Optional[torch.Tensor]
70
+
71
+ local_attn_metadata: Optional[LocalAttentionMetadata] = None
72
+
73
+
74
+class TritonAttentionMetadataBuilder(
75
+ AttentionMetadataBuilder[TritonAttentionMetadata]):
76
+ full_cudagraph_supported: ClassVar[bool] = True
77
78
def __init__(self, runner: "GPUModelRunner", kv_cache_spec: AttentionSpec,
79
block_table: BlockTable):
33
- super().__init__(runner, kv_cache_spec, block_table)
34
- self.aot_schedule = False
80
+ self.runner = runner
81
+ self.block_size = kv_cache_spec.block_size
82
+ self.kv_cache_spec = kv_cache_spec
83
+ self.block_table = block_table
84
+
85
+ def build_for_cudagraph_capture(
86
+ self, common_attn_metadata: CommonAttentionMetadata
87
+ ) -> TritonAttentionMetadata:
88
+ attn_metadata = self.build(0, common_attn_metadata)
89
+ # When doing full graph capture, setting seq_lens to
90
+ # max_model_len will cause graph capture to be extremely
91
+ # slow, so here we set it to 1.
92
+ attn_metadata.seq_lens.fill_(1)
93
+ return attn_metadata
94
+
95
+ def build(
96
+ self, common_prefix_len: int,
97
+ common_attn_metadata: CommonAttentionMetadata
98
+ ) -> TritonAttentionMetadata:
99
+ num_reqs = common_attn_metadata.num_reqs
100
+ num_actual_tokens = common_attn_metadata.num_actual_tokens
101
+ max_query_len = common_attn_metadata.max_query_len
102
+
103
+ max_seq_len = int(self.runner.seq_lens_np[:num_reqs].max())
104
+ query_start_loc = common_attn_metadata.query_start_loc
105
+ seq_lens = common_attn_metadata.seq_lens
106
+ block_table = self.block_table
107
+ block_table_tensor = block_table.get_device_tensor()[:num_reqs]
108
+
109
+ block_table.slot_mapping[:num_actual_tokens].copy_(
110
+ block_table.slot_mapping_cpu[:num_actual_tokens],
111
+ non_blocking=True)
112
+ # Fill unused with -1. Needed for reshape_and_cache in full cuda graph
113
+ # mode.
114
+ block_table.slot_mapping[num_actual_tokens:].fill_(-1)
115
+
116
+ slot_mapping = block_table.slot_mapping[:num_actual_tokens]
117
+
118
+ # for local attention
119
+ local_attn_metadata = None
120
+ if self.runner.attention_chunk_size is not None:
121
+ seqlens_q_local_np, virt_q_cu_seqlens_np, virt_k_seqlens_np, \
122
+ virt_block_table_tensor = make_local_attention_virtual_batches(
123
+ self.runner.attention_chunk_size,
124
+ self.runner.query_start_loc_np[:num_reqs + 1],
125
+ self.runner.seq_lens_np[:num_reqs],
126
+ block_table_tensor,
127
+ self.block_size,
128
+ )
129
+ local_query_start_loc = torch.from_numpy(virt_q_cu_seqlens_np).to(
130
+ self.runner.device, non_blocking=True)
131
+ local_seqused_k = torch.from_numpy(virt_k_seqlens_np).to(
132
+ self.runner.device, non_blocking=True)
133
+ local_max_query_len = seqlens_q_local_np.max()
134
+ local_max_seq_len = virt_k_seqlens_np.max()
135
+
136
+ local_attn_metadata = TritonAttentionMetadata \
137
+ .LocalAttentionMetadata(
138
+ local_query_start_loc=local_query_start_loc,
139
+ local_seqused_k=local_seqused_k,
140
+ local_block_table=virt_block_table_tensor,
141
+ local_max_query_len=local_max_query_len,
142
+ local_max_seq_len=local_max_seq_len,
143
+ local_scheduler_metadata=None,
144
+ )
145
+
146
+ use_cascade = common_prefix_len > 0
147
+
148
+ if use_cascade:
149
+ cu_prefix_query_lens = torch.tensor([0, num_actual_tokens],
150
+ dtype=torch.int32,
151
+ device=self.runner.device)
152
+ prefix_kv_lens = torch.tensor([common_prefix_len],
153
+ dtype=torch.int32,
154
+ device=self.runner.device)
155
+ suffix_kv_lens = (self.runner.seq_lens_np[:num_reqs] -
156
+ common_prefix_len)
157
+ suffix_kv_lens = torch.from_numpy(suffix_kv_lens).to(
158
+ self.runner.device)
159
+ else:
160
+ cu_prefix_query_lens = None
161
+ prefix_kv_lens = None
162
+ suffix_kv_lens = None
163
+ prefix_scheduler_metadata = None
164
+
165
+ attn_metadata = TritonAttentionMetadata(
166
+ num_actual_tokens=num_actual_tokens,
167
+ max_query_len=max_query_len,
168
+ query_start_loc=query_start_loc,
169
+ max_seq_len=max_seq_len,
170
+ seq_lens=seq_lens,
171
+ block_table=block_table_tensor,
172
+ slot_mapping=slot_mapping,
173
+ use_cascade=use_cascade,
174
+ common_prefix_len=common_prefix_len,
175
+ cu_prefix_query_lens=cu_prefix_query_lens,
176
+ prefix_kv_lens=prefix_kv_lens,
177
+ suffix_kv_lens=suffix_kv_lens,
178
+ local_attn_metadata=local_attn_metadata,
179
+ prefix_scheduler_metadata=prefix_scheduler_metadata,
180
+ )
181
+ return attn_metadata
182
+
183
+ def can_run_in_cudagraph(
184
+ self, common_attn_metadata: CommonAttentionMetadata) -> bool:
185
+ # Full CUDA Graph always supported
186
+ return True
187
188
189
class TritonAttentionBackend(AttentionBackend):
@@ -52,7 +204,7 @@ class TritonAttentionBackend(AttentionBackend):
204
205
@staticmethod
206
def get_metadata_cls() -> type["AttentionMetadata"]:
55
- return FlashAttentionMetadata
207
+ return TritonAttentionMetadata
208
209
@staticmethod
210
def get_kv_cache_shape(
vllm/v1/attention/backends/utils.py
+168
@@ -9,6 +9,8 @@ from typing import TYPE_CHECKING, ClassVar, Generic, TypeVar
9
import numpy as np
10
import torch
11
12
+from vllm.utils import cdiv
13
+
14
if TYPE_CHECKING:
15
from vllm.v1.core.sched.output import SchedulerOutput
16
from vllm.v1.worker.gpu_input_batch import InputBatch
@@ -140,3 +142,169 @@ def get_kv_cache_layout():
142
"detected. Setting KV cache layout to %s.", cache_layout)
143
144
return cache_layout
145
+
146
+
147
+#
148
+# Take in `query_start_loc_np` and `seq_lens_np` and break the sequences into
149
+# local attention blocks, where each block is passed to the attention kernel
150
+# as an independent local ("virtual") batch item.
151
+#
152
+# For example, if are performing a chunked prefill a batch of 3 sequences:
153
+# q_seqlens = [4, 10, 5]
154
+# kv_seqlens = [6, 17, 9]
155
+# Then normally for regular attention we would compute with an attention mask
156
+# for batch idx 0 (q_seqlens = 4, kv_seqlens = 6) like:
157
+# batch idx: 0 (q_seqlens = 4, kv_seqlens = 6)
158
+# k_toks > 0 1 2 3 4 5
159
+# q_toks v _____________
160
+# 0 | 1 1 1
161
+# 1 | 1 1 1 1
162
+# 2 | 1 1 1 1 1
163
+# 3 | 1 1 1 1 1 1
164
+#
165
+# for local attention (with attn_chunk_size = 4) we would compute with an
166
+# attention mask like:
167
+# batch idx: 0 (q_seqlens = 4, kv_seqlens = 6, attn_chunk_size = 4)
168
+# k_toks > 0 1 2 3 4 5
169
+# q_toks v _____________
170
+# 0 | 1 1 1
171
+# 1 | 1 1 1 1
172
+# 2 | 1
173
+# 3 | 1 1
174
+#
175
+# We can simulate this mask using standard flash-attention by breaking the
176
+# sequences into local ("virtual") batches, where each local batch item is a
177
+# local attention block, so in this case batch idx 0 would be broken up into:
178
+#
179
+# local-batch idx: 0 (q_seqlens = 2, kv_seqlens = 4) (batch 0)
180
+# k_toks > 0 1 2 3
181
+# q_toks v _____________
182
+# 0 | 1 1 1
183
+# 1 | 1 1 1 1
184
+# local-batch idx: 1 (q_seqlens = 2, kv_seqlens = 2) (batch 0)
185
+# k_toks > 4 5
186
+# q_toks v _____________
187
+# 2 | 1
188
+# 3 | 1 1
189
+#
190
+# e.g. if we have:
191
+# attn_chunk_size = 4
192
+# query_start_loc_np = [0, 4, 14, 19] (q_seqlens = [4, 10, 5])
193
+# Then this function would return:
194
+# __b0__ ______b1______ __b2__ < orig batch indices
195
+# q_seqlens_local = [ 2, 2, 1, 4, 4, 1, 4, 1]
196
+# cu_seqlens_q_local = [0, 4, 6, 10, 14, 18, 19, 23, 24]
197
+# seqlens_k_local = [ 4, 2, 4, 4, 4, 1, 4, 1]
198
+# block_table_local : shape[local_virtual_batches, pages_per_local_batch]
199
+def make_local_attention_virtual_batches(
200
+ attn_chunk_size: int,
201
+ query_start_loc_np: np.ndarray,
202
+ seq_lens_np: np.ndarray,
203
+ block_table: torch.Tensor,
204
+ block_size: int = 0,
205
+) -> tuple[np.ndarray, np.ndarray, np.ndarray, torch.Tensor]:
206
+ q_seqlens = query_start_loc_np[1:] - query_start_loc_np[:-1]
207
+ actual_batch_size = seq_lens_np.shape[0]
208
+
209
+ # Handle if we are starting in the middle of a local attention block,
210
+ # we assume q_seqlens > 0 (for all elements), for each batch idx we compute
211
+ # the number of tokens that are not in the first local attention block and
212
+ # then we can simply use a cdiv for the rest.
213
+ # For example if we have:
214
+ # attn_chunk_size = 4
215
+ # q_seqlens = [4, 10, 5]
216
+ # k_seqlens = [6, 17, 9]
217
+ # Then we would get:
218
+ # new_tokens_in_first_block = [2, 1, 4]
219
+ # local_blocks = [2, 4, 2]
220
+ q_tokens_in_first_block = np.minimum(
221
+ attn_chunk_size - ((seq_lens_np - q_seqlens) % attn_chunk_size),
222
+ q_seqlens).astype(np.int32)
223
+ tokens_in_last_block = attn_chunk_size + (seq_lens_np % -attn_chunk_size)
224
+ local_blocks = 1 + cdiv(q_seqlens - q_tokens_in_first_block,
225
+ attn_chunk_size)
226
+
227
+ # Once we know the number of local blocks we can compute the request spans
228
+ # for each batch idx, we can figure out the number of "virtual" requests we
229
+ # have to make,
230
+ # For the above example we would get:
231
+ # seqlens_q_local = [2, 2, 1, 4, 4, 1, 4, 1]
232
+ #
233
+ # First Get batched arange. (E.g., [2, 4, 2] -> [0, 1, 0, 1, 2, 3, 0, 1])
234
+ # (TODO: max a utility to share this code with _prepare_inputs)
235
+ # arange step 1. [2, 4, 2] -> [2, 6, 8]
236
+ cu_num_blocks = np.cumsum(local_blocks)
237
+ virtual_batches = cu_num_blocks[-1]
238
+ # arange step 2. [2, 6, 8] -> [0, 0, 2, 2, 2, 2, 6, 6]
239
+ block_offsets = np.repeat(cu_num_blocks - local_blocks, local_blocks)
240
+ # arange step 3. [0, 1, 0, 1, 2, 3, 0, 1]
241
+ arange = np.arange(virtual_batches, dtype=np.int32) - block_offsets
242
+ # also compute reverse arange (i.e. [1, 0, 3, 2, 1, 0, 1, 0])
243
+ rarange = np.repeat(local_blocks, local_blocks) - arange - 1
244
+ # Then we can compute the seqlens_q_local, handling the fact that the
245
+ # first and last blocks could be partial
246
+ seqlens_q_local = \
247
+ np.repeat(q_seqlens - q_tokens_in_first_block, local_blocks)
248
+ # set the first block since this may be a partial block
249
+ seqlens_q_local[arange == 0] = q_tokens_in_first_block
250
+ # set the remaining blocks
251
+ seqlens_q_local[arange > 0] = np.minimum(
252
+ seqlens_q_local - attn_chunk_size * (arange - 1),
253
+ attn_chunk_size)[arange > 0]
254
+
255
+ # convert from q_seqlens to cu_seqlens_q
256
+ cu_seqlens_q_local = np.pad(np.cumsum(seqlens_q_local), (1, 0))\
257
+ .astype(np.int32)
258
+
259
+ # compute the seqlens_k_local,
260
+ # basically a full local attention block for all but the last block in each
261
+ # batch
262
+ # For our example this will be:
263
+ # seqlens_k_local = [4, 2, 4, 4, 4, 1, 4, 1]
264
+ seqlens_k_local = np.full(cu_num_blocks[-1],
265
+ attn_chunk_size,
266
+ dtype=np.int32)
267
+ seqlens_k_local[cu_num_blocks - 1] = tokens_in_last_block
268
+
269
+ k_seqstarts_absolute = np.repeat(seq_lens_np, local_blocks) - \
270
+ (rarange * attn_chunk_size + \
271
+ np.repeat(tokens_in_last_block, local_blocks))
272
+ # For the example the local attention blocks start at:
273
+ # _b0_ _____b1_____ _b2_
274
+ # k_seqstarts_absolute = [0, 4, 4, 8, 12, 16, 4, 8]
275
+ block_starts = k_seqstarts_absolute // block_size
276
+ assert attn_chunk_size % block_size == 0, \
277
+ f"attn_chunk_size {attn_chunk_size} is not " \
278
+ f"divisible by block_size {block_size}"
279
+ pages_per_local_batch = attn_chunk_size // block_size
280
+
281
+ # Create a block_table for the local attention blocks
282
+ # For out example if we have a block-table like (assuming block_size=2):
283
+ # block_table = [
284
+ # [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], < batch 0
285
+ # [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], < batch 1
286
+ # [20, 21, 22, 23, 24, 25, 26, 27, 28, 29], < batch 2
287
+ # ]
288
+ # Then for the local batches we would want a block-table like
289
+ # block_table_local = [
290
+ # [ 0, 1 ], < local-batch 0, (batch 0, starting from k[0])
291
+ # [ 2, 3 ], < local-batch 1, (batch 0, starting from k[4])
292
+ # [ 12, 13 ], < local-batch 2, (batch 1, starting from k[4])
293
+ # [ 14, 15 ], < local-batch 3, (batch 1, starting from k[8])
294
+ # [ 16, 17 ], < local-batch 4, (batch 1, starting from k[12])
295
+ # [ 18, 19 ], < local-batch 5, (batch 1, starting from k[16])
296
+ # [ 22, 23 ], < local-batch 6, (batch 2, starting from k[4])
297
+ # [ 24, 25 ], < local-batch 7, (batch 2, starting from k[8])
298
+ # ]
299
+ block_indices= np.broadcast_to(
300
+ np.arange(pages_per_local_batch, dtype=np.int32),
301
+ (virtual_batches, pages_per_local_batch)) \
302
+ + np.expand_dims(block_starts, axis=1)
303
+ block_indices = block_indices.flatten().clip(max=block_table.shape[1] - 1)
304
+ batch_indices = np.repeat(np.arange(actual_batch_size, dtype=np.int32),
305
+ local_blocks * pages_per_local_batch)
306
+ block_table_local = block_table[batch_indices, block_indices]\
307
+ .view(virtual_batches, -1)
308
+
309
+ return seqlens_q_local, cu_seqlens_q_local, seqlens_k_local, \
310
+ block_table_local