fgar13
Add ASL Qwen training pipeline
babffc8
Raw
History Blame Contribute Delete
6.53 kB
from __future__ import annotations
import argparse
import sys
from pathlib import Path
import torch
from accelerate import Accelerator
from peft import PeftModel
from torch.utils.data import DataLoader
from tqdm import tqdm
sys.path.append(str(Path(__file__).resolve().parents[1]))
from src.datasets import VideoJsonlDataset
from src.io_utils import append_jsonl, apply_overrides, load_config, output_dir, save_config
from src.qwen_video_utils import load_model_for_training, load_processor, processor_inputs
from src.train_utils import (
apply_lora,
build_optimizer,
build_scheduler,
oom_help,
quantization_config_from_config,
)
class QwenVideoSFTCollator:
def __init__(self, processor):
self.processor = processor
def __call__(self, batch):
if len(batch) != 1:
raise ValueError(
"This first-version video collator supports per_device_train_batch_size=1. "
"Use gradient_accumulation_steps for larger effective batches."
)
sample = batch[0]
messages = sample["messages"]
if len(messages) < 2 or messages[-1].get("role") != "assistant":
raise ValueError(f"No assistant label in training sample id={sample.get('id')}")
full_inputs = processor_inputs(self.processor, [messages], add_generation_prompt=False)
prompt_messages = [m for m in messages if m.get("role") != "assistant"]
prompt_inputs = processor_inputs(self.processor, [prompt_messages], add_generation_prompt=True)
labels = full_inputs["input_ids"].clone()
prompt_len = min(prompt_inputs["input_ids"].shape[1], labels.shape[1])
labels[:, :prompt_len] = -100
pad_id = self.processor.tokenizer.pad_token_id
labels[full_inputs["input_ids"] == pad_id] = -100
full_inputs["labels"] = labels
return full_inputs
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--config", required=True)
parser.add_argument("--adapter_path", default=None)
parser.add_argument("--max_samples", type=int, default=None)
parser.add_argument("--model_name", default=None)
parser.add_argument("--output_dir", default=None)
args = parser.parse_args()
cfg = apply_overrides(load_config(args.config), model_name=args.model_name, output_dir=args.output_dir)
out_dir = output_dir(cfg)
adapter_dir = out_dir / "adapter"
out_dir.mkdir(parents=True, exist_ok=True)
accelerator = Accelerator(
gradient_accumulation_steps=int(cfg.get("gradient_accumulation_steps", 8)),
mixed_precision="bf16" if cfg.get("bf16", True) else ("fp16" if cfg.get("fp16", False) else "no"),
)
if not torch.cuda.is_available():
accelerator.print("Warning: CUDA is unavailable. QLoRA training is expected to require CUDA.")
processor = load_processor(cfg["model_name"])
dataset = VideoJsonlDataset(
cfg["train_jsonl"],
video_column=cfg["video_column"],
target_column=cfg["target_column"],
prompt=cfg["prompt"],
video_fps=cfg.get("video_fps"),
max_frames=cfg.get("max_frames"),
max_samples=args.max_samples or int(cfg.get("train_max_samples", 0)),
require_target=True,
)
if len(dataset) == 0:
raise ValueError("Training dataset is empty.")
batch_size = int(cfg.get("per_device_train_batch_size", 1))
loader = DataLoader(
dataset,
batch_size=batch_size,
shuffle=True,
collate_fn=QwenVideoSFTCollator(processor),
)
dtype = "bfloat16" if cfg.get("bf16", True) else "float16"
device_map = {"": accelerator.process_index} if torch.cuda.is_available() else None
model = load_model_for_training(
cfg["model_name"],
quantization_config=quantization_config_from_config(cfg),
device_map=device_map,
dtype=dtype,
)
if cfg.get("gradient_checkpointing", True):
model.gradient_checkpointing_enable()
if hasattr(model, "config"):
model.config.use_cache = False
if args.adapter_path:
accelerator.print(f"Continuing training from adapter: {args.adapter_path}")
model = PeftModel.from_pretrained(model, args.adapter_path, is_trainable=True)
else:
model = apply_lora(model, cfg)
if accelerator.is_main_process:
model.print_trainable_parameters()
save_config(cfg, out_dir / "training_config.yaml")
optimizer = build_optimizer(model, cfg)
steps_per_epoch = max(1, len(loader) // int(cfg.get("gradient_accumulation_steps", 8)))
scheduler = build_scheduler(optimizer, cfg, steps_per_epoch)
model, optimizer, loader, scheduler = accelerator.prepare(model, optimizer, loader, scheduler)
global_step = 0
model.train()
try:
for epoch in range(int(cfg.get("num_train_epochs", 1))):
progress = tqdm(loader, disable=not accelerator.is_main_process, desc=f"epoch {epoch + 1}")
for batch in progress:
with accelerator.accumulate(model):
outputs = model(**batch)
loss = outputs.loss
accelerator.backward(loss)
if accelerator.sync_gradients:
accelerator.clip_grad_norm_(model.parameters(), float(cfg.get("max_grad_norm", 1.0)))
optimizer.step()
scheduler.step()
optimizer.zero_grad()
if accelerator.sync_gradients:
global_step += 1
loss_value = float(loss.detach().float().cpu())
progress.set_postfix(loss=loss_value, step=global_step)
if accelerator.is_main_process:
append_jsonl(
out_dir / "train_log.jsonl",
{"epoch": epoch + 1, "step": global_step, "loss": loss_value},
)
except torch.cuda.OutOfMemoryError as exc:
raise RuntimeError(oom_help()) from exc
accelerator.wait_for_everyone()
if accelerator.is_main_process:
unwrapped = accelerator.unwrap_model(model)
unwrapped.save_pretrained(adapter_dir)
processor.save_pretrained(adapter_dir)
print(f"Saved adapter to {adapter_dir}")
print(f"Saved training config to {out_dir / 'training_config.yaml'}")
print(f"Saved train log to {out_dir / 'train_log.jsonl'}")
if __name__ == "__main__":
main()