For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
  • Introduction
    • Overview
    • Relevant Technologies
    • Getting Started
  • Setup
    • SDK Installation
    • Additional Setup
    • Third Party Hardware Setup
  • Using the SDK
    • Holoscan Core
    • GPU Resident Execution
    • Holoscan by Example
      • Hello World
      • Ping Simple
      • Ping Custom Op
      • Ping Multi Port
      • Video Replayer
      • Video Replayer Distributed
      • BYOM
      • Custom CUDA Kernel Sample
    • Create an Application
    • Create a Distributed Application
    • Create an Operator
    • Create an Operator via Decorator
    • Create a Condition
    • Dynamic Flow Control
    • CUDA Stream Handling
    • Logging
    • Data Logging
    • Debugging
    • Python Operator Bindings
  • Operators
    • Operators and Extensions
    • Visualization
    • Inference
    • Testing
    • Video I/O Vendor Implementation Guide
  • Components
    • Schedulers
    • Conditions
    • Resources
    • Analytics
  • AI Skills
    • Ai Skills
  • API reference
  • Performance
    • Performance Considerations
    • Flow Tracking
    • GXF Job Statistics
    • Nsight Profiling
  • HoloHub
    • HoloHub Overview
  • FAQ
    • FAQ
NVIDIANVIDIA
Developer-friendly docs for your API
Privacy Policy | Your Privacy Choices | Terms of Service | Accessibility | Corporate Policies | Product Security | Contact

Copyright © 2026, NVIDIA Corporation.

LogoLogoDocumentation
On this page
  • Operators and Workflow
  • User Defined Data Types
  • Defining an Explicit Number of Inputs and Outputs
  • Receiving Any Number of Inputs
  • Running the Application
Using the SDKHoloscan by Example

Ping Multi Port

||View as Markdown|
Previous

Ping Custom Op

Next

Video Replayer

In this section, we look at how to create an application with a more complex workflow where operators may have multiple input/output ports that send/receive a user-defined data type.

In this example we will cover:

  • How to send/receive messages with a custom data type.
  • How to add a port that can receive any number of inputs.

The example source code and run instructions can be found in the examples directory on GitHub, or under /opt/nvidia/holoscan/examples in the NGC container and the Debian package, alongside their executables.

Operators and Workflow

Here is the diagram of the operators and workflow used in this example.

A workflow with multiple inputs and outputs

In this example, PingTxOp sends a stream of odd integers to the out1 port, and even integers to the out2 port. PingMxOp receives these values using in1 and in2 ports, multiplies them by a constant factor, then forwards them to a single port - receivers - on PingRxOp.

User Defined Data Types

In the previous ping examples, the port types for our operators were integers, but the Holoscan SDK can send any arbitrary data type. In this example, we’ll see how to configure operators for our user-defined ValueData class.

C++
Python
1 #include <holoscan/holoscan.hpp>
2  
3 class ValueData {
4 public:
5 ValueData() = default;
6 explicit ValueData(int value) : data_(value) {
7 HOLOSCAN_LOG_TRACE("ValueData::ValueData(): {}", data_);
8 }
9 ~ValueData() { HOLOSCAN_LOG_TRACE("ValueData::~ValueData(): {}", data_); }
10  
11 void data(int value) { data_ = value; }
12  
13 int data() const { return data_; }
14  
15 private:
16 int data_;
17 };

The ValueData class wraps a simple integer (line 6, 16), but could have been arbitrarily complex.

The HOLOSCAN_LOG_<LEVEL>() macros can be used for logging with fmtlib syntax (lines 7, 9 above) as demonstrated across this example. See the Logging section for more details.

Defining an Explicit Number of Inputs and Outputs

After defining our custom ValueData class, we configure our operators’ ports to send/receive messages of this type, similarly to the previous example.

This is the first operator - PingTxOp - sending ValueData objects on two ports, out1 and out2:

C++
Python
1 namespace holoscan::ops {
2  
3 class PingTxOp : public Operator {
4 public:
5 HOLOSCAN_OPERATOR_FORWARD_ARGS(PingTxOp)
6  
7 PingTxOp() = default;
8  
9 void setup(OperatorSpec& spec) override {
10 spec.output<std::shared_ptr<ValueData>>("out1");
11 spec.output<std::shared_ptr<ValueData>>("out2");
12 }
13  
14 void compute(InputContext&, OutputContext& op_output, ExecutionContext&) override {
15 auto value1 = std::make_shared<ValueData>(index_++);
16 op_output.emit(value1, "out1");
17  
18 auto value2 = std::make_shared<ValueData>(index_++);
19 op_output.emit(value2, "out2");
20 };
21 int index_ = 1;
22 };
  • We configure the output ports with the ValueData type on lines 10 and 11 using spec.output<std::shared_ptr<ValueData>>(). Therefore, the data type for the output ports is an object to a shared pointer to a ValueData object.
  • The values are then sent out using op_output.emit() on lines 16 and 19. The port name is required since there is more than one port on this operator.

Data types of the output ports are shared pointers (std::shared_ptr), hence the call to std::make_shared<ValueData>(...) on lines 15 and 18.

We then configure the middle operator - PingMxOp - to receive that data on ports in1 and in2:

C++
Python
1 class PingMxOp : public Operator {
2 public:
3 HOLOSCAN_OPERATOR_FORWARD_ARGS(PingMxOp)
4  
5 PingMxOp() = default;
6  
7 void setup(OperatorSpec& spec) override {
8 spec.input<std::shared_ptr<ValueData>>("in1");
9 spec.input<std::shared_ptr<ValueData>>("in2");
10 spec.output<std::shared_ptr<ValueData>>("out1");
11 spec.output<std::shared_ptr<ValueData>>("out2");
12 spec.param(multiplier_, "multiplier", "Multiplier", "Multiply the input by this value", 2);
13 }
14  
15 void compute(InputContext& op_input, OutputContext& op_output, ExecutionContext&) override {
16 auto value1 = op_input.receive<std::shared_ptr<ValueData>>("in1").value();
17 auto value2 = op_input.receive<std::shared_ptr<ValueData>>("in2").value();
18  
19 HOLOSCAN_LOG_INFO("Middle message received (count: {})", count_++);
20  
21 HOLOSCAN_LOG_INFO("Middle message value1: {}", value1->data());
22 HOLOSCAN_LOG_INFO("Middle message value2: {}", value2->data());
23  
24 // Multiply the values by the multiplier parameter
25 value1->data(value1->data() * multiplier_);
26 value2->data(value2->data() * multiplier_);
27  
28 op_output.emit(value1, "out1");
29 op_output.emit(value2, "out2");
30 };
31  
32 private:
33 int count_ = 1;
34 Parameter<int> multiplier_;
35 };
  • We configure the input ports with the std::shared_ptr<ValueData> type on lines 8 and 9 using spec.input<std::shared_ptr<ValueData>>().
  • The values are received using op_input.receive() on lines 16 and 17 using the port names. The received values are of type std::shared_ptr<ValueData> as mentioned in the templated receive() method.

PingMxOp processes the data, then sends it out on two ports, similarly to what is done by PingTxOp above.

Receiving Any Number of Inputs

In this workflow, PingRxOp has a single input port - receivers - that is connected to two upstream ports from PingMxOp. When an input port needs to connect to multiple upstream ports, we define it with spec.input() and set the size to IOSpec::kAnySize (or IOSpec.ANY_SIZE in Python). This allows the input port to receive data from multiple sources. The inputs are then stored in a vector, following the order they were added with add_flow().

C++
Python
1 class PingRxOp : public Operator {
2 public:
3 HOLOSCAN_OPERATOR_FORWARD_ARGS(PingRxOp)
4  
5 PingRxOp() = default;
6  
7 void setup(OperatorSpec& spec) override {
8 // // Since Holoscan SDK v2.3, users can define a multi-receiver input port using 'spec.input()'
9 // // with 'IOSpec::kAnySize'.
10 // // The old way is to use 'spec.param()' with 'Parameter<std::vector<IOSpec*>> receivers_;'.
11 // spec.param(receivers_, "receivers", "Input Receivers", "List of input receivers.", {});
12 spec.input<std::vector<std::shared_ptr<ValueData>>>("receivers", IOSpec::kAnySize);
13 }
14  
15 void compute(InputContext& op_input, OutputContext&, ExecutionContext&) override {
16 auto value_vector =
17 op_input.receive<std::vector<std::shared_ptr<ValueData>>>("receivers").value();
18  
19 HOLOSCAN_LOG_INFO("Rx message received (count: {}, size: {})", count_++, value_vector.size());
20  
21 HOLOSCAN_LOG_INFO("Rx message value1: {}", value_vector[0]->data());
22 HOLOSCAN_LOG_INFO("Rx message value2: {}", value_vector[1]->data());
23 };
24  
25 private:
26 // // Since Holoscan SDK v2.3, the following line is no longer needed.
27 // Parameter<std::vector<IOSpec*>> receivers_;
28 int count_ = 1;
29 };
  • In the operator’s setup() method, we define an input port receivers (line 12) with holoscan::IOSpec::kAnySize to allow any number of upstream ports to connect to it.
  • The values are retrieved using op_input.receive<std::vector<std::shared_ptr<ValueData>>>(...).
  • The type of value_vector is std::vector<std::shared_ptr<ValueData>> (lines 16-17).

Please see Retrieving Any Number of Inputs Cpp for more information on how to retrieve any number of inputs in C++.

The rest of the code creates the application, operators, and defines the workflow:

C++
Python
1 class MyPingApp : public holoscan::Application {
2 public:
3 void compose() override {
4 using namespace holoscan;
5  
6 // Define the tx, mx, rx operators, allowing the tx operator to execute 10 times
7 auto tx = make_operator&lt;ops::PingTxOp&gt;("tx", make_condition<CountCondition>(10));
8 auto mx = make_operator&lt;ops::PingMxOp&gt;("mx", Arg("multiplier", 3));
9 auto rx = make_operator&lt;ops::PingRxOp&gt;("rx");
10  
11 // Define the workflow
12 add_flow(tx, mx, {{"out1", "in1"}, {"out2", "in2"}});
13 add_flow(mx, rx, {{"out1", "receivers"}, {"out2", "receivers"}});
14 }
15 };
16  
17 int main(int argc, char** argv) {
18 auto app = holoscan::make_application<MyPingApp>();
19 app->run();
20  
21 return 0;
22 }
  • The operators tx, mx, and rx are created in the application’s compose() similarly to previous examples.
  • Since the operators in this example have multiple input/output ports, we need to specify the third, port name pair argument when calling add_flow():
    • tx/out1 is connected to mx/in1, and tx/out2 is connected to mx/in2.
    • mx/out1 and mx/out2 are both connected to rx/receivers.

Running the Application

Running the application should give you output similar to the following in your terminal.

[info] [fragment.cpp:586] Loading extensions from configs...
[info] [gxf_executor.cpp:249] Creating context
[info] [gxf_executor.cpp:1960] Activating Graph...
[info] [gxf_executor.cpp:1992] Running Graph...
[info] [greedy_scheduler.cpp:191] Scheduling 3 entities
[info] [gxf_executor.cpp:1994] Waiting for completion...
[info] [ping_multi_port.cpp:80] Middle message received (count: 1)
[info] [ping_multi_port.cpp:82] Middle message value1: 1
[info] [ping_multi_port.cpp:83] Middle message value2: 2
[info] [ping_multi_port.cpp:116] Rx message received (count: 1, size: 2)
[info] [ping_multi_port.cpp:118] Rx message value1: 3
[info] [ping_multi_port.cpp:119] Rx message value2: 6
[info] [ping_multi_port.cpp:80] Middle message received (count: 2)
[info] [ping_multi_port.cpp:82] Middle message value1: 3
[info] [ping_multi_port.cpp:83] Middle message value2: 4
[info] [ping_multi_port.cpp:116] Rx message received (count: 2, size: 2)
[info] [ping_multi_port.cpp:118] Rx message value1: 9
[info] [ping_multi_port.cpp:119] Rx message value2: 12
...
[info] [ping_multi_port.cpp:80] Middle message received (count: 10)
[info] [ping_multi_port.cpp:82] Middle message value1: 19
[info] [ping_multi_port.cpp:83] Middle message value2: 20
[info] [ping_multi_port.cpp:116] Rx message received (count: 10, size: 2)
[info] [ping_multi_port.cpp:118] Rx message value1: 57
[info] [ping_multi_port.cpp:119] Rx message value2: 60
[info] [greedy_scheduler.cpp:372] Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.
[info] [greedy_scheduler.cpp:401] Scheduler finished.
[info] [gxf_executor.cpp:1997] Deactivating Graph...
[info] [gxf_executor.cpp:2005] Graph execution finished.
[info] [gxf_executor.cpp:278] Destroying context

Depending on your log level you may see more or fewer messages. The output above was generated using the default value of INFO. Refer to the Logging section for more details on how to set the log level.