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
17 changes: 11 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ jobs:
# dependency here.
CIBW_ARCHS_LINUX: x86_64
CIBW_ARCHS_WINDOWS: AMD64
# libgomp.so.1 is the one torch carries in torch/lib, so the wheel
# vendors none and the process has one OpenMP runtime.
CIBW_REPAIR_WHEEL_COMMAND_LINUX: auditwheel repair --exclude libgomp.so.1 -w {dest_dir} {wheel}
# 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
Expand Down Expand Up @@ -125,7 +128,7 @@ jobs:
grep -q 'licenses/external/finufft/LICENSE' contents.txt
grep -q 'licenses/finufft-dependencies/DUCC0/LICENSE' contents.txt
grep -q 'licenses/finufft-dependencies/xsimd/LICENSE' contents.txt
! grep -Ei 'finufft/.*\.(so|dylib|dll)|libomp|libiomp' contents.txt
! grep -Ei 'finufft/.*\.(so|dylib|dll)|libomp|libiomp|libgomp' contents.txt
mkdir unpacked && python -m zipfile -e wheelhouse/*.whl unpacked
case "${{ runner.os }}" in
Linux)
Expand Down Expand Up @@ -230,23 +233,25 @@ jobs:
python -m build --wheel --outdir dist/ \
-C cmake.define.BARTORCH_CUDA=ON \
-C "cmake.define.BARTORCH_MATHDX_DIR=$MATHDX_DIR"
# The CUDA runtimes are excluded rather than vendored: torch already
# brings them, and bundling them would add hundreds of megabytes.
# The CUDA runtimes and libgomp are excluded rather than vendored: torch
# already brings them, bundling the first would add hundreds of
# megabytes, and a second libgomp would be a second OpenMP runtime.
- name: Repair to a manylinux tag
run: |
/opt/python/cp312-cp312/bin/python -m pip install auditwheel
LD_LIBRARY_PATH=/usr/local/cuda-12.8/lib64 \
/opt/python/cp312-cp312/bin/python -m auditwheel repair dist/*.whl \
-w wheelhouse/ --plat manylinux_2_28_x86_64 \
--exclude libcudart.so.12 --exclude libcufft.so.11 \
--exclude libcublas.so.12 --exclude libcublasLt.so.12
--exclude libcublas.so.12 --exclude libcublasLt.so.12 \
--exclude libgomp.so.1
# cuFINUFFT is in the library, bound to the same cudart and cuFFT as
# BART's kernels, and nothing is vendored beside it but libgomp.
# BART's kernels, and nothing is vendored beside it.
- name: Inspect the CUDA wheel
run: |
/opt/python/cp312-cp312/bin/python -m auditwheel show wheelhouse/*.whl
/opt/python/cp312-cp312/bin/python -m zipfile -l wheelhouse/*.whl | tee contents.txt
! grep -Ei 'libcudart|libcufft|libcublas|cufinufft.*\.so' contents.txt
! grep -Ei 'libcudart|libcufft|libcublas|libgomp|cufinufft.*\.so' contents.txt
grep -q 'licenses/finufft-dependencies/DUCC0/LICENSE' contents.txt
# The wheel carries no interpreter, so it is loaded from the oldest and
# the newest Python the package supports.
Expand Down
41 changes: 23 additions & 18 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,18 +222,23 @@ as for BART's kernels. cuFINUFFT at this pin does not compile for sm_90 with CU
takes for that architecture arrives later), so a CUDA build that includes 90
needs 12.1 or newer; the wheel is built with 12.8.

**One OpenMP runtime, and it is torch's where that matters.**
**One OpenMP runtime, and it is torch's.**
`cmake/openmp.cmake` defines `OpenMP::OpenMP_C` and `OpenMP::OpenMP_CXX` for
the whole build, BART linking the first and FINUFFT the second, and answers
FINUFFT's own `find_package(OpenMP)` through `CMAKE_FIND_PACKAGE_REDIRECTS_DIR`
so it cannot pick a runtime of its own. On Linux it is the toolchain's
runtime. On Windows and macOS LLVM's runtime ends the process when a second
so it cannot pick a runtime of its own. On Linux the build links the
toolchain's libgomp, whose `libgomp.so.1` is also the name of the copy torch
carries in `torch/lib` from 2.7.1 on -- before that it had a hashed name,
hence the Linux floor in `pyproject.toml` -- so the copy torch has loaded
satisfies the library's `NEEDED` entry; the wheel is repaired with
`--exclude libgomp.so.1` and carries none, and `$ORIGIN/../torch/lib` is on
its runpath. On Windows and macOS LLVM's runtime ends the process when a second
copy initialises (`OMP: Error #15`), and torch has already loaded one, so the
library is linked against a stub that names torch's and lists the entry
points clang calls: an import library for `libiomp5md.dll` made from
`src/csrc/compat/libiomp5md.def`, and a text stub for `@rpath/libomp.dylib`
written from the same list, with `@loader_path/../torch/lib` on the rpath.
`_lib._load` imports torch before it loads the library on both. clang 19
`_lib._load` imports torch before it loads the library on every platform. clang 19
and later end a dynamically scheduled loop with `__kmpc_dispatch_deinit`,
which the libomp in torch 2.3 to 2.5 does not export, so
`src/csrc/substitute/openmp.c` answers it inside the library and forwards to
Expand All @@ -242,7 +247,7 @@ macOS, hence the platform floor in `pyproject.toml`. macOS needs
`omp.h` to compile, which Homebrew's `libomp` provides; nothing links against
that copy. `tests/test_openmp.py` runs threaded torch and threaded FINUFFT in
one process in either import order and reads the loaded images: one runtime
on macOS and Windows, and it is torch's, with no installed file changed.
on every platform, and it is torch's, with no installed file changed.
`KMP_DUPLICATE_LIB_OK` is not used.

**Underneath BART's own tools the seam is `nufft_create`, not the gridder.**
Expand Down Expand Up @@ -314,7 +319,7 @@ BART's gridder along with everything else in that file. So `compute_psf`,
`compute_psf2` and `compute_psf2_decomposed` are renamed too and `src/csrc/substitute/psf.c`
answers to them: the same squared weights and basis, the same doubled grid,
shifts and decomposition, with the transform in the middle being whichever
`nufft_create2` answers. `nlinv`, `moba`, `rtnlinv`, `noir/model2` and the
`nufft_create2` answers. `nlinv`, `moba`, `noir/model2` and the
`psf` tool call these directly.

What that is worth is in the numbers. On the un-doubled grid of a 16x16
Expand Down Expand Up @@ -612,8 +617,8 @@ points that read an array element by element rather than through `md_` --
taking a median of k-space -- are answered in `src/csrc/abi/host_reads.c` over a host
copy of that one array, which is what BART already does for its own virtual
pointers. What cannot be reached that way is a tool that allocates a temporary
of its own on the host and mixes it with its input: `pocsense` takes its
pattern from `md_alloc`, `nlinv` from `anon_cfl`, `ecalib` sets `bart_use_gpu`
of its own on the host and mixes it with its input: `nlinv` takes its
pattern from `anon_cfl`, `ecalib` sets `bart_use_gpu`
from its own flag. `md_` operations take the host path unless every argument
is on a device, and take it silently, so that is a segmentation fault rather
than a slower answer.
Expand Down Expand Up @@ -893,8 +898,8 @@ declared it.

An input file that is not there is the one thing the command line names itself.
A BART command that fails while loading its arguments leaves the library unable
to serve the next call in the same process -- `ecalib`, `nufft` and `pocsense`
handed a name with no file behind it all spin the call after them, while `fft`
to serve the next call in the same process -- `ecalib` and `nufft`
handed a name with no file behind it both spin the call after them, while `fft`
does not -- so a caller who runs `main` twice would hang rather than see the
second answer. `cli._missing` checks the names against the filesystem before
BART is asked.
Expand Down Expand Up @@ -926,10 +931,10 @@ reads no residual, and `iter2_pocs` is handed an `xupdate_op` that `pocs`
never calls. What the sets are belongs to the projections, so `apps.pocsense`
builds the application's three out of `linop.Sampling`, the range of a
`linop.CartesianSense`, and a `priors` term conjugated by the transform
between the samples and the coil images. The three are bit-identical to
`tools.pocsense` on a grid, in two dimensions and in three, on an even grid
and on an odd one. The odd grid is what decides how that projection is
written: there the modulation is a phase rather than a sign, so the scaling
between the samples and the coil images, and `tests/test_apps.py` holds a
fully sampled phantom to be a fixed point of them and an answer to lie in the
range of the coils. The odd grid is what decides how the sparsity projection
is written: there the modulation is a phase rather than a sign, so the scaling
has to ride in the same array BART puts it in rather than in a second
multiply, and the multiply itself has to be BART's `md_zmul2` and `md_zmulc2`
-- a `linop.Diagonal` and its adjoint -- rather than torch's. A complex
Expand Down Expand Up @@ -1101,13 +1106,13 @@ reconstruction is held to its data rather than to its transform. Two
exceptions: a caller who names an upsampling gets it -- `nufft_conf_s` carries
two as BART's own default, so zero is what says nobody asked, and BART gets
its two back before it sees the conf -- and the tools that calibrate before a
reconstruction is attempted (`nlinv`, `rtnlinv`, `ncalib`, in `_CALIBRATES`)
reconstruction is attempted (`nlinv` and `ncalib`, in `_CALIBRATES`)
run at FINUFFT's own tolerance on the textbook grid, which costs a few
megabytes at the resolution they fit sensitivities at.

Every BART entry point that builds a NUFFT is served: `nufft` forward,
adjoint, inverse and Toeplitz, `pics` with and without a pattern, `sqpics`,
`nlinv`, `rtnlinv`, `moba`, `ncalib` and `linop.NUFFT`, on the host
adjoint, inverse and Toeplitz, `pics` with and without a pattern,
`nlinv`, `moba`, `ncalib` and `linop.NUFFT`, on the host
and on the card, with BART's own gridder built zero times. `nlinv` and the
network models build theirs against dimensions alone and hand the trajectory
over afterwards, which is why `nufft_update_traj` installs one rather than
Expand All @@ -1121,7 +1126,7 @@ index sets that are not axes (channels, parameter maps) are tuples too; no
public argument takes a bitmask or a `-R` string. Hand-written wrappers
convert their own; derived wrappers and what a curated one passes through by
name follow `_call.TRANSLATED`, and `test_tools` fails on a dimension-like
argument that is in neither. `pics`, `sqpics`, `wshfl` and `moba` take
argument that is in neither. `pics`, `wshfl` and `moba` take
`bartorch.priors` terms, serialized by `Regularizer._argument`. A flag's value can be an
array rather than a number -- `pics(kspace, maps, t=traj)`, `-p` for a
sampling pattern, `-B` for a basis -- and is registered and copied like any
Expand Down
18 changes: 15 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ option(BARTORCH_CUDA "Build BART's CUDA kernels" OFF)
set(BARTORCH_CUDA_ARCHITECTURES "75;80;86;89;90" CACHE STRING
"Architectures to emit SASS for; PTX for the last one is added for newer cards")

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD 23)
set(CMAKE_C_EXTENSIONS ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_CUDA_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)

if(NOT CMAKE_BUILD_TYPE)
Expand Down Expand Up @@ -171,7 +172,7 @@ if(BARTORCH_CUDA)
enable_language(CUDA)
find_package(CUDAToolkit REQUIRED)

list(APPEND BART_C_DEFS USE_CUDA CUDA_GET_CUDA_DEVICE_NUM)
list(APPEND BART_C_DEFS USE_CUDA USE_GPU CUDA_GET_CUDA_DEVICE_NUM)

set(CMAKE_CUDA_STANDARD 17)
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
Expand Down Expand Up @@ -269,7 +270,7 @@ file(GLOB _tool_srcs CONFIGURE_DEPENDS "${BART_SRC}/*.c")
list(APPEND BART_SOURCES ${_tool_srcs})

if(BARTORCH_CUDA)
foreach(_dir calib motion noncart num wavelet)
foreach(_dir calib motion noncart num simu wavelet)
file(GLOB _cu CONFIGURE_DEPENDS "${BART_SRC}/${_dir}/*.cu")
list(APPEND BART_SOURCES ${_cu})
endforeach()
Expand Down Expand Up @@ -467,6 +468,10 @@ target_compile_definitions(bartorch PRIVATE
# 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>
# Elsewhere misc/dllspec.h gives BARTLIB_API default visibility, which
# would put BART's seq_*, num_rand_init and debug_printf in the dynamic
# table beside the bartorch_* ABI; defining its guard leaves them hidden.
$<$<NOT:$<BOOL:${WIN32}>>:BART_MISC_DLLSPEC;BARTLIB_API=;BARTLIB_CALL=>
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},finufft=${BARTORCH_FINUFFT_VERSION},finufft_fft=${BARTORCH_FINUFFT_FFT_NAME},finufft_simd=${_finufft_simd_info},finufft_simd_mkl=${_finufft_mkl_info},long=${BARTORCH_SIZEOF_LONG}")

target_compile_options(bartorch PRIVATE
Expand Down Expand Up @@ -506,6 +511,13 @@ if(TARGET bartorch_mkl_fftw)
set_property(TARGET bartorch APPEND PROPERTY INSTALL_RPATH "${BARTORCH_MKL_LIBRARY_DIR}")
endif()

# Linux: libgomp.so.1 is torch's in a wheel, which carries none of its own;
# torch is its sibling package. The copy torch has loaded satisfies the name
# first, so this is where it is found when torch has not been imported.
if(NOT APPLE AND NOT WIN32 AND OpenMP_C_FOUND)
set_property(TARGET bartorch APPEND PROPERTY INSTALL_RPATH "$ORIGIN/../torch/lib")
endif()

# macOS: the OpenMP stub names @rpath/libomp.dylib, which is torch's. In a
# wheel torch is the library's sibling package; an in-place build finds it
# where the interpreter that configured the build has it.
Expand Down
4 changes: 3 additions & 1 deletion cmake/openmp.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
# of the library are bound to the same runtime.
#
# Linux: the toolchain's own runtime, found by CMake's FindOpenMP -- libgomp
# under GCC, libomp under clang.
# under GCC, libomp under clang. libgomp.so.1 is also the name of the copy
# torch carries, so the copy torch has loaded satisfies the library's NEEDED
# entry; the wheel is repaired with --exclude libgomp.so.1 and carries none.
#
# Windows and macOS: the runtime torch carries and has already loaded. LLVM's
# runtime ends the process when a second copy of itself initialises (OMP: Error
Expand Down
2 changes: 1 addition & 1 deletion docs/api/apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ runs an app in place of the BART command where one exists.
| Object | BART command | Relation to the command |
| --- | --- | --- |
| {obj}`~bartorch.apps.pics` | `pics` | Identical output on a Cartesian grid; agrees to floating-point round-off along a trajectory |
| {obj}`~bartorch.apps.pocsense` | `pocsense` | Identical output |
| {obj}`~bartorch.apps.pocsense` | none | The POCSENSE projections swept by BART's `pocs` iteration |
| {obj}`~bartorch.apps.mobafit` | `mobafit` | Same Gauss-Newton method over a TorchSim model; returns named parameter maps in physical units |
3 changes: 0 additions & 3 deletions docs/api/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,10 @@ interface.
| --- | --- |
| {obj}`~bartorch.tools.pics` | Parallel-imaging compressed-sensing reconstruction (wrapped) |
| {obj}`~bartorch.tools.nlinv` | Nonlinear inversion: image and sensitivities jointly (wrapped) |
| {obj}`~bartorch.tools.rtnlinv` | Real-time nonlinear inversion of a time series |
| {obj}`~bartorch.tools.moba` | Model-based nonlinear reconstruction of parameter maps |
| {obj}`~bartorch.tools.mobafit` | Voxel-wise fit of a signal model to contrast images |
| {obj}`~bartorch.tools.looklocker` | $T_1$ from Look-Locker parameters $M_0$, $M_{ss}$ and $R_1^*$ |
| {obj}`~bartorch.tools.itsense` | Iterative SENSE with $\ell_2$ regularization |
| {obj}`~bartorch.tools.sqpics` | Parallel-imaging compressed-sensing reconstruction, BART's `sqpics` variant of `pics` |
| {obj}`~bartorch.tools.pocsense` | POCSENSE reconstruction |
| {obj}`~bartorch.tools.sake` | SAKE: low-rank matrix completion of k-space |
| {obj}`~bartorch.tools.lrmatrix` | Multi-scale low-rank matrix completion |
| {obj}`~bartorch.tools.homodyne` | Homodyne partial-Fourier reconstruction |
Expand Down
10 changes: 6 additions & 4 deletions docs/design/finufft-embedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ libbartorch (shared; exports the bartorch_* C ABI only)
│ └── CUDA::cudart, CUDA::cufft ── dynamic, from the nvidia wheels torch brings
└── OpenMP::OpenMP_C ────────────┤
└── cmake/openmp.cmake
Linux the toolchain's runtime (libgomp with GCC)
Linux libgomp.so.1 = torch/lib/libgomp.so.1
macOS @rpath/libomp.dylib = torch/lib/libomp.dylib
Windows libiomp5md.dll, as torch loads it
```
Expand Down Expand Up @@ -110,9 +110,11 @@ runtime's own when the process has one. torch 2.2 carries no libomp on macOS,
so the macOS floor is 2.3.

`tests/test_openmp.py` is the check: threaded torch and threaded FINUFFT in
one process, both import orders, the loaded OpenMP images listed. On Linux
the one image found is torch's `libgomp.so.1`, which satisfies the library's
`NEEDED` entry as well.
one process, both import orders, the loaded OpenMP images listed, and one
image found on every platform. On Linux it is torch's `libgomp.so.1`, which
satisfies the library's `NEEDED` entry; the wheel is repaired with
`--exclude libgomp.so.1`, and torch's copy has carried that name from 2.7.1
on, which is the Linux floor.

## FFT inside FINUFFT: DUCC0 and oneMKL

Expand Down
4 changes: 2 additions & 2 deletions docs/explanation/non-cartesian.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ chooses its kernel width.

| Setting | Default | Set by |
| --- | --- | --- |
| Tolerance $\varepsilon$ | $10^{-3}$; $10^{-6}$ for `ncalib`, `nlinv` and `rtnlinv` | The kernel width: `width` of {class}`~bartorch.linop.NUFFT`, BART's `-w` option of the commands, converted to the tolerance that gives that width |
| Oversampling $\sigma$ | 1.25; 2 for `ncalib`, `nlinv` and `rtnlinv` | `oversampling` of {class}`~bartorch.linop.NUFFT`, BART's `-o` option |
| Tolerance $\varepsilon$ | $10^{-3}$; $10^{-6}$ for `ncalib` and `nlinv` | The kernel width: `width` of {class}`~bartorch.linop.NUFFT`, BART's `-w` option of the commands, converted to the tolerance that gives that width |
| Oversampling $\sigma$ | 1.25; 2 for `ncalib` and `nlinv` | `oversampling` of {class}`~bartorch.linop.NUFFT`, BART's `-o` option |

The relative error of a transform is of the order of $\varepsilon$; the test
suite compares it with the explicit sum above. The default trades accuracy
Expand Down
13 changes: 7 additions & 6 deletions docs/guides/user/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Install the PyTorch build for the target device first, with the command the
[PyTorch installation selector](https://pytorch.org/get-started/locally/)
gives for the CPU or for a CUDA version. bartorch requires PyTorch 2.2 or
later; installing bartorch into an environment without PyTorch installs the
later (2.3 on macOS, 2.7.1 on Linux); installing bartorch into an environment without PyTorch installs the
default build from PyPI.

## bartorch installation
Expand Down Expand Up @@ -146,12 +146,13 @@ and the configurations that are refused.
## OpenMP runtime

BART and FINUFFT are compiled with OpenMP and share one runtime with PyTorch.
On macOS and Windows the library is linked against the runtime PyTorch
contains (`torch/lib/libomp.dylib`, `torch/lib/libiomp5md.dll`) and contains
none of its own: the LLVM runtime terminates the process when a second copy
The library is linked against the runtime PyTorch contains
(`torch/lib/libgomp.so.1`, `torch/lib/libomp.dylib`,
`torch/lib/libiomp5md.dll`) and the wheels contain none of their own; on macOS
and Windows the LLVM runtime terminates the process when a second copy
initializes (`OMP: Error #15`). No installed file is modified, and
`KMP_DUPLICATE_LIB_OK` is not used. On Linux the library uses the compiler's
runtime, `libgomp` for the GCC-built wheel.
`KMP_DUPLICATE_LIB_OK` is not used. On Linux this needs PyTorch 2.7.1 or
later, the first release whose copy is named `libgomp.so.1`.

## Command-line interface

Expand Down
Loading
Loading