在这里插入图片描述

        入门认知 5 篇 + 训练与微调 5 篇——前 10 篇我们走完了「模型怎么来」的全过程。

        从这一篇开始,我们进入推理优化篇(第 11-15 篇)。这是整个系列里和后端工程师最贴近的部分,也是我个人最想认真讲透的部分。

        为什么这么说?

        很多团队的真实情况是这样的:

  •  模型训完,丢给后端「请部署上线」

  •  后端 pip install transformers,写个 FastAPI,跑起来

  •  发现:单 H100 跑 70B,平均 30 tokens/s,并发 1

  •  老板:「为什么这么慢?vLLM 一个请求能跑 1000 tokens/s 啊!」

  •  后端:「……我也是按官方文档写的」

        这 30 倍的性能差距,就藏在 vLLM 等推理框架里的"魔法"中。这些"魔法"不是什么不可言说的黑科技,本质就是三个核心技术:

KV Cache、PagedAttention、Continuous Batching。

        任何一个想做大模型部署的工程师,必须把这三个东西理解到「能讲给别人听」的程度。这一篇我们就来做这件事。

        读完本文你将能:

  1.  理解 Prefill vs Decode 两阶段的本质差异

  2. 算清 KV Cache 显存账,并知道量化怎么优化

  3. 理解 PagedAttention 如何让显存利用率从 30% 跃升到 90%+

  4. 理解 Continuous Batching 如何让吞吐量提升 5-10×

  5. 用 vLLM 跑通一个高性能推理服务,并能监控关键指标

        我们开始。


        一、为什么推理优化是另一个 90% 的工作

        1.1 朴素推理 vs 优化推理的差距

        我们先用一组真实数字震撼一下:

        实验设置:Llama-3-70B INT8、单 H100 80G、prompt=2K、生成=512 token、batch=32

推理方式

单卡吞吐(tokens/s)

相对性能

HuggingFace Transformers (朴素)

~30

Transformers + KV Cache

~120

TGI(HF 生产版)

~600

20×

vLLM (默认)

~2000

67×

vLLM + INT8 + FP8 KV Cache

~3500

117×

TensorRT-LLM (极致优化)

~4500

150×

        150 倍的差距。这意味着什么?

  • 不优化:你需要 150 张 H100 服务 1000 并发用户

  • 优化好:1 张 H100 就够

        每月成本差距:2K。这就是为什么推理优化是大模型部署的核心议题。

        1.2 推理成本 > 训练成本

        一个被反复验证的事实:

模型上线后,推理累积成本会在几个月内超过训练成本。

项目

训练成本

推理成本(月)

多少月超过训练

Llama 3-70B

$13M

~$1M

13 个月

DeepSeek V3

$5.6M

~$0.6M

9 个月

中型客服应用

~$200K

~$30K

6 个月

小型 API 包装

$0

~$5K

N/A

        这就是为什么 OpenAI / Anthropic 都把推理优化看作头等技术战略——训练是一次性投入,推理是持续支出。

        1.3 推理优化的三大维度

        工业上把推理优化拆成三个独立维度:

显存优化      ── 让模型放得下
吞吐优化      ── 让单位时间处理的请求更多
延迟优化      ── 让单个请求更快返回

        三者经常冲突——比如增加 batch size 提升吞吐,但会增加单请求延迟。

        工程师的核心工作就是在三者之间找平衡。

        本系列推理优化篇 5 篇会系统讲清这一切:

篇号

主题

核心优化维度

11(本篇)

KV Cache / PagedAttention / Continuous Batching

吞吐 + 显存

12

量化压缩

显存

13

Flash Attention

延迟 + 显存

14

投机解码

延迟

15

长上下文优化

显存 + 延迟

        我们开始第一板斧。


        二、第一板斧:KV Cache

        第 2 篇我们讲过 KV Cache 的基础原理。这里我们做工程视角的深度拆解——什么时候用、用多少、怎么压缩、怎么复用。

        2.1 Prefill vs Decode:两阶段本质不同

        大模型生成的工作流程其实是两个完全不同的阶段:

[Prefill 阶段]         [Decode 阶段]
处理整个 prompt        每次生成 1 个 token
计算 n × n attention   计算 1 × (n+i) attention
计算密集               显存带宽密集
~1 次                  ~512 次(生成 512 token)
单次耗时长             单次耗时短

        关键洞察

  •  Prefill 是 compute-bound——GPU 算力是瓶颈

  •  Decode 是 memory-bound——GPU 显存带宽是瓶颈

        这两个阶段需要的优化完全不同:

阶段

主要瓶颈

优化方向

Prefill

算力

Flash Attention、Chunked Prefill

Decode

带宽

KV Cache、量化、Batching

        生产环境的延迟构成

TTFT(Time To First Token) = Prefill 耗时
TPOT(Time Per Output Token) = Decode 单 token 耗时
端到端延迟 = TTFT + N × TPOT

        TTFT 和 TPOT 是两个互相独立的优化目标。

        2.2 KV Cache 显存账(再算一次)

        KV Cache 显存公式:

KV_cache = 2 × batch × seq_len × num_layers × num_kv_heads × head_dim × dtype_size

        以 Llama-3-70B(80 layers, 8 KV heads with GQA, head_dim=128, FP16)为例:

上下文

单请求 KV Cache

可并发数 (单 H100 80G, 模型占 70G 留 10G 给 KV)

4K

1.3 GB

7

8K

2.6 GB

3

32K

10.7 GB

0(连一个都装不下)

        这就是短上下文 + 多并发 vs 长上下文 + 少并发的根本权衡。

        2.3 KV Cache 量化

        KV Cache 已经是显存大户,把它量化是最直接的优化。

        主流方案:

方案

显存节省

精度损失

支持框架

FP16(基准)

0%

全部

FP8 (E5M2 / E4M3)

< 0.5%

vLLM, TensorRT-LLM

INT8

1-2%

vLLM, SGLang

INT4

3-5%(部分任务掉点明显)

vLLM 实验

        生产推荐

  •  有 H100+ → FP8 KV Cache(性能精度兼顾)

  •  老硬件 → INT8 KV Cache

  •  极致显存 → INT4,但要测业务效果

        vLLM 启用 FP8 KV Cache:

vllm serve Qwen/Qwen3-32B \
    --kv-cache-dtype fp8 \
    --quantization fp8

        2.4 Prefix Caching:把"重复计算"省掉

        很多业务场景的 prompt 有大量重复前缀

  • 多轮对话:每次新对话都带历史,前 N 轮的 KV 是重复的

  • RAG:每次请求都有相同的 system prompt + 检索文档

  • Few-shot:固定的 examples 占了大半 prompt

  • Code 助手:每次都带整个项目上下文

        朴素做法:每次都重新做 Prefill 计算这些 KV。

        Prefix Caching 做法:把高频前缀的 KV 缓存起来,下次直接复用。

        收益惊人

  •  多轮对话场景:TTFT 降 50-80%

  •  RAG 场景:TTFT 降 70-90%

  •  极端情况(90% prompt 重复):TTFT 降 95%

        vLLM 启用:

vllm serve ... --enable-prefix-caching

        OpenAI / Anthropic 早已做这个——所以他们的 prompt cache 价格只有正常价的 10-50%。

        高级:Tree-based Prefix Caching(SGLang)

        SGLang 进一步做了 RadixAttention——用基数树管理多个 prompt 的前缀树,支持任意分叉共享:

     [system prompt]
        /        \
   [user A]    [user B]
      |           |
   [response] [response]

        A/B 用户共享 system prompt 的 KV,每个用户自己的对话单独存储——节省显存 + 加速 prefill


        三、第二板斧:PagedAttention

        PagedAttention 是 vLLM 的核心创新(2023.6,Berkeley),目前已经被全行业采纳。它解决了一个所有人都忽视但极其严重的问题——KV Cache 显存碎片化

        3.1 传统 KV Cache 的「三宗罪」

        罪 1:连续分配的浪费

        传统 KV Cache 给每个请求预分配最大长度的显存。

        假设你支持 32K 上下文,那即使一个请求只用了 1K,也得占 32K 的显存(万一它说话长呢)。

        实际利用率:30-50%。

        罪 2:碎片化

        不同请求长度不同,释放后留下碎片——新请求无法利用。

[已用][已用][已用][空][已用][空][空][已用][空]
                ↑           ↑   ↑
              小碎片,无法被大请求使用
        罪 3:长短请求混合崩溃

        短请求和长请求一起来时,短请求结束后留下的空间被长请求占满——后续短请求干等。

时刻 1:[长][短][短][短]    ← 短的快结束
时刻 2:[长][空][空][空]    ← 短的结束了,留下空
时刻 3:[长][新长][...]     ← 新长请求占满了空间
时刻 4:[长][新长]          ← 没法接新短请求了!

        整体显存利用率:通常只有 30-50%。一张 80G 卡,实际有效利用 24-40G。

        3.2 PagedAttention 的思路:借鉴 OS 虚拟内存

        操作系统怎么处理类似的"碎片化 + 不知道用多少"问题?

分页(Paging)。

        PagedAttention 直接借用了这个思想:

KV Cache 显存 →  切成固定大小的"页"(block)
                 默认 16 token / 页
                 
每个请求的 KV →  按需申请页
                 不需要连续
                 用多少分多少

        对比

传统:每请求一块连续大显存
[████████████████][░░░░░░░░░░][████████████████]
   占了用不到             小碎片

PagedAttention:所有请求共用一个页池
[█A][█B][█A][█C][░][█B][█A][█C][░][█A][░][█B]
   ↑   ↑   ↑   ↑       ↑   ↑   ↑       ↑
请求 A B A C       B  A  C       A  
散落但满载

        通过一张page table 把"虚拟的连续 KV"映射到"物理的离散页"——和 OS 完全一样。

        3.3 PagedAttention 的实际收益

        PagedAttention 让显存利用率从 30% 跃升到 90%+,直接带来:

  •  单卡可并发请求数提升 2-4×

  •  端到端吞吐提升 2-4×

  •  长短请求混合也不慌

        vLLM 官方论文实测数据:

模型

朴素 KV Cache

PagedAttention

提升

LLaMA-13B

12 reqs/s

28 reqs/s

2.3×

LLaMA-33B

5 reqs/s

16 reqs/s

3.2×

LLaMA-65B

2 reqs/s

8 reqs/s

        模型越大,PagedAttention 收益越明显。

        3.4 进阶:Copy-on-Write(CoW)

        PagedAttention 还能做一件更聪明的事——KV Cache 共享

        场景:beam search、并行 sampling、A/B 测试。多个序列共享相同的 prefix。

        朴素做法:每个序列复制一份 prefix KV。

        CoW 做法

  1. 多个序列指向同一份 prefix KV 页

  2.  一旦某个序列要修改某页,复制一份再改(Copy on Write)

  3. 没有修改的页继续共享

        效果:多采样场景显存省 50-80%

        3.5 PagedAttention 的代价

        PagedAttention 不是没代价:

  •  页索引开销:每次 attention 计算需要先查 page table,增加几个 % 的开销

  •  CUDA Kernel 复杂度高:需要专门的 paged attention kernel

  •  实现成本:vLLM 团队花了几个月才稳定

        但相比 2-4× 的吞吐收益,这点代价几乎可以忽略。

现在主流推理框架都已经"PagedAttention 化":vLLM、SGLang、TensorRT-LLM、TGI 都支持。


        四、第三板斧:Continuous Batching

        第三板斧解决另一个看似简单但极其重要的问题——多请求怎么 batch

        4.1 静态 Batching 的悲剧

        静态 Batching(朴素做法)

请求 1 到达 → 等其他请求 → 凑齐 batch=32 → 一起跑 → 等最长的结束 → 全部返回

        这有几个致命问题:

        问题 1:等待延迟

        请求来的间隔不规律。要凑齐 32 个,可能要等几秒。用户体验灾难

        问题 2:木桶效应
batch 内 32 个请求,输出长度差异巨大:
请求 1: 输出 50 token  → 99% 时间在等
请求 2: 输出 100 token → 95% 时间在等
请求 3: 输出 1000 token → 慢悠悠
...
请求 32: 输出 5000 token → 大家全部陪它跑完

        短请求被长请求"绑架",GPU 利用率惨不忍睹。

        问题 3:长尾恶化

        业务场景中"长 tail" 很常见。99 percentile 的请求可能 10× 于 median。统计上必然出现极差

        结果:静态 batching 实际有效 GPU 利用率 ~ 20-30%

        4.2 Continuous Batching:迭代级调度

        Continuous Batching 的核心改动:

不在请求级别 batch,而是在 iteration(每生成一个 token)级别 batch。

        关键改动

  1. 任意时刻可插入新请求——不需要等

  2. 任意时刻可移除完成的请求——立即返回

  3. 每个 iteration(生成一个 token)作为调度单位

        举例

iter 1: 请求 A 在 batch
iter 2: 请求 A 在 batch,请求 B 到达,B 也加入
iter 3: A, B 都在
iter 4: A 完成了!立即返回 A,B 继续
iter 5: 只有 B 在 batch,请求 C 到达加入
iter 6: B, C 都在
...

        效果

  •  GPU 利用率从 20-30% 提升到 80%+

  •  短请求不再被长请求绑架

  •  吞吐量提升 5-10×

        4.3 Continuous Batching 的实现挑战

        实现这个看似简单的优化,工程上其实非常难:

  1. 变长 KV Cache 管理——不同请求不同长度,需要 PagedAttention 配合

  2. 变长 attention mask——每个 iteration 重新计算

  3. 请求优先级与公平性——长请求会不会饿死

  4. 预填充与解码混合——新请求要先 prefill,旧请求在 decode

        这就是为什么 PagedAttention + Continuous Batching 必须一起做——分开不好实现。

        4.4 PagedAttention + Continuous Batching = vLLM 的魔法

        PagedAttention 解决"显存放得下",Continuous Batching 解决"GPU 跑得满"。

        两者协同的效果:

  •  单卡能放下的请求数:2-4×

  •  每个请求平均跑得快:2-3×

  •  综合吞吐:5-10×

        这也是为什么 vLLM 相对朴素推理能快 50-100 倍

        4.5 进阶:Chunked Prefill

        Continuous Batching 还有一个进化版——Chunked Prefill(vLLM 0.5+ 默认)。

        问题:Prefill 很重(O(n²) 计算),如果一个长 prompt 来了,它会独占 GPU,把所有 decode 请求阻塞。

        Chunked Prefill 的做法:把长 prompt 切成 chunk(如 512 token),逐 chunk 处理,和 decode 请求混合调度

        效果

  •  长 prompt 不再阻塞短请求

  •  整体延迟尾部大幅改善

  •  99% latency 降低 50%+

        启用:

vllm serve ... --enable-chunked-prefill

        五、实战 + 关键指标

        5.1 vLLM 完整部署示例

# 部署 Qwen3-32B + 全套优化
vllm serve Qwen/Qwen3-32B-Instruct \
    --max-model-len 32768 \
    --tensor-parallel-size 2 \
    --gpu-memory-utilization 0.9 \
    \
    --enable-prefix-caching \                # KV Cache 复用
    --enable-chunked-prefill \               # 分块 prefill
    --kv-cache-dtype fp8 \                   # KV Cache 量化
    \
    --max-num-batched-tokens 8192 \
    --max-num-seqs 256 \                     # 最大并发数
    --swap-space 16 \                        # CPU swap 备用
    \
    --port 8000

        关键参数解读

参数

作用

推荐值

--gpu-memory-utilization

显存使用上限

0.85-0.95

--enable-prefix-caching

开启 prefix 复用

强烈推荐

--enable-chunked-prefill

长 prompt 不阻塞

推荐

--kv-cache-dtype

KV Cache 精度

fp8 (H100) / int8

--max-num-seqs

最大并发

看显存

--max-num-batched-tokens

单次 batch 总 token

通常 = max_seq_len × 2-4

        5.2 关键监控指标

        部署后必须监控的 4 个指标:

指标

含义

目标值

TTFT

 (Time To First Token)

首 token 延迟

< 500ms(短 prompt)

TPOT

 (Time Per Output Token)

单 token 生成时间

< 50ms

Throughput

整体吞吐 (tokens/s)

越高越好

GPU Utilization

GPU 利用率

80%+

        vLLM 自带 Prometheus metrics endpoint:

curl http://localhost:8000/metrics | grep vllm

        关键 metrics:

vllm:time_to_first_token_seconds_histogram
vllm:time_per_output_token_seconds_histogram
vllm:request_success_total
vllm:num_requests_running
vllm:num_requests_waiting
vllm:gpu_cache_usage_perc
vllm:cpu_cache_usage_perc

        接到 Grafana 后能可视化看到效果。

        5.3 性能调优 Checklist

        部署后发现性能不达标,按这个顺序排查:

  1. GPU 利用率低(< 60%)

    •  是不是 batch 太小?提高 --max-num-seqs

    •  是不是请求太少?看 num_requests_waiting

  2.  TTFT 高

    •  prompt 太长 → 启用 chunked prefill

    •  没开 prefix caching → 启用

  3.  TPOT 高

    •  显存带宽满了?看 GPU 利用率

    •  模型太大?考虑量化

  4.  OOM

    •  KV Cache 量化(fp8/int8)

    •  减少 max num seqs

    •  升级到张量并行

  5. 5. 长尾恶化

    •  开 chunked prefill

    •  设置 max tokens 上限

        5.4 性能对比实测

        我们用 Qwen3-32B 做对比实验(单 H100、prompt=1K、output=512):

配置

吞吐 (tokens/s)

TTFT

TPOT

纯 transformers

25

1200ms

40ms

+ KV Cache

95

1100ms

11ms

vLLM 默认

1800

600ms

28ms (batch 摊薄)

vLLM + fp8 KV

2400

550ms

22ms

vLLM + 全套优化

3200

280ms

20ms

        127 倍 吞吐差距。这就是推理优化的威力。


        六、扩展话题与下一篇预告

        6.1 三板斧之外的优化

        除了这三板斧,2024-2025 还有几个新进展:

技术

解决问题

状态

Speculative Decoding

串行解码慢

主流(第 14 篇)

Tensor Parallel + Pipeline Parallel

单卡装不下

主流(第 20 篇)

Multi-LoRA Serving

多业务复用

主流

Disaggregated Prefill/Decode

资源利用不均

新兴

CPU/GPU 卸载

极致显存

实验

        6.2 推理框架横向对比预告

        下一篇我们会进入量化,第 17 篇会做完整的推理框架横评:

框架

优势

适合

vLLM

易用、社区好

通用首选

SGLang

复杂控制流、JSON

Agent / 结构化

TensorRT-LLM

极致性能

生产追求极限

TGI

HF 生态融合

HF 用户

        6.3 不要只看 benchmark

        最后一个反直觉的提醒

性能基准很重要,但不要只盯着 benchmark。

        很多框架在标准 benchmark 上跑得飞快,但生产场景(长尾、突发、长上下文)下表现不一致。所以:

  1. 真实业务数据做测试

  2. 监控 p99 / p999 而不只是平均值

  3. 测试长时间稳定性(运行 24 小时看看)


        七、结语:理解三板斧,理解大模型推理

        读完本文你应该明白:

  • KV Cache:解决重复计算,4-10× 加速。Prefill 与 Decode 是两个完全不同的阶段

  •  PagedAttention:解决显存碎片,2-4× 提升并发能力。借鉴 OS 虚拟内存的智慧

  •  Continuous Batching:解决静态 batch 的悲剧,5-10× 吞吐提升

  •  三者协同 = vLLM 的"魔法",相比朴素推理 50-150× 加速

  •  真正的生产部署还要看 TTFT/TPOT/Throughput/GPU 利用率 四大指标

  •  Chunked Prefill / Prefix Caching / KV Cache 量化 是当下推荐的进阶配置

参考文献:

推理加速三板斧:KV Cache、PagedAttention、Continuous Batching

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐