host: Add AVX2 support for uhd::convert#789
host: Add AVX2 support for uhd::convert#789anilgurses wants to merge 3 commits intoEttusResearch:masterfrom
Conversation
a060a42 to
db32fa6
Compare
|
CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅ |
Signed-off-by: Anıl Gürses <[email protected]>
|
I have read the CLA Document and I hereby sign the CLA |
db32fa6 to
1b78ab6
Compare
|
Hi! Is there anything else needed for this PR? |
|
Hey @anilgurses, sorry for never responding here. The problem is that AVX2 support is not ubiquitous, and we need a way to only deploy it on demand. Something like a glibc conditional dispatch. I was also thinking of merging this, but leaving it disabled unless explicitly enabled at compile time (this would not, for example, be the case for .deb files we distribute). But that's also work. |
host/lib/convert/CMakeLists.txt
Outdated
| ######################################################################## | ||
|
|
||
| # Check for SSE2 support | ||
| check_cxx_compiler_flag("-msse2" SSE2_SUPPORTED) |
There was a problem hiding this comment.
All of this assumes the compiling machine has the same arch as the executing machine.
host/lib/convert/CMakeLists.txt
Outdated
| # Check for AVX2 support | ||
| check_cxx_compiler_flag("-mavx512" AVX512_SUPPORTED) | ||
| if(AVX512_SUPPORTED) | ||
| message(STATUS "AVX512 is supported") |
There was a problem hiding this comment.
This means AVX512 is supported by the compiler, not that it's also supported by the CPU.
|
Thanks for the feedback! You are right. Let me check if I can find time to implement on-demand AVX512. I'll update this PR once it's ready. |
|
Sorry for the delay, I've worked on it and developed runtime dispatch of SIMD functions for converters. I've also added a converter benchmark tool under tests. I was wondering how much performance gain is achieved on which instruction. I'm putting the table below. The baseline for comparison is compiler optimizations for the generic converter. It is not surprising that avx2 performs better at bigger packets. I might implement avx512 and test it with that as well. I've been using the previous version of the code for a year. I didn't encounter any issues but I realized that I made a big mistake on the SIMD_PRIORITY, which I have fixed. I ran my tests on Xeon Gold 6240. Let me know if I am missing something or I can improve my PR. |
There was a problem hiding this comment.
Pull request overview
Adds AVX2-backed converter implementations and introduces runtime SIMD feature detection so UHD can select the best available uhd::convert implementation on a given CPU.
Changes:
- Added runtime CPU SIMD feature detection (SSE2/SSSE3/AVX2/AVX512F) and new SIMD priority levels.
- Implemented multiple AVX2 converters and updated existing SSE2/SSSE3 converters to register conditionally.
- Updated build system and tests/examples to account for new priorities and benchmarking.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| host/tests/convert_test.cpp | Updates priority list and changes benchmark test decorator behavior. |
| host/lib/convert/ssse3_unpack_sc12.cpp | Adds runtime SSSE3 check to avoid registering on unsupported CPUs. |
| host/lib/convert/ssse3_pack_sc12.cpp | Adds runtime SSSE3 check to avoid registering on unsupported CPUs. |
| host/lib/convert/sse2_sc8_to_fc64.cpp | Switches to SSE2 runtime-gated converter declaration macro. |
| host/lib/convert/sse2_sc8_to_fc32.cpp | Switches to SSE2 runtime-gated converter declaration macro. |
| host/lib/convert/sse2_sc16_to_sc16.cpp | Switches to SSE2 runtime-gated converter declaration macro. |
| host/lib/convert/sse2_sc16_to_fc64.cpp | Switches to SSE2 runtime-gated converter declaration macro. |
| host/lib/convert/sse2_sc16_to_fc32.cpp | Switches to SSE2 runtime-gated converter declaration macro. |
| host/lib/convert/sse2_fc64_to_sc8.cpp | Switches to SSE2 runtime-gated converter declaration macro. |
| host/lib/convert/sse2_fc64_to_sc16.cpp | Switches to SSE2 runtime-gated converter declaration macro. |
| host/lib/convert/sse2_fc32_to_sc8.cpp | Switches to SSE2 runtime-gated converter declaration macro. |
| host/lib/convert/sse2_fc32_to_sc16.cpp | Switches to SSE2 runtime-gated converter declaration macro. |
| host/lib/convert/simd_features.hpp | New header for runtime SIMD detection + logging helpers. |
| host/lib/convert/convert_impl.cpp | Logs SIMD capabilities during converter/item-size registration. |
| host/lib/convert/convert_common.hpp | Adds SIMD converter macros with runtime gating and AVX2/AVX512 priorities. |
| host/lib/convert/avx2_sc8_to_fc32.cpp | New AVX2 converter implementation(s). |
| host/lib/convert/avx2_sc16_to_sc16.cpp | New AVX2 converter implementation(s). |
| host/lib/convert/avx2_sc16_to_fc64.cpp | New AVX2 converter implementation(s). |
| host/lib/convert/avx2_sc16_to_fc32.cpp | New AVX2 converter implementation(s). |
| host/lib/convert/avx2_fc64_to_sc8.cpp | New AVX2 converter implementation(s). |
| host/lib/convert/avx2_fc64_to_sc16.cpp | New AVX2 converter implementation(s). |
| host/lib/convert/avx2_fc32_to_sc8.cpp | New AVX2 converter implementation(s). |
| host/lib/convert/avx2_fc32_to_sc16.cpp | New AVX2 converter implementation(s). |
| host/lib/convert/CMakeLists.txt | Adds compiler-flag-based SIMD build detection and includes AVX2 sources. |
| host/examples/convert_benchmark.cpp | Adds a standalone converter benchmarking example. |
| host/examples/CMakeLists.txt | Builds the new convert_benchmark example. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // dispatch according to alignment | ||
| if ((size_t(input) & 0xf) == 0) { | ||
| convert_fc32_1_to_sc8_item32_1_nswap_guts(_) | ||
| } else { | ||
| convert_fc32_1_to_sc8_item32_1_nswap_guts(u_) | ||
| } |
There was a problem hiding this comment.
Same issue as above in the LE converter: _mm256_load_ps requires 32-byte alignment, but the guard only checks 16-byte alignment (& 0xf). This can cause crashes on 16B-but-not-32B aligned buffers. Please switch the aligned path to require 32-byte alignment (& 0x1f) or use unaligned loads unconditionally.
| // need to dispatch according to alignment for fastest conversion | ||
| switch (size_t(input) & 0xf) { | ||
| case 0x0: | ||
| // the data is 16-byte aligned, so do the fast processing of the bulk of the | ||
| // samples | ||
| convert_fc32_1_to_item32_1_bswap_guts(_) break; | ||
| case 0x8: | ||
| // the first value is 8-byte aligned - process it and prepare the bulk of the | ||
| // data for fast conversion | ||
| xx_to_item32_sc16<uhd::htonx>(input, output, 1, scale_factor); | ||
| i++; | ||
| // do faster processing of the remaining samples now that we are 16-byte | ||
| // aligned | ||
| convert_fc32_1_to_item32_1_bswap_guts(_) break; | ||
| default: | ||
| // we are not 8 or 16-byte aligned, so do fast processing with the unaligned | ||
| // load | ||
| convert_fc32_1_to_item32_1_bswap_guts(u_) | ||
| } |
There was a problem hiding this comment.
Same 16B-vs-32B alignment bug in this converter: the dispatch checks size_t(input) & 0xf, but _mm256_load_ps requires 32-byte alignment when using the aligned-load form. Please update the aligned-path condition to 32-byte alignment (& 0x1f) or always use unaligned loads.
| // need to dispatch according to alignment for fastest conversion | ||
| switch (size_t(input) & 0xf) { | ||
| case 0x0: | ||
| // the data is 16-byte aligned, so do the fast processing of the bulk of the | ||
| // samples | ||
| convert_fc32_1_to_item32_1_guts(_) break; | ||
| case 0x8: | ||
| // the first sample is 8-byte aligned - process it to align the remainder of | ||
| // the samples to 16-bytes | ||
| xx_to_chdr_sc16(input, output, 1, scale_factor); | ||
| i++; | ||
| // do faster processing of the bulk of the samples now that we are 16-byte | ||
| // aligned | ||
| convert_fc32_1_to_item32_1_guts(_) break; | ||
| default: | ||
| // we are not 8 or 16-byte aligned, so do fast processing with the unaligned | ||
| // load | ||
| convert_fc32_1_to_item32_1_guts(u_) | ||
| } |
There was a problem hiding this comment.
Same alignment issue here: the aligned-load branch uses _mm256_load_ps but only checks for 16-byte alignment (& 0xf). This should be 32-byte alignment (& 0x1f) to be safe, or the code should use _mm256_loadu_ps for all loads.
| // dispatch according to alignment | ||
| if ((size_t(input) & 0xf) == 0) { | ||
| convert_fc64_1_to_item32_1_bswap_guts(_) | ||
| } else { | ||
| convert_fc64_1_to_item32_1_bswap_guts(u_) | ||
| } |
There was a problem hiding this comment.
Same alignment issue in this converter: _mm256_load_pd requires 32-byte alignment but the guard only checks 16-byte alignment (& 0xf). Please require 32-byte alignment (& 0x1f) for the aligned-load path or switch to _mm256_loadu_pd.
| * Priority hierarchy: | ||
| * PRIORITY_EMPTY = -1 (empty/null converter) | ||
| * PRIORITY_GENERAL = 0 (generic C++ implementation) | ||
| * PRIORITY_TABLE = 1 (table lookup) | ||
| * PRIORITY_SIMD = 3 (SSE2/NEON - baseline SIMD) | ||
| * PRIORITY_SIMD_AVX2 = 4 (AVX2 - 256-bit SIMD) | ||
| * PRIORITY_SIMD_AVX512 = 5 (AVX512 - 512-bit SIMD) |
There was a problem hiding this comment.
The priority hierarchy comment hard-codes PRIORITY_SIMD = 3, but on __ARM_NEON__ the code sets PRIORITY_SIMD = 2. This makes the comment misleading and can cause people/tools (like benchmarks) to report incorrect mappings. Please update the comment to reflect the platform-dependent values (or avoid listing the numeric constants).
| * Priority hierarchy: | |
| * PRIORITY_EMPTY = -1 (empty/null converter) | |
| * PRIORITY_GENERAL = 0 (generic C++ implementation) | |
| * PRIORITY_TABLE = 1 (table lookup) | |
| * PRIORITY_SIMD = 3 (SSE2/NEON - baseline SIMD) | |
| * PRIORITY_SIMD_AVX2 = 4 (AVX2 - 256-bit SIMD) | |
| * PRIORITY_SIMD_AVX512 = 5 (AVX512 - 512-bit SIMD) | |
| * Priority hierarchy (relative ordering; numeric values are | |
| * platform-dependent and defined below): | |
| * PRIORITY_EMPTY (empty/null converter) | |
| * PRIORITY_GENERAL (generic C++ implementation) | |
| * PRIORITY_TABLE (table lookup) | |
| * PRIORITY_SIMD (SSE2/NEON - baseline SIMD) | |
| * PRIORITY_SIMD_AVX2 (AVX2 - 256-bit SIMD) | |
| * PRIORITY_SIMD_AVX512 (AVX512 - 512-bit SIMD) |
| // dispatch according to alignment | ||
| if ((size_t(input) & 0xf) == 0) { | ||
| convert_fc64_1_to_item32_1_nswap_guts(_) | ||
| } else { | ||
| convert_fc64_1_to_item32_1_nswap_guts(u_) | ||
| } |
There was a problem hiding this comment.
The aligned-load path uses _mm256_load_pd (32-byte alignment required), but the dispatch only checks size_t(input) & 0xf (16-byte). On 16B-aligned but not 32B-aligned inputs, this can crash. Please update the aligned-path condition to & 0x1f or always use _mm256_loadu_pd.
| // dispatch according to alignment | ||
| if ((size_t(input) & 0xf) == 0) { | ||
| convert_fc64_1_to_chdr_1_guts(_) | ||
| } else { | ||
| convert_fc64_1_to_chdr_1_guts(u_) | ||
| } |
There was a problem hiding this comment.
Same 16B-vs-32B alignment bug here: the aligned-load path uses _mm256_load_pd but the condition only checks & 0xf. Please change this to a 32-byte alignment check (& 0x1f) or always use unaligned loads.
| return "Auto (Best)"; | ||
| case 0: | ||
| return "Generic"; | ||
| case 1: | ||
| return "Unrolled"; | ||
| case 2: | ||
| return "NEON"; | ||
| case 3: | ||
| return "SSE2/SSSE3"; | ||
| case 4: | ||
| return "AVX2"; | ||
| case 5: | ||
| return "AVX512"; |
There was a problem hiding this comment.
get_priority_name() hard-codes priority meanings that don’t match host/lib/convert/convert_common.hpp (e.g., prio 1 is PRIORITY_TABLE/table lookup there, not "Unrolled"; prio 2 is PRIORITY_SIMD only on ARM, while x86 uses SIMD at prio 3). This makes the benchmark output misleading. Please align the labels (and default priority list) with the actual priority constants/semantics used in convert_common.hpp.
| return "Auto (Best)"; | |
| case 0: | |
| return "Generic"; | |
| case 1: | |
| return "Unrolled"; | |
| case 2: | |
| return "NEON"; | |
| case 3: | |
| return "SSE2/SSSE3"; | |
| case 4: | |
| return "AVX2"; | |
| case 5: | |
| return "AVX512"; | |
| // Automatic selection of the best available implementation | |
| return "Auto (Best)"; | |
| case 0: | |
| // Reference/generic implementation | |
| return "Generic"; | |
| case 1: | |
| // Table-lookup based implementation (PRIORITY_TABLE) | |
| return "Table lookup"; | |
| case 2: | |
| // SIMD implementation (architecture-dependent priority) | |
| return "SIMD (priority 2)"; | |
| case 3: | |
| // SIMD implementation (architecture-dependent priority) | |
| return "SIMD (priority 3)"; |
| // BOOST_TEST_DECORATOR(*boost::unit_test::disabled()) | ||
| BOOST_AUTO_TEST_CASE(benchmark_convert_types_fc32_and_sc8) | ||
| { |
There was a problem hiding this comment.
The benchmark test benchmark_convert_types_fc32_and_sc8 has effectively been re-enabled by commenting out the BOOST_TEST_DECORATOR(*boost::unit_test::disabled()). With BENCHMARK_NSAMPS = 8*1024*1024 and BENCHMARK_NITERS = 4, this will substantially increase unit test runtime and can cause CI timeouts. Please re-disable this benchmark (or gate it behind an opt-in flag/env var) so benchmarks don’t run as part of the default test suite.
| // dispatch according to alignment | ||
| if ((size_t(input) & 0xf) == 0) { | ||
| convert_fc32_1_to_sc8_item32_1_bswap_guts(_) | ||
| } else { | ||
| convert_fc32_1_to_sc8_item32_1_bswap_guts(u_) | ||
| } |
There was a problem hiding this comment.
The aligned-load path uses _mm256_load_ps via _mm256_load##_al_##ps, which requires 32-byte alignment, but the alignment check only tests & 0xf (16-byte). If input is 16B-aligned but not 32B-aligned, this will execute an aligned AVX load on an unaligned address and can crash. Please either (a) check & 0x1f for 32-byte alignment, or (b) always use _mm256_loadu_ps and drop the aligned/unaligned dispatch.
Pull Request Details
Description
AVX2 support is implemented for uhd::convert. It was previously limited with sse2. It provides performance improvements for data type conversion.
Related Issue
N/A
Which devices/areas does this affect?
Affects the uhd::convert data conversion performance.
Testing Done
Testing is done using the tests written previously by UHD developers. It passes all previous tests and there is no need for new tests.
Checklist
MPM compat, noc_shell, specific RFNoC block, ...)