Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions monai/data/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,28 +686,36 @@ def worker_init_fn(worker_id: int) -> None:
set_rnd(worker_info.dataset, seed=worker_info.seed) # type: ignore[union-attr]


def set_rnd(obj, seed: int) -> int:
def set_rnd(obj, seed: int, _seen: set[int] | None = None) -> int:
"""
Set seed or random state for all randomizable properties of obj.

Args:
obj: object to set seed or random state for.
seed: set the random state with an integer seed.
_seen: internal set of already-visited object ids, used to guard against
infinite recursion on cyclic object graphs (e.g. OmegaConf/Hydra
configs whose child nodes back-reference their parent, see issue #8087).
"""
if _seen is None:
_seen = set()
if isinstance(obj, (tuple, list)): # ZipDataset.data is a list
_seed = seed
for item in obj:
_seed = set_rnd(item, seed=seed)
_seed = set_rnd(item, seed=seed, _seen=_seen)
return seed if _seed == seed else seed + 1 # return a different seed if there are randomizable items
if not hasattr(obj, "__dict__"):
return seed # no attribute
if id(obj) in _seen:
return seed # already visited: avoid infinite recursion on cyclic references
_seen.add(id(obj))
Comment on lines 702 to +711

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Track list and tuple identities before recursion.

Line 702 processes containers before the _seen check. A self-referential list still raises RecursionError.

Add the identity check before walking a list or tuple. Add a regression case where dataset.cfg contains itself.

Proposed fix
     if _seen is None:
         _seen = set()
     if isinstance(obj, (tuple, list)):  # ZipDataset.data is a list
+        if id(obj) in _seen:
+            return seed
+        _seen.add(id(obj))
         _seed = seed
         for item in obj:
             _seed = set_rnd(item, seed=seed, _seen=_seen)
         return seed if _seed == seed else seed + 1  # return a different seed if there are randomizable items
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if isinstance(obj, (tuple, list)): # ZipDataset.data is a list
_seed = seed
for item in obj:
_seed = set_rnd(item, seed=seed)
_seed = set_rnd(item, seed=seed, _seen=_seen)
return seed if _seed == seed else seed + 1 # return a different seed if there are randomizable items
if not hasattr(obj, "__dict__"):
return seed # no attribute
if id(obj) in _seen:
return seed # already visited: avoid infinite recursion on cyclic references
_seen.add(id(obj))
if _seen is None:
_seen = set()
if isinstance(obj, (tuple, list)): # ZipDataset.data is a list
if id(obj) in _seen:
return seed
_seen.add(id(obj))
_seed = seed
for item in obj:
_seed = set_rnd(item, seed=seed, _seen=_seen)
return seed if _seed == seed else seed + 1 # return a different seed if there are randomizable items
if not hasattr(obj, "__dict__"):
return seed # no attribute
if id(obj) in _seen:
return seed # already visited: avoid infinite recursion on cyclic references
_seen.add(id(obj))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@monai/data/utils.py` around lines 702 - 711, Update set_rnd to check and
record the list or tuple identity in _seen before recursively iterating its
items, while preserving the existing seed propagation and return behavior. Add a
regression test covering a dataset whose cfg contains itself, ensuring set_rnd
completes without RecursionError.

if hasattr(obj, "set_random_state"):
obj.set_random_state(seed=seed % MAX_SEED)
return seed + 1 # a different seed for the next component
for key in obj.__dict__:
if key.startswith("__"): # skip the private methods
continue
seed = set_rnd(obj.__dict__[key], seed=seed)
seed = set_rnd(obj.__dict__[key], seed=seed, _seen=_seen)
return seed


Expand Down
32 changes: 32 additions & 0 deletions tests/data/test_dataloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from __future__ import annotations

import sys
import types
import unittest

import numpy as np
Expand Down Expand Up @@ -99,5 +100,36 @@ def test_zipdataset(self):
assert_allclose(np.stack(output).flatten()[:7], np.array([594, 170, 594, 170, 594, 170, 524]))


class _CyclicConfigDataset(torch.utils.data.Dataset):
"""
Dataset holding an attribute whose object graph contains a reference cycle.

This mirrors OmegaConf/Hydra configs, whose child nodes hold a back-reference
to their parent node. Seeding such a dataset used to recurse forever in
``monai.data.utils.set_rnd`` (see issue #8087).
"""

def __init__(self):
parent = types.SimpleNamespace()
child = types.SimpleNamespace()
parent.child = child
child.parent = parent # reference cycle, as in an OmegaConf parent/child graph
self.cfg = parent

def __len__(self):
return 4

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


class TestLoaderRecursion(unittest.TestCase):
def test_cyclic_reference_no_recursion(self):
# Constructing the loader seeds the dataset (num_workers=0). A reference cycle in the
# dataset's attributes must not raise RecursionError while walking the object graph.
dataloader = DataLoader(_CyclicConfigDataset(), batch_size=1, num_workers=0, shuffle=False)
self.assertEqual(len(list(dataloader)), 4)


if __name__ == "__main__":
unittest.main()
Loading