Copyright (C) 2021 Intel Corporation SPDX-License-Identifier: BSD-3-Clause See: https://spdx.org/licenses/


Connect processes

This tutorial gives an introduction in how to connect Processes to build a network of asynchronously operating and interacting Processes.

Building a network of Processes

Processes are the main building blocks of Lava. Each Process can exercise different computations and usually depends on some input data and/or creates output data. Transfering I/O data between Processes is a key element of Lava. A Process can have various input and output Ports which are then connected via channels to corresponding Ports of another Process. This allows to build networks of asynchronously operating and interacting Processes.

Create a connection

The objective is to connect Process P1 with Process P2. P1 has an output Port OutPort called out and P2 has an input port InPort called inp. Data from P1 provided to the Port out should be transfered to P2 and received from Port inp.

a5652edb60af4de3a45bb495cd9e43b8

[1]:
from lava.magma.core.process.process import AbstractProcess
from lava.magma.core.process.ports.ports import InPort, OutPort

As first step we define the Processes P1 and P2 with their respective Ports out and inp.

[2]:
# Minimal process with an OutPort
class P1(AbstractProcess):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        shape = kwargs.get('shape', (2,))
        self.out = OutPort(shape=shape)


# Minimal process with an InPort
class P2(AbstractProcess):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        shape = kwargs.get('shape', (2,))
        self.inp = InPort(shape=shape)

Process P1 and P2 require a corresponding ProcessModel which implements their Ports and a simple RunConfig for sending and receiving data.

The ProcessModels can be written in Python and should be exectued on a CPU. The input and output Port should be able to receive/send a vector of integers and print the transferred data.

So the ProcessModel inherits from AbstractPyProcessModel in order to execute Python code and the configured ComputeResource is a CPU. A LavaPyType is used for the Ports. The LavaPyType specifies the expected data format for the Port. A dense vector of type integer is chosen with the parameters _PyOutPort._VEC_DENSE and int. The Ports can be used to send and receive data by calling send or recv. The sent and received data is afterwards printed out.

[3]:
import numpy as np
from lava.magma.core.model.py.model import PyLoihiProcessModel
from lava.magma.core.decorator import implements, requires, tag
from lava.magma.core.resources import CPU
from lava.magma.core.model.py.type import LavaPyType
from lava.magma.core.model.py.ports import PyInPort, PyOutPort
from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol
[4]:
# A minimal PyProcModel implementing P1
@implements(proc=P1, protocol=LoihiProtocol)
@requires(CPU)
@tag('floating_pt')
class PyProcModelA(PyLoihiProcessModel):
    out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, int)

    def run_spk(self):
        data = np.array([1, 2])
        self.out.send(data)
        print("Sent output data of P1: {}".format(data))



# A minimal PyProcModel implementing P2
@implements(proc=P2, protocol=LoihiProtocol)
@requires(CPU)
@tag('floating_pt')
class PyProcModelB(PyLoihiProcessModel):
    inp: PyInPort = LavaPyType(PyInPort.VEC_DENSE, int)

    def run_spk(self):
        in_data = self.inp.recv()
        print("Received input data for P2: {}".format(in_data))

Next, the processes P1 and P2 are instantiated and the output Port out from Process P1 is connected with the input Port inp of Process P2.

[5]:
sender = P1()
recv = P2()

# Connecting output port to an input port
sender.out.connect(recv.inp)

sender = P1()
recv = P2()

# ... or connecting an input port from an output port
recv.inp.connect_from(sender.out)

Calling run() on either of these Processes will first call the Compiler. During compilation the specified connection is setup by creating a channel between P1 and P2. Now data can be transfered during execution as seen by the output print statements.

[6]:
from lava.magma.core.run_configs import Loihi1SimCfg
from lava.magma.core.run_conditions import RunSteps
[7]:
sender.run(RunSteps(num_steps=1), Loihi1SimCfg())
sender.stop()
Sent output data of P1: [1 2]
Received input data for P2: [1 2]

The instance sender of P1 sent the data [1 2] via its OutPort out to the InPort in of the instance recv of P2, where the data is received.

Possible connections

This first example was very simple. In principle, Processes can have multiple input and output Ports which can be freely connected with each other. Also, Processes which execute on different compute resources can be connected in the same way.

2d29c0fc77184468ae05fc12263e4275

There are some things to consider though:

  • InPorts cannot connect to OutPorts

  • Shape and datatype of connect Ports must match

  • An InPort might get data from multiple OutPorts - default behavior is a summation of the incoming data

  • An OutPort might send data to multiple InPorts - all InPorts receive the same data

Connect multiple InPorts from a single OutPort

8f50eb91b61841c1976bcf181b34003c

[8]:
sender = P1()
recv1 = P2()
recv2 = P2()
recv3 = P2()

# An OutPort can connect to multiple InPorts
# Either at once...
sender.out.connect([recv1.inp, recv2.inp, recv3.inp])

sender = P1()
recv1 = P2()
recv2 = P2()
recv3 = P2()

# ... or consecutively
sender.out.connect(recv1.inp)
sender.out.connect(recv2.inp)
sender.out.connect(recv3.inp)
[9]:
sender.run(RunSteps(num_steps=1), Loihi1SimCfg())
sender.stop()
Received input data for P2: [1 2]
Sent output data of P1: [1 2]
Received input data for P2: [1 2]
Received input data for P2: [1 2]

The instance sender of P1 sent the data [1 2] to the 3 instances recv1, recv2, recv3 of P2.

Connecting multiple InPorts to a single OutPort

If multiple input Ports connect to the same output Port the default behavior is that the data from each input Port is added up at the output Port.

7c5f36e1838641488e2ea2c75428ba22

[10]:
sender1 = P1()
sender2 = P1()
sender3 = P1()
recv = P2()

# An InPort can connect to multiple OutPorts
# Either at once...
recv.inp.connect_from([sender1.out, sender2.out, sender3.out])

sender1 = P1()
sender2 = P1()
sender3 = P1()
recv = P2()

# ... or consecutively
sender1.out.connect(recv.inp)
sender2.out.connect(recv.inp)
sender3.out.connect(recv.inp)
[11]:
sender1.run(RunSteps(num_steps=1), Loihi1SimCfg())
sender1.stop()
Sent output data of P1: [1 2]
Received input data for P2: [3 6]
Sent output data of P1: [1 2]
Sent output data of P1: [1 2]

The 3 instances sender1, sender2, sender3 of P1 sent the data [1 2] to the instance recv of P2, where the data was summed up to [3 6].

How to learn more?

Learn how to implement and compose the behavior of a process using other processes in the next tutorial on hierarchical Processes.

If you want to find out more about connecting processes, have a look at the Lava documentation or dive into the source code.

To receive regular updates on the latest developments and releases of the Lava Software Framework please subscribe to the INRC newsletter.