Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .ci/docker/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ case "${IMAGE_NAME}" in
CUDA_VERSION=12.8
SKIP_PYTORCH=yes
;;
executorch-ubuntu-24.04-gcc14)
LINTRUNNER=""
OS_VERSION=24.04
GCC_VERSION=14
;;
*)
echo "Invalid image name ${IMAGE_NAME}"
exit 1
Expand Down
18 changes: 18 additions & 0 deletions .ci/scripts/test_riscv_qemu.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Copyright 2026 The ExecuTorch Authors.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

# CI wrapper: install RISC-V cross-compile + qemu-user tooling, then run the
# RISC-V Phase 1 smoke test (export, cross-compile, qemu-user execution) via
# examples/riscv/run.sh. The bundled-IO comparison and Test_result: PASS
# check are done by run.sh.

set -eu

script_dir=$(realpath "$(dirname "${BASH_SOURCE[0]}")")
et_root_dir=$(realpath "${script_dir}/../..")

bash "${et_root_dir}/examples/riscv/setup.sh"
bash "${et_root_dir}/examples/riscv/run.sh"
32 changes: 32 additions & 0 deletions .github/workflows/_test_riscv_qemu.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Test RISC-V QEMU smoke

permissions:
id-token: write
contents: read

on:
workflow_call:
inputs:
timeout:
description: 'Per-job timeout in minutes'
required: false
type: number
default: 30

jobs:
run:
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
with:
runner: linux.2xlarge
docker-image: ci-image:executorch-ubuntu-24.04-gcc14
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: ${{ inputs.timeout }}
script: |
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
conda activate "${CONDA_ENV}"

source .ci/scripts/utils.sh
install_executorch "--use-pt-pinned-commit"

bash .ci/scripts/test_riscv_qemu.sh
3 changes: 2 additions & 1 deletion .github/workflows/docker-builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ jobs:
executorch-ubuntu-22.04-zephyr-sdk,
executorch-ubuntu-22.04-qnn-sdk,
executorch-ubuntu-22.04-mediatek-sdk,
executorch-ubuntu-22.04-clang12-android
executorch-ubuntu-22.04-clang12-android,
executorch-ubuntu-24.04-gcc14
]
include:
- docker-image-name: executorch-ubuntu-22.04-gcc11-aarch64
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/pull.yml
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,13 @@ jobs:
exit 1
fi
test-riscv-qemu:
name: test-riscv-qemu
permissions:
id-token: write
contents: read
uses: ./.github/workflows/_test_riscv_qemu.yml

test-mcu-cortex-m-backend:
name: test-mcu-cortex-m-backend
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
Expand Down
14 changes: 14 additions & 0 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,20 @@
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/examples/arm/ethos-u-setup/aarch64-linux-musl-toolchain.cmake"
}
},
{
"name": "riscv64-linux",
"displayName": "Build ExecuTorch for riscv64 Linux (cross-compile)",
"inherits": ["common"],
"cacheVariables": {
"EXECUTORCH_BUILD_PRESET_FILE": "${sourceDir}/tools/cmake/preset/riscv64_linux.cmake",
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/examples/riscv/riscv64-linux-gnu-toolchain.cmake"
},
"condition": {
"lhs": "${hostSystemName}",
"type": "equals",
"rhs": "Linux"
}
},
{
"name": "mlx",
"displayName": "Build MLX delegate",
Expand Down
41 changes: 41 additions & 0 deletions examples/riscv/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# RISC-V Phase 1 smoke test (QEMU)

Cross-compile `executor_runner` for `riscv64-linux-gnu` and run it under
`qemu-user-static` against a small bundled program. The end-to-end check
mirrors the Arm Cortex-M e2e flow: a `Test_result: PASS` line in stdout from
the bundled-IO comparison path is the pass criterion.

This is the Phase 1 deliverable for the RISC-V Support RFC at
[pytorch/executorch#18991][rfc]. The cross-compile and runner artifacts
(toolchain file, preset, AOT script) are designed to carry over unchanged
to a hardware-runner job once one becomes available; only the invocation
step (qemu-user vs. native) would change.

[rfc]: https://github.com/pytorch/executorch/issues/18991

## Quick start (Ubuntu / Debian)

```bash
examples/riscv/setup.sh # apt: gcc-riscv64-linux-gnu, qemu-user-static
examples/riscv/run.sh # export, cross-compile, run under qemu-user
```

The driver does three steps:

1. `python examples/riscv/aot_riscv.py` exports a `torch.add` module to
`riscv_test/add_riscv.bpte` (a BundledProgram with reference outputs
embedded for two test cases).
2. `cmake --preset riscv64-linux` configures the cross-build using
`examples/riscv/riscv64-linux-gnu-toolchain.cmake` and
`tools/cmake/preset/riscv64_linux.cmake`. `executor_runner` is built
against portable kernels with `ET_BUNDLE_IO_ENABLED` defined.
3. `qemu-riscv64-static` invokes the runner with `--model_path` pointing at
the `.bpte`. The runner detects the bundle, runs every embedded test case,
and emits `Test_result: PASS` (or `FAIL`) per case.

## CI

`.github/workflows/_test_riscv_qemu.yml` is a reusable `workflow_call`
job (mirroring `_test_cortex_m_e2e.yml`) invoked from `pull.yml` to run on
every PR. It runs on the standard `linux.2xlarge` x86_64 runner using the
`executorch-ubuntu-24.04-gcc14` docker image.
71 changes: 71 additions & 0 deletions examples/riscv/aot_riscv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright 2026 The ExecuTorch Authors.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""AOT export for the RISC-V Phase 1.0 smoke test.

Exports a trivial ``torch.add`` module to a BundledProgram (.bpte) that the
portable executor_runner can load on a riscv64 target and verify against the
embedded reference output, emitting ``Test_result: PASS`` on success.
"""

import argparse
from pathlib import Path

import torch
from executorch.devtools import BundledProgram
from executorch.devtools.bundled_program.config import (
MethodTestCase,
MethodTestSuite,
)
from executorch.devtools.bundled_program.serialize import (
serialize_from_bundled_program_to_flatbuffer,
)
from executorch.exir import to_edge_transform_and_lower
from torch.export import export


class AddModule(torch.nn.Module):
def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
return x + y


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--output",
type=Path,
default=Path("add_riscv.bpte"),
help="Output .bpte path",
)
args = parser.parse_args()

model = AddModule().eval()
example_inputs = (torch.ones(1, 4), torch.full((1, 4), 2.0))

exported = export(model, example_inputs)
et_program = to_edge_transform_and_lower(exported).to_executorch()

test_inputs = [
(torch.ones(1, 4), torch.full((1, 4), 2.0)),
(torch.full((1, 4), 3.0), torch.full((1, 4), 4.0)),
]
test_suite = MethodTestSuite(
method_name="forward",
test_cases=[
MethodTestCase(inputs=inp, expected_outputs=(model(*inp),))
for inp in test_inputs
],
)

bundled = BundledProgram(et_program, [test_suite])
serialized = serialize_from_bundled_program_to_flatbuffer(bundled)

args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_bytes(serialized)
print(f"Wrote {args.output} ({len(serialized)} bytes)")


if __name__ == "__main__":
main()
47 changes: 47 additions & 0 deletions examples/riscv/riscv64-linux-gnu-toolchain.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright 2026 The ExecuTorch Authors.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

# CMake toolchain file for cross-compiling to riscv64 Linux glibc using the
# Ubuntu / Debian gcc-riscv64-linux-gnu and g++-riscv64-linux-gnu packages.
# Resulting binaries can be executed under qemu-user-static (qemu-riscv64) or
# directly on a riscv64 Linux host.

if(CMAKE_VERSION VERSION_LESS 3.20)
message(FATAL_ERROR "This toolchain file requires at least CMake 3.20")
endif()

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR riscv64)

set(_RISCV_TRIPLE "riscv64-linux-gnu")

set(CMAKE_C_COMPILER
"${_RISCV_TRIPLE}-gcc"
CACHE FILEPATH "RISC-V cross C compiler"
)
set(CMAKE_CXX_COMPILER
"${_RISCV_TRIPLE}-g++"
CACHE FILEPATH "RISC-V cross C++ compiler"
)
set(CMAKE_AR
"${_RISCV_TRIPLE}-ar"
CACHE FILEPATH "RISC-V archiver"
)
set(CMAKE_RANLIB
"${_RISCV_TRIPLE}-ranlib"
CACHE FILEPATH "RISC-V ranlib"
)
set(CMAKE_STRIP
"${_RISCV_TRIPLE}-strip"
CACHE FILEPATH "RISC-V strip"
)

# Sysroot installed by the apt package gcc-riscv64-linux-gnu.
set(CMAKE_SYSROOT "/usr/${_RISCV_TRIPLE}")
set(CMAKE_FIND_ROOT_PATH "${CMAKE_SYSROOT}")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
107 changes: 107 additions & 0 deletions examples/riscv/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# Copyright 2026 The ExecuTorch Authors.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

# RISC-V Phase 1 smoke test driver (pytorch/executorch#18991):
# 1. Export a tiny model to a BundledProgram (.bpte) on the x86_64 host.
# 2. Cross-compile executor_runner for riscv64 Linux glibc.
# 3. Invoke the runner under qemu-user-static and grep its stdout for the
# Test_result: PASS marker emitted by the bundled-IO comparison path.

set -eu

script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
et_root_dir=$(realpath "${script_dir}/../..")

build_only=false
build_dir="${et_root_dir}/cmake-out"
output_dir="${et_root_dir}/riscv_test"
qemu="qemu-riscv64-static"
qemu_timeout="600"

usage() {
cat <<EOF
Usage: $(basename "$0") [options]
Options:
--build_only Only export and cross-compile; do not invoke QEMU
--build_dir=<DIR> CMake build directory (default: ${build_dir})
--output_dir=<DIR> Directory for the exported .bpte (default: ${output_dir})
--qemu=<BIN> qemu-user binary (default: ${qemu})
--timeout=<SECONDS> Maximum QEMU runtime; matches run_fvp.sh --timelimit (default: ${qemu_timeout})
-h, --help Show this help
EOF
}

for arg in "$@"; do
case $arg in
--build_only) build_only=true ;;
--build_dir=*) build_dir="${arg#*=}" ;;
--output_dir=*) output_dir="${arg#*=}" ;;
--qemu=*) qemu="${arg#*=}" ;;
--timeout=*) qemu_timeout="${arg#*=}" ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $arg" >&2; usage; exit 1 ;;
esac
done

mkdir -p "${output_dir}"
bpte_path="${output_dir}/add_riscv.bpte"

echo "[run.sh] Step 1/3: AOT export on host"
python "${script_dir}/aot_riscv.py" --output "${bpte_path}"

echo "[run.sh] Step 2/3: cross-compile executor_runner for riscv64-linux"
cmake -S "${et_root_dir}" -B "${build_dir}" \
--preset riscv64-linux \
-DCMAKE_BUILD_TYPE=Release
cmake --build "${build_dir}" -j"$(nproc)" --target executor_runner

runner="${build_dir}/executor_runner"
[[ -x "${runner}" ]] || { echo "[run.sh] runner not found at ${runner}" >&2; exit 1; }

if file "${runner}" | grep -q "RISC-V"; then
echo "[run.sh] runner is a RISC-V ELF: $(file -b "${runner}")"
else
echo "[run.sh] WARNING: ${runner} does not look like a RISC-V ELF"
file "${runner}"
fi

if ${build_only}; then
echo "[run.sh] --build_only set, skipping QEMU invocation"
exit 0
fi

echo "[run.sh] Step 3/3: run under ${qemu}"
hash "${qemu}" 2>/dev/null || {
echo "[run.sh] ${qemu} not found on PATH; install with examples/riscv/setup.sh" >&2
exit 1
}

# QEMU_LD_PREFIX points qemu-user at the riscv64 sysroot so the dynamic
# linker (ld-linux-riscv64-lp64d.so.1) referenced in the ELF resolves.
export QEMU_LD_PREFIX="${QEMU_LD_PREFIX:-/usr/riscv64-linux-gnu}"

log_file=$(mktemp)
trap 'rm -f "${log_file}"' EXIT

set +e
timeout --signal=KILL "${qemu_timeout}" "${qemu}" "${runner}" \
--model_path="${bpte_path}" \
2>&1 | tee "${log_file}"
qemu_status=${PIPESTATUS[0]}
set -e

echo "[run.sh] qemu exit status: ${qemu_status}"

if grep -q "Test_result: PASS" "${log_file}"; then
echo "[run.sh] Bundled I/O check PASSED"
exit 0
elif grep -q "Test_result: FAIL" "${log_file}"; then
echo "[run.sh] Bundled I/O check FAILED"
exit 1
else
echo "[run.sh] No Test_result line found in QEMU output"
exit 1
fi
Loading
Loading