diff options
Diffstat (limited to 'src/armnn/test/SubgraphViewTests.cpp')
-rw-r--r-- | src/armnn/test/SubgraphViewTests.cpp | 524 |
1 files changed, 474 insertions, 50 deletions
diff --git a/src/armnn/test/SubgraphViewTests.cpp b/src/armnn/test/SubgraphViewTests.cpp index 3e762e2de5..0f2c5b3617 100644 --- a/src/armnn/test/SubgraphViewTests.cpp +++ b/src/armnn/test/SubgraphViewTests.cpp @@ -11,7 +11,11 @@ #include <SubgraphViewSelector.hpp> #include <backendsCommon/CpuTensorHandle.hpp> - +#include <fstream> +#include <map> +#include <queue> +#include <random> +#include <chrono> using namespace armnn; namespace @@ -513,16 +517,60 @@ BOOST_AUTO_TEST_CASE(MultipleLayersSelectedInTheMiddle) } } +BOOST_AUTO_TEST_CASE(DisjointGraphs) +{ + // The input graph has two disjoint sections and all layers are selected. + // This should result in two subgraphs being produced. + Graph graph; + + // the graph is constructed in reverse order + auto o0 = graph.AddLayer<OutputLayer>(0, "output0"); + auto n0 = graph.InsertNewLayer<ActivationLayer>(o0->GetInputSlot(0), ActivationDescriptor{}, "intermediate0"); + auto i0 = graph.InsertNewLayer<InputLayer>(n0->GetInputSlot(0), 0, "input0"); + + auto o1 = graph.AddLayer<OutputLayer>(1, "output1"); + auto n1 = graph.InsertNewLayer<ActivationLayer>(o1->GetInputSlot(0), ActivationDescriptor{}, "intermediate1"); + auto i1 = graph.InsertNewLayer<InputLayer>(n1->GetInputSlot(0), 1, "input1"); + + SubgraphViewSelector::Subgraphs subgraphs = + SubgraphViewSelector::SelectSubgraphs(graph, + // select the middle layers only + [](const Layer& l) { + return true; + }); + + // expected results to test against + auto expected1 = CreateSubgraphViewFrom({}, {}, { o0, n0, i0 }); + auto expected2 = CreateSubgraphViewFrom({}, {}, { o1, n1, i1 }); + BOOST_TEST(subgraphs.size() == 2); + if (subgraphs.size() == 2) + { + BOOST_TEST((subgraphs[0] != nullptr)); + BOOST_TEST((subgraphs[1] != nullptr)); + if (subgraphs[0].get() != nullptr && subgraphs[1].get() != nullptr) + { + if (std::find(subgraphs[0]->GetLayers().begin(), subgraphs[0]->GetLayers().end(), i0) != + subgraphs[0]->GetLayers().end()) + { + CompareSubgraphViews(subgraphs[0], expected1); + CompareSubgraphViews(subgraphs[1], expected2); + } + else + { + CompareSubgraphViews(subgraphs[0], expected2); + CompareSubgraphViews(subgraphs[1], expected1); + } + } + } +} + BOOST_AUTO_TEST_CASE(IslandInTheMiddle) { // This case represent the scenario when a non-selected X1 node placed in the middle - // of the selected M* nodes: - // - // X0 -> M1 -> M2 -> M3 -> X2 - // X0 -> M4 -> X1 -> M5 -> X2 - // + // of the selected M* nodes. + // This checks that we don't merge M6 and M3 and create a dependency loop. /* - X0 + M0 / \ M1 M4 | | @@ -530,59 +578,56 @@ BOOST_AUTO_TEST_CASE(IslandInTheMiddle) | | M3 M5 \ / - X2 + M6 */ - // The expected result for this is that M1,M2,M3,M4 will be part of one subgraph and - // M5 will be part of another subgraph and the input and output slots in the subgraphs - // will be set accordingly. - // Graph graph; OriginsDescriptor concatDescriptor(2); - auto x2 = graph.AddLayer<ConcatLayer>(concatDescriptor, "x2"); - auto m3 = graph.InsertNewLayer<ActivationLayer>(x2->GetInputSlot(0), - ActivationDescriptor{}, - "m3"); + auto m6 = graph.AddLayer<ConcatLayer>(concatDescriptor, "m6"); + auto m3 = graph.InsertNewLayer<ActivationLayer>(m6->GetInputSlot(0), + ActivationDescriptor{}, + "m3"); auto m2 = graph.InsertNewLayer<ActivationLayer>(m3->GetInputSlot(0), - ActivationDescriptor{}, - "m2"); + ActivationDescriptor{}, + "m2"); auto m1 = graph.InsertNewLayer<ActivationLayer>(m2->GetInputSlot(0), - ActivationDescriptor{}, - "m1"); - auto x0 = graph.InsertNewLayer<InputLayer>(m1->GetInputSlot(0), 0, "x0"); - - auto m5 = graph.InsertNewLayer<ActivationLayer>(x2->GetInputSlot(1), - ActivationDescriptor{}, - "m5"); - auto x1 = graph.InsertNewLayer<Convolution2dLayer>(m5->GetInputSlot(0), - Convolution2dDescriptor{}, - "x1"); + ActivationDescriptor{}, + "m1"); + auto m0 = graph.InsertNewLayer<InputLayer>(m1->GetInputSlot(0), 0, "m0"); + + auto m5 = graph.InsertNewLayer<ActivationLayer>(m6->GetInputSlot(1), + ActivationDescriptor{}, + "m5"); + auto x1 = graph.InsertNewLayer<ActivationLayer>(m5->GetInputSlot(0), + ActivationDescriptor{}, + "x1"); auto m4 = graph.InsertNewLayer<ActivationLayer>(x1->GetInputSlot(0), - ActivationDescriptor{}, - "m4"); + ActivationDescriptor{}, + "m4"); // Connect the other branch to the input layer - x0->GetOutputSlot(0).Connect(m4->GetInputSlot(0)); + m0->GetOutputSlot(0).Connect(m4->GetInputSlot(0)); // All selected 'M*' layers will be of Activation type SubgraphViewSelector::Subgraphs subgraphs = SubgraphViewSelector::SelectSubgraphs( graph, // select the middle layers only - [](const Layer & l) - { - bool toSelect = (l.GetType() == LayerType::Activation); - return toSelect; - }); + [](const Layer& l) + { + bool toSelect = std::string(l.GetName())[0] == 'm'; + return toSelect; + }); // expected results to test against - auto largerSubgraph = CreateSubgraphViewFrom(CreateInputsFrom({m1, m4}), - CreateOutputsFrom({m3, m4}), - {m1, m4, m2, m3}); + auto largerSubgraph = CreateSubgraphViewFrom(CreateInputsFrom({ m0 }), + CreateOutputsFrom({ m3, m4 }), + { m0, m1, m2, m3, m4 }); - auto smallerSubgraph = CreateSubgraphViewFrom(CreateInputsFrom({m5}), - CreateOutputsFrom({m5}), - {m5}); + auto smallerSubgraph = + CreateSubgraphViewFrom(std::vector<InputSlot*>{ &m5->GetInputSlot(0), & m6->GetInputSlot(0) }, + std::vector<OutputSlot*>{}, + { m5, m6 }); BOOST_TEST(subgraphs.size() == 2); if (subgraphs.size() == 2) @@ -595,15 +640,14 @@ BOOST_AUTO_TEST_CASE(IslandInTheMiddle) { // sort the subgraphs by layer size, so it is simpler to test std::sort(subgraphs.begin(), subgraphs.end(), - [](SubgraphViewSelector::SubgraphViewPtr & lhs, SubgraphViewSelector::SubgraphViewPtr & rhs) - { - return (lhs->GetLayers().size() < rhs->GetLayers().size()); - } + [](SubgraphViewSelector::SubgraphViewPtr& lhs, SubgraphViewSelector::SubgraphViewPtr& rhs) + { + return (lhs->GetLayers().size() < rhs->GetLayers().size()); + } ); - // one subgraph needs to be size=1 and the other one is 4 - BOOST_TEST(subgraphs[0]->GetLayers().size() == 1); - BOOST_TEST(subgraphs[1]->GetLayers().size() == 4); + BOOST_TEST(subgraphs[0]->GetLayers().size() == 2); + BOOST_TEST(subgraphs[1]->GetLayers().size() == 5); CompareSubgraphViews(subgraphs[0], smallerSubgraph); CompareSubgraphViews(subgraphs[1], largerSubgraph); @@ -804,7 +848,7 @@ BOOST_AUTO_TEST_CASE(SingleInputMultiOutput) Layer* layerX2 = graph.AddLayer<OutputLayer>(0, "layerX2"); Layer* layerX3 = graph.AddLayer<OutputLayer>(1, "layerX3"); - // X2 + // X1 // | // M1 // /| @@ -907,6 +951,386 @@ BOOST_AUTO_TEST_CASE(MultiInputMultiOutput) } } +BOOST_AUTO_TEST_CASE(ValidMerge) +{ + // Checks that a node that has multiple choices for merge candidates (M3 in this case) correctly merges with the + // one that it can (M0), and doesn't merge with the ones it can't (X2 and M2). + // + // X1 + // | + // M1 + // / \' + // X2 M2 M0 + // \ | / + // M3 + // + Graph graph; + + ActivationDescriptor activationDefaults; + OriginsDescriptor concatDescriptor(3); + + auto x1 = graph.AddLayer<InputLayer>(0, "x1"); + auto x2 = graph.AddLayer<ActivationLayer>(activationDefaults, "x2"); + auto m0 = graph.AddLayer<InputLayer>(1, "m0"); + auto m1 = graph.AddLayer<ActivationLayer>(activationDefaults, "m1"); + auto m2 = graph.AddLayer<ActivationLayer>(activationDefaults, "m2"); + auto m3 = graph.AddLayer<ConcatLayer>(concatDescriptor, "m3"); + + x1->GetOutputSlot(0).Connect(m1->GetInputSlot(0)); + m1->GetOutputSlot(0).Connect(x2->GetInputSlot(0)); + m1->GetOutputSlot(0).Connect(m2->GetInputSlot(0)); + x2->GetOutputSlot(0).Connect(m3->GetInputSlot(0)); + m2->GetOutputSlot(0).Connect(m3->GetInputSlot(1)); + m0->GetOutputSlot(0).Connect(m3->GetInputSlot(2)); + + SubgraphViewSelector::Subgraphs subgraphs = SubgraphViewSelector::SelectSubgraphs( + graph, + [](const Layer& l) { + return std::string(l.GetName())[0] == 'm'; + }); + + // expected results to test against + auto expectedSubgraph0 = + CreateSubgraphViewFrom( + CreateInputsFrom({ m1 }), + std::vector<OutputSlot*>{ &m1->GetOutputSlot(0), &m2->GetOutputSlot(0) }, + { m1, m2 }); + + auto expectedSubgraph1 = CreateSubgraphViewFrom( + std::vector<InputSlot*>{ &m3->GetInputSlot(0), & m3->GetInputSlot(1) }, + CreateOutputsFrom({ }), + { m0, m3 }); + + BOOST_TEST(subgraphs.size() == 2); + if (subgraphs.size() == 2) + { + // we need to have valid subgraph pointers here + BOOST_TEST((subgraphs[0] != nullptr)); + BOOST_TEST((subgraphs[1] != nullptr)); + + if (subgraphs[0].get() != nullptr && subgraphs[1].get() != nullptr) + { + if (subgraphs[0]->GetInputSlots().size() == 1) + { + CompareSubgraphViews(subgraphs[0], expectedSubgraph0); + CompareSubgraphViews(subgraphs[1], expectedSubgraph1); + } + else + { + CompareSubgraphViews(subgraphs[0], expectedSubgraph1); + CompareSubgraphViews(subgraphs[1], expectedSubgraph0); + } + } + } +} + +BOOST_AUTO_TEST_CASE(PropagatedDependencies) +{ + // Version of IslandInTheMiddle with longer chain + // to make sure antecedents are propagated. + /* + M0 + / \ + M1 M4 + | | + M2 X1 < the island in the middle ! + | | + | M10 + | | + | X2 < another island in the middle ! + | | + M3 M5 + \ / + M6 + */ + Graph graph; + + OriginsDescriptor concatDescriptor(2); + auto m6 = graph.AddLayer<ConcatLayer>(concatDescriptor, "m6"); + auto m3 = graph.InsertNewLayer<ActivationLayer>(m6->GetInputSlot(0), + ActivationDescriptor{}, + "m3"); + auto m2 = graph.InsertNewLayer<ActivationLayer>(m3->GetInputSlot(0), + ActivationDescriptor{}, + "m2"); + auto m1 = graph.InsertNewLayer<ActivationLayer>(m2->GetInputSlot(0), + ActivationDescriptor{}, + "m1"); + auto m0 = graph.InsertNewLayer<InputLayer>(m1->GetInputSlot(0), 0, "m0"); + + auto m5 = graph.InsertNewLayer<ActivationLayer>(m6->GetInputSlot(1), + ActivationDescriptor{}, + "m5"); + auto x2 = graph.InsertNewLayer<ActivationLayer>(m5->GetInputSlot(0), ActivationDescriptor{}, "x2"); + auto m10 = graph.InsertNewLayer<ActivationLayer>(x2->GetInputSlot(0), ActivationDescriptor{}, "m10"); + auto x1 = graph.InsertNewLayer<ActivationLayer>(m10->GetInputSlot(0), + ActivationDescriptor{}, + "x1"); + auto m4 = graph.InsertNewLayer<ActivationLayer>(x1->GetInputSlot(0), + ActivationDescriptor{}, + "m4"); + + // Connect the other branch to the input layer + m0->GetOutputSlot(0).Connect(m4->GetInputSlot(0)); + + // All selected 'M*' layers will be of Activation type + SubgraphViewSelector::Subgraphs subgraphs = + SubgraphViewSelector::SelectSubgraphs( + graph, + // select the middle layers only + [](const Layer& l) + { + bool toSelect = std::string(l.GetName())[0] == 'm'; + return toSelect; + }); + + // expected results to test against + auto largerSubgraph = CreateSubgraphViewFrom(CreateInputsFrom({ m0 }), + CreateOutputsFrom({ m3, m4 }), + { m0, m1, m2, m3, m4 }); + + auto mediumSubgraph = CreateSubgraphViewFrom(std::vector<InputSlot*>{ &m5->GetInputSlot(0), &m6->GetInputSlot(0) }, + std::vector<OutputSlot*>{}, { m5, m6 }); + + auto smallerSubgraph = + CreateSubgraphViewFrom(CreateInputsFrom({ m10 }), CreateOutputsFrom({ m10 }), { m10 }); + + BOOST_TEST(subgraphs.size() == 3); + if (subgraphs.size() == 3) + { + // we need to have valid subgraph pointers here + BOOST_TEST((subgraphs[0] != nullptr)); + BOOST_TEST((subgraphs[1] != nullptr)); + BOOST_TEST((subgraphs[2] != nullptr)); + + if (subgraphs[0].get() != nullptr && subgraphs[1].get() != nullptr && subgraphs[2].get() != nullptr) + { + // sort the subgraphs by layer size, so it is simpler to test + std::sort(subgraphs.begin(), subgraphs.end(), + [](SubgraphViewSelector::SubgraphViewPtr& lhs, SubgraphViewSelector::SubgraphViewPtr& rhs) + { + return (lhs->GetLayers().size() < rhs->GetLayers().size()); + } + ); + + CompareSubgraphViews(subgraphs[0], smallerSubgraph); + CompareSubgraphViews(subgraphs[1], mediumSubgraph); + CompareSubgraphViews(subgraphs[2], largerSubgraph); + } + } +} + +BOOST_AUTO_TEST_CASE(Random) +{ + // Creates random networks, splits them into subgraphs and checks the resulting subgraphs obey the required + // dependency rules. We can easily generate very large networks which helps cover corner cases the other + // small, manually crafted tests have missed. We can also use this to measure performance on large networks. + constexpr bool debug = false; // Enable this to dump dot files and performance timings. + + std::mt19937 randomGenerator; + + // Helper function to get a random number in [0, maxExclusive) + auto GetRandom = [&randomGenerator](auto maxExclusive) { + // Note we could use uniform_int_distribution here, but that gives inconsistent results across platforms + // which makes it harder to reproduce results. + // It appears that uniform_real_distribution is consistent across MSVC and gcc so we use that and round it. + std::uniform_real_distribution<float> uniform(0.0f, 1.0f); + return static_cast<decltype(maxExclusive)>(uniform(randomGenerator) * static_cast<float>(maxExclusive)); + }; + // Helper function to get a bool that has probability 'trueProb' of being true. + auto GetRandomFlag = [&randomGenerator](float trueProb) { + std::uniform_real_distribution<float> uniform(0.0f, 1.0f); + return uniform(randomGenerator) < trueProb; + }; + + constexpr uint32_t numTests = 100; + for (uint32_t testIdx = 0; testIdx < numTests; ++testIdx) + { + randomGenerator.seed(testIdx); // Set a deterministic seed for reproducibility. + + // Create random graph + Graph graph; + { + // First add the layers, without any connections. The following random constants determine the number of + // each layer to add, along with the chance that each layer will be 'supported' (i.e. selected for + // inclusion in the resulting subgraphs). + uint32_t numInputs = 1 + GetRandom(4u); + uint32_t numConstants = 1 + GetRandom(4u); + uint32_t numOutputs = 1 + GetRandom(4u); + uint32_t numConcats = 0 + GetRandom(500u); + uint32_t numSplits = 0 + GetRandom(500u); + float supportedProb = 0.7f; + + for (uint32_t i = 0; i < numInputs; ++i) + { + std::string name = "input" + std::to_string(i) + (GetRandomFlag(supportedProb) ? "S" : "N"); + graph.AddLayer<InputLayer>(static_cast<LayerBindingId>(i), name.c_str()); + } + for (uint32_t i = 0; i < numConstants; ++i) + { + std::string name = "constant" + std::to_string(i) + (GetRandomFlag(supportedProb) ? "S" : "N"); + graph.AddLayer<ConstantLayer>(name.c_str()); + } + for (uint32_t i = 0; i < numOutputs; ++i) + { + std::string name = "output" + std::to_string(i) + (GetRandomFlag(supportedProb) ? "S" : "N"); + graph.AddLayer<OutputLayer>(static_cast<LayerBindingId>(i), name.c_str()); + } + for (uint32_t i = 0; i < numConcats; ++i) + { + std::string name = "concat" + std::to_string(i) + (GetRandomFlag(supportedProb) ? "S" : "N"); + uint32_t numInputs = 1 + GetRandom(3u); + OriginsDescriptor concatDesc(numInputs); + graph.AddLayer<ConcatLayer>(concatDesc, name.c_str()); + } + for (uint32_t i = 0; i < numSplits; ++i) + { + std::string name = "split" + std::to_string(i) + (GetRandomFlag(supportedProb) ? "S" : "N"); + uint32_t numOutputs = 1 + GetRandom(3u); + ViewsDescriptor splitDesc(numOutputs); + graph.AddLayer<SplitterLayer>(splitDesc, name.c_str()); + } + + // Associate each layer with a "depth" parameter. This is used when creating connections to ensure + // that we don't have any loops, by only connecting to layers with a lower "depth". + // This can be thought of as distance from the "top" of the graph (assuming the graph flows top-to-bottom). + // Unfortunately this approach ends up producing very "wide" graphs, + // which probably isn't very representative of 'real' networks. + uint32_t maxLayerDepth = 5 + GetRandom(2000u); + std::map<Layer*, uint32_t> layerDepths; + std::map<uint32_t, std::vector<Layer*>> layersAtDepth; + for (Layer* layer : graph) + { + uint32_t depth; + if (layer->GetType() == LayerType::Input || layer->GetType() == LayerType::Constant) + { + // There needs to be at least one input-like layer above everything else, otherwise would be + // nothing for them to connect to! + depth = 0; + } + else + { + // Other layers are randomly assigned to later depths. + depth = 1 + GetRandom(maxLayerDepth); + } + layerDepths[layer] = depth; + layersAtDepth[depth].push_back(layer); + } + + // Connect layers to each other. Every input slot of every layer must be connected, but it doesn't + // matter if an output slot goes unused. + for (Layer* layer : graph) + { + for (uint32_t inputSlotIdx = 0; inputSlotIdx < layer->GetNumInputSlots(); ++inputSlotIdx) + { + InputSlot& inputSlot = layer->GetInputSlot(inputSlotIdx); + uint32_t maxLayerDepthToConnectTo = layerDepths[layer]; // This prevents a connection causing a loop + // Finding a layer to connect to may take multiple attempts, so keep trying until it works. + while (inputSlot.GetConnectedOutputSlot() == nullptr) + { + uint32_t layerDepth = GetRandom(maxLayerDepthToConnectTo); + const std::vector<Layer*>& layersToChooseFrom = layersAtDepth[layerDepth]; + if (layersToChooseFrom.size() == 0) + { + continue; + } + Layer* layerToConnectWith = layersToChooseFrom[GetRandom(layersToChooseFrom.size())]; + if (layerToConnectWith->GetNumOutputSlots() == 0) + { + continue; + } + uint32_t outputSlotIdx = GetRandom(layerToConnectWith->GetNumOutputSlots()); + layerToConnectWith->GetOutputSlot(outputSlotIdx).Connect(inputSlot); + } + } + } + } + + if (debug) + { + std::ofstream f("INPUT_" + std::to_string(testIdx) + ".dot"); + graph.SerializeToDot(f); + } + + // Run the splitting algorithm, selecting all nodes ending in an 'S' (as randomly assigned above). + auto startTime = std::chrono::high_resolution_clock::now(); + + SubgraphViewSelector::Subgraphs subgraphs = + SubgraphViewSelector::SelectSubgraphs(graph, + [](const Layer& l) { return std::string(l.GetName()).back() == 'S'; }); + + auto endTime = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::microseconds>(endTime - startTime); + if (debug) + { + std::cout << "Test " << testIdx << ": " << duration.count() << " microseconds" << std::endl; + } + + // Build a map of which subgraph is assigned to each layer. + // This helps some of the following code. + std::map<Layer*, SubgraphView*> layerToSubgraph; + for (Layer* layer : graph) + { + size_t i = 0; + for (std::unique_ptr<SubgraphView>& subgraph : subgraphs) + { + std::string name = std::to_string(i++); + if (std::find(subgraph->begin(), subgraph->end(), layer) != subgraph->end()) + { + layerToSubgraph[layer] = subgraph.get(); + break; + } + } + } + + if (debug) + { + // Before dumping the dot file, set each Layer's BackendId property so that the dot file + // shows the resulting subgraph assignments. + for (Layer* layer : graph) + { + std::string name = "NotAssigned"; + auto subgraphIt = layerToSubgraph.find(layer); + if (subgraphIt != layerToSubgraph.end()) + { + auto subgraphIdx = std::distance(subgraphs.begin(), + std::find_if(subgraphs.begin(), subgraphs.end(), + [&](auto& s) { return s.get() == subgraphIt->second; })); + name = std::to_string(subgraphIdx); + } + layer->SetBackendId(armnn::BackendId(name)); + } + + std::ofstream f("GRAPH_" + std::to_string(testIdx) + ".dot"); + graph.SerializeToDot(f); + } + + // Check the dependencies between subgraphs to make sure that the algorithm has produced a valid result. + // Starting from each of the input slots of each subgraph, recurse up the graph and ensure that we never + // encounter a layer that belongs to the subgraph that we started from. + for (std::unique_ptr<SubgraphView>& subgraph : subgraphs) + { + for (InputSlot* inputSlot : subgraph->GetInputSlots()) + { + std::queue<Layer*> toProcess; + toProcess.push(&inputSlot->GetConnectedOutputSlot()->GetOwningLayer()); + while (toProcess.size() > 0) + { + Layer* l = toProcess.front(); + toProcess.pop(); + + BOOST_CHECK(layerToSubgraph[l] != subgraph.get()); + + for (const InputSlot& is : l->GetInputSlots()) + { + toProcess.push(&is.GetConnectedOutputSlot()->GetOwningLayer()); + } + } + } + } + } +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(IntegrationTests) |