Skip to content

[bugfix]: stabilize GenRL reward and PPO training - #1391

Closed
Abecid wants to merge 7 commits into
hao-ai-lab:py/add_rlfrom
Abecid:pr/genrl-clean
Closed

[bugfix]: stabilize GenRL reward and PPO training#1391
Abecid wants to merge 7 commits into
hao-ai-lab:py/add_rlfrom
Abecid:pr/genrl-clean

Conversation

@Abecid

@Abecid Abecid commented May 24, 2026

Copy link
Copy Markdown

Purpose

Stabilize the GenRL / diffusion RL training path on top of py/add_rl.

This PR focuses on getting the RL pipeline to run more reliably for Wan T2V training with multi-reward scoring, especially HPSv3 + VideoAlign rewards, and reducing PPO/GRPO instability from noisy rollout grouping and reward-model memory pressure.

Fixes #

Changes

  • Keep repeated samples from the same prompt together in DistributedKRepeatSampler.

    • Each rank now receives whole prompt groups when sample_batch_size is divisible by num_video_per_prompt.
    • This avoids splitting one GRPO comparison group across ranks with different sampled SDE timestep windows.
  • Add safer PPO/GRPO training behavior.

    • Factor PPO loss and diagnostics into a helper.
    • Add optional rollout microbatch accumulation via accumulate_ppo_microbatches.
    • Add post-update logprob/KL diagnostics.
    • Add flash_tgr loss reweighting mode to avoid overly large LongCat timestep reweighting during full-parameter training.
    • Lower the LongCat config clip range to 1e-4.
  • Improve reward stack robustness.

    • Move decoded rollout videos to CPU before reward scoring/logging.
    • Avoid asynchronous GPU reward scoring when reward models are on GPU, preventing VideoAlign from overlapping with Wan denoising and causing CUDA OOM.
    • Clear cached GPU reward inferencers before PPO training.
    • Patch VideoAlign/HPSv3 compatibility issues for current Qwen2-VL / transformers behavior.
  • Add/adjust GenRL training configs.

    • Keep full fine-tuning as the default path for the Wan LongCat-style multi-reward config.
    • Keep LoRA options configurable.
    • Add fixed eval reward settings for more stable reward tracking.

Running Experiments

Example local/multi-GPU launch:

bash examples/train/run.sh \
  examples/train/configs/genrl_wan2.1_t2v_1.3B_longcat.yaml \
  --method.prompt_dataset_path /path/to/filtered_prompts \
  --training.checkpoint.output_dir /path/to/outputs/genrl_longcat

Experiment Environment Notes

I verified the GenRL run in a Modal environment roughly equivalent to:

  • Base image: nvidia/cuda:12.8.1-devel-ubuntu22.04
  • Python: 3.12
  • GPU stack: CUDA 12.8, PyTorch/torchvision/torchaudio from the cu128 index
  • System packages: ffmpeg, libgl1, libglib2.0-0, build-essential, ninja-build, cmake, git-lfs
  • Extra Python deps used by the reward stack:
    • datasets==3.6.0
    • matplotlib==3.10.3
    • peft==0.10.0
    • qwen-vl-utils==0.0.11
    • safetensors==0.5.3
    • timm==1.0.15
    • trl==0.8.6
    • numpy==1.26.4
    • scipy==1.15.2
  • FlashAttention: flash_attn-2.8.3+cu128torch2.10 prebuilt wheel

The Modal launcher itself is not included in this PR; these notes are only to document the environment used for testing/debugging.

Performance

Confirmed the following reward metrics going up over 40 iterations:

  • reward_videoalign_ta_raw: 1.5
  • reward_videoalign_mq_raw: 0.6
  • reward_hpsv3_percentile_raw: 8.67
  • reward_hpsv3_general_raw: 3.456

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Welcome to FastVideo! Thanks for your first pull request.

How our CI works:

PRs run a two-tier CI system:

  1. Pre-commit — formatting (yapf), linting (ruff), type checking (mypy). Runs immediately on every PR.
  2. Fastcheck — core GPU tests (encoders, VAEs, transformers, kernels, unit tests). Runs automatically via Buildkite on relevant file changes (~10-15 min).
  3. Full Suite — integration tests, training pipelines, SSIM regression. Runs only when a reviewer adds the ready label.

Before your PR is reviewed:

  • pre-commit run --all-files passes locally
  • You've added or updated tests for your changes
  • The PR description explains what and why

If pre-commit fails, a bot comment will explain how to fix it. Fastcheck and Full Suite results appear in the Checks section below.

Useful links:

@mergify mergify Bot added type: bugfix Bug fix scope: training Training pipeline, methods, configs scope: model Model architecture (DiTs, encoders, VAEs) labels May 24, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces LoRA support for Wan-based video RL training, implements PPO microbatch accumulation, and adds an evaluation suite during training. It also includes extensive compatibility patches for reward models, improves deterministic sampling, and optimizes GPU memory management. Review feedback identified several issues in the training logic, such as incorrect diagnostic capturing in the microbatch path, missing gradient resets for certain optimizer settings, and a validation mismatch regarding batch size divisibility. It was also suggested to replace assertions in the LoRA forward pass with explicit runtime errors to improve robustness.

Comment on lines +1048 to +1054
if probe_args is None:
probe_args = (
sample,
j,
embeds,
neg_embeds,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In the accumulated microbatch path, probe_args is captured only once at the beginning of the first timestep (j=0). When _log_post_update_probe is called inside the j loop (line 1074), it will always recompute diagnostics for timestep 0, regardless of which timestep j was actually updated. This makes the post-update KL diagnostics incorrect for all timesteps except the first one. probe_args should be updated for each timestep j to use a sample corresponding to that timestep.

                    probe_sample = batched_list[0]
                    probe_args = (
                        probe_sample,
                        j,
                        probe_sample["prompt_embeds"],
                        self._train_neg_embeds[:len(probe_sample["prompt_embeds"])] if self._use_cfg else None,
                    )

and len(batched_list) > 1
):
probe_args = None
for j in self._train_timesteps:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When accumulate_ppo_microbatches is True and optimizer_step_per_timestep is False, the optimizer gradients are never zeroed at the start of the inner epoch. This can lead to accumulating gradients from previous steps or epochs. An explicit self._optimizer.zero_grad() should be added before starting the timestep/microbatch loops.

Suggested change
for j in self._train_timesteps:
if not self._optimizer_step_per_timestep:
self._optimizer.zero_grad()
for j in self._train_timesteps:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

robustness

if not self.merged and not self.disable_lora:
lora_A = self.lora_A
lora_B = self.lora_B
assert lora_A is not None and lora_B is not None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using assert in the forward pass is generally discouraged for production code as it can be optimized away with the -O flag. It is better to use a proper conditional check and raise a ValueError or RuntimeError if the LoRA parameters are missing when they are expected.

Suggested change
assert lora_A is not None and lora_B is not None
if lora_A is None or lora_B is None:
raise RuntimeError("LoRA weights (lora_A, lora_B) must be initialized before forward pass when LoRA is enabled.")

Comment on lines +143 to +150
total_samples = self._world_size * self._sample_batch_size
if total_samples % self._num_video_per_prompt != 0:
raise ValueError(
"world_size * sample_batch_size must be divisible by "
"num_video_per_prompt for DistributedKRepeatSampler. Got "
f"{self._world_size} * {self._sample_batch_size} and "
f"{self._num_video_per_prompt}."
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The divisibility check here uses total_samples (global batch size), but the DistributedKRepeatSampler (in fastvideo/train/methods/rl/utils/data.py) enforces a stricter requirement: the per-rank batch_size must be divisible by num_video_per_prompt to ensure whole prompt groups are kept on the same rank. To avoid a ValueError later in the sampler initialization, this check should be updated to match.

        if self._sample_batch_size % self._num_video_per_prompt != 0:
            raise ValueError(
                "sample_batch_size (per rank) must be divisible by "
                "num_video_per_prompt for DistributedKRepeatSampler. Got "
                f"{self._sample_batch_size} and {self._num_video_per_prompt}."
            )

@Davids048 Davids048 May 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is tied to the first change. Keep repeated samples from the same prompt together in DistributedKRepeatSampler.
should patch to enforce the rule

Comment on lines +1174 to +1186
if not self._optimizer_step_per_timestep:
# Clip + step after accumulating all train timesteps.
grad_norm = clip_grad_norm_if_needed(
self.student.transformer,
self._max_grad_norm,
)
if self._beta > 0 and prev_mean_ref is not None:
info["kl_loss"].append(
kl_loss.detach().item()
)
info["loss"].append(
loss.detach().item()
self._optimizer.step()
self._lr_scheduler.step()
info["grad_norm"].append(grad_norm)
info["learning_rate"].append(
float(self._optimizer.param_groups[0]["lr"])
)

# Clip + step after all timesteps.
clip_grad_norm_if_needed(
self.student.transformer,
self._max_grad_norm,
)
self._optimizer.step()
self._optimizer.zero_grad()
self._optimizer.zero_grad()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The _log_post_update_probe call is missing in the non-accumulated path when optimizer_step_per_timestep is False. This results in missing post-update KL diagnostics for this specific configuration, which is inconsistent with the other branches.

@alexzms
alexzms self-requested a review May 24, 2026 22:21

@Davids048 Davids048 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the PR combines runtime/OOM fixes, reward-model compatibility patches, sampler behavior, PPO/GRPO training-loop changes, LoRA support, and config/default changes. That makes it hard to attribute regressions in the future.

I would recommend splitting this into a small stack of PRs. For example:

  • OOM fixes
  • Reward model compatibility fixes
  • prompt group placement
  • training loop changes
  • LoRA support for GenRL
    (Don't need to follow this exactly tho.)
    There is an agent skill that may help: link

The Gemini comments look valid to me and should be addressed before merge.

3 additional issues I think are important:

1. LoRA is inserted after the Wan transformer has already gone through the FSDP/HSDP loader path

In fastvideo/train/models/wan/wan_genrl.py, GenRLWanModel.__init__() calls the parent WanModel init first, which loads the transformer through load_module_from_path(). After that, when use_lora=True, _apply_fastvideo_lora() replaces modules and creates new LoRA parameters.

That means the LoRA parameters are introduced after the transformer has already gone through the distributed loader/sharding path. Those newly inserted parameters may not be managed with the same FSDP/HSDP device mesh or checkpointing assumptions as the rest of the model.

2. Synchronous GPU reward mode still retains decoded CPU videos for all rollout batches before scoring

With reward_on_gpu: true, async reward scoring is disabled, but sample_epoch() still stores decoded videos_cpu for each rollout batch and delays reward scoring until after all batches are sampled. With num_batches_per_epoch: 4, this can retain multiple large decoded video tensors on CPU at once.

3. Config naming

examples/train/configs/genrl_wan2.1_t2v_1.3B_longcat.yaml is confusing because FastVideo already has an actual LongCat-Video model. The "longcat" should be more specific and point to the reward method(s) used.Please rename it to something more explicit.

It is fine to document inside the file that the recipe is derived from GenRL's LongCat config.

Test

We should add testing (CI/unit tests) on the roadmap for this development.

@Abecid

Abecid commented May 27, 2026

Copy link
Copy Markdown
Author

Thanks for the review. I split this mega PR into six focused PRs so each concern can be reviewed independently:

  1. [feat] GenRL: stabilize reward model compatibility #1400 — reward model compatibility
  2. [feat] GenRL: keep repeated prompt samples on one rank #1401 — prompt group placement / sampler behavior
  3. [feat] GenRL: add runtime and memory stability helpers #1402 — runtime and memory stability helpers
  4. [feat] GenRL: fix PPO loop cadence and diagnostics #1403 — PPO/GRPO training-loop fixes and Gemini review comments
  5. [feat] GenRL: add Wan LoRA adapter support #1404 — GenRL Wan LoRA adapter support
  6. [feat] GenRL: add explicit HPSv3 VideoAlign recipes #1405 — explicit HPSv3 + VideoAlign config recipes / config rename

I also addressed the Gemini comments in the split PRs:

For your three additional issues:

I’m going to treat #1391 as superseded by this stack unless you prefer I keep it open for reference.

@Davids048
Davids048 marked this pull request as draft June 3, 2026 21:00
@mergify

mergify Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase:

git fetch origin main
git rebase origin/main
# Resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts label Jun 4, 2026
Davids048 pushed a commit to Abecid/FastVideo that referenced this pull request Jun 5, 2026
Extracted from hao-ai-lab#1391.
Renames the LongCat-labelled recipe to reflect the reward setup.

GenRL-Stack: 6/6
@SolitaryThinker

Copy link
Copy Markdown
Collaborator

closing for now, feel free to repo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebase PR has merge conflicts scope: model Model architecture (DiTs, encoders, VAEs) scope: training Training pipeline, methods, configs type: bugfix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants