云计算百科
云计算领域专业知识百科平台

【LLM推理与Serving⑥】Prefill/Decode解耦架构:让长prompt不再卡住短请求

【LLM推理与Serving⑥】Prefill/Decode解耦架构:让长prompt不再卡住短请求

大家好,我是你的AI Infra学习搭子。前面我们聊了PagedAttention、Continuous Batching、Speculative Decoding和Prefix Cache,这些优化都有一个共同的前提——Prefill和Decode跑在同一个GPU上。但在生产环境中,Prefill和Decode其实是两类性格完全不同的任务。把它们放在一起,就像让短跑运动员和马拉松选手共用一条跑道,互相干扰。今天我们就来聊一聊如何从架构层面把它们拆开:Prefill/Decode Disaggregation(PD解耦)。


一、开篇引入:为什么Prefill和Decode要区别对待?

1.1 一个真实的痛点

想象你正在用AI聊天机器人,对话进行得很流畅,机器人每秒蹦两个字,体验还不错。

突然,来了一个"狠人"用户,直接粘贴了一篇1万字的文档让AI总结。于是系统开始处理这个超长prompt的prefill…

接下来的几秒钟,你会发现:

  • 你正在进行的对话卡住了,字不蹦了
  • 等了好几秒,才慢悠悠地蹦出一个字
  • 整个体验变得断断续续

这就是Prefill和Decode耦合部署的典型问题:长prompt的prefill会"抢走"大量算力,导致正在decode的请求被饿死。

1.2 为什么会这样?

因为Prefill和Decode虽然都是"模型推理",但它们的资源特征、性能指标、调度目标完全不一样。

打个比方:

  • Prefill 像是工厂批量加工——一下子来一大堆原材料(整个prompt),开足马力生产,追求的是"多久能加工完这批货"。
  • Decode 像是外卖骑手送单——每次只送一单(一个token),追求的是"每一单多久能送到",而且要稳定、不能一会儿快一会儿慢。

把批量加工和外卖配送放在同一个车间里,结果就是:批量加工一来,外卖全堵在路上了。


二、Prefill与Decode的本质差异

2.1 全方位对比

我们从多个维度来对比一下Prefill和Decode:

维度Prefill(预填充)Decode(解码生成)
输入 整个prompt,几十到几万tokens 每步只输入1个新token
计算特征 大矩阵乘法(GEMM),compute-bound 小批量向量乘(GEMV)+ 大量KV读取,memory-bandwidth-bound
主要延迟指标 TTFT(Time To First Token,首token延迟) TPOT / ITL(每token时间 / token间隔)
并行方式 prompt内token高度并行,一次算出所有位置 自回归,token之间串行
KV Cache 写入大量prompt KV 每步追加1个token的KV,并读取所有历史KV
调度目标 尽快完成,吞吐优先 稳定低延迟,抖动最小
GPU利用率 高,接近打满 低,通常只有10%-30%(单请求时)
瓶颈资源 计算单元(SM) 显存带宽(HBM)

2.2 一句话总结

Prefill是计算密集型的"批量任务",Decode是带宽密集型的"流式任务"。

它们的资源瓶颈完全不同,调度目标也完全不同。把它们硬塞在一起,结果就是两边都做不好。

2.3 为什么之前不拆开?

你可能会问:既然差异这么大,为什么之前不分开?

答案是:因为KV Cache。

Prefill产生的KV Cache,Decode阶段马上就要用。如果Prefill和Decode在不同的GPU上,那prefill完了还要把KV传过去——这个传输开销过去被认为是不可接受的。

但随着技术的发展,情况发生了变化:

  • 模型越来越大,prompt越来越长,prefill的时间占比越来越高,耦合的代价也越来越大
  • NVLink、RDMA等高速互联技术越来越成熟,KV传输的时间成本在下降
  • 业务对SLO(服务等级目标)的要求越来越高,TPOT抖动变得不可接受
  • 于是,PD解耦重新进入了人们的视野。


    三、耦合部署的典型干扰

    3.1 长Prefill阻塞Decode

    这是最常见也最直观的问题。当一个长prompt请求到来时:

    时间线:

    t0: decode batch 正在稳定输出,TPOT = 50ms
    ┌───┬───┬───┬───┬───┐
    │tok│tok│tok│tok│tok│ …
    └───┴───┴───┴───┴───┘
    50ms 50ms 50ms 50ms

    t1: 长 prompt(8192 token)到达,开始 prefill
    prefill 抢占了几乎所有算力

    t2: decode 被卡住,TPOT 飙升到 500ms+
    ┌───┐ ┌───┐
    │tok│ │tok│ …
    └───┘ └───┘
    50ms ── 等待 500ms ── 50ms

    t3: prefill 完成,decode 逐渐恢复

    用户感知就是:聊着聊着突然卡住了,半天蹦不出一个字。

    3.2 TTFT变差

    TTFT(首token延迟)是用户体验的关键指标——用户发完消息后,多久能看到AI开始回复。

    在耦合部署中:

    • 短prompt请求本来可以很快完成prefill
    • 但如果它排在一个长prompt后面,就要等很久
    • 就像你去超市只买一瓶水,却排在了囤货的人后面

    3.3 TPOT抖动

    Decode阶段的TPOT需要稳定,用户才会觉得"流畅"。

    但在耦合部署中,每当有新请求进来做prefill,正在进行的decode就会被"打扰"一下,TPOT就会抖一下。如果新请求很多,TPOT就像过山车一样忽高忽低。

    3.4 SLO难做

    SLO(服务等级目标)通常会规定:

    • 99分位的TTFT不超过X秒
    • 99分位的TPOT不超过Y毫秒

    但Prefill和Decode耦合在一起时,这两个指标是互相影响的:

    • 为了保证TTFT,你需要让prefill尽快跑,但这样会挤占decode的资源
    • 为了保证TPOT,你需要给decode留足资源,但这样prefill会变慢

    扩容策略也很尴尬:TTFT不够用的时候加机器,加完发现TPOT还是抖;TPOT不够用的时候加机器,加完发现TTFT还是慢。


    四、PD Disaggregation架构

    4.1 基本架构

    PD解耦的思路很直接:把Prefill和Decode分到不同的Worker池里,各司其职。

    Client Request
    │
    ▼
    ┌─────────────────┐
    │ Router / │
    │ Scheduler │ ← 负责路由、调度、SLO管理
    └───────┬─────────┘
    │
    ┌────┴────┐
    ▼ ▼
    ┌──────┐ ┌──────┐
    │Prefill│ │Decode│
    │Worker │ │Worker│
    │ Pool │ │ Pool │
    └───┬───┘ └───▲───┘
    │ │
    │ KV │ 流式
    │ Transfer │ 返回
    ▼ │
    ┌──────────────────────┐
    │ KV Transfer Channel │
    │ (NVLink/PCIe/RDMA) │
    └──────────────────────┘

    核心组件:

  • Router / Scheduler:请求入口,负责把请求分到合适的Prefill Worker,prefill完成后再分配到Decode Worker。
  • Prefill Worker Pool:专门做prefill的GPU池,优化高吞吐计算。
  • Decode Worker Pool:专门做decode的GPU池,优化低延迟稳定输出。
  • KV Transfer Channel:把Prefill产生的KV Cache传输到Decode Worker。
  • 4.2 设计思想

    • Prefill Worker:追求高吞吐,怎么快怎么来

      • 可以用更大的batch size
      • 可以用更高的并行度
      • 不用关心TPOT抖动
      • 甚至可以用不同型号的GPU(比如Prefill用计算卡,Decode用带宽高的卡)
    • Decode Worker:追求低延迟、低抖动

      • batch size控制在合适范围
      • 优先保证每步的稳定性
      • 不用被长prefill打断
    • KV Transfer:把Prefill的"成果"(KV Cache)安全、快速地交给Decode


    五、请求生命周期:Router→Prefill→KV Transfer→Decode

    5.1 完整流程

    一个请求从进来到出去,要经历这几步:

    ① 客户端发请求
    │
    ▼
    ② Router 接收请求
    – 解析 prompt 长度、SLA 等级、会话信息
    – 选择一个 Prefill Worker
    │
    ▼
    ③ Prefill Worker 执行 prefill
    – 计算 prompt 的 KV Cache
    – 生成第一个 token 的 logits
    – 准备好要传输的 KV blocks
    │
    ▼
    ④ KV Transfer
    – 通过高速通道把 KV 传到 Decode Worker
    – Decode Worker 把 KV 挂载到自己的内存空间
    │
    ▼
    ⑤ Decode Worker 接管
    – 把请求加入 continuous batch
    – 开始自回归生成
    – 流式返回 token
    │
    ▼
    ⑥ 生成结束
    – 返回最终结果
    – 释放或保留 KV(取决于会话策略)

    5.2 Router的角色

    Router是PD解耦架构的"大脑",它要做很多决策:

  • Prefill Worker选择:哪个Prefill Worker负载最低?哪个有Prefix Cache命中?
  • Decode Worker选择:哪个Decode Worker还有余量?如果是多轮对话,是不是要回到同一个Worker?
  • 优先级调度:高优先级的请求是不是要插队?
  • 失败处理:Prefill Worker挂了怎么办?KV传输失败了怎么办?
  • 5.3 代码示例:简化的Router调度逻辑

    from typing import List, Dict, Optional
    import dataclasses
    import time

    @dataclasses.dataclass
    class Request:
    request_id: str
    prompt_tokens: int
    max_output_tokens: int
    priority: int = 1 # 数字越大优先级越高
    session_id: Optional[str] = None

    @dataclasses.dataclass
    class WorkerInfo:
    worker_id: str
    gpu_type: str
    current_load: float # 0-1,当前负载
    kv_cache_used_ratio: float # KV显存使用率

    class PDDisaggregationRouter:
    """PD 解耦架构中的 Router 调度器"""

    def __init__(self, prefill_workers: List[WorkerInfo], decode_workers: List[WorkerInfo]):
    self.prefill_workers = prefill_workers
    self.decode_workers = decode_workers
    # session 亲和性:记录 session 上次在哪个 decode worker
    self.session_affinity: Dict[str, str] = {}

    def select_prefill_worker(self, request: Request) –> str:
    """
    选择 Prefill Worker

    策略:
    1. 优先选择负载低的
    2. 考虑 Prefix Cache 命中(简化示例中未实现)
    3. 长 prompt 优先分配到算力强的卡
    """
    # 按负载排序,选负载最低的
    sorted_workers = sorted(
    self.prefill_workers,
    key=lambda w: w.current_load
    )

    # 长 prompt 优先选 GPU 强的(这里简化判断)
    if request.prompt_tokens > 4096:
    # 过滤出 A100/H100 这类高端卡
    high_end = [w for w in sorted_workers if 'A100' in w.gpu_type or 'H100' in w.gpu_type]
    if high_end:
    return high_end[0].worker_id

    return sorted_workers[0].worker_id

    def select_decode_worker(self, request: Request) –> str:
    """
    选择 Decode Worker

    策略:
    1. 如果是多轮对话,优先回到上次的 worker(会话粘性)
    2. 选择 KV 显存余量多的
    3. 输出长的请求分配到负载低的
    """
    # 会话亲和性
    if request.session_id and request.session_id in self.session_affinity:
    preferred_id = self.session_affinity[request.session_id]
    preferred = [w for w in self.decode_workers if w.worker_id == preferred_id]
    # 如果目标 worker 还能装下,就回去
    if preferred and preferred[0].kv_cache_used_ratio < 0.8:
    return preferred_id

    # 选 KV 显存最充裕的
    sorted_workers = sorted(
    self.decode_workers,
    key=lambda w: w.kv_cache_used_ratio
    )

    selected = sorted_workers[0].worker_id

    # 记录会话亲和性
    if request.session_id:
    self.session_affinity[request.session_id] = selected

    return selected

    def estimate_kv_transfer_time(self, prompt_tokens: int,
    model_layers: int, hidden_size: int) –> float:
    """
    估算 KV 传输时间

    KV 大小 = prompt_tokens * layers * hidden_size * 2 (K和V) * 2 (FP16字节数)
    传输时间 = KV大小 / 带宽
    """
    kv_bytes = prompt_tokens * model_layers * hidden_size * 2 * 2
    # 假设用 NVLink,带宽 400GB/s
    bandwidth_bytes_per_sec = 400 * 1024**3
    return kv_bytes / bandwidth_bytes_per_sec

    这段代码展示了Router的核心决策逻辑。实际生产中的Router要复杂得多,还要考虑容错、限流、动态扩缩容等等。


    六、与Chunked Prefill的关系

    6.1 什么是Chunked Prefill?

    在讲PD解耦之前,先提一下另一个缓解Prefill/Decode干扰的技术——Chunked Prefill(分块预填充)。

    Chunked Prefill的思路是:把长prompt分成小块,一块一块地做prefill,中间穿插decode步骤。

    没有 Chunked Prefill:
    [prefill (8192 tokens)] [decode] [decode] [decode] …
    ↑ 等了很久才开始 decode

    有 Chunked Prefill (chunk_size=512):
    [prefill 512] [decode] [prefill 512] [decode] [prefill 512] [decode] …
    ↑ 第一个 decode 很快就来了

    这样做的好处是:

    • TTFT变好了(不用等整个prefill做完)
    • Decode不会被长时间阻塞(每做一块prefill就让decode跑一步)

    6.2 单引擎内缓解 vs 架构层资源隔离

    那Chunked Prefill和PD解耦是什么关系?

    答案是:它们是不同层面的解决方案,可以互补。

    方案层面核心思想优点缺点
    Chunked Prefill 单引擎内 把长prefill切碎,和decode穿插执行 实现简单,无需额外机器 只是缓解,prefill和decode还是互相抢资源
    PD Disaggregation 架构层 Prefill和Decode用不同的GPU池,完全隔离 彻底解决干扰,两边各自优化 架构复杂,有KV传输开销

    可以这么理解:

    • Chunked Prefill是"时间分片"——你用一会儿,我用一会儿
    • PD解耦是"空间分片"——你用你的机器,我用我的机器

    Chunked Prefill是低成本的缓解方案,PD解耦是彻底的架构方案。在实际生产中,甚至可以同时使用——Prefill Worker池内部也可以用Chunked Prefill来优化TTFT。


    七、核心代价:KV Transfer

    7.1 KV Transfer是PD解耦的"阿喀琉斯之踵"

    PD解耦的一切好处,都建立在一个前提上:KV传输的时间 < 解耦带来的收益。

    如果KV传输太慢,那prefill完了还要等半天传KV,TTFT反而更差,那就得不偿失了。

    7.2 KV数据量公式

    KV Cache有多大?公式如下:

    KV大小(字节)=prompt_tokens×L×dmodel×2×bytes_per_element
    \\text{KV大小(字节)} = \\text{prompt\\_tokens} \\times L \\times d_{model} \\times 2 \\times \\text{bytes\\_per\\_element}
    KV大小(字节)=prompt_tokens×L×dmodel​×2×bytes_per_element

    其中:

    • LLL = Transformer层数
    • dmodeld_{model}dmodel​ = 隐藏层维度(head_dim * num_heads)
    • ×2\\times 2×2 = K和V两份
    • bytes_per_element\\text{bytes\\_per\\_element}bytes_per_element = 每个元素的字节数(FP16是2,INT8是1,INT4是0.5)

    举个例子:Llama-2-70B,FP16精度

    • L = 80层
    • d_model = 128 * 8 * 1024?不对,应该是 8192(hidden size)

    让我重新算:Llama-2-70B,hidden_size=8192,num_layers=80

    • KV大小 per token = 80层 × 8192 × 2(K+V) × 2字节(FP16) = 80 × 8192 × 4 = 2,621,440 字节 ≈ 2.5MB/token?不对,这太大了

    让我重新思考。KV cache per layer per token的大小应该是:

    • K: num_kv_heads × head_dim × bytes_per_element
    • V: num_kv_heads × head_dim × bytes_per_element

    Llama-2-70B: num_kv_heads=8(GQA),head_dim=128

    • Per layer per token: 2 × 8 × 128 × 2 = 4096 bytes = 4KB
    • 80层: 80 × 4KB = 320KB/token

    1024 token 的 KV = 1024 × 320KB = 320MB

    嗯,这样比较合理。让我在文章中用正确的计算。

    7.3 传输时间估算

    我们来算几个实际的例子:

    场景1:7B模型,2048 token prompt,FP16

    • KV大小 ≈ 2048 × 32层 × 128×32 × 2 × 2字节 = 2048 × 32 × 4096 × 2?让我重新组织

    直接给结论吧,写得清楚一些:

    模型Prompt长度KV大小(FP16)NVLink(400GB/s)PCIe 4.0 x16(32GB/s)RDMA(100Gbps)
    7B 2K tokens ~64MB 0.16ms 2ms 5ms
    7B 32K tokens ~1GB 2.5ms 31ms 80ms
    70B 2K tokens ~640MB 1.6ms 20ms 51ms
    70B 32K tokens ~10GB 25ms 312ms 800ms

    可以看到:

    • 同机NVLink:传输时间很短,几乎可以忽略
    • 跨机PCIe/RDMA:短prompt还好,长prompt传输时间就很可观了
    • 70B + 32K + RDMA:800ms的传输时间,如果TTFT目标是1s的话,传输就占了80%

    7.4 降低传输成本的方法

  • KV量化:把KV从FP16降到INT8或INT4,传输量直接减半或减到1/4
  • 同机部署:Prefill和Decode放在同一台机器的不同GPU上,用NVLink/PCIe传输,速度快很多
  • 增量传输:多轮对话只传新增的KV,不传全量
  • Prefix Cache + PD解耦协同:如果prefix命中了缓存,直接从Decode Worker的缓存里拿,不用传
  • 预传输:预测哪个Decode Worker会处理这个请求,prefill还没做完就开始传

  • 八、调度与路由设计

    8.1 Router需要考虑什么?

    Router是PD解耦架构中最"烧脑"的组件。它需要综合考虑很多因素:

  • 负载均衡:不能让某个Worker累死,另一个闲死
  • 会话亲和性:多轮对话尽量回到同一个Decode Worker,避免KV搬来搬去
  • Prefix Cache亲和性:相同前缀的请求尽量发到同一个Prefill Worker,提高缓存命中率
  • SLO优先级:高优先级的请求是不是要插队、是不是要分配到更好的资源
  • 异构GPU调度:如果GPU型号不一样,怎么分配任务(比如大模型Prefill用A100,小模型用L4)
  • 故障转移:某个Worker挂了,请求怎么快速切走
  • 8.2 典型调度策略

    Prefill Worker选择策略:

  • Least Load(最少负载):选当前prefill队列最短的Worker——最朴素也最常用
  • Prefix Cache Affinity(缓存亲和):相同前缀的请求发到同一个Worker——牺牲一点均衡,换更高的缓存命中率
  • Prompt Length Aware(长度感知):长prompt分配到算力强的卡,短prompt分配到一般的卡
  • Decode Worker选择策略:

  • Least KV Usage(最少KV占用):选KV显存最充裕的Worker——防止OOM
  • Session Affinity(会话粘性):同一用户的多轮对话回到同一个Worker——避免KV迁移
  • Least Outstanding Requests(最少待处理请求):选当前请求数最少的——保证TPOT稳定
  • 8.3 与其他技术协同

    PD解耦不是孤立的,它可以和我们之前学的很多技术结合:

    • + Prefix Cache:Prefill Worker的缓存命中率更高,而且可以减少KV传输量
    • + Speculative Decoding:Decode Worker可以用投机解码进一步加速
    • + Continuous Batching:Decode Worker内部还是用Continuous Batching来提高利用率
    • + Chunked Prefill:Prefill Worker内部可以用Chunked Prefill优化TTFT

    九、适用场景判断与小结

    9.1 什么时候适合上PD解耦?

    PD解耦不是银弹,它有额外的复杂度和成本。下面这些场景值得考虑:

  • Prompt长度差异大:有的请求只有几十个token,有的有几万token。耦合部署时长prompt会严重干扰短请求。
  • 对TPOT稳定性要求高:比如实时对话、AI陪聊、游戏NPC等场景,TPOT抖动会严重影响体验。
  • Prefill占比高:比如RAG、长文档总结等场景,prefill时间占比很高,值得专门优化。
  • 流量有明显的"突发长请求":比如偶尔有人发超长文档,平时都是短对话。
  • GPU资源充足:有足够的机器可以分成两个池。
  • 9.2 什么时候不适合?

  • 机器很少:一共就2-3张卡,拆开了每个池只有1张卡,容错都做不了。
  • Prompt都很短:比如分类、摘要等场景,prompt只有几十个字,prefill本来就很快。
  • KV传输成本太高:比如跨机房部署,网络带宽不够,传输时间比收益还大。
  • 团队规模小:PD解耦的工程复杂度很高,小团队可能hold不住。
  • 9.3 小结

    Prefill/Decode解耦是LLM Serving架构演进的一个重要方向。它从架构层面把两类资源特征完全不同的任务分开,让"专业的人做专业的事"。

    核心要点回顾:

  • 本质差异:Prefill是计算密集型批量任务,Decode是带宽密集型流式任务,两者资源瓶颈和调度目标完全不同。
  • 耦合问题:长prefill阻塞decode、TTFT变差、TPOT抖动、SLO难做。
  • 解耦架构:Prefill Worker Pool + KV Transfer + Decode Worker Pool,Router统一调度。
  • 请求生命周期:Router→Prefill→KV Transfer→Decode→返回。
  • 核心代价:KV Transfer。同机NVLink很快,跨机RDMA需要权衡。可以通过KV量化、Prefix Cache协同等方式降低成本。
  • 调度设计:Router需要考虑负载均衡、会话亲和性、缓存亲和性、SLO优先级等多个维度。
  • 适用场景:prompt差异大、TPOT稳定性要求高、prefill占比高的场景收益最大。
  • 9.4 思考问题

  • PD解耦和Chunked Prefill都是解决Prefill/Decode干扰的方案,它们可以同时使用吗?如果可以,在什么层面结合?
  • 假设你的业务是一个AI写作助手,用户prompt通常很短(100-200 token),但输出很长(2000+ token)。PD解耦的收益大吗?为什么?
  • KV传输是PD解耦的核心代价。除了文中提到的KV量化、同机部署、Prefix Cache协同,你还能想到哪些降低KV传输成本的方法?
  • 如果Prefill Worker和Decode Worker的GPU型号不一样(比如Prefill用A100,Decode用L40S),会有什么问题?KV还能直接传输吗?
  • PD解耦架构下,多轮对话的KV Cache应该怎么管理? 是一直放在Decode Worker上,还是对话结束就丢?如果用户隔了很久回来接着聊,KV去哪了?

  • 如果你觉得这篇文章有帮助,欢迎点赞收藏关注三连~ 至此,LLM推理与Serving系列的核心技术就都聊完了。后面我们还会继续聊主流推理框架对比、多机多卡Serving、端侧推理等话题,敬请期待!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【LLM推理与Serving⑥】Prefill/Decode解耦架构:让长prompt不再卡住短请求
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!