// Copyright (c) 2024 CINN 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. #include "paddle/cinn/optim/replace_cross_block_reduction.h" #include #include "paddle/cinn/adt/adt.h" #include "paddle/cinn/common/common.h" #include "paddle/cinn/hlir/pe/reduction.h" #include "paddle/cinn/ir/ir.h" #include "paddle/cinn/ir/ir_mutator.h" #include "paddle/cinn/ir/schedule/ir_schedule_util.h" #include "paddle/cinn/lang/compute.h" namespace cinn { namespace optim { namespace { ir::Expr CalcBufferSizeInBytes(const ir::Buffer& buffer) { const ir::Expr numel = buffer->SymbolicNumel(); return optim::ArithSimplify(numel * buffer->dtype.bytes()); } std::unordered_set GetReduceVarNames( const ir::ScheduleBlockRealize* block_realize) { const ir::ScheduleBlock* schedule_block = block_realize->schedule_block.As(); const std::vector& iter_values = block_realize->iter_values; const std::vector& iter_vars = schedule_block->iter_vars; std::unordered_set reduce_var_names; for (int i = 0; i < iter_values.size(); ++i) { if (!iter_vars[i]->is_reduce_axis) { continue; } ir::ir_utils::CollectIRNodesWithoutTensor( iter_values[i], [&](const ir::Expr* x) { if (x->as_var()) { reduce_var_names.insert(x->as_var()->name); } return false; }); } return reduce_var_names; } ir::Expr GetRightOperand(const ir::Expr& expr) { #define GET_RIGHT_OPERAND(OpT) \ if (expr.As()) { \ return expr.As()->b(); \ } GET_RIGHT_OPERAND(ir::Add); GET_RIGHT_OPERAND(ir::Mul); GET_RIGHT_OPERAND(ir::Max); GET_RIGHT_OPERAND(ir::Min); GET_RIGHT_OPERAND(ir::And); GET_RIGHT_OPERAND(ir::Or); #undef GET_RIGHT_OPERAND PADDLE_THROW( ::common::errors::InvalidArgument("Not a supported reduce op: %s", expr)); } struct BaseMutator : public ir::IRMutator<> { using ir::IRMutator<>::Visit; void operator()(ir::LoweredFunc fn) { Visit(fn.As()); } protected: bool IsGridReduce(const ir::ScheduleBlockRealize* block_realize) { if (cur_loops_.empty()) { return false; } auto* innermost_loop = cur_loops_.back(); if (!innermost_loop->is_gpu_block_binded()) { return false; } const std::unordered_set reduce_var_names = GetReduceVarNames(block_realize); return reduce_var_names.count(innermost_loop->loop_var->name) > 0; } void Visit(const ir::For* expr, ir::Expr* op) override { cur_loops_.push_back(expr); IRMutator::Visit(expr, op); cur_loops_.pop_back(); } void Visit(ir::Expr* expr) { IRMutator::Visit(expr, expr); } protected: std::vector cur_loops_; }; struct CrossBlockReductionReplacer : public BaseMutator { private: void InsertTempSpaceToFuncArgs(ir::_LoweredFunc_* func_node, const ir::Buffer& buffer, bool need_zero_init) { // insert the temp space after the last tensor argument and before the // first scalar argument auto insert_pos = std::find_if(func_node->args.begin(), func_node->args.end(), [](const ir::Argument& arg) { return arg.is_var(); }); int arg_idx = std::distance(func_node->args.begin(), insert_pos); func_node->temp_spaces.emplace_back( CalcBufferSizeInBytes(buffer), arg_idx, need_zero_init); ir::Argument temp_space_arg(buffer, ir::Argument::IO::kOutput); func_node->args.insert(insert_pos, temp_space_arg); } void ConvertHeapBuffersToFuncArgs(ir::_LoweredFunc_* func_node) { std::vector global_bufs; std::vector local_bufs; for (auto& buf : func_node->temp_bufs) { if (buf->memory_type == ir::MemoryType::Heap) { global_bufs.push_back(buf); } else { local_bufs.push_back(buf); } } for (auto& buf : global_bufs) { InsertTempSpaceToFuncArgs(func_node, buf, false); } func_node->temp_bufs = local_bufs; } ir::Expr GetBlockBindedSpatialLoopExtend( const ir::ScheduleBlockRealize* block_realize) { const std::unordered_set reduce_var_names = GetReduceVarNames(block_realize); std::vector loop_extends; for (auto* for_node : cur_loops_) { if (reduce_var_names.count(for_node->loop_var->name) == 0 && for_node->is_gpu_block_binded()) { loop_extends.push_back(for_node->extent); } } PADDLE_ENFORCE_EQ( loop_extends.size(), 1UL, ::common::errors::PreconditionNotMet( "There should be exactly one spatial loop binded on gpu block.")); return loop_extends[0]; } ir::Expr GetThreadBindedSpatialLoopExtend( const ir::ScheduleBlockRealize* block_realize) { const std::unordered_set reduce_var_names = GetReduceVarNames(block_realize); std::vector loop_extends; for (auto* for_node : cur_loops_) { if (reduce_var_names.count(for_node->loop_var->name) == 0 && for_node->is_gpu_thread_binded()) { loop_extends.push_back(for_node->extent); } } PADDLE_ENFORCE_LE( loop_extends.size(), 1UL, ::common::errors::PreconditionNotMet( "There could be at most one spatial loop binded on gpu thread.")); if (loop_extends.empty()) { return ir::Expr(1); } return loop_extends[0]; } void ReplaceByGridReduceExternCall(const ir::ScheduleBlock* schedule_block, const ir::Expr num_spatial_threads) { ir::Expr update_stmt = schedule_block->body; if (update_stmt.As()) { PADDLE_ENFORCE_EQ( update_stmt.As()->stmts.size(), 1UL, ::common::errors::InvalidArgument( "There should be exactly one statement inside schedule_block.")); update_stmt = update_stmt.As()->stmts[0]; } PADDLE_ENFORCE_NOT_NULL( update_stmt.As(), ::common::errors::InvalidArgument( "The top-level statement in schedule_block must be a store.")); auto* store_node = update_stmt.As(); ir::Expr rvalue = GetRightOperand(store_node->value); PADDLE_ENFORCE_NOT_NULL(rvalue.As(), ::common::errors::InvalidArgument( "The rvalue of reduce is not a load.")); std::string func_name = hlir::pe::GridReduceExternalFuncName( store_node->value, store_node->tensor->type()); auto* load_node = rvalue.As(); ir::Tensor rf_tensor = load_node->tensor.as_tensor_ref(); // The load's indices are like [ blockIdx.y, ... ]. // The loaded tensor's shape is like [ gridDim.y, ... ]. ir::Expr spatial_index = [&]() { load_node->indices[0] = ir::Expr(0); return load_node->index(); }(); ir::Expr spatial_size = [&]() { load_node->indices[0] = ir::Expr(1); for (int i = 1; i < load_node->indices.size(); i++) { load_node->indices[i] = ir::Expr(0); } return load_node->index(); }(); store_node->value = lang::CallExtern(func_name, {rf_tensor, spatial_size, spatial_index}); } void Visit(ir::_LoweredFunc_* fn) override { has_grid_reduce_ = false; func_arg_buffer_names_.clear(); for (auto& arg : fn->args) { if (arg.is_buffer()) { func_arg_buffer_names_.insert(arg.buffer_arg()->name); } } IRMutator::Visit(fn); if (!has_grid_reduce_) { return; } ConvertHeapBuffersToFuncArgs(fn); } void Visit(const ir::ScheduleBlockRealize* expr, ir::Expr* op) override { const ir::ScheduleBlock* schedule_block = expr->schedule_block.As(); if (schedule_block->name.substr(0, 4) == "root") { IRMutator::Visit(expr, op); return; } if (!IsGridReduce(expr)) { return; } if (!has_grid_reduce_) { has_grid_reduce_ = true; } ir::Expr num_spatial_threads = GetThreadBindedSpatialLoopExtend(expr); ReplaceByGridReduceExternCall(schedule_block, num_spatial_threads); } void Visit(const ir::Block* block, ir::Expr* op) override { // We override the Block visitor to facilitate statement insertion. std::vector old_parent_block_stmts; old_parent_block_stmts.swap(cur_parent_block_stmts_); auto* node = op->As(); for (auto& stmt : node->stmts) { IRMutator::Visit(&stmt, &stmt); cur_parent_block_stmts_.push_back(stmt); } node->stmts = std::move(cur_parent_block_stmts_); cur_parent_block_stmts_ = std::move(old_parent_block_stmts); } private: std::vector cur_parent_block_stmts_; std::unordered_set func_arg_buffer_names_; bool has_grid_reduce_{false}; }; } // namespace void ReplaceCrossBlockReduction(ir::LoweredFunc fn) { CrossBlockReductionReplacer()(fn); } } // namespace optim } // namespace cinn