Skip to content

Commit 898cee0

Browse files
committed
[Refactor] Add structured inference server config objects
ghstack-source-id: 9f2dd45 Pull-Request: #3893
1 parent ab9cea4 commit 898cee0

7 files changed

Lines changed: 549 additions & 72 deletions

File tree

benchmarks/bench_collectors.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
TransformedEnv,
4040
)
4141
from torchrl.modules import ConvNet, MLP
42+
from torchrl.modules.inference_server import (
43+
InferenceDeviceConfig,
44+
InferenceServerConfig,
45+
)
4246

4347
OBS_SHAPE = (3, 84, 84)
4448
ACTION_DIM = 6
@@ -577,13 +581,17 @@ def main() -> None:
577581
policy=policy_factory(),
578582
frames_per_batch=args.frames_per_batch,
579583
total_frames=-1,
580-
max_batch_size=max_batch_size,
581-
min_batch_size=min_batch_size,
582-
server_timeout=timeout,
583584
env_backend="threading",
584-
server_backend="thread",
585-
policy_device=policy_device,
586-
output_device=output_device,
585+
server_config=InferenceServerConfig(
586+
backend="thread",
587+
max_batch_size=max_batch_size,
588+
min_batch_size=min_batch_size,
589+
timeout=timeout,
590+
),
591+
device_config=InferenceDeviceConfig(
592+
policy_device=policy_device,
593+
output_device=output_device,
594+
),
587595
),
588596
env_name=args.env,
589597
num_envs=num_envs,
@@ -614,13 +622,17 @@ def main() -> None:
614622
policy=policy_factory(),
615623
frames_per_batch=args.frames_per_batch,
616624
total_frames=-1,
617-
max_batch_size=max_batch_size,
618-
min_batch_size=min_batch_size,
619-
server_timeout=timeout,
620625
env_backend="multiprocessing",
621-
server_backend="thread",
622-
policy_device=policy_device,
623-
output_device=output_device,
626+
server_config=InferenceServerConfig(
627+
backend="thread",
628+
max_batch_size=max_batch_size,
629+
min_batch_size=min_batch_size,
630+
timeout=timeout,
631+
),
632+
device_config=InferenceDeviceConfig(
633+
policy_device=policy_device,
634+
output_device=output_device,
635+
),
624636
),
625637
env_name=args.env,
626638
num_envs=num_envs,
@@ -651,13 +663,17 @@ def main() -> None:
651663
policy_factory=policy_factory,
652664
frames_per_batch=args.frames_per_batch,
653665
total_frames=-1,
654-
max_batch_size=max_batch_size,
655-
min_batch_size=min_batch_size,
656-
server_timeout=timeout,
657666
env_backend="multiprocessing",
658-
server_backend="process",
659-
policy_device=policy_device,
660-
output_device=output_device,
667+
server_config=InferenceServerConfig(
668+
backend="process",
669+
max_batch_size=max_batch_size,
670+
min_batch_size=min_batch_size,
671+
timeout=timeout,
672+
),
673+
device_config=InferenceDeviceConfig(
674+
policy_device=policy_device,
675+
output_device=output_device,
676+
),
661677
),
662678
env_name=args.env,
663679
num_envs=num_envs,

docs/source/reference/modules_inference_server.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ Core API
1717
:template: rl_template_noinherit.rst
1818

1919
InferenceServer
20+
InferenceServerConfig
21+
InferenceDeviceConfig
2022
ProcessInferenceServer
2123
InferenceClient
2224
InferenceTransport
@@ -67,6 +69,46 @@ threads in the same process:
6769
6870
server.shutdown()
6971
72+
Structured Configuration
73+
^^^^^^^^^^^^^^^^^^^^^^^^
74+
75+
Server execution, batching, and device placement are grouped into two
76+
dataclasses instead of loose keyword arguments: :class:`InferenceServerConfig`
77+
collects the execution ``backend`` (``"thread"`` or ``"process"``) and the
78+
batching/instrumentation knobs (``max_batch_size``, ``min_batch_size``,
79+
``timeout``, ``collect_stats``, ``stats_window_size``), and
80+
:class:`InferenceDeviceConfig` describes device placement across the
81+
collection pipeline (``policy_device``, ``output_device``, ``env_device``,
82+
``storing_device``). Both :class:`InferenceServer` and
83+
:class:`~torchrl.collectors.AsyncBatchedCollector` accept them through the
84+
``server_config`` and ``device_config`` keyword arguments; a config object is
85+
mutually exclusive with the individual keyword arguments it replaces, and the
86+
config objects are the only way to set the per-role devices and the server
87+
backend on the collector. Servers consume only the
88+
``policy_device``/``output_device`` fields (``env_device`` doubles as an
89+
``output_device`` fallback), while ``env_device`` and ``storing_device``
90+
drive the collector-side transfers:
91+
92+
.. code-block:: python
93+
94+
from torchrl.collectors import AsyncBatchedCollector
95+
from torchrl.modules.inference_server import (
96+
InferenceDeviceConfig,
97+
InferenceServerConfig,
98+
)
99+
100+
collector = AsyncBatchedCollector(
101+
create_env_fn=[make_env] * 8,
102+
policy=my_policy,
103+
frames_per_batch=200,
104+
server_config=InferenceServerConfig(max_batch_size=8, timeout=0.005),
105+
device_config=InferenceDeviceConfig(
106+
policy_device="cuda:0",
107+
env_device="cpu",
108+
storing_device="cpu",
109+
),
110+
)
111+
70112
Weight Synchronisation
71113
^^^^^^^^^^^^^^^^^^^^^^
72114

test/test_inference_server.py

Lines changed: 112 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020

2121
from torchrl.modules.inference_server import (
2222
InferenceClient,
23+
InferenceDeviceConfig,
2324
InferenceServer,
25+
InferenceServerConfig,
2426
InferenceTransport,
2527
MPTransport,
2628
ProcessInferenceServer,
@@ -283,6 +285,88 @@ def test_stats_accounting(self):
283285
assert stats["avg_batch_size"] > 0
284286
assert stats["p95_forward_ms"] >= 0
285287

288+
def test_structured_config(self):
289+
transport = ThreadingTransport()
290+
policy = _make_policy()
291+
server_config = InferenceServerConfig(max_batch_size=2, timeout=0.001)
292+
device_config = InferenceDeviceConfig(policy_device="cpu", output_device="cpu")
293+
with InferenceServer(
294+
policy,
295+
transport,
296+
server_config=server_config,
297+
device_config=device_config,
298+
) as server:
299+
# The config values must actually land on the server
300+
assert server.max_batch_size == 2
301+
assert server.timeout == 0.001
302+
assert server.policy_device == torch.device("cpu")
303+
assert server.output_device == torch.device("cpu")
304+
client = transport.client()
305+
result = client(TensorDict({"observation": torch.randn(4)}))
306+
stats = server.stats()
307+
assert result["action"].device.type == "cpu"
308+
assert stats["requests"] == 1
309+
310+
def test_server_config_exclusive_even_at_default_values(self):
311+
"""Passing an explicit kwarg equal to the default still raises."""
312+
transport = ThreadingTransport()
313+
policy = _make_policy()
314+
with pytest.raises(ValueError, match="mutually exclusive"):
315+
InferenceServer(
316+
policy,
317+
transport,
318+
max_batch_size=64,
319+
server_config=InferenceServerConfig(max_batch_size=8),
320+
)
321+
with pytest.raises(ValueError, match="mutually exclusive"):
322+
InferenceServer(
323+
policy,
324+
transport,
325+
device="cpu",
326+
device_config=InferenceDeviceConfig(policy_device="cpu"),
327+
)
328+
329+
def test_device_config_env_device_fallback_and_storing_device_rejected(self):
330+
config = InferenceDeviceConfig(env_device="cpu")
331+
assert config.server_output_device() == torch.device("cpu")
332+
config = InferenceDeviceConfig(env_device="cpu", output_device="meta")
333+
assert config.server_output_device() == torch.device("meta")
334+
transport = ThreadingTransport()
335+
policy = _make_policy()
336+
with pytest.raises(ValueError, match="storing_device is a collector-level"):
337+
InferenceServer(
338+
policy,
339+
transport,
340+
device_config=InferenceDeviceConfig(storing_device="cpu"),
341+
)
342+
343+
@pytest.mark.gpu
344+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="needs CUDA")
345+
def test_device_config_cuda_roundtrip(self):
346+
"""device_config must actually drive the policy and output moves."""
347+
transport = ThreadingTransport()
348+
policy = _make_policy()
349+
seen_devices = []
350+
inner_forward = policy.module.forward
351+
352+
def recording_forward(x):
353+
seen_devices.append(x.device)
354+
return inner_forward(x)
355+
356+
policy.module.forward = recording_forward
357+
with InferenceServer(
358+
policy,
359+
transport,
360+
device_config=InferenceDeviceConfig(
361+
policy_device="cuda:0", output_device="cpu"
362+
),
363+
):
364+
client = transport.client()
365+
result = client(TensorDict({"observation": torch.randn(4)}, device="cpu"))
366+
assert result["action"].device.type == "cpu"
367+
assert all(device.type == "cuda" for device in seen_devices)
368+
assert next(policy.parameters()).device.type == "cuda"
369+
286370
@pytest.mark.gpu
287371
@pytest.mark.skipif(not torch.cuda.is_available(), reason="needs CUDA")
288372
def test_cuda_policy_cpu_output(self):
@@ -1041,9 +1125,8 @@ def test_process_server_backend_smoke(self):
10411125
policy_factory=_make_counting_policy,
10421126
frames_per_batch=10,
10431127
total_frames=20,
1044-
max_batch_size=2,
10451128
env_backend="threading",
1046-
server_backend="process",
1129+
server_config=InferenceServerConfig(backend="process", max_batch_size=2),
10471130
)
10481131
total = 0
10491132
for batch in collector:
@@ -1052,13 +1135,8 @@ def test_process_server_backend_smoke(self):
10521135
assert total >= 20
10531136

10541137
def test_invalid_server_backend_raises(self):
1055-
with pytest.raises(ValueError, match="server_backend"):
1056-
AsyncBatchedCollector(
1057-
create_env_fn=[_counting_env_factory] * 2,
1058-
policy_factory=_make_counting_policy,
1059-
frames_per_batch=10,
1060-
server_backend="not-a-backend",
1061-
)
1138+
with pytest.raises(ValueError, match="backend"):
1139+
InferenceServerConfig(backend="not-a-backend")
10621140

10631141
def test_server_death_raises_instead_of_hanging(self):
10641142
"""Killing the server process surfaces an error in the iterator."""
@@ -1067,9 +1145,8 @@ def test_server_death_raises_instead_of_hanging(self):
10671145
policy_factory=_make_counting_policy,
10681146
frames_per_batch=10,
10691147
total_frames=-1,
1070-
max_batch_size=2,
10711148
env_backend="threading",
1072-
server_backend="process",
1149+
server_config=InferenceServerConfig(backend="process", max_batch_size=2),
10731150
)
10741151
try:
10751152
iterator = iter(collector)
@@ -1083,6 +1160,30 @@ def test_server_death_raises_instead_of_hanging(self):
10831160
finally:
10841161
collector.shutdown(timeout=0.5)
10851162

1163+
def test_device_config_and_server_config(self):
1164+
"""Collector accepts structured device and server config objects."""
1165+
collector = AsyncBatchedCollector(
1166+
create_env_fn=[_counting_env_factory] * 2,
1167+
policy=_make_counting_policy(),
1168+
frames_per_batch=10,
1169+
total_frames=20,
1170+
server_config=InferenceServerConfig(max_batch_size=2),
1171+
device_config=InferenceDeviceConfig(
1172+
policy_device="cpu",
1173+
output_device="cpu",
1174+
env_device="cpu",
1175+
storing_device="cpu",
1176+
),
1177+
)
1178+
total = 0
1179+
for batch in collector:
1180+
assert batch.device is None or batch.device.type == "cpu"
1181+
total += batch.numel()
1182+
stats = collector.server_stats()
1183+
collector.shutdown()
1184+
assert total >= 20
1185+
assert stats["requests"] > 0
1186+
10861187

10871188
# =============================================================================
10881189
# Tests: SlotTransport

0 commit comments

Comments
 (0)