5x2T / crazy.py
Dantonitowin's picture
Upload crazy.py
d4d5674 verified
Raw
History Blame Contribute Delete
21.5 kB
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import os
import time
import threading
import glob
import re
import json
import numpy as np
import wikipediaapi
# ============================================================
# 5x2T — Word Level Model with Disk Offloaded Optimizer
# Uses numpy for disk saves — much more memory efficient
# ============================================================
# ---------------- CONFIG ----------------
MAX_TRAIN_MIN = 60
BATCH_SIZE = 2
SEQ_LENGTH = 64
EMBED_SIZE = 192
HIDDEN_SIZE = 384
NUM_LAYERS = 1
DROPOUT = 0.2
LEARNING_RATE = 0.01
GRAD_CLIP = 1.0
AUTOSAVE_MIN = 5
MAX_VOCAB = 995600
TEMPERATURE = 0.8
RESPONSE_LENGTH = 40
MAX_LENGTH = 200
UNK_TOKEN = "<UNK>"
PAD_TOKEN = "<PAD>"
BASE_DIR = r"C:\Users\Eclipsed\Downloads\5x2T"
DATASET_DIR = os.path.join(BASE_DIR, "datasets")
MODEL_DIR = os.path.join(BASE_DIR, "5x2T-2")
MODEL_PATH = os.path.join(MODEL_DIR, "model.pth")
VOCAB_PATH = os.path.join(MODEL_DIR, "vocab.json")
OFFLOAD_DIR = os.path.join(MODEL_DIR, "offload")
DATASET_FOLDERS = [
os.path.join(DATASET_DIR, "chat_dataset"),
os.path.join(DATASET_DIR, "python_data"),
os.path.join(DATASET_DIR, "lua_dataset"),
os.path.join(DATASET_DIR, "dic_dataset"),
os.path.join(DATASET_DIR, "Wiki_dataset"),
r"E:\5x2T",
]
DEVICE = torch.device("cpu")
# ---------------- DISK OFFLOAD OPTIMIZER ----------------
class DiskOffloadSGD:
"""
SGD optimizer that stores momentum buffers on disk as numpy files.
Large buffers are processed in chunks to avoid RAM spikes.
"""
CHUNK = 4_000_000 # process 4 million elements at a time
def __init__(self, params, lr=0.01, momentum=0.9, offload_dir=OFFLOAD_DIR):
self.params = list(params)
self.lr = lr
self.momentum = momentum
self.offload_dir = offload_dir
os.makedirs(offload_dir, exist_ok=True)
print(f" Initialising {len(self.params)} momentum buffers on disk...")
for i, p in enumerate(self.params):
path = os.path.join(offload_dir, f"m_{i}.npy")
if not os.path.exists(path):
# Save in chunks to avoid allocating the full array at once
shape = p.data.shape
total = p.data.numel()
flat = np.zeros(total, dtype=np.float32)
np.save(path, flat.reshape(shape))
del flat
print(f" Momentum buffers ready in {offload_dir}\n")
def zero_grad(self):
for p in self.params:
if p.grad is not None:
p.grad.detach_()
p.grad.zero_()
def step(self):
for i, p in enumerate(self.params):
if p.grad is None:
continue
path = os.path.join(self.offload_dir, f"m_{i}.npy")
shape = p.data.shape
total = p.data.numel()
# Use memory mapped file — only the chunk we touch is in RAM
buf_mm = np.load(path, mmap_mode="r+")
buf_flat = buf_mm.reshape(-1)
grad_flat = p.grad.data.reshape(-1).numpy()
data_flat = p.data.reshape(-1).numpy()
# Process in chunks so RAM never spikes
for start in range(0, total, self.CHUNK):
end = min(start + self.CHUNK, total)
buf_flat[start:end] = (self.momentum * buf_flat[start:end]
+ grad_flat[start:end])
data_flat[start:end] -= self.lr * buf_flat[start:end]
# Write updated data back to param tensor
p.data.copy_(torch.from_numpy(data_flat.reshape(shape)))
# Flush mmap and free
buf_mm.flush()
del buf_mm, buf_flat, grad_flat, data_flat
def state_dict(self):
return {"lr": self.lr, "momentum": self.momentum}
def load_state_dict(self, state):
self.lr = state.get("lr", self.lr)
self.momentum = state.get("momentum", self.momentum)
# ---------------- DATASET DISCOVERY ----------------
def find_all_txt_files(folders):
all_files = []
for folder in folders:
if os.path.exists(folder):
found = glob.glob(os.path.join(folder, "**", "*.txt"), recursive=True)
all_files.extend(found)
print(f" [{os.path.basename(folder)}] -> {len(found)} file(s)")
else:
print(f" [SKIP] Not found: {folder}")
if not all_files:
raise FileNotFoundError("No .txt files found. Check your dataset paths.")
print(f"\n Total files: {len(all_files)}\n")
return all_files
# ---------------- TOKENISER ----------------
def tokenise(text):
return re.findall(r"\b\w+\b|[\"'.,!?;:\-\n]", text.lower())
def build_vocab(files, max_vocab=MAX_VOCAB):
print(" Building vocabulary...")
freq = {}
total_tokens = 0
for f in files:
try:
with open(f, "r", encoding="utf-8", errors="ignore") as file:
tokens = tokenise(file.read())
for t in tokens:
freq[t] = freq.get(t, 0) + 1
total_tokens += len(tokens)
except Exception as e:
print(f" [WARNING] Could not read {f}: {e}")
sorted_vocab = sorted(freq.items(), key=lambda x: x[1], reverse=True)
vocab_words = [PAD_TOKEN, UNK_TOKEN] + [w for w, _ in sorted_vocab[:max_vocab - 2]]
word2idx = {w: i for i, w in enumerate(vocab_words)}
idx2word = {i: w for i, w in enumerate(vocab_words)}
print(f" Total tokens : {total_tokens:,}")
print(f" Unique words : {len(freq):,}")
print(f" Vocab size : {len(vocab_words):,}\n")
return vocab_words, word2idx, idx2word
def save_vocab(vocab_words, path):
with open(path, "w", encoding="utf-8") as f:
json.dump(vocab_words, f)
def load_vocab(path):
with open(path, "r", encoding="utf-8") as f:
vocab_words = json.load(f)
word2idx = {w: i for i, w in enumerate(vocab_words)}
idx2word = {i: w for i, w in enumerate(vocab_words)}
return vocab_words, word2idx, idx2word
# ---------------- DATASET ----------------
class WordDataset(Dataset):
def __init__(self, files, word2idx):
self.data = []
unk_idx = word2idx.get(UNK_TOKEN, 1)
for f in files:
try:
with open(f, "r", encoding="utf-8", errors="ignore") as file:
tokens = tokenise(file.read())
self.data += [word2idx.get(t, unk_idx) for t in tokens]
except Exception as e:
print(f" [WARNING] Could not read {f}: {e}")
if not self.data:
raise ValueError("Dataset is empty after tokenisation.")
print(f" Dataset tokens: {len(self.data):,}\n")
def __len__(self):
return len(self.data) - SEQ_LENGTH
def __getitem__(self, idx):
x = torch.tensor(self.data[idx:idx + SEQ_LENGTH], dtype=torch.long)
y = torch.tensor(self.data[idx + 1:idx + SEQ_LENGTH + 1], dtype=torch.long)
return x, y
# ---------------- MODEL ----------------
class Model(nn.Module):
def __init__(self, vocab_size):
super().__init__()
self.embed = nn.Embedding(vocab_size, EMBED_SIZE, padding_idx=0)
self.dropout = nn.Dropout(DROPOUT)
self.lstm = nn.LSTM(
EMBED_SIZE, HIDDEN_SIZE,
num_layers=NUM_LAYERS,
batch_first=True,
dropout=0
)
self.norm = nn.LayerNorm(HIDDEN_SIZE)
self.fc = nn.Linear(HIDDEN_SIZE, vocab_size)
def forward(self, x, hc=None):
x = self.dropout(self.embed(x))
x, hc = self.lstm(x, hc)
x = self.norm(x)
x = self.fc(x)
return x, hc
# ---------------- SETUP ----------------
def setup_dirs():
os.makedirs(MODEL_DIR, exist_ok=True)
os.makedirs(OFFLOAD_DIR, exist_ok=True)
os.makedirs(os.path.join(MODEL_DIR, "questions"), exist_ok=True)
# ---------------- GENERATE ----------------
def generate(model, word2idx, idx2word, seed_text, length=RESPONSE_LENGTH, temperature=TEMPERATURE):
model.eval()
tokens = tokenise(seed_text)
unk_idx = word2idx.get(UNK_TOKEN, 1)
indices = [word2idx.get(t, unk_idx) for t in tokens]
hc = None
with torch.no_grad():
for _ in range(min(length, MAX_LENGTH)):
x = torch.tensor([indices[-SEQ_LENGTH:]], dtype=torch.long)
out, hc = model(x, hc)
logits = out[0, -1] / temperature
probs = torch.softmax(logits, dim=0)
next_idx = torch.multinomial(probs, 1).item()
indices.append(next_idx)
generated = indices[len(tokens):]
words = [idx2word.get(i, UNK_TOKEN) for i in generated]
return " ".join(words)
def format_response(text):
text = re.sub(r' ([.,!?;:])', r'\1', text)
text = re.sub(r'\n ', '\n', text)
if text:
text = text[0].upper() + text[1:]
return text
# ---------------- WIKIPEDIA ----------------
wiki_api = wikipediaapi.Wikipedia(
language='en',
extract_format=wikipediaapi.ExtractFormat.WIKI,
user_agent="5x2T-AI/1.0"
)
def search_wikipedia(query):
try:
search_term = query.lower()
for prefix in ["what is ", "what are ", "who is ", "who was ",
"tell me about ", "explain ", "define ",
"what was ", "how does ", "how do "]:
search_term = search_term.replace(prefix, "")
search_term = search_term.replace("?", "").strip()
page = wiki_api.page(search_term)
if page.exists():
return f"[Wikipedia: {page.title}]\n{page.summary[:600]}"
return None
except Exception as e:
print(f" [WARNING] Wikipedia lookup failed: {e}")
return None
def should_search_wiki(text):
triggers = [
"what is", "what are", "who is", "who was",
"tell me about", "explain", "define", "what was",
"how does", "how do"
]
return any(text.lower().strip().startswith(t) for t in triggers)
# ---------------- TRAINING ----------------
def train():
setup_dirs()
print("=" * 55)
print(" 5x2T — Word Level Training (Disk Offload)")
print(f" Device : {DEVICE}")
print(f" Offload dir : {OFFLOAD_DIR}")
print(f" Target : {MAX_TRAIN_MIN} minutes")
print("=" * 55 + "\n")
print("Scanning dataset folders...")
files = find_all_txt_files(DATASET_FOLDERS)
if os.path.exists(VOCAB_PATH):
print(" Found existing vocab — loading...")
vocab_words, word2idx, idx2word = load_vocab(VOCAB_PATH)
print(f" Vocab size: {len(vocab_words):,}\n")
else:
vocab_words, word2idx, idx2word = build_vocab(files)
save_vocab(vocab_words, VOCAB_PATH)
print(f" Vocab saved to {VOCAB_PATH}\n")
print("Loading dataset...")
dataset = WordDataset(files, word2idx)
loader = DataLoader(
dataset, batch_size=BATCH_SIZE,
shuffle=True, num_workers=0
)
vocab_size = len(vocab_words)
model = Model(vocab_size)
criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = DiskOffloadSGD(
model.parameters(),
lr=LEARNING_RATE,
momentum=0.9,
offload_dir=OFFLOAD_DIR
)
param_count = sum(p.numel() for p in model.parameters())
print(f" Model parameters : {param_count:,}")
print(f" Vocab size : {vocab_size:,}")
print(f" Optimizer : DiskOffloadSGD (numpy on disk)")
print(f" Offload folder : {OFFLOAD_DIR}\n")
if os.path.exists(MODEL_PATH + ".npz"):
load_path = MODEL_PATH + ".npz"
elif os.path.exists(MODEL_PATH):
load_path = MODEL_PATH
else:
load_path = None
if load_path:
try:
if load_path.endswith(".npz"):
raw = np.load(load_path)
checkpoint = {k: torch.from_numpy(raw[k]) for k in raw.files}
else:
checkpoint = torch.load(load_path, map_location="cpu")
model_state = model.state_dict()
loaded = 0
for k in checkpoint.keys():
if k in model_state and checkpoint[k].shape == model_state[k].shape:
model_state[k] = checkpoint[k]
loaded += 1
model.load_state_dict(model_state)
print(f" Resumed from checkpoint ({loaded} layers matched)\n")
except Exception as e:
print(f" Could not load checkpoint: {e} — starting fresh\n")
print("-" * 55)
print(" Training started...\n")
start_time = time.time()
epoch = 0
best_loss = float("inf")
total_tokens = 0
last_autosave = 0
epoch_loss = 0
batches = 0
loss = None
def print_progress():
while True:
elapsed_sec = time.time() - start_time
elapsed_min = elapsed_sec / 60
speed = total_tokens / (elapsed_sec + 1e-5)
avg_loss = epoch_loss / max(batches, 1) if batches > 0 else 0
mins = int(elapsed_sec // 60)
secs = int(elapsed_sec % 60)
current_loss = loss.item() if loss is not None else 0.0
print(
f" Epoch {epoch+1:>3} | "
f"Batch {batches:>5} | "
f"Loss: {current_loss:.4f} | "
f"Avg: {avg_loss:.4f} | "
f"Speed: {speed:.0f} tok/s | "
f"Time: {mins:02d}:{secs:02d}/{MAX_TRAIN_MIN:02d}:00",
end="\r"
)
time.sleep(1)
# Start progress printing thread
progress_thread = threading.Thread(target=print_progress, daemon=True)
progress_thread.start()
while (time.time() - start_time) / 60 < MAX_TRAIN_MIN:
epoch_loss = 0
batches = 0
for x, y in loader:
optimizer.zero_grad()
out, _ = model(x)
out = out.view(-1, vocab_size)
y = y.view(-1)
loss = criterion(out, y)
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), GRAD_CLIP)
optimizer.step()
epoch_loss += loss.item()
batches += 1
total_tokens += x.numel()
elapsed_min = (time.time() - start_time) / 60
mins = int(elapsed_min)
secs = int((elapsed_min - mins) * 60)
if elapsed_min - last_autosave >= AUTOSAVE_MIN:
try:
torch.save(model.state_dict(), MODEL_PATH)
last_autosave = elapsed_min
print(f"\n [Autosave] {mins:02d}:{secs:02d} -> {MODEL_PATH}")
except MemoryError:
try:
print(f"\n [Autosave] RAM full - saving directly to disk...")
tmp_path = MODEL_PATH + ".tmp"
with open(tmp_path, "wb") as f:
state = {k: v.numpy() for k, v in model.state_dict().items()}
np.savez_compressed(f, **state)
os.replace(tmp_path, MODEL_PATH + ".npz")
last_autosave = elapsed_min
print(f"\n [Autosave] {mins:02d}:{secs:02d} -> {MODEL_PATH}.npz")
except OSError:
print(f"\n [5xSc-404] Low storage or memory - autosave skipped")
except Exception as e:
print(f"\n [5xSc-9512] Unknown autosave error: {e}")
except OSError:
print(f"\n [5xSc-404] Low storage or memory - autosave skipped")
except KeyboardInterrupt:
print(f"\n [5xSc-80082] Training stopped early - saving...")
try:
torch.save(model.state_dict(), MODEL_PATH)
except Exception:
state = {k: v.numpy() for k, v in model.state_dict().items()}
np.savez_compressed(MODEL_PATH + ".npz", **state)
print(f" Model saved. Exiting.")
raise
except Exception as e:
err = str(e).lower()
if "corrupt" in err or "invalid" in err:
print(f"\n [5xSc-312] Corruption detected: {e}")
elif "allocat" in err or "memory" in err:
print(f"\n [5xSc-500] Memory allocation failed: {e}")
else:
print(f"\n [5xSc-9512] Unknown error: {e}")
if elapsed_min >= MAX_TRAIN_MIN:
break
print()
epoch += 1
avg_loss = epoch_loss / max(batches, 1)
if avg_loss < best_loss:
best_loss = avg_loss
try:
torch.save(model.state_dict(), MODEL_PATH)
except MemoryError:
print(f" [Save] RAM full — saving directly to disk...")
tmp_path = MODEL_PATH + ".tmp"
with open(tmp_path, "wb") as f:
state = {k: v.numpy() for k, v in model.state_dict().items()}
np.savez_compressed(f, **state)
os.replace(tmp_path, MODEL_PATH + ".npz")
print(f" [Saved] Best loss: {best_loss:.4f}\n")
if (time.time() - start_time) / 60 >= MAX_TRAIN_MIN:
break
print("-" * 55)
print(f" Done! Epochs: {epoch} | Best loss: {best_loss:.4f}")
print(f" Model saved to: {MODEL_PATH}\n")
print(" Sample generation:")
seed = '"what is marxism"\n"'
sample = generate(model, word2idx, idx2word, seed_text=seed, length=40)
print(f" {format_response(sample)}\n")
return model, word2idx, idx2word
# ---------------- CHAT ----------------
def chat(model=None, word2idx=None, idx2word=None):
print("=" * 55)
print(" 5x2T — Chat")
print(" Commands:")
print(" quit — exit")
print(" temp X — temperature e.g. temp 0.7")
print(" length X — response length e.g. length 60")
print(" maxlen X — max length cap e.g. maxlen 300")
print(" wiki X — force Wikipedia lookup e.g. wiki Python")
print("=" * 55 + "\n")
if model is None:
if not os.path.exists(VOCAB_PATH):
print("[ERROR] No vocab found. Run training first.")
return
if not os.path.exists(MODEL_PATH):
print("[ERROR] No model found. Run training first.")
return
vocab_words, word2idx, idx2word = load_vocab(VOCAB_PATH)
model = Model(len(vocab_words))
model.load_state_dict(torch.load(MODEL_PATH, map_location="cpu"))
model.eval()
param_count = sum(p.numel() for p in model.parameters())
print(f" Vocab size : {len(vocab_words):,}")
print(f" Model parameters : {param_count:,}")
print(f" Device : {DEVICE}\n")
temperature = TEMPERATURE
response_length = RESPONSE_LENGTH
max_length = MAX_LENGTH
while True:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() in ("quit", "exit", "q"):
print("Goodbye.")
break
if user_input.lower().startswith("temp "):
try:
temperature = float(user_input.split()[1])
print(f" Temperature -> {temperature}\n")
except:
print(" Usage: temp 0.8\n")
continue
if user_input.lower().startswith("length "):
try:
response_length = int(user_input.split()[1])
print(f" Length -> {response_length}\n")
except:
print(" Usage: length 50\n")
continue
if user_input.lower().startswith("maxlen "):
try:
max_length = int(user_input.split()[1])
print(f" Max length -> {max_length}\n")
except:
print(" Usage: maxlen 300\n")
continue
if user_input.lower().startswith("wiki "):
query = user_input[5:].strip()
result = search_wikipedia(query)
reply = result if result else f"No Wikipedia page found for '{query}'"
print(f"5x2T: {reply}\n")
continue
wiki_result = None
if should_search_wiki(user_input):
wiki_result = search_wikipedia(user_input)
if wiki_result:
print(f"5x2T: {wiki_result}\n")
else:
seed = f'"{user_input.lower()}"\n"'
raw = generate(model, word2idx, idx2word,
seed_text=seed,
length=min(response_length, max_length),
temperature=temperature)
reply = format_response(raw)
print(f"5x2T: {reply}\n")
# ---------------- ENTRY POINT ----------------
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "chat":
chat()
else:
model, word2idx, idx2word = train()
print("\nStarting chat...\n")
chat(model, word2idx, idx2word)