From 1ff55044b1d749481e7b438aea861b12169bebb2 Mon Sep 17 00:00:00 2001 From: exzile Date: Mon, 29 Jun 2026 09:30:20 -0400 Subject: [PATCH] Enable native ARM64 (aarch64) Docker build (#2648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build and run OpenVINO Model Server natively on aarch64 Linux as a CPU-only image. All changes are architecture-conditional; the x86_64 build path and outputs are unchanged (defaults verified identical). Nothing is removed — unlike the earlier draft #2485 which deleted S3FileSystem and hard-swapped lib paths (regressing x86). - Makefile: TARGETARCH (amd64/arm64) selects --platform, the OpenVINO lib subdir (intel64/aarch64) and the control-flow-protection flag; the GPU release image variant is restricted to amd64. - Dockerfile.ubuntu: arch-conditional Bazel install (arm64 standalone binary), parameterized OV runtime lib dir, arch-conditional control-flow flag, skip x86-only intel-opencl-icd and the Intel GPU driver install on arm64, and an arch-agnostic bazel-out glob. - common_settings.bzl: control-flow protection via select() — -fcf-protection=full on x86, -mbranch-protection=standard on aarch64. - third_party/opencv/install_opencv.sh: same control-flow flag selection. - third_party/openvino/BUILD: link libopenvino.so from lib/aarch64 on ARM. - src/main_capi.c: compare the C-API smoke-test output with a tolerance instead of an exact memcmp (ARM CPU defaults to f16 inference precision, so the exact f32 match spuriously failed). No-op on x86. - create_package.sh: arch-agnostic bazel-out glob; $(uname -m) multiarch dir for libOpenCL (tolerate absent on CPU-only ARM); OpenVINO runtime lib dir intel64/aarch64; guard patchelf of libopenvino_tokenizers.so (not shipped in the aarch64 OpenVINO GenAI package). - docs/build_from_source.md: TARGETARCH option and a "Building for ARM64" section. Validated on a native ubuntu-24.04-arm runner: build -> capi-build -> pkg -> release all build, and the resulting image runs (ovms --version) and serves the dummy model on the aarch64 CPU plugin with /v2/health/ready returning 200. Closes #2648 Co-Authored-By: Claude Opus 4.8 --- Dockerfile.ubuntu | 59 ++++++++++++++++++++-------- Makefile | 46 ++++++++++++++++++---- common_settings.bzl | 10 ++++- create_package.sh | 23 +++++++---- docs/build_from_source.md | 49 +++++++++++++++++++++++ src/main_capi.c | 18 ++++++++- third_party/opencv/install_opencv.sh | 8 +++- third_party/openvino/BUILD | 6 ++- 8 files changed, 183 insertions(+), 36 deletions(-) diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index ed38a3cb02..285a1985a0 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -72,7 +72,9 @@ RUN git clone --recurse-submodules --depth 1 --branch v2.10.16 https://github.co patch -d /azure/cpprestsdk/ -p1 < /ovms/third_party/cpprest/rest_sdk_v2.10.16.patch && \ patch -d /azure/azure-storage-cpp/ -p1 /espeak-ng/ # inside bazel-out. Both the shared library (picked up by the find above) # and the espeak-ng-data directory are required at runtime. -ESPEAK_DATA_SRC=$(find /ovms/bazel-out/k8-*/bin/external/espeak_ng -type d -name 'espeak-ng-data' 2>/dev/null | head -n 1 || true) +ESPEAK_DATA_SRC=$(find /ovms/bazel-out/*/bin/external/espeak_ng -type d -name 'espeak-ng-data' 2>/dev/null | head -n 1 || true) if [ -n "$ESPEAK_DATA_SRC" ] && [ -d "$ESPEAK_DATA_SRC" ] ; then mkdir -p /ovms_release/share cp -rL "$ESPEAK_DATA_SRC" /ovms_release/share/ ; @@ -96,8 +99,8 @@ if ! [[ $debug_bazel_flags == *"_py_off"* ]]; then mv /ovms_release/lib/python/b if ! [[ $debug_bazel_flags == *"_py_off"* ]]; then mkdir -p /ovms_release/lib/python/openvino_genai-2026.3.dist-info ; \ echo $'Metadata-Version: 1.0\nName: openvino-genai\nVersion: 2026.3\nRequires-Python: >=3.9\nRequires-Dist: openvino-genai~=2026.3.0' > /ovms_release/lib/python/openvino_genai-2026.3.dist-info/METADATA; fi -if [ -f /opt/intel/openvino/runtime/lib/intel64/plugins.xml ]; then cp /opt/intel/openvino/runtime/lib/intel64/plugins.xml /ovms_release/lib/ ; fi -find /opt/intel/openvino/runtime/lib/intel64/ -iname '*.mvcmd*' -exec cp -vP {} /ovms_release/lib/ \; +if [ -f /opt/intel/openvino/runtime/lib/${OV_LIB_ARCH}/plugins.xml ]; then cp /opt/intel/openvino/runtime/lib/${OV_LIB_ARCH}/plugins.xml /ovms_release/lib/ ; fi +find /opt/intel/openvino/runtime/lib/${OV_LIB_ARCH}/ -iname '*.mvcmd*' -exec cp -vP {} /ovms_release/lib/ \; if [ -d /opt/intel/openvino/runtime/3rdparty ] ; then find /opt/intel/openvino/runtime/3rdparty/ -iname '*libtbb.so*' -exec cp -vP {} /ovms_release/lib/ \;; fi if [[ $debug_bazel_flags == *"--copt=-g -c dbg"* ]]; then find /opt/intel/openvino/runtime/3rdparty/ -iname '*libtbb_debug*' -exec cp -vP {} /ovms_release/lib/ \;; fi find /opt/opencv/lib/ -iname '*.so*' -exec cp -vP {} /ovms_release/lib/ \; @@ -105,7 +108,7 @@ cp /opt/opencv/share/licenses/opencv4/* /ovms/release_files/thirdparty-licenses/ # Bundle eSpeak-ng license text when eSpeak artifacts are included. # The source repository is checked out under Bazel external trees. -ESPEAK_LICENSE_SRC=$(find /ovms/bazel-out/k8-*/bin/external/espeak_ng -type f \ +ESPEAK_LICENSE_SRC=$(find /ovms/bazel-out/*/bin/external/espeak_ng -type f \ \( -name 'COPYING*' -o -name 'LICENSE*' \) \ 2>/dev/null | head -n 1 || true) if [ -n "$ESPEAK_LICENSE_SRC" ] && [ -f "$ESPEAK_LICENSE_SRC" ] ; then @@ -113,7 +116,9 @@ if [ -n "$ESPEAK_LICENSE_SRC" ] && [ -f "$ESPEAK_LICENSE_SRC" ] ; then fi if [ "$BASE_OS" == "redhat" ] ; then cp -P /usr/lib64/libOpenCL.so* /ovms_release/lib/ ; fi -if [[ "$BASE_OS" =~ "ubuntu" ]] ; then cp -P /usr/lib/x86_64-linux-gnu/libOpenCL.so* /ovms_release/lib/ ; fi +# Use the running architecture's multiarch dir (x86_64-linux-gnu / aarch64-linux-gnu). +# OpenCL is only needed for GPU; on CPU-only aarch64 it may be absent, so tolerate it. +if [[ "$BASE_OS" =~ "ubuntu" ]] ; then cp -P /usr/lib/"$(uname -m)"-linux-gnu/libOpenCL.so* /ovms_release/lib/ 2>/dev/null || true ; fi if [ "$FUZZER_BUILD" == "0" ]; then find /ovms/bazel-bin/src -name 'ovms' -type f -exec cp -v {} /ovms_release/bin \; ; fi; cd /ovms_release/bin @@ -121,9 +126,11 @@ if [ "$FUZZER_BUILD" == "0" ]; then patchelf --remove-rpath ./ovms && patchelf - find /ovms_release/lib/ -iname '*.so*' -exec patchelf --debug --remove-rpath {} \; find /ovms_release/lib/ -iname '*.so*' -exec patchelf --debug --set-rpath '$ORIGIN/../lib' {} \; -find /opt/intel/openvino/runtime/lib/intel64/ -iname '*.so*' -exec cp -vP {} /ovms_release/lib/ \; +find /opt/intel/openvino/runtime/lib/${OV_LIB_ARCH}/ -iname '*.so*' -exec cp -vP {} /ovms_release/lib/ \; patchelf --debug --set-rpath '$ORIGIN' /ovms_release/lib/libopenvino.so -patchelf --debug --set-rpath '$ORIGIN' /ovms_release/lib/libopenvino_tokenizers.so +# libopenvino_tokenizers.so is not shipped in the aarch64 OpenVINO GenAI package, +# so only patch it when present (it is always present on x86_64). +if [ -e /ovms_release/lib/libopenvino_tokenizers.so ]; then patchelf --debug --set-rpath '$ORIGIN' /ovms_release/lib/libopenvino_tokenizers.so ; fi patchelf --debug --set-rpath '$ORIGIN' /ovms_release/lib/lib*plugin.so if [ -e /ovms_release/lib/libopenvino_genai_c.so ]; then rm -rf /ovms_release/lib/libopenvino_genai_c.so* ; fi diff --git a/docs/build_from_source.md b/docs/build_from_source.md index 1b3c9f8579..00ab42cce2 100644 --- a/docs/build_from_source.md +++ b/docs/build_from_source.md @@ -133,6 +133,19 @@ Example: make release_image GPU=1 ``` +### `TARGETARCH` + +Target CPU architecture of the produced image. Default value: `amd64`. Set to `arm64` +to build a native `aarch64` image (see [Building for ARM64](#building-for-arm64-aarch64) +below). On `arm64` the build automatically selects the ARM-appropriate toolchain +options (branch protection instead of Intel CET), the `aarch64` OpenVINO runtime +library layout, and skips the x86-only Intel GPU/NPU components. + +Example: +```bash +make ovms_builder_image TARGETARCH=arm64 BASE_OS=ubuntu22 OV_USE_BINARY=1 DLDT_PACKAGE_URL= +``` + ## Building minimal image Building minimalistic OVMS docker image requires disabling all optional features: @@ -141,6 +154,42 @@ Building minimalistic OVMS docker image requires disabling all optional features make release_image GPU=0 MEDIAPIPE_DISABLE=1 PYTHON_DISABLE=1 ``` +## Building for ARM64 (aarch64) + +OpenVINO Model Server can be built and run natively on `aarch64` (ARM64) Linux as a +CPU-only image. The build must run **on an `aarch64` host** (cross-building is not +supported here) and uses pre-built OpenVINO binaries via `OV_USE_BINARY=1`. + +Notes specific to ARM64: +- Set `TARGETARCH=arm64`. +- The OpenVINO GenAI nightly packages publish `aarch64` archives for **Ubuntu 22.04 + only**, so build with `BASE_OS=ubuntu22` and point `DLDT_PACKAGE_URL` at the + matching `..._arm64.tar.gz` archive. +- ARM64 is CPU-only: the Intel GPU/NPU drivers and `intel-opencl-icd` are x86-only + and are skipped automatically. Build with `GPU=0` and do not request the GPU image + variant. +- `MEDIAPIPE_DISABLE=1 PYTHON_DISABLE=1` are recommended for a minimal image. + +Build the release image: +```bash +make release_image \ + TARGETARCH=arm64 \ + BASE_OS=ubuntu22 \ + OV_USE_BINARY=1 \ + DLDT_PACKAGE_URL=https://storage.openvinotoolkit.org/repositories/openvino_genai/packages/nightly//openvino_genai_ubuntu22__arm64.tar.gz \ + GPU=0 MEDIAPIPE_DISABLE=1 PYTHON_DISABLE=1 +``` + +Verify the resulting image runs and serves a model: +```bash +docker run --rm openvino/model_server:latest --version +docker run -d --name ovms -p 8000:8000 \ + -v $(pwd)/src/test/dummy:/models/dummy:ro \ + openvino/model_server:latest \ + --model_name dummy --model_path /models/dummy --rest_port 8000 +curl http://localhost:8000/v2/health/ready +``` + ## Building Binary Package The binary packages includes the `ovms` executable and linked libraries for bare metal deployments. It includes also a shared library for the model server CAPI interface. Building `ovms.tar.gz` package is possible by using `targz_package` target: diff --git a/src/main_capi.c b/src/main_capi.c index 3f0e8e76a6..3e378a7095 100644 --- a/src/main_capi.c +++ b/src/main_capi.c @@ -111,7 +111,23 @@ int main() { for (int i = 0; i < NUM_ELEMENTS; i++) { expectedOutput[i] = INPUT_ELEMENT_VALUE + 1.0f; } - if (memcmp(oData, expectedOutput, DATA_SIZE) != 0) { + // Compare with a small tolerance rather than exact bytes: on some targets + // (e.g. ARM CPU) OpenVINO defaults to f16 inference precision, so results + // differ from the exact f32 value within rounding error and an exact memcmp + // would spuriously fail. The tolerance still catches genuinely wrong output. + const float* outFloats = (const float*)oData; + int outputCorrect = 1; + for (int i = 0; i < NUM_ELEMENTS; i++) { + float diff = outFloats[i] - expectedOutput[i]; + if (diff < 0.0f) { + diff = -diff; + } + if (diff > 0.01f) { + outputCorrect = 0; + break; + } + } + if (!outputCorrect) { fprintf(stderr, "output is not correct\n"); OVMS_InferenceResponseDelete(response); diff --git a/third_party/opencv/install_opencv.sh b/third_party/opencv/install_opencv.sh index 367c4706e4..c193260d2b 100755 --- a/third_party/opencv/install_opencv.sh +++ b/third_party/opencv/install_opencv.sh @@ -21,7 +21,13 @@ set -exo pipefail os=${os:-auto} opencv_branch=${opencv_branch:-${OPENCV_VERSION:-4.13.0}} work_dir=${work_dir:-/opt} -SDL_OPS="-Wl,-z,relro,-z,now,-z,noexecstack -Wall -Wextra -Wimplicit-fallthrough -fPIE -pie -fstack-protector-strong -fexceptions -fasynchronous-unwind-tables -fcf-protection -fpic -O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -fno-strict-overflow -Wall -Wno-unknown-pragmas -Wno-error=sign-compare -fno-delete-null-pointer-checks -fwrapv -fstack-clash-protection -Wformat -Wformat-security -Werror=format-security -s -D_GLIBCXX_USE_CXX11_ABI=1 -Wuninitialized" +# Control-flow protection flag is x86-only (-fcf-protection); ARM uses -mbranch-protection. +if [ "$(uname -m)" = "aarch64" ] || [ "$(uname -m)" = "arm64" ]; then + CF_PROTECTION="-mbranch-protection=standard" +else + CF_PROTECTION="-fcf-protection" +fi +SDL_OPS="-Wl,-z,relro,-z,now,-z,noexecstack -Wall -Wextra -Wimplicit-fallthrough -fPIE -pie -fstack-protector-strong -fexceptions -fasynchronous-unwind-tables ${CF_PROTECTION} -fpic -O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -fno-strict-overflow -Wall -Wno-unknown-pragmas -Wno-error=sign-compare -fno-delete-null-pointer-checks -fwrapv -fstack-clash-protection -Wformat -Wformat-security -Werror=format-security -s -D_GLIBCXX_USE_CXX11_ABI=1 -Wuninitialized" #=================================================================================================== diff --git a/third_party/openvino/BUILD b/third_party/openvino/BUILD index dbb366dbeb..a76e7100c2 100644 --- a/third_party/openvino/BUILD +++ b/third_party/openvino/BUILD @@ -50,8 +50,12 @@ cc_library( cc_library( name = "openvino", + # OpenVINO places its runtime libs under lib/intel64 on x86_64 and lib/aarch64 + # on ARM. glob ignores patterns that match nothing, so exactly one resolves per + # architecture and the link finds libopenvino.so on both. srcs = glob([ - "lib/intel64/libopenvino.so" + "lib/intel64/libopenvino.so", + "lib/aarch64/libopenvino.so", ]), strip_include_prefix = "include/ie", visibility = ["//visibility:public"],