本文最后更新于:2022年8月10日 上午
numpy 和 pytorch tensor 存在内存是否连续的情况,对运行速度甚至网络运行结果都存在影响。
含义
contiguous 本身是形容词**,**表示连续的。所谓contiguous array
,指的是数组在内存中存放的地址也是连续的(注意内存地址实际是一维的),即访问数组中的下一个元素,直接移动到内存中的下一个地址就可以。
在numpy和torch的数据结构中,都有表示变量是否在内存中数据连续存储的概念。
连续存储又分为按照行优先(C order )和按照 列优先(Fortran order )
行优先 C order
行是指多维数组一维展开的方式,对应的是列优先。C/C++中使用的是行优先方式(row major),Matlab、Fortran使用的是列优先方式(column major),PyTorch中Tensor底层实现是C,也是使用行优先顺序,因此也称为 C order
。
Pascal, C,C++,Python都是行优先存储的。
列优先 Fortran order
而Fortran Order
则指的是列优先的顺序(Column-major Order),即内存中同列的元素存在一起。
数据举例
行优先
考虑一个2维数组arr = np.arange(12).reshape(3,4)
。这个数组看起来结构是这样的:
在计算机的内存里,数组arr
实际存储是像下图所示的:
这意味着arr
是C连续的
(C contiguous
)的,因为在内存是行优先的,即某个元素在内存中的下一个位置存储的是它同行的下一个值。
如果想要向下移动一列,则只需要跳过3个块既可(例如,从0到4只需要跳过1,2和3)。
1 2 3 4 5 6 7 8 import numpy as npif __name__ == '__main__' : arr = np.arange(12 ).reshape(3 , 4 ) print (arr.flags) print (arr) pass
1 2 3 4 5 6 7 8 9 10 11 C_CONTIGUOUS : True F_CONTIGUOUS : False OWNDATA : False WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False UPDATEIFCOPY : False [[ 0 1 2 3 ] [ 4 5 6 7 ] [ 8 9 10 11 ]]
C_CONTIGUOUS 为 True 表示该矩阵行连续
也就是其中的行 [ 0 1 2 3] 在内存中连续,那么 [0 4 8] 就不会连续了,因此 F_CONTIGUOUS 为 False
列优先
上述数组的转置arr.T
则没有了C连续特性,因为同一行中的相邻元素现在并不是在内存中相邻存储的了:
这里要说明一下,如果直接用这些值创建的numpy变量是连续的,因为Python默认 C order,新创建的numpy都是行优先的
但是我们创建arr时是以 0 - 11 为顺序创建的,其中[0 1 2 3] [4 5 6 7] [8 9 10 11]连续,矩阵转置后只改变引用,内存数据并不发生变化
类似的操作如numpy 的 slice
、transpose
、转置
或 tensor中的 permute
等操作都可能导致改变之前数据与内存的行连续状况
转置后,内存上仍然是 [0 1 2 3] [4 5 6 7] [8 9 10 11]连续,在当前矩阵上就是列连续,因此这是个Fortran order 连续的矩阵
1 2 3 4 5 6 7 8 9 import numpy as npif __name__ == '__main__' : arr = np.arange(12 ).reshape(3 , 4 ) arr = arr.T print (arr.flags) print (arr) pass
1 2 3 4 5 6 7 8 9 10 11 12 C_CONTIGUOUS : False F_CONTIGUOUS : True OWNDATA : False WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False UPDATEIFCOPY : False [[ 0 4 8 ] [ 1 5 9 ] [ 2 6 10 ] [ 3 7 11 ]]
查看连续性
numpy
可以使用 data.contiguous
、data.c_contiguous
、 data.f_contiguous
属性
或者使用 flags
属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import numpy as npif __name__ == '__main__' : arr = np.arange(12 ).reshape(3 , 4 ) print (arr.data.contiguous) print (arr.data.c_contiguous) print (arr.data.f_contiguous) print (arr.flags) print (arr) pass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 True True False C_CONTIGUOUS : True F_CONTIGUOUS : False OWNDATA : False WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False UPDATEIFCOPY : False [[ 0 1 2 3 ] [ 4 5 6 7 ] [ 8 9 10 11 ]]
pytorch
pytorch 的 tensor 有方法 is_contiguous
用来查看是否 C 连续
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import torchimport numpy as npif __name__ == '__main__' : arr = np.arange(12 ).reshape(3 , 4 ) ten = torch.from_numpy(arr) print (ten.is_contiguous()) ten_t = torch.from_numpy(arr.T) print (ten_t.is_contiguous()) pass
产生的影响
性能影响
从性能上来说,获取内存中相邻的地址比不相邻的地址速度要快很多(从RAM读取一个数值的时候可以连着一起读一块地址中的数值,并且可以保存在Cache中),这意味着对连续数组的操作会快很多。
由于arr
是C连续的,因此对其进行行操作比进行列操作速度要快
通常来说
会比
稍微快些。
同理,在arr.T
上,列操作比行操作会快些。
结果影响
其实写这篇博客的原因,就是我的onnx模型对于完全相同数据的tensor产生了完全不同的表现,险些三观俱碎。挣扎了几个小时后发现原来是数据的连续性在作祟。
平台
影响
numpy
计算不连续的变量,结果不会受到影响
pytorch
输入不连续的tensor,结果不会受到影响
onnx
输入不连续的tensor,结果直接爆炸
在 python 的 tensor 中,如果不是C连续的tensor,在执行 view
方法时会报错:
1 2 invalid argument 2 : view size is not compatible with input tensor spans across two contiguous subspaces). Call .contiguous() before .view().
在 numpy 中某些需要连续的操作在遇到不连续的变量时也会报错:
1 2 ValueError: some of the strides of a given numpy array are negative. This is currently not supported, but will be added in future releases.
连带影响
不连续的numpy转为tensor后也是不连续的
不连续的tensor转为numpy后也是不连续的
修正连续性
变量可以通过重新开辟空间,将数据连续拷贝进去的方法将不连续的数据变成某种连续方式。
numpy
numpy 变量中连续性可以用自带的函数修正,不连续的变量通过函数 np.ascontiguousarray(arr)
变为C连续,np.asfortranarray(arr)
变为Fortran连续
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import numpy as npif __name__ == '__main__' : arr = np.arange(12 ).reshape(3 , 2 , 2 ) print (arr.data.c_contiguous) tran_arr = arr.transpose(2 , 0 , 1 ) print (tran_arr.data.contiguous) c_arr = np.ascontiguousarray(tran_arr) print (c_arr.flags) f_arr = np.asfortranarray(tran_arr) print (f_arr.flags) pass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 True False C_CONTIGUOUS : True F_CONTIGUOUS : False OWNDATA : True WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False UPDATEIFCOPY : False C_CONTIGUOUS : False F_CONTIGUOUS : True OWNDATA : True WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False UPDATEIFCOPY : False
pytorch
pytorch 的 tensor 在python中运行,需要C连续的变量,因此只有C连续的函数 contiguous()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import torchimport numpy as npif __name__ == '__main__' : arr = np.arange(12 ).reshape(3 , 2 , 2 ) tran_arr = arr.transpose(2 , 0 , 1 ) print (tran_arr.data.contiguous) ten = torch.from_numpy(tran_arr) print (ten.is_contiguous()) c_ten = ten.contiguous() print (c_ten.is_contiguous()) c_ten_arr = c_ten.numpy() print (c_ten_arr.flags) pass
1 2 3 4 5 6 7 8 9 10 False False True C_CONTIGUOUS : True F_CONTIGUOUS : False OWNDATA : False WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False UPDATEIFCOPY : False
参考资料