Voltline's picture
Release VimeML V4.2 epoch20 FP32 and Core ML INT8 (GPL-2.0)
c83c617 verified
Raw History Blame Contribute Delete
8.65 kB
"""Conditional OUT-sum scoring on explicitly allowlisted nonblind snapshots."""
from contextlib import contextmanager
import json
from pathlib import Path
from types import MethodType
import unicodedata
import torch
from torch.nn import functional as F
from vimeml.posttraining.data import ConversionCodec
from vimeml.posttraining.readings import hiragana
from vimeml.training.data import file_sha, write_json
from vimeml.training.model_v4 import TinyGPTV4
ROOT = Path(__file__).resolve().parents[3]
SCORE_CONTRACT = "full_vocabulary_OUT_logp_sum_excluding_EOS_v1"
READING_CONTRACT = "NFKC_then_katakana_to_hiragana_preserve_punctuation_v1"
# Fixed paths and hashes: no directory discovery, arbitrary split or blind mode.
TRACKS = {
"standard-development": (728, "bf8fc3d6d0bf7323ff6d196a13d555f2f6d099bbf946adf434827e450b687a06"),
"legacy-regression": (500, "dc2b9430c5676b95dae5c83156741bac35ca2d4c33ebff520b1394c224668e66"),
"ajimee": (200, "2c18523afc3c23f7341b096f72dd7d84e81a5a2e143d822fbf78512d008fef85"),
"historical-development": (137, "cd356733e6d6b743ad61cf8cc3f28303de27a84a5238a204615319897f71c8b5"),
}
def read_snapshot(track, root=ROOT):
if track not in TRACKS:
raise ValueError("Only explicitly frozen development/regression tracks are allowed")
count, expected = TRACKS[track]
if not expected:
raise ValueError("Unpinned snapshot")
path = Path(root) / f"outputs/evaluation-v4-20261015/inputs/{track}.json"
if file_sha(path) != expected:
raise ValueError(f"Frozen snapshot changed: {track}")
rows = json.loads(path.read_text(encoding="utf-8"))
if len(rows) != count:
raise ValueError("Frozen snapshot count mismatch")
return rows
@contextmanager
def fp32_evaluation(model):
"""Evaluate primitive FP32 operations, avoiding a second compiled graph.
Weights stay FP32 under BF16 AMP training. Never clone a second CUDA model.
"""
training, forward = model.training, model.forward
tf32 = torch.backends.cuda.matmul.allow_tf32
model.eval()
model.forward = MethodType(TinyGPTV4.forward, model)
torch.backends.cuda.matmul.allow_tf32 = False
try:
with torch.inference_mode(), torch.autocast(next(model.parameters()).device.type, enabled=False):
yield
finally:
torch.backends.cuda.matmul.allow_tf32 = tf32
model.forward = forward
model.train(training)
def normalize_reading(query):
return hiragana(unicodedata.normalize("NFKC", query))
def azookey_order(row):
pool = [x["text"] for x in row["candidates"]]
order = row.get("orders", {}).get("azookey", pool)
if not pool or len(set(pool)) != len(pool) or len(order) != len(pool) or set(order) != set(pool):
raise ValueError("Frozen candidate order/pool invalid")
return order
@torch.inference_mode()
def score_candidates(model, codec, context, reading, candidates):
"""Whole-case fallback on overflow; never crop context during evaluation."""
encoded = []
for candidate in candidates:
try:
item = codec.encode(context, reading, candidate)
except ValueError as error:
return {"scores": None, "fallback": "invalid_structured_field", "detail": str(error)}
if item is None or item[2]:
return {"scores": None, "fallback": "structured_context_overflow"}
ids, first, _ = item
encoded.append((ids, first))
if not encoded:
raise ValueError("Empty candidate pool")
device = next(model.parameters()).device
width = max(len(ids) - 1 for ids, _ in encoded)
inputs = torch.zeros((len(encoded), width), dtype=torch.long, device=device)
rows, positions, targets = [], [], []
for row, (ids, first) in enumerate(encoded):
inputs[row, :len(ids) - 1] = torch.tensor(ids[:-1], device=device)
# EOS is a training target; the fixed inference score excludes it.
for position in range(first, len(ids) - 1):
rows.append(row)
positions.append(position - 1)
targets.append(ids[position])
logits = model(inputs)
row_ids = torch.tensor(rows, device=device)
selected = logits[row_ids, torch.tensor(positions, device=device)]
logp = -F.cross_entropy(selected.float(), torch.tensor(targets, device=device), reduction="none")
scores = torch.zeros(len(encoded), device=device).scatter_add_(0, row_ids, logp)
if not bool(torch.isfinite(scores).all()):
raise FloatingPointError("Non-finite conditional candidate score")
return {"scores": scores.cpu().tolist(), "fallback": None}
def evaluate_snapshot(model, codec, rows, destination=None, condition="context_reading"):
if condition not in {"context_reading", "empty_reading", "empty_context"}:
raise ValueError("Unknown diagnostic condition")
counts, by_source, records = {"total": 0, "correct": 0, "azookey_correct": 0, "corrected": 0, "damaged": 0, "fallbacks": 0}, {}, []
for row in rows:
order = azookey_order(row)
# The user's query alone supplies READ. Answers are used only AFTER
# scoring for correctness; never synthesize reading from surface/gold.
reading = normalize_reading(row["query"])
context = row["left_context"]
actual_reading = "" if condition == "empty_reading" else reading
actual_context = "" if condition == "empty_context" else context
result = score_candidates(model, codec, actual_context, actual_reading, order)
ranked = order if result["scores"] is None else [order[i] for i in sorted(
range(len(order)), key=lambda i: (-result["scores"][i], i))]
correct, baseline = ranked[0] in row["answers"], order[0] in row["answers"]
source = row.get("source", "unspecified")
sub = by_source.setdefault(source, {"total": 0, "correct": 0})
sub["total"] += 1
sub["correct"] += int(correct)
counts["total"] += 1
counts["correct"] += int(correct)
counts["azookey_correct"] += int(baseline)
counts["corrected"] += int(correct and not baseline)
counts["damaged"] += int(baseline and not correct)
counts["fallbacks"] += int(result["fallback"] is not None)
records.append({"id": row["id"], "original_query": row["query"], "normalized_reading": reading,
"condition": condition, "context": actual_context, "answers": row["answers"],
"candidate_order": order, "scores": result["scores"], "ranked": ranked,
"correct": correct, "azookey_correct": baseline, "fallback": result["fallback"]})
counts["net_improvement"] = counts["corrected"] - counts["damaged"]
counts["top1"] = counts["correct"] / counts["total"]
report = {**counts, "by_source": by_source, "condition": condition, "precision": "fp32",
"score_contract": SCORE_CONTRACT, "reading_contract": READING_CONTRACT,
"overflow_policy": "whole-case original AzooKey order; full denominator; no context crop"}
if destination is not None:
destination = Path(destination)
destination.mkdir(parents=True, exist_ok=True)
write_json(destination / "metrics.json", report)
with (destination / "scores.jsonl").open("w", encoding="utf-8", newline="\n") as output:
for record in records:
output.write(json.dumps(record, ensure_ascii=False) + "\n")
return report
def load_conditional_checkpoint(path, tokenizer, device="cpu"):
from vimeml.training import model_factory_v4
from vimeml.training.model_factory import model_from_checkpoint
from vimeml.posttraining.runtime import TASK_CONTRACT, validate_task_checkpoint
checkpoint_path = Path(path)
saved = torch.load(checkpoint_path, map_location="cpu", weights_only=True)
validate_task_checkpoint(saved)
if saved["posttraining"]["task_contract"] != TASK_CONTRACT:
raise ValueError("Incompatible conditional scoring contract")
for name, path in (("conversion", ROOT / saved["config"]["conversion_dir"] / "manifest.json"),
("tokens", ROOT / saved["config"]["token_dir"] / "manifest.json")):
if file_sha(path) != saved["signatures"][name]:
raise ValueError(f"Conditional checkpoint {name} identity mismatch")
codec = ConversionCodec(tokenizer)
model = model_from_checkpoint(saved).to(device).eval()
return model, codec, {"step": saved["step"], "stage": saved["posttraining"]["stage"],
"checkpoint_sha256": file_sha(checkpoint_path), "source": saved["initialization"]}