Loading...
Searching...
No Matches
mpi.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * PlanewaveMPIExecutionPolicy — MPI implementation of PlanewaveExecutionPolicy.
5 *
6 * Extends MPIExecutionPolicy (which owns the MPI lifecycle and the process
7 * group communicator) with k-point pool collective operations specific to the
8 * planewave representation:
9 * - Pool communicator split (each pool owns a subset of k-points)
10 * - reduce_charge_density: Allreduce density across pools via pool communicator
11 * - reduce_scalar: Allreduce scalars (band energy, smearing entropy) across pools
12 * - local_kpoint_grid: Round-robin k-point distribution to this pool
13 *
14 * This is the only file in planewave/execution/ that includes <mpi.h>.
15 *
16 * Topology mapping: each k-point pool is one band group — the layer-0 process
17 * group doubles as the pool. Pools partition the k-point grid; ranks within a
18 * pool share each diagonalization (band parallelism via G-vector slabs).
19 *
20 * Lifecycle (owned by this object, RAII):
21 * Constructor: MPIExecutionPolicy ctor → MPI_Init (if needed), group comm = world
22 * configure_topology: MPI_Comm_split → group (pool) + inter-pool communicators
23 * Destructor: free inter-pool comm → MPIExecutionPolicy dtor → MPI_Finalize
24 */
25
27#ifdef Q3_WITH_FFTW_MPI
29#endif
31#ifdef Q3_WITH_FFTW_MPI
32#endif
36
37#include <algorithm>
38#include <exception>
39#include <mpi.h>
40#include <stdexcept>
41
42namespace q3 {
43namespace execution {
44
46 public:
47 /**
48 * Initialize MPI (via MPIExecutionPolicy), then split into k-point pools.
49 *
50 * Each process starts with 1 pool (all ranks in one band group) until
51 * configure_topology() is called. Quantum ESPRESSO's initial default is also
52 * a single pool, but note it does not stop there: when the rank count is
53 * large relative to the FFT z-dimension and more than one k-point exists, it
54 * raises the pool count on its own. Q3 never does -- the pool count is
55 * whatever the caller asked for.
56 */
58 number_of_pools_ = 1;
59 }
60
61 /**
62 * Configure the parallel topology from the user-requested pool count.
63 *
64 * All pool topology logic lives here so that callers (main, factory) need
65 * no knowledge of what a "pool" is or how ranks are distributed.
66 *
67 * @param requested_number_of_pools Number of k-point pools requested by the
68 * user. Pass 0 to default to 1 pool (all
69 * ranks share one band group — matching QE's
70 * default band/FFT parallelism behaviour).
71 * @throws std::invalid_argument if world_size is not divisible by the pool count.
72 */
73 void configure_topology(int requested_number_of_pools) {
74 number_of_pools_was_requested_ = (requested_number_of_pools != 0);
75 int number_of_pools = (requested_number_of_pools == 0) ? 1 : requested_number_of_pools;
76
77 if (world_size() % number_of_pools != 0) {
78 throw std::invalid_argument(
79 "Number of MPI ranks (" + std::to_string(world_size()) +
80 ") is not divisible by the requested number of k-point pools (" +
81 std::to_string(number_of_pools) +
82 "). Each pool must have the same number of ranks. "
83 "Choose a number of pools that divides evenly into the rank count.");
84 }
85
87 number_of_pools_ = number_of_pools;
88
89 /**
90 * Create inter-pool communicator: ranks with the same group_rank_index_
91 * work on the same grid slabs across different pools (k-point parallel groups).
92 */
93 if (inter_pool_communicator_ != MPI_COMM_NULL) {
94 MPI_Comm_free(&inter_pool_communicator_);
95 }
96 MPI_Comm_split(
97 MPI_COMM_WORLD, group_rank_index_, world_rank_index(), &inter_pool_communicator_);
98 }
99
100 /**
101 * Reconfigure the parallel topology after initial setup.
102 *
103 * Called by the factory when it needs to clamp the pool count to the number
104 * of available IBZ k-points (e.g., when --pools 4 is requested but only 2
105 * k-points exist). Delegates to configure_topology with the new count.
106 *
107 * @param new_number_of_pools Clamped pool count (must be <= original count).
108 */
109 void reconfigure_topology(int new_number_of_pools) override {
110 configure_topology(new_number_of_pools);
111 }
112
113 /**
114 * Free the inter-pool communicator, unless an exception is in flight.
115 *
116 * MPI_Comm_free is collective, and this destructor runs ahead of
117 * ~MPIExecutionPolicy() on the same object, so it declines for the same
118 * reason and must decline independently: a base class skipping its
119 * teardown does nothing for a derived one that goes first. Open MPI
120 * happens to complete this call without synchronizing, which is why the
121 * hang reproduced in ~MPIExecutionPolicy()'s barrier rather than here, but
122 * the standard makes it collective and an implementation is free to make
123 * it block -- so this cannot be left to rest on which MPI is installed.
124 */
126 if (std::uncaught_exceptions() > 0) {
127 return;
128 }
129 int finalized = 0;
130 MPI_Finalized(&finalized);
131 if (!finalized && inter_pool_communicator_ != MPI_COMM_NULL) {
132 MPI_Comm_free(&inter_pool_communicator_);
133 }
134 }
135
136 // Non-copyable
139
140 /**
141 * ── Collective vector ops: inherited from MPIExecutionPolicy ──────────────
142 * dot_product, add_scaled_vector, norm — reduce over the group communicator
143 */
144
148
149 // ── Pool-level ops ────────────────────────────────────────────────────────
150
151 /**
152 * Allreduce spin-resolved charge density across all k-point pools.
153 *
154 * Uses the inter-pool communicator (grouped by group_rank_index_) so that
155 * each slab of the real-space rho grid is summed across matching band-rank
156 * positions in different pools: rank i of pool 0 reduces with rank i of
157 * pool 1, etc. This correctly accumulates k-point contributions while
158 * keeping the slab decomposition intact.
159 *
160 * When group_size > 1, band ranks within the same pool hold non-overlapping
161 * slab portions of the rho grid at different LOCAL indices. These must NOT
162 * be Allreduced within the pool (that would add incompatible grid points).
163 * Each band rank's slab is already the correct contribution for that
164 * portion of the grid.
165 *
166 * Note: single-pool band parallelism (number_of_pools_==1, group_size_>1)
167 * is not supported by this Allreduce design. It would require MPI_Allgather
168 * to assemble the full density, which requires resizing charge_density_real_
169 * throughout the SCF. That architectural change is deferred.
170 */
171 void reduce_charge_density(std::vector<std::vector<Real>>& charge_density) const override {
172 MPI_Comm comm =
173 (inter_pool_communicator_ != MPI_COMM_NULL) ? inter_pool_communicator_ : MPI_COMM_SELF;
174 /**
175 * A zero-length channel still enters the Allreduce. MPI accepts count 0
176 * and never dereferences the buffer, whereas skipping the call on one
177 * rank would leave its peers blocked forever. Sizes are uniform here by
178 * construction -- rank i of every pool owns the same slab of identical
179 * grids -- but a collective must not depend on that holding.
180 */
181 for (auto& spin_channel : charge_density) {
182 MPI_Allreduce(MPI_IN_PLACE,
183 spin_channel.data(),
184 static_cast<int>(spin_channel.size()),
185 MPI_DOUBLE,
186 MPI_SUM,
187 comm);
188 }
189 }
190
191 /**
192 * Allreduce a scalar k-point sum across all pools.
193 *
194 * Uses the inter-pool communicator so that only one rank per pool
195 * participates, avoiding double-counting when group_size > 1.
196 */
197 void reduce_scalar(Real& value) const override {
198 MPI_Comm comm =
199 (inter_pool_communicator_ != MPI_COMM_NULL) ? inter_pool_communicator_ : MPI_COMM_WORLD;
200 MPI_Allreduce(MPI_IN_PLACE, &value, 1, MPI_DOUBLE, MPI_SUM, comm);
201 }
202
203 /**
204 * Allreduce a k-point-local vector across all pools.
205 *
206 * Uses the inter-pool communicator for the same reason as reduce_scalar.
207 */
208 void reduce_vector(std::vector<Real>& values) const override {
209 // No early return on an empty vector: see reduce_charge_density().
210 MPI_Comm comm =
211 (inter_pool_communicator_ != MPI_COMM_NULL) ? inter_pool_communicator_ : MPI_COMM_WORLD;
212 MPI_Allreduce(MPI_IN_PLACE,
213 values.data(),
214 static_cast<int>(values.size()),
215 MPI_DOUBLE,
216 MPI_SUM,
217 comm);
218 }
219
220 /**
221 * Allreduce an integer scalar across all pools using MPI_MAX.
222 *
223 * Used for summary statistics (e.g. max_plane_waves_per_kpoint) where each
224 * pool holds only its local k-points and the global maximum is required.
225 */
226 void reduce_scalar_max(int& value) const override {
227 MPI_Comm comm =
228 (inter_pool_communicator_ != MPI_COMM_NULL) ? inter_pool_communicator_ : MPI_COMM_WORLD;
229 MPI_Allreduce(MPI_IN_PLACE, &value, 1, MPI_INT, MPI_MAX, comm);
230 }
231
232 /**
233 * Allreduce a Real scalar across all pools using MPI_MAX.
234 *
235 * Used for global eigenvalue range (HOMO) in Fermi energy calculation.
236 */
237 void reduce_scalar_max(Real& value) const override {
238 MPI_Comm comm =
239 (inter_pool_communicator_ != MPI_COMM_NULL) ? inter_pool_communicator_ : MPI_COMM_WORLD;
240 MPI_Allreduce(MPI_IN_PLACE, &value, 1, MPI_DOUBLE, MPI_MAX, comm);
241 }
242
243 /**
244 * Allreduce a Real scalar across all pools using MPI_MIN.
245 *
246 * Used for global eigenvalue range (LUMO) in Fermi energy calculation.
247 */
248 void reduce_scalar_min(Real& value) const override {
249 MPI_Comm comm =
250 (inter_pool_communicator_ != MPI_COMM_NULL) ? inter_pool_communicator_ : MPI_COMM_WORLD;
251 MPI_Allreduce(MPI_IN_PLACE, &value, 1, MPI_DOUBLE, MPI_MIN, comm);
252 }
253
254 /**
255 * Allreduce an integer scalar within the band group using MPI_MIN.
256 *
257 * Uses the band communicator so that ranks within the same pool can
258 * collectively determine the minimum plane-wave count.
259 */
260 void reduce_band_group_scalar_min(int& value) const override {
261 MPI_Allreduce(MPI_IN_PLACE, &value, 1, MPI_INT, MPI_MIN, get_group_communicator());
262 }
263
264 /**
265 * Allreduce a real-space slab integral scalar within this pool.
266 *
267 * Uses the band communicator (within-pool) so that all slab contributions
268 * from ranks in the same pool are combined. No-op when group_size == 1.
269 */
270 void reduce_within_pool_scalar(Real& value) const override {
271 if (group_size_ <= 1)
272 return;
273 MPI_Allreduce(MPI_IN_PLACE, &value, 1, MPI_DOUBLE, MPI_SUM, get_group_communicator());
274 }
275
276 /**
277 * Allreduce a real-space slab integral vector within this pool.
278 *
279 * Uses the band communicator for the same reason as reduce_within_pool_scalar.
280 */
281 void reduce_within_pool_vector(std::vector<Real>& values) const override {
282 /**
283 * group_size_ is identical on every rank of the group, so returning on it
284 * is safe; the vector's local emptiness is not, and is no longer tested.
285 */
286 if (group_size_ <= 1)
287 return;
288 MPI_Allreduce(MPI_IN_PLACE,
289 values.data(),
290 static_cast<int>(values.size()),
291 MPI_DOUBLE,
292 MPI_SUM,
294 }
295
296 /**
297 * Distribute k-points round-robin across pools; return this pool's subset.
298 */
299 KPointsGrid local_kpoint_grid(const KPointsGrid& global_kpoint_grid) const override {
300 parallel::KPointDistributor distributor(
301 global_kpoint_grid, group_index(), number_of_pools_);
302 return distributor.local_kpoint_grid();
303 }
304
305 std::vector<Real>
306 assemble_global_kpoint_state_data(const std::vector<Real>& local_data,
307 int local_number_of_physical_kpoints,
308 int number_of_spin_components,
309 int number_of_values_per_state) const override {
310 if (local_number_of_physical_kpoints < 0 || number_of_spin_components < 1 ||
311 number_of_values_per_state < 1) {
312 throw std::invalid_argument("Global k-point assembly received invalid dimensions.");
313 }
314 const std::size_t expected_local_size =
315 static_cast<std::size_t>(local_number_of_physical_kpoints) * number_of_spin_components *
316 number_of_values_per_state;
317 if (local_data.size() != expected_local_size) {
318 throw std::invalid_argument(
319 "Global k-point assembly data size does not match its dimensions.");
320 }
321
322 int root_local_number_of_physical_kpoints =
323 (group_rank_index_ == 0) ? local_number_of_physical_kpoints : 0;
324 int global_number_of_physical_kpoints = 0;
325 MPI_Allreduce(&root_local_number_of_physical_kpoints,
326 &global_number_of_physical_kpoints,
327 1,
328 MPI_INT,
329 MPI_SUM,
330 MPI_COMM_WORLD);
331
332 std::vector<Real> global_data(static_cast<std::size_t>(global_number_of_physical_kpoints) *
333 number_of_spin_components * number_of_values_per_state,
334 0.0);
335 if (group_rank_index_ == 0) {
336 for (int spin_index = 0; spin_index < number_of_spin_components; ++spin_index) {
337 for (int local_kpoint_index = 0;
338 local_kpoint_index < local_number_of_physical_kpoints;
339 ++local_kpoint_index) {
340 int local_state_index =
341 spin_index * local_number_of_physical_kpoints + local_kpoint_index;
342 int global_state_index = spin_index * global_number_of_physical_kpoints +
343 global_kpoint_index(local_kpoint_index);
344 std::copy_n(local_data.begin() + static_cast<std::size_t>(local_state_index) *
345 number_of_values_per_state,
346 number_of_values_per_state,
347 global_data.begin() + static_cast<std::size_t>(global_state_index) *
348 number_of_values_per_state);
349 }
350 }
351 }
352 if (!global_data.empty()) {
353 MPI_Allreduce(MPI_IN_PLACE,
354 global_data.data(),
355 static_cast<int>(global_data.size()),
356 MPI_DOUBLE,
357 MPI_SUM,
358 MPI_COMM_WORLD);
359 }
360 return global_data;
361 }
362
363 std::unique_ptr<FFTGrid> create_fft_grid() const override {
364 /**
365 * When running with a single rank per band group (np=1), use the serial
366 * FFT grid which supports FFTW threading (OMP_NUM_THREADS). This avoids
367 * MPI FFT overhead and enables hybrid thread parallelism for FFTs.
368 */
369 if (group_size() <= 1) {
370 return std::make_unique<SerialFFTGrid>();
371 }
372 /**
373 * Ranks sharing a pool distribute reciprocal space by stick. The FFTW-MPI
374 * slab grid it replaces split a plane from its mirror, which is what
375 * forced gamma-only off under MPI, and divided unevenly enough to leave
376 * ranks empty at eight. The stick grid needs only serial FFTW plus MPI.
377 */
378 return std::make_unique<FFTGridSticks>(get_group_communicator());
379 }
380
381 // ── Pool topology accessors ───────────────────────────────────────────────
382
383 int pool_index() const {
384 return group_index();
385 }
386 bool number_of_pools_was_requested() const override {
387 return number_of_pools_was_requested_;
388 }
389
390 int number_of_pools() const override {
391 return number_of_pools_;
392 }
393 bool one_process_owns_complete_state() const override {
394 return world_size() == 1;
395 }
396
397 /**
398 * One rank per pool. Decided from the topology, not from group_size():
399 * a pool whose coefficient distribution was switched off reports a group
400 * of one while its ranks still divide the FFT grid.
401 */
402 bool each_process_owns_whole_kpoints() const override {
403 return world_size() == number_of_pools_;
404 }
405
406 /**
407 * Pool p holds global k-points p, p + P, p + 2P, ... -- the round-robin
408 * KPointDistributor deals out in local_kpoint_grid().
409 */
410 int global_kpoint_index(int local_kpoint_index) const override {
411 return group_index() + local_kpoint_index * number_of_pools_;
412 }
413
414 private:
415 int number_of_pools_ = 1;
416 bool number_of_pools_was_requested_ = false;
417 MPI_Comm inter_pool_communicator_ = MPI_COMM_NULL;
418};
419
420} // namespace execution
421} // namespace q3
void configure_group_topology(int number_of_groups)
Definition mpi.hpp:99
MPI_Comm get_group_communicator() const
Definition mpi.hpp:358
int group_size() const override
Definition mpi.hpp:262
PlanewaveMPIExecutionPolicy(const PlanewaveMPIExecutionPolicy &)=delete
void reduce_scalar(Real &value) const override
Definition mpi.hpp:197
bool one_process_owns_complete_state() const override
Definition mpi.hpp:393
KPointsGrid local_kpoint_grid(const KPointsGrid &global_kpoint_grid) const override
Definition mpi.hpp:299
void reduce_vector(std::vector< Real > &values) const override
Definition mpi.hpp:208
void reduce_scalar_max(int &value) const override
Definition mpi.hpp:226
bool number_of_pools_was_requested() const override
Definition mpi.hpp:386
void reduce_within_pool_vector(std::vector< Real > &values) const override
Definition mpi.hpp:281
int number_of_pools() const override
Definition mpi.hpp:390
void reduce_scalar_min(Real &value) const override
Definition mpi.hpp:248
void reconfigure_topology(int new_number_of_pools) override
Definition mpi.hpp:109
bool each_process_owns_whole_kpoints() const override
Definition mpi.hpp:402
std::unique_ptr< FFTGrid > create_fft_grid() const override
Definition mpi.hpp:363
void reduce_within_pool_scalar(Real &value) const override
Definition mpi.hpp:270
void reduce_band_group_scalar_min(int &value) const override
Definition mpi.hpp:260
void reduce_scalar_max(Real &value) const override
Definition mpi.hpp:237
std::vector< Real > assemble_global_kpoint_state_data(const std::vector< Real > &local_data, int local_number_of_physical_kpoints, int number_of_spin_components, int number_of_values_per_state) const override
Definition mpi.hpp:306
PlanewaveMPIExecutionPolicy & operator=(const PlanewaveMPIExecutionPolicy &)=delete
void reduce_charge_density(std::vector< std::vector< Real > > &charge_density) const override
Definition mpi.hpp:171
int global_kpoint_index(int local_kpoint_index) const override
Definition mpi.hpp:410
void configure_topology(int requested_number_of_pools)
Definition mpi.hpp:73
const KPointsGrid & local_kpoint_grid() const
double Real