Skip to content

[MOD-17845] Gate AVX512 VNNI dispatch on AVX512VL - #1019

Merged
dor-forer merged 6 commits into
mainfrom
MOD-17845-simd-dispatch-hygiene
Aug 25, 2026
Merged

[MOD-17845] Gate AVX512 VNNI dispatch on AVX512VL#1019
dor-forer merged 6 commits into
mainfrom
MOD-17845-simd-dispatch-hygiene

Conversation

@dor-forer

@dor-forer dor-forer commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Describe the changes in the pull request

The AVX512F_BW_VL_VNNI tier is compiled with -mavx512vl, so runtime dispatch must require avx512vl before returning a function from that translation unit. Six of the twelve production predicate sites omitted the check; all twelve now require the complete feature set.

The SQ8 unit-test guards now mirror the same predicate, including the dispatcher alignment tests.

Why this is necessary

A translation unit may use every ISA extension enabled by its compiler flags. Selecting it without checking avx512vl relies on current processor feature combinations rather than the actual compiled-code contract. No known shipping CPU exposes AVX512 VNNI without VL, so this closes a latent dispatch gap rather than an observed field crash.

Which issue this PR fixes

  1. MOD-17845: AVX512 VNNI/VL predicate mismatch

Files modified

  1. src/VecSim/spaces/IP_space.cpp
  2. src/VecSim/spaces/L2_space.cpp
  3. tests/unit/test_spaces.cpp

Verification

  • GCC 13 release build, including AVX512F_BW_VL_VNNI.cpp
  • test_spaces: 1584/1584 passed

Mark if applicable

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

Note

Low Risk
Tightens CPU-feature checks only; no kernel or API changes. On real CPUs VNNI already implies VL, so this is a latent-dispatch fix rather than a behavior change.

Overview
Runtime dispatch for the AVX512F_BW_VL_VNNI SQ8 kernels now requires avx512vl in addition to F/BW/VNNI, matching the ISA the kernels are compiled with (-mavx512vl).

The missing check is added on SQ8↔FP32 and SQ8↔SQ8 IP, cosine, and L2 choosers in IP_space.cpp and L2_space.cpp. Unit-test feature guards, including alignment-hint tests, use the same predicate so they only exercise that path when VL is present.

Reviewed by Cursor Bugbot for commit 809b522. Bugbot is set up for automated code reviews on this repo. Configure here.

@dor-forer
dor-forer force-pushed the MOD-17845-simd-dispatch-hygiene branch from 30b4d10 to 49f3e8d Compare August 20, 2026 08:35

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 49f3e8d. Configure here.

Comment thread src/VecSim/spaces/CMakeLists.txt Outdated
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.18%. Comparing base (2fc6c1c) to head (809b522).

Additional details and impacted files
@@                      Coverage Diff                       @@
##           MOD-17844-arm-simd-isa-safety    #1019   +/-   ##
==============================================================
  Coverage                          97.18%   97.18%           
==============================================================
  Files                                141      141           
  Lines                               8537     8540    +3     
==============================================================
+ Hits                                8297     8300    +3     
  Misses                               240      240           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

NEON_HP.cpp was one translation unit compiled with -march=armv8.2-a+fp16fml,
but its HP-only entry points were dispatched on features.asimdhp alone. Because
the whole TU carried +fp16fml, the compiler was licensed to emit FMLAL/FMLSL
instructions anywhere in it, including into the HP-only functions whose source
has no FMLAL intrinsic. Measured on arm-r8g.xlarge with gcc 12: the HP-only
wrappers compiled from identical source went from 32 AdvSIMD FMLAL/FMLSL
instructions at +fp16fml down to 0 once compiled at +fp16 alone. On a core with
asimdhp but without asimdfhm, the old HP path was therefore a SIGILL.

Tightening the predicate to require both asimdhp and asimdfhm would not have
fixed this: it would have deleted the HP-only fallback for exactly the CPUs
that need it. The fix is two tiers with two translation units, each compiled
only with the license its own kernels need: NEON_HP.cpp now builds at
+fp16, and the new NEON_FHM.cpp carries the FHM-only entry points at
+fp16fml, where they still measure 64 AdvSIMD FMLAL/FMLSL instructions, so the
fast path is intact. Dispatch sites gain a second, independently guarded
branch (asimdhp && asimdfhm) that tries NEON_FHM first and falls back to the
existing asimdhp-only NEON_HP branch.
@dor-forer
dor-forer force-pushed the MOD-17845-simd-dispatch-hygiene branch from 49f3e8d to 8e71421 Compare August 20, 2026 11:11
@dor-forer
dor-forer changed the base branch from main to MOD-17844-arm-simd-isa-safety August 20, 2026 11:11
dor-forer added a commit that referenced this pull request Aug 20, 2026
`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.
@dor-forer
dor-forer force-pushed the MOD-17845-simd-dispatch-hygiene branch from 8e71421 to 66b6e27 Compare August 23, 2026 07:43
dor-forer added a commit that referenced this pull request Aug 23, 2026
`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.
@dor-forer dor-forer changed the title [MOD-17845] Fix SIMD dispatch predicate and build-flag hygiene [MOD-17845] Gate AVX512 VNNI dispatch on AVX512VL Aug 23, 2026
@dor-forer
dor-forer force-pushed the MOD-17845-simd-dispatch-hygiene branch from 66b6e27 to ee83c94 Compare August 23, 2026 07:46
dor-forer added a commit that referenced this pull request Aug 23, 2026
`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.
@dor-forer
dor-forer requested a review from GuyAv46 August 23, 2026 08:36
GuyAv46
GuyAv46 previously approved these changes Aug 23, 2026
The three SQ8_FP16 optimization tests gated their FHM branch on
optimization.asimdfhm alone, while the dispatcher now requires
features.asimdhp && features.asimdfhm. The benchmark registrations already
match the dispatcher; these three did not.

Harmless in practice, since no core reports asimdfhm without asimdhp, but a
test whose guard is looser than the code it tests will not catch the case it
looks like it covers.
Each file under spaces/functions/ is compiled for one instruction-set tier under its own
-march flags, and the running CPU's feature bits pick which tier's Choose_* entry point is
called. Two of those tiers reuse another tier's kernel headers rather than having their own:
SVE2.cpp recompiles fourteen of SVE.cpp's headers at -march=armv9-a+sve2, and the NEON_HP and
NEON_FHM tiers added earlier in this branch share the two SQ8_FP16 headers at +fp16 and
+fp16fml. The kernels are templates at namespace scope with no static, so each instantiation
is a weak symbol that both objects define, holding bodies built for different architectures,
and the linker keeps whichever it saw first. Nothing in the source decides which.

SVE.cpp.o precedes SVE2.cpp.o in the archive, so the SVE bodies win and every Choose_*_SVE2
except the three SQ8_FP16 ones has been dispatching to armv8-a+sve code: the SVE2 tier is
selected on SVE2 hardware and runs base-SVE codegen. Measured on Neoverse with gcc 12, the
linked FP16_L2Sqr_SVE<false,0> is SVE.cpp.o's 51 instructions rather than SVE2.cpp.o's 49.
It is a silent downgrade rather than a fault only because none of the shared bodies currently
holds an SVE2-only opcode, and because link order alone picks the winner it can differ
between builds of the same commit.

The kernels are implementation details of one tier, so this gives them internal linkage: each
header opens an anonymous namespace after its own includes and closes it at the end of file.
The two tiers may then use identical names while producing independent bodies under their
respective flags, and only the Choose_* entry points stay externally visible. Putting the
namespace inside the header rather than around the include site is what lets the header keep
including its own dependencies, which must stay outside.

Covering the header rather than a list of names matters. An earlier attempt renamed each
kernel individually and missed SQ8_SQ8_InnerProductSIMD_SVE_IMP, because that list was derived
from the symbols nm reported in a release build and -O3 inlines that helper away entirely: 0
symbols in both objects against 8 each for its SQ8_FP32 sibling. It also could not cover the
eight step helpers declared plain inline rather than static inline, which collided at -O0 and
had no name to rename. The anonymous namespace takes the kernels, the _IMP helpers and the
inline helpers alike, so a function added to one of these headers is safe by default.

Also adds tests/unit/check_tier_linkage.py, run from ctest as tier_linkage, asserting that no
two tier objects in libVectorSimilaritySpaces.a define a symbol in common. It excludes
vecsim_types helpers, which come from a shared type header rather than a kernel header and are
scalar bit manipulation that every tier compiles to the same bytes, verified byte-identical;
float16::cvt is a member function and cannot take internal linkage regardless.

Neoverse, gcc 12: release and debug both build clean under -Werror -Wall with no warnings, and
tier_linkage passes on both, 8 tier objects over 28 pairs. Spaces suite 1529/1529 on each. The
debug run matters because -O0 inlines nothing away, so it is the configuration where a hidden
collision would show. On x86_64 the check covers 15 tier objects over 105 pairs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dor-forer
dor-forer force-pushed the MOD-17845-simd-dispatch-hygiene branch from ee83c94 to 03d47a5 Compare August 24, 2026 10:29
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.
@dor-forer
dor-forer force-pushed the MOD-17845-simd-dispatch-hygiene branch from 03d47a5 to 809b522 Compare August 24, 2026 10:54
@dor-forer
dor-forer requested a review from GuyAv46 August 25, 2026 06:15
@dor-forer
dor-forer added this pull request to the merge queue Aug 25, 2026
Base automatically changed from MOD-17844-arm-simd-isa-safety to main August 25, 2026 10:54
Merged via the queue into main with commit 2546f8e Aug 25, 2026
16 checks passed
@dor-forer
dor-forer deleted the MOD-17845-simd-dispatch-hygiene branch August 25, 2026 11:06
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.

2 participants