// Copyright (c) 2025 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. #include "paddle/cinn/optim/realize_composite_reduce_pass.h" #include "paddle/cinn/hlir/pe/reduction.h" #include "paddle/cinn/ir/ir_mutator.h" #include "paddle/cinn/ir/ir_printer.h" #include "paddle/cinn/ir/stmt_visitors.h" #include "paddle/cinn/ir/utils/ir_copy.h" #include "paddle/phi/core/enforce.h" namespace cinn { namespace optim { using ir::stmt::Alloc; using ir::stmt::BlockRef; using ir::stmt::Evaluate; using ir::stmt::For; using ir::stmt::Free; using ir::stmt::IfThenElse; using ir::stmt::Let; using ir::stmt::Schedule; using ir::stmt::StmtRef; using ir::stmt::Store; using ReduceType = ir::Reduce::ReduceType; namespace { ReduceType GetReduceType(const ir::Expr& expr) { if (auto it = expr.As()) { if (it->name == hlir::pe::kVarianceFuncName) { return ReduceType::kVariance; } else if (it->name == hlir::pe::kArgmaxFuncName) { return ReduceType::kArgmax; } else if (it->name == hlir::pe::kArgminFuncName) { return ReduceType::kArgmin; } } return ReduceType::kNone; } struct CompositeTypes : public std::vector { ReduceType type = ReduceType::kNone; explicit CompositeTypes(ReduceType _type = ReduceType::kNone) : type(_type) { this->reserve(2); } bool operator==(const CompositeTypes& other) const { if (this->type != other.type || other.size() != this->size()) return false; for (size_t i = 0; i < other.size(); i++) { if (this->at(i) != other.at(i)) return false; } return true; } void Print() const { VLOG(4) << "[CompositeTypes]: " << static_cast(this->type); for (auto _t : *this) { VLOG(4) << _t; } } }; CompositeTypes GetArgReduceUnderlyingType(const ir::Expr& expr) { if (auto it = expr.As()) { if (it->name == hlir::pe::kArgmaxFuncName || it->name == hlir::pe::kArgminFuncName) { // for cinn_argxxx func, the arg1 is the argidx // we need to check the type of the input auto argidx_call = it->read_args[1].As(); if (argidx_call != nullptr && argidx_call->name.find("argidx_") == 0) { CompositeTypes comp_types(it->name == hlir::pe::kArgminFuncName ? ReduceType::kArgmin : ReduceType::kArgmax); comp_types.push_back(argidx_call->read_args[0]->type()); comp_types.push_back(expr->type()); return comp_types; } } else if (it->name == hlir::pe::kVarianceFuncName) { return CompositeTypes(ReduceType::kVariance); } } return CompositeTypes(); } void SetInitValue(Store store_stmt, common::Type new_type, const CompositeTypes& comp_type, std::string prefix = "") { // prefix: if target is x86, we can not call constructor for POD struct // the intrinsic function for creating struct is usually "create_" + typename ir::Expr init_value = store_stmt->value(); auto call_op = init_value.As(); // if the type is already a call if (call_op != nullptr) { call_op->set_type(new_type); if (call_op->name.find("argidx_") == 0 || call_op->name.find("welford_") == 0) { call_op->name = prefix + call_op->name; } return; } if (comp_type.type == ReduceType::kVariance) { store_stmt->set_value(ir::Call::Make(new_type, prefix + new_type.customized_type(), {init_value, init_value, init_value}, {}, ir::CallType::Intrinsic)); } else if (comp_type.type == ReduceType::kArgmax || comp_type.type == ReduceType::kArgmin) { ir::Expr index_init = ir::Expr(0); index_init->set_type(common::Int(32)); if (comp_type.at(1).is_int(64)) { index_init->set_type(common::Int(64)); } store_stmt->set_value(ir::Call::Make(new_type, prefix + new_type.customized_type(), {init_value, index_init}, {}, ir::CallType::Intrinsic)); } else { PADDLE_THROW(::common::errors::Unimplemented( "reduce_type '%d' not allowed.", static_cast(comp_type.type))); } } /** * This function resolves undefined argidx type, for example: * \code * spatial inner loop (argidx type defined) * tensor_0[...] = cinn_argmax(tensor_1[...], argidx_f32_i64(tensor_2[...], * index)) * * follow up cross thread reduce (argidx type undefined) * tensor_3[...] = cinn_argmax(tensor_4[...], tensor_5[...]) * \endcode * In the above undefined case, we can not extract value type, since both * tensors (4 and 5) in the arguments will be of index type, which lefts the * argidx type undefined. So this function basically checks whether tensor_5 is * in the typed_buffers map. Since cross thread reduction usually follows * spatial inner loop reduction, so normally, tensor_0 and tensor_5 will * normally be the same in one reduce block. and since tensor_0's type is * defined, we can use it to resolve tensor_5 (and thus, the undefined * tensor_3)'s type. */ std::map ResolveUndefinedArgIdxType( std::map&& typed_buffers, std::vector&& stores) { for (const auto& store_stmt : stores) { if (auto call_stmt = store_stmt->value().As()) { if (call_stmt->name != hlir::pe::kArgmaxFuncName && call_stmt->name != hlir::pe::kArgminFuncName) continue; auto load_stmt = call_stmt->read_args[1].As(); PADDLE_ENFORCE_NOT_NULL(load_stmt, ::common::errors::PreconditionNotMet( "Non-spatial inner loop arg reduce func call " "second argument must be load.")); auto it = typed_buffers.find(load_stmt->tensor.as_tensor()->buffer); PADDLE_ENFORCE_NE(it, typed_buffers.end(), ::common::errors::PreconditionNotMet( "Referenced buffer '%s' should be defined.", load_stmt->tensor.as_tensor()->buffer->name)); auto composite_type = it->second; typed_buffers.emplace(store_stmt->tensor().as_tensor()->buffer, composite_type); } } return typed_buffers; } std::map CollectTypedReduceBuffers( const BlockRef& body, std::vector* arg_stores) { std::map typed_buffers; const auto VisitFn = [&](const StmtRef& stmt) { if (!stmt.isa()) return; Store store_stmt = stmt.as(); if (GetReduceType(store_stmt->value()) != ReduceType::kNone) { auto it = typed_buffers.find(store_stmt->tensor().as_tensor()->buffer); if (it == typed_buffers.end()) { auto composite_type = GetArgReduceUnderlyingType(store_stmt->value()); if (composite_type.type == ReduceType::kNone) { arg_stores->emplace_back(store_stmt); } else { // defined composite type can be immediately stored typed_buffers.emplace(store_stmt->tensor().as_tensor()->buffer, composite_type); } } else { // check whether we will have conflicted store types PADDLE_ENFORCE_EQ( it->second == GetArgReduceUnderlyingType(store_stmt->value()), true, ::common::errors::PreconditionNotMet( "Composite type conflict detected in the buffer map.")); } } }; ir::stmt::Visit(body, VisitFn, [](auto) {}); return typed_buffers; } void ReplaceOutputBufferX86( const BlockRef& body, const std::set& out_buffer_map, const std::map& typed_buffers) { // re-route the reduce_init buffer to the local staging buffer // and set the type for the buffers correctly struct BufferRelationRecorder { Store reduce_init; Store write_back; }; std::map buffer_relations; for (auto buffer : out_buffer_map) { buffer_relations.emplace(buffer, BufferRelationRecorder()); } const auto VisitFn = [&](const StmtRef& stmt) { if (!stmt.isa()) return; Store store_stmt = stmt.as(); auto* tensor = store_stmt->tensor().as_tensor(); auto& buffer = tensor->buffer; auto buffer_it = buffer_relations.find(buffer); // check whether the buffer is related to output args if (buffer_it == buffer_relations.end()) return; if (ir::IsReduceInitTensorName(tensor->name)) { buffer_it->second.reduce_init = store_stmt; } else { buffer_it->second.write_back = store_stmt; } }; ir::stmt::Visit(body, VisitFn, [](auto) {}); for (auto& [_, buffer_rel] : buffer_relations) { // both should be defined if (!buffer_rel.reduce_init.defined() || !buffer_rel.write_back.defined()) continue; auto wb_value = buffer_rel.write_back->value(); if (auto load_node = wb_value.As()) { auto wb_load_buffer = load_node->tensor.as_tensor()->buffer; auto wb_load_it = typed_buffers.find(wb_load_buffer); PADDLE_ENFORCE_NE(wb_load_it, typed_buffers.end(), ::common::errors::Fatal( "Buffer '%s' should be defined in typed_buffers.", wb_load_buffer->name)); // set the buffer of the reduce_init to write back buffer ir::Expr new_tensor = ir::ir_utils::IRCopy(buffer_rel.reduce_init->tensor()); new_tensor.as_tensor()->buffer = wb_load_buffer; buffer_rel.reduce_init->set_tensor(new_tensor); } } } Store GetStoreOfSchedule(const Schedule& stmt) { Store store_stmt; bool found = false; const auto VisitFn = [&](StmtRef stmt) { if (!found && stmt.isa()) { store_stmt = stmt.as(); found = true; } }; ir::stmt::Visit(stmt->body(), VisitFn, [](auto) {}); PADDLE_ENFORCE_EQ(found, true, ::common::errors::PreconditionNotMet( "One Schedule should have exactly one Store.")); return store_stmt; } Type GetCompositeReduceType(const Type& elem_type, const CompositeTypes& composite_reduce) { int type_bits = 0; std::string rtype_name = ""; if (composite_reduce.type == ReduceType::kVariance) { type_bits = elem_type.bits() * 3; rtype_name = "welford" + hlir::pe::Type2StrForReduce(elem_type); } else if (composite_reduce.type == ReduceType::kArgmax || composite_reduce.type == ReduceType::kArgmin) { PADDLE_ENFORCE_GT( composite_reduce.size(), 1, ::common::errors::InvalidArgument("CompositeTypes for arg reduce " "must have at least two types")); int max_bits = std::max(composite_reduce[0].bits(), composite_reduce[1].bits()); type_bits = max_bits * 2; rtype_name = "argidx" + hlir::pe::Type2StrForArgReduce(composite_reduce[0]) + hlir::pe::Type2StrForArgReduce(composite_reduce[1]); } else { PADDLE_THROW(::common::errors::InvalidArgument( "Unsupported composite reduce type: %d", static_cast(composite_reduce.type))); } Type customized_type(ir::Type::type_t::Customized, /* bits = */ type_bits, /* width = */ 1); customized_type.set_customized_type(rtype_name); customized_type.set_cpp_const(false); return customized_type; } struct StageReduceResultMutator : public ir::stmt::StmtMutator<> { explicit StageReduceResultMutator(ir::LoweredFunc func) : func_(func) { for (auto& arg : func->args) { if (arg.is_buffer()) arg_buffers_.insert(arg.buffer_arg()); } } void operator()(BlockRef block) { VisitBlock(block); } private: void VisitStmt(Schedule stmt) override { if (stmt->name().substr(0, 4) == "root") { ir::stmt::StmtMutator<>::VisitBlock(stmt->body()); return; } Store store_stmt = GetStoreOfSchedule(stmt.as()); auto* store_tensor = store_stmt->tensor().as_tensor(); if (GetReduceType(store_stmt->value()) == ReduceType::kNone) return; if (arg_buffers_.count(store_tensor->buffer) == 0) return; // Create the staging buffer. // We only need one element for this buffer, so its shape is {1}. const std::vector shape = {ir::Expr(1)}; const std::vector indices = {ir::Expr(0)}; ir::Tensor staging_tensor = ir::_Tensor_::Make(common::UniqName(store_tensor->name + "_local"), store_tensor->buffer->dtype, shape, shape); staging_tensor->WithBuffer("local", staging_tensor->name + "_buffer"); func_->temp_bufs.push_back(staging_tensor->buffer); // Create the staging Schedule. Schedule staging_schedule(stmt->iter_vars(), stmt->iter_values(), stmt->read_buffers(), stmt->write_buffers(), staging_tensor->name, ir::ir_utils::IRCopy(stmt->body()), stmt->attrs(), stmt->reduce_method()); sibling_stmts_.push_back(staging_schedule); // Replace all uses of the composite reduce buffer with the staging buffer. Store staging_store = GetStoreOfSchedule(staging_schedule); staging_store->set_tensor(staging_tensor); staging_store->set_indices(indices); ir::Expr staging_value = staging_store->value(); staging_value.As()->read_args[0] = ir::Load::Make(staging_tensor, indices); staging_store->set_value(staging_value); store_stmt->set_value(ir::Load::Make(staging_tensor, indices)); // Remove the reduction flags in the current Schedule, because reduction // has been done in the staging Schedule. std::vector new_iter_vars; for (auto& var : stmt->iter_vars()) { ir::Var new_var = var->Copy().as_var_ref(); new_var->is_reduce_axis = false; new_iter_vars.push_back(new_var); } stmt->set_iter_vars(new_iter_vars); } void VisitBlock(BlockRef block) override { std::vector old_stmts; old_stmts.swap(sibling_stmts_); for (StmtRef stmt : block->stmts()) { ir::stmt::StmtMutator<>::VisitStmt(stmt); sibling_stmts_.push_back(stmt); } block->set_stmts(sibling_stmts_); sibling_stmts_ = std::move(old_stmts); } void VisitStmt(For stmt) override { VisitBlock(stmt->body()); } void VisitStmt(IfThenElse stmt) override { ir::stmt::BlockRef true_case = stmt->true_case(); VisitBlock(true_case); stmt->set_true_case(true_case); if (stmt->false_case().defined()) { ir::stmt::BlockRef false_case = stmt->false_case(); VisitBlock(false_case); stmt->set_false_case(false_case); } } void VisitStmt(Let stmt) override { return; } void VisitStmt(Store stmt) override { return; } void VisitStmt(Alloc stmt) override { return; } void VisitStmt(Free stmt) override { return; } void VisitStmt(Evaluate stmt) override { return; } private: ir::LoweredFunc func_; // buffers in the function's argument list std::set arg_buffers_; // stmts at the same level with the currently visiting stmt std::vector sibling_stmts_; }; struct LoadTypeMutator : public ir::IRMutator<> { explicit LoadTypeMutator( const std::map>& buffer2type) : buffer2type_(buffer2type) {} void operator()(ir::Expr* expr) { ir::IRMutator<>::Visit(expr, expr); } private: void Visit(const ir::Load* op, ir::Expr* expr) override { ir::IRMutator<>::Visit(op, expr); auto* node = expr->As(); auto& buffer = node->tensor.as_tensor()->buffer; auto it = buffer2type_.find(buffer); if (it != buffer2type_.end()) { const auto& [buffer_type, composite_type] = it->second; ir::Type new_type = GetCompositeReduceType(buffer_type, composite_type); node->tensor.as_tensor()->set_type(new_type); buffer->dtype = new_type; *expr = ir::Cast::Make(buffer_type, *expr); } } void UncastType(ir::Expr* expr) { auto* cast_node = expr->As(); if (!cast_node) return; auto* load_node = cast_node->v().As(); if (!load_node) return; if (buffer2type_.count(load_node->tensor.as_tensor()->buffer) > 0) { *expr = cast_node->v(); } } void Visit(const ir::Call* op, ir::Expr* expr) override { // this function will cast the buffer from composite type // to an underlying type, for example welford_fp32 -> float // uncast will undo this process ir::IRMutator<>::Visit(op, expr); // By default, all tensors are casted back to their element type // before doing other computation. However, for the composite reduction // calls, we shouldn't cast the arguments back because they hold the // intermediate status. if (GetReduceType(*expr) != ReduceType::kNone) { auto* node = expr->As(); UncastType(&(node->read_args[0])); UncastType(&(node->read_args[1])); } } const std::map>& buffer2type_; }; void SetBufferType(ir::LoweredFunc func, const std::map& typed_buffers, bool is_x86_arch) { // Make a map from the buffers to their element and composite reduce types, // otherwise it's hard to know a buffer's original type. The original type // must be known to perform casting (back) in LoadTypeMutator::Visit() std::map> buffer2type; for (auto& [buffer, reduce_type] : typed_buffers) { buffer2type.emplace(buffer, std::make_pair(buffer->dtype, reduce_type)); } // Set function's temp_bufs type for (auto& buffer : func->temp_bufs) { auto it = buffer2type.find(buffer); if (it != buffer2type.end()) { const auto& [buffer_type, composite_type] = it->second; buffer->dtype = GetCompositeReduceType(buffer_type, composite_type); } } const auto VisitFn = [&](StmtRef stmt) { if (!stmt.isa()) return; Store store_stmt = stmt.as(); auto* tensor = store_stmt->tensor().as_tensor(); auto& buffer = tensor->buffer; // Set store buffer type auto it = buffer2type.find(buffer); if (it != buffer2type.end()) { ir::Expr new_tensor = ir::ir_utils::IRCopy(store_stmt->tensor()); const auto& [buffer_type, composite_type] = it->second; ir::Type new_type = GetCompositeReduceType(buffer_type, composite_type); new_tensor.as_tensor()->set_type(new_type); new_tensor.as_tensor()->buffer->dtype = new_type; store_stmt->set_tensor(new_tensor); stmt->set_type(new_type); if (ir::IsReduceInitTensorName(new_tensor.as_tensor()->name)) { std::string call_prefix = is_x86_arch ? "create_" : ""; SetInitValue(store_stmt, new_type, composite_type, call_prefix); } } // Set load buffer type ir::Expr new_value = ir::ir_utils::IRCopy(store_stmt->value()); LoadTypeMutator load_type_mutator(buffer2type); load_type_mutator(&new_value); store_stmt->set_value(new_value); }; ir::stmt::Mutate(func->body_block, VisitFn, [](auto) {}); } struct ReduceExternCallMutator : public ir::IRMutator<> { void operator()(ir::Expr* expr) { ir::IRMutator<>::Visit(expr, expr); } private: void Visit(const ir::Call* op, ir::Expr* expr) override { ir::IRMutator<>::Visit(op, expr); auto reduce_type_ = GetReduceType(*expr); if (reduce_type_ == ReduceType::kNone) return; ir::Expr lhs = op->read_args[0]; ir::Expr rhs = op->read_args[1]; if (lhs.type() != rhs.type()) { if (auto call_op = rhs.As()) { // for argidx type, avoid redundant type casting, but this is ugly if (call_op->name.find("argidx") != std::string::npos) { rhs->set_type(lhs.type()); } } else { rhs = ir::Cast::Make(lhs.type(), rhs); } } if (reduce_type_ == ReduceType::kVariance) { // replace cinn_reduce_variance to operator+ *expr = ir::Add::Make(lhs, rhs); } else if (reduce_type_ == ReduceType::kArgmax || reduce_type_ == ReduceType::kArgmin) { // replace cinn_argmxx_iyy to max or min (overloaded) if (op->name.find("argmax") != std::string::npos) { *expr = ir::Max::Make(lhs, rhs); } else { *expr = ir::Min::Make(lhs, rhs); } } } }; struct ReduceExternCallMutatorX86 : public ir::IRMutator<> { // unlike non x86 counterpart, we do not replace the call // by a arithmetic IR node, but instead call x86-exclusive funcs void operator()(ir::Expr* expr) { ir::IRMutator<>::Visit(expr, expr); } private: void Visit(const ir::Call* op, ir::Expr* expr) override { ir::IRMutator<>::Visit(op, expr); auto reduce_type_ = GetReduceType(*expr); if (reduce_type_ == ReduceType::kNone) return; ir::Expr lhs = op->read_args[0]; ir::Expr rhs = op->read_args[1]; std::string lhs_type = lhs.type().to_string(); if (lhs.type() != rhs.type()) { if (auto call_op = rhs.As()) { // for argidx type, avoid redundant type casting, but this is ugly if (call_op->name.find("argidx") == 0) { call_op->name = "create_" + call_op->name; rhs->set_type(lhs.type()); } } else { // welford pod type call create function on x86 ir::Expr m2_init(0.f), weight_init(1.f); if (lhs_type == "welford_fp64") { m2_init->set_type(common::F64()); weight_init->set_type(common::F64()); } rhs = ir::Call::Make(lhs.type(), "create_" + lhs_type, {rhs, m2_init, weight_init}, {}, ir::CallType::Intrinsic); } } std::string call_prefix = ""; switch (reduce_type_) { case ReduceType::kVariance: call_prefix = "sum_"; break; case ReduceType::kArgmax: call_prefix = "max_"; break; case ReduceType::kArgmin: call_prefix = "min_"; break; default: break; } *expr = ir::Call::Make(lhs.type(), call_prefix + lhs_type, {lhs, rhs}, {}, ir::CallType::Intrinsic); } }; void ReplaceReduceExternCall(const BlockRef& body, bool is_x86_arch = false) { const auto VisitFn = [&](StmtRef stmt) { if (!stmt.isa()) return; Store store_stmt = stmt.as(); ir::Expr new_value = ir::ir_utils::IRCopy(store_stmt->value()); if (is_x86_arch) { ReduceExternCallMutatorX86()(&new_value); } else { ReduceExternCallMutator()(&new_value); } store_stmt->set_value(new_value); }; ir::stmt::Mutate(body, VisitFn, [](auto) {}); } } // namespace LogicalResult RealizeCompositeReducePass::Run(ir::LoweredFunc func) { BlockRef body = func->body_block; // Step 1. Create a staging buffer for composite reduction result if it is // directly written to the function's argument. This is because the // result and the argument have different data types, and we need a staging // buffer to do casting properly. // Note: theoretically, we don't need this mutator if all reduction results // are explicitly written back to global memory by yield_stores. However, // current CINN frontend cannot guarantee this, so we need to do staging by // ourself if the expected yield_store is missing. StageReduceResultMutator mutator(func); mutator(body); // Step 2. Collect buffers that are used for reduce computation. std::vector arg_stores; auto typed_buffers = CollectTypedReduceBuffers(body, &arg_stores); if (typed_buffers.empty()) { // not a composite reduce func return LogicalResult::success(); } typed_buffers = ResolveUndefinedArgIdxType(std::move(typed_buffers), std::move(arg_stores)); bool is_x86_arch = false; target_.arch.Match( [&](std::variant) { /** * trace the CPU buffer for reduce init. For x86 pass, schedule pass * will not be applied, therefore, the reduce_init buffer will be the * same as the output buffer, which leads to incorrect buffer type and * op type for codegen * * (1) we first extract the buffer for each output arg * (2) find all stores to the corresponding output buffer, this op is * prior to the output type cast, for x86 IR, reduce_init and the * writing back op uses the same buffer (output tensor buffer). (3) * create a mapping. if the buffer of a store (the value of the store) * is in the typed_buffer, we try finding the reduce_init related op, * and change the the buffer and op type of the reduce_init */ is_x86_arch = true; std::set output_buffers; for (auto& arg : func->args) { if (!arg.is_output()) continue; output_buffers.emplace(arg.buffer_arg()); } ReplaceOutputBufferX86(body, output_buffers, typed_buffers); }, [&](std::variant) {}); // Step 3. Change the data type of buffers to the corresponding type. SetBufferType(func, typed_buffers, is_x86_arch); // Step 4. Replace the `cinn_reduce_variance` and `cinn_argmax` calls // in order to reuse the cross-thread/block reduction templates. ReplaceReduceExternCall(body, is_x86_arch); return LogicalResult::success(); } std::unique_ptr CreateRealizeCompositeReducePass(Target target) { return std::make_unique(target); } } // namespace optim } // namespace cinn