大模型的参数与显存成本同步膨胀:70B 模型的 FP16 权重就要 140GB 显存,消费级硬件与边缘设备根本无法容纳。模型压缩通过量化、剪枝、蒸馏、低秩分解四类手段,在不明显损伤精度的前提下大幅压缩模型体积与推理成本。本文给出完整的压缩技术栈与工程决策框架。
压缩全景:四大技术路线
模型压缩解决的是「精度-体积-速度」的三角矛盾。四大技术路线各有所长:
量化(Quantization):把高精度权重(FP32/FP16)映射到低精度(INT8/INT4),直接压缩体积并加速。是 LLM 与边缘部署的主流手段。
剪枝(Pruning):移除冗余权重或结构,稀疏化后要么直接省参数、要么靠稀疏算子加速。
知识蒸馏(Distillation):用大模型(教师)指导小模型(学生),让学生模型以小体积逼近教师能力。
低秩分解(Low-Rank Factorization):把权重矩阵分解为两个小矩阵的乘积,用低秩近似替代原始稠密矩阵。
| 技术 | 压缩原理 | 体积收益 | 速度收益 | 精度风险 |
|---|---|---|---|---|
| 量化 | 降低数值位宽 | 4-8 倍 | 2-4 倍 | 中 |
| 剪枝 | 移除冗余权重 | 2-10 倍 | 需稀疏算子 | 中高 |
| 蒸馏 | 大模型教小模型 | 5-50 倍 | 5-50 倍 | 中 |
| 低秩分解 | 矩阵低秩近似 | 2-4 倍 | 依赖算子 | 高 |
工程上四者常组合使用:蒸馏出一个较小的架构 → 量化到 INT8 → 再做结构化剪枝,可以在精度可控的前提下把模型压缩一到两个数量级。
量化基础:PTQ 与 QAT
量化的核心是把连续浮点值映射到离散整数值,同时最小化信息损失。基础是对称量化公式:
$$x_q = \text{round}\left(\frac{x}{s}\right), \quad \hat{x} = x_q \times s, \quad s = \frac{\max|x|}{2^{b-1}-1}$$
其中 $s$ 是缩放因子(scale),$b$ 是位宽。量化的关键难点是动态范围——权重的分布通常集中在零附近,直接用 min-max 映射会浪费低比特的表示空间。
PTQ(Post-Training Quantization,训练后量化):训练完成后直接量化,无需重新训练。用一小部分校准数据统计激活值的动态范围。成本低、速度快,是默认起点。但激活值范围对量化误差敏感,低比特(INT4)下精度下降明显。
QAT(Quantization-Aware Training,量化感知训练):在训练过程中模拟量化误差(直通估计 STE),让模型学会对量化噪声鲁棒。精度更高,但需要重训,成本高。
# PyTorch 官方 PTQ 流程
import torch
import torch.quantization as tq
# 准备:校准模型(统计激活范围)
model.eval()
model.qconfig = tq.get_default_qconfig("fbgemm") # 8-bit 对称量化
tq.prepare(model, inplace=True)
# 校准:喂入代表性样本,统计 min/max
with torch.no_grad():
for batch in calib_loader:
model(batch)
# 转换:实际量化
tq.convert(model, inplace=True)
# 推理时使用量化算子(fbgemm/x86 后端加速)
output = model(input_tensor)
| 对比 | PTQ | QAT | 动态量化 |
|---|---|---|---|
| 是否需要训练 | 否 | 是 | 否 |
| 校准数据 | 需要(几百条) | 不需要 | 不需要 |
| INT8 精度损失 | 1%-3% | <1% | 权重 INT8、激活 FP32 |
| 适用 | 快速压缩、CV/NLP | 低比特、精度敏感 | 仅权重量化(如 LLM) |
INT8 / INT4 实战:LLM 量化
LLM 的规模让低比特量化成为刚需。权重激活分布的两个特性(outlier 特征、逐通道尺度差异)决定了量化必须精细设计。
GPTQ:基于二阶信息(Hessian)的逐层(layer-wise)量化方法,通过 OBS 思想的优化逐列补偿量化误差,INT4 下 7B 模型精度几乎不损。
AWQ(Activation-aware Weight Quantization):基于「权重的重要性由激活决定」的观察,通过保留少量重要通道的精度、对不重要通道做缩放保护,实现无需重训的 INT4 量化。
# llama.cpp 使用 GGUF 量化(实际部署中最常用)
# 不同量化档位:Q4_K_M / Q5_K_M / Q8_0 等
# Q4_K_M:4bit 综合,体积约 4.5GB(7B 模型)
# Q8_0:8bit,体积约 7GB,精度接近 FP16
from llama_cpp import Llama
llm = Llama(
model_path="models/Qwen2.5-7B-Instruct-Q4_K_M.gguf",
n_ctx=8192, n_gpu_layers=-1, # 全部层上 GPU
verbose=False,
)
output = llm("中国的首都是", max_tokens=64)
# 用 llama.cpp 将模型转成 4bit GGUF
python convert_hf_to_gguf.py \
--outfile models/Qwen2.5-7B-Instruct-Q4_K_M.gguf \
--outtype q4_k_m \
models/Qwen2.5-7B-Instruct/
| 量化档位 | 位宽 | 7B 模型体积 | 精度(近似) | 适用 |
|---|---|---|---|---|
| FP16 | 16 | ~14GB | 100% | 基准 |
| INT8 / Q8_0 | 8 | ~7GB | ~99.5% | 通用部署 |
| INT4 / Q4_K_M | 4 | ~4.5GB | ~98% | 消费级 GPU |
| INT4 (GPTQ/AWQ) | 4 | ~4GB | ~99% | 服务化推理 |
| 2bit | 2 | ~2.5GB | ~95% | 内存极限场景 |
动态量化在 LLM 里也很常用:权重量化到 INT4/INT8,激活保持 FP16,KV cache 也做 FP16/INT8。这种「混合精度」在 vLLM 中通过 quantization="awq" 参数即插即用,无需修改推理代码。
剪枝:结构化与非结构化
剪枝把权重矩阵中的冗余元素置零。按粒度分为两类:
非结构化剪枝(Unstructured):置零任意位置的权重,稀疏度可极高(50%-90%),但产生不规则稀疏,需要专门的稀疏算子(如 cuSPARSE)才能加速,通用推理框架中往往只有显存收益而无速度收益。
结构化剪枝(Structured):整行/整列/整个通道(Channel)/整层移除,保持矩阵规则形状,兼容通用硬件加速。适合 CNN(通道剪枝)与 Transformer(移除 Attention head 或 FFN 中间维度)。
| 剪枝类型 | 粒度 | 加速效果 | 框架支持 | 精度风险 |
|---|---|---|---|---|
| 非结构化 | 单个权重 | 需稀疏算子 | 低 | 中 |
| 通道剪枝 | Channel | 好(规则稀疏) | 中 | 中 |
| 结构化(层/头) | 模块 | 好 | 高 | 高 |
| 1x1 卷积/微块 | 细粒度块 | 好 | 高(NVIDIA 2:4) | 低 |
Magnitude Pruning(幅值剪枝):把绝对值最小的权重置零,是最简单有效的方法。进阶的 Lottery Ticket Hypothesis(彩票假设) 认为存在稀疏子网络能匹配甚至超越原网络的性能。
2:4 稀疏:NVIDIA 在 Ampere+ 架构支持的半结构化稀疏——每 4 个权重中恰好 2 个非零,配合稀疏张量核可带来近 2 倍加速,是推理时「结构稀疏」的工程甜点。
# 幅值剪枝:置零最小幅度的权重
import torch
def magnitude_prune(weight: torch.Tensor, sparsity: float = 0.5):
"""按幅度剪枝:保留最大 sparsity 比例的权重。"""
mask = torch.zeros_like(weight, dtype=torch.bool)
k = int(weight.numel() * (1 - sparsity))
flat = weight.view(-1).abs()
_, top_idx = torch.topk(flat, k)
mask.view(-1)[top_idx] = True
return weight * mask # 被 mask 为 False 的位置置零
weight = torch.randn(256, 256)
pruned = magnitude_prune(weight, sparsity=0.5)
print(f"非零比例: {(pruned != 0).float().mean():.3f}") # ~0.5
剪枝的陷阱是稀疏度不等于速度:不规则稀疏在通用算子下反而可能更慢。生产实践应优先结构化剪枝或 NVIDIA 2:4 半结构化稀疏,并在剪枝后做少量重训(Prune-then-Finetune)恢复精度。
知识蒸馏
蒸馏的核心是「让学生模型模仿教师模型的输出分布,而不只是答案」。按蒸馏信号分为三类:
Logits 蒸馏:让学生学习教师模型的软标签(soft label),即经过温度 $T$ 缩放的输出概率分布。软标签携带了类别间的相对关系(「猫比狗更像狮子」),信息量远大于硬标签。Hinton 原始蒸馏损失:
$$\mathcal{L} = \alpha \cdot \text{CE}(y, \sigma(z_s)) + (1-\alpha) \cdot T^2 \cdot \text{KL}\left(\sigma(z_t/T) |\ \sigma(z_s/T)\right)$$
特征蒸馏:让学生网络的中间特征逼近教师网络的对应层特征(如 FitNets、DistilBERT 的隐藏层对齐),适合学生架构与教师不同构时。
LLM 蒸馏:教师 LLM 生成高质量样本(self-instruct 风格),让学生小模型以 SFT 方式学习,是当前大模型蒸馏的主流(如 MiniLLM)。
import torch
import torch.nn.functional as F
import torch.nn as nn
def distillation_loss(
student_logits, teacher_logits, labels, T=4.0, alpha=0.5
):
"""Hinton 蒸馏损失:软标签 KL + 硬标签 CE。"""
soft_targets = F.softmax(teacher_logits / T, dim=-1)
soft_pred = F.log_softmax(student_logits / T, dim=-1)
kl_loss = F.kl_div(soft_pred, soft_targets, reduction="batchmean") * (T * T)
ce_loss = F.cross_entropy(student_logits, labels)
return alpha * kl_loss + (1 - alpha) * ce_loss
| 蒸馏类型 | 教师→学生信号 | 学生架构 | 典型应用 |
|---|---|---|---|
| Logits | 输出分布(软标签) | 同构/异构 | 分类、语言模型 |
| 特征蒸馏 | 中间层特征 | 异构 | CNN(FitNets) |
| 关系蒸馏 | 样本间距离 | 异构 | 度量学习 |
| LLM 蒸馏 | 生成样本 + logits | 小 LLM | MiniLLM、DistilBERT |
| 自蒸馏 | 同一模型浅层学深层 | 同构 | 深层网络收敛 |
在线蒸馏 / 多教师蒸馏 是进阶方向:多个教师投票给一个学生,或教师学生同步训练(互相提升)。工程上蒸馏的收益取决于「教师优势」——如果教师模型本身质量不高,蒸馏收益有限。
低秩分解
低秩分解把稠密权重矩阵 $W \in \mathbb{R}^{m\times n}$ 近似为两个低秩矩阵的乘积 $W \approx UV^T$($U \in \mathbb{R}^{m\times r}$, $V \in \mathbb{R}^{n\times r}$,$r \ll \min(m,n)$),用 $r(m+n)$ 个参数近似 $mn$ 个参数。
**SVD(奇异值分解)**是最经典的方法:对 $W$ 做 SVD,保留前 $r$ 个奇异值,截断后的 $\hat{W} = U_r \Sigma_r V_r^T$ 是 Frobenius 范数意义下的最优低秩近似。
$$W \approx U_r \Sigma_r V_r^T, \quad \text{rank}=r$$
import torch
def low_rank_factorize(weight: torch.Tensor, r: int) -> tuple[torch.Tensor, torch.Tensor]:
"""SVD 低秩分解:返回两个小矩阵用于替换原权重。"""
U, S, Vh = torch.linalg.svd(weight, full_matrices=False)
# 只保留前 r 个奇异值
U_r = U[:, :r]
S_r = S[:r]
Vh_r = Vh[:r, :]
approx = (U_r * S_r) @ Vh_r
rank_ratio = r * (weight.shape[0] + weight.shape[1]) / weight.numel()
print(f"参数压缩比: {rank_ratio:.2%}")
return U_r * S_r, Vh_r # 两个低秩因子
| 分解方法 | 原理 | 适用层 | 精度风险 |
|---|---|---|---|
| SVD | 奇异值截断 | 全连接层 | 中 |
| Tucker | 高阶张量分解 | CNN 卷积核 | 中 |
| CP 分解 | 张量秩分解 | CNN | 高 |
| Toeplitz | 结构约束分解 | 卷积 | 低(特殊结构) |
低秩分解在浅层、宽层(如大 FFN、embedding 投影)上效果较好,深层语义信息密集处分解精度损失大。通常作为量化/剪枝之后的补充手段,单独使用很难达到与蒸馏相当的整体压缩比。
部署优化:ONNX / TensorRT / TVM
压缩后的模型要落地成可部署格式并充分发挥硬件加速。三条主流路径:
ONNX Runtime:开放模型交换格式,跨框架导出,图优化 + 支持各种 EP(Execution Provider,如 CPU/DirectML/CUDA),工程友好。
TensorRT:NVIDIA 专用推理引擎,对支持的网络做层融合、精度校准、kernel 自动调优,INT8/FP16 下吞吐最优。缺点:平台绑定 NVIDIA、构建时间长。
TVM / Apache TVM:编译器式优化,支持多后端(CPU/GPU/FPGA/专用加速器),自动图级 + 算子级优化。
# PyTorch → ONNX 导出(含动态维度)
import torch
class TinyBERT(nn.Module):
def forward(self, input_ids, attention_mask):
return self.bert(input_ids, attention_mask)[0]
model.eval()
dummy_ids = torch.randint(0, 30522, (1, 128))
dummy_mask = torch.ones(1, 128, dtype=torch.long)
torch.onnx.export(
model,
(dummy_ids, dummy_mask),
"model.onnx",
opset_version=17,
input_names=["input_ids", "attention_mask"],
output_names=["logits"],
dynamic_axes={ # 支持动态 batch 与序列长度
"input_ids": {0: "batch", 1: "seq_len"},
"attention_mask": {0: "batch", 1: "seq_len"},
},
)
# ONNX Runtime 推理 + 启用 TensorRT EP
import onnxruntime as ort
providers = [("TensorrtExecutionProvider", {
"trt_engine_cache_enable": True, # 缓存 engine,避免重复构建
"trt_fp16_enable": True, # FP16 加速
}), "CUDAExecutionProvider"]
sess = ort.InferenceSession("model.onnx", providers=providers)
outputs = sess.run(["logits"], {
"input_ids": input_ids.numpy(), "attention_mask": mask.numpy(),
})
| 部署路径 | 加速方式 | 平台 | 构建成本 | 最佳场景 |
|---|---|---|---|---|
| ONNX Runtime | 图优化 + 算子融合 | 跨平台 | 低 | 通用、快速上线 |
| TensorRT | 层融合 + 精度校准 | NVIDIA 独占 | 高 | GPU 生产吞吐 |
| TVM | 编译优化 + 自动调优 | 多后端 | 高 | 特殊硬件/异构 |
显存与吞吐权衡
压缩的收益最终要落到**显存(Memory Footprint)与吞吐(Throughput)**的可量化改善上。核心指标:
峰值显存 = 权重 + KV cache(LLM)+ 激活值 + 推理框架开销。量化直接压权重;KV cache 量化(FP16→INT8)与 PagedAttention 管理缓存是 LLM 推理的显存大头。
吞吐与延迟:压缩带来的提速取决于内存带宽瓶颈。LLM 推理是内存带宽密集型的(每生成一个 token 都要读一遍全部权重),权重从 FP16 降到 INT4,带宽需求降 4 倍,同硬件吞吐可提升 2-3 倍。
# 示例:7B 模型不同精度的 KV cache 显存对比
# 序列长 4096、40 层、32 头、head_dim=128
# FP16 KV cache = 2 × 4096 × 40 × 32 × 128 × 2B = ~2.7GB
# INT8 KV cache = 一半,~1.3GB
# GQA(分组查询注意力)进一步把 K/V 头数减半,显存再降
| 压缩目标 | 关键手段 | 收益量级 |
|---|---|---|
| 权重显存 | FP16→INT4 量化 | 4 倍 |
| KV cache 显存 | INT8 量化 + GQA | 2-4 倍 |
| 长序列显存 | PagedAttention + 分页 | 消除碎片 |
| 端到端吞吐 | INT4 + Continuous Batching | 2-4 倍 |
| 边缘延迟 | 蒸馏小模型 + TensorRT | 5-10 倍 |
权衡决策:压缩不是「越低越好」。每降一档位宽,精度风险上升一档。工程上的做法是逐档下探、卡在精度红线——先测 INT8 达标,再试 INT4,直到精度低于可接受阈值就回退一档。
压缩评估:精度保持度量
压缩必须回答「精度损失多少」,评估方法因任务而异:
| 任务 | 精度度量 | 可接受损失 |
|---|---|---|
| 分类 | Accuracy / F1 | <1% |
| CV 检测 | mAP | <1% |
| LLM 生成 | Perplexity / 任务基准 | PPL 增幅 <5% |
| LLM 对话 | 评测集质量分 | <2% |
| 翻译 | BLEU | <1 点 |
PPL(Perplexity) 是 LLM 压缩最敏感的粗筛指标:计算验证集上的困惑度,量化模型 PPL 上升即精度损失信号。PPL 变化先于任务指标,适合快速筛选量化档位。
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
def compute_perplexity(model, tokenizer, texts, stride=512):
model.eval()
total_nll, total_tokens = 0.0, 0
for text in texts:
encodings = tokenizer(text, return_tensors="pt")
input_ids = encodings.input_ids
with torch.no_grad():
for i in range(0, input_ids.size(1) - 1, stride):
inp = input_ids[:, i:i+stride]
out = input_ids[:, i+1:i+stride+1]
logits = model(inp).logits
shift_logits = logits[:, :-1, :].reshape(-1, logits.size(-1))
shift_labels = out[:, 1:].reshape(-1)
nll = torch.nn.functional.cross_entropy(
shift_logits, shift_labels, reduction="sum")
total_nll += nll.item()
total_tokens += shift_labels.numel()
return math.exp(total_nll / total_tokens)
# 对比 FP16 与 INT4 的 PPL,增幅 >5% 则回退档位
回归策略:任何压缩版本上线前必须在 Golden Set 上全量回归,并跑典型长尾用例。压缩后的模型可能出现「99% 用例没问题、1% 关键用例崩坏」的隐性退化,只有系统性回归能捕获。
实战案例:LLM 量化部署全流程
一个典型的 LLM 量化部署项目,路径如下:
[1] 基准测量:FP16 7B 的显存、延迟、PPL 基线
[2] 档位测试:INT8 → INT4(GPTQ/AWQ) → 2bit,逐档测 PPL
[3] 精度红线:PPL 增幅 <5%,任务基准(如 GSM8K)不降
[4] 部署格式:GGUF(本地)/ vLLM(服务化)
[5] 在线验证:影子部署对比量化版与 FP16 版输出质量
[6] 上线监控:显存、延迟、质量指标持续跟踪
# vLLM 服务化部署 AWQ 量化模型(零代码改动启用量化)
from vllm import LLM, SamplingParams
llm = LLM(
model="Qwen/Qwen2.5-7B-Instruct-AWQ", # 已量化权重
quantization="awq", # 指定量化方法
tensor_parallel_size=1,
gpu_memory_utilization=0.85,
max_model_len=8192,
)
outputs = llm.generate(["量化部署如何保证精度?"], SamplingParams(temperature=0.7))
print(outputs[0].outputs[0].text)
关键决策点:
- 量化方法:有重训预算选 QAT;无预算选 AWQ/GPTQ(按激活敏感度)
- 部署框架:服务化高并发用 vLLM;边缘单机用 llama.cpp GGUF
- 精度验证:PPL 粗筛 + 任务基准精测 + 影子部署终验
工程经验:LLM 量化的 80% 收益来自「FP16→INT4」这一档,往下的 2bit 收益递减且精度风险陡增。多数生产场景 Q4_K_M / AWQ-INT4 是性价比甜点。
总结
| 压缩技术 | 核心机制 | 体积收益 | 速度收益 | 精度风险 | 工程要点 |
|---|---|---|---|---|---|
| PTQ | 训练后直接量化 | 4 倍 | 2-3 倍 | 中 | 校准数据质量关键 |
| QAT | 训练中模拟量化 | 4 倍 | 2-3 倍 | 低 | 需重训成本 |
| INT4(GPTQ/AWQ) | 逐层 Hessian/激活感知 | 8 倍 | 3-4 倍 | 低-中 | LLM 服务化标配 |
| 剪枝 | 权重/通道移除 | 2-10 倍 | 需稀疏算子 | 中 | 优先结构化/2:4 |
| 蒸馏 | 教师教学生 | 5-50 倍 | 5-50 倍 | 中 | 教师质量决定上限 |
| 低秩分解 | SVD 低秩近似 | 2-4 倍 | 依赖算子 | 高 | 浅层宽层效果好 |
| ONNX/TensorRT/TVM | 编译优化 | - | 1.5-4 倍 | 低 | 按平台选型 |
模型压缩的成熟实践是「组合拳 + 逐档下探」:先蒸馏出小架构,再量化到目标位宽,最后用 TensorRT 等引擎榨干硬件性能。全程以 PPL 与任务基准为精度红线,把「能用的最小模型」变成「可上线的生产模型」。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。