SWIFT 多模态 GRPO 训练实战:从 ClevrCount 到几何问答与 Open-R1 多模态数据集的完整实验流程
【免费下载链接】swiftUse PEFT or Full-parameter to CPT/SFT/DPO/GRPO 600+ LLMs (Qwen3.6, DeepSeek-V4, GLM-5.1, InternLM3, Llama4, ...) and 300+ MLLMs (Qwen3-VL, Qwen3-Omni, InternVL3.5, Ovis2.5, GLM4.5v, Gemma4, Llava, Phi4, ...) (AAAI 2025).项目地址: https://gitcode.com/GitHub_Trending/swift1/swift
本文基于 SWIFT(ms-swift)仓库中的多模态 GRPO 最佳实践文档,系统讲解如何把视觉语言模型(VLM)接入 GRPO 强化学习流程:以Qwen2.5-VL-3B-Instruct为基座,覆盖自定义数据集预处理器、外部奖励函数插件、外部 vLLM rollout 部署以及三组完整可复现的训练命令(ClevrCount 计数、GEOQA 几何问答、Open-R1 多模态推理数据集),并给出每组的训练曲线观察结论。读完后你可以直接复制命令跑通多模态 GRPO 训练,并理解num_generations、beta、max_grad_norm、MAX_PIXELS等关键参数的工程取舍。
背景与整体流程
GRPO 用同一问题采样多条 completion,以组内奖励均值/标准差构造优势(advantage),相比 PPO 省去价值模型。对于多模态任务,SWIFT 的 GRPO 管线需要解决三个环节:
- 数据集:把「图片 + 问题 + 可验证答案」组织成
images/messages/solution结构,其中solution不进入模型输入,而是直接透传给奖励函数; - 奖励函数:内置的
format奖励保证think/answer输出格式,自定义的准确性奖励(如external_r1v_acc)通过--external_plugins插件机制注册到orms字典; - rollout 与训练:用独立的 vLLM 服务(
swift rollout)加速采样,训练侧用swift rlhf --rlhf_type grpo配合 DeepSpeed ZeRO-3 完成全参更新。
这套流程的基座选择了Qwen2.5-VL-3B-Instruct。文档中明确说明选择Instruct版本而非 base 模型的原因:指令微调过的模型能更快拿到 format 奖励,让训练尽早进入"准确性提升"阶段。
任务一:ClevrCount 图像计数
数据集定义
ClevrCount 基于clevr_cogen_a_train数据集,目标是让模型输出图中物体数量。由于原始数据集的 query 没有要求特定的思考/答案标签格式,需要重写一个预处理器来改写 query。
class ClevrPreprocessor(ResponsePreprocessor): def preprocess(self, row: Dict[str, Any]) -> Dict[str, Any]: query = row.get('query', '') query = f"""{query} Output the thinking process in <think> </think> and final answer (number) in <answer> </answer> tags.""" row.update({'query': query}) return super().preprocess(row) register_dataset( DatasetMeta( ms_dataset_id='AI-ModelScope/clevr_cogen_a_train', subsets=[ SubsetDataset( name='default', subset='default', split=['train'], ), ], preprocess_func=ClevrPreprocessor(), tags=['qa', 'math']))从源码看,ResponsePreprocessor定义在 preprocessor/core.py。它维护了三类列名映射:system(如system_prompt)、query(如question、problem)、response(含solution等)。preprocess会把response/query/system等键弹出并转换为标准字段,其余字段(包括自定义的solution)原样保留在行数据中,最终随 batch 一起透传给奖励函数——这正是"数据集自定义字段可直接作为 ORM 入参"的底层依据。
改写后的数据集样本形如:
{ "images": ["image_path1", "image_path2"], "messages": [ { "role": "user", "content": "How many items are there in the image? Output the thinking process in <think> </think> and\n final answer (number) in <answer> </answer> tags." } ], "solution": "<answer> 3 </answer>" }两个要点(原文档明确提示):
- 样本中若带有
{'role': 'assistant', 'content': '<answer> 3 </answer>'}这样的 assistant 消息,GRPOTrainer 会将其移除,可以忽略; solution字段会直接进入 ORM;自定义数据集时images字段需组织为路径列表["image_path1", "image_path2"]。
奖励函数:内置 format + 自定义准确性奖励
ClevrCount 使用两个奖励函数:
- format:DeepSeek-R1 风格的结构化格式奖励,已内置于 SWIFT,用
--reward_funcs format直接启用。对应实现见 rewards/orm.py 中的Format类,其校验正则要求输出严格为^<think>.*?</think>\s*<answer>.*?</answer>(?![\s\S]),即整个 completion 必须从think块开始、以answer块结束; - external_r1v_acc:自定义准确性奖励,通过
external_plugin机制注入。代码放在 plugin/plugin.py 中。
MultiModalAccuracyORM的实现如下,采用"先符号校验、失败再回退字符串匹配"的双层验证策略:
class MultiModalAccuracyORM(ORM): def __call__(self, completions, solution, **kwargs) -> List[float]: """ Reward function that checks if the completion is correct. Args: completions (list[str]): Generated outputs solution (list[str]): Ground Truths. Returns: list[float]: Reward scores """ rewards = [] from math_verify import parse, verify for content, sol in zip(completions, solution): reward = 0.0 # Try symbolic verification first try: answer = parse(content) if float(verify(answer, parse(sol))) > 0: reward = 1.0 except Exception: pass # Continue to next verification method if this fails # If symbolic verification failed, try string matching if reward == 0.0: try: # Extract answer from solution if it has think/answer tags sol_match = re.search(r'<answer>(.*?)</answer>', sol) ground_truth = sol_match.group(1).strip() if sol_match else sol.strip() # Extract answer from content if it has think/answer tags content_match = re.search(r'<answer>(.*?)</answer>', content) student_answer = content_match.group(1).strip() if content_match else content.strip() # Compare the extracted answers if student_answer == ground_truth: reward = 1.0 except Exception: pass # Keep reward as 0.0 if both methods fail rewards.append(reward) return rewards orms['external_r1v_acc'] = MultiModalAccuracyORM从源码结构看,奖励插件机制的工作方式是:ORM 基类定义在 rewards/orm.py,__call__接收completions(模型生成文本列表)加上数据集透传字段(此处为solution);子类只需实现打分逻辑,再通过orms['name'] = Class注册到全局奖励字典,训练侧用--external_plugins <path> --reward_funcs <name>即可生效。plugin/plugin.py 文件头部的注释也给出了这三步标准流程(定义奖励类 → 注册到orms→ 通过参数引用)。由于completions和solution都是列表,一个 batch 的所有 completion 可一次算完,任务变化时只需同步修改数据集字段与奖励函数。
训练命令与实验记录
实验在 8 卡上进行,SWIFT GRPO 支持多 GPU 部署以加速 rollout(此处用 2 张卡起 vLLM 数据并行服务,6 张卡训练)。若qwen2.5-vl在 vLLM 上遇到部署报错,可查阅 vLLM 社区 issue 处理。
先启动外部 vLLM rollout 服务:
CUDA_VISIBLE_DEVICES=6,7 \ swift rollout \ --model Qwen/Qwen2.5-VL-3B-Instruct \ --vllm_data_parallel_size 2再启动训练。任务简单,max_completion_length取 1024;学习率与beta分别为1e-6与0.001;batch_size与num_generations的配比逻辑可参考 GRPO 完整流程文档:
WANDB_API_KEY=your_wandb_api_key \ CUDA_VISIBLE_DEVICES=0,1,2,3,4,5 \ NPROC_PER_NODE=6 \ swift rlhf \ --rlhf_type grpo \ --model Qwen/Qwen2.5-VL-3B-Instruct \ --external_plugins examples/train/grpo/plugin/plugin.py \ --reward_funcs external_r1v_acc format \ --use_vllm true \ --vllm_mode server \ --vllm_server_host 127.0.0.1 \ --vllm_server_port 8000 \ --tuner_type full \ --torch_dtype bfloat16 \ --dataset 'AI-ModelScope/clevr_cogen_a_train' \ --load_from_cache_file true \ --max_completion_length 1024 \ --num_train_epochs 1 \ --per_device_train_batch_size 8 \ --per_device_eval_batch_size 8 \ --learning_rate 1e-6 \ --gradient_accumulation_steps 2 \ --save_strategy 'steps' \ --eval_strategy 'steps' \ --eval_steps 1000 \ --save_steps 1000 \ --save_total_limit 10 \ --logging_steps 1 \ --output_dir output/GRPO_CLEVR_COUNTDOWN \ --warmup_ratio 0.01 \ --dataloader_num_workers 4 \ --num_generations 24 \ --temperature 1.0 \ --system 'examples/train/grpo/prompt.txt' \ --deepspeed zero3 \ --log_completions true \ --report_to wandb \ --num_iterations 1 \ --async_generate false \ --beta 0.001 \几个值得注意的参数:
--vllm_mode server+--vllm_server_host/--vllm_server_port:训练进程不本地拉起 vLLM,而是连接上面swift rollout启动的独立服务,实现采样与训练的卡数解耦;--num_generations 24:GRPO 组内优势估计的采样条数,本任务简单,用较多采样换取更稳定的组内统计;--system 'examples/train/grpo/prompt.txt':从文件读取 system prompt。prompt.txt 的内容正是要求 Assistant 先推理再作答、并把过程与答案分别包在think与answer标签中,与format奖励的正则严格对应;--beta 0.001:KL 正则项权重,配合1e-6的小学习率保持训练平稳;--deepspeed zero3:3B 全参多模态训练下用 ZeRO-3 分摊显存。
实验观察
由于数据集与任务都比较简单,模型在约 500 步后收敛,文档给出的关键观察:
- 自定义准确性奖励(
external_r1v_acc)持续上升,任务成功率从初始 0.4 提升至接近 1,证明模型确实学会了计数任务; format奖励全程稳定在 1——因为所有样本的 query 格式一致,模型早期就掌握了输出结构;reward_std稳定在 0.1 以下,说明组内采样趋于同质(都答对),优势信号自然衰减;- completion 长度最终稳定在 60–80 token 区间,模型收敛出"逐个物体计数"的固定输出模式。
任务二:GEOQA 几何问答
数据集与奖励函数
几何问答任务要求:给定一张几何图形,回答与之相关的数学问题。数据源自相关论文,并经 R1-V 项目预处理为problem-solution格式(图片保留在image字段)。因此无需自定义预处理器,直接--dataset AI-ModelScope/GEOQA_R1V_Train_8K即可;奖励函数也直接复用上一节的MultiModalAccuracyORM,无需改动。
训练参数:两个关键差异
基座模型与大部分超参与 ClevrCount 相同,主要差异有两处:
--num_iterations 2:允许一次 rollout 的采样结果被多次复用做参数更新,等价于用更少的采样换取更多更新步,提升训练吞吐;--max_grad_norm 0.5:实验中发现数学类任务训练可能不稳定、甚至"崩溃"——表现为所有奖励骤降、loss、grad_norm与 KL 散度快速攀升且无法恢复。截断梯度范数到 0.5 是文档给出的防崩溃手段(原文同时提醒:该不稳定性有一定随机性,并非必然出现)。
此外通过环境变量MAX_PIXELS=401408控制单图最大像素,从而限制视觉 token 数、控制显存(从源码结构看,MAX_PIXELS等环境变量在多模态模型的 processor 配置中被映射为图像 token 上限,见 model/models/qwen.py)。
WANDB_API_KEY=your_wandb_api_key \ CUDA_VISIBLE_DEVICES=0,1,2,3,4,5 \ MAX_PIXELS=401408 \ NPROC_PER_NODE=6 \ swift rlhf \ --rlhf_type grpo \ --model Qwen/Qwen2.5-VL-3B-Instruct \ --external_plugins examples/train/grpo/plugin/plugin.py \ --reward_funcs external_r1v_acc format \ --use_vllm true \ --vllm_mode server \ --vllm_server_host 127.0.0.1 \ --vllm_server_port 8000 \ --tuner_type full \ --torch_dtype bfloat16 \ --dataset 'AI-ModelScope/GEOQA_R1V_Train_8K' \ --load_from_cache_file true \ --max_completion_length 1024 \ --num_train_epochs 1 \ --per_device_train_batch_size 8 \ --per_device_eval_batch_size 8 \ --learning_rate 1e-6 \ --gradient_accumulation_steps 2 \ --save_strategy 'steps' \ --eval_strategy 'steps' \ --eval_steps 400 \ --save_steps 400 \ --save_total_limit 10 \ --logging_steps 1 \ --output_dir output/GRPO_GEOQA \ --warmup_ratio 0.05 \ --dataloader_num_workers 4 \ --num_generations 8 \ --temperature 1.0 \ --repetition_penalty 1.1 \ --system 'examples/train/grpo/prompt.txt' \ --deepspeed zero3 \ --log_completions true \ --report_to wandb \ --num_iterations 2 \ --async_generate false \ --beta 0.001 \ --max_grad_norm 0.5 \相比 ClevrCount,还新增了--repetition_penalty 1.1抑制几何推理中常见的循环复述;num_generations从 24 降到 8(任务更难、rollout 更贵)。
实验观察
- 相比 ClevrCount,几何问答难度大得多,Accuracy Reward 收敛明显更慢、震荡更大;
completion_length呈现清晰的增长趋势,最终稳定在 250 token 左右——模型学会了写更长的推理链;- 奖励最终收敛,
reward_std在总共约 1300 步后稳定在 0.2 附近。
训练后的 completion 示例(几何切线题,答案 29°):
User: "If AB is the tangent to circle O with the tangent point A, and BO intersects circle O at point C, and point D is located on circle O, what is the measure of angle ADC if the degree of angle ABO is 32.0?" Assistant: " To find the measure of angle ADC, we first need to understand the properties of the tangent and the circle. Since AB is a tangent to the circle at point A, the line segment OA (which is perpendicular to the tangent AB) is perpendicular to AB. This means that angle OAB is 90 degrees. Given that angle ABO is 32 degrees, we can find angle BAO by subtracting angle ABO from 90 degrees: ∠BAO = 90° - ∠ABO = 90° - 32° = 58° Since angle BAO is an angle in the minor arc AD, the angle ADC, which is an angle in the minor arc AD, is half the angle BAO. Therefore, angle ADC is: ∠ADC = 1/2 × ∠BAO = 1/2 × 58° = 29° So, the measure of angle ADC is 29 degrees. <answer> The measure of angle ADC is 29 degrees. </answer>"任务三:Open-R1 多模态推理数据集
数据集说明
该实验对应 open-r1-multimodal 项目的设置,使用数据集lmms-lab/multimodal-open-r1-8k-verified:聚焦多模态数学推理,由 GPT4o 基于 Math360K 与 Geo170K 数据集生成,包含推理路径与可验证答案。数据集自带image、problem、solution字段,无需改写 prompt,也无需重新定义数据集;奖励函数继续复用MultiModalAccuracyORM。
训练参数:应对 OOM
与上一组实验几乎一致,唯一调整是针对OOM(显存溢出)问题,将MAX_PIXELS降到262144以减少视觉 token 带来的显存占用:
WANDB_API_KEY=your_wandb_api_key \ CUDA_VISIBLE_DEVICES=0,1,2,3,4,5 \ MAX_PIXELS=262144 \ MASTER_PORT=29600 \ NPROC_PER_NODE=6 \ swift rlhf \ --rlhf_type grpo \ --model Qwen/Qwen2.5-VL-3B-Instruct \ --external_plugins examples/train/grpo/plugin/plugin.py \ --reward_funcs external_r1v_acc format \ --use_vllm true \ --vllm_mode server \ --vllm_server_host 127.0.0.1 \ --vllm_server_port 8000 \ --tuner_type full \ --torch_dtype bfloat16 \ --dataset 'lmms-lab/multimodal-open-r1-8k-verified' \ --load_from_cache_file true \ --max_completion_length 1024 \ --num_train_epochs 1 \ --per_device_train_batch_size 8 \ --per_device_eval_batch_size 8 \ --learning_rate 1e-6 \ --gradient_accumulation_steps 2 \ --save_strategy 'steps' \ --eval_strategy 'steps' \ --eval_steps 400 \ --save_steps 400 \ --save_total_limit 10 \ --logging_steps 1 \ --output_dir output/GRPO_GEOQA \ --warmup_ratio 0.05 \ --dataloader_num_workers 4 \ --num_generations 8 \ --temperature 1.0 \ --repetition_penalty 1.1 \ --system 'examples/train/grpo/prompt.txt' \ --deepspeed zero3 \ --log_completions true \ --report_to wandb \ --num_iterations 2 \ --async_generate false \ --beta 0.001 \ --max_grad_norm 0.5 \实验观察
- 8k 样本规模下共训练1268 步,Accuracy Reward 收敛到0.5 左右——开放推理任务的答案空间更大,准确率天花板低于闭式问答;
completion_length稳定在 200 token 附近;reward_std最终在 0.2 上下震荡。
训练后 completion 示例(地图分区统计题):
User: "Based on the map, which state falls into the lowest percentage range of lots, and what is that percentage range?" Assistant: " The image provided shows a color-coded map of the USA indicating different ranges of lot percentages across various states. According to the legend at the bottom, the lowest percentage range (6.0% - 6.5%) is represented by white. In the image, Alabama (AL) is shaded in white, which corresponds to the 6.0% - 6.5% category. Therefore, based on the map, the state that falls into the lowest percentage range of lots is Alabama, with the percentage range of 6.0% - 6.5%. Therefore, the answer is 6.0% - 6.5%. <answer>Alabama</answer>"关键要点总结
- 数据集字段契约:
messages+images是模型输入,solution(以及任意自定义列)由ResponsePreprocessor透传给 ORM;自定义多模态数据集时images必须是路径列表。相关实现在 dataset/preprocessor/core.py 与 dataset/register.py。 - 奖励插件三步法:继承 ORM 基类 实现
__call__(completions, **dataset_fields)→orms['name'] = MyORM注册 →--external_plugins+--reward_funcs引用。format、math_accuracy等常用奖励均已内置,可直接组合使用。 - rollout/训练解耦:
swift rollout独立起 vLLM 服务(支持--vllm_data_parallel_size多卡并行),训练端通过--vllm_mode server连接;卡数、显存、采样吞吐均可独立调优。 - 稳定性三板斧(来自两组数学类实验的教训):小学习率(
1e-6)+ 小beta(0.001)打底;数学任务加--max_grad_norm 0.5防崩溃;图像任务用MAX_PIXELS环境变量压视觉 token 防 OOM。 - 超参随任务难度调整:简单任务用大
num_generations(24)获得更稳的组内优势;难任务降采样(8)并用--num_iterations 2复用 rollout、加repetition_penalty抑制重复推理。 - 收敛判据:准确性奖励曲线单调上行且
reward_std走低(ClevrCount 中低于 0.1)说明任务被"吃掉";若reward_std长期在 0.2 附近且 accuracy 停在 0.5,通常是任务本身答案开放度或数据规模决定的上限,而非训练配置问题。
更多 GRPO 配置细节(batch 与num_generations配比、总步数推算、KL 项讨论)见 docs/source_en/BestPractices/GRPO.md;更多奖励函数与外部奖励模型插件示例见 examples/train/grpo/plugin/plugin.py。
【免费下载链接】swiftUse PEFT or Full-parameter to CPT/SFT/DPO/GRPO 600+ LLMs (Qwen3.6, DeepSeek-V4, GLM-5.1, InternLM3, Llama4, ...) and 300+ MLLMs (Qwen3-VL, Qwen3-Omni, InternVL3.5, Ovis2.5, GLM4.5v, Gemma4, Llava, Phi4, ...) (AAAI 2025).项目地址: https://gitcode.com/GitHub_Trending/swift1/swift
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考