From 7ade5c55709d62769c253e37ef7d29c2e480dea0 Mon Sep 17 00:00:00 2001 From: Josh Bailey Date: Mon, 25 Sep 2023 16:59:57 +1300 Subject: [PATCH] add VkFFT vs. software FFT test. --- .github/workflows/test.yaml | 2 +- lib/libvkfft.cc | 6 +- lib/vkfft_short_impl.cc | 2 +- python/iqtlabs/qa_retune_fft.py | 157 +++++++++++++++++--------------- python/iqtlabs/qa_vkfft.py | 56 ++++++++++-- 5 files changed, 136 insertions(+), 87 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bb33082c..83618275 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: build_test run: | - bin/apt_get.sh && bin/build_test.sh && bin/test_grc310.sh + bin/apt_get.sh && TEST_VKFFT=1 bin/build_test.sh && bin/test_grc310.sh test-2004-gnuradio39: runs-on: ubuntu-20.04 steps: diff --git a/lib/libvkfft.cc b/lib/libvkfft.cc index 35b8ac57..9a5df01b 100644 --- a/lib/libvkfft.cc +++ b/lib/libvkfft.cc @@ -116,10 +116,10 @@ VkFFTResult _transferDataToCPU(char *cpu_arr) { if (res != VK_SUCCESS) return VKFFT_ERROR_MALLOC_FAILED; if (_shift) { + const size_t halfFftBufferSize = fftBufferSize / 2; for (int i = 0; i < vkConfiguration.numberBatches; ++i) { - memcpy(cpu_arr + fftBufferSize / 2, data, fftBufferSize / 2); - memcpy(cpu_arr, data + fftBufferSize / 2, fftBufferSize / 2); - cpu_arr += fftBufferSize; + memcpy(cpu_arr + halfFftBufferSize, data, halfFftBufferSize); + memcpy(cpu_arr, data + halfFftBufferSize, halfFftBufferSize); cpu_arr += fftBufferSize; } } else { diff --git a/lib/vkfft_short_impl.cc b/lib/vkfft_short_impl.cc index 3f84abfb..fbaad7ba 100644 --- a/lib/vkfft_short_impl.cc +++ b/lib/vkfft_short_impl.cc @@ -249,7 +249,7 @@ int vkfft_short_impl::work(int noutput_items, for (int i = 0; i < noutput_items; ++i) { const int buffer_index = i * vlen_ * 2; - _converter->conv(&in[i], &buffer[0], vlen_); + _converter->conv(&in[buffer_index], &buffer[0], vlen_); vkfft_offload((char *)&buffer[0], (char *)&out[buffer_index]); } diff --git a/python/iqtlabs/qa_retune_fft.py b/python/iqtlabs/qa_retune_fft.py index e83eb2d7..153bb94e 100755 --- a/python/iqtlabs/qa_retune_fft.py +++ b/python/iqtlabs/qa_retune_fft.py @@ -217,7 +217,6 @@ from gnuradio import gr, gr_unittest from gnuradio import blocks from gnuradio import fft -from gnuradio.fft import window try: from gnuradio.iqtlabs import ( @@ -280,6 +279,7 @@ def retune_fft(self, fft_roll): fft_write_count = 2 bucket_range = 1 fft_min = -1e9 + fft_batch_size = 1 with tempfile.TemporaryDirectory() as tmpdir: test_file = os.path.join(tmpdir, "samples.csv") @@ -287,7 +287,7 @@ def retune_fft(self, fft_roll): iqtlabs_retune_pre_fft_0 = retune_pre_fft( points, - 1, # fft_batch_size + fft_batch_size, "rx_freq", int(freq_start), int(freq_end), @@ -318,8 +318,19 @@ def retune_fft(self, fft_roll): True, ) pdu_decoder_0 = pdu_decoder() - fft_vxx_0 = fft.fft_vcc( - points, True, window.blackmanharris(points), True, 1 + fft_vxx_0 = fft.fft_vcc(points, True, [], fft_roll, 1) + + try: + if os.getenv("TEST_VKFFT", 0): + from gnuradio.iqtlabs import vkfft + + fft_vxx_0 = vkfft(fft_batch_size * points, points, fft_roll) + print("using VkFFT") + except ImportError: + print("using software FFT") + window = blocks.multiply_const_vff( + [val for val in fft.window.blackmanharris(points) for _ in range(2)] + * fft_batch_size ) blocks_throttle_0 = blocks.throttle( gr.sizeof_gr_complex * 1, samp_rate, True @@ -329,7 +340,6 @@ def retune_fft(self, fft_roll): blocks_complex_to_mag_0 = blocks.complex_to_mag(points) blocks_nlog10_ff_0 = blocks.nlog10_ff(20, points, 0) vr1 = vector_roll(points) - vr2 = vector_roll(points) self.tb.msg_connect( (iqtlabs_retune_pre_fft_0, "tune"), @@ -342,14 +352,14 @@ def retune_fft(self, fft_roll): self.tb.connect((blocks_complex_to_mag_0, 0), (blocks_nlog10_ff_0, 0)) self.tb.connect((blocks_nlog10_ff_0, 0), (iqtlabs_retune_fft_0, 0)) if fft_roll: + self.tb.connect((fft_vxx_0, 0), (blocks_complex_to_mag_0, 0)) + else: # double roll, is a no-op self.tb.connect((fft_vxx_0, 0), (vr1, 0)) - self.tb.connect((vr1, 0), (vr2, 0)) - self.tb.connect((vr2, 0), (blocks_complex_to_mag_0, 0)) - else: - self.tb.connect((fft_vxx_0, 0), (blocks_complex_to_mag_0, 0)) + self.tb.connect((vr1, 0), (blocks_complex_to_mag_0, 0)) self.tb.connect((iqtlabs_retune_fft_0, 0), (blocks_file_sink_0, 0)) - self.tb.connect((iqtlabs_retune_pre_fft_0, 0), (fft_vxx_0, 0)) + self.tb.connect((iqtlabs_retune_pre_fft_0, 0), (window, 0)) + self.tb.connect((window, 0), (fft_vxx_0, 0)) self.tb.connect((blocks_throttle_0, 0), (iqtlabs_retune_pre_fft_0, 0)) self.tb.connect((iqtlabs_tuneable_test_source_0, 0), (blocks_throttle_0, 0)) @@ -369,67 +379,70 @@ def retune_fft(self, fft_roll): self.assertTrue(os.path.exists(test_file)) with open(test_file, encoding="utf8") as f: - linebuffer = "" - last_data = time.time() - last_ts = 0 - last_buckets = None - last_tuning_range = None - file_poll_timeout = 0.001 - while tuning_range_changes < 5: - self.assertLess(time.time() - last_data, 5) - line = f.readline() - linebuffer = linebuffer + line - if not linebuffer.endswith("\n"): - time.sleep(file_poll_timeout) - continue - last_data = time.time() - line = linebuffer.strip() + try: linebuffer = "" - record = json.loads(line) - ts = record["ts"] - self.assertGreater(ts, last_ts) - last_ts = ts - self.assertGreaterEqual(ts, record["sweep_start"]) - config = record["config"] - self.assertEqual("a text description", config["description"]), - tuning_range_freq_start = config["tuning_range_freq_start"] - tuning_range_freq_end = config["tuning_range_freq_end"] - tuning_range = int(config["tuning_range"]) - if tuning_range != last_tuning_range: - tuning_range_changes += 1 - print("tuning_range_changes:", tuning_range_changes) - last_tuning_range = tuning_range - self.assertTrue( - ( - tuning_range_freq_start == freq_start - and tuning_range_freq_end == freq_mid - ) - or ( - tuning_range_freq_start == freq_mid + samp_rate - and tuning_range_freq_end == freq_end + last_data = time.time() + last_ts = 0 + last_buckets = None + last_tuning_range = None + file_poll_timeout = 0.001 + while tuning_range_changes < 5: + self.assertLess(time.time() - last_data, 5) + line = f.readline() + linebuffer = linebuffer + line + if not linebuffer.endswith("\n"): + time.sleep(file_poll_timeout) + continue + last_data = time.time() + line = linebuffer.strip() + linebuffer = "" + record = json.loads(line) + ts = round(record["ts"]) + self.assertGreaterEqual(ts, last_ts) + last_ts = ts + config = record["config"] + self.assertEqual("a text description", config["description"]), + tuning_range_freq_start = config["tuning_range_freq_start"] + tuning_range_freq_end = config["tuning_range_freq_end"] + tuning_range = int(config["tuning_range"]) + if tuning_range != last_tuning_range: + tuning_range_changes += 1 + print("tuning_range_changes:", tuning_range_changes) + last_tuning_range = tuning_range + self.assertTrue( + ( + tuning_range_freq_start == freq_start + and tuning_range_freq_end == freq_mid + ) + or ( + tuning_range_freq_start == freq_mid + samp_rate + and tuning_range_freq_end == freq_end + ) ) - ) - self.assertEqual(config["freq_start"], freq_start) - self.assertEqual(config["freq_end"], freq_end) - self.assertEqual(config["sample_rate"], samp_rate) - self.assertEqual(config["nfft"], points) - buckets = record["buckets"] - self.assertTrue(buckets, (last_buckets, buckets)) - bucket_counts[len(buckets)] += 1 - fs = [int(f) for f in buckets.keys()] - self.assertGreaterEqual(min(fs), tuning_range_freq_start) - self.assertLessEqual(max(fs), tuning_range_freq_end) - new_records = [ - { - "ts": ts, - "f": int(freq), - "v": float(value), - "t": int(tuning_range), - } - for freq, value in buckets.items() - ] - records.extend(new_records) - last_buckets = buckets + self.assertEqual(config["freq_start"], freq_start) + self.assertEqual(config["freq_end"], freq_end) + self.assertEqual(config["sample_rate"], samp_rate) + self.assertEqual(config["nfft"], points) + buckets = record["buckets"] + self.assertTrue(buckets, (last_buckets, buckets)) + bucket_counts[len(buckets)] += 1 + fs = [int(f) for f in buckets.keys()] + self.assertGreaterEqual(min(fs), tuning_range_freq_start) + self.assertLessEqual(max(fs), tuning_range_freq_end) + new_records = [ + { + "ts": ts, + "f": int(freq), + "v": float(value), + "t": int(tuning_range), + } + for freq, value in buckets.items() + ] + records.extend(new_records) + last_buckets = buckets + except Exception: + self.tb.stop() + raise self.tb.stop() self.tb.wait() @@ -446,7 +459,7 @@ def retune_fft(self, fft_roll): for _, df in all_df.groupby("t"): # must have plausible unscaled dB value - self.assertTrue(fft_min <= df["v"].min() <= 1, df["v"].min()) + self.assertTrue(fft_min <= df["v"].min() <= 1, (fft_min, df["v"].min())) self.assertTrue(50 <= df["v"].max() <= 61, df["v"].max()) df["m"] = df.groupby("f")["v"].apply(lambda x: x.max() - x.min()) non_unique_v = df[df["m"] > 2] @@ -499,7 +512,7 @@ def retune_fft(self, fft_roll): class qa_retune_fft_no_roll(gr_unittest.TestCase, qa_retune_fft_base): def setUp(self): - self.tb = gr.top_block(catch_exceptions=False) + self.tb = gr.top_block(catch_exceptions=True) def tearDown(self): self.tb = None @@ -510,7 +523,7 @@ def test_retune_fft_no_roll(self): class qa_retune_fft_roll(gr_unittest.TestCase, qa_retune_fft_base): def setUp(self): - self.tb = gr.top_block(catch_exceptions=False) + self.tb = gr.top_block(catch_exceptions=True) def tearDown(self): self.tb = None diff --git a/python/iqtlabs/qa_vkfft.py b/python/iqtlabs/qa_vkfft.py index ff867616..82e765de 100755 --- a/python/iqtlabs/qa_vkfft.py +++ b/python/iqtlabs/qa_vkfft.py @@ -203,22 +203,58 @@ # limitations under the License. # -from gnuradio import gr, gr_unittest +import os +import numpy as np +from gnuradio import blocks, fft, iqtlabs, gr, gr_unittest -# from gnuradio import blocks -from gnuradio.iqtlabs import vkfft + +def run_fft(fft_block, points, fft_roll, input_items): + src1 = blocks.vector_source_c(input_items, vlen=len(input_items)) + dst1 = blocks.vector_sink_c(vlen=len(input_items)) + tb = gr.top_block() + tb.connect(src1, fft_block) + tb.connect(fft_block, dst1) + tb.run() + data = dst1.data() + tb.stop() + tb.wait() + del tb + return data class qa_vkfft(gr_unittest.TestCase): - def setUp(self): - self.tb = gr.top_block() + def test_instance(self): + for fft_roll in (True, False): + fft_batch_size = 4 + points = 8 - def tearDown(self): - self.tb = None + batch_input_items = [] + for i in range(1, fft_batch_size + 1): + batch_input_items.extend( + 1j * np.arange(points) + ) # pytype: disable=wrong-arg-types - def test_instance(self): - # TODO: find workaround llvmpipe simulated gpu crashes under CI testing - instance = vkfft(1024, 1, True) + sw_data = [] + for i in range(1, fft_batch_size + 1): + batch = (i - 1) * points + sw_data.extend( + run_fft( + fft.fft_vcc(points, True, [], fft_roll, 1), + points, + fft_roll, + batch_input_items[batch : batch + points], + ) + ) + + vkfft_data = run_fft( + iqtlabs.vkfft(fft_batch_size * points, points, fft_roll), + points, + fft_roll, + batch_input_items, + ) + + if os.getenv("TEST_VKFFT", 0): + self.assertEqual(vkfft_data, sw_data) if __name__ == "__main__":