1900 lines
66 KiB
Python
1900 lines
66 KiB
Python
# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# 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.
|
|
|
|
from __future__ import annotations
|
|
|
|
import collections
|
|
import inspect
|
|
import os
|
|
import threading
|
|
import warnings
|
|
import weakref
|
|
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
|
|
from typing_extensions import ParamSpec, Self
|
|
|
|
import paddle
|
|
import paddle.pir.core as ir_static
|
|
from paddle import decomposition, get_flags
|
|
from paddle.base import core, framework
|
|
from paddle.base.data_feeder import check_type
|
|
from paddle.base.dygraph.base import (
|
|
param_guard,
|
|
switch_to_static_graph,
|
|
to_static_mode_guard,
|
|
)
|
|
from paddle.framework import in_dynamic_mode, use_pir_api
|
|
from paddle.nn.layer import layers
|
|
from paddle.pir import Value
|
|
from paddle.utils import flatten, gast
|
|
|
|
from . import error, logging_utils
|
|
from .function_spec import (
|
|
FunctionSpec,
|
|
_hash_spec_names,
|
|
get_buffers,
|
|
get_parameters,
|
|
)
|
|
from .origin_info import (
|
|
attach_origin_info,
|
|
create_and_update_origin_info_map,
|
|
update_op_callstack_with_origin_info,
|
|
)
|
|
from .partial_program import PartialProgramLayer, PartialProgramLayerHook
|
|
from .pir_partial_program import (
|
|
PartialProgramLayer as PirPartialProgramLayer,
|
|
PartialProgramLayerHook as PirPartialProgramLayerHook,
|
|
)
|
|
from .transformers import DygraphToStaticAst
|
|
from .utils import (
|
|
ALREADY_D2S,
|
|
NO_SHAPE_VAR_TYPE,
|
|
TransformOptions,
|
|
ast_to_func,
|
|
backend_guard,
|
|
cuda_pinned_tensors_move_to_excepted_place,
|
|
func_to_source_code,
|
|
graph_tracing_guard,
|
|
input_specs_compatible,
|
|
is_paddle_func,
|
|
make_hashable,
|
|
prim_or_cinn_is_enabled,
|
|
type_name,
|
|
use_specialized_device,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
from paddle._typing import NestedSequence
|
|
from paddle.static import InputSpec, Program
|
|
from paddle.static.amp.fp16_utils import AmpOptions
|
|
|
|
_RetT = TypeVar("_RetT")
|
|
_InputT = ParamSpec("_InputT")
|
|
|
|
__all__ = []
|
|
|
|
# For each traced function, we set `max_traced_program_count` = 10 to consider caching performance.
|
|
# Once exceeding the threshold, we will raise warning to users to make sure the conversion is as expected.
|
|
MAX_TRACED_PROGRAM_COUNT = 10
|
|
|
|
|
|
def synchronized(func):
|
|
func.__lock__ = threading.Lock()
|
|
|
|
def lock_func(*args, **kwargs):
|
|
with func.__lock__:
|
|
return func(*args, **kwargs)
|
|
|
|
return lock_func
|
|
|
|
|
|
def show_op_callstack(op):
|
|
op_callstack = op.callstack
|
|
target_lines = {
|
|
"outputs = static_func(*inputs)",
|
|
"outputs = static_func(*inputs, **_kwargs)",
|
|
}
|
|
op_callstack_message = ""
|
|
for index, line in enumerate(op_callstack):
|
|
if line.strip() in target_lines:
|
|
op_callstack_result = '\n'.join(op_callstack[index + 1 :])
|
|
op_callstack_message = (
|
|
f"In transformed code:\n\n{op_callstack_result}\n\n"
|
|
)
|
|
raise ValueError(
|
|
f"{op_callstack_message}Sorry about what's happened. In to_static mode, {op.name()}'s output variable is a viewed Tensor in dygraph. "
|
|
f"This will result in inconsistent calculation behavior between dynamic and static graphs. "
|
|
f"You must find the location of the strided ops be called, and call paddle.assign() before inplace input. "
|
|
f"If you certainly make sure it's safe, you can set env stride_in_no_check_dy2st_diff to 1."
|
|
)
|
|
|
|
|
|
def check_view_api_used_by_inplace(program: paddle.pir.Program) -> None:
|
|
"""
|
|
check viewed value used by inplace op in pir mode.
|
|
|
|
Two scenarios will raise ValueError:
|
|
# one
|
|
a = transpose(b)
|
|
a.add_(c)
|
|
# two
|
|
a = transpose(b)
|
|
b.add_(c)
|
|
"""
|
|
# TODO(ooooo): Deal with these inplace ops
|
|
skipped_inplace_ops = [
|
|
"pd_op.set_value_",
|
|
"pd_op.set_value_with_tensor_",
|
|
# It willn't change tensor imdeiately,but it's output is dangerous.
|
|
"pd_op.share_data_",
|
|
]
|
|
|
|
def val_is_used_by_stride_op(op, val):
|
|
return op.name() in framework.stride_ops and op.operand_source(
|
|
0
|
|
).is_same(val)
|
|
|
|
def is_used_by_inplace_op(op, val, info):
|
|
return op.name().endswith("_") and any(
|
|
op.operand_source(index).is_same(val) for index in info.values()
|
|
)
|
|
|
|
all_vars_list = program.list_vars()
|
|
for value in all_vars_list:
|
|
used_by_stride_ops = []
|
|
for op in reversed(value.all_used_ops()):
|
|
inplace_info = paddle.core.pir.get_op_inplace_info(op)
|
|
if val_is_used_by_stride_op(op, value):
|
|
used_by_stride_ops.append(op)
|
|
if is_used_by_inplace_op(op, value, inplace_info):
|
|
if op.name() in skipped_inplace_ops:
|
|
continue
|
|
if value.get_defining_op().name() in framework.stride_ops:
|
|
show_op_callstack(op)
|
|
if len(used_by_stride_ops) == 0:
|
|
continue
|
|
show_op_callstack(op)
|
|
|
|
|
|
class FunctionCache:
|
|
"""
|
|
Caches the transformed functions to avoid redundant conversions of the same function.
|
|
"""
|
|
|
|
def __init__(self):
|
|
# Caches the converted static functions. {dygraph_func: static_func}
|
|
self._converted_static_func_caches = weakref.WeakKeyDictionary()
|
|
# Caches the converted ast node for same source code. {source_code: ast_root}
|
|
self._code_to_ast_caches = {}
|
|
self._dygraph_to_static = DygraphToStaticAst()
|
|
|
|
def convert_with_cache(self, func):
|
|
"""
|
|
Returns the cached static function or converts it when first encounters the function.
|
|
"""
|
|
# If hit cache, return it directly.
|
|
static_func = self._converted_static_func_caches.get(func, None)
|
|
|
|
if static_func is None:
|
|
static_func = self._convert(func)
|
|
self._converted_static_func_caches[func] = static_func
|
|
|
|
return static_func
|
|
|
|
def _convert(self, func):
|
|
"""
|
|
Converts dygraph function into static function. For two functions with same dedent code,
|
|
the second function will reuse the transformed ast node of previous one.
|
|
|
|
For example:
|
|
# A.py
|
|
def foo(x, y):
|
|
z = x + y
|
|
return z
|
|
|
|
# B.py
|
|
def foo(x, y):
|
|
z = x + y
|
|
return z
|
|
|
|
If the conversion of A.foo happens after B.foo, it will reuse the transformed ast node of B.foo
|
|
to speed up the conversion.
|
|
"""
|
|
func = inspect.unwrap(func)
|
|
source_code = func_to_source_code(func)
|
|
|
|
# TODO(liym27):
|
|
# Consider this case: source_code in self._code_to_ast_caches,
|
|
# but actually they are methods in different classes.
|
|
# Maybe use (__class__, source_code) as key
|
|
if source_code in self._code_to_ast_caches:
|
|
root = self._code_to_ast_caches[source_code]
|
|
else:
|
|
root = gast.parse(source_code)
|
|
root = attach_origin_info(root, func)
|
|
root = self._dygraph_to_static.get_static_ast(root)
|
|
self._code_to_ast_caches[source_code] = root
|
|
|
|
# Get static function from AST
|
|
static_func, file_name = ast_to_func(root, func)
|
|
|
|
create_and_update_origin_info_map(root, static_func)
|
|
return static_func
|
|
|
|
def exist(self, func):
|
|
return func in self._converted_static_func_caches
|
|
|
|
|
|
_CACHE_LOCK = threading.Lock()
|
|
_FUNCTION_CACHE = FunctionCache()
|
|
|
|
|
|
def convert_to_static(function):
|
|
"""
|
|
Transforms function of dygraph into static function using the cache mechanism.
|
|
|
|
Note(dev): It will return function.__func__ if encountering class method.
|
|
|
|
Args:
|
|
function(callable): The function with dygraph layers that will be converted into static layers.
|
|
"""
|
|
if getattr(function, ALREADY_D2S, None):
|
|
return function
|
|
|
|
# Return directly if decorated with @jit.marker.unified and DO NOT Cache it
|
|
# or ignore paddle api
|
|
need_skip = (
|
|
not TransformOptions.check_fn_need_transform(
|
|
function, TransformOptions.ToStaticMode.AST
|
|
)
|
|
) or is_paddle_func(function)
|
|
if need_skip:
|
|
return function.__func__ if inspect.ismethod(function) else function
|
|
|
|
with _CACHE_LOCK:
|
|
static_func = _FUNCTION_CACHE.convert_with_cache(function)
|
|
setattr(static_func, ALREADY_D2S, True)
|
|
return static_func
|
|
|
|
|
|
class CacheKey:
|
|
"""
|
|
Cached key for ProgramCache.
|
|
"""
|
|
|
|
__slots__ = [
|
|
'function_spec',
|
|
'input_args_with_spec',
|
|
'input_kwargs_with_spec',
|
|
'class_instance',
|
|
'is_grad_enabled',
|
|
'kwargs',
|
|
'_spec_names_id',
|
|
'_pir_flags',
|
|
]
|
|
|
|
def __init__(
|
|
self,
|
|
function_spec,
|
|
input_args_with_spec,
|
|
input_kwargs_with_spec,
|
|
class_instance,
|
|
**kwargs,
|
|
):
|
|
"""
|
|
Initializes a cache key.
|
|
|
|
Args:
|
|
functions_spec(FunctionSpec): a FunctionSpec instance of decorated function.
|
|
input_args_with_spec(list[InputSpec]): actual input args with some arguments replaced by InputSpec.
|
|
input_kwargs_with_spec(list[{string:InputSpec}]): actual input kwargs with some arguments replaced by InputSpec.
|
|
class_instance(object): a instance of class `Layer`.
|
|
**kwargs(dict): manage other arguments used for better scalability
|
|
"""
|
|
self.function_spec = function_spec
|
|
self.input_args_with_spec = input_args_with_spec
|
|
self.input_kwargs_with_spec = input_kwargs_with_spec
|
|
self.class_instance = class_instance
|
|
self.is_grad_enabled = paddle.is_grad_enabled()
|
|
# NOTE: `kwargs` is usually not considered as basic member for `__hash__`
|
|
self.kwargs = kwargs
|
|
self._spec_names_id = _hash_spec_names(
|
|
input_args_with_spec, input_kwargs_with_spec
|
|
)
|
|
self._pir_flags = (
|
|
get_flags('FLAGS_enable_pir_in_executor')[
|
|
'FLAGS_enable_pir_in_executor'
|
|
]
|
|
or get_flags('FLAGS_enable_pir_with_pt_in_dy2st')[
|
|
'FLAGS_enable_pir_with_pt_in_dy2st'
|
|
]
|
|
)
|
|
|
|
@classmethod
|
|
def from_func_and_args(cls, function_spec, args, kwargs, class_instance):
|
|
"""
|
|
Generated a CacheKey instance by given inputs.
|
|
|
|
Args:
|
|
functions_spec(FunctionSpec): a FunctionSpec instance of decorated function.
|
|
args(tuple): tuple of actual inputs arguments.
|
|
kwargs(dict): dict of actual inputs keyword arguments.
|
|
class_instance(object): a instance of class `Layer`.
|
|
"""
|
|
# 1. filter `self` in args
|
|
if args and isinstance(args[0], layers.Layer):
|
|
args = args[1:]
|
|
# 2. convert tensor and numpy array into InputSpec
|
|
_args, _kwargs = function_spec.unified_args_and_kwargs(args, kwargs)
|
|
(
|
|
input_args_with_spec,
|
|
input_kwargs_with_spec,
|
|
) = function_spec.args_to_input_spec(_args, _kwargs)
|
|
|
|
# 3. check whether hit the cache or build a new program for the input arguments
|
|
return CacheKey(
|
|
function_spec,
|
|
input_args_with_spec,
|
|
input_kwargs_with_spec,
|
|
class_instance,
|
|
)
|
|
|
|
def __hash__(self):
|
|
error_msg = "Arguments to a `@paddle.jit.to_static` must be a hashable Python objects (or nested structures of these types)."
|
|
with_hook = self.kwargs.get("with_hook", False)
|
|
is_train = self.kwargs.get("is_train", False)
|
|
return hash(
|
|
(
|
|
id(self.function_spec),
|
|
make_hashable(self.input_args_with_spec, error_msg),
|
|
make_hashable(self.input_kwargs_with_spec, error_msg),
|
|
self._spec_names_id,
|
|
self.class_instance,
|
|
with_hook,
|
|
is_train,
|
|
self._pir_flags,
|
|
use_pir_api(),
|
|
self.is_grad_enabled,
|
|
)
|
|
)
|
|
|
|
def __eq__(self, other):
|
|
return (type(self) is type(other)) and hash(self) == hash(other)
|
|
|
|
def __neq__(self, other):
|
|
return not self == other
|
|
|
|
def __repr__(self):
|
|
return f"id(function_spec): {id(self.function_spec)}, input_args_with_spec: {self.input_args_with_spec}, input_kwargs_with_spec: {self.input_kwargs_with_spec}, class_instance: {self.class_instance}"
|
|
|
|
|
|
def unwrap_decorators(func):
|
|
"""
|
|
Unwraps a decorated function and returns the decorator list and inner target.
|
|
"""
|
|
decorators = []
|
|
cur = func
|
|
while True:
|
|
if isinstance(cur, StaticFunction):
|
|
decorators.append(cur)
|
|
# Note: if `cur` is a method, keep it as bound method of class.
|
|
instance = cur.class_instance
|
|
if instance is not None:
|
|
cur = cur.dygraph_function.__get__(instance)
|
|
else:
|
|
cur = cur.dygraph_function
|
|
else:
|
|
break
|
|
return decorators, cur
|
|
|
|
|
|
class StaticFunction(Generic[_InputT, _RetT]):
|
|
def __init__(self, function, input_spec=None, **kwargs):
|
|
"""
|
|
Initializes a `StaticFunction`.
|
|
|
|
Args:
|
|
function(callable): A function or method that will be converted into static program.
|
|
input_spec(list[InputSpec]): list of InputSpec to specify the `shape/dtype/name` information for each input argument, default None.
|
|
**kwargs(dict): other arguments like `build_strategy` et.al.
|
|
"""
|
|
# save the instance `self` while decorating a method of class.
|
|
|
|
if inspect.ismethod(function):
|
|
self._dygraph_function = function.__func__
|
|
self._class_instance = weakref.ref(function.__self__)
|
|
|
|
if not hasattr(self.class_instance, '_original_funcs'):
|
|
raise TypeError(
|
|
"When using 'to_static' to convert method of a class, "
|
|
"please ensure the class inherits from nn.Layer"
|
|
)
|
|
self.class_instance._original_funcs[function.__name__] = (
|
|
self._dygraph_function
|
|
)
|
|
else:
|
|
self._dygraph_function = function
|
|
self._class_instance = None
|
|
|
|
self._input_spec = input_spec
|
|
self._function_spec = FunctionSpec(function, input_spec)
|
|
self._program_cache = ProgramCache()
|
|
self._descriptor_cache = weakref.WeakKeyDictionary()
|
|
# Note: Hold a reference to ProgramTranslator for switching `enable_to_static`.
|
|
self._program_trans = ProgramTranslator()
|
|
self._kwargs = kwargs
|
|
self._training = True
|
|
self._property = kwargs.get("property", False)
|
|
# Note: Record the patched method name for rollback.
|
|
self._patched_name = None
|
|
|
|
@property
|
|
def is_property(self) -> bool:
|
|
# whether is class proproty to be exported.
|
|
return self._property
|
|
|
|
def train(self) -> None:
|
|
if (
|
|
isinstance(self.class_instance, layers.Layer)
|
|
and self.class_instance.training is False
|
|
):
|
|
raise RuntimeError(
|
|
f"Failed to switch train mode. {self.dygraph_function} is a Layer's method, "
|
|
"please use Layer.train() to switch train mode."
|
|
)
|
|
self._training = True
|
|
|
|
def eval(self) -> None:
|
|
if (
|
|
isinstance(self.class_instance, layers.Layer)
|
|
and self.class_instance.training is True
|
|
):
|
|
raise RuntimeError(
|
|
f"Failed to switch eval mode. {self.dygraph_function} is a Layer's method, "
|
|
"please use Layer.eval() to switch eval mode."
|
|
)
|
|
self._training = False
|
|
|
|
def __get__(self, instance, owner):
|
|
"""
|
|
Overrides this method to parse the class instance and call bound method correctly.
|
|
|
|
For example:
|
|
|
|
'''
|
|
class Net(Layer):
|
|
def __init__(self):
|
|
pass
|
|
|
|
@paddle.jit.to_static
|
|
def forward(self, x, y):
|
|
return x + y
|
|
|
|
net = Net()
|
|
out = net(x, y)
|
|
'''
|
|
|
|
In above case, `net(x, y)` will call `net.forward(x, y)` firstly that is a bound method
|
|
of `Net` instance. After decorated by `@paddle.jit.to_static`, it will firstly to call `__get__`
|
|
to parse the class instance correctly instead of the `StaticFunction` instance.
|
|
"""
|
|
if instance not in self._descriptor_cache:
|
|
if instance is None:
|
|
return self
|
|
# Note(Aurelius84): To construct new instance of StaticFunction when we
|
|
# first encounter the bound function of layer and cache it.
|
|
new_static_layer = self._clone()
|
|
if (
|
|
isinstance(instance, layers.Layer)
|
|
and hasattr(instance, "_original_funcs")
|
|
and self._dygraph_function.__name__
|
|
not in instance._original_funcs.keys()
|
|
):
|
|
instance._original_funcs[self._dygraph_function.__name__] = (
|
|
self._dygraph_function
|
|
)
|
|
new_static_layer._class_instance = weakref.ref(instance)
|
|
self._descriptor_cache[instance] = new_static_layer
|
|
|
|
return self._descriptor_cache[instance]
|
|
|
|
def _clone(self) -> Self:
|
|
return self.__class__(
|
|
self.dygraph_function, self._input_spec, **self._kwargs
|
|
)
|
|
|
|
def __call__(self, *args: _InputT.args, **kwargs: _InputT.kwargs) -> _RetT:
|
|
"""
|
|
Supports to call the returned instance with input `args` and `kwargs` directly.
|
|
|
|
Args:
|
|
*args(tuple): tuple of all input arguments from original decorated function.
|
|
**kwargs(dict): dict of all input keyword arguments from original decorated function.
|
|
|
|
Return:
|
|
Outputs of decorated function.
|
|
"""
|
|
if self._property:
|
|
return self._call_dygraph_function(*args, **kwargs)
|
|
|
|
# 1. call dygraph function directly if not enable `declarative`
|
|
if not self._program_trans.enable_to_static:
|
|
# NOTE(liym27):
|
|
# Here calls `warnings.warn` but not `logging_utils.warn` because by default warnings.warn(message)
|
|
# will show up **only once**. StaticFunction.__call__ will run many times, it is appropriate to
|
|
# display this warning message only once.
|
|
logging_utils.warn(
|
|
"The decorator '@paddle.jit.to_static' does NOT work when setting 'paddle.jit.enable_to_static' to False. "
|
|
"We will just return dygraph output. If you would like to get static graph output, please call API "
|
|
"paddle.jit.enable_to_static(True)"
|
|
)
|
|
return self._call_dygraph_function(*args, **kwargs)
|
|
|
|
if not in_dynamic_mode():
|
|
raise RuntimeError(
|
|
f"Failed to run the callable object {self.dygraph_function} decorated by '@paddle.jit.to_static', "
|
|
"because it is NOT in dynamic mode. Please disable the static graph mode to enter dynamic mode with the "
|
|
"following API: paddle.disable_static()."
|
|
)
|
|
|
|
return self._perform_call(*args, **kwargs)
|
|
|
|
def _is_train_mode(self) -> bool:
|
|
if self.class_instance is not None:
|
|
if not hasattr(self.class_instance, 'training'):
|
|
raise TypeError(
|
|
"When using 'to_static' to convert method of a class, "
|
|
"please ensure the class inherits from nn.Layer"
|
|
)
|
|
return self.class_instance.training
|
|
else:
|
|
return self._training
|
|
|
|
def _call_dygraph_function(
|
|
self, *args: _InputT.args, **kwargs: _InputT.kwargs
|
|
) -> _RetT:
|
|
"""
|
|
Calls dygraph function directly and returns the outputs.
|
|
|
|
Args:
|
|
*args(tuple): tuple of all input arguments from original decorated function.
|
|
**kwargs(dict): dict of all input keyword arguments from original decorated function.
|
|
|
|
Return:
|
|
Outputs of dygraph function.
|
|
"""
|
|
return self.dygraph_function(*args, **kwargs)
|
|
|
|
def _raise_when_property(self):
|
|
"""raise RuntimeError when property=True
|
|
|
|
Raises:
|
|
RuntimeError: can not call this func when property=True
|
|
"""
|
|
if self.is_property:
|
|
raise RuntimeError("Can not call the func when property=True.")
|
|
|
|
def get_concrete_program(
|
|
self, *args: _InputT.args, **kwargs: _InputT.kwargs
|
|
) -> tuple[ConcreteProgram, PirPartialProgramLayer]:
|
|
raise NotImplementedError("Not implemented yet.")
|
|
|
|
def get_concrete_program_with_cache_key(self, cached_key):
|
|
raise NotImplementedError("Not implemented yet.")
|
|
|
|
def get_traced_count(self):
|
|
raise NotImplementedError("Not implemented yet.")
|
|
|
|
@property
|
|
def code(self) -> str:
|
|
raise NotImplementedError("Not implemented yet.")
|
|
|
|
@property
|
|
def class_instance(self):
|
|
if self._class_instance is None:
|
|
return None
|
|
if self._class_instance() is None:
|
|
raise RuntimeError(
|
|
"The instance of class has been deleted, please re-create the instance."
|
|
)
|
|
return self._class_instance()
|
|
|
|
@property
|
|
def dygraph_function(self) -> Callable[_InputT, _RetT]:
|
|
"""
|
|
Returns the original decorated function.
|
|
"""
|
|
if self.class_instance is not None:
|
|
return self._dygraph_function.__get__(self.class_instance)
|
|
else:
|
|
return self._dygraph_function
|
|
|
|
@property
|
|
def concrete_program(self) -> ConcreteProgram:
|
|
raise NotImplementedError("Not implemented yet.")
|
|
|
|
def concrete_program_specify_input_spec(
|
|
self,
|
|
input_spec: NestedSequence[InputSpec] | None = None,
|
|
with_hook: bool = False,
|
|
is_prim_infer: bool = False,
|
|
):
|
|
raise NotImplementedError("Not implemented yet.")
|
|
|
|
def rollback(self) -> Callable[_InputT, _RetT]:
|
|
"""
|
|
Rollback into original dygraph functions for current class instance.
|
|
|
|
Returns:
|
|
Function or Method
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> # doctest: +SKIP('`paddle.jit.to_static` can not run in xdoctest')
|
|
>>> import paddle
|
|
|
|
>>> class Net(paddle.nn.Layer):
|
|
... def __init__(self):
|
|
... super().__init__()
|
|
...
|
|
... def forward(self, x, flag=True):
|
|
... if flag:
|
|
... out = x + 1
|
|
... else:
|
|
... out = x - 1
|
|
... return out
|
|
>>> x = paddle.randn([10, 1], 'float32')
|
|
>>> net = paddle.jit.to_static(Net()) # convert into static graph mode
|
|
>>> out = net(x)
|
|
|
|
>>> net.forward.rollback() # rollback into dygraph mode
|
|
>>> out = net(x)
|
|
"""
|
|
|
|
if self.class_instance is None:
|
|
return self._dygraph_function
|
|
|
|
# only rollback sub-functions on path of top _dygraph_function
|
|
fn_name = (
|
|
self._patched_name
|
|
if self._patched_name is not None
|
|
else self._dygraph_function.__name__
|
|
)
|
|
assert fn_name in self.class_instance._original_funcs, (
|
|
f"Not Found function '{fn_name}' in class '{self.class_instance.__class__}'."
|
|
)
|
|
func = self.class_instance._original_funcs[fn_name]
|
|
setattr(self.class_instance, fn_name, func.__get__(self.class_instance))
|
|
return getattr(self.class_instance, fn_name)
|
|
|
|
def __deepcopy__(self, memo):
|
|
"""
|
|
Customized behavior for copy.deepcopy, return a new StaticFunction instance.
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> import copy
|
|
>>> import paddle
|
|
|
|
>>> class Net(paddle.nn.Layer):
|
|
... def __init__(self):
|
|
... super().__init__()
|
|
...
|
|
... def forward(self, x, flag=True):
|
|
... if flag:
|
|
... out = x + 1
|
|
... else:
|
|
... out = x - 1
|
|
... return out
|
|
>>> x = paddle.randn([10, 1], 'float32')
|
|
>>> net = paddle.jit.to_static(Net()) # convert into static graph mode
|
|
|
|
>>> copy_net = copy.deepcopy(net) # still in static graph mode
|
|
"""
|
|
if self.class_instance is not None:
|
|
copied_static_fn = type(self)(
|
|
self._dygraph_function, self._input_spec, **self._kwargs
|
|
)
|
|
copied_static_fn._training = self._training
|
|
copied_static_fn._program_cache = self._program_cache
|
|
copied_static_fn._descriptor_cache = self._descriptor_cache
|
|
copied_static_fn._patched_name = self._patched_name
|
|
return copied_static_fn.__get__(
|
|
memo[id(self.class_instance)], type(self.class_instance)
|
|
)
|
|
else:
|
|
return self._dygraph_function
|
|
|
|
@property
|
|
def inputs(self) -> list[Any]:
|
|
raise NotImplementedError("Not implemented yet.")
|
|
|
|
@property
|
|
def outputs(self) -> list[Any]:
|
|
raise NotImplementedError("Not implemented yet.")
|
|
|
|
@property
|
|
def main_program(self) -> Program:
|
|
raise NotImplementedError("Not implemented yet.")
|
|
|
|
@property
|
|
def program_cache(self) -> ProgramCache:
|
|
raise NotImplementedError("Not implemented yet.")
|
|
|
|
@property
|
|
def function_spec(self) -> FunctionSpec:
|
|
raise NotImplementedError("Not implemented yet.")
|
|
|
|
|
|
def raise_error_template(func_str):
|
|
def _raise_error(*args, **kwargs):
|
|
error_template = (
|
|
"Can't call {func} when full_graph=False. "
|
|
"Use paddle.jit.to_static(full_graph=True) instead."
|
|
)
|
|
raise RuntimeError(error_template.format(func=func_str))
|
|
|
|
return _raise_error
|
|
|
|
|
|
class SymbolicStaticFunction(StaticFunction):
|
|
def __init__(self, function, input_spec=None, **kwargs):
|
|
if input_spec is not None:
|
|
warnings.warn(
|
|
"full_graph=False don't support input_spec arguments. It will not produce any effect.\n"
|
|
"You can set full_graph=True, then you can assign input spec.\n"
|
|
)
|
|
super().__init__(function, input_spec, **kwargs)
|
|
self.last_call_input_spec = None
|
|
|
|
def _perform_call(self, *args, **kwargs):
|
|
from ..sot import symbolic_translate
|
|
|
|
args, kwargs = self._function_spec.unified_args_and_kwargs(args, kwargs)
|
|
if not use_specialized_device():
|
|
cuda_pinned_tensors_move_to_excepted_place(args)
|
|
|
|
(
|
|
input_args_with_spec,
|
|
input_kwargs_with_spec,
|
|
) = self._function_spec.args_to_input_spec(args, kwargs)
|
|
self.last_call_input_spec = input_args_with_spec
|
|
|
|
build_strategy = self._kwargs.get("build_strategy", None)
|
|
backend = self._kwargs.get("backend", None)
|
|
traced_fun = symbolic_translate(
|
|
self._dygraph_function,
|
|
build_strategy=build_strategy,
|
|
training=self._is_train_mode(),
|
|
backend=backend,
|
|
)
|
|
if self.class_instance is not None:
|
|
args = (self.class_instance, *args)
|
|
return traced_fun(*args, **kwargs)
|
|
|
|
@property
|
|
def code(self):
|
|
raise_error_template("code")()
|
|
|
|
@property
|
|
def concrete_program(self):
|
|
raise_error_template("concrete_program")()
|
|
|
|
concrete_program_specify_input_spec = raise_error_template(
|
|
"concrete_program_specify_input_spec"
|
|
)
|
|
get_concrete_program = raise_error_template("get_concrete_program")
|
|
get_concrete_program_with_cache_key = raise_error_template(
|
|
"get_concrete_program_with_cache_key"
|
|
)
|
|
get_traced_count = raise_error_template("get_traced_count")
|
|
|
|
@property
|
|
def inputs(self):
|
|
raise_error_template("inputs")()
|
|
|
|
@property
|
|
def outputs(self):
|
|
raise_error_template("outputs")()
|
|
|
|
@property
|
|
def main_program(self):
|
|
raise_error_template("main_program")()
|
|
|
|
@property
|
|
def program_cache(self):
|
|
raise_error_template("program_cache")()
|
|
|
|
@property
|
|
def function_spec(self):
|
|
raise_error_template("function_spec")()
|
|
|
|
|
|
class ASTStaticFunction(StaticFunction[_InputT, _RetT]):
|
|
"""
|
|
Wrapper class to Manage program conversion of decorated function.
|
|
|
|
"""
|
|
|
|
def __init__(self, function, input_spec=None, **kwargs):
|
|
super().__init__(function, input_spec, **kwargs)
|
|
|
|
def _perform_call(self, *args, **kwargs):
|
|
# 1. trace ops from dygraph layers and cache the generated program.
|
|
args, kwargs = self._function_spec.unified_args_and_kwargs(args, kwargs)
|
|
|
|
try:
|
|
_, partial_program_layer = self.get_concrete_program(
|
|
*args, **kwargs, is_train=self._is_train_mode()
|
|
)
|
|
# 2. synchronize self.training attribute.
|
|
if isinstance(self.class_instance, layers.Layer):
|
|
partial_program_layer.training = self.class_instance.training
|
|
else:
|
|
partial_program_layer.training = self._training
|
|
|
|
# 3. return outputs.
|
|
try:
|
|
return partial_program_layer(args)
|
|
except Exception as e:
|
|
if not hasattr(e, error.ERROR_DATA):
|
|
# runtime error
|
|
error.attach_error_data(e, in_runtime=True)
|
|
raise
|
|
except Exception as e:
|
|
error_data = getattr(e, error.ERROR_DATA, None)
|
|
if error_data:
|
|
error_data.raise_new_exception()
|
|
else:
|
|
logging_utils.warn(
|
|
"Please file an issue at 'https://github.com/PaddlePaddle/Paddle/issues'"
|
|
f" if you can't handle this {type(e)} yourself."
|
|
)
|
|
raise e
|
|
|
|
def get_concrete_program(
|
|
self, *args: _InputT.args, **kwargs: _InputT.kwargs
|
|
) -> tuple[ConcreteProgram, PirPartialProgramLayer]:
|
|
"""
|
|
Returns traced concrete program and inner executable partial layer.
|
|
|
|
Args:
|
|
*args(tuple): input arguments values or InputSpec
|
|
**kwargs(dict) : input kwargs values.
|
|
|
|
Returns:
|
|
Traced ConcreteProgram and executable translated Layer.
|
|
"""
|
|
self._raise_when_property()
|
|
|
|
with_hook = kwargs.get("with_hook", False)
|
|
is_train = kwargs.get("is_train", True)
|
|
is_prim_infer = kwargs.get("is_prim_infer", False)
|
|
if "is_train" in kwargs:
|
|
kwargs.pop("is_train")
|
|
if "with_hook" in kwargs:
|
|
kwargs.pop("with_hook")
|
|
if "is_prim_infer" in kwargs:
|
|
kwargs.pop("is_prim_infer")
|
|
# 1. unify args/kwargs and replace Tensor with InputSpec
|
|
if len(args) != len(self._function_spec.args_name):
|
|
args, kwargs = self._function_spec.unified_args_and_kwargs(
|
|
args, kwargs
|
|
)
|
|
(
|
|
input_args_with_spec,
|
|
input_kwargs_with_spec,
|
|
) = self._function_spec.args_to_input_spec(args, kwargs)
|
|
|
|
# 2. generate cache key
|
|
cache_key = CacheKey(
|
|
self._function_spec,
|
|
input_args_with_spec,
|
|
input_kwargs_with_spec,
|
|
self.class_instance,
|
|
**self._kwargs,
|
|
with_hook=with_hook,
|
|
is_train=is_train,
|
|
)
|
|
if is_prim_infer:
|
|
(
|
|
concrete_program,
|
|
partial_program_layer,
|
|
) = self._program_cache.get_program_without_cache(cache_key)
|
|
else:
|
|
# 3. check whether hit the cache or build a new program for the input arguments
|
|
concrete_program, partial_program_layer = self._program_cache[
|
|
cache_key
|
|
]
|
|
return concrete_program, partial_program_layer
|
|
|
|
def get_concrete_program_with_cache_key(
|
|
self, cached_key: CacheKey
|
|
) -> tuple[ConcreteProgram, PartialProgramLayer | PirPartialProgramLayer]:
|
|
"""
|
|
Returns traced concrete program and inner executable partial layer by cached key.
|
|
|
|
Args:
|
|
cached_key(CacheKey): The cached key use to get concrete program.
|
|
|
|
Returns:
|
|
Traced ConcreteProgram and executable translated Layer.
|
|
"""
|
|
self._raise_when_property()
|
|
(
|
|
concrete_program,
|
|
partial_program_layer,
|
|
) = self._program_cache.get_program_without_cache(cached_key)
|
|
return concrete_program, partial_program_layer
|
|
|
|
def get_traced_count(self) -> int:
|
|
"""
|
|
Returns the number of traced programs for the decorated function.
|
|
"""
|
|
return len(self._program_cache)
|
|
|
|
@property
|
|
def code(self) -> str:
|
|
"""
|
|
Returns the source code of transformed static function for debugging.
|
|
"""
|
|
static_func = convert_to_static(self.dygraph_function)
|
|
source_code = func_to_source_code(static_func)
|
|
return source_code
|
|
|
|
@property
|
|
def concrete_program(self) -> ConcreteProgram:
|
|
"""
|
|
Returns recent ConcreteProgram instance of decorated function.
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> # doctest: +SKIP('`paddle.jit.to_static` can not run in xdoctest')
|
|
>>> import paddle
|
|
>>> from paddle.jit import to_static
|
|
>>> from paddle.static import InputSpec
|
|
|
|
>>> paddle.disable_static()
|
|
|
|
>>> def foo(x, y):
|
|
... z = x + y
|
|
... return z
|
|
>>> # usage 1:
|
|
>>> decorated_foo = to_static(foo, input_spec=[InputSpec([10], name='x'), InputSpec([10], name='y')])
|
|
>>> print(decorated_foo.concrete_program)
|
|
|
|
>>> # usage 2:
|
|
>>> decorated_foo = to_static(foo)
|
|
>>> out_foo = decorated_foo(paddle.rand([10]), paddle.rand([10]))
|
|
>>> print(decorated_foo.concrete_program)
|
|
"""
|
|
return self.concrete_program_specify_input_spec(input_spec=None)
|
|
|
|
def concrete_program_specify_input_spec(
|
|
self,
|
|
input_spec: NestedSequence[InputSpec] | None = None,
|
|
with_hook: bool = False,
|
|
is_prim_infer: bool = False,
|
|
) -> ConcreteProgram:
|
|
"""
|
|
Returns recent ConcreteProgram instance of decorated function while
|
|
specifying input_spec. If the self._function_spec already has
|
|
input_spec, it will check the compatibility of input input_spec and
|
|
the self._function_spec.input_spec. If input input_spec=None, then
|
|
this method uses self._function_spec.input_spec
|
|
|
|
args:
|
|
input_spec (list[InputSpec], optional): Describes the input of
|
|
the translate function.
|
|
"""
|
|
self._raise_when_property()
|
|
# if specific the `input_spec`, the length of program_cache will always 1,
|
|
# else, return the last one.
|
|
cached_program_len = len(self._program_cache)
|
|
# If specific `input_spec`, apply conversion from dygraph layers into static Program.
|
|
# NOTE(jiabin): is_prim_infer indicates this method called by paddle.jit.save and it is worked in prim mode
|
|
|
|
desired_input_spec = input_spec
|
|
if self._function_spec.input_spec is not None:
|
|
if input_spec is not None and not input_specs_compatible(
|
|
flatten(input_spec), flatten(self._function_spec.input_spec)
|
|
):
|
|
raise ValueError(
|
|
f"The `input_spec`: {input_spec} used to construct concrete_program is conflict with the `input_spec`: {self._function_spec.input_spec} in `@paddle.jit.to_static`"
|
|
)
|
|
# NOTE(chenweihang): we should always translated program based on the `input_spec`
|
|
# decorated on forward if it is valid
|
|
desired_input_spec = self._function_spec.input_spec
|
|
if input_spec is not None:
|
|
logging_utils.warn(
|
|
f"\n\nYou have specified `input_spec` both in function definition (higher priority) and `paddle.jit.save` (will be ignored.)\n\n\t Using: {desired_input_spec}\n\n\t Ignore: {input_spec}\n"
|
|
)
|
|
|
|
has_input_spec = desired_input_spec is not None
|
|
if has_input_spec:
|
|
concrete_program, _ = self.get_concrete_program(
|
|
*desired_input_spec,
|
|
with_hook=with_hook,
|
|
is_train=self._is_train_mode(),
|
|
is_prim_infer=is_prim_infer,
|
|
)
|
|
return concrete_program
|
|
else:
|
|
if cached_program_len != 0:
|
|
logging_utils.warn(
|
|
"No input_spec is found, save cached program instead"
|
|
)
|
|
if cached_program_len > 1:
|
|
logging_utils.warn(
|
|
f"Current {self._function_spec} has more than one cached programs: {cached_program_len}, the last traced program will be return by default."
|
|
)
|
|
|
|
cache_key = self._program_cache._recent_cache_key
|
|
|
|
if with_hook:
|
|
cache_key.kwargs["with_hook"] = True
|
|
|
|
if is_prim_infer:
|
|
(
|
|
concrete_program,
|
|
_,
|
|
) = self.get_concrete_program_with_cache_key(cache_key)
|
|
return concrete_program
|
|
else:
|
|
concrete_program, _ = self._program_cache[cache_key]
|
|
return concrete_program
|
|
|
|
else:
|
|
raise ValueError(
|
|
f"No valid transformed program for {self._function_spec}.\n\t Please specific `input_spec` in `@paddle.jit.to_static` or feed input tensor to call the decorated function at once.\n"
|
|
)
|
|
|
|
@property
|
|
def inputs(self) -> list[Any]:
|
|
"""
|
|
Returns input tensors of recent converted static program.
|
|
"""
|
|
self._raise_when_property()
|
|
concrete_program = self.concrete_program
|
|
inputs = [
|
|
var
|
|
for var in flatten(concrete_program.inputs)
|
|
if isinstance(var, (framework.Variable, Value))
|
|
]
|
|
return inputs
|
|
|
|
@property
|
|
def outputs(self) -> list[Any]:
|
|
"""
|
|
Returns output tensors of recent converted static program.
|
|
"""
|
|
self._raise_when_property()
|
|
concrete_program = self.concrete_program
|
|
outputs = [
|
|
var
|
|
for var in flatten(concrete_program.outputs)
|
|
if isinstance(var, (framework.Variable, Value))
|
|
]
|
|
|
|
return outputs
|
|
|
|
@property
|
|
def main_program(self) -> Program:
|
|
"""
|
|
Returns recent converted static main program.
|
|
"""
|
|
self._raise_when_property()
|
|
concrete_program = self.concrete_program
|
|
main_program = concrete_program.main_program
|
|
return main_program
|
|
|
|
@property
|
|
def program_cache(self) -> ProgramCache:
|
|
return self._program_cache
|
|
|
|
@property
|
|
def function_spec(self) -> FunctionSpec:
|
|
return self._function_spec
|
|
|
|
|
|
def _verify_init_in_dynamic_mode(class_instance):
|
|
"""
|
|
Verifies the instance is initialized in dynamic mode.
|
|
"""
|
|
if isinstance(class_instance, layers.Layer):
|
|
if not class_instance._init_in_dynamic_mode:
|
|
raise RuntimeError(
|
|
" `paddle.jit.to_static` is only available in dynamic mode. Please call `paddle.disable_static()` before "
|
|
f"initializing your Layer class `{class_instance}` . Because parameters of Layer class should be initialized firstly "
|
|
"in dynamic mode while applying transformation."
|
|
)
|
|
|
|
|
|
class HookHelper:
|
|
"""
|
|
Only For converting pre/post hooks operation in outermost layer while jit.save.
|
|
Because hooks in sublayer have been processed automatically.
|
|
"""
|
|
|
|
def __init__(self, func, class_instance, with_hook=False):
|
|
self.func = func
|
|
self.class_instance = class_instance
|
|
self.with_hook = with_hook
|
|
self.need_apply_hook = (
|
|
with_hook
|
|
and isinstance(self.class_instance, layers.Layer)
|
|
and func.__name__ == "forward"
|
|
)
|
|
|
|
def apply_pre_hooks(self, inputs):
|
|
"""
|
|
Apply _forward_pre_hooks from outermost layer
|
|
"""
|
|
if not self.need_apply_hook:
|
|
return inputs
|
|
|
|
inputs = inputs[1:]
|
|
for forward_pre_hook in self.class_instance._forward_pre_hooks.values():
|
|
hook_result = forward_pre_hook(self.class_instance, inputs)
|
|
if hook_result is not None:
|
|
if not isinstance(hook_result, tuple):
|
|
hook_result = (hook_result,)
|
|
inputs = hook_result
|
|
|
|
return [self.class_instance, *list(inputs)]
|
|
|
|
def apply_post_hooks(self, inputs, outputs):
|
|
"""
|
|
Apply _forward_post_hooks from outermost layer
|
|
"""
|
|
if not self.need_apply_hook:
|
|
return outputs
|
|
|
|
inputs = inputs[1:]
|
|
for (
|
|
forward_post_hook
|
|
) in self.class_instance._forward_post_hooks.values():
|
|
hook_result = forward_post_hook(
|
|
self.class_instance, inputs, outputs
|
|
)
|
|
if hook_result is not None:
|
|
outputs = hook_result
|
|
|
|
inputs.insert(0, self.class_instance)
|
|
return outputs
|
|
|
|
|
|
class ConcreteProgram:
|
|
__slots__ = [
|
|
'inputs',
|
|
'outputs',
|
|
'main_program',
|
|
"startup_program",
|
|
"parameters",
|
|
"function",
|
|
'kwargs',
|
|
'constraints',
|
|
]
|
|
|
|
def __init__(
|
|
self,
|
|
inputs,
|
|
outputs,
|
|
parameters,
|
|
function,
|
|
main_program,
|
|
startup_program=None,
|
|
*,
|
|
constraints=None,
|
|
**kwargs,
|
|
):
|
|
self.inputs = inputs
|
|
self.outputs = outputs
|
|
# Avoid mutable default argument pitfall (new list per instance)
|
|
self.constraints = constraints if constraints is not None else []
|
|
self.main_program = main_program
|
|
self.startup_program = startup_program
|
|
self.parameters = parameters
|
|
self.function = function
|
|
self.kwargs = kwargs
|
|
|
|
@staticmethod
|
|
def extract_constraints(input_specs):
|
|
"""
|
|
Extract constraints from input_specs
|
|
"""
|
|
input_specs = flatten(input_specs)
|
|
constraints = []
|
|
for input_spec in input_specs:
|
|
if not hasattr(input_spec, "ranges"):
|
|
return []
|
|
if len(input_spec.ranges):
|
|
for range in input_spec.ranges:
|
|
constraints.append((input_spec.name, range))
|
|
return constraints
|
|
|
|
@staticmethod
|
|
@switch_to_static_graph
|
|
def pir_from_func_spec(
|
|
func_spec, input_spec, input_kwargs_spec, class_instance, **kwargs
|
|
):
|
|
"""
|
|
Builds the main_program with specialized inputs and returns outputs
|
|
of program as fetch_list.
|
|
|
|
Args:
|
|
func_spec(FunctionSpec): A FunctionSpec instance for decorated function.
|
|
input_spec(list[InputSpec]):
|
|
"""
|
|
backend = kwargs["backend"]
|
|
# verify the instance is initialized in imperative mode.
|
|
_verify_init_in_dynamic_mode(class_instance)
|
|
|
|
# Transforms dygraph function into static function and caches it.
|
|
dygraph_function = func_spec.dygraph_function
|
|
static_func = convert_to_static(dygraph_function)
|
|
# apply pre\post hook for outermost layer
|
|
hook_helper = HookHelper(
|
|
dygraph_function, class_instance, kwargs.get("with_hook", False)
|
|
)
|
|
|
|
main_program, startup_program = ir_static.Program(), ir_static.Program()
|
|
# Note: The random seed should be synchronized into cached program
|
|
# if set in `fluid.dygraph_guard` because some ops rely on it, such as
|
|
# `fluid.layers.dropout`.
|
|
main_program.random_seed = (
|
|
paddle.static.default_main_program().random_seed
|
|
)
|
|
startup_program.random_seed = (
|
|
paddle.static.default_startup_program().random_seed
|
|
)
|
|
|
|
with (
|
|
ir_static.program_guard(main_program, startup_program),
|
|
graph_tracing_guard(main_program) as ctx,
|
|
):
|
|
# 1. Adds `paddle.static.data` layers for input if needed
|
|
static_inputs, program_inputs = (
|
|
func_spec.pir_to_static_inputs_with_spec(
|
|
input_spec, main_program
|
|
)
|
|
)
|
|
_kwargs, _ = func_spec.pir_to_static_inputs_with_spec(
|
|
input_kwargs_spec, main_program
|
|
)
|
|
if class_instance:
|
|
static_inputs = (
|
|
class_instance,
|
|
*list(static_inputs),
|
|
)
|
|
program_inputs = (
|
|
class_instance,
|
|
*list(program_inputs),
|
|
)
|
|
|
|
# 2. Builds program only once and returns the output Variables.
|
|
with (
|
|
param_guard(get_parameters(class_instance, True)),
|
|
param_guard(get_buffers(class_instance, True)),
|
|
backend_guard(backend),
|
|
):
|
|
try:
|
|
# only for jit.save, do nothing while train and eval process
|
|
inputs = hook_helper.apply_pre_hooks(static_inputs)
|
|
if _kwargs:
|
|
outputs = static_func(*inputs, **_kwargs)
|
|
else:
|
|
outputs = static_func(*inputs)
|
|
outputs = hook_helper.apply_post_hooks(inputs, outputs)
|
|
except BaseException as e:
|
|
# NOTE: If e is raised in compile time, e should be attached to ERROR_DATA here.
|
|
error.attach_error_data(e)
|
|
error_data = getattr(e, error.ERROR_DATA, None)
|
|
if error_data:
|
|
error_data.raise_new_exception()
|
|
raise
|
|
|
|
if outputs is not None:
|
|
need_wrap_into_list = (
|
|
not isinstance(outputs, (tuple, list)) or len(outputs) == 1
|
|
)
|
|
if need_wrap_into_list:
|
|
outputs = [outputs]
|
|
|
|
main_program = update_op_callstack_with_origin_info(main_program)
|
|
if not os.environ.get("stride_in_no_check_dy2st_diff", "0") == "1":
|
|
check_view_api_used_by_inplace(main_program)
|
|
|
|
constraints = ConcreteProgram.extract_constraints(input_spec)
|
|
return ConcreteProgram(
|
|
inputs=program_inputs,
|
|
outputs=outputs,
|
|
parameters=ctx.get_params_with_values(),
|
|
function=dygraph_function,
|
|
main_program=main_program,
|
|
startup_program=startup_program,
|
|
constraints=constraints,
|
|
**kwargs,
|
|
)
|
|
|
|
# TODO(@xiongkun): remove after new ir is switch
|
|
@staticmethod
|
|
@switch_to_static_graph
|
|
def from_func_spec(
|
|
func_spec, input_spec, input_kwargs_spec, class_instance, **kwargs
|
|
):
|
|
"""
|
|
Builds the main_program with specialized inputs and returns outputs
|
|
of program as fetch_list.
|
|
|
|
Args:
|
|
func_spec(FunctionSpec): A FunctionSpec instance for decorated function.
|
|
input_spec(list[InputSpec]):
|
|
"""
|
|
# verify the instance is initialized in imperative mode.
|
|
_verify_init_in_dynamic_mode(class_instance)
|
|
|
|
# Transforms dygraph function into static function and caches it.
|
|
dygraph_function = func_spec.dygraph_function
|
|
static_func = convert_to_static(dygraph_function)
|
|
# apply pre\post hook for outermost layer
|
|
hook_helper = HookHelper(
|
|
dygraph_function, class_instance, kwargs.get("with_hook", False)
|
|
)
|
|
|
|
main_program, startup_program = framework.Program(), framework.Program()
|
|
# Note: The random seed should be synchronized into cached program
|
|
# if set in `base.dygraph_guard` because some ops rely on it, such as
|
|
# `base.layers.dropout`.
|
|
main_program.random_seed = (
|
|
paddle.static.default_main_program().random_seed
|
|
)
|
|
startup_program.random_seed = (
|
|
paddle.static.default_startup_program().random_seed
|
|
)
|
|
|
|
ProgramTranslator.get_instance()._amp_records.clear()
|
|
|
|
with (
|
|
framework.program_guard(main_program, startup_program),
|
|
to_static_mode_guard(is_to_static=True),
|
|
):
|
|
# 1. Adds `paddle.static.data` layers for input if needed
|
|
static_inputs = func_spec.to_static_inputs_with_spec(
|
|
input_spec, main_program
|
|
)
|
|
_kwargs = func_spec.to_static_inputs_with_spec(
|
|
input_kwargs_spec, main_program
|
|
)
|
|
if class_instance:
|
|
static_inputs = (
|
|
class_instance,
|
|
*list(static_inputs),
|
|
)
|
|
|
|
# 2. Builds program only once and returns the output Variables.
|
|
with (
|
|
param_guard(get_parameters(class_instance, True)),
|
|
param_guard(get_buffers(class_instance, True)),
|
|
):
|
|
try:
|
|
# only for jit.save, do nothing while train and eval process
|
|
inputs = hook_helper.apply_pre_hooks(static_inputs)
|
|
if _kwargs:
|
|
outputs = static_func(*inputs, **_kwargs)
|
|
else:
|
|
outputs = static_func(*inputs)
|
|
outputs = hook_helper.apply_post_hooks(inputs, outputs)
|
|
except BaseException as e:
|
|
# NOTE: If e is raised in compile time, e should be attached to ERROR_DATA here.
|
|
error.attach_error_data(e)
|
|
error_data = getattr(e, error.ERROR_DATA, None)
|
|
if error_data:
|
|
error_data.raise_new_exception()
|
|
raise
|
|
|
|
# 3. Gets all ParamBases and buffered VarBases in the function
|
|
all_parameters_and_buffers = (
|
|
ProgramTranslator.get_instance()._params_recorder.pop(
|
|
main_program
|
|
)
|
|
)
|
|
|
|
if outputs is not None:
|
|
need_wrap_into_list = (
|
|
not isinstance(outputs, (tuple, list)) or len(outputs) == 1
|
|
)
|
|
if need_wrap_into_list:
|
|
outputs = [outputs]
|
|
|
|
main_program = update_op_callstack_with_origin_info(main_program)
|
|
|
|
return ConcreteProgram(
|
|
inputs=static_inputs,
|
|
outputs=outputs,
|
|
parameters=all_parameters_and_buffers,
|
|
function=dygraph_function,
|
|
main_program=main_program,
|
|
startup_program=startup_program,
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
def _program_hash(program):
|
|
"""
|
|
because program is not deleted while calling from_func_spec.
|
|
so it's ok to use id(program)
|
|
"""
|
|
return id(program)
|
|
|
|
|
|
class ParametersRecorder:
|
|
def __init__(self):
|
|
self.params_dict = {}
|
|
|
|
@synchronized
|
|
def add(self, program, param):
|
|
"""use the default_program as key, append param the parameter list."""
|
|
key = _program_hash(program)
|
|
if key not in self.params_dict:
|
|
self.params_dict[key] = set()
|
|
params = self.params_dict[key]
|
|
params.add(param)
|
|
|
|
def pop(self, program):
|
|
params = self.params_dict.get(_program_hash(program))
|
|
if params is None:
|
|
return []
|
|
del self.params_dict[_program_hash(program)]
|
|
params = list(params)
|
|
params.sort(key=lambda x: x.name)
|
|
return params
|
|
|
|
|
|
class InplaceMap:
|
|
def __init__(self):
|
|
self.params_dict = {}
|
|
|
|
@synchronized
|
|
def add(self, program, id, param):
|
|
"""use the default_program as key, append param the parameter list."""
|
|
key = _program_hash(program)
|
|
if key not in self.params_dict:
|
|
self.params_dict[key] = {}
|
|
|
|
params = self.params_dict[key]
|
|
params[id] = param
|
|
|
|
def get(self, program, id):
|
|
params = self.params_dict.get(_program_hash(program))
|
|
if params is None:
|
|
return None
|
|
if id not in params:
|
|
return None
|
|
root_var = params[id]
|
|
saved = []
|
|
while root_var.desc.id() in params.keys():
|
|
saved.append(root_var)
|
|
root_var = params[root_var.desc.id()]
|
|
for var in saved:
|
|
params[var.desc.id()] = root_var
|
|
return root_var
|
|
|
|
def restore_checkpoint(self, checkpoint):
|
|
# InplaceMap is a nested effect.
|
|
# when enter a block, we should save a checkpoint
|
|
# when exit a block, we should restore a checkpoint
|
|
# for example:
|
|
# if cond > 0:
|
|
# x [:] = 0
|
|
# return x
|
|
# x[:] only effect current cond block, we should restore in false block.
|
|
self.params_dict = checkpoint
|
|
|
|
def save_checkpoint(self):
|
|
ckp = {}
|
|
for program_id, params in self.params_dict.items():
|
|
new_params = dict(params.items())
|
|
ckp[program_id] = new_params
|
|
return ckp
|
|
|
|
|
|
class PirPrimHooker(PirPartialProgramLayerHook):
|
|
def __init__(self, original_program, backend):
|
|
self.backend = backend
|
|
self.custom_vjps = set()
|
|
with backend_guard(self.backend):
|
|
if core._is_all_prim_enabled():
|
|
self.custom_vjps = {
|
|
op.name()
|
|
for op in original_program.global_block().ops
|
|
if core.has_custom_vjp(op)
|
|
}
|
|
|
|
def before_append_backward(self, forward_program, src_vars):
|
|
with backend_guard(self.backend):
|
|
if core._is_fwd_prim_enabled():
|
|
dst_vars = decomposition.decompose(
|
|
forward_program, src_vars, blacklist=self.custom_vjps
|
|
)
|
|
return forward_program, dst_vars
|
|
return forward_program, src_vars
|
|
|
|
def after_append_backward(
|
|
self,
|
|
whole_program,
|
|
inputs,
|
|
src_vars,
|
|
grad_outputs,
|
|
forward_end_idx,
|
|
backward_start_idx,
|
|
):
|
|
with backend_guard(self.backend):
|
|
if core._is_fwd_prim_enabled() and len(self.custom_vjps) != 0:
|
|
backward_length = (
|
|
len(whole_program.global_block().ops) - forward_end_idx
|
|
)
|
|
# decompose forward program
|
|
dst_vars = decomposition.decompose(
|
|
whole_program,
|
|
src_vars,
|
|
whitelist=self.custom_vjps,
|
|
end_index=forward_end_idx,
|
|
)
|
|
new_start_index = (
|
|
len(whole_program.global_block().ops) - backward_length
|
|
)
|
|
# decompose backward program
|
|
dst_vars = decomposition.decompose(
|
|
whole_program,
|
|
dst_vars,
|
|
whitelist=self.custom_vjps,
|
|
start_index=new_start_index,
|
|
)
|
|
return whole_program, new_start_index, dst_vars
|
|
return whole_program, forward_end_idx, src_vars
|
|
|
|
def after_infer(self, infer_program):
|
|
with backend_guard(self.backend):
|
|
if core._is_fwd_prim_enabled():
|
|
targets = decomposition.decompose(
|
|
infer_program.program, infer_program.out_values
|
|
)
|
|
infer_program.out_values = targets
|
|
infer_program.forward_range = (
|
|
0,
|
|
len(infer_program.program.global_block().ops),
|
|
)
|
|
return
|
|
|
|
|
|
class PirAutoRecomputeHooker(PirPartialProgramLayerHook):
|
|
def __init__(self, recompute_ops=None):
|
|
self.recompute_ops = recompute_ops
|
|
|
|
def before_append_backward(self, forward_program, src_vars):
|
|
return forward_program, src_vars
|
|
|
|
def after_append_backward(
|
|
self,
|
|
whole_program,
|
|
inputs,
|
|
src_vars,
|
|
grad_outputs,
|
|
forward_end_idx,
|
|
backward_start_idx,
|
|
):
|
|
if core._enable_auto_recompute():
|
|
whole_program, forward_end_idx = decomposition.auto_recompute(
|
|
whole_program,
|
|
inputs,
|
|
src_vars,
|
|
grad_outputs,
|
|
forward_end_idx,
|
|
backward_start_idx,
|
|
)
|
|
return whole_program, forward_end_idx, src_vars
|
|
|
|
|
|
class ProgramCache:
|
|
"""
|
|
Wrapper class for the program functions defined by dygraph function.
|
|
"""
|
|
|
|
def __init__(self):
|
|
# {hash_id : (concrete_program, partial_layer)}
|
|
self._caches = collections.OrderedDict()
|
|
# trace mostly recent used program
|
|
self._recent_key = None
|
|
self._recent_cache_key = None
|
|
|
|
def _build_once(self, cache_key):
|
|
if use_pir_api():
|
|
concrete_program = ConcreteProgram.pir_from_func_spec(
|
|
func_spec=cache_key.function_spec,
|
|
input_spec=cache_key.input_args_with_spec,
|
|
input_kwargs_spec=cache_key.input_kwargs_with_spec,
|
|
class_instance=cache_key.class_instance,
|
|
**cache_key.kwargs,
|
|
)
|
|
else:
|
|
concrete_program = ConcreteProgram.from_func_spec(
|
|
func_spec=cache_key.function_spec,
|
|
input_spec=cache_key.input_args_with_spec,
|
|
input_kwargs_spec=cache_key.input_kwargs_with_spec,
|
|
class_instance=cache_key.class_instance,
|
|
**cache_key.kwargs,
|
|
)
|
|
|
|
backend = cache_key.kwargs['backend']
|
|
if not use_pir_api():
|
|
# decrease prim_is_enable() call to decrease print log
|
|
if prim_or_cinn_is_enabled(
|
|
cache_key.kwargs['build_strategy'], backend
|
|
):
|
|
for var in concrete_program.main_program.list_vars():
|
|
if var.type not in NO_SHAPE_VAR_TYPE and -1 in var.shape:
|
|
warnings.warn(
|
|
f"Now prim and cinn do not support -1 shape, but the shape of var {var.name} is {var.shape}"
|
|
)
|
|
|
|
if use_pir_api():
|
|
from .pir_partial_program import partial_program_from
|
|
|
|
partial_program = partial_program_from(
|
|
concrete_program, cache_key.class_instance is not None
|
|
)
|
|
else: # TODO(pir): remove later.
|
|
from .partial_program import partial_program_from
|
|
|
|
partial_program = partial_program_from(
|
|
concrete_program, cache_key.class_instance is not None
|
|
)
|
|
with backend_guard(backend):
|
|
if core._is_fwd_prim_enabled():
|
|
if use_pir_api():
|
|
partial_program.add_hooker(
|
|
PirPrimHooker(concrete_program.main_program, backend)
|
|
)
|
|
else:
|
|
partial_program.set_hooker(
|
|
PrimHooker(concrete_program.main_program, backend)
|
|
)
|
|
if use_pir_api() and core._enable_auto_recompute():
|
|
partial_program.add_hooker(PirAutoRecomputeHooker())
|
|
return concrete_program, partial_program
|
|
|
|
def __getitem__(self, item):
|
|
if not isinstance(item, CacheKey):
|
|
raise ValueError(
|
|
f'type(item) should be CacheKey, but received {type_name(item)}'
|
|
)
|
|
item_id = hash(item)
|
|
self._recent_cache_key = item
|
|
self._recent_key = item_id
|
|
if item_id not in self._caches:
|
|
self._caches[item_id] = self._build_once(item)
|
|
# Note: raise warnings if number of traced program is more than `max_tracing_count`
|
|
current_tracing_count = len(self._caches)
|
|
if current_tracing_count > MAX_TRACED_PROGRAM_COUNT:
|
|
logging_utils.warn(
|
|
f"Current traced program number: {current_tracing_count} > `max_tracing_count`:{MAX_TRACED_PROGRAM_COUNT}. Too much cached programs will bring expensive overhead. "
|
|
"The reason may be: (1) passing tensors with different shapes, (2) passing python objects instead of tensors."
|
|
)
|
|
|
|
return self._caches[item_id]
|
|
|
|
def get_program_without_cache(self, cache_key):
|
|
return self._build_once(cache_key=cache_key)
|
|
|
|
def get_program(self, item):
|
|
if not isinstance(item, CacheKey):
|
|
raise ValueError(
|
|
f"Input item's type should be FunctionSpec, but received {type_name(item)}"
|
|
)
|
|
item_id = hash(item)
|
|
if item_id not in self._caches:
|
|
raise RuntimeError(
|
|
"Failed to find program for input item, please decorate input function by `@paddle.jit.to_static`."
|
|
)
|
|
return self._caches[item_id]
|
|
|
|
def last(self):
|
|
assert len(self._caches) >= 1, (
|
|
"No valid cached program in ProgramCache."
|
|
)
|
|
assert self._recent_key is not None
|
|
return self._recent_key, self._caches[self._recent_key]
|
|
|
|
def __len__(self):
|
|
return len(self._caches)
|
|
|
|
def concrete_programs(self):
|
|
return [cp for key, (cp, _) in self._caches.items()]
|
|
|
|
def clear(self):
|
|
self._caches = collections.OrderedDict()
|
|
|
|
|
|
class PrimHooker(PartialProgramLayerHook):
|
|
def __init__(self, original_program, backend):
|
|
self.backend = backend
|
|
self.custom_vjps = set()
|
|
with backend_guard(self.backend):
|
|
if core._is_all_prim_enabled():
|
|
self.custom_vjps = {
|
|
op.type
|
|
for op in original_program.block(0).ops
|
|
if core.has_comp_grad_op_maker(op.type)
|
|
}
|
|
|
|
def before_append_backward(self, forward_program):
|
|
with backend_guard(self.backend):
|
|
if core._is_fwd_prim_enabled():
|
|
_to_prim(forward_program.blocks, blacklist=self.custom_vjps)
|
|
return forward_program
|
|
|
|
def after_append_backward(self, whole_program, backward_start_idx):
|
|
with backend_guard(self.backend):
|
|
backward_length = (
|
|
len(whole_program.block(0).ops) - backward_start_idx
|
|
)
|
|
if core._is_fwd_prim_enabled() and len(self.custom_vjps) != 0:
|
|
# only process backward part of block
|
|
_to_prim(whole_program.blocks, backward_length=backward_length)
|
|
new_start_index = len(whole_program.block(0).ops) - backward_length
|
|
if backward_length > 0:
|
|
# only process forward part of block
|
|
_to_prim(whole_program.blocks, start_idx=new_start_index)
|
|
return whole_program, new_start_index
|
|
|
|
def after_infer(self, infer_program):
|
|
with backend_guard(self.backend):
|
|
if core._is_fwd_prim_enabled():
|
|
_to_prim(infer_program.block(0))
|
|
return infer_program
|
|
|
|
|
|
class ProgramTranslator:
|
|
"""
|
|
Class to translate dygraph function into static graph function. The object
|
|
of this class is a singleton.
|
|
|
|
Args:
|
|
None.
|
|
|
|
Returns:
|
|
ProgramTranslator: the singleton object.
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> import paddle
|
|
|
|
>>> # Two methods get same object because ProgramTranslator is a singleton
|
|
>>> paddle.jit.dy2static.program_translator.ProgramTranslator()
|
|
>>> paddle.jit.dy2static.program_translator.ProgramTranslator.get_instance()
|
|
|
|
"""
|
|
|
|
_singleton_lock = threading.Lock()
|
|
_instance = None
|
|
|
|
@synchronized
|
|
def __new__(cls, *args, **kwargs):
|
|
if cls._instance is None:
|
|
cls._instance = object.__new__(cls, *args, **kwargs)
|
|
cls._instance._initialized = False
|
|
return cls._instance
|
|
|
|
@classmethod
|
|
def get_instance(cls):
|
|
if cls._instance is None:
|
|
with cls._singleton_lock:
|
|
cls._instance = cls()
|
|
return cls._instance
|
|
|
|
@classmethod
|
|
def reset(cls):
|
|
if cls._instance is not None:
|
|
cls._instance._initialized = False
|
|
cls._instance.__init__()
|
|
|
|
def __init__(self):
|
|
# To make sure that calls __init__ only once.
|
|
if self._initialized:
|
|
return
|
|
self._initialized = True
|
|
self._program_cache = ProgramCache()
|
|
self._params_recorder = ParametersRecorder()
|
|
self._inplace_map = InplaceMap()
|
|
self._amp_records: dict[int, list[tuple[AmpOptions, int, int]]] = {}
|
|
self.enable_to_static = True
|
|
|
|
def enable(self, enable_to_static):
|
|
check_type(
|
|
enable_to_static,
|
|
"enable_to_static",
|
|
bool,
|
|
"ProgramTranslator.enable",
|
|
)
|
|
self.enable_to_static = enable_to_static
|
|
|
|
|
|
def enable_to_static(enable_to_static_bool: bool) -> None:
|
|
"""
|
|
Enable or disable the converting from imperative to static graph by
|
|
ProgramTranslator globally.
|
|
|
|
Args:
|
|
enable_to_static_bool (bool): True or False to enable or disable converting to static.
|
|
|
|
Returns:
|
|
None.
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> import paddle
|
|
>>> @paddle.jit.to_static
|
|
>>> def func(x):
|
|
... if paddle.mean(x) > 0:
|
|
... x_v = x - 1
|
|
... else:
|
|
... x_v = x + 1
|
|
... return x_v
|
|
>>> paddle.jit.enable_to_static(False)
|
|
|
|
>>> x = paddle.ones([1, 2])
|
|
>>> # ProgramTranslator is disabled so the func is run in dygraph
|
|
>>> print(func(x))
|
|
Tensor(shape=[1, 2], dtype=float32, place=Place(cpu), stop_gradient=True,
|
|
[[0., 0.]])
|
|
|
|
"""
|
|
check_type(
|
|
enable_to_static_bool,
|
|
"enable_to_static_bool",
|
|
bool,
|
|
"paddle.jit.enable_to_static",
|
|
)
|
|
_program_trans = ProgramTranslator()
|
|
_program_trans.enable(enable_to_static_bool)
|
|
|
|
|
|
@switch_to_static_graph
|
|
def _to_prim(
|
|
blocks,
|
|
blacklist=frozenset(),
|
|
whitelist=frozenset(),
|
|
start_idx=-1,
|
|
backward_length=-1,
|
|
):
|
|
"""Switch to static graph and call to_prim."""
|
|
# TODO(Aurelius84): Fix this cycle import problem
|
|
from paddle.incubate.autograd import primapi
|
|
|
|
primapi.to_prim(
|
|
blocks,
|
|
blacklist=blacklist,
|
|
whitelist=whitelist,
|
|
start_idx=start_idx,
|
|
backward_length=backward_length,
|
|
)
|