diff --git a/.github/workflows/build-treelite-runtime.yml b/.github/workflows/build-treelite-runtime.yml new file mode 100644 index 00000000000..f6377b2225b --- /dev/null +++ b/.github/workflows/build-treelite-runtime.yml @@ -0,0 +1,207 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# treelite-runtime 3.9.1 is the last release of this distribution: starting with +# treelite 4.0.0, runtime/python was dropped from this repo and reborn as +# tl2cgen_runtime in dmlc/tl2cgen. Based on upstream's own manylinux wheel job, +# which builds both python/ and runtime/python/ from one CMake build and tags +# the runtime wheel py3-none via `bdist_wheel --universal`: +# https://github.com/dmlc/treelite/blob/3.9.1/ops/build-linux.sh +name: Build treelite-runtime wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/treelite-runtime.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-treelite-runtime.yml' + - 'docs/packages/treelite-runtime.yaml' + - 'patches/treelite-runtime/**' + push: + branches: [main] + paths: + - '.github/workflows/build-treelite-runtime.yml' + - 'docs/packages/treelite-runtime.yaml' + - 'patches/treelite-runtime/**' + +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 wheel's + # vendored libgomp came from. + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: treelite-runtime + 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 treelite-runtime ${{ matrix.version }} py3-none-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 180 + + env: + TREELITE_RUNTIME_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout treelite ${{ env.TREELITE_RUNTIME_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: dmlc/treelite + ref: ${{ env.TREELITE_RUNTIME_VERSION }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Patch treelite source + run: git apply python-wheels/patches/treelite-runtime/${{ env.TREELITE_RUNTIME_VERSION }}/0001-*.patch + + # setuptools' default license-files glob (LICEN[CSZ]E*) is rooted at + # setup.py's own directory, which for the runtime package is + # runtime/python/, not the repository root where LICENSE actually lives. + - name: Stage licence text for packaging + run: cp LICENSE runtime/python/LICENSE + + - name: Build wheel + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + package-dir: runtime/python + output-dir: wheelhouse/ + # runtime/python/setup.py declares ext_modules so cibuildwheel tags + # the wheel cp312-cp312; retagged py3-none below since + # libtreelite_runtime.so is ctypes-loaded, not interpreter-specific. + only: cp312-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # CMAKE_POLICY_VERSION_MINIMUM covers the vendored fmtlib's cmake_minimum_required. + CIBW_ENVIRONMENT: PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ CMAKE_POLICY_VERSION_MINIMUM=3.5 + # The root CMakeLists builds both the treelite and treelite_runtime + # shared libs in one pass; setup.py picks up the latter from + # {project}/build instead of rebuilding it. + CIBW_BEFORE_BUILD: | + cmake -S {project} -B {project}/build -DCMAKE_BUILD_TYPE=Release + cmake --build {project}/build --parallel + # python/ (the treelite package proper) is not published here -- it + # reuses the same {project}/build libtreelite.so and is installed + # only so a real model can be compiled and loaded, the way + # upstream's own test_basic.py does. + CIBW_BEFORE_TEST: pip install {project}/python + CIBW_TEST_ENVIRONMENT: PIP_ONLY_BINARY=numpy,scipy + CIBW_TEST_SOURCES: tests/examples + # test_basic.py itself is not run: every check builds its DMatrix + # from a SciPy CSR matrix via np.array(..., copy=False, dtype=cast), + # which numpy 2 (the only line with a riscv64 wheel) rejects outright + # on any platform, treelite-runtime predating numpy 2 -- not a + # riscv64 gap. DMatrix-from-dense-ndarray asks for no such cast. + CIBW_TEST_COMMAND: | + python3 - <<'PYEOF' + import numpy as np + import treelite, treelite_runtime + + treelite.Model.load('tests/examples/mushroom/mushroom.model', model_format='xgboost').export_lib( + toolchain='gcc', libpath='/tmp/mushroom.so', verbose=True) + predictor = treelite_runtime.Predictor(libpath='/tmp/mushroom.so', verbose=True) + data = np.random.rand(5, predictor.num_feature).astype('float32') + out = predictor.predict(treelite_runtime.DMatrix(data)) + assert out.shape == (5,) and np.isfinite(out).all(), out + print(out) + PYEOF + + # setup.py's bdist_wheel has no py3-none override for pip-driven builds + # (upstream's own ops/build-linux.sh passes --universal by hand), so + # cibuildwheel produces a cp312-cp312 tag; restore upstream's tag here. + - name: Retag the wheel py3-none (libtreelite_runtime.so is ctypes-loaded) + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq --no-install-recommends python3-venv + python3 -m venv .venv + . .venv/bin/activate + pip install -q "wheel>=0.42" + python3 -m wheel tags --python-tag py3 --abi-tag none --remove wheelhouse/treelite_runtime-*.whl + + - name: Check the wheel carries libtreelite_runtime and a vendored libgomp + run: | + python3 - wheelhouse/*.whl <<'EOF' + import sys, zipfile + + names = zipfile.ZipFile(sys.argv[1]).namelist() + for want in ("treelite_runtime/lib/libtreelite_runtime.so", "libgomp"): + if not any(want in n for n in names): + raise SystemExit(f"error: {want} missing from {sys.argv[1]}") + print("\n".join(n for n in names if ".so" in n)) + EOF + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: treelite-runtime-${{ env.TREELITE_RUNTIME_VERSION }}-py3-none-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + 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 treelite-runtime ${{ matrix.version }} + runs-on: ubuntu-24.04-riscv + env: + TREELITE_RUNTIME_VERSION: ${{ matrix.version }} + + steps: + # auditwheel vendors the image's libgomp.so.1 into the wheel. + - 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: treelite-runtime-${{ env.TREELITE_RUNTIME_VERSION }}-gpl-sources + path: gpl-sources.tar + if-no-files-found: error + + publish: + name: Publish treelite-runtime ${{ matrix.version }} + needs: [setup, build_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: treelite-runtime-${{ matrix.version }}-*-manylinux_riscv64 + gpl-sources-artifact: treelite-runtime-${{ matrix.version }}-gpl-sources + gpl-sources-description: gcc diff --git a/docs/packages/treelite-runtime.yaml b/docs/packages/treelite-runtime.yaml new file mode 100644 index 00000000000..f9a32c5d59a --- /dev/null +++ b/docs/packages/treelite-runtime.yaml @@ -0,0 +1,5 @@ +package-name: treelite-runtime +source-code: https://github.com/dmlc/treelite +license: Apache-2.0 +versions: +- version: 3.9.1 diff --git a/patches/treelite-runtime/3.9.1/0001-Pin-RapidJSON-past-v1.1.0-s-broken-operator.patch b/patches/treelite-runtime/3.9.1/0001-Pin-RapidJSON-past-v1.1.0-s-broken-operator.patch new file mode 100644 index 00000000000..bf3ede14b61 --- /dev/null +++ b/patches/treelite-runtime/3.9.1/0001-Pin-RapidJSON-past-v1.1.0-s-broken-operator.patch @@ -0,0 +1,34 @@ +From eaa233f4e012135829b4584d97b18a9bd534e4ee Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Mon, 5 Oct 2026 19:26:42 +0000 +Subject: [PATCH] Pin vendored RapidJSON past v1.1.0's broken + GenericStringRef::operator= + +GenericStringRef declares a copy-assignment operator that assigns to +its own const length member, which GCC 14 rejects outright instead of +just warning (operator= is a template-class member never odr-used +elsewhere in treelite, so older GCC never instantiated, and so never +type-checked, its body). Fixed 3 commits after v1.1.0 by explicitly +deleting the operator; pin to that commit instead of the tag. + +Upstream-Status: Backport [https://github.com/Tencent/rapidjson/commit/862c39be371278a45a88d4d1d75164be57bb7e2d] +--- + cmake/ExternalLibs.cmake | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/cmake/ExternalLibs.cmake b/cmake/ExternalLibs.cmake +index 996874f..4405f50 100644 +--- a/cmake/ExternalLibs.cmake ++++ b/cmake/ExternalLibs.cmake +@@ -29,7 +29,7 @@ else() + FetchContent_Declare( + RapidJSON + GIT_REPOSITORY https://github.com/Tencent/rapidjson +- GIT_TAG v1.1.0 ++ GIT_TAG 862c39be371278a45a88d4d1d75164be57bb7e2d + ) + FetchContent_Populate(RapidJSON) + message(STATUS "RapidJSON was downloaded at ${rapidjson_SOURCE_DIR}.") +-- +2.43.0 +