Skip to content

Commit 0f12fac

Browse files
author
James Nightingale
authored
Merge pull request #655 from PyAutoLabs/feature/finufft-cpu-nufft-backend
feat: finufft as the CPU NUFFT backend (JAX-free NumPy interferometer runs)
2 parents d99ec2b + 3f3c861 commit 0f12fac

11 files changed

Lines changed: 966 additions & 127 deletions

File tree

‎autoarray/dataset/interferometer/dataset.py‎

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -551,8 +551,11 @@ def apply_sparse_operator(
551551
the inversion module.
552552
553553
The backend the operator is *applied* with follows the inversion's `xp` (NumPy/scipy for
554-
`xp=np`, JAX for `xp=jnp`); `use_jax` below only selects which brute-force builder computes
555-
the preload, and never puts JAX on a NumPy fit's application path.
554+
`xp=np`, JAX for `xp=jnp`); `use_jax` below only selects the backend that *builds* the
555+
preload, and never puts JAX on a NumPy fit's application path. With the default
556+
`use_jax=False` the whole call -- precision operator and dirty image, both on `finufft` --
557+
never imports JAX, so a NumPy fit does not start XLA before a multiprocessing search forks
558+
its workers.
556559
557560
The default builder (`method="nufft"`) computes the precision operator as a type-1 NUFFT, so
558561
it costs `O(N_vis * nspread^2 + M log M)` for `M = 4 * Ny * Nx` — seconds even at a million
@@ -589,11 +592,11 @@ def apply_sparse_operator(
589592
show_memory
590593
If `True`, memory usage statistics are printed while computing the NUFFT precision matrix.
591594
use_jax
592-
Only honoured when a brute-force builder is selected: `method="numpy"` with
593-
`use_jax=True` runs the JAX brute force (equivalent to `method="jax"`). Under the
594-
default `method="nufft"` it is ignored, because the NUFFT already runs on JAX --
595-
so an existing `use_jax=True` call keeps the fast path rather than being demoted
596-
to the `O(N_vis * N_pix)` brute force.
595+
Selects the backend that builds the precision operator. Under the default
596+
`method="nufft"`, `False` runs the type-1 NUFFT on `finufft` (CPU, no JAX) and
597+
`True` on `nufftax` (JAX); both are fast paths. Under `method="numpy"`, `True` runs
598+
the JAX brute force (equivalent to `method="jax"`). The dirty image is always formed
599+
on the NumPy path (`finufft`), whatever this says.
597600
598601
`PYAUTO_DISABLE_JAX=1` overrides this to `False`. That variable is a
599602
harness-level switch, not a preference: it is the documented way to force the
@@ -602,7 +605,8 @@ def apply_sparse_operator(
602605
explicit `use_jax=True` in a script -- which is the right thing for a script
603606
demonstrating the production path to say -- must therefore not defeat it, or
604607
the harness pays 2.3-3.2 s of compile for a backend it asked to disable. (The
605-
same switch demotes the `"nufft"` builder to the NumPy brute force inside
608+
`"nufft"` builder then runs on `finufft`, which is not JAX; only if `finufft` is
609+
missing does the switch demote it to the NumPy brute force inside
606610
`nufft_precision_operator_from`.)
607611
pool_noise_map
608612
If `True`, pool the real and imaginary noise sigma of every visibility in
@@ -653,9 +657,9 @@ def apply_sparse_operator(
653657
is `False`.
654658
"""
655659

656-
# `use_jax` now only selects between the two brute forces (the `"nufft"` builder
657-
# runs on JAX whatever it says), so this clears it before it can upgrade
658-
# `method="numpy"` to the JAX brute force under the kill switch.
660+
# `use_jax` selects the builder's backend (`finufft` vs `nufftax` under `"nufft"`, the
661+
# NumPy vs JAX brute force under `"numpy"`), so this clears it before it can put the
662+
# build on JAX under the kill switch.
659663
if disable_jax():
660664
use_jax = False
661665

@@ -992,9 +996,10 @@ def psf_precision_operator_from(
992996
show_memory
993997
If `True`, memory usage statistics are printed during computation.
994998
use_jax
995-
Only honoured when a brute-force builder is selected: `method="numpy"` with
996-
`use_jax=True` runs the JAX brute force (equivalent to `method="jax"`). It is
997-
ignored under the default `method="nufft"`, which already runs on JAX.
999+
Selects the backend that builds the operator. Under the default `method="nufft"`,
1000+
`False` runs the type-1 NUFFT on `finufft` (CPU, never imports JAX) and `True` on
1001+
`nufftax` (JAX). Under `method="numpy"`, `True` runs the JAX brute force
1002+
(equivalent to `method="jax"`).
9981003
method
9991004
Which builder computes the operator: `"nufft"` (default), `"numpy"` or `"jax"`.
10001005
eps

‎autoarray/inversion/inversion/interferometer/inversion_interferometer_util.py‎

Lines changed: 79 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from autoarray import exc
1212

13+
from autoarray.operators import nufft_cpu
1314
from autoarray.operators.transformer import _load_nufftax, nufftax_exception
1415

1516
try:
@@ -474,25 +475,31 @@ def nufft_precision_operator_from(
474475
475476
- `"nufft"` (default) -- `nufft_precision_operator_via_nufft_from`, the type-1
476477
(adjoint) NUFFT. `O(K*nspread^2 + M log M)` for `M = 4*Ny*Nx`, i.e. seconds
477-
where the brute-force builders take minutes to hours. Needs `nufftax`.
478+
where the brute-force builders take minutes to hours. Runs `finufft` (CPU, no
479+
JAX) by default and `nufftax` (JAX) with `use_jax=True`.
478480
- `"numpy"` -- `nufft_precision_operator_via_np_from`, the brute-force
479481
`O(N_pix*K)` reference builder. Kept as the reference the NUFFT builder is
480482
pinned against, and used as the fallback below.
481483
- `"jax"` -- `nufft_precision_operator_via_jax_from`, the same brute force on
482484
JAX. `method="jax"` is the explicit way to ask for it.
483485
484-
`use_jax` is only honoured when a brute-force method is selected: it upgrades
485-
`method="numpy"` to `method="jax"` and is otherwise ignored. Under the default
486-
`method="nufft"` it does nothing, because the NUFFT already runs on JAX -- an
487-
existing `use_jax=True` caller therefore keeps the fast path rather than being
488-
demoted to the `O(N_pix*K)` brute force.
486+
`use_jax` picks the backend of the chosen method: under the default
487+
`method="nufft"`, `use_jax=False` runs the type-1 NUFFT on `finufft` (CPU, never
488+
imports JAX, so a NumPy fit does not start XLA before a multiprocessing search
489+
forks its workers) and `use_jax=True` runs it on `nufftax` (JAX); under
490+
`method="numpy"`, `use_jax=True` upgrades to `method="jax"`. Either way an
491+
existing `use_jax=True` caller keeps a NUFFT fast path rather than being demoted
492+
to the `O(N_pix*K)` brute force.
489493
490494
Two fallbacks to `"numpy"` are taken, both logged loudly (never silently),
491495
because they cost `O(N_pix*K)` where the NUFFT is `O(K*nspread^2 + M log M)`:
492496
493-
1. `disable_jax()` is true (`PYAUTO_DISABLE_JAX=1`, the test-mode kill switch).
494-
Both `"nufft"` and `"jax"` run on JAX, so both are demoted.
495-
2. `nufftax` is not importable, so `"nufft"` cannot run.
497+
1. `disable_jax()` is true (`PYAUTO_DISABLE_JAX=1`, the test-mode kill switch)
498+
and the method would run on JAX: `"jax"`, or `"nufft"` with `use_jax=True`
499+
(or with `finufft` missing, where the NumPy NUFFT falls back to `nufftax`).
500+
`"nufft"` on `finufft` is not JAX, so it is not demoted.
501+
2. No NUFFT backend for the requested route is importable: `nufftax` for
502+
`use_jax=True`, or neither `finufft` nor `nufftax` for `use_jax=False`.
496503
497504
Any other `method` raises `ValueError`.
498505
@@ -519,9 +526,9 @@ def nufft_precision_operator_from(
519526
chunk_k
520527
The visibility chunk size of the two brute-force builders.
521528
use_jax
522-
Only honoured when a brute-force method is selected: `method="numpy"` with
523-
`use_jax=True` runs the JAX brute force (equivalent to `method="jax"`). It is
524-
ignored under the default `method="nufft"`, which already runs on JAX.
529+
Under `method="nufft"`, `False` (default) runs the NUFFT on `finufft` and
530+
`True` on `nufftax` (JAX). Under `method="numpy"`, `True` runs the JAX brute
531+
force (equivalent to `method="jax"`).
525532
"""
526533
if method == "numpy" and use_jax:
527534
method = "jax"
@@ -533,23 +540,32 @@ def nufft_precision_operator_from(
533540
"brute-force reference builders)."
534541
)
535542

536-
if method in ("nufft", "jax") and disable_jax():
543+
# The NumPy NUFFT runs on JAX only when finufft is missing and it falls back to nufftax.
544+
nufft_on_jax = use_jax or nufft_cpu._load_finufft() is None
545+
546+
if (method == "jax" or (method == "nufft" and nufft_on_jax)) and disable_jax():
537547
logger.warning(
538548
f"INTERFEROMETER - `PYAUTO_DISABLE_JAX=1` is set, so the NUFFT precision "
539-
f"operator cannot be built with method={method!r} (both the NUFFT and the "
540-
f"JAX brute force run on JAX). Falling back to the NumPy brute force, which "
549+
f"operator cannot be built with method={method!r} on JAX (use_jax={use_jax}, "
550+
f"or `finufft` is not installed). Falling back to the NumPy brute force, which "
541551
f"is O(N_pix * K) rather than O(K * nspread^2 + M log M) and can take "
542552
f"minutes to hours on a real dataset."
543553
)
544554
method = "numpy"
545555

546-
if method == "nufft" and _load_nufftax() is None:
556+
# nufftax is only looked for on the JAX route; the NumPy route checks for a backend
557+
# without importing nufftax (which would start JAX).
558+
if method == "nufft" and (
559+
(use_jax and _load_nufftax() is None)
560+
or (not use_jax and not nufft_cpu.backend_available())
561+
):
547562
logger.warning(
548-
"INTERFEROMETER - `nufftax` is not installed, so the NUFFT precision "
549-
"operator cannot be built with the type-1 NUFFT. Falling back to the NumPy "
550-
"brute force, which is O(N_pix * K) rather than O(K * nspread^2 + M log M) "
551-
"and can take minutes to hours on a real dataset. Install it via "
552-
"`pip install nufftax`."
563+
f"INTERFEROMETER - No NUFFT backend is installed for use_jax={use_jax} "
564+
f"(`nufftax` for use_jax=True, `finufft` or `nufftax` for use_jax=False), so "
565+
f"the NUFFT precision operator cannot be built with the type-1 NUFFT. Falling "
566+
f"back to the NumPy brute force, which is O(N_pix * K) rather than "
567+
f"O(K * nspread^2 + M log M) and can take minutes to hours on a real dataset. "
568+
f"Install it via `pip install finufft` (or `pip install nufftax` for JAX)."
553569
)
554570
method = "numpy"
555571

@@ -561,6 +577,7 @@ def nufft_precision_operator_from(
561577
grid_radians_2d=grid_radians_2d,
562578
eps=eps,
563579
chunk_size=chunk_size,
580+
use_jax=use_jax,
564581
)
565582

566583
if method == "jax":
@@ -666,6 +683,7 @@ def _type1_real_grid_from(
666683
centred: bool = False,
667684
zero_nyquist: bool = False,
668685
out: Optional[np.ndarray] = None,
686+
use_jax: bool = False,
669687
) -> np.ndarray:
670688
"""
671689
The real part of a **type-1 (adjoint) NUFFT** of per-visibility `values` onto a regular
@@ -713,18 +731,23 @@ def _type1_real_grid_from(
713731
which is returned; nothing of the grid's size is allocated beyond `nufft2d1`'s own output
714732
and its real part per chunk. This is how `sparse_terms_from_chunks` accumulates the
715733
fine grids across a stream. If `None` a new array is returned.
734+
use_jax
735+
If `False` (default) the transform runs on `finufft` via `nufft_cpu` (CPU, never imports
736+
JAX); if `True` it runs on `nufftax` (JAX). The two agree to ~1e-14 relative and return
737+
the same NumPy array.
716738
717739
Returns
718740
-------
719741
np.ndarray
720742
`[N_y, N_x]` float64.
721743
"""
722-
nufftax = _load_nufftax()
744+
if use_jax:
745+
nufftax = _load_nufftax()
723746

724-
if nufftax is None:
725-
nufftax_exception()
747+
if nufftax is None:
748+
nufftax_exception()
726749

727-
import jax.numpy as jnp
750+
import jax.numpy as jnp
728751

729752
uv_wavelengths = np.asarray(uv_wavelengths, dtype=np.float64)
730753

@@ -761,14 +784,24 @@ def _type1_real_grid_from(
761784
for k0 in range(0, total_visibilities, chunk_size):
762785
k1 = min(total_visibilities, k0 + chunk_size)
763786

764-
f = nufftax.nufft2d1(
765-
jnp.asarray(-x[k0:k1]),
766-
jnp.asarray(y[k0:k1]),
767-
jnp.asarray(values[k0:k1], dtype=jnp.complex128),
768-
n_modes,
769-
eps,
770-
1,
771-
)
787+
if use_jax:
788+
f = nufftax.nufft2d1(
789+
jnp.asarray(-x[k0:k1]),
790+
jnp.asarray(y[k0:k1]),
791+
jnp.asarray(values[k0:k1], dtype=jnp.complex128),
792+
n_modes,
793+
eps,
794+
1,
795+
)
796+
else:
797+
f = nufft_cpu.nufft2d1(
798+
-x[k0:k1],
799+
y[k0:k1],
800+
np.asarray(values[k0:k1], dtype=np.complex128),
801+
n_modes,
802+
eps,
803+
1,
804+
)
772805

773806
if real_modes is not None:
774807
real_modes += np.asarray(np.real(f), dtype=np.float64)
@@ -803,6 +836,7 @@ def nufft_precision_operator_via_nufft_from(
803836
*,
804837
eps: float = 1.0e-12,
805838
chunk_size: Optional[int] = None,
839+
use_jax: bool = False,
806840
) -> np.ndarray:
807841
"""
808842
Returns the `W~` precision operator built as the real part of a **type-1 (adjoint)
@@ -833,7 +867,8 @@ def nufft_precision_operator_via_nufft_from(
833867
P[i, j] = Re sum_k w_k exp(i(-j * x_k + i * y_k))
834868
835869
i.e. the real part of a type-1 NUFFT of the weights onto the `(2Ny, 2Nx)` mode grid.
836-
`nufftax.nufft2d1(x, y, c, n_modes=(N1, N2), eps, isign)` returns
870+
`nufftax.nufft2d1(x, y, c, n_modes=(N1, N2), eps, isign)` (and `nufft_cpu.nufft2d1`, the
871+
`finufft` wrapper that mirrors it) returns
837872
`f[m2, m1] = sum_k c_k exp(isign * i(m1 * x_k + m2 * y_k))` on the **centred** mode
838873
grid, shape `(N2, N1)`, so the mapping is
839874
@@ -902,6 +937,9 @@ def nufft_precision_operator_via_nufft_from(
902937
The requested NUFFT precision.
903938
chunk_size
904939
Cap on the visibilities passed to `nufft2d1` in one call, or `None` for one shot.
940+
use_jax
941+
If `False` (default) the NUFFT runs on `finufft` (CPU, never imports JAX); if `True`
942+
on `nufftax` (JAX). Both return the same NumPy array to ~1e-14 relative.
905943
906944
Returns
907945
-------
@@ -927,6 +965,7 @@ def nufft_precision_operator_via_nufft_from(
927965
chunk_size=chunk_size,
928966
centred=False,
929967
zero_nyquist=True,
968+
use_jax=use_jax,
930969
)
931970

932971

@@ -2543,7 +2582,7 @@ def fine_grid_shapes_from(
25432582
def _log_fine_grid_memory(shape_operator_fine, shape_dirty_image_fine, oversample):
25442583
"""
25452584
Log one line estimating the memory the `oversample` fine grids need: the two float64 grids
2546-
held for the whole stream, plus the largest per-call `nufftax.nufft2d1` working set (its 2x
2585+
held for the whole stream, plus the largest per-call `nufft2d1` working set (its 2x
25472586
upsampled complex128 grid, its complex128 output and that output's float64 real part).
25482587
"""
25492588
held = 8 * (
@@ -2665,7 +2704,7 @@ def sparse_terms_from_chunks(
26652704
grids need the NUFFT: `oversample` with a transformer other than `TransformerNUFFT` raises.
26662705
26672706
Memory and cost. At a 400 x 400 native grid and `q = 8`, `precision_operator_fine` is 8000^2
2668-
float64 (512 MB) and `dirty_image_fine` 6400^2 (328 MB), and `nufftax`'s 2x upsampled complex
2707+
float64 (512 MB) and `dirty_image_fine` 6400^2 (328 MB), and the NUFFT's 2x upsampled complex
26692708
work grid adds ~4 GB during each call: plan for 5-6 GB peak (`q = 4` is ~1.5 GB; ~112 pixels
26702709
at `q = 8` is under 0.5 GB). One info line logs the estimate for the actual shapes. The FFT
26712710
part of each fine NUFFT costs the same whatever the chunk length (seconds per chunk at
@@ -2709,6 +2748,8 @@ def sparse_terms_from_chunks(
27092748
method, eps, chunk_size, chunk_k, use_jax, show_progress
27102749
Passed to `nufft_precision_operator_from` for each chunk (`chunk_size` is the NUFFT
27112750
builder's own inner chunk, a memory ceiling within a chunk). `eps=None` is `1e-12`.
2751+
`use_jax` also picks the backend of the `oversample` fine grids (`finufft` for `False`,
2752+
`nufftax` for `True`); the dirty image and beam always use the transformer's NumPy path.
27122753
phase_centre
27132754
The `(y, x)` phase-centre shift in arcseconds applied to every chunk's visibilities
27142755
before forming the dirty image (see "Phase-centre shift" above). `None` applies no
@@ -2889,6 +2930,7 @@ def sparse_terms_from_chunks(
28892930
centred=False,
28902931
zero_nyquist=False,
28912932
out=precision_operator_fine,
2933+
use_jax=use_jax,
28922934
)
28932935

28942936
_type1_real_grid_from(
@@ -2904,6 +2946,7 @@ def sparse_terms_from_chunks(
29042946
centred=True,
29052947
zero_nyquist=False,
29062948
out=dirty_image_fine,
2949+
use_jax=use_jax,
29072950
)
29082951

29092952
dirty_beam_native = np.asarray(

0 commit comments

Comments
 (0)