Files
open-instruct/open_instruct/online_dpo_vllm_thread.py
Hamish IvisonandGitHub d04dbc4107 Clean up rlvr a lil, add base support (#538)
* clean up rlvr a lil, add base support

* fix

* lint

* fix

* tweak prompt
2025-02-01 10:37:25 -08:00

1015 lines
48 KiB
Python

import gc
import json
import os
import random
import shutil
import signal
import subprocess
import threading
import time
from dataclasses import asdict, dataclass
from queue import Empty, Queue
from typing import List, Literal, Optional, Tuple
import numpy as np
import pandas as pd
import torch
import torch.nn.functional as F
import torch.optim as optim
import torch.utils
import torch.utils.data
from accelerate import Accelerator
from accelerate.utils import broadcast, gather_object
from datasets import DatasetDict
from huggingface_hub import HfApi
from rich.pretty import pprint
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from transformers import (
AutoConfig,
AutoModelForCausalLM,
AutoModelForSequenceClassification,
AutoTokenizer,
PreTrainedModel,
get_scheduler,
)
from vllm import LLM, SamplingParams
from open_instruct.dataset_processor import (
CHAT_TEMPLATES,
INPUT_IDS_PROMPT_KEY,
DatasetConfig,
SFTDatasetProcessor,
SimpleGenerateCollator,
visualize_token,
)
from open_instruct.model_utils import (
ModelConfig,
disable_dropout_in_model,
exact_div,
first_true_indices,
forward,
get_reward,
prepare_deepspeed,
print_rich_single_line_metrics,
print_rich_table,
push_folder_to_hub,
save_with_accelerate,
truncate_response,
unwrap_model_for_generation,
)
from open_instruct.utils import (
ArgumentParserPlus,
combine_dataset,
get_wandb_tags,
is_beaker_job,
maybe_get_beaker_config,
maybe_use_ai2_hf_entity,
maybe_use_ai2_wandb_entity,
upload_metadata_to_hf,
)
from open_instruct.vllm_utils import vllm_single_gpu_patch
api = HfApi()
INVALID_LOGPROB = 1.0
@dataclass
class Args:
# required dataset args
dataset_mixer: str = None
"""A dictionary of datasets (local or HF) to sample from."""
dataset_train_splits: List[str] = None
"""The dataset splits to use for training"""
dataset_eval_mixer: Optional[str] = None
"""A dictionary of datasets (local or HF) to sample from for evaluation"""
dataset_eval_splits: Optional[List[str]] = None
"""The dataset splits to use for evaluation"""
dataset_mixer_dict: Optional[dict] = None
"""The dataset mixer as a dictionary"""
dataset_eval_mixer_dict: Optional[dict] = None
"""The dataset eval mixer as a dictionary"""
# common args
exp_name: str = os.path.basename(__file__)[: -len(".py")]
"""The name of this experiment"""
seed: int = 1
"""Seed of the experiment"""
run_name: Optional[str] = None
"""A unique name of this run"""
# optimizer args
eps: float = 1e-5
"""The epsilon value for the optimizer"""
learning_rate: float = 2e-5
"""The initial learning rate for AdamW optimizer."""
lr_scheduler_type: Literal[
"linear", "cosine", "cosine_with_restarts", "polynomial", "constant", "constant_with_warmup"
] = "linear"
"""Which scheduler to use"""
warm_up_steps: int = 0
"""Number of warm up steps for the scheduler"""
# various batch sizes
num_train_epochs: int = 1
"""Number of epochs to train"""
gradient_accumulation_steps: int = 8
"""The number of gradient accumulation steps"""
per_device_train_batch_size: Optional[int] = 1
"""The forward batch size per device (local_micro_batch_size)"""
per_device_eval_batch_size: Optional[int] = 1
"""The forward batch size per device for evaluation (local_micro_batch_size)"""
total_episodes: Optional[int] = 100000
"""The total number of episodes in the dataset"""
world_size: Optional[int] = None
"""The number of processes (GPUs) to use"""
micro_batch_size: Optional[int] = None
"""The micro batch size across devices (HF's `per_device_train_batch_size` * `world_size`)"""
local_batch_size: Optional[int] = None
"""The batch size per GPU (HF's `per_device_train_batch_size` * `gradient_accumulation_steps`)"""
batch_size: Optional[int] = None
"""The batch size across devices (HF's `per_device_train_batch_size` * `world_size` * `gradient_accumulation_steps`)"""
num_training_steps: Optional[int] = None
"""The number of training_steps to train"""
num_evals: int = 4
"""The number of evaluations to run throughout training"""
eval_freq: Optional[int] = None
"""The frequency of evaluation steps"""
local_dataloader_batch_size: Optional[int] = None
"""The batch size per GPU for the dataloader"""
# online settings
num_epochs: int = 4
"""the number of epochs to train"""
num_mini_batches: int = 1
"""Number of minibatches to split a batch into"""
local_mini_batch_size: Optional[int] = None
"""the mini batch size per GPU"""
mini_batch_size: Optional[int] = None
"""the mini batch size across GPUs"""
local_rollout_forward_batch_size: int = 64
"""per rank no grad forward pass in the rollout phase"""
reward_model_path: str = "EleutherAI/pythia-160m"
"""the path to the reward model"""
reward_model_revision: Optional[str] = None
"""the revision of the reward model"""
# generation config
response_length: int = 53
"""the length of the response"""
stop_token: Optional[Literal["eos", "period"]] = None
"""the stop token"""
stop_token_id: Optional[int] = None
"""the truncation token id"""
min_response_length: int = 0
"""stop only after this many tokens"""
temperature: float = 0.7
"""the sampling temperature"""
penalty_reward_value: float = -1.0
"""the reward value for responses that do not contain `stop_token_id`"""
non_stop_penalty: bool = False
"""whether to penalize responses that do not contain `stop_token_id`"""
# online DPO specific args
beta: float = 0.05
"""the beta value of the RLHF objective (KL coefficient)"""
num_generation_per_prompt: int = 2
"""the number of generations per prompt (currently only support 2)"""
loss_type: Literal["sigmoid", "ipo"] = "sigmoid"
"""the loss type for the DPO algorithm"""
# vLLM settings. NOTE: currently we need to place the vLLM model on a separate GPU
# for generation to work properly because vLLM would pre-alocate the memory.
# To do so, we would need to do a moneky patch `vllm_single_gpu_patch` to make sure
# the vLLM model is placed on the correct GPU.
vllm_device: str = "cuda:1"
"""the device placement of the vllm model; typically we place the vllm model on a decicated GPU"""
vllm_gpu_memory_utilization: float = 0.8
"""the GPU memory utilization of the vllm model; passed to `gpu_memory_utilization` to the `vLLM` instance"""
# async setting
async_mode: bool = True
"""Whether to run the generation in async mode which learns from the second latest policy like Cleanba (https://arxiv.org/abs/2310.00036)"""
# wandb and HF tracking configs
with_tracking: bool = False
"""If toggled, this experiment will be tracked with Weights and Biases"""
wandb_project_name: str = "open_instruct_internal"
"""The wandb's project name"""
wandb_entity: Optional[str] = None
"""The entity (team) of wandb's project"""
push_to_hub: bool = True
"""Whether to upload the saved model to huggingface"""
hf_entity: Optional[str] = None
"""The user or org name of the model repository from the Hugging Face Hub"""
hf_repo_id: Optional[str] = None
"""The id of the saved model in the Hugging Face Hub (can be autoset if not given)"""
hf_repo_revision: Optional[str] = None
"""The revision of the saved model in the Hugging Face Hub (can be autoset if not given)"""
hf_repo_url: Optional[str] = None
"""The url of the saved model in the Hugging Face Hub (will be autoset)"""
output_dir: Optional[str] = None
"""Where to save the model"""
checkpoint_output_dir: Optional[str] = None
"""Where to save the model checkpoints in case of preemption"""
# Ai2 specific settings
try_launch_beaker_eval_jobs: bool = True
"""Whether to launch beaker evaluation jobs after training"""
hf_metadata_dataset: Optional[str] = "allenai/tulu-3-evals"
"""What dataset to upload the metadata to. If unset, don't upload metadata"""
def __post_init__(self):
self.dataset_mixer_dict, self.dataset_mixer = process_dataset_mixer(self.dataset_mixer)
if self.dataset_eval_mixer is not None:
self.dataset_eval_mixer_dict, self.dataset_eval_mixer = process_dataset_mixer(self.dataset_eval_mixer)
def process_dataset_mixer(value) -> Tuple[Optional[dict], Optional[str]]:
# if passed through cli: convert the dataset mixers to dictionaries
if isinstance(value, str):
return json.loads(value), value
# if passed through yaml: convert the dataset mixers to strings
elif isinstance(value, dict):
return value, json.dumps(value)
else:
raise ValueError("Input must be either a string or a dictionary")
def calculate_runtime_args_and_accelerator(args: Args, model_config: ModelConfig) -> Accelerator:
"""calculate (in-place) runtime args such as the effective batch size, word size, etc."""
accelerator = Accelerator(gradient_accumulation_steps=args.gradient_accumulation_steps)
args.world_size = accelerator.num_processes
args.local_batch_size = args.per_device_train_batch_size * args.gradient_accumulation_steps * args.num_mini_batches
args.micro_batch_size = int(args.per_device_train_batch_size * args.world_size)
args.batch_size = int(args.local_batch_size * args.world_size)
time_tensor = torch.tensor(int(time.time()), device=accelerator.device)
# set a unique run name with the current timestamp
time_int = broadcast(time_tensor, 0).item()
args.run_name = f"{args.exp_name}__{args.seed}__{time_int}"
args.mini_batch_size = exact_div(
args.batch_size, args.num_mini_batches, "`batch_size` must be a multiple of `num_mini_batches`"
)
args.local_mini_batch_size = exact_div(
args.local_batch_size, args.num_mini_batches, "`local_batch_size` must be a multiple of `num_mini_batches`"
)
args.num_training_steps = args.total_episodes // args.batch_size
args.eval_freq = max(1, args.num_training_steps // args.num_evals)
# DPO logic: repeats the same prompt `num_generation_per_prompt` times
args.local_dataloader_batch_size = exact_div(
args.local_batch_size,
args.num_generation_per_prompt,
"`local_batch_size` must be a multiple of `num_generation_per_prompt`",
)
if args.push_to_hub:
if args.hf_repo_id is None: # auto-generate one
args.hf_repo_id = "open_instruct_dev"
if args.hf_entity is None: # first try to use AI2 entity
args.hf_entity = maybe_use_ai2_hf_entity()
if args.hf_entity is None: # then try to use the user's entity
args.hf_entity = HfApi().whoami()["name"]
args.hf_repo_id = f"{args.hf_entity}/{args.hf_repo_id}"
if args.hf_repo_revision is None: # auto-generate one
args.hf_repo_revision = args.run_name
args.hf_repo_url = f"https://huggingface.co/{args.hf_repo_id}/tree/{args.hf_repo_revision}"
if args.with_tracking and accelerator.is_main_process:
if args.wandb_entity is None:
args.wandb_entity = maybe_use_ai2_wandb_entity()
return accelerator
def vllm_generate(
model_name_or_path: str,
model_revision: Optional[str],
max_model_len: int,
vllm_device: str,
vllm_gpu_memory_utilization: float,
generation_config: SamplingParams,
response_ids_Q: Queue,
param_prompt_Q: Queue,
num_training_steps: int,
sample_evaluation_prompt_token_ids: Optional[List[int]],
evaluation_Q: Queue,
eval_freq: int,
resume_training_step: int,
):
vllm_single_gpu_patch()
llm = LLM(
model=model_name_or_path,
revision=model_revision,
tokenizer_revision=model_revision,
tensor_parallel_size=1,
device=vllm_device,
gpu_memory_utilization=vllm_gpu_memory_utilization,
max_model_len=max_model_len,
)
print("🔥🔥🔥 vllm loaded")
llmp = llm.llm_engine.model_executor.driver_worker.model_runner.model
for training_step in range(resume_training_step, num_training_steps + 1):
items = param_prompt_Q.get()
if items is None:
break
unwrapped_model, g_queries_list = items
if unwrapped_model is not None:
start_time = time.time()
llmp.load_weights(unwrapped_model.named_parameters())
print(
f"🔥🔥🔥 Loading weights using shared memory; Time to load weights: {time.time() - start_time:.2f} seconds"
)
generation_start_time = time.time()
outputs = llm.generate(prompt_token_ids=g_queries_list, sampling_params=generation_config)
response_ids = [list(output.outputs[0].token_ids) for output in outputs]
print(f"🔥🔥🔥 Generation time: {time.time() - generation_start_time:.2f} seconds")
response_ids_Q.put(response_ids)
if sample_evaluation_prompt_token_ids is not None and (training_step - 1) % eval_freq == 0:
outputs = llm.generate(
prompt_token_ids=sample_evaluation_prompt_token_ids, sampling_params=generation_config
)
response_ids = [list(output.outputs[0].token_ids) for output in outputs]
evaluation_Q.put(response_ids)
def send_queries(accelerator, unwrapped_model, tokenizer, param_prompt_Q, queries):
g_queries_list = gather_object(queries.tolist())
if accelerator.is_main_process:
g_queries_list = [
[inneritem for inneritem in item if inneritem != tokenizer.pad_token_id] for item in g_queries_list
] # remove padding
param_prompt_Q.put((unwrapped_model, g_queries_list))
def main(args: Args, dataset_config: DatasetConfig, model_config: ModelConfig):
accelerator = calculate_runtime_args_and_accelerator(args, model_config)
local_seed = args.seed + accelerator.process_index
# set up experiment tracking and seeds
all_configs = {}
if is_beaker_job():
args.checkpoint_output_dir = os.environ.get("CHECKPOINT_OUTPUT_DIR", args.output_dir)
beaker_config = maybe_get_beaker_config()
# try saving to the beaker `/output`, which will be uploaded to the beaker dataset
if len(beaker_config.beaker_dataset_id_urls) > 0:
args.output_dir = "/output"
all_configs.update(vars(beaker_config))
all_configs.update(**asdict(args), **asdict(dataset_config), **asdict(model_config))
if accelerator.is_main_process:
if args.with_tracking:
import wandb
wandb.init(
project=args.wandb_project_name,
entity=args.wandb_entity,
sync_tensorboard=True,
config=all_configs,
name=args.run_name,
save_code=True,
tags=[args.exp_name] + get_wandb_tags(),
)
writer = SummaryWriter(f"runs/{args.run_name}")
writer.add_text(
"hyperparameters",
"|param|value|\n|-|-|\n%s" % ("\n".join([f"|{key}|{value}|" for key, value in vars(args).items()])),
)
device = torch.device(f"cuda:{accelerator.local_process_index}")
random.seed(local_seed)
np.random.seed(local_seed)
torch.manual_seed(local_seed)
torch.backends.cudnn.deterministic = True
# create a tokenizer (pad from right)
config = AutoConfig.from_pretrained(model_config.model_name_or_path, revision=model_config.model_revision)
tokenizer = AutoTokenizer.from_pretrained(
model_config.model_name_or_path, revision=model_config.model_revision, padding_side="right"
)
if config.architectures == "LlamaForCausalLM" and config.bos_token_id == 128000:
tokenizer.pad_token_id = 128002 # <|reserved_special_token_0|>
else:
tokenizer.add_special_tokens({"pad_token": "[PAD]"}) # NOTE: we do not resize the embedding
if dataset_config.chat_template is not None:
tokenizer.chat_template = CHAT_TEMPLATES[dataset_config.chat_template]
# create the dataset
dataset_dict = DatasetDict()
dataset_processor = SFTDatasetProcessor(tokenizer=tokenizer, config=dataset_config)
if len(args.dataset_train_splits) != len(args.dataset_mixer_dict) and len(args.dataset_train_splits) == 1:
args.dataset_train_splits = [args.dataset_train_splits[0]] * len(args.dataset_mixer_dict)
print(f"Dataset splits not provided for all datasets. Using the same {args.dataset_train_splits[0]} split for all datasets.")
if len(args.dataset_eval_splits) != len(args.dataset_eval_mixer_dict) and len(args.dataset_eval_splits) == 1:
args.dataset_eval_splits = [args.dataset_eval_splits[0]] * len(args.dataset_eval_mixer_dict)
print(f"Dataset splits not provided for all datasets. Using the same {args.dataset_eval_splits[0]} split for all datasets.")
train_dataset = combine_dataset(
args.dataset_mixer_dict,
splits=args.dataset_train_splits,
columns_to_keep=[dataset_config.sft_messages_key],
)
if dataset_config.sanity_check:
train_dataset = train_dataset.select(
range(0, min(len(train_dataset), dataset_config.sanity_check_max_samples))
)
with accelerator.main_process_first():
train_dataset = dataset_processor.tokenize(train_dataset)
train_dataset = dataset_processor.filter(train_dataset)
dataset_dict["train"] = train_dataset
eval_dataset = None
if args.dataset_eval_mixer is not None:
eval_dataset = combine_dataset(
args.dataset_eval_mixer_dict,
splits=args.dataset_eval_splits,
columns_to_keep=[dataset_config.sft_messages_key],
)
eval_dataset = eval_dataset.select(range(0, min(len(eval_dataset), dataset_config.sanity_check_max_samples)))
with accelerator.main_process_first():
eval_dataset = dataset_processor.tokenize(eval_dataset)
eval_dataset = dataset_processor.filter(eval_dataset)
dataset_dict["eval"] = eval_dataset
# some more runtime logging
if accelerator.is_main_process:
pprint([args, dataset_config, model_config])
visualize_token(train_dataset[0][INPUT_IDS_PROMPT_KEY], tokenizer)
if args.with_tracking:
# upload the visualized token length
dataset_processor.get_token_length_visualization(
dataset_dict, save_path=f"runs/{args.run_name}/token_length.png"
)
wandb.log({"token_length": wandb.Image(f"runs/{args.run_name}/token_length.png")})
# create the model and optimizer
policy: PreTrainedModel = AutoModelForCausalLM.from_pretrained(
model_config.model_name_or_path,
revision=model_config.model_revision,
torch_dtype=torch.bfloat16,
attn_implementation="flash_attention_2",
use_cache=False,
)
ref_model: PreTrainedModel = AutoModelForCausalLM.from_pretrained(
model_config.model_name_or_path,
revision=model_config.model_revision,
torch_dtype=torch.bfloat16,
attn_implementation="flash_attention_2",
use_cache=False,
)
reward_model: PreTrainedModel = AutoModelForSequenceClassification.from_pretrained(
args.reward_model_path,
revision=args.reward_model_revision,
num_labels=1,
torch_dtype=torch.bfloat16,
attn_implementation="flash_attention_2",
use_cache=False,
)
if policy.config.vocab_size != reward_model.config.vocab_size:
raise ValueError(
"Policy and reward model must have the same vocab size. "
f"Policy: {policy.config.vocab_size}, Reward: {reward_model.config.vocab_size}. "
"If they don't have the same vocab size, the policy could generate tokens which "
"is going to cause index out of bound error in the reward model."
)
model = policy
if model_config.gradient_checkpointing:
model.gradient_checkpointing_enable()
for module in [model, ref_model, reward_model]:
disable_dropout_in_model(module)
if args.stop_token:
if args.stop_token == "eos":
args.stop_token_id = tokenizer.eos_token_id
if args.stop_token == "period":
args.stop_token_id = tokenizer.encode(".")[0]
optimizer = optim.AdamW(model.parameters(), lr=args.learning_rate, eps=args.eps)
scheduler = get_scheduler(
args.lr_scheduler_type,
optimizer=optimizer,
num_warmup_steps=args.warm_up_steps,
num_training_steps=args.num_training_steps * args.num_train_epochs,
)
data_collator = SimpleGenerateCollator(pad_token_id=tokenizer.pad_token_id)
dataloader = DataLoader(
train_dataset,
batch_size=args.local_dataloader_batch_size,
shuffle=True,
collate_fn=data_collator,
drop_last=True, # needed; otherwise the last batch will be of ragged shape
)
# sync random states for DataLoader(shuffle=True) before `accelerator.prepare`
# see https://gist.github.com/vwxyzjn/2581bff1e48e185e0b85b6dfe1def79c
torch.manual_seed(args.seed)
model, optimizer, dataloader = accelerator.prepare(model, optimizer, dataloader)
torch.manual_seed(local_seed)
# resume from preemption
resume_training_step = 1
if os.path.exists(args.checkpoint_output_dir):
for item in os.listdir(args.checkpoint_output_dir):
print(item)
if "step_" in item:
old_checkpoint_path = os.path.join(args.checkpoint_output_dir, item)
# check if the directory is empty
if len(os.listdir(old_checkpoint_path)) == 0:
continue
accelerator.load_state(old_checkpoint_path)
resume_training_step = int(item.split("_")[-1])
print("Resuming training from step", resume_training_step)
if accelerator.is_main_process:
shutil.rmtree(old_checkpoint_path)
break
resume_training_step > 1
# handle preemption
class PreemptionHandler:
preempted = False
def __init__(self):
signal.signal(signal.SIGTERM, self.exit_gracefully)
def exit_gracefully(self, signum, frame):
output_dir = os.path.join(args.checkpoint_output_dir, f"step_{training_step - 1}")
print(f"SIGTERM received, saving to {output_dir} from {accelerator.local_process_index}")
accelerator.save_state(output_dir)
if accelerator.is_main_process and args.with_tracking:
wandb.log({"preempted": True}, commit=True)
wandb.mark_preempting()
if accelerator.is_main_process:
try:
param_prompt_Q.put(None, timeout=20)
response_ids_Q.get(timeout=20)
print("vllm thread terminated")
except Exception as e:
print(e)
self.preempted = True
ph = PreemptionHandler()
# deepspeed setup
is_deepspeed_enabled = getattr(accelerator.state, "deepspeed_plugin", None) is not None
mixed_precision = accelerator.state.mixed_precision
if is_deepspeed_enabled:
reward_model = prepare_deepspeed(reward_model, args.per_device_train_batch_size, mixed_precision)
ref_model = prepare_deepspeed(ref_model, args.per_device_train_batch_size, mixed_precision)
else:
reward_model = reward_model.to(device)
ref_model = ref_model.to(device)
# online generation config
def repeat_generator():
while True:
yield from dataloader
iter_dataloader = iter(repeat_generator())
generation_config = SamplingParams(
temperature=args.temperature,
top_p=1.0,
max_tokens=args.response_length,
include_stop_str_in_output=True,
)
param_prompt_Q = None
response_ids_Q = None
evaluation_Q = None
if accelerator.is_main_process:
response_ids_Q = Queue(maxsize=1)
param_prompt_Q = Queue(maxsize=1)
evaluation_Q = Queue(maxsize=1)
LOCAL_NUM_EVAL_SAMPLES = 4
num_eval_samples = LOCAL_NUM_EVAL_SAMPLES * accelerator.num_processes
sample_evaluation_prompt_token_ids = None
if eval_dataset is not None:
sample_evaluation_prompt_token_ids = eval_dataset[:num_eval_samples][INPUT_IDS_PROMPT_KEY]
thread = threading.Thread(
target=vllm_generate,
args=(
model_config.model_name_or_path,
model_config.model_revision,
dataset_config.max_prompt_token_length + args.response_length,
args.vllm_device,
args.vllm_gpu_memory_utilization,
generation_config,
response_ids_Q,
param_prompt_Q,
args.num_training_steps,
sample_evaluation_prompt_token_ids,
evaluation_Q,
args.eval_freq,
resume_training_step,
),
)
thread.start()
torch.cuda.set_device(device)
g_vllm_responses = torch.zeros((args.batch_size, args.response_length), device=device, dtype=torch.long)
# set up the metrics and initial states
stats_shape = (args.num_epochs, args.num_mini_batches, args.gradient_accumulation_steps)
loss_stats = torch.zeros(stats_shape, device=device)
chosen_rewards_stats = torch.zeros(stats_shape, device=device)
rejected_rewards_stats = torch.zeros(stats_shape, device=device)
chosen_logprobs_stats = torch.zeros(stats_shape, device=device)
rejected_logprobs_stats = torch.zeros(stats_shape, device=device)
local_metrics = torch.zeros((20,), device=device)
episode = args.batch_size * (resume_training_step - 1)
model.train()
# training loop
start_time = time.time()
data = next(iter_dataloader)
queries_next = data[INPUT_IDS_PROMPT_KEY].to(device)
queries_next = queries_next.repeat(args.num_generation_per_prompt, 1)
send_queries(accelerator, None, tokenizer, param_prompt_Q, queries_next)
for _ in range(1, resume_training_step): # we didn't store scheduler state
scheduler.step()
for training_step in range(resume_training_step, args.num_training_steps + 1):
episode += args.batch_size
scheduler.step()
queries = queries_next
if ph.preempted:
break
if accelerator.is_main_process:
try:
evaluation_responses = evaluation_Q.get(timeout=0.01)
print("🔥🔥🔥 Evaluation responses received")
table = {}
table["prompt"] = tokenizer.batch_decode(sample_evaluation_prompt_token_ids)
table["response"] = tokenizer.batch_decode(evaluation_responses)
table["response"] = [item.replace(tokenizer.pad_token, "") for item in table["response"]]
df = pd.DataFrame(table)
print_rich_table(df)
if args.with_tracking:
wandb.log({"sample_completions": wandb.Table(dataframe=df)})
else:
print_rich_table(df)
del table
except Empty:
print("🙈 Evaluation responses not received")
with unwrap_model_for_generation(model, accelerator) as unwrapped_model:
# (optionally) evaluate the model
generation_model = unwrapped_model
if args.async_mode:
if training_step != 1:
data = next(iter_dataloader)
queries_next = data[INPUT_IDS_PROMPT_KEY].to(device)
queries_next = queries_next.repeat(args.num_generation_per_prompt, 1)
send_queries(accelerator, generation_model, tokenizer, param_prompt_Q, queries_next)
else:
if training_step != 1:
# NOTE: important: the indent here is different for sync mode
# we also set to use `queries = queries_next` immediately
data = next(iter_dataloader)
queries_next = data[INPUT_IDS_PROMPT_KEY].to(device)
queries_next = queries_next.repeat(args.num_generation_per_prompt, 1)
send_queries(accelerator, generation_model, tokenizer, param_prompt_Q, queries_next)
queries = queries_next
training_time_start = time.time()
with torch.no_grad():
context_length = queries.shape[1]
responses = []
postprocessed_responses = []
ref_logprobs = []
scores = []
sequence_lengths = []
if accelerator.is_main_process:
g_response_token_ids = response_ids_Q.get()
DUMMY_PAD_TOKEN = 0 # we can't use tokenizer.pad_token_id because it's outside vocab and `torch.gather(all_logprob, 2, response.unsqueeze(-1))` will error out
g_padded_response_ids = [
response + [DUMMY_PAD_TOKEN] * (args.response_length - len(response))
for response in g_response_token_ids
]
for item in g_padded_response_ids:
assert len(item) == args.response_length
for inner_item in item:
if not inner_item < config.vocab_size:
assert inner_item < config.vocab_size, f"{inner_item=}, {tokenizer.vocab_size=}"
g_padded_response_ids = torch.tensor(g_padded_response_ids, device=device)
g_vllm_responses[:] = g_padded_response_ids
broadcast(g_vllm_responses, 0)
local_vllm_responses = g_vllm_responses[
accelerator.process_index
* queries.shape[0] : (accelerator.process_index + 1)
* queries.shape[0]
]
query_responses = torch.cat((queries, local_vllm_responses), 1)
for i in range(0, queries.shape[0], args.local_rollout_forward_batch_size):
query = queries[i : i + args.local_rollout_forward_batch_size]
query_response = query_responses[i : i + args.local_rollout_forward_batch_size]
response = query_response[:, context_length:]
ref_output = forward(ref_model, query_response, tokenizer.pad_token_id)
ref_logits = ref_output.logits[:, context_length - 1 : -1]
ref_logits /= args.temperature + 1e-7
ref_all_logprob = F.log_softmax(ref_logits, dim=-1)
ref_logprob = torch.gather(ref_all_logprob, 2, response.unsqueeze(-1)).squeeze(-1)
del ref_output, ref_logits, ref_all_logprob
torch.cuda.empty_cache()
# Response Processing 1. truncate response after the first occurrence of `stop_token_id`
postprocessed_response = response
if args.stop_token_id is not None: # handle the edge case when stop_token_id exists but is 0
postprocessed_response = truncate_response(
args.stop_token_id, tokenizer.pad_token_id, response
)
# Response Processing 2. run reward model on the truncated responses
postprocessed_query_response = torch.cat((query, postprocessed_response), 1)
sequence_length = first_true_indices(postprocessed_response == tokenizer.pad_token_id) - 1
_, score, _ = get_reward(
reward_model, postprocessed_query_response, tokenizer.pad_token_id, context_length
)
responses.append(response)
postprocessed_responses.append(postprocessed_response)
ref_logprobs.append(ref_logprob)
sequence_lengths.append(sequence_length)
scores.append(score)
responses = torch.cat(responses, 0)
postprocessed_responses = torch.cat(postprocessed_responses, 0)
ref_logprobs = torch.cat(ref_logprobs, 0)
sequence_lengths = torch.cat(sequence_lengths, 0)
scores = torch.cat(scores, 0)
accelerator.gather(scores)
del (ref_logprob, score)
gc.collect()
torch.cuda.empty_cache()
# Response Processing 3. filter response. Ensure that the sample contains stop_token_id
# responses not passing that filter will receive a low (fixed) score
# only query humans on responses that pass that filter
contain_stop_token = torch.any(postprocessed_responses == args.stop_token_id, dim=-1)
# NOTE: only apply the stop token filter if the response is long enough
# otherwise the model could learn to generate the first token as the stop token
contain_stop_token = contain_stop_token & (sequence_lengths >= args.min_response_length)
if args.non_stop_penalty:
scores = torch.where(
contain_stop_token, scores, torch.full_like(scores, args.penalty_reward_value)
)
# be very careful with `padding_mask_p1`; see https://excalidraw.com/#json=LWnzG4w2k5DjF_EOL_xPt,e2w3a-hFJ_gX5vOfeyXGTw
response_idxs = torch.arange(responses.shape[1], device=responses.device).repeat(responses.shape[0], 1)
padding_mask = response_idxs > sequence_lengths.unsqueeze(1)
ref_logprobs = torch.masked_fill(ref_logprobs, padding_mask, INVALID_LOGPROB)
# num_examples should be same as args.local_batch_size divided by 2
num_examples = scores.size(0) // 2
first_half = scores[:num_examples]
second_half = scores[num_examples:]
num_examples_range = torch.arange(num_examples).to(scores.device)
chosen_indices = torch.where(
first_half >= second_half, num_examples_range.clone(), num_examples_range.clone() + num_examples
)
rejected_indices = torch.where(
first_half < second_half, num_examples_range.clone(), num_examples_range.clone() + num_examples
)
scores_margin = scores[chosen_indices] - scores[rejected_indices]
logprobs = []
concat_indices = []
# Do multiple epochs of training on on-policy data (PPO-style), with a fresh random shuffle in each epoch
for epoch_idx in range(args.num_epochs):
b_inds = np.random.permutation(args.local_batch_size // args.num_generation_per_prompt)
minibatch_idx = 0
for mini_batch_start in range(
0,
args.local_batch_size // args.num_generation_per_prompt,
args.local_mini_batch_size // args.num_generation_per_prompt,
):
mini_batch_end = mini_batch_start + args.local_mini_batch_size // args.num_generation_per_prompt
mini_batch_inds = b_inds[mini_batch_start:mini_batch_end]
gradient_accumulation_idx = 0
for micro_batch_start in range(
0,
args.local_mini_batch_size // args.num_generation_per_prompt,
args.per_device_train_batch_size,
):
with accelerator.accumulate(model):
micro_batch_end = micro_batch_start + args.per_device_train_batch_size
micro_batch_inds = mini_batch_inds[micro_batch_start:micro_batch_end]
chosen_mb_inds = chosen_indices[micro_batch_inds]
chosen_responses = responses[chosen_mb_inds]
rejected_mb_inds = rejected_indices[micro_batch_inds]
rejected_responses = responses[rejected_mb_inds]
concat_mb_inds = torch.cat((chosen_mb_inds, rejected_mb_inds), dim=0)
concat_query_responses = query_responses[concat_mb_inds]
concat_output = forward(model, concat_query_responses, tokenizer.pad_token_id)
num_examples = chosen_mb_inds.shape[0]
chosen_logits = concat_output.logits[:num_examples]
rejected_logits = concat_output.logits[num_examples:]
# chosen
chosen_logits = chosen_logits[:, context_length - 1 : -1]
chosen_logits /= args.temperature + 1e-7
chosen_all_logprobs = F.log_softmax(chosen_logits, dim=-1)
chosen_logprobs = torch.gather(chosen_all_logprobs, 2, chosen_responses.unsqueeze(-1)).squeeze(
-1
)
chosen_logprobs = torch.masked_fill(
chosen_logprobs, padding_mask[chosen_mb_inds], INVALID_LOGPROB
)
chosen_ref_logprobs = ref_logprobs[chosen_mb_inds]
chosen_logprobs_sum = (chosen_logprobs * ~padding_mask[chosen_mb_inds]).sum(1)
chosen_ref_logprobs_sum = (chosen_ref_logprobs * ~padding_mask[chosen_mb_inds]).sum(1)
# rejected
rejected_logits = rejected_logits[:, context_length - 1 : -1]
rejected_logits /= args.temperature + 1e-7
rejected_all_logprobs = F.log_softmax(rejected_logits, dim=-1)
rejected_logprobs = torch.gather(
rejected_all_logprobs, 2, rejected_responses.unsqueeze(-1)
).squeeze(-1)
rejected_logprobs = torch.masked_fill(
rejected_logprobs, padding_mask[rejected_mb_inds], INVALID_LOGPROB
)
rejected_ref_logprobs = ref_logprobs[rejected_mb_inds]
rejected_logprobs_sum = (rejected_logprobs * ~padding_mask[rejected_mb_inds]).sum(1)
rejected_ref_logprobs_sum = (rejected_ref_logprobs * ~padding_mask[rejected_mb_inds]).sum(1)
pi_logratios = chosen_logprobs_sum - rejected_logprobs_sum
ref_logratios = chosen_ref_logprobs_sum - rejected_ref_logprobs_sum
logits = pi_logratios - ref_logratios
if args.loss_type == "sigmoid":
losses = -F.logsigmoid(args.beta * logits)
elif args.loss_type == "ipo":
losses = (logits - 1 / (2 * args.beta)) ** 2
else:
raise NotImplementedError(f"invalid loss type {args.loss_type}")
loss = losses.mean()
accelerator.backward(loss)
optimizer.step()
optimizer.zero_grad()
with torch.no_grad():
if epoch_idx == 0:
concat_indices.append(concat_mb_inds)
response = concat_query_responses[:, context_length:]
logits = concat_output.logits[:, context_length - 1 : -1]
logits /= args.temperature + 1e-7
all_logprob = F.log_softmax(logits, dim=-1)
logprob = torch.gather(all_logprob, 2, response.unsqueeze(-1)).squeeze(-1)
logprob = torch.masked_fill(logprob, padding_mask[concat_mb_inds], INVALID_LOGPROB)
logprobs.append(logprob)
del all_logprob
chosen_rewards = args.beta * (chosen_logprobs_sum - chosen_ref_logprobs_sum)
rejected_rewards = args.beta * (rejected_logprobs_sum - rejected_ref_logprobs_sum)
loss_stats[epoch_idx, minibatch_idx, gradient_accumulation_idx] = loss
chosen_rewards_stats[epoch_idx, minibatch_idx, gradient_accumulation_idx] = (
chosen_rewards.mean()
)
rejected_rewards_stats[epoch_idx, minibatch_idx, gradient_accumulation_idx] = (
rejected_rewards.mean()
)
chosen_logprobs_stats[epoch_idx, minibatch_idx, gradient_accumulation_idx] = (
chosen_logprobs_sum.mean()
)
rejected_logprobs_stats[epoch_idx, minibatch_idx, gradient_accumulation_idx] = (
rejected_logprobs_sum.mean()
)
gradient_accumulation_idx += 1
minibatch_idx += 1
# fmt: off
del (
loss, logits, concat_output, concat_query_responses,
chosen_logits, rejected_logits, chosen_logprobs, rejected_logprobs,
chosen_responses, rejected_responses,
)
# fmt: on
# del everything and empty cache
torch.cuda.empty_cache()
with torch.no_grad():
logprobs = torch.cat(logprobs, 0)
concat_indices = torch.cat(concat_indices, 0)
restore_logprobs = torch.zeros_like(logprobs)
restore_logprobs[concat_indices] = logprobs
kl = restore_logprobs - ref_logprobs
non_score_reward = -args.beta * kl
non_score_reward_sum = non_score_reward.sum(1)
rlhf_reward = scores + non_score_reward_sum
local_metrics[0] = sequence_lengths.float().mean()
local_metrics[1] = (responses == args.stop_token_id).sum().float().mean()
local_metrics[2] = kl.sum(1).mean()
local_metrics[3] = (-logprobs).sum(1).mean()
local_metrics[4] = non_score_reward_sum.mean()
local_metrics[5] = rlhf_reward.mean()
local_metrics[6] = scores.mean()
local_metrics[7] = scores_margin.mean()
local_metrics[8] = loss_stats.mean()
local_metrics[9] = chosen_rewards_stats.mean()
local_metrics[10] = rejected_rewards_stats.mean()
local_metrics[11] = (chosen_rewards_stats > rejected_rewards_stats).float().mean()
local_metrics[12] = (chosen_rewards_stats - rejected_rewards_stats).mean()
local_metrics[13] = chosen_logprobs_stats.mean()
local_metrics[14] = rejected_logprobs_stats.mean()
local_metrics[15] = ((kl) ** 2 / 2).sum(1).mean()
local_metrics[16] = ((-kl).exp() - 1 + kl).sum(1).mean()
global_metrics = accelerator.reduce(local_metrics, reduction="mean").tolist()
metrics = {
"episode": episode,
"training_step": training_step,
"lr": scheduler.get_last_lr()[0],
"epoch": episode / len(train_dataset),
"time/from_scratch": time.time() - start_time,
"time/training": time.time() - training_time_start,
"val/sequence_lengths": global_metrics[0],
"val/num_stop_token_ids": global_metrics[1],
"objective/kl": global_metrics[2],
"objective/kl2": global_metrics[15],
"objective/kl3": global_metrics[16],
"objective/entropy": global_metrics[3],
"objective/non_score_reward": global_metrics[4],
"objective/rlhf_reward": global_metrics[5],
"objective/scores": global_metrics[6],
"objective/scores_margin": global_metrics[7],
"objective/loss": global_metrics[8],
"rewards/chosen": global_metrics[9],
"rewards/rejected": global_metrics[10],
"rewards/accuracies": global_metrics[11],
"rewards/margins": global_metrics[12],
"logps/chosen": global_metrics[13],
"logps/rejected": global_metrics[14],
}
if accelerator.is_main_process:
print_rich_single_line_metrics(metrics)
for key, value in metrics.items():
writer.add_scalar(key, value, episode)
del (queries, responses, postprocessed_responses, logprobs, ref_logprobs, sequence_lengths, scores)
del (metrics, kl, non_score_reward, rlhf_reward)
gc.collect()
torch.cuda.empty_cache()
if not ph.preempted:
# save model
os.makedirs(os.path.dirname(args.output_dir), exist_ok=True)
original_tokenizer = AutoTokenizer.from_pretrained(
model_config.model_name_or_path, revision=model_config.model_revision
)
save_with_accelerate(
accelerator,
model,
original_tokenizer,
args.output_dir,
)
# Ai2 specific logic
if is_beaker_job() and accelerator.is_main_process:
if args.hf_metadata_dataset:
dataset_list = list(args.dataset_mixer_dict.keys())
# mainly just focussing here on what would be useful for the leaderboard.
# wandb will have even more useful information.
metadata_blob = {
"model_name": args.exp_name,
"model_type": "sft",
"datasets": dataset_list,
"base_model": model_config.model_name_or_path,
"wandb_path": wandb.run.get_url(),
"beaker_experiment": beaker_config.beaker_experiment_url,
"beaker_datasets": beaker_config.beaker_dataset_id_urls,
}
upload_metadata_to_hf(
metadata_blob,
"metadata.json",
args.hf_metadata_dataset,
"results/" + args.hf_repo_revision, # to match what the auto-evals name as.
)
if args.try_launch_beaker_eval_jobs and len(beaker_config.beaker_dataset_id_urls) > 0:
command = f"""\
python mason.py \
--cluster ai2/allennlp-cirrascale ai2/general-cirrascale-a5000 ai2/general-cirrascale-a5000 ai2/s2-cirrascale ai2/general-cirrascale \
--priority low \
--preemptible \
--budget ai2/allennlp \
--workspace ai2/tulu-2-improvements \
--image nathanl/open_instruct_auto \
--pure_docker_mode \
--gpus 0 -- python scripts/wait_beaker_dataset_model_upload_then_evaluate_model.py \
--beaker_workload_id {beaker_config.beaker_workload_id} \
--upload_to_hf {args.hf_metadata_dataset} \
--model_name {args.hf_repo_revision}
"""
process = subprocess.Popen(["bash", "-c", command], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
print(f"Submit jobs after model training is finished - Stdout:\n{stdout.decode()}")
print(f"Submit jobs after model training is finished - Stderr:\n{stderr.decode()}")
print(f"Submit jobs after model training is finished - process return code: {process.returncode}")
if args.push_to_hub:
push_folder_to_hub(
accelerator,
args.output_dir,
args.hf_repo_id,
args.hf_repo_revision,
)
if accelerator.is_main_process:
# remove args.checkpoint_output_dir
if os.path.exists(args.checkpoint_output_dir):
shutil.rmtree(args.checkpoint_output_dir, ignore_errors=True)
if __name__ == "__main__":
parser = ArgumentParserPlus((Args, DatasetConfig, ModelConfig))
main(*parser.parse())