Lightning-AI / Lightning-AI/pytorch-lightning

Validation has an impact on training

Open
#16,827 0 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug reproducibility
Dominant language
Python
Stars
31.4k
Forks
3.8k
Avg merge
6d 7h
Merged PRs (30d)
6

Description

Bug description

I have an inconsistency when training models. The training changes depending on the validation. I don't mean it changes because of the LR scheduler being fed with validation results, or early stopping, but simply doing (or not) the validation at the end of each epoch changes the loss of the model afterwards.

What I know:

  • It only happens when training in multi-gpu mode with DDP
  • If I change the validation batch size the training is different
  • If I don't go through validation at the end of each epoch, the training loss is different too
  • A larger batch size seems to provide results closer to the ones I get without validation
  • However if I don't change the validation process (bsz, or no validation), the results are consistent between each other
  • The model itself (state_dict) is exactly the same before and after validation
  • Before the first validation, the training losses are always exactly the same between the models. It's always after that first validation that the training losses start to diverge
  • I tried the same experiments without a HuggingFace transformer model (just a LinearLayer instead, or a torchaudio.models.Wav2Vec2Model), and I didn't noticed any difference. At first, this led me thinking the issue was from HuggingFace, but I'm not able to reproduce the bug without Pytorch Lightning.

On the screen below you can find 6 different runs using a MWE.

  • models ending by debug128 are validated using a valid batch size of 128
  • models ending by debug8 are validated using a valid batch size of 8
  • models ending with debugX-novalid are not validated, but the validation dataloader has been created with a batch size of X
    Screenshot 2023-02-21 at 15 43 32
How to reproduce the bug

Here is a example code that reproduce this bug. I used a first script to generate dummy random datasets. I used Wav2Vec2ForSequenceClassification as the HuggingFace model, but I had the issue with other HF models.

import torch

NUM_LABELS = 100

def generate_dummy_set(num_samples):
    input_lengths = torch.randint(low=16_000, high=4 * 16_000, size=(num_samples,)).tolist()
    return [
        {
            'input': torch.rand((input_lengths[sample],)),
            'label': torch.randint(low=0, high=NUM_LABELS, size=(1,))
        } for sample in range(num_samples)
    ],

torch.save(generate_dummy_set(1000), "random_train_1000.pt")
torch.save(generate_dummy_set(600), "random_valid_600.pt")
#!/usr/bin/env python3

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--valid_bsz', required=True, type=int)
parser.add_argument('--do_valid', action='store_true', default=False)
parser.add_argument('--model_version', required=True, type=str)
parser.add_argument('--wav2vec2_pretrained', default='facebook/wav2vec2-base', type=str)

args = parser.parse_args()

NUM_LABELS = 100

BSZ_TRAIN = 64
BSZ_VALID = args.valid_bsz

################################################################################

import os
import sys
import logging
import tempfile

import torch
import pytorch_lightning as pl
import torch.distributed as dist

logging.basicConfig(
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    level=os.environ.get("LOGLEVEL", "INFO").upper(),
    handlers=[logging.StreamHandler()]
)
logger = logging.getLogger()

################################################################################

# bypass slurm related mechanics of PL
for var in list(os.environ.keys()):
    if var.startswith("SLURM_"):
        del os.environ[var]

def get_trainer():
    return pl.Trainer(
        callbacks=[],
        logger=pl.loggers.TensorBoardLogger(
            save_dir='tensorboard/',
            version=args.model_version,
            name='debug',
        ),
        log_every_n_steps=1,
        strategy='ddp',
        accelerator='gpu',
        devices=-1,
        accumulate_grad_batches=4, # same issue with no accumulate grad
    )

class DataModule(pl.LightningDataModule):

    def __init__(self):
        super().__init__()

    def setup(self, stage):
        self.num_workers = 10 # 0 produces the same issue
        self.dummy_train = torch.load("random_train_1000.pt")
        self.dummy_val = torch.load("random_valid_600.pt")

    def train_dataloader(self):
        return torch.utils.data.DataLoader(
            self.dummy_train,
            shuffle=True,
            batch_size=BSZ_TRAIN,
            num_workers=self.num_workers,
            collate_fn=self.collate_fn
        )

    def val_dataloader(self):
        return torch.utils.data.DataLoader(
            self.dummy_val,
            shuffle=False,
            batch_size=BSZ_VALID,
            num_workers=self.num_workers,
            collate_fn=self.collate_fn
        )

    def collate_fn(self, samples):
        return {
            'input': torch.nn.utils.rnn.pad_sequence(
                [samples[i]['input'] for i in range(len(samples))],
                batch_first=True, padding_value=0
            ),
            'label': torch.nn.utils.rnn.pad_sequence(
                [samples[i]['label'] for i in range(len(samples))],
                batch_first=True, padding_value=0
            ),
            'bsz': len(samples)
        }

class HFModel(pl.LightningModule):

    def __init__(self):
        super().__init__()
        from transformers import Wav2Vec2ForSequenceClassification
        self.model = Wav2Vec2ForSequenceClassification.from_pretrained(
            args.wav2vec2_pretrained,
            num_labels=NUM_LABELS,
            gradient_checkpointing=False
        )

    def forward(self, batch, labels=None):
        return self.model(input_values=batch['input'], labels=labels)

    def training_step(self, batch, batch_idx):
        outputs = self(batch, labels=batch['label'][:,0])
        loss = outputs.loss
        self.log('train__loss', loss, batch_size=batch['bsz'])
        return loss

    def validation_step(self, batch, batch_idx):
        if args.do_valid:
            outputs = self(batch)

    def configure_optimizers(self):
        return {
            'optimizer': torch.optim.AdamW(self.parameters(), lr=0.02, weight_decay=0.0)
        }

def main():
    pl.seed_everything(42, workers=True)
    trainer = get_trainer()

    data = DataModule()
    model = HFModel()

    trainer.fit(
        model=model,
        datamodule=data,
    )

if __name__ ==  '__main__':
    main()
Error messages and logs

Tensorboard results can be found here: https://tensorboard.dev/experiment/gKs1b3ZuQTWnEpt4wlGdUg/#scalars&_smoothingWeight=0
Not all models were stopped at the same step, I manually stopped them when I noticed they diverged.

Environment
Current environment I just updated my environment, as I had the same issue with an older environment (torch 1.12.1, pl 1.8.0).
* CUDA:
        - GPU:               None
        - available:         False
        - version:           11.6
* Lightning:
        - lightning-utilities: 0.7.0
        - pytorch-lightning: 1.9.2
        - torch:             1.13.1+cu116
        - torchaudio:        0.13.1+cu116
        - torchmetrics:      0.11.1
        - torchvision:       0.14.1+cu116
* Packages:
        - aiofiles:          22.1.0
        - aiohttp:           3.8.4
        - aiosignal:         1.3.1
        - aiosqlite:         0.18.0
        - alembic:           1.9.4
        - anyio:             3.6.2
        - argon2-cffi:       21.3.0
        - argon2-cffi-bindings: 21.2.0
        - arrow:             1.2.3
        - asttokens:         2.2.1
        - async-generator:   1.10
        - async-timeout:     4.0.2
        - attrs:             22.2.0
        - babel:             2.11.0
        - backcall:          0.2.0
        - beautifulsoup4:    4.11.2
        - bleach:            6.0.0
        - certifi:           2022.12.7
        - certipy:           0.1.3
        - cffi:              1.15.1
        - charset-normalizer: 3.0.1
        - comm:              0.1.2
        - cryptography:      39.0.1
        - datasets:          2.9.0
        - debugpy:           1.6.6
        - decorator:         5.1.1
        - defusedxml:        0.7.1
        - dill:              0.3.6
        - executing:         1.2.0
        - fastjsonschema:    2.16.2
        - filelock:          3.9.0
        - fqdn:              1.5.1
        - frozenlist:        1.3.3
        - fsspec:            2023.1.0
        - greenlet:          2.0.2
        - huggingface-hub:   0.12.1
        - idna:              3.4
        - ipdb:              0.13.11
        - ipykernel:         6.21.2
        - ipython:           8.10.0
        - ipython-genutils:  0.2.0
        - isoduration:       20.11.0
        - jedi:              0.18.2
        - jinja2:            3.1.2
        - json5:             0.9.11
        - jsonpointer:       2.3
        - jsonschema:        4.17.3
        - jupyter-client:    8.0.3
        - jupyter-core:      5.2.0
        - jupyter-events:    0.6.3
        - jupyter-server:    2.3.0
        - jupyter-server-fileid: 0.7.0
        - jupyter-server-terminals: 0.4.4
        - jupyter-server-ydoc: 0.6.1
        - jupyter-telemetry: 0.1.0
        - jupyter-ydoc:      0.2.2
        - jupyterhub:        3.1.1
        - jupyterlab:        3.6.1
        - jupyterlab-pygments: 0.2.2
        - jupyterlab-server: 2.19.0
        - lightning-utilities: 0.7.0
        - mako:              1.2.4
        - markupsafe:        2.1.2
        - matplotlib-inline: 0.1.6
        - mistune:           2.0.5
        - multidict:         6.0.4
        - multiprocess:      0.70.14
        - nbclassic:         0.5.2
        - nbclient:          0.7.2
        - nbconvert:         7.2.9
        - nbformat:          5.7.3
        - nest-asyncio:      1.5.6
        - notebook:          6.5.2
        - notebook-shim:     0.2.2
        - numpy:             1.24.2
        - oauthlib:          3.2.2
        - packaging:         23.0
        - pamela:            1.0.0
        - pandas:            1.5.3
        - pandocfilters:     1.5.0
        - parso:             0.8.3
        - pexpect:           4.8.0
        - pickleshare:       0.7.5
        - pillow:            9.4.0
        - pip:               23.0.1
        - platformdirs:      3.0.0
        - prometheus-client: 0.16.0
        - prompt-toolkit:    3.0.36
        - protobuf:          3.20.3
        - psutil:            5.9.4
        - ptyprocess:        0.7.0
        - pure-eval:         0.2.2
        - pyarrow:           11.0.0
        - pycparser:         2.21
        - pygments:          2.14.0
        - pyopenssl:         23.0.0
        - pyrsistent:        0.19.3
        - python-dateutil:   2.8.2
        - python-json-logger: 2.0.6
        - pytorch-lightning: 1.9.2
        - pytz:              2022.7.1
        - pyyaml:            6.0
        - pyzmq:             25.0.0
        - regex:             2022.10.31
        - requests:          2.28.2
        - responses:         0.18.0
        - rfc3339-validator: 0.1.4
        - rfc3986-validator: 0.1.1
        - ruamel.yaml:       0.17.21
        - ruamel.yaml.clib:  0.2.7
        - send2trash:        1.8.0
        - setuptools:        67.3.2
        - six:               1.16.0
        - sniffio:           1.3.0
        - soupsieve:         2.4
        - sqlalchemy:        2.0.4
        - stack-data:        0.6.2
        - tensorboardx:      2.6
        - terminado:         0.17.1
        - tinycss2:          1.2.1
        - tokenizers:        0.13.2
        - tomli:             2.0.1
        - torch:             1.13.1+cu116
        - torchaudio:        0.13.1+cu116
        - torchmetrics:      0.11.1
        - torchvision:       0.14.1+cu116
        - tornado:           6.2
        - tqdm:              4.64.1
        - traitlets:         5.9.0
        - transformers:      4.26.1
        - typing-extensions: 4.5.0
        - uri-template:      1.2.0
        - urllib3:           1.26.14
        - wcwidth:           0.2.6
        - webcolors:         1.12
        - webencodings:      0.5.1
        - websocket-client:  1.5.1
        - wheel:             0.38.4
        - xxhash:            3.2.0
        - y-py:              0.5.9
        - yarl:              1.8.2
        - ypy-websocket:     0.8.2
* System:
        - OS:                Linux
        - architecture:
                - 64bit
                - ELF
        - processor:         x86_64
        - python:            3.10.9
        - version:           #1 SMP Thu Nov 17 16:37:07 EST 2022
More info

No response

cc @awaelchli

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the provided MWE, especially get_trainer, train_dataloader, val_dataloader, and validation_step, and compare DDP runs with validation enabled, disabled, and different validation batch sizes. Check when training losses first diverge after validation and verify the model state before and after validation. Done means the cause is identified and the inconsistent training behavior is reproduced by a regression test or otherwise corrected.

Written by the indexing model from the issue text.

Assessment

Tech stack
huggingface, python, pytorch
Domain
distributed-systems, machine-learning
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.