1153 lines
41 KiB
Python
1153 lines
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
|