如果您还没有看到有关为什么深度学习和神经网络使用GPU的那一集,请确保在这一集的旁边进行回顾,以最好地理解这些概念。
现在,我们将以PyTorch GPU示例为基础。
我们也可以使用to()方法。要进入GPU,我们要写入(‘cuda’),要进入CPU,我们要写入(‘cpu’)。 to()方法是首选方法,主要是因为它更灵活。我们将使用前两个示例来说明一个示例,然后默认将始终使用to()变体。
CPU | GPU |
---|---|
cpu() | cuda() |
to(‘cpu’) | to(‘cuda’) |
要在培训过程中使用我们的GPU,有两个基本要求。这些要求如下,必须将数据移至GPU,将网络移至GPU。
默认情况下,创建PyTorch张量或PyTorch神经网络模块时,将在CPU上初始化相应的数据。具体来说,数据存在于CPU的内存中。
现在,让我们创建一个张量和一个网络,并看看如何进行从CPU到GPU的迁移。
在这里,我们创建一个张量和一个网络:
t = torch.ones(1,1,28,28)
network = Network()
现在,我们调用cuda()方法并将张量和网络重新分配给已复制到GPU的返回值:
t = t.cuda()
network = network.cuda()
接下来,我们可以从网络获取预测,并查看预测张量的device属性确认数据在cuda(即GPU)上:
> gpu_pred = network(t)
> gpu_pred.device
device(type='cuda', index=0)
同样,我们可以采取相反的方式:
> t = t.cpu()
> network = network.cpu()
> cpu_pred = network(t)
> cpu_pred.device
device(type='cpu')
简而言之,这就是我们如何利用PyTorch的GPU功能。现在,我们要讨论的是一些重要的细节,这些细节潜伏在我们刚刚看到的代码的表面之下。
例如,尽管我们使用了cuda()和cpu()方法,但实际上它们并不是我们的最佳选择。此外,网络实例和张量实例之间的方法有什么区别?这些毕竟是不同的对象类型,这意味着这两种方法是不同的。最后,我们希望将此代码集成到一个有效的示例中并进行性能测试。
此时的主要结论是,我们的网络和数据都必须同时存在于GPU上,才能使用GPU进行计算,这适用于任何编程语言或框架。
正如我们将在下一个演示中看到的那样,对于CPU也是这样。 GPU和CPU是基于数据进行计算的计算设备,因此在计算中彼此直接使用的任何两个值必须存在于同一设备上。
让我们通过演示一些张量计算来更深入地研究。
我们将从创建两个张量开始:
t1 = torch.tensor([
[1,2],
[3,4]
])
t2 = torch.tensor([
[5,6],
[7,8]
])
现在,我们将通过检查device属性来检查这些张量在哪个设备上初始化:
> t1.device, t2.device
(device(type='cpu'), device(type='cpu'))
正如我们所期望的,我们看到确实两个张量都在同一设备上,即CPU。让我们将第一个张量t1移至GPU。
> t1 = t1.to('cuda')
> t1.device
device(type='cuda', index=0)
我们可以看到该张量的设备已更改为cuda(GPU)。请注意此处使用to()方法。无需调用特定方法移动到设备,而是调用相同方法并传递指定设备的参数。使用to()方法是在设备之间来回移动数据的首选方式。
另外,请注意重新分配。该操作不在适当的位置,因此需要重新分配。
让我们尝试一个实验。我想通过尝试对这两个张量t1和t2进行计算来测试我们之前讨论的内容,现在我们知道它们在不同的设备上。
由于我们预期会出现错误,因此我们将尝试包装该调用并捕获异常:
try:
t1 + t2
except Exception as e:
print(e)
expected device cuda:0 but got device cpu
通过反转操作顺序,我们可以看到错误也发生了变化:
try:
t2 + t1
except Exception as e:
print(e)
expected device cpu but got device cuda:0
这两个错误都告诉我们,二进制加号运算符期望第二个参数与第一个参数具有相同的设备。调试这些类型的设备不匹配时,了解此错误的含义可能会有所帮助。
最后,为了完成操作,让我们将第二张量移动到cuda设备以查看操作是否成功。
> t2 = t2.to('cuda')
> t1 + t2
tensor([[ 6, 8],
[10, 12]], device='cuda:0')
我们已经看到了如何在设备之间来回移动张量。现在,让我们看看如何使用PyTorch nn.Module实例完成此操作。
更一般而言,我们有兴趣了解网络在诸如GPU或CPU之类的设备上的含义以及含义。除了PyTorch,这是基本问题。
通过将网络参数移至该设备,将网络放置在该设备上。让我们创建一个网络,看看我们的意思。
network = Network()
现在,让我们看一下网络的参数:
for name, param in network.named_parameters():
print(name, '\t\t', param.shape)
conv1.weight torch.Size([6, 1, 5, 5])
conv1.bias torch.Size([6])
conv2.weight torch.Size([12, 6, 5, 5])
conv2.bias torch.Size([12])
fc1.weight torch.Size([120, 192])
fc1.bias torch.Size([120])
fc2.weight torch.Size([60, 120])
fc2.bias torch.Size([60])
out.weight torch.Size([10, 60])
out.bias torch.Size([10])
在这里,我们创建了一个PyTorch网络,并迭代了该网络的参数。如我们所见,网络的参数是网络内部的权重和偏差。
换句话说,这些只是存在于我们已经看到的设备上的张量。让我们通过检查每个参数的设备来验证这一点。
for n, p in network.named_parameters():
print(p.device, '', n)
cpu conv1.weight
cpu conv1.bias
cpu conv2.weight
cpu conv2.bias
cpu fc1.weight
cpu fc1.bias
cpu fc2.weight
cpu fc2.bias
cpu out.weight
cpu out.bias
这表明,默认情况下,网络中的所有参数都是在CPU上初始化的。
一个重要的考虑因素是它解释了为什么像网络这样的nn.Module实例实际上没有设备。不是网络驻留在设备上,而是网络内部的张量驻留在设备上。
让我们看看当我们要求将网络移至GPU时会发生什么:
network.to('cuda')
Network(
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=192, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=60, bias=True)
(out): Linear(in_features=60, out_features=10, bias=True)
)
请注意,此处不需要重新分配。这是因为就网络实例而言,该操作是就地的。但是,该操作可以用作重新分配操作。对于nn.Module实例和PyTorch张量之间的一致性,首选此方法。
在这里,我们可以看到,现在所有网络参数都有一个cuda设备:
for n, p in network.named_parameters():
print(p.device, '', n)
cuda:0 conv1.weight
cuda:0 conv1.bias
cuda:0 conv2.weight
cuda:0 conv2.bias
cuda:0 fc1.weight
cuda:0 fc1.bias
cuda:0 fc2.weight
cuda:0 fc2.bias
cuda:0 out.weight
cuda:0 out.bias
让我们通过将示例传递到网络来结束本演示。
sample = torch.ones(1,1,28,28)
sample.shape
torch.Size([1, 1, 28, 28])
这给了我们一个可以通过的张量样本:
try:
network(sample)
except Exception as e:
print(e)
Expected object of device type cuda but got device type cpu for argument #1 'self' in call to _thnn_conv2d_forward
由于我们的网络位于GPU上,并且默认情况下此新创建的示例位于CPU上,因此我们会收到错误消息。该错误告诉我们,在调用第一卷积层的forward方法时,CPU张量应为GPU张量。这正是我们之前直接添加两个张量时所看到的。
我们可以通过将示例发送到GPU来解决此问题,如下所示:
try:
pred = network(sample.to('cuda'))
print(pred)
except Exception as e:
print(e)
tensor([[-0.0685, 0.0201, 0.1223, 0.1075, 0.0810, 0.0686, -0.0336, -0.1088, -0.0995, 0.0639]]
, device='cuda:0'
, grad_fn=<AddmmBackward>
)
最后,一切都按预期进行,我们得到了预测。
在总结之前,我们需要讨论编写与设备无关的代码。术语“设备不可知”意味着我们的代码不依赖于底层设备。阅读PyTorch文档时,您可能会遇到此术语。
例如,假设我们编写了到处都使用cuda()方法的代码,然后将代码提供给没有GPU的用户。这行不通。不用担心我们有选择!
还记得我们之前看到的cuda()和cpu()方法吗?
我们之所以首选to()方法的原因之一是,因为to()方法是参数化的,这使得更改我们选择的设备变得更加容易,即它很灵活!
例如,用户可以将cpu或cuda作为参数传递给深度学习程序,这将使该程序与设备无关。
允许程序用户传递确定程序行为的参数,这可能是使程序与设备无关的最佳方法。但是,我们也可以使用PyTorch来检查支持的GPU,并以此方式设置设备。
torch.cuda.is_available()
True
就像,如果cuda可用,请使用它!
现在让我们看看如何将GPU的使用添加到训练循环中。 我们将使用本系列到目前为止开发的代码进行此添加。
这将使我们能够轻松比较CPU与GPU的时间。
在更新训练循环之前,我们需要更新RunManager类。在begin_run()方法内部,我们需要修改传递给add_graph方法的图像张量的设备。
它看起来应该像这样:
def begin_run(self, run, network, loader):
self.run_start_time = time.time()
self.run_params = run
self.run_count += 1
self.network = network
self.loader = loader
self.tb = SummaryWriter(comment=f'-{run}')
images, labels = next(iter(self.loader))
grid = torchvision.utils.make_grid(images)
self.tb.add_image('images', grid)
self.tb.add_graph(
self.network
,images.to(getattr(run, 'device', 'cpu'))
)
在这里,我们使用内置的getattr()函数来获取运行对象上设备的值。如果运行对象没有设备,则返回cpu。这使代码向后兼容。如果我们没有为运行指定设备,它将仍然有效。
请注意,网络不需要移动到设备,因为它是在传入设备之前设置的。但是,图像张量是从加载程序获得的。
我们将配置参数设置为具有设备。 这里的两个逻辑选项是cuda和cpu。
params = OrderedDict(
lr = [.01]
,batch_size = [1000, 10000, 20000]
, num_workers = [0, 1]
, device = ['cuda', 'cpu']
)
将这些设备值添加到我们的配置中后,现在就可以在我们的训练循环中对其进行访问。
在跑步的顶部,我们将创建一个将在跑步内和训练循环内传递的设备。
device = torch.device(run.device)
首先使用此设备是在初始化网络时。
network = Network().to(device)
这将确保将网络移动到适当的设备。 最后,我们将通过分别解压缩图像和标签张量并将它们发送到设备来更新图像和标签张量:
images = batch[0].to(device)
labels = batch[1].to(device)
这就是全部,我们已经准备好运行此代码并查看结果。