TensorPlay
Reference guides
latest (dev)
Copy
View Markdown

Latest development documentation · Updated 2026-10-15

Spiking neural networks

tensorplay.snn provides the primitives for building and training spiking neural networks (SNNs). The neuron dynamics run in fused native CPU/CUDA operators (snn_lif_step / snn_lif_step_backward for a single step and snn_lif_seq / snn_lif_seq_backward for whole spike trains); the Python layer wires them into the autograd engine and the nn.Module API.

import tensorplay as tp
from tensorplay import snn

lif = snn.LIFNode(threshold=1.0, decay=0.5)
x = tp.rand(4, 16)
spike = lif(x)          # single-step: [4, 16] binary spikes
lif.reset()

seq = snn.LIFNode(step_mode="m")
out = seq(tp.rand(8, 4, 16))   # multi-step: [8, 4, 16] spike train

Neuron models

All neuron models share one membrane dynamic per step:

h     = decay * v + x
spike = heaviside(h - threshold)
v_new = h - threshold * spike             (soft reset)
v_new = h * (1 - spike) + v_reset * spike (hard reset)

Module

Description

IFNode

Integrate-and-fire, decay = 1

LIFNode

Leaky integrate-and-fire, decay in [0, 1]

ParametricLIFNode

LIF with a learnable per-neuron decay parameter

ALIFNode

LIF with an adaptive threshold (a term added to the base threshold)

SpikeLinear

Linear layer intended for binary spike-train inputs

Each neuron module keeps its membrane potential in the buffer v, supports step_mode="s" (single step) and step_mode="m" (spike trains of shape [T, ...]), and exposes reset() to clear the state. LIFNode and its subclasses accept:

  • learnable_threshold=True – registers threshold as a trainable Parameter instead of a plain number.

  • return_v_seq=True – in step_mode="m", forward returns (spikes, v_seq) where v_seq is the per-step voltage trajectory of shape [T, ...].

ALIFNode adds the parameters base_threshold and decay_th (the leak of the adaptation variable). Its effective threshold is a + base_threshold, with a = decay_th * a + v after each step, so the threshold rises after strong activity and decays back to the base value. Because the effective threshold changes every step, ALIFNode runs the native single-step operator in a tight loop for multi-step inputs.

Surrogate gradients

The spike step is the Heaviside function, whose derivative is zero almost everywhere. Surrogate-gradient training replaces that derivative with a smooth approximation during backward:

Surrogate

Backward weight

StraightThroughSurrogate

1

SigmoidSurrogate(alpha)

alpha * sigmoid(alpha*x) * (1 - sigmoid(alpha*x))

FastSigmoidSurrogate(alpha)

`alpha / (1 + alpha*

PiecewiseLinearSurrogate(alpha)

1 inside (-alpha, alpha), else 0

ATanSurrogate(alpha)

alpha / (1 + (pi*alpha*x)^2)

ErfSurrogate(alpha)

alpha / sqrt(pi) * exp(-(alpha*x)^2)

PiecewiseQuadraticSurrogate(alpha)

`-alpha^2*

PiecewiseExpSurrogate(alpha)

`alpha/2 * exp(-alpha*

SuperSpikeSurrogate(alpha)

`alpha / (1 +

NonzeroSignLogAbsSurrogate(alpha)

`alpha / (1 + alpha*

PiecewiseLeakyRELUSurrogate(w, c)

1/(2w) inside `

LeakyKRELUSurrogate(leak, k)

k for x >= 0, leak for x < 0

S2NNSurrogate(alpha, beta)

sigmoid gradient for x < 0, beta/(x+1) else

QPseudoSpikeSurrogate(alpha)

`(1 + 2/(alpha-1)*

SquarewaveFourierSeriesSurrogate(n, T)

4/T * sum_i cos((2i-1)*2*pi/T*x)

The fused LIF kernels support the single-parameter families (ids 0-9); the multi-parameter surrogates (ids 10-14) are available through snn.spike() and snn.surrogate_grad().

detach_reset=True treats the spike in the voltage-update path as a constant during backward, which is the standard stabilizing choice.

Functional primitives

  • snn.heaviside(x) – the spike step.

  • snn.spike(x, surrogate) – Heaviside forward, surrogate backward.

  • snn.surrogate_grad(x, surrogate, alpha, beta, w, c, n, T_period) – evaluate a surrogate gradient (all 15 surrogate ids).

  • snn.lif_step(x, v, decay, threshold, v_reset, surrogate, detach_reset) – one fused neuron update, returns (spike, v_new).

  • snn.lif_seq(x, v_init, decay, threshold, v_reset, surrogate, detach_reset, return_v_seq) – a fused multi-step update on [T, B, ...] input, returns (spikes, v_final) (or (spikes, v_seq, v_final) with return_v_seq=True). The time loop runs inside one native kernel, so multi-step training (BPTT) does not pay a per-step launch overhead. The returned v_seq is differentiable: its gradient is injected into the per-step voltage back-propagation.

  • snn.spike_rate(spikes, dim) – average firing rate along the time axis.

  • snn.spike_rate_loss(spikes, target_rate, reduction) – mean-squared-error between the firing rate and a target rate.

Native implementation

The neuron dynamics are implemented as fused CPU/CUDA kernels. The single- step operators (snn_lif_step / snn_lif_step_backward) process every element in one pass (CPU: OpenMP parallel_for; CUDA: grid-stride threads) and support float32, float64, float16 and bfloat16 inputs, with per-neuron or scalar decay / threshold / v_reset broadcasting. The multi-step operators (snn_lif_seq / snn_lif_seq_backward) iterate the membrane dynamics over T inside the same kernel, so the whole spike-train forward/backward is a single kernel launch per device. The backward recomputes the membrane trajectory from the stored spikes (no full forward state is saved) and back-propagates the voltage gradient across time (BPTT), so decay, threshold, v_init and every input frame receive gradients.

v_reset selects the reset mode: None for the soft reset (subtract the threshold) and a scalar or broadcastable tensor for the hard reset (reset to that voltage). detach_reset controls whether the spike inside the voltage-update path is treated as a constant during backward; the default True is the standard stabilizing choice.

A throughput benchmark is provided in benchmark/snn_benchmark.py; it compares the fused multi-step operator with a Python time loop over snn.lif_step and reports spikes per second on both CPU and CUDA.

Spike encoders

  • snn.rate_encode(x, T) – Bernoulli spikes over T steps.

  • snn.poisson_encode(x, T) – Poisson-process encoding (same draw).

  • snn.temporal_encode(x, T, tau) – time-to-first-spike encoding.

  • snn.PeriodicEncoder(encoding) – repeats a fixed periodic spike pattern.

  • snn.GaussianTuningCurveEncoder(mu, sigma) – population encoding where each input feature drives a bank of Gaussian-tuned Bernoulli neurons.

  • snn.LatencyEncoder(T, enc_function) – one-hot time-to-first-spike encoder with linear or logarithmic firing-time map.

  • snn.WeightedPhaseEncoder(T) – binary-decomposition weighted-phase encoding.

Deep spiking layers

  • snn.SeqToANNContainer(module) – apply any ANN layer to every time step of a [T, B, ...] input.

  • snn.SpikingConv2d(...) – Conv2d (applied per time step) followed by a multi-step LIFNode, for [T, B, C, H, W] spike trains.

  • snn.SpikingBatchNorm1d / snn.SpikingBatchNorm2d – batch normalization with the time axis folded into the batch dimension, the standard stabilizer for deep SNN training.

Monitoring

snn.SpikeMonitor(module) attaches a forward hook to any nn.Module, records every spike tensor the module returns, and exposes spike_rate(), firing_rate(dim), total_spikes() and last_spike(). Call reset() to clear the records and detach() to remove the hook.

Training example

A complete convolutional spiking network (SpikingConv2d + SpikingBatchNorm2d + max pooling + rate readout) with a full surrogate-gradient BPTT training loop:

import tensorplay as tp
from tensorplay import snn

class ConvSpikingNetwork(tp.nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = snn.SpikingConv2d(1, 32, 3, padding=1)
        self.bn1 = snn.SpikingBatchNorm2d(32)
        self.pool1 = snn.SeqToANNContainer(tp.nn.MaxPool2d(2))
        self.conv2 = snn.SpikingConv2d(32, 64, 3, padding=1)
        self.bn2 = snn.SpikingBatchNorm2d(64)
        self.pool2 = snn.SeqToANNContainer(tp.nn.MaxPool2d(2))
        self.fc = snn.SpikeLinear(64 * 7 * 7, 10)
        self.readout = snn.LIFNode(step_mode="m")

    def forward(self, x):          # x: [T, B, 1, 28, 28] Poisson spikes
        x = self.pool1(self.bn1(self.conv1(x)))   # [T, B, 32, 14, 14]
        x = self.pool2(self.bn2(self.conv2(x)))   # [T, B, 64, 7, 7]
        x = x.reshape(x.shape[0], x.shape[1], -1)  # [T, B, 64*7*7]
        return self.readout(self.fc(x)).mean(0)    # [B, 10] firing rates

    def reset(self):
        for m in self.modules():
            if m is not self and hasattr(m, "reset"):
                m.reset()

# Full training loop (MNIST):
model = ConvSpikingNetwork()
opt = tp.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
T = 8
for epoch in range(epochs):
    for x, y in dataloader:          # x: [B, 1, 28, 28] uint8, y: [B] labels
        spikes = snn.poisson_encode(x.to(tp.float32) / 255.0, T=T)
        out = model(spikes)                            # [B, 10]
        target = tp.nn.functional.one_hot(y, 10).to(tp.float32)
        loss = ((out - target) ** 2).mean()
        opt.zero_grad()
        loss.backward()            # surrogate-gradient BPTT
        opt.step()
        model.reset()              # clear membrane potentials per batch

On this page

Ask DeepWiki