Lightning-AI / Lightning-AI/pytorch-lightning

environment variable WORLD_SIZE is incorrectly set to 1 after trainer.fit is done

Open
#20,232 0 comments 1 reaction 0 assignees View on GitHub
bug distributed strategy: ddp trainer ver: 2.4.x
Dominant language
Python
Stars
31.4k
Forks
3.8k
Avg merge
6d 7h
Merged PRs (30d)
6

Description

### Bug description

Hello, so far I was relying on environment variables to figure out rank and world size in my DDP CUDA environment.

However if I run the attached script it seems the WORLD_SIZE variable is somehow unset by the trainer.

Relevant parts of the log (`#` are my comments)

```
# create trainer
LOCAL_RANK= 0 WORLD_SIZE=1 trainer.global_rank=0 trainer.world_size=8 --- Before fit
# dist is initialized
LOCAL_RANK= 7 WORLD_SIZE=8 trainer.global_rank=7 trainer.world_size=8 --- Before fit
LOCAL_RANK= 3 WORLD_SIZE=8 trainer.global_rank=3 trainer.world_size=8 --- Before fit
LOCAL_RANK= 5 WORLD_SIZE=8 trainer.global_rank=5 trainer.world_size=8 --- Before fit
LOCAL_RANK= 6 WORLD_SIZE=8 trainer.global_rank=6 trainer.world_size=8 --- Before fit
LOCAL_RANK= 2 WORLD_SIZE=8 trainer.global_rank=2 trainer.world_size=8 --- Before fit
LOCAL_RANK= 1 WORLD_SIZE=8 trainer.global_rank=1 trainer.world_size=8 --- Before fit
LOCAL_RANK= 4 WORLD_SIZE=8 trainer.global_rank=4 trainer.world_size=8 --- Before fit
Initializing distributed: GLOBAL_RANK: 7, MEMBER: 8/8
Initializing distributed: GLOBAL_RANK: 5, MEMBER: 6/8
Initializing distributed: GLOBAL_RANK: 3, MEMBER: 4/8
Initializing distributed: GLOBAL_RANK: 6, MEMBER: 7/8
Initializing distributed: GLOBAL_RANK: 4, MEMBER: 5/8
Initializing distributed: GLOBAL_RANK: 1, MEMBER: 2/8
Initializing distributed: GLOBAL_RANK: 2, MEMBER: 3/8
----------------------------------------------------------------------------------------------------
distributed_backend=nccl
All distributed processes registered. Starting with 8 processes
----------------------------------------------------------------------------------------------------
# trains for 2 epochs
# afterwards WORLD_SIZE is 1
LOCAL_RANK= 3 WORLD_SIZE=1 trainer.global_rank=3 trainer.world_size=8 --- After fit
LOCAL_RANK= 5 WORLD_SIZE=1 trainer.global_rank=5 trainer.world_size=8 --- After fit
LOCAL_RANK= 4 WORLD_SIZE=1 trainer.global_rank=4 trainer.world_size=8 --- After fit
LOCAL_RANK= 2 WORLD_SIZE=1 trainer.global_rank=2 trainer.world_size=8 --- After fit
LOCAL_RANK= 7 WORLD_SIZE=1 trainer.global_rank=7 trainer.world_size=8 --- After fit
LOCAL_RANK= 0 WORLD_SIZE=1 trainer.global_rank=0 trainer.world_size=8 --- After fit
LOCAL_RANK= 6 WORLD_SIZE=1 trainer.global_rank=6 trainer.world_size=8 --- After fit
LOCAL_RANK= 1 WORLD_SIZE=1 trainer.global_rank=1 trainer.world_size=8 --- After fit
LOCAL_RANK: 3 - CUDA_VISIBLE_DEVICES: [0,1,2,3,4,5,6,7]
LOCAL_RANK: 5 - CUDA_VISIBLE_DEVICES: [0,1,2,3,4,5,6,7]
LOCAL_RANK: 7 - CUDA_VISIBLE_DEVICES: [0,1,2,3,4,5,6,7]
LOCAL_RANK: 6 - CUDA_VISIBLE_DEVICES: [0,1,2,3,4,5,6,7]
LOCAL_RANK: 4 - CUDA_VISIBLE_DEVICES: [0,1,2,3,4,5,6,7]
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1,2,3,4,5,6,7]
LOCAL_RANK: 1 - CUDA_VISIBLE_DEVICES: [0,1,2,3,4,5,6,7]
LOCAL_RANK: 2 - CUDA_VISIBLE_DEVICES: [0,1,2,3,4,5,6,7]
# runs the test
LOCAL_RANK= 7 WORLD_SIZE=1 trainer.global_rank=7 trainer.world_size=8 --- After test
LOCAL_RANK= 4 WORLD_SIZE=1 trainer.global_rank=4 trainer.world_size=8 --- After test
LOCAL_RANK= 2 WORLD_SIZE=1 trainer.global_rank=2 trainer.world_size=8 --- After test
LOCAL_RANK= 0 WORLD_SIZE=1 trainer.global_rank=0 trainer.world_size=8 --- After test
LOCAL_RANK= 5 WORLD_SIZE=1 trainer.global_rank=5 trainer.world_size=8 --- After test
LOCAL_RANK= 3 WORLD_SIZE=1 trainer.global_rank=3 trainer.world_size=8 --- After test
LOCAL_RANK= 1 WORLD_SIZE=1 trainer.global_rank=1 trainer.world_size=8 --- After test
LOCAL_RANK= 6 WORLD_SIZE=1 trainer.global_rank=6 trainer.world_size=8 --- After test
```

I understand that the WORLD_SIZE is 1 at the beginning (dist is not initialized yet).

However I think it's wrong to set the WORLD_SIZE back to 1 after fit has ended.

The advantage of env variables is that they don't depend on passing the refererence to the trainer around. I know that they can be different depending on the job environment (slurm etc.)

If using env variables here is bad practice / not supposed to work, maybe consider updating the documentation to warn users from using them.

Best,

Simon

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

v2.4

### How to reproduce the bug

```python
conda create -n litissue python=3.10 -y
conda activate litissue
pip install lightning
pip install torchvision

run below script on >= 2 CUDA GPUs

```python
import os

import lightning as lit
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
from lightning.pytorch.callbacks import ModelCheckpoint, LearningRateMonitor
from lightning.pytorch.loggers import CSVLogger
from torch.optim.lr_scheduler import OneCycleLR
from torch.utils.data import DataLoader, random_split
from torchmetrics.functional import accuracy
from torchvision.datasets import CIFAR10

def main():
lit.seed_everything(7)
PATH_DATASETS = os.environ.get("PATH_DATASETS", "./temp_data")
BATCH_SIZE = 64
NUM_WORKERS = 4
NUM_GPUS = torch.cuda.device_count()
assert NUM_GPUS > 1, f"Need at least 2 GPUs, got {NUM_GPUS}"

cifar10_normalization = torchvision.transforms.Normalize(
mean=[x / 255.0 for x in [125.3, 123.0, 113.9]],
std=[x / 255.0 for x in [63.0, 62.1, 66.7]],
)

train_transforms = torchvision.transforms.Compose(
[
torchvision.transforms.RandomCrop(32, padding=4),
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor(),
cifar10_normalization,
]
)
test_transforms = torchvision.transforms.Compose(
[
torchvision.transforms.ToTensor(),
cifar10_normalization,
]
)

dataset_train = CIFAR10(PATH_DATASETS, train=True, download=True, transform=train_transforms)
dataset_val = CIFAR10(PATH_DATASETS, train=True, download=True, transform=test_transforms)
dataset_train = split_dataset(dataset_train)
dataset_val = split_dataset(dataset_val, train=False)
dataset_test = CIFAR10(PATH_DATASETS, train=False, download=True, transform=test_transforms)

train_dataloader = DataLoader(
dataset_train, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS
)
val_dataloader = DataLoader(
dataset_val, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS
)
test_dataloader = DataLoader(
dataset_test, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS
)

model = LitResnet(lr=0.05, batch_size=BATCH_SIZE)

trainer = lit.Trainer(
max_epochs=2,
accelerator="cuda",
devices=NUM_GPUS,
logger=CSVLogger(save_dir="logs/"),
callbacks=[
LearningRateMonitor(logging_interval="step"),
ModelCheckpoint(
dirpath="temp_checkpoints",
filename="{epoch}-{step}",
save_last=True,
every_n_epochs=1,
enable_version_counter=False,
),
],
)

print_with_rank(trainer, f"Before fit")
trainer.fit(model, train_dataloaders=train_dataloader, val_dataloaders=val_dataloader)
print_with_rank(trainer, f"After fit")
trainer.test(model, dataloaders=test_dataloader)
print_with_rank(trainer, f"After test")

def split_dataset(dataset, val_split=0.2, train=True):
"""Splits the dataset into train and validation set."""
len_dataset = len(dataset)
splits = get_splits(len_dataset, val_split)
dataset_train, dataset_val = random_split(
dataset, splits, generator=torch.Generator().manual_seed(42)
)

if train:
return dataset_train
return dataset_val

def get_splits(len_dataset, val_split):
"""Computes split lengths for train and validation set."""
if isinstance(val_split, int):
train_len = len_dataset - val_split
splits = [train_len, val_split]
elif isinstance(val_split, float):
val_len = int(val_split * len_dataset)
train_len = len_dataset - val_len
splits = [train_len, val_len]
else:
raise ValueError(f"Unsupported type {type(val_split)}")

return splits

def create_model():
model = torchvision.models.resnet18(pretrained=False, num_classes=10)
model.conv1 = nn.Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
model.maxpool = nn.Identity()
return model

class LitResnet(lit.LightningModule):
def __init__(self, lr=0.05, batch_size=64):
super().__init__()
self.save_hyperparameters()
self.model = create_model()
self.batch_size = batch_size

def setup(self, stage):
print_with_rank(self.trainer, "Model setup {stage=}")

def forward(self, x):
out = self.model(x)
return F.log_softmax(out, dim=1)

def training_step(self, batch, batch_idx):
x, y = batch
logits = self(x)
loss = F.nll_loss(logits, y)
self.log("train_loss", loss)
return loss

def evaluate(self, batch, stage=None):
x, y = batch
logits = self(x)
loss = F.nll_loss(logits, y)
preds = torch.argmax(logits, dim=1)
acc = accuracy(preds, y, task="multiclass", num_classes=10)

if stage:
self.log(f"{stage}_loss", loss, prog_bar=True)
self.log(f"{stage}_acc", acc, prog_bar=True)

def validation_step(self, batch, batch_idx):
self.evaluate(batch, "val")

def test_step(self, batch, batch_idx):
self.evaluate(batch, "test")

def configure_optimizers(self):
optimizer = torch.optim.SGD(
self.parameters(),
lr=self.hparams.lr,
momentum=0.9,
weight_decay=5e-4,
)
steps_per_epoch = 45000 // self.batch_size
scheduler_dict = {
"scheduler": OneCycleLR(
optimizer,
0.1,
epochs=self.trainer.max_epochs,
steps_per_epoch=steps_per_epoch,
),
"interval": "step",
}
return {"optimizer": optimizer, "lr_scheduler": scheduler_dict}

def get_rank() -> int:
if "RANK" in os.environ:
rank = int(os.environ["RANK"])
else:
rank = int(os.environ.get("LOCAL_RANK", 0))
return rank

def get_world_size() -> int:
return int(os.environ.get("WORLD_SIZE", 1))

def print_with_rank(trainer, *args, **kwargs):
rank = get_rank()
world_size = get_world_size()
print(
f"LOCAL_RANK={rank:>2d} WORLD_SIZE={world_size} "
f"trainer.global_rank={trainer.global_rank} trainer.world_size={trainer.world_size} ---",
*args,
**kwargs,
)

if __name__ == "__main__":
main()
```
```

### Error messages and logs

see post

### Environment


Current environment

* CUDA:
- GPU:
- NVIDIA GeForce RTX 3090
- NVIDIA GeForce RTX 3090
- NVIDIA GeForce RTX 3090
- NVIDIA GeForce RTX 3090
- NVIDIA GeForce RTX 3090
- NVIDIA GeForce RTX 3090
- NVIDIA GeForce RTX 3090
- NVIDIA GeForce RTX 3090
- available: True
- version: 12.1
* Lightning:
- lightning: 2.4.0
- lightning-utilities: 0.11.6
- pytorch-lightning: 2.4.0
- torch: 2.4.0
- torchmetrics: 1.4.1
- torchvision: 0.19.0
* Packages:
- aiohappyeyeballs: 2.4.0
- aiohttp: 3.10.5
- aiosignal: 1.3.1
- async-timeout: 4.0.3
- attrs: 24.2.0
- autocommand: 2.2.2
- backports.tarfile: 1.2.0
- filelock: 3.15.4
- frozenlist: 1.4.1
- fsspec: 2024.6.1
- idna: 3.8
- importlib-metadata: 8.0.0
- importlib-resources: 6.4.0
- inflect: 7.3.1
- jaraco.context: 5.3.0
- jaraco.functools: 4.0.1
- jaraco.text: 3.12.1
- jinja2: 3.1.4
- lightning: 2.4.0
- lightning-utilities: 0.11.6 [0/8069]
- markupsafe: 2.1.5
- more-itertools: 10.3.0
- mpmath: 1.3.0
- multidict: 6.0.5
- networkx: 3.3
- numpy: 2.1.0
- nvidia-cublas-cu12: 12.1.3.1
- nvidia-cuda-cupti-cu12: 12.1.105
- nvidia-cuda-nvrtc-cu12: 12.1.105
- nvidia-cuda-runtime-cu12: 12.1.105
- nvidia-cudnn-cu12: 9.1.0.70
- nvidia-cufft-cu12: 11.0.2.54
- nvidia-curand-cu12: 10.3.2.106
- nvidia-cusolver-cu12: 11.4.5.107
- nvidia-cusparse-cu12: 12.1.0.106
- nvidia-nccl-cu12: 2.20.5
- nvidia-nvjitlink-cu12: 12.6.20
- nvidia-nvtx-cu12: 12.1.105
- ordered-set: 4.1.0
- packaging: 24.1
- pillow: 10.4.0
- pip: 24.2
- platformdirs: 4.2.2
- pytorch-lightning: 2.4.0
- pyyaml: 6.0.2
- setuptools: 72.1.0
- sympy: 1.13.2
- tomli: 2.0.1
- torch: 2.4.0
- torchmetrics: 1.4.1
- torchvision: 0.19.0
- tqdm: 4.66.5
- triton: 3.0.0
- typeguard: 4.3.0
- typing-extensions: 4.12.2
- wheel: 0.43.0
- yarl: 1.9.4
- zipp: 3.19.2
* System:
- OS: Linux
- architecture:
- 64bit
- ELF
- processor: x86_64
- python: 3.10.14
- release: 6.5.0-41-generic
- version: #41~22.04.2-Ubuntu SMP PREEMPT_DYNAMIC Mon Jun 3 11:32:55 UTC 2

### More info

_No response_

cc @lantiga @justusschock @borda

Contributor guide

Open the contributing guide

Research direction

Start by running the attached reproduction with multiple CUDA GPUs and compare the environment values before and after trainer.fit and trainer.test. Trace the trainer lifecycle and distributed environment handling around those entry points, then verify that WORLD_SIZE remains consistent with trainer.world_size after training without breaking the shown rank behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
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.