vLLM 分离式推理(Disaggregated Serving)实用指南
Taking vLLM Apart: A Practical Guide to Disaggregated Serving
vLLM 官方博客发布分离式推理实用指南,讲解 vLLM v0.30.0 及以上版本中 prefill/decode 分离、无 GPU render 前端及两者组合的原理与运行方法。
作者来自参与开发的 IBM Research,给出 P/D 与 render 拆分的收益数据、适用场景表和可复用运行方法。
单个 vllm serve 进程承担三项相互干扰的工作:处理提示(预填充)、生成 token(解码)以及围绕它们的大量 CPU 工作。vLLM 中的分离式服务将 LLM 推理的不同阶段分开。将预填充与解码分离可以避免长提示阻塞其他请求的输出,前提是 KV 缓存能在两者之间快速传输。将分词和解析移到仅 CPU 的前端(/render、/derender)可以将这些工作从 GPU 节点上卸下,让引擎纯粹以 token ID 工作。本文介绍每种拆分或分离在何时值得使用、如何在 vLLM v0.30.0 或更高版本中运行它们,以及仍待解决的关键差距和改进。
启动一个普通的 vllm serve,你会得到一个进程处理三种毫无共同点的工作负载。
当一个长提示到达时,数十个请求正处于响应中途,这些流中的每一个都会卡顿,直到它被处理完。这就是并发量一上去就会看到的尖峰。原因是两个几乎没有共同点的阶段共用一块 GPU。预填充一次性读取整个提示,受计算限制,决定了你的首 token 时间(TTFT)。解码则相反。它一次生成一个 token,制约它的是 GPU 从内存读取模型权重的速度,这决定了你的 token 间延迟(ITL)。把它们放在同一块 GPU 上,每个解码流都会在长预填充运行时等待。
然而第三项工作更安静,也更容易被忽视。聊天模板化、分词、去分词、推理解析、工具调用解析。全是纯 CPU 工作,却跑在你为了加速器而租用的机器上。
分离是显而易见的应对方式:不要让一个进程包揽所有事情。如果你想在继续之前了解 vLLM 引擎层面的全貌,Inside vLLM 介绍了调度器目前如何混合预填充和解码,并有一小节关于 P/D。
维度
vLLM 提供了多个可以拆分工作的点,并且它们可以组合使用。
预填充和解码作为两个实例运行。它们之间传递的是 KV 缓存:每个提示 token 的注意力键和值,解码在能输出任何内容之前需要这些。它很大。BF16 下的 Llama-3.1-70B 每个 token 存储 320 KiB,因此一个 10k token 的提示会交给解码约 3 GB。在 400 Gb/s 线速下,这大约是 65 ms,还不算任何开销,而且全部落在 TTFT 上。KV 连接器负责传输它,通常通过 RDMA。上游现在有十多个连接器,包括 NIXL、LMCache、Mooncake、FlexKV 和 AMD 的 MoRI-IO,还有一个 MultiConnector 可以将它们串联起来。
前端完全从 GPU 机器上移走。/render 将 OpenAI 请求转换为 token ID,引擎以 token 输入 / token 输出方式运行,/derender 将输出的 token ID 转换回带有 content、reasoning 和 tool_calls 的完整 OpenAI 响应,这些被拆分出来。最后这一段最近才落地,它完成了整个往返。
超越 P/D,还有用于多模态的编码器分离,以及用于在 MoE 模型中将注意力与 FFN 拆分的 AFD 插件。两者在模型的不同位置使用相同的思路。P/D 本身现在也覆盖了混合 SSM 模型,包括 Mamba 状态传输。
图 1。一个进程包揽一切,与同一条流水线被切成四层。
它带来什么,又付出什么代价
关键不在于峰值吞吐量。在没有延迟目标的情况下,把同一批 GPU 拆成 prefill 池和 decode 池并不一定会每秒处理更多 token。它带来的是有效吞吐量(goodput):即在请求仍能满足其 TTFT 和 ITL 目标的前提下,你能维持的请求速率。
你可以独立调节 TTFT 和 ITL。每一层使用不同的并行策略,按其运行的阶段来配置规模。Prefill 可以偏重 TP,decode 可以按 batch 来配置规模。两者的调整互不牵连。
随着负载上升,尾部延迟保持低位。一个只运行 decode 批次的 decode 实例,永远不会因为长时间的 prefill 而阻塞其数据流。分块 prefill 能让你部分达到这一效果,但合适的分块大小取决于流量,所以你最终还是要反复调参。
这是在单台机器上用两块 NVIDIA L40S GPU(48 GB,PCIe,无 NVLink)测得的:Qwen2.5-7B-Instruct,约 8k token 的提示词,256 个输出 token,每个速率 100 个泊松到达请求,前缀缓存关闭。混布方案是一个 vllm serve --data-parallel-size 2,因此两种配置都使用同样的两块 GPU。这并非最强的混布基线,因为 DP rank 之间可能互相拖累,而负载均衡器后面的两个独立副本可能会稍好一些。P/D 方案是在示例代理后面通过 NIXL 连接的一个 prefiller 和一个 decoder。
图 2。在 1 req/s 以内,两种配置的中位 ITL 均为 21–24 ms。在 0.4 req/s 时,混布方案的 p99 跃升至 169 ms,而 P/D 保持在 29 ms,且从未超过 52 ms。
在 0.4–0.6 req/s 时,中位数相当,而混布方案的 p99 约高出六倍。这是因为 8k token 的 prefill 落在一块同时也在做 decode 的 GPU 上,阻塞了其上每一条数据流,直到它们完成。P/D 将它们完全挡在 decode GPU 之外。
传输快时有效吞吐量会上升。AMD 的单节点 MoRI-IO 基准测试在一个 8-GPU MI300X 节点上以 8 req/s 运行 Qwen3-235B-A22B-FP8。100 个请求中有 73 个同时满足 1 s TTFT 和 50 ms ITL 目标,而混布服务为 100 个中 30 个。在相同硬件上,这大约是 2.4 倍的有效吞吐量。在集群规模下,llm-d 的 P/D 指南报告称,在 16 块 H200 上运行 gpt-oss-120b 时,与同样 GPU 作为聚合副本运行相比,平均端到端延迟降低约 59%,P95 降低 67%。
每个首 token 都要为传输付出代价。这些结果假设 KV 缓存传输很快:节点间通过 InfiniBand 或 RoCE 上的 RDMA,单节点内通过 NVLink 或 GPU 点对点。我们的测试机两者都没有。这对 L40S 无法进行点对点复制(nvidia-smi topo -p2p r 报告 NS),每个 8k 提示词约 470 MB 的 KV 缓存(Qwen2.5-7B 每 token 56 KiB)需要约 1.3 s 才能到达 decode。在 0.2 req/s 时,P/D 的中位 TTFT 为 2.2 s,而混布为 0.7 s,因此它在每个速率下都未达到 2 s 的 TTFT 目标,尽管其 ITL 尾部保持平稳。仅凭带宽无法解释 1.3 s。即使通过主机内存中转,PCIe 4.0 也应该在几十毫秒内完成传输。大部分时间花在复制周边的开销上,包括 decode 只有在其自身前向步骤之间轮询时才会注意到传输已完成。
所以在做任何其他基准测试之前,先检查传输。在单台机器上,nvidia-smi topo -p2p r 应显示你的 prefill 和 decode GPU 之间为 OK。然后一次发送几个长提示词,读取 decode 的 KV Transfer metrics 行。如果 Avg xfer time 在数百毫秒量级,先解决这个问题。
CPU 层很便宜。一旦分词和解析从 GPU 机器上移走,你就可以按 CPU 负载来扩展它们,而不必购买加速器时间来运行分词器。长提示词、多模态预处理以及推理或工具解析才是渲染层真正干活的地方,而这些都不需要 GPU。在同一台机器上,为 Qwen2.5-7B 模板化并分词一个 9k token 的聊天提示词,大约消耗 15 ms 的 CPU。一台使用默认设置的渲染服务器仅用略多于一个核心就达到了 73 req/s 的上限。而在 collocated 的尾部性能崩溃的 0.4 req/s 下,渲染占用不到一个核心的 1%。
代价是:你现在要运维三到四个服务而不是一个,而且 KV 传输是一个新的故障模式。对许多部署来说,collocated 仍然是正确的答案。
| 你的情况 | 建议 |
|---|---|
| 在生产负载下 ITL p99 未达到你的 SLO | 解耦。这是主要用例。 |
| 高并发下的长提示词 | 如果 KV 传输很快,就解耦。这里的 prefill 干扰最严重。 |
| 在增长的上下文上进行聊天或 agent 循环 | 解耦,并使用双向传输(见下文)。将其与 KV 卸载或共享 KV 缓存(如 LMCache 或 Mooncake)配合使用。 |
| 模板化、分词或解析出现在你 GPU 节点的 CPU 剖析中 | 拆分出渲染层。 |
| TTFT 是约束瓶颈 | 保持 collocated 或先测量。传输会落在每一个首 token 上。 |
你的 KV 传输很慢(检查 decode 的 KV Transfer metrics) | 修复它,或保持 collocated。它会影响 TTFT 并限制吞吐量。也要检查网络结构,因为配置错误的网络通常也会拖慢集合通信。 |
| 低流量、突发性或对延迟不敏感的流量 | 保持 collocated。 |
运行 Prefill/Decode
从这里开始的一切都假设使用 vLLM v0.30.0 或更高版本。示例使用 Qwen3-0.6B,因为它加载快。这对于检查接线没问题,但它太小了,无法展示 P/D 的收益,所以请用更大的模型进行基准测试(参见 从哪里开始)。
三个进程:prefiller、decoder、proxy。
# Prefiller on GPU 0
CUDA_VISIBLE_DEVICES=0 UCX_NET_DEVICES=all VLLM_NIXL_SIDE_CHANNEL_PORT=5600 \
vllm serve Qwen/Qwen3-0.6B --port 8100 \
--kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_producer"}'
# Decoder on GPU 1
CUDA_VISIBLE_DEVICES=1 UCX_NET_DEVICES=all VLLM_NIXL_SIDE_CHANNEL_PORT=5601 \
vllm serve Qwen/Qwen3-0.6B --port 8200 \
--kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_consumer"}'
# Proxy
python tests/v1/kv_connector/nixl_integration/toy_proxy_server.py --port 8192 \
--prefiller-hosts localhost --prefiller-ports 8100 \
--decoder-hosts localhost --decoder-ports 8200proxy 是将它们连接在一起的关键。对于每个请求,它首先调用 prefill,带上 max_tokens=1 和 kv_transfer_params: {"do_remote_decode": true}。Prefill 计算 KV 缓存,持有这些块并返回指向它们的 kv_transfer_params。然后 proxy 将原始请求转发给 decode,并附带这些参数,decode 在生成之前通过 NIXL 拉取这些块。
将你的客户端指向 8192,它看起来就像任何其他 OpenAI 端点。tests/ 中的 proxy 是一个示例,适合开发但不适合生产。对于生产环境,请查看 llm-d 或 Dynamo。
有三个设置值得尽早了解。VLLM_NIXL_SIDE_CHANNEL_PORT 在主机上每个 worker 必须唯一。kv_lease_duration(在 kv_connector_extra_config 中设置,默认 30 秒)控制 prefiller 在等待 decoder 收集块时持有它们的时间。在负载下,这就是你将调整的超时。而 kv_load_failure_policy 决定传输失败时会发生什么:fail(默认值)会让请求报错,而 recompute 会让 decode 自己重新计算缺失的 KV。更慢,但请求能存活下来。
如果你在 /v1/chat/completions 上运行 P/D,有一个简单的优化:prefill 阶段已经对提示词进行了分词,所以 decode 阶段不需要再做一次。在 prefill 上请求 return_token_ids,然后通过 kv_transfer_params 将 ID 传给 decode。这与 proxy 进行的两次调用相同,token ID 是唯一新增的部分:
from openai import OpenAI
MODEL = "Qwen/Qwen3-0.6B"
messages = [{"role": "user", "content": "What is 17 * 23?"}]
prefill_client = OpenAI(base_url="http://localhost:8100/v1", api_key="EMPTY")
decode_client = OpenAI(base_url="http://localhost:8200/v1", api_key="EMPTY")
prefill = prefill_client.chat.completions.create(model=MODEL, messages=messages, max_tokens=1,
extra_body={"return_token_ids": True, "kv_transfer_params": {"do_remote_decode": True}})
# prefill's kv_transfer_params point decode at its blocks; add the token IDs on top
decode = decode_client.chat.completions.create(model=MODEL, messages=messages, stream=True,
extra_body={"kv_transfer_params": {**prefill.kv_transfer_params,
"prompt_token_ids": prefill.prompt_token_ids}})一旦它能工作,确定规模就是最难的部分。Qwen3.8-2.4T PD 文章 介绍了如何从 KV 缓存容量出发选择 prefill 和 decode 拓扑。
多轮对话:停止重新计算对话
标准的 P/D 将缓存单向移动,这对聊天和 agent 循环来说是浪费的。在第二轮时,decode 仍然持有它刚刚生成的答案的 KV,但 prefill 从未计算过它,因此 prefill 会重新计算它。在两个实例上都设置 bidirectional_kv_xfer 后,prefill 会改为从 decode 拉回这些块,并且只计算新的 token。一个代理会跟踪哪些块属于哪个对话,以客户端发送的 conversation_id 作为键。双向 KV 传输文章深入介绍了设置、调优和基准测试。
对推理模型的一个警告。Decode 的块包含它生成的思考轨迹。如果下一轮的 prompt 丢弃了它们,就像 Qwen3 的聊天模板自己会做的那样,prefill 的 prompt 就不再与 decode 的块对齐,你会得到错误的输出,而不仅仅是慢输出。目前 vLLM 中没有任何东西能捕获这种不匹配(#43094),所以在开启此功能之前请检查你的聊天模板。
运行无 GPU 前端
将 CPU 工作从 GPU 机器上移走只是其中一部分。一旦引擎接收 token ID 并输出 token ID,它就不再需要聊天模板或解析器,调用方也能看到并使用这些 token ID。路由器可以渲染一个请求,然后根据真实 prompt token 的前缀缓存命中来选择副本,而不是从文本中猜测。RL 和评估流水线能获得模型看到和产生的确切 ID,这很重要,因为解码后的文本并不总是以相同方式重新分词。
多模态预处理也会迁移。图像解码和模型的处理器在渲染层运行,并且通过 编码器解耦,渲染层将处理后的张量发送给编码器,只将元数据发送给 prefill。处理后的负载可能比源图像大得多,因此请据此设置请求限制的大小。
两个服务器:一个没有 GPU 的渲染层,和一个只处理 token 的引擎。解析器放在渲染服务器上,因为 derender 在那里运行它们。
vllm launch render Qwen/Qwen3-0.6B --port 8100 \
--reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser hermes
vllm serve Qwen/Qwen3-0.6B --tokens-only --port 8200这两种模式都会始终暴露其横向扩展端点。在常规的 vllm serve 上,它们默认是关闭的。如果你想要在同时接受正常 OpenAI 流量的服务器上启用 /render、/derender 或 /inference/v1/generate,请添加 --enable-scale-out。
然后是三跳——渲染、生成、反渲染:
import httpx
MODEL = "Qwen/Qwen3-0.6B"
RENDER = "http://localhost:8100" # vllm launch render
ENGINE = "http://localhost:8200" # vllm serve --tokens-only
chat_request = {
"model": MODEL,
"messages": [{"role": "user", "content": "What is 17 * 23?"}],
"max_tokens": 2048,
}
with httpx.Client(timeout=60.0) as client:
# 1. request -> token IDs (no GPU)
generate_request = client.post(f"{RENDER}/v1/chat/completions/render", json=chat_request).json()
# 2. token IDs -> token IDs (GPU)
generate_response = client.post(f"{ENGINE}/inference/v1/generate", json=generate_request).json()
# 3. token IDs -> ChatCompletionResponse (no GPU)
response = client.post(f"{RENDER}/v1/chat/completions/derender", json={
"model": MODEL,
"generate_response": generate_response,
"prompt_tokens": len(generate_request["token_ids"]),
"chat_request": chat_request,
}).json()
message = response["choices"][0]["message"]
print(message["reasoning"], message["content"], sep="\n---\n")将 chat_request 传回 derender 步骤。解析器需要原始上下文(工具、tool_choice、include_reasoning)来产生与普通 vllm serve 相同的 content / reasoning / tool_calls 拆分。在配置了解析器的模型上,省略它会导致 400,而不是静默回退,让 <tool_call> 标记泄漏到 content 中。
流式传输
两个端点也都接受 stream: true。/render 在它返回的请求上保留 stream,因此 generate 也会流式传输,并且每个 generate 块都会自行经过 derender。继续上面的例子:
import json
chat_request = {**chat_request, "stream": True}
with httpx.Client(timeout=60.0) as client:
generate_request = client.post(f"{RENDER}/v1/chat/completions/render", json=chat_request).json()
prompt_token_ids = generate_request["token_ids"]
stream_state = None # one per choice index if n > 1
with client.stream("POST", f"{ENGINE}/inference/v1/generate", json=generate_request) as stream:
for line in stream.iter_lines():
if not line.startswith("data: ") or line == "data: [DONE]":
continue
derendered = client.post(f"{RENDER}/v1/chat/completions/derender", json={
"stream": True,
"model": MODEL,
"generate_chunk": json.loads(line[len("data: "):]),
"stream_state": stream_state,
"prompt_tokens": len(prompt_token_ids),
"prompt_token_ids": prompt_token_ids,
"chat_request": chat_request,
}).json()
stream_state = derendered["stream_state"]
for choice in derendered["chunk"]["choices"]:
print(choice["delta"].get("content") or "", end="", flush=True)在第一次调用时以 null 发送 stream_state,并回显上一次响应给你的任何内容。chat_request 和 prompt_token_ids 随每个块一起发送,因为服务器在调用之间不保留任何内容。因此,任何渲染副本都可以处理任何块。一旦涉及解析器,这也是成本的来源(参见 还有什么剩下)。
部署渲染层
渲染层不持有任何按请求的状态,即使在流式传输时也是如此,因此它可以像任何无状态 Web 服务一样扩展。将副本放在普通负载均衡器后面,并根据 CPU 独立于 GPU 池进行扩展。
保持配置同步。渲染、反渲染和引擎必须在模型、分词器、聊天模板和解析器标志(--chat-template、--default-chat-template-kwargs、--reasoning-parser、--tool-call-parser、--enable-auto-tool-choice)上保持一致。不匹配不会引发错误。你只会得到与 vllm serve 会返回的不同的 content / reasoning / tool_calls 拆分。
然后调整线程池大小。模板化、分词、多模态预处理和流式解析器重放都在渲染器的工作线程上运行,由 --renderer-num-workers 设置,默认值为 1。普通渲染开销很低(上述 9k token 的提示词大约消耗 15 毫秒 CPU)。通过解析器流式传输推理或工具调用模型的成本要高得多,因此请提高工作线程数或增加副本,以匹配你并发的已解析流。
将两种拆分结合起来
图 1 同时展示了两种拆分:四个层级,但只有三台服务器,因为一台 vllm launch render 服务器同时处理 /render 和 /derender。目前还没有上游组件为你驱动这四跳,但这些部分可以组合起来,因为 /inference/v1/generate 接受 kv_transfer_params,就像 /v1/chat/completions 一样。
# Render and derender, no GPU
vllm launch render Qwen/Qwen3-0.6B --port 8000 \
--reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser hermes
# Prefill on GPU 0
CUDA_VISIBLE_DEVICES=0 UCX_NET_DEVICES=all VLLM_NIXL_SIDE_CHANNEL_PORT=5600 \
vllm serve Qwen/Qwen3-0.6B --tokens-only --port 8100 \
--kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_producer"}'
# Decode on GPU 1
CUDA_VISIBLE_DEVICES=1 UCX_NET_DEVICES=all VLLM_NIXL_SIDE_CHANNEL_PORT=5601 \
vllm serve Qwen/Qwen3-0.6B --tokens-only --port 8200 \
--kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_consumer"}'然后你的代码在渲染和去渲染之间扮演代理的角色:
import httpx
MODEL = "Qwen/Qwen3-0.6B"
RENDER, PREFILL, DECODE = "http://localhost:8000", "http://localhost:8100", "http://localhost:8200"
chat_request = {
"model": MODEL,
"messages": [{"role": "user", "content": "What is 17 * 23?"}],
"max_tokens": 2048,
}
with httpx.Client(timeout=60.0) as client:
# 1. Render: request -> token IDs (no GPU)
generate_request = client.post(f"{RENDER}/v1/chat/completions/render", json=chat_request).json()
# 2. Prefill: compute the KV cache, generate one token, hold the blocks
prefill_response = client.post(f"{PREFILL}/inference/v1/generate", json={
**generate_request,
"sampling_params": {**generate_request["sampling_params"], "max_tokens": 1},
"kv_transfer_params": {"do_remote_decode": True},
}).json()
# 3. Decode: pull prefill's blocks over NIXL, then generate
generate_response = client.post(f"{DECODE}/inference/v1/generate", json={
**generate_request,
"kv_transfer_params": prefill_response["kv_transfer_params"],
}).json()
# 4. Derender: token IDs -> ChatCompletionResponse (no GPU)
response = client.post(f"{RENDER}/v1/chat/completions/derender", json={
"model": MODEL,
"generate_response": generate_response,
"prompt_tokens": len(generate_request["token_ids"]),
"chat_request": chat_request,
}).json()
message = response["choices"][0]["message"]
print(message["reasoning"], message["content"], sep="\n---\n")第 2 步与代理使用的技巧相同:一个 token 的预算加上 do_remote_decode,这样预填充会计算 KV 缓存并保留这些块。第 3 步将解码交给 kv_transfer_params 预填充返回的、指向这些块的句柄。示例代理只处理 /v1/completions 和 /v1/chat/completions,所以在生产环境中,这种编排需要你自己编写。这也是 llm-d 和 Dynamo 的工作,渲染器文档将它们列为其预期调用方。
谁在实际运行它
llm-d 使用 vLLM 原生的远程预填充/解码和 NIXL KV 传输能力。llm-d 的路由器和 EPP 调度器选择预填充/解码端点,并编排它们之间的 KV 缓存交接。NVIDIA Dynamo 在 Kubernetes 上以聚合或分离模式运行 vLLM,并带有自己的路由器和规划器。KServe 通过 LLMInferenceService 暴露它,构建在 llm-d 之上。
vLLM 生产栈通过 Helm 提供分离式预填充,AIBrix 覆盖控制平面侧。在连接器方面,Moonshot 的 Mooncake、LMCache 和 AMD 的 MoRI-IO 都是上游项目。而且由于解码位于连接器之后,它甚至不必是 vLLM:TileRT 将原版 vLLM 预填充与它自己的延迟优化解码引擎配对使用。
还有什么待完成?
使用解析器进行流式去渲染开销很大。推理和工具解析器持有的状态无法序列化,因此每个块都会重建一个新的解析器,并通过它重放 token 历史。传输是每个块 O(n),重放是每个块 O(n) 次解析调用,而对于 Hermes 和 DeepSeek-R1 等解析器,parse_delta 本身会重新扫描累积的文本。在长生成过程中,这是 O(n³) 的字符工作量。对数千 token 的推理输出进行的基准测试测得,在匹配负载下,进程内 CPU 大约为 9 倍,E2E p50 增加 15%。缓存层已列入计划(#57571)。在它落地之前,保持一个工作线程的渲染层在并发的已解析流下会饱和,因此请按上述方式调整其大小。
长提示词主导了网络传输。解析器路径在每个块上都会完整重发 prompt_token_ids。一个 100k token 的提示词加上 1k token 的输出,意味着提示词约占每个请求体的 99%。不使用解析器的普通去分词没有这个问题。
一些小的正确性缺口正在处理中。批量去渲染在解析工具调用时并不总是返回 finish_reason: "tool_calls"(修复待定于 #47931),而且它会自己生成工具调用 ID,而不是保留解析器的。Logprobs 从分词器免费引擎返回为 "token_id:N" 占位符字符串,这能用但不理想。#57574 提议返回真正的整数 token ID。
还有一个设计问题尚未确定。#56851 询问 /inference/v1/generate 是否应直接返回文本或去渲染输出,从而为不需要的调用方跳过第三跳。
总括性的 RFC 有 #42729(去分词批处理)、#47161(去分词流式)、#22817(tokens-in, tokens-out)和 #34407(分离式前端)。请对这些 RFC 发表意见,让我们知道你的想法。
从哪里开始
如果你有两块 GPU,用 7B 级模型运行上面的 NIXL 示例。Qwen3-0.6B 的预填充速度非常快,几乎不会干扰解码,所以 P/D 没有什么需要修复的。如前所述,先检查传输。然后在同样的两块 GPU 上,对 8192 上的代理和单个同置 vllm serve --data-parallel-size 2 运行相同的基准测试,并比较 p99 ITL、TTFT 和有效吞吐量。逐步提高 --request-rate,直到同置服务器的 p99 ITL 崩溃。在测试机上使用 8k 提示时,该值为 0.4 req/s。如果你在不同速率下复用 --seed,请用 --no-enable-prefix-caching 启动每个服务器。否则,后续速率会重放服务器已缓存的提示,而缓存命中会恰好掩盖你试图测量的预填充干扰。
vllm bench serve --model Qwen/Qwen2.5-7B-Instruct --port 8192 \
--dataset-name random --random-input-len 8192 --random-output-len 256 --ignore-eos \
--request-rate 0.4 --num-prompts 100 \
--percentile-metrics ttft,tpot,itl --metric-percentiles 50,99 \
--goodput ttft:2000 tpot:30服务聊天还是智能体?打开 bidirectional_kv_xfer,并测量第五轮的 TTFT,而不是第一轮。
如果你已经在使用 Kubernetes,请从 llm-d 或 Dynamo 开始,而不是自己构建代理。而且,如果你通过流式去渲染运行推理或工具调用模型,请在确定渲染层的规模之前对其进行基准测试——这是最有可能让你感到意外的地方。
文档:分离式预填充、NixlConnector、渲染器、去渲染器。
致谢
感谢 @aoshen02、Bongwoo Bak、Chauncey、Guan-Ming Chiu、Hyunkyun Moon、Konstantin Dunas、Maroon Ayoub、Nick Hill、Nicolò Lucchesi、Nithin Chalapathi、Robert Shaw、Sagi Ahrac、Seiji Eicher、@snadampal、Will Eaton、Yuqi Wang 和 Yuge Zhang,以及 vLLM 社区中的其他人,感谢他们帮助实现分离式服务。
来源:vLLM 官方博客(RSS) · vllm.ai