执行摘要
- 一句话:NixlPushConnector demo 的 prefill 与 decode 改为并发启动
- 推荐动作:值得花 10 分钟快速阅读,重点不在变更本身而在两个模式:其一,用生成器的
finally 回收后台 asyncio 任务(await prefill_task),保证流式响应生命周期与资源排空绑定,这是 Python 异步服务端编程中常见且易错的细节;其二,PR body 附带的 trace 时间线对比方法论(在 forward_request 上打点 + vllm bench 自动化对比)对任何代理类调试都有参考价值。不建议将其作为核心引擎并发逻辑的参照——它只是示例层修复,且没有自动化测试护航。
功能与动机
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 推送模式正常工作的前提,串行会人为放大端到端延迟。
实现拆解
- 变更入口与范围:唯一变更文件为
examples/disaggregated/disaggregated_serving/disagg_proxy_pushconnector_demo.py,新增 import asyncio,核心改动集中在 PushProxy._push_completion 方法中发起 prefill 与 decode 两路请求的段落。
- 排空逻辑封装:将原先散落在方法体中的
async for 排空循环提取为内层函数 drain_prefill(),语义与旧代码保持一致——不解析响应体,仅消费流直到上游 prefill 引擎释放请求槽位。这样排空逻辑变成一个可独立调度、可被引用的协程对象。
- 并发编排:通过
prefill_task = asyncio.create_task(drain_prefill()) 将排空任务放入事件循环后台调度,紧接着定义 stream_decode() 异步生成器并立即 return StreamingResponse(stream_decode(), ...)。decode 请求不再等待 prefill 排空,两个上游请求在同一个事件循环内并发执行。
- 资源回收保障:
stream_decode() 的 finally 分支中 await prefill_task,保证无论 decode 流正常走到结束、还是客户端中途断开触发 GeneratorExit,prefill 连接最终都会被排干,维持旧实现“上游 P 槽位最终被释放”的保证。这也使后台任务的生命周期与 HTTP 响应流绑定,避免任务泄漏。
- 测试配套:没有新增自动化测试文件;作者提供了完整的可复现对比脚本(
trace_proxy.py 在 forward_request 上打点 + vllm bench 触发 + before/after 时间线对比),并在 PR body 中贴出 2 × A100 上的实测数据。
关键文件:
examples/disaggregated/disaggregated_serving/disagg_proxy_pushconnector_demo.py(模块 示例代理;类别 source;类型 core-logic;符号 drain_prefill, stream_decode): 唯一的变更文件,修复 NIXL push 代理 demo 中 prefill 与 decode 严格串行的问题:旧代码先完整消费 prefill 响应再发起 decode,新代码用 asyncio 后台任务排空 prefill 并立即流式转发 decode。这是本 PR 的全部实现载体,直接决定示例的并发行为与资源回收语义。
关键符号:drain_prefill, stream_decode, _push_completion
关键源码片段
examples/disaggregated/disaggregated_serving/disagg_proxy_pushconnector_demo.py
唯一的变更文件,修复 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")
评论区精华
该 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 流收尾阶段,这是一个值得后续维护者注意的隐含语义变化。
- fork PR 的 review 流程与 CI 验证 (other): 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 排空, 客户端断开时任务取消语义
关联脉络
- PR #54689 [Bugfix][NIXL] Don't evict a remote engine a transfer is still reading from: 同属 NIXL KV 传输路径的稳定性修复,关注传输期间引擎生命周期与并发安全问题,与本 PR 的并发时序修复处于同一功能线。
- PR #55424 [Bugfix][KV Connector] Only enforce disk block alignment for O_DIRECT: 同属 KV Connector 的边界条件 bugfix 线,说明 connector 层在持续收敛启动与运行期的异常问题,本 PR 是对该模块示例代码的配套修正。
参与讨论