{ "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 }