☰
非结构化剪枝实战:6B大模型本地部署与效果评估
2026/10/21 2:40:45 网站建设 项目流程

干过本地部署大模型这活的人,对显存基本都有执念。6B参数的对话模型,FP16精度下光权重就要12GB显存,推理时KV cache、中间激活再叠上去,一张16GB消费卡跑长一点对话就开始抖。量化确实能省,但我这次想换个思路,试试给模型做“非结构化剪枝”——而且是尽量不动脑筋、偏暴力的那种剪法。

这个标题写的是“简单非结构化剪枝”,本质就是把权重矩阵里那些绝对值很小、对输出贡献微乎其微的值直接置成零。整个过程不涉及低秩分解、不重建动态稀疏训练、不改模型结构,就是纯纯的“减法”。适合谁参考呢?学过Transformer基础、会加载开源模型、想了解模型压缩但又不想一上来啃论文的人。下面把我这次尝试的完整过程、代码、数据和我自己踩的坑都摊开说。

1. 为什么选非结构化剪枝

1.1 三条压缩路线的取舍

先拆一下背景。现在给大模型瘦身,主流就三个方向:量化、结构化剪枝、非结构化剪枝。

量化是改权重存储的精度,效果最直观,4bit量化经常能把显存压到三分之一,很多推理框架直接支持。结构化剪枝是砍掉一整个注意力头、一整个FFN中间层,结构完整,理论上还能加速,但工程复杂度高,而且微调恢复的成本不低。非结构化剪枝是这次的主角——它不动结构,只把权重矩阵里“不太重要”的单个数值干掉。

看到这里你可能会问:既然量化效果那么猛,为什么还要剪枝?因为剪枝和量化是可以叠加的,剪完以后再量化,精度掉得往往比单独上高精度压缩更平缓。另外,稀疏率这个指标在部署到某些支持稀疏计算的硬件上很有价值。我这次尝试的主要目标,是先看“简单剪枝”在6B模型上到底能压到多少而不出现明显的人格崩坏。

三种路线放一张表里对比:

压缩方式改动对象是否需要微调恢复推理加速实现难度
4bit量化权重精度通常不用明显中
结构化剪枝注意力头/层通常需要明显高
非结构化剪枝单个权重最好需要取决于算子低

需要特别提醒:非结构化剪枝如果只看“推理速度”,在普通PyTorch环境里表现并不理想,原因放到4.3节专门说。

1.2 给“简单尝试”定个边界

既然标题叫“简单”,我就没打算做训练敏感度分析,也不打算写CUDA算子,只做最小闭环:加载模型 -> 按阈值或比例稀疏化 -> 保存 -> 重新加载 -> 评估。

这个边界很重要。如果不刻意收敛,很容易滚进SparseGPT、Wanda这些后续方法的兔子洞。先跑通一个最朴素的版本,效果好说明模型内部确实有冗余,效果不好也能知道问题出在哪。我的目标函数很简单:把稀疏度从10%逐步加到20%、30%、50%,分别看评估指标和对话可用度,找到临界点。

2. 环境准备与基线建立

2.1 环境与模型加载

我这边环境是Python 3.10、PyTorch 2.1.2、CUDA 12.1、transformers 4.37.2。这个模型早期加载一直依赖自定义代码的方式,新版transformers已经能原生加载,但为了保险我依旧开了trust_remote_code=True,省得遇到老兼容问题。

第一件事是确认加载方式。显存只有16GB,FP16全精度加载大约12GB,再留一部分给推理时的KV cache,刚好勉强能跑。为了隔离变量,剪枝前先完整跑一次基线,记录PPL和几个典型问题的回答质量。

2.2 剪枝前的两个基线指标

我用的第一个指标是困惑度PPL,即模型对一段连续文本的平均负对数似然,越低越好。PPL虽然不完全等价于对话质量,但它能非常灵敏地反映模型“认知连贯性”有没有被破坏。

计算方式可以直接用transformers的loss输出:

import math import torch from transformers import AutoTokenizer, AutoModel model_path = "your_local_chatglm3_6b_path" tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) model = AutoModel.from_pretrained( model_path, trust_remote_code=True, torch_dtype=torch.float16, ).cuda().eval() text = "那是一个盛夏的午后,蝉鸣穿过梧桐树浓密的树荫,老街上卖西瓜的老人正把最后一个瓜搬上板车。孩子们在巷口追逐打闹,空气中飘着刚切开的西瓜那种清甜的味道。" inputs = tokenizer(text, return_tensors="pt").to("cuda") with torch.no_grad(): outputs = model(**inputs, labels=inputs["input_ids"]) loss = outputs.loss ppl = math.exp(loss.item()) print("Baseline PPL:", round(ppl, 4))

这一段约500字,我测出来的基线PPL是4.89。这个值本身只对当前文本有意义,但作为后面剪枝的横向对比基准是够用的。

第二个指标是不用量化的对话测试。我固定了几个问题模板:一道数学题、一个代码生成、一个常识问答、一段开放闲聊。后面不管剪到多少比例,都用同一套问题去问,效果变化一目了然。

3. 剪枝实现与参数选择的完整过程

3.1 剪哪些层,不剪哪些层

模型内部权重类型很多,剪枝目标要挑“对结果影响相对可控”的那部分。我的策略是只处理Linear层的weight矩阵,具体到ChatGLM3-6B,就是注意力里的QKV投影和输出投影,以及FFN里的两个全连接权重。

有两类权重我坚决不碰:一是embedding层,它是词表和token之间的索引映射,剪掉任何一个位置都直接损失对应token的表达;二是所有LayerNorm和bias,这些参数数量少但承担全局的缩放和偏移作用,动它们容易让输出分布整体漂移。

打个比方:embedding像字典,你把某个词的解释划掉,这个词就彻底无家可归了;而权重矩阵里的零散小值,顶多是表达某个技能时少了一点素材,其他冗余还能补回来。

3.2 阈值怎么算

最简单的剪枝就是“绝对值阈值法”:权重绝对值低于某个阈值的,直接置零。但如果对每个矩阵都用同一个绝对阈值,其实不太公平,因为不同层、不同位置的权重分布尺度可能差好几倍。更通用的做法是“按比例稀疏”:先对目标权重算绝对值,然后取某个分位数作为阈值。

实际操作时我不会把所有层的绝对值cat成一个巨大向量再算分位数,那样内存太紧张。我选择分层分矩阵算,因为这样更稳,也更平衡。稀疏度30%,就在当前矩阵的绝对值分布里找30%分位点,把所有小于这个值的权重杀掉。

3.3 核心剪枝代码

这是这次尝试最核心的一段代码:

import torch def get_threshold_by_sparsity(param, sparsity): w = param.data if w.dim() < 2: return None flat = w.abs().flatten() k = max(1, int(flat.numel() * sparsity)) threshold = flat.topk(k, largest=False).values[-1] return threshold def prune_linear_weight(param, sparsity): if param.data.dim() < 2: return threshold = get_threshold_by_sparsity(param, sparsity) if threshold is None: return mask = param.data.abs() > threshold param.data.mul_(mask) return mask for name, param in model.named_parameters(): if "weight" not in name: continue if "embedding" in name or "norm" in name: continue if "bias" in name or param.data.dim() < 2: continue prune_linear_weight(param, sparsity_ratio)

这里用topk而不是torch.quantile,是因为topk在老版本PyTorch里对GPU大张量支持更稳定。topk取第k个最小值,等价于求分位数,跑得也快。

剪完以后直接原地mul_置零。为了让mask可复用、方便反悔,我也会顺手把mask存下来。这里有个经验:每次实验前先重新加载干净模型,不要在一个模型上反复叠加实验。否则前一次剪枝留下的“残缺”会和后一次混在一起,得到的结果完全不可信。

3.4 权重的保存与加载

剪枝完成后,直接保存合并后的权重文件即可,这样后续再加载就不用依赖mask文件:

model_cpu = model.cpu() model_cpu.save_pretrained("./chatglm3_6b_pruned_30pct", safe_serialization=True)

这里有大坑:如果你保存的是剪枝后的原始模型,它和原模型结构的权重shape完全一样,只是某些值变成了0,所以模型文件大小几乎不会缩小,因为保存的仍然是稠密参数。想真正缩小体积,得把稀疏部分单独用稀疏格式存储,或者配合压缩。这个坑我放到5.1节再细说。

4. 剪枝后效果评估:PPL、对话质量与显存分析

4.1 困惑度PPL的变化

我在5个稀疏度档位下分别做了PPL测试,评估文本固定不变,模型采样参数保持默认。结果如下:

稀疏度PPL相对基线变化
0%(基线)4.89-
10%4.96+1.4%
20%5.28+8.0%
30%5.61+14.7%
50%8.72+78.3%

这个数据很典型:小比例剪枝时PPL只涨一点,模型基本感知不到;超过30%以后PPL开始加速恶化,50%几乎就是“强行砍半”的极限状态。如果你的场景对生成质量要求不算苛刻,30%左右是一个比较可接受的临界点。

为了避免单条文本的偶然性,我后来建议自己换两三段不同题材的文本做PPL,取平均值作为结论。这次只测了一段长文,数据偏差问题在5.2节有补充说明。

4.2 对话效果对比

量化指标归量化指标,对话到底还能不能正常聊,得亲口问。

问“鸡兔同笼,笼里有35个头,94只脚,鸡和兔各几只?”基线能一步步列方程解出来;剪枝30%后的模型思路是对的,但中间第2步代数化简出现了一个小错误。这说明基本语言能力和推理框架还在,复杂执行的精度变差了。

问“写一个冒泡排序Python函数并调用”,剪枝后代码结构没有乱,但print输出漏了一个逗号,边界条件的while range写错了。它没有胡编乱造,只是会在细节上偷工减料。

问“水在标准大气压下多少度沸腾”,基线回答100℃,剪枝后给的是99.5℃。这个很有意思——剪枝后模型倾向于把精确数字模糊化,大方向对,但你明显能感受到“自信程度”下降了。

综合来看,10%到20%稀疏度几乎不影响对话体验;30%开始能明显观察到“记性变差”和“算术不过脑子”的迹象,但还在可接受范围;50%已经不适合直接对外提供服务。

4.3 显存和推理速度的真实变化

这里必须说破一个很多人容易误解的事:非结构化剪枝之后,如果还用PyTorch默认算子做推理,内存占用和计算速度基本不会改善。权重矩阵只是把某些位置变成0,但它仍然是一个稠密矩阵,显卡在矩阵乘法时根本不会跳过0,还是照单全收。

我在16GB卡上实测,剪枝前后的峰值显存几乎一样,都在13.4GB左右,推理速度也差别不大。你甚至会观察到速度略微下降,因为稀疏矩阵会破坏部分张量core的访问连续性。

真的想从非结构化剪枝里拿到性能收益,有两个方向:一是换成硬件友好的半结构化模式,比如2:4稀疏,配合专门算子;二是把剪枝后的权重存成稀疏格式,在推理时使用支持稀疏存储的推理框架。对“简单尝试”来说,现阶段最大的价值是验证“模型里有多少冗余”,而不是直接跑分变快。

5. 踩坑记录与常见问题速查

5.1 我踩过的几个坑

第一个坑是“叠加剪枝”。我有一次在同一个模型上依次做了20%、30%、50%实验,结果发现50%那组的PPL奇高无比,后来才反应过来自己把之前剪过的权重又剪了一遍,相当于二次伤害。解决办法是每次实验都重新加载干净模型。

第二个坑是“保存不全导致加载报错”。如果我保存模型时用了diff格式,但没有保留mask,加载时就缺了剪枝信息,模型实际等于没剪。后来干脆统一走save_pretrained的合并权重模式,省心。

第三个坑是“CUDA内存爆掉”。剪枝时所有大矩阵同时读进来,再加上debug变量副本,容易把原本就紧张的显存搞爆。我当时的做法是把模型先移到CPU上再剪,CPU剪完再转回GPU评估,虽然慢一点,但不会内存抖动。

第四个坑比较隐蔽:LayerNorm的weight虽然被我排除在剪枝列表外,但某些版本的模型里会有额外自定义权重层,名字不叫norm也不带embedding,如果不加白名单检查,可能会误伤。我在过滤条件里加了“只在标准Linear模块上处理”的判断,稳妥很多。

5.2 常见问题速查表

问题症状思路
剪完模型对话全是空白生成为空或大量重复检查是否误剪embedding;看阈值是否过大
PPL反而比基线低小稀疏度下的数值噪声多测几段文本取平均,排除偶然性
保存后文件大小不变用稠密格式保存这是正常的,想要体积变小得用稀疏格式
GPU显存爆炸剪枝时多个矩阵副本先CPU剪枝,或分批按层处理
模型加载报missing key用了diff格式但没保存mask改用合并权重保存

6. 这个尝试还能怎么扩展

做完“简单剪枝”后,最自然的延伸就是往更高级的方向走。第一个方向是敏感度感知剪枝,不是对所有权重一视同仁,而是先给每个矩阵施加一点扰动,观察PPL变化,对敏感度高的层少剪或不剪。第二个方向是半结构化剪枝,把权重强制剪成N:M稀疏这种硬件更友好的形态。第三个方向是剪枝+量化组合,先剪枝再4bit量化,很多场景下比单独量化掉点更少。

现在也有一些数据驱动的剪枝方法,比如SparseGPT和Wanda,它们利用了少量校准数据和激活信息做剪枝,效果比我这种纯统计阈值强不少。我这次先做“简单剪枝”,就是想先建立一个可对比的基线,后续拿这些方法和它比,才知道自己每一步到底改进了什么。

最后说点个人体会。这次尝试做完,我自己最深的感受是:剪枝过程真的一点都不难,难的是对结果保持诚实。上模型前先猜一个稀疏度,上模型后别急着只看PPL数字,多和模型聊几句,你会发现很多指标之外的真相。如果你也想动手试试,建议从10%开始,逐步往上,每次只改一个变量,保留所有日志,很快就能摸清你手上这个模型的冗余底牌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询