跳转至

python与C/C++数据交互的陷阱:numpy/torch中的视图

前言

最近碰到了一个bug,调试了两天。最后发现是数据存储排列方式的问题,由此忽然想到pytorch或者numpy中tensor(或者ndarray)里面视图的概念。

视图

  • 视图是数据的一个别称或引用,通过该别称或引用亦便可访问、操作原有数据,但原有数据不会产生拷贝。如果我们对视图进行修改,它会影响到原始数据,物理内存在同一位置。
  • 副本是一个数据的完整的拷贝,如果我们对副本进行修改,它不会影响到原始数据,物理内存不在同一位置。

视图一般发生在:

  1. numpy的切片操作返回原数据的视图。
  2. 调用 ndarray 的 view() 函数产生一个视图。
  3. 调用 transpose, .T
  4. 调用 np.split
  5. 调用其他不会发生内存copy的函数

特别的,如果对于 contiguous = False 的数据,直接对其.shape赋值会报错,调用reshape操作会发生copy操作。 副本一般发生在:

  1. Python 序列的切片操作,调用deepCopy()函数。
  2. 调用 ndarray 的 copy() 函数产生一个副本。

bug复现

既然创建视图不会发生内存复制,那对于所有拥有不同视图却拥有相同数据存储地址的ndarray或者tensor来说,指向的空间是一样的,这样python中数据传入c/c++时读取的实际上还是原数据。 比如针对于tensor A,现在想把A.T利用ctypes或者cython传给c程序,直接传入A.T就会产生bug,而且不易查找。

import numpy as np
A = np.random.rand(1,9,2)
B = A.T
np.savez('testB.npz', b = B)
C = np.load('testB.npz')
newB = C['b']
print(newB)
print(A)
# 按照内存排列方式查看数据
print(newB.ravel(order='K'))
print(A.ravel(order='K'))
正如上面code所示,如果直接看A或者newB,恰好可以看到newB恰好是A的转置,即使反序列化以后也是一样,但是按照内存排列方式打印A和newB就可以看到他们的数据排列是一样的。

这里ravel函数的order参数解释如下 order{‘C’,’F’, ‘A’, ‘K’}, optional The elements of a are read using this index order. ‘C’ means to index the elements in row-major, C-style order, with the last axis index changing fastest, back to the first axis index changing slowest. ‘F’ means to index the elements in column-major, Fortran-style order, with the first index changing fastest, and the last index changing slowest. Note that the ‘C’ and ‘F’ options take no account of the memory layout of the underlying array, and only refer to the order of axis indexing. ‘A’ means to read the elements in Fortran-like index order if a is Fortran contiguous in memory, C-like order otherwise. ‘K’ means to read the elements in the order they occur in memory, except for reversing the data when strides are negative. By default, ‘C’ index order is used. 所以尽管我们的目的是想给C/C++函数传入A的转置,但传入的依然是A。A.T仅仅是创建了一个新的视图,并且其数据也不再是contiguous,另外打印A.T.flags会发现

1
2
3
4
5
6
7
C_CONTIGUOUS : False
F_CONTIGUOUS : True
OWNDATA : False
WRITEABLE : True
ALIGNED : True
WRITEBACKIFCOPY : False
UPDATEIFCOPY : False
新的视图变成了列连续。(C代表C语言格式存储,行优先,F代表Fortran 表示列优先) 那么该如何解决呢? 实际上解决思路很简单,那就是将按照数据的视图重新复制一遍数据,将其变为contiguous的。即传入A的转置时调用np.ascontiguous(A.T)即可将其数据copy一边并设置为行优先存储。

小结

这个bug实在隐蔽,我对计算图的某个模块一层一层的打印和校对才找到,解决问题后分析原因写下这篇文章避免以后再出错。


最后更新: March 21, 2024
创建日期: March 21, 2024