11from collections import OrderedDict , namedtuple
22from functools import cached_property
3- import ctypes
4- import shutil
53from operator import attrgetter
64from math import ceil
75from tempfile import gettempdir
6+ from time import time
7+ import ctypes
8+ import glob
9+ import os
10+ import shutil
811
912from sympy import sympify
1013import sympy
1114import numpy as np
15+ import cloudpickle as pickle
1216
1317from devito .arch import (ANYCPU , Device , compiler_registry , platform_registry ,
1418 get_visible_devices )
3236 minimize_symbols , unevaluate , error_mapper , is_on_device , lower_dtypes
3337)
3438from 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+ )
3944from devito .types import (Buffer , Evaluable , host_layer , device_layer ,
4045 disk_layer )
4146from 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 ()
0 commit comments