Skip to content

[MOD-17844] Stop ARM SIMD tiers executing instructions the running CPU lacks - #1018

Open
dor-forer wants to merge 3 commits into
mainfrom
MOD-17844-arm-simd-isa-safety
Open

[MOD-17844] Stop ARM SIMD tiers executing instructions the running CPU lacks#1018
dor-forer wants to merge 3 commits into
mainfrom
MOD-17844-arm-simd-isa-safety

Conversation

@dor-forer

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

Copy link
Copy Markdown
Collaborator

Describe the changes in the pull request

Two defects that let an ARM SIMD tier execute instructions the running CPU does not have.

  1. functions/NEON_HP.cpp was one translation unit compiled with -march=armv8.2-a+fp16fml, holding both the HP-only and the FHM entry points. The whole TU therefore carried the fp16fml licence, and the compiler contracted the HP path's vcvt_f32_f16 plus vfmaq_f32 into fmlal, even though the HP source uses no FMLAL intrinsic. That path is dispatched on features.asimdhp alone, so on a core with asimdhp but without asimdfhm it is a SIGILL, reachable from any SQ8_FP16 IP, L2 or Cosine query. Measured on Neoverse with gcc 12: 32 AdvSIMD FMLAL instructions in the HP-only wrappers at +fp16fml, 0 at +fp16, 96 across the whole TU.

    Fixed by splitting into two tiers: NEON_HP at +fp16 predicated on asimdhp, and a new NEON_FHM at +fp16fml predicated on asimdhp && asimdfhm. Tightening the predicate to require both bits instead would have deleted the HP-only fallback for exactly the CPUs that need it. The FHM tier still emits 64 FMLAL instructions, so its fast path is unchanged.

  2. Two tier translation units that include the same kernel header emit the same weak COMDAT symbols with bodies compiled under different -march, so the linker picks one by order and nothing in the source decides which. SVE.o and SVE2.o shared 166 externally defined symbols, NEON.o and NEON_DOTPROD.o shared 93. All 15 x86 tier objects already share 0, because x86 kernel names embed the ISA.

    Fixed by giving each ARM tier TU internal linkage for its kernel instantiations: the shared dependencies are hoisted above an anonymous namespace that wraps only the kernel-header includes, so just the tier's Choose_* entry points stay external. All 28 ARM pairs now share 0 symbols, and SVE went from 219 external symbols to 21.

Validated on Neoverse (gcc 12) and Ice Lake (gcc 13): full builds, spaces suite 1528/1528 and 1565/1565.

No available CPU has asimdhp without asimdfhm, so defect 1 cannot be crash-tested on our hardware. The instruction counts above measure the cause instead.

Scenarios that fail today and are fixed here

  1. A deployment on an armv8.2-a core that implements FEAT_FP16 but not FEAT_FHM, where FHM is optional. Any SQ8_FP16 inner product, L2 or cosine query selects the HP kernel, because dispatch checks asimdhp only, and the kernel executes an fmlal the core does not implement. SIGILL, in the query path, on a CPU the dispatch layer believed was supported. Nothing in the build or the configure output warns, because the tier compiled and linked cleanly.

  2. An SVE-capable core without SVE2, where the link happened to keep the SVE2-compiled body of a symbol shared between SVE.o and SVE2.o. Dispatch checks features.sve and selects Choose_*_SVE, which now points at code compiled for armv9-a+sve2. Latent on gcc 12 today, since none of the 166 shared bodies currently contains an SVE2-only opcode, but it activates on a compiler upgrade, an edit to any shared SVE kernel, or a change in object link order. Nothing in the source decides which body wins, so this can flip between builds of the same commit.

  3. A plain armv8-a core without FEAT_DotProd, for the int8 and uint8 inner products, where the link kept the +dotprod-compiled body of the shared helper. The NEON path executes sdot or udot. This is the same mechanism as scenario 2, and [MOD-17527] Make the uint8 integer accumulators exact at any dimension #1014 fixes the two named helpers; the linkage change here removes the mechanism rather than the instances.

Which issues this PR fixes

  1. MOD-17844
  2. Partially overlaps [MOD-17527] Make the uint8 integer accumulators exact at any dimension #1014 (MOD-17527), which renames one colliding helper. Complementary: this makes the collision class unrepresentable on ARM rather than fixing instances.

Main objects this PR modified

  1. src/VecSim/spaces/functions/NEON_HP.{cpp,h} and new NEON_FHM.{cpp,h}
  2. src/VecSim/spaces/{IP,L2}_space.cpp dispatch sites for SQ8_FP16
  3. cmake/aarch64InstructionFlags.cmake and src/VecSim/spaces/CMakeLists.txt for the new tier
  4. The eight ARM tier TUs under src/VecSim/spaces/functions/
  5. tests/unit/test_spaces.cpp and tests/benchmark/spaces_benchmarks/

Mark if applicable

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

@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.16%. Comparing base (e647bc8) to head (f2757e4).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1018   +/-   ##
=======================================
  Coverage   97.16%   97.16%           
=======================================
  Files         141      141           
  Lines        8361     8361           
=======================================
  Hits         8124     8124           
  Misses        237      237           

☔ 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.
Two tier translation units that include the same kernel header emitted the
same weak (COMDAT) symbols, compiled under different -march flags. The
linker then kept one body and discarded the other, chosen by link order,
with nothing in the source deciding which.

For each of the eight ARM tier TUs (NEON, NEON_DOTPROD, NEON_HP, NEON_FHM,
NEON_BF16, SVE, SVE2, SVE_BF16), wrap the kernel-header includes in an
anonymous namespace so the kernel instantiations get internal linkage,
unique to each translation unit. Only the Choose_* entry points, still
declared in the tier header, keep external linkage. The shared dependencies
(space_includes.h, spaces.h, the type headers, and the ARM intrinsics
headers) are hoisted above the anonymous namespace: wrapping them along with
the kernel includes pulls the standard library into the anonymous namespace
and fails to compile.

Measured on arm-r8g.xlarge (gcc 12) with nm -g --defined-only before this
change: NEON and NEON_DOTPROD shared 93 externally-defined symbols, SVE and
SVE2 shared 166. This change makes the whole class of collision structurally
impossible rather than fixing only those two known pairs.

x86 was measured at 0 shared symbols across all 15 tier objects, every pair,
because x86 kernel names embed the ISA and no x86 tier TU reuses another
tier's kernel headers. x86 tier TUs are therefore untouched.

Verification of the after state (all 28 ARM pairs sharing zero symbols, and
every object still exporting its own Choose_* entry points) requires
building on real ARM hardware and was not run as part of this change.
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.
@dor-forer
dor-forer force-pushed the MOD-17844-arm-simd-isa-safety branch from f527661 to f2757e4 Compare August 20, 2026 08:35
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