對pytorch中Tensor的剖析


不是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層面也有一個對應的函數:

int THTensor_(isContiguous)(const THTensor *self)
判斷 Tensor 在內存中是否連續。 定義在 TH/generic/THTensor.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的數據結構,原來矩陣庫都是一毛一樣的。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM