<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Tensor是如何讓你的內(nèi)存/顯存泄漏的

          共 7597字,需瀏覽 16分鐘

           ·

          2022-07-17 09:19

          來源:知乎—Fatescript

          地址:https://zhuanlan.zhihu.com/p/517279248


          01
          前言
          本文適合算法研究員/工程師閱讀,如果你遇到奇怪的內(nèi)存泄漏問題,說不定本文能幫你找到答案,解答疑惑。
          雖然在大部分場景下,程序的內(nèi)存泄漏都和數(shù)據(jù)息息相關(guān)。但是讀完本文你就會了解,沒有被正確使用的Tensor也會導(dǎo)致內(nèi)存和顯存的泄漏。


          02
          起源
          某次組會的時候,同事報告了一個很好玩的issue:我司某組的一個codebase出現(xiàn)了奇怪的泄漏現(xiàn)象,奇怪的點有以下幾個方面:
          (1)不同的模型,內(nèi)存/顯存泄漏的現(xiàn)象不一樣。比如A模型和B模型泄露的速度是不一樣的
          (2)訓(xùn)練同一個模型的時候,如果在dataset中增加了數(shù)據(jù)量,相比不加數(shù)據(jù),會在更早的epoch就把內(nèi)存泄漏完。
          是不是聽起來現(xiàn)象非常離譜,本著”code never lies“的世界觀,我開始探求這個現(xiàn)象的真正原因。


          03
          復(fù)現(xiàn)
          要想解決一個大的問題,首先就要降低問題的復(fù)雜度。最小復(fù)現(xiàn)代碼是我們找問題的基礎(chǔ),而這個寫最小復(fù)現(xiàn)代碼的過程其實也是遵循了一定套路的,此處一并分享給大家:
          • 如果突然出現(xiàn)了歷史上沒有出現(xiàn)過的問題(比如在某個版本之后突然內(nèi)存開始泄漏了),用git bisect找到 first bad commit(前提項目管理的比較科學(xué),不會出現(xiàn)很多feature雜糅在一個commit里面;還有就是git checkout之后復(fù)現(xiàn)問題的成本不高)。如果bisect大法失效,考慮下面的復(fù)現(xiàn)流程。
          • 首先排除data的問題,也就是只創(chuàng)建一個dataloader,讓這個loader不停地供數(shù)據(jù),看看內(nèi)存會不會漲(通常data是一系列對不上點、內(nèi)存泄漏的重災(zāi)區(qū))。
          • 其次排除訓(xùn)練的問題,找一個固定數(shù)據(jù),不停地讓網(wǎng)絡(luò)訓(xùn)練固定數(shù)據(jù)進(jìn)行,看看是否發(fā)生泄漏。這一步主要是檢查模型、優(yōu)化器等組件的問題(通常模型本身不會發(fā)生泄漏,這一步經(jīng)常能查出來一些自定義op的case)
          • 最后就是檢查一些外圍組件了。比如各種自己寫的utils/misc的內(nèi)容。這塊通常不是啥重災(zāi)區(qū)。
          最后給出來我的最小復(fù)現(xiàn)(psutil需要通過pip安裝一下):
          import torchimport osimport psutil

          def log_device_usage(count, use_cuda): mem_Mb = psutil.Process(os.getpid()).memory_info().rss / 1024 ** 2 cuda_mem_Mb = torch.cuda.memory_allocated(0) / 1024 ** 2 if use_cuda else 0 print(f"iter {count}, mem: {int(mem_Mb)}Mb, gpu mem:{int(cuda_mem_Mb)}Mb")

          def leak(): use_cuda = torch.cuda.is_available() val = torch.rand(1).cuda() if use_cuda else torch.rand(1) count = 0 log_iter = 20000 while True: value = torch.rand(1).cuda() if use_cuda else torch.rand(1) val += value.requires_grad_() if count % log_iter == 0: log_device_usage(count, use_cuda) count += 1

          if __name__ == "__main__": leak()
          試著運行一下,你就會發(fā)現(xiàn)你的內(nèi)存和顯存開始泄露了,內(nèi)存泄漏的比顯存更快一些。過一段時間整個程序就會達(dá)到memory的上限卡死,接著被kill掉。作為對比,如果加上去的tensor沒有梯度(比如將requires_grad_()刪掉或者在后面加上detach()),就可以看到?jīng)]有泄漏發(fā)生的log了。
          寫完了最小復(fù)現(xiàn)之后,同事問了我兩個問題,大家可以提前思考一下,這兩個問題也是本文想要解釋清楚的問題:
          1. 為啥上面的程序會出現(xiàn)內(nèi)存/顯存泄漏?
          2. 為啥明明在gpu上的tensor會泄漏內(nèi)存?


          04
          探索
          首先第二個問題很好理解,因為雖然在概念上,torch中的tensor是在gpu上的,但是也只是數(shù)據(jù)的storage在gpu上,除了在顯存上存儲的數(shù)據(jù),tensor的一些其他信息(比如shape,stride和output_nr等)肯定也是要占據(jù)一定內(nèi)存的。所以在cuda available的時候,內(nèi)存和顯存都會泄漏。
          那么第一個問題是因為啥呢?我一時間也難以想明白,于是我打算直接通過torch的源碼去找問題的答案。這個過程略長一些,想要看結(jié)論的讀者可以直接跳到解惑部分。如果對torch內(nèi)部的東西稍微感興趣,可以繼續(xù)看下去。
          因為torch里面有很多code是生成出來的(有機會我們可以講一講torch的code gen),所以我們需要先編譯一下torch(我用的commit hash是2367face)。因為寫torch的cuda extension的時候,要使用Tensor就會需要 include <ATen/ATen.h>,以此為線索我最后定位到了一個叫做TensorBody.h的文件,通過fzf在torch/include/ATen/core下的TensorBody.h文件中找到了inplace add的定義,源碼如下(torch中inplace都是在原來的名字后面加_,比如add和add_)。
          inline at::Tensor & Tensor::add_(const at::Tensor & other, const at::Scalar & alpha) const {    return at::_ops::add__Tensor::call(const_cast<Tensor&>(*this), other, alpha);}
          再通過ag找add__Tensor的定義,最后在torch/csrc/autograd/generated文件夾下面的VariableTypeEverything.cpp文件找到。這個文件其實是由VariableType_{0,1,2,3}.cpp多個文件拼接成的。在VariableType_3.cpp中我們可以找到add__Tensor的具體定義。此處我們精簡一下和我們的case相關(guān)的code方便理解。
          at::Tensor & add__Tensor(c10::DispatchKeySet ks, at::Tensor & self, const at::Tensor & other, const at::Scalar & alpha) {    auto& self_ = unpack(self, "self", 0);    auto& other_ = unpack(other, "other", 1);    auto _any_requires_grad = compute_requires_grad( self, other );     (void)_any_requires_grad;    check_inplace(self, _any_requires_grad);    c10::optional<at::Tensor> original_self;    std::shared_ptr<AddBackward0> grad_fn;    if (_any_requires_grad) {      grad_fn = std::shared_ptr<AddBackward0>(new AddBackward0(), deleteNode);      grad_fn->set_next_edges(collect_next_edges( self, other ));      grad_fn->other_scalar_type = other.scalar_type();      grad_fn->alpha = alpha;      grad_fn->self_scalar_type = self.scalar_type();    }    {      at::AutoDispatchBelowAutograd guard;      at::redispatch::add_(ks & c10::after_autograd_keyset, self_, other_, alpha);    }    if (grad_fn) {        rebase_history(flatten_tensor_args( self ), grad_fn);    }    return self;}
          這里我們順便來看一下add__Tensor函數(shù)在干啥,unpack方法是對tensor的一個檢查,在完成對于tensor的檢查之后,計算一下輸入tensor是否需要梯度,如果需要梯度,就會進(jìn)行圖的構(gòu)建(也就是設(shè)置tensor對應(yīng)的一些屬性,比如grad_fn),之后用dispatcher發(fā)送add的kernel,完成tensor的加法運算。torch中其他的operator如sub、sigmoid等遵循的是一樣的邏輯,而正是因為torch里forward過程創(chuàng)建圖的邏輯是完全一樣的,不同的算子只是launch的kernel類型不同,這些operator的代碼才可以通過特定規(guī)則生成出來。
          解釋完了函數(shù)的邏輯,我們來重新看一下泄漏的問題。
          如果我們注釋掉grad_fn->set_next_edges(collect_next_edges( self, other )); 或 rebase_history(flatten_tensor_args( self ), grad_fn); 這兩行code中的任意一行,那么都不會出現(xiàn)內(nèi)存/顯存泄漏的現(xiàn)象,由此我們有證據(jù)懷疑是在構(gòu)建動態(tài)圖的過程中產(chǎn)生了內(nèi)存泄漏的。
          又因為rebase_history是后面才被調(diào)用的,所以set_next_edges過程肯定只是出現(xiàn)泄漏的一個誘因,真正發(fā)生泄漏的位置肯定在后調(diào)用的位置,由此我們進(jìn)一步來看rebase_history的實際代碼實現(xiàn)。從源碼邏輯來看,大部分是檢查和確保一些屬性的邏輯,核心在于set_gradient_edge(self, std::move(gradient_edge));這一句。由此,我們來看set_gradient_edges的邏輯,當(dāng)然,為方便理解,下面的code做了一些精簡(全部code的參考鏈接:set_gradient_edge,materialize_autograd_meta,get_auto_grad_meta)
          void set_gradient_edge(const Variable& self, Edge edge) {  auto* meta = materialize_autograd_meta(self);  meta->grad_fn_ = std::move(edge.function);  meta->output_nr_ = edge.input_nr;}
          AutogradMeta* materialize_autograd_meta(const at::TensorBase& self) { auto p = self.unsafeGetTensorImpl(); if (!p->autograd_meta()) { p->set_autograd_meta(std::make_unique<AutogradMeta>()); } return get_autograd_meta(self);}
          AutogradMeta* get_autograd_meta(const at::TensorBase& self) { return static_cast<AutogradMeta*>(self.unsafeGetTensorImpl()->autograd_meta());}
          看到這里,基本上熟悉pytorch中對于圖定義的同學(xué)大概就能知道是什么原因了。關(guān)于pytorch中forward過程構(gòu)建圖的原理,可以參考官網(wǎng)的blog,作為一個基礎(chǔ)概念,我們只需要了解:動態(tài)圖就是在forward過程中進(jìn)行圖的“創(chuàng)建”,在backward過程完成圖的“銷毀”。
          現(xiàn)在讓我們回到數(shù)據(jù)結(jié)構(gòu)中Graph(圖)的概念。在一個自動求導(dǎo)系統(tǒng)中,我們可以將Graph中的Edge(邊)簡單地理解為一個tensor,Graph中Node(節(jié)點)的概念理解為算子。比如在torch里寫 c = a + b,其實就是表示有一個a 表示的Edge和一個b代表的Edge連接到一個add的Node(節(jié)點)上,這個Node又會連接到一個叫做c的Edge上(下面是一個用mermaid畫的一個示意圖,其中Edge用矩形表示,Node用圓表示。不難看出,add就是一個入度為2,出度為1的Node)。

          c = a + b的Graph形式

          既然我們有了圖,那么就需要有一些結(jié)構(gòu)保存一部分基本的圖信息,這些基本圖信息會在自動求導(dǎo)(autograd)的時候使用。在torch中,AutogradMeta就是包含了諸如tensor的autograd歷史、hooks等信息的結(jié)構(gòu),而導(dǎo)致我們內(nèi)存/顯存泄漏的罪魁禍?zhǔn)滓舱沁@個AutogradMeta。
          現(xiàn)在,我們已經(jīng)知道m(xù)emory實際上泄漏的是啥了。跳回我們寫的code,結(jié)合gc機制,想一想問題1你是否知道了答案。


          05
          解惑
          至此,我們基本上就可以把問題1解釋清楚了:在Tensor的requires_grad為True的時候,Tensor的每次運算都會導(dǎo)致需要保存一份AutogradMeta信息,對應(yīng)的Tensor也會被加入到計算圖中。即使表面上來看你只是做了一些inplace add的操作,但是其實在torch內(nèi)部,那個臨時的Tensor已經(jīng)進(jìn)入到了圖里,成為了圖的一個Edge,且引用計數(shù) + 1,自然是要占據(jù)空間的。如果你的Tensor不requires_grad,那么就是只是進(jìn)行運算,不會有Meta等信息存在,那個暫時生成的Tensor就會引用計數(shù)清0被gc了,自然也不會有內(nèi)存泄漏了。
          除了問題1之外,結(jié)合上面介紹的內(nèi)容,我們也能理解,下面一段非常pythonic的code在pytorch里面并不科學(xué)的原因。
          total_loss = 0for data in dataloader:    loss = model(data)    total_loss += losstotal_loss.backward()
          現(xiàn)在,讓我們從最小復(fù)現(xiàn)代碼回歸到codebase,其實我給出的復(fù)現(xiàn)里面的代碼中的value就是loss,很多時候煉丹師會想要看一下loss的均值/最大值等統(tǒng)計信息,經(jīng)常會用一個meter保存歷史信息,也就對應(yīng)了復(fù)現(xiàn)代碼里面的val。
          很多奇怪的現(xiàn)象到此也就說的通了,比如不同模型泄漏速度不一樣,就是因為不同的模型loss的數(shù)量是不一樣的,泄漏的速度自然也是不一樣的;再比如增加數(shù)據(jù)會使得同一個模型在更早的epoch到達(dá)OOM狀態(tài),是因為當(dāng)數(shù)據(jù)增加的時候一個epoch內(nèi)的iter數(shù)就會變多,自然會有在更早的epoch把內(nèi)存泄漏完的現(xiàn)象;曾經(jīng)能訓(xùn)練的模型加了數(shù)據(jù)之后也有可能因此變得無法訓(xùn)練。


          06
          后記
          也許下面這句話對煉丹師來說聽起來有些反直覺,但我覺得還是有必要聲明一下:無論python前端中tensor看起來是如何動態(tài)地進(jìn)行運算,概念上計算圖中的每個節(jié)點都無法被inplace修改。
          在理解了本文要介紹的原理后,我們也可以輕易寫一些reviewer看起來好像沒啥問題的泄漏程序了(逃
          def leak():    use_cuda = torch.cuda.is_available()    val = torch.rand(1).cuda() if use_cuda else torch.rand(1)    val.requires_grad_()  # 比如這個requires_grad_是在某個地方偷偷加的    count = 0    log_iter = 20000    log_device_usage(count, use_cuda)    while True:        val += 1  # 這個1在torch里面會表示為一個cpu tensor        if count % log_iter == 0:            log_device_usage(count, use_cuda)        count += 1
          為了更好的表示上述代碼在執(zhí)行過程中發(fā)生了什么,我用manim寫了一個動畫來提供更直觀的解釋,放在結(jié)尾也是希望讀者能在讀完文章后,稍微讓頭腦休息一下吧:)



          猜您喜歡:

           戳我,查看GAN的系列專輯~!
          一頓午飯外賣,成為CV視覺前沿弄潮兒!
          CVPR 2022 | 25+方向、最新50篇GAN論文
           ICCV 2021 | 35個主題GAN論文匯總
          超110篇!CVPR 2021最全GAN論文梳理
          超100篇!CVPR 2020最全GAN論文梳理


          拆解組新的GAN:解耦表征MixNMatch

          StarGAN第2版:多域多樣性圖像生成


          附下載 | 《可解釋的機器學(xué)習(xí)》中文版

          附下載 |《TensorFlow 2.0 深度學(xué)習(xí)算法實戰(zhàn)》

          附下載 |《計算機視覺中的數(shù)學(xué)方法》分享


          《基于深度學(xué)習(xí)的表面缺陷檢測方法綜述》

          《零樣本圖像分類綜述: 十年進(jìn)展》

          《基于深度神經(jīng)網(wǎng)絡(luò)的少樣本學(xué)習(xí)綜述》


          瀏覽 107
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费的黄色国产视频 | 国产超清无码片内射 | 欧洲一区在线观看 | 日本A电影在线 | 影音先锋每日最新av |