diff --git a/cmake/CheckAVX.cmake b/cmake/CheckAVX.cmake index f39da4c2f..b17d4860d 100644 --- a/cmake/CheckAVX.cmake +++ b/cmake/CheckAVX.cmake @@ -50,46 +50,122 @@ set(AVX512IFMA_CODE " } ") -macro(check_avx_lang lang type flags) - set(__FLAG_I 1) - set(CMAKE_REQUIRED_FLAGS_SAVE ${CMAKE_REQUIRED_FLAGS}) - set(CMAKE_REQUIRED_QUIET_SAVE ${CMAKE_REQUIRED_QUIET}) - foreach(__FLAG ${flags}) - if(NOT ${lang}_${type}_FOUND) - set(CMAKE_REQUIRED_QUIET TRUE) - set(CMAKE_REQUIRED_FLAGS ${__FLAG}) +set(SMALL_PRIME_FFT_AVX2_CODE " + #include + #if !defined(__x86_64__) || !(defined(__GNUC__) || defined(__clang__)) || \ + !defined(__AVX2__) || !defined(__FMA__) + #error Small-prime AVX2/FMA kernels are not enabled + #endif + int main() { + __m256d a = _mm256_set1_pd(1.0); + a = _mm256_fmadd_pd(a, a, a); + return _mm256_cvtsd_f64(a) != 2.0; + } +") + +set(SMALL_PRIME_FFT_AVX512_CODE " + #include + #if !defined(__x86_64__) || !(defined(__GNUC__) || defined(__clang__)) || \ + !defined(__AVX2__) || !defined(__FMA__) || !defined(__AVX512F__) || !defined(__AVX512DQ__) + #error Small-prime AVX-512 kernels are not enabled + #endif + int main() { + __m512d a = _mm512_set1_pd(1.0); + a = _mm512_fmadd_pd(a, a, a); + a = _mm512_and_pd(a, a); + __m256d high = _mm512_extractf64x4_pd(a, 1); + return _mm256_cvtsd_f64(high) != 2.0; + } +") + +function(check_avx_lang lang type flags) + set(found FALSE) + set(selected_flags "") + set(required_flags_save "${CMAKE_REQUIRED_FLAGS}") + set(CMAKE_REQUIRED_QUIET TRUE) + foreach(flag ${flags}) + if(NOT found) + set(CMAKE_REQUIRED_FLAGS "${required_flags_save} ${flag}") + # Cache the probe, not a previous selection. Source/flag/toolchain changes must be + # checked again when an existing build directory is reconfigured. + string(CONCAT probe_context + "${${type}_CODE}|${CMAKE_REQUIRED_FLAGS}|${CMAKE_REQUIRED_DEFINITIONS}|" + "${CMAKE_REQUIRED_INCLUDES}|${CMAKE_REQUIRED_LIBRARIES}|" + "${CMAKE_REQUIRED_LINK_OPTIONS}|${CMAKE_REQUIRED_LINK_DIRECTORIES}|" + "${CMAKE_${lang}_COMPILER}|${CMAKE_${lang}_COMPILER_ID}|${CMAKE_${lang}_COMPILER_VERSION}|" + "${CMAKE_${lang}_COMPILER_TARGET}|${CMAKE_${lang}_FLAGS}|${CMAKE_${lang}_FLAGS_DEBUG}|" + "${CMAKE_${lang}_FLAGS_RELEASE}|${CMAKE_${lang}_FLAGS_RELWITHDEBINFO}|" + "${CMAKE_${lang}_FLAGS_MINSIZEREL}|${CMAKE_BUILD_TYPE}|${CMAKE_TRY_COMPILE_CONFIGURATION}|" + "${CMAKE_TRY_COMPILE_TARGET_TYPE}|${CMAKE_SYSROOT}|${CMAKE_OSX_ARCHITECTURES}|" + "${CMAKE_OSX_SYSROOT}|${CMAKE_OSX_DEPLOYMENT_TARGET}") + string(SHA256 probe_key "${probe_context}") + set(probe_var "${lang}_HAS_${type}_${probe_key}") + get_property(probe_checked GLOBAL PROPERTY "${probe_var}_CHECKED") + if(NOT probe_checked) + # Recheck once per configuration, including after linker/toolchain + # settings outside the probe's immediate context have changed. + unset(${probe_var} CACHE) + endif() if("${lang}" STREQUAL "CXX") - check_cxx_source_compiles("${${type}_CODE}" ${lang}_HAS_${type}_${__FLAG_I}) + check_cxx_source_compiles("${${type}_CODE}" ${probe_var}) else() - check_c_source_compiles("${${type}_CODE}" ${lang}_HAS_${type}_${__FLAG_I}) + check_c_source_compiles("${${type}_CODE}" ${probe_var}) endif() - if(${lang}_HAS_${type}_${__FLAG_I}) - set(${lang}_${type}_FOUND TRUE CACHE BOOL "${lang} ${type} support") - set(${lang}_${type}_FLAGS "${__FLAG}" CACHE STRING "${lang} ${type} flags") + set_property(GLOBAL PROPERTY "${probe_var}_CHECKED" TRUE) + if(${probe_var}) + set(found TRUE) + set(selected_flags "${flag}") endif() - math(EXPR __FLAG_I "${__FLAG_I}+1") endif() endforeach() - set(CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS_SAVE}) - set(CMAKE_REQUIRED_QUIET ${CMAKE_REQUIRED_QUIET_SAVE}) - - if(NOT ${lang}_${type}_FOUND) - set(${lang}_${type}_FOUND FALSE CACHE BOOL "${lang} ${type} support") - set(${lang}_${type}_FLAGS "" CACHE STRING "${lang} ${type} flags") - endif() - + set(${lang}_${type}_FOUND ${found} CACHE BOOL "${lang} ${type} support" FORCE) + set(${lang}_${type}_FLAGS "${selected_flags}" CACHE STRING "${lang} ${type} flags" FORCE) + set(${lang}_${type}_FOUND ${found} PARENT_SCOPE) + set(${lang}_${type}_FLAGS "${selected_flags}" PARENT_SCOPE) mark_as_advanced(${lang}_${type}_FOUND ${lang}_${type}_FLAGS) +endfunction() + +# These bundles match the guards in small_prime_fft_arithmetic.hpp. IFMA is a +# separate algebra capability; neither IFMA nor AVX512F alone establishes them. +macro(check_small_prime_fft) + check_host_cpu_feature(AVX2 "avx2") + check_host_cpu_feature(FMA "fma") + check_host_cpu_feature(AVX512F "avx512f") + check_host_cpu_feature(AVX512DQ "avx512dq") + foreach(feature SMALL_PRIME_FFT_AVX2 SMALL_PRIME_FFT_AVX512) + set(CXX_${feature}_FOUND FALSE) + set(CXX_${feature}_FLAGS "") + endforeach() + if(HOST_CPU_AVX2_FOUND AND HOST_CPU_FMA_FOUND) + # Always export explicit flags, even if the configuring project already supplies them. + check_avx_lang(CXX SMALL_PRIME_FFT_AVX2 "-mavx2 -mfma") + if(CXX_SMALL_PRIME_FFT_AVX2_FOUND AND HOST_CPU_AVX512F_FOUND AND HOST_CPU_AVX512DQ_FOUND) + check_avx_lang(CXX SMALL_PRIME_FFT_AVX512 "-mavx2 -mfma -mavx512f -mavx512dq") + endif() + endif() + foreach(feature SMALL_PRIME_FFT_AVX2 SMALL_PRIME_FFT_AVX512) + set(CXX_${feature}_FOUND ${CXX_${feature}_FOUND} CACHE BOOL "CXX ${feature} support" FORCE) + set(CXX_${feature}_FLAGS "${CXX_${feature}_FLAGS}" CACHE STRING "CXX ${feature} flags" FORCE) + mark_as_advanced(CXX_${feature}_FOUND CXX_${feature}_FLAGS) + endforeach() endmacro() # Both widths use the same host contract; VL is required by the four-lane kernel. macro(check_avx512ifma) + # Compiler AVX-512 options can also enable AVX2/FMA implicitly. Check that + # baseline without depending on the FFT probes or requiring AVX512DQ. + check_host_cpu_feature(AVX2 "avx2") + check_host_cpu_feature(FMA "fma") check_host_cpu_feature(AVX512F "avx512f") check_host_cpu_feature(AVX512IFMA "avx512ifma") check_host_cpu_feature(AVX512VL "avx512vl") - if(HOST_CPU_AVX512F_FOUND AND HOST_CPU_AVX512IFMA_FOUND AND HOST_CPU_AVX512VL_FOUND) - check_avx_lang(CXX "AVX512IFMA" " ;-mavx512f -mavx512vl -mavx512ifma;/arch:AVX512") + if(HOST_CPU_AVX2_FOUND AND HOST_CPU_FMA_FOUND AND HOST_CPU_AVX512F_FOUND AND + HOST_CPU_AVX512IFMA_FOUND AND HOST_CPU_AVX512VL_FOUND) + check_avx_lang(CXX "AVX512IFMA" "-mavx512f -mavx512vl -mavx512ifma;/arch:AVX512") else() + set(CXX_AVX512IFMA_FOUND FALSE) + set(CXX_AVX512IFMA_FLAGS "") set(CXX_AVX512IFMA_FOUND FALSE CACHE BOOL "CXX AVX512IFMA support" FORCE) set(CXX_AVX512IFMA_FLAGS "" CACHE STRING "CXX AVX512IFMA flags" FORCE) endif() @@ -106,6 +182,8 @@ macro(check_avx) else() set(C_AVX_FOUND FALSE CACHE BOOL "C AVX support" FORCE) set(CXX_AVX_FOUND FALSE CACHE BOOL "CXX AVX support" FORCE) + set(C_AVX_FLAGS "" CACHE STRING "C AVX flags" FORCE) + set(CXX_AVX_FLAGS "" CACHE STRING "CXX AVX flags" FORCE) endif() if(HOST_CPU_AVX2_FOUND) @@ -114,6 +192,8 @@ macro(check_avx) else() set(C_AVX2_FOUND FALSE CACHE BOOL "C AVX2 support" FORCE) set(CXX_AVX2_FOUND FALSE CACHE BOOL "CXX AVX2 support" FORCE) + set(C_AVX2_FLAGS "" CACHE STRING "C AVX2 flags" FORCE) + set(CXX_AVX2_FLAGS "" CACHE STRING "CXX AVX2 flags" FORCE) endif() if(HOST_CPU_AVX512F_FOUND) @@ -122,5 +202,7 @@ macro(check_avx) else() set(C_AVX512_FOUND FALSE CACHE BOOL "C AVX512 support" FORCE) set(CXX_AVX512_FOUND FALSE CACHE BOOL "CXX AVX512 support" FORCE) + set(C_AVX512_FLAGS "" CACHE STRING "C AVX512 flags" FORCE) + set(CXX_AVX512_FLAGS "" CACHE STRING "CXX AVX512 flags" FORCE) endif() endmacro() diff --git a/cmake/CheckHostCPUFeature.cmake b/cmake/CheckHostCPUFeature.cmake index 27e644aa3..e1aa84125 100644 --- a/cmake/CheckHostCPUFeature.cmake +++ b/cmake/CheckHostCPUFeature.cmake @@ -6,6 +6,25 @@ function(check_host_cpu_feature feature builtin_name) string(TOUPPER "${feature}" feature_upper) set(result_var "HOST_CPU_${feature_upper}_FOUND") + # Recheck the current host once per configuration. A cached result from a + # different host or toolchain is not evidence for this configuration. + string(CONCAT probe_context + "${CMAKE_CROSSCOMPILING}|${CMAKE_HOST_SYSTEM_PROCESSOR}|${CMAKE_CXX_COMPILER}|" + "${CMAKE_CXX_COMPILER_ID}|${CMAKE_CXX_COMPILER_VERSION}|${CMAKE_CXX_COMPILER_TARGET}|" + "${CMAKE_CXX_FLAGS}|${CMAKE_CXX_FLAGS_DEBUG}|${CMAKE_CXX_FLAGS_RELEASE}|" + "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}|${CMAKE_CXX_FLAGS_MINSIZEREL}|${CMAKE_BUILD_TYPE}|" + "${CMAKE_TRY_COMPILE_CONFIGURATION}|${CMAKE_SYSROOT}|${CMAKE_OSX_ARCHITECTURES}|" + "${CMAKE_OSX_SYSROOT}|${CMAKE_OSX_DEPLOYMENT_TARGET}|${CMAKE_REQUIRED_FLAGS}|${builtin_name}") + string(SHA256 context "${probe_context}") + get_property(checked_context GLOBAL PROPERTY "${result_var}_CONTEXT") + if(NOT "${checked_context}" STREQUAL "${context}") + unset(${result_var}) + unset(${result_var} CACHE) + unset(${result_var}_COMPILED CACHE) + unset(${result_var}_EXITCODE CACHE) + endif() + set_property(GLOBAL PROPERTY "${result_var}_CONTEXT" "${context}") + if(CMAKE_CROSSCOMPILING OR NOT CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|i[3-6]86)$") set(${result_var} FALSE CACHE BOOL "${feature} available on host CPU" FORCE) set(${result_var} FALSE PARENT_SCOPE) @@ -28,8 +47,12 @@ function(check_host_cpu_feature feature builtin_name) set(msvc_test "max_leaf >= 1 && (leaf1[2] & (1 << 27)) && (leaf1[2] & (1 << 28)) && ((_xgetbv(0) & 0x6) == 0x6)") elseif(feature_upper STREQUAL "AVX2") set(msvc_test "max_leaf >= 7 && (leaf1[2] & (1 << 27)) && (leaf1[2] & (1 << 28)) && ((_xgetbv(0) & 0x6) == 0x6) && (leaf7[1] & (1 << 5))") + elseif(feature_upper STREQUAL "FMA") + set(msvc_test "max_leaf >= 1 && (leaf1[2] & (1 << 12)) && (leaf1[2] & (1 << 27)) && (leaf1[2] & (1 << 28)) && ((_xgetbv(0) & 0x6) == 0x6)") elseif(feature_upper STREQUAL "AVX512F") set(msvc_test "max_leaf >= 7 && (leaf1[2] & (1 << 27)) && ((_xgetbv(0) & 0xE6) == 0xE6) && (leaf7[1] & (1 << 16))") + elseif(feature_upper STREQUAL "AVX512DQ") + set(msvc_test "max_leaf >= 7 && (leaf1[2] & (1 << 27)) && ((_xgetbv(0) & 0xE6) == 0xE6) && (leaf7[1] & (1 << 16)) && (leaf7[1] & (1 << 17))") elseif(feature_upper STREQUAL "AVX512IFMA") set(msvc_test "max_leaf >= 7 && (leaf1[2] & (1 << 27)) && ((_xgetbv(0) & 0xE6) == 0xE6) && (leaf7[1] & (1 << 16)) && (leaf7[1] & (1 << 21))") elseif(feature_upper STREQUAL "AVX512VL") diff --git a/cmake/tests/small_prime_features/CMakeLists.txt b/cmake/tests/small_prime_features/CMakeLists.txt new file mode 100644 index 000000000..e273b79a7 --- /dev/null +++ b/cmake/tests/small_prime_features/CMakeLists.txt @@ -0,0 +1,74 @@ +cmake_minimum_required(VERSION 3.22) +project(small_prime_feature_consumer LANGUAGES C CXX) + +get_filename_component(CRYPTO3_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE) +option(FEATURE_CHECK_COMPILE_ONLY "Build an object without running target code" OFF) +set(FEATURE_CHECK_HOST "native" CACHE STRING "native, full, avx2, no-fma, no-dq, no-ifma, or unsupported") + +if(NOT FEATURE_CHECK_HOST STREQUAL "native") + if(NOT FEATURE_CHECK_COMPILE_ONLY OR CMAKE_CROSSCOMPILING) + message(FATAL_ERROR "Simulated host checks require compile-only mode and a native compiler configuration") + endif() + # Isolate host-policy simulation here. The compiler probes and Crypto3 targets + # below are real; a simulated CPU never authorizes executing the built code. + list(APPEND CMAKE_MODULE_PATH "${CRYPTO3_SOURCE_DIR}/cmake") + include(CheckHostCPUFeature) + set(test_features AVX AVX2 FMA AVX512F AVX512DQ AVX512VL AVX512IFMA) + if(FEATURE_CHECK_HOST STREQUAL "avx2") + set(test_features AVX AVX2 FMA) + elseif(FEATURE_CHECK_HOST STREQUAL "no-fma") + list(REMOVE_ITEM test_features FMA) + elseif(FEATURE_CHECK_HOST STREQUAL "no-dq") + list(REMOVE_ITEM test_features AVX512DQ) + elseif(FEATURE_CHECK_HOST STREQUAL "no-ifma") + list(REMOVE_ITEM test_features AVX512IFMA) + elseif(FEATURE_CHECK_HOST STREQUAL "unsupported") + set(test_features "") + elseif(NOT FEATURE_CHECK_HOST STREQUAL "full") + message(FATAL_ERROR "Unknown simulated host: ${FEATURE_CHECK_HOST}") + endif() + function(check_host_cpu_feature feature builtin_name) + if(feature IN_LIST test_features) + set(supported TRUE) + else() + set(supported FALSE) + endif() + set(HOST_CPU_${feature}_FOUND ${supported} CACHE BOOL "Simulated host feature" FORCE) + set(HOST_CPU_${feature}_FOUND ${supported} PARENT_SCOPE) + endfunction() +endif() + +set(BUILD_TESTS OFF CACHE BOOL "Build only requested checks") +set(BUILD_BENCH_TESTS OFF CACHE BOOL "No profiling runs") +enable_testing() +add_subdirectory("${CRYPTO3_SOURCE_DIR}" crypto3 EXCLUDE_FROM_ALL) + +if(FEATURE_CHECK_COMPILE_ONLY) + add_library(feature_consumer OBJECT feature_consumer.cpp) +else() + add_executable(feature_consumer feature_consumer.cpp) + add_test(NAME small_prime_feature_consumer COMMAND feature_consumer) +endif() +target_compile_features(feature_consumer PRIVATE cxx_std_23) +# No instruction flags or feature definitions are supplied by this consumer. +target_link_libraries(feature_consumer PRIVATE crypto3::math) + +foreach(feature AVX2 AVX512) + if(CXX_SMALL_PRIME_FFT_${feature}_FOUND) + set(expect_${feature} 1) + else() + set(expect_${feature} 0) + endif() +endforeach() +set(expect_IFMA 0) +if(expect_AVX512 AND CXX_AVX512IFMA_FOUND) + set(expect_IFMA 1) +endif() +target_compile_definitions(feature_consumer PRIVATE + EXPECT_FFT_AVX2=${expect_AVX2} EXPECT_FFT_AVX512=${expect_AVX512} EXPECT_CRT_IFMA=${expect_IFMA}) +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/selection.txt" + "avx2=${expect_AVX2}\navx512=${expect_AVX512}\ncrt_ifma=${expect_IFMA}\n" + "avx2_flags=${CXX_SMALL_PRIME_FFT_AVX2_FLAGS}\navx512_flags=${CXX_SMALL_PRIME_FFT_AVX512_FLAGS}\n" + "ifma_found=${CXX_AVX512IFMA_FOUND}\nifma_flags=${CXX_AVX512IFMA_FLAGS}\n" + "cross=${CMAKE_CROSSCOMPILING}\ncompiler=${CMAKE_CXX_COMPILER_ID}\n" + "frontend=${CMAKE_CXX_COMPILER_FRONTEND_VARIANT}\n") diff --git a/cmake/tests/small_prime_features/feature_consumer.cpp b/cmake/tests/small_prime_features/feature_consumer.cpp new file mode 100644 index 000000000..8bfab2d68 --- /dev/null +++ b/cmake/tests/small_prime_features/feature_consumer.cpp @@ -0,0 +1,27 @@ +// Copyright (c) 2026. Distributed under the MIT License. + +#include + +#ifdef CRYPTO3_SMALL_PRIME_FFT_AVX2_SELECTED +constexpr bool fft_avx2 = true; +#else +constexpr bool fft_avx2 = false; +#endif +#ifdef CRYPTO3_SMALL_PRIME_FFT_AVX512_SELECTED +constexpr bool fft_avx512 = true; +#else +constexpr bool fft_avx512 = false; +#endif +#ifdef CRYPTO3_SMALL_PRIME_CRT_IFMA_SELECTED +constexpr bool crt_ifma = true; +#else +constexpr bool crt_ifma = false; +#endif + +static_assert(fft_avx2 == bool(EXPECT_FFT_AVX2), "AVX2/FMA FFT selection does not match configuration"); +static_assert(fft_avx512 == bool(EXPECT_FFT_AVX512), "AVX512F/DQ FFT selection does not match configuration"); +static_assert(crt_ifma == bool(EXPECT_CRT_IFMA), "IFMA CRT selection does not match configuration"); + +int main() { + return 0; +} diff --git a/cmake/tests/small_prime_features/run_checks.py b/cmake/tests/small_prime_features/run_checks.py new file mode 100644 index 000000000..62ca75878 --- /dev/null +++ b/cmake/tests/small_prime_features/run_checks.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Check native CMake propagation and compile-only feature/fallback configurations.""" +import argparse +import json +from pathlib import Path +import platform +import shlex +import subprocess +import tempfile +import time + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--log-root', type=Path) + parser.add_argument('--cmake-arg', action='append', default=[]) + parser.add_argument('--x86-compile-only', action='store_true', + help='also use Apple Clang x86 objects on an ARM Mac; never run those objects') + parser.add_argument('--run-small-prime-tests', action='store_true', + help='build and run the four focused suites in the native configuration') + args = parser.parse_args() + source = Path(__file__).resolve().parent + repo = source.parents[2] + parent = args.log_root or repo / '.test-logs/small-prime-cmake' + parent.mkdir(parents=True, exist_ok=True) + logs = Path(tempfile.mkdtemp(prefix='checks-', dir=parent)).resolve() + commands = [] + results = {} + print('LOG_DIRECTORY=' + str(logs), flush=True) + + def command(label, argv): + print('START ' + label, flush=True) + started = time.monotonic() + with (logs / (label + '.stdout.log')).open('w') as stdout, (logs / (label + '.stderr.log')).open('w') as stderr: + result = subprocess.run(argv, stdout=stdout, stderr=stderr, timeout=600) + commands.append(dict(label=label, argv=argv, exit_status=result.returncode, + seconds=time.monotonic() - started)) + (logs / 'commands.json').write_text(json.dumps(commands, indent=2) + '\n') + if result.returncode: + raise RuntimeError(f'{label} failed: see {logs / (label + ".stderr.log")}') + print('PASS ' + label, flush=True) + + def check(label, build, options, expected=None): + command(label + '-configure', ['cmake', '-S', str(source), '-B', str(build), + '-DCMAKE_BUILD_TYPE=Release', '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON', + *args.cmake_arg, *options]) + selection = dict(line.split('=', 1) for line in (build / 'selection.txt').read_text().splitlines()) + actual = tuple(int(selection[name]) for name in ('avx2', 'avx512', 'crt_ifma')) + if expected is not None and actual != expected: + raise RuntimeError(f'{label}: expected {expected}, configured {actual}') + command(label + '-build', ['cmake', '--build', str(build), '--target', 'feature_consumer', '--parallel', '2']) + entries = json.loads((build / 'compile_commands.json').read_text()) + entry = next(item for item in entries if Path(item['file']).resolve() == source / 'feature_consumer.cpp') + argv = entry.get('arguments') or shlex.split(entry['command']) + # Explicit interface options must reach an external consumer even when the + # configuring project's global flags already enabled those instructions. + for enabled, required in ((actual[0], ['-mavx2', '-mfma']), + (actual[1], ['-mavx512f', '-mavx512dq'])): + if enabled and not all(flag in argv for flag in required): + raise RuntimeError(f'{label}: missing transitive flags: {argv}') + results[label] = {'selection': selection, 'compile_command': argv} + (logs / 'results.json').write_text(json.dumps(results, indent=2) + '\n') + return selection + + native = logs / 'native' + native_options = ['-DFEATURE_CHECK_HOST=native', '-DFEATURE_CHECK_COMPILE_ONLY=OFF'] + native_selection = check('native-fresh', native, native_options) + check('native-reconfigure', native, native_options) + command('native-run', ['ctest', '--test-dir', str(native), '-R', '^small_prime_feature_consumer$', + '--output-on-failure']) + + x86 = platform.machine().lower() in ('x86_64', 'amd64') + architecture = [] + if args.x86_compile_only: + if platform.system() != 'Darwin' or x86: + parser.error('--x86-compile-only is for an ARM Mac using Apple Clang') + architecture = ['-DCMAKE_OSX_ARCHITECTURES=x86_64'] + x86 = True + supported_compiler = (native_selection['compiler'] in ('GNU', 'Clang', 'AppleClang') and + native_selection['frontend'] != 'MSVC') + if x86 and supported_compiler: + compile_only = ['-DFEATURE_CHECK_COMPILE_ONLY=ON', *architecture] + cases = [('full', (1, 1, 1)), ('avx2', (1, 0, 0)), ('no-fma', (0, 0, 0)), + ('no-dq', (1, 0, 0)), ('no-ifma', (1, 1, 0)), ('unsupported', (0, 0, 0))] + for host, expected in cases: + selection = check(host + '-fresh', logs / host, [*compile_only, '-DFEATURE_CHECK_HOST=' + host], expected) + if host == 'no-dq' and selection['ifma_found'] != 'TRUE': + raise RuntimeError('DQ detection incorrectly disabled independent IFMA support') + + # Reuse a directory with successful compiler probes, then remove a host + # feature, reject instruction probes, and restore the original settings. + reused = logs / 'full' + check('no-dq-reconfigure', reused, [*compile_only, '-DFEATURE_CHECK_HOST=no-dq'], (1, 0, 0)) + check('compiler-rejects', reused, [*compile_only, '-DFEATURE_CHECK_HOST=full', + '-DCMAKE_REQUIRED_FLAGS=-crypto3-invalid-feature-probe-option'], (0, 0, 0)) + check('compiler-restored', reused, [*compile_only, '-DFEATURE_CHECK_HOST=full', + '-DCMAKE_REQUIRED_FLAGS='], (1, 1, 1)) + + # Check the header guards themselves with one compiler feature removed. + # Positive propagation above uses only the target's options; these + # negative compile-only probes deliberately override the inherited flags. + for name, override, expected in [('missing-fma-guard', '-mno-fma', (0, 0, 0)), + ('missing-dq-guard', '-mno-avx512dq', (1, 0, 0))]: + original = iter(results['compiler-restored']['compile_command']) + argv = [] + definitions = dict(zip(('EXPECT_FFT_AVX2', 'EXPECT_FFT_AVX512', 'EXPECT_CRT_IFMA'), expected)) + for arg in original: + if arg in ('-o', '-MF', '-MT', '-MQ'): + next(original) + elif arg not in ('-c', '-MD', '-MMD'): + key = arg[2:].split('=', 1)[0] if arg.startswith('-D') else '' + argv.append('-D' + key + '=' + str(definitions[key]) if key in definitions else arg) + command(name, [*argv, override, '-fsyntax-only']) + + toolchain = logs / 'cross.cmake' + # Explicit system selection makes this a CMake cross configuration even + # when the available compiler targets the same OS. No target code runs. + toolchain.write_text(f'set(CMAKE_SYSTEM_NAME {platform.system()})\n' + 'set(CMAKE_SYSTEM_PROCESSOR x86_64)\n') + check('cross', logs / 'cross', [*compile_only, '-DFEATURE_CHECK_HOST=native', + '-DCMAKE_TOOLCHAIN_FILE=' + str(toolchain), + '-DHOST_CPU_AVX2_FOUND=TRUE', '-DHOST_CPU_FMA_FOUND=TRUE', + '-DHOST_CPU_AVX512F_FOUND=TRUE', '-DHOST_CPU_AVX512DQ_FOUND=TRUE', + '-DHOST_CPU_AVX512VL_FOUND=TRUE', '-DHOST_CPU_AVX512IFMA_FOUND=TRUE'], (0, 0, 0)) + if results['cross']['selection']['cross'] != 'TRUE': + raise RuntimeError('cross-compilation check was not a cross configuration') + + if args.run_small_prime_tests: + names = ['arithmetic', 'crt', 'transform', 'backend'] + targets = ['crypto3_math_small_prime_fft_' + name + '_test' for name in names] + command('native-tests-build', ['cmake', '--build', str(native), '--target', *targets, '--parallel', '2']) + pattern = '^crypto3_math_small_prime_fft_(' + '|'.join(names) + ')_test$' + command('native-tests-list', ['ctest', '--test-dir', str(native), '-R', pattern, '--show-only=json-v1']) + tests = json.loads((logs / 'native-tests-list.stdout.log').read_text())['tests'] + if {test['name'] for test in tests} != set(targets): + raise RuntimeError('the focused tests were not registered') + command('native-tests-run', ['ctest', '--test-dir', str(native), '-R', pattern, '--output-on-failure']) + print('All requested checks passed. Simulated and cross targets were compile-only.', flush=True) + + +if __name__ == '__main__': + main() diff --git a/libs/math/CMakeLists.txt b/libs/math/CMakeLists.txt index 96f012d2a..0fbd7d411 100644 --- a/libs/math/CMakeLists.txt +++ b/libs/math/CMakeLists.txt @@ -39,6 +39,19 @@ target_link_libraries(${CMAKE_WORKSPACE_NAME}_${CURRENT_PROJECT_NAME} INTERFACE Boost::headers Boost::random) +include(CheckAVX) +check_small_prime_fft() +if(CXX_SMALL_PRIME_FFT_AVX512_FOUND) + separate_arguments(SMALL_PRIME_FFT_OPTIONS NATIVE_COMMAND "${CXX_SMALL_PRIME_FFT_AVX512_FLAGS}") +elseif(CXX_SMALL_PRIME_FFT_AVX2_FOUND) + separate_arguments(SMALL_PRIME_FFT_OPTIONS NATIVE_COMMAND "${CXX_SMALL_PRIME_FFT_AVX2_FLAGS}") +else() + set(SMALL_PRIME_FFT_OPTIONS "") +endif() +# Header-only users need these options too. Algebra independently supplies IFMA +# options and its definition through the public dependency above. +target_compile_options(${CMAKE_WORKSPACE_NAME}_${CURRENT_PROJECT_NAME} INTERFACE ${SMALL_PRIME_FFT_OPTIONS}) + cm_deploy(TARGETS ${CMAKE_WORKSPACE_NAME}_${CURRENT_PROJECT_NAME} INCLUDE include NAMESPACE ${CMAKE_WORKSPACE_NAME}::) diff --git a/libs/math/docs/polynomial_arithmetic.md b/libs/math/docs/polynomial_arithmetic.md index 9f0832b57..e581f5d60 100644 --- a/libs/math/docs/polynomial_arithmetic.md +++ b/libs/math/docs/polynomial_arithmetic.md @@ -91,6 +91,44 @@ transform kernels depend on the compiler target, and IFMA acceleration also depe configuration. A binary must run on a CPU supporting its compiled instruction set. This backend executes serially and provides no constant-time guarantee. +For ordinary native CMake builds, `crypto3::math` exports the supported FFT instruction options through its +interface. Detection requires both CPU/OS support and a successful compiler probe matching the kernel guards: + +* AVX2 kernels require an x86-64 GCC-compatible compiler with AVX2 and FMA enabled. +* Eight-lane kernels additionally require AVX512F and AVX512DQ. +* IFMA CRT export additionally requires `CRYPTO3_HAS_AVX512IFMA`, supplied independently through `crypto3::algebra` + after checking AVX512F, AVX512VL, and AVX512IFMA, plus the AVX2/FMA baseline that compiler AVX-512 options can + implicitly enable. This check does not require DQ; the eight-lane FFT and CRT paths check DQ separately. + +A downstream target linking `crypto3::math` inherits these options and the algebra feature definition; it need not +add instruction flags itself. The probes use explicit instruction options, without enabling `-march=native` or +unsafe floating-point transformations. Unsupported configurations retain the existing scalar/exact fallbacks. +The SIMD FFT implementations currently use GCC-compatible x86-64 assembly; compiler acceptance of unrelated +intrinsics alone does not enable them. + +When `CMAKE_CROSSCOMPILING` is true, host probes do not run and host results cannot enable target instructions. +An explicitly configured cross toolchain may still supply target instruction options and feature definitions; +their suitability is the toolchain owner's responsibility. Set up cross-compilation through a CMake toolchain +file rather than using the build machine as evidence of target capabilities. + +The options also travel with installed interface targets. This is build-time selection, with no runtime CPU +dispatch: binaries require the enabled instruction sets on every machine where they run. An installed native +build does not redetect the downstream machine's CPU. Configure a separate build/toolchain for a different +deployment baseline, and use a fresh build directory when moving configuration to another host. + +The focused CMake consumer checks are available without running a profiling workload: + +```sh +python3 cmake/tests/small_prime_features/run_checks.py --run-small-prime-tests +``` + +On x86-64 this checks native propagation, actual FFT/CRT selection macros, missing-feature and compiler-rejection +cases, cross-compilation, and fresh/reused build directories. Simulated hosts and cross targets are compile-only; +only the native configuration runs the arithmetic, CRT, transform, and backend tests. Logs are retained under +`.test-logs/small-prime-cmake/`. On an ARM Mac, `--x86-compile-only` additionally checks the x86 configurations using +Apple Clang; this supplies no x86 runtime coverage. The repository's CMake modules submodule and Boost development +packages must be available, as for an ordinary build. + The constructor is ```cpp