[Kernel] Optimize long-context MLA cache gathers (#51739)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com> Co-authored-by: OpenAI Codex <noreply@openai.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Yongye Zhu committed
Aug 10, 2026 at 22:23 UTC
c76a425278bc41e9d661a377d7c166f5be42cf6b
6 files changed
+810
-235
.buildkite/test_areas/kernels.yaml
+1
-1
@@ -224,7 +224,7 @@ steps:
224
- label: Kernels Mamba Test
225
device: h200_35gb
226
key: kernels-mamba-test
227
- timeout_in_minutes: 40
227
+ timeout_in_minutes: 60
228
source_file_dependencies:
229
- csrc/mamba/
230
- tests/kernels/mamba
benchmarks/kernels/benchmark_cp_gather.py
new
+248
@@ -0,0 +1,248 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import math
5
+from collections.abc import Callable
6
+
7
+import torch
8
+
9
+from vllm import _custom_ops as ops
10
+from vllm.triton_utils import triton
11
+from vllm.utils.argparse_utils import FlexibleArgumentParser
12
+
13
+SCENARIOS = {
14
+ "single-60k": [60_000],
15
+ "single-300k": [300_000],
16
+ "skew-2": [60_000, 300_000],
17
+ "skew-4": [60_000, 100_000, 180_000, 300_000],
18
+ "skew-8": [
19
+ 60_000,
20
+ 60_000,
21
+ 80_000,
22
+ 100_000,
23
+ 140_000,
24
+ 180_000,
25
+ 240_000,
26
+ 300_000,
27
+ ],
28
+}
29
+DTYPES = {
30
+ "fp8": torch.float8_e4m3fn,
31
+ "bfloat16": torch.bfloat16,
32
+ "float32": torch.float32,
33
+}
34
+
35
+
36
+def make_page_table(
37
+ seq_lens: list[int],
38
+ block_size: int,
39
+ seq_starts: list[int] | None = None,
40
+) -> tuple[torch.Tensor, torch.Tensor, int]:
41
+ if seq_starts is None:
42
+ seq_starts = [0] * len(seq_lens)
43
+ blocks_per_req = [
44
+ math.ceil((start + length) / block_size)
45
+ for start, length in zip(seq_starts, seq_lens)
46
+ ]
47
+ total_blocks = sum(blocks_per_req)
48
+ block_table = torch.zeros(
49
+ (len(seq_lens), max(blocks_per_req)), dtype=torch.int32, device="cuda"
50
+ )
51
+ physical_blocks = torch.randperm(total_blocks, dtype=torch.int32, device="cuda")
52
+ block_offset = 0
53
+ for req_id, num_blocks in enumerate(blocks_per_req):
54
+ block_table[req_id, :num_blocks] = physical_blocks[
55
+ block_offset : block_offset + num_blocks
56
+ ]
57
+ block_offset += num_blocks
58
+
59
+ cu_seq_lens = torch.zeros(len(seq_lens) + 1, dtype=torch.int32, device="cuda")
60
+ cu_seq_lens[1:] = torch.tensor(seq_lens, dtype=torch.int32, device="cuda").cumsum(
61
+ dim=0
62
+ )
63
+ return block_table, cu_seq_lens, total_blocks
64
+
65
+
66
+def make_cache_gather(
67
+ seq_lens: list[int],
68
+ block_size: int,
69
+ entry_size: int,
70
+ dtype: torch.dtype,
71
+) -> tuple[Callable[[], None], int]:
72
+ seq_starts_list = [
73
+ 13 + (17 * req_id) % block_size for req_id in range(len(seq_lens))
74
+ ]
75
+ block_table, cu_seq_lens, total_blocks = make_page_table(
76
+ seq_lens, block_size, seq_starts_list
77
+ )
78
+ src_cache = torch.empty(
79
+ (total_blocks, block_size, entry_size), dtype=dtype, device="cuda"
80
+ )
81
+ dst = torch.empty((sum(seq_lens), entry_size), dtype=dtype, device="cuda")
82
+ seq_starts = torch.tensor(seq_starts_list, dtype=torch.int32, device="cuda")
83
+
84
+ def run() -> None:
85
+ ops.cp_gather_cache(
86
+ src_cache,
87
+ dst,
88
+ block_table,
89
+ cu_seq_lens,
90
+ len(seq_lens),
91
+ seq_starts,
92
+ )
93
+
94
+ bytes_moved = 2 * dst.numel() * dst.element_size()
95
+ return run, bytes_moved
96
+
97
+
98
+def make_fp8_upconvert(
99
+ seq_lens: list[int],
100
+ block_size: int,
101
+) -> tuple[Callable[[], None], int]:
102
+ entry_bytes = 656
103
+ output_elements = 576
104
+ seq_starts_list = [
105
+ 13 + (17 * req_id) % block_size for req_id in range(len(seq_lens))
106
+ ]
107
+ block_table, cu_seq_lens, total_blocks = make_page_table(
108
+ seq_lens, block_size, seq_starts_list
109
+ )
110
+ src_cache = torch.empty(
111
+ (total_blocks, block_size, entry_bytes),
112
+ dtype=torch.uint8,
113
+ device="cuda",
114
+ )
115
+ dst = torch.empty(
116
+ (sum(seq_lens), output_elements), dtype=torch.bfloat16, device="cuda"
117
+ )
118
+ seq_starts = torch.tensor(seq_starts_list, dtype=torch.int32, device="cuda")
119
+
120
+ def run() -> None:
121
+ ops.cp_gather_and_upconvert_fp8_kv_cache(
122
+ src_cache,
123
+ dst,
124
+ block_table,
125
+ cu_seq_lens[:-1],
126
+ len(seq_lens),
127
+ seq_starts,
128
+ )
129
+
130
+ bytes_moved = sum(seq_lens) * (entry_bytes + output_elements * 2)
131
+ return run, bytes_moved
132
+
133
+
134
+def make_maybe_dequant_gather(
135
+ seq_lens: list[int],
136
+ block_size: int,
137
+ entry_size: int,
138
+) -> tuple[Callable[[], None], int]:
139
+ seq_starts_list = [
140
+ 13 + (17 * req_id) % block_size for req_id in range(len(seq_lens))
141
+ ]
142
+ block_table, cu_seq_lens, total_blocks = make_page_table(
143
+ seq_lens, block_size, seq_starts_list
144
+ )
145
+ src_cache = torch.empty(
146
+ (total_blocks, block_size, entry_size),
147
+ dtype=torch.float8_e4m3fn,
148
+ device="cuda",
149
+ )
150
+ dst = torch.empty((sum(seq_lens), entry_size), dtype=torch.bfloat16, device="cuda")
151
+ token_to_seq = torch.repeat_interleave(
152
+ torch.arange(len(seq_lens), dtype=torch.int32, device="cuda"),
153
+ torch.tensor(seq_lens, dtype=torch.int32, device="cuda"),
154
+ )
155
+ seq_starts = torch.tensor(seq_starts_list, dtype=torch.int32, device="cuda")
156
+ scale = torch.tensor(0.1, dtype=torch.float32, device="cuda")
157
+
158
+ def run() -> None:
159
+ ops.gather_and_maybe_dequant_cache(
160
+ src_cache,
161
+ dst,
162
+ block_table,
163
+ cu_seq_lens,
164
+ token_to_seq,
165
+ sum(seq_lens),
166
+ "fp8",
167
+ scale,
168
+ seq_starts,
169
+ )
170
+
171
+ bytes_moved = sum(seq_lens) * entry_size * 3
172
+ return run, bytes_moved
173
+
174
+
175
+@torch.inference_mode()
176
+def run_scenario(
177
+ variant: str,
178
+ name: str,
179
+ seq_lens: list[int],
180
+ block_size: int,
181
+ entry_size: int,
182
+ dtype: torch.dtype,
183
+ warmup_ms: int,
184
+ rep_ms: int,
185
+) -> None:
186
+ if variant == "cache":
187
+ run, bytes_moved = make_cache_gather(seq_lens, block_size, entry_size, dtype)
188
+ elif variant == "fp8-upconvert":
189
+ run, bytes_moved = make_fp8_upconvert(seq_lens, block_size)
190
+ else:
191
+ run, bytes_moved = make_maybe_dequant_gather(seq_lens, block_size, entry_size)
192
+
193
+ latency_ms = triton.testing.do_bench(
194
+ run, warmup=warmup_ms, rep=rep_ms, return_mode="median"
195
+ )
196
+ bandwidth_gbps = bytes_moved / latency_ms / 1e6
197
+ lengths = ",".join(str(seq_len) for seq_len in seq_lens)
198
+ print(
199
+ f"{variant:15s} {name:10s} batch={len(seq_lens):2d} "
200
+ f"total={sum(seq_lens):7d} latency={latency_ms * 1e3:9.2f} us "
201
+ f"bandwidth={bandwidth_gbps:8.1f} GB/s lengths=[{lengths}]"
202
+ )
203
+
204
+
205
+def main() -> None:
206
+ parser = FlexibleArgumentParser(description="Benchmark cp_gather variants")
207
+ parser.add_argument(
208
+ "--variant",
209
+ choices=["all", "cache", "fp8-upconvert", "maybe-dequant"],
210
+ default="all",
211
+ )
212
+ parser.add_argument("--scenario", choices=["all", *SCENARIOS], default="all")
213
+ parser.add_argument("--dtype", choices=DTYPES, default="fp8")
214
+ parser.add_argument("--block-size", type=int, default=64)
215
+ parser.add_argument("--entry-size", type=int, default=576)
216
+ parser.add_argument("--warmup-ms", type=int, default=25)
217
+ parser.add_argument("--rep-ms", type=int, default=100)
218
+ parser.add_argument("--seed", type=int, default=0)
219
+ args = parser.parse_args()
220
+
221
+ torch.manual_seed(args.seed)
222
+ variants = (
223
+ ("cache", "fp8-upconvert", "maybe-dequant")
224
+ if args.variant == "all"
225
+ else (args.variant,)
226
+ )
227
+ scenarios = (
228
+ SCENARIOS
229
+ if args.scenario == "all"
230
+ else {args.scenario: SCENARIOS[args.scenario]}
231
+ )
232
+ for variant in variants:
233
+ for name, seq_lens in scenarios.items():
234
+ run_scenario(
235
+ variant,
236
+ name,
237
+ seq_lens,
238
+ args.block_size,
239
+ args.entry_size,
240
+ DTYPES[args.dtype],
241
+ args.warmup_ms,
242
+ args.rep_ms,
243
+ )
244
+ torch.accelerator.empty_cache()
245
+
246
+
247
+if __name__ == "__main__":
248
+ main()
csrc/libtorch_stable/cache_kernels.cu
+326
-232
@@ -1032,83 +1032,121 @@ void convert_fp8(torch::stable::Tensor& dst_cache,
1032
1033
namespace vllm {
1034
1035
-// grid is launched with dimensions (batch, num_splits)
1035
+struct GatherPageTask {
1036
+ int32_t req_id;
1037
+ int32_t logical_block;
1038
+ int32_t page_token_begin;
1039
+ int32_t page_token_end;
1040
+ int32_t output_token_begin;
1041
+};
1042
+
1043
+template <bool has_terminal_start>
1044
+__device__ __forceinline__ bool map_gather_page_task(
1045
+ int32_t task, const int32_t* __restrict__ output_starts, int32_t num_reqs,
1046
+ int32_t total_tokens, int32_t block_size,
1047
+ const int32_t* __restrict__ seq_starts, GatherPageTask& page) {
1048
+ int32_t relative_page = task;
1049
+ for (int32_t req_id = 0; req_id < num_reqs; ++req_id) {
1050
+ const int32_t output_begin = min(output_starts[req_id], total_tokens);
1051
+ int32_t output_end;
1052
+ if constexpr (has_terminal_start) {
1053
+ output_end = min(output_starts[req_id + 1], total_tokens);
1054
+ } else {
1055
+ output_end =
1056
+ min(req_id + 1 < num_reqs ? output_starts[req_id + 1] : total_tokens,
1057
+ total_tokens);
1058
+ }
1059
+ const int32_t seq_len = max(output_end - output_begin, 0);
1060
+ const int32_t source_begin = seq_starts == nullptr ? 0 : seq_starts[req_id];
1061
+ const int32_t first_block = source_begin / block_size;
1062
+ const int32_t num_pages =
1063
+ cuda_utils::ceil_div(source_begin + seq_len, block_size) - first_block;
1064
+ if (relative_page < num_pages) {
1065
+ page.req_id = req_id;
1066
+ page.logical_block = first_block + relative_page;
1067
+ const int32_t page_begin = page.logical_block * block_size;
1068
+ const int32_t copy_begin = max(source_begin, page_begin);
1069
+ const int32_t copy_end =
1070
+ min(source_begin + seq_len, page_begin + block_size);
1071
+ page.page_token_begin = copy_begin - page_begin;
1072
+ page.page_token_end = copy_end - page_begin;
1073
+ page.output_token_begin = output_begin + copy_begin - source_begin;
1074
+ return true;
1075
+ }
1076
+ relative_page -= num_pages;
1077
+ }
1078
+ return false;
1079
+}
1080
+
1081
template <typename scalar_t, typename cache_t, Fp8KVCacheDataType kv_dt,
1037
- int ENTRY_SIZE, int CTA_SIZE>
1038
-__global__ void gather_and_maybe_dequant_cache(
1039
- const cache_t* __restrict__ src_cache, // [NUM_BLOCKS, BLOCK_SIZE,
1040
- // ENTRIES...]
1041
- scalar_t* __restrict__ dst, // [TOT_TOKENS, ENTRIES...]
1042
- const int32_t* __restrict__ block_table, // [BATCH, BLOCK_INDICES]
1043
- const int32_t* __restrict__ cu_seq_lens, // [BATCH+1]
1044
- const int32_t* __restrict__ token_to_seq, // [MAX_TOKEN_ACROSS_CHUNK]
1082
+ int ENTRY_SIZE>
1083
+__global__ void gather_and_maybe_dequant_cache_page(
1084
+ const cache_t* __restrict__ src_cache, scalar_t* __restrict__ dst,
1085
+ const int32_t* __restrict__ block_table,
1086
+ const int32_t* __restrict__ cu_seq_lens, const int32_t num_reqs,
1087
const int32_t num_tokens, const int32_t block_size,
1088
const int64_t block_table_stride, const int64_t cache_block_stride,
1089
const int64_t cache_entry_stride, const int64_t dst_entry_stride,
1048
- const float* __restrict__ scale,
1049
- const int32_t* __restrict__ seq_starts) { // Optional: starting offsets per
1050
- // batch
1051
- constexpr int vec_size = sizeof(float4) / sizeof(scalar_t);
1090
+ const float* __restrict__ scale, const int32_t* __restrict__ seq_starts) {
1091
+ constexpr int32_t vec_size = sizeof(float4) / sizeof(scalar_t);
1092
+ constexpr int32_t vec_iter_cnt = ENTRY_SIZE / vec_size;
1093
+ static_assert(ENTRY_SIZE % vec_size == 0);
1094
using ltype = vllm::vec_n_t<cache_t, vec_size>;
1095
using stype = vllm::vec_n_t<scalar_t, vec_size>;
1054
- // We are adding this for code readability which will be optimized out when
1055
- // build in release.
1056
- assert(CTA_SIZE == blockDim.x);
1096
1058
-#pragma unroll
1059
- for (int token_id = blockIdx.x; token_id < num_tokens;
1060
- token_id += gridDim.x) {
1061
- int64_t batch_id = token_to_seq[token_id];
1062
- int64_t batch_start = cu_seq_lens[batch_id];
1063
- int64_t batch_end = cu_seq_lens[batch_id + 1];
1064
- int32_t batch_offset = token_id - batch_start;
1065
-
1066
- if (token_id >= batch_end) return;
1067
- int32_t offset = 0;
1068
- if (seq_starts != nullptr) {
1069
- offset = seq_starts[batch_id];
1097
+ __shared__ GatherPageTask page;
1098
+ __shared__ int32_t physical_block;
1099
+ __shared__ bool has_task;
1100
+ __shared__ bool copy_task;
1101
+ __shared__ float scale_value;
1102
+
1103
+ if (threadIdx.x == 0) {
1104
+ if constexpr (kv_dt != Fp8KVCacheDataType::kAuto) {
1105
+ scale_value = *scale;
1106
}
1071
- batch_offset += offset;
1072
- int32_t block_table_id = batch_offset / block_size;
1073
- int32_t slot_id = batch_offset % block_size;
1074
- // seq_starts may push the block index past the end of the batch's block
1075
- // table row.
1076
- if (block_table_id >= block_table_stride) continue;
1077
- int32_t block_table_offset = batch_id * block_table_stride + block_table_id;
1078
- int32_t block_id = block_table[block_table_offset];
1079
- int64_t cache_offset =
1080
- block_id * cache_block_stride + slot_id * cache_entry_stride;
1081
- constexpr int32_t vec_iter_cnt = ENTRY_SIZE / vec_size;
1082
- scalar_t* dst_ = dst + token_id * dst_entry_stride;
1083
- cache_t* src_ = const_cast<cache_t*>(src_cache) + cache_offset;
1107
+ }
1108
+ __syncthreads();
1109
+
1110
+ if (threadIdx.x == 0) {
1111
+ has_task =
1112
+ map_gather_page_task<true>(blockIdx.x, cu_seq_lens, num_reqs,
1113
+ num_tokens, block_size, seq_starts, page);
1114
+ copy_task = has_task && page.logical_block < block_table_stride;
1115
+ if (copy_task) {
1116
+ physical_block =
1117
+ block_table[page.req_id * block_table_stride + page.logical_block];
1118
+ }
1119
+ }
1120
+ __syncthreads();
1121
+ if (!has_task) {
1122
+ return;
1123
+ }
1124
+
1125
+ if (copy_task) {
1126
+ const int32_t page_vectors =
1127
+ (page.page_token_end - page.page_token_begin) * vec_iter_cnt;
1128
+ for (int32_t flat_idx = threadIdx.x; flat_idx < page_vectors;
1129
+ flat_idx += blockDim.x) {
1130
+ const int32_t token_offset = flat_idx / vec_iter_cnt;
1131
+ const int32_t idx = flat_idx - token_offset * vec_iter_cnt;
1132
+ const int32_t page_token = page.page_token_begin + token_offset;
1133
+ const int32_t output_token = page.output_token_begin + token_offset;
1134
+ const cache_t* src = src_cache + physical_block * cache_block_stride +
1135
+ page_token * cache_entry_stride;
1136
+ scalar_t* output = dst + output_token * dst_entry_stride;
1137
1085
-#pragma unroll
1086
- for (int idx = threadIdx.x; idx < vec_iter_cnt; idx += CTA_SIZE) {
1138
if constexpr (kv_dt == Fp8KVCacheDataType::kAuto) {
1088
- reinterpret_cast<stype*>(dst_)[idx] =
1089
- static_cast<stype>(reinterpret_cast<ltype*>(src_)[idx]);
1139
+ reinterpret_cast<stype*>(output)[idx] =
1140
+ static_cast<stype>(reinterpret_cast<const ltype*>(src)[idx]);
1141
} else {
1091
- ltype loaded_val = reinterpret_cast<ltype*>(src_)[idx];
1092
- stype store_val;
1142
+ const ltype loaded = reinterpret_cast<const ltype*>(src)[idx];
1143
+ stype converted;
1144
#pragma unroll
1094
- for (int j = 0; j < vec_size; ++j) {
1095
- store_val.val[j] = fp8::scaled_convert<scalar_t, cache_t, kv_dt>(
1096
- loaded_val.val[j], *scale);
1145
+ for (int32_t j = 0; j < vec_size; ++j) {
1146
+ converted.val[j] = fp8::scaled_convert<scalar_t, cache_t, kv_dt>(
1147
+ loaded.val[j], scale_value);
1148
}
1098
- reinterpret_cast<stype*>(dst_)[idx] = store_val;
1099
- }
1100
- }
1101
- // process tail
1102
- constexpr int32_t tail_cnt = ENTRY_SIZE % vec_size;
1103
- dst_ = dst_ + ENTRY_SIZE - tail_cnt;
1104
- src_ = src_ + ENTRY_SIZE - tail_cnt;
1105
-#pragma unroll
1106
- for (int idx = threadIdx.x; idx < tail_cnt; idx += CTA_SIZE) {
1107
- if constexpr (kv_dt == Fp8KVCacheDataType::kAuto) {
1108
- dst_[idx] = static_cast<scalar_t>(src_[idx]);
1109
- } else {
1110
- dst_[idx] =
1111
- fp8::scaled_convert<scalar_t, cache_t, kv_dt>(src_[idx], *scale);
1149
+ reinterpret_cast<stype*>(output)[idx] = converted;
1150
}
1151
}
1152
}
@@ -1120,18 +1158,17 @@ __global__ void gather_and_maybe_dequant_cache(
1158
// SCALAR_T is the data type of the destination tensor.
1159
// CACHE_T is the stored data type of kv-cache.
1160
// KV_DTYPE is the real data type of kv-cache.
1123
-#define CALL_GATHER_CACHE(SCALAR_T, CACHE_T, KV_DTYPE, ENTRY_SZ) \
1124
- vllm::gather_and_maybe_dequant_cache<SCALAR_T, CACHE_T, KV_DTYPE, ENTRY_SZ, \
1125
- thread_block_size> \
1126
- <<<grid, block, 0, stream>>>( \
1127
- reinterpret_cast<CACHE_T*>(src_cache.data_ptr()), \
1128
- reinterpret_cast<SCALAR_T*>(dst.data_ptr()), \
1129
- block_table.const_data_ptr<int32_t>(), \
1130
- cu_seq_lens.const_data_ptr<int32_t>(), \
1131
- token_to_seq.const_data_ptr<int32_t>(), num_tokens, block_size, \
1132
- block_table_stride, cache_block_stride, cache_entry_stride, \
1133
- dst_entry_stride, reinterpret_cast<const float*>(scale.data_ptr()), \
1134
- seq_starts_ptr);
1161
+#define CALL_GATHER_CACHE(SCALAR_T, CACHE_T, KV_DTYPE, ENTRY_SZ) \
1162
+ vllm::gather_and_maybe_dequant_cache_page<SCALAR_T, CACHE_T, KV_DTYPE, \
1163
+ ENTRY_SZ> \
1164
+ <<<grid, block, 0, stream>>>( \
1165
+ reinterpret_cast<CACHE_T*>(src_cache.data_ptr()), \
1166
+ reinterpret_cast<SCALAR_T*>(dst.data_ptr()), \
1167
+ block_table.const_data_ptr<int32_t>(), \
1168
+ cu_seq_lens.const_data_ptr<int32_t>(), num_reqs, \
1169
+ static_cast<int32_t>(num_tokens), block_size, block_table_stride, \
1170
+ cache_block_stride, cache_entry_stride, dst_entry_stride, \
1171
+ reinterpret_cast<const float*>(scale.data_ptr()), seq_starts_ptr)
1172
1173
#define CALL_GATHER_CACHE_576(SCALAR_T, CACHE_T, KV_DTYPE) \
1174
CALL_GATHER_CACHE(SCALAR_T, CACHE_T, KV_DTYPE, 576)
@@ -1142,7 +1179,6 @@ __global__ void gather_and_maybe_dequant_cache(
1179
// Gather sequences from the cache into the destination tensor.
1180
// - cu_seq_lens contains the cumulative sequence lengths for each batch
1181
// - block_table contains the cache block indices for each sequence
1145
-// - token_to_seq contains the back mapping from token_id to batch_id
1182
// - Optionally, seq_starts (if provided) offsets the starting block index by
1183
// (seq_starts[bid] / page_size)
1184
void gather_and_maybe_dequant_cache(
@@ -1158,6 +1194,7 @@ void gather_and_maybe_dequant_cache(
1194
torch::stable::accelerator::DeviceGuard device_guard(
1195
src_cache.get_device_index());
1196
const cudaStream_t stream = get_current_cuda_stream();
1197
+ (void)token_to_seq;
1198
1199
int32_t block_size = src_cache.size(1);
1200
int32_t head_dim = dst.size(-1);
@@ -1189,14 +1226,21 @@ void gather_and_maybe_dequant_cache(
1226
"src_cache and seq_starts must be on the same device");
1227
}
1228
1192
- int64_t block_table_stride = block_table.stride(0);
1193
- int64_t cache_block_stride = src_cache.stride(0);
1194
- int64_t cache_entry_stride = src_cache.stride(1);
1195
- int64_t dst_entry_stride = dst.stride(0);
1229
+ if (num_tokens == 0) {
1230
+ return;
1231
+ }
1232
1197
- constexpr int32_t thread_block_size = 64;
1198
- dim3 grid(num_tokens);
1199
- dim3 block(thread_block_size);
1233
+ const int32_t num_reqs = cu_seq_lens.size(0) - 1;
1234
+ const int64_t block_table_stride = block_table.stride(0);
1235
+ const int64_t cache_block_stride = src_cache.stride(0);
1236
+ const int64_t cache_entry_stride = src_cache.stride(1);
1237
+ const int64_t dst_entry_stride = dst.stride(0);
1238
+ const int32_t page_threads = num_tokens >= (1 << 20) ? 128 : 256;
1239
+ const int32_t required_blocks =
1240
+ cuda_utils::ceil_div(static_cast<int32_t>(num_tokens), block_size) +
1241
+ 2 * num_reqs;
1242
+ const dim3 grid(required_blocks);
1243
+ const dim3 block(page_threads);
1244
1245
const int32_t* seq_starts_ptr =
1246
seq_starts.has_value() ? seq_starts.value().const_data_ptr<int32_t>()
@@ -1213,151 +1257,161 @@ void gather_and_maybe_dequant_cache(
1257
1258
namespace vllm {
1259
1216
-// Gather and upconvert FP8 KV cache tokens to BF16 workspace
1217
-// Similar to cp_gather_cache but specifically for FP8->BF16 conversion
1218
-__global__ void cp_gather_and_upconvert_fp8_kv_cache(
1219
- const uint8_t* __restrict__ src_cache, // [NUM_BLOCKS, BLOCK_SIZE, 656]
1220
- __nv_bfloat16* __restrict__ dst, // [total_tokens, 576]
1221
- const int32_t* __restrict__ block_table, // [num_reqs, BLOCK_INDICES]
1222
- const int32_t* __restrict__ workspace_starts, // [num_reqs]
1223
- const int32_t num_reqs, const int32_t block_size,
1224
- const int32_t total_tokens, const int64_t block_table_stride,
1225
- const int64_t cache_block_stride, const int64_t cache_entry_stride,
1226
- const int64_t dst_entry_stride,
1227
- const int32_t* __restrict__ seq_starts) { // Optional source offsets
1228
- const int flat_warp_id = (blockIdx.x * blockDim.x + threadIdx.x) >> 5;
1229
- if (flat_warp_id >= total_tokens) return;
1230
- const int lane_id = threadIdx.x & 31;
1231
-
1232
- // Binary search to find which request owns this output token
1233
- int lo = 0, hi = num_reqs - 1;
1234
- while (lo < hi) {
1235
- int mid = (lo + hi + 1) >> 1;
1236
- if (workspace_starts[mid] <= flat_warp_id)
1237
- lo = mid;
1238
- else
1239
- hi = mid - 1;
1240
- }
1241
- const int req_id = lo;
1242
-
1243
- // Compute physical token address via block table
1244
- const int out_token_id = flat_warp_id;
1245
- int token_offset = out_token_id - workspace_starts[req_id];
1246
- if (seq_starts != nullptr) token_offset += seq_starts[req_id];
1247
- const int cache_block_idx = token_offset / block_size;
1248
- const int offset_in_block = token_offset % block_size;
1249
- const int physical_block =
1250
- block_table[req_id * block_table_stride + cache_block_idx];
1251
-
1252
- const uint8_t* token_ptr = src_cache + physical_block * cache_block_stride +
1253
- offset_in_block * cache_entry_stride;
1254
-
1255
- const int4* nope_src = reinterpret_cast<const int4*>(token_ptr);
1256
- const int4 fp8_data = nope_src[lane_id];
1257
-
1258
- const float* scales_ptr = reinterpret_cast<const float*>(token_ptr + 512);
1259
- const float scale = scales_ptr[lane_id >> 3];
1260
+__device__ __forceinline__ void gather_and_upconvert_fp8_token(
1261
+ const uint8_t* __restrict__ token_ptr, __nv_bfloat16* __restrict__ dst_ptr,
1262
+ int32_t lane_id) {
1263
+ const uint2* fp8_src = reinterpret_cast<const uint2*>(token_ptr);
1264
+ const float* scales = reinterpret_cast<const float*>(token_ptr + 512);
1265
+ int4* nope_dst = reinterpret_cast<int4*>(dst_ptr);
1266
1261
- const uint2 fp8_lo = make_uint2(fp8_data.x, fp8_data.y);
1262
- const uint2 fp8_hi = make_uint2(fp8_data.z, fp8_data.w);
1267
+#pragma unroll
1268
+ for (int32_t phase = 0; phase < 2; ++phase) {
1269
+ const int32_t chunk = phase * 32 + lane_id;
1270
+ const uint2 fp8_data = fp8_src[chunk];
1271
+ const float scale = scales[chunk >> 4];
1272
#ifdef USE_ROCM
1264
- const bf16_8_t bf16_lo =
1265
- fp8::scaled_vec_conversion<bf16_8_t, uint2>(fp8_lo, scale);
1266
- const bf16_8_t bf16_hi =
1267
- fp8::scaled_vec_conversion<bf16_8_t, uint2>(fp8_hi, scale);
1273
+ const bf16_8_t bf16_data =
1274
+ fp8::scaled_vec_conversion<bf16_8_t, uint2>(fp8_data, scale);
1275
#else
1269
- const bf16_8_t bf16_lo =
1270
- fp8::scaled_vec_conversion<bf16_8_t, uint2>(fp8_lo, scale, __NV_E4M3);
1271
- const bf16_8_t bf16_hi =
1272
- fp8::scaled_vec_conversion<bf16_8_t, uint2>(fp8_hi, scale, __NV_E4M3);
1276
+ const bf16_8_t bf16_data =
1277
+ fp8::scaled_vec_conversion<bf16_8_t, uint2>(fp8_data, scale, __NV_E4M3);
1278
#endif
1274
-
1275
- __nv_bfloat16* dst_ptr = dst + out_token_id * dst_entry_stride;
1276
- int4* nope_dst = reinterpret_cast<int4*>(dst_ptr) + lane_id * 2;
1277
- nope_dst[0] = *reinterpret_cast<const int4*>(&bf16_lo);
1278
- nope_dst[1] = *reinterpret_cast<const int4*>(&bf16_hi);
1279
+ nope_dst[chunk] = *reinterpret_cast<const int4*>(&bf16_data);
1280
+ }
1281
1282
const int* rope_src = reinterpret_cast<const int*>(token_ptr + 528);
1283
int* rope_dst = reinterpret_cast<int*>(dst_ptr + 512);
1284
rope_dst[lane_id] = rope_src[lane_id];
1285
}
1286
1285
-template <typename scalar_t>
1286
-// Note(hc): The cp_gather_cache allows seq_starts to no longer be divisible by
1287
-// block_size.
1288
-__global__ void cp_gather_cache(
1289
- const scalar_t* __restrict__ src_cache, // [NUM_BLOCKS, BLOCK_SIZE,
1290
- // ENTRY_SIZE]
1291
- scalar_t* __restrict__ dst, // [TOT_TOKENS, ENTRY_SIZE]
1292
- const int32_t* __restrict__ block_table, // [BATCH, BLOCK_INDICES]
1293
- const int32_t* __restrict__ cu_seq_lens, // [BATCH+1]
1294
- const int32_t block_size, const int32_t entry_size,
1287
+__global__ void cp_gather_and_upconvert_fp8_kv_cache_page(
1288
+ const uint8_t* __restrict__ src_cache, __nv_bfloat16* __restrict__ dst,
1289
+ const int32_t* __restrict__ block_table,
1290
+ const int32_t* __restrict__ workspace_starts, const int32_t num_reqs,
1291
+ const int32_t block_size, const int32_t total_tokens,
1292
const int64_t block_table_stride, const int64_t cache_block_stride,
1293
const int64_t cache_entry_stride, const int64_t dst_entry_stride,
1297
- const int32_t* __restrict__ seq_starts // Optional: starting offsets per
1298
- // batch
1299
-) {
1300
- const int64_t bid = blockIdx.x; // Batch ID
1301
- const int32_t num_splits = gridDim.y;
1302
- const int32_t split = blockIdx.y;
1303
- const int32_t seq_start = cu_seq_lens[bid];
1304
- const int32_t seq_end = cu_seq_lens[bid + 1];
1305
- const int32_t seq_len = seq_end - seq_start;
1306
- const int32_t tot_slots = seq_len;
1307
- const int32_t split_slots = cuda_utils::ceil_div(tot_slots, num_splits);
1308
-
1309
- const int32_t split_start = split * split_slots;
1310
- const int32_t split_end = min((split + 1) * split_slots, tot_slots);
1311
-
1312
- const bool is_active_split = (split_start < tot_slots);
1313
-
1314
- if (!is_active_split) return;
1315
-
1316
- // Adjust the pointer for the block_table for this batch.
1317
- // If seq_starts is provided, compute an offset based on it
1318
- const int32_t batch_offset = bid * block_table_stride;
1319
- int32_t offset = split_start;
1320
- if (seq_starts != nullptr) {
1321
- offset += seq_starts[bid];
1294
+ const int32_t* __restrict__ seq_starts) {
1295
+ constexpr int32_t warps_per_cta = 16;
1296
+ __shared__ GatherPageTask page;
1297
+ __shared__ int32_t physical_block;
1298
+ __shared__ bool has_task;
1299
+
1300
+ if (threadIdx.x == 0) {
1301
+ has_task =
1302
+ map_gather_page_task<false>(blockIdx.x, workspace_starts, num_reqs,
1303
+ total_tokens, block_size, seq_starts, page);
1304
+ if (has_task) {
1305
+ physical_block =
1306
+ block_table[page.req_id * block_table_stride + page.logical_block];
1307
+ }
1308
+ }
1309
+ __syncthreads();
1310
+ if (!has_task) {
1311
+ return;
1312
}
1323
- int32_t offset_div = offset / block_size;
1324
- offset = offset % block_size;
1325
- const int32_t* batch_block_table = block_table + batch_offset;
1313
1327
- // Adjust dst pointer based on the cumulative sequence lengths.
1328
- dst += seq_start * dst_entry_stride;
1314
+ const int32_t warp_id = threadIdx.x >> 5;
1315
+ const int32_t lane_id = threadIdx.x & 31;
1316
+ for (int32_t page_token = page.page_token_begin + warp_id;
1317
+ page_token < page.page_token_end; page_token += warps_per_cta) {
1318
+ const int32_t output_token =
1319
+ page.output_token_begin + page_token - page.page_token_begin;
1320
+ const uint8_t* token_ptr = src_cache + physical_block * cache_block_stride +
1321
+ page_token * cache_entry_stride;
1322
+ __nv_bfloat16* dst_ptr = dst + output_token * dst_entry_stride;
1323
+ gather_and_upconvert_fp8_token(token_ptr, dst_ptr, lane_id);
1324
+ }
1325
+}
1326
1330
- auto copy_entry = [&](const scalar_t* __restrict__ _src,
1331
- scalar_t* __restrict__ _dst) {
1332
- for (int i = threadIdx.x; i < entry_size; i += blockDim.x)
1333
- _dst[i] = _src[i];
1334
- };
1327
+template <bool contiguous_entries, bool vectorized>
1328
+__global__ void cp_gather_cache_page(
1329
+ const uint8_t* __restrict__ src_cache, // [NUM_BLOCKS, BLOCK_SIZE,
1330
+ // ENTRY_SIZE_BYTES]
1331
+ uint8_t* __restrict__ dst, // [TOT_TOKENS, ENTRY_SIZE_BYTES]
1332
+ const int32_t* __restrict__ block_table, // [BATCH, BLOCK_INDICES]
1333
+ const int32_t* __restrict__ cu_seq_lens, // [BATCH+1]
1334
+ const int32_t num_reqs, const int32_t block_size,
1335
+ const int32_t entry_size_bytes, const int32_t total_tokens,
1336
+ const int64_t block_table_stride, const int64_t cache_block_stride,
1337
+ const int64_t cache_entry_stride, const int64_t dst_entry_stride,
1338
+ const int32_t* __restrict__ seq_starts) {
1339
+ constexpr int32_t threads_per_token = 32;
1340
+ constexpr int32_t tokens_per_cta = 256 / threads_per_token;
1341
+ __shared__ GatherPageTask page;
1342
+ __shared__ int32_t physical_block;
1343
+ __shared__ bool has_task;
1344
+
1345
+ if (threadIdx.x == 0) {
1346
+ has_task =
1347
+ map_gather_page_task<true>(blockIdx.x, cu_seq_lens, num_reqs,
1348
+ total_tokens, block_size, seq_starts, page);
1349
+ if (has_task) {
1350
+ physical_block =
1351
+ block_table[page.req_id * block_table_stride + page.logical_block];
1352
+ }
1353
+ }
1354
+ __syncthreads();
1355
+ if (!has_task) {
1356
+ return;
1357
+ }
1358
1336
- for (int pid = split_start; pid < split_end; ++pid) {
1337
- auto block_id = batch_block_table[offset_div];
1338
- auto block_start_ptr = src_cache + block_id * cache_block_stride;
1339
- auto block_dst_ptr = dst + pid * dst_entry_stride;
1340
- copy_entry(block_start_ptr + offset * cache_entry_stride, block_dst_ptr);
1341
- offset += 1;
1342
- // bump to next block
1343
- if (offset == block_size) {
1344
- offset_div += 1;
1345
- offset = 0;
1359
+ if constexpr (contiguous_entries) {
1360
+ const int64_t src_offset = physical_block * cache_block_stride +
1361
+ page.page_token_begin * entry_size_bytes;
1362
+ const int64_t dst_offset = page.output_token_begin * entry_size_bytes;
1363
+ const int32_t copy_bytes =
1364
+ (page.page_token_end - page.page_token_begin) * entry_size_bytes;
1365
+ if constexpr (vectorized) {
1366
+ const int4* src = reinterpret_cast<const int4*>(src_cache + src_offset);
1367
+ int4* output = reinterpret_cast<int4*>(dst + dst_offset);
1368
+ const int32_t num_vectors = copy_bytes / sizeof(int4);
1369
+ int32_t idx = threadIdx.x;
1370
+ for (; idx + blockDim.x < num_vectors; idx += 2 * blockDim.x) {
1371
+ const int4 first = src[idx];
1372
+ const int4 second = src[idx + blockDim.x];
1373
+ output[idx] = first;
1374
+ output[idx + blockDim.x] = second;
1375
+ }
1376
+ for (; idx < num_vectors; idx += blockDim.x) {
1377
+ output[idx] = src[idx];
1378
+ }
1379
+ } else {
1380
+ const uint8_t* src = src_cache + src_offset;
1381
+ uint8_t* output = dst + dst_offset;
1382
+ for (int32_t idx = threadIdx.x; idx < copy_bytes; idx += blockDim.x) {
1383
+ output[idx] = src[idx];
1384
+ }
1385
+ }
1386
+ } else {
1387
+ const int32_t token_group = threadIdx.x / threads_per_token;
1388
+ const int32_t lane_id = threadIdx.x % threads_per_token;
1389
+ for (int32_t page_token = page.page_token_begin + token_group;
1390
+ page_token < page.page_token_end; page_token += tokens_per_cta) {
1391
+ const int32_t output_token =
1392
+ page.output_token_begin + page_token - page.page_token_begin;
1393
+ const uint8_t* src = src_cache + physical_block * cache_block_stride +
1394
+ page_token * cache_entry_stride;
1395
+ uint8_t* output = dst + output_token * dst_entry_stride;
1396
+ if constexpr (vectorized) {
1397
+ const int4* src_vec = reinterpret_cast<const int4*>(src);
1398
+ int4* dst_vec = reinterpret_cast<int4*>(output);
1399
+ const int32_t num_vectors = entry_size_bytes / sizeof(int4);
1400
+ for (int32_t idx = lane_id; idx < num_vectors;
1401
+ idx += threads_per_token) {
1402
+ dst_vec[idx] = src_vec[idx];
1403
+ }
1404
+ } else {
1405
+ for (int32_t idx = lane_id; idx < entry_size_bytes;
1406
+ idx += threads_per_token) {
1407
+ output[idx] = src[idx];
1408
+ }
1409
+ }
1410
}
1411
}
1412
}
1413
} // namespace vllm
1414
1351
-// Macro to dispatch the kernel based on the data type.
1352
-#define CALL_CP_GATHER_CACHE(CPY_DTYPE) \
1353
- vllm::cp_gather_cache<CPY_DTYPE><<<grid, block, 0, stream>>>( \
1354
- reinterpret_cast<CPY_DTYPE*>(src_cache.data_ptr()), \
1355
- reinterpret_cast<CPY_DTYPE*>(dst.data_ptr()), \
1356
- block_table.const_data_ptr<int32_t>(), \
1357
- cu_seq_lens.const_data_ptr<int32_t>(), block_size, entry_size, \
1358
- block_table_stride, cache_block_stride, cache_entry_stride, \
1359
- dst_entry_stride, seq_starts_ptr);
1360
-
1415
// Gather sequences from the cache into the destination tensor.
1416
// - cu_seq_lens contains the cumulative sequence lengths for each batch
1417
// - block_table contains the cache block indices for each sequence
@@ -1401,33 +1455,70 @@ void cp_gather_cache(
1455
"src_cache and seq_starts must be on the same device");
1456
}
1457
1404
- int64_t block_table_stride = block_table.stride(0);
1405
- int64_t cache_block_stride = src_cache.stride(0);
1406
- int64_t cache_entry_stride = src_cache.stride(1);
1407
- int64_t dst_entry_stride = dst.stride(0);
1408
-
1409
- // Decide on the number of splits based on the batch size.
1410
- int num_splits = batch_size > 128 ? 2 : batch_size > 64 ? 4 : 16;
1411
- dim3 grid(batch_size, num_splits);
1412
- dim3 block(1024);
1413
-
1458
STD_TORCH_CHECK(src_cache.scalar_type() == dst.scalar_type(),
1459
"src_cache and dst must have the same dtype");
1460
+ const int32_t element_size = src_cache.element_size();
1461
+ STD_TORCH_CHECK(element_size == 1 || element_size == 2 || element_size == 4,
1462
+ "Unsupported data type width: ", element_size * 8);
1463
+ STD_TORCH_CHECK(batch_size == cu_seq_lens.size(0) - 1,
1464
+ "batch_size must match cu_seq_lens");
1465
+
1466
+ const int32_t total_tokens = dst.size(0);
1467
+ if (total_tokens == 0) {
1468
+ return;
1469
+ }
1470
1417
- const int dtype_bits = src_cache.element_size() * 8;
1471
+ const int32_t entry_size_bytes = entry_size * element_size;
1472
+ const int64_t block_table_stride = block_table.stride(0);
1473
+ const int64_t cache_block_stride = src_cache.stride(0) * element_size;
1474
+ const int64_t cache_entry_stride = src_cache.stride(1) * element_size;
1475
+ const int64_t dst_entry_stride = dst.stride(0) * element_size;
1476
const int32_t* seq_starts_ptr =
1477
seq_starts.has_value() ? seq_starts.value().const_data_ptr<int32_t>()
1478
: nullptr;
1479
1422
- if (dtype_bits == 32) {
1423
- CALL_CP_GATHER_CACHE(uint32_t);
1424
- } else if (dtype_bits == 16) {
1425
- CALL_CP_GATHER_CACHE(uint16_t);
1426
- } else if (dtype_bits == 8) {
1427
- CALL_CP_GATHER_CACHE(uint8_t);
1480
+ constexpr std::uintptr_t vector_alignment = alignof(int4);
1481
+ const bool vectorized =
1482
+ reinterpret_cast<std::uintptr_t>(src_cache.data_ptr()) %
1483
+ vector_alignment ==
1484
+ 0 &&
1485
+ reinterpret_cast<std::uintptr_t>(dst.data_ptr()) % vector_alignment ==
1486
+ 0 &&
1487
+ entry_size_bytes % vector_alignment == 0 &&
1488
+ cache_block_stride % vector_alignment == 0 &&
1489
+ cache_entry_stride % vector_alignment == 0 &&
1490
+ dst_entry_stride % vector_alignment == 0;
1491
+ const bool contiguous_entries = cache_entry_stride == entry_size_bytes &&
1492
+ dst_entry_stride == entry_size_bytes;
1493
+
1494
+ const int32_t num_reqs = static_cast<int32_t>(batch_size);
1495
+ const int32_t required_blocks =
1496
+ cuda_utils::ceil_div(total_tokens, block_size) + 2 * num_reqs;
1497
+ const dim3 grid(required_blocks);
1498
+ const dim3 block(256);
1499
+
1500
+#define CALL_CP_GATHER_CACHE(CONTIGUOUS, VECTORIZED) \
1501
+ vllm::cp_gather_cache_page<CONTIGUOUS, VECTORIZED> \
1502
+ <<<grid, block, 0, stream>>>( \
1503
+ reinterpret_cast<const uint8_t*>(src_cache.data_ptr()), \
1504
+ reinterpret_cast<uint8_t*>(dst.data_ptr()), \
1505
+ block_table.const_data_ptr<int32_t>(), \
1506
+ cu_seq_lens.const_data_ptr<int32_t>(), num_reqs, block_size, \
1507
+ entry_size_bytes, total_tokens, block_table_stride, \
1508
+ cache_block_stride, cache_entry_stride, dst_entry_stride, \
1509
+ seq_starts_ptr)
1510
+
1511
+ if (contiguous_entries && vectorized) {
1512
+ CALL_CP_GATHER_CACHE(true, true);
1513
+ } else if (contiguous_entries) {
1514
+ CALL_CP_GATHER_CACHE(true, false);
1515
+ } else if (vectorized) {
1516
+ CALL_CP_GATHER_CACHE(false, true);
1517
} else {
1429
- STD_TORCH_CHECK(false, "Unsupported data type width: ", dtype_bits);
1518
+ CALL_CP_GATHER_CACHE(false, false);
1519
}
1520
+
1521
+#undef CALL_CP_GATHER_CACHE
1522
}
1523
1524
void cp_gather_and_upconvert_fp8_kv_cache(
@@ -1491,15 +1582,18 @@ void cp_gather_and_upconvert_fp8_kv_cache(
1582
}
1583
1584
const int total_tokens = dst.size(0);
1494
- constexpr int warps_per_block = 8;
1495
- const int grid_size = (total_tokens + warps_per_block - 1) / warps_per_block;
1496
- const int block_size_threads = warps_per_block * 32; // 256 threads
1585
+ if (total_tokens == 0) {
1586
+ return;
1587
+ }
1588
+ constexpr int warps_per_block = 16;
1589
+ const int block_size_threads = warps_per_block * 32;
1590
const int32_t* seq_starts_ptr =
1591
seq_starts.has_value() ? seq_starts.value().const_data_ptr<int32_t>()
1592
: nullptr;
1500
-
1501
- vllm::cp_gather_and_upconvert_fp8_kv_cache<<<grid_size, block_size_threads, 0,
1502
- stream>>>(
1593
+ const int required_blocks = cuda_utils::ceil_div(total_tokens, block_size) +
1594
+ 2 * static_cast<int32_t>(batch_size);
1595
+ vllm::cp_gather_and_upconvert_fp8_kv_cache_page<<<
1596
+ required_blocks, block_size_threads, 0, stream>>>(
1597
src_ptr, reinterpret_cast<__nv_bfloat16*>(dst.data_ptr()),
1598
block_table.const_data_ptr<int32_t>(),
1599
workspace_starts.const_data_ptr<int32_t>(),
tests/kernels/attention/test_cache.py
+179
@@ -1152,6 +1152,71 @@ def test_gather_and_maybe_dequant_cache_mla_with_seq_starts(
1152
torch.testing.assert_close(dst, expected)
1153
1154
1155
+@pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8"])
1156
+@pytest.mark.parametrize("use_seq_starts", [False, True])
1157
+@pytest.mark.parametrize("device", CUDA_DEVICES)
1158
+@torch.inference_mode()
1159
+def test_gather_and_maybe_dequant_cache_mla_large_uneven_sequences(
1160
+ kv_cache_dtype,
1161
+ use_seq_starts,
1162
+ device,
1163
+):
1164
+ block_size = 64
1165
+ entry_size = 576
1166
+ num_blocks = 1024
1167
+ dtype = torch.bfloat16
1168
+ scale = torch.tensor(0.1, dtype=torch.float32, device=device)
1169
+ src_cache = _create_mla_cache(
1170
+ num_blocks, block_size, entry_size, dtype, kv_cache_dtype, device
1171
+ )
1172
+ _fill_mla_cache(src_cache, kv_cache_dtype=kv_cache_dtype)
1173
+
1174
+ starts = torch.tensor([3, 17, 5], dtype=torch.int32, device=device)
1175
+ seq_starts = starts if use_seq_starts else None
1176
+ seq_lens = torch.tensor([17, 32_768, 71], dtype=torch.int32, device=device)
1177
+ batch_size = seq_lens.shape[0]
1178
+ total_tokens = seq_lens.sum().item()
1179
+ cu_seq_lens = torch.zeros(batch_size + 1, dtype=torch.int32, device=device)
1180
+ cu_seq_lens[1:] = seq_lens.cumsum(dim=0)
1181
+ token_to_seq = torch.repeat_interleave(
1182
+ torch.arange(batch_size, dtype=torch.int32, device=device), seq_lens
1183
+ )
1184
+ block_table = torch.stack(
1185
+ [torch.randperm(num_blocks, device=device) for _ in range(batch_size)]
1186
+ ).to(torch.int32)
1187
+
1188
+ if kv_cache_dtype == "fp8":
1189
+ dequant_src_cache = torch.empty_like(src_cache, dtype=dtype)
1190
+ ops.convert_fp8(dequant_src_cache, src_cache, scale.item())
1191
+ else:
1192
+ dequant_src_cache = src_cache
1193
+ expected_batches = []
1194
+ for req_id in range(batch_size):
1195
+ start = starts[req_id].item() if use_seq_starts else 0
1196
+ source_tokens = torch.arange(
1197
+ start, start + seq_lens[req_id].item(), device=device
1198
+ )
1199
+ physical_blocks = block_table[req_id, source_tokens // block_size].long()
1200
+ expected_batches.append(
1201
+ dequant_src_cache[physical_blocks, source_tokens % block_size]
1202
+ )
1203
+ expected = torch.cat(expected_batches)
1204
+ dst = torch.empty_like(expected)
1205
+
1206
+ ops.gather_and_maybe_dequant_cache(
1207
+ src_cache,
1208
+ dst,
1209
+ block_table,
1210
+ cu_seq_lens,
1211
+ token_to_seq,
1212
+ total_tokens,
1213
+ kv_cache_dtype,
1214
+ scale,
1215
+ seq_starts,
1216
+ )
1217
+ torch.testing.assert_close(dst, expected)
1218
+
1219
+
1220
@pytest.mark.parametrize("kv_lora_rank", [512])
1221
@pytest.mark.parametrize("qk_rope_head_dim", [64])
1222
@pytest.mark.parametrize("block_size", [16])
@@ -1228,6 +1293,120 @@ def test_cp_gather_cache_mla(
1293
torch.testing.assert_close(dst, expected)
1294
1295
1296
+@pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8"])
1297
+@pytest.mark.parametrize("unaligned", [False, True])
1298
+@pytest.mark.parametrize("device", CUDA_DEVICES)
1299
+@torch.inference_mode()
1300
+def test_cp_gather_cache_mla_with_seq_starts(
1301
+ kv_cache_dtype,
1302
+ unaligned,
1303
+ device,
1304
+):
1305
+ block_size = 16
1306
+ entry_size = 576
1307
+ num_blocks = 128
1308
+ src_storage = _create_mla_cache(
1309
+ num_blocks,
1310
+ block_size,
1311
+ entry_size + int(unaligned),
1312
+ torch.bfloat16,
1313
+ kv_cache_dtype,
1314
+ device,
1315
+ )
1316
+ src_cache = src_storage[..., 1:] if unaligned else src_storage
1317
+ _fill_mla_cache(src_cache, kv_cache_dtype=kv_cache_dtype)
1318
+
1319
+ seq_starts = torch.tensor([3, 17, 5, 31], dtype=torch.int32, device=device)
1320
+ seq_lens = torch.tensor([0, 1, 63, 4], dtype=torch.int32, device=device)
1321
+ batch_size = seq_lens.shape[0]
1322
+ cu_seq_lens = torch.zeros(batch_size + 1, dtype=torch.int32, device=device)
1323
+ cu_seq_lens[1:] = seq_lens.cumsum(dim=0)
1324
+ block_table = torch.stack(
1325
+ [torch.randperm(num_blocks, device=device) for _ in range(batch_size)]
1326
+ ).to(torch.int32)
1327
+
1328
+ expected_rows = []
1329
+ for req_id in range(batch_size):
1330
+ start = seq_starts[req_id].item()
1331
+ for source_token in range(start, start + seq_lens[req_id].item()):
1332
+ block_id = block_table[req_id, source_token // block_size]
1333
+ expected_rows.append(src_cache[block_id, source_token % block_size])
1334
+ expected = torch.stack(expected_rows)
1335
+ if unaligned:
1336
+ dst_storage = torch.empty(
1337
+ (expected.shape[0], entry_size + 1),
1338
+ dtype=expected.dtype,
1339
+ device=device,
1340
+ )
1341
+ dst = dst_storage[:, 1:]
1342
+ else:
1343
+ dst = torch.empty_like(expected)
1344
+
1345
+ ops.cp_gather_cache(
1346
+ src_cache,
1347
+ dst,
1348
+ block_table,
1349
+ cu_seq_lens,
1350
+ batch_size,
1351
+ seq_starts,
1352
+ )
1353
+ torch.testing.assert_close(dst, expected)
1354
+
1355
+
1356
+@pytest.mark.parametrize("kv_cache_dtype", ["auto", "fp8"])
1357
+@pytest.mark.parametrize("use_seq_starts", [False, True])
1358
+@pytest.mark.parametrize("device", CUDA_DEVICES)
1359
+@torch.inference_mode()
1360
+def test_cp_gather_cache_mla_large_uneven_sequences(
1361
+ kv_cache_dtype,
1362
+ use_seq_starts,
1363
+ device,
1364
+):
1365
+ block_size = 64
1366
+ entry_size = 576
1367
+ num_blocks = 1024
1368
+ src_cache = _create_mla_cache(
1369
+ num_blocks,
1370
+ block_size,
1371
+ entry_size,
1372
+ torch.bfloat16,
1373
+ kv_cache_dtype,
1374
+ device,
1375
+ )
1376
+ _fill_mla_cache(src_cache, kv_cache_dtype=kv_cache_dtype)
1377
+
1378
+ starts = torch.tensor([3, 17, 5], dtype=torch.int32, device=device)
1379
+ seq_starts = starts if use_seq_starts else None
1380
+ seq_lens = torch.tensor([17, 32_768, 71], dtype=torch.int32, device=device)
1381
+ batch_size = seq_lens.shape[0]
1382
+ cu_seq_lens = torch.zeros(batch_size + 1, dtype=torch.int32, device=device)
1383
+ cu_seq_lens[1:] = seq_lens.cumsum(dim=0)
1384
+ block_table = torch.stack(
1385
+ [torch.randperm(num_blocks, device=device) for _ in range(batch_size)]
1386
+ ).to(torch.int32)
1387
+
1388
+ expected_batches = []
1389
+ for req_id in range(batch_size):
1390
+ start = starts[req_id].item() if use_seq_starts else 0
1391
+ source_tokens = torch.arange(
1392
+ start, start + seq_lens[req_id].item(), device=device
1393
+ )
1394
+ physical_blocks = block_table[req_id, source_tokens // block_size].long()
1395
+ expected_batches.append(src_cache[physical_blocks, source_tokens % block_size])
1396
+ expected = torch.cat(expected_batches)
1397
+ dst = torch.empty_like(expected)
1398
+
1399
+ ops.cp_gather_cache(
1400
+ src_cache,
1401
+ dst,
1402
+ block_table,
1403
+ cu_seq_lens,
1404
+ batch_size,
1405
+ seq_starts,
1406
+ )
1407
+ torch.testing.assert_close(dst, expected)
1408
+
1409
+
1410
@pytest.mark.parametrize("kv_lora_rank", KV_LORA_RANKS)
1411
@pytest.mark.parametrize("qk_rope_head_dim", QK_ROPE_HEAD_DIMS)
1412
@pytest.mark.parametrize("num_tokens", NUM_TOKENS_MLA)
tests/kernels/test_cp_gather_fp8.py
+54
@@ -411,3 +411,57 @@ def test_cp_gather_fp8_large_seqlens(seq_lens, block_size):
411
dst[:, :NOPE_DIM], expected[:, :NOPE_DIM], atol=1e-3, rtol=1e-2
412
)
413
assert torch.equal(dst[:, NOPE_DIM:], expected[:, NOPE_DIM:])
414
+
415
+
416
+def test_cp_gather_fp8_large_uneven_sequences_with_starts():
417
+ """Gather uneven long request slices through the page-oriented path."""
418
+ gather_seq_lens = [17, 32_768, 71]
419
+ seq_starts = [3, 17, 5]
420
+ full_seq_lens = [
421
+ start + length for start, length in zip(seq_starts, gather_seq_lens)
422
+ ]
423
+ (
424
+ cache,
425
+ block_table,
426
+ full_workspace_starts,
427
+ num_reqs,
428
+ _total_tokens,
429
+ full_expected,
430
+ ) = _build_test_case_fast(full_seq_lens, block_size=64)
431
+
432
+ workspace_starts = torch.zeros(num_reqs, dtype=torch.int32, device="cuda")
433
+ workspace_starts[1:] = torch.tensor(
434
+ gather_seq_lens[:-1], dtype=torch.int32, device="cuda"
435
+ ).cumsum(dim=0)
436
+ seq_starts_t = torch.tensor(seq_starts, dtype=torch.int32, device="cuda")
437
+ dst = torch.empty(
438
+ sum(gather_seq_lens),
439
+ NOPE_DIM + ROPE_DIM,
440
+ dtype=torch.bfloat16,
441
+ device="cuda",
442
+ )
443
+
444
+ ops.cp_gather_and_upconvert_fp8_kv_cache(
445
+ cache,
446
+ dst,
447
+ block_table,
448
+ workspace_starts,
449
+ num_reqs,
450
+ seq_starts_t,
451
+ )
452
+
453
+ expected = torch.cat(
454
+ [
455
+ full_expected[
456
+ full_workspace_starts[req_id]
457
+ + seq_starts[req_id] : full_workspace_starts[req_id]
458
+ + seq_starts[req_id]
459
+ + gather_seq_lens[req_id]
460
+ ]
461
+ for req_id in range(num_reqs)
462
+ ]
463
+ )
464
+ torch.testing.assert_close(
465
+ dst[:, :NOPE_DIM], expected[:, :NOPE_DIM], atol=1e-3, rtol=1e-2
466
+ )
467
+ assert torch.equal(dst[:, NOPE_DIM:], expected[:, NOPE_DIM:])
vllm/model_executor/layers/attention/mla_attention.py
+2
-2
@@ -2574,7 +2574,7 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
2574
# FP8 path: gather cache without dequantization
2575
ops.cp_gather_cache(
2576
src_cache=kv_c_and_k_pe_cache,
2577
- dst=workspace,
2577
+ dst=workspace[:toks],
2578
block_table=block_table,
2579
cu_seq_lens=chunk.cu_seq_lens,
2580
batch_size=chunk.num_requests,
@@ -2683,7 +2683,7 @@ class MLACommonBaseImpl(MLAAttentionImpl[A], Generic[A]):
2683
else:
2684
ops.cp_gather_cache(
2685
src_cache=kv_c_and_k_pe_cache,
2686
- dst=workspace,
2686
+ dst=workspace[:toks],
2687
block_table=block_table,
2688
cu_seq_lens=padded_local_cu_seq_lens,
2689
batch_size=chunk.num_requests,