Skip to content

Commit 93ff950

Browse files
authored
vlm support (#5)
1 parent 06f9010 commit 93ff950

17 files changed

Lines changed: 1097 additions & 493 deletions

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,64 @@ Unless you overwrote `output_dir`, results will be stored in `outputs/training_t
5858

5959
When training is done, you can bundle your output checkpoint with `leap-bundle` to use it directly within LEAP. Checkout our [Quick Start guide](https://leap.liquid.ai/docs/leap-bundle/quick-start?utm_source=github&utm_medium=link&utm_campaign=LEAP&utm_content=general).
6060

61+
## 📊 Expected Dataset Formats
62+
63+
### SFT (Supervised Fine-Tuning)
64+
65+
```json
66+
{
67+
"messages": [
68+
{ "role": "user", "content": "What is the capital of France?" },
69+
{ "role": "assistant", "content": "The capital of France is Paris." }
70+
]
71+
}
72+
```
73+
74+
### DPO (Direct Preference Optimization)
75+
76+
```json
77+
{
78+
"prompt": "What is the capital of France?",
79+
"chosen": "The capital of France is Paris.",
80+
"rejected": "The capital of France is London."
81+
}
82+
```
83+
84+
### VLM SFT (Vision-Language Model)
85+
86+
```json
87+
{
88+
"messages": [
89+
{
90+
"role": "system",
91+
"content": [
92+
{
93+
"type": "text",
94+
"text": "You are an image-based assistant. Answer questions based on the provided image."
95+
}
96+
]
97+
},
98+
{
99+
"role": "user",
100+
"content": [
101+
{ "type": "image", "image": "/path/to/image.jpg" },
102+
{ "type": "text", "text": "What do you see in this image?" }
103+
]
104+
},
105+
{
106+
"role": "assistant",
107+
"content": [{ "type": "text", "text": "I see a car in the image." }]
108+
}
109+
]
110+
}
111+
```
112+
113+
> **Note**: VLM datasets commonly have images in a separate row and are referenced in the messages column. If your image URLs or Paths are in a separate column from your messages, you'll need to merge the images into the 'messages' section like above.
114+
61115
## 🧪 Advanced Configuration
62116

117+
### Default Configs Location and Adding New Configs
118+
63119
The default configurations are located in:
64120

65121
- **SFT Training**: [`src/leap_finetune/configs/sft_configs.py`](./src/leap_finetune/configs/sft_configs.py)

config.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
"mlabonne/orpo-dpo-mix-40k", "dpo", limit=1000, test_size=0.2, subset="default"
2828
)
2929

30+
example_vlm_sft_dataset = DatasetLoader(
31+
"alay2shah/example-vlm-sft-dataset", "vlm_sft", limit=None, test_size=0.2
32+
)
33+
3034

3135
#################################
3236
# Training Config #
@@ -37,10 +41,12 @@
3741
3842
Default SFT: TrainingConfig.DEFAULT_SFT
3943
Default DPO: TrainingConfig.DEFAULT_DPO
44+
Default VLM SFT: TrainingConfig.DEFAULT_VLM_SFT
4045
41-
Default LORA: PeftConfig.DEFAULT_LORA
42-
High R LORA: PeftConfig.HIGH_R_LORA
4346
No LORA (full finetuning): PeftConfig.NO_LORA
47+
Default LORA: PeftConfig.DEFAULT_LORA
48+
High R LoRA: PeftConfig.HIGH_R_LORA
49+
VLM LoRA: PeftConfig.DEFAULT_VLM_LORA
4450
4551
Args:
4652
output_dir: Output directory for training artifacts
@@ -76,7 +82,7 @@
7682
"""
7783

7884
JOB_CONFIG = JobConfig(
79-
job_name="my_job",
85+
job_name="my_job_name",
8086
model_name="LFM2-1.2B",
8187
training_type="sft",
8288
dataset=example_sft_dataset,

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ dependencies = [
1616
"transformers>=4.55.0",
1717
"trl>=0.18.2",
1818
"rich>=14.1.0",
19-
"liger-kernel>=0.6.1",
19+
"pillow>=11.3.0",
20+
"mpi4py>=4.1.0",
21+
"liger-kernel>=0.6.2",
2022
]
2123

2224
[project.scripts]

src/leap_finetune/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import sys
22

3-
from leap_finetune.utils.logging import setup_training_environment
3+
from leap_finetune.utils.logging_utils import setup_training_environment
44

55
setup_training_environment()
66

src/leap_finetune/configs/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55
from .sft_configs import (
66
DEFAULT_SFT_CONFIG,
77
)
8+
from .vlm_sft_config import (
9+
DEFAULT_VLM_SFT_CONFIG,
10+
)
811
from .peft_configs import (
912
LFM2_LORA_DEFAULT_CONFIG,
1013
LFM2_LORA_HIGH_R_CONFIG,
14+
LFM2_VLM_LORA_CONFIG,
1115
)
1216

1317

1418
class TrainingConfig(Enum):
1519
DEFAULT_SFT = DEFAULT_SFT_CONFIG
1620
DEFAULT_DPO = DEFAULT_DPO_CONFIG
21+
DEFAULT_VLM_SFT = DEFAULT_VLM_SFT_CONFIG
1722

1823
def override(self, **overrides):
1924
"""Create a custom TrainingConfig with overrides"""
@@ -29,6 +34,7 @@ class _CustomTrainingConfig(Enum):
2934

3035

3136
class PeftConfig(Enum):
37+
NO_LORA = None
3238
DEFAULT_LORA = LFM2_LORA_DEFAULT_CONFIG
3339
HIGH_R_LORA = LFM2_LORA_HIGH_R_CONFIG
34-
NO_LORA = None
40+
DEFAULT_VLM_LORA = LFM2_VLM_LORA_CONFIG

src/leap_finetune/configs/job_config.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
from dataclasses import dataclass
22
from typing import Any, Literal
3+
from pathlib import Path
34

45
from datasets import Dataset
56
from rich.console import Console
67
from rich.panel import Panel
78
from rich.table import Table
89

910
from leap_finetune.configs import PeftConfig, TrainingConfig
10-
from leap_finetune.utils.output_paths import (
11-
is_job_name_unique,
12-
resolve_model_output_path,
13-
)
11+
from leap_finetune.data_loaders.dataset_loader import DatasetLoader
12+
13+
14+
def is_job_name_unique(output_dir: str) -> bool:
15+
"""
16+
Checks if a job with the given name exists for a given training type.
17+
"""
18+
return not Path(output_dir).exists()
1419

1520

1621
@dataclass
@@ -19,13 +24,17 @@ class JobConfig:
1924

2025
job_name: str
2126
model_name: str = "LFM2-1.2B"
22-
training_type: Literal["sft", "dpo"] = "sft"
23-
dataset: Dataset | None = None
27+
training_type: Literal["sft", "dpo", "vlm_sft"] = "sft"
28+
dataset: DatasetLoader | tuple[Dataset, Dataset] | None = None
2429
training_config: TrainingConfig = TrainingConfig.DEFAULT_SFT
2530
peft_config: PeftConfig | None = PeftConfig.DEFAULT_LORA
2631

2732
def __post_init__(self):
28-
self.dataset = self.dataset.load() # Load dataset after init
33+
if isinstance(self.dataset, DatasetLoader):
34+
self.dataset = self.dataset.load()
35+
self.training_config.value["output_dir"] = str(
36+
Path(self.training_config.value.get("output_dir")) / self.job_name
37+
)
2938
self._validate_job_name()
3039
self._validate_training_config()
3140

@@ -38,10 +47,10 @@ def _validate_job_name(self):
3847

3948
# Check if job dir already exists - warn but don't fail
4049
# (Ray workers might import config after directory is created)
41-
if not is_job_name_unique(self.training_type, self.job_name):
50+
if not is_job_name_unique(self.training_config.value.get("output_dir")):
4251
raise ValueError(
43-
f"Job directory already exists for job '{self.job_name}' with training type '{self.training_type}'. "
44-
f"This might be from a previous run or concurrent Ray worker initialization."
52+
"Job output directory already exists\n"
53+
"This might be from a previous run or concurrent Ray worker initialization."
4554
)
4655

4756
def _validate_training_config(self):
@@ -75,7 +84,7 @@ def print_config_summary(self):
7584
console = Console()
7685

7786
# Calculate output directory
78-
output_dir = resolve_model_output_path(self.training_type, self.job_name)
87+
output_dir = self.training_config.value.get("output_dir")
7988

8089
# Create a table for the configuration
8190
table = Table(show_header=False, box=None, padding=(0, 2))

src/leap_finetune/configs/peft_configs.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,18 @@
2222
lora_dropout=0.1,
2323
target_modules=GLU_MODULES + MHA_MODULES + CONV_MODULES,
2424
)
25+
26+
27+
LFM_MODULES = ["q_proj", "k_proj", "v_proj", "out_proj", "in_proj"]
28+
VISION_TOWER_MODULES = ["fc1", "fc2"]
29+
MULTI_MODAL_PROJECTOR_MODULES = ["linear_1", "linear_2"]
30+
31+
LFM2_VLM_LORA_CONFIG = LoraConfig(
32+
task_type=TaskType.CAUSAL_LM,
33+
inference_mode=False,
34+
r=8,
35+
lora_alpha=16,
36+
lora_dropout=0.1,
37+
bias="none",
38+
target_modules=LFM_MODULES + VISION_TOWER_MODULES + MULTI_MODAL_PROJECTOR_MODULES,
39+
)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from leap_finetune.utils.constants import SFT_OUTPUT_PATH
2+
3+
########################
4+
# DEEPSEED CONFIGS #
5+
########################
6+
7+
8+
DEEPSEED_CONFIG = {
9+
"zero_optimization": {
10+
"stage": 2,
11+
"overlap_comm": True,
12+
},
13+
"train_batch_size": "auto",
14+
"train_micro_batch_size_per_gpu": "auto",
15+
"gradient_clipping": "auto",
16+
"gradient_accumulation_steps": "auto",
17+
"optimizer": {
18+
"type": "AdamW",
19+
"params": {
20+
"lr": "auto", # Uses learning_rate from training config
21+
"betas": "auto", # DEFAULT: (0.9, 0.999)
22+
"eps": "auto", # DEFAULT: 1e-8
23+
"weight_decay": "auto", # DEFAULT: 0.01
24+
},
25+
},
26+
"bf16": {"enabled": "auto"},
27+
"activation_checkpointing": {
28+
"partition_activations": False,
29+
"cpu_checkpointing": False,
30+
"contiguous_memory_optimization": False,
31+
"number_checkpoints": None,
32+
"synchronize_checkpoint_boundary": False,
33+
"profile": False,
34+
},
35+
}
36+
37+
38+
########################
39+
# SFT CONFIGS #
40+
########################
41+
42+
43+
DEFAULT_VLM_SFT_CONFIG = {
44+
"training_type": "vlm_sft",
45+
"output_dir": SFT_OUTPUT_PATH,
46+
"num_train_epochs": 3, # 1 to 5 generally (post-training goes for 2-3)
47+
"per_device_train_batch_size": 4, # adjust based on context length (post-training goes for 1-2 at 32k context length)
48+
"learning_rate": 5e-5, # anything from 1e-5 to 5e-5 seems ok. "end_learning_rate" would be 1e-7, not easy to set up with out-of-the-box SFTConfig
49+
"lr_scheduler_type": "linear",
50+
"warmup_steps": 100,
51+
"warmup_ratio": 0.2,
52+
"logging_steps": 10,
53+
"save_strategy": "epoch",
54+
"eval_strategy": "epoch",
55+
"load_best_model_at_end": True,
56+
"gradient_checkpointing": True,
57+
"dataset_kwargs": {"skip_prepare_dataset": True},
58+
"deepspeed": DEEPSEED_CONFIG,
59+
}

src/leap_finetune/data_loaders/dataset_loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class DatasetLoader:
1212
"""Dataset loader for training and testing datasets"""
1313

1414
dataset_path: str
15-
dataset_type: Literal["sft", "dpo"]
15+
dataset_type: Literal["sft", "dpo", "vlm_sft"]
1616
limit: Optional[int] = None # Default: all samples
1717
split: str = "train" # Default: "train"
1818
test_size: float = 0.2 # Default: 80/20 split
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from PIL import Image
2+
import io
3+
import requests
4+
5+
6+
def load_image(src):
7+
"""Load image from various sources and return PIL Image."""
8+
# bytes -> PIL
9+
if isinstance(src, (bytes, bytearray)):
10+
return Image.open(io.BytesIO(src))
11+
# URL -> PIL
12+
if isinstance(src, str) and src.startswith(("http://", "https://")):
13+
return Image.open(requests.get(src, stream=True).raw)
14+
# file path -> PIL
15+
return Image.open(src)

0 commit comments

Comments
 (0)