C++ grafik oluşturucu aşağıdakiler için güçlü bir araçtır:
- Karmaşık grafikler oluşturma
- Grafikleri parametreleştirme (ör.
InferenceCalculator
üzerinde bir temsilci belirleme, grafiğin bölümlerini etkinleştirme/devre dışı bırakma) - Grafikleri tekilleştirme (ör. pbtxt'de CPU ve GPU'ya ayrılmış grafikler yerine, gerekli grafikleri oluşturan ve mümkün olduğunca çok paylaşan tek bir kod kullanabilirsiniz)
- İsteğe bağlı grafik girişlerini/çıkışlarını destekleme
- Grafikleri platforma göre özelleştirme
Temel Kullanım
C++ grafik oluşturucunun basit bir grafik için nasıl kullanılabileceğini görelim:
# Graph inputs.
input_stream: "input_tensors"
input_side_packet: "model"
# Graph outputs.
output_stream: "output_tensors"
node {
calculator: "InferenceCalculator"
input_stream: "TENSORS:input_tensors"
input_side_packet: "MODEL:model"
output_stream: "TENSORS:output_tensors"
options: {
[drishti.InferenceCalculatorOptions.ext] {
# Requesting GPU delegate.
delegate { gpu {} }
}
}
}
Yukarıdaki CalculatorGraphConfig
öğesini derleme işlevi şöyle görünebilir:
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Graph inputs.
Stream<std::vector<Tensor>> input_tensors =
graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
SidePacket<TfLiteModelPtr> model =
graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();
auto& inference_node = graph.AddNode("InferenceCalculator");
auto& inference_opts =
inference_node.GetOptions<InferenceCalculatorOptions>();
// Requesting GPU delegate.
inference_opts.mutable_delegate()->mutable_gpu();
input_tensors.ConnectTo(inference_node.In("TENSORS"));
model.ConnectTo(inference_node.SideIn("MODEL"));
Stream<std::vector<Tensor>> output_tensors =
inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();
// Graph outputs.
output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));
// Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
return graph.GetConfig();
}
Kısa özet:
- Grafik girişlerini
Stream/SidePacket
olarak almak içinGraph::In/SideIn
kullanın - Düğüm çıkışlarını
Stream/SidePacket
olarak almak içinNode::Out/SideOut
kullanın - Akışları ve yan paketleri düğüm girişlerine (
Node::In/SideIn
) ve grafik çıkışlarına (Graph::Out/SideOut
) bağlamak içinStream/SidePacket::ConnectTo
hizmetini kullanınConnectTo
işlevinin yerine kullanabileceğiniz bir "kısayol" operatörü>>
vardır (ör.x >> node.In("IN")
).
Stream/SidePacket::Cast
,AnyType
öğesinin (ör.Stream<AnyType> in = graph.In(0);
) akışını veya yan paketini belirli bir türe yayınlamak için kullanılırAnyType
yerine gerçek türleri kullandığınızda, grafik oluşturucu özelliklerinden yararlanmak ve grafiklerinizin okunabilirliğini iyileştirmek için daha iyi bir yönteme sahip olursunuz.
Gelişmiş Kullanım
Yardımcı İşlevler
Okunabilirlik ve kodun yeniden kullanımı için çıkarım oluşturma kodunu özel bir yardımcı program işlevine çıkaralım:
// Updates graph to run inference.
Stream<std::vector<Tensor>> RunInference(
Stream<std::vector<Tensor>> tensors, SidePacket<TfLiteModelPtr> model,
const InferenceCalculatorOptions::Delegate& delegate, Graph& graph) {
auto& inference_node = graph.AddNode("InferenceCalculator");
auto& inference_opts =
inference_node.GetOptions<InferenceCalculatorOptions>();
*inference_opts.mutable_delegate() = delegate;
tensors.ConnectTo(inference_node.In("TENSORS"));
model.ConnectTo(inference_node.SideIn("MODEL"));
return inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Graph inputs.
Stream<std::vector<Tensor>> input_tensors =
graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
SidePacket<TfLiteModelPtr> model =
graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();
InferenceCalculatorOptions::Delegate delegate;
delegate.mutable_gpu();
Stream<std::vector<Tensor>> output_tensors =
RunInference(input_tensors, model, delegate, graph);
// Graph outputs.
output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));
return graph.GetConfig();
}
Sonuç olarak RunInference
, giriş/çıkışları ve türlerini belirten net bir arayüz sağlar.
Kolayca yeniden kullanılabilir. Örneğin, fazladan bir model çıkarımı çalıştırmak isterseniz yalnızca birkaç satırdan ibarettir:
// Run first inference.
Stream<std::vector<Tensor>> output_tensors =
RunInference(input_tensors, model, delegate, graph);
// Run second inference on the output of the first one.
Stream<std::vector<Tensor>> extra_output_tensors =
RunInference(output_tensors, extra_model, delegate, graph);
Ayrıca adları ve etiketleri (InferenceCalculator
, TENSORS
, MODEL
) kopyalamanız veya özel sabit değerler eklemeniz gerekmez. Bu ayrıntılar RunInference
işlevi için yerelleştirilir.
Yardımcı Sınıflar
Ayrıca, kesinlikle fonksiyonlarla ilgili değildir. Bazı durumlarda, grafik oluşturma kodunuzu daha okunabilir ve hataya daha az açık hale getirmenize yardımcı olabilecek yardımcı program sınıflarını kullanmak da yararlıdır.
MediaPipe, girdilerini geçiren PassThroughCalculator
hesaplayıcı sunar:
input_stream: "float_value"
input_stream: "int_value"
input_stream: "bool_value"
output_stream: "passed_float_value"
output_stream: "passed_int_value"
output_stream: "passed_bool_value"
node {
calculator: "PassThroughCalculator"
input_stream: "float_value"
input_stream: "int_value"
input_stream: "bool_value"
# The order must be the same as for inputs (or you can use explicit indexes)
output_stream: "passed_float_value"
output_stream: "passed_int_value"
output_stream: "passed_bool_value"
}
Yukarıdaki grafiği oluşturmak için basit C++ yapı kodunu inceleyelim:
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Graph inputs.
Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();
auto& pass_node = graph.AddNode("PassThroughCalculator");
float_value.ConnectTo(pass_node.In("")[0]);
int_value.ConnectTo(pass_node.In("")[1]);
bool_value.ConnectTo(pass_node.In("")[2]);
Stream<float> passed_float_value = pass_node.Out("")[0].Cast<float>();
Stream<int> passed_int_value = pass_node.Out("")[1].Cast<int>();
Stream<bool> passed_bool_value = pass_node.Out("")[2].Cast<bool>();
// Graph outputs.
passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));
// Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
return graph.GetConfig();
}
pbtxt
gösterimi hataya açık olabilir (geçecek çok sayıda giriş olduğunda), C++ kodu daha da kötü görünür: tekrar eden boş etiketler ve Cast
çağrıları. Şimdi, PassThroughNodeBuilder
özelliğini kullanıma sunarak nasıl daha iyi hale getirebileceğimize göz atalım:
class PassThroughNodeBuilder {
public:
explicit PassThroughNodeBuilder(Graph& graph)
: node_(graph.AddNode("PassThroughCalculator")) {}
template <typename T>
Stream<T> PassThrough(Stream<T> stream) {
stream.ConnectTo(node_.In(index_));
return node_.Out(index_++).Cast<T>();
}
private:
int index_ = 0;
GenericNode& node_;
};
Şimdi de grafik oluşturma kodu şöyle görünebilir:
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Graph inputs.
Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();
PassThroughNodeBuilder pass_node_builder(graph);
Stream<float> passed_float_value = pass_node_builder.PassThrough(float_value);
Stream<int> passed_int_value = pass_node_builder.PassThrough(int_value);
Stream<bool> passed_bool_value = pass_node_builder.PassThrough(bool_value);
// Graph outputs.
passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));
// Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
return graph.GetConfig();
}
Artık yapı kodunda yanlış sıralama veya dizine ekleme yapamaz ve PassThrough
girişinden Cast
türünü tahmin ederek bazı yazmalardan kurtulamazsınız.
Yapılması ve Yapılmaması Gerekenler
Mümkünse grafik girişlerini en başta tanımlayın
Aşağıdaki kodda:
- Grafikte kaç girdiniz olduğunu tahmin etmek zor olabilir.
- Genel olarak hataya açık olabilir ve ileride bakımı zor olabilir (ör. doğru bir dizin mi? adı mı? Bazı girişler kaldırılırsa veya isteğe bağlı yapılırsa ne olur?)
- Diğer grafikler farklı girdilere sahip olabileceğinden
RunSomething
yeniden kullanımı sınırlıdır
YAPILMAYACAKLAR. Bozuk kod örneği.
Stream<D> RunSomething(Stream<A> a, Stream<B> b, Graph& graph) {
Stream<C> c = graph.In(2).SetName("c").Cast<C>(); // Bad.
// ...
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
Stream<A> a = graph.In(0).SetName("a").Cast<A>();
// 10/100/N lines of code.
Stream<B> b = graph.In(1).SetName("b").Cast<B>() // Bad.
Stream<D> d = RunSomething(a, b, graph);
// ...
return graph.GetConfig();
}
Bunun yerine, grafik girişlerinizi grafik oluşturucunuzun en başında tanımlayın:
YAPILACAKLAR — iyi kod örneği.
Stream<D> RunSomething(Stream<A> a, Stream<B> b, Stream<C> c, Graph& graph) {
// ...
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).SetName("a").Cast<A>();
Stream<B> b = graph.In(1).SetName("b").Cast<B>();
Stream<C> c = graph.In(2).SetName("c").Cast<C>();
// 10/100/N lines of code.
Stream<D> d = RunSomething(a, b, c, graph);
// ...
return graph.GetConfig();
}
Her zaman tanımlı olmayan bir giriş akışınız veya yan paketiniz varsa std::optional
öğesini kullanın ve en başa yerleştirin:
YAPILACAKLAR — iyi kod örneği.
std::optional<Stream<A>> a;
if (needs_a) {
a = graph.In(0).SetName(a).Cast<A>();
}
Grafik çıktılarını en sonda tanımlayın
Aşağıdaki kodda:
- Grafikte kaç çıkışınız olduğunu tahmin etmek zor olabilir.
- Genel olarak hataya açık olabilir ve ileride bakımı zor olabilir (ör. doğru bir dizin mi? adı mı? Bazı çıkışlar kaldırılırsa veya isteğe bağlı hale getirilirse ne olur?)
- Diğer grafiklerin farklı çıktıları olabileceğinden
RunSomething
yeniden kullanımı sınırlıdır
YAPILMAYACAKLAR. Bozuk kod örneği.
void RunSomething(Stream<Input> input, Graph& graph) {
// ...
node.Out("OUTPUT_F")
.SetName("output_f").ConnectTo(graph.Out(2)); // Bad.
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// 10/100/N lines of code.
node.Out("OUTPUT_D")
.SetName("output_d").ConnectTo(graph.Out(0)); // Bad.
// 10/100/N lines of code.
node.Out("OUTPUT_E")
.SetName("output_e").ConnectTo(graph.Out(1)); // Bad.
// 10/100/N lines of code.
RunSomething(input, graph);
// ...
return graph.GetConfig();
}
Bunun yerine, grafik çıkışlarınızı, grafik oluşturucunuzun en sonunda tanımlayın:
YAPILACAKLAR — iyi kod örneği.
Stream<F> RunSomething(Stream<Input> input, Graph& graph) {
// ...
return node.Out("OUTPUT_F").Cast<F>();
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// 10/100/N lines of code.
Stream<D> d = node.Out("OUTPUT_D").Cast<D>();
// 10/100/N lines of code.
Stream<E> e = node.Out("OUTPUT_E").Cast<E>();
// 10/100/N lines of code.
Stream<F> f = RunSomething(input, graph);
// ...
// Outputs.
d.SetName("output_d").ConnectTo(graph.Out(0));
e.SetName("output_e").ConnectTo(graph.Out(1));
f.SetName("output_f").ConnectTo(graph.Out(2));
return graph.GetConfig();
}
Düğümleri birbirinden ayrılmış halde tutun
MediaPipe'te paket akışları ve yan paketler, işleme düğümleri kadar anlamlıdır. Ayrıca tüm düğüm giriş gereksinimleri ve çıkış ürünleri, tükettiği ve ürettiği akışlar ve yan paketler açısından açık ve bağımsız bir şekilde ifade edilir.
YAPILMAYACAKLAR. Bozuk kod örneği.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
auto& node1 = graph.AddNode("Calculator1");
a.ConnectTo(node1.In("INPUT"));
auto& node2 = graph.AddNode("Calculator2");
node1.Out("OUTPUT").ConnectTo(node2.In("INPUT")); // Bad.
auto& node3 = graph.AddNode("Calculator3");
node1.Out("OUTPUT").ConnectTo(node3.In("INPUT_B")); // Bad.
node2.Out("OUTPUT").ConnectTo(node3.In("INPUT_C")); // Bad.
auto& node4 = graph.AddNode("Calculator4");
node1.Out("OUTPUT").ConnectTo(node4.In("INPUT_B")); // Bad.
node2.Out("OUTPUT").ConnectTo(node4.In("INPUT_C")); // Bad.
node3.Out("OUTPUT").ConnectTo(node4.In("INPUT_D")); // Bad.
// Outputs.
node1.Out("OUTPUT").SetName("b").ConnectTo(graph.Out(0)); // Bad.
node2.Out("OUTPUT").SetName("c").ConnectTo(graph.Out(1)); // Bad.
node3.Out("OUTPUT").SetName("d").ConnectTo(graph.Out(2)); // Bad.
node4.Out("OUTPUT").SetName("e").ConnectTo(graph.Out(3)); // Bad.
return graph.GetConfig();
}
Yukarıdaki kodda:
- Düğümler birbirine bağlıdır.Örneğin
node4
, girişlerinin nereden geldiğini (node1
,node2
,node3
) bilir ve yeniden düzenleme, bakım ve kodun yeniden kullanılmasını karmaşık hale getirir.- Bu tür kullanım kalıbı, varsayılan olarak düğümlerin ayrıldığı proto gösterimden düşürülür.
node#.Out("OUTPUT")
çağrıları yineleniyor ve bunun yerine daha açık adlar kullanabileceğiniz ve gerçek bir tür sunabileceğiniz için okunabilirliği olumsuz etkiler.
Dolayısıyla, yukarıdaki sorunları gidermek için aşağıdaki grafik oluşturma kodunu yazabilirsiniz:
YAPILACAKLAR — iyi kod örneği.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
// `node1` usage is limited to 3 lines below.
auto& node1 = graph.AddNode("Calculator1");
a.ConnectTo(node1.In("INPUT"));
Stream<B> b = node1.Out("OUTPUT").Cast<B>();
// `node2` usage is limited to 3 lines below.
auto& node2 = graph.AddNode("Calculator2");
b.ConnectTo(node2.In("INPUT"));
Stream<C> c = node2.Out("OUTPUT").Cast<C>();
// `node3` usage is limited to 4 lines below.
auto& node3 = graph.AddNode("Calculator3");
b.ConnectTo(node3.In("INPUT_B"));
c.ConnectTo(node3.In("INPUT_C"));
Stream<D> d = node3.Out("OUTPUT").Cast<D>();
// `node4` usage is limited to 5 lines below.
auto& node4 = graph.AddNode("Calculator4");
b.ConnectTo(node4.In("INPUT_B"));
c.ConnectTo(node4.In("INPUT_C"));
d.ConnectTo(node4.In("INPUT_D"));
Stream<E> e = node4.Out("OUTPUT").Cast<E>();
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}
Artık gerekirse node1
öğesini kolayca kaldırıp b
öğesini bir grafik girişi haline getirebilirsiniz. node2
, node3
ve node4
öğeleri birbirinden ayrıldığından bunlar için herhangi bir güncelleme gerekmez (proto gösterimdeki gibi).
Genel olarak yukarıdaki kod, proto grafiğini daha yakından çoğaltır:
input_stream: "a"
node {
calculator: "Calculator1"
input_stream: "INPUT:a"
output_stream: "OUTPUT:b"
}
node {
calculator: "Calculator2"
input_stream: "INPUT:b"
output_stream: "OUTPUT:C"
}
node {
calculator: "Calculator3"
input_stream: "INPUT_B:b"
input_stream: "INPUT_C:c"
output_stream: "OUTPUT:d"
}
node {
calculator: "Calculator4"
input_stream: "INPUT_B:b"
input_stream: "INPUT_C:c"
input_stream: "INPUT_D:d"
output_stream: "OUTPUT:e"
}
output_stream: "b"
output_stream: "c"
output_stream: "d"
output_stream: "e"
Üstelik, artık diğer grafiklerde tekrar kullanmak üzere yardımcı program fonksiyonlarını çıkartabilirsiniz:
YAPILACAKLAR — iyi kod örneği.
Stream<B> RunCalculator1(Stream<A> a, Graph& graph) {
auto& node = graph.AddNode("Calculator1");
a.ConnectTo(node.In("INPUT"));
return node.Out("OUTPUT").Cast<B>();
}
Stream<C> RunCalculator2(Stream<B> b, Graph& graph) {
auto& node = graph.AddNode("Calculator2");
b.ConnectTo(node.In("INPUT"));
return node.Out("OUTPUT").Cast<C>();
}
Stream<D> RunCalculator3(Stream<B> b, Stream<C> c, Graph& graph) {
auto& node = graph.AddNode("Calculator3");
b.ConnectTo(node.In("INPUT_B"));
c.ConnectTo(node.In("INPUT_C"));
return node.Out("OUTPUT").Cast<D>();
}
Stream<E> RunCalculator4(Stream<B> b, Stream<C> c, Stream<D> d, Graph& graph) {
auto& node = graph.AddNode("Calculator4");
b.ConnectTo(node.In("INPUT_B"));
c.ConnectTo(node.In("INPUT_C"));
d.ConnectTo(node.In("INPUT_D"));
return node.Out("OUTPUT").Cast<E>();
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
Stream<B> b = RunCalculator1(a, graph);
Stream<C> c = RunCalculator2(b, graph);
Stream<D> d = RunCalculator3(b, c, graph);
Stream<E> e = RunCalculator4(b, c, d, graph);
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}
Daha iyi okunabilirlik için ayrı düğümleri
YAPILMAYACAKLAR. Bozuk kod örneği.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
auto& node1 = graph.AddNode("Calculator1");
a.ConnectTo(node1.In("INPUT"));
Stream<B> b = node1.Out("OUTPUT").Cast<B>();
auto& node2 = graph.AddNode("Calculator2");
b.ConnectTo(node2.In("INPUT"));
Stream<C> c = node2.Out("OUTPUT").Cast<C>();
auto& node3 = graph.AddNode("Calculator3");
b.ConnectTo(node3.In("INPUT_B"));
c.ConnectTo(node3.In("INPUT_C"));
Stream<D> d = node3.Out("OUTPUT").Cast<D>();
auto& node4 = graph.AddNode("Calculator4");
b.ConnectTo(node4.In("INPUT_B"));
c.ConnectTo(node4.In("INPUT_C"));
d.ConnectTo(node4.In("INPUT_D"));
Stream<E> e = node4.Out("OUTPUT").Cast<E>();
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}
Yukarıdaki kodda, her bir düğümün nerede başlayıp nerede bittiği fikrini anlamak zor olabilir. Bunu iyileştirmek ve kod okuyucularınıza yardımcı olmak için her düğümün öncesinde ve sonrasında boş satırlar kullanabilirsiniz:
YAPILACAKLAR — iyi kod örneği.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
auto& node1 = graph.AddNode("Calculator1");
a.ConnectTo(node1.In("INPUT"));
Stream<B> b = node1.Out("OUTPUT").Cast<B>();
auto& node2 = graph.AddNode("Calculator2");
b.ConnectTo(node2.In("INPUT"));
Stream<C> c = node2.Out("OUTPUT").Cast<C>();
auto& node3 = graph.AddNode("Calculator3");
b.ConnectTo(node3.In("INPUT_B"));
c.ConnectTo(node3.In("INPUT_C"));
Stream<D> d = node3.Out("OUTPUT").Cast<D>();
auto& node4 = graph.AddNode("Calculator4");
b.ConnectTo(node4.In("INPUT_B"));
c.ConnectTo(node4.In("INPUT_C"));
d.ConnectTo(node4.In("INPUT_D"));
Stream<E> e = node4.Out("OUTPUT").Cast<E>();
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}
Ayrıca, yukarıdaki temsil CalculatorGraphConfig
proto gösterimiyle daha iyi eşleşir.
Düğümleri yardımcı program fonksiyonlarına çıkarırsanız bunlar halihazırda işlevlerin kapsamına girer ve nerede başlayıp nerede bittiği bellidir. Bu nedenle tercihte sakınca yoktur:
YAPILACAKLAR — iyi kod örneği.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
Stream<B> b = RunCalculator1(a, graph);
Stream<C> c = RunCalculator2(b, graph);
Stream<D> d = RunCalculator3(b, c, graph);
Stream<E> e = RunCalculator4(b, c, d, graph);
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}