【问题标题】:Proper way to log things when using Pytorch Lightning DDP使用 Pytorch Lightning DDP 时记录事物的正确方法
【发布时间】:2021-06-25 11:29:23
【问题描述】:

我想知道在使用 DDP 时记录指标的正确方法是什么。我注意到如果我想在validation_epoch_end 中打印一些东西,使用 2 个 GPU 时会打印两次。我期待 validation_epoch_end 仅在等级 0 上被调用并接收来自所有 GPU 的输出,但我不确定这是否正确。因此我有几个问题:

  1. validation_epoch_end(self, outputs) - 使用 DDP 时,每个子进程是否接收从当前 GPU 处理的数据或从所有 GPU 处理的数据,即输入参数 outputs 是否包含来自所有 GPU 的整个验证集的输出?
  2. 如果 outputs 是 GPU/进程特定的,那么在使用 DDP 时,计算 validation_epoch_end 中整个验证集的任何指标的正确方法是什么?

我知道我可以通过检查 self.global_rank == 0 并仅在这种情况下打印/记录来解决打印问题,但是我正在尝试更深入地了解在这种情况下我正在打印/记录的内容。

这是我用例中的代码 sn-p。我希望能够报告整个验证数据集的 f1、精度和召回率,我想知道使用 DDP 时正确的做法是什么。

    def _process_epoch_outputs(self,
                               outputs: List[Dict[str, Any]]
                               ) -> Tuple[torch.Tensor, torch.Tensor]:
        """Creates and returns tensors containing all labels and predictions

        Goes over the outputs accumulated from every batch, detaches the
        necessary tensors and stacks them together.

        Args:
            outputs (List[Dict])
        """
        all_labels = []
        all_predictions = []

        for output in outputs:
            for labels in output['labels'].detach():
                all_labels.append(labels)

            for predictions in output['predictions'].detach():
                all_predictions.append(predictions)

        all_labels = torch.stack(all_labels).long().cpu()
        all_predictions = torch.stack(all_predictions).cpu()

        return all_predictions, all_labels

    def validation_epoch_end(self, outputs: List[Dict[str, Any]]) -> None:
        """Logs f1, precision and recall on the validation set."""

        if self.global_rank == 0:
            print(f'Validation Epoch: {self.current_epoch}')

        predictions, labels = self._process_epoch_outputs(outputs)
        for i, name in enumerate(self.label_columns):

            f1, prec, recall, t = metrics.get_f1_prec_recall(predictions[:, i],
                                                             labels[:, i],
                                                             threshold=None)
            self.logger.experiment.add_scalar(f'{name}_f1/Val',
                                              f1,
                                              self.current_epoch)
            self.logger.experiment.add_scalar(f'{name}_Precision/Val',
                                              prec,
                                              self.current_epoch)
            self.logger.experiment.add_scalar(f'{name}_Recall/Val',
                                              recall,
                                              self.current_epoch)

            if self.global_rank == 0:
                print((f'F1: {f1}, Precision: {prec}, '
                       f'Recall: {recall}, Threshold {t}'))

【问题讨论】:

    标签: python-3.x pytorch pytorch-lightning


    【解决方案1】:

    问题

    validation_epoch_end(self, outputs) - 当使用 DDP 时,每个 子进程接收当前GPU处理的数据或数据 从所有 GPU 处理,即输入参数是否输出 包含来自所有 GPU 的整个验证集的输出?

    仅从当前 GPU 处理的数据,输出不同步,只有 backward 同步(梯度在训练期间同步并分发到驻留在每个 GPU 上的模型的副本)。 p>

    想象一下,所有的输出都从1000 GPU 传递给这个可怜的主人,它可以很容易地给它一个 OOM

    如果输出是 GPU/进程特定的,那么计算的正确方法是什么 在validation_epoch_end 中的整个验证集上的任何度量 使用 DDP?

    根据documentation(强调我的):

    使用从每个批次中拆分数据的加速器进行验证时 跨 GPU,有时您可能需要在 ma​​ster 上聚合它们 GPU 用于处理(dp 或 ddp2)。

    这里是随附的代码(在这种情况下,validation_epoch_end 将通过单个步骤接收跨多个 GPU 的累积数据,也可以查看 cmets):

    # Done per-process (GPU)
    def validation_step(self, batch, batch_idx):
        x, y = batch
        y_hat = self.model(x)
        loss = F.cross_entropy(y_hat, y)
        pred = ...
        return {'loss': loss, 'pred': pred}
    
    # Gathered data from all processes (per single step)
    # Allows for accumulation so the whole data at the end of epoch
    # takes less memory
    def validation_step_end(self, batch_parts):
        gpu_0_prediction = batch_parts.pred[0]['pred']
        gpu_1_prediction = batch_parts.pred[1]['pred']
    
        # do something with both outputs
        return (batch_parts[0]['loss'] + batch_parts[1]['loss']) / 2
    
    def validation_epoch_end(self, validation_step_outputs):
       for out in validation_step_outputs:
           # do something with preds
    

    提示

    关注每个设备的计算和尽可能少的 GPU 间传输

    • validation_step 内部(或training_step,如果这是您想要的,这是一般情况)计算f1precisionrecall 和其他任何内容逐批
    • 返回这些值(例如,作为字典)。现在,您将从每个设备返回 3 数字,而不是 (batch, outputs)(可能要大得多)
    • validation_step_end 内部获取那些 3 值(如果您有 2 个 GPU,则实际上是 (2, 3))并对它们求和/取平均值并返回 3
    • 现在validation_epoch_end 将获得(steps, 3) 可用于累积的值

    如果不是在 validation_epoch_end 期间对值列表进行操作,而是将它们累积到另一个 3 值中,那就更好了(假设您有很多验证步骤,列表可能会变得太大),但这应该够了。

    AFAIK PyTorch-Lightning 不这样做(例如,不是添加到list,而是直接应用一些累加器),但我可能弄错了,所以任何更正都会很棒。

    【讨论】:

    • 感谢您的帮助!不幸的是,您提出的建议显然仅对文档中提到的 DP 和 DDP2 有效。我对此进行了测试并确认对于 DDP,即使我使用 validation_step_end,输出始终是 GPU 特定的......
    • @JovanAndonov 应该有 PyTorch “较低级别”的原语,如果是这种情况,您可能可以使用(不过可能会让人头疼)。
    猜你喜欢
    • 2021-10-18
    • 2021-05-21
    • 2021-06-01
    • 2021-02-06
    • 2021-03-03
    • 1970-01-01
    • 2021-10-12
    • 2021-06-30
    • 2021-06-04
    相关资源
    最近更新 更多