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

1396 lines
50 KiB
Python

# Copyright (c) 2020 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 itertools
from contextlib import contextmanager
from functools import cached_property
from typing import TYPE_CHECKING
import numpy as np
import paddle
import paddle.pir.core as ir_static
from paddle import _C_ops
from paddle.autograd.backward_utils import ValueDict
from paddle.autograd.ir_backward import grad
from paddle.base import core, framework
from paddle.base.compiler import BuildStrategy
from paddle.base.data_feeder import check_type
from paddle.base.dygraph.base import switch_to_static_graph
from paddle.pir import Value, fake_value, get_fake_value_name, is_fake_value
from ..profiler import event_register
from .logging_utils import TranslatorLogger
from .utils import (
RETURN_NO_VALUE_MAGIC_NUM,
Backend,
CUDAGraphState,
TimeCounter,
auto_layout_is_enabled,
backend_guard,
cse_is_enabled,
maybe_dynamic_shape_tensor,
use_specialized_device,
)
if TYPE_CHECKING:
from .program_translator import ConcreteProgram
__all__ = []
prog_logger = TranslatorLogger()
FAKE_VALUE_NAME = get_fake_value_name()
def get_value_name(value):
if is_fake_value(value):
return FAKE_VALUE_NAME
return value.name
def apply_general_passes(
program, *, enable_cse=True, enable_delete_assert_op=True
):
pm = paddle.pir.PassManager(2)
if enable_cse:
pm.add_pass("common_subexpression_elimination_pass", {})
if enable_delete_assert_op:
pm.add_pass("delete_assert_op_pass", {})
pm.run(program)
class NestSequence:
"""
A wrapper class that easily to flatten and restore the nest structure of
given sequence. It also remove the duplicate variables in the sequence.
For example:
>>> t = [v1, v2, v1]
>>> m = tolist(t)
[v1, v2]
>>> m.restore([t1, t2])
[t1, t2, t1]
"""
def __init__(self, raw_input):
self._raw_input = raw_input
self._var_map, self._var_list = self._tolist()
@property
def var_list(self):
return self._var_list
def _tolist(self):
"""
Flattens the nested sequences into single list and remove duplicate variables + non-variable elements.
"""
variable_map = ValueDict() # value -> list idx
variable_list = []
for value in paddle.utils.flatten(self._raw_input):
if not isinstance(value, Value):
continue
if value in variable_map:
# remove duplicate values.
continue
variable_map[value] = len(variable_list)
variable_list.append(value)
return variable_map, variable_list
def restore(self, tensor_result_list):
"""
Restores the nested sequence from tensor list.
"""
assert len(self._var_list) == len(tensor_result_list)
def to_tensor_result(x):
if isinstance(x, Value):
return tensor_result_list[self._var_map[x]]
return x
return paddle.utils.pack_sequence_as(
self._raw_input,
list(map(to_tensor_result, paddle.utils.flatten(self._raw_input))),
)
@cached_property
def quick_index_map(self):
raw_inputs = self._raw_input
if len(raw_inputs) == 1:
raw_inputs = raw_inputs[0]
assert all(isinstance(v, Value) for v in raw_inputs)
return [self._var_map[v] for v in raw_inputs]
def quick_restore(self, tensor_list):
return [tensor_list[idx] for idx in self.quick_index_map]
def __getitem__(self, item):
return self._var_list[item]
class RunnableProgram:
"""a pir program ready for run_program_op to run. constructed by 3 parts:
- pir program (pir::Program)
- in_out_values
- input_x values ([string | pir::Value])
- input_param values ([string | pir::Value])
- output values ([string | pir::Value])
- forward_backward_ranges
- forward_range (tuple(Int, Int)) | None
- backward_range (tuple(Int, Int)) | None
"""
@staticmethod
def _get_program_all_values(program):
all_values = []
def extend_values(block):
all_values.extend(block.kwargs().values())
for op in block.ops:
all_values.extend(op.results())
for block in op.blocks():
extend_values(block)
extend_values(program.global_block())
return all_values
@staticmethod
def _get_name_value_map_from_program(program) -> dict[str, Value]:
name_to_value_dict: dict[str, Value] = {FAKE_VALUE_NAME: fake_value()}
for value in RunnableProgram._get_program_all_values(program):
for name in value._names:
name_to_value_dict[name] = value
return name_to_value_dict
@cached_property
def name_value_map(self):
return RunnableProgram._get_name_value_map_from_program(self.program)
def convert_name(self, values):
if len(values) == 0:
return []
if isinstance(values[0], str):
return values
return [get_value_name(v) for v in values]
@cached_property
def x_values(self):
return [self.name_value_map[v] for v in self.x_names]
@cached_property
def param_values(self):
return [self.name_value_map[v] for v in self.param_names]
@cached_property
def out_values(self):
return [self.name_value_map[v] for v in self.out_names]
@cached_property
def x_grad_values(self):
return [self.name_value_map[v] for v in self.x_grad_names]
@cached_property
def param_grad_values(self):
return [self.name_value_map[v] for v in self.p_grad_names]
@cached_property
def out_grad_values(self):
return [self.name_value_map[v] for v in self.o_grad_names]
def __init__(
self,
program,
in_out_values,
out_stop_gradients,
grad_in_out_values=None,
forward_range=None,
backward_range=None,
):
assert isinstance(in_out_values, tuple), (
"in_out_values must be tuple with len == 3"
)
assert len(in_out_values) == 3, (
"in_out_values must be tuple with len == 3"
)
assert isinstance(in_out_values[0], list), (
"in_out_values must be tuple with len == 3"
)
self.program = program
self.x_names = self.convert_name(in_out_values[0])
self.param_names = self.convert_name(in_out_values[1])
self.out_names = self.convert_name(in_out_values[2])
self.out_stop_gradients = out_stop_gradients
self.forward_range = forward_range
self.backward_range = backward_range
self.has_splited = False
self.finish_pass = False
if self.forward_range is None:
self.forward_range = (0, len(self.program.global_block().ops))
if self.backward_range is None:
self.backward_range = (
len(self.program.global_block().ops),
len(self.program.global_block().ops),
)
if grad_in_out_values is None:
grad_in_out_values = [], [], []
self.x_grad_names = self.convert_name(grad_in_out_values[0])
self.p_grad_names = self.convert_name(grad_in_out_values[1])
self.o_grad_names = self.convert_name(grad_in_out_values[2])
# Flag operator, indicating the operator between the forward subgraph and the backward subgraph. After self.program is updated by the pass, it is recommended to use the self.update_op_range interface to update the forward_range and backward_range.
self.fwd_end_next_op = (
self.program.global_block().ops[self.forward_range[1]]
if self.forward_range[1] < len(self.program.global_block().ops)
else None
)
self.bwd_start_pre_op = (
self.program.global_block().ops[self.backward_range[0] - 1]
if (
self.backward_range[0] > 0
and self.backward_range[0] - 1
< len(self.program.global_block().ops)
)
else None
)
self.bwd_end_nex_op = (
self.program.global_block().ops[self.backward_range[1]]
if self.backward_range[1] < len(self.program.global_block().ops)
else None
)
def update_op_range(self):
if self.fwd_end_next_op is None or self.bwd_start_pre_op is None:
self.forward_range = (0, len(self.program.global_block().ops))
self.backward_range = (
len(self.program.global_block().ops),
len(self.program.global_block().ops),
)
else:
fwd_start = self.forward_range[0]
fwd_end = self.forward_range[1]
bwd_start = self.backward_range[0]
bwd_end = self.backward_range[1]
for idx, op in enumerate(self.program.global_block().ops):
if op == self.fwd_end_next_op:
fwd_end = idx
if op == self.bwd_start_pre_op:
bwd_start = idx + 1
if op == self.bwd_end_nex_op:
bwd_end = idx
if self.bwd_end_nex_op is None:
bwd_end = len(self.program.global_block().ops)
self.forward_range = (fwd_start, fwd_end)
self.backward_range = (bwd_start, bwd_end)
def clone(self):
cloned_program, _ = paddle.base.libpaddle.pir.clone_program(
self.program
)
return RunnableProgram(
cloned_program,
(self.x_names, self.param_names, self.out_names),
self.out_stop_gradients,
None,
self.forward_range,
self.backward_range,
)
def split_forward_backward(self):
assert self.has_splited is False, (
"Please ensure only split once! don't call split_forward_backward manually."
)
self.has_splited = True
self.update_op_range()
(
[
fwd_prog,
bwd_prog,
],
prog_attr,
) = paddle.base.libpaddle.pir.split_program(
self.program,
self.x_values,
self.param_values,
self.out_values,
self.x_grad_values,
self.param_grad_values,
self.out_grad_values,
self.forward_range,
self.backward_range,
)
return [fwd_prog, bwd_prog], prog_attr
def apply_pir_program_pass(self, pass_fn):
"""
Main entries for pass function, without considering any input/output and forward segmentation.
pass_fn' signature is:
1. This function will change forward and backward program.
2. call self.program_attr means start to run.
so we can't call this function after program_attr is called.
def pass_fn(forward_program, backward_program):
return forward_program, backward_program
"""
origin_fwd = self.forward_program
origin_bwd = self.backward_program
prog_logger.log(
1,
f"******** [JIT] PIR forward program before PIR PASS ********\n{origin_fwd} ",
)
prog_logger.log(
1,
f"******** [JIT] PIR backward program before PIR PASS ********\n{origin_bwd} ",
)
# NOTE(dev): Add this line to trigger program_name_attr logic
program_name_attr = self.program_name_attr
self.forward_program, self.backward_program = pass_fn(
origin_fwd, origin_bwd, program_name_attr
)
prog_logger.log(
1,
f"******** [JIT] PIR forward program after PIR PASS ********\n{origin_fwd} ",
)
prog_logger.log(
1,
f"******** [JIT] PIR backward program after PIR PASS ********\n{origin_bwd} ",
)
def is_distributed_program(self):
for op in self.program.global_block().ops:
if op.dist_attr is not None:
return True
return False
def apply_dist_pass_for_origin_program(self):
if self.is_distributed_program():
paddle.distributed.auto_parallel.static.mix_to_dist_pass.apply_mix2dist_pass(
self.program
)
def apply_dist_pass_for_whole_program(self):
if self.is_distributed_program():
paddle.distributed.auto_parallel.static.mix_to_dist_pass.apply_mix2dist_pass(
self.program
)
paddle.distributed.auto_parallel.static.pir_pass.apply_partition_pass(
self.program
)
paddle.distributed.auto_parallel.static.pir_pass.ReshardPasses.apply_reshard_pass(
self.program
)
paddle.base.libpaddle.pir.apply_dist2dense_pass(self.program)
paddle.distributed.auto_parallel.static.pir_pass.remove_unuseful_comm_op_pass(
self.program
)
# cached property can ensure program is splited only once.
@cached_property
def _forward_backward_program(self):
return self.split_forward_backward()
@cached_property # shouldn't changed when call this once.
def program_attr(self):
assert self.finish_pass is False, (
"program_attr() is called by PartialProgramLayer, don't call it manually, use program_name_attr instead."
)
# can't apply pass after call this function.
self.finish_pass = True
fwd_map = RunnableProgram._get_name_value_map_from_program(
self.forward_program
)
program_name_attr = self.program_name_attr
no_need_buffer_names = program_name_attr["no_need_buffers"]
rename_mapping = {}
rename_mapping = RunnableProgram.unify_value_names(
self.forward_program, rename_mapping
)
rename_mapping = RunnableProgram.unify_value_names(
self.backward_program, rename_mapping
)
# Update no_need_buffer_names by rename_mapping
for original_name, new_name in rename_mapping.items():
if {original_name, new_name} & set(no_need_buffer_names):
if original_name in no_need_buffer_names:
no_need_buffer_names.remove(original_name)
if new_name in no_need_buffer_names:
no_need_buffer_names.remove(new_name)
RunnableProgram.update_program_name_attr(
self.program_name_attr, rename_mapping
)
program_attr = {}
for k, ns in self.program_name_attr.items():
# Pass output values to create tensors in run program impl
if k == "fo":
program_attr[f"{k}_values"] = [
fwd_map.get(n, fake_value()) for n in ns
]
program_attr[f"{k}_names"] = ns
# Restore stop_gradient for output values
assert len(program_attr["fo_values"]) == len(self.out_stop_gradients), (
"Output values and stop gradients length mismatch"
)
for v, stop_gradient in zip(
program_attr["fo_values"], self.out_stop_gradients
):
if is_fake_value(v):
continue
v.stop_gradient = stop_gradient
return program_attr
@staticmethod
def unify_value_names(
program, rename_mapping: dict[str, str]
) -> dict[str, str]:
"""Ensure every value at most has one name in the program."""
rename_mapping = dict(rename_mapping)
for value in RunnableProgram._get_program_all_values(program):
if not value.has_name:
continue
new_name = value.name # get first name
new_name = rename_mapping.get(new_name, new_name)
rename_mapping.update(
value._rename(new_name, program.global_block())
)
# Get all values again because some values has been erased.
for value in RunnableProgram._get_program_all_values(program):
if value.has_name:
assert value._has_only_one_name(), (
f"Expected all values in Program have only one name, but {value} has multiple names: {value._names}"
)
return rename_mapping
@staticmethod
def update_program_name_attr(
name_attr: dict[str, list[str]], rename_mapping: dict[str, str]
):
for k, vs in name_attr.items():
name_attr[k] = [
rename_mapping[v] if v in rename_mapping else v for v in vs
]
@cached_property
def program_name_attr(self):
origin_attr = self._forward_backward_program[1]
_program_attr = {}
for k, vs in origin_attr.items():
_program_attr[k] = [get_value_name(v) for v in vs]
return _program_attr
@cached_property
def forward_program(self):
return self._forward_backward_program[0][0]
@cached_property
def backward_program(self):
return self._forward_backward_program[0][1]
class PartialProgramLayerHook:
def before_append_backward(self, forward_program, src_vars):
return forward_program, src_vars
def after_append_backward(
self,
whole_program,
inputs,
src_vars,
grad_outputs,
forward_end_idx,
backward_start_idx,
):
return whole_program, forward_end_idx, src_vars
def after_infer(self, infer_program):
return infer_program
class OperatorIndexPreservePass:
OP_NAME_PREFIX = "preserved_index_"
counter = 0
def __init__(self, index, pass_fn):
self.name = f"{OperatorIndexPreservePass.OP_NAME_PREFIX}{OperatorIndexPreservePass.counter}"
OperatorIndexPreservePass.counter += 1
self.pass_fn = pass_fn
self.index = index
def __call__(self, program):
if len(program.global_block().ops) == 0:
assert self.index == 0
return self.pass_fn(program)
paddle.base.libpaddle.pir.append_shadow_output(
program,
program.global_block().ops[0].result(0),
self.name,
self.index,
)
program = self.pass_fn(program)
new_index = 0
for op in program.global_block().ops:
if (
op.name() == "builtin.shadow_output"
and self.name in op.attrs()["output_name"]
):
break
new_index += 1
# remove forward_backward_separator
if new_index >= len(program.global_block().ops):
raise RuntimeError(
f"Can't find index preserve label {self.name}, don't remove it in pass."
)
program.global_block().remove_op(program.global_block().ops[new_index])
self.index = new_index
return program
class IndicesPreservePass:
def __init__(self, indices, pass_fn):
self.pass_fn = pass_fn
self.indices = indices
self.new_indices = None
def __call__(self, program):
passes = [self.pass_fn]
for idx, index in enumerate(self.indices):
passes.append(OperatorIndexPreservePass(index, passes[idx]))
new_program = passes[-1](program)
self.new_indices = [p.index for p in passes[1:]]
return new_program
class ValuePreservePass:
OP_NAME_PREFIX = "preserved_value_"
def __init__(self, values, use_cinn_pass):
self.values = values
self.use_cinn_pass = use_cinn_pass
def apply(self, program):
raise RuntimeError("Not implemented.")
@staticmethod
def create_name_generator(prefix):
count = 0
def name_gen():
nonlocal count
name = f"{prefix}{count}"
count += 1
return name
return name_gen
@staticmethod
def attach_preserved_name(value, program, value2name, name_generator):
if is_fake_value(value):
return None
if value in value2name:
return value2name[value]
name = name_generator()
value2name[value] = name
paddle.base.libpaddle.pir.append_shadow_output(
program,
value,
name,
len(program.global_block().ops),
)
return name
def __call__(self, program):
# create preserved op for args
value2name = ValueDict()
name_generator = ValuePreservePass.create_name_generator(
ValuePreservePass.OP_NAME_PREFIX
)
names = paddle.utils.map_structure(
lambda value: ValuePreservePass.attach_preserved_name(
value,
program,
value2name, # noqa: F821
name_generator,
),
self.values,
)
# NOTE(SigureMo): Value maybe removed in pass, don't use value2name after pass
del value2name
# apply program pass
program = self.apply(program)
# collect new value
name2new_value = {}
to_remove_op = []
for op in program.global_block().ops:
if op.name() == "builtin.shadow_output":
if op.attrs()["output_name"].startswith(
ValuePreservePass.OP_NAME_PREFIX
):
name2new_value[op.attrs()["output_name"]] = op.operand(
0
).source()
to_remove_op.append(op)
# remove old op
for op in to_remove_op:
program.global_block().remove_op(op)
self.values = paddle.utils.map_structure(
lambda name: name2new_value.get(name, fake_value()), names
)
return program
class FullGraphPreProcessPass(ValuePreservePass):
def apply(self, program):
program = paddle.base.libpaddle.pir.apply_bn_add_act_pass(program)
if self.use_cinn_pass:
# NOTE(gongshaotian): execute infer_symbolic_shape_pass before reduce_as_sum_pass
pm = paddle.base.libpaddle.pir.PassManager()
pm.add_pass("delete_assert_op_pass", {})
paddle.base.libpaddle.pir.infer_symbolic_shape_pass(pm, program)
paddle.base.libpaddle.pir.reduce_as_sum_pass(pm, program)
pm.run(program)
return program
class PartialProgramLayer:
"""
PartialProgramLayer wraps all the ops from layers decorated by `@to_static`
and execute them as a static subgraph.
.. note::
**1. This is a very low level API. Users should not use this API
directly. Please use `partial_program_from(concrete_program)`
to create it.
**2. TensorArray is not currently supported in the output.
Args:
main_program(Program): The main program that contains ops need to be executed.
inputs(list[Variable]): The input list of the decorated function by `@to_static`.
outputs(list[Variable]): The output list of the decorated function by `@to_static`.
parameters(list[Tensor]|None): All trainable parameters included in the program. Default None.
constraints(list[tuple[str, int|None, int|None]]): A list to specify the constraints of the program. Default None.
Returns:
Layer: A Layer object that run all ops internally in static graph mode.
"""
HOOKED_RUN_IMPL = None
def __init__(
self,
main_program,
inputs,
outputs,
parameters=None,
*,
constraints=None,
**kwargs,
):
super().__init__()
self._inputs = NestSequence(inputs)
self._outputs = NestSequence(outputs)
# Avoid mutable default argument pitfall (new list per instance)
self._constraints = constraints if constraints is not None else []
self._params, self._param_values = (
parameters if parameters is not None else ([], [])
)
self._build_strategy = kwargs.get('build_strategy', BuildStrategy())
assert isinstance(self._build_strategy, BuildStrategy)
self._origin_main_program = self._verify_program(
main_program, self._outputs
)
if parameters is not None:
parameters[0][:] = self._params
parameters[1][:] = self._param_values
# Set default mode to train
self.training = True
self._program_extra_info = {}
amp_dtype, custom_white_list, custom_black_list = None, None, None
tracer = framework._dygraph_tracer()
if tracer:
custom_white_list, custom_black_list = tracer._get_amp_op_list()
amp_dtype = tracer._amp_dtype
if amp_dtype is not None and amp_dtype in ['float16', 'bfloat16']:
# For AMP training
self._amp_list = (
paddle.static.amp.fp16_lists.AutoMixedPrecisionLists(
custom_white_list=custom_white_list,
custom_black_list=custom_black_list,
dtype=amp_dtype,
)
)
# program_id -> list(scope)
self._scope_cache = {}
self._hookers = []
self._backend = kwargs.get('backend', Backend.PHI)
self._grad_var_names = {}
self._compile_time_counter = TimeCounter()
self._prog_attrs_map_cache = {}
@staticmethod
def run_impl(partial_program_layer, inputs, parameters, attrs):
prog_attrs, cuda_graph_attrs = attrs
scope_cache_key = paddle.base.core.calc_scope_cache_key(
paddle.base.core.get_program_id_from_attrs(prog_attrs),
inputs,
cuda_graph_attrs["cuda_graph_state"] != CUDAGraphState.DISABLE,
cuda_graph_attrs["cuda_graph_dispatch_key"],
)
return _C_ops.run_program(
PartialProgramLayer._valid_vars(inputs),
PartialProgramLayer._valid_vars(parameters),
partial_program_layer._create_scope_vec(
cache_key=scope_cache_key,
use_scope_cache=True,
),
prog_attrs,
cuda_graph_attrs,
)
def __call__(self, inputs):
"""
Execute static graph by Interpreter and Return dynamic Tensors.
"""
attrs = self._prepare_attributes(in_sot_mode=False)
inputs = self._prepare_inputs(inputs)
out = self.call_run_impl_with_hook(
inputs,
self._params,
attrs,
)
restored_nest_out = self._restore_out(out)
return self._remove_no_value(restored_nest_out)
@event_register("sot call partial_program")
def sot_call(self, inputs):
"""
In sot, inputs and outputs of partial program only contain tensors, so we can skip some step to speed up
"""
attrs = self._prepare_attributes(in_sot_mode=True)
out = self.call_run_impl_with_hook(
inputs,
self._params,
attrs,
)
return self._outputs.quick_restore(out)
def call_run_impl_with_hook(
self,
inputs,
parameters,
attrs,
):
if PartialProgramLayer.HOOKED_RUN_IMPL is None:
return PartialProgramLayer.run_impl.__get__(self)(
inputs,
parameters,
attrs,
)
else:
return PartialProgramLayer.HOOKED_RUN_IMPL(
PartialProgramLayer.run_impl.__get__(self),
inputs,
parameters,
attrs,
)
@cached_property
def origin_runnable_program(self) -> RunnableProgram:
inputs = list(self._inputs.var_list)
outputs = list(self._outputs.var_list)
# NOTE(SigureMo): Record original stop gradient for output values to avoid
# losing during optimization passes.
out_stop_gradients = [v.stop_gradient for v in outputs]
params = self._param_values
paddle.base.libpaddle.pir.append_shadow_outputs(
self._origin_main_program,
outputs,
len(self._origin_main_program.global_block().ops),
"output_",
)
return RunnableProgram(
self._origin_main_program,
(inputs, params, outputs),
out_stop_gradients,
)
def add_hooker(self, hooker):
self._hookers.append(hooker)
def _get_scope(self, cache_key=None, use_scope_cache=False):
if not use_scope_cache:
return core.Scope()
if cache_key not in self._scope_cache:
self._scope_cache[cache_key] = []
cached_scopes = self._scope_cache[cache_key]
for scope in cached_scopes:
if scope._can_reused:
return scope
scope = core.Scope()
cached_scopes.append(scope)
return scope
# whole
@switch_to_static_graph
def _create_program(self, is_infer_mode=False) -> RunnableProgram:
if is_infer_mode:
def pass_fn(forward_program, backward_program, program_name_attr):
# if-else pass
if self._backend.is_cinn():
apply_general_passes(
forward_program,
enable_cse=cse_is_enabled(),
enable_delete_assert_op=self._backend.is_cinn(),
)
paddle.base.libpaddle.pir.bind_symbolic_constraints(
forward_program, self._constraints
)
paddle.base.libpaddle.pir.apply_cinn_pass(forward_program)
elif self._backend.is_pcc():
paddle.base.libpaddle.pir.bind_symbolic_constraints(
forward_program, self._constraints
)
paddle.base.libpaddle.pir.apply_pcc_pass(forward_program)
else:
apply_general_passes(
forward_program,
enable_cse=cse_is_enabled(),
enable_delete_assert_op=self._backend.is_cinn(),
)
paddle.base.libpaddle.pir.check_infer_symbolic_if_need(
forward_program
)
return forward_program, backward_program
# TODO(xiongkun) who to transfer the pruning program?
infer_program = self.origin_runnable_program.clone()
if auto_layout_is_enabled() and self._backend.is_cinn():
pm = paddle.pir.PassManager(2)
pm.add_pass("auto_layout_pass", {})
pm.run(infer_program.program)
for hooker in self._hookers:
hooker.after_infer(infer_program)
infer_program.apply_pir_program_pass(pass_fn)
return infer_program
else:
train_program: RunnableProgram = (
self.origin_runnable_program.clone()
)
train_program.apply_dist_pass_for_origin_program()
# Author(liujinnan): auto_layout_pass should be applied to the original_program, before append backward. So we put it here.
if auto_layout_is_enabled() and self._backend.is_cinn():
pm = paddle.pir.PassManager(2)
pm.add_pass("auto_layout_pass", {})
pm.run(train_program.program)
train_program = self._append_backward(train_program)
# Note: Only set grad type once after initializing train program. So we put it here.
self._set_grad_type(self._params, train_program)
def pass_fn(forward_program, backward_program, program_name_attr):
def init_backward_program_shape_analysis(
forward_program, backward_program
):
forward_shape_analysis = paddle.base.libpaddle.pir.get_shape_constraint_ir_analysis(
forward_program
)
backward_shape_analysis = paddle.base.libpaddle.pir.get_shape_constraint_ir_analysis(
backward_program
)
backward_shape_analysis.register_symbol_cstr_from_shape_analysis(
forward_shape_analysis
)
forward_name_value_map = {
name: item
for item in forward_program.list_vars()
for name in item._names
}
def share_symbol_shape_from_forward_to_backward(
forward_value, backward_value
):
backward_shape_analysis.set_shape_or_data_for_var(
backward_value,
forward_shape_analysis.get_shape_or_data_for_var(
forward_value
),
)
def get_kwargs_forward_matched_value(kw_name, kw_value):
if kw_name in program_name_attr['bo_g']:
idx = program_name_attr['bo_g'].index(kw_name)
return forward_name_value_map[
program_name_attr['fo'][idx]
]
elif kw_name in forward_name_value_map:
return forward_name_value_map[kw_name]
else:
raise Exception(f"kw_args: {kw_name} not found")
for [kw_name, kw_value] in (
backward_program.global_block().kwargs().items()
):
forward_matched_value = (
get_kwargs_forward_matched_value(kw_name, kw_value)
)
share_symbol_shape_from_forward_to_backward(
forward_matched_value, kw_value
)
apply_general_passes(
forward_program,
enable_cse=cse_is_enabled(),
enable_delete_assert_op=self._backend.is_cinn(),
)
apply_general_passes(
backward_program,
enable_cse=cse_is_enabled(),
enable_delete_assert_op=self._backend.is_cinn(),
)
if self._backend.is_cinn():
paddle.base.libpaddle.pir.bind_symbolic_constraints(
forward_program, self._constraints
)
paddle.base.libpaddle.pir.apply_cinn_pass(forward_program)
init_backward_program_shape_analysis(
forward_program, backward_program
)
paddle.base.libpaddle.pir.apply_cinn_pass(backward_program)
elif self._backend.is_pcc():
paddle.base.libpaddle.pir.bind_symbolic_constraints(
forward_program, self._constraints
)
paddle.base.libpaddle.pir.apply_pcc_pass(forward_program)
else:
paddle.base.libpaddle.pir.check_infer_symbolic_if_need(
forward_program
)
return forward_program, backward_program
train_program.apply_pir_program_pass(pass_fn)
return train_program
@cached_property
def _train_program_id(self):
program_id = paddle.utils._hash_with_id(self.train_program, self)
return program_id
@cached_property
def _infer_program_id(self):
return paddle.utils._hash_with_id(self.infer_program, self)
@property
def program(self) -> RunnableProgram:
"""
Return current train or eval program.
"""
if self.training:
return self.train_program
else:
return self.infer_program
@property
def program_id(self):
"""
Return current train or eval program hash id.
"""
if self.training:
return self._train_program_id
else:
return self._infer_program_id
@cached_property
def train_program(self) -> RunnableProgram:
with backend_guard(self._backend), self._compile_time_counter.record():
return self._create_program()
@cached_property
def infer_program(self) -> RunnableProgram:
with backend_guard(self._backend), self._compile_time_counter.record():
return self._create_program(is_infer_mode=True)
def _verify_program(self, main_program, outputs):
"""
Verify that the program parameter is initialized, prune some unused params,
and remove redundant op callstack.
"""
# Check all params from main program can be found in self._params
self._check_params_all_inited(main_program)
return main_program
@switch_to_static_graph
def _append_backward(
self, train_runnable_program: RunnableProgram
) -> RunnableProgram:
program = train_runnable_program.program
targets = train_runnable_program.out_values
# TODO(@zhuoge): refine the interface, use runnable_program to apply passes.
for hooker in self._hookers:
program, targets = hooker.before_append_backward(program, targets)
inputs = train_runnable_program.x_values
params = train_runnable_program.param_values
combined_inputs = list(itertools.chain(inputs, params))
forward_end_idx = len(program.global_block().ops)
forward_end_op = None
if forward_end_idx > 0:
forward_end_op = program.global_block().ops[-1]
grad_info_map = [None] * len(combined_inputs)
with backend_guard(self._backend):
check_type(
targets,
'targets',
(Value, list, tuple),
'paddle.static.gradients',
)
with ir_static.program_guard(program, None):
# create outputs_grad for backward to avoid full and full_like op.
forward_outputs_grads = []
for out_value in targets:
if out_value.stop_gradient is True:
forward_outputs_grads.append(fake_value())
else:
value = paddle.full_like(
out_value,
fill_value=1.0,
dtype=out_value.dtype,
)
forward_outputs_grads.append(value)
paddle.base.libpaddle.pir.append_shadow_outputs(
program,
forward_outputs_grads,
len(program.global_block().ops),
"grad_input_",
)
op_between_forward_and_backward = (
len(program.global_block().ops) - forward_end_idx
)
# call grad to get backward ops.
if (
len(
list(
filter(lambda x: x.stop_gradient is False, targets)
)
)
> 0
):
grad_info_map = grad(
inputs=combined_inputs,
outputs=list(
filter(lambda x: x.stop_gradient is False, targets)
),
grad_outputs=list(
filter(
lambda x: not is_fake_value(x),
forward_outputs_grads,
)
),
)
if forward_end_op is not None:
for idx, op in enumerate(program.global_block().ops):
if op == forward_end_op:
forward_end_idx = idx + 1
break
for hooker in self._hookers:
(
program,
forward_end_idx,
targets,
) = hooker.after_append_backward(
program,
combined_inputs,
targets,
forward_outputs_grads,
forward_end_idx,
forward_end_idx + op_between_forward_and_backward,
)
mapping_value = lambda x: x if isinstance(x, Value) else fake_value()
inputs_size = len(inputs)
x_grad_value = list(map(mapping_value, grad_info_map[0:inputs_size]))
p_grad_value = list(map(mapping_value, grad_info_map[inputs_size:]))
o_grad_value = list(map(mapping_value, forward_outputs_grads))
# insert grads name for RunnableProgram (we need name for grad_inputs and grad_outputs)
input_grads_to_append = list(
filter(lambda x: not is_fake_value(x), o_grad_value)
)
output_grads_to_append = list(
filter(lambda x: not is_fake_value(x), x_grad_value + p_grad_value)
)
backward_end_op_index = len(program.global_block().ops)
paddle.base.libpaddle.pir.append_shadow_outputs(
program,
output_grads_to_append,
backward_end_op_index,
"grad_output_",
)
backward_start_op_index = (
forward_end_idx + op_between_forward_and_backward
)
# construct a runnable program.
full_graph_pre_process_pass = FullGraphPreProcessPass(
[inputs, params, targets, x_grad_value, p_grad_value, o_grad_value],
self._backend.is_cinn(),
)
forward_index_pass = IndicesPreservePass(
[forward_end_idx, backward_start_op_index, backward_end_op_index],
full_graph_pre_process_pass,
)
program = forward_index_pass(program)
(
inputs,
params,
targets,
x_grad_value,
p_grad_value,
o_grad_value,
) = full_graph_pre_process_pass.values
(
forward_end_idx,
backward_start_op_index,
backward_end_op_index,
) = forward_index_pass.new_indices
whole_program = RunnableProgram(
program,
(inputs, params, targets),
train_runnable_program.out_stop_gradients,
(x_grad_value, p_grad_value, o_grad_value),
(0, forward_end_idx),
(backward_start_op_index, backward_end_op_index),
)
whole_program.apply_dist_pass_for_whole_program()
return whole_program
def _prepare_attributes(self, in_sot_mode=False):
prog_attr_key = (self.program_id, self.training, in_sot_mode)
if prog_attr_key not in self._prog_attrs_map_cache:
prog_attrs = {
'forward_program': self.program.forward_program,
'backward_program': self.program.backward_program,
'is_test': not self.training,
'program_id': self.program_id,
'in_sot_mode': in_sot_mode,
} | self.program.program_attr
self._prog_attrs_map_cache[prog_attr_key] = (
paddle.base.core.construct_program_attribute_map(prog_attrs)
)
cuda_graph_attrs = {
'cuda_graph_state': CUDAGraphState.DISABLE, # default value for not use cuda graph
'cuda_graph_dispatch_key': 0, # default value for not use cuda graph
}
return self._prog_attrs_map_cache[prog_attr_key], cuda_graph_attrs
def _prepare_inputs(self, inputs):
"""
Prepare inputs, outputs, attrs.
"""
assert isinstance(inputs, (tuple, list))
# Flatten inputs with nested structure into single list.
flatten_inputs = paddle.utils.flatten(inputs)
# Convert variable into Tensor and feed in training data.
input_vars = []
expected_place = framework._current_expected_place()
for i, value in enumerate(flatten_inputs):
if isinstance(value, np.ndarray):
var = None
var = core.eager.Tensor(
value=value,
persistable=False,
place=expected_place,
zero_copy=True,
)
elif isinstance(value, core.eager.Tensor):
# NOTE(Aurelius84): If var is on CPUPlace, it will be transformed multi times
# into CUDAPlace when it's as input of multi Ops. so we move it in advance to avoid this problem.
if (
value.stop_gradient
and not value.place._equals(expected_place)
and not use_specialized_device()
and not maybe_dynamic_shape_tensor(value)
):
var = value._copy_to(expected_place, False)
var.stop_gradient = True
else:
var = value
else:
continue
input_vars.append(var)
return input_vars
def _create_scope_vec(self, cache_key=None, use_scope_cache=False):
inner_scope = self._get_scope(
cache_key=cache_key, use_scope_cache=use_scope_cache
)
return [inner_scope]
def _restore_out(self, out_vars):
"""
Restores same nested outputs by only replacing the Variable with Tensor.
"""
outs = self._outputs.restore(out_vars)
if outs is not None and len(outs) == 1:
outs = outs[0]
return outs
@switch_to_static_graph
def _clone_for_test(self, main_program):
return main_program.clone(for_test=True)
def _is_no_value(self, var):
if isinstance(var, core.eager.Tensor) and var.shape == [1]:
# NOTE: .numpy() will insert MemcpySync operation, it hits performance.
if var.numpy()[0] == RETURN_NO_VALUE_MAGIC_NUM:
return True
return False
def _remove_no_value(self, out_vars):
"""
Removes invalid value for various-length return statement
"""
if isinstance(out_vars, core.eager.Tensor):
if self._is_no_value(out_vars):
return None
return out_vars
elif isinstance(out_vars, (tuple, list)):
if isinstance(out_vars, tuple):
res = tuple(
var for var in out_vars if not self._is_no_value(var)
)
else:
res = [var for var in out_vars if not self._is_no_value(var)]
has_removed = len(out_vars) > len(res)
# len(out_vars) > len(res) means we have removed var. This is
# preventing out_vars is empty or just one element at the beginning
if len(res) == 0 and has_removed:
return None
elif len(res) == 1 and has_removed:
return res[0]
return res
return out_vars
def _set_grad_type(self, params, train_program: RunnableProgram):
# NOTE: if user set sparse gradient mode, the param's gradient
# will be SelectedRows, not DenseTensor. But tracer will just
# set param grad Tensor by forward Tensor(DenseTensor)
# If we don't change grad_var type here, RunProgramOp need
# transform SelectedRows to DenseTensor forcibly, it may not
# be user wanted result.
forward_params_grads = train_program.param_grad_values
train_program = train_program.program
for param, value in zip(params, forward_params_grads):
if is_fake_value(value):
continue
if value.is_selected_row_type():
param._set_grad_type(
paddle.base.core.VarDesc.VarType.SELECTED_ROWS
)
elif value.is_dense_tensor_type():
param._set_grad_type(
paddle.base.core.VarDesc.VarType.DENSE_TENSOR
)
else:
raise NotImplementedError(
"only support selected_row and dense_tensor grad type."
)
def _check_params_all_inited(self, main_program):
"""
Check all params from main program are already initialized, see details as follows:
1. all parameters in self._params should be type `framework.EagerParamBase` which are created in dygraph.
2. all parameters from transformed program can be found in self._params.
Because they share same data with EagerParamBase of original dygraph.
"""
if not isinstance(self._params, (list, tuple)):
raise TypeError(
f"Type of self._params in PartialProgramLayer should be list or tuple, but received {type(self._params)}."
)
param_and_buffer_names_set = set()
for i, var in enumerate(self._params):
# self._params contains parameters and buffers with persistable=True.
if not isinstance(var, core.eager.Tensor):
raise TypeError(
f'Type of self._params[{i}] in PartialProgramLayer should be Parameter or Variable, but received {type(var)}.'
)
param_and_buffer_names_set.add(var.name)
@staticmethod
def _valid_vars(vars):
return vars if vars else None
@contextmanager
def replace_run_impl_guard(new_run_impl):
"""
A context manager to temporarily replace the run_impl of PartialProgramLayer.
This is used for testing purposes.
"""
old_run_impl = PartialProgramLayer.HOOKED_RUN_IMPL
PartialProgramLayer.HOOKED_RUN_IMPL = new_run_impl
try:
yield
finally:
PartialProgramLayer.HOOKED_RUN_IMPL = old_run_impl
def partial_program_from(
concrete_program: ConcreteProgram, from_method: bool = False
) -> PartialProgramLayer:
inputs = concrete_program.inputs
# NOTE(SigureMo): Remove the first arg `self` from method args.
if inputs and from_method:
inputs = inputs[1:]
return PartialProgramLayer(
concrete_program.main_program,
inputs,
concrete_program.outputs,
concrete_program.parameters,
constraints=concrete_program.constraints,
**concrete_program.kwargs,
)