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