Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 256 additions & 0 deletions .github/workflows/build-openvino.yml
Original file line number Diff line number Diff line change
@@ -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 <build|test>" >&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
5 changes: 5 additions & 0 deletions docs/packages/openvino.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package-name: openvino
source-code: https://github.com/openvinotoolkit/openvino
license: Apache-2.0
versions:
- version: 2026.3.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
From 9c0ff41dfe1be9aaeb6080e01fed7d4fb38777da Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
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<RVVGenerator>();
- return status;
+ // Deliberately never probe: can_execute_generated_code<RVVGenerator>() 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<ZvfhGenerator>();
- 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

Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
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

Loading