Skip to content

Commit 260fb1f

Browse files
committed
fix: only invert inverse prior transforms in MDN pickle (#1893 review)
The MDN __getstate__ stored self._prior_transform.inv unconditionally, which is correct for the inverse transform mcmc_transform produces but silently corrupts a concrete forward transform (as the __init__ contract allows): inverting it wraps it in an inverse whose tensors torch then drops on pickle. Guard on _InverseTransform so concrete transforms pickle as-is. Adds a regression test.
1 parent a957902 commit 260fb1f

2 files changed

Lines changed: 39 additions & 6 deletions

File tree

sbi/neural_nets/estimators/mixture_density_estimator.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import numpy as np
1818
import torch
1919
from torch import Tensor, nn
20+
from torch.distributions.transforms import _InverseTransform
2021
from torch.nn import functional as F
2122

2223
from sbi.neural_nets.estimators.base import ConditionalDensityEstimator
@@ -413,17 +414,20 @@ def _apply(self, fn):
413414
return self
414415

415416
def __getstate__(self):
416-
# torch drops an inverse transform's tensors on pickling; store the forward
417-
# and re-invert on load.
418417
state = dict(super().__getstate__())
419-
if self._prior_transform is not None:
420-
state["_prior_transform"] = self._prior_transform.inv
418+
t = self._prior_transform
419+
# torch drops an inverse transform's data (its `_inv`) on pickling; stash the
420+
# wrapped forward and rebuild on load. Concrete transforms pickle fine as-is.
421+
if isinstance(t, _InverseTransform):
422+
state["_prior_transform"] = None
423+
state["_prior_transform_forward"] = t.inv
421424
return state
422425

423426
def __setstate__(self, state):
427+
forward = state.pop("_prior_transform_forward", None)
424428
super().__setstate__(state)
425-
if self._prior_transform is not None:
426-
self._prior_transform = self._prior_transform.inv
429+
if forward is not None:
430+
self._prior_transform = forward.inv
427431

428432
@property
429433
def embedding_net(self) -> nn.Module:

tests/save_and_load_test.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,35 @@ def test_mdn_transform_to_unconstrained_picklable():
9494
assert torch.allclose(expected, actual)
9595

9696

97+
def test_mdn_pickle_preserves_concrete_prior_transform():
98+
"""A concrete (non-inverse) prior transform on an MDN survives pickling.
99+
100+
The `_inv`-dropping workaround only applies to inverse transforms. A concrete
101+
forward transform (e.g. a user-supplied AffineTransform, as the __init__ contract
102+
allows) must be pickled as-is; inverting it would wrap it in an inverse whose data
103+
torch then drops on pickle.
104+
"""
105+
from torch.distributions.transforms import AffineTransform
106+
107+
from sbi.neural_nets.net_builders.mdn import build_mdn
108+
from sbi.utils import BoxUniform
109+
110+
prior = BoxUniform(-2 * torch.ones(2), 2 * torch.ones(2))
111+
bx, by = prior.sample((256,)), torch.randn(256, 3)
112+
est = build_mdn(bx, by)
113+
# A concrete forward transform (maps constrained -> unconstrained), not the
114+
# inverse wrapper that mcmc_transform produces.
115+
est._prior_transform = AffineTransform(torch.zeros(2), 2 * torch.ones(2))
116+
117+
theta, cond = prior.sample((5,)).unsqueeze(1), torch.randn(1, 3)
118+
expected = est.log_prob(theta, cond)
119+
120+
reloaded = pickle.loads(pickle.dumps(est))
121+
actual = reloaded.log_prob(theta, cond)
122+
123+
assert torch.allclose(expected, actual)
124+
125+
97126
def test_zuko_transform_to_unconstrained_picklable():
98127
"""A Zuko flow using transform_to_unconstrained survives a pickle round-trip.
99128

0 commit comments

Comments
 (0)