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 7, 11:30 UTC
[Mamba1] - Kernel Level Chunk Alignment for Prefix Caching (#34798)
Signed-off-by: Josephasafg <ajgard7@gmail.com>
Asaf Gardin committed
Mar 1, 2026 at 14:40 UTC
bbf81f9a9284d572b69db2c4fb002c2a8a80d507
11 files changed
+251
-146
csrc/mamba/mamba_ssm/selective_scan.h
+3
-1
@@ -17,7 +17,7 @@
17
struct SSMParamsBase {
18
using index_t = size_t;
19
20
- int batch, dim, seqlen, dstate, n_groups, n_chunks;
20
+ int batch, dim, seqlen, dstate, n_groups;
21
int dim_ngroups_ratio;
22
bool is_variable_B;
23
bool is_variable_C;
@@ -72,6 +72,8 @@ struct SSMParamsBase {
72
void *__restrict__ block_idx_first_scheduled_token_ptr; // (batch,) - first block to write
73
void *__restrict__ block_idx_last_scheduled_token_ptr; // (batch,) - last block to write
74
void *__restrict__ initial_state_idx_ptr; // (batch,) - index of the initial state to use
75
+ void *__restrict__ cu_chunk_seqlen_ptr; // (nchunks+1,) - cumulative chunk token offsets
76
+ void *__restrict__ last_chunk_indices_ptr; // (batch,) - index of last chunk per sequence
77
};
78
79
csrc/mamba/mamba_ssm/selective_scan_fwd.cu
+68
-35
@@ -81,7 +81,6 @@ void selective_scan_fwd_kernel(SSMParamsBase params) {
81
constexpr bool kIsVariableC = Ktraits::kIsVariableC;
82
constexpr bool kHasZ = Ktraits::kHasZ;
83
constexpr bool kVarlen = Ktraits::kVarlen;
84
- constexpr int kNThreads = Ktraits::kNThreads;
84
constexpr int kNItems = Ktraits::kNItems;
85
constexpr int kNRows = Ktraits::kNRows;
86
constexpr bool kDirectIO = Ktraits::kDirectIO;
@@ -161,17 +160,8 @@ void selective_scan_fwd_kernel(SSMParamsBase params) {
160
}
161
}
162
164
-
165
- // for (int state_idx = threadIdx.x; state_idx < params.dstate; state_idx += blockDim.x) {
166
- // smem_a[state_idx] = A[state_idx * params.A_dstate_stride];
167
- // smem_bc[state_idx] = B[state_idx * params.B_dstate_stride] * C[state_idx * params.C_dstate_stride];
168
- // }
169
-
170
- constexpr int kChunkSize = kNThreads * kNItems;
171
-
163
// Use block_size for chunking when APC is enabled, otherwise use 2048 for backwards compatibility
173
- const int iteration_chunk_size = params.cache_enabled ? params.block_size : 2048;
174
- const int n_chunks = (seqlen + iteration_chunk_size - 1) / iteration_chunk_size;
164
+ const int block_size = params.cache_enabled ? params.block_size : 2048;
165
166
const int* batch_cache_indices = cache_indices != nullptr ?
167
cache_indices + batch_id * params.cache_indices_stride : nullptr;
@@ -181,10 +171,44 @@ void selective_scan_fwd_kernel(SSMParamsBase params) {
171
reinterpret_cast<const int*>(params.block_idx_last_scheduled_token_ptr) : nullptr;
172
const int* initial_state_idx = params.initial_state_idx_ptr != nullptr ?
173
reinterpret_cast<const int*>(params.initial_state_idx_ptr) : nullptr;
174
+ const int* cu_chunk_seqlen = params.cu_chunk_seqlen_ptr != nullptr ?
175
+ reinterpret_cast<const int*>(params.cu_chunk_seqlen_ptr) : nullptr;
176
+ const int* last_chunk_indices = params.last_chunk_indices_ptr != nullptr ?
177
+ reinterpret_cast<const int*>(params.last_chunk_indices_ptr) : nullptr;
178
179
const size_t load_cache_slot = params.cache_enabled && batch_cache_indices != nullptr ? batch_cache_indices[initial_state_idx[batch_id]] : cache_index;
180
181
+ const int block_idx_first = (params.cache_enabled && block_idx_first_scheduled != nullptr) ?
182
+ block_idx_first_scheduled[batch_id] : 0;
183
+
184
+ // Determine chunk boundaries from pre-computed metadata (APC mode)
185
+ // or fall back to simple block_size chunking.
186
+ int first_chunk_idx, n_chunks;
187
+ int current_position;
188
+
189
+ if (cu_chunk_seqlen != nullptr && last_chunk_indices != nullptr) {
190
+ const int last_chunk_idx = last_chunk_indices[batch_id];
191
+ first_chunk_idx = (batch_id == 0) ? 0 : last_chunk_indices[batch_id - 1] + 1;
192
+ n_chunks = last_chunk_idx - first_chunk_idx + 1;
193
+ // Derive current_position: if the first chunk is partial (fills remainder
194
+ // of a started block), offset into the block accordingly.
195
+ const int first_chunk_tokens = cu_chunk_seqlen[first_chunk_idx + 1] - cu_chunk_seqlen[first_chunk_idx];
196
+ const int chunk_start_offset = (n_chunks > 1 && first_chunk_tokens < block_size)
197
+ ? (block_size - first_chunk_tokens) : 0;
198
+ current_position = block_idx_first * block_size + chunk_start_offset;
199
+ } else {
200
+ first_chunk_idx = 0;
201
+ n_chunks = (seqlen + block_size - 1) / block_size;
202
+ current_position = 0;
203
+ }
204
+
205
+ int tokens_processed = 0;
206
+
207
for (int chunk = 0; chunk < n_chunks; ++chunk) {
208
+ const int chunk_tokens = (cu_chunk_seqlen != nullptr)
209
+ ? cu_chunk_seqlen[first_chunk_idx + chunk + 1] - cu_chunk_seqlen[first_chunk_idx + chunk]
210
+ : min(block_size, seqlen - tokens_processed);
211
+ if (chunk_tokens <= 0) break;
212
input_t u_vals[kNRows][kNItems], delta_vals_load[kNRows][kNItems];
213
214
__syncthreads();
@@ -193,12 +217,12 @@ void selective_scan_fwd_kernel(SSMParamsBase params) {
217
if constexpr (!kDirectIO) {
218
if (r > 0) { __syncthreads(); }
219
}
196
- load_input<Ktraits>(u + r * params.u_d_stride, u_vals[r], smem_load, seqlen - chunk * kChunkSize);
220
+ load_input<Ktraits>(u + r * params.u_d_stride, u_vals[r], smem_load, chunk_tokens);
221
if constexpr (!kDirectIO) { __syncthreads(); }
198
- load_input<Ktraits>(delta + r * params.delta_d_stride, delta_vals_load[r], smem_load, seqlen - chunk * kChunkSize);
222
+ load_input<Ktraits>(delta + r * params.delta_d_stride, delta_vals_load[r], smem_load, chunk_tokens);
223
}
200
- u += kChunkSize;
201
- delta += kChunkSize;
224
+ u += chunk_tokens;
225
+ delta += chunk_tokens;
226
227
float delta_vals[kNRows][kNItems], delta_u_vals[kNRows][kNItems], out_vals[kNRows][kNItems];
228
#pragma unroll
@@ -232,7 +256,7 @@ void selective_scan_fwd_kernel(SSMParamsBase params) {
256
weight_t B_vals[kNItems], C_vals[kNItems];
257
if constexpr (kIsVariableB) {
258
load_weight<Ktraits>(Bvar + state_idx * params.B_dstate_stride, B_vals,
235
- smem_load_weight, (seqlen - chunk * kChunkSize) * (1));
259
+ smem_load_weight, chunk_tokens);
260
if constexpr (!kIsVariableC) {
261
#pragma unroll
262
for (int r = 0; r < kNRows; ++r) {
@@ -243,7 +267,7 @@ void selective_scan_fwd_kernel(SSMParamsBase params) {
267
if constexpr (kIsVariableC) {
268
auto &smem_load_weight_C = !kIsVariableB ? smem_load_weight : smem_load_weight1;
269
load_weight<Ktraits>(Cvar + state_idx * params.C_dstate_stride, C_vals,
246
- smem_load_weight_C, (seqlen - chunk * kChunkSize) * (1));
270
+ smem_load_weight_C, chunk_tokens);
271
if constexpr (!kIsVariableB) {
272
#pragma unroll
273
for (int r = 0; r < kNRows; ++r) {
@@ -266,10 +290,8 @@ void selective_scan_fwd_kernel(SSMParamsBase params) {
290
for (int i = 0; i < kNItems; ++i) {
291
thread_data[i] = make_float2(exp2f(delta_vals[r][i] * A_val[r]),
292
!kIsVariableB ? delta_u_vals[r][i] : B_vals[i] * delta_u_vals[r][i]);
269
- if (seqlen % (kNItems * kNThreads) != 0) { // So that the last state is correct
270
- if (threadIdx.x * kNItems + i >= seqlen - chunk * kChunkSize) {
271
- thread_data[i] = make_float2(1.f, 0.f);
272
- }
293
+ if (threadIdx.x * kNItems + i >= chunk_tokens) {
294
+ thread_data[i] = make_float2(1.f, 0.f);
295
}
296
}
297
// Initialize running total
@@ -301,14 +323,14 @@ void selective_scan_fwd_kernel(SSMParamsBase params) {
323
if (threadIdx.x == 0) {
324
smem_running_prefix[state_idx + r * MAX_DSTATE] = prefix_op.running_prefix;
325
304
- // Store state at the end of each chunk when cache is enabled
326
+ // Store state at the end of each aligned chunk when cache is enabled
327
if (params.cache_enabled && batch_cache_indices != nullptr) {
306
-
328
size_t cache_slot;
329
if (chunk == n_chunks - 1) {
330
cache_slot = batch_cache_indices[block_idx_last_scheduled[batch_id]];
331
} else {
311
- cache_slot = batch_cache_indices[block_idx_first_scheduled[batch_id] + chunk];
332
+ const int block_idx_completed = (current_position + chunk_tokens - 1) / block_size;
333
+ cache_slot = batch_cache_indices[block_idx_completed];
334
}
335
336
size_t state_offset = cache_slot * params.ssm_states_batch_stride +
@@ -331,38 +353,41 @@ void selective_scan_fwd_kernel(SSMParamsBase params) {
353
}
354
}
355
input_t *out = reinterpret_cast<input_t *>(params.out_ptr) + sequence_start_index * params.out_batch_stride
334
- + dim_id * kNRows * params.out_d_stride + chunk * kChunkSize;
356
+ + dim_id * kNRows * params.out_d_stride + tokens_processed;
357
__syncthreads();
358
#pragma unroll
359
for (int r = 0; r < kNRows; ++r) {
360
if constexpr (!kDirectIO) {
361
if (r > 0) { __syncthreads(); }
362
}
341
- store_output<Ktraits>(out + r * params.out_d_stride, out_vals[r], smem_store, seqlen - chunk * kChunkSize);
363
+ store_output<Ktraits>(out + r * params.out_d_stride, out_vals[r], smem_store, chunk_tokens);
364
}
365
366
if constexpr (kHasZ) {
367
input_t *z = reinterpret_cast<input_t *>(params.z_ptr) + sequence_start_index * params.z_batch_stride
346
- + dim_id * kNRows * params.z_d_stride + chunk * kChunkSize;
368
+ + dim_id * kNRows * params.z_d_stride + tokens_processed;
369
input_t *out_z = reinterpret_cast<input_t *>(params.out_z_ptr) + sequence_start_index * params.out_z_batch_stride
348
- + dim_id * kNRows * params.out_z_d_stride + chunk * kChunkSize;
370
+ + dim_id * kNRows * params.out_z_d_stride + tokens_processed;
371
#pragma unroll
372
for (int r = 0; r < kNRows; ++r) {
373
input_t z_vals[kNItems];
374
__syncthreads();
353
- load_input<Ktraits>(z + r * params.z_d_stride, z_vals, smem_load, seqlen - chunk * kChunkSize);
375
+ load_input<Ktraits>(z + r * params.z_d_stride, z_vals, smem_load, chunk_tokens);
376
#pragma unroll
377
for (int i = 0; i < kNItems; ++i) {
378
float z_val = z_vals[i];
379
out_vals[r][i] *= z_val / (1 + expf(-z_val));
380
}
381
__syncthreads();
360
- store_output<Ktraits>(out_z + r * params.out_z_d_stride, out_vals[r], smem_store, seqlen - chunk * kChunkSize);
382
+ store_output<Ktraits>(out_z + r * params.out_z_d_stride, out_vals[r], smem_store, chunk_tokens);
383
}
384
}
385
364
- Bvar += kChunkSize * 1;
365
- Cvar += kChunkSize * 1;
386
+ Bvar += chunk_tokens;
387
+ Cvar += chunk_tokens;
388
+
389
+ tokens_processed += chunk_tokens;
390
+ current_position += chunk_tokens;
391
}
392
}
393
@@ -506,7 +531,9 @@ void set_ssm_params_fwd(SSMParamsBase ¶ms,
531
int64_t block_size,
532
const std::optional<torch::Tensor> &block_idx_first_scheduled_token,
533
const std::optional<torch::Tensor> &block_idx_last_scheduled_token,
509
- const std::optional<torch::Tensor> &initial_state_idx) {
534
+ const std::optional<torch::Tensor> &initial_state_idx,
535
+ const std::optional<torch::Tensor> &cu_chunk_seqlen,
536
+ const std::optional<torch::Tensor> &last_chunk_indices) {
537
538
// Reset the parameters
539
memset(¶ms, 0, sizeof(params));
@@ -548,6 +575,8 @@ void set_ssm_params_fwd(SSMParamsBase ¶ms,
575
params.block_idx_first_scheduled_token_ptr = block_idx_first_scheduled_token.has_value() ? block_idx_first_scheduled_token.value().data_ptr() : nullptr;
576
params.block_idx_last_scheduled_token_ptr = block_idx_last_scheduled_token.has_value() ? block_idx_last_scheduled_token.value().data_ptr() : nullptr;
577
params.initial_state_idx_ptr = initial_state_idx.has_value() ? initial_state_idx.value().data_ptr() : nullptr;
578
+ params.cu_chunk_seqlen_ptr = cu_chunk_seqlen.has_value() ? cu_chunk_seqlen.value().data_ptr() : nullptr;
579
+ params.last_chunk_indices_ptr = last_chunk_indices.has_value() ? last_chunk_indices.value().data_ptr() : nullptr;
580
581
// All stride are in elements, not bytes.
582
params.A_d_stride = A.stride(0);
@@ -633,7 +662,9 @@ void selective_scan_fwd(const torch::Tensor &u, const torch::Tensor &delta,
662
int64_t block_size,
663
const std::optional<torch::Tensor> &block_idx_first_scheduled_token,
664
const std::optional<torch::Tensor> &block_idx_last_scheduled_token,
636
- const std::optional<torch::Tensor> &initial_state_idx) {
665
+ const std::optional<torch::Tensor> &initial_state_idx,
666
+ const std::optional<torch::Tensor> &cu_chunk_seqlen,
667
+ const std::optional<torch::Tensor> &last_chunk_indices) {
668
auto input_type = u.scalar_type();
669
auto weight_type = A.scalar_type();
670
TORCH_CHECK(input_type == at::ScalarType::Float || input_type == at::ScalarType::Half || input_type == at::ScalarType::BFloat16);
@@ -778,7 +809,9 @@ void selective_scan_fwd(const torch::Tensor &u, const torch::Tensor &delta,
809
block_size,
810
block_idx_first_scheduled_token,
811
block_idx_last_scheduled_token,
781
- initial_state_idx
812
+ initial_state_idx,
813
+ cu_chunk_seqlen,
814
+ last_chunk_indices
815
);
816
817
csrc/ops.h
+3
-1
@@ -371,7 +371,9 @@ void selective_scan_fwd(
371
const torch::Tensor& ssm_states, int64_t pad_slot_id, int64_t block_size,
372
const std::optional<torch::Tensor>& block_idx_first_scheduled_token,
373
const std::optional<torch::Tensor>& block_idx_last_scheduled_token,
374
- const std::optional<torch::Tensor>& initial_state_idx);
374
+ const std::optional<torch::Tensor>& initial_state_idx,
375
+ const std::optional<torch::Tensor>& cu_chunk_seqlen,
376
+ const std::optional<torch::Tensor>& last_chunk_indices);
377
378
torch::Tensor dynamic_4bit_int_moe_cpu(
379
torch::Tensor x, torch::Tensor topk_ids, torch::Tensor topk_weights,
csrc/torch_bindings.cpp
+3
-1
@@ -640,7 +640,9 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
640
"int block_size,"
641
"Tensor? block_idx_first_scheduled_token,"
642
"Tensor? block_idx_last_scheduled_token,"
643
- "Tensor? initial_state_idx) -> ()");
643
+ "Tensor? initial_state_idx,"
644
+ "Tensor? cu_chunk_seqlen,"
645
+ "Tensor? last_chunk_indices) -> ()");
646
ops.impl("selective_scan_fwd", torch::kCUDA, &selective_scan_fwd);
647
648
// Hadamard transforms
tests/kernels/mamba/test_mamba_ssm.py
+4
@@ -183,6 +183,8 @@ def selective_scan_opcheck_fn(
183
block_idx_first_scheduled_token=None,
184
block_idx_last_scheduled_token=None,
185
initial_state_idx=None,
186
+ cu_chunk_seqlen=None,
187
+ last_chunk_indices=None,
188
):
189
"""if return_last_state is True, returns (out, last_state)
190
last_state has shape (batch, dim, dstate).
@@ -231,6 +233,8 @@ def selective_scan_opcheck_fn(
233
block_idx_first_scheduled_token,
234
block_idx_last_scheduled_token,
235
initial_state_idx,
236
+ cu_chunk_seqlen,
237
+ last_chunk_indices,
238
),
239
test_utils=["test_schema", "test_faketensor"],
240
)
vllm/_custom_ops.py
+4
@@ -2021,6 +2021,8 @@ def selective_scan_fwd(
2021
block_idx_first_scheduled_token: torch.Tensor | None = None,
2022
block_idx_last_scheduled_token: torch.Tensor | None = None,
2023
initial_state_idx: torch.Tensor | None = None,
2024
+ cu_chunk_seqlen: torch.Tensor | None = None,
2025
+ last_chunk_indices: torch.Tensor | None = None,
2026
):
2027
torch.ops._C.selective_scan_fwd(
2028
u,
@@ -2041,6 +2043,8 @@ def selective_scan_fwd(
2043
block_idx_first_scheduled_token,
2044
block_idx_last_scheduled_token,
2045
initial_state_idx,
2046
+ cu_chunk_seqlen,
2047
+ last_chunk_indices,
2048
)
2049
2050
vllm/model_executor/layers/mamba/mamba_mixer.py
+4
@@ -271,6 +271,8 @@ class MambaMixer(MambaBase, PluggableLayer):
271
conv_state = self_kv_cache[0].transpose(-1, -2)
272
ssm_state = self_kv_cache[1]
273
has_initial_states_p = attn_metadata.has_initial_states_p
274
+ cu_chunk_seqlen_p = attn_metadata.cu_chunk_seqlen_p
275
+ last_chunk_indices_p = attn_metadata.last_chunk_indices_p
276
277
# 1. Gated MLP's linear projection
278
projected_states = self.in_proj(hidden_states)[0].transpose(-2, -1)
@@ -376,6 +378,8 @@ class MambaMixer(MambaBase, PluggableLayer):
378
block_idx_first_scheduled_token=block_idx_first_scheduled_token_p,
379
block_idx_last_scheduled_token=block_idx_last_scheduled_token_p,
380
initial_state_idx=block_idx_last_computed_token_p,
381
+ cu_chunk_seqlen=cu_chunk_seqlen_p,
382
+ last_chunk_indices=last_chunk_indices_p,
383
)
384
ssm_outputs.append(scan_out_p)
385
vllm/model_executor/layers/mamba/ops/mamba_ssm.py
+4
@@ -497,6 +497,8 @@ def selective_scan_fn(
497
block_idx_first_scheduled_token=None,
498
block_idx_last_scheduled_token=None,
499
initial_state_idx=None,
500
+ cu_chunk_seqlen=None,
501
+ last_chunk_indices=None,
502
) -> torch.Tensor:
503
"""
504
u: (dim, total_length) for varlen or (batch, dim, seqlen)
@@ -588,6 +590,8 @@ def selective_scan_fn(
590
block_idx_first_scheduled_token,
591
block_idx_last_scheduled_token,
592
initial_state_idx,
593
+ cu_chunk_seqlen,
594
+ last_chunk_indices,
595
)
596
597
if z is None:
vllm/v1/attention/backends/mamba1_attn.py
+31
-2
@@ -1,9 +1,10 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-from dataclasses import dataclass
4
+from dataclasses import dataclass, replace
5
+from typing import Any
6
6
-from vllm.v1.attention.backend import AttentionBackend
7
+from vllm.v1.attention.backend import AttentionBackend, CommonAttentionMetadata
8
from vllm.v1.attention.backends.mamba_attn import (
9
BaseMambaAttentionMetadata,
10
BaseMambaAttentionMetadataBuilder,
@@ -29,3 +30,31 @@ class Mamba1AttentionMetadataBuilder(
30
BaseMambaAttentionMetadataBuilder[Mamba1AttentionMetadata]
31
):
32
metadata_cls = Mamba1AttentionMetadata
33
+
34
+ def build(
35
+ self,
36
+ common_prefix_len: int,
37
+ common_attn_metadata: CommonAttentionMetadata,
38
+ fast_build: bool = False,
39
+ **kwargs: Any,
40
+ ) -> Mamba1AttentionMetadata:
41
+ common = self._compute_common_metadata(common_attn_metadata)
42
+
43
+ if (
44
+ common.num_prefills > 0
45
+ and self.vllm_config.cache_config.mamba_cache_mode == "all"
46
+ ):
47
+ cu_chunk_seqlen_p, _, last_chunk_indices_p = (
48
+ self._build_chunk_metadata_tensors(
49
+ self.kv_cache_spec.block_size,
50
+ common,
51
+ common_attn_metadata,
52
+ )
53
+ )
54
+ return replace(
55
+ common,
56
+ cu_chunk_seqlen_p=cu_chunk_seqlen_p,
57
+ last_chunk_indices_p=last_chunk_indices_p,
58
+ )
59
+
60
+ return common
vllm/v1/attention/backends/mamba2_attn.py
+6
-106
@@ -7,7 +7,6 @@ from typing import Any
7
import torch
8
9
from vllm.config import VllmConfig
10
-from vllm.utils.math_utils import cdiv
10
from vllm.v1.attention.backend import (
11
AttentionBackend,
12
CommonAttentionMetadata,
@@ -105,14 +104,6 @@ class Mamba2AttentionMetadata(BaseMambaAttentionMetadata):
104
105
# Chunk-related metadata (only for prefill)
106
seq_idx_p: torch.Tensor | None = None
108
- # cu_chunk_seqlen_p is a tensor of shape (nchunks+1,) that contains, for
109
- # each chunk, its offsets into the varlen sequence dimension. It is defined
110
- # such that the i-th chunk contains tokens from cu_chunk_seqlen_p[i] to
111
- # cu_chunk_seqlen_p[i+1].
112
- cu_chunk_seqlen_p: torch.Tensor | None = None
113
- # last_chunk_indices_p is a tensor of shape (batch,) that contains the
114
- # index of the last chunk for every sequence in the (prefill) batch.
115
- last_chunk_indices_p: torch.Tensor | None = None
107
108
109
class Mamba2AttentionMetadataBuilder(
@@ -134,68 +125,6 @@ class Mamba2AttentionMetadataBuilder(
125
)
126
self.chunk_size: int = chunk_size
127
137
- def _compute_chunk_metadata(
138
- self,
139
- num_prefills: int,
140
- num_computed_tokens_p_cpu: torch.Tensor,
141
- query_start_loc_p_cpu: torch.Tensor,
142
- ) -> tuple[list[int], list[int], list[int]]:
143
- """
144
- Compute chunk-specific metadata for Mamba2.
145
-
146
- The code below carefully constructs the chunks such that:
147
- 1. Chunks contain tokens from a *single* sequence only.
148
- 2. For every sequence, we are guaranteed that we can
149
- retrieve the mamba state *every* chunk_size tokens.
150
- Constraint (1) dramatically simplifies the mamba2 kernels.
151
- Constraint (2) dramatically simplifies the implementation
152
- of prefix caching for mamba2 (wip). We need to take care
153
- of the interaction with chunked prefill in order to
154
- satisfy constraint (2).
155
- """
156
- # TODO (tdoublep): This code could probably be optimized.
157
- cu_chunk_seqlen = []
158
- seq_idx = []
159
- last_chunk_indices = []
160
- seqlen_pos = 0
161
-
162
- for req_idx in range(num_prefills):
163
- this_num_computed = num_computed_tokens_p_cpu[req_idx].item()
164
- this_new_tokens = (
165
- query_start_loc_p_cpu[req_idx + 1].item()
166
- - query_start_loc_p_cpu[req_idx].item()
167
- )
168
-
169
- # if computed tokens are not chunk-aligned, use the first
170
- # chunk to finish it off
171
- if this_num_computed % self.chunk_size != 0:
172
- seq_idx.append(req_idx)
173
- cu_chunk_seqlen.append(seqlen_pos)
174
- # how many tokens to finish the chunk?
175
- chunk_len = (
176
- cdiv(this_num_computed, self.chunk_size) * self.chunk_size
177
- - this_num_computed
178
- )
179
- # we can only use at most this_new_tokens
180
- chunk_len = min(chunk_len, this_new_tokens)
181
- seqlen_pos += chunk_len
182
- this_new_tokens -= chunk_len
183
-
184
- n_chunks = cdiv(this_new_tokens, self.chunk_size)
185
- for chunk in range(n_chunks):
186
- seq_idx.append(req_idx)
187
- cu_chunk_seqlen.append(seqlen_pos)
188
- chunk_len = min(self.chunk_size, this_new_tokens)
189
- seqlen_pos += chunk_len
190
- this_new_tokens -= chunk_len
191
-
192
- assert this_new_tokens == 0
193
- last_chunk_indices.append(len(cu_chunk_seqlen) - 1)
194
-
195
- cu_chunk_seqlen.append(seqlen_pos)
196
-
197
- return cu_chunk_seqlen, seq_idx, last_chunk_indices
198
-
128
def build(
129
self,
130
common_prefix_len: int,
@@ -220,41 +149,12 @@ class Mamba2AttentionMetadataBuilder(
149
else False
150
)
151
223
- num_reqs = common.num_reqs
224
- num_prefills = common.num_prefills
225
- num_decode_tokens = common.num_decode_tokens
226
-
227
- num_computed_tokens_cpu = (
228
- common_attn_metadata.compute_num_computed_tokens().cpu()
229
- )
230
- num_computed_tokens_p_cpu = num_computed_tokens_cpu[
231
- num_reqs - num_prefills : num_reqs
232
- ]
233
- query_start_loc_p_cpu = (
234
- common_attn_metadata.query_start_loc_cpu[-num_prefills - 1 :]
235
- - num_decode_tokens
236
- )
237
-
238
- cu_chunk_seqlen, seq_idx, last_chunk_indices = self._compute_chunk_metadata(
239
- num_prefills,
240
- num_computed_tokens_p_cpu,
241
- query_start_loc_p_cpu,
242
- )
243
-
244
- seq_idx_p = torch.as_tensor(
245
- seq_idx,
246
- device=common_attn_metadata.query_start_loc.device,
247
- dtype=torch.int32,
248
- )
249
- cu_chunk_seqlen_p = torch.as_tensor(
250
- cu_chunk_seqlen,
251
- device=common_attn_metadata.query_start_loc.device,
252
- dtype=torch.int32,
253
- )
254
- last_chunk_indices_p = torch.as_tensor(
255
- last_chunk_indices,
256
- device=common_attn_metadata.query_start_loc.device,
257
- dtype=torch.int32,
152
+ cu_chunk_seqlen_p, seq_idx_p, last_chunk_indices_p = (
153
+ self._build_chunk_metadata_tensors(
154
+ self.chunk_size,
155
+ common,
156
+ common_attn_metadata,
157
+ )
158
)
159
160
return replace(
vllm/v1/attention/backends/mamba_attn.py
+121
@@ -59,6 +59,15 @@ class BaseMambaAttentionMetadata:
59
# The following tensor is only used for prefix caching in align mode
60
seq_lens: torch.Tensor
61
62
+ # cu_chunk_seqlen_p is a tensor of shape (nchunks+1,) that contains, for
63
+ # each chunk, its offsets into the varlen sequence dimension. It is defined
64
+ # such that the i-th chunk contains tokens from cu_chunk_seqlen_p[i] to
65
+ # cu_chunk_seqlen_p[i+1].
66
+ cu_chunk_seqlen_p: torch.Tensor | None = None
67
+ # last_chunk_indices_p is a tensor of shape (batch,) that contains the
68
+ # index of the last chunk for every sequence in the (prefill) batch.
69
+ last_chunk_indices_p: torch.Tensor | None = None
70
+
71
# The following attributes are for triton implementation of causal_conv1d
72
nums_dict: dict | None = None
73
batch_ptr: torch.Tensor | None = None
@@ -185,6 +194,118 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
194
common_attn_metadata, num_accepted_tokens=num_accepted_tokens
195
)
196
197
+ def _compute_chunk_metadata(
198
+ self,
199
+ chunk_size: int,
200
+ num_prefills: int,
201
+ num_computed_tokens_p_cpu: torch.Tensor,
202
+ query_start_loc_p_cpu: torch.Tensor,
203
+ ) -> tuple[list[int], list[int], list[int]]:
204
+ """
205
+ Compute chunk-specific metadata for Mamba models.
206
+
207
+ The code below carefully constructs the chunks such that:
208
+ 1. Chunks contain tokens from a *single* sequence only.
209
+ 2. For every sequence, we are guaranteed that we can
210
+ retrieve the mamba state *every* chunk_size tokens.
211
+ Constraint (1) dramatically simplifies the mamba kernels.
212
+ Constraint (2) dramatically simplifies the implementation
213
+ of prefix caching for mamba (wip). We need to take care
214
+ of the interaction with chunked prefill in order to
215
+ satisfy constraint (2).
216
+ """
217
+ # TODO (tdoublep): This code could probably be optimized.
218
+ cu_chunk_seqlen = []
219
+ seq_idx = []
220
+ last_chunk_indices = []
221
+ seqlen_pos = 0
222
+
223
+ for req_idx in range(num_prefills):
224
+ this_num_computed = num_computed_tokens_p_cpu[req_idx].item()
225
+ this_new_tokens = (
226
+ query_start_loc_p_cpu[req_idx + 1].item()
227
+ - query_start_loc_p_cpu[req_idx].item()
228
+ )
229
+
230
+ # if computed tokens are not chunk-aligned, use the first
231
+ # chunk to finish it off
232
+ if this_num_computed % chunk_size != 0:
233
+ seq_idx.append(req_idx)
234
+ cu_chunk_seqlen.append(seqlen_pos)
235
+ # how many tokens to finish the chunk?
236
+ chunk_len = (
237
+ cdiv(this_num_computed, chunk_size) * chunk_size - this_num_computed
238
+ )
239
+ # we can only use at most this_new_tokens
240
+ chunk_len = min(chunk_len, this_new_tokens)
241
+ seqlen_pos += chunk_len
242
+ this_new_tokens -= chunk_len
243
+
244
+ n_chunks = cdiv(this_new_tokens, chunk_size)
245
+ for chunk in range(n_chunks):
246
+ seq_idx.append(req_idx)
247
+ cu_chunk_seqlen.append(seqlen_pos)
248
+ chunk_len = min(chunk_size, this_new_tokens)
249
+ seqlen_pos += chunk_len
250
+ this_new_tokens -= chunk_len
251
+
252
+ assert this_new_tokens == 0
253
+ last_chunk_indices.append(len(cu_chunk_seqlen) - 1)
254
+
255
+ cu_chunk_seqlen.append(seqlen_pos)
256
+
257
+ return cu_chunk_seqlen, seq_idx, last_chunk_indices
258
+
259
+ def _build_chunk_metadata_tensors(
260
+ self,
261
+ chunk_size: int,
262
+ common: M,
263
+ common_attn_metadata: CommonAttentionMetadata,
264
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
265
+ """
266
+ Compute chunk metadata and return as device tensors.
267
+ Returns (cu_chunk_seqlen_p, seq_idx_p, last_chunk_indices_p).
268
+ """
269
+ num_reqs = common.num_reqs
270
+ num_prefills = common.num_prefills
271
+ num_decode_tokens = common.num_decode_tokens
272
+
273
+ num_computed_tokens_cpu = (
274
+ common_attn_metadata.compute_num_computed_tokens().cpu()
275
+ )
276
+ num_computed_tokens_p_cpu = num_computed_tokens_cpu[
277
+ num_reqs - num_prefills : num_reqs
278
+ ]
279
+ query_start_loc_p_cpu = (
280
+ common_attn_metadata.query_start_loc_cpu[-num_prefills - 1 :]
281
+ - num_decode_tokens
282
+ )
283
+
284
+ cu_chunk_seqlen, seq_idx, last_chunk_indices = self._compute_chunk_metadata(
285
+ chunk_size,
286
+ num_prefills,
287
+ num_computed_tokens_p_cpu,
288
+ query_start_loc_p_cpu,
289
+ )
290
+
291
+ device = common_attn_metadata.query_start_loc.device
292
+ cu_chunk_seqlen_p = torch.as_tensor(
293
+ cu_chunk_seqlen,
294
+ device=device,
295
+ dtype=torch.int32,
296
+ )
297
+ seq_idx_p = torch.as_tensor(
298
+ seq_idx,
299
+ device=device,
300
+ dtype=torch.int32,
301
+ )
302
+ last_chunk_indices_p = torch.as_tensor(
303
+ last_chunk_indices,
304
+ device=device,
305
+ dtype=torch.int32,
306
+ )
307
+ return cu_chunk_seqlen_p, seq_idx_p, last_chunk_indices_p
308
+
309
def _compute_prefix_caching_block_indices(
310
self,
311
common_attn_metadata: CommonAttentionMetadata,