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 5, 09:45 UTC
[Refactor] Improve indexer decode path metadata preparation (#38865)
Yongye Zhu committed
Apr 8, 2026 at 23:49 UTC
2e98406048798c45eb623360de2b46825ef74848
4 files changed
+162
-102
csrc/sampler.cu
+24
-9
@@ -564,8 +564,9 @@ template <int kNumThreadsPerBlock, bool useRadixSort,
564
bool multipleBlocksPerRow = false, bool mergeBlocks = false>
565
static __global__ __launch_bounds__(kNumThreadsPerBlock) void topKPerRowDecode(
566
const float* logits, const int* seqLens, int* outIndices, int stride0,
567
- int stride1, const int topK, int next_n, float* outLogits = nullptr,
568
- const int numBlocksToMerge = 0, const int* indices = nullptr) {
567
+ int stride1, const int topK, int next_n, int seqLensIs2D = 0,
568
+ float* outLogits = nullptr, const int numBlocksToMerge = 0,
569
+ const int* indices = nullptr) {
570
// The number of bins in the histogram.
571
static constexpr int kNumBins = 2048;
572
@@ -574,8 +575,16 @@ static __global__ __launch_bounds__(kNumThreadsPerBlock) void topKPerRowDecode(
575
576
// The range of logits within the row.
577
int rowStart = 0;
577
- int seq_len = seqLens[rowIdx / next_n];
578
- int rowEnd = max(0, seq_len - next_n + (rowIdx % next_n) + 1);
578
+ int batch_idx = rowIdx / next_n;
579
+ int next_n_idx = rowIdx % next_n;
580
+ // seqLensIs2D=0: 1D seqLens — all rows in a batch share the same seq_len;
581
+ // kernel computes per-row effective length via offset.
582
+ // seqLensIs2D=1: 2D seqLens — each logit row has its own pre-computed
583
+ // effective length (flat index rowIdx = b*next_n + j maps
584
+ // directly to seqLens[b, j] in C-contiguous layout).
585
+ int seq_len = seqLensIs2D ? seqLens[rowIdx] : seqLens[batch_idx];
586
+ int rowEnd =
587
+ seqLensIs2D ? max(0, seq_len) : max(0, seq_len - next_n + next_n_idx + 1);
588
589
// Local pointers to this block
590
if constexpr (!multipleBlocksPerRow && !mergeBlocks) {
@@ -653,6 +662,11 @@ void top_k_per_row_decode(const torch::Tensor& logits, int64_t next_n,
662
const cudaStream_t stream = at::cuda::getCurrentCUDAStream();
663
const auto numColumns = logits.size(1);
664
665
+ // True if seqLens is 2D (B, next_n): each logit row has its own pre-computed
666
+ // effective seq_len. False if seqLens is 1D (B,): all rows in a batch share
667
+ // the same seq_len and the kernel computes the per-row offset itself.
668
+ int seqLensIs2D = seqLens.dim() == 2 ? 1 : 0;
669
+
670
if (numColumns < kSortingAlgorithmThreshold) {
671
// Use insertion sort
672
vllm::topKPerRowDecode<kNumThreadsPerBlock, false>
@@ -660,7 +674,7 @@ void top_k_per_row_decode(const torch::Tensor& logits, int64_t next_n,
674
logits.data_ptr<float>(), seqLens.data_ptr<int>(),
675
indices.data_ptr<int>(), static_cast<int>(stride0),
676
static_cast<int>(stride1), static_cast<int>(topK),
663
- static_cast<int>(next_n));
677
+ static_cast<int>(next_n), seqLensIs2D);
678
} else if (numColumns < kSplitWorkThreshold) {
679
// From this threshold, use radix sort instead
680
vllm::topKPerRowDecode<kNumThreadsPerBlock, true>
@@ -668,7 +682,7 @@ void top_k_per_row_decode(const torch::Tensor& logits, int64_t next_n,
682
logits.data_ptr<float>(), seqLens.data_ptr<int>(),
683
indices.data_ptr<int>(), static_cast<int>(stride0),
684
static_cast<int>(stride1), static_cast<int>(topK),
671
- static_cast<int>(next_n));
685
+ static_cast<int>(next_n), seqLensIs2D);
686
} else {
687
// Long sequences are run in two steps
688
constexpr auto multipleBlocksPerRowConfig = 10;
@@ -686,15 +700,16 @@ void top_k_per_row_decode(const torch::Tensor& logits, int64_t next_n,
700
logits.data_ptr<float>(), seqLens.data_ptr<int>(),
701
outIndicesAux.data_ptr<int>(), static_cast<int>(stride0),
702
static_cast<int>(stride1), static_cast<int>(topK),
689
- static_cast<int>(next_n), outLogitsAux.data_ptr<float>());
703
+ static_cast<int>(next_n), seqLensIs2D,
704
+ outLogitsAux.data_ptr<float>());
705
706
constexpr int kNumThreadsPerBlockMerge = 1024;
707
vllm::topKPerRowDecode<kNumThreadsPerBlockMerge, true, false, true>
708
<<<numRows, kNumThreadsPerBlockMerge, topK * sizeof(int32_t), stream>>>(
709
outLogitsAux.data_ptr<float>(), seqLens.data_ptr<int>(),
710
indices.data_ptr<int>(), multipleBlocksPerRowConfig * topK, 1,
696
- static_cast<int>(topK), static_cast<int>(next_n), nullptr,
697
- multipleBlocksPerRowConfig, outIndicesAux.data_ptr<int>());
711
+ static_cast<int>(topK), static_cast<int>(next_n), seqLensIs2D,
712
+ nullptr, multipleBlocksPerRowConfig, outIndicesAux.data_ptr<int>());
713
}
714
}
715
csrc/topk.cu
+4
-2
@@ -21,13 +21,15 @@ void persistent_topk(const torch::Tensor& logits, const torch::Tensor& lengths,
21
TORCH_CHECK(lengths.dtype() == torch::kInt32, "lengths must be int32");
22
TORCH_CHECK(output.dtype() == torch::kInt32, "output must be int32");
23
TORCH_CHECK(logits.dim() == 2, "logits must be 2D");
24
- TORCH_CHECK(lengths.dim() == 1, "lengths must be 1D");
24
+ TORCH_CHECK(lengths.dim() == 1 || lengths.dim() == 2,
25
+ "lengths must be 1D or 2D");
26
+ TORCH_CHECK(lengths.is_contiguous(), "lengths must be contiguous");
27
TORCH_CHECK(output.dim() == 2, "output must be 2D");
28
29
const int64_t num_rows = logits.size(0);
30
const int64_t stride = logits.size(1);
31
30
- TORCH_CHECK(lengths.size(0) == num_rows, "lengths size mismatch");
32
+ TORCH_CHECK(lengths.numel() == num_rows, "lengths size mismatch");
33
TORCH_CHECK(output.size(0) == num_rows && output.size(1) == k,
34
"output size mismatch");
35
namespace P = vllm::persistent;
vllm/model_executor/layers/sparse_attn_indexer.py
+7
-16
@@ -183,13 +183,15 @@ def sparse_attn_indexer(
183
# TODO: move and optimize below logic with triton kernels
184
batch_size = padded_q_fp8_decode_tokens.shape[0]
185
next_n = padded_q_fp8_decode_tokens.shape[1]
186
- assert batch_size == decode_metadata.seq_lens.shape[0]
186
num_padded_tokens = batch_size * next_n
187
+ seq_lens = decode_metadata.seq_lens[:batch_size]
188
+ # seq_lens is (B, next_n) for native spec decode, (B,) otherwise.
189
+ # fp8_paged_mqa_logits and all topk kernels accept both shapes.
190
logits = fp8_paged_mqa_logits(
191
padded_q_fp8_decode_tokens,
192
kv_cache,
193
weights[:num_padded_tokens],
192
- decode_metadata.seq_lens,
194
+ seq_lens,
195
decode_metadata.block_table,
196
decode_metadata.schedule_metadata,
197
max_model_len=max_model_len,
@@ -198,17 +200,6 @@ def sparse_attn_indexer(
200
num_rows = logits.shape[0]
201
topk_indices = topk_indices_buffer[:num_padded_tokens, :topk_tokens]
202
201
- if next_n == 1:
202
- lengths = decode_metadata.seq_lens
203
- else:
204
- # (bs,) -> (bs, 1) + (next_n,) -> (bs, next_n) -> (bs * next_n,)
205
- lengths = (
206
- decode_metadata.seq_lens.unsqueeze(1)
207
- - next_n
208
- + 1
209
- + decode_metadata.offsets
210
- ).flatten()
211
-
203
if current_platform.is_cuda():
204
workspace_manager = current_workspace_manager()
205
(topk_workspace,) = workspace_manager.get_simultaneous(
@@ -216,7 +207,7 @@ def sparse_attn_indexer(
207
)
208
torch.ops._C.persistent_topk(
209
logits,
219
- lengths,
210
+ decode_metadata.seq_lens,
211
topk_indices,
212
topk_workspace,
213
topk_tokens,
@@ -227,7 +218,7 @@ def sparse_attn_indexer(
218
ops.top_k_per_row_decode(
219
logits,
220
next_n,
230
- decode_metadata.seq_lens,
221
+ seq_lens,
222
topk_indices,
223
num_rows,
224
logits.stride(0),
@@ -238,7 +229,7 @@ def sparse_attn_indexer(
229
torch.ops._C.top_k_per_row_decode(
230
logits,
231
next_n,
241
- decode_metadata.seq_lens,
232
+ seq_lens,
233
topk_indices,
234
num_rows,
235
logits.stride(0),
vllm/v1/attention/backends/mla/indexer.py
+127
-75
@@ -141,11 +141,14 @@ class DeepseekV32IndexerPrefillMetadata:
141
@dataclass
142
class DeepSeekV32IndexerDecodeMetadata:
143
block_table: torch.Tensor
144
+ # seq_lens: per-token effective context lengths.
145
+ # - flatten path / plain decode: 1D (batch_size,)
146
+ # - native MTP path: 2D (B, next_n) where [b,j] = L_b - next_n + j + 1
147
+ # Both fp8_paged_mqa_logits and the topk kernels accept both shapes.
148
seq_lens: torch.Tensor
149
decode_lens: torch.Tensor
150
requires_padding: bool
151
schedule_metadata: torch.Tensor
148
- offsets: torch.Tensor | None # Precomputed offsets for speculative decoding
152
153
154
@dataclass
@@ -283,21 +286,31 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
286
sm_count = num_compute_units(self.device.index)
287
self.num_sms = sm_count
288
286
- self.decode_lens_buffer = torch.empty(
287
- (scheduler_config.max_num_batched_tokens,),
288
- dtype=torch.int32,
289
- device=self.device,
290
- )
289
self.offsets_buffer = torch.arange(
290
next_n, device=self.device, dtype=torch.int32
291
)
294
- self.arange_buffer = torch.arange(
295
- scheduler_config.max_num_seqs * next_n,
292
+ self.decode_lens_buffer = torch.zeros(
293
+ (scheduler_config.max_num_batched_tokens,),
294
dtype=torch.int32,
295
device=self.device,
296
)
299
- self.expanded_seq_lens_buffer = torch.zeros(
300
- (scheduler_config.max_num_batched_tokens,),
297
+ if not self.use_flattening and next_n > 1:
298
+ # Native MTP: 2D buffer for per-token seq_lens.
299
+ # Flattening path is never used, so no expanded_seq_lens_buffer.
300
+ self.decode_seq_lens_buffer = torch.zeros(
301
+ (scheduler_config.max_num_seqs, next_n),
302
+ dtype=torch.int32,
303
+ device=self.device,
304
+ )
305
+ else:
306
+ # Flattening or no MTP: 1D buffer for expanded per-token seq_lens.
307
+ self.decode_seq_lens_buffer = torch.zeros(
308
+ (scheduler_config.max_num_batched_tokens,),
309
+ dtype=torch.int32,
310
+ device=self.device,
311
+ )
312
+ self.arange_buffer = torch.arange(
313
+ scheduler_config.max_num_seqs * next_n,
314
dtype=torch.int32,
315
device=self.device,
316
)
@@ -367,6 +380,96 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
380
skip_kv_gather=skip_kv_gather,
381
)
382
383
+ def _prepare_decode_tensors(
384
+ self,
385
+ seq_lens: torch.Tensor,
386
+ block_table: torch.Tensor,
387
+ decode_lens: torch.Tensor,
388
+ decode_lens_cpu: torch.Tensor,
389
+ query_start_loc: torch.Tensor,
390
+ num_decodes: int,
391
+ num_decode_tokens: int,
392
+ use_native: bool,
393
+ next_n: int,
394
+ max_decode_len: int,
395
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, int, bool]:
396
+ """Expand seq_lens/block_table/decode_lens for the decode kernels.
397
+
398
+ Flatten path (not use_native, max_decode_len > 1):
399
+ Each multi-token decode request is expanded into individual
400
+ single-token entries so the kernel always sees next_n=1.
401
+
402
+ Native path (use_native or max_decode_len == 1):
403
+ Plain decode or spec-decode with 2D per-token context lengths.
404
+
405
+ Returns (seq_lens, block_table, decode_lens, batch_size, requires_padding).
406
+ seq_lens is 1D (batch_size,) for flatten/plain, 2D (B, next_n) for native MTP.
407
+ """
408
+ if not use_native and max_decode_len > 1:
409
+ assert self.decode_seq_lens_buffer.dim() == 1
410
+ # Assume 4 requests with seq_lens [10, 7, 12, 0] (the final req is
411
+ # padding) and decode_lens [3, 1, 4, 0] in the below example comments.
412
+ # The context lengths are therefore
413
+ # [10-3, 7-1, 12-4, 0-0] = [7, 6, 8, 0].
414
+
415
+ # 3 + 1 + 4 + 0 = 8
416
+ actual_expanded = int(decode_lens_cpu.sum().item())
417
+
418
+ # Fuse expanded_base and expanded_starts into a single repeat_interleave:
419
+ # seq_len_i = (context_start[b] - query_start_loc[b]) + arange[i] + 1
420
+ # where context_start[b] = seq_lens[b] - decode_lens[b].
421
+ # Example: offsets = [7-0, 6-3, 8-4, 0-8] = [7, 3, 4, -8]
422
+ # expanded_offsets = [7, 7, 7, 3, 4, 4, 4, 4]
423
+ # result = [8, 9, 10, 7, 9, 10, 11, 12]
424
+ expanded_offsets = torch.repeat_interleave(
425
+ seq_lens - decode_lens - query_start_loc,
426
+ decode_lens,
427
+ output_size=actual_expanded,
428
+ )
429
+
430
+ # [8, 9, 10, 7, 9, 10, 11, 12, ...] where ... is unused buffer space
431
+ self.decode_seq_lens_buffer[:actual_expanded] = (
432
+ expanded_offsets + self.arange_buffer[:actual_expanded] + 1
433
+ )
434
+ self.decode_seq_lens_buffer[actual_expanded:] = 0
435
+ seq_lens = self.decode_seq_lens_buffer[:num_decode_tokens]
436
+
437
+ # Give each of the flattened entries the same block table row as the
438
+ # original request.
439
+ self.expanded_block_table_buffer[:actual_expanded] = (
440
+ torch.repeat_interleave(
441
+ block_table, decode_lens, dim=0, output_size=actual_expanded
442
+ )
443
+ )
444
+ if actual_expanded < num_decode_tokens:
445
+ self.expanded_block_table_buffer[
446
+ actual_expanded:num_decode_tokens, 0
447
+ ] = 0
448
+ block_table = self.expanded_block_table_buffer[:num_decode_tokens]
449
+
450
+ # All reqs now have decode_len=1
451
+ self.decode_lens_buffer[:num_decode_tokens] = 1
452
+ decode_lens = self.decode_lens_buffer[:num_decode_tokens]
453
+ return seq_lens, block_table, decode_lens, num_decode_tokens, False
454
+ else:
455
+ # Native path: plain decode (next_n==1) or spec decode
456
+ # with 2D per-token context lengths (next_n > 1).
457
+ #
458
+ # When decode_lens are not truly uniform (e.g. some requests have
459
+ # decode_len < next_n due to padding or short prefills), the simple
460
+ # reshape in sparse_attn_indexer won't work. Use pack_seq_triton
461
+ # (requires_padding) instead.
462
+ min_decode_len = int(decode_lens_cpu.min().item())
463
+ requires_padding = min_decode_len != max_decode_len
464
+ if use_native and next_n > 1:
465
+ assert self.decode_seq_lens_buffer.dim() == 2
466
+ # (B, next_n): token j attends to L - next_n + j + 1 KV tokens
467
+ self.decode_seq_lens_buffer[:num_decodes] = (
468
+ seq_lens.unsqueeze(1) - next_n + 1 + self.offsets_buffer
469
+ )
470
+ seq_lens = self.decode_seq_lens_buffer[:num_decodes]
471
+ return seq_lens, block_table, decode_lens, num_decodes, requires_padding
472
+
473
def build(
474
self,
475
common_prefix_len: int,
@@ -434,68 +537,20 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
537
next_n = 1 + self.num_speculative_tokens
538
use_native = not self.use_flattening and max_decode_len == next_n
539
437
- if use_native and next_n > 1:
438
- offsets = self.offsets_buffer
439
- elif max_decode_len > 1:
440
- # Flatten multi-token decode requests into single-token
441
- # batch entries, expanding seq_lens and block tables so
442
- # the kernel always sees next_n=1.
443
-
444
- # Also handles the edge case where use_flattening=False
445
- # but max_decode_len != next_n (e.g. a batch containing some
446
- # short prefills (q_len < next_n) and no true decodes).
447
-
448
- # Assume 4 requests with seq_lens [10, 7, 12, 0] (the final req is
449
- # padding) and decode_lens [3, 1, 4, 0] in the below example comments.
450
- # The context lengths are therefore
451
- # [10-3, 7-1, 12-4, 0-0] = [7, 6, 8, 0].
452
-
453
- # 3 + 1 + 4 + 0 = 8
454
- actual_expanded = int(decode_lens_cpu.sum().item())
455
-
456
- # [7, 6, 8, 0] -> [7, 7, 7, 6, 8, 8, 8, 8]
457
- expanded_base = torch.repeat_interleave(
458
- seq_lens - decode_lens, decode_lens, output_size=actual_expanded
459
- )
460
-
461
- # [0, 3, 4, 8] -> [0, 0, 0, 3, 4, 4, 4, 4]
462
- expanded_starts = torch.repeat_interleave(
463
- common_attn_metadata.query_start_loc[:num_decodes],
464
- decode_lens,
465
- output_size=actual_expanded,
540
+ seq_lens, block_table, decode_lens, batch_size, requires_padding = (
541
+ self._prepare_decode_tensors(
542
+ seq_lens=seq_lens,
543
+ block_table=block_table,
544
+ decode_lens=decode_lens,
545
+ decode_lens_cpu=decode_lens_cpu,
546
+ query_start_loc=common_attn_metadata.query_start_loc[:num_decodes],
547
+ num_decodes=num_decodes,
548
+ num_decode_tokens=num_decode_tokens,
549
+ use_native=use_native,
550
+ next_n=next_n,
551
+ max_decode_len=max_decode_len,
552
)
467
-
468
- # [0, 1, 2, 0, 0, 1, 2, 3]
469
- positions_within = (
470
- self.arange_buffer[:actual_expanded] - expanded_starts
471
- )
472
-
473
- # [8, 9, 10, 7, 9, 10, 11, 12, ...] where ... is unused buffer space
474
- self.expanded_seq_lens_buffer[:actual_expanded] = (
475
- expanded_base + positions_within + 1
476
- )
477
- self.expanded_seq_lens_buffer[actual_expanded:] = 0
478
- seq_lens = self.expanded_seq_lens_buffer[:num_decode_tokens]
479
-
480
- # Give each of the flattened entries the same block table row as the
481
- # original request.
482
- self.expanded_block_table_buffer[:actual_expanded] = (
483
- torch.repeat_interleave(
484
- block_table, decode_lens, dim=0, output_size=actual_expanded
485
- )
486
- )
487
- if actual_expanded < num_decode_tokens:
488
- self.expanded_block_table_buffer[
489
- actual_expanded:num_decode_tokens, 0
490
- ] = 0
491
- block_table = self.expanded_block_table_buffer[:num_decode_tokens]
492
-
493
- # All reqs now have decode_len=1
494
- self.decode_lens_buffer[:num_decode_tokens] = 1
495
- decode_lens = self.decode_lens_buffer[:num_decode_tokens]
496
- offsets = None
497
- else:
498
- offsets = None
553
+ )
554
555
# DeepGEMM is required for the paged MQA logits on CUDA devices
556
if current_platform.is_cuda() and has_deep_gemm():
@@ -509,9 +564,8 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
564
block_table=block_table,
565
seq_lens=seq_lens,
566
decode_lens=decode_lens,
512
- requires_padding=False,
567
+ requires_padding=requires_padding,
568
schedule_metadata=self.scheduler_metadata_buffer,
514
- offsets=offsets,
569
)
570
571
attn_metadata = DeepseekV32IndexerMetadata(
@@ -531,6 +585,4 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
585
decode=decode_metadata,
586
)
587
534
- # if get_tensor_model_parallel_rank() == 0:
535
- # logger.info(f"attn_metadata: {attn_metadata}")
588
return attn_metadata