|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""Facilities for pickling Python code alongside other data. |
|
|
|
The pickled code is automatically imported into a separate Python module |
|
during unpickling. This way, any previously exported pickles will remain |
|
usable even if the original code is no longer available, or if the current |
|
version of the code is not consistent with what was originally pickled.""" |
|
|
|
import sys |
|
import pickle |
|
import io |
|
import inspect |
|
import copy |
|
import uuid |
|
import types |
|
import dnnlib |
|
|
|
|
|
|
|
_version = 6 |
|
_decorators = set() |
|
_import_hooks = [] |
|
_module_to_src_dict = dict() |
|
_src_to_module_dict = dict() |
|
|
|
|
|
|
|
def persistent_class(orig_class): |
|
r"""Class decorator that extends a given class to save its source code |
|
when pickled. |
|
|
|
Example: |
|
|
|
from torch_utils import persistence |
|
|
|
@persistence.persistent_class |
|
class MyNetwork(torch.nn.Module): |
|
def __init__(self, num_inputs, num_outputs): |
|
super().__init__() |
|
self.fc = MyLayer(num_inputs, num_outputs) |
|
... |
|
|
|
@persistence.persistent_class |
|
class MyLayer(torch.nn.Module): |
|
... |
|
|
|
When pickled, any instance of `MyNetwork` and `MyLayer` will save its |
|
source code alongside other internal state (e.g., parameters, buffers, |
|
and submodules). This way, any previously exported pickle will remain |
|
usable even if the class definitions have been modified or are no |
|
longer available. |
|
|
|
The decorator saves the source code of the entire Python module |
|
containing the decorated class. It does *not* save the source code of |
|
any imported modules. Thus, the imported modules must be available |
|
during unpickling, also including `torch_utils.persistence` itself. |
|
|
|
It is ok to call functions defined in the same module from the |
|
decorated class. However, if the decorated class depends on other |
|
classes defined in the same module, they must be decorated as well. |
|
This is illustrated in the above example in the case of `MyLayer`. |
|
|
|
It is also possible to employ the decorator just-in-time before |
|
calling the constructor. For example: |
|
|
|
cls = MyLayer |
|
if want_to_make_it_persistent: |
|
cls = persistence.persistent_class(cls) |
|
layer = cls(num_inputs, num_outputs) |
|
|
|
As an additional feature, the decorator also keeps track of the |
|
arguments that were used to construct each instance of the decorated |
|
class. The arguments can be queried via `obj.init_args` and |
|
`obj.init_kwargs`, and they are automatically pickled alongside other |
|
object state. A typical use case is to first unpickle a previous |
|
instance of a persistent class, and then upgrade it to use the latest |
|
version of the source code: |
|
|
|
with open('old_pickle.pkl', 'rb') as f: |
|
old_net = pickle.load(f) |
|
new_net = MyNetwork(*old_obj.init_args, **old_obj.init_kwargs) |
|
misc.copy_params_and_buffers(old_net, new_net, require_all=True) |
|
""" |
|
assert isinstance(orig_class, type) |
|
if is_persistent(orig_class): |
|
return orig_class |
|
|
|
assert orig_class.__module__ in sys.modules |
|
orig_module = sys.modules[orig_class.__module__] |
|
orig_module_src = _module_to_src(orig_module) |
|
|
|
class Decorator(orig_class): |
|
_orig_module_src = orig_module_src |
|
_orig_class_name = orig_class.__name__ |
|
|
|
def __init__(self, *args, **kwargs): |
|
super().__init__(*args, **kwargs) |
|
self._init_args = copy.deepcopy(args) |
|
self._init_kwargs = copy.deepcopy(kwargs) |
|
assert orig_class.__name__ in orig_module.__dict__ |
|
_check_pickleable(self.__reduce__()) |
|
|
|
@property |
|
def init_args(self): |
|
return copy.deepcopy(self._init_args) |
|
|
|
@property |
|
def init_kwargs(self): |
|
return dnnlib.EasyDict(copy.deepcopy(self._init_kwargs)) |
|
|
|
def __reduce__(self): |
|
fields = list(super().__reduce__()) |
|
fields += [None] * max(3 - len(fields), 0) |
|
if fields[0] is not _reconstruct_persistent_obj: |
|
meta = dict(type='class', version=_version, module_src=self._orig_module_src, class_name=self._orig_class_name, state=fields[2]) |
|
fields[0] = _reconstruct_persistent_obj |
|
fields[1] = (meta,) |
|
fields[2] = None |
|
return tuple(fields) |
|
|
|
Decorator.__name__ = orig_class.__name__ |
|
_decorators.add(Decorator) |
|
return Decorator |
|
|
|
|
|
|
|
def is_persistent(obj): |
|
r"""Test whether the given object or class is persistent, i.e., |
|
whether it will save its source code when pickled. |
|
""" |
|
try: |
|
if obj in _decorators: |
|
return True |
|
except TypeError: |
|
pass |
|
return type(obj) in _decorators |
|
|
|
|
|
|
|
def import_hook(hook): |
|
r"""Register an import hook that is called whenever a persistent object |
|
is being unpickled. A typical use case is to patch the pickled source |
|
code to avoid errors and inconsistencies when the API of some imported |
|
module has changed. |
|
|
|
The hook should have the following signature: |
|
|
|
hook(meta) -> modified meta |
|
|
|
`meta` is an instance of `dnnlib.EasyDict` with the following fields: |
|
|
|
type: Type of the persistent object, e.g. `'class'`. |
|
version: Internal version number of `torch_utils.persistence`. |
|
module_src Original source code of the Python module. |
|
class_name: Class name in the original Python module. |
|
state: Internal state of the object. |
|
|
|
Example: |
|
|
|
@persistence.import_hook |
|
def wreck_my_network(meta): |
|
if meta.class_name == 'MyNetwork': |
|
print('MyNetwork is being imported. I will wreck it!') |
|
meta.module_src = meta.module_src.replace("True", "False") |
|
return meta |
|
""" |
|
assert callable(hook) |
|
_import_hooks.append(hook) |
|
|
|
|
|
|
|
def _reconstruct_persistent_obj(meta): |
|
r"""Hook that is called internally by the `pickle` module to unpickle |
|
a persistent object. |
|
""" |
|
meta = dnnlib.EasyDict(meta) |
|
meta.state = dnnlib.EasyDict(meta.state) |
|
for hook in _import_hooks: |
|
meta = hook(meta) |
|
assert meta is not None |
|
|
|
assert meta.version == _version |
|
module = _src_to_module(meta.module_src) |
|
|
|
assert meta.type == 'class' |
|
orig_class = module.__dict__[meta.class_name] |
|
decorator_class = persistent_class(orig_class) |
|
obj = decorator_class.__new__(decorator_class) |
|
|
|
setstate = getattr(obj, '__setstate__', None) |
|
if callable(setstate): |
|
setstate(meta.state) |
|
else: |
|
obj.__dict__.update(meta.state) |
|
return obj |
|
|
|
|
|
|
|
def _module_to_src(module): |
|
r"""Query the source code of a given Python module. |
|
""" |
|
src = _module_to_src_dict.get(module, None) |
|
if src is None: |
|
src = inspect.getsource(module) |
|
_module_to_src_dict[module] = src |
|
_src_to_module_dict[src] = module |
|
return src |
|
|
|
def _src_to_module(src): |
|
r"""Get or create a Python module for the given source code. |
|
""" |
|
module = _src_to_module_dict.get(src, None) |
|
if module is None: |
|
module_name = "_imported_module_" + uuid.uuid4().hex |
|
module = types.ModuleType(module_name) |
|
sys.modules[module_name] = module |
|
_module_to_src_dict[module] = src |
|
_src_to_module_dict[src] = module |
|
exec(src, module.__dict__) |
|
return module |
|
|
|
|
|
|
|
def _check_pickleable(obj): |
|
r"""Check that the given object is pickleable, raising an exception if |
|
it is not. This function is expected to be considerably more efficient |
|
than actually pickling the object. |
|
""" |
|
def recurse(obj): |
|
if isinstance(obj, (list, tuple, set)): |
|
return [recurse(x) for x in obj] |
|
if isinstance(obj, dict): |
|
return [[recurse(x), recurse(y)] for x, y in obj.items()] |
|
if isinstance(obj, (str, int, float, bool, bytes, bytearray)): |
|
return None |
|
if f'{type(obj).__module__}.{type(obj).__name__}' in ['numpy.ndarray', 'torch.Tensor']: |
|
return None |
|
if is_persistent(obj): |
|
return None |
|
return obj |
|
with io.BytesIO() as f: |
|
pickle.dump(recurse(obj), f) |
|
|
|
|
|
|