Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/packages/warp-lang.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ versions:
gpl-sources:
filename: gpl-sources.tar
description: gcc
- version: 1.17.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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_architecture.py (shared by
build_lib.py, build_llvm.py, build_dll.py and setup.py) raises
"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 | 11 ++++++-----
warp/_src/build_architecture.py | 5 +++--
3 files changed, 12 insertions(+), 8 deletions(-)

diff --git a/build_llvm.py b/build_llvm.py
index 46cc828..7ac9c9b 100644
--- a/build_llvm.py
+++ b/build_llvm.py
@@ -166,7 +166,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
"""

@@ -224,6 +224,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 327bae0..998d9da 100644
--- a/setup.py
+++ b/setup.py
@@ -29,14 +29,14 @@ parser.add_argument(
"-P",
type=str,
default="",
- help="Wheel platform: windows-x86_64|windows-aarch64|linux-x86_64|linux-aarch64|macos-aarch64",
+ help="Wheel platform: windows-x86_64|windows-aarch64|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]

@@ -73,6 +73,7 @@ platforms = [
Platform("windows", "aarch64", "Windows ARM64", ".dll", "win_arm64"),
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"),
]

@@ -129,7 +130,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, windows-aarch64, linux-x86_64, linux-aarch64, macos-aarch64")
+ print("Available platforms: windows-x86_64, windows-aarch64, 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():
@@ -151,8 +152,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|windows-aarch64|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|windows-aarch64|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_architecture.py b/warp/_src/build_architecture.py
index 9df7173..68775d4 100644
--- a/warp/_src/build_architecture.py
+++ b/warp/_src/build_architecture.py
@@ -3,7 +3,7 @@

"""Detect and normalize architectures used by Warp builds.

-This module maps platform-specific x86-64 and ARM64 names to the
+This module maps platform-specific x86-64, ARM64 and RV64 names to the
canonical identifiers shared by Warp's build, packaging, and runtime code.
"""

@@ -12,13 +12,14 @@ from __future__ import annotations
import platform
from typing import Literal

-Architecture = Literal["x86_64", "aarch64"]
+Architecture = Literal["x86_64", "aarch64", "riscv64"]

_ARCHITECTURE_ALIASES: dict[str, Architecture] = {
"amd64": "x86_64",
"x86_64": "x86_64",
"arm64": "aarch64",
"aarch64": "aarch64",
+ "riscv64": "riscv64",
}


--
2.43.0

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
From bb045ca51b3f98290f3194192d5a72fde6af1667 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Sun, 20 Sep 2026 12:05:00 +0000
Subject: [PATCH 2/4] clang: name the rv64gc/lp64d target ABI for JIT-compiled
kernels

Upstream-Status: To upstream [no riscv64 runner upstream to regression-test it on]

warp-clang.so drives Clang through CompilerInvocation, i.e. at the cc1 level,
where none of the driver's per-target defaults are applied. On RV64 that leaves
clang::RISCVTargetInfo with a bare rv64i ISA (initFeatureMap sees no
-target-feature flags) and, because the invocation names no ABI, an ABIFLen of
zero -- the soft-float lp64 ABI.

warp.so, crt.cpp and warp-clang.so itself are compiled by the host gcc for
rv64gc/lp64d, so every call a JIT-compiled kernel makes into them that passes or
returns a float or a double -- _wp_isfinite(double) and the other crt.cpp shims
-- would read the value from the wrong register class. The base ISA is wrong for
the same reason: without M/A/F/D the frontend emits integer-multiply and
soft-float libcalls for operations the hardware performs natively.

Named explicitly rather than derived from the triple, because
LLVM_DEFAULT_TARGET_TRIPLE is whatever LLVM was configured with (build_llvm.py
sets riscv64-pc-linux) and cc1 infers neither a RISC-V ISA nor an ABI from the
triple's environment field.

Mirrors the x86_64 (+f16c) and aarch64 (+reserve-x28) blocks beside it.
---
warp/native/clang/clang.cpp | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)

diff --git a/warp/native/clang/clang.cpp b/warp/native/clang/clang.cpp
index a9fc417..d2ca34f 100644
--- a/warp/native/clang/clang.cpp
+++ b/warp/native/clang/clang.cpp
@@ -256,6 +256,24 @@ static std::unique_ptr<clang::CompilerInstance> create_compiler(
args.push_back("+f16c");
#endif

+#if defined(__riscv) && __riscv_xlen == 64
+ // cc1 defaults RV64 to a bare rv64i ISA and, with no ABI named, to soft-float lp64.
+ // warp.so and crt.cpp are compiled rv64gc/lp64d, so without this every kernel call
+ // into them (_wp_isfinite(double), ...) would pass floats in the wrong registers.
+ args.push_back("-target-abi");
+ args.push_back("lp64d");
+ args.push_back("-target-feature");
+ args.push_back("+m");
+ args.push_back("-target-feature");
+ args.push_back("+a");
+ args.push_back("-target-feature");
+ args.push_back("+f");
+ args.push_back("-target-feature");
+ args.push_back("+d");
+ args.push_back("-target-feature");
+ args.push_back("+c");
+#endif
+
#if defined(__aarch64__) || defined(_M_ARM64)
if (tiles_in_stack_memory) {
// Static memory support is broken on AArch64 CPUs. As a workaround we reserve some stack memory on kernel
--
2.43.0

Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
From 7bd5afd2c143e7db533bafc04c17ef4555c45a4b Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Mon, 21 Sep 2026 10:08:00 +0000
Subject: [PATCH 3/4] Convert fp16 in software on RISC-V targets without Zfh

Upstream-Status: To upstream [no riscv64 runner upstream to regression-test it on]

float_to_half()/half_to_float() convert through Clang's native _Float16 in
kernel code. RV64GC has no half-precision hardware, so Clang lowers both
conversions to the compiler-rt libcalls __extendhfsf2/__truncsfhf2, and
wp_load_obj() (warp/native/clang/clang.cpp) resolves a JIT-compiled module's
externals from a curated CRT table that carries no compiler builtins. Every
module holding an fp16 kernel therefore failed to materialize:

JIT session error: Symbols not found: [ __extendhfsf2, __truncsfhf2 ]
Failed to lookup symbol: Failed to materialize symbols: ...
RuntimeError: Failed to find forward kernel '...' for device 'cpu'

and took the module's non-fp16 kernels down with it -- 649 of the 1552 errors
in a full riscv64 run of warp.tests, spread over test_codegen, test_print,
test_spatial, test_transform, test_fabricarray and 40 more modules.

x86_64 sidesteps this by adding +f16c to the cc1 invocation, which lowers both
conversions to vcvtph2ps/vcvtps2ph; aarch64 gets fcvt from armv8-a. RISC-V has
no such baseline extension -- Zfh is optional, is not part of rv64gc, and
asking for it would emit fcvt.h.s on hardware that need not implement it -- so
convert in software instead.

The two routines are bit-for-bit what _Float16 yields where the hardware does
have half support: round to nearest with ties to even, signalling NaNs quieted,
NaN payloads preserved. Verified exhaustively against (_Float16) on x86_64
(+f16c) over all 2^32 float bit patterns and all 2^16 half bit patterns, zero
differences, so a riscv64 kernel rounds exactly as an x86_64 or aarch64 one
does rather than picking up the ties-away rounding of the Giesen routine
warp.cpp uses for the host-side conversions.

Guarded on !__riscv_zfh so a Zfh-enabled target keeps the native path.
---
warp/native/builtin.h | 105 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 105 insertions(+)

diff --git a/warp/native/builtin.h b/warp/native/builtin.h
index f0c23b5..b1ad4b7 100644
--- a/warp/native/builtin.h
+++ b/warp/native/builtin.h
@@ -367,6 +367,109 @@ CUDA_CALLABLE inline float bfloat16_to_float(wp_bfloat16 x) { return wp_bfloat16

#elif defined(__clang__)

+#if defined(__riscv) && !defined(__riscv_zfh)
+
+// RISC-V without Zfh has no half-precision hardware, so Clang lowers every _Float16 conversion
+// to the compiler-rt libcalls __extendhfsf2/__truncsfhf2. A JIT-compiled module resolves its
+// externals from the curated CRT table in wp_load_obj() (warp/native/clang/clang.cpp), which
+// carries no compiler builtins, so a module holding any fp16 kernel fails to materialize with
+// "JIT session error: Symbols not found: [ __extendhfsf2, __truncsfhf2 ]".
+//
+// Convert in software instead. Both routines are bit-for-bit what _Float16 yields where the
+// hardware does have half support -- round to nearest with ties to even, signalling NaNs
+// quieted, payloads preserved -- verified exhaustively over all 2^32 floats and all 2^16
+// halves, so kernels round exactly as they do on x86_64 (+f16c) and aarch64.
+CUDA_CALLABLE inline half float_to_half(float x)
+{
+ unsigned int bits;
+ memcpy(&bits, &x, sizeof(bits));
+
+ const unsigned int sign = (bits >> 16) & 0x8000u;
+ const unsigned int magnitude = bits & 0x7fffffffu;
+
+ unsigned int u;
+ if (magnitude >= 0x7f800000u) // Inf or NaN
+ {
+ const unsigned int mantissa = magnitude & 0x007fffffu;
+ u = sign | 0x7c00u | (mantissa ? ((mantissa >> 13) | 0x0200u) : 0u); // NaN quieted, Inf kept
+ }
+ else if (magnitude >= 0x47800000u) // 65536 and above overflows the half range
+ {
+ u = sign | 0x7c00u;
+ }
+ else if (magnitude >= 0x38800000u) // 2^-14 and above is a normal half
+ {
+ u = sign | (((magnitude >> 23) - 127 + 15) << 10) | ((magnitude >> 13) & 0x03ffu);
+ const unsigned int rest = magnitude & 0x1fffu; // the bits that do not fit
+ if (rest > 0x1000u || (rest == 0x1000u && (u & 1u)))
+ u += 1; // round to nearest, ties to even
+ }
+ else
+ {
+ const unsigned int exponent = magnitude >> 23;
+ const unsigned int shift = 126 - exponent; // a subnormal half's ulp is 2^-24
+ if (exponent == 0 || shift > 24)
+ {
+ u = sign; // underflows to zero
+ }
+ else
+ {
+ const unsigned int mantissa = (magnitude & 0x007fffffu) | 0x00800000u; // implicit bit
+ const unsigned int rest = mantissa & ((1u << shift) - 1u);
+ const unsigned int tie = 1u << (shift - 1);
+ u = sign | (mantissa >> shift);
+ if (rest > tie || (rest == tie && (u & 1u)))
+ u += 1; // round to nearest, ties to even
+ }
+ }
+
+ half h;
+ h.u = static_cast<unsigned short>(u);
+ return h;
+}
+
+CUDA_CALLABLE inline float half_to_float(half h)
+{
+ const unsigned int sign = (static_cast<unsigned int>(h.u) & 0x8000u) << 16;
+ const unsigned int exponent = (static_cast<unsigned int>(h.u) >> 10) & 0x001fu;
+ const unsigned int mantissa = static_cast<unsigned int>(h.u) & 0x03ffu;
+
+ unsigned int bits;
+ if (exponent == 0x1fu) // Inf or NaN
+ {
+ bits = sign | 0x7f800000u | (mantissa ? ((mantissa << 13) | 0x00400000u) : 0u);
+ }
+ else if (exponent == 0u) // zero or subnormal
+ {
+ if (mantissa == 0u)
+ {
+ bits = sign;
+ }
+ else
+ {
+ // Renormalize: shift the mantissa up until its leading one leaves the field.
+ unsigned int m = mantissa;
+ unsigned int e = 127 - 15 + 1;
+ while ((m & 0x0400u) == 0u)
+ {
+ m <<= 1;
+ e -= 1;
+ }
+ bits = sign | (e << 23) | ((m & 0x03ffu) << 13);
+ }
+ }
+ else
+ {
+ bits = sign | ((exponent + 127 - 15) << 23) | (mantissa << 13);
+ }
+
+ float val;
+ memcpy(&val, &bits, sizeof(val));
+ return val;
+}
+
+#else
+
// _Float16 is Clang's native half-precision floating-point type
CUDA_CALLABLE inline half float_to_half(float x)
{
@@ -381,6 +484,8 @@ CUDA_CALLABLE inline float half_to_float(half h)
return static_cast<float>(f16);
}

+#endif // __riscv && !__riscv_zfh
+
#ifndef WP_NO_BFLOAT16
CUDA_CALLABLE inline wp_bfloat16 float_to_bfloat16(float x)
{
--
2.43.0

Loading
Loading