[CI] Add bare-metal RISCV 64 sample and QEMU smoke test (#24844)
Adds `samples/baremetal_riscv64`, a single-op inference runner for
bare-metal RISCV 64 under `qemu-system-riscv64` (`-machine virt`, no OS,
semihosted I/O), together with a pkgci smoke test.
The sample covers two execution paths:
- `llvm-cpu` using `inline-dynamic` and the embedded-ELF loader.
- `vmvx-inline` using `inline-static`.
The runner uses the inline HAL instead of the full-HAL implementation
following `samples/simple_embedding`, because HAL device creation
currently requires a proactor that `IREE_PLATFORM_GENERIC` does not
provide.
The runtime is compiled and linked using an xPack GCC/newlib sysroot.
Its newlib is built with `-mcmodel=medany` required for QEMUs RAM base
and provides `semihost.specs` for semihosted I/O and exit handling.
No HAL device drivers are enabled; the embedded-ELF loader is enabled
explicitly for the LLVMCPU runner. The sample is `EXCLUDE_FROM_ALL` and
CI builds only `samples/baremetal_riscv64/all`, so it does not affect
other Generic RISCV builds.
Assisted-by: Claude Code
Signed-off-by: Pooja Hemashekar <hemashekar@roofline.ai>
diff --git a/.github/workflows/pkgci_test_riscv64.yml b/.github/workflows/pkgci_test_riscv64.yml
index 1d99c8d..0583df6 100644
--- a/.github/workflows/pkgci_test_riscv64.yml
+++ b/.github/workflows/pkgci_test_riscv64.yml
@@ -92,3 +92,77 @@
env:
QEMU_BIN: ${{ env.QEMU_PATH_PREFIX }}/qemu-riscv64
run: ./build_tools/cmake/test_riscv.sh
+
+ riscv64-baremetal:
+ runs-on: ubuntu-24.04
+ env:
+ PACKAGE_DOWNLOAD_DIR: ${{ github.workspace }}/.packages
+ VENV_DIR: ${{ github.workspace }}/.venv
+ GH_TOKEN: ${{ github.token }}
+ RISCV_TOOLCHAIN_ROOT: ${{ github.workspace }}/riscv/xpack-gcc
+ # xPack riscv-none-elf gcc: newlib is built -mcmodel=medany and ships
+ # semihost.specs, as samples/baremetal_riscv64 requires.
+ XPACK_GCC_VERSION: "15.2.0-1"
+ XPACK_GCC_SHA256: "aaaa8060c914851a3e5ee1ba82cc3d6f80972f90638a05c6e823a37557a33758"
+ steps:
+ # General setup.
+ - name: "Checking out repository"
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - name: "Checking out runtime submodules"
+ run: ./build_tools/scripts/git/update_runtime_submodules.sh
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ # Must match the subset of versions built in pkgci_build_packages.
+ python-version: "3.11"
+ - name: ccache
+ uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23
+ with:
+ key: ${{ github.job }}
+ save: ${{ inputs.write-caches == 1 }}
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ if: ${{ inputs.artifact_run_id == '' }}
+ with:
+ name: linux_x86_64_release_packages
+ path: ${{ env.PACKAGE_DOWNLOAD_DIR }}
+ - name: Install build dependencies
+ run: sudo apt update && sudo apt install -y ninja-build qemu-system-misc
+ - name: Setup base venv
+ run: |
+ ./build_tools/pkgci/setup_venv.py ${VENV_DIR} \
+ --artifact-path=${PACKAGE_DOWNLOAD_DIR} \
+ --fetch-gh-workflow=${{ inputs.artifact_run_id }}
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Restore toolchain cache
+ id: restore-toolchain
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: riscv
+ key: riscv-baremetal-toolchain-xpack-${{ env.XPACK_GCC_VERSION }}-${{ runner.arch }}
+ - name: Bootstrap toolchain
+ if: ${{ steps.restore-toolchain.outputs.cache-hit != 'true' }}
+ run: |
+ TARBALL="xpack-riscv-none-elf-gcc-${XPACK_GCC_VERSION}-linux-x64.tar.gz"
+ wget -q "https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases/download/v${XPACK_GCC_VERSION}/${TARBALL}"
+ echo "${XPACK_GCC_SHA256} ${TARBALL}" | sha256sum -c -
+ mkdir -p "${RISCV_TOOLCHAIN_ROOT}"
+ tar -xzf "${TARBALL}" -C "${RISCV_TOOLCHAIN_ROOT}" --no-same-owner --strip-components=1
+ rm "${TARBALL}"
+ - name: Save toolchain cache
+ if: ${{ inputs.write-caches == 1 && steps.restore-toolchain.outputs.cache-hit != 'true' }}
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: riscv
+ key: riscv-baremetal-toolchain-xpack-${{ env.XPACK_GCC_VERSION }}-${{ runner.arch }}
+ - name: Build riscv64 bare-metal runtime
+ env:
+ IREE_HOST_BIN_DIR: ${{ env.VENV_DIR }}/bin
+ IREE_READ_REMOTE_CCACHE: 0
+ IREE_WRITE_REMOTE_CCACHE: 0
+ IREE_READ_LOCAL_CCACHE: 1
+ IREE_WRITE_LOCAL_CCACHE: ${{ inputs.write-caches }}
+ run: ./build_tools/cmake/build_riscv_baremetal.sh
+ - name: Run bare-metal smoke tests on QEMU
+ env:
+ QEMU_BIN: qemu-system-riscv64
+ run: ./samples/baremetal_riscv64/run_qemu.sh
diff --git a/build_tools/cmake/build_riscv_baremetal.sh b/build_tools/cmake/build_riscv_baremetal.sh
new file mode 100755
index 0000000..683bdf0
--- /dev/null
+++ b/build_tools/cmake/build_riscv_baremetal.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+
+# Copyright 2026 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+# Cross-compile the runtime and the samples/baremetal_riscv64 runners for
+# bare-metal RISC-V 64 using build_tools/cmake/generic_riscv64_gcc.cmake.
+#
+# Requires RISCV_TOOLCHAIN_ROOT to point to a riscv-none-elf GCC toolchain
+# whose newlib was built with -mcmodel=medany and that provides
+# semihost.specs, such as xPack riscv-none-elf-gcc.
+#
+# IREE_HOST_BIN_DIR specifies the directory containing the prebuilt IREE host
+# tools and defaults to "build/install/bin".
+#
+# Pass the desired build directory as the first argument. Otherwise,
+# IREE_TARGET_BUILD_DIR is used and defaults to "build-riscv-baremetal".
+#
+# This script is designed for CI but can also be run manually. It must be run
+# from the IREE repository root.
+
+set -xeuo pipefail
+
+BUILD_DIR="${1:-${IREE_TARGET_BUILD_DIR:-build-riscv-baremetal}}"
+CMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE:-RelWithDebInfo}"
+IREE_ENABLE_ASSERTIONS="${IREE_ENABLE_ASSERTIONS:-ON}"
+IREE_HOST_BIN_DIR="$(realpath "${IREE_HOST_BIN_DIR:-build/install/bin}")"
+
+source build_tools/cmake/setup_build.sh
+source build_tools/cmake/setup_ccache.sh
+
+declare -a args
+args=(
+ "-G" "Ninja"
+ "-B" "${BUILD_DIR}"
+
+ "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}"
+ "-DIREE_ENABLE_ASSERTIONS=${IREE_ENABLE_ASSERTIONS}"
+ "-DPython3_EXECUTABLE=${IREE_PYTHON3_EXECUTABLE}"
+
+ # Cross compiling bare-metal RISC-V.
+ "-DCMAKE_TOOLCHAIN_FILE=$(realpath build_tools/cmake/generic_riscv64_gcc.cmake)"
+ "-DRISCV_TOOLCHAIN_ROOT=${RISCV_TOOLCHAIN_ROOT}"
+ "-DIREE_BUILD_COMPILER=OFF"
+ "-DIREE_BUILD_SAMPLES=ON"
+ "-DIREE_HOST_BIN_DIR=${IREE_HOST_BIN_DIR}"
+
+ # No HAL device drivers: the sample uses the inline HAL, which runs the
+ # module without creating a HAL device. The embedded-ELF loader (used by
+ # the hal_loader module for llvm-cpu kernels) must be enabled explicitly
+ # because loaders default off when no local driver is enabled.
+ "-DIREE_HAL_DRIVER_DEFAULTS=OFF"
+ "-DIREE_HAL_EXECUTABLE_LOADER_EMBEDDED_ELF=ON"
+)
+
+"${CMAKE_BIN}" "${args[@]}"
+# Build only the bare-metal sample and the runtime targets it depends on.
+# Other sample targets may not support this GCC/newlib configuration.
+"${CMAKE_BIN}" --build "${BUILD_DIR}" --target samples/baremetal_riscv64/all -- -k 0
+
+if (( IREE_USE_CCACHE == 1 )); then
+ ccache --show-stats
+fi
diff --git a/build_tools/cmake/generic_riscv64_gcc.cmake b/build_tools/cmake/generic_riscv64_gcc.cmake
new file mode 100644
index 0000000..5e7769c
--- /dev/null
+++ b/build_tools/cmake/generic_riscv64_gcc.cmake
@@ -0,0 +1,61 @@
+# Copyright 2026 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+# Toolchain file for bare-metal riscv-none-elf GCC/newlib cross-compilation,
+# using one sysroot for both compilation and linking.
+#
+# samples/baremetal_riscv64 requires newlib to be built with -mcmodel=medany
+# because QEMU's virt machine places RAM at 0x80000000. The toolchain must also
+# provide semihost.specs for console I/O and process exit.
+
+cmake_minimum_required(VERSION 3.26)
+
+# CMake invokes the toolchain file twice during the first build, but only once
+# during subsequent rebuilds. This was causing the various flags to be added
+# twice on the first build, and on a rebuild ninja would see only one set of
+# the flags and rebuild the world.
+# https://github.com/android-ndk/ndk/issues/323
+if(RISCV_TOOLCHAIN_INCLUDED)
+ return()
+endif()
+set(RISCV_TOOLCHAIN_INCLUDED true)
+
+set(CMAKE_SYSTEM_NAME Generic)
+set(CMAKE_SYSTEM_PROCESSOR riscv64)
+
+if(NOT "${RISCV_TOOLCHAIN_ROOT}" STREQUAL "")
+ set(CMAKE_AR "${RISCV_TOOLCHAIN_ROOT}/bin/riscv-none-elf-ar")
+ set(CMAKE_C_COMPILER "${RISCV_TOOLCHAIN_ROOT}/bin/riscv-none-elf-gcc")
+ set(CMAKE_CXX_COMPILER "${RISCV_TOOLCHAIN_ROOT}/bin/riscv-none-elf-g++")
+ set(CMAKE_RANLIB "${RISCV_TOOLCHAIN_ROOT}/bin/riscv-none-elf-ranlib")
+ set(CMAKE_STRIP "${RISCV_TOOLCHAIN_ROOT}/bin/riscv-none-elf-strip")
+endif()
+
+# Bare-metal: configure-time link checks have no crt0/syscalls.
+set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
+
+set(CMAKE_C_EXTENSIONS OFF CACHE BOOL "" FORCE) # gnu17 selects a clock_nanosleep() path newlib lacks.
+set(IREE_BUILD_TESTS OFF CACHE BOOL "" FORCE)
+set(IREE_ENABLE_POSIX OFF CACHE BOOL "" FORCE)
+set(IREE_ENABLE_THREADING OFF CACHE BOOL "" FORCE)
+set(IREE_SYNCHRONIZATION_DISABLE_UNSAFE ON CACHE BOOL "" FORCE)
+set(IREE_HAL_DRIVER_LOCAL_TASK OFF CACHE BOOL "" FORCE)
+set(IREE_HAL_EXECUTABLE_LOADER_SYSTEM_LIBRARY OFF CACHE BOOL "" FORCE)
+set(IREE_HAL_EXECUTABLE_PLUGIN_SYSTEM_LIBRARY OFF CACHE BOOL "" FORCE)
+
+# The default RISC-V ISA specification treats zicsr and zifencei as
+# separate extensions, so name them explicitly.
+set(RISCV_COMPILER_FLAGS "\
+ -march=rv64imafdc_zicsr_zifencei -mabi=lp64d -mcmodel=medany \
+ -DIREE_PLATFORM_GENERIC=1 \
+ -DIREE_FILE_IO_ENABLE=0 -DIREE_TIME_NOW_FN=\"\{ return 0; \}\" -DIREE_DEVICE_SIZE_T=uint64_t -DPRIdsz=PRIu64")
+
+set(CMAKE_C_FLAGS_INIT "${RISCV_COMPILER_FLAGS}")
+set(CMAKE_CXX_FLAGS_INIT "${RISCV_COMPILER_FLAGS}")
+set(CMAKE_ASM_FLAGS_INIT "${RISCV_COMPILER_FLAGS}")
+
+# GNUInstallDirs does not set CMAKE_INSTALL_LIBDIR in this configuration.
+set(CMAKE_INSTALL_LIBDIR "lib" CACHE PATH "")
diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt
index 0dfdc27..fe5cfa2 100644
--- a/samples/CMakeLists.txt
+++ b/samples/CMakeLists.txt
@@ -12,6 +12,9 @@
# included:
# compiler_plugins
+# Not in the default build: needs a riscv-none-elf GCC toolchain with
+# semihosting support and an explicit --target samples/baremetal_riscv64/all.
+add_subdirectory(baremetal_riscv64 EXCLUDE_FROM_ALL)
add_subdirectory(custom_dispatch)
add_subdirectory(custom_module)
add_subdirectory(dynamic_shapes)
diff --git a/samples/baremetal_riscv64/CMakeLists.txt b/samples/baremetal_riscv64/CMakeLists.txt
new file mode 100644
index 0000000..9fef406
--- /dev/null
+++ b/samples/baremetal_riscv64/CMakeLists.txt
@@ -0,0 +1,110 @@
+# Copyright 2026 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+# Bare-metal riscv64 single-op inference runners for QEMU's virt machine.
+# Requires a riscv-none-elf GCC cross build (build_tools/cmake/generic_riscv64_gcc.cmake)
+# and host tools; see build_tools/cmake/build_riscv_baremetal.sh
+
+if(NOT CMAKE_SYSTEM_NAME STREQUAL "Generic" OR
+ NOT CMAKE_SYSTEM_PROCESSOR STREQUAL "riscv64" OR
+ NOT IREE_HOST_BIN_DIR)
+ return()
+endif()
+
+# The link options below rely on GCC spec files (semihost.specs).
+if(NOT CMAKE_C_COMPILER_ID STREQUAL "GNU")
+ message(STATUS
+ "Skipping samples/baremetal_riscv64: requires a riscv-none-elf GCC "
+ "toolchain with semihosting support (CMAKE_C_COMPILER_ID is "
+ "${CMAKE_C_COMPILER_ID})")
+ return()
+endif()
+
+# QEMU virt: RAM starts at 0x80000000. start.S sets the stack pointer to
+# 0x88000000 (128 MiB above the RAM base) and the stack grows downward.
+set(_BAREMETAL_QEMU_VIRT_LINKOPTS
+ "--specs=semihost.specs"
+ "-Wl,-Ttext-segment=0x80000000"
+ "-Wl,--defsym=__stack_top=0x88000000"
+ "-Wl,-e,_enter"
+)
+
+iree_bytecode_module(
+ NAME
+ simple_mul_module_llvmcpu
+ SRC
+ "simple_mul.mlir"
+ C_IDENTIFIER
+ "iree_samples_baremetal_riscv64_module_llvmcpu"
+ FLAGS
+ "--iree-execution-model=inline-dynamic"
+ "--iree-hal-target-device=local"
+ "--iree-hal-local-target-device-backends=llvm-cpu"
+ "--iree-llvmcpu-target-triple=riscv64-unknown-unknown-elf"
+ "--iree-llvmcpu-target-cpu-features=+m,+a,+f,+d,+c"
+ "--iree-llvmcpu-target-abi=lp64d"
+ "--iree-llvmcpu-debug-symbols=false"
+ "--iree-vm-bytecode-module-strip-source-map=true"
+ "--iree-vm-emit-polyglot-zip=false"
+ PUBLIC
+)
+
+iree_bytecode_module(
+ NAME
+ simple_mul_module_vmvx
+ SRC
+ "simple_mul.mlir"
+ C_IDENTIFIER
+ "iree_samples_baremetal_riscv64_module_vmvx"
+ FLAGS
+ "--iree-execution-model=inline-static"
+ "--iree-hal-target-device=local"
+ "--iree-hal-local-target-device-backends=vmvx-inline"
+ "--iree-llvmcpu-debug-symbols=false"
+ "--iree-vm-bytecode-module-strip-source-map=true"
+ "--iree-vm-emit-polyglot-zip=false"
+ PUBLIC
+)
+
+iree_cc_binary(
+ NAME
+ runner_llvmcpu
+ SRCS
+ "runner.c"
+ "start.S"
+ DEFINES
+ "USE_LLVMCPU=1"
+ DEPS
+ ::simple_mul_module_llvmcpu_c
+ iree::base
+ iree::hal
+ iree::hal::local::loaders::embedded_elf_loader
+ iree::modules::hal::inline
+ iree::modules::hal::loader
+ iree::modules::hal::types
+ iree::vm
+ iree::vm::bytecode::module
+ LINKOPTS
+ ${_BAREMETAL_QEMU_VIRT_LINKOPTS}
+)
+
+iree_cc_binary(
+ NAME
+ runner_vmvx
+ SRCS
+ "runner.c"
+ "start.S"
+ DEPS
+ ::simple_mul_module_vmvx_c
+ iree::base
+ iree::hal
+ iree::modules::hal::inline
+ iree::modules::hal::types
+ iree::vm
+ iree::vm::bytecode::module
+ LINKOPTS
+ ${_BAREMETAL_QEMU_VIRT_LINKOPTS}
+)
diff --git a/samples/baremetal_riscv64/README.md b/samples/baremetal_riscv64/README.md
new file mode 100644
index 0000000..a2433fe
--- /dev/null
+++ b/samples/baremetal_riscv64/README.md
@@ -0,0 +1,41 @@
+# Bare-metal RISC-V 64 sample
+
+Runs a single-op IREE workload (elementwise `arith.mulf` on `4xf32`) on
+bare-metal riscv64 under `qemu-system-riscv64` (`-machine virt`, no OS,
+semihosted I/O), using the inline HAL. Two runners are built:
+
+- `runner_llvmcpu`: kernels compiled to RISC-V machine code by the `llvm-cpu`
+ backend (`--iree-execution-model=inline-dynamic`), loaded at runtime with
+ the embedded-ELF loader via the `hal_loader` VM module.
+- `runner_vmvx`: kernels as portable VM bytecode (`vmvx-inline`,
+ `--iree-execution-model=inline-static`); no loader involved.
+
+The full-HAL path used by `samples/simple_embedding` is not used here because
+its synchronous device creation currently requires a proactor, which
+`IREE_PLATFORM_GENERIC` does not provide. The inline HAL runs the module
+without a HAL device, threads, or a filesystem.
+
+## Building and running
+
+Requires a `riscv-none-elf` GCC toolchain that provides `semihost.specs` and
+uses a newlib built with `-mcmodel=medany`, such as
+[xPack riscv-none-elf-gcc](https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack),
+along with prebuilt IREE host tools and `qemu-system-riscv64`. From the repository
+root:
+
+```sh
+RISCV_TOOLCHAIN_ROOT=/path/to/xpack-gcc \
+IREE_HOST_BIN_DIR=/path/to/host/tools \
+ ./build_tools/cmake/build_riscv_baremetal.sh
+
+./samples/baremetal_riscv64/run_qemu.sh
+```
+
+## Boot and memory layout
+
+QEMU's `virt` machine places RAM at `0x80000000`, requiring
+`-mcmodel=medany` throughout, including for newlib. QEMU's generic loader
+loads the ELF and sets CPU 0's PC to its entry point, but does not initialize
+a stack. `start.S` sets `sp` to `__stack_top`—128 MiB above the RAM base—and
+enables the FPU before tailing into newlib's `crt0`. Semihosting provides
+console output and process exit.
diff --git a/samples/baremetal_riscv64/run_qemu.sh b/samples/baremetal_riscv64/run_qemu.sh
new file mode 100755
index 0000000..2245c2d
--- /dev/null
+++ b/samples/baremetal_riscv64/run_qemu.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# Copyright 2026 The IREE Authors
+#
+# Licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+# Runs the baremetal_riscv64 sample runners on qemu-system-riscv64
+# (-machine virt, no OS, semihosted I/O). The runners are built by
+# build_tools/cmake/build_riscv_baremetal.sh.
+#
+# Environment:
+# QEMU_BIN qemu-system-riscv64 binary (default: from PATH).
+# IREE_TARGET_BUILD_DIR build from build_riscv_baremetal.sh
+# (default: build-riscv-baremetal).
+
+set -xeuo pipefail
+
+QEMU_BIN="${QEMU_BIN:-qemu-system-riscv64}"
+IREE_TARGET_BUILD_DIR="${IREE_TARGET_BUILD_DIR:-build-riscv-baremetal}"
+SAMPLE_DIR="${IREE_TARGET_BUILD_DIR}/samples/baremetal_riscv64"
+
+for runner in runner_llvmcpu runner_vmvx; do
+ timeout 120 "${QEMU_BIN}" -machine virt -m 512M -nographic -semihosting \
+ -bios none \
+ -device loader,file="${SAMPLE_DIR}/${runner}",cpu-num=0 \
+ 2>&1 | tee "${SAMPLE_DIR}/${runner}.log"
+ grep -q "PASS: single-op inference on bare-metal riscv64" \
+ "${SAMPLE_DIR}/${runner}.log"
+done
diff --git a/samples/baremetal_riscv64/runner.c b/samples/baremetal_riscv64/runner.c
new file mode 100644
index 0000000..b69c1be
--- /dev/null
+++ b/samples/baremetal_riscv64/runner.c
@@ -0,0 +1,188 @@
+// Copyright 2026 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// Bare-metal single-op runner using the inline HAL: no HAL device, no
+// threads, no filesystem. The backend variant is selected by USE_LLVMCPU below;
+// the matching iree-compile invocations are in CMakeLists.txt.
+//
+// NOTE: Error paths may leak resources. This is acceptable for this single-shot
+// runner because main() reports the error and exits immediately. The successful
+// path releases all resources.
+
+#include <stdio.h>
+
+#include "iree/base/api.h"
+#include "iree/hal/api.h"
+#include "iree/modules/hal/inline/module.h"
+#include "iree/modules/hal/types.h"
+#include "iree/vm/api.h"
+#include "iree/vm/bytecode/module.h"
+
+// USE_LLVMCPU: vmfb compiled with llvm-cpu (inline-dynamic); kernels are real
+// RISC-V machine code in an embedded ELF, loaded via the hal_loader module.
+// Default: vmfb compiled with vmvx-inline (inline-static); kernels are VM
+// bytecode and only the hal_inline module is needed.
+#if defined(USE_LLVMCPU)
+#include "iree/hal/local/loaders/embedded_elf_loader.h"
+#include "iree/modules/hal/loader/module.h"
+#include "samples/baremetal_riscv64/simple_mul_module_llvmcpu_c.h"
+#define simple_mul_module_create \
+ iree_samples_baremetal_riscv64_module_llvmcpu_create
+#else
+#include "samples/baremetal_riscv64/simple_mul_module_vmvx_c.h"
+#define simple_mul_module_create \
+ iree_samples_baremetal_riscv64_module_vmvx_create
+#endif // USE_LLVMCPU
+
+static iree_status_t make_input_buffer_view(
+ iree_hal_allocator_t* device_allocator, const float* data,
+ iree_host_size_t count, iree_hal_buffer_view_t** out_view) {
+ iree_hal_buffer_params_t params = {
+ .type =
+ IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL | IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
+ .usage = IREE_HAL_BUFFER_USAGE_DEFAULT,
+ };
+ iree_hal_buffer_t* buffer = NULL;
+ IREE_RETURN_IF_ERROR(iree_hal_allocator_allocate_buffer(
+ device_allocator, params, count * sizeof(float), &buffer));
+ iree_status_t status =
+ iree_hal_buffer_map_write(buffer, 0, data, count * sizeof(float));
+ if (iree_status_is_ok(status)) {
+ iree_hal_dim_t shape[1] = {(iree_hal_dim_t)count};
+ status = iree_hal_buffer_view_create(buffer, 1, shape,
+ IREE_HAL_ELEMENT_TYPE_FLOAT_32,
+ IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
+ iree_allocator_system(), out_view);
+ }
+ iree_hal_buffer_release(buffer);
+ return status;
+}
+
+static iree_status_t run(void) {
+ iree_allocator_t host_allocator = iree_allocator_system();
+
+ iree_vm_instance_t* instance = NULL;
+ IREE_RETURN_IF_ERROR(iree_vm_instance_create(IREE_VM_TYPE_CAPACITY_DEFAULT,
+ host_allocator, &instance));
+ IREE_RETURN_IF_ERROR(iree_hal_module_register_inline_types(instance));
+
+ iree_hal_allocator_t* device_allocator = NULL;
+ IREE_RETURN_IF_ERROR(iree_hal_allocator_create_heap(
+ iree_make_cstring_view("bare-metal"), host_allocator, host_allocator,
+ &device_allocator));
+
+ iree_vm_module_t* hal_inline_module = NULL;
+ IREE_RETURN_IF_ERROR(iree_hal_inline_module_create(
+ instance, IREE_HAL_INLINE_MODULE_FLAG_NONE,
+ iree_hal_module_debug_sink_stdio(stderr), device_allocator,
+ host_allocator, &hal_inline_module));
+
+#if defined(USE_LLVMCPU)
+ IREE_RETURN_IF_ERROR(iree_hal_module_register_loader_types(instance));
+ iree_hal_executable_loader_t* elf_loader = NULL;
+ IREE_RETURN_IF_ERROR(iree_hal_embedded_elf_loader_create(
+ /*plugin_manager=*/NULL, host_allocator, &elf_loader));
+ iree_vm_module_t* hal_loader_module = NULL;
+ iree_status_t loader_status = iree_hal_loader_module_create(
+ instance, IREE_HAL_LOADER_MODULE_FLAG_NONE, /*loader_count=*/1,
+ &elf_loader, host_allocator, &hal_loader_module);
+ iree_hal_executable_loader_release(elf_loader);
+ IREE_RETURN_IF_ERROR(loader_status);
+#endif // USE_LLVMCPU
+
+ const struct iree_file_toc_t* module_toc = simple_mul_module_create();
+ iree_vm_module_t* bytecode_module = NULL;
+ IREE_RETURN_IF_ERROR(iree_vm_bytecode_module_create(
+ instance, IREE_VM_BYTECODE_MODULE_FLAG_NONE,
+ iree_make_const_byte_span(module_toc->data, module_toc->size),
+ iree_allocator_null(), host_allocator, &bytecode_module));
+
+ iree_vm_context_t* context = NULL;
+#if defined(USE_LLVMCPU)
+ iree_vm_module_t* modules[] = {hal_inline_module, hal_loader_module,
+ bytecode_module};
+#else
+ iree_vm_module_t* modules[] = {hal_inline_module, bytecode_module};
+#endif // USE_LLVMCPU
+ IREE_RETURN_IF_ERROR(iree_vm_context_create_with_modules(
+ instance, IREE_VM_CONTEXT_FLAG_NONE, IREE_ARRAYSIZE(modules), modules,
+ host_allocator, &context));
+ iree_vm_module_release(hal_inline_module);
+#if defined(USE_LLVMCPU)
+ iree_vm_module_release(hal_loader_module);
+#endif // USE_LLVMCPU
+ iree_vm_module_release(bytecode_module);
+
+ iree_vm_function_t main_function;
+ IREE_RETURN_IF_ERROR(iree_vm_context_resolve_function(
+ context, iree_make_cstring_view("module.simple_mul"), &main_function));
+
+ const float kFloat4[4] = {4.0f, 4.0f, 4.0f, 4.0f};
+ const float kFloat2[4] = {2.0f, 2.0f, 2.0f, 2.0f};
+ iree_hal_buffer_view_t* arg0 = NULL;
+ iree_hal_buffer_view_t* arg1 = NULL;
+ IREE_RETURN_IF_ERROR(make_input_buffer_view(device_allocator, kFloat4,
+ IREE_ARRAYSIZE(kFloat4), &arg0));
+ IREE_RETURN_IF_ERROR(make_input_buffer_view(device_allocator, kFloat2,
+ IREE_ARRAYSIZE(kFloat2), &arg1));
+
+ iree_vm_list_t* inputs = NULL;
+ IREE_RETURN_IF_ERROR(iree_vm_list_create(iree_vm_make_undefined_type_def(), 2,
+ host_allocator, &inputs));
+ iree_vm_ref_t arg0_ref = iree_hal_buffer_view_move_ref(arg0);
+ iree_vm_ref_t arg1_ref = iree_hal_buffer_view_move_ref(arg1);
+ IREE_RETURN_IF_ERROR(iree_vm_list_push_ref_move(inputs, &arg0_ref));
+ IREE_RETURN_IF_ERROR(iree_vm_list_push_ref_move(inputs, &arg1_ref));
+
+ iree_vm_list_t* outputs = NULL;
+ IREE_RETURN_IF_ERROR(iree_vm_list_create(iree_vm_make_undefined_type_def(), 1,
+ host_allocator, &outputs));
+
+ IREE_RETURN_IF_ERROR(
+ iree_vm_invoke(context, main_function, IREE_VM_INVOCATION_FLAG_NONE,
+ /*policy=*/NULL, inputs, outputs, host_allocator));
+
+ iree_hal_buffer_view_t* ret_view =
+ iree_vm_list_get_buffer_view_assign(outputs, 0);
+ if (!ret_view) {
+ return iree_make_status(IREE_STATUS_NOT_FOUND, "no result buffer view");
+ }
+
+ float results[4] = {0.0f, 0.0f, 0.0f, 0.0f};
+ IREE_RETURN_IF_ERROR(iree_hal_buffer_map_read(
+ iree_hal_buffer_view_buffer(ret_view), 0, results, sizeof(results)));
+
+ printf("simple_mul result: [%f %f %f %f]\n", results[0], results[1],
+ results[2], results[3]);
+ for (iree_host_size_t i = 0; i < IREE_ARRAYSIZE(results); ++i) {
+ if (results[i] != 8.0f) {
+ return iree_make_status(IREE_STATUS_UNKNOWN,
+ "result mismatch at %zu: %f != 8.0", (size_t)i,
+ results[i]);
+ }
+ }
+
+ iree_vm_list_release(inputs);
+ iree_vm_list_release(outputs);
+ iree_vm_context_release(context);
+ iree_hal_allocator_release(device_allocator);
+ iree_vm_instance_release(instance);
+ return iree_ok_status();
+}
+
+int main(void) {
+ printf("bare-metal iree runner starting\n");
+ iree_status_t status = run();
+ int ret = (int)iree_status_code(status);
+ if (iree_status_is_ok(status)) {
+ printf("PASS: single-op inference on bare-metal riscv64\n");
+ } else {
+ iree_status_fprint(stderr, status);
+ iree_status_free(status);
+ printf("FAIL\n");
+ }
+ return ret;
+}
diff --git a/samples/baremetal_riscv64/simple_mul.mlir b/samples/baremetal_riscv64/simple_mul.mlir
new file mode 100644
index 0000000..3d8f97e
--- /dev/null
+++ b/samples/baremetal_riscv64/simple_mul.mlir
@@ -0,0 +1,4 @@
+func.func @simple_mul(%arg0: tensor<4xf32>, %arg1: tensor<4xf32>) -> tensor<4xf32> {
+ %0 = arith.mulf %arg0, %arg1 : tensor<4xf32>
+ return %0 : tensor<4xf32>
+}
diff --git a/samples/baremetal_riscv64/start.S b/samples/baremetal_riscv64/start.S
new file mode 100644
index 0000000..374114f
--- /dev/null
+++ b/samples/baremetal_riscv64/start.S
@@ -0,0 +1,17 @@
+// Copyright 2026 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// Minimal boot shim for QEMU's virt machine.
+// QEMU's generic loader sets the PC to the ELF entry point but does not
+// initialize sp. newlib's crt0 (_start) expects sp to be valid. Enable the
+// FPU as well: mstatus.FS starts Off, so FP instructions would otherwise trap.
+ .section .text.enter, "ax", @progbits
+ .globl _enter
+_enter:
+ li t0, 0x6000 // mstatus.FS = 11 (Dirty): enable FPU
+ csrs mstatus, t0
+ la sp, __stack_top
+ tail _start