Skip to content

fix: shard TorchDataNodeDataLoader via DistributedSampler under DDP#106

Open
felixmaximilian wants to merge 2 commits into
mainfrom
fix/ddp-distributed-sampler
Open

fix: shard TorchDataNodeDataLoader via DistributedSampler under DDP#106
felixmaximilian wants to merge 2 commits into
mainfrom
fix/ddp-distributed-sampler

Conversation

@felixmaximilian

@felixmaximilian felixmaximilian commented Jul 12, 2026

Copy link
Copy Markdown

Problem

TorchDataNodeDataLoader hard-codes RandomSampler and is not a torch.utils.data.DataLoader, so pytorch-lightning cannot inject a DistributedSampler. Under trainer.devices>1, Lightning seeds all ranks identically → every rank draws the same sample sequence. The all-reduced gradient degenerates to a single rank's: N× the compute for an unchanged effective batch — silently.

We hit this in rmind DDP training (yaak-ai/rmind feat/throttle-rebalance-otf): first 2-GPU smoke showed both ranks iterating the identical unsharded 549-batch epoch. An audit of the W&B project found no prior devices>1 runs on this loader, so no historical results are affected — the trap was latent.

Fix

  • Lazy build: sampler + node graph construction is deferred to first iter()/len() — framework integrations (hydra + Lightning) instantiate the loader before trainer.fit initializes the process group, so an init-time is_initialized() check can never shard rank 0. A rebuild guard re-shards if the process group appears after a premature build.
  • DistributedSampler (disjoint shards; world_size × per-rank batch = effective batch) whenever torch.distributed is initialized; bit-identical previous behavior otherwise.
  • set_epoch() exposed for per-epoch reshuffling (Lightning only forwards epochs to plain DataLoaders; rmind ships a small callback calling this).
  • method='process' rejected under distributed training: ParallelMapper workers fork after CUDA init and deadlock (known failure, previously observed single-GPU with the shared-memory TensorDict).

Verification

  • 2-GPU DDP training in rmind on 2× RTX 4090: both ranks log distributed sampler rank={0,1} world_size=2 shard_len=N/2 and train normally.
  • New gloo-backend unit tests: fallback without process group, sharding + set_epoch with one, late-group rebuild guard, process-method rejection. (test_dataloaders[{thread,process}-yaak_dataset] fail identically on clean main in my environment — pre-existing, unrelated.)

🤖 Generated with Claude Code

felixmaximilian and others added 2 commits July 12, 2026 11:47
The loader hard-codes RandomSampler and is not a torch DataLoader, so
pytorch-lightning cannot inject a DistributedSampler. Under devices>1
every rank draws an identical (same-seed) sample sequence: the all-reduced
gradient degenerates to a single rank's — N times the compute for an
unchanged effective batch, silently.

- build the sampler/node graph lazily at first iter()/len(): framework
  integrations construct the loader before the process group exists, so
  the sharding decision must be taken at iteration time (with a rebuild
  guard if the group appears after a premature build)
- use DistributedSampler (disjoint shards; world_size x per-rank batch =
  effective batch) when torch.distributed is initialized; exact previous
  behavior otherwise
- expose set_epoch() for per-epoch reshuffling (callers: see rmind's
  DistributedSamplerEpochSetter callback)
- reject method='process' under distributed training: workers fork after
  CUDA init and deadlock

Verified in rmind 2-GPU training on 2x RTX 4090 (both ranks log disjoint
shard_len = N/2 and train to completion) plus gloo-backend unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The DDP-sharding change carried pyright ignore comments, but CI's
'just check' type-checks with ty, which reads only '# ty: ignore[rule]'.
Add ty pragmas at the torchdata/torch stub limitations (ParallelMapper
kwargs unpack, sampler dataset protocols, prefetch_factor arithmetic,
the deliberate _loader poke in the DDP rebuild test) and at the
pre-existing HydraConfig(target=...) alias sites ty now flags; drop
four ignore comments the call-site fix made unused.

No runtime changes; ruff/format/ty all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@felixmaximilian

Copy link
Copy Markdown
Author

@naidenovaleksei This answers your question why two GPUs were as fast as one wrt iter/s!

@egorchakov

egorchakov commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

iirc TorchDataNodeDataLoader was mostly added for multithreading (vs multiprocessing) -- why not just use vanilla torch.utils.data.DataLoader for your use case?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants