Skip to content

Commit ab46578

Browse files
committed
api: Add (rudimentary) autopickling support
1 parent 20da795 commit ab46578

4 files changed

Lines changed: 141 additions & 8 deletions

File tree

devito/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@ def reinit_compiler(val):
107107
# and will instead use the custom kernel
108108
configuration.add('jit-backdoor', 0, [0, 1], preprocessor=bool, impacts_jit=False)
109109

110+
# Enable/disable automatic pickling of named Operators. When enabled, named
111+
# Operators (that is, Operators instantiated with a name kwarg) are automatically
112+
# pickled to disk upon creation, and loaded from disk upon subsequent creations,
113+
# thus bypassing code generation and compilation. This is to be used with caution,
114+
# since it assumes that things such as Operator creation arguments (e.g., the
115+
# equations themselves), the Devito configuration, the compiler/runtime, etc,
116+
# have not changed between runs. Further, data carried by any of the input
117+
# objects is pickled as well, which may lead to large files on disk. Currently, no
118+
# safeguards are in place to deal with any of these cases, so... you have been
119+
# warned! Use at your own risk
120+
configuration.add('autopickling', 0, [0, 1], preprocessor=bool, impacts_jit=False)
121+
110122
# By default unsafe math is allowed as most applications are insensitive to
111123
# floating-point roundoff errors. Enabling this disables unsafe math
112124
# optimisations.

devito/operator/operator.py

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
from collections import OrderedDict, namedtuple
22
from functools import cached_property
3-
import ctypes
4-
import shutil
53
from operator import attrgetter
64
from math import ceil
75
from tempfile import gettempdir
6+
from time import time
7+
import ctypes
8+
import glob
9+
import os
10+
import shutil
811

912
from sympy import sympify
1013
import sympy
1114
import numpy as np
15+
import cloudpickle as pickle
1216

1317
from devito.arch import (ANYCPU, Device, compiler_registry, platform_registry,
1418
get_visible_devices)
@@ -32,10 +36,11 @@
3236
minimize_symbols, unevaluate, error_mapper, is_on_device, lower_dtypes
3337
)
3438
from devito.symbolics import estimate_cost, subs_op_args
35-
from devito.tools import (DAG, OrderedSet, Signer, ReducerMap, as_mapper, as_tuple,
36-
flatten, filter_sorted, frozendict, is_integer,
37-
split, timed_pass, timed_region, contains_val,
38-
CacheInstances, MemoryEstimate)
39+
from devito.tools import (
40+
DAG, OrderedSet, Signer, ReducerMap, as_mapper, as_tuple, flatten,
41+
filter_sorted, frozendict, is_integer, split, timed_pass, timed_region,
42+
contains_val, CacheInstances, MemoryEstimate, make_tempdir
43+
)
3944
from devito.types import (Buffer, Evaluable, host_layer, device_layer,
4045
disk_layer)
4146
from devito.types.dimension import Thickness
@@ -157,6 +162,12 @@ def __new__(cls, expressions, **kwargs):
157162
# can't do anything useful with it
158163
return super().__new__(cls, **kwargs)
159164

165+
# Maybe lookup an existing Operator from disk
166+
name = kwargs.get('name', default_op_name)
167+
op = autopickler.maybe_load(name)
168+
if op is not None:
169+
return op
170+
160171
# Parse input arguments
161172
kwargs = parse_kwargs(**kwargs)
162173

@@ -176,6 +187,9 @@ def __new__(cls, expressions, **kwargs):
176187
# Emit info about how long it took to perform the lowering
177188
op._emit_build_profiling()
178189

190+
# Maybe save the Operator to disk
191+
autopickler.maybe_save(op)
192+
179193
return op
180194

181195
@classmethod
@@ -479,7 +493,7 @@ def _lower_iet(cls, uiet, profiler=None, **kwargs):
479493
* Introduce optimizations for data locality;
480494
* Finalize (e.g., symbol definitions, array casts)
481495
"""
482-
name = kwargs.get("name", "Kernel")
496+
name = kwargs.get("name", default_op_name)
483497

484498
# Wrap the IET with an EntryFunction (a special Callable representing
485499
# the entry point of the generated library)
@@ -1216,6 +1230,10 @@ def __setstate__(self, state):
12161230
f'{type(self._compiler).__name__}.{self._language}.{self._platform}'
12171231
)
12181232

1233+
# Tag this Operator as unpickled -- might come in handy at the call site
1234+
# for sanity checks
1235+
self._unpickled = True
1236+
12191237

12201238
# *** Recursive compilation ("rcompile") machinery
12211239

@@ -1701,3 +1719,79 @@ def parse_kwargs(**kwargs):
17011719
kwargs['subs'] = {k: sympify(v) for k, v in kwargs.get('subs', {}).items()}
17021720

17031721
return kwargs
1722+
1723+
1724+
# The name assigned to an Operator when the user does not provide one
1725+
default_op_name = "Kernel"
1726+
1727+
1728+
class Autopickler:
1729+
1730+
def __init__(self):
1731+
self._initialized = False
1732+
self.registry = {}
1733+
1734+
@property
1735+
def _directory(self):
1736+
return make_tempdir('autopickling')
1737+
1738+
def _initialize(self):
1739+
# Search the `autopickling` temporary directory for pickled Operators
1740+
# and maintain a registry of them. Each pickled Operator is uniquely
1741+
# identified by a name; this might not be enough to avoid name clashes,
1742+
# but for now it is what we have. Thus, a generic filename is
1743+
# `<operator_name>.pkl`.
1744+
pkl_files = glob.glob(os.path.join(self._directory, '*.pkl'))
1745+
1746+
self.registry.update({
1747+
os.path.basename(pkl_file)[:-4]: pkl_file for pkl_file in pkl_files
1748+
})
1749+
1750+
self._initialized = True
1751+
1752+
def maybe_load(self, name):
1753+
if not configuration['autopickling']:
1754+
return None
1755+
1756+
tic = time()
1757+
1758+
if not self._initialized:
1759+
self._initialize()
1760+
1761+
if name is None or name == default_op_name:
1762+
return None
1763+
1764+
pkl_file = self.registry.get(name)
1765+
if pkl_file is None:
1766+
return None
1767+
1768+
with open(pkl_file, 'rb') as f:
1769+
op = pickle.load(f)
1770+
1771+
toc = time()
1772+
1773+
perf(f"Operator `{name}` unpickled from disk in {toc - tic:.2f} s")
1774+
1775+
return op
1776+
1777+
def maybe_save(self, op):
1778+
if not configuration['autopickling']:
1779+
return
1780+
1781+
if op.name == default_op_name:
1782+
return
1783+
1784+
assert self._initialized is not None
1785+
1786+
pkl_file = os.path.join(self._directory, f"{op.name}.pkl")
1787+
with open(pkl_file, 'wb') as f:
1788+
pickle.dump(op, f)
1789+
1790+
# Update the registry, in most cases this is unnecessary since
1791+
# autopickling is about saving time on subsequent runs, but just in case
1792+
self.registry[op.name] = pkl_file
1793+
1794+
debug(f"Operator `{op.name}` pickled to disk at `{pkl_file}`")
1795+
1796+
1797+
autopickler = Autopickler()

devito/parameters.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ def _signature_items(self):
152152
'DEVITO_AUTOTUNING': 'autotuning',
153153
'DEVITO_LOGGING': 'log-level',
154154
'DEVITO_FIRST_TOUCH': 'first-touch',
155+
'DEVITO_AUTOPICKLING': 'autopickling',
155156
'DEVITO_JIT_BACKDOOR': 'jit-backdoor',
156157
'DEVITO_IGNORE_UNKNOWN_PARAMS': 'ignore-unknowns',
157158
'DEVITO_SAFE_MATH': 'safe-math'

tests/test_pickle.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ctypes
22
import pickle as pickle0
3+
import shutil
34

45
import cloudpickle as pickle1
56
import pytest
@@ -9,10 +10,11 @@
910
from devito import (Constant, Eq, Function, TimeFunction, SparseFunction, Grid,
1011
Dimension, SubDimension, ConditionalDimension, IncrDimension,
1112
TimeDimension, SteppingDimension, Operator, MPI, Min, solve,
12-
PrecomputedSparseTimeFunction, SubDomain)
13+
PrecomputedSparseTimeFunction, SubDomain, switchconfig)
1314
from devito.ir import Backward, Forward, GuardFactor, GuardBound, GuardBoundNext
1415
from devito.data import LEFT, OWNED
1516
from devito.finite_differences.tools import direct, transpose, left, right, centered
17+
from devito.operator.operator import autopickler
1618
from devito.mpi.halo_scheme import Halo
1719
from devito.mpi.routines import (MPIStatusObject, MPIMsgEnriched, MPIRequestObject,
1820
MPIRegion)
@@ -1074,3 +1076,27 @@ def test_usave_sampled(self, pickle, subs):
10741076
op_new = pickle.load(open(tmp_pickle_op_fn, "rb"))
10751077

10761078
assert str(op_fwd) == str(op_new)
1079+
1080+
1081+
@pytest.fixture
1082+
def purged_autopickling_dir():
1083+
# Erase the content of the autopickling dir before and after the test
1084+
shutil.rmtree(autopickler._directory, ignore_errors=True)
1085+
yield
1086+
shutil.rmtree(autopickler._directory, ignore_errors=True)
1087+
1088+
1089+
class TestAutopickling:
1090+
1091+
@switchconfig(autopickling=True)
1092+
def test_basic(self, purged_autopickling_dir):
1093+
grid = Grid(shape=(3, 3, 3))
1094+
f = TimeFunction(name='f', grid=grid)
1095+
eqn = Eq(f.forward, f + 1)
1096+
op0 = Operator(eqn, name='TestOp')
1097+
1098+
# Expected to be unpickled from the autopickling dir
1099+
op1 = Operator(eqn, name='TestOp')
1100+
1101+
assert not getattr(op0, '_unpickled', False)
1102+
assert op1._unpickled is True

0 commit comments

Comments
 (0)