SymbolicLight-PoC / src /validate.py
symboliclight-ai's picture
Publish historical inference-only PoC
f511efd
Raw History Blame Contribute Delete
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
@torch.no_grad()
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
@torch.no_grad()
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()