不是python層面Tensor的剖析,是C層面的剖析。
看pytorch下lib庫中的TH好一陣子了,TH也是torch7下面的一個重要的庫。
可以在torch的github上看到相關文檔。看了半天才發現pytorch借鑒了很多torch7的東西。
pytorch大量借鑒了torch7下面lua寫的東西並且做了更好的設計和優化。
https://github.com/torch/torch7/tree/master/doc
pytorch中的Tensor是在TH中實現的。TH = torch
TH中先實現了一個THStorage,再在THStorage的基礎上實現了THTensor。
THStorage定義如下,定義在TH/generic/THStorage.h中
1 typedef struct THStorage 2 { 3 real *data; 4 ptrdiff_t size; 5 int refcount; 6 char flag; 7 THAllocator *allocator; 8 void *allocatorContext; 9 struct THStorage *view; 10 } THStorage;
這些成員里重點關注*data和size就可以了。
real *data中的real會在預編譯的時候替換成預先設計的數據類型,比如int,float,byte等。
比如 int a[3] = {1,2,3},data是數組a的地址,對應的size是3,不是sizeof(a)。
所以*data指向的是一段連續內存。是一維的!
講Tensor前先回顧下數組在內存中的排列方式。參看《C和指針》8.2節相關內容。
比如 int a[3][6]; 內存中的存儲順序為:
00 01 02 03 04 05 10 11 12 13 14 15 20 21 22 23 24 25
是連續存儲的。存儲順序按照最右邊的下標率先變化。
然后數組a是2維的,nDimension = 2。dimension從0開始算起。
size(a) = {3,6}
[3] 是 dimension 0 size[0] = 3
[6] 是 dimension 1 size[1] = 6
nDimension = 2
THTensor定義如下,定義在TH/generic/THTensor.h中
1 typedef struct THTensor 2 { 3 int64_t *size; // 注意是指針 4 int64_t *stride; // 注意是指針 5 int nDimension; 6 7 // Note: storage->size may be greater than the recorded size 8 // of a tensor 9 THStorage *storage; 10 ptrdiff_t storageOffset; 11 int refcount; 12 char flag; 13 } THTensor;
比如
z = torch.Tensor(2,3,4) // 新建一個張量,size為 2,3,4
size(z) = {2,3,4}
[2] 是 dimension 0 size[0] = 2
[3] 是 dimension 1 size[1] = 3
[4] 是 dimension 2 size[2] = 4
nDimension = 3
THStorage只管理內存,是一維的。
THTensor通過size和nDimension將THStorage管理的一維內存映射成邏輯上的多維張量,
底層還是一維的。但是注意,代表某個Tensor的底層內存是一維的但是未必是連續的!
把Tensor按照數組來理解好了。
Tensor a[3][6] 裁剪(narrow函數)得到一個 Tensor b[3][4],在內存中就是
Tensor a: 00 01 02 03 04 05 10 11 12 13 14 15 20 21 22 23 24 25 Tensor b: 00 01 02 03 x x 10 11 12 13 x x 20 21 22 23 x x
narrow函數並不會真正創建一個新的Tensor,Tensor b還是指向Tensor a的那段內存。
所以Tensor b在內存上就不是連續的了。
那么怎么體現Tensor在內存中是連續的呢?就靠THTensor結構體中的
size,stride,nDimension共同判斷了。
pytorch的Tensor有個 contiguous 函數,C層面也有一個對應的函數:
1 int THTensor_(isContiguous)(const THTensor *self) 2 { 3 int64_t z = 1; 4 int d; 5 for(d = self->nDimension-1; d >= 0; d--) 6 { 7 if(self->size[d] != 1) 8 { 9 if(self->stride[d] == z) 10 z *= self->size[d]; // 如果是連續的,應該在這循環完然后跳到下面return 1 11 else 12 return 0; 13 } 14 } 15 return 1; 16 }
把Tensor a[3][6] 作為這個函數的參數:
size[0] = 3 size[1] = 6 nDimension = 2 z =1
d = 1 if size(1) = 6 != 1 if stride[1] == 1 z = z*size(d)=6
d = 0 if size(0) = 3 != 1 if stride[0] == 6 z = z*size(d)=6*3 = 18
因此,對於連續存儲的a
stride = {6,1}
size = {3,6}
再舉一個Tensor c[2][3][4]的例子,如果c是連續存儲的,則:
stride = {12,4,1}
size = { 2,3,4} // 2所對應的stride就是 右邊的數相乘(3x4), 3所對應的stride就是右邊的數相乘(4)
stride(i)返回第i維的長度。stride又被翻譯成步長。
比如第0維,就是[2]所在的維度,Tensor c[ i ][ j ][ k ]跟Tensor c[ i+1 ][ j ][ k ]
在連續內存上就距離12個元素的距離。
對於內存連續的stride,計算方式就是相應的size數右邊的數相乘。
所以不連續呢?
對於a[3][6]
stride = {6,1}
size = {3,6}
對於從a中裁剪出來的b[3][4]
stride = {6,1}
size = {3,4}
stride和size符合不了 右邊的數相乘 的計算方法,所以就不連續了。
所以一段連續的一維內存,可以根據size和stride 解釋 成 邏輯上變化萬千,內存上是否連續 的張量。
比如24個元素,可以解釋成 4 x 6 的2維張量,也可以解釋成 2 x 3 x 4 的3維張量。
THTensor中的 storageOffset 就是說要從 THStorage 的第幾個元素開始 解釋 了。
連續的內存能給程序並行化和最優化算法提供很大的便利。
其實寫這篇博客是為了給理解 TH 中的 TH_TENSOR_APPLY2 等宏打基礎。
這個宏就像是在C中實現了broadcast。
2017年12月11日01:00:22
最近意識到,用 H x W x C 和 C x H x W 哪個來裝圖像更好,取決於矩陣在內存中是行存儲還是
列存儲,這個會影響內存讀取速度,進而影響算法用時。
后來意識到,這就是個cache-friendly的問題,大部分對程序性能的要求還上升不到要研究算法復雜度
這個地步,常規優化的話注意下緩存友好等問題就好了,再優化就要靠更專業團隊寫的庫或者榨干硬件了。
看了下numpy的文檔,怪不得說pytorch是numpy的gpu版本。。。
后來又看了下opencv的mat的數據結構,原來矩陣庫都是一毛一樣的。。。