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:
|
Served |
|---|---|
unset, or |
all 32 by MKL |
|
14 by |
|
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.