Prhub

#41189 [Bugfix] Fix persistent_topk cooperative deadlock at TopK=1024

原始 PR 作者 zyongye 合并时间 2026-04-30 12:03 文件变更 1 提交数 3 评论 3 代码增减 +59 / -4

执行摘要

修复 TopK=1024 时 persistent_topk 协作死锁

TopK=1024, MTP=1 场景下 GPU SM 数不足以同时调度所有 persistent CTA,导致协作栅栏死锁。PR body 明确说明:"for topk=1024 and MTP=1, this kernel will deadlock since there's no enough SMs to launch the grid and since each kernel is persistent, no kernel will finish."

值得精读,尤其是 CUDA 协作栅栏死锁的调试思路(occupancy 查询、headroom、fallback)。代码注释清晰,适合作为 CUDA kernel 并发安全设计的参考。

讨论亮点

Reviewer LopezCastroRoberto 提出两点:

  1. Headroom 逻辑应仅在 max_seq_len > RADIX_THRESHOLD 时生效,因为低于阈值时非 CTA-0 线程会提前退出,不会触发协作栅栏,无需预留 headroom。
  2. 超限时应回退到 FilteredTopKRaggedTransform 而非 TORCH_CHECK 失败,已在 B300 上 benchmark 确认该 fallback 性能最佳。
    第一点被采纳并体现在最终代码中,第二点作为进一步优化建议。gemini-code-assist[bot] 也指出 occupancy==1 时 headroom 逻辑应至少预留 1 个 CTA,该建议已反映在代码中。

实现拆解

  1. 按实际 vec_size 查询 occupancy:原有代码统一使用 vec_size=4 的模板实例查询 occupancy,可能导致 occupancy 高估(实际可能使用 vec_size=2 或 1)。修改为根据 vec_size 分别调用 cudaOccupancyMaxActiveBlocksPerMultiprocessor 的对应模板实例(persistent_topk_kernel<TopK, 4/2/1>),并加入 TORCH_CHECK 确保查询成功。

  2. 引入 headroom 机制:仅在 max_seq_len > RADIX_THRESHOLD 即需要协作栅栏时预留 headroom。当 occupancy > 1 时,预留一个 CTA/SM(共 num_sms 个);当 occupancy == 1 时,预留至少 1 个 CTA,避免最易死锁场景。max_resident_ctas 减去 headroom 后用于计算 group 数量。

  3. 超限回退到 FilteredTopK:若计算出的 total_ctas 超过硬件 resident 上限(num_sms * occupancy),则检查 max_smem_per_block >= 128KB,若满足则调用 FilteredTopKRaggedTransform 作为 fallback;否则报错退出。

  4. 代码优化:根据 reviewer 建议,将重复计算合并为 hw_resident_cap 变量。

文件 模块 状态 重要度
csrc/topk.cu 内核 modified 5.43

关键符号

launch_persistent_topk

关键源码片段

csrc/topk.cu core-logic

核心修复文件,修改了 persistent_topk launch 逻辑,包括 occupancy 查询、headroom 预留和 fallback 路径。

// csrc/topk.cu 中的 launch_persistent_topk 函数(关键片段)
// 修复点:按实际 vec_size 查询 occupancy
int occupancy = 1;
cudaError_t occ_err = cudaSuccess;
if (vec_size == 4) {
  occ_err = cudaOccupancyMaxActiveBlocksPerMultiprocessor(
      &occupancy, P::persistent_topk_kernel<TopK, 4>, P::kThreadsPerBlock,
      smem_size);
} else if (vec_size == 2) {
  occ_err = cudaOccupancyMaxActiveBlocksPerMultiprocessor(
      &occupancy, P::persistent_topk_kernel<TopK, 2>, P::kThreadsPerBlock,
      smem_size);
} else {
  occ_err = cudaOccupancyMaxActiveBlocksPerMultiprocessor(
      &occupancy, P::persistent_topk_kernel<TopK, 1>, P::kThreadsPerBlock,
      smem_size);
}
TORCH_CHECK(occ_err == cudaSuccess,
    "persistent_topk occupancy query failed: ",
    cudaGetErrorString(occ_err));// 仅在需要协作时才启用 headroom 逻辑
const bool needs_cooperative =
    static_cast<uint32_t>(max_seq_len) > P::RADIX_THRESHOLD;const uint32_t hw_resident_cap =
    static_cast<uint32_t>(num_sms) * static_cast<uint32_t>(occupancy);
uint32_t max_resident_ctas = hw_resident_cap;
if (needs_cooperative) {
  // 预留 headroom:occupancy>1 时每 SM 留一个 CTA,否则至少留 1 个
  uint32_t headroom = (occupancy > 1) ? static_cast<uint32_t>(num_sms) : 1u;
  if (max_resident_ctas >= headroom + ctas_per_group) {
    max_resident_ctas -= headroom;
  }
}uint32_t num_groups = std::min(max_resident_ctas / ctas_per_group,
    static_cast<uint32_t>(num_rows));
if (num_groups == 0) num_groups = 1;
uint32_t total_ctas = num_groups * ctas_per_group;// 若超限则回退到 FilteredTopK
if (needs_cooperative && total_ctas > hw_resident_cap) {
  TORCH_CHECK(max_smem_per_block >= 128 * 1024,
      "persistent_topk would oversubscribe and the FilteredTopK "
      "fallback requires >=128KB smem per block (have ",
      max_smem_per_block, "). total_ctas=", total_ctas,
      " > num_sms*occupancy=", hw_resident_cap, " (TopK=", TopK,
      ", vec_size=", vec_size, ", ctas_per_group=", ctas_per_group,
      ", smem=", smem_size, ").");
  // 调用 FilteredTopKRaggedTransform 作为 fallback
  cudaError_t status =
      vllm::FilteredTopKRaggedTransform<TopK>(...);
  // ...
}

评论区精华

headroom 逻辑应只在需要协作时启用 正确性

LopezCastroRoberto 指出 headroom/oversubscription 逻辑应仅当 params.max_seq_len > RADIX_THRESHOLD(32K)时运行,因为低于此阈值时非 CTA-0 线程会提前退出,不会触发协作栅栏。

结论:已采纳,代码中增加了 needs_cooperative 判断。 · 已解决

超限时应回退而非 TORCH_CHECK 正确性

LopezCastroRoberto 建议当无法安全启动时回退到 FilteredTopKRaggedTransform 或 top_k_per_row_decode,而不是 TORCH_CHECK 失败。他在 B300 上 benchmark 确认 FilteredTopKRaggedTransform 是最佳回退选项。

结论:已采纳,代码中增加了 fallback 路径。 · 已解决

occupancy==1 时 headroom 不足 设计

gemini-code-assist[bot] 指出 occupancy==1 时当前 headroom 逻辑可能不预留任何 CTA,这是最易死锁的情况,应至少保留一个 CTA slot。

结论:已修复,occupancy==1 时 headroom 设为 1。 · 已解决

hw_resident_cap 变量可复用 style

LopezCastroRoberto 指出 max_resident_ctas 的初始值等于 hw_resident_cap,建议直接复用变量避免冗余计算。

结论:已采纳,代码合并为 uint32_t max_resident_ctas = hw_resident_cap;。 · 已解决

风险与影响

  1. 回退内核性能风险:当超限回退到 FilteredTopKRaggedTransform 时,其性能可能低于 persistent_topk,但总比死锁好。Reviewer 已在 B300 上 benchmark 确认该回退方案可接受。
  2. 修改范围集中:仅修改 csrc/topk.cu 一个文件,风险可控。
  3. 缺少测试覆盖:PR 未包含测试用例,无法验证死锁修复和回退路径的正确性。

直接修复了 TopK=1024 且 MTP=1 时的死锁问题,影响使用该配置的模型(如 DeepSeek 等)。回退机制确保极端情况下系统不会挂起。无 API 或性能回归预期。

核心路径变更 缺少测试覆盖 回退路径性能未知

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论