Prhub

#33484 perf(hisparse): fuse the DSv4 value and scale swap-in copy on ROCm

原始 PR 作者 AMD-yanfeiwang 合并时间 2026-08-11 05:30 文件变更 2 提交数 2 评论 3 代码增减 +136 / -4

执行摘要

ROCm HiSparse DSv4 swap-in 融合为单次拷贝,decode 路径提速最高约 21%

DeepSeek-V4 C4 token 为 584 字节但并非连续内存:页面行内先有 64 个 576B value 再接 64 个 8B scale,导致 ROCm swap-in 需两次 transfer_item_warp。在 wave64 上这产生三段 wavefront pass,且 item_size_bytes 是运行时参数使编译器将 value 拷贝保留为带循环内 s_waitcnt 的滚动循环,三次 pass 在宿主内存延迟上完全串行化。PR body 明确指出 swap_in_selected_pages 位于 decode 关键路径、每个 C4 层每步执行一次,而 CUDA 分支的 transfer_item 已能合并 value 尾段与 scale,只是其 warp 逻辑硬编码为 32 lane 无法用于 ROCm。

值得精读。该 PR 是典型的 ROCm 波前级性能优化案例,包含反汇编驱动的根因分析、编译期常量展开技巧、以及一个反直觉的结论(更少 pass 的 128 位拷贝反而更慢),对理解 wave64 占用率与宿主内存延迟的权衡有较高参考价值。建议关注 transfer_dsv4_item_warp 的实现细节(空 lane 处理、双层 unroll 的负载/存储分区)以及新测试对“未写槽位保持填充”的断言设计。若团队在 AMD 平台使用 HiSparse DSv4,此 PR 应合入且可考虑在后续 CI 中补充 ROCm 内核反汇编级别的回归检查。

讨论亮点

Review 评论整体较少。HaiShaw 的最终 review 仅一句 ROCm/HIP gated 并 APPROVED,表明该改动被确认限定在 ROCm/HIP 编译门内,未触及 CUDA 路径。PR body 中作者详细记录了反汇编证据与两个备选方案的对比,核心讨论点是:为什么不用单次 128 位 pass(更少 round trip)?结论是 wave64 下 64 位全 lane 占用比 128 位部分 lane 占用更能隐藏宿主内存延迟,这一结论与同仓库 #33085 的观察一致。该决策属于实现层面的自我论证,未在评论区引发公开交锋,但体现了对 wavefront 占用率与访存延迟的深入考量。

实现拆解

  1. 新增融合拷贝函数:在 python/sglang/kernels/jit/csrc/hisparse.cuh 的 ROCm #ifdef 分支中新增 transfer_dsv4_item_warp。它以 lane_id 为索引将 576B value(72 个 64 位字)与 8B scale(1 个 64 位字)视为 73 字空间,用编译期常量 kValueWordskTotalWordskPasses(738 字节分 64 lane 得 2 pass)驱动 #pragma unroll 的加载循环与存储循环。先循环内将所有非空槽位加载到 staged[],再统一写入,从而让两条 global_load 先于 global_store 发出,将暴露的宿主往返从 3 次降至 1 次。
  2. 替换原调用:在 load_cache_to_device_buffer_kernelIsDsv4Layout ROCm 分支中,用 transfer_dsv4_item_warp(lane_id, src_value_ptr, src_scale_ptr, dst_value_ptr, dst_scale_ptr) 替换原先两次 transfer_item_warp 调用。CUDA 分支与通用(非 DSv4)路径完全不动。
  3. 新增针对性测试:在 test/registered/kernels/ops/kvcache/test_hisparse.py 中新增 test_load_cache_to_device_buffer_dsv4_fused_copy_multi_miss,构造一次启动内多个 miss、源与目的 page 偏移各异(含第二页、value/scale 分界落在不同位置)的场景,从内核输出读取落点而非假设驱逐顺序,并校验未被写入的槽位保持 0xFF 填充,防止过度拷贝越界。
  4. 性能数据与方案权衡:PR body 给出 MI355X 上 batch 1/10/24/100 的实测提升,并对比了单次 128 位 pass 方案:虽然 pass 数更少但 lane 参与度低(36 个 16B 向量 + 1 个 scale),反而在 batch 1 比基线慢约 1%–3.6%,说明 64 位全 wavefront 占用优于 128 位部分占用,与 #33085 的门控效应一致。
  5. 配套说明:无 API、配置、模板或文档改动;C++ 变更严格限制在 ROCm #ifdef 内。
文件 模块 状态 重要度
python/sglang/kernels/jit/csrc/hisparse.cuh HiSparse modified 5.23
test/registered/kernels/ops/kvcache/test_hisparse.py HiSparse modified 5.77

关键符号

transfer_dsv4_item_warp load_cache_to_device_buffer_kernel test_load_cache_to_device_buffer_dsv4_fused_copy_multi_miss

关键源码片段

python/sglang/kernels/jit/csrc/hisparse.cuh core-logic

核心实现文件,新增 `transfer_dsv4_item_warp` 并替换 ROCm DSv4 分支的两次 `transfer_item_warp` 调用,是本次性能优化的主要载体。

// 本函数在 ROCm 分支内,用于将一个 DSv4 C4 token(576B value + 8B scale)
// 作为单个 73 字空间拷贝,替代原先两次 transfer_item_warp 调用。
// 关键点:kTotalWords 是编译期常量,因此两轮 #pragma unroll 循环可以在
// 编译期确定 pass 数,使所有 global_load 先发出、再统一 global_store,
// 从而把宿主内存往返从 3 次降为 1 次。
__device__ __forceinline__ void transfer_dsv4_item_warp(
    int32_t lane_id,
    const int64_t* __restrict__ src_value,
    const int64_t* __restrict__ src_scale,
    int64_t* __restrict__ dst_value,
    int64_t* __restrict__ dst_scale) {
  constexpr int32_t kValueWords = static_cast<int32_t>(device::hisparse::kValueBytes / sizeof(int64_t));
  constexpr int32_t kTotalWords = static_cast<int32_t>(device::hisparse::kItemBytes / sizeof(int64_t));
  constexpr int32_t kPasses = (kTotalWords + WARP_SIZE - 1) / WARP_SIZE;
  // 对齐假设必须在编译期成立,否则无法安全按 64 位字访问
  static_assert(device::hisparse::kValueBytes % sizeof(int64_t) == 0, "value must be whole 64-bit words");
  static_assert(device::hisparse::kScaleBytes % sizeof(int64_t) == 0, "scale must be whole 64-bit words");  const int64_t* src_slot[kPasses];
  int64_t* dst_slot[kPasses];
  int64_t staged[kPasses];  // 第一轮:所有 lane 把各自负责的字加载到 staged,空 lane 对应的槽位为 nullptr
#pragma unroll
  for (int32_t p = 0; p < kPasses; ++p) {
    const int32_t i = p * WARP_SIZE + lane_id;
    const bool is_value = i < kValueWords;
    const bool is_scale = !is_value && i < kTotalWords;
    src_slot[p] = is_value ? src_value + i : (is_scale ? src_scale + (i - kValueWords) : nullptr);
    dst_slot[p] = is_value ? dst_value + i : (is_scale ? dst_scale + (i - kValueWords) : nullptr);
    if (src_slot[p] != nullptr) staged[p] = *src_slot[p];
  }
  // 第二轮:统一写入,编译器可根据依赖排除同一地址的冲突,
  // 并让两条 load 先于 store 发出(反汇编验证:两个 global_load 后跟一个
  // s_waitcnt 再是 global_store)
#pragma unroll
  for (int32_t p = 0; p < kPasses; ++p) {
    if (dst_slot[p] != nullptr) *dst_slot[p] = staged[p];
  }
}
test/registered/kernels/ops/kvcache/test_hisparse.py test-coverage

新增 `test_load_cache_to_device_buffer_dsv4_fused_copy_multi_miss` 测试,覆盖多 miss、可变源 / 目的偏移(含第二页与 value/scale 分界错位)以及未写槽位填充校验,是保障该优化正确性的关键配套。

@pytest.mark.skipif(
    not is_hip(), reason="Covers the ROCm wavefront64 fused DSv4 token copy."
)
def test_load_cache_to_device_buffer_dsv4_fused_copy_multi_miss() -> None:
    """Several DSv4 misses in one launch must each land byte-exact.    融合拷贝把 576B value 与 8B scale 当作 73 字空间行走,
    因此 value/scale 分界落在 lane 索引上而不是调用边界上。
    这里同时变化源与目的 page 偏移,包括第二页上的 token,
    确保分界不会总落在同一地址。
    """
    hot_buffer_size = 4
    num_pages = 2
    # seq_len 保持高于查询到的 token,避免新 token 被内核不经宿主拷贝直接放置
    seq_len = 16
    host_locs = list(range(seq_len))
    miss_tokens = [4, 5, 6, 7]
    # 源偏移:页中间、页 0 最后槽、页 1 第一槽、页 1 最后槽
    for token, loc in zip(miss_tokens, [10, 63, 64, 127]):
        host_locs[token] = loc
    # 目的偏移:页 0 第一、第二、最后槽,然后页 1
    device_locs = [0, 1, 63, 64, 65]
​
    # 用固定 seed 写入 host_cache,使每个 token 内容可预期
    host_cache = torch.zeros(
        (num_pages, DSV4_PAGE_BYTES), dtype=torch.uint8, device="cpu", pin_memory=True
    )
    for loc in host_locs:
        _write_dsv4_token(host_cache, loc, seed=loc + 1)
​
    # 设备缓冲填充 0xFF,用于后续检测未被拷贝的槽位是否被意外覆写
    device_buffer = torch.full(
        (num_pages, DSV4_PAGE_BYTES), 0xFF, dtype=torch.uint8, device=DEVICE
    )
​
    top_k_tokens = torch.tensor([miss_tokens], dtype=torch.int32, device=DEVICE)
    out = torch.full_like(top_k_tokens, -1)
​
    load_cache_to_device_buffer_dsv4_mla(
        top_k_tokens=top_k_tokens,
        device_buffer_tokens=torch.tensor(
            [[0, 1, 2, 3, -1]], dtype=torch.int32, device=DEVICE
        ),
        host_cache_locs=torch.tensor([host_locs], dtype=torch.int64, device=DEVICE),
        device_buffer_locs=torch.tensor([device_locs], dtype=torch.int32, device=DEVICE),
        host_cache=host_cache,
        device_buffer=device_buffer,
        top_k_device_locs=out,
        req_pool_indices=torch.tensor([0], dtype=torch.int64, device=DEVICE),
        seq_lens=torch.tensor([seq_len], dtype=torch.int32, device=DEVICE),
        lru_slots=torch.arange(hot_buffer_size, dtype=torch.int16, device=DEVICE).view(1, -1),
        item_size_bytes=DSV4_ITEM_BYTES,
        num_top_k=len(miss_tokens),
        hot_buffer_size=hot_buffer_size,
        page_size=DSV4_PAGE_SIZE,
        block_size=256,
        num_real_reqs=torch.tensor([1], dtype=torch.int32, device=DEVICE),
    )
    torch.cuda.synchronize()
​
    # 每个 miss 驱逐到哪个槽由 LRU 决定,因此从内核输出读取实际落点,
    # 只要求落点各不相同且在合法集合内
    landed = out.cpu().tolist()[0]
    assert len(set(landed)) == len(landed)
    assert set(landed).issubset(device_locs)
​
    # 逐 token 校验 byte-exact:目的槽内容必须与源完全一致
    device_cpu = device_buffer.cpu()
    for token, dst_loc in zip(miss_tokens, landed):
        assert torch.equal(
            _read_dsv4_token(device_cpu, dst_loc),
            _read_dsv4_token(host_cache, host_locs[token]),
        ), f"token {token} -> device loc {dst_loc}"
​
    # 未被内核写过的槽位必须保持 0xFF 填充,
    # 这样任何超出 value 或 scale 边界的过度拷贝都会被抓住
    for loc in set(device_locs) - set(landed):
        assert torch.all(_read_dsv4_token(device_cpu, loc) == 0xFF)

评论区精华

ROCm 路径门控确认 设计

HaiShaw 在最终 review 中说 'ROCm/HIP gated',表示确认改动只影响 ROCm/HIP 编译分支,CUDA 路径不受影响。

结论:Pr 被批准,改动被限定在 ROCm 条件编译内。 · 已解决

单次 128 位 pass 方案为何被否决 性能

PR body 详细对比了单次 128 位 pass(36 个 16B 向量 + 1 个 scale)与本 PR 的两趟 64 位方案:前者 pass 数更少但 batch 1 反而比基线慢约 1%–3.6%,后者在所有 batch 均更快。作者归因于 wave64 下全 lane 占用(64 位)比部分 lane 占用(128 位)更能隐藏宿主内存延迟,且与 #33085 的结论一致。

结论:采用两趟 64 位全 wavefront 方案,放弃单趟 128 位方案。 · 已解决

风险与影响

风险点集中在新增内核路径的正确性与回归上:1)transfer_dsv4_item_warpsrc_slot[p]/dst_slot[p]nullptr 的空 lane 处理依赖 is_value/is_scale 判断,若 kValueBytes/kScaleBytes 与 8 字节对齐假设被破坏(已用 static_assert 保护)或未来 DSv4 布局变化,可能导致越界访问;2)测试仅覆盖 ROCm(skipif not is_hip()),CUDA 侧的 DSv4 paged 用例本就不走该路径,但新测试并未在 CUDA 上执行,若未来统一入口被改动可能漏检;3)性能优化依赖编译器对 #pragma unroll 与加载/存储顺序的调度,不同 ROCm 编译器版本可能生成不同 SASS,收益幅度存在波动风险;4)load_cache_to_device_buffer_kernel 是 decode 关键路径,任何错误都可能静默污染 KV 缓存,所幸新增测试校验了 byte-exact 与未写槽位填充,能抓住大部分越界或错位问题。

影响范围限定在 ROCm (AMD) 平台、HiSparse DSv4 分页布局的 miss swap-in 路径,即 DeepSeek-V4 类模型在 HIP 上的 decode 关键路径。收益显著:batch 1 提升约 14%–17%,batch 10 提升约 20%–21%,且随 batch 增大收益衰减(带宽受限后 round-trip 不再主导)。对 CUDA 用户无影响,对非 DSv4 布局无影响。团队侧收益:为未来 DSv4 或类似非连续 token 布局的 ROCm 拷贝提供了可复用的实现范式,测试方法(多 miss、可变偏移、校验未写槽位)也可供后续 kernel 测试参考。

ROCm 专用路径 decode 关键路径 依赖编译器展开行为 测试仅覆盖 HIP

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论