Skip to content

Commit eab74af

Browse files
committed
fix(nets): improve VAE reparameterize memory efficiency and revert stray safeeval changes
- Avoid allocating a tensor of ones at inference by returning mu + std directly when use_mean_at_inference is disabled, and mu + eps * std only during training. - Revert unintended monai/utils/safeeval.py and tests/utils/test_safe_eval.py changes that were accidentally included in this branch. Signed-off-by: Lanre Shittu <136805224+Shizoqua@users.noreply.github.com>
1 parent a0b471d commit eab74af

4 files changed

Lines changed: 48 additions & 12 deletions

File tree

monai/networks/nets/fullyconnectednet.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,10 @@ def reparameterize(self, mu: torch.Tensor, logvar: torch.Tensor) -> torch.Tensor
195195
# term is only added during training (the reparameterization trick).
196196
return mu
197197
std = torch.exp(0.5 * logvar)
198-
eps = torch.randn_like(std) if self.training else torch.ones_like(std)
199-
return mu + eps * std
198+
if self.training:
199+
eps = torch.randn_like(std)
200+
return mu + eps * std
201+
return mu + std
200202

201203
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
202204
mu, logvar = self.encode_forward(x)

monai/networks/nets/varautoencoder.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,10 @@ def reparameterize(self, mu: torch.Tensor, logvar: torch.Tensor) -> torch.Tensor
165165
# term is only added during training (the reparameterization trick).
166166
return mu
167167
std = torch.exp(0.5 * logvar)
168-
eps = torch.randn_like(std) if self.training else torch.ones_like(std)
169-
return mu + eps * std
168+
if self.training:
169+
eps = torch.randn_like(std)
170+
return mu + eps * std
171+
return mu + std
170172

171173
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
172174
mu, logvar = self.encode_forward(x)

monai/utils/safeeval.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,12 @@ def __init__(self, int_type_str: str, float_type_str: str):
4747
self.float_type_str = float_type_str
4848

4949
def visit_Constant(self, node):
50-
if isinstance(node.value, (int, float)):
51-
type_str = self.int_type_str if isinstance(node.value, int) else self.float_type_str
52-
return ast.parse(f"{type_str}({node.value})")
53-
54-
return node
50+
if isinstance(node.value, bool) or not isinstance(node.value, (int, float)):
51+
return node
52+
type_str = self.int_type_str if isinstance(node.value, int) else self.float_type_str
53+
func_node = ast.parse(type_str, mode="eval").body
54+
call_node = ast.Call(func=func_node, args=[ast.Constant(value=node.value)], keywords=[])
55+
return ast.copy_location(call_node, node)
5556

5657

5758
def safe_eval(
@@ -74,7 +75,7 @@ def safe_eval(
7475
by `int_type_str` and `float_type_str`. These are expected to be constructor names prefixed with `np.` as Numpy
7576
will be present in the expression global variables under that name. The values can be changed to other types if
7677
needed, such as "int64". One advantage of doing this is to avoid denial-of-service attacks by attempting to evaluate
77-
an expressoini which is incredibly slow under native Python but fast (though potentially erroneous) under Numpy.
78+
an expression which is incredibly slow under native Python but fast (though potentially erroneous) under Numpy.
7879
7980
Args:
8081
expr: expression to evaluate, this will be stripped before parsing to avoid indentation complaints
@@ -101,6 +102,7 @@ def safe_eval(
101102

102103
if rewrite_np:
103104
parsed = _RewriteConstNp(int_type_str, float_type_str).visit(parsed)
104-
locals_vars = {"np": np, **(locals_vars or {})}
105+
ast.fix_missing_locations(parsed)
106+
locals_vars = {**(locals_vars or {}), "np": np}
105107

106-
return eval(expr, dict(globals_vars) if globals_vars else None, locals_vars)
108+
return eval(compile(parsed, "<safe_eval>", "eval"), dict(globals_vars) if globals_vars else None, locals_vars)

tests/utils/test_safe_eval.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import ast
1515
import unittest
1616

17+
import numpy as np
1718
from parameterized import parameterized
1819

1920
from monai.utils import safe_eval
@@ -62,6 +63,35 @@ def test_allowed_types(self):
6263
with self.assertRaises(ValueError):
6364
safe_eval("1*2", allowed_types=allowed)
6465

66+
def test_rewrite_np_produces_numpy_types(self):
67+
"""Test that rewrite_np wraps literals in numpy types."""
68+
result = safe_eval("2 + 3", rewrite_np=True)
69+
self.assertIsInstance(result, np.integer)
70+
71+
result = safe_eval("2.5 + 1.5", rewrite_np=True)
72+
self.assertIsInstance(result, np.floating)
73+
74+
def test_rewrite_np_large_exponent(self):
75+
"""Test that rewrite_np prevents slow native-Python exponentiation."""
76+
# Under native Python, 9**9**9 produces a ~369-million-digit integer;
77+
# under np.int32 it overflows and completes almost instantly.
78+
result = safe_eval("9**9**9", rewrite_np=True)
79+
self.assertIsInstance(result, np.integer)
80+
81+
def test_rewrite_np_preserves_bool(self):
82+
"""Test that rewrite_np does not wrap bool constants."""
83+
result = safe_eval("True", rewrite_np=True)
84+
self.assertIs(result, True)
85+
86+
result = safe_eval("False", rewrite_np=True)
87+
self.assertIs(result, False)
88+
89+
def test_rewrite_np_inf_constant(self):
90+
"""Test that rewrite_np handles overflowing infinity literals."""
91+
result = safe_eval("1e309", rewrite_np=True)
92+
self.assertIsInstance(result, np.floating)
93+
self.assertTrue(np.isinf(result))
94+
6595

6696
if __name__ == "__main__":
6797
unittest.main()

0 commit comments

Comments
 (0)