模型推理优化-量化剪枝与加速
训练好的模型如何在生产环境高效运行?
模型推理优化:量化、剪枝与加速
训练好的模型如何在生产环境高效运行?
先说清楚一个基本矛盾:训练时追求精度,推理时追求速度和成本。一个 70B 参数的 LLM,以 float16 格式存储需要约 140GB 显存,大多数公司的硬件根本装不下,更别说服务多个并发用户了。
推理优化的目标就是:在可接受的精度损失下,大幅降低硬件要求和推理延迟,让模型能真正用起来。
1.1 推理优化的四个目标
量化/剪枝/Flash Attention/ONNX 四种优化技术的原理、压缩比与精度损失对比
| 目标 | 意义 | 常用方法 |
|---|---|---|
| 降低延迟 | 用户等待时间更短,体验更好 | Flash Attention、KV Cache |
| 提高吞吐量 | 同时服务更多用户 | vLLM PagedAttention、连续批处理 |
| 减少显存占用 | 在更小的 GPU 上运行更大的模型 | 量化(INT8/INT4)、剪枝 |
| 减小模型体积 | 便于部署到边缘设备 | 量化、ONNX 转换 |
1.2 量化:减少数值精度,是最实用的优化
量化是目前收益最高、副作用最小的优化方法,应该是优化的第一选择。
核心思想:神经网络的权重通常以 float32(每个数字占 4 字节)或 float16(2 字节)存储。量化把这些高精度数值压缩成低精度格式——int8 每个数字只占 1 字节,int4 只占半字节,从而减少存储空间和计算量。
直觉类比:把高分辨率照片压缩成低分辨率——文件小很多,但看起来几乎没有差别。关键问题是:压多少精度会开始明显影响效果?实践表明,从 float16 降到 int8,效果损失通常不到 1%;降到 int4,损失在 1-3%,对大多数应用可以接受。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
model_name = "Qwen/Qwen2.5-7B-Instruct"
# 方法一:INT8 量化(推荐用于生产环境)
# 显存减少约 50%,推理速度基本持平,精度损失 < 1%
model_int8 = AutoModelForCausalLM.from_pretrained(
model_name,
load_in_8bit=True, # 启用 INT8 量化
device_map="auto" # 自动分配 GPU/CPU
)
# 方法二:4-bit NF4 量化(推荐用于显存受限场景)
# 显存减少约 75%,精度损失 1-3%,是 QLoRA 微调时使用的格式
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NF4 格式,精度优于普通 int4
bnb_4bit_compute_dtype=torch.float16 # 计算时恢复为 float16,提升精度
)
model_4bit = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto"
)
# 方法三:GGUF 量化(用于 llama.cpp / Ollama,适合本地部署)
# 优势:支持纯 CPU 推理,适合没有 GPU 的场景
# 使用 llama.cpp 工具转换:
# python convert_hf_to_gguf.py model_dir --outtype q4_k_m --outfile model_q4.gguf
量化方案对比:
| 量化方式 | 显存节省 | 速度变化 | 精度损失 | 适用场景 |
|---|---|---|---|---|
| FP16(基线) | 0% | 基线 | 0% | 训练、高精度推理 |
| INT8 | ~50% | 接近持平 | < 1% | 生产环境首选 |
| INT4 (NF4) | ~75% | 轻微下降 | 1-3% | 显存受限场景 |
| GGUF Q4_K_M | ~75% | CPU 可运行 | 1-3% | 本地/边缘部署 |
怎么选:优先试 INT8,精度够用且显存节省一半。如果 INT8 后显存还不够,换 INT4 NF4。本地部署用 GGUF + Ollama。
1.3 Flash Attention:长上下文的必需品
Flash Attention 不改变计算结果,只改变计算方式。它重写了注意力的计算顺序,大幅减少显存访问次数,从而在长上下文场景下显著提速。
适用条件很关键:
Flash Attention 的收益对序列长度高度敏感:
- 序列 < 512 token:收益有限,有时因额外开销反而略慢
- 序列 1K-2K token:开始有明显收益,速度提升约 1.5-2x
- 序列 2K+ token:收益显著,速度提升 2-4x,显存节省约 30%
- 序列 32K+ token:不用 Flash Attention 基本跑不动
原因是标准注意力的显存复杂度是 O(n²),序列越长,Flash Attention 的优势越大。短上下文(简单问答)用量化,长上下文(文档分析、多轮对话)用 Flash Attention。
from transformers import AutoModelForCausalLM
import torch
# 安装:pip install flash-attn --no-build-isolation
# 要求:NVIDIA GPU Ampere 架构及以上(A100/A10/RTX 30xx+)
# 适合场景:处理长文档、长对话历史(2K+ token)
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B-Instruct",
torch_dtype=torch.float16,
attn_implementation="flash_attention_2", # 启用 Flash Attention 2
device_map="auto"
)
# 验证是否启用成功
print(model.config._attn_implementation) # 应输出 "flash_attention_2"
1.4 剪枝:删除冗余神经元
剪枝的思路是:神经网络中有大量权重接近零的连接,这些连接贡献极小,可以删除。
非结构化剪枝 vs 结构化剪枝:
- 非结构化剪枝:随机将单个权重置零。存储空间减少,但不改变矩阵形状,GPU 不能加速。
- 结构化剪枝:删除整个神经元、注意力头或卷积滤波器。改变了网络结构,GPU 计算量真正减少。
对于部署提速,结构化剪枝更有实用价值。
import torch
import torch.nn as nn
import torch.nn.utils.prune as prune
class SimpleNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(128, 64)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(64, 10)
def forward(self, x):
return self.fc2(self.relu(self.fc1(x)))
model = SimpleNet()
# 结构化剪枝:按 L1 范数删除整行(对应整个输出神经元)
# amount=0.3 表示删除 30% 的输出神经元
prune.ln_structured(
model.fc1,
name="weight",
amount=0.3,
n=1, # L1 范数
dim=0 # 按行(输出维度)剪枝
)
# 查看剪枝效果
weight = model.fc1.weight
sparsity = (weight == 0).float().mean().item()
print(f"fc1 层权重稀疏度: {sparsity:.1%}")
# 永久化剪枝(实际删除参数)
prune.remove(model.fc1, "weight")
total_params = sum(p.numel() for p in model.parameters())
nonzero_params = sum(p.nonzero().shape[0] for p in model.parameters())
print(f"总参数量: {total_params}")
print(f"非零参数量: {nonzero_params}")
print(f"整体稀疏度: {1 - nonzero_params/total_params:.1%}")
剪枝的注意事项:
- 剪枝后通常需要微调来恢复精度损失,直接剪枝会导致较大性能下降
- 对 LLM,更常用的是注意力头剪枝(删除贡献最小的注意力头)
- 剪枝 + 量化可以组合,获得更高压缩率
1.5 ONNX:跨框架部署,CPU 推理加速
ONNX(Open Neural Network Exchange)是模型格式标准,允许把 PyTorch 训练的模型导出为通用格式,然后在 CPU 上用 ONNX Runtime 高效运行。
适合场景:没有 GPU 的服务器、需要在 CPU 上部署分类/NLU 类型的小模型(BERT 系列)。
import torch
import torch.onnx
from transformers import AutoModelForSequenceClassification, AutoTokenizer
model_name = "bert-base-chinese"
model = AutoModelForSequenceClassification.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
model.eval()
# 准备示例输入
sample_text = "今天天气很好"
inputs = tokenizer(sample_text, return_tensors="pt", max_length=128, padding="max_length")
# 导出为 ONNX 格式
torch.onnx.export(
model,
(inputs['input_ids'], inputs['attention_mask']),
"bert_classifier.onnx",
input_names=['input_ids', 'attention_mask'],
output_names=['logits'],
dynamic_axes={ # 允许动态 batch size 和序列长度
'input_ids': {0: 'batch_size', 1: 'sequence'},
'attention_mask': {0: 'batch_size', 1: 'sequence'},
'logits': {0: 'batch_size'}
},
opset_version=14
)
print("ONNX 模型已导出到 bert_classifier.onnx")
# 用 ONNX Runtime 进行推理(pip install onnxruntime)
import onnxruntime as ort
import numpy as np
session = ort.InferenceSession(
"bert_classifier.onnx",
providers=['CPUExecutionProvider'] # CPU 推理
)
inputs_onnx = {
'input_ids': inputs['input_ids'].numpy(),
'attention_mask': inputs['attention_mask'].numpy()
}
outputs = session.run(None, inputs_onnx)
print(f"ONNX 推理结果维度: {outputs[0].shape}")
ONNX Runtime 在 CPU 上比直接用 PyTorch 推理快 1.5-3 倍,适合无 GPU 的生产服务器部署 BERT 类小模型。
1.6 推理性能对比
| 优化方法 | 显存节省 | 速度提升 | 精度损失 | 适用场景 |
|---|---|---|---|---|
| INT8 量化 | ~50% | 接近持平 | < 1% | 生产环境,平衡首选 |
| INT4 量化 | ~75% | 接近持平 | 1-3% | 显存极度受限 |
| 结构化剪枝 | 10-40% | 10-40% | 需微调恢复 | 边缘设备部署 |
| Flash Attention | ~30% | 2-4x(2K+ token) | 0% | 长上下文场景 |
| ONNX Runtime | 0% | 1.5-3x(CPU) | 0% | 无 GPU 的 CPU 部署 |
| vLLM | 0% | 3-10x(高并发) | 0% | 生产级 LLM 服务 |
1.7 怎么选优化方案
根据实际场景选,不要过度优化:
本地单机推理(消费级 GPU 或无 GPU)
→ GGUF 量化 + Ollama,4-bit 量化可在 16GB 内存的 MacBook 上运行 7B 模型
生产 API 服务(有 GPU,中等并发,长上下文)
→ INT8 量化 + Flash Attention(序列 > 2K 时)
生产高并发 LLM 服务
→ vLLM + INT4 量化(详见第 04 篇)
边缘设备 / IoT 部署
→ 结构化剪枝 + INT4 量化 + ONNX 导出,最大化压缩
CPU 服务器,无 GPU
→ ONNX Runtime(BERT 类小模型)或 GGUF(生成式模型)
优化优先级:量化 > Flash Attention(长序列)> vLLM(高并发)> 剪枝(边缘部署)。
多数情况只需要量化就够了,不要过度优化。