把大模型的提示词处理和逐字生成拆到两组GPU上跑,每个请求的缓存都得从一组跨到另一组。字节本身很便宜,真正拖慢一切的是生成服务器主循环上的准备工作——它会让每个token都变慢,还可能把服务器的槽位耗光。
先看一个跟AI无关的旧bug。一个API通过20个连接的池子读数据库,某次发版给每个查询加了一小步同步操作。每个查询因此多占一会儿连接。正常流量下没人察觉,一到高峰,20个连接全忙,请求开始排队等连接。那一步只花毫秒,等待却要花秒。没人能从数据库那边查出问题,因为数据库本身好好的。
分离式服务犯的是同一个毛病。它把LLM服务器拆成两半:预填充GPU读完整段提示词,解码GPU再一个token一个token地写答案。拆开能避免长提示词卡住别人的回答。而在两者之间,每个请求的KV缓存——模型对提示词记下的笔记——要从一个池子搬到另一个池子。
首字变慢时,团队往往怪网络。数字说的不是这回事。vLLM在两组各八卡的H200服务器上测了Qwen3-32B。在每秒8个请求、提示词16000 token的条件下,默认交接方式的首字耗时约1744毫秒;换一种交接方式后降到约540毫秒。而搬运这段提示词的缓存本身只要约10毫秒。
丢掉的那一秒是准备工作。其中一部分跑在解码服务器的主循环上,会把正在进行的每个请求都暂停一下。
字节到底贵不贵
很便宜。Qwen3-32B每个token存256 KiB,16000 token的提示词就是4.2 GB。用张量并行摊到8张卡上,每张卡走自己的链路发自己那一片,按每卡50 GB/s算,大约10毫秒。
那这一秒去哪了?两个地方。其一,拉取模式在预填充之后串行执行各步骤:路由器转达数据块在哪,解码端分配空间,然后解码端读取数据块。其二,解码端在主循环上做这些准备,而这个循环同时要为每个请求产出每一个token。它在提交传输任务时,谁都拿不到token。
所以主循环损失的时间等于每秒到达数乘以每请求的准备时间。一份vLLM的拉取请求在2000 token提示词下测得每请求提交耗时14.7毫秒。按每秒8个请求算,主循环损失12%。把这项工作挪到线程后,每token耗时从6.14毫秒降到5.52毫秒。
token变慢为何拖累首字
因为一个请求要一直占着解码槽位,直到它的答案写完。占用的槽位数等于到达率乘以输出token数再乘以每token耗时,这就是利特尔法则。一旦超过--max-num-seqs,新请求就得先等一个槽位,缓存才能落地。
推送模式改了什么?解码端在请求一到达时就登记好自己的空闲数据块,预填充端从后台线程直接往里写。预填充之后没有等待,解码主循环上也不跑任何东西。
对你意味着什么
什么时候这事要紧:你用vLLM、SGLang、Dynamo或llm-d把预填充和解码放在不同GPU上,提示词长、流量高。
看vLLM那组16000 token、每秒8请求的测试。每token耗时在推送模式下是7.0毫秒,拉取模式下是10.4毫秒。按128 token的答案算,推送占用7.2个槽位,拉取占用10.6个。而基准测试启动服务器时用的是--max-num-seqs 8。也就是说拉取模式要的槽位比实际存在的还多,几个毫秒的每token开销就此变成一秒的等待。
这跟连接池是同一套算术。每请求的一点小开销在池子快满之前什么都决定不了,一旦快满就决定一切。这也解释了为什么两种模式在低负载下表现一致:每秒1个请求时,拉取模式也才占一个槽位出头,准备工作照做,但没人在后面排队。
该怎么做:先按你自己的流量算出高峰时占用的槽位数,用你实测的每token耗时,而不是基准里的数字,把它控制在--max-num-seqs的约80%以内。然后把交接工作挪出解码循环,重新测量。如果这个季度改不了交接方式,只有在解码显存允许时才调高--max-num-seqs——更多槽位意味着每张解码GPU上要更多KV缓存空间。
三个角色各管一段
容量规划的人负责定解码池大小。管服务配置的人负责选交接方式。跑压测的人负责证明它有效。
容量这边要算三个数,第一个通常很小,第三个才咬人:
- 缓存字节数:token数 × 层数 × 2 × KV头数 × 头维度 × 每元素字节数
- 传输毫秒数:字节数 ÷ 张量并行数 ÷ 链路带宽
- 占用槽位数:每秒请求数 × 输出token数 × 每token毫秒数 ÷ 1000
每token耗时要从生产环境高峰取,别用安静时的基准。输出长度也要用真实的:写长答案的智能体占槽位的时间远超聊天。一段2000 token的智能体答案,占槽位时间是128 token聊天回复的十六倍,同样的请求速率就需要十六倍的槽位。
配置这边,连接器是两台服务器上--kv-transfer-config里的一个设置。预填充端用生产者角色,解码端用消费者角色。两种模式混用会在第一个请求之后把服务器搞崩。推送还需要一个提前知道预填充服务器地址的代理。如果你要在预填充之后把每个请求路由到负载最轻的解码服务器,拉取仍是正确选择——推送提前就选定了解码端,等于放弃了这份灵活性。
测量这边,请求速率和提示词长度要一起扫,因为差距只在两者都拉高时才出现。判断是否奏效:低速率、短提示词下推送和拉取应当一致;扫描的高端,每token耗时应保持在轻载值附近;解码服务器上的等待请求数在高峰时应接近零。如果它往上爬,说明槽位不够,换什么交接方式都一样。再把首字耗时拆开,对比预填充服务器自己的首字时间和代理处的首字时间,差值就是交接加上等槽位的时间。改完之后,这个差值应当在长提示词、高负载时缩得最多。
什么时候不该拆
想要更高吞吐时别拆——拆分不增加吞吐,它是拿GPU换更稳的延迟。提示词短时别拆,预填充对解码的干扰本来就小,单机上的分块预填充可能就够了。只有一台GPU节点时别拆,交接不过网络,但两个池子和一个代理的成本照付。需要按负载路由解码时别拆,推送在预填充完成前就选好了解码端。解码池已经满了时也别拆,交接方式救不了槽位太少,先加解码容量再调交接。
上手之前先问自己三句:按利特尔法则,我的高峰流量占多少解码槽位?我的首字时间花在预填充、交接还是排队上?我需要在预填充之后再路由解码,还是可以提前选定?
热门跟贴