brsx 0.1.0__py3-none-any.whl

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.
brsx/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """brsx — AutoForge
2
+
3
+ Usage:
4
+ from brsx import automodel
5
+ automodel.run()
6
+ """
7
+
8
+ __version__ = "0.1.0"
brsx/automodel.py ADDED
@@ -0,0 +1,95 @@
1
+ """
2
+ automodel.py — AutoForge single entry point
3
+
4
+ from brsx import automodel
5
+ automodel.run()
6
+
7
+ Asks 1/2/3/4 and runs the main file inside the matching folder.
8
+ Works both as an installed package and when run directly.
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ import importlib
14
+
15
+ HERE = os.path.dirname(os.path.abspath(__file__))
16
+
17
+ # key → (label, description, folder, module file (no .py), entry function)
18
+ TASKS = {
19
+ "1": ("Transformer training",
20
+ "Train a standard Transformer from scratch",
21
+ "train_transformer", "full", "main"),
22
+ "2": ("MTP Transformer training",
23
+ "Train with Multi-Token Prediction heads",
24
+ "mtp", "brsx_mtp", "train_mtp"),
25
+ "3": ("HuggingFace model fine-tune",
26
+ "Fine-tune a HuggingFace model (local or hub)",
27
+ "ft_hf_models", "fine_tune", "main"),
28
+ "4": ("brsx (.pt) model fine-tune",
29
+ "Fine-tune an existing brsx .pt model",
30
+ "ft_brsx_model", "brsx_finetune", "main"),
31
+ }
32
+
33
+
34
+ def _run_task(folder, module_name, func_name):
35
+ """Add the task folder to sys.path, import the module, call its entry func.
36
+
37
+ Each folder is added to sys.path so the module's own imports
38
+ (import brsx_calibrate, from brsx_train import GPT, ...) keep working
39
+ exactly as they do when the file is run directly inside its folder.
40
+ """
41
+ folder_path = os.path.join(HERE, folder)
42
+ if not os.path.isdir(folder_path):
43
+ print(f" ! Folder not found: {folder_path}")
44
+ return
45
+
46
+ if folder_path not in sys.path:
47
+ sys.path.insert(0, folder_path)
48
+
49
+ try:
50
+ module = importlib.import_module(module_name)
51
+ except Exception as e:
52
+ print(f" ! Couldn't load {module_name}.py from {folder}/: {e}")
53
+ return
54
+
55
+ entry = getattr(module, func_name, None)
56
+ if not callable(entry):
57
+ print(f" ! {module_name}.py has no callable {func_name}().")
58
+ return
59
+
60
+ entry()
61
+
62
+
63
+ def _print_menu():
64
+ print("=" * 60)
65
+ print(" brsx — AutoModel")
66
+ print("=" * 60)
67
+ print(" What would you like to do?")
68
+ print("-" * 60)
69
+ for key, (label, desc, *_ ) in TASKS.items():
70
+ print(f" {key}) {label}")
71
+ print(f" {desc}")
72
+ print(" q) Quit")
73
+ print("-" * 60)
74
+
75
+
76
+ def run():
77
+ _print_menu()
78
+ valid = set(TASKS) | {"q", "quit", "exit"}
79
+ while True:
80
+ choice = input(" Choice: ").strip().lower()
81
+ if choice in valid:
82
+ break
83
+ print(f" ! Pick one of: {', '.join(TASKS)} or q")
84
+
85
+ if choice in ("q", "quit", "exit"):
86
+ print(" See you!")
87
+ return
88
+
89
+ label, _desc, folder, module_name, func_name = TASKS[choice]
90
+ print(f"\n → {label}\n")
91
+ _run_task(folder, module_name, func_name)
92
+
93
+
94
+ if __name__ == "__main__":
95
+ run()
@@ -0,0 +1,307 @@
1
+ """
2
+ ==============================================================
3
+ brsx_finetune.py - Fine-Tune for brsx's own models
4
+ ==============================================================
5
+ NOT HF. Fine-tunes your own GPT model (model.pt + config.json + tokenizer.json).
6
+
7
+ - Model architecture is IDENTICAL to the GPT in brsx_train.py (MultiheadAttention).
8
+ - Injects LoRA manually; only into the MLP Linear layers (not into the
9
+ attention out_proj - PyTorch looks it up internally via .weight, so
10
+ wrapping it breaks things). MLP LoRA is enough for fine-tuning.
11
+ - 'full' mode trains all weights.
12
+ - Interactive: everything is asked at runtime, no flags.
13
+ - If no data is found, downloads it from brsx-labs/datasets.
14
+
15
+ Must be in the same folder: brsx_train.py (for GPT + CFG)
16
+ Optional: brsx_tokenizer.py (falls back to byte-level if missing)
17
+
18
+ Run: python brsx_finetune.py
19
+ ==============================================================
20
+ """
21
+
22
+ import os, sys, json, time, math
23
+ import torch
24
+ import torch.nn as nn
25
+ import torch.nn.functional as F
26
+
27
+ try:
28
+ from brsx_train import GPT, CFG
29
+ except ImportError:
30
+ print("[ERROR] brsx_train.py must be in the same folder (for GPT, CFG).")
31
+ sys.exit(1)
32
+
33
+ HF_REPO = "brsx-labs/datasets"
34
+ DOMAINS = {
35
+ "1": ("Code (code.txt)", "code.txt"),
36
+ "2": ("General (general.txt)", "general.txt"),
37
+ "3": ("Math (math.txt)", "math.txt"),
38
+ "4": ("Chat (chat.txt)", "chat.txt"),
39
+ }
40
+
41
+
42
+ # --------------------------------------------------------------------------
43
+ # Input helpers
44
+ # --------------------------------------------------------------------------
45
+ def ask(msg, default=None, cast=str, hint=None):
46
+ if hint: print(f" ({hint})")
47
+ sfx = f" [{default}]" if default is not None else ""
48
+ while True:
49
+ raw = input(f" > {msg}{sfx}: ").strip()
50
+ if not raw and default is not None: return default
51
+ if not raw: print(" Enter a value."); continue
52
+ try: return cast(raw)
53
+ except ValueError: print(" Invalid, try again.")
54
+
55
+ def ask_choice(title, options, hint=None):
56
+ print(f"\n{title}")
57
+ if hint: print(f" ({hint})")
58
+ for k, v in options.items(): print(f" {k}) {v}")
59
+ while True:
60
+ c = input(" > Choice: ").strip()
61
+ if c in options: return c
62
+ print(" Pick from the list.")
63
+
64
+
65
+ # --------------------------------------------------------------------------
66
+ # LoRA
67
+ # --------------------------------------------------------------------------
68
+ class LoRALinear(nn.Module):
69
+ """Wraps an nn.Linear. Original weight is frozen; only A,B are trained."""
70
+ def __init__(self, base: nn.Linear, r=8, alpha=16, dropout=0.0):
71
+ super().__init__()
72
+ self.base = base
73
+ for p in self.base.parameters():
74
+ p.requires_grad = False
75
+ self.r = r
76
+ self.scale = alpha / r
77
+ self.drop = nn.Dropout(dropout)
78
+ self.A = nn.Linear(base.in_features, r, bias=False)
79
+ self.B = nn.Linear(r, base.out_features, bias=False)
80
+ nn.init.kaiming_uniform_(self.A.weight, a=math.sqrt(5))
81
+ nn.init.zeros_(self.B.weight)
82
+
83
+ def forward(self, x):
84
+ return self.base(x) + self.scale * self.B(self.A(self.drop(x)))
85
+
86
+
87
+ def inject_lora(model, r=8, alpha=16, dropout=0.05):
88
+ """
89
+ Injects LoRA ONLY into the MLP Linear layers.
90
+ Not into the MultiheadAttention out_proj (PyTorch looks for .weight -> breaks).
91
+ """
92
+ n = 0
93
+ for blk in model.blocks:
94
+ for i, layer in enumerate(blk.mlp):
95
+ if isinstance(layer, nn.Linear):
96
+ blk.mlp[i] = LoRALinear(layer, r, alpha, dropout)
97
+ n += 1
98
+ print(f" LoRA injected: {n} layers (MLP)")
99
+ if n == 0:
100
+ print(" [!] No LoRA layers could be injected - blk.mlp structure may differ.")
101
+ print(" To inspect layers: for n,_ in model.named_modules(): print(n)")
102
+ return model
103
+
104
+
105
+ # --------------------------------------------------------------------------
106
+ # Load model + tokenizer
107
+ # --------------------------------------------------------------------------
108
+ def load_brsx(model_dir, device):
109
+ cfgp = os.path.join(model_dir, "config.json")
110
+ mp = os.path.join(model_dir, "model.pt")
111
+ if not os.path.exists(cfgp) or not os.path.exists(mp):
112
+ print(f"[ERROR] {model_dir} must contain config.json and model.pt.")
113
+ sys.exit(1)
114
+
115
+ with open(cfgp, encoding="utf-8") as f:
116
+ conf = json.load(f)
117
+
118
+ cfg = CFG()
119
+ cfg.n_layer = conf["n_layer"]; cfg.n_head = conf["n_head"]
120
+ cfg.n_embd = conf["n_embd"]; cfg.block_size = conf["block_size"]
121
+ cfg.vocab_size = conf["vocab_size"]; cfg.device = device
122
+
123
+ model = GPT(cfg).to(device)
124
+ model.load_state_dict(torch.load(mp, map_location=device))
125
+ try:
126
+ pc = model.param_count()/1e6
127
+ except Exception:
128
+ pc = sum(p.numel() for p in model.parameters())/1e6
129
+ print(f" Model loaded: {pc:.1f}M params "
130
+ f"(n_layer={cfg.n_layer}, n_embd={cfg.n_embd}, vocab={cfg.vocab_size})")
131
+
132
+ tok = None
133
+ if conf.get("has_tokenizer"):
134
+ try:
135
+ from brsx_tokenizer import BPETokenizer
136
+ tok = BPETokenizer()
137
+ tok.load(os.path.join(model_dir, "tokenizer.json"))
138
+ print(" Tokenizer loaded (BPE).")
139
+ except Exception as e:
140
+ print(f" [!] Could not load tokenizer ({e}) -> byte-level.")
141
+ return model, cfg, tok
142
+
143
+
144
+ # --------------------------------------------------------------------------
145
+ # Data
146
+ # --------------------------------------------------------------------------
147
+ def get_data_text(tok):
148
+ print("\n--- DATA ---")
149
+ path = input(" > Data .txt path (EMPTY = download a ready-made dataset): ").strip()
150
+ if path and os.path.exists(path):
151
+ print(f" Found: {path}")
152
+ else:
153
+ if path: print(f" '{path}' not found, switching to download.")
154
+ dom = ask_choice("Which dataset?", {k: v[0] for k, v in DOMAINS.items()})
155
+ fname = DOMAINS[dom][1]
156
+ from huggingface_hub import hf_hub_download
157
+ print(f" Downloading: {HF_REPO}/{fname} ...")
158
+ path = hf_hub_download(repo_id=HF_REPO, filename=fname, repo_type="dataset")
159
+ print(f" Downloaded: {path}")
160
+ with open(path, "r", encoding="utf-8") as f:
161
+ return f.read()
162
+
163
+
164
+ def encode_data(text, tok, cfg):
165
+ if tok is not None:
166
+ ids = tok.encode(text)
167
+ else:
168
+ ids = list(text.encode("utf-8"))
169
+ data = torch.tensor(ids, dtype=torch.long)
170
+ if len(data) < cfg.block_size + 2:
171
+ print("[ERROR] Data too short.")
172
+ sys.exit(1)
173
+ return data
174
+
175
+
176
+ # --------------------------------------------------------------------------
177
+ # Training
178
+ # --------------------------------------------------------------------------
179
+ def train_ft(model, cfg, data, mode, steps, lr, batch, out_dir, save_every, tok):
180
+ device = cfg.device
181
+ if mode == "lora":
182
+ params = [p for p in model.parameters() if p.requires_grad]
183
+ else:
184
+ for p in model.parameters(): p.requires_grad = True
185
+ params = list(model.parameters())
186
+ ntr = sum(p.numel() for p in params)
187
+ print(f" Trainable params: {ntr/1e6:.2f}M")
188
+
189
+ opt = torch.optim.AdamW(params, lr=lr)
190
+ use_amp = (device == "cuda")
191
+ scaler = torch.amp.GradScaler('cuda', enabled=use_amp)
192
+
193
+ def get_batch():
194
+ ix = torch.randint(0, len(data) - cfg.block_size - 1, (batch,))
195
+ x = torch.stack([data[i:i+cfg.block_size] for i in ix]).to(device)
196
+ y = torch.stack([data[i+1:i+cfg.block_size+1] for i in ix]).to(device)
197
+ return x, y
198
+
199
+ model.train()
200
+ t0 = time.time()
201
+ for step in range(1, steps+1):
202
+ x, y = get_batch()
203
+ opt.zero_grad(set_to_none=True)
204
+ if use_amp:
205
+ with torch.amp.autocast('cuda'):
206
+ _, loss = model(x, y)
207
+ scaler.scale(loss).backward(); scaler.step(opt); scaler.update()
208
+ else:
209
+ _, loss = model(x, y)
210
+ loss.backward(); opt.step()
211
+
212
+ if step % 20 == 0 or step == 1:
213
+ ips = step / (time.time() - t0)
214
+ v = torch.cuda.memory_allocated()/1e6 if device == "cuda" else 0
215
+ print(f" step {step}/{steps} | loss {loss.item():.4f} | {ips:.1f} it/s"
216
+ + (f" | VRAM {v:.0f}MB" if device=='cuda' else ""))
217
+
218
+ if save_every and step % save_every == 0 and step < steps:
219
+ _save(model, cfg, tok, out_dir)
220
+ print(f" [checkpoint] {out_dir}")
221
+
222
+ _save(model, cfg, tok, out_dir)
223
+ print(f"\n Training finished ({time.time()-t0:.1f}s). Saved -> {out_dir}")
224
+
225
+
226
+ def _save(model, cfg, tok, out_dir):
227
+ os.makedirs(out_dir, exist_ok=True)
228
+ torch.save(model.state_dict(), os.path.join(out_dir, "model.pt"))
229
+ conf = {"n_layer": cfg.n_layer, "n_head": cfg.n_head, "n_embd": cfg.n_embd,
230
+ "block_size": cfg.block_size, "vocab_size": cfg.vocab_size,
231
+ "has_tokenizer": tok is not None, "finetuned": True}
232
+ with open(os.path.join(out_dir, "config.json"), "w", encoding="utf-8") as f:
233
+ json.dump(conf, f, indent=2)
234
+ if tok is not None:
235
+ try:
236
+ tok.save(os.path.join(out_dir, "tokenizer.json"))
237
+ except Exception:
238
+ pass
239
+
240
+
241
+ # --------------------------------------------------------------------------
242
+ # main
243
+ # --------------------------------------------------------------------------
244
+ def main():
245
+ print("=" * 60)
246
+ print(" brsx Fine-Tune (for your own models)")
247
+ print("=" * 60)
248
+ device = "cuda" if torch.cuda.is_available() else "cpu"
249
+ print(f" Device: {device}"
250
+ + (f" ({torch.cuda.get_device_name(0)})" if device=='cuda' else ""))
251
+
252
+ print("\n--- MODEL ---")
253
+ print("Folder of YOUR OWN model to fine-tune")
254
+ print("(folder containing model.pt + config.json + tokenizer.json).")
255
+ model_dir = ask("Model folder", default="brsx_model")
256
+ model_dir = os.path.abspath(os.path.expanduser(model_dir))
257
+
258
+ model, cfg, tok = load_brsx(model_dir, device)
259
+
260
+ mode_key = ask_choice("FT type:", {
261
+ "1": "lora - Only small adapters are trained (fast, low memory, RECOMMENDED)",
262
+ "2": "full - Whole model is trained (stronger but more memory)",
263
+ }, hint="If unsure, pick 1 (lora)")
264
+ mode = {"1": "lora", "2": "full"}[mode_key]
265
+
266
+ if mode == "lora":
267
+ r = ask("LoRA rank r (typically 4-16)", default=8, cast=int,
268
+ hint="Larger r = more capacity, more params")
269
+ model = inject_lora(model, r=r, alpha=2*r, dropout=0.05)
270
+ model.to(device)
271
+
272
+ text = get_data_text(tok)
273
+ data = encode_data(text, tok, cfg)
274
+ print(f" Data: {len(data)} tokens")
275
+
276
+ print("\n--- TRAINING SETTINGS ---")
277
+ lr = ask("Learning rate", default=(2e-4 if mode=="lora" else 5e-5), cast=float,
278
+ hint="2e-4 for LoRA, lower (5e-5) is better for full FT")
279
+ steps = ask("Max steps", default=1000, cast=int)
280
+ batch = ask("batch_size", default=8, cast=int,
281
+ hint="On 6GB try 8-16; lower it if you hit OOM")
282
+
283
+ print("\n--- CHECKPOINT ---")
284
+ if steps >= 1000:
285
+ sug = max(1, steps // 5)
286
+ save_every = ask("Save every N steps (0=off)", default=sug, cast=int)
287
+ else:
288
+ save_every = 0
289
+ print(" Few steps -> checkpointing off.")
290
+
291
+ out_dir = ask("Output folder", default="brsx_model_ft")
292
+ out_dir = os.path.abspath(os.path.expanduser(out_dir))
293
+
294
+ print("\n" + "=" * 60)
295
+ print(f" model={model_dir}")
296
+ print(f" mode={mode} | lr={lr} | steps={steps} | batch={batch}")
297
+ print(f" checkpoint={save_every} | output={out_dir}")
298
+ print("=" * 60)
299
+ if input(" Start? (y/n) [y]: ").strip().lower() not in ("","y","yes","e"):
300
+ print("Cancelled."); return
301
+
302
+ train_ft(model, cfg, data, mode, steps, lr, batch, out_dir, save_every, tok)
303
+ print("\nDone. Use the model.pt in the output folder with your own chat.py.")
304
+
305
+
306
+ if __name__ == "__main__":
307
+ main()