{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "*Copyright (C) 2021 Intel Corporation*
\n", "*SPDX-License-Identifier: BSD-3-Clause*
\n", "*See: https://spdx.org/licenses/*\n", "\n", "---\n", "\n", "# Processes\n", "\n", "Learn how to create _Processes_, the fundamental computational units used in Lava to build algorithms and applications." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Recommended tutorials before starting:\n", "\n", "- [Installing Lava](./tutorial01_installing_lava.ipynb \"Tutorial on Installing Lava\")\n", "\n", "\n", "## What is a _Process_?\n", "\n", "This tutorial will show how to create a _Process_ that simulates a group of leaky integrate-and-fire neurons. But in Lava, the concept of _Processes_ applies widely beyond this example. In general, a _Process_ describes an individual program unit which encapsulates\n", "
    \n", "
  1. data that store its state,
  2. \n", "
  3. algorithms that describe how to manipulate the data,
  4. \n", "
  5. ports that share data with other Processes, and
  6. \n", "
  7. an API that facilitates user interaction.
  8. \n", "
\n", "\n", "A _Process_ can thus be as simple as a single neuron or a synapse, as complex as a full neural network, and as non-neuromorphic as a streaming interface for a peripheral device or an executed instance of regular program code.\n", "\n", "\n", "\n", "_Processes_ are independent from each other as they primarily operate on their own local memory while they pass messages between each other via channels. Different _Processes_ thus proceed their computations simultaneously and asynchronously, mirroring the high parallelism inherent in neuromorphic hardware. The parallel _Processes_ are furthermore safe against side effects from shared-memory interaction.\n", "\n", "Once a _Process_ has been coded in Python, Lava allows to run it seamlessly across different backends such as a CPU, a GPU, or neuromorphic cores. Developers can thus easily test and benchmark their applications on classical computing hardware and then deploy it to neuromorphic hardware. Furthermore, Lava takes advantage of distributed, heterogeneous hardware such as Loihi as it can run some _Processes_ on neuromorphic cores and in parallel others on embedded conventional CPUs and GPUs. \n", "\n", "While Lava provides a growing [library of Processes](https://github.com/lava-nc/lava/tree/main/src/lava/proc \"Lava's process library\"), you can easily write your own processes that suit your needs." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## How to build a _Process_?\n", "\n", "#### Overall architecture\n", "\n", "All _Processes_ in Lava share a universal architecture as they inherit from the same _AbstractProcess_ class. Each _Process_ consists of the following four key components.\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### _AbstractProcess_: Defining _Vars_, _Ports_, and the API\n", "\n", "When you create your own new process, you need to inherit from the AbstractProcess class. As an example, we will implement the *class LIF*, a group of leaky integrate-and-fire (LIF) neurons.\n", "\n", "\n", "\n", "| Component | Name | Python | | \n", "| :- | :- | :- | :-|\n", "| **Ports** | $a_{in}$ | _Inport_ | Receives spikes from upstream neurons.\n", "| | $s_{out}$ | _Outport_ | Transmits spikes to downstream neurons.\n", "| **State** | $u$ | _Var_ | Synaptic current of the LIF neurons.\n", "| | $v$ | _Var_ | Membrane voltage of the LIF neurons.\n", "| | $du$ | _Var_ | A time constant describing the current leakage.\n", "| | $dv$ | _Var_ | A time constant describing the voltage leakage.\n", "| | $bias$ | _Var_ | A bias value.\n", "| | $vth$ | _Var_ | A constant threshold that the membrane voltage needs to exceed for a spike.\n", "| **API** | All Vars | _Var_ | All public _Vars_ are considered part of the _Process_ API.\n", "| | All Ports | _AbstractPort_ | All _Ports_ are considered part of the _Process_ API.\n", "| | print_vars | _def_ | A function that prints all internal variables to help the user see if the LIF neuron has correctly been set up.\n", "\n", "The following code implements the class _LIF_ that you can also find in Lava's _Process_ library, but extends it by an additional API method that prints the state of the LIF neurons." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "from lava.magma.core.process.process import AbstractProcess\n", "from lava.magma.core.process.variable import Var\n", "from lava.magma.core.process.ports.ports import InPort, OutPort\n", "\n", "\n", "class LIF(AbstractProcess):\n", " \"\"\"Leaky-Integrate-and-Fire neural process with activation input and spike\n", " output ports a_in and s_out.\n", " \"\"\"\n", " def __init__(self, **kwargs):\n", " super().__init__()\n", " shape = kwargs.get(\"shape\", (1,))\n", " self.a_in = InPort(shape=shape)\n", " self.s_out = OutPort(shape=shape)\n", " self.u = Var(shape=shape, init=0)\n", " self.v = Var(shape=shape, init=0)\n", " self.du = Var(shape=(1,), init=kwargs.pop(\"du\", 0))\n", " self.dv = Var(shape=(1,), init=kwargs.pop(\"dv\", 0))\n", " self.bias = Var(shape=shape, init=kwargs.pop(\"bias\", 0))\n", " self.vth = Var(shape=(1,), init=kwargs.pop(\"vth\", 10))\n", "\n", " def print_vars(self):\n", " \"\"\"Prints all variables of a LIF process and their values.\"\"\"\n", "\n", " sp = 3 * \" \"\n", " print(\"Variables of the LIF:\")\n", " print(sp + \"u: {}\".format(str(self.u.get())))\n", " print(sp + \"v: {}\".format(str(self.v.get())))\n", " print(sp + \"du: {}\".format(str(self.du.get())))\n", " print(sp + \"dv: {}\".format(str(self.dv.get())))\n", " print(sp + \"bias: {}\".format(str(self.bias.get())))\n", " print(sp + \"vth: {}\".format(str(self.vth.get())))\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You may have noticed that most of the _Vars_ were initialized by scalar integers. But the synaptic current _u_ illustrates that _Vars_ can in general be initialized with numeric objects that have a dimensionality equal or less than specified by its _shape_ argument. The initial value will be scaled up to match the _Var_ dimension at run time.\n", "\n", "There are two further important things to notice about the _Process_ class:\n", "
    \n", "
  1. It only defines the interface of the LIF neuron, but not its temporal behavior.
  2. \n", "
  3. It is fully agnostic to the computing backend and will thus remain the same if you want to run your code, for example, once on a CPU and once on Loihi.
  4. \n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### _ProcessModel_: Defining the behavior of a _Process_\n", "\n", "The behavior of a _Process_ is defined by its _ProcessModel_. For the specific example of LIF neuron, the _ProcessModel_ describes how their current and voltage react to a synaptic input, how these states evolve with time, and when the neurons should emit a spike.\n", "\n", "A single _Process_ can have several _ProcessModels_, one for each backend that you want to run it on.\n", "\n", "The following code implements a _ProcessModel_ that defines how a CPU should run the LIF _Process_. Please do not worry about the precise implementation here—the code will be explained in detail in the next [Tutorial on ProcessModels](./tutorial03_process_models.ipynb \"Tutorial on ProcessModels\")." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol\n", "from lava.magma.core.model.py.ports import PyInPort, PyOutPort\n", "from lava.magma.core.model.py.type import LavaPyType\n", "from lava.magma.core.resources import CPU\n", "from lava.magma.core.decorator import implements, requires, tag\n", "from lava.magma.core.model.py.model import PyLoihiProcessModel\n", "\n", "@implements(proc=LIF, protocol=LoihiProtocol)\n", "@requires(CPU)\n", "@tag('floating_pt')\n", "class PyLifModel(PyLoihiProcessModel):\n", " a_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, float)\n", " s_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, bool, precision=1)\n", " u: np.ndarray = LavaPyType(np.ndarray, float)\n", " v: np.ndarray = LavaPyType(np.ndarray, float)\n", " bias: np.ndarray = LavaPyType(np.ndarray, float)\n", " du: float = LavaPyType(float, float)\n", " dv: float = LavaPyType(float, float)\n", " vth: float = LavaPyType(float, float)\n", "\n", " def run_spk(self):\n", " a_in_data = self.a_in.recv()\n", " self.u[:] = self.u * (1 - self.du)\n", " self.u[:] += a_in_data\n", " bias = self.bias\n", " self.v[:] = self.v * (1 - self.dv) + self.u + bias\n", " s_out = self.v >= self.vth\n", " self.v[s_out] = 0 # Reset voltage to 0\n", " self.s_out.send(s_out)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Instantiating the _Process_\n", "\n", "Now we can create an instance of our _Process_, in this case a group of 3 LIF neurons." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "n_neurons = 3\n", "\n", "lif = LIF(shape=(3,), du=0, dv=0, bias=3, vth=10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Interacting with _Processes_\n", "\n", "Once you have instantiated a group of LIF neurons, you can easily interact with them.\n", "\n", "#### Accessing _Vars_\n", "\n", "You can always read out the current values of the process _Vars_ to determine the _Process_ state. For example, all three neurons should have been initialized with a zero membrane voltage." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0\n" ] } ], "source": [ "print(lif.v.get())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As described above, the _Var_ _v_ has in this example been initialized as a scalar value that describes the membrane voltage of all three neurons simultaneously." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Using custom APIs\n", "\n", "To facilitate how users can interact with your _Process_, they can use the custom APIs that you provide them with. For LIF neurons, you defined a custom function that allows the user to inspect the internal _Vars_ of the LIF _Process_. Have a look if all _Vars_ have been set up correctly." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Variables of the LIF:\n", " u: 0\n", " v: 0\n", " du: 0\n", " dv: 0\n", " bias: 3\n", " vth: 10\n" ] } ], "source": [ "lif.print_vars()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Executing a _Process_\n", "\n", "Once the _Process_ is instantiated and you are satisfied with its state, you can run the _Process_. As long as a _ProcessModel_ has been defined for the desired backend, the _Process_ can run seamlessly across computing hardware. Do not worry about the details here—you will learn all about how Lava builds, compiles, and runs _Processes_ in a [separate tutorial](./tutorial04_execution.ipynb \"Tutorial on Executing Processes\").\n", "\n", "To run a _Process_, specify the number of steps to run for and select the desired backend." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "from lava.magma.core.run_configs import Loihi1SimCfg\n", "from lava.magma.core.run_conditions import RunSteps\n", "\n", "lif.run(condition=RunSteps(num_steps=1), run_cfg=Loihi1SimCfg())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The voltage of each LIF neuron should now have increased by the bias value, 3, from their initial values of 0. Check if the neurons have evolved as expected." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[3. 3. 3.]\n" ] } ], "source": [ "print(lif.v.get())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Update _Vars_\n", "\n", "You can furthermore update the internal _Vars_ manually. You may, for example, set the membrane voltage to new values between two runs." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[1. 2. 3.]\n" ] } ], "source": [ "lif.v.set(np.array([1, 2, 3]) )\n", "print(lif.v.get())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the _set()_ method becomes available once the _Process_ has been run. Prior to the first run, use the *\\_\\_init\\_\\_* function of the _Process_ to set _Vars_.\n", "\n", "Later tutorials will illustrate more sophisticated ways to access, store, and change variables during run time using _Process_ code.\n", "\n", "In the end, stop the process to terminate its execution. " ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "lif.stop()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## How to learn more?\n", "\n", "Learn how to implement the behavior of _Processes_ in the [next tutorial on ProcessModels](./tutorial03_process_models.ipynb \"Tutorial on ProcessModels\").\n", "\n", "If you want to find out more about _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/process.py \"Process 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.8.10" } }, "nbformat": 4, "nbformat_minor": 5 }