Release 18.08
diff --git a/src/armnnCaffeParser/CaffeParser.cpp b/src/armnnCaffeParser/CaffeParser.cpp
index 254a819..35f4589 100644
--- a/src/armnnCaffeParser/CaffeParser.cpp
+++ b/src/armnnCaffeParser/CaffeParser.cpp
@@ -3,6 +3,7 @@
 // See LICENSE file in the project root for full license information.
 //
 #include "CaffeParser.hpp"
+#include "RecordByRecordCaffeParser.hpp"
 
 #include "armnn/Descriptors.hpp"
 #include "armnn/INetwork.hpp"
@@ -10,6 +11,7 @@
 #include "armnn/Exceptions.hpp"
 
 #include "GraphTopologicalSort.hpp"
+#include "VerificationHelpers.hpp"
 
 #include <boost/numeric/conversion/cast.hpp>
 #include <boost/assert.hpp>
@@ -43,7 +45,8 @@
 /// This contains a flat list of Caffe 'layers' (e.g. convolution, pooling etc.).
 /// Each layer has inputs (called "bottoms") and outputs (called "tops"). Data flows from bottom to top.
 /// The bottoms of a layer refer to the tops of other layers, not their names.
-/// The names of layers seem to be arbitrary (you could rename a layer and the network wouldn't need any other changes).
+/// The names of layers seem to be arbitrary (you could rename a layer and the network wouldn't
+/// need any other changes).
 ///
 /// Some layers (e.g. Relu) can be configured so that their top and bottom are both the same. This is called an
 /// "in-place" layer and is a Caffe runtime feature used to reduce memory usage by modifying tensors in-place.
@@ -58,63 +61,65 @@
 using namespace std;
 using namespace google::protobuf::io;
 
-const std::map<std::string, CaffeParser::OperationParsingFunction> CaffeParser::ms_CaffeLayerNameToParsingFunctions = {
-    { "Input",        &CaffeParser::ParseInputLayer },
-    { "Convolution",  &CaffeParser::ParseConvLayer },
-    { "Pooling",      &CaffeParser::ParsePoolingLayer },
-    { "ReLU",         &CaffeParser::ParseReluLayer },
-    { "LRN",          &CaffeParser::ParseLRNLayer },
-    { "InnerProduct", &CaffeParser::ParseInnerProductLayer },
-    { "Softmax",      &CaffeParser::ParseSoftmaxLayer },
-    { "Eltwise",      &CaffeParser::ParseEltwiseLayer },
-    { "Concat",       &CaffeParser::ParseConcatLayer },
-    { "BatchNorm",    &CaffeParser::ParseBatchNormLayer },
-    { "Scale",        &CaffeParser::ParseScaleLayer },
-    { "Split",        &CaffeParser::ParseSplitLayer },
-    { "Dropout",      &CaffeParser::ParseDropoutLayer},
-};
-
-ICaffeParser* ICaffeParser::CreateRaw()
-{
-    return new CaffeParser();
-}
-
-ICaffeParserPtr ICaffeParser::Create()
-{
-    return ICaffeParserPtr(CreateRaw(), &ICaffeParser::Destroy);
-}
-
-void ICaffeParser::Destroy(ICaffeParser* parser)
-{
-    delete parser;
-}
-
-CaffeParser::CaffeParser()
-: m_Network(nullptr, nullptr)
+namespace
 {
 
-}
-
-void GetDataFromBlob(const LayerParameter& layerParam, vector<float>& outData, unsigned int blobIndex)
+const float* GetArrayPtrFromBlob(const LayerParameter& layerParam, unsigned int blobIndex)
 {
-    if (blobIndex >= boost::numeric_cast<unsigned int>(layerParam.blobs_size()))
+    auto nBlobs = layerParam.blobs_size();
+    if (blobIndex >= boost::numeric_cast<unsigned int>(nBlobs))
     {
-        throw ParseException(boost::str(boost::format("Expected data blob at index %1% in layer %2% not found")
-            % blobIndex % layerParam.name()));
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Expected data blob at index %1% in layer %2% not found. nBlobs=%2%. %4%") %
+                    blobIndex %
+                    layerParam.name() %
+                    nBlobs %
+                    CHECK_LOCATION().AsString()));
     }
 
     const BlobProto& blob = layerParam.blobs(boost::numeric_cast<int>(blobIndex));
 
-    if (boost::numeric_cast<size_t>(blob.data_size()) != outData.size())
+    const float* arrayPtr = blob.data().data();
+    return arrayPtr;
+}
+
+void GetDataFromBlob(const LayerParameter& layerParam, vector<float>& outData, unsigned int blobIndex)
+{
+    auto nBlobs = layerParam.blobs_size();
+    if (blobIndex >= boost::numeric_cast<unsigned int>(nBlobs))
     {
-        throw ParseException(boost::str(boost::format(
-            "Data blob at index %1% in layer %2% has an unexpected size. Expected %3% elements but got %4% elements")
-            % blobIndex % layerParam.name() % outData.size() % blob.data_size()));
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Expected data blob at index %1% in layer %2% not found. %3%") %
+                    blobIndex %
+                    layerParam.name() %
+                    CHECK_LOCATION().AsString()));
     }
 
-    for (unsigned int i = 0; i < outData.size(); ++i)
+    const BlobProto& blob = layerParam.blobs(boost::numeric_cast<int>(blobIndex));
+
+    size_t blobSize = boost::numeric_cast<size_t>(blob.data_size());
+    if (blobSize != outData.size())
     {
-        outData[i] = blob.data(boost::numeric_cast<int>(i));
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Data blob at index %1% in layer %2% has an unexpected size. "
+                    "Expected %3% elements but got %4% elements. %5%") %
+                    blobIndex %
+                    layerParam.name() %
+                    outData.size() %
+                    blobSize %
+                    CHECK_LOCATION().AsString()));
+    }
+
+    int outSizeInt = boost::numeric_cast<int>(outData.size());
+    for (int i = 0; i < outSizeInt; ++i)
+    {
+        outData[static_cast<size_t>(i)] = blob.data(i);
     }
 }
 
@@ -136,39 +141,213 @@
     int numInputsActual = layerParameter.bottom_size();
     if (numInputs != boost::numeric_cast<unsigned int>(numInputsActual))
     {
-        throw ParseException("Loading layer: invalid number of inputs");
+        throw ParseException(
+            boost::str(
+                boost::format("Invalid number of inputs requested %1% for layer %2% "
+                              "while only %3% present. %4%") %
+                              numInputs %
+                              layerParameter.name() %
+                              numInputsActual %
+                              CHECK_LOCATION().AsString()));
     }
 
     int numOutputsActual = layerParameter.top_size();
     if (numOutputs != boost::numeric_cast<unsigned int>(numOutputsActual))
     {
-        throw ParseException("Loading layer: invalid number of outputs");
+        throw ParseException(
+            boost::str(
+                boost::format("Invalid number of outputs requested %1% for layer %2% "
+                              "while only %3% present. %4%") %
+                              numOutputs %
+                              layerParameter.name() %
+                              numOutputsActual %
+                              CHECK_LOCATION().AsString()));
     }
 }
 
-BindingPointInfo CaffeParser::GetNetworkInputBindingInfo(const std::string& name) const
+template <typename ParamType, typename ExtractOptional, typename ExtractFallback, typename ValueType>
+ValueType GetOptionalWithFallback(const ParamType& param,
+                                  ExtractOptional extractOptional,
+                                  ExtractFallback extractFallback,
+                                  ValueType defaultValue)
+{
+    auto optValue = extractOptional(param, defaultValue);
+    if (optValue.first)
+    {
+        return optValue.second;
+    }
+    auto fallbackValue = extractFallback(param, defaultValue);
+    return fallbackValue.second;
+}
+
+#define GET_OPTIONAL_WITH_VECTOR_FALLBACK(PARAM, \
+                                          PARAM_TYPE, \
+                                          OPTIONAL_VALUE, \
+                                          FALLBACK_VECTOR, \
+                                          VALUE_TYPE, \
+                                          DEFAULT_VALUE) \
+    GetOptionalWithFallback( \
+        PARAM, \
+        [](const PARAM_TYPE & param, VALUE_TYPE defaultValue) \
+        { \
+            if (param.has_##OPTIONAL_VALUE ()) \
+            { \
+                return std::make_pair(true, param.OPTIONAL_VALUE ()); \
+            } \
+            else \
+            { \
+                return std::make_pair(false, defaultValue); \
+            } \
+        }, \
+        [](const PARAM_TYPE & param, VALUE_TYPE defaultValue) \
+        { \
+            if (param.FALLBACK_VECTOR##_size() > 0) \
+            { \
+                return std::make_pair(true, (param.FALLBACK_VECTOR ()).Get(0)); \
+            } \
+            else \
+            { \
+                return std::make_pair(false, defaultValue); \
+            } \
+        }, \
+        DEFAULT_VALUE)
+
+#define GET_OPTIONAL_WITH_FALLBACK(PARAM, \
+                                   PARAM_TYPE, \
+                                   OPTIONAL_VALUE, \
+                                   FALLBACK_VALUE, \
+                                   VALUE_TYPE, \
+                                   DEFAULT_VALUE) \
+    GetOptionalWithFallback( \
+        PARAM, \
+        [](const PARAM_TYPE & param, VALUE_TYPE defaultValue) \
+        { \
+            if (param.has_##OPTIONAL_VALUE ()) \
+            { \
+                return std::make_pair(true, param.OPTIONAL_VALUE ()); \
+            } \
+            else \
+            { \
+                return std::make_pair(false, defaultValue); \
+            } \
+        }, \
+        [](const PARAM_TYPE & param, VALUE_TYPE defaultValue) \
+        { \
+            if (param.has_##FALLBACK_VALUE ()) \
+            { \
+                return std::make_pair(true, param.FALLBACK_VALUE ()); \
+            } \
+            else \
+            { \
+                return std::make_pair(false, defaultValue); \
+            } \
+        }, \
+        DEFAULT_VALUE)
+
+
+void ValidateEqualValuesInRange(unsigned int valueA,
+                                const char* valueNameA,
+                                unsigned int valueB,
+                                const char* valueNameB,
+                                unsigned int min,
+                                unsigned int max,
+                                const armnn::CheckLocation& location)
+{
+    if (!IsInRange(valueA, min, max) || !IsInRange(valueB, min, max) || (valueA != valueB))
+    {
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "%1%=%2% and %3%=%4% must be equal and within the valid range"
+                    "of [%5%, %6%] %7%") %
+                    valueNameA %
+                    valueA %
+                    valueNameB %
+                    valueB %
+                    min %
+                    max %
+                    location.AsString()));
+    }
+}
+
+#define VALIDATE_EQUAL_VALUES_IN_RANGE(A, B, MIN_RANGE, MAX_RANGE) \
+    ValidateEqualValuesInRange(A, #A, B, #B, MIN_RANGE, MAX_RANGE, CHECK_LOCATION())
+
+} // namespace <anonymous>
+
+const std::map<std::string, CaffeParserBase::OperationParsingFunction>
+    CaffeParserBase::ms_CaffeLayerNameToParsingFunctions = {
+    { "Input",        &CaffeParserBase::ParseInputLayer },
+    { "Convolution",  &CaffeParserBase::ParseConvLayer },
+    { "Pooling",      &CaffeParserBase::ParsePoolingLayer },
+    { "ReLU",         &CaffeParserBase::ParseReluLayer },
+    { "LRN",          &CaffeParserBase::ParseLRNLayer },
+    { "InnerProduct", &CaffeParserBase::ParseInnerProductLayer },
+    { "Softmax",      &CaffeParserBase::ParseSoftmaxLayer },
+    { "Eltwise",      &CaffeParserBase::ParseEltwiseLayer },
+    { "Concat",       &CaffeParserBase::ParseConcatLayer },
+    { "BatchNorm",    &CaffeParserBase::ParseBatchNormLayer },
+    { "Scale",        &CaffeParserBase::ParseScaleLayer },
+    { "Split",        &CaffeParserBase::ParseSplitLayer },
+    { "Dropout",      &CaffeParserBase::ParseDropoutLayer},
+};
+
+ICaffeParser* ICaffeParser::CreateRaw()
+{
+    return new RecordByRecordCaffeParser();
+}
+
+ICaffeParserPtr ICaffeParser::Create()
+{
+    return ICaffeParserPtr(CreateRaw(), &ICaffeParser::Destroy);
+}
+
+void ICaffeParser::Destroy(ICaffeParser* parser)
+{
+    delete parser;
+}
+
+CaffeParserBase::CaffeParserBase()
+    : m_Network(nullptr, nullptr)
+{
+
+}
+
+CaffeParser::CaffeParser()
+: CaffeParserBase()
+{
+
+}
+
+BindingPointInfo CaffeParserBase::GetNetworkInputBindingInfo(const std::string& name) const
 {
     return GetBindingInfo(name, "input", m_NetworkInputsBindingInfo);
 }
 
-BindingPointInfo CaffeParser::GetNetworkOutputBindingInfo(const std::string& name) const
+BindingPointInfo CaffeParserBase::GetNetworkOutputBindingInfo(const std::string& name) const
 {
     return GetBindingInfo(name, "output", m_NetworkOutputsBindingInfo);
 }
 
-std::pair<armnn::LayerBindingId, armnn::TensorInfo> CaffeParser::GetBindingInfo(const std::string& layerName,
+std::pair<armnn::LayerBindingId, armnn::TensorInfo> CaffeParserBase::GetBindingInfo(const std::string& layerName,
     const char* bindingPointDesc,
     const std::unordered_map<std::string, BindingPointInfo>& nameToBindingInfo)
 {
     auto it = nameToBindingInfo.find(layerName);
     if (it == nameToBindingInfo.end())
     {
-        throw InvalidArgumentException(boost::str(boost::format("Unknown %1% '%2%'") % bindingPointDesc % layerName));
+        throw InvalidArgumentException(
+            boost::str(
+                boost::format(
+                    "Unknown binding %1% for layer '%2%'. %3%") %
+                    bindingPointDesc %
+                    layerName %
+                    CHECK_LOCATION().AsString()));
     }
     return it->second;
 }
 
-TensorInfo CaffeParser::BlobShapeToTensorInfo(const caffe::BlobShape& blobShape) const
+TensorInfo CaffeParserBase::BlobShapeToTensorInfo(const caffe::BlobShape& blobShape) const
 {
     std::vector<unsigned int> shape;
     for (int j = 0; j < blobShape.dim_size(); ++j)
@@ -191,7 +370,9 @@
     return ret;
 }
 
-vector<const LayerParameter*> CaffeParser::GetInputs(const LayerParameter& layerParam)
+// Note: can move to CaffeParser when/if we optimise the text/string format
+//       to load on a layer by layer basis
+vector<const LayerParameter*> CaffeParserBase::GetInputs(const LayerParameter& layerParam)
 {
     std::vector<const caffe::LayerParameter*> ret;
     ret.reserve(boost::numeric_cast<size_t>(layerParam.bottom_size()));
@@ -202,8 +383,13 @@
         if (inputIt == m_CaffeLayersByTopName.end())
         {
             throw ParseException(
-                "Can't find Caffe layer with top called '" + inputName + "', which is listed as an input of '" +
-                layerParam.name() + "'");
+                boost::str(
+                    boost::format(
+                        "Can't find Caffe layer with top called '%1%', "
+                        "which is listed as an input of '%2%'. %3%") %
+                        inputName %
+                        layerParam.name() %
+                        CHECK_LOCATION().AsString()));
         }
         ret.push_back(inputIt->second);
     }
@@ -211,17 +397,18 @@
     return ret;
 }
 
-void CaffeParser::ParseInputLayer(const LayerParameter& layerParam)
+void CaffeParserBase::ParseInputLayer(const LayerParameter& layerParam)
 {
     BOOST_ASSERT(layerParam.type() == "Input");
     ValidateNumInputsOutputs(layerParam, 0, 1);
 
     const InputParameter& param = layerParam.input_param();
 
-    const armnn::LayerBindingId inputId = boost::numeric_cast<armnn::LayerBindingId>(m_NetworkInputsBindingInfo.size());
+    const armnn::LayerBindingId inputId = boost::numeric_cast<armnn::LayerBindingId>(
+        m_NetworkInputsBindingInfo.size());
     armnn::IConnectableLayer* const inputLayer = m_Network->AddInputLayer(inputId, layerParam.name().c_str());
 
-    // Decide on the tensor info for this input. This can be specified in the Caffe network but can also
+    // Decides the tensor info for this input. This can be specified in the Caffe network but can also
     // be overriden by user input (m_inputShapes).
     armnn::TensorInfo inputTensorInfo;
 
@@ -241,15 +428,23 @@
               || originalShape->dim(2) != overrideShape[2]
               || originalShape->dim(3) != overrideShape[3]))
         {
-            throw ParseException("Parsed input shape for '" + layerParam.name() +
-                "' is incompatible with the override provided");
+            throw ParseException(
+                boost::str(
+                    boost::format(
+                        "Parsed input shape for '%1%' is incompatible with the override provided. %2%") %
+                        layerParam.name() %
+                        CHECK_LOCATION().AsString()));
         }
         inputTensorInfo.SetShape(overrideShape);
     }
     else if (!originalShape)
     {
-        throw ParseException("No input descriptor given for '" + layerParam.name() +
-            "' and no input shape found in caffe model");
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "No input descriptor given for '%1%' and no input shape found in caffe model. %2%") %
+                    layerParam.name() %
+                    CHECK_LOCATION().AsString()));
     }
 
     TrackInputBinding(inputLayer, inputId, inputTensorInfo);
@@ -257,191 +452,110 @@
     SetArmnnOutputSlotForCaffeTop(layerParam.top(0), inputLayer->GetOutputSlot(0));
 }
 
-void CaffeParser::ParseConvLayer(const LayerParameter& layerParam)
+void CaffeParserBase::AddConvLayerWithSplits(const caffe::LayerParameter& layerParam,
+                                             const armnn::Convolution2dDescriptor& desc,
+                                             unsigned int kernelW,
+                                             unsigned int kernelH)
 {
     BOOST_ASSERT(layerParam.type() == "Convolution");
     ValidateNumInputsOutputs(layerParam, 1, 1);
 
-    ConvolutionParameter convParam      = layerParam.convolution_param();
+    ConvolutionParameter convParam = layerParam.convolution_param();
     BlobShape inputShape = TensorDescToBlobShape(GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0)).GetTensorInfo());
+    const unsigned int numGroups = convParam.has_group() ? convParam.group() : 1;
 
-    unsigned int kernelH = 0;
-    unsigned int kernelW = 0;
-    if (convParam.has_kernel_h() && convParam.has_kernel_w())
-    {
-        kernelH = convParam.kernel_h();
-        kernelW = convParam.kernel_w();
-    }
-    else if (convParam.kernel_size_size() > 0)
-    {
-        kernelH = (convParam.kernel_size()).Get(0);
-        kernelW = (convParam.kernel_size()).Get(0);
-    }
-    else
-    {
-        throw ParseException("Loading Convolution Layer: Kernel Size defined Illegally");
-    }
-
-    if (!IsInRange(kernelH, 0, 11) || !IsInRange(kernelW, 0, 11) || (kernelH != kernelW))
-    {
-        throw ParseException("Loading Convolution Layer: Kernel has invalid size");
-    }
-
-    unsigned int strideH = 0;
-    unsigned int strideW = 0;
-
-    if (convParam.has_stride_h() && convParam.has_stride_w())
-    {
-        strideH = convParam.stride_h();
-        strideW = convParam.stride_w();
-    }
-    else if (convParam.stride_size() > 0)
-    {
-        strideH = (convParam.stride()).Get(0);
-        strideW = (convParam.stride()).Get(0);
-    }
-    else
-    {
-        // Caffe stride default is 1
-        strideH = strideW = 1;
-    }
-
-    if (!IsInRange(strideH, 0, 11) || !IsInRange(strideW, 0, 11) || (strideH != strideW))
-    {
-        throw ParseException("Loading Convolution Layer: stride has invalid size");
-    }
-
-    unsigned int padH = 0;
-    unsigned int padW = 0;
-
-    if (convParam.has_pad_h() && convParam.has_pad_w())
-    {
-        padH = convParam.pad_h();
-        padW = convParam.pad_w();
-    }
-    else if (convParam.pad_size() > 0)
-    {
-        padH = (convParam.pad()).Get(0);
-        padW = (convParam.pad()).Get(0);
-    }
-    else
-    {
-        padH = 0;
-        padW = 0;
-    }
-
-    if (!IsInRange(padH, 0, 11) || !IsInRange(padW, 0, 11) || (padH != padW))
-    {
-        throw ParseException("Loading Convolution Layer: pad has invalid size");
-    }
+    // asusme these were already verified by the caller ParseConvLayer() function
+    BOOST_ASSERT(numGroups < inputShape.dim(1));
+    BOOST_ASSERT(numGroups > 1);
 
     // Handle grouping
-    const unsigned int numGroups = convParam.has_group() ? convParam.group() : 1;
     armnn::IOutputSlot& inputConnection = GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0));
 
     vector<string> convLayerNames(numGroups);
     vector<armnn::IConnectableLayer*> convLayers(numGroups);
     convLayerNames[0] = layerParam.name();
 
-    armnn::IConnectableLayer* splitterLayer = nullptr;
-    if (numGroups > 1)
+    // This convolution is to be applied to chunks of the input data so add a splitter layer
+
+    // Redirect the convolution input to the splitter
+    unsigned int splitterDimSizes[4] = {static_cast<unsigned int>(inputShape.dim(0)),
+                                        static_cast<unsigned int>(inputShape.dim(1)),
+                                        static_cast<unsigned int>(inputShape.dim(2)),
+                                        static_cast<unsigned int>(inputShape.dim(3))};
+
+    // Split dimension 1 of the splitter output shape and conv input shapes
+    // according to the number of groups
+
+    splitterDimSizes[1] /= numGroups;
+    inputShape.set_dim(1, splitterDimSizes[1]);
+
+    // This is used to describe how the input is to be split
+    ViewsDescriptor splitterDesc(numGroups);
+
+    // Create an output node for each group, giving each a unique name
+    for (unsigned int g = 0; g < numGroups; ++g)
     {
-        // This convolution is to be applied to chunks of the input data so add a splitter layer
+        // Work out the names of the splitter layers child convolutions
+        stringstream ss;
+        ss << layerParam.name() << "_" << g;
+        convLayerNames[g] = ss.str();
 
-        // Redirect the convolution input to the splitter
-        unsigned int splitterDimSizes[4] = {static_cast<unsigned int>(inputShape.dim(0)),
-                                            static_cast<unsigned int>(inputShape.dim(1)),
-                                            static_cast<unsigned int>(inputShape.dim(2)),
-                                            static_cast<unsigned int>(inputShape.dim(3))};
+        splitterDesc.SetViewOriginCoord(g, 1, splitterDimSizes[1] * g);
 
-        // Split dimension 1 of the splitter output shape and conv input shapes
-        // according to the number of groups
-        splitterDimSizes[1] /= numGroups;
-        inputShape.set_dim(1, splitterDimSizes[1]);
-
-        // This is used to describe how the input is to be split
-        ViewsDescriptor splitterDesc(numGroups);
-
-        // Create an output node for each group, giving each a unique name
-        for (unsigned int g = 0; g < numGroups; ++g)
+        // Set the size of the views.
+        for (unsigned int dimIdx=0; dimIdx < 4; dimIdx++)
         {
-            // Work out the names of the splitter layers child convolutions
-            stringstream ss;
-            ss << layerParam.name() << "_" << g;
-            convLayerNames[g] = ss.str();
-
-            splitterDesc.SetViewOriginCoord(g, 1, splitterDimSizes[1] * g);
-
-            // Set the size of the views.
-            for (unsigned int dimIdx=0; dimIdx < 4; dimIdx++)
-            {
-                splitterDesc.SetViewSize(g, dimIdx, splitterDimSizes[dimIdx]);
-            }
-        }
-
-        const std::string splitterLayerName = std::string("splitter_") + layerParam.bottom(0);
-
-        // Add the splitter layer
-        splitterLayer = m_Network->AddSplitterLayer(splitterDesc,
-            splitterLayerName.c_str());
-
-        inputConnection.Connect(splitterLayer->GetInputSlot(0));
-        for (unsigned int i = 0; i < splitterLayer->GetNumOutputSlots(); i++)
-        {
-            splitterLayer->GetOutputSlot(i).SetTensorInfo(BlobShapeToTensorInfo(inputShape));
+            splitterDesc.SetViewSize(g, dimIdx, splitterDimSizes[dimIdx]);
         }
     }
 
-    // Ignored Caffe Parameters
-    // * Dilation Size
-    // * Weight Filler
-    // * Bias Filler
-    // * Engine
-    // * Force nd_im2col
-    // * Axis
+    const std::string splitterLayerName = std::string("splitter_") + layerParam.bottom(0);
+    armnn::IConnectableLayer* splitterLayer = m_Network->AddSplitterLayer(splitterDesc, splitterLayerName.c_str());
 
-    // Not Available ArmNN Interface Parameters
-    // * Rounding policy;
-
-    Convolution2dDescriptor convolution2dDescriptor;
-    convolution2dDescriptor.m_PadLeft        = padW;
-    convolution2dDescriptor.m_PadRight       = padW;
-    convolution2dDescriptor.m_PadTop         = padH;
-    convolution2dDescriptor.m_PadBottom      = padH;
-    convolution2dDescriptor.m_StrideX        = strideW;
-    convolution2dDescriptor.m_StrideY        = strideH;
+    inputConnection.Connect(splitterLayer->GetInputSlot(0));
+    for (unsigned int i = 0; i < splitterLayer->GetNumOutputSlots(); i++)
+    {
+        splitterLayer->GetOutputSlot(i).SetTensorInfo(BlobShapeToTensorInfo(inputShape));
+    }
 
     unsigned int numFilters = convParam.num_output();
 
-    // Populate convolution output tensor descriptor dimensions
+    // Populates convolution output tensor descriptor dimensions.
     BlobShape outputShape;
     outputShape.add_dim(0);
     outputShape.set_dim(0, inputShape.dim(0));
     outputShape.add_dim(1);
-    // Ensure that dimension 1 of the convolution output is split according to the number of groups.
+    // Ensures that dimension 1 of the convolution output is split according to the number of groups.
     outputShape.set_dim(1, numFilters / numGroups);
     outputShape.add_dim(2);
     outputShape.set_dim(
-        2, (static_cast<int>(static_cast<float>(inputShape.dim(2) + 2 * padH - kernelH) /
-            boost::numeric_cast<float>(strideH)) + 1));
+        2, (static_cast<int>(
+                static_cast<float>(inputShape.dim(2) + 2 * desc.m_PadBottom - kernelH) /
+                static_cast<float>(desc.m_StrideY)) + 1));
     outputShape.add_dim(3);
     outputShape.set_dim(
-        3, (static_cast<int>(static_cast<float>(inputShape.dim(3) + 2 * padW - kernelW) /
-            boost::numeric_cast<float>(strideW)) + 1));
+        3, (static_cast<int>(
+                static_cast<float>(inputShape.dim(3) + 2 * desc.m_PadRight - kernelW) /
+                static_cast<float>(desc.m_StrideX)) + 1));
 
     // Load the weight data for ALL groups
-    vector<float> weightData(boost::numeric_cast<size_t>(numGroups * inputShape.dim(1) * outputShape.dim(1) *
-        kernelH * kernelW));
+    vector<float> weightData(boost::numeric_cast<size_t>(numGroups *
+                                                         inputShape.dim(1) *  // number of input channels
+                                                         outputShape.dim(1) * // number of output channels
+                                                         kernelH *
+                                                         kernelW));
     GetDataFromBlob(layerParam, weightData, 0);
 
     const unsigned int weightDimSizes[4] = {
-        static_cast<unsigned int>(outputShape.dim(1)), static_cast<unsigned int>(inputShape.dim(1)), kernelH, kernelW};
+        static_cast<unsigned int>(outputShape.dim(1)),
+        static_cast<unsigned int>(inputShape.dim(1)),
+        kernelH,
+        kernelW};
 
-    // Bias data - This defaults to true in Caffe
     TensorInfo biasInfo;
     vector<float> biasData;
-    convolution2dDescriptor.m_BiasEnabled = convParam.has_bias_term() ? convParam.bias_term() : true;
-    if (convolution2dDescriptor.m_BiasEnabled)
+
+    if (desc.m_BiasEnabled)
     {
         biasData.resize(boost::numeric_cast<size_t>(numGroups * outputShape.dim(1)), 1.f);
         GetDataFromBlob(layerParam, biasData, 1);
@@ -453,178 +567,407 @@
     const unsigned int numWeightsPerGroup = boost::numeric_cast<unsigned int>(weightData.size()) / numGroups;
     const unsigned int numBiasesPerGroup  = boost::numeric_cast<unsigned int>(biasData.size()) / numGroups;
 
-    armnn::IConnectableLayer* returnLayer = nullptr;
-
     for (unsigned int g = 0; g < numGroups; ++g)
     {
-        // set the slot index, group 0 should be connected to the 0th output of the splitter
-        // group 1 should be connected to the 1st output of the splitter
+        // Sets the slot index, group 0 should be connected to the 0th output of the splitter
+        // group 1 should be connected to the 1st output of the splitter.
 
-        // Pull out the weights for this group from that loaded from the model file earlier
+        // Pulls out the weights for this group from that loaded from the model file earlier.
         ConstTensor weights(TensorInfo(4, weightDimSizes, DataType::Float32),
                             weightData.data() + numWeightsPerGroup * g);
 
         IConnectableLayer* convLayer = nullptr;
-        if (convolution2dDescriptor.m_BiasEnabled)
+        if (desc.m_BiasEnabled)
         {
-            // Pull out the biases for this group from that loaded from the model file earlier
+            // Pulls out the biases for this group from that loaded from the model file earlier.
             ConstTensor biases(biasInfo, biasData.data() + numBiasesPerGroup * g);
 
-            convLayer = m_Network->AddConvolution2dLayer(convolution2dDescriptor,
-                weights, biases, convLayerNames[g].c_str());
+            convLayer =
+                m_Network->AddConvolution2dLayer(desc, weights, biases, convLayerNames[g].c_str());
         }
         else
         {
-            convLayer = m_Network->AddConvolution2dLayer(convolution2dDescriptor,
-                weights, convLayerNames[g].c_str());
+            convLayer =
+                m_Network->AddConvolution2dLayer(desc, weights, convLayerNames[g].c_str());
         }
         convLayers[g] = convLayer;
 
         // If we have more than one group then the input to the nth convolution the splitter layer's nth output,
         // otherwise it's the regular input to this layer.
-        armnn::IOutputSlot& splitterInputConnection = splitterLayer ? splitterLayer->GetOutputSlot(g) : inputConnection;
+        armnn::IOutputSlot& splitterInputConnection =
+            splitterLayer ? splitterLayer->GetOutputSlot(g) : inputConnection;
         splitterInputConnection.Connect(convLayer->GetInputSlot(0));
         convLayer->GetOutputSlot(0).SetTensorInfo(BlobShapeToTensorInfo(outputShape));
-
-        returnLayer = convLayer;
     }
 
-    if (numGroups > 1)
+    // If the convolution was performed in chunks, add a layer to merge the results
+
+    // The merge input shape matches that of the convolution output
+    unsigned int mergeDimSizes[4] = {static_cast<unsigned int>(outputShape.dim(0)),
+                                        static_cast<unsigned int>(outputShape.dim(1)),
+                                        static_cast<unsigned int>(outputShape.dim(2)),
+                                        static_cast<unsigned int>(outputShape.dim(3))};
+
+    // This is used to describe how the input is to be merged
+    OriginsDescriptor mergeDesc(numGroups);
+
+    // Now create an input node for each group, using the name from
+    // the output of the corresponding convolution
+    for (unsigned int g = 0; g < numGroups; ++g)
     {
-        // If the convolution was performed in chunks, add a layer to merge the results
+        mergeDesc.SetViewOriginCoord(g, 1, mergeDimSizes[1] * g);
+    }
 
-        // The merge input shape matches that of the convolution output
-        unsigned int mergeDimSizes[4] = {static_cast<unsigned int>(outputShape.dim(0)),
-                                         static_cast<unsigned int>(outputShape.dim(1)),
-                                         static_cast<unsigned int>(outputShape.dim(2)),
-                                         static_cast<unsigned int>(outputShape.dim(3))};
+    // Make sure the output from the merge is the correct size to hold the data for all groups
+    mergeDimSizes[1] *= numGroups;
+    outputShape.set_dim(1, mergeDimSizes[1]);
 
-        // This is used to describe how the input is to be merged
-        OriginsDescriptor mergeDesc(numGroups);
+    // Finally add the merge layer
+    IConnectableLayer* mergerLayer = m_Network->AddMergerLayer(mergeDesc, layerParam.name().c_str());
 
-        // Now create an input node for each group, using the name from
-        // the output of the corresponding convolution
-        for (unsigned int g = 0; g < numGroups; ++g)
-        {
-            mergeDesc.SetViewOriginCoord(g, 1, mergeDimSizes[1] * g);
-        }
+    if (!mergerLayer)
+    {
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Failed to create final merger layer for Split+Convolution+Merger. "
+                    "Layer=%1% #groups=%2% #filters=%3% %4%") %
+                    layerParam.name() %
+                    numGroups %
+                    numFilters %
+                    CHECK_LOCATION().AsString()));
+    }
 
-        // Make sure the output from the merge is the correct size to hold the data for all groups
-        mergeDimSizes[1] *= numGroups;
-        outputShape.set_dim(1, mergeDimSizes[1]);
+    for (unsigned int g = 0; g < numGroups; ++g)
+    {
+        convLayers[g]->GetOutputSlot(0).Connect(mergerLayer->GetInputSlot(g));
+    }
+    mergerLayer->GetOutputSlot(0).SetTensorInfo(armnn::TensorInfo(4, mergeDimSizes, DataType::Float32));
+    SetArmnnOutputSlotForCaffeTop(layerParam.top(0), mergerLayer->GetOutputSlot(0));
+}
 
-        // The merge layer just assumes the name of the original convolution
-        // layer so the following layer connection "just works"
-        const string mergeOutputName = layerParam.name();
+void CaffeParserBase::AddConvLayerWithDepthwiseConv(const caffe::LayerParameter& layerParam,
+                                                    const armnn::Convolution2dDescriptor& convDesc,
+                                                    unsigned int kernelW,
+                                                    unsigned int kernelH)
+{
+    BOOST_ASSERT(layerParam.type() == "Convolution");
+    ValidateNumInputsOutputs(layerParam, 1, 1);
 
-        // Finally add the merge layer
-        IConnectableLayer* layer = m_Network->AddMergerLayer(mergeDesc, mergeOutputName.c_str());
+    ConvolutionParameter convParam  = layerParam.convolution_param();
+    BlobShape inputShape = TensorDescToBlobShape(GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0)).GetTensorInfo());
 
-        for (unsigned int g = 0; g < numGroups; ++g)
-        {
-            convLayers[g]->GetOutputSlot(0).Connect(layer->GetInputSlot(g));
-        }
-        layer->GetOutputSlot(0).SetTensorInfo(armnn::TensorInfo(4, mergeDimSizes, DataType::Float32));
+    DepthwiseConvolution2dDescriptor desc;
+    desc.m_PadLeft      = convDesc.m_PadLeft;
+    desc.m_PadRight     = convDesc.m_PadRight;
+    desc.m_PadTop       = convDesc.m_PadTop;
+    desc.m_PadBottom    = convDesc.m_PadBottom;
+    desc.m_StrideX      = convDesc.m_StrideX;
+    desc.m_StrideY      = convDesc.m_StrideY;
+    desc.m_BiasEnabled  = convDesc.m_BiasEnabled;
 
-        returnLayer = layer;
+    unsigned int numFilters = convParam.num_output();
+
+    BlobShape outputShape;
+    outputShape.add_dim(0);
+    outputShape.set_dim(0, inputShape.dim(0));
+    outputShape.add_dim(1);
+    outputShape.set_dim(1, numFilters);
+    outputShape.add_dim(2);
+    outputShape.set_dim(
+        2, (static_cast<int>(
+                static_cast<float>(inputShape.dim(2) + 2 * desc.m_PadBottom - kernelH) /
+                static_cast<float>(desc.m_StrideY)) + 1));
+    outputShape.add_dim(3);
+    outputShape.set_dim(
+        3, (static_cast<int>(
+                static_cast<float>(inputShape.dim(3) + 2 * desc.m_PadRight - kernelW) /
+                static_cast<float>(desc.m_StrideX)) + 1));
+
+    // Load the weight data
+    size_t allWeightsSize = boost::numeric_cast<size_t>(inputShape.dim(1) * kernelH * kernelW);
+    vector<float> weightData(allWeightsSize);
+
+    GetDataFromBlob(layerParam, weightData, 0);
+
+    // depth multiplier will be 1 for the depthwise convolution
+    const unsigned int weightDimSizes[4] = {
+        static_cast<unsigned int>(1),                 // depth multiplier
+        static_cast<unsigned int>(inputShape.dim(1)), // #channels
+        kernelH,
+        kernelW};
+
+    armnn::IConnectableLayer* returnLayer = nullptr;
+    ConstTensor weights(TensorInfo(4, weightDimSizes, DataType::Float32), weightData.data());
+
+    if (desc.m_BiasEnabled)
+    {
+        TensorInfo biasInfo;
+        vector<float> biasData;
+
+        biasData.resize(boost::numeric_cast<size_t>(outputShape.dim(1)), 1.f);
+        GetDataFromBlob(layerParam, biasData, 1);
+
+        const unsigned int biasDimSizes[1] = {static_cast<unsigned int>(outputShape.dim(1))};
+        biasInfo = TensorInfo(1, biasDimSizes, DataType::Float32);
+
+        ConstTensor biases(biasInfo, biasData.data());
+        returnLayer = m_Network->AddDepthwiseConvolution2dLayer(desc, weights, biases, layerParam.name().c_str());
+    }
+    else
+    {
+        returnLayer = m_Network->AddDepthwiseConvolution2dLayer(desc, weights, layerParam.name().c_str());
     }
 
     if (!returnLayer)
     {
-        throw ParseException("Loading Convolution Layer: invalid return layer");
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Failed to create depthwise convolution layer. "
+                    "Layer=%1% #filters=%2% %3%") %
+                    layerParam.name() %
+                    numFilters %
+                    CHECK_LOCATION().AsString()));
+    }
+    armnn::IOutputSlot& inputConnection = GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0));
+    inputConnection.Connect(returnLayer->GetInputSlot(0));
+    returnLayer->GetOutputSlot(0).SetTensorInfo(BlobShapeToTensorInfo(outputShape));
+    SetArmnnOutputSlotForCaffeTop(layerParam.top(0), returnLayer->GetOutputSlot(0));
+}
+
+void CaffeParserBase::ParseConvLayer(const LayerParameter& layerParam)
+{
+    // Ignored Caffe Parameters
+    // * Dilation Size
+    // * Weight Filler
+    // * Bias Filler
+    // * Engine
+    // * Force nd_im2col
+    // * Axis
+
+    // Not Available ArmNN Interface Parameters
+    // * Rounding policy;
+
+    BOOST_ASSERT(layerParam.type() == "Convolution");
+    ValidateNumInputsOutputs(layerParam, 1, 1);
+
+    ConvolutionParameter convParam = layerParam.convolution_param();
+    BlobShape inputShape = TensorDescToBlobShape(GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0)).GetTensorInfo());
+    const unsigned int numGroups = convParam.has_group() ? convParam.group() : 1;
+    unsigned int numFilters = convParam.num_output();
+
+    const auto notFound = std::numeric_limits<unsigned int>::max();
+
+    unsigned int kernelH = GET_OPTIONAL_WITH_VECTOR_FALLBACK(convParam, ConvolutionParameter,
+                                                             kernel_h, kernel_size, unsigned int, notFound);
+    unsigned int kernelW = GET_OPTIONAL_WITH_VECTOR_FALLBACK(convParam, ConvolutionParameter,
+                                                             kernel_w, kernel_size, unsigned int, notFound);
+
+    unsigned int strideH = GET_OPTIONAL_WITH_VECTOR_FALLBACK(convParam, ConvolutionParameter,
+                                                             stride_h, stride, unsigned int, 1u);
+    unsigned int strideW = GET_OPTIONAL_WITH_VECTOR_FALLBACK(convParam, ConvolutionParameter,
+                                                             stride_w, stride, unsigned int, 1u);
+
+    unsigned int padH = GET_OPTIONAL_WITH_VECTOR_FALLBACK(convParam, ConvolutionParameter,
+                                                          pad_h, pad, unsigned int, 0u);
+    unsigned int padW = GET_OPTIONAL_WITH_VECTOR_FALLBACK(convParam, ConvolutionParameter,
+                                                          pad_w, pad, unsigned int, 0u);
+
+    VALIDATE_EQUAL_VALUES_IN_RANGE(kernelH, kernelW, 0, 11);
+    VALIDATE_EQUAL_VALUES_IN_RANGE(strideH, strideW, 0, 11);
+    VALIDATE_EQUAL_VALUES_IN_RANGE(padH, padW, 0, 11);
+
+    Convolution2dDescriptor convolution2dDescriptor;
+    convolution2dDescriptor.m_PadLeft     = padW;
+    convolution2dDescriptor.m_PadRight    = padW;
+    convolution2dDescriptor.m_PadTop      = padH;
+    convolution2dDescriptor.m_PadBottom   = padH;
+    convolution2dDescriptor.m_StrideX     = strideW;
+    convolution2dDescriptor.m_StrideY     = strideH;
+    convolution2dDescriptor.m_BiasEnabled = convParam.has_bias_term() ? convParam.bias_term() : true;
+
+    if (numGroups > numFilters)
+    {
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Error parsing Convolution: %1%. "
+                    "The 'group'=%2% parameter cannot be larger than the "
+                    "number of filters supplied ='%3%'. %4%") %
+                    layerParam.name() %
+                    numGroups %
+                    numFilters %
+                    CHECK_LOCATION().AsString()));
+    }
+
+    if (inputShape.dim_size() != 4)
+    {
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Convolution input shape is expected to have 4 dimensions. "
+                    "%1%'s input has only %2%. %3%") %
+                    layerParam.name() %
+                    inputShape.dim_size() %
+                    CHECK_LOCATION().AsString()));
+    }
+
+    if (numGroups > 1)
+    {
+        if (numGroups > inputShape.dim(1))
+        {
+            throw ParseException(
+                boost::str(
+                    boost::format(
+                        "Error parsing Convolution: %1%. "
+                        "The 'group'=%2% parameter cannot be larger than the "
+                        "channel of the input shape=%3% (in NCHW format). %4%") %
+                        layerParam.name() %
+                        numGroups %
+                        inputShape.dim(1) %
+                        CHECK_LOCATION().AsString()));
+        }
+        else if (numGroups == inputShape.dim(1))
+        {
+            // we use a depthwise convolution here, because the number of groups equals to the
+            // input channels
+            AddConvLayerWithDepthwiseConv(layerParam, convolution2dDescriptor, kernelW, kernelH);
+            return;
+        }
+        else
+        {
+            // we split the input by channels into channels/groups separate convolutions
+            // and merger the results afterwards
+            AddConvLayerWithSplits(layerParam, convolution2dDescriptor, kernelW, kernelH);
+            return;
+        }
+    }
+
+    // NOTE: at this point we only need to handle #group=1 case, all other cases should be
+    //       handled by the AddConvLayer* helpers
+
+    // Populate convolution output tensor descriptor dimensions
+    BlobShape outputShape;
+    outputShape.add_dim(0);
+    outputShape.set_dim(0, inputShape.dim(0));
+    outputShape.add_dim(1);
+    outputShape.set_dim(1, numFilters);
+    outputShape.add_dim(2);
+    outputShape.set_dim(
+        2, (static_cast<int>(
+                static_cast<float>(inputShape.dim(2) + 2 * padH - kernelH) /
+                static_cast<float>(strideH)) + 1));
+    outputShape.add_dim(3);
+    outputShape.set_dim(
+        3, (static_cast<int>(
+                static_cast<float>(inputShape.dim(3) + 2 * padW - kernelW) /
+                static_cast<float>(strideW)) + 1));
+
+    // Load the weight data for ALL groups
+    vector<float> weightData(boost::numeric_cast<size_t>(inputShape.dim(1) *
+                                                         outputShape.dim(1) *
+                                                         kernelH *
+                                                         kernelW));
+    GetDataFromBlob(layerParam, weightData, 0);
+
+    const unsigned int weightDimSizes[4] = {
+        static_cast<unsigned int>(outputShape.dim(1)), // output channels
+        static_cast<unsigned int>(inputShape.dim(1)),  // input channels
+        kernelH,
+        kernelW};
+
+    armnn::IConnectableLayer* returnLayer = nullptr;
+
+    // Pull out the weights for this group from that loaded from the model file earlier
+    ConstTensor weights(TensorInfo(4, weightDimSizes, DataType::Float32), weightData.data());
+
+    if (convolution2dDescriptor.m_BiasEnabled)
+    {
+        TensorInfo biasInfo;
+        vector<float> biasData;
+
+        biasData.resize(boost::numeric_cast<size_t>(outputShape.dim(1)), 1.f);
+        GetDataFromBlob(layerParam, biasData, 1);
+
+        const unsigned int biasDimSizes[1] = {static_cast<unsigned int>(outputShape.dim(1))};
+        biasInfo = TensorInfo(1, biasDimSizes, DataType::Float32);
+
+        // Pull out the biases for this group from that loaded from the model file earlier
+        ConstTensor biases(biasInfo, biasData.data());
+
+        returnLayer =
+            m_Network->AddConvolution2dLayer(convolution2dDescriptor, weights, biases, layerParam.name().c_str());
+    }
+    else
+    {
+        returnLayer = m_Network->AddConvolution2dLayer(convolution2dDescriptor, weights, layerParam.name().c_str());
+    }
+
+    armnn::IOutputSlot& inputConnection = GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0));
+    inputConnection.Connect(returnLayer->GetInputSlot(0));
+    returnLayer->GetOutputSlot(0).SetTensorInfo(BlobShapeToTensorInfo(outputShape));
+
+    if (!returnLayer)
+    {
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Failed to create Convolution layer. "
+                    "Layer=%1% #groups=%2% #filters=%3% %4%") %
+                    layerParam.name() %
+                    numGroups %
+                    numFilters %
+                    CHECK_LOCATION().AsString()));
     }
 
     SetArmnnOutputSlotForCaffeTop(layerParam.top(0), returnLayer->GetOutputSlot(0));
 }
 
-void CaffeParser::ParsePoolingLayer(const LayerParameter& layerParam)
+void CaffeParserBase::ParsePoolingLayer(const LayerParameter& layerParam)
 {
+    // Ignored Caffe Parameters
+    //      Stochastic Pooling
+    //      Engine
+
     ValidateNumInputsOutputs(layerParam, 1, 1);
-
     PoolingParameter param = layerParam.pooling_param();
-
     const TensorInfo& inputInfo = GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0)).GetTensorInfo();
 
-    // Kernel size
-    unsigned int kernel_h = 0;
-    unsigned int kernel_w = 0;
-    if (param.has_kernel_h() && param.has_kernel_w())
-    {
-        kernel_h = param.kernel_h();
-        kernel_w = param.kernel_w();
-    }
-    else if (param.kernel_size() > 0)
-    {
-        kernel_h = param.kernel_size();
-        kernel_w = param.kernel_size();
-    }
-    else if (param.has_global_pooling())
+    const auto notFound = std::numeric_limits<unsigned int>::max();
+
+    unsigned int kernel_h = GET_OPTIONAL_WITH_FALLBACK(param, PoolingParameter,
+                                                       kernel_h, kernel_size, unsigned int, notFound);
+    unsigned int kernel_w = GET_OPTIONAL_WITH_FALLBACK(param, PoolingParameter,
+                                                       kernel_w, kernel_size, unsigned int, notFound);
+
+    if ((kernel_h == notFound || kernel_w == notFound) && param.has_global_pooling())
     {
         kernel_h = inputInfo.GetShape()[2];
         kernel_w = inputInfo.GetShape()[3];
     }
-    else
+
+    VALIDATE_EQUAL_VALUES_IN_RANGE(kernel_h, kernel_w, 0, 11);
+
+    unsigned int stride_h = GET_OPTIONAL_WITH_FALLBACK(param, PoolingParameter,
+                                                       stride_h, stride, unsigned int, notFound);
+    unsigned int stride_w = GET_OPTIONAL_WITH_FALLBACK(param, PoolingParameter,
+                                                       stride_h, stride, unsigned int, notFound);
+
+    if ((stride_h == notFound || stride_w == notFound) && param.has_global_pooling())
     {
-        throw ParseException("Loading Pooling Layer: Kernel Size defined Illegally");
+        stride_h = 1;
+        stride_w = 1;
     }
 
-    if (!IsInRange(kernel_h, 0, 11) || !IsInRange(kernel_w, 0, 11) || (kernel_h != kernel_w))
-    {
-        throw ParseException(boost::str(
-            boost::format("Loading Pooling Layer: kernel has invalid size: %1% x %2%") % kernel_h % kernel_w));
-    }
+    VALIDATE_EQUAL_VALUES_IN_RANGE(stride_h, stride_w, 0, 11);
 
-    // Strides
-    // Default to a valid value for the case of global pooling (where the strides don't have to be explicitly set)
-    unsigned int stride_h = 1;
-    unsigned int stride_w = 1;
-    if (param.has_stride_h() && param.has_stride_w())
-    {
-        stride_h = param.stride_h();
-        stride_w = param.stride_w();
-    }
-    else if (param.has_stride())
-    {
-        stride_h = param.stride();
-        stride_w = param.stride();
-    }
-    else if (!param.has_global_pooling())
-    {
-        throw ParseException("Loading Pooling Layer: Stride Size defined Illegally");
-    }
+    unsigned int pad_h = GET_OPTIONAL_WITH_FALLBACK(param, PoolingParameter,
+                                                    pad_h, pad, unsigned int, 0u);
+    unsigned int pad_w = GET_OPTIONAL_WITH_FALLBACK(param, PoolingParameter,
+                                                    pad_w, pad, unsigned int, 0u);
 
-    if (!IsInRange(stride_h, 0, 11) || !IsInRange(stride_w, 0, 11) || (stride_h != stride_w))
-    {
-        throw ParseException("Loading Pooling Layer: stride has invalid size");
-    }
-
-    // Padding
-    unsigned int pad_h = 0;
-    unsigned int pad_w = 0;
-    if (param.has_pad_h() && param.has_pad_w())
-    {
-        pad_h = param.pad_h();
-        pad_w = param.pad_w();
-    }
-    else if (param.has_pad())
-    {
-        pad_h = param.pad();
-        pad_w = param.pad();
-    }
-    else
-    {
-        pad_h = 0;
-        pad_w = 0;
-    }
-
-    if (!IsInRange(pad_h, 0, 11) || !IsInRange(pad_w, 0, 11) || (pad_h != pad_w))
-    {
-        throw ParseException("Loading Pooling Layer: pad has invalid size");
-    }
-
-    // Ignored Caffe Parameters
-    //      Stochastic Pooling
-    //      Engine
+    VALIDATE_EQUAL_VALUES_IN_RANGE(pad_h, pad_w, 0, 11);
 
     // Populate Weight and Bias Filter Descriptor
     Pooling2dDescriptor pooling2dDescriptor;
@@ -645,17 +988,33 @@
             }
             case PoolingParameter_PoolMethod_STOCHASTIC:
             {
-                throw ParseException("Loading Pooling Layer: Stochastic Pooling Not Supported");
+                throw ParseException(
+                    boost::str(
+                        boost::format(
+                            "Pooling Layer: Stochastic Pooling Not Supported. Layer=%1% %2%") %
+                            layerParam.name() %
+                            CHECK_LOCATION().AsString()));
             }
             default:
             {
-                throw ParseException("Loading Pooling Layer: Mode Not Supported");
+                throw ParseException(
+                    boost::str(
+                        boost::format(
+                            "Pooling Layer: unknown pooling method: %1% for layer: %2% %3%") %
+                            p %
+                            layerParam.name() %
+                            CHECK_LOCATION().AsString()));
             }
         }
     }
     else
     {
-        throw ParseException("Loading Pooling Layer: No Pooling Method Defined");
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "No Pooling Method Defined for %1% %2%") %
+                    layerParam.name() %
+                    CHECK_LOCATION().AsString()));
     }
 
     pooling2dDescriptor.m_PadLeft     = pad_w;
@@ -673,7 +1032,6 @@
     armnn::IConnectableLayer* poolingLayer = m_Network->AddPooling2dLayer(pooling2dDescriptor,
         layerParam.name().c_str());
 
-
     TensorInfo outputInfo(
         { inputInfo.GetShape()[0],
           inputInfo.GetShape()[1],
@@ -690,7 +1048,7 @@
     SetArmnnOutputSlotForCaffeTop(layerParam.top(0), poolingLayer->GetOutputSlot(0));
 }
 
-void CaffeParser::ParseReluLayer(const LayerParameter& layerParam)
+void CaffeParserBase::ParseReluLayer(const LayerParameter& layerParam)
 {
     ValidateNumInputsOutputs(layerParam, 1, 1);
 
@@ -716,7 +1074,7 @@
     SetArmnnOutputSlotForCaffeTop(layerParam.top(0), activationLayer->GetOutputSlot(0));
 }
 
-void CaffeParser::ParseLRNLayer(const LayerParameter& layerParam)
+void CaffeParserBase::ParseLRNLayer(const LayerParameter& layerParam)
 {
     ValidateNumInputsOutputs(layerParam, 1, 1);
 
@@ -724,9 +1082,9 @@
 
     const TensorInfo& inputInfo = GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0)).GetTensorInfo();
 
-    // Ignored BATCH NORMALIZATION Caffe Parameters
-    // Ignored MVN Caffe Parameters
-    // Ignored LRN Caffe Parameters
+    // Ignored BATCH NORMALIZATION Caffe Parameters.
+    // Ignored MVN Caffe Parameters.
+    // Ignored LRN Caffe Parameters.
     //      Engine
 
     NormalizationDescriptor normalizationDescriptor;
@@ -746,12 +1104,20 @@
                 break;
             }
             default:
-                throw ParseException("Loading LRN Layer: Mode Not Supported");
+            {
+                throw ParseException(
+                    boost::str(
+                        boost::format(
+                            "Unknown region %1% for LRN layer %2% %3%") %
+                            n %
+                            layerParam.name() %
+                            CHECK_LOCATION().AsString()));
+            }
         }
     }
     else
     {
-        // Caffe defaults to normalization across channels
+        // Caffe defaults to normalization across channels.
         normalizationDescriptor.m_NormChannelType = NormalizationAlgorithmChannel::Across;
     }
 
@@ -762,7 +1128,12 @@
     }
     else
     {
-        throw ParseException("Loading LRN Layer: Local_size not defined");
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "local_size not defined for LRN layer %1% %2%") %
+                    layerParam.name() %
+                    CHECK_LOCATION().AsString()));
     }
 
     if (param.has_alpha())
@@ -772,7 +1143,12 @@
     }
     else
     {
-        throw ParseException("Loading LRN Layer: Alpha not defined");
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Alpha not defined for LRN layer %1% %2%") %
+                    layerParam.name() %
+                    CHECK_LOCATION().AsString()));
     }
     if (param.has_beta())
     {
@@ -780,14 +1156,22 @@
     }
     else
     {
-        throw ParseException("Loading LRN Layer: Beta not defined");
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Beta not defined for LRN layer %1% %2%") %
+                    layerParam.name() %
+                    CHECK_LOCATION().AsString()));
     }
+
     if (param.has_k())
     {
         normalizationDescriptor.m_K = param.k();
     }
     else
+    {
         normalizationDescriptor.m_K = 1;
+    }
 
     IConnectableLayer* const normLayer = m_Network->AddNormalizationLayer(normalizationDescriptor,
         layerParam.name().c_str());
@@ -797,7 +1181,7 @@
     SetArmnnOutputSlotForCaffeTop(layerParam.top(0), normLayer->GetOutputSlot(0));
 }
 
-void CaffeParser::ParseInnerProductLayer(const LayerParameter& layerParam)
+void CaffeParserBase::ParseInnerProductLayer(const LayerParameter& layerParam)
 {
     InnerProductParameter param = layerParam.inner_product_param();
 
@@ -805,7 +1189,7 @@
 
     unsigned int outputSize = param.num_output();
 
-    // Ignored Caffe Parameters
+    // Ignored Caffe Parameters:
     // Weight Filler
     // Bias Filler
     // Engine
@@ -815,12 +1199,12 @@
 
     if (param.has_transpose())
     {
-        // If true assume transposed weights
+        // If true, assumes transposed weights.
         tensorFullyConnectedDescriptor.m_TransposeWeightMatrix = param.transpose();
     }
     else
     {
-        // caffe defaults to transposed
+        // Caffe defaults to transposed.
         tensorFullyConnectedDescriptor.m_TransposeWeightMatrix = true;
     }
 
@@ -829,32 +1213,28 @@
     TensorInfo weightInfo;
     TensorInfo biasInfo;
 
-    // allow implicit flattening of extra dimensions
+    // Allows implicit flattening of extra dimensions.
     unsigned int inputSize = inputInfo.GetShape()[1];
     for (unsigned int i = 2; i < inputInfo.GetNumDimensions(); ++i)
     {
         inputSize *= inputInfo.GetShape()[i];
     }
 
-    vector<float> weightData(inputSize * outputSize);
-
-    GetDataFromBlob(layerParam, weightData, 0);
+    const float* weightDataPtr = GetArrayPtrFromBlob(layerParam, 0);
     const unsigned int swTD[2] = { outputSize, inputSize };
-    ConstTensor weights(TensorInfo(2, swTD, DataType::Float32), weightData);
+    ConstTensor weights(TensorInfo(2, swTD, DataType::Float32), weightDataPtr);
 
     tensorFullyConnectedDescriptor.m_BiasEnabled = true;
-    // Todo: check whether bias enabled
+    // Todo: check whether bias enabled.
     armnn::IConnectableLayer* fullyConnectedLayer = nullptr;
     if (tensorFullyConnectedDescriptor.m_BiasEnabled)
     {
         // BIAS VALUE
-        vector<float> biasData(outputSize);
-
-        GetDataFromBlob(layerParam, biasData, 1);
+        const float* biasDataPtr = GetArrayPtrFromBlob(layerParam, 1);
 
         const unsigned int sbTD[1] = { outputSize };
 
-        ConstTensor biases(TensorInfo(1, sbTD, DataType::Float32), biasData);
+        ConstTensor biases(TensorInfo(1, sbTD, DataType::Float32), biasDataPtr);
 
         fullyConnectedLayer = m_Network->AddFullyConnectedLayer(tensorFullyConnectedDescriptor, weights, biases,
             layerParam.name().c_str());
@@ -871,7 +1251,7 @@
     SetArmnnOutputSlotForCaffeTop(layerParam.top(0), fullyConnectedLayer->GetOutputSlot(0));
 }
 
-void CaffeParser::ParseSoftmaxLayer(const LayerParameter& layerParam)
+void CaffeParserBase::ParseSoftmaxLayer(const LayerParameter& layerParam)
 {
     ValidateNumInputsOutputs(layerParam, 1, 1);
 
@@ -879,7 +1259,7 @@
 
     const TensorInfo& inputInfo = GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0)).GetTensorInfo();
 
-    // Ignored Caffe Parameters
+    // Ignored Caffe Parameters:
     //      axis
     //      Engine
 
@@ -892,16 +1272,16 @@
     SetArmnnOutputSlotForCaffeTop(layerParam.top(0), softmaxLayer->GetOutputSlot(0));
 }
 
-void CaffeParser::ParseEltwiseLayer(const LayerParameter& layerParam)
+void CaffeParserBase::ParseEltwiseLayer(const LayerParameter& layerParam)
 {
     ValidateNumInputsOutputs(layerParam, 2, 1);
 
     const TensorInfo& inputInfo = GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0)).GetTensorInfo();
 
-    // Ignored Caffe Parameters
+    // Ignored Caffe Parameters:
     //      coeff
 
-    EltwiseParameter_EltwiseOp operation = EltwiseParameter_EltwiseOp_SUM; // default to sum as per caffe
+    EltwiseParameter_EltwiseOp operation = EltwiseParameter_EltwiseOp_SUM; // Defaults to sum as per caffe.
 
     if (layerParam.has_eltwise_param() && layerParam.eltwise_param().has_operation())
     {
@@ -923,7 +1303,13 @@
         }
         default:
         {
-            throw ParseException("Unsupported operation in Eltwise layer");
+            throw ParseException(
+                boost::str(
+                    boost::format(
+                        "Unsupported operation %1% in Eltwise layer %2% %3%") %
+                        operation %
+                        layerParam.name() %
+                        CHECK_LOCATION().AsString()));
         }
     }
 
@@ -933,14 +1319,15 @@
     SetArmnnOutputSlotForCaffeTop(layerParam.top(0), newLayer->GetOutputSlot(0));
 }
 
-void CaffeParser::ParseConcatLayer(const LayerParameter& layerParam)
+void CaffeParserBase::ParseConcatLayer(const LayerParameter& layerParam)
 {
     unsigned int numInputs = static_cast<unsigned int>(layerParam.bottom_size());
-    // we assume concat happens along the channel dimension, which is 1 in (0, 1, 2, 3)
+    // We assume concat happens along the channel dimension, which is 1 in (0, 1, 2, 3).
     unsigned int concatDim = 1;
     unsigned int numOfDims = 4;
 
-    OriginsDescriptor concatDescriptor(static_cast<uint32_t>(numInputs), numOfDims);// we only consider 4-D tensor here
+    // we only consider 4-D tensor here
+    OriginsDescriptor concatDescriptor(static_cast<uint32_t>(numInputs), numOfDims);
     std::vector<unsigned int>mergeDimSizes(numOfDims, 0u);
 
     unsigned int mergeDim = 0;
@@ -948,10 +1335,18 @@
     {
         const TensorInfo& inputInfo = GetArmnnOutputSlotForCaffeTop(
             layerParam.bottom(boost::numeric_cast<int>(viewIndex))).GetTensorInfo();
-        // Check whether the dimensions of the input tensors are actually 4
+        // Checks whether the dimensions of the input tensors are actually 4.
         if (inputInfo.GetNumDimensions()!=4)
         {
-            throw ParseException("The number of dimensions for input tensors of the concatenation op should be 4.");
+            throw ParseException(
+                boost::str(
+                    boost::format(
+                        "The number of dimensions for input tensors of "
+                        "the concatenation op should be 4. Inputs of %1% has "
+                        "%2% dimensions. %3%") %
+                        layerParam.name() %
+                        inputInfo.GetNumDimensions() %
+                        CHECK_LOCATION().AsString()));
         }
 
         mergeDimSizes[0] = inputInfo.GetShape()[0];
@@ -974,7 +1369,7 @@
     }
     mergeDimSizes[concatDim] = mergeDim;
 
-    armnn::IConnectableLayer *concatlayer = m_Network->AddMergerLayer(concatDescriptor, layerParam.name().c_str());
+    armnn::IConnectableLayer* concatlayer = m_Network->AddMergerLayer(concatDescriptor, layerParam.name().c_str());
     for (unsigned int i = 0; i < numInputs; ++i)
     {
         armnn::IOutputSlot& outputSlot = GetArmnnOutputSlotForCaffeTop(layerParam.bottom(boost::numeric_cast<int>(i)));
@@ -985,7 +1380,7 @@
     SetArmnnOutputSlotForCaffeTop(layerParam.top(0), concatlayer->GetOutputSlot(0));
 }
 
-void CaffeParser::ParseBatchNormLayer(const LayerParameter& layerParam)
+void CaffeParserBase::ParseBatchNormLayer(const LayerParameter& layerParam)
 {
     ValidateNumInputsOutputs(layerParam, 1, 1);
 
@@ -1000,9 +1395,14 @@
     {
         if (!param.use_global_stats())
         {
-            throw ParseException(boost::str(boost::format("Error parsing Batch Norm layer '%1%': "
-                "Parameter 'use_global_stats' is set to false, which is unsupported (value used for training).")
-                % name));
+            throw ParseException(
+                boost::str(
+                    boost::format(
+                        "Error parsing Batch Norm layer '%1%': "
+                        "Parameter 'use_global_stats' is set to false, which is "
+                        "unsupported (value used for training). %2%") %
+                        name %
+                        CHECK_LOCATION().AsString()));
         }
     }
 
@@ -1018,7 +1418,7 @@
     vector<float> varianceData(channels);
     GetDataFromBlob(layerParam, varianceData, 1);
 
-    // read moving average factor and apply scaling (if required)
+    // Reads moving average factor and applies scaling (if required).
     const BlobProto& blob = layerParam.blobs(boost::numeric_cast<int>(2));
     const float movingAverageFactor = blob.data(boost::numeric_cast<int>(0));
     if(movingAverageFactor != 0.0f)
@@ -1030,7 +1430,7 @@
         std::transform(meanData.begin(), meanData.end(), meanData.begin(), scaleFunction);
     }
 
-    // identity scale operation
+    // Identifies scale operation.
     vector<float> betaData(channels, 0.0f);
     vector<float> gammaData(channels, 1.0f);
 
@@ -1046,9 +1446,9 @@
     SetArmnnOutputSlotForCaffeTop(layerParam.top(0), batchNormLayer->GetOutputSlot(0));
 }
 
-void CaffeParser::ParseScaleLayer(const LayerParameter& layerParam)
+void CaffeParserBase::ParseScaleLayer(const LayerParameter& layerParam)
 {
-    // current unoptimal solution: add a batchnormalization layer with 0 mean and 1 variance
+    // Current unoptimal solution: add a batchnormalization layer with 0 mean and 1 variance.
     ValidateNumInputsOutputs(layerParam, 1, 1);
 
     const TensorInfo& inputInfo = GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0)).GetTensorInfo();
@@ -1059,14 +1459,21 @@
     if (param.axis() != 1)
     {
         // Would have to use something other than BatchNormalizationLayer in this case
-        throw ParseException("Loading Scale Layer: Only axis 1 supported currently");
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Loading Scale Layer: Only axis 1 is supported currently. "
+                    "Layer=%1% Axis=%2% %3%") %
+                    layerParam.name() %
+                    param.axis() %
+                    CHECK_LOCATION().AsString()));
     }
 
     unsigned int     channels = inputInfo.GetShape()[1];
     unsigned int     shape[]  = {channels};
 
     BatchNormalizationDescriptor desc;
-    desc.m_Eps = 0.0f; // don't need epsilon if variance is 1
+    desc.m_Eps = 0.0f; // Don't need epsilon if variance is 1.
     vector<float> meanData(channels, 0.0f);
     vector<float> varianceData(channels, 1.0f);
     vector<float> betaData(channels, 0.0f);
@@ -1091,12 +1498,19 @@
     SetArmnnOutputSlotForCaffeTop(layerParam.top(0), batchNormLayer->GetOutputSlot(0));
 }
 
-void CaffeParser::ParseSplitLayer(const caffe::LayerParameter& layerParam)
+void CaffeParserBase::ParseSplitLayer(const caffe::LayerParameter& layerParam)
 {
-    // Used in caffe to duplicate memory - not necessary in armnn
+    // Used in caffe to duplicate memory - not necessary in armnn.
     if (layerParam.bottom_size() != 1)
     {
-        throw ParseException("Split layer '" + layerParam.name() + "' should have exactly 1 bottom");
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Split layer '%1%' should have exactly 1 bottom. "
+                    "#bottoms=%2% %3%") %
+                    layerParam.name() %
+                    layerParam.bottom_size() %
+                    CHECK_LOCATION().AsString()));
     }
     armnn::IOutputSlot& outputSlot = GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0));
     for (int i = 0; i < layerParam.top_size(); i++)
@@ -1105,31 +1519,39 @@
     }
 }
 
-void CaffeParser::ParseDropoutLayer(const caffe::LayerParameter& layerParam)
+void CaffeParserBase::ParseDropoutLayer(const caffe::LayerParameter& layerParam)
 {
-    // Ignored for inference so patch the single input to its single output
+    // Ignored for inference, so patch the single input to its single output.
     if (layerParam.bottom_size() != 1 || layerParam.top_size() != 1)
     {
-        throw ParseException("Dropout layer '" + layerParam.name() + "' should have exactly 1 bottom and 1 top");
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Dropout layer '%1%' should have exactly 1 bottom and 1 top. "
+                    "#bottoms=%2% #tops=%3% %4%") %
+                    layerParam.name() %
+                    layerParam.bottom_size() %
+                    layerParam.top_size() %
+                    CHECK_LOCATION().AsString()));
     }
     SetArmnnOutputSlotForCaffeTop(layerParam.top(0), GetArmnnOutputSlotForCaffeTop(layerParam.bottom(0)));
 }
 
-void CaffeParser::TrackInputBinding(armnn::IConnectableLayer* layer,
+void CaffeParserBase::TrackInputBinding(armnn::IConnectableLayer* layer,
     armnn::LayerBindingId id,
     const armnn::TensorInfo& tensorInfo)
 {
     return TrackBindingPoint(layer, id, tensorInfo, layer->GetName(), m_NetworkInputsBindingInfo);
 }
 
-void CaffeParser::TrackOutputBinding(armnn::IConnectableLayer* layer,
+void CaffeParserBase::TrackOutputBinding(armnn::IConnectableLayer* layer,
     armnn::LayerBindingId id,
     const armnn::TensorInfo& tensorInfo)
 {
     return TrackBindingPoint(layer, id, tensorInfo, layer->GetName(), m_NetworkOutputsBindingInfo);
 }
 
-void CaffeParser::TrackBindingPoint(armnn::IConnectableLayer* layer,
+void CaffeParserBase::TrackBindingPoint(armnn::IConnectableLayer* layer,
     armnn::LayerBindingId id,
     const armnn::TensorInfo& tensorInfo,
     const char* bindingPointDesc,
@@ -1143,12 +1565,17 @@
     }
     else
     {
-        throw ParseException(boost::str(
-            boost::format("Id %1% used by more than one %2% layer") % id % bindingPointDesc));
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Id %1% used by more than one %2% layer %3%") %
+                    id %
+                    bindingPointDesc %
+                    CHECK_LOCATION().AsString()));
     }
 }
 
-armnn::IOutputSlot& CaffeParser::GetArmnnOutputSlotForCaffeTop(const std::string& caffeTopName) const
+armnn::IOutputSlot& CaffeParserBase::GetArmnnOutputSlotForCaffeTop(const std::string& caffeTopName) const
 {
     auto it = m_ArmnnOutputSlotForCaffeTop.find(caffeTopName);
     if (it != m_ArmnnOutputSlotForCaffeTop.end())
@@ -1157,12 +1584,17 @@
     }
     else
     {
-        throw ParseException(boost::str(boost::format(
-            "Could not find armnn output slot for Caffe top '%1%'") % caffeTopName));
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Could not find armnn output slot for Caffe top '%1%' %2%") %
+                    caffeTopName %
+                    CHECK_LOCATION().AsString()));
     }
 }
 
-void CaffeParser::SetArmnnOutputSlotForCaffeTop(const std::string& caffeTopName, armnn::IOutputSlot& armnnOutputSlot)
+void CaffeParserBase::SetArmnnOutputSlotForCaffeTop(
+    const std::string& caffeTopName, armnn::IOutputSlot& armnnOutputSlot)
 {
     auto it = m_ArmnnOutputSlotForCaffeTop.find(caffeTopName);
     if (it == m_ArmnnOutputSlotForCaffeTop.end())
@@ -1171,31 +1603,39 @@
     }
     else
     {
-        throw ParseException("Attempting to add duplicate entry for Caffe top '" + caffeTopName + "'");
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Attempting to add duplicate entry for Caffe top '%1%' %2%") %
+                    caffeTopName %
+                    CHECK_LOCATION().AsString()));
     }
 }
 
-void CaffeParser::ResolveInPlaceLayers(caffe::NetParameter& netParameter)
+// Note: can move to CaffeParser when/if we optimise the text/string format
+//       to load on a layer by layer basis
+void CaffeParserBase::ResolveInPlaceLayers(caffe::NetParameter& netParameter)
 {
-    // Find layers with the same top
+    // Finds layers with the same top.
     std::map<std::string, std::vector<caffe::LayerParameter*>> layersByTop;
     for (int layerIdx = 0; layerIdx < netParameter.layer_size(); ++layerIdx)
     {
         caffe::LayerParameter& layer = *netParameter.mutable_layer(layerIdx);
+        std::string name = layer.name();
         for (int i = 0; i < layer.top_size(); ++i)
         {
             layersByTop[layer.top(i)].push_back(&layer);
         }
     }
 
-    // For each set of layers with the same top, resolve them to a linear chain rather than in-place layers.
+    // For each set of layers with the same top, resolves them to a linear chain rather than in-place layers.
     // Note that for 'regular' layers, there will be a single layer in each group and so this will be a no-op.
     for (auto layersWithSameTopIt : layersByTop)
     {
         const std::string& top = layersWithSameTopIt.first;
         const std::vector<caffe::LayerParameter*>& layersWithSameTop = layersWithSameTopIt.second;
 
-        // Chain the layers together in the order that they are listed in the prototxt (hopefully this is correct).
+        // Chains the layers together in the order that they are listed in the prototxt (hopefully this is correct).
         // Note that the last layer will not have its top modified so that other layers will continue to reference it.
         for (unsigned int layerIdx = 0; layerIdx < layersWithSameTop.size() - 1; ++layerIdx)
         {
@@ -1203,25 +1643,41 @@
             caffe::LayerParameter& layer2 = *layersWithSameTop[layerIdx+1];
             if (layer1.top_size() != 1)
             {
-                throw ParseException("Node '" + layer1.name() + "' is an in-place layer but "
-                    "doesn't have exactly one top.");
+                throw ParseException(
+                    boost::str(
+                        boost::format(
+                            "Node '%1%' is an in-place layer but doesn't have exactly one "
+                            "top. It has %2% instead. %3%") %
+                            layer1.name() %
+                            layer1.top_size() %
+                            CHECK_LOCATION().AsString()));
             }
             std::string newTop = layer1.name() + "_top";
             layer1.set_top(0, newTop);
             if (layer2.bottom_size() != 1 || layer2.bottom(0) != top)
             {
-                throw ParseException("Node '" + layer2.name() + "' is an in-place layer but "
-                    " doesn't have exactly one bottom, or it doesn't match its top.");
+                throw ParseException(
+                    boost::str(
+                        boost::format(
+                            "Node '%1%' is an in-place layer but "
+                            "doesn't have exactly one bottom, or it doesn't match its top. "
+                            "#bottoms=%2%, first bottom is %3%, top is %4% %5%") %
+                            layer2.name() %
+                            layer2.bottom(0) %
+                            top %
+                            CHECK_LOCATION().AsString()));
             }
             layer2.set_bottom(0, newTop);
         }
     }
 }
 
-void CaffeParser::LoadNetParam(NetParameter& netParameter)
+// Note: can move to CaffeParser when/if we optimise the text/string format
+//       to load on a layer by layer basis
+void CaffeParserBase::LoadNetParam(NetParameter& netParameter)
 {
-    // caffe models sometimes have an implicit input layer.
-    // in that case, add an explicit one
+    // Caffe models sometimes have an implicit input layer.
+    // In that case, add an explicit one.
     if (netParameter.input_size() > 0)
     {
         LayerParameter* newLayer = netParameter.add_layer();
@@ -1240,10 +1696,10 @@
         }
     }
 
-    // Replace in-place layers with regular ones to make the rest of the parsing easier.
+    // Replaces in-place layers with regular ones to make the rest of the parsing easier.
     ResolveInPlaceLayers(netParameter);
 
-    // Create a lookup of Caffe layers by name
+    // Creates a lookup of Caffe layers by name.
     for (int i = 0; i < netParameter.layer_size(); ++i)
     {
         const caffe::LayerParameter& layer = netParameter.layer(i);
@@ -1253,19 +1709,24 @@
         }
     }
 
-    // Find the output layers the user requested
+    // Finds the output layers the user requested.
     std::vector<const caffe::LayerParameter*> targetLayers;
     for (const std::string& requestedOutputName : m_RequestedOutputs)
     {
         auto nodeIt = m_CaffeLayersByTopName.find(requestedOutputName);
         if (nodeIt == m_CaffeLayersByTopName.end())
         {
-            throw ParseException("Couldn't find requested output layer '" + requestedOutputName + "' in graph");
+            throw ParseException(
+                boost::str(
+                    boost::format(
+                        "Couldn't find requested output layer '%1%' in graph %2%") %
+                        requestedOutputName %
+                        CHECK_LOCATION().AsString()));
         }
         targetLayers.push_back(nodeIt->second);
     }
 
-    // Sort them into a linear ordering such that all inputs of a node are before the node itself
+    // Sorts them into a linear ordering such that all inputs of a node are before the node itself.
     std::vector<const caffe::LayerParameter*> sortedNodes;
     if (!armnnUtils::GraphTopologicalSort<const caffe::LayerParameter*>(
         targetLayers,
@@ -1275,22 +1736,32 @@
         },
         sortedNodes))
     {
-        throw ParseException("Cycle detected in graph");
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Cycle detected in graph. #nodes: %1% %2%") %
+                    sortedNodes.size() %
+                    CHECK_LOCATION().AsString()));
     }
 
-    // Parse each node in order, knowing that all inputs of a node will be processed before the node itself
+    // Parses each node in order, knowing that all inputs of a node will be processed before the node itself.
     for (const caffe::LayerParameter* current : sortedNodes)
     {
         auto it = ms_CaffeLayerNameToParsingFunctions.find(current->type());
         if (it == ms_CaffeLayerNameToParsingFunctions.end())
         {
-            throw ParseException("Unsupported layer type '" + current->type() + "'");
+            throw ParseException(
+                boost::str(
+                    boost::format("Unsupported layer type: '%1%' for layer %2% %3%") %
+                    current->type() %
+                    current->name() %
+                    CHECK_LOCATION().AsString()));
         }
         auto func = it->second;
         (this->*func)(*current);
     }
 
-    // Add ArmNN output layers connected to each requested output
+    // Adds ArmNN output layers connected to each requested output.
     for (const std::string& requestedOutput : m_RequestedOutputs)
     {
         armnn::IOutputSlot& outputSlot = GetArmnnOutputSlotForCaffeTop(requestedOutput);
@@ -1304,7 +1775,7 @@
     }
 }
 
-INetworkPtr CaffeParser::CreateNetworkFromTextFile(const char* graphFile,
+INetworkPtr CaffeParserBase::CreateNetworkFromTextFile(const char* graphFile,
     const std::map<std::string, armnn::TensorShape>& inputShapes,
     const std::vector<std::string>& requestedOutputs)
 {
@@ -1312,12 +1783,15 @@
 
     if (fd == nullptr)
     {
-        std::stringstream error;
-        error << "Graph file " << graphFile << " failed to open";
-        throw FileNotFoundException(error.str());
+        throw FileNotFoundException(
+            boost::str(
+                boost::format(
+                    "Failed to open graph file: %1% %2%") %
+                    graphFile %
+                    CHECK_LOCATION().AsString()));
     }
 
-    // Parse the file into a message
+    // Parses the file into a message.
     NetParameter netParam;
     auto         input   = new google::protobuf::io::FileInputStream(fileno(fd));
     bool         success = google::protobuf::TextFormat::Parse(input, &netParam);
@@ -1326,27 +1800,32 @@
 
     if (!success)
     {
-        std::stringstream error;
-        error << "Failed to parse graph file";
-        throw ParseException(error.str());
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Failed to parse graph file: %1% %2%") %
+                    graphFile %
+                    CHECK_LOCATION().AsString()));
     }
 
     return CreateNetworkFromNetParameter(netParam, inputShapes, requestedOutputs);
 }
 
-INetworkPtr CaffeParser::CreateNetworkFromString(const char* protoText,
+INetworkPtr CaffeParserBase::CreateNetworkFromString(const char* protoText,
     const std::map<std::string, armnn::TensorShape>& inputShapes,
     const std::vector<std::string>& requestedOutputs)
 {
-    // Parse the string into a message
+    // Parses the string into a message.
     NetParameter netParam;
     bool         success = google::protobuf::TextFormat::ParseFromString(protoText, &netParam);
 
     if (!success)
     {
-        std::stringstream error;
-        error << "Failed to parse graph string";
-        throw ParseException(error.str());
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Failed to parse graph string %1%") %
+                    CHECK_LOCATION().AsString()));
     }
 
     return CreateNetworkFromNetParameter(netParam, inputShapes, requestedOutputs);
@@ -1360,12 +1839,15 @@
 
     if (fd == nullptr)
     {
-        std::stringstream error;
-        error << "Graph file " << graphFile << " failed to open";
-        throw FileNotFoundException(error.str());
+        throw FileNotFoundException(
+            boost::str(
+                boost::format(
+                    "Failed to open graph file at: %1% %2%") %
+                    graphFile %
+                    CHECK_LOCATION().AsString()));
     }
 
-    // Parse the file into a message
+    // Parses the file into a message.
     NetParameter netParam;
 
     FileInputStream  inStream(fileno(fd));
@@ -1376,15 +1858,20 @@
 
     if (!success)
     {
-        std::stringstream error;
-        error << "Failed to parse protobuf file" << graphFile;
-        throw ParseException(error.str());
+        throw ParseException(
+            boost::str(
+                boost::format(
+                    "Failed to parse protobuf file: %1% %2%") %
+                    graphFile %
+                    CHECK_LOCATION().AsString()));
     }
 
     return CreateNetworkFromNetParameter(netParam, inputShapes, requestedOutputs);
 }
 
-INetworkPtr CaffeParser::CreateNetworkFromNetParameter(NetParameter& netParam,
+// Note: can move to CaffeParser when/if we optimise the text/string format
+//       to load on a layer by layer basis
+INetworkPtr CaffeParserBase::CreateNetworkFromNetParameter(NetParameter& netParam,
     const std::map<std::string, armnn::TensorShape>& inputShapes,
     const std::vector<std::string>& requestedOutputs)
 {
@@ -1415,15 +1902,15 @@
     return move(m_Network);
 }
 
-void CaffeParser::Cleanup()
-{
+void CaffeParserBase::Cleanup() {
     // cleanup, in case we reuse this parser
-    m_CaffeLayersByTopName.clear();
     m_InputShapes.clear();
     m_RequestedOutputs.clear();
     m_ArmnnOutputSlotForCaffeTop.clear();
+    // NOTE: when we get the text/string format
+    //       optimised for memory then this data structure can
+    //       also move to the CaffeParser class
+    m_CaffeLayersByTopName.clear();
 }
 
 }
-
-