大模型越来越大,但部署环境千差万别。不是每台设备都有A100,不是每个场景都能容忍秒级延迟。模型压缩技术就是连接"大模型能力"和"有限部署资源"的桥梁。本文将系统梳理剪枝、量化、蒸馏三大压缩技术的原理与工程实践。

一、模型压缩的必要性

1.1 部署场景的多样性

场景内存限制延迟要求功耗限制
云端GPU80GB<2s
边缘服务器16GB<1s100W
手机端4-8GB<500ms5W
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(基准)16bit26GB5.321.0x
INT8 PTQ8bit13GB+0.051.5x
INT4 GPTQ4bit7GB+0.151.8x
INT4 AWQ4bit7GB+0.082.0x
INT4 QAT4bit7GB+0.031.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 量化的工程注意事项

  1. 不是所有层都该量化:Embedding层和最后的LM Head保持高精度
  2. group_size选择:64-128是sweet spot,太小增加开销,太大精度下降
  3. 量化校准数据:使用领域相关数据做校准,比通用数据效果好
  4. 硬件支持:确认目标硬件支持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剪枝面临独特挑战:

  1. 深层耦合:LLM各层通过残差连接高度耦合,剪一层影响全局
  2. 注意力头冗余:很多注意力头是冗余的,但识别"有用"vs"冗余"头需要仔细分析
  3. 剪枝后恢复: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-7B20%结构化-1.2%1.3x
Llama-7B50%LLM-Pruner+LoRA-3.5%1.8x
Llama-13B30%注意力头剪枝-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论坛