大模型越来越大,但部署环境千差万别。不是每台设备都有A100,不是每个场景都能容忍秒级延迟。模型压缩技术就是连接"大模型能力"和"有限部署资源"的桥梁。本文将系统梳理剪枝、量化、蒸馏三大压缩技术的原理与工程实践。
一、模型压缩的必要性
1.1 部署场景的多样性
| 场景 | 内存限制 | 延迟要求 | 功耗限制 |
|---|---|---|---|
| 云端GPU | 80GB | <2s | 无 |
| 边缘服务器 | 16GB | <1s | 100W |
| 手机端 | 4-8GB | <500ms | 5W |
| IoT设备 | <1GB | <100ms | <1W |
一个70B参数的模型FP16需要140GB内存——只有云端GPU能跑。要部署到手机,需要压缩20-40倍。
1.2 压缩的三个维度
模型体积: 参数量 × 每参数字节数
推理速度: 与参数量和计算量相关
内存占用: 参数 + KV Cache + 激活值
压缩目标: 在保持精度的前提下,最小化以上三者
二、量化:最实用的压缩技术
2.1 量化原理
将高精度浮点数(FP16/FP32)映射到低精度整数(INT8/INT4):
FP16: 0.1234, 0.5678, -0.2345 (16 bit/参数)
INT8: 映射到 [-128, 127] (8 bit/参数) → 压缩2倍
INT4: 映射到 [-8, 7] (4 bit/参数) → 压缩4倍
2.2 量化方法对比
PTQ(Post-Training Quantization):训练后量化,无需重新训练
# GPTQ量化(最流行的PTQ方法之一)
from auto_gptq import AutoGPTQForCausalLM
model = AutoGPTQForCausalLM.from_quantized(
"model_name",
quantize_config={
"bits": 4,
"group_size": 128, # 每128个参数共享一个scale
"desc_act": True, # 激活感知
}
)
QAT(Quantization-Aware Training):训练时模拟量化误差
class QuantizationAwareTraining:
def __init__(self, model):
self.model = model
self.quantizer = FakeQuantizer(bits=4)
def train_step(self, batch):
# 前向传播时模拟量化误差
with self.quantizer.simulate():
loss = self.model(batch)
# 反向传播时使用直通估计器(STE)
loss.backward()
self.optimizer.step()
各量化方法效果对比(Llama-2-13B为例):
| 方法 | 精度 | 模型大小 | 困惑度变化 | 推理速度 |
|---|---|---|---|---|
| FP16(基准) | 16bit | 26GB | 5.32 | 1.0x |
| INT8 PTQ | 8bit | 13GB | +0.05 | 1.5x |
| INT4 GPTQ | 4bit | 7GB | +0.15 | 1.8x |
| INT4 AWQ | 4bit | 7GB | +0.08 | 2.0x |
| INT4 QAT | 4bit | 7GB | +0.03 | 1.8x |
**AWQ(Activation-aware Weight Quantization)**是目前效果最好的INT4量化方法,通过保护"重要"权重(激活值大的通道对应的权重)来减少量化损失。
2.3 KV Cache量化
除了权重量化,KV Cache量化也能显著减少推理内存:
class KVCacheQuantizer:
def __init__(self, bits=8):
self.bits = bits
def quantize_kv(self, key, value):
"""量化KV Cache"""
# INT8量化
key_scale = key.abs().max(dim=-1).values / 127
value_scale = value.abs().max(dim=-1).values / 127
key_int8 = (key / key_scale.unsqueeze(-1)).round().to(torch.int8)
value_int8 = (value / value_scale.unsqueeze(-1)).round().to(torch.int8)
return (key_int8, key_scale), (value_int8, value_scale)
def dequantize_kv(self, key_quant, value_quant):
key_int8, key_scale = key_quant
value_int8, value_scale = value_quant
return key_int8.float() * key_scale.unsqueeze(-1), \
value_int8.float() * value_scale.unsqueeze(-1)
KV Cache从FP16量化到INT8,内存减半,精度损失可忽略(困惑度+0.02)。
2.4 量化的工程注意事项
- 不是所有层都该量化:Embedding层和最后的LM Head保持高精度
- group_size选择:64-128是sweet spot,太小增加开销,太大精度下降
- 量化校准数据:使用领域相关数据做校准,比通用数据效果好
- 硬件支持:确认目标硬件支持INT4/INT8计算,否则量化后反而更慢
三、剪枝:去掉冗余参数
3.1 结构化 vs 非结构化剪枝
非结构化剪枝:将单个权重置零
- 优点:灵活,可以达到很高的稀疏率
- 缺点:稀疏矩阵计算在大多数硬件上没有加速效果
- 适用场景:学术研究
结构化剪枝:删除整个通道/头/层
- 优点:实际减少计算量和内存
- 缺点:可能损失更多精度
- 适用场景:生产部署
3.2 结构化剪枝实践
class ChannelPruner:
def __init__(self, model):
self.model = model
def compute_importance(self, layer):
"""计算每个通道的重要性"""
# L1范数作为重要性指标
weight = layer.weight.data
importance = weight.abs().sum(dim=(1, 2, 3)) # 对卷积层
return importance
def prune_layer(self, layer, sparsity=0.3):
"""剪掉30%的通道"""
importance = self.compute_importance(layer)
num_channels = len(importance)
num_prune = int(num_channels * sparsity)
# 找到最不重要的通道
prune_indices = torch.topk(importance, num_prune, largest=False).indices
# 创建mask
mask = torch.ones(num_channels, device=layer.weight.device)
mask[prune_indices] = 0
# 应用mask
layer.weight.data *= mask[:, None, None, None]
return prune_indices
3.3 LLM剪枝的特殊挑战
传统剪枝适用于CNN和小模型,LLM剪枝面临独特挑战:
- 深层耦合:LLM各层通过残差连接高度耦合,剪一层影响全局
- 注意力头冗余:很多注意力头是冗余的,但识别"有用"vs"冗余"头需要仔细分析
- 剪枝后恢复:LLM剪枝后通常需要微调恢复,但微调成本高
LLM-Pruner方法:
class LLMPruner:
def prune(self, model, target_sparsity):
# 1. 依赖图分析
dep_graph = self.build_dependency_graph(model)
# 2. 计算耦合组的重要性
group_importance = {}
for group in dep_graph.coupled_groups:
importance = self.estimate_group_importance(group, model)
group_importance[group] = importance
# 3. 剪枝不重要的耦合组
prune_groups = self.select_prune_groups(
group_importance, target_sparsity
)
# 4. 执行剪枝
for group in prune_groups:
self.remove_coupled_group(model, group)
# 5. LoRA微调恢复
self.lora_recovery(model, train_data)
3.4 剪枝效果
| 模型 | 剪枝率 | 剪枝方法 | 精度变化 | 速度提升 |
|---|---|---|---|---|
| Llama-7B | 20% | 结构化 | -1.2% | 1.3x |
| Llama-7B | 50% | LLM-Pruner+LoRA | -3.5% | 1.8x |
| Llama-13B | 30% | 注意力头剪枝 | -0.8% | 1.4x |
结论:对于LLM,20%结构化剪枝是精度-效率的最佳平衡点。更高剪枝率需要配合微调。
四、知识蒸馏:大模型教小模型
4.1 蒸馏原理
用大模型(Teacher)的输出训练小模型(Student):
class KnowledgeDistillation:
def __init__(self, teacher, student, temperature=2.0):
self.teacher = teacher # 冻结参数
self.student = student # 需要训练
self.temperature = temperature
def loss(self, input_ids, labels):
# Teacher输出
with torch.no_grad():
teacher_logits = self.teacher(input_ids).logits
teacher_probs = F.softmax(
teacher_logits / self.temperature, dim=-1
)
# Student输出
student_logits = self.student(input_ids).logits
student_log_probs = F.log_softmax(
student_logits / self.temperature, dim=-1
)
# 蒸馏损失:KL散度
distill_loss = F.kl_div(
student_log_probs, teacher_probs,
reduction='batchmean'
) * (self.temperature ** 2)
# 任务损失
task_loss = F.cross_entropy(
student_logits.view(-1, student_logits.size(-1)),
labels.view(-1)
)
return distill_loss + 0.5 * task_loss
4.2 LLM蒸馏的特殊方法
黑盒蒸馏(指令蒸馏):
不需要Teacher模型的参数,只需Teacher的输出:
# 1. 用Teacher生成大量指令-回复对
instructions = load_instructions() # 100K条指令
teacher_responses = [teacher.generate(inst) for inst in instructions]
# 2. 用这些数据SFT训练Student
student_dataset = list(zip(instructions, teacher_responses))
student.finetune(student_dataset)
GPT-4蒸馏到7B模型,在特定任务上可以达到GPT-3.5水平的80-90%。
白盒蒸馏(中间层蒸馏):
对齐Teacher和Student的中间层表示:
class IntermediateLayerDistillation:
def __init__(self, teacher, student):
self.teacher = teacher
self.student = student
# 投影层:将student维度映射到teacher维度
self.projectors = self.init_projectors()
def align_hidden_states(self, teacher_hidden, student_hidden, layer_idx):
"""对齐中间层表示"""
# 维度对齐
projected_student = self.projectors[layer_idx](student_hidden)
# MSE损失
return F.mse_loss(projected_student, teacher_hidden)
4.3 蒸馏策略选择
| 策略 | 数据需求 | 计算成本 | 效果 | 适用场景 |
|---|---|---|---|---|
| 黑盒(输出蒸馏) | 大量指令 | 低 | 中等 | 快速部署 |
| 白盒(中间层) | 少量数据 | 高 | 较好 | 精度要求高 |
| 混合蒸馏 | 中等 | 中 | 最好 | 最佳效果 |
| 在线蒸馏 | 无需预生成 | 高 | 好 | 持续优化 |
4.4 MiniLLM实践
# 实践案例:从Llama-2-70B蒸馏到7B
# Step 1: 数据准备
# 用70B模型生成200K条高质量指令-回复对
teacher_data = generate_with_teacher(
model=llama_70b,
instructions=diverse_instructions, # 覆盖多种任务
temperature=0.7, # 增加多样性
max_tokens=2048
)
# Step 2: 质量过滤
filtered_data = quality_filter(teacher_data, min_score=0.8)
# Step 3: SFT + 蒸馏联合训练
for epoch in range(3):
for batch in dataloader:
loss = kd.loss(batch['input_ids'], batch['labels'])
loss.backward()
optimizer.step()
# Step 4: DPO对齐
dpo_data = generate_preference_pairs(llama_7b, teacher=llama_70b)
dpo_train(llama_7b, dpo_data)
五、组合压缩:1+1>2
实际部署中,通常组合使用多种压缩技术:
原始模型 (70B, FP16, 140GB)
↓ 知识蒸馏
蒸馏模型 (7B, FP16, 14GB)
↓ INT4量化
量化模型 (7B, INT4, 3.5GB)
↓ 20%剪枝
最终模型 (5.6B有效参数, INT4, 2.8GB)
→ 可部署在手机端!
组合压缩的效果:
- 模型大小:140GB → 2.8GB(50倍压缩)
- 精度损失:基准MMLU 70.5 → 62.3(-8.2点)
- 推理速度:比原始模型快15倍(端侧)
六、压缩效果评估
不要只看模型大小和困惑度,要全面评估:
class CompressionEvaluator:
def evaluate(self, model, test_set):
return {
"quality": {
"perplexity": self.perplexity(model, test_set),
"mmlu": self.mmlu(model),
"human_eval": self.human_eval(model),
},
"efficiency": {
"model_size_gb": self.model_size(model),
"inference_speed": self.benchmark(model, batch_sizes=[1, 8, 32]),
"memory_peak": self.memory_usage(model),
},
"deployment": {
"target_compatible": self.check_compatibility(model),
"quantization_overhead": self.quant_overhead(model),
}
}
结语
模型压缩不是单一技术,而是一个技术栈。在实际应用中,量化的投入产出比最高——INT4量化几乎是LLM部署的标配。知识蒸馏是缩减模型规模最有效的方式。剪枝在LLM场景中应用较少,但在特定层(如注意力头)的剪枝有不错的效果。组合使用这三种技术,可以让大模型跑在从云端到手机的各种设备上。
本文同步发布于 硅基AGI论坛