Skip to content

Commit 9dfde8e

Browse files
alfCclaude
andcommitted
fft: apply plans on cursors (.home()), the complement of extents
A plan holds the shape (extents + precomputed tables); it is now applied on a cursor -- base + strides, no shape of its own: plan(A.home()); // primitive: cursor gives base+strides, plan gives shape plan(A); // sugar: checks A's extents == plan, then plan(A.home()) plan(cursor) rebuilds a strided view from the plan's extents and the cursor (via layout_t's size = nelems/stride invariant) and drives the unchanged orchestration; plan(array) delegates to it. Both entry points are SFINAE- disjoint (cursor = base()/strides() but not multi-like). Returns the cursor by value (safe when built from a temporary .home()). This is the GPU-facing execution primitive: a device cursor (device pointer base + strides) flows through plan(cursor) unchanged, so the memory-space dispatch can hang off it (see fft.NOTES.md section 8). fft.hpp now includes array_ref.hpp for layout_t/subarray and compiles standalone. Perf unchanged (reconstruction is one-time per execute). Verified: full suite + new cursor cases under GCC -Werror strict, Clang, _GLIBCXX_DEBUG, and libc++ hardening + ASan/UBSan; clang-tidy and clang-format clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent eef2da0 commit 9dfde8e

3 files changed

Lines changed: 160 additions & 24 deletions

File tree

include/boost/multi/algorithms/fft.NOTES.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,27 @@ are computed once and reused across repeated transforms):
2020
// reusable plan: tables + scratch built once, executed many times
2121
multi::fft_plan<complex, 2> plan{multi::extents_t<2>{1024, 1024}, multi::fft_forward};
2222
multi::fft_plan plan2{A, multi::fft_forward}; // CTAD from a prototype array
23-
plan(A); // in place; A: any array/subarray with the planned sizes,
24-
plan.execute(B); // of ANY strided layout, repeatedly, no re-allocation
23+
24+
// the plan holds the *extents*; it is applied on a *cursor* -- base + strides,
25+
// the complement of extents -- obtained from any array/subarray with `.home()`:
26+
plan(A.home()); // primitive form: cursor supplies base+strides, plan supplies shape
27+
plan(A); // sugar: checks A.extensions() == plan shape, then applies on A.home()
28+
plan.execute(B); // same, in place, any strided layout, repeatedly, no re-allocation
2529

2630
// one-shot convenience (builds a throw-away plan)
2731
multi::fft_inplace(A, sign); // also fft_inplace_forward / fft_inplace_backward
2832
```
2933
34+
The plan/cursor split is deliberate: a plan is *shape + precomputed tables*, and
35+
a cursor (`.home()`) is *where the data lives* (base pointer + strides), with no
36+
shape of its own. `plan(cursor)` trusts the cursor to have the planned shape (a
37+
cursor carries no sizes to check); `plan(array)` is the checked convenience that
38+
extracts `array.home()`. This is also the GPU-facing shape: a cursor is exactly
39+
what a device kernel receives (base + strides), so the same execution primitive
40+
carries over (see section 8). Internally `plan(cursor)` rebuilds a strided view
41+
from the plan's extents + the cursor via `layout_t`'s size = nelems/stride
42+
invariant; that view then drives the unchanged orchestration.
43+
3044
Properties:
3145
3246
- **No external dependency** (no FFTW/cuFFT). Pure C++17 + standard library.
@@ -414,7 +428,10 @@ layout-agnostic; and plans already separate "build tables once" (host) from
414428
2. **`fft_memory_space<Ptr>` trait** dispatching host (raw `T*`, current
415429
paths) vs device (thrust/Multi-CUDA fancy pointers), specialized in an
416430
opt-in adaptor header so the core stays CUDA-free. The current
417-
host-iterator gather fallback must *never* run on device pointers.
431+
host-iterator gather fallback must *never* run on device pointers. The
432+
entry point is already the right one: `plan(cursor)` accepts any pointer
433+
type in `cursor.base()`, so a device cursor flows in unchanged and the
434+
trait picks the device orchestration.
418435
3. **Device table mirror**: keep host `std::vector` tables; add a lazily
419436
uploaded POD `engine_view` (raw device pointers + sizes) per engine.
420437
4. **Device stage launchers**: one `__global__` kernel per stage kind around

include/boost/multi/algorithms/fft.hpp

Lines changed: 98 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
#ifndef BOOST_MULTI_ALGORITHMS_FFT_HPP
5757
#define BOOST_MULTI_ALGORITHMS_FFT_HPP
5858

59+
#include <boost/multi/array_ref.hpp> // for layout_t and subarray (cursor -> strided view reconstruction)
60+
5961
#include <algorithm> // for copy, fill, min, max, find_if
6062
#include <array> // for plan sizes
6163
#include <cassert> // for assert
@@ -1129,6 +1131,56 @@ void fft_apply_last(ViewND&& view, fft_engine<T> const& eng) { // NOLINT(cppcor
11291131
}
11301132
}
11311133

1134+
// A Multi cursor (`.home()`) is base + strides with no extents. These helpers
1135+
// recover its rank and rebuild a full strided view once the extents (which the
1136+
// plan owns) are supplied -- the "extents (plan) + cursor (target)" split.
1137+
template<class Cursor>
1138+
using fft_cursor_strides_t = std::decay_t<decltype(std::declval<Cursor>().strides())>;
1139+
1140+
// A cursor exposes base()/strides() but, unlike an array/subarray, no sizes().
1141+
template<class C, class = void> struct fft_is_cursor_like : std::false_type {};
1142+
template<class C>
1143+
struct fft_is_cursor_like<C, std::void_t<decltype(std::declval<C>().base()), decltype(std::declval<C>().strides()), std::enable_if_t<!fft_is_multi_like<C>::value>>> : std::true_type {};
1144+
1145+
template<class Cursor>
1146+
inline constexpr std::ptrdiff_t fft_cursor_rank = static_cast<std::ptrdiff_t>(std::tuple_size_v<fft_cursor_strides_t<Cursor>>);
1147+
1148+
// Build layout_t<D> from extents and strides (all offsets 0, as for a home
1149+
// cursor): each level carries nelems = size*stride, which is the invariant
1150+
// layout_t uses to recover size() = nelems/stride and extent() bounds.
1151+
template<std::ptrdiff_t D>
1152+
auto fft_layout_from(std::array<std::size_t, static_cast<std::size_t>(D)> const& ext, std::array<std::ptrdiff_t, static_cast<std::size_t>(D)> const& str) -> multi::layout_t<D> {
1153+
if constexpr(D == 0) {
1154+
return multi::layout_t<0>{};
1155+
} else {
1156+
std::array<std::size_t, static_cast<std::size_t>(D) - 1> sub_ext{};
1157+
std::array<std::ptrdiff_t, static_cast<std::size_t>(D) - 1> sub_str{};
1158+
for(std::size_t i = 0; i != static_cast<std::size_t>(D) - 1; ++i) {
1159+
sub_ext.at(i) = ext.at(i + 1);
1160+
sub_str.at(i) = str.at(i + 1);
1161+
}
1162+
return multi::layout_t<D>{
1163+
fft_layout_from<D - 1>(sub_ext, sub_str),
1164+
str[0], 0,
1165+
static_cast<std::ptrdiff_t>(ext[0]) * str[0]
1166+
}; // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index) [0] is valid for D>=1
1167+
}
1168+
}
1169+
1170+
template<class Cursor, std::size_t... Is>
1171+
auto fft_strides_array(Cursor const& cur, std::index_sequence<Is...> /*unused*/)
1172+
-> std::array<std::ptrdiff_t, sizeof...(Is)> {
1173+
return {{static_cast<std::ptrdiff_t>(cur.template stride<static_cast<multi::dimensionality_type>(Is)>())...}};
1174+
}
1175+
1176+
// (cursor, extents) -> strided subarray sharing the cursor's memory.
1177+
template<class T, std::ptrdiff_t D, class Cursor>
1178+
auto fft_view_from_cursor(Cursor const& cur, std::array<std::size_t, static_cast<std::size_t>(D)> const& ext) {
1179+
auto const str = fft_strides_array(cur, std::make_index_sequence<static_cast<std::size_t>(D)>{});
1180+
using ptr_type = typename Cursor::element_ptr;
1181+
return multi::subarray<T, D, ptr_type>{fft_layout_from<D>(ext, str), cur.base()};
1182+
}
1183+
11321184
} // end namespace detail
11331185

11341186
// Reusable multidimensional FFT plan: precomputes twiddle tables, stage
@@ -1202,32 +1254,57 @@ class fft_plan {
12021254
explicit fft_plan(MultiSubArray const& arr, int sign = fft_forward)
12031255
: sizes_{to_sizes_(arr.sizes(), std::make_index_sequence<static_cast<std::size_t>(D)>{})}, sign_{sign} { init_(); }
12041256

1205-
auto sign() const -> int { return sign_; }
1206-
1207-
// Execute the plan on `arr` in place. `arr` must have the planned sizes;
1208-
// its layout (strides, subarray-ness) is free to differ between calls.
1209-
template<class MultiSubArray>
1210-
auto operator()(MultiSubArray&& arr) const -> MultiSubArray&& { // NOLINT(cppcoreguidelines-missing-std-forward)
1211-
static_assert(std::decay_t<MultiSubArray>::dimensionality == D, "array rank must match the plan");
1212-
assert(matches_(arr.sizes(), std::make_index_sequence<static_cast<std::size_t>(D)>{}));
1257+
private:
1258+
// The axis walk, shared by the cursor and array entry points. `view` is a
1259+
// strided view of the planned shape; transformed in place.
1260+
template<class View>
1261+
void apply_(View&& view) const { // NOLINT(cppcoreguidelines-missing-std-forward)
12131262
if constexpr(D == 1) {
1214-
detail::fft_apply_last(arr(), engine_<0>());
1263+
detail::fft_apply_last(view, engine_<0>());
12151264
} else {
12161265
// Transform the last two axes together, slab by slab (cache
12171266
// locality), then the remaining axes 0 .. D-3 by static recursion
12181267
// over rotated views (each axis bound to its engine at compile
12191268
// time; no runtime axis parameter anywhere).
1220-
detail::fft_apply_last_pair(arr(), engine_<D - 1>(), engine_<D - 2>());
1269+
detail::fft_apply_last_pair(view, engine_<D - 1>(), engine_<D - 2>());
12211270
if constexpr(D >= 3) {
1222-
transform_middle_<1>(arr().rotated());
1271+
transform_middle_<1>(view.rotated());
12231272
}
12241273
}
1274+
}
1275+
1276+
public:
1277+
auto sign() const -> int { return sign_; }
1278+
1279+
auto sizes() const -> std::array<std::size_t, static_cast<std::size_t>(D)> const& { return sizes_; }
1280+
1281+
// Apply the plan on a cursor (`A.home()`) -- base + strides, no extents.
1282+
// The plan supplies the extents, so the cursor's rank must match and its
1283+
// shape is *trusted* to be the planned one (a cursor carries no sizes to
1284+
// check). This is the primitive execution form; the array overload below
1285+
// delegates to it. Transforms in place; returns the cursor.
1286+
template<class Cursor, std::enable_if_t<detail::fft_is_cursor_like<std::decay_t<Cursor>>::value, int> = 0> // NOLINT(modernize-use-constraints) C++17
1287+
auto operator()(Cursor const& home) const -> Cursor {
1288+
static_assert(detail::fft_cursor_rank<std::decay_t<Cursor>> == D, "cursor rank must match the plan");
1289+
auto view = detail::fft_view_from_cursor<T, D>(home, sizes_);
1290+
apply_(view);
1291+
return home; // cursors are value types (base + strides); returned by value
1292+
}
1293+
1294+
// Execute the plan on `arr` in place. `arr` must have the planned sizes;
1295+
// its layout (strides, subarray-ness) is free to differ between calls.
1296+
// Delegates to the cursor form via `arr.home()`.
1297+
template<class MultiSubArray, std::enable_if_t<detail::fft_is_multi_like<std::decay_t<MultiSubArray>>::value, int> = 0> // NOLINT(modernize-use-constraints) C++17
1298+
auto operator()(MultiSubArray&& arr) const -> MultiSubArray&& { // NOLINT(cppcoreguidelines-missing-std-forward)
1299+
static_assert(std::decay_t<MultiSubArray>::dimensionality == D, "array rank must match the plan");
1300+
assert(matches_(arr.sizes(), std::make_index_sequence<static_cast<std::size_t>(D)>{}));
1301+
operator()(arr.home());
12251302
return std::forward<MultiSubArray>(arr);
12261303
}
12271304

1228-
template<class MultiSubArray>
1229-
auto execute(MultiSubArray&& arr) const -> MultiSubArray&& {
1230-
return operator()(std::forward<MultiSubArray>(arr));
1305+
template<class Target>
1306+
auto execute(Target&& tgt) const -> decltype(auto) {
1307+
return operator()(std::forward<Target>(tgt));
12311308
}
12321309
};
12331310

@@ -1241,20 +1318,20 @@ fft_plan(MultiSubArray const&) -> fft_plan<typename MultiSubArray::element, Mult
12411318
// throw-away plan; for repeated transforms of the same shape, build an
12421319
// fft_plan once and execute it).
12431320
template<class MultiSubArray>
1244-
auto fft_inplace(MultiSubArray&& A, int sign = fft_forward) -> MultiSubArray&& {
1321+
auto fft_inplace(MultiSubArray&& arr, int sign = fft_forward) -> MultiSubArray&& {
12451322
using array_type = std::decay_t<MultiSubArray>;
1246-
fft_plan<typename array_type::element, array_type::dimensionality> const plan{A, sign};
1247-
return plan(std::forward<MultiSubArray>(A));
1323+
fft_plan<typename array_type::element, array_type::dimensionality> const plan{arr, sign};
1324+
return plan(std::forward<MultiSubArray>(arr));
12481325
}
12491326

12501327
template<class MultiSubArray>
1251-
auto fft_inplace_forward(MultiSubArray&& A) -> MultiSubArray&& {
1252-
return fft_inplace(std::forward<MultiSubArray>(A), fft_forward);
1328+
auto fft_inplace_forward(MultiSubArray&& arr) -> MultiSubArray&& {
1329+
return fft_inplace(std::forward<MultiSubArray>(arr), fft_forward);
12531330
}
12541331

12551332
template<class MultiSubArray>
1256-
auto fft_inplace_backward(MultiSubArray&& A) -> MultiSubArray&& {
1257-
return fft_inplace(std::forward<MultiSubArray>(A), fft_backward);
1333+
auto fft_inplace_backward(MultiSubArray&& arr) -> MultiSubArray&& {
1334+
return fft_inplace(std::forward<MultiSubArray>(arr), fft_backward);
12581335
}
12591336

12601337
} // end namespace boost::multi

test/algorithms_fft.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,48 @@ auto main() -> int { // NOLINT(readability-function-cognitive-complexity,bugpro
327327
BOOST_TEST( m / static_cast<double>(n) < tol );
328328
}
329329

330+
// plan applied on a cursor (.home()): extents live in the plan, the cursor
331+
// supplies base + strides
332+
{
333+
multi::array<complex, 2> arr({6, 10}, complex{});
334+
for(int i = 0; i != 6; ++i) {
335+
for(int j = 0; j != 10; ++j) {
336+
arr[i][j] = complex{static_cast<double>(i - j), static_cast<double>((i * j) % 7)};
337+
}
338+
}
339+
auto reference = arr;
340+
341+
multi::fft_plan<complex, 2> const plan{
342+
multi::extents_t<2>{6, 10},
343+
multi::fft_forward
344+
};
345+
plan(arr.home()); // apply on the cursor directly
346+
plan(reference); // apply on the array (delegates to .home())
347+
BOOST_TEST( max_abs_diff(arr.elements(), reference.elements()) < tol );
348+
}
349+
350+
// cursor application on a strided sub-block matches the same plan on a contiguous copy
351+
{
352+
multi::array<complex, 3> big({6, 7, 8}, complex{});
353+
int c = 0;
354+
for(auto& e : big.elements()) {
355+
e = complex{static_cast<double>(c % 11) - 5.0, static_cast<double>(c % 7) - 3.0};
356+
++c;
357+
}
358+
auto&& blk = big({1, 5}, {2, 6}, {1, 7}); // 4 x 4 x 6 strided view
359+
360+
multi::array<complex, 3> flat{blk}; // contiguous copy of the same values
361+
362+
multi::fft_plan<complex, 3> const plan{
363+
multi::extents_t<3>{4, 4, 6},
364+
multi::fft_forward
365+
};
366+
plan(flat.home()); // contiguous cursor
367+
plan(blk.home()); // strided cursor, same plan
368+
369+
BOOST_TEST( max_abs_diff(blk.elements(), flat.elements()) < tol );
370+
}
371+
330372
// 3D round-trip
331373
{
332374
multi::array<complex, 3> arr({3, 4, 5}, complex{});

0 commit comments

Comments
 (0)