File size: 5,066 Bytes
5997967
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#!/usr/bin/env python3
"""
Frox AI Morph 1.1 — Evaluation Harness

Runs three checks any time you finish a training phase:
  1. Perplexity on WikiText-2 (language modeling quality)
  2. Sanity generations on a fixed prompt set (coherence spot-check)
  3. Throughput benchmark (tokens/sec at a few sequence lengths)

Usage:
    python scripts/evaluate.py --model ./frox-morph-1-1-output/sft_final
    python scripts/evaluate.py --model ./frox-morph-1-1-output/sft_final --skip-generation
"""
from __future__ import annotations

import argparse
import json
import sys
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

import torch

from inference.engine.morph_engine import MorphInferenceEngine
from training.pipeline.trainer import evaluate_perplexity
from utils.common import print_banner, timer


SANITY_PROMPTS = [
    "What is the capital of France?",
    "Write a haiku about the ocean.",
    "Explain what a neural network is in two sentences.",
    "def fibonacci(n):\n    # Complete this function",
    "What's 17 times 23?",
    "Give me three tips for staying focused while studying.",
]


def run_perplexity(engine: MorphInferenceEngine) -> float:
    print("\n📊 Perplexity (WikiText-2)")
    ppl = evaluate_perplexity(
        engine.model.language_model, engine.tokenizer, engine.device,
        max_samples=500, seq_len=512,
        use_amp=engine.device.type == "cuda", amp_dtype=engine.dtype,
    )
    print(f"   Perplexity: {ppl}")
    return ppl


def run_sanity_generations(engine: MorphInferenceEngine) -> list:
    print("\n🧪 Sanity Generations")
    results = []
    for prompt in SANITY_PROMPTS:
        response = engine.generate(
            [{"role": "user", "content": prompt}],
            max_new_tokens=150, temperature=0.7,
        )
        has_content = len(response.strip()) > 5
        has_repetition = _check_repetition(response)
        status = "✓" if has_content and not has_repetition else "⚠"
        print(f"\n  {status} Q: {prompt}")
        print(f"     A: {response[:200]}{'...' if len(response) > 200 else ''}")
        results.append({
            "prompt": prompt, "response": response,
            "has_content": has_content, "has_repetition": has_repetition,
        })
    return results


def _check_repetition(text: str, min_repeat: int = 4) -> bool:
    """Flag degenerate repetition (a common failure mode of undertrained models)."""
    words = text.split()
    if len(words) < min_repeat * 2:
        return False
    for i in range(len(words) - min_repeat):
        window = tuple(words[i:i + min_repeat])
        rest = words[i + min_repeat:i + min_repeat * 2]
        if tuple(rest[:min_repeat]) == window:
            return True
    return False


def run_throughput_benchmark(engine: MorphInferenceEngine) -> dict:
    print("\n⚡ Throughput Benchmark")
    results = {}
    for max_tokens in (50, 200, 500):
        t0 = time.perf_counter()
        _ = engine.generate(
            [{"role": "user", "content": "Tell me a short story about a robot."}],
            max_new_tokens=max_tokens, temperature=0.7,
        )
        elapsed = time.perf_counter() - t0
        tok_s = max_tokens / elapsed
        results[f"{max_tokens}_tokens"] = {"elapsed_s": round(elapsed, 2), "tok_per_s": round(tok_s, 1)}
        print(f"   {max_tokens:>4} tokens: {elapsed:.2f}s ({tok_s:.1f} tok/s)")
    return results


def main():
    parser = argparse.ArgumentParser(description="Evaluate Frox AI Morph 1.1")
    parser.add_argument("--model", type=str, required=True)
    parser.add_argument("--skip-perplexity", action="store_true")
    parser.add_argument("--skip-generation", action="store_true")
    parser.add_argument("--skip-throughput", action="store_true")
    parser.add_argument("--output", type=str, default="./eval_results.json")
    args = parser.parse_args()

    print_banner()
    engine = MorphInferenceEngine.from_pretrained(args.model)

    report = {"model_path": args.model, "stats": engine.stats()}

    if not args.skip_perplexity:
        with timer("Perplexity eval"):
            report["perplexity"] = run_perplexity(engine)

    if not args.skip_generation:
        with timer("Sanity generations"):
            report["sanity_generations"] = run_sanity_generations(engine)
            n_ok = sum(1 for r in report["sanity_generations"]
                      if r["has_content"] and not r["has_repetition"])
            report["sanity_pass_rate"] = f"{n_ok}/{len(SANITY_PROMPTS)}"

    if not args.skip_throughput:
        with timer("Throughput benchmark"):
            report["throughput"] = run_throughput_benchmark(engine)

    Path(args.output).write_text(json.dumps(report, indent=2, default=str))
    print(f"\n✅ Full report saved to {args.output}")

    if "perplexity" in report:
        print(f"\n{'='*50}")
        print(f"SUMMARY: perplexity={report['perplexity']} | "
              f"sanity={report.get('sanity_pass_rate', 'skipped')}")
        print(f"{'='*50}")


if __name__ == "__main__":
    main()