Skip to content

Commit a26385b

Browse files
authored
Merge pull request #451 from PyAutoLabs/feature/lazy-heavy-imports
refactor: defer nufftax/jax and numba imports to first use
2 parents 012813b + 3aea542 commit a26385b

2 files changed

Lines changed: 81 additions & 24 deletions

File tree

autoarray/numba_util.py

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import logging
2+
import sys
3+
import threading
24

35
from autonerves import conf
46

@@ -15,24 +17,61 @@
1517
parallel = False
1618

1719

18-
def jit(nopython=nopython, cache=cache, parallel=parallel, fastmath=False):
20+
# Decorated functions are queued here and only handed to numba on the first
21+
# call of any of them, keeping ``import numba`` off the library import path.
22+
# Materialization converts every queued function at once and rebinds the
23+
# defining module's global, because numba's nopython mode must resolve
24+
# cross-calls between decorated functions to real dispatchers at compile time.
25+
_pending = []
26+
_materialize_lock = threading.Lock()
1927

20-
def wrapper(func):
2128

22-
try:
29+
def _materialize_all():
30+
with _materialize_lock:
31+
if not _pending:
32+
return
2333

34+
try:
2435
import numba
36+
except ModuleNotFoundError:
37+
numba = None
2538

26-
return numba.jit(
27-
func,
28-
nopython=nopython,
29-
cache=cache,
30-
parallel=parallel,
31-
fastmath=fastmath,
32-
)
39+
while _pending:
40+
func, options, placeholder, state = _pending.pop()
3341

34-
except ModuleNotFoundError:
42+
if numba is None:
43+
target = func
44+
else:
45+
target = numba.jit(func, **options)
46+
47+
state["target"] = target
48+
49+
module = sys.modules.get(func.__module__)
50+
if module is not None and getattr(module, func.__name__, None) is placeholder:
51+
setattr(module, func.__name__, target)
52+
53+
54+
def jit(nopython=nopython, cache=cache, parallel=parallel, fastmath=False):
55+
options = dict(
56+
nopython=nopython,
57+
cache=cache,
58+
parallel=parallel,
59+
fastmath=fastmath,
60+
)
61+
62+
def wrapper(func):
63+
import functools
64+
65+
state = {"target": None}
66+
67+
@functools.wraps(func)
68+
def lazy(*args, **kwargs):
69+
if state["target"] is None:
70+
_materialize_all()
71+
return state["target"](*args, **kwargs)
72+
73+
_pending.append((func, options, lazy, state))
3574

36-
return func
75+
return lazy
3776

3877
return wrapper

autoarray/operators/transformer.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,29 @@ class NUFFTPlaceholder:
2323
from autoarray.operators import transformer_util
2424

2525

26-
try:
27-
import nufftax as _nufftax
28-
except ModuleNotFoundError:
29-
_nufftax = None
26+
# nufftax pulls in jax at import (~0.7s), which sessions that never touch an
27+
# interferometer transformer should not pay for — deferred to _load_nufftax(),
28+
# called from TransformerNUFFT's entry points (not only __init__, because
29+
# unpickled instances in multiprocessing workers never re-run __init__).
30+
_nufftax = None
31+
_nufftax_loaded = False
32+
33+
34+
def _load_nufftax():
35+
global _nufftax, _nufftax_loaded
36+
if _nufftax_loaded:
37+
return _nufftax
38+
_nufftax_loaded = True
39+
try:
40+
import nufftax
41+
except ModuleNotFoundError:
42+
return None
43+
_nufftax = nufftax
44+
_version = tuple(int(v) for v in _nufftax.__version__.split(".")[:2])
45+
# Only the 0.6.x series both has the primitives module and needs the shim.
46+
if (0, 6) <= _version < (0, 7):
47+
_patch_nufftax_batchers()
48+
return _nufftax
3049

3150

3251
def _patch_nufftax_batchers():
@@ -87,13 +106,6 @@ def batcher(args, dims, **kwargs):
87106
)
88107

89108

90-
if _nufftax is not None:
91-
_version = tuple(int(v) for v in _nufftax.__version__.split(".")[:2])
92-
# Only the 0.6.x series both has the primitives module and needs the shim.
93-
if (0, 6) <= _version < (0, 7):
94-
_patch_nufftax_batchers()
95-
96-
97109
def pynufft_exception():
98110
raise ModuleNotFoundError(
99111
"\n--------------------\n"
@@ -643,7 +655,7 @@ def __init__(
643655
"""
644656
from astropy import units
645657

646-
if _nufftax is None:
658+
if _load_nufftax() is None:
647659
nufftax_exception()
648660

649661
if chunk_size is not None and chunk_size <= 0:
@@ -685,6 +697,8 @@ def _forward_native(self, image_native_2d, xp=np):
685697
fixed-size chunks via ``jax.lax.scan`` (JAX path) or a Python loop
686698
(numpy path) — caps the nufftax gather-buffer allocation per call.
687699
"""
700+
_load_nufftax()
701+
688702
K = int(self._x.shape[0])
689703

690704
if xp.__name__.startswith("jax"):
@@ -789,6 +803,8 @@ def image_from(
789803
the sparse-operator dirty image is scale-consistent across all three
790804
transformers.
791805
"""
806+
_load_nufftax()
807+
792808
n_y, n_x = self.real_space_mask.shape_native
793809
n_modes = (n_x, n_y) # nufftax wants (n1, n2) = (N_x, N_y)
794810
K = int(self._x.shape[0])
@@ -860,6 +876,8 @@ def transform_mapping_matrix(self, mapping_matrix, xp=np):
860876
``n_src`` separate NUFFT invocations and blow up the JIT graph
861877
for pixelization-heavy fits (notably double-source-plane).
862878
"""
879+
_load_nufftax()
880+
863881
n_src = mapping_matrix.shape[1]
864882
rows, cols = self.real_space_mask.slim_to_native_tuple
865883
n_y, n_x = self.real_space_mask.shape_native

0 commit comments

Comments
 (0)