[core] Have an ordered inputs mechanism for graph
Today, a graph defines its inputs/outputs but with no particular order. Returned nodes are stored in a std::set. From a topological point of view, it is not always possible to define an order a priori.
However, graph manipulation will often require for the user to specify an order. For example, to map a sub-graph to another one for replacement. Or to define a meta-operator. A possible solution is to add a mechanism to specify an order for the inputs/outputs of a GraphView.
Preserve the (construction) order(?)
In fact, if we want to be possible for the user to define the order of inputs/outputs of the GraphView, I believe it would make sense to preserve that order when manipulating the graph without altering its inputs/outputs. But in this case, it may be expected by the user that trivial changes affecting inputs/outputs also preserve the order, like just adding a node at one input or output (that would just update the corresponding input or output node in the GraphView inputs/outputs list without reshuffling it).
Therefore, I think that an ordering mechanism should also preserve the defined order whenever possible. In order to do so, the only way is to have a mechanism preserving order when adding or removing nodes to the GraphView, which would imply to have in fact a default construction order!
I know this is difficult, but I may have a first implementation proposal ready soon. It may very well have important loopholes, but lets see... see !53 (merged)
Example:
%%{init: {'flowchart': { 'curve': 'monotoneY'}, 'fontFamily': 'Verdana' } }%%
flowchart TB
Fictive_1(Fictive0)
Fictive_2(Fictive6)
Fictive_3(Fictive8)
Fictive_4(Fictive9)
Fictive_5(Fictive4)
Fictive_6(Fictive3)
Fictive_7(Fictive1)
Fictive_8(Fictive7)
Fictive_9(Fictive2)
Fictive_10(Fictive5)
Fictive_1-->|0..0|Fictive_7
Fictive_1-->|0..1|Fictive_6
Fictive_1-->|1..0|Fictive_7
Fictive_1-->|1..2|Fictive_9
Fictive_2-->|0..0|Fictive_8
Fictive_3-->|0..0|Fictive_4
Fictive_3-->|1..0|Fictive_4
Fictive_5-->|0..1|Fictive_10
Fictive_5-->|0..2|Fictive_2
Fictive_6-->|0..1|Fictive_5
Fictive_6-->|0..0|Fictive_10
Fictive_6-->|0..1|Fictive_3
Fictive_7-->|0..0|Fictive_9
Fictive_7-->|0..0|Fictive_5
Fictive_8-->|0..0|Fictive_3
Fictive_9-->|0..1|Fictive_2
Fictive_9-->|1..0|Fictive_6
Fictive_10-->|0..0|Fictive_2
input0((in#0)):::inputCls-->|..0|Fictive_1
input1((in#1)):::inputCls-->|..1|Fictive_1
input2((in#2)):::inputCls-->|..2|Fictive_7
input3((in#3)):::inputCls-->|..3|Fictive_7
input4((in#4)):::inputCls-->|..1|Fictive_9
input5((in#5)):::inputCls-->|..2|Fictive_5
input6((in#6)):::inputCls-->|..2|Fictive_4
Fictive_1-->|1..|output0((out#0)):::outputCls
Fictive_4-->|0..|output1((out#1)):::outputCls
Fictive_4-->|1..|output2((out#2)):::outputCls
Fictive_3-->|1..|output3((out#3)):::outputCls
classDef inputCls fill:#afa
classDef outputCls fill:#ffa
Implementation proposal
Store a std::vector in GraphView:
std::vector<std::pair<NodePtr, IOIndex_t>> mOrderedInputs;
std::vector<std::pair<NodePtr, IOIndex_t>> mOrderedOutputs;
Example:
- Argument order = graph input order
- If an argument if a list => a GraphView input that goes to multiple nodes (@cmoineau as required for the Swift operator)
gv->setOrderedInputs({node1, 0}, {{node2, 0}, {node3, 1}});
Or, if nodes have name (failure if name is not unique):
gv->setOrderedInputs({"node1 name", "input name"}, {{node2, "input name"}, {"node3 name", 1}});
Or, if an unique ID could be defined for nodes (see issue #52 (moved))
gv->setOrderedInputs({1, 0}, {{2, 0}, {3, 1}});
Usage :
// Replace complexNode with gv, mapping input complexNode:0 to node1:0 and complexNode:1 to node2:0 AND node3:1
g->replace(complexNode, gv);
This example would work also for MetaOperator, since it stores internally a GraphView. This would eliminate the need to handle inputs ordering in the MetaOperator, as currently done.
Another example where there is no way of infering the order: the replacement of Mul+Add with FC. Mul inputs are commutative, but FC inputs are not, as its implementation may threat differently the input and the weights (as well as recipies acting on FC, like batch norm fuse).
When trying to replace Mul+Add with FC, the user has to specify, at some point, which of the Mul input is the weights. This could be done by encapsulating the two operators in a GraphView, and specify an order with an argument of the recipy, that could have a default value.