Port Fast Top-K kernel

Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
This commit is contained in:
Woosuk Kwon
2026-04-26 06:53:35 +00:00
parent e5108f7443
commit 202e2c7cd0
14 changed files with 2465 additions and 0 deletions
+21
View File
@@ -564,6 +564,27 @@ if(VLLM_GPU_LANG STREQUAL "CUDA")
"in CUDA target architectures.")
endif()
# DeepSeek V4 indexer top-k. Needs thread-block clusters + TMA + PDL, so
# builds for Hopper (sm_90a) and Blackwell datacenter (sm_100/sm_103). Not
# supported on sm_120 (consumer Blackwell, no clusters). Requires CUDA >=
# 12.4 for the cuda::ptx mbarrier wrappers. Ported from sglang.
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 13.0)
cuda_archs_loose_intersection(DSV4_TOPK_ARCHS "9.0a;10.0f;11.0f" "${CUDA_ARCHS}")
else()
cuda_archs_loose_intersection(DSV4_TOPK_ARCHS "9.0a;10.0a;10.1a;10.3a" "${CUDA_ARCHS}")
endif()
if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 12.4 AND DSV4_TOPK_ARCHS)
set(DSV4_TOPK_SRC "csrc/deepseek_v4/fast_topk_v2.cu")
set_gencode_flags_for_srcs(
SRCS "${DSV4_TOPK_SRC}"
CUDA_ARCHS "${DSV4_TOPK_ARCHS}")
list(APPEND VLLM_EXT_SRC ${DSV4_TOPK_SRC})
message(STATUS "Building deepseek_v4 fast_topk_v2 for archs: ${DSV4_TOPK_ARCHS}")
else()
message(STATUS "Not building deepseek_v4 fast_topk_v2 (needs CUDA >= 12.4 "
"and a compatible Hopper+ arch).")
endif()
#
# Machete kernels
@@ -0,0 +1,221 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Microbench: fast_topk_v2 vs persistent_topk for k=512.
Both ops select the top-512 entries per row of a [B, L] float32 score tensor.
The vLLM `persistent_topk` is the existing path used by the indexer
(`sparse_attn_indexer.py`); `fast_topk_v2` is the new sm_90+ port from
sglang that adds Hopper thread-block clusters and a fused page-table gather.
For fairness:
- `persistent_topk` writes raw indices into a (B, k) tensor.
- `fast_topk_v2` writes page-table-resolved indices. To match the same
output (raw indices) we use ``page_size=1`` with an identity page table —
page_to_indices becomes a no-op, so the two kernels produce comparable
results on the hot path.
Timing uses **CUDA graph replay** to amortize launch overhead. We capture
N invocations of the same kernel into a single graph, replay the graph
many times, and divide. This isolates kernel work from per-launch host
latency (~35 µs on Blackwell) which would otherwise dominate at small
shapes.
Run::
.venv/bin/python benchmarks/kernels/benchmark_fast_topk_v2.py
"""
from __future__ import annotations
import argparse
import statistics
import sys
import torch
import vllm._C # noqa: F401 ensures schemas are registered
from vllm.v1.attention.ops.deepseek_v4_ops.fast_topk import (
plan_topk_v2,
workspace_ints_per_batch,
)
K = 512
RADIX_TOPK_WORKSPACE_SIZE = 1024 * 1024 # bytes; matches sparse_attn_indexer.py
def _capture_graph(callable_fn, *, calls_per_graph: int) -> torch.cuda.CUDAGraph:
"""Capture a CUDA graph that invokes `callable_fn` `calls_per_graph` times.
The kernel is run a few times outside capture (warmup + allocator priming)
to avoid capturing one-shot setup work like cudaFuncSetAttribute.
"""
# Outside-capture warmup: lets cudaFuncSetAttribute's static cache fire
# and primes any cublas/cudnn lookups the kernel might trigger.
for _ in range(3):
callable_fn()
torch.cuda.synchronize()
g = torch.cuda.CUDAGraph()
s = torch.cuda.Stream()
s.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(s):
with torch.cuda.graph(g, stream=s):
for _ in range(calls_per_graph):
callable_fn()
torch.cuda.current_stream().wait_stream(s)
return g
def time_graph_us(graph: torch.cuda.CUDAGraph, *, calls_per_graph: int,
warmup: int = 5, replays: int = 30) -> float:
"""Median per-call latency (µs) over many graph replays."""
for _ in range(warmup):
graph.replay()
torch.cuda.synchronize()
samples = []
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
for _ in range(replays):
start.record()
graph.replay()
end.record()
end.synchronize()
# ms -> us, then per-call.
samples.append(start.elapsed_time(end) * 1000.0 / calls_per_graph)
return statistics.median(samples)
def make_inputs(batch_size: int, seq_len: int, *, seed: int = 0):
device = torch.device("cuda")
g = torch.Generator(device=device).manual_seed(seed)
L = (seq_len + 3) & ~3 # TMA wants stride%4 == 0
scores = torch.randn(batch_size, L, generator=g, dtype=torch.float32,
device=device)
seq_lens = torch.full((batch_size,), seq_len, dtype=torch.int32,
device=device)
# Identity page table with page_size=1 -> page_to_indices is identity.
page_table = (
torch.arange(L, dtype=torch.int32, device=device)
.unsqueeze(0).expand(batch_size, -1).contiguous()
)
return scores, seq_lens, page_table, L
def bench_persistent_topk(scores, seq_lens, *, calls_per_graph: int) -> float:
B = scores.shape[0]
output = scores.new_empty((B, K), dtype=torch.int32)
workspace = scores.new_empty((RADIX_TOPK_WORKSPACE_SIZE,),
dtype=torch.uint8)
max_seq_len = scores.shape[1]
def run():
torch.ops._C.persistent_topk(
scores, seq_lens, output, workspace, K, max_seq_len)
graph = _capture_graph(run, calls_per_graph=calls_per_graph)
return time_graph_us(graph, calls_per_graph=calls_per_graph)
def bench_fast_topk_v2(scores, seq_lens, page_table, *,
calls_per_graph: int) -> tuple[float, float]:
"""Returns (kernel_only_us, with_plan_us). Both are per-call medians.
- kernel_only: just torch.ops._C.fast_topk_v2 (plan was done once,
metadata reused). This is the realistic decode-with-cudagraph case.
- with_plan: plan + main kernel captured together. Pessimistic for
cudagraph (plan would normally be done once outside the graph and
reused, since it depends only on seq_lens shape).
"""
B = scores.shape[0]
metadata = plan_topk_v2(seq_lens)
workspace = scores.new_empty((B, workspace_ints_per_batch()),
dtype=torch.int32)
page_indices = scores.new_empty((B, K), dtype=torch.int32)
def run_kernel_only():
torch.ops._C.fast_topk_v2(
scores, seq_lens, page_table, page_indices, 1, workspace, metadata)
def run_with_plan():
torch.ops._C.fast_topk_v2_plan(seq_lens, metadata, 0)
torch.ops._C.fast_topk_v2(
scores, seq_lens, page_table, page_indices, 1, workspace, metadata)
g_kernel = _capture_graph(run_kernel_only, calls_per_graph=calls_per_graph)
g_with_plan = _capture_graph(run_with_plan, calls_per_graph=calls_per_graph)
return (
time_graph_us(g_kernel, calls_per_graph=calls_per_graph),
time_graph_us(g_with_plan, calls_per_graph=calls_per_graph),
)
def fmt(us: float) -> str:
return f"{us:8.2f}"
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--batch-sizes", type=int, nargs="+",
default=[1, 4, 16, 32, 64, 128, 256])
parser.add_argument("--seq-lens", type=int, nargs="+",
default=[1024, 4096, 16384, 32768, 65536, 131072])
parser.add_argument("--calls-per-graph", type=int, default=64,
help="Invocations captured per graph (amortizes "
"graph-replay overhead, ~3-5 µs).")
parser.add_argument("--replays", type=int, default=30,
help="Graph replays per measurement.")
args = parser.parse_args()
if not torch.cuda.is_available():
print("CUDA is required for this benchmark.", file=sys.stderr)
sys.exit(1)
print(f"GPU: {torch.cuda.get_device_name(0)} "
f"(SM {torch.cuda.get_device_capability(0)})")
print(f"k = {K}; calls_per_graph={args.calls_per_graph}, "
f"replays={args.replays}")
print("Per-call medians via CUDA graph replay (host launch overhead "
"amortized).\n")
print(f"{'B':>4} {'L':>7} | "
f"{'persistent_topk':>17} | "
f"{'fast_topk_v2':>14} | "
f"{'fast_topk_v2+plan':>19} | "
f"{'speedup':>9} | {'path':<14}")
print("-" * 105)
for B in args.batch_sizes:
for L in args.seq_lens:
try:
scores, seq_lens, page_table, _ = make_inputs(B, L, seed=B * L)
p_us = bench_persistent_topk(
scores, seq_lens, calls_per_graph=args.calls_per_graph)
f_us, fp_us = bench_fast_topk_v2(
scores, seq_lens, page_table,
calls_per_graph=args.calls_per_graph)
speedup = p_us / f_us if f_us > 0 else float("inf")
if L <= 512:
path = "trivial"
elif L <= 4 * 4 * 1024:
path = "register-1p"
elif L <= 32768:
path = "register-2p"
elif B <= 15:
path = "cluster-fused"
else:
path = "cluster-2stg"
print(
f"{B:>4} {L:>7} | "
f"{fmt(p_us):>14} us | "
f"{fmt(f_us):>11} us | "
f"{fmt(fp_us):>16} us | "
f"{speedup:>6.2f}x | {path}"
)
except RuntimeError as e:
print(f"{B:>4} {L:>7} | ERROR: {e}")
if __name__ == "__main__":
main()
+566
View File
@@ -0,0 +1,566 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
//
// DeepSeek V4 indexer top-k (k = 512). Ported from sglang's
// jit_kernel/csrc/deepseek_v4/topk_v2.cuh.
//
// Combines three strategies (Register / Streaming / Cluster) dispatched per
// row by a separate plan kernel that decides a `cluster_threshold` from the
// observed seq_lens distribution. The host side picks one of three launch
// shapes:
// 1. all rows fit in the small (register) path -> single short kernel
// 2. small batch (<= kNumClusters) with some long rows -> fused cluster
// kernel (stage 1 + tie-break in one launch)
// 3. larger batch -> persistent cluster stage 1 + non-cluster stage 2
//
// Architecture support: Hopper (sm_90a) and Blackwell datacenter (sm_100/
// sm_103). Requires thread-block clusters, TMA bulk async copy, mbarrier,
// and Programmatic Dependent Launch — sm_120 (consumer Blackwell) lacks
// clusters and is not supported. The heuristic constants in `topk_plan`
// were tuned on B200 (sglang upstream); they are functionally correct on
// H100/H200 too but may be suboptimal until retuned.
#include "topk/cluster.cuh"
#include "topk/common.cuh"
#include "topk/register.cuh"
#include "topk/streaming.cuh"
#include "topk/utils.cuh"
#include "core/registration.h"
#include <ATen/cuda/CUDAContext.h>
#include <c10/util/Exception.h>
#include <cooperative_groups.h>
#include <cuda_runtime.h>
#include <torch/all.h>
#include <torch/library.h>
#include <algorithm>
#include <cstdint>
namespace vllm::dsv4_topk {
using Large = ClusterTopK<512>;
using Medium = StreamingTopK<512>;
using Small = RegisterTopK<512>;
using Metadata = Large::Metadata;
constexpr uint32_t kNumClusters = 15; // hardware-capped persistent count
constexpr uint32_t kClusterSize = Large::kClusterSize;
constexpr uint32_t kMax2PassLength = Small::kMax2PassLength;
constexpr uint32_t kMaxSupportedLength = Large::kMaxLength;
// Row 0 of the metadata tensor stores GlobalMetadata; rows [1..N+1) hold the
// per-item Metadata entries that the persistent stage-1 consumes.
struct alignas(16) GlobalMetadata {
uint32_t cluster_threshold;
uint32_t num_cluster_items;
uint32_t reserved[2];
};
static_assert(sizeof(GlobalMetadata) == sizeof(Metadata),
"metadata row 0 layout must match Metadata stride");
#define VLLM_SMALL_TOPK_KERNEL __global__ __launch_bounds__(kBlockSize, 2)
#define VLLM_LARGE_CLUSTER __cluster_dims__(1, kClusterSize, 1)
// Stage 1 is persistent + cluster -> high smem -> occupancy 1.
#define VLLM_LARGE_TOPK_STAGE_1 \
__global__ __launch_bounds__(kBlockSize, 1) VLLM_LARGE_CLUSTER
// Stage 2 is non-cluster + small smem -> occupancy 2.
#define VLLM_LARGE_TOPK_STAGE_2 __global__ __launch_bounds__(kBlockSize, 2)
#define VLLM_FUSED_COMBINE_KERNEL \
__global__ __launch_bounds__(kBlockSize, 1) VLLM_LARGE_CLUSTER
#define VLLM_PLAN_KERNEL __global__ __launch_bounds__(kBlockSize, 1)
struct TopKParams {
const uint32_t* __restrict__ seq_lens;
const float* __restrict__ scores;
const int32_t* __restrict__ page_table;
int32_t* __restrict__ page_indices;
int64_t score_stride;
int64_t page_table_stride;
uint8_t* __restrict__ workspace;
const Metadata* __restrict__ metadata = nullptr;
int64_t workspace_stride; // bytes per batch
uint32_t batch_size;
uint32_t page_bits;
VLLM_DSV4_DEVICE const float* get_scores(uint32_t batch_id) const {
return scores + batch_id * score_stride;
}
VLLM_DSV4_DEVICE TransformParams get_transform(uint32_t batch_id,
int32_t* indices) const {
return {
.page_table = page_table + batch_id * page_table_stride,
.indices_in = indices,
.indices_out = page_indices + batch_id * 512,
.page_bits = page_bits,
};
}
VLLM_DSV4_DEVICE const GlobalMetadata& get_global_metadata() const {
return *reinterpret_cast<const GlobalMetadata*>(metadata);
}
VLLM_DSV4_DEVICE const Metadata& get_item_metadata(uint32_t work_id) const {
return metadata[1 + work_id]; // skip the GlobalMetadata row
}
};
VLLM_DSV4_DEVICE uint2 partition_work(uint32_t length, uint32_t rank) {
constexpr uint32_t kTMAAlign = 4;
const auto total_units = (length + kTMAAlign - 1) / kTMAAlign;
const auto base = total_units / kClusterSize;
const auto extra = total_units % kClusterSize;
const auto local_units = base + (rank < extra ? 1u : 0u);
const auto offset_units = rank * base + min(rank, extra);
const auto offset = offset_units * kTMAAlign;
const auto finish = min(offset + local_units * kTMAAlign, length);
return {offset, finish - offset};
}
// --------------------------------------------------------------------------
// Plan kernel: decides cluster_threshold from the observed seq_lens
// distribution and compacts items with seq_len > threshold into metadata[1..].
// --------------------------------------------------------------------------
VLLM_PLAN_KERNEL void topk_plan(const uint32_t* __restrict__ seq_lens,
Metadata* __restrict__ metadata,
uint32_t batch_size,
uint32_t static_cluster_threshold) {
// (threshold, max_batch_size_for_that_threshold). Tuned on B200 by sglang.
struct Pair {
uint32_t threshold;
uint32_t max_batch_size;
};
constexpr Pair kCandidates[] = {
{32768, 30}, {40960, 45}, {49152, 45}, {65536, 60},
{98304, 60}, {131072, 75}, {196608, 90}, {262144, 105},
};
constexpr uint32_t kNumCandidates =
sizeof(kCandidates) / sizeof(kCandidates[0]);
constexpr uint32_t kMinBatchSize = kCandidates[0].max_batch_size;
static_assert(kCandidates[0].threshold == kMax2PassLength);
static_assert(kCandidates[kNumCandidates - 1].threshold ==
kMaxSupportedLength);
__shared__ uint32_t s_count;
__shared__ uint32_t s_counts[kNumCandidates];
__shared__ uint32_t s_threshold;
const auto tx = threadIdx.x;
if (tx == 0) s_count = 0;
if (tx < kNumCandidates) s_counts[tx] = 0;
__syncthreads();
if (static_cluster_threshold > 0) {
if (tx == 0) s_threshold = static_cluster_threshold;
} else if (batch_size <= kMinBatchSize) {
if (tx == 0) s_threshold = kMax2PassLength;
} else {
for (uint32_t i = tx; i < batch_size; i += kBlockSize) {
const uint32_t sl = seq_lens[i];
assert(sl <= kMaxSupportedLength);
uint32_t count = 0;
#pragma unroll
for (uint32_t j = 0; j < kNumCandidates; ++j) {
count += (sl > kCandidates[j].threshold ? 1 : 0);
}
if (count > 0) {
atomicAdd(&s_counts[count - 1], 1);
}
}
__syncthreads();
if (tx == 0) {
uint32_t accum = 0;
uint32_t chosen = kMaxSupportedLength;
#pragma unroll
for (uint32_t i = 0; i < kNumCandidates; ++i) {
const auto j = kNumCandidates - 1 - i;
accum += s_counts[j];
if (accum > kCandidates[j].max_batch_size) break;
chosen = kCandidates[j].threshold;
}
s_threshold = chosen;
}
}
__syncthreads();
const auto cluster_threshold = max(s_threshold, kMax2PassLength);
// Compact items with seq_len > cluster_threshold into metadata[1..N+1).
for (uint32_t i = tx; i < batch_size; i += kBlockSize) {
const uint32_t sl = seq_lens[i];
if (sl > cluster_threshold) {
const auto pos = atomicAdd(&s_count, 1);
metadata[1 + pos] = {i, sl, false};
}
}
__syncthreads();
const auto N = s_count;
// has_next chain for the persistent consumer + sentinel slots.
for (uint32_t i = tx; i < N; i += kBlockSize) {
if (i + kNumClusters < N) metadata[1 + i].has_next = true;
}
if (tx < kNumClusters && tx >= N) metadata[1 + tx] = {0, 0, false};
if (tx == 0) {
auto* g = reinterpret_cast<GlobalMetadata*>(metadata);
*g = {
.cluster_threshold = cluster_threshold,
.num_cluster_items = N,
.reserved = {0, 0},
};
}
}
// --------------------------------------------------------------------------
// Short kernel: all rows fit in the register path (max_seq_len <=
// Small::kMax1PassLength).
// --------------------------------------------------------------------------
VLLM_SMALL_TOPK_KERNEL void topk_short_transform(
const __grid_constant__ TopKParams params) {
alignas(128) extern __shared__ uint8_t smem[];
__shared__ int32_t s_topk_indices[512];
const auto batch_id = blockIdx.x;
const auto seq_len = params.seq_lens[batch_id];
const auto transform = params.get_transform(batch_id, s_topk_indices);
if (seq_len <= 512) {
trivial_transform(transform, seq_len, 512);
} else {
Small::run(params.get_scores(batch_id), s_topk_indices, seq_len, smem,
/*use_pdl=*/true);
pdl_trigger_secondary<true>();
Small::transform(transform);
}
}
// --------------------------------------------------------------------------
// Persistent stage 1 (cluster). One CTA per cluster; the persistent block
// walks `metadata[1..N]` round-robin and runs Large::stage1 per item.
// --------------------------------------------------------------------------
VLLM_LARGE_TOPK_STAGE_1 void topk_combine_preprocess(
const __grid_constant__ TopKParams params) {
alignas(128) extern __shared__ uint8_t smem[];
__shared__ int32_t s_topk_indices[512];
uint32_t work_id = blockIdx.x;
uint32_t batch_id = 0, seq_len = 0, length = 0, offset = 0;
bool has_next = false;
const auto cluster_rank = blockIdx.y;
const auto prefetch_metadata = [&] {
const auto m = params.get_item_metadata(work_id);
batch_id = m.batch_id;
seq_len = m.seq_len;
has_next = m.has_next;
work_id += kNumClusters;
};
const auto launch_prologue = [&] {
const auto partition = partition_work(seq_len, cluster_rank);
offset = partition.x;
length = partition.y;
Large::stage1_prologue(params.get_scores(batch_id) + offset, length, smem);
};
pdl_wait_primary<true>();
pdl_trigger_secondary<true>();
prefetch_metadata();
if (seq_len == 0) return;
Large::stage1_init(smem);
launch_prologue();
while (true) {
const auto this_length = length;
const auto this_offset = offset;
const auto need_prefetch = has_next;
const auto transform = params.get_transform(batch_id, s_topk_indices);
const auto ws = params.workspace + batch_id * params.workspace_stride;
if (need_prefetch) prefetch_metadata();
Large::stage1(s_topk_indices, this_length, smem, /*reuse=*/true);
if (need_prefetch) launch_prologue();
Large::stage1_epilogue(transform, this_offset, ws, smem);
if (!need_prefetch) break;
}
}
// --------------------------------------------------------------------------
// Stage 2 (non-cluster). Per-row dispatch: trivial / Small / Medium / Large.
// --------------------------------------------------------------------------
VLLM_LARGE_TOPK_STAGE_2 void topk_combine_transform(
const __grid_constant__ TopKParams params) {
alignas(128) extern __shared__ uint8_t smem[];
__shared__ int32_t s_topk_indices[512];
const auto batch_id = blockIdx.x;
const auto seq_len = params.seq_lens[batch_id];
const auto cluster_threshold = params.get_global_metadata().cluster_threshold;
const auto transform = params.get_transform(batch_id, s_topk_indices);
if (seq_len <= 512) {
trivial_transform(transform, seq_len, 512);
} else if (seq_len <= kMax2PassLength) {
if (seq_len <= Small::kMax1PassLength) {
Small::run(params.get_scores(batch_id), s_topk_indices, seq_len, smem);
} else {
__syncwarp();
Small::run<true>(params.get_scores(batch_id), s_topk_indices, seq_len,
smem);
}
Small::transform(transform);
} else if (seq_len <= cluster_threshold) {
Medium::run(params.get_scores(batch_id), seq_len, s_topk_indices, smem);
Medium::transform(transform, smem);
} else {
const auto ws = params.workspace + batch_id * params.workspace_stride;
pdl_wait_primary<true>();
Large::transform(transform, ws, smem);
}
}
// --------------------------------------------------------------------------
// Fused kernel for small batches. Both stage 1 and the tie-break run inside
// the same launch; cluster rank 0 finishes the row.
// --------------------------------------------------------------------------
VLLM_FUSED_COMBINE_KERNEL void topk_fused_transform(
const __grid_constant__ TopKParams params) {
alignas(128) extern __shared__ uint8_t smem[];
__shared__ int32_t s_topk_indices[512];
const auto batch_id = blockIdx.x;
const auto cluster_rank = blockIdx.y;
const auto seq_len = params.seq_lens[batch_id];
const auto transform = params.get_transform(batch_id, s_topk_indices);
if (seq_len <= 512) {
if (cluster_rank != 0) return;
trivial_transform(transform, seq_len, 512);
} else if (seq_len <= Small::kMax1PassLength) {
if (cluster_rank != 0) return;
Small::run(params.get_scores(batch_id), s_topk_indices, seq_len, smem,
/*use_pdl=*/true);
Small::transform(transform);
} else {
const auto partition = partition_work(seq_len, cluster_rank);
const auto offset = partition.x;
const auto length = partition.y;
const auto ws = params.workspace + batch_id * params.workspace_stride;
Large::stage1_init(smem);
pdl_wait_primary<true>();
Large::stage1_prologue(params.get_scores(batch_id) + offset, length, smem);
Large::stage1(s_topk_indices, length, smem);
Large::stage1_epilogue(transform, offset, ws, smem);
cooperative_groups::this_cluster().sync();
if (cluster_rank != 0) return;
Large::transform(transform, ws, smem);
}
}
constexpr size_t kStage1SMEM = sizeof(Large::Smem) + 128;
constexpr size_t kStage2SMEM =
(sizeof(Small::Smem) > sizeof(Medium::Smem) ? sizeof(Small::Smem)
: sizeof(Medium::Smem)) +
128;
// Per-(kernel, smem) memoization: each instantiation has its own static. This
// matters because cudaFuncSetAttribute is per-function and we want it to fire
// exactly once per kernel symbol.
template <auto* f, size_t kSmem>
void setup_kernel_smem_once() {
[[maybe_unused]] static const auto result = [] {
return cudaFuncSetAttribute(reinterpret_cast<const void*>(f),
cudaFuncAttributeMaxDynamicSharedMemorySize,
static_cast<int>(kSmem));
}();
TORCH_CHECK(result == cudaSuccess,
"fast_topk_v2: cudaFuncSetAttribute failed: ",
cudaGetErrorString(result));
}
// --------------------------------------------------------------------------
// Host-side launchers
// --------------------------------------------------------------------------
#define CHECK_CUDA(x) TORCH_CHECK(x.is_cuda(), #x " must be a CUDA tensor")
#define CHECK_DTYPE(x, t) \
TORCH_CHECK(x.scalar_type() == (t), #x " must be ", #t)
#define CHECK_CONTIG(x) TORCH_CHECK(x.is_contiguous(), #x " must be contiguous")
} // namespace vllm::dsv4_topk
void fast_topk_v2_plan(const torch::Tensor& seq_lens, torch::Tensor& metadata,
int64_t static_cluster_threshold) {
using namespace vllm::dsv4_topk;
CHECK_CUDA(seq_lens);
CHECK_CUDA(metadata);
CHECK_DTYPE(seq_lens, torch::kInt32);
CHECK_DTYPE(metadata, torch::kInt32);
TORCH_CHECK(seq_lens.dim() == 1);
TORCH_CHECK(metadata.dim() == 2 && metadata.size(1) == 4);
TORCH_CHECK(metadata.size(0) == seq_lens.size(0) + 1,
"metadata must be (batch_size + 1, 4)");
CHECK_CONTIG(seq_lens);
CHECK_CONTIG(metadata);
const auto batch_size = static_cast<uint32_t>(seq_lens.size(0));
if (batch_size <= kNumClusters) return; // metadata unused in fused path
const auto stream = at::cuda::getCurrentCUDAStream().stream();
cudaLaunchConfig_t cfg{};
cfg.gridDim = dim3(1);
cfg.blockDim = dim3(kBlockSize);
cfg.dynamicSmemBytes = 0;
cfg.stream = stream;
cfg.numAttrs = 0;
TORCH_CHECK(cudaLaunchKernelEx(
&cfg, &topk_plan,
reinterpret_cast<const uint32_t*>(seq_lens.data_ptr<int32_t>()),
reinterpret_cast<Metadata*>(metadata.data_ptr<int32_t>()),
batch_size,
static_cast<uint32_t>(static_cluster_threshold)) == cudaSuccess,
"fast_topk_v2_plan launch failed: ",
cudaGetErrorString(cudaGetLastError()));
}
void fast_topk_v2(const torch::Tensor& scores, const torch::Tensor& seq_lens,
const torch::Tensor& page_table, torch::Tensor& page_indices,
int64_t page_size, const torch::Tensor& workspace,
const torch::Tensor& metadata) {
using namespace vllm::dsv4_topk;
CHECK_CUDA(scores);
CHECK_CUDA(seq_lens);
CHECK_CUDA(page_table);
CHECK_CUDA(page_indices);
CHECK_CUDA(workspace);
CHECK_CUDA(metadata);
CHECK_DTYPE(scores, torch::kFloat32);
CHECK_DTYPE(seq_lens, torch::kInt32);
CHECK_DTYPE(page_table, torch::kInt32);
CHECK_DTYPE(page_indices, torch::kInt32);
CHECK_DTYPE(workspace, torch::kInt32);
CHECK_DTYPE(metadata, torch::kInt32);
TORCH_CHECK(scores.dim() == 2 && scores.stride(1) == 1,
"scores must be 2D with last stride 1");
TORCH_CHECK(seq_lens.dim() == 1 && seq_lens.is_contiguous());
TORCH_CHECK(page_table.dim() == 2 && page_table.stride(1) == 1,
"page_table must be 2D with last stride 1");
TORCH_CHECK(page_indices.dim() == 2 && page_indices.is_contiguous() &&
page_indices.size(1) == 512,
"page_indices must be (B, 512) contiguous");
TORCH_CHECK(workspace.dim() == 2 && workspace.stride(1) == 1 &&
workspace.size(1) == Large::kWorkspaceInts,
"workspace must be (B, kWorkspaceInts) with last stride 1");
TORCH_CHECK(metadata.dim() == 2 && metadata.size(1) == 4 &&
metadata.is_contiguous(),
"metadata must be (B + 1, 4) contiguous");
const auto batch_size = static_cast<uint32_t>(scores.size(0));
TORCH_CHECK(seq_lens.size(0) == batch_size);
TORCH_CHECK(page_table.size(0) == batch_size);
TORCH_CHECK(page_indices.size(0) == batch_size);
TORCH_CHECK(workspace.size(0) == batch_size);
TORCH_CHECK(metadata.size(0) == batch_size + 1);
const auto max_seq_len = static_cast<uint32_t>(scores.size(1));
TORCH_CHECK(page_size > 0 && (page_size & (page_size - 1)) == 0,
"page_size must be a positive power of 2");
TORCH_CHECK(scores.stride(0) % 4 == 0,
"score stride must be a multiple of 4 (TMA 16-byte alignment)");
// page_bits = log2(page_size). __builtin_ctzll is a host-side compiler
// builtin available under C++17 (vLLM compiles host code with C++17).
const auto page_bits = static_cast<uint32_t>(
__builtin_ctzll(static_cast<unsigned long long>(page_size)));
TopKParams params{
.seq_lens =
reinterpret_cast<const uint32_t*>(seq_lens.data_ptr<int32_t>()),
.scores = scores.data_ptr<float>(),
.page_table = page_table.data_ptr<int32_t>(),
.page_indices = page_indices.data_ptr<int32_t>(),
.score_stride = scores.stride(0),
.page_table_stride = page_table.stride(0),
.workspace = reinterpret_cast<uint8_t*>(workspace.data_ptr<int32_t>()),
.metadata =
reinterpret_cast<const Metadata*>(metadata.data_ptr<int32_t>()),
.workspace_stride =
workspace.stride(0) * static_cast<int64_t>(sizeof(int32_t)),
.batch_size = batch_size,
.page_bits = page_bits,
};
const auto stream = at::cuda::getCurrentCUDAStream().stream();
// Helper: build a cudaLaunchConfig with optional PDL + cluster attributes.
// The attribute storage must outlive cudaLaunchKernelEx (cfg.attrs points
// into it), so we keep it as a local in each call site.
auto make_cfg = [&](dim3 grid, dim3 block, size_t smem,
cudaLaunchAttribute* attrs, bool enable_cluster,
bool enable_pdl) {
cudaLaunchConfig_t cfg{};
cfg.gridDim = grid;
cfg.blockDim = block;
cfg.dynamicSmemBytes = static_cast<unsigned>(smem);
cfg.stream = stream;
int n = 0;
if (enable_pdl) {
attrs[n].id = cudaLaunchAttributeProgrammaticStreamSerialization;
attrs[n].val.programmaticStreamSerializationAllowed = 1;
++n;
}
if (enable_cluster) {
attrs[n].id = cudaLaunchAttributeClusterDimension;
attrs[n].val.clusterDim = {1, kClusterSize, 1};
++n;
}
cfg.numAttrs = n;
cfg.attrs = n ? attrs : nullptr;
return cfg;
};
auto check_launch = [](cudaError_t err) {
TORCH_CHECK(err == cudaSuccess,
"fast_topk_v2 launch failed: ", cudaGetErrorString(err));
};
if (max_seq_len <= Small::kMax1PassLength) {
setup_kernel_smem_once<&topk_short_transform, kStage2SMEM>();
cudaLaunchAttribute attrs[2];
auto cfg = make_cfg(dim3(batch_size), dim3(kBlockSize), kStage2SMEM, attrs,
/*cluster=*/false, /*pdl=*/true);
check_launch(cudaLaunchKernelEx(&cfg, topk_short_transform, params));
} else if (batch_size <= kNumClusters) {
constexpr size_t kFusedSMEM =
kStage1SMEM > kStage2SMEM ? kStage1SMEM : kStage2SMEM;
setup_kernel_smem_once<&topk_fused_transform, kFusedSMEM>();
cudaLaunchAttribute attrs[2];
auto cfg = make_cfg(dim3(batch_size, kClusterSize), dim3(kBlockSize),
kFusedSMEM, attrs, /*cluster=*/true, /*pdl=*/true);
check_launch(cudaLaunchKernelEx(&cfg, topk_fused_transform, params));
} else {
const auto num_clusters = std::min<uint32_t>(batch_size, kNumClusters);
setup_kernel_smem_once<&topk_combine_preprocess, kStage1SMEM>();
cudaLaunchAttribute attrs1[2];
auto cfg1 = make_cfg(dim3(num_clusters, kClusterSize), dim3(kBlockSize),
kStage1SMEM, attrs1, /*cluster=*/true, /*pdl=*/true);
check_launch(cudaLaunchKernelEx(&cfg1, topk_combine_preprocess, params));
setup_kernel_smem_once<&topk_combine_transform, kStage2SMEM>();
cudaLaunchAttribute attrs2[2];
auto cfg2 = make_cfg(dim3(batch_size), dim3(kBlockSize), kStage2SMEM,
attrs2, /*cluster=*/false, /*pdl=*/true);
check_launch(cudaLaunchKernelEx(&cfg2, topk_combine_transform, params));
}
}
int64_t fast_topk_v2_workspace_ints() {
return static_cast<int64_t>(vllm::dsv4_topk::Large::kWorkspaceInts);
}
// Register impls here (instead of in torch_bindings.cpp) so they only exist
// when CMake compiles this source — i.e., when the target build has a
// compatible Hopper / Blackwell-datacenter arch. On other configs the schema
// remains defined but a call surfaces a clear "no impl" runtime error.
TORCH_LIBRARY_IMPL_EXPAND(TORCH_EXTENSION_NAME, CUDA, m) {
m.impl("fast_topk_v2_plan", &fast_topk_v2_plan);
m.impl("fast_topk_v2", &fast_topk_v2);
}
TORCH_LIBRARY_IMPL_EXPAND(TORCH_EXTENSION_NAME, CompositeExplicitAutograd, m) {
m.impl("fast_topk_v2_workspace_ints", &fast_topk_v2_workspace_ints);
}
+264
View File
@@ -0,0 +1,264 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
//
// Cluster top-k strategy for very large N. Uses Hopper thread-block clusters
// (cooperative_groups::this_cluster) to parallelize histogram + scatter across
// up to ``kClusterSize`` blocks per row. Each row is processed in two stages:
// stage 1: per-block histogram, all-reduce across the cluster, threshold
// scatter, and an epilogue that page-translates strictly-above
// entries to global memory and stages ties into a per-row workspace.
// stage 2: tie-break across the cluster's combined ties (run by cluster
// rank 0 in the fused kernel, or as a separate launch otherwise).
// Ported from
// jit_kernel/include/sgl_kernel/deepseek_v4/topk/cluster.cuh.
#pragma once
#include "common.cuh"
#include "ptx.cuh"
#include "utils.cuh"
#include <cooperative_groups.h>
#include <cstdint>
namespace vllm::dsv4_topk {
template <uint32_t K>
struct ClusterTopK {
static constexpr uint32_t kClusterSize = 8;
static constexpr uint32_t kHistBits = 10;
static constexpr uint32_t kHistBins = 1 << kHistBits;
static constexpr uint32_t kElemPerStage = 8;
static constexpr uint32_t kSizePerStage = kElemPerStage * kBlockSize;
static constexpr uint32_t kNumStages = 4;
static constexpr uint32_t kMaxLength = kClusterSize * kNumStages * kSizePerStage;
static constexpr uint32_t kAboveBits = 11;
struct Smem {
uint64_t barrier[kNumStages];
uint32_t local_above_equal[kClusterSize];
uint32_t prefix_above_equal;
alignas(128) uint32_t counter_gt;
alignas(128) uint32_t counter_eq;
alignas(128) MatchBin match;
alignas(128) uint32_t warp_sum[kNumWarps];
uint32_t histogram[kHistBins];
alignas(128) float score_buffer[kNumStages][kSizePerStage];
Tie tie_buffer[kMaxTies];
};
// Per-row metadata produced by the plan kernel and consumed by the fused /
// stage-1 kernels. {batch_id, seq_len, has_next} arranged in an int4-sized
// 16-byte struct so the planner can do contiguous int32x4 stores.
struct alignas(16) Metadata {
uint32_t batch_id;
uint32_t seq_len;
bool has_next;
};
// Per-row workspace storing {(num_above, num_ties)} + the gathered ties.
struct WorkSpace {
uint2 metadata;
Tie ties[kMaxTies];
};
static constexpr uint32_t kWorkspaceInts = sizeof(WorkSpace) / sizeof(uint32_t);
VLLM_DSV4_DEVICE static void stage1_init(void* _smem) {
const auto tx = threadIdx.x;
__builtin_assume(tx < kBlockSize);
const auto smem = static_cast<Smem*>(_smem);
if (tx < kHistBins) smem->histogram[tx] = 0;
if (tx < kNumStages) ptx::mbarrier_init(&smem->barrier[tx], 1);
__syncthreads();
}
VLLM_DSV4_DEVICE static void stage1_prologue(const float* scores,
uint32_t length, void* _smem) {
if (threadIdx.x == 0) {
const auto smem = static_cast<Smem*>(_smem);
const auto num_stages = (length + kSizePerStage - 1) / kSizePerStage;
const auto length_aligned = (length + 3u) & ~3u;
#pragma unroll
for (uint32_t stage = 0; stage < kNumStages; stage++) {
if (stage >= num_stages) break;
const auto offset = stage * kSizePerStage;
const auto size = min(kSizePerStage, length_aligned - offset);
const auto size_bytes = size * sizeof(float);
const auto bar = &smem->barrier[stage];
ptx::tma_load(smem->score_buffer[stage], scores + offset, size_bytes,
bar);
ptx::mbarrier_arrive_expect_tx(bar, size_bytes);
}
}
}
VLLM_DSV4_DEVICE static void stage1(int32_t* indices, uint32_t length,
void* _smem, bool reuse = false) {
const auto smem = static_cast<Smem*>(_smem);
const auto tx = threadIdx.x;
__builtin_assume(tx < kBlockSize);
const auto lane_id = tx % kWarpThreads;
const auto warp_id = tx / kWarpThreads;
// Local histogram.
#pragma unroll
for (uint32_t stage = 0; stage < kNumStages; stage++) {
const auto offset = stage * kSizePerStage;
if (offset >= length) break;
const auto size = min(kSizePerStage, length - offset);
if (lane_id == 0) ptx::mbarrier_wait(&smem->barrier[stage], 0);
__syncwarp();
#pragma unroll
for (uint32_t i = 0; i < kElemPerStage; ++i) {
const auto idx = tx + i * kBlockSize;
if (idx >= size) break;
const auto score = smem->score_buffer[stage][idx];
const auto bin = extract_coarse_bin<kHistBits>(score);
atomicAdd(&smem->histogram[bin], 1);
}
}
static_assert(kHistBins <= kBlockSize);
// Two-shot all-reduce across the cluster.
{
auto cluster = cooperative_groups::this_cluster();
cluster.sync();
const auto cluster_rank = blockIdx.y;
const auto kLocalSize = kHistBins / kClusterSize;
const auto offset = kLocalSize * cluster_rank;
const auto src_tx = tx / kClusterSize;
const auto src_rank = tx % kClusterSize;
if (tx < kHistBins) {
const auto addr = &smem->histogram[offset + src_tx];
const auto src_addr = cluster.map_shared_rank(addr, src_rank);
*src_addr = warp_reduce_sum<kClusterSize>(*src_addr);
}
cluster.sync();
}
// Each block now holds the full cluster histogram. Find the threshold.
{
const auto value = tx < kHistBins ? smem->histogram[tx] : 0;
const auto warp_inc = warp_inclusive_sum(lane_id, value);
if (lane_id == kWarpThreads - 1) {
smem->warp_sum[warp_id] = warp_inc;
}
__syncthreads();
const auto tmp = smem->warp_sum[lane_id];
const auto total_length = warp_reduce_sum(tmp);
uint32_t prefix_sum = warp_reduce_sum(lane_id < warp_id ? tmp : 0);
prefix_sum += warp_inc;
const auto above = total_length - prefix_sum;
if (tx < kHistBins && above < K && above + value >= K) {
smem->counter_gt = smem->counter_eq = 0;
smem->match = {
.bin = tx,
.above_count = above,
.equal_count = value,
};
}
__syncthreads();
}
const auto thr_bin = smem->match.bin;
// Scatter strictly-above entries to `indices`, stash ties in tie_buffer.
#pragma unroll
for (uint32_t stage = 0; stage < kNumStages; stage++) {
const auto offset = stage * kSizePerStage;
if (offset >= length) break;
#pragma unroll
for (uint32_t i = 0; i < kElemPerStage; ++i) {
const auto buf_idx = tx + i * kBlockSize;
const auto global_idx = offset + buf_idx;
if (global_idx >= length) break;
const auto score = smem->score_buffer[stage][buf_idx];
const auto bin = extract_coarse_bin<kHistBits>(score);
if (bin > thr_bin) {
indices[atomicAdd(&smem->counter_gt, 1)] = global_idx;
} else if (bin == thr_bin) {
const auto pos = atomicAdd(&smem->counter_eq, 1);
if (pos < kMaxTies) smem->tie_buffer[pos] = {global_idx, score};
}
}
}
if (reuse) {
const auto num_stages = (length + kSizePerStage - 1) / kSizePerStage;
if (tx < kHistBins) smem->histogram[tx] = 0;
if (tx < num_stages) ptx::mbarrier_arrive(&smem->barrier[tx]);
}
__syncthreads();
}
VLLM_DSV4_DEVICE static void stage1_epilogue(TransformParams params,
uint32_t offset, void* _ws,
void* _smem) {
auto cluster = cooperative_groups::this_cluster();
const auto smem = static_cast<Smem*>(_smem);
const auto tx = threadIdx.x;
const auto local_above = smem->counter_gt;
const auto local_equal = smem->counter_eq;
const auto cluster_rank = blockIdx.y;
constexpr uint32_t kAboveMask = (1 << kAboveBits) - 1;
static_assert(kAboveMask >= K);
static_assert(kMaxTies <= kBlockSize);
const auto idx_above = tx < local_above ? params.indices_in[tx] : 0;
const auto tie_value = tx < local_equal ? smem->tie_buffer[tx] : Tie{0, 0.0f};
// Push counts to remote shared memory to reduce inter-block latency.
if (tx < kClusterSize) {
const auto value = (local_equal << kAboveBits) | local_above;
const auto dst_addr = cluster.map_shared_rank(smem->local_above_equal, tx);
dst_addr[cluster_rank] = value;
}
// After this final sync, every block can read only its own smem (peer
// ranks may have already exited), so we don't touch remote smem again.
cluster.sync();
if (tx < kClusterSize) {
const auto value = tx < cluster_rank ? smem->local_above_equal[tx] : 0;
const auto kActiveMask = (1u << kClusterSize) - 1;
smem->prefix_above_equal = warp_reduce_sum<kClusterSize>(value, kActiveMask);
}
__syncthreads();
const auto prefix_packed = smem->prefix_above_equal;
const auto prefix_above = prefix_packed & kAboveMask;
const auto prefix_equal = prefix_packed >> kAboveBits;
// Page-translate strictly-above entries.
if (tx < local_above) {
params.write(tx + prefix_above, idx_above + offset);
}
// Stage ties into the per-row workspace (regular global writes).
const auto ws = static_cast<WorkSpace*>(_ws);
if (tx < local_equal && tx + prefix_equal < kMaxTies) {
ws->ties[tx + prefix_equal] = {tie_value.idx + offset, tie_value.score};
}
// Last cluster rank publishes the sums into ws->metadata.
if (cluster_rank == kClusterSize - 1 && tx == 0) {
const auto sum_above = prefix_above + local_above;
const auto sum_equal = prefix_equal + local_equal;
ws->metadata = make_uint2(sum_above, sum_equal);
}
}
VLLM_DSV4_DEVICE static void transform(TransformParams params, const void* _ws,
void* _smem) {
const auto ws = static_cast<const WorkSpace*>(_ws);
const auto meta = &ws->metadata;
const auto num_above = meta->x;
const auto num_equal = meta->y;
if (num_above >= K || num_equal == 0) return;
const auto clamped_ties = min(num_equal, kMaxTies);
tie_handle_transform(ws->ties, clamped_ties, num_above, K, params, _smem);
}
};
} // namespace vllm::dsv4_topk
+195
View File
@@ -0,0 +1,195 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
//
// Shared types/utilities for the three DeepSeek V4 top-k strategies
// (Register / Streaming / Cluster). Ported from sglang's
// jit_kernel/include/sgl_kernel/deepseek_v4/topk/common.cuh.
#pragma once
#include "utils.cuh"
#include <cuda_fp16.h>
#include <cstdint>
namespace vllm::dsv4_topk {
inline constexpr uint32_t kMaxTopK = 1024;
inline constexpr uint32_t kBlockSize = 1024;
inline constexpr uint32_t kNumWarps = kBlockSize / kWarpThreads;
// 1 element per thread in the tie-breaking pass.
inline constexpr uint32_t kMaxTies = 1024;
inline constexpr uint32_t kRadixBins = 256;
static_assert(kMaxTopK <= kBlockSize && kMaxTies <= kBlockSize);
// Always vectorize global loads as float4.
using Vec4 = AlignedVector<float, 4>;
// page_to_indices: convert a flat compressed-token index into a (block * page_size + offset)
// page-table-resolved index. page_size must be a power of 2; page_bits = log2(page_size).
VLLM_DSV4_DEVICE int32_t page_to_indices(const int32_t* __restrict__ page_table,
uint32_t i, uint32_t page_bits) {
const uint32_t mask = (1u << page_bits) - 1u;
return (page_table[i >> page_bits] << page_bits) | (i & mask);
}
// Output-side description of the page-table fold-in: each strategy writes
// either (a) `transform(idx)` for entries already known to be in the top-k,
// or (b) `write(dst, src)` for entries whose final rank is determined later.
struct TransformParams {
const int32_t* __restrict__ page_table;
const int32_t* __restrict__ indices_in;
int32_t* __restrict__ indices_out;
uint32_t page_bits;
VLLM_DSV4_DEVICE void transform(uint32_t idx) const {
indices_out[idx] = page_to_indices(page_table, indices_in[idx], page_bits);
}
VLLM_DSV4_DEVICE void write(uint32_t dst, uint32_t src) const {
indices_out[dst] = page_to_indices(page_table, src, page_bits);
}
};
struct alignas(16) MatchBin {
uint32_t bin;
uint32_t above_count;
uint32_t equal_count;
};
struct alignas(8) Tie {
uint32_t idx;
float score;
};
// Shared-memory layout for the final tie-breaking radix pass. Reused by both
// the streaming kernel (overlapping `score_buffer`) and the cluster kernel.
struct TieHandleSmem {
alignas(128) uint32_t counter;
alignas(128) MatchBin match;
uint32_t histogram[kRadixBins];
uint32_t warp_sum[kNumWarps];
};
// Order-preserving fp32 -> uint key, truncated to the top kBits. Used for the
// coarse histogram pass.
template <uint32_t kBits>
VLLM_DSV4_DEVICE uint32_t extract_coarse_bin(float x) {
static_assert(0 < kBits && kBits < 15);
__half h = __float2half_rn(x);
uint16_t bits = __half_as_ushort(h);
uint16_t key = (bits & 0x8000) ? static_cast<uint16_t>(~bits)
: static_cast<uint16_t>(bits | 0x8000);
return key >> (16 - kBits);
}
// Full 32-bit order-preserving key, used in tie-breaking.
VLLM_DSV4_DEVICE uint32_t extract_exact_bin(float x) {
uint32_t bits = __float_as_uint(x);
return (bits & 0x80000000u) ? ~bits : (bits | 0x80000000u);
}
VLLM_DSV4_DEVICE uint32_t warp_inclusive_sum(uint32_t lane_id, uint32_t val) {
static_assert(kWarpThreads == 32);
#pragma unroll
for (uint32_t offset = 1; offset < 32; offset *= 2) {
uint32_t n = __shfl_up_sync(0xFFFFFFFF, val, offset);
if (lane_id >= offset) val += n;
}
return val;
}
// Fast path when seq_len <= K: identity mapping, padded to K with -1.
VLLM_DSV4_DEVICE void trivial_transform(const TransformParams& params,
uint32_t length, uint32_t K) {
const auto tx = threadIdx.x;
if (tx < length) {
params.write(tx, tx);
} else if (tx < K) {
params.indices_out[tx] = -1;
}
}
// Tie-break the threshold-bin candidates that didn't fit in the strict-above
// region. One block-wide radix pass over the full 32-bit key (fp32 bit
// pattern, with idx as a secondary key). Writes at most `K - num_above`
// entries via params.write(...).
VLLM_DSV4_DEVICE void tie_handle_transform(const Tie* __restrict__ ties,
uint32_t num_ties, uint32_t num_above,
uint32_t K, TransformParams params,
void* _smem) {
auto* smem = static_cast<TieHandleSmem*>(_smem);
const auto tx = threadIdx.x;
const auto lane_id = tx % kWarpThreads;
const auto warp_id = tx / kWarpThreads;
const bool has_elem = tx < num_ties;
const auto tie = has_elem ? ties[tx] : Tie{0, 0.0f};
const uint32_t key = extract_exact_bin(tie.score);
const uint32_t idx = tie.idx;
bool active = has_elem;
uint32_t topk_remain = K - num_above;
uint32_t write_pos = K;
smem->counter = 0;
__syncthreads();
// 256 bins / 32 lanes = 8 warps span the histogram inter-warp prefix.
constexpr uint32_t kRadixWarps = kRadixBins / kWarpThreads;
#pragma unroll
for (int round = 0; round < 4; round++) {
const uint32_t shift = 24 - round * 8;
const uint32_t bin = (key >> shift) & 0xFFu;
// 1. Histogram.
if (tx < kRadixBins) smem->histogram[tx] = 0;
__syncthreads();
if (active) atomicAdd(&smem->histogram[bin], 1);
__syncthreads();
// 2. Two-pass prefix sum across the 256 bins.
uint32_t hist_val = 0;
uint32_t warp_inc = 0;
if (tx < kRadixBins) {
hist_val = smem->histogram[tx];
warp_inc = warp_inclusive_sum(lane_id, hist_val);
if (lane_id == kWarpThreads - 1) smem->warp_sum[warp_id] = warp_inc;
}
__syncthreads();
if (tx < kRadixBins) {
const auto tmp = (lane_id < kRadixWarps) ? smem->warp_sum[lane_id] : 0;
const auto total = warp_reduce_sum(tmp);
const auto inter = warp_reduce_sum(lane_id < warp_id ? tmp : 0);
const auto prefix = inter + warp_inc;
const auto above = total - prefix;
// 3. Find threshold bin.
if (above < topk_remain && above + hist_val >= topk_remain) {
smem->match = {tx, above, topk_remain - above};
}
}
__syncthreads();
const auto thr = smem->match.bin;
const auto n_above = smem->match.above_count;
// 4. Scatter.
if (active) {
if (bin > thr) {
write_pos = num_above + atomicAdd(&smem->counter, 1);
active = false;
} else if (bin < thr) {
active = false;
} else if (round == 3) {
write_pos = K - atomicAdd(&smem->match.equal_count, -1u);
}
// bin == thr && round < 3: stay active for the next radix round.
}
topk_remain -= n_above;
if (topk_remain == 0) break;
}
if (write_pos < K) params.write(write_pos, idx);
}
} // namespace vllm::dsv4_topk
+66
View File
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
//
// Thin wrappers around the CUDA PTX intrinsics used by the top-k pipeline.
// All of these require sm_90+. Ported from sglang's
// jit_kernel/include/sgl_kernel/deepseek_v4/topk/ptx.cuh.
#pragma once
#include "utils.cuh"
#include <cuda/ptx>
#include <cstdint>
namespace vllm::dsv4_topk::ptx {
VLLM_DSV4_DEVICE void mbarrier_init(uint64_t* addr, uint32_t arrives) {
cuda::ptx::mbarrier_init(addr, arrives);
}
VLLM_DSV4_DEVICE void mbarrier_arrive(uint64_t* addr) {
cuda::ptx::mbarrier_arrive(cuda::ptx::sem_relaxed, cuda::ptx::scope_cta,
cuda::ptx::space_shared, addr);
}
VLLM_DSV4_DEVICE void mbarrier_arrive_expect_tx(uint64_t* addr, uint32_t tx) {
cuda::ptx::mbarrier_arrive_expect_tx(cuda::ptx::sem_relaxed,
cuda::ptx::scope_cta,
cuda::ptx::space_shared, addr, tx);
}
VLLM_DSV4_DEVICE void mbarrier_wait(uint64_t* addr, uint32_t phase) {
while (!cuda::ptx::mbarrier_try_wait_parity(cuda::ptx::sem_relaxed,
cuda::ptx::scope_cta, addr,
phase))
;
}
VLLM_DSV4_DEVICE void tma_load(void* dst, const void* src, uint32_t num_bytes,
uint64_t* mbar) {
cuda::ptx::cp_async_bulk(cuda::ptx::space_shared, cuda::ptx::space_global,
dst, src, num_bytes, mbar);
}
// elect.sync: pick a single arbitrary thread out of an active mask. Used to
// fire a single TMA load per warp without the full ``if (tx == 0)`` cost.
VLLM_DSV4_DEVICE uint32_t elect_sync() {
uint32_t pred = 0;
asm volatile(
"{\n\t"
".reg .pred %%px;\n\t"
"elect.sync _|%%px, %1;\n\t"
"@%%px mov.s32 %0, 1;\n\t"
"}"
: "+r"(pred)
: "r"(0xFFFFFFFF));
return pred;
}
VLLM_DSV4_DEVICE bool elect_sync_cta(uint32_t tx) {
const auto warp_id = tx / 32;
const auto uniform_warp_id = __shfl_sync(0xFFFFFFFF, warp_id, 0);
return (uniform_warp_id == 0 && elect_sync());
}
} // namespace vllm::dsv4_topk::ptx
+313
View File
@@ -0,0 +1,313 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
//
// Register-resident top-k strategy for the DeepSeek V4 indexer (small N
// fast path). One block per row; up to ``kMax2PassLength`` scores per row
// streamed through registers, with a single 12-bit-coarse radix pass and
// a final tie-break round. Ported from
// jit_kernel/include/sgl_kernel/deepseek_v4/topk/register.cuh.
#pragma once
#include "common.cuh"
#include "ptx.cuh"
#include "utils.cuh"
#include <cfloat>
#include <cstdint>
namespace vllm::dsv4_topk {
template <uint32_t K>
struct RegisterTopK {
static constexpr uint32_t kHistBits = 12;
static constexpr uint32_t kHistBins = 1 << kHistBits;
static constexpr uint32_t kVecsPerThread = 4;
static constexpr uint32_t kMaxTolerance = 0;
// Length covered by registers in a single pass.
static constexpr uint32_t kMax1PassLength = kVecsPerThread * 4 * kBlockSize;
// Extra length staged through shared memory in the 2-pass path.
static constexpr uint32_t kMaxExtraLength = kMax1PassLength;
static constexpr uint32_t kMax2PassLength = kMax1PassLength + kMaxExtraLength;
struct Smem {
using HistVec = AlignedVector<uint32_t, kHistBins / kBlockSize>;
alignas(128) uint32_t counter_gt;
alignas(128) uint32_t counter_eq;
uint64_t mbarrier; // for the cp.async.bulk in the 2-pass path
MatchBin match;
uint32_t warp_sum[kNumWarps];
union {
uint32_t histogram[kHistBins];
HistVec histogram_vec[kBlockSize];
Tie tie_buffer[kMaxTies];
};
alignas(16) float score_buffer[kMaxExtraLength];
};
template <bool kIs2Pass = false>
VLLM_DSV4_DEVICE static void run(const float* scores, int32_t* indices,
uint32_t length, void* _smem,
bool use_pdl = false) {
const auto smem = static_cast<Smem*>(_smem);
const auto tx = threadIdx.x;
const auto lane_id = tx % kWarpThreads;
const auto warp_id = tx / kWarpThreads;
// Init histogram + counters.
{
typename Smem::HistVec hist_vec;
hist_vec.fill(0);
smem->histogram_vec[tx] = hist_vec;
if (tx == 0) {
smem->counter_gt = smem->counter_eq = 0;
if constexpr (kIs2Pass) {
ptx::mbarrier_init(&smem->mbarrier, 1);
}
}
__syncthreads();
}
if (use_pdl) pdl_wait_primary<true>();
// Stream the first `kMax1PassLength` scores into registers.
Vec4 local[kVecsPerThread];
#pragma unroll
for (uint32_t v = 0; v < kVecsPerThread; ++v) {
const uint32_t base = (tx + v * kBlockSize) * 4;
if (base >= length) break;
local[v].load(scores, tx + v * kBlockSize);
}
// Issue the 2-pass TMA prefetch (next chunk of scores into smem).
if constexpr (kIs2Pass) {
if (ptx::elect_sync_cta(tx)) {
const auto length_aligned = (length + 3u - kMax1PassLength) & ~3u;
const auto size_bytes = length_aligned * sizeof(float);
ptx::tma_load(smem->score_buffer, scores + kMax1PassLength, size_bytes,
&smem->mbarrier);
ptx::mbarrier_arrive_expect_tx(&smem->mbarrier, size_bytes);
}
__syncwarp();
}
// Phase 1: histogram via shared-memory atomics.
#pragma unroll
for (uint32_t v = 0; v < kVecsPerThread; ++v) {
#pragma unroll
for (uint32_t e = 0; e < 4; ++e) {
if constexpr (!kIs2Pass) {
const uint32_t idx = (tx + v * kBlockSize) * 4 + e;
if (idx >= length) goto LABEL_ACC_FINISH;
}
atomicAdd(&smem->histogram[extract_coarse_bin<kHistBits>(local[v][e])],
1);
}
}
if constexpr (kIs2Pass) {
if (lane_id == 0) ptx::mbarrier_wait(&smem->mbarrier, 0);
__syncwarp();
for (uint32_t i = tx; i + kMax1PassLength < length; i += kBlockSize) {
const auto val = smem->score_buffer[i];
atomicAdd(&smem->histogram[extract_coarse_bin<kHistBits>(val)], 1);
}
}
[[maybe_unused]] LABEL_ACC_FINISH:
__syncthreads();
// Phase 2: prefix scan over the histogram, locate the threshold bin.
{
constexpr uint32_t kItems = kHistBins / kBlockSize;
uint32_t orig[kItems];
const auto hist_vec = smem->histogram_vec[tx];
uint32_t tmp_local_sum = 0;
#pragma unroll
for (uint32_t i = 0; i < kItems; ++i) {
orig[i] = hist_vec[i];
tmp_local_sum += orig[i];
}
const auto warp_inc = warp_inclusive_sum(lane_id, tmp_local_sum);
const auto warp_exc = warp_inc - tmp_local_sum;
if (lane_id == kWarpThreads - 1) {
smem->warp_sum[warp_id] = warp_inc;
}
__syncthreads();
const auto tmp = smem->warp_sum[lane_id];
// Exactly one bin satisfies above < K && above + count >= K.
uint32_t prefix_sum = warp_reduce_sum(lane_id < warp_id ? tmp : 0);
prefix_sum += warp_exc;
#pragma unroll
for (uint32_t i = 0; i < kItems; ++i) {
prefix_sum += orig[i];
const auto above = length - prefix_sum;
if (above < K && above + orig[i] >= K) {
smem->match = {
.bin = tx * kItems + i,
.above_count = above,
.equal_count = orig[i],
};
}
}
__syncthreads();
}
const auto thr_bin = smem->match.bin;
const auto num_above = smem->match.above_count;
const auto num_equal = smem->match.equal_count;
// Phase 3: Scatter.
// - bin > thr -> write directly to output (strictly above).
// - bin == thr -> when no tie-break is needed, admit first-come;
// otherwise stash into tie_buffer for phase 4.
const bool need_tiebreak = (num_equal + num_above > K + kMaxTolerance);
const auto topk_indices = indices;
const auto tie_buffer = smem->tie_buffer;
#pragma unroll
for (uint32_t v = 0; v < kVecsPerThread; ++v) {
#pragma unroll
for (uint32_t e = 0; e < 4; ++e) {
const uint32_t idx = (tx + v * kBlockSize) * 4 + e;
if constexpr (!kIs2Pass) {
if (idx >= length) goto LABEL_SCATTER_DONE;
}
const uint32_t bin = extract_coarse_bin<kHistBits>(local[v][e]);
if (bin > thr_bin) {
topk_indices[atomicAdd(&smem->counter_gt, 1)] = idx;
} else if (bin == thr_bin) {
const auto pos = atomicAdd(&smem->counter_eq, 1);
if (need_tiebreak) {
if (pos < kMaxTies) {
tie_buffer[pos] = {.idx = idx, .score = local[v][e]};
}
} else {
if (const auto which = pos + num_above; which < K) {
topk_indices[which] = idx;
}
}
}
}
// 2-pass: pull the next chunk in from the staged smem buffer.
if constexpr (kIs2Pass) {
local[v].load(smem->score_buffer, tx + v * kBlockSize);
}
}
if constexpr (kIs2Pass) {
#pragma unroll
for (uint32_t v = 0; v < kVecsPerThread; ++v) {
#pragma unroll
for (uint32_t e = 0; e < 4; ++e) {
const uint32_t idx =
(tx + v * kBlockSize) * 4 + e + kMax1PassLength;
if (idx >= length) goto LABEL_SCATTER_DONE;
const uint32_t bin = extract_coarse_bin<kHistBits>(local[v][e]);
if (bin > thr_bin) {
topk_indices[atomicAdd(&smem->counter_gt, 1)] = idx;
} else if (bin == thr_bin) {
const auto pos = atomicAdd(&smem->counter_eq, 1);
if (need_tiebreak) {
if (pos < kMaxTies) {
tie_buffer[pos] = {.idx = idx, .score = local[v][e]};
}
} else {
if (const auto which = pos + num_above; which < K) {
topk_indices[which] = idx;
}
}
}
}
}
}
[[maybe_unused]] LABEL_SCATTER_DONE:
if (!need_tiebreak) return;
// Phase 4: tie-break within the threshold bin. We assume num_ties <=
// kBlockSize (one block of ties), so each thread takes one tied element,
// counts the number of tied elements with strictly higher (score, -idx),
// and writes to output if its rank is below the remaining quota.
__syncthreads();
static_assert(kMaxTies <= kBlockSize);
const uint32_t num_ties = min(num_equal, kMaxTies);
const uint32_t topk_remain = K - num_above;
const auto is_greater = [](const Tie& a, const Tie& b) {
return (a.score > b.score) || (a.score == b.score && a.idx < b.idx);
};
if (num_ties <= kWarpThreads) {
static_assert(kWarpThreads <= kNumWarps);
if (lane_id >= num_ties || warp_id >= num_ties) return;
const uint32_t mask = (1ull << num_ties) - 1u;
const auto tie = tie_buffer[lane_id];
const auto target_tie = tie_buffer[warp_id];
const bool pred = is_greater(tie, target_tie);
const auto rank =
static_cast<uint32_t>(__popc(__ballot_sync(mask, pred)));
if (lane_id == 0 && rank < topk_remain) {
topk_indices[num_above + rank] = target_tie.idx;
}
} else if (num_ties <= kWarpThreads * 2) {
// 64x64 case: each thread takes 2 elements.
const auto lane_id_1 = lane_id + kWarpThreads;
const auto warp_id_1 = warp_id + kWarpThreads;
const auto invalid = Tie{.idx = 0xFFFFFFFFu, .score = -FLT_MAX};
const auto tie_0 = tie_buffer[lane_id];
const auto tie_1 = lane_id_1 < num_ties ? tie_buffer[lane_id_1] : invalid;
{
const auto target = tie_buffer[warp_id];
const bool pred_0 = is_greater(tie_0, target);
const bool pred_1 = is_greater(tie_1, target);
const auto rank_0 =
static_cast<uint32_t>(__popc(__ballot_sync(0xFFFFFFFF, pred_0)));
const auto rank_1 =
static_cast<uint32_t>(__popc(__ballot_sync(0xFFFFFFFF, pred_1)));
const auto rank = rank_0 + rank_1;
if (lane_id == 0 && rank < topk_remain) {
topk_indices[num_above + rank] = target.idx;
}
}
if (warp_id_1 < num_ties) {
const auto target = tie_buffer[warp_id_1];
const bool pred_0 = is_greater(tie_0, target);
const bool pred_1 = is_greater(tie_1, target);
const auto rank_0 =
static_cast<uint32_t>(__popc(__ballot_sync(0xFFFFFFFF, pred_0)));
const auto rank_1 =
static_cast<uint32_t>(__popc(__ballot_sync(0xFFFFFFFF, pred_1)));
const auto rank = rank_0 + rank_1;
if (lane_id == 0 && rank < topk_remain) {
topk_indices[num_above + rank] = target.idx;
}
}
} else {
[[unlikely]];
// Block-wide fallback. Rarely reached.
for (auto i = warp_id; i < num_ties; i += kNumWarps) {
const auto target_tie = tie_buffer[i];
uint32_t local_rank = 0;
for (auto j = lane_id; j < num_ties; j += kWarpThreads) {
const auto tie = tie_buffer[j];
if (is_greater(tie, target_tie)) local_rank++;
}
const auto rank = warp_reduce_sum(local_rank);
if (lane_id == 0 && rank < topk_remain) {
topk_indices[num_above + rank] = target_tie.idx;
}
}
}
}
VLLM_DSV4_DEVICE static void transform(TransformParams params) {
__syncthreads();
if (const auto tx = threadIdx.x; tx < K) params.transform(tx);
}
};
} // namespace vllm::dsv4_topk
+208
View File
@@ -0,0 +1,208 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
//
// Streaming top-k strategy for medium N. Uses a TMA-driven double-buffered
// histogram pass + scatter pass over chunks of `kSizePerStage` floats.
// Ported from
// jit_kernel/include/sgl_kernel/deepseek_v4/topk/streaming.cuh.
#pragma once
#include "common.cuh"
#include "ptx.cuh"
#include "utils.cuh"
#include <cfloat>
#include <cstdint>
namespace vllm::dsv4_topk {
template <uint32_t K>
struct StreamingTopK {
static constexpr uint32_t kHistBits = 12;
static constexpr uint32_t kHistBins = 1 << kHistBits;
static constexpr uint32_t kElemPerStage = 8;
static constexpr uint32_t kSizePerStage = kElemPerStage * kBlockSize;
static constexpr uint32_t kNumStages = 2; // double buffer
static constexpr uint32_t kHistItems = kHistBins / kBlockSize; // 4
static_assert(kHistItems * kBlockSize == kHistBins);
using HistVec = AlignedVector<uint32_t, kHistItems>;
struct Smem {
// [phase = 0 (histogram) | 1 (scatter)] x [buffer = 0 | 1]
uint64_t barrier[2][kNumStages];
alignas(128) uint32_t counter_gt;
alignas(128) uint32_t counter_eq;
alignas(128) MatchBin match;
alignas(128) uint32_t warp_sum[kNumWarps];
union {
uint32_t histogram[kHistBins];
HistVec histogram_vec[kBlockSize];
Tie tie_buffer[kMaxTies];
};
union {
float score_buffer[kNumStages][kSizePerStage];
TieHandleSmem stage2; // reused for the tie-handling phase
};
};
// length must be 4-aligned (caller rounds up); TMA wants 16-byte alignment.
template <bool kIsScatter>
VLLM_DSV4_DEVICE static void issue_tma(const float* scores, uint32_t stage,
uint32_t length, Smem* smem) {
const auto buf_idx = stage % kNumStages;
const auto offset = stage * kSizePerStage;
const auto size = min(kSizePerStage, length - offset);
const auto size_bytes = size * sizeof(float);
const auto bar = &smem->barrier[kIsScatter][buf_idx];
ptx::tma_load(smem->score_buffer[buf_idx], scores + offset, size_bytes,
bar);
ptx::mbarrier_arrive_expect_tx(bar, size_bytes);
}
// Unified streaming pass. kIsScatter=false: build histogram (phase A).
// kIsScatter=true: scatter using the threshold bin (phase C). Each barrier
// is reused across iterations via the reuse-arrive pattern.
template <bool kIsScatter>
VLLM_DSV4_DEVICE static void stream_pass(const float* scores, uint32_t length,
uint32_t thr_bin,
int32_t* s_topk_indices,
Smem* smem) {
const auto tx = threadIdx.x;
const auto num_iters = (length + kSizePerStage - 1) / kSizePerStage;
const auto lane_id = tx % kWarpThreads;
const auto length_aligned = (length + 3u) & ~3u;
if (tx == 0) {
#pragma unroll
for (uint32_t i = 0; i < kNumStages; i++) {
if (i >= num_iters) break;
issue_tma<kIsScatter>(scores, i, length_aligned, smem);
}
}
for (uint32_t iter = 0; iter < num_iters; iter++) {
const auto buf_idx = iter % kNumStages;
const auto offset = iter * kSizePerStage;
const auto this_size = min(kSizePerStage, length - offset);
if (lane_id == 1) {
const auto phase_bit = (iter / kNumStages) & 1;
ptx::mbarrier_wait(&smem->barrier[kIsScatter][buf_idx], phase_bit);
}
__syncwarp();
#pragma unroll
for (uint32_t i = 0; i < kElemPerStage; i++) {
const auto local_idx = tx + i * kBlockSize;
if (local_idx >= this_size) break;
const auto score = smem->score_buffer[buf_idx][local_idx];
const auto bin = extract_coarse_bin<kHistBits>(score);
if constexpr (kIsScatter) {
const auto global_idx = offset + local_idx;
if (bin > thr_bin) {
const auto pos = atomicAdd(&smem->counter_gt, 1);
if (pos < K) s_topk_indices[pos] = global_idx;
} else if (bin == thr_bin) {
const auto pos = atomicAdd(&smem->counter_eq, 1);
if (pos < kMaxTies) smem->tie_buffer[pos] = {global_idx, score};
}
} else {
atomicAdd(&smem->histogram[bin], 1);
}
}
__syncthreads();
if (tx == 0) {
if (const auto next_iter = iter + kNumStages; next_iter < num_iters) {
issue_tma<kIsScatter>(scores, next_iter, length_aligned, smem);
}
}
}
}
// Phase B: locate threshold bin via warp-level prefix scan.
VLLM_DSV4_DEVICE static void find_threshold(uint32_t length, Smem* smem) {
const auto tx = threadIdx.x;
const auto lane_id = tx % kWarpThreads;
const auto warp_id = tx / kWarpThreads;
uint32_t orig[kHistItems];
const auto hist_vec = smem->histogram_vec[tx];
uint32_t local_sum = 0;
#pragma unroll
for (uint32_t i = 0; i < kHistItems; ++i) {
orig[i] = hist_vec[i];
local_sum += orig[i];
}
const auto warp_inc = warp_inclusive_sum(lane_id, local_sum);
const auto warp_exc = warp_inc - local_sum;
if (lane_id == kWarpThreads - 1) smem->warp_sum[warp_id] = warp_inc;
__syncthreads();
const auto tmp = smem->warp_sum[lane_id];
uint32_t prefix_sum = warp_reduce_sum(lane_id < warp_id ? tmp : 0);
prefix_sum += warp_exc;
#pragma unroll
for (uint32_t i = 0; i < kHistItems; ++i) {
prefix_sum += orig[i];
const auto above = length - prefix_sum;
if (above < K && above + orig[i] >= K) {
smem->match = {
.bin = tx * kHistItems + i,
.above_count = above,
.equal_count = orig[i],
};
}
}
__syncthreads();
}
VLLM_DSV4_DEVICE static void run(const float* scores, uint32_t length,
int32_t* topk_indices, void* _smem) {
const auto smem = static_cast<Smem*>(_smem);
const auto tx = threadIdx.x;
__builtin_assume(tx < kBlockSize);
{
HistVec zero;
zero.fill(0);
smem->histogram_vec[tx] = zero;
if (tx < 2 * kNumStages) {
const auto base_barrier = &smem->barrier[0][0];
ptx::mbarrier_init(&base_barrier[tx], 1);
}
if (tx == 0) {
smem->counter_gt = 0;
smem->counter_eq = 0;
}
__syncthreads();
}
// Phase A: histogram.
stream_pass<false>(scores, length, 0, nullptr, smem);
// Phase B: threshold bin.
find_threshold(length, smem);
// Phase C: scatter.
stream_pass<true>(scores, length, smem->match.bin, topk_indices, smem);
}
VLLM_DSV4_DEVICE static void transform(TransformParams params, void* _smem) {
// Phase D: page-translate above entries, then refine ties.
const auto smem = static_cast<Smem*>(_smem);
const auto tx = threadIdx.x;
const auto num_above = smem->match.above_count;
if (tx < num_above) params.transform(tx);
const auto num_equal = smem->counter_eq;
if (num_above >= K || num_equal == 0) return;
const auto clamped_ties = min(num_equal, kMaxTies);
tie_handle_transform(smem->tie_buffer, clamped_ties, num_above, K, params,
&smem->stage2);
}
};
} // namespace vllm::dsv4_topk
+75
View File
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
//
// Minimal device-side utilities used by the DeepSeek V4 indexer top-k port.
// Replaces sgl_kernel/{utils,warp,vec,type}.cuh — we only need the bits the
// top-k kernels actually touch.
#pragma once
#include <cstddef>
#include <cstdint>
#include <cuda_fp16.h>
#include <cuda_runtime.h>
namespace vllm::dsv4_topk {
#define VLLM_DSV4_DEVICE __forceinline__ __device__
inline constexpr uint32_t kWarpThreads = 32u;
inline constexpr uint32_t kFullMask = 0xffffffffu;
// Programmatic Dependent Launch (sm_90+). When enabled, the kernel waits for
// the predecessor on the same stream to advance past its dependents-launch
// trigger before doing anything memory-dependent. Used to overlap the
// fp8_paged_mqa_logits epilogue with the first stage of top-k.
template <bool kUsePDL>
VLLM_DSV4_DEVICE void pdl_wait_primary() {
if constexpr (kUsePDL) {
asm volatile("griddepcontrol.wait;" ::: "memory");
}
}
template <bool kUsePDL>
VLLM_DSV4_DEVICE void pdl_trigger_secondary() {
if constexpr (kUsePDL) {
asm volatile("griddepcontrol.launch_dependents;" :::);
}
}
// Warp-level XOR-shuffle reduce. kThreads must be a power of 2 and <= 32.
template <uint32_t kThreads = kWarpThreads, typename T>
VLLM_DSV4_DEVICE T warp_reduce_sum(T value, uint32_t active_mask = kFullMask) {
#pragma unroll
for (auto offset = kThreads >> 1; offset > 0; offset >>= 1) {
value = value + __shfl_xor_sync(active_mask, value, offset, 32);
}
return value;
}
// 128-bit-aligned vector of N elements of T (N must be a power of 2, total
// size <= 16 bytes). Used for vectorized loads/stores into shared memory.
template <typename T, std::size_t N>
struct alignas(sizeof(T) * N) AlignedVector {
static_assert(N > 0 && (N & (N - 1)) == 0, "N must be a power of two");
static_assert(sizeof(T) * N <= 16,
"AlignedVector exceeds the 128-bit CUDA vector limit");
T data[N];
VLLM_DSV4_DEVICE void load(const void* ptr, std::size_t offset = 0) {
*reinterpret_cast<AlignedVector*>(this) =
reinterpret_cast<const AlignedVector*>(ptr)[offset];
}
VLLM_DSV4_DEVICE void store(void* ptr, std::size_t offset = 0) const {
reinterpret_cast<AlignedVector*>(ptr)[offset] = *this;
}
VLLM_DSV4_DEVICE void fill(T value) {
#pragma unroll
for (std::size_t i = 0; i < N; ++i) data[i] = value;
}
VLLM_DSV4_DEVICE T& operator[](std::size_t i) { return data[i]; }
VLLM_DSV4_DEVICE const T& operator[](std::size_t i) const { return data[i]; }
};
} // namespace vllm::dsv4_topk
+24
View File
@@ -125,6 +125,30 @@ void persistent_topk(const torch::Tensor& logits, const torch::Tensor& lengths,
torch::Tensor& output, torch::Tensor& workspace, int64_t k,
int64_t max_seq_len);
// DeepSeek V4 indexer top-k (k = 512). Hopper (sm_90a) and Blackwell
// datacenter (sm_100/sm_103) — needs thread-block clusters, TMA, and PDL.
// Two-step API:
// 1. fast_topk_v2_plan inspects the seq_lens distribution and writes a
// cluster_threshold + per-row Metadata into a (B+1, 4) int32 tensor. The
// plan is amortized when cudagraph-captured: once per shape, reused across
// layers.
// 2. fast_topk_v2 selects the top-512 indices per row, folds the page-table
// gather into the radix store, and writes (B, 512) int32 page indices.
// Dispatches per row to one of three strategies (Register / Streaming /
// Cluster) using the planned threshold.
//
// Returns the size in int32s of the per-row workspace required by
// fast_topk_v2 (allocate `(B, fast_topk_v2_workspace_ints())` int32 contig).
void fast_topk_v2_plan(const torch::Tensor& seq_lens, torch::Tensor& metadata,
int64_t static_cluster_threshold);
void fast_topk_v2(const torch::Tensor& scores, const torch::Tensor& seq_lens,
const torch::Tensor& page_table, torch::Tensor& page_indices,
int64_t page_size, const torch::Tensor& workspace,
const torch::Tensor& metadata);
int64_t fast_topk_v2_workspace_ints();
void rms_norm_static_fp8_quant(torch::Tensor& out, torch::Tensor& input,
torch::Tensor& weight, torch::Tensor& scale,
double epsilon);
+15
View File
@@ -215,6 +215,21 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
"Tensor workspace, int k, int max_seq_len) -> ()");
ops.impl("persistent_topk", torch::kCUDA, &persistent_topk);
// DeepSeek V4 indexer top-k (k=512), ported from sglang's topk_v2 family.
// Built for sm_90a (Hopper) + sm_100a/sm_103 (Blackwell datacenter).
// Schema only here; impl is registered in csrc/deepseek_v4/fast_topk_v2.cu
// so it's only present when CMake compiles the source for a supported arch.
ops.def(
"fast_topk_v2_plan(Tensor seq_lens, Tensor! metadata, "
"int static_cluster_threshold) -> ()");
ops.def(
"fast_topk_v2(Tensor scores, Tensor seq_lens, Tensor page_table, "
"Tensor! page_indices, int page_size, Tensor workspace, Tensor metadata)"
" -> ()");
ops.def("fast_topk_v2_workspace_ints() -> int");
// Layernorm-quant
// Apply Root Mean Square (RMS) Normalization to the input tensor.
ops.def(
+356
View File
@@ -0,0 +1,356 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Correctness tests for fast_topk_v2 (DeepSeek V4 indexer top-k, k=512).
Run::
.venv/bin/python -m pytest tests/kernels/test_fast_topk_v2.py -v
Coverage:
- All four execution paths: trivial (sl<=512), Register (1- and 2-pass),
Streaming, and Cluster.
- Both launch shapes: fused (batch<=kNumClusters=15) and two-stage (>15).
- Mixed-length batches that exercise the per-row dispatch in the stage-2
combine kernel.
- Page-table fold-in: parametrised across page_size in {1, 32, 64}.
The kernel emits page-table-resolved indices. By using
``page_table[b, i] = i`` with ``page_size=1`` we can compare the kernel's
output 1:1 against ``torch.topk`` on the masked scores. For other page sizes
the test inverts the page resolution before comparing.
"""
from __future__ import annotations
import pytest
import torch
from vllm.platforms import current_platform
from vllm.v1.attention.ops.deepseek_v4_ops.fast_topk import (
fast_topk_v2,
plan_topk_v2,
workspace_ints_per_batch,
)
# Match the kernel's compile-time constant.
TOPK = 512
# Thresholds inside the kernel (mirrors values in topk/register.cuh,
# topk_v2.cuh). Keep these in sync if the kernel changes.
SMALL_1PASS = 4 * 4 * 1024 # RegisterTopK::kMax1PassLength
SMALL_2PASS = 2 * SMALL_1PASS # RegisterTopK::kMax2PassLength = 32768
DEFAULT_CLUSTER_THRESHOLD = SMALL_2PASS # plan picks this for batch<=30
NUM_CLUSTERS = 15 # kNumClusters in fast_topk_v2.cu
# --------------------------------------------------------------------------
# Helpers
# --------------------------------------------------------------------------
def _max_blocks_for(seq_len: int, page_size: int) -> int:
return (seq_len + page_size - 1) // page_size
def _trivial_page_table(batch_size: int, max_blocks: int,
device: torch.device) -> torch.Tensor:
"""Identity page table: page_table[b, i] = i, so page_to_indices is a no-op
when ``page_size == 1`` (page_bits == 0)."""
return (
torch.arange(max_blocks, dtype=torch.int32, device=device)
.unsqueeze(0)
.expand(batch_size, -1)
.contiguous()
)
def _shuffled_page_table(batch_size: int, max_blocks: int, seed: int,
device: torch.device) -> torch.Tensor:
"""Per-row independent permutation of [0, max_blocks)."""
g = torch.Generator(device=device).manual_seed(seed)
rows = []
for _ in range(batch_size):
rows.append(torch.randperm(max_blocks, generator=g, device=device,
dtype=torch.int32))
return torch.stack(rows, dim=0)
def _resolve(raw_idx: int, b: int, page_table: torch.Tensor,
page_size: int) -> int:
"""Mirror of the device-side page_to_indices."""
block = raw_idx // page_size
offset = raw_idx % page_size
return int(page_table[b, block]) * page_size + offset
def _invert_resolved(resolved_idx: int, b: int, page_table: torch.Tensor,
page_size: int) -> int:
"""Find a raw_idx in [0, max_blocks*page_size) such that
_resolve(raw_idx, b) == resolved_idx. Used to translate kernel output
back to raw scores for comparison with torch.topk."""
block = resolved_idx // page_size
offset = resolved_idx % page_size
# Find the row in page_table[b] that holds `block`.
matches = (page_table[b] == block).nonzero(as_tuple=False)
assert matches.numel() == 1, (
f"page_table row {b} is not a permutation: block {block} appears "
f"{matches.numel()} times")
return int(matches.item()) * page_size + offset
def _reference_topk(scores: torch.Tensor, seq_lens: torch.Tensor,
page_table: torch.Tensor, page_size: int) -> list[set[int]]:
"""Per-row reference: page-resolved set of indices that fast_topk_v2
should emit (excluding -1 padding)."""
B, _ = scores.shape
out: list[set[int]] = []
for b in range(B):
sl = int(seq_lens[b])
if sl <= TOPK:
valid = list(range(sl))
else:
row = scores[b, :sl]
_, raw = torch.topk(row, TOPK)
valid = raw.tolist()
out.append({_resolve(i, b, page_table, page_size) for i in valid})
return out
def _check(scores: torch.Tensor, seq_lens: torch.Tensor,
page_table: torch.Tensor, page_size: int) -> None:
metadata = plan_topk_v2(seq_lens)
workspace = scores.new_empty(
(scores.shape[0], workspace_ints_per_batch()), dtype=torch.int32)
indices = fast_topk_v2(scores, seq_lens, page_table, page_size,
metadata=metadata, workspace=workspace)
torch.cuda.synchronize()
expected = _reference_topk(scores, seq_lens, page_table, page_size)
B = scores.shape[0]
for b in range(B):
sl = int(seq_lens[b])
valid_count = min(sl, TOPK)
row = indices[b].tolist()
# Padding region: -1 (only when sl < TOPK).
if sl < TOPK:
assert all(v == -1 for v in row[sl:]), (
f"row {b}: expected -1 padding after position {sl}, got "
f"{row[sl:sl + 8]}")
got = set(row[:valid_count])
assert -1 not in got, f"row {b}: -1 inside valid region (sl={sl})"
assert got == expected[b], (
f"row {b} (sl={sl}, page_size={page_size}): "
f"missing={len(expected[b] - got)} extra={len(got - expected[b])}")
# --------------------------------------------------------------------------
# Skip non-CUDA / non-Hopper-or-later
# --------------------------------------------------------------------------
def _supports_clusters() -> bool:
if not current_platform.is_cuda():
return False
major, _ = torch.cuda.get_device_capability()
# Thread-block clusters / TMA / PDL are sm_90+. sm_120 (consumer
# Blackwell) is missing some of these; skip when we detect it.
return major == 9 or major == 10
pytestmark = pytest.mark.skipif(
not _supports_clusters(),
reason="fast_topk_v2 requires sm_90 (Hopper) or sm_100 (Blackwell DC)",
)
# --------------------------------------------------------------------------
# Path coverage
# --------------------------------------------------------------------------
@pytest.mark.parametrize("seq_lens", [
pytest.param([1], id="trivial_1"),
pytest.param([300], id="trivial_300"),
pytest.param([512], id="trivial_boundary_512"),
pytest.param([513, 600, 100, 511, 512], id="trivial_mix"),
])
def test_trivial_path(seq_lens):
"""sl <= 512: identity-style fill, no radix, no tie-break."""
torch.manual_seed(0)
device = torch.device("cuda")
B = len(seq_lens)
L = max(max(seq_lens), 1024) # round up so stride is multiple of 4
L = (L + 3) & ~3
seq_lens_t = torch.tensor(seq_lens, dtype=torch.int32, device=device)
scores = torch.randn(B, L, dtype=torch.float32, device=device)
page_table = _trivial_page_table(B, _max_blocks_for(L, 1), device)
_check(scores, seq_lens_t, page_table, page_size=1)
@pytest.mark.parametrize("seq_len", [
pytest.param(513, id="just_above_topk"),
pytest.param(2048, id="2k"),
pytest.param(SMALL_1PASS - 1, id="register_1pass_max"),
pytest.param(SMALL_1PASS, id="register_1pass_boundary"),
pytest.param(SMALL_1PASS + 1, id="register_2pass_first"),
pytest.param(SMALL_2PASS - 1, id="register_2pass_max"),
])
def test_register_path(seq_len):
"""Register strategy (small N; both 1- and 2-pass)."""
torch.manual_seed(seq_len)
device = torch.device("cuda")
B = 4
L = (seq_len + 3) & ~3
scores = torch.randn(B, L, dtype=torch.float32, device=device)
seq_lens = torch.full((B,), seq_len, dtype=torch.int32, device=device)
page_table = _trivial_page_table(B, _max_blocks_for(L, 1), device)
_check(scores, seq_lens, page_table, page_size=1)
@pytest.mark.parametrize("seq_len", [
pytest.param(SMALL_2PASS, id="streaming_first"),
pytest.param(40000, id="streaming_40k"),
pytest.param(DEFAULT_CLUSTER_THRESHOLD, id="streaming_at_cluster_thresh"),
])
def test_streaming_path(seq_len):
"""Streaming strategy (medium N). With small batch and seq_len <=
auto-picked cluster_threshold (>= 32K when batch <= 30), the per-row
dispatch routes here."""
torch.manual_seed(seq_len)
device = torch.device("cuda")
B = 4
L = (seq_len + 3) & ~3
scores = torch.randn(B, L, dtype=torch.float32, device=device)
seq_lens = torch.full((B,), seq_len, dtype=torch.int32, device=device)
page_table = _trivial_page_table(B, _max_blocks_for(L, 1), device)
_check(scores, seq_lens, page_table, page_size=1)
@pytest.mark.parametrize("batch_size,seq_len", [
pytest.param(2, 65536, id="cluster_fused_64k"),
pytest.param(NUM_CLUSTERS, 131072, id="cluster_fused_max_batch"),
pytest.param(NUM_CLUSTERS + 1, 65536, id="cluster_two_stage_just_over"),
pytest.param(32, 96000, id="cluster_two_stage_32x96k"),
])
def test_cluster_path(batch_size, seq_len):
"""Large strategy (Hopper thread-block clusters). Force seq_len above
the auto threshold by passing static_cluster_threshold=SMALL_2PASS."""
torch.manual_seed(seq_len * batch_size)
device = torch.device("cuda")
L = (seq_len + 3) & ~3
scores = torch.randn(batch_size, L, dtype=torch.float32, device=device)
seq_lens = torch.full((batch_size,), seq_len, dtype=torch.int32,
device=device)
page_table = _trivial_page_table(batch_size, _max_blocks_for(L, 1), device)
metadata = plan_topk_v2(seq_lens, static_cluster_threshold=SMALL_2PASS)
indices = fast_topk_v2(scores, seq_lens, page_table, page_size=1,
metadata=metadata)
torch.cuda.synchronize()
expected = _reference_topk(scores, seq_lens, page_table, page_size=1)
for b in range(batch_size):
got = set(indices[b].tolist())
assert got == expected[b], (
f"row {b}: missing={len(expected[b] - got)} "
f"extra={len(got - expected[b])}")
@pytest.mark.parametrize("page_size", [1, 32, 64])
def test_page_table_fold_in(page_size):
"""page_to_indices: kernel-side fold of the page-table gather."""
torch.manual_seed(page_size)
device = torch.device("cuda")
B, seq_len = 4, 6000
L = (seq_len + 3) & ~3
max_blocks = (L + page_size - 1) // page_size
scores = torch.randn(B, L, dtype=torch.float32, device=device)
seq_lens = torch.full((B,), seq_len, dtype=torch.int32, device=device)
page_table = _shuffled_page_table(B, max_blocks, seed=page_size,
device=device)
_check(scores, seq_lens, page_table, page_size=page_size)
def test_mixed_lengths_route_per_row():
"""Per-row dispatch in topk_combine_transform: trivial / Register /
Streaming / Cluster all in one batch. Use static_cluster_threshold to
force a mix that includes the Large path."""
torch.manual_seed(7)
device = torch.device("cuda")
seq_lens = [
100, # trivial
SMALL_1PASS - 100, # 1-pass register
SMALL_2PASS - 100, # 2-pass register
50000, # streaming
40000, # streaming
80000, # cluster (above static_cluster_threshold)
]
B = len(seq_lens)
L = (max(seq_lens) + 3) & ~3
scores = torch.randn(B, L, dtype=torch.float32, device=device)
seq_lens_t = torch.tensor(seq_lens, dtype=torch.int32, device=device)
page_table = _trivial_page_table(B, _max_blocks_for(L, 1), device)
# Force seq_len > 49152 to take the Cluster path.
metadata = plan_topk_v2(seq_lens_t, static_cluster_threshold=49152)
indices = fast_topk_v2(scores, seq_lens_t, page_table, page_size=1,
metadata=metadata)
torch.cuda.synchronize()
expected = _reference_topk(scores, seq_lens_t, page_table, page_size=1)
for b, sl in enumerate(seq_lens):
valid = min(sl, TOPK)
row = indices[b].tolist()
if sl < TOPK:
assert all(v == -1 for v in row[sl:])
got = set(row[:valid])
assert got == expected[b], f"row {b} (sl={sl}) mismatched"
def test_metadata_can_be_reused_across_calls():
"""plan_topk_v2 is amortizable: same metadata reused across calls."""
torch.manual_seed(123)
device = torch.device("cuda")
B, seq_len = 8, 4096
L = (seq_len + 3) & ~3
seq_lens = torch.full((B,), seq_len, dtype=torch.int32, device=device)
page_table = _trivial_page_table(B, _max_blocks_for(L, 1), device)
metadata = plan_topk_v2(seq_lens)
# Two independent score buffers, same metadata.
scores_a = torch.randn(B, L, dtype=torch.float32, device=device)
scores_b = torch.randn(B, L, dtype=torch.float32, device=device)
out_a = fast_topk_v2(scores_a, seq_lens, page_table, page_size=1,
metadata=metadata)
out_b = fast_topk_v2(scores_b, seq_lens, page_table, page_size=1,
metadata=metadata)
torch.cuda.synchronize()
expected_a = _reference_topk(scores_a, seq_lens, page_table, page_size=1)
expected_b = _reference_topk(scores_b, seq_lens, page_table, page_size=1)
for b in range(B):
assert set(out_a[b].tolist()) == expected_a[b]
assert set(out_b[b].tolist()) == expected_b[b]
def test_workspace_can_be_preallocated():
"""Workspace passed in by the caller (cudagraph-friendly path)."""
torch.manual_seed(0)
device = torch.device("cuda")
B, seq_len = 16, 70000
L = (seq_len + 3) & ~3
seq_lens = torch.full((B,), seq_len, dtype=torch.int32, device=device)
page_table = _trivial_page_table(B, _max_blocks_for(L, 1), device)
scores = torch.randn(B, L, dtype=torch.float32, device=device)
metadata = plan_topk_v2(seq_lens, static_cluster_threshold=SMALL_2PASS)
workspace = scores.new_empty((B, workspace_ints_per_batch()),
dtype=torch.int32)
page_indices = scores.new_empty((B, TOPK), dtype=torch.int32)
out = fast_topk_v2(scores, seq_lens, page_table, page_size=1,
metadata=metadata, workspace=workspace,
page_indices=page_indices)
torch.cuda.synchronize()
assert out.data_ptr() == page_indices.data_ptr(), (
"kernel must write into the caller-supplied page_indices tensor")
expected = _reference_topk(scores, seq_lens, page_table, page_size=1)
for b in range(B):
assert set(out[b].tolist()) == expected[b]
@@ -7,6 +7,7 @@ from .cache_utils import (
dequantize_and_gather_k_cache,
quantize_and_insert_k_cache,
)
from .fast_topk import fast_topk_v2, plan_topk_v2, workspace_ints_per_batch
from .fused_indexer_q import MXFP4_BLOCK_SIZE, fused_indexer_q_rope_quant
from .fused_inv_rope_fp8_quant import fused_inv_rope_fp8_quant
from .fused_qk_rmsnorm import fused_q_kv_rmsnorm
@@ -16,8 +17,11 @@ __all__ = [
"combine_topk_swa_indices",
"compute_global_topk_indices_and_lens",
"dequantize_and_gather_k_cache",
"fast_topk_v2",
"fused_indexer_q_rope_quant",
"fused_inv_rope_fp8_quant",
"fused_q_kv_rmsnorm",
"plan_topk_v2",
"quantize_and_insert_k_cache",
"workspace_ints_per_batch",
]
@@ -0,0 +1,137 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""DeepSeek V4 indexer top-k (k = 512), ported from sglang's topk_v2 family.
The kernel is registered as ``torch.ops._C.fast_topk_v2`` (selection +
fused page-table gather) and ``torch.ops._C.fast_topk_v2_plan`` (per-batch
threshold and metadata). It is built for Hopper (sm_90a) and Blackwell
datacenter (sm_100/sm_103); sm_120 (consumer Blackwell) is not supported
because it lacks thread-block clusters.
Two-step usage::
metadata = plan_topk_v2(seq_lens)
page_indices = fast_topk_v2(scores, seq_lens, page_table, page_size,
metadata=metadata)
The plan can be amortized across same-shape forward calls (e.g. across all
indexer layers within one cudagraph capture), since it depends only on
``seq_lens``.
"""
from typing import Optional
import torch
# The metadata layout (rows of int32x4) is fixed by the kernel; the planner
# writes one GlobalMetadata row + one row per batch entry.
_PLAN_COLS = 4
# Output top-k size. Hardcoded in the kernel.
_TOPK = 512
def workspace_ints_per_batch() -> int:
"""Number of int32s the kernel needs in `(B, _)` workspace per row."""
return int(torch.ops._C.fast_topk_v2_workspace_ints())
def plan_topk_v2(
seq_lens: torch.Tensor,
static_cluster_threshold: int = 0,
metadata: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""Pick a per-batch ``cluster_threshold`` and build the per-row metadata.
Args:
seq_lens: int32 tensor of shape ``(B,)``. CUDA, contiguous. Each entry
is the number of valid scored tokens for that row (i.e., the
indexer's compressed seq_len).
static_cluster_threshold: when nonzero, override the auto-tuned
threshold and route any row with ``seq_len > threshold`` through
the cluster path. 0 means auto.
metadata: optional preallocated int32 tensor of shape ``(B + 1, 4)``.
When provided, the planner writes into it (useful for cudagraph
capture). When omitted, a fresh tensor is allocated.
Returns:
The metadata tensor.
"""
assert seq_lens.dim() == 1
assert seq_lens.dtype == torch.int32
assert seq_lens.is_cuda and seq_lens.is_contiguous()
batch_size = seq_lens.size(0)
if metadata is None:
metadata = torch.empty(
(batch_size + 1, _PLAN_COLS),
dtype=torch.int32,
device=seq_lens.device,
)
else:
assert metadata.shape == (batch_size + 1, _PLAN_COLS)
assert metadata.dtype == torch.int32
assert metadata.is_cuda and metadata.is_contiguous()
torch.ops._C.fast_topk_v2_plan(
seq_lens, metadata, int(static_cluster_threshold)
)
return metadata
def fast_topk_v2(
scores: torch.Tensor,
seq_lens: torch.Tensor,
page_table: torch.Tensor,
page_size: int,
*,
metadata: Optional[torch.Tensor] = None,
workspace: Optional[torch.Tensor] = None,
page_indices: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""Select top-512 indexer scores per row and fold the page-table gather.
Args:
scores: float32 logits of shape ``(B, L)``. ``stride(-1) == 1`` and
``stride(0) % 4 == 0`` (TMA 16-byte alignment).
seq_lens: int32 ``(B,)``; only the first ``seq_lens[b]`` columns are
considered for row b.
page_table: int32 ``(B, max_blocks)`` with ``stride(-1) == 1``.
page_size: power-of-2 page size used by the indexer KV cache.
metadata: optional plan tensor from :func:`plan_topk_v2`. If omitted,
it is built on the fly.
workspace: optional preallocated workspace ``(B, workspace_ints_per_batch())``
int32, ``stride(-1) == 1``. Used for inter-cluster tie staging in
the large-N path. Allocated on demand if absent.
page_indices: optional preallocated output ``(B, 512)`` int32
contiguous. Allocated on demand if absent.
Returns:
``(B, 512)`` int32 tensor of page-table-resolved indices.
"""
assert scores.dim() == 2
assert scores.dtype == torch.float32
assert scores.is_cuda
batch_size = scores.size(0)
if page_indices is None:
page_indices = scores.new_empty(
(batch_size, _TOPK), dtype=torch.int32
)
if workspace is None:
workspace = scores.new_empty(
(batch_size, workspace_ints_per_batch()), dtype=torch.int32
)
if metadata is None:
metadata = plan_topk_v2(seq_lens)
torch.ops._C.fast_topk_v2(
scores,
seq_lens,
page_table,
page_indices,
int(page_size),
workspace,
metadata,
)
return page_indices