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