Skip to content

Commit 9a3e006

Browse files
authored
[Feature] add key-level skip_existing (#1771)
1 parent a1a061e commit 9a3e006

2 files changed

Lines changed: 54 additions & 10 deletions

File tree

tensordict/nn/utils.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from typing import Any, Callable
1313

1414
import torch
15-
from tensordict.utils import _ContextManager, strtobool
15+
from tensordict._nestedkey import NestedKey
16+
from tensordict.utils import _ContextManager, strtobool, unravel_key_list
1617
from torch import nn
1718

1819
from torch.utils._contextlib import _DecoratorContextManager
@@ -159,17 +160,21 @@ class set_skip_existing(_DecoratorContextManager):
159160
will check the global value and execute the code accordingly.
160161
161162
When used as a method decorator, it will check the tensordict input keys
162-
and if the ``skip_existing()`` call returns ``True``, it will skip the method
163-
if all the output keys are already present.
163+
and skip the method if the current mode applies to all output keys and they
164+
are already present.
164165
This not not expected to be used as a decorator for methods that do not
165166
respect the following signature: ``def fun(self, tensordict, *args, **kwargs)``.
166167
167168
Args:
168-
mode (bool, optional):
169+
mode (bool, list of NestedKey, optional):
169170
If ``True``, it indicates that existing entries in the graph
170171
won't be overwritten, unless they are only partially present. :func:`~.skip_existing`
171172
will return ``True``.
172173
If ``False``, no check will be performed.
174+
If a list of keys, only modules whose output keys are all in the
175+
list can be skipped. The keys follow TensorDict's nested-key
176+
conventions. If any output key is not in the list, the module runs
177+
and recomputes all of its outputs.
173178
If ``None``, the value of :func:`~.skip_existing` will not be
174179
changed. This is intended to be used exclusively for decorating
175180
methods and allow their behaviour to depend on the same class
@@ -269,9 +274,12 @@ class set_skip_existing(_DecoratorContextManager):
269274
"""
270275

271276
def __init__(
272-
self, mode: bool | None = True, in_key_attr="in_keys", out_key_attr="out_keys"
277+
self,
278+
mode: bool | list[NestedKey] | None = True,
279+
in_key_attr="in_keys",
280+
out_key_attr="out_keys",
273281
):
274-
self.mode = mode
282+
self.mode = unravel_key_list(mode) if isinstance(mode, list) else mode
275283
self.in_key_attr = in_key_attr
276284
self.out_key_attr = out_key_attr
277285
self._called = False
@@ -303,8 +311,10 @@ def wrapper(_self, tensordict, *args: Any, **kwargs: Any) -> Any:
303311
in_keys = getattr(_self, self.in_key_attr)
304312
out_keys = getattr(_self, self.out_key_attr)
305313
# we use skip_existing to allow users to override the mode internally
314+
skip_mode = skip_existing()
306315
if (
307-
skip_existing()
316+
skip_mode
317+
and (skip_mode is True or all(key in skip_mode for key in out_keys))
308318
and all(key in tensordict.keys(True) for key in out_keys)
309319
and not any(key in out_keys for key in in_keys)
310320
):
@@ -359,8 +369,10 @@ def wrapper(_self, tensordict, *args: Any, **kwargs: Any) -> Any:
359369
in_keys = getattr(_self, self.in_key_attr)
360370
out_keys = getattr(_self, self.out_key_attr)
361371
# we use skip_existing to allow users to override the mode internally
372+
skip_mode = skip_existing()
362373
if (
363-
skip_existing()
374+
skip_mode
375+
and (skip_mode is True or all(key in skip_mode for key in out_keys))
364376
and all(key in tensordict.keys(True) for key in out_keys)
365377
and not any(key in out_keys for key in in_keys)
366378
):
@@ -386,8 +398,8 @@ def clone(self) -> _set_skip_existing_None:
386398
return out
387399

388400

389-
def skip_existing():
390-
"""Returns whether or not existing entries in a tensordict should be re-computed by a module."""
401+
def skip_existing() -> bool | list[NestedKey]:
402+
"""Returns which existing entries should not be re-computed by a module."""
391403
return _skip_existing.get_mode()
392404

393405

test/nn/test_nn.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,6 +1939,12 @@ def forward(self, tensordict):
19391939
with set_skip_existing(True):
19401940
td = module(TensorDict({"out": torch.zeros(())}, [])) # no print
19411941
assert (td["out"] == 0).all()
1942+
with set_skip_existing(["other"]):
1943+
td = module(TensorDict({"out": torch.zeros(())}, []))
1944+
assert (td["out"] == 1).all()
1945+
with set_skip_existing(["out"]):
1946+
td = module(TensorDict({"out": torch.zeros(())}, []))
1947+
assert (td["out"] == 0).all()
19421948
td = module(TensorDict({"out": torch.zeros(())}, []))
19431949
assert (td["out"] == 1).all()
19441950

@@ -2007,6 +2013,32 @@ def forward(self, tensordict):
20072013
module(td)
20082014
assert (td["out"] == 1).all()
20092015

2016+
def test_selected_keys(self):
2017+
value_module = TensorDictModule(
2018+
lambda value: value + 1, in_keys=["input"], out_keys=["value"]
2019+
)
2020+
memory_module = TensorDictModule(
2021+
lambda memory: memory + 1,
2022+
in_keys=["memory"],
2023+
out_keys=[("next", "memory")],
2024+
)
2025+
module = TensorDictSequential(value_module, memory_module)
2026+
td = TensorDict(
2027+
{
2028+
"input": torch.zeros(()),
2029+
"value": torch.full((), 10),
2030+
"memory": torch.zeros(()),
2031+
"next": {"memory": torch.full((), 10)},
2032+
},
2033+
[],
2034+
)
2035+
2036+
with set_skip_existing(["value"]):
2037+
module(td)
2038+
2039+
assert td["value"].item() == 10
2040+
assert td["next", "memory"].item() == 1
2041+
20102042

20112043
@pytest.mark.parametrize("out_d_key", [("d", "e"), ["d"], ["d", "e"]])
20122044
@pytest.mark.parametrize("unpack", [True, False])

0 commit comments

Comments
 (0)