Files
paddlepaddle--paddle/python/paddle/incubate/autograd/primx.py
T
2026-07-13 12:40:42 +08:00

691 lines
25 KiB
Python

# Copyright (c) 2022 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 logging
import typing
from collections import OrderedDict
from typing import TYPE_CHECKING, TypeVar
import paddle
from paddle.base import framework
from paddle.base.core import ops_contain_none, prim_config
from paddle.base.framework import Operator, default_main_program
from paddle.incubate.autograd.utils import as_tensors
from .composite_rules import _composite
from .primreg import (
lookup_composite,
lookup_orig2prim,
lookup_prim2orig,
)
from .primrules import _orig2prim, _prim2orig
from .utils import (
flatten_and_remove_none,
get_input_var_list,
get_output_var_list,
map_output_for_composite,
prepare_python_api_arguments,
)
if TYPE_CHECKING:
from collections.abc import Sequence
from paddle import Tensor
from paddle.base.framework import Block
_TensorOrTensorsT = TypeVar("_TensorOrTensorsT", Tensor, Sequence[Tensor])
def topo_path(
xs: Sequence[Tensor], ys: Sequence[Tensor], block: Block | None = None
) -> tuple[list[Tensor], list[Tensor], list[Tensor]]:
"""Returns the list of ops on the path from `xs` to `ys` in topological
order.
TODO(Tongxin): supporting control flow and nested blocks.
Args:
xs: a list|tuple of vars as source
ys: a list|tuple of vars as sink
block: the program block containing the path, optional
Returns:
(path, unused_xs, unreached_ys): a tuple comprised of the resulting op
path, the unused variables in `xs`, and the unreached variables in `ys`
"""
block = default_main_program().current_block() if block is None else block
path = []
backpath = []
reached_vars = OrderedDict()
used_vars = OrderedDict()
# Initialize reached vars
for x in xs:
assert x is None or x.block == block, (
'x is not None and x.block != block'
)
reached_vars[id(x)] = x
# Reaching test, returning whether an op is reached from the given input
reaching = lambda op: any(
id(v) in reached_vars
for v in flatten_and_remove_none(get_input_var_list(op))
)
# block.ops are supposedly in the order that preserves correct data
# dependence.
# Forward pass to identify all reached variables and ops
for op in block.ops:
if reaching(op):
path.append(op)
for var in flatten_and_remove_none(get_output_var_list(op)):
reached_vars[id(var)] = var
used_vars = OrderedDict((id(y), y) for y in ys if id(y) in reached_vars)
back_reaching = lambda op: any(
id(out) in used_vars
for out in flatten_and_remove_none(get_output_var_list(op))
)
# Backward pass to find all used variables
for op in reversed(path):
if back_reaching(op):
backpath.append(op)
for var in flatten_and_remove_none(get_input_var_list(op)):
used_vars[id(var)] = var
unused_xs = [x for x in xs if id(x) not in used_vars]
unreached_ys = [y for y in ys if id(y) not in reached_vars]
return list(reversed(backpath)), unused_xs, unreached_ys
def output_vars_on_path(
path: Sequence[Operator],
) -> OrderedDict[int, list[Tensor]]:
"""Returns the output variables of all the ops on the path from `xs`
to `ys`.
Args:
path: a list of ops on which to find the output variables
Returns:
vars: the output vars
"""
vars = OrderedDict()
for op in path:
for out in flatten_and_remove_none(get_output_var_list(op)):
vars[id(out)] = out
return vars
class VarMap:
"""A general map data structure for linking variables to variables.
An example is linking variables to their gradients.
"""
__slots__ = ['name', 'varset', 'tab']
name: str
varset: OrderedDict[int, Tensor]
tab: OrderedDict[int, int]
def __init__(self, name: str, varset: OrderedDict[int, Tensor]) -> None:
self.name = name
self.varset = varset
self.tab = OrderedDict()
def add(self, key_var: Tensor, value_var: Tensor) -> None:
self.tab[id(key_var)] = id(value_var)
def add_rec(self, key_vars: Tensor, value_vars: Tensor | None) -> None:
if value_vars is None:
return
if isinstance(
key_vars, (paddle.base.framework.Variable, paddle.pir.Value)
):
if not isinstance(
value_vars, (paddle.base.framework.Variable, paddle.pir.Value)
):
raise TypeError(
f'value_vars must be Variable, but got {type(value_vars)}'
)
self.tab[id(key_vars)] = id(value_vars)
else:
assert len(key_vars) == len(value_vars), (
f'len(key_vars) should be equal to len(value_vars), '
f'but len(key_vars)={len(key_vars)} and len(value_vars)={len(value_vars)}.'
)
for key_var, value_var in zip(key_vars, value_vars):
self.add_rec(key_var, value_var)
def lookup(self, key_var: Tensor) -> Tensor | None:
value_id = self.tab.get(id(key_var))
if value_id is not None:
return self.varset.get(value_id)
else:
return None
def delete(self, key_var: Tensor) -> None:
varid = id(key_var)
if varid in self.tab:
del self.tab[id(key_var)]
def delete_keyvars(self, key_vars: Sequence[Tensor]) -> None:
for var in key_vars:
varid = id(var)
if varid in self.tab:
del self.tab[varid]
def delete_valuevars(self, value_vars: Sequence[Tensor]) -> None:
ids = [id(v) for v in value_vars]
keys = [k for k, v in self.tab.items() if v in ids]
for k in keys:
del self.tab[k]
def contain_var(self, key_var: Tensor) -> bool:
return self.tab.__contains__(id(key_var))
def contain_value(self, value_var: Tensor) -> bool:
return id(value_var) in self.tab.values()
# TODO(lml): supporting control flow, nested blocks, and block other than current block of main program.
class Transform:
"""An object that maintains the state of transformations applied to a
primitive program."""
block: Block
vars: OrderedDict[int, Tensor]
var2dot: VarMap
dot2bar: VarMap
def __init__(self, block: Block) -> None:
assert block == default_main_program().current_block(), (
'only support transform on current block of main program.'
)
self.block = block
self.vars = self.init_vars(block)
self.var2dot = VarMap('var2dot', self.vars)
self.dot2bar = VarMap('dot2var', self.vars)
def init_vars(self, block: Block) -> OrderedDict[int, Tensor]:
vars = OrderedDict()
for _, var in block.vars.items():
vars[id(var)] = var
return vars
def add_vars(self, new_vars: Sequence[Tensor | None]) -> None:
self.vars.update({id(v): v for v in new_vars if v is not None})
def add_vars_rec(self, new_vars: Tensor | list[Tensor] | None) -> None:
if new_vars is None:
return
if isinstance(
new_vars, (paddle.base.framework.Variable, paddle.pir.Value)
):
self.vars.update({id(new_vars): new_vars})
return
if not isinstance(new_vars, list):
raise TypeError(f'new_vars must be list, but got {type(new_vars)}')
for var in new_vars:
self.add_vars_rec(var)
def erase_ops(self, ordered_indexes: Sequence[int]) -> None:
block = self.block
for op_index in reversed(ordered_indexes):
block.desc._remove_op(op_index, op_index + 1)
# remove from block.ops
for op_index in reversed(ordered_indexes):
del block.ops[op_index]
block._sync_with_cpp()
def erase_dots(self, vars_to_erase: Sequence[Tensor]) -> None:
for var in vars_to_erase:
if id(var) in self.vars:
del self.vars[id(var)]
self.dot2bar.delete_keyvars(vars_to_erase)
self.var2dot.delete_valuevars(vars_to_erase)
block = self.block
for var in vars_to_erase:
name = var.name
block.desc._remove_var(name.encode())
del block.vars[name]
block._sync_with_cpp()
def var2dot_rec(self, vars: _TensorOrTensorsT) -> _TensorOrTensorsT:
"""Lookup var2dot recursively."""
if isinstance(vars, (paddle.base.framework.Variable, paddle.pir.Value)):
dot = self.var2dot.lookup(vars)
return dot
dots = [self.var2dot_rec(var) for var in vars]
return dots
def dot2bar_rec(self, dots: _TensorOrTensorsT) -> _TensorOrTensorsT:
if isinstance(dots, (paddle.base.framework.Variable, paddle.pir.Value)):
bar = self.dot2bar.lookup(dots)
assert bar is not None, 'bar must be not None'
return bar
bars = [self.dot2bar_rec(dot) for dot in dots]
return bars
# TODO(lml): supporting control flow, nested blocks, and block other than current block of main program.
def _lower(block, reverse, blacklist):
# Some functions which are only used in _lower.
def bind(args, to_bind, value_table):
for i in range(len(args)):
if isinstance(args[i], list):
bind(args[i], to_bind, value_table)
elif args[i] is not None and args[i].name in to_bind:
args[i] = value_table[to_bind[args[i].name]]
def bind_name(names, to_bind):
return_list = []
for name in names:
if isinstance(name, list):
return_list.append(bind_name(name, to_bind))
else:
return_list.append(to_bind[name] if name in to_bind else name)
return return_list
def expand_nested_list(xs):
return_list = []
for x in xs:
if isinstance(x, list):
return_list = return_list + expand_nested_list(x)
else:
return_list.append(x)
return return_list
# Step1: Do some preparatory work for lower
lower_fn = _prim2orig if reverse else _orig2prim
lookup_fn = lookup_prim2orig if reverse else lookup_orig2prim
value_table = {}
to_bind = {}
to_bind_rev = {}
for var in block.desc.all_vars():
value_table[var.name()] = block.var(var.name())
ops_to_remove = []
vars_to_remove = set()
# Step2: Process all ops in the target block
for op_idx in range(len(block.ops)):
op = block.ops[op_idx]
ops_to_remove.append(op_idx)
if lookup_fn(op.type) is not None and op.type not in blacklist:
input_args = get_input_var_list(op)
bind(input_args, to_bind, value_table)
for orig_out, new_out in zip(
expand_nested_list(get_output_var_list(op)),
expand_nested_list(as_tensors(lower_fn(op, *input_args))),
):
assert not (orig_out is None) ^ (new_out is None), (
"orig_out and new_out should match."
)
vars_to_remove.add(new_out.name)
value_table[new_out.name] = new_out
to_bind[orig_out.name] = new_out.name
to_bind_rev[new_out.name] = orig_out.name
else:
inputs = {}
for i in range(len(op.input_names)):
inputs[op.input_names[i]] = bind_name(
op.input(op.input_names[i]), to_bind
)
outputs = {}
for i in range(len(op.output_names)):
outputs[op.output_names[i]] = op.output(op.output_names[i])
attrs = {}
for name in sorted(op.attr_names):
attrs[name] = op.attr(name)
from paddle.base.dygraph.base import param_guard
new_op_desc = block.desc.append_op()
with param_guard(inputs), param_guard(outputs):
op = Operator(
block=block,
desc=new_op_desc,
type=op.type,
inputs=inputs,
outputs=outputs,
attrs=attrs,
)
block.ops.append(op)
# Step3: Do some post-processing work
for op_idx in reversed(ops_to_remove):
block.desc._remove_op(op_idx, op_idx + 1)
del block.ops[op_idx]
block._sync_with_cpp()
for op_idx in range(len(block.ops)):
op = block.ops[op_idx]
for in_name in op.input_arg_names:
if in_name in to_bind_rev:
op._rename_input(in_name, to_bind_rev[in_name])
for out_name in op.output_arg_names:
if out_name in to_bind_rev:
op._rename_output(out_name, to_bind_rev[out_name])
for var_name in sorted(vars_to_remove):
assert var_name in to_bind_rev, (
f'var_name "{var_name}" is not in to_bind_rev.'
)
if var_name != to_bind_rev[var_name]:
block.desc._remove_var(var_name.encode())
del block.vars[var_name]
block._sync_with_cpp()
def _lower_composite(
block,
filter_: typing.Callable[[framework.Operator], bool] = lambda x: True,
start_idx=-1,
backward_length=-1,
):
"""The operators in block which satisfy the filter condition will be decomposite into primitives."""
def bind(args, to_bind, value_table):
for i in range(len(args)):
if isinstance(args[i], list):
bind(args[i], to_bind, value_table)
if not isinstance(
args[i], (paddle.base.framework.Variable, paddle.pir.Value)
):
continue
elif args[i] is not None and args[i].name in to_bind:
args[i] = value_table[to_bind[args[i].name]]
def bind_name(names, to_bind):
return_list = []
for name in names:
if isinstance(name, list):
return_list.append(bind_name(name, to_bind))
else:
return_list.append(to_bind[name] if name in to_bind else name)
return return_list
def expand_nested_list(xs):
return_list = []
for x in xs:
if isinstance(x, list):
return_list = return_list + expand_nested_list(x)
else:
return_list.append(x)
return return_list
if isinstance(block, paddle.base.framework.Block):
logging.info("Atomize composite op to primitive ops begin.")
# Step1: Do some preparatory work for lower
lower_fn = _composite
lookup_fn = lookup_composite
value_table = {}
to_bind = {}
to_bind_rev = {}
for var in block.desc.all_vars():
value_table[var.name()] = block.var(var.name())
ops_to_remove = []
vars_to_remove = set()
# if output var of composite rule is None, this means this var is not needed
none_vars_to_remove = set()
change = None
# Only process required sliced block
# If given start_idx, only ops[start_idx:] will be processed.
# If given backward_length, only ops[:-backward_length] will be processed.
# Note, start_idx and backward_length cannot be both given, because the length of non-processed part must be kept unchanged.
length = len(block.ops)
idx_list = range(length)
assert -1 <= backward_length <= length, (
f'expect -1 <= backward_length <= {length}, but got backward_length: {backward_length}'
)
assert -1 <= start_idx <= length, (
f'expect -1 <= start_idx <= {length}, but got start_idx: {start_idx}'
)
assert not (backward_length > -1 and start_idx > -1), (
f'got start_idx: {start_idx} and backward_length: {backward_length}'
)
if backward_length > -1:
idx_list = range(length - backward_length)
if start_idx > -1:
idx_list = range(start_idx, length)
lower = lower_pre = False # Flag of routing to lower or copy branch
# Step2: Process all ops in the target block
for op_idx in range(length):
op = block.ops[op_idx]
ops_to_remove.append(op_idx)
op_name = op.type
# NOTE: why need _sync_with_cpp here
# _sync_with_cpp after every copied operator is very slow.
# However, _sync_with_cpp only support continuous block currently.
# The lowering transformation will generate program which is
# crossed combination of copy block and lower block, such as
# op1(copy) -> op2(copy) -> op3(lower) -> op4(lower) -> op5(copy) -> op6(copy)
# It will cause _sync_with_cpp error.
# So, _sync_with_cpp will be executed only once after every continuous copy block.
lower = (
(lookup_fn(op_name) is not None)
and filter_(op)
and op_idx in idx_list
)
if not lower_pre and lower:
block._sync_with_cpp()
lower_pre = lower
if lower:
change = True
prim_config["composite_ops_record"].add(op_name)
input_args = prepare_python_api_arguments(op)
bind(input_args, to_bind, value_table)
orig_outs = expand_nested_list(map_output_for_composite(op))
new_outs = expand_nested_list(
as_tensors(lower_fn(op, *input_args))
)
assert len(orig_outs) == len(new_outs), (
f'when replace origin op {op_name} with composite rule, num of origin outs should be equal to new outs, '
f'but len(orig_outs) = {len(orig_outs)} and len(new_outs) = {len(new_outs)}'
)
for orig_out, new_out in zip(
orig_outs,
new_outs,
):
if (orig_out is None or new_out is None) and (
op_name not in ops_contain_none
):
raise ValueError(
f"op {op_name} should not contain any None value. original outs={orig_outs} and its composite rule outs={new_outs}"
)
if orig_out is None:
# to keep same as phi op definition, orig_out may receive None
continue
elif new_out is not None:
assert orig_out.dtype == new_out.dtype, (
f'when replace origin op {op_name} with composite rule, origin out dtype should be equal to new out dtype, '
f'but orig_out: {orig_out.name}.dtype={orig_out.dtype} and new_out: {new_out.name}.dtype={new_out.dtype}'
)
assert -1 not in new_out.shape, (
f'when replace origin op {op_name} with composite rule, composite out shape has -1.'
)
assert orig_out.shape == new_out.shape, (
f'when replace origin op {op_name} with composite rule, origin out shape should be equal to new out shape, '
f'but orig_out: {orig_out.name}.shape={orig_out.shape} and new_out: {new_out.name}.shape={new_out.shape}'
)
assert not (orig_out is None) ^ (new_out is None), (
"orig_out and new_out should match."
)
vars_to_remove.add(new_out.name)
value_table[new_out.name] = new_out
to_bind[orig_out.name] = new_out.name
to_bind_rev[new_out.name] = orig_out.name
else:
none_vars_to_remove.add(orig_out.name)
else:
op_desc = block.desc.append_op()
op_desc.copy_from(op.desc)
block._sync_with_cpp()
# Step3: Do some post-processing work
for op_idx in reversed(ops_to_remove):
block.desc._remove_op(op_idx, op_idx + 1)
del block.ops[op_idx]
block._sync_with_cpp()
for op_idx in range(len(block.ops)):
op = block.ops[op_idx]
for in_name in op.input_arg_names:
if in_name in to_bind_rev:
op._rename_input(in_name, to_bind_rev[in_name])
for out_name in op.output_arg_names:
if out_name in to_bind_rev:
op._rename_output(out_name, to_bind_rev[out_name])
for var_name in sorted(vars_to_remove):
assert var_name in to_bind_rev, (
f'var_name "{var_name}" is not in to_bind_rev.'
)
if var_name != to_bind_rev[var_name]:
block.desc._remove_var(var_name.encode())
del block.vars[var_name]
block._sync_with_cpp()
for var_name in sorted(none_vars_to_remove):
block.desc._remove_var(var_name.encode())
del block.vars[var_name]
block._sync_with_cpp()
for op in block.ops:
if op._has_kernel(op.desc.type()):
op.desc.infer_var_type(block.desc)
op.desc.infer_shape(block.desc)
# composite ops may contain other composite ops, thus, call _lower_composite again.
if change:
_lower_composite(
block,
filter_,
start_idx=start_idx,
backward_length=backward_length,
)
return
elif isinstance(block, typing.Sequence):
for item in block:
_lower_composite(
item,
filter_,
start_idx=start_idx,
backward_length=backward_length,
)
return
else:
raise TypeError
@framework.static_only
def orig2prim(block: Block | None = None) -> None:
"""
Note:
**This API is ONLY available in the static graph mode.**
**Args block must be None or current block of main program.**
All operators in the target block are processed as follows.
If it is an original operator, it will be transformed into
one or a series of automatic differential basic operators with
equivalent function.
Args:
block(paddle.static.Block|None, optional): The
target block to process on. Default None, and will
process on the current block of main program.
"""
block = default_main_program().current_block() if block is None else block
assert block == default_main_program().current_block(), (
'block is neither None nor current block of main program'
)
_lower(block, reverse=False, blacklist=[])
@framework.static_only
def prim2orig(
block: Block | None = None, blacklist: list[str] | None = None
) -> None:
"""
Note:
**ONLY available in the static graph mode.**
**Args block must be None or current block of main program.**
All operators in the target block are processed as follows.
If it is an automatic differential basic operator, it will be
transformed into one or a series of original operators with
equivalent function to support execution.
Args:
block(paddle.static.Block|None, optional): The
target block to process on. Default None, and will
process on the current block of main program.
blacklist(list[string]|None, optional): The names of automatic
differential basic operator that will not be transformed
into original operators. Default None, and the blacklist
is treated as empty list.
Examples:
.. code-block:: pycon
>>> import paddle
>>> from paddle.incubate.autograd import enable_prim, prim_enabled, prim2orig
>>> paddle.enable_static()
>>> enable_prim()
>>> x = paddle.ones(shape=[2, 2], dtype='float32')
>>> x.stop_gradient = False
>>> y = x * x
>>> dy_dx = paddle.static.gradients(y, x)
>>> if prim_enabled():
... prim2orig()
"""
block = default_main_program().current_block() if block is None else block
assert block == default_main_program().current_block(), (
'block is neither None nor current block of main program'
)
blacklist = [] if blacklist is None else blacklist
_lower(block, reverse=True, blacklist=blacklist)