Loading...
Searching...
No Matches
mpi.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * MPISubspaceDiagonalizer — ScaLAPACK implementation of SubspaceDiagonalizer.
5 *
6 * Solves the generalized eigenvalue problem on a distributed process grid
7 * using pzhegvx. Assumes the input matrices are already globally reduced
8 * (redundant) on all ranks of the communicator, and redistributes them
9 * to a block-cyclic layout for ScaLAPACK.
10 *
11 * This file self-guards with Q3_WITH_MPI so it can be safely included
12 * from the factory without requiring the consumer to add #ifdef guards.
13 */
14
15#ifdef Q3_WITH_MPI
16
17#include "io/logger.hpp"
22#include <algorithm>
23#include <cmath>
24#include <vector>
25
26namespace q3 {
27
28class MPISubspaceDiagonalizer : public SubspaceDiagonalizer {
29 public:
30 explicit MPISubspaceDiagonalizer(const execution::MPIExecutionPolicy& policy)
31 : policy_(policy) {}
32
33 int solve(int basis_size,
34 Complex* hamiltonian_matrix,
35 Complex* overlap_matrix,
36 Real* eigenvalues,
37 Complex* eigenvector_matrix,
38 bool is_gamma_only = false) override {
39 if (basis_size == 0)
40 return 0;
41
42 MPI_Comm comm = policy_.get_group_communicator();
43 if (comm == MPI_COMM_NULL)
44 return -1;
45
46 /**
47 * A replicated group solves locally, never collectively.
48 *
49 * disable_group_distribution() -- the replicated-G fallback, taken when
50 * a rank's FFT slab holds no plane waves -- sets group_size() to 1 but
51 * deliberately leaves the group communicator with all its ranks, so
52 * MPI_Comm_size() below still reports the full group. In that state
53 * every rank holds the whole problem and steps through Davidson on its
54 * own, reaching its own basis size at its own time. Entering a
55 * collective solve from there had each rank calling group-wide BLACS
56 * and MPI_Allreduce independently, with mismatched counts, which
57 * aborted the job in MPI_Allreduce with "Other MPI error" and a
58 * segmentation fault on every rank. It reproduced at any problem size
59 * and only ever with ScaLAPACK enabled -- the same width completes
60 * without it, because that path is already a local solve.
61 *
62 * MPIExecutionPolicy documents this: branch on group_size() <= 1, not
63 * on group_rank_index() == 0.
64 */
65 if (policy_.group_size() <= 1) {
66 SerialSubspaceDiagonalizer replicated_solver;
67 return replicated_solver.solve(basis_size,
68 hamiltonian_matrix,
69 overlap_matrix,
70 eigenvalues,
71 eigenvector_matrix,
72 is_gamma_only);
73 }
74
75#ifdef Q3_WITH_SCALAPACK
76 int rank, size;
77 MPI_Comm_rank(comm, &rank);
78 MPI_Comm_size(comm, &size);
79
80 /**
81 * 1. Set up BLACS grid
82 * We use a square-ish grid for best ScaLAPACK performance.
83 */
84 int nprow = static_cast<int>(std::sqrt(size));
85 while (size % nprow != 0)
86 nprow--;
87 int npcol = size / nprow;
88
89 /**
90 * Stay inside the C BLACS API. This previously took the handle from
91 * Csys2blacs_handle (C) into blacs_gridinit_ (Fortran), which reads its
92 * argument in a different numbering space: the handle came back as 1
93 * and BLACS then used it as a communicator, so MPICH aborted in
94 * MPI_Comm_group with "Invalid communicator, comm=0x1". It also went
95 * through blacs_get_(handle, 0, &ctxt) first, which asks for the
96 * *default* system context -- built on MPI_COMM_WORLD, not on `comm` --
97 * so the grid would have spanned world ranks while nprow*npcol was
98 * sized from `comm`, leaving the surplus ranks outside the grid.
99 *
100 * None of this had ever run: Q3 called pzhegv_, which is not in the
101 * ScaLAPACK API, so no ScaLAPACK-enabled build linked.
102 */
103 int blacs_system_handle = scalapack::Csys2blacs_handle(comm);
104 int blacs_context = blacs_system_handle;
105 scalapack::Cblacs_gridinit(&blacs_context, "R", nprow, npcol);
106 int myrow, mycol;
107 scalapack::Cblacs_gridinfo(blacs_context, &nprow, &npcol, &myrow, &mycol);
108
109 /**
110 * Every rank of `comm` is in the grid, so this is defensive. A rank
111 * outside the grid has no context to exit -- BLACS treats exiting one
112 * as fatal rather than as a no-op -- but the system handle is still
113 * this rank's to release.
114 */
115 if (myrow < 0 || mycol < 0) {
116 scalapack::Cfree_blacs_system_handle(blacs_system_handle);
117 return 0;
118 }
119
120 // 2. Set up descriptors
121 int nb = 32; // Block size
122 int desc[9];
123 int info;
124 int lld = scalapack::numroc_(&basis_size, &nb, &myrow, &zero_, &nprow);
125 lld = std::max(1, lld);
126 scalapack::descinit_(
127 desc, &basis_size, &basis_size, &nb, &nb, &zero_, &zero_, &blacs_context, &lld, &info);
128
129 // 3. Redistribute/Copy redundant data to local ScaLAPACK blocks
130 int local_rows = scalapack::numroc_(&basis_size, &nb, &myrow, &zero_, &nprow);
131 int local_cols = scalapack::numroc_(&basis_size, &nb, &mycol, &zero_, &npcol);
132 std::vector<Complex> local_h(static_cast<size_t>(local_rows) * local_cols);
133 std::vector<Complex> local_s(static_cast<size_t>(local_rows) * local_cols);
134 std::vector<Complex> local_z(static_cast<size_t>(local_rows) * local_cols);
135
136 for (int j_local = 0; j_local < local_cols; ++j_local) {
137 // Map local column index to global column index
138 int j_global = (j_local / nb) * npcol * nb + mycol * nb + (j_local % nb);
139 for (int i_local = 0; i_local < local_rows; ++i_local) {
140 int i_global = (i_local / nb) * nprow * nb + myrow * nb + (i_local % nb);
141 local_h[j_local * local_rows + i_local] =
142 hamiltonian_matrix[j_global * basis_size + i_global];
143 local_s[j_local * local_rows + i_local] =
144 overlap_matrix[j_global * basis_size + i_global];
145 }
146 }
147
148 // 4. Solve
149 int ibtype = 1; // H*x = lambda*S*x
150 char jobz = 'V';
151 char range = 'A'; // every eigenvalue; vl/vu/il/iu are then unused
152 char uplo = 'U';
153 int one = 1;
154
155 /**
156 * PZHEGVX selection and tolerance arguments. abstol <= 0 asks for the
157 * default (ulp * ||A||), which is what the simple driver used to apply
158 * implicitly. orfac is the cluster threshold above which eigenvectors
159 * are reorthogonalized; 1e-3 is ScaLAPACK's own default.
160 */
161 double vl = 0.0;
162 double vu = 0.0;
163 int il = 1;
164 int iu = basis_size;
165 double abstol = 0.0;
166 double orfac = 1.0e-3;
167 int number_of_eigenvalues_found = 0;
168 int number_of_eigenvectors_computed = 0;
169
170 /**
171 * Diagnostics PZHEGV did not have. iclustr and gap are sized by the
172 * process grid rather than the matrix: iclustr holds a start/end index
173 * pair per process and gap one value per process.
174 */
175 std::vector<int> ifail(static_cast<size_t>(std::max(1, basis_size)));
176 std::vector<int> iclustr(static_cast<size_t>(2 * std::max(1, nprow * npcol)));
177 std::vector<double> gap(static_cast<size_t>(std::max(1, nprow * npcol)));
178
179 auto* cast_h = reinterpret_cast<std::complex<double>*>(local_h.data());
180 auto* cast_s = reinterpret_cast<std::complex<double>*>(local_s.data());
181 auto* cast_z = reinterpret_cast<std::complex<double>*>(local_z.data());
182
183 /**
184 * Workspace query: lwork/lrwork/liwork = -1 returns the sizes in the
185 * first element of each workspace array.
186 */
187 Complex work_query;
188 Real rwork_query = 0.0;
189 int iwork_query = 0;
190 int lwork = -1;
191 int lrwork = -1;
192 int liwork = -1;
193 auto* cast_work_query = reinterpret_cast<std::complex<double>*>(&work_query);
194
195 scalapack::pzhegvx_(&ibtype,
196 &jobz,
197 &range,
198 &uplo,
199 &basis_size,
200 cast_h,
201 &one,
202 &one,
203 desc,
204 cast_s,
205 &one,
206 &one,
207 desc,
208 &vl,
209 &vu,
210 &il,
211 &iu,
212 &abstol,
213 &number_of_eigenvalues_found,
214 &number_of_eigenvectors_computed,
215 eigenvalues,
216 &orfac,
217 cast_z,
218 &one,
219 &one,
220 desc,
221 cast_work_query,
222 &lwork,
223 &rwork_query,
224 &lrwork,
225 &iwork_query,
226 &liwork,
227 ifail.data(),
228 iclustr.data(),
229 gap.data(),
230 &info);
231
232 /**
233 * The real workspace the query reports is only enough when no
234 * eigenvalue cluster needs reorthogonalizing; ScaLAPACK's own
235 * documentation says so and recommends padding when jobz='V' and
236 * orfac > 0. Under-allocating here does not fail loudly -- it returns
237 * info with the "could not reorthogonalize" bit set and eigenvectors
238 * that are not orthogonal, which would show up as a wrong subspace
239 * rather than an error. The pad is proportional to the work a cluster
240 * spanning the whole basis would need.
241 */
242 lwork = std::max(1, static_cast<int>(work_query.real()));
243 lrwork = std::max(1, static_cast<int>(rwork_query)) + basis_size * basis_size;
244 liwork = std::max(1, iwork_query);
245
246 std::vector<Complex> work(static_cast<size_t>(lwork));
247 std::vector<Real> rwork(static_cast<size_t>(lrwork));
248 std::vector<int> iwork(static_cast<size_t>(liwork));
249
250 auto* cast_work = reinterpret_cast<std::complex<double>*>(work.data());
251
252 scalapack::pzhegvx_(&ibtype,
253 &jobz,
254 &range,
255 &uplo,
256 &basis_size,
257 cast_h,
258 &one,
259 &one,
260 desc,
261 cast_s,
262 &one,
263 &one,
264 desc,
265 &vl,
266 &vu,
267 &il,
268 &iu,
269 &abstol,
270 &number_of_eigenvalues_found,
271 &number_of_eigenvectors_computed,
272 eigenvalues,
273 &orfac,
274 cast_z,
275 &one,
276 &one,
277 desc,
278 cast_work,
279 &lwork,
280 rwork.data(),
281 &lrwork,
282 iwork.data(),
283 &liwork,
284 ifail.data(),
285 iclustr.data(),
286 gap.data(),
287 &info);
288
289 /**
290 * A short solve is as wrong as a failed one, and the expert driver can
291 * return fewer than n of either: info carries "eigenvectors did not
292 * converge" in bit 0 and "could not reorthogonalize" in bit 1. The
293 * caller treats a non-zero return as a failure to fall back on, so
294 * report a short result that way rather than leaving the tail of
295 * eigenvalues and eigenvectors at whatever they held.
296 */
297 if (info == 0 && (number_of_eigenvalues_found != basis_size ||
298 number_of_eigenvectors_computed != basis_size)) {
299 log::warn("ScaLAPACK subspace solve returned {} of {} eigenvalues and {} of {} "
300 "eigenvectors; treating as a failure so the caller can fall back.",
301 number_of_eigenvalues_found,
302 basis_size,
303 number_of_eigenvectors_computed,
304 basis_size);
305 info = -1;
306 }
307
308 /**
309 * 5. Redistribute results back to all ranks (redundant)
310 * Eigenvalues are already redundant (returned on all ranks by pzhegvx).
311 * Eigenvectors (local_z) need to be gathered.
312 */
313 std::vector<Complex> global_z(static_cast<size_t>(basis_size) * basis_size,
314 Complex(0.0, 0.0));
315 for (int j_local = 0; j_local < local_cols; ++j_local) {
316 int j_global = (j_local / nb) * npcol * nb + mycol * nb + (j_local % nb);
317 for (int i_local = 0; i_local < local_rows; ++i_local) {
318 int i_global = (i_local / nb) * nprow * nb + myrow * nb + (i_local % nb);
319 global_z[j_global * basis_size + i_global] =
320 local_z[j_local * local_rows + i_local];
321 }
322 }
323
324 /**
325 * Global reduction (sum) to make eigenvector_matrix redundant.
326 * Each rank contributed its local pieces, so a SUM reduction gathers the full matrix.
327 */
328 execution::sum_complex(global_z.data(), eigenvector_matrix, basis_size * basis_size, comm);
329
330 scalapack::Cblacs_gridexit(blacs_context);
331 scalapack::Cfree_blacs_system_handle(blacs_system_handle);
332 return info;
333#else
334 // Fallback: Redundant serial solve on each rank.
335 SerialSubspaceDiagonalizer serial_solver;
336 return serial_solver.solve(basis_size,
337 hamiltonian_matrix,
338 overlap_matrix,
339 eigenvalues,
340 eigenvector_matrix,
341 is_gamma_only);
342#endif
343 }
344
345 /**
346 * ScaLAPACK solve() is collective (BLACS grid + pzhegvx + Allreduce span the
347 * whole group communicator); the redundant-serial fallback is local-only,
348 * and so is a replicated group, which solve() routes to the serial path.
349 * This must agree with solve(): the subspace manager reads it to decide
350 * whether the group root broadcasts the result, and a solver that claims to
351 * be collective while solving locally would leave that decision resting on
352 * a false premise.
353 */
354 bool is_collective() const override {
355#ifdef Q3_WITH_SCALAPACK
356 return policy_.group_size() > 1;
357#else
358 return false;
359#endif
360 }
361
362 std::string name() const override {
363 return "Parallel (ScaLAPACK)";
364 }
365
366 private:
367 const execution::MPIExecutionPolicy& policy_;
368 const int zero_ = 0;
369};
370
371} // namespace q3
372
373#endif // Q3_WITH_MPI
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
double Real
std::complex< double > Complex