<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>

          PyTorch 深度剖析:如何使用模型并行技術(shù) (Model Parallel)

          共 10639字,需瀏覽 22分鐘

           ·

          2021-11-19 16:30

          ↑ 點擊藍字?關(guān)注極市平臺

          作者丨科技猛獸
          編輯丨極市平臺

          極市導(dǎo)讀

          ?

          本文介紹了模型并行技術(shù) Model Parallel,與 DataParallel 相反,Model Parallel 將一個單一的模型分割到不同的 GPU 上,而不是在每個 GPU 上復(fù)制整個模型。Pipeline Model Parallel 是一種進一步加速模型并行的策略。?>>加入極市CV技術(shù)交流群,走在計算機視覺的最前沿

          0 背景

          模型并行 (Single Machine Model Parallel) 在分布式訓(xùn)練技術(shù)中廣泛使用。

          它和 DataParallel 不同,DataParallel 是在多個GPU上訓(xùn)練神經(jīng)網(wǎng)絡(luò),將同一個模型復(fù)制到所有 GPU 上,每個 GPU 消耗不同的輸入數(shù)據(jù)分區(qū)。雖然 DataParallel 可以大大加快訓(xùn)練過程,但是,有的時候一些模型太大,沒辦法裝入單個 GPU 的時候,就不適用了。

          這篇文章展示了如何通過使用模型并行來解決這個問題,與 DataParallel 相反,Model Parallel 將一個單一的模型分割到不同的 GPU 上,而不是在每個 GPU 上復(fù)制整個模型。具體來說,比如一個模型 m 包含10層:當(dāng)使用 DataParallel 時,每個 GPU 將有這10層的每個副本,而在兩個 GPU 上使用模型并行時,每個 GPU 只需要承載5層。

          Model Parallel 的 High-level 的理念是將模型的不同子網(wǎng)絡(luò)放在不同的設(shè)備上,并相應(yīng)地實現(xiàn) forward() 方法,在 GPU 之間移動模型中間輸出。每個模型被拆成了多塊,只有一塊在單獨的設(shè)備上運行,所以,一組設(shè)備可以共同為一個更大的模型服務(wù)。

          在這篇文章中,我們不會試圖構(gòu)建巨大的模型,并將其硬擠入數(shù)量有限的 GPU。相反,這篇文章的重點是展示 Model Parallel 的具體操作方法。

          1 Model Parallel 基操

          比如現(xiàn)在有一個包含2個 Linear layers 的模型,我們想在2塊 GPU 上 run 它,辦法可以是在每塊 GPU 上放置1個 Linear layer,并且把得到的中間結(jié)果在 GPU 之間移動。代碼可以是這樣子:

          import torch
          import torch.nn as nn
          import torch.optim as optim


          class ToyModel(nn.Module):
          def __init__(self):
          super(ToyModel, self).__init__()
          self.net1 = torch.nn.Linear(10, 10).to('cuda:0')
          self.relu = torch.nn.ReLU()
          self.net2 = torch.nn.Linear(10, 5).to('cuda:1')

          def forward(self, x):
          x = self.relu(self.net1(x.to('cuda:0')))
          return self.net2(x.to('cuda:1'))

          注意,上述 ToyModel 看起來與在單個 GPU 上的實現(xiàn)方式非常相似,除了四個 to(device) 的調(diào)用,將 Linear layer 和張量放在適當(dāng)?shù)脑O(shè)備上。這是該模型中唯一需要改變的地方。backward() 和 torch.optim 將自動處理梯度問題,就像模型是在一個 GPU 上一樣。

          你只需要確保在調(diào)用損失函數(shù)時,標(biāo)簽和輸出是在同一個設(shè)備上。像下面這樣:

          model = ToyModel()
          loss_fn = nn.MSELoss()
          optimizer = optim.SGD(model.parameters(), lr=0.001)

          optimizer.zero_grad()
          outputs = model(torch.randn(20, 10))
          labels = torch.randn(20, 5).to('cuda:1')
          loss_fn(outputs, labels).backward()
          optimizer.step()

          這里應(yīng)該把標(biāo)簽 labels 放在1號 GPU 上面,因為模型的輸出就在1號 GPU 上。

          2 對已有的模塊使用 Model Parallel

          這段我們介紹如何在多個 GPU 上運行一個現(xiàn)有的單 GPU 模塊,只需做幾行修改即可。

          下面的代碼顯示了如何將 torchvision.models.resnet50() 分解到兩個 GPU。這個想法是繼承現(xiàn)有的 ResNet 模塊,并在構(gòu)建過程中將各層分割到兩個 GPU 上面。然后,overwrite forward() 方法,通過相應(yīng)地移動中間輸出來縫合兩個子網(wǎng)絡(luò)。

          from torchvision.models.resnet import ResNet, Bottleneck

          num_classes = 1000


          class ModelParallelResNet50(ResNet):
          def __init__(self, *args, **kwargs):
          super(ModelParallelResNet50, self).__init__(
          Bottleneck, [3, 4, 6, 3], num_classes=num_classes, *args, **kwargs)

          self.seq1 = nn.Sequential(
          self.conv1,
          self.bn1,
          self.relu,
          self.maxpool,

          self.layer1,
          self.layer2
          ).to('cuda:0')

          self.seq2 = nn.Sequential(
          self.layer3,
          self.layer4,
          self.avgpool,
          ).to('cuda:1')

          self.fc.to('cuda:1')

          def forward(self, x):
          x = self.seq2(self.seq1(x).to('cuda:1'))
          return self.fc(x.view(x.size(0), -1))

          上述實現(xiàn)解決了模型太大,無法裝入單個GPU的情況下的問題。然而,對于運行速度而言,它將比在單個 GPU 上運行的速度要慢。這是因為,在任何時候,兩個 GPU 中只有一個在工作,而另一個則是坐在那里啥也不干。由于中間輸出需要在第二層和第三層之間從 cuda:0復(fù)制到 cuda:1,所以性能會進一步惡化。

          下面我們通過一個實驗,看看具體的程序運行時間的量化對比。在這個實驗中,我們通過運行隨機輸入和標(biāo)簽來訓(xùn)練 ModelParallelResNet50 和現(xiàn)有的 torchvision.models.resnet50()。在訓(xùn)練之后,這些模型不會產(chǎn)生任何有用的預(yù)測,但我們可以對執(zhí)行時間有一個合理的了解。

          import torchvision.models as models

          num_batches = 3
          batch_size = 120
          image_w = 128
          image_h = 128


          def train(model):
          model.train(True)
          loss_fn = nn.MSELoss()
          optimizer = optim.SGD(model.parameters(), lr=0.001)

          one_hot_indices = torch.LongTensor(batch_size) \
          .random_(0, num_classes) \
          .view(batch_size, 1)

          for _ in range(num_batches):
          # generate random inputs and labels
          inputs = torch.randn(batch_size, 3, image_w, image_h)
          labels = torch.zeros(batch_size, num_classes) \
          .scatter_(1, one_hot_indices, 1)

          # run forward pass
          optimizer.zero_grad()
          outputs = model(inputs.to('cuda:0'))

          # run backward pass
          labels = labels.to(outputs.device)
          loss_fn(outputs, labels).backward()
          optimizer.step()

          上面的 train(model) 方法使用 nn.MSELoss 作為損失函數(shù),optim.SGD 作為優(yōu)化器。它模擬在128 × 128的圖像上進行訓(xùn)練,這些圖像被組織成3個 batches,每個批次包含120張圖像。然后,我們使用 timeit 運行 train(model) 方法10次,并繪制執(zhí)行時間的標(biāo)準(zhǔn)差,代碼如下:

          import matplotlib.pyplot as plt
          plt.switch_backend('Agg')
          import numpy as np
          import timeit

          num_repeat = 10

          stmt = "train(model)"

          setup = "model = ModelParallelResNet50()"
          mp_run_times = timeit.repeat(
          stmt, setup, number=1, repeat=num_repeat, globals=globals())
          mp_mean, mp_std = np.mean(mp_run_times), np.std(mp_run_times)

          setup = "import torchvision.models as models;" + \
          "model = models.resnet50(num_classes=num_classes).to('cuda:0')"
          rn_run_times = timeit.repeat(
          stmt, setup, number=1, repeat=num_repeat, globals=globals())
          rn_mean, rn_std = np.mean(rn_run_times), np.std(rn_run_times)


          def plot(means, stds, labels, fig_name):
          fig, ax = plt.subplots()
          ax.bar(np.arange(len(means)), means, yerr=stds,
          align='center', alpha=0.5, ecolor='red', capsize=10, width=0.6)
          ax.set_ylabel('ResNet50 Execution Time (Second)')
          ax.set_xticks(np.arange(len(means)))
          ax.set_xticklabels(labels)
          ax.yaxis.grid(True)
          plt.tight_layout()
          plt.savefig(fig_name)
          plt.close(fig)


          plot([mp_mean, rn_mean],
          [mp_std, rn_std],
          ['Model Parallel', 'Single GPU'],
          'mp_vs_rn.png')

          實驗結(jié)果:

          圖1:Model Parallel 和 Single GPU 的運行時間比較

          結(jié)果顯示,Model Parallel 實現(xiàn)的執(zhí)行時間比現(xiàn)有的 Single GPU 實現(xiàn)長 4.02/3.75-1=7%。因此,我們可以得出結(jié)論,在 GPU 之間來回復(fù)制張量的開銷大約是7%。還有改進的余地,如何改進?

          3 通過 Pipelining Inputs 加速模型并行

          因為我們知道兩個 GPU 中的一個在整個執(zhí)行過程中是閑置的。一個選擇是將每個批次的 images 進一步劃分為一個個的 splits,這樣當(dāng)一個 split 到達第二個子網(wǎng)絡(luò)時,下面的 split 可以被送入第一個子網(wǎng)絡(luò)。通過這種方式,兩個連續(xù)的 splits 可以在兩個 GPU 上同時運行。

          要理解這波操作,就得首先學(xué)習(xí)一個 torch.split 函數(shù):

          https://pytorch.org/docs/stable/generated/torch.split.html

          torch.split(tensor,split_size_or_sections,dim=0)

          Parameters

          • tensor (Tensor) – tensor to split.
          • split_size_or_sections (int) or (list(int)) – size of a single chunk or list of sizes for each chunk
          • dim (int) – dimension along which to split the tensor.

          tensor:https://pytorch.org/docs/stable/tensors.html#torch.Tensor

          list:https://docs.python.org/3/library/stdtypes.html#list

          int:https://docs.python.org/3/library/functions.html#int

          它的作用的官方描述是:Splits the tensor into chunks. Each chunk is a view of the original tensor.

          如果 split_size_or_sections 是一個整數(shù)類型,那么張量將被分割成同等大小的塊。如果張量沿著給定的維度 dim 的大小不能被 split_size 整除,那么最后一個塊會更小。

          如果 split_size_or_sections 是一個列表,那么張量將被分割成 len(split_size_or_sections) 個小塊,其大小與 split_size_or_sections 相同。

          舉例:

          >>> a = torch.arange(10).reshape(5,2)
          >>> a
          tensor([[0, 1],
          [2, 3],
          [4, 5],
          [6, 7],
          [8, 9]])
          >>> torch.split(a, 2)
          (tensor([[0, 1],
          [2, 3]]),
          tensor([[4, 5],
          [6, 7]]),
          tensor([[8, 9]]))
          >>> torch.split(a, [1,4])
          (tensor([[0, 1]]),
          tensor([[2, 3],
          [4, 5],
          [6, 7],
          [8, 9]]))

          接下來,我們回到 PipelineParallelResNet50 模型,進一步將每個 batch 的120張圖片分成20張圖片的 split,這步操作可以通過 splits = iter(x.split(self.split_size, dim=0)) 來完成。

          由于PyTorch是異步啟動CUDA操作的,因此該實現(xiàn)不需要催生多個線程來實現(xiàn)并發(fā)。代碼如下,簡單梳理一下代碼的含義:

          在 forward() 函數(shù)里面做以下這些事情:

          對輸入的 batch=120 的圖片分成相同大小為20的 splits:

          splits = iter(x.split(self.split_size, dim=0))

          從頭開始,每次取出一個 split:

          s_next = next(splits)

          把第1個 split 通過第1段模型:

          s_prev = self.seq1(s_next).to('cuda:1')

          for 循環(huán)可以看做每次循環(huán)做2件事:

          A. 前半段模型的輸出傳到后半段并前向傳播:

          s_prev = self.seq2(s_prev)

          B. 下一個 split 輸入前半段模型:

          s_prev = self.seq1(s_next).to('cuda:1')

          class PipelineParallelResNet50(ModelParallelResNet50):
          def __init__(self, split_size=20, *args, **kwargs):
          super(PipelineParallelResNet50, self).__init__(*args, **kwargs)
          self.split_size = split_size

          def forward(self, x):
          splits = iter(x.split(self.split_size, dim=0))
          s_next = next(splits)
          s_prev = self.seq1(s_next).to('cuda:1')
          ret = []

          for s_next in splits:
          # A. s_prev runs on cuda:1
          s_prev = self.seq2(s_prev)
          ret.append(self.fc(s_prev.view(s_prev.size(0), -1)))

          # B. s_next runs on cuda:0, which can run concurrently with A
          s_prev = self.seq1(s_next).to('cuda:1')

          s_prev = self.seq2(s_prev)
          ret.append(self.fc(s_prev.view(s_prev.size(0), -1)))

          return torch.cat(ret)


          setup = "model = PipelineParallelResNet50()"
          pp_run_times = timeit.repeat(
          stmt, setup, number=1, repeat=num_repeat, globals=globals())
          pp_mean, pp_std = np.mean(pp_run_times), np.std(pp_run_times)

          plot([mp_mean, rn_mean, pp_mean],
          [mp_std, rn_std, pp_std],
          ['Model Parallel', 'Single GPU', 'Pipelining Model Parallel'],
          'mp_vs_rn_vs_pp.png')

          整個過程可以用下圖2表示,其中數(shù)字1代表進入for循環(huán)前的第1步:s_prev = self.seq1(s_next).to('cuda:1'),2-6代表5次for循環(huán),數(shù)字7代表最后一步:s_prev = self.seq2(s_prev)。

          同一個 batch 的數(shù)據(jù),有些提前進入后半段模型 Model Part 2,而不用等待全部數(shù)據(jù)走完前半段模型 Model Part 1之后再統(tǒng)一進入后半段模型 Model Part 2。這樣子節(jié)約了運行時間

          圖2:Pipeline Model Parallel過程,同一個 batch 的數(shù)據(jù),有些提前進入后半段模型 Model Part 2,而不用等待全部數(shù)據(jù)走完前半段模型 Model Part 1之后再統(tǒng)一進入后半段模型 Model Part 2。這樣子節(jié)約了運行時間

          實驗結(jié)果:

          圖3:Pipeline Model Parallel,Model Parallel 和 Single GPU 的運行時間比較

          實驗結(jié)果表明,通過流水線輸入并行 ResNet50 模型,訓(xùn)練過程大約加快了 3.75/2.51-1=49%。這與理想的 100% 的速度仍然相差甚遠。由于我們在管道并行實現(xiàn)中引入了一個新的參數(shù) split_sizes,目前還不清楚這個新參數(shù)對整個訓(xùn)練時間有什么影響。直觀地說,使用小的 split_size 會導(dǎo)致許多微小的 CUDA 內(nèi)核啟動,而使用大的 split_size 會導(dǎo)致在第一次和最后一次分割時出現(xiàn)相對較長的空閑時間。兩者都不是最優(yōu)的。對于這個特定的實驗來說,可能會有一個最佳的 split_size 配置。讓我們通過使用幾個不同的 split_size 值進行實驗來找到它。代碼如下:

          means = []
          stds = []
          split_sizes = [1, 3, 5, 8, 10, 12, 20, 40, 60]

          for split_size in split_sizes:
          setup = "model = PipelineParallelResNet50(split_size=%d)" % split_size
          pp_run_times = timeit.repeat(
          stmt, setup, number=1, repeat=num_repeat, globals=globals())
          means.append(np.mean(pp_run_times))
          stds.append(np.std(pp_run_times))

          fig, ax = plt.subplots()
          ax.plot(split_sizes, means)
          ax.errorbar(split_sizes, means, yerr=stds, ecolor='red', fmt='ro')
          ax.set_ylabel('ResNet50 Execution Time (Second)')
          ax.set_xlabel('Pipeline Split Size')
          ax.set_xticks(split_sizes)
          ax.yaxis.grid(True)
          plt.tight_layout()
          plt.savefig("split_size_tradeoff.png")
          plt.close(fig)

          實驗結(jié)果:

          圖4:Pipeline Split Size 對加速效果的影響

          如上圖所示,結(jié)果顯示,將 split_size 設(shè)置為12可以達到最快的訓(xùn)練速度,實現(xiàn)了 3.75/2.43-1=54% 的速度提升。

          總結(jié)

          本文介紹了模型并行技術(shù) Model Parallel,與 DataParallel 相反,Model Parallel 將一個單一的模型分割到不同的 GPU 上,而不是在每個 GPU 上復(fù)制整個模型。Pipeline Model Parallel 是一種進一步加速模型并行的策略。

          如果覺得有用,就請分享到朋友圈吧!

          △點擊卡片關(guān)注極市平臺,獲取最新CV干貨

          公眾號后臺回復(fù)“transformer”獲取最新Transformer綜述論文下載~


          極市干貨
          課程/比賽:珠港澳人工智能算法大賽保姆級零基礎(chǔ)人工智能教程
          算法trick目標(biāo)檢測比賽中的tricks集錦從39個kaggle競賽中總結(jié)出來的圖像分割的Tips和Tricks
          技術(shù)綜述:一文弄懂各種loss function工業(yè)圖像異常檢測最新研究總結(jié)(2019-2020)

          #?極市平臺簽約作者#


          科技猛獸

          知乎:科技猛獸


          清華大學(xué)自動化系19級碩士

          研究領(lǐng)域:AI邊緣計算 (Efficient AI with Tiny Resource):專注模型壓縮,搜索,量化,加速,加法網(wǎng)絡(luò),以及它們與其他任務(wù)的結(jié)合,更好地服務(wù)于端側(cè)設(shè)備。


          作品精選

          搞懂 Vision Transformer 原理和代碼,看這篇技術(shù)綜述就夠了
          用Pytorch輕松實現(xiàn)28個視覺Transformer,開源庫 timm 了解一下?。ǜ酱a解讀)
          輕量高效!清華智能計算實驗室開源基于PyTorch的視頻 (圖片) 去模糊框架SimDeblur



          投稿方式:
          添加小編微信Fengcall(微信號:fengcall19),備注:姓名-投稿
          △長按添加極市平臺小編

          覺得有用麻煩給個在看啦~??
          瀏覽 147
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  久久无码中文 | av无码aV天天aV天天爽第一集 | 爱搞在线| av黄色电影一区天堂一区二区三区 | 日韩午夜av |