-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathtrain.py
More file actions
267 lines (227 loc) · 9.2 KB
/
Copy pathtrain.py
File metadata and controls
267 lines (227 loc) · 9.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
"""Train a VLM using TRL SFTTrainer + Unsloth.
This script provides the main training entry point for openadapt-ml.
It uses TRL's SFTTrainer with optional Unsloth optimizations for
efficient VLM fine-tuning.
Usage:
# Train on synthetic data
python -m openadapt_ml.scripts.train --config configs/qwen3vl_synthetic_som.yaml
# Train on capture recording
python -m openadapt_ml.scripts.train --config configs/qwen3vl_capture.yaml \
--capture /path/to/capture --goal "Task description" --open
"""
from __future__ import annotations
from pathlib import Path
from typing import Dict, Any, Optional
import yaml
from openadapt_ml.ingest.synthetic import generate_synthetic_episodes
from openadapt_ml.training.trl_trainer import (
TRLTrainingConfig,
train_with_trl,
train_from_jsonl,
)
def _load_config(path: str | Path) -> dict:
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def _load_capture_episodes(capture_path: str | Path, goal: str | None = None) -> list:
"""Load episodes from an openadapt-capture recording."""
from openadapt_ml.ingest.capture import capture_to_episode
capture_path = Path(capture_path)
episode = capture_to_episode(capture_path, instruction=goal)
return [episode]
def main(
config_path: str,
capture_path: str | None = None,
goal: str | None = None,
output_dir: str | None = None,
open_dashboard: bool = False,
use_unsloth: bool = True,
jsonl_path: str | None = None,
) -> None:
"""Train a VLM using TRL SFTTrainer.
Args:
config_path: Path to YAML config file
capture_path: Optional path to openadapt-capture recording
goal: Task goal/description (overrides recording's task description)
output_dir: Output directory for logs and dashboard
open_dashboard: Open training dashboard in browser after training
use_unsloth: Enable Unsloth optimizations (default True)
jsonl_path: Optional path to JSONL training data (internal SFT format)
"""
cfg = _load_config(config_path)
model_name = cfg["model"]["name"]
load_in_4bit = cfg["model"].get("load_in_4bit", False)
# LoRA config
raw_lora_cfg = cfg.get("lora")
lora_cfg: Optional[Dict[str, Any]] = None
if isinstance(raw_lora_cfg, dict):
lora_cfg = {k: v for k, v in raw_lora_cfg.items() if k != "weights_path"}
else:
lora_cfg = raw_lora_cfg
# Determine output directory
train_cfg_raw = cfg.get("training", {})
if output_dir is None:
output_dir = train_cfg_raw.get("output_dir", "training_output")
print(f"Using TRL trainer (Unsloth: {use_unsloth})")
# Build TRL config from YAML config
lora_dict = lora_cfg if isinstance(lora_cfg, dict) else {}
trl_config = TRLTrainingConfig(
model_name=model_name,
load_in_4bit=load_in_4bit,
max_seq_length=train_cfg_raw.get("max_seq_length", 4096),
lora_r=lora_dict.get("r", 16),
lora_alpha=lora_dict.get("lora_alpha", 32),
lora_dropout=lora_dict.get("lora_dropout", 0.0),
finetune_vision_layers=lora_dict.get("finetune_vision_layers", False),
target_modules=lora_dict.get("target_modules"),
num_epochs=train_cfg_raw.get("num_train_epochs", 3),
batch_size=train_cfg_raw.get("per_device_train_batch_size", 1),
gradient_accumulation_steps=train_cfg_raw.get("gradient_accumulation_steps", 4),
learning_rate=train_cfg_raw.get("learning_rate", 2e-4),
warmup_ratio=train_cfg_raw.get("warmup_ratio", 0.03),
lr_scheduler_type=train_cfg_raw.get("lr_scheduler_type", "cosine"),
weight_decay=train_cfg_raw.get("weight_decay", 0.0),
max_grad_norm=train_cfg_raw.get("max_grad_norm", 1.0),
output_dir=output_dir,
logging_steps=train_cfg_raw.get("logging_steps", 10),
save_strategy=train_cfg_raw.get("save_strategy", "epoch"),
early_stop_loss=train_cfg_raw.get("early_stop_loss", 0.0),
early_stop_patience=train_cfg_raw.get("early_stop_patience", 5),
early_stop_min_delta=train_cfg_raw.get("early_stop_min_delta", 0.0),
early_stop_plateau_patience=train_cfg_raw.get("early_stop_plateau_patience", 5),
)
# Disable Unsloth if requested
if not use_unsloth:
import os
os.environ["OPENADAPT_DISABLE_UNSLOTH"] = "1"
# JSONL path: skip episode loading, use train_from_jsonl directly
if jsonl_path:
print(f"Training from JSONL: {jsonl_path}")
checkpoint_path = train_from_jsonl(
jsonl_path=jsonl_path,
config=trl_config,
)
print(f"Training complete. Checkpoint saved to: {checkpoint_path}")
if open_dashboard:
import webbrowser
dashboard_path = Path(output_dir) / "dashboard.html"
if dashboard_path.exists():
webbrowser.open(f"file://{dashboard_path.absolute()}")
return
# Load data - either from capture or synthetic
use_som = cfg.get("synthetic_data", {}).get("use_som", False)
if capture_path:
# Load from real openadapt-capture recording
print(f"Loading capture from: {capture_path}")
episodes = _load_capture_episodes(capture_path, goal=goal)
data_source = f"capture '{Path(capture_path).name}'"
else:
# Generate synthetic data
synth_cfg = cfg.get("synthetic_data", {})
num_sessions = synth_cfg.get("num_sessions", 10)
seed = synth_cfg.get("seed")
default_output_dir = str(Path("synthetic") / "train")
synth_output = synth_cfg.get("output_dir", default_output_dir)
use_som = synth_cfg.get("use_som", False)
scenario = synth_cfg.get("scenario", "login")
episodes = generate_synthetic_episodes(
num_episodes=num_sessions,
seed=seed,
output_dir=synth_output,
use_som=use_som,
scenario=scenario,
)
data_source = f"synthetic '{scenario}'"
base_path = Path(capture_path).parent if capture_path else None
print(f"Training on {len(episodes)} episodes from {data_source}")
checkpoint_path = train_with_trl(
episodes=episodes,
config=trl_config,
use_som=use_som,
base_path=base_path,
)
print(f"Training complete. Checkpoint saved to: {checkpoint_path}")
# Open dashboard in browser if requested
if open_dashboard:
import webbrowser
dashboard_path = Path(output_dir) / "dashboard.html"
if dashboard_path.exists():
webbrowser.open(f"file://{dashboard_path.absolute()}")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="Train Qwen-VL adapter on synthetic data or openadapt-capture recordings."
)
parser.add_argument(
"--config", type=str, required=True, help="Path to YAML config file."
)
parser.add_argument(
"--capture", type=str, help="Path to openadapt-capture recording directory."
)
parser.add_argument(
"--goal",
type=str,
help="Task goal/description (overrides recording's task description).",
)
parser.add_argument(
"--output-dir", type=str, help="Output directory for logs and dashboard."
)
parser.add_argument(
"--open", action="store_true", help="Open training dashboard in browser."
)
parser.add_argument(
"--jsonl",
type=str,
help="Path to JSONL training data (internal SFT format with images + messages).",
)
parser.add_argument(
"--demo-dir",
type=str,
help="Directory with annotated demo JSON files (auto-converts to JSONL bundle).",
)
parser.add_argument(
"--captures-dir",
type=str,
help="Parent directory containing capture directories (for screenshot resolution).",
)
parser.add_argument(
"--mapping",
type=str,
help="Pre-computed screenshot_mapping.json (optional with --demo-dir).",
)
parser.add_argument(
"--use-unsloth",
action="store_true",
default=True,
help="Enable Unsloth optimizations (default).",
)
parser.add_argument(
"--no-unsloth", action="store_true", help="Disable Unsloth optimizations."
)
args = parser.parse_args()
# Determine effective flags
use_unsloth = args.use_unsloth and not args.no_unsloth
# Auto-convert demos to JSONL if --demo-dir provided
jsonl_path = args.jsonl
if args.demo_dir and not jsonl_path:
from openadapt_ml.training.convert_demos import prepare_bundle
if not args.captures_dir:
parser.error("--captures-dir is required with --demo-dir")
print("=" * 50)
print("Converting demos to training bundle...")
print("=" * 50)
bundle_dir = prepare_bundle(
demo_dir=args.demo_dir,
captures_dir=args.captures_dir,
mapping_path=args.mapping,
)
jsonl_path = str(bundle_dir / "training_data.jsonl")
print(f"Bundle ready: {jsonl_path}\n")
main(
args.config,
capture_path=args.capture,
goal=args.goal,
output_dir=args.output_dir,
open_dashboard=args.open,
use_unsloth=use_unsloth,
jsonl_path=jsonl_path,
)