# Backends and timings

The details behind {doc}`../../../explanation/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 {func}`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.
