Prhub

#56346 [Perf][Kernel] Add sampled filtering for persistent top-k

原始 PR 作者 mgoin 合并时间 2026-09-16 05:16 文件变更 5 提交数 2 评论 4 代码增减 +556 / -35

执行摘要

长行 top-k 采样过滤提速,B300 长上下文 decode 延迟近半减

PR body 明确指出目标是 "Speed up long FP32 sparse-indexer decode top-k with sampling inspired by DeepSelect"。长上下文下稀疏 MLA 索引器的 decode top-k 需要对每行数万至数十万 FP32 score 做精确选择,批量 + 超长 ragged 行时成为延迟热点;作者通过对 B300 做 batch、长度、seed、cache mode 全参数扫描得到 crossover envelope,并指出 k=512 在 80K~86K 长度区间存在减速,因此启用阈值取保守值(k=512 为 98304,其余为 65536),既保证收益又避开退化区间。作者还声明短行回退最多增加 3.32% 延迟(绝对 1.215 µs),并确认与现有 #44606(调度调优)、#55872(FlashInfer 后端)、#55314(stash overflow 修复)不重复。

值得精读的 kernel 性能优化 PR。三个设计决策值得学习:1)用受控采样把长行 top-k 从“全行扫描 + 精确选择”降为“采样估计 cutoff + 幸存者压缩 + 精确收尾”,正确性由候选数 min/max 双回退兜底,且 stash 溢出从“截断子集”改为“全行精确重试”,这是对 #55314 老问题的根治;2)启用阈值由 B300 crossover envelope 数据驱动(k=512 刻意避开 80K~86K 减速区),而非拍脑袋;3)CUDA graph 场景下用运行时 length 与捕获时 length 解耦,短行在相同 launch 内复用既有精确路径。阅读时重点关注 threshold() 的逐字节细化与 emit() 的 warp 级压缩原语,以及 persistent_topk.cuh 的 CheckOverflow 重构如何保持向后兼容。

讨论亮点

本 PR 来自 fork 分支,claude[bot] 自动评审被禁用(评论原文:"This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review."),合入前没有任何人工 review 线程,review_comments_count 为 0。评审价值主要来自作者在 PR body 中预埋的自证证据:B300 全参数 crossover 扫描与 envelope 图、6265 例 forced-backend + 541 例 final-dispatch 零选择错误、memcheck/synccheck 零错误、DeepSeek-V4-Flash-0731 TP2 128 请求端到端 FP32 精确性审计(128/128 算术正确、GSM8K 123/128)。作者也主动披露局限:short-row fallback 最多 +3.32%(1.215 µs)、k=512 在 80K~86K 有减速区间、token 等价性未建立(75/128 长生成匹配 EOS)。AI 辅助(OpenAI Codex 联合作者)参与实现与测试,但缺少独立评审视角的挑战。

实现拆解

  1. 新增采样过滤内核 csrc/libtorch_stable/sampled_topk.cuh(新增 253 行):在 vllm::sampled_topk 命名空间实现 sampled_topk_kernel<K>。Storage 用动态共享内存承载 8192 个 uint2 幸存者候选、2048 桶 coarse 直方图与 cub::BlockScan 临时存储;ordered() 把 FP32 位模式重排为 uint32 全序 key,select_bin() 用 InclusiveScan 定位目标分箱,threshold() 先 coarse 分桶再逐字节细化出精确 cutoff。压缩阶段按 warp 做 __ballot_sync 命中掩码、warp 内前缀和、原子预留槽位;幸存者数超过 kCapacity 时继续计数而不截断,从而触发溢出回退。行长低于 kMinSampledLength<K> 的行复用原有 filtered_topk_row 精确路径,单一 launch 内长短行共存。

  2. 重构既有筛选内核 csrc/libtorch_stable/persistent_topk.cuh(+52/-34):把 FilteredTopKUnifiedKernel 的静态共享内存变量(直方图、计数器、阈值分箱、索引 stash)提取为 FilteredTopKStorage<MAX_K> 结构体,内核主体抽成 __device__ filtered_topk_row<DType, IdType, VEC_SIZE, MAX_K, UsePredicatedShortLoads, CheckOverflow> 设备函数。新增 CheckOverflow 模板参数:细化阶段若候选数超过 SMEM_INPUT_SIZE 则返回 false,让调用方改用全行精确选择,避免旧实现“用截断 stash 子集”造成的精度偏差。

  3. 调度接线 csrc/libtorch_stable/topk.cu(+17/-1):launch_persistent_topk 在原有 num_rows > 32 && max_smem_per_block >= 128 KiB 分支前插入新分支:num_rows > 64 && max_seq_len >= kMinSampledLength<TopK> && max_smem_per_block >= 144 KiB 时走 sampled_topk_kernel,并调用 cudaFuncSetAttribute 设置 128 KiB 动态共享内存上限;条件不满足完全退回原路径。#include "sampled_topk.cuh" 位于 #ifndef USE_ROCM 内,ROCm 构建不受影响。

  4. 图回放正确性测试 tests/kernels/test_top_k_per_row.py(+100):新增 test_persistent_topk_sampled_graph,参数化 rows=[65, 128] × top_k=[512, 1024, 2048] × distribution=[random, 10LSBits, ascending, constant, sampled_peaks]。行宽取 kMinSampledLength + 3 贴合启用边界;sampled_peaks 分布刻意让采样落空(幸存者 < k)以覆盖 underflow 回退;CUDA graph replay 3 轮中把 lengths 逐个替换为 [-1, 0, 1, k-1, k, 32769, 门限±1, 行宽, 行宽+17] 等边界值并翻转一次 logits,与精确 topk 做 atol=0, rtol=0 严格对照。共享内存不足 144 KiB 的卡直接 skip。

  5. 基准脚本 benchmarks/kernels/benchmark_persistent_topk.py(新增 134 行):独立 CUPTI 基准,支持 rows × widths × top_k 全组合、--capacity 模拟更大 graph 捕获上界、--full-length 与 80%~100% ragged 长度、warm/cold L2 状态,输出 JSON 便于 base/candidate 两侧对比;内部自带完整正确性断言(无效位 -1、无重复索引、与参考 topk 严格相等)。

文件 模块 状态 重要度
csrc/libtorch_stable/sampled_topk.cuh 采样内核 added 6.79
csrc/libtorch_stable/persistent_topk.cuh 筛选内核 modified 5.08
csrc/libtorch_stable/topk.cu 调度入口 modified 4.49
tests/kernels/test_top_k_per_row.py 内核测试 modified 6.47
benchmarks/kernels/benchmark_persistent_topk.py 基准脚本 added 7.41

关键符号

sampled_topk_kernel threshold select_bin emit reserve ordered coarse filtered_topk_row FilteredTopKUnifiedKernel launch_persistent_topk test_persistent_topk_sampled_graph

关键源码片段

csrc/libtorch_stable/topk.cu core-logic

launch_persistent_topk 调度入口新增采样分支,串起启用门限(行数、行长、共享内存)与内核选择,决定新旧路径边界

// topk.cu:launch_persistent_topk 中新增的采样分支。
// 只有 batch > 64、最大行长达标、且可申请 144 KiB opt-in 共享内存时才启用,
// 其余情况完全沿用原有 cooperative / persistent 路径,保证短行与旧卡无回归。
if (num_rows > 64 &&
    max_seq_len >= vllm::sampled_topk::kMinSampledLength<TopK> &&
    max_smem_per_block >= 144 * 1024) {
    auto kernel = vllm::sampled_topk::sampled_topk_kernel<TopK>;
    // 128 KiB 动态共享内存槽位,采样内核复用为 Storage 幸存者缓冲区
    constexpr size_t smem_size = vllm::filtered_topk::FILTERED_TOPK_SMEM_DYNAMIC;
    cudaError_t status = cudaFuncSetAttribute(
        kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_size);
    STD_TORCH_CHECK(status == cudaSuccess,
                    "sampled_topk smem failed: ", cudaGetErrorString(status));
    kernel<<<num_rows, vllm::sampled_topk::kThreads, smem_size, stream>>>(
        logits.const_data_ptr<float>(), lengths.const_data_ptr<int32_t>(),
        output.mutable_data_ptr<int32_t>(), stride,
        static_cast<int>(std::min(max_seq_len, logits.size(1))));
} else if (num_rows > 32 && max_smem_per_block >= 128 * 1024) {
    // 原有 FilteredTopKRaggedTransform 分支保持不变
}

评论区精华

fork PR 自动 review 状态 other

claude[bot] 评论:"This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review." 后续维护者未触发一次性 review,因此没有任何人工评审线程。

结论:未触发人工 / 自动代码评审;合入决策依赖作者自述的测试与基准数据以及 CI 结果 · closed

风险与影响

  1. 验证面窄:性能结论仅基于 B300(SM103a/SM80 只编译通过、无运行验证);其他 SM 90 系卡即使满足启用条件也会走采样路径,收益/回归未被实测。
  2. 启发式正确性依赖回退兜底:采样 cutoff 是统计估计,测试构造了 5 种分布(含故意漏采样的 sampled_peaks)并以 atol=0 严格对照,但真实 logits 的极端长尾分布仍可能暴露未覆盖边界。
  3. 性能盲区:k=512 在 80K~86K 区间存在减速,cutoff 取 98304 只是避开而非消除;短行回退最多 +3.32%。
  4. 运维健壮性:topk.cu 中 cudaFuncSetAttribute 失败会直接 STD_TORCH_CHECK 崩溃,是新增的 panic 点;144 KiB opt-in 共享内存门槛使旧硬件静默走旧路径,但也意味着新路径的硬件覆盖靠运行时属性保证。
  5. 测试中 rows=128 时 lengths 用 view(-1, 4) 暗示 TMA 对齐约束,若未来接入其他调用方需注意 shape 契约。

用户侧:DeepSeek V4 长上下文稀疏索引 decode(batch ≥64、行长 ≥98K 或 ≥65K token)延迟显著下降约 35%~48%(128K/k=2048:316.58→207.06 µs;256K/k=512:584.45→301.72 µs);短行与旧硬件完全无感知,cooperative prefill 路径不变。系统侧:persistent top-k 调度从 2 分支变为 3 分支,共享内存峰值需求从 128 KiB 升到 144 KiB,且采样内核在单 launch 内同时承担采样与精确回退两条子路径,复杂度上升。团队侧:新增 253 行核心 CUDA + 100 行测试 + 134 行基准,且为 AI 辅助实现,后续需要多架构(尤其非 B300 SM90)回归验证;采样算法与回退契约成为新的维护面。

性能验证仅限 B300 采样启发式依赖回退兜底 144 KiB 共享内存门槛 k=512 存在 80K-86K 减速区间 fork 提交无人工 review

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论