// Copyright (c) 2023 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/common/errors.h" #include "paddle/phi/backends/onednn/onednn_reuse.h" #include "paddle/phi/core/compat/convert_utils.h" #include "paddle/phi/core/enforce.h" #include "paddle/phi/core/expect.h" #include "paddle/phi/core/utils/data_type.h" #include "paddle/phi/core/kernel_registry.h" namespace phi::fusion { using funcs::CreateKey; using funcs::OneDNNGetDataType; using funcs::OneDNNMemDesc; using funcs::RNNReorderType; using phi::OneDNNContext; using OneDNNMemoryFormat = dnnl::memory::format_tag; template class GRUOneDNNHandler : public funcs::OneDNNHandlerT { public: GRUOneDNNHandler(const OneDNNContext& dev_ctx, const dnnl::engine onednn_engine, phi::Place cpu_place UNUSED, const DenseTensor* input, const DenseTensor* weight_h, const DenseTensor* h0, const bool is_reverse, const float scale_data, const float shift_data, const std::string& gate_activation, const std::string& activation, const std::vector& scale_weights, const int64_t N, const int64_t Ti, const int64_t IC, const int64_t OC) : funcs::OneDNNHandlerT( dev_ctx, dev_ctx.GetEngine(), cpu_place, CreateKey(dev_ctx, dev_ctx.GetInputsName("X")[0] + dev_ctx.GetInputsName("WeightH")[0], OneDNNGetDataType(), Ti)), N(N), Ti(Ti), IC(IC), OC(OC), G(3) { std::string unique_name = dev_ctx.GetInputsName("X")[0] + dev_ctx.GetInputsName("WeightH")[0]; // Create memory key without Ti because weights, bias and h0 memories // do not depend on Ti size but primitive and input/output memory do memory_key_ = funcs::ExtendKeyWithThreadInfoIfNeeded( dev_ctx, CreateKey(dev_ctx, unique_name, OneDNNGetDataType())); // Is it int8 kernel const bool is_INT8 = std::is_same::value; if (is_INT8) { const int weights_scale_mask = 0 + (1 << 3) // bit, indicating the unique scales for `g` dim in `ldigo` + (1 << 4); // bit, indicating the unique scales for `o` dim in `ldigo` attr_.set_rnn_data_qparams(scale_data, shift_data); attr_.set_rnn_weights_qparams(weights_scale_mask, scale_weights); } if (unlikely(!this->isCached())) { // oneDNN kernel has hardcoded activation functions PADDLE_ENFORCE_EQ( gate_activation, "sigmoid", common::errors::Unimplemented( "oneDNN fusion_gru supports only sigmoid as a gate activation.")); PADDLE_ENFORCE_EQ( activation, "tanh", common::errors::Unimplemented( "oneDNN fusion_gru supports only tanh as an activation.")); // Weights for int8 kernel are of a type s8 const auto weights_dt = is_INT8 ? dnnl::memory::data_type::s8 : OneDNNGetDataType(); // oneDNN RNN dimensions const int64_t D = 1; // Directions const int64_t L = 1; // Layers (PP supports only 1 stacked layer) const int64_t G = 3; // Number of Gates, 3 for GRU // Create memory descriptors auto input_md = OneDNNMemDesc( {Ti, N, IC}, OneDNNGetDataType(), OneDNNMemoryFormat::any); auto weight_x_md = OneDNNMemDesc({L, D, IC, G, OC}, weights_dt, OneDNNMemoryFormat::any); auto weight_h_md = OneDNNMemDesc({L, D, OC, G, OC}, weights_dt, OneDNNMemoryFormat::any); auto bias_md = OneDNNMemDesc( {L, D, G, OC}, OneDNNGetDataType(), OneDNNMemoryFormat::ldgo); auto hidden_md = OneDNNMemDesc( {Ti, N, OC}, OneDNNGetDataType(), OneDNNMemoryFormat::any); auto h0_md = OneDNNMemDesc( {L, D, N, OC}, OneDNNGetDataType(), OneDNNMemoryFormat::ldnc); // Create GRU oneDNN primitive const auto direction = is_reverse ? dnnl::rnn_direction::unidirectional_right2left : dnnl::rnn_direction::unidirectional_left2right; this->AcquireForwardPrimitiveDescriptor( this->attr_, dnnl::prop_kind::forward_inference, direction, input_md, h0_md, weight_x_md, weight_h_md, bias_md, hidden_md, dnnl::memory::desc()); } } bool is_NTC() { return this->is_NTC(this->fwd_pd_->dst_desc()); } bool is_NTC(const dnnl::memory::desc& md) { auto ntc_md = dnnl::memory::desc( md.get_dims(), md.get_data_type(), dnnl::memory::format_tag::ntc); return md == ntc_md; } void reorderRNNdata(void* input_data, void* output_data, std::vector lod, const bool is_reverse, RNNReorderType reorder_type) { switch (reorder_type) { // Reorder input memory [WORDS, C] + LoD -> [N, T, C] case RNNReorderType::PP_NTC: { auto* input_data_iter = reinterpret_cast(input_data); auto* output_data_iter = reinterpret_cast(output_data); for (int n = 0; n < N; ++n) { const auto num_elements = (lod[n + 1] - lod[n]) * IC; const auto offset = is_reverse ? (Ti * IC - num_elements) : 0; memcpy(output_data_iter + n * Ti * IC + offset, input_data_iter, sizeof(T) * num_elements); input_data_iter += num_elements; } } break; // Reorder input memory [WORDS, C] + LoD -> [T, N, C] case RNNReorderType::PP_TNC: { auto* input_data_iter = reinterpret_cast(input_data); auto* output_data_iter = reinterpret_cast(output_data); for (int n = 0; n < N; ++n) { const auto num_elements = (lod[n + 1] - lod[n]); const auto offset = is_reverse ? (Ti - num_elements) : 0; for (size_t t = 0; t < num_elements; ++t) { memcpy(output_data_iter + (t + offset) * N * IC + n * IC, input_data_iter, sizeof(T) * IC); input_data_iter += IC; } } } break; // Reorder output values to PP format [N, T, C] -> [WORDS, C] case RNNReorderType::NTC_PP: { auto* input_data_iter = reinterpret_cast(input_data); auto* output_data_iter = reinterpret_cast(output_data); for (int n = 0; n < N; ++n) { const auto num_elements = (lod[n + 1] - lod[n]) * OC; const auto offset = is_reverse ? (Ti * OC - num_elements) : 0; memcpy(output_data_iter, input_data_iter + n * Ti * OC + offset, sizeof(T_out) * num_elements); output_data_iter += num_elements; } } break; // Reorder output values to PP format [T, N, C] -> [WORDS, C] case RNNReorderType::TNC_PP: { auto* input_data_iter = reinterpret_cast(input_data); auto* output_data_iter = reinterpret_cast(output_data); for (int n = 0; n < N; ++n) { const auto num_elements = lod[n + 1] - lod[n]; const auto offset = is_reverse ? (Ti - num_elements) : 0; for (size_t t = 0; t < num_elements; ++t) { memcpy(output_data_iter, input_data_iter + (t + offset) * N * OC + n * OC, sizeof(T_out) * OC); output_data_iter += OC; } } } break; } } std::shared_ptr AcquireInputMemoryWithReorder( const DenseTensor* input, const bool is_reverse) { const auto name = this->key_ + "@input_mem"; auto memory_p = std::static_pointer_cast(this->dev_ctx_.GetBlob(name)); if (!memory_p) { memory_p = std::make_shared(this->fwd_pd_->src_desc(), this->engine_); this->dev_ctx_.SetBlob(name, memory_p); } const auto& input_lod = input->lod()[0]; auto* x_data = funcs::to_void_cast(input->data()); auto* x_onednn_data = memory_p->get_data_handle(); memset(x_onednn_data, 0, sizeof(T) * N * Ti * IC); if (is_NTC(this->fwd_pd_->src_desc())) { reorderRNNdata( x_data, x_onednn_data, input_lod, is_reverse, RNNReorderType::PP_NTC); } else { reorderRNNdata( x_data, x_onednn_data, input_lod, is_reverse, RNNReorderType::PP_TNC); } return memory_p; } std::shared_ptr AcquireOutputMemory() { const auto name = this->key_ + "@output_mem"; auto memory_p = std::static_pointer_cast(this->dev_ctx_.GetBlob(name)); if (!memory_p) { memory_p = std::make_shared(this->fwd_pd_->dst_desc(), this->engine_); this->dev_ctx_.SetBlob(name, memory_p); } return memory_p; } // H0 is for now persistable template std::shared_ptr AcquireH0Memory(const DenseTensor* h0) { const std::string h0_key = memory_key_ + "@h0"; auto memory_p = std::static_pointer_cast(this->dev_ctx_.GetBlob(h0_key)); if (!memory_p) { auto user_h0_memory = dnnl::memory(); if (h0) { user_h0_memory = dnnl::memory( {{1, 1, N, OC}, OneDNNGetDataType(), OneDNNMemoryFormat::ldnc}, this->engine_, funcs::to_void_cast(h0->data())); } else { user_h0_memory = dnnl::memory( {{1, 1, N, OC}, OneDNNGetDataType(), OneDNNMemoryFormat::ldnc}, this->engine_); memset(user_h0_memory.get_data_handle(), 0, sizeof(U) * N * OC); } memory_p = std::make_shared(this->fwd_pd_->src_iter_desc(), this->engine_); auto& astream = phi::OneDNNContext::tls().get_stream(); dnnl::reorder(user_h0_memory, *memory_p, attr_) .execute(astream, user_h0_memory, *memory_p); this->dev_ctx_.SetBlob(h0_key, memory_p); } return memory_p; } template std::shared_ptr AcquireWeightXMemory( const DenseTensor* weight_x, const bool origin_mode) { const std::string wx_key = this->memory_key_ + "@weight_x"; auto memory_p = std::static_pointer_cast(this->dev_ctx_.GetBlob(wx_key)); if (!memory_p) { auto user_md = OneDNNMemDesc({1, 1, this->IC, this->G, this->OC}, OneDNNGetDataType(), OneDNNMemoryFormat::ldigo); auto user_memory = dnnl::memory(user_md, this->engine_); auto* weight_x_data = reinterpret_cast(user_memory.get_data_handle()); memcpy(weight_x_data, weight_x->data(), sizeof(U) * this->IC * this->G * this->OC); if (origin_mode == false) { for (int64_t i = 0; i < this->IC; ++i) { for (int64_t j = 0; j < this->OC; ++j) { U minus_one(-1.0f); weight_x_data[j] = minus_one * weight_x_data[j]; } weight_x_data += 3 * this->OC; } } memory_p = std::make_shared( this->fwd_pd_->weights_layer_desc(), this->engine_); auto& astream = OneDNNContext::tls().get_stream(); dnnl::reorder(user_memory, *memory_p, this->attr_) .execute(astream, user_memory, *memory_p); this->dev_ctx_.SetBlob(wx_key, memory_p); } return memory_p; } template std::shared_ptr AcquireWeightHMemory( const DenseTensor* weight_h, const bool origin_mode) { const std::string wh_key = this->memory_key_ + "@weight_h"; auto memory_p = std::static_pointer_cast(this->dev_ctx_.GetBlob(wh_key)); if (!memory_p) { auto user_md = OneDNNMemDesc({1, 1, this->OC, this->G, this->OC}, OneDNNGetDataType(), OneDNNMemoryFormat::ldigo); auto user_memory = dnnl::memory(user_md, this->engine_); // Reorder weights_h from PP format [OC, 2OC] + [OC, OC] to // oneDNN format [OC, 3OC] auto* weight_h_data = reinterpret_cast(user_memory.get_data_handle()); auto* user_weight_h_data = weight_h->data(); auto src1_iter = user_weight_h_data; auto src2_iter = user_weight_h_data + 2 * this->OC * this->OC; for (int64_t c = 0; c < this->OC; ++c) { memcpy(weight_h_data, src1_iter, 2 * this->OC * sizeof(U)); memcpy(weight_h_data + 2 * this->OC, src2_iter, this->OC * sizeof(U)); src1_iter += 2 * this->OC; src2_iter += this->OC; weight_h_data += 3 * this->OC; } weight_h_data = reinterpret_cast(user_memory.get_data_handle()); if (origin_mode == false) { for (int64_t i = 0; i < this->OC; ++i) { for (int64_t j = 0; j < this->OC; ++j) { U minus_one(-1.0f); weight_h_data[j] = minus_one * weight_h_data[j]; } weight_h_data += 3 * this->OC; } } memory_p = std::make_shared( this->fwd_pd_->weights_iter_desc(), this->engine_); auto& astream = OneDNNContext::tls().get_stream(); dnnl::reorder(user_memory, *memory_p, this->attr_) .execute(astream, user_memory, *memory_p); this->dev_ctx_.SetBlob(wh_key, memory_p); } return memory_p; } std::shared_ptr AcquireBiasMemory(const DenseTensor* bias, const bool origin_mode) { const std::string bias_key = this->memory_key_ + "@bias"; auto memory_p = std::static_pointer_cast( this->dev_ctx_.GetBlob(bias_key)); if (!memory_p) { memory_p = std::make_shared(this->fwd_pd_->bias_desc(), this->engine_); auto* bias_data = reinterpret_cast(memory_p->get_data_handle()); if (bias) { const float* user_bias_data = bias->data(); // Bias in oneDNN is always float memcpy(bias_data, user_bias_data, sizeof(float) * this->G * this->OC); } else { // oneDNN always need bias memory, if it's not provided in PP, let // oneDNN allocate memory and set it to 0 memset(bias_data, 0, sizeof(float) * this->G * this->OC); } if (origin_mode == false && bias) { for (int64_t i = 0; i < this->OC; ++i) { bias_data[i] *= -1; } } this->dev_ctx_.SetBlob(bias_key, memory_p); } return memory_p; } protected: // RNN dimensions // N - Batch Size // Ti - Max sentence length // IC - Input Channels // OC - Output Channels // G - Number of gates const int64_t N, Ti, IC, OC, G; // Memory size of weights, bias and h0 does not depend // on Ti size, thus we need another key to cache them std::string memory_key_; dnnl::primitive_attr attr_; }; template void RunKernel(const phi::OneDNNContext& dev_ctx, const DenseTensor& x, const optional& h0, const DenseTensor& weight_x, const DenseTensor& weight_h, const optional& bias, const std::string& activation, const std::string& gate_activation, const bool is_reverse, const bool use_seq, const bool origin_mode, const float scale_data, const float shift_data, const std::vector& scale_weights, DenseTensor* reordered_h0, DenseTensor* xx, DenseTensor* batched_input, DenseTensor* batched_out, DenseTensor* hidden) { const auto& onednn_engine = dev_ctx.GetEngine(); auto x_dims = x.dims(); auto x_mat_dims = (x_dims.size() == 3 && x_dims[1] == 1) ? common::flatten_to_2d(x_dims, 1) : x_dims; // Get tensor dimensions const auto x_mat_dims_vec = vectorize(x_mat_dims); const auto weight_h_dims = vectorize(weight_h.dims()); const auto& input_lod = x.lod()[0]; // Calculate RNN dimensions const int64_t N = static_cast(input_lod.size() - 1); // Number of sentences (batches) const int64_t Ti = // Max length of the sentence in a batch [&input_lod]() { size_t res = 0; for (size_t i = 0; i < (input_lod.size() - 1); ++i) { res = std::max(res, input_lod[i + 1] - input_lod[i]); } return res; }(); const int64_t IC = x_mat_dims_vec[1]; // Input channels const int64_t OC = weight_h_dims[0]; // Output channels GRUOneDNNHandler handler(dev_ctx, onednn_engine, dev_ctx.GetPlace(), &x, &weight_h, h0.get_ptr(), is_reverse, scale_data, shift_data, gate_activation, activation, scale_weights, N, Ti, IC, OC); auto input_memory_p = handler.AcquireInputMemoryWithReorder(&x, is_reverse); std::shared_ptr h0_memory_p, weight_h_memory_p, weight_x_memory_p; if (weight_h.dtype() == DataType::FLOAT32) { h0_memory_p = handler.template AcquireH0Memory(h0.get_ptr()); weight_x_memory_p = handler.template AcquireWeightXMemory(&weight_x, origin_mode); weight_h_memory_p = handler.template AcquireWeightHMemory(&weight_h, origin_mode); } else if (weight_h.dtype() == DataType::BFLOAT16) { h0_memory_p = handler.template AcquireH0Memory(h0.get_ptr()); weight_x_memory_p = handler.template AcquireWeightXMemory( &weight_x, origin_mode); weight_h_memory_p = handler.template AcquireWeightHMemory( &weight_h, origin_mode); } else { h0_memory_p = handler.template AcquireH0Memory(h0.get_ptr()); weight_x_memory_p = handler.template AcquireWeightXMemory(&weight_x, origin_mode); weight_h_memory_p = handler.template AcquireWeightHMemory(&weight_h, origin_mode); } auto bias_memory_p = handler.AcquireBiasMemory(bias.get_ptr(), origin_mode); auto hidden_onednn_memory_p = handler.AcquireOutputMemory(); std::unordered_map gru_args = { {DNNL_ARG_SRC_LAYER, *input_memory_p}, {DNNL_ARG_SRC_ITER, *h0_memory_p}, {DNNL_ARG_WEIGHTS_LAYER, *weight_x_memory_p}, {DNNL_ARG_WEIGHTS_ITER, *weight_h_memory_p}, {DNNL_ARG_BIAS, *bias_memory_p}, {DNNL_ARG_DST_LAYER, *hidden_onednn_memory_p}}; auto gru_forward_p = handler.AcquireForwardPrimitive(); auto& astream = OneDNNContext::tls().get_stream(); gru_forward_p->execute(astream, gru_args); astream.wait(); auto* hidden_onednn_data = hidden_onednn_memory_p->get_data_handle(); auto* hidden_tmp_data = dev_ctx.template Alloc(hidden); auto* hidden_data = funcs::to_void_cast(hidden_tmp_data); if (handler.is_NTC()) { handler.reorderRNNdata(hidden_onednn_data, hidden_data, input_lod, is_reverse, RNNReorderType::NTC_PP); } else { handler.reorderRNNdata(hidden_onednn_data, hidden_data, input_lod, is_reverse, RNNReorderType::TNC_PP); } } template void FusionGRUKernel(const Context& dev_ctx, const DenseTensor& x, const optional& h0, const DenseTensor& weight_x, const DenseTensor& weight_h, const optional& bias, const std::string& activation, const std::string& gate_activation, const bool is_reverse, const bool use_seq, const bool origin_mode, const bool force_fp32_output, DenseTensor* reordered_h0, DenseTensor* xx, DenseTensor* batched_input, DenseTensor* batched_out, DenseTensor* hidden) { const std::string mkldnn_data_type = dev_ctx.HasDnnAttr("mkldnn_data_type") ? PADDLE_GET_CONST(std::string, dev_ctx.GetDnnAttr("mkldnn_data_type")) : "float32"; const std::string onednn_data_type = (dev_ctx.HasDnnAttr("onednn_data_type") && PADDLE_GET_CONST(std::string, dev_ctx.GetDnnAttr("onednn_data_type")) != "") ? PADDLE_GET_CONST(std::string, dev_ctx.GetDnnAttr("onednn_data_type")) : mkldnn_data_type; std::vector onednn_data_type_list = { "float32", "int8", "bfloat16"}; PADDLE_ENFORCE_EQ(std::find(onednn_data_type_list.begin(), onednn_data_type_list.end(), onednn_data_type) != onednn_data_type_list.end(), true, common::errors::InvalidArgument( "The onednn_data_type should be [float32, " "int8, bfloat16], but found %s.", onednn_data_type.c_str())); const float scale_data = dev_ctx.HasDnnAttr("Scale_data") ? PADDLE_GET_CONST(float, dev_ctx.GetDnnAttr("Scale_data")) : 1.0f; const float shift_data = dev_ctx.HasDnnAttr("Shift_data") ? PADDLE_GET_CONST(float, dev_ctx.GetDnnAttr("Shift_data")) : 1.0f; std::vector tmp_scale_weights = {1.0f}; const std::vector scale_weights = dev_ctx.HasDnnAttr("Scale_weights") ? PADDLE_GET_CONST(std::vector, dev_ctx.GetDnnAttr("Scale_weights")) : tmp_scale_weights; const bool is_bf16 = std::is_same::value; // BF16 does not support force output if (!is_bf16 && force_fp32_output) { // NOLINT RunKernel(dev_ctx, x, h0, weight_x, weight_h, bias, activation, gate_activation, is_reverse, use_seq, origin_mode, scale_data, shift_data, scale_weights, reordered_h0, xx, batched_input, batched_out, hidden); } else { RunKernel(dev_ctx, x, h0, weight_x, weight_h, bias, activation, gate_activation, is_reverse, use_seq, origin_mode, scale_data, shift_data, scale_weights, reordered_h0, xx, batched_input, batched_out, hidden); } } } // namespace phi::fusion PD_REGISTER_KERNEL(fusion_gru, OneDNN, ONEDNN, phi::fusion::FusionGRUKernel, float, phi::bfloat16, uint8_t) {}