qWisp / qwisp /__init__.py
Qarvexium's picture
Upload 6 files
320ca79 verified
Raw
History Blame Contribute Delete
9.03 kB
import argparse
import json
import math
from pathlib import Path
from contextlib import nullcontext
import torch
import torch.nn as nn
import torch.nn.functional as F
import sentencepiece as spm
torch.set_num_threads(12)
torch.set_num_interop_threads(12)
# ---------------------------------------------------------------------------
# Model definition — must match the training script exactly, or the
# checkpoint's state_dict won't line up with the module structure.
# ---------------------------------------------------------------------------
class RMSNorm(nn.Module):
def __init__(self, dim, eps=1e-6):
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.eps = eps
def forward(self, x):
return x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps) * self.weight
def rope_cache(seq_len, head_dim, device, dtype):
inv_freq = 1.0 / (10000 ** (torch.arange(0, head_dim, 2, device=device, dtype=torch.float32) / head_dim))
pos = torch.arange(seq_len, device=device, dtype=torch.float32)
freqs = torch.outer(pos, inv_freq)
cos = freqs.cos().to(dtype=dtype)[None, None, :, :]
sin = freqs.sin().to(dtype=dtype)[None, None, :, :]
return cos, sin
def apply_rope(x, cos, sin):
x1 = x[..., ::2]
x2 = x[..., 1::2]
out = torch.empty_like(x)
out[..., ::2] = x1 * cos - x2 * sin
out[..., 1::2] = x1 * sin + x2 * cos
return out
class CausalSelfAttention(nn.Module):
def __init__(self, dim, n_head, dropout):
super().__init__()
assert dim % n_head == 0
self.n_head = n_head
self.head_dim = dim // n_head
assert self.head_dim % 2 == 0
self.qkv = nn.Linear(dim, 3 * dim)
self.proj = nn.Linear(dim, dim)
self.dropout = dropout
def forward(self, x):
B, T, C = x.shape
q, k, v = self.qkv(x).chunk(3, dim=-1)
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)
cos, sin = rope_cache(T, self.head_dim, x.device, x.dtype)
q = apply_rope(q, cos, sin)
k = apply_rope(k, cos, sin)
if hasattr(F, "scaled_dot_product_attention"):
a = F.scaled_dot_product_attention(q, k, v, dropout_p=0.0, is_causal=True)
else:
att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
mask = torch.triu(torch.ones(T, T, device=x.device, dtype=torch.bool), diagonal=1)
att = att.masked_fill(mask, float("-inf"))
att = F.softmax(att, dim=-1)
a = att @ v
a = a.transpose(1, 2).contiguous().view(B, T, C)
return self.proj(a)
class SwiGLU(nn.Module):
def __init__(self, dim, dropout):
super().__init__()
hidden = 4 * dim
self.fc = nn.Linear(dim, hidden * 2)
self.proj = nn.Linear(hidden, dim)
self.drop = nn.Dropout(dropout)
def forward(self, x):
x1, x2 = self.fc(x).chunk(2, dim=-1)
return self.drop(self.proj(F.silu(x1) * x2))
class Block(nn.Module):
def __init__(self, dim, n_head, dropout):
super().__init__()
self.n1 = RMSNorm(dim)
self.attn = CausalSelfAttention(dim, n_head, dropout)
self.n2 = RMSNorm(dim)
self.mlp = SwiGLU(dim, dropout)
def forward(self, x):
x = x + self.attn(self.n1(x))
x = x + self.mlp(self.n2(x))
return x
class GPT(nn.Module):
def __init__(self, vocab_size, block_size, n_layer, n_head, n_embd, dropout=0.0):
super().__init__()
self.block_size = block_size
self.tok_emb = nn.Embedding(vocab_size, n_embd)
self.drop = nn.Dropout(dropout)
self.blocks = nn.ModuleList([Block(n_embd, n_head, dropout) for _ in range(n_layer)])
self.norm_f = RMSNorm(n_embd)
self.lm_head = nn.Linear(n_embd, vocab_size, bias=False)
self.lm_head.weight = self.tok_emb.weight
def forward(self, idx):
B, T = idx.shape
if T > self.block_size:
idx = idx[:, -self.block_size:]
x = self.tok_emb(idx)
x = self.drop(x)
for block in self.blocks:
x = block(x)
x = self.norm_f(x)
logits = self.lm_head(x)
return logits
def get_stop_ids(sp):
ids = set()
for piece in ("<|user|>", "<|system|>"):
pid = sp.piece_to_id(piece)
if pid != sp.unk_id():
ids.add(pid)
return ids
DEFAULT_CONFIG = {
"vocab_size": 32000,
"block_size": 512,
"n_layer": 10,
"n_head": 8,
"n_embd": 576,
}
def pick_device():
if torch.cuda.is_available():
return torch.device("cuda")
if torch.backends.mps.is_available():
return torch.device("mps")
return torch.device("cpu")
_MODULE_DIR = Path(__file__).resolve().parent
DEFAULT_CKPT = _MODULE_DIR / "qWisp-base-v1.pt"
DEFAULT_TOKENIZER = _MODULE_DIR / "qWisp.model"
class Wisp:
def __init__(self):
self.device = pick_device()
self.model = None
self.tokenizer = None
self.stop_ids = None
self.config = DEFAULT_CONFIG.copy()
def load(
self,
ckpt=str(DEFAULT_CKPT),
tokenizer=str(DEFAULT_TOKENIZER),
device=None,
):
if device is not None:
self.device = torch.device(device)
self.tokenizer = spm.SentencePieceProcessor()
self.tokenizer.load(tokenizer)
self.model = GPT(
vocab_size=self.config["vocab_size"],
block_size=self.config["block_size"],
n_layer=self.config["n_layer"],
n_head=self.config["n_head"],
n_embd=self.config["n_embd"],
dropout=0.0,
)
obj = torch.load(ckpt, map_location=self.device)
state_dict = obj["model"] if isinstance(obj, dict) and "model" in obj else obj
self.model.load_state_dict(state_dict, strict=True)
self.model.to(self.device)
self.model.eval()
self.stop_ids = get_stop_ids(self.tokenizer)
return self
def unload(self):
self.model = None
self.tokenizer = None
self.stop_ids = None
if torch.cuda.is_available():
torch.cuda.empty_cache()
def encode(self, text):
if self.tokenizer is None:
raise RuntimeError("Model not loaded.")
return self.tokenizer.encode(text, out_type=int)
def decode(self, ids):
if self.tokenizer is None:
raise RuntimeError("Model not loaded.")
return self.tokenizer.decode(ids)
@torch.no_grad()
def generate(
self,
prompt,
max_new_tokens=200,
temperature=0.8,
top_k=50,
):
if self.model is None:
raise RuntimeError("Model not loaded.")
ids = self.tokenizer.encode(prompt, out_type=int)
if len(ids) == 0:
ids = [self.tokenizer.bos_id()]
x = torch.tensor([ids], dtype=torch.long, device=self.device)
self.model.eval()
for _ in range(max_new_tokens):
x_cond = x[:, -self.model.block_size :]
logits = self.model(x_cond)
logits = logits[:, -1] / max(temperature, 1e-6)
if top_k is not None and top_k > 0:
values, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits = torch.where(
logits < values[:, [-1]],
torch.full_like(logits, float("-inf")),
logits,
)
probs = F.softmax(logits, dim=-1)
next_token = torch.multinomial(probs, 1)
token = int(next_token.item())
if token == self.tokenizer.eos_id() or token in self.stop_ids:
break
x = torch.cat((x, next_token), dim=1)
return self.tokenizer.decode(x[0].tolist())
def chat(
self,
message,
max_new_tokens=200,
temperature=0.8,
top_k=50,
syspr="Answer to what user have said."
):
prompt = f"<|system|> {syspr}\n<|user|> {message}\n<|assistant|>"
return self.generate(
prompt,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_k=top_k,
)
def __call__(
self,
message,
max_new_tokens=200,
temperature=0.8,
top_k=50,
):
return self.chat(
message,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_k=top_k,
)
def __repr__(self):
loaded = self.model is not None
return (
f"Wisp("
f"loaded={loaded}, "
f"device='{self.device}', "
f"layers={self.config['n_layer']}, "
f"hidden={self.config['n_embd']}, "
f"vocab={self.config['vocab_size']}"
f")"
)