Prhub

#42212 [Perf] Triton fast path for small CPU→GPU `swap_blocks_batch` in the offloading connector

原始 PR 作者 Etelis 合并时间 2026-06-03 18:38 文件变更 3 提交数 49 评论 46 代码增减 +180 / -11

执行摘要

Triton 内核加速 CPU→GPU 小块批量拷贝

cuMemcpyBatchAsync在CPU→GPU(onload)方向对4-24 KiB的小块描述符拷贝吞吐量仅为6-7 GB/s,远低于PCIe Gen5的~55 GB/s上限,且随着描述符数量增加性能进一步下降。PR body通过详细微基准图表展示了DMA小页面悬崖现象,而KV offload实际运行在8-16 KiB区间,因此需要绕过DMA路径。采用Triton内核后,在同类条件下拷贝带宽提升至37-48 GB/s。

建议精读。该PR展示了如何通过数据驱动的调优(阈值扫描、SM数量选择)将定制Triton内核应用于关键IO路径,并提供了详实的E2E基准验证。设计决策(如初始化时解析函数、缓冲复用)值得借鉴。

讨论亮点
  • 函数位置:orozery 最初要求将包装函数移到 gpu_worker.py,经多轮迭代后决定保留在独立文件 swap_blocks_triton.py 中,与内核和常量共置。
  • 平台兼容性:orozery 询问对AMD/ROCm、XPU等的支持,Etelis 回应Triton内核可在ROCm运行但阈值需重调,最终通过 HAS_TRITON 守卫避免在非CUDA平台使用。
  • 内存固定:AI评论指出地址张量未固定导致 .to("cuda", non_blocking=True) 实为同步,Etelis 通过 _new_descriptor_buffers 使用 pin_memory 修复。
  • 单元测试:orozery 要求单元测试,Etelis 添加 test_swap_blocks_triton.py
  • SM竞争顾虑:ivanium 担心Triton内核占用SM导致计算延迟,Etelis 提供E2E数据证明 sm12 相对于 sm20 无性能损失,且收益显著。

实现拆解

  1. 创建 swap_blocks_triton.py:定义Triton JIT内核 _swap_blocks_kernel,以及包装函数 swap_blocks_batch。该包装函数包含保底逻辑:当批量大小小于 MIN_N(16)时回退到C++ DMA;否则将描述符张量非阻塞复制到GPU后启动内核。内核采用SM循环(NUM_SMS=12)和按块拷贝(BYTES_PER_CHUNK=8192),处理任意大小和对齐。

  2. 修改 gpu_worker.py:新增 _select_swap_blocks_fn 函数,在处理器初始化时根据方向(GPU→CPU或CPU→GPU)、平台是否支持Triton、页面大小(≤THRESHOLD_BYTES=28 KiB且8字节对齐)决定使用哪个 swap 函数。对于CPU→GPU且满足条件时,通过 functools.partial 绑定 bytes_per_chunk 参数并返回 swap_blocks_batch;否则返回C++ ops.swap_blocks_batch

  3. 缓冲重用:修改 Transfer dataclass,新增 batch_srcbatch_dstbatch_sizes 三个固定内存张量,由 _new_descriptor_buffers 分配并复用,避免每次传输分配新缓冲。

  4. 添加单元测试:创建 test_swap_blocks_triton.py,使用多种8字节对齐大小(包括块边界和尾部无掩盖)验证Triton拷贝的正确性。

  5. 平台守卫:在 gpu_worker.py 中导入 HAS_TRITONtriton,并在 _select_swap_blocks_fn 中检查 HAS_TRITON,确保在无Triton环境(如ROCm旧版)回退DMA。

文件 模块 状态 重要度
vllm/v1/kv_offload/cpu/swap_blocks_triton.py 交换内核 added 8.09
vllm/v1/kv_offload/cpu/gpu_worker.py 卸载连接器 modified 7.85
tests/v1/kv_offload/cpu/test_swap_blocks_triton.py 交换测试 added 6.45

关键符号

_swap_blocks_kernel swap_blocks_batch _select_swap_blocks_fn _new_descriptor_buffers

关键源码片段

vllm/v1/kv_offload/cpu/swap_blocks_triton.py core-logic

核心新文件,包含 Triton 内核和包装函数,是性能优化的技术主体。

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Triton kernel + tuned constants for the ``swap_blocks_batch`` fast path."""from __future__ import annotationsimport torchfrom vllm import _custom_ops as ops
from vllm.triton_utils import tl, triton# Constants tuned empirically on H100 (PCIe Gen5):
# NUM_SMS - 最小的 SM 切片,在 8-32 KB 等实际块大小下距离峰值带宽 5% 以内
# THRESHOLD_BYTES - Triton 胜出 DMA 的最大每描述符字节数;超过此值则 C++ cuMemcpyBatchAsync 更快
# MIN_N - 最能摊还 Triton 发射开销的最小批量数;低于此值则 DMA 更优
NUM_SMS = 12
THRESHOLD_BYTES = 28 * 1024
MIN_N = 16
​
​
@triton.jit
def _swap_blocks_kernel(
    src_addrs,
    dst_addrs,
    sizes,
    n_jobs,
    BYTES_PER_CHUNK: tl.constexpr,
):
    """Triton 内核:将 CPU 端数据按描述符描述的非连续区域拷贝到 GPU 端。    每个程序处理一个 job(即一个描述符),多个程序通过 while 循环共享工作。
    对于每个 job,按 CHUNK 粒度逐块拷贝,并处理尾部不对齐的情况。
    """
    pid = tl.program_id(0)
    num_progs = tl.num_programs(0)
    WORDS_PER_CHUNK: tl.constexpr = BYTES_PER_CHUNK // 8
    offsets = tl.arange(0, WORDS_PER_CHUNK)
    job = pid
    while job < n_jobs:
        src = tl.load(src_addrs + job).to(tl.pointer_type(tl.int64))
        dst = tl.load(dst_addrs + job).to(tl.pointer_type(tl.int64))
        words = tl.load(sizes + job) // 8
        for start in range(0, words, WORDS_PER_CHUNK):
            idx = start + offsets
            mask = idx < words
            data = tl.load(src + idx, mask=mask, other=0)
            tl.store(dst + idx, data, mask=mask)
        job += num_progs
​
​
def swap_blocks_batch(
    src_addrs: torch.Tensor,
    dst_addrs: torch.Tensor,
    sizes: torch.Tensor,
    is_src_access_order_any: bool = False,
    *,
    bytes_per_chunk: int,
) -> None:
    """Triton 实现 ``swap_blocks_batch``,专用于小型 CPU->GPU 批量拷贝。    若批量大小小于 MIN_N,则直接回退到 C++ 的 cuMemcpyBatchAsync 路径。
    否则,将地址和大小张量异步复制到 GPU,然后启动 Triton 内核,
    使用预先绑定的 ``bytes_per_chunk`` 块大小。
    """
    n = src_addrs.numel()
    if n < MIN_N:
        ops.swap_blocks_batch(
            src_addrs,
            dst_addrs,
            sizes,
            is_src_access_order_any=is_src_access_order_any,
        )
        return
    _swap_blocks_kernel[(min(NUM_SMS, n),)](
        src_addrs.to("cuda", non_blocking=True),
        dst_addrs.to("cuda", non_blocking=True),
        sizes.to("cuda", non_blocking=True),
        n,
        BYTES_PER_CHUNK=bytes_per_chunk,
    )

评论区精华

函数位置和组织 设计

orozery 最初要求将包装函数从独立文件移到 gpu_worker.py,经过多次迭代后最终决定保留在独立文件 swap_blocks_triton.py 中,与内核和常量共置。

结论:接受独立文件方案,内核和 Python 包装保持在同一个文件中。 · 已解决

平台兼容性和 Triton 守卫 设计

orozery 询问 AMD/ROCm、XPU 等是否支持,Etelis 回应 Triton 内核可在 ROCm 运行但阈值需重测,最终通过 HAS_TRITON 守卫和 gpu_to_cpu 方向过滤确保非 CUDA 平台回退。

结论:在 _select_swap_blocks_fn 中检查 HAS_TRITON,不满足时回退 C++ DMA。 · 已解决

内存固定以支持异步拷贝 性能

AI 评论指出地址张量来自 unpinned numpy 数组,导致 .to("cuda", non_blocking=True) 实际同步。Etelis 通过 _new_descriptor_buffers 使用 pin_memory 固定内存修复。

结论:新建固定内存缓冲,并在 transfer 中复用,确保异步 H2D 拷贝。 · 已解决

单元测试要求 测试

orozery 要求添加单元测试覆盖 Triton swap 路径。Etelis 添加 test_swap_blocks_triton.py,使用多种 8 字节对齐大小测试拷贝正确性。

结论:添加单元测试,验证 Triton kernel 拷贝结果与源字节一致。 · 已解决

风险与影响

  • 平台兼容性:Triton内核在非CUDA平台(如ROCm)可能未充分测试,但通过 HAS_TRITON 回退和 gpu_to_cpu 方向过滤降低了风险。阈值和SM数量基于H100 PCIe Gen5调优,其他GPU可能非最优,但可安全回退DMA。
  • 内核竞争:Triton内核使用SM进行拷贝,可能与计算内核争抢资源,但E2E测试表明SM12相对于SM16/20无差别,且整体吞吐量提升大幅覆盖潜在竞争开销。
  • 内存固定开销:引入固定内存缓冲,但数量可控(每方向一次性分配),不会显著增加系统压力。
  • 测试覆盖:仅一个单元测试基本正确性,缺乏压力测试、边界条件和错误路径覆盖。
  • 阈值硬编码NUM_SMSTHRESHOLD_BYTESMIN_N 基于特定硬件调优,但回退机制确保不会出错。
  • 用户:KV offloading 用户获得显著性能提升(请求吞吐量提高105-154%),尤其是共享前缀长、生成长度短的工作负载。
  • 系统:增加了Triton代码路径,略微增加代码复杂度和依赖(需Triton),但保持与C++ DMA的兼容回退。
  • 团队:为未来offloading connector的优化提供了性能框架;非NVIDIA平台可能需要重新校准阈值,但目前回退保证功能正确。
非 NVIDIA 平台阈值未校准 SM 竞争风险 固定内存开销 测试覆盖不足 阈值硬编码

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论