Files
hao-ai-lab-FastVideo/fastvideo/train/methods/rl/utils/sampling.py
T
2026-06-05 01:14:42 +00:00

248 lines
6.8 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Sampling epoch for RL training — generate videos and
compute rewards."""
from __future__ import annotations
import time
from collections.abc import Callable
from typing import Any
import torch
from fastvideo.logger import init_logger
from fastvideo.train.methods.rl.utils.embeddings import (
compute_text_embeddings,
)
from fastvideo.train.methods.rl.utils.pipeline import (
wan_denoising_with_logprob,
)
logger = init_logger(__name__)
SEED_EPOCH_STRIDE = 10_000
def create_generator(
prompts: list[str],
base_seed: int,
device: torch.device,
) -> list[torch.Generator]:
"""Create deterministic generators seeded by prompt."""
generators = []
for prompt in prompts:
g = torch.Generator(device=device)
g.manual_seed(base_seed + hash(prompt) % (2**31))
generators.append(g)
return generators
def sample_epoch(
model,
scheduler,
train_sampler,
train_iter,
reward_fn: Callable,
sample_neg_prompt_embeds: torch.Tensor,
text_encoder,
tokenizer,
executor,
epoch: int,
global_step: int,
*,
# Config values passed explicitly.
sample_batch_size: int,
num_batches_per_epoch: int,
num_inference_steps: int,
guidance_scale: float,
height: int,
width: int,
num_frames: int,
noise_level: float,
sde_type: str,
diffusion_clip: bool,
diffusion_clip_value: float,
sde_window_size: int,
sde_window_range: tuple[int, int] | None,
kl_reward: float,
same_latent: bool,
seed: int,
device: torch.device,
is_main_process: bool,
ref_transformer: torch.nn.Module | None = None,
lora_model: Any | None = None,
tracker: Any | None = None,
) -> tuple[
list[dict[str, Any]],
list[torch.Tensor],
list[list[str]],
]:
"""Run one sampling epoch: generate videos, compute
rewards asynchronously.
Returns:
Tuple of (samples, all_videos, all_prompts):
- samples: list of sample dicts with prompt_ids,
prompt_embeds, latents, log_probs, kl,
timesteps, rewards.
- all_videos: list of decoded video tensors
per batch, each (B, 3, T, H, W) in [0, 1].
- all_prompts: list of prompt string lists
per batch.
"""
samples = []
all_videos: list[torch.Tensor] = []
all_prompts: list[list[str]] = []
for i in range(num_batches_per_epoch):
current_epoch_tag = (
epoch * num_batches_per_epoch + i
)
train_sampler.set_epoch(current_epoch_tag)
# Drain until epoch tag matches.
while True:
epoch_tag, prompts, prompt_metadata = next(
train_iter
)
if epoch_tag == current_epoch_tag:
break
torch.cuda.synchronize()
_t_batch_start = time.perf_counter()
_t_embed = time.perf_counter()
prompt_embeds = compute_text_embeddings(
prompts,
text_encoder,
tokenizer,
max_sequence_length=512,
device=device,
)
prompt_ids = tokenizer(
prompts,
padding="max_length",
max_length=512,
truncation=True,
return_tensors="pt",
).input_ids.to(device)
# Generator setup.
if same_latent:
gen = create_generator(
prompts,
base_seed=epoch * SEED_EPOCH_STRIDE + i,
device=device,
)
else:
gen = torch.Generator(device=device)
gen.manual_seed(
seed + epoch * SEED_EPOCH_STRIDE + i
)
torch.cuda.synchronize()
_t_embed_done = time.perf_counter()
with torch.no_grad():
(
videos,
latents_list,
log_probs_list,
kls_list,
timesteps_list,
) = wan_denoising_with_logprob(
model,
scheduler,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=(
sample_neg_prompt_embeds
),
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
height=height,
width=width,
num_frames=num_frames,
generator=gen,
noise_level=noise_level,
sde_type=sde_type,
diffusion_clip=diffusion_clip,
diffusion_clip_value=diffusion_clip_value,
sde_window_size=sde_window_size,
sde_window_range=sde_window_range,
kl_reward=kl_reward,
ref_transformer=ref_transformer,
lora_model=lora_model,
)
torch.cuda.synchronize()
_t_denoise_done = time.perf_counter()
latents = torch.stack(latents_list, dim=1)
log_probs = torch.stack(log_probs_list, dim=1)
kls = torch.stack(kls_list, dim=1)
kl = kls.detach()
timesteps = (
torch.stack(timesteps_list)
.unsqueeze(0)
.repeat(sample_batch_size, 1)
)
# Collect decoded videos and prompts for logging.
all_videos.append(videos)
all_prompts.append(list(prompts))
# Async reward computation.
rewards_future = executor.submit(
reward_fn,
videos,
prompts,
prompt_metadata,
True,
)
time.sleep(0)
logger.info(
"[sample_epoch] batch %d/%d: "
"text_embed=%.1fs denoise=%.1fs "
"batch_total=%.1fs",
i + 1,
num_batches_per_epoch,
_t_embed_done - _t_embed,
_t_denoise_done - _t_embed_done,
_t_denoise_done - _t_batch_start,
)
samples.append(
{
"prompt_ids": prompt_ids,
"prompt_embeds": prompt_embeds,
"negative_prompt_embeds": (
sample_neg_prompt_embeds
),
"timesteps": timesteps,
"latents": latents[:, :-1],
"next_latents": latents[:, 1:],
"log_probs": log_probs,
"kl": kl,
"rewards": rewards_future,
}
)
# Wait for all rewards.
torch.cuda.synchronize()
_t_reward_wait = time.perf_counter()
for sample in samples:
rewards, _ = sample["rewards"].result()
sample["rewards"] = {
key: torch.as_tensor(value, device=device).float()
for key, value in rewards.items()
}
_t_reward_done = time.perf_counter()
logger.info(
"[sample_epoch] reward_wait=%.1fs",
_t_reward_done - _t_reward_wait,
)
return samples, all_videos, all_prompts