363 lines
13 KiB
Python
363 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 inspect
|
|
import sys
|
|
import warnings
|
|
from textwrap import dedent
|
|
|
|
import polygraphy
|
|
from polygraphy import config
|
|
from polygraphy.logger import G_LOGGER
|
|
from polygraphy.mod.util import version
|
|
|
|
|
|
def _add_to_all(symbol, module):
|
|
if hasattr(module, "__all__"):
|
|
module.__all__.append(symbol)
|
|
else:
|
|
module.__all__ = [symbol]
|
|
|
|
|
|
def _define_in_module(name, symbol, module):
|
|
assert name not in vars(module), "This symbol is already defined!"
|
|
vars(module)[name] = symbol
|
|
_add_to_all(name, module)
|
|
|
|
|
|
def export(funcify=False, func_name=None):
|
|
"""
|
|
Decorator that exports a symbol into the ``__all__`` attribute of
|
|
the caller's module. This makes the symbol visible in a ``*`` import
|
|
(e.g. ``from module import *``) and hides other symbols unless they are
|
|
also present in ``__all__``.
|
|
|
|
Args:
|
|
funcify (bool):
|
|
Whether to create and export a function that will call a decorated Polygraphy loader.
|
|
The decorated type *must* be a subclass of ``BaseLoader`` if ``funcify=True``.
|
|
|
|
This is useful to provide convenient short-hands to immediately evaluate loaders.
|
|
For example:
|
|
::
|
|
|
|
@mod.export(funcify=True)
|
|
class SuperCoolModelFromPath(BaseLoader):
|
|
def __init__(self, init_params):
|
|
...
|
|
|
|
def call_impl(self, call_params):
|
|
...
|
|
|
|
# We can now magically access an immediately evaluated functional
|
|
# variant of the loader:
|
|
model = super_cool_model_from_path(init_params, call_params)
|
|
|
|
# Which is equivalent to:
|
|
load_model = SuperCoolModelFromPath(init_params)
|
|
model = load_model(call_params)
|
|
|
|
|
|
The signature of the generated function is a combination of the signatures
|
|
of ``__init__`` and ``call_impl``. Specifically, parameters without defaults will
|
|
precede those with defaults, and ``__init__`` parameters will precede ``call_impl``
|
|
parameters. Special parameters like ``*args`` and ``**kwargs`` will always be the last
|
|
parameters in the generated signature if they are present in the loader method signatures.
|
|
The return value(s) will always come from ``call_impl``.
|
|
|
|
For example:
|
|
::
|
|
|
|
# With __init__ signature:
|
|
def __init__(a, b=0) -> None:
|
|
|
|
# And call_impl signature:
|
|
def call_impl(c, d=0) -> z:
|
|
|
|
# The generated function will have a signature:
|
|
def generated(a, c, b=0, d=0) -> z:
|
|
|
|
func_name (str):
|
|
If funcify is True, this controls the name of the generated function.
|
|
By default, the exported function will use the same name as the loader, but
|
|
``snake_case`` instead of ``PascalCase``.
|
|
"""
|
|
module = inspect.getmodule(sys._getframe(1))
|
|
|
|
# Find a method by wallking the inheritance hierarchy of a type:
|
|
def find_method(symbol, method):
|
|
hierarchy = inspect.getmro(symbol)
|
|
for ancestor in hierarchy:
|
|
if method in vars(ancestor):
|
|
return vars(ancestor)[method]
|
|
|
|
assert (
|
|
False
|
|
), f"Could not find method: {method} in the inheritance hierarcy of: {symbol}"
|
|
|
|
def export_impl(func_or_cls):
|
|
_add_to_all(func_or_cls.__name__, module)
|
|
|
|
if funcify:
|
|
# We only support funcify-ing BaseLoaders, and only if __init__ and call_impl
|
|
# have no overlapping parameters.
|
|
from polygraphy.backend.base import BaseLoader
|
|
|
|
assert inspect.isclass(
|
|
func_or_cls
|
|
), "Decorated type must be a loader to use funcify=True"
|
|
assert BaseLoader in inspect.getmro(
|
|
func_or_cls
|
|
), "Decorated type must derive from BaseLoader to use funcify=True"
|
|
|
|
def get_params(method):
|
|
return list(
|
|
inspect.signature(
|
|
find_method(func_or_cls, method)
|
|
).parameters.values()
|
|
)[1:]
|
|
|
|
def is_variadic(param):
|
|
return param.kind in [param.VAR_POSITIONAL, param.VAR_KEYWORD]
|
|
|
|
def has_default(param):
|
|
return param.default != param.empty
|
|
|
|
def get_param_name(p):
|
|
# For variadic arguments, p.name will drop the *, **
|
|
return str(p) if is_variadic(p) else p.name
|
|
|
|
def param_names(params):
|
|
return [get_param_name(p) for p in params]
|
|
|
|
loader = func_or_cls
|
|
|
|
init_params = get_params("__init__")
|
|
call_impl_params = get_params("call_impl")
|
|
|
|
assert (
|
|
set(param_names(call_impl_params)) - set(param_names(init_params))
|
|
) == set(
|
|
param_names(call_impl_params)
|
|
), "Cannot funcify a type where call_impl and __init__ have the same argument names!"
|
|
|
|
# Dynamically generate a function with the right signature.
|
|
|
|
# To generate the signature, we use the init and call_impl arguments,
|
|
# but move required arguments (i.e. without default values) to the front.
|
|
|
|
def build_arg_list(should_include):
|
|
def str_from_param(p):
|
|
return get_param_name(p) + (
|
|
f"={p.default}" if has_default(p) else ""
|
|
)
|
|
|
|
arg_list = [str_from_param(p) for p in init_params if should_include(p)]
|
|
arg_list += [
|
|
str_from_param(p) for p in call_impl_params if should_include(p)
|
|
]
|
|
return arg_list
|
|
|
|
non_default_args = build_arg_list(
|
|
should_include=lambda p: not is_variadic(p) and not has_default(p)
|
|
)
|
|
default_args = build_arg_list(
|
|
should_include=lambda p: not is_variadic(p) and has_default(p)
|
|
)
|
|
special_args = build_arg_list(should_include=is_variadic)
|
|
|
|
signature = ", ".join(non_default_args + default_args + special_args)
|
|
|
|
init_args = ", ".join(param_names(init_params))
|
|
call_impl_args = ", ".join(param_names(call_impl_params))
|
|
|
|
def pascal_to_snake(name):
|
|
return "".join(
|
|
f"_{c.lower()}" if c.isupper() else c for c in name
|
|
).lstrip("_")
|
|
|
|
nonlocal func_name
|
|
func_name = func_name or pascal_to_snake(loader.__name__)
|
|
|
|
func_code = dedent(
|
|
f"""
|
|
def {func_name}({signature}):
|
|
return loader_binding({init_args})({call_impl_args})
|
|
|
|
func_var = {func_name}
|
|
"""
|
|
)
|
|
|
|
new_locals = {}
|
|
exec(
|
|
func_code,
|
|
# Need to bind the loader this way, or it won't be accesible from func_code.
|
|
{"loader_binding": loader},
|
|
new_locals,
|
|
)
|
|
func = new_locals["func_var"]
|
|
|
|
# Next we setup the docstring so that it is a combination of the __init__
|
|
# and call_impl docstrings.
|
|
func.__doc__ = f"Immediately evaluated functional variant of :class:`{loader.__name__}` .\n"
|
|
|
|
def try_add_method_doc(method):
|
|
call_impl = find_method(loader, method)
|
|
if call_impl.__doc__:
|
|
func.__doc__ += dedent(call_impl.__doc__)
|
|
|
|
try_add_method_doc("__init__")
|
|
try_add_method_doc("call_impl")
|
|
|
|
# Now that the function has been defined, we just need to add it into the module's
|
|
# __dict__ so it is accessible like a normal symbol.
|
|
_define_in_module(func_name, func, module)
|
|
|
|
# We don't actually want to modify the decorated object.
|
|
return func_or_cls
|
|
|
|
return export_impl
|
|
|
|
|
|
def warn_deprecated(
|
|
name, use_instead, remove_in, module_name=None, always_show_warning=False
|
|
):
|
|
|
|
if version(polygraphy.__version__) >= version(remove_in):
|
|
G_LOGGER.internal_error(
|
|
f"{name} should have been removed in version: {remove_in}"
|
|
)
|
|
|
|
full_obj_name = f"{module_name}.{name}" if module_name else name
|
|
msg = (
|
|
f"{full_obj_name} is deprecated and will be removed in Polygraphy {remove_in}."
|
|
)
|
|
if use_instead is not None:
|
|
msg += f" Use {use_instead} instead."
|
|
|
|
warnings.warn(msg, DeprecationWarning, stacklevel=3)
|
|
if always_show_warning:
|
|
G_LOGGER.warning(msg)
|
|
|
|
|
|
def deprecate(remove_in, use_instead, module_name=None, name=None):
|
|
"""
|
|
Decorator that marks a function or class as deprecated.
|
|
When the function or class is used, a warning will be issued.
|
|
|
|
Args:
|
|
remove_in (str):
|
|
The version in which the decorated type will be removed.
|
|
use_instead (str):
|
|
The function or class to use instead.
|
|
module_name (str):
|
|
The name of the containing module. This will be used to
|
|
generate more informative warnings.
|
|
Defaults to None.
|
|
name (str):
|
|
The name of the object being deprecated.
|
|
If not provided, this is automatically determined based on the decorated type.
|
|
Defaults to None.
|
|
"""
|
|
|
|
def deprecate_impl(obj):
|
|
if config.INTERNAL_CORRECTNESS_CHECKS and version(
|
|
polygraphy.__version__
|
|
) >= version(remove_in):
|
|
G_LOGGER.internal_error(
|
|
f"{obj} should have been removed in version: {remove_in}"
|
|
)
|
|
|
|
nonlocal name
|
|
name = name or obj.__name__
|
|
|
|
if inspect.ismodule(obj):
|
|
|
|
class DeprecatedModule:
|
|
def __getattr__(self, attr_name):
|
|
warn_deprecated(name, use_instead, remove_in, module_name)
|
|
self = obj
|
|
return getattr(self, attr_name)
|
|
|
|
def __setattr__(self, attr_name, value):
|
|
warn_deprecated(name, use_instead, remove_in, module_name)
|
|
self = obj
|
|
return setattr(self, attr_name, value)
|
|
|
|
DeprecatedModule.__doc__ = f"Deprecated: Use {use_instead} instead"
|
|
return DeprecatedModule()
|
|
elif inspect.isclass(obj):
|
|
|
|
class Deprecated(obj):
|
|
def __init__(self, *args, **kwargs):
|
|
warn_deprecated(name, use_instead, remove_in, module_name)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
Deprecated.__doc__ = f"Deprecated: Use {use_instead} instead"
|
|
return Deprecated
|
|
elif inspect.isfunction(obj):
|
|
|
|
def wrapped(*args, **kwargs):
|
|
warn_deprecated(name, use_instead, remove_in, module_name)
|
|
return obj(*args, **kwargs)
|
|
|
|
wrapped.__doc__ = f"Deprecated: Use {use_instead} instead"
|
|
return wrapped
|
|
else:
|
|
G_LOGGER.internal_error(f"deprecate is not implemented for: {obj}")
|
|
|
|
return deprecate_impl
|
|
|
|
|
|
def export_deprecated_alias(name, remove_in, use_instead=None):
|
|
"""
|
|
Decorator that creates and exports a deprecated alias for
|
|
the decorated class or function.
|
|
|
|
The alias will behave like the decorated type, except it will
|
|
issue a deprecation warning when used.
|
|
|
|
To create a deprecated alias for an entire module, invoke the
|
|
function manually within the module like so:
|
|
::
|
|
|
|
mod.export_deprecated_alias("old_mod_name", remove_in="0.0.0")(sys.modules[__name__])
|
|
|
|
Args:
|
|
name (str):
|
|
The name of the deprecated alias.
|
|
remove_in (str):
|
|
The version, as a string, in which the deprecated alias will be removed.
|
|
use_instead (str):
|
|
The name of the function, class, or module to use instead.
|
|
If this is ``None``, the new name will be automatically determined.
|
|
Defaults to None.
|
|
"""
|
|
module = inspect.getmodule(sys._getframe(1))
|
|
|
|
def export_deprecated_alias_impl(obj):
|
|
new_obj = deprecate(
|
|
remove_in,
|
|
use_instead=use_instead or obj.__name__,
|
|
module_name=module.__name__,
|
|
name=name,
|
|
)(obj)
|
|
_define_in_module(name, new_obj, module)
|
|
return obj
|
|
|
|
return export_deprecated_alias_impl
|