发表时间: 2026-09 · Blog post by Martin Hickey (IBM Research) (blog.vllm.ai)
原文: https://blog.vllm.ai/blog/2026-09-29-disaggregated-serving-guide
Martin Hickey (IBM Research)
一句话结论
vLLM 通过将大模型推理拆分为独立的预填充(Prefill)、解码(Decode)和纯 CPU 前端渲染层,彻底消除了长提示词对并发生成任务的显存与算力抢占,在保持系统吞吐量的同时将长尾词间延迟降低了数倍。
要解决什么问题
传统的单体服务模式将三种特征完全不同的负载强行绑定在同一张 GPU 上,导致严重的资源互相干扰。首先,预填充阶段(处理提示词)是计算密集型的,需要一次性读取全部提示词,决定了首字母延迟(TTFT);而解码阶段(生成词)是访存带宽密集型的,逐词生成,决定了词间延迟(ITL)。当这两者共享 GPU 时,一旦系统接收到一个长提示词请求,庞大的预填充计算会瞬间霸占算力,导致该 GPU 上所有正在进行解码的并发流全部卡顿。其次,诸如聊天模板拼接、分词(Tokenization)、逆分词以及推理过程和工具调用的解析,本质上全是纯 CPU 计算,将这些任务留在 GPU 节点上运行,白白浪费了昂贵的加速器资源。
怎么做的
核心思路是实施分离式服务(Disaggregated Serving),将单体推理流水线物理切割为三个独立层级,各自独立扩展。第一部分是预填充与解码分离(P/D 分离)。预填充实例专门负责处理长提示词,计算出注意力机制所需的键值对(KV Cache),随后通过 KV 连接器(如 NIXL)经由 RDMA 或 PCIe 将这些显存块传输给解码实例。解码实例只负责运行纯粹的生成批处理,由于没有预填充任务的插入,生成流永远不会被中断。针对多轮对话,系统设计了双向传输机制,预填充节点可以从解码节点把上一轮的 KV 块拉回,只计算新增的 Token,避免重复计算历史上下文。第二部分是剥离无 GPU 需求的纯 CPU 前端。引入 /render 和 /derender 端点,将 OpenAI 格式的请求在 CPU 节点上直接转化为 Token ID,使得后端的 GPU 引擎变成纯粹的“进 Token、出 Token”黑盒。在实际调度中,代理路由会先调用渲染层获取 ID,接着向预填充节点下发仅生成 1 个 Token 的预算并要求其保留 KV 块,随后将指向这些块的参数(kv_transfer_params)传递给解码节点完成生成,最后由逆渲染层将输出的 ID 还原为包含内容、推理过程和工具调用的标准响应。
效果如何
核心对比实验在一台配备双 NVIDIA L40S GPU(48GB,PCIe,无 NVLink)的机器上进行。测试模型为 Qwen2.5-7B-Instruct,负载为 8K Token 提示词和 256 个输出 Token,按泊松分布发送 100 个请求。对比基线是传统的同位部署(Collocated)路线,即在两张卡上运行 vllm serve --data-parallel-size 2(代表数据并行的单体副本模式)。结果显示,在 0.4 req/s 的负载下,基线路线的 p99 词间延迟(ITL)从 23ms 暴增至 169ms,而分离式架构的 p99 ITL 始终稳定在 25 到 52ms 之间,长尾延迟降低了约 6 倍。在 AMD MI300X 集群上运行 Qwen3-235B-A22B-FP8 时,分离架构在满足严格延迟目标下的有效吞吐量(Goodput)是基线的 2.4 倍。但该方法有明确的代价和失效场景:KV 传输的耗时会全部叠加在首字母延迟(TTFT)上。在上述缺乏高速互联(如 NVLink 或 P2P)的 L40S 测试中,470MB 的 KV 传输耗时约 1.3 秒,导致分离架构的 TTFT 中位数高达 2.2 秒(基线仅为 0.7 秒),直接错失了 2 秒的 SLO 目标。此外,作者承认当前的流式逆渲染解析器存在严重的性能局限:由于推理和工具解析器的状态无法序列化,每个流式数据块都需要重建解析器并重放 Token 历史,其字符级计算复杂度达到了 $O(n^3)$,在长文本推理输出时会导致 CPU 负载飙升。
在默认情况下,单一的 vllm serve 进程需要同时处理三个互不相关的任务:处理提示词(Prefill阶段)、生成Token(Decode阶段)以及围绕这两者的大量CPU工作。由于Prefill阶段是计算密集型的,它会一次性读取整个提示词并决定了首Token时间(TTFT);而Decode阶段是内存带宽密集型的,每次只生成一个Token,决定了Token间延迟(ITL)。当它们共享同一块GPU时,一个长提示词的到达会阻塞正在进行的所有Decode流,导致输出严重卡顿。此外,如聊天模板化、分词、解分词以及推理和工具调用解析等纯CPU工作,也白白消耗了昂贵的GPU节点资源。
本文的核心贡献在于系统性地介绍了vLLM中的解耦服务(Disaggregated Serving),旨在将LLM推理的不同阶段分离开来:
/render和/derender),将这些工作从GPU节点卸载,使得底层引擎完全只处理Token IDs。解耦的维度与架构设计
MultiConnector。/render端点负责将OpenAI格式的请求转换为Token IDs,GPU引擎执行纯粹的“Token进/Token出”计算,随后/derender端点将输出的Token IDs转换回标准的OpenAI响应格式(包含拆分好的content、reasoning和tool_calls)。这最后一步近期才合并入主分支,完成了整个请求的闭环。
图1. 单一进程处理所有任务与同一流水线被切分为四层的对比。
解耦的收益与成本权衡
运行Prefill/Decode分离
--kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_producer"}'将其设定为KV Cache的生产者;Decoder运行在另一张GPU上,角色设定为kv_consumer。需要特别注意的是,主机上每个Worker的VLLM_NIXL_SIDE_CHANNEL_PORT必须唯一。# 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 8200
max_tokens=1以及附加参数kv_transfer_params: {"do_remote_decode": true}。Prefill计算完KV Cache后,会保留这些数据块,并返回指向这些数据块的kv_transfer_params。随后,Proxy将原始请求连同这些参数转发给Decode,Decode在开始生成Token前,会通过NIXL拉取这些数据块。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将这些Token IDs直接传递给Decode。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的kv_transfer_params指向decode所需的块;在此基础上添加token IDs
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}})
多轮对话:停止重新计算对话
bidirectional_kv_xfer,Prefill可以从Decode反向拉取这些数据块,从而只计算新增加的Token。Proxy通过客户端发送的conversation_id来追踪哪些数据块属于哪次对话。运行无GPU的前端
/render、/derender或/inference/v1/generate,需添加--enable-scale-out。请求流程分为三跳:1. 请求转为Token IDs(无GPU);2. Token IDs到Token IDs的生成(GPU);3. Token IDs转为标准响应(无GPU)。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
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步骤。解析器需要原始上下文(如Tools、tool_choice、include_reasoning)来产生与普通vllm serve相同的content / reasoning / tool_calls拆分。如果遗漏,会导致400错误,而不是将<tool_call>标记泄漏到content中的静默失败。流式输出(Streaming)
/render和生成端点都支持stream: true。生成的每个Chunk都需要独立通过Derender。在首次调用时,发送stream_state为null,随后回传上一次响应给出的状态。因为服务器在调用之间不保留任何状态,所以chat_request和prompt_token_ids必须随每个Chunk一起发送。这也意味着任何Render副本都可以处理任何Chunk。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 # 如果 n > 1,则每个choice index一个
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)
部署Render层
--renderer-num-workers设置(默认为1)。普通的渲染成本很低(9k Token约15ms),但通过解析器流式传输推理或工具调用模型的成本非常高。因此,必须提高Worker数量或增加副本以匹配并发解析流的数量。将两种解耦结合(Putting Both Splits Together)
/inference/v1/generate接口同样接受kv_transfer_params。# 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"}'
do_remote_decode,计算并保留KV Cache;3. Decode阶段通过NIXL拉取Prefill的数据块并进行生成;4. Derender阶段将Token IDs转换为最终响应格式。在生产环境中,这种编排工作通常由llm-d或Dynamo等框架接管。硬件配置:
nvidia-smi topo -p2p r 显示 NS 不支持点对点复制)。模型与参数:
软件配置:vLLM v0.31.0或更高版本,使用NIXL连接器。基线共置测试使用 vllm serve --data-parallel-size 2。测试时关闭前缀缓存(Prefix Caching off)以防止缓存命中掩盖Prefill的干扰。
实验组1:L40S双卡上的共置与P/D解耦对比 (Qwen2.5-7B-Instruct)
实验组2:KV传输速度对TTFT的影响分析 (L40S平台)
实验组3:AMD MI300X上的高速传输基准测试 (Qwen3-235B-A22B-FP8)
实验组4:集群规模下的端到端延迟测试 (gpt-oss-120b on 16 H200s)
目前,解耦服务已经在多个生产级框架中得到了实际应用:
LLMInferenceService对外暴露解耦能力。vLLM的解耦服务通过分离Prefill、Decode以及纯CPU的前端任务,成功解决了长提示词阻塞并发生成以及CPU任务抢占GPU资源的核心痛点。在具备高速互联硬件的支持下,解耦架构不仅能显著压平尾部延迟,还能大幅提升系统在严格SLO下的有效吞吐量(Goodput)。
然而,该架构仍有一些亟待解决的问题与未来工作:
prompt_token_ids。对于一个100k Token的提示词和1k的输出,提示词占据了每个请求体约99%的体积。finish_reason: "tool_calls"的问题([#47931]),以及引擎返回的Logprobs为字符串占位符的问题(提议返回真实的整数Token IDs,[#57574])。同时,社区正在讨论/inference/v1/generate是否应该直接返回文本或Derender后的输出,以省去不需要此步骤的调用者的第三跳开销([#56851])。