443 lines
13 KiB
Python
443 lines
13 KiB
Python
#
|
|
# SPDX-FileCopyrightText: Copyright (c) 1993-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
|
|
import base64
|
|
import functools
|
|
import io
|
|
import json
|
|
from collections import OrderedDict
|
|
|
|
from polygraphy import constants, mod, util
|
|
from polygraphy.logger import G_LOGGER
|
|
|
|
np = mod.lazy_import("numpy")
|
|
torch = mod.lazy_import("torch>=1.13.0")
|
|
|
|
|
|
def legacy_str_from_type(typ):
|
|
return "__polygraphy_encoded_" + typ.__name__
|
|
|
|
|
|
def str_from_type(typ):
|
|
return typ.__name__
|
|
|
|
|
|
class BaseCustomImpl:
|
|
"""
|
|
Base class for Polygraphy's JSON encoder/decoder.
|
|
"""
|
|
|
|
@classmethod
|
|
def register(cls, typ, alias=None):
|
|
"""
|
|
Decorator that registers JSON encoding/decoding functions for types.
|
|
|
|
Args:
|
|
typ (type): The type to register
|
|
alias (str):
|
|
An alias under which to also register the decoder function.
|
|
This can be used to retain backwards-compatibility when a class
|
|
name changes.
|
|
|
|
For the documentation that follows, assume we have a class:
|
|
::
|
|
|
|
class Dummy:
|
|
def __init__(self, x):
|
|
self.x = x
|
|
|
|
========
|
|
Encoders
|
|
========
|
|
|
|
Encoder functions should accept instances of the specified type and
|
|
return dictionaries.
|
|
|
|
For example:
|
|
::
|
|
|
|
@Encoder.register(Dummy)
|
|
def encode(dummy):
|
|
return {"x": dummy.x}
|
|
|
|
|
|
To use the custom encoder, use the `to_json` helper:
|
|
::
|
|
|
|
d = Dummy(x=1)
|
|
d_json = to_json(d)
|
|
|
|
|
|
========
|
|
Decoders
|
|
========
|
|
|
|
Decoder functions should accept dictionaries, and return instances of the
|
|
type.
|
|
|
|
For example:
|
|
::
|
|
|
|
@Decoder.register(Dummy)
|
|
def decode(dct):
|
|
return Dummy(x=dct["x"])
|
|
|
|
|
|
To use the custom decoder, use the `from_json` helper:
|
|
::
|
|
|
|
from_json(d_json)
|
|
|
|
|
|
Args:
|
|
typ (type): The type of the class for which to register the function.
|
|
"""
|
|
|
|
def register_impl(func):
|
|
def add(key, val):
|
|
if key in cls.polygraphy_registered:
|
|
G_LOGGER.critical(
|
|
f"Duplicate serialization function for type: {key}.\nNote: Existing function: {cls.polygraphy_registered[key]}, New function: {func}"
|
|
)
|
|
cls.polygraphy_registered[key] = val
|
|
|
|
if cls == Encoder:
|
|
|
|
def wrapped(obj):
|
|
dct = func(obj)
|
|
dct[constants.TYPE_MARKER] = str_from_type(typ)
|
|
return dct
|
|
|
|
add(typ, wrapped)
|
|
return wrapped
|
|
elif cls == Decoder:
|
|
|
|
def wrapped(dct):
|
|
if constants.TYPE_MARKER in dct:
|
|
del dct[constants.TYPE_MARKER]
|
|
|
|
type_name = legacy_str_from_type(typ)
|
|
if type_name in dct:
|
|
del dct[type_name]
|
|
|
|
return func(dct)
|
|
|
|
add(legacy_str_from_type(typ), wrapped)
|
|
add(str_from_type(typ), wrapped)
|
|
if alias is not None:
|
|
add(alias, wrapped)
|
|
else:
|
|
G_LOGGER.critical("Cannot register for unrecognized class type: ")
|
|
|
|
return register_impl
|
|
|
|
|
|
@mod.export()
|
|
class Encoder(BaseCustomImpl, json.JSONEncoder):
|
|
"""
|
|
Polygraphy's custom JSON Encoder implementation.
|
|
"""
|
|
|
|
polygraphy_registered = {}
|
|
|
|
def default(self, o):
|
|
if type(o) in self.polygraphy_registered:
|
|
return self.polygraphy_registered[type(o)](o)
|
|
return super().default(o)
|
|
|
|
|
|
@mod.export()
|
|
class Decoder(BaseCustomImpl):
|
|
"""
|
|
Polygraphy's custom JSON Decoder implementation.
|
|
"""
|
|
|
|
polygraphy_registered = {}
|
|
|
|
def __call__(self, pairs):
|
|
# The encoder will insert special key-value pairs into dictionaries encoded from
|
|
# custom types. If we find one, then we know to decode using the corresponding custom
|
|
# type function.
|
|
dct = OrderedDict(pairs)
|
|
|
|
# Handle legacy naming first - these keys should not be present in JSON generated by more recent versions of Polygraphy.
|
|
for type_str, func in self.polygraphy_registered.items():
|
|
if (
|
|
type_str in dct and dct[type_str] == constants.LEGACY_TYPE_MARKER
|
|
): # Found a custom type!
|
|
return func(dct)
|
|
|
|
type_name = dct.get(constants.TYPE_MARKER)
|
|
if type_name is not None:
|
|
if type_name not in self.polygraphy_registered:
|
|
user_type_name = {
|
|
"Tensor": "torch.Tensor",
|
|
"ndarray": "np.ndarray",
|
|
}.get(type_name, type_name)
|
|
G_LOGGER.critical(
|
|
f"Could not decode serialized type: {user_type_name}. This could be because a required module is missing. "
|
|
)
|
|
return self.polygraphy_registered[type_name](dct)
|
|
|
|
return dct
|
|
|
|
|
|
NUMPY_REGISTRATION_SUCCESS = False
|
|
TORCH_REGISTRATION_SUCCESS = False
|
|
COMMON_REGISTRATION_SUCCESS = False
|
|
|
|
|
|
def try_register_common_json(func):
|
|
"""
|
|
Decorator that attempts to register common JSON encode/decode methods
|
|
if the methods have not already been registered.
|
|
|
|
This needs to be attempted multiple times because dependencies may become available in the
|
|
middle of execution - for example, if using dependency auto-installation.
|
|
"""
|
|
|
|
@functools.wraps(func)
|
|
def wrapped(*args, **kwargs):
|
|
global NUMPY_REGISTRATION_SUCCESS
|
|
if not NUMPY_REGISTRATION_SUCCESS and np.is_installed() and np.is_importable():
|
|
# We define this alongside load_json/save_json so that it is guaranteed to be
|
|
# imported before we need to encode/decode NumPy arrays.
|
|
@Encoder.register(np.ndarray)
|
|
def encode(array):
|
|
outfile = io.BytesIO()
|
|
np.save(outfile, array, allow_pickle=False)
|
|
outfile.seek(0)
|
|
data = base64.b64encode(outfile.read()).decode()
|
|
return {"array": data}
|
|
|
|
@Decoder.register(np.ndarray)
|
|
def decode(dct):
|
|
def load(mode="base64"):
|
|
if mode == "base64":
|
|
data = base64.b64decode(dct["array"].encode(), validate=True)
|
|
elif mode == "latin-1":
|
|
data = dct["array"].encode(mode)
|
|
else:
|
|
assert False, f"Unsupported mode: {mode}"
|
|
infile = io.BytesIO(data)
|
|
return np.load(infile, allow_pickle=False)
|
|
|
|
try:
|
|
arr = load()
|
|
except:
|
|
arr = load("latin-1") # For backwards compatibility
|
|
if isinstance(arr, np.ndarray):
|
|
return arr
|
|
return list(arr.values())[0] # For backwards compatibility
|
|
|
|
NUMPY_REGISTRATION_SUCCESS = True
|
|
|
|
global TORCH_REGISTRATION_SUCCESS
|
|
if (
|
|
not TORCH_REGISTRATION_SUCCESS
|
|
and torch.is_installed()
|
|
and torch.is_importable()
|
|
):
|
|
|
|
@Encoder.register(torch.Tensor)
|
|
def encode(tensor):
|
|
outfile = io.BytesIO()
|
|
torch.save(tensor, outfile)
|
|
outfile.seek(0)
|
|
data = base64.b64encode(outfile.read()).decode()
|
|
return {"tensor": data}
|
|
|
|
@Decoder.register(torch.Tensor)
|
|
def decode(dct):
|
|
data = base64.b64decode(dct["tensor"].encode(), validate=True)
|
|
infile = io.BytesIO(data)
|
|
return torch.load(infile)
|
|
|
|
TORCH_REGISTRATION_SUCCESS = True
|
|
|
|
global COMMON_REGISTRATION_SUCCESS
|
|
if not COMMON_REGISTRATION_SUCCESS:
|
|
# Pull in some common types so that we can get their associated serialization/deserialization
|
|
# functions. This allows the user to avoid importing these manually.
|
|
# Note: We can only do this here for submodules with no external dependencies.
|
|
# That means, for example, nothing from `backend/` can be imported here.
|
|
from polygraphy.common import FormattedArray
|
|
from polygraphy.comparator import RunResults
|
|
|
|
COMMON_REGISTRATION_SUCCESS = True
|
|
return func(*args, **kwargs)
|
|
|
|
return wrapped
|
|
|
|
|
|
@mod.export()
|
|
@try_register_common_json
|
|
def to_json(obj):
|
|
"""
|
|
Encode an object to JSON.
|
|
|
|
NOTE: For Polygraphy objects, you should use the ``to_json()`` method instead.
|
|
|
|
Returns:
|
|
str: A JSON representation of the object.
|
|
"""
|
|
return json.dumps(obj, cls=Encoder, indent=constants.TAB)
|
|
|
|
|
|
@mod.export()
|
|
@try_register_common_json
|
|
def from_json(src):
|
|
"""
|
|
Decode a JSON string to an object.
|
|
|
|
NOTE: For Polygraphy objects, you should use the ``from_json()`` method instead.
|
|
|
|
Args:
|
|
src (str):
|
|
The JSON representation of the object
|
|
|
|
Returns:
|
|
object: The decoded instance
|
|
"""
|
|
return json.loads(src, object_pairs_hook=Decoder())
|
|
|
|
|
|
@mod.export()
|
|
@try_register_common_json
|
|
def save_json(obj, dest, description=None):
|
|
"""
|
|
Encode an object as JSON and save it to a file.
|
|
|
|
NOTE: For Polygraphy objects, you should use the ``save()`` method instead.
|
|
|
|
Args:
|
|
obj : The object to save.
|
|
src (Union[str, file-like]): The path or file-like object to save to.
|
|
"""
|
|
util.save_file(to_json(obj), dest, mode="w", description=description)
|
|
|
|
|
|
@mod.export()
|
|
@try_register_common_json
|
|
def load_json(src, description=None):
|
|
"""
|
|
Loads a file and decodes the JSON contents.
|
|
|
|
NOTE: For Polygraphy objects, you should use the ``load()`` method instead.
|
|
|
|
Args:
|
|
src (Union[str, file-like]): The path or file-like object to load from.
|
|
|
|
Returns:
|
|
object: The object, or `None` if nothing could be read.
|
|
"""
|
|
return from_json(util.load_file(src, mode="r", description=description))
|
|
|
|
|
|
@mod.export()
|
|
def add_json_methods(description=None):
|
|
"""
|
|
Decorator that adds 4 JSON helper methods to a class:
|
|
|
|
- to_json(): Convert to JSON string
|
|
- from_json(): Convert from JSON string
|
|
- save(): Convert to JSON and save to file
|
|
- load(): Load from file and convert from JSON
|
|
|
|
Args:
|
|
description (str):
|
|
A description of what is being saved or loaded.
|
|
"""
|
|
|
|
def add_json_methods_impl(cls):
|
|
# JSON methods
|
|
|
|
def check_decoded(obj):
|
|
if not isinstance(obj, cls):
|
|
G_LOGGER.critical(
|
|
f"Provided JSON cannot be decoded into a {cls.__name__}.\nNote: JSON was decoded into a {type(obj)}:\n{obj}"
|
|
)
|
|
return obj
|
|
|
|
def _to_json_method(self):
|
|
"""
|
|
Encode this instance as a JSON object.
|
|
|
|
Returns:
|
|
str: A JSON representation of this instance.
|
|
"""
|
|
return to_json(self)
|
|
|
|
def _from_json_method(src):
|
|
return check_decoded(from_json(src))
|
|
|
|
_from_json_method.__doc__ = f"""
|
|
Decode a JSON object and create an instance of this class.
|
|
|
|
Args:
|
|
src (str):
|
|
The JSON representation of the object
|
|
|
|
Returns:
|
|
{cls.__name__}: The decoded instance
|
|
|
|
Raises:
|
|
PolygraphyException:
|
|
If the JSON cannot be decoded to an instance of {cls.__name__}
|
|
"""
|
|
|
|
cls.to_json = _to_json_method
|
|
cls.from_json = staticmethod(_from_json_method)
|
|
|
|
# Save/Load methods
|
|
|
|
def _save_method(self, dest):
|
|
"""
|
|
Encode this instance as a JSON object and save it to the specified path
|
|
or file-like object.
|
|
|
|
Args:
|
|
dest (Union[str, file-like]):
|
|
The path or file-like object to write to.
|
|
|
|
"""
|
|
save_json(self, dest, description=description)
|
|
|
|
def _load_method(src):
|
|
return check_decoded(load_json(src, description=description))
|
|
|
|
_load_method.__doc__ = f"""
|
|
Loads an instance of this class from a JSON file.
|
|
|
|
Args:
|
|
src (Union[str, file-like]): The path or file-like object to read from.
|
|
|
|
Returns:
|
|
{cls.__name__}: The decoded instance
|
|
|
|
Raises:
|
|
PolygraphyException:
|
|
If the JSON cannot be decoded to an instance of {cls.__name__}
|
|
"""
|
|
|
|
cls.save = _save_method
|
|
cls.load = staticmethod(_load_method)
|
|
|
|
return cls
|
|
|
|
return add_json_methods_impl
|