Output of strings libarm_compute-static.a | grep arm_compute_version:
arm_compute_version=v0.0-unreleased Build options: {'neon': '1', 'opencl': '0', 'examples': '0', 'Werror': '0', 'gemm_tuner': '0', 'reference_openmp': '0', 'validation_tests': '0', 'benchmark_tests': '0', 'data_layout_support': 'all', 'arch': 'arm64-v8a', 'build_dir': 'arm64-v8a', 'openmp': '0', 'cppthreads': '1', 'estate': '64', 'multi_isa': '1', 'os': 'linux', 'build': 'cross_compile', 'toolchain_prefix': 'aarch64-linux-gnu-', 'fixed_format_kernels': 'True'} Git hash=b'5ef9d31bd7c63b4a096d30a362bd1111ae426f51'
Platform: Raspberry Pi 4, Cortex-A72, AArch64; CPU features are fp asimd evtstrm crc32 cpuid (no SVE)
Operating System: Debian/Raspberry Pi OS, kernel 6.12.75+rpt-rpi-v8
Problem description
A GCC-built multi_isa=1 static library can export an SVE-autovectorized weak/COMDAT definition of the common inline arm_compute::Iterator constructor. Depending on normal static-link extraction order, that definition can replace the baseline Armv8-A copy used by CpuCastKernel. Running a public NECast operation on a non-SVE CPU then executes mov z0.b, #0 and terminates with SIGILL.
This reproduces from clean ACL main commit 5ef9d31bd7c63b4a096d30a362bd1111ae426f51, without OpenVINO, LTO, internal ACL symbols, --undefined, or --whole-archive.
Reproducer
activation_component.cpp:
#include "arm_compute/core/TensorInfo.h"
#include "arm_compute/function_info/ActivationLayerInfo.h"
#include "arm_compute/runtime/NEON/functions/NEActivationLayer.h"
#include "arm_compute/runtime/Tensor.h"
int run_activation_component()
{
using namespace arm_compute;
Tensor input, output;
input.allocator()->init(TensorInfo(TensorShape(64U), 1, DataType::F32));
output.allocator()->init(TensorInfo(TensorShape(64U), 1, DataType::F32));
NEActivationLayer activation;
activation.configure(&input, &output,
ActivationLayerInfo(ActivationLayerInfo::ActivationFunction::RELU));
input.allocator()->allocate();
output.allocator()->allocate();
activation.run();
return 0;
}
cast_main.cpp:
#include "arm_compute/core/TensorInfo.h"
#include "arm_compute/runtime/NEON/functions/NECast.h"
#include "arm_compute/runtime/Tensor.h"
#include <cstdint>
#include <iostream>
int run_activation_component();
int main()
{
using namespace arm_compute;
Tensor input, output;
input.allocator()->init(TensorInfo(TensorShape(64U), 1, DataType::S16));
output.allocator()->init(TensorInfo(TensorShape(64U), 1, DataType::S32));
NECast cast;
cast.configure(&input, &output, ConvertPolicy::SATURATE);
input.allocator()->allocate();
output.allocator()->allocate();
auto *src = reinterpret_cast<int16_t *>(input.buffer());
for (int i = 0; i < 64; ++i) src[i] = static_cast<int16_t>(i - 32);
std::cout << "before_cast" << std::endl;
cast.run();
std::cout << "after_cast" << std::endl;
return run_activation_component();
}
Cross-build with GCC 10.5.0:
git clone https://github.com/ARM-software/ComputeLibrary.git
cd ComputeLibrary
git checkout 5ef9d31bd7c63b4a096d30a362bd1111ae426f51
scons -j8 neon=1 opencl=0 examples=0 Werror=0 gemm_tuner=0 \
reference_openmp=0 validation_tests=0 benchmark_tests=0 \
data_layout_support=all arch=arm64-v8a build_dir=arm64-v8a \
openmp=0 cppthreads=1 estate=64 multi_isa=1 \
os=linux build=cross_compile toolchain_prefix=aarch64-linux-gnu- \
fixed_format_kernels=True build/arm64-v8a/libarm_compute-static.a
CXX=aarch64-linux-gnu-g++
LIB="$PWD/build/arm64-v8a/libarm_compute-static.a"
$CXX -std=c++17 -O2 -g -march=armv8-a -I. -Iinclude \
-c ../activation_component.cpp -o activation_component.o
$CXX -std=c++17 -O2 -g -march=armv8-a -I. -Iinclude \
-c ../cast_main.cpp -o cast_main.o
# Failing order. The archive is repeated normally; no member is forced in.
$CXX -Wl,-Map,acl-public-reproducer.map \
activation_component.o "$LIB" cast_main.o "$LIB" \
-pthread -ldl -o acl-public-reproducer
# A/B control: identical inputs, only component order is reversed.
$CXX -Wl,-Map,acl-public-control.map \
cast_main.o "$LIB" activation_component.o "$LIB" \
-pthread -ldl -o acl-public-control
Copy both executables to a non-SVE AArch64 system and run them.
Actual result
The reproducer failed in 5/5 fresh processes; the control passed 3/3:
run=1 rc=132 output=before_cast
run=2 rc=132 output=before_cast
run=3 rc=132 output=before_cast
run=4 rc=132 output=before_cast
run=5 rc=132 output=before_cast
control_run=1 rc=0 output=before_cast after_cast
control_run=2 rc=0 output=before_cast after_cast
control_run=3 rc=0 output=before_cast after_cast
The tested SHA-256 values were 53ce7ad058fcc89d9230d3b21a577d69f9833ea60ffba45ca73ba8685010b03f for libarm_compute-static.a, 2c5d3c83393cbe421310177afc2e7f4fcdcafbbcf16fd7ccdae3580eeaaf0b1a for the failing executable, and 221348c3c75b3251af245a2ac27da04f0e99380c750c46e554ad56282f7bfefc for the control.
GDB shows the unguarded SVE instruction and baseline call path:
Thread 1 received signal SIGILL, Illegal instruction.
arm_compute::Iterator::Iterator(...):
=> mov z0.b, #0
#0 arm_compute::Iterator::Iterator(...)
#1 arm_compute::cpu::kernels::CpuCastKernel::run_op(...)
#2 arm_compute::IScheduler::schedule_common(...)
#3 arm_compute::experimental::INEOperator::run(...)
#4 arm_compute::NECast::run()
#5 main() at cast_main.cpp:33
The failing link map selects the constructor from the SVE activation object:
.text._ZN11arm_compute8IteratorC2EPKNS_7ITensorERKNS_6WindowE
0x00000000000337f0 0x214 libarm_compute-static.a(fp32.o)
arm_compute::Iterator::Iterator(...)
The preceding map symbol identifies that member as src/cpu/kernels/activation/generic/sve/fp32.cpp. Its constructor starts with:
337fc: mov z0.b, #0
33810: whilelo p0.d, xzr, x2
33830: st1d {z0.d}, p0, [x4, x3, lsl #3]
In the passing control, the retained constructor instead comes from CpuCastKernel.o and contains only baseline AArch64 instructions.
Expected result
A library built with multi_isa=1 arch=arm64-v8a must not execute SVE instructions before runtime ISA dispatch on an Armv8-A/ASIMD-only CPU. Both link orders should complete successfully.
Root cause
Iterator is inline and therefore emitted by both baseline and ISA-specific translation units. GCC 10 auto-vectorizes its _dims() initialization with SVE when emitted from activation/generic/sve/fp32.cpp. The linker coalesces these same-named weak/COMDAT definitions and may retain the SVE body for baseline callers.
For reference, the monolithic arm_compute_validation Cast test passed 3/3 on the same target because its link layout retained the baseline constructor. That does not cover this link-order-dependent case.
Proposed fix
Pull request #1317
Fix validation
The patched tree was rebuilt from scratch with the same GCC 10.5.0 command.
The exact formerly failing executable was run on the same non-SVE Cortex-A72
target:
run=1 rc=0 output=before_cast after_cast
run=2 rc=0 output=before_cast after_cast
run=3 rc=0 output=before_cast after_cast
run=4 rc=0 output=before_cast after_cast
run=5 rc=0 output=before_cast after_cast
control_run=1 rc=0 output=before_cast after_cast
control_run=2 rc=0 output=before_cast after_cast
control_run=3 rc=0 output=before_cast after_cast
Output of
strings libarm_compute-static.a | grep arm_compute_version:Platform: Raspberry Pi 4, Cortex-A72, AArch64; CPU features are
fp asimd evtstrm crc32 cpuid(no SVE)Operating System: Debian/Raspberry Pi OS, kernel
6.12.75+rpt-rpi-v8Problem description
A GCC-built
multi_isa=1static library can export an SVE-autovectorized weak/COMDAT definition of the common inlinearm_compute::Iteratorconstructor. Depending on normal static-link extraction order, that definition can replace the baseline Armv8-A copy used byCpuCastKernel. Running a publicNECastoperation on a non-SVE CPU then executesmov z0.b, #0and terminates withSIGILL.This reproduces from clean ACL
maincommit5ef9d31bd7c63b4a096d30a362bd1111ae426f51, without OpenVINO, LTO, internal ACL symbols,--undefined, or--whole-archive.Reproducer
activation_component.cpp:cast_main.cpp:Cross-build with GCC 10.5.0:
Copy both executables to a non-SVE AArch64 system and run them.
Actual result
The reproducer failed in 5/5 fresh processes; the control passed 3/3:
The tested SHA-256 values were
53ce7ad058fcc89d9230d3b21a577d69f9833ea60ffba45ca73ba8685010b03fforlibarm_compute-static.a,2c5d3c83393cbe421310177afc2e7f4fcdcafbbcf16fd7ccdae3580eeaaf0b1afor the failing executable, and221348c3c75b3251af245a2ac27da04f0e99380c750c46e554ad56282f7bfefcfor the control.GDB shows the unguarded SVE instruction and baseline call path:
The failing link map selects the constructor from the SVE activation object:
The preceding map symbol identifies that member as
src/cpu/kernels/activation/generic/sve/fp32.cpp. Its constructor starts with:In the passing control, the retained constructor instead comes from
CpuCastKernel.oand contains only baseline AArch64 instructions.Expected result
A library built with
multi_isa=1 arch=arm64-v8amust not execute SVE instructions before runtime ISA dispatch on an Armv8-A/ASIMD-only CPU. Both link orders should complete successfully.Root cause
Iteratoris inline and therefore emitted by both baseline and ISA-specific translation units. GCC 10 auto-vectorizes its_dims()initialization with SVE when emitted fromactivation/generic/sve/fp32.cpp. The linker coalesces these same-named weak/COMDAT definitions and may retain the SVE body for baseline callers.For reference, the monolithic
arm_compute_validationCast test passed 3/3 on the same target because its link layout retained the baseline constructor. That does not cover this link-order-dependent case.Proposed fix
Pull request #1317
Fix validation
The patched tree was rebuilt from scratch with the same GCC 10.5.0 command.
The exact formerly failing executable was run on the same non-SVE Cortex-A72
target: