Backends and timings

Backends and timings#

The details behind Performance. The code is src/bartorch/_backend.py (the routine table), src/csrc/substitute/fft.cpp (BART’s FFT), src/csrc/substitute/finufft.c and src/bartorch/_finufft.py (FINUFFT’s levels, FFT and settings); the tests are tests/test_core.py, tests/test_fft.py and tests/test_finufft.py. Measurements of FINUFFT’s builds are recorded in docs/design/finufft-embedding.md.

Routine table#

_backend.install asks each source in turn for every routine the library declares, and takes the first address found. A source is a shared library (MKL’s libmkl_rt, PyTorch’s libtorch_cpu, Accelerate on macOS, and the symbols already in the process outside Windows) or SciPy’s Cython capsules, which hold the addresses of SciPy’s compiled OpenBLAS routines. The library carries reference BLAS for a few level-1 and level-2 routines and no LAPACK, so a LAPACK routine that no source exports is reported as missing.

What each order serves, from bartorch.backend_sources() on Linux x86-64 with the mkl extra installed (MKL 2026.1.0), counting the 30 BLAS and LAPACK routines and the two FFT entries:

BARTORCH_BLAS_LIBRARY

Served

unset, or mkl

all 32 by MKL

torch

14 by libtorch_cpu (13 routines and BART’s FFT), 18 by MKL

scipy

30 routines by SciPy, both FFTs by MKL

PyTorch links MKL statically and exports only the routines it calls itself, which is why MKL is preferred: it is the only source that covers every routine.

BART’s FFT#

BART plans through the FFTW guru interface. fft.cpp executes a plan with MKL’s DFTI: DFTI takes the transformed axes and the longest loop axis, and the remaining loop axes are walked in the library. DFTI is used through MKL’s LP64 interface, whose MKL_LONG is 32 bits on Windows; a length or stride beyond it leaves the loop axis to be walked, or the plan to the compiled-in pocketfft. A DFTI transform is limited to one thread (DFTI_THREAD_LIMIT), so it runs no thread team beside BART’s. Where no source has DFTI, as on macOS, the compiled-in transform serves every plan.

Recorded timings#

The figures below are recorded in the repository’s AGENTS.md; they are not regenerated by a test or by the documentation build.

BART’s tools on a 256 x 256 eight-coil dataset, with MKL against PyTorch’s library and SciPy:

ecalib

pics

svd 512

MKL

0.13 s

0.08 s

0.04 s

PyTorch and SciPy

0.29 s

0.08 s

0.04 s

The difference is in ecalib, whose per-voxel eigendecompositions are the LAPACK-heavy part; pics is bound by the FFT. On small arrays MKL is slower, because its own OpenMP runtime runs beside BART’s. MKL_THREADING_LAYER=GNU selects GNU OpenMP for MKL should a duplicate-runtime error appear.

FINUFFT’s instruction-set levels: at x86-64-v2 the transform takes 0.5 to 0.8 of the baseline’s time in 2D and 0.8 to 0.9 in 3D; x86-64-v3 and x86-64-v4 are within a few per cent of x86-64-v2 on the one processor measured. FINUFFT on oneMKL’s FFT is 10 to 35 per cent faster than on DUCC0 for a 2D transform and up to 15 per cent for a 3D one on an AVX-512 processor, less on AVX2.

The NUFFT grid on a 160³ volume with eight coils, four subspace coefficients and 3.5 million samples, at a tolerance an order below the error of BART’s gridder: an adjoint takes 6.7 s with FINUFFT choosing the grid (1.29 GB) or on a grid oversampled by 2 (1.45 GB), and 13.2 s on a grid oversampled by 1.25, where spreading dominates. At a tolerance of \(10^{-3}\) it takes 2.1 s.

On an RTX 4060 Laptop GPU with CUDA 12.8, pics on a 256 x 256 eight-coil radial dataset of 401 spokes takes 0.17 to 0.25 s with the point spread function and 0.11 to 0.28 s with the forward and adjoint transforms, against 2.2 to 2.4 s and 3.1 to 3.8 s on the same machine’s CPU, best of five. One, two and four CUDA streams give the same time within the spread of repetitions.