ArmNN
 25.11
Loading...
Searching...
No Matches
SubgraphViewSelector.cpp
Go to the documentation of this file.
1//
2// Copyright © 2017,2024 Arm Ltd and Contributors. All rights reserved.
3// SPDX-License-Identifier: MIT
4//
5
7#include "Graph.hpp"
8
12
13#include <algorithm>
14#include <map>
15#include <queue>
16#include <unordered_set>
17
18namespace armnn
19{
20
21namespace
22{
23
24/// Intermediate data-structure to store the subgraph that a layer has been assigned to.
25/// This is a "disjoint set" data structure that allows efficient merging of subgraphs,
26/// which is a key part of the algorithm. Subgraphs are arranged in singly-linked trees
27/// (with each node storing a pointer to its parent). Subgraphs in the same tree are considered
28/// to have been merged. Merging subgraphs is performed by attaching one tree to another,
29/// which is a simple pointer update.
30///
31/// NOTE: Due to the way this is stored, it is almost never correct to directly compare pointers
32/// to two PartialSubgraphs to check if two layers belong in the same subgraph. Instead you
33/// should use IsMergedWith().
34///
35/// This structure also stores information about the dependencies of each subgraph, which is needed
36/// to determine whether certain subgraphs can be merged. Checking whether a subgraph
37/// depends on another subgraph is a frequent operation in the algorithm (see AssignSplitId) and so this is optimized
38/// in preference to the merging of subgraphs. This leads to an approach where each subgraph stores
39/// a set of all the subgraphs it depends on (for a fast lookup). In order to efficiently update this
40/// set as subgraphs are merged means we also store a set of subgraphs which *depend on us* (i.e. the
41/// complement of our dependencies).
42class PartialSubgraph
43{
44public:
45 /// If this subgraph has been merged with another then there is an agreed "representative" for the combined
46 /// subgraph, which uniquely identifies the subgraph.
47 PartialSubgraph* GetRepresentative()
48 {
49 // Recurse up the tree to find the root node.
50 if (m_Parent == nullptr)
51 {
52 return this;
53 }
54 else
55 {
56 PartialSubgraph* result = m_Parent->GetRepresentative();
57 // Update our parent pointer to point directly to the root in order to speed up future calls to this method.
58 // This essentially "flattens" the tree.
59 m_Parent = result;
60 return result;
61 }
62 }
63
64 /// Merges this subgraph with another.
65 void MergeWith(PartialSubgraph* other)
66 {
67 if (m_Parent == nullptr)
68 {
69 other = other->GetRepresentative();
70 if (this == other)
71 {
72 // Already merged - no-op
73 return;
74 }
75 m_Parent = other;
76
77 // Update others' dependency sets to point to the new representative rather than us.
78 // Keeping these up-to-date means we can rely on these sets containing representatives when
79 // we perform a lookup in HasAntecedent() and so don't need to resolve the representative for each element
80 // of the set. See description at the top of this class for more rationale.
81 for (PartialSubgraph* a : m_Antecedents)
82 {
83 size_t numErased = a->m_Dependants.erase(this);
84 if (numErased != 1)
85 {
86 throw armnn::Exception("number of dependents erased must only be 1.");
87 }
88 a->m_Dependants.insert(m_Parent);
89 }
90 for (PartialSubgraph* a : m_Dependants)
91 {
92 size_t numErased = a->m_Antecedents.erase(this);
93 if (numErased != 1)
94 {
95 throw armnn::Exception("number of antecedents erased must only be 1.");
96 }
97 IgnoreUnused(numErased);
98 a->m_Antecedents.insert(m_Parent);
99 }
100
101 // Merge our dependency sets into our new representative.
102 // We no longer need to maintain our own sets, as requests will always be forwarded to the representative.
103 m_Parent->m_Antecedents.insert(m_Antecedents.begin(), m_Antecedents.end());
104 m_Antecedents.clear();
105 m_Parent->m_Dependants.insert(m_Dependants.begin(), m_Dependants.end());
106 m_Dependants.clear();
107 }
108 else
109 {
110 // Defer request to the representative
111 GetRepresentative()->MergeWith(other);
112 }
113 }
114
115 /// Checks if this subgraph has been merged with the given subgraph.
116 bool IsMergedWith(PartialSubgraph* other)
117 {
118 return GetRepresentative() == other->GetRepresentative();
119 }
120
121 /// Marks the given subgraph as a direct antecedent (dependency) of this one.
122 void AddDirectAntecedent(PartialSubgraph* antecedent)
123 {
124 if (m_Parent == nullptr)
125 {
126 antecedent = antecedent->GetRepresentative();
127
128 m_Antecedents.insert(antecedent);
129 // Also record all of its antecedents, so that we end up with direct and indirect antecedents.
130 // This makes the lookup in HasAntecedent() faster.
131 m_Antecedents.insert(antecedent->m_Antecedents.begin(), antecedent->m_Antecedents.end());
132 // All of our dependents also need to include the new antecedents
133 for (PartialSubgraph* d : m_Dependants)
134 {
135 d->m_Antecedents.insert(antecedent);
136 d->m_Antecedents.insert(antecedent->m_Antecedents.begin(), antecedent->m_Antecedents.end());
137 }
138
139 // Store reverse dependencies as well, required so that we can efficiently navigate the graph
140 // when making updates.
141 antecedent->m_Dependants.insert(this);
142 antecedent->m_Dependants.insert(m_Dependants.begin(), m_Dependants.end());
143 for (PartialSubgraph* a : antecedent->m_Antecedents)
144 {
145 a->m_Dependants.insert(this);
146 a->m_Dependants.insert(m_Dependants.begin(), m_Dependants.end());
147 }
148 }
149 else
150 {
151 // Defer request to the representative
152 GetRepresentative()->AddDirectAntecedent(antecedent);
153 }
154 }
155
156 /// Checks if this subgraph is dependent on the given subgraph, either directly or indirectly.
157 bool HasAntecedent(PartialSubgraph* antecedent)
158 {
159 if (m_Parent == nullptr)
160 {
161 antecedent = antecedent->GetRepresentative();
162 // Thanks to keeping this set updated in MergeWith and AddDirectAntecedent, we can do an efficient lookup.
163 return m_Antecedents.count(antecedent) > 0;
164 }
165 else
166 {
167 // Defer request to the representative
168 return GetRepresentative()->HasAntecedent(antecedent);
169 }
170 }
171
172private:
173 /// Pointer to the parent node in the tree. If this is null then we are the representative for our merged subgraph.
174 PartialSubgraph* m_Parent;
175 /// The representatives of all the subgraphs which we depend on, either directly or indirectly.
176 std::unordered_set<PartialSubgraph*> m_Antecedents;
177 /// The representatives of all the subgraphs which depend on us, either directly or indirectly.
178 std::unordered_set<PartialSubgraph*> m_Dependants;
179};
180
181/// Intermediate data structure to store information associated with a particular layer.
182struct LayerSelectionInfo
183{
184 using LayerInfoContainer = std::map<IConnectableLayer*, LayerSelectionInfo>;
185 using LayerInfoQueue = std::queue<LayerSelectionInfo*>;
186
187 LayerSelectionInfo(Layer* layer, const SubgraphViewSelector::LayerSelectorFunction& selector)
188 : m_Layer{layer}
189 , m_Subgraph{nullptr}
190 , m_IsSelected{selector(*layer)}
191 , m_IsProcessed(false)
192 {
193 }
194
195 bool IsInputLayer() const
196 {
197 return m_Layer->GetType() == armnn::LayerType::Input || m_Layer->GetType() == armnn::LayerType::Constant;
198 }
199
200 void CollectNonSelectedInputs(LayerSelectionInfo::LayerInfoContainer& layerInfos,
201 SubgraphView::IInputSlots& inputSlots)
202 {
203 for (auto&& slot = PolymorphicDowncast<Layer*>(m_Layer)->BeginInputSlots();
204 slot != PolymorphicDowncast<Layer*>(m_Layer)->EndInputSlots();
205 ++slot)
206 {
207 OutputSlot* parentLayerOutputSlot = slot->GetConnectedOutputSlot();
208
209 if (!parentLayerOutputSlot)
210 {
211 throw armnn::NullPointerException("The input slots must be connected here.");
212 }
213
214 if (parentLayerOutputSlot)
215 {
216 Layer& parentLayer = parentLayerOutputSlot->GetOwningLayer();
217 auto parentInfo = layerInfos.find(&parentLayer);
218 if (parentInfo == layerInfos.end() ||
219 !m_Subgraph->IsMergedWith(parentInfo->second.m_Subgraph.get()))
220 {
221 // Avoid collecting duplicate input slots
222 InputSlot* inputSlot = &(*slot);
223 if (std::find(inputSlots.begin(), inputSlots.end(), inputSlot) == inputSlots.end())
224 {
225 inputSlots.push_back(inputSlot);
226 }
227 }
228 }
229 }
230 }
231
232 void CollectNonSelectedOutputSlots(LayerSelectionInfo::LayerInfoContainer& layerInfos,
233 SubgraphView::IOutputSlots& outputSlots)
234 {
235 for (auto&& slot = PolymorphicDowncast<Layer*>(m_Layer)->BeginOutputSlots();
236 slot != PolymorphicDowncast<Layer*>(m_Layer)->EndOutputSlots();
237 ++slot)
238 {
239 for (InputSlot* childLayerInputSlot : slot->GetConnections())
240 {
241 Layer& childLayer = childLayerInputSlot->GetOwningLayer();
242 auto childInfo = layerInfos.find(&childLayer);
243 if (childInfo == layerInfos.end() ||
244 !m_Subgraph->IsMergedWith(childInfo->second.m_Subgraph.get()))
245 {
246 // Avoid collecting duplicate output slots
247 OutputSlot* outputSlot = &(*slot);
248 if (std::find(outputSlots.begin(), outputSlots.end(), outputSlot) == outputSlots.end())
249 {
250 outputSlots.push_back(outputSlot);
251 }
252 }
253 }
254 }
255 }
256
257 IConnectableLayer* m_Layer;
258 /// Which subgraph this layer has been assigned to. Only valid once m_IsProcessed is true.
259 /// Two layers with different m_Subgraph pointers may in fact have been merged into the same subgraph -
260 /// see the description of the PartialSubgraph class.
261 std::shared_ptr<PartialSubgraph> m_Subgraph;
262 bool m_IsSelected;
263 bool m_IsProcessed;
264};
265
266} // namespace <anonymous>
267
270{
271 SubgraphView subgraph(graph);
272 return SubgraphViewSelector::SelectSubgraphs(subgraph, selector);
273}
274
275
276template<typename Delegate>
277void ForEachLayerInput(LayerSelectionInfo::LayerInfoContainer& layerInfos,
278 LayerSelectionInfo& layerInfo,
279 Delegate function)
280{
281 Layer& layer = *PolymorphicDowncast<Layer*>(layerInfo.m_Layer);
282
283 for (auto inputSlot : layer.GetInputSlots())
284 {
285 auto connectedInput = PolymorphicDowncast<OutputSlot*>(inputSlot.GetConnection());
286 if (!connectedInput)
287 {
288 throw armnn::Exception("Dangling input slot detected.");
289 }
290 Layer& inputLayer = connectedInput->GetOwningLayer();
291
292 auto parentInfo = layerInfos.find(&inputLayer);
293 if (parentInfo != layerInfos.end())
294 {
295 function(parentInfo->second);
296 }
297 }
298}
299
300template<typename Delegate>
301void ForEachLayerOutput(LayerSelectionInfo::LayerInfoContainer& layerInfos,
302 LayerSelectionInfo& layerInfo,
303 Delegate function)
304{
305 Layer& layer = *PolymorphicDowncast<Layer*>(layerInfo.m_Layer);
306
307 for (auto& outputSlot : layer.GetOutputSlots())
308 {
309 for (auto& output : outputSlot.GetConnections())
310 {
311 Layer& childLayer = output->GetOwningLayer();
312
313 auto childInfo = layerInfos.find(&childLayer);
314 if (childInfo != layerInfos.end())
315 {
316 function(childInfo->second);
317 }
318 }
319 }
320}
321
322void AssignSplitId(LayerSelectionInfo::LayerInfoContainer& layerInfos, LayerSelectionInfo& layerInfo)
323{
324 // Check each input to see if we can attach ourselves to any of the subgraphs that have already been assigned.
325 ForEachLayerInput(layerInfos, layerInfo, [&](LayerSelectionInfo& parentInfo)
326 {
327 // We can only attach ourselves to the subgraph from this input if there isn't a cut here.
328 if (layerInfo.m_IsSelected == parentInfo.m_IsSelected)
329 {
330 // We also need to check that merging into this subgraph won't cause a dependency cycle between subgraphs.
331 // This will be the case if the subgraph that we will become part of is already a dependency
332 // of one of the subgraphs that are input to this layer, e.g:
333 //
334 // 0 | The numbers (0, 1) are the subgraph IDs of each layer and we are looking at layer X.
335 // / \ |
336 // 1 0 | We can't merge X into subgraph 0, because the left-hand input already depends on subgraph 0.
337 // \ / | We can however merge X into subgraph 1.
338 // X |
339 //
340 bool dependenciesOk = true;
341 ForEachLayerInput(layerInfos, layerInfo, [&](LayerSelectionInfo& otherParentInfo)
342 {
343 // We call HasAntecedent() ~ n^2 times, where n is the number of inputs to this layer.
344 // Hence it is important that this is efficient - see PartialSubgraph class description.
345 if (otherParentInfo.m_Subgraph->HasAntecedent(parentInfo.m_Subgraph.get()))
346 {
347 dependenciesOk = false;
348 }
349 });
350
351 if (dependenciesOk)
352 {
353 // Merge into the subgraph of this input. If we have already been merged into another subgraph
354 // (from another input of this layer), then merge both of them together.
355 if (layerInfo.m_Subgraph == nullptr)
356 {
357 layerInfo.m_Subgraph = parentInfo.m_Subgraph;
358 }
359 else
360 {
361 // We call MergeWith() ~ n times, where n is the number of inputs to this layer.
362 // Therefore it does not need to be as performant as HasAntecedent().
363 layerInfo.m_Subgraph->MergeWith(parentInfo.m_Subgraph.get());
364 }
365 }
366 }
367 });
368
369 // If we weren't able to merge into an existing subgraph then we need to make a new one
370 if (layerInfo.m_Subgraph == nullptr)
371 {
372 layerInfo.m_Subgraph = std::make_shared<PartialSubgraph>();
373 }
374
375 // Record dependencies of the chosen subgraph based on the inputs of this layer.
376 ForEachLayerInput(layerInfos, layerInfo, [&](LayerSelectionInfo& parentInfo)
377 {
378 // These functions are called ~n times, where n is the number of inputs to this layer.
379 // Therefore it does not need to be as performant as HasAntecedent().
380 if (!layerInfo.m_Subgraph->IsMergedWith(parentInfo.m_Subgraph.get()))
381 {
382 layerInfo.m_Subgraph->AddDirectAntecedent(parentInfo.m_Subgraph.get());
383 }
384 });
385}
386
387bool IsReadyForSplitAssignment(LayerSelectionInfo::LayerInfoContainer& layerInfos, LayerSelectionInfo& layerInfo)
388{
389 bool ready = true;
390 ForEachLayerInput(layerInfos, layerInfo,
391 [&ready](LayerSelectionInfo& parentInfo)
392 {
393 if (!parentInfo.m_IsProcessed)
394 {
395 ready = false;
396 }
397 });
398 return ready;
399}
400
403{
404 LayerSelectionInfo::LayerInfoContainer layerInfos;
405
406 LayerSelectionInfo::LayerInfoQueue processQueue;
407 const SubgraphView::IConnectableLayers& subgraphLayers = subgraph.GetIConnectableLayers();
408 for (auto& layer : subgraphLayers)
409 {
410
411 auto emplaced = layerInfos.emplace(layer, LayerSelectionInfo{PolymorphicDowncast<Layer*>(layer), selector});
412 LayerSelectionInfo& layerInfo = emplaced.first->second;
413
414 // Start with Input type layers
415 if (layerInfo.IsInputLayer())
416 {
417 processQueue.push(&layerInfo);
418 }
419 }
420
421 const SubgraphView::IInputSlots& subgraphInputSlots = subgraph.GetIInputSlots();
422 for (auto& inputSlot : subgraphInputSlots)
423 {
424 Layer& layer = PolymorphicDowncast<InputSlot*>(inputSlot)->GetOwningLayer();
425 auto emplaced = layerInfos.emplace(&layer, LayerSelectionInfo{&layer, selector});
426 LayerSelectionInfo& layerInfo = emplaced.first->second;
427
428 processQueue.push(&layerInfo);
429 }
430
431 while (!processQueue.empty())
432 {
433 LayerSelectionInfo& layerInfo = *processQueue.front();
434 processQueue.pop(); // remove front from queue
435
436 // This layerInfo may have been added to the queue multiple times, so skip if we have already processed it
437 if (!layerInfo.m_IsProcessed)
438 {
439 // Only process this layerInfo if all inputs have been processed
440 if (!IsReadyForSplitAssignment(layerInfos, layerInfo))
441 {
442 // Put back of the process queue if we can't process it just yet
443 processQueue.push(&layerInfo);
444 continue; // Skip to next iteration
445 }
446
447 // Now we do the processing
448 AssignSplitId(layerInfos, layerInfo);
449
450 // Queue any child nodes for processing
451 ForEachLayerOutput(layerInfos, layerInfo, [&processQueue](LayerSelectionInfo& childInfo)
452 {
453 processQueue.push(&childInfo);
454 });
455
456 // We don't need to process this node again
457 layerInfo.m_IsProcessed = true;
458 }
459 }
460
461 // Collect all selected layers keyed by subgraph representative into a map
462 using SelectionInfoPtrs = std::vector<LayerSelectionInfo*>;
463 std::map<PartialSubgraph*, SelectionInfoPtrs> splitMap;
464 for (auto& info : layerInfos)
465 {
466 if (info.second.m_IsSelected)
467 {
468 auto it = splitMap.find(info.second.m_Subgraph->GetRepresentative());
469 if (it == splitMap.end())
470 {
471 splitMap.insert(
472 std::make_pair(info.second.m_Subgraph->GetRepresentative(), SelectionInfoPtrs{&info.second}));
473 }
474 else
475 {
476 it->second.push_back(&info.second);
477 }
478 }
479 }
480
481 // Now each entry in splitMap represents a subgraph
482 Subgraphs result;
483 for (auto& splitGraph : splitMap)
484 {
488 for (auto&& infoPtr : splitGraph.second)
489 {
490 infoPtr->CollectNonSelectedInputs(layerInfos, inputs);
491 infoPtr->CollectNonSelectedOutputSlots(layerInfos, outputs);
492 layers.push_back(infoPtr->m_Layer);
493 }
494
495 // Sort lists into deterministic order, not relying on pointer values which may be different on each execution.
496 // This makes debugging the optimised graph much easier as subsequent stages can also be deterministic.
497 std::sort(inputs.begin(), inputs.end(), [](const IInputSlot* a, const IInputSlot* b)
498 {
499 auto* castA = PolymorphicDowncast<const InputSlot*>(a);
500 auto* castB = PolymorphicDowncast<const InputSlot*>(b);
501 const LayerGuid guidA = castA->GetOwningLayer().GetGuid();
502 const LayerGuid guidB = castB->GetOwningLayer().GetGuid();
503 if (guidA < guidB)
504 {
505 return true;
506 }
507 else if (guidA == guidB)
508 {
509 return (castA->GetSlotIndex() < castB->GetSlotIndex());
510 }
511 return false;
512 });
513 std::sort(outputs.begin(), outputs.end(), [](const IOutputSlot* a, const IOutputSlot* b)
514 {
515 auto* castA = PolymorphicDowncast<const OutputSlot*>(a);
516 auto* castB = PolymorphicDowncast<const OutputSlot*>(b);
517 const LayerGuid guidA = castA->GetOwningLayer().GetGuid();
518 const LayerGuid guidB = castB->GetOwningLayer().GetGuid();
519 if (guidA < guidB)
520 {
521 return true;
522 }
523 else if (guidA == guidB)
524 {
525 return (a->CalculateIndexOnOwner() < b->CalculateIndexOnOwner());
526 }
527 return false;
528 });
529 layers.sort([](const IConnectableLayer* a, const IConnectableLayer* b) { return a->GetGuid() < b->GetGuid(); });
530
531 // Create a new sub-graph with the new lists of input/output slots and layer
532 result.emplace_back(std::make_unique<SubgraphView>(std::move(layers),
533 std::move(inputs),
534 std::move(outputs)));
535 }
536
537 // Sort subgraphs list into deterministic order, not relying on pointer values which may be different on each
538 // execution. This makes debugging the optimised graph much easier as subsequent stages can also be
539 // deterministic.
540 std::sort(result.begin(), result.end(), [](const SubgraphView::SubgraphViewPtr& a,
541 const SubgraphView::SubgraphViewPtr& b)
542 {
543 return a->GetIConnectableLayers().front()->GetGuid() < b->GetIConnectableLayers().front()->GetGuid();
544 });
545
546 return result;
547}
548
549} // namespace armnn
Base class for all ArmNN exceptions so that users can filter to just those.
Interface for a layer that is connectable to other layers via InputSlots and OutputSlots.
Definition INetwork.hpp:81
virtual LayerGuid GetGuid() const =0
Returns the unique id of the layer.
An input connection slot for a layer.
Definition INetwork.hpp:26
An output connection slot for a layer.
Definition INetwork.hpp:54
virtual unsigned int CalculateIndexOnOwner() const =0
const std::vector< OutputSlot > & GetOutputSlots() const
Definition Layer.hpp:259
const std::vector< InputSlot > & GetInputSlots() const
Definition Layer.hpp:258
The SubgraphView class represents a subgraph of a Graph.
std::vector< IOutputSlot * > IOutputSlots
std::vector< IInputSlot * > IInputSlots
const IConnectableLayers & GetIConnectableLayers() const
std::list< IConnectableLayer * > IConnectableLayers
const IInputSlots & GetIInputSlots() const
std::function< bool(const Layer &)> LayerSelectorFunction
static Subgraphs SelectSubgraphs(Graph &graph, const LayerSelectorFunction &selector)
Selects subgraphs from a graph based on the selector function and the algorithm.
std::vector< SubgraphView::SubgraphViewPtr > Subgraphs
Copyright (c) 2021 ARM Limited and Contributors.
void AssignSplitId(LayerSelectionInfo::LayerInfoContainer &layerInfos, LayerSelectionInfo &layerInfo)
bool IsReadyForSplitAssignment(LayerSelectionInfo::LayerInfoContainer &layerInfos, LayerSelectionInfo &layerInfo)
DestType PolymorphicDowncast(SourceType *value)
Polymorphic downcast for build in pointers only.
void ForEachLayerInput(LayerSelectionInfo::LayerInfoContainer &layerInfos, LayerSelectionInfo &layerInfo, Delegate function)
void ForEachLayerOutput(LayerSelectionInfo::LayerInfoContainer &layerInfos, LayerSelectionInfo &layerInfo, Delegate function)
void IgnoreUnused(Ts &&...)