百万 QPS LLM 推理服务系统性优化

假设你要运营一个每秒有 140 万人同时在和 AI 对话的服务。每个人都在实时等待模型一个字一个字地输出答案。

这是一个极端的工程挑战——OpenAI、Google、字节、百度这个量级的公司才会面对。但理解它的优化方法,能让你真正搞清楚 LLM 推理系统是怎么工作的,以及为什么它和你之前接触的所有推理系统都不一样。

本文从零开始,由浅入深地讲解每一个优化手段。先把最基础的问题讲清楚,再一层一层往深了走。

一、LLM 推理和传统推理有什么不同

在讲优化之前,先搞清楚一件事:LLM 推理为什么和你之前接触的所有推理系统都不一样

传统深度学习推理(图像分类、目标检测)是这样工作的:输入一张图 → 模型跑一次前向传播 → 输出结果。整个过程是一次性的,输入和输出都是固定大小。

LLM 推理完全不同。它分为两个截然不同的阶段:

Prefill 和 Decode:两个性质完全不同的阶段

Prefill(预填充):处理用户输入的 prompt。假设用户说了 200 个字,模型需要把这 200 个 token 全部读进去,并行计算一遍,得到"理解了这段话"的内部状态。这一步很像传统推理:一次性处理,速度快。

Decode(解码):逐字生成输出。模型每次只能生成一个 token(一个字或一个词),生成完这个 token 才能生成下一个,无法并行。用户看到的"AI 一个字一个字地输出",就是这个阶段。

flowchart LR
    Input[用户输入\n200个token] --> Prefill
    subgraph Prefill[Prefill 一次性完成]
        P1[200个token并行计算\n速度快 算力密集]
    end
    Prefill --> KVCache[KV Cache\n保存计算中间结果]
    KVCache --> Decode
    subgraph Decode[Decode 逐token串行生成]
        D1[生成第1个token] --> D2[生成第2个token]
        D2 --> D3[生成第3个token]
        D3 --> D4[...]
    end
    Decode --> Output[最终输出\n假设200个token]

Decode 阶段是问题的核心。生成 200 个 token 就要走 200 步,每步都要读取整个模型的权重(几十 GB)来计算一个 token——这是显存带宽密集型操作,不是算力密集型。GPU 的算力大量空闲,瓶颈在数据读取速度。

KV Cache:为什么显存是核心瓶颈

Transformer 有一个机制叫 Attention(注意力),它需要"看"之前所有已生成的 token 来决定下一个 token 是什么。为了避免重复计算,模型会把之前每个 token 的中间计算结果(Key 和 Value 向量)都缓存起来,这就是 KV Cache

KV Cache 会随着对话变长而持续增长。以 Llama-70B 模型为例:

KV Cache 大小(每个请求,序列长度 1000 token,8卡合计):
  2(K和V)× 80层 × 64个注意力头 × 128维度 × 1000 token × 2字节
  ≈ 2.44 GB

每张 A100-80GB 显存分配:
  模型权重:35 GB(140B参数 ÷ 8卡 × 2字节)
  剩余给 KV Cache:45 GB
  最多同时服务:45 GB ÷ 305 MB/请求 ≈ 147 个并发请求

这就是为什么服务能同时处理的请求数是有上限的,而且这个上限不是算力决定的,是显存决定的

现在回到百万 QPS 的问题:

每秒 140 万新请求,每个请求平均占用 GPU 约 3 秒(生成 200 个 token)
→ 同时在处理中的请求数:140万 × 3 = 420万个并发请求
→ 每个节点(8卡A100)最多服务 150 个并发
→ 需要节点数:420万 ÷ 150 = 2.8万个节点 = 22万张 A100

这是不加任何优化的悲观估算。优化的目标,就是让每张卡能服务更多请求。

接下来每一个优化手段,本质上都是在回答同一个问题:怎么让同等显存能服务更多请求,或让同等请求数用更少显存

三、Continuous Batching:最高优先级的优化

要理解 Continuous Batching,先要理解 LLM 推理的一个基本特点:每个请求的输出长度完全不可预测。有人问"今天天气怎么样",输出 10 个 token;有人让模型写一篇文章,输出 500 个 token。

Static Batching 的问题

传统推理框架(Static Batching)的做法是:凑一批请求,把它们打包在一起推理,等这批请求全部完成,再接下一批。

问题就在这里。假设一个 batch 里有 3 个请求:

  • req1:需要生成 200 个 token(长)
  • req2:需要生成 50 个 token(短)
  • req3:需要生成 180 个 token(长)

在第 50 步,req2 已经生成完了(输出了 EOS token),但 req1 和 req3 还要继续。Static Batching 会怎么做?让 req2 的 GPU 槽位继续空占着,等 req1 和 req3 也完成,才能整体结束这个 batch、放入下一批请求。

这意味着从第 50 步到第 200 步,GPU 里有 1/3 的计算槽位在空跑——输入是 padding,没有任何有效计算。GPU 利用率约 30%。

gantt
    title Static Batching vs Continuous Batching
    dateFormat X
    axisFormat %s步

    section Static ~30%
    req1 推理中     :active, 0, 200
    req2 推理中     :active, 0, 50
    req2 空转等待   :crit,   50, 200
    req3 推理中     :active, 0, 180

    section Continuous ~80%+
    req1 推理      :active, 0, 200
    req2 移出      :done,   0, 50
    req3 推理      :active, 0, 180
    req4 第50步加入 :active, 50, 200

Continuous Batching 如何解决

Continuous Batching 的核心思路很简单:谁完成了,谁的位置立刻让出来给新请求,不等其他人

具体来说,每完成一个 decode step(生成一个 token),推理引擎就检查:哪个请求生成了 EOS token(结束标记)?把它从当前 batch 移出,同时把等待队列里的下一个请求插进来,填满这个空出来的槽位。

这样 GPU 的每一步都是满载的:

  • 第 0-50 步:req1 + req2 + req3 同时推理
  • 第 50 步:req2 完成,立刻移出;req4 插入
  • 第 50-180 步:req1 + req4 + req3 同时推理(GPU 仍然满载)
  • 第 180 步:req3 完成,移出;req5 插入……以此类推

GPU 利用率从 30% 提升到 80%+,相当于同样的硬件,有效吞吐量提升 2-3 倍。

为什么之前没人这么做?因为 NLP 任务传统上都是输入/输出等长的(翻译、分类),可以整齐地打 batch。LLM 的 decode 是逐 token 生成、长度不定的,这个特点让 Static Batching 变得非常低效,Continuous Batching 才因此被发明出来。

vLLM、TGI(Text Generation Inference)、SGLang 都默认实现了 Continuous Batching。任何 LLM 推理服务,这是必须开启的基础能力。

四、PagedAttention:解决显存碎片问题

现在你已经知道 Continuous Batching 解决了 GPU 利用率的问题。接下来的问题是:显存本身也在被浪费

直觉:酒店订房的比喻

想象一下,GPU 显存是一家酒店,每个请求是一位住客。传统方式(不用 PagedAttention)是这样管理的:

客人一进门,前台就给他预留一个最大规格的套房(比如 2048 个 token 的显存空间),不管他实际要住几天。结果是:

  • 客人 A 只住了 200 个 token,但套房里 1848 个 token 的空间全空着——内部浪费
  • 因为套房都被预占了,客人 D 即使只要一个小单间,也得在门口等——外部阻塞

这种管理方式下,显存利用率只有约 30%——70% 的空间是预留但没用上的空洞。

PagedAttention:按需分页,不预留

PagedAttention(vLLM 的核心发明)借鉴了操作系统管理内存的思路:不预留大块连续空间,用多少分多少,用小页面拼凑

把显存切成很多固定大小的小块(每块 16 个 token)。每个请求需要更多空间时,从空闲块池里取一块;请求完成时,把块归还。块不需要连续,用一张"映射表"记录每个请求的块在哪里:

graph LR
    subgraph Requests[两个请求的逻辑视图]
        R1[req1 对话中\n已生成48个token]
        R2[req2 对话中\n已生成32个token]
    end
    subgraph Memory[GPU显存物理块(每块16 token)]
        B3[块3\nreq1 token 1-16]
        B1[块1\nreq1 token 17-32]
        B7[块7\nreq1 token 33-48]
        B2[块2 空闲]
        B5[块5\nreq2 token 1-16]
        B9[块9\nreq2 token 17-32]
        B4[块4 空闲]
    end
    R1 -->|映射到| B3
    R1 -->|映射到| B1
    R1 -->|映射到| B7
    R2 -->|映射到| B5
    R2 -->|映射到| B9

结果:只有每个请求最后一个不满的块有轻微浪费,整体显存利用率从 30% 提升到 90% 以上。同样的显存,能同时服务 3 倍的并发请求。

额外收益:共享相同前缀

分页管理还带来了一个意外收获。如果两个请求有相同的前缀(比如都用同一段 System Prompt),它们可以直接共享同一批物理块,完全不需要复制:

1000个请求,都用同一段200 token的 System Prompt:

不共享:1000 × 200 token × 每token几百字节 = 占用大量显存

共享后:System Prompt 的 KV Cache 只存一份
       1000个请求共享这同一份,节省了 999/1000 = 99.9% 的 System Prompt 显存

这个特性叫 Prefix Caching,我们下一节会详细讲。

# vLLM 启动时开启 PagedAttention + Prefix Caching
from vllm import LLM, SamplingParams

llm = LLM(
    model="meta-llama/Llama-3-70b-instruct",
    tensor_parallel_size=8,
    gpu_memory_utilization=0.90,   # 90% 显存用于 KV Cache
    enable_prefix_caching=True,    # 开启前缀共享
)
outputs = llm.generate(prompts, SamplingParams(max_tokens=512))

五、Prefix Caching:让重复计算变成零

几乎每个生产级 LLM 服务都有一个固定的 System Prompt,比如:

"你是一个专业的客服助手,负责回答关于我们产品的问题。你应该友好、简洁……"

这段话可能有 500 个 token。每次用户发来一条消息,服务都会把 System Prompt + 用户消息拼在一起,从头做一遍 Prefill 计算。

问题是:System Prompt 对每个请求都是一样的,为什么要算 140 万次?

Prefix Cache 的原理

答案很简单:算一次,缓存结果,后续请求直接用缓存。

具体来说,System Prompt 部分的 KV Cache 算完后存起来。下一个请求来了,发现前缀和缓存里的一样,直接跳过这部分的 Prefill,从用户实际说的话开始算:

没有 Prefix Cache(传统方式):
  请求1:Prefill(500 token System Prompt + 20 token 用户输入) → 520 token prefill
  请求2:Prefill(500 token System Prompt + 15 token 用户输入) → 515 token prefill
  请求3:Prefill(500 token System Prompt + 30 token 用户输入) → 530 token prefill
  ...重复计算 140 万次 System Prompt

有 Prefix Cache:
  请求1:正常计算,缓存 System Prompt 的 KV Cache
  请求2:命中缓存!只计算 15 token 用户输入
  请求3:命中缓存!只计算 30 token 用户输入
  ...System Prompt 只算了 1 次

节省:500 ÷ 520 ≈ 96% 的 prefill 计算量!

这要归功于 PagedAttention 的分页设计——System Prompt 的 KV Cache 存在固定的物理块里,所有请求的 Block Table 都指向同一批块。不需要复制,不需要额外显存。

SGLang 的 RadixAttention 更进一步,用一棵前缀树管理所有历史对话的 KV Cache。不只是 System Prompt,连多轮对话里之前轮次的内容也能复用,在 Agent 和长对话场景下效果极好。

六、投机解码:让大模型不再一步一步等

回想一下 Decode 阶段的问题:大模型每次只能生成一个 token,生成完才能继续。100 个 token 的输出,大模型就要跑 100 次完整的前向传播,每次读取几十 GB 的权重。

有没有办法让大模型一次多做几步?

投机解码的思路:猜测 + 验证

用一个小模型(速度是大模型 10 倍以上)先猜测接下来的 5 个 token,然后让大模型一次性验证这 5 个猜测是否正确。

大模型验证 5 个 token 的计算量,和生成 1 个 token 几乎一样——因为验证可以并行,就像 Prefill 阶段一样。

假设小模型猜测:"the cat sat on the"

大模型验证(并行,只需 1 步时间):
  "the" ✓ 正确
  "cat" ✓ 正确
  "sat" ✓ 正确
  "on"  ✗ 错误(大模型认为应该是"in")

接受前 3 个 token,从第 4 个重新来

净效率:1 步时间 = 生成了 3 个 token(如果全猜对就是 5 个)
普通情况:1 步时间 = 生成了 1 个 token

吞吐提升:3倍(接受率约 70% 时)

关键问题是"接受率"——小模型猜测有多少比例被大模型接受。接受率越高,提升越大:

  • 用同系列小模型(Llama-8B)猜测 Llama-70B:接受率约 70-85%,吞吐提升 2-3 倍
  • 用 n-gram 匹配(从历史文本里直接找重复片段):接受率约 40-60%,无需额外模型,代码生成场景很有效

一个重要的注意点:投机解码提升的是吞吐量,不一定降低单个请求的延迟。如果接受率低(小模型猜得不准),额外的验证开销反而会增加延迟。建议在生产上先测量你的业务场景下接受率是多少,再决定是否启用。

# vLLM 中使用投机解码
llm = LLM(
    model="meta-llama/Llama-3-70b-instruct",
    speculative_model="meta-llama/Llama-3-8b-instruct",  # 小模型负责猜测
    num_speculative_tokens=5,   # 每次猜 5 个 token
    tensor_parallel_size=8,
)

七、量化:用更低精度存储,省显存又提速

前面几个优化都是关于"怎么管理 KV Cache"。量化解决的是另一个问题:模型权重本身能不能存得更小?

量化是什么

神经网络的权重(参数)默认用 FP16(16位浮点数)存储。量化就是用更少的位数来表示这些权重,以换取更小的显存占用和更快的读取速度。

Llama-70B 不同精度的显存占用:

FP32(32位):70B × 4字节 = 280 GB  (训练时用,推理一般不用)
FP16(16位):70B × 2字节 = 140 GB  (标准推理)
FP8(8位):  70B × 1字节 = 70 GB   (轻微精度损失)
INT4(4位):  70B × 0.5字节 = 35 GB (精度损失稍大但通常可接受)

模型小了,Decode 阶段每步读取权重的时间也短了——这直接提升了 token 生成速度。

FP8:H100 的首选,几乎无损

FP8 量化精度损失极小(PPL 只下降不到 0.5%),相当于几乎看不出来区别,但显存节省了一半,速度提升 1.5-2 倍。NVIDIA H100 原生支持 FP8 计算,是 H100 集群的标准配置。

INT4:更激进,省显存更多

INT4 量化把权重从 16 位压到 4 位,显存减少 75%。代价是精度损失比 FP8 稍大,但 AWQ(Activation-aware Weight Quantization)等算法通过智能地选择哪些权重更重要,把精度损失控制在可接受范围内。

from awq import AutoAWQForCausalLM

# AWQ INT4 量化:把模型压缩到 1/4 大小
model = AutoAWQForCausalLM.from_pretrained("meta-llama/Llama-3-70b-instruct")
model.quantize(tokenizer, quant_config={"w_bit": 4, "q_group_size": 128})
model.save_quantized("llama3-70b-awq-int4")
# 量化后:权重约 35GB(原来 140GB),单张 A100-80GB 可以跑

实际部署注意:INT4 量化后权重是 35GB,但加上 KV Cache 和框架开销,单卡 A100-80GB 的 KV Cache 空间只剩约 30GB,并发数有限。更好的方案是 2 卡 TP(每卡 17.5GB 权重),给 KV Cache 留出更多空间。

KV Cache 也能量化

不仅模型权重可以量化,KV Cache 也可以用低精度存储,再次压缩显存占用:

llm = LLM(
    model="meta-llama/Llama-3-70b-instruct",
    kv_cache_dtype="fp8",   # KV Cache 用 FP8 而不是 FP16 存储
    # 效果:KV Cache 显存减半,同等显存能服务约 2 倍的并发请求
)

八、多卡并行:一个模型跑在多张 GPU 上

前面的优化都假设模型能放进一张或少数几张 GPU 里。但 Llama-70B FP16 需要 140GB 显存,一张 A100-80GB 根本放不下。这就需要多卡并行。

张量并行:把模型"横着切"

张量并行(Tensor Parallelism,TP)是最常用的方案:把模型每一层的权重矩阵切分到多张 GPU 上,每张卡只存一部分,计算时各自算自己那部分,再把结果汇总。

Llama-70B FP16,8 卡张量并行(每台服务器 8 张 A100):

每张卡存储:140GB ÷ 8 = 17.5GB 模型权重
剩余显存:80 - 17.5 = 62.5GB 可用于 KV Cache
单节点最大并发:约 200 个请求

对比不拆分(需要 2 张卡才能放下模型):
每张卡存储:70GB 模型权重
剩余显存:80 - 70 = 10GB(KV Cache 极少,并发很低)

卡数越多,每张卡存的权重越少,留给 KV Cache 的空间越多,能同时服务的请求也越多。代价是卡之间需要频繁通信——用 NVLink(服务器内部,600GB/s)没问题,跨服务器用 InfiniBand 就会有明显延迟,通常 TP 不超过 2 台服务器。

流水线并行:把模型"竖着切"

对于 175B、405B 这种超大模型,张量并行一台服务器装不下,需要流水线并行(PP):把模型的层按顺序分给不同服务器,数据依次流过每台服务器:

Llama-405B,4 台服务器流水线:
  服务器1:第 1-25 层
  服务器2:第 26-50 层
  服务器3:第 51-75 层
  服务器4:第 76-101 层

数据流向:输入 → 服务器1 → 服务器2 → 服务器3 → 服务器4 → 输出

实际部署通常把两种并行结合:比如 4 台服务器流水线并行,每台服务器内部 8 卡张量并行,总共 32 张 GPU 跑一个 405B 模型。

九、Prefill-Decode 分离:进阶的工业级架构

到目前为止,所有优化都假设 Prefill 和 Decode 在同一台服务器上进行。现在来讲一个更激进的架构:把 Prefill 和 Decode 拆开,分别部署在不同的服务器上

为什么要分离:两个阶段是"死对头"

回忆一下两个阶段的特性:

  • Prefill:处理输入,算力密集,一次计算量很大,适合大 batch 集中处理
  • Decode:逐 token 生成,带宽密集,需要持续占用显存保存 KV Cache,需要低延迟

混在一台服务器上会有什么问题?

想象一个用户发来了 2000 个 token 的长文档(比如"帮我总结这篇文章")。Prefill 阶段要处理这 2000 个 token,可能需要 500ms。在这 500ms 里,这台服务器上所有其他正在 Decode 的请求(正在一个字一个字地给用户输出)全部被卡住了,等待这个 Prefill 完成

用户体验上的表现:其他用户在正常对话,突然输出卡了半秒钟。这就是"TTFT 抖动"——首 token 延迟不稳定。

分离架构设计

graph TD
    Client["请求进来"] --> LB["路由层(Load Balancer)"]
    LB --> PF["Prefill 集群 算力优化,少量节点 高算力 GPU,大 batch prefill"]
    LB --> DC["Decode 集群 显存优化,大量节点 大显存 GPU,Continuous Batch"]
    PF -->|"KV Cache 传输 (RDMA/NVLink-C2C)"| DC
    DC --> Output["输出 token 流"]

KV Cache 迁移:Prefill 节点计算完 KV Cache 后,通过高速网络(RDMA/NVLink-C2C)传输给 Decode 节点,Decode 节点持续生成 token 直到完成。

最大的工程挑战:KV Cache 传输带宽

一个 Llama-70B 请求的 KV Cache 约 2.44GB(1000 token 序列)。如果每秒新增 140 万请求,理论需要 140w × 2.44GB = 3.4 PB/s 的传输带宽——这远超任何现有网络。实际上需要:

  • 尽量缩短 prompt 或启用 Prefix Caching(只传增量 KV Cache,不传缓存部分)
  • Prefill 和 Decode 节点尽量在同一机架或通过 NVLink 直连,而非跨节点 InfiniBand
  • KV Cache 量化到 FP8/INT8 再传输,减少 50-75% 传输量
  • 并非所有请求都需要 PD 分离,短 prompt 请求可以走混合部署

收益:

  • TTFT(Time To First Token)更稳定,不被 Decode 占用的 GPU 影响
  • Prefill 集群可以针对算力优化(更激进的 batch),Decode 集群针对显存优化
  • 独立扩容:请求量增加时只扩 Decode 集群;长 prompt 请求增加时扩 Prefill 集群

十、调度优化:请求级别的精细控制

优先级调度

# 不同业务优先级,避免低优先级请求阻塞高优先级
from vllm import LLM
from vllm.core.scheduler import SchedulingBudget

# 高优先级请求(付费用户、实时场景)优先分配资源
# 低优先级请求(离线任务、批处理)在 GPU 有余量时才执行

抢占式调度(Preemption)

当显存压力大时,vLLM 可以抢占低优先级请求(把它们的 KV Cache 换出到 CPU 内存),为高优先级请求腾出空间:

显存紧张时:
  1. 低优先级请求的 KV Cache 被 swap 到 CPU 内存(慢但不丢失)
  2. 高优先级请求使用腾出的显存继续推理
  3. 显存释放后,低优先级请求 swap 回来继续

代价:CPU 内存带宽 <<< GPU 显存带宽,swap 会增加延迟
适用:显存极度紧张时的保底手段,不建议频繁触发

Chunked Prefill:降低首 Token 延迟抖动

长 prompt 的 Prefill 会占用 GPU 很长时间,期间所有 Decode 请求无法推进。Chunked Prefill 把长 Prefill 切成小块,穿插在 Decode 步骤之间:

gantt
    title Chunked Prefill 对比
    dateFormat X
    axisFormat %s ms

    section 不分块(其他请求 Decode 被阻塞500ms)
    Prefill 2000tok   :crit, 0, 500
    Decode step1      :active, 500, 530
    Decode step2      :active, 530, 560

    section Chunked Prefill(每块256token,延迟平滑)
    Prefill chunk1    :active, 0, 60
    Decode×N          :done,   60, 100
    Prefill chunk2    :active, 100, 160
    Decode×N          :done,   160, 200
    Prefill chunk3    :active, 200, 260
    Decode×N          :done,   260, 300

十一、主流框架对比与选型

框架核心优势适用场景生产成熟度
vLLMPagedAttention + Continuous Batching,生态最完善大多数在线服务场景⭐⭐⭐⭐⭐
TensorRT-LLMNVIDIA 官方,算子融合最激进,延迟最低追求极限延迟,NVIDIA 硬件⭐⭐⭐⭐
SGLangRadixAttention,多轮对话/RAG 场景前缀命中率高Agent、多轮对话、RAG⭐⭐⭐⭐
lmdeploy国内开源,对国产芯片支持好国产 GPU 场景⭐⭐⭐
Triton IS多模型统一管理,企业级运维模型种类多的企业⭐⭐⭐⭐

百万 QPS 场景的推荐:

  • 在线对话服务:vLLM(PagedAttention + 成熟的生产运维)
  • 多轮对话/Agent:SGLang(RadixAttention 前缀缓存命中率更高)
  • NVIDIA 专有优化:TensorRT-LLM(每 ms 都重要的场景)
  • 架构层:Prefill-Decode 分离需要自建调度层(参考 Mooncake 论文)

十二、成本优化:让每一张 GPU 都物尽其用

混合精度部署

不同层的精度敏感性不同:
  Embedding 层:对量化敏感,保持 FP16
  Attention 层:中等敏感,FP8 或 INT8
  FFN 层:相对不敏感,INT4 权重(AWQ)

混合精度策略:敏感层高精度,其他层低精度
  整体效果接近 FP16,显存接近 INT4

KV Cache 卸载到 CPU

# 多轮对话中,历史轮次的 KV Cache 卸载到 CPU 内存
# 当用户继续对话时,再从 CPU 加载回 GPU
llm = LLM(
    model="meta-llama/Llama-3-70b-instruct",
    cpu_offload_gb=40,  # 允许 40GB CPU 内存用于 KV Cache 卸载
)

弹性扩缩容

LLM 服务流量有明显的日/周规律(工作日白天高峰,凌晨低谷)。弹性扩缩容可以节省 40-60% 的成本:

  • 高峰期:全速部署,GPU 利用率 70-80%
  • 低谷期:缩容到最小集群,GPU 利用率提升到 90%+
  • 预测式扩容:基于历史流量预测,提前 5-10 分钟扩容,避免冷启动延迟

十三、把所有优化串起来

让我们用一张图把这篇文章的所有优化手段串起来,你会发现它们都在解决同一个核心问题的不同方面:

graph TD
    Problem[核心问题\n有限显存能服务多少并发请求]

    Problem --> A[GPU利用率低\nGPU大量空转]
    Problem --> B[显存利用率低\n预分配浪费]
    Problem --> C[Decode太慢\n每步只生成1个token]
    Problem --> D[模型太大\n放不进显存]
    Problem --> E[Prefill阻塞Decode\n延迟抖动]

    A --> Sol_A[Continuous Batching\n谁完成谁让位\nGPU利用率30%→80%]
    B --> Sol_B[PagedAttention\n按需分页\n显存利用率30%→90%]
    B --> Sol_B2[Prefix Caching\nSystem Prompt只算一次\n节省60%+prefill]
    C --> Sol_C[投机解码\n小模型猜+大模型验\n吞吐提升2-3x]
    D --> Sol_D[量化FP8/INT4\n显存减半\n速度提升1.5-3x]
    E --> Sol_E[Prefill-Decode分离\n两类节点各司其职\n延迟更稳定]
    E --> Sol_E2[Chunked Prefill\n长prompt切块穿插执行\n不阻塞其他请求]

    style Problem fill:#fde8d8,stroke:#e67e22
    style Sol_A fill:#d5f5e3,stroke:#27ae60
    style Sol_B fill:#d5f5e3,stroke:#27ae60
    style Sol_B2 fill:#d5f5e3,stroke:#27ae60
    style Sol_C fill:#d5f5e3,stroke:#27ae60
    style Sol_D fill:#d5f5e3,stroke:#27ae60
    style Sol_E fill:#d6eaf8,stroke:#2980b9
    style Sol_E2 fill:#d6eaf8,stroke:#2980b9

优化的优先级(按投入产出比排序)

优先级优化手段收益难度
⭐⭐⭐⭐⭐Continuous BatchingGPU 利用率 2-3x低(框架默认支持)
⭐⭐⭐⭐⭐PagedAttention并发数 3x低(vLLM 默认)
⭐⭐⭐⭐Prefix Cachingprefill 节省 60%+低(一行配置)
⭐⭐⭐⭐量化(FP8/INT4)显存减半中(需要验证精度)
⭐⭐⭐投机解码吞吐 2-3x中(需要测接受率)
⭐⭐Chunked Prefill延迟更稳定低(框架支持)
Prefill-Decode 分离延迟稳定+独立扩容高(需要自建调度层)

对大多数团队来说,把前四项(Continuous Batching + PagedAttention + Prefix Caching + 量化)做好,就已经能覆盖 80% 的优化收益。后面的手段适合在前四项都做完、仍有瓶颈时再考虑。