Skip to content

Commit a353e7d

Browse files
committed
harden+docs: guard zuko pickle re-link; document transform persistence (#1893)
- ZukoFlow.__setstate__ now fails loudly (RuntimeError) if a prior transform existed before pickling but cannot be recovered from the flow on load (e.g. if a future zuko version changes the internal `Partial.f` structure the re-link relies on), instead of silently degrading to None and dropping device moves. - Note the prior transform's pickle/device behavior and state_dict omission in the build_mdn and ZukoFlow docstrings.
1 parent 6fc2be9 commit a353e7d

2 files changed

Lines changed: 28 additions & 6 deletions

File tree

sbi/neural_nets/estimators/zuko_flow.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ def __init__(
4141
prior_transform: Optional unconstrained prior transform prepended to the
4242
flow (for ``z_score_x="transform_to_unconstrained"``). Kept as a
4343
reference so it follows ``.to()``/``.double()``; its tensors are
44-
buried in a plain attribute that ``nn.Module`` cannot reach.
44+
buried in a plain attribute that ``nn.Module`` cannot reach. It is
45+
preserved by pickling the estimator (the standard sbi save path) but
46+
is not part of ``state_dict()``; reconstruct via the builder before
47+
``load_state_dict()``.
4548
"""
4649

4750
# assert len(condition_shape) == 1, "Zuko Flows require 1D conditions."
@@ -61,18 +64,34 @@ def __getstate__(self):
6164
# `_prior_transform` is the same inverse-transform object the flow holds
6265
# (inside a CallableTransform). Torch would drop its data on pickling, so
6366
# don't pickle this redundant reference — the CallableTransform pickles the
64-
# transform correctly, and we re-link to it on load (see __setstate__).
67+
# transform correctly, and we re-link to it on load (see __setstate__). Keep
68+
# a flag so __setstate__ can fail loudly if the re-link ever comes up empty.
6569
state = dict(super().__getstate__())
66-
if self._prior_transform is not None:
67-
state["_prior_transform"] = None
70+
state["_had_prior_transform"] = self._prior_transform is not None
71+
state["_prior_transform"] = None
6872
return state
6973

7074
def __setstate__(self, state):
75+
had_prior_transform = state.pop("_had_prior_transform", False)
7176
super().__setstate__(state)
7277
self._prior_transform = self._find_prior_transform()
78+
if had_prior_transform and self._prior_transform is None:
79+
raise RuntimeError(
80+
"ZukoFlow lost its prior transform while unpickling: it could not "
81+
"be recovered from the flow. This likely means the installed `zuko` "
82+
"version changed the flow's internal structure (the transform is "
83+
"located via `zuko`'s `Partial.f` attribute in "
84+
"`_find_prior_transform`)."
85+
)
7386

7487
def _find_prior_transform(self) -> Optional[TorchTransform]:
75-
"""Recover the prior transform from the flow's CallableTransform, if any."""
88+
"""Recover the prior transform from the flow's CallableTransform, if any.
89+
90+
Assumes at most one `CallableTransform` in the flow (true today: only the
91+
single `transform_to_unconstrained` prior transform is wrapped this way).
92+
Returns the first match; revisit if `_prepare_x_transforms` ever composes
93+
multiple `CallableTransform`s.
94+
"""
7695
for module in self.net.modules():
7796
wrapped = getattr(module, "f", None)
7897
if isinstance(wrapped, CallableTransform):

sbi/neural_nets/net_builders/mdn.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ def build_mdn(
4444
over the entire batch, instead of per-dimension. Should be used when each
4545
sample is, for example, a time series or an image.
4646
- `transform_to_unconstrained`: transform inputs to unconstrained space
47-
using the prior's support. Requires `x_dist` to be provided.
47+
using the prior's support. Requires `x_dist` to be provided. This
48+
transform follows `.to()`/`.cuda()`/`.double()` and is preserved by
49+
pickling the estimator (the standard sbi save path), but is not part of
50+
`state_dict()`; reconstruct via the builder before `load_state_dict()`.
4851
z_score_y: Whether to z-score ys passing into the network, same options as
4952
z_score_x.
5053
hidden_features: Number of hidden features.

0 commit comments

Comments
 (0)