目录
- 模型压缩与加速概述
- 知识蒸馏:大模型教小模型
- 模型量化:FP16 到 INT4
- 模型剪枝:去除冗余参数
- 压缩方法的协同:组合策略
- 压缩效果评估
- 实践建议与最佳实践
摘要
模型压缩与加速通过知识蒸馏、量化和剪枝的协同,同时压低模型体积、推理延迟与显存占用。
以 7B 模型为基准,蒸馏到 3B 再叠加 INT4 量化与 20% 结构化剪枝,权重体积从 14GB 降至约 1.2GB,吞吐从 20 tokens/s 升至 90 tokens/s,MMLU 精度从 63.2% 回落至 61.6%。
不同方法的执行顺序决定最终精度损失落在 2% 还是 5%,本文给出可复现的组合策略与评估方法。
1. 模型压缩与加速概述
模型压缩与加速的核心目标是让同样的模型跑在更小的内存、更快的
延迟和更低的成本上。三种主要手段各有分工:知识蒸馏改变模型结构,
量化改变数值表示,剪枝去除冗余权重。本节先回答三个问题:为什么
必须压缩、压缩要付什么代价、以及有哪些方法可以选。
1.1 压缩的必要性
部署一个未压缩的 7B 模型,权重本身在 FP16 下占 14GB,加上激活
和 KV 缓存,单实例实测峰值在 17-19GB。一张 80GB 的 A100 只能
同时驻留 4 个实例;换到 32GB 显存的 V100,权重加运行时开销
已经逼近上限,批处理大小被压到 1。边缘设备(8GB 内存)和移动端
(6GB)则完全无法加载未压缩权重,这类设备只能跑 3B 以下且
经过压缩的模型。
延迟和成本是第二重约束。7B FP16 模型在 A100 上单条请求的生成
速度约 20 tokens/s,用户可感知的流畅线通常在 40 tokens/s 以上。
处理 4096 token 的长上下文时,预填充阶段的首 token 延迟达到
300-500ms,交互式体验已经出现明显停顿。按当前云 GPU 时租价格
折算,支撑 100 路并发需要约 25 张 A100;压缩到 INT4 后单卡
并发翻倍,需要的卡数降到 5 张以内,月度成本差距在 5 倍左右。
三类约束驱动的压缩需求可以归纳如下:
- 内存约束:权重、激活、KV 缓存三者叠加超出显存预算,
只能降精度或减参数量 - 延迟约束:交互式场景要求首 token 低于 200ms、生成速率
不低于 40 tokens/s,需降低计算量 - 成本约束:按吞吐计价,单位 token 成本与占用 GPU 数量和
时长成正比,需提高单卡利用率
不同精度下同一个模型的存储占用,按 0.5 字节每参数(INT4)到
4 字节每参数(FP32)计算:
| 模型 | 参数量 | FP32 | FP16 | INT8 | INT4 |
|---|---|---|---|---|---|
| 7B | 7e9 | 28GB | 14GB | 7GB | 3.5GB |
| 13B | 1.3e10 | 52GB | 26GB | 13GB | 6.5GB |
| 70B | 7e10 | 280GB | 140GB | 70GB | 35GB |
从表格可以看出,INT4 相对 FP16 体积减小 75%,这是把 70B 模型
压进 4 卡 80GB 显存的唯一路径。压缩还有额外收益:权重减少意味着
显存带宽占用下降,而 LLM 生成阶段是带宽受限的,体积缩小通常
直接换算成吞吐提升,两者近似线性关系。
实测案例给出数量级参考。某推理服务把 13B FP16 压到 INT4 后,
单卡并发从 8 路升到 32 路,单位 token 成本下降 72%。另一个
边缘盒子的案例是蒸馏加 INT8 的 3B 模型,从完全无法运行变成
24ms 内响应,整机功耗控制在 15W 以内。这两个案例说明压缩的
收益同时落在成本和时延两条线上。
边界同样存在:压缩解决显存与吞吐,但不改变模型本身的能力上限。
对 GSM8K 这类多步推理任务,量化后的精度损失会被放大,压缩前的
任务难度决定压缩后的损失上界。任务越难、越依赖长链路推理,
压缩比例就越要保守。压缩是部署优化手段,不是能力增强手段,
两者的边界在立项时就要写清楚。
# 来源:自实现 / memory_footprint.pyfromdataclassesimportdataclass@dataclassclassModelFootprint:"""按精度计算模型权重存储开销,单位字节每参数。"""num_params:intbytes_per_weight={"fp32":4,"fp16":2,"int8":1,"int4":0.5}defsize_gb(self,dtype:str)->float:ifdtypenotinself.bytes_per_weight:raiseValueError(f"不支持的精度{dtype}")returnself.num_params*self.bytes_per_weight[dtype]/1024**3defshrink(self,base:str,target:str)->float:"""返回目标精度相对基准精度的体积缩减比例。"""return1.0-self.size_gb(target)/self.size_gb(base)if__name__=="__main__":m=ModelFootprint(num_params=7_000_000_000)fp16=m.size_gb("fp16")int4=m.size_gb("int4")print(f"7B FP16 占用{fp16:.1f}GB,INT4 占用{int4:.1f}GB")print(f"INT4 相对 FP16 体积减小{m.shrink('fp16','int4')*100:.0f}%")1.2 压缩的权衡
压缩是在体积、速度、精度三个目标之间寻找可行域,三种方法在
三个维度上的代价分布并不相同。下表给出典型区间,数据来自公开
实现和常见部署报告,具体值随模型规模与硬件上下浮动:
| 方法 | 体积缩减 | 速度提升 | 精度损失 | 额外成本 |
|---|---|---|---|---|
| 知识蒸馏 | 2-10 倍 | 1.5-3 倍 | 1-3% | 需重新训练,成本高 |
| INT8 量化 | 2 倍 | 1.5-2 倍 | 小于 0.5% | 校准数据,分钟级 |
| INT4 量化 | 4 倍 | 2-3 倍 | 1-2% | 需校准与算子支持 |
| 结构化剪枝 20% | 1.25 倍 | 1.5-2 倍 | 3-5% | 需微调恢复精度 |
权衡的本质是不同应用对三个目标的权重不同。交互式对话场景把首
token 延迟当作硬约束,量化是首选,因为它在不动网络结构的前提
下把计算压下来,工程量最小。离线批处理场景优先看吞吐和单位成本,
可以接受 3-5% 的精度损失换取更高的压缩比,剪枝在这个场景下
更常用。医疗和法务场景对精度敏感,损失超过 2% 就不可接受,
只能选择蒸馏加 INT8 的组合,并放弃 INT4。
架构层面的权衡在于改动范围。量化改动的是数值表示,影响全部算子,
必须配套推理内核才能兑现加速,否则只省显存不省时间,吞吐反而
因反量化开销下降。蒸馏改动的是模型结构,小模型是全新网络,
需要完整训练流程,模型体积的收益来自结构而不是数值。剪枝改动的
是张量形状,结构化剪枝让矩阵变窄,任何推理框架都能直接受益,
非结构化剪枝则依赖稀疏算子库,收益受硬件支持制约。
方法之间并不独立,这一点常被忽略。蒸馏得到的更小模型权重分布
更规整,后续量化的精度损失通常更小,两项叠加优于单独使用之和。
先量化再剪枝时,量化噪声和剪枝误差会叠加,精度损失可能超过
单独使用之和。这解释了为什么第 5 章要把执行顺序纳入决策,
而不是简单地叠加三种方法。
精度损失的阈值选择有实测依据。量化精度损失在 1-2% 区间时,
人工对比评测基本无感知;超过 3% 后,长文本生成的连贯性和代码
任务的编译通过率会出现可观察的质量回落。多步推理任务的阈值
要更严,GSM8K 类任务建议把回落上限设在 2%。权衡的产出是一份
决策表:每个候选方案对应体积、速度、精度三列数字,上线时直接
对照阈值勾选。
# 来源:自实现 / tradeoff.pyfromcollectionsimportnamedtuple Tradeoff=namedtuple("Tradeoff",["name","size_ratio","speedup","acc_drop"])CANDIDATES=[Tradeoff("fp16 基线",1.0,1.0,0.0),Tradeoff("int8 量化",0.5,1.8,0.5),Tradeoff("int4 量化",0.25,2.6,1.5),Tradeoff("蒸馏到 3B",0.43,1.7,1.0),Tradeoff("三管齐下",0.09,4.5,3.5),]defrank_tradeoffs(w_size:float,w_speed:float,w_acc:float):"""按加权代价排序,权重越大代表该维度越重要。"""scored=[]forcinCANDIDATES:cost=w_size*c.size_ratio+w_speed/c.speedup+w_acc*c.acc_drop scored.append((cost,c.name))scored.sort()returnscoredif__name__=="__main__":# 边缘部署场景:体积权重 0.6,速度 0.3,精度 0.1forcost,nameinrank_tradeoffs(0.6,0.3,0.1):print(f"{name}: 综合代价{cost:.3f}")1.3 压缩方法的分类
压缩方法按两个维度分类:是否需要训练、是否改变模型结构。训练时
压缩把压缩目标纳入损失函数,典型代表是知识蒸馏、量化感知训练
(QAT)、剪枝加微调;训练后压缩直接处理已训练权重,典型代表是
训练后量化(PTQ)和一次性剪枝。结构级压缩改变网络形状,数值级
压缩只改变数值表示,两者的推理框架要求完全不同。
| 类别 | 代表方法 | 是否需要重训 | 是否改变结构 | 是否需要数据 |
|---|---|---|---|---|
| 训练时压缩 | 蒸馏、QAT、剪枝加微调 | 是 | 是 | 需要完整数据集 |
| 训练后压缩 | PTQ、SparseGPT、GPTQ | 否 | 部分 | 少量校准数据 |
| 混合流程 | 蒸馏后量化、量化后剪枝 | 部分 | 是 | 各阶段分开配给 |
分类的意义在于决定压缩流程放在模型生命周期的哪个阶段。训练后
压缩可以直接复用已经训练好的权重和验证结论,PTQ 在单卡上几分钟
完成,SparseGPT 剪掉 50% 权重也只需约 5 分钟,代价是精度损失
受权重分布限制。训练时压缩把精度损失写进优化目标,效果更好,
但需要 GPU 训练预算:7B 模型在 8 卡 A100 上完成一次完整训练的
成本量级在数万美元,多数团队先走训练后压缩,精度不达标再升级
到训练时方案。
方法间还有隐含的依赖关系。量化感知训练本身需要完整的训练管线;
蒸馏后的学生模型是全新权重,需要从头走训练流程;剪枝加微调
需要保留原始训练数据,数据缺失时只能退化为一次性剪枝。因此
分类表同时是资源需求表,选择方法之前先核对数据可得性和算力预算,
这两项往往是压缩项目的真正瓶颈。
演进趋势是把更多工作移到训练后阶段。GPTQ 用 128 条校准数据
完成 7B 模型的 INT4 量化,AWQ 用激活统计挑选敏感通道,SparseGPT
无需微调完成 50% 稀疏化。训练后压缩成本低、可重复、可自动化,
是当前压缩工具链的主线。分类边界也在变模糊,LoRA 低秩适配
本质上也是一种参数压缩,只是把压缩目标从模型本身移到了适配器上,
这说明分类是分析工具,不是固定标签。
边界分析指出一个常见误区:把方法分类当作能力等级排序。训练时
压缩并不总是优于训练后压缩,小规模模型上 PTQ 的精度损失接近
QAT 的一半水平,而成本低两个数量级。正确的做法是按目标压缩比
分层尝试:4 倍以内用训练后压缩,8 倍以上再引入训练时方法,
每次切换都以实测数据为准。
# 来源:自实现 / classify.pyMETHOD_TABLE=[{"name":"知识蒸馏","phase":"训练时","structure":True,"retrain":True},{"name":"量化感知训练","phase":"训练时","structure":False,"retrain":True},{"name":"训练后量化","phase":"训练后","structure":False,"retrain":False},{"name":"结构化剪枝加微调","phase":"训练时","structure":True,"retrain":True},{"name":"SparseGPT 剪枝","phase":"训练后","structure":True,"retrain":False},]defclassify_methods():"""按训练阶段分组,并统计各组的结构改动占比。"""groups={}forrowinMETHOD_TABLE:groups.setdefault(row["phase"],[]).append(row)returngroupsif__name__=="__main__":forphase,rowsinsorted(classify_methods().items()):names=[r["name"]forrinrows]structure=sum(1forrinrowsifr["structure"])print(f"{phase}({len(rows)}种,结构改动{structure}种):{'、'.join(names)}")2. 知识蒸馏:大模型教小模型
知识蒸馏用大模型(教师)的输出分布去指导小模型(学生)的训练,
让学生在不改变推理结构的前提下继承教师的能力。对 70B 到 7B 的
蒸馏,学生能达到教师 90-95% 的基准分数,而推理成本按参数量比例
下降约 10 倍。本节拆解蒸馏原理、黑盒与白盒两种路径,以及效果
随教师学生差距变化的规律。