from collections import defaultdict import contextlib import os import datetime from concurrent import futures import time import json from absl import app, flags from accelerate import Accelerator from ml_collections import config_flags from accelerate.utils import set_seed, ProjectConfiguration from accelerate.logging import get_logger from diffusers import StableDiffusion3Pipeline, FlowMatchEulerDiscreteScheduler from diffusers.utils.torch_utils import is_compiled_module from diffusers.training_utils import compute_density_for_timestep_sampling, compute_loss_weighting_for_sd3 import numpy as np import flow_grpo.prompts import flow_grpo.rewards from flow_grpo.stat_tracking import PerPromptStatTracker from flow_grpo.diffusers_patch.sd3_pipeline_with_logprob import pipeline_with_logprob from flow_grpo.diffusers_patch.sd3_sde_with_logprob import sde_step_with_logprob from flow_grpo.diffusers_patch.train_dreambooth_lora_sd3 import encode_prompt import torch import wandb from functools import partial import tqdm import tempfile import itertools from PIL import Image from peft import LoraConfig, get_peft_model, set_peft_model_state_dict, PeftModel from peft.utils import get_peft_model_state_dict import random from torch.utils.data import Dataset, DataLoader, Sampler from flow_grpo.ema import EMAModuleWrapper tqdm = partial(tqdm.tqdm, dynamic_ncols=True) FLAGS = flags.FLAGS config_flags.DEFINE_config_file("config", "config/base.py", "Training configuration.") logger = get_logger(__name__) class TextPromptDataset(Dataset): def __init__(self, dataset, split='train'): self.file_path = os.path.join(dataset, f'{split}.txt') with open(self.file_path, 'r') as f: self.prompts = [line.strip() for line in f.readlines()] def __len__(self): return len(self.prompts) def __getitem__(self, idx): return {"prompt": self.prompts[idx], "metadata": {}} @staticmethod def collate_fn(examples): prompts = [example["prompt"] for example in examples] metadatas = [example["metadata"] for example in examples] return prompts, metadatas class GenevalPromptDataset(Dataset): def __init__(self, dataset, split='train'): self.file_path = os.path.join(dataset, f'{split}_metadata.jsonl') with open(self.file_path, 'r', encoding='utf-8') as f: self.metadatas = [json.loads(line) for line in f] self.prompts = [item['prompt'] for item in self.metadatas] def __len__(self): return len(self.prompts) def __getitem__(self, idx): return {"prompt": self.prompts[idx], "metadata": self.metadatas[idx]} @staticmethod def collate_fn(examples): prompts = [example["prompt"] for example in examples] metadatas = [example["metadata"] for example in examples] return prompts, metadatas class DistributedKRepeatSampler(Sampler): def __init__(self, dataset, batch_size, k, num_replicas, rank, seed=0): self.dataset = dataset self.batch_size = batch_size # Batch size per replica self.k = k # Number of repetitions per sample self.num_replicas = num_replicas # Total number of replicas self.rank = rank # Current replica rank self.seed = seed # Random seed for synchronization # Compute the number of unique samples needed per iteration self.total_samples = self.num_replicas * self.batch_size assert self.total_samples % self.k == 0, f"k can not divide n*b, k{k}-num_replicas{num_replicas}-batch_size{batch_size}" self.m = self.total_samples // self.k # Number of unique samples self.epoch = 0 def __iter__(self): while True: # Generate a deterministic random sequence to ensure all replicas are synchronized g = torch.Generator() g.manual_seed(self.seed + self.epoch) # Randomly select m unique samples indices = torch.randperm(len(self.dataset), generator=g)[:self.m].tolist() # Repeat each sample k times to generate n*b total samples repeated_indices = [idx for idx in indices for _ in range(self.k)] # Shuffle to ensure uniform distribution shuffled_indices = torch.randperm(len(repeated_indices), generator=g).tolist() shuffled_samples = [repeated_indices[i] for i in shuffled_indices] # Split samples to each replica per_card_samples = [] for i in range(self.num_replicas): start = i * self.batch_size end = start + self.batch_size per_card_samples.append(shuffled_samples[start:end]) # Return current replica's sample indices yield per_card_samples[self.rank] def set_epoch(self, epoch): self.epoch = epoch # Used to synchronize random state across epochs def compute_text_embeddings(prompt, text_encoders, tokenizers, max_sequence_length, device): with torch.no_grad(): prompt_embeds, pooled_prompt_embeds = encode_prompt( text_encoders, tokenizers, prompt, max_sequence_length ) prompt_embeds = prompt_embeds.to(device) pooled_prompt_embeds = pooled_prompt_embeds.to(device) return prompt_embeds, pooled_prompt_embeds def copy_learner_to_ref(transformer): for name, param in transformer.named_parameters(): if "learner" in name: ref_name = name.replace("learner", "ref") ref_param = dict(transformer.named_parameters())[ref_name] ref_param.data.copy_(param.data) def calculate_zero_std_ratio(prompts, gathered_rewards): """ Calculate the proportion of unique prompts whose reward standard deviation is zero. Args: prompts: List of prompts. gathered_rewards: Dictionary containing rewards, must include the key 'ori_avg'. Returns: zero_std_ratio: Proportion of prompts with zero standard deviation. prompt_std_devs: Mean standard deviation across all unique prompts. """ # Convert prompt list to NumPy array prompt_array = np.array(prompts) # Get unique prompts and their group information unique_prompts, inverse_indices, counts = np.unique( prompt_array, return_inverse=True, return_counts=True ) # Group rewards for each prompt grouped_rewards = gathered_rewards['ori_avg'][np.argsort(inverse_indices)] split_indices = np.cumsum(counts)[:-1] reward_groups = np.split(grouped_rewards, split_indices) # Calculate standard deviation for each group prompt_std_devs = np.array([np.std(group) for group in reward_groups]) # Calculate the ratio of zero standard deviation zero_std_count = np.count_nonzero(prompt_std_devs == 0) zero_std_ratio = zero_std_count / len(prompt_std_devs) return zero_std_ratio, prompt_std_devs.mean() def get_sigmas(noise_scheduler, timesteps, accelerator, n_dim=4, dtype=torch.float32): sigmas = noise_scheduler.sigmas.to(device=accelerator.device, dtype=dtype) schedule_timesteps = noise_scheduler.timesteps.to(accelerator.device) timesteps = timesteps.to(accelerator.device) step_indices = [(schedule_timesteps == t).nonzero().item() for t in timesteps] sigma = sigmas[step_indices].flatten() while len(sigma.shape) < n_dim: sigma = sigma.unsqueeze(-1) return sigma def eval(pipeline, test_dataloader, text_encoders, tokenizers, config, accelerator, global_step, reward_fn, executor, autocast, num_train_timesteps, ema, transformer_trainable_parameters): pipeline.transformer.set_adapter("learner") if config.train.ema: ema.copy_ema_to(transformer_trainable_parameters, store_temp=True) neg_prompt_embed, neg_pooled_prompt_embed = compute_text_embeddings([""], text_encoders, tokenizers, max_sequence_length=128, device=accelerator.device) sample_neg_prompt_embeds = neg_prompt_embed.repeat(config.sample.test_batch_size, 1, 1) sample_neg_pooled_prompt_embeds = neg_pooled_prompt_embed.repeat(config.sample.test_batch_size, 1) # test_dataloader = itertools.islice(test_dataloader, 2) all_rewards = defaultdict(list) for test_batch in tqdm( test_dataloader, desc="Eval: ", disable=not accelerator.is_local_main_process, position=0, ): prompts, prompt_metadata = test_batch prompt_embeds, pooled_prompt_embeds = compute_text_embeddings( prompts, text_encoders, tokenizers, max_sequence_length=128, device=accelerator.device ) # The last batch may not be full batch_size if len(prompt_embeds)= config.train.batch_size # assert config.sample.train_batch_size % config.train.batch_size == 0 # assert samples_per_epoch % total_train_batch_size == 0 epoch = 0 global_step = 0 train_iter = iter(train_dataloader) while True: #################### EVAL #################### pipeline.transformer.eval() if epoch % config.eval_freq == 0: eval(pipeline, test_dataloader, text_encoders, tokenizers, config, accelerator, global_step, eval_reward_fn, executor, autocast, num_train_timesteps, ema, transformer_trainable_parameters) if epoch % config.save_freq == 0 and epoch > 0 and accelerator.is_main_process: save_ckpt(config.save_dir, transformer, global_step, accelerator, ema, transformer_trainable_parameters, config) #################### SAMPLING #################### pipeline.transformer.eval() samples = [] prompts = [] for i in tqdm( range(config.sample.num_batches_per_epoch), desc=f"Epoch {epoch}: sampling", disable=not accelerator.is_local_main_process, position=0, ): train_sampler.set_epoch(epoch * config.sample.num_batches_per_epoch + i) prompts, prompt_metadata = next(train_iter) prompt_embeds, pooled_prompt_embeds = compute_text_embeddings( prompts, text_encoders, tokenizers, max_sequence_length=128, device=accelerator.device ) prompt_ids = tokenizers[0]( prompts, padding="max_length", max_length=256, truncation=True, return_tensors="pt", ).input_ids.to(accelerator.device) # sample if global_step>0 and global_step%config.train.ref_update_step==0: copy_learner_to_ref(transformer) with autocast(): with torch.no_grad(): pipeline.transformer.set_adapter("ref") images, latents, log_probs = pipeline_with_logprob( pipeline, prompt_embeds=prompt_embeds, pooled_prompt_embeds=pooled_prompt_embeds, negative_prompt_embeds=sample_neg_prompt_embeds, negative_pooled_prompt_embeds=sample_neg_pooled_prompt_embeds, num_inference_steps=config.sample.num_steps, guidance_scale=config.sample.guidance_scale, output_type="pt", height=config.resolution, width=config.resolution, noise_level=config.sample.noise_level, ) # compute rewards asynchronously rewards = executor.submit(reward_fn, images, prompts, prompt_metadata, only_strict=True) # yield to to make sure reward computation starts time.sleep(0) samples.append( { "prompt_ids": prompt_ids, "prompt_embeds": prompt_embeds, "pooled_prompt_embeds": pooled_prompt_embeds, "latents": latents[-1], "rewards": rewards, } ) # wait for all rewards to be computed for sample in tqdm( samples, desc="Waiting for rewards", disable=not accelerator.is_local_main_process, position=0, ): rewards, reward_metadata = sample["rewards"].result() # accelerator.print(reward_metadata) sample["rewards"] = { key: torch.as_tensor(value, device=accelerator.device).float() for key, value in rewards.items() } # collate samples into dict where each entry has shape (num_batches_per_epoch * sample.batch_size, ...) samples = { k: torch.cat([s[k] for s in samples], dim=0) if not isinstance(samples[0][k], dict) else { sub_key: torch.cat([s[k][sub_key] for s in samples], dim=0) for sub_key in samples[0][k] } for k in samples[0].keys() } if epoch % 10 == 0 and accelerator.is_main_process: # this is a hack to force wandb to log the images as JPEGs instead of PNGs with tempfile.TemporaryDirectory() as tmpdir: num_samples = min(15, len(images)) sample_indices = random.sample(range(len(images)), num_samples) for idx, i in enumerate(sample_indices): image = images[i] pil = Image.fromarray( (image.cpu().numpy().transpose(1, 2, 0) * 255).astype(np.uint8) ) pil = pil.resize((config.resolution, config.resolution)) pil.save(os.path.join(tmpdir, f"{idx}.jpg")) # 使用新的索引 sampled_prompts = [prompts[i] for i in sample_indices] sampled_rewards = [rewards['avg'][i] for i in sample_indices] wandb.log( { "images": [ wandb.Image( os.path.join(tmpdir, f"{idx}.jpg"), caption=f"{prompt:.100} | avg: {avg_reward:.2f}", ) for idx, (prompt, avg_reward) in enumerate(zip(sampled_prompts, sampled_rewards)) ], }, step=global_step, ) samples["rewards"]["ori_avg"] = samples["rewards"]["avg"] samples["rewards"]["avg"] = samples["rewards"]["avg"].unsqueeze(-1) # gather rewards across processes gathered_rewards = {key: accelerator.gather(value) for key, value in samples["rewards"].items()} gathered_rewards = {key: value.cpu().numpy() for key, value in gathered_rewards.items()} # log rewards and images if accelerator.is_main_process: wandb.log( { "epoch": epoch, **{f"reward_{key}": value.mean() for key, value in gathered_rewards.items() if '_strict_accuracy' not in key and '_accuracy' not in key}, }, step=global_step, ) # per-prompt mean/std tracking if config.per_prompt_stat_tracking: # gather the prompts across processes prompt_ids = accelerator.gather(samples["prompt_ids"]).cpu().numpy() prompts = pipeline.tokenizer.batch_decode( prompt_ids, skip_special_tokens=True ) advantages = stat_tracker.update(prompts, gathered_rewards['avg'], type=config.train.algorithm) if accelerator.is_local_main_process: print("len(prompts)", len(prompts)) print("len unique prompts", len(set(prompts))) group_size, trained_prompt_num = stat_tracker.get_stats() zero_std_ratio, reward_std_mean = calculate_zero_std_ratio(prompts, gathered_rewards) if accelerator.is_main_process: wandb.log( { "group_size": group_size, "trained_prompt_num": trained_prompt_num, "zero_std_ratio": zero_std_ratio, }, step=global_step, ) stat_tracker.clear() else: advantages = (gathered_rewards['avg'] - gathered_rewards['avg'].mean()) / (gathered_rewards['avg'].std() + 1e-4) # ungather advantages; we only need to keep the entries corresponding to the samples on this process advantages = torch.as_tensor(advantages) samples["advantages"] = ( advantages.reshape(accelerator.num_processes, -1, advantages.shape[-1])[accelerator.process_index] .to(accelerator.device) ) if accelerator.is_local_main_process: print("advantages: ", samples["advantages"].abs().mean()) del samples["rewards"] del samples["prompt_ids"] total_batch_size = len(samples["latents"]) num_timesteps = config.sample.num_steps #################### TRAINING #################### pipeline.transformer.set_adapter("learner") for inner_epoch in range(config.train.num_inner_epochs): # shuffle samples along batch dimension perm = torch.randperm(total_batch_size, device=accelerator.device) # perm = torch.arange(total_batch_size, device=accelerator.device) samples = {k: v[perm] for k, v in samples.items()} # rebatch for training samples_batched = { k: v.reshape(-1, total_batch_size//config.sample.num_batches_per_epoch, *v.shape[1:]) for k, v in samples.items() } # dict of lists -> list of dicts for easier iteration samples_batched = [ dict(zip(samples_batched, x)) for x in zip(*samples_batched.values()) ] # train pipeline.transformer.train() info = defaultdict(list) for i, sample in tqdm( list(enumerate(samples_batched)), desc=f"Epoch {epoch}.{inner_epoch}: training", position=0, disable=not accelerator.is_local_main_process, ): embeds = sample["prompt_embeds"] pooled_embeds = sample["pooled_prompt_embeds"] with accelerator.accumulate(transformer): model_input = sample["latents"] # Convert images to latent space noise = torch.randn_like(model_input) bsz = model_input.shape[0] # Sample a random timestep for each image # for weighting schemes where we sample timesteps non-uniformly u = compute_density_for_timestep_sampling( weighting_scheme='logit_normal', batch_size=bsz, logit_mean=0, logit_std=1, mode_scale=1.29, ) indices = (u * noise_scheduler.config.num_train_timesteps).long() timesteps = noise_scheduler.timesteps[indices].to(device=model_input.device) # Add noise according to flow matching. # zt = (1 - texp) * x + texp * z1 sigmas = get_sigmas(noise_scheduler, timesteps, accelerator, n_dim=model_input.ndim, dtype=model_input.dtype) noisy_model_input = (1.0 - sigmas) * model_input + sigmas * noise with autocast(): pipeline.transformer.set_adapter("learner") model_pred = transformer( hidden_states=noisy_model_input, timestep=timesteps, encoder_hidden_states=embeds, pooled_projections=pooled_embeds, return_dict=False, )[0] if config.train.beta > 0: with torch.no_grad(): with transformer.module.disable_adapter(): model_pred_ref = transformer( hidden_states=noisy_model_input, timestep=timesteps, encoder_hidden_states=embeds, pooled_projections=pooled_embeds, return_dict=False, )[0] target = noise - model_input fm_loss = ((model_pred.float() - target.float()) ** 2).mean(dim=(1, 2, 3)) info["fm_loss"].append(fm_loss) if config.train.beta > 0: kl_loss = ((model_pred.float() - model_pred_ref.float()) ** 2).mean(dim=(1, 2, 3)) info["kl_loss"].append(kl_loss) loss = (sample["advantages"].squeeze(1)*fm_loss + config.train.beta * kl_loss).mean() else: loss = fm_loss.mean() info["loss"].append(loss) # backward pass accelerator.backward(loss) if accelerator.sync_gradients: accelerator.clip_grad_norm_( transformer.parameters(), config.train.max_grad_norm ) optimizer.step() optimizer.zero_grad() # Checks if the accelerator has performed an optimization step behind the scenes if accelerator.sync_gradients: # assert (j == train_timesteps[-1]) and ( # i + 1 # ) % config.train.gradient_accumulation_steps == 0 # log training-related stuff info = {k: torch.mean(torch.stack(v)) for k, v in info.items()} info = accelerator.reduce(info, reduction="mean") info.update({"epoch": epoch, "inner_epoch": inner_epoch}) if accelerator.is_main_process: wandb.log(info, step=global_step) info = defaultdict(list) global_step += 1 if config.train.ema: ema.step(transformer_trainable_parameters, global_step) epoch+=1 if __name__ == "__main__": app.run(main)