Skip to content
Open
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
66 changes: 66 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Release
# Publishing a GitHub release uploads the package: a pre-release goes to
# TestPyPI, a full release goes to PyPI after approval of the `pypi`
# environment. Releasing a New Version in dev_maintenance.rst is the
# step-by-step procedure.

on:
release:
types: [published]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Check that the tag matches the package version
run: |
TAG="${{ github.event.release.tag_name }}"
VERSION=$(python -c "import re; print(re.search(r'__version__\s*=\s*[\"\x27]([^\"\x27]+)', open('mbirtorch/__init__.py').read()).group(1))")
if [ "$TAG" != "v$VERSION" ]; then
echo "tag $TAG does not match __version__ $VERSION (expected v$VERSION)"
exit 1
fi
- name: Build the sdist and wheel
run: |
pip install build
python -m build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

publish-testpypi:
# Pre-releases (tag vX.Y.ZrcN from prerelease) go to TestPyPI.
if: github.event.release.prerelease
needs: build
runs-on: ubuntu-latest
environment: testpypi
permissions:
id-token: write # Trusted Publishing; no token is stored.
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/

publish-pypi:
# Full releases (tag vX.Y.Z on main) go to PyPI after approval.
if: "!github.event.release.prerelease"
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # Trusted Publishing; no token is stored.
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
5 changes: 5 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ build:
# from PyPI, which on Linux is the CUDA build -- several GB of nvidia-* wheels
# that exceed RTD's build disk and time limits. Autodoc only needs to import
# the package, so the CPU wheel is sufficient.
#
# RTD seeds the environment with an old pip that rejects wheels whose
# metadata spells the name with an underscore (typing_extensions broke
# the 2026-08-12 build), so upgrade pip first.
- pip install --upgrade pip
- pip install torch --index-url https://download.pytorch.org/whl/cpu

# Build the Sphinx docs.
Expand Down
59 changes: 59 additions & 0 deletions dev_scripts/release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/bin/bash
# Run one stage of the release procedure in dev_maintenance.rst.
#
# dev_scripts/release.sh 0.2.0rc1 # rc: publish a pre-release to TestPyPI
# dev_scripts/release.sh 0.2.0 # final: open the PR from prerelease to main
# dev_scripts/release.sh 0.2.0 --publish # after the PR merges: publish to PyPI
#
# Requires the gh CLI, logged in. Uploads still need approval of the pypi
# environment on the workflow run page.
set -euo pipefail

cd "$(dirname "$0")/.."
VERSION="${1:?usage: release.sh X.Y.Z[rcN] [--publish]}"
PUBLISH="${2:-}"
INIT=mbirtorch/__init__.py

case "$VERSION" in
*rc*) STAGE=rc ;;
*) STAGE=final ;;
esac
if [[ "$PUBLISH" == "--publish" && "$STAGE" == "rc" ]]; then
echo "--publish is for a final version; an rc publishes on its own" >&2
exit 2
fi

if [[ "$PUBLISH" == "--publish" ]]; then
# The tag must point at main, so main must already carry this version.
git fetch -q origin main
if ! git show origin/main:$INIT | grep -q "__version__ = \"$VERSION\""; then
echo "main does not have __version__ = \"$VERSION\"; merge the PR first" >&2
exit 1
fi
gh release create "v$VERSION" --target main --title "MBIRTorch v$VERSION" \
--generate-notes
echo "Release v$VERSION created. Approve the pypi environment on the"
echo "workflow run page, then check with:"
echo " dev_scripts/check_published_wheel.sh --version $VERSION"
exit 0
fi

git checkout -q prerelease
git pull -q origin prerelease
sed -i '' "s/^__version__ = \".*\"/__version__ = \"$VERSION\"/" $INIT
grep -q "__version__ = \"$VERSION\"" $INIT
git add $INIT
git commit -q -m "Set version to $VERSION"
git push -q origin prerelease

if [[ "$STAGE" == "rc" ]]; then
gh release create "v$VERSION" --target prerelease --prerelease \
--title "MBIRTorch v$VERSION" --generate-notes
echo "Pre-release v$VERSION created; TestPyPI upload is running. Check with:"
echo " dev_scripts/check_published_wheel.sh --testpypi --version $VERSION"
else
gh pr create --base main --head prerelease --title "Release $VERSION" \
--body "Merges prerelease into main for the $VERSION release."
echo "Merge the PR when the checks pass, then run:"
echo " dev_scripts/release.sh $VERSION --publish"
fi
48 changes: 34 additions & 14 deletions docs/source/dev_maintenance.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,48 @@ The same tests run automatically on every push and pull request.
Releasing a New Version
-----------------------

This is only available for registered maintainers.
This is only available for registered maintainers. It requires the ``gh``
command, logged in to GitHub. The example below releases version 0.2.0.

1. Update ``__version__`` in ``mbirtorch/__init__.py`` and merge to
``prerelease``. This is the only place the version number is written.
Releasing to TestPyPI
+++++++++++++++++++++

2. On GitHub, draft a new release: tag ``vX.Y.ZrcN``, target ``prerelease``,
check "Set as a pre-release", and publish. This uploads to TestPyPI.
1. Publish a release candidate to TestPyPI::

3. Check the TestPyPI upload::
dev_scripts/release.sh 0.2.0rc1

dev_scripts/check_published_wheel.sh --testpypi --version X.Y.ZrcN
2. Check the TestPyPI upload::

4. Open a pull request from ``prerelease`` to ``main`` and merge it when the
checks pass.
dev_scripts/check_published_wheel.sh --testpypi --version 0.2.0rc1

5. Draft a new release: tag ``vX.Y.Z``, target ``main``, and publish. Then
approve the ``pypi`` environment on the workflow run page. This uploads
to PyPI.
If it fails, fix the problem and repeat from step 1 with ``0.2.0rc2``.

6. Check the PyPI upload::
Releasing to PyPI
+++++++++++++++++

dev_scripts/check_published_wheel.sh --version X.Y.Z
3. Open the release pull request::

dev_scripts/release.sh 0.2.0

Merge it on GitHub when the checks pass.

4. Publish the release::

dev_scripts/release.sh 0.2.0 --publish

Then approve the ``pypi`` environment on the workflow run page. This
uploads to PyPI.

5. Check the PyPI upload::

dev_scripts/check_published_wheel.sh --version 0.2.0

Each ``release.sh`` stage sets ``__version__`` in ``mbirtorch/__init__.py``,
commits, and creates the matching ``v``-prefixed tag; the upload fails if the
tag and ``__version__`` ever disagree. The manual procedure behind the
script: edit ``__version__``, commit to ``prerelease``, and draft a GitHub
release with tag ``v`` + ``__version__`` — target ``prerelease`` with "Set as
a pre-release" checked for an rc, target ``main`` for a final version.

The documentation rebuilds automatically: ``latest`` follows ``main``, and
``stable`` follows the highest release tag.
Expand Down
45 changes: 45 additions & 0 deletions docs/source/dev_performance_dashboard.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Performance Dashboard
=====================

MBIRTorch's reconstruction performance — run time, peak memory, and **correctness** — is tracked
automatically over time, on both CPU and GPU, by a companion project,
`mbirjax_metrics <https://github.com/gbuzzard/mbirjax_metrics>`__. When a tracked branch changes, a
scheduled job re-measures it and publishes an interactive dashboard:

**Live dashboard:** https://gbuzzard.github.io/mbirjax_metrics/

MBIRTorch and MBIRJAX share this dashboard: MBIRTorch's measurements appear on the ``cpu-torch``
and ``gpu-torch`` platforms next to MBIRJAX's, so the two implementations can be compared directly.
The dashboard rebuilds and republishes automatically whenever new measurements are pushed, so that
link is always current; you do not need to run anything to read it.

What it measures
----------------

For each tracked branch, the job runs MBIRTorch's reconstruction operators — the
direct-reconstruction filter, forward projection, back projection, and the iterative VCD
reconstruction — across a range of problem sizes and device counts, on both CPU and GPU. For every
configuration it records:

- **run time** (the minimum over repeated trials),
- **peak memory**, and
- a numeric **fingerprint** of the output, used to detect correctness changes.

How to read it
--------------

The dashboard explains itself: open the live page and expand the **"How to read this dashboard"**
panel at the top. It walks through the tiles, the red correctness banner, the History and Scaling
views, and the colors & marks.

That reading guide is authored *inside* the dashboard and ships in the page itself, so it can never
drift from the UI it describes — which is why this page links to it rather than duplicating it.

Running it yourself
-------------------

The dashboard is a single self-contained page generated from a YAML time series; no server is needed.
The measurement engine, the nightly harness, and the build script all live in the
`mbirjax_metrics <https://github.com/gbuzzard/mbirjax_metrics>`__ repository — see its ``README`` and
the ``action_scripts/`` and ``tooling/`` guides there for how runs are measured, gated, and scheduled,
and how to build the dashboard locally.
9 changes: 2 additions & 7 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ MBIRTorch: High-performance tomographic reconstruction
Based on PyTorch_, MBIRTorch can easily run on CPU or GPU.


.. PENDING(dashboard): point the Developer Docs card below back at
dev_performance_dashboard when that page lands (held at Greg's request).

.. grid:: 3

.. grid-item-card:: :material-regular:`rocket_launch;2em` Getting Started
Expand All @@ -65,7 +62,7 @@ MBIRTorch: High-performance tomographic reconstruction
.. grid-item-card:: :material-regular:`laptop_chromebook;2em` Developer Docs
:class-card: developer-docs
:columns: 12 6 6 4
:link: dev_sharding_overview
:link: dev_performance_dashboard
:link-type: doc


Expand Down Expand Up @@ -96,13 +93,11 @@ MBIRTorch: High-performance tomographic reconstruction
:maxdepth: 4
:caption: Developer Guide

dev_performance_dashboard
dev_sharding_overview
dev_projector_kernels
dev_api
dev_maintenance

.. PENDING(dashboard): restore dev_performance_dashboard to the toctree above
when that page lands (held at Greg's request).


.. _PyTorch: https://pytorch.org/docs/stable/index.html
2 changes: 1 addition & 1 deletion mbirtorch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
kept for API compatibility; here it means "return the device tensor").
"""

__version__ = "0.0.1"
__version__ = "0.0.1rc2"

# ── persistent torch.compile cache ────────────────────────────────────────────
# The inductor cache directory defaults to /tmp/torchinductor_<user>, which the
Expand Down
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
import pytest
import torch

# Start CUDA before any test runs. torch checks every device in
# range(device_count()) at the first CUDA use, and tests below fake
# device_count, so a first use inside one of them asks for a device that does
# not exist and fails.
if torch.cuda.is_available():
torch.zeros(1, device="cuda")


def available_devices():
devices = ["cpu"]
Expand Down
Loading