# Copyright (c) 2021 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 collections import defaultdict import numpy as np # (TODO: GhostScreaming) It will be removed later. from paddle.base import core from paddle.distributed import fleet from paddle.framework import Block, Program, in_dynamic_mode class HybridParallelInferenceHelper: """ A helper class to split program for inference with hybrid parallelism. Args: startup_program (Program): the startup program. main_program (Program): the main program. num_mp (int): number of model parallel degree. Default ``1``. num_pp (int): number of pipeline parallel degree. Default ``1``. micro_batch_size (int): number of micro batch size. Default ``1``. beam_size (int): number of beam search size. Default ``1``. init_comm (bool): whether if initialize communication group. Default ``True``. role_maker (RoleMakerBase or subclass): user custom define RoleMakerBase. If ``role_maker==None``, then use PaddleCloudRoleMaker. Default ``None``. Returns: None. Write Paradigm: .. code-block:: text :name: text-example1 >>> # doctest: +REQUIRES(env:DISTRIBUTED, env:GPU) >>> import paddle >>> # while op pattern >>> with paddle.base.device_guard(f'{device}:all'): ... # init global cond ... max_len = paddle.full(shape=[1], dtype="int64", fill_value=10) ... step_idx = paddle.full(shape=[1], dtype="int64", fill_value=0) ... cond_int = paddle.full(shape=[1], dtype="int64", fill_value=0, name="cond_int") ... cond = layers.cast(step_idx < max_len, dtype="bool") ... while_op = layers.While(cond, is_test=True) ... # init global lod_tensor_array for generation task ... arr = paddle.tensor.array_write(data, step_idx) >>> with while_op.block(): ... with paddle.base.device_guard(f'{device}:all'): ... # read data from global lod_tensor_array ... element_in_arr = paddle.tensor.array_read(array=arr, i=step_idx) ... # write placeholder data to global lod_tensor_array, ... # it need for send_v2 of lod_tensor_array ... paddle.increment(x=step_idx, value=1.0) ... paddle.tensor.array_write(element_in_arr, i=step_idx, array=arr) ... with paddle.base.device_guard(f'{device}:0'): ... pass # some code ... with paddle.base.device_guard(f'{device}:1'): ... pass # some code ... with paddle.base.device_guard(f'{device}:{num_pp - 1}'): ... # generate some data in while block and write to global lod_tensor_array ... # that they are read in next while step. ... # we will using send_v2 to send global lod_tensor_array to other pipeline and sync ... paddle.tensor.array_write(other_var, i=step_idx, array=arr) ... # update cond and assign to cond_int, we will sync cond_int ... layers.assign(layers.cast(cond, dtype="int32"), cond_int) ... with paddle.base.device_guard(f'{model._device}:all'): ... # the code below must at end of while block and exists in device:all ... layers.assign(layers.cast(cond_int, dtype='bool'), cond) >>> with paddle.base.device_guard(f'{model._device}:all'): ... # use a empty lod_tensor_array to clear lod_tensor_array ... layers.assign(layers.create_array(data.dtype), arr) Examples: .. code-block:: pycon :name: code-example1 >>> # doctest: +REQUIRES(env:DISTRIBUTED, env:GPU) >>> import os >>> import numpy as np >>> import paddle >>> import paddle.distributed.fleet as fleet >>> from paddle.distributed.fleet.utils import hybrid_parallel_inference >>> paddle.enable_static() >>> nranks = int(os.getenv("PADDLE_TRAINERS_NUM", 1)) >>> rank = int(os.getenv("PADDLE_TRAINER_ID", 0)) >>> dev_id = int(os.getenv("FLAGS_selected_gpus", 0)) >>> main_program = paddle.static.Program() >>> startup_program = paddle.static.Program() >>> if nranks > 1: ... dist_strategy = fleet.DistributedStrategy() ... dist_strategy.without_graph_optimization = True ... fleet.init(is_collective=True, strategy=dist_strategy) >>> device = "gpu" >>> with paddle.static.program_guard(main_program, startup_program): ... with paddle.base.device_guard(f'{device}:0'): ... X = paddle.static.data(name='X', shape=[None, 2], dtype='float32') ... with paddle.base.device_guard(f'{device}:all'): ... max_len = paddle.full(shape=[1], dtype="int64", fill_value=5, name="n") ... step_idx = paddle.full(shape=[1], dtype="int64", fill_value=0, name="i") ... data = paddle.tensor.array_write(X, step_idx) ... cond_int = paddle.full(shape=[1], dtype="int64", fill_value=0, name="cond_int") ... cond = paddle.less_than(x=step_idx, y=max_len) ... while_op = paddle.static.nn.control_flow.While(cond, is_test=True) ... with while_op.block(): ... with paddle.base.device_guard(f'{device}:all'): ... input = paddle.tensor.array_read(array=data, i=step_idx) ... paddle.increment(x=step_idx, value=1.0) ... paddle.tensor.array_write(input, i=step_idx, array=data) ... with paddle.base.device_guard(f'{device}:0'): ... param_attr = paddle.ParamAttr(initializer=paddle.nn.initializer.Constant(1.0)) ... weight1 = paddle.static.create_parameter(shape=[2, 5], dtype='float32', attr=param_attr, is_bias=False) ... hidden1 = paddle.matmul(input, weight1) ... with paddle.base.device_guard(f'{device}:1'): ... param_attr = paddle.ParamAttr(initializer=paddle.nn.initializer.Constant(2.0)) ... weight2 = paddle.static.create_parameter(shape=[5, 2], dtype='float32', attr=param_attr, is_bias=False) ... hidden2 = paddle.matmul(hidden1, weight2) ... paddle.tensor.array_write(hidden2, i=step_idx, array=data) ... # update cond and assign to cond_int, we will sync cond_int ... paddle.assign(paddle.less_than(x=step_idx, y=max_len), cond) ... paddle.assign(paddle.cast(cond, dtype="int32"), cond_int) ... with paddle.base.device_guard(f'{device}:all'): ... # the code below must at end of while block and exists in device:all ... paddle.assign(paddle.cast(cond_int, dtype='bool'), cond) ... with paddle.base.device_guard(f'{device}:all'): ... out = paddle.tensor.create_array(data.dtype) ... paddle.assign(data, out) ... with paddle.base.device_guard(f'{device}:all'): ... # use a empty lod_tensor_array to clear lod_tensor_array ... paddle.assign(paddle.tensor.create_array(data.dtype), data) >>> helper = hybrid_parallel_inference.HybridParallelInferenceHelper( ... startup_program, ... main_program, ... micro_batch_size=2, ... num_pp=2, ... init_comm=nranks > 1, ... ) >>> helper.gen_infer_program(['array_write_0.out'], ['cond_int.tmp_0']) >>> exe = paddle.static.Executor(paddle.CUDAPlace(dev_id)) >>> exe.run(startup_program) >>> np.random.seed(2333) >>> for step in range(5): ... init_data = np.random.uniform(low=0.0, high=1.0, size=[2, 2]).astype('float32') ... [res] = exe.run(main_program, feed={"X": init_data}, fetch_list=[out]) ... print('-------- step', step, ' --------') ... print(res) """ def __init__( self, startup_program, main_program, num_mp=1, num_pp=1, micro_batch_size=1, beam_size=1, init_comm=True, role_maker=None, ): assert isinstance(startup_program, Program) assert isinstance(main_program, Program) self._device = None if core.is_compiled_with_cuda(): self._device = "gpu" assert self._device, "Only gpu are supported." assert not in_dynamic_mode(), "Only static graph mode is supported." op_maker = core.op_proto_and_checker_maker self._op_role = op_maker.OpRole self._op_role_key = op_maker.kOpRoleAttrName() self._op_device_key = op_maker.kOpDeviceAttrName() self._param_device_map = {} self._pipeline_pair = [] self._pipeline_pair_in_while = [] self._pp_ring_map = {} self.ring_id = 20 # Just a magic number self.micro_batch_size = micro_batch_size self.beam_size = beam_size self.init_comm = init_comm self._output_var_to_op = None self._input_var_to_op = None self._main_program = main_program self._startup_program = startup_program if role_maker is None: self.role_maker = fleet.base.role_maker.PaddleCloudRoleMaker( is_collective=True ) else: if isinstance(role_maker, fleet.base.role_maker.RoleMakerBase): assert role_maker._is_collective self.role_maker = role_maker # communication_group info self.mp_ring_id = 0 self.global_ring_id = 1 self.endpoints = self.role_maker._get_trainer_endpoints() self.current_endpoint = self.endpoints[self.role_maker._worker_index()] self.rank = self.role_maker._worker_index() self.nranks = self.role_maker._worker_num() assert num_mp * num_pp == self.nranks self.num_pp = num_pp self.num_mp = num_mp # global ring info self.global_endpoints = self.endpoints self.global_rank = self.rank self.global_nranks = self.nranks arr = np.arange(0, self.num_pp * self.num_mp).reshape( [self.num_pp, self.num_mp] ) ipp, imp = np.where(arr == self.rank) ipp = ipp[0] imp = imp[0] self.mp_group = arr[ipp, :] self.pp_group = arr[:, imp] self._stage = ipp def _init_communication_group(self): dev_ids = [] for pair in self._pipeline_pair: prev_id, cur_id = pair if prev_id not in dev_ids: dev_ids.append(prev_id) if cur_id not in dev_ids: dev_ids.append(cur_id) num_pp = len(dev_ids) num_pp = max(1, num_pp) assert num_pp == self.num_pp, ( f'num_pp: {num_pp}, self.num_pp: {self.num_pp}' ) collective_helper = fleet.meta_optimizers.common.CollectiveHelper( self.role_maker, wait_port=False ) # Create global rings collective_helper._init_communicator( self._startup_program, self.current_endpoint, self.global_endpoints, self.global_rank, self.global_ring_id, True, self.global_ring_id, True, ) # Create mp rings if self.num_mp > 1: mp_endpoints = [self.endpoints[mp_idx] for mp_idx in self.mp_group] mp_rank = next( idx for idx, mp_idx in enumerate(self.mp_group) if mp_idx == self.rank ) collective_helper._init_communicator( self._startup_program, self.current_endpoint, mp_endpoints, mp_rank, self.mp_ring_id, True, self.global_ring_id, True, ) # Create pipeline rings if self.num_pp > 1: for pair in self._pipeline_pair: pair_key = pair[0] * 1000 + pair[1] ring_id = self._pp_ring_map[pair_key] first_node = self.pp_group[pair[0]] second_node = self.pp_group[pair[1]] if self.rank != first_node and self.rank != second_node: collective_helper._init_communicator( self._startup_program, None, None, None, None, False, self.global_ring_id, True, ) continue pipeline_endpoints = [ self.endpoints[first_node], self.endpoints[second_node], ] pipeline_rank = 0 if self.rank == first_node else 1 collective_helper._init_communicator( self._startup_program, self.current_endpoint, pipeline_endpoints, pipeline_rank, ring_id, False, self.global_ring_id, True, ) def _get_input_output_info(self, block): ''' Get info of op input and output. ''' # A map from output var to op which generate it. output_var_to_op = defaultdict(list) # A map from var to op which takes it as input. input_var_to_op = defaultdict(list) for index, op in enumerate(block.ops): for var_name in op.input_arg_names: input_var_to_op[var_name].append([op, index]) for var_name in op.output_arg_names: output_var_to_op[var_name].append([op, index]) return output_var_to_op, input_var_to_op def _update_param_device_map(self): """ Get the device info for parameters. """ params = [param.name for param in self._main_program.all_parameters()] for each_block in self._main_program.blocks: for op in each_block.ops: for var_name in op.input_arg_names: if ( var_name not in params or var_name in self._param_device_map ): continue device = op.attr(self._op_device_key) self._param_device_map[var_name] = device def _split_program(self, program, stage, block_idx): """ Split a program and get the one with the given pipeline stage. Args: stage (int): pipeline stage block_idx (int): block index Returns: used_var_names (set): used var names in block_idx block """ used_var_names = set() block = program.block(block_idx) op_idx = 0 for op in list(block.ops): op_stage = op.attr(self._op_device_key).split(':')[1] # Copy ops whose op_device set to "gpu:all" to all sections. if op_stage == "all" or int(op_stage) == stage: op_idx += 1 if op.type == "while": sub_block_id = int(op.attr('sub_block').id) sub_used_var_names = self._split_program( program, stage, sub_block_id ) used_var_names.update(sub_used_var_names) input_idxs = [] input_arg_names = op.input("X") for i, name in enumerate(input_arg_names): if name not in sub_used_var_names: input_idxs.append(i) if len(input_idxs) > 0: for i in reversed(input_idxs): input_arg_names.pop(i) op.desc.set_input("X", input_arg_names) output_idxs = [] output_arg_names = op.output("Out") for i, name in enumerate(output_arg_names): if name not in sub_used_var_names: output_idxs.append(i) if len(output_idxs) > 0: for i in reversed(output_idxs): output_arg_names.pop(i) op.desc.set_output("Out", output_arg_names) for var_name in op.input_arg_names + op.output_arg_names: used_var_names.add(var_name) else: block._remove_op(op_idx) for var_name in list(block.vars.keys()): if var_name not in used_var_names: block._remove_var(var_name) return used_var_names # def _find_post_op(self, index, var_name): # """ # Find the post op that has variable named var_name as input. # """ # # bugfix for uniform hybrid parallelism # if '.cast_fp32' in var_name: # var_name = var_name.replace('.cast_fp32', '') # if '.cast_fp16' in var_name: # var_name = var_name.replace('.cast_fp16', '') # post_ops = self._input_var_to_op[var_name] # if post_ops == None: return None # result_op = None # for post_op, post_idx in reversed(post_ops): # if post_idx > index: # result_op = post_op # break # return result_op def _find_prev_op(self, index, var_name): """ Find the previous op of op with index that outputs variable named var_name. """ prev_ops = self._output_var_to_op[var_name] if prev_ops is None: return None result_op = None for prev_op, prev_idx in reversed(prev_ops): if prev_idx < index: result_op = prev_op break return result_op def _add_op_device_attr(self, block): """ Add op_device attribute for ops in block that have not that attribute set. Args: block (Block): the block to process. """ assert isinstance(block, Block) # Ops should be copied to all pipeline stages. device_all_ops = [ "create_py_reader", "read", "create_double_buffer_reader", "while", ] for op in block.ops: if op.type in device_all_ops: # We use "gpu:all" to represent an op should be put on all # pipeline stages, such as read ops. Note that: "gpu:all" # is only used by pipeline as an indicator. op._set_attr(self._op_device_key, self._device + ":all") if op.type == "while": sub_block_id = op.attr('sub_block').id sub_block = block.program.block(sub_block_id) self._add_op_device_attr(sub_block) def _check_validation(self, block): """ Check whether ops in a block have both the op_device and the op_role attributes set. """ assert isinstance(block, Block) pre_stage_id = None for op in block.ops: assert op.has_attr(self._op_role_key), ( f"{op.type} has no {self._op_role_key} set ." ) op_role = op.attr(self._op_role_key) assert op_role == int(self._op_role.Forward), ( "Only forward is supported for inference." ) if not op._has_kernel(op.type): assert op.type in [ "while", "conditional_block", ], "The only supported op without kernel is while." sub_block_id = op.attr('sub_block').id sub_block = block.program.block(sub_block_id) self._check_validation(sub_block) assert op.has_attr(self._op_device_key), ( f"{op.type} has no {self._op_device_key} set." ) device = op.attr(self._op_device_key) assert device, f"{op.type} has no {self._op_device_key} set." if device.split(':')[1] == "all": continue dev_type = device.split(':')[0] assert dev_type == self._device stage_id = int(device.split(':')[1]) pre_stage_id = stage_id def _insert_sendrecv_ops_for_boundaries(self, block, is_while_block): """ Insert a pair of send and recv ops for every two consecutive ops on different devices. """ # A map from var to device where op takes it as input, # avoiding multiple send and recv ops. input_var_to_device = {} extra_index_info = { 'index': 0, } for index, op in enumerate(list(block.ops)): cur_device = op.attr(self._op_device_key) if cur_device.split(':')[-1] == "all": continue for var_name in op.input_arg_names: if not block.has_var(var_name) and block._find_var_recursive( var_name ): continue var = block.var(var_name) # skip data var if var.is_data: continue prev_device = None generate_ops = self._output_var_to_op.get(var_name) if generate_ops is None: if var_name not in self._param_device_map: continue prev_device = self._param_device_map[var_name] prev_op = self._find_prev_op(index, var_name) if not prev_device: prev_device = ( prev_op.attr(self._op_device_key) if prev_op else None ) if prev_device is None or prev_device.split(":")[-1] == "all": continue if prev_device == cur_device: continue if var_name not in input_var_to_device: input_var_to_device[var_name] = [] if (cur_device, prev_device) in input_var_to_device[var_name]: continue assert self._device == cur_device.split(':')[0], ( "More than one device type found." ) device_type = cur_device.split(':')[0] + ':' def _insert_send_recv(cur_id, prev_id): assert cur_id > prev_id cur_dev = device_type + str(cur_id) prev_dev = device_type + str(prev_id) if (cur_dev, prev_dev) in input_var_to_device[var_name]: return if cur_id - prev_id > 1: _insert_send_recv(cur_id - 1, prev_id) _insert_send_recv(cur_id, cur_id - 1) input_var_to_device[var_name].append( (cur_dev, prev_dev) ) return assert cur_id - prev_id == 1 input_var_to_device[var_name].append((cur_dev, prev_dev)) op_role = op.attr(self._op_role_key) var = block.vars[var_name] pair = (prev_id, cur_id) if ( is_while_block and pair not in self._pipeline_pair_in_while ): self._pipeline_pair_in_while.append(pair) # 1000 is just a magic number pair_key = prev_id * 1000 + cur_id if pair not in self._pipeline_pair: self._pipeline_pair.append(pair) self._pp_ring_map[pair_key] = self.ring_id ring_id = self.ring_id self.ring_id += 1 else: ring_id = self._pp_ring_map[pair_key] block._insert_op_without_sync( index=index + extra_index_info['index'], type='send_v2', inputs={'X': var}, attrs={ self._op_device_key: prev_dev, self._op_role_key: op_role, 'use_calc_stream': True, 'peer': 1, 'ring_id': ring_id, }, ) extra_index_info['index'] += 1 var_shape = list(var.shape) if var_shape[0] < 0: if is_while_block: var_shape[0] = ( self.micro_batch_size * self.beam_size ) else: var_shape[0] = self.micro_batch_size block._insert_op_without_sync( index=index + extra_index_info['index'], type='recv_v2', outputs={'Out': [var]}, attrs={ 'out_shape': var_shape, 'dtype': var.dtype, self._op_device_key: cur_dev, self._op_role_key: op_role, 'use_calc_stream': True, 'peer': 0, 'ring_id': ring_id, }, ) extra_index_info['index'] += 1 _insert_send_recv( int(cur_device.split(':')[1]), int(prev_device.split(':')[1]), ) block._sync_with_cpp() def _insert_sendrecv_ops_in_while_block( self, block, sync_in_while_lastpp2firstpp_var_names, sync_in_while_var_names, stage, ): dev_ids = [] for pair in self._pipeline_pair_in_while: prev_id, cur_id = pair if prev_id not in dev_ids: dev_ids.append(prev_id) if cur_id not in dev_ids: dev_ids.append(cur_id) if len(dev_ids) == 0: return first_id = min(dev_ids) last_id = max(dev_ids) assert len(block.ops) > 2, ( "It must have more than 2 ops in while sub block, " "layers.assign(layers.cast(cond_int, dtype='bool'), cond) must at end of while block, " "because nccl cannot send bool dtype var" ) index = len(block.ops) - 2 for prev_id in dev_ids: if prev_id == cur_id: continue assert cur_id > prev_id pair = (prev_id, cur_id) # 1000 is just a magic number pair_key = prev_id * 1000 + cur_id if pair not in self._pipeline_pair: self._pipeline_pair.append(pair) self._pp_ring_map[pair_key] = self.ring_id ring_id = self.ring_id self.ring_id += 1 else: ring_id = self._pp_ring_map[pair_key] if cur_id == last_id and prev_id == first_id: var_names = ( sync_in_while_lastpp2firstpp_var_names + sync_in_while_var_names ) else: var_names = sync_in_while_var_names for var_name in var_names: var = block._var_recursive(var_name) if stage == cur_id: block._insert_op_without_sync( index=index, type='send_v2', inputs={'X': var}, attrs={ self._op_device_key: self._device + ':' + str(cur_id), self._op_role_key: int(self._op_role.Forward), 'use_calc_stream': True, 'peer': 0, 'ring_id': ring_id, }, ) else: var_shape = list(var.shape) print(var_name) if len(var.shape) > 0: var_shape[0] = ( self.micro_batch_size if var_shape[0] < 0 else var_shape[0] ) block._insert_op_without_sync( index=index, type='recv_v2', outputs={'Out': [var]}, attrs={ 'out_shape': var_shape, 'dtype': var.dtype, self._op_device_key: self._device + ':' + str(prev_id), self._op_role_key: int(self._op_role.Forward), 'use_calc_stream': True, 'peer': 1, 'ring_id': ring_id, }, ) index += 1 block._sync_with_cpp() def _get_while_block(self): """ Get the while sub-block. """ main_block = self._main_program.global_block() num_while = 0 sub_block_id = None for op in main_block.ops: assert num_while < 2, "More than one while op found." if op.type == 'while': sub_block_id = op.attr('sub_block').id num_while += 1 if sub_block_id: return op, self._main_program.block(sub_block_id) return None, None def gen_infer_program( self, sync_in_while_lastpp2firstpp_var_names=None, sync_in_while_var_names=None, debug=False, ): """ Generate inference program. Params: sync_in_while_lastpp2firstpp_var_names (list(str)): the vars in the last pipeline that need to send var to first pipeline and exclude bool dtype var sync_in_while_var_names (list(str)): the vars sync among all pipeline in while block e.g cond. Note that cond cannot be bool dtype. debug (bool): the flag indicate debug """ main_block = self._main_program.global_block() startup_block = self._startup_program.global_block() if debug: with open('main_program.txt', 'w') as f: f.write(str(self._main_program)) with open('startup_program.txt', 'w') as f: f.write(str(self._startup_program)) # step1: add op_device attribute for all ops self._add_op_device_attr(startup_block) self._check_validation(startup_block) self._add_op_device_attr(main_block) self._check_validation(main_block) # step2: add send/recv ops self._update_param_device_map() # step2.1: add send/recv for main_block out_var_to_op, in_var_to_op = self._get_input_output_info(main_block) self._output_var_to_op = out_var_to_op self._input_var_to_op = in_var_to_op self._insert_sendrecv_ops_for_boundaries(main_block, False) # step2.2: add send/recv for while_block while_op, while_block = self._get_while_block() if while_block: out_var_to_op, in_var_to_op = self._get_input_output_info( while_block ) self._output_var_to_op = out_var_to_op self._input_var_to_op = in_var_to_op self._insert_sendrecv_ops_for_boundaries(while_block, True) self._insert_sendrecv_ops_in_while_block( while_block, sync_in_while_lastpp2firstpp_var_names, sync_in_while_var_names, self._stage, ) # step3: split programs self._split_program(self._startup_program, self._stage, 0) self._split_program(self._main_program, self._stage, 0) if debug: with open(f'main_program.txt.{self.rank}', 'w') as f: f.write(str(self._main_program)) with open(f'startup_program.txt.{self.rank}', 'w') as f: f.write(str(self._startup_program)) if self.init_comm: self._init_communication_group()