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
198 changes: 198 additions & 0 deletions .github/workflows/build-ai-dynamo-runtime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# This workflow is based on the runtime_wheel_builder stage of
# https://github.com/ai-dynamo/dynamo/blob/v1.5.1/container/templates/wheel_builder.Dockerfile
# and the CPU pre-merge pytest selection of
# https://github.com/ai-dynamo/dynamo/blob/v1.5.1/.github/workflows/pr.yaml
name: Build ai-dynamo-runtime wheels (riscv64)

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

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

permissions:
contents: read

jobs:
setup:
uses: $/.github/workflows/_setup.yml
with:
package: ai-dynamo-runtime
version: ${{ inputs.version }}

build_wheel:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
name: Build ai-dynamo-runtime ${{ matrix.version }} cp310-abi3-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 720
container:
image: quay.io/pypa/manylinux_2_39_riscv64
defaults:
run:
shell: bash
env:
AI_DYNAMO_RUNTIME_VERSION: ${{ matrix.version }}
PROTOC: /usr/bin/protoc
PROTOC_INCLUDE: /usr/include

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

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

- name: Apply patches
run: git apply -v python-wheels/patches/ai-dynamo-runtime/${{ env.AI_DYNAMO_RUNTIME_VERSION }}/*.patch

# Upstream downloads a protoc release zip, which has no riscv64 build.
- name: Install system dependencies
run: |
dnf -y --enablerepo=crb install protobuf-compiler protobuf-devel clang-devel
protoc --version
echo "/opt/python/cp312-cp312/bin" >> "$GITHUB_PATH"

- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain none --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
channel="$(sed -n 's/^channel = "\(.*\)"$/\1/p' rust-toolchain.toml)"
"$HOME/.cargo/bin/rustup" toolchain install "$channel" --profile minimal

- name: Build wheel
working-directory: lib/bindings/python
run: |
python -m pip install maturin
maturin build --release \
--features "kv-indexer,slot-tracker,select-service,mm-routing,aic-forward-pass,request-trace-s3" \
--out dist

- name: Bundle third-party Rust licences
run: |
find "$HOME/.cargo/registry/src" -mindepth 2 -maxdepth 2 -type d | while IFS= read -r crate; do
dest="rust-licenses/$(basename "$crate")"
for lf in "$crate"/LICENSE* "$crate"/LICENCE* "$crate"/COPYING* "$crate"/NOTICE* "$crate"/UNLICENSE*; do
[ -e "$lf" ] || continue
mkdir -p "$dest" && cp "$lf" "$dest/" 2>/dev/null || true
done
done
python -m pip install pyyaml
for whl in lib/bindings/python/dist/*.whl; do
PYTHONPATH=container python -m compliance.bundle_wheel_notices --wheel "$whl" --licenses-dir rust-licenses -v
python -c 'import sys, zipfile; assert any(n.endswith(".dist-info/licenses/THIRD-PARTY-RUST-LICENSES.txt") for n in zipfile.ZipFile(sys.argv[1]).namelist())' "$whl"
done

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ai-dynamo-runtime-${{ env.AI_DYNAMO_RUNTIME_VERSION }}-cp310-abi3-manylinux_riscv64
path: lib/bindings/python/dist/*.whl
if-no-files-found: error

test_wheel:
needs: [setup, build_wheel]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
python: ["3.12", "3.13", "3.14"]
name: Test ai-dynamo-runtime ${{ matrix.version }} cp${{ matrix.python }}
runs-on: ubuntu-24.04-riscv
timeout-minutes: 90
env:
AI_DYNAMO_RUNTIME_VERSION: ${{ matrix.version }}
UV_EXTRA_INDEX_URL: https://pypi.riseproject.dev/simple/
UV_INDEX_STRATEGY: unsafe-best-match
UV_ONLY_BINARY: pydantic-core,uvloop,numpy,aiohttp,frozenlist,multidict,propcache,yarl
UV_PYTHON_PREFERENCE: only-managed
# etcd 3.4 refuses to start on architectures it does not list as supported.
ETCD_UNSUPPORTED_ARCH: riscv64

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

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ai-dynamo-runtime-${{ env.AI_DYNAMO_RUNTIME_VERSION }}-cp310-abi3-manylinux_riscv64
path: dist

- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: ${{ matrix.python }}
activate-environment: true
enable-cache: false

# Upstream's test image provides nats-server and etcd for the bindings' autouse fixture.
- name: Install NATS and etcd
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq --no-install-recommends nats-server etcd-server

- name: Install wheel
run: |
uv pip install dist/*.whl
uv pip install pytest pytest-asyncio pytest-timeout pytest-forked pytest-benchmark aiohttp numpy

# test_kserve_grpc.py needs tritonclient[grpc], which upstream gets from its framework images.
- name: Test
run: |
python -m pytest lib/bindings/python/tests \
-m "pre_merge and gpu_0" \
--ignore=lib/bindings/python/tests/test_kserve_grpc.py

publish:
name: Publish ai-dynamo-runtime ${{ matrix.version }}
needs: [setup, build_wheel, test_wheel]
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: ai-dynamo-runtime-${{ matrix.version }}-*-manylinux_riscv64
6 changes: 6 additions & 0 deletions docs/packages/ai-dynamo-runtime.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package-name: ai-dynamo-runtime
source-code: https://github.com/ai-dynamo/dynamo
license: Apache-2.0
versions:
- version: 1.5.1
patched: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Tue, 13 Oct 2026 00:00:00 +0000
Subject: [PATCH] bindings: use nixl-sys stub API on riscv64

dynamo-llm (block-manager, on by default) and dynamo-memory pull in
nixl-sys 1.3.2. Its build.rs is meant to fall back to a dlopen stub API
when NIXL is not installed - which is how upstream's own wheel is built
("sans nixl") - but build_nixl() calls get_arch() before it looks for
libnixl.so, and get_arch() panics on anything but x86_64/aarch64:

panicked at build.rs: Unsupported architecture: riscv64

A panic is not the Err that triggers the stub fallback, so the build
dies on riscv64. Requesting nixl-sys's `stub-api` feature skips
build_nixl() entirely and compiles the same stubs upstream's wheel
ships. It is scoped to riscv64 so other targets are unchanged; the
proper fix is for nixl-sys's get_arch() to stop panicking.

Upstream-Status: Inappropriate [workaround for nixl-sys get_arch() panicking on riscv64; the real fix belongs in ai-dynamo/nixl]

Signed-off-by: Ludovic Henry <git@ludovic.dev>
---
diff --git a/lib/bindings/python/Cargo.lock b/lib/bindings/python/Cargo.lock
index a87d665..8054f83 100644
--- a/lib/bindings/python/Cargo.lock
+++ b/lib/bindings/python/Cargo.lock
@@ -2404,6 +2404,7 @@ dependencies = [
"dynamo-vllm-sidecar",
"dynamo-worker-selection-policy-catalog",
"futures",
+ "nixl-sys",
"once_cell",
"opentelemetry",
"opentelemetry_sdk",
diff --git a/lib/bindings/python/Cargo.toml b/lib/bindings/python/Cargo.toml
index 414c748..0055d28 100644
--- a/lib/bindings/python/Cargo.toml
+++ b/lib/bindings/python/Cargo.toml
@@ -127,6 +127,10 @@ pythonize = "0.23"
[target.'cfg(target_os = "linux")'.dependencies]
dynamo-llm = { path = "../../llm" }

+# nixl-sys's build.rs only knows x86_64/aarch64 and panics before its stub fallback.
+[target.'cfg(target_arch = "riscv64")'.dependencies]
+nixl-sys = { version = "=1.3.2", features = ["stub-api"] }
+
[target.'cfg(not(target_os = "linux"))'.dependencies]
dynamo-llm = { path = "../../llm", default-features = false }

Loading