Skip to content

Commit 933b476

Browse files
authored
Add RISC-V smoke test on QEMU (pytorch#19399)
### Summary Implements Phase 1 of the [RISC-V Support RFC](pytorch#18991): cross-compile `executor_runner` for `riscv64-linux-gnu`, run a small BundledProgram under `qemu-user-static` on a x86_64 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` [1]). The `riscv64-linux` preset mirrors `arm-ethosu-linux`. The only deviation is glibc instead of musl to avoid the `MUSL_TOOLCHAIN_ROOT` tarball setup. The reusable `_test_riscv.yml` workflow is is triggered via `pull.yml` on every pull request. [1]: https://github.com/pytorch/executorch/blob/3185f029b7b4668ee3aef8781688158653d33ff9/examples/portable/executor_runner/executor_runner.cpp#L646 ### Test plan Ran locally, and integration to CI for automated testing. cc @GregoryComer @digantdesai @cbilgin @psiddh @AdrianLundell @rascani @mergennachin
1 parent 371cb1c commit 933b476

11 files changed

Lines changed: 399 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: linux.2xlarge
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/riscv64.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: RISC-V
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- release/*
8+
tags:
9+
- ciflow/trunk/*
10+
pull_request:
11+
paths:
12+
- .github/workflows/riscv64.yml
13+
- .ci/scripts/test_riscv_qemu.sh
14+
- tools/cmake/preset/riscv64_linux.cmake
15+
- examples/riscv/**
16+
workflow_dispatch:
17+
schedule:
18+
- cron: '0 10 * * *' # Runs daily at 2 AM PST
19+
20+
concurrency:
21+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.event_name == 'workflow_dispatch' }}-${{ github.event_name == 'schedule' }}
22+
cancel-in-progress: true
23+
24+
jobs:
25+
test-riscv:
26+
name: test-riscv
27+
uses: ./.github/workflows/_test_riscv.yml
28+
permissions:
29+
id-token: write
30+
contents: read

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ __pycache__/
99

1010
# Build and tool-generated files
1111
arm_test/
12+
riscv_test/
1213
buck-out/
1314
buck2-bin/
1415
build/

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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 MethodTestCase, MethodTestSuite
19+
from executorch.devtools.bundled_program.serialize import (
20+
serialize_from_bundled_program_to_flatbuffer,
21+
)
22+
from executorch.exir import to_edge_transform_and_lower
23+
from torch.export import export
24+
25+
26+
class AddModule(torch.nn.Module):
27+
def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
28+
return x + y
29+
30+
31+
def main() -> None:
32+
parser = argparse.ArgumentParser(description=__doc__)
33+
parser.add_argument(
34+
"--output",
35+
type=Path,
36+
default=Path("add_riscv.bpte"),
37+
help="Output .bpte path",
38+
)
39+
args = parser.parse_args()
40+
41+
model = AddModule().eval()
42+
example_inputs = (torch.ones(1, 4), torch.full((1, 4), 2.0))
43+
44+
exported = export(model, example_inputs)
45+
et_program = to_edge_transform_and_lower(exported).to_executorch()
46+
47+
test_inputs = [
48+
(torch.ones(1, 4), torch.full((1, 4), 2.0)),
49+
(torch.full((1, 4), 3.0), torch.full((1, 4), 4.0)),
50+
]
51+
test_suite = MethodTestSuite(
52+
method_name="forward",
53+
test_cases=[
54+
MethodTestCase(inputs=inp, expected_outputs=(model(*inp),))
55+
for inp in test_inputs
56+
],
57+
)
58+
59+
bundled = BundledProgram(et_program, [test_suite])
60+
serialized = serialize_from_bundled_program_to_flatbuffer(bundled)
61+
62+
args.output.parent.mkdir(parents=True, exist_ok=True)
63+
args.output.write_bytes(serialized)
64+
print(f"Wrote {args.output} ({len(serialized)} bytes)")
65+
66+
67+
if __name__ == "__main__":
68+
main()
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
set(CMAKE_SYSTEM_NAME Linux)
12+
set(CMAKE_SYSTEM_PROCESSOR riscv64)
13+
14+
set(CMAKE_C_COMPILER
15+
"riscv64-linux-gnu-gcc"
16+
CACHE FILEPATH "RISC-V cross C compiler"
17+
)
18+
set(CMAKE_CXX_COMPILER
19+
"riscv64-linux-gnu-g++"
20+
CACHE FILEPATH "RISC-V cross C++ compiler"
21+
)
22+
set(CMAKE_AR
23+
"riscv64-linux-gnu-ar"
24+
CACHE FILEPATH "RISC-V archiver"
25+
)
26+
set(CMAKE_RANLIB
27+
"riscv64-linux-gnu-ranlib"
28+
CACHE FILEPATH "RISC-V ranlib"
29+
)
30+
set(CMAKE_STRIP
31+
"riscv64-linux-gnu-strip"
32+
CACHE FILEPATH "RISC-V strip"
33+
)
34+
35+
# Sysroot installed by the apt package gcc-riscv64-linux-gnu.
36+
set(CMAKE_FIND_ROOT_PATH "/usr/riscv64-linux-gnu")
37+
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
38+
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
39+
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
40+
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

examples/riscv/run.sh

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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-riscv"
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] ERROR: ${runner} does not look like a RISC-V ELF"
68+
file "${runner}"
69+
exit 1
70+
fi
71+
72+
if ${build_only}; then
73+
echo "[run.sh] --build_only set, skipping QEMU invocation"
74+
exit 0
75+
fi
76+
77+
echo "[run.sh] Step 3/3: run under ${qemu}"
78+
hash "${qemu}" 2>/dev/null || {
79+
echo "[run.sh] ERROR: ${qemu} not found on PATH; install with examples/riscv/setup.sh" >&2
80+
exit 1
81+
}
82+
83+
# QEMU_LD_PREFIX points qemu-user at the riscv64 sysroot so the dynamic
84+
# linker (ld-linux-riscv64-lp64d.so.1) referenced in the ELF resolves.
85+
export QEMU_LD_PREFIX="${QEMU_LD_PREFIX:-/usr/riscv64-linux-gnu}"
86+
87+
log_file=$(mktemp)
88+
trap 'rm -f "${log_file}"' EXIT
89+
90+
set +e
91+
timeout --signal=KILL "${qemu_timeout}" "${qemu}" "${runner}" \
92+
--model_path="${bpte_path}" \
93+
2>&1 | tee "${log_file}"
94+
qemu_status=${PIPESTATUS[0]}
95+
set -e
96+
97+
echo "[run.sh] qemu exit status: ${qemu_status}"
98+
99+
if grep -q "Test_result: PASS" "${log_file}"; then
100+
echo "[run.sh] Bundled I/O check PASSED"
101+
exit 0
102+
elif grep -q "Test_result: FAIL" "${log_file}"; then
103+
echo "[run.sh] ERROR: Bundled I/O check FAILED"
104+
exit 1
105+
else
106+
echo "[run.sh] ERROR: No Test_result line found in QEMU output"
107+
exit 1
108+
fi

0 commit comments

Comments
 (0)