🏠 总目录📚 本教程 剪枝与稀疏化 ← →
📑 本页目录(点开跳转)

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 章的访存模型:

因果链

稠密 matmul 为什么快:
· 权重在显存里【连续】→访存可以合并(coalesced),带宽跑满
· 形状规整→直接进 Tensor Core
稀疏 matmul 为什么不快:
· 每个非零值旁边还得读一个【索引】→多搬了 50~100% 的字节 ⚠️
· 索引指向的位置是【随机的】→访存合并失效,实际带宽掉一大截
· 形状不规整→Tensor Core 用不上,退回普通 CUDA core 💀
⭐ 最关键的一条:
剪枝省掉的是【乘法次数】= 算力,
而解码本来就是【带宽瓶颈】,算力有 95% 在闲置(第 15 章)——
你省掉了一个本来就不是瓶颈的东西,等于没省。

⭐ 这条要背下来: 非结构化稀疏度和实际加速比几乎没有关系。 90% 稀疏可能带来 0 倍加速。判断标准从来不是稀疏度, 而是「目标硬件上有没有能吃这种稀疏的 kernel」。


🧱 二、半结构化 2:4:能真加速的最低门槛

结果对照

规则:每【连续 4 个权重】里,恰好有 2 个是 0
[ w0 w1 w2 w3 ]→[ 0 w1 0 w3 ] ✅ 合法
[ 0 0 0 w3 ] ❌ 不合法(3 个零)
⭐ Ampere(A100)起,稀疏 Tensor Core 原生支持这个模式
理论 2 倍算力,实测 1.3~1.7 倍
⚠️ 但稀疏度被【锁死在 50%】,压缩比远不如非结构化

🪓 三、结构化剪枝:唯一确定加速的那种

整头、整通道、整层地删。 删完之后剩下的还是一个稠密张量,只是变小了 —— 所以它一定加速,加速比就是尺寸比,不需要任何特殊 kernel。

粒度 在 LLM 上是什么 备注
删注意力头 32 头 → 24 头 很多头确实是冗余的
缩 FFN 中间维 ⭐ 11008 → 8192 LLM 参数大头在 FFN,收益最大
砍层(depth) 32 层 → 24 层 最粗暴也最有效,⚠️ 但对推理能力伤害大
缩 hidden(width) 4096 → 3072 全局改动,最需要重训

⭐ 现在 LLM 领域的主流不是"纯剪枝",是组合拳: 结构化剪枝把尺寸砍下来 → 立刻用原模型当 teacher 做蒸馏恢复。 Sheared-LLaMA、Minitron 这类工作都是这个套路 —— 剪枝负责"选一个好的小架构",蒸馏负责"把能力搬回来"。 这也是本章把两件事放在一起讲的原因:它们在实践中就是一件事。


🎯 四、剪谁:幅度剪枝和它的改良

结果对照

幅度剪枝(magnitude pruning):|w| 越小越先剪
极简单、极强的 baseline,至今没被大幅超越
⚠️ 全局阈值 vs 逐层阈值:
全局排序容易把某一层【整层剪光】(各层权重尺度天然不同)
实践上按层设配额更稳

LLM 上的两个改良,思路和量化那边一模一样:

方法 重要性怎么算
Wanda ⭐ $\lvert w \rvert \times \lVert x \rVert$ —— 权重大小 × 对应输入激活的范数
SparseGPT 用二阶信息,逐层剪完立刻重建剩余权重来补偿

⭐ 注意这个呼应: 第 18 章的 AWQ 说「重要性要看激活,不能只看权重」, 剪枝这边的 Wanda 说的是完全同一句话。 压缩问题的核心永远是"什么是重要的",而答案永远是"看它被用得多不多",不是"看它多大"。


🎟️ 五、迭代剪枝与彩票假说

流程图

⭐ 一次剪到 90% ≠ 分 5 次剪到 90%
一次到位: 90% → 精度崩
迭代: 50%→微调→70%→微调→85%→微调→90% ✅
每次让网络有机会重新分配容量

彩票假说一句话就够:随机初始化的大网络里,存在一个小子网络,用同样的初始化单独训练就能达到原网络的精度。 ⚠️ 但你必须先把大网络训完才能找到这张彩票 —— 所以它是个漂亮的理论结果,不是省训练成本的方法。


💀 六、事故复盘:90% 稀疏度,显存省了 0,延迟没变

发生了什么:一个 13B 推理服务做非结构化剪枝,剪到 90% 稀疏。团队按「非零参数量 13B → 1.3B」报了排期:显存预计 26GB → 2.6GB,部署从 2×A100 降到 1×A10,采购单都提了。 上线前压测:

信息关系

预期 实测
显存 26GB→2.6GB 💀 26.1GB(不降反升,mask 本身还占了 0.1GB)
P50 decode 41ms 💀 43ms(略微变慢)

为什么没发现:整个验证过程只盯着两个指标 —— 稀疏度和精度,两个都很漂亮。而剪枝框架产出的是 mask + 原张量,训练脚本里打印的"参数量"是按 mask 里的非零数算的,看起来就是降了 10 倍。没有任何人跑过一次端到端的显存快照和延迟压测。

代价:三周(两周剪枝迭代 + 一周精度恢复微调)报废,A10 采购单撤回,还耽误了一个季度的降本目标。

补什么:

操作步骤

  1. ⭐ ① 压缩项的验收指标只有两个:【显存快照】和【P50/P99 延迟】
  2. 「稀疏度」「参数量」「压缩比」全是过程指标,不能当验收
  3. ⭐ ② 立项当天先做 30 分钟的「形状体检」(本节那段代码):
  4. 在【目标硬件】上确认这种稀疏到底能不能被吃掉
  5. 要真加速就换路线:结构化剪枝,或者 2:4 半结构化

💥 它和上一章那个蒸馏事故是同一个形状: 压缩不会报错。 剪枝不会告诉你"我没加速",蒸馏不会告诉你"我伤了长尾"。 这和第 18 章那句「量化最危险的地方是它不会报错」是完全同一条规律 —— 压缩类优化的唯一防线,就是你事先定义好的、足够细的验收指标。


🔗 和站内其他章的关系

相关的地方 这里的位置
第 3 章 访存合并与算术强度 ⭐ 非结构化剪枝不加速的根因:稀疏破坏了访存模式
第 15 章 解码是带宽瓶颈 ⭐ 更深一层的根因:剪枝省的是算力,而算力本来就有 95% 在闲置
第 18 章 AWQ「只有 1% 的权重重要」 剪枝的 Wanda 说的是同一句话:重要性要看激活,不是看权重大小 ⭐
第 18b 章 知识蒸馏 ⭐ 组合拳:结构化剪枝负责"选一个好的小架构",蒸馏负责"把能力搬回来"
第 2 章 Tensor Core 2:4 半结构化能真加速,就是因为 Ampere 起的稀疏 Tensor Core 原生吃这个模式
《模型上线之后》18 成本与容量 成本核算 ⭐ 那个撤回的 A10 采购单 —— 降本项的验收指标该怎么定
《大模型全景导论》06 推理优化 部署武器清单 ⭐ 那张清单只有 量化 / 批处理 / 投机解码三件,剪枝不在里面 —— 本章给出它落选的理由,以及它在什么条件下才算武器

✅ 检查点

  1. ⭐ 为什么非结构化剪枝在通用硬件上通常不加速?要能说出两层原因。
  2. 剪到 90% 稀疏,张量占用会变成原来的多少?为什么?
  3. 实测里 B=1 的稀疏矩阵乘比稠密还慢,B=1 对应的是推理的哪个阶段?
  4. 2:4 半结构化的规则是什么?它的代价是什么?
  5. 结构化剪枝为什么一定加速?LLM 上剪哪一部分收益最大?
  6. 幅度剪枝用全局阈值有什么风险?
  7. Wanda 的重要性怎么算?它和量化那边的哪个方法是同一句话?
  8. 为什么迭代剪枝好过一次剪到位?彩票假说能不能用来省训练成本?
  9. 那个事故里,团队盯的两个指标是什么?验收指标应该换成什么?
👀 答案
  1. (表层) 稀疏矩阵每个非零值旁边要额外读一个索引,多搬 50~100% 的字节;索引指向的位置随机,访存合并失效;形状不规整,Tensor Core 用不上。(根本) 剪枝省掉的是乘法次数即算力,而解码本来就是带宽瓶颈、算力有 95% 在闲置 —— 省掉一个本来就不是瓶颈的东西,等于没省。
  2. 完全不变(64 MiB → 64 MiB)。置零不改变张量形状,dtype 也没变,显存一个字节都不省。要省必须换稀疏格式(CSR),但索引本身也要占空间(实测 19.2 MiB,而不是理想的 6.4 MiB)。
  3. 自回归解码(decode)。实测比值 1.09x —— 稀疏反而更慢,正好落在最要命的那个形状上。
  4. 每连续 4 个权重里恰好 2 个是 0。代价是稀疏度被锁死在 50%,压缩比远不如非结构化;收益是 Ampere 起的稀疏 Tensor Core 原生支持,实测能拿到 1.3~1.7 倍。
  5. 因为它整头、整通道、整层地删,剩下的还是一个稠密张量,只是变小了 —— 加速比就是尺寸比,不需要任何特殊 kernel。LLM 参数大头在 FFN,缩中间维收益最大(11008 → 8192)。
  6. 各层权重尺度天然不同,全局排序容易把某一层整层剪光。实践上按层设配额更稳。
  7. $\lvert w \rvert \times \lVert x \rVert$ —— 权重大小 × 对应输入激活的范数。它和量化那边的 AWQ 是同一句话:重要性看它被用得多不多,不看它本身多大。
  8. 一次剪到 90% 精度会崩;分几次剪、每次微调,网络有机会重新分配容量。彩票假说⚠️ 不能省训练成本 —— 你必须先把大网络训完才能找到那张彩票,它是个理论结果不是方法。
  9. 团队只盯 稀疏度和精度,两个都很漂亮。验收指标应该只有两个:显存快照和 P50/P99 延迟 —— 稀疏度、参数量、压缩比全是过程指标,不能当验收。另外立项当天就该在目标硬件上做一次 30 分钟的「形状体检」。

🛑 可以停在这里

⚡ 走神救援

⭐ 这一章只有一条主线:稀疏度和加速比几乎没有关系。

非结构化剪枝逐个权重置零、扔掉九成精度基本不掉,纸面漂亮;但实跑下来 ⭐ 张量占用一个字节没省(置零不改变形状,dtype 也没变),换成稀疏格式也省不到理想值(索引本身要钱),⚠️ 而且 batch=1 时稀疏比稠密还慢——而 batch=1 正是自回归解码的形状。

⭐ 根本原因:剪枝省的是乘法次数、也就是算力,而解码本来就是带宽瓶颈,算力大部分在闲置——省掉一个本来就不是瓶颈的东西,等于没省。

⭐ 判断标准从来不是稀疏度,而是目标硬件上有没有能吃这种稀疏的 kernel。 真能加速的两条路:2:4 半结构化(硬件原生支持,⚠️ 但稀疏度锁死在一半)和结构化剪枝(删头、缩 FFN 中间维、砍层——⭐ 剩下的还是稠密张量,所以一定加速,加速比就是尺寸比)。

⭐ 现在 LLM 的主流不是纯剪枝,而是结构化剪枝选出小架构 → 立刻用原模型当 teacher 蒸馏恢复。

剪谁:幅度剪枝是极强的 baseline,⚠️ 但全局阈值会把某一层整层剪光,按层配额更稳。⭐ 改良版的那句话和量化那边的 AWQ 是同一句:重要性看被用得多不多,不看本身多大。

💀 那个事故的形状:按「非零参数从 130 亿降到 13 亿」报了显存和机型的采购单,而实测显存不降反升(mask 自己还要占),延迟也没改善。

下一节 👉 19-投机解码.md

打卡记录保存在你的浏览器里,首页能看到总进度