Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

589 lines
27 KiB
C++

// Copyright (C) 2025 Advanced Micro Devices, Inc. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#ifndef CLIENT_DATA_LAYOUT_HELPERS_H
#define CLIENT_DATA_LAYOUT_HELPERS_H
#include "fft_enums.h"
#include <algorithm>
#include <array>
#include <iterator>
#include <limits>
#include <numeric>
#include <optional>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <vector>
// type traits to distinguish between std::array and other supposedly dynamically-sized containers
template <typename C>
struct is_std_array : std::false_type
{
};
template <typename T, std::size_t N>
struct is_std_array<std::array<T, N>> : std::true_type
{
};
/**
* @brief calculates the default strides for a given Discrete Fourier transform (observing row-major convention by default).
* @note This function assumes interleaved complex representation for hermitian-symmetric data.
* @note Empty strides are returned for empty lengths.
*
* @tparam C container type for the transform's lengths (container's value_type must be an integral type).
* The same type is used for the returned strides.
* @param[in] dft_type type of transform
* @param[in] placement placement of the transform
* @param[in] io input/output flag
* @param[in] lengths container of lengths for the transform of interest
* @param[in] dim_order optional vector of dimension ordering for ``lengths``. If used, the content
* of this vector must be a permutation of {0, 1, ..., lengths.size() - 1} (:= default natural order)
* and the corresponding dimension ordering is observed when generating the strides. For instance,
* column-major ordering is observed when setting ``dim_order`` to {lengths.size()-1, ..., 1, 0}.
* @return desired default strides (same type as ``lengths`` input argument)
*/
template <typename C, std::enable_if_t<std::is_integral_v<typename C::value_type>, bool> = true>
static C default_strides(fft_transform_type dft_type,
fft_result_placement placement,
fft_io io,
const C& lengths,
const std::optional<std::vector<size_t>>& dim_order = std::nullopt)
{
validate_enums_or_throw("default_strides", dft_type, placement, io);
if(dim_order)
{
if(dim_order->size() != lengths.size())
throw std::invalid_argument(
"default_strides: size mismatch between dim_order and lengths");
std::vector<size_t> natural_order(lengths.size());
std::iota(natural_order.begin(), natural_order.end(), 0);
if(!std::is_permutation(dim_order->begin(), dim_order->end(), natural_order.begin()))
throw std::invalid_argument("default_strides: invalid dim_order (not a permutation of "
"{0, 1, ..., lengths.size() - 1}");
}
C ret(lengths);
typename C::value_type def_stride = 1;
for(auto l_idx = lengths.size(); l_idx-- > 0;)
{
const auto dim_idx = dim_order ? dim_order->at(l_idx) : l_idx;
ret[dim_idx] = def_stride;
if(dim_idx == lengths.size() - 1 && is_real(dft_type))
{
if((io == fft_io_out) == is_fwd(dft_type))
def_stride *= (lengths[dim_idx] / 2 + 1);
else
{
if(placement == fft_placement_inplace)
def_stride *= 2 * (lengths[dim_idx] / 2 + 1);
else
def_stride *= lengths[dim_idx];
}
}
else
def_stride *= lengths[dim_idx];
}
return ret;
}
/**
* @brief calculates the default distances for a given Discrete Fourier transform,
* batched possibly multiple times (observing row-major convention by default).
* @note This function assumes interleaved complex representation for hermitian-symmetric data.
* @note Empty distances are returned for empty batches.
*
* @tparam T integral value type for lengths and batches, the same type is used for the returned distances
* @param[in] dft_type type of transform
* @param[in] placement placement of the transform
* @param[in] io input/output flag
* @param[in] lengths vector of lengths for the transform of interest
* @param[in] batches vector of (possibly many) batch sizes for the transform of interest
* @param[in] len_dim_order optional vector of dimension ordering for ``lengths``. If used, the content
* of this vector must be a permutation of {0, 1, ..., lengths.size() - 1} (:= default natural order)
* and the corresponding dimension ordering is observed when generating the strides. For instance,
* column-major ordering is observed when setting ``len_dim_order`` to {lengths.size()-1, ..., 1, 0}.
* @return std::vector<T> desired default distances (of size batches.size())
*/
template <typename T, std::enable_if_t<std::is_integral_v<T>, bool> = true>
static std::vector<T> default_distances(fft_transform_type dft_type,
fft_result_placement placement,
fft_io io,
const std::vector<T>& lengths,
const std::vector<T>& batches,
const std::optional<std::vector<size_t>>& len_dim_order
= std::nullopt)
{
validate_enums_or_throw("default_distances", dft_type, placement, io);
if(batches.empty())
return std::vector<T>(); // empty as well
auto temp_lengths = batches;
std::copy(lengths.begin(), lengths.end(), std::back_inserter(temp_lengths));
std::vector<T> ret;
if(!len_dim_order)
{
ret = default_strides(dft_type, placement, io, temp_lengths);
}
else
{
if(len_dim_order->size() != lengths.size())
throw std::invalid_argument(
"default_distances: size mismatch between len_dim_order and lengths");
std::vector<size_t> dim_order(batches.size() + lengths.size());
std::iota(dim_order.begin(), dim_order.begin() + batches.size(), 0);
for(size_t len_idx = 0; len_idx < lengths.size(); len_idx++)
dim_order[batches.size() + len_idx] = batches.size() + len_dim_order->at(len_idx);
ret = default_strides(dft_type, placement, io, temp_lengths, dim_order);
}
ret.resize(batches.size());
return ret;
}
/**
* @brief equivalent of default_distances for one-dimensional batches
*/
template <typename T, std::enable_if_t<std::is_integral_v<T>, bool> = true>
static T default_distance(fft_transform_type dft_type,
fft_result_placement placement,
fft_io io,
const std::vector<T>& lengths,
const T& batch_sz,
const std::optional<std::vector<size_t>>& len_dim_order = std::nullopt)
{
validate_enums_or_throw("default_distance", dft_type, placement, io);
if(lengths.empty())
throw std::invalid_argument("empty lengths rejected by default_distance");
const auto tmp = default_distances(
dft_type, placement, io, lengths, std::vector<T>(1, batch_sz), len_dim_order);
return tmp.front();
}
struct ionembed_exception : public std::runtime_error
{
ionembed_exception()
: std::runtime_error("")
{
}
};
/**
* @brief exception thrown by ionembed_t objects when a type conversion error is detected.
*/
struct ionembed_type_conversion_error : public ionembed_exception
{
using ionembed_exception::ionembed_exception;
};
/**
* @brief exception thrown at construction of ionembed_t objects from generalized strides,
* if/when those strides are incompatible with the "advanced layout" parameters.
*/
struct strides_are_not_nembed_compatible : ionembed_exception
{
using ionembed_exception::ionembed_exception;
};
/**
* @brief helper structure for converting hipFFT or hipFFTW advanced plans data
* layout parameters [istride, ostride, inembed, onembed] to/from general strides.
* @note This structure observes row-major ordering convention.
*
* @tparam T integral type to be considered for the istride, ostride, inembed,
* and onembed structure members.
* @tparam ignore_elementary_stride_if_null_nembed boolean flag distinguishing whether the
* elementary strides istride or ostride are ignored (and assumed equal to 1) whenever inembed
* or onembed is not used (aka "implicit default").
* If set to false, elementary strides are not ignored (behavior consistent with FFTW3 and hipFFTW).
* If set to true, elementary strides are ignored (behavior consistent with hipFFT and cuFFT).
*/
template <typename T,
bool ignore_elementary_stride_if_null_nembed,
std::enable_if_t<std::is_integral_v<T>, bool> = true>
struct ionembed_t
{
// default constructor := default layout
ionembed_t()
: istride(T(1))
, ostride(T(1))
, inembed(std::nullopt)
, onembed(std::nullopt)
{
}
/**
* @brief Construct a new ionembed t object
*
* @tparam U value type for the constructor's input arguments (may be different than
* the internally-used value type).
* @param[in] istride_ elementary stride for input data (along the last dimension);
* @param[in] inembed_ vector of input-embedding lengths, implicit default embedding is used if empty;
* @param[in] ostride_ elementary stride for output data (along the last dimension);
* @param[in] onembed_ vector of output-embedding lengths, implicit default embedding is used if empty;
* @note: if non empty, the sizes of inembed_ and onembed_ must be equal (an std::invalid_argument
* exception is thrown otherwise)
* @note: if U is different than the internally-used value type, a ionembed_type_conversion_error may
* be thrown by this constructor.
*/
template <typename U, std::enable_if_t<std::is_integral_v<U>, bool> = true>
ionembed_t(U istride_,
const std::vector<U>& inembed_,
U ostride_,
const std::vector<U>& onembed_)
: istride(convert_to<T>(istride_))
, ostride(convert_to<T>(ostride_))
, inembed(make_optional_nembed_vector(inembed_))
, onembed(make_optional_nembed_vector(onembed_))
{
if(inembed && onembed && inembed->size() != onembed->size())
throw std::invalid_argument("inconsistent sizes of inembed and onembed used when "
"constructing an ionembed_t object");
}
/**
* @brief Construct a new ionembed t object
*
* @tparam U value type for the constructor's input arguments (may be different than
* the internally-used value type).
* @param[in] rank dimensionality of the transform (must be strictly positive unless both inembed_ and onembed_ are ``nullptr``);
* @param[in] istride_ elementary stride for input data (along the last dimension);
* @param[in] inembed_ array of ``rank`` input-embedding lengths, implicit default embedding is used if nullptr;
* @param[in] ostride_ elementary stride for output data (along the last dimension);
* @param[in] onembed_ array of ``rank`` output-embedding lengths, implicit default embedding is used if nullptr;
* @note: if U is different than the internally-used value type, a ionembed_type_conversion_error may
* be thrown by this constructor.
*/
template <typename U, std::enable_if_t<std::is_integral_v<U>, bool> = true>
ionembed_t(int rank, U istride_, const U* inembed_, U ostride_, const U* onembed_)
: ionembed_t(istride_,
make_nembed_vector_from_array(rank, inembed_),
ostride_,
make_nembed_vector_from_array(rank, onembed_))
{
}
/**
* @brief Construct a new ionembed t object from generalized strides, if compatible.
*
* @tparam U value type for the constructor's input arguments (may be different than
* the internally-used value type).
* @param[in] istrides generalized input strides (row-major ordering)
* @param[in] ostrides generalized output strides (row-major ordering)
* @param[in] lengths transform lengths (row-major ordering)
* @param[in] dft_kind type of the discrete Fourier transform of interest.
* @param[in] placement placement of the discrete Fourier transform of interest.
* @param[in] use_nullptr_for_default_inembed self-exaplanatory boolean flag
* @param[in] use_nullptr_for_default_onembed self-exaplanatory boolean flag
* @note this constructor may throw
* - a ionembed_type_conversion_error exception if a U value cannot be safely
* converted to the internally-used value type;
* - a strides_are_not_nembed_compatible exception if the generalized strides are
* not compatible with an "advanced layout" parameterization.
*/
template <typename U, std::enable_if_t<std::is_integral_v<U>, bool> = true>
ionembed_t(const std::vector<U>& istrides,
const std::vector<U>& ostrides,
const std::vector<U>& lengths,
fft_transform_type dft_kind,
fft_result_placement placement,
bool use_nullptr_for_default_inembed,
bool use_nullptr_for_default_onembed)
: istride(convert_to<T>(istrides.empty() ? 1 : istrides.back()))
, ostride(convert_to<T>(ostrides.empty() ? 1 : ostrides.back()))
, inembed(calc_nembed_from_generalized_strides<U>(istrides,
lengths,
fft_io::fft_io_in,
dft_kind,
placement,
use_nullptr_for_default_inembed))
, onembed(calc_nembed_from_generalized_strides<U>(ostrides,
lengths,
fft_io::fft_io_out,
dft_kind,
placement,
use_nullptr_for_default_onembed))
{
// istrides.size() == lengths.size() and ostrides.size() == lengths.size() verified
// separately in initialization steps, so istrides.size() == ostrides.size()
}
// default copy and move constructors/assignment operators
ionembed_t(const ionembed_t& other) = default;
ionembed_t& operator=(const ionembed_t& other) = default;
ionembed_t(ionembed_t&& other) = default;
ionembed_t& operator=(ionembed_t&& other) = default;
/**
* @brief strict equality comparison operator.
* @note While unset {i,o}nembed are conceptually equivalent to explicitly-set default values,
* this operator cannot verify that possibility without additional information (like the kind
* of transform and its placement): it's a simple member-wise equality check.
* @return true iff the two structures have equal members
*/
bool operator==(const ionembed_t& other) const
{
return istride == other.istride && ostride == other.ostride && inembed == other.inembed
&& onembed == other.onembed;
}
bool operator!=(const ionembed_t& other) const
{
return !(*this == other);
}
/**
* @brief calculates generalized (row-major) strides from the advanced plan's data layout parameters
*
* @tparam C container type for the generalized strides and the transform's lengths (container's
* value_type must be an integral type).
* @param[in] io flag for requesting input (fft_io::fft_io_in) or output (fft_io::fft_io_out) strides.
* @param[in] dft_kind type of the discrete Fourier transform of interest.
* @param[in] placement placement of the discrete Fourier transform of interest.
* @param[in] lengths lengths of the discrete Fourier transform of interest (row-major ordering).
* @return generalized strides calculated from the advanced plan's data layout parameters
* or from the provided args if implicit default layout is used. ``dft_kind``, ``placement``,
* and ``lengths`` are all ignored if implicit default layout is NOT used (i.e., if
* {i,o}nembed is explicitly set internally). The returned value is of the same type as ``lengths``.
* @note a ionembed_type_conversion_error may be thrown if an error is detected when converting
* internally-calculated values to C::value_type (e.g. if a negative value is to be assigned but
* C::value_type is unsigned, or if C::value_type := int32_t and T := int64_t and an overflow/underflow
* is detected internally).
*/
template <typename C, std::enable_if_t<std::is_integral_v<typename C::value_type>, bool> = true>
C as_generalized_strides(fft_io io,
fft_transform_type dft_kind,
fft_result_placement placement,
const C& lengths) const
{
using C_val_t = typename C::value_type;
validate_enums_or_throw("ionembed_t::as_generalized_strides", io, dft_kind, placement);
const auto& nembed = io == fft_io::fft_io_in ? inembed : onembed;
const C_val_t elem_stride
= convert_to<C_val_t>(io == fft_io::fft_io_in ? istride : ostride);
if(!nembed)
{
// --> default strides, use lengths, type of transform, etc.
// note: value type used below is same of lengths', i.e., C_val_t
auto ret = default_strides(dft_kind, placement, io, lengths);
if constexpr(!ignore_elementary_stride_if_null_nembed)
{
if(elem_stride != 1)
std::for_each(
ret.begin(), ret.end(), [&](C_val_t& val) { val *= elem_stride; });
}
return ret;
}
C ret;
if constexpr(is_std_array<C>::value)
{
if(ret.size() != nembed->size())
throw std::invalid_argument(
"Incompatible size of std::array ``lengths`` w.r.t. internal {i,o}nembed "
"detected in ionembed_t::as_generalized_strides");
}
else
{
ret.resize(nembed->size());
}
ret.back() = elem_stride;
for(auto dim = ret.size() - 1; dim-- > 0;)
{
ret[dim] = ret[dim + 1] * convert_to<C_val_t>(nembed->at(dim + 1));
}
return ret;
}
const T* get_nembed(fft_io io) const
{
validate_enums_or_throw("ionembed_t::get_nembed", io);
const auto& tmp = io == fft_io::fft_io_in ? inembed : onembed;
if(!tmp)
return nullptr;
return tmp->data();
}
size_t get_nembed_size(fft_io io) const
{
validate_enums_or_throw("ionembed_t::get_nembed", io);
const auto& tmp = io == fft_io::fft_io_in ? inembed : onembed;
if(!tmp)
return 0;
return tmp->size();
}
T get_elementary_stride(fft_io io) const
{
validate_enums_or_throw("ionembed_t::get_elementary_stride", io);
return io == fft_io::fft_io_in ? istride : ostride;
}
private:
using nembed_t = std::optional<std::vector<T>>;
T istride;
T ostride;
nembed_t inembed;
nembed_t onembed;
template <typename U, std::enable_if_t<std::is_integral_v<U>, bool> = true>
static nembed_t make_optional_nembed_vector(const std::vector<U>& nembed_vec)
{
if(nembed_vec.empty())
return std::nullopt;
typename nembed_t::value_type ret(nembed_vec.size());
for(auto dim = ret.size(); dim-- > 0;)
ret[dim] = convert_to<T>(nembed_vec[dim]);
return nembed_t(ret);
}
template <typename U, std::enable_if_t<std::is_integral_v<U>, bool> = true>
static std::vector<U> make_nembed_vector_from_array(int rank, const U* nembed_array)
{
std::vector<U> ret;
if(!nembed_array)
return ret;
if(rank < 0)
throw std::invalid_argument(
"invalid rank encountered by ionembed_t::make_nembed_vector_from_array");
ret.assign(nembed_array, nembed_array + rank);
return ret;
}
template <typename U, std::enable_if_t<std::is_integral_v<U>, bool> = true>
static nembed_t calc_nembed_from_generalized_strides(const std::vector<U>& strides,
const std::vector<U>& lengths,
fft_io io,
fft_transform_type dft_kind,
fft_result_placement placement,
bool use_nullptr_if_default)
{
validate_enums_or_throw("ionembed_t::calc_nembed_from_strides", io, dft_kind, placement);
if(strides.size() != lengths.size())
throw std::invalid_argument("inconsistent strides.size() vs lengths.size() encountered "
"by ionembed_t::calc_nembed_from_generalized_strides");
if(strides.empty())
{
// degenerate case: lengths and strides are empty (possible use case supposedly considered
// only for edge-case testing)
// --> no actual value can (nor should) be legitimately set
return std::nullopt;
}
const auto elem_stride = strides.back();
const auto def_strides = default_strides(dft_kind, placement, io, lengths);
if(use_nullptr_if_default
&& std::equal(strides.begin(), strides.end(), def_strides.begin(), [&](U s, U def_s) {
if constexpr(ignore_elementary_stride_if_null_nembed)
return s == def_s;
else
return s == elem_stride * def_s;
}))
{
return std::nullopt;
}
else
{
typename nembed_t::value_type nembed(strides.size());
for(auto nembed_dim = nembed.size(); nembed_dim-- > 0;)
{
U nembed_val;
if(nembed_dim > 0)
{
if(strides[nembed_dim] == 0
|| strides[nembed_dim - 1] % strides[nembed_dim] != 0)
throw strides_are_not_nembed_compatible();
nembed_val = strides[nembed_dim - 1] / strides[nembed_dim];
}
else
{
// nembed_dim == 0, i.e., actually irrelevant/unused value...
// set it to the minimum value documented to be valid by FFTW3
// to guard against (reference) plan creation failures
if(is_real(dft_kind) && lengths.size() == 1)
{
const auto cmplx_stride = lengths[nembed_dim] / 2 + 1;
if((io == fft_io_in) == is_bwd(dft_kind))
nembed_val = cmplx_stride;
else
{
if(placement == fft_placement_inplace)
nembed_val = 2 * cmplx_stride;
else
nembed_val = lengths[nembed_dim];
}
}
else
nembed_val = lengths[nembed_dim];
}
nembed[nembed_dim] = convert_to<T>(nembed_val);
}
return nembed_t(nembed);
}
}
template <
typename dest_t,
typename src_t,
std::enable_if_t<std::is_integral_v<src_t> && std::is_integral_v<dest_t>, bool> = true>
static dest_t convert_to(const src_t& val)
{
if constexpr(std::is_signed_v<src_t>)
{
if constexpr(std::is_unsigned_v<dest_t>)
{
// do not use std::numeric_limits<dest_t>::lowest()
// to avoid implicit promotion of val to dest_t:
if(val < 0)
throw ionembed_type_conversion_error(); // underflow
}
else if constexpr(sizeof(dest_t) < sizeof(src_t))
{
if(val < std::numeric_limits<dest_t>::lowest())
throw ionembed_type_conversion_error(); // underflow
}
}
if constexpr(sizeof(dest_t) < sizeof(src_t)
|| (sizeof(dest_t) == sizeof(src_t)
&& std::is_signed_v<dest_t> && std::is_unsigned_v<src_t>))
{
if(val > std::numeric_limits<dest_t>::max())
throw ionembed_type_conversion_error(); // overflow
}
return static_cast<dest_t>(val);
}
};
// in hipFFT/cuFFT:
// @ T := int or T := long long int
// @ T := size_t internally to the implementation details on AMD platforms
// @ elementary strides are ignored when implicit default inembed/onembed are used
// --> "true" template specialization value for second template arg of ionembed_t
template <typename T>
using hipfft_ionembed_t = ionembed_t<T, true>;
// in FFTW3/hipFFTW
// @ T := int
// @ T := ptrdiff_t internally to the implementation details of hipFFTW
// @ elementary strides are not ignored when implicit default inembed/onembed are used
// --> "false" template specialization value for second template arg of ionembed_t
using hipfftw_ionembed_t = ionembed_t<int, false>;
using fftw_ionembed_t = ionembed_t<int, false>;
using hipfftw_internal_ionembed_t = ionembed_t<ptrdiff_t, false>;
#endif // CLIENT_DATA_LAYOUT_HELPERS_H