Skip to content

api: fix handling of multiple conditions for buffering - #2850

Open
mloubout wants to merge 10 commits into
mainfrom
multi-cond-again
Open

api: fix handling of multiple conditions for buffering#2850
mloubout wants to merge 10 commits into
mainfrom
multi-cond-again

Conversation

@mloubout

Copy link
Copy Markdown
Contributor

No description provided.

@mloubout mloubout added the API api (symbolics, types, ...) label Feb 16, 2026
@codecov

codecov Bot commented Feb 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.04673% with 64 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.66%. Comparing base (3df12db) to head (0eb1800).

Files with missing lines Patch % Lines
devito/passes/clusters/buffering.py 80.29% 17 Missing and 10 partials ⚠️
devito/passes/clusters/asynchrony.py 75.00% 11 Missing and 3 partials ⚠️
devito/ir/support/guards.py 75.92% 11 Missing and 2 partials ⚠️
devito/ir/stree/algorithms.py 20.00% 3 Missing and 1 partial ⚠️
devito/types/utils.py 20.00% 4 Missing ⚠️
devito/ir/clusters/algorithms.py 81.81% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2850      +/-   ##
==========================================
+ Coverage   83.64%   83.66%   +0.01%     
==========================================
  Files         257      257              
  Lines       54235    54524     +289     
  Branches     4629     4680      +51     
==========================================
+ Hits        45367    45618     +251     
- Misses       8072     8099      +27     
- Partials      796      807      +11     
Flag Coverage Δ
pytest-gpu-aomp-amdgpuX 68.52% <68.66%> (-0.02%) ⬇️
pytest-gpu-gcc- 78.27% <74.29%> (-0.03%) ⬇️
pytest-gpu-icx- 78.21% <74.29%> (-0.02%) ⬇️
pytest-gpu-nvc-nvidiaX 69.18% <68.66%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread devito/symbolics/extended_sympy.py Outdated
return CondNe(*self.args, evaluate=False)

@property
def _as_min(self):

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.

I would drop this and rather have a singledispatch handler for CondEq where necessary

Comment thread devito/types/relational.py
Comment thread devito/ir/equations/equation.py Outdated
expr = uxreplace(expr, {d: IntDiv(index, d.symbolic_factor)})

# Merge conditionals when possible. E.g if we have an implicit_dim
# and there is a dimension with the same parent, we ca merged

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.

Dimension

"ca merged"

"their conditions"

you could also make the example a bit more practical

Comment thread devito/ir/equations/equation.py Outdated
for d in input_expr.implicit_dims:
if d not in conditionals:
continue
for cd in dict(conditionals):

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.

list(...) is fine

Comment thread devito/ir/equations/equation.py Outdated
# Replace the ConditionalDimensions in `expr`
for d, cond in conditionals.items():
# Replace dimension with index
index = d.index

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.

you can spare this line

Comment thread devito/ir/equations/equation.py Outdated
ispace = IterationSpace(intervals, iterators)

# Construct the conditionals and replace the ConditionalDimensions in `expr`
# Construct the conditionals

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.

I think we should place this whole block of code, which constructs/lowers the conditionals, into its own separate functions, and a docstring with some examples

@mloubout
mloubout force-pushed the multi-cond-again branch 4 times, most recently from ef708e5 to b997156 Compare May 22, 2026 15:13
@mloubout
mloubout force-pushed the multi-cond-again branch 5 times, most recently from 7a1a6aa to c7786ea Compare May 28, 2026 17:17
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@mloubout
mloubout force-pushed the multi-cond-again branch 11 times, most recently from f904760 to 0500469 Compare May 29, 2026 16:45
Comment thread devito/ir/equations/equation.py Outdated
shift = relational_shift(cond, d.parent)
expr = uxreplace(expr, {d: IntDiv(index, d.symbolic_factor) + shift})

# Merge conditionals when possible. E.g if we have an implicit_dim

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.

btw this block imho deserves its own function

Comment thread devito/passes/clusters/asynchrony.py Outdated
if d is not dim:
continue

if d in c0.guards and not c0.guards[d].has(Mod):

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.

searching for Mod is a bit meh, I'd rather add a special guard to ir/support/guards.py and look for that instead (there's quite a few already in there!)

Comment thread devito/passes/clusters/asynchrony.py Outdated
_actions_from_update_memcpy(c, d, clusters, actions, sregistry)
elif d.is_Custom and is_integer(c.ispace[d].size):
_actions_from_init(c, d, actions)
_actions_from_init(c, d, clusters, actions)

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.

leftover, I guess

Comment thread devito/passes/clusters/asynchrony.py Outdated


def _actions_from_init(c, d, actions):
def _actions_from_init(c, d, clusters, actions):

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.

leftover, I guess

Comment thread examples/userapi/05_conditional_dimension.ipynb
@mloubout
mloubout force-pushed the multi-cond-again branch from 0500469 to 30790f0 Compare May 30, 2026 14:35
@mloubout
mloubout force-pushed the multi-cond-again branch from 89aea51 to 99f0a9e Compare July 3, 2026 07:41
Comment thread devito/ir/equations/algorithms.py Outdated
if d not in conditionals:
continue
for cd in list(conditionals):
if cd.parent == d.parent and cd is not d:

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.

nitpicking: cd.parent is d.parent ...

Comment thread devito/ir/equations/algorithms.py Outdated
continue
for cd in list(conditionals):
if cd.parent == d.parent and cd is not d:
cond = conditionals.pop(d)

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.

nitpicking: you don't need this line here, you may just .pop in the else branch, and avoid the = cond in the if branch

Comment thread devito/ir/equations/algorithms.py Outdated

# Replace the ConditionalDimensions in `expr`
for d, cond in conditionals.items():
# Replace dimension with index

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.

redundant comment?

Comment thread devito/ir/equations/algorithms.py
Comment thread devito/passes/clusters/asynchrony.py Outdated
g = c0.guards.get(d)
# Explicit compute guards need no pipeline; memcpy clusters
# still need WithLock for the copy-back sync
if g is not None and not wraps_memcpy(c0):

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.

I think a lot of the complexity inherent in this guard and the nested one could be mitigated by simply introducing somehting along the lines of c0.guards.has(d, GuardFactor)

Comment thread devito/passes/clusters/buffering.py Outdated

# Alias deferred entries to an existing f-buffer; create one if none
for f, deferred in extras.items():
reusable = [mapper[(ff, kk)] for (ff, kk) in mapper if ff == f]

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.

ff is f

Comment thread devito/passes/clusters/buffering.py Outdated
# Add all missing buffering dimensions and reorder to
# avoid duplicates with different ordering
ispaces.add(c.ispace.insert(self.dim, edims).reorder())
elif ((self.f in c.scope.reads or self.f in c.scope.writes) and

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.

self.f in c.scope.functions or in fact self.f in c.functions should suffice

Comment thread devito/passes/clusters/buffering.py Outdated

lhs = b.indexify()._subs(v.xd, v.first_idx.b)
rhs = f.indexify()._subs(v.dim, v.first_idx.f)
for b, vb in descriptors.items():

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.

another potential occurrence of descriptors.flatten

Comment thread devito/passes/clusters/buffering.py Outdated
)
else:
size = async_degree
for k, ck in groupby(clusters, key=lambda c: c.guards):

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.

ultra-uber-nitpicking, typically we use letter g for groups

Comment thread devito/passes/clusters/buffering.py Outdated
return p, offset


def _buffer_dim(f, key, ck):

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.

this is potentially a legacy over-complication

is it basically what in PRO refer to as .hdim ?

because imho, if so, we could/should somehow abstract the concept here, and just use f.hdim (or whatever name you wanna give it)

@mloubout
mloubout force-pushed the multi-cond-again branch 6 times, most recently from ab8cd52 to b1147aa Compare July 30, 2026 22:25
Comment thread devito/ir/equations/algorithms.py
Comment thread devito/ir/stree/algorithms.py
@mloubout
mloubout force-pushed the multi-cond-again branch 6 times, most recently from 267a4ed to cfa4221 Compare August 13, 2026 18:34
Comment thread devito/ir/clusters/algorithms.py Outdated
# the purpose of protecting from OOB accesses
cds = [d for d in cds if not d.indirect]
modes = [cd.relation for cd in cds]
strict = ConditionalDimension._STRICT

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.

STRICT instead of _STRICT since it's not a private class attribute

Comment thread devito/passes/clusters/asynchrony.py Outdated
# still need WithLock for the copy-back sync. An "explicit"
# guard is a plain relation on `d` (e.g., `d == K`); subsampling
# guards (GuardFactor) still require the standard async pipeline
if d in c0.guards and not wraps_memcpy(c0) \

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.

ultra nitpick, for homogeneity we typically do:

if d in c0.guards and \
   not wraps_memcpy(c0) and \
   not c0.guards.has(d, GuardFactor):

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.

blank lines surrounding this new block would help also

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok

Comment thread devito/passes/clusters/buffering.py Outdated
descriptors = {b: BufferDescriptor(f, b, bfmap[f])
for f, b in self.mapper.items()
if f in bfmap}
descriptors = {}

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.

ultra-nitpick memo

Comment thread devito/passes/clusters/buffering.py Outdated
properties = c.properties.sequentialize(d)
if not isinstance(d, BufferDimension):
if not isinstance(d, BufferDimension) and \
not _explicit_guard(c.guards.get(d, None), d):

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.

since _explicit_guard is called only here, you can probably move the not isinstance(d, BufferDimension) part of the condition inside of it, thus simplifying this caller site

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok

Comment thread devito/passes/clusters/buffering.py Outdated
index_mapper = {i: mds[(v.xd, i)] for i in v.indices}
for a in accesses:
subs[a.access] = b.indexed[[index_mapper.get(i, i) for i in a]]
subs = defaultdict(dict)

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.

I'd do this, it'd shorten this loop body significantly

Comment thread devito/passes/clusters/buffering.py Outdated

cls = callback or Array
name = sregistry.make_name(prefix=f'{f.name}b')
return cls(name=name, dimensions=dimensions, dtype=f.dtype,

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.

u now might be able to fit this in two lines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok

Comment thread devito/passes/clusters/buffering.py Outdated

# Indirect accessors define the ispace only for a read-only streamed
# buffer, where nothing iterates the buffer's own Dimensions directly
if not ispaces:

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.

this could be glued with the if len(ispaces) > 1:, u could have:

if len(ispaces) == 0:
    # Indirect accessors define the ispace only for a read-only streamed
    # buffer, where nothing iterates the buffer's own Dimensions directly
    ispace = indirect.pop()
elif len(ispaces) == 1:
    # <comment>
    ispace = ispaces.pop()
else:
    # Best effort to make buffering work in the presence of multiple
    # IterationSpaces
    <code>
    ispace = ispaces.pop()

return ispace

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.

in retrospect, do you really need indirect ? are u sure u cannot just reuse the same ispaces ? I guess u know better than me...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

actually can probably just use ispaces yeah, droping indirect

Comment thread devito/passes/clusters/buffering.py Outdated
buf = next((b for b in reusable
if set(k) & set(b.dimensions)), None)
if buf is None and reusable:
buf = reusable[0]

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.

are u expecting len(reusable) == 1 ? if yes, is it worth asserting it? if not, why do we pick [0]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, that was lazy, tweked for better selection

Comment thread devito/passes/clusters/buffering.py Outdated


def _make_buffer(f, dim, k, ck, xds, async_degree, sregistry, callback):
"""Build (or retrieve) the buffer Array for `f` along `dim` under guards `k`."""

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.

then instead of _make_buffer I'd call it differently (pick_buffer ?) and remove the parentheses surrounding "or retrieve" since it's very important , not a minor detail

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok

Comment thread devito/types/utils.py
return self.getters[d]
raise KeyError

def dindex(self, dim):

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.

why not just a def index override? if the arg is an integer you call into super(), otherwise do the logic below

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's only used in one place (for transforms) and it makes the standard index work better as the dict's default one

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API api (symbolics, types, ...) no-pro-trigger Skip the devitopro submodule update on merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants