Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/event-pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ jobs:
uses: ./.github/workflows/codeql-analysis.yml
secrets: inherit

spaces-isa-baseline:
uses: ./.github/workflows/spaces-isa-baseline.yml
secrets: inherit

spellcheck:
runs-on: ubuntu-latest
steps:
Expand All @@ -88,6 +92,7 @@ jobs:
- sanitizer
- coverage
- codeql-analysis
- spaces-isa-baseline
- spellcheck
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/spaces-isa-baseline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: spaces ISA baseline guard

# These jobs assert negatives: that configuring with an ISA-raising flag injected through CXXFLAGS
# makes CMake fail, because src/VecSim/spaces/CMakeLists.txt rejects it. They are kept separate
# from the main build jobs, since a green run here proves nothing about the build itself, only that
# each rejection still fires. Every case below was a real bypass at some point in review, so a case
# that stops failing is a regression, not a cleanup opportunity.

on: [workflow_call, workflow_dispatch]

jobs:
reject-isa-override:
name: "configure must fail: CXXFLAGS=${{ matrix.flag }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# A native baseline is unknown at build time, so it can raise the fallback's ISA above
# any deployment CPU.
- flag: -march=native
- flag: -mtune=native
- flag: -mcpu=native
# An explicit feature flag is NOT cancelled by a later -march= baseline: measured on
# gcc 13, "-mavx2 ... -march=x86-64" still emitted hundreds of AVX instructions into the
# scalar fallback.
- flag: -mavx2
- flag: -mavx512f
- flag: -mfma
steps:
- name: checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: assert configure fails on an inherited ${{ matrix.flag }}
env:
CXXFLAGS: ${{ matrix.flag }}
run: |
set +e
output=$(cmake -S . -B build-reject -DVECSIM_BUILD_TESTS=OFF 2>&1)
status=$?
echo "$output"
set -e
if [ "$status" -eq 0 ]; then
echo "Expected the configure to fail because of the inherited ${{ matrix.flag }}, but it succeeded."
exit 1
fi
if ! echo "$output" | grep -q -- "Refusing to configure"; then
echo "Configure failed, but not with the expected rejection message."
exit 1
fi

accept-explicit-baseline:
name: "configure must succeed and warn: CXXFLAGS=-march=x86-64-v2"
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: assert an explicit non-native baseline still configures
env:
CXXFLAGS: -march=x86-64-v2
run: |
set +e
output=$(cmake -S . -B build-accept -DVECSIM_BUILD_TESTS=OFF 2>&1)
status=$?
echo "$output"
set -e
if [ "$status" -ne 0 ]; then
echo "A consumer declaring a legitimate deployment floor must still be able to build."
exit 1
fi
if ! echo "$output" | grep -qi "baseline"; then
echo "Expected a warning naming the baseline, so the difference is not silent."
exit 1
fi
77 changes: 77 additions & 0 deletions cmake/tier_probe.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Probe whether the current toolchain can actually build a SIMD tier, instead of inferring the
# toolchain's capability from a side channel (a compiler flag check alone, or a binutils version
# table). CHECK_CXX_COMPILER_FLAG only asks the compiler whether it recognizes a flag; it does
# not ask whether the compiler and assembler can carry a real translation unit through to a
# finished object under the tier's complete flag combination. This probe does that: it
# try_compiles the tier's own source file under the tier's own flags, so a flag combination that
# the compiler accepts individually but rejects together, or a flag whose instructions the
# assembler cannot emit, fails here rather than reaching the build.
#
# vecsim_tier_compiles(<result_var> SOURCE <path> FLAGS <flag string> [NAME <label>])
#
# Sets <result_var> to true/false depending on whether <path> compiles under <flag string>.
# Callers should define each tier's flag string once in a variable and pass that same variable
# both here and to set_source_files_properties(), so the probe can never test a different flag
# set than the one the build actually uses.

include(CheckCXXCompilerFlag)

function(vecsim_tier_compiles result_var)
set(_one_value_args SOURCE FLAGS NAME)
cmake_parse_arguments(_tier_probe "" "${_one_value_args}" "" ${ARGN})

if(NOT _tier_probe_SOURCE)
message(FATAL_ERROR "vecsim_tier_compiles: SOURCE is required")
endif()
if(NOT _tier_probe_NAME)
set(_tier_probe_NAME "${_tier_probe_SOURCE}")
endif()

# The cache variable name follows the same idea CHECK_CXX_COMPILER_FLAG relies on (cache the
# answer under a name that identifies what was asked), extended here with the compiler
# identity/version and the flag string themselves, so a compiler upgrade or an edit to the
# tier's flags cannot reuse a stale answer computed under a different compiler or different
# flags.
string(MAKE_C_IDENTIFIER
"TIER_COMPILES_${_tier_probe_NAME}_${CMAKE_CXX_COMPILER_ID}_${CMAKE_CXX_COMPILER_VERSION}_${_tier_probe_FLAGS}"
_tier_probe_cache_var)

if(NOT DEFINED ${_tier_probe_cache_var})
# Resolve relative to the caller's source directory (functions/*.cpp is written relative
# to src/VecSim/spaces/CMakeLists.txt), since try_compile does not carry that context.
get_filename_component(_tier_probe_source_abs "${_tier_probe_SOURCE}" ABSOLUTE)

# cpu_features arrives via FetchContent_MakeAvailable(cpu_features) in
# cmake/cpu_features.cmake, included before any tier block runs, so the target exists
# here. Read its include directory from the target itself rather than hardcoding a path
# under the FetchContent _deps tree, which is an implementation detail that can move.
get_target_property(_tier_probe_cpu_features_dir cpu_features SOURCE_DIR)

# The tier translation units have no main(): they are dispatch kernels selected at
# runtime, not entry points. A default try_compile probe builds an executable and would
# fail to LINK for every tier for that reason alone, which would silently disable the
# whole dispatch layer while still reporting a successful configure. Building a static
# library instead only requires the tier's own translation unit to compile and assemble.
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

try_compile(${_tier_probe_cache_var}
"${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/tier_probe/${_tier_probe_cache_var}"
SOURCES "${_tier_probe_source_abs}"
CMAKE_FLAGS
"-DINCLUDE_DIRECTORIES:STRING=${root}/src;${_tier_probe_cpu_features_dir}/include"
COMPILE_DEFINITIONS "${_tier_probe_FLAGS}"
CXX_STANDARD ${CMAKE_CXX_STANDARD}
CXX_STANDARD_REQUIRED ON
OUTPUT_VARIABLE _tier_probe_output
)

if(NOT ${_tier_probe_cache_var})
message(STATUS "Skipping tier ${_tier_probe_NAME}: toolchain failed to compile "
"${_tier_probe_SOURCE} with '${_tier_probe_FLAGS}'. A tier that cannot compile "
"on this toolchain is expected on some machines; the runtime degrades to a "
"lower tier.")
endif()
endif()

set(${result_var} ${${_tier_probe_cache_var}} PARENT_SCOPE)
endfunction()
154 changes: 154 additions & 0 deletions cmake/vecsim_manifest.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Reads spaces/isa_features.def and spaces/isa_tiers.def and turns them into CMake variables, so the
# manifests are the single place that says what a tier is. Nothing here decides policy: every value
# comes from a manifest row, and a row that names something the other manifest does not define is a
# configure error rather than a silently skipped tier.
#
# vecsim_load_manifests(<features.def> <tiers.def>)
# VECSIM_FEATURE_TOKENS all tokens
# VECSIM_FEATURE_<TOK>_{ARCH,FRAGMENT,LEVEL,FIELD}
# VECSIM_TIERS all tiers, in manifest order
# VECSIM_TIER_<T>_{ARCH,STEM,PRIORITY,FLAG_TOKENS,GUARANTEE_TOKENS}
#
# vecsim_tier_flags(<out_var> <tier>) the compile flag string derived from FLAG_TOKENS alone
#
# The flag string is derived; the runtime predicate is not. GUARANTEE_TOKENS is carried through
# untouched for the C++ side to fold into TierInfo<T>::supported(), because a flag fragment expands
# to a compiler-defined bundle that differs between compilers for the same -march string. Deriving a
# predicate from flags would reintroduce exactly that unsoundness.

# ARM -march levels, low to high. A tier's level is the highest among its tokens, because ARM -march
# strings are monolithic: one level plus "+feature" suffixes, not a union of independent options.
set(VECSIM_ARM_LEVELS "armv8-a;armv8.1-a;armv8.2-a;armv8.3-a;armv8.4-a;armv8.5-a;armv8.6-a;armv9-a")

function(_vecsim_arm_level_rank out_var level)
list(FIND VECSIM_ARM_LEVELS "${level}" _rank)
if(_rank EQUAL -1)
message(FATAL_ERROR
"vecsim manifest: unknown ARM architecture level '${level}'. Add it to "
"VECSIM_ARM_LEVELS in cmake/vecsim_manifest.cmake, in ascending order, so the "
"highest-level comparison stays meaningful.")
endif()
set(${out_var} ${_rank} PARENT_SCOPE)
endfunction()

function(vecsim_load_manifests features_def tiers_def)
if(NOT EXISTS "${features_def}")
message(FATAL_ERROR "vecsim manifest: cannot read ${features_def}")
endif()
if(NOT EXISTS "${tiers_def}")
message(FATAL_ERROR "vecsim manifest: cannot read ${tiers_def}")
endif()

set(_tokens "")
file(STRINGS "${features_def}" _lines)
foreach(_line IN LISTS _lines)
if(_line MATCHES "^FEATURE\\( *([A-Za-z0-9_]+) *, *([A-Z0-9]+) *, *\"([^\"]*)\" *, *([^ ,]+) *, *([A-Za-z0-9_]+) *\\)")
set(_tok "${CMAKE_MATCH_1}")
list(APPEND _tokens "${_tok}")
set(VECSIM_FEATURE_${_tok}_ARCH "${CMAKE_MATCH_2}" PARENT_SCOPE)
set(VECSIM_FEATURE_${_tok}_FRAGMENT "${CMAKE_MATCH_3}" PARENT_SCOPE)
set(VECSIM_FEATURE_${_tok}_LEVEL "${CMAKE_MATCH_4}" PARENT_SCOPE)
set(VECSIM_FEATURE_${_tok}_FIELD "${CMAKE_MATCH_5}" PARENT_SCOPE)
# also visible inside this function, for the validation below
set(VECSIM_FEATURE_${_tok}_ARCH "${CMAKE_MATCH_2}")
set(VECSIM_FEATURE_${_tok}_FRAGMENT "${CMAKE_MATCH_3}")
set(VECSIM_FEATURE_${_tok}_LEVEL "${CMAKE_MATCH_4}")
endif()
endforeach()
if(NOT _tokens)
message(FATAL_ERROR "vecsim manifest: ${features_def} defined no FEATURE rows. A parser that "
"silently reads zero rows would disable every tier while the configure still succeeded.")
endif()

set(_tiers "")
file(STRINGS "${tiers_def}" _lines)
foreach(_line IN LISTS _lines)
if(_line MATCHES "^TIER\\( *([A-Za-z0-9_]+) *, *([A-Z0-9]+) *, *([A-Za-z0-9_.]+) *, *([0-9]+) *, *\\(([^)]*)\\) *, *\\(([^)]*)\\) *\\)")
set(_tier "${CMAKE_MATCH_1}")
set(_arch "${CMAKE_MATCH_2}")
# The manifest separates tokens with commas for readability; CMake lists are
# semicolon-separated, so convert rather than relying on a comma string behaving
# like a list (it does not, and foreach(IN LISTS) silently sees one element).
string(REPLACE " " "" _flag_tokens "${CMAKE_MATCH_5}")
string(REPLACE "," ";" _flag_tokens "${_flag_tokens}")
string(REPLACE " " "" _guar_tokens "${CMAKE_MATCH_6}")
string(REPLACE "," ";" _guar_tokens "${_guar_tokens}")
list(APPEND _tiers "${_tier}")

# Validate before exporting, so a typo is a configure error and not a missing tier.
foreach(_t IN LISTS _flag_tokens _guar_tokens)
list(FIND _tokens "${_t}" _known)
if(_known EQUAL -1)
message(FATAL_ERROR
"vecsim manifest: tier ${_tier} names feature token '${_t}', which has no "
"FEATURE row in ${features_def}.")
endif()
if(NOT "${VECSIM_FEATURE_${_t}_ARCH}" STREQUAL "${_arch}")
message(FATAL_ERROR
"vecsim manifest: tier ${_tier} is ${_arch} but names token '${_t}', which "
"is ${VECSIM_FEATURE_${_t}_ARCH}. A tier may only name tokens of its own "
"architecture.")
endif()
endforeach()

set(VECSIM_TIER_${_tier}_ARCH "${_arch}" PARENT_SCOPE)
set(VECSIM_TIER_${_tier}_STEM "${CMAKE_MATCH_3}" PARENT_SCOPE)
set(VECSIM_TIER_${_tier}_PRIORITY "${CMAKE_MATCH_4}" PARENT_SCOPE)
set(VECSIM_TIER_${_tier}_FLAG_TOKENS "${_flag_tokens}" PARENT_SCOPE)
set(VECSIM_TIER_${_tier}_GUARANTEE_TOKENS "${_guar_tokens}" PARENT_SCOPE)
endif()
endforeach()
if(NOT _tiers)
message(FATAL_ERROR "vecsim manifest: ${tiers_def} defined no TIER rows.")
endif()

set(VECSIM_FEATURE_TOKENS "${_tokens}" PARENT_SCOPE)
set(VECSIM_TIERS "${_tiers}" PARENT_SCOPE)
endfunction()

# Derive a tier's compile flags from its FLAG_TOKENS. x86 unions the -m fragments in token order;
# ARM emits one -march at the highest level in the token list, then each distinct fragment.
function(vecsim_tier_flags out_var tier)
set(_arch "${VECSIM_TIER_${tier}_ARCH}")
set(_tokens "${VECSIM_TIER_${tier}_FLAG_TOKENS}")

if("${_arch}" STREQUAL "X86")
set(_parts "")
foreach(_t IN LISTS _tokens)
set(_frag "${VECSIM_FEATURE_${_t}_FRAGMENT}")
list(FIND _parts "${_frag}" _dup)
if(_frag AND _dup EQUAL -1)
list(APPEND _parts "${_frag}")
endif()
endforeach()
string(JOIN " " _flags ${_parts})
elseif("${_arch}" STREQUAL "ARM")
set(_best_level "armv8-a")
_vecsim_arm_level_rank(_best_rank "${_best_level}")
foreach(_t IN LISTS _tokens)
set(_level "${VECSIM_FEATURE_${_t}_LEVEL}")
if(NOT "${_level}" STREQUAL "-")
_vecsim_arm_level_rank(_rank "${_level}")
if(_rank GREATER _best_rank)
set(_best_rank ${_rank})
set(_best_level "${_level}")
endif()
endif()
endforeach()
set(_suffix "")
set(_seen "")
foreach(_t IN LISTS _tokens)
set(_frag "${VECSIM_FEATURE_${_t}_FRAGMENT}")
list(FIND _seen "${_frag}" _dup)
if(_frag AND _dup EQUAL -1)
list(APPEND _seen "${_frag}")
string(APPEND _suffix "${_frag}")
endif()
endforeach()
set(_flags "-march=${_best_level}${_suffix}")
else()
message(FATAL_ERROR "vecsim manifest: tier ${tier} has unknown architecture '${_arch}'")
endif()

set(${out_var} "${_flags}" PARENT_SCOPE)
endfunction()
Loading