Prhub

#56760 [Bugfix] Measure complete pooling responses

原始 PR 作者 taneem-ibrahim 合并时间 2026-09-15 21:21 文件变更 1 提交数 4 评论 9 代码增减 +15 / -1

执行摘要

Pooling 基准修复:度量完整响应体并支持 bytes_only

PR body 明确列出三个问题:Pooling benchmarks 在响应 headers 到达时就记录 E2E 延迟,JSON 响应在解析时被消费,但二进制 bytes 响应从未被消费,因此报告的延迟不包含响应传输时间,且连接可能在服务端仍在写时被关闭;新增的 bytes_only 格式被当作 JSON 处理,每个请求都失败(报错 Attempt to decode JSON with unexpected mimetype: application/octet-stream);用确定性延迟服务器实测,main 分支 bytes 格式报告 latency 为 0.0005 秒而实际 wall 时间为 0.25 秒以上,证明测量完全失真。

值得精读的小型 bugfix。它清晰展示了异步 HTTP 客户端中“延迟测量点”与“响应体消费”之间常见的陷阱,iter_any() 增量消费避免内存膨胀的做法有借鉴价值;bytes_only 无 metadata 时回退客户端估算的取舍也可作为后续协议设计的参考。适合 benchmark 工具维护者和性能团队阅读。

讨论亮点

本 PR 没有产生技术性 review 讨论。claude[bot] 因该 PR 来自 fork 而禁用自动评审;维护者 yewentao256 直接批准,评论为“LGTM, thanks for the work!”,无未解决疑虑。PR body 中作者对 bytes_only 的 prompt token 为客户端估算这一权衡做了明确说明,属于有意的设计取舍。

实现拆解

变更集中在 vllm/benchmarks/lib/endpoint_request_func.py 的 _run_pooling_request,并按以下步骤推进:

  1. 调整延迟测量点:把 output.ttft = output.latency = time.perf_counter() - st 从“响应 headers 到达后立即记录”移动到“响应 body 消费完成后记录”,使延迟真正覆盖端到端传输。
  2. 按 encoding_format 分策略消费 body:bytes / bytes_only 使用 async for _ in response.content.iter_any(): pass 增量读取,避免把 embedding 大数组留在内存中;float 等其他格式使用 await response.read() 完整读入后再解析。
  3. token 用量来源三分支:bytes 从响应 metadata header 读取 usage;bytes_only 因该格式刻意不携带 metadata,回退为客户端侧 prompt_len;float 从 JSON body 读取 usage。
  4. 调用链配套透传:async_request_openai_embeddings、async_request_vllm_rerank、async_request_openai_embeddings_chat、async_request_infinity_embeddings、async_request_vllm_pooling 五个入口均新增 prompt_len=request_func_input.prompt_len 参数传入。
  5. 测试配套:本 PR 未新增测试文件,作者用既有 tests/benchmarks/test_endpoint_request_func_timing.py(18 passed)加 MRV2 E2E 冒烟验证(/v1/embeddings 与 /pooling 三种格式共 128 请求全部通过)。
文件 模块 状态 重要度
vllm/benchmarks/lib/endpoint_request_func.py 基准工具 modified 5.66

关键符号

_run_pooling_request async_request_openai_embeddings async_request_vllm_rerank async_request_openai_embeddings_chat async_request_infinity_embeddings async_request_vllm_pooling

关键源码片段

vllm/benchmarks/lib/endpoint_request_func.py core-logic

唯一变更文件,修复 pooling 基准的延迟度量点并支持 bytes_only 格式,是整个 PR 的核心。

async def _run_pooling_request(
    session: aiohttp.ClientSession,
    api_url: str,
    payload: dict[str, Any],
    headers: dict[str, Any],
    pbar: tqdm | None = None,
    num_input_sequences: int = 1,
    prompt_len: int = 0,
) -> RequestFuncOutput:
    """发送 pooling 类请求并记录端到端延迟。
​
    修复前延迟在响应 headers 到达时即被记录,二进制 body 从未被消费,
    导致延迟被低估且连接可能在服务端仍在写响应时被关闭。
    """
    output = RequestFuncOutput(num_input_sequences=num_input_sequences)
    st = time.perf_counter()
    output.start_time = st
    try:
        async with session.post(url=api_url, headers=headers, json=payload) as response:
            if response.status == 200:
                encoding_format = payload.get('encoding_format', 'float')
                if encoding_format in ('bytes', 'bytes_only'):
                    # 二进制 embedding 体通常较大,增量消费但不保留数据在内存
                    async for _ in response.content.iter_any():
                        pass
                else:
                    # JSON 体相对较小,直接完整读入再解析
                    await response.read()
                # 延迟必须在 body 消费完成后记录,才是真正的端到端延迟
                output.ttft = output.latency = time.perf_counter() - st
​
                if encoding_format == 'bytes':
                    # bytes 格式的 token 用量来自响应 metadata header
                    metadata = json.loads(response.headers['metadata'])
                    usage = metadata.get('usage', {})
                elif encoding_format == 'bytes_only':
                    # bytes_only 刻意不携带 metadata,回退到客户端侧 prompt 长度
                    usage = {'prompt_tokens': prompt_len}
                else:
                    data = await response.json()
                    usage = data.get('usage', {})
​
                output.success = True
                output.generated_text = ''
                output.prompt_len = usage.get('prompt_tokens', 0)
            else:
                output.success = False
                output.error = response.reason or ''
    except Exception as e:
        output.success = False
        output.error = str(e)
​
    if pbar:
        pbar.update(1)
    return output

评论区精华

维护者审批 other

claude[bot] 因 PR 来自 fork 而禁用自动评审;维护者 yewentao256 直接批准。

结论:无追加修改即合并。 · 已解决

风险与影响

  1. 无新增自动化测试:修复依赖既有 18 个用例与作者手工 E2E 冒烟,回归保护较弱。
  2. bytes_only token 统计偏差:prompt_len 来自客户端侧估算,与 tokenizer 真实统计可能有出入,但作者声明不影响请求成功与延迟记录。
  3. benchmark 请求将等待完整 body:若服务端响应 body 迟迟不发,benchmark 请求会挂住(原实现会提前关闭连接),这是端到端延迟测量的正确语义,但改变了挂起时的失败模式。
  4. 影响面可控:变更只落在 vllm/benchmarks/lib/endpoint_request_func.py,不触及生产 serving 代码路径。

影响范围为性能团队与 benchmark 使用者:pooling / embedding 类基准的 E2E 延迟、成功率与 token 统计从此反映完整响应传输,bytes_only 格式可正常跑通。对生产服务、API 用户无直接影响;对依赖 benchmark 数据做性能对比的团队而言,修复后数据可信度显著提升,但历史基线数据与修复后数据不可直接对比。

无新增测试覆盖 bytes_only 延迟统计为客户端估算 benchmark 请求将等待完整 body

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论