From 1de1372f36b287713482f18385929da290d88541 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Mon, 28 Sep 2026 07:24:55 +0000 Subject: [PATCH 1/4] nutpie: add riscv64 build (0.16.11) Rust/pyo3 maturin project; nuts-rs uses faer (pure Rust) for linear algebra, no BLAS/LAPACK anywhere in the tree, and cargo metadata --filter-platform riscv64gc-unknown-linux-gnu resolves the identical 340-crate set as x86_64 (minus cpufeatures). The optional tch (libtorch) dependency is unused by any default feature. Mirrors upstream's own PyO3/maturin-action-based CI. Testing drives the compiled sampler through nutpie's dependency-free compiled_pyfunc API instead of upstream's stan/pymc/flow pytest suites: stanc3 ships no riscv64 binary, and pymc/flow need numba (-> llvmlite, parked) or jax. All of nutpie's own core runtime deps (pyarrow, pandas, arro3-core, obstore) already have riscv64 wheels on our registry. --- .github/workflows/build-nutpie.yml | 167 +++++++++++++++++++++++++++++ docs/packages/nutpie.yaml | 5 + 2 files changed, 172 insertions(+) create mode 100644 .github/workflows/build-nutpie.yml create mode 100644 docs/packages/nutpie.yaml diff --git a/.github/workflows/build-nutpie.yml b/.github/workflows/build-nutpie.yml new file mode 100644 index 00000000000..13b2d6ef29a --- /dev/null +++ b/.github/workflows/build-nutpie.yml @@ -0,0 +1,167 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the `build_linux` + `test_linux` jobs of +# https://github.com/pymc-devs/nutpie/blob/v0.16.11/.github/workflows/ci.yml +name: Build nutpie wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/nutpie.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-nutpie.yml' + - 'docs/packages/nutpie.yaml' + push: + branches: [main] + paths: + - '.github/workflows/build-nutpie.yml' + - 'docs/packages/nutpie.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: nutpie + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build nutpie ${{ matrix.version }} manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 480 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + + env: + NUTPIE_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout nutpie v${{ env.NUTPIE_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: pymc-devs/nutpie + ref: v${{ env.NUTPIE_VERSION }} + persist-credentials: false + + # nuts-rs/faer/zarrs are all pure Rust (no BLAS/LAPACK), so the riscv64gc + # target resolves the same dependency tree as x86_64/aarch64. bridgestan's + # build.rs only runs bindgen over a header (no Stan compilation at build + # time), which needs libclang. + - name: Build wheels + uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 + with: + target: riscv64gc-unknown-linux-gnu + args: --release --out dist --locked --interpreter 3.12 3.13 3.14 + manylinux: '2_39' + before-script-linux: dnf install -y clang-libs clang || apt install -y llvm-dev libclang-dev clang + + - name: Verify the wheels ship the compiled extension + run: | + set -euo pipefail + for whl in dist/*.whl; do + python3 - "$whl" <<'EOF' + import sys, zipfile + names = zipfile.ZipFile(sys.argv[1]).namelist() + sos = [n for n in names if n.endswith(".so")] + assert any("_lib" in n for n in sos), sos + assert any(n.endswith(".dist-info/licenses/LICENSE") for n in names), names + print(sys.argv[1], "->", sos) + EOF + done + + - name: Install uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: false + + # Upstream gates its release on the "stan" pytest suite, but that needs + # stanc3 (github.com/stan-dev/stanc3 releases), which ships no riscv64 + # build; "pymc"/"flow" need numba (-> llvmlite, parked, gotcha + # feasibility-and-triage.md) or jax. None of upstream's own suites can + # run here, so this drives the compiled sampler directly through + # nutpie's dependency-free `compiled_pyfunc` API instead (gotcha 187). + - name: Sample a toy model with each wheel + env: + PIP_EXTRA_INDEX_URL: https://pypi.riseproject.dev/simple/ + run: | + set -euo pipefail + for py in 3.12 3.13 3.14; do + venv="$RUNNER_TEMP/venv-$py" + uv venv --python "$py" "$venv" + tag="cp$(echo "$py" | tr -d '.')" + uv pip install --python "$venv/bin/python" dist/nutpie-*-"$tag"-*.whl + "$venv/bin/python" - <<'EOF' + import numpy as np + import nutpie + from nutpie.compiled_pyfunc import from_pyfunc + + def make_logp_fn(): + def logp(x): + return float(-0.5 * x[0] ** 2), np.array([-x[0]], dtype="float64") + + return logp + + def make_expand_fn(seed1, seed2, chain): + def expand(x): + return {"x": np.asarray(x, dtype="float64", order="C")} + + return expand + + model = from_pyfunc( + ndim=1, + make_logp_fn=make_logp_fn, + make_expand_fn=make_expand_fn, + expanded_dtypes=[np.dtype("float64")], + expanded_shapes=[(1,)], + expanded_names=["x"], + ) + trace = nutpie.sample( + model, chains=2, tune=300, draws=300, cores=1, seed=42, progress_bar=False + ) + draws = trace.posterior["x"].to_numpy() + mean = draws.mean() + std = draws.std() + assert abs(mean) < 0.3, mean + assert 0.6 < std < 1.4, std + print("sampled standard normal via compiled_pyfunc: mean", mean, "std", std) + EOF + done + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: nutpie-${{ env.NUTPIE_VERSION }}-manylinux_riscv64 + path: dist/*.whl + if-no-files-found: error + + publish: + name: Publish nutpie ${{ matrix.version }} + needs: [setup, build_wheels] + 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: nutpie-${{ matrix.version }}-manylinux_riscv64 diff --git a/docs/packages/nutpie.yaml b/docs/packages/nutpie.yaml new file mode 100644 index 00000000000..69c29cc78cf --- /dev/null +++ b/docs/packages/nutpie.yaml @@ -0,0 +1,5 @@ +package-name: nutpie +source-code: https://github.com/pymc-devs/nutpie +license: MIT +versions: +- version: 0.16.11 From 2d2165f4508fce567c09c56f1d33ebbf83236e58 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Mon, 28 Sep 2026 08:42:06 +0000 Subject: [PATCH 2/4] nutpie: drop --locked from the maturin build (PR #2420 CI fix) The v0.16.11 tag's committed Cargo.lock still lists the "nutpie" crate itself as version 0.16.10 (a stale self-entry left by upstream's release tooling ahead of the Cargo.toml bump), so cargo needs to update that one line on every build. --locked forbids it: `cargo metadata` fails with "cannot update the lock file ... because --locked was passed", which maturin surfaces as "cargo metadata failed. Does your crate compile with `cargo build`?". Upstream's own ci.yml never passes --locked to maturin-action either, so this also brings the build back in line with upstream's own build shape. Verified locally: cloning the v0.16.11 tag and running `cargo metadata --locked` reproduces the exact error, and the diff from a plain `cargo metadata` (no --locked) touches only that one version line, not the wider dependency graph. --- .github/workflows/build-nutpie.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-nutpie.yml b/.github/workflows/build-nutpie.yml index 13b2d6ef29a..23297ebea9d 100644 --- a/.github/workflows/build-nutpie.yml +++ b/.github/workflows/build-nutpie.yml @@ -63,11 +63,17 @@ jobs: # target resolves the same dependency tree as x86_64/aarch64. bridgestan's # build.rs only runs bindgen over a header (no Stan compilation at build # time), which needs libclang. + # + # No `--locked` (matching upstream's own ci.yml, which never passes it): + # the v0.16.11 tag's committed Cargo.lock still lists the "nutpie" crate + # itself as version 0.16.10 (a stale self-entry left by their release + # tooling), so cargo needs to update that one line and --locked forbids + # it. - name: Build wheels uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: riscv64gc-unknown-linux-gnu - args: --release --out dist --locked --interpreter 3.12 3.13 3.14 + args: --release --out dist --interpreter 3.12 3.13 3.14 manylinux: '2_39' before-script-linux: dnf install -y clang-libs clang || apt install -y llvm-dev libclang-dev clang From 5117d4e3637641b3e5c93e5c74a330acc3ba4664 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Mon, 28 Sep 2026 08:42:42 +0000 Subject: [PATCH 3/4] gotcha 608: stale self-version in a tagged release's Cargo.lock vs --locked A crate's own release tooling can tag before regenerating Cargo.lock, so the lock's self-entry for the crate names the previous version. cargo happily fixes that one line on an unlocked build, but --locked forbids it outright, and this isn't the "floating deps" case of gotcha 10 (no lock in the sdist at all) since here the whole tree stays pinned except that one line. The fix is to drop --locked to match upstream's own build workflow rather than patch or regenerate Cargo.lock (nutpie 0.16.11, PR #2420). --- .../references/gotchas-index.md | 4 +++ .../gotchas/rust-maturin-and-pyo3.md | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/skills/python-project-porting/references/gotchas-index.md b/skills/python-project-porting/references/gotchas-index.md index d45480fca53..d57cea8a94a 100644 --- a/skills/python-project-porting/references/gotchas-index.md +++ b/skills/python-project-porting/references/gotchas-index.md @@ -705,6 +705,10 @@ The porting gotchas (550 of them) live in [`references/gotchas/`](gotchas/), spl sysroot or container: host `clang --target=riscv64-linux-gnu` with the x86_64 glibc headers, plus stubs for `gnu/stubs-32.h` and the `regparm` in `pthreadtypes-arch.h` (the foxglove-sdk case). +- **608** — A tagged release's own committed `Cargo.lock` can be stale by one version bump in + the crate's own self-entry (release tooling bumps `Cargo.toml` before regenerating the + lock), which only `--locked` turns into a build failure; drop `--locked` to match upstream's + own CI rather than patching or regenerating `Cargo.lock` (the nutpie 0.16.11 case). ### Bazel & driving the build container — [`gotchas/native-build-bazel-and-drivers.md`](gotchas/native-build-bazel-and-drivers.md) diff --git a/skills/python-project-porting/references/gotchas/rust-maturin-and-pyo3.md b/skills/python-project-porting/references/gotchas/rust-maturin-and-pyo3.md index 50d29097a53..9a4ab260054 100644 --- a/skills/python-project-porting/references/gotchas/rust-maturin-and-pyo3.md +++ b/skills/python-project-porting/references/gotchas/rust-maturin-and-pyo3.md @@ -28,6 +28,8 @@ To pull up one entry: `grep -n '^N\. ' references/gotchas/rust-maturin-and-pyo3. different path in the git checkout than in the PyPI sdist. - **259** — A maturin `bindings = "bin"` project can declare two `[[bin]]` targets where - **260** — `puccinialin` (and similar rust-bootstrap-on-demand helpers) has no riscv64 entry +- **608** — A tagged release's own committed `Cargo.lock` can be stale by one version bump in + the crate's own self-entry, which only `--locked` turns into a build failure. - **371** — pyo3 0.22's version ceiling (gotcha 306) is a hard ceiling for a non-abi3, per-interpreter build too, one minor above its own release-time latest — the `PYO3_USE_ABI3_FORWARD_COMPATIBILITY` escape hatch works without turning abi3 on. @@ -91,6 +93,9 @@ To pull up one entry: `grep -n '^N\. ' references/gotchas/rust-maturin-and-pyo3. reverse-dependency closure in `Cargo.lock`, not by "one extension per interpreter". - **597** — A riscv64 `cargo check` of a tree whose `-sys` crates compile C needs no riscv64 sysroot: host clang plus x86_64 glibc headers and two stub headers. +- **608** — A tagged release's committed `Cargo.lock` can have a stale self-version entry + (the crate's own `[[package]] version` a release behind its `Cargo.toml`), which only + `maturin-action`'s `--locked` turns into a build failure. --- @@ -1481,3 +1486,30 @@ To pull up one entry: `grep -n '^N\. ' references/gotchas/rust-maturin-and-pyo3. so they say nothing about riscv64 C codegen. That matters little for the well-trodden `lz4-sys`/`zstd-sys`, but it is no substitute for gotcha 412's generic-path check when the C is the unknown. + +608. **A tagged release's own committed `Cargo.lock` can be stale by one version bump, and + `maturin-action`'s `args: --locked` turns that into a hard failure even when upstream's + own CI never passes `--locked` at all** (nutpie 0.16.11: `cargo metadata`/`cargo build` + error `cannot update the lock file ... because --locked was passed`, `💥 maturin failed`). + The mismatch isn't a real dependency drift — it's the crate's *own* self-entry: the + package's release tooling bumps `Cargo.toml`'s `version` but tags before regenerating + `Cargo.lock`, so the lock's `[[package]] name = "" version = "..."` line still + names the previous release. `cargo metadata --locked` (or a plain build) refuses to fix + even that one line without `--locked` being dropped or `--offline` with a pre-populated + registry being used instead. Diagnose by cloning the upstream tag and comparing: + ```bash + git clone --depth 1 --branch /tmp/x && cd /tmp/x + cargo metadata --locked --format-version 1 # reproduces the exact CI error + git diff Cargo.lock # after a metadata run *without* --locked + ``` + If the diff is a single `version = "..."` line inside the crate's own `[[package]]` + block, this is that trap, not a real re-resolution risk (gotcha 10's "floating deps" + case is different: there the *whole* tree re-resolves because no lock ships at all). + Fix: check whether upstream's own release workflow passes `--locked` to + `maturin-action`/`cargo build` — it usually doesn't, because upstream never builds from + a lock file this stale — and drop `--locked` from our own `args:` to match (see + `build-tombi.yml` for the same pattern from a version-patch step leaving workspace-local + `Cargo.lock` entries stale). Regenerating and committing a patched `Cargo.lock` under + `patches///` is unnecessary extra surface for a one-line, upstream-caused + mismatch that plain `cargo build` (no `--locked`) fixes on every run without touching any + other dependency. From da60c2ae1a9d40524789ca3dbb4a16933e48ef24 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Mon, 28 Sep 2026 15:03:47 +0000 Subject: [PATCH 4/4] nutpie: fix smoke-test env vars for uv (gotcha 244) uv pip install only honors UV_* env vars, not PIP_*. The step used PIP_EXTRA_INDEX_URL, which uv silently ignored, so it resolved against public PyPI only and source-built numpy/scipy/pandas/pyarrow instead of using the riscv64 wheels already on the registry, then failed outright on pyarrow's sdist (needs a system Arrow C++ install not present here). Swap to UV_EXTRA_INDEX_URL, add UV_INDEX_STRATEGY=unsafe-best-match so uv's default first-index strategy doesn't stop at PyPI before reaching the registry, and add UV_ONLY_BINARY for the packages observed building from source. --- .github/workflows/build-nutpie.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-nutpie.yml b/.github/workflows/build-nutpie.yml index 23297ebea9d..d3e4d4662b4 100644 --- a/.github/workflows/build-nutpie.yml +++ b/.github/workflows/build-nutpie.yml @@ -104,7 +104,9 @@ jobs: # nutpie's dependency-free `compiled_pyfunc` API instead (gotcha 187). - name: Sample a toy model with each wheel env: - PIP_EXTRA_INDEX_URL: https://pypi.riseproject.dev/simple/ + UV_EXTRA_INDEX_URL: https://pypi.riseproject.dev/simple/ + UV_INDEX_STRATEGY: unsafe-best-match + UV_ONLY_BINARY: numpy,scipy,pandas,pyarrow run: | set -euo pipefail for py in 3.12 3.13 3.14; do