📑 本页目录(点开跳转)
18c · 剪枝与稀疏化
⏱ 36 分钟 | ⭐ 稀疏度和加速比几乎没有关系
🎯 一句话
剪枝是压缩三件套里唯一一个"做完了可能一点收益都没有"的。 90% 的权重扔掉、精度基本不掉 —— 论文里漂亮得不像话; 上线一测:显存一个字节没省,延迟一毫秒没降,decode 上甚至更慢了。 这一章讲清楚为什么会这样,以及哪一种剪枝是真的有用的。
✂️ 一、非结构化剪枝:纸面很美,上线归零
⭐ 本章最重要的一条
逐个权重置零,不管位置。它在纸面上极漂亮:90% 的权重可以扔掉而精度基本不变。
然后你上线,发现 —— 显存没省,延迟没降。
import time, torch
torch.manual_seed(0)
torch.set_num_threads(4)
W = torch.randn(4096, 4096)
k = int(W.numel() * 0.90)
thr = W.abs().flatten().kthvalue(k).values
W_pruned = W * (W.abs() > thr) # ⭐ 幅度剪枝:|w| 最小的 90% 置零
print("稀疏度 : %.1f%%" % ((W_pruned == 0).float().mean() * 100))
print("原张量占用 : %.1f MiB" % (W.element_size() * W.nelement() / 2**20))
print("剪枝后占用 : %.1f MiB" % (W_pruned.element_size() * W_pruned.nelement() / 2**20))
# ⭐ 上面两行一定相等:置零不改变形状,dtype 也没变,显存一个字节都没省
W_csr = W_pruned.to_sparse_csr() # 换成稀疏格式才可能省,但索引本身也要钱
csr_bytes = (W_csr.values().numel() * 4
+ W_csr.col_indices().numel() * 8
+ W_csr.crow_indices().numel() * 8)
print("CSR 占用 : %.1f MiB" % (csr_bytes / 2**20))
# 该看到的数字(4096×4096 FP32,90% 稀疏度,已实跑):
# 原张量 64.0 MiB → 剪枝后 64.0 MiB(一个字节没省)
# → CSR 19.2 MiB ⭐ 而"只存非零值"的理想值是 6.4 MiB
# 多出来的 12.8 MiB 全是行/列索引 —— 稀疏格式的账要连索引一起算
def bench(fn, warmup=3, runs=10):
for _ in range(warmup):
fn()
t0 = time.perf_counter()
for _ in range(runs):
fn()
return (time.perf_counter() - t0) / runs * 1000
for B in [1, 64, 1024]: # B=1 就是 LLM decode 的形状
x = torch.randn(B, 4096)
td = bench(lambda: x @ W_pruned.T)
ts = bench(lambda: torch.sparse.mm(W_csr, x.T))
print(f"B={B:<5} 稠密 {td:8.2f} ms 稀疏 {ts:8.2f} ms ⭐ 比值 {ts/td:.2f}x")
实跑结果(4 线程 CPU,绝对数字随机器变,两条结论不变):
算一算
稀疏度 : 90.0%
原张量占用 : 64.0 MiB
剪枝后占用 : 64.0 MiB ⭐ 一个字节没省
CSR 占用 : 19.2 MiB (值 25.6MB 的 1/4,但索引又吃掉一大块)
B=1 稠密 1.61 ms 稀疏 1.76 ms ⭐ 比值 1.09x ← 反而更慢!
B=64 稠密 14.56 ms 稀疏 5.26 ms 比值 0.36x
B=1024 稠密 366.22 ms 稀疏 215.33 ms 比值 0.59x
对照
⭐ 90% 的乘法被省掉了,但最好的情况也只快了不到 3 倍,
而在 B=1(也就是【自回归解码的形状】)上【反而更慢】💀
为什么?回到第 3 章的访存模型:
因果链
⭐ 这条要背下来: 非结构化稀疏度和实际加速比几乎没有关系。 90% 稀疏可能带来 0 倍加速。判断标准从来不是稀疏度, 而是「目标硬件上有没有能吃这种稀疏的 kernel」。
🧱 二、半结构化 2:4:能真加速的最低门槛
结果对照
🪓 三、结构化剪枝:唯一确定加速的那种
整头、整通道、整层地删。 删完之后剩下的还是一个稠密张量,只是变小了 —— 所以它一定加速,加速比就是尺寸比,不需要任何特殊 kernel。
| 粒度 | 在 LLM 上是什么 | 备注 |
|---|---|---|
| 删注意力头 | 32 头 → 24 头 | 很多头确实是冗余的 |
| 缩 FFN 中间维 ⭐ | 11008 → 8192 | LLM 参数大头在 FFN,收益最大 |
| 砍层(depth) | 32 层 → 24 层 | 最粗暴也最有效,⚠️ 但对推理能力伤害大 |
| 缩 hidden(width) | 4096 → 3072 | 全局改动,最需要重训 |
⭐ 现在 LLM 领域的主流不是"纯剪枝",是组合拳: 结构化剪枝把尺寸砍下来 → 立刻用原模型当 teacher 做蒸馏恢复。 Sheared-LLaMA、Minitron 这类工作都是这个套路 —— 剪枝负责"选一个好的小架构",蒸馏负责"把能力搬回来"。 这也是本章把两件事放在一起讲的原因:它们在实践中就是一件事。
🎯 四、剪谁:幅度剪枝和它的改良
结果对照
LLM 上的两个改良,思路和量化那边一模一样:
| 方法 | 重要性怎么算 |
|---|---|
| Wanda ⭐ | $\lvert w \rvert \times \lVert x \rVert$ —— 权重大小 × 对应输入激活的范数 |
| SparseGPT | 用二阶信息,逐层剪完立刻重建剩余权重来补偿 |
⭐ 注意这个呼应: 第 18 章的 AWQ 说「重要性要看激活,不能只看权重」, 剪枝这边的 Wanda 说的是完全同一句话。 压缩问题的核心永远是"什么是重要的",而答案永远是"看它被用得多不多",不是"看它多大"。
🎟️ 五、迭代剪枝与彩票假说
流程图
彩票假说一句话就够:随机初始化的大网络里,存在一个小子网络,用同样的初始化单独训练就能达到原网络的精度。 ⚠️ 但你必须先把大网络训完才能找到这张彩票 —— 所以它是个漂亮的理论结果,不是省训练成本的方法。
💀 六、事故复盘:90% 稀疏度,显存省了 0,延迟没变
发生了什么:一个 13B 推理服务做非结构化剪枝,剪到 90% 稀疏。团队按「非零参数量 13B → 1.3B」报了排期:显存预计 26GB → 2.6GB,部署从 2×A100 降到 1×A10,采购单都提了。 上线前压测:
信息关系
为什么没发现:整个验证过程只盯着两个指标 —— 稀疏度和精度,两个都很漂亮。而剪枝框架产出的是 mask + 原张量,训练脚本里打印的"参数量"是按 mask 里的非零数算的,看起来就是降了 10 倍。没有任何人跑过一次端到端的显存快照和延迟压测。
代价:三周(两周剪枝迭代 + 一周精度恢复微调)报废,A10 采购单撤回,还耽误了一个季度的降本目标。
补什么:
操作步骤
- ⭐ ① 压缩项的验收指标只有两个:【显存快照】和【P50/P99 延迟】
- 「稀疏度」「参数量」「压缩比」全是过程指标,不能当验收
- ⭐ ② 立项当天先做 30 分钟的「形状体检」(本节那段代码):
- 在【目标硬件】上确认这种稀疏到底能不能被吃掉
- 要真加速就换路线:结构化剪枝,或者 2:4 半结构化
💥 它和上一章那个蒸馏事故是同一个形状: 压缩不会报错。 剪枝不会告诉你"我没加速",蒸馏不会告诉你"我伤了长尾"。 这和第 18 章那句「量化最危险的地方是它不会报错」是完全同一条规律 —— 压缩类优化的唯一防线,就是你事先定义好的、足够细的验收指标。
🔗 和站内其他章的关系
| 相关的地方 | 这里的位置 |
|---|---|
| 第 3 章 访存合并与算术强度 | ⭐ 非结构化剪枝不加速的根因:稀疏破坏了访存模式 |
| 第 15 章 解码是带宽瓶颈 | ⭐ 更深一层的根因:剪枝省的是算力,而算力本来就有 95% 在闲置 |
| 第 18 章 AWQ「只有 1% 的权重重要」 | 剪枝的 Wanda 说的是同一句话:重要性要看激活,不是看权重大小 ⭐ |
| 第 18b 章 知识蒸馏 | ⭐ 组合拳:结构化剪枝负责"选一个好的小架构",蒸馏负责"把能力搬回来" |
| 第 2 章 Tensor Core | 2:4 半结构化能真加速,就是因为 Ampere 起的稀疏 Tensor Core 原生吃这个模式 |
| 《模型上线之后》18 成本与容量 成本核算 | ⭐ 那个撤回的 A10 采购单 —— 降本项的验收指标该怎么定 |
| 《大模型全景导论》06 推理优化 部署武器清单 | ⭐ 那张清单只有 量化 / 批处理 / 投机解码三件,剪枝不在里面 —— 本章给出它落选的理由,以及它在什么条件下才算武器 |
✅ 检查点
- ⭐ 为什么非结构化剪枝在通用硬件上通常不加速?要能说出两层原因。
- 剪到 90% 稀疏,张量占用会变成原来的多少?为什么?
- 实测里 B=1 的稀疏矩阵乘比稠密还慢,B=1 对应的是推理的哪个阶段?
- 2:4 半结构化的规则是什么?它的代价是什么?
- 结构化剪枝为什么一定加速?LLM 上剪哪一部分收益最大?
- 幅度剪枝用全局阈值有什么风险?
- Wanda 的重要性怎么算?它和量化那边的哪个方法是同一句话?
- 为什么迭代剪枝好过一次剪到位?彩票假说能不能用来省训练成本?
- 那个事故里,团队盯的两个指标是什么?验收指标应该换成什么?
👀 答案
- (表层) 稀疏矩阵每个非零值旁边要额外读一个索引,多搬 50~100% 的字节;索引指向的位置随机,访存合并失效;形状不规整,Tensor Core 用不上。(根本) 剪枝省掉的是乘法次数即算力,而解码本来就是带宽瓶颈、算力有 95% 在闲置 —— 省掉一个本来就不是瓶颈的东西,等于没省。
- 完全不变(64 MiB → 64 MiB)。置零不改变张量形状,dtype 也没变,显存一个字节都不省。要省必须换稀疏格式(CSR),但索引本身也要占空间(实测 19.2 MiB,而不是理想的 6.4 MiB)。
- 自回归解码(decode)。实测比值 1.09x —— 稀疏反而更慢,正好落在最要命的那个形状上。
- 每连续 4 个权重里恰好 2 个是 0。代价是稀疏度被锁死在 50%,压缩比远不如非结构化;收益是 Ampere 起的稀疏 Tensor Core 原生支持,实测能拿到 1.3~1.7 倍。
- 因为它整头、整通道、整层地删,剩下的还是一个稠密张量,只是变小了 —— 加速比就是尺寸比,不需要任何特殊 kernel。LLM 参数大头在 FFN,缩中间维收益最大(11008 → 8192)。
- 各层权重尺度天然不同,全局排序容易把某一层整层剪光。实践上按层设配额更稳。
- $\lvert w \rvert \times \lVert x \rVert$ —— 权重大小 × 对应输入激活的范数。它和量化那边的 AWQ 是同一句话:重要性看它被用得多不多,不看它本身多大。
- 一次剪到 90% 精度会崩;分几次剪、每次微调,网络有机会重新分配容量。彩票假说⚠️ 不能省训练成本 —— 你必须先把大网络训完才能找到那张彩票,它是个理论结果不是方法。
- 团队只盯 稀疏度和精度,两个都很漂亮。验收指标应该只有两个:显存快照和 P50/P99 延迟 —— 稀疏度、参数量、压缩比全是过程指标,不能当验收。另外立项当天就该在目标硬件上做一次 30 分钟的「形状体检」。
🛑 可以停在这里
⚡ 走神救援
⭐ 这一章只有一条主线:稀疏度和加速比几乎没有关系。
非结构化剪枝逐个权重置零、扔掉九成精度基本不掉,纸面漂亮;但实跑下来 ⭐ 张量占用一个字节没省(置零不改变形状,dtype 也没变),换成稀疏格式也省不到理想值(索引本身要钱),⚠️ 而且 batch=1 时稀疏比稠密还慢——而 batch=1 正是自回归解码的形状。
⭐ 根本原因:剪枝省的是乘法次数、也就是算力,而解码本来就是带宽瓶颈,算力大部分在闲置——省掉一个本来就不是瓶颈的东西,等于没省。
⭐ 判断标准从来不是稀疏度,而是目标硬件上有没有能吃这种稀疏的 kernel。 真能加速的两条路:2:4 半结构化(硬件原生支持,⚠️ 但稀疏度锁死在一半)和结构化剪枝(删头、缩 FFN 中间维、砍层——⭐ 剩下的还是稠密张量,所以一定加速,加速比就是尺寸比)。
⭐ 现在 LLM 的主流不是纯剪枝,而是结构化剪枝选出小架构 → 立刻用原模型当 teacher 蒸馏恢复。
剪谁:幅度剪枝是极强的 baseline,⚠️ 但全局阈值会把某一层整层剪光,按层配额更稳。⭐ 改良版的那句话和量化那边的 AWQ 是同一句:重要性看被用得多不多,不看本身多大。
💀 那个事故的形状:按「非零参数从 130 亿降到 13 亿」报了显存和机型的采购单,而实测显存不降反升(mask 自己还要占),延迟也没改善。
下一节 👉 19-投机解码.md