Skip to content

[MOD-17925] SIMD dispatch: declarative ISA manifest, with the SQ8_FP32 pilot - #1023

Draft
dor-forer wants to merge 12 commits into
fp16-avx512fp16-accumulatorfrom
simd-dispatch-pilot
Draft

[MOD-17925] SIMD dispatch: declarative ISA manifest, with the SQ8_FP32 pilot#1023
dor-forer wants to merge 12 commits into
fp16-avx512fp16-accumulatorfrom
simd-dispatch-pilot

Conversation

@dor-forer

Copy link
Copy Markdown
Collaborator

Draft, and open early on purpose so the mechanism can be argued with before more of the library
depends on it.
Stacked on #1021, which is on #1019, which is on #1018. Nothing dispatches through
the new machinery yet, so this changes no behaviour: the legacy choosers still serve every family.

Describe the changes in the pull request

The dispatch layer states the same facts in three places: cmake/*InstructionFlags.cmake decides which tiers the toolchain can build, spaces/CMakeLists.txt restates each tier's flags, and about 40 chooser bodies restate each tier's runtime predicate, alignment rule and dimension gate behind stacked #ifdefs. This replaces that with two declarative manifests and, eventually, one generic dispatch<Metric, Type>().

The whole design turns on keeping two gates independent, because this library is compiled on one machine and run on another:

  • Compile time, the toolchain decides. A tier's translation unit is compiled only if this compiler and assembler can carry it to an object, gated by a whole-tier try_compile of the tier's real source under its real flags. The build machine's own CPU is deliberately irrelevant; gating on it would ship binaries missing every tier the builder happened to lack.
  • Runtime, the deployment CPU decides. A tier is selected only if the running CPU's bits satisfy that tier's GUARANTEES.

isa_tiers.def therefore carries FLAGS_FROM and GUARANTEES as two columns rather than one list. A flag fragment expands to a compiler-defined bundle that differs between gcc and clang for the same -march string, so a predicate derived from flags can claim support the hardware lacks. Where the columns differ, the difference is load-bearing and commented: NEON_FHM is compiled at +fp16fml but requires asimdhp && asimdfhm at runtime, which is the SIGILL fixed in #1018; SSE4_F16C carries avx because -mf16c is VEX-encoded.

What is in this draft

  1. spaces/isa_features.def, 23 features. spaces/isa_tiers.def, 23 tiers. Every row derived by reading the build, not inferred.
  2. cmake/vecsim_manifest.cmake parses both and derives each tier's flags from FLAGS_FROM alone. Verified byte-for-byte identical to all 23 hardcoded flag strings, twice: offline by diffing, and inside the configure by reading each tier's COMPILE_FLAGS back off the source property and asserting equality, so the two descriptions cannot drift while both exist.
  3. cmake/tier_probe.cmake gates buildability on the whole-tier try_compile, with CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY because these TUs have no main(). An excluded tier is always reported, never silently dropped.
  4. VECSIM_BUILT_<TIER> defined as 0 or 1 always, on an internal interface target consumed by the library and the tests, so if constexpr is well formed for every tier and consumers see none of it. functions/all_tiers.h is generated from the tiers that built.
  5. spaces/tier_info.h: the Tier enum and TierInfo<Tier>, with supported() folded exclusively from GUARANTEES.
  6. spaces/kernel.h: Kernel<Metric, Type, Tier> and TypePolicy<Type>, populated for SQ8_FP32 across L2, IP and Cosine on all seven tiers that implement it. Each make() body is the legacy chooser's macro invocation copied unchanged, so the kernel selected for a given dim cannot differ.

Still to come: the generic dispatch<>() and repointing the three SQ8_FP32 choosers at it, a parity gate against the legacy choosers over the enumerated CPU masks, codegen equivalence per changed tier object, and LTO hardening.

Which issues this PR fixes

  1. MOD-17925

Main objects this PR modified

  1. New src/VecSim/spaces/isa_features.def and isa_tiers.def
  2. New cmake/vecsim_manifest.cmake and cmake/tier_probe.cmake
  3. src/VecSim/spaces/CMakeLists.txt, the manifest layer
  4. New src/VecSim/spaces/tier_info.h and kernel.h
  5. The seven pilot tiers' functions/<tier>.{h,cpp}
  6. tests/unit/test_spaces.cpp, two TierInfo cases

Verification

Measured after each commit on x86 (Ice Lake, gcc 13) and ARM (Neoverse-N1, gcc 12): 15 and 8 tiers built, zero flag mismatches, spaces suites 1568/1568 and 1531/1531, both TierInfo tests passing. Test counts are exactly additive over the stack, so these commits add nothing to that suite and move nothing in it.

Both gates were also made to fail deliberately, since a check that cannot fail proves nothing. Pointing a bogus tier at an existing stem tripped the equality assertion, naming both flag strings. Giving a bogus tier its own source and an unaccepted flag failed the probe instead: configure still exited 0, the tier was reported unavailable, its define came out 0, and it was absent from the umbrella while the other 15 includes remained.

Review focus

The manifest format is the part worth arguing about now, while only one family depends on it. Specifically: whether FLAGS_FROM and GUARANTEES as two columns reads better than the alternatives, whether the tier granularity is right (the three F16C combination TUs are modelled as first-class tiers, not variants of AVX2/AVX2_FMA/SSE4), and whether a contributor adding a kernel would find the tier header's rows self-explanatory without reading the design doc.

Mark if applicable

  • This PR introduces API changes
  • This PR introduces serialization changes

The AVX512F_BW_VL_VNNI tier is compiled with -mavx512vl, but 6 of 12
runtime predicate sites omitted the avx512vl check, while 6 included it.
A CPU with avx512f, avx512bw, and avx512vnni but without avx512vl would
therefore be handed a function pointer into a TU the compiler was licensed
to emit VL-encoded instructions in. Add avx512vl to all 12 sites, making
them consistent.
The spaces/CMakeLists.txt correctly uses CMAKE_SYSTEM_PROCESSOR for
the target machine, but tests/unit/CMakeLists.txt and
tests/benchmark/CMakeLists.txt used CMAKE_HOST_SYSTEM_PROCESSOR,
which breaks cross-compilation: a build machine with AVX512 targeting
ARM would incorrectly pull in x86-specific instruction flags. Switch
both to CMAKE_SYSTEM_PROCESSOR.
getCpuOptimizationFeatures() let a caller-supplied arch_opt override replace
the detected CPU features outright, so a caller could claim a feature the
real hardware lacks and be handed a function pointer into a SIMD tier that
executes an illegal instruction. The override is a test-only affordance:
no production caller passes a non-null arch_opt (every chooser in
spaces.cpp uses the nullptr default), tests use it to force a specific
tier. Every existing test only ever clears bits (232 clears in
test_spaces.cpp, zero sets), so intersecting the override with the
detected features instead of replacing them is a no-op for the whole
existing suite while closing the hole for any future or malformed test.

The new intersectWithDetectedFeatures() helper performs a byte-wise AND
over the two structs' object representation rather than hand-listing
fields, since cpu_features::X86Features and cpu_features::Aarch64Features
are composed entirely of `int <name> : 1;` bitfields with no other
members: a byte-wise AND therefore performs a per-feature logical AND, and
hand-writing per-field assignments would rot the moment cpu_features adds
a field. A static_assert guards the trivially-copyable assumption the
memcpy-based AND relies on.

The new CpuFeatureIntersectionX86 / CpuFeatureIntersectionAarch64 tests in
test_spaces.cpp enforce the pure-bitfield layout assumption itself: they
walk every enum value via cpu_features' own GetX86FeaturesEnumValue /
GetAarch64FeaturesEnumValue readers and compare against the field-wise
AND, so a future non-bitfield member would make the byte-wise AND diverge
and fail loudly. They also cover the two cases called out by the task:
clearing one feature out of an all-detected mask leaves every other
feature unchanged, and claiming a feature the detected set lacks still
comes out clear.
VectorSimilaritySpaces_no_optimization (L2/L2.cpp, IP/IP.cpp) is the scalar
fallback that runtime dispatch degrades to, and the "no architecture flag"
property that makes it safe on every deployment CPU was only a comment.
The root CMakeLists.txt repeatedly appends to whatever CMAKE_CXX_FLAGS it
inherits, and consumers export CXXFLAGS into this build (RediSearch's
build.sh does), so an inherited -march=native could silently vectorize the
fallback itself with instructions the target CPU may not have, defeating
runtime dispatch in the one place that must be universally safe.

Pin a conservative baseline (-march=x86-64 or -march=armv8-a, guarded by
CHECK_CXX_COMPILER_FLAG, empty on other architectures) and apply it with
target_compile_options(... PRIVATE ...) to VectorSimilaritySpaces_no_optimization
and VectorSimilaritySpaces. The target-level flag cannot clobber the
per-tier ISA flags that set_source_files_properties() attaches to each
functions/*.cpp, because CMake emits source-level COMPILE_FLAGS after
target-level COMPILE_OPTIONS on the same command line, and gcc does not
let a later -march= disable an earlier -m feature flag either way.

Reject at configure time any -march=native, -mtune=native, -mcpu=native,
or -march=/-mcpu= set to something other than the chosen baseline, found
in CMAKE_CXX_FLAGS, CMAKE_CXX_FLAGS_<BUILD_TYPE>, or the CXXFLAGS
environment variable. A non-native -mtune value is deliberately still
allowed: it only changes instruction scheduling, not which instructions
are legal, so rejecting it would break legitimate consumer builds for no
safety gain.

Add a small, separate CI job that injects -march=native through CXXFLAGS
and asserts the configure fails; it proves a negative, so it stays out of
the main build jobs.

The job is dispatched through event-pull_request.yml like the other build
jobs, instead of triggering on pull_request directly.

Its job id is listed in pr-validation's needs, so a failure there blocks the pull request like every other required check.
Downgrade the ISA value-mismatch case in _spaces_reject_isa_override
(src/VecSim/spaces/CMakeLists.txt) from FATAL_ERROR to a warning. The
target-level baseline applied further down with target_compile_options()
already keeps the scalar fallback (VectorSimilaritySpaces_no_optimization) on
the conservative baseline regardless of what a consumer sets, so the fatal
error was never the actual protection mechanism. Refusing to configure on a
merely different, explicit, non-native -march/-mcpu (for example
-march=x86-64-v2, a distro default, or -mcpu=neoverse-n1) broke consumers
declaring a legitimate deployment floor, and RediSearch exports CXXFLAGS into
this build. The =native case keeps FATAL_ERROR: there is no safe way to honor
an auto-detected baseline.

De-duplicate the CMAKE_SYSTEM_PROCESSOR regex matching into a single
SPACES_ARCH_FAMILY variable, computed once, with the baseline computation and
both tier-gating if() blocks branching on it instead of re-matching the
regex.

Fix the spaces-isa-baseline workflow to grep for "Refusing to configure"
instead of "-march=native": the job asserts a negative (configure must fail),
and grepping for the same string it injected as CXXFLAGS would let an
unrelated configure failure that happens to echo the flag pass as if it were
the expected rejection. The retained FATAL_ERROR message still contains that
exact phrase.

Add GetCpuOptimizationFeaturesAppliesMaskX86 and
GetCpuOptimizationFeaturesAppliesMaskAarch64 to tests/unit/test_spaces.cpp.
The existing CpuFeatureIntersection* tests only exercise
intersectWithDetectedFeatures() directly; they never call
getCpuOptimizationFeatures(&mask), which is the actual entry point every real
caller goes through and the function whose behavior the mask-intersection fix
changed. The new tests build an all-0xFF claiming mask, deliberately not
derived from the detected set, so they cannot pass vacuously.
Two ways the scalar fallback could still end up compiled above the deployment
baseline, both found in review and both measured on hardware.

The baseline probe failed open. CHECK_CXX_COMPILER_FLAG compiles its test with
CMAKE_CXX_FLAGS included and treats a warning that names a flag as failure, so
an inherited -mcpu=neoverse-n1 makes gcc warn that the switch conflicts with
-march=armv8-a and the probe reports Failed. On x86_64 and aarch64 the baseline
flag is supported by every compiler that can build this library, so a failed
probe never means the flag is unsupported: it means the inherited flags broke
the probe. Continuing there dropped the baseline from the scalar fallback while
the configure still succeeded, which is the hazard the guard exists to prevent.
It now fails closed and names the likely cause. Verified on a Neoverse-N1.

Explicit ISA feature flags bypassed the guard entirely. It only matched -march
and -mcpu, and a later -march= baseline does not cancel a feature flag:
measured with gcc 13, "-mavx2 ... -march=x86-64" still emitted 317 and 161
AVX-family instructions into the two scalar fallback objects. Those flags
cannot be neutralised reliably, so they are refused. Disabling forms (-mno-...)
and -mtune= stay allowed, since neither raises the set of legal instructions.

The guard's CI job asserted only the -march=native case, so it caught neither
of these. It is now a matrix over the native variants and the feature flags,
plus a positive case checking that an explicit non-native baseline still
configures and still warns.

Also gate the 22 single-line SQ8 VNNI test guards on avx512vl, so the tests
mirror the production predicate this branch tightened. The multi-line INT8 and
UINT8 guards already required it.
`FP16_L2Sqr` and `FP16_InnerProduct` widen each stored half with FP16_to_FP32
and accumulate into a `float`. Four SIMD tiers did not: AVX512FP16 kept
`__m512h sum` and reduced into a `_Float16`, NEON kept `float16x8_t acc` with
`vfmaq_f16`, and SVE kept `svfloat16_t acc` with `svmla_f16_x`. On any CPU that
selects one of those tiers the whole vector was summed in an 11-bit mantissa, so
the same function returned a different answer depending on the machine.

Two consequences, both silent. Precision: simulating both accumulation orders
over 20,000 random 128-dimension fp16 vectors gives a maximum relative error of
5.6e-3 for the fp16 accumulator against 1.9e-7 for the fp32 one, so nearest
neighbours and their ordering change. Overflow: 65504 is the largest finite fp16
value, so 32 elements of 200.0, all ordinary fp16 values, drive a half precision
accumulator past it and the result becomes infinity.

All four tiers now widen and accumulate in fp32. The x86 kernels mirror
L2_AVX512F_FP16.h and IP_AVX512F_FP16.h step for step, since after widening
there is nothing half-precision-specific left to do differently; they also gain
the second accumulator that #984 added to the sibling fp16 tiers and not to
these. NEON and SVE keep their existing four-way unrolling, with each
accumulator becoming a pair covering the lower and upper halves of a register.

The unit tests could not have caught this. Both fp16 baselines accumulated the
reference in `_Float16` too, so the test compared a half precision kernel
against a half precision reference and passed within its 1% tolerance whatever
the kernel did. They now accumulate in `float` via FP16_to_FP32, mirroring the
scalar functions exactly. That alone is still not a regression test: the
randomized cases draw values from [-0.99, 0.99], where a half precision
accumulator's worst error over dim 32..256 is about 0.54%, inside the 1% budget.
FP16SpacesTest.LargeValuesDoNotOverflowTheAccumulator closes that gap with
inputs whose expected totals are exact in fp32 and infinite in fp16, and it
calls the public choosers so whichever tier the running CPU selects is tested.

The ARM half of this change depends on the NEON_HP/NEON_FHM translation unit
split from #1018. Widening to fp32 and then issuing vfmaq_f32 is exactly the
pattern gcc contracts into FMLAL, so in a translation unit compiled with
+fp16fml the fix would emit FMLAL into the plain half-precision path and fault
on any core without FEAT_FHM. Measured on gcc 12: the NEON kernels compile to 4
FMLAL at -march=armv8.2-a+fp16fml and 0 at +fp16. With #1018 the HP tier is
compiled at +fp16 only, and NEON_HP.cpp.o contains no FMLAL.

Verified on an AWS Graviton2 (Neoverse-N1, asimdhp without asimdfhm, gcc 12),
which executes the NEON path: the full spaces suite passes 1529/1529 with this
change on top of #1019, where the same change on a main base fails 106 tests
with SIGILL. On x86 (Ice Lake, gcc 13) the suite passes 1569/1569, and the
AVX512FP16 kernels compile with -mavx512fp16 -Werror leaving no fmadd*ph, subph
or mulph. The AVX512FP16 tier itself needs Sapphire Rapids or later to execute
and the SVE tier needs an SVE core, so neither runs on the hardware available
here; both are covered by CI.
Two declarative tables that will become the single source of truth for the SIMD
dispatch layer. Nothing consumes them yet: this commit only states, in one place
and in one vocabulary, what is currently spread across cmake/*InstructionFlags.cmake,
spaces/CMakeLists.txt, and about 40 hand-written chooser bodies.

isa_features.def has one row per CPU feature: the token tiers name, its
architecture, the compiler flag fragment it contributes, the lowest -march level
that admits that fragment, and the cpu_features field naming the runtime bit.
Three relations are spelled distinctly rather than as one "implies", because they
answer different questions: ARCH_IMPLIES enumerates which CPU masks are possible,
FLAG_ENABLES records what a fragment forces the compiler to assume, and
LEVEL_REQUIRES what raising -march forces.

isa_tiers.def has one row per tier, with FLAGS_FROM and GUARANTEES as separate
columns. That separation is the point of the whole design: a flag fragment expands
to a compiler-defined bundle, so compiling with a flag does not tell you which
bits the deployment CPU must have. Two live bugs came from conflating them, and
both are now visible as a column difference instead of being invisible.

Every row was derived by reading the build rather than by inference, and three
things that reading corrected:

- The tier set is 23, matching the 23 set_source_files_properties() entries. An
  earlier count of 20 came from grepping OPT_* in the two dispatch files, which
  misses tiers guarded by another tier's macro.
- AVX2_F16C, AVX2_FMA_F16C and SSE4_F16C are tiers in their own right, not
  variants of AVX2, AVX2_FMA and SSE4. Each is its own translation unit with its
  own flags and predicate, carrying the SQ8-to-FP16 kernels that need vcvtph2ps,
  while the plain tiers carry the SQ8-to-FP32 kernels that do not. Legacy code
  expressed this as one shared OPT_F16C guard around three differently
  predicated branches, which is why they look like variants.
- The F16C and SSE4_F16C predicates include bits beyond the tier's nominal
  feature: f16c && fma3 && avx and sse4_1 && f16c && avx respectively. -mf16c is
  VEX-encoded, so the flag puts AVX-state instructions in reach and the
  predicates already account for it.

Priorities are spaced by 10 so a tier can be inserted without renumbering, and
they reproduce the legacy selection order for both SQ8_FP32 (VNNI, AVX2_FMA,
AVX2, SSE4) and SQ8_FP16 (AVX512F, AVX2_FMA_F16C, AVX2_F16C, SSE4_F16C). A
parity test will pin that rather than leaving it asserted here.
vecsim_manifest.cmake reads both .def files into CMake variables and derives a
tier's compile flag string from its FLAGS_FROM column alone. x86 unions the -m
fragments in token order; ARM emits one -march at the highest level among the
tier's tokens, then each distinct +fragment, because ARM -march strings are
monolithic rather than a union of independent options.

GUARANTEES is parsed and carried through untouched, never consulted here. The
runtime predicate is not derived from flags, because a fragment expands to a
compiler-defined bundle that differs between compilers for the same -march
string. Keeping the derivation one-directional is what stops this module from
quietly recreating the unsound design.

Verified: the derived flags are byte-for-byte identical to the 23 hardcoded
strings in spaces/CMakeLists.txt today, compared by extracting both and diffing.
That equality is the whole claim behind calling the manifest a source of truth,
so it is worth stating as a measurement rather than an intention. Nothing
consumes the derivation yet, so this commit cannot change what the build emits.

Validation is deliberately fatal rather than skip-on-error: a tier naming an
undefined token, a tier naming a token of the other architecture, an unknown ARM
level, or a manifest that parses to zero rows all stop the configure. Each of
those would otherwise disable tiers silently while the configure still
succeeded, which is the failure mode this whole series has been about.

tier_probe.cmake is carried over unchanged from the held P0.3 work: it
try_compiles a tier's real source under the tier's real flags with
CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY, since these translation units have
no main() and a default executable probe would fail to link for every tier and
disable the entire dispatch layer while reporting success.

Two parser bugs found by running it rather than reading it. Commas are not CMake
list separators, so the token columns arrived as single strings and
foreach(IN LISTS) saw one element; they are now converted to semicolons. And the
first version set CMP0057 to use IN_LIST, which leaks policy into the includer
under CMP0011, so the membership tests use list(FIND) and the module needs no
policy at all.
Adds the manifest layer next to the existing tier blocks rather than replacing
them. The blocks above still compile the translation units; this block derives
the same facts from the manifests, asserts the two agree, and exposes what the
dispatch layer will need.

The assertion is the reason both descriptions can coexist safely. For every tier
the legacy blocks compiled, its COMPILE_FLAGS are read back off the source
property the build will actually use and compared against the manifest's
derivation, and a mismatch stops the configure. Nothing is copied by hand, so
the two cannot drift silently while the migration is in progress. When the last
legacy chooser goes, the legacy blocks go with it and the manifest is the only
description left.

Availability is VECSIM_BUILT_<TIER>, always defined as 0 or 1 rather than
positive-only, so if constexpr sees a value for every tier on every build,
including tiers belonging to the other architecture. The defines live on an
internal INTERFACE target consumed by the library and the tests, not on the
installed target: directory scope is how OPT_* silently vanished from
tests/unit earlier in this series, and consumers should not see private build
capabilities at all.

Buildability comes from the whole-tier probe, which compiles the tier's real
source under its real derived flags. A tier that fails is reported and excluded,
never silently dropped.

functions/all_tiers.h is generated with file(GENERATE) from the tiers that
actually built. A TIER() expansion cannot emit an #include, and a
hand-maintained list would need a checker to prove it matched the manifest; a
generated one cannot disagree with its source.

Measured on both machines. x86 (Ice Lake, gcc 13): configure clean, 15 legacy
tiers built, 15 probes recorded in the cache keyed by compiler identity and flag
string, 15 includes generated, all 23 defines emitted as 0 or 1, and zero
flag-mismatch failures. ARM (Neoverse-N1, gcc 12): configure clean, 8 tiers, 8
includes, same 0-or-1 coverage, zero mismatches.

Both gates were also shown to fail when they should, since a check that cannot
fail proves nothing. Pointing a bogus tier at an existing stem tripped the
equality assertion with the two flag strings named. Giving a bogus tier its own
source and an unaccepted flag failed the probe instead: configure still exited
0, the tier was reported unavailable, VECSIM_BUILT_BOGUS_TIER came out 0, and it
was absent from the umbrella while the other 15 includes remained.
tier_info.h turns both .def files into a Tier enumeration and a TierInfo<Tier>
trait, by including the manifests with the row macros defined. It names no
intrinsics, because it is compiled into every dispatching translation unit
including the baseline ones.

Two facts per tier come from two different columns and are kept apart:
compiled comes from VECSIM_BUILT_<TIER>, which CMake sets from the whole-tier
compile probe, and answers whether this build produced the tier's object.
supported() is folded exclusively from GUARANTEES and answers whether the CPU in
front of us may execute it. supported() never reads FLAGS_FROM, because a flag
fragment expands to a compiler-defined bundle that differs between compilers for
the same -march string, so a flags-derived predicate can claim support the
hardware lacks.

That is not an abstract concern, and the tests pin it on the real rows where the
columns differ rather than on a synthetic tier. NEON_FHM has FLAGS_FROM
(ASIMDFHM) and GUARANTEES (ASIMDHP, ASIMDFHM): with asimdfhm set and asimdhp
clear, supported() returns false, where a flags-derived predicate would return
true. That combination is the SIGILL reproduced on a Graviton2 earlier in this
series. On x86 the same test covers SSE4_F16C, whose predicate carries avx
because -mf16c is VEX-encoded, and the VNNI tier, whose predicate carries
avx512vl because the tier is compiled with it.

Measured on both machines: x86 (Ice Lake, gcc 13) 15 tiers, zero flag
mismatches, spaces suite 1568/1568 and both TierInfo tests passing. ARM
(Neoverse-N1, gcc 12) 8 tiers, zero mismatches, 1531/1531 and both passing. The
spaces totals are exactly additive over the stack (1528 + 2 from #1019 + 1 from
#1021 on ARM), so this commit adds no tests to that suite and moves none.

Two implementation notes worth keeping:

FeaturesType was a function-local alias inside getCpuOptimizationFeatures, so it
is hoisted to namespace scope in spaces.h. Repeating the architecture selection
in a second header is the duplication this refactor exists to remove.

The predicate body is selected by the preprocessor from the tier's arch column,
not by if constexpr. The discarded branch of an if constexpr in a non-template
function still has to name things that exist, and an x86 build never declares
the ARM feature accessors, so the if constexpr version did not compile. Every
tier still gets a specialization on every build, so arch, priority and name stay
visible to tests and tooling regardless of architecture.
kernel.h declares what a tier provides for one (metric, data type) pair. The
primary template means "not provided", so a combination no tier implements is
simply absent rather than needing a guard, and because dispatch will test
`if constexpr (Kernel<...>::exists && TierInfo<T>::compiled)`, make() is never
referenced for a combination that does not exist nor linked for a tier that did
not compile. That is what keeps the two gates separate in the generated code:
the toolchain decides what exists, the running CPU decides what is selected, and
neither is allowed to stand in for the other.

The fields are the ones that genuinely differ per combination, taken from the
existing choosers rather than invented: residual_mod and residual_policy
(FixedModulus for the CHOOSE_IMPLEMENTATION tiers, SveRuntime for the two that
defer to the runtime vector length), align_mod and align_bytes, min_dim and
max_dim. No priority override field is included, because no tier needs one and an
unused field is configuration surface the pilot is supposed to be proving it does
not need.

Specializations are declared with a VECSIM_KERNEL macro so a tier header reads as
a short table and a contributor adds a kernel by copying a neighbouring row. They
live in the tier's declaration header; make() is defined in the tier's own
translation unit, the only one carrying that tier's flags, and inside the region
where the chooser macro is defined.

Populated for the pilot family: SQ8_FP32 for L2, IP and Cosine across the seven
tiers that implement it, 21 rows and 21 make() bodies. Every body is the legacy
chooser's macro invocation copied unchanged rather than rewritten, so the kernel
selected for a given dim cannot differ from today's. The trait values were read
out of the choosers the same way: residual step 32 for the three AVX2-and-up
tiers, 16 for SSE4 and NEON, runtime for SVE and SVE2; alignment 16/8/8/4 bytes
on the four x86 tiers and none on ARM.

One asymmetry is now expressed more precisely than the legacy code expressed it.
The x86 choosers early-return the scalar path for dim < 8 with one arch-wide
check, while the ARM side has no such gate. That becomes min_dim on the
specialization: 8 on the four x86 tiers, 1 on the three ARM ones. Same behaviour,
stated per tier instead of per architecture, so a future tier with a different
floor does not have to fight an arch-wide constant.

TypePolicy is left generic. SQ8_FP32 has neither a predicate gate nor a dimension
ceiling, so specializing it here would add unused code; the BF16 endianness gate
and the uint8 exactness bound belong to the families that have them.

Measured on both machines: x86 15 tiers, 12 make() definitions across four tier
objects, spaces 1568/1568; ARM 8 tiers, 9 across three, 1531/1531. Zero flag
mismatches, TierInfo tests passing on both. Nothing dispatches through these yet,
so behaviour is unchanged by construction.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant