OpenSoftware-World-OSW1 / model_init.py
OpenSoftware-World's picture
Now, when you specify a separate folder for the model, the โ€œopensoftware_world_osw1_tokenizer.modelโ€ file will also be searched for in that folder.
019c697 verified
Raw
History Blame Contribute Delete
9.62 kB
import os
import re
import sys
import glob
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
import sentencepiece as spm
from config.model_config import *
NUM_THREADS = os.cpu_count() or 4
torch.set_num_threads(NUM_THREADS)
try:
torch.set_num_interop_threads(max(1, NUM_THREADS // 2))
except RuntimeError:
pass
# --- Automatic device selection: use a compatible GPU if available, otherwise fall back to CPU ---
if torch.cuda.is_available():
DEVICE = torch.device("cuda")
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
DEVICE = torch.device("mps")
else:
DEVICE = torch.device("cpu")
print(f"๐Ÿงต Number of CPU threads : {NUM_THREADS}")
print(f"๐Ÿ–ฅ๏ธ Selected device : {DEVICE.type.upper()}"
+ (f" ({torch.cuda.get_device_name(0)})" if DEVICE.type == "cuda" else ""))
class Vocab:
PAD = "<pad>"
UNK = "<unk>"
BOS = "<bos>"
EOS = "<eos>"
def __init__(self, model_path="opensoftware_world_osw1_tokenizer.model"):
self.sp = spm.SentencePieceProcessor()
self.sp.load(model_path)
self.pad_id = self.sp.pad_id()
self.unk_id = self.sp.unk_id()
self.bos_id = self.sp.bos_id()
self.eos_id = self.sp.eos_id()
def encode(self, text, add_bos=False, add_eos=False):
ids = self.sp.encode(text, out_type=int)
if add_bos:
ids = [self.bos_id] + ids
if add_eos:
ids = ids + [self.eos_id]
return ids
def decode(self, ids):
ids = [
i for i in ids
if i not in (self.pad_id, self.bos_id)
]
if self.eos_id in ids:
ids = ids[:ids.index(self.eos_id)]
return self.sp.decode(ids)
def __len__(self):
return self.sp.get_piece_size()
@property
def stoi(self):
return {
self.PAD: self.pad_id,
self.UNK: self.unk_id,
self.BOS: self.bos_id,
self.EOS: self.eos_id,
}
@property
def itos(self):
return [
self.sp.id_to_piece(i)
for i in range(self.sp.get_piece_size())
]
class CausalSelfAttention(nn.Module):
def __init__(self, d_model, n_head, dropout):
super().__init__()
assert d_model % n_head == 0, "d_model must be evenly divisible by n_head"
self.n_head = n_head
self.head_dim = d_model // n_head
self.qkv = nn.Linear(d_model, 3 * d_model)
self.proj = nn.Linear(d_model, d_model)
self.attn_drop = nn.Dropout(dropout)
self.resid_drop = nn.Dropout(dropout)
def forward(self, x, attn_mask):
B, T, C = x.shape
qkv = self.qkv(x)
q, k, v = qkv.split(C, dim=2)
q = q.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
k = k.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
v = v.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
att = att.masked_fill(attn_mask, float("-inf"))
att = F.softmax(att, dim=-1)
att = self.attn_drop(att)
out = att @ v
out = out.transpose(1, 2).contiguous().view(B, T, C)
return self.resid_drop(self.proj(out))
class TransformerBlock(nn.Module):
def __init__(self, d_model, n_head, d_ff, dropout):
super().__init__()
self.ln1 = nn.LayerNorm(d_model)
self.attn = CausalSelfAttention(d_model, n_head, dropout)
self.ln2 = nn.LayerNorm(d_model)
self.mlp = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Linear(d_ff, d_model),
nn.Dropout(dropout),
)
def forward(self, x, attn_mask):
x = x + self.attn(self.ln1(x), attn_mask)
x = x + self.mlp(self.ln2(x))
return x
class OSW1Model(nn.Module):
def __init__(self, vocab_size, cfg: dict, pad_id: int):
super().__init__()
self.cfg = cfg
self.pad_id = pad_id
self.block_size = cfg["block_size"]
self.tok_emb = nn.Embedding(vocab_size, cfg["d_model"])
self.pos_emb = nn.Embedding(cfg["block_size"], cfg["d_model"])
self.drop = nn.Dropout(cfg["dropout"])
self.blocks = nn.ModuleList([
TransformerBlock(cfg["d_model"], cfg["n_head"], cfg["d_ff"], cfg["dropout"])
for _ in range(cfg["n_layer"])
])
self.ln_f = nn.LayerNorm(cfg["d_model"])
self.head = nn.Linear(cfg["d_model"], vocab_size, bias=False)
self.head.weight = self.tok_emb.weight # weight tying
def forward(self, idx):
B, T = idx.shape
pos = torch.arange(T, device=idx.device).unsqueeze(0)
x = self.drop(self.tok_emb(idx) + self.pos_emb(pos))
mask = torch.triu(torch.ones(T, T, dtype=torch.bool, device=idx.device), diagonal=1)
for block in self.blocks:
x = block(x, mask)
x = self.ln_f(x)
return self.head(x)
@torch.no_grad()
def generate(self, idx, max_new_tokens, temperature=0.85, top_k=40, eos_id=None):
self.eval()
# Make sure the input tensor lives on the same device as the model itself,
# so generation works no matter which device the checkpoint was trained on.
model_device = next(self.parameters()).device
idx = idx.to(model_device)
for _ in range(max_new_tokens):
idx_cond = idx[:, -self.block_size:]
logits = self(idx_cond)
logits = logits[:, -1, :] / max(temperature, 1e-5)
if top_k is not None:
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < v[:, [-1]]] = float("-inf")
probs = F.softmax(logits, dim=-1)
next_id = torch.multinomial(probs, num_samples=1)
idx = torch.cat([idx, next_id], dim=1)
if eos_id is not None and next_id.item() == eos_id:
break
return idx
def find_checkpoint():
candidates = glob.glob("opensoftware_world_osw1_*.pth")
if not candidates:
return None
candidates.sort(key=os.path.getmtime, reverse=True)
return candidates[0]
def load_checkpoint(path: str):
print(f"๐Ÿ“ฆ Loading: {path}")
# map_location="cpu" guarantees the checkpoint can always be read back,
# regardless of which device (GPU/MPS/CPU) it was trained on.
ckpt = torch.load(path, map_location="cpu")
tokenizer_path = os.path.join(os.path.dirname(path), "opensoftware_world_osw1_tokenizer.model")
cfg = ckpt["config"]
vocab = Vocab(tokenizer_path)
pad_id = vocab.sp.pad_id()
model = OSW1Model(len(vocab), cfg, pad_id=pad_id).to(DEVICE)
model.load_state_dict(ckpt["model_state_dict"])
model.eval()
param_count = ckpt.get("param_count", sum(p.numel() for p in model.parameters()))
training_time = ckpt.get("training_time_sec", None)
final_loss = ckpt.get("final_loss", None)
trained_on = ckpt.get("trained_on_device", "unknown")
print("\n" + "=" * 64)
print("๐Ÿง  OpenSoftware-World OSW1 โ€” LOADED MODEL INFORMATION")
print("=" * 64)
print(f" File : {path}")
print(f" Vocab size : {len(vocab):,}")
print(f" Number of parameters : {param_count:,}")
print(f" d_model / n_layer : {cfg['d_model']} / {cfg['n_layer']}")
print(f" n_head / d_ff : {cfg['n_head']} / {cfg['d_ff']}")
print(f" Context window : {cfg['block_size']}")
if training_time is not None:
print(f" Training time : {training_time/60:.2f} minutes")
if final_loss is not None:
print(f" Final training loss : {final_loss:.4f}")
print(f" Trained on device : {trained_on} -> Running on: {DEVICE.type}")
print("=" * 64 + "\n")
return model, vocab, cfg
def chat_loop(model: OSW1Model, vocab: Vocab):
print("=" * 64)
print("๐Ÿ’ฌ OpenSoftware-World-OSW1 ready! You can start chatting. Type 'exit' to quit.")
print("=" * 64)
model_device = next(model.parameters()).device
eos_id = vocab.stoi[Vocab.EOS]
bos_id = vocab.stoi[Vocab.BOS]
while True:
try:
user_in = input("\nYou: ").strip()
except (EOFError, KeyboardInterrupt):
print("\n๐Ÿ‘‹ Goodbye!")
break
if user_in.lower() in ("exit", "quit"):
print("๐Ÿ‘‹ Goodbye!")
break
if not user_in:
continue
ids = [bos_id] + vocab.encode(user_in)
x = torch.tensor([ids], dtype=torch.long, device=model_device)
out = model.generate(x, max_new_tokens=init_max_new_tokens, temperature=init_temperature, top_k=init_top_k, eos_id=eos_id)
answer_ids = out[0, len(ids):].tolist()
answer = vocab.decode(answer_ids)
print(f"OpenSoftware-World-OSW1: {answer if answer else '(...silence...)'}")
def main():
if len(sys.argv) > 1:
ckpt_path = sys.argv[1]
if not os.path.isfile(ckpt_path):
print(f"โŒ File not found: {ckpt_path}")
sys.exit(1)
else:
ckpt_path = find_checkpoint()
if ckpt_path is None:
print(
"โŒ No checkpoint files found in the directory.\n"
" Please train a model using 'python3 model_training.py' or\n"
" specify a checkpoint file using 'python3 model_init.py <file_path>'."
)
sys.exit(1)
model, vocab, cfg = load_checkpoint(ckpt_path)
chat_loop(model, vocab)
if __name__ == "__main__":
main()