torch.sparse
警告
PyTorch 中稀疏张量的 API 目前处于测试阶段,将来可能有所变动。我们非常欢迎用户通过 GitHub 提交功能请求、错误报告和各种建议。
何时何地使用稀疏性
默认情况下,PyTorch 会将 torch.Tensor
元素连续存储在物理内存中。这使得需要快速访问元素的各种数组处理算法能够高效运行。
现在,一些用户可能会选择使用张量来表示数据,例如图的邻接矩阵、剪枝权重或点云等,而这些张量中的元素大多为零值。我们认识到这些应用场景的重要性,并致力于通过稀疏存储格式为它们提供性能优化。
多年来,人们开发了各种稀疏存储格式,例如 COO、CSR/CSC、半结构化和 LIL 等。尽管它们在具体布局上有所不同,但都通过高效表示零值元素来实现数据压缩。我们将未被压缩的值称为 指定 的,而将被压缩的零值元素称为 未指定 的。
通过压缩重复的零,稀疏存储格式旨在节约各种CPU和GPU上的内存和计算资源。尤其是在高稀疏度或高度结构化的情况下,这种做法会对性能产生显著的影响。因此,稀疏存储格式可以视为一种性能优化手段。
像许多其他的性能优化一样,稀疏存储格式并不总是有优势。当你尝试为特定用例使用稀疏格式时,可能会发现执行时间反而增加了。
如果你分析后预计会看到性能显著提升,但实际测量结果却是性能下降,请不要犹豫,打开一个 GitHub 问题。这样可以帮助我们优先考虑实现高效内核和进行更广泛的性能优化。
我们让您可以轻松地尝试不同的稀疏布局,并在它们之间进行转换,不对哪种方法最适合您的应用程序持有任何偏见。
功能概览
我们希望为每种布局提供转换方法,从而能够轻松地从给定的稠密张量创建稀疏张量。
在接下来的例子中,我们将一个默认密集(带状)布局的二维张量转换成使用COO内存布局的二维张量。在这个例子中,只保存了非零元素的值和索引。
>>> a = torch.tensor([[0, 2.], [3, 0]]) >>> a.to_sparse() tensor(indices=tensor([[0, 1], [1, 0]]), values=tensor([2., 3.]), size=(2, 2), nnz=2, layout=torch.sparse_coo)
PyTorch 当前支持 COO、CSR、CSC、BSR 和 BSC 稀疏矩阵格式。
我们还有一个原型实现来支持半结构化稀疏性(详见此处)。请参阅相关文献以获取更多信息。
请注意,我们对这些格式提供了一些轻微的泛化版本。
批处理:为了实现最佳性能,像 GPU 这样的设备需要进行批处理,因此我们支持批处理维度。
目前,我们提供了一种非常简单的批处理版本,其中稀疏格式的每个组成部分都被单独批处理。这还要求每批次条目具有相同数量的指定元素。在此示例中,我们将一个3D密集型张量转换为一个3D(批量)CSR张量。
>>> t = torch.tensor([[[1., 0], [2., 3.]], [[4., 0], [5., 6.]]]) >>> t.dim() 3 >>> t.to_sparse_csr() tensor(crow_indices=tensor([[0, 1, 3], [0, 1, 3]]), col_indices=tensor([[0, 0, 1], [0, 0, 1]]), values=tensor([[1., 2., 3.], [4., 5., 6.]]), size=(2, 2, 2), nnz=3, layout=torch.sparse_csr)
密集维度:另一方面,某些数据(如图嵌入)可能更适合作为向量的稀疏集合来处理,而不是单独的标量。
在这个例子中,我们从一个3D Strided 张量创建了一个具有2个稀疏维度和1个稠密维度的3D Hybrid COO 张量。如果在3D Strided 张量中的某一行全为零,则该行不会被存储。然而,如果有任何非零值存在,则整行都会被完整地存储下来。这减少了索引的数量,因为我们只需要每行一个索引而不是每个元素一个索引。但是这也增加了值的存储量。只有完全为零的行可以省略,如果一行中包含任何非零值,整个行都需要被存储。
>>> t = torch.tensor([[[0., 0], [1., 2.]], [[0., 0], [3., 4.]]]) >>> t.to_sparse(sparse_dim=2) tensor(indices=tensor([[0, 1], [1, 1]]), values=tensor([[1., 2.], [3., 4.]]), size=(2, 2, 2), nnz=2, layout=torch.sparse_coo)
操作员概览
从根本上说,使用稀疏存储格式的张量操作与使用连续或其它存储格式的张量操作行为一致。虽然数据的物理布局会影响操作的性能,但它不应该改变操作的意义。
我们正在积极增加稀疏张量的操作覆盖率。用户目前不应期望稀疏张量能获得与密集张量相同级别的支持。请参阅我们的操作符文档以获取详细列表。
>>> b = torch.tensor([[0, 0, 1, 2, 3, 0], [4, 5, 0, 6, 0, 0]]) >>> b_s = b.to_sparse_csr() >>> b_s.cos() Traceback (most recent call last): File "<stdin>", line 1, in <module> RuntimeError: unsupported tensor layout: SparseCsr >>> b_s.sin() tensor(crow_indices=tensor([0, 3, 6]), col_indices=tensor([2, 3, 4, 0, 1, 3]), values=tensor([ 0.8415, 0.9093, 0.1411, -0.7568, -0.9589, -0.2794]), size=(2, 6), nnz=6, layout=torch.sparse_csr)
如上例所示,我们不支持类似 cos 这样的非零保持一元运算符。这类运算的结果无法像输入数据那样充分利用稀疏存储格式,并可能导致内存急剧增加。因此,我们需要用户先将数据显式地转换为密集张量,然后再进行操作。
>>> b_s.to_dense().cos() tensor([[ 1.0000, -0.4161], [-0.9900, 1.0000]])
我们注意到有些用户希望在进行如cos
等运算时忽略被压缩的零值,而不是保留操作的确切语义。为此,我们可以参考torch.masked
及其MaskedTensor
,后者则依赖于稀疏存储格式和内核。
还需要注意的是,目前用户不能选择输出布局。例如,将稀疏张量添加到常规步长张量中会生成一个步长张量。有些用户可能希望结果仍保持为稀疏布局,因为他们知道结果仍然是足够稀疏的。
>>> a + b.to_sparse() tensor([[0., 3.], [3., 0.]])
我们承认,能够高效生成不同输出布局的内核访问是非常有用的。后续操作可能会从特定布局中获益良多。我们正在开发一个用于控制结果布局的API,并认为这对于为任何给定模型规划最优执行路径非常重要。
稀疏半结构张量
警告
稀疏半结构化张量目前是试验性功能,可能还会更改。如发现问题或有反馈意见,请随时提交issue。
半结构化稀疏是一种稀疏数据布局,最早在NVIDIA的Ampere架构中被提出。它还被称为细粒度结构化稀疏或2:4结构化稀疏。
这种稀疏布局每存储2n
个元素中的n
个元素,其中n
由张量数据类型的宽度(dtype)决定。最常见的dtype是float16,此时n=2
,因此称为“2:4结构化稀疏性”。
关于半结构化稀疏性的详细解释,请参阅这篇 NVIDIA 博客文章。
在 PyTorch 中,半结构化稀疏性是通过一个 Tensor 子类来实现的。通过继承这个子类,我们可以重写 __torch_dispatch__
方法,在执行矩阵乘法时使用更快的稀疏内核。此外,我们还可以将张量以压缩形式存储在子类中,从而减少内存开销。
在这种压缩形式下,稀疏张量仅通过保存指定的元素和一些元数据来进行存储,而这些元数据则包含了掩码的信息。
注意
半结构化稀疏张量的指定元素和元数据掩码共同存储在同一个扁平压缩张量中。这些元素和掩码依次连接起来,形成一个连续的内存块。
压缩张量 = [原始张量中的指定元素 | 元数据掩码]
对于大小为(r, c)的原始张量,我们期望前m * k // 2个元素被保留,而张量的其余部分则包含元数据。
为了便于用户查看指定的元素和掩码,可以分别使用 .indices()
访问掩码,使用 .values()
访问指定的元素。
-
.values()
返回一个大小为 (r, c//2)、数据类型与密集矩阵相同的张量。 -
.indices()
返回一个大小为 (r, c//2) 的张量。如果数据类型是torch.float16
或torch.bfloat16
,则元素类型为torch.int16
;如果是torch.int8
,则元素类型为torch.int32
。
对于2:4的稀疏张量,元数据开销非常小,每个指定元素只需要2位。
注意
值得注意的是,torch.float32
仅支持1:2的稀疏模式,因此不会遵循上面提到的公式。
在这里,我们详细说明了如何计算一个2:4稀疏张量的压缩比(密集大小除以稀疏大小)。
令 (r, c) = tensor.shape 和 e = bitwidth(tensor.dtype)。对于 torch.float16
和 torch.bfloat16
,e = 16;而对于 torch.int8
,e = 8。
通过这些计算,我们可以确定原始密集型和新稀疏表示的总内存占用情况。
这提供了一个简单的压缩比率公式,该公式仅依赖于张量数据类型的位宽。
使用此公式,我们发现对于 torch.float16
或 torch.bfloat16
的压缩比为 56.25%,而对于 torch.int8
的压缩比为 62.5%。
构造稀疏半结构化张量
你可以通过简单地使用torch.to_sparse_semi_structured
函数,将密集张量转换为稀疏半结构化张量。
请留意,由于半结构化稀疏性仅在NVIDIA GPU上受支持,因此我们只提供对CUDA张量的支持。
以下数据类型支持半结构化的稀疏性。请留意,每种数据类型都具有特定的形状约束和压缩因子。
PyTorch 数据类型 |
形状约束 |
压缩因子 |
稀疏矩阵模式 |
---|---|---|---|
|
张量必须是二维的,且 r 和 c 都必须是 64 的正整数倍 |
9/16 |
2:4 |
|
张量必须是二维的,且 r 和 c 都必须是 64 的正整数倍 |
9/16 |
2:4 |
|
张量必须是二维的,且 r 和 c 都必须是 128 的正整数倍 |
10/16 |
2:4 |
要构建半结构化的稀疏张量,首先需要创建一个符合2:4(或半结构化)格式的密集张量。为此,我们用一个小的1x4条带进行平铺,生成一个16x16的密集float16张量。之后,我们可以调用to_sparse_semi_structured
函数来压缩它,从而加速推理过程。
>>> from torch.sparse import to_sparse_semi_structured >>> A = torch.Tensor([0, 0, 1, 1]).tile((128, 32)).half().cuda() tensor([[0., 0., 1., ..., 0., 1., 1.], [0., 0., 1., ..., 0., 1., 1.], [0., 0., 1., ..., 0., 1., 1.], ..., [0., 0., 1., ..., 0., 1., 1.], [0., 0., 1., ..., 0., 1., 1.], [0., 0., 1., ..., 0., 1., 1.]], device='cuda:0', dtype=torch.float16) >>> A_sparse = to_sparse_semi_structured(A) SparseSemiStructuredTensor(shape=torch.Size([128, 128]), transposed=False, values=tensor([[1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], ..., [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.]], device='cuda:0', dtype=torch.float16), metadata=tensor([[-4370, -4370, -4370, ..., -4370, -4370, -4370], [-4370, -4370, -4370, ..., -4370, -4370, -4370], [-4370, -4370, -4370, ..., -4370, -4370, -4370], ..., [-4370, -4370, -4370, ..., -4370, -4370, -4370], [-4370, -4370, -4370, ..., -4370, -4370, -4370], [-4370, -4370, -4370, ..., -4370, -4370, -4370]], device='cuda:0', dtype=torch.int16))
稀疏半结构张量运算
目前支持以下针对半结构化稀疏张量的操作:
-
torch.addmm(bias, dense, sparse.t())
-
torch.mm(dense, sparse)
-
torch.mm(sparse, dense)
-
aten.linear.default(密集矩阵, 稀疏矩阵, 偏置项)
-
aten.t.default(sparse)
-
aten.t.detach(sparse)
要使用这些操作,在张量以半结构化稀疏格式包含0时,将to_sparse_semi_structured(tensor)
的输出作为参数传递,而不是直接使用tensor
,如下所示:
>>> a = torch.Tensor([0, 0, 1, 1]).tile((64, 16)).half().cuda() >>> b = torch.rand(64, 64).half().cuda() >>> c = torch.mm(a, b) >>> a_sparse = to_sparse_semi_structured(a) >>> torch.allclose(c, torch.mm(a_sparse, b)) True
利用半结构化稀疏性加速 nn.Linear
如果您的模型权重已是半结构化的稀疏形式,只需几行代码即可加速线性层。
>>> input = torch.rand(64, 64).half().cuda() >>> mask = torch.Tensor([0, 0, 1, 1]).tile((64, 16)).cuda().bool() >>> linear = nn.Linear(64, 64).half().cuda() >>> linear.weight = nn.Parameter(to_sparse_semi_structured(linear.weight.masked_fill(~mask, 0)))
稀疏COO tensor
PyTorch 使用所谓的坐标格式(COO 格式)来存储稀疏张量。在 COO 格式中,每个元素通过一个包含其索引和对应值的元组进行存储。
指定元素的索引被收集到大小为
(ndim, nse)
且元素类型为torch.int64
的indices
张量中。相应的值被收集到大小为
(nse,)
的values
张量中,其元素类型可以是任意整数或浮点数。
其中 ndim
表示张量的维度,nse
表示指定元素的数量。
注意
稀疏 COO 张量的内存消耗至少为 (ndim * 8 + <元素类型所占字节大小>)
乘以 nse
字节(加上存储其他张量数据的常数开销)。
步长张量的内存消耗至少为 product(
。
例如,一个10,000 x 10,000大小的张量包含100,000个非零32位浮点数,在使用COO张量布局时,其内存消耗至少为(2 * 8 + 4) * 100,000 = 2,000,000
字节。而在使用默认的步长张量布局时,则需要10,000 * 10,000 * 4 = 400,000,000
字节。请注意,通过采用COO存储格式可以节省200倍的内存。
建筑
或者更常见的是:建造
由于“Construction”在不同语境下可以有不同的翻译,根据上下文,“构建”可能更适合技术或软件相关领域。如果是在一般语境中使用,则“建筑”或“建造”更为自然。 但是,按照原文的直接对应关系,最接近的是:施工
考虑到通用性和广泛适用性,建议采用:建设
最终答案根据具体上下文而定。如果必须选择一个最贴近技术语境的答案,则为:构建
可以通过提供索引张量和值张量,以及稀疏张量的大小(如果无法从索引和值张量推断出来的话),来构造一个稀疏 COO 张量。这些参数需要传递给函数 torch.sparse_coo_tensor()
。
假设我们想定义一个稀疏张量,在位置 (0, 2) 的值为 3,位置 (1, 0) 的值为 4,位置 (1, 2) 的值为 5。未指定的元素默认填充为零。我们可以这样写:
>>> i = [[0, 1, 1], [2, 0, 2]] >>> v = [3, 4, 5] >>> s = torch.sparse_coo_tensor(i, v, (2, 3)) >>> s tensor(indices=tensor([[0, 1, 1], [2, 0, 2]]), values=tensor([3, 4, 5]), size=(2, 3), nnz=3, layout=torch.sparse_coo) >>> s.to_dense() tensor([[0, 0, 3], [4, 0, 5]])
请注意,输入的i
不是一组索引元组。如果你希望以这种格式编写索引,则在传递给稀疏构造函数之前需要先进行转置。
>>> i = [[0, 2], [1, 0], [1, 2]] >>> v = [3, 4, 5 ] >>> s = torch.sparse_coo_tensor(list(zip(*i)), v, (2, 3)) >>> # Or another equivalent formulation to get s >>> s = torch.sparse_coo_tensor(torch.tensor(i).t(), v, (2, 3)) >>> torch.sparse_coo_tensor(i.t(), v, torch.Size([2,3])).to_dense() tensor([[0, 0, 3], [4, 0, 5]])
可以通过仅指定其尺寸来创建一个空的稀疏COO张量:
>>> torch.sparse_coo_tensor(size=(2, 3)) tensor(indices=tensor([], size=(2, 0)), values=tensor([], size=(0,)), size=(2, 3), nnz=0, layout=torch.sparse_coo)
稀疏混合COO格式张量
PyTorch 实现了稀疏张量的扩展,即将标量值的稀疏张量扩展为具有(连续)张量值的稀疏张量。这种张量称为混合张量。
PyTorch 混合 COO 张量扩展了稀疏 COO 张量,允许 values
张量为多维张量,从而使我们能够拥有:
指定元素的索引被收集到大小为
(sparse_dims, nse)
且元素类型为torch.int64
的indices
张量中。对应的(张量)值被收集在大小为
(nse, dense_dims)
的values
张量中,元素类型可以是任意整数或浮点数。
注意
我们使用一个(M + K)维的张量来表示一个N维的稀疏混合张量,其中M表示稀疏维度的数量,K表示密集维度的数量,满足M + K = N的关系。
假设我们想创建一个(2+1)维张量,在位置(0, 2)包含[3, 4],在位置(1, 0)包含[5, 6],在位置(1, 2)包含[7, 8]。我们可以这样写:
>>> i = [[0, 1, 1], [2, 0, 2]] >>> v = [[3, 4], [5, 6], [7, 8]] >>> s = torch.sparse_coo_tensor(i, v, (2, 3, 2)) >>> s tensor(indices=tensor([[0, 1, 1], [2, 0, 2]]), values=tensor([[3, 4], [5, 6], [7, 8]]), size=(2, 3, 2), nnz=3, layout=torch.sparse_coo)
>>> s.to_dense() tensor([[[0, 0], [0, 0], [3, 4]], [[5, 6], [0, 0], [7, 8]]])
通常来说,如果s
是一个稀疏COO张量,并且设M = s.sparse_dim()
和K = s.dense_dim()
,那么我们有以下不变性:
M + K == len(s.shape) == s.ndim
- 张量的维度等于稀疏维度和密集维度的数量之和。
s.indices().shape == (M, nse)
- 索引以稀疏形式被显式存储。
s.values().shape == (nse,) + s.shape[M : M + K]
- 混合张量的值是 K 维的张量,
s.values().layout == torch.strided
- 值以 strided 张量的形式存储。
注意
稠密维度总是位于稀疏维度之后,这意味着稠密和稀疏维度的混合不受支持。
注意
为了确保构造的稀疏张量具有一致的索引、值和大小,可以通过在每个张量创建时设置check_invariants=True
关键字参数来启用不变性检查,或者全局使用torch.sparse.check_sparse_tensor_invariants
上下文管理器实例。默认情况下,稀疏张量的不变性检查是关闭的。
未融合的稀疏COO张量
PyTorch 稀疏 COO 张量格式允许未合并的稀疏张量,在这种情况下,索引中可能存在重复坐标。如果存在重复坐标,则该索引处的值为所有重复值之和。例如,可以为相同的索引 1
指定多个值 3
和 4
,从而形成一个 1-D 的未合并张量:
>>> i = [[1, 1]] >>> v = [3, 4] >>> s=torch.sparse_coo_tensor(i, v, (3,)) >>> s tensor(indices=tensor([[1, 1]]), values=tensor( [3, 4]), size=(3,), nnz=2, layout=torch.sparse_coo)
而在合并过程中,多值元素会被求和并转换为单一值。
>>> s.coalesce() tensor(indices=tensor([[1]]), values=tensor([7]), size=(3,), nnz=1, layout=torch.sparse_coo)
通常,torch.Tensor.coalesce()
方法的输出是一个具有以下特性的稀疏张量:
-
指定张量元素的索引各不相同
-
索引按照字母顺序排列,
-
torch.Tensor.is_coalesced()
返回True
。
注意
大多数情况下,你不必关心稀疏张量是否已经合并,因为大多数操作对于已合并和未合并的稀疏张量来说效果是一样的。
然而,某些操作在未合并的张量上更高效,而其他一些操作在已合并的张量上更高效。
例如,稀疏 COO 张量的加法是通过将索引和值张量简单拼接起来实现的。
>>> a = torch.sparse_coo_tensor([[1, 1]], [5, 6], (2,)) >>> b = torch.sparse_coo_tensor([[0, 0]], [7, 8], (2,)) >>> a + b tensor(indices=tensor([[0, 0, 1, 1]]), values=tensor([7, 8, 5, 6]), size=(2,), nnz=4, layout=torch.sparse_coo)
如果你反复执行可能导致重复项的操作(例如,torch.Tensor.add()
),你应该偶尔合并稀疏张量,以防它们变得过大。
另一方面,按字典顺序排列的索引在实现涉及大量元素选择操作(如切片或矩阵乘法)的算法时可能会带来优势。
处理稀疏COO张量
让我们来看一下以下示例:
>>> i = [[0, 1, 1], [2, 0, 2]] >>> v = [[3, 4], [5, 6], [7, 8]] >>> s = torch.sparse_coo_tensor(i, v, (2, 3, 2))
如上所述,稀疏 COO 张量是一个 torch.Tensor
实例。为了将其与其他布局的 Tensor 实例区分开来,可以使用 torch.Tensor.is_sparse
或 torch.Tensor.layout
属性:
>>> isinstance(s, torch.Tensor) True >>> s.is_sparse True >>> s.layout == torch.sparse_coo True
可以分别使用方法torch.Tensor.sparse_dim()
和 torch.Tensor.dense_dim()
来获取稀疏和稠密维度的数量。例如:
>>> s.sparse_dim(), s.dense_dim() (2, 1)
如果 s
是一个稀疏的 COO 张量,可以使用方法 torch.Tensor.indices()
和 torch.Tensor.values()
获取其 COO 格式的数据。
注意
目前,只有在张量实例被合并后,才能获取到 COO 格式的数据。
>>> s.indices() RuntimeError: Cannot get indices on an uncoalesced tensor, please call .coalesce() first
要获取未合并张量的COO格式数据,请使用torch.Tensor._values()
和 torch.Tensor._indices()
:
>>> s._indices() tensor([[0, 1, 1], [2, 0, 2]])
警告
调用 torch.Tensor._values()
将返回一个 分离的 张量。要追踪梯度,应使用 torch.Tensor.coalesce().values()
。
构建一个新的稀疏COO张量会生成一个未合并的张量:
>>> s.is_coalesced() False
但可以使用 torch.Tensor.coalesce()
方法来构建稀疏 COO 张量的合并副本。
>>> s2 = s.coalesce() >>> s2.indices() tensor([[0, 1, 1], [2, 0, 2]])
在处理未合并的稀疏COO张量时,需要考虑到未合并数据具有累加性质:相同索引的值是求和运算中的项,其计算结果给出对应张量元素的值。例如,对一个稀疏未合并张量进行标量乘法可以通过将所有未合并的值与标量相乘来实现,因为 c * (a + b) == c * a + c * b
成立。然而,任何非线性操作(如平方根),不能通过直接对未合并的数据应用该操作来实现,因为 sqrt(a + b) != sqrt(a) + sqrt(b)
通常不成立。
稀疏 COO 张量的切片操作(正步长)仅适用于密集维度。无论是稀疏维度还是密集维度,都支持索引操作:
>>> s[1] tensor(indices=tensor([[0, 2]]), values=tensor([[5, 6], [7, 8]]), size=(3, 2), nnz=2, layout=torch.sparse_coo) >>> s[1, 0, 1] tensor(6) >>> s[1, 0, 1:] tensor([6])
在 PyTorch 中,稀疏张量的默认填充值为零,并且不能显式指定。然而,某些操作会以不同的方式解释这个填充值。例如,torch.sparse.softmax()
假设填充值为负无穷大来计算 softmax。
稀疏压缩 tensors
Sparse Compressed Tensors 表示一类具有共同特征的稀疏张量:使用某种编码来压缩特定维度上的索引。这种编码能够优化稀疏压缩张量的线性代数内核操作,并基于Compressed Sparse Row (CSR) 格式。PyTorch 的稀疏压缩张量在此基础上扩展了对稀疏张量批的支持,允许处理多维张量值,并以密集块的形式存储稀疏张量值。
注意
我们使用 (B + M + K) 维张量来表示一个 N 维稀疏压缩混合张量。其中 B、M 和 K 分别代表批处理维度、稀疏维度和密集维度的数量,并且满足B + M + K == N
的条件。对于稀疏压缩张量,其稀疏维度的数量始终为两个,即M == 2
。
注意
如果一个索引张量 compressed_indices
使用了 CSR 编码,则以下不变量必须成立:
-
compressed_indices
是一个连续的、带有 stride 的 32 或 64 位整数张量 -
compressed_indices
的形状是(*batchsize, compressed_dim_size + 1)
,其中compressed_dim_size
表示压缩维度的数量(例如行数或列数)。 -
compressed_indices[..., 0] == 0
,其中...
表示批次索引 -
compressed_indices[..., compressed_dim_size] == nse
,其中nse
表示指定元素的数目。 -
0 <= compressed_indices[..., i] - compressed_indices[..., i - 1] <= plain_dim_size
对于i=1, ..., compressed_dim_size
,其中plain_dim_size
表示未压缩维度的数量(与压缩维度正交,例如列或行)。
为了确保构造的稀疏张量具有一致的索引、值和大小,可以通过在每个张量创建时设置check_invariants=True
关键字参数来启用不变性检查,或者全局使用torch.sparse.check_sparse_tensor_invariants
上下文管理器实例。默认情况下,稀疏张量的不变性检查是关闭的。
注意
将稀疏压缩布局推广到N维张量可能会导致对指定元素数量的理解混淆。当一个稀疏压缩张量包含批量维度时,每个批次中的指定元素数量会有所不同。如果该张量有密集维度,则考虑的元素是一个K维数组。对于块稀疏压缩布局而言,2-D 块被视为被指定的元素。例如,有一个3维块稀疏张量,它有一个长度为b
的批量维度和一个形状为 p, q
的块形。如果该张量有n
个指定元素,则实际上每个批次中有n
块被指定。此张量将具有形状为(b, n, p, q)
的values
。这种对指定元素数量的解释来自于所有稀疏压缩布局都源自一个2维矩阵的压缩。批量维度被视为稀疏矩阵的堆叠,密集维度则改变了元素的意义,从简单的标量值变为具有自身维度的数组。
稀疏CSR tensor
与COO格式相比,CSR格式的主要优点在于更有效地利用存储空间,并且在执行如稀疏矩阵向量乘法等计算操作时,使用MKL和MAGMA后端能显著提高速度。
在最简单的情况下,一个 (0 + 2 + 0) 维的稀疏 CSR 张量由三个一维张量组成:分别是 crow_indices
、col_indices
和 values
。
crow_indices
张量包含压缩后的行索引,其大小为nrows + 1
(即行数加 1)。张量的最后一个值是总的非零元素数量nse
。这个张量用于确定在给定行开始时,values
和col_indices
中对应的索引位置。张量中每个连续数字与前一个数字之差表示该行中的元素数量。
col_indices
张量包含了每个元素的列索引。这是一维张量,其大小为nse
。
values
张量包含了 CSR 张量元素的值,其大小为nse
的一维张量。
注意
索引张量 crow_indices
和 col_indices
的元素类型应该是 torch.int64
(默认)或 torch.int32
。如果你想使用支持 MKL 的矩阵操作,应选择 torch.int32
类型。这是因为 PyTorch 默认链接的是 MKL LP64 版本,它使用 32 位整数索引。
通常情况下,(B + 2 + K)维稀疏CSR张量由两个(B + 1)维索引张量crow_indices
和col_indices
以及一个(1 + K)维的values
张量组成,满足以下条件:
crow_indices.shape == (*batchsize, nrows + 1)
col_indices.shape == (batchsize, nse)
values.shape == (nse, *densesize)
稀疏CSR张量的形状为(*batchsize, nrows, ncols, *densesize)
,其中len(batchsize) == B
且 len(densesize) == K
。
注意
稀疏CSR张量的批次之间存在依赖关系:每个批次中指定元素的数量必须相同。这种稍微有些人工设定的限制,使得能够高效地存储不同CSR批次的索引。
注意
可以通过torch.Tensor.sparse_dim()
和 torch.Tensor.dense_dim()
方法获取稀疏和稠密维度的数量。批次维度可以通过张量的形状计算得出:batchsize = tensor.shape[:-tensor.sparse_dim() - tensor.dense_dim()]
。
注意
稀疏 CSR 张量的内存消耗至少为 (nrows * 8 + (8 + <元素类型大小(以字节为单位)> * prod(densesize)) * nse) * prod(batchsize)
字节,还包括存储其他张量数据的常量开销。
参考稀疏COO格式介绍中的示例数据,当使用CSR张量布局时,一个大小为10,000 x 10,000的张量(包含100,000个非零32位浮点数),至少需要(10000 * 8 + (8 + 4 * 1) * 100,000) * 1 = 1,280,000
字节的内存。请注意,与使用COO和strided格式相比,CSR存储格式分别节省了1.6倍和310倍的内存。
构建CSR张量
稀疏CSR张量可以直接通过使用torch.sparse_csr_tensor()
函数构建。用户需要分别提供行和列索引以及值的张量,其中行索引必须采用CSR压缩编码方式指定。size
参数是可选的,如果未提供,则会根据crow_indices
和col_indices
进行推断。
>>> crow_indices = torch.tensor([0, 2, 4]) >>> col_indices = torch.tensor([0, 1, 0, 1]) >>> values = torch.tensor([1, 2, 3, 4]) >>> csr = torch.sparse_csr_tensor(crow_indices, col_indices, values, dtype=torch.float64) >>> csr tensor(crow_indices=tensor([0, 2, 4]), col_indices=tensor([0, 1, 0, 1]), values=tensor([1., 2., 3., 4.]), size=(2, 2), nnz=4, dtype=torch.float64) >>> csr.to_dense() tensor([[1., 2.], [3., 4.]], dtype=torch.float64)
注意
推断出的 size
中稀疏维度的值是通过计算 crow_indices
的大小和 col_indices
中的最大索引值得到的。如果需要的列数大于推断出的 size
中的列数,则必须显式指定 size
参数。
构造2-D稀疏CSR张量的最简单方法是从带步长的或稀疏的COO张量开始,使用torch.Tensor.to_sparse_csr()
方法。该方法会将张量中的零值解释为稀疏张量中的缺失值。
>>> a = torch.tensor([[0, 0, 1, 0], [1, 2, 0, 0], [0, 0, 0, 0]], dtype=torch.float64) >>> sp = a.to_sparse_csr() >>> sp tensor(crow_indices=tensor([0, 1, 3, 3]), col_indices=tensor([2, 0, 1]), values=tensor([1., 1., 2.]), size=(3, 4), nnz=3, dtype=torch.float64)
CSR张量运算
稀疏矩阵与向量的乘法可以通过tensor.matmul()
方法实现。目前,这是在CSR张量上唯一支持的数学运算。
>>> vec = torch.randn(4, 1, dtype=torch.float64) >>> sp.matmul(vec) tensor([[0.9078], [1.3180], [0.0000]], dtype=torch.float64)
稀疏CSC tensor
稀疏 CSC(压缩稀疏列)张量格式实现了用于存储二维张量的 CSC 格式,并且扩展了对稀疏 CSC 张量批次的支持,以及对多维张量值的支持。
注意
当交换稀疏维度时,稀疏 CSC 张量实际上是对稀疏 CSR 张量进行转置。
类似于稀疏CSR张量,一个稀疏CSC张量由三个部分组成:ccol_indices
、row_indices
和 values
:
ccol_indices
张量包含压缩后的列索引,其形状为(*batchsize, ncols + 1)
的 (B + 1)-D 张量。最后一个元素表示指定的非零元素数量nse
。此张量编码了在给定列开始时values
和row_indices
中的索引位置。张量中每个连续数字与前一个数字之差,表示给定列中的元素数量。
row_indices
张量包含了每个元素的行索引。这是一一个形状为(*batchsize, nse)
的 (B + 1) 维张量。
values
张量包含了 CSC 张量元素的值。这是一个形状为(nse, *densesize)
的 (1 + K) 维张量。
CSC张量的构造
稀疏 CSC 张量可以直接通过使用torch.sparse_csc_tensor()
函数构建。用户需要分别提供行索引、列索引和值张量,其中列索引必须使用 CSR 压缩编码指定。size
参数是可选的,如果未提供,则会从row_indices
和 ccol_indices
张量中推断出来。
>>> ccol_indices = torch.tensor([0, 2, 4]) >>> row_indices = torch.tensor([0, 1, 0, 1]) >>> values = torch.tensor([1, 2, 3, 4]) >>> csc = torch.sparse_csc_tensor(ccol_indices, row_indices, values, dtype=torch.float64) >>> csc tensor(ccol_indices=tensor([0, 2, 4]), row_indices=tensor([0, 1, 0, 1]), values=tensor([1., 2., 3., 4.]), size=(2, 2), nnz=4, dtype=torch.float64, layout=torch.sparse_csc) >>> csc.to_dense() tensor([[1., 3.], [2., 4.]], dtype=torch.float64)
注意
在稀疏 CSC 张量构造函数中,压缩列索引参数位于行索引参数之前。
可以使用torch.Tensor.to_sparse_csc()
方法从任何二维张量构造 (0 + 2 + 0) 维稀疏 CSC 张量。张量中的零值将被视为稀疏张量中的缺失值:
>>> a = torch.tensor([[0, 0, 1, 0], [1, 2, 0, 0], [0, 0, 0, 0]], dtype=torch.float64) >>> sp = a.to_sparse_csc() >>> sp tensor(ccol_indices=tensor([0, 1, 2, 3, 3]), row_indices=tensor([1, 1, 0]), values=tensor([1., 2., 1.]), size=(3, 4), nnz=3, dtype=torch.float64, layout=torch.sparse_csc)
稀疏 BSR 张量
稀疏的 BSR(块压缩稀疏行)张量格式实现了用于存储二维张量的 BSR 格式,并且扩展了对稀疏 BSR 张量批次的支持,其中值为多维张量的块。
一个稀疏的 BSR 张量包含三个张量: crow_indices
、col_indices
和 values
:
crow_indices
张量包含压缩后的行索引,其形状为(*batchsize, nrowblocks + 1)
的 (B+1)-D 张量。张量的最后一个元素表示指定块的数量(nse
)。该张量编码了在给定列块开始的位置上values
和col_indices
中的索引位置。张量中每个连续数字与前一个数字之差表示给定行中的块数。
col_indices
张量包含了每个元素的列块索引。这是一张形状为(*batchsize, nse)
的 (B + 1) 维张量。
values
张量包含了稀疏 BSR 张量元素在二维块中的值。这是一个形状为(nse, nrowblocks, ncolblocks, *densesize)
的 (1 + 2 + K)-维张量。
构建 BSR 张量
稀疏 BSR 张量可以直接通过使用torch.sparse_bsr_tensor()
函数构建。用户需要分别提供行和列的块索引以及值张量,其中行的块索引必须使用 CSR 压缩编码指定。size
参数是可选的,并且如果未提供,则会从crow_indices
和 col_indices
张量中推断出来。
>>> crow_indices = torch.tensor([0, 2, 4]) >>> col_indices = torch.tensor([0, 1, 0, 1]) >>> values = torch.tensor([[[0, 1, 2], [6, 7, 8]], ... [[3, 4, 5], [9, 10, 11]], ... [[12, 13, 14], [18, 19, 20]], ... [[15, 16, 17], [21, 22, 23]]]) >>> bsr = torch.sparse_bsr_tensor(crow_indices, col_indices, values, dtype=torch.float64) >>> bsr tensor(crow_indices=tensor([0, 2, 4]), col_indices=tensor([0, 1, 0, 1]), values=tensor([[[ 0., 1., 2.], [ 6., 7., 8.]], [[ 3., 4., 5.], [ 9., 10., 11.]], [[12., 13., 14.], [18., 19., 20.]], [[15., 16., 17.], [21., 22., 23.]]]), size=(4, 6), nnz=4, dtype=torch.float64, layout=torch.sparse_bsr) >>> bsr.to_dense() tensor([[ 0., 1., 2., 3., 4., 5.], [ 6., 7., 8., 9., 10., 11.], [12., 13., 14., 15., 16., 17.], [18., 19., 20., 21., 22., 23.]], dtype=torch.float64)
可以使用torch.Tensor.to_sparse_bsr()
方法从任何二维张量构造 (0 + 2 + 0) 维稀疏 BSR 张量,该方法还需要指定值块大小。
>>> dense = torch.tensor([[0, 1, 2, 3, 4, 5], ... [6, 7, 8, 9, 10, 11], ... [12, 13, 14, 15, 16, 17], ... [18, 19, 20, 21, 22, 23]]) >>> bsr = dense.to_sparse_bsr(blocksize=(2, 3)) >>> bsr tensor(crow_indices=tensor([0, 2, 4]), col_indices=tensor([0, 1, 0, 1]), values=tensor([[[ 0, 1, 2], [ 6, 7, 8]], [[ 3, 4, 5], [ 9, 10, 11]], [[12, 13, 14], [18, 19, 20]], [[15, 16, 17], [21, 22, 23]]]), size=(4, 6), nnz=4, layout=torch.sparse_bsr)
稀疏BSC tensor
稀疏 BSC(块压缩稀疏列)张量格式实现了 BSC 格式,用于存储二维张量,并且扩展了对稀疏 BSC 张量批处理的支持,其中值是多维张量的块。
一个稀疏的BSC张量包含三个张量:分别是ccol_indices
、row_indices
和values
。
ccol_indices
张量包含压缩的列索引,其形状为(*batchsize, ncolblocks + 1)
。最后一个元素是已指定的非零元素数量nse
。此张量编码了在给定行块开始的位置上,在values
和row_indices
中的索引位置。张量中每个连续数字与前一个数字之差表示给定列中的非零元素数量。
row_indices
张量包含了每个元素的行块索引。这是一张形状为(*batchsize, nse)
的 (B + 1) 维张量。
values
张量包含了稀疏 BSC 张量元素的值,这些值被收集到二维块中。这是一个形状为(nse, nrowblocks, ncolblocks, *densesize)
的 (1 + 2 + K)-维张量。
BSC张量的构造
稀疏 BSC 张量可以直接通过使用torch.sparse_bsc_tensor()
函数构建。用户需要分别提供行和列块索引以及值张量,其中列块索引必须使用 CSR 压缩编码指定。size
参数是可选的,并且如果未提供,则会从ccol_indices
和 row_indices
张量中推断出来。
>>> ccol_indices = torch.tensor([0, 2, 4]) >>> row_indices = torch.tensor([0, 1, 0, 1]) >>> values = torch.tensor([[[0, 1, 2], [6, 7, 8]], ... [[3, 4, 5], [9, 10, 11]], ... [[12, 13, 14], [18, 19, 20]], ... [[15, 16, 17], [21, 22, 23]]]) >>> bsc = torch.sparse_bsc_tensor(ccol_indices, row_indices, values, dtype=torch.float64) >>> bsc tensor(ccol_indices=tensor([0, 2, 4]), row_indices=tensor([0, 1, 0, 1]), values=tensor([[[ 0., 1., 2.], [ 6., 7., 8.]], [[ 3., 4., 5.], [ 9., 10., 11.]], [[12., 13., 14.], [18., 19., 20.]], [[15., 16., 17.], [21., 22., 23.]]]), size=(4, 6), nnz=4, dtype=torch.float64, layout=torch.sparse_bsc)
处理稀疏压缩张量的工具
所有稀疏压缩张量(包括CSR、CSC、BSR和BSC张量)在概念上非常相似,它们的索引数据被分为两部分:一部分是使用CSR编码的压缩索引,另一部分是与压缩索引正交的普通索引。这使得这些张量上的各种工具可以共享由张量布局参数化的相同实现。
构建稀疏压缩张量
Sparse CSR、CSC、BSR 和 CSC 张量可以通过使用函数 torch.sparse_compressed_tensor()
构造,该函数与构造函数 torch.sparse_csr_tensor()
、torch.sparse_csc_tensor()
、torch.sparse_bsr_tensor()
和 torch.sparse_bsc_tensor()
具有相同的接口,但需要额外的必需参数 layout
。以下示例说明了如何使用相同的输入数据通过指定相应的布局参数来构造 CSR 和 CSC 张量:
>>> compressed_indices = torch.tensor([0, 2, 4]) >>> plain_indices = torch.tensor([0, 1, 0, 1]) >>> values = torch.tensor([1, 2, 3, 4]) >>> csr = torch.sparse_compressed_tensor(compressed_indices, plain_indices, values, layout=torch.sparse_csr) >>> csr tensor(crow_indices=tensor([0, 2, 4]), col_indices=tensor([0, 1, 0, 1]), values=tensor([1, 2, 3, 4]), size=(2, 2), nnz=4, layout=torch.sparse_csr) >>> csc = torch.sparse_compressed_tensor(compressed_indices, plain_indices, values, layout=torch.sparse_csc) >>> csc tensor(ccol_indices=tensor([0, 2, 4]), row_indices=tensor([0, 1, 0, 1]), values=tensor([1, 2, 3, 4]), size=(2, 2), nnz=4, layout=torch.sparse_csc) >>> (csr.transpose(0, 1).to_dense() == csc.to_dense()).all() tensor(True)
支持的操作
线性代数运算
下表总结了支持的稀疏矩阵线性代数操作,其中运算符的布局可能不同。这里T[layout]
表示具有特定布局的张量。M[layout]
表示一个矩阵(2-D PyTorch 张量),而V[layout]
表示一个向量(1-D PyTorch 张量)。此外,f
表示标量(浮点数或 0-D PyTorch 张量),*
表示逐元素乘法,@
表示矩阵乘法。
PyTorch操作 |
稀疏梯度? |
布局签名 |
---|---|---|
不 |
|
|
不 |
|
|
不 |
|
|
不 |
|
|
不 |
|
|
不 |
|
|
不 |
|
|
不 |
|
|
不 |
|
|
是 |
|
|
不 |
|
|
不 |
|
|
不 |
|
|
不 |
|
|
不 |
|
|
不 |
|
|
是 |
|
|
不 |
|
|
不 |
|
|
不 |
|
|
是 |
|
|
是 |
|
其中,“稀疏梯度?”列表示该PyTorch操作是否支持针对稀疏矩阵参数的反向传播。除了torch.smm()
之外,所有PyTorch操作都支持针对带步长矩阵参数的反向传播。
注意
目前,PyTorch 不支持布局标识为 M[strided] @ M[sparse_coo]
的矩阵乘法。然而,应用程序仍然可以使用以下矩阵关系来计算:D @ S == (S.t() @ D.t()).t()
张量方法与稀疏性
以下与稀疏张量相关的张量方法:
如果张量采用稀疏COO存储布局,则值为 |
|
如果张量采用稀疏CSR存储布局,则值为 |
|
返回 |
|
返回 |
|
返回一个新的稀疏张量,该张量的值来自步进张量 |
|
返回张量的稀疏版本。 |
|
将张量转换为坐标格式。 |
|
将张量转换为压缩稀疏行(CSR)格式。 |
|
将张量转换为压缩列存储(CSC)格式。 |
|
将张量转换为给定块大小的块稀疏行(BSR)存储格式。 |
|
将张量转换为给定块大小的块稀疏列(BSC)存储格式。 |
|
如果 |
|
返回稀疏 COO 张量的值张量。 |
以下张量方法专为稀疏 COO 张量设计:
如果 |
|
将 |
|
从一个稀疏张量 |
|
如果 |
|
返回稀疏 COO 张量的索引张量。 |
当 |
|
当 如果 |
以下张量方法支持稀疏COO张量:
`add()` `add_()` `mm()` `mul()` `mul_()` `mv()` `narrow_copy()` `neg()` `neg_()` `negative()` `negative_()` `numel()` `pow()` `sqrt()` `square()` `smm()` `sspaddmm()` `sub()` `sub_()` `t()` `t_()` `transpose()` `transpose_()` `zero_()` `get_device()` `index_select()` `isnan()` `log1p()` `log1p_()` `resize_as_()` `size()` `floor_divide()` `floor_divide_()` `rad2deg()` `rad2deg_()`
专为稀疏张量设计的Torch函数
根据指定的 |
|
构建一个CSR(压缩稀疏行)格式的稀疏张量,并在给定的 |
|
构建一个CSC(压缩稀疏列)格式的稀疏张量,并在给定的 |
|
构建一个以指定二维块形式的BSR(块压缩稀疏行)格式的稀疏张量,这些块位于给定的 |
|
构建一个BSC(块压缩稀疏列)格式的稀疏张量,在给定的 |
|
构建一个以压缩稀疏格式(CSR、CSC、BSR 或 BSC)表示的稀疏张量,并在给定的 |
|
返回给定稀疏张量每行的元素和。 |
|
此函数在前向传播中与 |
|
在由 |
|
对稀疏矩阵 |
|
矩阵先将稀疏张量 |
|
执行一个稀疏COO矩阵 |
|
对稀疏矩阵 |
|
使用softmax函数。 |
|
计算具有唯一解的平方线性方程组的解。 |
|
先应用softmax函数,然后计算结果的对数。 |
|
通过将 |
其他功能
以下 torch
函数支持稀疏张量:
cat()
dstack()
empty()
empty_like()
hstack()
index_select()
is_complex()
is_floating_point()
is_nonzero()
is_same_size()
is_signed()
is_tensor()
lobpcg()
mm()
native_norm()
pca_lowrank()
select()
stack()
svd_lowrank()
unsqueeze()
vstack()
zeros()
zeros_like()
有关管理稀疏张量不变量检查的内容,请参见:
一个用于控制稀疏张量不变性检查的工具。 |
要使用稀疏张量和 gradcheck()
函数,请参见:
使用装饰器函数来扩展稀疏张量的梯度检查功能。 |
保零一元函数
我们的目标是支持所有“保零一元函数”,即把零映射为零的函数。
如果你发现缺少了你需要的保持零值不变的一元函数,请提出功能请求。如常,提交问题前请先使用搜索功能。
以下运算符目前支持稀疏 COO、CSR、CSC、BSR 和 CSR 格式的张量输入。
abs()
asin()
asinh()
atan()
atanh()
ceil()
conj_physical()
floor()
log1p()
neg()
round()
sin()
sinh()
sign()
sgn()
signbit()
tan()
tanh()
trunc()
expm1()
sqrt()
angle()
isinf()
isposinf()
isneginf()
isnan()
erf()
erfinv()