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

623 lines
19 KiB
Python

# 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 copy
import textwrap
import warnings
import numpy as np
from paddle.base import unique_name
from paddle.jit.dy2static.ast_utils import ast_to_source_code
from paddle.utils import gast
from ..utils import PADDLE_MODULE_PREFIX, is_api_in_module_helper
GET_ARGS_FUNC_PREFIX = 'get_args'
SET_ARGS_FUNC_PREFIX = 'set_args'
ARGS_NAME = '__args'
TRUE_FUNC_PREFIX = 'true_fn'
FALSE_FUNC_PREFIX = 'false_fn'
FOR_ITER_INDEX_PREFIX = '__for_loop_var_index'
FOR_ITER_TUPLE_PREFIX = '__for_loop_iter_tuple'
FOR_ITER_TARGET_PREFIX = '__for_loop_iter_target'
FOR_ITER_ITERATOR_PREFIX = '__for_loop_iter_iterator'
FOR_ITER_TUPLE_INDEX_PREFIX = '__for_loop_iter_tuple_index'
FOR_ITER_VAR_LEN_PREFIX = '__for_loop_var_len'
FOR_ITER_VAR_NAME_PREFIX = '__for_loop_iter_var'
FOR_ITER_ZIP_TO_LIST_PREFIX = '__for_loop_iter_zip'
WHILE_CONDITION_PREFIX = 'while_condition'
WHILE_BODY_PREFIX = 'while_body'
FOR_CONDITION_PREFIX = 'for_loop_condition'
FOR_BODY_PREFIX = 'for_loop_body'
def index_in_list(array_list, item):
try:
return array_list.index(item)
except ValueError:
# Item not in array_list
return -1
class BaseNodeVisitor(gast.NodeVisitor):
"""
Implement customized NodeVisitor inherited from gast.NodeVisitor.
Ancestor nodes are traced to easily support more operations of currently
visited node.
"""
def __init__(self):
self.ancestor_nodes = []
def visit(self, node):
"""Visit a node."""
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 create_undefined_var(name):
func_code = f"{name} = _jst.UndefinedVar('{name}')"
return gast.parse(func_code).body[0]
def create_bool_node(name, value):
'''
Create a assign stmt for name = value .
'''
assert isinstance(value, bool)
node = f"{name} = {value}"
return gast.parse(node).body[0]
def get_parent_mapping(root):
to_parent: dict[gast.AST, gast.AST] = {}
for node in gast.walk(root):
for child in gast.iter_child_nodes(node):
to_parent[child] = node
return to_parent
def create_name_str(name_ids):
"""
Return "('x', 'y')" for [x, y]
"""
if not name_ids:
return 'None'
names_str = ["'{}'".format(name.replace("'", "\\'")) for name in name_ids]
return "({}, )".format(','.join(names_str))
def create_function_def_node(nodes, name, input_args, return_name_ids):
"""
Wrapper all statements of nodes into one ast.FunctionDef, which can be
called by ast.Call.
"""
nodes = copy.copy(nodes)
# add return statement
if return_name_ids:
nodes.append(gast.Return(value=generate_name_node(return_name_ids)))
else:
nodes.append(gast.Return(value=None))
func_def_node = gast.FunctionDef(
name=name,
args=input_args,
body=nodes,
decorator_list=[],
returns=None,
type_comment=None,
type_params=[],
)
return func_def_node
def create_assign_node(name, node):
"""
Creates a `gast.Assign` node by given name_id as target and node as value.
"""
targets = generate_name_node(name, ctx=gast.Store())
assign_node = gast.Assign(
targets=[targets],
value=node,
type_comment=None,
)
return targets, assign_node
def create_get_args_node(names):
"""
Create get_args function as follows:
def get_args_0():
nonlocal x, y
return x, y
"""
def empty_node():
func_def = f"""
def {unique_name.generate(GET_ARGS_FUNC_PREFIX)}():
return
"""
return gast.parse(textwrap.dedent(func_def)).body[0]
assert isinstance(names, (list, tuple))
node = create_nonlocal_stmt_nodes(names)
if not names:
return empty_node()
if node == []:
nonlocal_vars = "\n"
else:
nonlocal_vars = ast_to_source_code(node[0])
template = """
def {func_name}():
{nonlocal_vars}
return {vars},
"""
func_def = template.format(
func_name=unique_name.generate(GET_ARGS_FUNC_PREFIX),
nonlocal_vars=nonlocal_vars,
vars=",".join(names),
)
return gast.parse(textwrap.dedent(func_def)).body[0]
def create_set_args_node(names):
"""
Create set_args function as follows:
def set_args_0(__args):
nonlocal x, y
x, y = __args
"""
def empty_node():
func_def = f"""
def {unique_name.generate(SET_ARGS_FUNC_PREFIX)}({ARGS_NAME}):
pass
"""
return gast.parse(textwrap.dedent(func_def)).body[0]
assert isinstance(names, (list, tuple))
node = create_nonlocal_stmt_nodes(names)
if not names:
return empty_node()
if node == []:
nonlocal_vars = "\n"
else:
nonlocal_vars = ast_to_source_code(node[0])
template = """
def {func_name}({args}):
{nonlocal_vars}
{vars}, = {args}
"""
func_def = template.format(
func_name=unique_name.generate(SET_ARGS_FUNC_PREFIX),
args=ARGS_NAME,
nonlocal_vars=nonlocal_vars,
vars=",".join(names),
)
return gast.parse(textwrap.dedent(func_def)).body[0]
def create_nonlocal_stmt_nodes(names):
assert isinstance(names, (list, tuple))
mapped = list(filter(lambda n: '.' not in n, names))
mapped = list(filter(lambda n: '[' not in n, mapped))
names = sorted(
mapped, key=mapped.index
) # to keep the order, we can't use set() to unique
if not names:
return []
func_code = "nonlocal {}".format(','.join(names))
return [gast.parse(func_code).body[0]]
def generate_name_node(name_ids, ctx=gast.Load(), gen_tuple_if_single=False):
"""
If name_ids is list or tuple or set with multiple strings, this function
generates gast.Tuple of gast.Name.
If the name_ids is single string or contains only 1 string, this function
returns gast.Name if gen_tuple_if_single==False else returns gast.Tuple
with only one gast.Name
This function is used at several gast.Return statements.
"""
if isinstance(name_ids, str):
name_ids = [name_ids]
if not isinstance(name_ids, (list, tuple, set)):
raise TypeError(
f'name_ids must be list or tuple or set, but received {type(name_ids)}'
)
def create_node_for_name(name):
if '.' not in name:
return gast.Name(
id=name, ctx=ctx, annotation=None, type_comment=None
)
return gast.parse(name).body[0].value
gast_names = [create_node_for_name(name_id) for name_id in name_ids]
if len(gast_names) == 1 and not gen_tuple_if_single:
name_node = gast_names[0]
else:
name_node = gast.Tuple(elts=gast_names, ctx=ctx)
return name_node
def get_attribute_full_name(node):
assert isinstance(node, gast.Attribute), (
"Input non-Attribute node to get attribute full name"
)
return ast_to_source_code(node).strip()
def is_api_in_module(node, module_prefix):
assert isinstance(node, gast.Call), (
"Input non-Call node for is_api_in_module"
)
# Python can have gast.Call as function, for example: convert_call(func)(x)
# We only check the most outside function
func_node = node.func
while isinstance(func_node, gast.Call):
func_node = func_node.func
func_str = ast_to_source_code(func_node).strip()
try:
import paddle
import paddle.jit.dy2static as _jst
from paddle import to_tensor
globals = {
'np': np,
'paddle': paddle,
'_jst': _jst,
'to_tensor': to_tensor,
}
fn = eval(func_str, globals)
return is_api_in_module_helper(fn, module_prefix)
except Exception:
return False
def is_paddle_api(node):
return is_api_in_module(node, PADDLE_MODULE_PREFIX)
class NameScope:
def __init__(self):
"""
A NameScope is a object which manager all the variable names.
only FunctionDef and Controlflow node will have a namescope property.
type can be "function" and "controlflow"
we don't analyze the read only variable because they don't affect the analysis.
"""
self.globals = set()
self.nonlocals = set()
self.args = set()
self.father = None # point to the nearest function name scope.
self.w_vars = set() # all qualified + normal names been stored
self.created = set() # useful for control flow compatibility
# only valid in control_flow nodes
# may be remove later.
self.push_pop_vars = set() # we call push and pop in the vars
def set_father(self, father):
self.father = father
def existed_vars(self):
"""vars existing in current scope.
they must not contain qualified names.
"""
local_vars = self.w_vars - self.globals - self.nonlocals - self.args
return set(filter(lambda x: '.' not in x, local_vars))
def created_vars(self):
return self.created
def modified_vars(self):
# may be globals / non-locals / args / qualified names and created_vars
return self.w_vars
def variadic_length_vars(self):
"""
At present, we do not support global append, such as
import numpy as np
a = []
def func():
a.append() # global names `a`, we will raise a warning.
p.append(a, 1) # global names `np`, we will raise a warning.
"""
non_global_push_pop_names = []
for var in self.push_pop_vars:
if self._is_simple_name(var) and self.is_global_var(var):
warnings.warn(
f"Find variable `{var}` defined in global scope"
f" and call `{var}.append() or {var}.pop()`"
f", which will be ignored and never be transferred into"
f" tensor array."
)
else:
non_global_push_pop_names.append(var)
return set(non_global_push_pop_names)
def control_flow_vars(self):
valid_names = self.w_vars
tmp = (self.father.global_vars & valid_names,)
return {"global": tmp, "nonlocal": self.w_vars - tmp}
def _is_simple_name(self, name):
if '.' in name or '[' in name:
return False
return True
def is_global_var(self, name):
"""
Return whether the name is a var created in global scope.
Search from bottom to top. If it is not created or modified,
it means global vars; otherwise, it means local vars.
Only valid after FunctionNameLivenessAnalysis visitor.
"""
assert self._is_simple_name(name), (
"is_global_var accept a simple name, but get `{name}`."
)
ancestor = self
while ancestor is not None:
if name in ancestor.globals:
return True
if name in (ancestor.nonlocals | ancestor.w_vars):
return False
ancestor = ancestor.father
return True
def is_local_var(self, name):
return not self.is_global_var(name)
def merge_from(self, name_scope):
self.globals |= name_scope.globals
self.nonlocals |= name_scope.nonlocals
self.args |= name_scope.args
self.w_vars |= name_scope.w_vars
self.push_pop_vars |= name_scope.push_pop_vars
class FunctionNameLivenessAnalysis(gast.NodeVisitor):
"""analyze the liveness of a function.
every variables stored in this scope will be collected,
in addition with global/nonlocal information and
push_pop information.
1. global variable is stored in node.var_globals.
2. nonlocal variable is stored in node.var_nonlocals.
3. arguments is stored in node.var_args.
4. if a variable's push and pop attribute is called,
it will be collected in push_pop_vars. They are
used for transformation to tensor_array.
NOTE: push_pop_vars **may not** in w_vars.
a.push(0) don't modify the variable a, but the content
of a.
For example:
def func(*args, **kargs):
a = 12
global i,j
nonlocal x,y
print(a)
i = k
b = []
c = [1,2,3]
for m in range(10):
q = 12
b.push(1)
c.pop()
After this visitor we have:
# node is the FunctionDef node with name: "func"
node.pd_scope = NameScope(
globals = ['i', 'j'],
nonlocals = ['x', 'y'],
args = ['args', 'kargs'],
wr_vars = ['a', 'i', 'q', 'm', 'c', 'b']
push_pop_vars = ['b', 'c']
)
"""
def __init__(self, root_node):
self.scope_node_stack = [] # controlflow, functiondef node
self.visit(root_node)
def _reset_name_scope(self, node):
# always reset the node as empty namescope.
node.pd_scope = NameScope()
def _get_name_scope(self, node):
if not hasattr(node, "pd_scope"):
node.pd_scope = NameScope()
return node.pd_scope
def _current_name_scope(self):
return self._get_name_scope(self.scope_node_stack[-1])
def _father_name_scope(self):
if len(self.scope_node_stack) == 1:
return None
return self._get_name_scope(self.scope_node_stack[-2])
def _nearest_function_scope(self):
if len(self.scope_node_stack) == 1:
return None
for node in self.scope_node_stack[-2::-1]:
if isinstance(node, gast.FunctionDef):
return self._get_name_scope(node)
def visit_ListComp(self, node):
"""[ i for i in range(10) ]
In this case, `i` will not created in FunctionScope.
We don't collect `i` by not calling generic_visit.
"""
pass
def visit_DictComp(self, node):
"""the same as ListComp."""
pass
def visit_Name(self, node):
self.generic_visit(node)
write_context = (gast.Store, gast.AugStore, gast.Del)
if isinstance(node.ctx, write_context):
self._current_name_scope().w_vars.add(node.id)
def visit_FunctionDef(self, node):
def pre_func():
self._current_name_scope().args |= set(
self._get_argument_names(node)
)
def post_func():
"""NOTE: why we need merge w_vars and push_pop_vars here ?
because we do ifelse_transformer after loop_transformer. Loops will changed into functions. but we know this function will be called in if. so we add w_vars to father function scope.
"""
control_flow_function_def = [
WHILE_BODY_PREFIX,
WHILE_BODY_PREFIX,
FOR_CONDITION_PREFIX,
FOR_BODY_PREFIX,
TRUE_FUNC_PREFIX,
FALSE_FUNC_PREFIX,
]
def is_control_flow_def_node():
for prefix in control_flow_function_def:
if node.name.startswith(prefix):
return True
return False
if self._father_name_scope() and is_control_flow_def_node():
self._father_name_scope().w_vars |= (
self._current_name_scope().w_vars
)
self._father_name_scope().push_pop_vars |= (
self._current_name_scope().push_pop_vars
)
self._visit_scope_node(node, pre_func, post_func)
def _visit_scope_node(self, node, pre_func, post_func):
"""scope node main visit logic.
pre_func and post_func is callbacks
"""
self._reset_name_scope(node)
self.scope_node_stack.append(node)
self._current_name_scope().set_father(self._nearest_function_scope())
if pre_func:
pre_func()
self.generic_visit(node)
if post_func:
post_func()
self.scope_node_stack.pop()
def _visit_controlflow_node(self, node):
def post_func():
self._father_name_scope().merge_from(self._current_name_scope())
self._nearest_function_scope().merge_from(
self._current_name_scope()
)
self._current_name_scope().created = (
self._nearest_function_scope().existed_vars()
- node.before_created
)
# gather created vars into father and used in CreateUndefinedVarTransform
self._nearest_function_scope().created |= (
self._current_name_scope().created
)
def pre_func():
node.before_created = self._nearest_function_scope().existed_vars()
self._visit_scope_node(node, pre_func, post_func)
def visit_For(self, node):
self._visit_controlflow_node(node)
def visit_While(self, node):
self._visit_controlflow_node(node)
def visit_If(self, node):
self._visit_controlflow_node(node)
def visit_Global(self, node):
self._current_name_scope().globals |= set(node.names)
def visit_Nonlocal(self, node):
self._current_name_scope().nonlocals |= set(node.names)
def visit_Attribute(self, node):
self.generic_visit(node)
write_context = (gast.Store, gast.AugStore, gast.Del)
if isinstance(node.ctx, write_context):
name = ast_to_source_code(node).strip()
self._current_name_scope().w_vars.add(name)
def visit_Subscript(self, node):
self.generic_visit(node)
write_context = (gast.Store, gast.AugStore, gast.Del)
if isinstance(node.ctx, write_context):
while isinstance(node.value, gast.Subscript):
node = node.value
if isinstance(node.value, gast.Name):
self._current_name_scope().w_vars.add(node.value.id)
def visit_Call(self, node):
self.generic_visit(node)
if not isinstance(node.func, gast.Attribute):
return
variadic_length_method = ['append', 'pop']
if node.func.attr not in variadic_length_method:
return
# we don't treat push and pop as a write operator. such as a[i]=10 is not modify a.
name = ast_to_source_code(node.func.value).strip()
self._current_name_scope().push_pop_vars.add(name)
def _get_argument_names(self, node):
"""get all arguments name in the functiondef node.
this node is local to the function and shouldn't
be created.
"""
assert isinstance(node, gast.FunctionDef), (
"Input node is not function define node"
)
names = list(node.args.args)
names.append(node.args.vararg)
names.append(node.args.kwarg)
names = [i.id for i in names if i is not None]
return names