diff --git a/.github/workflows/build-openvino.yml b/.github/workflows/build-openvino.yml new file mode 100644 index 00000000000..4094dd01214 --- /dev/null +++ b/.github/workflows/build-openvino.yml @@ -0,0 +1,256 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on: https://github.com/openvinotoolkit/openvino/blob/2026.3.1/.github/workflows/manylinux_2_28.yml +# (the recipe behind the published PyPI wheels) with the riscv64 cmake options of +# https://github.com/openvinotoolkit/openvino/blob/2026.3.1/.github/workflows/linux_riscv.yml +name: Build openvino wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/openvino.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-openvino.yml' + - 'docs/packages/openvino.yaml' + - 'patches/openvino/**' + push: + branches: [main] + paths: + - '.github/workflows/build-openvino.yml' + - 'docs/packages/openvino.yaml' + - 'patches/openvino/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + # Upstream's AzureCR build image isn't public; manylinux is the closest equivalent. + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: openvino + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build openvino ${{ matrix.version }} manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 1440 # 24h + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + + env: + OPENVINO_VERSION: ${{ matrix.version }} + # Upstream builds the runtime once, then loops the interpreters over src/bindings/python. + PYTHON_TAGS: cp312 cp313 cp314 cp314t + + steps: + - name: Checkout openvino ${{ matrix.version }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: openvinotoolkit/openvino + ref: ${{ env.OPENVINO_VERSION }} + submodules: true + persist-credentials: false + path: openvino + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Patch openvino source + working-directory: openvino + run: git apply -v ../python-wheels/patches/openvino/${{ env.OPENVINO_VERSION }}/*.patch + + - name: Write container script + run: | + mkdir -p build install + cat > openvino/riscv64-build-and-test.sh <<'EOF' + #!/bin/bash + set -e -x + + # Vendored snappy and xbyak_riscv predate CMake 4's floor; upstream calls /usr/bin/cmake too. + # Retried: a mirrorlist hiccup here otherwise throws away a multi-hour runner slot. + for i in 1 2 3; do dnf install -y cmake && break || sleep 30; done + + # gcc-14 bug, as in upstream's manylinux job: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107532 + export CXXFLAGS="-Wno-dangling-reference" + + case "$1" in + build|test) ;; + *) echo "usage: $0 " >&2; exit 1 ;; + esac + + if [ "$1" = build ]; then + # cmake/dependencies.cmake downloads a prebuilt oneTBB for riscv64 + # (oneapi-tbb-2022.3.0-lin-riscv-release.tgz) that is a T-Head Xuantie + # toolchain build carrying vendor CUSTOM-0 opcodes (CLAUDE.md gotcha 449), + # not portable off this fleet. Build the same oneTBB release from source + # with this image's own generic rv64gc toolchain and point OpenVINO at it + # via -DTBB_DIR, which ov_find_package_tbb() honors ahead of ever calling + # ov_download_tbb(). + git clone --branch v2022.3.0 --depth 1 https://github.com/oneapi-src/oneTBB.git /tmp/onetbb + /usr/bin/cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/opt/onetbb \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DTBB_TEST=OFF \ + -DTBB_EXAMPLES=OFF \ + -DTBB_STRICT=OFF \ + -S /tmp/onetbb \ + -B /tmp/onetbb-build + /usr/bin/cmake --build /tmp/onetbb-build --parallel "$(nproc)" + /usr/bin/cmake --install /tmp/onetbb-build + + # oneDNN compiles all of itself with -march=rv64gcv whenever the compiler can, with no + # runtime gate; its static initializers then SIGILL at plugin dlopen on non-RVV-1.0 cores. + /usr/bin/cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -DCAN_COMPILE_RVV_INTRINSICS=OFF \ + -DENABLE_INTEL_GPU=OFF \ + -DENABLE_INTEL_NPU=OFF \ + -DENABLE_SAMPLES=OFF \ + -DENABLE_NCC_STYLE=OFF \ + -DENABLE_PYTHON=OFF \ + -DENABLE_JS=OFF \ + -DENABLE_OV_JAX_FRONTEND=OFF \ + -DENABLE_STRICT_DEPENDENCIES=OFF \ + -DTBB_DIR=/opt/onetbb/lib/cmake/TBB \ + -S /openvino \ + -B /build + /usr/bin/cmake --build /build --parallel "$(nproc)" + fi + + for python_tag in ${PYTHON_TAGS}; do + case "${python_tag}" in + *t) python_impl="${python_tag%t}" ;; + *) python_impl="${python_tag}" ;; + esac + python_exe="/opt/python/${python_impl}-${python_tag}/bin/python" + + if [ "$1" = build ]; then + "${python_exe}" -m pip install -r /openvino/src/bindings/python/wheel/requirements-dev.txt + + gil_option=() + if [ "${python_tag}" != "${python_impl}" ]; then + gil_option=(-DENABLE_GIL_PYTHON_API=OFF) + fi + + /usr/bin/cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -DPython3_EXECUTABLE="${python_exe}" \ + -DOpenVINODeveloperPackage_DIR=/build \ + -DENABLE_PYTHON=ON \ + -DENABLE_WHEEL=ON \ + "${gil_option[@]}" \ + -S /openvino/src/bindings/python \ + -B "/build/python_${python_tag}" + /usr/bin/cmake --build "/build/python_${python_tag}" --parallel "$(nproc)" + /usr/bin/cmake --install "/build/python_${python_tag}" --prefix /install --component python_wheels + else + wheel=(/install/wheels/openvino-*-"${python_impl}"-"${python_tag}"-*.whl) + "${python_exe}" -m pip install "${wheel[0]}" pytest pillow + + # Run from /tmp so the checkout's src/bindings/python tree cannot shadow the wheel. + cd /tmp + "${python_exe}" -c ' + import numpy as np + import openvino as ov + from openvino import opset14 as ops + + print(ov.get_version()) + assert ov._pyopenvino.__file__.endswith(".so"), ov._pyopenvino.__file__ + + core = ov.Core() + print(core.available_devices) + assert "CPU" in core.available_devices + + param = ops.parameter([1, 4], ov.Type.f32) + model = ov.Model(ops.relu(param), [param], "relu") + compiled = core.compile_model(model, "CPU") + result = compiled(np.array([[-2.0, -1.0, 0.0, 3.0]], dtype=np.float32))[0] + assert np.array_equal(result, [[0.0, 0.0, 0.0, 3.0]]), result' + + # cp314t passes the smoke check above but deadlocked in test_start_async (a Python + # callback run from an inference worker thread) for 11h; the GIL builds pass all 7019. + if [ "${python_tag}" = "${python_impl}" ]; then + tests=/openvino/src/bindings/python/tests + "${python_exe}" -m pytest -v \ + "${tests}/test_runtime" \ + "${tests}/test_graph" \ + "${tests}/test_transformations" \ + -m 'not template_extension' + fi + fi + done + EOF + + - name: Build wheels + run: | + podman run -t \ + --log-driver=none \ + --network=host \ + -v "${PWD}/openvino":/openvino \ + -v "${PWD}/build":/build \ + -v "${PWD}/install":/install \ + --workdir /openvino \ + -e PYTHON_TAGS="${PYTHON_TAGS}" \ + -e PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ \ + --pull=newer \ + "${MANYLINUX_RISCV64_IMAGE}" \ + bash /openvino/riscv64-build-and-test.sh build + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: openvino-${{ env.OPENVINO_VERSION }}-wheels-manylinux_riscv64 + path: install/wheels/*.whl + if-no-files-found: error + + - name: Test wheels + timeout-minutes: 60 # upstream's job_python_api_tests.yml caps its own run at 30 + run: | + podman run -t \ + --log-driver=none \ + --network=host \ + -v "${PWD}/openvino":/openvino \ + -v "${PWD}/install":/install \ + --workdir /openvino \ + -e PYTHON_TAGS="${PYTHON_TAGS}" \ + -e PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ \ + "${MANYLINUX_RISCV64_IMAGE}" \ + bash /openvino/riscv64-build-and-test.sh test + + publish: + name: Publish openvino ${{ matrix.version }} + needs: [setup, build_wheels] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: openvino-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/openvino.yaml b/docs/packages/openvino.yaml new file mode 100644 index 00000000000..8e692b2d3a5 --- /dev/null +++ b/docs/packages/openvino.yaml @@ -0,0 +1,5 @@ +package-name: openvino +source-code: https://github.com/openvinotoolkit/openvino +license: Apache-2.0 +versions: +- version: 2026.3.1 diff --git a/patches/openvino/2026.3.1/0001-cpu-riscv64-never-probe-for-RVV-1.0-support.patch b/patches/openvino/2026.3.1/0001-cpu-riscv64-never-probe-for-RVV-1.0-support.patch new file mode 100644 index 00000000000..3185980ae0c --- /dev/null +++ b/patches/openvino/2026.3.1/0001-cpu-riscv64-never-probe-for-RVV-1.0-support.patch @@ -0,0 +1,66 @@ +From 9c0ff41dfe1be9aaeb6080e01fed7d4fb38777da Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Thu, 24 Sep 2026 11:43:42 +0000 +Subject: [PATCH] cpu: riscv64: never probe for RVV 1.0 support + +can_compile_rvv100()/can_compile_zvfh() try to detect RVV 1.0 by executing a +v1.0 vsetivli under a SIGILL handler and recovering via sigsetjmp/siglongjmp +if it traps. On RVV 0.7.1 hardware (T-Head Xuantie C906/C910/C920: TH1520, +SG2042) that recovery does not hold -- the illegal instruction brings the +whole process down (SIGILL, exit 132) instead of returning false, observed +as a crash inside ov.Core()/device enumeration the moment the CPU plugin is +loaded, well before any model is compiled. + +Hardcode both probes to report RVV as unsupported. Every mayiuse(gv)/ +mayiuse(gv_zvfh) caller in this plugin already treats a false result as +'fall back to the scalar/generic executor', and Transformations::MainSnippets() +already turns Snippets/Subgraph tokenization off entirely when +is_supported_isa() (== mayiuse(gv)) is false -- both existing, already- +exercised code paths for riscv64 targets without RVV. This just makes this +target take them unconditionally, instead of taking the branch that crashes. + +We do not have a kernel/toolchain-level root cause or a backtrace from the +affected hardware for why the SIGILL recovery itself fails here (it would +need re-running just the test leg against an existing wheel under gdb); until +that exists to accompany a report, this is carried as a local workaround. + +Upstream-Status: To upstream [no root-cause backtrace yet to accompany a report] +--- + .../nodes/kernels/riscv64/cpu_isa_traits.cpp | 18 ++++++++++++++---- + 1 file changed, 14 insertions(+), 4 deletions(-) + +diff --git a/src/plugins/intel_cpu/src/nodes/kernels/riscv64/cpu_isa_traits.cpp b/src/plugins/intel_cpu/src/nodes/kernels/riscv64/cpu_isa_traits.cpp +index 567d627..cc2293d 100644 +--- a/src/plugins/intel_cpu/src/nodes/kernels/riscv64/cpu_isa_traits.cpp ++++ b/src/plugins/intel_cpu/src/nodes/kernels/riscv64/cpu_isa_traits.cpp +@@ -74,13 +74,23 @@ bool can_execute_generated_code() { + } + + bool can_compile_rvv100() { +- static const bool status = can_execute_generated_code(); +- return status; ++ // Deliberately never probe: can_execute_generated_code() is supposed to ++ // detect RVV 1.0 support by executing a v1.0 vsetivli under a SIGILL handler and ++ // recovering if it traps, but on RVV 0.7.1 hardware (e.g. T-Head Xuantie C906/C910/C920) ++ // the illegal instruction does not recover cleanly through sigsetjmp/siglongjmp -- it ++ // brings the whole process down instead of returning false, which is exactly the crash ++ // this patch works around. Hardcoding "unsupported" routes every mayiuse(gv) caller ++ // through OpenVINO's existing non-RVV fallback (scalar/generic node executors, and ++ // Snippets/Subgraph tokenization already turns itself off via is_supported_isa() when ++ // mayiuse(gv) is false) instead of ever attempting to execute or JIT-compile RVV 1.0 ++ // code on this target. ++ return false; + } + + bool can_compile_zvfh() { +- static const bool status = can_execute_generated_code(); +- return status; ++ // See can_compile_rvv100() above: RVV support is unconditionally disabled for this ++ // target, so the (narrower, RVV-1.0-dependent) Zvfh probe must never run either. ++ return false; + } + + } // namespace +-- +2.43.0 + diff --git a/patches/openvino/2026.3.1/0002-cpu-riscv64-do-not-read-vlenb-unless-RVV-1.0-is-usable.patch b/patches/openvino/2026.3.1/0002-cpu-riscv64-do-not-read-vlenb-unless-RVV-1.0-is-usable.patch new file mode 100644 index 00000000000..3dcffd90167 --- /dev/null +++ b/patches/openvino/2026.3.1/0002-cpu-riscv64-do-not-read-vlenb-unless-RVV-1.0-is-usable.patch @@ -0,0 +1,66 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 25 Sep 2026 14:30:00 +0000 +Subject: [PATCH] cpu: riscv64: do not read vlenb unless RVV 1.0 is usable + +mayiuse() constructs the Xbyak_riscv::CPU singleton on every call, for +every ISA. That constructor JIT-compiles and runs `csrr a0, vlenb` whenever +AT_HWCAP advertises V -- with no SIGILL handler around it, unlike the RVV 1.0 +probe in can_compile_rvv100(). On cores whose kernel sets the V hwcap but +whose vector unit is not RVV 1.0 (T-Head Xuantie C906/C910/C920, RVV 0.7.1), +a vector-CSR access is exactly the kind of instruction that can trap, and +nothing would catch it. The only callers outside this file ask for gv or +gv_zvfh, reached from compile_model() (transformation pipeline, eltwise +executor), so this sits on every model compilation. + +Evaluate the RVV 1.0 probe first in the gv case, and build the CPU object +only for the g case. With can_compile_rvv100() hardcoded to false by the +previous patch, mayiuse(gv)/mayiuse(gv_zvfh) now return false without +executing any vector instruction. Behaviour on RVV 1.0 hardware with the +probe restored is unchanged: the probe runs under its own SIGILL handler +before the HWCAP/vlenb query instead of after it. + +This one is defensive: the SIGILL observed on the CI hardware comes from +oneDNN being compiled with -march=rv64gcv (a static initializer at plugin +dlopen time), which is handled at configure time, not here. It has not been +seen crashing on hardware by itself, so there is no backtrace to report yet. + +Upstream-Status: To upstream [defensive fix, no hardware crash observed from this path yet to accompany a report] +--- + .../intel_cpu/src/nodes/kernels/riscv64/cpu_isa_traits.cpp | 10 +++++++--- + 1 file changed, 7 insertions(+), 3 deletions(-) + +diff --git a/src/plugins/intel_cpu/src/nodes/kernels/riscv64/cpu_isa_traits.cpp b/src/plugins/intel_cpu/src/nodes/kernels/riscv64/cpu_isa_traits.cpp +index cc2293de..d3e15af7 100644 +--- a/src/plugins/intel_cpu/src/nodes/kernels/riscv64/cpu_isa_traits.cpp ++++ b/src/plugins/intel_cpu/src/nodes/kernels/riscv64/cpu_isa_traits.cpp +@@ -96,19 +96,23 @@ bool can_compile_zvfh() { + } // namespace + + bool mayiuse(const cpu_isa_t cpu_isa) { +- const auto cpu = CPU::getInstance(); ++ // Xbyak_riscv::CPU's constructor JIT-executes `csrr a0, vlenb` with no SIGILL handler whenever ++ // AT_HWCAP advertises V. Build it only for `g` (which nothing outside this file queries), and have ++ // `gv` consult the RVV 1.0 probe first so that it never gets that far on this target. + switch (cpu_isa) { +- case g: ++ case g: { ++ const auto& cpu = CPU::getInstance(); + return cpu.hasExtension(RISCVExtension::I) && cpu.hasExtension(RISCVExtension::M) && + cpu.hasExtension(RISCVExtension::A) && cpu.hasExtension(RISCVExtension::F) && + cpu.hasExtension(RISCVExtension::D); ++ } + // cpu.hasExtension(RISCVExtension::V) checks only RVV support on the device. + // To figure out RVV version, we try to execute code with RVV1.0 instructions. + // If there is no `SEGILL`, the device supports RVV1.0. + // Otherwise we consider that there is no RVV support + // [TODO] If needed, support other RVV versions + case gv: +- return mayiuse(g) && cpu.hasExtension(RISCVExtension::V) && can_compile_rvv100(); ++ return can_compile_rvv100() && mayiuse(g) && CPU::getInstance().hasExtension(RISCVExtension::V); + case gv_zvfh: + return mayiuse(gv) && can_compile_zvfh(); + case isa_all: +-- +2.43.0 +