Skip to content

Commit 85d3c32

Browse files
authored
Implement CPU polyphase channelizer (#1188)
* Implement CPU polyphase channelizer Add host executor support for channelize_poly by computing the per-branch FIR stage directly and applying the unnormalized positive-sign DFT used by the CUDA path. Cover the CPU path with real, double, complex, oversampled, batched, operator-input, and PropAccum/PropOutput tests. * Address CPU channelizer review feedback Precompute host DFT twiddles once per invocation and reuse per-thread scratch storage for FIR branch accumulators so the CPU path avoids repeated hot-path work. Add deterministic SelectThreadsHostExecutor coverage comparing threaded and single-threaded host results. * Update channelizer executor compatibility docs Mark channelize_poly as HostExecutor-compatible now that the CPU implementation is available.
1 parent 7ef1425 commit 85d3c32

4 files changed

Lines changed: 487 additions & 4 deletions

File tree

docs_input/executor_compatibility.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ fused JIT expression; non-JIT CUDA execution through cudaExecutor remains availa
7070
"cart2sph", "|yes|", "|yes|", "|yes|", "Element-wise coordinate conversion expression."
7171
"ceil", "|yes|", "|yes|", "|yes|", "Element-wise expression."
7272
"cgsolve", "|no|", "|yes|", "|no|", "CUDA iterative solver path."
73-
"channelize_poly", "|no|", "|yes|", "|no|", "CUDA-only polyphase channelizer."
73+
"channelize_poly", "|yes|", "|yes|", "|no|", "Polyphase channelizer; host path directly computes the per-branch FIR and DFT stages."
7474
"chirp", "|yes|", "|yes|", "|yes|", "Generator expression."
7575
"chol", "|yes|", "|yes|", "|yes|", "Host support requires the CPU solver backend. CUDAJITExecutor support uses cuSolverDx through MathDx for supported rank 2-4 square float, double, complex-float, and complex-double matrices."
7676
"clone", "|yes|", "|yes|", "|yes|", "View expression."

include/matx/operators/channelize_poly.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,20 @@ namespace detail {
117117

118118
template <typename Out, typename Executor>
119119
void Exec(Out &&out, Executor &&ex) const {
120-
static_assert(is_cuda_executor_v<Executor>, "channelize_poly() only supports the CUDA executor currently");
120+
static_assert(is_cuda_executor_v<Executor> || is_host_executor_v<Executor>,
121+
"channelize_poly() only supports CUDA and host executors");
121122

122123
// The accumulator type should always be real (it will be promoted to complex when necessary), so the
123124
// default accumulator type is the output's inner type. The outputs of channelize_poly are always complex
124125
// due to the IFFT, but the filtering that is applied prior to the IFFT can be either real or complex.
125126
using accum_type = get_property_or<PropAccum, typename inner_op_type_t<value_type>::type, CurrentProps...>::type;
126-
channelize_poly_impl<decltype(cuda::std::get<0>(out)), decltype(a_), FilterType, accum_type>(
127-
cuda::std::get<0>(out), a_, f_, num_channels_, decimation_factor_, ex.getStream());
127+
if constexpr (is_cuda_executor_v<Executor>) {
128+
channelize_poly_impl<decltype(cuda::std::get<0>(out)), decltype(a_), FilterType, accum_type>(
129+
cuda::std::get<0>(out), a_, f_, num_channels_, decimation_factor_, ex.getStream());
130+
} else {
131+
channelize_poly_impl<decltype(cuda::std::get<0>(out)), decltype(a_), FilterType, accum_type>(
132+
cuda::std::get<0>(out), a_, f_, num_channels_, decimation_factor_, ex);
133+
}
128134
}
129135

130136
template <typename ShapeType, typename Executor>

include/matx/transforms/channelize_poly.h

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,18 @@
3232

3333
#pragma once
3434

35+
#include <algorithm>
36+
#include <cmath>
3537
#include <cstdint>
3638
#include <cstdio>
3739
#include <type_traits>
3840
#include <numeric>
41+
#include <vector>
3942

4043
#include "matx/core/error.h"
4144
#include "matx/core/nvtx.h"
4245
#include "matx/core/tensor.h"
46+
#include "matx/executors/host.h"
4347
#include "matx/kernels/channelize_poly.cuh"
4448
#include "matx/operators/fft.h"
4549
#include "matx/operators/slice.h"
@@ -211,6 +215,119 @@ inline bool ShouldUseSmemTiled(
211215
<= SmemTiledMaxBytes;
212216
}
213217

218+
template <typename AccumT, typename FilterT>
219+
__MATX_HOST__ __MATX_INLINE__ auto HostChannelizeCastFilter(FilterT v)
220+
{
221+
if constexpr (is_complex_v<FilterT>) {
222+
return static_cast<AccumT>(v);
223+
} else if constexpr (is_complex_v<AccumT>) {
224+
using accum_scalar_t = typename inner_op_type_t<AccumT>::type;
225+
return static_cast<accum_scalar_t>(v);
226+
} else {
227+
return static_cast<AccumT>(v);
228+
}
229+
}
230+
231+
template <typename AccumT, typename InputT>
232+
__MATX_HOST__ __MATX_INLINE__ auto HostChannelizeCastInput(InputT v)
233+
{
234+
if constexpr (is_complex_v<InputT>) {
235+
return static_cast<AccumT>(v);
236+
} else if constexpr (is_complex_v<AccumT>) {
237+
using accum_scalar_t = typename inner_op_type_t<AccumT>::type;
238+
return static_cast<accum_scalar_t>(v);
239+
} else {
240+
return static_cast<AccumT>(v);
241+
}
242+
}
243+
244+
template <typename AccumT, typename FilterValT, typename InputValT>
245+
__MATX_HOST__ __MATX_INLINE__ void HostChannelizeCmac(
246+
AccumT &accum, FilterValT hv, InputValT iv)
247+
{
248+
if constexpr (is_complex_v<AccumT> && is_complex_v<FilterValT> && is_complex_v<InputValT>) {
249+
auto h_re = hv.real(), h_im = hv.imag();
250+
auto i_re = iv.real(), i_im = iv.imag();
251+
auto a_re = accum.real(), a_im = accum.imag();
252+
a_re = h_re * i_re + a_re;
253+
a_re = -(h_im * i_im) + a_re;
254+
a_im = h_re * i_im + a_im;
255+
a_im = h_im * i_re + a_im;
256+
accum = {a_re, a_im};
257+
} else if constexpr (is_complex_v<AccumT> && !is_complex_v<FilterValT> && is_complex_v<InputValT>) {
258+
auto a_re = accum.real(), a_im = accum.imag();
259+
a_re = hv * iv.real() + a_re;
260+
a_im = hv * iv.imag() + a_im;
261+
accum = {a_re, a_im};
262+
} else if constexpr (is_complex_v<AccumT> && is_complex_v<FilterValT> && !is_complex_v<InputValT>) {
263+
auto a_re = accum.real(), a_im = accum.imag();
264+
a_re = hv.real() * iv + a_re;
265+
a_im = hv.imag() * iv + a_im;
266+
accum = {a_re, a_im};
267+
} else {
268+
accum += hv * iv;
269+
}
270+
}
271+
272+
template <typename Op, typename Arr, size_t... Is>
273+
__MATX_HOST__ __MATX_INLINE__ decltype(auto) HostReadSignalImpl(
274+
const Op &op, const Arr &batch_idx, index_t sample_idx,
275+
cuda::std::index_sequence<Is...>)
276+
{
277+
return op(batch_idx[Is]..., sample_idx);
278+
}
279+
280+
template <typename Op, typename Arr>
281+
__MATX_HOST__ __MATX_INLINE__ decltype(auto) HostReadSignal(
282+
const Op &op, const Arr &batch_idx, index_t sample_idx)
283+
{
284+
return HostReadSignalImpl(
285+
op, batch_idx, sample_idx,
286+
cuda::std::make_index_sequence<static_cast<size_t>(Op::Rank() - 1)>{});
287+
}
288+
289+
template <typename OutType, typename Arr, typename ValueT, size_t... Is>
290+
__MATX_HOST__ __MATX_INLINE__ void HostWriteOutputImpl(
291+
OutType &out, const Arr &batch_idx, index_t output_idx, index_t channel,
292+
const ValueT &value, cuda::std::index_sequence<Is...>)
293+
{
294+
out(batch_idx[Is]..., output_idx, channel) =
295+
static_cast<typename OutType::value_type>(value);
296+
}
297+
298+
template <typename OutType, typename Arr, typename ValueT>
299+
__MATX_HOST__ __MATX_INLINE__ void HostWriteOutput(
300+
OutType &out, const Arr &batch_idx, index_t output_idx, index_t channel,
301+
const ValueT &value)
302+
{
303+
HostWriteOutputImpl(
304+
out, batch_idx, output_idx, channel, value,
305+
cuda::std::make_index_sequence<static_cast<size_t>(OutType::Rank() - 2)>{});
306+
}
307+
308+
template <typename ComplexAccumT, typename ValueT>
309+
__MATX_HOST__ __MATX_INLINE__ ComplexAccumT HostAsComplex(ValueT v)
310+
{
311+
using scalar_t = typename inner_op_type_t<ComplexAccumT>::type;
312+
if constexpr (is_complex_v<ValueT>) {
313+
return static_cast<ComplexAccumT>(v);
314+
} else {
315+
return ComplexAccumT{static_cast<scalar_t>(v), static_cast<scalar_t>(0)};
316+
}
317+
}
318+
319+
template <typename ComplexAccumT>
320+
__MATX_HOST__ __MATX_INLINE__ ComplexAccumT HostTwiddle(index_t channel, index_t branch, index_t num_channels)
321+
{
322+
using scalar_t = typename inner_op_type_t<ComplexAccumT>::type;
323+
constexpr double pi = 3.141592653589793238462643383279502884;
324+
const double arg = 2.0 * pi * static_cast<double>(channel) *
325+
static_cast<double>(branch) / static_cast<double>(num_channels);
326+
return ComplexAccumT{
327+
static_cast<scalar_t>(std::cos(arg)),
328+
static_cast<scalar_t>(std::sin(arg))};
329+
}
330+
214331
template <int CTILE, typename OutType, typename InType, typename FilterType, typename AccumType>
215332
inline void SmemTiledImpl(
216333
OutType o, const InType &i, const FilterType &filter,
@@ -730,4 +847,184 @@ inline void channelize_poly_impl(OutType out, const InType &in, const FilterType
730847
}
731848
}
732849
}
850+
851+
/**
852+
* @brief Host implementation of the 1D polyphase channelizer.
853+
*
854+
* This is a feature-parity implementation for CPU executors. It directly
855+
* computes the per-branch FIR values and then applies the unnormalized,
856+
* positive-sign DFT used by the CUDA channelizer.
857+
*/
858+
template <typename OutType, typename InType, typename FilterType, typename AccumType, ThreadsMode MODE>
859+
inline void channelize_poly_impl(OutType out, const InType &in, const FilterType &f,
860+
index_t num_channels, index_t decimation_factor,
861+
[[maybe_unused]] const HostExecutor<MODE> &exec) {
862+
MATX_NVTX_START("", matx::MATX_NVTX_LOG_API)
863+
using OutputOp = std::remove_cv_t<std::remove_reference_t<OutType>>;
864+
using InputOp = std::remove_cv_t<std::remove_reference_t<InType>>;
865+
using FilterOp = std::remove_cv_t<std::remove_reference_t<FilterType>>;
866+
using input_t = typename InputOp::value_type;
867+
using filter_t = typename FilterOp::value_type;
868+
using output_t = typename OutputOp::value_type;
869+
using filtering_accum_t = cuda::std::conditional_t<
870+
is_complex_v<input_t> || is_complex_v<filter_t>,
871+
typename detail::scalar_to_complex<AccumType>::ctype,
872+
AccumType>;
873+
using complex_accum_t = typename detail::scalar_to_complex<AccumType>::ctype;
874+
875+
static_assert(!is_complex_v<AccumType>,
876+
"channelize_poly: accumulator type must be real; it will be treated as complex when necessary");
877+
878+
constexpr int IN_RANK = InputOp::Rank();
879+
constexpr int OUT_RANK = OutputOp::Rank();
880+
881+
MATX_STATIC_ASSERT_STR(OUT_RANK == IN_RANK+1, matxInvalidDim,
882+
"channelize_poly: output rank should be 1 higher than input");
883+
MATX_STATIC_ASSERT_STR(is_complex_v<output_t> || is_complex_half_v<output_t>,
884+
matxInvalidType, "channelize_poly: output type must be complex");
885+
MATX_STATIC_ASSERT_STR(FilterType::Rank() == 1, matxInvalidDim,
886+
"channelize_poly: currently only support 1D filters");
887+
888+
MATX_ASSERT_STR(num_channels > 0, matxInvalidParameter,
889+
"channelize_poly: num_channels must be positive");
890+
MATX_ASSERT_STR(decimation_factor > 0, matxInvalidParameter,
891+
"channelize_poly: decimation_factor must be positive");
892+
MATX_ASSERT_STR(decimation_factor <= num_channels, matxInvalidParameter,
893+
"channelize_poly: decimation_factor must be <= num_channels");
894+
895+
for (int i = 0; i < IN_RANK-1; i++) {
896+
MATX_ASSERT_STR(out.Size(i) == in.Size(i), matxInvalidDim,
897+
"channelize_poly: input/output must have matched batch sizes");
898+
}
899+
900+
const index_t input_len = in.Size(IN_RANK-1);
901+
const index_t num_elem_per_channel = (input_len + decimation_factor - 1) / decimation_factor;
902+
MATX_ASSERT_STR(out.Size(OUT_RANK-1) == num_channels, matxInvalidDim,
903+
"channelize_poly: output size OUT_RANK-1 mismatch");
904+
MATX_ASSERT_STR(out.Size(OUT_RANK-2) == num_elem_per_channel, matxInvalidDim,
905+
"channelize_poly: output size OUT_RANK-2 mismatch");
906+
907+
const index_t filter_full_len = f.Size(FilterOp::Rank()-1);
908+
const index_t filter_phase_len = (filter_full_len + num_channels - 1) / num_channels;
909+
index_t batch_count = 1;
910+
for (int i = 0; i < IN_RANK-1; i++) {
911+
batch_count *= in.Size(i);
912+
}
913+
914+
std::vector<complex_accum_t> twiddles(static_cast<size_t>(num_channels * num_channels));
915+
for (index_t channel = 0; channel < num_channels; channel++) {
916+
for (index_t branch = 0; branch < num_channels; branch++) {
917+
twiddles[static_cast<size_t>(channel * num_channels + branch)] =
918+
detail::cpoly::HostTwiddle<complex_accum_t>(channel, branch, num_channels);
919+
}
920+
}
921+
922+
const index_t num_thread_buffers = std::max<index_t>(1, exec.GetNumThreads());
923+
std::vector<filtering_accum_t> filtered_storage(
924+
static_cast<size_t>(num_thread_buffers * num_channels));
925+
926+
const auto compute_output = [&](index_t batch, index_t t) {
927+
const auto in_batch_idx = detail::BlockToIdx(in, batch, 1);
928+
const auto out_batch_idx = detail::BlockToIdx(out, batch, 2);
929+
index_t thread_index = 0;
930+
#ifdef MATX_EN_OMP
931+
if (num_thread_buffers > 1) {
932+
thread_index = static_cast<index_t>(omp_get_thread_num());
933+
}
934+
#endif
935+
auto *filtered = filtered_storage.data() +
936+
static_cast<size_t>(thread_index * num_channels);
937+
938+
for (index_t branch = 0; branch < num_channels; branch++) {
939+
filtering_accum_t accum{};
940+
index_t h_ind = branch;
941+
index_t sample_idx = 0;
942+
index_t niter = 0;
943+
944+
if (decimation_factor == num_channels) {
945+
const index_t s = num_channels - 1 - branch;
946+
sample_idx = s + t * num_channels;
947+
index_t h_skip = 0;
948+
if (sample_idx >= input_len) {
949+
h_skip = 1;
950+
sample_idx -= num_channels;
951+
}
952+
953+
index_t available_taps = filter_phase_len;
954+
if (filter_phase_len > 0 &&
955+
((filter_phase_len - 1) * num_channels + branch) >= filter_full_len) {
956+
available_taps--;
957+
}
958+
959+
if (available_taps > h_skip && (t + 1) > h_skip) {
960+
niter = std::min(available_taps - h_skip, t + 1 - h_skip);
961+
h_ind = branch + h_skip * num_channels;
962+
}
963+
} else {
964+
const index_t r_remapped = (branch + num_channels - decimation_factor) % num_channels;
965+
const index_t s = num_channels - 1 - r_remapped;
966+
const index_t last_arrived = t * decimation_factor + decimation_factor - 1;
967+
if (last_arrived >= s) {
968+
const index_t A = last_arrived - s;
969+
sample_idx = last_arrived - (A % num_channels);
970+
const index_t causal_count = A / num_channels + 1;
971+
const index_t phase = (branch + t * decimation_factor) % num_channels;
972+
index_t h_skip = 0;
973+
if (sample_idx >= input_len) {
974+
h_skip = 1;
975+
sample_idx -= num_channels;
976+
}
977+
978+
index_t available_taps = filter_phase_len;
979+
if (filter_phase_len > 0 &&
980+
((filter_phase_len - 1) * num_channels + phase) >= filter_full_len) {
981+
available_taps--;
982+
}
983+
984+
if (available_taps > h_skip && causal_count > h_skip) {
985+
niter = std::min(available_taps - h_skip, causal_count - h_skip);
986+
h_ind = phase + h_skip * num_channels;
987+
}
988+
}
989+
}
990+
991+
for (index_t i = 0; i < niter; i++) {
992+
const input_t in_val = detail::cpoly::HostReadSignal(in, in_batch_idx, sample_idx);
993+
const filter_t h_val = f(h_ind);
994+
detail::cpoly::HostChannelizeCmac(accum,
995+
detail::cpoly::HostChannelizeCastFilter<filtering_accum_t>(h_val),
996+
detail::cpoly::HostChannelizeCastInput<filtering_accum_t>(in_val));
997+
h_ind += num_channels;
998+
sample_idx -= num_channels;
999+
}
1000+
1001+
filtered[static_cast<size_t>(branch)] = accum;
1002+
}
1003+
1004+
for (index_t channel = 0; channel < num_channels; channel++) {
1005+
complex_accum_t dft{};
1006+
for (index_t branch = 0; branch < num_channels; branch++) {
1007+
dft += detail::cpoly::HostAsComplex<complex_accum_t>(
1008+
filtered[static_cast<size_t>(branch)]) *
1009+
twiddles[static_cast<size_t>(channel * num_channels + branch)];
1010+
}
1011+
detail::cpoly::HostWriteOutput(out, out_batch_idx, t, channel, dft);
1012+
}
1013+
};
1014+
1015+
const index_t total_outputs = batch_count * num_elem_per_channel;
1016+
#ifdef MATX_EN_OMP
1017+
if (exec.GetNumThreads() > 1) {
1018+
#pragma omp parallel for num_threads(exec.GetNumThreads())
1019+
for (index_t i = 0; i < total_outputs; i++) {
1020+
compute_output(i / num_elem_per_channel, i % num_elem_per_channel);
1021+
}
1022+
} else
1023+
#endif
1024+
{
1025+
for (index_t i = 0; i < total_outputs; i++) {
1026+
compute_output(i / num_elem_per_channel, i % num_elem_per_channel);
1027+
}
1028+
}
1029+
}
7331030
} // end namespace matx

0 commit comments

Comments
 (0)