Prhub

#41163 [Perf] Optimize `AllPool.forward` by slicing first, 51% faster in the method level benchmark

原始 PR 作者 yewentao256 合并时间 2026-04-29 23:11 文件变更 1 提交数 1 评论 1 代码增减 +11 / -7

执行摘要

AllPool.forward 提速 51%

该 PR 是 Issue #35631 "Pooling Model Performance Optimizations" 跟踪列表中的一项,旨在通过优化 AllPool.forward 的实现来提升 pooling 模型的性能。PR 提供的 benchmark 显示优化后 median latency 从 59.89 us 降至 39.65 us,提升约 51%。

该 PR 值得精读,其优化思路(减少 GPU-CPU 同步)具有普适性,可作为类似场景的参考。建议关注 num_scheduled_tokens_cpu 的新增属性,确保后续扩展时保持兼容。

讨论亮点

Review 评论较少,主要由 CI 自动检查(pre-commit 失败)和一些自动化 bot 评论组成。无实质性设计争议。

实现拆解

  1. 将逐个 request 的独立切片改为一次大切片 + split:原始代码对每个 request 分别执行 hidden_states[first : last + 1],每个切片都需调用 .tolist() 将 GPU tensor 同步到 CPU,导致多次设备同步。新代码先计算整个 subgroup 的范围(group_startgroup_end),只调用两次 .item() 和一次 .tolist(),然后通过 split(split_sizes) 一次完成所有 request 的子张量划分。
  2. 处理空情况:当 split_sizes 为空时,直接返回空列表,保持正确性。
  3. 依赖数据契约变更:优化依赖 PoolingCursor 新增的 num_scheduled_tokens_cpu 属性(由 DispatchPooler 提供),属于 data-contract 层面的改动。
文件 模块 状态 重要度
vllm/model_executor/layers/pooler/tokwise/methods.py 池化层 modified 6.46

关键符号

AllPool.forward

关键源码片段

vllm/model_executor/layers/pooler/tokwise/methods.py data-contract

核心优化文件,修改 AllPool.forward 方法,将逐个 request 切片改为一次大切片后 split,消除多次 GPU 同步。

def forward(
    self,
    hidden_states: torch.Tensor,
    pooling_metadata: PoolingMetadata,
) -> list[TokenPoolingMethodOutputItem]:
    pooling_cursor = pooling_metadata.get_pooling_cursor()
    # 通过 num_scheduled_tokens_cpu 一次性获取每个 request 的 token 数(CPU 列表)
    split_sizes = pooling_cursor.num_scheduled_tokens_cpu.tolist() # 只调一次 tolist
    if split_sizes:
        # DispatchPooler 传入的是完整 hidden_states tensor
        # 先计算整个 subgroup 的起始和结束位置(只调两次 .item())
        group_start = int(pooling_cursor.first_token_indices_gpu[0].item())
        group_end = int(pooling_cursor.last_token_indices_gpu[-1].item()) + 1
        # 一次切片得到整个 subgroup
        hidden_states_group = hidden_states[group_start:group_end]
        # 按 per-request token 数量 split,避免逐个 request 切片
        hidden_states_lst = list(hidden_states_group.split(split_sizes))
    else:
        hidden_states_lst = []
​
    if not self.enable_chunked_prefill:
        return hidden_states_lst
​
    # chunked_prefill 分支保持不变
    pooling_states = pooling_metadata.pooling_states
    for p, hs_chunk in zip(pooling_states, hidden_states_lst):
        p.hidden_states_cache.append(hs_chunk)
​
    output_list = list[TokenPoolingMethodOutputItem]()
    for p, finished in zip(pooling_states, pooling_cursor.is_finished()):
        if finished:
            hidden_states_cache = p.hidden_states_cache
            if len(hidden_states_cache) == 1:
                output_list.append(hidden_states_cache[0])
            else:
                output_list.append(torch.concat(hidden_states_cache, dim=0))
            p.clean()
        else:
            output_list.append(None)
    return output_list

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  • 兼容性风险:优化依赖 PoolingCursor 新增的 num_scheduled_tokens_cpu 属性,如果其他调用方未设置该属性,可能导致 AttributeError。但当前仅 DispatchPooler 使用 AllPool,风险有限。
  • 性能退化风险:当 split_sizes 为空时新代码正确返回空列表,无明显退化。
  • 逻辑正确性:新逻辑假设 first_token_indices_gpulast_token_indices_gpu 是连续的 subgroup,若后续引入非连续调用,可能出错。但当前 DispatchPooler 保证连续性。
  • 用户影响:使用 pooling 模型的用户将获得更低的延迟,特别是 batch size 较大时收益更明显。
  • 系统影响:减少 GPU 同步次数,减轻 CPU-GPU 通信压力,可能提升整体吞吐。
  • 团队影响:该 PR 是 pooling 性能优化系列的一部分,完成后有助于关闭 Issue #35631。
数据契约变更 缺少新增测试覆盖

关联 Issue

#35631 [Feature]: Pooling Model Performance Optimizations

完整报告

参与讨论