Files
2026-07-13 12:40:42 +08:00

1547 lines
58 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 logging
import warnings
from typing import TYPE_CHECKING
import paddle.pir
from paddle.autograd.backward_utils import (
State,
ValueDict,
ValueSet,
_as_list,
all_input_stop_gradient_true,
all_output_grad_none,
all_stop_gradient_true,
argument_to_value,
check_type,
dynamic_shape_prim_vjp_guard,
get_grad_semantic_info,
get_real_op_inputs,
get_real_op_outputs,
get_split_op,
inverse_sort_op,
is_builtin_op,
is_control_flow,
is_inplace_net,
op_has_vjp,
parent_total_ops,
remove_op,
remove_useless_full_like_ops,
return_map_value,
return_map_value_list,
some_in_set,
update_if_output_stopgradient,
update_no_grad_set_by_stopgradient,
update_tuple_pop_origin_inputs,
update_while_output_stopgradient,
value_in_block,
warning_once,
while_prune_check,
)
from paddle.base.framework import pir_op_name_guard
from paddle.base.libpaddle.pir import (
build_pipe_for_block,
get_used_external_value,
)
if TYPE_CHECKING:
from collections.abc import Sequence
from paddle.pir import Value
"""
grad: for template test, will combine in paddle.grad .
calc_gradient: for internal use, optest, parallel etc .
calc_gradient_helper: for dygraph to static .
"""
__all__ = ['grad', 'calc_gradient', 'calc_gradient_helper']
def append_full_like(float_value, copy_value, value, state, backward_ops):
with paddle.amp.auto_cast(enable=False):
if paddle.pir.is_fake_value(value):
state.value_to_valuegrad[value] = [[paddle.pir.fake_value()]]
return
if copy_value.is_dense_tensor_array_type():
value_grad = paddle._C_ops.create_array_like(
copy_value,
float_value,
)
full_like_op = value_grad.get_defining_op()
backward_ops_ = [full_like_op]
elif copy_value.is_combine():
raise ValueError(
"This kind of scene, where VectorType grad be fulled with zeros should not occur."
)
else:
value_grad = paddle.full_like(
copy_value,
float_value,
dtype=copy_value.dtype,
)
full_like_op = value_grad.get_defining_op()
full_op = full_like_op.operand_source(1).get_defining_op()
backward_ops_ = [full_like_op, full_op]
update_bwdop_structure(
backward_ops,
state.op_to_opgrad[value.get_defining_op()],
backward_ops_,
)
if copy_value.is_combine():
state.value_to_valuegrad[value] = [value_grad]
else:
state.value_to_valuegrad[value] = [[value_grad]]
return value_grad
def append_add_n(
op, value, state, backward_ops, bwd_value_to_block_argument_map
):
_MAX_ADD_NUM_ = paddle.framework._global_flags()[
'FLAGS_max_inplace_grad_add'
]
with paddle.amp.auto_cast(enable=False):
# value is input of more than one fwd_op,
# so more than one bwd_op create input_grad,
# need add sum op to accumulate gradient
add_n_list = []
for item in state.value_to_valuegrad[value]:
if item[0] is not None:
add_n_list.append(
return_map_value(item[0], bwd_value_to_block_argument_map)
)
from paddle.amp.auto_cast import amp_global_state
cast_op = []
if (
amp_global_state().use_master_grad
and amp_global_state().amp_dtype in ['float16', 'bfloat6']
and op.name() == 'builtin.parameter'
and value.dtype in [paddle.float16, paddle.bfloat16]
):
add_n_list = [paddle.cast(v, 'float32') for v in add_n_list]
cast_op = [v.get_defining_op() for v in add_n_list]
if len(add_n_list) == 0:
for tmp in state.value_to_valuegrad[value]:
state.value_to_sumvaluegrad[value].append(tmp)
state.value_to_valuegrad[value] = []
elif len(add_n_list) <= _MAX_ADD_NUM_:
grad_value = add_n_list[0]
index = 1
grad_op_list = []
while index < len(add_n_list):
grad_value = paddle._C_ops.add_(grad_value, add_n_list[index])
grad_op_list.append(grad_value.get_defining_op())
index += 1
update_bwdop_structure(
backward_ops, state.op_to_opgrad[op], cast_op + grad_op_list
)
for tmp in state.value_to_valuegrad[value]:
state.value_to_sumvaluegrad[value].append(tmp)
state.value_to_valuegrad[value] = [[grad_value]]
elif len(add_n_list) == 1:
update_bwdop_structure(
backward_ops,
state.op_to_opgrad[op],
cast_op,
)
for tmp in state.value_to_valuegrad[value]:
state.value_to_sumvaluegrad[value].append(tmp)
state.value_to_valuegrad[value] = [add_n_list]
else:
if value.is_dense_tensor_array_type():
add_n_value = paddle._C_ops.add_n_array(add_n_list)
else:
add_n_value = paddle.add_n(add_n_list)
add_n_op = add_n_value.get_defining_op()
combine_op = add_n_op.operand_source(0).get_defining_op()
update_bwdop_structure(
backward_ops,
state.op_to_opgrad[op],
[*cast_op, combine_op, add_n_op],
)
for tmp in state.value_to_valuegrad[value]:
state.value_to_sumvaluegrad[value].append(tmp)
state.value_to_valuegrad[value] = [[add_n_value]]
def update_bwdop_structure(backward_ops, op_to_opgrad_list, grad_op_list):
for grad_op in grad_op_list:
backward_ops.append(grad_op)
op_to_opgrad_list.append(grad_op)
def update_bwdop_structure_(
backward_ops, op_to_opgrad_list, grad_op_list, st, ed
):
for i in range(st, ed):
backward_ops.append(grad_op_list[i])
op_to_opgrad_list.append(grad_op_list[i])
def prepare_grad_outputs(grad_outputs, outputs, state):
"""
if grad_outputs is none, add fill_1 op to create grad_outputs,
else check whether outputs shape and dtype is same to grad_outputs, otherwise raise error.
if only part of op's outputs in outputs, add fill_0 op to create other grad_outputs.
eg: split.
update value_to_valuegrad and op_to_opgrad.
return complete_outputs, backward_ops.
"""
if not grad_outputs:
grad_outputs = [None] * len(outputs)
if len(grad_outputs) != len(outputs):
raise ValueError(
"grad_outputs should have the same length of as outputs."
)
def _check_shape(output, grad) -> bool:
if len(output.shape) != len(grad.shape):
return False
for o_dim, g_dim in zip(output.shape, grad.shape):
if o_dim == -1 or g_dim == -1:
# Skip comparison if any dimension is -1 (wildcard for dynamic shape)
continue
if o_dim != g_dim:
return False
return True
backward_ops = []
for i, grad in enumerate(grad_outputs):
output = outputs[i]
# fwd : op1 -> op2 -> op3 -> output
# bwd : op1G <- op2G <- op3G <- outputG <- full_likeop/feedop
if grad is None:
grad_value = append_full_like(
1.0, output, output, state, backward_ops
)
grad_outputs[i] = grad_value
else:
if not _check_shape(output, grad):
raise ValueError(
f"The shape of grad_output[{i}] {grad.shape} should be the same as the shape of output[{i}] {output.shape}"
)
if output.dtype != grad.dtype:
warnings.warn(
f"The dtype of grad_output[{i}] {grad.dtype} is not same as the dtype of output[{i}] {output.dtype}"
)
feedop = grad.get_defining_op()
update_bwdop_structure(
backward_ops,
state.op_to_opgrad[output.get_defining_op()],
[feedop],
)
state.value_to_valuegrad[output] = [[grad]]
# add input for bwd first op
complete_outputs = outputs
visited_output = ValueSet()
for output in outputs:
if output in visited_output:
continue
for opresult in output.get_defining_op().results():
if opresult.is_combine():
continue
if opresult in state.value_to_valuegrad:
visited_output.add(opresult)
continue
else:
if paddle.pir.is_fake_value(opresult):
state.value_to_valuegrad[opresult] = [
[paddle.pir.fake_value()]
]
else:
grad_value = append_full_like(
0.0, opresult, opresult, state, backward_ops
)
visited_output.add(opresult)
complete_outputs.append(opresult)
if opresult not in state.value_to_valuegrad:
state.value_to_valuegrad[opresult] = [[grad_value]]
return grad_outputs, complete_outputs, backward_ops
def prune_ops(total_ops, inputs_set, outputs_set, no_grad_set):
'''
prune ops which do not in the path from inputs_set to outputs_set,
prune ops which do not in the path from outputs_set to inputs_set,
pruned op in total_ops is uneffective_ops, else is effective_ops
'''
intersection_op_flags = [True] * len(total_ops)
union_op_flags = [False] * len(total_ops)
# from input to output
if inputs_set:
for i, op in enumerate(total_ops):
if some_in_set(get_real_op_outputs(op), inputs_set):
union_op_flags[i] = True
continue
if some_in_set(get_real_op_inputs(op), inputs_set):
union_op_flags[i] = True
for value in get_real_op_outputs(op):
if value not in no_grad_set:
inputs_set.add(value)
else:
intersection_op_flags[i] = False
# from output to input
for i, op in reversed(list(enumerate(total_ops))):
if some_in_set(get_real_op_outputs(op), outputs_set):
union_op_flags[i] = True
for operand in get_real_op_inputs(op):
if operand not in no_grad_set:
outputs_set.add(operand)
else:
union_op_flags[i] = False
intersection_op_flags[i] = False
# some inputs in no_grad_set but its next op is effective,
# add their defining op here.
total_ops_list = list(total_ops)
for i, op in enumerate(total_ops_list):
if union_op_flags[i] is False:
for result in op.results():
if result.has_one_use():
next_op = result.first_use().owner()
if (
next_op in total_ops
and union_op_flags[total_ops_list.index(next_op)]
is True
):
union_op_flags[i] = True
else:
continue
effective_ops = [
total_ops[i] for i in range(len(total_ops)) if intersection_op_flags[i]
]
uneffective_ops = [
total_ops[i] for i in range(len(total_ops)) if not union_op_flags[i]
]
return effective_ops, uneffective_ops
def append_backward_ops(
base_op,
base_inputs,
base_input_grads,
fwd_block,
bwd_block,
effective_forward_ops,
no_grad_set,
backward_ops,
state,
bwd_value_to_block_argument_map=ValueDict(),
control_flow_value_to_copyvalue_map=ValueDict(),
):
'''
add grad_op in order of topological inverse sort
eg:
from :op1 -> v1 -> op2 -> v2 -> op3 -> v3
to: og1_g <- v1_g <- op2_g <- v2_g <- op3_g <- v3_g
if op has grad_op, prepare its grad_op's inputs by value_to_valuegrad,
eg:
value_to_valuegrad[v3] = [[v3_g]];
v2_g = call_vjp(op3, [[v2]], [[v3]],[[v3_g]], [[v2_stopgradient]])
special pattern:
v11 -> combine_op -> v1 -> op -> v3
v12 ->
v2 ->
value_to_valuegrad[v3] = [[v3_g]]
v1 is inside python api, we don't describe it in backward process(state)
so v1_grad is inside vjp, we don't describe it in backward process(state)
[[v11_g, v12_g], v2_g] = call_vjp(op, [[v11, v12]], [[v3]],[[v3_g]], [[v11_stopgradient, v12_stopgradient], v2_stop_gradient])
op_vjp is:
v11_g <- split_op <- v1_g <- op_g <- v3_g
v12_g <-
v2_g <-
value_to_valuegrad[v11] = [[v11_g]]
value_to_valuegrad[v12] = [[v12_g]]
value_to_valuegrad[v2] = [[v2_g]]
if op don't has grad_op:
if it don't has input and it's output has more than
one output_grad, add sumop for grad aggregation.
(eg: full op and parameter op etc.)
else continue to next op.
'''
def make_output_with_output_grad(op):
zero_flag = [False] * op.num_results()
outputs = []
output_grads = []
if op.name() == "pd_op.array_write_":
output_list = [op.operand_source(0)]
elif op.name() == "pd_op.assign_out_":
output_list = [op.operand_source(1)]
else:
output_list = op.results()
for i, value in enumerate(output_list):
new_value = [
return_map_value(value, control_flow_value_to_copyvalue_map)
]
value = return_map_value(
value, state.inside_value_to_outside_value_map
)
if (
value in state.value_to_valuegrad
and len(state.value_to_valuegrad[value]) > 1
):
append_add_n(
op,
value,
state,
backward_ops,
bwd_value_to_block_argument_map,
)
if (
value not in state.value_to_valuegrad
or state.value_to_valuegrad[value] == []
or state.value_to_valuegrad[value][0][0] is None
):
if not value.use_empty() and get_split_op(value) is not None:
# pattern case:
# this fwd_op's output is vectorType, it will split to
# Type by builtin_split op, so need get from split op's outputs.
(
split_zero_flag,
split_outputs,
split_output_grad,
) = make_output_with_output_grad(get_split_op(value))
zero_flag[i] = all(split_zero_flag)
grad_values = [value[0] for value in split_output_grad]
state.value_to_valuegrad[value] = [grad_values]
new_value = [info[0] for info in split_outputs]
else:
# first case:
# this fwd_op's output didn't used by other fwd_op,
# so no output_grad created.
# second case:
# last bwd_op return None because input in no_grad_set,
# but this bwd_op need a input.
append_full_like(
0.0, new_value[0], value, state, backward_ops
)
zero_flag[i] = True
outputs.append(new_value)
grad_value = state.value_to_valuegrad[value][0]
if grad_value[0] is None:
zero_flag[i] = True
output_grads.append(
return_map_value_list(
grad_value, bwd_value_to_block_argument_map
)
)
if op.name() == "pd_op.array_read":
value = op.operand_source(0)
value = return_map_value(
value, state.inside_value_to_outside_value_map
)
if value in state.value_to_valuegrad:
if len(state.value_to_valuegrad[value]) > 1:
append_add_n(
op,
value,
state,
backward_ops,
bwd_value_to_block_argument_map,
)
if (
value not in state.value_to_valuegrad
or state.value_to_valuegrad[value] == []
):
append_full_like(
0.0,
return_map_value(
value, control_flow_value_to_copyvalue_map
),
value,
state,
backward_ops,
)
grad_value = state.value_to_valuegrad[value][0]
output_grads.append(
return_map_value_list(
grad_value, bwd_value_to_block_argument_map
)
)
return zero_flag, outputs, output_grads
def make_input_with_input_stopgradient(op):
inputs = []
input_grad_stopgradients = []
for input, grad_semantic in zip(
get_real_op_inputs(op), get_grad_semantic_info(op)
):
if not grad_semantic:
if (
op.name() not in ["cf.tuple_push", "pd_op.if"]
and input.get_defining_op() is not None
and input.get_defining_op().name() == "builtin.combine"
):
tmp_input = []
for tmp in input.get_defining_op().operands_source():
tmp_input.append(
return_map_value(
tmp, control_flow_value_to_copyvalue_map
)
)
inputs.append(tmp_input)
else:
tmp_input = [
return_map_value(
input, control_flow_value_to_copyvalue_map
)
]
inputs.append(tmp_input)
continue
if (
op.name() != "cf.tuple_push"
and input.get_defining_op() is not None
and input.get_defining_op().name() == "builtin.combine"
):
(
combine_inputs,
combine_stop_gradient,
) = make_input_with_input_stopgradient(input.get_defining_op())
inputs.append([info[0] for info in combine_inputs])
input_grad_stopgradients.append(
[info[0] for info in combine_stop_gradient]
)
else:
tmp_input = [
return_map_value(input, control_flow_value_to_copyvalue_map)
]
inputs.append(tmp_input)
if input in no_grad_set or input.stop_gradient is True:
input_grad_stopgradients.append([True])
else:
input_grad_stopgradients.append([False])
return inputs, input_grad_stopgradients
def update_input_grad_map(op, input_grads, all_inputs):
i = 0
for input, grad_semantic in zip(all_inputs, get_grad_semantic_info(op)):
if not grad_semantic:
continue
if (
op.name() != "cf.tuple_push"
and input.get_defining_op() is not None
and input.get_defining_op().name() == "builtin.combine"
):
update_input_grad_map(
input.get_defining_op(),
input_grads[i],
input.get_defining_op().operands_source(),
)
else:
input_grad = input_grads[i]
if isinstance(input_grad, list):
state.value_to_valuegrad[input].append(input_grad)
else:
state.value_to_valuegrad[input].append([input_grad])
i += 1
def update_if_double_grad_input_grad_map(input_grads, all_inputs):
assert len(input_grads) == len(all_inputs), (
"input_grads should same to all_inputs"
)
for input, input_grad in zip(all_inputs, input_grads):
if isinstance(input_grad, list):
state.value_to_valuegrad[input].append(input_grad)
else:
state.value_to_valuegrad[input].append([input_grad])
def append_yield(
block,
base_op,
base_grad_op,
base_inputs,
base_inputs_grad,
):
(
fwd_block_argument_to_value_map,
fwd_value_to_block_argument_map,
) = argument_to_value(base_op)
with block:
inputs_grad = []
if base_op.name() == "pd_op.while":
new_cond = paddle.base.libpaddle.pir.cf_has_elements(base_op)
inputs_grad.append(new_cond)
# while use block_arg to create grad_op
for idx in range(len(base_inputs[: base_op.num_operands()])):
operands = base_inputs[idx]
operands = return_map_value(
operands, fwd_value_to_block_argument_map
)
base_inputs[idx] = operands
for value, value_grad in zip(base_inputs, base_inputs_grad):
if value_grad is None:
continue
value = return_map_value(
value, state.inside_value_to_outside_value_map
)
if value in state.value_to_valuegrad:
if len(state.value_to_valuegrad[value]) > 1:
append_add_n(
base_op,
value,
state,
backward_ops,
bwd_value_to_block_argument_map,
)
else:
new_value = return_map_value(
value, control_flow_value_to_copyvalue_map
)
if not value_in_block(new_value, block):
# new_value.defining_op is another if block's tuple_pop
state.value_to_valuegrad[value] = [
[paddle.pir.fake_value()]
]
else:
append_full_like(
0.0, new_value, value, state, backward_ops
)
input_grad = return_map_value(
state.value_to_valuegrad[value][0][0],
bwd_value_to_block_argument_map,
)
inputs_grad.append(input_grad)
paddle.base.libpaddle.pir.cf_yield(inputs_grad)
# there are four patterns:
# [builtin.combine , op1] (op1's one input is vectorType, outputs are not vectorType)
# [op2 , builtin.split] (op2's inputs are not vectorType, one output is vectorType)
# [builtin.combine , op3 , builtin.split] (op3's one input and one output are vectorType)
# [op4] (op4's inputs and outputs are not vectorType)
if (
len(effective_forward_ops) > 1
and effective_forward_ops[-1].name() == "cf.yield"
):
yield_op = effective_forward_ops[-1]
if base_op.name() == "pd_op.while":
# while op yield [cond, loop_vars],
# but outputs only has loop_vars.
inside_outputs = yield_op.operands_source()[1:]
else:
inside_outputs = yield_op.operands_source()
for outside_output, inside_output in zip(
base_op.results(), inside_outputs
):
state.inside_value_to_outside_value_map[inside_output] = (
outside_output
)
forward_ops = effective_forward_ops[:-1]
else:
forward_ops = effective_forward_ops
if is_inplace_net(forward_ops):
inverse_effective_forward_ops = reversed(forward_ops)
else:
inverse_effective_forward_ops = inverse_sort_op(forward_ops)
clear_effective_forward_ops = []
for op in inverse_effective_forward_ops:
if op.name() != "builtin.combine" and op.name() != "builtin.split":
clear_effective_forward_ops.append(op)
with bwd_block:
while_tuple_ops = []
for op in clear_effective_forward_ops:
if op_has_vjp(op):
# prepare output_grad
zero_flag, outputs, output_grads = make_output_with_output_grad(
op
)
# prepare input_grad stop_gradient info.
(
inputs,
input_grad_stopgradients,
) = make_input_with_input_stopgradient(op)
if op.name() == "cf.tuple_push":
stackop = op.operand_source(0).get_defining_op()
if stackop.result(2).use_empty():
with dynamic_shape_prim_vjp_guard(op, inputs):
copy_out = paddle.framework.core.call_vjp(
op,
inputs,
outputs,
output_grads,
input_grad_stopgradients,
)
pop_op = bwd_block.ops[-1]
while_tuple_ops.append(pop_op)
while_tuple_ops.append(op)
while_tuple_ops.append(stackop)
update_bwdop_structure(
backward_ops, state.op_to_opgrad[op], [pop_op]
)
for output, copy_output in zip(
inputs[1:], copy_out[1:]
):
control_flow_value_to_copyvalue_map[output[0]] = (
copy_output[0]
)
else:
update_bwdop_structure(
backward_ops,
state.op_to_opgrad[op],
[stackop.result(2).first_use().owner()],
)
else:
# all(zero_flag) support this op has no contribution for grad
# should be delete (prune sub_graph)
if (
len(output_grads) == 0
or all(zero_flag)
or all_output_grad_none(output_grads)
) and op.name() not in [
"pd_op.while",
"pd_op.if",
"pd_op.increment_",
]:
continue
if all_input_stop_gradient_true(
input_grad_stopgradients
) and op.name() not in [
"pd_op.array_read",
"pd_op.array_write_",
"pd_op.increment_",
]:
continue
if op.name() == "pd_op.if":
origin_inputs = get_real_op_inputs(op)
for sub_block in op.blocks():
build_pipe_for_block(sub_block)
# only for double grad if op
true_block = op.as_if_op().true_block()
false_block = op.as_if_op().false_block()
true_block_pop_inputs = []
true_block_pop_input_grad_stopgradients = []
if true_block.ops[0].name() == "cf.tuple_pop":
for result in true_block.ops[0].results():
true_block_pop_inputs.append([result])
true_block_pop_input_grad_stopgradients.append(
[result.stop_gradient]
)
false_block_pop_inputs = []
false_block_pop_input_grad_stopgradients = []
if false_block.ops[0].name() == 'cf.tuple_pop':
for result in false_block.ops[0].results():
false_block_pop_inputs.append([result])
false_block_pop_input_grad_stopgradients.append(
[result.stop_gradient]
)
if (
true_block_pop_inputs != []
or false_block_pop_inputs != []
):
inputs = (
inputs
+ true_block_pop_inputs
+ false_block_pop_inputs
)
input_grad_stopgradients = (
input_grad_stopgradients
+ true_block_pop_input_grad_stopgradients
+ false_block_pop_input_grad_stopgradients
)
with dynamic_shape_prim_vjp_guard(op, inputs):
input_grads = paddle.framework.core.call_vjp(
op,
inputs,
outputs,
output_grads,
input_grad_stopgradients,
)
grad_op = bwd_block.ops[-1]
update_bwdop_structure(
backward_ops, state.op_to_opgrad[op], [grad_op]
)
inputs_used_by_other_op = []
for sub_fwd_block, sub_bwd_block in zip(
op.blocks(), grad_op.blocks()
):
sub_state = state.copy(sub_fwd_block)
for input_ in origin_inputs:
if input_ in state.value_to_valuegrad:
origin_grad = state.value_to_valuegrad[
input_
].copy()
inputs_used_by_other_op.append(
(input_, origin_grad)
)
sub_backward_ops = []
sub_control_flow_value_to_copyvalue_map = (
control_flow_value_to_copyvalue_map.copy()
)
append_backward_ops(
op,
[input[0] for input in inputs[1:]],
[input_grad[0] for input_grad in input_grads],
sub_fwd_block,
sub_bwd_block,
sub_fwd_block.ops,
no_grad_set,
sub_backward_ops,
sub_state,
control_flow_value_to_copyvalue_map=sub_control_flow_value_to_copyvalue_map,
)
for input_tuple in inputs_used_by_other_op:
state.value_to_valuegrad[input_tuple[0]] = (
input_tuple[1]
)
update_if_output_stopgradient(
grad_op,
grad_op.as_if_op().true_block().ops[-1],
grad_op.as_if_op().false_block().ops[-1],
)
for input_tuple in inputs_used_by_other_op:
state.value_to_valuegrad[input_tuple[0]] = []
# update input_grad map
if (
true_block_pop_inputs != []
or false_block_pop_inputs != []
):
true_block_pop_inputs = (
update_tuple_pop_origin_inputs(
true_block_pop_inputs
)
)
false_block_pop_inputs = (
update_tuple_pop_origin_inputs(
false_block_pop_inputs
)
)
# delete cond inputs
origin_inputs = (
origin_inputs[1:]
+ true_block_pop_inputs
+ false_block_pop_inputs
)
update_if_double_grad_input_grad_map(
input_grads, origin_inputs
)
else:
update_input_grad_map(
op, input_grads, origin_inputs
)
elif op.name() == "pd_op.while":
origin_inputs = get_real_op_inputs(op)
# prepare while[cond, loop_vars, other_input] other_input's grad
while_block = op.as_while_op().body()
sub_state = state.copy(while_block)
for i, input in enumerate(
get_used_external_value(while_block)
):
if input in sub_state.value_to_valuegrad:
if len(sub_state.value_to_valuegrad[input]) > 1:
append_add_n(
op,
input,
state,
backward_ops,
bwd_value_to_block_argument_map,
)
if (
input not in sub_state.value_to_valuegrad
or sub_state.value_to_valuegrad[input] == []
):
append_full_like(
0.0, input, input, sub_state, backward_ops
)
grad_value = sub_state.value_to_valuegrad[input][0]
for tmp in state.value_to_valuegrad[input]:
state.value_to_sumvaluegrad[input].append(tmp)
state.value_to_valuegrad[input] = []
output_grads.append(
return_map_value_list(
grad_value,
bwd_value_to_block_argument_map,
)
)
build_pipe_for_block(while_block)
with dynamic_shape_prim_vjp_guard(op, inputs):
input_grads = paddle.framework.core.call_vjp(
op,
inputs,
outputs,
output_grads,
input_grad_stopgradients,
)
grad_op = bwd_block.ops[-1]
update_bwdop_structure(
backward_ops, state.op_to_opgrad[op], [grad_op]
)
# update grad_op structure
(
_,
sub_bwd_value_to_block_argument_map,
) = argument_to_value(grad_op)
sub_bwd_value_to_block_argument_map.update(
bwd_value_to_block_argument_map
)
sub_control_flow_value_to_copyvalue_map = (
control_flow_value_to_copyvalue_map.copy()
)
while_grad_block = grad_op.as_while_op().body()
sub_backward_ops = []
append_backward_ops(
op,
[input[0] for input in inputs],
[input_grad[0] for input_grad in input_grads],
while_block,
while_grad_block,
while_block.ops,
no_grad_set,
sub_backward_ops,
sub_state,
sub_bwd_value_to_block_argument_map,
sub_control_flow_value_to_copyvalue_map,
)
update_while_output_stopgradient(
grad_op, while_grad_block.ops[-1]
)
# update input_grad map
update_input_grad_map(op, input_grads, origin_inputs)
elif op.name() == "pd_op.pylayer":
# create grad_op
before_ops_num = len(bwd_block.ops)
with dynamic_shape_prim_vjp_guard(op, inputs):
input_grads = paddle.framework.core.call_vjp(
op,
inputs,
outputs,
output_grads,
input_grad_stopgradients,
)
after_ops_num = len(bwd_block.ops)
update_bwdop_structure_(
backward_ops,
state.op_to_opgrad[op],
bwd_block.ops,
before_ops_num,
after_ops_num,
)
# update input_grad map
update_input_grad_map(
op, input_grads, get_real_op_inputs(op)
)
else:
# create grad_op
before_ops_num = len(bwd_block.ops)
with (
dynamic_shape_prim_vjp_guard(op, inputs),
pir_op_name_guard(op.name() + '_grad'),
):
input_grads = paddle.framework.core.call_vjp(
op,
inputs,
outputs,
output_grads,
input_grad_stopgradients,
)
after_ops_num = len(bwd_block.ops)
update_bwdop_structure_(
backward_ops,
state.op_to_opgrad[op],
bwd_block.ops,
before_ops_num,
after_ops_num,
)
# update input_grad map
update_input_grad_map(
op, input_grads, op.operands_source()
)
else:
if (
op.num_operands() == 0
and op.num_results() != 0
or op.name() == "pd_op.full_like"
or op.name() == "cf.tuple_pop"
):
for value in op.results():
if value not in state.value_to_valuegrad:
continue
if len(state.value_to_valuegrad[value]) > 1:
append_add_n(
op,
value,
state,
backward_ops,
bwd_value_to_block_argument_map,
)
else:
state.op_to_opgrad[op] = []
else:
all_results_stop_gradient = True
for value in op.results():
if not value.stop_gradient:
all_results_stop_gradient = False
if (
not is_builtin_op(op)
and not paddle.core.is_forward_only(op)
and not all_results_stop_gradient
):
raise ValueError(
f"op '{op.name()}' has no grad op, consider enable prim to decompose it."
)
state.op_to_opgrad[op] = []
if fwd_block != bwd_block:
if while_prune_check(while_tuple_ops):
remove_op(bwd_block, while_tuple_ops[0], state)
while_tuple_ops[1].get_parent_block().remove_op(
while_tuple_ops[1]
)
while_tuple_ops[2].get_parent_block().remove_op(
while_tuple_ops[2]
)
append_yield(
bwd_block,
base_op,
bwd_block.parent_op,
base_inputs,
base_input_grads,
)
def prepare_backward_prune_set(inputs, outputs):
outputs_fwd_set = ValueSet()
for input_ in inputs:
if not input_.use_empty():
for used_op in input_.all_used_ops():
for item in get_real_op_inputs(used_op):
outputs_fwd_set.add(item)
else:
warning_once("input provided by inputs has no use")
inputs_fwd_set = ValueSet()
for output in outputs:
inputs_fwd_set.add(output)
return outputs_fwd_set, inputs_fwd_set
def create_backward_prune_set(
outputs_fwd_set, inputs_fwd_set, no_grad_set, state
):
outputs_set = ValueSet()
for item in outputs_fwd_set:
if state.value_to_valuegrad[item] != []:
outputs_set.add(state.value_to_valuegrad[item][0][0])
inputs_set = ValueSet()
for item in inputs_fwd_set:
if state.value_to_valuegrad[item] != []:
inputs_set.add(state.value_to_valuegrad[item][0][0])
inputs_set_tmp = ValueSet()
for out_grad in inputs_set:
if not out_grad.use_empty():
for item in get_real_op_inputs(out_grad.first_use().owner()):
inputs_set_tmp.add(item)
inputs_set.update(inputs_set_tmp)
no_gradvar_set = ValueSet() # grad_value of value in no_grad_set
for key in state.value_to_valuegrad:
if key in no_grad_set and state.value_to_valuegrad[key] != []:
no_gradvar_set.add(state.value_to_valuegrad[key][0][0])
for key in state.value_to_sumvaluegrad:
if key in no_grad_set:
for item in state.value_to_sumvaluegrad[key][0]:
no_gradvar_set.add(item)
return outputs_set, inputs_set, no_gradvar_set
def _complete_grad_op_chunk_id(block, state):
dist_skip_op_list = ["builtin.split", "builtin.combine"]
def infer_dist_skip_op_chunk_id(op):
if op.name() == "builtin.split":
op_chunk_id = (
op.operand_source(0).get_defining_op().dist_attr.chunk_id
)
elif op.name() == "builtin.combine":
op_chunk_id = op.result(0).get_defining_op().dist_attr.chunk_id
else:
# TODO(luchang): need to support more ops such as pd_op.pylayer and so on
op_chunk_id = -1
return op_chunk_id
# TODO(Ruibiao): Reorganize these unclear codes about chunk_id
def get_op_chunk_id(op):
if op.has_attr("chunk_id"):
op_chunk_id = op.chunk_id
elif op.dist_attr is None:
op_chunk_id = -1
if op.name() in dist_skip_op_list:
op_chunk_id = infer_dist_skip_op_chunk_id(op)
else:
op_chunk_id = op.dist_attr.chunk_id
if op_chunk_id == -1 and op.name() == "dist_op.reshard":
prev_op = op.operand_source(0).get_defining_op()
if prev_op.name() in dist_skip_op_list:
op_chunk_id = infer_dist_skip_op_chunk_id(prev_op)
else:
op_chunk_id = prev_op.dist_attr.chunk_id
return op_chunk_id
is_dist_program = False
for op in block.ops:
if op.dist_attr is not None:
is_dist_program = True
break
if not is_dist_program:
return
for op in reversed(block.ops):
if op not in state.op_to_opgrad:
continue
fwd_op_chunk_id = get_op_chunk_id(op)
for bwd_op in state.op_to_opgrad[op]:
if op.has_attr("chunk_id"):
bwd_op.set_int_attr("chunk_id", op.chunk_id)
continue
elif bwd_op.dist_attr is None:
continue
if bwd_op.name() in ["pd_op.add_", "pd_op.add_n_"]:
bwd_op_chunk_id = -1
for operand_idx in range(bwd_op.num_operands()):
prev_op_chunk_id = get_op_chunk_id(
bwd_op.operand_source(operand_idx).get_defining_op()
)
if bwd_op_chunk_id == -1:
bwd_op_chunk_id = prev_op_chunk_id
else:
assert bwd_op_chunk_id == prev_op_chunk_id, (
f"Inconsistent prev_op chunk id with {bwd_op_chunk_id} != {prev_op_chunk_id}\n"
"{bwd_op.operand_source(operand_idx-1).get_defining_op()}\n"
"{bwd_op.operand_source(operand_idx).get_defining_op()}"
)
else:
bwd_op_chunk_id = fwd_op_chunk_id
bwd_op.dist_attr = (
paddle.base.libpaddle.pir.create_op_dist_attribute(
bwd_op.dist_attr.process_mesh,
bwd_op.dist_attr.operands(),
bwd_op.dist_attr.results(),
bwd_op_chunk_id,
)
)
def calc_gradient_helper(
outputs: Value | Sequence[Value],
inputs: Value | Sequence[Value],
grad_outputs: Value | Sequence[Value | None] | None = None,
no_grad_set: set[Value] | None = None,
) -> ValueDict:
block = paddle.base.libpaddle.pir.get_current_insertion_point().block()
state = State(block)
if all_stop_gradient_true(block):
logging.warning(
"all op in block stop_gradient is True, no grad will be calculate"
)
return state.value_to_valuegrad
total_ops = parent_total_ops(block)
# update no_grad_set if some value stop_gradient=True
update_no_grad_set_by_stopgradient(block, no_grad_set)
with block:
(
complete_grad_outputs,
complete_outputs,
backward_ops,
) = prepare_grad_outputs(grad_outputs, outputs, state)
inputs_set = ValueSet(inputs)
stop_gradient_false_outputs = []
for output in complete_outputs:
if output not in no_grad_set:
stop_gradient_false_outputs.append(output)
outputs_set = ValueSet(stop_gradient_false_outputs)
if is_inplace_net(total_ops):
effective_forward_ops = total_ops
else:
effective_forward_ops, _ = prune_ops(
total_ops, inputs_set, outputs_set, no_grad_set
)
outputs_fwd_set, inputs_fwd_set = prepare_backward_prune_set(
inputs, complete_outputs
)
append_backward_ops(
None,
None,
None,
block,
block,
effective_forward_ops,
no_grad_set,
backward_ops,
state,
ValueDict(),
)
# now value_to_valuegrad should be value <-> value (add sum op for the same values's grad value)
outputs_set, inputs_set, no_gradvar_set = create_backward_prune_set(
outputs_fwd_set, inputs_fwd_set, no_grad_set, state
)
# set chunk id for grad ops
_complete_grad_op_chunk_id(block, state)
remove_ops = []
if not is_inplace_net(backward_ops) and inputs:
_, remove_ops = prune_ops(
backward_ops, inputs_set, outputs_set, no_gradvar_set
)
state.turn_map()
remove_ops = set(remove_ops)
for op in inverse_sort_op(list(backward_ops)):
if op.name() == 'pd_op.full_like':
if op.result(0).use_empty():
remove_ops.add(op)
remove_ops.add(op.operand_source(1).get_defining_op())
elif is_control_flow(op):
for sub_block in op.blocks():
remove_useless_full_like_ops(sub_block, sub_block.ops, state)
for bwd_op in inverse_sort_op(remove_ops):
if bwd_op.result(0) in ValueSet(complete_grad_outputs):
continue
if bwd_op.result(0).use_empty():
remove_op(block, bwd_op, state)
state.turn_map()
input_grad_map = state.value_to_valuegrad
return input_grad_map
def calc_gradient(
outputs: Value | Sequence[Value],
inputs: Value | Sequence[Value],
grad_outputs: Value | Sequence[Value | None] | None = None,
no_grad_set: set[Value] | None = None,
) -> list[Value | None]:
"""
calculate gradient of input
Args:
outputs (Value|list(Value)|tuple(Value)): the output Value or
Value list/tuple of the graph to compute gradients.
inputs (Value|list(Value)|tuple(Value)): the input Value or
Value list/tuple of the graph to compute gradients. The returned
values of this API are the gradients of `inputs` .
grad_outputs (Value|list(Value|None)|tuple(Value|None), optional):
initial gradient values of `outputs` . If `grad_outputs` is None,
the initial gradient values of `outputs` would be Values filled with 1;
if `grad_outputs` is not None, it must have the same length as `outputs` ,
and in this case, the initial gradient value of the i-th `outputs` would
be: (1) a Value filled with 1 when the i-th element of `grad_outputs`
is None; (2) the i-th element of `grad_outputs` when the i-th element of
`grad_outputs` is a Value. Default None.
no_grad_set (set(Value), optional):
the Values whose gradients are not needed to compute. Default None.
Return:
list[Value]:A list of gradients for inputs
If an input does not affect targets, the corresponding gradient Tensor
will be None
TODO if allow_unused=False raise TypeError() if input_grad has None
"""
# record input value and its gradient (Value to Value)
input_to_inputgrad_map = calc_gradient_helper(
outputs,
inputs,
grad_outputs=grad_outputs,
no_grad_set=ValueSet(no_grad_set),
)
inputgrad = []
for input in inputs:
inputgrad.append(
input_to_inputgrad_map[input][0][0]
if input_to_inputgrad_map[input] != []
else None
)
return inputgrad
def grad(
outputs: Value | Sequence[Value],
inputs: Value | Sequence[Value],
grad_outputs: Value | Sequence[Value | None] | None = None,
retain_graph: bool | None = None,
create_graph: bool | None = False,
only_inputs: bool | None = True,
allow_unused: bool | None = False,
no_grad_vars: Value | Sequence[Value] | set[Value] | None = None,
) -> list[Value | None]:
'''
.. note::
**This API is ONLY available in imperative mode.**
This API computes the sum of gradients of `outputs` with respect to each `inputs` .
Parameters:
outputs (Value|list(Value)|tuple(Value)): the output Value or
Value list/tuple of the graph to compute gradients.
inputs (Value|list(Value)|tuple(Value)): the input Value or
Value list/tuple of the graph to compute gradients. The returned
values of this API are the gradients of `inputs` .
grad_outputs (Value|list(Value|None)|tuple(Value|None), optional):
initial gradient values of `outputs` . If `grad_outputs` is None,
the initial gradient values of `outputs` would be Values filled with 1;
if `grad_outputs` is not None, it must have the same length as `outputs` ,
and in this case, the initial gradient value of the i-th `outputs` would
be: (1) a Value filled with 1 when the i-th element of `grad_outputs`
is None; (2) the i-th element of `grad_outputs` when the i-th element of
`grad_outputs` is a Value. Default None.
retain_graph (bool, optional): whether to retain the forward graph which
is used to calculate the gradient. When it is True, the graph would
be retained, in which way users can calculate backward twice for the
same graph. When it is False, the graph would be freed. Default None,
which means it is equal to `create_graph` .
create_graph (bool, optional): whether to create the gradient graphs of
the computing process. When it is True, higher order derivatives are
supported to compute; when it is False, the gradient graphs of the
computing process would be discarded. Default False.
only_inputs (bool, optional): whether to only compute the gradients of
`inputs` . If it is False, the gradients of all remaining leaf
Values in the graph would be also computed and accumulated.
If it is True, only the gradients of `inputs` would be computed.
Default True. only_inputs=False is under development, and it is
not supported yet.
allow_unused (bool, optional): whether to raise error or return None if some
Values of `inputs` are unreachable in the graph. If some Values of
`inputs` are unreachable in the graph (i.e., their gradients are None),
error would be raised if allow_unused=False, or None would be returned as
their gradients if allow_unused=True. Default False.
no_grad_vars (Value|list(Value)|tuple(Value)|set(Value), optional):
the Values whose gradients are not needed to compute. Default None.
Returns:
list: a list of Values, whose length is the same as the Value number
inside `inputs`, and the i-th returned Value is the sum of gradients of
`outputs` with respect to the i-th `inputs`.
'''
check_type(
outputs,
'outputs',
(paddle.pir.Value, list, tuple),
'paddle.autograd.ir_backward.grad',
)
check_type(
inputs,
'inputs',
(paddle.pir.Value, list, tuple),
'paddle.autograd.ir_backward.grad',
)
check_type(
grad_outputs,
'grad_outputs',
(paddle.pir.Value, list, tuple, type(None)),
'paddle.autograd.ir_backward.grad',
)
check_type(
no_grad_vars,
'no_grad_vars',
(
paddle.pir.Value,
list,
tuple,
set,
ValueSet,
type(None),
),
'paddle.autograd.ir_backward.grad',
)
outputs = _as_list(outputs)
inputs = _as_list(inputs)
grad_outputs = _as_list(grad_outputs)
if no_grad_vars is None:
no_grad_set = ValueSet()
else:
no_grad_set = ValueSet(no_grad_vars)
input_grad = calc_gradient(outputs, inputs, grad_outputs, no_grad_set)
return input_grad
def append_backward(loss, parameter_list=None, no_grad_set=None):
'''
Parameters:
loss (Value): The loss Tensor of the network
parameter_list (Value|list(Value)|tuple(Value)): List/Tuple of Parameters
that need to be updated by optimizers.
If it is None, all parameters
will be updated.
Default: None.
no_grad_vars (Value|list(Value)|tuple(Value)|set(Value), optional):
the Values whose gradients are not needed to compute. Default None.
Returns:
list of tuple (Value): Pairs of parameter and its corresponding gradients.
The key is the parameter and the value is gradient Tensor.
'''
check_type(
loss,
'loss',
paddle.pir.Value,
'paddle.autograd.ir_backward.append_backward',
)
if parameter_list is not None:
check_type(
parameter_list,
'parameter_list',
(list, tuple, set),
'paddle.autograd.ir_backward.append_backward',
)
for i, param in enumerate(parameter_list):
check_type(
param,
f'parameter_list[{i}]',
paddle.pir.Value,
'base.backward.append_backward',
)
else:
ops = loss.get_defining_op().get_parent_block().ops
parameter_list = []
for op in ops:
if not op.has_attr("persistable"):
continue
persist_value = [
result for result in op.results() if result.persistable
]
parameter_list.extend(persist_value)
if no_grad_set is None:
no_grad_set_ = ValueSet()
else:
no_grad_set_ = ValueSet(no_grad_set)
input_to_inputgrad_map = calc_gradient_helper(
_as_list(loss),
[],
grad_outputs=[],
no_grad_set=ValueSet(no_grad_set_),
)
input_inputs_grad = []
for input in parameter_list:
input_inputs_grad.append(
(
input,
(
input_to_inputgrad_map[input][0][0]
if input_to_inputgrad_map[input] != []
else None
),
)
)
return input_inputs_grad