Analyse et optimisation des performances du modèle PyTorch

Utilisez vos petites mains pour faire fortune et donnez-lui un like !

La formation de modèles d’apprentissage profond, en particulier de grands modèles, peut représenter une dépense coûteuse. L’optimisation des performances est l’une des principales méthodes que nous pouvons utiliser pour gérer ces coûts. L'optimisation des performances est un processus itératif dans lequel nous recherchons continuellement des opportunités pour améliorer les performances des applications, puis exploitons ces opportunités. Dans des articles précédents (comme ici ), nous avons souligné l'importance de disposer des outils appropriés pour effectuer cette analyse. Le choix de l'outil peut dépendre de nombreux facteurs, notamment du type d'accélérateur de formation (par exemple, GPU, HPU ou autre) et du cadre de formation.

alt

Cet article [1] se concentre sur l'utilisation de PyTorch pour la formation sur GPU. Plus précisément, nous nous concentrerons sur le profileur de performances intégré de PyTorch, PyTorch Profiler, et sur l'un des moyens d'afficher ses résultats, le plugin TensorBoard PyTorch Profiler.

Cet article n'est pas destiné à remplacer la documentation officielle de PyTorch sur PyTorch Profiler ou à utiliser le plugin TensorBoard pour analyser les résultats du profileur. Notre objectif est de montrer comment utiliser ces outils dans votre processus de développement quotidien.

Depuis quelques temps, je suis particulièrement intéressé par une section du tutoriel du plugin TensorBoard. Ce didacticiel présente un modèle de classification (basé sur l'architecture Resnet) formé sur le jeu de données populaire Cifar10. Ensuite, nous montrons comment utiliser les plugins PyTorch Profiler et TensorBoard pour identifier et corriger les goulots d'étranglement dans les chargeurs de données.

alt

Si vous regardez attentivement, vous verrez que l'utilisation optimisée du GPU est de 40,46 %. Maintenant, il n’y a aucun moyen d’édulcorer cela : ces résultats sont absolument terribles et devraient vous empêcher de dormir la nuit. Comme nous l'avons développé dans le passé, le GPU est la ressource la plus coûteuse de notre machine d'entraînement et notre objectif devrait être de maximiser son utilisation. Un résultat d'utilisation de 40,46 % représente souvent une opportunité significative d'accélérer la formation et de réduire les coûts. Bien sûr, nous pouvons faire mieux ! Dans cet article de blog, nous essaierons de faire mieux. Nous allons d'abord essayer de reproduire les résultats fournis dans le tutoriel officiel et voir si nous pouvons utiliser les mêmes outils pour améliorer encore les performances de l'entraînement.

Exemple de jouet

Le bloc de code suivant contient la boucle d'entraînement définie dans le didacticiel du plug-in TensorBoard, avec deux modifications mineures :

  1. Nous utilisons un faux ensemble de données avec les mêmes propriétés et comportement que l'ensemble de données CIFAR10 utilisé dans ce didacticiel.
  2. Nous initialisons torch.profiler.schedule, en définissant l'indicateur d'échauffement sur 3 et l'indicateur de répétition sur 1. Nous avons constaté qu'une légère augmentation du nombre d'étapes de préchauffage améliorait la stabilité des résultats analytiques.
import numpy as np
import torch
import torch.nn
import torch.optim
import torch.profiler
import torch.utils.data
import torchvision.datasets
import torchvision.models
import torchvision.transforms as T
from torchvision.datasets.vision import VisionDataset
from PIL import Image

class FakeCIFAR(VisionDataset):
    def __init__(self, transform):
        super().__init__(root=None, transform=transform)
        self.data = np.random.randint(low=0,high=256,size=(10000,32,32,3),dtype=np.uint8)
        self.targets = np.random.randint(low=0,high=10,size=(10000),dtype=np.uint8).tolist()

    def __getitem__(self, index):
        img, target = self.data[index], self.targets[index]
        img = Image.fromarray(img)
        if self.transform is not None:
            img = self.transform(img)
        return img, target

    def __len__(self) -> int:
        return len(self.data)

transform = T.Compose(
    [T.Resize(224),
     T.ToTensor(),
     T.Normalize((0.50.50.5), (0.50.50.5))])

train_set = FakeCIFAR(transform=transform)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=32
                                           shuffle=True)

device = torch.device("cuda:0")
model = torchvision.models.resnet18(weights='IMAGENET1K_V1').cuda(device)
criterion = torch.nn.CrossEntropyLoss().cuda(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
model.train()

# train step
def train(data):
    inputs, labels = data[0].to(device=device), data[1].to(device=device)
    outputs = model(inputs)
    loss = criterion(outputs, labels)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

# training loop wrapped with profiler object
with torch.profiler.profile(
        schedule=torch.profiler.schedule(wait=1, warmup=4, active=3, repeat=1),
        on_trace_ready=torch.profiler.tensorboard_trace_handler('./log/resnet18'),
        record_shapes=True,
        profile_memory=True,
        with_stack=True
as prof:
    for step, batch_data in enumerate(train_loader):
        if step >= (1 + 4 + 3) * 1:
            break
        train(batch_data)
        prof.step()  # Need to call this at the end of each step

Le GPU utilisé dans ce tutoriel est le Tesla V100-DGXS-32GB. Dans cet article, nous essayons de reproduire les résultats de performances de ce didacticiel et de l'améliorer à l'aide d'une instance Amazon EC2 p3.2xlarge contenant un GPU Tesla V100-SXM2-16 Go. Bien qu’ils partagent la même architecture, il existe quelques différences entre ces deux GPU. Nous utilisons l'image Docker AWS PyTorch 2.0 pour exécuter le script de formation. Les résultats de performances du script de formation affichés sur la page de présentation de la visionneuse TensorBoard sont présentés ci-dessous :

alt

Nous remarquons d'abord que, contrairement au tutoriel, la page de présentation de notre expérience (torch-tb-profiler version 0.4.1) combine trois étapes d'analyse en une seule. Par conséquent, le temps de pas total moyen est de 80 ms, et non les 240 ms indiqués. Cela se voit clairement dans l'onglet Suivi (qui, d'après notre expérience, fournit presque toujours des rapports plus précis), où chaque étape prend environ 80 millisecondes.

alt

Notez que notre point de départ d'une utilisation du GPU de 31,65 % et d'un temps de pas de 80 ms est différent des points de départ de 23,54 % et 132 ms présentés respectivement dans le didacticiel. Cela peut être dû aux différences dans les environnements de formation, notamment le type de GPU et la version de PyTorch. Nous avons également remarqué que même si les résultats de base du didacticiel diagnostiquaient clairement le problème de performances comme un goulot d'étranglement dans le DataLoader, ce n'était pas le cas de nos résultats. Nous constatons souvent que les goulots d'étranglement lors du chargement des données se déguisent en un pourcentage élevé de "CPU Exec" ou "Autre" dans l'onglet Présentation.

Optimisation 1 : chargement de données multi-processus

让我们首先应用本教程中所述的多进程数据加载。由于 Amazon EC2 p3.2xlarge 实例有 8 个 vCPU,我们将 DataLoader 工作线程的数量设置为 8 以获得最大性能:

train_loader = torch.utils.data.DataLoader(train_set, batch_size=32
                               shuffle=True, num_workers=8)

本次优化的结果如下所示:

alt

对单行代码的更改使 GPU 利用率提高了 200% 以上(从 31.65% 增加到 72.81%),并将训练步骤时间减少了一半以上(从 80 毫秒减少到 37 毫秒)。

本教程中的优化过程到此结束。虽然我们的 GPU 利用率 (72.81%) 比教程中的结果 (40.46%) 高很多,但我毫不怀疑,像我们一样,您会发现这些结果仍然非常不令人满意。

个人评论,您可以随意跳过:想象一下,如果 PyTorch 在 GPU 上训练时默认应用多进程数据加载,可以节省多少全球资金!确实,使用多重处理可能会产生一些不需要的副作用。尽管如此,必须有某种形式的自动检测算法可以运行,以排除识别潜在问题场景的存在,并相应地应用此优化。

优化2:内存固定

如果我们分析上次实验的 Trace 视图,我们可以看到大量时间(37 毫秒中的 10 毫秒)仍然花费在将训练数据加载到 GPU 上。

alt

为了解决这个问题,我们将应用 PyTorch 推荐的另一个优化来简化数据输入流,即内存固定。使用固定内存可以提高主机到 GPU 数据复制的速度,更重要的是,允许我们使它们异步。这意味着我们可以在 GPU 中准备下一个训练批次,同时在当前批次上运行训练步骤。有关更多详细信息以及内存固定的潜在副作用,请参阅 PyTorch 文档。

此优化需要更改两行代码。首先,我们将 DataLoader 的 pin_memory 标志设置为 True。

train_loader = torch.utils.data.DataLoader(train_set, batch_size=32
                          shuffle=True, num_workers=8, pin_memory=True)

然后我们将主机到设备的内存传输(在训练函数中)修改为非阻塞:

inputs, labels = data[0].to(device=device, non_blocking=True), \
                 data[1].to(device=device, non_blocking=True)

内存固定优化的结果如下所示:

alt

我们的 GPU 利用率现在达到了可观的 92.37%,并且我们的步数时间进一步减少。但我们仍然可以做得更好。请注意,尽管进行了这种优化,性能报告仍然表明我们花费了大量时间将数据复制到 GPU 中。我们将在下面的步骤 4 中再次讨论这一点。

优化3:增加批量大小

对于我们的下一个优化,我们将注意力集中在上一个实验的内存视图上:

alt

该图表显示,在 16 GB 的 GPU 内存中,我们的利用率峰值低于 1 GB。这是资源利用不足的一个极端例子,通常(尽管并非总是)表明有提高性能的机会。控制内存利用率的一种方法是增加批处理大小。在下图中,我们显示了将批处理大小增加到 512(内存利用率增加到 11.3 GB)时的性能结果。

alt

虽然 GPU 利用率指标没有太大变化,但我们的训练速度显着提高,从每秒 1200 个样本(批量大小 32 为 46 毫秒)到每秒 1584 个样本(批量大小 512 为 324 毫秒)。

注意:与我们之前的优化相反,增加批量大小可能会对训练应用程序的行为产生影响。不同的模型对批量大小的变化表现出不同程度的敏感度。有些可能只需要对优化器设置进行一些调整即可。对于其他人来说,调整到大批量可能会更困难甚至不可能。请参阅上一篇文章,了解大批量训练中涉及的一些挑战。

优化4:减少主机到设备的复制

您可能注意到了我们之前的结果中饼图中代表主机到设备数据副本的红色大碍眼。解决这种瓶颈最直接的方法就是看看是否可以减少每批的数据量。请注意,在图像输入的情况下,我们将数据类型从 8 位无符号整数转换为 32 位浮点数,并在执行数据复制之前应用归一化。在下面的代码块中,我们建议对输入数据流进行更改,其中我们延迟数据类型转换和规范化,直到数据位于 GPU 上:

# maintain the image input as an 8-bit uint8 tensor
transform = T.Compose(
    [T.Resize(224),
     T.PILToTensor()
     ])
train_set = FakeCIFAR(transform=transform)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=1024, shuffle=True, num_workers=8, pin_memory=True)

device = torch.device("cuda:0")
model = torch.compile(torchvision.models.resnet18(weights='IMAGENET1K_V1').cuda(device), fullgraph=True)
criterion = torch.nn.CrossEntropyLoss().cuda(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
model.train()

# train step
def train(data):
    inputs, labels = data[0].to(device=device, non_blocking=True), \
                     data[1].to(device=device, non_blocking=True)
    # convert to float32 and normalize
    inputs = (inputs.to(torch.float32) / 255. - 0.5) / 0.5
    outputs = model(inputs)
    loss = criterion(outputs, labels)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

由于这一变化,从 CPU 复制到 GPU 的数据量减少了 4 倍,并且红色碍眼的现象几乎消失了:

alt

我们现在的 GPU 利用率达到新高,达到 97.51%(!!),训练速度达到每秒 1670 个样本!让我们看看我们还能做什么。

优化5:将渐变设置为“无”

在这个阶段,我们似乎充分利用了 GPU,但这并不意味着我们不能更有效地利用它。一种流行的优化据说可以减少 GPU 中的内存操作,即在每个训练步骤中将模型参数梯度设置为 None 而不是零。有关此优化的更多详细信息,请参阅 PyTorch 文档。实现此优化所需要做的就是将optimizer.zero_grad调用的set_to_none设置为True:

optimizer.zero_grad(set_to_none=True)

在我们的例子中,这种优化并没有以任何有意义的方式提高我们的性能。

优化6:自动混合精度

GPU 内核视图显示 GPU 内核处于活动状态的时间量,并且可以成为提高 GPU 利用率的有用资源:

alt

该报告中最引人注目的细节之一是未使用 GPU Tensor Core。 Tensor Core 可在相对较新的 GPU 架构上使用,是用于矩阵乘法的专用处理单元,可以显着提高 AI 应用程序性能。它们的缺乏使用可能代表着优化的主要机会。

由于 Tensor Core 是专门为混合精度计算而设计的,因此提高其利用率的一种直接方法是修改我们的模型以使用自动混合精度(AMP)。在 AMP 模式下,模型的部分会自动转换为较低精度的 16 位浮点并在 GPU TensorCore 上运行。

重要的是,请注意,AMP 的完整实现可能需要梯度缩放,但我们的演示中并未包含该梯度缩放。在进行调整之前,请务必查看有关混合精度训练的文档。

下面的代码块演示了启用 AMP 所需的训练步骤的修改。

def train(data):
    inputs, labels = data[0].to(device=device, non_blocking=True), \
                     data[1].to(device=device, non_blocking=True)
    inputs = (inputs.to(torch.float32) / 255. - 0.5) / 0.5
    with torch.autocast(device_type='cuda', dtype=torch.float16):
        outputs = model(inputs)
        loss = criterion(outputs, labels)
    # Note - torch.cuda.amp.GradScaler() may be required  
    optimizer.zero_grad(set_to_none=True)
    loss.backward()
    optimizer.step()

对 Tensor Core 利用率的影响如下图所示。尽管它继续表明有进一步改进的机会,但仅用一行代码,利用率就从 0% 跃升至 26.3%。

alt

除了提高 Tensor Core 利用率之外,使用 AMP 还可以降低 GPU 内存利用率,从而释放更多空间来增加批处理大小。下图捕获了 AMP 优化且批量大小设置为 1024 后的训练性能结果:

alt

尽管 GPU 利用率略有下降,但我们的主要吞吐量指标进一步增加了近 50%,从每秒 1670 个样本增加到 2477 个样本。我们正在发挥作用!

注意:降低模型部分的精度可能对其收敛产生有意义的影响。与增加批量大小(见上文)的情况一样,使用混合精度的影响会因模型而异。在某些情况下,AMP 会毫不费力地工作。其他时候,您可能需要更加努力地调整自动缩放器。还有一些时候,您可能需要显式设置模型不同部分的精度类型(即手动混合精度)。

优化7:在图形模式下训练

我们将应用的最终优化是模型编译。与默认的 PyTorch 急切执行模式相反,其中每个 PyTorch 操作都“急切”运行,编译 API 将模型转换为中间计算图,然后以最适合底层的方式编译为低级计算内核。

以下代码块演示了应用模型编译所需的更改:

model = torchvision.models.resnet18(weights='IMAGENET1K_V1').cuda(device)
model = torch.compile(model)

模型编译优化结果如下所示:

alt

与之前实验中的 2477 个样本相比,模型编译进一步将我们的吞吐量提高到每秒 3268 个样本,性能额外提升了 32% (!!)。

图编译改变训练步骤的方式在 TensorBoard 插件的不同视图中非常明显。例如,内核视图表明使用了新的(融合的)GPU 内核,而跟踪视图(如下所示)显示了与我们之前看到的完全不同的模式。

alt

总结

在这篇文章中,我们展示了玩具分类模型性能优化的巨大潜力。尽管还有其他性能分析器可供您使用,每种分析器都有其优点和缺点,但我们选择了 PyTorch Profiler 和 TensorBoard 插件,因为它们易于集成。

我们应该强调的是,成功优化的路径将根据训练项目的细节(包括模型架构和训练环境)而有很大差异。在实践中,实现您的目标可能比我们在此介绍的示例更困难。我们描述的一些技术可能对您的表现影响不大,甚至可能使情况变得更糟。我们还注意到,我们选择的精确优化以及我们选择应用它们的顺序有些随意。强烈鼓励您根据项目的具体细节开发自己的工具和技术来实现优化目标。

机器学习工作负载的性能优化有时被视为次要的、非关键的和令人讨厌的。我希望我们已经成功地让您相信,节省开发时间和成本的潜力值得在性能分析和优化方面进行有意义的投资。

Reference

[1]

Source: https://towardsdatascience.com/pytorch-model-performance-analysis-and-optimization-10c3c5822869

Cet article est publié par mdnice multi-plateforme

Je suppose que tu aimes

Origine blog.csdn.net/swindler_ice/article/details/132397302
conseillé
Classement