Prhub

#55088 [Bugfix][Examples] Launch prefill and decode concurrently in NixlPushConnector demo

原始 PR 作者 junuxyz 合并时间 2026-09-12 14:22 文件变更 1 提交数 5 评论 7 代码增减 +18 / -10

执行摘要

NixlPushConnector demo 的 prefill 与 decode 改为并发启动

PR body 明确指出:旧实现的 async for 循环必须完整消费 prefill 响应后,执行流才会到达 decode 请求,因此两个上游请求只能串行运行。其本质原因是没有把“排空 prefill 连接”当成可并发的后台任务。作者用 trace 脚本对比时间线验证:修复前 decode_start 出现在 +143.968ms(紧跟在 prefill_end +128.740ms 之后),修复后 decode_start 提前到 +0.863ms,与 prefill 同步启动;对 KV 分离部署而言,P/D 并行是 NIXL 推送模式正常工作的前提,串行会人为放大端到端延迟。

值得花 10 分钟快速阅读,重点不在变更本身而在两个模式:其一,用生成器的 finally 回收后台 asyncio 任务(await prefill_task),保证流式响应生命周期与资源排空绑定,这是 Python 异步服务端编程中常见且易错的细节;其二,PR body 附带的 trace 时间线对比方法论(在 forward_request 上打点 + vllm bench 自动化对比)对任何代理类调试都有参考价值。不建议将其作为核心引擎并发逻辑的参照——它只是示例层修复,且没有自动化测试护航。

讨论亮点

该 PR 的 review 过程没有产生实质技术讨论:claude[bot] 因 PR 来自 fork 而跳过自动 review;NickLucche 直接 APPROVED(原话 “ty @junuxyz”)。Issue 评论均为机器人通知(mergify 文档预览、两次 Buildkite CI 触发 #87731 与 #88481)。唯一值得留意的设计取舍并未在讨论中出现:将 await prefill_task 放在 stream_decode 的 finally 中,实际上把 prefill 后台任务的异常暴露时点推迟到了 decode 流收尾阶段,这是一个值得后续维护者注意的隐含语义变化。

实现拆解

  1. 变更入口与范围:唯一变更文件为 examples/disaggregated/disaggregated_serving/disagg_proxy_pushconnector_demo.py,新增 import asyncio,核心改动集中在 PushProxy._push_completion 方法中发起 prefill 与 decode 两路请求的段落。
  2. 排空逻辑封装:将原先散落在方法体中的 async for 排空循环提取为内层函数 drain_prefill(),语义与旧代码保持一致——不解析响应体,仅消费流直到上游 prefill 引擎释放请求槽位。这样排空逻辑变成一个可独立调度、可被引用的协程对象。
  3. 并发编排:通过 prefill_task = asyncio.create_task(drain_prefill()) 将排空任务放入事件循环后台调度,紧接着定义 stream_decode() 异步生成器并立即 return StreamingResponse(stream_decode(), ...)。decode 请求不再等待 prefill 排空,两个上游请求在同一个事件循环内并发执行。
  4. 资源回收保障:stream_decode() 的 finally 分支中 await prefill_task,保证无论 decode 流正常走到结束、还是客户端中途断开触发 GeneratorExit,prefill 连接最终都会被排干,维持旧实现“上游 P 槽位最终被释放”的保证。这也使后台任务的生命周期与 HTTP 响应流绑定,避免任务泄漏。
  5. 测试配套:没有新增自动化测试文件;作者提供了完整的可复现对比脚本(trace_proxy.py 在 forward_request 上打点 + vllm bench 触发 + before/after 时间线对比),并在 PR body 中贴出 2 × A100 上的实测数据。
文件 模块 状态 重要度
examples/disaggregated/disaggregated_serving/disagg_proxy_pushconnector_demo.py 示例代理 modified 6.48

关键符号

drain_prefill stream_decode _push_completion

关键源码片段

examples/disaggregated/disaggregated_serving/disagg_proxy_pushconnector_demo.py core-logic

唯一的变更文件,修复 NIXL push 代理 demo 中 prefill 与 decode 严格串行的问题:旧代码先完整消费 prefill 响应再发起 decode,新代码用 asyncio 后台任务排空 prefill 并立即流式转发 decode。这是本 PR 的全部实现载体,直接决定示例的并发行为与资源回收语义。

# 关键改动集中在 PushProxy._push_completion 方法中:
# 旧实现先串行排空 prefill 响应(实测约 128ms),decode 在其后才启动,
# 新实现让两者并发执行。
​
# 排空逻辑:不解析响应体,仅消费流,使上游 prefill 引擎能释放请求槽位
async def drain_prefill():
    async for _ in self.forward_request(
        f"http://{prefill_instance}{path}", prefill_request, headers
    ):
        continue
​
# create_task 立即返回,decode 请求无需等待 prefill 排空
prefill_task = asyncio.create_task(drain_prefill())
​
async def stream_decode():
    try:
        async for chunk in self.forward_request(
            f"http://{decode_instance}{path}", decode_request, headers
        ):
            yield chunk
    finally:
        # 流结束(正常完成或客户端断开)时确保 prefill 连接被排干,
        # 避免上游 P 侧请求槽位泄漏;此处 await 会把 prefill 任务
        # 的异常延迟到 decode 流收尾时才暴露
        await prefill_task
​
# StreamingResponse 立刻返回,decode 流边生成边推给客户端
return StreamingResponse(stream_decode(), media_type="application/json")

评论区精华

fork PR 的 review 流程与 CI 验证 other

claude[bot] 指出该 PR 来自 fork,自动 review 被禁用;作者请求 NickLucche 查看,随后通过 `/ci run` 触发两次 Buildkite CI(#87731、#88481)。Issue 评论中只有 mergify 的文档预览与 CI 机器人通知,没有对实现本身的技术性讨论。

结论:NickLucche 直接 APPROVED 并以 “ty @junuxyz” 收尾,PR 在 CI 通过后合并,未留下未解决的疑虑。 · 已解决

风险与影响

1) 后台任务异常的可观测性下降:prefill 请求失败不再在 _push_completion 内立即抛出,而是延迟到 stream_decode 的 finally: await prefill_task 中暴露;此时流式响应可能已经向客户端发出了部分数据,错误只能通过中断 SSE 流来体现,外层 create_completion 的 try/except 无法捕获生成器内部的异步异常。
2) 响应收尾可能被挂起的 prefill 阻塞:若上游 prefill 引擎卡死,await prefill_task 会一直等待直到 aiohttp 的 6 小时超时(AIOHTTP_TIMEOUT),客户端断开后的资源回收同样会延迟。
3) 客户端提前断开场景:CancelledError/GeneratorExit 触发 finally 后仍需等待 prefill 排空,行为与旧的串行实现一致,但引入了 asyncio 任务取消语义(如 “Task was destroyed but it is pending” 告警风险)。
4) 无自动化测试:该 demo 不在任何单测覆盖内,回归只能依赖人工复现脚本。

影响范围仅限 examples/ 目录下的演示脚本,核心库零改动,不影响线上用户。对使用该 demo 进行 KV 分离(P/D 分离)验证的用户,首 token 前的代理转发延迟降低约 140ms 量级,且并发行为更接近真实生产代理。对团队而言,该改动为 kv-connector 示例提供了一个“后台排空 + 流式转发”的 asyncio 并发范式,可复用到 pull-mode demo 或其他代理类示例;但要注意示例文件不受任何自动化测试覆盖,行为回归只能靠人工复现脚本发现。

示例代码无自动化测试 后台任务异常延迟暴露 流结束依赖 prefill 排空 客户端断开时任务取消语义

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论