使用 Pytorch Lightning DDP 时记录内容的正确方法

Jov*_*nov 8 python-3.x pytorch pytorch-lightning

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

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

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

这是我的用例中的代码片段。我希望能够报告整个验证数据集的 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}'))
Run Code Online (Sandbox Code Playgroud)

Szy*_*zke 3

问题

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

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

想象一下,所有输出都从1000GPU 传递到这个可怜的主控器,它很容易就会导致 OOM

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

根据文件(强调我的):

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

这里是附带的代码(validation_epoch_end在这种情况下,将通过单步接收跨多个 GPU 的累积数据,另请参阅注释):

# 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
Run Code Online (Sandbox Code Playgroud)

尖端

专注于每个设备的计算以及尽可能少的 GPU 之间的传输

  • 内部validation_step(或者training_step如果这是你想要的,这是一般的)计算f1precision,以及每批次的recall其他内容
  • 返回这些值(例如,作为字典)。现在,您将从3每个设备返回数字,而不是(batch, outputs)(可能会大得多)
  • 在里面validation_step_end获取这些3值(实际上(2, 3)如果你有 2 个 GPU)并对它们求和/取平均值并返回3
  • 现在validation_epoch_end将获得(steps, 3)您可以用来积累的价值

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

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