Loading...
Searching...
No Matches
mpi.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * MPIExecutionPolicy — MPI implementation of ExecutionPolicy.
5 *
6 * Owns the MPI process lifecycle (MPI_Init / MPI_Finalize) and the
7 * communicator topology (world → process groups). Provides group-collective
8 * vector operations (dot_product, norm) via MPI_Allreduce over the group
9 * communicator.
10 *
11 * This and mpi_datatype.hpp are the only files in layer_0_core/execution/
12 * that include <mpi.h>.
13 *
14 * This layer is representation-agnostic: it knows nothing about bands,
15 * k-points, or any physical basis. What a process group *means* (e.g. a band
16 * group of processes diagonalizing the same k-point) is assigned by higher
17 * layers — see layer_2_quantum_system/execution/init.hpp.
18 *
19 * Topology:
20 * World communicator — all processes
21 * Group communicator — the subset of processes this rank shares
22 * collectives with (defaults to world until
23 * configure_group_topology() is called)
24 */
25
29
30#include <cmath>
31#include <cstdlib>
32#include <cstring>
33#include <exception>
34#ifdef Q3_WITH_FFTW_MPI
35#include <fftw3-mpi.h>
36#endif
37#include <mpi.h>
38#include <stdexcept>
39#include <vector>
40
41namespace q3 {
42namespace execution {
43
44class MPIExecutionPolicy : public virtual ExecutionPolicy {
45 public:
46 /**
47 * Initialize MPI (if not already initialized) and set up communicators.
48 *
49 * Uses MPI_Init(nullptr, nullptr) — valid since MPI-2. If MPI is already
50 * initialized (e.g., by a test harness), initialization is skipped and
51 * MPI_Finalize will not be called on destruction.
52 */
54 int already_initialized = 0;
55 MPI_Initialized(&already_initialized);
56 if (!already_initialized) {
57 MPI_Init(nullptr, nullptr);
58#ifdef Q3_USE_FFTW_THREADS
59 /**
60 * FFTW requires fftw_init_threads before fftw_mpi_init when using
61 * threaded FFT plans. Only initialize when threads are requested
62 * to avoid the ~4% overhead the threading hook adds to all plans.
63 */
64 {
65 const char* omp_env = std::getenv("OMP_NUM_THREADS");
66 if (omp_env && std::atoi(omp_env) > 1) {
67 fftw_init_threads();
68 }
69 }
70#endif
71#ifdef Q3_WITH_FFTW_MPI
72 fftw_mpi_init();
73#endif
74 owns_mpi_lifecycle_ = true;
75 }
76
77 MPI_Comm_rank(MPI_COMM_WORLD, &world_rank_index_);
78 MPI_Comm_size(MPI_COMM_WORLD, &world_size_);
79
80 /**
81 * Default: group communicator is the full world until configure_group_topology()
82 * is called. Future multi-rank group parallelism will split this into process groups.
83 */
84 group_communicator_ = MPI_COMM_WORLD;
85 group_rank_index_ = world_rank_index_;
86 group_size_ = world_size_;
87 group_index_ = 0;
88 }
89
90 /**
91 * Configure the process group topology by splitting the world communicator.
92 *
93 * Processes are divided into 'number_of_groups' equal groups of
94 * 'world_size / number_of_groups' consecutive ranks. Collective vector
95 * operations subsequently reduce over the group communicator.
96 *
97 * @param number_of_groups Number of independent process groups.
98 */
99 void configure_group_topology(int number_of_groups) {
100 if (number_of_groups < 1 || number_of_groups > world_size_) {
101 throw std::invalid_argument("MPIExecutionPolicy: number_of_groups out of range.");
102 }
103
104 // Free old group communicator if it was a split one
105 if (group_communicator_ != MPI_COMM_WORLD && group_communicator_ != MPI_COMM_NULL) {
106 MPI_Comm_free(&group_communicator_);
107 }
108
109 const int group_size = world_size_ / number_of_groups;
110 group_index_ = world_rank_index_ / group_size;
111
112 /**
113 * Split world into groups based on their index.
114 * All ranks in the same group share the same color.
115 */
116 MPI_Comm_split(MPI_COMM_WORLD, group_index_, world_rank_index_, &group_communicator_);
117
118 MPI_Comm_rank(group_communicator_, &group_rank_index_);
119 MPI_Comm_size(group_communicator_, &group_size_);
120 }
121
122 /**
123 * Disable data distribution within the process group.
124 *
125 * Resets group_size to 1 so that all group-collective reductions
126 * (dot_product, reduce_within_group, and the pool-level reductions built
127 * on them) become no-ops. Used by higher layers when their data
128 * distribution degenerates and they fall back to replicated data — see
129 * PlanewaveExecutionPolicy::disable_band_distribution().
130 *
131 * The group communicator and configured group index remain intact. Only
132 * the active reduction size changes, so higher layers retain the correct
133 * k-point-pool identity while working with replicated coefficient data.
134 *
135 * Note what this leaves behind: group_size() becomes 1 while
136 * group_rank_index() keeps its pool-local value, so on ranks other than the
137 * pool root the two disagree about whether "this is a group of one". Code
138 * choosing which rank performs a redundant computation must branch on
139 * group_size() <= 1, not on group_rank_index() == 0.
140 */
144
145 /**
146 * Switch this pool from distributing reciprocal space to distributing bands.
147 *
148 * Records how many ranks share the pool, then disables group distribution so
149 * that every rank keeps whole coefficient vectors and each reduce_within_group
150 * becomes a no-op. The group communicator is deliberately left intact: it is
151 * what the band allgather and the density reduction run over.
152 */
158
159 /**
160 * Free split communicators and finalize MPI (if we initialized it).
161 *
162 * Guards every MPI call with MPI_Finalized() so that this destructor is
163 * safe to invoke even if MPI has already been finalized (e.g., when the
164 * policy object is owned by a unique_ptr that outlives global teardown).
165 *
166 * The barrier before MPI_Finalize() ensures that all ranks have finished
167 * their pending work before any rank finalizes. Without it, on macOS with
168 * --oversubscribe, rank 0 can reach MPI_Finalize() while non-root ranks
169 * are still processing trailing work (e.g., logging), causing SIGSEGV in
170 * OpenMPI's shared-memory cleanup.
171 *
172 * Teardown is skipped entirely while an exception is in flight, because
173 * every step of it is collective -- MPI_Comm_free and MPI_Finalize no less
174 * than the barrier -- and each one only completes once every rank calls it.
175 * A rank unwinding alone has no reason to believe its peers are unwinding
176 * too: they are still inside a physics collective, and will not arrive.
177 * Running teardown anyway is what turns a rank-local error -- an
178 * unreadable file, a failed allocation, anything one rank sees and the
179 * others do not -- into a job that hangs instead of failing, with the
180 * message still unsent because the handler that prints it is never
181 * reached. Doing nothing here leaves MPI live for that handler, which
182 * reports the error and calls MPI_Abort to take the blocked peers down
183 * with it (see main_mpi.cpp). Communicators leak, which costs nothing in a
184 * process that is about to die and cannot be avoided regardless: there is
185 * no non-collective way to release them.
186 *
187 * A collective failure -- every rank throwing the same thing, such as the
188 * shared-pool refusal in the Kohn-Sham factory -- also lands here, on
189 * every rank, and takes the same path. That path is safe for it too: each
190 * rank reports before it aborts.
191 */
193 int finalized = 0;
194 MPI_Finalized(&finalized);
195 if (finalized) {
196 return;
197 }
198 if (std::uncaught_exceptions() > 0) {
199 return;
200 }
201 if (group_communicator_ != MPI_COMM_WORLD && group_communicator_ != MPI_COMM_NULL) {
202 MPI_Comm_free(&group_communicator_);
203 }
204 if (owns_mpi_lifecycle_) {
205 MPI_Barrier(MPI_COMM_WORLD);
206 MPI_Finalize();
207 }
208 }
209
210 // Non-copyable — owns MPI resources
213
214 // ── ExecutionPolicy implementation ────────────────────────────────────────
215
216 /**
217 * Dot product of two (possibly distributed) coefficient vectors.
218 *
219 * With data distribution, each rank holds a slice of the
220 * coefficients. The local dot product is partial and must be summed
221 * across group ranks via MPI_Allreduce.
222 */
223 ComplexValue dot_product(int number_of_elements,
224 const ComplexValue* vector_x,
225 const ComplexValue* vector_y) const override {
226 ComplexValue local_result =
227 serial_policy_.dot_product(number_of_elements, vector_x, vector_y);
228 if (group_size_ > 1) {
229 sum_complex_in_place(&local_result, 1, group_communicator_);
230 }
231 return local_result;
232 }
233
234 /**
235 * Scaled vector addition: purely local, no communication.
236 */
237 void add_scaled_vector(int number_of_elements,
238 ComplexValue scaling_factor,
239 const ComplexValue* vector_x,
240 ComplexValue* vector_y) const override {
241 serial_policy_.add_scaled_vector(number_of_elements, scaling_factor, vector_x, vector_y);
242 }
243
244 /**
245 * Norm of a (possibly distributed) coefficient vector.
246 *
247 * Delegates to dot_product which handles the group reduction.
248 */
249 Real norm(int number_of_elements, const ComplexValue* vector_x) const override {
250 ComplexValue dot = dot_product(number_of_elements, vector_x, vector_x);
251 return std::sqrt(std::real(dot));
252 }
253
254 bool is_root() const override {
255 return world_rank_index_ == 0;
256 }
257
258 int group_rank_index() const override {
259 return group_rank_index_;
260 }
261
262 int group_size() const override {
263 return group_size_;
264 }
265
266 int number_of_processes() const override {
267 return world_size_;
268 }
269
270 int global_number_of_elements(int local_number_of_elements) const override {
271 int total_number_of_elements = local_number_of_elements;
272 if (group_size_ > 1) {
273 MPI_Allreduce(
274 MPI_IN_PLACE, &total_number_of_elements, 1, MPI_INT, MPI_SUM, group_communicator_);
275 }
276 return total_number_of_elements;
277 }
278
279 /**
280 * Allreduce complex data within the process group.
281 *
282 * With data distribution, inner products (e.g. subspace matrices,
283 * orthogonalization overlaps) contract over local elements and
284 * need to be summed across group ranks.
285 */
286 void reduce_within_group(ComplexValue* data, int number_of_elements) const override {
287 if (group_size_ <= 1)
288 return;
289 sum_complex_in_place(data, number_of_elements, group_communicator_);
290 }
291
292 /**
293 * Allreduce real data within the process group.
294 */
295 void reduce_within_group(Real* data, int number_of_elements) const override {
296 if (group_size_ <= 1)
297 return;
298 MPI_Allreduce(
299 MPI_IN_PLACE, data, number_of_elements, MPI_DOUBLE, MPI_SUM, group_communicator_);
300 }
301
302 /**
303 * Reduce group-distributed results across the process group.
304 *
305 * Used specifically by the task-group H|ψ⟩ path where different ranks
306 * compute disjoint contributions to the same array and need to sum them.
307 * Unlike reduce_within_group (which is a no-op for replicated data),
308 * this always performs the Allreduce.
309 */
311 int number_of_elements) const override {
312 if (group_size_ <= 1)
313 return;
314 sum_complex_in_place(data, number_of_elements, group_communicator_);
315 }
316
317 void allgather_within_group(const Real* local_data,
318 int local_count,
319 Real* full_data,
320 int /*total_count*/) const override {
321 if (group_size_ <= 1) {
322 std::memcpy(full_data, local_data, local_count * sizeof(Real));
323 return;
324 }
325 // Gather per-rank counts to compute displacements.
326 std::vector<int> receive_counts(group_size_);
327 MPI_Allgather(
328 &local_count, 1, MPI_INT, receive_counts.data(), 1, MPI_INT, group_communicator_);
329 std::vector<int> displacements(group_size_, 0);
330 for (int rank = 1; rank < group_size_; ++rank) {
331 displacements[rank] = displacements[rank - 1] + receive_counts[rank - 1];
332 }
333 MPI_Allgatherv(local_data,
334 local_count,
335 MPI_DOUBLE,
336 full_data,
337 receive_counts.data(),
338 displacements.data(),
339 MPI_DOUBLE,
341 }
342
343 // ── Process topology accessors ────────────────────────────────────────────
344
345 int world_rank_index() const {
346 return world_rank_index_;
347 }
348 int world_size() const {
349 return world_size_;
350 }
351 int group_index() const {
352 return group_index_;
353 }
354
355 MPI_Comm get_world_communicator() const {
356 return MPI_COMM_WORLD;
357 }
358 MPI_Comm get_group_communicator() const {
359 return group_communicator_;
360 }
361
362 void broadcast_from_group_root(ComplexValue* data, int number_of_elements) const override {
363 if (group_size_ <= 1) {
364 return;
365 }
366 MPI_Bcast(data, number_of_elements, complex_datatype(), 0, group_communicator_);
367 }
368
369 void broadcast_from_group_root(Real* data, int number_of_elements) const override {
370 if (group_size_ <= 1) {
371 return;
372 }
373 MPI_Bcast(data, number_of_elements, MPI_DOUBLE, 0, group_communicator_);
374 }
375
376 int band_distribution_size() const override {
378 }
379
380 int band_distribution_rank_index() const override {
382 }
383
385 const int* counts,
386 const int* displacements) const override {
388 return;
389 /**
390 * Counts arrive in complex elements because that is the unit bands are
391 * partitioned in; the complex datatype makes that the wire unit too, so
392 * there is no doubling here. MPI_IN_PLACE says the local block is
393 * already at its own displacement, which is the contract this method
394 * documents.
395 *
396 * The send arguments are ignored under MPI_IN_PLACE, but they remain
397 * arguments an implementation may check, so they name the same datatype
398 * rather than a null one.
399 */
400 MPI_Allgatherv(MPI_IN_PLACE,
401 0,
403 data,
404 counts,
405 displacements,
408 }
409
410 void broadcast_across_bands(ComplexValue* data, int number_of_elements) const override {
411 if (band_distribution_size_ <= 1 || number_of_elements <= 0)
412 return;
413 MPI_Bcast(data, number_of_elements, complex_datatype(), 0, group_communicator_);
414 }
415
416 void broadcast_across_bands(int* data, int number_of_elements) const override {
417 if (band_distribution_size_ <= 1 || number_of_elements <= 0)
418 return;
419 MPI_Bcast(data, number_of_elements, MPI_INT, 0, group_communicator_);
420 }
421
422 void broadcast_across_bands(Real* data, int number_of_elements) const override {
423 if (band_distribution_size_ <= 1 || number_of_elements <= 0)
424 return;
425 MPI_Bcast(data, number_of_elements, MPI_DOUBLE, 0, group_communicator_);
426 }
427
428 void reduce_across_bands(Real* data, int number_of_elements) const override {
429 if (band_distribution_size_ <= 1 || number_of_elements <= 0)
430 return;
431 MPI_Allreduce(
432 MPI_IN_PLACE, data, number_of_elements, MPI_DOUBLE, MPI_SUM, group_communicator_);
433 }
434
435 protected:
438 MPI_Comm group_communicator_ = MPI_COMM_NULL;
440 int group_size_ = 1;
442
443 private:
444 SerialExecutionPolicy serial_policy_;
445 int world_rank_index_ = 0;
446 int world_size_ = 1;
447 bool owns_mpi_lifecycle_ = false;
448};
449
450} // namespace execution
451} // namespace q3
Real norm(int number_of_elements, const ComplexValue *vector_x) const override
Definition mpi.hpp:249
void broadcast_across_bands(int *data, int number_of_elements) const override
Integer overload, for the counts that decide collective sizes.
Definition mpi.hpp:416
void reduce_group_distributed_result(ComplexValue *data, int number_of_elements) const override
Definition mpi.hpp:310
MPI_Comm get_world_communicator() const
Definition mpi.hpp:355
void configure_group_topology(int number_of_groups)
Definition mpi.hpp:99
int band_distribution_rank_index() const override
Definition mpi.hpp:380
ComplexValue dot_product(int number_of_elements, const ComplexValue *vector_x, const ComplexValue *vector_y) const override
Definition mpi.hpp:223
void allgather_within_group(const Real *local_data, int local_count, Real *full_data, int) const override
Definition mpi.hpp:317
void reduce_within_group(Real *data, int number_of_elements) const override
Definition mpi.hpp:295
int global_number_of_elements(int local_number_of_elements) const override
Definition mpi.hpp:270
void reduce_across_bands(Real *data, int number_of_elements) const override
Definition mpi.hpp:428
int number_of_processes() const override
Definition mpi.hpp:266
MPIExecutionPolicy(const MPIExecutionPolicy &)=delete
void broadcast_from_group_root(ComplexValue *data, int number_of_elements) const override
Definition mpi.hpp:362
bool is_root() const override
Definition mpi.hpp:254
int band_distribution_size() const override
Definition mpi.hpp:376
void broadcast_from_group_root(Real *data, int number_of_elements) const override
Definition mpi.hpp:369
int group_rank_index() const override
Definition mpi.hpp:258
void broadcast_across_bands(Real *data, int number_of_elements) const override
Real overload, for eigenvalues – the numbers convergence decisions are made from.
Definition mpi.hpp:422
void add_scaled_vector(int number_of_elements, ComplexValue scaling_factor, const ComplexValue *vector_x, ComplexValue *vector_y) const override
Definition mpi.hpp:237
MPI_Comm get_group_communicator() const
Definition mpi.hpp:358
MPIExecutionPolicy & operator=(const MPIExecutionPolicy &)=delete
int group_size() const override
Definition mpi.hpp:262
void reduce_within_group(ComplexValue *data, int number_of_elements) const override
Definition mpi.hpp:286
void broadcast_across_bands(ComplexValue *data, int number_of_elements) const override
Definition mpi.hpp:410
void allgather_across_bands(ComplexValue *data, const int *counts, const int *displacements) const override
Definition mpi.hpp:384
ComplexValue dot_product(int number_of_elements, const ComplexValue *vector_x, const ComplexValue *vector_y) const override
Definition serial.hpp:27
void add_scaled_vector(int number_of_elements, ComplexValue scaling_factor, const ComplexValue *vector_x, ComplexValue *vector_y) const override
Definition serial.hpp:36
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().
double Real