blob: 6902f898a6739b9ad3ac5b2e0611ba69c56d379e [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
/*!
* \file dnnl_quantized_rnn.cc
* \brief Common functions for quantized recurrent neural network
* \author Zixuan Wei
*/
#if MXNET_USE_ONEDNN == 1
#include "operator/quantization/quantization_utils.h"
#include "operator/quantization/dnnl/dnnl_quantized_rnn-inl.h"
namespace mxnet {
namespace op {
std::vector<float> GetDNNLRnnWeightsQParams(const DNNLRnnFullParam& full_param, float* w_ptr) {
const int nthreads = mxnet::engine::OpenMP::Get()->GetRecommendedOMPThreadCount();
const int num_gates = 4;
const RNNParam& default_param = full_param.default_param;
const LayerParamVector& layer_params = full_param.layer_params;
const DNNLRnnLayerParam& layer_param0 = layer_params.at(0);
const size_t w_size0 = layer_param0.single_w_size;
const size_t wx_size0 = num_gates * layer_param0.state_size * layer_param0.input_size;
const size_t wh_size0 = num_gates * layer_param0.state_size * layer_param0.state_size;
int directions = 1;
float* wx = w_ptr;
float* wh = wx + wx_size0;
float* fake_wx = wx;
float* fake_wh = wh;
std::vector<float> wx_goi_max;
std::vector<float> wh_goi_max;
if (default_param.bidirectional) {
directions = 2;
wx_goi_max.resize(wx_size0);
wh_goi_max.resize(wh_size0);
fake_wx = wx_goi_max.data();
fake_wh = wh_goi_max.data();
#pragma omp parallel for num_threads(nthreads)
for (index_t i = 0; i < static_cast<index_t>(wx_size0); ++i) {
fake_wx[i] = MaxAbs(wx[i], wx[i + w_size0]);
}
#pragma omp parallel for num_threads(nthreads)
for (index_t i = 0; i < static_cast<index_t>(wh_size0); ++i) {
fake_wh[i] = MaxAbs(wh[i], wh[i + w_size0]);
}
}
std::vector<float> w_max(num_gates * layer_param0.state_size, 0.0);
const index_t input_size = layer_param0.input_size; // input
const index_t state_size = layer_param0.state_size; // state
const index_t gates_nblks = num_gates * layer_param0.state_size; // gates * state
for (index_t go = 0; go < gates_nblks; ++go) {
float tmp_max = w_max[go];
for (index_t i = 0; i < input_size; ++i) {
tmp_max = MaxAbs(fake_wx[go * input_size + i], tmp_max);
}
for (index_t i = 0; i < state_size; ++i) {
tmp_max = MaxAbs(fake_wh[go * state_size + i], tmp_max);
}
w_max[go] = tmp_max;
}
wx += layer_param0.single_w_size * directions;
wh += layer_param0.single_w_size * directions;
std::vector<float> goi_max(wh_size0, 0.0);
for (size_t lyr = 1; lyr < layer_params.size(); ++lyr) {
const DNNLRnnLayerParam& layer_param = layer_params.at(lyr);
const int weight_nblks = layer_param.num_layer * directions;
for (int blk = 0; blk < weight_nblks; ++blk) {
#pragma omp parallel for num_threads(nthreads)
for (index_t i = 0; i < static_cast<index_t>(wh_size0); ++i) {
goi_max[i] = MaxAbs(wx[i], wh[i]);
}
for (index_t go = 0; go < gates_nblks; ++go) {
float tmp = w_max[go];
// NOTES: min/max reductions were supported since OpenMP 3.1, which was
// released in Jul 2011 (hence the version number).
#if _OPENMP >= 201107
#pragma omp parallel for reduction(max : tmp) num_threads(nthreads)
#endif
for (index_t i = 0; i < state_size; ++i) {
tmp = Max(goi_max[go * state_size + i], tmp);
}
w_max[go] = tmp;
}
}
wx += layer_param.single_w_size * directions;
wh = wx + wh_size0;
}
#pragma omp parallel for num_threads(nthreads)
for (index_t i = 0; i < static_cast<index_t>(w_max.size()); ++i) {
w_max[i] = mshadow::red::limits::MaxValue<int8_t>() / w_max[i];
}
return w_max;
}
void DNNLQuantizedRnnOp::Init(const OpContext& op_ctx,
const std::vector<NDArray>& inputs,
const std::vector<OpReqType>& req,
const std::vector<NDArray>& outputs) {
using format_tag = dnnl::memory::format_tag;
// Get the bytes of a real type
const Context& ctx = op_ctx.run_ctx.ctx;
const NDArray& weights = inputs[rnn_enum::kParams];
int dtype = weights.dtype();
int weights_dtype = weights.dtype();
size_t dtype_bytes = mshadow::mshadow_sizeof(dtype);
const RNNParam& default_param = full_param_.default_param;
const size_t weights_size =
weights.data().Size() - GetRnnBiasSize(default_param.num_layers,
default_param.state_size,
default_param.bidirectional + 1,
default_param.mode);
char* weights_ptr = static_cast<char*>(weights.data().dptr_);
char* bias_ptr = weights_ptr + weights_size * dtype_bytes;
// In the `autograd.record()` context, RNNOp is required to run into
// `forward_training` mode.
const size_t num_fusion = full_param_.layer_params.size();
if (fwd_inf_vec_.size() < num_fusion) {
size_t buffer_size = 0; // Element number, instead of bytes, in the buffer
for (auto& layer_param : full_param_.layer_params) {
buffer_size += layer_param.workspace_size + layer_param.reserve_size;
}
buffer_size += outputs[rnn_enum::kOut].data().Size() * (num_fusion - 1);
buffer_size += kDNNLAlign * num_fusion * 5; // Add margin for alignment
for (auto& layer_param : full_param_.layer_params) {
fwd_inf_vec_.emplace_back(
ctx, layer_param, false, inputs[rnn_enum::kData], inputs[rnn_enum::kParams], rnn_attr_);
buffer_size += fwd_inf_vec_.back().GetSize();
}
mgr_.Init(buffer_size, ctx);
}
for (auto& fwd_layer : fwd_inf_vec_) {
size_t single_w_bytes = fwd_layer.GetParam().single_w_size * dtype_bytes;
size_t single_b_bytes = fwd_layer.GetParam().native_single_b_size * dtype_bytes;
size_t directions = fwd_layer.GetParam().bidirectional ? 2 : 1;
size_t layer_weights_bytes = single_w_bytes * directions;
size_t layer_bias_bytes = single_b_bytes * directions; // Native MXNet has double bias
if (!fwd_layer.IsInitialized())
fwd_layer.SetWeightsMem(weights_ptr, bias_ptr, false, weights_dtype);
weights_ptr += layer_weights_bytes;
bias_ptr += layer_bias_bytes;
}
CHECK_EQ(num_fusion, fwd_inf_vec_.size())
<< "Layer vector's size has a different value than the number of fusion.";
if (dst_.size() < num_fusion - 1) {
const int data_dtype = outputs[rnn_enum::kOut].dtype();
// Here we need `fwd_inf_vec_.size() - 1` spaces for the intermediate
// results of the multiple fused layers. And for the result of the last
// fused layer, `outputs[rnn_enum::kOut]` could provide the space. Hence,
// `forward_inf_vec_.back()` is excluded when allocates the spaces for
// intermediate results.
for (std::vector<DNNLRnnForward>::const_iterator fwd = fwd_inf_vec_.begin();
fwd != fwd_inf_vec_.end() - 1;
++fwd)
dst_.push_back(
mgr_.Alloc({fwd->GetParam().dst_dims, get_dnnl_type(data_dtype), format_tag::tnc}));
}
initialized_ = true;
}
template <typename DNNLRnnX>
inline void RegisterDNNLRnn(DNNLRnnX const& rnn) {
DNNLStream::Get()->RegisterPrimArgs(rnn.GetFwd(), rnn.GetArgsMap());
}
template <>
inline void RegisterDNNLRnn(DNNLRnnBackward const& rnn) {
DNNLStream::Get()->RegisterPrimArgs(rnn.GetBwd(), rnn.GetArgsMap());
rnn.SetNativeWeightsGrads();
}
void DNNLQuantizedRnnOp::Forward(const OpContext& op_ctx,
const std::vector<NDArray>& inputs,
const std::vector<OpReqType>& req,
const std::vector<NDArray>& outputs) {
TmpMemMgr::Get()->Init(op_ctx.requested[0]);
const RNNParam& default_param = full_param_.default_param;
const uint32_t num_base_inputs = GetRnnNumInputs(default_param);
float data_scale = inputs[num_base_inputs + quantized_rnn::kDataScale].data().dptr<float>()[0];
float data_shift = inputs[num_base_inputs + quantized_rnn::kDataShift].data().dptr<float>()[0];
const bool need_reset_weight = (!dmlc::GetEnv("MXNET_RNN_USE_WEIGHT_CACHE", 0) &&
weights_ver_ != inputs[rnn_enum::kParams].version()) ?
true :
false;
const NDArray& weights = inputs.at(rnn_enum::kParams);
float* weights_ptr = weights.data().dptr<float>();
if (!initialized_ || fwd_inf_vec_.empty()) {
weights_ver_ = inputs[rnn_enum::kParams].version();
cached_data_scale_ = data_scale;
cached_data_shift_ = data_shift;
rnn_attr_->set_rnn_data_qparams(data_scale, data_shift);
if (need_reset_weight || fwd_inf_vec_.empty())
rnn_attr_->set_rnn_weights_qparams(0 + (1 << 3) + (1 << 4),
GetDNNLRnnWeightsQParams(full_param_, weights_ptr));
}
// Initialize weights version
if (!initialized_ && weights_ver_ == 0) {
weights_ver_ = inputs[rnn_enum::kParams].version();
cached_data_scale_ = data_scale;
cached_data_shift_ = data_shift;
}
if (!fwd_inf_vec_.empty() &&
((cached_data_scale_ != data_scale || cached_data_shift_ != data_shift))) {
initialized_ = false;
weights_ver_ = inputs[rnn_enum::kParams].version();
cached_data_scale_ = data_scale;
cached_data_shift_ = data_shift;
}
// Check if weights NDArray was changed. If so, reset initialized_
if (fwd_inf_vec_.size() > 0 && weights_ver_ != inputs[rnn_enum::kParams].version()) {
initialized_ = false;
for (auto& fwd : fwd_inf_vec_)
fwd.Reset();
weights_ver_ = inputs[rnn_enum::kParams].version();
cached_data_scale_ = data_scale;
cached_data_shift_ = data_shift;
}
if (!initialized_ || fwd_inf_vec_.empty()) {
Init(op_ctx, inputs, req, outputs);
}
// Get data type
int data_dtype = outputs[rnn_enum::kOut].dtype();
// Get temporary memory for output, state_out, statecell_out
const int num_layers = default_param.num_layers;
const int seq_length = default_param.seq_length_;
const int batch_size = default_param.batch_size_;
const int state_size = default_param.state_size;
const int directions = default_param.bidirectional ? 2 : 1;
dnnl::memory::desc dst_desc({seq_length, batch_size, directions * state_size},
get_dnnl_type(data_dtype),
dnnl::memory::format_tag::tnc);
dnnl::memory::desc state_desc({num_layers, directions, batch_size, state_size},
get_dnnl_type(data_dtype),
dnnl::memory::format_tag::ldnc);
auto out_mem = CreateDNNLMem(outputs[rnn_enum::kOut], dst_desc, req[rnn_enum::kOut]);
dnnl_output_t stateout_mem;
dnnl_output_t statecellout_mem;
// Get input & output NDArray
char* src = static_cast<char*>(inputs[rnn_enum::kData].data().dptr_);
char* src_state = static_cast<char*>(inputs[rnn_enum::kState].data().dptr_);
char* dst = static_cast<char*>(out_mem.second->get_data_handle());
char* dst_state = nullptr; // Output state
char* src_state_cell = nullptr; // Used in LSTM for cell state
char* dst_state_cell = nullptr; // Used in LSTM for cell state
if (default_param.state_outputs && req[rnn_enum::kStateOut] != kNullOp) {
stateout_mem =
CreateDNNLMem(outputs[rnn_enum::kStateOut], state_desc, req[rnn_enum::kStateOut]);
dst_state = static_cast<char*>(stateout_mem.second->get_data_handle());
}
if (default_param.mode == rnn_enum::kLstm) {
src_state_cell = static_cast<char*>(inputs[rnn_enum::kStateCell].data().dptr_);
if (default_param.state_outputs && req[rnn_enum::kStateCellOut] != kNullOp) {
statecellout_mem =
CreateDNNLMem(outputs[rnn_enum::kStateCellOut], state_desc, req[rnn_enum::kStateCellOut]);
dst_state_cell = static_cast<char*>(statecellout_mem.second->get_data_handle());
}
}
if (fwd_inf_vec_.size() == 1) {
fwd_inf_vec_.front().SetNewDataMem(
src, src_state, src_state_cell, dst, dst_state, dst_state_cell, data_dtype);
} else {
CHECK_EQ(fwd_inf_vec_.size(), dst_.size() + 1) << "Output memory error.";
size_t cell_bytes = (default_param.bidirectional + 1) * default_param.batch_size_ *
default_param.state_size * mshadow::mshadow_sizeof(data_dtype);
// Set input data memory for the first layer. This stores intermediate
// output results in this->xxx, used as the source input of the next layer.
fwd_inf_vec_.front().SetNewDataMem(src,
src_state,
src_state_cell,
this->dst_.front()->get_data_handle(),
dst_state,
dst_state_cell,
data_dtype);
// 1st_lyr -> dst_handle -> next_lyr -> dst_handle -> next_lyr -> ...
for (size_t lyr = 1; lyr < fwd_inf_vec_.size() - 1; ++lyr) {
src_state += cell_bytes;
if (src_state_cell)
src_state_cell += cell_bytes;
if (dst_state)
dst_state += cell_bytes;
if (dst_state_cell)
dst_state_cell += cell_bytes;
fwd_inf_vec_.at(lyr).SetNewDataMem(this->dst_.at(lyr - 1)->get_data_handle(),
src_state,
src_state_cell,
this->dst_.at(lyr)->get_data_handle(),
dst_state,
dst_state_cell,
data_dtype);
}
// Set output data memory for the last layer.
src_state += cell_bytes;
if (src_state_cell)
src_state_cell += cell_bytes;
if (dst_state)
dst_state += cell_bytes;
if (dst_state_cell)
dst_state_cell += cell_bytes;
fwd_inf_vec_.back().SetNewDataMem(this->dst_.back()->get_data_handle(),
src_state,
src_state_cell,
dst,
dst_state,
dst_state_cell,
data_dtype);
}
for (auto& inf_lyr : fwd_inf_vec_)
RegisterDNNLRnn(inf_lyr);
CommitOutput(outputs[rnn_enum::kOut], out_mem);
if (default_param.state_outputs) {
CommitOutput(outputs[rnn_enum::kStateOut], stateout_mem);
if (default_param.mode == rnn_enum::kLstm)
CommitOutput(outputs[rnn_enum::kStateCellOut], statecellout_mem);
}
DNNLStream::Get()->Submit();
}
} // namespace op
} // namespace mxnet
#endif // MXNET_USE_ONEDNN == 1