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")" )