Prhub

#33085 perf(hisparse): 128-bit non-temporal swap-in copy on ROCm

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

执行摘要

HiSparse ROCm miss 拷贝改 128 位非临时加载,decode 提速

PR body 指出 transfer_item_warp 在仓库中存在两份实现:HiCache 的 L2→L1 gather 版本(kernels/aot/csrc/kvcacheio/transfer.cu)正由 #30024 加宽,而 HiSparse 的 swap-in miss 拷贝版本(kernels/jit/csrc/hisparse.cuh)在 ROCm 上仍是普通 64 位标量循环、无 non-temporal 提示,同一文件里的 CUDA 分支却已使用 128 位配对 ld.global.nc.v2.b64,ROCm 路径严格窄于同文件 CUDA 路径。swap-in 位于 decode 关键路径:swap_in_selected_pages 每个 C4 层每个 decode 步在主 stream 上运行,位于 indexer 与 attention 之间,无法与计算重叠,每次 miss 都是 wavefront 直读 pinned host DRAM。

值得精读,重点看三处:(1) 门控条件的推导——为什么 128 位拷贝在 512B item 下反而更慢,item_size_bytes >= WARP_SIZE * 16 背后的 lane 利用率模型;(2) __builtin_nontemporal_load 与缓存 store 的取舍——源只读一次用 non-temporal,目标被后续 attention 内核读所以保留缓存;(3) 接缝测试的参数设计——覆盖宽路径与 64 位/字节余量循环的每个衔接形状,并用 skipif(not is_hip()) 明确 CUDA 分支的契约边界。建议把 PR body 的 benchmark 表格与 commit 2 的隔离实验一起看,这是一个完整的“优化→发现回归→定位根因→门控→补测试”闭环。

讨论亮点

PR 没有 review 评论,HaiShaw 直接 APPROVED;关键讨论记录在 issue 评论与提交历史中。HaiShaw 在 issue 评论里标注 /tag-and-rerun-ciROCm/HIP gated,对应第三个 commit 将新增接缝测试门控到 HIP:该测试在 CUDA CI 触发 CUDBG_EXCEPTION_WARP_MISALIGNED_ADDRESS 使 pytest 进程 exit -6 并 fast-fail 依赖任务,而 CUDA 分支契约只支持 16B 对齐且无 sub-8B 余量的 item。另一个核心讨论是门控的必要性:commit 6ffa2684 记录无条件 128 位加宽在 512B item 下是 14-22% 的回归,隔离实验证明 non-temporal hint 单独使用近乎中性(81.1 vs 81.2 us),回归完全由拷贝宽度引入,因此 item_size_bytes >= WARP_SIZE * 16 是承重门控而非保守开关。

实现拆解

  1. 扩展 transfer_item_warp 的 ROCm 分支python/sglang/kernels/jit/csrc/hisparse.cuh):新增 TransferVec4__vector_size__(16)uint32_t 向量),当源与目标均 16B 对齐且 item_size_bytes >= WARP_SIZE * 16 时执行宽路径,每个 lane 每次搬 16B;源加载使用 __builtin_nontemporal_load(流式数据只读一次、避免污染 L2/MALL),store 保持缓存语义供后续 attention 内核直接读取。宽路径结束后从 byte_pos 续走原有 64 位循环,未对齐与 sub-threshold 场景完全由 64 位循环覆盖。
  2. 门控修正回归:第一版无条件加宽在 bench_hisparse.py 默认 512B item 下反而慢 14-22%,根因是 wave64 下 16B/lane 只覆盖 32/64 lane,而迭代次数与 8B 路径相同,等价于半波闲置;据此引入 item_size_bytes >= WARP_SIZE * 16 门控,保证 1024B(恰好等于门限)时整波参与、单条指令搬完一个 item。
  3. 新增接缝测试test/registered/kernels/ops/kvcache/test_hisparse.py):test_load_cache_to_device_buffer_miss_copy_is_byte_exact 参数化覆盖 16/20/24/40B item 在 16B 对齐与未对齐源下的 miss 拷贝,断言落点字节精确且邻槽无越界覆盖写;测试用 skipif(not is_hip()) 限定 ROCm。
  4. CI 适配:第三个 commit 将测试门控到 HIP,修复其在 CUDA CI 触发 WARP_MISALIGNED_ADDRESS 导致 pytest 整体退出(exit -6)的问题,HaiShaw 用 /tag-and-rerun-ci 重跑 CI。
文件 模块 状态 重要度
python/sglang/kernels/jit/csrc/hisparse.cuh 缓存内核 modified 5.27
test/registered/kernels/ops/kvcache/test_hisparse.py 缓存测试 modified 5.78

关键符号

transfer_item_warp test_load_cache_to_device_buffer_miss_copy_is_byte_exact

关键源码片段

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

核心源码变更:ROCm 分支的 `transfer_item_warp` 新增 128 位向量 + 非临时加载路径,并用 item_size 门控避免 wave64 半波闲置回归;这是 swap-in miss 拷贝的 decode 关键路径。

#ifdef USE_ROCM
// 128 位向量类型:4 个 uint32 共 16B,宽路径每次搬一个 dwordx4
using TransferVec4 = __attribute__((__vector_size__(4 * sizeof(uint32_t)))) uint32_t;__device__ __forceinline__ void transfer_item_warp(
    int32_t lane_id,
    const void* __restrict__ src_addr,
    void* __restrict__ dst_addr,
    int64_t item_size_bytes) {
  const auto src = static_cast<const char*>(src_addr);
  auto dst = static_cast<char*>(dst_addr);  int64_t byte_pos = 0;
  // 源与目标都 16B 对齐,且 item 大小能填满整个 wavefront 时才走宽路径(每个 lane 搬 16B)。
  // 门控是承重的:实测 MI355X(wave64)上 512B item 时 16B/lane 只覆盖 32/64 个 lane,
  // 迭代次数却与 8B 路径相同,导致 14-22% 的负优化;1024B(= WARP_SIZE * 16)时整波参与。
  const bool aligned_16b =
      ((reinterpret_cast<uintptr_t>(src) | reinterpret_cast<uintptr_t>(dst)) & 0xF) == 0;
  const bool wide_fills_wave =
      item_size_bytes >= static_cast<int64_t>(WARP_SIZE) * 16;
  if (aligned_16b && wide_fills_wave) {
    constexpr int64_t kVecBytes = static_cast<int64_t>(sizeof(TransferVec4));
    const int64_t vec_count = item_size_bytes / kVecBytes;
    const auto src_vec = reinterpret_cast<const TransferVec4*>(src);
    auto dst_vec = reinterpret_cast<TransferVec4*>(dst);
    for (int64_t i = lane_id; i < vec_count; i += WARP_SIZE) {
      // 源是 pinned host DRAM 流式数据,只读一次,用 non-temporal 加载避免污染 L2/MALL;
      // store 故意保留缓存语义,因为下一个 attention 内核立刻要读这块 device buffer。
      dst_vec[i] = __builtin_nontemporal_load(&src_vec[i]);
    }
    byte_pos = vec_count * kVecBytes;
  }  // 64 位路径:完整覆盖未对齐场景,并处理宽路径留下的 <=8B 余量;
  // 之后的 <8B 字节尾部循环保持原样,未在片段中重复。
  const int64_t word_count = item_size_bytes / static_cast<int64_t>(sizeof(uint64_t));
  const int64_t word_start = byte_pos / static_cast<int64_t>(sizeof(uint64_t));
  const auto src_words = reinterpret_cast<const uint64_t*>(src);
  auto dst_words = reinterpret_cast<uint64_t*>(dst);
  for (int64_t i = word_start + lane_id; i < word_count; i += WARP_SIZE) {
    dst_words[i] = src_words[i];
  }
}
#endif
test/registered/kernels/ops/kvcache/test_hisparse.py test-coverage

新增接缝测试 `test_load_cache_to_device_buffer_miss_copy_is_byte_exact`:此前 ROCm 用例全部使用 32B item,远低于宽路径门限,宽路径与余量循环的接缝完全没有覆盖;该测试补上了这块空白。

@pytest.mark.skipif(
    not is_hip(),
    reason="CUDA transfer_item_warp assumes 16B-aligned items with no sub-8B remainder.",
)
@pytest.mark.parametrize(
    "kv_dim,miss_token",
    [
        # token 0..3 常驻,查询 >=4 才会 miss;目标槽固定为 0,因此源偏移
        # (miss_token * item_size)决定 16B 对齐检查。
        (256, 4), # 1024B:恰好等于门限,每 lane 一个 16B 步,无余量
        (257, 4), # 1028B:宽路径 + 4B 字节尾部
        (258, 4), # 1032B:宽路径 + 一个 64 位字
        (260, 4), # 1040B:lane 0 上两轮宽迭代
        (257, 5), # 1028B 且源偏移 5140 未对齐:跳过宽路径
        (5, 4), # 20B:低于门限,走 64 位循环 + 4B 字节尾部
    ],
)
def test_load_cache_to_device_buffer_miss_copy_is_byte_exact(
    kv_dim: int, miss_token: int
) -> None:
    """miss 拷贝必须对任意 item 大小与对齐都字节精确。    本文件其它 ROCm 用例都用 32B item,远低于 WARP_SIZE * 16 宽拷贝门限,
    完全到不了宽路径,更测不到宽路径与余量循环之间的接缝;这些尺寸
    覆盖门限两侧以及每种余量形状。
    """
    item_size_bytes = kv_dim * torch.empty((), dtype=DTYPE).element_size()
    host_cache = torch.empty(
        (HOST_CACHE_SIZE, 1, kv_dim), dtype=DTYPE, device="cpu", pin_memory=True
    )
    host_cache.copy_(torch.arange(host_cache.numel(), dtype=DTYPE).view_as(host_cache))
    device_buffer = torch.full(
        (DEVICE_CACHE_SIZE, 1, kv_dim), -1, dtype=DTYPE, device=DEVICE
    )
    # slot 0..3 放 token 0..3;slot 4 是保留的新槽位。
    device_buffer_locs = torch.tensor([[0, 1, 2, 3, 4]], dtype=torch.int32, device=DEVICE)
    device_buffer_tokens = torch.tensor([[0, 1, 2, 3, -1]], dtype=torch.int32, device=DEVICE)
    for slot in range(HOT_BUFFER_SIZE):
        device_buffer[slot].copy_(host_cache[slot].to(DEVICE))
    torch.cuda.synchronize()
​
    top_k_tokens = torch.tensor([[miss_token]], dtype=torch.int32, device=DEVICE)
    out = torch.full_like(top_k_tokens, -1)
​
    load_cache_to_device_buffer_mla(
        top_k_tokens=top_k_tokens,
        device_buffer_tokens=device_buffer_tokens,
        host_cache_locs=torch.arange(
            HOST_CACHE_SIZE, dtype=torch.int64, device=DEVICE
        ).view(1, -1),
        device_buffer_locs=device_buffer_locs,
        host_cache=host_cache,
        device_buffer=device_buffer,
        top_k_device_locs=out,
        req_pool_indices=torch.arange(1, dtype=torch.int64, device=DEVICE),
        seq_lens=torch.full((1,), 8, dtype=torch.int32, device=DEVICE),
        lru_slots=torch.arange(HOT_BUFFER_SIZE, dtype=torch.int16, device=DEVICE).view(
            1, -1
        ),
        item_size_bytes=item_size_bytes,
        num_top_k=1,
        hot_buffer_size=HOT_BUFFER_SIZE,
        page_size=1,
        block_size=256,
        num_real_reqs=torch.tensor([1], dtype=torch.int32, device=DEVICE),
    )
    torch.cuda.synchronize()
​
    # miss 淘汰 LRU 头(slot 0,物理 loc 0)并落到这里。
    assert torch.equal(out.cpu(), torch.tensor([[0]], dtype=torch.int32))
    assert torch.equal(device_buffer[0].cpu(), host_cache[miss_token])
    # 邻槽不能被越界拷写污染。
    for slot in range(1, HOT_BUFFER_SIZE):
        assert torch.equal(device_buffer[slot].cpu(), host_cache[slot])

评论区精华

宽拷贝门控:wave64 lane 利用率 vs 拷贝宽度 性能

第一版无条件加宽在 512B item 下比基线慢 14-22%;隔离实验显示 non-temporal hint 单独使用中性(81.1 vs 81.2 us),回归完全来自 128 位宽度导致 512/16=32 个 lane 参与而 512/8=64 个 lane 全参与。

结论:引入 item_size_bytes >= WARP_SIZE * 16 门控并保留 64 位循环作为未对齐与余量路径;1024B(DeepSeek-V4 C4 size)在低 batch 获得 13-20% 收益。 · 已解决

接缝测试在 CUDA CI 崩溃,测试门控到 ROCm 测试

新增 byte-exact 测试在 CUDA CI 触发 CUDBG_EXCEPTION_WARP_MISALIGNED_ADDRESS,pytest 进程 exit -6 并 fast-fail 依赖任务;CUDA 分支只处理 16B 对齐且无 sub-8B 余量的 item,非 16B 倍数尺寸超出其契约。

结论:用 skipif(not is_hip()) 将测试限定在 ROCm/HIP;作者说明该测试是回归保护而非演示既有 bug,原有 13 passed / 2 skipped 保持不变。 · 已解决

bench_hisparse.py 只测顺序 gather 的偶然发现 other

PR body 指出 `bench_hisparse.py` 的 misses 构建为 `hot_buffer_size + arange(num_misses)`,host_cache_locs 也是 `arange(...)`,所有 miss 读连续 host 槽,而真实 HiSparse miss 是分散的;作者用随机化 host_cache_locs 复测,差异仅 1.00-1.05x。

结论:对带宽结论不构成误导,但顺序模式从代码不易看出,建议后续为 benchmark 增加随机模式作为独立改进。 · 待处理

风险与影响

技术风险集中在 ROCm 专属路径:

1) 门控依赖 wave64 的 lane 利用率结论,若未来 ROCm 引入 wave32 或其他波前尺寸,门控语义与收益需要重新验证(hisparse.cuhWARP_SIZE 宏决定);
2) __builtin_nontemporal_load 改变 L2/MALL 缓存行为,当前结论只在 MI355X(gfx950)上实测,其他 ROCm 显卡或混合负载需复测;
3) 新增测试只跑 HIP,CUDA 分支未来若被改动,该测试无法提供保护;
4) 性能收益边界明确:只在低 batch 且 item_size >= 1024B 时显现,高 batch 已接近约 55 GB/s 的 host-fabric 带宽上限,不应期待端到端收益。

影响范围仅限 AMD/ROCm + HiSparse(host-backed unified KV cache)的 decode swap-in 路径;TTFT/prefill 走 HiCache 的 kvcacheio/transfer.cu 不受影响,CUDA 与 CPU 侧行为完全不变。性能上,1024B item 在 batch 1/10 时 swap-in 拷贝延迟分别下降约 19.9%/12.8%,batch >= 24 后基本持平(0.5% 以内)。对团队而言,本 PR 提供了 wave64 宽拷贝门控的实测方法论和接缝测试范式,后续在 ROCm 上做同类向量化优化可直接复用。

ROCm 专属路径变更 decode 关键路径 wave64 门控假设 测试仅 HIP 运行

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论