Files
alexsjones--llmfit/scripts/validate_generation_scoring.py
2026-07-13 12:12:21 +08:00

361 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Validate generation-aware quality scoring on the scraped model database.
Loads hf_models.json and applies the same generation parsing logic as the Rust
code to demonstrate that newer model generations are scored appropriately.
"""
import json
import re
import sys
from collections import defaultdict
def parse_generation(architecture: str | None, name: str) -> float | None:
"""Mirror of models::parse_generation in Rust."""
if architecture:
arch = architecture.lower()
# DeepSeek
if arch.startswith("deepseek"):
if "v4" in arch:
return 4.0
elif "v3" in arch:
return 3.0
elif "v2" in arch:
return 2.0
return 1.0
# Qwen
if arch.startswith("qwen"):
suffix = arch[len("qwen"):]
if suffix.startswith("3_5") or suffix.startswith("3.5"):
return 3.5
if suffix.startswith("3_next") or suffix.startswith("3next"):
return 3.8
if suffix.startswith("3"):
return 3.0
if suffix.startswith("2"):
return 2.0
if suffix.startswith("1"):
return 1.0
return 1.0
# Llama
if arch.startswith("llama"):
suffix = arch[len("llama"):]
if suffix.startswith("4"):
return 4.0
# fall through to name
# Gemma
if arch.startswith("gemma"):
suffix = arch[len("gemma"):]
if suffix.startswith("4"):
return 4.0
if suffix.startswith("3"):
return 3.0
if suffix.startswith("2"):
return 2.0
return 1.0
# Phi
if arch.startswith("phi"):
suffix = arch[len("phi"):]
if suffix.startswith("4"):
return 4.0
if suffix.startswith("3") or suffix.startswith("moe"):
return 3.0
if suffix.startswith("2"):
return 2.0
return 1.0
# Mistral/Mixtral
if arch.startswith("mistral") or arch.startswith("mixtral"):
return 1.0
# Cohere
if arch.startswith("cohere"):
suffix = arch[len("cohere"):]
if suffix.startswith("2"):
return 2.0
return 1.0
# Falcon
if arch.startswith("falcon"):
suffix = arch[len("falcon"):]
if suffix.startswith("3"):
return 3.0
return 1.0
# Granite
if arch.startswith("granite"):
suffix = arch[len("granite"):]
if suffix.startswith("4"):
return 4.0
return 1.0
# Fallback: name-based
name_lower = name.lower()
if "qwen3.6" in name_lower or "qwen3_6" in name_lower:
return 3.6
if "qwen3.5" in name_lower or "qwen3_5" in name_lower:
return 3.5
if "qwen3" in name_lower:
return 3.0
if "qwen2.5" in name_lower or "qwen2_5" in name_lower:
return 2.5
if "qwen2" in name_lower:
return 2.0
if "llama-4" in name_lower or "llama4" in name_lower:
return 4.0
if "llama-3.3" in name_lower or "llama3.3" in name_lower:
return 3.3
if "llama-3.2" in name_lower or "llama3.2" in name_lower:
return 3.2
if "llama-3.1" in name_lower or "llama3.1" in name_lower:
return 3.1
if "llama-3" in name_lower or "llama3" in name_lower:
return 3.0
if "llama-2" in name_lower or "llama2" in name_lower:
return 2.0
if "gemma-4" in name_lower or "gemma4" in name_lower:
return 4.0
if "gemma-3" in name_lower or "gemma3" in name_lower:
return 3.0
if "gemma-2" in name_lower or "gemma2" in name_lower:
return 2.0
if "deepseek-v4" in name_lower:
return 4.0
if "deepseek-v3" in name_lower:
return 3.0
if "deepseek-v2" in name_lower:
return 2.0
if "phi-4" in name_lower or "phi4" in name_lower:
return 4.0
if "phi-3" in name_lower or "phi3" in name_lower:
return 3.0
return None
def generation_bonus(architecture: str | None, name: str) -> float:
gen = parse_generation(architecture, name)
if gen is None:
return 0.0
return min((gen - 1.0) * 3.0, 9.0)
def params_b(model: dict) -> float:
"""Extract parameter count in billions."""
if model.get("parameters_raw"):
return model["parameters_raw"] / 1e9
pc = model.get("parameter_count", "")
m = re.search(r"([\d.]+)\s*[Bb]", pc)
if m:
return float(m.group(1))
m = re.search(r"([\d.]+)\s*[Mm]", pc)
if m:
return float(m.group(1)) / 1000
return 0.0
def quality_score_old(model: dict) -> float:
"""Old scoring without generation bonus."""
params = params_b(model)
if params < 1.0:
base = 30.0
elif params < 3.0:
base = 45.0
elif params < 7.0:
base = 60.0
elif params < 10.0:
base = 75.0
elif params < 20.0:
base = 82.0
elif params < 40.0:
base = 89.0
else:
base = 95.0
name_lower = model["name"].lower()
if "qwen" in name_lower:
family_bump = 2.0
elif "deepseek" in name_lower:
family_bump = 3.0
elif "llama" in name_lower:
family_bump = 2.0
elif "mistral" in name_lower or "mixtral" in name_lower:
family_bump = 1.0
elif "gemma" in name_lower:
family_bump = 1.0
else:
family_bump = 0.0
return min(base + family_bump - 5.0, 100.0) # -5 for Q4_K_M penalty
def quality_score_new(model: dict) -> float:
"""New scoring with generation bonus."""
params = params_b(model)
if params < 1.0:
base = 30.0
elif params < 3.0:
base = 45.0
elif params < 7.0:
base = 60.0
elif params < 10.0:
base = 75.0
elif params < 20.0:
base = 82.0
elif params < 40.0:
base = 89.0
else:
base = 95.0
name_lower = model["name"].lower()
if "qwen" in name_lower:
family_bump = 2.0
elif "deepseek" in name_lower:
family_bump = 3.0
elif "llama" in name_lower:
family_bump = 2.0
elif "mistral" in name_lower or "mixtral" in name_lower:
family_bump = 1.0
elif "gemma" in name_lower:
family_bump = 1.0
else:
family_bump = 0.0
gen_bonus = generation_bonus(model.get("architecture"), model["name"])
return min(base + family_bump + gen_bonus - 5.0, 100.0) # -5 for Q4_K_M penalty
def main():
data_path = "llmfit-core/data/hf_models.json"
with open(data_path) as f:
models = json.load(f)
total = len(models)
print(f"Loaded {total} models from {data_path}\n")
# Count models with parseable generation
gen_counts = defaultdict(int)
family_gens = defaultdict(set)
no_gen = 0
has_gen = 0
for m in models:
gen = parse_generation(m.get("architecture"), m["name"])
if gen is not None:
has_gen += 1
gen_counts[gen] += 1
# Extract family
name_lower = m["name"].lower()
for fam in ["qwen", "llama", "deepseek", "gemma", "phi", "mistral", "falcon", "granite"]:
if fam in name_lower:
family_gens[fam].add(gen)
break
else:
no_gen += 1
print(f"Generation coverage: {has_gen}/{total} ({100*has_gen/total:.1f}%)")
print(f"No generation info: {no_gen}/{total} ({100*no_gen/total:.1f}%)\n")
print("Generation distribution:")
for gen in sorted(gen_counts.keys()):
print(f" Gen {gen:>4.1f}: {gen_counts[gen]:>4} models")
print(f"\nFamilies with multiple generations ({len([f for f in family_gens if len(family_gens[f]) > 1])}):")
for fam in sorted(family_gens.keys()):
gens = sorted(family_gens[fam])
if len(gens) > 1:
print(f" {fam:>10}: gens {', '.join(f'{g:.1f}' for g in gens)}")
# Show specific ranking improvements
print("\n" + "=" * 70)
print("KEY COMPARISONS: Generation-aware scoring fixes")
print("=" * 70)
comparisons = [
("Qwen/Qwen3.6-35B-A3B", "Qwen/Qwen2.5-72B-Instruct"),
("Qwen/Qwen3-8B", "Qwen/Qwen2.5-7B-Instruct"),
("google/gemma-4-E2B-it", "google/gemma-2-2b-it"),
("google/gemma-3-27b-it", "google/gemma-2-27b-it"),
]
model_lookup = {m["name"]: m for m in models}
for newer_name, older_name in comparisons:
newer = model_lookup.get(newer_name)
older = model_lookup.get(older_name)
if not newer or not older:
print(f"\n SKIP: {newer_name} or {older_name} not found in database")
continue
old_newer = quality_score_old(newer)
old_older = quality_score_old(older)
new_newer = quality_score_new(newer)
new_older = quality_score_new(older)
newer_gen = parse_generation(newer.get("architecture"), newer["name"])
older_gen = parse_generation(older.get("architecture"), older["name"])
print(f"\n {newer_name} (gen {newer_gen}, {params_b(newer):.1f}B)")
print(f" vs {older_name} (gen {older_gen}, {params_b(older):.1f}B)")
print(f" OLD: {old_newer:.1f} vs {old_older:.1f} (gap: {old_older - old_newer:+.1f})")
print(f" NEW: {new_newer:.1f} vs {new_older:.1f} (gap: {new_older - new_newer:+.1f})")
if old_older - old_newer > new_older - new_newer:
print(f" ✓ Gap narrowed by {(old_older - old_newer) - (new_older - new_newer):.1f} points")
if new_newer > new_older:
print(f" ✓ RANKING FIXED: newer model now scores higher")
# Show top models by new score in each family
print("\n" + "=" * 70)
print("TOP 5 BY QUALITY SCORE (new vs old) — selected families")
print("=" * 70)
for family in ["qwen", "llama", "gemma", "deepseek"]:
family_models = [m for m in models if family in m["name"].lower()]
# Sort by new score, take top 5
family_models.sort(key=lambda m: quality_score_new(m), reverse=True)
print(f"\n {family.upper()} (top 5):")
for m in family_models[:5]:
gen = parse_generation(m.get("architecture"), m["name"])
old = quality_score_old(m)
new = quality_score_new(m)
print(f" {m['name'][:50]:<50} gen={gen} old={old:5.1f} new={new:5.1f} Δ={new-old:+.1f}")
# Summary stats
print("\n" + "=" * 70)
print("SUMMARY")
print("=" * 70)
score_changes = []
for m in models:
old = quality_score_old(m)
new = quality_score_new(m)
if new != old:
score_changes.append((m["name"], old, new, new - old))
print(f"\n Models with score changes: {len(score_changes)}/{total} ({100*len(score_changes)/total:.1f}%)")
print(f" Models unchanged: {total - len(score_changes)}/{total}")
if score_changes:
deltas = [x[3] for x in score_changes]
print(f" Average score increase: {sum(deltas)/len(deltas):+.2f}")
print(f" Max score increase: {max(deltas):+.1f}")
print(f" Min score increase: {min(deltas):+.1f}")
if __name__ == "__main__":
main()