Skip to main content

Surrogate gradients

A spike is a Heaviside step, whose derivative is zero everywhere and undefined at threshold. A surrogate gradient replaces that derivative with something usable in the backward pass while keeping the forward pass exactly binary.

Defined as relaxations, not as VJPs

In jaxpike a surrogate is defined by a smooth relaxation of the step, and its gradient comes from autodiff:

class MySurrogate(jp.Surrogate):
slope: float = 10.0

def relaxation(self, v):
return jax.nn.sigmoid(self.slope * v)

__call__ returns the exact binary spike forward and the relaxation's derivative backward, via the straight-through identity soft + stop_gradient(hard - soft).

Two consequences. Defining a surrogate means writing one smooth function and nothing else — no custom VJP, no chance of forward and backward disagreeing. And because the derivative is autodiff-consistent with relaxation, it can be finite-difference checked, which a hand-written surrogate derivative cannot be.

This catches errors that are otherwise invisible. A Triangular relaxation written on jnp.sign returns zero gradient exactly at threshold — the one point where the surrogate must be strongest — because jnp.sign has zero derivative everywhere. The implementation here uses u - u|u|/2, which agrees in value and has the right derivative.

The cost is that the forward pass evaluates the relaxation even though its value is discarded. That is a few elementwise ops.

Built in

All take their parameter as a keyword with the default shown.

SurrogateDerivativeNotes
jp.FastSigmoid(slope=25.0)1 / (1 + slope·abs(v))^2the default; matches snnTorch's fast_sigmoid
jp.ATan(alpha=2.0)(alpha/2) / (1 + (pi·alpha·v/2)^2)matches snnTorch's atan
jp.Triangular(width=1.0)max(0, 1 - abs(v)/width) / widthcompact support, cheapest backward
jp.Boxcar(width=1.0)1/width inside the window, 0 outsidethe straight-through estimator
jp.LIF(tau=20.0, surrogate=jp.ATan(alpha=2.0))

FastSigmoid() is used when you pass nothing.

Choosing one

The choice matters less than the scale. A surrogate with a narrow effective width gives no gradient to neurons sitting far from threshold, which is the same silence problem as a badly initialized network — see Why deep SNNs go silent. If training stalls, widening the surrogate (lower slope, higher width) is worth trying before anything more elaborate.

Boxcar is the most aggressive and the least smooth; ATan and FastSigmoid have long tails, so distant neurons still receive a small gradient.

Where the derivative is evaluated

At the pre-reset membrane, always. The threshold comparison happens before reset, so that is where psi belongs. This is invisible for reset-free neurons, where the two membranes coincide, and it is a large error for LIF: evaluating it post-reset dropped e-prop's LIF gradient alignment from 0.9988 to 0.32. If you are implementing a learning rule that needs psi explicitly, recompute the pre-reset membrane rather than reading the returned state.