Prhub

#37591 [ROCm] Make DSA indexer top-k exact with cooperative selection

原始 PR 作者 jiejingzhangamd 合并时间 2026-09-06 14:55 文件变更 5 提交数 3 评论 5 代码增减 +1672 / -2

执行摘要

为 ROCm 平台实现精确的 DSA 索引器 top-k 内核,修复召回率问题并显著提升性能。

动机源于一个严重的精度问题:先前的 ROCm 路径使用通过 hipify topk.cu 生成的 topk.hip,其 fp16 粗键在处理来自 GLM-5.2 解码的高集中度日志时,会将大量有效元素折叠到同一个桶中,导致候选缓冲区溢出并静默丢弃有效的获胜者。在实测的 GLM-5.2 日志上,选择值的召回率(相对于 torch.topk)低至 0.53,这意味着近一半的 KV 被错误地丢弃。为了解决这个影响模型正确性的关键问题,需要为 ROCm 实现一个原生、精确的 top-k 内核。

这个 PR 值得精读,尤其对于关注 ROCm 支持、DSA 模型性能或 GPU 内核开发的工程师。其价值不仅在于修复了一个关键 bug,更在于其严谨的方法论:清晰的动机陈述(附带量化问题)、详细的解决方案设计(包含算法权衡)、全面的精确性测试(针对不同分布)、详尽的性能对比(对比新旧方案及 topk_v2),以及对构建集成的细致处理。关于 topk_v2 默认启用导致的路径选择问题,是未来整合时需要关注的重要设计点。

讨论亮点

核心讨论围绕新内核的使用路径展开。评论者 1am9trash 指出,PR #36851 已经在 ROCm 上为 GLM 模型默认启用了 topk_v2(通过环境变量 SGLANG_OPT_USE_TOPK_V2=1),这意味着当使用 topk_v2 时,本次 PR 实现的这个原生 top-k 内核实际上不会被调用。这引发了一个关键的设计权衡问题:PR 中提供了详尽的性能对比(新内核 vs. topk_v2 vs. 旧 topk.hip),并证明在集群分布(clustered)下 topk_v2 是不精确的(召回率 0.022),而新内核保持精确(召回率 1.0)。因此,问题变成:为了正确性,是否应该将 topk_v2 在某些场景下默认关闭,或者让调用路径更明确?目前此问题在评论中被提出,但未形成明确的决策结论。

实现拆解

  1. 核心算法实现:在 python/sglang/kernels/aot/include/hip/dsa_topk_coop.cuh 中实现了新的协作式选择内核。算法流程为:一次粗直方图 pass(使用 fp32 键的高 16 位作为键)、找到阈值桶、scatter pass(高于阈值的桶直接作为赢家,阈值桶的成员作为 ties)、以及对 ties 使用完整 fp32 键进行基数排序来精确解决。关键创新在于使用 fp32 粗键而非 fp16 以保留更多区分度,并在候选缓冲区溢出时重新扫描整行而非信任截断的缓冲区,从而保证精确性。实现了两种路径:单块(one-block)内核和行分割(row-split)多块内核,后者将长行分割以提升 GPU 利用率。
  2. 包装器与 API 暴露:在 python/sglang/kernels/aot/csrc/elementwise/topk.hip 中提供了与原 topk.cu 相同的三个 API 入口点(fast_topk_v2, fast_topk_transform_fused, fast_topk_transform_ragged_fused)的包装器。它负责设备检查、根据行长度和批处理大小决定是否启用行分割(通过 row_split 函数),并调用相应的 CUDA 内核启动逻辑。常量 kTopK=2048, kHistBits=12, kTieCap=4096 固化了算法参数。
  3. 全面测试覆盖:在 python/sglang/kernels/aot/tests/test_topk.py 中新增了严格的精确性测试。包括:test_topk_is_exact_for_indexer_distributions 使用模拟高集中度分布(diffuse, banded, narrow, subnormal)验证精确性;test_topk_variants_are_exact_on_variable_windows 测试所有三个 API 在可变窗口、行起始偏移和变换输出下的精确性;test_topk_uses_score_device_and_rejects_mixed_devices 测试多设备场景和错误处理。测试通过精确值多集比较(assert_exact, assert_exact_rows)而非仅索引比较,确保发现静默精度损失。
  4. 构建系统集成:在 python/sglang/kernels/aot/setup_rocm.py 和 3rdparty/amd/wheel/sgl-kernel/rocm_hipify.py 中,将编译源从 topk.cu 更改为 topk.hip,确保 ROCm 构建使用新的原生 HIP 实现。后者明确从 hipify 源列表中移除了 topk.cu,以防止在 wheel 构建过程中新内核被意外覆盖。
文件 模块 状态 重要度
python/sglang/kernels/aot/include/hip/dsa_topk_coop.cuh 内核 added 6.79
python/sglang/kernels/aot/csrc/elementwise/topk.hip 内核 added 6.6
python/sglang/kernels/aot/tests/test_topk.py 测试 modified 7.52
python/sglang/kernels/aot/setup_rocm.py 构建配置 modified 4.82
3rdparty/amd/wheel/sgl-kernel/rocm_hipify.py AMD 构建脚本 modified 4.52

关键符号

coop_topk_kernel coop_mb_hist0 coop_mb_hist1 coop_mb_scatter coop_mb_refine row_split launch fast_topk_v2 fast_topk_transform_fused fast_topk_transform_ragged_fused _make_scores assert_exact assert_exact_rows

关键源码片段

python/sglang/kernels/aot/include/hip/dsa_topk_coop.cuh core-logic

核心协作式 top-k 内核的完整 HIP 实现,包含精确算法(fp32 粗直方图、tie 细化、溢出重扫描)和两种执行路径(单块、行分割)。是理解本 PR 技术方案的关键。

// 摘自 dsa_topk_coop.cuh,展示核心算法思想与关键实现单元。
​
// 有序 32 位键:翻转符号位使无符号比较等价于浮点比较。
__device__ __forceinline__ uint32_t order_key32(float x) {
  uint32_t bits = __float_as_uint(x);
  return (bits & 0x80000000u) ? ~bits : (bits | 0x80000000u);
}
​
// 粗直方图键:取有序键的高 16 位(符号位 +8 位指数 +7 位尾数)。
// 截断单调键仍保持单调性,这是分区正确的基础。
__device__ __forceinline__ uint32_t order_key_hi16(float x) {
  return order_key32(x) >> 16;
}
​
// 单块内核的启动参数结构
struct CoopParams {
  const float* input; // [B, L] 分数矩阵
  int32_t* out_idx; // [B, k] 输出索引
  const int32_t* row_starts; // [B] 行起始偏移(用于 ragged API)
  const int32_t* lengths; // [B] 每行的有效长度
  OutMap map; // 输出映射(raw/paged/ragged)
  int64_t stride; // 行步长
};
​
// 单块内核入口点。遍历整个行,执行两轮粗直方图(round0: 12 位 , round1: 4 位)
// 和一次 scatter/refine。
template <uint32_t kTopK, uint32_t kHistBits, uint32_t kTieCap, uint32_t BlockSize>
__global__ void coop_topk_kernel(CoopParams p) {
  // ... 内核实现细节 ...
  // 核心逻辑:
  // 1. 线程协作构建全局直方图。
  // 2. 二分搜索找到阈值桶。
  // 3. Scatter pass:将高于阈值的桶直接输出,阈值桶的成员存入 tie 缓冲区。
  // 4. 如果 tie 缓冲区溢出(kTieCap 不够),则调用 fallback 重扫描整行。
  // 5. 否则,对 tie 缓冲区使用完整 fp32 键进行基数排序以精确解决 ties。
  // 这保证了即使在最坏分布下(如 88% 的元素落入一个桶),结果也是精确的。
}
python/sglang/kernels/aot/csrc/elementwise/topk.hip core-logic

新 top-k 内核的包装器,暴露与原有 `topk.cu` 相同的 API,并实现了关键的行分割决策逻辑和设备检查。是理解内核如何集成到系统中的桥梁。

// 摘自 topk.hip,展示行分割决策和启动逻辑。
​
// 行分割策略:决定是否将一个行分割给多个线程块处理。
// 基于批处理大小和行长度(使用行步长作为提示)的启发式。
// 分割可以提升 GPU 利用率,但会引入额外的全局同步开销。
int row_split(int batch, int64_t row_len_hint) {
  // 允许通过环境变量强制禁用分割(值设为 0)以便基准测试。
  if (const char* e = std::getenv("SGL_DSA_TOPK_ROW_SPLIT"); e != nullptr && *e != '\0') {
    const int v = std::atoi(e);
    if (v >= 0) { return v; }
  }
  // 查询并缓存当前设备的计算单元数量。
  // ... 省略设备属性查询 ...
  constexpr int64_t kMinRowLen = 65536; // 低于此长度不分割,因为收益小且开销相对大。
  constexpr int kMinSplit = 4; // 最少分割块数,低于此则不分割。
  constexpr int kMaxSplit = 32; // 最大分割块数,受缓存的计算单元数限制。
  if (row_len_hint < kMinRowLen) { return 0; }
  const int g = std::min(cached_cu / std::max(batch, 1), kMaxSplit);
  return g < kMinSplit ? 0 : g;
}
​
// 启动函数:根据 row_split 结果选择单块或行分割多块内核。
void launch(...) {
  const int g = row_split(batch, stride);
  if (g == 0) {
    // 单块路径:启动 coop_topk_kernel,每个行块一个线程块。
    coop_topk_kernel<<<batch, kBlock, 0, stream>>>(...);
  } else {
    // 行分割路径:分配工作区,启动多轮内核(hist0, hist1, scatter, refine)。
    // 使用 PyTorch 分配器分配工作区,避免 HIP 图捕获时的地址固化问题。
    at::Tensor ws = at::empty(...);
    coop_mb_hist0<<<grid, kBlock>>>(p, g);
    coop_mb_hist1<<<grid, kBlock>>>(p, g);
    coop_mb_scatter<<<grid, kBlock>>>(p, g);
    coop_mb_refine<<<batch, kBlock>>>(p); // refine 是行级的,不分割。
  }
}

评论区精华

新内核的实际使用路径与默认配置 设计

评论者 `1am9trash` 指出,PR #36851 已在 ROCm 上为 GLM 模型默认启用了 `topk_v2`(通过 `SGLANG_OPT_USE_TOPK_V2=1`)。这意味着当使用默认配置时,本次 PR 实现的原生 top-k 内核可能不会被调用,除非显式禁用 `topk_v2`。

结论:未解决。此评论指出了一个重要的设计权衡:在默认配置下,为了向后兼容或性能,可能仍在使用一个已知在某些分布下不精确的内核(topk_v2),而新实现的精确内核处于备用状态。这需要产品层面决定是否调整默认值或提供更清晰的路由机制。 · 待处理

风险与影响

  1. 正确性风险:新内核的行分割决策是基于启发式(row_split 函数,使用 SGL_DSA_TOPK_ROW_SPLIT 环境变量控制)。虽然测试覆盖了两种路径,但行分割的阈值(kMinRowLen=65536, kMinSplit=4)是基于特定硬件(MI355X)测量得出的。在不同硬件(如 gfx942)或未来数据分布变化时,这个启发式可能不是最优的,但不会导致不正确结果。
  2. 性能与资源风险:协作式内核的 LDS 内存消耗是固定的(12 位直方图 + 4096 ties ≈ 48KB),在 gfx942 的 64KB 静态限制内。行分割路径使用来自 PyTorch 分配器的工作区内存(而非固定全局缓冲区),避免了 HIP 图捕获时的地址固化问题,这是一个谨慎的设计。
  3. 兼容性风险:变更仅影响 ROCm 构建路径。CUDA 构建不受影响(继续使用 topk.cu)。主要风险在于 topk_v2 与本内核的并存。如讨论所述,当 SGLANG_OPT_USE_TOPK_V2=1 时,本内核不会被使用。这可能导致用户困惑或错误地认为性能问题已解决,而实际上仍在使用可能不精确的 topk_v2。需要明确的文档或未来整合来解决此路径选择问题。
  1. 直接影响:对所有使用 ROCm 平台并运行 DSA 模型(DeepSeek-V3.2, GLM-5.2 等,DeepSeek-V4 除外)的用户产生直接影响。修复了关键的 top-k 精度问题,避免了在推理过程中因无效 top-k 选择导致的质量下降。性能提升(1.5-3x)直接转化为更高的吞吐量或更低的延迟。
  2. 系统影响:为 ROCm 平台贡献了一个高质量的原生 HIP 内核实现,展示了如何为特定硬件特征(如 CDNA 的 64-lane warp)进行优化,并解决了 CUDA 到 HIP 移植中的精度陷阱。增加了测试基础设施中对精确性验证的重视。
  3. 团队影响:为 AMD 团队维护 ROCm 支持提供了新的、经过测试的基线代码。PR 中详细的性能对比和正确性分析(包含与 topk_v2 的对比)为未来内核选择和优化提供了宝贵的数据。
    * 不确定性:如讨论中所述,该内核在默认配置下(当 SGLANG_OPT_USE_TOPK_V2=1)可能未被积极使用。其影响程度取决于未来是否调整默认配置或为不同模型提供显式选择。
核心路径变更 正确性 / 性能权衡 平台特定代码 默认配置不明确 启发式决策

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论