Download src/model.py from SymbolicLight-AGI/SymbolicLight-PoC: direct link, hf CLI and curl.
- Browser
- Download file 15.4 kB
-
https://huggingface.co/SymbolicLight-AGI/SymbolicLight-PoC/resolve/main/src/model.py
- Command line
-
hf download hf://SymbolicLight-AGI/SymbolicLight-PoC/src/model.py
-
curl -L -o model.py https://huggingface.co/SymbolicLight-AGI/SymbolicLight-PoC/resolve/main/src/model.py
15.4 kB
| #!/usr/bin/env python3 | |
| """Inference-only model definition for the historical SymbolicLight PoC.""" | |
| import math | |
| from dataclasses import dataclass | |
| from typing import Tuple | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| # ============================================================================ | |
| # Configuration | |
| # ============================================================================ | |
| class SymbolicLightConfig: | |
| """Configuration required to construct the historical PoC model.""" | |
| vocab_size: int = 32000 # Vocabulary size (reusing Qwen/Llama tokenizer) | |
| embed_dim: int = 768 # Embedding dimension | |
| n_layers: int = 12 # Number of SymbolicLightBlock layers | |
| n_heads: int = 12 # Number of channels in SparseTCAM | |
| head_dim: int = 64 # Dimension per channel (embed_dim / n_heads) | |
| intermediate_dim: int = 2048 # Feedforward intermediate dimension | |
| max_seq_len: int = 2048 # Maximum sequence length | |
| dropout: float = 0.1 | |
| # --- SymbolicLight Specific Parameters --- | |
| spike_threshold: float = 1.0 # LIF neuron firing threshold | |
| leak_factor: float = 0.95 # Membrane potential leak factor | |
| entropy_exit_threshold: float = 0.3 # EntropyGate early exit threshold | |
| enable_entropy_exit: bool = False | |
| # ============================================================================ | |
| # Inference-only spike function | |
| # ============================================================================ | |
| def hard_spike(membrane_potential: torch.Tensor, threshold: float = 1.0) -> torch.Tensor: | |
| """Return the same hard-threshold spikes used by the original forward pass.""" | |
| return (membrane_potential >= threshold).float() | |
| # ============================================================================ | |
| # ② SpikeEncoder | |
| # ============================================================================ | |
| class SpikeEncoder(nn.Module): | |
| """ | |
| Converts discrete token IDs into spatio-temporal spike tensors. | |
| Process: token_id -> Embedding -> LayerNorm -> LIF Spiking | |
| """ | |
| def __init__(self, config: SymbolicLightConfig): | |
| super().__init__() | |
| self.embedding = nn.Embedding(config.vocab_size, config.embed_dim) | |
| self.norm = nn.LayerNorm(config.embed_dim) | |
| self.threshold = config.spike_threshold | |
| self.leak = config.leak_factor | |
| # Learnable positional embedding. | |
| self.pos_embedding = nn.Embedding(config.max_seq_len, config.embed_dim) | |
| # Membrane potential (managed dynamically per batch) | |
| self.register_buffer("v_mem", None) | |
| def _init_membrane(self, shape: torch.Size, device: torch.device): | |
| """Initialize/reset membrane potential""" | |
| self.v_mem = torch.zeros(shape, device=device) | |
| def forward(self, token_ids: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: | |
| """ | |
| Args: | |
| token_ids: [batch, seq_len] | |
| Returns: | |
| spikes: [batch, seq_len, embed_dim] Sparse 0/1 spikes | |
| continuous: [batch, seq_len, embed_dim] Continuous representation (used for residuals) | |
| """ | |
| B, S = token_ids.shape | |
| positions = torch.arange(S, device=token_ids.device).unsqueeze(0) | |
| # Embedding + Positional encoding | |
| x = self.embedding(token_ids) + self.pos_embedding(positions) | |
| x = self.norm(x) | |
| # LIF spiking: process step-by-step over time (seq dimension) | |
| self._init_membrane((B, x.size(-1)), x.device) | |
| spikes_list = [] | |
| for t in range(S): | |
| # Leak + Integrate | |
| self.v_mem = self.v_mem * self.leak + x[:, t, :] | |
| # Fire a hard-threshold spike. | |
| spike = hard_spike(self.v_mem, self.threshold) | |
| # Reset after firing | |
| self.v_mem = self.v_mem * (1.0 - spike) | |
| spikes_list.append(spike) | |
| spikes = torch.stack(spikes_list, dim=1) # [B, S, D] | |
| return spikes, x # Return spikes and continuous representation (continuous repr used for residual connections) | |
| # ============================================================================ | |
| # ③a SparseTCAM — In-Memory Compute Sparse Routing (Replaces Self-Attention) | |
| # ============================================================================ | |
| class SparseTCAM(nn.Module): | |
| """Historical cumulative-context routing block implemented in PyTorch.""" | |
| def __init__(self, config: SymbolicLightConfig): | |
| super().__init__() | |
| self.n_heads = config.n_heads | |
| self.head_dim = config.head_dim | |
| self.embed_dim = config.embed_dim | |
| self.threshold = config.spike_threshold | |
| self.leak = config.leak_factor | |
| # TCAM weight matrix (In-memory compute: serves as both storage and computation) | |
| self.tcam_proj = nn.Linear(config.embed_dim, config.embed_dim, bias=False) | |
| # Output projection | |
| self.out_proj = nn.Linear(config.embed_dim, config.embed_dim, bias=False) | |
| self.norm = nn.LayerNorm(config.embed_dim) | |
| self.dropout = nn.Dropout(config.dropout) | |
| def forward(self, spikes: torch.Tensor, continuous: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: | |
| """ | |
| Args: | |
| spikes: [B, S, D] Input spikes (sparse 0/1) | |
| continuous: [B, S, D] Continuous representation (for residual connections) | |
| Returns: | |
| out_spikes: [B, S, D] Output spikes | |
| out_continuous: [B, S, D] Updated continuous representation | |
| """ | |
| B, S, D = spikes.shape | |
| # 1. Spike mask routing: only compute at positions \"with spikes\" | |
| # spike_mask indicates which positions have active spikes | |
| spike_energy = spikes.sum(dim=-1) # [B, S] Spike energy at each position | |
| active_mask = (spike_energy > 0).unsqueeze(-1).float() # [B, S, 1] | |
| # 2. Project the spike-masked representation. | |
| tcam_out = self.tcam_proj(spikes * active_mask) | |
| # 3. Multi-channel information fusion (similar to multi-head but based on spike channels) | |
| tcam_out = tcam_out.view(B, S, self.n_heads, self.head_dim) | |
| # 4. Temporal context aggregation (using causal mask to prevent seeing future info) | |
| # Simplified version: use cumulative mean replacing all-to-all attention | |
| causal_cumsum = torch.cumsum(tcam_out, dim=1) | |
| counts = torch.arange(1, S + 1, device=spikes.device).float().view(1, S, 1, 1) | |
| context = causal_cumsum / counts | |
| # 5. Merge channels + Output projection | |
| context = context.view(B, S, D) | |
| output = self.out_proj(self.dropout(context)) | |
| # 6. Residual connection + Layer normalization | |
| out_continuous = self.norm(continuous + output) | |
| # 7. LIF spiking output | |
| out_spikes = hard_spike(out_continuous, self.threshold) | |
| return out_spikes, out_continuous | |
| # ============================================================================ | |
| # ③b EntropyGate — Entropy Gating (On-demand compute depth) | |
| # ============================================================================ | |
| class EntropyGate(nn.Module): | |
| """Compute an optional entropy-based early-exit signal.""" | |
| def __init__(self, config: SymbolicLightConfig): | |
| super().__init__() | |
| self.threshold = config.entropy_exit_threshold | |
| # Retained for checkpoint parameter compatibility. | |
| self.gate = nn.Linear(config.embed_dim, 1) | |
| def forward(self, spikes: torch.Tensor) -> Tuple[torch.Tensor, bool]: | |
| """ | |
| Returns: | |
| entropy: Information entropy of the current spike stream [B] | |
| should_exit: Whether early exit is recommended | |
| """ | |
| # Calculate spike firing rate (how many dimensions are activated per position) | |
| firing_rate = spikes.mean(dim=-1) # [B, S] | |
| # Information entropy: H = -p*log(p) - (1-p)*log(1-p) | |
| p = firing_rate.clamp(1e-7, 1 - 1e-7) | |
| entropy = -(p * p.log() + (1 - p) * (1 - p).log()).mean() | |
| # Low entropy -> Can exit | |
| should_exit = entropy.item() < self.threshold | |
| return entropy, should_exit | |
| # ============================================================================ | |
| # ③c FeedForward — Feed-Forward Network (with spike sparsity) | |
| # ============================================================================ | |
| class SpikingFeedForward(nn.Module): | |
| """ | |
| Replaces the 2-layer MLP of Transformers. | |
| Key difference: Intermediate layer uses LIF spike activation instead of GELU/ReLU. | |
| """ | |
| def __init__(self, config: SymbolicLightConfig): | |
| super().__init__() | |
| self.up = nn.Linear(config.embed_dim, config.intermediate_dim, bias=False) | |
| self.down = nn.Linear(config.intermediate_dim, config.embed_dim, bias=False) | |
| self.norm = nn.LayerNorm(config.embed_dim) | |
| self.threshold = config.spike_threshold | |
| self.dropout = nn.Dropout(config.dropout) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| residual = x | |
| h = self.up(x) | |
| # Use LIF spikes replacing GELU (extremely sparsifying the intermediate layer) | |
| h = hard_spike(h, self.threshold) | |
| h = self.down(self.dropout(h)) | |
| return self.norm(residual + h) | |
| # ============================================================================ | |
| # ③ SymbolicLightBlock — Single Layer Compute Block | |
| # ============================================================================ | |
| class SymbolicLightBlock(nn.Module): | |
| """ | |
| A complete SymbolicLight layer, including: | |
| - SparseTCAM (In-memory compute routing) | |
| - SpikingFeedForward (Spiking feed-forward) | |
| - EntropyGate (Entropy gating) | |
| """ | |
| def __init__(self, config: SymbolicLightConfig): | |
| super().__init__() | |
| self.tcam = SparseTCAM(config) | |
| self.ffn = SpikingFeedForward(config) | |
| self.entropy_gate = EntropyGate(config) | |
| def forward(self, spikes: torch.Tensor, continuous: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, bool]: | |
| # 1. In-memory compute routing | |
| spikes, continuous = self.tcam(spikes, continuous) | |
| # 2. Spiking sequence iteration | |
| continuous = self.ffn(continuous) | |
| # Re-spiking | |
| spikes = hard_spike(continuous, self.tcam.threshold) | |
| # 3. Entropy gating | |
| _, should_exit = self.entropy_gate(spikes) | |
| return spikes, continuous, should_exit | |
| # ============================================================================ | |
| # ④ BayesianHead — Bayesian Output Head (Replaces Softmax) | |
| # ============================================================================ | |
| class BayesianHead(nn.Module): | |
| """Output projection with a learned token-prior bias.""" | |
| def __init__(self, config: SymbolicLightConfig): | |
| super().__init__() | |
| self.output_proj = nn.Linear(config.embed_dim, config.vocab_size, bias=False) | |
| # Prior probability (learnable word frequency bias) | |
| self.log_prior = nn.Parameter(torch.zeros(config.vocab_size)) | |
| def forward(self, continuous: torch.Tensor) -> torch.Tensor: | |
| """ | |
| Args: | |
| continuous: [B, S, D] | |
| Returns: | |
| logits: [B, S, vocab_size] token scores | |
| """ | |
| # Likelihood term: P(context|word) | |
| log_likelihood = self.output_proj(continuous) # [B, S, V] | |
| # Prior term: P(word) | |
| # Bayesian update (log domain addition): log P(word|context) = log P(context|word) + log P(word) | |
| logits = log_likelihood + self.log_prior.unsqueeze(0).unsqueeze(0) | |
| return logits | |
| # ============================================================================ | |
| # Complete Model | |
| # ============================================================================ | |
| class SymbolicLightModel(nn.Module): | |
| """Historical SymbolicLight PoC model for checkpoint loading and inference.""" | |
| def __init__(self, config: SymbolicLightConfig): | |
| super().__init__() | |
| self.config = config | |
| self.spike_encoder = SpikeEncoder(config) | |
| self.blocks = nn.ModuleList([ | |
| SymbolicLightBlock(config) for _ in range(config.n_layers) | |
| ]) | |
| self.output_head = BayesianHead(config) | |
| n_params = sum(p.numel() for p in self.parameters()) | |
| print(f"[SymbolicLight] Model initialization complete | Parameters: {n_params/1e6:.1f}M ({n_params/1e9:.3f}B)") | |
| def forward(self, token_ids: torch.Tensor) -> torch.Tensor: | |
| """ | |
| Forward inference. | |
| Args: | |
| token_ids: [batch, seq_len] | |
| Returns: | |
| logits: [batch, seq_len, vocab_size] | |
| """ | |
| # ② Spike encoding | |
| spikes, continuous = self.spike_encoder(token_ids) | |
| # ③ Layer-by-layer processing | |
| for block in self.blocks: | |
| spikes, continuous, should_exit = block(spikes, continuous) | |
| # The published configuration keeps optional early exit disabled. | |
| if should_exit and self.config.enable_entropy_exit: | |
| break | |
| # ④ Bayesian Output | |
| logits = self.output_head(continuous) | |
| return logits | |
| def generate(self, prompt_ids: torch.Tensor, max_new_tokens: int = 100, | |
| temperature: float = 0.8, top_k: int = 50) -> torch.Tensor: | |
| """ | |
| Autoregressive text generation | |
| Args: | |
| prompt_ids: [1, prompt_len] Prompt token IDs | |
| max_new_tokens: Maximum number of new tokens to generate | |
| temperature: Sampling temperature (higher = more random) | |
| top_k: Sample only from the top k highest probability tokens | |
| Returns: | |
| Generated complete token sequence | |
| """ | |
| self.eval() | |
| generated = prompt_ids.clone() | |
| for _ in range(max_new_tokens): | |
| # Truncate to maximum length | |
| input_ids = generated[:, -self.config.max_seq_len:] | |
| # Forward inference | |
| logits = self.forward(input_ids) | |
| # Take logits only at the last position | |
| next_logits = logits[:, -1, :] / temperature | |
| # Top-K sampling | |
| if top_k > 0: | |
| top_k_vals, _ = torch.topk(next_logits, top_k) | |
| min_top_k = top_k_vals[:, -1].unsqueeze(-1) | |
| next_logits[next_logits < min_top_k] = float('-inf') | |
| # Sample next token | |
| probs = F.softmax(next_logits, dim=-1) | |
| next_token = torch.multinomial(probs, num_samples=1) | |
| # Append to sequence | |
| generated = torch.cat([generated, next_token], dim=1) | |
| # EOS detection (token_id=2 is usually EOS) | |
| if next_token.item() == 2: | |
| break | |
| return generated | |
| def get_sparsity_stats(self) -> dict: | |
| """Return sparsity statistics for a small synthetic input.""" | |
| stats = {} | |
| with torch.no_grad(): | |
| device = self.spike_encoder.embedding.weight.device | |
| dummy = torch.randint(0, min(100, self.config.vocab_size), (1, 32), device=device) | |
| spikes, _ = self.spike_encoder(dummy) | |
| stats['encoder_sparsity'] = 1.0 - spikes.mean().item() | |
| for i, block in enumerate(self.blocks): | |
| spikes, _, _ = block(spikes, spikes) | |
| stats[f'block_{i}_sparsity'] = 1.0 - spikes.mean().item() | |
| return stats | |