@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.
@@ -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()