2020
2121from 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