Pytorch源代碼分析

1.Tensor

張量(Tensor)是現(xiàn)代機(jī)器學(xué)習(xí)的基礎(chǔ)喜最。它的核心是一個(gè)數(shù)據(jù)容器皮钠。


張量.png

Tensor在pytorch中的實(shí)現(xiàn)

Tensor由存儲(chǔ)和解釋組成怕享。存儲(chǔ)即底層數(shù)據(jù)的存儲(chǔ)空間同眯,和內(nèi)存管理相關(guān)南蓬;解釋賦予了存儲(chǔ)空間含義调炬,例如存儲(chǔ)空間的數(shù)據(jù)類型是什么语盈,數(shù)據(jù)的維度是多少等等。


例1.png
  • Tensor類在aten/src/ATen/templates/TensorBody.h
/// Returns a `Tensor`'s dtype (`TypeMeta`). Defined in TensorMethods.h
caffe2::TypeMeta dtype() const noexcept;

/// Returns a `Tensor`'s device.
Device device() const;

int64_t numel() const ;
  // Length of one array element in bytes.  This is the traditional
  // Numpy naming.
size_t itemsize() const;
...
//TensorImpl指針
c10::intrusive_ptr<TensorImpl, UndefinedTensorImpl> impl_;

Tensor類提供了眾多成員方法查詢Tensor的相關(guān)信息缰泡。在pytorch里真正意義上的Tensor實(shí)現(xiàn)類是TensorImpl刀荒,Tensor類中包含impl_這個(gè)TensorImpl類變量。

  • TensorImpl類在c10/core/TensorImpl.h
//存儲(chǔ)類
Storage storage_;
//自動(dòng)梯度元數(shù)據(jù)
std::unique_ptr<c10::AutogradMetaInterface> autograd_meta_ = nullptr;

TensorImpl維護(hù)一個(gè)存儲(chǔ)類(Storage)的對(duì)象(storage_)棘钞,即Storage表示了Tensor存儲(chǔ)空間照棋。

  • Storage類在c10/core/Storage.h
c10::intrusive_ptr<StorageImpl> storage_impl_;

和Tensor、TensorImpl類比武翎,Storage類中包含StorageImpl類變量烈炭,pytorch真正意義上的存儲(chǔ)類是StorageImpl

  • Storage類在c10/core/StorageImpl.h
private:
  DataPtr data_ptr_;//數(shù)據(jù)指針
  size_t size_bytes_;//所占的空間大小
  bool resizable_;
  // Identifies that Storage was received from another process and doesn't have
  // local to process cuda memory allocation
  bool received_cuda_;
  Allocator* allocator_;//分配器

其中,最重要的是分配器(Allocator)和數(shù)據(jù)指針(DataPtr)宝恶。

  • DataPtr類和Allocator類的定義在c10/core/Allocator.h
// A DataPtr is a unique pointer (with an attached deleter and some
// context for the deleter) to some memory, which also records what
// device is for its data.
Dataptr指向一段內(nèi)存符隙,并且記錄了數(shù)據(jù)所在設(shè)備和內(nèi)存的析構(gòu)函數(shù)
class DataPtr
private:
  c10::detail::UniqueVoidPtr ptr_;//指向內(nèi)存的指針并帶有析構(gòu)函數(shù)
  Device device_;

class UniqueVoidPtr 
private:
  // Lifetime tied to ctx_
  void* data_;
//當(dāng)data_指針生命周期到趴捅,自動(dòng)執(zhí)行DeleteFnPtr函數(shù)
  std::unique_ptr<void, DeleterFnPtr> ctx_;

因此數(shù)據(jù)指針DataPtr三個(gè)重要部分是void* data指針、設(shè)備類型霹疫、deleter拱绑。


struct C10_API Allocator {
  virtual ~Allocator() = default;
//分配指定大小的內(nèi)存,返回DataPtr丽蝎,用來Storage使用
  virtual DataPtr allocate(size_t n) const = 0;
  virtual DeleterFnPtr raw_deleter() const {
    return nullptr;
  }
//分配指定大小的內(nèi)存猎拨,返回指針
  void* raw_allocate(size_t n) {
    auto dptr = allocate(n);
    AT_ASSERT(dptr.get() == dptr.get_context());
    return dptr.release_context();
  }
//釋放指針指向空間
  void raw_deallocate(void* ptr) {
    auto d = raw_deleter();
    AT_ASSERT(d);
    d(ptr);
  }
};

Allocator類作為分配類的基類,有三個(gè)重要的函數(shù):分配返回原始指針屠阻、分配返回DataPtr红省、釋放空間。其中virtual DataPtr allocate(size_t n) const = 0;虛擬方法供不同的繼承類實(shí)現(xiàn)国觉。pytorch實(shí)現(xiàn)了不同內(nèi)存的分配類吧恃。

  1. DefaultCPUAllocator類在c10/core/CPUAllocator.cpp
    分配CPU內(nèi)存
at::DataPtr allocate(size_t nbytes) const override {
    void* data = alloc_cpu(nbytes);
    profiledCPUMemoryReporter().New(data, nbytes);
    return {data, data, &ReportAndDelete, at::Device(at::DeviceType::CPU)};
  }

其中分配函數(shù)alloc_cpu調(diào)用 posix_memalign或者_aligned_malloc或者memalign

void* alloc_cpu(size_t nbytes) {
... 
void* data;
#ifdef __ANDROID__
  data = memalign(gAlignment, nbytes);
#elif defined(_MSC_VER)
  data = _aligned_malloc(nbytes, gAlignment);
#else
  int err = posix_memalign(&data, gAlignment, nbytes);
 ...
#endif
...
  return data;
}
  1. THCCachingHostAllocator類在aten/src/THC/THCCachingHostAllocator.cpp
    分配pinned memory
  2. CudaCachingAllocator類在c10/cuda/CUDACachingAllocator.cpp
    分配GPU內(nèi)存

2.數(shù)據(jù)讀入

數(shù)據(jù)讀入深度學(xué)習(xí)程序運(yùn)行的第一個(gè)操作。數(shù)據(jù)讀入也就是把磁盤中的數(shù)據(jù)讀入內(nèi)存麻诀,又可以分為兩個(gè)部分:第一步痕寓,在內(nèi)存中分配空間,寫入分配的內(nèi)存中蝇闭。
在pytorch呻率,數(shù)據(jù)都是以Tensor形式存在的。因此呻引,數(shù)據(jù)的輸入以創(chuàng)建Tensor再填充Tensor的形式礼仗。填充Tensor也就是對(duì)Tensor的數(shù)據(jù)內(nèi)存上賦值,通過數(shù)據(jù)指針訪問內(nèi)存苞七。

創(chuàng)建Tensor

創(chuàng)建Tensor主要是分配Tensor所需的空間
pytorch提供empty函數(shù)創(chuàng)建一個(gè)Tensor
在cpu上的實(shí)現(xiàn)代碼在aten/src/ATen/native/TensorFactories.cpp
在cuda上的實(shí)現(xiàn)代碼aten/src/ATen/native/cuda/TensorFactories.cu
empty的主要流程可以分為4個(gè)部分藐守。

1.分配器
c10::Allocator* allocator;
2.計(jì)算分配空間
int64_t size_bytes = nelements * dtype.itemsize();
3.構(gòu)建storageImpl對(duì)象
auto storage_impl = c10::make_intrusive<StorageImpl>(
      c10::StorageImpl::use_byte_size_t(),
      size_bytes,
      allocator->allocate(size_bytes),//返回DataPtr對(duì)象
      allocator,
      /*resizeable=*/true);
4.構(gòu)建Tensor
auto tensor = detail::make_tensor<TensorImpl>(
      std::move(storage_impl), at::DispatchKey::CPU, dtype);

CIFAR10數(shù)據(jù)集輸入

CIFAR10二進(jìn)制文件

一個(gè)CIFAR10二進(jìn)制文件包括10000條記錄,每一條記錄包括label和數(shù)據(jù)蹂风,數(shù)據(jù)是一張33232的圖片的像素值卢厂,二進(jìn)制文件以這樣的格式存儲(chǔ)。因此文件大小是10000*3073個(gè)字節(jié)

<1×標(biāo)簽> <3072×像素>
....
<1×標(biāo)簽> <3072×像素>
CIFAR10數(shù)據(jù)輸入
1.創(chuàng)建10000*3*32*32的圖像Tensor和10000的labelTensor
images_ = torch::empty({10000, 3,32, 32}, torch::kByte);
targets_ =  torch::empty(10000, torch::kByte);
2.根據(jù)目錄創(chuàng)建輸入流
std::ifstream reader(path,std::ios::binary);
for(int i=0;i<10000;i++){
      3.輸入流讀取指定的字節(jié)到Tensor的數(shù)據(jù)指針指向的內(nèi)存
      reader.read(reinterpret_cast<char*>(targets_.data_ptr())+i/**偏移值**/,1);
      reader.read(reinterpret_cast<char*>(images_.data_ptr())+i*3072/**偏移值**/,3072);
}

代碼創(chuàng)建的Images_,targets_是一個(gè)二進(jìn)制文件的輸入Tensor惠啄,包含10000張圖片慎恒。之后如果設(shè)置batch size,可以根據(jù)偏移取輸入Tensor的子Tensor作為一個(gè)iteration的輸入撵渡。

3.計(jì)算

深度學(xué)習(xí)的核心部分就是計(jì)算操作融柬,深度學(xué)習(xí)的訓(xùn)練過程包括了一系列的數(shù)學(xué)運(yùn)算。pytorch里實(shí)現(xiàn)成百上千個(gè)計(jì)算子趋距,包括簡(jiǎn)單的加減數(shù)學(xué)運(yùn)算和復(fù)雜的卷積操作等粒氧。

1.前向計(jì)算函數(shù)
static inline Tensor mul(const Tensor & self, const Tensor & other);
static inline Tensor & mul_out(Tensor & out, const Tensor & self, const Tensor & other);
static inline Tensor mul(const Tensor & self, Scalar other);
static inline Tensor mv(const Tensor & self, const Tensor & vec);
2后向計(jì)算函數(shù)
variable_list MulBackward0::apply(variable_list&& grads) ;
variable_list MvBackward::apply(variable_list&& grads);

在pytorch中,最簡(jiǎn)單的計(jì)算過程代碼是

y = net(input);
y.backward();

這兩句分別代表了前向計(jì)算和后向計(jì)算节腐。

網(wǎng)絡(luò)結(jié)構(gòu)

網(wǎng)絡(luò)由layer組成外盯,layer封裝了計(jì)算子和參數(shù)摘盆。

net是由三層全連接層組成的。

struct net : torch::nn::Module{
    torch::Tensor forward(Torch::Tensor x){
        torch::Tensor x1 = fc1->forward(x);
        torch::Tensor x2 = fc2->forward(x);
        torch::Tensor x3 = fc3->forward(x);
    }
    torch::nn::Linear fc1;
    torch::nn::Linear fc2;
    torch::nn::Linear fc2;
};
前向計(jì)算

net的前向計(jì)算依次經(jīng)過三層全連接層的forward


net.png

那么具體全連接層的forward執(zhí)行什么樣的操作饱苟,先了解全連接層的類定義

class TORCH_API LinearImpl : public Cloneable<LinearImpl> {
 public:
  ...
  Tensor forward(const Tensor& input);
  LinearOptions options;
  Tensor weight;
  Tensor bias;
  ...
};

看代碼可以知道LinearImpl類中含有參數(shù)weight和bias孩擂,由此可見,網(wǎng)絡(luò)的參數(shù)其實(shí)是由每一層的參數(shù)組成箱熬。再看一下forward函數(shù)的具體實(shí)現(xiàn)

return torch::addmm(bias, input, weight.t());

追蹤代碼类垦,發(fā)現(xiàn)foward函數(shù)最終調(diào)用的addmm計(jì)算操作,輸入為linear層的輸入城须,參數(shù)weight和bias蚤认。
那么這個(gè)網(wǎng)絡(luò)的前向計(jì)算也可以展開為


net展開.png

類似的,復(fù)雜的網(wǎng)絡(luò)前向計(jì)算過程也可以看成參數(shù)酿傍、輸入烙懦、中間數(shù)據(jù)經(jīng)過計(jì)算子的計(jì)算圖

后向計(jì)算

后向計(jì)算的作用是更新參數(shù)的梯度驱入,更新梯度需要用到中間數(shù)據(jù)赤炒,因此中間數(shù)據(jù)暫存在內(nèi)存中。x1雖然在執(zhí)行第三次addmm操作時(shí)沒有被訪問亏较,但是需要暫存在內(nèi)存等待后向傳播時(shí)使用莺褒。
前向傳播的數(shù)學(xué)公式為
x1 = w1x+b1 x2 = w2x1+b2 x3 = w3x2+b3
后向傳播的數(shù)學(xué)公式為
\frac{\partial x3 }{\partial x2}=w3 \frac{\partial x3 }{\partial w3}=x2 \frac{\partial x3 }{\partial b3}=1
\frac{\partial x3}{\partial x1}= \frac{\partial x3 }{\partial x2}.w2 \frac{\partial x3}{\partial w2}= \frac{\partial x3 }{\partial x2}.x1 \frac{\partial x3}{\partial b2}= \frac{\partial x3 }{\partial x2}.1
\frac{\partial x3}{\partial x}= \frac{\partial x3 }{\partial x1}.w1 \frac{\partial x3}{\partial w1}= \frac{\partial x3 }{\partial x1}.x \frac{\partial x3}{\partial b1}= \frac{\partial x3 }{\partial x1}.1
從公式中可以發(fā)現(xiàn),后向傳播的計(jì)算遵從鏈?zhǔn)椒▌t雪情,梯度從后向前傳播遵岩。
根據(jù)圖和公式,
addmm前向計(jì)算的輸入是w1巡通、x尘执、b1,輸出是x1
后向傳播的輸入是w1宴凉、x誊锭、b1和\frac{\partial x3}{\partial x1}(目標(biāo)值對(duì)前向輸出x1的梯度),輸出是目標(biāo)值對(duì)w1弥锄、x丧靡、b1的梯度
那么pytorch是怎么實(shí)現(xiàn)這樣的計(jì)算呢
與操作前向相對(duì)應(yīng)的,pytorch也實(shí)現(xiàn)了操作的后向計(jì)算籽暇,例如addmm的后向函數(shù)是AddmmBackward
但是温治,與前向不同的時(shí),后向傳播搭建顯示的靜態(tài)計(jì)算圖戒悠。構(gòu)成靜態(tài)圖的節(jié)點(diǎn)是后向操作熬荆,通過邊連接后向傳播的輸入輸出。例子的后向過程就如下圖所示

后向展開.png

后向函數(shù)的輸入除了前一個(gè)后向函數(shù)的輸出绸狐,還有前向函數(shù)的輸入卤恳,也就是說前向函數(shù)的輸入也是后向函數(shù)的輸入捏顺。
那么后向是怎么通過后向操作構(gòu)建計(jì)算流程的呢,后向操作又是怎么訪問前向操作的輸入呢纬黎?
Node是一個(gè)關(guān)鍵的類幅骄,定義在torch/csrc/autograd/function.h
各種后向操作都繼承了這個(gè)類,AddmmBackward也是繼承了Node類本今。
在Tensor類定義中,有g(shù)rad_fn成員函數(shù)
const std::shared_ptr<torch::autograd::Node>& grad_fn() const;
grad_fn理解為梯度函數(shù)拆座,這個(gè)函數(shù)的意思是,tensor生成操作的后向函數(shù)冠息。例如x1由前向傳播圖中第一個(gè)addmm生成挪凑,x1.grad_fn()就是后向傳播圖上的第一個(gè)AddmmBackward
x1的梯度函數(shù)是什么時(shí)候創(chuàng)建的呢逛艰,是在進(jìn)行前向計(jì)算的過程中躏碳。

1. 創(chuàng)建后向函數(shù)
std::shared_ptr<AddmmBackward> grad_fn;
  if (compute_requires_grad( self, mat1, mat2 )) {
    grad_fn = std::shared_ptr<AddmmBackward>(new AddmmBackward(), deleteNode);
    grad_fn->set_next_edges(collect_next_edges( self, mat1, mat2 ));
    2. 保存后向函數(shù)需要的輸入,這里的mat1散怖,mat2也是前向函數(shù)的輸入
    grad_fn->mat1_ = SavedVariable(mat1, false);
    grad_fn->mat2_ = SavedVariable(mat2, false);
    grad_fn->alpha = alpha;
    grad_fn->mat2_sizes = mat2.sizes().vec();
    grad_fn->beta = beta;
  }
3 進(jìn)行前向計(jì)算菇绵,result即為前向函數(shù)的輸出Tensor
auto tmp = ([&]() {
    at::AutoNonVariableTypeMode non_var_type_mode(true);
    return at::addmm(self_, mat1_, mat2_, beta, alpha);
  })();
  auto result = std::move(tmp);
4 設(shè)置result的梯度函數(shù)
if (grad_fn) {
      set_history(flatten_tensor_args( result ), grad_fn);
  }

經(jīng)過4個(gè)步驟,在進(jìn)行前向傳播的同時(shí)镇眷,對(duì)每一個(gè)輸出Tensor都構(gòu)建后向操作咬最,同時(shí)把輸入的數(shù)據(jù)放入后向操作變量中,對(duì)這些后向操作節(jié)點(diǎn)構(gòu)建圖(計(jì)算流程)欠动。
x3.backward()這一句簡(jiǎn)單的后向傳播也就可以分解為永乌,

通過x3.grad_fn()獲得梯度函數(shù),執(zhí)行這個(gè)函數(shù)具伍,輸出三個(gè)梯度翅雏。
x3.grad_fn()的下一個(gè)節(jié)點(diǎn)是x2.grad_fn(),并且其中x3對(duì)x2的梯度作為x2.grad_fn()的一個(gè)輸入。最后執(zhí)行x1.grad_fn()人芽。
最終望几,參數(shù)w1、w2啼肩、w3橄妆、b1、b2祈坠、b3都獲得了梯度害碾。


后向傳播的代碼主要在torch/csrc/autograd

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市赦拘,隨后出現(xiàn)的幾起案子慌随,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阁猜,死亡現(xiàn)場(chǎng)離奇詭異丸逸,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)剃袍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門黄刚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人民效,你說我怎么就攤上這事憔维。” “怎么了畏邢?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵业扒,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我舒萎,道長(zhǎng)程储,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任臂寝,我火速辦了婚禮章鲤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘交煞。我一直安慰自己咏窿,他們只是感情好斟或,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布素征。 她就那樣靜靜地躺著,像睡著了一般萝挤。 火紅的嫁衣襯著肌膚如雪御毅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天怜珍,我揣著相機(jī)與錄音端蛆,去河邊找鬼。 笑死酥泛,一個(gè)胖子當(dāng)著我的面吹牛今豆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播柔袁,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼呆躲,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了捶索?” 一聲冷哼從身側(cè)響起插掂,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后辅甥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酝润,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年璃弄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了要销。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡夏块,死狀恐怖蕉陋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拨扶,我是刑警寧澤凳鬓,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站患民,受9級(jí)特大地震影響缩举,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜匹颤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一仅孩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧印蓖,春花似錦辽慕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至他宛,卻和暖如春船侧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厅各。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工镜撩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人队塘。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓袁梗,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親憔古。 傳聞我的和親對(duì)象是個(gè)殘疾皇子遮怜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348