# Copyright 2025 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Supervised fine-tuning script for decoder language models. Usage: # One 1 node of 8 x H100s accelerate launch --config_file=configs/zero3.yaml src/open_r1/sft.py \ --model_name_or_path Qwen/Qwen2.5-1.5B-Instruct \ --dataset_name HuggingFaceH4/Bespoke-Stratos-17k \ --learning_rate 2.0e-5 \ --num_train_epochs 1 \ --packing \ --max_seq_length 4096 \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 4 \ --gradient_checkpointing \ --bf16 \ --logging_steps 5 \ --eval_strategy steps \ --eval_steps 100 \ --output_dir data/Qwen2.5-1.5B-Open-R1-Distill """ import logging import os import sys import datasets from dataclasses import dataclass, field from typing import Optional import torch import transformers from datasets import load_dataset, load_from_disk, Dataset, DatasetDict from transformers import AutoTokenizer, set_seed, AutoProcessor from transformers.trainer_utils import get_last_checkpoint import trl from trl import ( ModelConfig, ScriptArguments, SFTTrainer, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) from tqdm import tqdm import json import random from qwen_vl_utils import process_vision_info logger = logging.getLogger(__name__) @dataclass class SFTScriptArguments(ScriptArguments): """ Script arguments for the GRPO training script. Args: reward_funcs (`list[str]`): List of reward functions. Possible values: 'iou', 'format'. """ train_data_path: str = field( default="./Charades/charades_annotation/train.json", metadata={"help": "Path to the training data JSON file."}, ) eval_data_path: str = field( default="./Charades/charades_annotation/val.json", metadata={"help": "Path to the evaluation data JSON file."}, ) video_folder: str = field( default="./Charades/Charades_v1", # Replace with your actual video folder path metadata={"help": "Path to the folder containing video files."}, ) preprocessed_data_path: Optional[str] = field( # Add preprocessed_data_path argument default="", metadata={"help": "Path to the preprocessed dataset directory. If provided, load preprocessed data instead of raw videos."}, ) @dataclass class SFTConfig(trl.SFTConfig): """ args for callbacks, benchmarks etc """ benchmarks: list[str] = field( default_factory=lambda: [], metadata={"help": "The benchmarks to run after training."} ) callbacks: list[str] = field( default_factory=lambda: [], metadata={"help": "The callbacks to run during training."} ) system_prompt: Optional[str] = field( default=None, metadata={"help": "The optional system prompt to use for benchmarking."}, ) hub_model_revision: Optional[str] = field( default="main", metadata={"help": "The Hub model branch to push the model to."}, ) overwrite_hub_revision: bool = field(default=False, metadata={"help": "Whether to overwrite the Hub revision."}) push_to_hub_revision: bool = field(default=False, metadata={"help": "Whether to push to a Hub revision/branch."}) def load_json_dataset(train_data_path, eval_data_path, video_folder, preprocessed_data_path=None): # Modified to accept preprocessed_data_path def create_dataset_from_json(file_path, split_name): with open(file_path, 'r') as f: data = json.load(f) examples = [] for video_id, video_data in tqdm(data.items()): for sentence_id, (timestamps, sentence) in enumerate(zip(video_data['timestamps'], video_data['sentences'])): sentence = sentence.strip().lower() if sentence.endswith("."): sentence = sentence[:-1] video_filename_base = video_id video_path = None for ext in ['mp4', 'mkv', 'webm']: candidate_path = os.path.join(video_folder, f"{video_filename_base}.{ext}") if os.path.isfile(candidate_path): video_path = candidate_path break if video_path is None: print(f"Warning: Video file not found for ID: {video_id}") continue example = { "problem": sentence, # "solution": (timestamps[0] / video_data['duration'], timestamps[1] / video_data['duration']), "solution": (timestamps[0], timestamps[1]), "video_path": video_path, "durations": video_data['duration'], "preprocessed_path": "" # Initialize preprocessed_path as None } if preprocessed_data_path != "": # If preprocessed data path is provided, construct the path example["preprocessed_path"] = os.path.join(preprocessed_data_path, split_name, f"{video_id}_{sentence_id}") examples.append(example) random.shuffle(examples) print(len(examples)) print(examples[:5]) dataset = Dataset.from_list(examples) def __getitem__(self, idx): # Define getitem within the scope where dataset is available example = dataset[idx] # return example data_to_return = {k: v for k, v in example.items()} # Create a copy to avoid modifying original dataset if example["preprocessed_path"] != "": # Check if preprocessed path exists try: data_to_return["video_inputs"] = [torch.load(os.path.join(example["preprocessed_path"][0], "video_inputs.pt"))] with open(os.path.join(example["preprocessed_path"][0], "video_kwargs.json"), 'r') as f: data_to_return["video_kwargs"] = [json.load(f)] data_to_return["use_preprocessed"] = [True] # Flag to indicate preprocessed data is used except Exception as e: print(f"Warning: Error loading preprocessed data from {example['preprocessed_path'][0]}, falling back to video_path. Error: {e}") data_to_return["use_preprocessed"] = [False] # Fallback to video_path if loading fails else: data_to_return["use_preprocessed"] = [False] # No preprocessed data to use or path invalid return data_to_return dataset.__getitem__ = __getitem__.__get__(dataset, Dataset) # Bind getitem to the dataset return dataset train_dataset = create_dataset_from_json(train_data_path, "train") eval_dataset = create_dataset_from_json(eval_data_path, "eval") return DatasetDict({"train": train_dataset, "eval": eval_dataset}) processor = None QUESTION_TEMPLATE = """To accurately pinpoint the event "[EVENT]" in the video, determine the precise time period of the event. Provide the start and end times (in seconds, precise to two decimal places) in the format "start time to end time" within the tags. For example: "12.54 to 17.83".""" def convert_example(example): """ correct example into "messages" eg: { "system": "You are a helpful assistant.", "conversations": [ {"from": "user", "value": "How many objects are included in this image?", "image_path": "/path/to/image.png"}, {"from": "assistant", "value": "\nI can see 10 objects\n\n\n10\n"} ] } """ messages = [] messages.append({ "role": "user", "content": [ {"type": "text", "text": QUESTION_TEMPLATE.replace("[EVENT]", example["problem"])}, {"type": "video", "video": example["video_path"], "total_pixels": 3584 * 28 * 28, "min_pixels": 16 * 28 * 28, }, ] }) st, ed = example["solution"] answer_text = f"{str(st)} to {str(ed)}" messages.append({ "role": "assistant", "content": answer_text, }) example["messages"] = messages return example def collate_fn(examples): texts = [ processor.apply_chat_template( convert_example(example)["messages"], tokenize=False, add_generation_prompt=True) for example in examples ] video_inputs = [x["video_inputs"] for x in examples] fps_inputs = [x["video_kwargs"]["fps"] for x in examples] video_inputs = video_inputs[0] fps_inputs = fps_inputs[0] image_inputs = None batch = processor( text=[texts[0]], images=image_inputs, videos=[video_inputs[0]], fps=[fps_inputs[0]], return_tensors="pt", padding=True, ) labels = batch["input_ids"].clone() labels[labels == processor.tokenizer.pad_token_id] = -100 video_token_id = processor.tokenizer.convert_tokens_to_ids(processor.video_token) labels[labels == video_token_id] = -100 batch["labels"] = labels return batch def main(script_args, training_args, model_args): # Set seed for reproducibility set_seed(training_args.seed) ############### # Setup logging ############### logging.basicConfig( format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", handlers=[logging.StreamHandler(sys.stdout)], ) log_level = training_args.get_process_log_level() logger.setLevel(log_level) datasets.utils.logging.set_verbosity(log_level) transformers.utils.logging.set_verbosity(log_level) transformers.utils.logging.enable_default_handler() transformers.utils.logging.enable_explicit_format() # Log on each process a small summary logger.warning( f"Process rank: {training_args.local_rank}, device: {training_args.device}, n_gpu: {training_args.n_gpu}" + f" distributed training: {bool(training_args.local_rank != -1)}, 16-bits training: {training_args.fp16}" ) logger.info(f"Model parameters {model_args}") logger.info(f"Script parameters {script_args}") logger.info(f"Data parameters {training_args}") # Check for last checkpoint last_checkpoint = None if os.path.isdir(training_args.output_dir): last_checkpoint = get_last_checkpoint(training_args.output_dir) if last_checkpoint is not None and training_args.resume_from_checkpoint is None: logger.info(f"Checkpoint detected, resuming training at {last_checkpoint=}.") ################ # Load datasets ################ # dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) dataset = load_json_dataset( script_args.train_data_path, script_args.eval_data_path, script_args.video_folder, script_args.preprocessed_data_path # Pass preprocessed_data_path ) ################ # Load tokenizer ################ global processor if "vl" in model_args.model_name_or_path.lower(): processor = AutoProcessor.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) logger.info("Using AutoProcessor for vision-language model.") else: processor = AutoTokenizer.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, use_fast=True ) logger.info("Using AutoTokenizer for text-only model.") if hasattr(processor, "pad_token") and processor.pad_token is None: processor.pad_token = processor.eos_token elif hasattr(processor.tokenizer, "pad_token") and processor.tokenizer.pad_token is None: processor.tokenizer.pad_token = processor.tokenizer.eos_token ################### # Model init kwargs ################### logger.info("*** Initializing model kwargs ***") torch_dtype = ( model_args.torch_dtype if model_args.torch_dtype in ["auto", None] else getattr(torch, model_args.torch_dtype) ) quantization_config = get_quantization_config(model_args) model_kwargs = dict( revision=model_args.model_revision, trust_remote_code=model_args.trust_remote_code, attn_implementation=model_args.attn_implementation, torch_dtype=torch_dtype, use_cache=False if training_args.gradient_checkpointing else True, device_map=get_kbit_device_map() if quantization_config is not None else None, quantization_config=quantization_config, use_sliding_window=True, ) # training_args.model_init_kwargs = model_kwargs from transformers import Qwen2_5_VLForConditionalGeneration model = Qwen2_5_VLForConditionalGeneration.from_pretrained( model_args.model_name_or_path, # torch_dtype=torch.bfloat16, **model_kwargs ) ############################ # Initialize the SFT Trainer ############################ training_args.dataset_kwargs = { "skip_prepare_dataset": True, } training_args.remove_unused_columns = False trainer = SFTTrainer( model=model, args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"] if training_args.eval_strategy != "no" else None, processing_class=processor.tokenizer, data_collator=collate_fn, peft_config=get_peft_config(model_args) ) ############### # Training loop ############### logger.info("*** Train ***") checkpoint = None if training_args.resume_from_checkpoint is not None: checkpoint = training_args.resume_from_checkpoint elif last_checkpoint is not None: checkpoint = last_checkpoint train_result = trainer.train(resume_from_checkpoint=checkpoint) metrics = train_result.metrics metrics["train_samples"] = len(dataset[script_args.dataset_train_split]) trainer.log_metrics("train", metrics) trainer.save_metrics("train", metrics) trainer.save_state() ################################## # Save model and create model card ################################## logger.info("*** Save model ***") trainer.save_model(training_args.output_dir) processor.save_pretrained(training_args.output_dir) logger.info(f"Model saved to {training_args.output_dir}") # Save everything else on main process kwargs = { "dataset_name": script_args.dataset_name, "tags": ["R1-V"], } if trainer.accelerator.is_main_process: trainer.create_model_card(**kwargs) # Restore k,v cache for fast inference trainer.model.config.use_cache = True trainer.model.config.save_pretrained(training_args.output_dir) ############# # push to hub ############# if training_args.push_to_hub: logger.info("Pushing to hub...") trainer.push_to_hub(**kwargs) processor.push_to_hub(training_args.hub_model_id) if __name__ == "__main__": parser = TrlParser((SFTScriptArguments, SFTConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() main(script_args, training_args, model_args)