九个技巧让你的PyTorch模型训练飞快!

2023年 12月 29日 29.7k 0

也许你仍然在使用32位精度进行计算,或者甚至只是在单个GPU上进行训练。

然而,随着科技的进步,我们已经有了更好的选择。使用更高精度的计算,如16位浮点数或混合精度,可以提高训练速度并减少内存消耗。同时,利用多个GPU进行并行训练,可以大大加快训练过程。

笔者在这里总结了提升Pytorch模型训练速度的9个技巧,与大家分享~

这些优化技巧可以在PyTorch-Lightning库中找到。PyTorch-Lightning是建立在PyTorch之上的一个封装,它提供了自动化训练的功能,同时允许开发者完全控制关键的模型组件。

这里以MNIST定义LightningModel并使用Trainer来训练模型为例。

#导入PyTorch-Lightning库中的Trainer类,用于管理训练过程
from pytorch_lightning import Trainer
#创建LightningModule实例,作为要训练的模型
model = LightningModule(…)
#创建Trainer实例,用于配置和管理训练过程
trainer = Trainer()
#开始训练
trainer.fit(model)

trainer.fit()方法将根据LightningModule中定义的训练逻辑来执行训练步骤,并自动处理数据加载、优化器配置、学习率调度等细节。

使用DataLoaders

使用DataLoaders来加载数据是获得训练速度提升的最简单方法之一。通过保存h5py或numpy文件以加速数据加载的时代已经过去了,而现在可以「使用PyTorch的DataLoader来轻松加载图像数据」(对于NLP数据,请参考TorchText库)。

在PyTorch-Lightning中,不需要显式地编写训练循环,只需要定义好DataLoaders和Trainer,PyTorch-Lightning会在需要的时候自动调用它们。

下面是一个加载MNIST数据集并使用DataLoader进行训练的示例代码:

from torch.utils.data import DataLoader
from torchvision.datasets import MNIST

dataset = MNIST(root=self.hparams.data_root, train=train, download=True)
loader = DataLoader(dataset, batch_size=32, shuffle=True)

for batch in loader:
    x, y = batch
    model.training_step(x, y)
    # 其他训练逻辑

在这个示例中,首先创建了一个MNIST数据集的实例,然后使用DataLoader将其封装成一个可迭代的数据加载器。在训练循环中,可以遍历DataLoader,每次获取一个batch的数据,并将其传递给模型的training_step()方法进行训练。

通过使用DataLoaders,可以更高效地加载和处理大量的训练数据,从而提高训练速度。此外,DataLoader还支持数据的随机打乱(shuffle)、批量大小(batch_size)等参数设置,可以根据实际需求进行调整。

DataLoaders中设置num_workers参数

在DataLoaders中,可以通过设置num_workers参数来允许批量并行加载数据,从而加速训练过程。下面是一个示例代码:

# 慢的方式
loader = DataLoader(dataset, batch_size=32, shuffle=True)

# 快的方式(使用10个workers)
loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=10)

在第一个示例中,创建了一个DataLoader,没有指定num_workers参数,这意味着数据加载将在主进程中进行,而不会并行化。

在第二个示例中,通过将num_workers设置为10,启用了批量并行加载。这意味着数据加载将在10个worker进程中进行,并行地加载多个batch,从而加速数据加载的过程。

通过适当设置num_workers参数,可以根据系统的硬件和资源情况,选择合适的worker数量来提高数据加载的效率。然而,需要注意的是,并不是worker越多越好,过多的worker可能会导致资源竞争和性能下降。

Batch size

在进行下一个优化步骤之前,增加批量大小(batch size)到CPU-RAM或GPU-RAM允许的最大范围是一个重要的优化策略。

增加批量大小可以带来以下好处:

  • 更高效地利用计算资源,尤其是GPU的并行计算能力。
  • 减少数据加载和传输的次数,提高训练速度。
  • 可以获得更稳定的梯度估计,有助于模型收敛。

然而,增加批量大小也会带来一些挑战:

  • 内存占用增加:较大的批量大小需要更多的内存空间来存储数据和梯度。
  • 学习率调整:增加批量大小后,通常需要相应地增加学习率,以保持相似的收敛行为。

因此,在增加批量大小之前,需要确保你的硬件和资源可以支持更大的批量大小,并相应地调整学习率。

梯度累积

梯度累积(Gradient Accumulation)是一种在计算资源有限的情况下,模拟较大批量大小的技术。通过多次执行前向传播、反向传播和优化步骤,将梯度累积起来,以获得与较大批量大小相同的效果。

下面是一个使用梯度累积的示例代码:

# 清除上一步的梯度
optimizer.zero_grad()

# 16次梯度累积步骤
scaled_loss = 0
for accumulated_step_i in range(16):
    out = model.forward()
    loss = some_loss(out, y)
    loss.backward()
    scaled_loss += loss.item()

# 更新权重
optimizer.step()

# 损失值现在按累积批次数量进行缩放
actual_loss = scaled_loss / 16

在这个示例中,通过循环执行16个梯度累积步骤,每个步骤进行前向传播、计算损失、反向传播和梯度累积。然后调用optimizer.step()来更新权重。

在PyTorch-Lightning中,只需要设置accumulate_grad_batches参数来指定梯度累积的次数。例如:

trainer = Trainer(accumulate_grad_batches=16)
trainer.fit(model)

保留的计算图

在记录损失值时,为了避免撑爆内存,只存储损失的数值而不是整个计算图。可以使用.item()方法来获取损失的数值。

# 方式1
losses.append(loss)

# 方式2
losses.append(loss.item())

在方式1中,损失值loss会保留整个计算图的副本,这会占用大量的内存空间。而方式2中,使用loss.item()来获取损失的数值,并将其存储到列表中,这样就只保留了数值,而不会占用过多的内存。

PyTorch-Lightning会非常小心地确保不会保留计算图的副本,尽量减少内存的占用。因此,在使用PyTorch-Lightning时,可以放心地使用.item()方法来获取损失的数值,而不必担心内存问题。

单个GPU训练

完成上述步骤之后,即可开始在GPU上进行训练。GPU上进行训练可以利用多个GPU核心之间的并行计算,从而加速训练过程。

在进行GPU训练时,需要做两件事情:

  • 将模型移动到GPU上;
  • 在每次数据通过时将数据放到GPU上。
  • 下面是在PyTorch中进行GPU训练的示例代码:

    # 将模型放到GPU上
    model.cuda()
    # 将数据放到GPU上
    x = x.cuda()
    # 在GPU上运行
    model(x)

    如果使用PyTorch-Lightning,几乎不需要做任何额外的工作,只需要设置Trainer的gpus参数来指定要使用的GPU数量。

    # 指定训练的gpu id
    trainer = Trainer(gpus=[0])
    trainer.fit(model)

    在进行GPU训练时,需注意限制CPU和GPU之间的数据传输次数。尽量避免频繁地在CPU和GPU之间复制数据。

    此外,还要注意调用强制GPU同步的操作,如清空内存缓存torch.cuda.empty_cache()。这样的操作会阻塞所有GPU,直到它们都完成同步。

    然而,如果使用PyTorch-Lightning,则通常不需要担心这些问题。PyTorch-Lightning会小心地处理GPU同步和内存管理,以确保高效的训练过程。

    使用16-bit精度

    使用16-bit精度是一种惊人的技术,可以将内存占用减半。大多数模型通常使用32位精度进行训练,但是研究表明,使用16位精度的模型也可以表现得很好。混合精度则意味着在某些部分使用16位精度,但将权重等内容保持在32位精度。

    要在PyTorch中使用16位精度,可以安装NVIDIA的apex库,并对模型进行如下更改:

    # 在模型和优化器上启用16位精度
    model, optimizers = amp.initialize(model, optimizers, opt_level='O2')
    
    # 当进行.backward()时,让amp处理以便它可以对损失进行缩放
    with amp.scale_loss(loss, optimizer) as scaled_loss:
        scaled_loss.backward()

    apex库会处理大部分工作,包括梯度缩放,以防止梯度爆炸或接近零。

    在PyTorch-Lightning中,启用16位精度不需要修改模型的任何内容,也不需要执行上述代码。只需在创建Trainer时设置precision=16即可。

    trainer = Trainer(amp_level='O2', use_amp=False)
    trainer.fit(model)

    通过这样的设置,PyTorch-Lightning将自动启用16位精度,并根据需要进行梯度缩放和其他必要的操作。

    移动到多个GPUs中

    在多个GPU上进行训练有几种方法可以选择。以下是其中三种常见的方法:

    分批次训练(Batch Splitting)

    分批次训练是指将模型复制到每个GPU上,并将每个GPU中的一部分批次数据进行训练。

    # 在每个GPU上复制模型,并将批次的四分之一分配给每个GPU
    model = DataParallel(model, devices=[0, 1, 2, 3])
    
    # out有4个输出(每个GPU一个)
    out = model(x.cuda(0))

    在PyTorch-Lightning中,只需要增加gpus参数来指定使用的GPU数量,其他的无需更改。

    trainer = Trainer(gpus=[0, 1, 2, 3])
    trainer.fit(model)

    模型分布训练(Model Parallelism)

    模型可能太大无法完全放入内存中。例如,带有编码器和解码器的序列到序列模型可能需要占用大量内存。在这种情况下,可以将编码器和解码器放在不同的GPU上进行训练。

    # 将编码器放在GPU 0上,将解码器放在GPU 1上
    encoder_rnn.cuda(0)
    decoder_rnn.cuda(1)
    
    # 在GPU 0上运行输入数据通过编码器
    encoder_out = encoder_rnn(x.cuda(0))
    
    # 在GPU 1上运行输出通过解码器
    out = decoder_rnn(encoder_out.cuda(1))
    
    # 将输出数据移回GPU 0上
    out = out.cuda(0)

    在PyTorch-Lightning中,不需要指定任何GPU,只需将模型的模块放在正确的GPU上即可。

    class MyModule(LightningModule):
        def __init__():
            self.encoder = RNN(...)
            self.decoder = RNN(...)
        def forward(x):
            self.encoder.cuda(0)
            self.decoder.cuda(1)
            out = self.encoder(x)
            out = self.decoder(out.cuda(1))
    
    model = MyModule()
    trainer = Trainer()
    trainer.fit(model)

    混合使用(Hybrid Approach)

    # 更改这些行
    self.encoder = RNN(...)
    self.decoder = RNN(...)

    # 更改为
    # 现在每个RNN都基于不同的GPU设备
    self.encoder = DataParallel(self.encoder, devices=[0, 1, 2, 3])
    self.decoder = DataParallel(self.encoder, devices=[4, 5, 6, 7])

    # 在forward中...
    out = self.encoder(x.cuda(0))

    # 注意输入数据放在设备列表中的第一个设备上
    out = self.decoder(out.cuda(4)) #

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论