LLM PD 分离背后的架构问题


LLM PD 分离背后的架构问题

仅用于站内搜索,没有排版格式,具体信息请跳转上方微信公众号内链接

PD分离(PrefillingDecodingDisaggregation)推理是指将大模型推理的预填充阶段(P)和解码(D)阶段分离,以减少预填充与解码相互之间的影响,以便对两个阶段分别进行优化,提升GPU硬件的利用率,并减少推理延迟的一种推理技术方案。
在DistServe、Mooncake等论文中介绍分离式架构之后,DeepSeekV3的报告让大家更进一步意识到PD分离可能是影响成本和性能的关键技术。
vLLM对PD分离已经有了一个1P1D的实验版本。除此之外的开源框架大多还都不支持,不过很多已经在计划、实现中了。但纵览这些实现、文章或者计划,可以看到PD分离的架构选型上有很多问题需要思考,我尝试列举一下:
PD直连就是预填充节点直接将KVCache发送给解码节点,它的好处是延迟低。但也意味着在整个batch的计算过程中锁定了P、D节点的对应关系,一旦解码节点出现了问题,比如压力过大、服务出错、传输阻塞,在重试时无法仅调度D节点,需要重新进行整个预填充、解码过程。在prompt较长时,或者在PD节点数不对等的场景下,例如2个P对应到1个D,重调度意味着抛弃较长或者多个prefillbatch,重调度的沉没成本较高。
使用KVCacheStore/Pool是在P和D之间增加了一个中间存储,预填充节点先将KVCache写到中间存储,解码节点从中间存储读。这样做数据会多传输一次,增加了延迟,也增加了一些复杂度。但好处是容错性更好,还有就是预填充阶段本身也可以利用这个中间存储做PrefixCaching。
中间存储也会对其它一些架构变动的复杂度产生影响,参见下面问题四和五。
目前来看,KimiMooncacke、vLLM的下一步设计、阿里RTP-LLM都使用或者计划使用基于KVCacheStore/Pool的方案,DeepSeekV3报告中没有提到这部分。
在一些计算配比均衡、故障风险较小的场景下,比如同机多卡之间的PD分离,PD直连的方案也有其简单易部署的优势。
预填充最简单的实现是预填充节点完成第一个token的生成后,将所有的KVCache传输给解码节点,这也是vLLM当前的实现。但这样实现有个问题,因为KVCache的规模有可能非常大(尤其是原始MHA),一个batch的KVCache可能会是GB级别,都放在计算完成后传输,传输的延迟开销会比较大。
KimiMooncacke和阿里RTP-LLM都采取了按层传输的方案,这是利用了LLM多层计算的自然特性。在完成一层的计算以后,就将这一层的KVCache发送出去。这样KVCache的发送就呈流式,既能降低延迟,也能使数据的发送更平滑。还存在一个更显著的优势,是KVCache占用显存的时间更短,在显存紧张的情况下显存效率更高。
但按层发送对推理引擎的修改显然更大。我还没有看到开源的实现,猜测按层发送的引入对推理引擎的优化应该会有一定的影响,这里可能还需要一些精巧的设计才能减少影响。另外,按层发送对于PD非直连的场景下,中间存储的实现也会显著更复杂,QPS*num_hidden_layers,考虑到连续性可能还需要存储预分配和session保持。
因此对于MLA这种KVCache偏小的注意力实现,比如DeepSeekV3的KVCache是576B/token/layer,是否要做按层发送,也许要看一下实际收益。
解码阶段和预填充阶段有所不同。解码需要多次迭代,在第一次迭代实现按层解码也没太大意义,而且涉及到计算的编排,应该需要拿到所有层的KVCache才会开始计算。而且解码的计算时间比较长,如果解码的计算能够掩盖接收的延迟,不一定非要实现按层接收。
解码时按层接收,对调度也有一定挑战。从时序上来说,先发请求给预填充,完成后再发请求给解码会更自然。同时请求预填充和解码,需要处理一些同步问题,比如预填充压力大、解码等KVCache超时等等。比如像阿里RTP-LLM,它会观测预填充的排队情况,当一个请求进入预填充执行阶段时,解码端开始启动显存申请。
通常来说,预填充的同时会顺便把第一个Token计算出来,但计算到hiddenstates还是tokenid需要做一个选择。
计算到hiddenstates的好处是,预填充节点完全不需要加载和计算lm_head参数。比如DeepSeekV3的lm_head参数量是0.9B,如果计算到hiddenstates,这部分参数就完全不需要加载了。vLLM目前就是采取的这个方式,预填充除了需要发送KVCache之外,还需要发送一个hiddenstates,解码时引擎也需要能支持加载hiddenstates延续计算。

当到达请求的prompt长度有差异性的时候,预填充和解码就会出现压力的不均衡问题。因为整体的吞吐取决于P和D的全局资源利用,当P过载但D闲置,或者P闲置但D过载的时候,成本和性能都不是最优的。
所以就需要考虑在P和D之间做负载均衡,要么从整个节点层面直接切换P和D的角色,要么P和D节点能够承担一些混杂的请求,比如通过chunkedprefill。
这时候P和D是否直连对实现复杂度就有一些影响了,如果有中间存储的存在,通过PD转换做负载均衡的实现难度会降低很多。
如果业务应用场景中会将生成的context也作为下一轮的输入,还可能需要考虑Decoder填充KVCache,用于下一轮的prefixcaching复用。这时候,KVCacheStore/Pool的存在,对流畅交互有比较大的意义。
有别于我们通常的KV存储,由于GPU、RDMA(IB、RoCE)、NVLink新硬件的存在,KVCacheStore/Pool的设计抉择点会非常多。

A从VRAM复制到DRAM再写B的DRAM
A从VRAM复制到DRAM再让B读A的DRAM
A直接从VRAM复制到B的DRAM
B直接读A的VRAM
如果再加上NVMeoverRDMA,那要考虑的东西就更多了。P发送到Store,D从Store接收,到底要通过哪些模式支持,是需要思考的。目前来看,预填充节点更适合单边写到Store,这样能减少状态传输,更快地释放显存,但如果预填充节点也要读prefixcache,那情况可能反过来;解码节点可能更适合单边读Store。
在分布式架构上,无论是做集群式的KVCacheStore,还是单机side-car式的KVCacheStore,都需要存储一些meta,并且在P、D之间传输一些控制信息。学术界有一些完全基于RDMA实现的分布式KV数据库,但目前看复杂度还是比较高,也没有开源的实现。目前业界实现还是倾向于使用传统的RPC方式来传输控制信息,并且通过分布式技术方案做meta节点的一致性、可靠性设计。
在接口API上,KVCacheStore比传统的KVStore要复杂一些。比如要支持写的时候分layer写,读的时候能读到连续的内容;还可能要支持队列式的读,写完的layer可以很快被读走。如果要支持prefixcaching,还存在KVCache的链式关系,写的时候不仅要分layer,还要分page,读的时候也是。TP/SP等并行计算机制,对API可能还会有一些额外的要求。
在数据结构上,如果希望从VRAM直接写Store,减少一次复制,引擎本身的KVCache数据结构就需要与Store的数据结构进行一定程度的对齐;如果希望同时兼做prefixcaching,那store的数据排布就要考虑相同prefix的page更接近,甚至共享。比如用prompt的所有page的hash组成string,按前缀range分桶,桶内对相同前缀做merge/引用等等,这在存储优化上会是一个挑战。
整体来看,PD分离的实现上有很多架构问题需要抉择,目前还没有一个理想的架构方案,或许未来也会是根据不同场景有很多参数化的灵活配置。


文章作者: ZejunCao
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ZejunCao !
  目录