AI全栈知识13:模型加速 - 量化、KV Cache与推理优化
写在前面
你部署了一个大模型,用户问一个问题,模型想了5秒才回答。用户又问了一个,又等5秒。
5秒看起来不长,但如果100个人同时问呢?排队等到天荒地老。
模型加速要解决的就是这个问题:同一个模型、同样的回答质量,怎么让它更快、更省资源?
这不是换一个更大的GPU就能解决的(那是"加钱"不是"加速")。真正的加速是用更聪明的方法让模型跑得更快。
为什么大模型推理慢
先搞清楚"慢"在哪里。大模型推理的瓶颈不是计算(GPU算得飞快),而是:
瓶颈一:模型太大,显存装不下
| 模型 | 参数量 | FP16显存需求 | 一张T4能装下吗 |
|---|---|---|---|
| Qwen2-1.5B | 15亿 | 约3GB | 能 |
| Qwen2-7B | 70亿 | 约14GB | 勉强(T4是16GB) |
| Llama3-70B | 700亿 | 约140GB | 不能(需要多卡) |
模型参数占显存。参数越多,需要的显存越大。一张卡装不下就得多卡,多卡就贵。
瓶颈二:生成是串行的
大模型生成文字是一个Token一个Token往外蹦的,不能并行生成。
用户问:"K8s的Pod有哪些状态?" 模型内部: 第1步:生成 "Pod" 第2步:生成 "的" 第3步:生成 "状态" 第4步:生成 "有" ... 第50步:生成最后一个Token 每步都要把整个模型跑一遍!50个Token = 跑50次模型。每次跑一遍需要读取所有参数。模型越大,每次读取越慢。
瓶颈三:重复计算浪费
生成第50个Token时,模型需要"回顾"前面49个Token的信息。如果不做优化,每次都要重新算一遍前面所有Token的注意力,大量重复计算。
四大加速技术
| 技术 | 解决什么问题 | 类比 |
|---|---|---|
| 量化 | 模型太大装不下 | 搬家时把大箱子换成小箱子 |
| KV Cache | 重复计算浪费 | 做过的笔记不用重写,直接翻 |
| Flash Attention | 注意力计算慢 | 优化读写顺序,减少来回搬运 |
| Continuous Batching | 多用户排队慢 | 银行从叫号变成多窗口并行 |
技术一:量化 - 用更小的数字表示参数
原理
模型参数本来是高精度浮点数(FP16,每个数占2字节)。量化就是把它们转成低精度的整数(INT8占1字节,INT4占0.5字节)。
生活类比:
原来你记账精确到"分":127.53元。量化后你记到"元":128元。虽然不那么精确了,但:
- 记得更快(计算量小)
- 账本更小(占空间少)
- 对日常使用影响不大(谁在乎那几毛钱)
量化级别对比
| 量化级别 | 每参数大小 | 7B模型显存 | 速度提升 | 精度损失 |
|---|---|---|---|---|
| FP16(不量化) | 2字节 | 14GB | 基准 | 无 |
| INT8 | 1字节 | 7GB | 约1.5-2x | 几乎无 |
| INT4 | 0.5字节 | 3.5GB | 约2-3x | 轻微 |
| INT3 | 0.375字节 | 2.6GB | 约3-4x | 明显 |
量化方法
| 方法 | 原理 | 特点 |
|---|---|---|
| GPTQ | 逐层量化,用少量数据校准 | 离线量化,速度快,社区模型多 |
| AWQ | 保护重要参数不量化 | 精度损失更小 |
| GGUF | llama.cpp生态,CPU也能跑 | 适合本地部署 |
| bitsandbytes | 动态量化,加载时转换 | 最简单,一行代码搞定 |
实际效果(以Qwen2-7B为例)
测试环境:T4 16GB GPU FP16: - 显存占用:14.2GB - 速度:12 tokens/s - 能装下但几乎满了 INT8(GPTQ): - 显存占用:7.1GB - 速度:20 tokens/s - 还剩一半显存 INT4(GPTQ): - 显存占用:3.8GB - 速度:28 tokens/s - 显存很富裕什么时候用量化
| 场景 | 建议 |
|---|---|
| GPU显存够,追求最佳效果 | 不量化(FP16) |
| 显存刚好不够装下模型 | INT8(精度几乎无损) |
| 想在小GPU上跑大模型 | INT4(精度轻微损失) |
| 本地笔记本跑模型 | GGUF INT4(CPU也行) |
| 批量推理追求吞吐量 | INT8(速度和精度兼顾) |
量化的代价
量化不是免费的午餐:
| 代价 | 说明 |
|---|---|
| 精度下降 | INT4对复杂推理、数学题影响较大 |
| 量化本身需要时间 | GPTQ量化一个7B模型约10-30分钟 |
| 不是所有模型都适合 | 小模型(<3B)量化后掉点明显 |
| 需要对应推理框架支持 | vLLM支持GPTQ/AWQ,不是所有格式都支持 |
技术二:KV Cache - 记笔记避免重复计算
原理
模型生成每个Token时都要"看"前面所有Token。Transformer的注意力机制需要对每个历史Token算出Key和Value两个向量。
没有KV Cache时:
生成第1个Token:计算第0个Token的KV 生成第2个Token:重新计算第0、1个Token的KV 生成第3个Token:重新计算第0、1、2个Token的KV ... 生成第50个Token:重新计算第0到49个Token的KV(50次重复!)有KV Cache时:
生成第1个Token:计算第0个的KV → 存起来 生成第2个Token:读缓存拿到第0的KV + 只新算第1个的KV → 存起来 生成第3个Token:读缓存拿到第0、1的KV + 只新算第2个的KV ... 生成第50个Token:读缓存拿到0-48的KV + 只新算第49个 → 快很多!生活类比:
写一篇长文章。没有KV Cache就像每写一段新话之前,都要从头到尾重读一遍前面写的所有内容。有KV Cache就像你在旁边贴了便签纸,记着"前面讲了啥",写新段落时瞄一眼便签就行,不用重读全文。
KV Cache的代价:占显存
| 模型 | 序列长度 | KV Cache显存 |
|---|---|---|
| 7B模型 | 2048 tokens | 约2GB |
| 7B模型 | 8192 tokens | 约8GB |
| 7B模型 | 32K tokens | 约32GB |
序列越长,缓存越大。这就是为什么"长上下文"模型需要更多显存。
PagedAttention:vLLM的杀手锏
传统KV Cache有个问题:为每个请求预分配最大长度的显存。如果最大支持8192 tokens,就算用户只问了10个字,也分配8192的缓存空间。浪费!
vLLM的PagedAttention解决了这个问题:
像操作系统管理内存的"分页"机制一样,把KV Cache切成小块(页),按需分配。用多少分多少,不浪费。
传统方式: 请求A:预分配8192 tokens的显存 → 实际只用了200 → 浪费7992 请求B:显存不够了 → 排队等 PagedAttention: 请求A:先分配1页(256 tokens),用完再加1页 → 实际用了200,只占1页 请求B:显存充裕 → 同时处理效果:同样的GPU,能同时服务的请求数提升2-4倍。
技术三:Flash Attention - 优化读写顺序
原理
注意力计算涉及大量矩阵运算。瓶颈不在"算"(GPU计算很快),而在"搬数据"(在GPU的高速缓存和主显存之间来回搬)。
生活类比:
你在厨房做菜。冰箱(主显存)很大但离灶台(计算单元)远,台面(高速缓存)很小但在手边。
普通方式:每用一个食材就跑一趟冰箱。来回跑浪费大量时间。
Flash Attention:先规划好菜谱,一次性把需要的食材都拿到台面上,集中处理。减少来回跑冰箱的次数。
效果
| 指标 | 标准Attention | Flash Attention |
|---|---|---|
| 速度 | 基准 | 快2-4倍 |
| 显存 | O(N^2) | O(N)(省很多) |
| 精度 | 标准 | 完全一致(无损) |
Flash Attention是"免费午餐":不损失精度,纯粹通过优化内存读写顺序来加速。
使用方式
# vLLM默认开启Flash Attention,不需要手动配置# 如果用Hugging Face Transformers:model=AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-7B",attn_implementation="flash_attention_2"# 一行搞定)技术四:Continuous Batching - 动态批处理
原理
传统批处理(Static Batching):收集一批请求,等最长的那个生成完了,整批才算结束。短请求已经生成完了也得等长请求。
类比:旅行团坐大巴,到景点后所有人必须集合了才能走。快的人等慢的人。
Continuous Batching:谁先生成完谁先走,空出来的位置立刻让排队的新请求进来。
类比:改成出租车模式。到目的地的人直接下车,新乘客立刻上车。车不空跑。
效果
场景:10个用户同时提问,回答长度不同(有的20字有的200字) Static Batching: - 所有人等最长的那个(200字)生成完 - 短的用户明明1秒就能拿到结果,却等了10秒 - GPU利用率:先高后低(短请求完了GPU空转等长请求) Continuous Batching: - 短请求1秒就返回,不等别人 - 空出来的GPU资源立刻给排队的新请求 - GPU利用率:始终保持高位| 指标 | Static Batching | Continuous Batching |
|---|---|---|
| 用户体验 | 短请求被拖慢 | 每个请求尽快返回 |
| GPU利用率 | 波动大 | 持续高位 |
| 吞吐量 | 中 | 高(提升2-3倍) |
vLLM为什么快:四合一
vLLM之所以成为最流行的推理框架,是因为它把上面四种优化都集成了:
| 技术 | vLLM中的实现 |
|---|---|
| 量化 | 支持GPTQ、AWQ格式,自动识别 |
| KV Cache | PagedAttention(独创) |
| Flash Attention | 默认开启 |
| Continuous Batching | 默认开启 |
# 一行命令启动,所有优化自动生效vllm serve Qwen/Qwen2-7B-Instruct-GPTQ-Int4 \--max-model-len4096\--gpu-memory-utilization0.9不需要你手动配置每项优化。vLLM帮你做完了。
实际性能对比
同一张T4 GPU,同一个Qwen2-7B模型: 原始HuggingFace推理: - 单请求:12 tokens/s - 并发10:2 tokens/s/请求(排队) - 显存:14.2GB vLLM(INT4 + PagedAttention + Flash Attention + Continuous Batching): - 单请求:28 tokens/s - 并发10:10 tokens/s/请求(批处理) - 显存:4.5GB 提升: - 单请求速度:2.3倍 - 并发吞吐:5倍 - 显存节省:68%其他加速手段(了解即可)
| 技术 | 原理 | 适用场景 |
|---|---|---|
| TensorRT-LLM | NVIDIA专用编译优化 | NVIDIA GPU上追求极致性能 |
| Speculative Decoding | 小模型猜、大模型验证 | 长文本生成场景 |
| Prefix Caching | 相同前缀的请求共享KV Cache | 系统提示词相同的批量请求 |
| Tensor Parallelism | 模型切分到多张GPU | 单卡装不下的大模型 |
| 动态量化 | 推理时实时量化 | 不想提前转换模型格式 |
选择加速方案的决策树
你的模型能装进一张GPU吗? ├── 能 → 直接用vLLM(默认优化全开) │ └── 想再快?→ 用INT8量化版 └── 不能 → 两条路: ├── 用INT4量化(缩小到一张卡装得下)→ 用vLLM └── 用多卡(Tensor Parallelism)→ 用vLLM + --tensor-parallel-size 2运维视角:怎么选模型大小和量化级别
上面讲的是技术原理,但作为运维你实际面对的问题是:业务要上一个AI功能,给你一张卡,你怎么选模型?
第一步:从需求反推
| 问题 | 影响什么 |
|---|---|
| 模型要干什么(简单问答还是复杂推理) | 决定模型大小 |
| 要扛多少并发 | 决定要留多少显存给KV Cache |
| 有几张什么卡 | 决定显存上限 |
| 延迟要求(用户能等几秒) | 决定是否需要量化加速 |
第二步:选模型大小
| 任务复杂度 | 推荐参数量 | 典型场景 |
|---|---|---|
| 简单(分类、提取关键词) | 1.5B-3B | 工单分类、意图识别 |
| 中等(问答、总结、翻译) | 7B | 客服助手、内部知识库 |
| 复杂(代码生成、多步推理) | 14B-72B | 编程助手、复杂Agent |
原则:能用小模型搞定的就不用大模型。大模型不一定更好,但一定更贵更慢。
第三步:选量化级别
GPU显存够装FP16吗? ├── 够,且并发低(<5) → FP16(效果最好) ├── 够,但并发高(>10)→ INT8(省显存给KV Cache和并发) └── 不够 → 两条路: ├── INT4量化后能装下 → INT4 └── INT4也装不下 → 换更小的模型,或加卡实际选型表(单张T4 16GB)
| 模型 | FP16 | INT8 | INT4 | 推荐方案 |
|---|---|---|---|---|
| Qwen2-1.5B | 3GB,富裕 | 没必要 | 没必要 | 直接FP16 |
| Qwen2-7B | 14GB,满了 | 7GB,有余量 | 3.5GB,很宽裕 | INT8或INT4 |
| Qwen2-14B | 28GB,装不下 | 14GB,刚好 | 7GB,宽裕 | INT4推荐 |
| Qwen2-72B | 144GB,不可能 | 72GB,不可能 | 36GB,不行 | 单卡T4放弃 |
显存估算公式
模型显存 ≈ 参数量(B) × 每参数字节数 FP16:7B × 2字节 = 14GB INT8:7B × 1字节 = 7GB INT4:7B × 0.5字节 = 3.5GB 实际还要加KV Cache和框架开销(约多20-30%): 7B INT4实际占用 ≈ 3.5 × 1.3 ≈ 4.5GB简单记:GPU显存的70%给模型,30%留给运行时(KV Cache + 并发)。
第四步:量化后验证效果
量化不能盲目上线,要测试:
1. 准备20-50条业务场景的测试问题 2. 分别跑FP16版和量化版 3. 对比: - 答案正确率是否下降 - 格式是否正常(JSON输出会不会乱) - 复杂推理是否明显变差 4. INT4效果不行 → 退到INT8 5. INT8也不行 → 不量化,换大卡运维选型checklist
| 检查项 | 说明 |
|---|---|
| 模型显存 < GPU 70% | 留30%给KV Cache和并发 |
| 量化后跑测试集对比 | 不能盲目量化就上线 |
| 压测吞吐量 | vLLM自带benchmark工具 |
| P99延迟达标 | 最慢请求也在可接受范围 |
| 算总成本 | 大模型多卡 vs 小模型单卡,哪个划算 |
面试怎么说
如果被问"你了解模型加速吗":
"了解。大模型推理的瓶颈主要是模型太大(显存不够)和生成是串行的(Token一个一个出)。
常用的加速技术有四种:
- 量化:把FP16参数压成INT8或INT4,显存减半或减到1/4,速度翻倍
- KV Cache:缓存历史Token的计算结果,避免重复计算
- Flash Attention:优化显存读写顺序,速度提升2-4倍且无精度损失
- Continuous Batching:动态批处理,短请求先返回,GPU不空转
实际部署中我用vLLM,它把这四种优化都集成了。在T4上跑Qwen2-7B的INT4量化版,单请求28 tokens/s,并发10时总吞吐量能到100+ tokens/s。
选型方面,我的经验是先看任务复杂度选模型大小,再看显存选量化级别。70%显存给模型,30%留给运行时。量化后必须跑测试集验证效果再上线。"
延伸思考
| 问题 | 答案 |
|---|---|
| 量化后模型变笨了怎么办 | INT8几乎无损。如果必须用INT4且效果不行,换更大的模型量化到INT4可能比小模型FP16好 |
| KV Cache占满显存了怎么办 | 限制最大序列长度,或用PagedAttention(vLLM自动处理) |
| Flash Attention要自己装吗 | vLLM自带。用Transformers需要单独安装flash-attn包 |
| vLLM和TensorRT-LLM哪个好 | vLLM更易用(一行命令),TensorRT-LLM更极致但配置复杂 |
| 加速是运维干的事还是算法干的事 | 都有关。量化方案可能算法定,部署配置和资源分配是运维的活 |
小结
本篇核心收获:
- 推理瓶颈:不是GPU算不快,是模型太大(显存)+ 生成串行(带宽)+ 重复计算(浪费)
- 量化:用低精度(INT8/INT4)表示参数,显存减半以上,速度翻倍,精度轻微损失
- KV Cache:缓存历史计算结果,避免重复计算。vLLM的PagedAttention按需分配不浪费
- Flash Attention:优化显存读写顺序,快2-4倍且无损。是"免费午餐"
- Continuous Batching:动态批处理,短请求先走,GPU利用率持续高位
- 实战选择:直接用vLLM,四种优化全部内置,一行命令搞定
下一篇预告
AI全栈知识14:GPU资源弹缩实战 - 从HPA到Cluster Autoscaler
下一篇进入资源管理领域:
- 基于推理队列长度的HPA配置
- GPU节点自动扩缩容
- 竞价实例混合策略
- 弹缩延迟和冷启动优化
参考链接
- vLLM官方文档
- PagedAttention论文
- Flash Attention论文
- GPTQ量化
- AWQ量化