MPI: clamp localised SubDimension thickness to the rank's local size - #2993
MPI: clamp localised SubDimension thickness to the rank's local size#2993ggorman wants to merge 2 commits into
Conversation
`Thickness._arg_values` localises a left/right SubDimension thickness with
tkn = glb_to_loc(root, rtkn-1, side)
tkn = tkn + 1
The `+1` is correct when `index_glb_to_loc(offset, side)` returns a relative
local *index*, which it does whenever the offset falls inside the rank. But
when the offset lies beyond the rank -- i.e. the rank sits wholly inside the
boundary region -- the same call saturates and returns `top + 1`, which is a
*count* already. The `+1` is then applied twice and the rank is handed a
thickness one greater than the number of points it owns.
The generated boundary loop consequently runs one point past the rank's
extent. With a 64-point x dimension over 4 ranks (local extent 16) and
thickness 20, rank 0 gets `x_ltkn = 17`; its left taper loop writes
`r0[0..16]` into a temporary allocated for 16, and rank 3's right loop
writes `r1[-1..15]`. The write corrupts heap state rather than faulting
immediately, so the symptom typically surfaces later as an allocator abort
("double free or corruption") at free or finalize -- and on some allocator
layouts does not surface at all, leaving a silently wrong boundary region.
Clamping the localised thickness to the rank's local size fixes the
over-count and is a no-op whenever `glb_to_loc` genuinely returned an index.
The new test asserts the invariant directly -- a localised thickness may
never exceed the rank's extent -- and fails `assert 17 <= 16` without the
clamp on ranks 0 and 3.
|
For context: this had already been anticipated on Slack, and I think this PR closes out that question rather than raising a new one. In #development on 2026-05-27, @FabioLuporini asked "why do we allow thicknesses to be overridden by the user?", adding "I think it potentially exposes us to nasty issues…", and then specifically:
and on where overrides are handled:
That is exactly the function this PR patches. The thread concluded the handling was sound, and it very nearly is — the localisation is correct in the ordinary case. What it misses is the saturating branch of Two things worth drawing out from that exchange: "They cannot be negative" is right, and it corrects our own earlier diagnosis. An internal note of ours had recorded this failure as "the taper loop start goes negative". It never does. The thickness comes out one too large (17 against a local extent of 16), and the loop runs one element past the end. Anyone searching for a negative bound would not have found this. The concern was raised six days before we hit it in production. Our first Happy to add a comment in |
| tkn = tkn+1 if tkn is not None else 0 | ||
| # The `+1` assumes `glb_to_loc` returned a local index. When the | ||
| # offset lies beyond this rank -- i.e. the rank sits wholly inside | ||
| # the boundary region -- it saturates and returns a count already, |
There was a problem hiding this comment.
"and returns a count already"
then it means the conceptual flaw is in glb_to_loc, which btw simply calls into https://github.com/devitocodes/devito/blob/main/devito/data/decomposition.py#L137
that function should always return indices and not counts
so ask the AI to determine where the inconsistency is
Follow-up to review feedback: move the fix to the root, as requested, rather than clamping at the call site. `index_glb_to_loc(offset, side)` had two return conventions. In range it gives a local index; when the offset covers the subdomain entirely it returned `top + 1` (LEFT) or `glb_max - base + 1` (RIGHT). Neither is an index -- both sit one past the end of the valid local range, on every subdomain including the first. The LEFT form also mixes frames, `top` being absolute while the in-range result is relative to `base`, so its overshoot grows with the subdomain's offset instead of staying at one. Both saturating branches now return `top - base`, the subdomain's last local index. Callers convert to a count themselves, so `Thickness._arg_values` needs no clamp and the previous one is removed. `Thickness._arg_values` had the matching inconsistency: the `left`/`right` branch converts `rtkn-1` as an index and adds one, while `middle` passed `rtkn` and used the result directly as a count. That only worked while saturation returned a count; both branches now convert the same way. Without this, a wholly covered rank's middle thickness came back one short. Tests. `test_glb_to_loc_w_side` gains the saturating cases, which nothing covered before -- the reason this survived. `test_glb_to_loc_w_side_is_always_an_index` asserts the invariant over every subdomain, both sides and all offsets. `test_subdimension_middle_thickness_localisation` pins the middle path. Each was confirmed to fail without its fix.
|
You were right, and the inconsistency is a bit worse than either of us put it. Fix moved to the root; the call-site clamp is gone. Where it is
if side == LEFT:
rel_ofs = glb_min + abs_ofs - base
if abs_ofs >= base and abs_ofs <= top:
return rel_ofs # a local index, relative to `base`
elif abs_ofs > top:
return top + 1 # not an index
On a 64-point dimension over 4 ranks (local extent 16), offset 40:
Both saturating branches now return A second inconsistency this exposed
Why nothing caught it
One thing I did not touchThe (Correcting myself on the earlier comment: I described the old return as "a count", and said the discrepancy was invisible on rank 0. Neither is quite right — it is an out-of-range bound on every rank, and rank 0 is off by one too. The clamp happened to paper over both.) |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2993 +/- ##
==========================================
- Coverage 83.54% 83.53% -0.02%
==========================================
Files 257 257
Lines 53922 53965 +43
Branches 4613 4622 +9
==========================================
+ Hits 45047 45077 +30
- Misses 8076 8093 +17
+ Partials 799 795 -4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The bug
Thickness._arg_valueslocalises a left/rightSubDimensionthickness withThe
+1is correct whenindex_glb_to_loc(offset, side)returns a relative local index, which it does whenever the offset falls inside the rank. But when the offset lies beyond the rank — i.e. the rank sits wholly inside the boundary region — the same call saturates and returnstop + 1, which is a count already. The+1is then applied twice and the rank is handed a thickness one greater than the number of points it owns.Symptom
The generated boundary loop runs one point past the rank's extent. On a 64-point
xover 4 ranks (local extent 16) with thickness 20, rank 0 getsx_ltkn = 17:x_ltkn=17x_ltkn=4x_rtkn=4x_rtkn=17x_ltkn=8x_rtkn=8In the generated code the 1-D temporaries are allocated for
x_size=16, so rank 0's left loop writesr0[0..16](one past the end) and rank 3's right loop writesr1[-1..15](one before the start).The write corrupts heap state rather than faulting at the point of the write, so it typically surfaces later as an allocator abort (
double free or corruption) atfree()or at MPI finalize — and on some allocator layouts it does not surface at all, silently leaving a wrong boundary region. We hit both flavours on the same code.The fix
Clamp the localised thickness to the rank's local size. This is a no-op whenever
glb_to_locgenuinely returned an index, so only the saturating case changes.Test
tests/test_mpi.py::TestCodeGeneration::test_subdimension_thickness_localisation(mode=4) asserts the invariant directly — a localised thickness may never exceed the rank's extent. Without the clamp it failsassert 17 <= 16on ranks 0 and 3.Full
tests/test_mpi.py+tests/test_subdomains.pyon the patched tree: 679 passed, 1 xfailed. One test,TestOperatorAdvanced::test_interpolation_at_uforward[1], is deselected because it fails identically at unpatchedmain(pre-existing, unrelated).Provenance
Found via a standalone MPI reproducer for a silently-wrong absorbing boundary in a downstream seismic package. With this clamp the out-of-bounds writes are gone; a separate downstream fix is needed for the profile arithmetic itself, which is not a Devito issue.