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 +8 -0
- brsx/automodel.py +95 -0
- brsx/ft_brsx_model/brsx_finetune.py +307 -0
- brsx/ft_brsx_model/brsx_train.py +494 -0
- brsx/ft_hf_models/fine_tune.py +372 -0
- brsx/mtp/brsx_calibrate.py +561 -0
- brsx/mtp/brsx_mtp.py +668 -0
- brsx/train_transformer/brsx_calibrate.py +562 -0
- brsx/train_transformer/brsx_chat_template.py +195 -0
- brsx/train_transformer/brsx_data.py +319 -0
- brsx/train_transformer/brsx_mem_probe.py +79 -0
- brsx/train_transformer/brsx_tokenizer.py +223 -0
- brsx/train_transformer/brsx_train.py +494 -0
- brsx/train_transformer/full.py +204 -0
- brsx-0.1.0.dist-info/METADATA +85 -0
- brsx-0.1.0.dist-info/RECORD +19 -0
- brsx-0.1.0.dist-info/WHEEL +5 -0
- brsx-0.1.0.dist-info/licenses/LICENSE +201 -0
- brsx-0.1.0.dist-info/top_level.txt +1 -0
brsx/__init__.py
ADDED
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()
|