This project is a research environment for fine-tuning Vision Language Models (VLMs) using Group Relative Policy Optimization (GRPO). It provides a structured setup for data processing, model training, evaluation, and inference.
├── configs/ # Experiment configuration files (e.g., hyperparameters)
├── data/ # Datasets (raw and processed)
├── models/ # Saved model checkpoints (local)
├── notebooks/ # Jupyter notebooks for exploration and analysis
├── results/ # Experiment results, logs, and outputs
├── src/ # Reusable source code
│ ├── data/ # Data loading and preprocessing
│ ├── evaluation/ # Model evaluation scripts
│ ├── inference/ # Inference scripts
│ └── training/ # Core training logic
├── tests/ # Unit and integration tests
├── .github/workflows/ # CI/CD workflows
├── Dockerfile # Docker container definition
├── README.md # This file
└── requirements.txt # Python dependencies
Clone the repository:
git clone <your-repo-url>
cd vqa-nle
Create and activate a virtual environment (recommended):
conda create --name venv python=3.10 -y
conda activate venv
Install dependencies:
pip install -r requirements.txt
To run the GRPO training script, execute the following command from the project's root directory:
python -m src.training.run_grpo
For training with InternVL models, use the specialized script:
bash VLM-R1/run_scripts/run_grpo_rec_internvl.sh
This guide provides comprehensive instructions for running the GRPO training script (run_grpo_rec_internvl.sh), including configuration, execution, and customization.
Before running the training script, ensure the following requirements are met:
requirements.txt are installed in your activated conda environment. DeepSpeed is required.ViVQA-X_train_grpo.jsonl exists in the data/processed/ directory.wandb login in your terminal. To disable it, uncomment export WANDB_DISABLED=true (line 26) in the script.The training process is controlled by variables and arguments in VLM-R1/run_scripts/run_grpo_rec_internvl.sh. Below is a detailed breakdown of key parameters.
These must be configured correctly for your environment.
| Variable | Line | Description |
|---|---|---|
data_paths | 8 | Required. Path to the ViVQA-X_train_grpo.jsonl file. |
image_folders | 9 | Required. Path to the directory containing COCO images (train2014). |
model_path | 10 | The Hugging Face model identifier or a local path to the pretrained model (e.g., OpenGVLab/InternVL3-1B). |
EXP_NAME | 15 | A unique name for your experiment. Logs and checkpoints will be saved under this name. |
CUDA_VISIBLE_DEVICES | 27 | The specific GPU(s) to use (e.g., 0, 0,1). Default is 2. |
--nproc_per_node | 28 | The number of GPUs to use. This should match the count in CUDA_VISIBLE_DEVICES. |
Important Note on
EXP_NAME: Avoid using forward slashes (/) in your experiment name (e.g.,my_experiment/run_1). The system will interpret the slash as a directory separator and create nested folders. This can cause theresume_from_checkpointlogic to fail because it may not correctly locate the checkpoint files inside the nested structure. It is recommended to use hyphens (-) or underscores (_) instead.
These parameters control the training loop and performance.
| Argument | Line | Description |
|---|---|---|
--per_device_train_batch_size | 42 | The number of samples processed per GPU in one forward pass. Adjust based on VRAM. |
--gradient_accumulation_steps | 43 | Number of updates steps to accumulate gradients before performing a backward pass. Effective batch size = nproc_per_node * per_device_train_batch_size * gradient_accumulation_steps. |
--learning_rate | 60 | The initial learning rate for the optimizer. |
--max_steps | 46 | The total number of training steps to perform. Overrides num_train_epochs. |
--save_steps | 52 | How often to save a model checkpoint, specified in number of steps. |
These parameters are unique to the Group Relative Policy Optimization algorithm.
| Argument | Line | Description |
|---|---|---|
--num_generations | 53 | The number of candidate responses to generate for each prompt during training. |
--reward_funcs | 55 | A space-separated list of reward functions to use for scoring generations (e.g., accuracy format). |
--beta | 56 | The KL divergence penalty coefficient. Controls how much the policy model can deviate from the reference model. |
| Argument | Line | Description |
|---|---|---|
--gradient_checkpointing | 44 | A memory optimization technique that trades compute for memory. Set to true to reduce VRAM usage. |
--freeze_vision_modules | 61 | If true, the weights of the vision encoder are frozen and not updated during training. |
--push_to_hub | 62 | If true, automatically pushes the final trained model to the Hugging Face Hub. |
--hub_model_id | 63 | The repository name for the model on the Hugging Face Hub (e.g., YourUsername/YourModelName). |
VLM-R1 directory.cd /home/vlai-vqa-nle/minhtq/vqa-nle/VLM-R1
```
2. **Run the Script**:
```bash
bash run_scripts/run_grpo_rec_internvl.sh
VLM-R1/runs/${EXP_NAME}/log/. If DEBUG_MODE (line 19) is "true", the log will include detailed rollout information.VLM-R1/checkpoints/rl/${EXP_NAME}/.wandb.ai under the project and run name you configured.0, change CUDA_VISIBLE_DEVICES to 0 and ensure --nproc_per_node is "1". If you encounter CUDA "out of memory" errors, reduce --per_device_train_batch_size.--reward_funcs argument (line 55) to add or remove rewards. For example, to add the explanation reward, change it to --reward_funcs accuracy format explanation.Adapting for a New VQA Dataset:
src/data/dataset_loader.py to handle your custom dataset's format..jsonl file consistent with the required format (containing id, image, and conversations fields).data_paths variable in the run script to point to your new dataset file.Switching to a Different Model:
model_path variable (line 10) to the Hugging Face identifier or local path of the new model.--per_device_train_batch_size and --learning_rate depending on the new model's size and architecture.InvernVLModule or implement a new module if needed.--per_device_train_batch_size. If that's not enough, enable --gradient_checkpointing true. You can also try reducing --max_completion_length.local_scripts/zero2.json) is correctly formatted and that DeepSpeed was installed properly.grpo_jsonl.py contains hard-coded logic that automatically resumes from a checkpoint if one is found in the output directory. This ignores and overrides the --resume_from_checkpoint False flag set in the run_grpo_rec_internvl.sh script. To start a fresh training run when parameters are changed, you have three options:
Option 1 (Workaround, Recommended): Change the EXP_NAME variable (line 15) in the run script. This creates a new, empty directory for checkpoints and logs, forcing a fresh start.
Option 2 (Workaround): Manually delete the contents of the existing checkpoint directory (VLM-R1/checkpoints/rl/${EXP_NAME}/).
Option 3 (Permanent Fix): Modify the logic in VLM-R1/src/open-r1-multimodal/src/open_r1/grpo_jsonl.py to respect the script's arguments.
Original Code (lines 1216-1219):
if list(pathlib.Path(training_args.output_dir).glob("checkpoint-*")):
trainer.train(resume_from_checkpoint=True)
else:
trainer.train()
Modified Code:
# This allows the --resume_from_checkpoint flag to control the behavior.
trainer.train(resume_from_checkpoint=training_args.resume_from_checkpoint)
The project includes custom data processing modules specifically designed for InternVL training:
src/data/dataset_loader.py)Purpose: Converts ViVQA-X dataset to GRPO-compatible JSONL format
Key Features:
Usage:
from src.data.dataset_loader import create_jsonl_for_grpo
# Create training data
create_jsonl_for_grpo("train")
# Create validation data
create_jsonl_for_grpo("val")
# Create test data
create_jsonl_for_grpo("test")
Output Format:
{
"id": 1,
"image": "COCO_train2014_000000000139.jpg",
"conversations": [
{
"from": "human",
"value": "<image>Bạn là một trợ lý AI chuyên gia... Câu hỏi: {question}"
},
{
"from": "gpt",
"value": "<answer>{answer}</answer><explain>{explanation}</explain>"
}
]
}
Prepare ViVQA-X Dataset:
# Ensure ViVQA-X data is available at:
/mnt/VLAI_data/ViVQA-X/
├── ViVQA-X_train.json
├── ViVQA-X_val.json
└── ViVQA-X_test.json
Prepare COCO Images:
# Ensure COCO images are available at:
/mnt/VLAI_data/COCO_Images/
├── train2014/
└── val2014/
Generate GRPO Data:
cd /home/vlai-vqa-nle/minhtq/vqa-nle
python -m src.data.dataset_loader
This project has been specifically adapted to support InternVL models with the following key modifications:
File: VLM-R1/src/open-r1-multimodal/src/open_r1/grpo_jsonl.py (line 942)
Added "vintern" detection to route to InvernVLModule:
elif "internvl" in model_name_or_path.lower() or "vintern" in model_name_or_path.lower():
return InvernVLModule
File: VLM-R1/run_scripts/run_grpo_rec_internvl.sh
1 (line 1) to skip CUDA compatibility checks.2 for 1-GPU training (line 26).torchrun --nproc_per_node="1" to match GPU count (line 27).File: VLM-R1/run_scripts/run_grpo_rec_internvl.sh
--gradient_checkpointing false, line 44).--per_device_train_batch_size 1 (line 42).Note on PEFT and InternVL: Using PEFT (LoRA) with InternVL can cause a
TypeError: InternVLChatModel.forward() got an unexpected keyword argument 'inputs_embeds'. This happens because:
- PEFT automatically injects the
inputs_embedsargument when using LoRA with CausalLM models.- The
InternVLmodel'sforwardmethod does not accept this argument.- The GRPO trainer passes all keyword arguments from PEFT to the model, leading to a crash. For this reason, PEFT is disabled for InternVL training.
File: VLM-R1/src/open-r1-multimodal/local_scripts/zero2.json
Optimized for InternVL with ZeRO Stage 2 and optimizer offloading:
{
"zero_optimization": {
"stage": 2,
"offload_optimizer": {
"device": "cpu",
"pin_memory": true
}
}
}
To avoid the self.img_context_token_id is not None error, the model initialization logic has been updated to correctly handle PEFT models by accessing the underlying base model:
if is_peft_model(model):
self.vlm_module.post_model_init(model.base_model.model, processing_class)
else:
self.vlm_module.post_model_init(model, processing_class)
if self.ref_model is not None:
if is_peft_model(self.ref_model):
self.vlm_module.post_model_init(self.ref_model.base_model.model, processing_class)
else:
self.vlm_module.post_model_init(self.ref_model, processing_class)
The training script logic was adjusted to ensure that training restarts from the beginning when parameters are changed, rather than always resuming from the latest checkpoint. The resume_from_checkpoint=True parameter is now conditional.
# Original logic always resumed if a checkpoint existed
if list(pathlib.Path(training_args.output_dir).glob("checkpoint-*")):
trainer.train(resume_from_checkpoint=True)
else:
trainer.train()
grpo_jsonl.py)File: VLM-R1/src/open-r1-multimodal/src/open_r1/grpo_jsonl.py
Problem: The original data loading logic was designed to automatically wrap solutions in <answer> tags. It first stripped any existing <answer> tags from the ground truth and then added them back. This created a logic conflict with pre-formatted data for ViVQA-X, which already contains both <answer> and <explain> tags, leading to incorrect nested tags (e.g., <answer>...<explain>...</explain></answer>).
Modification: The data processing pipeline has been adjusted to preserve the original tags from the input data. The code that stripped and re-added <answer> tags has been changed. The script now directly uses the solution string from the JSONL file, assuming it is already correctly formatted with all necessary tags.
Original Logic (Before):
# In the data loading loop
item['solution'] = solution_value.replace('<answer>', '').replace('</answer>', '').strip()
# In make_conversation_from_jsonl function
'solution': f"<answer> {example['solution']} </answer>",
Modified Logic (After):
# In the data loading loop
item['solution'] = solution_value # Directly use the pre-formatted string
# In make_conversation_from_jsonl function
'solution': example['solution'], # Use the solution as-is
This section provides guidance on how to customize the training pipeline, such as adding new reward functions or understanding how data is handled.
To integrate a new reward function into the GRPO training pipeline, follow these steps:
Implement the Reward Function:
VLM-R1/src/open-r1-multimodal/src/open_r1/grpo_jsonl.py.completions, solution, and **kwargs as arguments and return a list of floating-point scores. kwargs can be used to access additional data like image_path.def my_new_reward(completions, solution, **kwargs):
scores = []
# ... your logic here ...
return scores
Normalize the Reward Score:
accuracy, format, explanation).Register the Function:
reward_funcs_registry dictionary.
reward_funcs_registry = {
"accuracy": accuracy_reward,
"format": format_reward,
"explanation": explanation_reward,
# Add your new function here
"my_new_reward": my_new_reward,
}
Activate in Training Script:
VLM-R1/run_scripts/run_grpo_rec_internvl.sh).--reward_funcs argument.
--reward_funcs accuracy format explanation my_new_reward \
The data pipeline is optimized to handle images efficiently, especially for reward functions that require image data (e.g., CLIP-based scores).
image_path field. This is done in grpo_jsonl.py by combining the image_folders path from the run script with the image filename from the JSONL data.**kwargs dictionary for all reward functions. A function can access it like this:
def my_reward_with_images(completions, solution, **kwargs):
if 'image_path' in kwargs:
image_paths_list = kwargs['image_path']
# Now you can open and process images using these paths
for path in image_paths_list:
# ...
initialize_explanation_scorer). This ensures the model is loaded into memory only once per process, preventing repeated initialization on every batch.This section provides a deeper dive into the specific customizations made for the Vietnamese VQA task, including data preparation, custom reward functions, and VLM module modifications.
To adapt the training for the ViVQA-X dataset, a custom data loader was created at src/data/dataset_loader.py.
Key Features:
Vietnamese System Prompt: A specialized Vietnamese prompt is used to guide the model's response structure, requiring it to generate <think>, <answer>, and <explain> tags. This ensures the model follows a structured reasoning process.
prompt = """
<image> You are a Visual Question Answering system. Your task is to answer questions based on the content of the provided image.
You must respond in Vietnamese and your response **must** include all the tags <think> </think>, <answer> </answer>, <explain> </explain>.
Follow these steps precisely:
1. In the <think> tag, provide a step-by-step reasoning process.
2. In the <answer> tag, give one word or one short phrase.
3. In the <explain> tag, provide one brief sentence that justifies your answer.
Now, answer this question based on the image:
Question: {question}
""".strip()
GRPO JSONL Conversion: The script converts the standard ViVQA-X JSON format into the JSONL format required by the GRPO trainer, injecting the system prompt and structuring the ground truth solution.
# format câu trả lời
solution = f"<answer>{answer}</answer><explain>{explanation}</explain>"
To better evaluate the model's performance on the Vietnamese VQA task, custom reward functions were developed in the src/rewards/ directory and integrated into VLM-R1/src/open-r1-multimodal/src/open_r1/grpo_jsonl.py.
File: src/rewards/explaination_rewards.py
This reward function evaluates the quality of the generated explanation in the <explain> tag by combining two scores:
The final score is a weighted average of the two, normalized to a [0, 1] range. A singleton pattern (initialize_explanation_scorer) is used in grpo_jsonl.py to ensure the CLIP model is loaded only once.
File: src/rewards/outcome_rewards.py
This function provides a more nuanced evaluation of the answer in the <answer> tag compared to simple exact matching.
Scoring Logic:
This logic is integrated into grpo_jsonl.py at line 893 as the default accuracy reward.
The core training script grpo_jsonl.py was modified to support custom models and handle data more efficiently.
A VLM module system (VLM-R1/src/open-r1-multimodal/src/open_r1/vlm_modules/) was implemented to easily switch between different model architectures (e.g., Qwen2-VL, InternVL). To add a new model, you would create a new module inheriting from VLMBaseModule and implement the required methods. The model is then registered in the get_vlm_module function in grpo_jsonl.py.
elif "internvl" in model_name_or_path.lower() or "vintern" in model_name_or_path.lower():
return InvernVLModule
else:
raise ValueError(f"Unsupported model: {model_name_or_path}")
The original data loading logic in the GRPO trainer would strip <answer> tags and then re-add them. This conflicted with our pre-formatted data which also included <explain> tags. The logic was modified to preserve the solution string as-is, ensuring the format is maintained.
item['solution'] = solution_value.replace('<answer>', '')...line 1142): item['solution'] = solution_valueInstead of loading all images into memory at the start, the data pipeline was modified to only store image file paths. The images are loaded just-in-time during the training step. This significantly reduces memory usage and speeds up initialization. The image paths are then passed to reward functions that require them (like the Explanation Reward).
Here is a summary of how to use these customizations together:
Prepare Data: Use the custom script to generate the GRPO-compatible JSONL file.
from src.data.dataset_loader import create_jsonl_for_grpo
create_jsonl_for_grpo("train")
Integrate Custom Rewards: In grpo_jsonl.py, import and register your custom reward functions.
# Import your custom reward functions
from src.rewards.explaination_rewards import ExplanationRewardScorer
from src.rewards.outcome_rewards import accuracy_reward as custom_accuracy_reward
# In accuracy_reward function (line 893)
reward = custom_accuracy_reward(content, sol)
# In reward_funcs_registry, ensure `explanation` is active
reward_funcs_registry = {
"accuracy": accuracy_reward,
"format": format_reward,
"explanation": explanation_reward,
}
Configure Training Script: In run_grpo_rec_internvl.sh, add explanation to the list of active rewards.
--reward_funcs accuracy format explanation \
45 commits
3 commits
Jupyter Notebook
89.8%
Python
9.5%
This project is a research environment for fine-tuning Vision Language Models (VLMs) using Group Relative Policy Optimization (GRPO). It provides a structured setup for data processing, model training, evaluation, and inference.
├── configs/ # Experiment configuration files (e.g., hyperparameters)
├── data/ # Datasets (raw and processed)
├── models/ # Saved model checkpoints (local)
├── notebooks/ # Jupyter notebooks for exploration and analysis
├── results/ # Experiment results, logs, and outputs
├── src/ # Reusable source code
│ ├── data/ # Data loading and preprocessing
│ ├── evaluation/ # Model evaluation scripts
│ ├── inference/ # Inference scripts
│ └── training/ # Core training logic
├── tests/ # Unit and integration tests
├── .github/workflows/ # CI/CD workflows
├── Dockerfile # Docker container definition
├── README.md # This file
└── requirements.txt # Python dependencies
Clone the repository:
git clone <your-repo-url>
cd vqa-nle
Create and activate a virtual environment (recommended):
conda create --name venv python=3.10 -y
conda activate venv
Install dependencies:
pip install -r requirements.txt
To run the GRPO training script, execute the following command from the project's root directory:
python -m src.training.run_grpo
For training with InternVL models, use the specialized script:
bash VLM-R1/run_scripts/run_grpo_rec_internvl.sh
This guide provides comprehensive instructions for running the GRPO training script (run_grpo_rec_internvl.sh), including configuration, execution, and customization.
Before running the training script, ensure the following requirements are met:
requirements.txt are installed in your activated conda environment. DeepSpeed is required.ViVQA-X_train_grpo.jsonl exists in the data/processed/ directory.wandb login in your terminal. To disable it, uncomment export WANDB_DISABLED=true (line 26) in the script.The training process is controlled by variables and arguments in VLM-R1/run_scripts/run_grpo_rec_internvl.sh. Below is a detailed breakdown of key parameters.
These must be configured correctly for your environment.
| Variable | Line | Description |
|---|---|---|
data_paths | 8 | Required. Path to the ViVQA-X_train_grpo.jsonl file. |
image_folders | 9 | Required. Path to the directory containing COCO images (train2014). |
model_path | 10 | The Hugging Face model identifier or a local path to the pretrained model (e.g., OpenGVLab/InternVL3-1B). |
EXP_NAME | 15 | A unique name for your experiment. Logs and checkpoints will be saved under this name. |
CUDA_VISIBLE_DEVICES | 27 | The specific GPU(s) to use (e.g., 0, 0,1). Default is 2. |
--nproc_per_node | 28 | The number of GPUs to use. This should match the count in CUDA_VISIBLE_DEVICES. |
Important Note on
EXP_NAME: Avoid using forward slashes (/) in your experiment name (e.g.,my_experiment/run_1). The system will interpret the slash as a directory separator and create nested folders. This can cause theresume_from_checkpointlogic to fail because it may not correctly locate the checkpoint files inside the nested structure. It is recommended to use hyphens (-) or underscores (_) instead.
These parameters control the training loop and performance.
| Argument | Line | Description |
|---|---|---|
--per_device_train_batch_size | 42 | The number of samples processed per GPU in one forward pass. Adjust based on VRAM. |
--gradient_accumulation_steps | 43 | Number of updates steps to accumulate gradients before performing a backward pass. Effective batch size = nproc_per_node * per_device_train_batch_size * gradient_accumulation_steps. |
--learning_rate | 60 | The initial learning rate for the optimizer. |
--max_steps | 46 | The total number of training steps to perform. Overrides num_train_epochs. |
--save_steps | 52 | How often to save a model checkpoint, specified in number of steps. |
These parameters are unique to the Group Relative Policy Optimization algorithm.
| Argument | Line | Description |
|---|---|---|
--num_generations | 53 | The number of candidate responses to generate for each prompt during training. |
--reward_funcs | 55 | A space-separated list of reward functions to use for scoring generations (e.g., accuracy format). |
--beta | 56 | The KL divergence penalty coefficient. Controls how much the policy model can deviate from the reference model. |
| Argument | Line | Description |
|---|---|---|
--gradient_checkpointing | 44 | A memory optimization technique that trades compute for memory. Set to true to reduce VRAM usage. |
--freeze_vision_modules | 61 | If true, the weights of the vision encoder are frozen and not updated during training. |
--push_to_hub | 62 | If true, automatically pushes the final trained model to the Hugging Face Hub. |
--hub_model_id | 63 | The repository name for the model on the Hugging Face Hub (e.g., YourUsername/YourModelName). |
VLM-R1 directory.cd /home/vlai-vqa-nle/minhtq/vqa-nle/VLM-R1
```
2. **Run the Script**:
```bash
bash run_scripts/run_grpo_rec_internvl.sh
VLM-R1/runs/${EXP_NAME}/log/. If DEBUG_MODE (line 19) is "true", the log will include detailed rollout information.VLM-R1/checkpoints/rl/${EXP_NAME}/.wandb.ai under the project and run name you configured.0, change CUDA_VISIBLE_DEVICES to 0 and ensure --nproc_per_node is "1". If you encounter CUDA "out of memory" errors, reduce --per_device_train_batch_size.--reward_funcs argument (line 55) to add or remove rewards. For example, to add the explanation reward, change it to --reward_funcs accuracy format explanation.Adapting for a New VQA Dataset:
src/data/dataset_loader.py to handle your custom dataset's format..jsonl file consistent with the required format (containing id, image, and conversations fields).data_paths variable in the run script to point to your new dataset file.Switching to a Different Model:
model_path variable (line 10) to the Hugging Face identifier or local path of the new model.--per_device_train_batch_size and --learning_rate depending on the new model's size and architecture.InvernVLModule or implement a new module if needed.--per_device_train_batch_size. If that's not enough, enable --gradient_checkpointing true. You can also try reducing --max_completion_length.local_scripts/zero2.json) is correctly formatted and that DeepSpeed was installed properly.grpo_jsonl.py contains hard-coded logic that automatically resumes from a checkpoint if one is found in the output directory. This ignores and overrides the --resume_from_checkpoint False flag set in the run_grpo_rec_internvl.sh script. To start a fresh training run when parameters are changed, you have three options:
Option 1 (Workaround, Recommended): Change the EXP_NAME variable (line 15) in the run script. This creates a new, empty directory for checkpoints and logs, forcing a fresh start.
Option 2 (Workaround): Manually delete the contents of the existing checkpoint directory (VLM-R1/checkpoints/rl/${EXP_NAME}/).
Option 3 (Permanent Fix): Modify the logic in VLM-R1/src/open-r1-multimodal/src/open_r1/grpo_jsonl.py to respect the script's arguments.
Original Code (lines 1216-1219):
if list(pathlib.Path(training_args.output_dir).glob("checkpoint-*")):
trainer.train(resume_from_checkpoint=True)
else:
trainer.train()
Modified Code:
# This allows the --resume_from_checkpoint flag to control the behavior.
trainer.train(resume_from_checkpoint=training_args.resume_from_checkpoint)
The project includes custom data processing modules specifically designed for InternVL training:
src/data/dataset_loader.py)Purpose: Converts ViVQA-X dataset to GRPO-compatible JSONL format
Key Features:
Usage:
from src.data.dataset_loader import create_jsonl_for_grpo
# Create training data
create_jsonl_for_grpo("train")
# Create validation data
create_jsonl_for_grpo("val")
# Create test data
create_jsonl_for_grpo("test")
Output Format:
{
"id": 1,
"image": "COCO_train2014_000000000139.jpg",
"conversations": [
{
"from": "human",
"value": "<image>Bạn là một trợ lý AI chuyên gia... Câu hỏi: {question}"
},
{
"from": "gpt",
"value": "<answer>{answer}</answer><explain>{explanation}</explain>"
}
]
}
Prepare ViVQA-X Dataset:
# Ensure ViVQA-X data is available at:
/mnt/VLAI_data/ViVQA-X/
├── ViVQA-X_train.json
├── ViVQA-X_val.json
└── ViVQA-X_test.json
Prepare COCO Images:
# Ensure COCO images are available at:
/mnt/VLAI_data/COCO_Images/
├── train2014/
└── val2014/
Generate GRPO Data:
cd /home/vlai-vqa-nle/minhtq/vqa-nle
python -m src.data.dataset_loader
This project has been specifically adapted to support InternVL models with the following key modifications:
File: VLM-R1/src/open-r1-multimodal/src/open_r1/grpo_jsonl.py (line 942)
Added "vintern" detection to route to InvernVLModule:
elif "internvl" in model_name_or_path.lower() or "vintern" in model_name_or_path.lower():
return InvernVLModule
File: VLM-R1/run_scripts/run_grpo_rec_internvl.sh
1 (line 1) to skip CUDA compatibility checks.2 for 1-GPU training (line 26).torchrun --nproc_per_node="1" to match GPU count (line 27).File: VLM-R1/run_scripts/run_grpo_rec_internvl.sh
--gradient_checkpointing false, line 44).--per_device_train_batch_size 1 (line 42).Note on PEFT and InternVL: Using PEFT (LoRA) with InternVL can cause a
TypeError: InternVLChatModel.forward() got an unexpected keyword argument 'inputs_embeds'. This happens because:
- PEFT automatically injects the
inputs_embedsargument when using LoRA with CausalLM models.- The
InternVLmodel'sforwardmethod does not accept this argument.- The GRPO trainer passes all keyword arguments from PEFT to the model, leading to a crash. For this reason, PEFT is disabled for InternVL training.
File: VLM-R1/src/open-r1-multimodal/local_scripts/zero2.json
Optimized for InternVL with ZeRO Stage 2 and optimizer offloading:
{
"zero_optimization": {
"stage": 2,
"offload_optimizer": {
"device": "cpu",
"pin_memory": true
}
}
}
To avoid the self.img_context_token_id is not None error, the model initialization logic has been updated to correctly handle PEFT models by accessing the underlying base model:
if is_peft_model(model):
self.vlm_module.post_model_init(model.base_model.model, processing_class)
else:
self.vlm_module.post_model_init(model, processing_class)
if self.ref_model is not None:
if is_peft_model(self.ref_model):
self.vlm_module.post_model_init(self.ref_model.base_model.model, processing_class)
else:
self.vlm_module.post_model_init(self.ref_model, processing_class)
The training script logic was adjusted to ensure that training restarts from the beginning when parameters are changed, rather than always resuming from the latest checkpoint. The resume_from_checkpoint=True parameter is now conditional.
# Original logic always resumed if a checkpoint existed
if list(pathlib.Path(training_args.output_dir).glob("checkpoint-*")):
trainer.train(resume_from_checkpoint=True)
else:
trainer.train()
grpo_jsonl.py)File: VLM-R1/src/open-r1-multimodal/src/open_r1/grpo_jsonl.py
Problem: The original data loading logic was designed to automatically wrap solutions in <answer> tags. It first stripped any existing <answer> tags from the ground truth and then added them back. This created a logic conflict with pre-formatted data for ViVQA-X, which already contains both <answer> and <explain> tags, leading to incorrect nested tags (e.g., <answer>...<explain>...</explain></answer>).
Modification: The data processing pipeline has been adjusted to preserve the original tags from the input data. The code that stripped and re-added <answer> tags has been changed. The script now directly uses the solution string from the JSONL file, assuming it is already correctly formatted with all necessary tags.
Original Logic (Before):
# In the data loading loop
item['solution'] = solution_value.replace('<answer>', '').replace('</answer>', '').strip()
# In make_conversation_from_jsonl function
'solution': f"<answer> {example['solution']} </answer>",
Modified Logic (After):
# In the data loading loop
item['solution'] = solution_value # Directly use the pre-formatted string
# In make_conversation_from_jsonl function
'solution': example['solution'], # Use the solution as-is
This section provides guidance on how to customize the training pipeline, such as adding new reward functions or understanding how data is handled.
To integrate a new reward function into the GRPO training pipeline, follow these steps:
Implement the Reward Function:
VLM-R1/src/open-r1-multimodal/src/open_r1/grpo_jsonl.py.completions, solution, and **kwargs as arguments and return a list of floating-point scores. kwargs can be used to access additional data like image_path.def my_new_reward(completions, solution, **kwargs):
scores = []
# ... your logic here ...
return scores
Normalize the Reward Score:
accuracy, format, explanation).Register the Function:
reward_funcs_registry dictionary.
reward_funcs_registry = {
"accuracy": accuracy_reward,
"format": format_reward,
"explanation": explanation_reward,
# Add your new function here
"my_new_reward": my_new_reward,
}
Activate in Training Script:
VLM-R1/run_scripts/run_grpo_rec_internvl.sh).--reward_funcs argument.
--reward_funcs accuracy format explanation my_new_reward \
The data pipeline is optimized to handle images efficiently, especially for reward functions that require image data (e.g., CLIP-based scores).
image_path field. This is done in grpo_jsonl.py by combining the image_folders path from the run script with the image filename from the JSONL data.**kwargs dictionary for all reward functions. A function can access it like this:
def my_reward_with_images(completions, solution, **kwargs):
if 'image_path' in kwargs:
image_paths_list = kwargs['image_path']
# Now you can open and process images using these paths
for path in image_paths_list:
# ...
initialize_explanation_scorer). This ensures the model is loaded into memory only once per process, preventing repeated initialization on every batch.This section provides a deeper dive into the specific customizations made for the Vietnamese VQA task, including data preparation, custom reward functions, and VLM module modifications.
To adapt the training for the ViVQA-X dataset, a custom data loader was created at src/data/dataset_loader.py.
Key Features:
Vietnamese System Prompt: A specialized Vietnamese prompt is used to guide the model's response structure, requiring it to generate <think>, <answer>, and <explain> tags. This ensures the model follows a structured reasoning process.
prompt = """
<image> You are a Visual Question Answering system. Your task is to answer questions based on the content of the provided image.
You must respond in Vietnamese and your response **must** include all the tags <think> </think>, <answer> </answer>, <explain> </explain>.
Follow these steps precisely:
1. In the <think> tag, provide a step-by-step reasoning process.
2. In the <answer> tag, give one word or one short phrase.
3. In the <explain> tag, provide one brief sentence that justifies your answer.
Now, answer this question based on the image:
Question: {question}
""".strip()
GRPO JSONL Conversion: The script converts the standard ViVQA-X JSON format into the JSONL format required by the GRPO trainer, injecting the system prompt and structuring the ground truth solution.
# format câu trả lời
solution = f"<answer>{answer}</answer><explain>{explanation}</explain>"
To better evaluate the model's performance on the Vietnamese VQA task, custom reward functions were developed in the src/rewards/ directory and integrated into VLM-R1/src/open-r1-multimodal/src/open_r1/grpo_jsonl.py.
File: src/rewards/explaination_rewards.py
This reward function evaluates the quality of the generated explanation in the <explain> tag by combining two scores:
The final score is a weighted average of the two, normalized to a [0, 1] range. A singleton pattern (initialize_explanation_scorer) is used in grpo_jsonl.py to ensure the CLIP model is loaded only once.
File: src/rewards/outcome_rewards.py
This function provides a more nuanced evaluation of the answer in the <answer> tag compared to simple exact matching.
Scoring Logic:
This logic is integrated into grpo_jsonl.py at line 893 as the default accuracy reward.
The core training script grpo_jsonl.py was modified to support custom models and handle data more efficiently.
A VLM module system (VLM-R1/src/open-r1-multimodal/src/open_r1/vlm_modules/) was implemented to easily switch between different model architectures (e.g., Qwen2-VL, InternVL). To add a new model, you would create a new module inheriting from VLMBaseModule and implement the required methods. The model is then registered in the get_vlm_module function in grpo_jsonl.py.
elif "internvl" in model_name_or_path.lower() or "vintern" in model_name_or_path.lower():
return InvernVLModule
else:
raise ValueError(f"Unsupported model: {model_name_or_path}")
The original data loading logic in the GRPO trainer would strip <answer> tags and then re-add them. This conflicted with our pre-formatted data which also included <explain> tags. The logic was modified to preserve the solution string as-is, ensuring the format is maintained.
item['solution'] = solution_value.replace('<answer>', '')...line 1142): item['solution'] = solution_valueInstead of loading all images into memory at the start, the data pipeline was modified to only store image file paths. The images are loaded just-in-time during the training step. This significantly reduces memory usage and speeds up initialization. The image paths are then passed to reward functions that require them (like the Explanation Reward).
Here is a summary of how to use these customizations together:
Prepare Data: Use the custom script to generate the GRPO-compatible JSONL file.
from src.data.dataset_loader import create_jsonl_for_grpo
create_jsonl_for_grpo("train")
Integrate Custom Rewards: In grpo_jsonl.py, import and register your custom reward functions.
# Import your custom reward functions
from src.rewards.explaination_rewards import ExplanationRewardScorer
from src.rewards.outcome_rewards import accuracy_reward as custom_accuracy_reward
# In accuracy_reward function (line 893)
reward = custom_accuracy_reward(content, sol)
# In reward_funcs_registry, ensure `explanation` is active
reward_funcs_registry = {
"accuracy": accuracy_reward,
"format": format_reward,
"explanation": explanation_reward,
}
Configure Training Script: In run_grpo_rec_internvl.sh, add explanation to the list of active rewards.
--reward_funcs accuracy format explanation \
45 commits
3 commits
Jupyter Notebook
89.8%
Python
9.5%