Fix lru_cache type-conflation in dace.symbolic.simplify#2404
Open
Spielopoly wants to merge 4 commits into
Open
Fix lru_cache type-conflation in dace.symbolic.simplify#2404Spielopoly wants to merge 4 commits into
lru_cache type-conflation in dace.symbolic.simplify#2404Spielopoly wants to merge 4 commits into
Conversation
simplify() is memoized with @lru_cache, which keys entries with hash(). In Python, booleans are integers (hash(True) == hash(1), True == sympy.Integer(1)), so the cache cannot distinguish simplify(True) from simplify(sympy.Integer(1)) — they collapse onto one entry. Whichever is evaluated first wins; the other caller gets a wrong-typed result. This is process-global and order-dependent: any path feeding a plain bool to simplify (e.g. a comparison simplify(s == 1)) poisons the cache so later simplify(Integer(1)) returns BooleanTrue. Downstream this crashes memlet-volume propagation for single-iteration maps (volume == Integer(1)) with "TypeError: Property volume must be a literal or symbolic expression, got BooleanTrue". Fix: Construct the cache with typed=True so arguments of different types get distinct entries even when they hash and compare equal
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a regression test suite and fixes a long-standing functools.lru_cache key-collision issue in dace.symbolic.simplify by using typed caching, preventing bool/sympy.Integer cache poisoning.
Changes:
- Switch
dace.symbolic.simplifyto@lru_cache(..., typed=True)to separate cache entries by argument type. - Add regression tests covering
True/1andFalse/0cache collisions. - Add an end-to-end repro test ensuring memlet volume propagation is not broken by prior boolean simplification.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
dace/symbolic.py |
Enables typed LRU caching for simplify to prevent bool/sympy.Integer conflation. |
tests/simplify_cache_typed_test.py |
Adds focused regression + end-to-end tests to prevent cache poisoning regressions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+58
to
+61
| result = dace.symbolic.simplify(True) | ||
| assert isinstance(result, Boolean), \ | ||
| f"expected a sympy boolean, got {type(result).__name__}: {result!r}" | ||
| assert bool(result) is True |
Comment on lines
+81
to
+84
| result = dace.symbolic.simplify(False) | ||
| assert isinstance(result, Boolean), \ | ||
| f"expected a sympy boolean, got {type(result).__name__}: {result!r}" | ||
| assert bool(result) is False |
Comment on lines
+46
to
+50
| result = dace.symbolic.simplify(sympy.Integer(1)) | ||
| assert isinstance(result, sympy.Integer), \ | ||
| f"expected sympy.Integer, got {type(result).__name__}: {result!r}" | ||
| assert not isinstance(result, Boolean) | ||
| assert result == 1 |
Importing fparser's Fortran2008 before Fortran2003 triggers a Fortran2008 -> Fortran2003 -> label_do_stmt_r816 -> Fortran2008.Loop_Control back-import that fails on Python 3.14 with recent fparser (0.2.3+): `ImportError: cannot import name 'Loop_Control' from partially initialized module 'fparser.two.Fortran2008'`. Import Fortran2003 first to break the cycle, and relax the requirements pin from `fparser==0.1.4` to `fparser>0.1.3` so the fixed newer fparser can be used.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
dace.symbolic.simplifyis memoized withfunctools.lru_cache.lru_cachekeys its entries by
hash/==, and in Python booleans are integers:As a result the cache cannot tell
simplify(True)apart fromsimplify(sympy.Integer(1))— they collapse onto a single entry. Whichever isevaluated first wins, and the other caller gets the wrong-typed result:
The same conflation applies to
False/Integer(0).Impact
The corruption is process-global and order dependent: any code path that
feeds a plain Python
booltosimplify(for example a comparison likesimplify(s == 1), which evaluates toTrue/Falsebefore reachingsimplify) poisons the cache for every latersimplify(sympy.Integer(1))/simplify(sympy.Integer(0)).Downstream, this surfaces as a hard crash in memlet-volume propagation. For a
single-iteration map the propagated volume is
sympy.Integer(1), sopropagate_subset(dace/sdfg/propagation.py) callssimplify(Integer(1)),gets back
BooleanTrue, and theMemlet.volumesetter rejects it:Because it depends on evaluation order across the whole process, the failure is
intermittent and hard to attribute — an unrelated SDFG breaks long after the
boolwas simplified.Fix
Construct the cache with
typed=Trueso arguments of different types are cachedunder distinct entries, even when they hash and compare equal:
With
typed=True,simplify(True)(abool) andsimplify(sympy.Integer(1))(a
sympy.Integer) occupy separate cache slots, so neither can return theother's result.
Testing
New regression test:
tests/simplify_cache_typed_test.py. It covers:simplify(True)thensimplify(Integer(1))(and the reverse) stay correctlytyped — both directions, for
True/1andFalse/0.(
cache_info().hits).boolis passed tosimplify, memlet-volume propagationover a single-iteration map still succeeds (this reproduced the original
TypeErrorverbatim before the fix).