Skip to content

Commit c5d27c8

Browse files
authored
Add FFT fast-length helper and pad OpenBC FFT domains (#5494)
Add FFT::nextFastLen for choosing 2/3/5/7/11-smooth padded FFT sizes. Use the helper in OpenBCSolver to pad internal doubled convolution domains to fast lengths by default. Add FFT::Info controls to disable or tune OpenBC padding, pass Info through PoissonOpenBC, update docs, and test padded vs. unpadded OpenBC solves.
1 parent 07311ca commit c5d27c8

5 files changed

Lines changed: 309 additions & 40 deletions

File tree

Docs/sphinx_documentation/source/FFT.rst

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ object. Therefore, one should cache it for reuse if possible. Although
6666
:cpp:`FFT::R2C` does not have a default constructor, one could always use
6767
:cpp:`std::unique_ptr<FFT::R2C<Real>>` to store an object in one's class.
6868

69+
Choosing FFT lengths
70+
--------------------
71+
72+
:cpp:`FFT::nextFastLen(target, nfactors)` returns the smallest FFT length
73+
greater than or equal to :cpp:`target` whose prime factors are limited to the
74+
first :cpp:`nfactors` values from :cpp:`{2, 3, 5, 7, 11, 13}`. If
75+
:cpp:`nfactors` is omitted, :cpp:`FFT::FastNumPrimeFactors()` provides the
76+
platform-dependent default. It currently returns 5 for CUDA and 6 for other
77+
backends. This default is a performance tuning policy and may change in the
78+
future. This helper can be used to choose a padded FFT domain size that is
79+
expected to perform well with common FFT backends.
80+
6981

7082
Class template `FFT::R2C` also supports batched FFTs. The batch size is set
7183
in an :cpp:`FFT::Info` object passed to the constructor of
@@ -177,7 +189,14 @@ boundaries.
177189

178190
:cpp:`FFT::OpenBCSolver` currently supports one right-hand-side component per
179191
solve. It does not support :cpp:`FFT::Info::setBatchSize` values greater than
180-
one.
192+
one. The solver uses an internal doubled convolution domain in each transformed
193+
direction. By default, the one-sided length is rounded up with
194+
:cpp:`FFT::nextFastLen(n)` before doubling, using the platform-dependent
195+
:cpp:`FFT::FastNumPrimeFactors()` default described above. This extra padding
196+
changes only the internal FFT work arrays, not the user-provided
197+
:cpp:`MultiFab` domains. Users can disable it with
198+
:cpp:`FFT::Info::setOpenBCPadding(false)` or tune the factor count with
199+
:cpp:`FFT::Info::setOpenBCPaddingNumPrimeFactors(nfactors)`.
181200

182201
.. highlight:: c++
183202

Src/FFT/AMReX_FFT_Helper.H

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ AMREX_ENUM( Boundary, periodic, even, odd );
6060
enum struct Kind { none, r2c_f, r2c_b, c2c_f, c2c_b, r2r_ee_f, r2r_ee_b,
6161
r2r_oo_f, r2r_oo_b, r2r_eo, r2r_oe };
6262

63+
/**
64+
* \brief Return the default factor count used for selecting fast FFT lengths.
65+
*
66+
* The default is platform dependent. CUDA currently uses 5; other backends
67+
* currently use 6. This tuning default may change in the future.
68+
*
69+
* \return Default number of prime factors used by FFT::nextFastLen.
70+
*/
71+
[[nodiscard]] constexpr int
72+
FastNumPrimeFactors () noexcept
73+
{
74+
#if defined(AMREX_USE_CUDA)
75+
return 5;
76+
#else
77+
return 6;
78+
#endif
79+
}
80+
6381
struct Info
6482
{
6583
//! Domain composition strategy.
@@ -89,6 +107,13 @@ struct Info
89107
//! Max number of processes to use
90108
int nprocs = std::numeric_limits<int>::max();
91109

110+
//! Whether OpenBCSolver pads internal FFT lengths for better performance.
111+
bool openbc_padding = true;
112+
113+
//! Number of prime factors used by FFT::nextFastLen for OpenBCSolver padding.
114+
//! Defaults to FFT::FastNumPrimeFactors().
115+
int openbc_padding_nfactors = FastNumPrimeFactors();
116+
92117
/**
93118
* \brief Select how the domain is decomposed across MPI ranks.
94119
*
@@ -135,8 +160,112 @@ struct Info
135160
* \return Reference to this Info.
136161
*/
137162
Info& setNumProcs (int n) { nprocs = n; return *this; }
163+
/**
164+
* \brief Enable or disable OpenBCSolver internal FFT padding.
165+
*
166+
* \param x True to pad OpenBCSolver FFT lengths to fast sizes.
167+
* \return Reference to this Info.
168+
*/
169+
Info& setOpenBCPadding (bool x) { openbc_padding = x; return *this; }
170+
/**
171+
* \brief Set the factor count used for OpenBCSolver FFT padding.
172+
*
173+
* \param n Number of prime factors passed to FFT::nextFastLen. It must be between 3 and 6.
174+
* \return Reference to this Info.
175+
*/
176+
Info& setOpenBCPaddingNumPrimeFactors (int n)
177+
{
178+
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(
179+
3 <= n && 6 >= n,
180+
"amrex::FFT::Info::setOpenBCPaddingNumPrimeFactors: nfactors must be between 3 and 6");
181+
openbc_padding_nfactors = n;
182+
return *this;
183+
}
138184
};
139185

186+
/// \cond DOXYGEN_IGNORE
187+
namespace detail
188+
{
189+
inline void next_fast_len_doit (Long target, Long n, int const* factors,
190+
int nfactors, int ifactor, Long& best)
191+
{
192+
if (n >= target) {
193+
best = std::min(best, n);
194+
return;
195+
}
196+
197+
if (ifactor == nfactors) {
198+
// Factors 2 and 3 are handled here: try each n * 3^b, multiply it
199+
// by the smallest power of 2 that reaches target, and keep the best.
200+
Long const max_m3 = std::numeric_limits<int>::max() / 3;
201+
Long const max_m2 = std::numeric_limits<int>::max() / 2;
202+
for (Long m3 = n; m3 < best; ) {
203+
Long k = m3;
204+
bool overflow = false;
205+
while (k < target) {
206+
if (k > max_m2) {
207+
overflow = true;
208+
break;
209+
}
210+
k *= 2;
211+
}
212+
if (!overflow && k < best) {
213+
best = k;
214+
}
215+
if (m3 > max_m3) {
216+
break;
217+
}
218+
m3 *= 3;
219+
}
220+
return;
221+
}
222+
223+
int const factor = factors[ifactor];
224+
Long const max_len = std::numeric_limits<int>::max();
225+
Long const max_m = max_len / factor;
226+
for (Long m = n; m < best; ) {
227+
next_fast_len_doit(target, m, factors, nfactors, ifactor+1, best);
228+
if (m > max_m) {
229+
break;
230+
}
231+
m *= factor;
232+
}
233+
}
234+
}
235+
/// \endcond
236+
237+
/**
238+
* \brief Return the smallest fast FFT length greater than or equal to \p target.
239+
*
240+
* The allowed factors are the first \p nfactors values from {2, 3, 5, 7, 11, 13}.
241+
* When \p nfactors is omitted, FFT::FastNumPrimeFactors() provides a
242+
* platform-dependent default.
243+
*
244+
* \param target Minimum requested FFT length. It must be positive.
245+
* \param nfactors Number of supported prime factors. It must be between 3 and 6.
246+
* \return A fast FFT length no smaller than \p target.
247+
*/
248+
[[nodiscard]] inline int
249+
nextFastLen (int target, int nfactors = FastNumPrimeFactors())
250+
{
251+
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(target > 0,
252+
"amrex::FFT::nextFastLen: target must be positive");
253+
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(3 <= nfactors && 6 >= nfactors,
254+
"amrex::FFT::nextFastLen: nfactors must be between 3 and 6");
255+
256+
int constexpr factors[] = {5, 7, 11, 13, 17}; // The extra '17' is to make the buggy GCC -Warray-bounds quiet.
257+
258+
Long const max_len = std::numeric_limits<int>::max();
259+
Long best = max_len + Long(1);
260+
261+
// Factors 2 and 3 are handled directly in the base case.
262+
detail::next_fast_len_doit(target, Long(1), factors, nfactors-2, 0, best);
263+
264+
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(best <= max_len,
265+
"amrex::FFT::nextFastLen: result exceeds int range");
266+
return static_cast<int>(best);
267+
}
268+
140269
#ifdef AMREX_USE_HIP
141270
/// \cond DOXYGEN_IGNORE
142271
namespace detail { void hip_execute (rocfft_plan plan, void **in, void **out); }

Src/FFT/AMReX_FFT_OpenBCSolver.H

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,41 +60,78 @@ public:
6060
*/
6161
[[nodiscard]] Box const& Domain () const { return m_domain; }
6262

63+
/**
64+
* \brief Access the one-sided padded length used to build the internal FFT domain.
65+
*
66+
* \return Padded length before the open-boundary convolution domain is doubled.
67+
*/
68+
[[nodiscard]] IntVect const& PaddedLength () const { return m_padded_length; }
69+
6370
private:
64-
static Box make_grown_domain (Box const& domain, Info const& info);
71+
static IntVect make_padded_length (Box const& domain, Info const& info);
72+
static Box make_grown_domain (Box const& domain, IntVect const& padded_len,
73+
Info const& info);
6574

6675
Box m_domain;
6776
Info m_info;
77+
IntVect m_padded_length;
6878
R2C<T> m_r2c;
6979
cMF m_G_fft;
7080
std::unique_ptr<R2C<T>> m_r2c_green;
7181
};
7282

7383
template <typename T>
74-
Box OpenBCSolver<T>::make_grown_domain (Box const& domain, Info const& info)
84+
IntVect OpenBCSolver<T>::make_padded_length (Box const& domain, Info const& info)
7585
{
7686
IntVect len = domain.length();
87+
int ndims = AMREX_SPACEDIM;
88+
#if (AMREX_SPACEDIM == 3)
89+
if (info.twod_mode) { ndims = 2; }
90+
#else
91+
amrex::ignore_unused(info);
92+
#endif
93+
if (info.openbc_padding) {
94+
for (int idim = 0; idim < ndims; ++idim) {
95+
len[idim] = FFT::nextFastLen(len[idim], info.openbc_padding_nfactors);
96+
}
97+
}
98+
return len;
99+
}
100+
101+
template <typename T>
102+
Box OpenBCSolver<T>::make_grown_domain (Box const& domain, IntVect const& padded_len,
103+
Info const& info)
104+
{
105+
IntVect len = padded_len;
106+
int ndims = AMREX_SPACEDIM;
77107
#if (AMREX_SPACEDIM == 3)
78-
if (info.twod_mode) { len[2] = 0; }
108+
if (info.twod_mode) { ndims = 2; }
79109
#else
80110
amrex::ignore_unused(info);
81111
#endif
82-
return Box(domain.smallEnd(), domain.bigEnd()+len, domain.ixType());
112+
for (int idim = 0; idim < ndims; ++idim) {
113+
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(
114+
len[idim] <= std::numeric_limits<int>::max()/2,
115+
"FFT::OpenBCSolver: padded domain length exceeds int range");
116+
len[idim] *= 2;
117+
}
118+
return Box(domain.smallEnd(), domain.smallEnd()+len-IntVect(1), domain.ixType());
83119
}
84120

85121
template <typename T>
86122
OpenBCSolver<T>::OpenBCSolver (Box const& domain, Info const& info)
87123
: m_domain(domain),
88124
m_info(info),
89-
m_r2c(OpenBCSolver<T>::make_grown_domain(domain,info),
125+
m_padded_length(OpenBCSolver<T>::make_padded_length(domain, info)),
126+
m_r2c(OpenBCSolver<T>::make_grown_domain(domain, m_padded_length, info),
90127
m_info.setDomainStrategy(FFT::DomainStrategy::slab))
91128
{
92129
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(m_info.batch_size == 1,
93130
"FFT::OpenBCSolver does not support FFT::Info::batch_size > 1");
94131

95132
#if (AMREX_SPACEDIM == 3)
96133
if (m_info.twod_mode) {
97-
auto gdom = make_grown_domain(domain,m_info);
134+
auto gdom = make_grown_domain(domain, m_padded_length, m_info);
98135
gdom.enclosedCells(2);
99136
gdom.setSmall(2, 0);
100137
int nprocs = std::min({ParallelContext::NProcsSub(),
@@ -124,7 +161,8 @@ void OpenBCSolver<T>::setGreensFunction (F const& greens_function)
124161
: detail::get_fab(m_r2c.m_rx);
125162
auto const& lo = m_domain.smallEnd();
126163
auto const& lo3 = lo.dim3();
127-
auto const& len = m_domain.length3d();
164+
auto const len3d = m_padded_length.dim3();
165+
GpuArray<int,3> len{len3d.x, len3d.y, len3d.z};
128166
if (infab) {
129167
auto const& a = infab->array();
130168
auto box = infab->box();

Src/FFT/AMReX_FFT_Poisson.H

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,14 @@ public:
149149
* \param geom Geometry describing the problem domain.
150150
* \param ixtype Index type (cell vs. node) for the working arrays.
151151
* \param ngrow Number of grow cells applied prior to solving.
152+
* \param info OpenBC solver options.
152153
*/
153154
template <typename FA=MF>
154155
requires (IsFabArray_v<FA>)
155156
explicit PoissonOpenBC (Geometry const& geom,
156157
IndexType ixtype = IndexType::TheCellType(),
157-
IntVect const& ngrow = IntVect(0));
158+
IntVect const& ngrow = IntVect(0),
159+
Info const& info = Info{});
158160

159161
/**
160162
* \brief Solve the open-boundary Poisson problem.
@@ -169,7 +171,16 @@ public:
169171
*/
170172
void define_doit (); // has to be public for cuda
171173

174+
/**
175+
* \brief Access the one-sided padded length used by the internal OpenBC solver.
176+
*
177+
* \return Padded length before the open-boundary convolution domain is doubled.
178+
*/
179+
[[nodiscard]] IntVect const& PaddedLength () const { return m_solver.PaddedLength(); }
180+
172181
private:
182+
static Info make_solver_info (Info const& info);
183+
173184
Geometry m_geom;
174185
Box m_grown_domain;
175186
IntVect m_ngrow;
@@ -398,15 +409,24 @@ template <typename MF>
398409
template <typename FA>
399410
requires (IsFabArray_v<FA>)
400411
PoissonOpenBC<MF>::PoissonOpenBC (Geometry const& geom, IndexType ixtype,
401-
IntVect const& ngrow)
412+
IntVect const& ngrow, Info const& info)
402413
: m_geom(geom),
403414
m_grown_domain(amrex::grow(amrex::convert(geom.Domain(),ixtype),ngrow)),
404415
m_ngrow(ngrow),
405-
m_solver(m_grown_domain)
416+
m_solver(m_grown_domain, make_solver_info(info))
406417
{
407418
define_doit();
408419
}
409420

421+
template <typename MF>
422+
Info PoissonOpenBC<MF>::make_solver_info (Info const& info)
423+
{
424+
Info solver_info;
425+
solver_info.openbc_padding = info.openbc_padding;
426+
solver_info.openbc_padding_nfactors = info.openbc_padding_nfactors;
427+
return solver_info;
428+
}
429+
410430
template <typename MF>
411431
void PoissonOpenBC<MF>::define_doit ()
412432
{

0 commit comments

Comments
 (0)