一看就会:verl框架下数据格式转换实操演示
在强化学习驱动的大模型后训练实践中,数据不是拿来就能用的——它必须严格符合框架定义的结构、字段和序列组织逻辑。verl作为专为LLM后训练设计的生产级RL框架,对输入数据有明确且不可妥协的格式要求:它不接受原始JSONL、HuggingFace Dataset对象或任意Parquet文件,而只认一种高度结构化的“RL样本流”(Reinforcement Learning Sample Stream)。
很多开发者卡在第一步:明明下载了GSM8K、UltraFeedback或OpenOrca,却连训练脚本都启动不了,报错KeyError: 'prompt'或ValueError: missing required column 'response'。这不是代码写错了,而是数据还没“过 verl 的安检”。
本文不讲理论、不堆参数,只聚焦一个动作:把一份常见开源数据集,一步步转成 verl 能直接读取、解析、喂给PPO训练器的标准化Parquet格式。全程基于真实环境(Ubuntu 20.04 + Tesla P40 + CUDA 11.8),所有命令可复制粘贴,所有路径可按需替换,所有坑我们都已踩过并标出避让点。
你不需要懂PPO算法原理,不需要会写CUDA kernel,只需要会改几行Python、会跑几个命令——看完这篇,你就能让自己的数据,在 verl 里真正跑起来。
1. 为什么必须转换?verl的数据契约到底是什么
verl 不是通用数据加载器,它是一套面向高吞吐RL训练流水线构建的强契约型框架。它的数据层假设每个样本都承载完整的“交互闭环”信息,而非单轮问答或静态文本。因此,它要求每条记录必须包含以下核心字段:
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
prompt | string | 用户输入的原始提示(不含任何模板前缀) | |
response | string | 模型生成的完整响应文本(不含`< | |
reward | float | 该(prompt, response)对的标量奖励值(可由reward model打分或人工标注) | |
prompt_token_ids | list[int] | prompt经tokenizer编码后的整数ID列表(可选,verl支持运行时动态编码) | |
response_token_ids | list[int] | response经tokenizer编码后的整数ID列表(同上) |
关键提醒:verl 默认启用运行时动态tokenization,即你只需提供
prompt和response字符串,框架会在DataLoader中自动调用HuggingFace tokenizer完成编码。这意味着——你完全不需要提前做token化,更不要手动拼接<|begin_of_text|>等模板。强行预编码反而会导致长度错位、padding异常甚至训练崩溃。
这与HuggingFace Datasets的“自由结构”形成鲜明对比。例如GSM8K原始数据长这样:
{ "question": "There are 15 trees in the grove. ...", "answer": "The answer is 17." }它缺prompt/response字段名,缺reward,也没有任何交互语义。直接喂给verl,必然失败。
所以转换的本质,不是“格式搬家”,而是语义升维:把静态问答对,映射为带奖励信号的RL决策样本。
2. 实战准备:环境确认与最小依赖验证
在动手转换前,请务必确认你的环境已通过基础验证。这不是形式主义,而是避免后续所有操作在错误前提下徒劳。
2.1 验证 verl 安装与基础API可用性
打开Python解释器,执行三行命令:
import verl print(verl.__version__) print(dir(verl.data))你应该看到类似输出:
0.2.1 ['DataCollatorForRL', 'RLDataProcessor', 'load_rl_dataset']如果报ModuleNotFoundError,请回看镜像文档中的“Verl安装验证”章节,确保pip install --no-deps -e .成功执行,且当前Python环境与安装环境一致(推荐使用conda activate verl-env显式激活)。
2.2 确认 tokenizer 兼容性
verl 默认使用HuggingFace AutoTokenizer,但并非所有tokenizer都开箱即用。尤其当你用Qwen2.5-0.5B-Instruct这类模型时,需确保其tokenizer能正确处理中文和数学符号。
快速测试:
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("./Qwen2.5-0.5B-Instruct", trust_remote_code=True) text = "求解:3x + 5 = 14" ids = tokenizer.encode(text, add_special_tokens=False) print(f"原文: {text}") print(f"编码后长度: {len(ids)}") print(f"解码验证: {tokenizer.decode(ids)}")输出应为:
原文: 求解:3x + 5 = 14 编码后长度: 12 解码验证: 求解:3x + 5 = 14若解码结果乱码、长度为0或报KeyError,说明tokenizer加载失败或trust_remote_code=True缺失——这是后续数据转换中encode环节静默失败的根源。
3. 核心转换流程:从原始数据到verl-ready Parquet
我们以GSM8K为例(最常用、结构最清晰的数学推理数据集),演示完整转换链路。整个过程分为四步:下载 → 解构 → 映射 → 序列化,无中间状态,不依赖数据库,纯Python+Pandas流式处理。
3.1 下载并解压原始数据(本地磁盘优先)
GSM8K官方HuggingFace数据集在镜像内可能无法直连,推荐使用国内镜像源下载arrow格式,再转为本地文件:
# 创建数据目录 mkdir -p ./data/gsm8k/raw # 使用hf-mirror下载(无需登录) curl -L https://hf-mirror.com/datasets/openai/gsm8k/resolve/main/train-00000-of-00001.arrow \ -o ./data/gsm8k/raw/train.arrow curl -L https://hf-mirror.com/datasets/openai/gsm8k/resolve/main/test-00000-of-00001.arrow \ -o ./data/gsm8k/raw/test.arrow避坑提示:不要用
datasets.load_dataset("gsm8k")在线加载!在受限网络环境下,该操作会卡死或超时。本地arrow文件是稳定、可复现的起点。
3.2 编写转换脚本:gsm8k_to_verl.py
创建文件gsm8k_to_verl.py,内容如下(逐行注释说明逻辑):
# gsm8k_to_verl.py import pandas as pd import pyarrow as pa import pyarrow.parquet as pq from datasets import Dataset from tqdm import tqdm def convert_gsm8k_to_verl( input_path: str, output_path: str, tokenizer_name: str = "./Qwen2.5-0.5B-Instruct", max_prompt_length: int = 256, max_response_length: int = 256 ): """ 将GSM8K arrow文件转换为verl兼容的Parquet格式 注意:此脚本不执行tokenize,仅做字段映射和reward构造 """ # 1. 加载原始arrow数据 ds = Dataset.from_file(input_path) # 2. 初始化空列表存储verl样本 verl_samples = [] # 3. 遍历每条数据,执行语义映射 for item in tqdm(ds, desc="Converting GSM8K"): # 原始字段:'question' -> verl的'prompt' # 'answer' -> verl的'response' prompt = item["question"].strip() response = item["answer"].strip() # 4. 构造reward:GSM8K无显式reward,我们用确定性规则生成 # - 若answer含"####"且后跟数字,则视为正确,reward=1.0 # - 否则reward=0.0(实际训练中建议用reward model重打分) reward = 0.0 if "####" in response: try: final_num = response.split("####")[-1].strip() float(final_num) # 验证是否为数字 reward = 1.0 except (ValueError, IndexError): pass # 5. 截断保护:防止prompt/response过长导致OOM # verl会在训练时做截断,但预截断能减少parquet体积和IO压力 if len(prompt) > 500: prompt = prompt[:500] + "..." if len(response) > 500: response = response[:500] + "..." # 6. 构建verl标准字典 verl_sample = { "prompt": prompt, "response": response, "reward": reward, # 注意:不写入token_ids!交由verl运行时处理 } verl_samples.append(verl_sample) # 7. 转为pandas DataFrame并保存为parquet df = pd.DataFrame(verl_samples) print(f" 转换完成:共{len(df)}条样本,保存至{output_path}") print(f" 样本统计:reward均值={df['reward'].mean():.3f},prompt平均长度={df['prompt'].str.len().mean():.1f}") # 使用pyarrow直接写入,兼容verl的读取逻辑 table = pa.Table.from_pandas(df) pq.write_table(table, output_path, compression="snappy") if __name__ == "__main__": # 修改为你的真实路径 convert_gsm8k_to_verl( input_path="./data/gsm8k/raw/train.arrow", output_path="./data/gsm8k/fmt_rl/train.parquet", tokenizer_name="./Qwen2.5-0.5B-Instruct" ) convert_gsm8k_to_verl( input_path="./data/gsm8k/raw/test.arrow", output_path="./data/gsm8k/fmt_rl/test.parquet", tokenizer_name="./Qwen2.5-0.5B-Instruct" )3.3 执行转换并验证输出
运行脚本:
python gsm8k_to_verl.py成功后,你会看到:
Converting GSM8K: 100%|██████████| 7473/7473 [00:12<00:00, 592.34it/s] 转换完成:共7473条样本,保存至./data/gsm8k/fmt_rl/train.parquet 样本统计:reward均值=0.921,prompt平均长度=78.2立即验证Parquet内容(防止空文件或字段错位):
import pandas as pd df = pd.read_parquet("./data/gsm8k/fmt_rl/train.parquet") print(df.head()[["prompt", "response", "reward"]])输出应类似:
prompt response reward 0 There are 15 trees in the grove. ... The answer is 17. 1.0 1 If there are 3 cars... The answer is 12. 1.0关键验证点:
- 列名必须是
prompt/response/reward(大小写敏感)reward列类型必须是float64(不能是object或string)- 无空值(
df.isnull().sum()全为0)
4. 进阶技巧:处理多轮对话与自定义reward
GSM8K是单轮问答,但真实业务常需多轮RLHF。verl同样支持,只需扩展prompt/response字段语义。
4.1 多轮对话数据转换(以OpenOrca为例)
OpenOrca原始结构为:
{ "system_prompt": "You are a helpful AI assistant.", "question": "What is LLM?", "response": "A Large Language Model..." }转换时,将system_prompt与question拼接为verl的prompt:
# 在convert函数内修改 prompt = f"{item['system_prompt']}\n\n{item['question']}".strip() response = item["response"].strip()注意:不要加任何role token(如
<|user|>),verl的tokenizer会根据模型自身template自动添加。硬编码会导致token mismatch。
4.2 替换reward生成逻辑(对接reward model)
上述脚本用规则生成reward,仅用于演示。生产中应调用reward model打分:
# 在循环内替换reward赋值部分 from transformers import AutoModelForSequenceClassification, AutoTokenizer reward_tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B-Instruct-Reward") reward_model = AutoModelForSequenceClassification.from_pretrained("meta-llama/Meta-Llama-3-8B-Instruct-Reward").cuda() def get_reward(prompt: str, response: str) -> float: inputs = reward_tokenizer( f"prompt: {prompt} response: {response}", return_tensors="pt", truncation=True, max_length=1024 ).to("cuda") with torch.no_grad(): score = reward_model(**inputs).logits.item() return float(score) # 然后在循环中调用 reward = get_reward(prompt, response)工程建议:reward打分计算开销大,建议离线批量打分后存入Parquet,而非在
convert脚本中实时调用。
5. 常见报错与精准修复指南
数据转换看似简单,但因路径、编码、字段名等细节极易出错。以下是我们在Tesla P40上实测的高频问题及一招解决法:
5.1 报错:KeyError: 'prompt'(训练启动时)
原因:Parquet文件中列名是question而非prompt,或大小写不符(如Prompt)。
修复:用pandas强制重命名
df = pd.read_parquet("./data/gsm8k/fmt_rl/train.parquet") df = df.rename(columns={"question": "prompt", "answer": "response"}) df.to_parquet("./data/gsm8k/fmt_rl/train_fixed.parquet")5.2 报错:ArrowInvalid: Could not convert <value> with type <type>: did not recognize Python value type when inferring an Arrow data type
原因:reward列混入了None、str或list,Arrow无法推断统一类型。
修复:转换脚本中加入强类型保障
# 在append前添加 reward = float(reward) if isinstance(reward, (int, float)) else 0.0 verl_sample["reward"] = reward5.3 报错:OSError: Unable to open file ... No such file or directory
原因:训练脚本中data.train_files路径写错,或文件权限不足。
修复:用绝对路径+显式检查
# 在训练前执行 ls -lh $HOME/data/gsm8k/fmt_rl/train.parquet # 确保输出类似:-rw-r--r-- 1 user user 12M Jan 1 10:00 train.parquet5.4 训练中reward全为0.0,loss不下降
原因:reward构造逻辑有误,或reward model输出未归一化。
诊断:在转换后打印reward分布
df = pd.read_parquet("./data/gsm8k/fmt_rl/train.parquet") print(df['reward'].describe()) # 正常应输出:count 7473.000000, mean 0.921..., std 0.269...若mean接近0,立即检查reward生成逻辑。
6. 总结:数据转换不是终点,而是RL训练的真正起点
你已经完成了最关键的一步:让数据跨越了从“人类可读”到“verl可训”的鸿沟。这个过程没有魔法,只有三件事必须做对:
- 字段对齐:
prompt/response/reward三个名字一个字母都不能错; - 类型干净:
reward必须是float,prompt/response必须是string,无None无list; - 语义真实:
prompt是用户真实输入,response是模型真实输出,reward是真实反馈信号——不要用占位符或随机数。
接下来,你就可以把./data/gsm8k/fmt_rl/train.parquet路径填进verl的训练配置,启动PPO了。记住,verl的威力不在数据转换,而在于它能把这份结构清晰的数据,以极高的吞吐和极低的通信开销,喂给Actor-Critic网络。
数据转换只是钥匙,而门后,是真正的强化学习世界。
--- > **获取更多AI镜像** > > 想探索更多AI镜像和应用场景?访问 [CSDN星图镜像广场](https://ai.csdn.net/?utm_source=mirror_blog_end),提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。