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

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 采购单撤回,还耽误了一个季度的降本目标。

补什么

   ⭐ ① 压缩项的验收指标只有两个:【显存快照】和【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 推理优化 部署武器清单 ⭐ 那张清单只有 量化 / 批处理 / 投机解码三件,剪枝不在里面 —— 本章给出它落选的理由,以及它在什么条件下才算武器

✅ 检查点

  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 分钟的「形状体检」。

🛑 可以停在这里

走神救援

⭐⭐这一章只有一条主线:稀疏度和加速比几乎没有关系。非结构化剪枝逐个权重置零、90% 扔掉精度基本不掉,纸面漂亮;但实跑 4096×4096 剪到 90%:张量占用 64MiB 一个字节没省(⭐置零不改变形状,dtype 也没变),换成 CSR 稀疏格式是 19.2MiB 而不是理想的 6.4MiB(索引本身也要钱),而且B=1 时稀疏比稠密还慢 1.09 倍——B=1 正是自回归解码的形状,最要命的那个。⭐两层原因表层是每个非零值要额外读索引、多搬 50~100% 字节、索引位置随机导致访存合并失效、形状不规整用不上 Tensor Core;⭐根本原因是剪枝省的是乘法次数即算力,而解码本来就是带宽瓶颈、算力有 95% 在闲置——省掉一个本来就不是瓶颈的东西等于没省(第 3、15 章)。判断标准从来不是稀疏度,而是目标硬件上有没有能吃这种稀疏的 kernel。真能加速的两条路2:4 半结构化(每连续 4 个里恰好 2 个零,Ampere 起稀疏 Tensor Core 原生支持,实测 1.3~1.7 倍,⚠️但稀疏度锁死 50%);⭐结构化剪枝(删头、缩 FFN 中间维、砍层、缩 hidden)——剩下的还是稠密张量,所以一定加速,加速比就是尺寸比LLM 参数大头在 FFN,缩中间维收益最大。⭐现在 LLM 的主流不是纯剪枝而是组合拳:结构化剪枝选出小架构 → 立刻用原模型当 teacher 蒸馏恢复(Sheared-LLaMA、Minitron),这也是它和上一章紧挨着的原因。剪谁:幅度剪枝是极强的 baseline 至今没被大幅超越,⚠️全局阈值会把某一层整层剪光(各层尺度不同),按层配额更稳;改良里 Wanda = |w| × 激活范数,⭐和量化那边的 AWQ 是同一句话:重要性看被用得多不多,不看本身多大迭代剪枝远好于一次剪到位(每次微调让网络重新分配容量);彩票假说漂亮但⚠️必须先训完大网络才能找到那张彩票,不省训练成本。💀事故:13B 服务剪到 90% 稀疏,按「非零参数 13B→1.3B」报了显存 26GB→2.6GB、2×A100 降 1×A10,采购单都提了;实测显存 26.1GB 不降反升(mask 自己还占 0.1GB)、P50 从 41ms 变 43ms 略微变慢。没发现的原因是全程只看稀疏度和精度两个都很漂亮的指标,而框架产出的是 mask + 原张量、打印的"参数量"是按非零数算的,看起来就是降了 10 倍,没有任何人跑过一次端到端显存快照和延迟压测。代价:三周报废、采购单撤回、耽误一个季度降本目标。⭐补丁:压缩项的验收指标只有显存快照和 P50/P99 延迟,稀疏度/参数量/压缩比全是过程指标不能当验收;立项当天先在目标硬件上做 30 分钟形状体检。💥和上一章那个蒸馏事故是同一个形状:压缩不会报错——剪枝不会告诉你"我没加速",蒸馏不会告诉你"我伤了长尾",唯一的防线是事先定义好的、足够细的验收指标

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

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