Skip to content

Commit c10b1f1

Browse files
committed
[BugFix] Stabilize LIBERO VLA GRPO training
Align preprocessing, CPU action decoding, inference microbatching, collector lifecycle, replay grouping, evaluator metrics, and policy synchronization with the validated runtime. Add paper-shaped configuration controls and focused regression coverage for the end-to-end VLA GRPO path. Absorbs the async collection-mode work (per-worker group-id offsets, mode-selection plumbing) whose earlier prototype implementation this commit replaces. Review fixes: version stamping and weight updates run under the model lock; the control-plane timeout raises TimeoutError; the tokenizer decode path works on CUDA-resident modules, returns float32 on the input device, and drops its unreachable tensor branch; the evaluator warns on missing trajectory info instead of silently reporting one episode; async eval no longer stalls on shutdown when nothing is pending; thread-backend video recording refuses to silently narrow multi-task evals; the sbatch launch matches its resource request; weight sync ships trainable-only parameters by default; dead config keys removed and README metrics/trajectory counts corrected. ghstack-source-id: ec0fc2f Pull-Request: #3940 # Conflicts: # torchrl/modules/inference_server/_server.py
1 parent 9d59aa4 commit c10b1f1

31 files changed

Lines changed: 3831 additions & 1848 deletions

sota-implementations/vla_grpo/README.md

Lines changed: 147 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ vendored modeling code under `openvla_oft/` comes from
166166
[SimpleVLA-RL](https://github.com/PRIME-RL/SimpleVLA-RL) (MIT).
167167

168168
Important compatibility note: the official continuous-head OpenVLA-OFT
169-
checkpoints are not interchangeable with this token-head variant. Use the
170-
SimpleVLA-RL SFT checkpoints, for example `Haozhan72/*`:
169+
checkpoints are not interchangeable with this token-head variant for GRPO
170+
training. Use the SimpleVLA-RL SFT checkpoints, for example `Haozhan72/*`:
171171

172172
```python
173173
from openvla import OpenVLAOFTWrapper
@@ -196,38 +196,58 @@ pytest sota-implementations/vla_grpo/test_openvla.py
196196
## What gets logged
197197

198198
With a logger configured (`logger.backend=wandb`, the default), each iteration
199-
logs reward curves (`train/reward_mean`, `train/reward_max`), success rate, and
200-
throughput split into collection and optimization:
199+
logs the training success rate (`train/success_rate`), trajectory-return
200+
aggregates (`collector/trajectory_return_sum`,
201+
`collector/trajectory_return_max`), and throughput split into collection and
202+
optimization:
201203

202204
- `throughput/inference_env_steps_per_s`
203205
- `throughput/inference_decisions_per_s`
204206
- `throughput/train_decisions_per_s`
205207
- `throughput/optim_steps_per_s`
206208

207-
Eval rollouts can also be rendered to video (`logger.record_video=true`, on by
208-
default). A dedicated single-environment recorder is built with
209-
`from_pixels=True`: `ToyVLAEnv` renders the tracking scene, while `LiberoEnv`
210-
exposes its camera. `torchrl.record.VideoRecorder` writes
211-
`logger.video_episodes` greedy episodes to `eval/video` on every eval. wandb
212-
video encoding needs `moviepy` from the `dev` dependency group. Disable videos
213-
with `logger.record_video=false`.
209+
The collector path is fixed: a TorchRL `MultiCollector` launches rollout
210+
workers, each worker owns a sync `ParallelEnv(envs_per_collector)`, and all
211+
workers plus the evaluator share one process policy server.
212+
213+
```bash
214+
python sota-implementations/vla_grpo/vla-grpo.py --config-name vla_grpo_libero
215+
```
216+
217+
Rollout clients request random sampling; eval and video clients request
218+
deterministic decoding from the same server, so rollout and eval are synced by
219+
one explicit TensorDict weight update after each optimizer step. The replay
220+
buffer is passed to the collector and receives complete trajectories as they
221+
finish; training waits until the consuming replay buffer has enough sampleable
222+
decisions.
214223

215-
Checkpointing is shared by the toy and LIBERO configs. `checkpoint_latest.pt`
216-
is written to the hydra run directory every `checkpoint.save_iter` iterations;
224+
With the thread evaluator backend, eval rollouts are rendered to video whenever
225+
a logger is configured. A dedicated single-environment evaluator is built with
226+
`from_pixels=True`: `ToyVLAEnv` renders the tracking scene, while `LiberoEnv`
227+
exposes its camera. `torchrl.record.VideoRecorder` writes the evaluator rollout
228+
to `eval/video` on every eval. wandb video encoding needs `moviepy` from the
229+
`dev` dependency group. Process-backend evaluator video dumping still needs a
230+
TorchRL-side remote `VideoRecorder.dump` path.
231+
232+
Checkpointing is shared by the toy and LIBERO configs. `checkpoint_latest`
233+
is written as a TensorDict directory in the hydra run directory every
234+
`checkpoint.save_iter` iterations;
217235
resume with:
218236

219237
```bash
220238
python sota-implementations/vla_grpo/vla-grpo.py \
221-
checkpoint.resume=/path/to/checkpoint_latest.pt
239+
checkpoint.resume=/path/to/checkpoint_latest
222240
```
223241

224242
## LIBERO configuration details
225243

226244
The full LIBERO config follows the SimpleVLA-RL hyper-parameter shape:
227245

228246
- groups of `n=8` rollouts per initial state;
229-
- 64 initial states per iteration, for 512 trajectories before dynamic
230-
filtering;
247+
- 40 initial states per iteration (`collector.groups_per_iter`), for 320
248+
trajectories before dynamic filtering -- one aligned group wave across the
249+
320 rollout envs; the paper uses 64 initial states (512 trajectories) per
250+
iteration, which is a known deviation of the shipped config;
231251
- 512 base environment steps, or 64 chunk decisions, per episode;
232252
- rollout temperature 1.6 and greedy evaluation;
233253
- dynamic sampling bounds `(0.1, 0.9)` to drop groups that are all failure or
@@ -241,47 +261,53 @@ A sequence-level ratio remains available as a config switch for ablations, but
241261
for a 56-token action chunk it saturates the clip range much more easily than
242262
per-token ratios.
243263

244-
LIBERO simulation runs in parallel worker processes (`env.num_envs`, one MuJoCo
245-
instance each), and policy inference batches across workers. Group accounting is
246-
the main thing to keep in mind: GRPO needs repeated attempts from the same
247-
initial state under the same policy. Each worker owns a disjoint `group_id`
248-
block so advantages never mix across unrelated groups.
249-
250-
Because groups are repeated serially within each worker, `env.num_envs` should
251-
not exceed `collector.groups_per_iter`; otherwise many same-policy collection
252-
polls are needed before each worker can finish all `group_size` rollouts for a
253-
group and the replay buffer receives advantaged decisions. For best throughput,
254-
set `env.num_envs` to a divisor of `collector.groups_per_iter`, often the same
255-
value.
256-
257-
When `env.parallel_group_repeats=true`, `env.num_envs / collector.group_size`
258-
logical workers each run one repeated-initial-state group in parallel. In this
259-
mode, prefer setting `collector.groups_per_iter` to that logical worker count
260-
so one target group wave is aligned. If `collector.candidate_group_size` is
261-
larger than `collector.group_size`, each worker in a logical group repeats the
262-
same initial state serially enough times to produce up to the requested
263-
candidate count. For example, 8 parallel workers x 2 serial repeats gives at
264-
most 16 candidates. Groups can be written earlier if the candidates already
265-
contain a useful selected subset.
266-
267-
The replay-buffer writer polls the collector at one outer step per worker, so
268-
complete trajectories are handed to the replay buffer shortly after they finish
269-
instead of waiting for a full max-length rollout from every worker.
264+
LIBERO simulation runs through `collector.num_collectors` MultiCollector
265+
workers. Each worker hosts a synchronous
266+
`ParallelEnv(collector.envs_per_collector)`. Policy inference runs on the
267+
shared process server and each worker owns a disjoint `group_id` block so
268+
advantages never mix across unrelated groups.
269+
270+
Without `env.parallel_group_repeats`, groups are repeated serially within each
271+
worker, so the total rollout worker count should not exceed
272+
`collector.groups_per_iter`. For that serial mode, set
273+
`collector.num_collectors * collector.envs_per_collector` to a divisor of
274+
`collector.groups_per_iter`, often the same value.
275+
276+
When `env.parallel_group_repeats=true`, the shared replay buffer centralizes
277+
`MCAdvantage` write state, so same-initial-state groups may straddle
278+
subcollectors. The logical worker count is the total rollout worker count
279+
divided by `collector.group_size`. In this mode, prefer setting
280+
`collector.groups_per_iter` to that logical worker count so one target group
281+
wave is aligned. If `collector.candidate_group_size` is larger than
282+
`collector.group_size`, each worker in a logical group repeats the same initial
283+
state serially enough times to produce up to the requested candidate count. For
284+
example, 8 parallel workers x 2 serial repeats gives at most 16 candidates.
285+
Groups can be written earlier if the candidates already contain a useful
286+
selected subset.
287+
288+
The training script starts the collector once, waits until the consuming replay
289+
buffer has enough sampleable decisions, pauses collection, runs the PPO update,
290+
clears incomplete same-policy advantage queues and partial trajectories, pushes
291+
the TensorDict policy weights to the shared policy server, and then lets the
292+
collector resume.
270293
`MCAdvantage` runs as the replay-buffer transform and keeps incomplete groups
271-
queued across same-policy polls until all siblings arrive.
272-
`max_collect_batches_per_iter` sets the safety cap in target group waves, and
273-
`collector.min_replay_decisions` can require a minimum number of useful replay
274-
decisions before the PPO update.
294+
queued only within a single policy window. `collector.min_replay_decisions` can
295+
require a minimum number of useful replay decisions before the PPO update.
296+
Set `TORCHRL_MC_ADVANTAGE_LOCAL_QUEUES=1` to keep grouping state in each replay
297+
writer instead of a multiprocessing manager. At every policy boundary the
298+
trainer reads those worker-local counters while collection is paused, clears
299+
their queues, and resets in-flight collector trajectories before the policy
300+
version advances.
275301

276302
Candidate selection is delegated to `MCAdvantageSelector` (`first`, `uniform`,
277303
or `balanced`), so the replay-buffer transform owns the sample-selection policy
278-
while the collector only supplies same-policy completed trajectories. At the
279-
policy-update boundary the replay buffer, incomplete advantage queues, and
280-
in-flight collector trajectories are cleared before the next policy is rolled
281-
out. LIBERO workers stamp parallel-repeat group ids from the cycled
282-
initial-state id so fast and slow sibling workers can still complete a
283-
same-initial-state GRPO group under the same policy even when their episode
284-
lengths differ.
304+
while the collector only supplies same-policy completed trajectories. The
305+
consuming replay buffer removes sampled decisions after
306+
`buffer.consume_after_n_samples` samples, and the policy-boundary pause keeps
307+
rollout and optimization phases explicit. LIBERO workers stamp
308+
parallel-repeat group ids from the cycled initial-state id so fast and slow
309+
sibling workers can still complete a same-initial-state GRPO group under the
310+
same policy even when their episode lengths differ.
285311

286312
Run the LIBERO recipe with:
287313

@@ -295,31 +321,82 @@ Requirements beyond the toy scale: LIBERO (see the `torchrl.envs.LiberoEnv`
295321
docs for install notes), `transformers`, `timm`, `Pillow`, and `peft` when
296322
`policy.lora_rank` is set.
297323

324+
For reference-parity rollouts, set `policy.image_backend=tensorflow`. This uses
325+
the SimpleVLA JPEG, Lanczos resize, and center-crop order. Normalized
326+
vocabulary-tail action tokens are detokenized through the NumPy float64 CPU
327+
path before the gripper transform is applied once in the environment. Use
328+
`env.train_init_state_mode=fixed env.train_init_state_id=<id>` for a fixed
329+
LIBERO initial state. `collector.policy_micro_batch_size` only slices actual
330+
model calls inside the inference-server policy; it does not change PPO
331+
minibatching.
332+
298333
## Hardware notes
299334

300-
- The default configuration trains a LoRA adapter (`policy.lora_rank: 32`) on a
301-
single GPU while the simulation workers occupy CPU cores. Rollout wall-clock
302-
dominates, so scale `env.num_envs` with the available cores first, while
303-
keeping it within the GRPO grouping constraint above.
304-
- Set `collector.policy_device` to a different CUDA device to keep rollout
305-
inference on a separate policy replica. The training loop copies only the
306-
trainable state dict after optimizer updates, so this split is intended for
307-
LoRA/adapters rather than full-parameter fine-tuning.
335+
- The default H100 configuration trains a LoRA adapter
336+
(`policy.lora_rank: 32`) on `policy.device: cuda:0` and serves rollout plus
337+
evaluator inference from `collector.policy_device: cuda:1`. Four
338+
collectors each run `ParallelEnv(80)` for 320 rollout envs total. On
339+
single-GPU runs, override `policy.device=null`,
340+
`collector.policy_device=null`, `collector.num_collectors=1`, and
341+
`collector.envs_per_collector` to the number of local envs.
342+
- Rollout wall-clock dominates, so scale `collector.num_collectors` and
343+
`collector.envs_per_collector` with the available CPU cores while keeping
344+
the GRPO grouping constraint above. The H100 default uses
345+
`collector.num_collectors=4`, `collector.envs_per_collector=80`,
346+
`collector.groups_per_iter=40`, and parallel group repeats enabled. The
347+
training loop pushes TensorDict policy weights to the shared policy server
348+
after optimizer updates.
308349
- Headless LIBERO rendering uses MuJoCo/robosuite EGL by default
309350
(`env.render_backend: egl`). `env.render_gpu_ids` controls the EGL-visible
310-
render device ids assigned to workers, round-robin. The default `[0]` works
311-
on a single-GPU allocation; on a multi-GPU node, override it, for example
312-
`env.render_gpu_ids=[0,1,2,3]`, to spread render workers across GPUs. These
313-
ids are the devices visible to EGL inside the process/container and may not
314-
match global CUDA ordinals.
315-
- Set `logger.eval_process=true` to move greedy eval into a dedicated process.
316-
Use `logger.eval_device` for its policy device and `env.eval_render_gpu_ids`
317-
for its EGL render workers; when the latter is left null, eval reuses
318-
`env.render_gpu_ids`.
351+
render device ids assigned to rollout workers, round-robin. The H100 default
352+
spreads rollout rendering over `[2,3,4,5]` and reserves
353+
`env.eval_render_gpu_ids=[7]` for eval/video rendering. These ids are the
354+
devices visible to EGL inside the process/container and may not match global
355+
CUDA ordinals.
356+
- Use `logger.eval_backend` for the TorchRL evaluator backend. The evaluator
357+
shares the same policy server as rollout and uses `env.eval_render_gpu_ids`
358+
for EGL rendering; when the latter is left null, eval reuses
359+
`env.render_gpu_ids`. The LIBERO default uses `process` to isolate simulator
360+
work; use `thread` only when local VideoRecorder dumping is required. Note
361+
the caveat: with a logger, the thread backend swaps the eval env for a
362+
single-env video recorder bound to the first task, so on a multi-task suite
363+
`eval/success_rate` covers one task instead of the whole suite (and
364+
`env.eval_num_envs` is ignored). This requires the explicit opt-in
365+
`logger.record_video_single_task=true`.
319366
- Minimal CUDA containers often lack the NVIDIA EGL/GLVND userspace stack.
320367
Before debugging TorchRL, verify that `libEGL_nvidia`, `libnvidia-eglcore`,
321368
`libGLX_nvidia`, and `/usr/share/glvnd/egl_vendor.d/10_nvidia.json` are
322369
visible in the runtime.
370+
- On an H200 container with the 595 driver, the userspace libraries can be
371+
extracted without installing Debian packages into the image:
372+
373+
```bash
374+
mkdir -p /opt/nvidia-595-deb/download /opt/nvidia-595-deb/extract
375+
cd /opt/nvidia-595-deb/download
376+
apt-get download \
377+
libnvidia-gl-595 libegl1 libglvnd0 libopengl0 libgl1 libgles2 libglx0
378+
for deb in ./*.deb; do
379+
dpkg-deb -x "$deb" /opt/nvidia-595-deb/extract
380+
done
381+
382+
export LIBDIR=/opt/nvidia-595-deb/extract/usr/lib/x86_64-linux-gnu
383+
export LD_LIBRARY_PATH="$LIBDIR:${LD_LIBRARY_PATH:-}"
384+
export LD_PRELOAD="$LIBDIR/libOpenGL.so.0${LD_PRELOAD:+:$LD_PRELOAD}"
385+
export __EGL_VENDOR_LIBRARY_FILENAMES=/opt/nvidia-595-deb/extract/usr/share/glvnd/egl_vendor.d/10_nvidia.json
386+
export MUJOCO_GL=egl PYOPENGL_PLATFORM=egl ROBOT_PLATFORM=LIBERO
387+
388+
python - <<'PY'
389+
from OpenGL import EGL, GL
390+
import mujoco
391+
from libero.libero import benchmark
392+
393+
assert EGL is not None and GL.glGetError is not None
394+
assert mujoco is not None and benchmark is not None
395+
PY
396+
```
397+
398+
The validated parity runtime pins `mujoco==3.2.3`, `robosuite==1.4.1`,
399+
`transformers==4.40.1`, and `peft==0.11.1`.
323400
- Full-parameter fine-tuning of the 7B model requires sharded training (FSDP)
324401
and a multi-GPU inference/training split with explicit weight
325402
synchronization. That topology should be sized on the target hardware:

0 commit comments

Comments
 (0)