1. 稀疏计算基础:稀疏矩阵定义、存储格式与稀疏度计算

大家好,欢迎来到《稀疏计算加速卷积网络实战》的第一章。

说实话,做深度学习加速这些年,我踩过最大的坑之一,就是「明明模型里大部分权重都是零,却还在用稠密矩阵做乘法」。你想想看,一个卷积核里 70% 的参数都是 0,那这 70% 的乘加运算不就是白费电吗?

稀疏计算,说白了就是「别做无用功」。咱们这一章先把地基打牢——搞清楚什么是稀疏矩阵、怎么存它、怎么算它的稀疏度。后面再聊怎么用它加速卷积。

1.1 什么是稀疏矩阵?

先给个直觉。一个矩阵里,零元素占绝大多数,就叫稀疏矩阵。反过来,零很少的叫稠密矩阵。

但「绝大多数」到底是多少?业内有个不成文的规矩:稀疏度超过 50%,就可以考虑用稀疏格式来存储和计算了。当然,实际项目中我一般等到稀疏度超过 70% 才动手,因为格式转换本身也有开销。

稀疏矩阵的数学定义:设矩阵 A 的维度为 m×n,非零元素个数为 nnz,则稀疏度 sparsity = 1 - nnz / (m × n)。

当 sparsity → 1 时,矩阵越稀疏。

举个例子,一个 1000×1000 的矩阵,只有 100 个非零元素,那稀疏度就是 1 - 100/1,000,000 = 0.9999,也就是 99.99%。这种矩阵用稠密格式存,纯属浪费内存。

我的经验:在剪枝后的卷积网络中,权重矩阵的稀疏度经常能达到 90% 以上。但要注意,稀疏度越高,加速收益不一定线性增长——因为存储格式的开销和计算模式的适配度也会影响最终性能。

1.2 三种主流稀疏存储格式

存稀疏矩阵,核心思路就一个:只存非零元素,不存零。但怎么存、存哪些辅助信息,不同格式各有千秋。

1.2.1 COO 格式(Coordinate Format)

COO 是最直观的格式。它用三个数组:

  • row:非零元素的行索引
  • col:非零元素的列索引
  • data:非零元素的值

三个数组长度都是 nnz。每个非零元素对应一组 (row[i], col[i], data[i])。

# COO 格式示例
import numpy as np

# 稠密矩阵
dense = np.array([
    [0, 0, 3, 0],
    [4, 0, 0, 0],
    [0, 0, 0, 5],
    [0, 6, 0, 0]
])

# 提取非零元素
rows, cols = np.where(dense != 0)
data = dense[rows, cols]

print("row:", rows)   # [0, 1, 2, 3]
print("col:", cols)   # [2, 0, 3, 1]
print("data:", data)  # [3, 4, 5, 6]

COO 的优点:构建简单,支持重复索引(可以累加)。缺点:做矩阵乘法时,按行或按列访问都不连续,缓存不友好。

我曾经踩过的坑:在 PyTorch 里用 torch.sparse_coo_tensor 时,忘了指定 size 参数,结果自动推断的 shape 比实际小,导致后续计算越界。记住:COO 必须显式指定矩阵尺寸。

1.2.2 CSR 格式(Compressed Sparse Row)

CSR 是工业界最常用的格式。它把 COO 的 row 数组做了压缩:

  • row_ptr:长度为 m+1,记录每行第一个非零元素在 col 和 data 中的起始位置
  • col:非零元素的列索引,长度 nnz
  • data:非零元素的值,长度 nnz
# CSR 格式示例
row_ptr = [0, 1, 2, 3, 4]  # 每行有 1 个非零元素
col     = [2, 0, 3, 1]
data    = [3, 4, 5, 6]

# 解读:第 0 行非零元素在 col[0] 和 data[0]
#       第 1 行非零元素在 col[1] 和 data[1]
#       以此类推

CSR 的优势:按行访问非常快,适合行优先的矩阵乘法。Scipy、TensorFlow、PyTorch 的稀疏张量底层都大量使用 CSR。

我个人的习惯:做卷积网络剪枝后的权重存储,首选 CSR。因为卷积的 im2col 展开后,权重矩阵的行对应输出通道,按行访问正好匹配推理时的计算顺序。

1.2.3 CSC 格式(Compressed Sparse Column)

CSC 是 CSR 的「转置版」。它压缩的是列索引:

  • col_ptr:长度为 n+1,记录每列第一个非零元素的位置
  • row:非零元素的行索引
  • data:非零元素的值

CSC 适合按列访问的场景,比如稀疏矩阵乘以稠密向量时,如果向量是列向量,CSC 的缓存命中率更高。

1.2.4 三种格式对比

特性 COO CSR CSC
存储空间 3 × nnz 2 × nnz + m + 1 2 × nnz + n + 1
构建难度 最简单 中等 中等
行访问效率 低 高 低
列访问效率 低 低 高
矩阵乘法 慢 快(行优先) 快(列优先)
适用场景 构建阶段、增量添加 推理、行优先计算 列优先计算、稀疏分解

1.3 稀疏度计算与可视化

稀疏度计算本身很简单,但实际项目中我经常用它来做「剪枝效果评估」。

def compute_sparsity(matrix):
    """
    计算矩阵的稀疏度
    matrix: numpy array 或 torch.Tensor
    """
    total = matrix.size
    zeros = (matrix == 0).sum()
    sparsity = zeros / total
    return sparsity.item()

# 示例
import torch
weight = torch.randn(64, 256)
weight[weight.abs() < 0.5] = 0  # 简单剪枝
sparsity = compute_sparsity(weight)
print(f"稀疏度: {sparsity:.2%}")  # 输出类似 58.73%

光看数字不够直观。我习惯把稀疏模式画出来,一眼就能看出哪些通道被剪掉了。

import matplotlib.pyplot as plt

def plot_sparsity_pattern(matrix, title="Sparsity Pattern"):
    plt.figure(figsize=(6, 6))
    plt.spy(matrix, markersize=0.5)
    plt.title(title)
    plt.xlabel("Columns")
    plt.ylabel("Rows")
    plt.show()

# 可视化一个剪枝后的卷积权重
plot_sparsity_pattern(weight.numpy(), "Conv Layer 1 Weight")

你会看到,剪枝后的权重往往呈现「结构化稀疏」——整行或整列都是零。这种模式对硬件加速特别友好,因为可以跳过整个通道的计算。

1.4 本章知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以把它当作一个「稀疏计算入门地图」。

稀疏计算基础:知识体系 稀疏矩阵 定义与稀疏度 sparsity = 1 - nnz / (m×n) 存储格式 COO CSR CSC 应用场景 剪枝、量化、模型压缩 三种格式对比 格式 存储量 访问效率 推荐场景 COO 3×nnz 低 构建阶段 CSR 2×nnz+m+1 行访问高 推理加速 CSC 2×nnz+n+1 列访问高 列优先计算

1.5 小结与避坑指南

这一章我们覆盖了三个核心点:

  1. 稀疏矩阵的定义:零元素占主导,稀疏度 = 1 - nnz / total
  2. 三种存储格式:COO 简单直观,CSR 行优先高效,CSC 列优先灵活
  3. 稀疏度计算与可视化:用代码算、用图看,快速评估剪枝效果

避坑指南(我亲身经历):

  • 别在 GPU 上用 COO 做矩阵乘法——CUDA 的原子操作会把你卡死。CSR 或 CSC 才是 GPU 上的正确选择。
  • 稀疏度不是越高越好。超过 99% 时,非零元素分布极度不均匀,会导致负载不均衡,加速效果反而下降。
  • 转换格式时注意数据类型。int32 的索引在超大矩阵(nnz > 2^31)时会溢出,记得用 int64。