compiler: Fix issue #2235 - #2996
Open
georgebisbas wants to merge 1 commit into
Open
Conversation
… TimeFunction Cluster.dspace computes, per Function, an IntervalGroup of access offsets (parts), then unions them into one global Interval per Dimension. A regular (non-save) TimeFunction's modulo/stepping dimension (e.g. t) gets promote()'d onto its parent (time), reusing its offsets unchanged -- e.g. t's [0,1] becomes time[0,1], even though that "+1" is only ever safe because t wraps around its own circular buffer and says nothing about the raw time axis. When a save-mode TimeFunction is also present -- genuinely, exactly defined over time -- the union blindly combines the two, and the harmless promoted offset inflates the save-mode Function's exact bound. That inflated upper bound then flows into Dimension._arg_values, which subtracts it from the save-mode Function's own (already correct) time_M candidate, producing a value one too small. Fix: compute a second union restricted to Functions that natively define the Dimension (i.e. it's genuinely one of their own `.dimensions`, not merely reached via promotion), and use it to override just the upper bound of any Dimension that has such a native definer. The lower bound is left untouched: Dimension._arg_values only ever tightens it for a genuinely negative offset (`min(interval.lower, 0)`), which a promoted contribution satisfies correctly regardless of promotion -- confirmed by testing symmetric treatment first, which broke the existing test_indirection (dspace[time].lower shifted from 0 to 1 there because that scenario's non-native contribution is exactly the one supplying the correct lower bound). No new methods added to Interval/IntervalGroup; the fix is entirely local to Cluster.dspace using only union/indexing/construction already used elsewhere in this file. Adds test_default_timeM_with_saved (tests/test_dimension.py) covering the reported reproducer end to end (default time_M value and apply() correctness). Verified: tests/test_dimension.py + test_operator.py + test_ir.py (429 passed, including test_indirection), test_adjoint.py (74 passed), and a broader sweep across test_dse/test_buffering/test_checkpointing/ test_lower_clusters/test_lower_exprs/test_dle/test_visitors/ test_subdomains/test_interpolation (1004 passed, 2 pre-existing xfailed). 1507 tests passed total, 0 failures.
| # Function is natively -- and exactly -- defined over that same | ||
| # parent Dimension (e.g., a `save`-mode TimeFunction, whose data | ||
| # space along `time` is precisely its own declared shape), unioning | ||
| # in a merely-promoted upper offset incorrectly inflates the |
Contributor
There was a problem hiding this comment.
This line is where this comment starts becoming hard to understand
IMO, this whole comment could be replaced by a strightforward example, such as
# Handle inconsistencies due to nonsaved- and saved-TimeFunctions, if any
# E.g., given `f(t, x, y)` and `fsave(time, x, y)`, ...
# ...
should be a 3~4 lines long comment with an example to stand the change of getting understood. Otherwise, a simple :
# Handle inconsistencies due to nonsaved- and saved-TimeFunctions, if any; see issue #2235
Comment on lines
+432
to
+435
| natives = {f: IntervalGroup([i for i in v if i.dim in f.dimensions], | ||
| relations=v.relations, mode=v.mode) | ||
| for f, v in parts.items()} | ||
| natives = {f: v for f, v in natives.items() if v} |
Contributor
There was a problem hiding this comment.
likely just
key = lambda ...
natives = {f: intervals.project(key) for f, intervals in parts.items()}
| ] | ||
| intervals = IntervalGroup(rebuilt, relations=intervals.relations, | ||
| mode=intervals.mode) | ||
|
|
Contributor
There was a problem hiding this comment.
OK so I got to this point and I have a very simple question; if there are both nonsaved- and saved-TimeFunction, instead of this pretty complicated logic, why don't you simply filter the nonsaved TimeFunctions off parts before constructing the intervals? something along the lines of
from devito.tools import split
....
....
# <comment as above>
functions = {f for f in parts if f.is_TimeFunction}
nonsaved, saved = split(functions, lambda f: f.save is None)
if saved:
parts = {f: v for f, v in parts.items() if f not in nonsaved}
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.
In place of #2933.
The bug (issue #2235)
time_Mcomes out one too small whenever asave-modeTimeFunctionismixed with a regular (modulo-buffered) one.
Root cause
Cluster.dspacebuilds a per-FunctionIntervalGroupof access offsets(
parts), then unions them into one globalIntervalper Dimension:Inspecting
op._dspace.partsfor the reproducer above:u'stime[0,1]isn't native touat all — it'st's (the modulostepping dimension) own
[0,1], relabeled onto its parenttimebyintervals.promote(lambda d: d.is_SubIterator).t's+1is only eversafe because
twraps around a 2-slot circular buffer; it says nothingabout the raw
timeaxis.usave, on the other hand, is genuinely andexactly defined over
time—[0,0]is its real requirement.The union combines the two into
time[0,1]: the harmless promoted offsetinflates the exact
save-mode bound. That1then flows intoDimension._arg_values, which subtracts it fromusave's own (alreadycorrect)
time_Mcandidate:This fix vs. #2933
#2933 computes the (buggy) union first, then loops back over every
(function, interval-group)pair a second time, intersects eachfunction's own interval against the already-polluted global one, and
mutates the global interval's upper bound via a new
Interval.set_upper/IntervalGroup.set_uppermethod introduced for the purpose.This PR instead prevents the bad union from happening in the first place,
with no new API surface on
Interval/IntervalGroupat all — the wholefix is local to
Cluster.dspace:partsinto "native" (dimension is genuinely one of thefunction's own
.dimensions) vs. the rest.usave's truetime[0,0],uncontaminated by
u's promoted[0,1].Intervalkeeping the normal, full-union
lowerbut overridingupperwith thenative-only union's upper.
Why upper-only isn't an arbitrary special case
I tried it symmetric first (native-only for both bounds), which broke the
existing
test_operator.py::TestInternals::test_indirection: dropping anon-native
[0,0]lower contribution shifteddspace[time].lowerfrom 0to 1.
Dimension._arg_valuesis already asymmetric between the two bounds:A promoted SubIterator's lower offset is consumed correctly regardless of
promotion (the
min(x, 0)clamp makes a non-negative lower a no-opeither way); only its upper offset can wrongly shrink an unrelated
native Function's bound. Restricting the fix to upper matches an
asymmetry that already provably exists in the consumer, rather than being
invented for this fix.
Testing
time_M == 4,apply()gives theexpected data).
test_dimension.py::TestBufferedDimension::test_default_timeM_with_saved.tests/test_dimension.py+tests/test_operator.py+tests/test_ir.py:429 passed, 0 failed (includes
test_indirection, which a symmetricversion of the fix broke).
tests/test_adjoint.py: 74 passed, 0 failed.test_dse,test_buffering,test_checkpointing,test_lower_clusters,test_lower_exprs,test_dle,test_visitors,test_subdomains,test_interpolation): 1004 passed, 2 pre-existingxfailed, 0 failed.