PyTorch 모델 성능 분석 및 최적화

당신의 작은 손을 이용해 돈을 벌고 좋아요를 눌러주세요!

딥 러닝 모델, 특히 대형 모델을 훈련하는 데는 비용이 많이 들 수 있습니다. 이러한 비용을 관리하는 데 사용할 수 있는 주요 방법 중 하나는 성능 최적화입니다. 성능 최적화는 애플리케이션 성능을 향상할 수 있는 기회를 지속적으로 찾은 다음 이러한 기회를 활용하는 반복적인 프로세스입니다. 이전 기사(여기 등)에서 우리는 이 분석을 수행하는 데 적절한 도구를 갖는 것이 중요하다는 점을 강조했습니다. 도구 선택은 훈련 가속기 유형(예: GPU, HPU 또는 기타) 및 훈련 프레임워크를 포함한 여러 요인에 따라 달라질 수 있습니다.

대체

이 기사 [1] 의 초점은 GPU 훈련에 PyTorch를 사용하는 것입니다. 보다 구체적으로 PyTorch에 내장된 성능 프로파일러인 PyTorch Profiler와 그 결과를 보는 방법 중 하나인 PyTorch Profiler TensorBoard 플러그인에 중점을 둘 것입니다.

이 문서는 PyTorch 프로파일러에 대한 공식 PyTorch 문서를 대체하거나 TensorBoard 플러그인을 사용하여 프로파일러 결과를 분석하기 위한 것이 아닙니다. 우리의 목표는 일상적인 개발 프로세스에서 이러한 도구를 사용하는 방법을 보여주는 것입니다.

한동안 저는 TensorBoard 플러그인 튜토리얼의 한 섹션에 특히 관심을 가졌습니다. 이 튜토리얼에서는 인기 있는 Cifar10 데이터 세트에서 훈련된 분류 모델(Resnet 아키텍처 기반)을 소개합니다. 다음으로 PyTorch Profiler 및 TensorBoard 플러그인을 사용하여 데이터 로더의 병목 현상을 식별하고 수정하는 방법을 보여줍니다.

대체

자세히 살펴보면 최적화된 GPU 활용도가 40.46%임을 알 수 있습니다. 이제 이것을 설탕으로 코팅할 방법이 없습니다. 이러한 결과는 정말 끔찍하며 밤에 잠을 설치게 해야 합니다. 과거에 확장한 것처럼 GPU는 훈련 기계에서 가장 비싼 리소스이며 우리의 목표는 활용도를 극대화하는 것입니다. 40.46%의 활용률 결과는 종종 교육을 가속화하고 비용을 절감할 수 있는 중요한 기회를 나타냅니다. 물론 우리는 더 잘할 수 있습니다! 이 블로그 게시물에서 우리는 더 나은 결과를 얻으려고 노력할 것입니다. 먼저 공식 튜토리얼에 제공된 결과를 재현하고 동일한 도구를 사용하여 훈련 성능을 더욱 향상시킬 수 있는지 살펴보겠습니다.

장난감 예

다음 코드 블록에는 TensorBoard 플러그인 튜토리얼에 정의된 훈련 루프가 포함되어 있으며 두 가지가 약간 수정되었습니다.

  1. 우리는 이 튜토리얼에서 사용된 CIFAR10 데이터세트와 동일한 속성과 동작을 가진 가짜 데이터세트를 사용합니다.
  2. torch.profiler.schedule을 초기화하여 준비 플래그를 3으로, 반복 플래그를 1로 설정합니다. 예열 단계 수가 약간 증가하면 분석 결과의 안정성이 향상되는 것으로 나타났습니다.
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

이 튜토리얼에서 사용된 GPU는 Tesla V100-DGXS-32GB입니다. 이 기사에서는 Tesla V100-SXM2–16GB GPU가 포함된 Amazon EC2 p3.2xlarge 인스턴스를 사용하여 이 튜토리얼의 성능 결과를 재현하고 이를 개선하려고 합니다. 동일한 아키텍처를 공유하지만 이 두 GPU 간에는 몇 가지 차이점이 있습니다. AWS PyTorch 2.0 Docker 이미지를 사용하여 훈련 스크립트를 실행합니다. TensorBoard 뷰어 개요 페이지에 표시된 학습 스크립트의 성능 결과는 다음과 같습니다.

대체

튜토리얼과 달리 실험의 개요 페이지(torch-tb-profiler 버전 0.4.1)는 세 가지 분석 단계를 하나로 결합한다는 점을 먼저 확인했습니다. 따라서 평균 총 단계 시간은 보고된 240ms가 아닌 80ms입니다. 이는 각 단계에 약 80밀리초가 소요되는 추적 탭(우리 경험에 따르면 거의 항상 더 정확한 보고를 제공함)에서 명확하게 확인할 수 있습니다.

대체

31.65% GPU 활용률과 80ms 단계 시간의 시작점은 튜토리얼에서 각각 소개된 23.54% 및 132ms 시작점과 다릅니다. 이는 GPU 유형 및 PyTorch 버전을 포함한 교육 환경의 차이로 인해 발생할 수 있습니다. 또한 튜토리얼 기준 결과에서는 성능 문제를 DataLoader의 병목 현상으로 명확하게 진단했지만 결과는 그렇지 않았습니다. 데이터 로딩 병목 현상은 개요 탭에서 "CPU Exec" 또는 "기타"의 높은 비율로 위장되는 경우가 많습니다.

최적화 1: 다중 프로세스 데이터 로딩

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

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

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

대체

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

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

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

优化2:内存固定

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

대체

为了解决这个问题,我们将应用 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)

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

대체

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

优化3:增加批量大小

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

대체

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

대체

虽然 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 倍,并且红色碍眼的现象几乎消失了:

대체

我们现在的 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 利用率的有用资源:

대체

该报告中最引人注目的细节之一是未使用 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%。

대체

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

대체

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

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

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

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

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

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

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

대체

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

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

대체

总结

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

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

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

Reference

[1]

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

이 글은 mdnice 멀티플랫폼 에 게재되었습니다.

추천

출처blog.csdn.net/swindler_ice/article/details/132397302