Skip to content

Commit 850d851

Browse files
committed
Add RISC-V Phase 1 smoke test on QEMU
Implements Phase 1 of the RISC-V Support RFC (#18991): cross-compile executor_runner for riscv64-linux-gnu, run a small BundledProgram under qemu-user-static on a stock x86_64 GitHub runner, and assert the standard "Test_result: PASS" marker that the portable executor_runner already emits via the bundled-IO comparison path (examples/portable/executor_runner/executor_runner.cpp:646). The riscv64-linux preset mirrors arm-ethosu-linux: same Linux cross-compile shape, same standard executor_runner, same filesystem .pte, same option set with EXECUTORCH_ENABLE_BUNDLE_IO added so the runner self-checks. Single deviation is glibc-via-apt instead of musl to avoid the MUSL_TOOLCHAIN_ROOT tarball setup. The reusable _test_riscv.yml workflow is wired into pull.yml on every PR.
1 parent ada8e35 commit 850d851

10 files changed

Lines changed: 378 additions & 0 deletions

File tree

.ci/scripts/test_riscv_qemu.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
# Copyright 2026 The ExecuTorch Authors.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# CI wrapper: install RISC-V cross-compile + qemu-user tooling, then run the
8+
# RISC-V Phase 1 smoke test (export, cross-compile, qemu-user execution) via
9+
# examples/riscv/run.sh. The bundled-IO comparison and Test_result: PASS
10+
# check are done by run.sh.
11+
12+
set -eu
13+
14+
script_dir=$(realpath "$(dirname "${BASH_SOURCE[0]}")")
15+
et_root_dir=$(realpath "${script_dir}/../..")
16+
17+
bash "${et_root_dir}/examples/riscv/setup.sh"
18+
bash "${et_root_dir}/examples/riscv/run.sh"

.github/workflows/_test_riscv.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Test RISC-V QEMU smoke
2+
3+
permissions:
4+
id-token: write
5+
contents: read
6+
7+
on:
8+
workflow_call:
9+
inputs:
10+
timeout:
11+
description: 'Per-job timeout in minutes'
12+
required: false
13+
type: number
14+
default: 30
15+
16+
jobs:
17+
run:
18+
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
19+
with:
20+
runner: ubuntu-latest
21+
docker-image: ci-image:executorch-ubuntu-22.04-gcc11
22+
submodules: 'recursive'
23+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
24+
timeout: ${{ inputs.timeout }}
25+
script: |
26+
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
27+
conda activate "${CONDA_ENV}"
28+
29+
source .ci/scripts/utils.sh
30+
install_executorch "--use-pt-pinned-commit"
31+
32+
bash .ci/scripts/test_riscv_qemu.sh

.github/workflows/pull.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,13 @@ jobs:
637637
# To run cortex_m tests
638638
pytest --config-file=backends/arm/test/pytest.ini backends/cortex_m/test
639639
640+
test-riscv:
641+
name: test-riscv
642+
uses: ./.github/workflows/_test_riscv.yml
643+
permissions:
644+
id-token: write
645+
contents: read
646+
640647
android:
641648
uses: ./.github/workflows/_android.yml
642649
permissions:

CMakePresets.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,20 @@
313313
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/examples/arm/ethos-u-setup/aarch64-linux-musl-toolchain.cmake"
314314
}
315315
},
316+
{
317+
"name": "riscv64-linux",
318+
"displayName": "Build ExecuTorch for riscv64 Linux (cross-compile)",
319+
"inherits": ["common"],
320+
"cacheVariables": {
321+
"EXECUTORCH_BUILD_PRESET_FILE": "${sourceDir}/tools/cmake/preset/riscv64_linux.cmake",
322+
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/examples/riscv/riscv64-linux-gnu-toolchain.cmake"
323+
},
324+
"condition": {
325+
"lhs": "${hostSystemName}",
326+
"type": "equals",
327+
"rhs": "Linux"
328+
}
329+
},
316330
{
317331
"name": "mlx",
318332
"displayName": "Build MLX delegate",

examples/riscv/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# RISC-V
2+
3+
Cross-compile `executor_runner` for `riscv64-linux-gnu` and run it under
4+
`qemu-user-static` against a small bundled program. The end-to-end check
5+
mirrors the Arm Cortex-M e2e flow: a `Test_result: PASS` line in stdout from
6+
the bundled-IO comparison path is the pass criterion.
7+
8+
This is the Phase 1 deliverable for the RISC-V Support RFC at
9+
[pytorch/executorch#18991][rfc]. The cross-compile and runner artifacts
10+
(toolchain file, preset, AOT script) are designed to carry over unchanged
11+
to a hardware-runner job once one becomes available; only the invocation
12+
step (qemu-user vs. native) would change.
13+
14+
[rfc]: https://github.com/pytorch/executorch/issues/18991
15+
16+
## Quick start (Ubuntu / Debian)
17+
18+
```bash
19+
examples/riscv/setup.sh # apt: gcc-riscv64-linux-gnu, qemu-user-static
20+
examples/riscv/run.sh # export, cross-compile, run under qemu-user
21+
```
22+
23+
The driver does three steps:
24+
25+
1. `python examples/riscv/aot_riscv.py` exports a `torch.add` module to
26+
`riscv_test/add_riscv.bpte` (a BundledProgram with reference outputs
27+
embedded for two test cases).
28+
2. `cmake --preset riscv64-linux` configures the cross-build using
29+
`examples/riscv/riscv64-linux-gnu-toolchain.cmake` and
30+
`tools/cmake/preset/riscv64_linux.cmake`. `executor_runner` is built
31+
against portable kernels with `ET_BUNDLE_IO_ENABLED` defined.
32+
3. `qemu-riscv64-static` invokes the runner with `--model_path` pointing at
33+
the `.bpte`. The runner detects the bundle, runs every embedded test case,
34+
and emits `Test_result: PASS` (or `FAIL`) per case.
35+
36+
## CI
37+
38+
`.github/workflows/_test_riscv_qemu.yml` is a reusable `workflow_call`
39+
job (mirroring `_test_cortex_m_e2e.yml`) invoked from `pull.yml` to run on
40+
every PR. It runs on the standard `linux.2xlarge` x86_64 runner using the
41+
`executorch-ubuntu-22.04-gcc11` docker image.

examples/riscv/aot_riscv.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright 2026 The ExecuTorch Authors.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
"""AOT export for the RISC-V Phase 1.0 smoke test.
7+
8+
Exports a trivial ``torch.add`` module to a BundledProgram (.bpte) that the
9+
portable executor_runner can load on a riscv64 target and verify against the
10+
embedded reference output, emitting ``Test_result: PASS`` on success.
11+
"""
12+
13+
import argparse
14+
from pathlib import Path
15+
16+
import torch
17+
from executorch.devtools import BundledProgram
18+
from executorch.devtools.bundled_program.config import (
19+
MethodTestCase,
20+
MethodTestSuite,
21+
)
22+
from executorch.devtools.bundled_program.serialize import (
23+
serialize_from_bundled_program_to_flatbuffer,
24+
)
25+
from executorch.exir import to_edge_transform_and_lower
26+
from torch.export import export
27+
28+
29+
class AddModule(torch.nn.Module):
30+
def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
31+
return x + y
32+
33+
34+
def main() -> None:
35+
parser = argparse.ArgumentParser(description=__doc__)
36+
parser.add_argument(
37+
"--output",
38+
type=Path,
39+
default=Path("add_riscv.bpte"),
40+
help="Output .bpte path",
41+
)
42+
args = parser.parse_args()
43+
44+
model = AddModule().eval()
45+
example_inputs = (torch.ones(1, 4), torch.full((1, 4), 2.0))
46+
47+
exported = export(model, example_inputs)
48+
et_program = to_edge_transform_and_lower(exported).to_executorch()
49+
50+
test_inputs = [
51+
(torch.ones(1, 4), torch.full((1, 4), 2.0)),
52+
(torch.full((1, 4), 3.0), torch.full((1, 4), 4.0)),
53+
]
54+
test_suite = MethodTestSuite(
55+
method_name="forward",
56+
test_cases=[
57+
MethodTestCase(inputs=inp, expected_outputs=(model(*inp),))
58+
for inp in test_inputs
59+
],
60+
)
61+
62+
bundled = BundledProgram(et_program, [test_suite])
63+
serialized = serialize_from_bundled_program_to_flatbuffer(bundled)
64+
65+
args.output.parent.mkdir(parents=True, exist_ok=True)
66+
args.output.write_bytes(serialized)
67+
print(f"Wrote {args.output} ({len(serialized)} bytes)")
68+
69+
70+
if __name__ == "__main__":
71+
main()
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright 2026 The ExecuTorch Authors.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
# CMake toolchain file for cross-compiling to riscv64 Linux glibc using the
7+
# Ubuntu / Debian gcc-riscv64-linux-gnu and g++-riscv64-linux-gnu packages.
8+
# Resulting binaries can be executed under qemu-user-static (qemu-riscv64) or
9+
# directly on a riscv64 Linux host.
10+
11+
if(CMAKE_VERSION VERSION_LESS 3.20)
12+
message(FATAL_ERROR "This toolchain file requires at least CMake 3.20")
13+
endif()
14+
15+
set(CMAKE_SYSTEM_NAME Linux)
16+
set(CMAKE_SYSTEM_PROCESSOR riscv64)
17+
18+
set(_RISCV_TRIPLE "riscv64-linux-gnu")
19+
20+
set(CMAKE_C_COMPILER
21+
"${_RISCV_TRIPLE}-gcc"
22+
CACHE FILEPATH "RISC-V cross C compiler"
23+
)
24+
set(CMAKE_CXX_COMPILER
25+
"${_RISCV_TRIPLE}-g++"
26+
CACHE FILEPATH "RISC-V cross C++ compiler"
27+
)
28+
set(CMAKE_AR
29+
"${_RISCV_TRIPLE}-ar"
30+
CACHE FILEPATH "RISC-V archiver"
31+
)
32+
set(CMAKE_RANLIB
33+
"${_RISCV_TRIPLE}-ranlib"
34+
CACHE FILEPATH "RISC-V ranlib"
35+
)
36+
set(CMAKE_STRIP
37+
"${_RISCV_TRIPLE}-strip"
38+
CACHE FILEPATH "RISC-V strip"
39+
)
40+
41+
# Sysroot installed by the apt package gcc-riscv64-linux-gnu.
42+
set(CMAKE_SYSROOT "/usr/${_RISCV_TRIPLE}")
43+
set(CMAKE_FIND_ROOT_PATH "${CMAKE_SYSROOT}")
44+
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
45+
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
46+
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
47+
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

examples/riscv/run.sh

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env bash
2+
# Copyright 2026 The ExecuTorch Authors.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# RISC-V Phase 1 smoke test driver (pytorch/executorch#18991):
8+
# 1. Export a tiny model to a BundledProgram (.bpte) on the x86_64 host.
9+
# 2. Cross-compile executor_runner for riscv64 Linux glibc.
10+
# 3. Invoke the runner under qemu-user-static and grep its stdout for the
11+
# Test_result: PASS marker emitted by the bundled-IO comparison path.
12+
13+
set -eu
14+
15+
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
16+
et_root_dir=$(realpath "${script_dir}/../..")
17+
18+
build_only=false
19+
build_dir="${et_root_dir}/cmake-out"
20+
output_dir="${et_root_dir}/riscv_test"
21+
qemu="qemu-riscv64-static"
22+
qemu_timeout="600"
23+
24+
usage() {
25+
cat <<EOF
26+
Usage: $(basename "$0") [options]
27+
Options:
28+
--build_only Only export and cross-compile; do not invoke QEMU
29+
--build_dir=<DIR> CMake build directory (default: ${build_dir})
30+
--output_dir=<DIR> Directory for the exported .bpte (default: ${output_dir})
31+
--qemu=<BIN> qemu-user binary (default: ${qemu})
32+
--timeout=<SECONDS> Maximum QEMU runtime; matches run_fvp.sh --timelimit (default: ${qemu_timeout})
33+
-h, --help Show this help
34+
EOF
35+
}
36+
37+
for arg in "$@"; do
38+
case $arg in
39+
--build_only) build_only=true ;;
40+
--build_dir=*) build_dir="${arg#*=}" ;;
41+
--output_dir=*) output_dir="${arg#*=}" ;;
42+
--qemu=*) qemu="${arg#*=}" ;;
43+
--timeout=*) qemu_timeout="${arg#*=}" ;;
44+
-h|--help) usage; exit 0 ;;
45+
*) echo "Unknown option: $arg" >&2; usage; exit 1 ;;
46+
esac
47+
done
48+
49+
mkdir -p "${output_dir}"
50+
bpte_path="${output_dir}/add_riscv.bpte"
51+
52+
echo "[run.sh] Step 1/3: AOT export on host"
53+
python "${script_dir}/aot_riscv.py" --output "${bpte_path}"
54+
55+
echo "[run.sh] Step 2/3: cross-compile executor_runner for riscv64-linux"
56+
cmake -S "${et_root_dir}" -B "${build_dir}" \
57+
--preset riscv64-linux \
58+
-DCMAKE_BUILD_TYPE=Release
59+
cmake --build "${build_dir}" -j"$(nproc)" --target executor_runner
60+
61+
runner="${build_dir}/executor_runner"
62+
[[ -x "${runner}" ]] || { echo "[run.sh] runner not found at ${runner}" >&2; exit 1; }
63+
64+
if file "${runner}" | grep -q "RISC-V"; then
65+
echo "[run.sh] runner is a RISC-V ELF: $(file -b "${runner}")"
66+
else
67+
echo "[run.sh] WARNING: ${runner} does not look like a RISC-V ELF"
68+
file "${runner}"
69+
fi
70+
71+
if ${build_only}; then
72+
echo "[run.sh] --build_only set, skipping QEMU invocation"
73+
exit 0
74+
fi
75+
76+
echo "[run.sh] Step 3/3: run under ${qemu}"
77+
hash "${qemu}" 2>/dev/null || {
78+
echo "[run.sh] ${qemu} not found on PATH; install with examples/riscv/setup.sh" >&2
79+
exit 1
80+
}
81+
82+
# QEMU_LD_PREFIX points qemu-user at the riscv64 sysroot so the dynamic
83+
# linker (ld-linux-riscv64-lp64d.so.1) referenced in the ELF resolves.
84+
export QEMU_LD_PREFIX="${QEMU_LD_PREFIX:-/usr/riscv64-linux-gnu}"
85+
86+
log_file=$(mktemp)
87+
trap 'rm -f "${log_file}"' EXIT
88+
89+
set +e
90+
timeout --signal=KILL "${qemu_timeout}" "${qemu}" "${runner}" \
91+
--model_path="${bpte_path}" \
92+
2>&1 | tee "${log_file}"
93+
qemu_status=${PIPESTATUS[0]}
94+
set -e
95+
96+
echo "[run.sh] qemu exit status: ${qemu_status}"
97+
98+
if grep -q "Test_result: PASS" "${log_file}"; then
99+
echo "[run.sh] Bundled I/O check PASSED"
100+
exit 0
101+
elif grep -q "Test_result: FAIL" "${log_file}"; then
102+
echo "[run.sh] Bundled I/O check FAILED"
103+
exit 1
104+
else
105+
echo "[run.sh] No Test_result line found in QEMU output"
106+
exit 1
107+
fi

examples/riscv/setup.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env bash
2+
# Copyright 2026 The ExecuTorch Authors.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# Install host tooling needed for the RISC-V Phase 1.0 smoke test:
8+
# - gcc/g++/binutils for riscv64-linux-gnu (cross-compiler + sysroot)
9+
# - qemu-user-static (qemu-riscv64 user-mode emulator)
10+
11+
set -eu
12+
13+
if ! command -v apt-get >/dev/null 2>&1; then
14+
echo "[$(basename "$0")] this setup script targets Debian/Ubuntu (apt-get not found)" >&2
15+
exit 1
16+
fi
17+
18+
SUDO=""
19+
if [[ $EUID -ne 0 ]]; then
20+
SUDO="sudo"
21+
fi
22+
23+
${SUDO} apt-get update
24+
${SUDO} apt-get install -y --no-install-recommends \
25+
gcc-riscv64-linux-gnu \
26+
g++-riscv64-linux-gnu \
27+
binutils-riscv64-linux-gnu \
28+
qemu-user-static
29+
30+
riscv64-linux-gnu-gcc --version | head -n1
31+
qemu-riscv64-static --version | head -n1
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright 2026 The ExecuTorch Authors.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
set_overridable_option(EXECUTORCH_BUILD_EXECUTOR_RUNNER ON)
7+
set_overridable_option(EXECUTORCH_BUILD_EXTENSION_EVALUE_UTIL ON)
8+
set_overridable_option(EXECUTORCH_BUILD_EXTENSION_RUNNER_UTIL ON)
9+
set_overridable_option(EXECUTORCH_BUILD_DEVTOOLS ON)
10+
set_overridable_option(EXECUTORCH_ENABLE_BUNDLE_IO ON)

0 commit comments

Comments
 (0)