Copyright (C) 2021 Intel Corporation SPDX-License-Identifier: BSD-3-Clause See: https://spdx.org/licenses/


ProcessModels

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.

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.

733aefe532cd435980a924dcda64cad3

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.

Create a LIF Process

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.) Here the LIF neural Process accepts activity from synaptic inputs via InPort a_in and outputs spiking activity via OutPort s_out.

[1]:
from lava.magma.core.process.process import AbstractProcess
from lava.magma.core.process.variable import Var
from lava.magma.core.process.ports.ports import InPort, OutPort

class LIF(AbstractProcess):
    """Leaky-Integrate-and-Fire (LIF) neural Process.

    LIF dynamics abstracts to:
    u[t] = u[t-1] * (1-du) + a_in                              # neuron current
    v[t] = v[t-1] * (1-dv) + u[t] + bias_mant * 2 ** bias_exp  # neuron voltage
    s_out = v[t] > vth                                         # spike if threshold is exceeded
    v[t] = 0                                                   # reset at spike

    Parameters
    ----------
    du: Inverse of decay time-constant for current decay.
    dv: Inverse of decay time-constant for voltage decay.
    bias_mant: Mantissa part of neuron bias.
    bias_exp: Exponent part of neuron bias, if needed. Mostly for fixed point
              implementations. Unnecessary for floating point
              implementations. If specified, bias = bias_mant * 2**bias_exp.
    vth: Neuron threshold voltage, exceeding which, the neuron will spike.
    """
    def __init__(self, **kwargs):
        super().__init__()
        shape = kwargs.get("shape", (1,))
        du = kwargs.pop("du", 0)
        dv = kwargs.pop("dv", 0)
        bias_mant = kwargs.pop("bias_mant", 0)
        bias_exp = kwargs.pop("bias_exp", 0)
        vth = kwargs.pop("vth", 10)

        self.shape = shape
        self.a_in = InPort(shape=shape)
        self.s_out = OutPort(shape=shape)
        self.u = Var(shape=shape, init=0)
        self.v = Var(shape=shape, init=0)
        self.du = Var(shape=(1,), init=du)
        self.dv = Var(shape=(1,), init=dv)
        self.bias_mant = Var(shape=shape, init=bias_mant)
        self.bias_exp = Var(shape=shape, init=bias_exp)
        self.vth = Var(shape=(1,), init=vth)

Create a Python LeafProcessModel that implements the LIF Process

Now, we will create a Python ProcessModel, or PyProcessModel, that runs on a CPU compute resource and implements the LIF Process behavior.

Setup

We begin by importing the required Lava classes. 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.

[2]:
import numpy as np
from lava.magma.core.decorator import implements, requires
from lava.magma.core.resources import CPU
from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol

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.

[3]:
from lava.magma.core.model.py.model import PyLoihiProcessModel
from lava.magma.core.model.py.ports import PyInPort, PyOutPort
from lava.magma.core.model.py.type import LavaPyType

Defining a PyLifModel for LIF

We now define a LeafProcessModel PyLifModel that implements the behavior of the LIF Process.

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.

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.

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.

[4]:
import numpy as np
from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol
from lava.magma.core.model.py.ports import PyInPort, PyOutPort
from lava.magma.core.model.py.type import LavaPyType
from lava.magma.core.resources import CPU
from lava.magma.core.decorator import implements, requires, tag
from lava.magma.core.model.py.model import PyLoihiProcessModel

@implements(proc=LIF, protocol=LoihiProtocol)
@requires(CPU)
@tag('floating_pt')
class PyLifModel1(PyLoihiProcessModel):
    a_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, float)
    s_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, bool, precision=1)
    u: np.ndarray = LavaPyType(np.ndarray, float)
    v: np.ndarray = LavaPyType(np.ndarray, float)
    bias_mant: np.ndarray = LavaPyType(np.ndarray, float)
    bias_exp: np.ndarray = LavaPyType(np.ndarray, float)
    du: float = LavaPyType(float, float)
    dv: float = LavaPyType(float, float)
    vth: float = LavaPyType(float, float)

    def run_spk(self):
        a_in_data = self.a_in.recv()
        self.u[:] = self.u * (1 - self.du)
        self.u[:] += a_in_data
        bias = self.bias_mant * (2 ** self.bias_exp)
        self.v[:] = self.v * (1 - self.dv) + self.u + bias
        s_out = self.v >= self.vth
        self.v[s_out] = 0  # Reset voltage to 0
        self.s_out.send(s_out)

Compile and run PyLifModel

[5]:
from lava.magma.core.run_configs import Loihi1SimCfg
from lava.magma.core.run_conditions import RunSteps

lif = LIF(shape=(3,), du=0, dv=0, bias_mant=3, vth=10)

run_cfg = Loihi1SimCfg()
lif.run(condition=RunSteps(num_steps=10), run_cfg=run_cfg)
print(lif.v.get())
[6. 6. 6.]

Selecting 1 ProcessModel: More on LeafProcessModel attributes and relations

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.

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.

e6abca0ce1ff47dcb731394951ee8255

How to learn more?

Learn how to execute single Processes and networks of Processes in the next tutorial.

If you want to find out more about ProcessModels, have a look at the Lava documentation or dive into the source code.

To receive regular updates on the latest developments and releases of the Lava Software Framework please subscribe to our newsletter.