@tuned-tensor/local 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +54 -2
- package/dist/contracts.d.ts +64 -0
- package/dist/contracts.js +19 -3
- package/dist/contracts.js.map +1 -1
- package/dist/dataset.d.ts +1 -0
- package/dist/dataset.js +46 -1
- package/dist/dataset.js.map +1 -1
- package/dist/evaluation.js +138 -12
- package/dist/evaluation.js.map +1 -1
- package/dist/model-registry.js +2 -2
- package/dist/model-registry.js.map +1 -1
- package/dist/orchestrator.js +30 -3
- package/dist/orchestrator.js.map +1 -1
- package/dist/process-training.d.ts +13 -0
- package/dist/process-training.js +74 -0
- package/dist/process-training.js.map +1 -1
- package/dist/run-reporter.js +1 -1
- package/dist/run-reporter.js.map +1 -1
- package/package.json +1 -1
- package/training/sft-local/pyproject.toml +6 -1
- package/training/sft-local/src/evaluate.py +163 -8
- package/training/sft-local/src/train.py +186 -16
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
5
6
|
import tarfile
|
|
6
7
|
import time
|
|
7
|
-
|
|
8
|
+
import urllib.request
|
|
9
|
+
from io import BytesIO
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
from tempfile import TemporaryDirectory
|
|
10
12
|
from typing import Any
|
|
11
13
|
|
|
12
14
|
import torch
|
|
15
|
+
from datasets import Dataset as HFDataset
|
|
13
16
|
from peft import LoraConfig, get_peft_model
|
|
17
|
+
from PIL import Image
|
|
14
18
|
from torch.utils.data import Dataset
|
|
15
19
|
from transformers import (
|
|
16
20
|
AutoModelForCausalLM,
|
|
21
|
+
AutoModelForImageTextToText,
|
|
22
|
+
AutoProcessor,
|
|
17
23
|
AutoTokenizer,
|
|
18
24
|
DataCollatorForLanguageModeling,
|
|
19
25
|
Trainer,
|
|
20
26
|
TrainingArguments,
|
|
21
27
|
)
|
|
28
|
+
from trl import SFTConfig, SFTTrainer
|
|
22
29
|
|
|
23
30
|
|
|
24
31
|
TRAINING_DIR = Path(os.environ.get("SM_CHANNEL_TRAINING", "/opt/ml/input/data/training"))
|
|
@@ -28,6 +35,7 @@ HYPERPARAMETERS_PATH = Path(
|
|
|
28
35
|
)
|
|
29
36
|
MODEL_DIR = Path(os.environ.get("SM_MODEL_DIR", "/opt/ml/model"))
|
|
30
37
|
OUTPUT_DIR = Path(os.environ.get("SM_OUTPUT_DIR", "/opt/ml/output"))
|
|
38
|
+
ROW_SOURCE_DIR_KEY = "_tt_source_dir"
|
|
31
39
|
|
|
32
40
|
|
|
33
41
|
def load_hyperparameters() -> dict[str, str]:
|
|
@@ -71,6 +79,7 @@ def load_rows() -> list[dict[str, Any]]:
|
|
|
71
79
|
row = json.loads(line)
|
|
72
80
|
if "messages" not in row:
|
|
73
81
|
raise ValueError(f"{path}:{line_number} missing messages")
|
|
82
|
+
row[ROW_SOURCE_DIR_KEY] = str(path.parent)
|
|
74
83
|
rows.append(row)
|
|
75
84
|
if not rows:
|
|
76
85
|
raise ValueError(f"No chat JSONL rows found under {TRAINING_DIR}")
|
|
@@ -132,7 +141,82 @@ class TextDataset(Dataset[dict[str, torch.Tensor]]):
|
|
|
132
141
|
}
|
|
133
142
|
|
|
134
143
|
|
|
135
|
-
def
|
|
144
|
+
def image_from_data_uri(value: str) -> Image.Image:
|
|
145
|
+
_, encoded = value.split(",", 1)
|
|
146
|
+
return Image.open(BytesIO(base64.b64decode(encoded))).convert("RGB")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def load_image(value: str, base_dir: Path) -> Image.Image:
|
|
150
|
+
if value.startswith("data:"):
|
|
151
|
+
return image_from_data_uri(value)
|
|
152
|
+
if value.startswith("http://") or value.startswith("https://"):
|
|
153
|
+
with urllib.request.urlopen(value, timeout=30) as response:
|
|
154
|
+
return Image.open(BytesIO(response.read())).convert("RGB")
|
|
155
|
+
path_value = value[7:] if value.startswith("file://") else value
|
|
156
|
+
path = Path(path_value).expanduser()
|
|
157
|
+
if not path.is_absolute():
|
|
158
|
+
path = base_dir / path
|
|
159
|
+
return Image.open(path).convert("RGB")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def image_value_from_part(part: dict[str, Any], fallback: str | None) -> str | None:
|
|
163
|
+
for key in ("image", "path", "uri", "data_uri"):
|
|
164
|
+
value = part.get(key)
|
|
165
|
+
if isinstance(value, str) and value:
|
|
166
|
+
return value
|
|
167
|
+
return fallback
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def multimodal_content(
|
|
171
|
+
content: Any,
|
|
172
|
+
top_level_images: list[str],
|
|
173
|
+
base_dir: Path,
|
|
174
|
+
) -> tuple[str | list[dict[str, Any]], list[Image.Image]]:
|
|
175
|
+
if isinstance(content, str):
|
|
176
|
+
return content, []
|
|
177
|
+
if not isinstance(content, list):
|
|
178
|
+
return str(content), []
|
|
179
|
+
|
|
180
|
+
images: list[Image.Image] = []
|
|
181
|
+
image_index = 0
|
|
182
|
+
converted: list[dict[str, Any]] = []
|
|
183
|
+
for part in content:
|
|
184
|
+
if not isinstance(part, dict):
|
|
185
|
+
converted.append({"type": "text", "text": str(part)})
|
|
186
|
+
continue
|
|
187
|
+
if part.get("type") != "image":
|
|
188
|
+
converted.append(part)
|
|
189
|
+
continue
|
|
190
|
+
fallback = top_level_images[image_index] if image_index < len(top_level_images) else None
|
|
191
|
+
image_index += 1
|
|
192
|
+
image_value = image_value_from_part(part, fallback)
|
|
193
|
+
if image_value:
|
|
194
|
+
images.append(load_image(image_value, base_dir))
|
|
195
|
+
converted.append({"type": "image"})
|
|
196
|
+
return converted, images
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def row_to_multimodal_example(row: dict[str, Any]) -> dict[str, Any]:
|
|
200
|
+
source_dir = Path(str(row.get(ROW_SOURCE_DIR_KEY) or TRAINING_DIR))
|
|
201
|
+
top_level_images = [str(value) for value in row.get("images", []) if isinstance(value, str)]
|
|
202
|
+
all_images: list[Image.Image] = []
|
|
203
|
+
messages: list[dict[str, Any]] = []
|
|
204
|
+
for message in row["messages"]:
|
|
205
|
+
content, images = multimodal_content(message.get("content", ""), top_level_images, source_dir)
|
|
206
|
+
all_images.extend(images)
|
|
207
|
+
messages.append({
|
|
208
|
+
"role": message.get("role", "user"),
|
|
209
|
+
"content": content,
|
|
210
|
+
})
|
|
211
|
+
if not all_images and top_level_images:
|
|
212
|
+
all_images = [load_image(value, source_dir) for value in top_level_images]
|
|
213
|
+
return {
|
|
214
|
+
"messages": messages,
|
|
215
|
+
"images": all_images,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def create_text_model_and_tokenizer(model_source: str):
|
|
136
220
|
trust_remote_code = hp_bool("trust_remote_code", True)
|
|
137
221
|
token = os.getenv("HF_TOKEN")
|
|
138
222
|
tokenizer = AutoTokenizer.from_pretrained(
|
|
@@ -155,6 +239,36 @@ def create_model_and_tokenizer(model_source: str):
|
|
|
155
239
|
return model, tokenizer
|
|
156
240
|
|
|
157
241
|
|
|
242
|
+
def create_multimodal_model_and_processor(model_source: str):
|
|
243
|
+
trust_remote_code = hp_bool("trust_remote_code", True)
|
|
244
|
+
token = os.getenv("HF_TOKEN")
|
|
245
|
+
processor = AutoProcessor.from_pretrained(
|
|
246
|
+
model_source,
|
|
247
|
+
trust_remote_code=trust_remote_code,
|
|
248
|
+
token=token,
|
|
249
|
+
)
|
|
250
|
+
if getattr(processor, "tokenizer", None) is not None and processor.tokenizer.pad_token is None:
|
|
251
|
+
processor.tokenizer.pad_token = processor.tokenizer.eos_token
|
|
252
|
+
|
|
253
|
+
dtype = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16
|
|
254
|
+
model = AutoModelForImageTextToText.from_pretrained(
|
|
255
|
+
model_source,
|
|
256
|
+
trust_remote_code=trust_remote_code,
|
|
257
|
+
token=token,
|
|
258
|
+
torch_dtype=dtype if torch.cuda.is_available() else None,
|
|
259
|
+
device_map="auto" if torch.cuda.is_available() else None,
|
|
260
|
+
)
|
|
261
|
+
model.config.use_cache = False
|
|
262
|
+
return model, processor
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def lora_target_modules(default: str) -> list[str] | str:
|
|
266
|
+
raw = hp("lora_target_modules", default) or default
|
|
267
|
+
if raw == "all-linear":
|
|
268
|
+
return raw
|
|
269
|
+
return [item.strip() for item in raw.split(",") if item.strip()]
|
|
270
|
+
|
|
271
|
+
|
|
158
272
|
def maybe_apply_lora(model: Any):
|
|
159
273
|
rank = hp_int("lora_rank", 16)
|
|
160
274
|
alpha = hp_int("lora_alpha", 32)
|
|
@@ -165,11 +279,21 @@ def maybe_apply_lora(model: Any):
|
|
|
165
279
|
lora_dropout=dropout,
|
|
166
280
|
bias="none",
|
|
167
281
|
task_type="CAUSAL_LM",
|
|
168
|
-
target_modules="all-linear",
|
|
282
|
+
target_modules=lora_target_modules("all-linear"),
|
|
169
283
|
)
|
|
170
284
|
return get_peft_model(model, config)
|
|
171
285
|
|
|
172
286
|
|
|
287
|
+
def multimodal_lora_config() -> LoraConfig:
|
|
288
|
+
return LoraConfig(
|
|
289
|
+
r=hp_int("lora_rank", 16),
|
|
290
|
+
lora_alpha=hp_int("lora_alpha", 32),
|
|
291
|
+
lora_dropout=hp_float("lora_dropout", 0.05),
|
|
292
|
+
bias="none",
|
|
293
|
+
target_modules=lora_target_modules("q_proj,v_proj"),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
173
297
|
def create_model_archive() -> Path:
|
|
174
298
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
175
299
|
archive_path = OUTPUT_DIR / "model.tar.gz"
|
|
@@ -178,14 +302,8 @@ def create_model_archive() -> Path:
|
|
|
178
302
|
return archive_path
|
|
179
303
|
|
|
180
304
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
MODEL_DIR.mkdir(parents=True, exist_ok=True)
|
|
184
|
-
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
185
|
-
|
|
186
|
-
rows = load_rows()
|
|
187
|
-
model_source = resolve_model_source()
|
|
188
|
-
model, tokenizer = create_model_and_tokenizer(model_source)
|
|
305
|
+
def run_text_training(rows: list[dict[str, Any]], model_source: str) -> dict[str, Any]:
|
|
306
|
+
model, tokenizer = create_text_model_and_tokenizer(model_source)
|
|
189
307
|
model = maybe_apply_lora(model)
|
|
190
308
|
|
|
191
309
|
texts = [row_to_text(tokenizer, row) for row in rows]
|
|
@@ -216,17 +334,69 @@ def main() -> None:
|
|
|
216
334
|
|
|
217
335
|
model.save_pretrained(MODEL_DIR)
|
|
218
336
|
tokenizer.save_pretrained(MODEL_DIR)
|
|
337
|
+
return result.metrics
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def run_multimodal_training(rows: list[dict[str, Any]], model_source: str) -> dict[str, Any]:
|
|
341
|
+
model, processor = create_multimodal_model_and_processor(model_source)
|
|
342
|
+
dataset = HFDataset.from_list([row_to_multimodal_example(row) for row in rows])
|
|
343
|
+
|
|
344
|
+
with TemporaryDirectory() as tmp:
|
|
345
|
+
args = SFTConfig(
|
|
346
|
+
output_dir=tmp,
|
|
347
|
+
num_train_epochs=hp_int("n_epochs", 3),
|
|
348
|
+
learning_rate=hp_float("learning_rate", 0.00001),
|
|
349
|
+
per_device_train_batch_size=hp_int("per_device_train_batch_size", 1),
|
|
350
|
+
gradient_accumulation_steps=hp_int("gradient_accumulation_steps", 8),
|
|
351
|
+
logging_steps=1,
|
|
352
|
+
save_strategy="no",
|
|
353
|
+
report_to="none",
|
|
354
|
+
remove_unused_columns=False,
|
|
355
|
+
bf16=torch.cuda.is_available() and torch.cuda.is_bf16_supported(),
|
|
356
|
+
fp16=torch.cuda.is_available() and not torch.cuda.is_bf16_supported(),
|
|
357
|
+
gradient_checkpointing=True,
|
|
358
|
+
gradient_checkpointing_kwargs={"use_reentrant": False},
|
|
359
|
+
dataset_kwargs={"skip_prepare_dataset": True},
|
|
360
|
+
max_length=None,
|
|
361
|
+
)
|
|
362
|
+
trainer = SFTTrainer(
|
|
363
|
+
model=model,
|
|
364
|
+
args=args,
|
|
365
|
+
train_dataset=dataset,
|
|
366
|
+
processing_class=processor,
|
|
367
|
+
peft_config=multimodal_lora_config(),
|
|
368
|
+
)
|
|
369
|
+
result = trainer.train()
|
|
370
|
+
trainer.save_model(MODEL_DIR)
|
|
371
|
+
processor.save_pretrained(MODEL_DIR)
|
|
372
|
+
return result.metrics
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def main() -> None:
|
|
376
|
+
started = time.time()
|
|
377
|
+
MODEL_DIR.mkdir(parents=True, exist_ok=True)
|
|
378
|
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
379
|
+
|
|
380
|
+
rows = load_rows()
|
|
381
|
+
model_source = resolve_model_source()
|
|
382
|
+
model_loader = hp("model_loader", "causal_lm")
|
|
383
|
+
if model_loader == "image_text_to_text":
|
|
384
|
+
metrics = run_multimodal_training(rows, model_source)
|
|
385
|
+
else:
|
|
386
|
+
metrics = run_text_training(rows, model_source)
|
|
387
|
+
|
|
219
388
|
archive_path = create_model_archive()
|
|
220
|
-
|
|
389
|
+
output_metrics = {
|
|
221
390
|
"training_rows": len(rows),
|
|
222
391
|
"model_source": model_source,
|
|
392
|
+
"model_loader": model_loader,
|
|
223
393
|
"train_runtime": round(time.time() - started, 3),
|
|
224
|
-
**{key: float(value) for key, value in
|
|
394
|
+
**{key: float(value) for key, value in metrics.items() if isinstance(value, (int, float))},
|
|
225
395
|
"model_archive": str(archive_path),
|
|
226
396
|
}
|
|
227
|
-
(MODEL_DIR / "training-metrics.json").write_text(json.dumps(
|
|
228
|
-
(OUTPUT_DIR / "training-metrics.json").write_text(json.dumps(
|
|
229
|
-
print(json.dumps(
|
|
397
|
+
(MODEL_DIR / "training-metrics.json").write_text(json.dumps(output_metrics, indent=2))
|
|
398
|
+
(OUTPUT_DIR / "training-metrics.json").write_text(json.dumps(output_metrics, indent=2))
|
|
399
|
+
print(json.dumps(output_metrics, indent=2))
|
|
230
400
|
|
|
231
401
|
|
|
232
402
|
if __name__ == "__main__":
|