chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:42 +08:00
commit e25996e7db
15472 changed files with 3536181 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Copyright (c) 2023 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 . import (
profiler as profiler,
psdb, # noqa: F401
)
from .opcode_translator.breakpoint import ( # noqa: F401
BM,
add_breakpoint,
add_event,
)
from .translate import symbolic_translate # noqa: F401
+656
View File
@@ -0,0 +1,656 @@
# Copyright (c) 2023 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 copy
from contextlib import nullcontext
from typing import TYPE_CHECKING, Any, TypeVar
import paddle
from paddle.base.data_feeder import convert_dtype
from paddle.base.framework import convert_nptype_to_datatype_or_vartype
from paddle.base.unique_name import (
UniqueNameGenerator,
guard as UniqueNameGuard,
)
from paddle.distributed.auto_parallel.placement_type import (
get_shard_spec,
to_placements,
)
from paddle.distributed.auto_parallel.static.dist_input_spec import (
DistributedInputSpec,
)
from paddle.distributed.auto_parallel.static.utils import (
convert_to_dims_mapping,
)
from paddle.jit.dy2static.utils import (
ALREADY_D2S,
extract_tensor_dynamic_dims,
graph_tracing_guard,
)
from paddle.pir import is_fake_value
from paddle.static import InputSpec
from paddle.utils import flatten, is_sequence
from .symbolic_shape.symbolic_value import SymbolicInt
from .utils import (
Cache,
Singleton,
get_min_non_specialized_number,
map_if_extend,
meta_str,
)
from .utils.exceptions import BreakGraphError, NullMetaBreak
if TYPE_CHECKING:
import numpy.typing as npt
DynamicSymbolT = TypeVar("DynamicSymbolT")
SOT_INFER_META_INNER_VAR = "___SOT_INFER_META_INNER_VAR"
class DistInfo:
def __init__(self, mesh=None, dims_mapping=None, local_shape=None):
self.mesh = mesh
self.dims_mapping = dims_mapping
self.local_shape = local_shape
@staticmethod
def from_tensor(tensor: paddle.Tensor) -> DistInfo:
assert isinstance(tensor, paddle.Tensor) and tensor.is_dist(), (
f"Expect a Tensor, but got a {type(tensor)}."
)
mesh = tensor.process_mesh
sharding_specs = get_shard_spec(
mesh, tensor.placements, len(tensor.shape)
)
dims_mapping = convert_to_dims_mapping(sharding_specs, mesh)
local_shape = tensor._local_value().shape
return DistInfo(mesh, dims_mapping, local_shape)
@staticmethod
def from_value(value: paddle.pir.Value) -> DistInfo:
assert isinstance(value, paddle.pir.Value) and value.is_dist(), (
f"Expect a Value, but got a {type(value)}."
)
return DistInfo(
value.dist_attr().process_mesh,
value.dist_attr().dims_mapping,
value._local_shape,
)
def __deepcopy__(self, memo):
return DistInfo(
mesh=copy.deepcopy(self.mesh),
dims_mapping=copy.deepcopy(self.dims_mapping),
local_shape=copy.deepcopy(self.local_shape),
)
def __repr__(self) -> str:
return f"DistInfo(mesh={self.mesh}, dims_mapping={self.dims_mapping}, local_shape={self.local_shape})"
class MetaInfoOrNull:
def __init__(self, meta: MetaInfo | None):
self.meta = meta
@staticmethod
def null():
return MetaInfoOrNull(None)
def is_null(self):
return self.meta is None
def unwrap_or_breakgraph(self):
if self.meta is None:
raise BreakGraphError(NullMetaBreak())
return self.meta
def unwrap_unsafe(self):
assert self.meta is not None, "MetaInfo is None"
return self.meta
def with_dynamic_axes(
self, name: str, dynamic_axes: list[int]
) -> MetaInfoOrNull:
if self.meta is None:
return MetaInfoOrNull.null()
return self.meta.with_dynamic_axes(name, dynamic_axes).wrap()
def to_input_spec(self):
if self.meta is None:
return None
return self.meta.to_input_spec()
def guard_str(self):
if self.meta is None:
return "(Null)"
return self.meta.guard_str()
def __deepcopy__(self, memo):
if self.meta is None:
return MetaInfoOrNull(None)
return MetaInfoOrNull(copy.deepcopy(self.meta))
@staticmethod
def mix_axes(axes1: list[int], axes2: list[int]) -> list[int]:
return sorted(set(axes1 + axes2))
@staticmethod
def from_tensor(
tensor: paddle.Tensor, *, dynamic_axes: list[int] | None = None
) -> MetaInfoOrNull:
if not tensor._is_dense_tensor_hold_allocation():
return MetaInfoOrNull.null()
assert isinstance(tensor, paddle.Tensor), (
"Expect a Tensor, but got a Value."
)
assert -1 not in tensor.shape, (
"Tensor shape should not contain -1, maybe you pass a Value to from_tensor"
)
user_specified_dynamic_axes = extract_tensor_dynamic_dims(tensor)
dynamic_axes = dynamic_axes or []
dynamic_axes = MetaInfoOrNull.mix_axes(
dynamic_axes, list(user_specified_dynamic_axes)
)
shape = [
SymbolicInt(dim) if i in dynamic_axes else dim
for i, dim in enumerate(tensor.shape)
]
if tensor.is_dist():
dist_info = DistInfo.from_tensor(tensor)
else:
dist_info = None
return MetaInfo(
shape,
tensor.dtype,
tensor.stop_gradient,
tensor.name,
tensor.persistable,
tensor.type,
tensor.place,
None,
dist_info=dist_info,
).wrap()
@staticmethod
def from_numpy(
nparray: npt.NDArray[Any], *, dynamic_axes: list[int] | None = None
) -> MetaInfoOrNull:
dtype = convert_nptype_to_datatype_or_vartype(nparray.dtype)
dynamic_axes = dynamic_axes or []
shape = [
SymbolicInt() if i in dynamic_axes else dim
for i, dim in enumerate(nparray.shape)
]
return MetaInfo(
shape,
dtype,
True, # stop_gradient
None,
None, # persistable
None,
None,
None,
dist_info=None,
).wrap()
@staticmethod
def from_value(value) -> MetaInfoOrNull:
if is_fake_value(value):
return MetaInfoOrNull.null()
name = SOT_INFER_META_INNER_VAR
shape = [SymbolicInt() if dim == -1 else dim for dim in value.shape]
for dim in shape:
if isinstance(dim, int):
assert dim >= 0, (
"Dimensions must be non-negative integers or SymbolicInt. "
f"Encountered value {dim} in shape {shape}."
)
if isinstance(value, paddle.pir.Value) and value.is_dist():
dist_info = DistInfo.from_value(value)
else:
dist_info = None
return MetaInfo(
shape,
value.dtype,
value.stop_gradient,
name,
value.persistable,
None, # type is not a unified attribute in dygraph and static mode.
None, # We can't infer the right place in compile time.
None, # there's no spec_name specified when from_value.
dist_info=dist_info,
).wrap()
def __repr__(self):
if self.meta is None:
return "MetaInfoOrNull(None)"
return f"MetaInfoOrNull({self.meta})"
def __eq__(self, other):
if self.meta is None:
return other.meta is None
if other.meta is None:
return False
return self.meta == other.meta
def __hash__(self):
if self.meta is None:
return hash(None)
return hash(self.meta)
class MetaInfo:
shape: list[int | SymbolicInt]
def __init__(
self,
shape,
dtype,
stop_gradient,
name,
persistable,
type,
place,
spec_name=None,
dist_info=None,
):
assert -1 not in shape, (
"NOTE: Shape should not contain -1, consider convert it to SymbolicInt."
)
self.name = name
self.persistable = persistable
self.type = type
self.place = place
self.shape = shape
self.dtype = dtype
self.stop_gradient = stop_gradient
self.dist_info = dist_info
self.spec_name = spec_name
def wrap(self):
return MetaInfoOrNull(self)
def shape_with_special_symbol(
self, dynamic_symbol: DynamicSymbolT = -1
) -> list[int | DynamicSymbolT]:
return [
dynamic_symbol if isinstance(dim, SymbolicInt) else dim
for dim in self.shape
]
def with_dynamic_axes(self, name: str, dynamic_axes: list[int]) -> MetaInfo:
mixed_dynamic_axes = MetaInfoOrNull.mix_axes(
self.dynamic_axes, dynamic_axes
)
# NOTE(SigureMo): Make sure create a new shape list with dynamic axes.
# We will create a new shape list variable lazily in the future.
shape = [
(
SymbolicInt(dim)
if (
i in mixed_dynamic_axes and not isinstance(dim, SymbolicInt)
)
else dim
)
for i, dim in enumerate(self.shape)
]
return MetaInfo(
shape,
self.dtype,
self.stop_gradient,
self.name,
self.persistable,
self.type,
self.place,
spec_name=name,
dist_info=self.dist_info,
)
@property
def dynamic_axes(self):
return [
i
for i, dim in enumerate(self.shape)
if isinstance(dim, SymbolicInt)
]
def is_inner_var(self):
return self.name == SOT_INFER_META_INNER_VAR
def is_dynamic_shape(self):
"""
if SymbolicInt in shape, return True
else: return False
"""
return len(self.dynamic_axes) > 0
def to_input_spec(self) -> DistributedInputSpec | ConstrainedInputSpec:
shape = self.shape_with_special_symbol(None)
if self.dist_info is not None:
placements = to_placements(
self.dist_info.dims_mapping, self.dist_info.mesh
)
return DistributedInputSpec(
shape,
dtype=self.dtype,
stop_gradient=self.stop_gradient,
mesh=self.dist_info.mesh,
placements=placements,
local_shape=self.dist_info.local_shape,
)
else:
return ConstrainedInputSpec(
self.dynamic_axes,
shape,
dtype=self.dtype,
name=self.spec_name,
stop_gradient=self.stop_gradient,
)
def guard_str(self):
shape = self.shape_with_special_symbol(SymbolicInt())
return f"({shape}, {self.dtype}, {self.stop_gradient})"
def __deepcopy__(self, memo):
return MetaInfo(
list(self.shape),
self.dtype,
self.stop_gradient,
self.name,
self.persistable,
self.type,
self.place,
self.spec_name,
dist_info=copy.deepcopy(self.dist_info),
)
def __repr__(self):
return meta_str(self.shape, self.dtype, self.stop_gradient)
def __eq__(self, meta):
return (
self.shape == meta.shape
and self.dtype == meta.dtype
and self.stop_gradient == meta.stop_gradient
)
def __hash__(self):
return hash((tuple(self.shape), self.dtype, self.stop_gradient))
class VariableCreator(metaclass=Singleton):
"""
We use the static graph Variable to infer the meta information of Tensor.
This singleton class is used to create Variable for infer meta.
"""
def __init__(self):
self.var_name_generator = UniqueNameGenerator(SOT_INFER_META_INNER_VAR)
self.var_cache = {}
self.main_program = paddle.static.Program()
self.startup_program = paddle.static.Program()
def gen_name(self, meta_or_null: MetaInfoOrNull):
if meta_or_null.is_null():
return "null"
meta = meta_or_null.unwrap_unsafe()
name = f"{meta.dtype}_{meta.stop_gradient}_"
name += "_".join(map(str, meta.shape))
return name
def create_var(self, meta_or_null: MetaInfoOrNull):
if meta_or_null.is_null():
return None
meta = meta_or_null.unwrap_unsafe()
shape = meta.shape_with_special_symbol(-1)
with paddle.static.program_guard(
self.main_program, self.startup_program
):
var = paddle.static.input.data(
name=self.gen_name(meta.wrap()),
shape=shape,
dtype=convert_dtype(meta.dtype),
)
var.stop_gradient = meta.stop_gradient
if meta.dist_info is not None:
mesh = meta.dist_info.mesh
placements = to_placements(meta.dist_info.dims_mapping, mesh)
var = paddle._pir_ops.shard_tensor(var, mesh, placements)
var.stop_gradient = meta.stop_gradient
assert not isinstance(var, paddle.Tensor), (
"Expect a Variable, but got a Tensor."
)
return var
def get_variable(self, meta: MetaInfoOrNull, without_cache=False):
var_feature_name = self.gen_name(meta)
if without_cache:
return self.create_var(meta)
if var_feature_name not in self.var_cache:
self.var_cache[var_feature_name] = self.create_var(meta)
return self.var_cache[var_feature_name]
def infer_meta(self, func, *args, **kwargs):
with (
paddle.base.framework._dygraph_guard(None),
UniqueNameGuard(self.var_name_generator),
):
if func is paddle.distributed.shard_tensor:
args, kwargs = (
convert_meta_to_variable(args, without_cache=True),
convert_meta_to_variable(kwargs, without_cache=True),
)
else:
args, kwargs = (
convert_meta_to_variable(args),
convert_meta_to_variable(kwargs),
)
graph_tracing_context_manager = nullcontext()
with paddle.static.program_guard(
self.main_program, self.startup_program
):
if isinstance(func, str):
# TODO(Aurelius84): Is length of args always greater than 0?
# Do we need add condition check here?
func = getattr(args[0], func)
args = args[1:]
if hasattr(func, ALREADY_D2S):
graph_tracing_context_manager = graph_tracing_guard(
self.main_program
)
with graph_tracing_context_manager:
out = func(*args, **kwargs)
return convert_variable_to_meta_info(out)
def convert_meta_to_variable(args, without_cache=False):
return map_if_extend(
args,
pred=lambda x: isinstance(x, MetaInfoOrNull),
true_fn=lambda x: VariableCreator().get_variable(
x, without_cache=without_cache
),
false_fn=lambda x: x,
)
def convert_meta_to_input_spec(args):
return map_if_extend(
args,
pred=lambda x: isinstance(x, MetaInfoOrNull),
true_fn=lambda x: x.to_input_spec(),
# TODO(xiongkun): can x be tensor ?
false_fn=lambda x: (
paddle.static.InputSpec.from_tensor(x)
if isinstance(x, paddle.Tensor)
else x
),
)
def convert_variable_to_meta_info(args):
return map_if_extend(
args,
pred=lambda x: isinstance(x, paddle.pir.Value),
true_fn=lambda x: MetaInfoOrNull.from_value(x),
false_fn=lambda x: x,
)
def infer_meta(func, *args, **kwargs):
fn = SpecialInferMeta().get_infermeta_fn(func)
if fn:
return fn(*args, **kwargs)
return VariableCreator().infer_meta(func, *args, **kwargs)
def infer_meta_for_layer(layer, *args, **kwargs):
assert isinstance(layer, paddle.nn.Layer), (
f"Expect a Layer, but got {layer}."
)
layer = paddle.jit.to_static(layer, full_graph=True)
args_, kwargs_ = convert_meta_to_input_spec((args, kwargs))
(
concrete_program,
partial_program_layer,
) = layer.forward.get_concrete_program(*args_, **kwargs_)
output_values = partial_program_layer._outputs.var_list
out = partial_program_layer._restore_out(
[
x
for x in paddle.utils.flatten(
convert_variable_to_meta_info(output_values)
)
if isinstance(x, MetaInfoOrNull)
]
)
layer.forward.rollback()
return out
def ast_infer_meta(static_function, *args, **kwargs):
args_, kwargs_ = convert_meta_to_input_spec((args, kwargs))
(
concrete_program,
partial_program_layer,
) = static_function.get_concrete_program(*args_, **kwargs_)
out = partial_program_layer._restore_out(
[
x
for x in paddle.utils.flatten(
convert_variable_to_meta_info(concrete_program.outputs)
)
if isinstance(x, MetaInfoOrNull)
]
)
return out
class SpecialInferMeta(metaclass=Singleton):
"""
There are some functions that cannot be inferred directly through static graph,
and need to be implemented manually. This class is used to implement infer meta
for these functions.
"""
def __init__(self):
pass
def get_infermeta_fn(self, fn):
try:
funcname = fn.__name__
return getattr(self, f"infermeta_{funcname}")
except:
pass
return None
def infermeta_grad(
self,
outputs,
inputs,
grad_outputs=None,
retain_graph=None,
create_graph=False,
only_inputs=True,
allow_unused=False,
no_grad_vars=None,
):
if not is_sequence(inputs):
inputs = [inputs]
return inputs
class InferMetaCache(Cache, metaclass=Singleton):
def __init__(self):
super().__init__(copy=True)
def key_fn(
self, func, *args, **kwargs
): # args & kwargs have transformed to MetaInfo
return (
func,
tuple(flatten(args)),
tuple(kwargs.keys()),
tuple(flatten(kwargs)),
)
def value_fn(self, func, *args, **kwargs):
return infer_meta(func, *args, **kwargs)
class LayerInferMetaCache(Cache, metaclass=Singleton):
def __init__(self):
super().__init__(copy=True)
def key_fn(self, layer, *args, **kwargs):
params = [
MetaInfoOrNull.from_tensor(x)
for x in layer.parameters(include_sublayers=True)
]
return (
layer,
tuple(params),
tuple(flatten(args)),
tuple(kwargs.keys()),
tuple(flatten(kwargs)),
)
def value_fn(self, layer, *args, **kwargs):
return infer_meta_for_layer(layer, *args, **kwargs)
class ConstrainedInputSpec(InputSpec):
def __init__(self, dynamic_axes: list[int], *args, **kwargs):
self.ranges: list[
tuple[int, int | None, int | None]
] = [] # (idx of dim, min, max)
super().__init__(*args, **kwargs)
min_non_specialized_number = get_min_non_specialized_number()
for i in dynamic_axes:
self.ranges.append((i, min_non_specialized_number, None))
@@ -0,0 +1,18 @@
# Copyright (c) 2023 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 .eval_frame_callback import eval_frame_callback # noqa: F401
from .skip_files import setup_skip_files
setup_skip_files()
@@ -0,0 +1,180 @@
# Copyright (c) 2023 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.
import inspect
import traceback
from dataclasses import dataclass
from ..opcode_translator.instruction_utils import instrs_info
from ..utils import Singleton, log
from .executor.opcode_executor import OpcodeExecutorBase
# this file is a debug utils files for quick debug
# >>> sot.add_breakpoint(file, line)
# >>> sot.remove_breakpoint(file, line)
@dataclass
class Breakpoint:
file: str
line: int
co_name: str
offset: int
def __hash__(self):
return hash((self.file, self.line, self.co_name, self.offset))
class BreakpointManager(metaclass=Singleton):
def __init__(self):
self.breakpoints = set()
self.executors = OpcodeExecutorBase.call_stack
self.activate = 0
self.record_event = []
def clear_event(self, event):
self.record_event.clear()
def add_event(self, event):
"""
event in ['All' ,'FallbackError', 'BreakGraphError', 'InnerError']
"""
self.record_event.append(event)
def add(self, file, line, co_name=None, offset=None):
log(1, f"add breakpoint at {file}:{line}\n")
self.breakpoints.add(Breakpoint(file, line, co_name, offset))
def addn(self, *lines):
"""
called inside a executor. add a list of line number in current file.
"""
if not isinstance(lines, (list, tuple)):
lines = [lines]
for line in lines:
file = self.cur_exe.vframe.code.co_filename
self.add(file, line)
def clear(self):
self.breakpoints.clear()
def hit(self, file, line, co_name, offset):
if Breakpoint(file, line, None, None) in self.breakpoints:
return True
if Breakpoint(file, line, co_name, offset) in self.breakpoints:
return True
return False
def locate(self, exe):
for i, _e in enumerate(self.executors):
if _e is exe:
self.activate = i
return
raise RuntimeError("Not found executor.")
def up(self):
if self.activate == 0:
return
self.activate -= 1
print("current function is: ", self.cur_exe.vframe.code.co_name)
def down(self):
if self.activate >= len(self.executors) - 1:
return
self.activate += 1
print("current function is: ", self.cur_exe.vframe.code.co_name)
def opcode(self, cur_exe=None):
if cur_exe is None:
cur_exe = self.cur_exe
instr = cur_exe._instructions[cur_exe.vframe.lasti - 1]
message = f"[Translate {cur_exe}] (line {cur_exe._current_line:>3}) {instr.opname:<12} {instr.argval}, stack is {cur_exe._stack}\n"
return message
def bt(self):
"""
display all inline calls: backtrace.
"""
for exe in self.executors:
lines, _ = inspect.getsourcelines(exe.vframe.code)
print(
" "
+ exe.vframe.code.co_filename
+ f"({exe._current_line})"
+ f"{exe.vframe.code.co_name}()"
)
print(f"-> {lines[0].strip()}")
print(f"-> {self.opcode(exe)}")
pass
def on_event(self, event):
if "All" in self.record_event or event in self.record_event:
print("event captured.")
self.activate = len(self.executors) - 1
breakpoint() # noqa: T100
def _dis_source_code(self):
cur_exe = self.executors[self.activate]
lines, start_line = inspect.getsourcelines(cur_exe.vframe.code)
cur_line = cur_exe._current_line
lines[cur_line - start_line + 1 : cur_line - start_line + 1] = (
" ^^^^^ HERE \n"
)
print("\033[31mSource Code is: \033[0m")
print("".join(lines))
def dis(self, range=5):
"""
display all instruction code and source code.
"""
print("displaying debug info...")
cur_exe = self.cur_exe
print(self._dis_source_code())
print(f"\n{cur_exe.vframe.code}")
lasti = cur_exe.vframe.lasti
instr_str = instrs_info(
cur_exe._instructions, lasti - 1, range, want_str=True
)
print(instr_str)
@property
def cur_exe(self):
exe = self.executors[self.activate]
return exe
def sir(self):
"""
display sir in a page.
"""
print("displaying sir...")
self.cur_exe.print_sir()
def pe(self, e):
"""
print exception.
"""
lines = traceback.format_tb(e.__traceback__)
print("".join(lines))
def add_breakpoint(file, line, co_name=None, offset=None):
BM.add(file, line, co_name, offset)
def add_event(event):
BM.add_event(event)
BM = BreakpointManager()
@@ -0,0 +1,25 @@
# Copyright (c) 2023 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
from typing import TYPE_CHECKING, NamedTuple
if TYPE_CHECKING:
from types import CodeType
class CustomCode(NamedTuple):
code: CodeType | None
disable_eval_frame: bool
@@ -0,0 +1,92 @@
# Copyright (c) 2023 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 dis
from typing import TYPE_CHECKING
from ..profiler import EventGuard
from ..utils import log_do, log_enabled, log_format
from .executor.executor_cache import OpcodeExecutorCache
if TYPE_CHECKING:
from .custom_code import CustomCode
def print_locals(frame):
local_key = [
key for key in frame.f_locals.keys() if not key.startswith("__")
]
print(
f"[eval_frame_callback] {frame.f_code.co_name} with locals {local_key}"
)
print(
f"[eval_frame_callback] {' ' * len(frame.f_code.co_name)} with cellvars + freevars: {frame.f_code.co_cellvars + frame.f_code.co_freevars}"
)
def convert_obj(obj):
import paddle
if isinstance(obj, paddle.Tensor):
return "Tensor(" + str(obj.shape) + ")"
if isinstance(obj, list):
return [convert_obj(i) for i in obj]
return obj
for key in local_key:
print(
f"[eval_frame_callback] {' ' * len(frame.f_code.co_name)} {key} = {convert_obj(frame.f_locals[key])}"
)
def eval_frame_callback(frame, **kwargs) -> CustomCode:
with EventGuard(
f"eval_frame_callback: {frame.f_code.co_name}", event_level=2
):
# A quick way to check if the log is enabled
need_log = log_enabled(1)
if need_log:
log_format(
2,
"[eval_frame_callback] start to translate: {}\n",
frame.f_code,
)
log_do(4, lambda: print_locals(frame))
log_format(
3,
"[eval_frame_callback] OriginCode: {}\n",
frame.f_code.co_name,
)
log_do(3, lambda: dis.dis(frame.f_code))
custom_code = OpcodeExecutorCache()(frame, **kwargs)
if need_log:
if custom_code.code is None:
log_format(
3,
"[eval_frame_callback] NewCode (same as origin code): {}\n",
frame.f_code.co_name,
)
else:
log_format(
3,
"[eval_frame_callback] NewCode: {}\n",
custom_code.code.co_name,
)
log_do(3, lambda: dis.dis(custom_code.code))
return custom_code
@@ -0,0 +1,15 @@
# Copyright (c) 2023 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 . import variable_dispatch # noqa: F401
@@ -0,0 +1,71 @@
# Copyright (c) 2023 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.
# This file stores the customized function that will be called by the dispatch mechanism.
from __future__ import annotations
from ...utils import BreakGraphError, BreakGraphReasonBase, FallbackError
def create_raise_break_graph_handler(reason: BreakGraphReasonBase):
def raise_break_graph_fn(*args, **kwarg):
raise BreakGraphError(reason)
return raise_break_graph_fn
def raise_not_implement_fn(*args, **kwarg):
raise FallbackError("raise by raise_break_graph_fn.")
# just a function for operator.in
def operator_in(left, right):
return left in right
def operator_not_in(left, right):
return left not in right
def operator_exception_match(left, right):
pass
def operator_BAD(left, right):
pass
def operator_is_none(val):
pass
def operator_is_not_none(val):
pass
def tensor_dim(x):
pass
def generator_send(x):
pass
def place_get_device_id():
pass
def place_get_device_type():
pass
@@ -0,0 +1,298 @@
# Copyright (c) 2023 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 copy
import inspect
import operator
from functools import cached_property, reduce
from typing import TYPE_CHECKING, Any, TypeVar
from ...utils import InnerError, NameGenerator, hashable
if TYPE_CHECKING:
from collections.abc import Callable
T = TypeVar("T")
Args = tuple[T, ...]
Kwargs = dict[str, T]
def format_type(type_: type[Any] | tuple[type[Any], ...]) -> str:
if not isinstance(type_, tuple):
type_ = (type_,)
return " | ".join([t.__name__ for t in type_])
def format_param(param: Parameter) -> str:
kind = param.kind
if kind == inspect.Parameter.VAR_POSITIONAL:
return f"*{format_type(param.type)}"
elif kind == inspect.Parameter.VAR_KEYWORD:
return f"**{format_type(param.type)}"
else:
return format_type(param.type)
def convert_annotation_to_type(type_str: str) -> tuple[type[Any], ...]:
"""
Convert type annotation to runtime value. Because we are using :pep:`563`
to use the future annotation syntax, we cannot use `get_type_hints <https://docs.python.org/3.8/library/typing.html#typing.get_type_hints>`_
directly. Currently, only the builtins and variables namespaces are supported.
Returns:
tuple: The converted type.
"""
import builtins
from . import variables
type_str = type_str.strip()
if type_str == "Any":
type_str = "object"
if "|" in type_str:
return reduce(
operator.add, map(convert_annotation_to_type, type_str.split("|"))
)
search_namespaces = [variables, builtins]
for namespace in search_namespaces:
if hasattr(namespace, type_str):
return (getattr(namespace, type_str),)
raise InnerError(f"Cannot find type {type_str} in {search_namespaces}")
class Parameter:
name_gen = NameGenerator("param_")
annotation: str
name: str
def __init__(
self,
annotation: str,
*,
kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_OR_KEYWORD,
name: str | None = None,
default: Any = inspect._empty,
):
self.name = name if name is not None else Parameter.name_gen.next()
self.annotation = annotation
self.kind = kind
self.default = default
def to_parameter(self) -> inspect.Parameter:
return inspect.Parameter(
self.name,
kind=self.kind,
annotation=self.annotation,
default=copy.copy(self.default),
)
@cached_property
def type(self) -> tuple[type[Any], ...]:
return convert_annotation_to_type(self.annotation)
def match_arg(self, arg: Any) -> bool:
if self.kind == inspect.Parameter.VAR_POSITIONAL:
is_tuple = isinstance(arg, tuple)
return is_tuple and all(isinstance(a, self.type) for a in arg)
elif self.kind == inspect.Parameter.VAR_KEYWORD:
is_dict = isinstance(arg, dict)
return is_dict and all(
isinstance(a, self.type) for a in arg.values()
)
else:
return isinstance(arg, self.type)
@staticmethod
def from_str(annotation: str) -> Parameter:
return Parameter(annotation)
@staticmethod
def from_parameter(parameter: inspect.Parameter) -> Parameter:
if parameter.annotation != parameter.empty and not isinstance(
parameter.annotation, str
):
raise InnerError(
f"Parameter {parameter} has annotation {parameter.annotation} "
"which is not a string. Please add `from __future__ import annotations` "
"to the top of your file."
)
annotation = (
parameter.annotation
if parameter.annotation != parameter.empty
else "Any"
)
return Parameter(
annotation,
kind=parameter.kind,
name=parameter.name,
default=parameter.default,
)
def __repr__(self) -> str:
default_repr = f"= {self.default!r}"
return f"Parameter({', '.join([self.annotation, default_repr])})"
def optional(annotation: str, default: Any = None) -> Parameter:
return Parameter(annotation, default=default)
class Pattern:
parameters: dict[str, Parameter]
signature: inspect.Signature
def __init__(
self,
*parameters: Parameter,
):
self.parameters = {
parameter.name: parameter for parameter in parameters
}
self.signature = inspect.Signature(
[parameter.to_parameter() for parameter in self.parameters.values()]
)
def match_inputs(self, /, *args: Any, **kwargs: Any) -> bool:
"""
Match the input parameters of the function.
Returns:
bool: Whether the input parameters match the pattern.
"""
try:
bound_args = self.signature.bind(*args, **kwargs)
except TypeError:
return False
for arg_name, arg_value in bound_args.arguments.items():
if arg_name not in self.parameters:
continue
if not self.parameters[arg_name].match_arg(arg_value):
return False
return True
def __repr__(self) -> str:
types_repr = ", ".join(
[format_param(param) for param in self.parameters.values()]
)
return f"Pattern({types_repr})"
class Dispatcher:
"""
Used for pattern registration and distribution.
For more design ideas, refer to the `Builtin dispatcher <https://github.com/PaddlePaddle/PaddleSOT/blob/develop/docs/design/builtin-dispatcher.md>`_ for details.
Examples:
>>> def builtin_add(a: int, b: int) -> int: ...
>>> Dispatcher.register(builtin_add, ("int", "int"), lambda a, b: a + b)
>>> handler = Dispatcher.dispatch(builtin_add, 1, 2)
>>> handler(1, 2)
3
"""
handlers: dict[
Callable[..., Any], list[tuple[Pattern, Callable[..., Any]]]
] = {}
graph: Any = None
@classmethod
def register(
cls,
fn: Callable[..., Any],
parameters: tuple[str | Parameter, ...],
handler: Callable[..., Any],
):
"""
Registering function signature.
Args:
fn: The function to be registered.
parameters: The parameters of the function to be registered.
handler: The handler function.
"""
_parameters = tuple(
(
Parameter.from_str(parameter)
if isinstance(parameter, str)
else parameter
)
for parameter in parameters
)
if fn not in cls.handlers:
cls.handlers[fn] = []
cls.handlers[fn].append((Pattern(*_parameters), handler))
@classmethod
def register_decorator(cls, fn: Callable[..., Any]):
"""
Decorator mode of register, Used to register some complex functions.
Args:
fn: The function to be registered.
Examples:
>>> def builtin_add(a: int, b: int) -> int: ...
>>> @Dispatcher.register_decorator(builtin_add)
... def builtin_add_dispatcher(a: int, b: int) -> int:
... return a + b
>>> handler = Dispatcher.dispatch(builtin_add, 1, 2)
>>> handler(1, 2)
3
"""
def decorator(handler: Callable[..., Any]):
signature = inspect.signature(handler)
parameters = tuple(
Parameter.from_parameter(parameter)
for parameter in signature.parameters.values()
)
cls.register(fn, parameters, handler)
return decorator
@classmethod
def call(cls, fn, *args, **kwargs):
func = cls.dispatch(fn, *args, **kwargs)
if func is None:
raise InnerError(
f"Cannot find handler for {fn} with args {args} and kwargs {kwargs}"
)
return func(*args, **kwargs)
@classmethod
def dispatch(
cls, fn: Callable[..., Any], *args: Any, **kwargs: Any
) -> Callable[..., Any] | None:
"""
Find the matching handler from the registered functions.
Args:
fn: The function to be dispatched.
args: The args of the function.
kwargs: The kwargs of the function.
"""
if not hashable(fn) or fn not in cls.handlers:
return None
for pattern, handler in cls.handlers[fn]:
if pattern.match_inputs(*args, **kwargs):
return handler
return None
@@ -0,0 +1,129 @@
# Copyright (c) 2025 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 dataclasses
from typing import TYPE_CHECKING
from ...utils import InnerError
from .variables import ConstantVariable, ExceptionVariable
if TYPE_CHECKING:
from .function_graph import FunctionGraph
@dataclasses.dataclass
class ExceptionStack:
# This data structure manages exceptions as in CPython, primarily handling
# the __context__ attribute of SotCapturedException.
_exception_stack: list[ExceptionVariable] = dataclasses.field(
default_factory=list
)
_current_exception: ExceptionVariable | None = dataclasses.field(
default=None
)
def clear_current_exception(self):
self._current_exception = None
def set_current_exception(
self, val: ExceptionVariable, graph: FunctionGraph
):
self._set_context_and_break_context_reference_cycle(val, graph)
self._current_exception = val
def move_current_exception_to_stack(self):
self.push(self.get_current_exception())
self.clear_current_exception()
def get_current_exception(self):
if self._current_exception is None:
raise InnerError("Current exception should not be None")
return self._current_exception
def _set_context_and_break_context_reference_cycle(
self, val: ExceptionVariable, graph: FunctionGraph
):
# set Exception.__context__
self._set_context_recursive(val, len(self._exception_stack) - 1)
self._break_context_reference_cycle(val, graph)
def _set_context_recursive(self, val: ExceptionVariable, prev_idx: int):
# Recursively sets the __context__ attribute for ExceptionVariable objects
# in self._exception_stack. Ensures that __context__ is properly linked
# to the previous exception in the stack.
if (ctx := val.__context__) and not isinstance(ctx, ConstantVariable):
return val
if (
len(self._exception_stack) + prev_idx > 0
): # Prevent invalid negative indexing
prev = self._exception_stack[prev_idx]
self._set_context_recursive(prev, prev_idx - 1)
val.setattr("__context__", prev)
return val
def _break_context_reference_cycle(
self, val: ExceptionVariable, graph: FunctionGraph
):
# Detects and breaks cycles in exception __context__ chains using Floyd's algorithm,
# following CPython's implementation.
fast = slow = val
slow_update_toggle = False
while True:
context = fast.__context__
if isinstance(
context, ConstantVariable
): # End of the chain; no context set
break
if context is val:
# The chain loops back to the original exception; break the cycle.
fast.setattr(
"__context__", ConstantVariable.wrap_literal(None, graph)
)
break
fast = context
if fast is slow:
# Cycle detected; all exceptions on the path have been visited and checked.
break
if slow_update_toggle:
slow = slow.__context__
slow_update_toggle = not slow_update_toggle
def pop(self) -> ExceptionVariable:
return self._exception_stack.pop()
def push(self, val: ExceptionVariable) -> None:
self._exception_stack.append(val)
def empty(self) -> bool:
return len(self._exception_stack) == 0
def __len__(self):
return len(self._exception_stack)
def __repr__(self):
return f"ExceptionStack({self._exception_stack})"
def __getitem__(self, idx: int) -> ExceptionVariable:
return self._exception_stack[idx]
def cleanup(self) -> None:
self._exception_stack[:] = []
self._current_exception = None
@@ -0,0 +1,483 @@
# Copyright (c) 2023 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 gc
import traceback
from collections import defaultdict
from typing import TYPE_CHECKING
import paddle
from ...profiler import EventGuard, event_register
from ...psdb import NO_FALLBACK_CODES
from ...utils import (
ENV_SOT_ALLOW_DYNAMIC_SHAPE,
ENV_SOT_ENABLE_COMPILE_TIME_LIMIT,
ENV_SOT_ENABLE_GUARD_TREE,
ENV_SOT_ENABLE_STRICT_GUARD_CHECK,
ENV_SOT_UNSAFE_CACHE_FASTPATH,
BreakGraphError,
CompileCountInfo,
ConditionalFallbackError,
FallbackError,
InfoCollector,
InnerError,
Singleton,
SotCapturedException,
is_strict_mode,
log,
log_do,
log_once,
)
from ..custom_code import CustomCode
from .function_graph import FunctionGraph
from .guard import Guard
from .opcode_executor import OpcodeExecutor, OpcodeExecutorBase
from .virtual_frame import VirtualFrame
if TYPE_CHECKING:
import types
GuardedFunction = tuple[CustomCode, Guard]
GuardedFunctions = list[GuardedFunction]
GuardChain = list[paddle.framework.core.GuardNodeBase]
GuardChainList = list[GuardChain]
dummy_guard: Guard = lambda frame: True
dummy_guard.expr = "lambda frame: True"
dummy_guard.inlined_expr = "lambda frame: True"
if ENV_SOT_ENABLE_STRICT_GUARD_CHECK.get():
dummy_guard.mirror_guard = lambda frame: True
class OpcodeExecutorCache(metaclass=Singleton):
"""
A singleton class that implements a cache for translated instructions.
This cache is used to store previously translated instructions along with their corresponding guard functions.
Attributes:
cache (dict): A dictionary that maps code objects to tuples of a cache getter function and a list of guarded functions.
translate_count (int): The count of how many instructions have been translated. It is used to test whether the cache hits.
"""
MAX_CACHE_SIZE = 20
MAX_COMPILE_TIME_PER_CODE = 40
MAX_COMPILE_TIME_TOTAL = 15 * 60
CACHE_HIT_FASTPATH_THRESHOLD = 32
cache: dict[
types.CodeType, tuple[GuardedFunctions, paddle.framework.core.GuardTree]
]
translate_count: int
code_symbolic_inputs: dict[types.CodeType, dict[str, None | dict[int, int]]]
compile_time_stats: dict[types.CodeType, float]
consecutive_cache_hit_count: defaultdict[types.CodeType, int]
def __init__(self):
self.cache = {}
self.translate_count = 0
self.code_symbolic_inputs = {}
self.compile_time_stats = {}
self.consecutive_cache_hit_count = defaultdict(int)
def get_symbolic_inputs(
self, code: types.CodeType
) -> dict[str, dict[int, int] | None]:
self.code_symbolic_inputs.setdefault(code, {})
return self.code_symbolic_inputs[code]
def clear(self):
"""
Clears the cache and resets the translate count.
"""
self.cache.clear()
self.translate_count = 0
self.code_symbolic_inputs.clear()
self.compile_time_stats.clear()
def dump_state(self):
return {
"cache": self.cache,
"translate_count": self.translate_count,
"code_symbolic_inputs": self.code_symbolic_inputs,
"compile_time_stats": self.compile_time_stats,
}
def load_state(self, state):
self.cache = state["cache"]
self.translate_count = state["translate_count"]
self.code_symbolic_inputs = state["code_symbolic_inputs"]
self.compile_time_stats = state["compile_time_stats"]
def __call__(self, frame: types.FrameType, **kwargs) -> CustomCode:
code: types.CodeType = frame.f_code
if code not in self.cache:
log(2, f"[Cache] Firstly call {code}\n")
new_custom_code, guard_fn, guard_chain = self.translate(
frame, **kwargs
)
assert guard_fn is not None
assert guard_chain is not None
self.cache[code] = (
[(new_custom_code, guard_fn)],
paddle.framework.core.GuardTree([guard_chain]),
)
return new_custom_code
guarded_fns, guard_tree = self.cache[code]
compile_time_for_code = self.compile_time_stats.get(code, 0)
compile_time_total = sum(self.compile_time_stats.values())
return self.lookup(
frame,
guarded_fns,
guard_tree,
compile_time_for_code,
compile_time_total,
**kwargs,
)
def is_fastpath_threshold_reached(self, code):
# Returns True if the number of consecutive cache hits for the given code
# exceeds the UNSAFE_CACHE_FASTPATH threshold.
return (
self.consecutive_cache_hit_count.get(code, 0)
>= self.CACHE_HIT_FASTPATH_THRESHOLD
)
@event_register("lookup")
def lookup(
self,
frame: types.FrameType,
guarded_fns: GuardedFunctions,
guard_tree: paddle.framework.core.GuardTree,
compile_time_for_code: float,
compile_time_total: float,
**kwargs,
) -> CustomCode:
"""
Looks up the cache for a matching code object and returns a custom code object if a matching guard function is found, otherwise None.
Args:
frame (types.FrameType): The frame whose code object needs to be looked up in the cache.
guarded_fns (GuardedFunctions): The list of guarded functions associated with the code object.
Returns:
CustomCode: The custom code object if a matching guard function is found, otherwise None.
"""
code: types.CodeType = frame.f_code
if len(guarded_fns) >= self.MAX_CACHE_SIZE:
log(2, "[Cache] Exceed max cache size, skip it\n")
return CustomCode(None, False)
enable_strict_guard = ENV_SOT_ENABLE_STRICT_GUARD_CHECK.get()
enable_guard_tree = ENV_SOT_ENABLE_GUARD_TREE.get()
enable_unsafe_cache_fastpath = ENV_SOT_UNSAFE_CACHE_FASTPATH.get()
enable_compile_time_limit = ENV_SOT_ENABLE_COMPILE_TIME_LIMIT.get()
if enable_unsafe_cache_fastpath and (
self.is_fastpath_threshold_reached(code)
):
# NOTE: In inference scenarios, cache misses are generally rare, so we can enable this unsafe short path.
log(
2,
"[Cache] The CACHE_HIT_FASTPATH_THRESHOLD has been reached, so fast path is now enabled\n",
)
return guarded_fns[0][0]
cache_index = None
if enable_strict_guard or enable_guard_tree:
log(4, f"[Cache] Guard tree: \n{guard_tree.stringify()}")
cache_index = guard_tree.lookup(frame)
if not enable_strict_guard and enable_guard_tree:
if cache_index is not None:
# TODO(zrr1999): add a mapping between custom_code and cache_index
return guarded_fns[cache_index][0]
else:
log(2, "[Cache] all guards missed (guard tree mode)\n")
if (
enable_compile_time_limit
and compile_time_for_code >= self.MAX_COMPILE_TIME_PER_CODE
):
log(
2,
"[Cache] Exceed max compile time per code, skip it\n",
)
return CustomCode(None, False)
if (
enable_compile_time_limit
and compile_time_total >= self.MAX_COMPILE_TIME_TOTAL
):
log_once(
f"[SOT] Current total compile time is {compile_time_total}, exceed max compile time total {self.MAX_COMPILE_TIME_TOTAL}, fallback new function to dygraph"
)
log(
2,
"[Cache] Exceed max compile time total, skip it\n",
)
return CustomCode(None, False)
new_custom_code, guard_fn, guard_chain = self.translate(
frame, **kwargs
)
if guard_fn is not None:
assert guard_chain is not None
guarded_fns.append((new_custom_code, guard_fn))
guard_tree.add_guard_chain(guard_chain)
return new_custom_code
for index, (custom_code, guard_fn) in enumerate(guarded_fns):
if enable_strict_guard:
mirror_guard_error = None
try:
with EventGuard("try mirror guard"):
mirror_guard_result = guard_fn.mirror_guard(frame)
except Exception as e:
log(2, f"[Cache] Mirror guard error: {e}\n")
mirror_guard_error = e
try:
with EventGuard("try guard"):
guard_result = guard_fn(frame)
if enable_strict_guard and (not enable_unsafe_cache_fastpath):
assert mirror_guard_result == guard_result, (
"faster guard result is not equal to guard result, "
f"guard_expr: {getattr(guard_fn, 'expr', 'None')} \n"
f"faster_guard_expr: {getattr(guard_fn.mirror_guard, 'expr', 'None')},"
)
if guard_result:
log(
2,
f"[Cache] Cache hit, Guard is \n{getattr(guard_fn, 'expr', 'None')}\n",
)
if not enable_unsafe_cache_fastpath:
# TODO(zrr1999): cache_index should be equal to index when enable_strict_guard.
assert cache_index is None or index == cache_index, (
f"cache_index({cache_index}) is not equal to index({index})"
)
if enable_unsafe_cache_fastpath:
if index == 0:
self.consecutive_cache_hit_count[code] += 1
else:
# Move the current hit to the front
# Note: Be cautious when modifying the order of elements in a list during iteration,
# as it can lead to unexpected behavior.
guarded_fns[:] = [
guarded_fns[index],
*guarded_fns[:index],
*guarded_fns[index + 1 :],
]
self.consecutive_cache_hit_count[code] = 0
return custom_code
else:
log_do(
4,
self.analyse_guard_global_object(guard_fn),
)
log(
2,
f"[Cache] Cache miss, Guard is \n{getattr(guard_fn, 'expr', 'None')}\n",
)
log_do(
2,
self.analyse_guard_error(guard_fn, frame),
)
except Exception as e:
log(2, f"[Cache] Guard function error: {e}\n")
log(
2,
f"[Cache] Guard is \n{getattr(guard_fn, 'expr', 'None')}\n",
)
log_do(
2,
self.analyse_guard_error(guard_fn, frame),
)
if enable_strict_guard and (not enable_unsafe_cache_fastpath):
assert type(e) == type(mirror_guard_error) and str(
e
) == str(mirror_guard_error), (
"mirror guard error is not equal to guard error, "
f"guard_error: {e} \n"
f"mirror_guard_error: {mirror_guard_error},"
)
log(2, "[Cache] all guards missed\n")
if (
enable_compile_time_limit
and compile_time_for_code >= self.MAX_COMPILE_TIME_PER_CODE
):
log(2, "[Cache] Exceed max compile time per code, skip it\n")
return CustomCode(None, False)
if (
enable_compile_time_limit
and compile_time_total >= self.MAX_COMPILE_TIME_TOTAL
):
log_once(
f"[SOT] Current compile time total is {compile_time_total}, exceed max compile time total {self.MAX_COMPILE_TIME_TOTAL}, fallback new function to dygraph"
)
log(
2,
"[Cache] Exceed max compile time total, skip it\n",
)
return CustomCode(None, False)
new_custom_code, guard_fn, guard_chain = self.translate(frame, **kwargs)
if guard_fn is not None:
assert guard_chain is not None
guarded_fns.append((new_custom_code, guard_fn))
guard_tree.add_guard_chain(guard_chain)
return new_custom_code
def before_translate_hook(self, frame: types.FrameType):
if not ENV_SOT_ALLOW_DYNAMIC_SHAPE.get():
return
def translate(
self, frame: types.FrameType, **kwargs
) -> tuple[CustomCode, Guard | None, GuardChain | None]:
"""
Translates the given frame's code object and returns the cache getter function and a guarded function for the translated code object.
Args:
frame (types.FrameType): The frame whose code object needs to be translated.
Returns:
tuple[CustomCode, Guard]: The cache getter function and a guarded function for the translated code object.
"""
self.before_translate_hook(frame)
self.translate_count += 1
custom_new_code, guard_fn, guard_chain = start_translate(
frame, **kwargs
)
return custom_new_code, guard_fn, guard_chain
def analyse_guard_global_object(self, guard_fn):
def inner():
for key in guard_fn.__globals__.keys():
if key.startswith("__object"):
print(
f"[Cache] meet global object: {key} : {guard_fn.__globals__[key]}",
)
return inner
def analyse_guard_error(self, guard_fn, frame):
def inner():
guard_expr = guard_fn.inlined_expr
lambda_head = "lambda frame: "
guard_expr = guard_expr.replace(lambda_head, "")
guards = guard_expr.split(" and ")
for guard_str in guards:
guard = eval(lambda_head + guard_str, guard_fn.__globals__)
result = False
try:
result = guard(frame)
except Exception as e:
print(
f"[Cache] Error occurred when checking guard {guard_str}: {e}"
)
return
if result is False:
print(f"[Cache] missed at {guard_str}")
return
print("[Cache] missed guard not found.")
return inner
def start_translate(
frame: types.FrameType,
**kwargs,
) -> tuple[CustomCode, Guard | None, GuardChain | None]:
"""
Starts the translation process for the given frame and returns the translated code object, its guard function and its guard tree node, or None if translation fails.
Args:
frame: The frame to be translated.
Returns:
tuple[CustomCode, Guard | None, GuardChain | None]: The translated code object, its guard function and its guard tree node, or None if translation fails.
"""
simulator = None
graph = FunctionGraph(frame.f_code, frame.f_globals, **kwargs)
try:
vframe = VirtualFrame.from_real_frame(frame, graph)
simulator = OpcodeExecutor(vframe, graph)
simulator.check_code_simulatable()
InfoCollector().attach(CompileCountInfo, frame.f_code)
new_custom_code, guard_fn = simulator.transform(frame)
if ENV_SOT_ENABLE_STRICT_GUARD_CHECK.get():
assert guard_fn(frame)
assert guard_fn.mirror_guard(frame)
if not simulator._graph.need_cache:
return (
CustomCode(None, True),
None,
None,
)
guard_chain = simulator.guard_chain
if len(guard_chain) == 0:
guard_chain: GuardChain = [paddle.framework.core.DummyGuardNode()]
return new_custom_code, guard_fn, guard_chain
# TODO(0x45f): handle BreakGraphError to trigger fallback
except BreakGraphError as e:
raise RuntimeError(
f"Found BreakGraphError raised, it should not be catch at start_translate!\n{e}"
)
except FallbackError as e:
if frame.f_code in NO_FALLBACK_CODES:
raise InnerError(
f"{frame.f_code.co_name} should not fallback, but got '{e}'"
)
if is_strict_mode():
raise
log(
2,
f"Unsupported Frame is {frame.f_code}, error message is: \n"
+ "".join(traceback.format_exception(type(e), e, e.__traceback__)),
)
dummy_guard_chain: GuardChain = [paddle.framework.core.DummyGuardNode()]
guard, guard_chain = dummy_guard, dummy_guard_chain
if isinstance(e, ConditionalFallbackError):
# Guard global variables only
graph.input_variables.clear()
guard = graph.guard_fn
guard_chain = graph.guard_chain
return (
CustomCode(None, e.disable_eval_frame),
guard,
guard_chain,
)
except SotCapturedException as e:
log(
1,
"Note: This fallback may be triggered by user code, or it could result from an internal "
"SOT exception being incorrectly captured. Please investigate carefully.\n",
)
if is_strict_mode():
raise
dummy_guard_chain: GuardChain = [paddle.framework.core.DummyGuardNode()]
return (CustomCode(None, True), dummy_guard, dummy_guard_chain)
except Exception as e:
raise InnerError(OpcodeExecutorBase.error_message_summary(e)) from e
finally:
if simulator is not None:
simulator.cleanup()
del simulator
gc.collect()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,327 @@
# Copyright (c) 2023 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 types
import weakref
from collections.abc import Callable
from functools import cached_property
from typing import TYPE_CHECKING, Any, TypeVar
import paddle
from ...profiler import EventGuard
from ...utils import (
ENV_SOT_ENABLE_FASTER_GUARD,
ENV_SOT_ENABLE_STRICT_GUARD_CHECK,
current_symbol_registry,
log,
log_do,
)
Guard = Callable[[types.FrameType], bool]
if TYPE_CHECKING:
from .variables import VariableBase
GuardBase = paddle.framework.core.GuardBase
CheckGuardInputT = TypeVar("CheckGuardInputT", bound=VariableBase)
# NOTE(SigureMo): [How to write Stringified Guard?]
# 1. we should capture free variables manually, the string cannot capture free
# variables automatically.
# 2. Be aware that the comparison logic before and after stringify may be different.
# 3. we should compute as much as possible at "compile time" and encode the
# computation in the Guard string, rather than passing it to runtime to minimize
# runtime overhead.
class StringifiedExpression:
"""
Used to store string based expressions for generating Guard.
"""
def __init__(
self,
expr_template: str,
sub_exprs: list[StringifiedExpression],
free_vars: dict[str, Any],
):
self.expr_template = expr_template
expr = self.expr_template.format(
*[sub_expr.symbol for sub_expr in sub_exprs]
)
self.registered_expr = expr
self.symbol = current_symbol_registry().request_symbol(expr)
self.sub_exprs = sub_exprs
self.free_vars = free_vars
@cached_property
def inlined_expr(self):
return self.expr_template.format(
*[sub_expr.inlined_expr for sub_expr in self.sub_exprs]
)
def gen_expr(self):
def gen_expr_fn():
return self.expr_template.format(
*[sub_expr.gen_expr() for sub_expr in self.sub_exprs]
)
return current_symbol_registry().gen_expr(
self.registered_expr, gen_expr_fn
)
def __hash__(self):
if self.free_vars:
return hash((self.inlined_expr, id(self)))
else:
return hash(self.inlined_expr)
class FasterStringifiedExpression(StringifiedExpression):
def __init__(
self,
expr_template: str,
faster_guard: GuardBase,
sub_exprs: list[StringifiedExpression],
free_vars: dict[str, Any],
):
self.faster_guard = faster_guard
if ENV_SOT_ENABLE_FASTER_GUARD.get():
if ENV_SOT_ENABLE_STRICT_GUARD_CHECK.get():
self.py_guard_expr_template = original_expr_template = (
expr_template
)
else:
original_expr_template = expr_template
expr_template, free_vars = gen_faster_guard_expr_template(
faster_guard, sub_exprs, free_vars
)
log(
3,
f"[FasterGuard] transform {original_expr_template} to {expr_template}\n",
)
super().__init__(expr_template, sub_exprs, free_vars)
def gen_mirror_guard(
self, enable_faster_gurad: bool
) -> StringifiedExpression:
if not enable_faster_gurad:
# gen faster_guard_expr
expr_template, expr_free_vars = gen_faster_guard_expr_template(
self.faster_guard,
self.sub_exprs,
self.free_vars,
)
return StringifiedExpression(
expr_template, self.sub_exprs, expr_free_vars
)
# gen pyGuard_expr
return StringifiedExpression(
self.py_guard_expr_template, self.sub_exprs, self.free_vars
)
def gen_faster_guard_expr_template(
faster_guard: GuardBase,
sub_exprs: list[StringifiedExpression],
free_vars: dict[str, Any],
) -> tuple[str, dict[str, Any]]:
guard_cls_name = faster_guard.__class__.__name__
guard_name = f"{guard_cls_name}_{id(faster_guard)}"
expr_template = guard_name + "(" + ", ".join(["{}"] * len(sub_exprs)) + ")"
free_vars = union_free_vars(free_vars, {guard_name: faster_guard.check})
return expr_template, free_vars
def union_free_vars(*free_vars: dict[str, Any]):
return {k: v for d in free_vars for k, v in d.items()}
def make_guard(stringified_guards: list[StringifiedExpression]) -> Guard:
"""
Make a guard from a list of StringifiedExpression.
For more design ideas, refer to the `Stringified guard <https://github.com/PaddlePaddle/PaddleSOT/blob/develop/docs/design/stringify-guard.md>`_ for details.
Args:
stringified_guards: a list of StringifiedExpression.
"""
with EventGuard("make_guard"):
num_guards = len(stringified_guards)
if not num_guards:
guard = lambda frame: True
guard.expr = "lambda frame: True"
guard.original_guard = guard
if ENV_SOT_ENABLE_STRICT_GUARD_CHECK.get():
guard.mirror_guard = lambda frame: True
return guard
free_vars = union_free_vars(
*(expr.free_vars for expr in stringified_guards)
)
inlined_guard_expr = "lambda frame: " + " and ".join(
[expr.inlined_expr for expr in stringified_guards]
)
guard_expr: str = "lambda frame: " + " and ".join(
[expr.gen_expr() for expr in stringified_guards]
)
guard = eval(guard_expr, free_vars)
log(3, f"[Guard] {inlined_guard_expr}\n")
guard.inlined_expr = inlined_guard_expr
guard.expr = guard_expr
def check_guard_callable(guard: GuardBase):
assert callable(guard), "guard must be callable."
if ENV_SOT_ENABLE_STRICT_GUARD_CHECK.get():
mirror_guard_expr_list: list[str] = []
mirror_guard_temp_free_vars: dict[str, Any] = {}
enable_faster_gurad = ENV_SOT_ENABLE_FASTER_GUARD.get()
for expr in stringified_guards:
if isinstance(expr, FasterStringifiedExpression):
expr = expr.gen_mirror_guard(enable_faster_gurad)
mirror_guard_expr_list.append(expr.inlined_expr)
mirror_guard_temp_free_vars.update(expr.free_vars)
mirror_guard_expr = "lambda frame: " + " and ".join(
mirror_guard_expr_list
)
mirror_guard_free_vars = union_free_vars(
mirror_guard_temp_free_vars
)
guard.mirror_guard = eval(mirror_guard_expr, mirror_guard_free_vars)
guard.mirror_guard.expr = mirror_guard_expr
check_guard_callable(guard.mirror_guard)
check_guard_callable(guard)
return guard
def support_weak_ref(obj):
if isinstance(obj, types.FunctionType):
return True
return False
# TODO(zrr1999): unify check_guard and check_faster_guard
def check_guard(
fn: Callable[[CheckGuardInputT], list[StringifiedExpression]],
) -> Callable[[CheckGuardInputT], list[StringifiedExpression]]:
def wrapper(self: CheckGuardInputT) -> list[StringifiedExpression]:
assert self.tracker.is_traceable(), (
"Cannot make guard from a non-tracable guard variable."
)
def guard_log():
frame_value_tracer = self.tracker.trace_value_from_frame()
print(
f"[Guard] guard_fn for {self}, tracker={self.tracker.__class__.__name__}, value={frame_value_tracer.registered_expr}"
)
log_do(4, guard_log)
return fn(self)
return wrapper
def check_faster_guard(
fn: Callable[[CheckGuardInputT], list[paddle.framework.core.GuardNodeBase]],
) -> Callable[[CheckGuardInputT], list[paddle.framework.core.GuardNodeBase]]:
def wrapper(
self: CheckGuardInputT,
) -> list[paddle.framework.core.GuardNodeBase]:
assert self.tracker.is_traceable(), (
"Cannot make guard from a non-tracable guard variable."
)
def guard_log():
frame_value_tracer = self.tracker.trace_value_from_frame()
print(
f"[Guard Tree] guard_fn for {self}, tracker={self.tracker.__class__.__name__}, value={frame_value_tracer.registered_expr}"
)
log_do(4, guard_log)
return fn(self)
return wrapper
@check_guard
def object_equal_stringified_guard(self) -> list[StringifiedExpression]:
frame_value_tracer = self.tracker.trace_value_from_frame()
obj_free_var_name = f"__{self.id}"
weak_ref_obj = self.get_py_value()
if support_weak_ref(weak_ref_obj):
weak_ref_obj = weakref.ref(self.get_py_value())
return [
FasterStringifiedExpression(
f"{obj_free_var_name}() is not None and {{}} == {obj_free_var_name}()",
paddle.framework.core.WeakRefMatchGuard(self.get_py_value()),
[frame_value_tracer],
union_free_vars(
frame_value_tracer.free_vars,
{obj_free_var_name: weak_ref_obj},
),
)
]
return [
FasterStringifiedExpression(
f"{{}} == {obj_free_var_name}",
paddle.framework.core.ValueMatchGuard(weak_ref_obj),
[frame_value_tracer],
union_free_vars(
frame_value_tracer.free_vars,
{obj_free_var_name: self.get_py_value()},
),
)
]
@check_faster_guard
def object_equal_faster_guard(
self,
) -> list[paddle.framework.core.GuardNodeBase]:
expr_node = self.tracker.guard_tree_expr_node()
weak_ref_obj = self.get_py_value()
if support_weak_ref(weak_ref_obj):
weak_ref_obj = weakref.ref(self.get_py_value())
return [
paddle.framework.core.GuardNode(
paddle.framework.core.WeakRefMatchGuard(self.get_py_value()),
[expr_node],
)
]
return [
paddle.framework.core.GuardNode(
paddle.framework.core.ValueMatchGuard(weak_ref_obj),
[expr_node],
)
]
def stringify_pyobject(obj: object) -> tuple[str, dict[str, Any]]:
if isinstance(obj, paddle.core.VarDesc.VarType):
return f"paddle.core.VarDesc.VarType({obj.value})", {"paddle": paddle}
elif isinstance(obj, paddle.core.DataType):
return f"paddle.core.DataType({obj.value})", {"paddle": paddle}
# For builtin values
return f"{obj!r}", {}
@@ -0,0 +1,72 @@
# Copyright (c) 2023 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.
# flags for instructions
from enum import Enum
class FORMAT_VALUE_FLAG:
FVC_MASK = 0x3
FVC_NONE = 0x0
FVC_STR = 0x1
FVC_REPR = 0x2
FVC_ASCII = 0x3
FVS_MASK = 0x4
FVS_HAVE_SPEC = 0x4
class CONVERT_VALUE_FLAG:
CV_STR = 1
CV_REPR = 2
CV_ASCII = 3
# https://github.com/python/cpython/blob/3.14/Include/internal/pycore_opcode_utils.h#L63-L68
class MAKE_FUNCTION_FLAG:
MF_HAS_ANNOTATE = 0x10
MF_HAS_CLOSURE = 0x08
MF_HAS_ANNOTATION = 0x04
MF_HAS_KWDEFAULTS = 0x02
MF_HAS_DEFAULTS = 0x01
class CALL_FUNCTION_EX_FLAG:
CFE_HAS_KWARGS = 0x01
# see https://github.com/python/cpython/blob/3.12/Python/intrinsics.c#L211-L225
class IntrinsicsUnaryFunctions(Enum):
INTRINSIC_1_INVALID = 0
INTRINSIC_PRINT = 1 # no support, only non-interactive mode
INTRINSIC_IMPORT_STAR = 2 # no support, `from module import *`
INTRINSIC_STOPITERATION_ERROR = 3 # no support, generator or coroutine
INTRINSIC_ASYNC_GEN_WRAP = 4 # no support, async
INTRINSIC_UNARY_POSITIVE = 5
INTRINSIC_LIST_TO_TUPLE = 6
INTRINSIC_TYPEVAR = 7 # no support, PEP 695
INTRINSIC_PARAMSPEC = 8 # no support, PEP 695
INTRINSIC_TYPEVARTUPLE = 9 # no support, PEP 695
INTRINSIC_SUBSCRIPT_GENERIC = 10 # no support, PEP 695
INTRINSIC_TYPEALIAS = 11 # no support, PEP 695
# https://github.com/python/cpython/blob/3.14/Include/internal/pycore_opcode_utils.h#L70-L76
# All are attributes of 'builtins'
LOAD_COMMON_CONSTANT_FLAG = (
"AssertionError",
"NotImplementedError",
"tuple",
"all",
"any",
)
@@ -0,0 +1,306 @@
# Copyright (c) 2023 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
from collections.abc import Callable
from typing import Any, Concatenate, Generic, TypeAlias, TypeVar
from typing_extensions import ParamSpec
P = ParamSpec("P")
R = TypeVar("R")
MutableDataT = TypeVar("MutableDataT", bound="MutableData")
DataGetter: TypeAlias = Callable[[MutableDataT, Any], Any]
InnerMutableDataT = TypeVar(
"InnerMutableDataT", bound="dict[str, Any] | list[Any]"
)
class Mutation:
ABBR: str
class MutationSet(Mutation):
"""
Setting a value.
This mutation is used for MutableDictLikeData and MutableListLikeData.
"""
ABBR = "S"
def __init__(self, key, value):
self.key = key
self.value = value
def __repr__(self):
return f"MutationSet({self.key}, {self.value})"
class MutationDel(Mutation):
"""
Deleting a value.
This mutation is used for MutableDictLikeData and MutableListLikeData.
"""
ABBR = "D"
def __init__(self, key):
self.key = key
def __repr__(self):
return f"MutationDel({self.key})"
class MutationNew(Mutation):
"""
Adding a new value.
This mutation is only used for MutableDictLikeData.
"""
ABBR = "N"
def __init__(self, key, value):
self.key = key
self.value = value
def __repr__(self):
return f"MutationNew({self.key}, {self.value})"
class MutationInsert(Mutation):
"""
Inserting a value.
This mutation is only used for MutableListLikeData.
"""
ABBR = "I"
def __init__(self, index, value):
self.index = index
self.value = value
def __repr__(self):
return f"MutationInsert({self.index}, {self.value})"
class MutationPermutate(Mutation):
"""
Permutating all the values.
This mutation is only used for MutableListLikeData.
"""
ABBR = "P"
def __init__(self, permutation):
self.permutation = permutation
def __repr__(self):
return f"MutationPermutate({self.permutation})"
def record_mutation(
mutation_fn: Callable[Concatenate[MutableDataT, P], Mutation],
) -> Callable[Concatenate[MutableDataT, P], None]:
def wrapper(self, *args: P.args, **kwargs: P.kwargs):
mutation = mutation_fn(self, *args, **kwargs)
self.records.append(mutation)
return wrapper
class MutableData(Generic[InnerMutableDataT]):
"""
An intermediate data structure between data and variable, it records all the mutations.
"""
read_cache: InnerMutableDataT
class Empty:
def __repr__(self):
return "Empty()"
def __init__(self, data: Any, getter: DataGetter):
self.original_data = data
self.getter = getter
self.records: list[Mutation] = []
def is_empty(self, value):
return isinstance(value, MutableData.Empty)
@property
def version(self):
return len(self.records)
@property
def has_changed(self):
return self.version != 0
def check_changed(self, key: Any) -> bool:
raise NotImplementedError
def rollback(self, version: int):
assert version <= self.version
self.records[:] = self.records[:version]
def get(self, key):
raise NotImplementedError
def set(self, key, value):
raise NotImplementedError
def apply(self, mutation: Mutation, write_cache: InnerMutableDataT):
raise NotImplementedError
def reproduce(self, version: int | None = None) -> InnerMutableDataT:
if version is None:
version = self.version
write_cache = self.read_cache.copy()
for mutation in self.records[:version]:
self.apply(mutation, write_cache)
return write_cache
def __repr__(self) -> str:
records_abbrs = "".join([mutation.ABBR for mutation in self.records])
return f"{self.__class__.__name__}({records_abbrs})"
class MutableDictLikeData(MutableData["dict[str, Any]"]):
def __init__(self, data: Any, getter: DataGetter):
super().__init__(data, getter)
self.read_cache = {}
def clear_read_cache(self):
self.read_cache.clear()
def check_changed(self, key: Any) -> bool:
if not self.has_changed:
return False
for mutation in self.records:
if (
isinstance(mutation, (MutationNew, MutationDel, MutationSet))
and mutation.key == key
):
return True
return False
def get(self, key: Any):
# TODO(SigureMo): Optimize performance of this.
write_cache = self.reproduce(self.version)
if key not in write_cache:
self.read_cache[key] = self.getter(self, key)
return self.reproduce(self.version)[key]
def get_all(self):
original_keys = list(self.original_data.keys())
for mutation in self.records:
if isinstance(mutation, MutationNew):
original_keys.append(mutation.key)
elif isinstance(mutation, MutationDel):
original_keys.remove(mutation.key)
return {key: self.get(key) for key in original_keys}
@record_mutation
def set(self, key: Any, value: Any) -> Mutation:
is_new = False
if self.is_empty(self.get(key)):
is_new = True
return (
MutationSet(key, value) if not is_new else MutationNew(key, value)
)
@record_mutation
def delete(self, key):
return MutationDel(key)
def apply(self, mutation: Mutation, write_cache: dict[str, Any]):
if isinstance(mutation, MutationNew):
write_cache[mutation.key] = mutation.value
elif isinstance(mutation, MutationSet):
write_cache[mutation.key] = mutation.value
elif isinstance(mutation, MutationDel):
write_cache[mutation.key] = MutableData.Empty()
else:
raise ValueError(f"Unknown mutation type {mutation}")
def reproduce(self, version: int | None = None):
if version is None:
version = self.version
write_cache = self.read_cache.copy()
for mutation in self.records[:version]:
self.apply(mutation, write_cache)
return write_cache
class MutableListLikeData(MutableData["list[Any]"]):
def __init__(self, data: Any, getter: DataGetter):
super().__init__(data, getter)
self.read_cache = [
self.getter(self, idx) for idx in range(len(self.original_data))
]
def clear_read_cache(self):
self.read_cache[:] = []
def check_changed(self, key: Any) -> bool:
return self.has_changed
@property
def length(self):
return len(self.reproduce())
def get(self, key):
write_cache = self.reproduce(self.version)
return write_cache[key]
def get_all(self) -> list[Any]:
items = self.reproduce(self.version)
return items
@record_mutation
def set(self, key: int, value: Any):
return MutationSet(self._regularize_index(key), value)
@record_mutation
def delete(self, key: int):
return MutationDel(self._regularize_index(key))
@record_mutation
def insert(self, index: int, value: Any):
return MutationInsert(self._regularize_index(index), value)
@record_mutation
def permutate(self, permutation: list[int]):
return MutationPermutate(permutation)
def _regularize_index(self, index: int):
if index < 0:
index += self.length
return index
def apply(self, mutation: Mutation, write_cache: list[Any]):
if isinstance(mutation, MutationSet):
write_cache[mutation.key] = mutation.value
elif isinstance(mutation, MutationDel):
write_cache[:] = (
write_cache[: mutation.key] + write_cache[mutation.key + 1 :]
)
elif isinstance(mutation, MutationInsert):
write_cache.insert(mutation.index, mutation.value)
elif isinstance(mutation, MutationPermutate):
write_cache[:] = [write_cache[i] for i in mutation.permutation]
else:
raise ValueError(f"Unknown mutation type {mutation}")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,228 @@
# Copyright (c) 2023 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 inspect
import sys
from typing import TYPE_CHECKING
from ...utils import (
BreakGraphError,
DataDependencyControlFlowBreak,
FallbackError,
UnsupportedIteratorBreak,
)
from ...utils.exceptions import SotCapturedStopIteration
from ..instruction_utils import Instruction
from .dispatch_functions import generator_send
from .opcode_executor import OpcodeExecutorBase, Stop
from .tracker import DanglingTracker
from .variables import (
BuiltinVariable,
ConstantVariable,
GeneratorVariable,
IterVariable,
ObjectVariable,
UserDefinedIterVariable,
VariableBase,
)
if TYPE_CHECKING:
from .function_graph import FunctionGraph
from .virtual_frame import VirtualFrame
def inline_for_iter_impl(exe: OpcodeExecutorBase, instr: Instruction):
iterator = exe.stack.top
assert isinstance(iterator, IterVariable)
exe._graph.add_global_guarded_variable(iterator)
# simply get next
if not isinstance(iterator, UserDefinedIterVariable):
try:
exe.stack.push(iterator.next())
except SotCapturedStopIteration:
exe.stack.pop()
assert isinstance(instr.jump_to, Instruction)
exe.vframe.lasti = exe.indexof(instr.jump_to)
if sys.version_info >= (3, 12):
assert exe._instructions[exe.vframe.lasti].opname == "END_FOR"
skip_n_instrs = 2 if sys.version_info >= (3, 13) else 1
exe.vframe.lasti += skip_n_instrs
else:
exe._graph.remove_global_guarded_variable(iterator)
raise BreakGraphError(
UnsupportedIteratorBreak(
reason_str=f"Found {iterator.__class__.__name__} as iterator."
)
)
class OpcodeInlineExecutor(OpcodeExecutorBase):
"""
A class that represents an executor for inlined opcode operations.
Args:
fn_variable: The function variable.
"""
def __init__(
self,
vframe: VirtualFrame,
code_var: VariableBase,
graph: FunctionGraph,
):
super().__init__(vframe, graph)
self.return_value: VariableBase | None = None
self._code_var = code_var
self._name = "InlineFn"
def inline_call(self) -> VariableBase:
"""
Execute the inline call of the function.
"""
self._graph.add_global_guarded_variable(self._code_var)
self.run()
assert self.return_value is not None
return self.return_value
def RETURN_VALUE(self, instr: Instruction):
assert len(self.stack) == 1, (
f"Stack must have one element, but get {len(self.stack)} elements."
)
self.return_value = self.stack.pop()
return Stop(state="Return")
def RETURN_CONST(self, instr: Instruction):
self.return_value = self.vframe.consts[instr.arg]
return Stop(state="Return")
def _break_graph_when_if(self, result, instr: Instruction):
"""
Helper method to raise a BreakGraphError when breaking the graph in a jump operation.
Args:
result: The result of the operation.
instr (Instruction): The jump instruction.
"""
raise BreakGraphError(DataDependencyControlFlowBreak())
def FOR_ITER(self, instr: Instruction):
return inline_for_iter_impl(self, instr)
class OpcodeInlineGeneratorExecutor(OpcodeExecutorBase):
def __init__(
self,
vframe: VirtualFrame,
code_var: VariableBase,
graph: FunctionGraph,
):
super().__init__(vframe, graph)
self.return_value: VariableBase | None = None
self._code_var = code_var
self._name = "InlineGen"
def inline_call(self) -> VariableBase:
self._graph.add_global_guarded_variable(self._code_var)
self.run()
assert self.return_value is not None
return self.return_value
def RETURN_GENERATOR(self, instr: Instruction):
vframe = self.vframe
code_var = self._code_var
# NOTE: we set the real tracker in calling function
self.return_value = GeneratorVariable(
code_var, vframe, self._graph, DanglingTracker()
)
return Stop(state="Return")
def SEND(self, instr: Instruction):
assert len(self.stack) >= 2
recv = self.stack.pop()
source_obj = self.stack.top
if not isinstance(source_obj, IterVariable):
raise FallbackError(
"Yield from for non-generator object is not supported."
)
self.stack.push(
BuiltinVariable(generator_send, self._graph, DanglingTracker())(
source_obj, recv
)
)
def END_SEND(self, instr: Instruction):
value = self.stack.pop()
receiver = self.stack.pop() # pop the receiver
self.stack.push(value)
def GEN_START(self, instr: Instruction):
tos = self.stack.pop()
assert isinstance(tos, ConstantVariable)
assert tos.value is None
def YIELD_VALUE(self, instr: Instruction):
assert len(self.stack) >= 1
self.return_value = self.stack.pop()
return Stop(state="Yield")
def GET_YIELD_FROM_ITER(self, instr: Instruction):
source_obj = self.stack.top
if isinstance(source_obj, ObjectVariable) and inspect.iscoroutine(
source_obj.value
):
raise FallbackError(
"Get yield from iter for coroutine object is not supported."
)
if isinstance(source_obj, GeneratorVariable):
return
source_obj = self.stack.pop()
iter_variable = BuiltinVariable(iter, self._graph, DanglingTracker())(
source_obj
)
self.stack.push(iter_variable)
def YIELD_FROM(self, instr: Instruction):
recv = self.stack.pop()
source_obj = self.stack.top
if not isinstance(source_obj, IterVariable):
raise FallbackError(
"Yield from for non-generator object is not supported."
)
self.return_value = BuiltinVariable(
generator_send, self._graph, DanglingTracker()
)(source_obj, recv)
assert self.vframe.lasti > 0
self.vframe.lasti -= 1
return Stop(state="Yield")
def FOR_ITER(self, instr: Instruction):
return inline_for_iter_impl(self, instr)
def RETURN_VALUE(self, instr: Instruction):
assert len(self.stack) == 1, (
f"Stack must have one element, but get {len(self.stack)} elements."
)
self.return_value = self.stack.pop()
return Stop(state="Return")
def RETURN_CONST(self, instr: Instruction):
self.return_value = self.vframe.consts[instr.arg]
return Stop(state="Return")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,237 @@
# Copyright (c) 2023 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
from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar
if TYPE_CHECKING:
from collections.abc import Callable
from typing import TypeAlias
from .mutable_data import DataGetter, MutableData
from .pycode_generator import PyCodeGen
from .variables import VariableBase
IdGetter: TypeAlias = Callable[[Any], int]
MutableDataT = TypeVar("MutableDataT", bound=MutableData)
class SideEffectsState(NamedTuple):
data_id_to_proxy: dict[int, MutableData]
proxy_variables: list[VariableBase]
mutable_variables: list[VariableBase]
proxy_versions: list[int]
mutable_attrs: list[dict[str, Any]]
class SideEffects:
def __init__(self):
self.data_id_to_proxy: dict[int, MutableData] = {}
self.proxy_variables: list[VariableBase] = []
self.mutable_variables: list[VariableBase] = []
def record_proxy_variable(self, variable: VariableBase):
if variable not in self.proxy_variables:
self.proxy_variables.append(variable)
def record_mutable_variable(self, variable: VariableBase):
if variable not in self.mutable_variables:
self.mutable_variables.append(variable)
def get_proxy(
self,
proxy_type: type[MutableDataT],
data: Any,
getter: DataGetter,
id_getter: IdGetter = id,
) -> MutableDataT:
data_id = id_getter(data)
if data_id not in self.data_id_to_proxy:
self.data_id_to_proxy[data_id] = proxy_type(data, getter)
return self.data_id_to_proxy[data_id] # type: ignore
def get_state(self):
return SideEffectsState(
self.data_id_to_proxy.copy(),
self.proxy_variables.copy(),
self.mutable_variables.copy(),
[proxy.version for proxy in self.data_id_to_proxy.values()],
[
{attr: getattr(var, attr)}
for var in self.mutable_variables
for attr in var.mutable_attrs
],
)
def restore_state(self, state: SideEffectsState):
self.data_id_to_proxy = state.data_id_to_proxy
self.proxy_variables = state.proxy_variables
self.mutable_variables = state.mutable_variables
# NOTE(SigureMo): We can use the `strict=True` option in zip after
# Python 3.10.
assert len(self.data_id_to_proxy.values()) == len(
state.proxy_versions
), "proxy_versions length not match"
assert sum(
len(var.mutable_attrs) for var in self.mutable_variables
) == len(state.mutable_attrs), "mutable_attrs length not match"
for proxy, version in zip(
self.data_id_to_proxy.values(), state.proxy_versions
):
proxy.rollback(version)
for (variable, attr), attr_dict in zip(
(
(var, attr)
for var in self.mutable_variables
for attr in var.mutable_attrs
),
(attr_dict for attr_dict in state.mutable_attrs),
):
setattr(variable, attr, attr_dict[attr])
class SideEffectRestorer:
def pre_gen(self, codegen: PyCodeGen):
raise NotImplementedError
def post_gen(self, codegen: PyCodeGen):
raise NotImplementedError
class DictSideEffectRestorer(SideEffectRestorer):
"""
old_dict.clear()
old_dict.update(new_dict)
"""
def __init__(self, var: VariableBase):
super().__init__()
self.var = var
def pre_gen(self, codegen: PyCodeGen):
# Reference to the original dict.
# load old_dict.update and new_dict to stack.
self.var.reconstruct(codegen)
codegen.gen_load_method("update")
# Generate dict by each key-value pair.
self.var.reconstruct(codegen, use_tracker=False)
# load old_dict.clear to stack.
self.var.reconstruct(codegen)
codegen.gen_load_method("clear")
def post_gen(self, codegen: PyCodeGen):
# Call methods to apply side effects.
codegen.gen_call_method(0) # call clear
codegen.gen_pop_top()
codegen.gen_call_method(1) # call update
codegen.gen_pop_top()
class ListSideEffectRestorer(SideEffectRestorer):
"""
old_list[:] = new_list
"""
def __init__(self, var: VariableBase):
super().__init__()
self.var = var
def pre_gen(self, codegen: PyCodeGen):
# Reference to the original list.
# load new_list to stack.
self.var.reconstruct(codegen, use_tracker=False)
# load old_list[:] to stack.
self.var.reconstruct(codegen)
codegen.gen_load_const(None)
codegen.gen_load_const(None)
codegen.gen_build_slice(2)
def post_gen(self, codegen: PyCodeGen):
# Call STORE_SUBSCR to apply side effects.
codegen.gen_store_subscr()
class GlobalSetSideEffectRestorer(SideEffectRestorer):
"""
global_var = new_value
"""
def __init__(self, name: str, var: VariableBase):
super().__init__()
self.name = name
self.var = var
def pre_gen(self, codegen: PyCodeGen):
self.var.reconstruct(codegen)
def post_gen(self, codegen: PyCodeGen):
codegen.gen_store_global(self.name)
class GlobalDelSideEffectRestorer(SideEffectRestorer):
"""
del global_var
"""
def __init__(self, name: str):
super().__init__()
self.name = name
def pre_gen(self, codegen: PyCodeGen):
# do nothing
...
def post_gen(self, codegen: PyCodeGen):
codegen.gen_delete_global(self.name)
class ObjSetSideEffectRestorer(SideEffectRestorer):
"""
obj.attr = new_value
"""
def __init__(self, obj: VariableBase, name: str, var: VariableBase):
super().__init__()
self.obj = obj
self.name = name
self.var = var
def pre_gen(self, codegen: PyCodeGen):
# value
self.var.reconstruct(codegen)
# obj
self.obj.reconstruct(codegen)
def post_gen(self, codegen: PyCodeGen):
codegen.gen_store_attr(self.name)
class ObjDelSideEffectRestorer(SideEffectRestorer):
"""
del obj.attr
"""
def __init__(self, obj: VariableBase, name: str):
super().__init__()
self.obj = obj
self.name = name
def pre_gen(self, codegen: PyCodeGen):
self.obj.reconstruct(codegen)
def post_gen(self, codegen: PyCodeGen):
codegen.gen_delete_attr(self.name)
@@ -0,0 +1,619 @@
# Copyright (c) 2023 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 builtins
import sys
from itertools import chain
from typing import TYPE_CHECKING
import paddle
from ...utils import InnerError, NameGenerator
from .guard import StringifiedExpression, stringify_pyobject, union_free_vars
if TYPE_CHECKING:
from collections.abc import Sequence
from ...utils.magic_methods import BinaryOp, UnaryOp
from .pycode_generator import PyCodeGen
from .variables import FunctionVariable, VariableBase
class Tracker:
"""
Tracker is a base class responsible for tracking variables or objects in Python code.
It is used to identify how a variable is derived from the initial state of the frame.
Args:
inputs: The list of variables to be tracked.
Note:
It serves as an abstract class and should not be instantiated directly.
"""
inputs: Sequence[VariableBase]
name_generator = NameGenerator("tracker_")
def __init__(self, inputs: Sequence[VariableBase], changed: bool = False):
self.inputs = inputs
self.changed = changed
self.id = Tracker.name_generator.next()
def gen_instructions(self, codegen: PyCodeGen) -> None:
"""
Generate instructions based on the tracked variables.
Args:
codegen (PyCodeGen): An instance of PyCodeGen to generate instructions.
"""
raise NotImplementedError
def guard_tree_expr_node(self) -> paddle.framework.core.ExprNodeBase:
raise NotImplementedError(
f"{self.__class__.__name__} has no guard_tree_expr_node"
)
# TODO(xiongkun): trace_value_from_frame is not a good name, it should be more related to guard but not traceable.
def trace_value_from_frame(self) -> StringifiedExpression:
"""
Trace the value of the tracked variables from the frame. It used for generating the guard.
Returns:
The value of the tracked variables.
"""
raise NotImplementedError
def is_traceable(self) -> bool:
"""
Determine if all the tracked variables can be traced from the frame.
Returns:
bool: True if all tracked variables are traceable, False otherwise.
"""
if self.changed:
return False
for input in self.inputs:
if not input.tracker.is_traceable():
return False
return True
def need_guard(self) -> bool:
return self.is_traceable()
class DummyTracker(Tracker):
"""
DummyTracker is a subclass of Tracker that specifically tracks variables cannot be reproduced from the frame.
It is mostly generated by complex operations (instructions).
Args:
inputs (list[VariableBase]): The input variables associated with the generated variables.
"""
def __init__(self, inputs: Sequence[VariableBase]):
super().__init__(inputs)
def gen_instructions(self, codegen: PyCodeGen):
raise InnerError("DummyTracker has no instructions")
def trace_value_from_frame(self):
raise InnerError("DummyTracker can't trace value from frame")
def is_traceable(self):
return False
def __repr__(self) -> str:
return f"DummyTracker(num_inputs={len(self.inputs)})"
def need_guard(self) -> bool:
return False
class SymbolicOperationTracker(Tracker):
"""
SymbolicOperationTracker is a subclass of Tracker that specifically tracks variables cannot be reproduced from the frame.
It is mostly generated by complex operations of symbolic variables.
Args:
inputs (list[VariableBase]): The input variables associated with the generated variables.
"""
def __init__(self, inputs: Sequence[VariableBase], op: UnaryOp | BinaryOp):
super().__init__(inputs)
self.op = op
def gen_instructions(self, codegen: PyCodeGen):
raise InnerError("SymbolicOperationTracker has no instructions")
def trace_value_from_frame(self):
raise InnerError(
"SymbolicOperationTracker can't trace value from frame"
)
def __repr__(self) -> str:
return f"SymbolicOperationTracker(num_inputs={len(self.inputs)})"
def is_traceable(self):
return False
class DanglingTracker(Tracker):
"""
DanglingTracker is a subclass of Tracker that specifically tracks variables that are not in the frame.
Variables whose tracker is DanglingTracker should not be placed on the stack, except for NullVariable.
DanglingTracker is often used in conjunction with BuiltinVariable to reuse the dispatch mechanism.
Examples:
>>> import operator
>>> from sot.opcode_translator.executor.variables import (
... BuiltinVariable,
... ConstantVariable,
... )
>>> a = ConstantVariable.wrap_literal(1, None)
>>> b = ConstantVariable.wrap_literal(2, None)
>>> c = BuiltinVariable(operator.add, None, DanglingTracker())(a, b)
>>> c.value
3
"""
def __init__(self):
super().__init__([])
def gen_instructions(self, codegen: PyCodeGen):
raise InnerError("DanglingTracker has no instructions")
def trace_value_from_frame(self):
raise InnerError("DanglingTracker can't trace value from frame")
def is_traceable(self):
return False
def __repr__(self) -> str:
return "DanglingTracker()"
class LocalTracker(Tracker):
"""
LocalTracker is a subclass of Tracker that specifically tracks variables from f_locals of frame.
Args:
name (str): The name of the variable in f_locals to be tracked.
"""
def __init__(self, name: str):
super().__init__([])
self.name = name
def gen_instructions(self, codegen: PyCodeGen) -> None:
codegen.gen_load_fast(self.name)
def guard_tree_expr_node(self) -> paddle.framework.core.ExprNodeBase:
return paddle.framework.core.LocalVarExprNode(self.name)
def trace_value_from_frame(self) -> StringifiedExpression:
return StringifiedExpression(f"frame.f_locals['{self.name}']", [], {})
def __repr__(self) -> str:
return f"LocalTracker(name={self.name})"
class CellTracker(LocalTracker):
def gen_instructions(self, codegen: PyCodeGen):
codegen.gen_load_deref(self.name)
def guard_tree_expr_node(self) -> paddle.framework.core.ExprNodeBase:
return paddle.framework.core.LocalVarExprNode(self.name)
def trace_value_from_frame(self):
return StringifiedExpression(f"frame.f_locals['{self.name}']", [], {})
def __repr__(self) -> str:
return f"CellTracker(name={self.name})"
class GlobalTracker(Tracker):
"""
GlobalTracker is a subclass of Tracker that specifically tracks variables from f_globals of frame.
Args:
name (str): The name of the variable in f_globals to be tracked.
"""
def __init__(self, name: str):
super().__init__([])
self.name = name
def gen_instructions(self, codegen: PyCodeGen) -> None:
codegen.gen_load_global(self.name, push_null=False)
def guard_tree_expr_node(self) -> paddle.framework.core.ExprNodeBase:
return paddle.framework.core.GlobalVarExprNode(self.name)
def trace_value_from_frame(self) -> StringifiedExpression:
return StringifiedExpression(f"frame.f_globals['{self.name}']", [], {})
def __repr__(self) -> str:
return f"GlobalTracker(name={self.name})"
class BuiltinTracker(Tracker):
"""
BuiltinTracker is a subclass of Tracker that specifically tracks variables from f_builtins of frame.
Args:
name (str): The name of the variable in f_builtins to be tracked.
"""
def __init__(self, name: str):
super().__init__([])
self.name = name
def gen_instructions(self, codegen: PyCodeGen) -> None:
codegen.gen_load_global(self.name, push_null=False)
def guard_tree_expr_node(self) -> paddle.framework.core.ExprNodeBase:
return paddle.framework.core.ConstantExprNode(
getattr(builtins, self.name)
)
def trace_value_from_frame(self) -> StringifiedExpression:
return StringifiedExpression(
f"builtins.__dict__['{self.name}']", [], {"builtins": builtins}
)
def __repr__(self) -> str:
return f"BuiltinTracker(name={self.name})"
class ConstTracker(Tracker):
"""
ConstTracker is a subclass of Tracker that specifically tracks a constant value.
Args:
value (Any): The value of the constant.
"""
def __init__(self, value):
super().__init__([])
self.value = value
def gen_instructions(self, codegen: PyCodeGen):
codegen.gen_load_const(self.value)
def guard_tree_expr_node(self) -> paddle.framework.core.ExprNodeBase:
return paddle.framework.core.ConstantExprNode(self.value)
def trace_value_from_frame(self):
value_str, value_free_vars = stringify_pyobject(self.value)
return StringifiedExpression(
value_str, [], union_free_vars(value_free_vars)
)
def __repr__(self) -> str:
return f"ConstTracker(value={self.value})"
def need_guard(self) -> bool:
return False
class GetAttrTracker(Tracker):
"""
GetAttrTracker is a subclass of Tracker that specifically tracks the attribute access of an variable.
Args:
obj (VariableBase): The object whose attribute is to be tracked.
attr (str): The attribute to be tracked.
"""
def __init__(self, obj: VariableBase, attr: str, changed: bool = False):
super().__init__([obj], changed)
self.obj = obj
self.attr = attr
def gen_instructions(self, codegen: PyCodeGen):
self.obj.tracker.gen_instructions(codegen)
codegen.gen_load_attr(self.attr)
def guard_tree_expr_node(self) -> paddle.framework.core.ExprNodeBase:
obj_tracer = self.obj.tracker.guard_tree_expr_node()
return paddle.framework.core.AttributeExprNode(
obj_tracer,
self.attr,
)
def trace_value_from_frame(self):
obj_tracer = self.obj.tracker.trace_value_from_frame()
if self.attr.isidentifier():
expr = f"{{}}.{self.attr}"
else:
expr = f"getattr({{}}, '{self.attr}')"
return StringifiedExpression(
expr,
[obj_tracer],
union_free_vars(obj_tracer.free_vars),
)
def __repr__(self) -> str:
return f"GetAttrTracker(attr={self.attr})"
def need_guard(self) -> bool:
return self.is_traceable() and self.obj.tracker.need_guard()
class GetItemTracker(Tracker):
"""
GetItemTracker is a subclass of Tracker that specifically tracks item access of a container variable.
It generates instructions and traces the item value from the frame.
Args:
container_var (VariableBase): The container object whose item is to be tracked.
key: The key/index of the item to be tracked.
"""
def __init__(self, container_var: VariableBase, key: object, changed=False):
super().__init__([container_var], changed)
self.container = container_var
self.key = key
def gen_instructions(self, codegen: PyCodeGen):
self.container.tracker.gen_instructions(codegen)
if isinstance(self.key, slice):
codegen.gen_load_const(self.key.start)
codegen.gen_load_const(self.key.stop)
codegen.gen_load_const(self.key.step)
codegen.gen_build_slice(3)
else:
codegen.gen_load_const(self.key)
codegen.gen_subscribe()
def guard_tree_expr_node(self) -> paddle.framework.core.ExprNodeBase:
container_tracer = self.container.tracker.guard_tree_expr_node()
return paddle.framework.core.ItemExprNode(
container_tracer,
paddle.framework.core.ConstantExprNode(self.key),
)
def trace_value_from_frame(self):
container_tracer = self.container.tracker.trace_value_from_frame()
key_string, key_free_vars = stringify_pyobject(self.key)
return StringifiedExpression(
f"{{}}[{key_string}]",
[container_tracer],
union_free_vars(container_tracer.free_vars, key_free_vars),
)
def __repr__(self) -> str:
return f"GetItemTracker(key={self.key!r})"
def need_guard(self) -> bool:
return self.is_traceable() and self.container.tracker.need_guard()
class GetIterTracker(Tracker):
"""
GetIterTracker is a subclass of Tracker that specifically tracks iteration of a variable.
It generates instructions and traces the iterator from the frame.
Args:
iter_source (VariableBase): The source variable to be iterated.
"""
def __init__(self, iter_source: VariableBase):
super().__init__([iter_source])
self.iter_source = iter_source
def gen_instructions(self, codegen: PyCodeGen):
self.iter_source.tracker.gen_instructions(codegen)
codegen.add_instr("GET_ITER")
def guard_tree_expr_node(self) -> paddle.framework.core.ExprNodeBase:
# TODO(zrr1999): implement IterExprNode
raise NotImplementedError("IterExprNode is not implemented")
def trace_value_from_frame(self):
iter_source_tracer = self.iter_source.tracker.trace_value_from_frame()
return StringifiedExpression(
"iter({})",
[iter_source_tracer],
union_free_vars(iter_source_tracer.free_vars),
)
def __repr__(self) -> str:
return "GetIterTracker()"
class CreateLayerTracker(Tracker):
def __init__(self, layer_class, args, kwargs):
super().__init__([layer_class, *list(args), *list(kwargs.values())])
self.layer_class = layer_class
self.args = args
self.kwargs = kwargs
def gen_instructions(self, codegen: PyCodeGen):
if sys.version_info >= (3, 11):
codegen.gen_push_null()
self.layer_class.reconstruct(codegen)
for variable in self.args:
variable.reconstruct(codegen)
if len(self.kwargs) == 0:
codegen.gen_call_function(argc=len(self.args))
else:
codegen.gen_build_tuple(len(self.args))
for k, v in self.kwargs.items():
codegen.gen_load_const(k)
v.reconstruct(codegen)
codegen.gen_build_map(len(self.kwargs))
codegen.gen_call_function_ex(has_kwargs=True)
def guard_tree_expr_node(self) -> paddle.framework.core.ExprNodeBase:
# TODO(zrr1999): implement LayerExprNode.guard_tree_expr_node
raise NotImplementedError("LayerExprNode is not implemented")
def trace_value_from_frame(self):
class_tracer = self.layer_class.tracker.trace_value_from_frame()
arg_tracers = [
arg.tracker.trace_value_from_frame() for arg in self.args
]
kwarg_tracers_dict = {
k: v.tracker.trace_value_from_frame()
for k, v in self.kwargs.items()
}
kwarg_tracers = list(kwarg_tracers_dict.values())
expr = "{}("
expr += ", ".join(["{}"] * len(arg_tracers))
if len(arg_tracers) and len(kwarg_tracers) > 0:
expr += ", "
expr += ", ".join(f"{k}={{}}" for k in kwarg_tracers_dict.keys())
expr += ")"
return StringifiedExpression(
expr,
[class_tracer, *arg_tracers, *kwarg_tracers],
union_free_vars(
*(
tracer.free_vars
for tracer in chain(
[class_tracer], arg_tracers, kwarg_tracers
)
)
),
)
def __repr__(self) -> str:
return f"CreateLayerTracker(Layer={self.layer_class}, args={self.args}, kwargs={self.kwargs})"
class FunctionClosureTracker(Tracker):
"""
A tracker class that represents a function closure variable.
Args:
fn: The FunctionVariable object.
idx: The index of the closure variable.
"""
def __init__(self, fn: FunctionVariable, idx: int):
super().__init__([fn])
self.fn = fn
self.idx = idx
def gen_instructions(self, codegen: PyCodeGen):
"""
Generate bytecode instructions to trace the value of the function closure variable.
Args:
codegen: The PyCodeGen object used to generate bytecode.
"""
self.fn.tracker.gen_instructions(codegen)
codegen.gen_load_attr("__closure__")
codegen.gen_load_const(self.idx)
codegen.gen_subscribe()
codegen.gen_load_attr("cell_contents")
def guard_tree_expr_node(self) -> paddle.framework.core.ExprNodeBase:
fn_tracer = self.fn.tracker.guard_tree_expr_node()
return paddle.framework.core.AttributeExprNode(
paddle.framework.core.ItemExprNode(
paddle.framework.core.AttributeExprNode(
fn_tracer,
"__closure__",
),
paddle.framework.core.ConstantExprNode(self.idx),
),
"cell_contents",
)
def trace_value_from_frame(self):
"""
Trace the value of the function closure variable from the frame.
Returns:
The traced value of the function closure variable.
"""
fn_tracer = self.fn.tracker.trace_value_from_frame()
return StringifiedExpression(
f"{{}}.__closure__[{self.idx}].cell_contents",
[fn_tracer],
union_free_vars(fn_tracer.free_vars),
)
def __repr__(self) -> str:
return f"FunctionClosureTracker(fn={self.fn}, idx={self.idx})"
class FunctionGlobalTracker(Tracker):
"""
A tracker class that represents a function global variable.
Args:
fn: FunctionVariable object.
name: The name of the global variable.
"""
def __init__(self, fn: FunctionVariable, name: str):
super().__init__([fn])
self.fn = fn
self.name = name
def gen_instructions(self, codegen: PyCodeGen):
"""
Generate bytecode instructions in order to put the variables at the top of the stack.
Args:
codegen: The PyCodeGen object used to generate bytecode.
"""
self.fn.tracker.gen_instructions(codegen)
codegen.gen_load_attr("__globals__")
codegen.gen_load_const(self.name)
codegen.gen_subscribe()
def guard_tree_expr_node(self) -> paddle.framework.core.ExprNodeBase:
fn_tracer = self.fn.tracker.guard_tree_expr_node()
return paddle.framework.core.ItemExprNode(
paddle.framework.core.AttributeExprNode(
fn_tracer,
"__globals__",
),
paddle.framework.core.ConstantExprNode(self.name),
)
def trace_value_from_frame(self) -> StringifiedExpression:
"""
Trace the value of the function global variable from the frame.
Returns:
StringifiedExpression: The traced value of the function global variable.
"""
fn_tracer = self.fn.tracker.trace_value_from_frame()
return StringifiedExpression(
f"{{}}.__globals__['{self.name}']",
[fn_tracer],
union_free_vars(fn_tracer.free_vars),
)
def __repr__(self) -> str:
return f"FunctionGlobalTracker(fn={self.fn}, name={self.name})"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,217 @@
# Copyright (c) 2023 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
from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload
if TYPE_CHECKING:
from collections.abc import Callable
ValidateValueFunc = Callable[[Any], None]
StackDataT = TypeVar("StackDataT")
class VariableStack(Generic[StackDataT]):
"""
A stack class for storing variables.
Examples:
>>> var1, var2, var3, var4 = range(1, 5)
>>> stack = VariableStack()
>>> stack.push(var1)
>>> stack.push(var3)
>>> stack.insert(1, var2)
>>> stack
[1, 2, 3]
>>> stack.pop()
3
>>> stack.pop_n(2)
[1, 2]
>>> stack.push(var1)
>>> stack.push(var2)
>>> stack.push(var3)
>>> stack
[1, 2, 3]
>>> stack.top
3
>>> stack.peek[1]
3
>>> stack.peek[:1]
[3]
>>> stack.peek[:2]
[2, 3]
>>> stack.peek[1] = var4
>>> stack
[1, 2, 4]
"""
class VariablePeeker:
@overload
def __getitem__(self, index: int) -> StackDataT: ...
@overload
def __getitem__(self, index: slice) -> list[StackDataT]: ...
@overload
def __call__(self, index: int = 1) -> StackDataT: ...
@overload
def __call__(self, index: slice) -> list[StackDataT]: ...
def __init__(
self, data: list[StackDataT], validate_value_func: ValidateValueFunc
):
self._data = data
self.validate_value_func = validate_value_func
def __getitem__(
self, index: int | slice
) -> StackDataT | list[StackDataT]:
if isinstance(index, int):
assert 0 < index <= len(self._data)
return self._data[-index]
if isinstance(index, slice):
assert index.start is None and index.step is None, (
"slice which has start or step not supported"
)
assert 0 < index.stop <= len(self._data)
return self._data[-index.stop :]
raise NotImplementedError(f"index type {type(index)} not supported")
def __setitem__(self, index: int, value: Any):
assert isinstance(index, int), (
f"index type {type(index)} not supported"
)
assert 0 < index <= len(self._data), (
f"index should be in [1, {len(self._data)}], but get {index}"
)
self.validate_value_func(value)
self._data[-index] = value
def __call__(
self, index: int | slice = 1
) -> StackDataT | list[StackDataT]:
return self[index]
def __init__(
self,
data: list[StackDataT] | None = None,
*,
validate_value_func: ValidateValueFunc | None = None,
):
if data is None:
data = []
else:
data = data.copy()
self.validate_value_func = (
(lambda _: None)
if validate_value_func is None
else validate_value_func
)
self._data = data
self._peeker = VariableStack.VariablePeeker(
self._data, self.validate_value_func
)
def copy(self):
return VariableStack(
self._data, validate_value_func=self.validate_value_func
)
def push(self, val: StackDataT):
"""
Pushes a variable onto the stack.
Args:
val: The variable to be pushed.
"""
self.validate_value_func(val)
self._data.append(val)
def insert(self, index: int, val: StackDataT):
"""
Inserts a variable onto the stack.
Args:
index: The index at which the variable is to be inserted, the top of the stack is at index 0.
val: The variable to be inserted.
"""
assert 0 <= index <= len(self), (
f"index should be in [0, {len(self)}], but get {index}"
)
self.validate_value_func(val)
self._data.insert(len(self) - index, val)
def pop(self) -> StackDataT:
"""
Pops the top value from the stack.
Returns:
The popped value.
"""
assert len(self) > 0, "stack is empty"
return self._data.pop()
def pop_n(self, n: int) -> list[StackDataT]:
"""
Pops the top n values from the stack.
Args:
n: The number of values to pop.
Returns:
A list of the popped values.
"""
assert len(self) >= n >= 0, (
f"n should be in [0, {len(self)}], but get {n}"
)
if n == 0:
return []
retval = self._data[-n:]
self._data[-n:] = []
return retval
@property
def peek(self) -> VariablePeeker:
return self._peeker
@property
def top(self) -> StackDataT:
assert len(self) > 0, "stack is empty"
return self.peek[1]
@top.setter
def top(self, value):
assert len(self) > 0, "stack is empty"
self.peek[1] = value
def __contains__(self, value):
return value in self._data
def __iter__(self):
return iter(self._data)
def __len__(self) -> int:
return len(self._data)
def __repr__(self) -> str:
return str(self._data)
@@ -0,0 +1,80 @@
# Copyright (c) 2023 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 .base import ( # noqa: F401
VariableBase,
VariableFactory,
find_traceable_vars,
map_variables,
)
from .basic import ( # noqa: F401
CellVariable,
ConstantVariable,
DataClassInstanceVariable,
DataVariable,
DygraphTracerVariable,
EnumVariable,
ExceptionVariable,
FunctionGlobalVariable,
GlobalVariable,
InterpolationVariable,
ModuleVariable,
NullVariable,
NumPyArrayVariable,
NumPyNumberVariable,
NumPyVariable,
ObjectVariable,
ParameterVariable,
PlaceVariable,
SliceVariable,
SuperVariable,
SymbolicVariable,
TemplateVariable,
TensorVariable,
)
from .callable import ( # noqa: F401
BuiltinVariable,
CallableVariable,
ClassVariable,
ContainerLayerVariable,
DataClassVariable,
FunctionVariable,
LayerVariable,
MethodVariable,
NumPyApiVariable,
PaddleApiVariable,
PaddleLayerVariable,
PartialVariable,
UserCodeVariable,
UserDefinedFunctionVariable,
UserDefinedGeneratorFunctionVariable,
UserDefinedLayerVariable,
)
from .container import ( # noqa: F401
ContainerVariable,
DictVariable,
ListVariable,
RangeVariable,
SizeVariable,
TupleVariable,
)
from .iter import ( # noqa: F401
EnumerateVariable,
GeneratorVariable,
IterVariable,
MapVariable,
SequenceIterVariable,
UserDefinedIterVariable,
ZipVariable,
)
@@ -0,0 +1,731 @@
# Copyright (c) 2023 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 inspect
import operator
from contextlib import contextmanager
from dataclasses import fields
from functools import cached_property
from queue import Queue
from typing import TYPE_CHECKING, Any
import paddle
from paddle.jit.dy2static.utils import (
dataclass_from_dict,
)
from ....profiler import event_register
from ....utils import (
NameGenerator,
get_unbound_method,
log,
)
from ....utils.exceptions import FallbackError, HasNoAttributeError
from ..dispatcher import Dispatcher
from ..guard import (
FasterStringifiedExpression,
StringifiedExpression,
check_faster_guard,
check_guard,
union_free_vars,
)
from ..mutable_data import MutableDictLikeData
from ..tracker import (
BuiltinTracker,
ConstTracker,
DummyTracker,
GetAttrTracker,
GetItemTracker,
GetIterTracker,
GlobalTracker,
LocalTracker,
Tracker,
)
if TYPE_CHECKING:
from collections.abc import Callable
from typing import TypeAlias
from ..function_graph import FunctionGraph
from ..pycode_generator import PyCodeGen
# Each variable object should implement a method called `from_value`,
# which should adhere to the FromValueFunc signature.
FromValueFunc: TypeAlias = Callable[
[Any, FunctionGraph, Tracker], "VariableBase | None"
]
@event_register("find_traceable_vars")
def find_traceable_vars(
root_vars: list[VariableBase],
) -> list[VariableBase]:
"""
This function is used to find all traceable variables in the given list of variables.
Args:
root_vars (list[VariableBase]): A list of root variables from which the ordering starts.
Returns:
list[VariableBase]: A list of variables that are traceable.
"""
results: list[VariableBase] = []
visited: set[VariableBase] = set()
queue: Queue[VariableBase] = Queue()
for root in root_vars:
queue.put(root)
while not queue.empty():
var = queue.get()
if var in visited:
continue
visited.add(var)
if var.tracker.need_guard():
results.append(var)
continue
# Pruning traceable variable, if the variable is traceable, we don't need to
# trace its inputs.
inputs = var.get_inputs()
for var in inputs:
if var not in visited and var not in queue.queue:
queue.put(var)
return results
def map_variables(
map_func,
variables: list[VariableBase],
*,
restore_variable=False,
) -> list[VariableBase]:
"""
This function maps the given map_func to the given list of variables in a recursive manner.
Args:
map_func (Callable[[VariableBase], Any]): The function to be mapped to each variable.
variables (list[VariableBase]): A list of variables to which the map_func is to be applied.
Returns:
tuple: The result of applying the map_func to the variables.
"""
from .basic import DataClassInstanceVariable, SliceVariable
from .container import ContainerVariable
def _map_container_variable(variable: VariableBase | object):
if not isinstance(variable, ContainerVariable):
return variable
new_container = paddle.utils.map_structure(
_map_variable, variable.get_wrapped_items()
)
if not restore_variable:
return new_container
return VariableFactory.from_value(
new_container,
variable.graph,
DummyTracker(paddle.utils.flatten(new_container)),
)
def _map_slice_variable(variable: VariableBase | object):
if not isinstance(variable, SliceVariable):
return variable
new_slice = slice(
map_func(variable.getattr("start")),
map_func(variable.getattr("stop")),
map_func(variable.getattr("step")),
)
if not restore_variable:
return new_slice
return VariableFactory.from_value(
new_slice,
variable.graph,
DummyTracker([new_slice.start, new_slice.stop, new_slice.step]),
)
def _map_dataclass_variable(variable: VariableBase | object):
if not isinstance(variable, DataClassInstanceVariable):
return variable
new_dataclass = dataclass_from_dict(
variable.get_py_type(),
{
fd.name: _map_variable(variable.getattr(fd.name))
for fd in fields(variable.get_py_type())
},
)
if not restore_variable:
return new_dataclass
return VariableFactory.from_value(
new_dataclass,
variable.graph,
DummyTracker(
[
variable.getattr(fd.name)
for fd in fields(variable.get_py_type())
]
),
)
def _map_variable(variable: VariableBase | object):
variable = _map_container_variable(variable)
variable = _map_slice_variable(variable)
variable = _map_dataclass_variable(variable)
return map_func(variable)
return paddle.utils.map_structure(_map_variable, variables)
class VariableFactory:
"""
A factory class for creating variables from arbitrary values.
This class provides a set of registration and factory methods for creating variables
of different types based on the type of the input value.
"""
registered_funcs: dict[str, list[str]] = {"default": []}
mapping_str_func: dict[str, FromValueFunc] = {}
@staticmethod
def default_from_value(value, graph, tracker):
"""
A default factory function that creates an ObjectVariable from the given value.
Args:
value: The input value.
graph: The FunctionGraph object that this variable is associated with.
tracker: The Tracker object that tracks the information of this variable.
Returns:
ObjectVariable: A new ObjectVariable representing the input value.
"""
from .basic import ObjectVariable
return ObjectVariable(value, graph, tracker)
@staticmethod
def register_from_value(*, successor: str | None = None):
"""
A decorator function that registers a function for creating a Variable from a value.
Args:
successor (str | None, optional): The name of the successor function that will be called after this function when creating a Variable. If None, the function is added to a default list of functions.
Returns:
The _register_from_value decorator function, which takes the function to be registered as an argument.
"""
registered_funcs = VariableFactory.registered_funcs
mapping_str_func = VariableFactory.mapping_str_func
def _register_from_value(func: FromValueFunc):
"""
Function to register a function for creating a Variable from a value
"""
# Get the name of the function
name = func.__qualname__.split(".")[0]
# Map the name of the function to the function
mapping_str_func[name] = func
if successor is None:
registered_funcs["default"].append(
name
) # If successor is None, add the function to the "default" list
elif successor not in registered_funcs.keys():
registered_funcs[successor] = [
name
] # If the successor is not in the registered_funcs dictionary, set the value to a list containing only name
else:
registered_funcs[successor].append(
name
) # If the successor is in the registered_funcs dictionary, append name to the existing list of functions for that successor
log(
4, VariableFactory.registered_funcs
) # Print the registered_funcs dictionary if the logging level is at least 4
return _register_from_value
@staticmethod
def from_value(
value: Any,
graph: FunctionGraph,
tracker: Tracker,
) -> VariableBase:
"""
Create a new variable object from the given value.
This method searches through the registered from_value functions to find one
that can create a variable object from the given value. If no matching function
is found, the default_from_value function is used.
Args:
value (Any): The input value.
graph (FunctionGraph): The FunctionGraph object that this variable is associated with.
tracker (Tracker): The Tracker object that tracks the information of this variable.
Returns:
VariableBase: A new variable object representing the input value.
"""
registered_funcs = VariableFactory.registered_funcs
def _find_var(key: str = "default") -> VariableBase | None:
for name in registered_funcs[key]:
if name in registered_funcs.keys():
# If the function name is a key in the registered_funcs dictionary, recursively find a Variable using that function
var = _find_var(name)
if var is not None:
return var
# Get the function corresponding to the name from the mapping_str_func dictionary
func = VariableFactory.mapping_str_func[name]
var = func(
value, graph, tracker
) # Call the function to create a Variable from the value
if var is not None:
return var
var = _find_var()
if var is None:
var = VariableFactory.default_from_value(
value, graph, tracker
) # If a Variable could not be found using the registered functions, use the default function to create a new Variable
return var
def infer_debug_name_from_tracker(tracker: Tracker) -> str | None:
res = None
if isinstance(tracker, (LocalTracker, GlobalTracker, BuiltinTracker)):
res = f"{tracker.name}"
elif isinstance(tracker, ConstTracker):
res = f"{tracker.value}"
elif isinstance(tracker, GetItemTracker) and tracker.container.debug_name:
res = f"{tracker.container.debug_name}[{tracker.key}]"
elif isinstance(tracker, GetAttrTracker) and tracker.obj.debug_name:
res = f"{tracker.obj.debug_name}.{tracker.attr}"
return res
class VariableBase:
"""
VariableBase is a basic concept and each symbols in VM stack is regarded as
an Variable Object in symbolic tracing process.
There are two key data structures during Python runtime:
PyFrameObject, which provides the instance for function logical lock usage,
and PyCodeObject, which provides the bytecode for the corresponding function.
With these data, the Python virtual machine executes the bytecode sequentially on a stack to complete function logic.
Args:
tracker(Tracker): The Tracker object that tracks the information of this variable.
Note:
We should push an object of a subclass of VariableBase instead of an object of VariableBase onto the VM stack.
It serves as an abstract class and should not be instantiated directly.
"""
tracker: Tracker # An attribute to store the Tracker object associated with the variable
value: Any
name_generator = NameGenerator(
"object_"
) # A class-level attribute to generate names for new variables
mutable_attrs = []
def __init__(self, graph: FunctionGraph, tracker: Tracker):
self.graph = graph
self.tracker = tracker
self.id = VariableBase.name_generator.next()
self.debug_name = infer_debug_name_from_tracker(tracker)
@property
def main_info(self) -> dict[str, Any]:
"""
Property method to return a dictionary of main information about the variable
Returns:
main_info: Main information of the variable.
"""
return {}
@property
def debug_info(self) -> dict[str, Any]:
"""
Property method to return a dictionary of debug information about the variable
"""
info = {
"id": self.id,
}
if self.debug_name:
info["debug_name"] = self.debug_name
return info
def __hash__(self):
return hash(self.id)
@check_faster_guard
def make_faster_guard(self) -> list[paddle.framework.core.GuardNodeBase]:
expr_node = self.tracker.guard_tree_expr_node()
return [
paddle.framework.core.GuardNode(
paddle.framework.core.ValueMatchGuard(self.get_py_value()),
[expr_node],
)
]
@check_guard
def make_stringified_guard(self) -> list[StringifiedExpression]:
"""
Create a StringifiedExpression object that represents a guard expression for this variable.
Returns:
StringifiedExpression: An object that contains the guard expression and the free variables used in the expression.
"""
# Get a ValueTracer object from the Tracker object associated with the variable
frame_value_tracer = self.tracker.trace_value_from_frame()
return [
FasterStringifiedExpression(
f"id(type({{0}})) == {id(self.get_py_type())} and {{0}} == {self.get_py_value()!r}",
paddle.framework.core.ValueMatchGuard(self.get_py_value()),
[frame_value_tracer],
union_free_vars(frame_value_tracer.free_vars),
)
]
def get_py_value(self, allow_tensor=False) -> Any:
"""
Abstract method to get the value of the variable
"""
raise NotImplementedError
def get_py_type(self):
"""
Method to get the type of the variable's value
"""
return type(self.get_py_value())
def is_none(self) -> bool:
"""
Method to check if the variable's value is None
"""
return self.get_py_value() is None
def reconstruct(
self,
codegen: PyCodeGen,
*,
use_tracker: bool = True,
add_to_global_guarded_vars: bool = True,
):
if self.tracker.is_traceable() and use_tracker:
self.tracker.gen_instructions(codegen)
else:
if add_to_global_guarded_vars:
self.graph.add_global_guarded_variable(self)
self._reconstruct(codegen)
def _reconstruct(self, codegen: PyCodeGen) -> None:
"""
Abstract method to construct an opcode and append it into codegen.instructions
"""
raise FallbackError(
f'{self.__class__.__name__} does not implement "_reconstruct" method'
)
def flatten_inner_vars(self) -> list[VariableBase]:
"""
Recursively flatten the items in this container variable to a list of Variable objects.
Returns:
list[VariableBase]: Flattened items of a container variable.
"""
return [self]
def get_inputs(self) -> list[VariableBase]:
"""
This method is used to get the inputs for the current variable.
Returns:
list[VariableBase]: Inputs for the current variable.
"""
return self.tracker.inputs
def get_traceable_inputs(self) -> list[VariableBase]:
"""
This method is used to get the traceable inputs for the current variable.
Returns:
list[VariableBase]: Traceable inputs for the current variable.
"""
return list(
filter(lambda x: x.tracker.is_traceable(), self.tracker.inputs)
)
def call_function(self, /, *args, **kwargs):
pass
@cached_property
def attr_proxy(self):
return self.graph.side_effects.get_proxy(
MutableDictLikeData, self.get_py_value(), self.attr_proxy_getter
)
def attr_proxy_getter(self, proxy: MutableDictLikeData, name: str):
if not hasattr(proxy.original_data, name): # can't true.
return MutableDictLikeData.Empty()
attr = getattr(proxy.original_data, name)
if inspect.ismethod(attr) or (
hasattr(attr, "__self__")
and inspect.ismethoddescriptor(
getattr(attr.__self__.__class__, name, None)
)
):
from .callable import MethodVariable
fn = None
instance = self
if inspect.ismethoddescriptor(
getattr(attr.__self__.__class__, name, None)
):
class_var = VariableFactory.from_value(
self.get_py_type(),
self.graph,
GetAttrTracker(self, "__class__"),
)
fn = VariableFactory.from_value(
getattr(attr.__self__.__class__, name),
self.graph,
GetAttrTracker(class_var, name),
)
if not hasattr(self.get_py_type(), name):
instance = None
return MethodVariable.wrap_method(
value=attr,
instance=instance,
fn=fn,
graph=self.graph,
tracker=GetAttrTracker(self, name),
)
return VariableFactory.from_value(
attr, self.graph, tracker=GetAttrTracker(self, name)
)
def hasattr(self, name: str):
from .basic import ConstantVariable
try:
self.getattr(name)
return ConstantVariable(
True, graph=self.graph, tracker=DummyTracker([self])
)
except HasNoAttributeError:
# NOTE(SigureMo): Only the HasNoAttributeError is raised, we can
# ensure that the attribute does not exist. Otherwise, we should
# raise the error.
return ConstantVariable(
False, graph=self.graph, tracker=DummyTracker([self])
)
def getattr(self, name: str, default=None):
result = self.attr_proxy.get(name)
if isinstance(result, MutableDictLikeData.Empty):
if default is not None:
assert isinstance(default, VariableBase)
return default
raise HasNoAttributeError(
f"{self.__class__.__name__} {self} has no attribute {name}"
)
return result
def setattr(self, key: str, value):
from .basic import ConstantVariable
self.attr_proxy.set(key, value)
self.graph.side_effects.record_proxy_variable(self)
return ConstantVariable.wrap_literal(None, self.graph)
def delattr(self, key: str):
from .basic import ConstantVariable
self.attr_proxy.delete(key)
self.graph.side_effects.record_proxy_variable(self)
return ConstantVariable.wrap_literal(None, self.graph)
def __setitem__(self, key, value):
return self.setitem(key, value)
def setitem(self, key, value):
raise FallbackError(f"{self} is not support setitem.")
def __repr__(self):
info = self.main_info | self.debug_info
info_str = ", ".join([f"{value}" for value in info.values()])
return f"{self.__class__.__name__}({info_str})"
def __str__(self):
return self.__repr__()
def __getitem__(self, idx):
return Dispatcher.call(operator.getitem, self, idx)
def getitem(self, item):
class_var = VariableFactory.from_value(
self.get_py_value().__class__,
self.graph,
GetAttrTracker(self, '__class__'),
)
fn_var = VariableFactory.from_value(
get_unbound_method(self.get_py_value(), '__getitem__'),
self.graph,
GetAttrTracker(class_var, '__getitem__'),
)
self.graph.add_global_guarded_variable(item)
item = item.get_py_value()
output = fn_var(self, item)
return output
def __call__(self, /, *args, **kwargs):
"""
Call the object represented by this variable with the given arguments.
Args:
*args: Positional arguments to pass to the object's __call__ method.
**kwargs: Keyword arguments to pass to the object's __call__ method.
Returns:
VariableBase: A new variable representing the result of calling the object's __call__ method.
"""
from .callable import BuiltinVariable, UserDefinedFunctionVariable
class_var = VariableFactory.from_value(
self.get_py_value().__class__,
self.graph,
GetAttrTracker(self, '__class__'),
)
assert class_var is not None
# if __call__ is a method, we should add self to arguments.
if inspect.ismethod(self.get_py_value().__call__):
args = (self, *args)
unbound_method = get_unbound_method(self.get_py_value(), '__call__')
if hasattr(unbound_method, "__code__"):
fn_var = UserDefinedFunctionVariable(
unbound_method,
self.graph,
GetAttrTracker(class_var, '__call__'),
)
else:
fn_var = BuiltinVariable(
self.value,
self.graph,
GetAttrTracker(class_var, '__call__'),
)
output = fn_var(*args, **kwargs)
return output
def get_iter(self):
from . import (
BuiltinVariable,
ConstantVariable,
SequenceIterVariable,
UserDefinedFunctionVariable,
UserDefinedIterVariable,
)
if not hasattr(self.value, "__iter__"):
return UserDefinedIterVariable(
self, self.graph, GetIterTracker(self)
)
iter_name_var = ConstantVariable.wrap_literal("__iter__", self.graph)
iter_method = BuiltinVariable(
getattr, graph=self.graph, tracker=DummyTracker([self])
)(self, iter_name_var)
# If the target object is a builtin object like list_iterator, the iter_method's fn will be a ObjectVariable instead of UserDefinedFunctionVariable.
if not isinstance(iter_method.fn, UserDefinedFunctionVariable):
return UserDefinedIterVariable(
self, self.graph, GetIterTracker(self)
)
iter_result = iter_method()
if not isinstance(iter_result, SequenceIterVariable):
return UserDefinedIterVariable(
self, self.graph, GetIterTracker(self)
)
return iter_result
@VariableFactory.register_from_value()
def from_value(
value: Any,
graph: FunctionGraph | None,
tracker: Tracker,
) -> VariableBase | None:
"""
Create a new variable from a given value, or return None if the value cannot be converted to a variable.
Args:
value (Any): The value to create a variable from.
graph (FunctionGraph | None): The graph in which the variable will be used.
tracker (Tracker): The variable tracker to put the new variable in if created.
Returns:
VariableBase | None: A new variable if one can be created from the given value, or None if the value cannot be converted to a variable.
"""
if isinstance(value, VariableBase):
return value
return None
@contextmanager
def signature_clear_guard(fn, name):
if not hasattr(fn, name):
yield
else:
saved_attr = getattr(fn, name)
delattr(fn, name)
yield
setattr(fn, name, saved_attr)
def fn_bind_inputs(
fn: Callable[..., Any],
graph: FunctionGraph,
*args: Any,
**kwargs: Any,
):
# temparay clear the fn.__signature__ to avoid signature check error
with (
signature_clear_guard(fn, "__signature__"),
signature_clear_guard(fn, "__wrapped__"),
):
sig = inspect.signature(fn)
bound_args = sig.bind(*args, **kwargs)
bound_args.apply_defaults()
parameters = {}
for name, value in bound_args.arguments.items():
assert name in sig.parameters
# Convert varargs and kwargs to Variable
if sig.parameters[name].kind == inspect.Parameter.VAR_POSITIONAL:
tracker = DummyTracker(value)
elif sig.parameters[name].kind == inspect.Parameter.VAR_KEYWORD:
tracker = DummyTracker(list(value.values()))
# Convert default args to Variable
elif not isinstance(value, VariableBase):
tracker = ConstTracker(value)
else:
tracker = value.tracker
value = VariableFactory.from_value(value, graph, tracker)
parameters[name] = value
return parameters
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,460 @@
# Copyright (c) 2023 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 types
from typing import TYPE_CHECKING, Any
from paddle._typing import unreached
from ....profiler import EventGuard
from ....utils import do_until_stop_iteration
from ....utils.exceptions import (
BreakGraphError,
BreakGraphInlineCallBreak,
FallbackError,
FallbackInlineCallBreak,
OtherInlineCallBreak,
SotCapturedExceptionFactory,
SotCapturedStopIteration,
SotErrorBase,
UnsupportedOperationBreak,
)
from ..guard import check_faster_guard
from ..tracker import ConstTracker, DanglingTracker, DummyTracker
from .base import (
VariableBase,
VariableFactory,
)
from .basic import ConstantVariable
from .callable import BuiltinVariable
from .container import TupleVariable
if TYPE_CHECKING:
from collections.abc import Sequence
import paddle
from ..function_graph import FunctionGraph
from ..pycode_generator import PyCodeGen
from ..tracker import Tracker
from ..virtual_frame import VirtualFrame
class IterVariable(VariableBase):
"""
This Variable (include subclasses) should be generated only when simulate GET_ITER opcode
"""
def __init__(self, graph: FunctionGraph, tracker: Tracker):
super().__init__(graph, tracker)
def next(self):
raise NotImplementedError(f"Can not simulate `next` for {type(self)}")
def to_list(self):
raise NotImplementedError(
f"Can not simulate `to_list` for {type(self)}"
)
def send(self, value: VariableBase):
return self.next()
def get_iter(self):
return self
class SequenceIterVariable(IterVariable):
"""
The basic SequenceIterVariable wraps iterators which can be simulated by call getitem
Currently includes: List | Tuple | Dict (keys) | Range | Tensor | nn.LayerList
these interfaces is needed:
- next
- to_list
- has_side_effect
- _reconstruct
"""
mutable_attrs = ["idx"]
def __init__(
self,
held: VariableBase | list[VariableBase],
graph: FunctionGraph,
tracker: Tracker,
):
if not isinstance(held, list):
held = [held]
super().__init__(graph, tracker)
self.holds = held
self.idx = 0
self.graph.side_effects.record_mutable_variable(self)
@check_faster_guard
def make_faster_guard(self) -> list[paddle.framework.core.GuardNodeBase]:
return [
guard for held in self.holds for guard in held.make_faster_guard()
]
def make_stringified_guard(self):
return [
guard
for held in self.holds
for guard in held.make_stringified_guard()
]
def next(self):
held = self.holds[0]
if self.idx < len(held):
val = held[self.idx]
self.idx += 1
return val
else:
raise SotCapturedExceptionFactory.create(StopIteration())
def to_list(self) -> list:
if self.has_side_effect():
raise FallbackError("Can not convert an used iterator into list")
held = self.holds[0]
self.idx = len(held)
retval = []
for i in range(len(held)):
retval.append(held[i])
return retval
def has_side_effect(self) -> bool:
return self.idx != 0
def _reconstruct(self, codegen: PyCodeGen):
if self.has_side_effect():
super()._reconstruct(codegen)
else:
self.holds[0].reconstruct(codegen)
codegen.gen_get_iter()
@property
def main_info(self) -> dict[str, Any]:
return {
"idx": self.idx,
}
def flatten_inner_vars(self) -> list[VariableBase]:
held = self.holds
return [
inner_var for obj in held for inner_var in obj.flatten_inner_vars()
]
class EnumerateVariable(SequenceIterVariable):
"""
EnumerateVariable holds a SequenceIterVariable and return additional index
"""
def __init__(
self, val_iterator: IterVariable, graph: FunctionGraph, tracker: Tracker
):
super().__init__(val_iterator, graph, tracker)
def next(self):
val = self.holds[0].next()
idx_var = ConstantVariable(self.idx, self.graph, ConstTracker(self.idx))
self.idx += 1
return TupleVariable(
(idx_var, val), self.graph, DummyTracker([idx_var, val])
)
def to_list(self):
values = self.holds[0].to_list()
idx = [
ConstantVariable(i, self.graph, ConstTracker(i))
for i in range(len(values))
]
return list(zip(idx, values))
def has_side_effect(self) -> bool:
return self.holds[0].has_side_effect()
def _reconstruct(self, codegen: PyCodeGen):
if self.has_side_effect():
super()._reconstruct(codegen)
else:
codegen.gen_load_global("enumerate", push_null=True)
self.holds[0].reconstruct(codegen)
codegen.gen_call_function(1)
@staticmethod
def from_iterator(value, graph: FunctionGraph | None, tracker: Tracker):
iter_variable = value.get_iter()
if isinstance(iter_variable, UserDefinedIterVariable):
return UserDefinedIterVariable(value, graph, tracker)
else:
return EnumerateVariable(iter_variable, graph, tracker)
class ZipVariable(SequenceIterVariable):
"""
ZipVariable holds a list of SequenceIterVariable
"""
def __init__(
self, iters: list[IterVariable], graph: FunctionGraph, tracker: Tracker
):
super().__init__(iters, graph, tracker)
def next(self):
# can not use <listcomp> here, because it will raise a RuntimeError("StopIteration")
# but we want a StopIteration Exception
values = []
for iter_var in self.holds:
next_var = iter_var.next()
values.append(next_var)
return VariableFactory.from_value(
tuple(values), self.graph, DummyTracker(values)
)
def to_list(self):
lists = [iter_vars.to_list() for iter_vars in self.holds]
min_len = min(len(l) for l in lists)
result = []
for i in range(min_len):
result.append(
VariableFactory.from_value(
tuple(l[i] for l in lists),
self.graph,
DummyTracker(list(self.holds)),
)
)
return result
def has_side_effect(self) -> bool:
return any(iter_var.has_side_effect() for iter_var in self.holds)
def _reconstruct(self, codegen: PyCodeGen):
if self.has_side_effect():
super()._reconstruct(codegen)
else:
codegen.gen_load_global("zip", push_null=True)
for iter_var in self.holds:
iter_var.reconstruct(codegen)
codegen.gen_call_function(len(self.holds))
@staticmethod
def from_iterator(
value: Sequence[VariableBase],
graph: FunctionGraph | None,
tracker: Tracker,
):
assert isinstance(value, (list, tuple))
zip_targets = []
for variable in value:
iter_variable = variable.get_iter()
if isinstance(iter_variable, UserDefinedIterVariable):
return UserDefinedIterVariable(value, graph, tracker)
zip_targets.append(iter_variable)
return ZipVariable(zip_targets, graph, tracker)
class MapVariable(SequenceIterVariable):
"""
MapVariable holds a SequenceIterVariable and return a Iterable Variable after map function
"""
def __init__(self, fn, iters: list[IterVariable], graph, tracker):
super().__init__(iters, graph, tracker)
self.fn = fn
def next(self):
return self.fn(*[iter_var.next() for iter_var in self.holds])
def to_list(self) -> list:
lists = [iter_var.to_list() for iter_var in self.holds]
min_len = min(len(l) for l in lists)
result = []
for i in range(min_len):
result.append(self.fn(*(l[i] for l in lists)))
return result
def has_side_effect(self) -> bool:
return any(iter_var.has_side_effect() for iter_var in self.holds)
def _reconstruct(self, codegen: PyCodeGen):
if self.has_side_effect():
super()._reconstruct(codegen)
else:
codegen.gen_load_global("map", push_null=True)
self.fn.reconstruct(codegen)
for iter_var in self.holds:
iter_var.reconstruct(codegen)
codegen.gen_call_function(len(self.holds) + 1)
@staticmethod
def from_iterator(
fn,
value: Sequence[VariableBase],
graph: FunctionGraph | None,
tracker: Tracker,
):
map_targets = []
for variable in value:
iter_variable = variable.get_iter()
if isinstance(iter_variable, UserDefinedIterVariable):
return UserDefinedIterVariable(value, graph, tracker)
map_targets.append(iter_variable)
return MapVariable(fn, map_targets, graph, tracker)
class GeneratorVariable(IterVariable):
def __init__(
self,
code_var: VariableBase,
vframe: VirtualFrame,
graph: FunctionGraph,
tracker: Tracker,
):
self.code_var = code_var
self.vframe = vframe
self.shared_stack = []
super().__init__(graph, tracker)
def send(self, /, value: VariableBase):
from ..opcode_inline_executor import OpcodeInlineGeneratorExecutor
checkpoint = self.graph.save_memo()
frame_state = self.vframe.get_state()
try:
inline_gen_executor = OpcodeInlineGeneratorExecutor(
self.vframe, self.code_var, self.graph
)
self.vframe.stack.push(value)
with EventGuard(
f"Inline Gen Call: {inline_gen_executor.vframe.code.co_name}, file {inline_gen_executor.vframe.code.co_filename}, line {int(inline_gen_executor.vframe.code.co_firstlineno)}"
):
output: VariableBase = inline_gen_executor.inline_call()
if inline_gen_executor.stop_state == "Return":
raise SotCapturedExceptionFactory.create(StopIteration())
except SotCapturedStopIteration:
raise
except SotErrorBase as error:
self.graph.restore_memo(checkpoint)
self.vframe.restore_state(frame_state)
filename = self.code_var.value.co_filename
lineno = self.code_var.value.co_firstlineno
code_name = self.code_var.value.co_name
location_info = f'File "{filename}", line {lineno}, in {code_name}'
exception_class = OtherInlineCallBreak
if isinstance(error, BreakGraphError):
exception_class = BreakGraphInlineCallBreak
elif isinstance(error, FallbackError):
exception_class = FallbackInlineCallBreak
raise BreakGraphError(
exception_class(
f"{location_info} encountered breakgraph error caused by\n {error}"
)
)
return output
def getattr(self, name: str, default=None):
from ..dispatch_functions import generator_send
known_generator_attrs = {"send"}
if name not in known_generator_attrs:
raise BreakGraphError(
UnsupportedOperationBreak(
reason_str=f"Get attribute {name} from generator is not supported."
)
)
if name == "send":
return BuiltinVariable(
generator_send, self.graph, DanglingTracker()
).bind_dangling_fn(self, "send")
unreached()
def get_py_value(self, allow_tensor=False):
raise BreakGraphError(
UnsupportedOperationBreak(
reason_str="Get real value from generator is not supported."
)
)
def get_py_type(self):
return types.GeneratorType
def next(self):
return self.send(ConstantVariable.wrap_literal(None, self.graph))
def to_list(self):
return do_until_stop_iteration(lambda: self.next())
@property
def main_info(self) -> dict[str, Any]:
return {
"co_name": self.code_var.value.co_name,
}
# @VariableFactory.register_from_value()
# def from_value(value: Any, graph: FunctionGraph, tracker: Tracker):
# if inspect.isgenerator(value):
# return GeneratorVariable()
# return None
# what UserDefinedIterVariable holds doesn't matter, because use user defined iterator will trigger break graph
class UserDefinedIterVariable(IterVariable):
def __init__(
self,
held: VariableBase | list[VariableBase],
graph: FunctionGraph,
tracker: Tracker,
):
if not isinstance(held, list):
held = [held]
self.holds = held
super().__init__(graph, tracker)
def to_list(self):
raise BreakGraphError(
UnsupportedOperationBreak(
reason_str="Break graph when iterating user defined iterator"
)
)
def next(self):
raise BreakGraphError(
UnsupportedOperationBreak(
reason_str="Break graph when iterating user defined iterator"
)
)
@check_faster_guard
def make_faster_guard(self) -> list[paddle.framework.core.GuardNodeBase]:
return [
guard for held in self.holds for guard in held.make_faster_guard()
]
def make_stringified_guard(self):
return [
guard
for held in self.holds
for guard in held.make_stringified_guard()
]
@@ -0,0 +1,238 @@
# Copyright (c) 2025 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 builtins
import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, NamedTuple
from ...utils import log
from .tracker import (
BuiltinTracker,
CellTracker,
ConstTracker,
DanglingTracker,
FunctionClosureTracker,
LocalTracker,
)
from .variable_stack import VariableStack
from .variables.base import VariableBase, VariableFactory, fn_bind_inputs
from .variables.basic import (
CellVariable,
FunctionGlobalVariable,
GlobalVariable,
NullVariable,
)
if TYPE_CHECKING:
import types
from typing import TypeAlias
from ..instruction_utils import Instruction
from .function_graph import FunctionGraph
from .variables.callable import FunctionVariable
# The type to represent the (*args, **kwargs) pack in the call.
CallArgsPack: TypeAlias = tuple[tuple[Any, ...], dict[str, Any]]
def validate_value(value):
assert isinstance(value, VariableBase), (
f"value: {value}, type should be VariableBase(or derived), but get {type(value)}"
)
assert not isinstance(value.tracker, DanglingTracker) or isinstance(
value, (NullVariable, CellVariable)
), f"dangling variable {value} should not be pushed into stack."
@dataclass
class BlockStackItem:
# `PyTryBlock` in CPython source code
type: str
inst: Instruction
handler: Instruction
level: int
class VirtualFrameState(NamedTuple):
locals: dict[str, VariableBase]
builtins: dict[str, VariableBase]
cells: dict[str, VariableBase]
lasti: int
stack_data: list[VariableBase]
block_stack: list[BlockStackItem]
class VirtualFrame:
code: types.CodeType
locals: dict[str, Any] # TODO: should we use DictVariable instead of dict?
globals: GlobalVariable
builtins: dict[str, Any]
consts: list[Any]
cells: dict[str, Any]
lasti: int
stack: VariableStack
block_stack: list[BlockStackItem]
def __init__(self, code: types.CodeType):
self.code = code
self.locals = {}
self.globals = None # type: ignore
self.builtins = {}
self.cells = {}
self.lasti = 0
self.consts = []
self.stack = VariableStack(validate_value_func=validate_value)
self.block_stack: list[BlockStackItem] = []
@staticmethod
def from_real_frame(frame: types.FrameType, graph: FunctionGraph):
code = frame.f_code
locals = frame.f_locals
vframe = VirtualFrame(code)
# convert locals
free_or_cell_vars = code.co_cellvars + code.co_freevars
for name, value in locals.items():
tracker = (
CellTracker(name)
if name in free_or_cell_vars
else LocalTracker(name)
)
vframe.locals[name] = VariableFactory.from_value(
value, graph, tracker
)
for name in free_or_cell_vars:
# create a cell for each variable.
vframe.cells[name] = CellVariable() # put in cells.
if name in vframe.locals:
vframe.cells[name].set_value(vframe.locals[name])
# convert globals
vframe.globals = GlobalVariable(
frame.f_globals,
graph,
DanglingTracker(),
)
# convert builtins
for name, value in builtins.__dict__.items():
vframe.builtins[name] = VariableFactory.from_value(
value, graph, BuiltinTracker(name)
)
# Temporarily use the builtins from the graph to avoid the conversion overhead.
graph.builtins = vframe.builtins
# prepare consts
for value in code.co_consts:
vframe.consts.append(
VariableFactory.from_value(value, graph, ConstTracker(value))
)
return vframe
@staticmethod
def from_inline_call(
code: types.CodeType,
fn_var: FunctionVariable,
fn_value: types.FunctionType,
graph: FunctionGraph,
call_args_pack: CallArgsPack,
):
call_args, call_kwargs = call_args_pack
vframe = VirtualFrame(code)
vframe.globals = FunctionGlobalVariable(
fn_var,
fn_value.__globals__,
graph,
DanglingTracker(),
)
# convert builtins
# NOTE(SigureMo): inline call should inherit the builtins from the caller to reduce the conversion overhead.
vframe.builtins = graph.builtins
# prepare consts
for value in code.co_consts:
vframe.consts.append(
VariableFactory.from_value(value, graph, ConstTracker(value))
)
# convert locals
vframe.locals.update(
fn_bind_inputs(fn_value, graph, *call_args, **call_kwargs)
)
log(
5,
f"[INLINE CALL] {code.co_name} with locals: ",
vframe.locals,
)
# handle implicit variables in comprehensions
vframe.handle_comps(fn_value)
# convert closures
closure = fn_var.get_py_value().__closure__
for name in code.co_cellvars + code.co_freevars:
# create a cell for each variable.
vframe.cells[name] = CellVariable() # put in cells.
if name in vframe.locals:
vframe.cells[name].set_value(vframe.locals[name])
if closure is None:
return vframe
assert len(closure) == len(code.co_freevars)
for idx, (name, cell) in enumerate(zip(code.co_freevars, closure)):
value = cell.cell_contents
value = VariableFactory.from_value(
value, graph, FunctionClosureTracker(fn_var, idx)
)
# wrapped by a CellVariable
if not isinstance(value, CellVariable):
value = CellVariable(value)
vframe.cells[name] = value
return vframe
def handle_comps(self, fn_value):
is_comp = any(
x in fn_value.__name__
for x in ['<listcomp>', '<dictcomp>', '<setcomp>', '<genexpr>']
)
if not is_comp:
return
pattern = r'implicit\d+'
for name in list(self.locals.keys()):
if re.match(pattern, name):
self.locals[name.replace('implicit', '.')] = self.locals[name]
def get_state(self):
return VirtualFrameState(
locals=self.locals.copy(),
builtins=self.builtins.copy(),
cells=self.cells.copy(),
lasti=self.lasti,
stack_data=list(self.stack._data),
block_stack=self.block_stack.copy(),
)
def restore_state(self, state: VirtualFrameState):
self.locals = state.locals
self.builtins = state.builtins
self.cells = state.cells
self.lasti = state.lasti
self.stack._data = state.stack_data
self.block_stack = state.block_stack
@@ -0,0 +1,35 @@
# Copyright (c) 2023 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 .instruction_pass import apply_instr_pass # noqa: F401
from .instruction_utils import ( # noqa: F401
Instruction,
Space,
calc_offset_from_bytecode_offset,
calc_stack_effect,
convert_instruction,
gen_instr,
get_instruction_size,
get_instructions,
instrs_info,
modify_extended_args,
modify_instrs,
modify_vars,
relocate_jump_target,
replace_instr,
reset_offset,
)
from .opcode_analysis import ( # noqa: F401
analysis_used_names,
)
@@ -0,0 +1,334 @@
# Copyright (c) 2023 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 dis
import sys
from typing import TYPE_CHECKING
from paddle.jit.sot.utils import log, log_do
from ...utils import InnerError
from .instruction_utils import instrs_info
from .opcode_info import TO_FUSED_INSTS
from .stack_analyse import StackAnalyser
if TYPE_CHECKING:
from .instruction_utils import Instruction
def apply_instr_pass(instrs: list[Instruction], code_options):
log(4, f"[Opcode Pass]: Original New Code {code_options['co_name']}:\n")
log_do(4, lambda: print(instrs_info(instrs)))
supported_passes = [
remove_load_store_pass,
remove_duplicate_resume,
check_precall_followed_by_call,
]
if sys.version_info >= (3, 12):
supported_passes.append(check_for_iter_jump_to)
if sys.version_info >= (3, 13):
supported_passes.append(fuse_double_super_instrs)
for instr_pass in supported_passes:
instr_pass(instrs, code_options)
log(
4,
f"[Opcode Pass]: New Code After Opcode Pass {code_options['co_name']}:\n",
)
log_do(4, lambda: print(instrs_info(instrs)))
def find_stored_once_local_vars(instrs: list[Instruction], code_options):
"""
find out the local var names which is only stored once
"""
stored_vars = {}
# The input vars are considered as stored at the beginning
input_names = code_options['co_varnames'][: code_options['co_argcount']]
for name in input_names:
stored_vars[name] = 1
for instr in instrs:
if instr.opname == "STORE_FAST":
if instr.argval in stored_vars:
stored_vars[instr.argval] += 1
else:
stored_vars[instr.argval] = 1
stored_once = {name for name, count in stored_vars.items() if count == 1}
return stored_once
def find_loaded_once_local_vars(instrs: list[Instruction], code_options):
"""
find out the local var names which is only stored once
"""
loaded_vars = {}
for instr in instrs:
if instr.opname in ["LOAD_FAST", "LOAD_FAST_BORROW", "LOAD_FAST_CHECK"]:
if instr.argval in loaded_vars:
loaded_vars[instr.argval] += 1
else:
loaded_vars[instr.argval] = 1
loaded_once = {name for name, count in loaded_vars.items() if count == 1}
return loaded_once
def find_related_local_opcodes(instrs: list[Instruction], code_options):
"""
Find opcode pairs consisting of LOAD_FAST, LOAD_FAST_BORROW, STORE_FAST, and LOAD_FAST_CHECK.
"""
stack = []
opcode_pairs = []
for instr in instrs:
if instr.opname in ["LOAD_FAST", "LOAD_FAST_BORROW", "LOAD_FAST_CHECK"]:
stack.append(instr)
elif instr.opname == "STORE_FAST":
if len(stack) > 0 and stack[-1] is not None:
opcode_pairs.append((stack[-1], instr))
stack.pop()
elif "ROT" in instr.opname or "DUP" in instr.opname:
return []
else:
try:
pop_n, push_n = StackAnalyser().stack_effect(instr)
if pop_n == 0:
stack.extend([None] * push_n)
else:
stack = stack[:-pop_n] + [None] * push_n
except AttributeError:
break
return opcode_pairs
def remove_load_store_pass(instrs: list[Instruction], code_options):
"""
This question is extremely complex, so we just simplify it as
'remove renames which is between var names who only stored once'
and we only consider the local vars.
"""
def stored_from(load_instr, instrs):
idx = instrs.index(load_instr) - 1
while idx >= 0:
instr = instrs[idx]
if (
instr.opname == "STORE_FAST"
and instr.argval == load_instr.argval
):
return instr
idx -= 1
return None
def code_exist(opname, argval, instrs):
for instr in instrs:
if instr.opname == opname and instr.argval == argval:
return True
return False
# remove rename and load store
jump_target = {
instr.jump_to for instr in instrs if instr.jump_to is not None
}
modified = True
while modified:
modified = False
stored_once = find_stored_once_local_vars(instrs, code_options)
# find out all LOAD_FAST -> STORE_FAST pair
opcode_pairs = find_related_local_opcodes(instrs, code_options)
for load_a, store_b in opcode_pairs:
if load_a in jump_target or store_b in jump_target:
continue
a_name = load_a.argval
b_name = store_b.argval
# if these two names are only stored once
# it means these two name only have one value all the time
# so we can just rename them, to delete some codes
if a_name in stored_once and b_name in stored_once:
instrs.remove(load_a)
instrs.remove(store_b)
if a_name != b_name:
for instr in instrs:
if (
instr.opname
in (
"LOAD_FAST_CHECK",
"LOAD_FAST",
"LOAD_FAST_BORROW",
"STORE_FAST",
)
and instr.argval == b_name
):
instr.argval = a_name
instr.arg = load_a.arg
modified = True
# if
# LOAD A
# STORE B
# A or B is not stored only once (maybe it is input)
# we give a more general way to simplify the codes
#
# if A will not be loaded again after (6)STORE B, it means we can move (6)STORE B ahead to (1)STORE A
# TIP: there is no more STORE A between (1) and (5)
# (1) STORE A -> STORE B
# ... ...
# (2) LOAD A -> LOAD B
# ...
# (3) LOAD B -> not support
# ...
# (4) STORE B -> not support
# ... ...
# (5) LOAD A -> ---- (rm)
# (6) STORE B ---- (rm)
# ...
# (7) STORE B
# (8) LOAD A
# so we can rename the rest LOAD A below as LOAD B
#
# What changed:
# 1. if (4) exist, B changed:
# (1) ~ (4), (6) ~
# 2. if (4) not exist, B changed:
# (1), (6)
# 3. A changed:
# (1) ~
#
# To do this transform, we should make sure
# 1. (4) is not exist in (1) ~ (5): it is too complex
# 2. (3) is not exist in (1) ~ (5): load B in the range that B value is changed
# 3. (7) (8) is not exist in (6)~: load A in range that A value is changed, if we load B instead, but B also changed
# we can simplify this as "no more LOAD A after (6)"
else:
last_store_a = stored_from(load_a, instrs)
if last_store_a is None:
# if last store a just not exist, we can not do this transform
continue
last_store_idx = instrs.index(last_store_a)
code_range = instrs[last_store_idx : instrs.index(store_b)]
if (
not code_exist("STORE_FAST", b_name, code_range)
and not code_exist("LOAD_FAST_CHECK", b_name, code_range)
and not code_exist("LOAD_FAST", b_name, code_range)
and not code_exist("LOAD_FAST_BORROW", b_name, code_range)
and not code_exist(
"LOAD_FAST_CHECK",
a_name,
instrs[instrs.index(store_b) :],
)
and not code_exist(
"LOAD_FAST", a_name, instrs[instrs.index(store_b) :]
)
and not code_exist(
"LOAD_FAST_BORROW",
a_name,
instrs[instrs.index(store_b) :],
)
):
last_store_a.argval = b_name
last_store_a.arg = store_b.arg
instrs.remove(load_a)
instrs.remove(store_b)
for instr in instrs[last_store_idx:]:
if (
instr.opname
in (
"LOAD_FAST_CHECK",
"LOAD_FAST",
"LOAD_FAST_BORROW",
"STORE_FAST",
)
and instr.argval == a_name
):
instr.argval = b_name
instr.arg = store_b.arg
def remove_duplicate_resume(instrs: list[Instruction], code_options):
resumes = list(filter(lambda instr: instr.opname == "RESUME", instrs))
if not resumes:
return
for resume in resumes[1:]:
instrs.remove(resume)
def check_precall_followed_by_call(instrs: list[Instruction], code_options):
"""
PRECALL should be followed by CALL, otherwise it will cause a segmentation fault
"""
for instr, next_instr in zip(instrs[:-1], instrs[1:]):
if instr.opname == "PRECALL" and next_instr.opname != "CALL":
raise InnerError(
f"PRECALL is not followed by CALL in {code_options['co_name']}"
)
def check_for_iter_jump_to(instrs: list[Instruction], code_options):
"""
Check if the `jump_to` of FOR_ITER is END_FOR, in Python3.12+
"""
for instr in instrs:
if instr.opname == "FOR_ITER":
assert instr.jump_to is not None
if instr.jump_to.opname != "END_FOR":
raise InnerError("FOR_ITER jump_to is not END_FOR")
def fuse_double_super_instrs(instrs: list[Instruction], code_options):
"""
Fuse two consecutive LOAD_FAST or STORE_FAST instructions into one.
"""
co_varnames = code_options['co_varnames']
def able_to_merge(idx: int):
return (
idx > 0
and (instrs[idx - 1].opname, instrs[idx].opname)
in TO_FUSED_INSTS.keys()
and not instrs[idx].is_jump_target
and not instrs[idx - 1].is_jump_target
and co_varnames.index(instrs[idx - 1].argval) < 16
and co_varnames.index(instrs[idx].argval) < 16
)
def merge_two_op(prev_instr: Instruction, instr: Instruction):
merge_key = (instrs[idx - 1].opname, instrs[idx].opname)
prev_instr.opname = TO_FUSED_INSTS[merge_key]
prev_instr.opcode = dis.opmap[prev_instr.opname]
prev_instr.is_generated = True
prev_instr.argval = (prev_instr.argval, instr.argval)
instrs.remove(instr)
idx = 0
# We must manually control the indices, so we cannot use a for loop.
while idx < len(instrs):
if able_to_merge(idx):
merge_two_op(instrs[idx - 1], instrs[idx])
continue
idx += 1
@@ -0,0 +1,586 @@
# Copyright (c) 2023 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 dataclasses
import dis
import sys
from enum import Enum
from typing import TYPE_CHECKING, Any
from ...utils import InnerError
from .opcode_info import (
ABS_JUMP,
ALL_JUMP,
FUSED_INSTS,
PYOPCODE_CACHE_SIZE,
REL_BWD_JUMP,
REL_JUMP,
)
if TYPE_CHECKING:
import types
@dataclasses.dataclass
class Instruction:
opcode: int
opname: str
arg: int | None
argval: Any
offset: int | None = None
starts_line: int | None = None
is_jump_target: bool = False
jump_to: Instruction | None = None
is_generated: bool = True
# for analysis EXTENDED_ARG
first_ex_arg: Instruction | None = None
ex_arg_for: Instruction | None = None
# used in modify_extended_args
def __hash__(self):
return id(self)
def __eq__(self, instr):
return id(self) == id(instr)
def get_instruction_size(instr: Instruction) -> int:
cache_size = 0
if sys.version_info >= (3, 11):
cache_size = PYOPCODE_CACHE_SIZE.get(instr.opname, 0)
return 2 * (cache_size + 1)
def gen_instr(name, arg=None, argval=None, gened=True, jump_to=None):
return Instruction(
opcode=dis.opmap[name],
opname=name,
arg=arg,
argval=argval,
is_generated=gened,
jump_to=jump_to,
)
def convert_instruction(instr: dis.Instruction) -> Instruction:
"""
Converts a disassembled instruction to a customized Instruction object.
Args:
instr (dis.Instruction): The disassembled instruction.
Returns:
Instruction: A customized Instruction object.
"""
return Instruction(
instr.opcode,
instr.opname,
instr.arg,
instr.argval,
instr.offset,
instr.line_number if sys.version_info >= (3, 13) else instr.starts_line,
instr.is_jump_target,
jump_to=None,
is_generated=False,
)
def replace_jump_target(
instrs: list[Instruction],
replacements: dict[Instruction, Instruction],
) -> None:
"""Replace jump targets based on the replacements dictionary.
Args:
instrs (list[Instruction]): The list of instructions to modify.
replacements (dict[Instruction, Instruction]): Mapping from old jump targets to new ones.
"""
for instr in instrs:
if instr.jump_to in replacements:
instr.jump_to = replacements[instr.jump_to]
def expand_super_instrs(instructions: list[Instruction]) -> list[Instruction]:
expanded_instrs = []
replacements = {}
def copy_instruction(
instr, opname, argval, arg, is_jump_target, is_generated
):
return Instruction(
opcode=dis.opmap[opname],
opname=opname,
arg=arg,
argval=argval,
is_jump_target=is_jump_target,
is_generated=is_generated,
jump_to=instr.jump_to,
)
for instr in instructions:
if instr.opname in FUSED_INSTS:
instr1 = copy_instruction(
instr,
FUSED_INSTS[instr.opname][0],
instr.argval[0],
instr.arg >> 4,
instr.is_jump_target,
True,
)
instr2 = copy_instruction(
instr,
FUSED_INSTS[instr.opname][1],
instr.argval[1],
instr.arg & 15,
False,
False,
)
replacements[instr] = instr1
expanded_instrs.append(instr1)
expanded_instrs.append(instr2)
# If the LOAD_ATTR opcode will lead to load_method in 3.13+, we manually split it into two instructions,
# to avoid Uncontrollable specialization that changes the behavior of LOAD_ATTR,
# which can lead to incorrect results when the current graph is smaller than the MIN_GRAPH_SIZE
elif (
sys.version_info >= (3, 13)
and instr.opname == "LOAD_ATTR"
and instr.arg & 1
):
instr1 = copy_instruction(
instr,
"LOAD_ATTR",
instr.argval,
instr.arg & ~1,
instr.is_jump_target,
True,
)
instr2 = Instruction(
dis.opmap["PUSH_NULL"],
"PUSH_NULL",
None,
None,
is_generated=True,
)
replacements[instr] = instr1
expanded_instrs.append(instr1)
expanded_instrs.append(instr2)
else:
expanded_instrs.append(instr)
replace_jump_target(expanded_instrs, replacements)
return expanded_instrs
def replace_load_fast_borrow_with_strong_ref(
instructions: list[Instruction],
) -> list[Instruction]:
"""
Patch LOAD_FAST_BORROW to LOAD_FAST for Python 3.14+.
LOAD_FAST_BORROW loads a value using a borrowing reference and does not
increment the reference count. In some cases this can cause subsequent
STORE_FAST or other operations to retain a reference that becomes invalid
once the borrowed value is released, leading to incorrect behavior or
crashes when the variable is accessed later.
To avoid these issues, we replace LOAD_FAST_BORROW with LOAD_FAST here.
"""
replacements = {}
expanded_instrs = []
for instr in instructions:
if instr.opname == "LOAD_FAST_BORROW":
instr1 = Instruction(
dis.opmap["LOAD_FAST"],
"LOAD_FAST",
instr.arg,
instr.argval,
is_generated=instr.is_generated,
is_jump_target=instr.is_jump_target,
jump_to=instr.jump_to,
)
replacements[instr] = instr1
expanded_instrs.append(instr1)
else:
expanded_instrs.append(instr)
replace_jump_target(expanded_instrs, replacements)
return expanded_instrs
def get_instructions(code: types.CodeType) -> list[Instruction]:
"""
Returns parsed instructions from the given code object and exclude
any opcodes that contain `EXTENDED_ARG`.
Args:
code (types.CodeType): The code object to extract instructions from.
Returns:
list[Instruction]: A list of Instruction objects representing the
bytecode instructions in the code object.
"""
# instrs do not contain EXTENDED_ARG
instrs = list(map(convert_instruction, dis.get_instructions(code)))
for instr in instrs:
if instr.opname in ALL_JUMP:
origin_jump_target = calc_offset_from_bytecode_offset(
instr.argval, instrs
)
jump_offset = origin_jump_target
while instrs[jump_offset].opname == "EXTENDED_ARG":
jump_offset += 1
if origin_jump_target != jump_offset:
# copy infos from EXTENDED_ARG to other opcode
if instrs[origin_jump_target].is_jump_target:
instrs[jump_offset].is_jump_target = instrs[
origin_jump_target
].is_jump_target
if instrs[origin_jump_target].starts_line:
instrs[jump_offset].starts_line = instrs[
origin_jump_target
].starts_line
instr.jump_to = instrs[jump_offset]
# if the origin opcode contains EXTENDED_ARG, it should be like:
# >> EXTENDED_ARG 1
# XX 388 <- 256 + 132
# filter all EXTENDED_ARG here
instrs = [x for x in instrs if x.opname != "EXTENDED_ARG"]
prepare_passes = [expand_super_instrs]
if sys.version_info >= (3, 14):
prepare_passes.append(replace_load_fast_borrow_with_strong_ref)
for pass_fn in prepare_passes:
instrs = pass_fn(instrs)
return instrs
def modify_instrs(instructions: list[Instruction]) -> None:
"""
Modifies the given list of instructions. It contains three steps:
1. reset offset
2. relocate jump target
3. add EXTENDED_ARG instruction if needed
Args:
instructions (list): The list of Instruction objects representing bytecode instructions.
Returns:
None
"""
modify_completed = False
while not modify_completed:
reset_offset(instructions)
has_inverted_jump = relocate_jump_target(instructions)
modify_completed = (
modify_extended_args(instructions) and not has_inverted_jump
)
def reset_offset(instructions: list[Instruction]) -> None:
"""
Resets the offset for each instruction in the list.
Args:
instructions (list): The list of Instruction objects representing bytecode instructions.
Returns:
None
"""
from ..executor.pycode_generator import get_instruction_size
if sys.version_info >= (3, 11):
current_offset = 0
for instr in instructions:
instr.offset = current_offset
current_offset += get_instruction_size(instr)
return
for idx, instr in enumerate(instructions):
instr.offset = idx * 2
def correct_jump_direction(
instr: Instruction, arg: int
) -> tuple[Instruction, bool]:
"""
Corrects the jump direction of the given instruction.
NOTE(zrr1999): In Python 3.11, JUMP_ABSOLUTE is removed, so python generates JUMP_FORWARD or JUMP_BACKWARD instead,
but in for loop breakgraph, we reuse JUMP_BACKWARD to jump forward, so we need to change it to JUMP_FORWARD.
Args:
instr (Instruction): The instruction to be corrected.
invert_jump (bool): Whether to invert the jump direction.
"""
if instr.opname in ABS_JUMP:
instr.arg = arg
return instr, False
elif instr.opname in REL_JUMP:
if arg < 0:
if instr.opname in REL_BWD_JUMP:
forward_op_name = instr.opname.replace("BACKWARD", "FORWARD")
if forward_op_name not in dis.opmap:
raise InnerError(f"Unknown jump type {instr.opname}")
instr.opname = forward_op_name
instr.opcode = dis.opmap[forward_op_name]
else: # instr.opname in REL_FWD_JUMP
backward_op_name = instr.opname.replace("FORWARD", "BACKWARD")
if backward_op_name not in dis.opmap:
raise InnerError(f"Unknown jump type {instr.opname}")
instr.opname = backward_op_name
instr.opcode = dis.opmap[backward_op_name]
instr.arg = -arg
invert_jump = True
else:
instr.arg = arg
invert_jump = False
return instr, invert_jump
else:
raise ValueError(f"unknown jump type: {instr.opname}")
def relocate_jump_target(instructions: list[Instruction]) -> bool:
"""
If a jump instruction is found, this function will adjust the jump targets based on the presence of EXTENDED_ARG instructions.
If an EXTENDED_ARG instruction exists for the jump target, use its offset as the new target.
Args:
instructions (list): The list of Instruction objects representing bytecode instructions.
Returns:
bool: True if the jump direction is inverted, False otherwise.
"""
has_inverted_jump = False
extended_arg = []
for instr in instructions:
if instr.opname == "EXTENDED_ARG":
extended_arg.append(instr)
continue
if instr.opname in ALL_JUMP:
assert instr.jump_to is not None
assert instr.offset is not None
# if jump target has extended_arg, should jump to the first extended_arg opcode
jump_target = (
instr.jump_to.offset
if instr.jump_to.first_ex_arg is None
else instr.jump_to.first_ex_arg.offset
)
assert jump_target is not None
if instr.opname in ABS_JUMP:
new_arg = jump_target
else: # instr.opname in REL_JUMP
cache_size = PYOPCODE_CACHE_SIZE.get(instr.opname, 0)
new_arg = jump_target - (2 * cache_size) - instr.offset - 2
if instr.opname in REL_BWD_JUMP:
new_arg = -new_arg
new_arg //= 2
_, invert_jump = correct_jump_direction(instr, new_arg)
has_inverted_jump = has_inverted_jump or invert_jump
assert instr.arg is not None
if extended_arg:
instr.arg &= 0xFF
new_arg = new_arg >> 8
for ex in reversed(extended_arg):
ex.arg = new_arg & 0xFF
new_arg = new_arg >> 8
# need more extended_args instr
# set arg in the first extended_arg
if new_arg > 0:
extended_arg[0].arg += new_arg << 8
extended_arg.clear()
return has_inverted_jump
def modify_extended_args(instructions: list[Instruction]) -> bool:
"""
This function replaces any instruction with an argument greater than or equal to 256 with one or more EXTENDED_ARG instructions.
Args:
instructions (list): The list of Instruction objects representing bytecode instructions.
Returns:
bool: True if the modification is completed, False otherwise.
"""
modify_completed = True
extend_args_record = {}
for instr in instructions:
if instr.arg and instr.arg >= 256: # more than one byte
_instrs = [
instr
] # replace instr with _instrs later (it is a set of instrs), all operations will be recorded in extend_args_record
val = instr.arg
instr.arg = val & 0xFF
val = val >> 8
while val > 0:
_instrs.append(gen_instr("EXTENDED_ARG", arg=val & 0xFF))
val = val >> 8
extend_args_record.update({instr: list(reversed(_instrs))})
if extend_args_record:
# if new EXTENDED_ARG inserted, we need update offset and jump target
modify_completed = False
def bind_ex_arg_with_instr(ex_arg, instr):
# move opcode info to EXTENDED_ARG
ex_arg.starts_line = instr.starts_line
instr.starts_line = None
ex_arg.is_jump_target = instr.is_jump_target
instr.is_jump_target = False
if instr.ex_arg_for is not None:
# instr is also an ex_arg for another instr
instr.ex_arg_for.first_ex_arg = ex_arg
ex_arg.ex_arg_for = instr.ex_arg_for
instr.ex_arg_for = None
else:
instr.first_ex_arg = ex_arg
ex_arg.ex_arg_for = instr
for key, val in extend_args_record.items():
bind_ex_arg_with_instr(val[0], key)
replace_instr(instructions, instr=key, new_instr=val)
return modify_completed
def modify_vars(instructions: list[Instruction], code_options):
co_varnames = code_options['co_varnames']
co_freevars = code_options['co_freevars']
for instrs in instructions:
if instrs.opname in [
'LOAD_FAST',
'LOAD_FAST_BORROW',
'LOAD_FAST_CHECK',
'STORE_FAST',
'DELETE_FAST',
]:
assert instrs.argval in co_varnames, (
f"`{instrs.argval}` not in {co_varnames}"
)
instrs.arg = co_varnames.index(instrs.argval)
elif instrs.opname == "LOAD_DEREF" or instrs.opname == "STORE_DEREF":
if sys.version_info >= (3, 11):
namemap = co_varnames + co_freevars
assert instrs.argval in namemap, (
f"`{instrs.argval}` not in {namemap}"
)
instrs.arg = namemap.index(instrs.argval)
elif instrs.opname in FUSED_INSTS.keys():
assert instrs.argval[0] in co_varnames, (
f"`{instrs.argval[0]}` not in {co_varnames}"
)
assert instrs.argval[1] in co_varnames, (
f"`{instrs.argval[1]}` not in {co_varnames}"
)
instrs.arg = (
co_varnames.index(instrs.argval[0]) << 4
) + co_varnames.index(instrs.argval[1])
def calc_offset_from_bytecode_offset(
bytecode_offset: int,
instructions: list[dis.Instruction] | list[Instruction],
) -> int:
"""
Calculate the index from bytecode offset, because it have 2 bytes per instruction (for Python <= 3.10).
Args:
bytecode_offset (int): The bytecode offset of the instruction.
Returns:
int: The index of the instruction in the instruction list.
"""
if sys.version_info >= (3, 11):
instruction_offsets = [x.offset for x in instructions]
return instruction_offsets.index(bytecode_offset)
return bytecode_offset // 2
def replace_instr(instructions, instr, new_instr):
idx = instructions.index(instr)
instructions[idx : idx + 1] = new_instr
def instrs_info(instrs, mark=None, range=None, want_str=True):
ret = []
start = -1
end = 1000000
if mark is not None and range is not None:
start = mark - range
end = mark + range + 1
for idx, instr in enumerate(instrs):
if idx < start or idx >= end:
continue
if instr.starts_line is not None:
ret.append("")
ret.append(
"{line:<8s}{is_jump_target:>2s}{offset:>4d} {opname:<30s}{arg:<4s}{argval:<40s}{mark}".format(
line=str(instr.starts_line) if instr.starts_line else "",
is_jump_target=">>" if instr.is_jump_target else " ",
offset=(
instr.offset if instr.offset or instr.offset == 0 else -1
),
opname=instr.opname,
arg=str(instr.arg) if instr.arg is not None else "",
argval=f"({instr.argval})" if instr.argval else "",
mark="",
)
)
if idx == mark:
ret[-1] = "\033[31m" + ret[-1] + "\033[0m"
if want_str:
return "\n".join(ret)
return ret
def calc_stack_effect(instr: Instruction, *, jump: bool | None = None) -> int:
"""
Gets the stack effect of the given instruction. In Python 3.11, the stack effect of `CALL` is -1,
refer to https://github.com/python/cpython/blob/3.11/Python/compile.c#L1123-L1124.
Args:
instr: The instruction.
Returns:
The stack effect of the instruction.
"""
if sys.version_info[:2] == (3, 11):
if instr.opname == "PRECALL":
return 0
elif instr.opname == "CALL":
# NOTE(zrr1999): push_n = 1, pop_n = oparg + 2, stack_effect = push_n - pop_n = -oparg-1
assert instr.arg is not None
return -instr.arg - 1
return dis.stack_effect(instr.opcode, instr.arg, jump=jump)
class Space(Enum):
locals = 1
globals = 2
cells = 3
not_found = 4
@@ -0,0 +1,161 @@
# Copyright (c) 2023 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 dataclasses
from typing import TYPE_CHECKING
from paddle.jit.utils import OrderedSet
from .opcode_info import (
ALL_JUMP,
HAS_FREE,
HAS_LOCAL,
UNCONDITIONAL_JUMP,
)
if TYPE_CHECKING:
from .instruction_utils import Instruction
@dataclasses.dataclass
class NameRecorder:
reads: OrderedSet[str]
writes: OrderedSet[str]
def __or__(self, other):
reads = self.reads | other.reads
writes = self.writes | other.writes
return NameRecorder(reads, writes)
def is_read_opcode(opname):
if opname in [
"LOAD_FAST",
"LOAD_FAST_CHECK",
"LOAD_DEREF",
"LOAD_NAME",
"LOAD_GLOBAL",
"LOAD_CLOSURE",
"LOAD_FAST_BORROW",
]:
return True
if opname in (
"DELETE_FAST",
"DELETE_DEREF",
"DELETE_NAME",
"DELETE_GLOBAL",
):
return True
return False
def is_write_opcode(opname):
if opname in ["STORE_FAST", "STORE_NAME", "STORE_DEREF", "STORE_GLOBAL"]:
return True
if opname in (
"DELETE_FAST",
"DELETE_DEREF",
"DELETE_NAME",
"DELETE_GLOBAL",
):
return True
return False
def analysis_used_names(
instructions: list[Instruction],
current_instr_idx: int,
stop_instr_idx: int | None = None,
) -> tuple[OrderedSet[str], OrderedSet[str]]:
"""
Analyze the inputs of the instructions from current_instr_idx to stop_instr_idx.
Args:
instructions (list[Instruction]): The instructions to analyze.
current_instr_idx (int): The index of the current instruction.
stop_instr_idx (int | None, optional): The index of the instruction to stop. Defaults to None.
If None, the analysis will stop at the end of the instructions.
Returns:
State: The analysis result.
"""
name_recorder = NameRecorder(OrderedSet(), OrderedSet())
# start idx and writes names can decide the analysis result below
# so, just check the pair of (idx, writes), to skip repeat simulation
# (writes can decide if a name should be add to reads)
# one idx can has multi writes for whom is not subset with each other
# if A is subset of B, we just record A, simulate A might add more reads
visited_states = {}
def check_and_update_visited_states(idx, writes):
writes = set(writes)
if idx in visited_states:
history = visited_states[idx]
for record in history:
if record.issubset(writes):
return True
elif writes.issubset(record):
history.remove(record)
history.append(writes)
return False
else:
visited_states[idx] = [writes]
return False
def fork(
name_recorder: NameRecorder, start: int, jump: bool, jump_target: int
) -> NameRecorder:
new_start = start + 1 if not jump else jump_target
new_state = NameRecorder(
OrderedSet(name_recorder.reads),
OrderedSet(name_recorder.writes),
)
return walk(new_state, new_start)
def walk(name_recorder: NameRecorder, start: int) -> NameRecorder:
end = len(instructions) if stop_instr_idx is None else stop_instr_idx
for i in range(start, end):
if check_and_update_visited_states(i, name_recorder.writes):
return name_recorder
instr = instructions[i]
if instr.opname in HAS_LOCAL | HAS_FREE:
if is_read_opcode(instr.opname) and instr.argval not in (
name_recorder.writes
):
name_recorder.reads.add(instr.argval)
elif is_write_opcode(instr.opname):
name_recorder.writes.add(instr.argval)
elif instr.opname in ALL_JUMP:
assert instr.jump_to is not None
target_idx = instructions.index(instr.jump_to)
# Fork to two branches, jump or not
jump_branch = fork(name_recorder, i, True, target_idx)
not_jump_branch = (
fork(name_recorder, i, False, target_idx)
if instr.opname not in UNCONDITIONAL_JUMP
else NameRecorder(OrderedSet(), OrderedSet())
)
return jump_branch | not_jump_branch
elif instr.opname == "RETURN_VALUE":
return name_recorder
return name_recorder
name_recorder = walk(name_recorder, current_instr_idx)
return name_recorder.reads, name_recorder.writes
@@ -0,0 +1,167 @@
# Copyright (c) 2023 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 opcode
import sys
from enum import Enum
REL_JUMP = {opcode.opname[x] for x in opcode.hasjrel}
REL_BWD_JUMP = {opname for opname in REL_JUMP if "BACKWARD" in opname}
REL_FWD_JUMP = REL_JUMP - REL_BWD_JUMP
ABS_JUMP = {opcode.opname[x] for x in opcode.hasjabs}
HAS_LOCAL = {opcode.opname[x] for x in opcode.haslocal}
HAS_FREE = {opcode.opname[x] for x in opcode.hasfree}
NEED_TO_BOOL = {"UNARY_NOT", "POP_JUMP_IF_FALSE", "POP_JUMP_IF_TRUE"}
ALL_JUMP = REL_JUMP | ABS_JUMP
UNCONDITIONAL_JUMP = {"JUMP_ABSOLUTE", "JUMP_FORWARD"}
if sys.version_info >= (3, 11):
UNCONDITIONAL_JUMP.add("JUMP_BACKWARD")
RETURN = {"RETURN_VALUE"}
if (3, 12) <= sys.version_info < (3, 14):
RETURN.add("RETURN_CONST")
class JumpDirection(Enum):
FORWARD = "FORWARD"
BACKWARD = "BACKWARD"
class PopJumpCond(Enum):
FALSE = "FALSE"
TRUE = "TRUE"
NONE = "NONE"
NOT_NONE = "NOT_NONE"
def _get_pyopcode_cache_size() -> dict[str, int]:
if sys.version_info >= (3, 11) and sys.version_info < (3, 12):
# Cache for some opcodes, it's for Python 3.11
# https://github.com/python/cpython/blob/3.11/Include/internal/pycore_opcode.h#L41-L53
return {
"BINARY_SUBSCR": 4,
"STORE_SUBSCR": 1,
"UNPACK_SEQUENCE": 1,
"STORE_ATTR": 4,
"LOAD_ATTR": 4,
"COMPARE_OP": 2,
"LOAD_GLOBAL": 5,
"BINARY_OP": 1,
"LOAD_METHOD": 10,
"PRECALL": 1,
"CALL": 4,
}
elif sys.version_info >= (3, 12) and sys.version_info < (3, 13):
# Cache for some opcodes, it's for Python 3.12
# https://github.com/python/cpython/blob/3.12/Include/internal/pycore_opcode.h#L34-L47
return {
"BINARY_SUBSCR": 1,
"STORE_SUBSCR": 1,
"UNPACK_SEQUENCE": 1,
"FOR_ITER": 1,
"STORE_ATTR": 4,
"LOAD_ATTR": 9,
"COMPARE_OP": 1,
"LOAD_GLOBAL": 4,
"BINARY_OP": 1,
"SEND": 1,
"LOAD_SUPER_ATTR": 1,
"CALL": 3,
}
elif sys.version_info >= (3, 13) and sys.version_info < (3, 14):
# Cache for some opcodes, it's for Python 3.13
# https://github.com/python/cpython/blob/3.13/Include/internal/pycore_opcode_metadata.h#L1598-L1618
return {
"JUMP_BACKWARD": 1,
"TO_BOOL": 3,
"BINARY_SUBSCR": 1,
"STORE_SUBSCR": 1,
"SEND": 1,
"UNPACK_SEQUENCE": 1,
"STORE_ATTR": 4,
"LOAD_GLOBAL": 4,
"LOAD_SUPER_ATTR": 1,
"LOAD_ATTR": 9,
"COMPARE_OP": 1,
"CONTAINS_OP": 1,
"POP_JUMP_IF_TRUE": 1,
"POP_JUMP_IF_FALSE": 1,
"POP_JUMP_IF_NONE": 1,
"POP_JUMP_IF_NOT_NONE": 1,
"FOR_ITER": 1,
"CALL": 3,
"BINARY_OP": 1,
}
elif sys.version_info >= (3, 14) and sys.version_info < (3, 15):
# Cache for some opcodes, it's for Python 3.14
# https://github.com/python/cpython/blob/3.14/Include/internal/pycore_opcode_metadata.h#L1764-L1784
return {
"TO_BOOL": 3,
"STORE_SUBSCR": 1,
"SEND": 1,
"UNPACK_SEQUENCE": 1,
"STORE_ATTR": 4,
"LOAD_GLOBAL": 4,
"LOAD_SUPER_ATTR": 1,
"LOAD_ATTR": 9,
"COMPARE_OP": 1,
"CONTAINS_OP": 1,
"JUMP_BACKWARD": 1,
"POP_JUMP_IF_TRUE": 1,
"POP_JUMP_IF_FALSE": 1,
"POP_JUMP_IF_NONE": 1,
"POP_JUMP_IF_NOT_NONE": 1,
"FOR_ITER": 1,
"CALL": 3,
"CALL_KW": 3,
"BINARY_OP": 5,
}
elif sys.version_info >= (3, 15):
raise NotImplementedError(
f"Need to supplement cache operation code, for Python {sys.version_info}"
)
else:
return {}
PYOPCODE_CACHE_SIZE = _get_pyopcode_cache_size()
class ExceptionHandler:
opcode = 257
opname = "EXCEPT_HANDLER"
def _get_binary_op_arg_map() -> dict[str, int]:
if sys.version_info < (3, 11):
return {}
res = {}
for i, op in enumerate(opcode._nb_ops):
res[op[0]] = i
return res
BINARY_OP_ARG_MAP: dict[str, int] = _get_binary_op_arg_map()
FUSED_INSTS: dict[str, tuple[str, str]] = {
"LOAD_FAST_LOAD_FAST": ("LOAD_FAST", "LOAD_FAST"),
"LOAD_FAST_BORROW_LOAD_FAST_BORROW": (
"LOAD_FAST_BORROW",
"LOAD_FAST_BORROW",
),
"STORE_FAST_STORE_FAST": ("STORE_FAST", "STORE_FAST"),
"STORE_FAST_LOAD_FAST": ("STORE_FAST", "LOAD_FAST"),
}
TO_FUSED_INSTS = {v: k for k, v in FUSED_INSTS.items()}
@@ -0,0 +1,93 @@
# Copyright (c) 2023 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 ...utils import Singleton
class StackAnalyser(metaclass=Singleton):
def stack_effect(self, instr):
if "BINARY" in instr.opname or "INPLACE" in instr.opname:
return 2, 1
elif "UNARY" in instr.opname:
return 1, 1
return getattr(self, instr.opname)(instr)
def LOAD_GLOBAL(self, instr):
return 0, 1
def LOAD_CONST(self, instr):
return 0, 1
def LOAD_FAST(self, instr):
return 0, 1
def LOAD_ATTR(self, instr):
return 1, 1
def LOAD_METHOD(self, instr):
return 1, 2
def STORE_FAST(self, instr):
return 1, 0
def BUILD_TUPLE(self, instr):
return instr.arg, 1
def BUILD_LIST(self, instr):
return instr.arg, 1
def BUILD_SLICE(self, instr):
if instr.arg == 3:
return 3, 1
else:
return 2, 1
def UNPACK_SEQUENCE(self, instr):
return 1, instr.arg
def CALL_FUNCTION(self, instr):
return instr.arg + 1, 1
def DUP_TOP(self, instr):
return 0, 1
def DUP_TOP_TWO(self, instr):
return 0, 2
def ROT_N(self, instr):
return 0, 0
def ROT_TWO(self, instr):
return 0, 0
def ROT_THREE(self, instr):
return 0, 0
def ROT_FOUR(self, instr):
return 0, 0
def GET_ITER(self, instr):
return 1, 1
def POP_TOP(self, instr):
return 1, 0
def PUSH_NULL(self, instr):
return 0, 1
def NOP(self, instr):
return 0, 0
def EXTENDED_ARG(self, instr):
return 0, 0
@@ -0,0 +1,167 @@
# Copyright (c) 2023 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.
import _collections_abc
import _weakrefset
import abc
import codecs
import collections
import contextlib
import copy
import copyreg
import dataclasses
import enum
import functools
import genericpath
import importlib
import inspect
import linecache
import logging
import multiprocessing
import operator
import os
import posixpath
import random
import re
import selectors
import signal
import sys
import tempfile
import threading
import tokenize
import traceback
import types
import typing
import unittest
import uuid
import warnings
import weakref
import google.protobuf
import numpy
import setuptools
import paddle
NEED_SKIP_THIRD_PARTY_MODULES = {
abc,
collections,
contextlib,
copy,
copyreg,
dataclasses,
enum,
functools,
google.protobuf,
importlib,
inspect,
linecache,
logging,
multiprocessing,
numpy,
operator,
os,
posixpath,
random,
re,
selectors,
signal,
tempfile,
threading,
tokenize,
traceback,
types,
typing,
unittest,
weakref,
_collections_abc,
_weakrefset,
codecs,
uuid,
setuptools,
warnings,
genericpath,
}
if sys.version_info < (3, 11):
import sre_compile
import sre_parse
NEED_SKIP_THIRD_PARTY_MODULES.add(sre_compile)
NEED_SKIP_THIRD_PARTY_MODULES.add(sre_parse)
if sys.version_info < (3, 12):
import distutils
NEED_SKIP_THIRD_PARTY_MODULES.add(distutils)
def _extend_skip_modules_if_exists(module_name: str):
"""Extend skip modules set if the module exists."""
try:
module = importlib.import_module(module_name)
NEED_SKIP_THIRD_PARTY_MODULES.add(module)
except ImportError:
pass
# Some modules may not be installed in the environment, so we try to import them.
_extend_skip_modules_if_exists("coverage")
_extend_skip_modules_if_exists("colorama")
def _strip_init_py(s):
return re.sub(r"__init__.py$", "", s)
def _module_dir(m: types.ModuleType):
module_file = getattr(m, "__file__", None)
if module_file is None:
return None
return _strip_init_py(module_file)
skip_file_names = {
path
for path in [_module_dir(m) for m in NEED_SKIP_THIRD_PARTY_MODULES]
if path is not None
}
sot_path = os.path.dirname(__file__).rpartition(os.sep)[0] + os.sep
paddle_path = sys.modules["paddle"].__file__.rpartition(os.sep)[0] + os.sep
skip_file_names.add(sot_path)
skip_file_names.add(paddle_path)
skip_file_names.add(
"<frozen importlib",
)
skip_file_names.add("<__array_function__ internals>")
skip_file_name_re = re.compile(
f"^({'|'.join(map(re.escape, skip_file_names))})"
)
no_skip_code = {paddle.nn.Sequential.forward.__code__}
with_graph_codes = (
paddle.nn.Layer.__call__.__code__,
paddle.nn.Layer._dygraph_call_func.__code__,
)
def setup_skip_files():
paddle.framework.core.eval_frame_skip_file_prefix(tuple(skip_file_names))
paddle.framework.core.eval_frame_no_skip_codes(tuple(no_skip_code))
paddle.framework.core.sot_setup_codes_with_graph(with_graph_codes)
@@ -0,0 +1,21 @@
# Copyright (c) 2024 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 paddle.jit.profiler import (
EventGuard as EventGuard,
SotProfiler as SotProfiler,
event_register as event_register,
)
from .kernel_stats import SotStepProfilerGuard as SotStepProfilerGuard
@@ -0,0 +1,236 @@
# Copyright (c) 2024 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 atexit
import re
from dataclasses import dataclass
from enum import Enum
from paddle import profiler
from paddle.base.core import tracer_event_type_to_string
from ..utils.envs import ENV_ENABLE_SOT_STEP_PROFILER
class EventVisitor:
def visit(self, event_node):
event_type_name = tracer_event_type_to_string(event_node.type)
visit_method_name = f"visit_{event_type_name}"
if not hasattr(self, visit_method_name):
self.generic_visit(event_node)
return
getattr(self, visit_method_name)(event_node)
def generic_visit(self, event_node):
for child in event_node.children_node:
self.visit(child)
def __call__(self, events):
for event_node in events.values():
self.visit(event_node)
class KernelRunMode(Enum):
Dygraph = 1
Static = 2
def safe_divide(a, b):
# Avoid division by zero
return a / b if b != 0 else 0
@dataclass
class KernelInfo:
name: str
run_mode: KernelRunMode
duration: float
cuda_kernels: list[CudaKernelInfo]
@dataclass
class CudaKernelInfo:
name: str
duration: float
# TODO(SigureMo): Split into multiple files by visitor type
class KernelStatsVisitor(EventVisitor):
SKIP_KERNEL_NAMES = {"full", "full_int_array", "shadow_feed"}
KERNEL_NAME_REGEX = re.compile("(?P<kernel_name>.+) kernel launch")
def __init__(self):
self.kernels = []
def get_kernel_name(self, event_name):
if match_obj := self.KERNEL_NAME_REGEX.match(event_name):
return match_obj.group("kernel_name")
raise ValueError(f"Unexpected event name: {event_name}")
def calc_kernel_count(self, mode):
return len(
[
kernel
for kernel in self.kernels
if (
kernel.run_mode == mode
and kernel.name not in KernelStatsVisitor.SKIP_KERNEL_NAMES
)
]
)
def calc_kernel_duration(self, mode):
return sum(
[
kernel.duration
for kernel in self.kernels
if (
kernel.run_mode == mode
and kernel.name not in KernelStatsVisitor.SKIP_KERNEL_NAMES
)
]
)
def find_all_cuda_kernels(self, host_event):
# TODO(SigureMo): Find a better way to find all CUDA kernels
return [
CudaKernelInfo(
device_event.name, device_event.end_ns - device_event.start_ns
)
for runtime_event in host_event.runtime_node
for device_event in runtime_event.device_node
]
def visit_DygraphKernelLaunch(self, event_node):
duration = event_node.end_ns - event_node.start_ns
kernel_name = self.get_kernel_name(event_node.name)
all_cuda_kernels = self.find_all_cuda_kernels(event_node)
self.kernels.append(
KernelInfo(
kernel_name, KernelRunMode.Dygraph, duration, all_cuda_kernels
)
)
self.generic_visit(event_node)
def visit_StaticKernelLaunch(self, event_node):
duration = event_node.end_ns - event_node.start_ns
kernel_name = self.get_kernel_name(event_node.name)
all_cuda_kernels = self.find_all_cuda_kernels(event_node)
self.kernels.append(
KernelInfo(
kernel_name, KernelRunMode.Static, duration, all_cuda_kernels
)
)
self.generic_visit(event_node)
def summary(self) -> str:
static_kernel_duration = self.calc_kernel_duration(KernelRunMode.Static)
dygraph_kernel_duration = self.calc_kernel_duration(
KernelRunMode.Dygraph
)
static_kernel_count = self.calc_kernel_count(KernelRunMode.Static)
dygraph_kernel_count = self.calc_kernel_count(KernelRunMode.Dygraph)
percentage_static_kernel_count = safe_divide(
static_kernel_count, static_kernel_count + dygraph_kernel_count
)
percentage_static_kernel_duration = safe_divide(
static_kernel_duration,
static_kernel_duration + dygraph_kernel_duration,
)
step_summary = ""
step_summary += f"dygraph kernel count: {dygraph_kernel_count}\n"
step_summary += f"static kernel count: {static_kernel_count}\n"
step_summary += f"percentage static kernel count: {percentage_static_kernel_count:.2%}\n"
step_summary += f"dygraph kernel duration: {dygraph_kernel_duration / 1000000:.2f} ms\n"
step_summary += f"static kernel duration: {static_kernel_duration / 1000000:.2f} ms\n"
step_summary += f"percentage static kernel duration: {percentage_static_kernel_duration:.2%}\n"
return step_summary
class SotStepProfilerGuard:
EXPORT_CHROME_TRACING_PATH = "./sot-chrome-tracing/"
STEP_CNT = 0
LAST_INFO_SUMMARY = None
def __init__(self, enable_kernel_stats=True, enable_chrome_tracing=False):
self.enable_kernel_stats = enable_kernel_stats
self.enable_chrome_tracing = enable_chrome_tracing
self.started = False
self.record_event = None
self.summary = None
def _kernel_stats(self, prof) -> str:
kernel_stats_visitor = KernelStatsVisitor()
kernel_stats_visitor(prof.profiler_result.get_data())
return kernel_stats_visitor.summary()
def collect_step_info_summary(self, prof) -> str:
summary = ""
if self.enable_kernel_stats:
summary += self._kernel_stats(prof)
if self.enable_chrome_tracing:
# If you want to export chrome tracing, you can enable this flag
# and view the tracing result in https://ui.perfetto.dev/#!/viewer
profiler.export_chrome_tracing(
SotStepProfilerGuard.EXPORT_CHROME_TRACING_PATH,
f"step_{SotStepProfilerGuard.STEP_CNT:03d}",
)(prof)
return summary
def on_trace_ready(self, prof):
summary = self.collect_step_info_summary(prof)
if SotStepProfilerGuard.STEP_CNT == 0:
coldstart_title = f"SOT step profiler info summary (ColdStart, step#{SotStepProfilerGuard.STEP_CNT}):"
coldstart_report = f"{coldstart_title}\n{summary}"
print(coldstart_report)
self.summary = coldstart_report
else:
warmup_title = f"SOT step profiler info summary (Warmup, step#{SotStepProfilerGuard.STEP_CNT}):"
warmup_report = f"{warmup_title}\n{summary}"
if SotStepProfilerGuard.LAST_INFO_SUMMARY is None:
atexit.register(
lambda: print(SotStepProfilerGuard.LAST_INFO_SUMMARY)
)
SotStepProfilerGuard.LAST_INFO_SUMMARY = warmup_report
self.summary = warmup_report
def start(self):
if ENV_ENABLE_SOT_STEP_PROFILER.get():
self.profiler = profiler.Profiler(
targets=[
profiler.ProfilerTarget.CPU,
profiler.ProfilerTarget.GPU,
],
on_trace_ready=self.on_trace_ready,
)
self.profiler.start()
self.started = True
def stop(self):
if self.started:
assert self.profiler is not None
self.profiler.stop()
self.profiler = None # Avoid to hold the profiler instance
SotStepProfilerGuard.STEP_CNT += 1
def __enter__(self):
self.start()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.stop()
+68
View File
@@ -0,0 +1,68 @@
# Copyright (c) 2023 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 builtins
from typing import TYPE_CHECKING, TypeVar
from typing_extensions import ParamSpec
if TYPE_CHECKING:
from collections.abc import Callable
from types import CodeType
T = TypeVar("T")
P = ParamSpec("P")
NO_BREAKGRAPH_CODES: set[CodeType] = set()
NO_FALLBACK_CODES: set[CodeType] = set()
def assert_true(input: bool):
assert input
def print(*args, **kwargs):
builtins.print("[Dygraph]", *args, **kwargs)
def breakpoint():
import paddle
old = paddle.framework.core.set_eval_frame(None)
builtins.breakpoint() # noqa: T100
paddle.framework.core.set_eval_frame(old)
def check_no_breakgraph(fn: Callable[P, T]) -> Callable[P, T]:
NO_BREAKGRAPH_CODES.add(fn.__code__)
return fn
def breakgraph():
pass
def check_no_fallback(fn: Callable[P, T]) -> Callable[P, T]:
NO_FALLBACK_CODES.add(fn.__code__)
return fn
def fallback(recursive=False):
pass
def in_sot():
return False
+192
View File
@@ -0,0 +1,192 @@
# Copyright (c) 2023 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
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any
from ..utils import log
from .compile_cache import CompileSIRCache
from .statement_ir import (
ApiStatement,
ASTStatement,
CallStatement,
LayerStatement,
MethodStatement,
ParametersHolder,
StatementContext,
StatementIR,
StatementIRFactory,
Symbol,
)
if TYPE_CHECKING:
from collections.abc import Callable
from paddle.static import InputSpec
class StatementIRBuilder:
"""
A class to build a StatementIR.
"""
def __init__(self):
self.reset()
def reset(self):
"""
Reset the context.
"""
self.statement_factory = StatementIRFactory()
self._current_statement_ctxs = []
self._current_sir: StatementIR = self.statement_factory.create()
@property
def current_sir(self) -> StatementIR:
"""
Get the current SIR.
"""
return self._current_sir
def replace_current_sir(self, sir: StatementIR):
"""
Replace the current SIR with a new SIR.
"""
self._current_sir = sir
self.statement_factory.update(sir)
def call_SIR(self, sirname, inputs, outputs, stacks):
"""
Call a SIR, which is a subgraph.
"""
stmt = CallStatement(
sirname, inputs, outputs, list(self._current_statement_ctxs), stacks
)
self.current_sir.add_statement(stmt)
def call_API(self, api, inputs, outputs, stacks):
"""
Call a paddle api.
"""
assert callable(api), "call_API must receive a paddle api."
stmt = ApiStatement(
api, inputs, outputs, list(self._current_statement_ctxs), stacks
)
self.current_sir.add_statement(stmt)
def call_METHOD(self, method_name, inputs, outputs, stacks):
"""
Call a method of a api. The API here can be python or Paddle
"""
assert isinstance(method_name, str), (
"call_METHOD must method api name. string."
)
assert isinstance(inputs[0][0], Symbol), (
"call_METHOD first argument must be Symbol Variable."
)
stmt = MethodStatement(
method_name,
inputs,
outputs,
list(self._current_statement_ctxs),
stacks,
)
self.current_sir.add_statement(stmt)
def call_LAYER(self, layer, inputs, outputs, stacks):
"""
Call a layer of a api.
"""
stmt = LayerStatement(
layer, inputs, outputs, list(self._current_statement_ctxs), stacks
)
self.current_sir.add_statement(stmt)
def call_AST(self, static_function, inputs, outputs, stacks):
stmt = ASTStatement(
static_function,
inputs,
outputs,
list(self._current_statement_ctxs),
stacks,
)
self.current_sir.add_statement(stmt)
def get_sir(self, name: str):
"""
Get a SIR from statement_factory.
Args:
name (str): the name of SIR.
Returns:
StatementIR: the SIR.
"""
return self.statement_factory[name]
@contextmanager
def attach_statement_context_guard(self, ctx: StatementContext):
"""
Attach a statement context to the current SIR.
"""
self._current_statement_ctxs.append(ctx)
try:
yield
finally:
self._current_statement_ctxs.pop()
def finalize(self, ret_vals):
current_sir: StatementIR = self.current_sir
current_sir.inputs, current_sir.params = current_sir.analyse_inputs()
current_sir.outputs = ret_vals
log(2, "start subgraph compile and execution.\n")
log(2, current_sir, "\n")
return current_sir
def compile_do_nothing(self) -> Callable[..., Any]:
"""
Return a dummy function, which will return an empty list.
Args:
ret_vals (list[Symbol]): the return values of the function.
"""
class DummyFunc:
def __call__(*args, **kwargs):
return []
def graph_size(self):
return 0
return DummyFunc()
def compile_fn(
self,
sir_name: str,
parameters_holder: ParametersHolder,
input_spec: tuple[InputSpec | None, ...],
**kwargs,
):
"""
start compile and return the python function, which must can be to_static without errors.
"""
static_func = CompileSIRCache()(
self, sir_name, parameters_holder, input_spec, **kwargs
)
return static_func
@@ -0,0 +1,322 @@
# Copyright (c) 2023 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 inspect
from typing import TYPE_CHECKING
import paddle
from paddle.jit.profiler import EventGuard, event_register
from ..infer_meta import convert_meta_to_input_spec
from ..utils import (
ENV_SOT_EXPORT,
Cache,
InfoCollector,
NewSymbolHitRateInfo,
Singleton,
SIRToCodeMap,
StepInfoManager,
SubGraphInfo,
SubGraphRelationInfo,
log_do,
)
from .export import export
from .interpreter import compile_sir
if TYPE_CHECKING:
from paddle.static import InputSpec, Program
from .builder import StatementIRBuilder
from .statement_ir import ParametersHolder
def trace_back_frames():
frame = inspect.currentframe()
while frame.f_back is not None:
frame = frame.f_back
code = frame.f_code
paddle.framework.core.sot_set_with_graph(code)
def clear_eager_tensor_name(output_tensors):
for output_tensor in output_tensors:
output_tensor.name = ""
def _is_builtin_op(op):
dialect_name, opname = op.name().split(".")
return dialect_name == "builtin"
def _is_computation_op(op):
return not _is_builtin_op(op) and op.name() not in ["pd_op.data"]
class UniqueIdGenerator:
def __init__(self):
self._id = 0
def generate(self):
self._id += 1
return self._id
def __call__(self):
return self.generate()
class TensorIdAllocator(metaclass=Singleton):
TENSOR_ID_ATTR = "__tensor_id__"
def __init__(self):
self._id_generator = UniqueIdGenerator()
def allocate(self, tensor):
if not hasattr(tensor, self.TENSOR_ID_ATTR):
setattr(tensor, self.TENSOR_ID_ATTR, self._id_generator())
return getattr(tensor, self.TENSOR_ID_ATTR)
class FallbackWrapper:
"""
Used to store and call static graph methods generated by paddle.jit.to_static
"""
def __init__(self, compiled_fn, SIR, is_training: bool):
self.compiled_fn = compiled_fn
self.partial_program = None
self.concrete_program = None
self.SIR = SIR # for debug
self.is_training = is_training
self.exported = False
self.is_first_call = True
def graph_size(self):
if self.partial_program is None:
input_spec = convert_meta_to_input_spec(
tuple(
self.SIR.symbol_meta_map[symbol]
for symbol in self.SIR.inputs
)
)
(
self.concrete_program,
self.partial_program,
) = self.compiled_fn.get_concrete_program(input_spec)
self.partial_program.training = self.is_training
global_block_ops = self.concrete_program.main_program.global_block().ops
non_builtin_ops = list(filter(_is_computation_op, global_block_ops))
return len(non_builtin_ops)
def collect_new_symbol_hit_rate(self, inputs, outputs):
if not InfoCollector().need_collect(NewSymbolHitRateInfo):
return
input_tensor_ids = []
output_tensor_ids = []
assert len(inputs) == 1
assert isinstance(inputs[0], tuple)
for i, arg in enumerate(inputs[0]):
assert isinstance(arg, paddle.Tensor), f"Expect Tensor, got {arg}"
tensor_id = TensorIdAllocator().allocate(arg)
input_tensor_ids.append(tensor_id)
for i, out in enumerate(outputs):
assert isinstance(out, paddle.Tensor)
tensor_id = TensorIdAllocator().allocate(out)
output_tensor_ids.append(tensor_id)
InfoCollector().attach(
NewSymbolHitRateInfo, input_tensor_ids, output_tensor_ids
)
def collect_subgraph_relation(self, inputs, outputs, partial_program_layer):
if not InfoCollector().need_collect(SubGraphRelationInfo):
return
input_shape_infos = []
output_shape_infos = []
forward_input_values = partial_program_layer.program.program_attr['fx']
forward_output_values = partial_program_layer.program.program_attr['fo']
assert len(inputs) == 1
assert isinstance(inputs[0], tuple)
assert len(inputs[0]) == len(forward_input_values)
assert len(outputs) == len(forward_output_values)
for i, arg in enumerate(inputs[0]):
assert isinstance(arg, paddle.Tensor), f"Expect Tensor, got {arg}"
tensor_id = TensorIdAllocator().allocate(arg)
input_ir_shape = forward_input_values[i].shape
input_real_shape = arg.shape
input_shape_info = SubGraphRelationInfo.ConcreteShapeInfo(
tensor_id, input_ir_shape, input_real_shape
)
input_shape_infos.append(input_shape_info)
for i, out in enumerate(outputs):
assert isinstance(out, paddle.Tensor)
tensor_id = TensorIdAllocator().allocate(out)
output_ir_shape = forward_output_values[
partial_program_layer._outputs.quick_index_map[i]
].shape
output_real_shape = out.shape
output_shape_info = SubGraphRelationInfo.ConcreteShapeInfo(
tensor_id, output_ir_shape, output_real_shape
)
output_shape_infos.append(output_shape_info)
InfoCollector().attach(
SubGraphRelationInfo,
self.SIR.name,
input_shape_infos,
output_shape_infos,
self.is_first_call,
self.graph_size(),
)
def collect_subgraph_info(self, program: Program):
if not InfoCollector().need_collect(SubGraphInfo):
return
InfoCollector().attach(
SubGraphInfo,
str(program),
self.graph_size(),
self.SIR.name,
)
def update_compile_time_info(self, SIR, partial_program_layer):
if not self.is_first_call:
return
from ..opcode_translator.executor.executor_cache import (
OpcodeExecutorCache,
)
code = SIRToCodeMap().get(SIR)
assert code is not None, f"Cannot find code for SIR: {SIR}"
OpcodeExecutorCache().compile_time_stats.setdefault(code, 0)
OpcodeExecutorCache().compile_time_stats[code] += (
partial_program_layer._compile_time_counter.get_total_time()
)
@event_register(
lambda self, *args, **kwargs: f"FallbackWrapper: {self.SIR.name}"
)
def __call__(self, *args, **kwargs):
if StepInfoManager().need_back_trace:
trace_back_frames()
log_do(
2,
lambda: print("[FallbackWrapper] start run SIR: \n", self.SIR),
)
log_do(
4,
lambda: print(
self.compiled_fn.get_concrete_program(*args, **kwargs)[
1
].train_program
),
)
if self.partial_program is None:
with EventGuard("FallbackWrapper: get_concrete_program"):
(
self.concrete_program,
self.partial_program,
) = self.compiled_fn.get_concrete_program(*args, **kwargs)
self.partial_program.training = self.is_training
outputs = self.partial_program.sot_call(*args, **kwargs)
clear_eager_tensor_name(outputs)
log_do(
4,
lambda: print("[CompileCache] run sir forward success."),
)
self.collect_new_symbol_hit_rate(args, outputs)
self.collect_subgraph_relation(args, outputs, self.partial_program)
self.collect_subgraph_info(self.concrete_program.main_program)
self.update_compile_time_info(self.SIR, self.partial_program)
if ENV_SOT_EXPORT.get() != "" and not self.exported:
export(self.SIR, ENV_SOT_EXPORT.get())
self.exported = True
self.is_first_call = False
return outputs
class CompileSIRCache(Cache, metaclass=Singleton):
"""
Cache the compiled function of SIR
"""
def __init__(self):
super().__init__(weak=False)
def key_fn(
self,
builder: StatementIRBuilder,
sir_name: str,
parameters_holder: ParametersHolder,
input_spec: tuple[InputSpec | None, ...],
**kwargs,
):
"""
generate a hash key for a SIR
Args:
context: The context to compile
sir_name: The name of the sir to compile
build_strategy: The build strategy to compile
Returns:
The hash key of the SIR
"""
sir = builder.get_sir(sir_name)
# NOTE(dev): Is str(sir) a heavy operation ?
hash_key = hash(
(str(sir), *input_spec, id(parameters_holder), kwargs['training'])
)
return hash_key
def value_fn(
self,
builder: StatementIRBuilder,
sir_name: str,
parameters_holder: ParametersHolder,
input_spec: tuple[InputSpec | None, ...],
**kwargs,
):
"""
Generate static graph function
Args:
context: The context to compile
sir_name: The name of the sir to compile
build_strategy: The build strategy to compile
Returns:
The static graph function
"""
build_strategy = kwargs.get("build_strategy", None)
backend = kwargs.get("backend", None)
return FallbackWrapper(
paddle.jit.to_static(
compile_sir(builder, sir_name, parameters_holder),
input_spec=[input_spec],
build_strategy=build_strategy,
backend=backend,
full_graph=True,
),
builder.get_sir(sir_name),
is_training=kwargs['training'],
)
+387
View File
@@ -0,0 +1,387 @@
# Copyright (c) 2024 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.
import os
from itertools import chain
import paddle
from paddle.utils import flatten
from ..utils import ConstTypes, ExportError, NameGenerator, get_api_fullname
from .statement_ir import Symbol
class PyStatement:
tab = " " * 4
def __init__(self, *lines):
self.sub_statement = []
self.lines = lines
def get_lines(self, prefix=""):
lines = [prefix + line for line in self.lines]
for statement in self.sub_statement:
lines.extend(statement.get_lines(self.tab + prefix))
return lines
def add_sub(self, *lines):
sub = PyStatement(*lines)
self.sub_statement.append(sub)
return sub
def __str__(self):
return "\n".join(self.get_lines())
class NameGener:
def __init__(self, SIR):
self.SIR = SIR
self.name_map = {}
self.param_name_generator = NameGenerator("self.parameter_")
self.non_param_name_generator = NameGenerator("var_")
def __call__(self, var):
return self.get_str(var)
def get_str(self, var):
if isinstance(var, list):
return self.get_list_str(var)
elif isinstance(var, tuple):
return self.get_tuple_str(var)
elif isinstance(var, dict):
return self.get_dict_str(var)
elif isinstance(var, set):
return self.get_set_str(var)
else:
return self.get_obj_str(var)
def get_list_str(self, list_):
return "[{}]".format(", ".join(self.get_str(var) for var in list_))
def get_tuple_str(self, tuple_):
return "({},)".format(", ".join(self.get_str(var) for var in tuple_))
def get_dict_str(self, dict_):
return "{{{},}}".format(
", ".join(
f"{self.get_str(k)}: {self.get_str(v)}"
for k, v in dict_.items()
)
)
def get_set_str(self, set_):
return "{{{},}}".format(", ".join(self.get_str(var) for var in set_))
def get_obj_str(self, var):
if isinstance(var, Symbol):
if var not in self.name_map:
self.register_symbol(var)
return self.name_map[var]
elif isinstance(var, str):
return f"'{var}'"
else:
return str(var)
def register_symbol(self, symbol):
if symbol in self.SIR.param_symbol:
name = self.param_name_generator.next()
else:
name = self.non_param_name_generator.next()
self.name_map[symbol] = name
class PyFileGen:
def __init__(self, SIR):
self.SIR = SIR
self.roots = []
self.name_gener = NameGener(self.SIR)
self.SIR_sig = "||".join(
f"{stmt.type}:{stmt.name}" for stmt in SIR.statements
)
def new_root(self, *args):
stmt = PyStatement(*args)
self.roots.append(stmt)
return stmt
def roots_to_string(self):
lines = []
for root in self.roots:
lines.extend(root.get_lines())
return "\n".join(lines)
def gen_py_codes(self):
self.check_exportable()
self.create_header()
self.new_root("\n")
self.create_layer()
self.new_root("\n")
self.create_inputs()
self.new_root("\n")
self.create_test()
self.new_root("\n")
self.create_tail()
return self.roots_to_string()
def is_exportable_type(self, value):
if (
isinstance(value, (ConstTypes, Symbol, paddle.dtype))
or value is Ellipsis # NOINT
):
return True
if isinstance(value, slice):
return (
self.is_exportable_type(value.start)
and self.is_exportable_type(value.stop)
and self.is_exportable_type(value.step)
)
return False
def check_exportable(self):
for stmt in self.SIR.statements:
for inp in flatten(stmt.inputs):
if not self.is_exportable_type(inp):
raise ExportError(
f"Not support create python file with input: {inp}"
)
def create_header(self):
self.new_root(
f"# {self.SIR_sig}",
"import paddle",
"import unittest",
"import numpy as np",
)
def create_layer(self):
layer_class = self.new_root("class LayerCase(paddle.nn.Layer):")
init_fn = layer_class.add_sub("def __init__(self):")
init_fn.add_sub("super().__init__()")
for param in self.SIR.param_symbol:
meta = self.SIR.symbol_meta_map[param].unwrap_unsafe()
init_fn.add_sub(
f"{self.name_gener(param)} = self.create_parameter(",
f" shape={meta.shape},",
f" dtype={meta.dtype},",
")",
)
for stmt in self.SIR.statements:
if stmt.type == "layer":
layer = stmt.layer()
init_fn.add_sub(self.init_sub_layer(layer))
forward_definition = ["def forward(", " self,"]
for inp in self.SIR.inputs:
if inp in self.SIR.non_param_symbol:
meta = self.SIR.symbol_meta_map[inp]
forward_definition.append(
f" {self.name_gener(inp)}, # {meta}"
)
forward_definition.append("):")
forward_fn = layer_class.add_sub(*forward_definition)
for stmt in self.SIR.statements:
forward_fn.add_sub(*self.create_stmt_line(stmt))
forward_fn.add_sub(
"return {}".format(
", ".join(self.name_gener(out) for out in self.SIR.outputs)
)
)
def create_inputs(self):
create_paddle_inputs = self.new_root("def create_paddle_inputs():")
self.new_root("\n")
create_numpy_inputs = self.new_root("def create_numpy_inputs():")
paddle_inputs = ["inputs = ("]
numpy_inputs = ["inputs = ("]
for inp in self.SIR.inputs:
if inp in self.SIR.non_param_symbol:
meta = self.SIR.symbol_meta_map[inp.name].unwrap_unsafe()
shape_str = "[1]" if len(meta.shape) == 0 else str(meta.shape)
if meta.dtype in (
paddle.int8,
paddle.int16,
paddle.int32,
paddle.int64,
):
paddle_inputs.append(
f" paddle.randint(low=0, high=10, shape={shape_str}, dtype={meta.dtype}),"
)
numpy_inputs.append(
" np.random.randint(low=0, high=10, size={}, dtype='{}'),".format(
shape_str, str(meta.dtype).replace('paddle.', '')
)
)
elif meta.dtype is paddle.bool:
paddle_inputs.append(
f" paddle.randint(low=0, high=2, shape={shape_str}, dtype=paddle.int32).cast(paddle.bool),"
)
numpy_inputs.append(
f" np.random.randint(low=0, high=2, size={shape_str}, dtype='int').astype('bool'),"
)
else:
paddle_inputs.append(
f" paddle.rand(shape={shape_str}, dtype={meta.dtype}),"
)
numpy_inputs.append(
" np.random.random(size={}).astype('{}'),".format(
shape_str, str(meta.dtype).replace('paddle.', '')
)
)
paddle_inputs.append(")")
paddle_inputs.append("return inputs")
numpy_inputs.append(")")
numpy_inputs.append("return inputs")
create_paddle_inputs.add_sub(*paddle_inputs)
create_numpy_inputs.add_sub(*numpy_inputs)
def create_test(self):
test_class = self.new_root("class TestLayer(unittest.TestCase):")
setup = test_class.add_sub("def setUp(self):")
setup.add_sub("self.inputs = create_paddle_inputs()")
setup.add_sub("self.net = LayerCase()")
train = test_class.add_sub(
"def train(self, net, to_static, with_prim=False, with_cinn=False):"
)
train.add_sub(
"if to_static:",
" paddle.base.core._set_prim_all_enabled(with_prim)",
" if with_cinn:",
' assert with_prim, "with_cinn=True but with_prim=False is unsupported"',
' net = paddle.jit.to_static(net, backend="CINN", full_graph=True)',
" else:",
" net = paddle.jit.to_static(net, backend=None, full_graph=True)",
"paddle.seed(123)",
"outs = net(*self.inputs)",
"return outs",
)
test_ast_cinn_static = test_class.add_sub(
"def test_ast_prim_cinn(self):"
)
test_ast_cinn_static.add_sub(
"st_out = self.train(self.net, to_static=True)",
"cinn_out = self.train(self.net, to_static=True, with_prim=True, with_cinn=True)",
"for st, cinn in zip(paddle.utils.flatten(st_out), paddle.utils.flatten(cinn_out)):",
" np.testing.assert_allclose(st.numpy(), cinn.numpy(), atol=1e-8)",
)
def create_tail(self):
self.new_root(
"if __name__ == '__main__':",
" unittest.main()",
)
def init_sub_layer(self, layer, layer_name):
# TODO @wuzhanfei need more efficient way to create a sub layer
# now, we just close call_Layer behavior
raise ExportError("Not support create sub layer now.")
def create_input_string(self, args, kwargs):
return ", ".join(
chain(
(self.name_gener(arg) for arg in args),
(f"{k}={self.name_gener(v)}" for k, v in kwargs.items()),
)
)
def create_unpack_output_string(self, outputs):
path = ["out"]
result = []
def search(outputs, path, result):
if isinstance(outputs, (list, tuple)):
search_sequence(outputs, path, result)
elif isinstance(outputs, dict):
search_dict(outputs, path, result)
elif isinstance(outputs, Symbol):
result.append(self.name_gener(outputs) + " = " + "".join(path))
def search_sequence(outputs, path, result):
for idx, out in enumerate(outputs):
path.append(f"[{idx}]")
search(out, path, result)
path.pop()
def search_dict(outputs, path, result):
for k, out in outputs.items():
path.append(f"[{k}]")
search(out, path, result)
path.pop()
search(outputs, path, result)
return result
def create_stmt_line(self, stmt):
return getattr(self, "create_" + stmt.type + "_stmt")(stmt)
def create_api_stmt(self, stmt):
args, kwargs = stmt.inputs
input_str = self.create_input_string(args, kwargs)
api = stmt.api
api_str = get_api_fullname(api)
if api_str is None:
raise ExportError(f"Can not find module of {api}")
if isinstance(stmt.outputs, Symbol):
return [f"{self.name_gener(stmt.outputs)} = {api_str}({input_str})"]
else:
compute_code = f"out = {api_str}({input_str})"
unpack_codes = self.create_unpack_output_string(stmt.outputs)
return [compute_code, *unpack_codes]
def create_method_stmt(self, stmt):
args, kwargs = stmt.inputs
input_str = self.create_input_string(args[1:], kwargs)
method_str = self.name_gener(args[0]) + "." + stmt.method
if isinstance(stmt.outputs, Symbol):
return [
f"{self.name_gener(stmt.outputs)} = {method_str}({input_str})"
]
else:
compute_code = f"out = {method_str}({input_str})"
unpack_codes = self.create_unpack_output_string(stmt.outputs)
return [compute_code, *unpack_codes]
def export(SIR, path):
try:
pygen = PyFileGen(SIR)
string = pygen.gen_py_codes()
except ExportError as e:
print(f"[SOT] Export {SIR.name} Failed:", e)
return
if not os.path.exists(path):
os.makedirs(path)
with open(os.path.join(path, f"{SIR.name}.py"), "w") as f:
f.write(string)
print(
f"[SOT] Export {SIR.name} Success with size {len(SIR.statements)}"
)
@@ -0,0 +1,220 @@
# Copyright (c) 2023 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
from typing import TYPE_CHECKING
import paddle
from paddle.jit.dy2static.utils import compose_guards
from paddle.utils import to_sequence
from ..utils import (
InnerError,
log_do,
map_if,
map_if_extend,
)
from .statement_ir import (
ParametersHolder,
StatementContext,
StatementContextRegistry,
Symbol,
)
if TYPE_CHECKING:
from .builder import StatementIRBuilder
from .statement_ir import Statement, StatementIR
def replace_symbol(
values: list[Symbol] | list[object], state: dict[str, Symbol]
):
"""
Replaces Symbol objects with their corresponding values.
Args:
values: A list of values that may contain Symbol objects.
state: A dict mapping Symbol names to their corresponding values.
Returns:
A new list with Symbol objects replaced by their corresponding values in the state dict.
"""
# deal with list / map etc.
values = map_if_extend(
values,
pred=lambda x: isinstance(x, Symbol),
true_fn=lambda x: state[x.name],
false_fn=lambda x: x,
)
return values
def _append_opstack_between(start, end, stack):
# The range is [start, end)
for op in for_each_ops_between(start, end):
op.callstack = stack
def for_each_ops_between(start, end):
# [start, end)
program = paddle.static.default_main_program()
ops = program.global_block().ops[start:end]
yield from ops
def opnum_in_program():
program = paddle.static.default_main_program()
return len(program.global_block().ops)
class Interpreter:
"""
Interpreter is used to interpret and execute SIR.
"""
def __init__(self, builder: StatementIRBuilder):
self._builder = builder
def get_sir(self, name: str) -> StatementIR:
"""
Returns the StatementIR object by given name.
Args:
name: The name of the StatementIR.
Returns:
The StatementIR object with the given name.
"""
return self._builder.get_sir(name)
def run_sir(self, name: str, state: dict[str, Symbol]):
"""
Runs the StatementIR with the given name using the provided state.
Args:
name: The name of the given StatementIR to run.
state: A dict mapping Symbol names to their corresponding values.
Returns:
A list of the Symbol of the StatementIR after execution.
"""
def _set(v, s):
state[s.name] = v
SIR = self.get_sir(name)
for stmt in SIR.statements:
stmt: Statement
before_stmt_opnum = opnum_in_program()
inputs = replace_symbol(stmt.inputs, state)
with create_context_guard(stmt.contexts)():
outs = getattr(self, stmt.type)(stmt, inputs)
if len(to_sequence(outs)) != len(to_sequence(stmt.outputs)):
raise InnerError("Number output mismatch, some error happen.")
log_do(
3,
lambda: _append_opstack_between(
before_stmt_opnum, opnum_in_program() + 1, stmt.stmt_stack
),
)
map_if(
outs,
stmt.outputs,
pred=lambda v, s: isinstance(s, Symbol),
true_fn=lambda v, s: _set(v, s),
false_fn=lambda v, s: None,
)
# fetch outputs
return replace_symbol(SIR.outputs, state)
def api(self, stmt, inputs):
args, kwargs = inputs
return stmt.api(*args, **kwargs)
def method(self, stmt, inputs):
args, kwargs = inputs
var = args[0]
return getattr(var, stmt.method)(*args[1:], **kwargs)
def layer(self, stmt, inputs):
args, kwargs = inputs
layer = stmt.layer()
assert layer is not None, "SIR bound layer is None."
return layer(*args, **kwargs)
def AST(self, stmt, inputs):
args, kwargs = inputs
return stmt.converted_func(*args, **kwargs)
def compile_sir(
builder: StatementIRBuilder, name: str, parameters_holder: ParametersHolder
):
"""
Compile a SIR to a new function
Args:
context: The context to compile
name: The name of the sir to compile
"""
@paddle.jit.not_to_static
def wrapper(args):
"""
This function will be decorated by paddle.to_static.
so the args is variables, not eager tensors.
"""
interpreter = Interpreter(builder)
SIR = interpreter.get_sir(name)
state = prepare_state(SIR, args, parameters_holder)
return interpreter.run_sir(name, state)
return wrapper
def prepare_state(
SIR: StatementIR, inputs, parameters_holder: ParametersHolder
):
state = {}
# bind inputs
assert len(SIR.inputs) == len(inputs), "Inputs length mismatch."
for sir_inp, inp in zip(SIR.inputs, inputs):
state[sir_inp.name] = inp
for sir_param in SIR.params:
state[sir_param.name] = paddle.base.dygraph.base._convert_into_variable(
parameters_holder.get(sir_param.name)
)
return state
def create_context_guard(contexts: list[StatementContext]):
guards = list(
map(
lambda ctx: (
lambda: StatementContextRegistry.get_context_guard(type(ctx))(
ctx
)
),
contexts,
)
)
return compose_guards(*guards)
@@ -0,0 +1,419 @@
# Copyright (c) 2023 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 functools
import weakref
from typing import TYPE_CHECKING, Any, TypeVar
from weakref import WeakValueDictionary
import paddle
from paddle.jit.dy2static.utils import parameters_persistent_mode_is_enabled
from paddle.jit.utils import OrderedSet
from paddle.utils import flatten, map_structure
from ..utils import (
InnerError,
NameGenerator,
Singleton,
flatten_extend,
get_api_fullname,
)
if TYPE_CHECKING:
from collections.abc import Callable
from contextlib import AbstractContextManager
_StatementContextT = TypeVar("_StatementContextT", bound="StatementContext")
class ParametersHolder:
def __init__(self):
self._params = WeakValueDictionary[
str, paddle.base.framework.EagerParamBase
]()
def set(self, name, param):
self._params[name] = param
def get(self, name):
if (param := self._params.get(name)) is None:
raise InnerError(
f"Parameter '{name}' not found in ParametersHolder."
)
return param
def copy(self):
new_holder = ParametersHolder()
new_holder._params = self._params.copy()
return new_holder
class Reference: # to unify weak_ref and strong_ref
def __init__(self, value, is_weak):
self.is_weak = is_weak
if is_weak is True:
self.ref = weakref.ref(value)
else:
self.ref = value
def __call__(self):
if self.is_weak is True:
return self.ref()
else:
return self.ref
class Symbol:
"""
Symbol is used to distinguish a string and a `math variable`.
"""
def __init__(self, name: str):
self.name = name
def __str__(self):
return self.name
def __repr__(self):
return str(self)
def __eq__(self, other):
if isinstance(other, str):
return self.name == other
return self.name == other.name
def __hash__(self):
return hash(self.name)
def __deepcopy__(self, memo=None):
return Symbol(self.name)
class StatementContext: ...
class StatementContextRegistry:
_ctx_map: dict[
type[Any],
Callable[[Any], AbstractContextManager[None]],
] = {}
@classmethod
def register_context_guard(
cls,
ctx_cls: type[_StatementContextT],
handler: Callable[[_StatementContextT], AbstractContextManager[None]],
):
"""
Register a context handler for the given context.
"""
if ctx_cls in cls._ctx_map:
raise ValueError(f"Context {ctx_cls} is already registered.")
cls._ctx_map[ctx_cls] = handler
@classmethod
def register_context(
cls,
handler: Callable[[_StatementContextT], AbstractContextManager[None]],
):
def decorator(ctx_cls: type[_StatementContextT]):
cls.register_context_guard(ctx_cls, handler)
return ctx_cls
return decorator
@classmethod
def get_context_guard(
cls,
ctx_cls: type[_StatementContextT],
) -> Callable[[_StatementContextT], AbstractContextManager[None]]:
"""
Get the context handler for the given context.
"""
if ctx_cls not in cls._ctx_map:
raise ValueError(f"Context {ctx_cls} is not registered.")
return cls._ctx_map[ctx_cls]
class Statement:
"""
Statement is used to represent a sentence of code for building the neural network model,
which has four types: "call", "api", "method", and "layer".
Note:
Statement temporarily does not support control flow.
"""
def __init__(
self,
type: str,
name: str,
inputs: list[Symbol],
outputs: list[Symbol],
contexts: list[StatementContext],
stacks: list[str],
):
assert type in ["call", "api", "method", "layer", "AST"]
self.name = name
self.inputs = inputs # (list of Symbols, dict of Symbols)
self.outputs = outputs # list of Symbol | PythonObj
self.contexts = contexts # list of StatementContext
self.stmt_stack = (
stacks # a list of string to record the source code callstack.
)
self.type = type
def __str__(self):
return "{} || {} = {} ({}) ".format(
self.type + " " * (10 - len(self.type)),
self.to_string(self.outputs),
self.name,
self.to_string(self.inputs),
)
def __repr__(self):
return self.__str__()
@staticmethod
def to_string(inps):
return ", ".join(repr(x) for x in flatten(inps))
class CallStatement(Statement):
def __init__(
self,
name: str,
inputs: list[Symbol],
outputs: list[Symbol],
contexts: list[StatementContext],
stacks: list[str],
):
super().__init__("call", name, inputs, outputs, contexts, stacks)
self.sir_name = name
class ApiStatement(Statement):
def __init__(
self,
api: Callable,
inputs: list[Symbol],
outputs: list[Symbol],
contexts: list[StatementContext],
stacks: list[str],
):
fullname = get_api_fullname(api)
if fullname is None:
fullname = "paddle." + api.__name__
super().__init__("api", fullname, inputs, outputs, contexts, stacks)
self.api = api
class MethodStatement(Statement):
def __init__(
self,
name: str,
inputs: list[Symbol],
outputs: list[Symbol],
contexts: list[StatementContext],
stacks: list[str],
):
super().__init__("method", name, inputs, outputs, contexts, stacks)
self.method = name
class LayerStatement(Statement):
def __init__(
self,
layer: Reference, # Reference of paddle.nn.Layer
inputs: list[Symbol],
outputs: list[Symbol],
contexts: list[StatementContext],
stacks: list[str],
):
if isinstance(layer, Reference):
name = layer().__class__.__name__
else:
name = layer.__class__.__name__
super().__init__(
"layer",
name,
inputs,
outputs,
contexts,
stacks,
)
self.layer = layer
class ASTStatement(Statement):
def __init__(
self,
static_function,
inputs: list[Symbol],
outputs: list[Symbol],
contexts: list[StatementContext],
stacks: list[str],
):
# this dygraph_function always has attr __code__, which is checked before
dygraph_func = static_function.dygraph_function
super().__init__(
"AST",
dygraph_func.__code__.co_name,
inputs,
outputs,
contexts,
stacks,
)
converted_func = paddle.jit.dy2static.convert_to_static(dygraph_func)
func_self = getattr(dygraph_func, '__self__', None)
if func_self is not None:
converted_func = functools.partial(converted_func, func_self)
self.converted_func = converted_func
class StatementIR:
"""
StatementIR is the carrier that records the code for building the neural network model.It is
a representation of a purely computational structure, and does not care about specific values.
The function converted from StatementIR can ensure that it can be turned into a static state.
In this way, we can reuse the original `to_static` function to realize the execution of the static graph.
Note:
Don't create by yourself, just use the StatementIRFactory.create()
"""
def __init__(self, name: str):
self.name = name
self.inputs: list[Symbol] = []
self.params: list[Symbol] = []
self.outputs: list[Symbol] = []
self.statements: list[Statement] = []
self.symbol_meta_map = {}
self.param_symbol = set()
self.non_param_symbol = set()
@property
def input_with_params(self):
return self.inputs + self.params
def __len__(self):
return len(self.statements)
def __deepcopy__(self, memo=None):
new_sir = StatementIR(self.name)
new_sir.inputs = list(self.inputs)
new_sir.params = list(self.params)
new_sir.outputs = list(self.outputs)
new_sir.statements = list(self.statements)
new_sir.symbol_meta_map = dict(self.symbol_meta_map.items())
new_sir.param_symbol = set(self.param_symbol)
new_sir.non_param_symbol = set(self.non_param_symbol)
return new_sir
def set_parameter_info(self, params, non_params):
self.param_symbol.update(params)
self.non_param_symbol.update(non_params)
def set_symbol_meta_map(self, meta_map):
# if the meta of a input symbol inplace changed, we should get the origin meta as input of SIR
meta_map.update(self.symbol_meta_map)
self.symbol_meta_map = meta_map
def add_input(self, input):
self.inputs.append(input)
def add_output(self, output):
self.outputs.append(output)
def add_statement(self, statement):
assert isinstance(statement, Statement)
self.statements.append(statement)
def analyse_inputs(self):
used_symbols = OrderedSet()
generated_symbols = OrderedSet()
for stmt in self.statements:
for inp in flatten_extend(stmt.inputs):
if isinstance(inp, Symbol) and inp not in generated_symbols:
used_symbols.add(inp)
for out in flatten_extend(stmt.outputs):
if isinstance(out, Symbol):
generated_symbols.add(out)
used_symbols = sorted(used_symbols, key=lambda x: x.name)
if not parameters_persistent_mode_is_enabled():
return used_symbols, []
input_symbols = [
symbol for symbol in used_symbols if symbol not in self.param_symbol
]
param_symbols = [
symbol for symbol in used_symbols if symbol in self.param_symbol
]
return input_symbols, param_symbols
def __str__(self):
strs = []
strs.append(f"StatementIR: {self.name}")
strs.append(f" inputs: {map_structure(lambda x: x.name, self.inputs)}")
strs.append(f" params: {map_structure(lambda x: x.name, self.params)}")
strs.append(
f" outputs: {map_structure(lambda x: x.name, self.outputs)}"
)
strs.append(" statements: ")
for stmt in self.statements:
strs.append(f" {stmt}")
return "\n".join(strs)
def __repr__(self):
return self.__str__()
class StatementIRFactory(metaclass=Singleton):
"""
It is used to create a StatementIR.
"""
def __init__(self):
self.cache = {}
self.name_generator = NameGenerator("SIR_")
def __getitem__(self, key):
return self.cache[key]
def create(self, input_name=None):
if input_name:
name = input_name
else:
name = self.name_generator.next()
sir = StatementIR(name)
self.cache[name] = sir
return sir
def update(self, stmt_ir):
name = stmt_ir.name
self.cache[name] = stmt_ir
def clear(self):
want_clear = [
key
for key in self.cache.keys()
if self.name_generator.match_name(key)
]
for key in want_clear:
del self.cache[key]
@@ -0,0 +1,13 @@
# Copyright (c) 2025 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.
@@ -0,0 +1,262 @@
# Copyright (c) 2025 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
from typing import TYPE_CHECKING
import paddle
from ..utils.exceptions import InnerError
if TYPE_CHECKING:
from ..opcode_translator.executor.guard import StringifiedExpression
class ConstraintNode:
def __init__(self, inputs: list[ConstraintNode]):
self.inputs = inputs
def create_guard_expr(
self, extern_vars: dict[str, StringifiedExpression]
) -> StringifiedExpression:
raise NotImplementedError
def create_guard_node(
self, extern_vars: dict[str, paddle.framework.core.ExprNodeBase]
) -> paddle.framework.core.ExprNodeBase:
raise NotImplementedError(
f"{self.__class__.__name__}.create_guard_node is not implemented"
)
class LeafConstraintNode(ConstraintNode):
def __init__(self):
super().__init__([])
class UnaryConstraintNode(ConstraintNode):
READABLE_SYMBOL: str
def __init__(self, input: ConstraintNode):
super().__init__([input])
self.input = input
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.input})"
def create_guard_expr(
self, extern_vars: dict[str, StringifiedExpression]
) -> StringifiedExpression:
from ..opcode_translator.executor.guard import (
StringifiedExpression,
union_free_vars,
)
input = self.input.create_guard_expr(extern_vars)
return StringifiedExpression(
f"{self.READABLE_SYMBOL}({{}})",
[input],
union_free_vars(input.free_vars),
)
def create_guard_node(
self, extern_vars: dict[str, paddle.framework.core.ExprNodeBase]
) -> paddle.framework.core.ExprNodeBase:
input = self.input.create_guard_node(extern_vars)
return paddle.framework.core.UnaryExprNode(input, self.READABLE_SYMBOL)
class BinaryConstraintNode(ConstraintNode):
READABLE_SYMBOL: str
def __init__(self, lhs: ConstraintNode, rhs: ConstraintNode):
super().__init__([lhs, rhs])
self.lhs = lhs
self.rhs = rhs
def create_guard_expr(
self, extern_vars: dict[str, StringifiedExpression]
) -> StringifiedExpression:
from ..opcode_translator.executor.guard import (
StringifiedExpression,
union_free_vars,
)
lhs = self.lhs.create_guard_expr(extern_vars)
rhs = self.rhs.create_guard_expr(extern_vars)
return StringifiedExpression(
f"({{}} {self.READABLE_SYMBOL} {{}})",
[lhs, rhs],
union_free_vars(lhs.free_vars, rhs.free_vars),
)
def create_guard_node(
self, extern_vars: dict[str, paddle.framework.core.ExprNodeBase]
) -> paddle.framework.core.ExprNodeBase:
lhs = self.lhs.create_guard_node(extern_vars)
rhs = self.rhs.create_guard_node(extern_vars)
return paddle.framework.core.BinaryExprNode(
lhs, rhs, self.READABLE_SYMBOL
)
def __repr__(self):
return f"{self.__class__.__name__}({self.lhs}, {self.rhs})"
class ConstantConstraintNode(LeafConstraintNode):
def __init__(self, value):
super().__init__()
self.value = value
def create_guard_expr(
self, extern_vars: dict[str, StringifiedExpression]
) -> StringifiedExpression:
from ..opcode_translator.executor.guard import (
StringifiedExpression,
)
return StringifiedExpression(f"{self.value!r}", [], {})
def create_guard_node(
self, extern_vars: dict[str, paddle.framework.core.ExprNodeBase]
) -> paddle.framework.core.ExprNodeBase:
return paddle.framework.core.ConstantExprNode(self.value)
def __repr__(self):
return f"{self.__class__.__name__}({self.value})"
class SymbolicConstraintNode(LeafConstraintNode):
def __init__(self, name: str):
super().__init__()
self.name = name
def create_guard_expr(
self, extern_vars: dict[str, StringifiedExpression]
) -> StringifiedExpression:
from ..opcode_translator.executor.guard import (
StringifiedExpression,
union_free_vars,
)
if self.name not in extern_vars:
raise InnerError(
f"Symbolic variable {self.name} not found in extern_vars."
)
return StringifiedExpression(
"{}",
[extern_vars[self.name]],
union_free_vars(extern_vars[self.name].free_vars),
)
def create_guard_node(
self, extern_vars: dict[str, paddle.framework.core.ExprNodeBase]
) -> paddle.framework.core.ExprNodeBase:
if self.name not in extern_vars:
raise InnerError(
f"Symbolic variable {self.name} not found in extern_vars."
)
return extern_vars[self.name]
def __repr__(self):
return f"{self.__class__.__name__}({self.name})"
class NegativeConstraintNode(UnaryConstraintNode):
READABLE_SYMBOL = "-"
class BitwiseNotConstraintNode(UnaryConstraintNode):
READABLE_SYMBOL = "~"
class AddConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "+"
class SubConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "-"
class MulConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "*"
class TrueDivConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "/"
class FloorDivConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "//"
class ModConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "%"
class PowConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "**"
class BitwiseLShiftConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "<<"
class BitwiseRShiftConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = ">>"
class BitwiseAndConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "&"
class BitwiseOrConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "|"
class BitwiseXorConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "^"
class LogicalToBoolConstraintNode(UnaryConstraintNode):
READABLE_SYMBOL = "bool"
class LogicalNotConstraintNode(UnaryConstraintNode):
READABLE_SYMBOL = "not"
class EqualConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "=="
class NotEqualConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "!="
class LessThanConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "<"
class LessEqualConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = "<="
class GreaterThanConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = ">"
class GreaterEqualConstraintNode(BinaryConstraintNode):
READABLE_SYMBOL = ">="
@@ -0,0 +1,104 @@
# Copyright (c) 2025 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 operator
from typing import TYPE_CHECKING
import paddle
if TYPE_CHECKING:
from ..utils.magic_methods import BinaryOp, UnaryOp
def symbolic_to_bool(x):
# Unified api for python number and paddle Tensor
return x != 0
def symbolic_not(x):
return x == 0
def symbolic_truediv(x, y):
# NOTE(SigureMo): In Paddle, the truediv maybe has precision issue.
# For example, paddle.tensor(168) / 7, in Python it should be 24.0,
# but in Paddle it will construct a Scale OP, which will calculate
# as 168 * (1 / 7) = 24.00000191, which may cause some unexpected
# bugs. So we cast the tensor and scalar both to float64 to avoid
# this issue.
is_need_cast_tensor = lambda v: (
isinstance(v, paddle.pir.Value) and v.dtype is not paddle.float64
)
cast_tensor_if_needed = lambda v: (
v.cast(paddle.float64) if is_need_cast_tensor(v) else v
)
cast_scalar_if_needed = lambda v: (
paddle.full([], v, dtype=paddle.float64)
if isinstance(v, (int, float))
else v
)
cast_if_needed = lambda v: cast_tensor_if_needed(cast_scalar_if_needed(v))
has_tensor_need_cast = is_need_cast_tensor(x) or is_need_cast_tensor(y)
if not has_tensor_need_cast:
return operator.truediv(x, y)
x = cast_if_needed(x)
y = cast_if_needed(y)
return operator.truediv(x, y)
# All symbolic operations need unified for python number and paddle Tensor
SYMBOLIC_UNARY_MATH_OPS: list[UnaryOp] = [
# Basic
operator.neg,
# Bitwise
operator.invert,
]
SYMBOLIC_BINARY_MATH_OPS: list[BinaryOp] = [
# Basic
operator.add,
operator.sub,
operator.mul,
symbolic_truediv,
operator.floordiv,
operator.pow,
operator.mod,
# Bitwise
operator.lshift,
operator.rshift,
operator.and_,
operator.or_,
operator.xor,
]
SYMBOLIC_UNARY_LOGICAL_OPS: list[UnaryOp] = [
symbolic_to_bool,
symbolic_not,
]
SYMBOLIC_BINARY_LOGICAL_OPS: list[BinaryOp] = [
operator.eq,
operator.ne,
operator.lt,
operator.le,
operator.gt,
operator.ge,
]
SYMBOLIC_MATH_OPS = SYMBOLIC_UNARY_MATH_OPS + SYMBOLIC_BINARY_MATH_OPS
SYMBOLIC_MATH_OPS = SYMBOLIC_UNARY_MATH_OPS + SYMBOLIC_BINARY_MATH_OPS
SYMBOLIC_UNARY_OPS: list[UnaryOp] = (
SYMBOLIC_UNARY_MATH_OPS + SYMBOLIC_UNARY_LOGICAL_OPS
)
SYMBOLIC_BINARY_OPS: list[BinaryOp] = (
SYMBOLIC_BINARY_MATH_OPS + SYMBOLIC_BINARY_LOGICAL_OPS
)
@@ -0,0 +1,57 @@
# Copyright (c) 2025 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
from typing import Generic, TypeVar
_T = TypeVar("_T", "int", "float", "bool")
class SymbolicValue(Generic[_T]):
example_value: _T | None
def __init__(self, example_value=None):
self.example_value = example_value
def __repr__(self) -> str:
if self.is_backed():
return f"{self.__class__.__name__}({self.example_value})"
return f"{self.__class__.__name__}()"
def get_static_type(self) -> type[_T]:
raise NotImplementedError("get_py_type is not implemented.")
def is_backed(self):
return self.example_value is not None
def get_example_value(self) -> _T:
if self.example_value is None:
raise ValueError(f"{self} is not backed by a value.")
return self.example_value
class SymbolicBool(SymbolicValue):
def get_static_type(self) -> type[bool]:
return bool
class SymbolicInt(SymbolicValue):
def get_static_type(self) -> type[int]:
return int
class SymbolicFloat(SymbolicValue):
def get_static_type(self) -> type[float]:
return float
+118
View File
@@ -0,0 +1,118 @@
# Copyright (c) 2023 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
from typing import TYPE_CHECKING, TypeVar
from typing_extensions import ParamSpec
import paddle
from .opcode_translator import eval_frame_callback
from .profiler import SotStepProfilerGuard
from .utils import (
InfoCollector,
StepInfoManager,
)
if TYPE_CHECKING:
from collections.abc import Callable
P = ParamSpec("P")
R = TypeVar("R")
def symbolic_translate(fn: Callable[P, R], **kwargs) -> Callable[P, R]:
"""
This function is the entry point of PaddleSOT. It sets eval_frame_callback before input
function to achieve Opcode-level translation. The translation process depends on the
simulation execution, in which information will be collected, especially the network
code. After the simulation execution is completed, the network code will be compiled
into a static graph Program to improve performance.
Args:
fn: The input function.
Returns:
Callable, The wrapped function.
Examples:
>>> # doctest: +SKIP("Could not get source code of function foo."")
>>> import paddle
>>> import numpy as np
>>> from sot.translate import symbolic_translate
>>> def foo(cond: paddle.Tensor, x: paddle.Tensor):
... x += 1
... if cond:
... x += 1
... else:
... x -= 1
... return x
>>> symbolic_translate_foo = symbolic_translate(foo)
>>> # For the true branch, the output is 2.
>>> cond = paddle.to_tensor(True)
>>> x = paddle.to_tensor(0)
>>> dygraph_out = foo(cond, x)
>>> symbolic_translate_out = symbolic_translate_foo(cond, x)
>>> dygraph_out
Tensor(shape=[], dtype=int64, place=Place(cpu), stop_gradient=True,
2)
>>> symbolic_translate_out
Tensor(shape=[], dtype=int64, place=Place(cpu), stop_gradient=True,
2)
>>> np.testing.assert_allclose(dygraph_out.numpy(), symbolic_translate_out.numpy())
>>> # For the false branch, the output is 0.
>>> cond = paddle.to_tensor(False)
>>> dygraph_out = foo(cond, x)
>>> symbolic_translate_out = symbolic_translate_foo(cond, x)
>>> dygraph_out
Tensor(shape=[], dtype=int64, place=Place(cpu), stop_gradient=True,
0)
>>> symbolic_translate_out
Tensor(shape=[], dtype=int64, place=Place(cpu), stop_gradient=True,
0)
>>> np.testing.assert_allclose(dygraph_out.numpy(), symbolic_translate_out.numpy())
"""
if not paddle.framework.use_pir_api():
raise RuntimeError(
"SOT is only supported when running in PIR mode. Please set the environment variable "
"FLAGS_enable_pir_api=1 to enable it."
)
kwargs.setdefault('training', True)
def callback(frame):
return eval_frame_callback(frame, **kwargs)
def impl(*args: P.args, **kwargs: P.kwargs) -> R:
assert hasattr(fn, "__code__"), (
"Target function doesn't have code for simulating."
)
with StepInfoManager().step_guard(fn.__code__), SotStepProfilerGuard():
InfoCollector().clear_step_info()
paddle.framework.core.set_eval_frame(callback)
try:
outs = fn(*args, **kwargs)
except Exception as e:
raise e
finally:
paddle.framework.core.set_eval_frame(None)
InfoCollector().print_step_report()
return outs
return impl
+128
View File
@@ -0,0 +1,128 @@
# Copyright (c) 2023 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 .call_ast_utils import get_static_function, try_ast_func # noqa: F401
from .envs import ( # noqa: F401
ENV_MIN_GRAPH_SIZE,
ENV_SOT_ALLOW_DYNAMIC_SHAPE,
ENV_SOT_CE_DEBUG_MODE,
ENV_SOT_COLLECT_INFO,
ENV_SOT_ENABLE_0_SIZE_FALLBACK,
ENV_SOT_ENABLE_COMPILE_TIME_LIMIT,
ENV_SOT_ENABLE_FASTER_GUARD,
ENV_SOT_ENABLE_GUARD_TREE,
ENV_SOT_ENABLE_STRICT_GUARD_CHECK,
ENV_SOT_EXPORT,
ENV_SOT_FORCE_FALLBACK_SIR_IDS,
ENV_SOT_LOG_LEVEL,
ENV_SOT_SERIALIZE_INFO,
ENV_SOT_TRACE_NUMPY,
ENV_SOT_UNSAFE_CACHE_FASTPATH,
ENV_SOT_WITH_CONTROL_FLOW,
ENV_STRICT_MODE,
PEP508LikeEnvironmentVariable,
allow_dynamic_shape_guard,
enable_0_size_fallback_guard,
export_guard,
faster_guard_guard,
guard_tree_guard,
min_graph_size_guard,
sot_step_profiler_guard,
specialized_dim_numbers_guard,
strict_mode_guard,
with_control_flow_guard,
)
from .exceptions import ( # noqa: F401
BreakGraphError,
BreakGraphReasonBase,
BuiltinFunctionBreak,
ConditionalFallbackError,
DataDependencyControlFlowBreak,
DataDependencyDynamicShapeBreak,
DataDependencyOperationBreak,
ExportError,
FallbackError,
InnerError,
PsdbBreakReason,
SotCapturedException,
SotCapturedExceptionFactory,
SotErrorBase,
UnsupportedIteratorBreak,
UnsupportedOperationBreak,
inner_error_default_handler,
)
from .info_collector import ( # noqa: F401
BreakGraphReasonInfo,
CompileCountInfo,
InfoCollector,
NewSymbolHitRateInfo,
SubGraphInfo,
SubGraphRelationInfo,
)
from .magic_methods import magic_method_builtin_dispatch # noqa: F401
from .numpy_utils import ( # noqa: F401
NUMPY_API_SUPPORTED_DICT,
)
from .paddle_api_config import ( # noqa: F401
get_tensor_methods,
is_break_graph_tensor_methods,
is_directly_run_api,
is_inplace_api,
is_not_supported_paddle_layer,
)
from .utils import ( # noqa: F401
Cache,
ConstTypes,
NameGenerator,
ResumeFnNameFactory,
Singleton,
SIRToCodeMap,
SotUndefinedVar,
StepInfoManager,
already_unified_in_dynamic_and_static_graph,
count_if,
current_symbol_registry,
do_until_stop_iteration,
execute_time,
flatten,
flatten_extend,
get_api_fullname,
get_min_non_specialized_number,
get_numpy_ufuncs,
get_obj_stable_repr,
get_unbound_method,
hashable,
in_paddle_module,
is_break_graph_api,
is_builtin_fn,
is_comprehensive_name,
is_namedtuple_class,
is_paddle_api,
is_strict_mode,
list_contain_by_id,
list_find_index_by_id,
log,
log_do,
log_enabled,
log_format,
log_once,
map_if,
map_if_extend,
meta_str,
need_capture_control_flow,
no_eval_frame,
printable,
switch_symbol_registry,
update_list_inplace,
)
@@ -0,0 +1,95 @@
# Copyright (c) 2023 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.
import inspect
import types
import paddle
from .envs import ENV_SOT_WITH_CONTROL_FLOW
from .exceptions import InnerError
from .utils import Singleton
try_ast_codes = set()
def try_ast_func(func):
def _is_wrapped(f):
return hasattr(f, '__wrapped__')
unwrapped_f = func
if hasattr(unwrapped_f, "__code__"):
try_ast_codes.add(func.__code__)
while _is_wrapped(unwrapped_f):
unwrapped_f = unwrapped_f.__wrapped__
if hasattr(unwrapped_f, "__code__"):
try_ast_codes.add(func.__code__)
return func
class StaticFunctionManager(metaclass=Singleton):
def __init__(self):
self.code_map = {}
def ast_transform_with_frame(self, frame):
code = frame.f_code
if code not in try_ast_codes:
return None
if code not in self.code_map:
if code.co_name.startswith("#") or code.co_name.startswith("$"):
self.code_map[code] = None
elif len(code.co_cellvars) + len(code.co_freevars) != 0:
self.code_map[code] = None
else:
function = types.FunctionType(
code,
frame.f_globals,
code.co_name,
(),
(),
)
function = paddle.jit.to_static(function, full_graph=True)
self.code_map[code] = function
return self.code_map[code]
def ast_transform_with_callable(self, fn):
if not inspect.isfunction(fn) or not hasattr(fn, "__code__"):
return None
code = fn.__code__
if code not in try_ast_codes:
return None
if code not in self.code_map:
if code.co_name.startswith("#") or code.co_name.startswith("$"):
self.code_map[code] = None
elif len(code.co_cellvars) + len(code.co_freevars) != 0:
self.code_map[code] = None
else:
self.code_map[code] = paddle.jit.to_static(fn, full_graph=True)
return self.code_map[code]
def get_static_function(obj, type_):
if ENV_SOT_WITH_CONTROL_FLOW.get():
if type_ == "eval_frame":
return StaticFunctionManager().ast_transform_with_frame(obj)
elif type_ == "inline_call":
return StaticFunctionManager().ast_transform_with_callable(obj)
else:
raise InnerError(f"Can not get static function with type {type_}.")
return None
+238
View File
@@ -0,0 +1,238 @@
# Copyright (c) 2023 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 os
from contextlib import contextmanager
from paddle.utils.environments import (
BooleanEnvironmentVariable,
EnvironmentVariable,
EnvironmentVariableGuard,
IntegerEnvironmentVariable,
StringEnvironmentVariable,
)
class PEP508LikeEnvironmentVariable(EnvironmentVariable[dict[str, list[str]]]):
"""
Environment variable parser following PEP 508 extras specification syntax.
https://peps.python.org/pep-0508/
Processes strings using PEP 508-style bracket notation for optional components:
"feat1[opt1,opt2], feat2[opt3,opt4]" -> {'feat1': ['opt1', 'opt2'], 'feat2': ['opt3', 'opt4']}
"""
def __init__(self, name: str, default: dict[str, list[str]]):
super().__init__(name, default)
assert isinstance(default, dict), "default must be a dict"
def parse_from_string(self) -> dict[str, list[str]]:
env_var = os.getenv(self.name)
if env_var is None or env_var == "":
return self.default
items = self.split_by_unbracketed_commas(env_var)
ret = {}
for item in items:
ret.update(self.parse_parameterized_key(item))
return ret
def convert_to_string(self, value: dict[str, list[str]]) -> str:
assert isinstance(value, dict), "The input must be a dict"
assert all(isinstance(x, str) for x in value.keys()), (
"Keys must be a string"
)
assert all(isinstance(x, list) for x in value.values()), (
"Values must be a list"
)
env_list = []
for k, v in value.items():
env_list.append(f"{k}" + (f"[{','.join(v)}]" if len(v) else ""))
return ",".join(env_list)
@staticmethod
def split_by_unbracketed_commas(input_str: str) -> list[str]:
"""Split string by commas that are not enclosed in square brackets"""
# "feat1[opt1,opt2], feat2[opt3], feat3" -> ["feat1[opt1,opt2]", "feat2[opt3]", "feat3"]
bracket_depth = 0
split_parts = []
_start = 0
for _current, char in enumerate(input_str):
if char == "[":
bracket_depth += 1
elif char == "]":
bracket_depth = max(
0, bracket_depth - 1
) # Prevent negative depth
if char == "," and bracket_depth == 0:
split_parts.append(input_str[_start:_current].strip())
_start = _current + 1 # Skip comma
# Add remaining content after last comma
if remaining := input_str[_start:].strip():
split_parts.append(remaining)
return split_parts
@staticmethod
def parse_parameterized_key(input_str: str) -> dict[str, list[str]]:
"""Parse key with parameters in brackets into a dictionary."""
start_bracket = input_str.find("[")
end_bracket = input_str.rfind("]")
if start_bracket == -1 or end_bracket == -1:
return {input_str: []}
parameter_key = input_str[:start_bracket].strip()
# Extract and clean parameters
parameters_str = input_str[start_bracket + 1 : end_bracket]
parameter_values = [
v.strip() for v in parameters_str.split(",") if v.strip()
]
return {parameter_key: parameter_values}
ENV_MIN_GRAPH_SIZE = IntegerEnvironmentVariable("MIN_GRAPH_SIZE", 10)
ENV_SOT_LOG_LEVEL = IntegerEnvironmentVariable("SOT_LOG_LEVEL", 0)
ENV_STRICT_MODE = BooleanEnvironmentVariable("STRICT_MODE", False)
ENV_SOT_WITH_CONTROL_FLOW = BooleanEnvironmentVariable(
"SOT_WITH_CONTROL_FLOW", True
)
ENV_SOT_EXPORT = StringEnvironmentVariable("SOT_EXPORT", "")
ENV_SOT_ALLOW_DYNAMIC_SHAPE = BooleanEnvironmentVariable(
"SOT_ALLOW_DYNAMIC_SHAPE",
# Enable SOT dynamic shape as default in PIR mode
True,
)
ENV_SOT_ENABLE_FASTER_GUARD = BooleanEnvironmentVariable(
"SOT_ENABLE_FASTER_GUARD",
False,
)
ENV_SOT_ENABLE_STRICT_GUARD_CHECK = BooleanEnvironmentVariable(
"SOT_ENABLE_STRICT_GUARD_CHECK",
False,
)
ENV_SOT_ENABLE_GUARD_TREE = BooleanEnvironmentVariable(
"SOT_ENABLE_GUARD_TREE",
False,
)
ENV_ENABLE_SOT_STEP_PROFILER = BooleanEnvironmentVariable(
"ENABLE_SOT_STEP_PROFILER", False
)
ENV_SOT_BREAK_GRAPH_ON_GET_SYMBOLIC_VALUE = BooleanEnvironmentVariable(
"SOT_BREAK_GRAPH_ON_GET_SYMBOLIC_VALUE", False
)
ENV_SOT_COLLECT_INFO = PEP508LikeEnvironmentVariable("SOT_COLLECT_INFO", {})
ENV_SOT_SERIALIZE_INFO = BooleanEnvironmentVariable("SOT_SERIALIZE_INFO", False)
ENV_SOT_CE_DEBUG_MODE = BooleanEnvironmentVariable("SOT_CE_DEBUG_MODE", False)
ENV_SOT_FORCE_FALLBACK_SIR_IDS = StringEnvironmentVariable(
"SOT_FORCE_FALLBACK_SIR_IDS", ""
)
ENV_SOT_TRACE_NUMPY = BooleanEnvironmentVariable("ENV_SOT_TRACE_NUMPY", True)
ENV_SOT_UNSAFE_CACHE_FASTPATH = BooleanEnvironmentVariable(
"SOT_UNSAFE_CACHE_FASTPATH", False
)
ENV_SOT_ENABLE_0_SIZE_FALLBACK = BooleanEnvironmentVariable(
"SOT_ENABLE_0_SIZE_FALLBACK", True
)
ENV_SOT_SPECIALIZED_DIM_NUMBERS = StringEnvironmentVariable(
"SOT_SPECIALIZED_DIM_NUMBERS", "0"
)
ENV_SOT_ENABLE_COMPILE_TIME_LIMIT = BooleanEnvironmentVariable(
"SOT_ENABLE_COMPILE_TIME_LIMIT", True
)
def update_ce_flags():
if not ENV_SOT_CE_DEBUG_MODE.get():
return
# Enable information collection flags to facilitate debugging and analysis
collected_info_item: dict[str, list[str]] = ENV_SOT_COLLECT_INFO.get()
collected_info_item.setdefault("breakgraph_reason", [])
collected_info_item.setdefault("subgraph_info", [])
ENV_SOT_COLLECT_INFO.set(collected_info_item)
ENV_SOT_SERIALIZE_INFO.set(True)
update_ce_flags()
@contextmanager
def strict_mode_guard(value: bool):
with EnvironmentVariableGuard(ENV_STRICT_MODE, value):
yield
@contextmanager
def min_graph_size_guard(value: int):
with EnvironmentVariableGuard(ENV_MIN_GRAPH_SIZE, value):
yield
@contextmanager
def with_control_flow_guard(value: bool):
with EnvironmentVariableGuard(ENV_SOT_WITH_CONTROL_FLOW, value):
yield
@contextmanager
def export_guard(value: str):
with EnvironmentVariableGuard(ENV_SOT_EXPORT, value):
yield
@contextmanager
def allow_dynamic_shape_guard(value: bool):
with EnvironmentVariableGuard(ENV_SOT_ALLOW_DYNAMIC_SHAPE, value):
yield
@contextmanager
def faster_guard_guard(value: bool):
with EnvironmentVariableGuard(ENV_SOT_ENABLE_FASTER_GUARD, value):
yield
@contextmanager
def guard_tree_guard(value: bool):
with EnvironmentVariableGuard(ENV_SOT_ENABLE_GUARD_TREE, value):
yield
@contextmanager
def sot_step_profiler_guard(value: bool):
with EnvironmentVariableGuard(ENV_ENABLE_SOT_STEP_PROFILER, value):
yield
@contextmanager
def specialized_dim_numbers_guard(value: str):
with EnvironmentVariableGuard(ENV_SOT_SPECIALIZED_DIM_NUMBERS, value):
yield
@contextmanager
def enable_0_size_fallback_guard(value: bool):
with EnvironmentVariableGuard(ENV_SOT_ENABLE_0_SIZE_FALLBACK, value):
yield
+488
View File
@@ -0,0 +1,488 @@
# Copyright (c) 2023 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 traceback
from typing import TYPE_CHECKING
from .info_collector import BreakGraphReasonInfo
if TYPE_CHECKING:
from ..opcode_translator.executor.variables.base import VariableBase
class BreakGraphReasonBase:
"""Base class for representing reasons why graph execution was interrupted.
Attributes:
reason_str (str): Description of the break reason
file_path (str): Path to the file where break occurred
line_number (int): Line number where break occurred
"""
def __init__(
self,
reason_str,
file_path="",
line_number=-1,
):
self.reason_str = reason_str
self.file_path = file_path
self.line_number = line_number
def __repr__(self) -> str:
return f"{self.reason_str}"
class DataDependencyBreak(BreakGraphReasonBase):
pass
class DataDependencyControlFlowBreak(DataDependencyBreak):
"""Break reason for control flow execution."""
def __init__(self, reason_str=None, file_path="", line_number=-1):
if reason_str is None:
reason_str = "OpcodeInlineExecutor want break graph when simulate control flow."
super().__init__(
reason_str,
file_path,
line_number,
)
class DataDependencyDynamicShapeBreak(DataDependencyBreak):
pass
class DataDependencyOperationBreak(DataDependencyBreak):
pass
class UnsupportedOperationBreak(BreakGraphReasonBase):
def __init__(
self,
*,
left_type=None,
right_type=None,
operator=None,
reason_str=None,
file_path="",
line_number=-1,
):
if reason_str is None:
reason_str = f"Unsupported operator '{operator}' between {left_type} and {right_type}"
super().__init__(reason_str, file_path, line_number)
class UnsupportedPaddleAPIBreak(UnsupportedOperationBreak):
def __init__(
self,
*,
fn_name=None,
reason_str=None,
file_path="",
line_number=-1,
):
if reason_str is None:
reason_str = f"Not support Paddlepaddle API: {fn_name}"
super().__init__(
reason_str=reason_str,
file_path=file_path,
line_number=line_number,
)
class UnsupportedNumPyAPIBreak(UnsupportedOperationBreak):
def __init__(
self,
*,
fn_name=None,
reason_str=None,
file_path="",
line_number=-1,
):
if reason_str is None:
reason_str = f"Not support NumPy API: {fn_name}"
super().__init__(
reason_str=reason_str,
file_path=file_path,
line_number=line_number,
)
class UnsupportedRandomAPIBreak(UnsupportedOperationBreak):
def __init__(
self,
*,
fn_name=None,
reason_str=None,
file_path="",
line_number=-1,
):
if reason_str is None:
reason_str = f"Random function {fn_name} is not supported."
super().__init__(
reason_str=reason_str,
file_path=file_path,
line_number=line_number,
)
class ForceBreak(UnsupportedOperationBreak):
def __init__(
self,
*,
reason_str=None,
file_path="",
line_number=-1,
):
if reason_str is None:
reason_str = "Force break graph execution"
super().__init__(
reason_str=reason_str,
file_path=file_path,
line_number=line_number,
)
class BuiltinFunctionBreak(UnsupportedOperationBreak):
"""Break reason for unsupported built-in function calls.
Args:
fn_name (str): Name of the builtin function
arg_types (list): Types of the arguments passed to the function
file_path (str): Path to the file where break occurred
line_number (int): Line number where break occurred
"""
def __init__(
self,
*,
fn_name=None,
arg_types=None,
reason_str=None,
file_path="",
line_number=-1,
):
if reason_str is None:
reason_str = f"Not support builtin function: {fn_name} with args: Args({arg_types})"
super().__init__(
reason_str=reason_str,
file_path=file_path,
line_number=line_number,
)
class SideEffectBreak(BreakGraphReasonBase):
pass
class UnsupportedIteratorBreak(SideEffectBreak):
pass
class InlineCallBreak(BreakGraphReasonBase):
pass
class FallbackInlineCallBreak(InlineCallBreak):
pass
class BreakGraphInlineCallBreak(InlineCallBreak):
pass
class OtherInlineCallBreak(InlineCallBreak):
pass
class DygraphInconsistentWithStaticBreak(BreakGraphReasonBase):
pass
class PsdbBreakReason(BreakGraphReasonBase):
pass
class InferMetaBreak(BreakGraphReasonBase):
"""Break reason during meta information inference phase."""
pass
class NullMetaBreak(BreakGraphReasonBase):
def __init__(
self,
*,
file_path="",
line_number=-1,
):
super().__init__(
"Access attribute from null meta", file_path, line_number
)
class SotErrorBase(Exception):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from ..opcode_translator.breakpoint import BreakpointManager
BreakpointManager().on_event(f"{self.__class__.__name__}")
def print(self):
lines = traceback.format_tb(self.__traceback__)
print("".join(lines))
class InnerError(SotErrorBase):
pass
class HasNoAttributeError(InnerError):
pass
class FallbackError(SotErrorBase):
def __init__(self, msg, disable_eval_frame=False):
super().__init__(msg)
self.disable_eval_frame = disable_eval_frame
class ConditionalFallbackError(FallbackError): ...
# raise in inline function call strategy.
class BreakGraphError(SotErrorBase):
def __init__(self, reason: BreakGraphReasonBase = None):
super().__init__(str(reason))
if not isinstance(reason, BreakGraphReasonBase):
raise ValueError(
"reason must be a subclass of BreakGraphReasonBase"
)
self.reason = reason
BreakGraphReasonInfo.collect_break_graph_reason(reason)
def inner_error_default_handler(func, message_fn):
"""Wrap function and an error handling function and throw an InnerError."""
def impl(*args, **kwargs):
try:
return func(*args, **kwargs)
except SotErrorBase as e:
raise e
except Exception as e:
message = message_fn(*args, **kwargs)
origin_exception_message = "\n".join(
traceback.format_exception(type(e), e, e.__traceback__)
)
raise InnerError(
f"{message}\nOrigin Exception is: \n {origin_exception_message}"
) from e
return impl
class ExportError(SotErrorBase):
pass
class SotExtraInfo:
SOT_EXTRA_INFO_ATTR_NAME = "__SOT_EXTRA_INFO__"
def __init__(self, *, need_breakgraph: bool = False):
self.need_breakgraph = need_breakgraph
def set_need_breakgraph(self, need_breakgraph: bool):
self.need_breakgraph = need_breakgraph
def attach(self, err: BaseException):
setattr(err, SotExtraInfo.SOT_EXTRA_INFO_ATTR_NAME, self)
@staticmethod
def default() -> SotExtraInfo:
return SotExtraInfo()
@staticmethod
def from_exception(err: BaseException) -> SotExtraInfo:
info = getattr(
err, SotExtraInfo.SOT_EXTRA_INFO_ATTR_NAME, SotExtraInfo.default()
)
setattr(err, SotExtraInfo.SOT_EXTRA_INFO_ATTR_NAME, info)
return info
class SotCapturedException(SotErrorBase):
# Represents an exception encountered during bytecode execution simulation.
# This exception is used by SOT to handle Python exceptions by mapping them to
# SotCapturedException for consistent exception handling in the simulation process.
...
class SotCapturedLookupError(SotCapturedException): ...
class SotCapturedIndexError(SotCapturedLookupError): ...
class SotCapturedKeyError(SotCapturedLookupError): ...
class SotCapturedArithmeticError(SotCapturedException): ...
class SotCapturedFloatingPointError(SotCapturedArithmeticError): ...
class SotCapturedOverflowError(SotCapturedArithmeticError): ...
class SotCapturedZeroDivisionError(SotCapturedArithmeticError): ...
class SotCapturedImportError(SotCapturedException): ...
class SotCapturedModuleNotFoundError(SotCapturedImportError): ...
class SotCapturedRuntimeError(SotCapturedException): ...
class SotCapturedNotImplementedError(SotCapturedRuntimeError): ...
class SotCapturedRecursionError(SotCapturedRuntimeError): ...
class SotCapturedNameError(SotCapturedException): ...
class SotCapturedUnboundLocalError(SotCapturedNameError): ...
class SotCapturedSyntaxError(SotCapturedException): ...
class SotCapturedIndentationError(SotCapturedSyntaxError): ...
class SotCapturedTabError(SotCapturedIndentationError): ...
class SotCapturedOSError(SotCapturedException): ...
class SotCapturedFileExistsError(SotCapturedOSError): ...
class SotCapturedFileNotFoundError(SotCapturedOSError): ...
class SotCapturedIsADirectoryError(SotCapturedOSError): ...
class SotCapturedNotADirectoryError(SotCapturedOSError): ...
class SotCapturedPermissionError(SotCapturedOSError): ...
class SotCapturedTimeoutError(SotCapturedOSError): ...
class SotCapturedStopIteration(SotCapturedOSError): ...
class SotCapturedExceptionFactory:
# This dictionary maps common built-in Python Exception types to their corresponding SotCapturedException
# types, preserving the original exception hierarchy for proper inheritance behavior.
# Reference: https://docs.python.org/3/library/exceptions.html#exception-hierarchy
MAPPING = {
Exception: SotCapturedException,
LookupError: SotCapturedLookupError,
IndexError: SotCapturedIndexError,
KeyError: SotCapturedKeyError,
ArithmeticError: SotCapturedArithmeticError,
FloatingPointError: SotCapturedFloatingPointError,
OverflowError: SotCapturedOverflowError,
ZeroDivisionError: SotCapturedZeroDivisionError,
ImportError: SotCapturedImportError,
ModuleNotFoundError: SotCapturedModuleNotFoundError,
RuntimeError: SotCapturedRuntimeError,
NotImplementedError: SotCapturedNotImplementedError,
NameError: SotCapturedNameError,
UnboundLocalError: SotCapturedUnboundLocalError,
SyntaxError: SotCapturedSyntaxError,
IndentationError: SotCapturedIndentationError,
TabError: SotCapturedTabError,
OSError: SotCapturedOSError,
FileExistsError: SotCapturedFileExistsError,
FileNotFoundError: SotCapturedFileNotFoundError,
IsADirectoryError: SotCapturedIsADirectoryError,
NotADirectoryError: SotCapturedNotADirectoryError,
PermissionError: SotCapturedPermissionError,
TimeoutError: SotCapturedTimeoutError,
StopIteration: SotCapturedStopIteration,
}
@classmethod
def get(
cls,
exc_type: type[Exception],
) -> type[SotCapturedException]:
if isinstance(exc_type, type) and issubclass(
exc_type, SotCapturedException
):
return exc_type
if exc_type not in cls.MAPPING:
name = getattr(exc_type, "__name__", str(exc_type))
cls.MAPPING[exc_type] = type(
f"SotCaptured{name}", (SotCapturedException,), {}
)
return cls.MAPPING[exc_type]
@classmethod
def create(
cls,
origin_exc: Exception,
tracked_args: list[VariableBase] | None = None,
) -> SotCapturedException:
# transform an Exception to SotCapturedException
exc_type = origin_exc.__class__
new_exc_type = cls.get(exc_type)
new_exc = new_exc_type(*origin_exc.args)
new_exc.__cause__ = origin_exc.__cause__
new_exc.__context__ = origin_exc.__context__
new_exc.__suppress_context__ = origin_exc.__suppress_context__
new_exc.__traceback__ = origin_exc.__traceback__
# Propagating Exception Parameters through SotCapturedException
if tracked_args is None:
tracked_args = []
new_exc.tracked_args = tracked_args
return new_exc
@@ -0,0 +1,468 @@
# Copyright (c) 2024 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 atexit
import base64
import json
import sys
from abc import ABC, abstractmethod
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple
from typing_extensions import Self
from .envs import ENV_SOT_COLLECT_INFO, ENV_SOT_SERIALIZE_INFO
from .utils import Singleton
if TYPE_CHECKING:
import types
from .exceptions import BreakGraphReasonBase
PREFIX = "<sot>"
SUFFIX = "</sot>"
ENCODING = "utf-8"
def try_import_graphviz():
try:
import graphviz
return graphviz
except ImportError:
return None
class InfoType(Enum):
STEP_INFO = 0
E2E_INFO = 1
class InfoCollector(metaclass=Singleton):
def __init__(self):
self._step_info: dict[str, list[InfoBase]] = {}
self._e2e_info: dict[str, list[InfoBase]] = {}
def get_info_dict(self, info_type: InfoType) -> dict[str, list[InfoBase]]:
if info_type == InfoType.STEP_INFO:
return self._step_info
else:
return self._e2e_info
def attach(self, cls: type[InfoBase], *args, **kwargs) -> None:
if self.need_collect(cls):
info = cls(*args, **kwargs)
self.register(info)
def register(self, info: InfoBase) -> None:
info_class_name = info.__class__.__name__
info_type = info.TYPE
info_dict = self.get_info_dict(info_type)
info_dict.setdefault(info_class_name, [])
info_dict[info_class_name].append(info)
def need_collect(self, cls: type[InfoBase]) -> bool:
return cls.SHORT_NAME in ENV_SOT_COLLECT_INFO.get()
def clear_step_info(self):
self._step_info.clear()
def clear_e2e_info(self):
self._e2e_info.clear()
def clear(self):
self.clear_step_info()
self.clear_e2e_info()
def print_step_report(self):
self.print_report(InfoType.STEP_INFO)
def print_e2e_info_atexit(self) -> None:
def atexit_hook():
self.print_report(InfoType.E2E_INFO)
sys.stdout.flush()
self.clear()
atexit.register(atexit_hook)
def print_report(self, info_type: InfoType) -> None:
if info_dict := self.get_info_dict(info_type):
print(self.generate_report(info_dict))
def generate_report(self, info_dict: dict[str, list[InfoBase]]) -> str:
report = ""
for info_class_name, info_list in info_dict.items():
cls = info_list[0].__class__
report += f"{info_class_name} ({cls.SHORT_NAME}):\n"
if ENV_SOT_SERIALIZE_INFO.get():
report += cls.json_report(info_list)
else:
report += cls.summary(info_list)
report += "\n"
return report
InfoCollector().print_e2e_info_atexit()
class InfoBase(ABC):
SHORT_NAME: ClassVar[str]
TYPE: ClassVar[InfoType]
def __init__(self): ...
@classmethod
@abstractmethod
def summary(cls, history: list[Self]) -> str: ...
@classmethod
def serialize(cls, obj: dict[str:Any]) -> str:
json_data = json.dumps(obj)
b64_bytes = base64.b64encode(json_data.encode(ENCODING))
return b64_bytes.decode(ENCODING)
@classmethod
def deserialize(cls, data: bytes | str) -> dict:
if isinstance(data, str):
data = data.encode(ENCODING)
json_str = base64.b64decode(data).decode(ENCODING)
return json.loads(json_str)
class NewSymbolHitRateInfo(InfoBase):
SHORT_NAME = "new_symbol_hit_rate"
TYPE = InfoType.STEP_INFO
def __init__(
self, input_tensor_ids: list[int], output_tensor_ids: list[int]
):
super().__init__()
self.input_tensor_ids = input_tensor_ids
self.output_tensor_ids = output_tensor_ids
@classmethod
def summary(cls, history: list[Self]) -> str:
if len(history) == 0:
return f"No {cls.SHORT_NAME} info"
if len(history) == 1:
return "Only one subgraph is generated"
known_tensor_ids = set()
hit_count = 0
all_count = sum([len(info.input_tensor_ids) for info in history[1:]])
for i, info in enumerate(history):
for tensor_id in info.input_tensor_ids:
# Skip the first graph
if i == 0:
continue
if tensor_id in known_tensor_ids:
hit_count += 1
for tensor_id in info.output_tensor_ids:
known_tensor_ids.add(tensor_id)
summary = f"All tensor count: {all_count}, hit count: {hit_count}\n"
summary += f"Hit rate: {hit_count / all_count:.2f}"
return summary
@classmethod
def json_report(cls, history: list[Self]) -> str:
# TODO: need to support serialize the output
return cls.summary(history)
class SubGraphRelationInfo(InfoBase):
SHORT_NAME = "subgraph_relation"
TYPE = InfoType.STEP_INFO
STEP_UNIQUE_ID = 0
class ConcreteShapeInfo(NamedTuple):
id: int
ir_shape: list[int]
real_shape: list[int]
def __init__(
self,
subgraph_name: str,
input_shape_infos: list[SubGraphRelationInfo.ConcreteShapeInfo],
output_shape_infos: list[SubGraphRelationInfo.ConcreteShapeInfo],
is_first_call: bool,
graph_size: int,
):
super().__init__()
self.subgraph_name = subgraph_name
self.input_shape_infos = input_shape_infos
self.output_shape_infos = output_shape_infos
self.is_first_call = is_first_call
self.graph_size = graph_size
@classmethod
def summary(cls, history: list[Self]) -> str:
# TODO: attach input shape (with dynamic shape info)
cls.STEP_UNIQUE_ID += 1
if len(history) == 0:
return f"No {cls.SHORT_NAME} info"
if all(not subgraph_info.is_first_call for subgraph_info in history):
return "All subgraph are not the first call"
graphviz = try_import_graphviz()
if graphviz is None:
return "Please install graphviz to show the subgraph relation"
dot = graphviz.Digraph()
shape_infos = [
shape_info
for info in history
for shape_info in info.input_shape_infos + info.output_shape_infos
]
def to_tensor_node_name(
shape_info: SubGraphRelationInfo.ConcreteShapeInfo,
):
return f"tensor_{shape_info.id}"
visited_shape = set()
for shape_info in shape_infos:
if shape_info.id in visited_shape:
continue
visited_shape.add(shape_info.id)
dot.node(
to_tensor_node_name(shape_info),
f"Tensor {shape_info.id} shape={shape_info.real_shape}",
shape="rect",
)
for i, info in enumerate(history):
subgraph_id = f"subgraph_{i}"
dot.node(
subgraph_id,
f"Subgraph {i} ({info.subgraph_name}, size={info.graph_size})",
shape="oval",
fillcolor="cyan" if info.is_first_call else None,
style="filled" if info.is_first_call else None,
)
for shape_info in info.input_shape_infos:
dot.edge(
to_tensor_node_name(shape_info),
subgraph_id,
label=str(shape_info.ir_shape),
)
for shape_info in info.output_shape_infos:
dot.edge(
subgraph_id,
to_tensor_node_name(shape_info),
label=str(shape_info.ir_shape),
)
directory = Path(".") / "subgraph_relation"
directory.mkdir(exist_ok=True, parents=True)
filename = f"subgraph_relation_{cls.STEP_UNIQUE_ID}"
dot.render(directory / filename, format="svg", cleanup=True)
return f"Please check {directory / filename}.svg for subgraph relation"
@classmethod
def json_report(cls, history: list[Self]) -> str:
# TODO: need to support serialize the output
return cls.summary(history)
class CompileCountInfo(InfoBase):
SHORT_NAME = "compile_count"
TYPE = InfoType.E2E_INFO
def __init__(self, code: types.CodeType):
super().__init__()
self.code = code
@classmethod
def summary(cls, history: list[Self]) -> str:
if len(history) == 0:
return f"No {cls.SHORT_NAME} info"
code_count = {}
for info in history:
code_count[info.code] = code_count.get(info.code, 0) + 1
summary_lines = []
for code, count in sorted(
code_count.items(), key=lambda x: x[1], reverse=True
):
filename, lineno = code.co_filename, code.co_firstlineno
summary_lines.append(
f" {code.co_name} ({filename}:{lineno}): {count}"
)
summary = "\n".join(summary_lines)
return summary
@classmethod
def json_report(cls, history: list[Self]) -> str:
# TODO: need to support serialize the output
return cls.summary(history)
class BreakGraphReasonInfo(InfoBase):
SHORT_NAME = "breakgraph_reason"
TYPE = InfoType.E2E_INFO
def __init__(self, reason: BreakGraphReasonBase):
super().__init__()
self.reason = reason
@classmethod
def classify(cls, history: list[Self]) -> str:
reasons_dict = {}
for info in history:
name = info.reason.__class__.__name__
if name not in reasons_dict:
reasons_dict[name] = []
reasons_dict[name].append(str(info.reason))
sorted_reasons = list(reasons_dict.items())
sorted_reasons.sort(key=lambda x: len(x[1]), reverse=True)
return reasons_dict, sorted_reasons
@classmethod
def summary(cls, history: list[Self]) -> str:
reason_dict, reason_list = cls.classify(history)
return "\n".join(
[
f"{name} ({len(reasons)}):\n\t" + "\n\t".join(reasons)
for name, reasons in reason_list
]
)
@classmethod
def json_report(cls, history: list[Self]) -> str:
reason_dict, sorted_reasons = cls.classify(history)
reason_dict["count"] = {k: len(v) for k, v in sorted_reasons}
serialized = cls.serialize({cls.SHORT_NAME: reason_dict})
return f"{PREFIX}{serialized}{SUFFIX}"
@classmethod
def restore_from_string(cls, serialized: str) -> list[Self]:
# This method is the inverse of json_report
from paddle.jit.sot.utils import exceptions
history = []
obj = cls.deserialize(serialized)[cls.SHORT_NAME]
obj.pop("count")
for classname in obj:
ReasonClass = getattr(exceptions, classname, None)
for reason in obj[classname]:
history.append(cls(ReasonClass(reason_str=reason)))
return history
@staticmethod
def collect_break_graph_reason(reason: BreakGraphReasonBase):
if not InfoCollector().need_collect(BreakGraphReasonInfo):
return
InfoCollector().attach(BreakGraphReasonInfo, reason)
class SubGraphInfo(InfoBase):
SHORT_NAME = "subgraph_info"
TYPE = InfoType.STEP_INFO
def __init__(self, graph: str, op_num: int, sir_name: str):
# NOTE: All data should be serializable
super().__init__()
self.graph = graph
self.op_num = op_num
self.sir_name = sir_name
def __str__(self):
return (
f"[SIR Name] {self.sir_name} [OpNum] {self.op_num}\n{self.graph}"
)
@classmethod
def summary(cls, history: list[Self]) -> str:
num_of_subgraph = len(history)
sum_of_op_num = sum(item.op_num for item in history)
need_details = "details" in ENV_SOT_COLLECT_INFO.get().get(
cls.SHORT_NAME, []
)
details = ""
if need_details:
details = "\n".join(
[
f"[SubGraphIdx] {idx} {info}"
for idx, info in enumerate(map(str, history))
]
)
summary = f"[Number of subgraph] {num_of_subgraph} [Sum of opnum] {sum_of_op_num}"
return f"{summary}\n{details}"
@classmethod
def json_report(cls, history: list[Self]) -> str:
need_details = "details" in ENV_SOT_COLLECT_INFO.get().get(
cls.SHORT_NAME, []
)
aggregated_info_list = []
for idx, record in enumerate(history):
entry_data = {}
entry_data["SIR_name"] = record.sir_name
entry_data["OpNum"] = record.op_num
entry_data["Graph"] = ""
if need_details:
entry_data["Graph"] = str(record.graph)
aggregated_info_list.append(entry_data)
serialized = cls.serialize({cls.SHORT_NAME: aggregated_info_list})
return f"{PREFIX}{serialized}{SUFFIX}"
@classmethod
def restore_from_string(cls, serialized: str) -> list[Self]:
# This method is the inverse of json_report
history = []
obj = cls.deserialize(serialized)[cls.SHORT_NAME]
for entry in obj:
history.append(
SubGraphInfo(
graph=entry["Graph"],
op_num=entry["OpNum"],
sir_name=entry["SIR_name"],
)
)
return history
def __eq__(self, other):
need_graph_equal = "details" in ENV_SOT_COLLECT_INFO.get().get(
self.SHORT_NAME, []
)
graph_equal_or_not = True
if need_graph_equal:
graph_equal_or_not = self.graph == other.graph
return (
graph_equal_or_not
and self.op_num == other.op_num
and self.sir_name == other.sir_name
)
@@ -0,0 +1,155 @@
# Copyright (c) 2023 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 operator
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from .utils import hashable
if TYPE_CHECKING:
from collections.abc import Callable
BinaryOp = Callable[[Any, Any], Any]
UnaryOp = Callable[[Any], Any]
INPLACE_BINARY_OPS_TO_MAGIC_NAMES: dict[BinaryOp, tuple[str, BinaryOp]] = {
# inplace op fn: (magic name, non-inplace op fn)
operator.iadd: ("__iadd__", operator.add),
operator.iand: ("__iand__", operator.and_),
operator.iconcat: ("__iconcat__", operator.concat),
operator.ifloordiv: ("__ifloordiv__", operator.floordiv),
operator.ilshift: ("__ilshift__", operator.lshift),
operator.imatmul: ("__imatmul__", operator.matmul),
operator.imod: ("__imod__", operator.mod),
operator.imul: ("__imul__", operator.mul),
operator.ior: ("__ior__", operator.or_),
operator.ipow: ("__ipow__", operator.pow),
operator.irshift: ("__irshift__", operator.rshift),
operator.isub: ("__isub__", operator.sub),
operator.itruediv: ("__itruediv__", operator.truediv),
operator.ixor: ("__ixor__", operator.xor),
}
NON_INPLACE_BINARY_OPS_TO_MAGIC_NAMES: dict[
BinaryOp, tuple[str, str | None]
] = {
# op fn: (magic name, reverse magic name)
operator.add: ("__add__", "__radd__"),
operator.and_: ("__and__", "__rand__"),
operator.contains: ("__contains__", None),
operator.delitem: ("__delitem__", None),
operator.eq: ("__eq__", "__eq__"),
operator.floordiv: ("__floordiv__", "__rfloordiv__"),
operator.ge: ("__ge__", "__le__"),
operator.getitem: ("__getitem__", None),
operator.gt: ("__gt__", "__lt__"),
operator.le: ("__le__", "__ge__"),
operator.lshift: ("__lshift__", "__rlshift__"),
operator.lt: ("__lt__", "__gt__"),
operator.matmul: ("__matmul__", "__rmatmul__"),
operator.mod: ("__mod__", "__rmod__"),
operator.mul: ("__mul__", "__rmul__"),
operator.ne: ("__ne__", "__ne__"),
operator.or_: ("__or__", "__ror__"),
operator.pow: ("__pow__", "__rpow__"),
operator.rshift: ("__rshift__", "__rrshift__"),
operator.sub: ("__sub__", "__rsub__"),
operator.truediv: ("__truediv__", "__rtruediv__"),
operator.xor: ("__xor__", "__rxor__"),
}
UNARY_OPS_TO_MAGIC_NAMES: dict[UnaryOp, str] = {
operator.neg: "__neg__",
operator.invert: "__invert__",
operator.pos: "__pos__",
operator.abs: "__abs__",
operator.index: "__index__",
operator.inv: "__inv__",
operator.truth: "__bool__",
bool: "__bool__",
abs: "__abs__",
float: "__float__",
len: "__len__",
int: "__int__",
complex: "__complex__",
}
# TODO(SigureMo): support any, all, sum
INPLACE_BINARY_OPS = set(INPLACE_BINARY_OPS_TO_MAGIC_NAMES.keys())
NON_INPLACE_BINARY_OPS = set(NON_INPLACE_BINARY_OPS_TO_MAGIC_NAMES.keys())
BINARY_OPS = INPLACE_BINARY_OPS | NON_INPLACE_BINARY_OPS
UNARY_OPS = set(UNARY_OPS_TO_MAGIC_NAMES.keys())
# NOTE: Both operator.pow and operator.ipow should be considered for inclusion in this list,
# as they raise ZeroDivisionError when evaluating 0^n where n < 0 (division by zero).
NEED_GUARD_ZERO_DIVISION_ERROR_OPS: list[BinaryOp] = [
operator.floordiv,
operator.truediv,
operator.mod,
operator.ifloordiv,
operator.itruediv,
operator.imod,
]
@dataclass
class MagicMethod:
name: str
is_inplace: bool = False
is_reverse: bool = False
def magic_method_builtin_dispatch(fn: BinaryOp | UnaryOp) -> list[MagicMethod]:
if not hashable(fn):
return []
if fn in INPLACE_BINARY_OPS:
inplace_magic_name, non_inplace_op = INPLACE_BINARY_OPS_TO_MAGIC_NAMES[
fn
]
return [
MagicMethod(inplace_magic_name, is_inplace=True),
*magic_method_builtin_dispatch(non_inplace_op),
]
elif fn in NON_INPLACE_BINARY_OPS:
magic_name, reverse_magic_name = NON_INPLACE_BINARY_OPS_TO_MAGIC_NAMES[
fn
]
magic_methods = [MagicMethod(magic_name)]
if reverse_magic_name is not None:
magic_methods.append(
MagicMethod(reverse_magic_name, is_reverse=True)
)
return magic_methods
elif fn in UNARY_OPS:
magic_name = UNARY_OPS_TO_MAGIC_NAMES[fn]
return [MagicMethod(magic_name)]
return []
def non_inplace_op_to_inplace_op(
fn: BinaryOp,
) -> BinaryOp | None:
for inplace_op, (
_,
non_inplace_op,
) in INPLACE_BINARY_OPS_TO_MAGIC_NAMES.items():
if fn is non_inplace_op:
return inplace_op
return None
@@ -0,0 +1,25 @@
# Copyright (c) 2025 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.
import numpy as np
import paddle
NUMPY_API_SUPPORTED_DICT = {
np.add: paddle.add,
np.subtract: paddle.subtract,
np.multiply: paddle.multiply,
np.divide: paddle.divide,
np.equal: paddle.equal,
}
@@ -0,0 +1,168 @@
# Copyright (c) 2023 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.
import inspect
import paddle
def is_inplace_api(func):
inplace_apis = {paddle.static.setitem}
return func in inplace_apis
def get_tensor_methods():
return [
member_name
for member_name, member in inspect.getmembers(paddle.pir.Value)
if inspect.isfunction(member) or inspect.ismethoddescriptor(member)
]
def get_paddle_api():
modules = [
paddle,
paddle.nn.functional,
paddle.nn.quant,
paddle.incubate.nn.functional,
paddle.linalg,
paddle.signal,
paddle.fft,
paddle.vision.ops,
paddle.metric,
paddle.geometric,
]
distributed_apis = [
paddle.distributed.all_reduce,
paddle.distributed.shard_tensor,
paddle.distributed.reshard,
paddle.distributed.all_gather,
paddle.distributed.alltoall,
paddle.distributed.barrier,
paddle.distributed.recv,
paddle.distributed.send,
paddle.distributed.broadcast,
paddle.distributed.unshard_dtensor,
paddle.distributed.auto_parallel.api.dtensor_to_local,
paddle.distributed.auto_parallel.api.dtensor_from_local,
paddle.distributed.auto_parallel.api.moe_global_mesh_tensor,
paddle.distributed.auto_parallel.api.moe_sub_mesh_tensors,
]
special_paddle_apis = [
paddle.tensor.fill_constant,
paddle.tensor.top_p_sampling,
]
non_operator_related_apis = [
paddle.in_dynamic_mode,
paddle.save,
paddle.load,
paddle.get_cuda_rng_state,
paddle.set_rng_state,
paddle.set_cuda_rng_state,
paddle.get_rng_state,
paddle.set_default_dtype,
paddle.check_shape,
paddle.summary,
paddle.finfo,
paddle.iinfo,
paddle.enable_static,
paddle.disable_static,
paddle.is_grad_enabled,
]
# TODO: users should not call static_apis, but we need to use, so add static_apis here temporary
static_apis = [paddle.static.setitem, paddle.static.accuracy]
paddle_api_list = []
for module in modules:
for fn_name in getattr(module, "__all__", []):
fn = getattr(module, fn_name)
if inspect.isfunction(fn):
paddle_api_list.append(fn)
return list(
set(special_paddle_apis)
| set(distributed_apis)
| set(static_apis)
| set(paddle_api_list) - set(non_operator_related_apis)
)
paddle_api_list = get_paddle_api()
# TODO(Aurelius84): It seems that we use it to judge 'in_paddle_module()'.
# Bug what does 'is_paddle_module' really means? Is all paddle.xx sub module
# considered as paddle module
paddle_api_module_prefix = {
"paddle.nn.functional",
}
break_graph_functions = set()
break_graph_layer_classes = set()
break_graph_tensor_method = {
'register_hook',
'numpy',
'clear_gradient',
'tolist',
'item',
# TODO: Browse all possible functions and make prior judgments.
}
not_supported_paddle_layer = {paddle.nn.RNN}
def is_not_supported_paddle_layer(layer_class):
return layer_class in not_supported_paddle_layer
def is_break_graph_tensor_methods(method_name):
return method_name in break_graph_tensor_method
def add_break_graph_function(fn):
break_graph_functions.add(fn)
def add_break_graph_layer_class(layer_class: type[paddle.nn.Layer]):
break_graph_layer_classes.add(layer_class)
def is_directly_run_api(api):
from .utils import hashable
if not hashable(api):
return False
NATIVE_CODE_PURE_FUNCTIONS = {
paddle.base.libpaddle.is_compiled_with_avx,
paddle.base.libpaddle.is_compiled_with_cuda,
paddle.base.libpaddle.is_compiled_with_cudnn_frontend,
paddle.base.libpaddle.is_compiled_with_rocm,
paddle.base.libpaddle.is_compiled_with_custom_device,
paddle.base.libpaddle.is_compiled_with_ipu,
paddle.base.libpaddle.is_compiled_with_xpu,
paddle.base.libpaddle.is_compiled_with_mkldnn,
paddle.base.libpaddle.is_compiled_with_onednn,
paddle.base.libpaddle.is_compiled_with_nccl,
paddle.base.libpaddle.is_compiled_with_mpi,
paddle.base.libpaddle.is_compiled_with_mpi_aware,
paddle.base.libpaddle.is_compiled_with_cinn,
paddle.base.libpaddle.is_compiled_with_distribute,
paddle.base.libpaddle.is_compiled_with_brpc,
paddle.base.libpaddle.is_compiled_with_dist,
paddle.base.libpaddle.is_compiled_with_flagcx,
}
if hasattr(paddle.base.libpaddle, "get_device_properties"):
NATIVE_CODE_PURE_FUNCTIONS.add(
paddle.base.libpaddle.get_device_properties
)
return api in NATIVE_CODE_PURE_FUNCTIONS
+541
View File
@@ -0,0 +1,541 @@
# Copyright (c) 2023 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 builtins
import copy
import inspect
import sys
import time
import types
import weakref
from collections import OrderedDict
from collections.abc import Callable
from contextlib import contextmanager
from dataclasses import is_dataclass
from functools import lru_cache
from typing import TYPE_CHECKING, Any, TypeVar
from weakref import WeakValueDictionary
import numpy as np
import paddle
from paddle.jit.dy2static.utils import (
TransformOptions,
dataclass_as_dict,
dataclass_from_dict,
)
from paddle.utils import flatten, map_structure
from .envs import (
ENV_SOT_LOG_LEVEL,
ENV_SOT_SPECIALIZED_DIM_NUMBERS,
ENV_STRICT_MODE,
)
from .paddle_api_config import (
break_graph_functions,
paddle_api_list,
paddle_api_module_prefix,
)
if TYPE_CHECKING:
from collections.abc import Callable
from paddle._typing import NestedStructure
T = TypeVar("T")
T1 = TypeVar("T1")
T2 = TypeVar("T2")
T3 = TypeVar("T3")
ConstTypes = (int, float, str, bool, type(None), bytes)
class Singleton(type):
_instances: dict[Any, Any] = {}
def __call__(cls, *args: Any, **kwargs: Any):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class NameGenerator:
def __init__(self, prefix):
self.counter = 0
self.prefix = prefix
def next(self):
name = self.prefix + str(self.counter)
self.counter += 1
return name
def match_name(self, name: str) -> bool:
return name.startswith(self.prefix)
class SymbolRegistry:
def __init__(self):
self.symbol_generator = NameGenerator(prefix="___t_")
self.tmp_names_record = OrderedDict()
self.declared_symbols: set[str] = set()
self.symbol_table = {}
def next_symbol(self) -> str:
return self.symbol_generator.next()
def request_symbol(self, expr: str) -> str:
if expr in self.symbol_table:
return self.symbol_table[expr]
symbol = self.next_symbol()
self.symbol_table[expr] = symbol
return symbol
def gen_expr(self, expr: str, gen_expr_fn):
symbol = self.symbol_table[expr]
if symbol in self.declared_symbols:
return symbol
self.declared_symbols.add(symbol)
return f"({symbol} := ({gen_expr_fn()}))"
_symbol_registry = SymbolRegistry()
@contextmanager
def switch_symbol_registry():
global _symbol_registry
original_registry = _symbol_registry
_symbol_registry = SymbolRegistry()
yield
_symbol_registry = original_registry
def current_symbol_registry():
global _symbol_registry
return _symbol_registry
class ResumeFnNameFactory(metaclass=Singleton):
def __init__(self) -> None:
self.gen = NameGenerator('resume_')
def next(self):
name = self.gen.next()
return name
class SIRToCodeMap(metaclass=Singleton):
def __init__(self):
self._map = {}
def register(self, sir, code):
self._map[sir.name] = code
def get(self, sir):
return self._map.get(sir.name)
def log(level, *args):
cur_level = ENV_SOT_LOG_LEVEL.get()
if level <= cur_level:
print(*args, end="", flush=True)
def log_do(level, fn):
cur_level = ENV_SOT_LOG_LEVEL.get()
if level <= cur_level:
fn()
def log_format(level, str, *args):
cur_level = ENV_SOT_LOG_LEVEL.get()
if level <= cur_level:
print(str.format(*args), end="", flush=True)
def log_enabled(level):
return level <= ENV_SOT_LOG_LEVEL.get()
@lru_cache
def log_once(msg):
print(msg, flush=True)
def no_eval_frame(func):
def no_eval_frame_func(*args, **kwargs):
old_cb = paddle.framework.core.set_eval_frame(None)
try:
retval = func(*args, **kwargs)
except:
raise
finally:
paddle.framework.core.set_eval_frame(old_cb)
return retval
return no_eval_frame_func
def is_comprehensive_name(name):
return name in ["<listcomp>", "<dictcomp>", "<setcomp>", "<genexpr>"]
def is_paddle_api(func):
if isinstance(func, paddle.nn.Layer): # ignore all the classes
return False
if hasattr(func, "__self__"): # ignore all the methods
return False
if inspect.isclass(
func
): # paddle.Tensor should not be wrapped, but how about other situations?
return False
return in_paddle_module(func) or func in paddle_api_list
def already_unified_in_dynamic_and_static_graph(fn):
if is_paddle_api(fn):
return True
return not TransformOptions.check_fn_need_transform(
fn, TransformOptions.ToStaticMode.SOT
)
def need_capture_control_flow(fn):
return TransformOptions.check_fn_need_capture_control_flow(fn)
def is_builtin_fn(fn):
special_builtin_fns = [weakref.ref]
if fn in special_builtin_fns:
return True
if isinstance(fn, types.BuiltinFunctionType):
return True
for member_name, member in inspect.getmembers(builtins):
if member is fn and isinstance(member, type):
return True
return False
def in_paddle_module(func):
if hasattr(func, "__module__"):
module_str = func.__module__
if module_str is None:
return False
log(5, "find paddle function with __module__: ", module_str, "\n")
if hasattr(func, "__name__"):
log(
5, " with __name__ : ", func.__name__, "\n"
)
log(5, " with results : ")
for prefix in paddle_api_module_prefix:
if module_str.startswith(prefix):
log(5, " True\n")
return True
log(5, " False\n")
return False
def is_break_graph_api(func):
return func in break_graph_functions
def is_namedtuple_class(cls):
if not inspect.isclass(cls):
return False
if not issubclass(cls, tuple):
return False
# The signature created by nametuple function
namedtuple_attrs = {"_make", "_asdict", "_fields", "_replace"}
cls_attrs = set(dir(cls))
return namedtuple_attrs.issubset(cls_attrs)
def map_if(
*structures: NestedStructure[T1],
pred: Callable[[T1], bool],
true_fn: Callable[[T1], T2],
false_fn: Callable[[T1], T3],
) -> NestedStructure[T2 | T3]:
def replace(*args):
if pred(*args):
return true_fn(*args)
return false_fn(*args)
return map_structure(replace, *structures)
def flatten_extend(structure):
for item in flatten(structure):
if isinstance(item, slice):
yield item.start
yield item.stop
yield item.step
else:
yield item
def map_if_extend(structure, pred, true_fn, false_fn):
"""support extended structures like slice and SliceVariable"""
def wrapped_pred(x):
if isinstance(x, slice):
return True
if is_dataclass(x) and not isinstance(x, type):
return True
return pred(x)
def wrapped_true_fn(x):
if isinstance(x, (slice)):
l = [x.start, x.stop, x.step]
l = map_if_extend(l, pred, true_fn, false_fn)
return slice(*l)
if is_dataclass(x) and not isinstance(x, type):
dt_dict = dataclass_as_dict(x)
dt_dict = map_if_extend(dt_dict, pred, true_fn, false_fn)
return dataclass_from_dict(type(x), dt_dict)
return true_fn(x)
return map_if(
structure, pred=wrapped_pred, true_fn=wrapped_true_fn, false_fn=false_fn
)
def count_if(*structures, pred):
def is_true(*args):
if pred(*args):
return 1
return 0
return sum(flatten(map_structure(is_true, *structures)))
class Cache:
def __init__(self, weak=False, copy=False):
if not weak:
self.cache = {}
else:
self.cache = WeakValueDictionary()
self.hit_num = 0
self.copy = copy
def __call__(self, *args, **kwargs):
cache_key = self.key_fn(*args, **kwargs)
if not hashable(cache_key):
return self.value_fn(*args, **kwargs)
if cache_key in self.cache:
log(5, "cache hit: ", cache_key, "\n")
self.hit_num += 1
cache_item = self.cache[cache_key]
if self.copy:
cache_item = copy.deepcopy(cache_item)
return cache_item
value = self.value_fn(*args, **kwargs)
self.cache[cache_key] = value
return value
def clear(self):
self.cache.clear()
self.hit_num = 0
def key_fn(self, *args, **kwargs):
raise NotImplementedError
def value_fn(self, *args, **kwargs):
raise NotImplementedError
def execute_time(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
execution_time = end_time - start_time
print("Execute time:", execution_time)
return result
return wrapper
def meta_str(shape, dtype, stop_gradient):
return f"(shape: {shape}, dtype: {dtype}, stop_gradient: {stop_gradient})"
def is_strict_mode():
return ENV_STRICT_MODE.get()
def list_find_index_by_id(li: list[Any], item: Any) -> int:
return [id(it) for it in li].index(id(item))
def list_contain_by_id(li: list[Any], item: Any) -> int:
return id(item) in [id(it) for it in li]
def get_unbound_method(obj, name):
# TODO(dev): Consider the case of patching methods to instances
return getattr(obj.__class__, name)
class SotUndefinedVar(metaclass=Singleton):
pass
def hashable(obj):
try:
hash(obj)
return True
except TypeError as e:
return False
def printable(obj):
try:
str(obj)
return True
except Exception as e:
return False
class StepInfo:
BACK_TRACE_STEPS = 20
def __init__(self):
self.step_count = -1
def need_back_trace(self):
return self.step_count < self.BACK_TRACE_STEPS
class StepInfoManager(metaclass=Singleton):
def __init__(self):
self.step_record = {}
self.current_code = None
self.current_step_info = None
@contextmanager
def step_guard(self, code):
try:
old_code = self.current_code
old_info = self.current_step_info
self.current_code = code
if code not in self.step_record:
self.step_record[code] = StepInfo()
self.current_step_info = self.step_record[code]
self.current_step_info.step_count += 1
yield
finally:
self.current_code = old_code
self.current_step_info = old_info
@property
def need_back_trace(self):
return (
self.current_step_info is not None
and self.current_step_info.need_back_trace()
)
@property
def current_step(self):
return self.current_step_info.step_count
def clear(self):
self.step_record.clear()
self.current_code = None
self.current_step = -1
def get_api_fullname(api):
api_name = api.__name__
module_str = api.__module__
while len(module_str) > 0:
if module_str not in sys.modules:
return api_name
module = sys.modules[module_str]
if hasattr(module, api_name):
return module_str + "." + api_name
module_str = module_str.rpartition(".")[0]
return None
def get_numpy_ufuncs():
ufuncs = [
ufunc
for _, ufunc in inspect.getmembers(
np, lambda member: isinstance(member, np.ufunc)
)
]
unary_ufuncs = filter(lambda ufunc: ufunc.nin == 1, ufuncs)
binary_ufuncs = filter(lambda ufunc: ufunc.nin == 2, ufuncs)
return list(unary_ufuncs), list(binary_ufuncs)
def do_until_stop_iteration(fn: Callable[[], T]) -> list[T]:
from paddle.jit.sot.utils.exceptions import SotCapturedStopIteration
res = []
while True:
try:
res.append(fn())
except SotCapturedStopIteration:
break
return res
def update_list_inplace(
original_list: list[T], new_contents: list[T]
) -> list[T]:
original_list.clear()
original_list.extend(new_contents)
return original_list
def get_obj_stable_repr(obj) -> str:
if hasattr(obj, '__qualname__'):
return obj.__qualname__
if hasattr(obj, '__name__'):
return obj.__name__
class_name = obj.__class__.__name__
# If module is available and not __main__, include it
if hasattr(obj, "__class__") and hasattr(obj.__class__, "__module__"):
module = obj.__class__.__module__
if module not in ("__main__", "builtins"):
return f"{module}.{class_name}()"
return f"{class_name}()"
def get_min_non_specialized_number() -> int:
specialized_dim_numbers_raw_str = (
ENV_SOT_SPECIALIZED_DIM_NUMBERS.get().lower()
)
assert specialized_dim_numbers_raw_str in [
"no",
"0",
"01",
], f"Unsupported specialized_dim_numbers: {specialized_dim_numbers_raw_str}"
to_min_non_specialized_number = {
# specialized numbers, minimum non-specialized number
"no": 0,
"0": 1,
"01": 2,
}
return to_min_non_specialized_number[specialized_dim_numbers_raw_str]