Skip to content

Commit 9d84e0e

Browse files
committed
[Feature] Add max-inflight guard for remote policy clients
ghstack-source-id: 6384e65 Pull-Request: #3897
1 parent 722eb77 commit 9d84e0e

3 files changed

Lines changed: 58 additions & 3 deletions

File tree

test/test_inference_server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ def test_forward_as_tensordict_module(self):
344344
transport,
345345
in_keys=["observation"],
346346
out_keys=["action"],
347+
max_inflight=1,
347348
)
348349
td = TensorDict({"observation": torch.randn(4)})
349350
result = remote_policy(td)

torchrl/collectors/_async_batched.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ class AsyncBatchedCollector(BaseCollector):
196196
policy_version_key (NestedKey or None, optional): TensorDict key used
197197
for behavior-policy version annotations. ``None`` disables
198198
annotations. Defaults to ``"policy_version"``.
199+
max_inflight_per_env (int, optional): maximum unresolved remote-policy
200+
requests per environment coordinator. Defaults to ``1``.
199201
backend (str, optional): global default backend for both
200202
environments and policy inference. Specific overrides
201203
``env_backend`` and ``policy_backend`` take precedence when set.
@@ -291,6 +293,7 @@ def __init__(
291293
device_config: InferenceDeviceConfig | None = None,
292294
policy_version: int = 0,
293295
policy_version_key: NestedKey | None = "policy_version",
296+
max_inflight_per_env: int | None = 1,
294297
server_backend: Literal["thread", "process"] = "thread",
295298
):
296299
if policy is not None and policy_factory is not None:
@@ -420,6 +423,7 @@ def __init__(
420423
policy_version_key=policy_version_key,
421424
)
422425
self._policy_version_key = policy_version_key
426+
self._max_inflight_per_env = max_inflight_per_env
423427

424428
# ---- collector settings -----------------------------------------------
425429
self.requested_frames_per_batch = frames_per_batch
@@ -469,6 +473,7 @@ def _ensure_started(self) -> None:
469473
self._clients = [
470474
PolicyClientModule(
471475
self._transport.client(),
476+
max_inflight=self._max_inflight_per_env,
472477
policy_version_key=self._policy_version_key or "policy_version",
473478
)
474479
for _ in range(self._num_envs)

torchrl/modules/inference_server/_client.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
# LICENSE file in the root directory of this source tree.
55
from __future__ import annotations
66

7+
import threading
78
from collections.abc import Callable, Sequence
9+
from typing import Any
810

911
import torch
1012
from 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+
3058
class 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

Comments
 (0)