From 1b146c46549755756d84454a72d0731903521b1b Mon Sep 17 00:00:00 2001 From: Phillip Vu <36169227+phillipvu@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:51:05 -0700 Subject: [PATCH 1/2] benchmark_pcps_grid: add concurrent channel throughput case The existing cases time a single engine, but each acquisition block owns its own engine and CUDA stream, so the figure that bounds how many channels can search at once is the aggregate rate with several engines resident on the device. Add bm_pcps_grid_cuda_channels, which gives every benchmark thread its own engine, wipe-off grid and output buffers, and sweeps 1 to 8 threads. Rate counters are summed across threads and are fed the iteration count already totaled over threads, so kAvgThreads yields the aggregate device rate and a 1/channels scale yields the per-channel rate; both were checked against the reported per-thread time. On a Jetson Orin Nano at 4000 samples and 41 Doppler bins the aggregate rate is 2757, 3839, 5943 and 5307 dwells/s for 1, 2, 4 and 8 channels, so the device saturates at about four concurrent channels. Signed-off-by: Phillip Vu <36169227+phillipvu@users.noreply.github.com> --- tests/benchmarks/benchmark_pcps_grid.cc | 100 ++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/tests/benchmarks/benchmark_pcps_grid.cc b/tests/benchmarks/benchmark_pcps_grid.cc index 3d0cd24ad..ca8a1f487 100644 --- a/tests/benchmarks/benchmark_pcps_grid.cc +++ b/tests/benchmarks/benchmark_pcps_grid.cc @@ -163,6 +163,98 @@ void bm_pcps_grid_cuda(benchmark::State& state) set_counters(state, fft_size, bins); state.SetLabel(gpu.device_name()); } + +// -------------------------------------------------------------------------- +// Concurrent channels: one engine (and one CUDA stream) per thread, as the +// receiver does when several channels are in acquisition at the same time. +// -------------------------------------------------------------------------- +struct MultiChannelFixture +{ + std::vector> engines; + std::vector> magnitude; + std::vector> out_rows; + cvec in; + cvec fft_codes; + std::vector wipeoffs; + std::string error; + + bool setup(uint32_t fft_size, uint32_t bins, int channels) + { + const double fs = 4.0e6; + in = random_cvec(fft_size, 1U); + fft_codes = random_cvec(fft_size, 2U); + wipeoffs = make_wipeoffs(fft_size, bins, fs); + std::vector*> wipe_rows(bins); + for (uint32_t k = 0; k < bins; k++) wipe_rows[k] = wipeoffs[k].data(); + + engines.resize(channels); + magnitude.resize(channels); + out_rows.resize(channels); + for (int c = 0; c < channels; c++) + { + engines[c] = std::make_unique(fft_size, fft_size, bins); + if (!engines[c]->is_valid()) + { + error = engines[c]->last_error(); + return false; + } + magnitude[c].assign(bins, fvec(fft_size)); + out_rows[c].resize(bins); + for (uint32_t k = 0; k < bins; k++) out_rows[c][k] = magnitude[c][k].data(); + if (!engines[c]->set_doppler_wipeoffs(CudaPcpsEngine::MAIN_GRID, wipe_rows.data(), bins) || + !engines[c]->set_fft_codes(fft_codes.data()) || + !engines[c]->compute_grid(in.data(), CudaPcpsEngine::MAIN_GRID, bins, 0, false, out_rows[c].data())) + { + error = engines[c]->last_error(); + return false; + } + } + return true; + } +}; + +void bm_pcps_grid_cuda_channels(benchmark::State& state) +{ + const auto fft_size = static_cast(state.range(0)); + const auto bins = static_cast(state.range(1)); + const int channels = state.threads(); + static MultiChannelFixture fixture; + + if (state.thread_index() == 0) + { + fixture = MultiChannelFixture(); + if (!fixture.setup(fft_size, bins, channels)) + { + state.SkipWithError(fixture.error.c_str()); + return; + } + } + + const int c = state.thread_index(); + for (auto _ : state) + { + if (!fixture.engines[c]->compute_grid(fixture.in.data(), CudaPcpsEngine::MAIN_GRID, bins, 0, false, fixture.out_rows[c].data())) + { + state.SkipWithError(fixture.engines[c]->last_error().c_str()); + break; + } + benchmark::DoNotOptimize(fixture.magnitude[c][0].data()); + } + // Counter values are summed across threads, and the rate counters are fed + // the iteration count already totaled over threads. kAvgThreads therefore + // turns an iteration-invariant rate into the aggregate device rate, and + // scaling the value by 1/channels turns it into the per-channel rate. + const double per_channel = 1.0 / static_cast(channels); + state.counters["fft_size"] = benchmark::Counter(fft_size, benchmark::Counter::kAvgThreads); + state.counters["bins"] = benchmark::Counter(bins, benchmark::Counter::kAvgThreads); + state.counters["channels"] = benchmark::Counter(channels, benchmark::Counter::kAvgThreads); + state.counters["dwells/s/ch"] = benchmark::Counter(per_channel, + benchmark::Counter::kIsIterationInvariantRate | benchmark::Counter::kAvgThreads); + state.counters["dwells/s_all_ch"] = benchmark::Counter(1, + benchmark::Counter::kIsIterationInvariantRate | benchmark::Counter::kAvgThreads); + state.counters["grid_cells/s_all_ch"] = benchmark::Counter(static_cast(fft_size) * bins, + benchmark::Counter::kIsIterationInvariantRate | benchmark::Counter::kAvgThreads); +} #endif } // namespace @@ -178,4 +270,12 @@ BENCHMARK(bm_pcps_grid_cpu)->ArgsProduct(grid_args)->Unit(benchmark::kMicrosecon BENCHMARK(bm_pcps_grid_cuda)->ArgsProduct(grid_args)->Unit(benchmark::kMicrosecond)->UseRealTime(); #endif +#if CUDA_GPU_ACCEL +BENCHMARK(bm_pcps_grid_cuda_channels) + ->ArgsProduct({{4000, 8000, 20000}, {41}}) + ->ThreadRange(1, 8) + ->Unit(benchmark::kMicrosecond) + ->UseRealTime(); +#endif + BENCHMARK_MAIN(); From 37e830c0e08179fb7854667a3bd417e0b36dc2fc Mon Sep 17 00:00:00 2001 From: Carles Fernandez Date: Thu, 8 Oct 2026 19:06:32 +0200 Subject: [PATCH 2/2] benchmark_pcps_grid: skip all channel threads when setup fails If the multi-channel fixture setup fails on thread 0 (no CUDA device, or out of device memory at high channel counts), thread 0 skips and returns before the timed loop. Google Benchmark then releases the other threads from the start barrier, and they call compute_grid() on engines that were never created, which crashes the whole benchmark binary. Add a ready flag that setup() sets only on success. Every thread checks it at the top of the timed loop, which is the first point where threads 1..N-1 can see the outcome of the setup, and skips the run instead. Also add the missing include for the fixture's error member. --- tests/benchmarks/benchmark_pcps_grid.cc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/benchmarks/benchmark_pcps_grid.cc b/tests/benchmarks/benchmark_pcps_grid.cc index ca8a1f487..172fe203a 100644 --- a/tests/benchmarks/benchmark_pcps_grid.cc +++ b/tests/benchmarks/benchmark_pcps_grid.cc @@ -35,6 +35,7 @@ #include #include #include +#include #include #if CUDA_GPU_ACCEL #include "cuda_pcps_engine.h" @@ -177,6 +178,7 @@ struct MultiChannelFixture cvec fft_codes; std::vector wipeoffs; std::string error; + bool ready = false; bool setup(uint32_t fft_size, uint32_t bins, int channels) { @@ -209,6 +211,7 @@ struct MultiChannelFixture return false; } } + ready = true; return true; } }; @@ -233,6 +236,13 @@ void bm_pcps_grid_cuda_channels(benchmark::State& state) const int c = state.thread_index(); for (auto _ : state) { + // Threads other than 0 only learn about a setup failure here, after + // the start barrier releases them. + if (!fixture.ready) + { + state.SkipWithError("multi-channel setup failed"); + break; + } if (!fixture.engines[c]->compute_grid(fixture.in.data(), CudaPcpsEngine::MAIN_GRID, bins, 0, false, fixture.out_rows[c].data())) { state.SkipWithError(fixture.engines[c]->last_error().c_str());