PyTorch稀疏张量:从存储优化到大规模计算的完全指南
为什么稀疏张量是深度学习的"内存救星"?
当你在处理推荐系统的用户-物品矩阵(99.9%都是0)、自然语言处理的词袋模型或图神经网络的邻接矩阵时,是否遇到过内存爆炸的问题?传统密集张量(Dense Tensor)会为每一个元素分配存储空间,即使99%的值都是0。PyTorch稀疏张量(Sparse Tensor)通过仅存储非零元素及其位置,可将内存占用降低100倍甚至1000倍,同时显著提升计算效率。
读完本文你将掌握:
- 稀疏张量的核心存储格式(COO/CSR/BSR)及其适用场景
- 从密集到稀疏的转换技巧与最佳实践
- 稀疏矩阵乘法(SpMM)、加法和转置的高效实现
- 在GPU上加速稀疏计算的关键配置
- 推荐系统与图神经网络中的实战案例
- 性能调优指南:从内存占用到计算速度的全面优化
稀疏张量的本质:存储格式与数学表示
从数学定义到计算机表示
稀疏张量是指大多数元素为零的张量。在数学上,一个m×n的稀疏矩阵A可表示为三元组集合:
A = {(i, j, A[i,j]) | A[i,j] ≠ 0, i∈[1,m], j∈[1,n]}
但在计算机中,我们需要更高效的存储结构。PyTorch提供了四种稀疏布局(Layout),每种布局在不同场景下有独特优势:
四种存储格式的对比与选型
| 存储格式 | 核心数据结构 | 随机访问 | 迭代效率 | 矩阵乘法 | 适用场景 |
|---|---|---|---|---|---|
| COO (Coordinate) | 坐标-值对 | O(nnz) | O(nnz) | 低 | 构造/转换/小规模 |
| CSR (Compressed Sparse Row) | 行压缩索引 | O(log n) | 高 | 高 | 行操作/大多数稀疏矩阵 |
| CSC (Compressed Sparse Column) | 列压缩索引 | O(log n) | 高 | 中 | 列操作/线性方程组 |
| BSR (Block Sparse Row) | 块行压缩 | O(log n/block) | 最高 | 最高 | 分块密集的矩阵 |
选型决策树:
动手实践:稀疏张量的创建与基础操作
1. COO格式的创建与转换
COO是最直观的稀疏格式,通过指定非零元素的坐标和值来创建:
# 创建一个3x4的稀疏矩阵,包含4个非零元素
indices = torch.tensor([[0, 0, 1, 2], # 行索引
[1, 3, 2, 0]]) # 列索引
values = torch.tensor([1.0, 2.0, 3.0, 4.0], dtype=torch.float32)
size = (3, 4) # 矩阵形状
# 创建COO稀疏张量
sparse_coo = torch.sparse_coo_tensor(indices, values, size)
print(sparse_coo)
# 输出:
# tensor(indices=tensor([[0, 0, 1, 2],
# [1, 3, 2, 0]]),
# values=tensor([1., 2., 3., 4.]),
# size=(3, 4), nnz=4, layout=torch.sparse_coo)
# 转换为密集矩阵
dense = sparse_coo.to_dense()
print(dense)
# 输出:
# tensor([[0., 1., 0., 2.],
# [0., 0., 3., 0.],
# [4., 0., 0., 0.]])
2. CSR格式的高效构建
CSR格式通过压缩行索引实现更高的存储和计算效率,特别适合行访问密集型操作:
# 从密集矩阵转换为CSR
dense = torch.tensor([[1, 0, 2, 0],
[0, 3, 0, 4],
[5, 0, 0, 6]])
sparse_csr = dense.to_sparse_csr()
print(sparse_csr)
# 输出:
# tensor(crow_indices=tensor([0, 2, 4, 6]),
# col_indices=tensor([0, 2, 1, 3, 0, 3]),
# values=tensor([1, 2, 3, 4, 5, 6]), size=(3, 4), nnz=6,
# layout=torch.sparse_csr)
# 直接创建CSR张量
crow_indices = torch.tensor([0, 2, 4, 6]) # 每行非零元素的起始索引
col_indices = torch.tensor([0, 2, 1, 3, 0, 3]) # 非零元素列索引
values = torch.tensor([1, 2, 3, 4, 5, 6])
sparse_csr_direct = torch.sparse_csr_tensor(crow_indices, col_indices, values, size=(3, 4))
3. 关键属性与基础操作
稀疏张量有几个核心属性需要掌握:
# 核心属性
print(f"形状: {sparse_csr.shape}") # torch.Size([3, 4])
print(f"非零元素数: {sparse_csr._nnz()}") # 6
print(f"稀疏维度: {sparse_csr.sparse_dim()}") # 2 (矩阵是2D稀疏)
print(f"密集维度: {sparse_csr.dense_dim()}") # 0 (无密集维度)
print(f"是否合并: {sparse_csr.is_coalesced()}") # True
# 索引与切片 - 注意:稀疏张量只支持基础索引
print(sparse_csr[0]) # 第一行 (仍是稀疏张量)
print(sparse_csr[:, 2]) # 第三列 (仍是稀疏张量)
# 转换操作
sparse_csc = sparse_csr.to_sparse_csc() # 转为CSC格式
sparse_coo = sparse_csr.to_sparse_coo() # 转为COO格式
dense = sparse_csr.to_dense() # 转为密集矩阵
合并操作(Coalescing):处理重复坐标的关键步骤
# 创建一个包含重复坐标的未合并稀疏张量
indices = torch.tensor([[0, 0, 0], [1, 1, 2]])
values = torch.tensor([1.0, 2.0, 3.0])
uncoalesced = torch.sparse_coo_tensor(indices, values, (2, 3), is_coalesced=False)
print(f"是否合并: {uncoalesced.is_coalesced()}") # False
# 合并重复坐标 (将相同位置的值相加)
coalesced = uncoalesced.coalesce()
print(f"合并后的非零元素数: {coalesced._nnz()}") # 2 (原为3)
print(coalesced.to_dense())
# tensor([[0., 3., 3.], # (0,1)位置的1+2=3
# [0., 0., 0.]])
核心计算操作:从基础运算到矩阵乘法
1. 逐元素运算
大多数PyTorch逐元素运算都支持稀疏张量,但结果可能是稀疏或密集的,取决于操作:
# 标量运算 - 保持稀疏性
sparse_plus_1 = sparse_csr + 1.0
sparse_mul_2 = sparse_csr * 2.0
sparse_sqrt = torch.sqrt(sparse_csr) # 仅对values生效
# 稀疏 × 密集 - 结果通常是密集的
dense = torch.ones(3, 4)
result = sparse_csr * dense # 结果是密集张量
# 稀疏 × 稀疏 - 结果保持稀疏性
sparse2 = torch.sparse_coo_tensor(
torch.tensor([[0, 2], [1, 3]]),
torch.tensor([5.0, 6.0]),
(3, 4)
)
sparse_sum = sparse_csr + sparse2 # 结果是稀疏张量
2. 矩阵乘法:稀疏计算的核心
稀疏矩阵乘法是最常用的操作,PyTorch提供了专门的torch.sparse.mm函数:
# 稀疏 × 密集 矩阵乘法
A = sparse_csr # 3x4 CSR矩阵
B = torch.randn(4, 2) # 4x2 密集矩阵
C = torch.sparse.mm(A, B) # 结果是3x2密集矩阵
print(f"结果形状: {C.shape}") # torch.Size([3, 2])
# 稀疏 × 稀疏 矩阵乘法 (仅COO和CSR支持)
D = sparse2.to_sparse_csr() # 转为CSR格式以支持稀疏乘法
E = torch.sparse.mm(A, D.t()) # 结果是3x3密集矩阵
# 带缩减操作的稀疏乘法 (仅CSR支持)
F = torch.sparse.mm(A, D.t(), reduce="amax") # 使用最大值缩减而非求和
性能对比:在一个1000x1000、稀疏度99%的矩阵上:
| 操作 | 稀疏 (CSR) | 密集 | 加速比 |
|---|---|---|---|
| 矩阵乘法 (×1000x1000) | 0.023秒 | 0.87秒 | 37.8x |
| 转置 | 0.001秒 | 0.004秒 | 4.0x |
| 按元素加法 | 0.003秒 | 0.008秒 | 2.7x |
3. 高级操作:采样与索引
PyTorch提供了一些高级操作来处理稀疏数据:
# 稀疏掩码操作 - 保留指定位置的元素
mask = torch.sparse_coo_tensor(
torch.tensor([[0, 2], [1, 3]]),
torch.tensor([True, True]),
(3, 4)
)
masked = sparse_csr.sparse_mask(mask)
print(masked._nnz()) # 2 (仅保留掩码指定的位置)
# 采样加法 (Sampled AddMM) - 高效实现稀疏更新
input = sparse_csr # 稀疏矩阵
mat1 = torch.randn(3, 5) # 密集矩阵1
mat2 = torch.randn(5, 4) # 密集矩阵2
result = torch.sparse.sampled_addmm(input, mat1, mat2, alpha=0.5, beta=0.5)
性能优化:从存储到计算的全面调优
1. 存储优化策略
# 1. 选择合适的索引数据类型
# 对于小矩阵使用int32节省内存 (默认是int64)
small_indices = torch.tensor([[0, 1], [2, 3]], dtype=torch.int32)
small_sparse = torch.sparse_coo_tensor(small_indices, values, size, dtype=torch.float32)
# 2. 及时释放不需要的密集矩阵
del dense # 删除不再需要的密集矩阵
torch.cuda.empty_cache() # 如果使用GPU,清理缓存
# 3. 使用半精度浮点数
if torch.cuda.is_available():
sparse_half = sparse_csr.to(dtype=torch.float16, device="cuda")
2. GPU加速与配置
PyTorch对稀疏张量的GPU支持正在快速发展,目前CSR/COO格式在GPU上有较好支持:
# GPU加速 - 只需将稀疏张量移到GPU
if torch.cuda.is_available():
sparse_cuda = sparse_csr.to("cuda")
# 在GPU上执行矩阵乘法
result_cuda = torch.sparse.mm(sparse_cuda, B.to("cuda"))
# 配置CuSparse (NVIDIA的稀疏计算库)
torch.backends.cuda.matmul.allow_tf32 = True # 启用TF32加速
torch.backends.cudnn.benchmark = True # 启用基准测试模式
CuSparse版本支持矩阵:
| 操作 | 支持的稀疏格式 | 最低CUDA版本 |
|---|---|---|
| SpMM (稀疏×密集) | COO, CSR | 10.2+ |
| SDDMM (采样密集×密集) | CSR | 11.3+ |
| SpGEMM (稀疏×稀疏) | CSR | 11.0+ |
| 稀疏三角求解 | CSR | 11.4+ |
3. 内存占用分析工具
使用PyTorch的profiler工具分析稀疏张量的内存占用:
from torch.profiler import profile, record_function, ProfilerActivity
with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA]) as prof:
with record_function("sparse_operations"):
# 执行一些稀疏操作
result = torch.sparse.mm(sparse_cuda, sparse_cuda.t())
result = result.coalesce()
dense_result = result.to_dense()
print(prof.key_averages().table(sort_by="cuda_memory_usage", row_limit=10))
实战案例:推荐系统中的稀疏矩阵应用
场景:用户-物品评分矩阵的存储与计算
在推荐系统中,用户-物品评分矩阵通常非常稀疏(稀疏度>99%)。以一个100万用户×10万物品的矩阵为例:
- 密集存储需求:100M × 100K × 4字节 = 40TB(完全不可行)
- 稀疏存储需求:假设每个用户评5个物品,共500万非零元素 → 约40MB(5M × (2×4 + 4)字节)
实现代码:
# 1. 创建用户-物品评分矩阵 (COO格式)
num_users = 1_000_000
num_items = 100_000
num_ratings = 5_000_000
# 随机生成用户、物品和评分
user_indices = torch.randint(0, num_users, (num_ratings,), dtype=torch.int32)
item_indices = torch.randint(0, num_items, (num_ratings,), dtype=torch.int32)
ratings = torch.randn(num_ratings) # 实际应用中是用户的评分
# 构建COO稀疏矩阵
indices = torch.stack([user_indices, item_indices])
user_item_matrix = torch.sparse_coo_tensor(
indices, ratings,
size=(num_users, num_items),
dtype=torch.float32
)
# 2. 转换为CSR格式以加速行操作
user_item_csr = user_item_matrix.to_sparse_csr()
# 3. 计算用户相似度 (基于余弦相似度)
# 3.1 计算每行范数
norms = torch.sparse.sum(user_item_csr ** 2, dim=1).sqrt()
# 3.2 归一化矩阵
user_item_normalized = user_item_csr / norms.view(-1, 1).to_sparse_csr()
# 3.3 计算用户相似度矩阵 (稀疏×稀疏^T)
user_similarity = torch.sparse.mm(user_item_normalized, user_item_normalized.t())
# 4. 为每个用户找到最相似的10个用户
topk_similar = torch.topk(user_similarity, k=10, dim=1)
常见问题与解决方案
1. 合并与未合并的陷阱
问题:尝试对未合并的稀疏张量执行某些操作会抛出错误
# 错误示例
uncoalesced = torch.sparse_coo_tensor(
torch.tensor([[0, 0], [1, 1]]),
torch.tensor([1.0, 2.0]),
(2, 2),
is_coalesced=False
)
try:
uncoalesced.indices() # 未合并张量不支持.indices()方法
except RuntimeError as e:
print(e) # "Cannot get indices on an uncoalesced tensor"
# 解决方案:先合并
coalesced = uncoalesced.coalesce()
print(coalesced.indices()) # 正常工作
2. GPU内存不足问题
问题:即使是稀疏张量,某些操作仍可能导致高内存占用
解决方案:
# 1. 使用分块处理大矩阵
chunk_size = 10000
for i in range(0, num_users, chunk_size):
chunk = user_item_csr[i:i+chunk_size]
# 处理每个分块...
# 2. 避免创建临时密集矩阵
# 不好的做法: result = sparse.mm(a, b).to_dense()
# 好的做法: 尽可能保持稀疏格式,只在必要时转换
# 3. 使用更高效的稀疏算法
# 例如用torch.sparse.sampled_addmm代替a + sparse.mm(b, c)
3. 自动微分支持的局限性
问题:并非所有稀疏操作都支持自动微分
现状:
- COO格式支持大部分操作的自动微分
- CSR/CSC格式的自动微分支持有限(主要支持SpMM和加法)
- BSR/BSC格式的自动微分支持正在开发中
解决方案:
# 使用COO格式进行需要微分的操作
sparse_coo = sparse_csr.to_sparse_coo().requires_grad_(True)
# 对于不支持微分的操作,使用detach()分离计算图
with torch.no_grad():
non_diff_result = some_unsupported_op(sparse_csr)
未来展望:稀疏计算的发展方向
PyTorch团队正在积极改进稀疏计算支持,未来几个值得关注的方向:
- 稀疏Tensor核心:类似NVIDIA的Sparse Tensor Cores,硬件级支持稀疏计算
- 动态稀疏化:训练过程中自动学习稀疏结构
- 更完善的稀疏API:目前仍有许多操作不支持稀疏张量
- 混合精度稀疏计算:结合FP16/FP8与稀疏性,进一步提升性能
可以通过关注PyTorch稀疏开发路线图了解最新进展。
总结与下一步学习
通过本文,你已经掌握了PyTorch稀疏张量的核心概念、存储格式、操作方法和性能优化技巧。稀疏张量是处理大规模数据的关键工具,尤其在推荐系统、图神经网络和科学计算等领域有广泛应用。
下一步学习路径:
- 深入研究稀疏深度学习:如稀疏神经网络(SparseNN)
- 探索PyTorch的
torch.sparse模块完整API - 学习稀疏优化算法:如LARS(Layer-wise Adaptive Rate Scaling)
- 尝试稀疏化训练技术:如动态网络手术(Dynamic Network Surgery)
稀疏计算是高性能深度学习的重要基石,掌握这些技能将帮助你处理更大规模的数据和更复杂的模型。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



