Lightning-AI / Lightning-AI/pytorch-lightning

Variable length number of batches is not supported

Open
#18,023 2 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug data handling help wanted ver: 2.0.x
Dominant language
Python
Stars
31.4k
Forks
3.8k
Avg merge
6d 7h
Merged PRs (30d)
6

Description

### Bug description

BatchSamplers that do not always return same number of batches are broken in Pytorch Lightning.

Related: https://github.com/Lightning-AI/lightning/issues/17793.

There are several issues:
- `batch_size` is not required for a PyTorch `Sampler`--Pytorch Lightning is monkey patching this class such that a valid `Sampler` is incompatible with pytorch-lightning
- even though Pytorch Lightning repeatedly calls `len` on the Sampler, this information is disregarded, and the epoch often terminates early
- **if you use DDP** (>= 2 GPUs) and set the number of batches to a number larger than the actual number of batches (e.g. 100), **it causes a deadlock**
- behavior of DDP and single-gpu differs (see output of `batch_size`)

The code below reproduces all of these issues.

### What version are you seeing the problem on?

v2.0

### How to reproduce the bug

```python
import os

import torch, numpy as np
from torch.utils.data import DataLoader, Dataset

from pytorch_lightning import LightningModule, Trainer
from time import sleep

class RandomDataset(Dataset):
def __init__(self, size, length):
self.len = length
self.data = torch.randn(length, size)

def __getitem__(self, index):
return self.data[index]

def __len__(self):
return self.len

class BoringModel(LightningModule):
def __init__(self):
super().__init__()
self.layer = torch.nn.Linear(32, 2)

def forward(self, x):
return self.layer(x)

def training_step(self, batch, batch_idx):
loss = self(batch).sum()
self.log("train_loss", loss)
return {"loss": loss}

def validation_step(self, batch, batch_idx):
loss = self(batch).sum()
self.log("valid_loss", loss)

def test_step(self, batch, batch_idx):
loss = self(batch).sum()
self.log("test_loss", loss)

def configure_optimizers(self):
return torch.optim.SGD(self.layer.parameters(), lr=0.1)

class VarLengthBatchSampler(torch.utils.data.sampler.Sampler):
def __init__(self, data_source, max_batch_size=None, drop_last=True, seed=12340,
batch_size=None):
"""Randomly return batches of length [1,max_batch_size].

Args:
data_source: a dataset
max_batch_size: the maximum batch size. This is required
drop_last: if True, drop the last batch if too small
seed: random seed
"""
super().__init__(data_source)
self.data_len = len(data_source)
self.max_batch_size = max_batch_size
self.seed = seed
# in actual application, need to increment this in the training loop
self.epoch = 0
# self.approx_len = int(np.ceil(self.data_len / (self.max_batch_size // 2)))
self.approx_len = 5 # to dramatically illustrate problem of not updating length
# self.approx_len = 100 # causes deadlock!!
print(f"approx_len: {self.approx_len}")
self.len = self.approx_len # pytorch lightning
self.drop_last = drop_last

def rand_bz(self):
return torch.randint(1,self.max_batch_size,(1,))[0]

def __iter__(self):
g = torch.Generator()
g.manual_seed(self.seed + self.epoch)
indices = torch.randperm(self.data_len, generator=g).tolist()
batch = [] # accumulate idxs, then reset
batches = [] # accumulate batches
bz = self.rand_bz()
# print(f"\n{bz=}")
for idx in indices:
batch.append(idx)
if len(batch) == bz:
batches.append(batch)
bz = self.rand_bz()
# print(f"\nnew {bz=}")
batch = []
if not self.drop_last and len(batch) > 0:
batches.append(batch)

# update length to match random sampling results
self.len = len(batches)
print(f"\nnumber of batches should be: {self.len}")
return iter(batches)

def __len__(self):
print(f"\n__len__: {self.len=}")
return self.len

class PLVarLengthBatchSampler(VarLengthBatchSampler):
def __init__(self, data_source, batch_size=None, max_batch_size=8, drop_last=True, seed=12340):
"""Wrapper for for pytorch lightning compatibility
"""
print(f"batch_size: {batch_size}")
super().__init__(data_source, max_batch_size=max_batch_size, drop_last=drop_last, seed=seed)
self.batch_size = max_batch_size

def run():
train_dset = RandomDataset(32, 64)
bz = 8
train_data = DataLoader(train_dset,
batch_sampler=PLVarLengthBatchSampler(train_dset, max_batch_size=bz))
val_data = DataLoader(RandomDataset(32, 64), batch_size=32)
test_data = DataLoader(RandomDataset(32, 64), batch_size=32)

model = BoringModel()
trainer = Trainer(
# devices=1, # uses first __len__ call for all lengths
devices=2, # uses approx_len for all lengths
default_root_dir=os.getcwd(),
num_sanity_val_steps=0,
max_epochs=5,
enable_model_summary=False
)
trainer.fit(model, train_dataloaders=train_data, val_dataloaders=val_data)
trainer.test(model, dataloaders=test_data)

run()
```

### Error messages and logs

# One GPU
```
batch_size: None
approx_len: 5

number of batches should be: 15

__len__: self.len=15

__len__: self.len=15
Epoch 0: 0%| | 0[/15](https://file+.vscode-resource.vscode-cdn.net/15) [00:00
Current environment

```
#- Lightning Component (e.g. Trainer, LightningModule, LightningApp, LightningWork, LightningFlow):
#- PyTorch Lightning Version (e.g., 1.5.0):
#- Lightning App Version (e.g., 0.5.2):
#- PyTorch Version (e.g., 2.0):
#- Python version (e.g., 3.9):
#- OS (e.g., Linux):
#- CUDA/cuDNN version:
#- GPU models and configuration:
#- How you installed Lightning(`conda`, `pip`, source):
#- Running environment of LightningApp (e.g. local, cloud):
```

### More info

_No response_

cc @justusschock @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 data connector entry point referenced in the report, site-packages/pytorch_lightning/trainer/connectors/data_connector.py, and reproduce the example with one GPU and with devices=2. Trace how Trainer.fit handles the custom batch sampler's changing __len__; done means variable batch counts complete without early termination or a DDP deadlock.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, pytorch
Domain
distributed-systems, machine-learning
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.