Graph Model
Overview
Holoflow represents computational pipelines as graphs. This abstraction makes it possible to describe workflows in a clear, declarative way, while giving the runtime the flexibility to manage execution and resources efficiently.
Graph Structure
Formally, a graph is \(G = (V, E)\) where:
- \(V\) is the set of vertices (nodes). In Holoflow, nodes are computational tasks (operations).
- \(E\) is the set of edges. In Holoflow, edges describe data flow from one node’s output to another node’s input.
Holoflow uses two kinds of graphs:
- Graph Specification: a high-level declarative structure describing the pipeline: which tasks exist, how they are connected, and what parameters they use.
- Executable Graph: a low-level structure produced by the Holoflow compiler. This is the validated, resource-aware graph that the scheduler executes.
Restrictions
Currently, Holoflow graphs are tree-structured:
- There is a single root node.
- Each node has exactly one parent but may produce multiple outputs.
- Each output may connect to one or more children.
- Cycles are not allowed.
This restriction simplifies scheduling and resource management.
Graph Specification
A node contains three key fields:
- name: a unique identifier for the node in the graph.
- kind: the operation type (e.g., FFT, Display, File Reader).
- settings: a JSON object describing parameters specific to that operation.
An edge connects one output of a parent node to one input of a child node. Edges are labeled by (parent_output_index, child_input_index).
Examples
Example 1: Linear pipeline (full node details)
A simple chain of tasks.
flowchart LR
A["
name: camera
kind: PhantomS710
settings: {
"fps": 100,
"exposure": 10
}
"]
B["
name: fft
kind: FFT1D
settings: {
"window_size": 1024,
"window_stride": 512
}
"]
C["
name: display
kind: QtDisplay
settings: {
"fps": 30
}
"]
A -->|"0|0"| B
B -->|"0|0"| C
Here, the camera produces a stream (output 0) consumed by the FFT, which in turn produces an output displayed by the GUI.
Example 2: Branching output
One output feeding multiple children.
flowchart LR
A["camera"]
B["fft"]
C["saver"]
D["display"]
A -->|"0|0"| B
B -->|"0|0"| C
B -->|"0|0"| D
The FFT node produces a single result stream that is written to disk (saver) and simultaneously displayed.
Example 3: Multiple outputs
A node producing different outputs.
flowchart LR
A["splitter"]
B["fft branch"]
C["filter branch"]
A -->|"0|0"| B
A -->|"1|0"| C
The splitter produces two separate streams: one is sent to an FFT, the other to a filter. Each output index has its own consumers.
Example 4: Mixed case
Combining multiple outputs and multiple children.
flowchart LR
A["camera"]
B["splitter"]
C["fft"]
D["display"]
E["saver"]
A -->|"0|0"| B
B -->|"0|0"| C
B -->|"1|0"| E
C -->|"0|0"| D
The camera feeds a splitter. The splitter has two outputs: one goes to an FFT (then displayed), the other is saved to disk.
Code Example
Here is a simple graph specification in C++ using the Holoflow API:
Graph Compilation & Execution
Factories
The link between the graph specification and the actual operations is made through factories. A factory is a class that can create instances of a specific task. A registry of factories is an associative map from task kind (string) to factory instance. The user is responsible for populating this registry with all the tasks they want to use in their graphs.
Code Example
Here is a simple registry in C++ using the Holoflow API:
Note
The "sync" in register_sync indicates that these tasks are synchronous. More information about task types can be found in the Tasks documentation.
Compilation
The graph specification, as the name suggests, is only a specification. It holds no behaviour and is declarative only. The Holoflow compiler translates this high-level description into a combination of:
- Execution resources: instanciated tasks, allocated memory, etc. Any data used while running the model.
- Graph Planification: lower-level representation of the graph, linked to resources, ready for execution. Contains metadata about the nature of each node.
- Sections: subgraphs that can be executed concurrently. Contains the topological ordering of nodes and allows pipeline parallelism.
Compiling a graph specification may take a noticeable amount of time, largely due to resource allocation (e.g., GPU memory) and tasks initialization (e.g., opening camera connections, preparing FFT plans). This is a one-time cost paid before execution. It can be amortized on updates by reusing the same compiled graph multiple times. Doing so triggers an update instead of a full recompile, which is much faster as it skips nodes that have not changed, and may reuse resources for some tasks.
The compilation process also performs validation, ensuring that the graph is well-formed and that all tasks can be executed with the given settings. Each task is responsible for validating its own settings and exposes metadata about its outputs given those settings and inputs. This is the signature of a task. The compiler uses these signatures to ensure that connected nodes are compatible. Therefore, a graph is guaranteed to be semantically valid once compiled.
Code Example
Here is a simple compilation in C++ using the Holoflow API:
Execution
The holoflow scheduler is started with those three components and executes the graph accordingly. It is also responsible for runtime metrics collection. Its API remains very simple, with only three main methods:
start(): starts execution of the graph. (non-blocking)stop(): stops execution of the graph. (non-blocking)wait(): blocks until execution is fully stopped.
Code Example
Here is a simple execution in C++ using the Holoflow API:
Warning
The GraphPlan, Sections and Resources returned by the compiler must be kept alive while the scheduler is running. Therefore the Scheduler should be recreated if the graph is updated.