Files
paddlepaddle--paddle/python/paddle/jit/dy2static/transformers/ifelse_transformer.py
T
2026-07-13 12:40:42 +08:00

448 lines
14 KiB
Python

# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
from collections import defaultdict
from paddle.base import unique_name
from paddle.jit.dy2static.utils import (
GetterSetterHelper,
ast_to_source_code,
)
# gast is a generic AST to represent Python2 and Python3's Abstract Syntax Tree(AST).
# It provides a compatibility layer between the AST of various Python versions,
# as produced by ast.parse from the standard ast module.
# See details in https://github.com/serge-sans-paille/gast/
from paddle.utils import gast
from .base import BaseTransformer
from .utils import (
FALSE_FUNC_PREFIX,
FOR_ITER_INDEX_PREFIX,
FOR_ITER_ITERATOR_PREFIX,
FOR_ITER_TARGET_PREFIX,
FOR_ITER_TUPLE_INDEX_PREFIX,
FOR_ITER_TUPLE_PREFIX,
FOR_ITER_VAR_LEN_PREFIX,
FOR_ITER_VAR_NAME_PREFIX,
FOR_ITER_ZIP_TO_LIST_PREFIX,
TRUE_FUNC_PREFIX,
FunctionNameLivenessAnalysis,
create_function_def_node,
create_get_args_node,
create_name_str,
create_nonlocal_stmt_nodes,
create_set_args_node,
)
__all__ = []
GET_ARGS_FUNC_PREFIX = 'get_args'
SET_ARGS_FUNC_PREFIX = 'set_args'
ARGS_NAME = '__args'
class IfElseTransformer(BaseTransformer):
"""
Transform if/else statement of Dygraph into Static Graph.
"""
def __init__(self, root):
self.root = root
FunctionNameLivenessAnalysis(
self.root
) # name analysis of current ast tree.
def transform(self):
"""
Main function to transform AST.
"""
self.visit(self.root)
def visit_If(self, node):
self.generic_visit(node)
(
true_func_node,
false_func_node,
get_args_node,
set_args_node,
return_name_ids,
push_pop_ids,
) = transform_if_else(node, self.root)
new_node = create_convert_ifelse_node(
return_name_ids,
push_pop_ids,
node.test,
true_func_node,
false_func_node,
get_args_node,
set_args_node,
)
return [
get_args_node,
set_args_node,
true_func_node,
false_func_node,
new_node,
]
def visit_Call(self, node):
# Remove `numpy()` statement, like `Tensor.numpy()[i]` -> `Tensor[i]`
if isinstance(node.func, gast.Attribute):
attribute = node.func
if attribute.attr == 'numpy':
node = attribute.value
self.generic_visit(node)
return node
def visit_IfExp(self, node):
"""
Transformation with `true_fn(x) if Tensor > 0 else false_fn(x)`
"""
self.generic_visit(node)
new_node = create_convert_ifelse_node(
None, None, node.test, node.body, node.orelse, None, None, True
)
# Note: A blank line will be added separately if transform gast.Expr
# into source code. Using gast.Expr.value instead to avoid syntax error
# in python.
if isinstance(new_node, gast.Expr):
new_node = new_node.value
return new_node
class NameVisitor(gast.NodeVisitor):
def __init__(self, after_node=None, end_node=None):
# The start node (exclusive) of the visitor
self.after_node = after_node
# The terminate node of the visitor.
self.end_node = end_node
# Dict to store the names and ctxs of vars.
self.name_ids = defaultdict(list)
# List of current visited nodes
self.ancestor_nodes = []
# True when in range (after_node, end_node).
self._in_range = after_node is None
self._candidate_ctxs = (gast.Store, gast.Load, gast.Param)
self._def_func_names = set()
def visit(self, node):
"""Visit a node."""
if self.after_node is not None and node == self.after_node:
self._in_range = True
return
if node == self.end_node:
self._in_range = False
return
self.ancestor_nodes.append(node)
method = 'visit_' + node.__class__.__name__
visitor = getattr(self, method, self.generic_visit)
ret = visitor(node)
self.ancestor_nodes.pop()
return ret
def visit_If(self, node):
"""
For nested `if/else`, the created vars are not always visible for parent node.
In addition, the vars created in `if.body` are not visible for `if.orelse`.
Case 1:
x = 1
if m > 1:
res = new_tensor
res = res + 1 # Error, `res` is not visible here.
Case 2:
if x_tensor > 0:
res = new_tensor
else:
res = res + 1 # Error, `res` is not visible here.
In above two cases, we should consider to manage the scope of vars to parsing
the arguments and returned vars correctly.
"""
if not self._in_range or not self.end_node:
self.generic_visit(node)
return
else:
before_if_name_ids = copy.deepcopy(self.name_ids)
body_name_ids = self._visit_child(node.body)
# If traversal process stops early in `if.body`, return the currently seen name_ids.
if not self._in_range:
self._update_name_ids(before_if_name_ids)
else:
else_name_ids = self._visit_child(node.orelse)
# If traversal process stops early in `if.orelse`, return the currently seen name_ids.
if not self._in_range:
self._update_name_ids(before_if_name_ids)
else:
# Blocks the vars in `if.body` and only inserts the vars both created in 'if/else' branch
# into name_ids.
new_name_ids = self._find_new_name_ids(
body_name_ids, else_name_ids
)
for new_name_id in new_name_ids:
before_if_name_ids[new_name_id].append(gast.Store())
self.name_ids = before_if_name_ids
def visit_Attribute(self, node):
if not self._in_range or not self._is_call_func_name_node(node):
self.generic_visit(node)
def visit_Name(self, node):
if not self._in_range:
self.generic_visit(node)
return
blacklist = {'True', 'False', 'None'}
if node.id in blacklist:
return
if node.id in self._def_func_names:
return
if not self._is_call_func_name_node(node):
if isinstance(node.ctx, self._candidate_ctxs):
self.name_ids[node.id].append(node.ctx)
def visit_Assign(self, node):
if not self._in_range:
self.generic_visit(node)
return
# Visit `value` firstly.
node._fields = ('value', 'targets')
self.generic_visit(node)
def visit_FunctionDef(self, node):
# NOTE: We skip to visit names of get_args and set_args, because they contains
# nonlocal statement such as 'nonlocal x, self' where 'self' should not be
# parsed as returned value in control flow.
if (
GET_ARGS_FUNC_PREFIX in node.name
or SET_ARGS_FUNC_PREFIX in node.name
):
return
if not self._in_range:
self.generic_visit(node)
return
self._def_func_names.add(node.name)
if not self.end_node:
self.generic_visit(node)
else:
before_name_ids = copy.deepcopy(self.name_ids)
self.name_ids = defaultdict(list)
self.generic_visit(node)
if not self._in_range:
self._update_name_ids(before_name_ids)
else:
self.name_ids = before_name_ids
def _visit_child(self, node):
self.name_ids = defaultdict(list)
if isinstance(node, list):
for item in node:
if isinstance(item, gast.AST):
self.visit(item)
elif isinstance(node, gast.AST):
self.visit(node)
return copy.deepcopy(self.name_ids)
def _find_new_name_ids(self, body_name_ids, else_name_ids):
def is_required_ctx(ctxs, required_ctx):
for ctx in ctxs:
if isinstance(ctx, required_ctx):
return True
return False
candidate_name_ids = set(body_name_ids.keys()) & set(
else_name_ids.keys()
)
store_ctx = gast.Store
new_name_ids = set()
for name_id in candidate_name_ids:
if is_required_ctx(
body_name_ids[name_id], store_ctx
) and is_required_ctx(else_name_ids[name_id], store_ctx):
new_name_ids.add(name_id)
return new_name_ids
def _is_call_func_name_node(self, node):
white_func_names = {'append', 'extend'}
if len(self.ancestor_nodes) > 1:
assert self.ancestor_nodes[-1] == node
parent_node = self.ancestor_nodes[-2]
if isinstance(parent_node, gast.Call) and parent_node.func == node:
# e.g: var_list.append(elem), var_list is also a name_id.
should_skip = (
isinstance(node, gast.Attribute)
and node.attr in white_func_names
)
if not should_skip:
return True
return False
def _update_name_ids(self, new_name_ids):
for name_id, ctxs in new_name_ids.items():
self.name_ids[name_id] = ctxs + self.name_ids[name_id]
def _valid_nonlocal_names(return_name_ids, nonlocal_names):
"""
All var in return_name_ids should be in nonlocal_names.
Moreover, we will always put return_name_ids in front of nonlocal_names.
For Example:
return_name_ids: [x, y]
nonlocal_names : [a, y, b, x]
Return:
nonlocal_names : [x, y, a, b]
"""
assert isinstance(return_name_ids, list)
for name in return_name_ids:
if name not in nonlocal_names:
raise ValueError(
f"Required returned var '{name}' must be in 'nonlocal' statement '', but not found."
)
nonlocal_names.remove(name)
return return_name_ids + nonlocal_names
def transform_if_else(node, root):
"""
Transform ast.If into control flow statement of Paddle static graph.
"""
# TODO(liym27): Consider variable like `self.a` modified in if/else node.
return_name_ids = sorted(node.pd_scope.modified_vars())
push_pop_ids = sorted(node.pd_scope.variadic_length_vars())
nonlocal_names = list(return_name_ids)
nonlocal_names.sort()
# NOTE: All var in return_name_ids should be in nonlocal_names.
nonlocal_names = _valid_nonlocal_names(return_name_ids, nonlocal_names)
# TODO(dev): Need a better way to deal this.
# LoopTransformer will create some special vars, which is not visible by users. so we can sure it's safe to remove them.
filter_names = [
ARGS_NAME,
FOR_ITER_INDEX_PREFIX,
FOR_ITER_TUPLE_PREFIX,
FOR_ITER_TARGET_PREFIX,
FOR_ITER_ITERATOR_PREFIX,
FOR_ITER_TUPLE_INDEX_PREFIX,
FOR_ITER_VAR_LEN_PREFIX,
FOR_ITER_VAR_NAME_PREFIX,
FOR_ITER_ZIP_TO_LIST_PREFIX,
]
def remove_if(x):
for name in filter_names:
if x.startswith(name):
return False
return True
nonlocal_names = list(filter(remove_if, nonlocal_names))
return_name_ids = nonlocal_names
nonlocal_stmt_node = create_nonlocal_stmt_nodes(nonlocal_names)
empty_arg_node = gast.arguments(
args=[],
posonlyargs=[],
vararg=None,
kwonlyargs=[],
kw_defaults=None,
kwarg=None,
defaults=[],
)
true_func_node = create_function_def_node(
nonlocal_stmt_node + node.body,
name=unique_name.generate(TRUE_FUNC_PREFIX),
input_args=empty_arg_node,
return_name_ids=[],
)
false_func_node = create_function_def_node(
nonlocal_stmt_node + node.orelse,
name=unique_name.generate(FALSE_FUNC_PREFIX),
input_args=empty_arg_node,
return_name_ids=[],
)
helper = GetterSetterHelper(None, None, nonlocal_names, push_pop_ids)
get_args_node = create_get_args_node(helper.union())
set_args_node = create_set_args_node(helper.union())
return (
true_func_node,
false_func_node,
get_args_node,
set_args_node,
return_name_ids,
push_pop_ids,
)
def create_convert_ifelse_node(
return_name_ids,
push_pop_ids,
pred,
true_func,
false_func,
get_args_func,
set_args_func,
is_if_expr=False,
):
"""
Create `paddle.jit.dy2static.convert_ifelse(
pred, true_fn, false_fn, get_args, set_args, return_name_ids)`
to replace original `python if/else` statement.
"""
if is_if_expr:
true_func_source = f"lambda : {ast_to_source_code(true_func)}"
false_func_source = f"lambda : {ast_to_source_code(false_func)}"
else:
true_func_source = true_func.name
false_func_source = false_func.name
convert_ifelse_layer = gast.parse(
'_jst.IfElse('
'{pred}, {true_fn}, {false_fn}, {get_args}, {set_args}, {return_name_ids}, push_pop_names={push_pop_ids})'.format(
pred=ast_to_source_code(pred),
true_fn=true_func_source,
false_fn=false_func_source,
get_args=(
get_args_func.name if not is_if_expr else 'lambda: None'
), # TODO: better way to deal with this
set_args=(
set_args_func.name if not is_if_expr else 'lambda args: None'
),
return_name_ids=create_name_str(return_name_ids),
push_pop_ids=create_name_str(push_pop_ids),
)
).body[0]
return convert_ifelse_layer