Skip to content

Commit 3c3e954

Browse files
Start adapting script for VLM training
1 parent c4d4126 commit 3c3e954

9 files changed

Lines changed: 139 additions & 34 deletions

File tree

README_AGENTS.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,16 @@ Launch:
33
sbatch --nodes=1 slurm/train.slurm --model SmolLM2-1.7B-Instruct --task sft --config agent --accelerator zero3
44
```
55
Refers to the config recipes/SmolLM2-1.7B-Instruct/sft/config_agent.yaml
6-
zero3 is one of the accelerate configs in recipes/accelerate_configs
6+
zero3 is one of the accelerate configs in recipes/accelerate_configs
7+
8+
9+
10+
Launch VLM training:
11+
```bash
12+
sbatch --nodes=1 slurm/train.slurm --model Qwen2.5-VL-3B-Instruct --task sft --config agent --accelerator zero3
13+
```
14+
15+
Simple mode
16+
```bash
17+
sbatch --nodes=1 slurm/train.slurm --model Qwen2.5-VL-3B-Instruct --task sft --config agent --accelerator ddp
18+
```

logs/.gitkeep

Whitespace-only changes.

recipes/Qwen2.5-VL-3B-Instruct/sft/config_agent.yaml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Model arguments
22
# You can download the model and manually change the rope to 300k/500k and max_position_embeddings to 32768
33
model_name_or_path: Qwen/Qwen2.5-VL-3B-Instruct
4+
vision_model: true
45
model_revision: main
56
torch_dtype: bfloat16
67
attn_implementation: sdpa
@@ -42,4 +43,16 @@ report_to:
4243
save_strategy: "steps"
4344
save_steps: 500
4445
save_total_limit: 1
45-
seed: 42
46+
seed: 42
47+
48+
dataset_mixture:
49+
datasets: # List of datasets to include in the mixture
50+
- id: smolagents/aguvis-stage-2 # Hub dataset ID
51+
config: mind2web # Name of the dataset config
52+
split: train # Split to use from the dataset
53+
columns: # Columns to keep
54+
- images
55+
- texts
56+
weight: 1. # Fraction of dataset to use
57+
seed: 42 # Seed for shuffling the combined dataset
58+
test_split_size: 0.1

recipes/SmolLM2-1.7B-Instruct/sft/config_agent.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ per_device_train_batch_size: 4 # Change this depending on the context length of
2222

2323
# SFT trainer config
2424
max_steps: -1
25-
num_train_epochs: 6
25+
num_train_epochs: 1
2626
bf16: true
2727
do_eval: false
2828
eval_strategy: 'no'

slurm/train.slurm

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ if [[ "$*" == *"--help"* ]]; then
2323
exit 0
2424
fi
2525

26-
HF_DATASETS_CACHE="/fsx/aymeric/.cache/datasets"
27-
TRANSFORMERS_CACHE="/fsx/aymeric/.cache/transformers"
2826

2927
# Specific configuration optimized for the Hugging Face Compute Cluster
3028
module load cuda/12.4
@@ -88,10 +86,8 @@ while [[ $# -gt 0 ]]; do
8886
esac
8987
done
9088

91-
export HF_DATASETS_CACHE="/fsx/aymeric/.cache/datasets"
92-
export TRANSFORMERS_CACHE="/fsx/aymeric/.cache/transformers"
93-
HF_DATASETS_CACHE="/fsx/aymeric/.cache/datasets"
94-
TRANSFORMERS_CACHE="/fsx/aymeric/.cache/transformers"
89+
export HF_HOME="/fsx/aymeric/.cache/"
90+
HF_HOME="/fsx/aymeric/.cache/"
9591

9692
# Validate required arguments
9793
if [[ -z "$MODEL" || -z "$TASK" || -z "$CONFIG_SUFFIX" || -z "$ACCELERATOR" ]]; then
@@ -143,8 +139,8 @@ if [[ "$USE_VLLM" == "true" ]]; then
143139
fi
144140

145141
# force crashing on nccl issues like hanging broadcast
146-
export NCCL_ASYNC_ERROR_HANDLING=1
147-
# export NCCL_DEBUG=INFO
142+
export TORCH_NCCL_ASYNC_ERROR_HANDLING=1
143+
export NCCL_DEBUG=INFO
148144
# export NCCL_DEBUG_SUBSYS=COLL
149145
# export NCCL_SOCKET_NTHREADS=1
150146
# export NCCL_NSOCKS_PERTHREAD=1

src/open_r1/configs.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ class SFTConfig(trl.SFTConfig):
185185
default=None,
186186
metadata={"help": "The optional system prompt to use for benchmarking."},
187187
)
188+
vision_model: bool = field(
189+
default=False,
190+
metadata={"help": "Whether this is a vision-language model training."},
191+
)
188192
hub_model_revision: Optional[str] = field(
189193
default="main",
190194
metadata={"help": "The Hub model branch to push the model to."},

src/open_r1/sft.py

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
"""
16-
Supervised fine-tuning script for decoder language models.
16+
Supervised fine-tuning script for decoder language models and vision-language models.
1717
1818
Usage:
1919
@@ -39,18 +39,48 @@
3939

4040
import datasets
4141
import transformers
42-
from transformers import set_seed
42+
from transformers import set_seed, AutoModelForVision2Seq, AutoProcessor, LlavaForConditionalGeneration
4343
from transformers.trainer_utils import get_last_checkpoint
4444
from trl import ModelConfig, SFTTrainer, TrlParser, get_peft_config, setup_chat_format
4545

4646
from open_r1.configs import ScriptArguments, SFTConfig
47-
from open_r1.utils import get_dataset, get_model, get_tokenizer
47+
from open_r1.utils import get_dataset, get_model, get_tokenizer, get_processor
4848
from open_r1.utils.callbacks import get_callbacks
4949
from open_r1.utils.wandb_logging import init_wandb_training
5050

5151
logger = logging.getLogger(__name__)
5252

5353

54+
def create_vlm_collate_fn(processor):
55+
"""Create a data collator for VLM training that handles images and text."""
56+
57+
def collate_fn(examples):
58+
# Get the texts and images, and apply the chat template
59+
texts = [processor.apply_chat_template(example["messages"], tokenize=False) for example in examples]
60+
images = [example["images"] for example in examples]
61+
62+
# Handle LLaVA 1.5 which doesn't support multiple images
63+
if isinstance(processor.model, LlavaForConditionalGeneration):
64+
images = [image[0] if image else None for image in images]
65+
66+
# Tokenize the texts and process the images
67+
batch = processor(text=texts, images=images, return_tensors="pt", padding=True)
68+
69+
# The labels are the input_ids, and we mask the padding tokens in the loss computation
70+
labels = batch["input_ids"].clone()
71+
labels[labels == processor.tokenizer.pad_token_id] = -100
72+
73+
# Ignore the image token index in the loss computation (model specific)
74+
if hasattr(processor, 'image_token'):
75+
image_token_id = processor.tokenizer.convert_tokens_to_ids(processor.image_token)
76+
labels[labels == image_token_id] = -100
77+
78+
batch["labels"] = labels
79+
return batch
80+
81+
return collate_fn
82+
83+
5484
def main(script_args, training_args, model_args):
5585
set_seed(training_args.seed)
5686

@@ -84,29 +114,54 @@ def main(script_args, training_args, model_args):
84114
init_wandb_training(training_args)
85115

86116
######################################
87-
# Load dataset, tokenizer, and model #
117+
# Load dataset, processor/tokenizer, and model #
88118
######################################
89119
dataset = get_dataset(script_args)
90-
tokenizer = get_tokenizer(model_args, training_args)
91-
model = get_model(model_args, training_args)
92120

93-
if tokenizer.chat_template is None:
94-
logger.info("No chat template provided, defaulting to ChatML.")
95-
model, tokenizer = setup_chat_format(model, tokenizer, format="chatml")
121+
if training_args.vision_model:
122+
logger.info("Setting up vision-language model training")
123+
124+
# Set VLM-specific training arguments (following TRL reference)
125+
training_args.gradient_checkpointing_kwargs = dict(use_reentrant=False)
126+
training_args.remove_unused_columns = False
127+
training_args.dataset_kwargs = {"skip_prepare_dataset": True}
128+
129+
# Load processor and model for VLM
130+
processor = get_processor(model_args, training_args)
131+
model = get_model(model_args, training_args) # This should return AutoModelForVision2Seq
132+
data_collator = create_vlm_collate_fn(processor)
133+
processing_class = processor.tokenizer
134+
model_tags = ["open-r1", "vision-language", "vlm"]
135+
136+
else:
137+
logger.info("Setting up text-only model training")
138+
139+
# Load tokenizer and model for text-only
140+
tokenizer = get_tokenizer(model_args, training_args)
141+
model = get_model(model_args, training_args)
142+
143+
if tokenizer.chat_template is None:
144+
logger.info("No chat template provided, defaulting to ChatML.")
145+
model, tokenizer = setup_chat_format(model, tokenizer, format="chatml")
146+
147+
data_collator = None # Use default
148+
processing_class = tokenizer
149+
model_tags = ["open-r1"]
96150

97151
############################
98152
# Initialize the SFT Trainer
99153
############################
100154
trainer = SFTTrainer(
101155
model=model,
102156
args=training_args,
157+
data_collator=data_collator,
103158
train_dataset=dataset[script_args.dataset_train_split],
104159
eval_dataset=(
105160
dataset[script_args.dataset_test_split]
106161
if training_args.eval_strategy != "no"
107162
else None
108163
),
109-
processing_class=tokenizer,
164+
processing_class=processing_class,
110165
peft_config=get_peft_config(model_args),
111166
callbacks=get_callbacks(training_args, model_args),
112167
)
@@ -131,16 +186,13 @@ def main(script_args, training_args, model_args):
131186
# Save model and create model card
132187
##################################
133188
logger.info("*** Save model ***")
134-
# Align the model's generation config with the tokenizer's eos token
135-
# to avoid unbounded generation in the transformers `pipeline()` function
136-
trainer.model.generation_config.eos_token_id = tokenizer.eos_token_id
137189
trainer.save_model(training_args.output_dir)
138190
logger.info(f"Model saved to {training_args.output_dir}")
139191

140192
# Save everything else on main process
141193
kwargs = {
142194
"dataset_name": script_args.dataset_name,
143-
"tags": ["open-r1"],
195+
"tags": model_tags,
144196
}
145197
if trainer.accelerator.is_main_process:
146198
trainer.create_model_card(**kwargs)
@@ -164,6 +216,9 @@ def main(script_args, training_args, model_args):
164216
if training_args.push_to_hub:
165217
logger.info("Pushing to hub...")
166218
trainer.push_to_hub(**kwargs)
219+
# Also push processor for VLM models
220+
if training_args.vision_model and trainer.accelerator.is_main_process:
221+
processor.push_to_hub(training_args.hub_model_id)
167222

168223

169224
if __name__ == "__main__":

src/open_r1/utils/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .data import get_dataset
22
from .import_utils import is_e2b_available, is_morph_available
3-
from .model_utils import get_model, get_tokenizer
3+
from .model_utils import get_model, get_tokenizer, get_processor
44

55

6-
__all__ = ["get_tokenizer", "is_e2b_available", "is_morph_available", "get_model", "get_dataset"]
6+
__all__ = ["get_tokenizer", "get_processor", "is_e2b_available", "is_morph_available", "get_model", "get_dataset"]

src/open_r1/utils/model_utils.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import torch
2-
from transformers import AutoModelForCausalLM, AutoTokenizer, PreTrainedTokenizer
2+
from transformers import AutoModelForCausalLM, AutoTokenizer, PreTrainedTokenizer, AutoProcessor, AutoModelForVision2Seq
33

44
from trl import ModelConfig, get_kbit_device_map, get_quantization_config
55

@@ -20,8 +20,22 @@ def get_tokenizer(model_args: ModelConfig, training_args: SFTConfig | GRPOConfig
2020
return tokenizer
2121

2222

23-
def get_model(model_args: ModelConfig, training_args: SFTConfig | GRPOConfig) -> AutoModelForCausalLM:
24-
"""Get the model"""
23+
def get_processor(model_args: ModelConfig, training_args: SFTConfig | GRPOConfig) -> AutoProcessor:
24+
"""Get the processor for VLM models."""
25+
processor = AutoProcessor.from_pretrained(
26+
model_args.model_name_or_path,
27+
revision=model_args.model_revision,
28+
trust_remote_code=model_args.trust_remote_code,
29+
)
30+
31+
if training_args.chat_template is not None:
32+
processor.chat_template = training_args.chat_template
33+
34+
return processor
35+
36+
37+
def get_model(model_args: ModelConfig, training_args: SFTConfig | GRPOConfig) -> AutoModelForCausalLM | AutoModelForVision2Seq:
38+
"""Get the model - supports both text-only and vision-language models"""
2539
torch_dtype = (
2640
model_args.torch_dtype if model_args.torch_dtype in ["auto", None] else getattr(torch, model_args.torch_dtype)
2741
)
@@ -35,8 +49,19 @@ def get_model(model_args: ModelConfig, training_args: SFTConfig | GRPOConfig) ->
3549
device_map=get_kbit_device_map() if quantization_config is not None else None,
3650
quantization_config=quantization_config,
3751
)
38-
model = AutoModelForCausalLM.from_pretrained(
39-
model_args.model_name_or_path,
40-
**model_kwargs,
41-
)
52+
53+
# Check if this is a VLM model using the explicit flag
54+
if hasattr(training_args, 'vision_model') and training_args.vision_model:
55+
# Load as vision-language model
56+
model = AutoModelForVision2Seq.from_pretrained(
57+
model_args.model_name_or_path,
58+
**model_kwargs,
59+
)
60+
else:
61+
# Load as text-only model
62+
model = AutoModelForCausalLM.from_pretrained(
63+
model_args.model_name_or_path,
64+
**model_kwargs,
65+
)
66+
4267
return model

0 commit comments

Comments
 (0)