pytorch——关于GPU的一些知识

一、前言

最近在学习pytorch框架,记录一些涉及到的知识点,方便后续查找和学习。

二、主要内容

内容可能有些散,初次学习,可能把握不好知识之间的连贯性和整体性,后续适当调整。

关于CUDA的一些函数接口:
torch.cuda.is_available()  #查看系统GPU是否可以使用,经常用来判断是否装好gpu版的pytorch

torch.cuda.current_device()  #返回当前设备序号

torch.cuda.get_device_name(0) #返回第0号设备的name

torch.cuda.device_count() #返回可使用的gpu的数量

torch.cuda.memory_allocated(device=‘cuda:0)  #返回0号设备的当前GPU显存使用量(以字节为单位)
torch.device

作为Tensor 的一种属性,其包含了两种设备类型,cpu和gpu,通常通过两种方式来进行创建:

1.通过字符串创建
eg: 
torch.device('cuda:0')
torch.device('cpu')
torch.device('cuda')  # 当前cuda设备

2.通过字符串加设备编号创建
eg:
torch.device('cuda', 0)
torch.device('cpu', 0)

常见的几种创建gpu设备上的Tensor对象:
torch.randn((2,3),device = torch.device('cuda:0'))
torch.randn((2,3),device = 'cuda:0')
torch.randn((2,3),device = 0)  #历史遗留做法,仅支持gpu

.to()

进行设备转化,也是常用的设置gpu的方式。


通常可以这样调用:

to(device=None, dtype=None, non_blocking=False)
#第一个可以设置当前的设备,比如device=torch.device('cuda:0')
#第二个就是数据类型了,如torch.float,torch.int,torch.double
#第三个参数,若设置为True, 并且此对象的资源存储在固定内存上(pinned memory),那么此cuda()函数产生的复制将与host端的原storage对象保持同步。否则此参数不起作用
使用指定的GPU:

PyTorch默认使用从0开始的GPU,常有以下两种方式来指定特定的GPU

1.CUDA_VISIBLE_DEVICES
终端设置:  CUDA_VISIBLE_DEVICES=1,2  python train.py    (举个例子)
代码里设置:
			import os
			os.environ["CUDA_VISIBLE_DEVICES"] = '1,2'
2.torch.cuda.set_device()
代码里设置:
			import torch
			torch.cuda.set_device(id)

不过官方建议使用CUDA_VISIBLE_DEVICES,不建议使用 set_device 函数。

----- 分割线----

了解了以上的一些gpu的操作后,我们来说一些实际训练时的方法。

单卡训练:

这个比较简单的一种操作方式。例子如下:

#模型置于gpu
device = torch.device("cuda: 0")
model.to(device)   # or   model.cuda(device),数据处理类似可这样

#数据置于gpu
mytensor = my_tensor.to(device)  #这里注意,mytensor 是my_tensor的一个gpu上的一个副本,而不是重写了my_tensor

多卡训练:

为了提高训练速度,常常一个机器有多张gpu,这时候便可以进行并行训练,提高效率。而并行训练又可分为数据并行处理模型并行处理
数据并行处理指的是使用同一个模型,将数据均匀分到多种gpu上,进行训练;
模型并行处理指的是多张gpu训练模型的不同部分,使用同一批数据。
下面将就这个两个方法进行记录。

数据并行处理:

当我们的数据量太大时,可以考虑数据并行处理方法。

API:
class torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)

# module:即所用到的模型
# device_ids:进行数据并行处理的设备编号,默认使用设备的所有gpu。若指定gpu,常用编号组成的int列表
# output_device:输出数据的位置,默认为第一个编号的gpu

使用方法:

import torch.nn as nn

device = torch.device("cuda: 0" if torch.cuda.is_available() else "cpu")

if torch.cuda.device_count() > 1: 
	nn.DataParallel(model)  #也可根据已有gpu进行指定,eg:nn.DataParallel(model,device_ids = [0,1,2])
	
model.to(device)

这里稍微解释下:上述处理模型的操作,数据处理的操作和单卡一样,放于设定的device上即可,可能有人会问,这不是和单卡训练一样嘛。其实是不一样的,nn.DataParallel()的设定,使得模型进行处理数据时,会将model复制到选定的gpu上,然后将批量的数据进行均分,分配到选定gpu上,当然,这就隐含一个条件,批量的数据必须是大于选定的gpu数量的,要不怎么分?然后进行计算,最后将结果返回选定的第一个gpu上,这里注意一点,所选的gpu必须要包含device所设定gpu,可以理解为主gpu。
比较详细的例子见:pytorch官网关于数据并行处理的例子

模型并行处理:

这种方法,自然对应的是模型太大的情况。当模型较大时,将模型的不同层放于不同的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'))

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

# 其中,backward()和torch.optim将自动处理梯度,只需确保调用损失函数时标签与输出在同一设备上。

然而,上述方法虽解决了模型太大带来的问题,但其花费时间是大于单gpu的。原因是在任何时间点,两个GPU中只有一个在工作,而另一个在那儿什么也没做。
如下图,是pytorch官网实现的reset50ModelParallelResNet50和torchvision.models.reset50()的消耗时间对比图:(具体例子可见pytorch官网
在这里插入图片描述
后又用流水线输入加速
eg:以下例子来自于pytorch官网

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

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

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

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

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)

num_repeat = 10

# model parallel
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)

# Single GPU
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)

# Pipelining model parallel 
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')
     

在这里插入图片描述
最后时间对比图:
在这里插入图片描述
当然,不同的split 会有不同的效果,这个在官网也说了可以寻找最佳的split 设置。

三、结束语

以上就是一些gpu的操作知识,关于后面说到的数据并行处理和模型并行处理,只用过数据并行处理,模型并行处理未使用过,所以其中的一些细节可能把握不是很好。而且后面还有分布式数据并行,DistributedDataParallel(DDP),这个可以与模型并行处理一起使用,DataParallel是不可以的,这样更加大大加快模型训练速度,但限于时间,还未学习这一块内容,后续有时间在进行完善一下。

参考链接1:PyTorch中使用指定的GPU
参考链接2:Python:timeit模块详解
参考链接3:pytorch中文翻译版官网

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值