1. 项目背景与目标定义
在端侧设备上部署大语言模型,一直是团队降本增效的重要方向。过去一年,我们围绕一款面向移动端的智能助手产品,尝试将 7B 级模型压缩到可在中端手机上流畅运行。整个过程中经历了多次失败与调优,最终沉淀出一套可复用的量化与剪枝实操流程。本文以这次真实项目为案例,完整复盘从问题定义、方案选型、落地踩坑到最终效果的全过程。
2. 基线方案设计与首次踩坑
2.1 基线方案:PTQ 均匀量化
项目启动时,团队直接选用开源 7B 模型,计划通过 PTQ(训练后量化)将权重从 FP16 压缩到 INT8,期望内存占用直接减半。当时评估认为,端侧推理引擎对 INT8 支持成熟,这条路径实施成本最低。
2.2 问题定位:精度崩坏
实际部署后,模型在部分任务上出现明显精度下降,尤其是代码生成和数学推理场景,输出质量基本不可用。定位发现两个核心问题:
- 模型存在明显的敏感层,直接均匀量化导致这些层误差被放大;
- 校准数据集与真实业务分布偏差过大,量化参数估计不准。
2.3 复盘小结
第一次尝试的教训是:量化不是简单的位宽压缩,而是误差分配问题。均匀量化忽略了层间敏感度差异,必须引入更精细的策略。
案例复盘:代码生成场景的精度崩坏
以我们产品中的「代码补全」功能为例,第一次部署 INT8 模型后,该功能基本不可用。原本能正确补全的 for 循环、函数签名等场景,输出变成了语法错误或逻辑混乱的代码。我们定位到问题出在模型的 lm_head 层和部分深层 FFN 层——这些层对数值精度极其敏感,均匀量化后误差被显著放大。
解决思路:我们先用逐层敏感度分析脚本定位出误差最大的 5 个层,对它们单独保留 FP16,其余层使用 INT8。同时,将校准数据从通用文本替换为真实的代码片段(含 Python、Java、SQL 三类),重新计算量化参数。
复盘总结:这次案例让我们认识到,敏感层识别不能靠经验猜测,必须用数据驱动的方式逐层量化评估;同时校准集必须覆盖目标场景的真实分布,否则量化参数会严重失真。
3. 量化方案演进与二次复盘
3.1 从 PTQ 转向混合精度量化
针对敏感层问题,我们引入混合精度量化:对敏感层保留 FP16,对非敏感层使用 INT8。通过逐层评估量化误差,确定每层的合适位宽。
import torch
def evaluate_layer_sensitivity(model, calib_loader, layer_name):
"""评估指定层对量化误差的敏感度"""
model.eval()
original_state = model.state_dict()
errors = []
for batch in calib_loader:
with torch.no_grad():
# 记录原始输出
original_out = model(batch)
# 对该层做 INT8 量化模拟
layer = dict(model.named_modules())[layer_name]
quantized_layer = torch.quantization.quantize_dynamic(
layer, {torch.nn.Linear}, dtype=torch.qint8
)
# 替换并计算误差
quantized_out = model(batch)
errors.append((original_out – quantized_out).abs().mean().item())
return sum(errors) / len(errors)
3.2 校准数据重构
第二次复盘发现,校准集必须贴近真实业务。我们重新从线上日志中采样,构建了覆盖对话、摘要、代码三类任务的校准集,并加入领域专属样本。
3.3 二次复盘结论
混合精度量化将精度损失控制在可接受范围,但模型体积下降有限。此时我们意识到,单纯量化无法满足端侧内存预算,必须叠加剪枝。
案例复盘:对话摘要任务的校准集重构
在混合精度量化落地时,我们最初沿用公开的 WikiText 作为校准集,结果模型在「对话摘要」任务上表现不佳——摘要内容经常丢失关键实体。复盘发现,公开语料与真实对话的句式、长度、专有名词分布差异巨大。
解决思路:我们从线上日志中随机采样 5000 条真实对话,按「用户提问—助手回复」结构切分,并人工标注其中的关键实体(人名、地名、产品名)。用这批数据重新生成校准集后,量化参数的分布估计显著更贴合业务。
复盘总结:校准数据的质量直接决定量化效果的上限。与其追求数据量,不如优先保证数据与线上业务分布的一致性。此后我们建立了「校准集月度更新」机制,持续从线上日志补充新样本。
4. 剪枝方案设计与三次复盘
4.1 结构化剪枝 vs 非结构化剪枝
非结构化剪枝虽然压缩率高,但稀疏矩阵在端侧 CPU 上难以获得实际加速。我们最终选择结构化剪枝,按通道维度裁剪,确保剪枝后仍能利用常规矩阵运算库。
import torch.nn.utils.prune as prune
def structured_channel_prune(model, layer_name, amount):
"""对指定卷积层做通道级结构化剪枝"""
layer = dict(model.named_modules())[layer_name]
# 使用 L1 范数衡量通道重要性
prune.ln_structured(
layer,
name="weight",
amount=amount,
n=1,
dim=0 # 按输出通道裁剪
)
prune.remove(layer, "weight") # 固化剪枝结果
return model
4.2 剪枝与量化的顺序问题
第三次踩坑出现在顺序上。我们先剪枝再量化,结果精度二次下降。复盘发现,剪枝改变了权重分布,后续量化需要重新校准。最终调整为:先量化感知训练,再剪枝,最后做量化校准。
4.3 三次复盘结论
剪枝与量化必须联合设计,不能当作两个独立步骤简单串联。顺序、校准、微调三者需要整体编排。
案例复盘:剪枝顺序导致的精度二次下降
在剪枝落地时,我们曾尝试「先剪枝、再量化」的流水线,结果在数学推理任务上精度再次明显下滑。复盘发现,剪枝改变了剩余通道的权重分布,此时再套用之前基于原始权重计算的量化参数,误差被二次放大。
解决思路:我们调整流程为「先做量化感知训练(QAT),再执行结构化剪枝,最后基于剪枝后的权重重新做量化校准」。同时,在剪枝后加入 200 步的轻量微调,帮助模型适应新的权重分布。
复盘总结:剪枝与量化不是两个独立步骤,而是一个联合优化问题。顺序、校准、微调必须整体编排,任何一步单独执行都可能破坏前一步的成果。这个案例也让我们建立了「每步操作后必须验证精度」的强制检查点。
5. 端侧推理引擎适配与四次复盘
5.1 算子支持瓶颈
模型压缩完成后,部署到端侧推理引擎时又遇到新问题:部分自定义算子不被端侧引擎支持,导致无法直接加载。我们不得不对模型结构做等价改写,用标准算子替换自定义实现。
5.2 内存与延迟实测
在目标中端机型上实测,模型内存占用从 14GB 降至 3.2GB,首 token 延迟从 2.8 秒降至 0.9 秒。但发热和耗电仍偏高,需要进一步优化。
5.3 四次复盘结论
端侧部署不是模型压缩的终点,算子兼容性与运行时资源消耗同样决定成败。压缩策略必须与目标推理引擎的能力对齐。
案例复盘:自定义算子的端侧适配
模型压缩完成后,我们在端侧引擎加载时遇到算子不兼容问题——模型中的 RotaryPositionEmbedding 自定义实现无法被目标引擎识别,导致模型直接加载失败。
解决思路:我们没有重写引擎,而是对模型结构做等价改写:将自定义的旋转位置编码拆解为标准的 reshape + matmul + add 算子组合,并验证改写前后输出完全一致(误差 < 1e-6)。同时,将模型中其他自定义激活函数替换为引擎已支持的 gelu 近似实现。
复盘总结:压缩方案必须与目标推理引擎的能力对齐。在模型设计阶段就应调研端侧引擎支持的算子清单,避免在部署阶段才做结构改写。这次案例也促使我们建立了「算子兼容性预检」流程,在模型导出前自动扫描不兼容算子。
6. 最终方案与效果总结
6.1 完整落地流程
经过四轮复盘,最终沉淀出如下标准流程:
#mermaid-svg-mmPjDfsqpGatW76r{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mmPjDfsqpGatW76r .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mmPjDfsqpGatW76r .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mmPjDfsqpGatW76r .error-icon{fill:#552222;}#mermaid-svg-mmPjDfsqpGatW76r .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mmPjDfsqpGatW76r .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mmPjDfsqpGatW76r .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mmPjDfsqpGatW76r .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mmPjDfsqpGatW76r .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mmPjDfsqpGatW76r .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mmPjDfsqpGatW76r .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mmPjDfsqpGatW76r .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mmPjDfsqpGatW76r .marker.cross{stroke:#333333;}#mermaid-svg-mmPjDfsqpGatW76r svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mmPjDfsqpGatW76r p{margin:0;}#mermaid-svg-mmPjDfsqpGatW76r .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-mmPjDfsqpGatW76r .cluster-label text{fill:#333;}#mermaid-svg-mmPjDfsqpGatW76r .cluster-label span{color:#333;}#mermaid-svg-mmPjDfsqpGatW76r .cluster-label span p{background-color:transparent;}#mermaid-svg-mmPjDfsqpGatW76r .label text,#mermaid-svg-mmPjDfsqpGatW76r span{fill:#333;color:#333;}#mermaid-svg-mmPjDfsqpGatW76r .node rect,#mermaid-svg-mmPjDfsqpGatW76r .node circle,#mermaid-svg-mmPjDfsqpGatW76r .node ellipse,#mermaid-svg-mmPjDfsqpGatW76r .node polygon,#mermaid-svg-mmPjDfsqpGatW76r .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mmPjDfsqpGatW76r .rough-node .label text,#mermaid-svg-mmPjDfsqpGatW76r .node .label text,#mermaid-svg-mmPjDfsqpGatW76r .image-shape .label,#mermaid-svg-mmPjDfsqpGatW76r .icon-shape .label{text-anchor:middle;}#mermaid-svg-mmPjDfsqpGatW76r .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mmPjDfsqpGatW76r .rough-node .label,#mermaid-svg-mmPjDfsqpGatW76r .node .label,#mermaid-svg-mmPjDfsqpGatW76r .image-shape .label,#mermaid-svg-mmPjDfsqpGatW76r .icon-shape .label{text-align:center;}#mermaid-svg-mmPjDfsqpGatW76r .node.clickable{cursor:pointer;}#mermaid-svg-mmPjDfsqpGatW76r .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mmPjDfsqpGatW76r .arrowheadPath{fill:#333333;}#mermaid-svg-mmPjDfsqpGatW76r .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mmPjDfsqpGatW76r .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mmPjDfsqpGatW76r .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mmPjDfsqpGatW76r .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mmPjDfsqpGatW76r .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mmPjDfsqpGatW76r .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mmPjDfsqpGatW76r .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mmPjDfsqpGatW76r .cluster text{fill:#333;}#mermaid-svg-mmPjDfsqpGatW76r .cluster span{color:#333;}#mermaid-svg-mmPjDfsqpGatW76r div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mmPjDfsqpGatW76r .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mmPjDfsqpGatW76r rect.text{fill:none;stroke-width:0;}#mermaid-svg-mmPjDfsqpGatW76r .icon-shape,#mermaid-svg-mmPjDfsqpGatW76r .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mmPjDfsqpGatW76r .icon-shape p,#mermaid-svg-mmPjDfsqpGatW76r .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mmPjDfsqpGatW76r .icon-shape .label rect,#mermaid-svg-mmPjDfsqpGatW76r .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mmPjDfsqpGatW76r .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mmPjDfsqpGatW76r .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mmPjDfsqpGatW76r :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
步骤1:逐层评估敏感度,确定混合精度量化方案(对应案例一:代码生成场景的精度崩坏)
步骤2:基于业务日志构建贴近真实分布的校准集(对应案例二:对话摘要任务的校准集重构)
步骤3:执行结构化通道剪枝,控制稀疏度在 30% 以内(对应案例三:剪枝顺序导致的精度二次下降)
步骤4:剪枝后重新量化校准,必要时做少量微调(对应案例三:剪枝顺序导致的精度二次下降)
步骤5:针对目标端侧引擎改写不兼容算子(对应案例四:自定义算子的端侧适配)
步骤6:在真机上验证内存、延迟、功耗与精度四项指标(对应案例四:自定义算子的端侧适配)
6.2 最终效果
| 模型内存占用 | 14 GB | 3.2 GB | 77% |
| 首 token 延迟 | 2.8 s | 0.9 s | 68% |
| 平均精度 | 基准 | -1.8% | 可接受 |
6.3 复盘总结
附录:精度验证与问题排查清单
为便于后续项目快速定位问题,我们将四轮复盘中的关键信息沉淀为一张排查清单,覆盖每个案例的失败现象、根因定位方法、解决动作与验证指标。
| 案例一:代码生成场景的精度崩坏 | 代码补全输出语法错误、逻辑混乱,功能基本不可用 | 逐层敏感度分析脚本,定位 lm_head 与深层 FFN 层误差放大 | 敏感层保留 FP16,其余层 INT8;校准集替换为真实代码片段 | 代码补全精度恢复至可用水平,误差最大的 5 个层单独保留 FP16 |
| 案例二:对话摘要任务的校准集重构 | 摘要丢失关键实体,输出质量不佳 | 对比公开语料与真实对话的分布差异,定位校准集偏差 | 从线上日志采样 5000 条真实对话,人工标注关键实体后重建校准集 | 摘要关键实体召回率显著提升,量化参数分布贴合业务 |
| 案例三:剪枝顺序导致的精度二次下降 | 数学推理任务精度再次明显下滑 | 对比剪枝前后权重分布,确认量化参数基于原始权重计算导致误差二次放大 | 调整为「QAT → 结构化剪枝 → 重新量化校准」,剪枝后加 200 步轻量微调 | 数学推理精度恢复,稀疏度控制在 30% 以内 |
| 案例四:自定义算子的端侧适配 | 模型加载失败,RotaryPositionEmbedding 不被引擎识别 | 导出前扫描算子兼容性,定位不兼容的自定义算子 | 将旋转位置编码拆解为 reshape + matmul + add 标准算子组合,替换自定义激活函数 | 模型可正常加载,改写前后输出误差 < 1e-6 |
通用排查建议
基于以上四轮复盘,我们沉淀出三条可复用的排查建议,建议在后续项目中作为强制检查点执行:
- 每步操作后强制验证精度:无论是量化、剪枝还是算子改写,每一步完成后都要跑一遍目标任务的精度评测,避免误差在流水线中逐级累积、到最终阶段才暴露。
- 校准集月度更新:业务分布会随时间漂移,校准集不能一劳永逸。建议建立月度更新机制,持续从线上日志补充新样本,保证量化参数始终贴合真实业务。
- 算子兼容性预检前置:在模型设计阶段就调研目标端侧引擎支持的算子清单,并在模型导出前自动扫描不兼容算子,避免在部署阶段才做结构改写,大幅降低返工成本。
这次端侧大模型降本实践,核心收获可以归纳为三点:
- 量化与剪枝必须联合设计,顺序和校准策略直接影响最终精度;
- 校准数据要贴近真实业务,脱离业务分布的量化参数不可靠;
- 端侧引擎的算子兼容性是硬约束,压缩方案要提前对齐部署目标。



