{
"cells": [
{
"cell_type": "markdown",
"id": "3874ace0",
"metadata": {},
"source": [
"*Copyright (C) 2021 Intel Corporation*
\n",
"*SPDX-License-Identifier: BSD-3-Clause*
\n",
"*See: https://spdx.org/licenses/*\n",
"\n",
"---\n",
"\n",
"# Hierarchical _Processes_ and _SubProcessModels_\n",
"\n",
"Previous tutorials have briefly covered that there are two categories of _ProcessModels_: _LeafProcessModels_ and _SubProcessModels_. The [ProcessModel Tutorial](./tutorial03_process_models.ipynb) explained _LeafProcessModels_ in detail. These implement the behavior of a _Process_ directly, in the language (for example, Python or Loihi Neurocore API) required for a particular compute resource (for example, a CPU or Loihi Neurocores). _SubProcessModels_, by contrast, allow users to implement and compose the behavior of a process _using other processes_. This enables the creation of _Hierarchical Processes_ and reuse of primitive _ProcessModels_ to realize more complex _ProcessModels_. _SubProcessModels_ inherit all compute resource requirements from the sub _Processes_ they instantiate. \n",
"\n",
"\n",
"\n",
"In this tutorial, we will create a Dense Layer Hierarchical _Process_ that has the behavior of Leaky-Integrate-and-Fire (LIF) neurons. The Dense Layer _ProcessModel_ implements this behavior via the primitive LIF and Dense Connection _Processes_ and their respective _PyLoihiProcessModels_."
]
},
{
"cell_type": "markdown",
"id": "caf49931",
"metadata": {},
"source": [
"## 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",
"- [Connecting Processes](./tutorial05_connect_processes.ipynb \"Tutorial on Connecting Processes\")"
]
},
{
"cell_type": "markdown",
"id": "93b41ea3",
"metadata": {},
"source": [
"## Create LIF and Dense _Processes_ and _ProcessModels_"
]
},
{
"cell_type": "markdown",
"id": "0bdc5ce8",
"metadata": {},
"source": [
"The [ProcessModel Tutorial](./tutorial03_process_models.ipynb) walks through the creation of a LIF _Process_ and an implementing _PyLoihiProcessModel_. Our DenseLayer _Process_ additionally requires a Dense Lava _Process_ and _ProcessModel_ that have the behavior of a dense set of synaptic connections and weights. The Dense Connection _Process_ can be used to connect neural _Processes_. For completeness, we'll first briefly show an example LIF and Dense _Process_ and _PyLoihiProcessModel_."
]
},
{
"cell_type": "markdown",
"id": "d60a928a",
"metadata": {},
"source": [
"#### Create a Dense connection _Process_"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "4fc1ffa1",
"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",
"\n",
"class Dense(AbstractProcess):\n",
" \"\"\"Dense connections between neurons.\n",
" Realizes the following abstract behavior:\n",
" a_out = W * s_in\n",
" \"\"\"\n",
"\n",
" def __init__(self, **kwargs):\n",
" super().__init__(**kwargs)\n",
" shape = kwargs.get(\"shape\", (1, 1))\n",
" self.s_in = InPort(shape=(shape[1],))\n",
" self.a_out = OutPort(shape=(shape[0],))\n",
" self.weights = Var(shape=shape, init=kwargs.pop(\"weights\", 0))"
]
},
{
"cell_type": "markdown",
"id": "b8767584",
"metadata": {},
"source": [
"#### Create a Python Dense connection _ProcessModel_ implementing the Loihi Sync Protocol and requiring a CPU compute resource"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "019c4fc7",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\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\n",
"from lava.magma.core.model.py.model import PyLoihiProcessModel\n",
"\n",
"@implements(proc=Dense, protocol=LoihiProtocol)\n",
"@requires(CPU)\n",
"class PyDenseModel(PyLoihiProcessModel):\n",
" s_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, bool)\n",
" a_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, float)\n",
" weights: np.ndarray = LavaPyType(np.ndarray, float)\n",
"\n",
" def run_spk(self):\n",
" s_in = self.s_in.recv()\n",
" a_out = self.weights[:, s_in].sum(axis=1)\n",
" self.a_out.send(a_out) "
]
},
{
"cell_type": "markdown",
"id": "f40e9bb8",
"metadata": {},
"source": [
"#### Create a LIF neuron _Process_"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "ff1de3df",
"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",
"\n",
"class LIF(AbstractProcess):\n",
" \"\"\"Leaky-Integrate-and-Fire (LIF) neural Process.\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 # neuron voltage\n",
" s_out = v[t] > vth # spike if threshold is exceeded\n",
" v[t] = 0 # reset at spike\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: Neuron bias.\n",
" vth: Neuron threshold voltage, exceeding which, the neuron will spike.\n",
" \"\"\"\n",
" def __init__(self, **kwargs):\n",
" super().__init__(**kwargs)\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",
" 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.vth = Var(shape=(1,), init=vth)"
]
},
{
"cell_type": "markdown",
"id": "978efd0d",
"metadata": {},
"source": [
"#### Create a Python LIF neuron _ProcessModel_ implementing the Loihi Sync Protocol and requiring a CPU compute resource"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "d3a66cf5",
"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\n",
"from lava.magma.core.model.py.model import PyLoihiProcessModel\n",
"\n",
"@implements(proc=LIF, protocol=LoihiProtocol)\n",
"@requires(CPU)\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_mant: 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",
" self.v[:] = self.v * (1 - self.dv) + self.u + self.bias_mant\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",
"id": "1583a5dc",
"metadata": {},
"source": [
"## Create a DenseLayer Hierarchical _Process_ that encompasses Dense and LIF _Process_ behavior"
]
},
{
"cell_type": "markdown",
"id": "894846d3",
"metadata": {},
"source": [
"Now, we create a DenseLayer _Hierarchical Process_ combining LIF neural _Processes_ and Dense connection _Processes_. Our _Hierarchical Process_ contains all of the variables (`u`, `v`, `bias`, `du`, `dv` and `vth`) native to the LIF _Process_ plus the `weights` variable native to the Dense _Process_. The InPort to our _Hierarchical Process_ is `s_in`, which represents the spike inputs to our Dense synaptic connections. These Dense connections synapse onto a population of LIF neurons. The OutPort of our _Hierarchical Process_ is `s_out`, which represents the spikes output by the layer of LIF neurons. We do not have to define the _PortOut_ of the _Dense_ Process nor the _PortIn_ of the _LIF_ Process in the _DenseLayer_ Process, as they are only used internally and won't be exposed."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "7c646865",
"metadata": {},
"outputs": [],
"source": [
"class DenseLayer(AbstractProcess):\n",
" \"\"\"Combines Dense and LIF Processes.\n",
" \"\"\"\n",
" def __init__(self, **kwargs):\n",
" super().__init__(**kwargs)\n",
" shape = kwargs.get(\"shape\", (1, 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",
" weights = kwargs.pop(\"weights\", 0)\n",
"\n",
" self.s_in = InPort(shape=(shape[1],))\n",
" self.s_out = OutPort(shape=(shape[0],))\n",
"\n",
" self.weights = Var(shape=shape, init=weights)\n",
" self.u = Var(shape=(shape[0],), init=0)\n",
" self.v = Var(shape=(shape[0],), init=0)\n",
" self.bias_mant = Var(shape=(shape[0],), init=bias_mant)\n",
" self.du = Var(shape=(1,), init=du)\n",
" self.dv = Var(shape=(1,), init=dv)\n",
" self.vth = Var(shape=(1,), init=vth)"
]
},
{
"cell_type": "markdown",
"id": "476d0b27",
"metadata": {},
"source": [
"## Create a _SubProcessModel_ that implements the DenseLayer _Process_ using Dense and LIF child _Processes_"
]
},
{
"cell_type": "markdown",
"id": "bade89b3",
"metadata": {},
"source": [
"Now, we will create the _SubProcessModel_ that implements our DenseLayer _Process_. This inherits from the _AbstractSubProcessModel_ class. Recall that _SubProcessModels_ also inherit the compute resource requirements from the _ProcessModels_ of their child _Processes_. In this example, we will use the LIF and Dense _ProcessModels_ requiring a CPU compute resource that were defined earlier in the tutorial, and `SubDenseLayerModel` will therefore implicitly require the CPU compute resource. \n",
"\n",
"The `__init__()` constructor of `SubDenseLayerModel` builds the sub _Process_\n",
" structure of the `DenseLayer` _Process_. The `DenseLayer` _Process_ gets\n",
" passed to the `__init__()` method via the `proc` attribute. The `__init__()`\n",
" constructor first instantiates the child LIF and Dense _Processes_. Initial\n",
" conditions of the `DenseLayer` _Process_, which are required to\n",
" instantiate the child LIF and Dense _Processes_, are accessed through\n",
" `proc.proc_params`.\n",
"\n",
"We then `connect()` the in-port of the Dense child _Process_ to the in-port of the `DenseLayer` parent _Process_ and the out-port of the LIF child _Process_ to the out-port of the `DenseLayer` parent _Process_. Note that ports of the `DenseLayer` parent process are accessed using `proc.in_ports` or `proc.out_ports`, while ports of a child _Process_ like LIF are accessed using `self.lif.in_ports` and `self.lif.out_ports`. Our _ProcessModel_ also internally `connect()`s the out-port of the Dense connection child _Process_ to the in-port of the LIF neural child _Process_. \n",
"\n",
"The `alias()` method exposes the variables of the LIF and Dense child _Processes_ to the `DenseLayer` parent _Process_. Note that the variables of the `DenseLayer` parent _Process_ are accessed using `proc.vars`, while the variables of a child _Process_ like LIF are accessed using `self.lif.vars`. Note that unlike a _LeafProcessModel_, a _SubProcessModel_ does not require variables to be initialized with a specified data type or precision. This is because the data types and precisions of all `DenseLayer` _Process_ variables (`proc.vars`) are determined by the particular _ProcessModels_ chosen by the Run Configuration to implement the LIF and Dense child _Processes_. This allows the same _SubProcessModel_ to be used flexibly across multiple languages and compute resources when the child _Processes_ have multiple _ProcessModel_ implementations. _SubProcessModels_ thus enable the composition of complex applications agnostic of platform-specific implementations. In this example, we will implement the LIF and Dense _Processes_ with the _PyLoihiProcessModels_ defined earlier in the tutorial, so the `DenseLayer` variables aliased from LIF and Dense implicity have type `LavaPyType` and precisions as specified in `PyLifModel` and `PyDenseModel`."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "fd97abd6",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"from lava.proc.dense.process import Dense\n",
"from lava.magma.core.model.sub.model import AbstractSubProcessModel\n",
"\n",
"from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol\n",
"from lava.magma.core.decorator import implements\n",
"\n",
"@implements(proc=DenseLayer, protocol=LoihiProtocol)\n",
"class SubDenseLayerModel(AbstractSubProcessModel):\n",
"\n",
" def __init__(self, proc):\n",
" \"\"\"Builds sub Process structure of the Process.\"\"\"\n",
" \n",
" # Instantiate child processes\n",
" # The input shape is a 2D vector (shape of the weight matrix).\n",
" shape = proc.proc_params.get(\"shape\", (1, 1))\n",
" weights = proc.proc_params.get(\"weights\", (1, 1))\n",
" bias_mant = proc.proc_params.get(\"bias_mant\", (1, 1))\n",
" vth = proc.proc_params.get(\"vth\", (1, 1))\n",
"\n",
" self.dense = Dense(weights=weights)\n",
" self.lif = LIF(shape=(shape[0], ), bias_mant=bias_mant, vth=vth)\n",
" \n",
" # Connect the parent InPort to the InPort of the Dense child-Process.\n",
" proc.in_ports.s_in.connect(self.dense.in_ports.s_in)\n",
" \n",
" # Connect the OutPort of the Dense child-Process to the InPort of the\n",
" # LIF child-Process.\n",
" self.dense.out_ports.a_out.connect(self.lif.in_ports.a_in)\n",
" \n",
" # Connect the OutPort of the LIF child-Process to the OutPort of the\n",
" # parent Process.\n",
" self.lif.out_ports.s_out.connect(proc.out_ports.s_out)\n",
"\n",
" proc.vars.u.alias(self.lif.vars.u)\n",
" proc.vars.v.alias(self.lif.vars.v)\n",
" proc.vars.bias_mant.alias(self.lif.vars.bias_mant)\n",
" proc.vars.du.alias(self.lif.vars.du)\n",
" proc.vars.dv.alias(self.lif.vars.dv)\n",
" proc.vars.vth.alias(self.lif.vars.vth)\n",
" proc.vars.weights.alias(self.dense.vars.weights)"
]
},
{
"cell_type": "markdown",
"id": "a75db393",
"metadata": {},
"source": [
"## Run the DenseLayer _Process_"
]
},
{
"cell_type": "markdown",
"id": "96b73f3e",
"metadata": {},
"source": [
"#### Run Connected DenseLayer _Processes_"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "b1db8925",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Layer 1 weights: \n",
" [[0. 0. 0.]\n",
" [0. 1. 0.]\n",
" [0. 0. 0.]] \n",
"\n",
"\n",
" ----- \n",
"\n",
"t: 0\n",
"Layer 0 v: [4. 4. 4.]\n",
"Layer 1 u: [0. 0. 0.]\n",
"Layer 1 v: [4. 4. 4.]\n",
"\n",
" ----- \n",
"\n",
"t: 1\n",
"Layer 0 v: [8. 8. 8.]\n",
"Layer 1 u: [0. 0. 0.]\n",
"Layer 1 v: [8. 8. 8.]\n",
"\n",
" ----- \n",
"\n",
"t: 2\n",
"Layer 0 v: [0. 0. 0.]\n",
"Layer 1 u: [0. 0. 0.]\n",
"Layer 1 v: [0. 0. 0.]\n",
"\n",
" ----- \n",
"\n",
"t: 3\n",
"Layer 0 v: [4. 4. 4.]\n",
"Layer 1 u: [0. 1. 0.]\n",
"Layer 1 v: [4. 5. 4.]\n",
"\n",
" ----- \n",
"\n",
"t: 4\n",
"Layer 0 v: [8. 8. 8.]\n",
"Layer 1 u: [0. 1. 0.]\n",
"Layer 1 v: [8. 0. 8.]\n",
"\n",
" ----- \n",
"\n",
"t: 5\n",
"Layer 0 v: [0. 0. 0.]\n",
"Layer 1 u: [0. 1. 0.]\n",
"Layer 1 v: [0. 5. 0.]\n",
"\n",
" ----- \n",
"\n",
"t: 6\n",
"Layer 0 v: [4. 4. 4.]\n",
"Layer 1 u: [0. 2. 0.]\n",
"Layer 1 v: [4. 0. 4.]\n",
"\n",
" ----- \n",
"\n",
"t: 7\n",
"Layer 0 v: [8. 8. 8.]\n",
"Layer 1 u: [0. 2. 0.]\n",
"Layer 1 v: [8. 6. 8.]\n",
"\n",
" ----- \n",
"\n",
"t: 8\n",
"Layer 0 v: [0. 0. 0.]\n",
"Layer 1 u: [0. 2. 0.]\n",
"Layer 1 v: [0. 0. 0.]\n",
"\n",
" ----- \n",
"\n"
]
}
],
"source": [
"from lava.magma.core.run_configs import RunConfig, Loihi1SimCfg\n",
"from lava.magma.core.run_conditions import RunSteps\n",
"from lava.proc.io import sink, source\n",
"\n",
"dim = (3, 3)\n",
"# Create the weight matrix.\n",
"weights0 = np.zeros(shape=dim)\n",
"weights0[1,1]=1\n",
"weights1 = weights0\n",
"# Instantiate two DenseLayers.\n",
"layer0 = DenseLayer(shape=dim, weights=weights0, bias_mant=4, vth=10)\n",
"layer1 = DenseLayer(shape=dim, weights=weights1, bias_mant=4, vth=10)\n",
"# Connect the first DenseLayer to the second DenseLayer.\n",
"layer0.s_out.connect(layer1.s_in)\n",
"\n",
"print('Layer 1 weights: \\n', layer1.weights.get(),'\\n')\n",
"print('\\n ----- \\n')\n",
"\n",
"rcfg = Loihi1SimCfg(select_tag='floating_pt', select_sub_proc_model=True)\n",
"\n",
"for t in range(9):\n",
" # Run the entire network of Processes.\n",
" layer1.run(condition=RunSteps(num_steps=1), run_cfg=rcfg)\n",
" print('t: ',t)\n",
" print('Layer 0 v: ', layer0.v.get())\n",
" print('Layer 1 u: ', layer1.u.get())\n",
" print('Layer 1 v: ', layer1.v.get())\n",
" #print('Layer 1 spikes: ', layer1.spikes.get())\n",
" print('\\n ----- \\n')\n",
"\n",
"layer1.stop()"
]
},
{
"cell_type": "markdown",
"id": "0d41b968",
"metadata": {},
"source": [
"## How to learn more?\n",
"\n",
"Learn how to access memory from other Processes in the [Remote Memory Access Tutorial](./tutorial07_remote_memory_access.ipynb).\n",
"\n",
"If you want to find out more about _SubProcessModels_, 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/sub/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.8.10 ('lava_nx_env')",
"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"
},
"vscode": {
"interpreter": {
"hash": "7ebb4c32c029abbab1fd16ef4d8ac43152261b56d4033e55d2744ce843ecba08"
}
}
},
"nbformat": 4,
"nbformat_minor": 5
}