[ROCm][Perf] Optimize DSV4.1 K=512 decode top-k on gfx950 (#56743)
Signed-off-by: fai <fangzhouai@gmail.com>
Fangzhou Ai committed
Sep 15, 2026 at 14:34 UTC
dffbb714e4e8e4b95ccc888df98d47c7d2cef78d
3 files changed
+406
-58
csrc/libtorch_stable/sampler.cu
+205
-58
@@ -154,7 +154,8 @@ __device__ void vectorized_process(size_t thread_rank, size_t num_threads,
154
}
155
156
template <int step, int kNumThreadsPerBlock, int kNumBins, int kNumFinalItems,
157
- bool multipleBlocksPerRow, bool mergeBlocks, typename SmemFinalType,
157
+ bool multipleBlocksPerRow, bool mergeBlocks,
158
+ bool useTopK512Optimization, typename SmemFinalType,
159
typename SmemOutputType>
160
__device__ bool processHistogramStep(
161
const int* indices, const float* logits, int rowEnd, uint32_t& logitPattern,
@@ -180,10 +181,15 @@ __device__ bool processHistogramStep(
181
<< patternShift;
182
}
183
184
+ int negativeInfinityCount = 0;
185
auto distributeToBins = [&](float logit, int /* idx */ = 0) {
186
if (isPartialMatch<patternShift>(logit, logitPattern)) {
187
uint32_t binIdx = extractBinIdx<step>(logit);
186
- atomicAdd(&smemFinal.histo.data[binIdx], 1);
188
+ if (useTopK512Optimization && __float_as_uint(logit) == 0xff800000u) {
189
+ ++negativeInfinityCount;
190
+ } else {
191
+ atomicAdd(&smemFinal.histo.data[binIdx], 1);
192
+ }
193
}
194
};
195
@@ -198,6 +204,12 @@ __device__ bool processHistogramStep(
204
distributeToBins(logit, idx);
205
}
206
}
207
+ if constexpr (useTopK512Optimization) {
208
+ if (negativeInfinityCount > 0) {
209
+ const auto bin = extractBinIdx<step>(-INFINITY);
210
+ atomicAdd(&smemFinal.histo.data[bin], negativeInfinityCount);
211
+ }
212
+ }
213
// Make sure the histogram is ready.
214
__syncthreads();
215
@@ -254,6 +266,12 @@ __device__ bool processHistogramStep(
266
267
// The threshold bin.
268
thresholdBinIdx = smemThresholdBinIdx[0];
269
+ // Resolve a -inf cutoff with exact tie emission, keeping sort padding out.
270
+ const bool finalBinFits =
271
+ smemFinalBinSize[0] <= kNumFinalItems &&
272
+ !(useTopK512Optimization && step < 3 &&
273
+ isPartialMatch<patternShift>(-INFINITY, logitPattern) &&
274
+ thresholdBinIdx == extractBinIdx<step>(-INFINITY));
275
276
auto processBins = [&](float logit, int idx) {
277
if (isPartialMatch<patternShift>(logit, logitPattern)) {
@@ -262,8 +280,7 @@ __device__ bool processHistogramStep(
280
// 1. This is step 0 and the threshold bin is small enough (no step 1)
281
// 2. This is step >= 1 (where pattern matching filters correctly)
282
// This prevents duplicates when step 0 and step 1 both run.
265
- bool shouldWriteDirectly =
266
- (step == 0 && smemFinalBinSize[0] <= kNumFinalItems) || (step >= 1);
283
+ bool shouldWriteDirectly = (step == 0 && finalBinFits) || (step >= 1);
284
if (binIdx < thresholdBinIdx && shouldWriteDirectly) {
285
// The element is part of the top-k selection
286
int dstIdx = atomicAdd(&smemFoundTopKValues[0], 1);
@@ -279,8 +296,7 @@ __device__ bool processHistogramStep(
296
}
297
if constexpr (step < 3) {
298
// Only fill the final items for sorting if the threshold bin fits
282
- if (binIdx == thresholdBinIdx &&
283
- smemFinalBinSize[0] <= kNumFinalItems) {
299
+ if (binIdx == thresholdBinIdx && finalBinFits) {
300
int dstIdx = atomicAdd(&smemFinalDstIdx[0], 1);
301
smemFinal.items.logits[dstIdx] = logit;
302
if constexpr (mergeBlocks) {
@@ -325,20 +341,21 @@ __device__ bool processHistogramStep(
341
__syncthreads();
342
343
// Check if we should continue to next step
328
- return smemFinalBinSize[0] > kNumFinalItems;
344
+ return !finalBinFits;
345
}
346
347
// Follows half - 11 - 11 - 10 bit iterations
348
// Keep the adaptive kernel's device instantiation separate from legacy calls.
349
template <int kNumThreadsPerBlock, int kNumBins, bool useRadixSort,
350
bool multipleBlocksPerRow = false, bool mergeBlocks = false,
335
- bool deviceLengthAware = false>
351
+ bool deviceLengthAware = false, bool useTopK512Optimization = false>
352
static __device__ void topKPerRowJob(const int* indices, const float* logits,
353
int rowStart, int rowEnd, int* outIndices,
354
float* outLogits, int stride1, int topK) {
355
static_assert(!deviceLengthAware || multipleBlocksPerRow != mergeBlocks);
356
+ static_assert(!useTopK512Optimization || kNumThreadsPerBlock == 1024);
357
// The number of slots for the final pass.
341
- static constexpr int kNumFinalItems = 2048;
358
+ static constexpr int kNumFinalItems = useTopK512Optimization ? 1024 : 2048;
359
// The number of elements per thread for the final sort.
360
static constexpr int kNumFinalItemsPerThread =
361
kNumFinalItems / kNumThreadsPerBlock;
@@ -404,7 +421,7 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits,
421
rowIt += kNumThreadsPerBlock) {
422
outIndices[rowIt] = -1;
423
if constexpr (multipleBlocksPerRow) {
407
- outLogits[rowIt] = -FLT_MAX;
424
+ outLogits[rowIt] = useTopK512Optimization ? -INFINITY : -FLT_MAX;
425
}
426
}
427
@@ -422,7 +439,8 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits,
439
// Step 0: Process first 11 bits of half representation
440
bool continueToNextStep =
441
processHistogramStep<0, kNumThreadsPerBlock, kNumBins, kNumFinalItems,
425
- multipleBlocksPerRow, mergeBlocks>(
442
+ multipleBlocksPerRow, mergeBlocks,
443
+ useTopK512Optimization>(
444
indices, logits, rowEnd, logitPattern, thresholdBinIdx, smemOutput,
445
smemThresholdBinIdx, smemFinalDstIdx, smemFinalBinSize,
446
smemFoundTopKValues, smemFinal, stride1, rowStart, topK);
@@ -431,7 +449,8 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits,
449
// Step 1: Process next 11 bits
450
continueToNextStep =
451
processHistogramStep<1, kNumThreadsPerBlock, kNumBins, kNumFinalItems,
434
- multipleBlocksPerRow, mergeBlocks>(
452
+ multipleBlocksPerRow, mergeBlocks,
453
+ useTopK512Optimization>(
454
indices, logits, rowEnd, logitPattern, thresholdBinIdx, smemOutput,
455
smemThresholdBinIdx, smemFinalDstIdx, smemFinalBinSize,
456
smemFoundTopKValues, smemFinal, stride1, rowStart, topK);
@@ -441,7 +460,8 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits,
460
// Step 2: Process next 11 bits
461
continueToNextStep =
462
processHistogramStep<2, kNumThreadsPerBlock, kNumBins, kNumFinalItems,
444
- multipleBlocksPerRow, mergeBlocks>(
463
+ multipleBlocksPerRow, mergeBlocks,
464
+ useTopK512Optimization>(
465
indices, logits, rowEnd, logitPattern, thresholdBinIdx, smemOutput,
466
smemThresholdBinIdx, smemFinalDstIdx, smemFinalBinSize,
467
smemFoundTopKValues, smemFinal, stride1, rowStart, topK);
@@ -450,7 +470,8 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits,
470
if (continueToNextStep) {
471
// Step 3: Process last 10 bits
472
processHistogramStep<3, kNumThreadsPerBlock, kNumBins, kNumFinalItems,
453
- multipleBlocksPerRow, mergeBlocks>(
473
+ multipleBlocksPerRow, mergeBlocks,
474
+ useTopK512Optimization>(
475
indices, logits, rowEnd, logitPattern, thresholdBinIdx, smemOutput,
476
smemThresholdBinIdx, smemFinalDstIdx, smemFinalBinSize,
477
smemFoundTopKValues, smemFinal, stride1, rowStart, topK);
@@ -460,50 +481,7 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits,
481
// The histogram did not proceed to the final 10 bits, therefore we need to
482
// sort the final items The logits of the elements to be sorted in the final
483
// pass.
463
- if constexpr (useRadixSort) {
464
- // Sorting with radix sort
465
- float finalLogits[kNumFinalItemsPerThread];
466
- // The indices of the elements to be sorted in the final pass.
467
- int finalIndices[kNumFinalItemsPerThread];
468
-
469
-#pragma unroll
470
- for (int ii = 0; ii < kNumFinalItemsPerThread; ++ii) {
471
- finalLogits[ii] = -FLT_MAX;
472
- }
473
-
474
- // Read the elements from SMEM.
475
-#pragma unroll
476
- for (int ii = 0; ii < kNumFinalItemsPerThread; ++ii) {
477
- int srcIdx = ii * kNumThreadsPerBlock + threadIdx.x;
478
- if (srcIdx < smemFinalDstIdx[0]) {
479
- finalLogits[ii] = smemFinal.items.logits[srcIdx];
480
- finalIndices[ii] = smemFinal.items.indices[srcIdx];
481
- }
482
- }
483
- // Make sure the shared memory has been read.
484
- __syncthreads();
485
-
486
- // Sort the elements.
487
- FinalSort(smemFinal.finalSort)
488
- .SortDescendingBlockedToStriped(finalLogits, finalIndices);
489
-
490
- // Copy the data back to the shared memory storage.
491
- int baseIdx = smemFoundTopKValues[0];
492
-
493
-#pragma unroll
494
- for (int ii = 0; ii < kNumFinalItemsPerThread; ++ii) {
495
- int srcIdx = ii * kNumThreadsPerBlock + threadIdx.x;
496
- int dstIdx = baseIdx + srcIdx;
497
-
498
- if (dstIdx < topK) {
499
- smemOutput[dstIdx] = finalIndices[ii];
500
- if constexpr (multipleBlocksPerRow) {
501
- reinterpret_cast<float*>(smemOutput + topK)[dstIdx] =
502
- finalLogits[ii];
503
- }
504
- }
505
- }
506
- } else {
484
+ auto insertionSort = [&]() {
485
// Sorting with insertion sort
486
auto baseIdx = smemFoundTopKValues[0];
487
for (int i = threadIdx.x; i < smemFinalDstIdx[0];
@@ -525,6 +503,57 @@ static __device__ void topKPerRowJob(const int* indices, const float* logits,
503
}
504
}
505
}
506
+ };
507
+ if constexpr (useRadixSort) {
508
+ if (useTopK512Optimization && smemFinalDstIdx[0] <= 128) {
509
+ insertionSort();
510
+ } else {
511
+ // Sorting with radix sort
512
+ float finalLogits[kNumFinalItemsPerThread];
513
+ // The indices of the elements to be sorted in the final pass.
514
+ int finalIndices[kNumFinalItemsPerThread];
515
+
516
+#pragma unroll
517
+ for (int ii = 0; ii < kNumFinalItemsPerThread; ++ii) {
518
+ finalLogits[ii] = useTopK512Optimization ? -INFINITY : -FLT_MAX;
519
+ if constexpr (useTopK512Optimization) finalIndices[ii] = -1;
520
+ }
521
+
522
+ // Read the elements from SMEM.
523
+#pragma unroll
524
+ for (int ii = 0; ii < kNumFinalItemsPerThread; ++ii) {
525
+ int srcIdx = ii * kNumThreadsPerBlock + threadIdx.x;
526
+ if (srcIdx < smemFinalDstIdx[0]) {
527
+ finalLogits[ii] = smemFinal.items.logits[srcIdx];
528
+ finalIndices[ii] = smemFinal.items.indices[srcIdx];
529
+ }
530
+ }
531
+ // Make sure the shared memory has been read.
532
+ __syncthreads();
533
+
534
+ // Sort the elements.
535
+ FinalSort(smemFinal.finalSort)
536
+ .SortDescendingBlockedToStriped(finalLogits, finalIndices);
537
+
538
+ // Copy the data back to the shared memory storage.
539
+ int baseIdx = smemFoundTopKValues[0];
540
+
541
+#pragma unroll
542
+ for (int ii = 0; ii < kNumFinalItemsPerThread; ++ii) {
543
+ int srcIdx = ii * kNumThreadsPerBlock + threadIdx.x;
544
+ int dstIdx = baseIdx + srcIdx;
545
+
546
+ if (dstIdx < topK) {
547
+ smemOutput[dstIdx] = finalIndices[ii];
548
+ if constexpr (multipleBlocksPerRow) {
549
+ reinterpret_cast<float*>(smemOutput + topK)[dstIdx] =
550
+ finalLogits[ii];
551
+ }
552
+ }
553
+ }
554
+ }
555
+ } else {
556
+ insertionSort();
557
}
558
__syncthreads();
559
}
@@ -669,6 +698,70 @@ __launch_bounds__(kNumThreadsPerBlock) void topKPerRowDecodeDeviceLengthAware(
698
stride1, topK);
699
}
700
701
+#ifdef USE_ROCM
702
+// The grid and workspace stay fixed while graph replays change device lengths.
703
+__device__ int gfx950TopK512ActiveBlocks(int rowLength, int numRows,
704
+ int maxBlocks) {
705
+ int activeBlocks = 1;
706
+ if (numRows <= 32 && rowLength > 16 * 1024) {
707
+ activeBlocks = rowLength <= 128 * 1024 ? 8
708
+ : rowLength <= 256 * 1024 ? 10
709
+ : 16;
710
+ } else if (numRows <= 128 && rowLength > 64 * 1024) {
711
+ activeBlocks = rowLength <= 512 * 1024 ? 4 : 10;
712
+ } else if (numRows > 128 && numRows <= 256 && rowLength > 256 * 1024) {
713
+ activeBlocks = numRows <= 160 ? 3 : 2;
714
+ }
715
+ // Every block in a merged row must contain at least k real indices.
716
+ return min(min(activeBlocks, maxBlocks), max(1, rowLength / 512));
717
+}
718
+
719
+template <int kNumThreadsPerBlock, bool mergeBlocks = false>
720
+static __global__
721
+__launch_bounds__(kNumThreadsPerBlock) void topKPerRowDecode512DeviceLengthAware(
722
+ const float* logits, const int* seqLens, int* outIndices,
723
+ int* outIndicesAux, float* outLogitsAux, int stride0, int next_n,
724
+ int seqLensIs2D, int maxBlocks) {
725
+ #if defined(__gfx950__)
726
+ constexpr int topK = 512;
727
+ constexpr int kNumBins = 2048;
728
+ const int rowIdx = blockIdx.x;
729
+ const int rowEnd =
730
+ seqLensIs2D
731
+ ? max(0, seqLens[rowIdx])
732
+ : max(0, seqLens[rowIdx / next_n] - next_n + rowIdx % next_n + 1);
733
+ const int activeBlocks =
734
+ gfx950TopK512ActiveBlocks(rowEnd, gridDim.x, maxBlocks);
735
+ outIndices += static_cast<int64_t>(rowIdx) * topK;
736
+ if constexpr (mergeBlocks) {
737
+ if (activeBlocks == 1) return;
738
+ const int64_t offset = static_cast<int64_t>(rowIdx) * maxBlocks * topK;
739
+ topKPerRowJob<kNumThreadsPerBlock, kNumBins, true, false, true, false,
740
+ true>(outIndicesAux + offset, outLogitsAux + offset, 0,
741
+ activeBlocks * topK, outIndices, nullptr, 1, topK);
742
+ } else {
743
+ if (blockIdx.y >= activeBlocks) return;
744
+ logits += static_cast<int64_t>(rowIdx) * stride0;
745
+ if (activeBlocks == 1) {
746
+ topKPerRowJob<kNumThreadsPerBlock, kNumBins, true, false, false, false,
747
+ true>(nullptr, logits, 0, rowEnd, outIndices, nullptr, 1,
748
+ topK);
749
+ return;
750
+ }
751
+ const int blockSize = rowEnd / activeBlocks;
752
+ const int rowStart = blockSize * blockIdx.y;
753
+ const int blockRowEnd =
754
+ blockIdx.y + 1 == activeBlocks ? rowEnd : rowStart + blockSize;
755
+ const int64_t offset =
756
+ (static_cast<int64_t>(rowIdx) * maxBlocks + blockIdx.y) * topK;
757
+ topKPerRowJob<kNumThreadsPerBlock, kNumBins, true, true, false, false,
758
+ true>(nullptr, logits, rowStart, blockRowEnd,
759
+ outIndicesAux + offset, outLogitsAux + offset, 1, topK);
760
+ }
761
+ #endif
762
+}
763
+#endif
764
+
765
} // namespace vllm
766
767
void apply_repetition_penalties_(
@@ -731,6 +824,60 @@ void top_k_per_row_decode(const torch::stable::Tensor& logits, int64_t next_n,
824
// the same seq_len and the kernel computes the per-row offset itself.
825
int seqLensIs2D = seqLens.dim() == 2 ? 1 : 0;
826
827
+#ifdef USE_ROCM
828
+ const bool useGfx950DeviceLengthAwareTopK512 =
829
+ topK == 512 && stride1 == 1 && numRows > 0 && numRows <= 384 &&
830
+ numColumns <= 1024 * 1024 &&
831
+ std::strncmp(get_device_prop()->gcnArchName, "gfx950", 6) == 0;
832
+ if (useGfx950DeviceLengthAwareTopK512) {
833
+ int multipleBlocksPerRowConfig = 1;
834
+ if (numRows <= 32 && numColumns > 16 * 1024) {
835
+ multipleBlocksPerRowConfig = 16;
836
+ } else if (numRows <= 128 && numColumns > 64 * 1024) {
837
+ multipleBlocksPerRowConfig = 10;
838
+ } else if (numRows <= 160 && numColumns > 256 * 1024) {
839
+ multipleBlocksPerRowConfig = 3;
840
+ } else if (numRows <= 256 && numColumns > 256 * 1024) {
841
+ multipleBlocksPerRowConfig = 2;
842
+ }
843
+
844
+ constexpr int kNumThreadsPerSplitBlock = 1024;
845
+ if (multipleBlocksPerRowConfig == 1) {
846
+ vllm::topKPerRowDecode512DeviceLengthAware<kNumThreadsPerSplitBlock>
847
+ <<<numRows, kNumThreadsPerSplitBlock, 2 * topK * sizeof(int32_t),
848
+ stream>>>(logits.const_data_ptr<float>(),
849
+ seqLens.const_data_ptr<int>(),
850
+ indices.mutable_data_ptr<int>(), nullptr, nullptr,
851
+ static_cast<int>(stride0), static_cast<int>(next_n),
852
+ seqLensIs2D, multipleBlocksPerRowConfig);
853
+ return;
854
+ }
855
+ const auto outIndicesAux = torch::stable::empty(
856
+ {numRows, multipleBlocksPerRowConfig, topK},
857
+ torch::headeronly::ScalarType::Int, std::nullopt, logits.device());
858
+ const auto outLogitsAux = torch::stable::empty(
859
+ {numRows, multipleBlocksPerRowConfig, topK},
860
+ torch::headeronly::ScalarType::Float, std::nullopt, logits.device());
861
+ vllm::topKPerRowDecode512DeviceLengthAware<kNumThreadsPerSplitBlock>
862
+ <<<dim3(numRows, multipleBlocksPerRowConfig), kNumThreadsPerSplitBlock,
863
+ 2 * topK * sizeof(int32_t), stream>>>(
864
+ logits.const_data_ptr<float>(), seqLens.const_data_ptr<int>(),
865
+ indices.mutable_data_ptr<int>(),
866
+ outIndicesAux.mutable_data_ptr<int>(),
867
+ outLogitsAux.mutable_data_ptr<float>(), static_cast<int>(stride0),
868
+ static_cast<int>(next_n), seqLensIs2D, multipleBlocksPerRowConfig);
869
+ constexpr int kNumThreadsPerBlockMerge = 1024;
870
+ vllm::topKPerRowDecode512DeviceLengthAware<kNumThreadsPerBlockMerge, true>
871
+ <<<numRows, kNumThreadsPerBlockMerge, topK * sizeof(int32_t), stream>>>(
872
+ nullptr, seqLens.const_data_ptr<int>(),
873
+ indices.mutable_data_ptr<int>(),
874
+ outIndicesAux.mutable_data_ptr<int>(),
875
+ outLogitsAux.mutable_data_ptr<float>(), 0, static_cast<int>(next_n),
876
+ seqLensIs2D, multipleBlocksPerRowConfig);
877
+ return;
878
+ }
879
+#endif
880
+
881
if (numColumns < kSortingAlgorithmThreshold) {
882
// Use insertion sort
883
vllm::topKPerRowDecode<kNumThreadsPerBlock, false>
tests/kernels/test_top_k_per_row.py
+189
@@ -22,11 +22,22 @@ def _has_device_capability(major: int) -> bool:
22
return current_platform.is_cuda() and current_platform.has_device_capability(major)
23
24
25
+def _on_gfx950() -> bool:
26
+ if not current_platform.is_rocm():
27
+ return False
28
+ from vllm.platforms.rocm import on_gfx950
29
+
30
+ return on_gfx950()
31
+
32
+
33
# DeepSelect is compiled for sm_100a/sm_103a only.
34
requires_sm100 = pytest.mark.skipif(
35
not current_platform.is_device_capability_family(100),
36
reason="DeepSelect requires SM100a/SM103a",
37
)
38
+requires_gfx950 = pytest.mark.skipif(
39
+ not _on_gfx950(), reason="This test exercises the gfx950 launch configuration"
40
+)
41
42
43
COOPERATIVE_TOPK_BACKEND = pytest.param(
@@ -487,6 +498,150 @@ def test_top_k_per_row_decode_gfx950_long_c4a_1d_seq_lens() -> None:
498
)
499
500
501
+def _assert_exact_topk(
502
+ logits: torch.Tensor, indices: torch.Tensor, row_ends: torch.Tensor
503
+) -> None:
504
+ ends = row_ends.reshape(-1, 1).clamp_min(0)
505
+ valid = (indices >= 0) & (indices < ends)
506
+ assert torch.equal(valid.sum(1), ends.clamp_max(indices.shape[1]).flatten())
507
+ assert torch.all(valid | (indices == -1))
508
+ ordered = indices.sort(1).values
509
+ assert not ((ordered[:, 1:] == ordered[:, :-1]) & (ordered[:, 1:] >= 0)).any()
510
+
511
+ width = int(ends.max())
512
+ visible = logits[:, :width].clone()
513
+ columns = torch.arange(width, device=logits.device)
514
+ visible.masked_fill_(columns[None, :] >= ends, float("-inf"))
515
+ expected = visible.topk(min(indices.shape[1], width), dim=1).values
516
+ selected = logits.gather(1, indices.clamp_min(0).long())
517
+ selected.masked_fill_(~valid, float("-inf"))
518
+ selected = selected.sort(1, descending=True).values
519
+ assert torch.equal(selected[:, : expected.shape[1]], expected)
520
+
521
+
522
+@requires_gfx950
523
+@torch.inference_mode()
524
+def test_top_k_per_row_decode_gfx950_k512_1d_seq_lens() -> None:
525
+ """Honor causal offsets and exact compressed lengths, including short rows."""
526
+ next_n, width, top_k = 6, 16_384, 512
527
+ lengths = torch.tensor(
528
+ [0, 1, 511, 512, 513, 1023, 1024, 16_000],
529
+ dtype=torch.int32,
530
+ device="cuda",
531
+ )
532
+ offsets = torch.arange(next_n, dtype=torch.int32, device="cuda")
533
+ row_ends = (lengths[:, None] - next_n + offsets + 1).clamp_min(0).flatten()
534
+ rows = row_ends.numel()
535
+ storage = torch.full((rows, width + 8), 1e20, dtype=torch.float32, device="cuda")
536
+ logits = storage[:, 1 : width + 1]
537
+ logits[:, :16_000] = torch.arange(16_000, dtype=torch.float32, device="cuda")
538
+ indices = torch.full((rows, top_k), -777, dtype=torch.int32, device="cuda")
539
+ _run_topk_backend(
540
+ "top_k_per_row_decode", logits, lengths, indices, top_k, width, next_n
541
+ )
542
+ _assert_exact_topk(logits, indices, row_ends)
543
+
544
+
545
+@pytest.mark.parametrize(
546
+ ("rows", "row_length"),
547
+ [
548
+ pytest.param(128, 65_536, id="single-before-64k"),
549
+ pytest.param(128, 65_537, id="four-splits-after-64k"),
550
+ pytest.param(129, 262_144, id="single-before-three-split-length"),
551
+ pytest.param(129, 262_145, id="three-splits-after-256k"),
552
+ pytest.param(160, 262_145, id="three-splits-upper-row-bound"),
553
+ pytest.param(161, 262_144, id="single-before-two-split-length"),
554
+ pytest.param(161, 262_145, id="two-splits-after-256k"),
555
+ pytest.param(256, 262_145, id="two-splits-upper-row-bound"),
556
+ pytest.param(257, 262_145, id="large-row-single-block"),
557
+ pytest.param(384, 262_145, id="optimized-row-limit"),
558
+ pytest.param(385, 262_145, id="native-fallback"),
559
+ ],
560
+)
561
+@requires_gfx950
562
+@torch.inference_mode()
563
+def test_top_k_per_row_decode_gfx950_k512_split_policy_boundaries(
564
+ rows: int, row_length: int
565
+) -> None:
566
+ width, top_k = 262_145, 512
567
+ row = torch.arange(width, dtype=torch.float32, device="cuda")
568
+ logits = row.expand(rows, -1)
569
+ lengths = torch.full((rows,), row_length, dtype=torch.int32, device="cuda")
570
+ indices = torch.empty((rows, top_k), dtype=torch.int32, device="cuda")
571
+
572
+ _run_topk_backend("top_k_per_row_decode", logits, lengths, indices, top_k, width)
573
+
574
+ expected = torch.arange(
575
+ row_length - top_k, row_length, dtype=torch.int32, device="cuda"
576
+ ).expand_as(indices)
577
+ assert torch.equal(indices.sort(1).values, expected)
578
+
579
+
580
+@requires_gfx950
581
+@torch.inference_mode()
582
+def test_top_k_per_row_decode_gfx950_k512_masked_graph_replay() -> None:
583
+ """Preserve visible top-k scores when masks and lengths change on replay."""
584
+ rows, next_n, width, top_k = 24, 6, 262_145, 512
585
+ logits = torch.full((rows, width), 1e20, dtype=torch.float32, device="cuda")
586
+ lengths = torch.full(
587
+ (rows // next_n, next_n), 70_000, dtype=torch.int32, device="cuda"
588
+ )
589
+ indices = torch.full((rows, top_k), -777, dtype=torch.int32, device="cuda")
590
+ logits[:, :70_000] = float("-inf")
591
+ bin_sizes = [128, 129, 1024, 1025]
592
+ for row in range(rows):
593
+ pattern = row % 9
594
+ if pattern < 4:
595
+ count = [0, 1, 511, 513][pattern]
596
+ logits[row, :count] = torch.arange(
597
+ count, dtype=torch.float32, device="cuda"
598
+ )
599
+ elif pattern == 4:
600
+ logits[row, :511] = 0
601
+ logits[row, 511] = torch.finfo(torch.float32).min
602
+ else:
603
+ count = bin_sizes[pattern - 5]
604
+ higher = max(0, top_k - count // 2)
605
+ logits[row, :higher] = 2
606
+ logits[row, higher : higher + count] = 1
607
+ logits[row, higher] = 1 + 2**-23
608
+
609
+ def run() -> None:
610
+ _run_topk_backend(
611
+ "top_k_per_row_decode", logits, lengths, indices, top_k, width, next_n
612
+ )
613
+
614
+ run()
615
+ _assert_exact_topk(logits, indices, lengths)
616
+ torch.accelerator.synchronize()
617
+ graph = torch.cuda.CUDAGraph()
618
+ with torch.cuda.graph(graph):
619
+ run()
620
+ indices.fill_(-777)
621
+ graph.replay()
622
+ _assert_exact_topk(logits, indices, lengths)
623
+
624
+ bounds = [
625
+ 0,
626
+ 511,
627
+ 512,
628
+ 513,
629
+ 16_384,
630
+ 16_385,
631
+ 131_072,
632
+ 131_073,
633
+ 262_144,
634
+ 262_145,
635
+ ]
636
+ row_bounds = torch.tensor(bounds, dtype=torch.int32, device="cuda")
637
+ repeats = (rows + len(bounds) - 1) // len(bounds)
638
+ lengths.flatten().copy_(row_bounds.repeat(repeats)[:rows])
639
+ logits[:] = torch.arange(width, dtype=torch.float32, device="cuda")
640
+ indices.fill_(-777)
641
+ graph.replay()
642
+ _assert_exact_topk(logits, indices, lengths)
643
+
644
+
645
@pytest.mark.skipif(not current_platform.is_rocm(), reason="This test requires ROCm")
646
@torch.inference_mode()
647
def test_aiter_c4a_prefill_topk_returns_sequence_local_indices() -> None:
@@ -620,6 +775,31 @@ def test_aiter_c4a_topk_kernel_selection(monkeypatch) -> None:
775
compress_ratio=4,
776
num_rows=257,
777
max_valid_seq_len=250_000,
778
+ num_columns=524_288,
779
+ on_gfx950=True,
780
+ ),
781
+ decode_kernel,
782
+ ),
783
+ (
784
+ dict(
785
+ is_prefill=False,
786
+ compress_ratio=2,
787
+ num_rows=385,
788
+ max_valid_seq_len=250_000,
789
+ num_columns=524_288,
790
+ topk_tokens=512,
791
+ on_gfx950=True,
792
+ ),
793
+ decode_kernel,
794
+ ),
795
+ (
796
+ dict(
797
+ is_prefill=False,
798
+ compress_ratio=2,
799
+ num_rows=384,
800
+ max_valid_seq_len=50_000,
801
+ num_columns=1_048_577,
802
+ topk_tokens=512,
803
on_gfx950=True,
804
),
805
decode_kernel,
@@ -638,6 +818,15 @@ def test_aiter_c4a_topk_kernel_selection(monkeypatch) -> None:
818
)
819
ineligible_cases = [
820
dict(is_prefill=True, compress_ratio=1, num_rows=1, on_gfx950=True),
821
+ dict(
822
+ is_prefill=False,
823
+ compress_ratio=2,
824
+ num_rows=384,
825
+ max_valid_seq_len=1_048_576,
826
+ num_columns=1_048_576,
827
+ topk_tokens=512,
828
+ on_gfx950=True,
829
+ ),
830
dict(
831
is_prefill=False,
832
compress_ratio=4,
vllm/v1/attention/ops/rocm_aiter_mla_sparse.py
+12
@@ -58,6 +58,7 @@ def _get_aiter_sparse_prefill_opus() -> Callable[..., torch.Tensor] | None:
58
59
_GFX950_C4A_AITER_MAX_COMPRESSED_SEQ_LEN = 64 * 1024
60
_GFX950_C4A_NATIVE_MAX_ROWS = 256
61
+_GFX950_DSV4_NATIVE_MAX_COLUMNS = 1024 * 1024
62
# Conservative perf gate, not a correctness bound: OPUS is correct for any query
63
# count, but Triton stays faster below this measured crossover.
64
_GFX950_AITER_SPARSE_PREFILL_OPUS_MIN_QUERIES = 1024
@@ -69,6 +70,8 @@ def _get_aiter_top_k_kernel(
70
compress_ratio: int,
71
num_rows: int,
72
max_valid_seq_len: int | None = None,
73
+ num_columns: int | None = None,
74
+ topk_tokens: int = 1024,
75
on_gfx950: bool = _ON_GFX950,
76
) -> Callable[..., None] | None:
77
if compress_ratio <= 1 or not on_gfx950:
@@ -76,6 +79,13 @@ def _get_aiter_top_k_kernel(
79
80
if not is_prefill:
81
assert max_valid_seq_len is not None
82
+ if (
83
+ topk_tokens == 512
84
+ and 0 < num_rows <= 384
85
+ and num_columns is not None
86
+ and num_columns <= _GFX950_DSV4_NATIVE_MAX_COLUMNS
87
+ ):
88
+ return None
89
# AITER v0.1.19 decode is one-block only. This measured gfx950
90
# FP32/k=1024 compressed-row boundary is independent of the native
91
# split-count boundary in sampler.cu.
@@ -1261,6 +1271,8 @@ def rocm_aiter_sparse_attn_indexer(
1271
compress_ratio=compress_ratio,
1272
num_rows=num_rows,
1273
max_valid_seq_len=max_compressed_seq_len,
1274
+ num_columns=logits.shape[1],
1275
+ topk_tokens=topk_tokens,
1276
)
1277
if aiter_topk_kernel is not None:
1278
_launch_aiter_top_k_per_row_decode(