Skip to content

Commit 58e2282

Browse files
authored
Add SGLRW (#150)
1 parent 4c5a40d commit 58e2282

6 files changed

Lines changed: 204 additions & 0 deletions

File tree

docs/api/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ thermostat (SGNHT) algorithm from [Ding et al, 2014](https://proceedings.neurips
3636
(SGHMC with adaptive friction coefficient).
3737
- [`sgmcmc.baoa`](sgmcmc/baoa.md) implements the BAOA integrator for SGHMC
3838
from [Leimkuhler and Matthews, 2015 - p271](https://link.springer.com/book/10.1007/978-3-319-16375-8).
39+
- [`sgmcmc.sglrw`](sgmcmc/sglrw.md) implements the stochastic gradient lattice random
40+
walk (SGLRW) algorithm from [Mensch et al, 2026](https://arxiv.org/abs/2602.15925).
3941

4042
For an overview and unifying framework for SGMCMC methods, see [Ma et al, 2015](https://arxiv.org/abs/1506.04696).
4143

docs/api/sgmcmc/sglrw.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# SGLRW
2+
3+
::: posteriors.sgmcmc.sglrw

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ nav:
8282
- api/sgmcmc/sghmc.md
8383
- api/sgmcmc/sgnht.md
8484
- api/sgmcmc/baoa.md
85+
- api/sgmcmc/sglrw.md
8586
- VI:
8687
- Dense: api/vi/dense.md
8788
- Diag: api/vi/diag.md

posteriors/sgmcmc/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from posteriors.sgmcmc import sghmc
33
from posteriors.sgmcmc import sgnht
44
from posteriors.sgmcmc import baoa
5+
from posteriors.sgmcmc import sglrw

posteriors/sgmcmc/sglrw.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# type: ignore posteriors is not typed
2+
from typing import Any
3+
from functools import partial
4+
import torch
5+
from torch import Tensor
6+
from torch.func import grad_and_value
7+
from tensordict import TensorClass
8+
9+
from posteriors.types import TensorTree, Transform, LogProbFn, Schedule
10+
from posteriors.tree_utils import flexi_tree_map, tree_insert_
11+
from posteriors.utils import CatchAuxError
12+
13+
14+
def build(
15+
log_posterior: LogProbFn,
16+
lr: float | Schedule,
17+
temperature: float | Schedule = 1.0,
18+
) -> Transform:
19+
"""Builds SGLRW transform - Stochastic Gradient Lattice Random Walk.
20+
21+
Algorithm from [Mensch et al, 2026](https://arxiv.org/abs/2602.15925)
22+
adapted from [Duffield et al, 2025](https://arxiv.org/abs/2508.20883):
23+
$$
24+
θ_{t+1} = θ_t + δx Δ(θₜ, t)
25+
$$
26+
where $δx = √(lr * 2 * T)$ is a spatial stepsize and $Δ(θₜ, t)$ is a random
27+
binary valued vector defined in the paper.
28+
29+
Targets $p_T(θ) \\propto \\exp( \\log p(θ) / T)$ with temperature $T$,
30+
as it discretizes the overdamped Langevin SDE:
31+
$$
32+
dθ = ∇ log p_T(θ) dt + √(2 T) dW
33+
$$
34+
35+
The log posterior and temperature are recommended to be [constructed in tandem](../../log_posteriors.md)
36+
to ensure robust scaling for a large amount of data and variable batch size.
37+
38+
Args:
39+
log_posterior: Function that takes parameters and input batch and
40+
returns the log posterior value (which can be unnormalised)
41+
as well as auxiliary information, e.g. from the model call.
42+
lr: Learning rate,
43+
scalar or schedule (callable taking step index, returning scalar).
44+
temperature: Temperature of the sampling distribution.
45+
Scalar or schedule (callable taking step index, returning scalar).
46+
47+
Returns:
48+
SGLRW transform (posteriors.types.Transform instance).
49+
"""
50+
update_fn = partial(
51+
update,
52+
log_posterior=log_posterior,
53+
lr=lr,
54+
temperature=temperature,
55+
)
56+
return Transform(init, update_fn)
57+
58+
59+
class SGLRWState(TensorClass["frozen"]):
60+
"""State encoding params for SG-LRW (binary).
61+
62+
Attributes:
63+
params: Parameters.
64+
log_posterior: Last log posterior evaluation.
65+
step: Current step count.
66+
"""
67+
68+
params: TensorTree
69+
log_posterior: Tensor = torch.tensor(torch.nan)
70+
step: Tensor = torch.tensor(0)
71+
72+
73+
def init(params: TensorTree) -> SGLRWState:
74+
"""Initialise SG-LRW."""
75+
return SGLRWState(params)
76+
77+
78+
def update(
79+
state: SGLRWState,
80+
batch: Any,
81+
log_posterior: LogProbFn,
82+
lr: float | Schedule,
83+
temperature: float | Schedule = 1.0,
84+
inplace: bool = False,
85+
) -> tuple[SGLRWState, TensorTree]:
86+
with torch.no_grad(), CatchAuxError():
87+
grads, (log_post, aux) = grad_and_value(log_posterior, has_aux=True)(
88+
state.params, batch
89+
)
90+
91+
# Resolve schedules
92+
lr_val = lr(state.step) if callable(lr) else lr
93+
T_val = temperature(state.step) if callable(temperature) else temperature
94+
lr_val = torch.as_tensor(
95+
lr_val, dtype=state.params.dtype, device=state.params.device
96+
)
97+
T_val = torch.as_tensor(T_val, dtype=state.params.dtype, device=state.params.device)
98+
99+
# Spatial stepsize to make update binary
100+
diffusion_val = torch.sqrt(2.0 * T_val)
101+
delta_x = torch.sqrt(lr_val) * diffusion_val
102+
103+
# Per-parameter binary LRW transform
104+
def transform_params(p, g):
105+
p_plus = ternary_probs(g, diffusion_val, lr_val, delta_x)[:, 2]
106+
107+
u = torch.rand_like(p_plus)
108+
step_sign = torch.where(
109+
u < p_plus, torch.ones_like(p_plus), -torch.ones_like(p_plus)
110+
)
111+
step = delta_x * step_sign
112+
return p + step
113+
114+
params = flexi_tree_map(transform_params, state.params, grads, inplace=inplace)
115+
116+
if inplace:
117+
tree_insert_(state.log_posterior, log_post.detach())
118+
tree_insert_(state.step, state.step + 1)
119+
return state, aux
120+
return SGLRWState(params, log_post.detach(), state.step + 1), aux
121+
122+
123+
def ternary_probs(
124+
drift_val: Tensor,
125+
diffusion_val: Tensor,
126+
stepsize: Tensor,
127+
delta_x: Tensor,
128+
) -> Tensor:
129+
"""
130+
Generate the probabilities for the ternary update
131+
from the discretization parameters.
132+
133+
Args:
134+
drift_val: Evaluation of the Drift function.
135+
diffusion_val: Evaluation of the Diffusion function.
136+
stepsize: Temporal stepsize value.
137+
delta_x: Spatial stepsize value.
138+
139+
Returns:
140+
Update probabilities as a tensor, with last axis being [p_minus, p_zero, p_plus].
141+
"""
142+
desired_mean = stepsize * drift_val
143+
desired_var = stepsize * diffusion_val**2
144+
scaled_mean = desired_mean / delta_x
145+
scaled_var = desired_var / delta_x**2
146+
147+
# Ensure p_minus + p_plus <= 1
148+
scaled_var = torch.clamp(scaled_var, 0.0, 1.0)
149+
150+
# Ensure positive probs
151+
scaled_mean = torch.clamp(scaled_mean, -scaled_var, scaled_var)
152+
153+
# Clip probs for numerical stability
154+
p_plus = torch.clamp(0.5 * (scaled_var + scaled_mean), 0.0, 1.0)
155+
p_minus = torch.clamp(0.5 * (scaled_var - scaled_mean), 0.0, 1.0)
156+
p_zero = torch.clamp(1 - p_plus - p_minus, 0.0, 1.0)
157+
158+
return torch.stack([p_minus, p_zero, p_plus], dim=-1)

tests/sgmcmc/test_sglrw.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from functools import partial
2+
import torch
3+
from posteriors.sgmcmc import sglrw
4+
from tests.scenarios import get_multivariate_normal_log_prob
5+
from tests.utils import verify_inplace_update
6+
from tests.sgmcmc.utils import run_test_sgmcmc_gaussian
7+
8+
9+
def test_sglrw():
10+
torch.manual_seed(42)
11+
12+
# Set inference parameters
13+
lr = 1e-2
14+
15+
# Run MCMC test on Gaussian
16+
run_test_sgmcmc_gaussian(
17+
partial(sglrw.build, lr=lr),
18+
)
19+
20+
21+
def test_sglrw_inplace_step():
22+
torch.manual_seed(42)
23+
24+
# Load log posterior
25+
dim = 5
26+
log_prob, _ = get_multivariate_normal_log_prob(dim)
27+
28+
# Set inference parameters
29+
def lr(step):
30+
return 1e-2 * (step + 1) ** -0.33
31+
32+
# Build transform
33+
transform = sglrw.build(log_prob, lr)
34+
35+
# Initialise
36+
params = torch.randn(dim)
37+
38+
# Verify inplace update
39+
verify_inplace_update(transform, params, None)

0 commit comments

Comments
 (0)