Local updates#
Updates in VMC are often local, for instance, flipping a few spins. If the variational wavefunction has specific internal structures, one doesn’t have to recompute the wavefunctions from scratch. Therefore, defining a different computing graph for local updates might greatly accelerate the VMC simulation.
The local update technique has been widely adopted in mean-field fermionic wavefunctions and tensor networks. In this tutorial, we will introduce how to define local updates in your wavefunction.
As a prerequisite, please read Build your network to understand how to build a network in Quantax.
import jax
import jax.numpy as jnp
import equinox as eqx
import quantax as qtx
qtx.set_default_dtype(jnp.float64)
L = 64
lattice = qtx.sites.Chain(L)
RefModel#
Let’s consider a restricted Boltzmann machine (RBM) wavefunction,
In this example, we don’t consider possible overflow of \(\psi\) for simplicity.
class RBM(eqx.Module):
linear: eqx.nn.Linear
def __init__(self, M: int):
key = qtx.get_subkeys()
linear = eqx.nn.Linear(L, M, key=key)
self.linear = qtx.nn.apply_lecun_normal(key, linear)
def __call__(self, s: jax.Array) -> jax.Array:
h = self.linear(s)
return jnp.prod(jnp.cosh(h))
If \(s\) is flipped locally to generate \(s'\), then
Assume the number of hidden units \(M\) is of order \(O(L)\). Then the local update reduces complexity from \(O(L^2)\) to \(O(L)\).
Quantax provides RefModel, a subclass of eqx.Module, for local updates. Here, we construct RBM_Ref as an RBM with local updates.
from typing import Union, Any
from jaxtyping import PyTree
class RBM_Ref(qtx.nn.RefModel):
linear: eqx.nn.Linear
def __init__(self, M: int):
"""The same as usual RBM"""
key = qtx.get_subkeys()
linear = eqx.nn.Linear(L, M, key=key)
self.linear = qtx.nn.apply_lecun_normal(key, linear)
def __call__(self, s: jax.Array) -> jax.Array:
"""The same as usual RBM"""
h = self.linear(s)
return jnp.prod(jnp.cosh(h))
def init_internal(self, s: jax.Array) -> tuple[jax.Array, PyTree]:
"""Compute the initial hidden units for local updates in `ref_forward`."""
h = self.linear(s)
psi = jnp.prod(jnp.cosh(h))
return psi, h
@property
def required_update_modes(self) -> tuple[str, ...]:
"""
The required update modes for accelerated ref_forward pass.
"""
return ("nflips",)
def ref_forward(
self,
s: jax.Array,
s_old: jax.Array,
update_mode: dict[str, Any],
internal: PyTree,
return_update: bool,
) -> jax.Array | tuple[jax.Array, PyTree]:
"""
Forward pass with reference to the old configuration and the number of flipped spins.
This is the core function of local updates.
"""
# A marker indicating that local updates are being compiled.
print("Using local updates ...")
nflips = update_mode["nflips"]
diff = s - s_old
idx_flip = jnp.flatnonzero(diff, size=nflips)
h_diff = self.linear.weight[:, idx_flip] @ diff[idx_flip]
h_new = internal + h_diff
psi = jnp.prod(jnp.cosh(h_new))
if return_update:
return psi, h_new
else:
return psi
Be careful that init_internal and ref_forward will be automatically jitted in Quantax.
In ref_forward, only update_mode and return_update will be treated as static arguments. Therefore, the shapes of arrays shouldn’t depend on other inputs. For instance, jnp.flatnonzero will trigger jit error if size=nflips is not specified.
The correctness of local updates is checked here.
model = RBM_Ref(4 * L)
s_old = qtx.utils.rand_states()
psi0, internal = model.init_internal(s_old)
update_mode = {"nflips": 1}
s_new = s_old.at[0].multiply(-1)
print("Testing direct forward ...")
psi_direct = model(s_new)
print("Direct forward psi: ", psi_direct)
print("Testing local updates ...")
psi_ref = model.ref_forward(s_new, s_old, update_mode, internal, return_update=False)
print("Local updates psi: ", psi_ref)
assert jnp.isclose(psi_direct, psi_ref)
Testing direct forward ...
Direct forward psi: 1.0715160422898089e+39
Testing local updates ...
Using local updates ...
Local updates psi: 1.0715160422898078e+39
VMC with local updates#
To use RefModel in VMC, one needs to wrap it by Variational. Variational with RefModel provides batched and jitted init_internal(), ref_forward(), and ref_forward_with_updates() methods.
state = qtx.state.Variational(model)
s_old = qtx.utils.rand_states(16384)
psi0, internal = state.init_internal(s_old)
update_mode = {"nflips": 1}
s_new = s_old.at[:, 0].multiply(-1)
print("Testing direct forward ...")
psi_direct = state(s_new)
print("Testing local updates ...")
psi_ref = state.ref_forward(s_new, s_old, update_mode, internal)
assert jnp.allclose(psi_direct, psi_ref)
print("Local update test passed!")
Testing direct forward ...
Testing local updates ...
Using local updates ...
Local update test passed!
Then we can test that ref_forward is indeed faster than direct forward pass.
The actual improvement depends on the utilized machine.
%timeit jax.block_until_ready(state(s_new))
%timeit jax.block_until_ready(state.ref_forward(s_new, s_old, update_mode, internal))
The samplers in Quantax automatically utilize local updates whenever possible.
One should ensure that update_mode in the sampler matches required_update_modes in RefModel. Most built-in samplers include "nflips" in their update_mode. For instance, LocalFlip flips 1 spin at a time, so "nflips" is 1.
sampler = qtx.sampler.LocalFlip(state, nsamples=16384)
print(sampler.update_mode)
samples = sampler.sweep()
Using local updates ...
{'nflips': 1}
You might see memory overflow errors if internal stores too many values. To avoid it, please check the documentation of max_parallel in quantax.state.Variational.__init__().
The local updates are also utilized when computing local energies. To turn on local updates, please call apply_update_mode_filter() to reorder the internal data structure of Operator. Here is an example.
H = qtx.operator.Ising(h=1.0)
H.apply_update_mode_filter(qtx.operator.nflips_filter)
Eloc = H.Oloc(state, samples)
Using local updates ...
To disable local updates, one can set use_ref=False when defining Variational. Using this trick, we can check that local energies computed by local updates and direct forward passes are equivalent.
state_direct = qtx.state.Variational(state.model, use_ref=False)
Eloc_direct = H.Oloc(state_direct, samples)
assert jnp.allclose(Eloc, Eloc_direct)
print("Local energy test passed!")
Local energy test passed!