@tuned-tensor/local 0.2.9 → 0.3.0
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 +59 -0
- package/README.md +84 -210
- package/dist/artifacts.d.ts +3 -4
- package/dist/artifacts.js +55 -33
- package/dist/artifacts.js.map +1 -1
- package/dist/compare.d.ts +1 -8
- package/dist/compare.js +1 -23
- package/dist/compare.js.map +1 -1
- package/dist/contracts.d.ts +59 -366
- package/dist/contracts.js +73 -224
- package/dist/contracts.js.map +1 -1
- package/dist/dataset.d.ts +2 -4
- package/dist/dataset.js +31 -125
- package/dist/dataset.js.map +1 -1
- package/dist/doctor.d.ts +2 -3
- package/dist/doctor.js +23 -161
- package/dist/doctor.js.map +1 -1
- package/dist/evaluation.d.ts +11 -71
- package/dist/evaluation.js +212 -572
- package/dist/evaluation.js.map +1 -1
- package/dist/huggingface-cache.d.ts +8 -7
- package/dist/huggingface-cache.js +16 -8
- package/dist/huggingface-cache.js.map +1 -1
- package/dist/index.d.ts +0 -10
- package/dist/index.js +203 -626
- package/dist/index.js.map +1 -1
- package/dist/local-project.d.ts +1 -11
- package/dist/local-project.js +33 -61
- package/dist/local-project.js.map +1 -1
- package/dist/model-registry.d.ts +3 -16
- package/dist/model-registry.js +76 -292
- package/dist/model-registry.js.map +1 -1
- package/dist/model-server.d.ts +1 -2
- package/dist/model-server.js +9 -18
- package/dist/model-server.js.map +1 -1
- package/dist/orchestrator.d.ts +11 -25
- package/dist/orchestrator.js +246 -566
- package/dist/orchestrator.js.map +1 -1
- package/dist/prefetch.d.ts +1 -5
- package/dist/prefetch.js +14 -19
- package/dist/prefetch.js.map +1 -1
- package/dist/process-runner.d.ts +10 -29
- package/dist/process-runner.js +63 -169
- package/dist/process-runner.js.map +1 -1
- package/dist/process-training.d.ts +0 -1
- package/dist/process-training.js +10 -87
- package/dist/process-training.js.map +1 -1
- package/dist/store.d.ts +1 -3
- package/dist/store.js +92 -290
- package/dist/store.js.map +1 -1
- package/docs/architecture.md +87 -152
- package/docs/spark.md +66 -97
- package/examples/dry-runner.json +14 -0
- package/examples/local-runner.json +8 -6
- package/examples/smoke-spec.json +41 -0
- package/package.json +6 -6
- package/training/local-runner/pyproject.toml +0 -5
- package/training/local-runner/src/evaluate.py +89 -234
- package/training/local-runner/src/model_contract.py +47 -0
- package/training/local-runner/src/prefetch.py +18 -4
- package/training/local-runner/src/serve.py +54 -115
- package/training/local-runner/src/sft_data.py +97 -0
- package/training/local-runner/src/train.py +146 -346
- package/training/local-runner/uv.lock +0 -1197
- package/dist/labeling-sanitize.d.ts +0 -31
- package/dist/labeling-sanitize.js +0 -158
- package/dist/labeling-sanitize.js.map +0 -1
- package/dist/labeling.d.ts +0 -155
- package/dist/labeling.js +0 -496
- package/dist/labeling.js.map +0 -1
- package/dist/openrouter.d.ts +0 -29
- package/dist/openrouter.js +0 -66
- package/dist/openrouter.js.map +0 -1
- package/dist/server.d.ts +0 -9
- package/dist/server.js +0 -211
- package/dist/server.js.map +0 -1
- package/docs/local-workflow-remediation-2026-07-13.md +0 -130
- package/docs/local-workflow-ux-review-2026-07-13.md +0 -458
- package/examples/dpo-preferences.jsonl +0 -2
- package/examples/dpo-run-request.json +0 -34
- package/examples/smoke-run-request.json +0 -34
- package/training/local-runner/src/train_dpo.py +0 -286
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import base64
|
|
4
3
|
import json
|
|
5
4
|
import os
|
|
6
5
|
import tarfile
|
|
7
6
|
import time
|
|
8
|
-
import urllib.request
|
|
9
|
-
from io import BytesIO
|
|
10
7
|
from pathlib import Path
|
|
11
8
|
from tempfile import TemporaryDirectory
|
|
12
9
|
from typing import Any
|
|
13
10
|
|
|
14
11
|
import torch
|
|
15
|
-
from
|
|
16
|
-
from
|
|
17
|
-
from PIL import Image
|
|
12
|
+
from peft import LoraConfig, get_peft_model
|
|
13
|
+
from torch.nn.utils.rnn import pad_sequence
|
|
18
14
|
from torch.utils.data import Dataset
|
|
19
|
-
from transformers import
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
DataCollatorForLanguageModeling,
|
|
25
|
-
Trainer,
|
|
26
|
-
TrainingArguments,
|
|
15
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments
|
|
16
|
+
|
|
17
|
+
from model_contract import (
|
|
18
|
+
CERTIFIED_BASE_MODEL,
|
|
19
|
+
assert_certified_model_config,
|
|
27
20
|
)
|
|
28
|
-
from
|
|
21
|
+
from sft_data import IGNORE_INDEX, build_assistant_only_example
|
|
29
22
|
|
|
30
23
|
|
|
31
24
|
TRAINING_DIR = Path(os.environ.get("SM_CHANNEL_TRAINING", "/opt/ml/input/data/training"))
|
|
@@ -35,15 +28,14 @@ HYPERPARAMETERS_PATH = Path(
|
|
|
35
28
|
)
|
|
36
29
|
MODEL_DIR = Path(os.environ.get("SM_MODEL_DIR", "/opt/ml/model"))
|
|
37
30
|
OUTPUT_DIR = Path(os.environ.get("SM_OUTPUT_DIR", "/opt/ml/output"))
|
|
38
|
-
ROW_SOURCE_DIR_KEY = "_tt_source_dir"
|
|
39
|
-
MAX_ARCHIVE_MEMBERS = 20_000
|
|
40
|
-
MAX_ARCHIVE_EXPANDED_BYTES = 20 * 1024 * 1024 * 1024
|
|
41
31
|
|
|
42
32
|
|
|
43
33
|
def load_hyperparameters() -> dict[str, str]:
|
|
44
34
|
if not HYPERPARAMETERS_PATH.is_file():
|
|
45
35
|
return {}
|
|
46
|
-
raw = json.loads(HYPERPARAMETERS_PATH.read_text())
|
|
36
|
+
raw = json.loads(HYPERPARAMETERS_PATH.read_text(encoding="utf-8"))
|
|
37
|
+
if not isinstance(raw, dict):
|
|
38
|
+
raise ValueError("Hyperparameters must be a JSON object")
|
|
47
39
|
return {str(key): str(value) for key, value in raw.items()}
|
|
48
40
|
|
|
49
41
|
|
|
@@ -68,329 +60,174 @@ def hp_float(name: str, default: float) -> float:
|
|
|
68
60
|
return float(hp(name, str(default)) or default)
|
|
69
61
|
|
|
70
62
|
|
|
71
|
-
def hp_bool(name: str, default: bool) -> bool:
|
|
72
|
-
return (hp(name, str(default)) or "").lower() in {"1", "true", "yes", "y"}
|
|
73
|
-
|
|
74
|
-
|
|
75
63
|
def model_revision_kwargs(model_source: str) -> dict[str, str]:
|
|
76
64
|
revision = hp("base_model_revision")
|
|
77
65
|
return {"revision": revision} if revision and not Path(model_source).exists() else {}
|
|
78
66
|
|
|
79
67
|
|
|
68
|
+
def assert_certified_request() -> None:
|
|
69
|
+
requested_model = hp("base_model", CERTIFIED_BASE_MODEL)
|
|
70
|
+
if requested_model != CERTIFIED_BASE_MODEL:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"The bundled trainer currently certifies only {CERTIFIED_BASE_MODEL}; got {requested_model!r}"
|
|
73
|
+
)
|
|
74
|
+
loader = hp("model_loader", "causal_lm")
|
|
75
|
+
if loader != "causal_lm":
|
|
76
|
+
raise ValueError(f"The bundled trainer is text-only and requires model_loader=causal_lm; got {loader!r}")
|
|
77
|
+
def require_cuda() -> dict[str, Any]:
|
|
78
|
+
if not torch.cuda.is_available():
|
|
79
|
+
raise RuntimeError(
|
|
80
|
+
"TT Local bundled training requires an NVIDIA CUDA GPU. "
|
|
81
|
+
"Run this workflow on DGX Spark and verify it first with tt-local doctor."
|
|
82
|
+
)
|
|
83
|
+
device_index = torch.cuda.current_device()
|
|
84
|
+
properties = torch.cuda.get_device_properties(device_index)
|
|
85
|
+
return {
|
|
86
|
+
"device_index": device_index,
|
|
87
|
+
"device_name": properties.name,
|
|
88
|
+
"compute_capability": f"{properties.major}.{properties.minor}",
|
|
89
|
+
"bf16_supported": torch.cuda.is_bf16_supported(),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
80
93
|
def load_rows() -> list[dict[str, Any]]:
|
|
81
94
|
rows: list[dict[str, Any]] = []
|
|
82
95
|
for path in sorted(TRAINING_DIR.rglob("*.jsonl")):
|
|
83
|
-
for line_number, line in enumerate(path.read_text().splitlines(), start=1):
|
|
96
|
+
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
|
84
97
|
if not line.strip():
|
|
85
98
|
continue
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
try:
|
|
100
|
+
row = json.loads(line)
|
|
101
|
+
except json.JSONDecodeError as exc:
|
|
102
|
+
raise ValueError(f"{path}:{line_number} contains invalid JSON") from exc
|
|
103
|
+
if not isinstance(row, dict) or "messages" not in row:
|
|
104
|
+
raise ValueError(f"{path}:{line_number} must contain a messages array")
|
|
90
105
|
rows.append(row)
|
|
91
106
|
if not rows:
|
|
92
107
|
raise ValueError(f"No chat JSONL rows found under {TRAINING_DIR}")
|
|
93
108
|
return rows
|
|
94
109
|
|
|
95
110
|
|
|
96
|
-
def
|
|
97
|
-
destination.mkdir(parents=True, exist_ok=True)
|
|
98
|
-
destination_root = destination.resolve()
|
|
99
|
-
with tarfile.open(path, "r:gz") as tar:
|
|
100
|
-
members = tar.getmembers()
|
|
101
|
-
if len(members) > MAX_ARCHIVE_MEMBERS:
|
|
102
|
-
raise ValueError(f"Model archive exceeds {MAX_ARCHIVE_MEMBERS} members")
|
|
103
|
-
expanded_bytes = sum(max(0, member.size) for member in members if member.isfile())
|
|
104
|
-
if expanded_bytes > MAX_ARCHIVE_EXPANDED_BYTES:
|
|
105
|
-
raise ValueError("Model archive exceeds the 20 GiB expanded-size limit")
|
|
106
|
-
for member in members:
|
|
107
|
-
destination_path = (destination / member.name).resolve()
|
|
108
|
-
try:
|
|
109
|
-
destination_path.relative_to(destination_root)
|
|
110
|
-
except ValueError:
|
|
111
|
-
raise ValueError(f"Unsafe archive member: {member.name}")
|
|
112
|
-
if member.issym() or member.islnk() or member.isdev():
|
|
113
|
-
raise ValueError(f"Unsafe archive member type: {member.name}")
|
|
114
|
-
tar.extractall(destination)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def resolve_model_source(tmp: Path) -> str:
|
|
118
|
-
archive = BASE_MODEL_DIR / "model.tar.gz"
|
|
119
|
-
if archive.is_file():
|
|
120
|
-
extracted = tmp / "base-model"
|
|
121
|
-
safe_extract_archive(archive, extracted)
|
|
122
|
-
candidates = [path for path in extracted.iterdir() if path.is_dir()]
|
|
123
|
-
return str(candidates[0] if len(candidates) == 1 else extracted)
|
|
111
|
+
def resolve_model_source() -> str:
|
|
124
112
|
if BASE_MODEL_DIR.is_dir() and any(BASE_MODEL_DIR.iterdir()):
|
|
125
113
|
return str(BASE_MODEL_DIR)
|
|
126
|
-
return
|
|
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
|
-
def chat_template_kwargs() -> dict[str, Any]:
|
|
154
|
-
"""Optional kwargs forwarded to apply_chat_template (for example
|
|
155
|
-
{"enable_thinking": false} for thinking-mode models), passed by the
|
|
156
|
-
runner as a JSON hyperparameter so training text matches inference."""
|
|
157
|
-
raw = hp("chat_template_kwargs")
|
|
158
|
-
if not raw:
|
|
159
|
-
return {}
|
|
160
|
-
if isinstance(raw, dict):
|
|
161
|
-
return raw
|
|
162
|
-
try:
|
|
163
|
-
parsed = json.loads(str(raw))
|
|
164
|
-
return parsed if isinstance(parsed, dict) else {}
|
|
165
|
-
except json.JSONDecodeError:
|
|
166
|
-
return {}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def row_to_text(tokenizer: Any, row: dict[str, Any]) -> str:
|
|
170
|
-
messages = row["messages"]
|
|
171
|
-
try:
|
|
172
|
-
return tokenizer.apply_chat_template(
|
|
173
|
-
messages,
|
|
174
|
-
tokenize=False,
|
|
175
|
-
add_generation_prompt=False,
|
|
176
|
-
**chat_template_kwargs(),
|
|
177
|
-
)
|
|
178
|
-
except Exception:
|
|
179
|
-
parts: list[str] = []
|
|
180
|
-
for message in messages:
|
|
181
|
-
role = message.get("role", "user")
|
|
182
|
-
content = message.get("content", "")
|
|
183
|
-
if not isinstance(content, str):
|
|
184
|
-
content = json.dumps(content, ensure_ascii=False)
|
|
185
|
-
parts.append(f"{role}: {content}")
|
|
186
|
-
return "\n".join(parts)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
class TextDataset(Dataset[dict[str, torch.Tensor]]):
|
|
190
|
-
def __init__(self, texts: list[str], tokenizer: Any, max_length: int):
|
|
191
|
-
self.examples = [
|
|
192
|
-
tokenizer(
|
|
193
|
-
text,
|
|
194
|
-
truncation=True,
|
|
195
|
-
max_length=max_length,
|
|
196
|
-
padding=False,
|
|
197
|
-
return_tensors=None,
|
|
198
|
-
)
|
|
199
|
-
for text in texts
|
|
200
|
-
]
|
|
114
|
+
return CERTIFIED_BASE_MODEL
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class AssistantOnlyDataset(Dataset[dict[str, torch.Tensor]]):
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
rows: list[dict[str, Any]],
|
|
121
|
+
tokenizer: Any,
|
|
122
|
+
*,
|
|
123
|
+
max_length: int,
|
|
124
|
+
):
|
|
125
|
+
self.examples: list[dict[str, torch.Tensor]] = []
|
|
126
|
+
for row_index, row in enumerate(rows, start=1):
|
|
127
|
+
try:
|
|
128
|
+
tokenized = build_assistant_only_example(
|
|
129
|
+
tokenizer,
|
|
130
|
+
row["messages"],
|
|
131
|
+
max_length=max_length,
|
|
132
|
+
)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
raise ValueError(f"Training row {row_index} is not valid Qwen SFT data: {exc}") from exc
|
|
135
|
+
self.examples.append({
|
|
136
|
+
key: torch.tensor(value, dtype=torch.long)
|
|
137
|
+
for key, value in tokenized.items()
|
|
138
|
+
})
|
|
201
139
|
|
|
202
140
|
def __len__(self) -> int:
|
|
203
141
|
return len(self.examples)
|
|
204
142
|
|
|
205
143
|
def __getitem__(self, index: int) -> dict[str, torch.Tensor]:
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
"input_ids": torch.tensor(item["input_ids"], dtype=torch.long),
|
|
209
|
-
"attention_mask": torch.tensor(item["attention_mask"], dtype=torch.long),
|
|
210
|
-
}
|
|
144
|
+
return self.examples[index]
|
|
211
145
|
|
|
212
146
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return value
|
|
236
|
-
return fallback
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def multimodal_content(
|
|
240
|
-
content: Any,
|
|
241
|
-
top_level_images: list[str],
|
|
242
|
-
base_dir: Path,
|
|
243
|
-
) -> tuple[str | list[dict[str, Any]], list[Image.Image]]:
|
|
244
|
-
if isinstance(content, str):
|
|
245
|
-
return content, []
|
|
246
|
-
if not isinstance(content, list):
|
|
247
|
-
return str(content), []
|
|
248
|
-
|
|
249
|
-
images: list[Image.Image] = []
|
|
250
|
-
image_index = 0
|
|
251
|
-
converted: list[dict[str, Any]] = []
|
|
252
|
-
for part in content:
|
|
253
|
-
if not isinstance(part, dict):
|
|
254
|
-
converted.append({"type": "text", "text": str(part)})
|
|
255
|
-
continue
|
|
256
|
-
if part.get("type") != "image":
|
|
257
|
-
converted.append(part)
|
|
258
|
-
continue
|
|
259
|
-
fallback = top_level_images[image_index] if image_index < len(top_level_images) else None
|
|
260
|
-
image_index += 1
|
|
261
|
-
image_value = image_value_from_part(part, fallback)
|
|
262
|
-
if image_value:
|
|
263
|
-
images.append(load_image(image_value, base_dir))
|
|
264
|
-
converted.append({"type": "image"})
|
|
265
|
-
return converted, images
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def row_to_multimodal_example(row: dict[str, Any]) -> dict[str, Any]:
|
|
269
|
-
source_dir = Path(str(row.get(ROW_SOURCE_DIR_KEY) or TRAINING_DIR))
|
|
270
|
-
top_level_images = [str(value) for value in row.get("images", []) if isinstance(value, str)]
|
|
271
|
-
all_images: list[Image.Image] = []
|
|
272
|
-
messages: list[dict[str, Any]] = []
|
|
273
|
-
for message in row["messages"]:
|
|
274
|
-
content, images = multimodal_content(message.get("content", ""), top_level_images, source_dir)
|
|
275
|
-
all_images.extend(images)
|
|
276
|
-
messages.append({
|
|
277
|
-
"role": message.get("role", "user"),
|
|
278
|
-
"content": content,
|
|
279
|
-
})
|
|
280
|
-
if not all_images and top_level_images:
|
|
281
|
-
all_images = [load_image(value, source_dir) for value in top_level_images]
|
|
282
|
-
return {
|
|
283
|
-
"messages": messages,
|
|
284
|
-
"images": all_images,
|
|
285
|
-
}
|
|
147
|
+
class AssistantOnlyCollator:
|
|
148
|
+
def __init__(self, pad_token_id: int):
|
|
149
|
+
self.pad_token_id = pad_token_id
|
|
150
|
+
|
|
151
|
+
def __call__(self, features: list[dict[str, torch.Tensor]]) -> dict[str, torch.Tensor]:
|
|
152
|
+
return {
|
|
153
|
+
"input_ids": pad_sequence(
|
|
154
|
+
[feature["input_ids"] for feature in features],
|
|
155
|
+
batch_first=True,
|
|
156
|
+
padding_value=self.pad_token_id,
|
|
157
|
+
),
|
|
158
|
+
"attention_mask": pad_sequence(
|
|
159
|
+
[feature["attention_mask"] for feature in features],
|
|
160
|
+
batch_first=True,
|
|
161
|
+
padding_value=0,
|
|
162
|
+
),
|
|
163
|
+
"labels": pad_sequence(
|
|
164
|
+
[feature["labels"] for feature in features],
|
|
165
|
+
batch_first=True,
|
|
166
|
+
padding_value=IGNORE_INDEX,
|
|
167
|
+
),
|
|
168
|
+
}
|
|
286
169
|
|
|
287
170
|
|
|
288
|
-
def
|
|
289
|
-
trust_remote_code = hp_bool("trust_remote_code", True)
|
|
171
|
+
def create_model_and_tokenizer(model_source: str) -> tuple[Any, Any, torch.dtype]:
|
|
290
172
|
token = os.getenv("HF_TOKEN")
|
|
291
|
-
|
|
292
|
-
model_source,
|
|
173
|
+
source_kwargs: dict[str, Any] = {
|
|
293
174
|
**model_revision_kwargs(model_source),
|
|
294
|
-
trust_remote_code
|
|
295
|
-
token
|
|
296
|
-
|
|
297
|
-
if
|
|
175
|
+
"trust_remote_code": False,
|
|
176
|
+
"token": token,
|
|
177
|
+
}
|
|
178
|
+
if not Path(model_source).exists():
|
|
179
|
+
source_kwargs["local_files_only"] = True
|
|
180
|
+
tokenizer = AutoTokenizer.from_pretrained(model_source, **source_kwargs)
|
|
181
|
+
if tokenizer.eos_token_id is None:
|
|
182
|
+
raise ValueError(f"{CERTIFIED_BASE_MODEL} tokenizer has no EOS token")
|
|
183
|
+
if tokenizer.pad_token_id is None:
|
|
298
184
|
tokenizer.pad_token = tokenizer.eos_token
|
|
299
185
|
|
|
300
|
-
dtype = torch.bfloat16 if torch.cuda.
|
|
186
|
+
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
|
|
301
187
|
model = AutoModelForCausalLM.from_pretrained(
|
|
302
188
|
model_source,
|
|
303
|
-
**
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
torch_dtype=dtype if torch.cuda.is_available() else None,
|
|
307
|
-
device_map="auto" if torch.cuda.is_available() else None,
|
|
308
|
-
)
|
|
309
|
-
model.config.use_cache = False
|
|
310
|
-
return model, tokenizer
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def create_multimodal_model_and_processor(model_source: str):
|
|
314
|
-
trust_remote_code = hp_bool("trust_remote_code", True)
|
|
315
|
-
token = os.getenv("HF_TOKEN")
|
|
316
|
-
processor = AutoProcessor.from_pretrained(
|
|
317
|
-
model_source,
|
|
318
|
-
**model_revision_kwargs(model_source),
|
|
319
|
-
trust_remote_code=trust_remote_code,
|
|
320
|
-
token=token,
|
|
321
|
-
)
|
|
322
|
-
if getattr(processor, "tokenizer", None) is not None and processor.tokenizer.pad_token is None:
|
|
323
|
-
processor.tokenizer.pad_token = processor.tokenizer.eos_token
|
|
324
|
-
|
|
325
|
-
dtype = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16
|
|
326
|
-
model = AutoModelForImageTextToText.from_pretrained(
|
|
327
|
-
model_source,
|
|
328
|
-
**model_revision_kwargs(model_source),
|
|
329
|
-
trust_remote_code=trust_remote_code,
|
|
330
|
-
token=token,
|
|
331
|
-
torch_dtype=dtype if torch.cuda.is_available() else None,
|
|
332
|
-
device_map="auto" if torch.cuda.is_available() else None,
|
|
189
|
+
**source_kwargs,
|
|
190
|
+
dtype=dtype,
|
|
191
|
+
device_map={"": torch.cuda.current_device()},
|
|
333
192
|
)
|
|
193
|
+
assert_certified_model_config(model.config)
|
|
334
194
|
model.config.use_cache = False
|
|
335
|
-
return model,
|
|
195
|
+
return model, tokenizer, dtype
|
|
336
196
|
|
|
337
197
|
|
|
338
|
-
def
|
|
339
|
-
raw = hp("lora_target_modules", default) or default
|
|
340
|
-
if raw == "all-linear":
|
|
341
|
-
return raw
|
|
342
|
-
return [item.strip() for item in raw.split(",") if item.strip()]
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def maybe_apply_lora(model: Any):
|
|
346
|
-
rank = hp_int("lora_rank", 16)
|
|
347
|
-
alpha = hp_int("lora_alpha", 32)
|
|
348
|
-
dropout = hp_float("lora_dropout", 0.05)
|
|
198
|
+
def apply_lora(model: Any) -> Any:
|
|
349
199
|
config = LoraConfig(
|
|
350
|
-
r=rank,
|
|
351
|
-
lora_alpha=alpha,
|
|
352
|
-
lora_dropout=dropout,
|
|
353
|
-
bias="none",
|
|
354
|
-
task_type="CAUSAL_LM",
|
|
355
|
-
target_modules=lora_target_modules("all-linear"),
|
|
356
|
-
)
|
|
357
|
-
return get_peft_model(model, config)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
def multimodal_lora_config() -> LoraConfig:
|
|
361
|
-
return LoraConfig(
|
|
362
200
|
r=hp_int("lora_rank", 16),
|
|
363
201
|
lora_alpha=hp_int("lora_alpha", 32),
|
|
364
202
|
lora_dropout=hp_float("lora_dropout", 0.05),
|
|
365
203
|
bias="none",
|
|
366
|
-
|
|
204
|
+
task_type="CAUSAL_LM",
|
|
205
|
+
target_modules="all-linear",
|
|
367
206
|
)
|
|
207
|
+
return get_peft_model(model, config)
|
|
368
208
|
|
|
369
209
|
|
|
370
210
|
def create_model_archive() -> Path:
|
|
371
211
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
372
212
|
archive_path = OUTPUT_DIR / "model.tar.gz"
|
|
373
|
-
with tarfile.open(archive_path, "w:gz") as
|
|
374
|
-
|
|
213
|
+
with tarfile.open(archive_path, "w:gz") as archive:
|
|
214
|
+
archive.add(MODEL_DIR, arcname="model")
|
|
375
215
|
return archive_path
|
|
376
216
|
|
|
377
217
|
|
|
378
|
-
def
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
texts = [row_to_text(tokenizer, row) for row in rows]
|
|
389
|
-
dataset = TextDataset(texts, tokenizer, max_length=hp_int("max_seq_length", 2048))
|
|
390
|
-
collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
|
|
218
|
+
def run_training(rows: list[dict[str, Any]], model_source: str) -> tuple[dict[str, Any], torch.dtype]:
|
|
219
|
+
model, tokenizer, dtype = create_model_and_tokenizer(model_source)
|
|
220
|
+
model = apply_lora(model)
|
|
221
|
+
dataset = AssistantOnlyDataset(
|
|
222
|
+
rows,
|
|
223
|
+
tokenizer,
|
|
224
|
+
max_length=hp_int("max_seq_length", 2048),
|
|
225
|
+
)
|
|
226
|
+
collator = AssistantOnlyCollator(tokenizer.pad_token_id)
|
|
391
227
|
|
|
228
|
+
with TemporaryDirectory(prefix="tt-local-sft-") as output_dir:
|
|
392
229
|
args = TrainingArguments(
|
|
393
|
-
output_dir=
|
|
230
|
+
output_dir=output_dir,
|
|
394
231
|
num_train_epochs=hp_int("n_epochs", 3),
|
|
395
232
|
learning_rate=hp_float("learning_rate", 0.00001),
|
|
396
233
|
per_device_train_batch_size=hp_int("per_device_train_batch_size", 1),
|
|
@@ -399,8 +236,9 @@ def run_text_training(rows: list[dict[str, Any]], model_source: str) -> dict[str
|
|
|
399
236
|
save_strategy="no",
|
|
400
237
|
report_to=[],
|
|
401
238
|
remove_unused_columns=False,
|
|
402
|
-
bf16=
|
|
403
|
-
fp16=
|
|
239
|
+
bf16=dtype == torch.bfloat16,
|
|
240
|
+
fp16=dtype == torch.float16,
|
|
241
|
+
optim="adamw_torch",
|
|
404
242
|
)
|
|
405
243
|
trainer = Trainer(
|
|
406
244
|
model=model,
|
|
@@ -412,77 +250,39 @@ def run_text_training(rows: list[dict[str, Any]], model_source: str) -> dict[str
|
|
|
412
250
|
|
|
413
251
|
model.save_pretrained(MODEL_DIR)
|
|
414
252
|
tokenizer.save_pretrained(MODEL_DIR)
|
|
415
|
-
return result.metrics
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
def run_multimodal_training(rows: list[dict[str, Any]], model_source: str) -> dict[str, Any]:
|
|
419
|
-
with TemporaryDirectory() as tmp:
|
|
420
|
-
parent_adapter = resolve_adapter_path(hp("parent_model_artifact"), Path(tmp))
|
|
421
|
-
model, processor = create_multimodal_model_and_processor(model_source)
|
|
422
|
-
if parent_adapter:
|
|
423
|
-
model = PeftModel.from_pretrained(model, parent_adapter, is_trainable=True)
|
|
424
|
-
dataset = HFDataset.from_list([row_to_multimodal_example(row) for row in rows])
|
|
425
|
-
|
|
426
|
-
args = SFTConfig(
|
|
427
|
-
output_dir=tmp,
|
|
428
|
-
num_train_epochs=hp_int("n_epochs", 3),
|
|
429
|
-
learning_rate=hp_float("learning_rate", 0.00001),
|
|
430
|
-
per_device_train_batch_size=hp_int("per_device_train_batch_size", 1),
|
|
431
|
-
gradient_accumulation_steps=hp_int("gradient_accumulation_steps", 8),
|
|
432
|
-
logging_steps=1,
|
|
433
|
-
save_strategy="no",
|
|
434
|
-
report_to="none",
|
|
435
|
-
remove_unused_columns=False,
|
|
436
|
-
bf16=torch.cuda.is_available() and torch.cuda.is_bf16_supported(),
|
|
437
|
-
fp16=torch.cuda.is_available() and not torch.cuda.is_bf16_supported(),
|
|
438
|
-
gradient_checkpointing=True,
|
|
439
|
-
gradient_checkpointing_kwargs={"use_reentrant": False},
|
|
440
|
-
dataset_kwargs={"skip_prepare_dataset": True},
|
|
441
|
-
max_length=None,
|
|
442
|
-
)
|
|
443
|
-
trainer_values: dict[str, Any] = {
|
|
444
|
-
"model": model,
|
|
445
|
-
"args": args,
|
|
446
|
-
"train_dataset": dataset,
|
|
447
|
-
"processing_class": processor,
|
|
448
|
-
}
|
|
449
|
-
if not parent_adapter:
|
|
450
|
-
trainer_values["peft_config"] = multimodal_lora_config()
|
|
451
|
-
trainer = SFTTrainer(**trainer_values)
|
|
452
|
-
result = trainer.train()
|
|
453
|
-
trainer.save_model(MODEL_DIR)
|
|
454
|
-
processor.save_pretrained(MODEL_DIR)
|
|
455
|
-
return result.metrics
|
|
253
|
+
return result.metrics, dtype
|
|
456
254
|
|
|
457
255
|
|
|
458
256
|
def main() -> None:
|
|
459
257
|
started = time.time()
|
|
258
|
+
assert_certified_request()
|
|
259
|
+
cuda = require_cuda()
|
|
460
260
|
MODEL_DIR.mkdir(parents=True, exist_ok=True)
|
|
461
261
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
462
262
|
|
|
463
263
|
rows = load_rows()
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
model_loader = hp("model_loader", "causal_lm")
|
|
467
|
-
if model_loader == "image_text_to_text":
|
|
468
|
-
metrics = run_multimodal_training(rows, model_source)
|
|
469
|
-
else:
|
|
470
|
-
metrics = run_text_training(rows, model_source)
|
|
471
|
-
|
|
264
|
+
model_source = resolve_model_source()
|
|
265
|
+
metrics, dtype = run_training(rows, model_source)
|
|
472
266
|
archive_path = create_model_archive()
|
|
473
267
|
output_metrics = {
|
|
474
268
|
"training_rows": len(rows),
|
|
475
|
-
"
|
|
269
|
+
"base_model": CERTIFIED_BASE_MODEL,
|
|
476
270
|
"base_model_revision": hp("base_model_revision"),
|
|
477
|
-
"
|
|
478
|
-
"
|
|
271
|
+
"model_source": model_source,
|
|
272
|
+
"model_loader": "causal_lm",
|
|
273
|
+
"loss_mask": "final_assistant_only",
|
|
274
|
+
"device": "cuda",
|
|
275
|
+
"cuda_device": cuda["device_name"],
|
|
276
|
+
"cuda_compute_capability": cuda["compute_capability"],
|
|
277
|
+
"dtype": str(dtype).removeprefix("torch."),
|
|
479
278
|
"train_runtime": round(time.time() - started, 3),
|
|
480
279
|
**{key: float(value) for key, value in metrics.items() if isinstance(value, (int, float))},
|
|
481
280
|
"model_archive": str(archive_path),
|
|
482
281
|
}
|
|
483
|
-
|
|
484
|
-
(
|
|
485
|
-
|
|
282
|
+
metrics_json = json.dumps(output_metrics, indent=2)
|
|
283
|
+
(MODEL_DIR / "training-metrics.json").write_text(metrics_json, encoding="utf-8")
|
|
284
|
+
(OUTPUT_DIR / "training-metrics.json").write_text(metrics_json, encoding="utf-8")
|
|
285
|
+
print(metrics_json)
|
|
486
286
|
|
|
487
287
|
|
|
488
288
|
if __name__ == "__main__":
|