paddlepaddle--paddle
1153 行
41 KiB
Python
1153 行
41 KiB
Python
# 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 logging
|
|
import math
|
|
import os
|
|
import time
|
|
from typing import TYPE_CHECKING
|
|
|
|
import paddle
|
|
from paddle import pir
|
|
from paddle.autograd import backward_utils
|
|
from paddle.base import core
|
|
from paddle.base.framework import in_cinn_debug_mode
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Sequence
|
|
|
|
_PADDLE_DTYPE_2_NBYTES = {
|
|
core.DataType.BOOL: 1,
|
|
core.DataType.FLOAT16: 2,
|
|
core.DataType.BFLOAT16: 2,
|
|
core.DataType.FLOAT32: 4,
|
|
core.DataType.FLOAT64: 8,
|
|
core.DataType.FLOAT8_E4M3FN: 1,
|
|
core.DataType.FLOAT8_E5M2: 1,
|
|
core.DataType.INT8: 1,
|
|
core.DataType.INT16: 2,
|
|
core.DataType.INT32: 4,
|
|
core.DataType.INT64: 8,
|
|
core.DataType.UINT8: 1,
|
|
core.DataType.COMPLEX64: 8,
|
|
core.DataType.COMPLEX128: 16,
|
|
}
|
|
|
|
# define the default recompute ops that can be fused between pairs
|
|
DEFAULT_RECOMPUTABLE_OPS: list[str] = [
|
|
"pd_op.full_int_array",
|
|
"pd_op.full",
|
|
# "pd_op.sum",
|
|
"pd_op.divide",
|
|
"pd_op.subtract",
|
|
"pd_op.add",
|
|
"pd_op.multiply",
|
|
"pd_op.elementwise_pow",
|
|
"pd_op.rsqrt",
|
|
"pd_op.reshape",
|
|
"pd_op.full_like",
|
|
"pd_op.assign",
|
|
"pd_op.expand",
|
|
"pd_op.scale",
|
|
"pd_op.exp",
|
|
"pd_op.sin",
|
|
"pd_op.cos",
|
|
"pd_op.add_n",
|
|
# "pd_op.any",
|
|
"pd_op.cast",
|
|
"pd_op.concat",
|
|
"pd_op.full_with_tensor",
|
|
"pd_op.gather_nd",
|
|
"pd_op.logical_and",
|
|
"pd_op.logical_not",
|
|
"pd_op.where",
|
|
"pd_op.pow",
|
|
"pd_op.shape",
|
|
"pd_op.shape64",
|
|
"pd_op.slice",
|
|
"pd_op.squeeze",
|
|
"pd_op.unsqueeze",
|
|
"pd_op.transpose",
|
|
# "pd_op.prod",
|
|
"pd_op.log",
|
|
"pd_op.log1p",
|
|
"pd_op.logit",
|
|
# "pd_op.max",
|
|
# "pd_op.min",
|
|
"pd_op.expand_as",
|
|
"pd_op.split",
|
|
"pd_op.arange",
|
|
"pd_op.put_along_axis",
|
|
"pd_op.tanh",
|
|
"pd_op.atan",
|
|
"pd_op.atanh",
|
|
"pd_op.sinh",
|
|
"pd_op.asin",
|
|
"pd_op.asinh",
|
|
"pd_op.cosh",
|
|
"pd_op.acos",
|
|
"pd_op.acosh",
|
|
"pd_op.abs",
|
|
"pd_op.sign",
|
|
"pd_op.expm1",
|
|
"pd_op.erf",
|
|
"pd_op.erfinv",
|
|
"pd_op.ceil",
|
|
"pd_op.floor",
|
|
"pd_op.frac",
|
|
"pd_op.round",
|
|
"pd_op.trunc",
|
|
"pd_op.angle",
|
|
"pd_op.as_complex",
|
|
"pd_op.as_real",
|
|
"pd_op.complex",
|
|
"pd_op.real",
|
|
"pd_op.imag",
|
|
"pd_op.conj",
|
|
"pd_op.greater_equal",
|
|
"pd_op.greater_than",
|
|
"pd_op.not_equal",
|
|
"pd_op.equal",
|
|
"pd_op.less_equal",
|
|
"pd_op.less_than",
|
|
"pd_op.bitwise_and",
|
|
"pd_op.bitwise_or",
|
|
"pd_op.bitwise_xor",
|
|
"pd_op.bitwise_not",
|
|
"pd_op.isinf",
|
|
"pd_op.isnan",
|
|
# "pd_op.gather",
|
|
"pd_op.sigmoid",
|
|
]
|
|
|
|
# define the ops that are tending to recompute.These ops are more likely to save memory and get fused.
|
|
TENDING_TO_RECOMPUTE_OPS: list[str] = [
|
|
"pd_op.full_int_array",
|
|
"pd_op.full",
|
|
]
|
|
|
|
VIEW_OPS: list[str] = []
|
|
|
|
RANDOM_OPS: list[str] = ["pd_op.randint", "pd_op.uniform", "pd_op.dropout"]
|
|
|
|
COMPUTE_INTENSIVE_OPS: list[str] = [
|
|
"pd_op.matmul",
|
|
"pd_op.conv2d",
|
|
"pd_op.layer_norm",
|
|
"pd_op.batchnorm",
|
|
"pd_op.softmax",
|
|
"pd_op.all_reduce_",
|
|
"pd_op.c_broadcast_",
|
|
"pd_op.reduce_",
|
|
]
|
|
|
|
IGNORE_OPS: list[str] = [
|
|
"cf.stack_create",
|
|
]
|
|
|
|
AGGRESSIVE_RECOMPUTATION = False
|
|
# Restricts the amount of computation recompute can do.
|
|
MAX_DIST_FROM_BW = 3
|
|
|
|
MINIMUM_WEIGHT = 0.1
|
|
|
|
|
|
def DebugPrint(*args):
|
|
flag = os.getenv("FLAGS_print_auto_recompute_debug")
|
|
if flag and str(flag).lower() in ("1", "true"):
|
|
print(*args, flush=True)
|
|
|
|
|
|
class JudgeFusionLoop:
|
|
def __init__(self, program, unrecomputable_ops):
|
|
self.ops = program.global_block().ops
|
|
self.unrecomputable_ops = unrecomputable_ops
|
|
self.downstream_unrecomputable_ops_map = {op: set() for op in self.ops}
|
|
self.upstream_unrecomputable_ops_map = {op: set() for op in self.ops}
|
|
self._set_has_unfusible_on_path_map()
|
|
|
|
def _set_has_unfusible_on_path_map(self):
|
|
def _get_used_external_value(op):
|
|
defined_values = set()
|
|
used_values = []
|
|
_get_used_external_value_impl(defined_values, used_values, op)
|
|
return used_values
|
|
|
|
def _get_used_external_value_impl(defined_values, used_values, op):
|
|
for operand in op.operands_source():
|
|
if operand not in defined_values:
|
|
used_values.append(operand)
|
|
defined_values.add(operand)
|
|
for block in op.blocks():
|
|
for value in block.args():
|
|
defined_values.add(value)
|
|
for _, value in block.kwargs():
|
|
defined_values.add(value)
|
|
for block in op.blocks():
|
|
for inner_op in block.ops:
|
|
_get_used_external_value_impl(
|
|
defined_values, used_values, inner_op
|
|
)
|
|
for result_value in op.results():
|
|
defined_values.add(result_value)
|
|
|
|
def _get_producer_ops(op):
|
|
producers = set()
|
|
for operand in _get_used_external_value(op):
|
|
if operand.get_defining_op() is None:
|
|
continue
|
|
source_op = operand.get_defining_op()
|
|
if source_op.get_parent_block() == op.get_parent_block():
|
|
producers.add(source_op)
|
|
return producers
|
|
|
|
def _get_consumer_ops(op):
|
|
consumers = set()
|
|
for result in op.results():
|
|
for parent_op in result.all_used_ops_in_same_block():
|
|
if parent_op is not None:
|
|
consumers.add(parent_op)
|
|
return consumers
|
|
|
|
def _get_upstream_ops_recursively(cur):
|
|
upstream_unrecomputable_ops = set()
|
|
for new_op in _get_producer_ops(cur):
|
|
upstream_unrecomputable_ops |= (
|
|
self.upstream_unrecomputable_ops_map[new_op]
|
|
)
|
|
if cur.name() in self.unrecomputable_ops:
|
|
upstream_unrecomputable_ops.add(cur)
|
|
return upstream_unrecomputable_ops
|
|
|
|
def _get_downstream_ops_recursively(cur):
|
|
downstream_unrecomputable_ops = set()
|
|
for new_op in _get_consumer_ops(cur):
|
|
downstream_unrecomputable_ops |= (
|
|
self.downstream_unrecomputable_ops_map[new_op]
|
|
)
|
|
if cur.name() in self.unrecomputable_ops:
|
|
downstream_unrecomputable_ops.add(cur)
|
|
return downstream_unrecomputable_ops
|
|
|
|
for op in self.ops:
|
|
self.upstream_unrecomputable_ops_map[op] |= (
|
|
_get_upstream_ops_recursively(op)
|
|
)
|
|
for op in reversed(self.ops):
|
|
self.downstream_unrecomputable_ops_map[op] |= (
|
|
_get_downstream_ops_recursively(op)
|
|
)
|
|
|
|
def _has_unfusible_op_on_any_path(self, op1, op2):
|
|
no_unfusible_op_on_path = (
|
|
len(
|
|
self.downstream_unrecomputable_ops_map[op1]
|
|
& self.upstream_unrecomputable_ops_map[op2]
|
|
)
|
|
== 0
|
|
and len(
|
|
self.downstream_unrecomputable_ops_map[op2]
|
|
& self.upstream_unrecomputable_ops_map[op1]
|
|
)
|
|
== 0
|
|
)
|
|
return (
|
|
not no_unfusible_op_on_path
|
|
if op1 is not None and op2 is not None
|
|
else False
|
|
)
|
|
|
|
|
|
class Op2IdxMap:
|
|
def __init__(self, program):
|
|
self.op_to_idx_map = {}
|
|
for idx, op_iter in enumerate(program.global_block().ops):
|
|
self.op_to_idx_map[op_iter] = idx
|
|
|
|
def get_idx(self, op):
|
|
if self.op_to_idx_map.get(op, None):
|
|
return self.op_to_idx_map[op]
|
|
raise RuntimeError("op not found in program")
|
|
|
|
|
|
def auto_recompute(
|
|
program: paddle.static.Program,
|
|
inputs: Sequence[pir.Value],
|
|
outputs: Sequence[pir.Value],
|
|
grad_outputs: Sequence[pir.Value],
|
|
fwd_op_end_idx: int,
|
|
backward_op_start_idx: int,
|
|
recomputable_ops: Sequence[str] | None = None,
|
|
) -> tuple[paddle.static.Program, int]:
|
|
'''
|
|
Considering the compiler fuse strategy, we model the pir graph.
|
|
Convert the pir calculation graph into a networkx calculation
|
|
graph. Find the cut point through the min-cut algorithm,
|
|
which is the value to be saved in pir forward calculation graph.
|
|
|
|
Recompute the forward computation graph to replace intermediate
|
|
variables in the forward graph held by the backward graph.
|
|
|
|
.. warning::
|
|
This API is experimental and likely to change.
|
|
|
|
Args:
|
|
program (Program): The program to be recomputed.
|
|
inputs:(list[Value]|tuple(Value)): The input Values
|
|
of the forward graph.
|
|
outputs:(list[Value]|tuple(Value)): The out Values
|
|
of the forward graph.
|
|
grad_outputs:(list[Value]|tuple(Value)): initial gradient values
|
|
of `outputs` .
|
|
forward_op_end_idx(int): The index of the last forward op.
|
|
backward_op_start_idx(int): The index of the start backward op.
|
|
recomputable_ops(list[str]|tuple(str)|None): The op names that can
|
|
be recomputed. If 'recompute_ops' is None, we will use the
|
|
default recomputable_ops. Default None.
|
|
Returns:
|
|
recomputed_program(Program): The recomputed program.
|
|
fwd_op_end_idx(int): The index of the last forward op in recomputed program.
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> import numpy as np
|
|
>>> import paddle
|
|
>>> from paddle.autograd.ir_backward import grad as ir_grad
|
|
>>> from paddle.base import core
|
|
>>> from paddle.decomposition import decompose
|
|
>>> def forward(x):
|
|
... y = paddle.sin(x)
|
|
... z = paddle.cos(y)
|
|
... return z
|
|
|
|
>>> np_x = np.random.random(size=[4096, 4096]).astype("float32")
|
|
>>> paddle.enable_static()
|
|
>>> core._set_prim_all_enabled(True)
|
|
>>> main_program = paddle.static.Program()
|
|
>>> with paddle.static.program_guard(main_program):
|
|
>>> x = paddle.static.data(
|
|
>>> name="x", shape=[4096, 4096], dtype="float32"
|
|
>>> )
|
|
>>> x.stop_gradient = False
|
|
>>> out = forward(x)
|
|
>>> out_grad = paddle.full(
|
|
>>> shape=out.shape, fill_value=3, dtype="float32"
|
|
>>> )
|
|
>>> [out] = decompose(main_program, [out])
|
|
>>> [dx] = ir_grad(out, [x], out_grad)
|
|
>>> main_program, _ = paddle.decomposition.auto_recompute(
|
|
>>> main_program,
|
|
>>> [x],
|
|
>>> [out],
|
|
>>> grad_outputs=[out_grad],
|
|
>>> fwd_op_end_idx=2,
|
|
>>> backward_op_start_idx=4
|
|
>>> )
|
|
>>> exe = paddle.static.Executor(paddle.CUDAPlace(0))
|
|
>>> res = exe.run(
|
|
>>> feed={'x': np_x},
|
|
>>> fetch_list=[dx],
|
|
>>> )
|
|
>>> print(main_program)
|
|
{
|
|
(%0) = "pd_op.data" () {dtype:(pd_op.DataType)float32,name:"x",place:(pd_op.Place)Place(undefined:0),shape:(pd_op.IntArray)[4096,4096],stop_gradient:[false]} : () -> pd_op.tensor<4096x4096xf32>
|
|
(%1) = "pd_op.sin" (%0) {stop_gradient:[false]} : (pd_op.tensor<4096x4096xf32>) -> pd_op.tensor<4096x4096xf32>
|
|
(%2) = "pd_op.cos" (%1) {stop_gradient:[false]} : (pd_op.tensor<4096x4096xf32>) -> pd_op.tensor<4096x4096xf32>
|
|
(%3) = "pd_op.full" () {dtype:(pd_op.DataType)float32,place:(pd_op.Place)Place(undefined:0),shape:(pd_op.IntArray)[4096,4096],stop_gradient:[true],value:(Float)3} : () -> pd_op.tensor<4096x4096xf32>
|
|
(%4) = "pd_op.sin" (%0) {stop_gradient:[false]} : (pd_op.tensor<4096x4096xf32>) -> pd_op.tensor<4096x4096xf32>
|
|
(%5) = "pd_op.sin" (%4) {stop_gradient:[false]} : (pd_op.tensor<4096x4096xf32>) -> pd_op.tensor<4096x4096xf32>
|
|
(%6) = "pd_op.full" () {dtype:(pd_op.DataType)float32,place:(pd_op.Place)Place(cpu),shape:(pd_op.IntArray)[1],stop_gradient:[true],value:(Float)-1} : () -> pd_op.tensor<1xf32>
|
|
(%7) = "pd_op.scale" (%5, %6) {bias:(Float)0,bias_after_scale:true,stop_gradient:[false]} : (pd_op.tensor<4096x4096xf32>, pd_op.tensor<1xf32>) -> pd_op.tensor<4096x4096xf32>
|
|
(%8) = "pd_op.multiply" (%7, %3) {stop_gradient:[false]} : (pd_op.tensor<4096x4096xf32>, pd_op.tensor<4096x4096xf32>) -> pd_op.tensor<4096x4096xf32>
|
|
(%9) = "pd_op.cos" (%0) {stop_gradient:[false]} : (pd_op.tensor<4096x4096xf32>) -> pd_op.tensor<4096x4096xf32>
|
|
(%10) = "pd_op.multiply" (%9, %8) {stop_gradient:[false]} : (pd_op.tensor<4096x4096xf32>, pd_op.tensor<4096x4096xf32>) -> pd_op.tensor<4096x4096xf32>
|
|
(%11) = "pd_op.fetch" (%10) {col:(Int32)0,is_persistable:[true],name:"fetch0",stop_gradient:[false]} : (pd_op.tensor<4096x4096xf32>) -> pd_op.tensor<4096x4096xf32>
|
|
}
|
|
'''
|
|
DebugPrint("program before recompute:", program)
|
|
# 1. find smart recompute needed saved values by min-cut algorithm
|
|
# 1.1 classify value nodes
|
|
import networkx as nx
|
|
|
|
start_time = time.time()
|
|
|
|
# model value as graph's node, op as graph's edge
|
|
(
|
|
required_fw_value_nodes,
|
|
required_bw_value_nodes,
|
|
unclaimed_value_nodes,
|
|
) = classify_value_node(program, grad_outputs, fwd_op_end_idx)
|
|
|
|
if len(required_bw_value_nodes) == 0 or backward_op_start_idx >= len(
|
|
program.global_block().ops
|
|
):
|
|
return program, fwd_op_end_idx
|
|
|
|
all_ops = program.global_block().ops
|
|
# 1.2 cal value nodes dist to backward
|
|
dist_from_bw = cal_value_nodes_dist_to_backward(
|
|
all_ops, required_fw_value_nodes
|
|
)
|
|
|
|
# 1.3 classify ops
|
|
default_recomputable_ops = DEFAULT_RECOMPUTABLE_OPS
|
|
view_ops = VIEW_OPS
|
|
|
|
default_recomputable_ops += view_ops
|
|
|
|
recomputable_ops = (
|
|
set(recomputable_ops)
|
|
if recomputable_ops is not None
|
|
else set(default_recomputable_ops)
|
|
)
|
|
|
|
random_ops = RANDOM_OPS
|
|
compute_intensive_ops = COMPUTE_INTENSIVE_OPS
|
|
tending_to_recompute_ops = TENDING_TO_RECOMPUTE_OPS
|
|
|
|
unrecomputable_ops = random_ops + compute_intensive_ops
|
|
|
|
fusible_ops = recomputable_ops | set(random_ops)
|
|
|
|
# 1.4 Model pir graph. Convert the pir calculation graph into a networkx calculation graph.
|
|
outputs = backward_utils.ValueSet(outputs)
|
|
inputs = backward_utils.ValueSet(inputs)
|
|
placeholder_value_nodes = inputs | outputs
|
|
|
|
value_id_dict = {}
|
|
nx_graph = nx.DiGraph()
|
|
|
|
judge_fusion_loop = JudgeFusionLoop(program, unrecomputable_ops)
|
|
forward_ops = set(program.global_block().ops[: fwd_op_end_idx + 1])
|
|
|
|
def _get_bw_no_need_buffer_values(program, backward_op_start_idx):
|
|
need_buffer_values = backward_utils.ValueSet()
|
|
all_values = backward_utils.ValueSet()
|
|
for op in program.global_block().ops[backward_op_start_idx:]:
|
|
for op_operand_source in op.operands_source():
|
|
all_values.add(op_operand_source)
|
|
if op.is_no_need_buffer(op_operand_source):
|
|
continue
|
|
need_buffer_values.add(op_operand_source)
|
|
bw_no_need_buffer_values = all_values - need_buffer_values
|
|
return bw_no_need_buffer_values
|
|
|
|
bw_no_need_buffer_values = _get_bw_no_need_buffer_values(
|
|
program, backward_op_start_idx
|
|
)
|
|
|
|
def _is_fusible(value_node1, value_node2):
|
|
return (
|
|
value_node1.get_defining_op().name() in fusible_ops
|
|
and value_node2.get_defining_op().name() in fusible_ops
|
|
)
|
|
|
|
def _is_materialized_backwards(value_node):
|
|
cur_value_nodes = backward_utils.ValueSet()
|
|
cur_value_nodes.add(value_node)
|
|
while len(cur_value_nodes) > 0:
|
|
cur_value_node = cur_value_nodes.pop()
|
|
users = find_value_node_users(
|
|
cur_value_node, bw_no_need_buffer_values, True, forward_ops
|
|
)
|
|
for user in users:
|
|
if user not in required_fw_value_nodes and not _is_fusible(
|
|
cur_value_node, user
|
|
):
|
|
return True
|
|
if (
|
|
user not in required_fw_value_nodes
|
|
and get_real_define_op_name(user) in view_ops
|
|
):
|
|
cur_value_nodes.add(user)
|
|
return False
|
|
|
|
def _is_materialized(value_node, placeholder_value_nodes):
|
|
if value_node in placeholder_value_nodes:
|
|
return True
|
|
users = find_value_node_users(
|
|
value_node, bw_no_need_buffer_values, True, forward_ops
|
|
)
|
|
return not all(_is_fusible(value_node, user) for user in users)
|
|
|
|
def _get_node_weight(value_node, placeholder_value_nodes):
|
|
mem_sz = cal_value_node_size(value_node)
|
|
|
|
if (
|
|
value_node.get_defining_op().name() in tending_to_recompute_ops
|
|
and mem_sz == 0
|
|
):
|
|
return MINIMUM_WEIGHT
|
|
|
|
# Heuristic to bias towards nodes closer to the backwards pass
|
|
mem_sz = int(
|
|
mem_sz * (1.1 ** max(min(dist_from_bw[value_node], 100), 1))
|
|
)
|
|
|
|
if _is_materialized(value_node, placeholder_value_nodes):
|
|
return mem_sz
|
|
else:
|
|
return mem_sz * 2
|
|
|
|
def _ban_recomputation(value_node):
|
|
if AGGRESSIVE_RECOMPUTATION:
|
|
return value_node.get_defining_op().name() in unrecomputable_ops
|
|
else:
|
|
if value_node.get_defining_op().name() in tending_to_recompute_ops:
|
|
return False
|
|
|
|
if value_node.get_defining_op().name() not in recomputable_ops:
|
|
return True
|
|
|
|
# If a node *must* be materialized in the backwards pass, then we
|
|
# should never recompute it. This is a pretty subtle point. In
|
|
# general, the assumption we make is that recomputing a node in the
|
|
# backwards pass is "free". However, if a node must be materialized
|
|
# in the backwards pass, then recomputing it is never free.
|
|
if _is_materialized_backwards(value_node):
|
|
return True
|
|
|
|
if dist_from_bw[value_node] > MAX_DIST_FROM_BW:
|
|
return True
|
|
# If the output of an op is 4x smaller (arbitrary choice),
|
|
# then we don't allow recomputation.
|
|
output_size = cal_value_node_size(value_node)
|
|
inputs = get_real_input_nodes(value_node)
|
|
inputs_size = sum(cal_value_node_size(i) for i in inputs)
|
|
return output_size * 4 < inputs_size
|
|
|
|
for value_node in (
|
|
required_fw_value_nodes
|
|
| required_bw_value_nodes
|
|
| unclaimed_value_nodes
|
|
):
|
|
if not value_node.initialized():
|
|
continue
|
|
|
|
if value_node.get_defining_op().name() == "builtin.combine":
|
|
continue
|
|
|
|
if value_node.get_defining_op().name() in IGNORE_OPS:
|
|
continue
|
|
|
|
if len(
|
|
value_node.all_used_ops_in_same_block()
|
|
) == 1 and value_node.all_used_ops_in_same_block()[0].name() in [
|
|
"builtin.split",
|
|
"builtin.slice",
|
|
]:
|
|
continue
|
|
|
|
if value_node in required_bw_value_nodes:
|
|
DebugPrint(
|
|
"add edge link from: ", value_node.id, " -> ", "sink", " (inf) "
|
|
)
|
|
nx_graph.add_edge(value_node.id + "_in", "sink", capacity=math.inf)
|
|
value_id_dict[value_node.id] = value_node
|
|
continue
|
|
|
|
if value_node in inputs:
|
|
DebugPrint(
|
|
"add edge link from: ",
|
|
" source ",
|
|
" -> ",
|
|
value_node.id,
|
|
" (inf)",
|
|
)
|
|
nx_graph.add_edge(
|
|
"source", value_node.id + "_in", capacity=math.inf
|
|
)
|
|
value_id_dict[value_node.id] = value_node
|
|
|
|
# If a node can't be recomputed (too expensive or involves randomness),
|
|
# we prevent it from being recomputed by adding an inf edge to the source
|
|
# We only need to ban nodes in the fw pass, as those are the only ones that would be recomputed.
|
|
if (
|
|
_ban_recomputation(value_node)
|
|
and value_node in required_fw_value_nodes
|
|
):
|
|
DebugPrint(
|
|
"add edge link from: ",
|
|
" source ",
|
|
" -> ",
|
|
value_node.id,
|
|
"(inf)",
|
|
)
|
|
nx_graph.add_edge(
|
|
"source", value_node.id + "_in", capacity=math.inf
|
|
)
|
|
value_id_dict[value_node.id] = value_node
|
|
|
|
weight = _get_node_weight(
|
|
value_node,
|
|
placeholder_value_nodes,
|
|
)
|
|
|
|
# Creates the weights on the "node" edge
|
|
nx_graph.add_edge(
|
|
value_node.id + "_in", value_node.id + "_out", capacity=weight
|
|
)
|
|
value_id_dict[value_node.id] = value_node
|
|
|
|
users = find_value_node_users(
|
|
value_node, bw_no_need_buffer_values, True, forward_ops
|
|
)
|
|
for user in users:
|
|
DebugPrint(
|
|
"add edge link from: ",
|
|
value_node.id,
|
|
" -> ",
|
|
user.id,
|
|
" (inf) ",
|
|
)
|
|
nx_graph.add_edge(
|
|
value_node.id + "_out", user.id + "_in", capacity=math.inf
|
|
)
|
|
for user in value_node.all_used_ops_in_same_block():
|
|
if user in forward_ops:
|
|
if judge_fusion_loop._has_unfusible_op_on_any_path(
|
|
value_node.get_defining_op(), user
|
|
):
|
|
DebugPrint(
|
|
"add edge link from: ",
|
|
" source ",
|
|
" -> ",
|
|
value_node.id,
|
|
"(inf)",
|
|
)
|
|
nx_graph.add_edge(
|
|
"source", value_node.id + "_in", capacity=math.inf
|
|
)
|
|
|
|
DebugPrint(
|
|
"add edge link from: ",
|
|
value_node.id,
|
|
" -> ",
|
|
" sink ",
|
|
"(inf)",
|
|
)
|
|
|
|
nx_graph.add_edge(
|
|
value_node.id + "_out", "sink", capacity=math.inf
|
|
)
|
|
|
|
# 1.5 find saved values by minimum cut.
|
|
cut_value, partition = nx.minimum_cut(nx_graph, "source", "sink")
|
|
DebugPrint("Cut Value:", cut_value)
|
|
reachable, non_reachable = partition
|
|
cutset = set()
|
|
for u, nbrs in ((n, nx_graph[n]) for n in reachable):
|
|
cutset.update((u, v) for v in nbrs if v in non_reachable)
|
|
|
|
cut_value_nodes = backward_utils.ValueSet()
|
|
for value_node_in, value_node_out in cutset:
|
|
assert value_node_in[:-3] == value_node_out[:-4]
|
|
value_node = value_id_dict[value_node_in[:-3]]
|
|
cut_value_nodes.add(value_node)
|
|
|
|
saved_values = cut_value_nodes
|
|
# (TODO: wanghao107): remove it and fix model
|
|
# saved_values = cut_value_nodes | inputs
|
|
saved_values = cut_value_nodes
|
|
# 2.partition the joint graph by saved values.
|
|
(
|
|
program_after_recompute,
|
|
fwd_op_end_idx_after_recompute,
|
|
) = partition_joint_graph(
|
|
program,
|
|
saved_values,
|
|
inputs,
|
|
outputs,
|
|
bw_no_need_buffer_values,
|
|
fwd_op_end_idx,
|
|
backward_op_start_idx,
|
|
)
|
|
DebugPrint("program after recompute:", program_after_recompute)
|
|
end_time = time.time()
|
|
if in_cinn_debug_mode():
|
|
logger = logging.getLogger("auto-recompute")
|
|
logger.setLevel(logging.INFO)
|
|
logger.info(
|
|
f"Time of auto recompute program: ***** [ {end_time - start_time} ] ***** seconds."
|
|
)
|
|
return program_after_recompute, fwd_op_end_idx_after_recompute
|
|
|
|
|
|
def partition_joint_graph(
|
|
program: paddle.static.Program,
|
|
saved_values: list[pir.Value],
|
|
inputs: list[pir.Value],
|
|
outputs: list[pir.Value],
|
|
bw_no_need_buffer_values: list[pir.Value],
|
|
fwd_op_end_idx: int,
|
|
backward_op_start_idx: int,
|
|
) -> tuple[paddle.static.Program, int]:
|
|
"""
|
|
Partition the joint graph, recompute the intermediate values
|
|
by saved values to save memory.
|
|
Args:
|
|
program(Program): The program to be recomputed.
|
|
saved_values(list[valueiable]): The saved values
|
|
of forward graph which used by backward graph.
|
|
inputs:(list[Value]|tuple(Value)): The input Values
|
|
of the forward graph.
|
|
outputs(list[valueiable]): The out values
|
|
of the forward graph.
|
|
forward_op_end_idx(int): The index of the last forward op.
|
|
backward_op_start_idx(int): The index of the start backward op.
|
|
Returns:
|
|
recomputed_program(Program): The recomputed program.
|
|
fwd_op_end_idx(int): The index of the last forward op in
|
|
recomputed program.
|
|
"""
|
|
saved_values = backward_utils.ValueSet(saved_values)
|
|
outputs = backward_utils.ValueSet(outputs)
|
|
|
|
# 1. Analyze the program, get all forward program mid hold values
|
|
mid_hold_values = analyze_mid_hold_values(
|
|
program,
|
|
saved_values,
|
|
inputs,
|
|
outputs,
|
|
bw_no_need_buffer_values,
|
|
fwd_op_end_idx,
|
|
backward_op_start_idx,
|
|
)
|
|
DebugPrint("saved values: ")
|
|
DebugPrint([f"({v}, {v.get_defining_op().id()})" for v in saved_values])
|
|
DebugPrint("mid values: ")
|
|
DebugPrint([f"({v}, {v.get_defining_op().id()})" for v in mid_hold_values])
|
|
|
|
mem = 0
|
|
for mid in mid_hold_values:
|
|
mem += cal_value_node_size(mid)
|
|
DebugPrint("Saved Memory is: ", mem / 1024 / 1024 / 1024, "GB")
|
|
|
|
# 2. Extract the recompute subgraph and replace forward mid hold values with recompute subgraph's outputs
|
|
program, fwd_op_end_idx = replace_mid_values_with_forward_subgraph(
|
|
program,
|
|
saved_values,
|
|
mid_hold_values,
|
|
fwd_op_end_idx,
|
|
backward_op_start_idx,
|
|
)
|
|
|
|
return program, fwd_op_end_idx
|
|
|
|
|
|
def replace_mid_values_with_forward_subgraph(
|
|
program, saved_values, mid_values, fwd_op_end_idx, backward_op_start_idx
|
|
):
|
|
def _extract_forward_recompute_subgraph_for_backward(
|
|
saved_values, mid_values
|
|
):
|
|
def _find_recompute_ops(
|
|
recompute_value,
|
|
saved_values,
|
|
marked_recompute_ops,
|
|
needed_saved_values,
|
|
chain,
|
|
):
|
|
new_chain = list(chain)
|
|
new_chain.append(recompute_value)
|
|
define_op = recompute_value.get_defining_op()
|
|
if define_op in marked_recompute_ops or define_op is None:
|
|
return
|
|
if define_op.name() in [
|
|
"builtin.parameter",
|
|
"pd_op.data",
|
|
]:
|
|
if recompute_value not in needed_saved_values:
|
|
needed_saved_values.add(recompute_value)
|
|
return
|
|
op_inputs = define_op.operands_source()
|
|
if len(op_inputs) == 0 and define_op.name() not in [
|
|
"pd_op.full",
|
|
"pd_op.full_int_array",
|
|
]:
|
|
raise Exception(
|
|
f"Every path to recompute value {recompute_value} must have saved value or starting point of the path is one of op in [pd_op.full, pd_op.full_int_array], but find {define_op.name()} op, op ir is {define_op}"
|
|
)
|
|
for op_input in op_inputs:
|
|
if op_input in saved_values:
|
|
if op_input not in needed_saved_values:
|
|
needed_saved_values.add(op_input)
|
|
continue
|
|
_find_recompute_ops(
|
|
op_input,
|
|
saved_values,
|
|
marked_recompute_ops,
|
|
needed_saved_values,
|
|
new_chain,
|
|
)
|
|
marked_recompute_ops.add(define_op)
|
|
|
|
return
|
|
|
|
recompute_subgraph_ops = set()
|
|
recompute_subgraph_inputs = backward_utils.ValueSet()
|
|
recompute_subgraph_outputs_backward_needed = mid_values
|
|
|
|
for recompute_value in mid_values:
|
|
_find_recompute_ops(
|
|
recompute_value,
|
|
saved_values,
|
|
recompute_subgraph_ops,
|
|
recompute_subgraph_inputs,
|
|
[],
|
|
)
|
|
|
|
DebugPrint("Recompute Ops: ", len(recompute_subgraph_ops))
|
|
DebugPrint("Recompute Ops: ", recompute_subgraph_ops)
|
|
recompute_subgraph = {
|
|
"inputs": recompute_subgraph_inputs,
|
|
"recompute_ops": recompute_subgraph_ops,
|
|
"outputs": recompute_subgraph_outputs_backward_needed,
|
|
}
|
|
return recompute_subgraph
|
|
|
|
op_2_id_map = Op2IdxMap(program)
|
|
|
|
forward_ops = set(program.global_block().ops[: fwd_op_end_idx + 1])
|
|
backward_ops = set(program.global_block().ops[backward_op_start_idx:])
|
|
first_backward_op = program.global_block().ops[backward_op_start_idx]
|
|
|
|
# 1. find forward subgraph to recompute mid values that backward need to hold.
|
|
recompute_forward_subgraph = (
|
|
_extract_forward_recompute_subgraph_for_backward(
|
|
saved_values, mid_values
|
|
)
|
|
)
|
|
|
|
# 2. clone subgraph which need to be recomputed
|
|
origin_ops = recompute_forward_subgraph["recompute_ops"]
|
|
origin_subgraph_inputs = recompute_forward_subgraph["inputs"]
|
|
origin_subgraph_outputs = recompute_forward_subgraph["outputs"]
|
|
cloned_ops, value_map, cloned_op_first_grad_user_map = clone_graph(
|
|
program,
|
|
origin_ops,
|
|
origin_subgraph_inputs,
|
|
first_backward_op,
|
|
backward_ops,
|
|
op_2_id_map,
|
|
)
|
|
|
|
for origin_op in origin_ops:
|
|
origin_op.set_bool_attr("is_recompute_op", True)
|
|
for cloned_op in cloned_ops:
|
|
cloned_op.set_bool_attr("is_recompute_bw_op", True)
|
|
|
|
# 3. replace mid values that backward need to hold with recompute subgraph's outputs
|
|
cloned_subgraph_outputs = backward_utils.ValueSet()
|
|
for origin_value in origin_subgraph_outputs:
|
|
cloned_value = value_map.look_up(origin_value)
|
|
origin_value.replace_grad_users_with(cloned_value, backward_ops)
|
|
cloned_subgraph_outputs.add(cloned_value)
|
|
|
|
# 4. reset recomputed ops location in program
|
|
for op in reversed(cloned_ops):
|
|
first_subgraph_grad_user = cloned_op_first_grad_user_map.get(op, None)
|
|
for op_outputs in op.results():
|
|
for child in op_outputs.all_used_ops_in_same_block():
|
|
if cloned_op_first_grad_user_map.get(child, 0):
|
|
if first_subgraph_grad_user is None or op_2_id_map.get_idx(
|
|
cloned_op_first_grad_user_map[child]
|
|
) < op_2_id_map.get_idx(first_subgraph_grad_user):
|
|
first_subgraph_grad_user = (
|
|
cloned_op_first_grad_user_map[child]
|
|
)
|
|
assert first_subgraph_grad_user is not None
|
|
cloned_op_first_grad_user_map[op] = first_subgraph_grad_user
|
|
|
|
for cloned_op in cloned_ops:
|
|
cloned_op.move_before(cloned_op_first_grad_user_map[cloned_op])
|
|
return program, fwd_op_end_idx
|
|
|
|
|
|
def classify_value_node(program, grad_outputs, fwd_op_end_idx):
|
|
all_ops = program.global_block().ops
|
|
required_fw_ops = set(all_ops[: fwd_op_end_idx + 1])
|
|
|
|
required_fw_op_idxs = list(range(0, fwd_op_end_idx + 1))
|
|
required_fw_value_nodes = backward_utils.ValueSet(
|
|
program.global_block().get_values_by_op_idx(required_fw_op_idxs)
|
|
)
|
|
|
|
required_bw_op_idxs = list(range(fwd_op_end_idx + 1, len(all_ops)))
|
|
required_bw_value_nodes = backward_utils.ValueSet(
|
|
program.global_block().get_values_by_op_idx(required_bw_op_idxs)
|
|
)
|
|
|
|
# TODO(chenxi67) optimize classify algorithm by using unclaimed_ops. Remove them to fasten bw_ops detecting time.
|
|
# unclaimed_ops = {
|
|
# op
|
|
# for op in all_ops
|
|
# if op not in required_fw_ops and op not in required_bw_ops
|
|
# }
|
|
|
|
# unclaimed_op_idxs = []
|
|
# for idx, op in enumerate(all_ops):
|
|
# if op in unclaimed_ops:
|
|
# unclaimed_op_idxs.append(idx)
|
|
# unclaimed_value_nodes = backward_utils.ValueSet(
|
|
# program.global_block().get_values_by_op_idx(unclaimed_op_idxs)
|
|
# )
|
|
|
|
return (
|
|
required_fw_value_nodes,
|
|
required_bw_value_nodes,
|
|
backward_utils.ValueSet(),
|
|
)
|
|
|
|
|
|
# Sometimes we need to discard no_need_buffer values because they‘re not REAL tensor users.
|
|
def find_value_node_users(
|
|
value_node,
|
|
bw_no_need_buffer_values={},
|
|
without_no_need_buffer=False,
|
|
forward_ops={},
|
|
):
|
|
'''
|
|
Find all the value nodes which use the same value node to be computed.
|
|
'''
|
|
users = backward_utils.ValueSet()
|
|
ops = value_node.all_used_ops_in_same_block()
|
|
if without_no_need_buffer:
|
|
if value_node in bw_no_need_buffer_values:
|
|
ops = [op for op in ops if op in forward_ops]
|
|
for op in ops:
|
|
if op.name() == "builtin.combine":
|
|
combine_result = op.results()[0]
|
|
for (
|
|
combine_res_used_op
|
|
) in combine_result.all_used_ops_in_same_block():
|
|
results = combine_res_used_op.results()
|
|
for result in results:
|
|
if len(
|
|
result.all_used_ops_in_same_block()
|
|
) == 1 and result.all_used_ops_in_same_block()[
|
|
0
|
|
].name() in [
|
|
"builtin.split",
|
|
"builtin.slice",
|
|
]:
|
|
split_results = result.all_used_ops_in_same_block()[
|
|
0
|
|
].results()
|
|
users |= backward_utils.ValueSet(split_results)
|
|
else:
|
|
users.add(result)
|
|
else:
|
|
results = op.results()
|
|
for result in results:
|
|
if len(
|
|
result.all_used_ops_in_same_block()
|
|
) == 1 and result.all_used_ops_in_same_block()[0].name() in [
|
|
"builtin.split",
|
|
"builtin.slice",
|
|
]:
|
|
split_results = result.all_used_ops_in_same_block()[
|
|
0
|
|
].results()
|
|
users |= backward_utils.ValueSet(split_results)
|
|
else:
|
|
users.add(result)
|
|
return users
|
|
|
|
|
|
def get_real_input_nodes(output_value_node):
|
|
real_input_nodes = backward_utils.ValueSet()
|
|
define_op = output_value_node.get_defining_op()
|
|
if define_op.name() in ["builtin.split", "builtin.slice"]:
|
|
op_input = define_op.operands_source()[0]
|
|
real_define_op = op_input.get_defining_op()
|
|
input_value_nodes = real_define_op.operands_source()
|
|
else:
|
|
input_value_nodes = define_op.operands_source()
|
|
for input_value_node in input_value_nodes:
|
|
if (
|
|
input_value_node.get_defining_op()
|
|
and input_value_node.get_defining_op().name() == "builtin.combine"
|
|
):
|
|
real_input_nodes |= backward_utils.ValueSet(
|
|
input_value_node.get_defining_op().operands_source()
|
|
)
|
|
else:
|
|
real_input_nodes.add(input_value_node)
|
|
return real_input_nodes
|
|
|
|
|
|
def get_real_define_op_name(value_node):
|
|
define_op = value_node.get_defining_op()
|
|
if define_op.name() in ["builtin.split", "builtin.slice"]:
|
|
op_input = define_op.operands_source()[0]
|
|
return op_input.get_defining_op().name()
|
|
else:
|
|
return define_op.name()
|
|
|
|
|
|
def is_dynamic_value_node(value_node):
|
|
try:
|
|
return -1 in value_node.shape
|
|
except:
|
|
raise ValueError(f"value node not found in program: {value_node} ")
|
|
|
|
|
|
def is_vector_value_node(value_node):
|
|
try:
|
|
return value_node.type().as_vec_type() is not None
|
|
except:
|
|
raise ValueError(f"value node illegal: {value_node} ")
|
|
|
|
|
|
def cal_value_node_size_impl(value_node):
|
|
if is_dynamic_value_node(value_node):
|
|
value_node_shape = [i for i in value_node.shape if i != -1]
|
|
else:
|
|
value_node_shape = value_node.shape
|
|
return (
|
|
functools.reduce(lambda x, y: x * y, value_node_shape, 1)
|
|
* _PADDLE_DTYPE_2_NBYTES[value_node.dtype]
|
|
)
|
|
|
|
|
|
def cal_value_node_size(value_node):
|
|
if is_vector_value_node(value_node):
|
|
value_vec = value_node.type().as_vec_type().as_list()
|
|
sum_res = 0
|
|
for child_node in value_vec:
|
|
sum_res += cal_value_node_size_impl(child_node)
|
|
return sum_res
|
|
return cal_value_node_size_impl(value_node)
|
|
|
|
|
|
def cal_value_nodes_dist_to_backward(all_ops, required_fw_value_nodes):
|
|
dist_from_bw = backward_utils.ValueDict()
|
|
# calculate value node the shortest dist to backward graph
|
|
for op in reversed(all_ops):
|
|
if op.name() == "builtin.combine":
|
|
continue
|
|
op_results = op.results()
|
|
for op_result in op_results:
|
|
used_ops = op_result.all_used_ops_in_same_block()
|
|
if len(used_ops) == 1 and used_ops[0].name() in [
|
|
"builtin.split",
|
|
"builtin.slice",
|
|
]:
|
|
continue
|
|
real_users = find_value_node_users(op_result)
|
|
if op_result not in required_fw_value_nodes:
|
|
dist_from_bw[op_result] = 0
|
|
else:
|
|
dist_from_bw[op_result] = int(1e9)
|
|
for user in real_users:
|
|
dist_from_bw[op_result] = min(
|
|
dist_from_bw[op_result], dist_from_bw[user] + 1
|
|
)
|
|
return dist_from_bw
|
|
|
|
|
|
def all_used_op_consider_combine(program, value):
|
|
def filter_unused_combine(op):
|
|
if (
|
|
op.name() == "builtin.combine"
|
|
and len(op.result(0).all_used_ops_in_same_block()) == 0
|
|
):
|
|
return False
|
|
return True
|
|
|
|
return list(
|
|
filter(filter_unused_combine, value.all_used_ops_in_same_block())
|
|
)
|
|
|
|
|
|
def analyze_mid_hold_values(
|
|
program,
|
|
saved_values,
|
|
inputs,
|
|
outputs,
|
|
no_need_buffer_values,
|
|
fwd_op_end_idx,
|
|
backward_op_start_idx,
|
|
):
|
|
forward_ops = set(program.global_block().ops[: fwd_op_end_idx + 1])
|
|
backward_ops = set(program.global_block().ops[backward_op_start_idx:])
|
|
mid_hold_values = backward_utils.ValueSet()
|
|
for op in forward_ops:
|
|
for result in op.results():
|
|
all_used_ops = all_used_op_consider_combine(program, result)
|
|
if (
|
|
any(used_op in backward_ops for used_op in all_used_ops)
|
|
and result not in saved_values
|
|
and result not in outputs
|
|
and result not in inputs
|
|
and result not in no_need_buffer_values
|
|
and op.name() not in IGNORE_OPS
|
|
):
|
|
mid_hold_values.add(result)
|
|
return mid_hold_values
|
|
|
|
|
|
def get_first_backward_use_op(fwd_op, backward_ops, op_2_id_map):
|
|
first_backward_use_op = None
|
|
for user_op in fwd_op.results()[0].all_used_ops_in_same_block():
|
|
if user_op in backward_ops and (
|
|
first_backward_use_op is None
|
|
or op_2_id_map.get_idx(user_op)
|
|
< op_2_id_map.get_idx(first_backward_use_op)
|
|
):
|
|
first_backward_use_op = user_op
|
|
return first_backward_use_op
|
|
|
|
|
|
def clone_graph(
|
|
program,
|
|
origin_ops,
|
|
graph_inputs,
|
|
clone_insertion_op,
|
|
backward_ops,
|
|
op_2_id_map,
|
|
):
|
|
pir.set_insertion_point(clone_insertion_op)
|
|
all_ops = program.global_block().ops
|
|
value_map = paddle.pir.IrMapping()
|
|
origin_ops = set(origin_ops)
|
|
cloned_ops = []
|
|
cloned_op_first_grad_user_map = {}
|
|
for input_value in graph_inputs:
|
|
value_map.add(input_value, input_value)
|
|
for op in all_ops:
|
|
if op in origin_ops:
|
|
new_op = op.clone(
|
|
value_map, paddle.pir.CloneOptions(False, True, True)
|
|
)
|
|
first_backward_use_op = get_first_backward_use_op(
|
|
op, backward_ops, op_2_id_map
|
|
)
|
|
if (
|
|
first_backward_use_op is not None
|
|
and first_backward_use_op.has_attr('op_role')
|
|
and first_backward_use_op.has_attr('chunk_id')
|
|
):
|
|
new_op.set_int_attr("op_role", first_backward_use_op.op_role)
|
|
new_op.set_int_attr("chunk_id", first_backward_use_op.chunk_id)
|
|
cloned_ops.append(new_op)
|
|
if first_backward_use_op is not None:
|
|
cloned_op_first_grad_user_map[new_op] = first_backward_use_op
|
|
pir.set_insertion_point_to_block_end(program.global_block())
|
|
return cloned_ops, value_map, cloned_op_first_grad_user_map
|