Skip to content
Draft
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
275 changes: 275 additions & 0 deletions .github/workflows/build-executorch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# This workflow is based on:
# https://github.com/pytorch/executorch/blob/v1.4.1/.github/workflows/build-wheels-linux.yml
name: Build executorch wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'Version glob to (re)build; empty builds every version of docs/packages/executorch.yaml not released yet'
required: false
default: ''
pull_request:
branches: [main]
paths:
- '.github/workflows/build-executorch.yml'
- 'docs/packages/executorch.yaml'
- 'patches/executorch/**'
push:
branches: [main]
paths:
- '.github/workflows/build-executorch.yml'
- 'docs/packages/executorch.yaml'
- 'patches/executorch/**'

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read

env:
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64

jobs:
setup:
uses: $/.github/workflows/_setup.yml
with:
package: executorch
version: ${{ inputs.version }}

build_wheels:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
# cp310/cp311 are dropped: torch and pytorch-tokenizers (both hard runtime
# deps) have no riscv64 wheel for either on pypi.riseproject.dev. cp314t
# is dropped too: upstream itself ships no free-threaded wheel.
# TEMP: restricted to cp312 only while diagnosing the cpuinfo abort
# under gdb (gotcha 115) - restore ["cp312", "cp313", "cp314"] once done.
python: ["cp312"]
name: Build executorch ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 720

env:
EXECUTORCH_VERSION: ${{ matrix.version }}

steps:
- name: Checkout executorch v${{ env.EXECUTORCH_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: pytorch/executorch
ref: v${{ env.EXECUTORCH_VERSION }}
submodules: recursive
persist-credentials: false

- name: Checkout python-wheels
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: python-wheels
persist-credentials: false

- name: Apply patches
run: git apply python-wheels/patches/executorch/${{ env.EXECUTORCH_VERSION }}/*.patch

# Every library below is statically linked into the compiled extensions, and
# setuptools' license-files (widened by the patch above) only reaches the
# project root.
- name: Stage the vendored licences
run: |
set -euo pipefail
cp backends/xnnpack/third-party/FP16/LICENSE LICENSE.FP16
cp backends/xnnpack/third-party/FXdiv/LICENSE LICENSE.FXdiv
cp backends/xnnpack/third-party/XNNPACK/LICENSE LICENSE.XNNPACK
cp backends/xnnpack/third-party/cpuinfo/LICENSE LICENSE.cpuinfo
cp backends/xnnpack/third-party/pthreadpool/LICENSE LICENSE.pthreadpool
cp third-party/flatbuffers/LICENSE LICENSE.flatbuffers
cp third-party/flatcc/LICENSE LICENSE.flatcc
cp kernels/optimized/third-party/eigen/LICENSE LICENSE.eigen
cp third-party/pocketfft/LICENSE.md LICENSE.pocketfft
cp third-party/json/LICENSE.MIT LICENSE.json
cp third-party/pybind11/LICENSE LICENSE.pybind11

# Upstream's own smoke test (.ci/scripts/wheel/test_linux.py) exercises the
# examples/ tree, which needs torchvision for its MobileNetV3 model;
# torchvision has no riscv64 wheel. This test keeps upstream's own
# assertions (registered backend names) and replaces the model with a small
# hand-built one, run through both the portable (non-delegated) path and a
# real XNNPACK-delegated export, so the compiled extension, the portable
# kernels and the XNNPACK backend are all genuinely exercised end to end.
- name: Write the riscv64 smoke test
run: |
cat > riscv64_smoke_test.py <<'EOF'
import platform

import torch
from torch.export import export

from executorch.backends.xnnpack.partition.xnnpack_partitioner import (
XnnpackPartitioner,
)
from executorch.exir import to_edge, to_edge_transform_and_lower
from executorch.extension.pybindings.portable_lib import (
_get_registered_backend_names,
_load_for_executorch_from_buffer,
)

registered = _get_registered_backend_names()
assert "XnnpackBackend" in registered, registered
assert "OpenvinoBackend" in registered, registered
if platform.machine() in ("x86_64", "amd64"):
assert "QnnBackend" in registered, registered


class Model(torch.nn.Module):
def __init__(self):
super().__init__()
self.linear = torch.nn.Linear(4, 4)

def forward(self, x):
return self.linear(x).relu()


model = Model().eval()
example_inputs = (torch.randn(2, 4),)
expected = model(*example_inputs)

portable_prog = to_edge(export(model, example_inputs, strict=True)).to_executorch()
portable_module = _load_for_executorch_from_buffer(portable_prog.buffer)
portable_out = portable_module.forward(example_inputs)[0]
assert torch.allclose(portable_out, expected, atol=1e-4), (portable_out, expected)
print("portable (non-delegated) execution OK")

xnnpack_prog = to_edge_transform_and_lower(
export(model, example_inputs, strict=True),
partitioner=[XnnpackPartitioner()],
).to_executorch()
xnnpack_module = _load_for_executorch_from_buffer(xnnpack_prog.buffer)
xnnpack_out = xnnpack_module.forward(example_inputs)[0]
assert torch.allclose(xnnpack_out, expected, atol=1e-4), (xnnpack_out, expected)
print("XNNPACK-delegated execution OK")
EOF

- uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0
with:
only: ${{ matrix.python }}-manylinux_riscv64
env:
CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }}
# No riscv64 cmake wheel exists, so pip builds it from source as a
# build dependency; its own CMake bootstrap needs OpenSSL's dev
# headers to find libcrypto.
CIBW_BEFORE_ALL_LINUX: dnf -y install openssl-devel
# get_torch_base_path (tools/cmake/Utils.cmake) runs `import torch` in
# the same interpreter as the build backend to add it to
# CMAKE_PREFIX_PATH, but torch is only a runtime dependency
# (setup.py), not a build-system.requires entry, so PEP 517 isolation
# hides it from that interpreter. Preinstall build-system.requires
# plus torch and turn isolation off (mirrors build-torchaudio.yml /
# build-torchcodec.yml).
CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation"
# torch_pin.py pins TORCH_VERSION="2.13.0": this CMake build compiles
# against the installed torch's real headers alongside executorch's own
# vendored, 2.13.0-frozen copy of them (runtime/core/portable_type/c10),
# so an unbounded floor (setup.py's own unpinned "torch>=2.13.0a0") lets
# pip resolve a newer registry release whose headers conflict with the
# vendored ones in the same translation unit (gotcha 615).
CIBW_BEFORE_BUILD: >-
pip install "cmake>=3.24,<4.0.0" "packaging>=24.2" pyyaml
"setuptools>=77.0.3" wheel zstd certifi &&
pip install --only-binary=:all: "torch==2.13.0"
# TEMP diagnostic (gotcha 115): keep debug symbols so gdb's backtrace
# resolves symbols instead of bare addresses. setup.py's get_build_type()
# only offers Debug/Release via $DEBUG, but appends $CMAKE_ARGS after its
# own -DCMAKE_BUILD_TYPE flag, so CMAKE_ARGS wins (RelWithDebInfo matches
# this repo's own precedent in build-tesserocr.yml, and keeps optimizations
# on so the crash still reproduces the same way as the real release build).
CIBW_ENVIRONMENT: >-
CMAKE_BUILD_PARALLEL_LEVEL=8
PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/
CFLAGS=-g CXXFLAGS=-g
CMAKE_ARGS=-DCMAKE_BUILD_TYPE=RelWithDebInfo
# The torch wheel already carries these and loads them RTLD_GLOBAL (gotcha 17).
CIBW_REPAIR_WHEEL_COMMAND: >-
auditwheel repair -w {dest_dir} {wheel}
--exclude libtorch.so
--exclude libtorch_cpu.so
--exclude libtorch_python.so
--exclude libtorch_global_deps.so
--exclude libc10.so
--exclude libgomp.so.1
--exclude libgfortran.so.5
--exclude libopenblas.so.0
# Match the build-time pin (gotcha 615): the wheel's own metadata still
# says "torch>=2.13.0a0", so without this the test install would resolve
# a newer torch than the extension was actually compiled against.
CIBW_TEST_REQUIRES: torch==2.13.0
CIBW_TEST_SOURCES: riscv64_smoke_test.py
# TEMP diagnostic (gotcha 115): gdb works on the real riscv64 runner
# (not under QEMU). The abort is 100% deterministic across all 3
# python legs already observed, so a single gdb run is enough - no
# retry loop needed. Fail the step either way so the log is easy to
# spot: once with the backtrace if the abort reproduces, once with a
# clear "did not reproduce" message otherwise.
CIBW_BEFORE_TEST_LINUX: dnf -y install gdb
CIBW_TEST_COMMAND: >-
gdb -batch -ex run -ex "thread apply all bt 25" --args python riscv64_smoke_test.py
> /tmp/gdb-backtrace.log 2>&1;
gdb_status=$?;
cat /tmp/gdb-backtrace.log;
echo "gdb exit status: $gdb_status";
if grep -qE "SIGABRT|Aborted" /tmp/gdb-backtrace.log;
then echo "=== crash reproduced under gdb (backtrace above) ===";
exit 1;
else echo "=== SIGABRT/Aborted NOT found - crash did not reproduce under gdb ===";
exit 1;
fi

- name: Check the extensions and the vendored licences made it into the wheel
run: |
python3 - wheelhouse/*.whl <<'EOF'
import sys, zipfile
expected = {"LICENSE", "LICENSE.FP16", "LICENSE.FXdiv", "LICENSE.XNNPACK",
"LICENSE.cpuinfo", "LICENSE.pthreadpool", "LICENSE.flatbuffers",
"LICENSE.flatcc", "LICENSE.eigen", "LICENSE.pocketfft",
"LICENSE.json", "LICENSE.pybind11"}
for whl in sys.argv[1:]:
names = zipfile.ZipFile(whl).namelist()
assert [n for n in names
if n.startswith("executorch/extension/pybindings/_portable_lib.")
and n.endswith(".so")], whl
licenses = {n.split(".dist-info/licenses/", 1)[1] for n in names
if ".dist-info/licenses/" in n and not n.endswith("/")}
assert licenses == expected, f"{whl}: {sorted(licenses)}"
print(whl, "ok")
EOF

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: executorch-${{ env.EXECUTORCH_VERSION }}-${{ matrix.python }}-manylinux_riscv64
path: wheelhouse/*.whl
if-no-files-found: error

publish:
name: Publish executorch ${{ 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: executorch-${{ matrix.version }}-*-manylinux_riscv64
5 changes: 5 additions & 0 deletions docs/packages/executorch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package-name: executorch
source-code: https://github.com/pytorch/executorch
license: BSD-3-Clause AND MIT AND BSD-2-Clause AND Apache-2.0 AND MPL-2.0
versions:
- version: 1.4.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
From 0000000000000000000000000000000000000001 Mon Sep 17 00:00:00 2001
From: RISE Project CI <ci@riseproject.dev>
Date: Thu, 10 Jul 2026 00:00:00 +0000
Subject: [PATCH] packaging: Include vendored third-party licences in the
wheel

The compiled extensions in this wheel statically link FP16, FXdiv,
XNNPACK, cpuinfo, pthreadpool, flatbuffers, flatcc, Eigen, pocketfft,
nlohmann/json and pybind11, but `license-files` only reaches the
project's own LICENSE. RISE distributes these wheels and must also
carry the licence of every statically linked dependency, so widen the
glob to pick up the `LICENSE.<dep>` files the riscv64 build stages at
the project root before `pip wheel` runs.

Upstream-Status: Inappropriate [only relevant to a distributor bundling vendored third-party licences; reproduces identically on any architecture]

Signed-off-by: RISE Project CI <ci@riseproject.dev>
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index 0000000000..0000000000 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -100,7 +100,7 @@
# package, and package_data to restrict the set up non-python files we
# include. See also setuptools/discovery.py for custom finders.
[tool.setuptools]
-license-files = ["LICENSE"]
+license-files = ["LICENSE", "LICENSE.*"]

[tool.setuptools.package-dir]
# Tell setuptools to follow the symlink: src/executorch/* -> * for all first level
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
From 0000000000000000000000000000000000000002 Mon Sep 17 00:00:00 2001
From: RISE Project CI <ci@riseproject.dev>
Date: Thu, 10 Jul 2026 00:00:00 +0000
Subject: [PATCH] cmake: stop requiring the checkout dir be named `executorch`

CMakeLists.txt hard-fails configure unless the checkout's directory is
named exactly `executorch`, because `_common_include_directories` adds
the checkout's *parent* directory so in-tree code can say
`#include <executorch/path/to/header.h>` (upstream tracks lifting this
restriction at https://github.com/pytorch/executorch/issues/6475).

cibuildwheel always mounts/copies the source into its manylinux
container at the fixed path `/project`, so this check always trips
there, regardless of what directory this repo is checked out to on the
runner. Provide the same `<executorch/...>` include path through a
configure-time symlink instead of requiring a literally named parent
directory, so the build works unmodified under cibuildwheel.

Upstream-Status: Inappropriate [only relevant to cibuildwheel, which always mounts the source at a fixed /project path inside its build container; see pytorch/executorch#6475]

Signed-off-by: RISE Project CI <ci@riseproject.dev>
---
CMakeLists.txt | 20 +++++++++-----------
1 file changed, 9 insertions(+), 11 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index be0cec2..4668d0a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -447,22 +447,20 @@ if(NOT DEFINED CMAKE_POSITION_INDEPENDENT_CODE
list(APPEND _common_compile_options $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-fPIC>)
endif()

-# Let files say "include <executorch/path/to/header.h>".
-# TODO(#6475): This requires/assumes that the repo lives in a directory named
-# exactly `executorch`. Check the assumption first. Remove this check once we
-# stop relying on the assumption.
-cmake_path(GET CMAKE_CURRENT_SOURCE_DIR FILENAME _repo_dir_name)
-if(NOT "${_repo_dir_name}" STREQUAL "executorch")
- message(
- FATAL_ERROR
- "The ExecuTorch repo must be cloned into a directory named exactly "
- "`executorch`; found `${_repo_dir_name}`. See "
- "https://github.com/pytorch/executorch/issues/6475 for progress on a "
- "fix for this restriction."
+# Let files say "include <executorch/path/to/header.h>", regardless of what
+# the checkout directory is actually named (see
+# https://github.com/pytorch/executorch/issues/6475). Provide that include
+# path unconditionally through a configure-time symlink, rather than
+# requiring the checkout itself be named `executorch`.
+set(_executorch_include_shim "${CMAKE_BINARY_DIR}/_executorch_include")
+file(MAKE_DIRECTORY "${_executorch_include_shim}")
+if(NOT EXISTS "${_executorch_include_shim}/executorch")
+ file(CREATE_LINK "${CMAKE_CURRENT_SOURCE_DIR}"
+ "${_executorch_include_shim}/executorch" SYMBOLIC
)
endif()
set(_common_include_directories
- $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>
+ $<BUILD_INTERFACE:${_executorch_include_shim}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/runtime/core/portable_type/c10>
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
--
2.34.1
Loading
Loading