上海建站模板搭建,中国刚刚发生8件大事,wordpress 5.2设置中文版,企业qq下载官网下载安装这[1]是关于使用 PyTorch Profiler 和 TensorBoard 分析和优化 PyTorch 模型主题的系列文章的第三部分。我们的目的是强调基于 GPU 的训练工作负载的性能分析和优化的好处及其对训练速度和成本的潜在影响。特别是#xff0c;我们希望向所有机器学习开发人员展示 PyTorch Profi… 这[1]是关于使用 PyTorch Profiler 和 TensorBoard 分析和优化 PyTorch 模型主题的系列文章的第三部分。我们的目的是强调基于 GPU 的训练工作负载的性能分析和优化的好处及其对训练速度和成本的潜在影响。特别是我们希望向所有机器学习开发人员展示 PyTorch Profiler 和 TensorBoard 等分析工具的可访问性。您无需成为 CUDA 专家即可通过应用我们在帖子中讨论的技术获得有意义的性能提升。 在我们的第一篇文章中我们演示了如何使用 PyTorch Profiler TensorBoard 插件的不同视图来识别性能问题并回顾了一些用于加速训练的流行技术。在第二篇文章中我们展示了如何使用 TensorBoard 插件 Trace View 来识别张量何时从 CPU 复制到 GPU 以及返回。这种数据移动——可能会导致同步点并大大降低训练速度——通常是无意的有时很容易避免。这篇文章的主题是我们遇到 GPU 和 CPU 之间与张量副本无关的同步点的情况。与张量副本的情况一样这些可能会导致训练步骤停滞并大大减慢训练的整体时间。我们将演示此类事件的存在、如何使用 PyTorch Profiler 和 PyTorch Profiler TensorBoard 插件 Trace View 来识别它们以及以最小化此类同步事件的方式构建模型的潜在性能优势。 与我们之前的文章一样我们将定义一个玩具 PyTorch 模型然后迭代地分析其性能、识别瓶颈并尝试修复它们。我们将在 Amazon EC2 g5.2xlarge 实例包含 NVIDIA A10G GPU 和 8 个 vCPU上运行实验并使用官方 AWS PyTorch 2.0 Docker 映像。请记住我们描述的某些行为可能因 PyTorch 版本而异。 玩具示例 在下面的块中我们介绍了一个玩具 PyTorch 模型它对 256x256 输入图像执行语义分割即它采用 256x256 RGB 图像并输出来自十个语义类别的“每像素”标签的 256x256 映射。 import torchimport torch.nn as nnimport torch.nn.functional as Fimport torch.optimimport torch.profilerimport torch.utils.datafrom torch import Tensorclass Net(nn.Module): def __init__(self, num_hidden10, num_classes10): super().__init__() self.conv_in nn.Conv2d(3, 10, 3, paddingsame) hidden [] for i in range(num_hidden): hidden.append(nn.Conv2d(10, 10, 3, paddingsame)) hidden.append(nn.ReLU()) self.hidden nn.Sequential(*hidden) self.conv_out nn.Conv2d(10, num_classes, 3, paddingsame) def forward(self, x): x F.relu(self.conv_in(x)) x self.hidden(x) x self.conv_out(x) return x 为了训练我们的模型我们将使用标准交叉熵损失并进行一些修改 我们假设目标标签包含一个忽略值指示我们想要从损失计算中排除的像素。 我们假设语义标签之一将某些像素识别为属于图像的“背景”。我们定义损失函数来将它们视为忽略标签。 仅当我们遇到目标张量至少包含两个唯一值的批次时我们才会更新模型权重。 虽然我们出于演示目的选择了这些修改但这些类型的操作并不罕见并且可以在许多“标准”PyTorch 模型中找到。由于我们已经是性能分析方面的“专家”因此我们已经使用 torch.profiler.record_function 上下文管理器将每个操作包装在损失函数中如我们的第二篇文章中所述。 class MaskedLoss(nn.Module): def __init__(self, ignore_val-1, num_classes10): super().__init__() self.ignore_val ignore_val self.num_classes num_classes self.loss torch.nn.CrossEntropyLoss() def cross_entropy(self, pred: Tensor, target: Tensor) - Tensor: # create a boolean mask of valid labels with torch.profiler.record_function(create mask): mask target ! self.ignore_val # permute the logits in preparation for masking with torch.profiler.record_function(permute): permuted_pred torch.permute(pred, [0, 2, 3, 1]) # apply the boolean mask to the targets and logits with torch.profiler.record_function(mask): masked_target target[mask] masked_pred permuted_pred[mask.unsqueeze(-1).expand(-1, -1, -1, self.num_classes)] masked_pred masked_pred.reshape(-1, self.num_classes) # calculate the cross-entropy loss with torch.profiler.record_function(calc loss): loss self.loss(masked_pred, masked_target) return loss def ignore_background(self, target: Tensor) - Tensor: # discover all indices where target label is background with torch.profiler.record_function(non_zero): inds torch.nonzero(target self.num_classes - 1, as_tupleTrue) # reset all background labels to the ignore index with torch.profiler.record_function(index assignment): target[inds] self.ignore_val return target def forward(self, pred: Tensor, target: Tensor) - Tensor: # ignore background labels target self.ignore_background(target) # retrieve a list of unique elements in target with torch.profiler.record_function(unique): unique torch.unique(target) # check if the number of unique items pass the threshold with torch.profiler.record_function(numel): ignore_loss torch.numel(unique) 2 # calculate the cross-entropy loss loss self.cross_entropy(pred, target) # zero the loss in the case that the number of unique elements # is below the threshold if ignore_loss: loss 0. * loss return loss 我们的损失函数看起来很简单对吧错误的正如我们将在下面看到的损失函数包括许多触发主机设备同步事件的操作这些操作会大大降低训练速度 - 这些操作都不涉及将张量复制到 GPU 中或从 GPU 中复制出来。正如我们在上一篇文章中一样我们要求您在继续阅读之前尝试找出三个性能优化的机会。 为了演示的目的我们使用随机生成的图像和每像素标签图如下定义。 from torch.utils.data import Dataset# A dataset with random images and label mapsclass FakeDataset(Dataset): def __init__(self, num_classes10): super().__init__() self.num_classes num_classes self.img_size [256, 256] def __len__(self): return 1000000 def __getitem__(self, index): rand_image torch.randn([3]self.img_size, dtypetorch.float32) rand_label torch.randint(low-1, highself.num_classes, sizeself.img_size) return rand_image, rand_labeltrain_set FakeDataset()train_loader torch.utils.data.DataLoader(train_set, batch_size256, shuffleTrue, num_workers8, pin_memoryTrue) 最后我们使用根据我们的需求配置的 PyTorch Profiler 定义训练步骤 device torch.device(cuda:0)model Net().cuda(device)criterion MaskedLoss().cuda(device)optimizer torch.optim.SGD(model.parameters(), lr0.001, momentum0.9)model.train()# training loop wrapped with profiler objectwith torch.profiler.profile( scheduletorch.profiler.schedule(wait1, warmup4, active3, repeat1), on_trace_readytorch.profiler.tensorboard_trace_handler(/tmp/prof), record_shapesTrue, profile_memoryTrue, with_stackTrue) as prof: for step, data in enumerate(train_loader): inputs data[0].to(devicedevice, non_blockingTrue) labels data[1].to(devicedevice, non_blockingTrue) if step (1 4 3) * 1: break outputs model(inputs) loss criterion(outputs, labels) optimizer.zero_grad(set_to_noneTrue) loss.backward() optimizer.step() prof.step() 如果您天真地运行这个训练脚本您可能会看到 GPU 利用率很高~90%但不知道它有什么问题。只有通过分析我们才能识别潜在的性能瓶颈和训练加速的潜在机会。那么话不多说让我们看看我们的模型的表现如何。 初始性能结果 在这篇文章中我们将重点介绍 PyTorch Profiler TensorBoard 插件的跟踪视图。请参阅我们之前的文章了解有关如何使用该插件支持的其他一些视图的提示。 在下图中我们显示了玩具模型单个训练步骤的跟踪视图。 我们可以清楚地看到我们的 1.3 秒长训练步骤完全由损失函数第一行中的 torch.nonzero 运算符主导。所有其他操作都聚集在巨大的 cudaMemcpyAsyn 事件的两侧。到底是怎么回事为何如此看似平淡无奇的行动却会引起如此大的眼花缭乱呢 也许我们不应该如此惊讶因为 torch.nonzero 文档确实包含以下注释“当输入位于 CUDA 上时torch.nonzero() 会导致主机设备同步。”与其他常见的 PyTorch 操作相反torch.nonzero 返回的张量的大小不是预先确定的因此需要同步。 CPU提前不知道输入张量中有多少个非零元素。它需要等待来自 GPU 的同步事件以便执行适当的 GPU 内存分配并适当地准备后续的 PyTorch 操作。 请注意cudaMempyAsync 的长度并不表示 torch.nonzero 操作的复杂性而是反映了 CPU 需要等待 GPU 完成 CPU 启动的所有先前内核的时间量。例如如果我们在第一个调用之后立即进行额外的 torch.nonzero 调用那么我们的第二个 cudaMempyAsync 事件将比第一个事件显着短因为 CPU 和 GPU 已经或多或少“同步”。 请记住这个解释来自非 CUDA 专家所以请随意理解…… 优化 #1减少 torch.nonzero 操作的使用 现在我们了解了瓶颈的根源挑战就变成了寻找执行相同逻辑但不会触发主机设备同步事件的替代操作序列。对于我们的损失函数我们可以使用 torch.where 运算符轻松完成此操作如下面的代码块所示 def ignore_background(self, target: Tensor) - Tensor: with torch.profiler.record_function(update background): target torch.where(targetself.num_classes-1, -1*torch.ones_like(target),target) return target 在下图中我们显示了此更改后的跟踪视图。 虽然我们成功删除了来自 torch.nonzero 操作的 cudaMempyAsync但它已立即被来自 torch.unique 操作的 cudaMempyAsync 替换并且我们的步骤时间没有变化。这里的 PyTorch 文档不太友好但根据我们之前的经验我们可以假设由于我们使用了大小不确定的张量我们再次遭受主机设备同步事件的困扰。 优化 #2减少 torch.unique 操作的使用 用等效的替代方案替换 torch.unique 运算符并不总是可行的。然而在我们的例子中我们实际上不需要知道唯一标签的值我们只需要知道唯一标签的数量。这可以通过在展平的目标张量上应用 torch.sort 操作并计算所得步骤函数中的步骤数来计算。 def forward(self, pred: Tensor, target: Tensor) - Tensor: # ignore background labels target self.ignore_background(target) # sort the list of labels with torch.profiler.record_function(sort): sorted,_ torch.sort(target.flatten()) # indentify the steps of the resultant step function with torch.profiler.record_function(deriv): deriv sorted[1:]-sorted[:-1] # count the number of steps with torch.profiler.record_function(count_nonzero): num_unique torch.count_nonzero(deriv)1 # calculate the cross-entropy loss loss self.cross_entropy(pred, target) # zero the loss in the case that the number of unique elements # is below the threshold with torch.profiler.record_function(where): loss torch.where(num_unique2, 0.*loss, loss) return loss 在下图中我们捕获了第二次优化后的跟踪视图 我们再次解决了一个瓶颈但又面临一个新的瓶颈这次来自布尔掩码例程。 布尔掩码是我们常用的例程用于减少所需的机器操作总数。在我们的例子中我们的目的是通过删除“忽略”像素并将交叉熵计算限制为感兴趣的像素来减少计算量。显然这适得其反。和以前一样应用布尔掩码会导致大小不确定的张量并且它触发的 cudaMempyAsync 大大掩盖了排除“忽略”像素所节省的任何费用。 优化 #3注意布尔掩码操作 在我们的例子中解决这个问题相当简单因为 PyTorch CrossEntropyLoss 有一个用于设置ignore_index的内置选项。 class MaskedLoss(nn.Module): def __init__(self, ignore_val-1, num_classes10): super().__init__() self.ignore_val ignore_val self.num_classes num_classes self.loss torch.nn.CrossEntropyLoss(ignore_index-1) def cross_entropy(self, pred: Tensor, target: Tensor) - Tensor: with torch.profiler.record_function(calc loss): loss self.loss(pred, target) return loss 在下图中我们显示了生成的跟踪视图 天啊我们的步数时间已一路下降至 5.4 毫秒。这比我们开始时快了 240 (!!) 倍。通过简单地改变一些函数调用并且不对损失函数逻辑进行任何修改我们能够显着优化训练步骤的性能。 重要提示在我们选择的玩具示例中我们为减少 cudaMempyAsync 事件数量而采取的步骤对训练步骤时间有明显影响。然而在某些情况下相同类型的更改可能会损害而不是提高性能。例如在布尔掩码的情况下如果我们的掩码非常稀疏并且原始张量非常大那么应用掩码所节省的计算量可能会超过主机设备同步的成本。重要的是应根据具体情况评估每次优化的影响。 总结 在这篇文章中我们重点关注由主机设备同步事件引起的训练应用程序中的性能问题。我们看到了触发此类事件的 PyTorch 运算符的几个示例 - 所有这些运算符的共同属性是它们输出的张量的大小取决于输入。您可能还会遇到来自其他操作员的同步事件本文未介绍。我们演示了如何使用 PyTorch Profiler 等性能分析器及其关联的 TensorBoard 插件来识别此类事件。 在我们的玩具示例中我们能够找到有问题的运算符的等效替代方案这些运算符使用固定大小的张量并避免需要同步事件。这些导致训练时间显着缩短。然而在实践中您可能会发现解决此类瓶颈要困难得多甚至是不可能的。有时克服它们可能需要重新设计模型的某些部分。 Reference [1] Source: https://towardsdatascience.com/pytorch-model-performance-analysis-and-optimization-part-3-1c5876d78fe2 本文由 mdnice 多平台发布