diff --git a/.github/workflows/continuous.yml b/.github/workflows/continuous.yml index 56a8d5056..42e65f3c1 100644 --- a/.github/workflows/continuous.yml +++ b/.github/workflows/continuous.yml @@ -74,45 +74,23 @@ jobs: uses: SimenB/github-actions-cpu-cores@v2 id: cpu-cores - # FindSIMD, MeshFEMCore, and Tight-Inclusion all compile with - # -march=native, which bakes the building runner's ISA into every cached - # object file. ccache hashes the literal flag string, not what it expands - # to, so restored objects are only safe on a runner with the same ISA. - # The CPU model name has not been enough to guarantee that: EPYC 9V74 - # runners restored each other's objects and crashed with SIGILL. On Linux - # we also hash the flags the compiler expands -march=native to, which is - # exactly what ends up in the object files. We keep the model name in the - # hash too, so the key is never weaker than before. - - name: CPU fingerprint (Linux/macOS) - if: runner.os != 'Windows' - run: | - if [ "$RUNNER_OS" = "Linux" ]; then - CPU=$(grep -m1 '^model name' /proc/cpuinfo | cut -d: -f2-) - ISA=$(echo | c++ -march=native -E -v - 2>&1 | grep -oE -- ' -m[a-z0-9=._+-]+' | sort -u | tr -d '\n') - echo "ISA:$ISA" - KEY=$(printf '%s\n%s' "$CPU" "$ISA" | sha256sum | cut -c1-12) - else - CPU=$(sysctl -n machdep.cpu.brand_string) - KEY=$(printf '%s' "$CPU" | shasum -a 256 | cut -c1-12) - fi - echo "CPU: $CPU" - echo "CPU_KEY=$KEY" >> "$GITHUB_ENV" - - - name: CPU fingerprint (Windows) - if: runner.os == 'Windows' + # On Linux, explicitly build for x86-64-v3 instead of -march=native. This + # avoids issues with ccache restoring from a runner with a different ISA. + - name: Check x86-64-v3 support (Linux) + if: runner.os == 'Linux' run: | - $cpu = Get-CimInstance Win32_Processor | Select-Object -First 1 -ExpandProperty Name - Write-Host "CPU: $cpu" - $stream = [IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($cpu)) - $key = (Get-FileHash -InputStream $stream -Algorithm SHA256).Hash.Substring(0, 12) - "CPU_KEY=$key" | Out-File -FilePath $env:GITHUB_ENV -Append + for f in avx2 bmi1 bmi2 f16c fma abm movbe xsave; do + grep -qw "$f" /proc/cpuinfo || { echo "::error::Runner CPU lacks $f, which x86-64-v3 requires"; exit 1; } + done + echo "CPU: $(grep -m1 '^model name' /proc/cpuinfo | cut -d: -f2-)" + echo "SIMD_ARGS=-DSIMD_CXX_FLAGS=-march=x86-64-v3" >> "$GITHUB_ENV" - name: Cache Build id: cache-build uses: actions/cache@v5 with: path: ${{ env.CACHE_PATH }} - key: ${{ runner.os }}-${{ matrix.config }}-${{ env.CPU_KEY }}-cache + key: ${{ runner.os }}-${{ matrix.config }}-cache - name: Prepare ccache run: | @@ -120,6 +98,9 @@ jobs: ccache -V && ccache --show-config ccache --show-stats && ccache --zero-stats + # Debug keeps the asserts (NDEBUG stays undefined) and -O0 but drops -g. + # Debug info made the Debug ccache about 20x larger than Release, and CI + # never reads it: assert and Catch2 messages carry __FILE__ and __LINE__. - name: Configure (Linux/macOS) if: runner.os != 'Windows' run: | @@ -127,6 +108,9 @@ jobs: cd build cmake .. \ -DIPC_TOOLKIT_BUILD_TESTS=ON \ + -DCMAKE_C_FLAGS_DEBUG=-O0 \ + -DCMAKE_CXX_FLAGS_DEBUG=-O0 \ + $SIMD_ARGS \ -DCMAKE_BUILD_TYPE=${{ matrix.config }} - name: Configure (Windows) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c39c230a6..be126ddc9 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -37,35 +37,27 @@ jobs: uses: SimenB/github-actions-cpu-cores@v2 id: cpu-cores - # FindSIMD, MeshFEMCore, and Tight-Inclusion all compile with - # -march=native, which bakes the building runner's ISA into every cached - # object file. ccache hashes the literal flag string, not what it expands - # to, so restored objects are only safe on a runner with the same ISA. - # The CPU model name has not been enough to guarantee that: EPYC 9V74 - # runners restored each other's objects and crashed with SIGILL. We also - # hash the flags the compiler expands -march=native to, which is exactly - # what ends up in the object files. We keep the model name in the hash too, - # so the key is never weaker than before. - - name: CPU fingerprint + # On Linux, explicitly build for x86-64-v3 instead of -march=native. This + # avoids issues with ccache restoring from a runner with a different ISA. + - name: Check x86-64-v3 support run: | - CPU=$(grep -m1 '^model name' /proc/cpuinfo | cut -d: -f2-) - ISA=$(echo | c++ -march=native -E -v - 2>&1 | grep -oE -- ' -m[a-z0-9=._+-]+' | sort -u | tr -d '\n') - echo "CPU: $CPU" - echo "ISA:$ISA" - echo "CPU_KEY=$(printf '%s\n%s' "$CPU" "$ISA" | sha256sum | cut -c1-12)" >> "$GITHUB_ENV" + for f in avx2 bmi1 bmi2 f16c fma abm movbe xsave; do + grep -qw "$f" /proc/cpuinfo || { echo "::error::Runner CPU lacks $f, which x86-64-v3 requires"; exit 1; } + done + echo "CPU: $(grep -m1 '^model name' /proc/cpuinfo | cut -d: -f2-)" - name: Cache Build id: cache-build uses: actions/cache@v5 with: path: ${{ env.CACHE_PATH }} - key: coverage-${{ env.CPU_KEY }}-cache + key: coverage-cache - name: Prepare ccache run: | ccache --max-size=1.0G ccache -V && ccache --show-config - ccache --show-stats && ccache --zero-stats\ + ccache --show-stats && ccache --zero-stats - name: Configure run: | @@ -74,7 +66,8 @@ jobs: cmake .. \ -DIPC_TOOLKIT_BUILD_TESTS=ON \ -DIPC_TOOLKIT_WITH_CODE_COVERAGE=ON \ - -DCMAKE_BUILD_TYPE=Release \ + -DSIMD_CXX_FLAGS=-march=x86-64-v3 \ + -DCMAKE_BUILD_TYPE=Release - name: Build run: | diff --git a/.github/workflows/cuda.yml b/.github/workflows/cuda.yml index 8f96fcf14..f8a907aad 100644 --- a/.github/workflows/cuda.yml +++ b/.github/workflows/cuda.yml @@ -51,32 +51,24 @@ jobs: uses: SimenB/github-actions-cpu-cores@v2 id: cpu-cores - # FindSIMD, MeshFEMCore, and Tight-Inclusion all compile with - # -march=native, which bakes the building runner's ISA into every cached - # object file. ccache hashes the literal flag string, not what it expands - # to, so restored objects are only safe on a runner with the same ISA. - # The CPU model name has not been enough to guarantee that: EPYC 9V74 - # runners restored each other's objects and crashed with SIGILL. We also - # hash the flags the compiler expands -march=native to, which is exactly - # what ends up in the object files. We keep the model name in the hash too, - # so the key is never weaker than before. - - name: CPU fingerprint + # On Linux, explicitly build for x86-64-v3 instead of -march=native. This + # avoids issues with ccache restoring from a runner with a different ISA. + - name: Check x86-64-v3 support run: | - CPU=$(grep -m1 '^model name' /proc/cpuinfo | cut -d: -f2-) - ISA=$(echo | c++ -march=native -E -v - 2>&1 | grep -oE -- ' -m[a-z0-9=._+-]+' | sort -u | tr -d '\n') - echo "CPU: $CPU" - echo "ISA:$ISA" - echo "CPU_KEY=$(printf '%s\n%s' "$CPU" "$ISA" | sha256sum | cut -c1-12)" >> "$GITHUB_ENV" + for f in avx2 bmi1 bmi2 f16c fma abm movbe xsave; do + grep -qw "$f" /proc/cpuinfo || { echo "::error::Runner CPU lacks $f, which x86-64-v3 requires"; exit 1; } + done + echo "CPU: $(grep -m1 '^model name' /proc/cpuinfo | cut -d: -f2-)" # The CUDA key gets its own prefix because continuous.yml uses the same - # -- names, and cache entries are immutable. Whichever - # workflow saved first would otherwise own the entry for both builds. + # - names, and cache entries are immutable. Whichever workflow + # saved first would otherwise own the entry for both builds. - name: Cache Build id: cache-build uses: actions/cache@v5 with: path: ${{ env.CACHE_PATH }} - key: cuda-${{ runner.os }}-${{ matrix.config }}-${{ env.CPU_KEY }}-cache + key: cuda-${{ runner.os }}-${{ matrix.config }}-cache - name: Prepare ccache run: | @@ -84,12 +76,21 @@ jobs: ccache -V && ccache --show-config ccache --show-stats && ccache --zero-stats + # Debug keeps the asserts (NDEBUG stays undefined) and -O0 but drops -g. + # Debug info made the Debug ccache about 20x larger than Release, and CI + # never reads it: assert and Catch2 messages carry __FILE__ and __LINE__. + # The CUDA Debug flags are emptied instead of set to -O0, which nvcc would + # also apply to the device code. - name: Configure run: | mkdir -p build cd build cmake .. \ -DIPC_TOOLKIT_WITH_CUDA=ON \ + -DSIMD_CXX_FLAGS=-march=x86-64-v3 \ + -DCMAKE_C_FLAGS_DEBUG=-O0 \ + -DCMAKE_CXX_FLAGS_DEBUG=-O0 \ + -DCMAKE_CUDA_FLAGS_DEBUG= \ -DCMAKE_CUDA_ARCHITECTURES=75 \ -DSCALABLE_CCD_CUDA_ARCHITECTURES=75 \ -DIPC_TOOLKIT_BUILD_TESTS=ON \ diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 280f68c22..a9f51ddba 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,10 +29,24 @@ jobs: - name: Requirements run: | sudo apt-get update - sudo apt-get install doxygen python3-sphinx graphviz + sudo apt-get install doxygen python3-sphinx graphviz ccache + echo 'CACHE_PATH=~/.cache/ccache' >> "$GITHUB_ENV" python -m pip install --upgrade pip pip install -r docs/requirements.txt + - name: Cache Build + id: cache-build + uses: actions/cache@v5 + with: + path: ${{ env.CACHE_PATH }} + key: docs-ccache + + - name: Prepare Ccache + run: | + ccache --max-size=1.0G + ccache -V && ccache --show-config + ccache --show-stats && ccache --zero-stats + - name: Copy release docs run: | git fetch --all --tags @@ -90,14 +104,21 @@ jobs: done cd .. - # We build ipctk without ccache on purpose. Its dependencies compile with - # -march=native, and ccache hashes that flag string, not the CPU it expands - # to. A cached object could therefore be restored onto a runner that lacks - # some of its instructions, which crashed `import ipctk` with SIGILL when - # Sphinx started. The tradeoff is a cold build of about 5 minutes instead of - # about 1, which is fine because docs only build on pushes to main. + # On Linux, explicitly build for x86-64-v3 instead of -march=native. This + # avoids issues with ccache restoring from a runner with a different ISA. + - name: Check x86-64-v3 support + run: | + for f in avx2 bmi1 bmi2 f16c fma abm movbe xsave; do + grep -qw "$f" /proc/cpuinfo || { echo "::error::Runner CPU lacks $f, which x86-64-v3 requires"; exit 1; } + done + echo "CPU: $(grep -m1 '^model name' /proc/cpuinfo | cut -d: -f2-)" + - name: Build ipctk - run: pip install --verbose . + env: + CMAKE_ARGS: -DSIMD_CXX_FLAGS=-march=x86-64-v3 + run: | + pip install --verbose . + ccache --show-stats - name: Build docs run: | diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 2eb4c112a..f9ccef82b 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -50,6 +50,11 @@ jobs: CIBW_ARCHS_MACOS: ${{ matrix.arch }} CIBW_BUILD: ${{ matrix.python-version }}-* CIBW_SKIP: "cp38-* pp* *musllinux* *manylinux1* *-win32 *linux_i686 *linux_aarch64 *linux_ppc64le *linux_s390x" + # Without this the wheel takes -march=native from whichever runner built it, + # and can contain AVX-512 that users' CPUs lack. x86-64-v3 is the baseline + # the 1.6.0 wheels already required (AVX2, FMA, BMI1/2, LZCNT). The build + # runs in a manylinux container, so it has to go through cibuildwheel. + CIBW_ENVIRONMENT_LINUX: CMAKE_ARGS="-DSIMD_CXX_FLAGS=-march=x86-64-v3" - uses: actions/upload-artifact@v7 with: diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 5f18ea154..6ab37f141 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -64,45 +64,22 @@ jobs: choco install ccache "CACHE_PATH=${env:LOCALAPPDATA}\ccache" | Out-File -FilePath $env:GITHUB_ENV -Append - # FindSIMD, MeshFEMCore, and Tight-Inclusion all compile with - # -march=native, which bakes the building runner's ISA into every cached - # object file. ccache hashes the literal flag string, not what it expands - # to, so restored objects are only safe on a runner with the same ISA. - # The CPU model name has not been enough to guarantee that: EPYC 9V74 - # runners restored each other's objects and crashed with SIGILL. On Linux - # we also hash the flags the compiler expands -march=native to, which is - # exactly what ends up in the object files. We keep the model name in the - # hash too, so the key is never weaker than before. - - name: CPU fingerprint (Linux/macOS) - if: runner.os != 'Windows' - run: | - if [ "$RUNNER_OS" = "Linux" ]; then - CPU=$(grep -m1 '^model name' /proc/cpuinfo | cut -d: -f2-) - ISA=$(echo | c++ -march=native -E -v - 2>&1 | grep -oE -- ' -m[a-z0-9=._+-]+' | sort -u | tr -d '\n') - echo "ISA:$ISA" - KEY=$(printf '%s\n%s' "$CPU" "$ISA" | sha256sum | cut -c1-12) - else - CPU=$(sysctl -n machdep.cpu.brand_string) - KEY=$(printf '%s' "$CPU" | shasum -a 256 | cut -c1-12) - fi - echo "CPU: $CPU" - echo "CPU_KEY=$KEY" >> "$GITHUB_ENV" - - - name: CPU fingerprint (Windows) - if: runner.os == 'Windows' + # On Linux, explicitly build for x86-64-v3 instead of -march=native. This + # avoids issues with ccache restoring from a runner with a different ISA. + - name: Check x86-64-v3 support (Linux) + if: runner.os == 'Linux' run: | - $cpu = Get-CimInstance Win32_Processor | Select-Object -First 1 -ExpandProperty Name - Write-Host "CPU: $cpu" - $stream = [IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($cpu)) - $key = (Get-FileHash -InputStream $stream -Algorithm SHA256).Hash.Substring(0, 12) - "CPU_KEY=$key" | Out-File -FilePath $env:GITHUB_ENV -Append + for f in avx2 bmi1 bmi2 f16c fma abm movbe xsave; do + grep -qw "$f" /proc/cpuinfo || { echo "::error::Runner CPU lacks $f, which x86-64-v3 requires"; exit 1; } + done + echo "CPU: $(grep -m1 '^model name' /proc/cpuinfo | cut -d: -f2-)" - name: Cache Build id: cache-build uses: actions/cache@v5 with: path: ${{ env.CACHE_PATH }} - key: ${{ runner.os }}-Python${{ matrix.python-version }}-${{ env.CPU_KEY }}-cache + key: ${{ runner.os }}-Python${{ matrix.python-version }}-cache - name: Prepare Ccache run: | @@ -112,7 +89,7 @@ jobs: - name: Build and Install env: - IPCTK_WITH_SIMD: ${{ runner.os == 'Linux' && '0' || '1' }} + CMAKE_ARGS: ${{ runner.os == 'Linux' && '-DSIMD_CXX_FLAGS=-march=x86-64-v3' || '' }} run: | pip install --verbose . && ccache --show-stats diff --git a/CMakeLists.txt b/CMakeLists.txt index 206196e34..7f6c06c5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -170,17 +170,31 @@ endif() # SIMD support if(IPC_TOOLKIT_WITH_SIMD) - # Figure out SIMD support - message(STATUS "Testing SIMD capabilities...") - find_package(SIMD) - if (SIMD_CXX_FLAGS) - message(STATUS "SIMD support found: ${SIMD_CXX_FLAGS}") + if(SIMD_CXX_FLAGS) + # The flags were given on the command line, or cached by an earlier + # configure, so we use them as they are instead of probing the host. We only + # check that the compiler accepts them. If they are rejected we stop with an + # error instead of falling back to a build without SIMD, because a silent + # fallback would let a typo in the flags pass as a working non-SIMD build. + include(ipc_toolkit_filter_flags) + ipc_toolkit_check_cxx_flags(SIMD_CXX_FLAGS SIMD_CXX_FLAGS_ARE_VALID) + if(NOT SIMD_CXX_FLAGS_ARE_VALID) + message(FATAL_ERROR "SIMD_CXX_FLAGS=\"${SIMD_CXX_FLAGS}\" is not accepted by the compiler. Unset it (-USIMD_CXX_FLAGS) to auto-detect the flags, or pass -DIPC_TOOLKIT_WITH_SIMD=OFF to build without SIMD.") + endif() + message(STATUS "Using SIMD flags: ${SIMD_CXX_FLAGS}") else() - message(WARNING "SIMD support requested but not found. Continuing without SIMD.") - # We shadow the cache entry with a normal variable instead of forcing the - # cache off. Forcing would overwrite the user's request, and the option - # would stay OFF in this build directory even after SIMD becomes available. - set(IPC_TOOLKIT_WITH_SIMD OFF) + # Figure out SIMD support + message(STATUS "Testing SIMD capabilities...") + find_package(SIMD) + if (SIMD_CXX_FLAGS) + message(STATUS "SIMD support found: ${SIMD_CXX_FLAGS}") + else() + message(WARNING "SIMD support requested but not found. Continuing without SIMD.") + # We shadow the cache entry with a normal variable instead of forcing the + # cache off. Forcing would overwrite the user's request, and the option + # would stay OFF in this build directory even after SIMD becomes available. + set(IPC_TOOLKIT_WITH_SIMD OFF) + endif() endif() endif() diff --git a/cmake/ipc_toolkit/ipc_toolkit_filter_flags.cmake b/cmake/ipc_toolkit/ipc_toolkit_filter_flags.cmake index 36583d36e..cf971b37d 100644 --- a/cmake/ipc_toolkit/ipc_toolkit_filter_flags.cmake +++ b/cmake/ipc_toolkit/ipc_toolkit_filter_flags.cmake @@ -24,6 +24,29 @@ function(ipc_toolkit_filter_flags flags) set(${flags} ${output_flags} PARENT_SCOPE) endfunction() +# Check that the C++ compiler accepts all of `flags` together, and set `result` +# to TRUE or FALSE in the caller's scope. Both arguments are variable names. +# +# Unlike ipc_toolkit_filter_flags(), which tests each flag on its own and drops +# the ones it rejects, this tests the whole list at once and reports a single +# answer. We use it when the flags were chosen by the user, where silently +# dropping a flag would hide a mistake. +# +# We only compile a trivial program and never run it, so flags that target a +# different CPU than the one doing the build still pass. The answer is cached +# under a name built from the flags, so changing the flags re-runs the check +# instead of reusing an old result. +function(ipc_toolkit_check_cxx_flags flags result) + include(CheckCXXSourceCompiles) + string(MAKE_C_IDENTIFIER "${${flags}}" flags_id) + # CMAKE_REQUIRED_FLAGS wants one space-separated string, not a CMake list. We + # set it inside this function so it does not leak into the caller's scope. + string(REPLACE ";" " " CMAKE_REQUIRED_FLAGS "${${flags}}") + check_cxx_source_compiles( + "int main() { return 0; }" IPC_TOOLKIT_CXX_FLAGS_OK${flags_id}) + set(${result} ${IPC_TOOLKIT_CXX_FLAGS_OK${flags_id}} PARENT_SCOPE) +endfunction() + # The nvcc counterpart of ipc_toolkit_filter_flags(): keep the flags of `flags` # that nvcc's host compiler accepts, checked by actually compiling with nvcc and # `-Xcompiler=`, and wrap each so it applies only to CUDA sources compiled