Skip to content

Commit a309c43

Browse files
committed
[Feature] Add async VLA GRPO collection modes
ghstack-source-id: af32c10 Pull-Request: #3909
1 parent 5e9435f commit a309c43

8 files changed

Lines changed: 680 additions & 33 deletions

File tree

sota-implementations/vla_grpo/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,32 @@ throughput split into collection and optimization:
204204
- `throughput/train_decisions_per_s`
205205
- `throughput/optim_steps_per_s`
206206

207+
The collector can also be switched between synchronous and async execution
208+
paths for throughput experiments:
209+
210+
```bash
211+
# fully synchronous baseline
212+
python sota-implementations/vla_grpo/vla-grpo.py \
213+
collector.async_env=false collector.async_policy=false
214+
215+
# asynchronous env slots, but no policy auto-batching
216+
python sota-implementations/vla_grpo/vla-grpo.py \
217+
collector.async_env=true collector.async_policy=false env.num_envs=8
218+
219+
# asynchronous env slots plus auto-batched policy inference
220+
python sota-implementations/vla_grpo/vla-grpo.py \
221+
collector.async_env=true collector.async_policy=true env.num_envs=8 \
222+
collector.server_max_batch_size=8 collector.server_timeout=0.01
223+
```
224+
225+
`collector.async_env=true` uses `AsyncBatchedCollector` so faster environment
226+
slots do not wait at a global step barrier. `collector.async_policy=true` routes
227+
policy calls through an inference server; with multiple async env slots this
228+
enables auto-batching and logs `policy_server/*` counters such as average batch
229+
size, request rate, and queue/forward latency. The `false/true` combination is
230+
available as a policy-server plumbing ablation, but policy auto-batching is most
231+
meaningful when several env slots submit requests concurrently.
232+
207233
Eval rollouts can also be rendered to video (`logger.record_video=true`, on by
208234
default). A dedicated single-environment recorder is built with
209235
`from_pixels=True`: `ToyVLAEnv` renders the tracking scene, while `LiberoEnv`

sota-implementations/vla_grpo/config/vla_grpo_libero.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,23 @@ collector:
9191
min_replay_decisions: null # null/0 = collect one target group wave
9292
total_iters: 100 # the paper's total_epochs
9393
policy_device: null # null = policy.device; set e.g. cuda:1 for rollout inference
94+
# Execution-mode switches for throughput ablations:
95+
# - false/false: regular synchronous TorchRL Collector.
96+
# - true/false: async env slots with one request per policy forward.
97+
# - true/true: async env slots plus auto-batched policy inference.
98+
# - false/true: sync env stepping through the policy server path.
99+
async_env: false
100+
async_policy: false
101+
env_backend: threading # AsyncBatchedCollector env backend: threading | multiprocessing
102+
policy_backend: threading # inference transport: threading | multiprocessing | ray | monarch
103+
server_backend: thread # process server needs a policy_factory and is not used here
104+
server_max_batch_size: null # null = env.num_envs when async_policy=true
105+
server_min_batch_size: 1
106+
server_timeout: 0.01
107+
server_collect_stats: true
108+
server_stats_window_size: 1024
109+
max_inflight_per_env: 1
110+
storing_device: null
94111

95112
advantage:
96113
trajectory_return: sum # binary success return per trajectory

sota-implementations/vla_grpo/config/vla_grpo_toy.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ env:
1111
success_tol: 0.35 # sized so a random policy succeeds sometimes (cold-start signal)
1212
max_outer_steps: 6 # episode truncation, in chunk decisions
1313
render_size: 64 # side length of the from_pixels eval-video frame
14+
num_envs: 1 # async-env workers; ToyVLAEnv grouped rollouts are per worker
1415
seed: 0
1516

1617
tokenizer:
@@ -35,6 +36,23 @@ collector:
3536
max_same_policy_collect_attempts: 2
3637
min_replay_decisions: null # null/0 = collect one target group wave
3738
total_iters: 200
39+
# Execution-mode switches for throughput ablations:
40+
# - false/false: regular synchronous TorchRL Collector.
41+
# - true/false: async env slots with one request per policy forward.
42+
# - true/true: async env slots plus auto-batched policy inference.
43+
# - false/true: sync env stepping through the policy server path.
44+
async_env: false
45+
async_policy: false
46+
env_backend: threading # AsyncBatchedCollector env backend: threading | multiprocessing
47+
policy_backend: threading # inference transport: threading | multiprocessing | ray | monarch
48+
server_backend: thread # process server needs a policy_factory and is not used here
49+
server_max_batch_size: null # null = number of async envs when async_policy=true
50+
server_min_batch_size: 1
51+
server_timeout: 0.01
52+
server_collect_stats: true
53+
server_stats_window_size: 1024
54+
max_inflight_per_env: 1
55+
storing_device: null
3856

3957
advantage:
4058
trajectory_return: sum # binary success return per trajectory

sota-implementations/vla_grpo/test_openvla.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,187 @@ def hook(_):
357357
assert captured["kwargs"]["frames_per_batch"] == 4
358358

359359

360+
def test_make_collector_async_env_uses_async_batched_collector(monkeypatch):
361+
captured = {}
362+
363+
class _FakeAsyncCollector:
364+
def __init__(self, *args, **kwargs):
365+
captured["args"] = args
366+
captured["kwargs"] = kwargs
367+
368+
def __iter__(self):
369+
return self
370+
371+
def __next__(self):
372+
raise StopIteration
373+
374+
def server_stats(self, *, reset=False):
375+
return {"requests": 0}
376+
377+
def shutdown(self):
378+
captured["shutdown"] = True
379+
380+
class _FakeEnv:
381+
batch_size = torch.Size([1])
382+
device = torch.device("cpu")
383+
384+
cfg = SimpleNamespace(
385+
collector=SimpleNamespace(
386+
groups_per_iter=4,
387+
group_size=2,
388+
async_env=True,
389+
async_policy=True,
390+
server_min_batch_size=2,
391+
),
392+
env=SimpleNamespace(
393+
backend="toy",
394+
action_dim=2,
395+
state_dim=4,
396+
image_shape=(3, 8, 8),
397+
render_size=16,
398+
success_steps=2,
399+
success_tol=0.25,
400+
max_outer_steps=3,
401+
num_envs=4,
402+
seed=0,
403+
),
404+
)
405+
monkeypatch.setattr(utils, "AsyncBatchedCollector", _FakeAsyncCollector)
406+
407+
collector = utils.make_collector(
408+
cfg,
409+
_FakeEnv(),
410+
object(),
411+
torch.device("cpu"),
412+
tokenizer=object(),
413+
replay_buffer=object(),
414+
)
415+
collector._ensure_collector()
416+
417+
assert len(captured["kwargs"]["create_env_fn"]) == 4
418+
assert captured["kwargs"]["yield_completed_trajectories"]
419+
server_config = captured["kwargs"]["server_config"]
420+
assert server_config.max_batch_size == 4
421+
assert server_config.min_batch_size == 2
422+
423+
424+
def test_make_collector_async_env_without_policy_batching(monkeypatch):
425+
captured = {}
426+
427+
class _FakeAsyncCollector:
428+
def __init__(self, *args, **kwargs):
429+
captured["kwargs"] = kwargs
430+
431+
def __iter__(self):
432+
return self
433+
434+
def __next__(self):
435+
raise StopIteration
436+
437+
def server_stats(self, *, reset=False):
438+
return {}
439+
440+
def shutdown(self):
441+
pass
442+
443+
class _FakeEnv:
444+
batch_size = torch.Size([1])
445+
device = torch.device("cpu")
446+
447+
cfg = SimpleNamespace(
448+
collector=SimpleNamespace(
449+
groups_per_iter=2,
450+
group_size=2,
451+
async_env=True,
452+
async_policy=False,
453+
),
454+
env=SimpleNamespace(
455+
backend="toy",
456+
action_dim=2,
457+
state_dim=4,
458+
image_shape=(3, 8, 8),
459+
render_size=16,
460+
success_steps=2,
461+
success_tol=0.25,
462+
max_outer_steps=3,
463+
num_envs=2,
464+
seed=0,
465+
),
466+
)
467+
monkeypatch.setattr(utils, "AsyncBatchedCollector", _FakeAsyncCollector)
468+
469+
collector = utils.make_collector(
470+
cfg,
471+
_FakeEnv(),
472+
object(),
473+
torch.device("cpu"),
474+
tokenizer=object(),
475+
)
476+
collector._ensure_collector()
477+
478+
server_config = captured["kwargs"]["server_config"]
479+
assert server_config.max_batch_size == 1
480+
assert server_config.timeout == 0.0
481+
482+
483+
def test_make_collector_sync_env_can_use_policy_server(monkeypatch):
484+
captured = {}
485+
486+
class _FakeCollector:
487+
def __init__(self, *args, **kwargs):
488+
captured["collector_args"] = args
489+
captured["collector_kwargs"] = kwargs
490+
self.requested_frames_per_batch = kwargs["frames_per_batch"]
491+
492+
def shutdown(self, *args, **kwargs):
493+
captured["collector_shutdown"] = True
494+
495+
def reset(self, *args, **kwargs):
496+
captured["collector_reset"] = True
497+
498+
class _FakeServer:
499+
def __init__(self, *args, **kwargs):
500+
captured["server_args"] = args
501+
captured["server_kwargs"] = kwargs
502+
503+
def start(self):
504+
return self
505+
506+
def shutdown(self):
507+
captured["server_shutdown"] = True
508+
509+
def stats(self, *, reset=False):
510+
return {"requests": 0}
511+
512+
class _FakeEnv:
513+
batch_size = torch.Size([2])
514+
device = None
515+
516+
policy = SimpleNamespace(
517+
in_keys=["observation"], out_keys=[("vla_action", "tokens")]
518+
)
519+
cfg = SimpleNamespace(
520+
collector=SimpleNamespace(
521+
groups_per_iter=2,
522+
group_size=1,
523+
async_policy=True,
524+
),
525+
env=SimpleNamespace(max_outer_steps=3),
526+
)
527+
monkeypatch.setattr(utils, "Collector", _FakeCollector)
528+
monkeypatch.setattr(utils, "InferenceServer", _FakeServer)
529+
530+
collector = utils.make_collector(cfg, _FakeEnv(), policy, torch.device("cpu"))
531+
532+
assert isinstance(collector, utils._ServerBackedCollector)
533+
assert isinstance(captured["collector_args"][1], utils.PolicyClientModule)
534+
assert captured["server_kwargs"]["server_config"].max_batch_size == 2
535+
assert captured["collector_kwargs"]["policy_device"] == torch.device("cpu")
536+
assert captured["collector_kwargs"]["trust_policy"] is True
537+
collector.shutdown()
538+
assert captured["server_shutdown"]
539+
540+
360541
def test_make_replay_buffer_scales_capacity_with_overcollection():
361542
cfg = SimpleNamespace(
362543
collector=SimpleNamespace(

0 commit comments

Comments
 (0)