a

a / main_evaluation.py
khan3341ad's picture
Upload 12 files
4d23d0b verified
Raw
History Blame Contribute Delete
12.4 kB
# Copyright 2025 Sina Weibo Ltd. and/or its affiliates
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Generate responses given a dataset of prompts
"""
import csv
import ray
import numpy as np
import hydra
import os
import time
from tabulate import tabulate
from collections import Counter
os.environ['NCCL_DEBUG'] = 'WARN'
os.environ['TOKENIZERS_PARALLELISM'] = 'true'
# os.environ['TORCH_COMPILE_DISABLE'] = '1'
from verl.utils.model import compute_position_id_with_mask
import pandas as pd
from transformers import AutoTokenizer
from verl import DataProto
from verl.utils.fs import copy_local_path_from_hdfs
from verl.workers.fsdp_workers import ActorRolloutRefWorker
from verl.utils.hdfs_io import makedirs
from verl.single_controller.ray import RayClassWithInitArgs, RayResourcePool, RayWorkerGroup
from rllm.rewards.rl_reward import rllm_reward_fn
from rllm.rewards.math_utils.utils import extract_answer
@hydra.main(config_path='config', config_name='generation', version_base=None)
def main(config):
start_time = time.time()
from pprint import pprint
from omegaconf import OmegaConf
pprint(OmegaConf.to_container(config, resolve=True)) # resolve=True will eval symbol values
OmegaConf.resolve(config)
local_path = copy_local_path_from_hdfs(config.model.path)
from verl.utils import hf_tokenizer
tokenizer = hf_tokenizer(local_path)
# Check if output file already exists
if os.path.exists(config.data.output_path):
print(f"Output file {config.data.output_path} already exists. Skipping generation and proceeding to evaluation.")
if config.data.output_path.endswith('.parquet'):
dataset = pd.read_parquet(config.data.output_path)
elif config.data.output_path.endswith('.json'):
dataset = pd.read_json(config.data.output_path, orient='records', lines=True)
else:
if config.rollout.temperature == 0.:
assert config.data.n_samples == 1, 'When temperature=0, n_samples must be 1.'
# read dataset. Note that the dataset should directly contain chat template format (e.g., a list of dictionary)
dataset = pd.read_parquet(config.data.path)
chat_lst = dataset[config.data.prompt_key].tolist()
chat_lst = [chat.tolist() for chat in chat_lst]
tokenizer.padding_side = 'left'
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
ray_cls_with_init = RayClassWithInitArgs(cls=ray.remote(ActorRolloutRefWorker), config=config, role='rollout')
resource_pool = RayResourcePool(process_on_nodes=[config.trainer.n_gpus_per_node] * config.trainer.nnodes)
wg = RayWorkerGroup(resource_pool=resource_pool, ray_cls_with_init=ray_cls_with_init)
wg.init_model()
total_samples = len(dataset)
config_batch_size = config.data.batch_size
dp_size = wg.world_size // config.rollout.tensor_model_parallel_size
num_batch = (total_samples + config_batch_size - 1) // config_batch_size
output_lst = [] # We'll reshape at the end
print('len(dataset):', total_samples)
print('wg.worker_names:', wg.worker_names)
for batch_idx in range(num_batch):
print(f'[{batch_idx+1}/{num_batch}] Start to process.')
batch_chat_lst = chat_lst[batch_idx * config_batch_size:(batch_idx + 1) * config_batch_size]
# Repeat the batch n_samples times using block repetition inside the batch
repeated_chat_lst = []
for chat in batch_chat_lst:
repeated_chat_lst.extend([chat] * config.data.n_samples)
inputs = tokenizer.apply_chat_template(repeated_chat_lst,
add_generation_prompt=True,
padding=True,
truncation=True,
max_length=config.rollout.prompt_length,
return_tensors='pt',
return_dict=True,
tokenize=True)
input_ids = inputs['input_ids']
attention_mask = inputs['attention_mask']
position_ids = compute_position_id_with_mask(attention_mask)
batch_dict = {'input_ids': input_ids, 'attention_mask': attention_mask, 'position_ids': position_ids}
print(f'main_gen.py, input_ids.shape = {input_ids.shape}, content:', input_ids[:, -32:-26])
data = DataProto.from_dict(batch_dict)
real_batch_size = data.batch['input_ids'].shape[0]
original_dp_size = dp_size
dp_size = wg.world_size # ray会先把数据分到每个worker,再每个tp group内收集,所以要保证总数能被worker数整除,该校验不应考虑tp
if real_batch_size % dp_size != 0:
dummy_data_size = dp_size - real_batch_size % dp_size
dummy_data = data[:dummy_data_size]
data = DataProto.concat([data, dummy_data])
print(
f'dp_size {dp_size} is not divisible by real_batch_size {real_batch_size}, add {dummy_data_size} dummy data'
)
dp_size = original_dp_size
batch_size = data.batch['input_ids'].shape[0]
assert batch_size % dp_size == 0, f'batch_size {batch_size} is not divisible by dp_size {dp_size}'
print(f'[{batch_idx+1}/{num_batch}] Start to generate.')
# Generate all samples at once
print('ZHS batch len:', len(data.batch['input_ids']))
output = wg.generate_sequences(data)
# Remove dummy data
output = output[:real_batch_size]
output_text = tokenizer.batch_decode(output.batch['input_ids'][:, -config.rollout.response_length:],
skip_special_tokens=False)
# Remove padding
pad_token = tokenizer.pad_token
output_text_unpad = []
for text in output_text:
output_text_unpad.append(text.replace(pad_token, ''))
output_lst.extend(output_text_unpad)
# Reshape output_lst from (total_samples,) to (n_data, n_samples)
total_generated = len(output_lst)
n_data = total_generated // config.data.n_samples
output_lst = np.array(output_lst).reshape(n_data, config.data.n_samples).tolist()
# Add to the data frame
dataset['responses'] = output_lst
# add correctness field
total_lst = compute_correctness(dataset, config.data.data_source_key)
dataset['correctness'] = total_lst
# Write to a new parquet
output_dir = os.path.dirname(config.data.output_path)
makedirs(output_dir, exist_ok=True)
dataset.to_json(config.data.output_path, orient='records', force_ascii=False, lines=True)
if 'correctness' not in dataset:
total_lst = compute_correctness(dataset,config.data.data_source_key)
dataset['correctness'] = total_lst
dataset.to_json(config.data.output_path, orient='records', force_ascii=False, lines=True)
print(f"Output file {config.data.output_path} doesn't have correctness field. Have computed each answer's correctness and saved.")
output_dir = os.path.dirname(config.data.output_path)
# Compute evaluation metrics
prompts = dataset[config.data.prompt_key]
responses = dataset['responses'] # Using the generated responses
data_sources = dataset[config.data.data_source_key]
reward_model_data = dataset[config.data.reward_model_key]
# 统计长度均值和截断比例
output_lst = [str(r) for responses_this in list(responses) for r in responses_this]
# print(output_lst)
print(type(output_lst), type(output_lst[0]))
unpad_tokenized = tokenizer(output_lst, add_special_tokens=False).input_ids
len_response_tokens = [len(tokens) for tokens in unpad_tokenized]
len_mean = np.mean(len_response_tokens)
cutoff_ratio = sum([l == config.rollout.response_length for l in len_response_tokens]) / len(unpad_tokenized)
print('length cutoff ratio:', cutoff_ratio)
passes = 0
total = len(dataset)
total_scores = []
conses = 0
for i in range(total):
response_lst = responses[i]
data_source = data_sources[i]
prompt = prompts[i]
reward_data = reward_model_data[i]
reward_fn = select_reward_fn(data_source)
ground_truth = reward_data['ground_truth']
score_lst = []
for r in response_lst:
score = reward_fn(data_source, r, ground_truth)
score_lst.append(score)
max_score = np.max(score_lst)
total_scores.append(score_lst)
if max_score == 1:
passes += 1
extracted_lst = [extract_answer(r) for r in response_lst]
extracted_lst = [r for r in extracted_lst if r is not None]
cons_answers = find_mode(extracted_lst)
cons_response_lst = [r for r in response_lst if extract_answer(r) in cons_answers]
is_cons_correct_list = list()
for r in cons_response_lst:
score = reward_fn(data_source, r, ground_truth)
is_cons_correct_list.append(score)
if any(is_cons_correct_list):
conses += np.mean(is_cons_correct_list)
n_samples = config.data.n_samples
pass_at_n = passes / total
pass_at_1 = np.mean(total_scores)
cons_at_n = conses / total
spent_time = time.time() - start_time
spent_hours = spent_time / 60 / 60
# Save metrics to CSV
csv_path = os.path.join(output_dir, f'pass_{spent_hours:.2f}h.csv')
# Prepare the row data
# Extract the dataset name from the path
dataset_name = os.path.basename(config.data.path)
row_data = {
'model_path': config.model.path,
'dataset': dataset_name,
'pass@1': pass_at_1,
f'pass@{n_samples}': pass_at_n,
f'cons@{n_samples}': cons_at_n,
'cutoff_raio': cutoff_ratio,
'mean_response_tokens': len_mean,
'run_hours': spent_hours
}
# Check if file exists
file_exists = os.path.isfile(csv_path)
# Write to CSV
with open(csv_path, mode='a', newline='') as f: # 追加写,不会覆盖,所以没事
writer = csv.DictWriter(f, fieldnames=row_data.keys())
if not file_exists:
writer.writeheader()
writer.writerow(row_data)
# Convert the row data into a list of lists format for tabulate
table_data = [[k, v] for k, v in row_data.items()]
# Print table
print(tabulate(table_data, headers=['Metric', 'Value'], tablefmt='grid'))
def compute_correctness(dataset, data_source_key):
total_lst = list()
for i in range(len(dataset)):
row = dataset.iloc[i]
prompt = row['prompt']
gt = row['reward_model']['ground_truth']
# print(gt)
responses_this = row['responses']
true_false = [int(rllm_reward_fn(row[data_source_key], response, gt)) for response in responses_this]
total_lst.append(true_false)
return total_lst
def find_mode(lst):
if len(lst) == 0:
return list()
counter = Counter(lst)
max_count = max(counter.values())
mode = [k for k, v in counter.items() if v == max_count]
return mode
# Add the select_reward_fn from main_eval.py
def select_reward_fn(data_source):
if data_source == 'lighteval/MATH':
from verl.utils.reward_score import math
return math.compute_score
else:
from rllm.rewards.rl_reward import rllm_reward_fn
return rllm_reward_fn
if __name__ == '__main__':
main()