Loading...
Searching...
No Matches
mpi_datatype.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * How Q3's Complex buffers cross MPI, named in one place because the wrong
5 * choice fails on some MPI builds and not others.
6 *
7 * Two separate hazards live here, and they have different answers.
8 *
9 * Moving complex data (Bcast, Allgather, Alltoall) needs a datatype whose
10 * handle the library recognizes. Complex is std::complex<double>, and
11 * MPI_CXX_DOUBLE_COMPLEX is what the MPI standard defines for it. The
12 * similar-looking MPI_DOUBLE_COMPLEX is Fortran's DOUBLE COMPLEX, which a
13 * library built without Fortran never initializes: it arrives as
14 * MPI_DATATYPE_NULL and the job aborts. Q3 passed that Fortran datatype from
15 * C++ everywhere until 2026-09-11 (camsai/q3#11). The rule that fixed it
16 * stands: nothing in the C++ tree may name a Fortran datatype.
17 *
18 * Summing complex data is the harder one, and it is why reductions below do
19 * not use a complex datatype at all (camsai/q3#17). Under `podman-hpc --mpi`
20 * and Shifter's `--module=mpich` the host's MPICH replaces the container's at
21 * run time, and the host library rejected MPI_CXX_DOUBLE_COMPLEX for MPI_SUM:
22 *
23 * MPIR_SUM_check_dtype(83): MPI_Op MPI_SUM operation not defined
24 * for this datatype
25 *
26 * while the same image running its own MPICH summed it correctly. What is
27 * established about that, and what is not:
28 *
29 * - MPICH classifies MPI_CXX_DOUBLE_COMPLEX under COMPLEX_EXTRA, commented
30 * in its own source as "extra types that are not required to be supported
31 * by the MPI Standard", and gates it on HAVE_CXX_COMPLEX -- a configure
32 * result, so whether MPI_SUM accepts it is a property of how the library
33 * was built. MPI_DOUBLE is in no such group.
34 * - It is NOT a handle-numbering difference, though that is the intuitive
35 * explanation. The values are fixed per MPICH release rather than derived
36 * from what a build enables: configuring 4.2.3 with and without Fortran
37 * yields byte-identical constants, and 3.4.3 -- the generation the Cray
38 * library derives from -- assigns the same 0x4c001035 and the same C
39 * index. Rebuilding the image's MPICH with Fortran, the fix camsai/q3#17
40 * proposes, would therefore have changed nothing.
41 * - Which build option the host library was configured with is not
42 * determinable from here, and does not need to be.
43 *
44 * So the reduction avoids the question rather than betting on an answer.
45 * MPI_DOUBLE is the one datatype observed to survive the substitution -- it is
46 * what the one-rank-per-pool path reduces, which is why that path never
47 * aborted -- and complex addition is componentwise, so summing 2N doubles is
48 * bit-for-bit the same operation as summing N complex numbers. That holds for
49 * any MPI, needs nothing from the library beyond the MPI_DOUBLE/MPI_SUM pair
50 * the standard requires of every implementation, and costs nothing: no extra
51 * copy, no change in message size.
52 */
53
55
56#include <mpi.h>
57
58#include <algorithm>
59
60namespace q3 {
61namespace execution {
62
63/**
64 * The MPI datatype matching Complex on the wire, for moving data -- NOT for
65 * reductions, which must go through sum_complex_in_place(). See the file
66 * comment.
67 */
68inline MPI_Datatype complex_datatype() {
69 static_assert(sizeof(Complex) == 2 * sizeof(double),
70 "Complex must be layout-compatible with MPI_CXX_DOUBLE_COMPLEX");
71 return MPI_CXX_DOUBLE_COMPLEX;
72}
73
74/**
75 * MPI_SUM over a complex buffer, reduced as pairs of doubles.
76 *
77 * The only way complex data may be summed in Q3 -- see the file comment for
78 * why a complex datatype is not used. Reinterpreting the buffer is well
79 * defined: C++ requires an array of N std::complex<double> to be
80 * reinterpretable as an array of 2N doubles, real part first.
81 *
82 * A null `source` reduces `destination` in place; otherwise the two must not
83 * overlap, as for MPI_Allreduce itself.
84 *
85 * Chunked because the element count doubles: a caller may legitimately pass
86 * any int, and 2N would overflow one past 2^30 elements. Each chunk is an
87 * independent reduction, so splitting changes no result.
88 */
89inline int sum_complex(const Complex* source,
90 Complex* destination,
91 int number_of_elements,
92 MPI_Comm communicator) {
93 constexpr int max_complex_per_call = 1 << 30;
94 const auto* source_doubles = reinterpret_cast<const double*>(source);
95 auto* destination_doubles = reinterpret_cast<double*>(destination);
96 for (int offset = 0; offset < number_of_elements;) {
97 const int chunk = std::min(number_of_elements - offset, max_complex_per_call);
98 const void* send_buffer = source == nullptr
99 ? MPI_IN_PLACE
100 : static_cast<const void*>(source_doubles + 2 * offset);
101 const int status = MPI_Allreduce(send_buffer,
102 destination_doubles + 2 * offset,
103 2 * chunk,
104 MPI_DOUBLE,
105 MPI_SUM,
106 communicator);
107 if (status != MPI_SUCCESS)
108 return status;
109 offset += chunk;
110 }
111 return MPI_SUCCESS;
112}
113
114/// In-place complex MPI_SUM -- see sum_complex().
115inline int sum_complex_in_place(Complex* data, int number_of_elements, MPI_Comm communicator) {
116 return sum_complex(nullptr, data, number_of_elements, communicator);
117}
118
119} // namespace execution
120} // namespace q3
MPI_Datatype complex_datatype()
int sum_complex_in_place(Complex *data, int number_of_elements, MPI_Comm communicator)
In-place complex MPI_SUM – see sum_complex().
int sum_complex(const Complex *source, Complex *destination, int number_of_elements, MPI_Comm communicator)
std::complex< double > Complex