Files
paddlepaddle--paddle/python/paddle/distributed/passes/auto_parallel_recompute_pir.py
T
2026-07-13 12:40:42 +08:00

290 lines
12 KiB
Python

# Copyright (c) 2024 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.
import logging
import paddle
from paddle.base import core
OpRole = core.op_proto_and_checker_maker.OpRole
from paddle.autograd import backward_utils
from ..auto_parallel.static.utils import (
get_logger,
)
from .pass_base import PassBase, register_pass
logger = get_logger(logging.INFO)
@register_pass("auto_parallel_recompute_pir")
class AutoParallelRecomputePIRPass(PassBase):
def __init__(self):
super().__init__()
self.program_ops = []
def _check_self(self):
return True
def _check_conflict(self, other_pass):
return True
def get_fwd_bwd_ops(self):
fwd_ops = []
bwd_ops = []
for op in self.program_ops:
if op.op_role == int(OpRole.Forward):
fwd_ops.append(op)
elif op.op_role == int(OpRole.Backward):
bwd_ops.append(op)
assert len(fwd_ops) and len(bwd_ops)
return fwd_ops, bwd_ops
def get_first_bwd_used_op(self, fwd_op, bwd_ops):
# Find the first user op of the op result in backward op list.
first_op = bwd_ops[-1]
for res in fwd_op.results():
for user_op in res.all_used_ops():
if user_op in bwd_ops and first_op.id() >= user_op.id():
first_op = user_op
return first_op
def is_seed_used_by_dropout(self, seed_op):
# Ensure that the random operator has the same output in backward recompute.
if seed_op.name() != "seed":
return False
seed_value = seed_op.results()[0]
dropout_ops = ["pd_op.dropout", "pd_op.fused_dropout_add"]
return any(
True
for used_op in seed_value.all_used_ops()
if used_op.name() in dropout_ops
)
def remove_outgoing_op(self, segment):
# An OP is considered an outgoing OP if all of results' user OPs are not in segment.
# These OPs do not participate in the backward gradient computation and therefore
# do not need to have a recomputation during backward.
segment_ops = [self.program_ops[idx] for idx in segment]
segment_len = len(segment)
for idx in range(segment_len - 1, 0, -1):
op = segment_ops[idx]
user_ops = set()
for res in op.results():
user_ops = user_ops | set(res.all_used_ops())
if user_ops & set(segment_ops):
continue
segment.pop(idx)
logger.info(
f"Remove outgoing OP '{op.name()}' from the segment for recomputation, as it does not participate in the backward."
)
return segment
def get_segments(self):
# `fwd_recompute_id` indicates the ID assigned to the segment for
# which the OP requires recompute.
# A segment comprises all OPs within a program, ranging from the OP
# with the minimum index to the OP with the maximum index, and all
# these operations share the same `fwd_recompute_id`.
segment_beg = {}
segment_end = {}
max_op_id = len(self.program_ops)
for idx, op in enumerate(self.program_ops):
# 1. Find the OPs marked with `fwd_recompute_id`.
if not op.has_attr("fwd_recompute_id"):
continue
# 2. Delineate the segment range marked by `fwd_recompute_id`.
# Note: there may be some unmarked OPs in between.
rc_id = op.attrs()["fwd_recompute_id"]
if rc_id not in segment_beg:
segment_beg[rc_id] = max_op_id
segment_end[rc_id] = 0
segment_beg[rc_id] = min(segment_beg[rc_id], idx)
segment_end[rc_id] = max(segment_end[rc_id], idx)
# 3. Aggregate all segment information into a dictionary.
# The key is the id of the segment, which is used to uniquely identify each segment.
# The value is a list of indices of the segment OPs in `self.program_ops`.
segments = {}
assert len(segment_beg.keys()) == len(segment_end.keys())
for segment_id, beg_id in segment_beg.items():
assert segment_id in segment_end.keys()
end_id = segment_end[segment_id]
assert beg_id <= end_id
segment = list(range(beg_id, end_id + 1))
# 4. Remove the outgoing OPs from the segment, as these OPs
# do not participate in the backward gradient computation.
segments[segment_id] = self.remove_outgoing_op(segment)
logger.info(
f"Segment ID {segment_id} contains {len(segment)} OPs, all of which will be recomputed."
)
return segments
def get_op_name(self, op):
return op.name().split('.')[1]
def match_pattern(
self,
op,
visit,
fetch_id,
fetch_pattern,
target_pattern,
pre_len,
main_len,
count,
max_count,
):
if count >= max_count:
return max_count
if len(fetch_pattern) > len(target_pattern):
return count
if self.get_op_name(op) != target_pattern[fetch_id]:
return count
if fetch_id == len(target_pattern) - 1:
for idx in range(pre_len, pre_len + main_len):
fetch_op = fetch_pattern[idx]
visit[fetch_op] = -1
refined_segment = list(set(visit.values()))
refined_segment.sort()
refined_segment = [idx for idx in refined_segment if idx != -1]
return count + 1
for res_val in op.results():
for user_op in res_val.all_used_ops():
fetch_pattern[fetch_id + 1] = user_op
count = self.match_pattern(
op=user_op,
visit=visit,
fetch_id=fetch_id + 1,
fetch_pattern=fetch_pattern,
target_pattern=target_pattern,
pre_len=pre_len,
main_len=main_len,
count=count,
max_count=max_count,
)
return count
def _apply_single_impl(self, main_program, startup_program, context=None):
self.program_ops = list(main_program.global_block().ops)
# 1. Get the recompute segments information form program.
segments = self.get_segments()
assert len(segments) > 0, (
"No segment found in the PIR recompute pass.\n \
Please disable 'recompute.enable' or check 'recompute()' usage in model code."
)
# 2. Get the forward and backward OPs from program.
fwd_ops, bwd_ops = self.get_fwd_bwd_ops()
# 3. Refine the segments based on the patterns.
refined_ops_patterns = self.get_attr("refined_ops_patterns")
for refined_ops_pattern in refined_ops_patterns:
# 3.1 get the refined pattern information.
# refined_ops_patterns = pre_ops + main_ops + suf_ops
# `main_ops` pattern: it does not participate in backward recomputation
# and needs to be removed from the segment.
# `pre_ops` pattern: it serve only as markers and do require recomputation.
# `suf_ops` pattern: it serve only as markers and do require recomputation.
# `num` : it limits the maximum number of `main_ops` patterns identified
# within each segment. A value of -1 represents all patterns.
num = int(refined_ops_pattern['num'])
num = num if num >= 0 else len(fwd_ops)
main_ops = refined_ops_pattern['main_ops']
pre_ops = refined_ops_pattern['pre_ops']
suf_ops = refined_ops_pattern['suf_ops']
pattern_ops = pre_ops + main_ops + suf_ops
for rc_id in segments.keys():
# 3.2 Identify and mark the first 'num' patterns in each segment.
# The dictionary 'op_idx_map' has keys as OP information.
# If an OP belongs to a pattern, its value in the dictionary is marked as -1.
op_idx_map = {
self.program_ops[idx]: idx for idx in segments[rc_id]
}
pattern_count = 0
fetch_pattern = [None] * len(pattern_ops)
for idx in segments[rc_id]:
op = self.program_ops[idx]
fetch_pattern[0] = op
pattern_count = self.match_pattern(
op=self.program_ops[idx],
visit=op_idx_map,
fetch_id=0,
fetch_pattern=fetch_pattern,
target_pattern=pattern_ops,
pre_len=len(pre_ops),
main_len=len(main_ops),
count=pattern_count,
max_count=num,
)
# 3.3 Refined segment to exclude the specified pattern.
refined_segment = list(set(op_idx_map.values()))
refined_segment.sort()
refined_segment = [idx for idx in refined_segment if idx != -1]
segments[rc_id] = refined_segment
# 4. Construct the segment for backward recomputation.
# 4.1 Build IrMapping to eplace forward value with backward recompute value.
input_value = main_program.list_vars()
value_map = paddle.pir.IrMapping()
for val in input_value:
value_map.add(val, val)
for rc_id, segment in segments.items():
# 4.2 Find the insertion position for the backward segment,
# which should be before backward gradient computation.
first_bwd_used_op = bwd_ops[-1]
for idx in segment:
op = self.program_ops[idx]
bwd_used_op = self.get_first_bwd_used_op(op, bwd_ops)
if first_bwd_used_op.id() > bwd_used_op.id():
first_bwd_used_op = bwd_used_op
ori_segment_outputs = backward_utils.ValueSet()
paddle.pir.set_insertion_point(first_bwd_used_op)
# 4.3 Clone the segment OPs and replace the forward
# value with backward recompute value.
for idx in segment:
op = self.program_ops[idx]
ori_segment_outputs.update(op.results())
# Random OPs should produce the same output before and after recomputation.
if self.is_seed_used_by_dropout(op):
continue
rc_op = op.clone(
value_map, paddle.pir.CloneOptions(False, True, True)
)
# The forward segment and the backward segment have the same segment ID.
if rc_op.has_attr("fwd_recompute_id"):
rc_op.erase_attr("fwd_recompute_id")
rc_op.set_int_attr("bwd_recompute_id", rc_id)
# Updtate attributes.
if first_bwd_used_op.has_attr('op_role'):
rc_op.set_int_attr("op_role", first_bwd_used_op.op_role)
if first_bwd_used_op.has_attr('chunk_id'):
rc_op.set_int_attr("chunk_id", first_bwd_used_op.chunk_id)
# 4.4 Replace the forward value with backward recompute value.
for ori_value in ori_segment_outputs:
rc_value = value_map.look_up(ori_value)
ori_value.replace_grad_users_with(rc_value, set(bwd_ops))