Download src/validate.py from SymbolicLight-AGI/SymbolicLight-PoC: direct link, hf CLI and curl.
- Browser
- Download file 9.59 kB
-
https://huggingface.co/SymbolicLight-AGI/SymbolicLight-PoC/resolve/main/src/validate.py
- Command line
-
hf download hf://SymbolicLight-AGI/SymbolicLight-PoC/src/validate.py
-
curl -L -o validate.py https://huggingface.co/SymbolicLight-AGI/SymbolicLight-PoC/resolve/main/src/validate.py
9.59 kB
| #!/usr/bin/env python3 | |
| """ | |
| SymbolicLight-PoC — Inference Validation | |
| ========================================== | |
| Evaluate the historical checkpoint on the TinyStories validation set. | |
| Metrics: | |
| 1. Validation Loss / Perplexity | |
| 2. Optional text generation demo | |
| Usage: | |
| python validate.py | |
| python validate.py --checkpoint best.pt | |
| python validate.py --generate --prompt "Once upon a time" | |
| """ | |
| import argparse | |
| import math | |
| import time | |
| from pathlib import Path | |
| import torch | |
| import torch.nn.functional as F | |
| from model import SymbolicLightConfig, SymbolicLightModel | |
| DEFAULT_CHECKPOINT = Path(__file__).resolve().parent / "best.pt" | |
| def parse_args(): | |
| p = argparse.ArgumentParser(description="SymbolicLight Validation") | |
| p.add_argument("--checkpoint", type=str, default=str(DEFAULT_CHECKPOINT), | |
| help="Model checkpoint path") | |
| p.add_argument("--max_samples", type=int, default=5000, | |
| help="Maximum number of validation samples (to reduce wait time)") | |
| p.add_argument("--batch_size", type=int, default=16, | |
| help="Validation batch size") | |
| p.add_argument("--seq_len", type=int, default=256, | |
| help="Sequence length") | |
| p.add_argument("--generate", action="store_true", | |
| help="Whether to run text generation demo") | |
| p.add_argument("--prompt", type=str, default="Once upon a time", | |
| help="Prompt for generation") | |
| p.add_argument("--max_new_tokens", type=int, default=200, | |
| help="Maximum number of generated tokens") | |
| p.add_argument("--temperature", type=float, default=0.8, | |
| help="Generation temperature") | |
| p.add_argument("--top_k", type=int, default=50, | |
| help="Top-K sampling") | |
| return p.parse_args() | |
| def load_model(checkpoint_path, device): | |
| """Load model and checkpoint""" | |
| print(f"[Model] Loading checkpoint: {checkpoint_path}") | |
| ckpt = torch.load(checkpoint_path, map_location=device, weights_only=True) | |
| if not isinstance(ckpt, dict): | |
| raise ValueError(f"Checkpoint must be a dict, got {type(ckpt).__name__}") | |
| # Restore configuration from checkpoint | |
| cfg_dict = ckpt.get("config") | |
| if not isinstance(cfg_dict, dict): | |
| raise KeyError("Checkpoint is missing a 'config' dictionary") | |
| config = SymbolicLightConfig(**cfg_dict) | |
| config.enable_entropy_exit = False | |
| print("[Model] Config loaded from checkpoint") | |
| model = SymbolicLightModel(config) | |
| # Load the model-only state dictionary. | |
| if "model" in ckpt: | |
| state_dict = ckpt["model"] | |
| elif "model_state_dict" in ckpt: | |
| state_dict = ckpt["model_state_dict"] | |
| else: | |
| raise KeyError("Checkpoint is missing model weights under 'model' or 'model_state_dict'") | |
| model.load_state_dict(state_dict, strict=True) | |
| model = model.to(device) | |
| model.eval() | |
| # Disable EntropyGate early exit during validation. | |
| # EntropyGate causes exit at layer 0 in eval mode, must be disabled for fair evaluation | |
| for block in model.blocks: | |
| block.entropy_gate.threshold = 0.0 # Do not early exit | |
| print("[Model] Disabled entropy gate early exit for validation") | |
| # Print model information | |
| n_params = sum(p.numel() for p in model.parameters()) | |
| print(f"[Model] Parameters: {n_params:,} ({n_params/1e6:.1f}M)") | |
| return model, config | |
| def load_validation_data(seq_len, max_samples): | |
| """Load TinyStories validation set""" | |
| import tiktoken | |
| from datasets import load_dataset | |
| enc = tiktoken.get_encoding("gpt2") | |
| print(f"[Data] Loading TinyStories (validation) from HuggingFace...") | |
| ds = load_dataset("roneneldan/TinyStories", split="validation") | |
| print(f"[Data] Loaded {len(ds):,} validation stories") | |
| # Tokenize | |
| print(f"[Data] Tokenizing...") | |
| all_tokens = [] | |
| for i, example in enumerate(ds): | |
| text = example.get("text", "") | |
| tokens = enc.encode(text, allowed_special=set()) | |
| all_tokens.extend(tokens) | |
| if len(all_tokens) > max_samples * seq_len * 2: | |
| break # Enough | |
| if (i + 1) % 50000 == 0: | |
| print(f" ... tokenized {i+1:,} stories ({len(all_tokens):,} tokens)") | |
| n_samples = min(max_samples, (len(all_tokens) - 1) // seq_len) | |
| print(f"[Data] Total: {len(all_tokens):,} tokens, {n_samples:,} validation samples") | |
| # Convert to tensor | |
| tokens_tensor = torch.tensor(all_tokens[:n_samples * seq_len + 1], dtype=torch.long) | |
| return tokens_tensor, n_samples, enc | |
| def validate(model, tokens_tensor, n_samples, seq_len, batch_size, device): | |
| """Calculate loss and perplexity on the validation set.""" | |
| model.eval() | |
| total_loss = 0.0 | |
| total_tokens = 0 | |
| n_batches = 0 | |
| print(f"\n{'='*60}") | |
| print(f" VALIDATION ({n_samples:,} samples, batch_size={batch_size})") | |
| print(f"{'='*60}") | |
| start_time = time.time() | |
| for start_idx in range(0, n_samples, batch_size): | |
| end_idx = min(start_idx + batch_size, n_samples) | |
| actual_bs = end_idx - start_idx | |
| # Construct batch | |
| x_list = [] | |
| y_list = [] | |
| for i in range(start_idx, end_idx): | |
| offset = i * seq_len | |
| x_list.append(tokens_tensor[offset:offset + seq_len]) | |
| y_list.append(tokens_tensor[offset + 1:offset + seq_len + 1]) | |
| x = torch.stack(x_list).to(device) | |
| y = torch.stack(y_list).to(device) | |
| # Forward (model.forward only returns logits) | |
| with torch.amp.autocast( | |
| 'cuda', dtype=torch.float16, enabled=device.type == 'cuda' | |
| ): | |
| logits = model(x) | |
| # Loss | |
| loss = F.cross_entropy( | |
| logits.view(-1, logits.size(-1)), | |
| y.view(-1) | |
| ) | |
| total_loss += loss.item() * actual_bs * seq_len | |
| total_tokens += actual_bs * seq_len | |
| n_batches += 1 | |
| if n_batches % 50 == 0: | |
| avg_loss_so_far = total_loss / total_tokens | |
| avg_ppl_so_far = math.exp(min(avg_loss_so_far, 20)) # Prevent overflow | |
| elapsed = time.time() - start_time | |
| print(f" Batch {n_batches:4d} | Loss: {avg_loss_so_far:.4f} | " | |
| f"PPL: {avg_ppl_so_far:7.2f} | " | |
| f"Time: {elapsed:.1f}s") | |
| # Final results | |
| avg_loss = total_loss / total_tokens | |
| avg_ppl = math.exp(avg_loss) | |
| elapsed = time.time() - start_time | |
| print(f"\n{'='*60}") | |
| print(f" VALIDATION RESULTS") | |
| print(f"{'='*60}") | |
| print(f" Validation Loss: {avg_loss:.4f}") | |
| print(f" Validation Perplexity: {avg_ppl:.2f}") | |
| print(f" Total tokens: {total_tokens:,}") | |
| print(f" Time: {elapsed:.1f}s") | |
| print(f" Throughput: {total_tokens/elapsed:,.0f} tok/s") | |
| print(f"{'='*60}\n") | |
| return avg_loss, avg_ppl | |
| def generate_text(model, enc, prompt, max_new_tokens, temperature, top_k, device): | |
| """Autoregressive text generation""" | |
| model.eval() | |
| print(f"\n{'='*60}") | |
| print(f" TEXT GENERATION") | |
| print(f"{'='*60}") | |
| print(f" Prompt: \"{prompt}\"") | |
| print(f" Temperature: {temperature}, Top-K: {top_k}") | |
| print(f" Max new tokens: {max_new_tokens}") | |
| print(f"{'='*60}\n") | |
| # Encode prompt | |
| token_ids = enc.encode(prompt, allowed_special=set()) | |
| tokens = torch.tensor([token_ids], dtype=torch.long, device=device) | |
| generated = list(token_ids) | |
| start_time = time.time() | |
| for i in range(max_new_tokens): | |
| # Truncate to max_seq_len | |
| input_ids = tokens[:, -model.config.max_seq_len:] | |
| with torch.amp.autocast( | |
| 'cuda', dtype=torch.float16, enabled=device.type == 'cuda' | |
| ): | |
| logits = model(input_ids) | |
| # Take logits at the last position | |
| next_logits = logits[:, -1, :] / temperature | |
| # Top-K filtering | |
| if top_k > 0: | |
| values, _ = torch.topk(next_logits, top_k) | |
| min_val = values[:, -1].unsqueeze(-1) | |
| next_logits = torch.where( | |
| next_logits < min_val, | |
| torch.full_like(next_logits, float('-inf')), | |
| next_logits | |
| ) | |
| probs = F.softmax(next_logits, dim=-1) | |
| next_token = torch.multinomial(probs, num_samples=1) | |
| generated.append(next_token.item()) | |
| tokens = torch.cat([tokens, next_token], dim=1) | |
| # Can stop early when encountering eos token | |
| # (TinyStories has no special eos token, stop by length) | |
| elapsed = time.time() - start_time | |
| output_text = enc.decode(generated) | |
| print(f"--- Generated Text ---") | |
| print(output_text) | |
| print(f"--- End ---") | |
| print(f"\n[{max_new_tokens} tokens in {elapsed:.2f}s, " | |
| f"{max_new_tokens/elapsed:.1f} tok/s]") | |
| return output_text | |
| def main(): | |
| args = parse_args() | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| print(f"[Device] {device}") | |
| # Load model | |
| model, config = load_model(args.checkpoint, device) | |
| # Load validation data | |
| tokens_tensor, n_samples, enc = load_validation_data(args.seq_len, args.max_samples) | |
| # Validate | |
| val_loss, val_ppl = validate(model, tokens_tensor, n_samples, | |
| args.seq_len, args.batch_size, device) | |
| # Text generation demo | |
| if args.generate: | |
| generate_text( | |
| model, | |
| enc, | |
| args.prompt, | |
| args.max_new_tokens, | |
| args.temperature, | |
| args.top_k, | |
| device, | |
| ) | |
| if __name__ == "__main__": | |
| main() | |