{
"cells": [
{
"cell_type": "markdown",
"id": "e9c959ab",
"metadata": {
"pycharm": {
"name": "#%% md\n"
}
},
"source": [
"*Copyright (C) 2021 Intel Corporation*
\n",
"*SPDX-License-Identifier: BSD-3-Clause*
\n",
"*See: https://spdx.org/licenses/*\n",
"\n",
"---\n",
"\n",
"# Connect processes\n",
"\n",
"This tutorial gives an introduction in how to connect _Processes_ to build a network of asynchronously operating and interacting _Processes_.\n",
"\n",
"## Recommended tutorials before starting:\n",
"\n",
"- [Installing Lava](./tutorial01_installing_lava.ipynb \"Tutorial on Installing Lava\")\n",
"- [Processes](./tutorial02_processes.ipynb \"Tutorial on Processes\")\n",
"- [ProcessModel](./tutorial03_process_models.ipynb \"Tutorial on ProcessModels\")\n",
"- [Execution](./tutorial04_execution.ipynb \"Tutorial on Executing Processes\")\n",
"\n",
" \n",
"## Building a network of _Processes_\n",
"\n",
"_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_.\n",
"\n",
"## Create a connection\n",
"\n",
"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_.\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "911c7c62",
"metadata": {},
"outputs": [],
"source": [
"from lava.magma.core.process.process import AbstractProcess\n",
"from lava.magma.core.process.ports.ports import InPort, OutPort"
]
},
{
"cell_type": "markdown",
"id": "b359cb60",
"metadata": {},
"source": [
"As first step we define the _Processes_ _P1_ and _P2_ with their respective _Ports_ _out_ and _inp_."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "628ec281",
"metadata": {},
"outputs": [],
"source": [
"# Minimal process with an OutPort\n",
"class P1(AbstractProcess):\n",
" def __init__(self, **kwargs):\n",
" super().__init__(**kwargs)\n",
" shape = kwargs.get('shape', (2,))\n",
" self.out = OutPort(shape=shape)\n",
"\n",
"\n",
"# Minimal process with an InPort\n",
"class P2(AbstractProcess):\n",
" def __init__(self, **kwargs):\n",
" super().__init__(**kwargs)\n",
" shape = kwargs.get('shape', (2,))\n",
" self.inp = InPort(shape=shape)"
]
},
{
"cell_type": "markdown",
"id": "5517b6a2",
"metadata": {},
"source": [
"_Process_ _P1_ and _P2_ require a corresponding _ProcessModel_ which implements their _Ports_ and a simple RunConfig for sending and receiving data.\n",
"\n",
"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.\n",
"\n",
"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."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "1bf11a81",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"from lava.magma.core.model.py.model import PyLoihiProcessModel\n",
"from lava.magma.core.decorator import implements, requires, tag\n",
"from lava.magma.core.resources import CPU\n",
"from lava.magma.core.model.py.type import LavaPyType\n",
"from lava.magma.core.model.py.ports import PyInPort, PyOutPort\n",
"from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "4cab9b45",
"metadata": {},
"outputs": [],
"source": [
"# A minimal PyProcModel implementing P1\n",
"@implements(proc=P1, protocol=LoihiProtocol)\n",
"@requires(CPU)\n",
"@tag('floating_pt')\n",
"class PyProcModelA(PyLoihiProcessModel):\n",
" out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, int)\n",
"\n",
" def run_spk(self):\n",
" data = np.array([1, 2])\n",
" self.out.send(data)\n",
" print(\"Sent output data of P1: {}\".format(data))\n",
"\n",
"\n",
"\n",
"# A minimal PyProcModel implementing P2\n",
"@implements(proc=P2, protocol=LoihiProtocol)\n",
"@requires(CPU)\n",
"@tag('floating_pt')\n",
"class PyProcModelB(PyLoihiProcessModel):\n",
" inp: PyInPort = LavaPyType(PyInPort.VEC_DENSE, int)\n",
"\n",
" def run_spk(self):\n",
" in_data = self.inp.recv()\n",
" print(\"Received input data for P2: {}\".format(in_data))"
]
},
{
"cell_type": "markdown",
"id": "b5fdc5ac",
"metadata": {},
"source": [
"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_."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "9f3ff826",
"metadata": {},
"outputs": [],
"source": [
"sender = P1()\n",
"recv = P2()\n",
"\n",
"# Connecting output port to an input port\n",
"sender.out.connect(recv.inp)\n",
"\n",
"sender = P1()\n",
"recv = P2()\n",
"\n",
"# ... or connecting an input port from an output port\n",
"recv.inp.connect_from(sender.out)"
]
},
{
"cell_type": "markdown",
"id": "dc243685",
"metadata": {},
"source": [
"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."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "41a6fb52",
"metadata": {},
"outputs": [],
"source": [
"from lava.magma.core.run_configs import Loihi1SimCfg\n",
"from lava.magma.core.run_conditions import RunSteps"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "f457f0ce",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Sent output data of P1: [1 2]\n",
"Received input data for P2: [1 2]\n"
]
}
],
"source": [
"sender.run(RunSteps(num_steps=1), Loihi1SimCfg())\n",
"sender.stop()"
]
},
{
"cell_type": "markdown",
"id": "f7f639ca",
"metadata": {},
"source": [
"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."
]
},
{
"cell_type": "markdown",
"id": "708a1c95",
"metadata": {},
"source": [
"## Possible connections\n",
"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.\n",
"\n",
"\n",
"\n",
"#### There are some things to consider though:\n",
"- _InPorts_ cannot connect to _OutPorts_\n",
"- Shape and datatype of connect _Ports_ must match\n",
"- An _InPort_ might get data from multiple _OutPorts_ - default behavior is a summation of the incoming data\n",
"- An _OutPort_ might send data to multiple _InPorts_ - all _InPorts_ receive the same data\n"
]
},
{
"cell_type": "markdown",
"id": "490422b6",
"metadata": {},
"source": [
"## Connect multiple _InPorts_ from a single _OutPort_\n",
"\n",
""
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "597e7d19",
"metadata": {},
"outputs": [],
"source": [
"sender = P1()\n",
"recv1 = P2()\n",
"recv2 = P2()\n",
"recv3 = P2()\n",
"\n",
"# An OutPort can connect to multiple InPorts\n",
"# Either at once...\n",
"sender.out.connect([recv1.inp, recv2.inp, recv3.inp])\n",
"\n",
"sender = P1()\n",
"recv1 = P2()\n",
"recv2 = P2()\n",
"recv3 = P2()\n",
"\n",
"# ... or consecutively\n",
"sender.out.connect(recv1.inp)\n",
"sender.out.connect(recv2.inp)\n",
"sender.out.connect(recv3.inp)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "f2dccd5f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Received input data for P2: [1 2]\n",
"Sent output data of P1: [1 2]\n",
"Received input data for P2: [1 2]\n",
"Received input data for P2: [1 2]\n"
]
}
],
"source": [
"sender.run(RunSteps(num_steps=1), Loihi1SimCfg())\n",
"sender.stop()"
]
},
{
"cell_type": "markdown",
"id": "d6a69af4",
"metadata": {},
"source": [
"The instance `sender` of P1 sent the data `[1 2]` to the 3 instances `recv1, recv2, recv3` of P2."
]
},
{
"cell_type": "markdown",
"id": "69ae6cee",
"metadata": {},
"source": [
"## Connecting multiple _InPorts_ to a single _OutPort_\n",
"\n",
"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_.\n",
"\n",
""
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "b1f8659a",
"metadata": {},
"outputs": [],
"source": [
"sender1 = P1()\n",
"sender2 = P1()\n",
"sender3 = P1()\n",
"recv = P2()\n",
"\n",
"# An InPort can connect to multiple OutPorts\n",
"# Either at once...\n",
"recv.inp.connect_from([sender1.out, sender2.out, sender3.out])\n",
"\n",
"sender1 = P1()\n",
"sender2 = P1()\n",
"sender3 = P1()\n",
"recv = P2()\n",
"\n",
"# ... or consecutively\n",
"sender1.out.connect(recv.inp)\n",
"sender2.out.connect(recv.inp)\n",
"sender3.out.connect(recv.inp)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "b94ed812",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Sent output data of P1: [1 2]\n",
"Received input data for P2: [3 6]\n",
"Sent output data of P1: [1 2]\n",
"Sent output data of P1: [1 2]\n"
]
}
],
"source": [
"sender1.run(RunSteps(num_steps=1), Loihi1SimCfg())\n",
"sender1.stop()"
]
},
{
"cell_type": "markdown",
"id": "17348f04",
"metadata": {},
"source": [
"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]`."
]
},
{
"cell_type": "markdown",
"id": "24ceb7ca",
"metadata": {},
"source": [
"## How to learn more?\n",
"\n",
"Learn how to implement and compose the behavior of a process using other processes in the [next tutorial on hierarchical Processes](./tutorial06_hierarchical_processes.ipynb \"Tutorial on Hierarchical Processes\").\n",
"\n",
"If you want to find out more about connecting processes, have a look at the [Lava documentation](https://lava-nc.org/ \"Lava Documentation\") or dive into the [source code](https://github.com/lava-nc/lava/tree/main/src/lava/magma/core/process/ports/ports.py \"Port Source Code\").\n",
"\n",
"To receive regular updates on the latest developments and releases of the Lava Software Framework please subscribe to the [INRC newsletter](http://eepurl.com/hJCyhb \"INRC Newsletter\")."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.4"
}
},
"nbformat": 4,
"nbformat_minor": 5
}