Loading...
Searching...
No Matches
fft_grid_serial.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * The build picks one serial FFT backend (CMake option Q3_FFT_BACKEND), and
5 * SerialFFTGrid names it. With PocketFFT selected this header never includes
6 * <fftw3.h>, so nothing in the binary refers to FFTW.
7 */
8#ifdef Q3_FFT_BACKEND_POCKETFFT
9
11
12namespace q3 {
13using SerialFFTGrid = SerialPocketFFTGrid;
14} // namespace q3
15
16#else
17
18#include <algorithm>
19#include <cstddef>
20#include <cstdlib>
21#include <cstring>
22#include <fftw3.h>
23#include <thread>
24#include <vector>
25
30
31namespace q3 {
32
33/**
34 * SerialFFTGrid — node-local 3D FFT implementation using FFTW3.
35 *
36 * This implementation assumes the entire grid resides in the memory of a
37 * single process. It supports OpenMP multi-threading within FFTW if
38 * Q3_USE_FFTW_THREADS is defined.
39 */
40class SerialFFTGrid : public FFTGrid {
41 /**
42 * One thread's scratch and the plans bound to it. Slot 0 is not one of
43 * these -- it is `work_` / `output_buffer_` and the plans beside them, so
44 * that the single-slot case allocates and plans exactly what it used to.
45 */
46 struct TransformSlot {
47 mutable std::vector<Complex> input;
48 mutable std::vector<Complex> output;
49 fftw_plan plan_forward{nullptr};
50 fftw_plan plan_inverse{nullptr};
51 };
52
53 public:
54 SerialFFTGrid() = default;
55 ~SerialFFTGrid() override {
56 if (plan_forward_)
57 fftw_destroy_plan(plan_forward_);
58 if (plan_inverse_)
59 fftw_destroy_plan(plan_inverse_);
60 for (TransformSlot& slot : additional_slots_) {
61 if (slot.plan_forward)
62 fftw_destroy_plan(slot.plan_forward);
63 if (slot.plan_inverse)
64 fftw_destroy_plan(slot.plan_inverse);
65 }
66 }
67
68 // FFTW plans are non-copyable resources
69 SerialFFTGrid(const SerialFFTGrid&) = delete;
73
74 void init(int dimension_1,
75 int dimension_2,
76 int dimension_3,
77 bool use_measured_planning = false) override {
78 grid_dimension_1_ = dimension_1;
79 grid_dimension_2_ = dimension_2;
80 grid_dimension_3_ = dimension_3;
81 int total_points = dimension_1 * dimension_2 * dimension_3;
82 work_.resize(total_points);
83
84 /**
85 * How many transforms callers may drive at once. One slot -- the
86 * historical behaviour -- when there is one thread to run them on, so
87 * a single-threaded run allocates nothing extra and executes the same
88 * plan on the same buffers it always did.
89 *
90 * Each additional slot costs two more grids of scratch. That is small
91 * beside the wavefunction storage a grid this size implies, and it is
92 * what buys a band loop that scales. The alternative -- FFTW's own
93 * intra-transform threading, which is what this class used to do with
94 * `OMP_NUM_THREADS` -- pays a fork and join inside every one of the
95 * thousands of transforms a Davidson iteration issues, and the
96 * measurements in docs/performance_nersc_perlmutter.md show it
97 * bottoming out at eight threads and then getting worse than serial.
98 * That document names threading across bands with serial transforms as
99 * the scalable axis; this is it.
100 */
101 int requested_slots = execution::threading::maximum_threads();
102
103 /**
104 * A ceiling on what the extra slots may cost, because the thread count
105 * is the machine's and the grid size is the calculation's, and their
106 * product is nobody's decision. Sixty-four threads on a 128^3 grid
107 * would otherwise claim four gibibytes of scratch that no input asked
108 * for. Past this point the slots stop being added and the band loop
109 * runs on fewer threads than the machine has -- which costs some
110 * parallelism, and is the right way round: a slower run is recoverable
111 * and an out-of-memory run is not.
112 */
113 constexpr std::size_t maximum_additional_scratch_bytes = 1024ull * 1024ull * 1024ull;
114 std::size_t bytes_per_slot = 2 * static_cast<std::size_t>(total_points) * sizeof(Complex);
115 int affordable_slots =
116 1 + static_cast<int>(maximum_additional_scratch_bytes /
117 std::max<std::size_t>(bytes_per_slot, std::size_t{1}));
118 requested_slots = std::min(requested_slots, affordable_slots);
119
120#ifdef Q3_USE_FFTW_THREADS
121 static bool fftw_threads_initialized = false;
122 int number_of_fft_threads = 1;
123 const char* omp_threads_env = std::getenv("OMP_NUM_THREADS");
124 if (omp_threads_env != nullptr) {
125 number_of_fft_threads = std::atoi(omp_threads_env);
126 if (number_of_fft_threads < 1) {
127 number_of_fft_threads = 1;
128 }
129 }
130 /**
131 * Threads go to whole transforms, not to the inside of one. A plan
132 * that forks its own team inside a band-parallel region would
133 * oversubscribe the machine, and -- because a two-thread transform
134 * decomposes differently from a one-thread transform -- would also
135 * make a band's last bits depend on which slot it landed in. One
136 * thread per transform keeps every slot numerically the same plan.
137 */
138 if (requested_slots > 1) {
139 number_of_fft_threads = 1;
140 }
141 if (number_of_fft_threads > 1) {
142 if (!fftw_threads_initialized) {
143 fftw_init_threads();
144 fftw_threads_initialized = true;
145 }
146 fftw_plan_with_nthreads(number_of_fft_threads);
147 }
148#endif
149
150 unsigned int planning_flags = use_measured_planning ? FFTW_MEASURE : FFTW_ESTIMATE;
151 output_buffer_.resize(total_points);
152
153 /**
154 * Out-of-place plans: input from work_, output to output_buffer_.
155 * Out-of-place avoids internal data shuffling and is typically faster.
156 */
157 plan_forward_ = fftw_plan_dft_3d(dimension_1,
158 dimension_2,
159 dimension_3,
160 reinterpret_cast<fftw_complex*>(work_.data()),
161 reinterpret_cast<fftw_complex*>(output_buffer_.data()),
162 FFTW_FORWARD,
163 planning_flags);
164
165 plan_inverse_ = fftw_plan_dft_3d(dimension_1,
166 dimension_2,
167 dimension_3,
168 reinterpret_cast<fftw_complex*>(work_.data()),
169 reinterpret_cast<fftw_complex*>(output_buffer_.data()),
170 FFTW_BACKWARD,
171 planning_flags);
172
173 /**
174 * Slots 1..N-1. Each gets its own plan over its own arrays rather than
175 * borrowing slot 0's through fftw_execute_dft, because that shortcut
176 * is only valid when the new arrays share the plan's alignment class
177 * -- a property of the allocator, not of anything checked here.
178 * Planning is serial by construction: FFTW's planner is not
179 * re-entrant, and this runs once at setup, before any parallel region.
180 */
181 additional_slots_.resize(static_cast<std::size_t>(std::max(requested_slots - 1, 0)));
182 for (TransformSlot& slot : additional_slots_) {
183 slot.input.resize(total_points);
184 slot.output.resize(total_points);
185 slot.plan_forward =
186 fftw_plan_dft_3d(dimension_1,
187 dimension_2,
188 dimension_3,
189 reinterpret_cast<fftw_complex*>(slot.input.data()),
190 reinterpret_cast<fftw_complex*>(slot.output.data()),
191 FFTW_FORWARD,
192 planning_flags);
193 slot.plan_inverse =
194 fftw_plan_dft_3d(dimension_1,
195 dimension_2,
196 dimension_3,
197 reinterpret_cast<fftw_complex*>(slot.input.data()),
198 reinterpret_cast<fftw_complex*>(slot.output.data()),
199 FFTW_BACKWARD,
200 planning_flags);
201 }
202 }
203
204 [[nodiscard]] int grid_dimension_1() const override {
205 return grid_dimension_1_;
206 }
207 [[nodiscard]] int grid_dimension_2() const override {
208 return grid_dimension_2_;
209 }
210 [[nodiscard]] int grid_dimension_3() const override {
211 return grid_dimension_3_;
212 }
213 [[nodiscard]] int total_grid_points() const override {
214 return grid_dimension_1_ * grid_dimension_2_ * grid_dimension_3_;
215 }
216
217 void forward(Complex* data) const override {
219 int total_points = grid_dimension_1_ * grid_dimension_2_ * grid_dimension_3_;
220 // Copy to the plan's input buffer (guaranteed alignment), execute, copy back
221 std::memcpy(work_.data(), data, total_points * sizeof(Complex));
222 fftw_execute(plan_forward_);
223 std::memcpy(data, output_buffer_.data(), total_points * sizeof(Complex));
224 }
225
226 void inverse(Complex* data) const override {
228 int total_points = grid_dimension_1_ * grid_dimension_2_ * grid_dimension_3_;
229 std::memcpy(work_.data(), data, total_points * sizeof(Complex));
230 fftw_execute(plan_inverse_);
231 Real inverse_total_points = 1.0 / total_points;
232 for (int point_index = 0; point_index < total_points; point_index++) {
233 data[point_index] = output_buffer_[point_index] * inverse_total_points;
234 }
235 }
236
237 void inverse_raw(Complex* data) const override {
239 int total_points = grid_dimension_1_ * grid_dimension_2_ * grid_dimension_3_;
240 std::memcpy(work_.data(), data, total_points * sizeof(Complex));
241 fftw_execute(plan_inverse_);
242 std::memcpy(data, output_buffer_.data(), total_points * sizeof(Complex));
243 }
244
245 [[nodiscard]] int
246 g_vector_to_fft_grid_index(int miller_1, int miller_2, int miller_3) const override {
247 int wrapped_index_1 = (miller_1 >= 0) ? miller_1 : miller_1 + grid_dimension_1_;
248 int wrapped_index_2 = (miller_2 >= 0) ? miller_2 : miller_2 + grid_dimension_2_;
249 int wrapped_index_3 = (miller_3 >= 0) ? miller_3 : miller_3 + grid_dimension_3_;
250 return (wrapped_index_1 * grid_dimension_2_ + wrapped_index_2) * grid_dimension_3_ +
251 wrapped_index_3;
252 }
253
254 [[nodiscard]] int local_grid_points() const override {
255 return total_grid_points();
256 }
257
258 [[nodiscard]] int local_offset() const override {
259 return 0;
260 }
261
262 void reduce_g_space(Complex* /*data*/, int /*number_of_elements*/) const override {
263 // No-op for serial: data is already redundant
264 }
265
266 Complex* input_buffer() const override {
267 return work_.data();
268 }
269
270 Complex* output_buffer() const override {
271 return output_buffer_.data();
272 }
273
274 void execute_inverse_raw_on_buffers() const override {
276 fftw_execute(plan_inverse_);
277 }
278
279 void execute_forward_on_buffers() const override {
281 fftw_execute(plan_forward_);
282 }
283
284 void prepare_forward_from_output() const override {
285 // Out-of-place plan: copy output → input for the next forward transform
286 int total_points = grid_dimension_1_ * grid_dimension_2_ * grid_dimension_3_;
287 std::memcpy(work_.data(), output_buffer_.data(), total_points * sizeof(Complex));
288 }
289
290 /**
291 * How many transforms may run at once: one per set of scratch arrays
292 * `init()` planned. Slot 0 is the pair every other entry point already
293 * uses, so a caller that asks for one slot runs the historical code.
294 */
295 [[nodiscard]] int concurrent_transform_slots() const override {
296 return 1 + static_cast<int>(additional_slots_.size());
297 }
298
299 [[nodiscard]] Complex* slot_input_buffer(int slot) const override {
300 return slot == 0 ? work_.data()
301 : additional_slots_[static_cast<std::size_t>(slot - 1)].input.data();
302 }
303
304 [[nodiscard]] Complex* slot_output_buffer(int slot) const override {
305 return slot == 0 ? output_buffer_.data()
306 : additional_slots_[static_cast<std::size_t>(slot - 1)].output.data();
307 }
308
309 void execute_inverse_raw_on_slot(int slot) const override {
311 fftw_execute(slot == 0
312 ? plan_inverse_
313 : additional_slots_[static_cast<std::size_t>(slot - 1)].plan_inverse);
314 }
315
316 void execute_forward_on_slot(int slot) const override {
318 fftw_execute(slot == 0
319 ? plan_forward_
320 : additional_slots_[static_cast<std::size_t>(slot - 1)].plan_forward);
321 }
322
323 /**
324 * The same copy-in, transform, copy-out `inverse_raw()` performs, driven
325 * through slot `slot`'s scratch and plan. Slot 0 is `inverse_raw()` itself.
326 */
327 void inverse_raw_on_slot(Complex* data, int slot) const override {
329 std::size_t total_points =
330 static_cast<std::size_t>(grid_dimension_1_) * grid_dimension_2_ * grid_dimension_3_;
331 std::memcpy(slot_input_buffer(slot), data, total_points * sizeof(Complex));
332 fftw_execute(slot == 0
333 ? plan_inverse_
334 : additional_slots_[static_cast<std::size_t>(slot - 1)].plan_inverse);
335 std::memcpy(data, slot_output_buffer(slot), total_points * sizeof(Complex));
336 }
337
338 void prepare_forward_from_output_on_slot(int slot) const override {
339 int total_points = grid_dimension_1_ * grid_dimension_2_ * grid_dimension_3_;
340 std::memcpy(slot_input_buffer(slot),
341 slot_output_buffer(slot),
342 static_cast<std::size_t>(total_points) * sizeof(Complex));
343 }
344
345 private:
346 int grid_dimension_1_{}, grid_dimension_2_{}, grid_dimension_3_{};
347 fftw_plan plan_forward_{nullptr};
348 fftw_plan plan_inverse_{nullptr};
349 mutable std::vector<Complex> work_; // input workspace for FFTW plan creation
350 mutable std::vector<Complex> output_buffer_; // output buffer for out-of-place FFTs
351 mutable std::vector<TransformSlot> additional_slots_; // slots 1..N-1, one per extra thread
352};
353
354} // namespace q3
355
356#endif // Q3_FFT_BACKEND_POCKETFFT
void inverse(Complex *data) const override
int local_grid_points() const override
SerialFFTGrid(const SerialFFTGrid &)=delete
int local_offset() const override
void forward(Complex *data) const override
SerialFFTGrid & operator=(SerialFFTGrid &&)=default
Complex * slot_input_buffer(int slot) const override
void execute_forward_on_buffers() const override
Complex * slot_output_buffer(int slot) const override
SerialFFTGrid & operator=(const SerialFFTGrid &)=delete
void reduce_g_space(Complex *, int) const override
int grid_dimension_3() const override
SerialFFTGrid(SerialFFTGrid &&)=default
int grid_dimension_1() const override
void execute_forward_on_slot(int slot) const override
Forward transform on slot slot, the slot-aware execute_forward_on_buffers().
void init(int dimension_1, int dimension_2, int dimension_3, bool use_measured_planning=false) override
int concurrent_transform_slots() const override
void inverse_raw(Complex *data) const override
Complex * output_buffer() const override
void prepare_forward_from_output_on_slot(int slot) const override
Output-to-input copy on slot slot, the slot-aware prepare_forward_from_output().
SerialFFTGrid()=default
Complex * input_buffer() const override
void inverse_raw_on_slot(Complex *data, int slot) const override
void execute_inverse_raw_on_buffers() const override
int total_grid_points() const override
int grid_dimension_2() const override
int g_vector_to_fft_grid_index(int miller_1, int miller_2, int miller_3) const override
void execute_inverse_raw_on_slot(int slot) const override
Inverse transform on slot slot, the slot-aware execute_inverse_raw_on_buffers().
void prepare_forward_from_output() const override
double Real
std::complex< double > Complex
std::atomic< long long > fft_call_count
Definition counters.hpp:22
static ProfilingCounters & instance()
Definition counters.hpp:220