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
32 changes: 29 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,34 @@ jobs:
# ninja and a C++ compiler from someone installing a wheel. aarch64
# is served by the sdist, where that toolchain is already the price of
# entry.
os: [ubuntu-latest, macos-latest]
#
# Windows is CPU only: clang from MSYS2's CLANG64 environment, which
# links against the Universal C Runtime official CPython uses.
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
# setuptools-scm reads the version from the tag history.
fetch-depth: 0
- name: Install clang, CMake and Ninja (Windows)
if: runner.os == 'Windows'
id: msys2
uses: msys2/setup-msys2@v2
with:
msystem: CLANG64
update: true
install: >-
mingw-w64-clang-x86_64-clang
mingw-w64-clang-x86_64-cmake
mingw-w64-clang-x86_64-ninja
mingw-w64-clang-x86_64-llvm-openmp
mingw-w64-clang-x86_64-llvm-tools
- name: Put CLANG64 on the path (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
"${{ steps.msys2.outputs.msys2-location }}\clang64\bin" >> $env:GITHUB_PATH
- uses: pypa/cibuildwheel@v2.23
env:
# One build per platform: the wheel is tagged py3 and carries no
Expand All @@ -75,6 +96,10 @@ jobs:
# wheel in any case: torch publishes no i686 build, and it is a hard
# dependency here.
CIBW_ARCHS_LINUX: x86_64
CIBW_ARCHS_WINDOWS: AMD64
# The library imports the system's DLLs and the OpenMP runtime torch
# carries, so there is nothing to vendor and no repair step.
CIBW_ENVIRONMENT_WINDOWS: CC=clang CXX=clang++ CMAKE_GENERATOR=Ninja
# BART's nested functions need heap trampolines -- GCC 14 -- or
# clang's Blocks. cibuildwheel's default Linux image has neither:
# manylinux2014 is CentOS 7, whose GCC is 10 and whose `clang` is
Expand Down Expand Up @@ -104,8 +129,9 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ubuntu-latest, macos-latest, windows-latest]
# The oldest and the newest Python the package supports.
python: ["3.10", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand Down
74 changes: 65 additions & 9 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,41 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

jobs:
# libbartorch is a clang build: BART's nested functions become Blocks.
# BART's nested functions become Blocks under clang and heap trampolines
# under GCC 14. Each platform runs the oldest and the newest Python the
# package supports; the wheel workflow tests the wheel on both as well.
# Windows is clang from MSYS2's CLANG64 environment, linked against the
# Universal C Runtime that official CPython uses.
tests:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
python-version: "3.10"
cc: clang
cc: gcc-14
extras: "[mkl]"
- os: ubuntu-latest
python-version: "3.12"
python-version: "3.14"
cc: clang
# One job measures coverage for Codecov; the rest only test.
coverage: true
- os: ubuntu-latest
python-version: "3.12"
cc: gcc-14
extras: "[mkl]"
- os: macos-latest
python-version: "3.12"
python-version: "3.10"
cc: clang
- os: macos-latest
python-version: "3.14"
cc: clang
- os: windows-latest
python-version: "3.10"
cc: clang
- os: windows-latest
python-version: "3.14"
cc: clang
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
# Codecov takes the upload on GitHub's OIDC token, so no secret is needed.
permissions:
contents: read
Expand All @@ -57,6 +70,38 @@ jobs:
if: runner.os == 'macOS'
run: brew install cmake

# omp.h comes from the openmp package; the library links against the
# OpenMP runtime torch carries, not against MSYS2's.
- name: Install clang, CMake and Ninja (Windows)
if: runner.os == 'Windows'
id: msys2
uses: msys2/setup-msys2@v2
with:
msystem: CLANG64
update: true
install: >-
mingw-w64-clang-x86_64-clang
mingw-w64-clang-x86_64-cmake
mingw-w64-clang-x86_64-ninja
mingw-w64-clang-x86_64-llvm-openmp
mingw-w64-clang-x86_64-llvm-tools

- name: Put CLANG64 on the path (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
"${{ steps.msys2.outputs.msys2-location }}\clang64\bin" >> $env:GITHUB_PATH
"CMAKE_GENERATOR=Ninja" >> $env:GITHUB_ENV

# long is 32 bits here, which is the case the fixed-width types exist
# for; this prints it, and tests/test_abi.py holds the library to it.
- name: The data model (Windows)
if: runner.os == 'Windows'
run: |
printf '#include <stdio.h>\nint main(void) { printf("long %%d, long long %%d, void* %%d\\n", (int)sizeof(long), (int)sizeof(long long), (int)sizeof(void*)); return 4 != sizeof(long); }\n' > model.c
clang model.c -o model.exe
./model.exe

- name: Install torch (CPU) and the build backend
run: |
pip install scikit-build-core setuptools-scm numpy scipy pytest pytest-cov numpydoc
Expand All @@ -73,7 +118,7 @@ jobs:
# Linux only because that index publishes no macOS wheel -- there both
# come from PyPI, which is consistent with itself.
- name: Install torchvision from the same index as torch
if: runner.os == 'Linux'
if: runner.os != 'macOS'
run: pip install torchvision --index-url https://download.pytorch.org/whl/cpu

- name: Build and install bartorch
Expand All @@ -82,6 +127,15 @@ jobs:
CXX: ${{ matrix.cc == 'gcc-14' && 'g++-14' || 'clang++' }}
run: pip install -e ".${{ matrix.extras }}" --no-build-isolation -v

# The library imports the system's DLLs and torch's OpenMP runtime and
# nothing from MSYS2, so it loads into official CPython without it.
- name: Nothing from MSYS2 in the library (Windows)
if: runner.os == 'Windows'
run: |
lib=$(python -c "from bartorch._lib import library_path; print(library_path())")
llvm-readobj --coff-imports "$lib" | grep 'Name:' | sort -u | tee imports.txt
! grep -Ei 'libomp|libc\+\+|libunwind|libwinpthread|msys' imports.txt

# Both wheels carry their own copy of LLVM's OpenMP runtime, and the
# runtime ends the process rather than run beside a second copy. The
# repair is bartorch's own, on the first NUFFT, so this reaches it the
Expand All @@ -107,6 +161,8 @@ jobs:
logging.basicConfig(level=logging.DEBUG, format="%(name)s: %(message)s")
import bartorch
print("build:", bartorch.build_info())
import ctypes, sys
print("platform:", sys.platform, "sizeof(long):", ctypes.sizeof(ctypes.c_long))
print("backends:", bartorch.backend_sources())
try:
from bartorch import _finufft
Expand Down
29 changes: 23 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ runtime spinning against BART's, which inside a tool costs several times more
than the transform gains. Where no source has DFTI, which is macOS, and for a
description DFTI declines, the transform compiled into the library serves
instead. On a device it is cuFFT, through BART's own `fft-cuda.c`.
DFTI is taken through MKL's LP64 interface, whose `MKL_LONG` is `long`: 32
bits on Windows. A length or stride past that is a description DFTI is not
handed -- the loop axis is walked instead, and a transformed axis leaves the
plan to pocketfft -- so LP64 is enough and ILP64 is not needed; the loop is
over BART's own `ptrdiff_t` strides. BLAS and LAPACK take `int` everywhere,
and BART range-checks each extent before a call (`checked_int`).

**CUDA is the same ABI.** `-DBARTORCH_CUDA=ON` compiles BART's thirteen `.cu`
files with nvcc and links cudart, cuFFT and cuBLAS dynamically, which are the
Expand Down Expand Up @@ -620,10 +626,22 @@ configure time. BART's own `NOEXEC_STACK` workaround does not help here: it
parses a trampoline layout GCC emits only for non-PIC executables, not for a
shared library. Both compilers are built and tested in CI.

Windows is not a platform here. BART does not build on it, and nothing in
this repository carries a path toward one: no `.dll` among the names the
loader tries, no `__declspec(dllexport)`, no `win32` branch picking a
different library. WSL2 is a Linux install and is the answer.
**Windows** is LLP64: `long` is 32 bits, so BART keeps every extent, stride
and flag set in the fixed-width types of `misc/dimtypes.h` (`bart_dim_t`,
`bart_stride_t`, `bart_flags_t`, all 64 bits), and the ABI spells them
`int64_t` and `uint64_t`; `api.c` asserts the two agree, and
`tests/test_abi.py` holds the header to having no `long` and `build_info()`'s
`long=` to the platform. The library is clang from MSYS2's CLANG64
environment: Blocks from the vendored runtime, linked in as objects because
clang declares it `dllimport`; everything the toolchain would bring as a DLL
linked statically; the Universal C Runtime official CPython uses. OpenMP is
torch's: torch loads Intel's `libiomp5md.dll`, LLVM's runtime refuses to start
beside it, and the two share an ABI, so the library links against an import
library made from `src/csrc/compat/libiomp5md.def` and the loader binds it to
the copy torch has loaded. A build without OpenMP there is refused unless
`-DBARTORCH_OPENMP=OFF` asks for it. BART's POSIX gaps -- `getsubopt`,
C11 threads, `SIGSTOP`, `SIGPIPE`, `readlink` -- are patches on the fork, and
named pipes are off (`NO_FIFO`). CPU only: there is no CUDA build on Windows.

The compiler's own runtime is linked statically on Linux, because otherwise
the toolchain's floor becomes the target system's: a GCC 14 build asks
Expand Down Expand Up @@ -1113,8 +1131,7 @@ of it is written. What comes back is finite stack memory. `_call`'s
`_PRECONDITIONS` refuses those combinations before the command runs, which is
where any further "BART does not define this" case belongs.

Windows is not on this list because it is not a target: BART does not build
there, and WSL2 is a Linux install like any other.
A CUDA build on Windows.

A tool that takes device memory as it stands. BART guards the host reads that
would break -- `estimate_im_dims` copies to the host when it is handed one --
Expand Down
65 changes: 60 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,16 @@ set(BART_EXTRA_LIBS "")
if(CMAKE_C_COMPILER_ID MATCHES "Clang")
list(APPEND BART_C_OPTIONS -fblocks)
set(BARTORCH_NESTED "clang-blocks")
if(NOT APPLE)
if(WIN32)
# clang declares the runtime dllimport on Windows, and the linker
# resolves an import to a definition in the same image only when that
# definition is already present: objects are, archive members are not
# pulled in for it.
add_library(blocksruntime OBJECT
external/blocksruntime/runtime.c
external/blocksruntime/data.c)
list(APPEND BART_EXTRA_LIBS $<TARGET_OBJECTS:blocksruntime>)
elseif(NOT APPLE)
add_library(blocksruntime STATIC
external/blocksruntime/runtime.c
external/blocksruntime/data.c)
Expand Down Expand Up @@ -109,6 +118,11 @@ endif()
# the way BART's own fallback does. Tested rather than guessed, because
# defining it where it does exist collides with the system header.
include(CheckCSourceCompiles)
# The data model, reported in build_info: long is 64 bits on LP64 (Linux,
# macOS) and 32 on LLP64 (Windows), and nothing BART or the ABI stores in an
# array's shape depends on it.
include(CheckTypeSize)
check_type_size(long BARTORCH_SIZEOF_LONG LANGUAGE C)
# Through <complex.h>, because that is where BART's translation units get it:
# glibc typedefs it in <bits/floatn-common.h>, which <complex.h> and <math.h>
# pull in and <stdio.h> does not on every glibc, and clang does not offer it as
Expand All @@ -125,7 +139,14 @@ endif()
# would carry the toolchain's floor into every target system -- a GCC 14 build
# asks for GCC_14.0.0 from libgcc_s, which no released distribution has -- and
# that is a load-time failure, not a fallback. Static leaves libc and libgomp.
if(NOT APPLE)
#
# On Windows the same goes for everything the MinGW toolchain would otherwise
# bring as a DLL -- winpthreads and the compiler's runtime -- because Python
# does not search PATH for a library's dependencies. What is left is the
# Universal C Runtime and the system's own libraries.
if(WIN32)
list(APPEND BART_LINK_OPTIONS -static)
elseif(NOT APPLE)
list(APPEND BART_LINK_OPTIONS -static-libgcc -static-libstdc++)
endif()

Expand Down Expand Up @@ -164,7 +185,28 @@ if(BARTORCH_CUDA)
message(STATUS "CUDA ${CUDAToolkit_VERSION}: architectures ${BARTORCH_CUDA_ARCH_SPEC}")
endif()

if(BARTORCH_OPENMP)
if(BARTORCH_OPENMP AND WIN32)
# torch loads Intel's OpenMP runtime on Windows, and LLVM's refuses to
# initialise beside it (OMP: Error #15), as on macOS. The two share an
# ABI, so the library is bound to the runtime torch has already loaded:
# clang compiles with -fopenmp, and the link is against an import library
# for libiomp5md.dll made from the entry points clang calls. The loader
# resolves that name to the copy in the process.
if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang")
message(FATAL_ERROR "OpenMP on Windows is built with clang; pass -DBARTORCH_OPENMP=OFF to build without it")
endif()
find_program(BARTORCH_DLLTOOL NAMES llvm-dlltool dlltool x86_64-w64-mingw32-dlltool REQUIRED)
set(_iomp_def "${CMAKE_CURRENT_SOURCE_DIR}/src/csrc/compat/libiomp5md.def")
set(_iomp_lib "${CMAKE_CURRENT_BINARY_DIR}/libiomp5md.dll.a")
add_custom_command(OUTPUT "${_iomp_lib}"
COMMAND "${BARTORCH_DLLTOOL}" -m i386:x86-64 -d "${_iomp_def}" -l "${_iomp_lib}" -D libiomp5md.dll
DEPENDS "${_iomp_def}"
VERBATIM)
add_custom_target(iomp5_import DEPENDS "${_iomp_lib}")
set(OpenMP_C_FOUND TRUE)
list(APPEND BART_C_OPTIONS -fopenmp)
list(APPEND BART_EXTRA_LIBS "${_iomp_lib}")
elseif(BARTORCH_OPENMP)
find_package(OpenMP COMPONENTS C)
if(OpenMP_C_FOUND)
list(APPEND BART_EXTRA_LIBS OpenMP::OpenMP_C)
Expand Down Expand Up @@ -230,7 +272,12 @@ file(GLOB _module_dirs LIST_DIRECTORIES true CONFIGURE_DEPENDS "${BART_SRC}/*")
foreach(_dir ${_module_dirs})
if(IS_DIRECTORY "${_dir}")
get_filename_component(_mod "${_dir}" NAME)
if(NOT _mod MATCHES "^(ismrm|lapacke|win)$")
# `win/` is BART's own port of mmap, fmemopen, getsubopt and the rest
# of what the Windows C runtime lacks, and is built there alone.
if(_mod STREQUAL "win" AND NOT WIN32)
continue()
endif()
if(NOT _mod MATCHES "^(ismrm|lapacke)$")
file(GLOB _srcs CONFIGURE_DEPENDS "${_dir}/*.c")
list(APPEND BART_SOURCES ${_srcs})
endif()
Expand Down Expand Up @@ -407,7 +454,11 @@ target_compile_definitions(bartorch PRIVATE
NO_PNG
_GNU_SOURCE
${BART_C_DEFS}
BARTORCH_BUILD_INFO="bart=${BART_VERSION},compiler=${CMAKE_C_COMPILER_ID}-${CMAKE_C_COMPILER_VERSION},nested=${BARTORCH_NESTED},openmp=${OpenMP_C_FOUND},cuda=${BARTORCH_CUDA}")
# Windows: a 64-bit off_t, so a file past 2 GiB can be mapped; BART's own
# functions linked into this library rather than imported from its DLL
# (misc/dllspec.h); and no named pipes, which are a POSIX file type.
$<$<BOOL:${WIN32}>:_FILE_OFFSET_BITS=64;BARTLIB_STATIC;NO_FIFO>
BARTORCH_BUILD_INFO="bart=${BART_VERSION},compiler=${CMAKE_C_COMPILER_ID}-${CMAKE_C_COMPILER_VERSION},nested=${BARTORCH_NESTED},openmp=${OpenMP_C_FOUND},cuda=${BARTORCH_CUDA},long=${BARTORCH_SIZEOF_LONG}")

target_compile_options(bartorch PRIVATE
$<$<COMPILE_LANGUAGE:C>:${BART_C_OPTIONS}>
Expand Down Expand Up @@ -436,6 +487,10 @@ target_link_libraries(bartorch PRIVATE Threads::Threads ${BART_EXTRA_LIBS})

target_link_libraries(bartorch PRIVATE m)

if(TARGET iomp5_import)
add_dependencies(bartorch iomp5_import)
endif()

set_target_properties(bartorch PROPERTIES
OUTPUT_NAME bartorch
PREFIX "lib")
Expand Down
8 changes: 6 additions & 2 deletions docs/guides/developer/prerequisites.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@ suite is run from `src/` without installing, install it separately
skip, because the substitution declining is an error. `pip install mkl
deepinv` enables the tests that need MKL and the DeepInverse adapter.

Windows is not a target: BART does not build on it, and WSL2 is used as a Linux
environment.
On Windows the compiler is clang from MSYS2's CLANG64 environment, with the
packages `mingw-w64-clang-x86_64-{clang,cmake,ninja,llvm-openmp,llvm-tools}`,
`clang64/bin` on `PATH` and `CMAKE_GENERATOR=Ninja`. The build links against
the OpenMP runtime PyTorch installs rather than MSYS2's, and loads into the
official CPython with nothing from MSYS2 at run time. There is no CUDA build on
Windows.
4 changes: 2 additions & 2 deletions docs/guides/developer/pull-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ template:
| Validation | The commands run and their results; for a numerical change, the reference and the tolerance; hardware or optional dependencies that were not available |
| Public API and documentation | Changes to public behaviour and the documentation updated for them |

CI runs the test matrix (Linux with clang and GCC 14, macOS, and a CUDA build
without a device), the lint and spelling checks, and two documentation builds:
CI runs the test matrix (Linux with GCC 14 and clang, macOS and Windows with
clang, each on Python 3.10 and 3.14, and a CUDA build without a device), the lint and spelling checks, and two documentation builds:
the reference without the compiled library, and the executed gallery, which is
published from `main` and from release tags. A pull request is merged when CI passes and a
maintainer has approved it.
4 changes: 2 additions & 2 deletions docs/guides/user/prerequisites.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| Requirement | Version |
| --- | --- |
| Python | 3.10 or later; wheels are tested on 3.10 to 3.14 |
| Python | 3.10 or later; each wheel is tested on 3.10 and 3.14 |
| PyTorch | 2.2 or later, CPU or CUDA build |
| NumPy, SciPy | NumPy 1.24 and SciPy 1.10 or later; MRI-NUFFT, a dependency, raises these to NumPy 2.2 and SciPy 1.13 |
| FINUFFT | 2.2 or later, installed as a dependency |
Expand All @@ -18,7 +18,7 @@
| macOS 11 or later on Apple silicon | Wheel | CPU only; see {ref}`macos-openmp` |
| Linux aarch64 | Source distribution | BART is compiled on installation; FINUFFT publishes no wheel for this platform and is built from source as well |
| macOS on Intel | Source distribution | BART is compiled on installation; FINUFFT releases after 2.4.0 have no wheel for this platform and are built from source as well |
| Windows | Not supported | BART does not build on Windows; WSL2 provides a Linux environment |
| Windows 10 or later on x86-64 | Wheel | CPU only; OpenMP is the runtime PyTorch installs |

A source installation needs the toolchain listed under {ref}`source-builds`.
Apple MPS devices are not supported; the device paths are CPU and CUDA.
Expand Down
2 changes: 1 addition & 1 deletion external/bart
Submodule bart updated 698 files
Loading
Loading