// // Copyright © 2017 Arm Ltd. All rights reserved. // SPDX-License-Identifier: MIT // #include "TfParser.hpp" #include #include #include #include #include #include #include #include #include "tensorflow/core/framework/graph.pb.h" #include #include #include #include using namespace armnnUtils; using namespace armnn; namespace armnnTfParser { namespace { const PermutationVector NHWCToArmNN = { 0, 2, 3, 1 }; const PermutationVector ArmNNToNHWC = { 0, 3, 1, 2 }; template void ReadMandatoryNodeAttributeImpl(const tensorflow::NodeDef& nodeDef, const std::string& attribName, tensorflow::AttrValue::ValueCase expectedValueCase, Callable callable) { auto iter = nodeDef.attr().find(attribName); if (iter != nodeDef.attr().end()) { const auto& attrValue = iter->second; if (attrValue.value_case() == expectedValueCase) { callable(attrValue); } else { throw ParseException( boost::str( boost::format( "Attribute %1% of node %2% expected to have %3% as tensorflow::AttrValue::ValueCase, " "but found %4% instead %5%") % attribName % nodeDef.name() % static_cast(expectedValueCase) % static_cast(attrValue.value_case()) % CHECK_LOCATION().AsString())); } } else { throw ParseException( boost::str( boost::format( "Could not find required attribute %1% in node %2% %3%") % attribName % nodeDef.name() % CHECK_LOCATION().AsString())); } } template void ReadOptionalNodeAttributeImpl(const tensorflow::NodeDef& nodeDef, const std::string& attribName, tensorflow::AttrValue::ValueCase expectedValueCase, Callable callable) { auto iter = nodeDef.attr().find(attribName); if (iter != nodeDef.attr().end()) { const auto& attrValue = iter->second; if (attrValue.value_case() == expectedValueCase) { callable(attrValue); } else { throw ParseException( boost::str( boost::format( "Attribute %1% of node %2% expected to have %3% as tensorflow::AttrValue::ValueCase, " "but found %4% instead %5%") % attribName % nodeDef.name() % static_cast(expectedValueCase) % static_cast(attrValue.value_case()) % CHECK_LOCATION().AsString())); } } } float ReadMandatoryNodeFloatAttribute(const tensorflow::NodeDef& nodeDef, const std::string& name) { float attribValue = 0.0f; ReadMandatoryNodeAttributeImpl(nodeDef, name, tensorflow::AttrValue::kF, [&attribValue](const tensorflow::AttrValue& attrValue) { attribValue = attrValue.f(); }); return attribValue; } int32_t ReadMandatoryNodeInt32Attribute(const tensorflow::NodeDef& nodeDef, const std::string& name) { int32_t attribValue = 0u; ReadMandatoryNodeAttributeImpl(nodeDef, name, tensorflow::AttrValue::kI, [&attribValue](const tensorflow::AttrValue& attrValue) { attribValue = static_cast(attrValue.i()); }); return attribValue; } bool ReadMandatoryNodeBoolAttribute(const tensorflow::NodeDef& nodeDef, const std::string& name) { bool attribValue = false; ReadMandatoryNodeAttributeImpl(nodeDef, name, tensorflow::AttrValue::kB, [&attribValue](const tensorflow::AttrValue& attrValue) { attribValue = static_cast(attrValue.b()); }); return attribValue; } uint32_t ReadMandatoryNodeUint32Attribute(const tensorflow::NodeDef& nodeDef, const std::string& name) { uint32_t attribValue = 0u; ReadMandatoryNodeAttributeImpl(nodeDef, name, tensorflow::AttrValue::kI, [&attribValue](const tensorflow::AttrValue& attrValue) { attribValue = static_cast(attrValue.i()); }); return attribValue; } std::string ReadMandatoryNodeStringAttribute(const tensorflow::NodeDef& nodeDef, const std::string& name) { std::string attribValue = ""; ReadMandatoryNodeAttributeImpl(nodeDef, name, tensorflow::AttrValue::kS, [&attribValue](const tensorflow::AttrValue& attrValue) { attribValue = attrValue.s(); }); return attribValue; } std::vector ReadMandatoryNodeUint32ListAttribute(const tensorflow::NodeDef& nodeDef, const std::string& name) { std::vector attriList; ReadMandatoryNodeAttributeImpl(nodeDef, name, tensorflow::AttrValue::kList, [&attriList](const tensorflow::AttrValue& attrValue) { for (int attriNum = 0; attriNum < attrValue.list().i_size(); ++attriNum) { attriList.push_back(static_cast(attrValue.list().i(attriNum))); } }); return attriList; } std::vector ReadOptionalNodeUint32ListAttribute(const tensorflow::NodeDef& nodeDef, const std::string& name) { std::vector attriList; ReadOptionalNodeAttributeImpl(nodeDef, name, tensorflow::AttrValue::kList, [&attriList](const tensorflow::AttrValue& attrValue) { for (int attriNum = 0; attriNum < attrValue.list().i_size(); ++attriNum) { attriList.push_back(static_cast(attrValue.list().i(attriNum))); } }); return attriList; } bool ReadOptionalNodeBoolAttribute(const tensorflow::NodeDef& nodeDef, const std::string& name, bool defaultValue = false) { bool attribValue = defaultValue; ReadOptionalNodeAttributeImpl(nodeDef, name, tensorflow::AttrValue::kB, [&attribValue](const tensorflow::AttrValue& attrValue) { attribValue = attrValue.b(); }); return attribValue; } tensorflow::DataType ReadMandatoryNodeTypeAttribute(const tensorflow::NodeDef& nodeDef, const std::string& name) { tensorflow::DataType attribValue = tensorflow::DT_INVALID; ReadMandatoryNodeAttributeImpl(nodeDef, name, tensorflow::AttrValue::kType, [&attribValue](const tensorflow::AttrValue& attrValue) { attribValue = attrValue.type(); }); return attribValue; } TensorInfo PrepareReshape(const TensorInfo& input, const std::vector& targetDims) { std::vector outDims(targetDims.begin(), targetDims.end()); const auto stretchDim = std::find(targetDims.begin(), targetDims.end(), -1); if (stretchDim != targetDims.end()) { if (std::find(std::next(stretchDim), targetDims.end(), -1) != targetDims.end()) { throw ParseException( boost::str( boost::format( "At most one component of shape can be -1 %1%") % CHECK_LOCATION().AsString())); } auto targetNumElements = boost::numeric_cast( std::accumulate(targetDims.begin(), targetDims.end(), -1, std::multiplies())); auto stretchIndex = static_cast(std::distance(targetDims.begin(), stretchDim)); outDims[stretchIndex] = input.GetNumElements() / targetNumElements; } TensorInfo reshapeInfo = input; reshapeInfo.SetShape(TensorShape{ static_cast(outDims.size()), outDims.data() }); return reshapeInfo; } // We need the input0Slot to guide the reshape for input1Slot. IOutputSlot* AddBroadcastReshapeLayer(IOutputSlot* input0Slot, IOutputSlot* input1Slot, bool isNHWC, INetwork& m_Network, const tensorflow::NodeDef& nodeDef) { const TensorInfo& input1Info = input1Slot->GetTensorInfo(); const TensorInfo inputTensorInfo = input0Slot->GetTensorInfo(); const unsigned int matchDim = inputTensorInfo.GetNumDimensions() - (isNHWC ? 1 : 3); std::array reshapedDimensions; std::fill_n(reshapedDimensions.begin(), inputTensorInfo.GetNumDimensions(), 1); reshapedDimensions[matchDim] = input1Info.GetShape()[0]; armnn::TensorInfo reshapedInfo = input1Info; reshapedInfo.SetShape(TensorShape{ inputTensorInfo.GetNumDimensions(), reshapedDimensions.data() }); const std::string reshapeLayerName = "reshape_for-" + nodeDef.name(); ReshapeDescriptor reshapeDesc; reshapeDesc.m_TargetShape = reshapedInfo.GetShape(); IConnectableLayer* const reshapeLayer = m_Network.AddReshapeLayer(reshapeDesc, reshapeLayerName.c_str()); input1Slot->Connect(reshapeLayer->GetInputSlot(0)); reshapeLayer->GetOutputSlot(0).SetTensorInfo(reshapedInfo); input1Slot = &reshapeLayer->GetOutputSlot(0); return input1Slot; } OutputId ParseOutputId(const std::string & name) { unsigned int outputNum = 0; size_t colonPos = name.find_last_of(":"); if (colonPos != std::string::npos) { int n = std::stoi(name.substr(colonPos+1)); if (n<0 || n>100) { throw ParseException( boost::str( boost::format( "Output tensor id is out of range for %1% %2%") % name % CHECK_LOCATION().AsString())); } outputNum = static_cast(n); } return OutputId(name.substr(0,colonPos),outputNum); } #define CHECK_DATA_FORMAT(NODE_DEF, FORMAT, NODE_TYPE) \ if( FORMAT != "NHWC" && FORMAT != "NCHW" ) \ { \ throw ParseException( \ boost::str( \ boost::format( \ "Unsupported data format %1% passed for %2% node %3%. " \ "Only NHWC and NCHW supported %4%") \ % FORMAT \ % NODE_TYPE \ % NODE_DEF.name() \ % CHECK_LOCATION().AsString())); \ } #define CHECK_PADDING_TYPE(NODE_DEF, PADDING) \ if(PADDING != "SAME" && PADDING != "VALID" ) \ { \ throw ParseException( \ boost::str( \ boost::format( \ "Only 'SAME' and 'VALID' padding supported. Got %1% for %2% %3%") \ % PADDING \ % NODE_DEF.name() \ % CHECK_LOCATION().AsString())); \ } \ } // namespace const std::map TfParser::ms_OperationNameToParsingFunctions = { { "Const", &TfParser::ParseConst }, { "Add", &TfParser::ParseAdd }, { "AddN", &TfParser::ParseAddN }, { "BiasAdd", &TfParser::ParseBiasAdd }, { "Identity", &TfParser::ParseIdentity }, { "Conv2D", &TfParser::ParseConv2D }, { "DepthwiseConv2dNative", &TfParser::ParseDepthwiseConv2D }, { "ExpandDims", &TfParser::ParseExpandDims }, { "FusedBatchNorm", &TfParser::ParseFusedBatchNorm }, { "Gather", &TfParser::ParseGather}, { "Greater", &TfParser::ParseGreater}, { "ConcatV2", &TfParser::ParseConcat }, { "LRN", &TfParser::ParseLrn }, { "MatMul", &TfParser::ParseMatMul }, { "Mean", &TfParser::ParseMean }, { "Mul", &TfParser::ParseMul }, { "Placeholder", &TfParser::ParsePlaceholder }, { "RealDiv", &TfParser::ParseRealDiv }, { "Relu", &TfParser::ParseRelu }, { "Relu6", &TfParser::ParseRelu6 }, { "Reshape", &TfParser::ParseReshape }, { "ResizeBilinear", &TfParser::ParseResizeBilinear }, { "Rsqrt", &TfParser::ParseRsqrt }, { "Shape", &TfParser::ParseShape }, { "Squeeze", &TfParser::ParseSqueeze }, { "Sigmoid", &TfParser::ParseSigmoid }, { "Softmax", &TfParser::ParseSoftmax }, { "Softplus", &TfParser::ParseSoftplus }, { "Split", &TfParser::ParseSplit }, { "Tanh", &TfParser::ParseTanh }, { "MaxPool", &TfParser::ParseMaxPool }, { "AvgPool", &TfParser::ParseAvgPool }, { "Maximum", &TfParser::ParseMaximum }, { "Minimum", &TfParser::ParseMinimum }, { "Equal", &TfParser::ParseEqual }, { "Pad", &TfParser::ParsePad }, { "Sub", &TfParser::ParseSub } }; const std::list TfParser::m_ControlInputs = { "Assert" }; ITfParser* ITfParser::CreateRaw() { return new TfParser(); } ITfParserPtr ITfParser::Create() { return ITfParserPtr(CreateRaw(), &ITfParser::Destroy); } void ITfParser::Destroy(ITfParser* parser) { delete parser; } inline void CalculateSamePadding(uint32_t inputSize, uint32_t stride, uint32_t filterSize, bool samePadding, uint32_t* paddingFront, uint32_t* paddingBack) { *paddingFront = 0; *paddingBack = 0; if (samePadding) { uint32_t outputSize = (inputSize + stride - 1) / stride; uint32_t temp = (outputSize - 1) * stride + filterSize; if (temp > inputSize) { *paddingFront = (temp - inputSize) / 2; *paddingBack = (temp - inputSize) - *paddingFront; } } } void CalcPadding(uint32_t input, uint32_t kernel, uint32_t stride, uint32_t& outPadHead, uint32_t& outPadTail, bool samePadding) { CalculateSamePadding(input, stride, kernel, samePadding, &outPadHead, &outPadTail); } /// An Abstract base class which represents a single tensorflow operation (node) /// that has been (potentially partially) converted to Armnn. /// It may not yet have been fully converted into actual Armnn layers. class ParsedTfOperation { public: ParsedTfOperation(TfParser* parser, const tensorflow::NodeDef& node) : m_Parser(parser) , m_Node(node) { } virtual ~ParsedTfOperation() {}; const tensorflow::NodeDef& GetNode() const { return m_Node; } /// Gets the ArmNN IOutputSlot corresponding to the given output index of the Tensorflow operation. /// This may result in the creation of Armnn layers if this was deferred (e.g. see ParsedConstTfOperation). virtual IOutputSlot& ResolveArmnnOutputSlot(unsigned int tfOutputIndex) = 0; /// If this operation is an Identity then this will follow return the 'parent' operation (recursively). virtual ParsedTfOperation* ResolveIdentityOperations() { return this; } protected: TfParser* m_Parser; const tensorflow::NodeDef& m_Node; }; /// An ParsedTfOperation where the Armnn equivalent is a single layer, /// with output slots that correspond directly to the Tf node outputs. class SingleLayerParsedTfOperation : public ParsedTfOperation { public: SingleLayerParsedTfOperation(TfParser* parser, const tensorflow::NodeDef& node, IConnectableLayer* layer) : ParsedTfOperation(parser, node) , m_Layer(layer) { } IOutputSlot& ResolveArmnnOutputSlot(unsigned int tfOutputIndex) override { BOOST_ASSERT(m_Layer); // Assumes one-to-one mapping between Tf and armnn output slots. unsigned int armnnOutputSlotIdx = tfOutputIndex; if (armnnOutputSlotIdx >= m_Layer->GetNumOutputSlots()) { throw ParseException( boost::str( boost::format( "The requested output slot #%1% " "for %2% does not exist %3%") % armnnOutputSlotIdx % m_Layer->GetName() % CHECK_LOCATION().AsString())); } return m_Layer->GetOutputSlot(armnnOutputSlotIdx); } protected: IConnectableLayer* m_Layer; }; /// A SingleLayerParsedTfOperation for deferred layer creation. class DeferredSingleLayerParsedTfOperation : public SingleLayerParsedTfOperation { public: DeferredSingleLayerParsedTfOperation(TfParser* parser, const tensorflow::NodeDef& node) : SingleLayerParsedTfOperation(parser, node, nullptr) { } IOutputSlot& ResolveArmnnOutputSlot(unsigned int tfOutputIndex) override { if (!m_Layer) { CreateLayerDeferred(); } return SingleLayerParsedTfOperation::ResolveArmnnOutputSlot(tfOutputIndex); } private: virtual void CreateLayerDeferred() = 0; }; TfParser::TfParser() : m_Network(nullptr, nullptr) { } const tensorflow::NodeDef* TfParser::ResolveIdentityNode(const tensorflow::NodeDef* nodeDef) { if (nodeDef->op() != "Identity") { return nodeDef; } if (nodeDef->input_size() != 1) { throw ParseException( boost::str( boost::format( "Identity node should have a single input! %1% has %2% inputs %3%") % nodeDef->name() % nodeDef->input_size() % CHECK_LOCATION().AsString())); } auto it = m_NodesByName.find(nodeDef->input(0)); if (it != m_NodesByName.end()) { const tensorflow::NodeDef* inputNode = it->second; return ResolveIdentityNode(inputNode); } else { throw ParseException( boost::str( boost::format( "Cannot find what the Identity node %1% is linked to! %2%") % nodeDef->name() % CHECK_LOCATION().AsString())); } } std::vector TfParser::GetTfInputNodes(const tensorflow::NodeDef& nodeDef) const { std::vector ret; if (nodeDef.op() == "Const") { // For some reason const node can have "Control Inputs". We ignore them for now. return ret; } ret.reserve(boost::numeric_cast(nodeDef.input_size())); for (int j = 0; j < nodeDef.input_size(); ++j) { OutputId outputId = ParseOutputId(nodeDef.input(j)); if (nodeDef.input(j)[0] == '^') // I couldn't find a better test for control inputs. { // We currently allow Control Input from TensorFlow graph but we ignore them from ArmNN graph. continue; } auto inputIt = m_NodesByName.find(outputId.m_IndexedValue); if (inputIt == m_NodesByName.end()) { throw ParseException( boost::str( boost::format( "Can't find node '%1%', which is listed as an input of '%2%' %3%") % nodeDef.input(j) % nodeDef.name() % CHECK_LOCATION().AsString())); } ret.push_back(OutputOfConstNodeDef(inputIt->second,outputId.m_Index)); } return ret; } std::vector TfParser::GetInputParsedTfOperationsChecked(const tensorflow::NodeDef& nodeDef, std::size_t expectedNumInputs) { // Fetches the tensorflow nodes connected as inputs and validate the size. std::vector nodes = GetTfInputNodes(nodeDef); const std::size_t numInputs = nodes.size(); if (numInputs != expectedNumInputs) { throw ParseException( boost::str( boost::format( "Unexpected number of inputs for node %1%. Expected %2%, found %3% %4%") % nodeDef.name() % expectedNumInputs % numInputs % CHECK_LOCATION().AsString())); } // Fetches the corresponding ParsedTfOperation operations std::vector result; for (auto&& node : nodes) { auto it = m_ParsedTfOperations.find(node.m_IndexedValue->name()); if (it == m_ParsedTfOperations.end()) { throw ParseException( boost::str( boost::format( "Node with name '%1%' has not been parsed %2%") % node.m_IndexedValue->name() % CHECK_LOCATION().AsString())); } ParsedTfOperation* parsedOp = it->second.get(); // Transparently 'skip' any Identity operations. This simplifies the logic inside the ParseXXX() functions. parsedOp = parsedOp->ResolveIdentityOperations(); result.push_back(OutputOfParsedTfOperation(parsedOp,node.m_Index)); } return result; } IConnectableLayer* TfParser::CreateAdditionLayer( const tensorflow::NodeDef& nodeDef, IOutputSlot* input0Slot, IOutputSlot* input1Slot, const std::string& layerName) { const TensorInfo& input0Info = input0Slot->GetTensorInfo(); const TensorInfo& input1Info = input1Slot->GetTensorInfo(); const unsigned int input0Dim = input0Info.GetNumDimensions(); const unsigned int input1Dim = input1Info.GetNumDimensions(); if (input0Dim != input1Dim) { // broadcasting where input0 and input1 have different number of dimensions // is only supported for 1D and 4D tensors pair if (input0Dim == 1 && input1Dim == 4) { input0Slot = AddBroadcastReshapeLayer(input1Slot, input0Slot, true, *m_Network, nodeDef); } else if (input0Dim == 4 && input1Dim == 1) { input1Slot = AddBroadcastReshapeLayer(input0Slot, input1Slot, true, *m_Network, nodeDef); } else { throw ParseException( boost::str( boost::format("Unsupported broadcast configuration for %1% operation %2% %3%") % layerName % nodeDef.name() % CHECK_LOCATION().AsString())); } } IConnectableLayer* const layer = m_Network->AddAdditionLayer(layerName.c_str()); input0Slot->Connect(layer->GetInputSlot(0)); input1Slot->Connect(layer->GetInputSlot(1)); // Ensure the output tensor has the correct dimensions even if a broadcast has been done TensorInfo outputInfo = input0Slot->GetTensorInfo(); std::vector outputShape; const TensorShape& input0Shape = input0Slot->GetTensorInfo().GetShape(); const TensorShape& input1Shape = input1Slot->GetTensorInfo().GetShape(); for (unsigned int i = 0; i < input0Shape.GetNumDimensions(); i++) { outputShape.push_back(std::max(input0Shape[i], input1Shape[i])); } outputInfo.SetShape(TensorShape(input0Shape.GetNumDimensions(), outputShape.data())); layer->GetOutputSlot(0).SetTensorInfo(outputInfo); return layer; } IConnectableLayer* TfParser::CreateAdditionLayer( const tensorflow::NodeDef& nodeDef, IConnectableLayer* layerOne, IConnectableLayer* layerTwo, unsigned int numberOfAddition, unsigned long numberOfLayersToConnect, bool isOdd) { IOutputSlot* input0Slot = &layerOne->GetOutputSlot(0); IOutputSlot* input1Slot = &layerTwo->GetOutputSlot(0); std::string layerName(nodeDef.name()); if (isOdd || numberOfLayersToConnect != 2) { // we are not connecting the final layer layerName.append("_addN_").append(std::to_string(numberOfAddition)); } return CreateAdditionLayer(nodeDef, input0Slot, input1Slot, layerName); } IConnectableLayer* TfParser::CreateAdditionLayer( const tensorflow::NodeDef& nodeDef, const OutputOfParsedTfOperation& opOne, const OutputOfParsedTfOperation& opTwo, unsigned int numberOfAddition) { IOutputSlot* input0Slot = &opOne.m_IndexedValue->ResolveArmnnOutputSlot(opOne.m_Index); IOutputSlot* input1Slot = &opTwo.m_IndexedValue->ResolveArmnnOutputSlot(opTwo.m_Index); std::string layerName(nodeDef.name()); layerName.append("_addN_").append(std::to_string(numberOfAddition)); return CreateAdditionLayer(nodeDef, input0Slot, input1Slot, layerName); } IConnectableLayer* TfParser::CreateAdditionLayer( const tensorflow::NodeDef& nodeDef, const OutputOfParsedTfOperation& op, IConnectableLayer* layer) { IOutputSlot* input0Slot = &op.m_IndexedValue->ResolveArmnnOutputSlot(op.m_Index); IOutputSlot* input1Slot = &layer->GetOutputSlot(0); return CreateAdditionLayer(nodeDef, input0Slot, input1Slot, nodeDef.name()); } ParsedTfOperationPtr TfParser::ParseAddN(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { uint32_t numberOfInputs = ReadMandatoryNodeUint32Attribute(nodeDef, "N"); if (numberOfInputs < 2) { // should never happen throw ParseException( boost::str( boost::format( "AddN Node with name '%1%' has less than two (%2) inputs %3%") % nodeDef.name() % std::to_string(numberOfInputs) % CHECK_LOCATION().AsString())); } else if (numberOfInputs == 2) { //this is the same as a simple Add operation return AddAdditionLayer(nodeDef, false); } else { // build a binary tree of Add layers and return the final Add as the return from the function // if we have an odd number of inputs then the final Add will consist of a layer connecting to an // OutputOfParsedTfOperation, otherwise it will be two layers being added together std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, numberOfInputs); unsigned int numberOfAdditions = 0; std::vector layers; // NOTE: at this point we will have a minimum of three inputs for (unsigned int i = 0; i < numberOfInputs; ++i) { // every time i is odd we have two inputs to process. bool onSecondItem = i % 2; if (onSecondItem) { ++numberOfAdditions; IConnectableLayer* newLayer = CreateAdditionLayer( nodeDef, inputs[ i - 1], inputs[i], numberOfAdditions); layers.push_back(newLayer); } } std::vector layersToConnect(layers); unsigned long numberOfLayersToConnect = layersToConnect.size(); bool isOdd = numberOfInputs % 2; while (numberOfLayersToConnect > 1) { layers.clear(); for (unsigned long i = 0; i < numberOfLayersToConnect; ++i) { bool onSecondItem = i % 2; if (onSecondItem) { ++numberOfAdditions; IConnectableLayer* newLayer = CreateAdditionLayer( nodeDef, layersToConnect[i - 1], layersToConnect[i], numberOfAdditions, numberOfLayersToConnect, isOdd); layers.push_back(newLayer); } } //OK... need to go again... maybe layersToConnect = layers; numberOfLayersToConnect = layersToConnect.size(); } IConnectableLayer* finalLayer = layersToConnect[0]; // if we had an odd number of inputs we need to connect the final layer to the // last OutputOfParsedTfOperation in order to create the last Add layer we will // be handing back. if (isOdd) { // connect the final layer to the last op finalLayer = CreateAdditionLayer(nodeDef, inputs[numberOfInputs - 1], finalLayer); } return std::make_unique(this, nodeDef, finalLayer); } } ParsedTfOperationPtr TfParser::ParseAdd(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); // If one of the inputs is a MatMul and the other is a const, then we handle both nodes // together as FullyConnected. if (inputs[0].m_IndexedValue->GetNode().op() == "MatMul" && HasParsedConstTensor(inputs[1].m_IndexedValue->GetNode().name())) { IConnectableLayer* layer = AddFullyConnectedLayer(inputs[0].m_IndexedValue->GetNode(), &nodeDef,nodeDef.name().c_str()); return std::make_unique(this, nodeDef, layer); } else if (HasParsedConstTensor(inputs[0].m_IndexedValue->GetNode().name()) && inputs[1].m_IndexedValue->GetNode().op() == "MatMul") { IConnectableLayer* layer = AddFullyConnectedLayer(inputs[1].m_IndexedValue->GetNode(), &nodeDef,nodeDef.name().c_str()); return std::make_unique(this, nodeDef, layer); } else { // Otherwise it's just a regular addition. return AddAdditionLayer(nodeDef); } } ParsedTfOperationPtr TfParser::ParseBiasAdd(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { return AddAdditionLayer(nodeDef, true); } /// An ParsedTfOperation which forwards to another (used for Identity nodes). class ParsedIdentityTfOperation : public ParsedTfOperation { public: ParsedIdentityTfOperation(TfParser* parser, const tensorflow::NodeDef& node, ParsedTfOperation* representative) : ParsedTfOperation(parser, node) , m_Representative(representative) { } virtual IOutputSlot& ResolveArmnnOutputSlot(unsigned int tfOutputIndex) override { BOOST_ASSERT(m_Representative); return m_Representative->ResolveArmnnOutputSlot(tfOutputIndex); } virtual ParsedTfOperation* ResolveIdentityOperations() override { return m_Representative->ResolveIdentityOperations(); } private: ParsedTfOperation* m_Representative; }; ParsedTfOperationPtr TfParser::ParseIdentity(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 1); // Any requests for the output slots of this node should be forwarded to the node connected as input. return std::make_unique(this, nodeDef, inputs[0].m_IndexedValue); } /// An ParsedTfOperation for a Const node. /// Creation of the armnn ConstLayer is deferred until it is actually needed, because Const nodes are mostly used /// for weight inputs to MatMul/Conv2D nodes and in these cases armnn doesn't need a ConstLayer. template class ParsedConstTfOperation : public DeferredSingleLayerParsedTfOperation { public: ParsedConstTfOperation(TfParser* parser, const tensorflow::NodeDef& node, const T* tensorData, const TensorInfo& tensorInfo) : DeferredSingleLayerParsedTfOperation(parser, node), m_Storage(tensorData, tensorData + tensorInfo.GetNumElements()), m_TensorInfo(tensorInfo) { BOOST_ASSERT(GetDataTypeSize(tensorInfo.GetDataType()) == sizeof(T)); } void CreateLayerDeferred() override { BOOST_ASSERT(m_Layer == nullptr); m_Layer = m_Parser->m_Network->AddConstantLayer(ConstTensor(m_TensorInfo, m_Storage), m_Node.name().c_str()); m_Layer->GetOutputSlot(0).SetTensorInfo(m_TensorInfo); } ConstTensor GetConstTensor(std::vector& outputTensorData) const { outputTensorData.resize(m_TensorInfo.GetNumElements()); memcpy(outputTensorData.data(), m_Storage.data(), m_TensorInfo.GetNumBytes()); // Updates the result to point to the user provided storage. ConstTensor constTensor(m_TensorInfo, outputTensorData); return constTensor; } const T* GetStorage() const { return m_Storage.data(); } const TensorInfo& GetTensorInfo() const { return m_TensorInfo; } private: ///< Manages the lifetime of the tensor data. std::vector m_Storage; ///< Describes the layout of the tensor and points to the data in m_Storage. TensorInfo m_TensorInfo; }; DataType ConvertTfTensorDataType(const tensorflow::DataType tfDataType, const tensorflow::NodeDef& nodeDef) { switch (tfDataType) { case tensorflow::DT_FLOAT: return DataType::Float32; break; case tensorflow::DT_INT32: return DataType::Signed32; break; default: throw ParseException( boost::str( boost::format( "Unknown DataType %1% for node %2% %3%") % tensorflow::DataType_Name(tfDataType) % nodeDef.name() % CHECK_LOCATION().AsString())); } } struct ParseTfTensorValueList { template static void Parse( const tensorflow::TensorProto& tfTensor, unsigned int dstElements, std::vector& outputData); template static void ReadData(const void* srcData, unsigned int numSrcElements, std::vector& dstData, unsigned int numDstElements) { // If there are no entries in the list, perform no action. if (numSrcElements == 0) { return; } // If no size was provided, use the length of the value list. if (numDstElements == 0) { numDstElements = numSrcElements; } // Allocates memory. dstData.resize(std::max(numSrcElements, numDstElements) * sizeof(DataType)); const DataType* srcTensor = reinterpret_cast(srcData); DataType* dstTensor = reinterpret_cast(dstData.data()); // Copies the value list entries into the destination. std::copy(srcTensor, srcTensor + numSrcElements, dstTensor); if (numDstElements > numSrcElements) { // Uses the last element in the list to fill the remaining entries. std::fill(dstTensor + numSrcElements, dstTensor + numDstElements, srcTensor[numSrcElements - 1]); } } }; template <> void ParseTfTensorValueList::Parse(const tensorflow::TensorProto& tfTensor, unsigned int dstElements, std::vector& outputData) { ReadData(tfTensor.float_val().data(), static_cast(tfTensor.float_val_size()), outputData, dstElements); } template <> void ParseTfTensorValueList::Parse(const tensorflow::TensorProto& tfTensor, unsigned int dstElements, std::vector& outputData) { ReadData(tfTensor.int_val().data(), static_cast(tfTensor.int_val_size()), outputData, dstElements); } template class OperatorType, typename T = int8_t> struct MakeTfOperation { template inline static std::unique_ptr> Parse(TfParser* parser, const tensorflow::NodeDef& node, Args&&... args) { return std::make_unique>(parser, node, std::forward(args)...); } }; template <> struct MakeTfOperation { template inline static std::unique_ptr> Parse(TfParser* parser, const tensorflow::NodeDef& node, const std::vector& tensorData, const TensorInfo& tensorInfo) { return std::make_unique>(parser, node, reinterpret_cast(tensorData.data()), tensorInfo); } }; template struct InvokeParseFunction { template inline static ResType Result(DataType dataType, Args&&... args) { if (dataType == DataType::Float32) { return FuncType::template Parse(std::forward(args)...); } else if (dataType == DataType::Signed32) { return FuncType::template Parse(std::forward(args)...); } return ResType(); } template inline static void Result(DataType dataType, Args&&... args) { if (dataType == DataType::Float32) { FuncType::template Parse(std::forward(args)...); } else if (dataType == DataType::Signed32) { FuncType::template Parse(std::forward(args)...); } } }; ParsedTfOperationPtr TfParser::ParseConst(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { BOOST_ASSERT(nodeDef.op() == "Const"); if (nodeDef.attr().count("value") == 0) { throw ParseException( boost::str( boost::format( "Value not found for Const node - %1% %2%") % nodeDef.name() % CHECK_LOCATION().AsString())); } const tensorflow::TensorProto& tfTensor = nodeDef.attr().at("value").tensor(); const tensorflow::TensorShapeProto& tfTensorShape = tfTensor.tensor_shape(); const tensorflow::DataType tfDataType = ReadMandatoryNodeTypeAttribute(nodeDef, "dtype"); const auto GetDimensionSize = [](auto& d) { return d.size(); }; std::vector dimensionSizes; std::transform(tfTensorShape.dim().begin(), tfTensorShape.dim().end(), std::back_inserter(dimensionSizes), GetDimensionSize); // Calculates number of elements. const DataType dataType = ConvertTfTensorDataType(tfDataType, nodeDef); unsigned int numElements = 0U; if (!dimensionSizes.empty()) { numElements = std::accumulate(dimensionSizes.begin(), dimensionSizes.end(), 1U, std::multiplies()); } std::vector tensorData; // Get tensor data from the list of values attribute. if (tfTensor.tensor_content().empty()) { InvokeParseFunction::Result(dataType, tfTensor, numElements, tensorData); // If the tensor shape is not defined, but there is a value list, then interpret the data as a 1D // tensor of the provided number of elements. if (numElements == 0) { const unsigned int tfNumElements = static_cast(tensorData.size()) / GetDataTypeSize(dataType); dimensionSizes.push_back(tfNumElements); } } // Gets tensor data from tensor content attribute. else { tensorData.assign(tfTensor.tensor_content().begin(), tfTensor.tensor_content().end()); // Checks if a tensor shape is defined for the tensor content. if (numElements == 0) { throw ParseException( boost::str( boost::format( "No tensor shape found for Const node - %1% %2%") % nodeDef.name() % CHECK_LOCATION().AsString())); } } // Const node requires at least a list of values or a content attribute. if (tensorData.empty()) { throw ParseException( boost::str( boost::format( "No tensor data found for Const node - %1% %2%") % nodeDef.name() % CHECK_LOCATION().AsString())); } const TensorInfo tensorInfo(static_cast(dimensionSizes.size()), dimensionSizes.data(), dataType); // If we have a list of values, then the length of the list must be // less than or equal to the number of elements implied by the shape argument. if (tensorData.size() > tensorInfo.GetNumBytes()) { throw ParseException( boost::str( boost::format( "Number of elements (%1%) should be less than or equal " "to the number of elements implied by the shape argument (%2%) for Const node - %3% %4%") % (tensorData.size() / GetDataTypeSize(dataType)) % tensorInfo.GetNumElements() % nodeDef.name() % CHECK_LOCATION().AsString())); } return InvokeParseFunction>::Result( dataType, this, nodeDef, tensorData, tensorInfo); } template bool TfParser::HasParsedConstTensor(const std::string & nodeName) const { auto it = m_ParsedTfOperations.find(nodeName); if (it == m_ParsedTfOperations.end()) { return false; } return dynamic_cast*>(it->second.get()) != nullptr; } template bool TfParser::HasParsedConstTensor(ParsedTfOperation* parsedTfOpPtr) const { return dynamic_cast*>(parsedTfOpPtr) != nullptr; } unsigned int TfParser::GetConstInputIndex(const std::vector& inputs) { for (unsigned int i = 0; i < inputs.size(); i++) { if (HasParsedConstTensor(inputs[i].m_IndexedValue->GetNode().name())) { return i; } } throw ParseException( boost::str( boost::format( "ArmNN only supports operators with constant axis. %1%") % CHECK_LOCATION().AsString())); } ParsedTfOperationPtr TfParser::ParseConv2D(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); IOutputSlot& inputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); TensorInfo inputTensorInfo = inputSlot.GetTensorInfo(); if (!HasParsedConstTensor(inputs[1].m_IndexedValue->GetNode().name())) { throw ParseException( boost::str( boost::format( "ArmNN only supports Convolution layers with constant weights for %1%, input %2% %3%") % nodeDef.name() % inputs[1].m_IndexedValue->GetNode().name() % CHECK_LOCATION().AsString())); } ParsedConstTfOperation* weightNode = boost::polymorphic_downcast *>(inputs[1].m_IndexedValue); std::string paddingString = ReadMandatoryNodeStringAttribute(nodeDef, "padding"); std::string dataFormat = ReadMandatoryNodeStringAttribute(nodeDef, "data_format"); std::vector strides = ReadMandatoryNodeUint32ListAttribute(nodeDef, "strides"); // Read the dilations, if present - only [1,1,1,1] (the default) is supported. std::vector dilations = ReadOptionalNodeUint32ListAttribute(nodeDef, "dilations"); if (!dilations.empty()) { for (auto dilation : dilations) { if (dilation != 1u) { throw ParseException( boost::str( boost::format( "ArmNN only supports Convolution layers with dilations [1,1,1,1] for %1% %2%") % nodeDef.name() % CHECK_LOCATION().AsString())); } } } Convolution2dDescriptor desc; desc.m_BiasEnabled = false; CHECK_DATA_FORMAT(nodeDef, dataFormat, "Conv2D"); DataLayout dataLayout = dataFormat == "NHWC" ? DataLayout::NHWC : DataLayout::NCHW; desc.m_DataLayout = dataLayout; DataLayoutIndexed dataLayoutIndexed(dataLayout); desc.m_StrideX = strides[dataLayoutIndexed.GetWidthIndex()]; desc.m_StrideY = strides[dataLayoutIndexed.GetHeightIndex()]; uint32_t inputHeight = inputTensorInfo.GetShape()[dataLayoutIndexed.GetHeightIndex()]; uint32_t inputWidth = inputTensorInfo.GetShape()[dataLayoutIndexed.GetWidthIndex()]; // Mappings from TensorFlow filter tensors to the ArmNN filter tensors. // Tensorflow weights are [H, W, In, Out]. // ArmNN weights have to be [Out, H, W, In] when the data layout is NHWC, // and [Out, In, H, W] when the data layout is NCHW. PermutationVector permutationVector = dataLayout == DataLayout::NHWC ? std::initializer_list{ 1, 2, 3, 0 } : // NHWC: [H, W, In, Out] -> [Out, H, W, In] std::initializer_list{ 2, 3, 1, 0 }; // NCHW: [H, W, In, Out] -> [Out, In, H, W] // Swizzle the tensor using the given permutation vector. const TensorInfo& weightTensorInfo = weightNode->GetTensorInfo(); const TensorInfo weightTensorSwizzledInfo = armnnUtils::Permuted(weightTensorInfo, permutationVector); // Swizzles the content of the tensor's permanent storage into a local storage. std::vector weightTensorSwizzledData(weightTensorInfo.GetNumElements()); armnnUtils::Permute(weightTensorSwizzledInfo.GetShape(), permutationVector, weightNode->GetStorage(), weightTensorSwizzledData.data(), sizeof(float)); // Create a weight tensor with the newly swizzled data. ConstTensor weightTensor(weightTensorSwizzledInfo, weightTensorSwizzledData); uint32_t weightHeight = weightTensor.GetShape()[dataLayoutIndexed.GetHeightIndex()]; uint32_t weightWidth = weightTensor.GetShape()[dataLayoutIndexed.GetWidthIndex()]; bool padding = false; TensorInfo outputInfo; unsigned int outputHeight = 0; unsigned int outputWidth = 0; CHECK_PADDING_TYPE(nodeDef, paddingString); if (paddingString == "SAME") { padding = true; outputHeight = static_cast(ceil(static_cast(inputHeight) / static_cast(desc.m_StrideY))); outputWidth = static_cast(ceil(static_cast(inputWidth) / static_cast(desc.m_StrideX))); } else if (paddingString == "VALID") { padding = false; outputHeight = static_cast(ceil(static_cast(inputHeight - weightHeight + 1) / static_cast(desc.m_StrideY))); outputWidth = static_cast(ceil(static_cast(inputWidth - weightWidth + 1) / static_cast(desc.m_StrideX))); } switch (dataLayout) { case DataLayout::NHWC: outputInfo = TensorInfo({ inputTensorInfo.GetShape()[0], outputHeight, outputWidth, weightTensor.GetShape()[0] }, DataType::Float32); break; case DataLayout::NCHW: default: outputInfo = TensorInfo({ inputTensorInfo.GetShape()[0], weightTensor.GetShape()[0], outputHeight, outputWidth }, DataType::Float32); break; } CalcPadding(inputHeight, weightHeight, desc.m_StrideY, desc.m_PadTop, desc.m_PadBottom, padding); CalcPadding(inputWidth, weightWidth, desc.m_StrideX, desc.m_PadLeft, desc.m_PadRight, padding); IConnectableLayer* layer = m_Network->AddConvolution2dLayer(desc, weightTensor, nodeDef.name().c_str()); layer->GetOutputSlot(0).SetTensorInfo(outputInfo); inputSlot.Connect(layer->GetInputSlot(0)); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseDepthwiseConv2D(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); IOutputSlot& inputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); TensorInfo inputTensorInfo = inputSlot.GetTensorInfo(); if (!HasParsedConstTensor(inputs[1].m_IndexedValue->GetNode().name())) { throw ParseException( boost::str( boost::format( "ArmNN only supports Depthwise Convolution layer with constant weights. " "Non const input found %1% for node %2% %3%") % inputs[1].m_IndexedValue->GetNode().name() % nodeDef.name() % CHECK_LOCATION().AsString())); } ParsedConstTfOperation* weightNode = boost::polymorphic_downcast *>(inputs[1].m_IndexedValue); std::string paddingString = ReadMandatoryNodeStringAttribute(nodeDef, "padding"); std::string dataFormat = ReadMandatoryNodeStringAttribute(nodeDef, "data_format"); std::vector strides = ReadMandatoryNodeUint32ListAttribute(nodeDef, "strides"); DepthwiseConvolution2dDescriptor desc; desc.m_BiasEnabled = false; CHECK_DATA_FORMAT(nodeDef, dataFormat, "DepthwiseConv2dNative"); DataLayout dataLayout = dataFormat == "NHWC" ? DataLayout::NHWC : DataLayout::NCHW; desc.m_DataLayout = dataLayout; DataLayoutIndexed dataLayoutIndexed(dataLayout); desc.m_StrideX = strides[dataLayoutIndexed.GetWidthIndex()]; desc.m_StrideY = strides[dataLayoutIndexed.GetHeightIndex()]; uint32_t inputHeight = inputTensorInfo.GetShape()[dataLayoutIndexed.GetHeightIndex()]; uint32_t inputWidth = inputTensorInfo.GetShape()[dataLayoutIndexed.GetWidthIndex()]; // Mappings from TensorFlow filter tensors to the ArmNN filter tensors. // Tensorflow weights come in the format [H, W, I, M]. // ArmNN weights have to be [M, I, H, W]. PermutationVector permutationVector{ 2, 3, 1, 0 }; // [H, W, I, M] -> [M, I, H, W] // Swizzle the tensor using the given permutation vector. const TensorInfo& weightTensorInfo = weightNode->GetTensorInfo(); const TensorInfo weightTensorSwizzledInfo = armnnUtils::Permuted(weightTensorInfo, permutationVector); // Swizzles the content of the tensor's permanent storage into a local storage. std::vector weightTensorSwizzledData(weightTensorInfo.GetNumElements()); armnnUtils::Permute(weightTensorSwizzledInfo.GetShape(), permutationVector, weightNode->GetStorage(), weightTensorSwizzledData.data(), sizeof(float)); // Create a weight tensor with the newly swizzled data. ConstTensor weightTensor(weightTensorSwizzledInfo, weightTensorSwizzledData); uint32_t weightHeight = weightTensor.GetShape()[2]; uint32_t weightWidth = weightTensor.GetShape()[3]; bool padding = false; TensorInfo outputInfo; unsigned int outputHeight = 0; unsigned int outputWidth = 0; CHECK_PADDING_TYPE(nodeDef, paddingString); if (paddingString == "SAME") { padding = true; outputHeight = static_cast(ceil(static_cast(inputHeight) / static_cast(desc.m_StrideY))); outputWidth = static_cast(ceil(static_cast(inputWidth) / static_cast(desc.m_StrideX))); } else if (paddingString == "VALID") { padding = false; outputHeight = static_cast(ceil(static_cast(inputHeight - weightHeight + 1) / static_cast(desc.m_StrideY))); outputWidth = static_cast(ceil(static_cast(inputWidth - weightWidth + 1) / static_cast(desc.m_StrideX))); } switch (dataLayout) { case DataLayout::NHWC: outputInfo = TensorInfo({ inputTensorInfo.GetShape()[0], outputHeight, outputWidth, weightTensor.GetShape()[0] * weightTensor.GetShape()[1]}, DataType::Float32); break; case DataLayout::NCHW: default: outputInfo = TensorInfo({ inputTensorInfo.GetShape()[0], weightTensor.GetShape()[0] * weightTensor.GetShape()[1], outputHeight, outputWidth }, DataType::Float32); break; } CalcPadding(inputHeight, weightHeight, desc.m_StrideY, desc.m_PadTop, desc.m_PadBottom, padding); CalcPadding(inputWidth, weightWidth, desc.m_StrideX, desc.m_PadLeft, desc.m_PadRight, padding); IConnectableLayer* layer = m_Network->AddDepthwiseConvolution2dLayer(desc, weightTensor, nodeDef.name().c_str()); layer->GetOutputSlot(0).SetTensorInfo(outputInfo); inputSlot.Connect(layer->GetInputSlot(0)); return std::make_unique(this, nodeDef, layer); } TensorInfo OutputShapeOfExpandDims(const tensorflow::NodeDef& nodeDef, TensorInfo inputTensorInfo) { BOOST_ASSERT(nodeDef.op() == "ExpandDims"); if (inputTensorInfo.GetNumDimensions() > 4) { throw ParseException( boost::str( boost::format( "Unsupported number of dimensions: %1% for input shape for ExpandDims %2% %3%") % inputTensorInfo.GetNumDimensions() % nodeDef.name() % CHECK_LOCATION().AsString())); } std::int32_t expandDim = ReadMandatoryNodeInt32Attribute(nodeDef, "Tdim"); std::int32_t inputDimSize = boost::numeric_cast(inputTensorInfo.GetNumDimensions()); std::vector outputDims; // expandDim operation requires: -1-input.dims() <= dim <= input.dims() if (expandDim >= -1 - inputDimSize && expandDim <= inputDimSize) { // add current input shape to outputDims for (unsigned int i = 0; i < inputTensorInfo.GetNumDimensions(); ++i) { auto currentDimension = inputTensorInfo.GetShape()[i]; outputDims.push_back(currentDimension); } // insert a dimension of 1 at index 'expandDim' of inputs shape if (expandDim >= 0) { auto getPosition = std::next(outputDims.begin() + 0, expandDim); outputDims.insert(getPosition, 1); } // if negative number for 'expandDim' then count backwards from the last element // and insert 1 dimension at index 'expandDim' if (expandDim < 0) { int outputDimSize = boost::numeric_cast(outputDims.size() + 1); auto getPosition = std::next(outputDims.begin() + outputDimSize, expandDim); outputDims.insert(getPosition, 1); } } else { throw InvalidArgumentException( boost::str( boost::format( "Cannot expand dimension %1% in input tensor with %2% dimension %3%") % expandDim % inputDimSize % CHECK_LOCATION().AsString())); } if (outputDims.size() > 4) { throw ParseException( boost::str( boost::format( "Unsupported number of dimensions: %1% for output shape for ExpandDims %2% %3%") % outputDims.size() % nodeDef.name() % CHECK_LOCATION().AsString())); } TensorShape outShape = TensorShape(static_cast(outputDims.size()), outputDims.data()); TensorInfo outTensorInfo = inputTensorInfo; outTensorInfo.SetShape(outShape); return outTensorInfo; } ParsedTfOperationPtr TfParser::ParseExpandDims(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 1); IOutputSlot& prevLayerOutputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); TensorInfo inputTensorInfo = prevLayerOutputSlot.GetTensorInfo(); TensorInfo outputInfo; outputInfo = OutputShapeOfExpandDims(nodeDef, inputTensorInfo); ReshapeDescriptor reshapeDesc; reshapeDesc.m_TargetShape = outputInfo.GetShape(); IConnectableLayer* layer = m_Network->AddReshapeLayer(reshapeDesc, nodeDef.name().c_str()); prevLayerOutputSlot.Connect(layer->GetInputSlot(0)); layer->GetOutputSlot(0).SetTensorInfo(outputInfo); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseFusedBatchNorm(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 5); if (!HasParsedConstTensor(inputs[1].m_IndexedValue->GetNode().name())) { throw ParseException( boost::str( boost::format( "ArmNN only supports FusedBatchNormalization layers with constant scale. " "Input %1%. Node %2% %3%") % inputs[1].m_IndexedValue->GetNode().name() % nodeDef.name() % CHECK_LOCATION().AsString())); } ParsedConstTfOperation* scaleNode = boost::polymorphic_downcast *>(inputs[1].m_IndexedValue); if (!HasParsedConstTensor(inputs[2].m_IndexedValue->GetNode().name())) { throw ParseException( boost::str( boost::format( "ArmNN only supports FusedBatchNormalization layers with constant offset. " "Input %1%. Node %2% %3%") % inputs[2].m_IndexedValue->GetNode().name() % nodeDef.name() % CHECK_LOCATION().AsString())); } ParsedConstTfOperation* offsetNode = boost::polymorphic_downcast *>(inputs[2].m_IndexedValue); if (!HasParsedConstTensor(inputs[3].m_IndexedValue->GetNode().name())) { throw ParseException( boost::str( boost::format( "ArmNN only supports FusedBatchNormalization layers with constant mean. " "Input %1%. Node %2% %3%") % inputs[3].m_IndexedValue->GetNode().name() % nodeDef.name() % CHECK_LOCATION().AsString())); } ParsedConstTfOperation* meanNode = boost::polymorphic_downcast *>(inputs[3].m_IndexedValue); if (!HasParsedConstTensor(inputs[4].m_IndexedValue->GetNode().name())) { throw ParseException( boost::str( boost::format( "ArmNN only supports FusedBatchNormalization layers with constant variance. " "Input %1%. Node %2% %3%") % inputs[4].m_IndexedValue->GetNode().name() % nodeDef.name() % CHECK_LOCATION().AsString())); } ParsedConstTfOperation* varianceNode = boost::polymorphic_downcast *>(inputs[4].m_IndexedValue); const std::string dataFormat = ReadMandatoryNodeStringAttribute(nodeDef, "data_format"); CHECK_DATA_FORMAT(nodeDef, dataFormat, "FusedBatchNorm"); // The descriptor only has the epsilon attribute. BatchNormalizationDescriptor desc; desc.m_Eps = ReadMandatoryNodeFloatAttribute(nodeDef, "epsilon"); desc.m_DataLayout = dataFormat == "NHWC" ? DataLayout::NHWC : DataLayout::NCHW; // Data for the parsed tensor args (scale, offset, mean, variance) must be stored // locally until the layer is added. std::vector scaleTensorData; ConstTensor scaleTensor = scaleNode->GetConstTensor(scaleTensorData); std::vector offsetTensorData; ConstTensor offsetTensor = offsetNode->GetConstTensor(offsetTensorData); std::vector meanTensorData; ConstTensor meanTensor = meanNode->GetConstTensor(meanTensorData); std::vector varianceTensorData; ConstTensor varianceTensor = varianceNode->GetConstTensor(varianceTensorData); IConnectableLayer* layer = m_Network->AddBatchNormalizationLayer(desc, meanTensor, varianceTensor, offsetTensor, scaleTensor, nodeDef.name().c_str()); IOutputSlot& inputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); layer->GetOutputSlot(0).SetTensorInfo(inputSlot.GetTensorInfo()); inputSlot.Connect(layer->GetInputSlot(0)); return std::make_unique(this, nodeDef, layer); } bool TfParser::IsSupportedLeakyReluPattern(const tensorflow::NodeDef& mulNodeDef, size_t alphaLayerIndex, const OutputOfParsedTfOperation& otherOp, armnn::IOutputSlot** outputOfLeakyRelu, armnn::ActivationDescriptor & desc) { const tensorflow::NodeDef& otherNodeDef = otherOp.m_IndexedValue->GetNode(); // Verifying all these assumptions hold: // // 1, the mulNodeDef is an elementwise multiplication node "Mul" // 2, the alphaLayerIndex selects a constant node from the inputs of the "Mul" node // 3, the inputLayerIndex selects a layer which has the same name as otherNodeDef // if (mulNodeDef.op() == "Mul") { size_t otherLayerIndex = (alphaLayerIndex == 0 ? 1 : 0); std::vector inputs = GetInputParsedTfOperationsChecked(mulNodeDef, 2); BOOST_ASSERT(inputs.size() == 2); BOOST_ASSERT((otherLayerIndex == 0 || alphaLayerIndex == 0)); BOOST_ASSERT((otherLayerIndex == 1 || alphaLayerIndex == 1)); BOOST_ASSERT(((otherLayerIndex + alphaLayerIndex) == 1)); if (inputs[otherLayerIndex].m_IndexedValue->GetNode().name() == otherNodeDef.name()) { if (HasParsedConstTensor(inputs[alphaLayerIndex].m_IndexedValue->GetNode().name())) { ParsedConstTfOperation* alpha = boost::polymorphic_downcast *>( inputs[alphaLayerIndex].m_IndexedValue); std::vector const_data; ConstTensor const_tensor = alpha->GetConstTensor(const_data); if (const_data.size() == 1) { desc.m_Function = ActivationFunction::LeakyReLu; desc.m_A = const_data[0]; *outputOfLeakyRelu = &(otherOp.m_IndexedValue->ResolveArmnnOutputSlot(otherOp.m_Index)); return true; } } } } return false; } ParsedTfOperationPtr TfParser::ParseMaximum(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); if (inputs.size() != 2) { throw ParseException( boost::str( boost::format( "Maximum expects two inputs!. Got %1% for Node %2% %3%") % inputs.size() % nodeDef.name() % CHECK_LOCATION().AsString())); } auto inputNode0 = inputs[0].m_IndexedValue->GetNode(); auto inputNode1 = inputs[1].m_IndexedValue->GetNode(); IOutputSlot* outputOfLeakyRelu = nullptr; ActivationDescriptor desc; // A max node may be part of a LeakyRelu, with one input as a multiplication with a scalar constant, // i.e. one of the four possible scenarios: // 1, max(mul(a, x), x) // 2, max(mul(x, a), x) // 3, max(x, mul(a, x)) // 4, max(x, mul(x, a)) // These are handled by an activation layer. if (IsSupportedLeakyReluPattern(inputNode0, 0, inputs[1], &outputOfLeakyRelu, desc) || IsSupportedLeakyReluPattern(inputNode0, 1, inputs[1], &outputOfLeakyRelu, desc) || IsSupportedLeakyReluPattern(inputNode1, 0, inputs[0], &outputOfLeakyRelu, desc) || IsSupportedLeakyReluPattern(inputNode1, 1, inputs[0], &outputOfLeakyRelu, desc)) { BOOST_ASSERT(outputOfLeakyRelu != nullptr); IConnectableLayer* const layer = m_Network->AddActivationLayer(desc, nodeDef.name().c_str()); outputOfLeakyRelu->Connect(layer->GetInputSlot(0)); layer->GetOutputSlot(0).SetTensorInfo(outputOfLeakyRelu->GetTensorInfo()); return std::make_unique(this, nodeDef, layer); } else { // Anything else is just a maximum layer. return AddMaximumLayer(nodeDef); } } std::pair TfParser::ProcessElementwiseInputSlots( const tensorflow::NodeDef& nodeDef, const std::string& layerName) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); IOutputSlot* input0Slot = &inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); IOutputSlot* input1Slot = &inputs[1].m_IndexedValue->ResolveArmnnOutputSlot(inputs[1].m_Index); const unsigned int input0Dim = input0Slot->GetTensorInfo().GetNumDimensions(); const unsigned int input1Dim = input1Slot->GetTensorInfo().GetNumDimensions(); if (input0Dim != input1Dim) { // broadcasting where input0 and input1 have different number of dimensions // is only supported for 1D and 4D tensors pair if (input0Dim == 1 && input1Dim == 4) { input0Slot = AddBroadcastReshapeLayer(input1Slot, input0Slot, true, *m_Network, nodeDef); } else if (input0Dim == 4 && input1Dim == 1) { input1Slot = AddBroadcastReshapeLayer(input0Slot, input1Slot, true, *m_Network, nodeDef); } else { throw ParseException( boost::str( boost::format("Unsupported broadcast configuration for %1% operation %2% %3%") % layerName % nodeDef.name() % CHECK_LOCATION().AsString())); } } return {input0Slot, input1Slot}; } ParsedTfOperationPtr TfParser::ProcessComparisonLayer( IOutputSlot* input0Slot, IOutputSlot* input1Slot, IConnectableLayer* const layer, const tensorflow::NodeDef& nodeDef) { input0Slot->Connect(layer->GetInputSlot(0)); input1Slot->Connect(layer->GetInputSlot(1)); TensorInfo outputInfo = input0Slot->GetTensorInfo(); outputInfo.SetDataType(DataType::Boolean); std::vector outputShape; const TensorShape& input0Shape = input0Slot->GetTensorInfo().GetShape(); const TensorShape& input1Shape = input1Slot->GetTensorInfo().GetShape(); for (unsigned int i = 0; i < input0Shape.GetNumDimensions(); i++) { outputShape.push_back(std::max(input0Shape[i], input1Shape[i])); } outputInfo.SetShape(TensorShape(input0Shape.GetNumDimensions(), outputShape.data())); layer->GetOutputSlot(0).SetTensorInfo(outputInfo); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ProcessElementwiseLayer( IOutputSlot* input0Slot, IOutputSlot* input1Slot, IConnectableLayer* const layer, const tensorflow::NodeDef& nodeDef) { input0Slot->Connect(layer->GetInputSlot(0)); input1Slot->Connect(layer->GetInputSlot(1)); TensorInfo outputInfo = input0Slot->GetTensorInfo(); std::vector outputShape; const TensorShape& input0Shape = input0Slot->GetTensorInfo().GetShape(); const TensorShape& input1Shape = input1Slot->GetTensorInfo().GetShape(); for (unsigned int i = 0; i < input0Shape.GetNumDimensions(); i++) { outputShape.push_back(std::max(input0Shape[i], input1Shape[i])); } outputInfo.SetShape(TensorShape(input0Shape.GetNumDimensions(), outputShape.data())); layer->GetOutputSlot(0).SetTensorInfo(outputInfo); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseGather(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); IOutputSlot& params = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); IOutputSlot& indices = inputs[1].m_IndexedValue->ResolveArmnnOutputSlot(inputs[1].m_Index); // Infer shape of output tensor unsigned int paramsDim = params.GetTensorInfo().GetNumDimensions(); unsigned int indicesDim = indices.GetTensorInfo().GetNumDimensions(); unsigned int outputDim = paramsDim - 1 + indicesDim; std::vector dimSizes; for (unsigned int i = 0; i < indicesDim; ++i) { dimSizes.push_back(indices.GetTensorInfo().GetShape()[i]); } for (unsigned int i = 1; i < paramsDim; ++i) { dimSizes.push_back(params.GetTensorInfo().GetShape()[i]); } const TensorShape& inferredShape = TensorShape(outputDim, dimSizes.data()); const TensorInfo inferredOutputInfo(inferredShape, params.GetTensorInfo().GetDataType()); IConnectableLayer* const layer = m_Network->AddGatherLayer(nodeDef.name().c_str()); layer->GetOutputSlot(0).SetTensorInfo(inferredOutputInfo); params.Connect(layer->GetInputSlot(0)); indices.Connect(layer->GetInputSlot(1)); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseGreater(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::pair inputLayers = ProcessElementwiseInputSlots(nodeDef, "Greater"); IOutputSlot* input0Slot = inputLayers.first; IOutputSlot* input1Slot = inputLayers.second; IConnectableLayer* const layer = m_Network->AddGreaterLayer(nodeDef.name().c_str()); return ProcessComparisonLayer(input0Slot, input1Slot, layer, nodeDef); } ParsedTfOperationPtr TfParser::ParseEqual(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::pair inputLayers = ProcessElementwiseInputSlots(nodeDef, "Equal"); IOutputSlot* input0Slot = inputLayers.first; IOutputSlot* input1Slot = inputLayers.second; IConnectableLayer* const layer = m_Network->AddEqualLayer(nodeDef.name().c_str()); return ProcessComparisonLayer(input0Slot, input1Slot, layer, nodeDef); } ParsedTfOperationPtr TfParser::ParseMinimum(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::pair inputLayers = ProcessElementwiseInputSlots(nodeDef, "Minimum"); IOutputSlot* input0Slot = inputLayers.first; IOutputSlot* input1Slot = inputLayers.second; IConnectableLayer* const layer = m_Network->AddMinimumLayer(nodeDef.name().c_str()); return ProcessElementwiseLayer(input0Slot, input1Slot, layer, nodeDef); } ParsedTfOperationPtr TfParser::ParseSub(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); IOutputSlot* input0Slot = &inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); IOutputSlot* input1Slot = &inputs[1].m_IndexedValue->ResolveArmnnOutputSlot(inputs[1].m_Index); const TensorInfo& input0Info = input0Slot->GetTensorInfo(); const TensorInfo& input1Info = input1Slot->GetTensorInfo(); if (input0Info.GetNumDimensions() == 1) { const bool isNHWC = true; input0Slot = AddBroadcastReshapeLayer(input1Slot, input0Slot, isNHWC, *m_Network, nodeDef); } if (input1Info.GetNumDimensions() == 1) { const bool isNHWC = true; input1Slot = AddBroadcastReshapeLayer(input0Slot, input1Slot, isNHWC, *m_Network, nodeDef); } IConnectableLayer* const layer = m_Network->AddSubtractionLayer(nodeDef.name().c_str()); input0Slot->Connect(layer->GetInputSlot(0)); input1Slot->Connect(layer->GetInputSlot(1)); if (input0Info.GetNumDimensions() == 1) { layer->GetOutputSlot(0).SetTensorInfo(input1Slot->GetTensorInfo()); } else { layer->GetOutputSlot(0).SetTensorInfo(input0Slot->GetTensorInfo()); } return std::make_unique(this, nodeDef, layer); } unsigned int CheckPaddingTensor(const ConstTensor& paddingTensor, const TensorInfo& inputTensorInfo, const std::string& nodeName) { unsigned int rank = paddingTensor.GetShape()[0]; unsigned int expectedRank = inputTensorInfo.GetNumDimensions(); if (rank != expectedRank) { throw ParseException( boost::str( boost::format( "Expected the padding tensor to be of rank %1 not %2 on Node %3 %4.") % expectedRank % rank % nodeName % CHECK_LOCATION().AsString())); } unsigned int second = paddingTensor.GetShape()[1]; if (second != 2) { throw ParseException( boost::str( boost::format( "Expected the padding tensor to be of dimensions [%1, 2] not [%1, %2] on Node %3 %4.") % rank % second % nodeName % CHECK_LOCATION().AsString())); } return rank; } TensorInfo CalculatePaddedOutputTensorInfo(const TensorInfo& inputTensorInfo, const std::vector>& padList) { unsigned int numDims = inputTensorInfo.GetNumDimensions(); std::vector outDims; for (unsigned int i = 0; i < numDims; ++i) { unsigned int dimSize = inputTensorInfo.GetShape()[i]; const std::pair& dimPadding = padList[i]; dimSize += dimPadding.first; dimSize += dimPadding.second; outDims.push_back(dimSize); } TensorInfo paddedTensorInfo = inputTensorInfo; unsigned int outDimsSize = static_cast(outDims.size()); paddedTensorInfo.SetShape(TensorShape{ outDimsSize, outDims.data() }); return paddedTensorInfo; } ParsedTfOperationPtr TfParser::ParsePad(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { // input consists of: // input[0] the tensor which will be padded // input[1] the tensor holding the padding values std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); IOutputSlot& previousLayerOutputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); TensorInfo inputTensorInfo = previousLayerOutputSlot.GetTensorInfo(); if (!HasParsedConstTensor(inputs[1].m_IndexedValue)) { throw ParseException( boost::str( boost::format( "ArmNN only supports Pad with constant padding. " "Input %1%. Node %2% %3%") % inputs[1].m_IndexedValue->GetNode().name() % nodeDef.name() % CHECK_LOCATION().AsString())); } ParsedConstTfOperation* paddingTensorOp = boost::polymorphic_downcast*>(inputs[1].m_IndexedValue); std::vector paddingTensorData; ConstTensor paddingTensor = paddingTensorOp->GetConstTensor(paddingTensorData); // paddings is an integer tensor with shape [n, 2], where n is the rank of tensor // and should match the rank of the input tensor that is being padded. // For each dimension D of input, paddings[D, 0] indicates how many values to add // before the contents of tensor in that dimension, and paddings[D, 1] indicates how // many values to add after the contents of tensor in that dimension // This needs to be translated into a padList for ACL std::vector> padList; unsigned int rank = CheckPaddingTensor(paddingTensor, inputTensorInfo, nodeDef.name()); for (unsigned int i = 0; i < rank; ++i) { std::pair paddingForDim; for (unsigned int j = 0; j < 2; j++) { unsigned int index = (i * 2) + j; int paddingAmount = paddingTensorData[index]; // make sure we can cast to an unsigned value if (paddingAmount < 0) { throw ParseException( boost::str( boost::format( "Negative amount %1 specified at [%2, %3] of padding tensor on Node %4 %5.") % paddingAmount % i % j % nodeDef.name() % CHECK_LOCATION().AsString())); } if (j == 0) { paddingForDim.first = static_cast(paddingAmount); } else { paddingForDim.second = static_cast(paddingAmount); } } padList.push_back(paddingForDim); } PadDescriptor padDescriptor(padList); IConnectableLayer* layer = m_Network->AddPadLayer(padDescriptor, nodeDef.name().c_str()); previousLayerOutputSlot.Connect(layer->GetInputSlot(0)); // Use the padding to calculate the new output tensor shape TensorInfo outputTensorInfo = CalculatePaddedOutputTensorInfo(inputTensorInfo, padList); layer->GetOutputSlot(0).SetTensorInfo(outputTensorInfo); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseConcat(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector nodes = GetTfInputNodes(nodeDef); // In tensorflow, we have the last input of the Concat layer as the axis for concatenation. unsigned int numInputs = static_cast(nodes.size()); std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, numInputs); // Constant tensor index unsigned int index = GetConstInputIndex(inputs); // Get the axis tensor data ParsedConstTfOperation* shapeNode = boost::polymorphic_downcast*>(inputs[index].m_IndexedValue); std::vector axisTensorData; shapeNode->GetConstTensor(axisTensorData); // This concatDim indicates the data format: 3 is the NHWC, 1 is the NCHW. const unsigned int concatDim = static_cast(axisTensorData[0]); // Armnn supports concatenation along the channel dimension for data formats NHWC and NCHW. if (concatDim == 0 || concatDim == 2) { throw ParseException( boost::str( boost::format( "Dimension %1% for concatenation is not supported by Armnn. " "Node %2% %3%") % concatDim % nodeDef.name() % CHECK_LOCATION().AsString())); } unsigned int numConcatViews = numInputs - 1; OriginsDescriptor concatDescriptor(static_cast(numConcatViews), MaxNumOfTensorDimensions); concatDescriptor.SetConcatAxis(concatDim); TensorShape mergeDims(MaxNumOfTensorDimensions); unsigned int mergeDim = 0; for (unsigned int viewIndex = 0; viewIndex < numConcatViews; ++viewIndex) { // Need to double check whether it should be IOutputSlot& inputSlot = inputs[viewIndex].m_IndexedValue->ResolveArmnnOutputSlot(inputs[viewIndex].m_Index); TensorInfo inputTensorInfo = inputSlot.GetTensorInfo(); // Double check dimensions of the tensors if (inputTensorInfo.GetNumDimensions() != MaxNumOfTensorDimensions) { throw armnn::ParseException( boost::str( boost::format( "The number of dimensions: %1% for input tensors of the " "concatenation op should be %2% %3%") % inputTensorInfo.GetNumDimensions() % MaxNumOfTensorDimensions % CHECK_LOCATION().AsString())); } // Copy the input tensor shape to mergeDimSizes and initialize the view origin coordinates for the current input mergeDims = inputTensorInfo.GetShape(); unsigned int* viewOrigin = const_cast(concatDescriptor.GetViewOrigin(viewIndex)); std::fill(viewOrigin, viewOrigin + MaxNumOfTensorDimensions, 0); // Update the view origin coordinates and the merge dimension value concatDescriptor.SetViewOriginCoord(viewIndex, concatDim, mergeDim); mergeDim += mergeDims[concatDim]; } // Update the output shape mergeDims[concatDim] = mergeDim; armnn::IConnectableLayer *layer = m_Network->AddMergerLayer(concatDescriptor, nodeDef.name().c_str()); layer->GetOutputSlot(0).SetTensorInfo(armnn::TensorInfo(mergeDims, DataType::Float32)); for (unsigned int viewIndex = 0; viewIndex < numConcatViews; ++viewIndex) { IOutputSlot& inputSlot = inputs[viewIndex].m_IndexedValue->ResolveArmnnOutputSlot(inputs[viewIndex].m_Index); inputSlot.Connect(layer->GetInputSlot(viewIndex)); } return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseShape(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { // Note: the Shape layer is handled in a special way, because: // 1. ARMNN doesn't support int32 tensors which it outputs. // 2. ARMNN works with statically shaped tensors which are known at parse time. // 3. because of 1. and 2. we treat the output of Shape as a temporary const int32 // tensor which may be used as an input to other ops, most likely a Reshape. const tensorflow::DataType tfDataType = ReadMandatoryNodeTypeAttribute(nodeDef, "out_type"); if (tfDataType != tensorflow::DT_INT32) { throw ParseException( boost::str( boost::format( "Armnn only supports DT_INT32 as out_type. Got %1% for Node %2% %3%") % tensorflow::DataType_Name(tfDataType) % nodeDef.name() % CHECK_LOCATION().AsString())); } const std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 1); IOutputSlot& prevLayerOutputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); const TensorInfo& prevLayerTensorInfo = prevLayerOutputSlot.GetTensorInfo(); unsigned int prevLayerDimensions = prevLayerTensorInfo.GetNumDimensions(); std::vector shapeTensorData; shapeTensorData.reserve(prevLayerDimensions); for (unsigned int i=0; i(prevLayerTensorInfo.GetShape()[i])); } TensorInfo shapeTensorInfo(1, &prevLayerDimensions, DataType::Signed32); return std::make_unique>(this, nodeDef, &shapeTensorData[0], shapeTensorInfo); } ParsedTfOperationPtr TfParser::ParseReshape(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); ParsedTfOperation* inputNode = inputs[0].m_IndexedValue; if (!HasParsedConstTensor(inputs[1].m_IndexedValue->GetNode().name())) { throw ParseException( boost::str( boost::format( "ArmNN only supports Reshape layers with constant shapes. " "Input %1% Node %2% %3%") % inputs[1].m_IndexedValue->GetNode().name() % nodeDef.name() % CHECK_LOCATION().AsString())); } ParsedConstTfOperation* shapeNode = boost::polymorphic_downcast*>(inputs[1].m_IndexedValue); armnn::IOutputSlot& prevLayerOutputSlot = inputNode->ResolveArmnnOutputSlot(inputs[0].m_Index); TensorInfo inputTensorInfo = prevLayerOutputSlot.GetTensorInfo(); std::vector shapeTensorData; ConstTensor shapeTensor = shapeNode->GetConstTensor(shapeTensorData); const TensorInfo outputTensorInfo = PrepareReshape(inputTensorInfo, shapeTensorData); TensorShape targetShape = outputTensorInfo.GetShape(); ReshapeDescriptor reshapeDesc; reshapeDesc.m_TargetShape = targetShape; IConnectableLayer* layer = m_Network->AddReshapeLayer(reshapeDesc, nodeDef.name().c_str()); prevLayerOutputSlot.Connect(layer->GetInputSlot(0)); layer->GetOutputSlot(0).SetTensorInfo(outputTensorInfo); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseResizeBilinear(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); if (!HasParsedConstTensor(inputs[1].m_IndexedValue->GetNode().name())) { throw ParseException( boost::str( boost::format( "ArmNN only supports ResizeBilinear layers with constant sizes. " "Input %1%. Node %2% %3%") % inputs[1].m_IndexedValue->GetNode().name() % nodeDef.name() % CHECK_LOCATION().AsString())); } ParsedConstTfOperation* sizeNode = boost::polymorphic_downcast*>(inputs[1].m_IndexedValue); // Checks the align_corners attribute is not set. if (ReadOptionalNodeBoolAttribute(nodeDef, "align_corners", false)) { throw ParseException( boost::str( boost::format( "ArmNN only supports ResizeBilinear layers with align_corners set to false. " "Node %1% %2%") % nodeDef.name() % CHECK_LOCATION().AsString())); } // Data for the parsed tensor args (size) must be stored locally. std::vector sizeTensorData; ConstTensor sizeTensor = sizeNode->GetConstTensor(sizeTensorData); // The descriptor only has target height and width attributes, which we get from the size tensor. ResizeBilinearDescriptor desc; desc.m_TargetHeight = static_cast (sizeTensorData[0]); desc.m_TargetWidth = static_cast (sizeTensorData[1]); desc.m_DataLayout = armnn::DataLayout::NHWC; IConnectableLayer* layer = m_Network->AddResizeBilinearLayer(desc, nodeDef.name().c_str()); IOutputSlot& inputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); TensorInfo inputTensorInfo = inputSlot.GetTensorInfo(); // The input shape is always in BHWC format, this will be swizzled below; for now, // get the batch and channels to make up the ArmNN output shape with the target size. unsigned int outBatch = inputTensorInfo.GetShape()[0]; unsigned int outChannels = inputTensorInfo.GetShape()[3]; unsigned int outHeight = desc.m_TargetHeight; unsigned int outWidth = desc.m_TargetWidth; TensorShape outShape({outBatch, outHeight, outWidth, outChannels }); // The output DataType is always Float32, regardless of the input DataType. const TensorInfo outputTensorInfo(outShape, armnn::DataType::Float32); layer->GetOutputSlot(0).SetTensorInfo(outputTensorInfo); inputSlot.Connect(layer->GetInputSlot(0)); return std::make_unique(this, nodeDef, layer); } TensorInfo OutputShapeOfSqueeze(const tensorflow::NodeDef& nodeDef, TensorInfo inputTensorInfo) { BOOST_ASSERT(nodeDef.op() == "Squeeze"); tensorflow::DataType tfDataType = ReadMandatoryNodeTypeAttribute(nodeDef, "T"); DataType type; if (tfDataType == tensorflow::DT_FLOAT) { type = DataType::Float32; } else if (tfDataType == tensorflow::DT_INT32) { type = DataType::Signed32; } else { throw ParseException( boost::str( boost::format("Unsupported DataType %1% for Squeeze operation %2% %3%") % tensorflow::DataType_Name(tfDataType) % nodeDef.name() % CHECK_LOCATION().AsString())); } if (inputTensorInfo.GetNumDimensions() > 4) { throw ParseException( boost::str( boost::format( "Unsupported number of dimensions: %1% for input shape for Squeeze %2% %3%") % inputTensorInfo.GetNumDimensions() % nodeDef.name() % CHECK_LOCATION().AsString())); } std::vector squeezeDims = ReadOptionalNodeUint32ListAttribute(nodeDef, "squeeze_dims"); static const uint32_t dimensionSequence[] = { 0, 1, 2, 3 }; if (squeezeDims.empty()) { squeezeDims.assign(dimensionSequence, dimensionSequence+inputTensorInfo.GetNumDimensions()); } std::vector outputDims; for(unsigned int i = 0; i < inputTensorInfo.GetNumDimensions(); i++) { bool skipSqueeze = (std::find(squeezeDims.begin(), squeezeDims.end(), i) == squeezeDims.end()); auto currentDimension = inputTensorInfo.GetShape()[i]; if (skipSqueeze || currentDimension != 1) { outputDims.push_back(currentDimension); } } if (outputDims.size() > 4) { throw ParseException( boost::str( boost::format( "Unsupported number of dimensions: %1% for output shape for Squeeze %2% %3%") % outputDims.size() % nodeDef.name() % CHECK_LOCATION().AsString())); } TensorShape outShape = TensorShape(static_cast(outputDims.size()), outputDims.data()); TensorInfo outTensorInfo = inputTensorInfo; outTensorInfo.SetShape(outShape); outTensorInfo.SetDataType(type); return outTensorInfo; } ParsedTfOperationPtr TfParser::ParseSqueeze(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 1); IOutputSlot& prevLayerOutputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); TensorInfo inputTensorInfo = prevLayerOutputSlot.GetTensorInfo(); TensorInfo outputInfo; outputInfo = OutputShapeOfSqueeze(nodeDef, inputTensorInfo); ReshapeDescriptor reshapeDesc; reshapeDesc.m_TargetShape = outputInfo.GetShape(); IConnectableLayer* layer = m_Network->AddReshapeLayer(reshapeDesc, nodeDef.name().c_str()); prevLayerOutputSlot.Connect(layer->GetInputSlot(0)); layer->GetOutputSlot(0).SetTensorInfo(outputInfo); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseLrn(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 1); NormalizationDescriptor normalizationDescriptor; normalizationDescriptor.m_NormMethodType = NormalizationAlgorithmMethod::LocalBrightness; normalizationDescriptor.m_NormChannelType = NormalizationAlgorithmChannel::Across; normalizationDescriptor.m_Alpha = ReadMandatoryNodeFloatAttribute(nodeDef, "alpha"); normalizationDescriptor.m_Beta = ReadMandatoryNodeFloatAttribute(nodeDef, "beta"); normalizationDescriptor.m_K = ReadMandatoryNodeFloatAttribute(nodeDef, "bias"); normalizationDescriptor.m_NormSize = ReadMandatoryNodeUint32Attribute(nodeDef, "depth_radius"); normalizationDescriptor.m_DataLayout = armnn::DataLayout::NHWC; // The window size must be an odd value. For a window size of (2 * n + 1), TensorFlow defines depth_radius = n. normalizationDescriptor.m_NormSize = normalizationDescriptor.m_NormSize * 2 + 1; IOutputSlot& prevLayerOutputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); IConnectableLayer* layer = m_Network->AddNormalizationLayer(normalizationDescriptor, nodeDef.name().c_str()); prevLayerOutputSlot.Connect(layer->GetInputSlot(0)); layer->GetOutputSlot(0).SetTensorInfo(prevLayerOutputSlot.GetTensorInfo()); return std::make_unique(this, nodeDef, layer); } /// An ParsedTfOperation for a MatMul node. /// Creation of the armnn FullyConnected layer is deferred until it is actually needed, because /// MatMul nodes are often used for the first part of a biased FullyConnected (MatMul followed /// by Add) and in these cases armnn doesn't need a separate layer for the MatMul. /// class ParsedMatMulTfOperation : public DeferredSingleLayerParsedTfOperation { public: ParsedMatMulTfOperation(TfParser* parser, const tensorflow::NodeDef& node) : DeferredSingleLayerParsedTfOperation(parser, node) { } void CreateLayerDeferred() override { BOOST_ASSERT(m_Layer == nullptr); m_Layer = m_Parser->AddFullyConnectedLayer(m_Node, nullptr, m_Node.name().c_str()); } }; ParsedTfOperationPtr TfParser::ParseMatMul(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { // Defers the creation of the layer (see ParsedMatMulTfOperation). return std::make_unique(this, nodeDef); } ParsedTfOperationPtr TfParser::ParseMean(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); IOutputSlot& inputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); TensorInfo inputTensorInfo = inputSlot.GetTensorInfo(); if (inputs.size() != 2) { throw ParseException( boost::str(boost::format("Mean expects two inputs!. Got %1% for Node %2% %3%") % inputs.size() % nodeDef.name() % CHECK_LOCATION().AsString())); } bool keepDims = ReadMandatoryNodeBoolAttribute(nodeDef, "keep_dims"); ParsedConstTfOperation* axisNode = boost::polymorphic_downcast*>(inputs[1].m_IndexedValue); const TensorInfo& axisTensorInfo = axisNode->GetTensorInfo(); ConstTensor axisTensor(axisTensorInfo, axisNode->GetStorage()); const int* axisData = static_cast(axisTensor.GetMemoryArea()); TensorInfo outputTensorInfo; MeanDescriptor meanDescriptor; meanDescriptor.m_KeepDims = keepDims; // Negative axis values are supported so that the process requires // to convert them into the corresponding positive ones. // Duplicate values are also removed. std::vector rawAxisVector(axisData, axisData + axisTensorInfo.GetNumElements()); std::set positiveAxisSet; int rank = static_cast(inputTensorInfo.GetNumDimensions()); std::transform(rawAxisVector.begin(), rawAxisVector.end(), std::inserter(positiveAxisSet, positiveAxisSet.begin()), [rank](int i) -> unsigned int { return static_cast((i + rank) % rank); }); CalculateReducedOutputTensoInfo(inputTensorInfo, axisTensorInfo, positiveAxisSet, keepDims, outputTensorInfo); if (inputTensorInfo.GetNumDimensions() > positiveAxisSet.size()) { meanDescriptor.m_Axis.assign(positiveAxisSet.begin(), positiveAxisSet.end()); } IConnectableLayer* layer = m_Network->AddMeanLayer(meanDescriptor, nodeDef.name().c_str()); layer->GetOutputSlot(0).SetTensorInfo(outputTensorInfo); inputSlot.Connect(layer->GetInputSlot(0)); return std::make_unique(this, nodeDef, layer); } /// An ParsedTfOperation for a Mul node. /// Creation of the armnn Mul layer is deferred until it is actually needed, because Mul nodes /// are also used for the first part of a leaky relu activation function (Mul followed by Maximum) /// and in these cases armnn doesn't need a separate layer for the Mul. /// class ParsedMulTfOperation : public DeferredSingleLayerParsedTfOperation { public: ParsedMulTfOperation(TfParser* parser, const tensorflow::NodeDef& node) : DeferredSingleLayerParsedTfOperation(parser, node) { } void CreateLayerDeferred() override { BOOST_ASSERT(m_Layer == nullptr); m_Layer = m_Parser->AddMultiplicationLayer(m_Node); } }; ParsedTfOperationPtr TfParser::ParseMul(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { boost::ignore_unused(graphDef); return std::make_unique(this, nodeDef); } ParsedTfOperationPtr TfParser::ParsePlaceholder(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { boost::ignore_unused(graphDef); std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 0); const LayerBindingId layerId = boost::numeric_cast(m_NetworkInputsBindingInfo.size()); auto it = m_InputShapes.find(nodeDef.name()); if (it == m_InputShapes.end()) { throw ParseException( boost::str( boost::format( "Missing input shape for Placeholder '%1%' %2%") % nodeDef.name() % CHECK_LOCATION().AsString())); } TensorInfo tensorInfo(it->second, DataType::Float32); IConnectableLayer* const layer = m_Network->AddInputLayer(layerId, nodeDef.name().c_str()); layer->GetOutputSlot(0).SetTensorInfo(tensorInfo); TrackInputBinding(layer, layerId, tensorInfo); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseRealDiv(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { boost::ignore_unused(graphDef); return AddRealDivLayer(nodeDef); } ParsedTfOperationPtr TfParser::ParseRelu(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { boost::ignore_unused(graphDef); ActivationDescriptor activationDesc; activationDesc.m_Function = ActivationFunction::ReLu; return AddActivationLayer(nodeDef, activationDesc); } ParsedTfOperationPtr TfParser::ParseRelu6(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { boost::ignore_unused(graphDef); ActivationDescriptor activationDesc; activationDesc.m_Function = ActivationFunction::BoundedReLu; activationDesc.m_A = 6.0f; activationDesc.m_B = 0.0f; return AddActivationLayer(nodeDef, activationDesc); } ParsedTfOperationPtr TfParser::ParseSigmoid(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { boost::ignore_unused(graphDef); ActivationDescriptor activationDesc; activationDesc.m_Function = ActivationFunction::Sigmoid; return AddActivationLayer(nodeDef, activationDesc); } ParsedTfOperationPtr TfParser::ParseRsqrt(const tensorflow::NodeDef &nodeDef, const tensorflow::GraphDef &graphDef) { boost::ignore_unused(graphDef); std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 1); IConnectableLayer* const layer = m_Network->AddRsqrtLayer(nodeDef.name().c_str()); IOutputSlot& prevLayerOutputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); prevLayerOutputSlot.Connect(layer->GetInputSlot(0)); layer->GetOutputSlot(0).SetTensorInfo(prevLayerOutputSlot.GetTensorInfo()); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseSoftmax(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { boost::ignore_unused(graphDef); std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 1); SoftmaxDescriptor softmaxDescriptor; IConnectableLayer* const layer = m_Network->AddSoftmaxLayer(softmaxDescriptor, nodeDef.name().c_str()); IOutputSlot& prevLayerSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); prevLayerSlot.Connect(layer->GetInputSlot(0)); layer->GetOutputSlot(0).SetTensorInfo(prevLayerSlot.GetTensorInfo()); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseSplit(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { boost::ignore_unused(graphDef); std::vector nodes = GetTfInputNodes(nodeDef); unsigned int numInputs = static_cast(nodes.size()); std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, numInputs); // Constant tensor index unsigned int index = GetConstInputIndex(inputs); // Get the axis tensor data ParsedConstTfOperation* shapeNode = boost::polymorphic_downcast*>(inputs[index].m_IndexedValue); std::vector axisTensorData; shapeNode->GetConstTensor(axisTensorData); // This splitDim indicates the data format: 3 is the NHWC, 1 is the NCHW. const unsigned int splitDim = static_cast(axisTensorData[0]); // Armnn supports split along the channel dimension for data formats NHWC and NCHW. if (splitDim == 0 || splitDim == 2) { throw armnn::ParseException( boost::str( boost::format( "Dimension %1% for split is not supported by Armnn. " "Node %2% %3%") % splitDim % nodeDef.name() % CHECK_LOCATION().AsString())); } // As Armnn only supports splitter outputs of the same shape, therefore num_splits will be limited to an integer. uint32_t num_split = ReadMandatoryNodeUint32Attribute(nodeDef, "num_or_size_splits"); IOutputSlot& inputSlot = inputs[1 - index].m_IndexedValue->ResolveArmnnOutputSlot(inputs[1 - index].m_Index); TensorInfo inputTensorInfo = inputSlot.GetTensorInfo(); auto inputDimSize = inputTensorInfo.GetNumDimensions(); if (inputDimSize != MaxNumOfTensorDimensions) { throw armnn::ParseException( boost::str( boost::format( "The number of dimensions: %1% for input tensors of the " "split op should be %2% %3%") % inputTensorInfo.GetNumDimensions() % MaxNumOfTensorDimensions % CHECK_LOCATION().AsString())); } std::vector splitterDimSizes(inputDimSize); // Add current input shape to splitterDimSizes for (unsigned int i = 0; i < inputDimSize; ++i) { splitterDimSizes[i] = inputTensorInfo.GetShape()[i]; } if (splitterDimSizes[splitDim] % num_split != 0) { throw ParseException("Number of splits must evenly divide the dimension"); } splitterDimSizes[splitDim] /= num_split; SplitterDescriptor splitDesc(num_split); for (unsigned int g = 0; g < num_split; ++g) { // Set the size of the views. for (unsigned int dimIdx = 0; dimIdx < splitterDimSizes.size(); ++dimIdx) { splitDesc.SetViewSize(g, dimIdx, splitterDimSizes[dimIdx]); } splitDesc.SetViewOriginCoord(g, splitDim, splitterDimSizes[splitDim] * g); } IConnectableLayer *layer = m_Network->AddSplitterLayer(splitDesc, nodeDef.name().c_str()); inputSlot.Connect(layer->GetInputSlot(0)); TensorShape outShape = TensorShape(static_cast(splitterDimSizes.size()), splitterDimSizes.data()); for (unsigned int i = 0; i < layer->GetNumOutputSlots(); ++i) { layer->GetOutputSlot(i).SetTensorInfo(armnn::TensorInfo(outShape, inputTensorInfo.GetDataType())); } return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseSoftplus(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { boost::ignore_unused(graphDef); ActivationDescriptor activationDesc; activationDesc.m_Function = ActivationFunction::SoftReLu; return AddActivationLayer(nodeDef, activationDesc); } ParsedTfOperationPtr TfParser::ParseTanh(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { boost::ignore_unused(graphDef); ActivationDescriptor activationDesc; activationDesc.m_Function = ActivationFunction::TanH; activationDesc.m_A = 1.0f; activationDesc.m_B = 1.0f; return AddActivationLayer(nodeDef, activationDesc); } ParsedTfOperationPtr TfParser::AddActivationLayer(const tensorflow::NodeDef& nodeDef, ActivationDescriptor& activationDesc) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 1); IConnectableLayer* const layer = m_Network->AddActivationLayer(activationDesc, nodeDef.name().c_str()); IOutputSlot& prevLayerOutputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); prevLayerOutputSlot.Connect(layer->GetInputSlot(0)); layer->GetOutputSlot(0).SetTensorInfo(prevLayerOutputSlot.GetTensorInfo()); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::ParseMaxPool(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { return ParsePooling2d(nodeDef, graphDef, PoolingAlgorithm::Max); } ParsedTfOperationPtr TfParser::ParseAvgPool(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { return ParsePooling2d(nodeDef, graphDef, PoolingAlgorithm::Average); } ParsedTfOperationPtr TfParser::ParsePooling2d(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef, PoolingAlgorithm pooltype) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 1); IOutputSlot& inputSlot = inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); TensorInfo inputTensorInfo = inputSlot.GetTensorInfo(); if (inputs.size() != 1) { throw ParseException( boost::str( boost::format( "2D Pooling expects one input!. Got %1% for Node %2% %3%") % inputs.size() % nodeDef.name() % CHECK_LOCATION().AsString())); } std::string paddingString = ReadMandatoryNodeStringAttribute(nodeDef, "padding"); std::string dataFormat = ReadMandatoryNodeStringAttribute(nodeDef, "data_format"); std::vector strides = ReadMandatoryNodeUint32ListAttribute(nodeDef, "strides"); std::vector ksize = ReadMandatoryNodeUint32ListAttribute(nodeDef, "ksize"); // size of pool windows Pooling2dDescriptor pooling2dDescriptor; pooling2dDescriptor.m_PoolType = pooltype; pooling2dDescriptor.m_PaddingMethod = PaddingMethod::Exclude; pooling2dDescriptor.m_OutputShapeRounding = OutputShapeRounding::Floor; CHECK_DATA_FORMAT(nodeDef, dataFormat, "Pooling2D"); DataLayout dataLayout = dataFormat == "NHWC" ? DataLayout::NHWC : DataLayout::NCHW; pooling2dDescriptor.m_DataLayout = dataLayout; DataLayoutIndexed dataLayoutIndexed(dataLayout); pooling2dDescriptor.m_StrideX = strides[dataLayoutIndexed.GetWidthIndex()]; pooling2dDescriptor.m_StrideY = strides[dataLayoutIndexed.GetHeightIndex()]; pooling2dDescriptor.m_PoolWidth = ksize[dataLayoutIndexed.GetWidthIndex()]; pooling2dDescriptor.m_PoolHeight = ksize[dataLayoutIndexed.GetHeightIndex()]; uint32_t inputHeight = inputTensorInfo.GetShape()[dataLayoutIndexed.GetHeightIndex()]; uint32_t inputWidth = inputTensorInfo.GetShape()[dataLayoutIndexed.GetWidthIndex()]; bool padding = false; TensorInfo outputInfo; unsigned int outputHeight = 0; unsigned int outputWidth = 0; CHECK_PADDING_TYPE(nodeDef, paddingString); if (paddingString == "SAME") { padding = true; outputHeight = static_cast(ceil(static_cast(inputHeight) / static_cast(pooling2dDescriptor.m_StrideY))); outputWidth = static_cast(ceil(static_cast(inputWidth) / static_cast(pooling2dDescriptor.m_StrideX))); } else if (paddingString == "VALID") { padding = false; outputHeight = static_cast(ceil( static_cast(inputHeight - pooling2dDescriptor.m_PoolHeight + 1) / static_cast(pooling2dDescriptor.m_StrideY))); outputWidth = static_cast(ceil( static_cast(inputWidth - pooling2dDescriptor.m_PoolWidth + 1) / static_cast(pooling2dDescriptor.m_StrideX))); } switch (dataLayout) { case DataLayout::NHWC: outputInfo = TensorInfo({ inputTensorInfo.GetShape()[0], outputHeight, outputWidth, inputTensorInfo.GetShape()[3] }, DataType::Float32); break; case DataLayout::NCHW: outputInfo = TensorInfo({ inputTensorInfo.GetShape()[0], inputTensorInfo.GetShape()[1], outputHeight, outputWidth }, DataType::Float32); break; } CalcPadding(inputWidth, pooling2dDescriptor.m_PoolWidth, pooling2dDescriptor.m_StrideX, pooling2dDescriptor.m_PadLeft, pooling2dDescriptor.m_PadRight, padding); CalcPadding(inputHeight, pooling2dDescriptor.m_PoolHeight, pooling2dDescriptor.m_StrideY, pooling2dDescriptor.m_PadTop, pooling2dDescriptor.m_PadBottom, padding); IConnectableLayer* layer = m_Network->AddPooling2dLayer(pooling2dDescriptor, nodeDef.name().c_str()); if (layer == nullptr) { throw ParseException( boost::str( boost::format( "Failed to add pooling2d layer for %1% %2%") % nodeDef.name() % CHECK_LOCATION().AsString())); } layer->GetOutputSlot(0).SetTensorInfo(outputInfo); inputSlot.Connect(layer->GetInputSlot(0)); return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::AddAdditionLayer(const tensorflow::NodeDef& nodeDef, bool isBiasAdd) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); IOutputSlot* input0Slot = &inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); IOutputSlot* input1Slot = &inputs[1].m_IndexedValue->ResolveArmnnOutputSlot(inputs[1].m_Index); const TensorInfo& input0Info = input0Slot->GetTensorInfo(); const TensorInfo& input1Info = input1Slot->GetTensorInfo(); if (isBiasAdd) { // BiasAdd takes bias as a 1D tensor. We need to add a reshape layer to create a 4D tensor // with the same data in the correct dimension for broadcast in addition. if(input1Info.GetNumDimensions() != 1) { throw ParseException( boost::str( boost::format( "Unsupported bias for BiasAdd. It should be a 1D vector. " "Got %1% dimensions for input %2%. Node %3% %4%") % input1Info.GetNumDimensions() % inputs[1].m_IndexedValue->GetNode().name() % nodeDef.name() % CHECK_LOCATION().AsString())); } const std::string dataFormat = ReadMandatoryNodeStringAttribute(nodeDef, "data_format"); CHECK_DATA_FORMAT(nodeDef, dataFormat, "BiasAdd"); input1Slot = AddBroadcastReshapeLayer(input0Slot, input1Slot, dataFormat == "NHWC", *m_Network, nodeDef); } else { if (input0Info.GetNumDimensions() == 1) { const bool isNHWC = true; input0Slot = AddBroadcastReshapeLayer(input1Slot, input0Slot, isNHWC, *m_Network, nodeDef); } if (input1Info.GetNumDimensions() == 1) { const bool isNHWC = true; input1Slot = AddBroadcastReshapeLayer(input0Slot, input1Slot, isNHWC, *m_Network, nodeDef); } } IConnectableLayer* const layer = m_Network->AddAdditionLayer(nodeDef.name().c_str()); input0Slot->Connect(layer->GetInputSlot(0)); input1Slot->Connect(layer->GetInputSlot(1)); if (input0Info.GetNumDimensions() == input1Info.GetNumDimensions()) { const TensorShape& input0Shape = input0Info.GetShape(); const TensorShape& input1Shape = input1Info.GetShape(); std::vector outputShape; outputShape.reserve(input0Shape.GetNumDimensions()); TensorInfo outputInfo(input0Info); for (unsigned int i = 0; i < input0Shape.GetNumDimensions(); i++) { outputShape.push_back(std::max(input0Shape[i], input1Shape[i])); } outputInfo.SetShape(TensorShape(input0Shape.GetNumDimensions(), outputShape.data())); layer->GetOutputSlot(0).SetTensorInfo(outputInfo); } else if (input0Info.GetNumDimensions() == 1 && isBiasAdd == false) { layer->GetOutputSlot(0).SetTensorInfo(input1Slot->GetTensorInfo()); } else { layer->GetOutputSlot(0).SetTensorInfo(input0Slot->GetTensorInfo()); } return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::AddRealDivLayer(const tensorflow::NodeDef& nodeDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); IConnectableLayer* const layer = m_Network->AddDivisionLayer(nodeDef.name().c_str()); IOutputSlot* input0Slot = &inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); IOutputSlot* input1Slot = &inputs[1].m_IndexedValue->ResolveArmnnOutputSlot(inputs[1].m_Index); auto const input0NumDims = input0Slot->GetTensorInfo().GetNumDimensions(); auto const input1NumDims = input1Slot->GetTensorInfo().GetNumDimensions(); if (input0NumDims < input1NumDims) { const bool isNHWC = true; input0Slot = AddBroadcastReshapeLayer(input1Slot, input0Slot, isNHWC, *m_Network, nodeDef); } if (input1NumDims < input0NumDims) { const bool isNHWC = true; input1Slot = AddBroadcastReshapeLayer(input0Slot, input1Slot, isNHWC, *m_Network, nodeDef); } input0Slot->Connect(layer->GetInputSlot(0)); input1Slot->Connect(layer->GetInputSlot(1)); if (input0NumDims < input1NumDims) { layer->GetOutputSlot(0).SetTensorInfo(input1Slot->GetTensorInfo()); } else { layer->GetOutputSlot(0).SetTensorInfo(input0Slot->GetTensorInfo()); } return std::make_unique(this, nodeDef, layer); } ParsedTfOperationPtr TfParser::AddMaximumLayer(const tensorflow::NodeDef& nodeDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); IOutputSlot* input0Slot = &inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); IOutputSlot* input1Slot = &inputs[1].m_IndexedValue->ResolveArmnnOutputSlot(inputs[1].m_Index); auto const input0NumDims = input0Slot->GetTensorInfo().GetNumDimensions(); auto const input1NumDims = input1Slot->GetTensorInfo().GetNumDimensions(); if (input0NumDims < input1NumDims) { const bool isNHWC = true; input0Slot = AddBroadcastReshapeLayer(input1Slot, input0Slot, isNHWC, *m_Network, nodeDef); } if (input1NumDims < input0NumDims) { const bool isNHWC = true; input1Slot = AddBroadcastReshapeLayer(input0Slot, input1Slot, isNHWC, *m_Network, nodeDef); } IConnectableLayer* const layer = m_Network->AddMaximumLayer(nodeDef.name().c_str()); input0Slot->Connect(layer->GetInputSlot(0)); input1Slot->Connect(layer->GetInputSlot(1)); TensorInfo outputInfo = input0Slot->GetTensorInfo(); std::vector outputShape; const TensorShape& input0Shape = input0Slot->GetTensorInfo().GetShape(); const TensorShape& input1Shape = input1Slot->GetTensorInfo().GetShape(); for (unsigned int i = 0; i < input0Shape.GetNumDimensions(); i++) { outputShape.push_back(std::max(input0Shape[i], input1Shape[i])); } outputInfo.SetShape(TensorShape(input0Shape.GetNumDimensions(), outputShape.data())); layer->GetOutputSlot(0).SetTensorInfo(outputInfo); return std::make_unique(this, nodeDef, layer); } IConnectableLayer* TfParser::AddMultiplicationLayer(const tensorflow::NodeDef& nodeDef) { std::vector inputs = GetInputParsedTfOperationsChecked(nodeDef, 2); IConnectableLayer* const layer = m_Network->AddMultiplicationLayer(nodeDef.name().c_str()); IOutputSlot* input0Slot = &inputs[0].m_IndexedValue->ResolveArmnnOutputSlot(inputs[0].m_Index); IOutputSlot* input1Slot = &inputs[1].m_IndexedValue->ResolveArmnnOutputSlot(inputs[1].m_Index); auto const input0NumDims = input0Slot->GetTensorInfo().GetNumDimensions(); auto const input1NumDims = input1Slot->GetTensorInfo().GetNumDimensions(); if (input0NumDims < input1NumDims) { const bool isNHWC = true; input0Slot = AddBroadcastReshapeLayer(input1Slot, input0Slot, isNHWC, *m_Network, nodeDef); } if (input1NumDims < input0NumDims) { const bool isNHWC = true; input1Slot = AddBroadcastReshapeLayer(input0Slot, input1Slot, isNHWC, *m_Network, nodeDef); } input0Slot->Connect(layer->GetInputSlot(0)); input1Slot->Connect(layer->GetInputSlot(1)); if (input0NumDims < input1NumDims) { layer->GetOutputSlot(0).SetTensorInfo(input1Slot->GetTensorInfo()); } else { layer->GetOutputSlot(0).SetTensorInfo(input0Slot->GetTensorInfo()); } return layer; } IConnectableLayer* TfParser::AddFullyConnectedLayer(const tensorflow::NodeDef& matMulNodeDef, const tensorflow::NodeDef* addNodeDef, const char* armnnLayerName) { // Finds bias const (if applicable). ParsedConstTfOperation* biasNode = nullptr; if (addNodeDef != nullptr) { std::vector addInputs = GetInputParsedTfOperationsChecked(*addNodeDef, 2); // Finds our inputs. if (HasParsedConstTensor(addInputs[0].m_IndexedValue->GetNode().name())) { biasNode = boost::polymorphic_downcast*>(addInputs[0].m_IndexedValue); } else if (HasParsedConstTensor(addInputs[1].m_IndexedValue->GetNode().name())) { biasNode = boost::polymorphic_downcast*>(addInputs[1].m_IndexedValue); } else { throw ParseException( boost::str( boost::format( "ArmNN only supports fully connected layers with constant bias. " "Inputs %1% and %2%. AddNode %3%. MatMulNode %4% %5%") % addInputs[0].m_IndexedValue->GetNode().name() % addInputs[1].m_IndexedValue->GetNode().name() % addNodeDef->name() % matMulNodeDef.name() % CHECK_LOCATION().AsString())); } } // Finds matmul inputs. ParsedConstTfOperation* weightNode = nullptr; ParsedTfOperation* inputNode = nullptr; unsigned int inputIdx = 0; std::vector mulInputs = GetInputParsedTfOperationsChecked(matMulNodeDef, 2); if (HasParsedConstTensor(mulInputs[0].m_IndexedValue->GetNode().name())) { weightNode = boost::polymorphic_downcast*>(mulInputs[0].m_IndexedValue); inputNode = mulInputs[1].m_IndexedValue; inputIdx = mulInputs[1].m_Index; } else if (HasParsedConstTensor(mulInputs[1].m_IndexedValue->GetNode().name())) { weightNode = boost::polymorphic_downcast*>(mulInputs[1].m_IndexedValue); inputNode = mulInputs[0].m_IndexedValue; inputIdx = mulInputs[0].m_Index; } else { throw ParseException( boost::str( boost::format( "ArmNN only supports fully connected layers with constant weights. " "Inputs %1% and %2%. MatMulNode %3% %4%") % mulInputs[0].m_IndexedValue->GetNode().name() % mulInputs[1].m_IndexedValue->GetNode().name() % matMulNodeDef.name() % CHECK_LOCATION().AsString())); } std::vector weightTensorData; // Handles weight. ConstTensor weights = weightNode->GetConstTensor(weightTensorData); FullyConnectedDescriptor desc; desc.m_BiasEnabled = addNodeDef != nullptr; IConnectableLayer* layer = nullptr; // Makes the layer. if (addNodeDef != nullptr) { std::vector biasTensorData; ConstTensor biases = biasNode->GetConstTensor(biasTensorData); if (weights.GetShape()[1] != biases.GetShape()[0]) { throw ParseException( boost::str( boost::format( "Shape of matmul weights and bias do not match. " "AddNode %1%. MatMulNode %2% %3%") % addNodeDef->name() % matMulNodeDef.name() % CHECK_LOCATION().AsString())); } layer = m_Network->AddFullyConnectedLayer(desc, weights, biases, armnnLayerName); } else { layer = m_Network->AddFullyConnectedLayer(desc, weights, armnnLayerName); } BOOST_ASSERT(layer != nullptr); inputNode->ResolveArmnnOutputSlot(inputIdx).Connect(layer->GetInputSlot(0)); unsigned int batches = inputNode->ResolveArmnnOutputSlot(inputIdx).GetTensorInfo().GetShape()[0]; // Handles output. TensorInfo outputInfo({ batches, weights.GetShape()[1] }, DataType::Float32); layer->GetOutputSlot(0).SetTensorInfo(outputInfo); return layer; } void TfParser::LoadNodeDef(const tensorflow::NodeDef& nodeDef, const tensorflow::GraphDef& graphDef) { // Gets the type of the node (assume float). tensorflow::DataType type = tensorflow::DT_FLOAT; if (nodeDef.attr().count("T") != 0) { auto attr = nodeDef.attr().at("T"); type = attr.type(); } else if (nodeDef.attr().count("dtype") != 0) { auto attr = nodeDef.attr().at("dtype"); type = attr.type(); } if ((type != tensorflow::DT_FLOAT && type != tensorflow::DT_INT32) && nodeDef.op() != "Const") { throw ParseException( boost::str( boost::format( "Currently only FLOAT and INT32 are supported for tensorflow nodes (apart from Const). " "Got %1% for Node %2% %3%") % tensorflow::DataType_Name(type) % nodeDef.name() % CHECK_LOCATION().AsString())); } const std::string& operation = nodeDef.op(); auto itControlInput = std::find(m_ControlInputs.begin(), m_ControlInputs.end(), operation); if (itControlInput != m_ControlInputs.end()) { // We currently allow Control Input from TensorFlow graph but we ignore them from ArmNN graph. return; } auto it = ms_OperationNameToParsingFunctions.find(operation); if (it != ms_OperationNameToParsingFunctions.end()) { auto func = it->second; ParsedTfOperationPtr parsedTfOperation = (this->*func)(nodeDef, graphDef); ParsedTfOperation* parsedTfOperationRaw = parsedTfOperation.get(); // Stores the parsed operation so that dependent layers can connect to it. auto it = m_ParsedTfOperations.find(nodeDef.name()); if (it != m_ParsedTfOperations.end()) { throw ParseException(boost::str(boost::format("Name %1% used by more than one node") % nodeDef.name())); } m_ParsedTfOperations[nodeDef.name()] = std::move(parsedTfOperation); // If this node was requested as an output from the network, then adds an ArmNN output layer. if (std::find(m_RequestedOutputs.begin(), m_RequestedOutputs.end(), nodeDef.name()) != m_RequestedOutputs.end()) { auto outId = ParseOutputId(nodeDef.name()); const LayerBindingId layerId = boost::numeric_cast(m_NetworkOutputsBindingInfo.size()); IOutputSlot& prevSlot = parsedTfOperationRaw->ResolveArmnnOutputSlot(outId.m_Index); TensorInfo tensorInfo = prevSlot.GetTensorInfo(); IConnectableLayer* outputLayer = m_Network->AddOutputLayer(layerId, nodeDef.name().c_str()); prevSlot.Connect(outputLayer->GetInputSlot(0)); TrackOutputBinding(outputLayer, layerId, tensorInfo); } } else { throw ParseException( boost::str( boost::format( "Unsupported operation %1% in tensorflow::GraphDef %2%") % operation % CHECK_LOCATION().AsString())); } } void TfParser::LoadGraphDef(const tensorflow::GraphDef& graphDef) { // Adds all nodes to our map. m_NodesByName.clear(); m_NetworkInputsBindingInfo.clear(); m_NetworkOutputsBindingInfo.clear(); for (int i = 0; i < graphDef.node_size(); ++i) { const tensorflow::NodeDef& node = graphDef.node(i); m_NodesByName[node.name()] = &node; } // Finds the output nodes the user requested. std::vector targetNodes; for (const std::string& requestedOutputName : m_RequestedOutputs) { auto nodeIt = m_NodesByName.find(requestedOutputName); if (nodeIt == m_NodesByName.end()) { throw ParseException( boost::str( boost::format( "Couldn't find requested output node '%1%' in graph %2%") % requestedOutputName % CHECK_LOCATION().AsString())); } targetNodes.push_back(nodeIt->second); } // Sorts them into a linear ordering such that all inputs of a node are before the node itself. std::vector sortedNodes; if (!armnnUtils::GraphTopologicalSort( targetNodes, [this](const tensorflow::NodeDef* node) { auto outputs = GetTfInputNodes(*node); std::vector nodesOnly; for (const auto & o : outputs) { nodesOnly.push_back(o.m_IndexedValue); } return nodesOnly; }, sortedNodes)) { throw ParseException( boost::str( boost::format( "Cycle detected in graph %1%") % CHECK_LOCATION().AsString())); } // Parses each node in order, knowing that all inputs of a node will be processed before the node itself. for (const auto& it : sortedNodes) { const tensorflow::NodeDef& currentNode = *it; LoadNodeDef(currentNode, graphDef); } } INetworkPtr TfParser::CreateNetworkFromTextFile(const char* graphFile, const std::map& inputShapes, const std::vector& requestedOutputs) { FILE* fd = fopen(graphFile, "r"); if (fd == nullptr) { throw FileNotFoundException( boost::str( boost::format( "Graph file %1% failed to open %2%") % graphFile % CHECK_LOCATION().AsString())); } // Parses the file into a message. tensorflow::GraphDef graphDef; auto input = new google::protobuf::io::FileInputStream(fileno(fd)); bool success = google::protobuf::TextFormat::Parse(input, &graphDef); delete input; fclose(fd); if (!success) { throw ParseException( boost::str( boost::format( "Failed to parse graph file %1%") % CHECK_LOCATION().AsString())); } return CreateNetworkFromGraphDef(graphDef, inputShapes, requestedOutputs); } INetworkPtr TfParser::CreateNetworkFromString(const char* protoText, const std::map& inputShapes, const std::vector& requestedOutputs) { // Parses the string into a message. tensorflow::GraphDef graphDef; bool success = google::protobuf::TextFormat::ParseFromString(protoText, &graphDef); if (!success) { throw ParseException( boost::str( boost::format( "Failed to parse graph file %1%") % CHECK_LOCATION().AsString())); } return CreateNetworkFromGraphDef(graphDef, inputShapes, requestedOutputs); } INetworkPtr TfParser::CreateNetworkFromBinaryFile(const char* graphFile, const std::map& inputShapes, const std::vector& requestedOutputs) { FILE* fd = fopen(graphFile, "rb"); if (fd == nullptr) { throw FileNotFoundException( boost::str( boost::format( "Graph file %1% failed to open %2%") % graphFile % CHECK_LOCATION().AsString())); } // Parses the file into a message. tensorflow::GraphDef graphDef; google::protobuf::io::FileInputStream inStream(fileno(fd)); google::protobuf::io::CodedInputStream codedStream(&inStream); codedStream.SetTotalBytesLimit(INT_MAX, INT_MAX); bool success = graphDef.ParseFromCodedStream(&codedStream); fclose(fd); if (!success) { throw ParseException( boost::str( boost::format( "Failed to parse protobuf file %1% %2%") % graphFile % CHECK_LOCATION().AsString())); } return CreateNetworkFromGraphDef(graphDef, inputShapes, requestedOutputs); } INetworkPtr TfParser::CreateNetworkFromGraphDef(const tensorflow::GraphDef& graphDef, const std::map& inputShapes, const std::vector& requestedOutputs) { m_Network = INetwork::Create(); m_InputShapes = inputShapes; if (requestedOutputs.size() == 0) { throw ParseException( boost::str( boost::format( "requestedOutputs must have at least one entry %1%") % CHECK_LOCATION().AsString())); } m_RequestedOutputs = requestedOutputs; try { LoadGraphDef(graphDef); } catch (const ParseException& e) { Cleanup(); throw e; } Cleanup(); return std::move(m_Network); } void TfParser::Cleanup() { // Cleanup, in case we reuse this parser. m_InputShapes.clear(); m_RequestedOutputs.clear(); m_NodesByName.clear(); m_ParsedTfOperations.clear(); } BindingPointInfo TfParser::GetNetworkInputBindingInfo(const std::string& name) const { return GetBindingInfo(name, "input", m_NetworkInputsBindingInfo); } BindingPointInfo TfParser::GetNetworkOutputBindingInfo(const std::string& name) const { return GetBindingInfo(name, "output", m_NetworkOutputsBindingInfo); } std::pair TfParser::GetBindingInfo(const std::string& layerName, const char* bindingPointDesc, const std::unordered_map& nameToBindingInfo) { auto it = nameToBindingInfo.find(layerName); if (it == nameToBindingInfo.end()) { throw InvalidArgumentException( boost::str( boost::format( "Unknown %1% '%2%' %3%") % bindingPointDesc % layerName % CHECK_LOCATION().AsString())); } return it->second; } void TfParser::TrackInputBinding(IConnectableLayer* layer, LayerBindingId id, const TensorInfo& tensorInfo) { return TrackBindingPoint(layer, id, tensorInfo, "input", m_NetworkInputsBindingInfo); } void TfParser::TrackOutputBinding(IConnectableLayer* layer, LayerBindingId id, const TensorInfo& tensorInfo) { return TrackBindingPoint(layer, id, tensorInfo, "output", m_NetworkOutputsBindingInfo); } void TfParser::TrackBindingPoint(IConnectableLayer* layer, LayerBindingId id, const TensorInfo& tensorInfo, const char* bindingPointDesc, std::unordered_map& nameToBindingInfo) { const std::string layerName = layer->GetName(); auto it = nameToBindingInfo.find(layerName); if (it == nameToBindingInfo.end()) { nameToBindingInfo[layerName] = std::make_pair(id, tensorInfo); } else { throw ParseException( boost::str( boost::format( "Id %1% used by more than one %2% layer %3%") % id % bindingPointDesc % CHECK_LOCATION().AsString())); } } } // namespace armnnTfParser