While reviewing mpif I found an error in MPICH's implementation of strided arrays in the Fortran 2008 bindings. MPICH creates a datatype to describe the strided array layout, and this code contains an error. I have asked AI to describe the error, and I attach this report below.
This is for MPICH's commit ab53493dad85ffee0fc95812b250e1c8dacf7982 which is at or near the head of the main branch.
Summary
With MPI_SUBARRAYS_SUPPORTED = .true., passing an array section whose inner
dimension is strided and which has at least one dimension above it silently
transfers the wrong data. On integer :: a(20,3),
call MPI_Sendrecv(a(1:20:2, :), 30, MPI_INTEGER, 0, 0, &
flat, 30, MPI_INTEGER, 0, 0, MPI_COMM_SELF, MPI_STATUS_IGNORE)
delivers 20 of the 30 elements wrong. No error is returned. Every routine whose
cdesc wrapper walks a descriptor is affected — point-to-point, RMA, file I/O,
MPI_Bcast, the fixed sides of the collectives.
Where
src/binding/fortran/use_mpi_f08/wrappers_c/cdesc.c, in
cdesc_create_datatype:
int accum_sm = cdesc->elem_len; /* line 19 */
...
if (extent <= INT_MAX) {
if (cdesc->dim[i].sm == accum_sm) { /* line 59 */
mpi_errno = PMPI_Type_contiguous(extent, types[i], &types[i + 1]);
} else {
mpi_errno = PMPI_Type_create_hvector(extent, 1, cdesc->dim[i].sm,
types[i], &types[i + 1]);
}
} else {
... /* line 66, same test */
}
...
accum_sm = cdesc->dim[i].sm * cdesc->dim[i].extent; /* line 84 */
accum_sm is the span the levels below cover: stride times extent of the
outermost of them. But MPI_Type_contiguous replicates at multiples of the
inner type's extent, and the two are not the same number once any lower
level was built with MPI_Type_create_hvector: an hvector of n blocks at
stride sm over a type of extent e has extent
So dim[i].sm == accum_sm is satisfied — the dimension really is dense against
the span below it — while PMPI_Type_contiguous then places replica i at
i * (accum_sm - (sm - e)). Every replica but the first is short, by a
multiple of the deficit. Both the extent <= INT_MAX branch and the _c
branch have the same test.
For a(1:20:2, :): dimension 0 has sm = 8, accum_sm = elem_len = 4, so it
becomes hvector(10, 1, 8, MPI_INT), of extent 76. accum_sm then becomes
8 * 10 = 80. Dimension 1 has sm = 80, which equals accum_sm, so it becomes
contiguous(3, that) — and columns 2 and 3 land at bytes 76 and 152 instead of
80 and 160, i.e. 1 and 2 elements early.
Verifiable without any Fortran:
MPI_Datatype hv;
MPI_Type_create_hvector(10, 1, 8, MPI_INT, &hv);
MPI_Aint lb, ext;
MPI_Type_get_extent(hv, &lb, &ext); /* 76; the span it covers is 80 */
Measured on the build below:
extent(hvector(10,1,8,MPI_INT)) = 76 span it covers = 80
extent(contiguous(3, that)) = 228 span needed = 240
Reproducer
program mpich_cdesc_bug
use mpi_f08
implicit none
integer, parameter :: n = 20, m = 3
integer :: a(n, m), flat(n / 2 * m)
integer :: i, j, p, nwrong
call MPI_Init()
print '(a,l1)', 'MPI_SUBARRAYS_SUPPORTED = ', MPI_SUBARRAYS_SUPPORTED
do j = 1, m
do i = 1, n
a(i, j) = 100 * j + i
end do
end do
flat = -1
call MPI_Sendrecv(a(1:n:2, :), 30, MPI_INTEGER, 0, 0, &
flat, 30, MPI_INTEGER, 0, 0, &
MPI_COMM_SELF, MPI_STATUS_IGNORE)
nwrong = 0
p = 0
do j = 1, m
do i = 1, n, 2
p = p + 1
if (flat(p) /= a(i, j)) nwrong = nwrong + 1
end do
end do
print '(a,i0,a)', 'wrong elements: ', nwrong, ' of 30'
print '(a)', 'expected:'
print '(10i5)', ((a(i, j), i = 1, n, 2), j = 1, m)
print '(a)', 'received:'
print '(10i5)', flat
call MPI_Finalize()
end program mpich_cdesc_bug
Output, one rank:
MPI_SUBARRAYS_SUPPORTED = T
wrong elements: 20 of 30
expected:
101 103 105 107 109 111 113 115 117 119
201 203 205 207 209 211 213 215 217 219
301 303 305 307 309 311 313 315 317 319
received:
101 103 105 107 109 111 113 115 117 119
120 202 204 206 208 210 212 214 216 218
219 301 303 305 307 309 311 313 315 317
a(20,1) = 120 arrives where a(1,2) = 201 belongs — the 4-byte shift — and
column 3 is 8 bytes early.
The rule is general: any dimension above a strided one. Rank 3 with the stride
in the middle, integer :: t(2,4,3) and the section t(:, 1:4:2, :) (dense
innermost, strided, then dense) gives 8 of 12 wrong:
expected:
111 112 131 132 211 212 231 232 311 312 331 332
received:
111 112 131 132 141 142 221 222 231 232 311 312
A receiving section is corrupted the same way, and the wrong type also writes
outside the section the caller named. Receiving 30 elements into
b(1:20:2, :), with b pre-filled with -7:
section elements wrong: 20 of 30
elements clobbered outside the section: 10 of 30
b(20,1) = 1011 (should still be -7)
The displacements only shrink, so nothing lands past the end of b in this
case — but the even-numbered rows of b, which the section does not name, are
overwritten.
Environment
- MPICH
main at ab53493dad85ffee0fc95812b250e1c8dacf7982, reporting
MPICH Version: 5.1.0a1
./configure --disable-dependency-tracking --disable-doc --enable-cxx=no --enable-fortran --enable-mpi-abi --enable-shared=yes --enable-static=no --with-device=ch3 --with-hwloc
- macOS 26.6 (Darwin 25.6.0), arm64; gfortran 15 (MacPorts), clang for C
MPI_SUBARRAYS_SUPPORTED is .true. in this build
The walker is independent of --enable-mpi-abi; nothing in the affected path
consults it.
Suggested fix
Either would do:
- Take the contiguous branch only while the invariant
extent(types[i]) == accum_sm still holds — that is, only while every level
built so far has been dense. Once one level is an hvector, keep using
hvector for the levels above it: an hvector places its blocks at exact byte
offsets, so it is correct whatever the inner extent is, and it costs one
datatype either way.
- Or resize each hvector level to the span it covers
(MPI_Type_create_resized(..., 0, accum_sm, ...)), restoring the invariant
so that contiguous stays usable above it. This costs an extra datatype per
strided level and gives the type lb/ub markers.
We took (1) in a downstream Fortran binding, seeding the flag by measuring the
base type's extent rather than assuming extent(oldtype) == elem_len (a
resized oldtype would break the innermost level the same way). The one-line
version of the change is to gate line 59 and line 66 on a dense flag and
clear that flag whenever the hvector branch is taken.
Separate observation, not measured
In the same function accum_sm and extent are int (lines 19 and 22) while
cdesc->dim[i].sm and cdesc->dim[i].extent are CFI_index_t. The
extent <= INT_MAX guard at line 58 covers the argument passed to the
constructors but not the accum_sm = cdesc->dim[i].sm * cdesc->dim[i].extent
product at line 84, which is computed in int. A section spanning more than
2 GiB would overflow it and then compare wrongly at line 59. I have not tried
to trigger this.
While reviewing mpif I found an error in MPICH's implementation of strided arrays in the Fortran 2008 bindings. MPICH creates a datatype to describe the strided array layout, and this code contains an error. I have asked AI to describe the error, and I attach this report below.
This is for MPICH's commit
ab53493dad85ffee0fc95812b250e1c8dacf7982which is at or near the head of themainbranch.Summary
With
MPI_SUBARRAYS_SUPPORTED = .true., passing an array section whose innerdimension is strided and which has at least one dimension above it silently
transfers the wrong data. On
integer :: a(20,3),delivers 20 of the 30 elements wrong. No error is returned. Every routine whose
cdescwrapper walks a descriptor is affected — point-to-point, RMA, file I/O,MPI_Bcast, the fixed sides of the collectives.Where
src/binding/fortran/use_mpi_f08/wrappers_c/cdesc.c, incdesc_create_datatype:accum_smis the span the levels below cover: stride times extent of theoutermost of them. But
MPI_Type_contiguousreplicates at multiples of theinner type's extent, and the two are not the same number once any lower
level was built with
MPI_Type_create_hvector: an hvector ofnblocks atstride
smover a type of extentehas extentSo
dim[i].sm == accum_smis satisfied — the dimension really is dense againstthe span below it — while
PMPI_Type_contiguousthen places replica i ati * (accum_sm - (sm - e)). Every replica but the first is short, by amultiple of the deficit. Both the
extent <= INT_MAXbranch and the_cbranch have the same test.
For
a(1:20:2, :): dimension 0 hassm = 8,accum_sm = elem_len = 4, so itbecomes
hvector(10, 1, 8, MPI_INT), of extent 76.accum_smthen becomes8 * 10 = 80. Dimension 1 hassm = 80, which equalsaccum_sm, so it becomescontiguous(3, that)— and columns 2 and 3 land at bytes 76 and 152 instead of80 and 160, i.e. 1 and 2 elements early.
Verifiable without any Fortran:
Measured on the build below:
Reproducer
Output, one rank:
a(20,1) = 120arrives wherea(1,2) = 201belongs — the 4-byte shift — andcolumn 3 is 8 bytes early.
The rule is general: any dimension above a strided one. Rank 3 with the stride
in the middle,
integer :: t(2,4,3)and the sectiont(:, 1:4:2, :)(denseinnermost, strided, then dense) gives 8 of 12 wrong:
A receiving section is corrupted the same way, and the wrong type also writes
outside the section the caller named. Receiving 30 elements into
b(1:20:2, :), withbpre-filled with-7:The displacements only shrink, so nothing lands past the end of
bin thiscase — but the even-numbered rows of
b, which the section does not name, areoverwritten.
Environment
mainatab53493dad85ffee0fc95812b250e1c8dacf7982, reportingMPICH Version: 5.1.0a1./configure --disable-dependency-tracking --disable-doc --enable-cxx=no --enable-fortran --enable-mpi-abi --enable-shared=yes --enable-static=no --with-device=ch3 --with-hwlocMPI_SUBARRAYS_SUPPORTEDis.true.in this buildThe walker is independent of
--enable-mpi-abi; nothing in the affected pathconsults it.
Suggested fix
Either would do:
extent(types[i]) == accum_smstill holds — that is, only while every levelbuilt so far has been dense. Once one level is an hvector, keep using
hvector for the levels above it: an hvector places its blocks at exact byte
offsets, so it is correct whatever the inner extent is, and it costs one
datatype either way.
(
MPI_Type_create_resized(..., 0, accum_sm, ...)), restoring the invariantso that contiguous stays usable above it. This costs an extra datatype per
strided level and gives the type lb/ub markers.
We took (1) in a downstream Fortran binding, seeding the flag by measuring the
base type's extent rather than assuming
extent(oldtype) == elem_len(aresized
oldtypewould break the innermost level the same way). The one-lineversion of the change is to gate line 59 and line 66 on a
denseflag andclear that flag whenever the hvector branch is taken.
Separate observation, not measured
In the same function
accum_smandextentareint(lines 19 and 22) whilecdesc->dim[i].smandcdesc->dim[i].extentareCFI_index_t. Theextent <= INT_MAXguard at line 58 covers the argument passed to theconstructors but not the
accum_sm = cdesc->dim[i].sm * cdesc->dim[i].extentproduct at line 84, which is computed in
int. A section spanning more than2 GiB would overflow it and then compare wrongly at line 59. I have not tried
to trigger this.