Download modeling_sol_pro.py from solintellegence/sol-pro: direct link, hf CLI and curl.
- Browser
- Download file 14.2 kB
-
https://huggingface.co/solintellegence/sol-pro/resolve/main/modeling_sol_pro.py
- Command line
-
hf download hf://solintellegence/sol-pro/modeling_sol_pro.py
-
curl -L -o modeling_sol_pro.py https://huggingface.co/solintellegence/sol-pro/resolve/main/modeling_sol_pro.py
14.2 kB
| """Sol Pro 138M: a width/depth-scaled Sol Flash causal language model.""" | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| import math | |
| import torch | |
| from torch import nn | |
| import torch.nn.functional as F | |
| class SolProConfig: | |
| vocab_size: int = 4096 | |
| width: int = 768 | |
| heads: int = 24 | |
| kv_heads: int = 8 | |
| stored_blocks: int = 12 | |
| ffn_width: int = 3968 | |
| recurrent_start: int = 1 | |
| recurrent_blocks: int = 4 | |
| recurrent_passes: int = 2 | |
| tn_gram_rank: int = 299 | |
| max_ngram_order: int = 4 | |
| use_tn_gram: bool = True | |
| use_qk_norm: bool = True | |
| use_loop_conditioning: bool = True | |
| max_position_embeddings: int = 2048 | |
| rope_theta: float = 20000.0 | |
| def head_dim(self) -> int: | |
| return self.width // self.heads | |
| def effective_blocks(self) -> int: | |
| return self.stored_blocks + self.recurrent_blocks * (self.recurrent_passes - 1) | |
| def validate(self) -> None: | |
| if self.width % self.heads: | |
| raise ValueError("width must be divisible by heads") | |
| if self.heads % self.kv_heads: | |
| raise ValueError("heads must be divisible by kv_heads") | |
| if self.head_dim % 2: | |
| raise ValueError("head_dim must be even for RoPE") | |
| if self.recurrent_start + self.recurrent_blocks > self.stored_blocks: | |
| raise ValueError("recurrent block range exceeds stored blocks") | |
| if self.max_ngram_order != 4: | |
| raise ValueError("Sol Pro TN-gram currently supports orders 2 through 4") | |
| if self.tn_gram_rank < 1: | |
| raise ValueError("tn_gram_rank must be positive") | |
| def model_config() -> SolProConfig: | |
| return SolProConfig() | |
| class RMSNorm(nn.Module): | |
| def __init__(self, width: int, eps: float = 1e-6): | |
| super().__init__() | |
| self.weight = nn.Parameter(torch.ones(width)) | |
| self.eps = eps | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| normalized = x * torch.rsqrt(x.float().square().mean(-1, keepdim=True) + self.eps) | |
| return normalized.to(x.dtype) * self.weight.to(x.dtype) | |
| def apply_rope(x: torch.Tensor, theta: float) -> torch.Tensor: | |
| _, _, length, dim = x.shape | |
| inv = theta ** (-torch.arange(0, dim, 2, device=x.device, dtype=torch.float32) / dim) | |
| angles = torch.arange(length, device=x.device, dtype=torch.float32)[:, None] * inv[None, :] | |
| cos = angles.cos().to(x.dtype)[None, None, :, :] | |
| sin = angles.sin().to(x.dtype)[None, None, :, :] | |
| even, odd = x[..., ::2], x[..., 1::2] | |
| return torch.stack((even * cos - odd * sin, even * sin + odd * cos), dim=-1).flatten(-2) | |
| def _linear_2d(module: nn.Module, hidden: torch.Tensor) -> torch.Tensor: | |
| shape = hidden.shape | |
| projected = module(hidden.reshape(-1, shape[-1])) | |
| return projected.reshape(*shape[:-1], -1) | |
| class TNGramMemory(nn.Module): | |
| """Shared tensorized 2-, 3-, and 4-gram memory with CP-style factors.""" | |
| def __init__(self, cfg: SolProConfig): | |
| super().__init__() | |
| self.vocab_size = cfg.vocab_size | |
| self.rank = cfg.tn_gram_rank | |
| self.width = cfg.width | |
| self.position_factors = nn.ModuleList( | |
| [nn.Embedding(cfg.vocab_size, self.rank) for _ in range(cfg.max_ngram_order)] | |
| ) | |
| # Shared vectors absorb the unused oldest positions for lower orders. | |
| self.order_absorption = nn.Parameter(torch.ones(cfg.max_ngram_order - 2, self.rank)) | |
| self.log_order_scales = nn.Parameter(torch.zeros(cfg.max_ngram_order - 1)) | |
| self.key_projection = nn.Linear(3 * self.rank, cfg.width, bias=False) | |
| self.value_projection = nn.Linear(3 * self.rank, cfg.width, bias=False) | |
| self.query_norm = RMSNorm(cfg.width) | |
| self.key_norm = RMSNorm(cfg.width) | |
| self.value_norm = RMSNorm(cfg.width) | |
| self.local_conv = nn.Conv1d( | |
| cfg.width, | |
| cfg.width, | |
| kernel_size=3, | |
| dilation=cfg.max_ngram_order, | |
| groups=cfg.width, | |
| bias=True, | |
| ) | |
| self.residual_scale = nn.Parameter(torch.tensor(0.1)) | |
| def _lag(ids: torch.Tensor, lag: int) -> torch.Tensor: | |
| if lag == 0: | |
| return ids | |
| return F.pad(ids, (lag, 0), value=0)[:, : ids.shape[1]] | |
| def _rms_normalize(x: torch.Tensor) -> torch.Tensor: | |
| normalized = x * torch.rsqrt(x.float().square().mean(-1, keepdim=True) + 1e-6) | |
| return normalized.to(x.dtype) | |
| def _order_features(self, ids: torch.Tensor) -> torch.Tensor: | |
| # Factors A1..A4 correspond to oldest-to-newest token positions. | |
| by_position = [ | |
| self.position_factors[index](self._lag(ids, 3 - index)) | |
| for index in range(4) | |
| ] | |
| absorption = self.order_absorption.to(by_position[0].dtype) | |
| bigram = by_position[2] * by_position[3] | |
| bigram = bigram * absorption[0] * absorption[1] | |
| trigram = by_position[1] * by_position[2] * by_position[3] | |
| trigram = trigram * absorption[0] | |
| quagram = by_position[0] * by_position[1] * by_position[2] * by_position[3] | |
| orders = [bigram, trigram, quagram] | |
| scales = self.log_order_scales.exp().to(by_position[0].dtype) | |
| normalized = [ | |
| self._rms_normalize(features) * scales[index] | |
| for index, features in enumerate(orders) | |
| ] | |
| return torch.cat(normalized, dim=-1) | |
| def forward(self, ids: torch.Tensor, hidden: torch.Tensor) -> torch.Tensor: | |
| features = self._order_features(ids) | |
| query = self.query_norm(hidden) | |
| key = self.key_norm(_linear_2d(self.key_projection, features)) | |
| value = _linear_2d(self.value_projection, features) | |
| similarity = (query * key).sum(dim=-1, keepdim=True) / math.sqrt(self.width) | |
| gate_logits = similarity.sign() * similarity.abs().clamp_min(1e-6).sqrt() | |
| gated_value = torch.sigmoid(gate_logits).to(value.dtype) * value | |
| conv_input = self.value_norm(gated_value).transpose(1, 2) | |
| causal_padding = (self.local_conv.kernel_size[0] - 1) * self.local_conv.dilation[0] | |
| conv_input = F.pad(conv_input, (causal_padding, 0)) | |
| local_update = F.silu(self.local_conv(conv_input).transpose(1, 2)) | |
| memory = gated_value + local_update | |
| return hidden + self.residual_scale.tanh().to(hidden.dtype) * memory.to(hidden.dtype) | |
| class XSAAttention(nn.Module): | |
| """Sol's causal GQA with per-head Q/K RMSNorm and XSA value subtraction.""" | |
| def __init__(self, cfg: SolProConfig): | |
| super().__init__() | |
| self.cfg = cfg | |
| self.qkv = nn.Linear( | |
| cfg.width, | |
| (cfg.heads + 2 * cfg.kv_heads) * cfg.head_dim, | |
| bias=False, | |
| ) | |
| self.o = nn.Linear(cfg.heads * cfg.head_dim, cfg.width, bias=False) | |
| self.q_norm = RMSNorm(cfg.head_dim) if cfg.use_qk_norm else nn.Identity() | |
| self.k_norm = RMSNorm(cfg.head_dim) if cfg.use_qk_norm else nn.Identity() | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| batch, length, _ = x.shape | |
| q_width = self.cfg.heads * self.cfg.head_dim | |
| kv_width = self.cfg.kv_heads * self.cfg.head_dim | |
| q_flat, k_flat, v_flat = _linear_2d(self.qkv, x).split( | |
| (q_width, kv_width, kv_width), dim=-1 | |
| ) | |
| q = q_flat.view(batch, length, self.cfg.heads, self.cfg.head_dim).transpose(1, 2) | |
| k = k_flat.view(batch, length, self.cfg.kv_heads, self.cfg.head_dim).transpose(1, 2) | |
| v = v_flat.view(batch, length, self.cfg.kv_heads, self.cfg.head_dim).transpose(1, 2) | |
| q = apply_rope(self.q_norm(q), self.cfg.rope_theta) | |
| k = apply_rope(self.k_norm(k), self.cfg.rope_theta) | |
| groups = self.cfg.heads // self.cfg.kv_heads | |
| attended = F.scaled_dot_product_attention( | |
| q, k, v, is_causal=True, enable_gqa=(groups > 1) | |
| ) | |
| if groups > 1: | |
| unit_v = F.normalize(v, p=2, dim=-1, eps=1e-6).unsqueeze(2) | |
| grouped = attended.view( | |
| batch, self.cfg.kv_heads, groups, length, self.cfg.head_dim | |
| ) | |
| attended = ( | |
| grouped - (grouped * unit_v).sum(-1, keepdim=True) * unit_v | |
| ).reshape(batch, self.cfg.heads, length, self.cfg.head_dim) | |
| else: | |
| unit_v = F.normalize(v, p=2, dim=-1, eps=1e-6) | |
| attended = attended - (attended * unit_v).sum(-1, keepdim=True) * unit_v | |
| attended = attended.transpose(1, 2).contiguous().view(batch, length, -1) | |
| return _linear_2d(self.o, attended) | |
| class Block(nn.Module): | |
| def __init__(self, cfg: SolProConfig): | |
| super().__init__() | |
| self.attn_norm = RMSNorm(cfg.width) | |
| self.attn = XSAAttention(cfg) | |
| self.ffn_norm = RMSNorm(cfg.width) | |
| self.gate_up = nn.Linear(cfg.width, 2 * cfg.ffn_width, bias=False) | |
| self.down = nn.Linear(cfg.ffn_width, cfg.width, bias=False) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| x = x + self.attn(self.attn_norm(x)) | |
| normed = self.ffn_norm(x) | |
| gate_up = _linear_2d(self.gate_up, normed) | |
| gate, up = gate_up.chunk(2, dim=-1) | |
| return x + _linear_2d(self.down, F.silu(gate) * up) | |
| class SolProForCausalLM(nn.Module): | |
| """Sol Flash architecture scaled to a 138,037,594-parameter model.""" | |
| def __init__(self, cfg: SolProConfig | None = None): | |
| super().__init__() | |
| self.config = cfg or SolProConfig() | |
| self.config.validate() | |
| cfg = self.config | |
| self.embedding = nn.Embedding(cfg.vocab_size, cfg.width) | |
| self.tn_gram = TNGramMemory(cfg) if cfg.use_tn_gram else None | |
| self.blocks = nn.ModuleList([Block(cfg) for _ in range(cfg.stored_blocks)]) | |
| self.loop_embeddings = nn.Parameter(torch.zeros(cfg.recurrent_passes, cfg.width)) | |
| self.loop_gates = nn.Parameter(torch.zeros(cfg.recurrent_passes, cfg.recurrent_blocks, cfg.width)) | |
| self.norm = RMSNorm(cfg.width) | |
| nn.init.normal_(self.embedding.weight, std=0.02) | |
| if self.tn_gram is not None: | |
| for factor in self.tn_gram.position_factors: | |
| nn.init.normal_(factor.weight, std=1.0) | |
| nn.init.normal_(self.loop_embeddings, std=0.01) | |
| def forward(self, ids: torch.Tensor) -> torch.Tensor: | |
| if ids.shape[1] > self.config.max_position_embeddings: | |
| raise ValueError("input sequence exceeds configured context length") | |
| x = self.embedding(ids) | |
| if torch.is_autocast_enabled("cuda"): | |
| x = x.to(torch.get_autocast_dtype("cuda")) | |
| if self.tn_gram is not None: | |
| x = self.tn_gram(ids, x) | |
| start = self.config.recurrent_start | |
| stop = start + self.config.recurrent_blocks | |
| for block in self.blocks[:start]: | |
| x = block(x) | |
| for pass_index in range(self.config.recurrent_passes): | |
| if self.config.use_loop_conditioning: | |
| x = x + self.loop_embeddings[pass_index].to(x.dtype) | |
| for local_index, block in enumerate(self.blocks[start:stop]): | |
| proposal = block(x) | |
| if self.config.use_loop_conditioning: | |
| gate = torch.sigmoid(self.loop_gates[pass_index, local_index]).to(x.dtype)[None, None, :] | |
| x = x + gate * (proposal - x) | |
| else: | |
| x = proposal | |
| for block in self.blocks[stop:]: | |
| x = block(x) | |
| return F.linear(self.norm(x), self.embedding.weight) | |
| class SolProCausalLoss(nn.Module): | |
| """Return BF16 cross-entropy directly to keep the training graph compact.""" | |
| def __init__(self, model: nn.Module): | |
| super().__init__() | |
| self.model = model | |
| self.vocab_size = model.config.vocab_size | |
| def forward(self, token_batch: torch.Tensor) -> torch.Tensor: | |
| logits = self.model(token_batch[:, :-1]) | |
| targets = token_batch[:, 1:] | |
| return F.cross_entropy( | |
| logits.reshape(-1, self.vocab_size), targets.reshape(-1) | |
| ) | |
| def parameter_count(model: nn.Module) -> int: | |
| return sum(parameter.numel() for parameter in model.parameters()) | |
| def load_model(model_dir, device=None): | |
| """Load the original tokenizer and released Sol Pro inference weights.""" | |
| import json | |
| from pathlib import Path | |
| from safetensors.torch import load_file | |
| from tokenizers import Tokenizer | |
| root = Path(model_dir) | |
| settings = json.loads((root / "config.json").read_text()) | |
| settings = {name: value for name, value in settings.items() if name in SolProConfig.__dataclass_fields__} | |
| device = device or ("cuda" if torch.cuda.is_available() else "cpu") | |
| dtype = torch.bfloat16 if str(device).startswith("cuda") else torch.float32 | |
| model = SolProForCausalLM(SolProConfig(**settings)).to(device=device, dtype=dtype) | |
| model.load_state_dict(load_file(str(root / "model.safetensors"), device="cpu"), strict=True) | |
| model.eval() | |
| tokenizer = Tokenizer.from_file(str(root / "tokenizer.json")) | |
| return model, tokenizer | |
| def generate(model, tokenizer, prompt, max_new_tokens=64, temperature=0.0): | |
| """Return prompt plus continuation, without exceeding the context window.""" | |
| if max_new_tokens < 0: | |
| raise ValueError("max_new_tokens must be nonnegative") | |
| ids = tokenizer.encode(prompt, add_special_tokens=False).ids | |
| eos = tokenizer.token_to_id("<eos>") | |
| if not ids: | |
| if eos is None: | |
| raise ValueError("An empty prompt requires an EOS token") | |
| ids = [eos] | |
| capacity = model.config.max_position_embeddings | |
| if len(ids) + max_new_tokens > capacity: | |
| raise ValueError("Prompt and requested continuation exceed the 2,048-token context") | |
| device = next(model.parameters()).device | |
| for _ in range(max_new_tokens): | |
| scores = model(torch.tensor([ids], device=device, dtype=torch.long))[0, -1].float() | |
| if temperature > 0: | |
| next_id = int(torch.multinomial(torch.softmax(scores / temperature, dim=-1), 1).item()) | |
| else: | |
| next_id = int(scores.argmax().item()) | |
| ids.append(next_id) | |
| if next_id == eos: | |
| break | |
| return tokenizer.decode(ids, skip_special_tokens=True) | |