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
229 changes: 229 additions & 0 deletions .github/workflows/build-warp-lang.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# Based on upstream's Linux wheel pipeline, which compiles the Clang/LLVM SDK from source in a
# manylinux container and then cuts one wheel per platform:
# https://github.com/NVIDIA/warp/blob/v1.16.0/.github/workflows/build-llvm-sdk.yml
# https://github.com/NVIDIA/warp/blob/v1.16.0/.gitlab-ci.yml
name: Build warp-lang wheels (riscv64)

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

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

permissions:
contents: read # to fetch code (actions/checkout)

env:
# Pinned explicitly so the gpl_sources job pulls the same image the statically linked
# libstdc++/libgcc in the wheel came from.
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64

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

build_wheels:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
name: Build warp-lang ${{ matrix.version }} py3-none-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 1440 # full Clang/LLVM compile from source

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

steps:
- name: Checkout warp v${{ env.WARP_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: NVIDIA/warp
ref: v${{ env.WARP_VERSION }}
persist-credentials: false

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

# setup.py packages examples/assets and tests/assets, and both are git-lfs; nothing
# guarantees a git-lfs client on the self-hosted runner, so fetch the blobs over the
# media CDN instead of checking out with lfs: true (gotcha 296).
- name: Fetch the git-lfs assets the wheel packages
run: |
for f in warp/examples/assets/* warp/tests/assets/*; do
grep -q '^version https://git-lfs' "$f" || continue
curl -sSfL -o "$f" \
"https://media.githubusercontent.com/media/NVIDIA/warp/v${WARP_VERSION}/$f"
done

- name: Apply patches
run: git apply python-wheels/patches/warp-lang/${{ env.WARP_VERSION }}/*.patch

- name: Pull manylinux_riscv64 image
run: docker pull "${MANYLINUX_RISCV64_IMAGE}"

- name: Write the JIT smoke test
run: |
cat > "${RUNNER_TEMP}/smoke.py" <<'EOF'
import numpy as np

import warp as wp

wp.init()
print(wp.get_devices())


@wp.kernel
def saxpy(a: float, x: wp.array(dtype=float), y: wp.array(dtype=float)):
i = wp.tid()
y[i] = a * x[i] + wp.sqrt(y[i])


x = wp.array(np.arange(16, dtype=np.float32), dtype=float, device="cpu")
y = wp.array(np.full(16, 4.0, dtype=np.float32), dtype=float, device="cpu")
wp.launch(saxpy, dim=16, inputs=[2.0, x, y], device="cpu")
np.testing.assert_allclose(y.numpy(), 2.0 * np.arange(16) + 2.0, rtol=1e-6)
print("JIT smoke test OK")
EOF

# Upstream ships no prebuilt Clang/LLVM SDK for riscv64, so --build-llvm compiles it here
# instead of downloading one; --no-cuda is upstream's own CPU-only configuration. cmake and
# ninja are pinned as in upstream's tools/llvm/ci/build-linux.sh, ahead of the image's own
# cmake 4. libstdc++-static is CRB-only, and warp links -static-libstdc++/-static-libgcc.
- name: Build warp and the wheel
run: |
docker run --rm -v "$(pwd):/work" -v "${RUNNER_TEMP}/smoke.py:/smoke.py:ro" \
-w /work -e WARP_VERSION "${MANYLINUX_RISCV64_IMAGE}" bash -c '
set -euxo pipefail
dnf -y --enablerepo=crb install libstdc++-static
pybin=/opt/python/cp312-cp312/bin
"$pybin/pip" install -q --extra-index-url https://pypi.riseproject.dev/simple/ \
numpy build setuptools wheel cmake==3.31.6 ninja==1.11.1.3
export PATH="$pybin:$PATH"
"$pybin/python3" build_lib.py --no-cuda --build-llvm
file warp/bin/warp.so warp/bin/warp-clang.so
readelf -A warp/bin/warp-clang.so || true
ldd warp/bin/warp.so warp/bin/warp-clang.so
"$pybin/python3" -m build --wheel --no-isolation \
-C--build-option=-Plinux-riscv64 -C--build-option=-Mmanylinux_2_39
for tag in cp312-cp312 cp313-cp313 cp314-cp314 cp314-cp314t; do
"/opt/python/$tag/bin/python3" -m venv "/tmp/venv-$tag"
"/tmp/venv-$tag/bin/pip" install -q \
--extra-index-url https://pypi.riseproject.dev/simple/ dist/*.whl
(cd /tmp && "/tmp/venv-$tag/bin/python3" /smoke.py)
done
'

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: warp-lang-${{ env.WARP_VERSION }}-py3-none-manylinux_riscv64
path: dist/*.whl
if-no-files-found: error

test_wheels:
needs: [setup, build_wheels]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
name: Test warp-lang ${{ matrix.version }} py3-none-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 1440 # every kernel in the suite is JIT-compiled by Clang on the runner

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

steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: warp-lang-${{ env.WARP_VERSION }}-py3-none-manylinux_riscv64
path: dist

- name: Pull manylinux_riscv64 image
run: docker pull "${MANYLINUX_RISCV64_IMAGE}"

# The runner's four cores compile every kernel in the suite with Clang, which does not fit
# unittest_parallel's 3600s default wall-clock limit (patch 0004 makes it settable).
- name: Run upstream's test suite against the wheel
run: |
docker run --rm -v "$(pwd)/dist:/dist:ro" -w /tmp -e WARP_TEST_SUITE_TIMEOUT=43200 \
"${MANYLINUX_RISCV64_IMAGE}" bash -c '
set -euxo pipefail
pybin=/opt/python/cp312-cp312/bin
"$pybin/pip" install -q --extra-index-url https://pypi.riseproject.dev/simple/ /dist/*.whl
"$pybin/python3" -m warp.tests --maxjobs 4
'

gpl_sources:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
name: Collect GPL sources (gcc) for warp-lang ${{ matrix.version }}
runs-on: ubuntu-24.04-riscv

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

steps:
- name: Collect gcc source RPM from manylinux_riscv64
uses: riseproject-dev/python-wheels/actions/collect-gpl-sources@main
with:
image: ${{ env.MANYLINUX_RISCV64_IMAGE }}
packages: gcc
output: gpl-sources.tar

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: warp-lang-${{ env.WARP_VERSION }}-gpl-sources
path: gpl-sources.tar
if-no-files-found: error

publish:
name: Publish warp-lang ${{ matrix.version }}
needs: [setup, build_wheels, test_wheels, gpl_sources]
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: warp-lang-${{ matrix.version }}-*-manylinux_riscv64
gpl-sources-artifact: warp-lang-${{ matrix.version }}-gpl-sources
gpl-sources-description: gcc
14 changes: 14 additions & 0 deletions docs/packages/warp-lang.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package-name: warp-lang
source-code: https://github.com/NVIDIA/warp
license: Apache-2.0
versions:
- version: 1.16.0
patched: true
comment: >-
Built in upstream's own CPU-only configuration (``build_lib.py --no-cuda``), the one it
uses for macOS: kernels are JIT-compiled to native riscv64 code by the bundled
Clang/LLVM, and ``wp.get_devices()`` reports the CPU device only. The patches add riscv64
to the build and packaging platform tables, name the rv64gc/lp64d ABI for the JIT (which
the Clang frontend does not default to), convert fp16 in software because rv64gc has no
half-precision hardware and the JIT resolves no compiler-rt builtins, and let the
environment raise the test runner's wall-clock limit.
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
From 8b64e2e85baff0f1e96beb7b3ad4d25ac8cbd82e Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Sun, 20 Sep 2026 12:00:00 +0000
Subject: [PATCH 1/4] Add riscv64 to the build and packaging platform tables

Upstream-Status: To upstream [no riscv64 CI runner or prebuilt Clang/LLVM SDK upstream yet]

machine_architecture() in warp/_src/build_dll.py and in setup.py both raise
"Unrecognized machine architecture riscv64", which aborts build_lib.py before
anything is compiled.

setup.py's `platforms` table is what WarpBDistWheel.get_tag() reads to stamp the
wheel's platform tag, so riscv64 needs an entry there as well. manylinux_2_39 is
the flavour of the image these wheels are built in
(quay.io/pypa/manylinux_2_39_riscv64, Rocky Linux 10, glibc 2.39).

build_llvm.py picks LLVM_TARGETS_TO_BUILD from the same canonical architecture
string and otherwise falls back to X86, which would build an LLVM unable to emit
code for the machine it runs on; RISCV is the matching backend name.

Nothing else in the tree is architecture-specific: warp/native carries no SIMD
intrinsics and the Linux compile flags are a generic -O3 -fPIC --std=c++17.
---
build_llvm.py | 4 +++-
setup.py | 14 +++++++++-----
warp/_src/build_dll.py | 3 +++
3 files changed, 15 insertions(+), 6 deletions(-)

diff --git a/build_llvm.py b/build_llvm.py
index 22a9b45..3d9e2cd 100644
--- a/build_llvm.py
+++ b/build_llvm.py
@@ -150,7 +150,7 @@ def build_llvm_clang_from_source_for_arch(args, arch: str, llvm_source: str) ->

Args:
args: Command line arguments
- arch: Architecture to build for ("aarch64" or "x86_64")
+ arch: Architecture to build for ("aarch64", "riscv64" or "x86_64")
llvm_source: Path to the LLVM source code
"""

@@ -208,6 +208,8 @@ def build_llvm_clang_from_source_for_arch(args, arch: str, llvm_source: str) ->

if arch == "aarch64":
target_backend = "AArch64"
+ elif arch == "riscv64":
+ target_backend = "RISCV"
else:
target_backend = "X86"

diff --git a/setup.py b/setup.py
index b57e7ac..6562e89 100644
--- a/setup.py
+++ b/setup.py
@@ -21,14 +21,14 @@ parser.add_argument(
"-P",
type=str,
default="",
- help="Wheel platform: windows-x86_64|linux-x86_64|linux-aarch64|macos-aarch64",
+ help="Wheel platform: windows-x86_64|linux-x86_64|linux-aarch64|linux-riscv64|macos-aarch64",
)
parser.add_argument(
"--manylinux",
"-M",
type=str,
default="manylinux_2_28",
- help="Manylinux flavor for Linux wheels: manylinux_2_28|manylinux_2_34",
+ help="Manylinux flavor for Linux wheels: manylinux_2_28|manylinux_2_34|manylinux_2_39",
)
args = parser.parse_known_args()[0]

@@ -36,12 +36,15 @@ args = parser.parse_known_args()[0]
# returns a canonical machine architecture string
# - "x86_64" for x86-64, aka. AMD64, aka. x64
# - "aarch64" for AArch64, aka. ARM64
+# - "riscv64" for 64-bit RISC-V, aka. RV64
def machine_architecture() -> str:
machine = platform.machine()
if machine == "x86_64" or machine == "AMD64":
return "x86_64"
if machine == "aarch64" or machine == "arm64":
return "aarch64"
+ if machine == "riscv64":
+ return "riscv64"
raise RuntimeError(f"Unrecognized machine architecture {machine}")


@@ -76,6 +79,7 @@ platforms = [
Platform("windows", "x86_64", "Windows x86-64", ".dll", "win_amd64"),
Platform("linux", "x86_64", "Linux x86-64", ".so", "manylinux_2_28_x86_64"),
Platform("linux", "aarch64", "Linux AArch64", ".so", "manylinux_2_34_aarch64"),
+ Platform("linux", "riscv64", "Linux RISC-V 64", ".so", "manylinux_2_39_riscv64"),
Platform("macos", "aarch64", "macOS ARM64", ".dylib", "macosx_11_0_arm64"),
]

@@ -132,7 +136,7 @@ if args.command == "bdist_wheel":
if len(detected_platforms) > 1:
print("Libraries for multiple platforms were detected.")
print("Run `python -m build --wheel -C--build-option=-P<platform>` to select a specific one.")
- print("Available platforms: windows-x86_64, linux-x86_64, linux-aarch64, macos-aarch64")
+ print("Available platforms: windows-x86_64, linux-x86_64, linux-aarch64, linux-riscv64, macos-aarch64")
# Select the libraries corresponding with the this machine's platform
for p in platforms:
if p.os == machine_os() and p.arch == machine_architecture():
@@ -154,8 +158,8 @@ class WarpBDistWheel(bdist_wheel):
# setuptools.Command can validate the command line options.
user_options: ClassVar[list[tuple[str, str, str]]] = [
*bdist_wheel.user_options,
- ("platform=", "P", "Wheel platform: windows-x86_64|linux-x86_64|linux-aarch64|macos-aarch64"),
- ("manylinux=", "M", "Manylinux flavor for Linux wheels: manylinux_2_28|manylinux_2_34"),
+ ("platform=", "P", "Wheel platform: windows-x86_64|linux-x86_64|linux-aarch64|linux-riscv64|macos-aarch64"),
+ ("manylinux=", "M", "Manylinux flavor for Linux wheels: manylinux_2_28|manylinux_2_34|manylinux_2_39"),
]

def initialize_options(self):
diff --git a/warp/_src/build_dll.py b/warp/_src/build_dll.py
index f47f4a1..cdd61e7 100644
--- a/warp/_src/build_dll.py
+++ b/warp/_src/build_dll.py
@@ -60,12 +60,15 @@ def machine_architecture() -> str:
"""Return a canonical machine architecture string.
- "x86_64" for x86-64, aka. AMD64, aka. x64
- "aarch64" for AArch64, aka. ARM64
+ - "riscv64" for 64-bit RISC-V, aka. RV64
"""
machine = platform.machine()
if machine == "x86_64" or machine == "AMD64":
return "x86_64"
if machine == "aarch64" or machine == "arm64":
return "aarch64"
+ if machine == "riscv64":
+ return "riscv64"
raise RuntimeError(f"Unrecognized machine architecture {machine}")


--
2.43.0

Loading
Loading