Skip to content

Fix RecursionError in set_rnd on cyclic object graphs (#8087) - #9056

Open
ousamabenyounes wants to merge 1 commit into
Project-MONAI:devfrom
ousamabenyounes:fix/issue-8087
Open

Fix RecursionError in set_rnd on cyclic object graphs (#8087)#9056
ousamabenyounes wants to merge 1 commit into
Project-MONAI:devfrom
ousamabenyounes:fix/issue-8087

Conversation

@ousamabenyounes

Copy link
Copy Markdown

Description

monai.data.utils.set_rnd seeds all randomizable properties of an object by
recursively walking obj.__dict__. When a dataset holds a config object whose
graph contains a reference cycle — e.g. an OmegaConf/Hydra config, whose child
nodes back-reference their parent node — the recursion never terminates and
raises RecursionError while constructing a DataLoader with num_workers=0:

RecursionError: maximum recursion depth exceeded in comparison
    full_key: set_random_state
    object_type=<DataclassName>

This reproduces the Hydra DataLoader recursion reported in the issue: the
DataLoader.__init__ seeding path calls set_rnd(dataset, ...), and the
dataset's config attribute forms a parent/child cycle.

Fix

Track the ids of already-visited objects in an internal _seen set and skip any
object already seen, breaking cyclic references while still seeding every
reachable randomizable component exactly once. Non-cyclic graphs and existing
seeding semantics are unchanged (shared randomizable objects are now seeded once
per call, which is the correct deterministic behavior).

Types of changes

  • Non-breaking change (fix or new feature that would not break existing functionality).
  • New tests added to cover the changes.

Fixes #8087

Test verification (RED → GREEN)

New regression test TestLoaderRecursion.test_cyclic_reference_no_recursion
constructs a DataLoader over a dataset whose attribute graph has a reference
cycle.

RED — on dev before the fix:

monai/data/dataloader.py:87: in __init__
    set_rnd(dataset, int(_seed))
monai/data/utils.py: in set_rnd
    seed = set_rnd(obj.__dict__[key], seed=seed)
E   RecursionError: maximum recursion depth exceeded
FAILED tests/data/test_dataloader.py::TestLoaderRecursion::test_cyclic_reference_no_recursion
1 failed, 6 passed

GREEN — with the fix:

tests/data/test_dataloader.py ....... [100%]
7 passed

…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>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

set_rnd now tracks visited object IDs during recursive traversal of lists, tuples, and object attributes. Recursive calls share the visited set and stop when they encounter an already visited object. A cyclic dataset fixture and regression test verify that a single-worker DataLoader constructs and yields four items without RecursionError.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the fix for recursion errors in set_rnd on cyclic object graphs.
Description check ✅ Passed The description explains the problem, fix, scope, linked issue, and regression test results.
Linked Issues check ✅ Passed The changes address issue #8087 by preventing cyclic traversal errors and preserving random-seeding behavior with regression coverage.
Out of Scope Changes check ✅ Passed The changes are limited to set_rnd cycle detection and a focused DataLoader regression test.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
monai/data/utils.py (1)

689-699: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the return value.

set_rnd returns the next seed, but its docstring has no Returns section.

As per path instructions, “Docstrings should be present for all definition which describe each variable, return value, and raised exception in the appropriate section of the Google-style of docstrings.”

🤖 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 689 - 699, Update the set_rnd docstring to
add a Google-style Returns section documenting that the function returns the
next seed as an integer.

Source: Path instructions

tests/data/test_dataloader.py (1)

112-123: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add docstrings to the added definitions.

__init__, __len__, __getitem__, and test_cyclic_reference_no_recursion lack docstrings.

As per path instructions, “Docstrings should be present for all definition which describe each variable, return value, and raised exception in the appropriate section of the Google-style of docstrings.”

Also applies to: 127-131

🤖 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 `@tests/data/test_dataloader.py` around lines 112 - 123, Add Google-style
docstrings to the added __init__, __len__, __getitem__, and
test_cyclic_reference_no_recursion definitions, documenting their relevant
attributes, arguments, return values, and any exceptions raised; preserve the
existing behavior and test logic.

Source: Path instructions

🤖 Prompt for all review comments with 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.

Inline comments:
In `@monai/data/utils.py`:
- Around line 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.

---

Nitpick comments:
In `@monai/data/utils.py`:
- Around line 689-699: Update the set_rnd docstring to add a Google-style
Returns section documenting that the function returns the next seed as an
integer.

In `@tests/data/test_dataloader.py`:
- Around line 112-123: Add Google-style docstrings to the added __init__,
__len__, __getitem__, and test_cyclic_reference_no_recursion definitions,
documenting their relevant attributes, arguments, return values, and any
exceptions raised; preserve the existing behavior and test logic.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a702579-4303-457b-9a49-7f6a0e95349c

📥 Commits

Reviewing files that changed from the base of the PR and between 87060c4 and cd726d6.

📒 Files selected for processing (2)
  • monai/data/utils.py
  • tests/data/test_dataloader.py

Comment thread monai/data/utils.py
Comment on lines 702 to +711
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))

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.

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.

Recursion Error when setting DataLoader Random Seed with Hydra configuration

1 participant