@tuned-tensor/local 0.2.3 → 0.2.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.
@@ -0,0 +1,232 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import json
5
+ import os
6
+ import tarfile
7
+ import time
8
+ from pathlib import Path
9
+ from tempfile import TemporaryDirectory
10
+ from typing import Any
11
+
12
+ import torch
13
+ from datasets import Dataset as HFDataset
14
+ from peft import LoraConfig
15
+ from transformers import AutoModelForCausalLM, AutoTokenizer
16
+ from trl import DPOConfig, DPOTrainer
17
+
18
+
19
+ TRAINING_DIR = Path(os.environ.get("SM_CHANNEL_TRAINING", "/opt/ml/input/data/training"))
20
+ BASE_MODEL_DIR = Path(os.environ.get("SM_CHANNEL_BASE_MODEL", "/opt/ml/input/data/base_model"))
21
+ HYPERPARAMETERS_PATH = Path(
22
+ os.environ.get("TT_HYPERPARAMETERS_PATH", "/opt/ml/input/config/hyperparameters.json")
23
+ )
24
+ MODEL_DIR = Path(os.environ.get("SM_MODEL_DIR", "/opt/ml/model"))
25
+ OUTPUT_DIR = Path(os.environ.get("SM_OUTPUT_DIR", "/opt/ml/output"))
26
+
27
+
28
+ def load_hyperparameters() -> dict[str, str]:
29
+ if not HYPERPARAMETERS_PATH.is_file():
30
+ return {}
31
+ raw = json.loads(HYPERPARAMETERS_PATH.read_text())
32
+ return {str(key): str(value) for key, value in raw.items()}
33
+
34
+
35
+ HP = load_hyperparameters()
36
+
37
+
38
+ def hp(name: str, default: str | None = None) -> str | None:
39
+ value = os.getenv(f"SM_HP_{name.upper()}", HP.get(name, default))
40
+ if value is None:
41
+ return None
42
+ value = str(value).strip()
43
+ if len(value) >= 2 and value[0] == value[-1] == '"':
44
+ return value[1:-1]
45
+ return value
46
+
47
+
48
+ def hp_int(name: str, default: int) -> int:
49
+ return int(hp(name, str(default)) or default)
50
+
51
+
52
+ def hp_float(name: str, default: float) -> float:
53
+ return float(hp(name, str(default)) or default)
54
+
55
+
56
+ def hp_bool(name: str, default: bool) -> bool:
57
+ return (hp(name, str(default)) or "").lower() in {"1", "true", "yes", "y"}
58
+
59
+
60
+ def supported_kwargs(callable_obj: Any, values: dict[str, Any]) -> dict[str, Any]:
61
+ accepted = set(inspect.signature(callable_obj).parameters)
62
+ return {key: value for key, value in values.items() if key in accepted}
63
+
64
+
65
+ def load_preference_rows() -> list[dict[str, str]]:
66
+ rows: list[dict[str, str]] = []
67
+ for path in sorted(TRAINING_DIR.rglob("*.jsonl")):
68
+ for line_number, line in enumerate(path.read_text().splitlines(), start=1):
69
+ if not line.strip():
70
+ continue
71
+ row = json.loads(line)
72
+ cleaned: dict[str, str] = {}
73
+ for key in ("prompt", "chosen", "rejected"):
74
+ value = row.get(key)
75
+ if not isinstance(value, str) or not value.strip():
76
+ raise ValueError(f"{path}:{line_number} missing non-empty string field {key}")
77
+ cleaned[key] = value
78
+ rows.append(cleaned)
79
+ if not rows:
80
+ raise ValueError(f"No preference JSONL rows found under {TRAINING_DIR}")
81
+ return rows
82
+
83
+
84
+ def resolve_model_source() -> str:
85
+ archive = BASE_MODEL_DIR / "model.tar.gz"
86
+ if archive.is_file():
87
+ extracted = Path("/tmp/base-model")
88
+ extracted.mkdir(parents=True, exist_ok=True)
89
+ with tarfile.open(archive, "r:gz") as tar:
90
+ extracted_root = extracted.resolve()
91
+ for member in tar.getmembers():
92
+ destination = (extracted / member.name).resolve()
93
+ try:
94
+ destination.relative_to(extracted_root)
95
+ except ValueError:
96
+ raise ValueError(f"Unsafe archive member: {member.name}")
97
+ tar.extractall(extracted)
98
+ candidates = [path for path in extracted.iterdir() if path.is_dir()]
99
+ return str(candidates[0] if len(candidates) == 1 else extracted)
100
+ return hp("base_model") or "Qwen/Qwen3.5-2B"
101
+
102
+
103
+ def create_model_and_tokenizer(model_source: str):
104
+ trust_remote_code = hp_bool("trust_remote_code", True)
105
+ token = os.getenv("HF_TOKEN")
106
+ tokenizer = AutoTokenizer.from_pretrained(
107
+ model_source,
108
+ trust_remote_code=trust_remote_code,
109
+ token=token,
110
+ )
111
+ if tokenizer.pad_token is None:
112
+ tokenizer.pad_token = tokenizer.eos_token
113
+
114
+ dtype = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16
115
+ model = AutoModelForCausalLM.from_pretrained(
116
+ model_source,
117
+ trust_remote_code=trust_remote_code,
118
+ token=token,
119
+ torch_dtype=dtype if torch.cuda.is_available() else None,
120
+ device_map="auto" if torch.cuda.is_available() else None,
121
+ )
122
+ model.config.use_cache = False
123
+ return model, tokenizer
124
+
125
+
126
+ def lora_target_modules(default: str) -> list[str] | str:
127
+ raw = hp("lora_target_modules", default) or default
128
+ if raw == "all-linear":
129
+ return raw
130
+ return [item.strip() for item in raw.split(",") if item.strip()]
131
+
132
+
133
+ def lora_config() -> LoraConfig:
134
+ return LoraConfig(
135
+ r=hp_int("lora_rank", 16),
136
+ lora_alpha=hp_int("lora_alpha", 32),
137
+ lora_dropout=hp_float("lora_dropout", 0.05),
138
+ bias="none",
139
+ task_type="CAUSAL_LM",
140
+ target_modules=lora_target_modules("all-linear"),
141
+ )
142
+
143
+
144
+ def create_model_archive() -> Path:
145
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
146
+ archive_path = OUTPUT_DIR / "model.tar.gz"
147
+ with tarfile.open(archive_path, "w:gz") as tar:
148
+ tar.add(MODEL_DIR, arcname="model")
149
+ return archive_path
150
+
151
+
152
+ def dpo_config(output_dir: str) -> DPOConfig:
153
+ max_prompt_length = hp("max_prompt_length")
154
+ max_completion_length = hp("max_completion_length")
155
+ config_values: dict[str, Any] = {
156
+ "output_dir": output_dir,
157
+ "num_train_epochs": hp_int("n_epochs", 3),
158
+ "learning_rate": hp_float("learning_rate", 0.00001),
159
+ "per_device_train_batch_size": hp_int("per_device_train_batch_size", 1),
160
+ "gradient_accumulation_steps": hp_int("gradient_accumulation_steps", 8),
161
+ "logging_steps": 1,
162
+ "save_strategy": "no",
163
+ "report_to": "none",
164
+ "remove_unused_columns": False,
165
+ "bf16": torch.cuda.is_available() and torch.cuda.is_bf16_supported(),
166
+ "fp16": torch.cuda.is_available() and not torch.cuda.is_bf16_supported(),
167
+ "gradient_checkpointing": True,
168
+ "gradient_checkpointing_kwargs": {"use_reentrant": False},
169
+ "beta": hp_float("dpo_beta", 0.1),
170
+ "loss_type": hp("dpo_loss_type", "sigmoid"),
171
+ "label_smoothing": hp_float("dpo_label_smoothing", 0.0),
172
+ "reference_free": hp_bool("dpo_reference_free", False),
173
+ "max_length": hp_int("max_seq_length", 2048),
174
+ "max_prompt_length": int(max_prompt_length) if max_prompt_length else None,
175
+ "max_completion_length": int(max_completion_length) if max_completion_length else None,
176
+ }
177
+ return DPOConfig(**supported_kwargs(
178
+ DPOConfig,
179
+ {key: value for key, value in config_values.items() if value is not None},
180
+ ))
181
+
182
+
183
+ def run_training(rows: list[dict[str, str]], model_source: str) -> dict[str, Any]:
184
+ model, tokenizer = create_model_and_tokenizer(model_source)
185
+ dataset = HFDataset.from_list(rows)
186
+
187
+ with TemporaryDirectory() as tmp:
188
+ args = dpo_config(tmp)
189
+ trainer_values: dict[str, Any] = {
190
+ "model": model,
191
+ "ref_model": None,
192
+ "args": args,
193
+ "train_dataset": dataset,
194
+ "processing_class": tokenizer,
195
+ "tokenizer": tokenizer,
196
+ "peft_config": lora_config(),
197
+ }
198
+ trainer = DPOTrainer(**supported_kwargs(DPOTrainer, trainer_values))
199
+ result = trainer.train()
200
+ trainer.save_model(MODEL_DIR)
201
+
202
+ tokenizer.save_pretrained(MODEL_DIR)
203
+ return result.metrics
204
+
205
+
206
+ def main() -> None:
207
+ started = time.time()
208
+ MODEL_DIR.mkdir(parents=True, exist_ok=True)
209
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
210
+
211
+ rows = load_preference_rows()
212
+ model_source = resolve_model_source()
213
+ metrics = run_training(rows, model_source)
214
+
215
+ archive_path = create_model_archive()
216
+ output_metrics = {
217
+ "training_method": "dpo",
218
+ "preference_rows": len(rows),
219
+ "model_source": model_source,
220
+ "dpo_beta": hp_float("dpo_beta", 0.1),
221
+ "dpo_loss_type": hp("dpo_loss_type", "sigmoid"),
222
+ "train_runtime": round(time.time() - started, 3),
223
+ **{key: float(value) for key, value in metrics.items() if isinstance(value, (int, float))},
224
+ "model_archive": str(archive_path),
225
+ }
226
+ (MODEL_DIR / "training-metrics.json").write_text(json.dumps(output_metrics, indent=2))
227
+ (OUTPUT_DIR / "training-metrics.json").write_text(json.dumps(output_metrics, indent=2))
228
+ print(json.dumps(output_metrics, indent=2))
229
+
230
+
231
+ if __name__ == "__main__":
232
+ main()