@tuned-tensor/cli 0.13.1-beta.5 → 0.13.1-beta.7
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/README.md +24 -6
- package/dist/{chunk-AVMRHDFY.js → chunk-AKJ5WZ2S.js} +75 -52
- package/dist/chunk-AKJ5WZ2S.js.map +1 -0
- package/dist/index.js +142 -22
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.js +71 -6
- package/dist/local-runtime.js.map +1 -1
- package/docs/local-runtime/architecture.md +24 -6
- package/docs/local-runtime/foundation-long-runs.md +108 -0
- package/package.json +1 -1
- package/training/foundation/src/checkpoint.py +235 -0
- package/training/foundation/src/common.py +30 -2
- package/training/foundation/src/data.py +154 -6
- package/training/foundation/src/evaluate.py +43 -9
- package/training/foundation/src/pretrain.py +439 -27
- package/training/foundation/src/rl.py +11 -3
- package/training/foundation/src/train_tokenizer.py +38 -5
- package/dist/chunk-AVMRHDFY.js.map +0 -1
|
@@ -8,7 +8,7 @@ from tokenizers import Tokenizer
|
|
|
8
8
|
from torch.nn import functional as F
|
|
9
9
|
|
|
10
10
|
from common import load_config, write_json
|
|
11
|
-
from data import END, decoded_byte_count,
|
|
11
|
+
from data import END, decoded_byte_count, encode_generation_prompt, example_pairs, iter_corpus_documents, smoke_corpus, window_tokens
|
|
12
12
|
from model import bits_per_byte, generate, load_model
|
|
13
13
|
|
|
14
14
|
|
|
@@ -45,12 +45,19 @@ def evaluate_chat(
|
|
|
45
45
|
correct = 0
|
|
46
46
|
model.eval()
|
|
47
47
|
for example in examples:
|
|
48
|
-
|
|
48
|
+
context_length = int(model.config["sequence_length"])
|
|
49
|
+
prompt_ids, completion_tokens = encode_generation_prompt(
|
|
50
|
+
tokenizer,
|
|
51
|
+
system_prompt,
|
|
52
|
+
example["input"],
|
|
53
|
+
context_length,
|
|
54
|
+
32,
|
|
55
|
+
)
|
|
49
56
|
tokens = torch.tensor([prompt_ids], dtype=torch.long, device=device)
|
|
50
57
|
generated = generate(
|
|
51
58
|
model,
|
|
52
59
|
tokens,
|
|
53
|
-
max_new_tokens=
|
|
60
|
+
max_new_tokens=completion_tokens,
|
|
54
61
|
stop_token_id=tokenizer.token_to_id(END),
|
|
55
62
|
)[0].tolist()[len(prompt_ids):]
|
|
56
63
|
text = tokenizer.decode(generated).split(END)[0].strip()
|
|
@@ -65,14 +72,27 @@ def evaluate_chat(
|
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
|
|
68
|
-
def evaluate_inference(
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
def evaluate_inference(
|
|
76
|
+
model,
|
|
77
|
+
tokenizer: Tokenizer,
|
|
78
|
+
system_prompt: str,
|
|
79
|
+
user_prompt: str,
|
|
80
|
+
device: torch.device,
|
|
81
|
+
) -> dict[str, float | int | str]:
|
|
82
|
+
context_length = int(model.config["sequence_length"])
|
|
83
|
+
prompt_ids, completion_tokens = encode_generation_prompt(
|
|
84
|
+
tokenizer,
|
|
85
|
+
system_prompt,
|
|
86
|
+
user_prompt,
|
|
87
|
+
context_length,
|
|
88
|
+
32,
|
|
89
|
+
)
|
|
90
|
+
tokens = torch.tensor([prompt_ids], dtype=torch.long, device=device)
|
|
71
91
|
if device.type == "cuda":
|
|
72
92
|
torch.cuda.reset_peak_memory_stats(device)
|
|
73
93
|
torch.cuda.synchronize()
|
|
74
94
|
started = time.perf_counter()
|
|
75
|
-
generated = generate(model, tokens, max_new_tokens=
|
|
95
|
+
generated = generate(model, tokens, max_new_tokens=completion_tokens)
|
|
76
96
|
if device.type == "cuda":
|
|
77
97
|
torch.cuda.synchronize()
|
|
78
98
|
elapsed = max(time.perf_counter() - started, 1e-6)
|
|
@@ -98,13 +118,27 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
98
118
|
output_dir = Path(config["output_dir"])
|
|
99
119
|
metrics: dict = {"ok": True, "evaluator": evaluator, "device": str(device)}
|
|
100
120
|
if evaluator == "bpb":
|
|
101
|
-
|
|
121
|
+
validation_path = config.get("validation_path")
|
|
122
|
+
if validation_path:
|
|
123
|
+
validation_chars = int(config.get("validation_max_chars") or 2_000_000)
|
|
124
|
+
corpus = "".join(iter_corpus_documents(validation_path, validation_chars))
|
|
125
|
+
metrics["held_out"] = True
|
|
126
|
+
metrics["validation_path"] = str(validation_path)
|
|
127
|
+
else:
|
|
128
|
+
corpus = smoke_corpus(examples, int(config.get("max_chars") or 20_000), extra=str(config.get("system_prompt") or ""))
|
|
129
|
+
metrics["held_out"] = False
|
|
102
130
|
metrics.update(evaluate_bpb(model, tokenizer, corpus, sequence_length, device))
|
|
103
131
|
elif evaluator == "chat":
|
|
104
132
|
metrics.update(evaluate_chat(model, tokenizer, str(config.get("system_prompt") or ""), examples, device))
|
|
105
133
|
elif evaluator == "inference":
|
|
106
134
|
prompt = examples[0]["input"] if examples else "hello"
|
|
107
|
-
metrics.update(evaluate_inference(
|
|
135
|
+
metrics.update(evaluate_inference(
|
|
136
|
+
model,
|
|
137
|
+
tokenizer,
|
|
138
|
+
str(config.get("system_prompt") or ""),
|
|
139
|
+
prompt,
|
|
140
|
+
device,
|
|
141
|
+
))
|
|
108
142
|
else:
|
|
109
143
|
raise SystemExit(f"Unknown foundation evaluator: {evaluator}")
|
|
110
144
|
write_json(output_dir / "metrics.json", metrics)
|
|
@@ -1,63 +1,475 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
3
7
|
from pathlib import Path
|
|
8
|
+
import signal
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any
|
|
4
11
|
|
|
12
|
+
import numpy as np
|
|
5
13
|
import torch
|
|
6
14
|
from tokenizers import Tokenizer
|
|
7
15
|
from torch.nn import functional as F
|
|
8
16
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from
|
|
17
|
+
from checkpoint import CheckpointManager
|
|
18
|
+
from common import append_jsonl, ensure_private_directory, load_config, make_private_file, require_cuda, write_json
|
|
19
|
+
from data import (
|
|
20
|
+
END,
|
|
21
|
+
corpus_hash,
|
|
22
|
+
corpus_manifest,
|
|
23
|
+
example_pairs,
|
|
24
|
+
instruction_corpus_documents,
|
|
25
|
+
iter_corpus_documents,
|
|
26
|
+
smoke_corpus,
|
|
27
|
+
)
|
|
28
|
+
from model import FoundationGPT, model_config_from_depth, param_count, save_model
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def stable_fingerprint(value: Any) -> str:
|
|
32
|
+
return hashlib.sha256(
|
|
33
|
+
json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8"),
|
|
34
|
+
).hexdigest()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def file_sha256(path: str | Path) -> str:
|
|
38
|
+
digest = hashlib.sha256()
|
|
39
|
+
with Path(path).open("rb") as source:
|
|
40
|
+
while chunk := source.read(1_048_576):
|
|
41
|
+
digest.update(chunk)
|
|
42
|
+
return digest.hexdigest()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def source_signature(config: dict[str, Any]) -> dict[str, Any]:
|
|
46
|
+
corpus_path = config.get("corpus_path")
|
|
47
|
+
if corpus_path:
|
|
48
|
+
instruction = str(config.get("system_prompt") or "").strip()
|
|
49
|
+
return {
|
|
50
|
+
"kind": "files",
|
|
51
|
+
"manifest": corpus_manifest(corpus_path),
|
|
52
|
+
"max_chars": int(config.get("max_chars") or 0),
|
|
53
|
+
"instruction_sha256": corpus_hash(instruction),
|
|
54
|
+
}
|
|
55
|
+
examples = example_pairs(config.get("examples") or [])
|
|
56
|
+
corpus = smoke_corpus(
|
|
57
|
+
examples,
|
|
58
|
+
int(config.get("max_chars") or 20_000),
|
|
59
|
+
extra=str(config.get("system_prompt") or ""),
|
|
60
|
+
)
|
|
61
|
+
return {"kind": "smoke", "sha256": corpus_hash(corpus), "chars": len(corpus)}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def prepare_token_cache(
|
|
65
|
+
tokenizer: Tokenizer,
|
|
66
|
+
tokenizer_path: Path,
|
|
67
|
+
config: dict[str, Any],
|
|
68
|
+
output_dir: Path,
|
|
69
|
+
) -> tuple[np.memmap, dict[str, Any]]:
|
|
70
|
+
cache_path = output_dir / "pretrain-tokens.bin"
|
|
71
|
+
metadata_path = output_dir / "pretrain-tokens.json"
|
|
72
|
+
signature = {
|
|
73
|
+
"source": source_signature(config),
|
|
74
|
+
"tokenizer_sha256": file_sha256(tokenizer_path),
|
|
75
|
+
}
|
|
76
|
+
if cache_path.is_file() and metadata_path.is_file():
|
|
77
|
+
try:
|
|
78
|
+
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
|
79
|
+
except json.JSONDecodeError:
|
|
80
|
+
metadata = {}
|
|
81
|
+
expected_size = int(metadata.get("tokens", 0)) * np.dtype(np.uint32).itemsize
|
|
82
|
+
if (
|
|
83
|
+
metadata.get("signature") == signature
|
|
84
|
+
and expected_size == cache_path.stat().st_size
|
|
85
|
+
and isinstance(metadata.get("tokens_sha256"), str)
|
|
86
|
+
and file_sha256(cache_path) == metadata["tokens_sha256"]
|
|
87
|
+
):
|
|
88
|
+
return np.memmap(cache_path, dtype=np.uint32, mode="r"), metadata
|
|
89
|
+
|
|
90
|
+
partial_path = output_dir / ".pretrain-tokens.partial"
|
|
91
|
+
partial_metadata_path = output_dir / ".pretrain-tokens.partial.json"
|
|
92
|
+
partial_metadata: dict[str, Any] = {}
|
|
93
|
+
if partial_metadata_path.is_file():
|
|
94
|
+
try:
|
|
95
|
+
partial_metadata = json.loads(partial_metadata_path.read_text(encoding="utf-8"))
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
partial_metadata = {}
|
|
98
|
+
if cache_path.is_file() and partial_metadata.get("signature") == signature:
|
|
99
|
+
expected_size = int(partial_metadata.get("bytes", -1))
|
|
100
|
+
expected_tokens = int(partial_metadata.get("tokens", 0))
|
|
101
|
+
expected_hash = partial_metadata.get("tokens_sha256")
|
|
102
|
+
if (
|
|
103
|
+
expected_size == cache_path.stat().st_size
|
|
104
|
+
and expected_tokens > 0
|
|
105
|
+
and expected_tokens * np.dtype(np.uint32).itemsize == expected_size
|
|
106
|
+
and isinstance(expected_hash, str)
|
|
107
|
+
and file_sha256(cache_path) == expected_hash
|
|
108
|
+
):
|
|
109
|
+
metadata = {
|
|
110
|
+
"signature": signature,
|
|
111
|
+
"tokens": int(partial_metadata["tokens"]),
|
|
112
|
+
"documents": int(partial_metadata["documents"]),
|
|
113
|
+
"source_documents": int(partial_metadata.get("source_documents", partial_metadata["documents"])),
|
|
114
|
+
"tokens_sha256": str(partial_metadata["tokens_sha256"]),
|
|
115
|
+
}
|
|
116
|
+
write_json(metadata_path, metadata)
|
|
117
|
+
partial_metadata_path.unlink(missing_ok=True)
|
|
118
|
+
return np.memmap(cache_path, dtype=np.uint32, mode="r"), metadata
|
|
119
|
+
if partial_metadata.get("signature") != signature or not partial_path.is_file():
|
|
120
|
+
partial_path.unlink(missing_ok=True)
|
|
121
|
+
partial_metadata_path.unlink(missing_ok=True)
|
|
122
|
+
partial_metadata = {}
|
|
123
|
+
|
|
124
|
+
digest = hashlib.sha256()
|
|
125
|
+
token_count = int(partial_metadata.get("tokens", 0))
|
|
126
|
+
document_count = int(partial_metadata.get("documents", 0))
|
|
127
|
+
source_documents = int(partial_metadata.get("source_documents", document_count))
|
|
128
|
+
recorded_bytes = int(partial_metadata.get("bytes", 0))
|
|
129
|
+
if partial_path.is_file():
|
|
130
|
+
partial_size = partial_path.stat().st_size
|
|
131
|
+
valid_partial = (
|
|
132
|
+
0 <= recorded_bytes <= partial_size
|
|
133
|
+
and token_count >= 0
|
|
134
|
+
and token_count * np.dtype(np.uint32).itemsize == recorded_bytes
|
|
135
|
+
and isinstance(partial_metadata.get("tokens_sha256"), str)
|
|
136
|
+
)
|
|
137
|
+
if valid_partial:
|
|
138
|
+
with partial_path.open("r+b") as partial:
|
|
139
|
+
partial.truncate(recorded_bytes)
|
|
140
|
+
with partial_path.open("rb") as partial:
|
|
141
|
+
while chunk := partial.read(1_048_576):
|
|
142
|
+
digest.update(chunk)
|
|
143
|
+
valid_partial = digest.hexdigest() == partial_metadata["tokens_sha256"]
|
|
144
|
+
if not valid_partial:
|
|
145
|
+
partial_path.unlink(missing_ok=True)
|
|
146
|
+
partial_metadata_path.unlink(missing_ok=True)
|
|
147
|
+
partial_metadata = {}
|
|
148
|
+
digest = hashlib.sha256()
|
|
149
|
+
token_count = 0
|
|
150
|
+
document_count = 0
|
|
151
|
+
source_documents = 0
|
|
152
|
+
recorded_bytes = 0
|
|
153
|
+
corpus_path = config.get("corpus_path")
|
|
154
|
+
if corpus_path:
|
|
155
|
+
documents = instruction_corpus_documents(
|
|
156
|
+
str(config.get("system_prompt") or ""),
|
|
157
|
+
iter_corpus_documents(corpus_path, int(config.get("max_chars") or 0)),
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
examples = example_pairs(config.get("examples") or [])
|
|
161
|
+
documents = iter([smoke_corpus(
|
|
162
|
+
examples,
|
|
163
|
+
int(config.get("max_chars") or 20_000),
|
|
164
|
+
extra=str(config.get("system_prompt") or ""),
|
|
165
|
+
)])
|
|
166
|
+
separator = tokenizer.token_to_id(END)
|
|
167
|
+
|
|
168
|
+
def persist_partial(destination: Any) -> None:
|
|
169
|
+
destination.flush()
|
|
170
|
+
os.fsync(destination.fileno())
|
|
171
|
+
write_json(partial_metadata_path, {
|
|
172
|
+
"signature": signature,
|
|
173
|
+
"tokens": token_count,
|
|
174
|
+
"documents": document_count,
|
|
175
|
+
"source_documents": source_documents,
|
|
176
|
+
"bytes": destination.tell(),
|
|
177
|
+
"tokens_sha256": digest.hexdigest(),
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
mode = "ab" if partial_path.is_file() else "wb"
|
|
181
|
+
with partial_path.open(mode) as destination:
|
|
182
|
+
os.fchmod(destination.fileno(), 0o600)
|
|
183
|
+
for source_index, document in enumerate(documents):
|
|
184
|
+
if source_index < source_documents:
|
|
185
|
+
continue
|
|
186
|
+
source_documents += 1
|
|
187
|
+
ids = tokenizer.encode(document).ids
|
|
188
|
+
if separator is not None:
|
|
189
|
+
ids.append(separator)
|
|
190
|
+
if not ids:
|
|
191
|
+
continue
|
|
192
|
+
values = np.asarray(ids, dtype=np.uint32)
|
|
193
|
+
payload = values.tobytes()
|
|
194
|
+
destination.write(payload)
|
|
195
|
+
digest.update(payload)
|
|
196
|
+
token_count += len(ids)
|
|
197
|
+
document_count += 1
|
|
198
|
+
if document_count % 1024 == 0:
|
|
199
|
+
persist_partial(destination)
|
|
200
|
+
persist_partial(destination)
|
|
201
|
+
if source_signature(config) != signature["source"]:
|
|
202
|
+
partial_path.unlink(missing_ok=True)
|
|
203
|
+
partial_metadata_path.unlink(missing_ok=True)
|
|
204
|
+
raise RuntimeError("Foundation corpus changed while the token cache was being built.")
|
|
205
|
+
if token_count < 2:
|
|
206
|
+
partial_path.unlink(missing_ok=True)
|
|
207
|
+
partial_metadata_path.unlink(missing_ok=True)
|
|
208
|
+
raise ValueError("Foundation pretraining corpus produced fewer than two tokens.")
|
|
209
|
+
os.replace(partial_path, cache_path)
|
|
210
|
+
make_private_file(cache_path)
|
|
211
|
+
metadata = {
|
|
212
|
+
"signature": signature,
|
|
213
|
+
"tokens": token_count,
|
|
214
|
+
"documents": document_count,
|
|
215
|
+
"source_documents": source_documents,
|
|
216
|
+
"tokens_sha256": digest.hexdigest(),
|
|
217
|
+
}
|
|
218
|
+
write_json(metadata_path, metadata)
|
|
219
|
+
partial_metadata_path.unlink(missing_ok=True)
|
|
220
|
+
return np.memmap(cache_path, dtype=np.uint32, mode="r"), metadata
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def coprime_stride(windows: int, seed: int) -> int:
|
|
224
|
+
if windows <= 1:
|
|
225
|
+
return 1
|
|
226
|
+
candidate = max(1, (2 * abs(seed) + 1) % windows)
|
|
227
|
+
while math.gcd(candidate, windows) != 1:
|
|
228
|
+
candidate = (candidate + 1) % windows or 1
|
|
229
|
+
return candidate
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def token_batch(
|
|
233
|
+
tokens: np.memmap,
|
|
234
|
+
*,
|
|
235
|
+
samples_consumed: int,
|
|
236
|
+
batch_size: int,
|
|
237
|
+
sequence_length: int,
|
|
238
|
+
seed: int,
|
|
239
|
+
) -> np.ndarray:
|
|
240
|
+
if len(tokens) < sequence_length + 1:
|
|
241
|
+
repeats = math.ceil((sequence_length + 1) / len(tokens))
|
|
242
|
+
expanded = np.tile(np.asarray(tokens), repeats)
|
|
243
|
+
return np.stack([expanded[:sequence_length + 1]] * batch_size)
|
|
244
|
+
windows = max(1, (len(tokens) - 1) // sequence_length)
|
|
245
|
+
stride = coprime_stride(windows, seed)
|
|
246
|
+
offset = abs(seed) % windows
|
|
247
|
+
rows: list[np.ndarray] = []
|
|
248
|
+
for index in range(batch_size):
|
|
249
|
+
window = ((samples_consumed + index) * stride + offset) % windows
|
|
250
|
+
start = window * sequence_length
|
|
251
|
+
rows.append(np.asarray(tokens[start:start + sequence_length + 1]))
|
|
252
|
+
return np.stack(rows)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def learning_rate_multiplier(step: int, *, warmup_steps: int, total_steps: int, min_ratio: float) -> float:
|
|
256
|
+
if warmup_steps > 0 and step < warmup_steps:
|
|
257
|
+
return max((step + 1) / warmup_steps, 1 / max(warmup_steps, 1))
|
|
258
|
+
decay_steps = max(total_steps - warmup_steps, 1)
|
|
259
|
+
progress = min(max((step - warmup_steps) / decay_steps, 0.0), 1.0)
|
|
260
|
+
cosine = 0.5 * (1.0 + math.cos(math.pi * progress))
|
|
261
|
+
return min_ratio + (1.0 - min_ratio) * cosine
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class StopRequest:
|
|
265
|
+
signal_number: int | None = None
|
|
266
|
+
|
|
267
|
+
def install(self) -> None:
|
|
268
|
+
def request_stop(number: int, _frame: Any) -> None:
|
|
269
|
+
self.signal_number = number
|
|
270
|
+
|
|
271
|
+
signal.signal(signal.SIGINT, request_stop)
|
|
272
|
+
signal.signal(signal.SIGTERM, request_stop)
|
|
12
273
|
|
|
13
274
|
|
|
14
275
|
def main(argv: list[str] | None = None) -> None:
|
|
15
276
|
config = load_config(argv)
|
|
16
277
|
require_cuda("pretrain")
|
|
17
278
|
device = torch.device("cuda")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
279
|
+
torch.set_float32_matmul_precision("high")
|
|
280
|
+
seed = int(config.get("seed") or 1337)
|
|
281
|
+
torch.manual_seed(seed)
|
|
282
|
+
torch.cuda.manual_seed_all(seed)
|
|
283
|
+
|
|
284
|
+
tokenizer_path = Path(config["tokenizer_dir"]) / "tokenizer.json"
|
|
285
|
+
tokenizer = Tokenizer.from_file(str(tokenizer_path))
|
|
22
286
|
sequence_length = int(config.get("sequence_length") or 64)
|
|
23
|
-
windows = window_tokens(token_ids, sequence_length)
|
|
24
287
|
depth = int(config.get("depth") or 2)
|
|
25
288
|
model_config = model_config_from_depth(depth, tokenizer.get_vocab_size(), sequence_length)
|
|
26
289
|
model = FoundationGPT(model_config).to(device)
|
|
27
|
-
|
|
28
|
-
|
|
290
|
+
|
|
291
|
+
output_dir = ensure_private_directory(config["output_dir"])
|
|
292
|
+
work_dir = ensure_private_directory(config.get("work_dir") or output_dir)
|
|
293
|
+
tokens, token_metadata = prepare_token_cache(tokenizer, tokenizer_path, config, work_dir)
|
|
29
294
|
steps = max(1, int(config.get("steps") or 2))
|
|
295
|
+
batch_size = max(1, int(config.get("batch_size") or 2))
|
|
296
|
+
accumulation_steps = max(1, int(config.get("gradient_accumulation_steps") or 1))
|
|
297
|
+
learning_rate = float(config.get("learning_rate") or 3e-4)
|
|
298
|
+
weight_decay = float(config.get("weight_decay") if config.get("weight_decay") is not None else 0.1)
|
|
299
|
+
configured_warmup = config.get("warmup_steps")
|
|
300
|
+
warmup_steps = min(
|
|
301
|
+
max(0, int(configured_warmup)) if configured_warmup is not None else min(2000, max(1, steps // 100)),
|
|
302
|
+
steps,
|
|
303
|
+
)
|
|
304
|
+
min_lr_ratio = float(config.get("min_lr_ratio") if config.get("min_lr_ratio") is not None else 0.1)
|
|
305
|
+
gradient_clip = float(config.get("gradient_clip") or 1.0)
|
|
306
|
+
bf16 = bool(config.get("bf16", True))
|
|
307
|
+
if bf16 and not torch.cuda.is_bf16_supported():
|
|
308
|
+
raise RuntimeError("Foundation BF16 training was requested, but this CUDA device does not support BF16.")
|
|
309
|
+
|
|
310
|
+
optimizer = torch.optim.AdamW(
|
|
311
|
+
model.parameters(),
|
|
312
|
+
lr=learning_rate,
|
|
313
|
+
weight_decay=weight_decay,
|
|
314
|
+
fused=True,
|
|
315
|
+
)
|
|
316
|
+
scheduler = torch.optim.lr_scheduler.LambdaLR(
|
|
317
|
+
optimizer,
|
|
318
|
+
lambda current: learning_rate_multiplier(
|
|
319
|
+
current,
|
|
320
|
+
warmup_steps=warmup_steps,
|
|
321
|
+
total_steps=steps,
|
|
322
|
+
min_ratio=min_lr_ratio,
|
|
323
|
+
),
|
|
324
|
+
)
|
|
325
|
+
recovery_config = {
|
|
326
|
+
"model": model_config,
|
|
327
|
+
"steps": steps,
|
|
328
|
+
"batch_size": batch_size,
|
|
329
|
+
"gradient_accumulation_steps": accumulation_steps,
|
|
330
|
+
"learning_rate": learning_rate,
|
|
331
|
+
"weight_decay": weight_decay,
|
|
332
|
+
"warmup_steps": warmup_steps,
|
|
333
|
+
"min_lr_ratio": min_lr_ratio,
|
|
334
|
+
"gradient_clip": gradient_clip,
|
|
335
|
+
"bf16": bf16,
|
|
336
|
+
"seed": seed,
|
|
337
|
+
"token_cache": token_metadata,
|
|
338
|
+
}
|
|
339
|
+
manager = CheckpointManager(
|
|
340
|
+
work_dir,
|
|
341
|
+
stable_fingerprint(recovery_config),
|
|
342
|
+
keep=int(config.get("keep_checkpoints") or 3),
|
|
343
|
+
backup_dir=config.get("checkpoint_backup_dir"),
|
|
344
|
+
)
|
|
345
|
+
start_step = 0
|
|
346
|
+
samples_consumed = 0
|
|
347
|
+
tokens_seen = 0
|
|
30
348
|
last_loss = 0.0
|
|
31
|
-
|
|
349
|
+
if bool(config.get("resume", True)):
|
|
350
|
+
resumed = manager.load(model, optimizer, scheduler, device)
|
|
351
|
+
if resumed is not None:
|
|
352
|
+
start_step = resumed.step
|
|
353
|
+
samples_consumed = resumed.samples_consumed
|
|
354
|
+
tokens_seen = resumed.tokens_seen
|
|
355
|
+
last_loss = resumed.last_loss
|
|
356
|
+
print(json.dumps({"event": "resumed", "step": start_step, "checkpoint": str(resumed.path)}), flush=True)
|
|
357
|
+
|
|
358
|
+
checkpoint_steps = max(1, int(config.get("checkpoint_interval_steps") or 500))
|
|
359
|
+
checkpoint_seconds = max(1, int(config.get("checkpoint_interval_seconds") or 1800))
|
|
360
|
+
log_steps = max(1, int(config.get("log_interval_steps") or 10))
|
|
361
|
+
metrics_path = work_dir / "training-metrics.jsonl"
|
|
362
|
+
stop = StopRequest()
|
|
363
|
+
stop.install()
|
|
364
|
+
started = time.monotonic()
|
|
365
|
+
last_log_time = started
|
|
366
|
+
last_log_tokens = tokens_seen
|
|
367
|
+
last_checkpoint_time = started
|
|
32
368
|
model.train()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if len(batch) < batch_size:
|
|
36
|
-
batch = (batch + windows)[:batch_size]
|
|
37
|
-
tensor = torch.tensor(batch, dtype=torch.long, device=device)
|
|
38
|
-
inputs = tensor[:, :-1]
|
|
39
|
-
labels = tensor[:, 1:]
|
|
40
|
-
logits = model(inputs)
|
|
41
|
-
loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), labels.reshape(-1))
|
|
369
|
+
|
|
370
|
+
for step in range(start_step, steps):
|
|
42
371
|
optimizer.zero_grad(set_to_none=True)
|
|
43
|
-
|
|
372
|
+
accumulated_loss = 0.0
|
|
373
|
+
for _microstep in range(accumulation_steps):
|
|
374
|
+
rows = token_batch(
|
|
375
|
+
tokens,
|
|
376
|
+
samples_consumed=samples_consumed,
|
|
377
|
+
batch_size=batch_size,
|
|
378
|
+
sequence_length=sequence_length,
|
|
379
|
+
seed=seed,
|
|
380
|
+
)
|
|
381
|
+
tensor = torch.from_numpy(rows.astype(np.int64, copy=False)).to(device=device, non_blocking=True)
|
|
382
|
+
inputs = tensor[:, :-1]
|
|
383
|
+
labels = tensor[:, 1:]
|
|
384
|
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16, enabled=bf16):
|
|
385
|
+
logits = model(inputs)
|
|
386
|
+
raw_loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), labels.reshape(-1))
|
|
387
|
+
loss = raw_loss / accumulation_steps
|
|
388
|
+
if not bool(torch.isfinite(raw_loss)):
|
|
389
|
+
manager.save(
|
|
390
|
+
model, optimizer, scheduler,
|
|
391
|
+
step=step,
|
|
392
|
+
samples_consumed=samples_consumed,
|
|
393
|
+
tokens_seen=tokens_seen,
|
|
394
|
+
last_loss=last_loss,
|
|
395
|
+
reason="non-finite-loss",
|
|
396
|
+
)
|
|
397
|
+
raise FloatingPointError(f"Non-finite pretraining loss at step {step}: {raw_loss.item()}")
|
|
398
|
+
loss.backward()
|
|
399
|
+
accumulated_loss += float(raw_loss.item())
|
|
400
|
+
samples_consumed += batch_size
|
|
401
|
+
tokens_seen += int(labels.numel())
|
|
402
|
+
gradient_norm = float(torch.nn.utils.clip_grad_norm_(model.parameters(), gradient_clip).item())
|
|
403
|
+
if not math.isfinite(gradient_norm):
|
|
404
|
+
manager.save(
|
|
405
|
+
model, optimizer, scheduler,
|
|
406
|
+
step=step,
|
|
407
|
+
samples_consumed=samples_consumed - batch_size * accumulation_steps,
|
|
408
|
+
tokens_seen=tokens_seen - batch_size * sequence_length * accumulation_steps,
|
|
409
|
+
last_loss=last_loss,
|
|
410
|
+
reason="non-finite-gradient",
|
|
411
|
+
)
|
|
412
|
+
raise FloatingPointError(f"Non-finite gradient norm at step {step}: {gradient_norm}")
|
|
44
413
|
optimizer.step()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
414
|
+
scheduler.step()
|
|
415
|
+
completed_step = step + 1
|
|
416
|
+
last_loss = accumulated_loss / accumulation_steps
|
|
417
|
+
now = time.monotonic()
|
|
418
|
+
|
|
419
|
+
if completed_step % log_steps == 0 or completed_step == steps:
|
|
420
|
+
interval = max(now - last_log_time, 1e-6)
|
|
421
|
+
record = {
|
|
422
|
+
"event": "progress",
|
|
423
|
+
"step": completed_step,
|
|
424
|
+
"total_steps": steps,
|
|
425
|
+
"loss": last_loss,
|
|
426
|
+
"learning_rate": float(scheduler.get_last_lr()[0]),
|
|
427
|
+
"gradient_norm": gradient_norm,
|
|
428
|
+
"tokens_seen": tokens_seen,
|
|
429
|
+
"tokens_per_second": (tokens_seen - last_log_tokens) / interval,
|
|
430
|
+
"elapsed_seconds": now - started,
|
|
431
|
+
"cuda_allocated_bytes": int(torch.cuda.memory_allocated(device)),
|
|
432
|
+
"cuda_reserved_bytes": int(torch.cuda.memory_reserved(device)),
|
|
433
|
+
}
|
|
434
|
+
append_jsonl(metrics_path, record)
|
|
435
|
+
print(json.dumps(record, separators=(",", ":")), flush=True)
|
|
436
|
+
last_log_time = now
|
|
437
|
+
last_log_tokens = tokens_seen
|
|
438
|
+
|
|
439
|
+
checkpoint_due = (
|
|
440
|
+
completed_step % checkpoint_steps == 0
|
|
441
|
+
or now - last_checkpoint_time >= checkpoint_seconds
|
|
442
|
+
or completed_step == steps
|
|
443
|
+
or stop.signal_number is not None
|
|
444
|
+
)
|
|
445
|
+
if checkpoint_due:
|
|
446
|
+
reason = "signal" if stop.signal_number is not None else ("complete" if completed_step == steps else "periodic")
|
|
447
|
+
checkpoint = manager.save(
|
|
448
|
+
model, optimizer, scheduler,
|
|
449
|
+
step=completed_step,
|
|
450
|
+
samples_consumed=samples_consumed,
|
|
451
|
+
tokens_seen=tokens_seen,
|
|
452
|
+
last_loss=last_loss,
|
|
453
|
+
reason=reason,
|
|
454
|
+
)
|
|
455
|
+
print(json.dumps({"event": "checkpoint", "step": completed_step, "path": str(checkpoint), "reason": reason}), flush=True)
|
|
456
|
+
last_checkpoint_time = now
|
|
457
|
+
if stop.signal_number is not None:
|
|
458
|
+
raise SystemExit(128 + stop.signal_number)
|
|
48
459
|
|
|
49
|
-
output_dir = Path(config["output_dir"])
|
|
50
460
|
save_model(model, output_dir)
|
|
51
461
|
write_json(output_dir / "metrics.json", {
|
|
52
462
|
"ok": True,
|
|
53
463
|
"steps": steps,
|
|
54
464
|
"loss": last_loss,
|
|
55
|
-
"bpb": last_bpb,
|
|
56
465
|
"parameters": param_count(model),
|
|
57
466
|
"depth": depth,
|
|
58
467
|
"width": model_config["width"],
|
|
59
468
|
"heads": model_config["heads"],
|
|
60
|
-
"
|
|
469
|
+
"tokens_seen": tokens_seen,
|
|
470
|
+
"training_tokens": int(token_metadata["tokens"]),
|
|
471
|
+
"token_cache_sha256": token_metadata["tokens_sha256"],
|
|
472
|
+
"bf16": bf16,
|
|
61
473
|
"device": str(device),
|
|
62
474
|
})
|
|
63
475
|
|
|
@@ -7,7 +7,7 @@ from tokenizers import Tokenizer
|
|
|
7
7
|
from torch.nn import functional as F
|
|
8
8
|
|
|
9
9
|
from common import load_config, require_cuda, write_json
|
|
10
|
-
from data import END,
|
|
10
|
+
from data import END, encode_generation_prompt, example_pairs, numeric_reward
|
|
11
11
|
from model import FoundationGPT, load_model, param_count, save_model
|
|
12
12
|
|
|
13
13
|
|
|
@@ -73,10 +73,18 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
73
73
|
last_loss = 0.0
|
|
74
74
|
for step in range(steps):
|
|
75
75
|
example = examples[step % len(examples)]
|
|
76
|
-
|
|
76
|
+
context_length = int(model.config["sequence_length"])
|
|
77
|
+
prompt_ids, completion_tokens = encode_generation_prompt(
|
|
78
|
+
tokenizer,
|
|
79
|
+
str(config.get("system_prompt") or ""),
|
|
80
|
+
example["input"],
|
|
81
|
+
context_length,
|
|
82
|
+
24,
|
|
83
|
+
)
|
|
77
84
|
prompt_ids, completion_tokens = bounded_rollout(
|
|
78
85
|
prompt_ids,
|
|
79
|
-
|
|
86
|
+
context_length,
|
|
87
|
+
completion_tokens,
|
|
80
88
|
)
|
|
81
89
|
prompt = torch.tensor([prompt_ids], dtype=torch.long, device=device)
|
|
82
90
|
sampled = sample_rollout(
|