Skip to content

Commit 0fe67b6

Browse files
CompN3rdLuisVasquezBSC
authored andcommitted
👁️ [GRPO] Add VLM training capabilities to the trainer (huggingface#3072)
1 parent 8bcf40e commit 0fe67b6

11 files changed

Lines changed: 2217 additions & 170 deletions

File tree

docs/source/grpo_trainer.md

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ training_args = GRPOConfig(
228228

229229
Depending on the model size and the overall GPU memory requirements for training, you may need to adjust the `vllm_gpu_memory_utilization` parameter in [`GRPOConfig`] to avoid underutilization or out-of-memory errors.
230230

231-
We provide a [small script](https://huggingface.co/spaces/trl-lib/recommend-vllm-memory) to help estimate the recommended GPU memory utilization based on your model configuration and experiment settings. Simply use it as follows to get `vllm_gpu_memory_utilization` recommendation:
231+
We provide a [HF Space](https://huggingface.co/spaces/trl-lib/recommend-vllm-memory) to help estimate the recommended GPU memory utilization based on your model configuration and experiment settings. Simply use it as follows to get `vllm_gpu_memory_utilization` recommendation:
232232

233233
<iframe
234234
src="https://trl-lib-recommend-vllm-memory.hf.space"
@@ -242,7 +242,9 @@ If the recommended value does not work in your environment, we suggest adding a
242242
</Tip>
243243

244244
<Tip>
245+
245246
By default, GRPO uses `MASTER_ADDR=localhost` and `MASTER_PORT=12345` for vLLM, but you can override these values by setting the environment variables accordingly.
247+
246248
</Tip>
247249

248250
For more information, see [Speeding up training with vLLM](speeding_up_training#vllm-for-fast-generation-in-online-methods).
@@ -329,7 +331,7 @@ The [`GRPOTrainer`] supports using custom reward functions instead of dense rewa
329331
- `prompts` (contains the prompts),
330332
- `completions` (contains the generated completions),
331333
- `completions_ids` (contains the tokenized completions),
332-
- `trainer_state` ([`transformers.TrainerState`]): The current state of the trainer. This can be used to implement dynamic reward functions, such as curriculum learning, where the reward is adjusted based on the training progress. For more details on the available attributes, refer to the [`TrainerState`](https://huggingface.co/docs/transformers/main/en/main_classes/callback#transformers.TrainerState) documentation.
334+
- `trainer_state` ([`~transformers.TrainerState`]): The current state of the trainer. This can be used to implement dynamic reward functions, such as curriculum learning, where the reward is adjusted based on the training progress.
333335
- All columns names (but `prompt`) that the dataset may have. For example, if the dataset contains a column named `ground_truth`, the function will be called with `ground_truth` as a keyword argument.
334336

335337
The easiest way to comply with this requirement is to use `**kwargs` in the function signature.
@@ -520,10 +522,66 @@ trainer = GRPOTrainer(
520522
...,
521523
)
522524
```
525+
523526
and the reward will be computed as the sum of the rewards from each function, or the weighted sum if `reward_weights` is provided in the config.
524527

525528
Note that [`GRPOTrainer`] supports multiple reward functions of different types. See the parameters documentation for more details.
526529

530+
## Vision-Language Model (VLM) Training
531+
532+
GRPO supports training Vision-Language Models (VLMs) on multimodal datasets containing both text and images.
533+
534+
### Supported Models
535+
536+
Tested with:
537+
538+
- **Qwen2.5-VL** — e.g., `Qwen/Qwen2.5-VL-3B-Instruct`
539+
- **Qwen2-VL** — e.g., `Qwen/Qwen2-VL-2B-Instruct`
540+
- **Gemma3** — e.g., `google/gemma-3-4b-it`
541+
542+
<Tip>
543+
Compatibility with all VLMs is not guaranteed. If you believe a model should be supported, feel free to open an issue on GitHub — or better yet, submit a pull request with the required changes.
544+
</Tip>
545+
546+
### Quick Start
547+
548+
Use [grpo\_vlm.py](https://github.com/huggingface/trl/blob/main/examples/scripts/grpo_vlm.py) to fine-tune a VLM. Example command for training on [`lmms-lab/multimodal-open-r1-8k-verified`](https://huggingface.co/datasets/lmms-lab/multimodal-open-r1-8k-verified):
549+
550+
```bash
551+
accelerate launch \
552+
--config_file=examples/accelerate_configs/deepspeed_zero3.yaml \
553+
examples/scripts/grpo_vlm.py \
554+
--model_name_or_path Qwen/Qwen2.5-VL-3B-Instruct \
555+
--output_dir grpo-Qwen2.5-VL-3B-Instruct \
556+
--learning_rate 1e-5 \
557+
--gradient_checkpointing \
558+
--torch_dtype bfloat16 \
559+
--max_prompt_length 2048 \
560+
--max_completion_length 1024 \
561+
--use_vllm \
562+
--vllm_mode colocate \
563+
--use_peft \
564+
--lora_target_modules "q_proj", "v_proj" \
565+
--log_completions
566+
```
567+
568+
### Configuration Tips
569+
570+
- Use LoRA on vision-language projection layers
571+
- VLM may require a lot of image tokens, which cannot be truncated. Set `max_prompt_length` to a higher value (e.g., 2048 in the above example) to accommodate longer prompts.
572+
- Enable 4-bit quantization to reduce memory usage
573+
- VLMs are memory-intensive — start with smaller batch sizes
574+
- Most models are compatible with vLLM (`server` and `colocate` modes)
575+
576+
### Dataset Format
577+
578+
Each training sample should include:
579+
580+
- `prompt`: Text formatted via the processor's chat template
581+
- `image`: A single image (PIL or NumPy array)
582+
583+
The trainer automatically handles image-to-tensor conversion via the model’s image processor.
584+
527585
## GRPOTrainer
528586

529587
[[autodoc]] GRPOTrainer

examples/scripts/grpo_vlm.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Copyright 2020-2025 The HuggingFace Team. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
pip install math_verify
17+
18+
accelerate launch \
19+
--config_file=examples/accelerate_configs/deepspeed_zero3.yaml \
20+
examples/scripts/grpo_vlm.py \
21+
--model_name_or_path Qwen/Qwen2.5-VL-3B-Instruct \
22+
--output_dir grpo-Qwen2.5-VL-3B-Instruct \
23+
--learning_rate 1e-5 \
24+
--gradient_checkpointing \
25+
--torch_dtype bfloat16 \
26+
--max_prompt_length 2048 \
27+
--max_completion_length 1024 \
28+
--use_vllm \
29+
--vllm_mode colocate \
30+
--use_peft \
31+
--lora_target_modules "q_proj", "v_proj" \
32+
--log_completions
33+
"""
34+
35+
import torch
36+
from datasets import load_dataset
37+
from latex2sympy2_extended import NormalizationConfig
38+
from math_verify import LatexExtractionConfig, parse, verify
39+
40+
from trl import (
41+
GRPOConfig,
42+
GRPOTrainer,
43+
ModelConfig,
44+
ScriptArguments,
45+
TrlParser,
46+
get_kbit_device_map,
47+
get_peft_config,
48+
get_quantization_config,
49+
)
50+
from trl.rewards import think_format_reward
51+
52+
53+
if __name__ == "__main__":
54+
parser = TrlParser((ScriptArguments, GRPOConfig, ModelConfig))
55+
script_args, training_args, model_args = parser.parse_args_and_config()
56+
################
57+
# Model & Processor
58+
################
59+
torch_dtype = (
60+
model_args.torch_dtype if model_args.torch_dtype in ["auto", None] else getattr(torch, model_args.torch_dtype)
61+
)
62+
quantization_config = get_quantization_config(model_args)
63+
model_kwargs = dict(
64+
revision=model_args.model_revision,
65+
attn_implementation=model_args.attn_implementation,
66+
torch_dtype=torch_dtype,
67+
device_map=get_kbit_device_map() if quantization_config is not None else None,
68+
quantization_config=quantization_config,
69+
)
70+
71+
################
72+
# Dataset
73+
################
74+
dataset = load_dataset("lmms-lab/multimodal-open-r1-8k-verified", split="train")
75+
dataset = dataset.train_test_split(test_size=100, seed=42)
76+
77+
SYSTEM_PROMPT = (
78+
"A conversation between user and assistant. The user asks a question, and the assistant solves it. The "
79+
"assistant first thinks about the reasoning process in the mind and then provides the user with the answer. "
80+
"The reasoning process and answer are enclosed within <think></think> tags, i.e., <think>\nThis is my "
81+
"reasoning.\n</think>\nThis is my answer."
82+
)
83+
84+
def make_conversation(example):
85+
prompt = [
86+
{"role": "system", "content": SYSTEM_PROMPT},
87+
{"role": "user", "content": example["problem"]},
88+
]
89+
return {"prompt": prompt}
90+
91+
dataset = dataset.map(make_conversation)
92+
93+
# Filter have big images
94+
def filter_big_images(example):
95+
image = example["image"]
96+
return image.size[0] < 512 and image.size[1] < 512
97+
98+
dataset = dataset.filter(filter_big_images)
99+
100+
train_dataset = dataset["train"]
101+
eval_dataset = dataset["test"] if training_args.eval_strategy != "no" else None
102+
103+
################
104+
# Reward Function for Training
105+
################
106+
def accuracy_reward(completions, solution: list[str], **kwargs):
107+
"""Reward function that checks if the completion matches the ground truth.
108+
- If both gold and prediction are parseable → use math verification.
109+
- If not parseable → compare as normalized text.
110+
"""
111+
rewards = []
112+
contents = [completion[0]["content"] for completion in completions]
113+
for content, sol in zip(contents, solution):
114+
try:
115+
gold_parsed = parse(sol, extraction_mode="first_match")
116+
except Exception:
117+
gold_parsed = []
118+
119+
if len(gold_parsed) != 0:
120+
# Try parsing predicted answer too
121+
try:
122+
answer_parsed = parse(
123+
content,
124+
extraction_config=[
125+
LatexExtractionConfig(
126+
normalization_config=NormalizationConfig(
127+
nits=False,
128+
malformed_operators=False,
129+
basic_latex=True,
130+
boxed="all",
131+
units=True,
132+
),
133+
boxed_match_priority=0,
134+
try_extract_without_anchor=False,
135+
)
136+
],
137+
extraction_mode="first_match",
138+
)
139+
reward = float(verify(gold_parsed, answer_parsed))
140+
except Exception as e:
141+
print(f"verify failed: {e}, answer: {content}, gold: {sol}")
142+
reward = None
143+
else:
144+
# fallback to text match
145+
reward = float(content.strip().lower() == sol.strip().lower())
146+
147+
rewards.append(reward)
148+
149+
return rewards
150+
151+
################
152+
# Training
153+
################
154+
trainer = GRPOTrainer(
155+
model=model_args.model_name_or_path,
156+
args=training_args,
157+
reward_funcs=[think_format_reward, accuracy_reward],
158+
train_dataset=train_dataset,
159+
eval_dataset=eval_dataset,
160+
peft_config=get_peft_config(model_args),
161+
)
162+
163+
trainer.train()
164+
165+
# Save and push to hub
166+
trainer.save_model(training_args.output_dir)
167+
if training_args.push_to_hub:
168+
trainer.push_to_hub(dataset_name=script_args.dataset_name)

scripts/generate_tiny_models.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
BartModel,
2525
BloomConfig,
2626
BloomForCausalLM,
27-
CLIPVisionConfig,
2827
CohereConfig,
2928
CohereForCausalLM,
3029
DbrxConfig,
@@ -35,6 +34,8 @@
3534
FalconMambaForCausalLM,
3635
Gemma2Config,
3736
Gemma2ForCausalLM,
37+
Gemma3Config,
38+
Gemma3ForConditionalGeneration,
3839
GemmaConfig,
3940
GemmaForCausalLM,
4041
GPT2Config,
@@ -58,19 +59,21 @@
5859
PaliGemmaForConditionalGeneration,
5960
Phi3Config,
6061
Phi3ForCausalLM,
62+
Qwen2_5_VLConfig,
63+
Qwen2_5_VLForConditionalGeneration,
6164
Qwen2Config,
6265
Qwen2ForCausalLM,
6366
Qwen2ForSequenceClassification,
67+
Qwen2VLConfig,
68+
Qwen2VLForConditionalGeneration,
6469
Qwen3Config,
6570
Qwen3ForCausalLM,
6671
Qwen3ForSequenceClassification,
6772
Qwen3MoeConfig,
6873
Qwen3MoeForCausalLM,
69-
SiglipVisionConfig,
7074
T5Config,
7175
T5ForConditionalGeneration,
7276
)
73-
from transformers.models.idefics2.configuration_idefics2 import Idefics2VisionConfig
7477

7578

7679
ORGANIZATION = "trl-internal-testing"
@@ -211,8 +214,8 @@ def push_to_hub(model, tokenizer, prefix=None, suffix=None):
211214

212215
# Encoder-decoder models
213216
for model_id, config_class, model_class, suffix in [
214-
("google/flan-t5-small", T5Config, T5ForConditionalGeneration, None),
215217
("facebook/bart-base", BartConfig, BartModel, None),
218+
("google/flan-t5-small", T5Config, T5ForConditionalGeneration, None),
216219
]:
217220
tokenizer = AutoTokenizer.from_pretrained(model_id)
218221
config = config_class(
@@ -232,35 +235,44 @@ def push_to_hub(model, tokenizer, prefix=None, suffix=None):
232235

233236

234237
# Vision Language Models
235-
# fmt: off
236-
for model_id, config_class, text_config_class, vision_config_class, model_class in [
237-
("HuggingFaceM4/idefics2-8b", Idefics2Config, MistralConfig, Idefics2VisionConfig, Idefics2ForConditionalGeneration),
238-
("llava-hf/llava-1.5-7b-hf", LlavaConfig, LlamaConfig, CLIPVisionConfig, LlavaForConditionalGeneration),
239-
("llava-hf/llava-v1.6-mistral-7b-hf", LlavaNextConfig, MistralConfig, CLIPVisionConfig, LlavaNextForConditionalGeneration),
240-
("google/paligemma-3b-pt-224", PaliGemmaConfig, GemmaConfig, SiglipVisionConfig, PaliGemmaForConditionalGeneration),
238+
for model_id, config_class, model_class in [
239+
("google/gemma-3-4b-it", Gemma3Config, Gemma3ForConditionalGeneration),
240+
("google/paligemma-3b-pt-224", PaliGemmaConfig, PaliGemmaForConditionalGeneration),
241+
("HuggingFaceM4/idefics2-8b", Idefics2Config, Idefics2ForConditionalGeneration),
242+
("llava-hf/llava-1.5-7b-hf", LlavaConfig, LlavaForConditionalGeneration),
243+
("llava-hf/llava-v1.6-mistral-7b-hf", LlavaNextConfig, LlavaNextForConditionalGeneration),
244+
("Qwen/Qwen2-VL-2B-Instruct", Qwen2VLConfig, Qwen2VLForConditionalGeneration),
245+
("Qwen/Qwen2.5-VL-3B-Instruct", Qwen2_5_VLConfig, Qwen2_5_VLForConditionalGeneration),
241246
]:
242-
# fmt: on
243247
processor = AutoProcessor.from_pretrained(model_id)
244248
kwargs = {}
249+
text_kwargs = {}
250+
vision_kwargs = {}
245251
if config_class == PaliGemmaConfig:
246252
kwargs["projection_dim"] = 8
247-
vision_kwargs = {}
248-
if vision_config_class in [CLIPVisionConfig, SiglipVisionConfig]:
253+
if config_class in [LlavaConfig, LlavaNextConfig, PaliGemmaConfig]:
249254
vision_kwargs["projection_dim"] = 8
250-
if vision_config_class == CLIPVisionConfig:
255+
if config_class in [LlavaConfig, LlavaNextConfig]:
251256
vision_kwargs["image_size"] = 336
252257
vision_kwargs["patch_size"] = 14
258+
if config_class in [Qwen2VLConfig, Qwen2_5_VLConfig]:
259+
kwargs["vision_start_token_id"] = 151652
260+
text_kwargs["rope_scaling"] = {"type": "mrope", "mrope_section": [1]}
261+
vision_kwargs["depth"] = 4
262+
vision_kwargs["embed_dim"] = 64
263+
253264
config = config_class(
254-
text_config=text_config_class(
265+
text_config=dict(
255266
vocab_size=processor.tokenizer.vocab_size + len(processor.tokenizer.added_tokens_encoder),
256267
hidden_size=8,
257268
num_attention_heads=4,
258269
num_key_value_heads=2,
259270
num_hidden_layers=2,
260271
intermediate_size=32,
272+
**text_kwargs,
261273
),
262-
vision_config=vision_config_class(
263-
hidden_size=8,
274+
vision_config=dict(
275+
hidden_size=16,
264276
num_attention_heads=4,
265277
num_hidden_layers=2,
266278
intermediate_size=32,

0 commit comments

Comments
 (0)