Skip to content

Commit 3dc5fac

Browse files
Now running tree_check when initialising a Module
1 parent 339611d commit 3dc5fac

4 files changed

Lines changed: 37 additions & 6 deletions

File tree

equinox/_module.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ._caches import internal_lru_caches
1515
from ._doc_utils import doc_repr
1616
from ._pretty_print import tree_pformat
17-
from ._tree import tree_equal
17+
from ._tree import tree_check_internal, tree_equal
1818

1919

2020
_P = ParamSpec("_P")
@@ -110,6 +110,11 @@ def _not_magic(k: str) -> bool:
110110

111111

112112
_has_dataclass_init = weakref.WeakKeyDictionary()
113+
_has_been_checked = weakref.WeakValueDictionary()
114+
115+
116+
def _skip(node):
117+
return isinstance(node, Module) and node is _has_been_checked.get(id(node), None)
113118

114119

115120
# Inherits from ABCMeta as a convenience for a common use-case.
@@ -201,6 +206,10 @@ def __call__(cls, *args, **kwargs):
201206
else:
202207
setattr(self, field.name, converter(getattr(self, field.name)))
203208
object.__setattr__(self, "__class__", cls)
209+
# Note that this only runs during the initial creation, and not during
210+
# unflattening.
211+
tree_check_internal(self, _skip)
212+
_has_been_checked[id(self)] = self
204213
return self
205214

206215

equinox/_tree.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,14 @@ def is_leaf(node):
338338
return jtu.tree_flatten(pytree, is_leaf=is_leaf)
339339

340340

341+
def tree_check_internal(pytree, skip) -> None:
342+
"""As `tree_check`, but can skips checking some nodes (typically those that have
343+
alread been checked).
344+
"""
345+
all_nodes = {}
346+
_tree_check(pytree, all_nodes, skip)
347+
348+
341349
def tree_check(pytree: Any) -> None:
342350
"""Checks if the PyTree is well-formed: does it have no self-references, and does
343351
it have no duplicate layers.
@@ -389,13 +397,13 @@ def tree_check(pytree: Any) -> None:
389397
A `ValueError` if the PyTree is not well-formed.
390398
"""
391399
all_nodes = {}
392-
_tree_check(pytree, all_nodes)
400+
_tree_check(pytree, all_nodes, skip=lambda _: False)
393401

394402

395403
_leaf_treedef = jtu.tree_structure(0)
396404

397405

398-
def _tree_check(node, all_nodes):
406+
def _tree_check(node, all_nodes, skip):
399407
subnodes, treedef = tree_flatten_one_level(node)
400408
# We allow duplicate leaves and empty containers, so don't raise an error with those
401409
if treedef != _leaf_treedef and treedef.num_leaves > 0:
@@ -424,5 +432,6 @@ def _tree_check(node, all_nodes):
424432
type_string = "<unknown type>"
425433
all_nodes[id(node)] = (True, type_string)
426434
for subnode in subnodes:
427-
_tree_check(subnode, all_nodes)
435+
if not skip(subnode):
436+
_tree_check(subnode, all_nodes, skip)
428437
all_nodes[id(node)] = (False, type_string)

tests/test_module.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import functools as ft
2+
import gc
23
from typing import Any
34

45
import jax
@@ -252,3 +253,15 @@ def test_wrapped(getkey):
252253
y = eqx.filter_vmap(eqx.nn.Linear(2, 2, key=getkey()))
253254
x, y = eqx.filter((x, y), eqx.is_array)
254255
jtu.tree_map(lambda x, y: x + y, x, y)
256+
257+
258+
def test_tree_check_cache(getkey):
259+
gc.collect()
260+
has_been_checked = eqx._module._has_been_checked
261+
num_checked = len(has_been_checked)
262+
mlp = eqx.nn.MLP(2, 2, 2, 2, key=getkey())
263+
# +4: one for `MLP`, and three for its `Linear` layers inside.
264+
assert len(has_been_checked) == num_checked + 4
265+
del mlp
266+
gc.collect()
267+
assert len(has_been_checked) == num_checked

tests/test_tree.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,9 @@ def __init__(self, test=lambda x: 2 * x) -> None:
231231
def _transform(self, x):
232232
return self.test(x)
233233

234-
a = SubComponent()
235234
with pytest.raises(ValueError):
236-
eqx.tree_check(a)
235+
# Checking occurs when initialising the module.
236+
SubComponent()
237237

238238

239239
def test_tree_check_none():

0 commit comments

Comments
 (0)