Loading...
Searching...
No Matches
memory_estimate.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Closed-form estimate of a plane-wave run's memory footprint, per rank.
5 *
6 * Printing one before the run starts is how a user learns that a deck will not
7 * fit without spending an allocation finding out. Q3 had none, which is how a
8 * 432-atom example came to carry a comment saying it needed 45 GB per rank when
9 * it needs more than twice that.
10 *
11 * The estimate is deliberately arithmetic on the dimensions the setup already
12 * knows, not a sum over live allocations: it has to be printable before the
13 * solver has allocated anything. Its accuracy is therefore a claim about the
14 * code that can be checked against the measured resident set, and the
15 * checkpoint probes in ProfilingCounters exist to check it.
16 *
17 * Every term below is replicated on every rank when bands are distributed,
18 * because that mode keeps the whole G-sphere on each rank by construction.
19 */
20
21#include <cstdint>
22#include <string>
23#include <vector>
24
25namespace q3 {
26
27/// One line of the estimate: what it is, how it is sized, and how large it is.
29 std::string label;
30 std::string formula;
31 std::uint64_t bytes = 0;
32};
33
34/**
35 * The dimensions an estimate needs, gathered from the basis, the
36 * pseudopotential and the solver configuration.
37 */
48 std::int64_t fft_rho_grid_points = 0;
49 std::int64_t fft_wave_grid_points = 0;
51 /// False for ultrasoft and PAW, where S is not the identity and is stored.
52 bool overlap_is_identity = false;
53 /// False for the direct solver, which holds no Davidson workspace.
55};
56
57/**
58 * A footprint estimate: the terms that persist, the terms that only spike, and
59 * the totals a reader actually acts on.
60 */
62 std::vector<PlanewaveMemoryEstimateTerm> steady_state;
63 std::vector<PlanewaveMemoryEstimateTerm> transient;
64 std::uint64_t steady_state_bytes = 0;
65 /// What the solver workspace alone contributes to the steady state.
66 std::uint64_t solver_bytes = 0;
67 /// The tallest point of setup, before the solver workspace exists.
68 std::uint64_t setup_peak_bytes = 0;
69 std::uint64_t peak_bytes = 0;
71
73};
74
77 const std::uint64_t complex_size = 16;
78 const std::uint64_t real_size = 8;
79 const std::uint64_t bands = static_cast<std::uint64_t>(inputs.number_of_bands);
80 const std::uint64_t plane_waves = static_cast<std::uint64_t>(inputs.max_plane_waves_per_kpoint);
81 const std::uint64_t g_vectors = static_cast<std::uint64_t>(inputs.number_of_g_vectors);
82 const std::uint64_t projectors = static_cast<std::uint64_t>(inputs.number_of_projectors);
83 const std::uint64_t spins = static_cast<std::uint64_t>(inputs.number_of_spin_components);
84
86
87 /**
88 * The Davidson basis is sized by the GLOBAL band count, never by a rank's
89 * local range: band distribution splits the loops over these arrays, not
90 * the arrays themselves.
91 */
92 int expanded = inputs.davidson_basis_expansion_factor * inputs.number_of_bands;
93 int clamped =
94 expanded < inputs.max_plane_waves_per_kpoint ? expanded : inputs.max_plane_waves_per_kpoint;
95 estimate.maximum_basis_size =
96 clamped > inputs.number_of_bands ? clamped : inputs.number_of_bands;
97 const std::uint64_t basis = static_cast<std::uint64_t>(estimate.maximum_basis_size);
98
99 auto add_steady = [&estimate](const std::string& label,
100 const std::string& formula,
101 std::uint64_t bytes,
102 bool belongs_to_solver = false) {
103 if (bytes == 0) {
104 return;
105 }
106 estimate.steady_state.push_back({label, formula, bytes});
107 estimate.steady_state_bytes += bytes;
108 if (belongs_to_solver) {
109 estimate.solver_bytes += bytes;
110 }
111 };
112 auto add_transient =
113 [&estimate](const std::string& label, const std::string& formula, std::uint64_t bytes) {
114 if (bytes == 0) {
115 return;
116 }
117 estimate.transient.push_back({label, formula, bytes});
118 };
119
120 if (inputs.diagonalizer_is_davidson) {
121 add_steady("Davidson basis V, HV",
122 "2 x M x npw x 16",
123 2 * basis * plane_waves * complex_size,
124 true);
125 /**
126 * S|V> is stored only when S is not the identity. For norm-conserving
127 * runs it would be a verbatim copy of the basis, so the solver aliases
128 * the two and this term disappears.
129 */
130 if (!inputs.overlap_is_identity) {
131 add_steady(
132 "Davidson basis SV", "M x npw x 16", basis * plane_waves * complex_size, true);
133 }
134 /**
135 * One band-sized buffer, borrowed for the two rotations that cannot be
136 * done in place. The Ritz vectors, H|Ritz>, S|Ritz> and the residual
137 * array are gone: correction vectors are built straight into the free
138 * basis columns.
139 */
140 add_steady("rotation scratch", "nb x npw x 16", bands * plane_waves * complex_size, true);
141 if (!inputs.overlap_is_identity) {
142 add_steady(
143 "Ritz overlap vectors", "nb x npw x 16", bands * plane_waves * complex_size, true);
144 }
145 /**
146 * H and S in the subspace, the eigenvector matrix, and the copy of S the
147 * eigensolver is allowed to destroy; the Hamiltonian's copy is the
148 * eigenvector buffer itself. At Gamma the solver packs to real in place
149 * rather than into copies of its own.
150 */
151 add_steady("subspace matrices", "4 x M^2 x 16", 4 * basis * basis * complex_size, true);
152 }
153
154 add_steady("wavefunctions",
155 "nkstates x nb x npw x 16",
156 static_cast<std::uint64_t>(inputs.local_number_of_kpoint_states) * bands *
157 plane_waves * complex_size);
158 add_steady("nonlocal projectors", "nkb x npw x 16", projectors * plane_waves * complex_size);
159 /**
160 * The D matrix is block-diagonal by atom and is stored that way. As a dense
161 * nkb x nkb it was 967 MB on the 432-atom deck, of which 2 MB was nonzero.
162 * The estimate does not carry the per-atom channel count, so it uses the
163 * average, nkb^2 / nat.
164 */
165 if (inputs.number_of_atoms > 0) {
166 add_steady("block-diagonal D matrix",
167 "nkb^2 / nat x 16",
168 projectors * projectors / static_cast<std::uint64_t>(inputs.number_of_atoms) *
169 complex_size);
170 }
171 add_steady("Broyden history",
172 "3 x ndim x nspin x ngm x 16",
173 3 * static_cast<std::uint64_t>(inputs.mixing_history_depth) * spins * g_vectors *
174 complex_size);
175 add_steady("G-vector table", "ngm x 80", g_vectors * 80);
176 add_steady("FFT buffers",
177 "2 x (Nrho + 2 x Nwave) x 16",
178 2 *
179 (static_cast<std::uint64_t>(inputs.fft_rho_grid_points) +
180 2 * static_cast<std::uint64_t>(inputs.fft_wave_grid_points)) *
181 complex_size);
182 add_steady("density and potential",
183 "(4 + 3 x nspin) x Nrho x 8",
184 (4 + 3 * spins) * static_cast<std::uint64_t>(inputs.fft_rho_grid_points) *
185 real_size);
186
187 /**
188 * The atomic-orbital start holds five arrays of max(natwfc, nb) columns at
189 * once. It is a spike, not a resident cost, but it is the tallest point of
190 * a small run and it is what an undersized allocation dies in.
191 */
192 std::uint64_t starting_columns = static_cast<std::uint64_t>(
194 : inputs.number_of_bands);
195 add_transient("wavefunction initialization",
196 "5 x max(natwfc, nb) x npw x 16",
197 5 * starting_columns * plane_waves * complex_size);
198
199 /**
200 * The setup spike and the solver workspace never coexist: the initializer's
201 * arrays are gone by the time the first solve allocates. So the peak is the
202 * taller of the two phases, not their sum.
203 */
204 std::uint64_t largest_transient = 0;
205 for (const auto& term : estimate.transient) {
206 if (term.bytes > largest_transient) {
207 largest_transient = term.bytes;
208 }
209 }
210 estimate.setup_peak_bytes =
211 estimate.steady_state_bytes - estimate.solver_bytes + largest_transient;
212 estimate.peak_bytes = estimate.steady_state_bytes > estimate.setup_peak_bytes
213 ? estimate.steady_state_bytes
214 : estimate.setup_peak_bytes;
215 return estimate;
216}
217
218} // namespace q3
bool overlap_is_identity
False for ultrasoft and PAW, where S is not the identity and is stored.
bool diagonalizer_is_davidson
False for the direct solver, which holds no Davidson workspace.
One line of the estimate: what it is, how it is sized, and how large it is.
std::vector< PlanewaveMemoryEstimateTerm > transient
std::uint64_t setup_peak_bytes
The tallest point of setup, before the solver workspace exists.
std::vector< PlanewaveMemoryEstimateTerm > steady_state
static PlanewaveMemoryEstimate compute(const PlanewaveMemoryEstimateInputs &inputs)
std::uint64_t solver_bytes
What the solver workspace alone contributes to the steady state.