@tuned-tensor/cli 0.13.1-beta.1 → 0.13.1-beta.2
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 +31 -12
- package/dist/{chunk-YJYCGSHJ.js → chunk-2P2RL23O.js} +144 -6
- package/dist/chunk-2P2RL23O.js.map +1 -0
- package/dist/index.js +514 -29
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.js +77 -9
- package/dist/local-runtime.js.map +1 -1
- package/docs/local-runtime/architecture.md +24 -0
- package/examples/local-runtime/foundation-spec.json +31 -0
- package/package.json +6 -3
- package/training/foundation/pyproject.toml +24 -0
- package/training/foundation/src/common.py +41 -0
- package/training/foundation/src/data.py +135 -0
- package/training/foundation/src/evaluate.py +115 -0
- package/training/foundation/src/finetune.py +96 -0
- package/training/foundation/src/model.py +157 -0
- package/training/foundation/src/pretrain.py +66 -0
- package/training/foundation/src/rl.py +121 -0
- package/training/foundation/src/train_tokenizer.py +26 -0
- package/training/foundation/uv.lock +922 -0
- package/dist/chunk-YJYCGSHJ.js.map +0 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
from tokenizers import Tokenizer
|
|
7
|
+
from torch.nn import functional as F
|
|
8
|
+
|
|
9
|
+
from common import load_config, require_cuda, write_json
|
|
10
|
+
from data import corpus_hash, decoded_byte_count, example_pairs, smoke_corpus, window_tokens
|
|
11
|
+
from model import FoundationGPT, bits_per_byte, model_config_from_depth, param_count, save_model
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(argv: list[str] | None = None) -> None:
|
|
15
|
+
config = load_config(argv)
|
|
16
|
+
require_cuda("pretrain")
|
|
17
|
+
device = torch.device("cuda")
|
|
18
|
+
tokenizer = Tokenizer.from_file(str(Path(config["tokenizer_dir"]) / "tokenizer.json"))
|
|
19
|
+
examples = example_pairs(config.get("examples") or [])
|
|
20
|
+
corpus = smoke_corpus(examples, int(config.get("max_chars") or 20_000), extra=str(config.get("system_prompt") or ""))
|
|
21
|
+
token_ids = tokenizer.encode(corpus).ids
|
|
22
|
+
sequence_length = int(config.get("sequence_length") or 64)
|
|
23
|
+
windows = window_tokens(token_ids, sequence_length)
|
|
24
|
+
depth = int(config.get("depth") or 2)
|
|
25
|
+
model_config = model_config_from_depth(depth, tokenizer.get_vocab_size(), sequence_length)
|
|
26
|
+
model = FoundationGPT(model_config).to(device)
|
|
27
|
+
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
|
|
28
|
+
batch_size = max(1, int(config.get("batch_size") or 2))
|
|
29
|
+
steps = max(1, int(config.get("steps") or 2))
|
|
30
|
+
last_loss = 0.0
|
|
31
|
+
last_bpb = 0.0
|
|
32
|
+
model.train()
|
|
33
|
+
for step in range(steps):
|
|
34
|
+
batch = windows[step % len(windows): step % len(windows) + batch_size]
|
|
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))
|
|
42
|
+
optimizer.zero_grad(set_to_none=True)
|
|
43
|
+
loss.backward()
|
|
44
|
+
optimizer.step()
|
|
45
|
+
last_loss = float(loss.item())
|
|
46
|
+
byte_count = decoded_byte_count(tokenizer, labels.detach().cpu().tolist())
|
|
47
|
+
last_bpb = bits_per_byte(loss.detach() * labels.numel(), max(byte_count, 1))
|
|
48
|
+
|
|
49
|
+
output_dir = Path(config["output_dir"])
|
|
50
|
+
save_model(model, output_dir)
|
|
51
|
+
write_json(output_dir / "metrics.json", {
|
|
52
|
+
"ok": True,
|
|
53
|
+
"steps": steps,
|
|
54
|
+
"loss": last_loss,
|
|
55
|
+
"bpb": last_bpb,
|
|
56
|
+
"parameters": param_count(model),
|
|
57
|
+
"depth": depth,
|
|
58
|
+
"width": model_config["width"],
|
|
59
|
+
"heads": model_config["heads"],
|
|
60
|
+
"corpus_sha256": corpus_hash(corpus),
|
|
61
|
+
"device": str(device),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
main()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
from tokenizers import Tokenizer
|
|
7
|
+
from torch.nn import functional as F
|
|
8
|
+
|
|
9
|
+
from common import load_config, require_cuda, write_json
|
|
10
|
+
from data import END, encode_ids, example_pairs, format_prompt, numeric_reward
|
|
11
|
+
from model import FoundationGPT, load_model, param_count, save_model
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def bounded_rollout(
|
|
15
|
+
prompt_ids: list[int],
|
|
16
|
+
context_length: int,
|
|
17
|
+
requested_completion_tokens: int = 24,
|
|
18
|
+
) -> tuple[list[int], int]:
|
|
19
|
+
"""Fit the newest prompt tokens and a useful completion inside one context."""
|
|
20
|
+
if context_length < 2:
|
|
21
|
+
raise ValueError("RL requires a model context of at least two tokens.")
|
|
22
|
+
completion_tokens = min(requested_completion_tokens, context_length // 2)
|
|
23
|
+
prompt_tokens = context_length - completion_tokens
|
|
24
|
+
return (prompt_ids or [0])[-prompt_tokens:], completion_tokens
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def policy_gradient_loss(token_log_probs: torch.Tensor, reward: float) -> torch.Tensor:
|
|
28
|
+
"""Use a fixed binary-reward baseline so failed samples are discouraged."""
|
|
29
|
+
advantage = reward - 0.5
|
|
30
|
+
return -(advantage * token_log_probs.mean())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@torch.no_grad()
|
|
34
|
+
def sample_rollout(
|
|
35
|
+
model: FoundationGPT,
|
|
36
|
+
prompt: torch.Tensor,
|
|
37
|
+
max_new_tokens: int,
|
|
38
|
+
*,
|
|
39
|
+
temperature: float = 1.0,
|
|
40
|
+
stop_token_id: int | None = None,
|
|
41
|
+
generator: torch.Generator | None = None,
|
|
42
|
+
) -> torch.Tensor:
|
|
43
|
+
"""Sample one on-policy completion, bounded by the model context."""
|
|
44
|
+
if temperature <= 0:
|
|
45
|
+
raise ValueError("RL sampling temperature must be positive.")
|
|
46
|
+
model.eval()
|
|
47
|
+
tokens = prompt
|
|
48
|
+
limit = int(model.config["sequence_length"])
|
|
49
|
+
for _ in range(max_new_tokens):
|
|
50
|
+
logits = model(tokens[:, -limit:])[:, -1, :] / temperature
|
|
51
|
+
probabilities = torch.softmax(logits, dim=-1)
|
|
52
|
+
next_token = torch.multinomial(probabilities, num_samples=1, generator=generator)
|
|
53
|
+
tokens = torch.cat([tokens, next_token], dim=1)
|
|
54
|
+
if stop_token_id is not None and bool(torch.all(next_token == stop_token_id)):
|
|
55
|
+
break
|
|
56
|
+
return tokens
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main(argv: list[str] | None = None) -> None:
|
|
60
|
+
config = load_config(argv)
|
|
61
|
+
require_cuda("rl")
|
|
62
|
+
device = torch.device("cuda")
|
|
63
|
+
tokenizer = Tokenizer.from_file(str(Path(config["tokenizer_dir"]) / "tokenizer.json"))
|
|
64
|
+
model = load_model(config["model_dir"], map_location=device)
|
|
65
|
+
examples = example_pairs(config.get("examples") or [])
|
|
66
|
+
if not examples:
|
|
67
|
+
raise SystemExit("Foundation RL needs chat examples with verifiable answers.")
|
|
68
|
+
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
|
|
69
|
+
generator = torch.Generator(device=device)
|
|
70
|
+
generator.manual_seed(int(config.get("seed") or 0))
|
|
71
|
+
steps = max(1, int(config.get("steps") or 1))
|
|
72
|
+
last_reward = 0.0
|
|
73
|
+
last_loss = 0.0
|
|
74
|
+
for step in range(steps):
|
|
75
|
+
example = examples[step % len(examples)]
|
|
76
|
+
prompt_ids = encode_ids(tokenizer, format_prompt(str(config.get("system_prompt") or ""), example["input"]))
|
|
77
|
+
prompt_ids, completion_tokens = bounded_rollout(
|
|
78
|
+
prompt_ids,
|
|
79
|
+
int(model.config["sequence_length"]),
|
|
80
|
+
)
|
|
81
|
+
prompt = torch.tensor([prompt_ids], dtype=torch.long, device=device)
|
|
82
|
+
sampled = sample_rollout(
|
|
83
|
+
model,
|
|
84
|
+
prompt,
|
|
85
|
+
max_new_tokens=completion_tokens,
|
|
86
|
+
stop_token_id=tokenizer.token_to_id(END),
|
|
87
|
+
generator=generator,
|
|
88
|
+
)
|
|
89
|
+
completion_ids = sampled[0, len(prompt_ids):].tolist()
|
|
90
|
+
text = tokenizer.decode(completion_ids).split(END)[0]
|
|
91
|
+
reward = numeric_reward(text, example["output"])
|
|
92
|
+
model.train()
|
|
93
|
+
logits = model(sampled[:, :-1])
|
|
94
|
+
completion = sampled[:, len(prompt_ids):]
|
|
95
|
+
if completion.size(1) == 0:
|
|
96
|
+
last_reward = reward
|
|
97
|
+
continue
|
|
98
|
+
step_logits = logits[:, len(prompt_ids) - 1: len(prompt_ids) - 1 + completion.size(1)]
|
|
99
|
+
log_probs = F.log_softmax(step_logits, dim=-1)
|
|
100
|
+
token_log_probs = log_probs.gather(-1, completion.unsqueeze(-1)).squeeze(-1)
|
|
101
|
+
loss = policy_gradient_loss(token_log_probs, reward)
|
|
102
|
+
optimizer.zero_grad(set_to_none=True)
|
|
103
|
+
loss.backward()
|
|
104
|
+
optimizer.step()
|
|
105
|
+
last_reward = reward
|
|
106
|
+
last_loss = float(loss.item())
|
|
107
|
+
|
|
108
|
+
output_dir = Path(config["output_dir"])
|
|
109
|
+
save_model(model, output_dir)
|
|
110
|
+
write_json(output_dir / "metrics.json", {
|
|
111
|
+
"ok": True,
|
|
112
|
+
"steps": steps,
|
|
113
|
+
"reward": last_reward,
|
|
114
|
+
"loss": last_loss,
|
|
115
|
+
"parameters": param_count(model),
|
|
116
|
+
"device": str(device),
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from common import ensure_private_directory, load_config, make_private_file, write_json
|
|
4
|
+
from data import corpus_hash, example_pairs, smoke_corpus, train_tokenizer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main(argv: list[str] | None = None) -> None:
|
|
8
|
+
config = load_config(argv)
|
|
9
|
+
output_dir = ensure_private_directory(config["output_dir"])
|
|
10
|
+
examples = example_pairs(config.get("examples") or [])
|
|
11
|
+
corpus = smoke_corpus(examples, int(config.get("max_chars") or 20_000), extra=str(config.get("system_prompt") or ""))
|
|
12
|
+
tokenizer = train_tokenizer(corpus, int(config.get("vocab_size") or 256))
|
|
13
|
+
tokenizer_path = output_dir / "tokenizer.json"
|
|
14
|
+
tokenizer.save(str(tokenizer_path))
|
|
15
|
+
make_private_file(tokenizer_path)
|
|
16
|
+
write_json(output_dir / "metrics.json", {
|
|
17
|
+
"ok": True,
|
|
18
|
+
"vocab_size": tokenizer.get_vocab_size(),
|
|
19
|
+
"corpus_chars": len(corpus),
|
|
20
|
+
"corpus_sha256": corpus_hash(corpus),
|
|
21
|
+
"tokenizer_path": str(tokenizer_path),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main()
|