44# LICENSE file in the root directory of this source tree.
55from __future__ import annotations
66
7+ import threading
78from collections .abc import Callable , Sequence
9+ from typing import Any
810
911import torch
1012from tensordict .base import TensorDictBase
@@ -27,6 +29,32 @@ def result(self, timeout: float | None = None) -> TensorDictBase:
2729 return self ._result
2830
2931
32+ class _ReleaseOnResultFuture :
33+ def __init__ (self , future , release : Callable [[], None ]):
34+ self ._future = future
35+ self ._release = release
36+ self ._released = False
37+ self ._lock = threading .Lock ()
38+
39+ def _release_once (self ) -> None :
40+ with self ._lock :
41+ if not self ._released :
42+ self ._released = True
43+ self ._release ()
44+
45+ def done (self ) -> bool :
46+ return self ._future .done ()
47+
48+ def result (self , timeout : float | None = None ) -> TensorDictBase :
49+ try :
50+ return self ._future .result (timeout = timeout )
51+ finally :
52+ self ._release_once ()
53+
54+ def __getattr__ (self , name : str ) -> Any :
55+ return getattr (self ._future , name )
56+
57+
3058class PolicyClientModule (TensorDictModuleBase ):
3159 """TensorDict policy wrapper for remote inference-server clients.
3260
@@ -45,6 +73,8 @@ class PolicyClientModule(TensorDictModuleBase):
4573 module. The full input TensorDict is still sent to the server.
4674 out_keys (sequence of NestedKey, optional): output keys advertised by
4775 the module.
76+ max_inflight (int, optional): maximum number of unresolved asynchronous
77+ requests submitted through this module. ``None`` means unbounded.
4878 target_policy_version (int, optional): expected latest policy version
4979 used for bounded-staleness checks.
5080 max_policy_lag (int, optional): maximum allowed
@@ -83,6 +113,7 @@ def __init__(
83113 * ,
84114 in_keys : Sequence [NestedKey ] | None = None ,
85115 out_keys : Sequence [NestedKey ] | None = None ,
116+ max_inflight : int | None = None ,
86117 target_policy_version : int | None = None ,
87118 max_policy_lag : int | None = None ,
88119 policy_version_key : NestedKey = "policy_version" ,
@@ -93,9 +124,21 @@ def __init__(
93124 self .client = client
94125 self .in_keys = list (in_keys or [])
95126 self .out_keys = list (out_keys or [])
127+ self .max_inflight = max_inflight
96128 self .target_policy_version = target_policy_version
97129 self .max_policy_lag = max_policy_lag
98130 self .policy_version_key = policy_version_key
131+ self ._inflight_sem = (
132+ threading .BoundedSemaphore (max_inflight )
133+ if max_inflight is not None
134+ else None
135+ )
136+
137+ def _acquire_inflight (self ) -> Callable [[], None ]:
138+ if self ._inflight_sem is None :
139+ return lambda : None
140+ self ._inflight_sem .acquire ()
141+ return self ._inflight_sem .release
99142
100143 def _check_policy_lag (self , tensordict : TensorDictBase ) -> None :
101144 if self .target_policy_version is None or self .max_policy_lag is None :
@@ -125,14 +168,20 @@ def submit(self, tensordict: TensorDictBase):
125168 Returns:
126169 Future-like object whose ``result()`` method returns a TensorDict.
127170 """
171+ release = self ._acquire_inflight ()
128172 submit = getattr (self .client , "submit" , None )
129173 if submit is None :
130174 try :
131175 result = self .client (tensordict )
132- return _ImmediateFuture (result )
176+ return _ReleaseOnResultFuture ( _ImmediateFuture (result ), release )
133177 except BaseException as exc :
134- return _ImmediateFuture (exc )
135- return submit (tensordict )
178+ return _ReleaseOnResultFuture (_ImmediateFuture (exc ), release )
179+ try :
180+ future = submit (tensordict )
181+ except BaseException :
182+ release ()
183+ raise
184+ return _ReleaseOnResultFuture (future , release )
136185
137186 def forward (self , tensordict : TensorDictBase ) -> TensorDictBase :
138187 result = self .submit (tensordict ).result ()
0 commit comments