OpenSoftware-World-OSW1 / model_init.py
OpenSoftware-World's picture
The model training and model initialization code has been updated. (CPU/GPU auto-detection has been added.)
8edb682 verified
Raw
History Blame
9.55 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")
cfg = ckpt["config"]
vocab = Vocab("opensoftware_world_osw1_tokenizer.model")
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()