Skip to content

Commit 6d28a5c

Browse files
Fix RecursionError in set_rnd on cyclic object graphs (#8087)
monai.data.utils.set_rnd recursively walks obj.__dict__ to seed randomizable components. When a dataset holds an OmegaConf/Hydra config (whose child nodes back-reference their parent), or any object graph with a reference cycle, the recursion never terminates and raises RecursionError while building a DataLoader with num_workers=0. Track visited object ids in an internal _seen set and skip already-visited objects, breaking the cycle while still seeding every reachable randomizable component exactly once. Signed-off-by: Ben Younes <2910651+ousamabenyounes@users.noreply.github.com>
1 parent e04a802 commit 6d28a5c

2 files changed

Lines changed: 43 additions & 3 deletions

File tree

monai/data/utils.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -686,28 +686,36 @@ def worker_init_fn(worker_id: int) -> None:
686686
set_rnd(worker_info.dataset, seed=worker_info.seed) # type: ignore[union-attr]
687687

688688

689-
def set_rnd(obj, seed: int) -> int:
689+
def set_rnd(obj, seed: int, _seen: set[int] | None = None) -> int:
690690
"""
691691
Set seed or random state for all randomizable properties of obj.
692692
693693
Args:
694694
obj: object to set seed or random state for.
695695
seed: set the random state with an integer seed.
696+
_seen: internal set of already-visited object ids, used to guard against
697+
infinite recursion on cyclic object graphs (e.g. OmegaConf/Hydra
698+
configs whose child nodes back-reference their parent, see issue #8087).
696699
"""
700+
if _seen is None:
701+
_seen = set()
697702
if isinstance(obj, (tuple, list)): # ZipDataset.data is a list
698703
_seed = seed
699704
for item in obj:
700-
_seed = set_rnd(item, seed=seed)
705+
_seed = set_rnd(item, seed=seed, _seen=_seen)
701706
return seed if _seed == seed else seed + 1 # return a different seed if there are randomizable items
702707
if not hasattr(obj, "__dict__"):
703708
return seed # no attribute
709+
if id(obj) in _seen:
710+
return seed # already visited: avoid infinite recursion on cyclic references
711+
_seen.add(id(obj))
704712
if hasattr(obj, "set_random_state"):
705713
obj.set_random_state(seed=seed % MAX_SEED)
706714
return seed + 1 # a different seed for the next component
707715
for key in obj.__dict__:
708716
if key.startswith("__"): # skip the private methods
709717
continue
710-
seed = set_rnd(obj.__dict__[key], seed=seed)
718+
seed = set_rnd(obj.__dict__[key], seed=seed, _seen=_seen)
711719
return seed
712720

713721

tests/data/test_dataloader.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from __future__ import annotations
1313

1414
import sys
15+
import types
1516
import unittest
1617

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

101102

103+
class _CyclicConfigDataset(torch.utils.data.Dataset):
104+
"""
105+
Dataset holding an attribute whose object graph contains a reference cycle.
106+
107+
This mirrors OmegaConf/Hydra configs, whose child nodes hold a back-reference
108+
to their parent node. Seeding such a dataset used to recurse forever in
109+
``monai.data.utils.set_rnd`` (see issue #8087).
110+
"""
111+
112+
def __init__(self):
113+
parent = types.SimpleNamespace()
114+
child = types.SimpleNamespace()
115+
parent.child = child
116+
child.parent = parent # reference cycle, as in an OmegaConf parent/child graph
117+
self.cfg = parent
118+
119+
def __len__(self):
120+
return 4
121+
122+
def __getitem__(self, index):
123+
return torch.tensor([index])
124+
125+
126+
class TestLoaderRecursion(unittest.TestCase):
127+
def test_cyclic_reference_no_recursion(self):
128+
# Constructing the loader seeds the dataset (num_workers=0). A reference cycle in the
129+
# dataset's attributes must not raise RecursionError while walking the object graph.
130+
dataloader = DataLoader(_CyclicConfigDataset(), batch_size=1, num_workers=0, shuffle=False)
131+
self.assertEqual(len(list(dataloader)), 4)
132+
133+
102134
if __name__ == "__main__":
103135
unittest.main()

0 commit comments

Comments
 (0)