{ "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", "# _ProcessModels_\n", "\n", "This tutorial explains how Lava _ProcessModels_ implement the behavior of Lava _Processes_. Each Lava _Process_ must have one or more _ProcessModels_, which provide the instructions for how to execute a Lava _Process_. Lava _ProcessModels_ allow a user to specify a Process's behavior in one or more languages (like Python, C, or the Loihi neurocore interface) and for various compute resources (like CPUs, GPUs, or Loihi chips). In this way, _ProcessModels_ enable seamles cross-platform execution of _Processes_ and allow users to build applications and algorithms agonostic of platform-specific implementations.\n", "\n", "There are two broad classes of _ProcessModels_: _LeafProcessModel_ and _SubProcessModel_. _LeafProcessModels_, which will be the focus of this tutorial, implement the behavior of a process directly. _SubProcessModels_ allow users to implement and compose the behavior of a _Process_ using other _Processes_, thus enabling the creation of Hierarchical _Processes_." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this tutorial, we walk through the creation of multiple _LeafProcessModels_ that could be used to implement the behavior of a Leaky Integrate-and-Fire (LIF) neuron _Process_." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Recommended tutorials before starting: \n", "- [Installing Lava](./tutorial01_installing_lava.ipynb \"Tutorial on Installing Lava\")\n", "- [Processes](./tutorial02_processes.ipynb \"Tutorial on Processes\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create a LIF _Process_" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First, we will define our LIF _Process_ exactly as it is defined in the `Magma` core library of Lava. (For more information on defining Lava Processes, see the [previous tutorial](./tutorial02_processes.ipynb).) Here the LIF neural _Process_ accepts activity from synaptic inputs via _InPort_ `a_in` and outputs spiking activity via _OutPort_ `s_out`." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "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", "class LIF(AbstractProcess):\n", " \"\"\"Leaky-Integrate-and-Fire (LIF) neural Process.\n", "\n", " LIF dynamics abstracts to:\n", " u[t] = u[t-1] * (1-du) + a_in # neuron current\n", " v[t] = v[t-1] * (1-dv) + u[t] + bias_mant * 2 ** bias_exp # neuron voltage\n", " s_out = v[t] > vth # spike if threshold is exceeded\n", " v[t] = 0 # reset at spike\n", "\n", " Parameters\n", " ----------\n", " du: Inverse of decay time-constant for current decay.\n", " dv: Inverse of decay time-constant for voltage decay.\n", " bias_mant: Mantissa part of neuron bias.\n", " bias_exp: Exponent part of neuron bias, if needed. Mostly for fixed point\n", " implementations. Unnecessary for floating point\n", " implementations. If specified, bias = bias_mant * 2**bias_exp.\n", " vth: Neuron threshold voltage, exceeding which, the neuron will spike.\n", " \"\"\"\n", " def __init__(self, **kwargs):\n", " super().__init__()\n", " shape = kwargs.get(\"shape\", (1,))\n", " du = kwargs.pop(\"du\", 0)\n", " dv = kwargs.pop(\"dv\", 0)\n", " bias_mant = kwargs.pop(\"bias_mant\", 0)\n", " bias_exp = kwargs.pop(\"bias_exp\", 0)\n", " vth = kwargs.pop(\"vth\", 10)\n", "\n", " self.shape = shape\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=du)\n", " self.dv = Var(shape=(1,), init=dv)\n", " self.bias_mant = Var(shape=shape, init=bias_mant)\n", " self.bias_exp = Var(shape=shape, init=bias_exp)\n", " self.vth = Var(shape=(1,), init=vth)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create a Python _LeafProcessModel_ that implements the LIF _Process_" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we will create a Python _ProcessModel_, or _PyProcessModel_, that runs on a CPU compute resource and implements the LIF _Process_ behavior." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Setup" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We begin by importing the required Lava classes.\n", "First, we setup our compute resources (CPU) and our _SyncProtocol_. A _SyncProtocol_ defines how and when parallel _Processes_ synchronize. Here we use the _LoihiProtoicol_ which defines the synchronization phases required for execution on the Loihi chip, but users may also specify a completely asynchronous protocol or define a custom _SyncProtocol_. The decorators imported will be necessary to specify the resource _Requirements_ and _SyncProtocol_ of our _ProcessModel_. " ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from lava.magma.core.decorator import implements, requires\n", "from lava.magma.core.resources import CPU\n", "from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "Now we import the parent class from which our _ProcessModel_ inherits, as well as our required _Port_ and _Variable_ types. _PyLoihiProcessModel_ is the abstract class for a Python _ProcessModel_ that implements the _LoihiProtocol_. Our _ProcessModel_ needs _Ports_ and _Variables_ that mirror those the LIF _Process_. The in-ports and out-ports of a Python _ProcessModel_ have types _PyInPort_ and _PyOutPort_, respectively, while variables have type _LavaPyType_." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "from lava.magma.core.model.py.model import PyLoihiProcessModel\n", "from lava.magma.core.model.py.ports import PyInPort, PyOutPort\n", "from lava.magma.core.model.py.type import LavaPyType" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Defining a _PyLifModel_ for LIF" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "We now define a _LeafProcessModel_ `PyLifModel` that implements the behavior of the LIF _Process_.\n", "\n", "The `@implements` decorator specifies the _SyncProtocol_ (`protocol=LoihiProtocol`) and the class of the _Process_ (`proc=LIF`) corresponding to the _ProcessModel_. The `@requires` decorator specifies the CPU compute resource required by the _ProcessModel_. The `@tag` decorator specifies the precision of the _ProcessModel_. Here we illustrate a _ProcessModel_ with standard, floating point precision.\n", "\n", "Next we define the _ProcessModel_ variables and ports. The variables and ports defined in the _ProcessModel_ must exactly match (by name and number) the variables and ports defined in the corresponding _Process_ for compilation. Our LIF example _Process_ and `PyLifModel` each have 1 input port, 1 output port, and variables for `u`, `v`, `du`, `dv`, `bias`, `bias_exp`, and `vth`. Variables and ports in a _ProcessModel_ must be initialized with _LavaType_ objects specific to the language of the _LeafProcessModel_ implementation. Here, variables are initialized with the `LavaPyType` to match our Python _LeafProcessModel_ implementation. In general, _LavaTypes_ specify the class-types of variables and ports, including their numeric d_type, precision and dynamic range. The Lava _Compiler_ reads these _LavaTypes_ to initialize concrete class objects from the initial values provided in the _Process_.\n", "\n", "We then fill in the `run_spk()` method to execute the LIF neural dynamics. `run_spk()` is a method specific to _LeafProcessModels_ of type `PyLoihiProcessModel` that executes user-defined neuron dynamics with correct handling of all phases our `LoihiProtocol` _SyncProtocol_. In this example, `run_spike` will accept activity from synaptic inputs via _PyInPort_ `a_in`, and, after integrating current and voltage according to current-based (CUBA) dynamics, output spiking activity via _PyOutPort_ `s_out`. `recv()` and `send()` are the methods that support the channel based communication of the inputs and outputs to our _ProcessModel_. For more detailed information about Ports and channel-based communication, see the [Connection Tutorial](./tutorial05_connect_processes.ipynb)." ] }, { "cell_type": "code", "execution_count": 4, "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 PyLifModel1(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_mant: np.ndarray = LavaPyType(np.ndarray, float)\n", " bias_exp: 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_mant * (2 ** self.bias_exp)\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": [ "#### Compile and run _PyLifModel_" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[6. 6. 6.]\n" ] } ], "source": [ "from lava.magma.core.run_configs import Loihi1SimCfg\n", "from lava.magma.core.run_conditions import RunSteps\n", "\n", "lif = LIF(shape=(3,), du=0, dv=0, bias_mant=3, vth=10)\n", "\n", "run_cfg = Loihi1SimCfg()\n", "lif.run(condition=RunSteps(num_steps=10), run_cfg=run_cfg)\n", "print(lif.v.get())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Selecting 1 _ProcessModel_: More on _LeafProcessModel_ attributes and relations" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We have demonstrated multiple _ProcessModel_ implementations of a single LIF _Process_. How is one of several _ProcessModels_ then selected as the implementation of a _Process_ during runtime? To answer that question, we take a deeper dive into the attributes of a _LeafProcessModel_ and the relationship between a _LeafProcessModel_, a _Process_, and a _SyncProtocol_. \n", "\n", "As shown below, a _LeafProcessModel_ implements both a Process (in our example, LIF) and a _SyncProtocol_ (in our example, the _LoihiProtocol_). A _LeafProcessModel_ has a single _Type_. In this tutorial `PyLifModel` has Type `PyLoihiProcessModel`. A _LeafProcessModel_ also has one or more resource _Requirements_ that specify the compute resources (for example, a CPU, a GPU, or Loihi Neurocores) or peripheral resources (like access to a camera) that are required for execution. Finally, a _LeafProcessModel_ can have one and more user-defineable _Tags_. _Tags_ can be used, among other customizable reasons, to group multiple _ProcessModels_ for a multi- _Process_ application or to distinguish between multiple _LeafProcessModel_ implementations with the same _Type_ and _SyncProtocol_. As an example, we illustrated above a `PyLoihiProcessModel` for LIF that uses floating point precision and has the tag `@tag('floating_pt')`. There also exists a `PyLoihiProcessModel` that uses fixed point precision and has behavior that is bit-accurate with LIF execution on a Loihi chip; this _ProcessModel_ is distinguished by the tag `@tag('fixed_pt')`. Together, the _Type_, _Tag_ and _Requirement_ attributes of a _LeafProcessModel_ allow users to define a _RunConfig_ that chooses which of several _LeafProcessModels_ is used to implement a _Process_ at runtime. The Core Lava Library will also provide several preconfigured _RunConfigs_. \n", "\n", "" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## How to learn more?\n", "\n", "Learn how to execute single _Processes_ and networks of _Processes_ in the [next tutorial](./tutorial04_execution.ipynb).\n", "\n", "If you want to find out more about _ProcessModels_, have a look at the [Lava documentation](https://lava-nc.org/) or dive into the [source code](https://github.com/lava-nc/lava/tree/main/src/lava/magma/core/model/model.py).\n", "\n", "To receive regular updates on the latest developments and releases of the Lava Software Framework please subscribe to [our newsletter](http://eepurl.com/hJCyhb)." ] } ], "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 }