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


Processes

Learn how to create Processes, the fundamental computational units used in Lava to build algorithms and applications.

What is a Process?

This tutorial will show how to create a Process that simulates a group of leaky integrate-and-fire neurons. But in Lava, the concept of Processes applies widely beyond this example. In general, a Process describes an individual program unit which encapsulates

  1. data that store its state,

  2. algorithms that describe how to manipulate the data,

  3. ports that share data with other Processes, and

  4. an API that facilitates user interaction.

A Process can thus be as simple as a single neuron or a synapse, as complex as a full neural network, and as non-neuromorphic as a streaming interface for a peripheral device or an executed instance of regular program code.

c2f3256ac7fb440ebd6d6f3cb1e915ac

Processes are independent from each other as they primarily operate on their own local memory while they pass messages between each other via channels. Different Processes thus proceed their computations simultaneously and asynchronously, mirroring the high parallelism inherent in neuromorphic hardware. The parallel Processes are furthermore safe against side effects from shared-memory interaction.

Once a Process has been coded in Python, Lava allows to run it seamlessly across different backends such as a CPU, a GPU, or neuromorphic cores. Developers can thus easily test and benchmark their applications on classical computing hardware and then deploy it to neuromorphic hardware. Furthermore, Lava takes advantage of distributed, heterogeneous hardware such as Loihi as it can run some Processes on neuromorphic cores and in parallel others on embedded conventional CPUs and GPUs.

While Lava provides a growing library of Processes, you can easily write your own processes that suit your needs.

How to build a Process?

Overall architecture

All Processes in Lava share a universal architecture as they inherit from the same AbstractProcess class. Each Process consists of the following four key components. 3b401e71d31d4d7a8a022fd83dfadf36

AbstractProcess: Defining Vars, Ports, and the API

When you create your own new process, you need to inherit from the AbstractProcess class. As an example, we will implement the class LIF, a group of leaky integrate-and-fire (LIF) neurons.

2cb88b532c8a4a689eb8ba50ec1ad44f

Component

Name

Python

Ports

a_{in}

Inport

Receives spikes from upstream neurons.

s_{out}

Outport

Transmits spikes to downstream neurons.

State

u

Var

Synaptic current of the LIF neurons.

v

Var

Membrane voltage of the LIF neurons.

du

Var

A time constant describing the current leakage.

dv

Var

A time constant describing the voltage leakage.

bias

Var

A bias value.

vth

Var

A constant threshold that the membrane voltage needs to exceed for a spike.

API

All Vars

Var

All public Vars are considered part of the Process API.

All Ports

AbstractPort

All Ports are considered part of the Process API.

print_vars

def

A function that prints all internal variables to help the user see if the LIF neuron has correctly been set up.

The following code implements the class LIF that you can also find in Lava’s Process library, but extends it by an additional API method that prints the state of the LIF neurons.

[1]:
import numpy as np

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 neural process with activation input and spike
    output ports a_in and s_out.
    """
    def __init__(self, **kwargs):
        super().__init__()
        shape = kwargs.get("shape", (1,))
        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=kwargs.pop("du", 0))
        self.dv = Var(shape=(1,), init=kwargs.pop("dv", 0))
        self.bias = Var(shape=shape, init=kwargs.pop("bias", 0))
        self.vth = Var(shape=(1,), init=kwargs.pop("vth", 10))

    def print_vars(self):
        """Prints all variables of a LIF process and their values."""

        sp = 3 * "  "
        print("Variables of the LIF:")
        print(sp + "u:    {}".format(str(self.u.get())))
        print(sp + "v:    {}".format(str(self.v.get())))
        print(sp + "du:   {}".format(str(self.du.get())))
        print(sp + "dv:   {}".format(str(self.dv.get())))
        print(sp + "bias: {}".format(str(self.bias.get())))
        print(sp + "vth:  {}".format(str(self.vth.get())))

You may have noticed that most of the Vars were initialized by scalar integers. But the synaptic current u illustrates that Vars can in general be initialized with numeric objects that have a dimensionality equal or less than specified by its shape argument. The initial value will be scaled up to match the Var dimension at run time.

There are two further important things to notice about the Process class:

  1. It only defines the interface of the LIF neuron, but not its temporal behavior.

  2. It is fully agnostic to the computing backend and will thus remain the same if you want to run your code, for example, once on a CPU and once on Loihi.

ProcessModel: Defining the behavior of a Process

The behavior of a Process is defined by its ProcessModel. For the specific example of LIF neuron, the ProcessModel describes how their current and voltage react to a synaptic input, how these states evolve with time, and when the neurons should emit a spike.

A single Process can have several ProcessModels, one for each backend that you want to run it on.

The following code implements a ProcessModel that defines how a CPU should run the LIF Process. Please do not worry about the precise implementation here—the code will be explained in detail in the next Tutorial on ProcessModels.

[2]:
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 PyLifModel(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: 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
        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)

Instantiating the Process

Now we can create an instance of our Process, in this case a group of 3 LIF neurons.

[3]:
n_neurons = 3

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

Interacting with Processes

Once you have instantiated a group of LIF neurons, you can easily interact with them.

Accessing Vars

You can always read out the current values of the process Vars to determine the Process state. For example, all three neurons should have been initialized with a zero membrane voltage.

[4]:
print(lif.v.get())
0

As described above, the Var v has in this example been initialized as a scalar value that describes the membrane voltage of all three neurons simultaneously.

Using custom APIs

To facilitate how users can interact with your Process, they can use the custom APIs that you provide them with. For LIF neurons, you defined a custom function that allows the user to inspect the internal Vars of the LIF Process. Have a look if all Vars have been set up correctly.

[5]:
lif.print_vars()
Variables of the LIF:
      u:    0
      v:    0
      du:   0
      dv:   0
      bias: 3
      vth:  10

Executing a Process

Once the Process is instantiated and you are satisfied with its state, you can run the Process. As long as a ProcessModel has been defined for the desired backend, the Process can run seamlessly across computing hardware. Do not worry about the details here—you will learn all about how Lava builds, compiles, and runs Processes in a separate tutorial.

To run a Process, specify the number of steps to run for and select the desired backend.

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

lif.run(condition=RunSteps(num_steps=1), run_cfg=Loihi1SimCfg())

The voltage of each LIF neuron should now have increased by the bias value, 3, from their initial values of 0. Check if the neurons have evolved as expected.

[7]:
print(lif.v.get())
[3. 3. 3.]

Update Vars

You can furthermore update the internal Vars manually. You may, for example, set the membrane voltage to new values between two runs.

[8]:
lif.v.set(np.array([1, 2, 3]) )
print(lif.v.get())
[1. 2. 3.]

Note that the set() method becomes available once the Process has been run. Prior to the first run, use the __init__ function of the Process to set Vars.

Later tutorials will illustrate more sophisticated ways to access, store, and change variables during run time using Process code.

In the end, stop the process to terminate its execution.

[9]:
lif.stop()

How to learn more?

Learn how to implement the behavior of Processes in the next tutorial on ProcessModels.

If you want to find out more about Processes, 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 the INRC newsletter.