Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
init.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * ExecutionPolicy — abstract interface for process-group collective operations.
5 *
6 * Defines the contract consumed by DavidsonDiagonalizer for vector operations
7 * on distributed coefficient vectors. Implementations provide serial (BLAS only)
8 * or distributed (BLAS + MPI_Allreduce over the group communicator) variants.
9 *
10 * This interface is representation-agnostic: it operates on raw complex vectors
11 * and has no knowledge of k-points, plane waves, bands, or any physical basis.
12 * What a process group *means* (e.g. a band group of processes diagonalizing
13 * the same k-point together) is assigned by higher layers — see
14 * layer_2_quantum_system/execution/init.hpp.
15 */
16
18
19#include <algorithm>
20#include <vector>
21
22#include <complex>
23
24namespace q3 {
25namespace execution {
26
27/**
28 * A contiguous, near-equal division of bands across the ranks sharing them.
29 *
30 * `counts` and `displacements` are in complex elements (bands multiplied by
31 * elements_per_band) so they can be handed straight to
32 * ExecutionPolicy::allgather_across_bands. The remainder is spread one unit per
33 * rank over the lowest ranks rather than dumped on the last one, which keeps the
34 * longest local block as short as possible -- the batch is only finished when the
35 * slowest rank is.
36 *
37 * `band_granularity` is the number of bands that must stay together on one rank.
38 * It exists for the gamma-only Hamiltonian, which packs two real wavefunctions
39 * into the real and imaginary parts of a single complex transform and so walks
40 * bands in pairs. Splitting 130 bands as 65 and 65 hands both ranks an odd count,
41 * which breaks that pairing and makes each rank compute a different result from
42 * the serial one. Passing 2 keeps every rank's block even, so the pairing -- and
43 * the arithmetic -- match serial exactly.
44 */
46 std::vector<int> counts;
47 std::vector<int> displacements;
50};
51
52inline BandPartition partition_bands(int number_of_bands,
53 int number_of_ranks,
54 int rank_index,
55 int elements_per_band,
56 int band_granularity = 1) {
57 BandPartition partition;
58 partition.counts.assign(number_of_ranks, 0);
59 partition.displacements.assign(number_of_ranks, 0);
60 int granularity = std::max(band_granularity, 1);
61 /**
62 * Partition whole units, then convert back to bands. The final unit may be
63 * short when the band count is not a multiple of the granularity, so every
64 * block is clamped to what is actually left.
65 */
66 int number_of_units = (number_of_bands + granularity - 1) / granularity;
67 int base_units = number_of_units / number_of_ranks;
68 int remainder_units = number_of_units % number_of_ranks;
69 int first_band = 0;
70 for (int rank = 0; rank < number_of_ranks; rank++) {
71 int units_for_rank = base_units + (rank < remainder_units ? 1 : 0);
72 int bands_for_rank =
73 std::min(units_for_rank * granularity, std::max(number_of_bands - first_band, 0));
74 partition.displacements[rank] = first_band * elements_per_band;
75 partition.counts[rank] = bands_for_rank * elements_per_band;
76 if (rank == rank_index) {
77 partition.local_first_band = first_band;
78 partition.local_number_of_bands = bands_for_rank;
79 }
80 first_band += bands_for_rank;
81 }
82 return partition;
83}
84
86 public:
88
89 virtual ~ExecutionPolicy() = default;
90
91 /**
92 * Compute the inner product x† · y, optionally reduced across the process group.
93 *
94 * @param number_of_elements Length of both vectors.
95 * @param vector_x Left operand (conjugated).
96 * @param vector_y Right operand.
97 * @return The (globally summed) complex inner product.
98 */
99 virtual ComplexValue dot_product(int number_of_elements,
100 const ComplexValue* vector_x,
101 const ComplexValue* vector_y) const = 0;
102
103 /**
104 * Scaled vector addition in-place: vector_y += scaling_factor * vector_x.
105 *
106 * @param number_of_elements Length of both vectors.
107 * @param scaling_factor Complex scaling coefficient.
108 * @param vector_x Source vector.
109 * @param vector_y Destination vector (modified in-place).
110 */
111 virtual void add_scaled_vector(int number_of_elements,
112 ComplexValue scaling_factor,
113 const ComplexValue* vector_x,
114 ComplexValue* vector_y) const = 0;
115
116 /**
117 * Compute the Euclidean norm, optionally reduced across the process group.
118 *
119 * @param number_of_elements Length of the vector.
120 * @param vector_x Input vector.
121 * @return The real-valued (globally reduced) norm.
122 */
123 virtual Real norm(int number_of_elements, const ComplexValue* vector_x) const = 0;
124
125 /**
126 * Check if this process is the root process (rank 0 in the world communicator).
127 *
128 * Used by entry points to ensure only one process writes output files or
129 * logs non-essential information.
130 */
131 virtual bool is_root() const = 0;
132
133 /**
134 * Index of this process within its process group.
135 *
136 * 0 in serial mode. NOT necessarily 0 when group_size() == 1: a group whose
137 * data distribution was disabled at run time (see
138 * MPIExecutionPolicy::disable_group_distribution) reports group_size() == 1
139 * while keeping its pool-local rank index, because the pool identity is
140 * still needed. Use group_size() <= 1 to ask "is my data replicated"; use
141 * group_rank_index() == 0 only to pick one representative per pool.
142 */
143 virtual int group_rank_index() const = 0;
144
145 /**
146 * Number of processes in this process group.
147 * Always 1 in serial mode or when each group contains a single process.
148 */
149 virtual int group_size() const = 0;
150
151 /**
152 * Number of processes in the complete execution topology.
153 */
154 virtual int number_of_processes() const = 0;
155
156 /**
157 * Sum a rank-local element count across the process group.
158 *
159 * Distributed vector algorithms use this to distinguish the local slab
160 * length from the dimension of the complete vector space.
161 */
162 virtual int global_number_of_elements(int local_number_of_elements) const = 0;
163
164 /**
165 * Allreduce an array of complex values within the process group.
166 *
167 * Used after ZGEMM-based inner products over the distributed vector
168 * dimension (e.g., subspace matrices, orthogonalization overlaps)
169 * when coefficient vectors are distributed across group ranks.
170 * No-op when group_size == 1.
171 *
172 * @param data In-out array of complex values.
173 * @param number_of_elements Number of complex elements to reduce.
174 */
175 virtual void reduce_within_group(ComplexValue* data, int number_of_elements) const = 0;
176
177 /**
178 * Allreduce an array of real values within the process group.
179 *
180 * Same as the complex variant but for real-valued arrays.
181 *
182 * @param data In-out array of real values.
183 * @param number_of_elements Number of real elements to reduce.
184 */
185 virtual void reduce_within_group(Real* data, int number_of_elements) const = 0;
186
187 /**
188 * Allgather rank-local slab data into a full-size buffer on every rank.
189 *
190 * Gathers each group rank's contiguous local slice into a contiguous
191 * full array on every rank (concatenated in rank order).
192 * No-op when group_size == 1.
193 *
194 * @param local_data Rank-local slab data.
195 * @param local_count Number of real elements in the local slab.
196 * @param full_data Output: full-grid data (all slabs concatenated).
197 * @param total_count Total number of real elements across all ranks.
198 */
199 virtual void allgather_within_group(const Real* local_data,
200 int local_count,
201 Real* full_data,
202 int total_count) const = 0;
203
204 /**
205 * Reduce group-distributed partial results across the process group.
206 *
207 * Used when different ranks compute disjoint contributions to the same
208 * output array. Unlike reduce_within_group
209 * (which is a no-op when the input vectors are replicated), this always performs
210 * the reduction when group_size > 1. No-op in serial.
211 *
212 * @param data In-out array of complex values.
213 * @param number_of_elements Number of complex elements to reduce.
214 */
216 int /*number_of_elements*/) const {}
217
218 /**
219 * ── Band distribution ─────────────────────────────────────────────────────
220 *
221 * A second, mutually exclusive way to divide work within a k-point pool.
222 * Where group distribution splits the reciprocal-space coefficients of every
223 * band across ranks, band distribution splits the *bands* and leaves each
224 * rank holding complete coefficient vectors.
225 *
226 * The two cannot both be active: a rank either owns all of G for some bands
227 * or some of G for all bands. When bands are distributed, group distribution
228 * is switched off (group_size() reports 1), so every reduce_within_group()
229 * correctly becomes a no-op -- the coefficients each rank contracts over are
230 * whole, not partial. What replaces those reductions is an allgather over
231 * the band axis, once per batch, rather than a reduction per inner product.
232 *
233 * Every method below is defaulted to the replicated, single-rank answer, so
234 * a policy that does not distribute bands inherits correct behaviour.
235 */
236
237 /**
238 * Number of ranks sharing the band split. 1 when bands are not distributed.
239 */
240 virtual int band_distribution_size() const {
241 return 1;
242 }
243
244 /**
245 * This rank's index within the band split. 0 when bands are not distributed.
246 */
247 virtual int band_distribution_rank_index() const {
248 return 0;
249 }
250
251 /**
252 * Complete an array whose local band block is already written in place.
253 *
254 * Each rank has filled its own contiguous block of `data`; afterwards every
255 * rank holds all blocks. `counts` and `displacements` are in complex
256 * elements and indexed by band-distribution rank, so a caller hands over the
257 * same partition it used to divide the work.
258 *
259 * The in-place contract is deliberate. Gathering from a sub-range of the
260 * destination into that same destination aliases the send and receive
261 * buffers, which MPI forbids; requiring the block to be in place already is
262 * what lets the implementation say so with MPI_IN_PLACE instead.
263 * No-op when bands are not distributed -- the one block is the whole array.
264 */
265 virtual void allgather_across_bands(ComplexValue* /*data*/,
266 const int* /*counts*/,
267 const int* /*displacements*/) const {}
268
269 /**
270 * Sum an array across the band split, so that a quantity accumulated from
271 * disjoint band ranges (the charge density above all) becomes whole.
272 * No-op when bands are not distributed.
273 */
274 virtual void reduce_across_bands(Real* /*data*/, int /*number_of_elements*/) const {}
275
276 /**
277 * Republish an array from the first rank of the band split to the rest.
278 *
279 * This exists because "every rank recomputes the same thing from the same
280 * data, so every rank agrees" is not true in practice. Vendor BLAS picks its
281 * blocking and summation order from what the machine looks like at the time,
282 * so two processes running identical arithmetic on identical inputs can
283 * differ in the last bits -- measured here at a relative 1e-15 on the first
284 * expansion. Davidson then amplifies that: orthogonalisation rejects
285 * candidates on a threshold, so a last-bit difference changes how many
286 * vectors a rank accepts, the ranks stop agreeing on the size of the next
287 * batch, and the collective that follows fails outright.
288 *
289 * Broadcasting the redundantly computed result makes one rank's arithmetic
290 * authoritative, which restores exactly the agreement the design assumed.
291 * No-op when bands are not distributed.
292 */
293 virtual void broadcast_across_bands(ComplexValue* /*data*/, int /*number_of_elements*/) const {}
294
295 /// Integer overload, for the counts that decide collective sizes.
296 virtual void broadcast_across_bands(int* /*data*/, int /*number_of_elements*/) const {}
297
298 /// Real overload, for eigenvalues -- the numbers convergence decisions are made from.
299 virtual void broadcast_across_bands(Real* /*data*/, int /*number_of_elements*/) const {}
300
301 /**
302 * Broadcast complex data from the group root (rank 0) to all ranks in the group.
303 *
304 * Used to avoid replicated LAPACK computation: rank 0 computes, then
305 * broadcasts the result. No-op when group_size == 1.
306 *
307 * @param data In-out array of complex values.
308 * @param number_of_elements Number of complex elements to broadcast.
309 */
311 int /*number_of_elements*/) const {}
312
313 /**
314 * Broadcast real data from the group root (rank 0) to all ranks in the group.
315 *
316 * @param data In-out array of real values.
317 * @param number_of_elements Number of real elements to broadcast.
318 */
319 virtual void broadcast_from_group_root(Real* /*data*/, int /*number_of_elements*/) const {}
320};
321
322} // namespace execution
323} // namespace q3
virtual void broadcast_from_group_root(Real *, int) const
Definition init.hpp:319
virtual void broadcast_across_bands(int *, int) const
Integer overload, for the counts that decide collective sizes.
Definition init.hpp:296
virtual ComplexValue dot_product(int number_of_elements, const ComplexValue *vector_x, const ComplexValue *vector_y) const =0
virtual int band_distribution_rank_index() const
Definition init.hpp:247
virtual int group_size() const =0
virtual void allgather_within_group(const Real *local_data, int local_count, Real *full_data, int total_count) const =0
virtual void add_scaled_vector(int number_of_elements, ComplexValue scaling_factor, const ComplexValue *vector_x, ComplexValue *vector_y) const =0
virtual void broadcast_across_bands(Real *, int) const
Real overload, for eigenvalues – the numbers convergence decisions are made from.
Definition init.hpp:299
virtual ~ExecutionPolicy()=default
virtual void allgather_across_bands(ComplexValue *, const int *, const int *) const
Definition init.hpp:265
virtual void broadcast_from_group_root(ComplexValue *, int) const
Definition init.hpp:310
virtual void reduce_group_distributed_result(ComplexValue *, int) const
Definition init.hpp:215
virtual int number_of_processes() const =0
virtual int group_rank_index() const =0
virtual int global_number_of_elements(int local_number_of_elements) const =0
virtual void broadcast_across_bands(ComplexValue *, int) const
Definition init.hpp:293
virtual void reduce_within_group(Real *data, int number_of_elements) const =0
virtual int band_distribution_size() const
Definition init.hpp:240
virtual bool is_root() const =0
virtual Real norm(int number_of_elements, const ComplexValue *vector_x) const =0
virtual void reduce_across_bands(Real *, int) const
Definition init.hpp:274
virtual void reduce_within_group(ComplexValue *data, int number_of_elements) const =0
BandPartition partition_bands(int number_of_bands, int number_of_ranks, int rank_index, int elements_per_band, int band_granularity=1)
Definition init.hpp:52
double Real
std::complex< double > Complex
std::vector< int > counts
Definition init.hpp:46
std::vector< int > displacements
Definition init.hpp:47